Twenty-five years ago today, Borland launched Delphi and the world of software development was changed forever. I was very fortunate to have been involved in the months leading up to the launch by writing articles about Delphi for my Blazing Pascal column which was published in PC Techniques magazine. Below is my first Delphi article published in the Oct/Nov 94 issue. The information is just as relevant today as it was 25 years ago.
Happy 25th Delphi! Thank you to all of the people at Borland who delivered such a great product! #Delphi25th #Delphi25
PC Techniques Magazine
Oct/Nov 1994
Blazing Pascal
Introducing Delphi 95
This is certainly an exciting time to be a Pascal developer. For the next several issues, I am going to be focusing on Borland’s new visual development tool, code-named “Delphi 95.” At the writing (August 1994) the product has not been formally announced, and neither its actual name nor pricing information has been revealed.
Starting off Delphi coverage in this column will be a two-part series on the new language extensions that have been added to the dialect of Pascal that underlies Delphi. Please note that Delphi is not the next version of Borland Pascal. Instead, Delphi is a completely new visual development environment for Windows that uses an extended Pascal (extended significantly beyond Borland Pascal 7.0, in fact) as its supporting language. The product will be targeted at client-server database-oriented development, but in fact can be used to develop nearly any kind of software.
Also, please note that I have been working with field test releases of the product, and the factual content of this description will probably change some before Borland actually ships the final version, but as Pascal developers I suspect you won’t mind some minor changes in the cause of getting an early look at the product!
If you are already familiar with Microsoft’s Visual Basic, you will immediately see many similarities between the two products. Both focus on drag-and-drop programming. Users create event-driven applications by dropping controls, or (in the preferred Delphi jargon) components, onto forms, by setting properties, and by writing code to respond to events. Although all visual development environments have their similarities, Delphi stands out in several areas.
First, Delphi’s component architecture is completely object-oriented. The suite of components provided in Delphi make up the Visual Component Library (VCL). What makes the VCL so powerful is that it’s extensible. Being object-oriented, components are easily subclassed through inheritance and polymorphism. Visual Basic custom controls (VBXs), on the other hand, while completely supported in Delphi, are much more difficult to subclass because they are not object-oriented.
Another area in which Delphi shines is in what it produces. Delphi generates small, tight executable files without the need for any run-time engine. This compilation happens faster than that of any other Pascal compiler on the market (and certainly faster than any C compiler), making Delphi a powerful tool for rapid application development.
Borland has also adopted a notebook style interface in Delphi. Figure 1 shows how this metaphor significantly reduces the amount of screen space taken up by tool palettes, code windows, etc. The tool palette in particular takes up considerably less space using notebook tabs than it would if it were a floating toolbox. Also note the tool-tip (in yellow) displayed in Figure 1 as the mouse is dragged over the group box component. Other features include an object browser and a built-in GUI debugger.
Extending Pascal
Borland has introduced many exciting enhancements to their dialect of the Pascal language, now called Object Pascal, that will have even the most avid C++ programmers taking a second look (and perhaps drooling in envy?). In this installment of Blazing Pascal, I will be investigating how Borland has introduced exception handling into the Pascal language. Over the past several months, all of the major C++ compiler vendors have been touting their exception handling capabilities. And, as a result, if you wanted to use exception handling, C++ was your only choice.
Those days are over. With the addition of exception handling and a new object model, Delphi rivals any C++ implementation on the market. But I’m getting ahead of myself. I will cover the new object model in the next issue. For now let’s get back to exceptions. Handling exceptions, as I hope to show in this article, provides programmers with an elegant way of dealing with error conditions.
What is an Exception?
An exception is an indication that an error has occurred somewhere in your application. In Delphi, an exception is actually an object that contains information about what error occurred and where it happened. Exceptions can be created, or raised, under two circumstances: The run-time library (RTL) will raise an exception when a run-time error occurs, or your application itself can explicitly raise an exception.
It is important to realize that once an exception is raised, it stays raised until it is handled or until the application terminates. Therefore, when an exception is raised, an application must respond to the exception by either executing some termination code, handling the exception, or both.
So why use exceptions? Exceptions make it easier to write safe applications. Specifically, a safe application handles errors in a consistent manner, allows for the possibility of recovering from an error, and exits gracefully if the application must terminate.
To further explain the advantages of exceptions, consider the following scenario: You’ve been given the task of writing an application that processes records in a file and updates a remote database with the information stored in the records. Think of it as an import process. In order to create a safe application, there are many tests that must be performed during the course of the process. For example, the input file must be tested for existence and the database connection must be verified. Furthermore, the information in the file records may need to be modified before it can go into the database. If calculations are performed, those results must be tested for validity. Any of these conditions may cause your application to behave unexpectedly, unless you prepare for them.
Preparing for these conditions usually requires cluttering your code with tests that check for errors. The problem with this is that all of these tests can take away from the normal flow of your algorithm—not to mention wasting time checking perfectly valid data. Exception handling allows you to separate the algorithm from the error checking. Actually, you don’t really test for an error, you let the error occur. When it does, an exception is raised. At this point, your application can then respond to the exception in whatever way it sees fit.
Recall that an application can respond to an exception in any of the following ways: by executing some termination code, handling the exception, or both. However, to use exceptions effectively, you must specify which program statements you want to protect. Delphi provides two new constructs that specify which statements get protected and which exceptions are handled. A try…finally block is used for protecting resource allocations and executing terminating code. A try…except block is used to create exception handlers. Because both constructs are implemented as Pascal blocks, they can be nested making it possible to combine their effects.
Protecting Resources
One of the most common problems developers experience when creating a Windows application is forgetting to free GDI resources. Problems of this nature are very difficult to locate. However, evidence of their existence is usually detected after the program is executed four or five times. This is especially true if you forget to release a device context. Anyone who has experienced this can tell you that Windows behaves very strangely when all of its device contexts are used up.
Although the VCL does a very good job of protecting the programmer from having to worry about device contexts, and other GDI objects, through the use of a form’s Canvas property, programmers will still need to make sure other resources are handled correctly. In fact, there are two other types of resources that must be equally protected: files and memory.
Delphi uses try…finally blocks to protect resource allocations. Resource allocations are protected by allowing the application to release the resource in the event of an error. For example, consider the TMainWnd.NoProtBtnClick method in Listing 1, the MainForm unit. This method gets executed when the user presses the “No Protection” button on the main form. Although the example is contrived, it demonstrates what happens when a resource is not protected. That is, the StrToInt function will generate an EConvertError exception because NumStr contains ‘23d’ which is not a valid integer. Unfortunately, since none of these statements are protected, the call to FreeMem never gets executed, and that 1K buffer is lost.
Now consider the TMainWnd.MinProtBtnClick method also in Listing 1. When the user presses the “Minimal Protection” button, this method performs the same tasks as the first example, but this time a try…finally block is used to specify some termination code, and thus protect the resource allocation. Specifically, statements that have the potential of generating an exception should be placed in the try part of the block. Likewise, code that must be executed, in this case to free the memory, is placed in the finally part of the block. Therefore, the call to FreeMem will be executed regardless of whether the StrToInt statement is successful or not.
The key to this construct is that the finally part of the block always gets executed, even if an exception was raised. More precisely, if an exception is raised during the execution of the try part, the program immediately jumps to the finally part of the block. Therefore, it is possible that some statements in the try part will not be executed as a result of the exception.
Since the termination code specified in the finally part of a block always gets executed, this construct is perfectly suited for protecting resource allocations. However, to actually recover from the exception in a graceful way, we need to create exception handlers.
Exception Handlers
Exceptions are not fatal to execution. This is one of the major advantages of using exceptions. By handling an exception, you are given the opportunity to recover from an error condition and continue running, or terminate gracefully. Having this opportunity leads to more robust applications.
So how does one handle an exception? By creating an exception handler, of course. As I mentioned earlier, the new try…except construct provides the means of creating exception handlers. Program statements that could generate exceptions are placed in the try part of the block, just as with try…finally blocks. Within the except part of the block, exception handlers are created for exception classes. Recall that exceptions are objects. Therefore, each exception must belong to a certain class. You create exception handlers by writing code to handle a particular class of exceptions.
Let’s take a look at an example. Consider the TMainWnd.AverageBtnClick method in Listing 1. When the user presses the “Average =” button, the text from the SumEdt and NumItemsEdt edit fields are converted to integers, and the values are used in calculating an average. There are two possible exceptions that could result from these steps. First, an EConvertError exception will be raised if either one of the text strings does not contain a valid integer value. Second, an EZeroDivide exception will be raised if the user enters a zero in the NumItemsEdt field.
Since there are two possible exceptions, I create two exception handlers in the except part of the block. Each handler has the following format:
on <ExceptionClass> do
begin
{ Code to Handle the Exception };
end;
In the EZeroDivide handler, a Boolean flag is set to indicate that the average is undefined when the number of items is zero. This flag is used later in the method to determine what to display in the AverageEdt field. The EConvertError handler starts out by displaying a message box to the user, indicating an invalid integer was entered. At this point, since the exception has been handled, the program could continue executing the code following the try…except block. Note, however, that if an EConvertError exception is raised, the average is not calculated. That is because once an exception is raised in the try part of the block, the program immediately jumps to the except part to search for an exception handler. Therefore, I make a call to Exit, and thus give the user another chance to enter valid numbers.
Exceptions in the RTL
As the previous examples have demonstrated, Delphi’s run-time library (RTL) is now exception-aware. By default the RTL does not raise an exception when an error occurs. This is to maintain compatibility with existing code. However, when the Excepts unit is added to your program’s uses statement, or to any unit used by your program, the RTL becomes exception-aware. Figure 2 shows the Delphi exception class hierarchy. Not all of the exception classes displayed are RTL exceptions. Many come from within the Visual Component Library.
There are five main classes of RTL exceptions: EInOutError, EHeapError, EIntError, EMathError, and EInvalidCast. EInOutError exceptions are raised when an I/O problem occurs, such as not finding a file. Memory errors cause EHeapError exceptions. And EIntError and EMathError are for integer and floating point mathematical errors, respectively. The EInvalidCast exception class is used for indicating problems using the new is and as operators when typecasting a variable. (I will be covering these two new operators, as well as several other enhancements to the Pascal language in the next issue.)
The Scope of Exception Handlers
It is important to realize that you do not have to provide handlers for every kind of exception within every try…except block. Exceptions are very similar to Windows messages. Just as a window does not have to respond to every message that it receives, an exception block does not have to handle every exception. If a window does not process a message, the message is sent to the window’s owner. However, the concept of ownership is not relevant when dealing with exceptions. Instead, the notion of the Pascal block is appropriate. Therefore, exceptions have a scope associated with them which allows for localized exception handling. Consider the following example.
Suppose your application generates an EInvalidPointer exception. If the current try…except block does not provide a handler for the EInvalidPointer exception, the application will look to the block that contains the current block for the appropriate handler. If it doesn’t find one there, it continues with each parent block until it reaches the outermost block. At this point the application’s default exception handler takes over.
Re-raising an Exception
In the same way a virtual method in a class can be used to override or augment an ancestor method, so to can exception handlers override or augment the handling performed by its ancestor blocks. In the previous examples, and under normal circumstances, the handlers simply override any previously defined handlers. Augmenting an exception handler simply means that you want to provide some localized handling of the exception, but still want previously defined handlers to handle the exception. This process of augmenting an exception handler is accomplished by re-raising the exception.
To re-raise an exception, simply handle the exception as normal, and at the end of the handler code, simply call the standard procedure raise as shown in the following code fragment:
try
{ Statements to Protect }
except
on ESomeException do
begin
{ Local Handling of Exception }
raise; { Re-raise the Exception }
end;
end;
Since the local exception handler has already handled the exception, calling raise forces the exception to move up the ancestor block chain looking for the next handler.
Creating New Exceptions and Exception Classes
There are two steps that must be performed in order to utilize a new exception. First the new exception must be defined. Since exceptions are objects, you must first define the class from which the new exception object will be created. Under most circumstances, you will define a new exception class as a descendent of the EMessage class (see Figure 2). The EMessage class allows you to specify a string to be associated with the exception. Since EMessage defines all of the necessary methods, the type definition for a new exception class is quite simple. For example, a new exception class is defined in the MainForm unit.
type
EMouseClickError = class( EMessage );
After a new exception class is defined, you can start utilizing it in your code. To utilize a new exception, simply add the code that will raise the exception. To raise an exception, construct an object instance of the new exception class and call the standard procedure raise. The TMainWnd.HitTest method demonstrates how to raise an exception. HitTest is called to determine if the user presses the mouse on the word “Exception” displayed in the large panel in the lower half of the window. If yes, then an EMouseClickError exception is raised using the following call:
raise EMouseClickError.Create( SMsg );
The Create constructor takes a single string as an argument. A Delphi application’s default message handler displays this string in a message box if you do not provide an exception handler for this exception. Although you must create the exception object by calling the Create method, you do not have to worry about destroying the object. Once an exception handler actually handles the exception, it destroys the object.
Ride Into The Sun
It is indeed an exciting time for Pascal programmers. The Delphi visual development environment reestablishes Borland’s commitment to the Pascal language and the Pascal developer community. Next time we’ll look at the new object model—get ready for some heavy duty object-oriented programming!
Listing 1
unit Mainform; interface uses WinTypes, WinProcs, Classes, Graphics, Forms, Controls, Apps, MsgDlg, Excepts; type TMainWnd = class(TForm) GroupBox1: TGroupBox; AverageBtn: TButton; SumEdt: TEdit; NumItemsEdt: TEdit; Label1: TLabel; Label2: TLabel; AverageEdt: TEdit; GroupBox2: TGroupBox; NoProtBtn: TButton; GroupBox3: TGroupBox; MinProtBtn: TButton; CompProtBtn: TButton; Panel1: TPanel; ClickHereLbl: TLabel; procedure AverageBtnClick(Sender: TObject); procedure NoProtBtnClick(Sender: TObject); procedure ClickHereLblClick(Sender: TObject); procedure MinProtBtnClick(Sender: TObject); procedure CompProtBtnClick(Sender: TObject); procedure ClickHereLblMouseDown( Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); private { Private declarations } MousePt : TPoint; procedure HitTest; public { Public declarations } end; EMouseClickError = class( EMessage ); var MainWnd: TMainWnd; implementation procedure TMainWnd.NoProtBtnClick(Sender: TObject); var Num : Integer; NumStr : string; WastedPtr : Pointer; begin GetMem( WastedPtr, 1024 ); { Allocate some memory } NumStr := '23b'; { Invalid Integer String } { StrToInt will raise an EConvertError exception } Num := StrToInt( NumStr ); { Since nothing handles the exception, the } { following two statements never get executed } MessageDlg( 'About to Free Memory', mtInformation, [mbOk], 0 ); FreeMem( WastedPtr, 1024 ); end; procedure TMainWnd.MinProtBtnClick(Sender: TObject); var Num : Integer; NumStr : string; WastedPtr : Pointer; begin GetMem( WastedPtr, 1024 ); { Allocate some memory } NumStr := '23b'; { Invalid Integer String } { StrToInt will still raise an EConvertError exception. } { But this time, since it is part of a try…finally block, } { the memory allocated above can be released } try Num := StrToInt( NumStr ); finally MessageDlg( 'About to Free Memory', mtInformation, [mbOk], 0 ); FreeMem( WastedPtr, 1024 ); end; { Note that after the 'About to Free Memory' message box is } { displayed, the 'Invalid Integer…' message box appears } end; {= In this final version of the resource protection example, =} {= we take things to the completion by adding an exception =} {= handler to handle the EConvertError exception such that the =} {= 'Invalid Integer…' error message is no longer displayed. =} procedure TMainWnd.CompProtBtnClick(Sender: TObject); var Num : Integer; NumStr : string; WastedPtr : Pointer; begin GetMem( WastedPtr, 1024 ); { Allocate some memory } NumStr := '23b'; { Invalid Integer String } try { Since try… blocks are blocks, we can nest them } try Num := StrToInt( NumStr ); except on EConvertError do { Handle EConvertError exception } Num := 0; end; finally MessageDlg( 'About to Free Memory', mtInformation, [mbOk], 0 ); FreeMem( WastedPtr, 1024 ); end; end; procedure TMainWnd.AverageBtnClick(Sender: TObject); var Sum, Num : Integer; Average : Real; AvgStr : string; Undefined : Boolean; begin Undefined := False; { Undefined is True if Calculation Fails } try Sum := StrToInt( SumEdt.Text ); { Convert Text to Number } Num := StrToInt( NumItemsEdt.Text ); Average := Sum / Num; except on EZeroDivide do { If Num = 0 then exception raised } Undefined := True; on EConvertError do begin MessageDlg( 'Invalid Integer. Please try again.', mtError, [mbOK], 0 ); Exit; end; end; { except } if Undefined then AverageEdt.Text := 'Undefined' else begin Str( Average:8:2, AvgStr ); { Convert Average to String } AverageEdt.Text := AvgStr; { Set text of control to AvgStr } end; end; procedure TMainWnd.HitTest; var SMsg : string; begin with MousePt do begin { Test to see if mouse was clicked on the word 'Exception' } if ( X >= 185 ) and ( X <= 270 ) and ( Y >= 5 ) and ( Y <= 18 ) then begin { If user clicked on 'Exception', raise one } SMsg := 'You Clicked on Exception'; raise EMouseClickError.Create( SMsg ); end; end; end; procedure TMainWnd.ClickHereLblClick(Sender: TObject); begin try HitTest; except on EMouseClickError do MessageDlg( 'Look, Mom! I''m Handling EMouseClickError', mtInformation, [mbOk], 0 ); end; end; procedure TMainWnd.ClickHereLblMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin MousePt := Point( X, Y ); end; end.