The Delphi development environment is based on an object-oriented extension of the Pascal programming language known as Object Pascal. Recently, Borland stated its intention to refer to the language as "the Delphi language," probably because the company wants to be able to say that Kylix uses the Delphi language and because Borland will provide the Delphi language on the Microsoft .NET platform. Due to years of habit, I'll use the two names interchangeably.
Most modern programming languages support object-oriented programming (OOP). OOP languages are based on three fundamental concepts: encapsulation (usually implemented with classes), inheritance, and polymorphism (or late binding). Although you can write Delphi code without understanding the core features of its language, you won't be able to master this environment until you fully understand the programming language.
Note |
Due to space constraints and to the fact that the language hasn't changed much in recent years, in this chapter you'll find only a very fast-paced introduction to the language. You can read the more detailed description found in past editions of the book in the material available on my website (see Appendix C, "Free Companion Books on Delphi," for details). This material also includes Essential Pascal, a complete introduction to the standard Pascal language. |
The following topics are covered in this chapter:
The Delphi language is an OOP extension of the classic Pascal language, which Borland pushed forward for many years with its Turbo Pascal compilers. The syntax of the Pascal language is known to be quite verbose and more readable than, for example, the C language. Its OOP extension follows the same approach, delivering the same power of the recent breed of OOP languages, from Java to C#.
Even the core language is subject to continuous changes, but few of them will affect your everyday programming needs. In Delphi 6, for example, Borland added support for several features more or less related to the development of Kylix, the Linux version of Delphi:
Delphi 7 adds three additional compiler warnings: unsafe type, unsafe code, and unsafe cast. These warnings are emitted in case of operations that you won't be able to use to produce safe "managed" code on the Microsoft .NET platform (more on this in Chapter 25, "Delphi for .NET Preview: The Language and the RTL").
Another change relates to unit names, which can now be formed from multiple words separated by dot, as in the marco.test unit, saved in the marco.test.pas file. This feature will help support namespaces and more flexible unit references in Delphi for .NET and future versions of the Delphi compiler for Windows, but in Delphi 7 it has limited use.
Delphi is based on OOP concepts, and in particular on the definition of new class types. The use of OOP is partially enforced by the visual development environment, because for every new form defined at design time, Delphi automatically defines a new class. In addition, every component visually placed on a form is an object of a class type available in or added to the system library.
Note |
The terms class and object are commonly used and often misused, so let's be sure we agree on their definitions. A class is a user-defined data type, which has a state (its representation or internal data) and some operations (its behavior or its methods). An object is an instance of a class, or a variable of the data type defined by the class. Objects are actual entities. When the program runs, objects take up some memory for their internal representation. The relationship between object and class is the same as the one between variable and type. |
As in most other modern OOP languages (including Java and C#), in Delphi a class-type variable doesn't provide the storage for the object, but is only a pointer or reference to the object in memory. Before you use the object, you must allocate memory for it by creating a new instance or by assigning an existing instance to the variable:
var Obj1, Obj2: TMyClass; begin // assign a newly created object Obj1 := TMyClass.Create; // assign to an existing object Obj2 := ExistingObject;
The call to Create invokes a default constructor available for every class, unless the class redefines it (as described later). To declare a new class data type in Delphi, with some local data fields and some methods, use the following syntax:
type TDate = class Month, Day, Year: Integer; procedure SetValue (m, d, y: Integer); function LeapYear: Boolean; end;
Note |
The convention in Delphi is to use the letter T as a prefix for the name of every class you write and every other type (T stands for Type). This is just a convention—to the compiler, T is just a letter like any other—but it is so common that following it will make your code easier for other developers to understand. |
A method is defined with the function or procedure keyword, depending on whether it has a return value. Inside the class definition, methods can only be declared; they must be then defined in the implementation portion of the same unit. In this case, you prefix each method name with the name of the class it belongs to, using dot notation:
procedure TDate.SetValue (m, d, y: Integer); begin Month := m; Day := d; Year := y; end; function TDate.LeapYear: Boolean; begin // call IsLeapYear in SysUtils.pas Result := IsLeapYear (Year); end;
Tip |
If you press Ctrl+Shift+C while the cursor is within the class definition, the Class Completion feature of the Delphi editor will generate the skeleton of the definition of the methods declared in a class. |
This is how you can use an object of the previously defined class:
var ADay: TDate; begin // create an object ADay := TDate.Create; try // use the object ADay.SetValue (1, 1, 2000); if ADay.LeapYear then ShowMessage ('Leap year: ' + IntToStr (ADay.Year)); finally // destroy the object ADay.Free; end; end;
Notice that ADay.LeapYear is an expression similar to ADay.Year, although the first is a function call and the second a direct data access. You can optionally add parentheses after the call of a function with no parameters. You can find the previous code snippets in the source code of the Dates1 example; the only difference is that the program creates a date based on the year provided in an edit box.
Note |
The code snippet above uses a try/finally block to ensure the destruction of the object even in the case of exceptions in the code. You can find an introduction to the topic of exceptions at the end of this chapter. |
There is a lot more to say about methods. Here are some short notes about the features available in Delphi:
To emphasize the fact that Delphi components aren't much different from other objects (and also to demonstrate the use of the Self keyword), I've written the CreateComps example. This program has a form with no components and a handler for its OnMouseDown event, which I've chosen because it receives as a parameter the position of the mouse click (unlike the OnClick event). I need this information to create a button component in that position. Here is the method's code:
procedure TForm1.FormMouseDown (Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var Btn: TButton; begin Btn := TButton.Create (Self); Btn.Parent := Self; Btn.Left := X; Btn.Top := Y; Btn.Width := Btn.Width + 50; Btn.Caption := Format ('Button at %d, %d', [X, Y]); end;
The effect of this code is to create buttons at mouse-click positions, as you can see in Figure 2.1. In the code, notice in particular the use of the Self keyword as both the parameter of the Create method (to specify the component's owner) and the value of the Parent property. I'll discuss these two elements (ownership and the Parent property) in Chapter 4, "Core Library Classes."
Figure 2.1: The output of the CreateComps example, which creates Button components at run time
When writing code like this, you might be tempted to use the Form1 variable instead of Self. In this specific example, that change wouldn't make any practical difference; but if there are multiple instances of a form, using Form1 would be an error. In fact, if the Form1 variable refers to the first form of that type being created, then by clicking in another form of the same type, the new button will always be displayed in the first form. The button's Owner and Parent will be Form1, not the form the user has clicked. In general, referring to a particular instance of a class when the current object is required is bad OOP practice.
A class can have any amount of data and any number of methods. However, for a good object-oriented approach, data should be hidden, or encapsulated, inside the class using it. When you access a date, for example, it makes no sense to change the value of the day by itself. In fact, changing the value of the day might result in an invalid date, such as February 30. Using methods to access the internal representation of an object limits the risk of generating erroneous situations, because the methods can check whether the date is valid and refuse to modify the new value if it is not. Encapsulation is important because it allows the class writer to modify the internal representation in a future version.
The concept of encapsulation is often indicated by the idea of a "black box." You don't know about the internals: You only know how to interface with the black box or use it regardless of its internal structure. The "how to use" portion, called the class interface, allows other parts of a program to access and use the objects of that class. However, when you use the objects, most of their code is hidden. You seldom know what internal data the object has, and you usually have no way to access the data directly. Of course, you are supposed to use methods to access the data, which is shielded from unauthorized access. This is the object-oriented approach to a classical programming concept known as information hiding. However, in Delphi there is the extra level of hiding, through properties, as we'll see later in this chapter.
Delphi implements this class-based encapsulation, but it still supports the classic module-based encapsulation using the structure of units. Every identifier that you declare in the interface portion of a unit becomes visible to other units of the program, provided there is a uses statement referring back to the unit that defines the identifier. On the other hand, identifiers declared in the implementation portion of the unit are local to that unit.
For class-based encapsulation, the Delphi language has three access specifiers: private, protected, and public. A fourth, published, controls run-time type information (RTTI) and design-time information (as discussed in more detail in Chapter 4), but it gives the same programmatic accessibility as public. Here are the three classic access specifiers:
Generally, the fields of a class should be private and the methods public. However, this is not always the case. Methods can be private or protected if they are needed only internally to perform some partial computation or to implement properties. Fields might be declared as protected so that you can manipulate them in inherited classes, although this isn't considered a good OOP practice.
Warning |
Access specifiers only restrict code outside your unit from accessing certain members of classes declared in the interface section of your unit. This means that if two classes are in the same unit, there is no protection for their private fields. |
As an example, consider this new version of the TDate class:
type TDate = class private Month, Day, Year: Integer; public procedure SetValue (y, m, d: Integer); overload; procedure SetValue (NewDate: TDateTime); overload; function LeapYear: Boolean; function GetText: string; procedure Increase; end;
You might think of adding other functions, such as GetDay, GetMonth, and GetYear, which return the corresponding private data, but similar direct data-access functions are not always needed. Providing access functions for each and every field might reduce the encapsulation and make it harder to modify the internal implementation of a class. Access functions should be provided only if they are part of the logical interface of the class you are implementing.
Another new method is the Increase procedure, which increases the date by one day. This calculation is far from simple, because you need to consider the different lengths of the various months as well as leap and non–leap years. To make it easier to write the code, I'll change the internal implementation of the class to Delphi's TDateTime type for the internal implementation. The class definition will change to the following (the complete code is in the DateProp example):
type TDate = class private fDate: TDateTime; public procedure SetValue (y, m, d: Integer); overload; procedure SetValue (NewDate: TDateTime); overload; function LeapYear: Boolean; function GetText: string; procedure Increase; end;
Notice that because the only change is in the private portion of the class, you won't have to modify any of your existing programs that use it. This is the advantage of encapsulation!
Note |
The TDateTime type is a floating-point number. The integral portion of the number indicates the date since 12/30/1899, the same base date used by OLE Automation and Microsoft Win32 applications. (Use negative values to express previous years.) The decimal portion indicates the time as a fraction. For example, a value of 3.75 stands for the second of January 1900, at 6:00 a.m. (three-quarters of a day). To add or subtract dates, you can add or subtract the number of days, which is much simpler than adding days with a day/month/year representation. |
Properties are a very sound OOP mechanism, or a well-thought-out application of the idea of encapsulation. Essentially, you have a name that completely hides its implementation details. This allows you to modify the class extensively without affecting the code using it. A good definition of properties is that of virtual fields. From the perspective of the user of the class that defines them, properties look exactly like fields, because you can generally read or write their value. For example, you can read the value of the Caption property of a button and assign it to the Text property of an edit box with the following code:
Edit1.Text := Button1.Caption;
It looks like you are reading and writing fields. However, properties can be directly mapped to data, as well as to access methods, for reading and writing the value. When properties are mapped to methods, the data they access can be part of the object or outside of it, and they can produce side effects, such as repainting a control after you change one of its values. Technically, a property is an identifier that is mapped to data or methods using a read and a write clause. For example, here is the definition of a Month property for a date class:
property Month: Integer read FMonth write SetMonth;
To access the value of the Month property, the program reads the value of the private field FMonth; to change the property value, it calls the method SetMonth (which must be defined inside the class, of course).
Different combinations are possible (for example, you could also use a method to read the value or directly change a field in the write directive), but the use of a method to change the value of a property is common. Here are two alternative definitions for the property, mapped to two access methods or mapped directly to data in both directions:
property Month: Integer read GetMonth write SetMonth; property Month: Integer read FMonth write FMonth;
Often, the actual data and access methods are private (or protected), whereas the property is public. For this reason, you must use the property to have access to those methods or data, a technique that provides both an extended and a simplified version of encapsulation. It is an extended encapsulation because not only can you change the representation of the data and its access functions, but you can also add or remove access functions without changing the calling code. A user only needs to recompile the program using the property.
Tip |
When you're defining properties, take advantage of the extended class completion feature of Delphi's editor, which you activate with the Ctrl+Shift+C key combination. After you write the property name, type, and semicolon, press Ctrl+Shift+C, and Delphi will provide you with a complete definition and the skeleton of the setter method. Write Get in front of the name of the identifier after the read keyword, and you'll also have a getter method with almost no typing. |
Properties for the TDate Class
As an example, I've added properties for accessing the year, the month, and the day to an object of the TDate class discussed earlier. These properties are not mapped to specific fields, but they all map to the single fDate field storing the complete date information. This is why all the properties have both getter and setter methods:
type TDate = class public property Year: Integer read GetYear write SetYear; property Month: Integer read GetMonth write SetMonth; property Day: Integer read GetDay write SetDay;
Each of these methods is easily implemented using functions available in the DateUtils unit (more details in Chapter 3, "The Run Time Library"). Here is the code for two of them (the others are very similar):
function TDate.GetYear: Integer; begin Result := YearOf (fDate); end; procedure TDate.SetYear(const Value: Integer); begin fDate := RecodeYear (fDate, Value); end;
The code for this class is available in the DateProp example. The program uses a secondary unit for the definition of the TDate class to enforce encapsulation and creates a single-date object that is stored in a form variable and kept in memory for the entire execution of the program. Using a standard approach, the object is created in the form OnCreate event handler and destroyed in the form OnDestroy event handler. The program form (see Figure 2.2) has three edit boxes and buttons to copy the values of these edit boxes to and from the properties of the date object.
Figure 2.2: The DateProp example's form
Warning |
When writing the values, the program uses the SetValue method instead of setting each of the properties. Assigning the month and the day separately can cause you trouble when the month is not valid for the current day. For example, suppose the day is currently January 31, and you want to assign to it February 20. If you assign the month first, this part of the assignment will fail, because February 31 does not exist. If you assign the day first, the problem will arise when doing the reverse assignment. Due to the validity rules for dates, it is better to assign everything at once. |
Advanced Features of Properties
Properties have several advanced features I'll focus on in future chapters. Specifically, in Chapter 4 I'll cover the TPersistent class, RTTI, and streaming and I'll discuss writing custom Delphi components in Chapter 9, "Writing Delphi Components." Here is a short summary of these more advanced features:
Note |
You can usually assign a value to a property or read it, and you can even use properties in expressions, but you cannot always pass a property as a parameter to a procedure or method. This is the case because a property is not a memory location, so it cannot be used as a var or out parameter; it cannot be passed by reference. |
One of the key ideas of encapsulation is to reduce the number of global variables used by a program. A global variable can be accessed from every portion of a program. For this reason, a change in a global variable affects the whole program. On the other hand, when you change the representation of a class's field, you only need to change the code of some methods of that class and nothing else. Therefore, we can say that information hiding refers to encapsulating changes.
Let me clarify this idea with an example. When you have a program with multiple forms, you can make some data available to every form by declaring it as a global variable in the interface portion of the unit of one of the forms:
var Form1: TForm1; nClicks: Integer;
This approach works, but the data is connected to the entire program rather than a specific instance of the form. If you create two forms of the same type, they'll share the data. If you want every form of the same type to have its own copy of the data, the only solution is to add it to the form class:
type TForm1 = class(TForm) public nClicks: Integer; end;
Adding Properties to Forms
The previous class uses public data, so for the sake of encapsulation, you should instead change it to use private data and data-access functions. An even better solution is to add a property to the form. Every time you want to make some information of a form available to other forms, you should use a property, for all the reasons discussed in the section "Encapsulating with Properties." To do so, change the field declaration of the form (in the previous code) by adding the keyword property in front of it, and then press Ctrl+Shift+C to activate code completion. Delphi will automatically generate all the extra code you need.
The complete code for this form class is available in the FormProp example and illustrated in Figure 2.3. The program can create multi-instances of the form (that is, multiple objects based on the same form class), each with its own click count.
Figure 2.3: Two forms of the FormProp example at run time
Note |
Notice that adding a property to a form doesn't add to the list of the form properties in the Object Inspector. |
In my opinion, properties should also be used in the form classes to encapsulate the access to the components of a form. For example, if you have a main form with a status bar used to display some information (and with the SimplePanel property set to True) and you want to modify the text from a secondary form, you might be tempted to write
Form1.StatusBar1.SimpleText := 'new text';
This is a standard practice in Delphi, but it's not a good one, because it doesn't provide any encapsulation of the form structure or components. If you have similar code in many places throughout an application, and you later decide to modify the user interface of the form (for example, replacing StatusBar with another control or activating multiple panels), you'll have to fix the code in many places. The alternative is to use a method or, even better, a property to hide the specific control. This property can be defined as
property StatusText: string read GetText write SetText;
with GetText and SetText methods that read from and write to the SimpleText property of the status bar (or the caption of one of its panels). In the program's other forms, you can refer to the form's StatusText property; and if the user interface changes, only the setter and getter methods of the property are affected.
Note |
See Chapter 4 for a detailed discussion of how you can avoid having published form fields for components, which will improve encapsulation. But don't rush there: The description requires a good knowledge of Delphi, and the technique discussed has a few drawbacks. |
So far, to allocate memory for objects, I've called the Create method. This is a constructor— a special method that you can apply to a class to allocate memory for an instance of that class. The instance is returned by the constructor and can be assigned to a variable for storing the object and using it later. All the data of the new instance is set to zero. If you want your instance data to start out with specific values, then you need to write a custom constructor to do that.
Use the constructor keyword in front of your constructor. Although you can use any name for a constructor, you should stick to the standard name, Create. If you use a name other than Create, the Create constructor of the base TObject class will still be available, but a programmer calling this default constructor might bypass the initialization code you've provided because they don't recognize the name.
By defining a Create constructor with some parameters, you replace the default definition with a new one and make its use compulsory. For example, after you define
type TDate = class public constructor Create (y, m, d: Integer);
you'll only be able to call this constructor and not the standard Create:
var ADay: TDate; begin // Error, does not compile: ADay := TDate.Create; // OK: ADay := TDate.Create (1, 1, 2000);
The rules for writing constructors for custom components are different, as you'll see in Chapter 9. The reason is that in this case you have to override a virtual constructor. Overloading is particularly relevant for constructors, because you can add multiple constructors to a class and call them all Create; this approach makes the constructors easy to remember and follows a standard path provided by other OOP languages in which constructors must all have the same name. As an example, I've added to the class two separate Create constructors: one with no parameters, which hides the default constructor; and one with initialization values. The constructor with no parameter uses as the default value today's date (as you can see in the complete code of the DataView example):
type TDate = class public constructor Create; overload; constructor Create (y, m, d: Integer); overload;
In the same way that a class can have a custom constructor, it can have a custom destructor—a method declared with the destructor keyword and called Destroy. Just as a constructor call allocates memory for the object, a destructor call frees the memory. Destructors are needed only for objects that acquire external resources in their constructors or during their lifetime. You can write custom code for a destructor, generally overriding the default Destroy destructor, to let an object execute some clean-up code before it is destroyed.
Destroy is a virtual destructor of the TObject class. You should never define a different destructor, because objects are usually destroyed by calling the Free method, and this method calls the Destroy virtual destructor of the specific class (virtual methods will be discussed later in this chapter).
Free is a method of the TObject class, inherited by all other classes. The Free method basically checks whether the current object (Self) is not nil before calling the Destroy virtual destructor. Free doesn't set the object to nil automatically; this is something you should do yourself! The object doesn't know which variables may be referring to it, so it has no way to set them all to nil.
Delphi 5 introduced a FreeAndNil procedure you can use to free an object and set its reference to nil at the same time. Call FreeAndNil(Obj1) instead of writing the following:
Obj1.Free; Obj1 := nil;
Note |
There's more on this topic in the section "Destroying Objects Only Once" later in this chapter. |
In some OOP languages, declaring a variable of a class type creates an instance of that class. Delphi, instead, is based on an object reference model. The idea is that a variable of a class type, such as the TheDay variable in the preceding ViewDate example, does not hold the value of the object. Rather, it contains a reference, or a pointer, to indicate the memory location where the object has been stored. You can see this structure depicted in Figure 2.4.
Figure 2.4: A representation of the structure of an object in memory, with a variable referring to it
The only problem with this approach is that when you declare a variable, you don't create an object in memory (which is inconsistent with all other variables, confusing new users of Delphi); you only reserve the memory location for a reference to an object. Object instances must be created manually, at least for the objects of the classes you define. Instances of the components you place on a form are built automatically by the Delphi library.
You've seen how to create an instance of an object by applying a constructor to its class. Once you have created an object and you've finished using it, you need to dispose of it (to avoid filling up memory you don't need any more, which causes what is known as a memory leak). This can be accomplished by calling the Free method. As long as you create objects when you need them and free them when you're finished with them, the object reference model works without a glitch. The object reference model has many consequences on assigning object and on managing memory, as you'll see in the next two sections.
If a variable holding an object only contains a reference to the object in memory, what happens if you copy the value of that variable? Suppose you write the BtnTodayClick method of the ViewDate example in the following way:
procedure TDateForm.BtnTodayClick(Sender: TObject); var NewDay: TDate; begin NewDay := TDate.Create; TheDay := NewDay; LabelDate.Caption := TheDay.GetText; end;
This code copies the memory address of the NewDay object to the TheDay variable (as shown in Figure 2.5); it doesn't copy the data of one object into the other. In this particular circumstance, this is not a very good approach—you keep allocating memory for a new object every time the button is clicked, but you never release the memory of the object the TheDay variable was previously pointing to.
Figure 2.5: A representation of the operation of assigning an object reference to another object. This is different from copying the actual content of an object to another.
This specific issue can be solved by freeing the old object, as in the following code (which is also simplified, without the use of an explicit variable for the newly created object):
procedure TDateForm.BtnTodayClick(Sender: TObject); begin TheDay.Free; TheDay := TDate.Create;
The important thing to keep in mind is that, when you assign an object to another object, Delphi copies the reference to the object in memory to the new object reference. You should not consider this a negative: In many cases, being able to define a variable referring to an existing object can be a plus. For example, you can store the object returned by accessing a property and use it in subsequent statements, as this code snippet indicates:
var ADay: TDate; begin ADay := UserInformation.GetBirthDate; // use a ADay
The same thing happens if you pass an object as a parameter to a function: You don't create a new object, but you refer to the same one in two different places in the code. For example, by writing this procedure and calling it as follows, you'll modify the Caption property of the Button1 object, not of a copy of its data in memory (which would be totally useless):
procedure CaptionPlus (Button: TButton); begin Button.Caption := Button.Caption + '+'; end; // call... CaptionPlus (Button1)
This means that the object is being passed by reference without the use of the var keyword and without any other obvious indication of the pass-by-reference semantic, which also confuses newcomers. What if you really want to change the data inside an existing object, so that it matches the data of another object? You have to copy each field of the object, which is possible only if they are all public, or provide a specific method to copy the internal data. Some classes of the VCL have an Assign method, which performs this copy operation. To be more precise, most of the VCL classes that inherit from TPersistent, but do not inherit from TComponent, have the Assign method. Other TComponent-derived classes have this method but raise an exception when it is called.
In the DateCopy example, I've added an Assign method to the TDate class and called it from the Today button, with the following code:
procedure TDate.Assign (Source: TDate); begin fDate := Source.fDate; end; procedure TDateForm.BtnTodayClick(Sender: TObject); var NewDay: TDate; begin NewDay := TDate.Create; TheDay.Assign(NewDay); LabelDate.Caption := TheDay.GetText; NewDay.Free; end;
Memory management in Delphi is subject to three rules, at least if you allow the system to work in harmony without Access Violations and without consuming unneeded memory:
Whether you must perform these operations in your code or can let Delphi handle memory management for you depends on the model you choose among the different approaches provided by Delphi.
Delphi supports three types of memory management for dynamic elements:
Destroying Objects Only Once
If you call the Free method (or call the Destroy destructor) of an object twice, you get an error. However, if you remember to set the object to nil, you can call Free twice with no problem.
Note |
You might wonder why you can safely call Free if the object reference is nil, but you can't call Destroy. The reason is that Free is a known method at a given memory location, whereas the virtual function Destroy is determined at run time by looking at the type of the object—a very dangerous operation if the object no longer exists. |
To sum things up, here are a couple of guidelines:
In general, you can also check whether an object is nil by using the Assigned function. The following two statements are equivalent in most cases:
if Assigned (ADate) then ... if ADate <> nil then ...
Notice that these statements test only whether the pointer is not nil; they do not check whether it is a valid pointer. If you write the following code, the test will be satisfied, and you'll get an error on the line with the call to the object method:
ToDestroy.Free; if ToDestroy <> nil then ToDestroy.DoSomething;
It is important to realize that calling Free doesn't set the object to nil.
You'll often need to use a slightly different version of an existing class. For example, you might need to add a new method or slightly change an existing one. If you copy and paste the original class and then modify it (certainly a terrible programming practice, unless there is a specific reason to do so), you'll duplicate your code, bugs, and headaches. Instead, in such a circumstance you should use a key feature of OOP: inheritance.
To inherit from an existing class in Delphi, you only need to indicate that class at the beginning of the declaration of the new class. For example, this is done each time you create a new form:
type TForm1 = class(TForm) end;
This definition indicates that the TForm1 class inherits all the methods, fields, properties, and events of the TForm class. You can call any public method of the TForm class for an object of the TForm1 type. TForm, in turn, inherits some of its methods from another class, and so on, up to the TObject base class.
As an example of inheritance, you can derive a new class from TDate and modify its GetText function. You can find this code in the Dates unit of the NewDate example:
type TNewDate = class (TDate) public function GetText: string; end;
To implement the new version of the GetText function, I used the FormatDateTime function, which uses (among other features) the predefined month names available in Windows; these names depend on the user's regional and language settings. (Many of these regional settings are copied by Delphi into constants defined in the library, such as LongMonthNames, ShortMonthNames, and others you can find under the "Currency and date/time formatting variables" topic in the Delphi Help file.) Here is the GetText method, where 'dddddd' stands for the long date format:
function TNewDate.GetText: string; begin GetText := FormatDateTime ('dddddd', fDate); end;
Tip |
Using regional information, the NewDate program automatically adapts itself to different Windows user settings. If you run this program on a computer with regional settings referring to a language other than English, it will automatically show month names in that language. To test this behavior, you just need to change the regional settings. Notice that regional-setting changes immediately affect the running programs. |
Once you have defined the new class, you need to use this new data type in the code of the form of the NewDate example, defining the TheDay object of type TNewDate and creating an object of this new class in the FormCreate method. You don't have to change the code with method calls, because the inherited methods still work exactly in the same way; however, their effect changes, as the new output demonstrates (see Figure 2.6).
Figure 2.6: The output of the NewDate program, with the name of the month and of the day depending on Windows regional settings
The code of the GetText method of the TNewDate class compiles only if it is written in the same unit as the TDate class. In fact, it accesses the fDate private field of the ancestor class. If we want to place the descendant class in a new unit, we must either declare the fDate field as protected or add a protected access method in the ancestor class to read the value of the private field.
Many developers believe that the first solution is always the best, because declaring most of the fields as protected will make a class more extensible and will make it easier to write inherited classes. However, this approach violates the idea of encapsulation. In a large hierarchy of classes, changing the definition of some protected fields of the base classes becomes as difficult as changing some global data structures. If 10 derived classes are accessing this data, changing its definition means potentially modifying the code in each of the 10 classes.
In other words, flexibility, extension, and encapsulation often become conflicting objectives. When this happens, you should try to favor encapsulation. If you can do so without sacrificing flexibility, that will be even better. Often this intermediate solution can be obtained by using a virtual method, a topic I'll discuss in detail in the section "Late Binding and Polymorphism." If you choose not to use encapsulation in order to obtain faster coding of the inherited classes, then your design might not follow the object-oriented principles.
Accessing Protected Data of Other Classes (or, the "Protected Hack")
You've seen that in Delphi, the private and protected data of a class is accessible to any functions or methods that appear in the same unit as the class. For example, consider this class (part of the Protection example):
type TTest = class protected ProtectedData: Integer; end;
Once you place this class in its own unit, you won't be able to access its protected portion from other units directly. Accordingly, if you write the following code
var Obj: TTest; begin Obj := TTest.Create; Obj.ProtectedData := 20; // won't compile
the compiler will issue an error message, "Undeclared identifier: 'ProtectedData.'" At this point, you might think there is no way to access the protected data of a class defined in a different unit. However, there is a way. Consider what happens if you create an apparently useless derived class, such as the following:
type TTestHack = class (TTest);
Now, if you make a direct cast of the object to the new class and access the protected data through it, this is how the code will look:
var Obj: TTest; begin Obj := TTest.Create; TTestHack (Obj).ProtectedData := 20; // compiles!
This code compiles and works properly, as you can see by running the Protection program. How is it possible for this approach to work? Well, if you think about it, the TTestHack class automatically inherits the protected fields of the TTest base class, and because the TTestHack class is in the same unit as the code that tries to access the data in the inherited fields, the protected data is accessible. As you would expect, if you move the declaration of the TTestHack class to a secondary unit, the program will no longer compile.
Now that I've shown you how to do this, I must warn you that violating the class-protection mechanism this way is likely to cause errors in your program (from accessing data that you really shouldn't), and it runs counter to good OOP technique. However, there are times when using this technique is the best solution, as you'll see by looking at the VCL source code and the code of many Delphi components. Two examples that come to mind are accessing the Text property of the TControl class and the Row and Col positions of the DBGrid control. These two ideas are demonstrated by the TextProp and DBGridCol examples, respectively. (These examples are quite advanced, so I suggest that only programmers with a good background in Delphi programming read them at this point in the text—other readers might come back later.) Although the first example shows a reasonable example of using the typecast cracker, the DBGrid example of Row and Col is a counterexample—it illustrates the risks of accessing bits that the class writer chose not to expose. The row and column of a DBGrid do not mean the same thing as they do in a DrawGrid or StringGrid (the base classes). First, DBGrid does not count the fixed cells as actual cells (it distinguishes data cells from decoration), so your row and column indexes will have to be adjusted by whatever decorations are currently in effect on the grid (and those can change on the fly). Second, the DBGrid is a virtual view of the data. When you scroll up in a DBGrid, the data may move underneath it, but the currently selected row might not change.
This technique—declaring a local type only so that you can access protected data members of a class—is often described as a hack, and it should be avoided whenever possible. The problem is not accessing protected data of a class in the same unit but declaring a class for the sole purpose of accessing protected data of an existing object of a different class! The danger of this technique is in the hard-coded typecast of an object from a class to a different one.
Inheritance and Type Compatibility
Pascal is a strictly typed language. This means that you cannot, for example, assign an integer value to a Boolean variable, unless you use an explicit typecast. The rule is that two values are type-compatible only if they are of the same data type, or (to be more precise) if their data type refers to a single type definition. To simplify your life, Delphi makes some predefined types assignment compatible: you can assign an Extended to a Double and vice versa, with automatic promotion or demotion (and potential accuracy loss).
Warning |
If you redefine the same data type in two different units, the types won't be compatible, even if their names are identical. A program using two equally named types from two different units will be a nightmare to compile and debug. |
There is an important exception to this rule in the case of class types. If you declare a class, such as TAnimal, and derive from it a new class, say TDog, you can then assign an object of type TDog to a variable of type TAnimal. You can do so because a dog is an animal! As a general rule, you can use an object of a descendant class any time an object of an ancestor class is expected. However, the reverse is not legal; you cannot use an object of an ancestor class when an object of a descendant class is expected. To simplify the explanation, here it is again in code terms:
var MyAnimal: TAnimal; MyDog: TDog; begin MyAnimal := MyDog; // This is OK MyDog := MyAnimal; // This is an error!!!
Pascal functions and procedures are usually based on static or early binding. This means that a method call is resolved by the compiler and linker, which replace the request with a call to the specific memory location where the function or procedure resides (the routine's address). OOP languages allow the use of another form of binding, known as dynamic or late binding. In this case, the actual address of the method to be called is determined at run time based on the type of the instance used to make the call.
This technique is known as polymorphism (a Greek word meaning "many forms"). Polymorphism means you can call a method, applying it to a variable, but which method Delphi actually calls depends on the type of the object the variable relates to. Delphi cannot determine until run time the class of the object the variable refers to, because of the type-compatibility rule discussed in the previous section. The advantage of polymorphism is being able to write simpler code, treating disparate object types as if they were the same and getting the correct runtime behavior.
For example, suppose that a class and an inherited class (let's say TAnimal and TDog) both define a method, and this method has late binding. You can apply this method to a generic variable, such as MyAnimal, which at run time can refer either to an object of class TAnimal or to an object of class TDog. The actual method to call is determined at run time, depending on the class of the current object.
The PolyAnimals example demonstrates this technique. The TAnimal and TDog classes have a Voice method that outputs the sound made by the selected animal, both as text and as sound (using a call to the PlaySound API function defined in the MMSystem unit). The Voice method is defined as virtual in the TAnimal class and is later overridden when you define the TDog class, by the use of the virtual and override keywords:
type TAnimal = class public function Voice: string; virtual; TDog = class (TAnimal) public function Voice: string; override;
The effect of the call MyAnimal.Voice depends. If the MyAnimal variable currently refers to an object of the TAnimal class, it will call the method TAnimal.Voice. If it refers to an object of the TDog class, it will call the method TDog.Voice, instead. This happens only because the function is virtual (you can experiment by removing this keyword and recompiling).
The call to MyAnimal.Voice will work for an object that is an instance of any descendant of the TAnimal class, even classes that are defined in other units—or that haven't been written yet! The compiler doesn't need to know about all the descendants in order to make the call compatible with them; only the ancestor class is needed. In other words, this call to MyAnimal.Voice is compatible with all future TAnimal inherited classes.
Note |
This is the key technical reason why object-oriented programming languages favor reusability. You can write code that uses classes within a hierarchy without any knowledge of the specific classes that are part of that hierarchy. In other words, the hierarchy—and the program—is still extensible, even when you've written thousands of lines of code using it. Of course, there is one condition: The ancestor classes of the hierarchy need to be designed very carefully. |
In Figure 2.7, you can see an example of the output of the PolyAnimals program. By running it, you'll also hear the corresponding sounds produced by the PlaySound API call.
Figure 2.7: The output of the PolyAnimals example
As you have just seen, to override a late-bound method in a descendant class, you need to use the override keyword. Note that this can take place only if the method was defined as virtual (or dynamic) in the ancestor class. Otherwise, if it is a static method, there is no way to activate late binding, other than to change the code of the ancestor class.
The rules are simple: A method defined as static remains static in every inherited class, unless you hide it with a new virtual method having the same name. A method defined as virtual remains late-bound in every inherited class (unless you hide it with a static method, which is quite a foolish thing to do). There is no way to change this behavior, because of the way the compiler generates different code for late-bound methods.
To redefine a static method, you add a method to an inherited class having the same parameters or different parameters than the original one, without any further specifications. To override a virtual method, you must specify the same parameters and use the override keyword:
type TMyClass = class procedure One; virtual; procedure Two; {static method} end; TMyDerivedClass = class (MyClass) procedure One; override; procedure Two; end;
Typically, you can override a method two ways: replace the method of the ancestor class with a new version, or add more code to the existing method. This can be accomplished by using the inherited keyword to call the same method of the ancestor class. For example, you can write:
procedure TMyDerivedClass.One; begin // new code ... // call inherited procedure MyClass.One inherited One; end;
When you override an existing virtual method of a base class, you must use the same parameters. When you introduce a new version of a method in a descendent class, you can declare it with the parameters you want. In fact, this will be a new method unrelated to the ancestor method of the same name—they only happen to use the same name. Here is an example:
type TMyClass = class procedure One; end; TMyDerivedClass = class (TMyClass) procedure One (S: string); end;
Note |
Using the class definitions just given, when you create an object of the TMyDerivedClass class, you can call its One method with the string parameter, but not the parameter-less version defined in the base class. If this is what you need, it can be accomplished by marking the redeclared method (the one in the derived class) with the overload keyword. If the method has different parameters than the version in the base class, it becomes effectively an overloaded method; otherwise it replaces the base class method. Notice that the method doesn't need to be marked with overload in the base class. However, if the method in the base class is virtual, the compiler issues the warning "Method 'One' hides virtual method of base type 'TMyClass.'" To avoid this message and to instruct the compiler more precisely on your intentions, you can use the reintroduce directive. If you are interested in this advanced topic, you can find this code in the Reintr example and experiment with it further. |
In Delphi, there are two ways to activate late binding. You can declare the method as virtual, as you have seen, or declare it as dynamic. The syntax of the virtual and dynamic keywords is exactly the same, and the result of their use is also the same. What is different is the internal mechanism used by the compiler to implement late binding.
Virtual methods are based on a virtual method table (VMT, also known as a vtable), which is an array of method addresses. For a call to a virtual method, the compiler generates code to jump to an address stored in the nth slot in the object's virtual method table. VMTs allow fast execution of the method calls, but they require an entry for each virtual method for each descendant class, even if the method is not overridden in the inherited class.
Dynamic method calls, on the other hand, are dispatched using a unique number indicating the method, which is stored in a class only if the class defines or overrides it. The search for the corresponding function is generally slower than the one-step table lookup for virtual methods. The advantage is that dynamic method entries only propagate in descendants when the descendants override the method.
Message Handlers
A late-bound method can be used to handle a Windows message, too, although the technique is somewhat different. For this purpose Delphi provides yet another directive, message, to define message-handling methods, which must be procedures with a single var parameter. The message directive is followed by the number of the Windows message the method wants to handle.
Warning |
The message directive is also available in Kylix and is fully supported by the language and the run-time library (RTL). However, the visual portion of the CLX application framework does not use message methods to dispatch notifications to controls. For this reason, whenever possible, you should use a virtual method provided by the library rather than handle a Windows message directly. Of course, this matters only if you want your code to be more portable. |
For example, the following code allows you to handle a user-defined message, with the numeric value indicated by the wm_User Windows constant:
type TForm1 = class(TForm) ... procedure WMUser (var Msg: TMessage); message wm_User; end;
The name of the procedure and the type of the parameters are up to you, although there are several predefined record types for the various Windows messages. You could later generate this message, invoking the corresponding method, by writing:
PostMessage (Form1.Handle, wm_User, 0, 0);
This technique can be extremely useful for veteran Windows programmers, who know all about Windows messages and API functions. You can also dispatch a message immediately by calling the SendMessage API or the VCL Perform method.
The abstract keyword is used to declare methods that will be defined only in inherited classes of the current class. The abstract directive fully defines the method; it is not a forward declaration. If you try to provide a definition for the method, the compiler will complain. In Delphi, you can create instances of classes that have abstract methods. However, when you try to do so, Delphi's 32-bit compiler issues the warning message "Constructing instance of containing abstract methods." If you happen to call an abstract method at run time, Delphi will raise an exception, as demonstrated by the AbstractAnimals example (an extension of the PolyAnimals example), which uses the following class:
type TAnimal = class public function Voice: string; virtual; abstract;
Note |
Most other OOP languages use a stricter approach: you cannot generally create instances of classes containing abstract methods. |
You might wonder why you would want to use abstract methods. The reason lies in the use of polymorphism. If class TAnimal has the virtual method Voice, every inherited class can redefine it. If it has the abstract method Voice, every inherited class must redefine it.
In early versions of Delphi, if a method overriding an abstract method called inherited, the result was in an abstract method call. Since Delphi 6, the compiler has been enhanced to notice the presence of the abstract method and skip the inherited call. This means you can safely use inherited in every overridden method, unless you specifically want to disable executing some code of the base class.
The Delphi type-compatibility rule for descendant classes allows you to use a descendant class where an ancestor class is expected. As I mentioned earlier, the reverse is not possible. Now, suppose the TDog class has an Eat method, which is not present in the TAnimal class. If the variable MyAnimal refers to a dog, it should be possible to call the function. But if you try, and the variable is referring to another class, the result is an error. By making an explicit typecast, you could cause a nasty run-time error (or worse, a subtle memory overwrite problem), because the compiler cannot determine whether the type of the object is correct and the methods you are calling actually exist.
To solve the problem, you can use techniques based on run-time type information (RTTI, for short). Essentially, because each object "knows" its type and its parent class, you can ask for this information with the is operator (or in some peculiar cases using the InheritsFrom method of TObject). The parameters of the is operator are an object and a class type, and the return value is a Boolean:
if MyAnimal is TDog then ...
The is expression evaluates as True only if the MyAnimal object is currently referring to an object of class TDog or a type descendant from TDog. This means that if you test whether a TDog object is of type TAnimal, the test will succeed. In other words, this expression evaluates as True if you can safely assign the object (MyAnimal) to a variable of the data type (TDog).
Now that you know for sure that the animal is a dog, you can make a safe typecast (or type conversion). You can accomplish this direct cast by writing the following code:
var MyDog: TDog; begin if MyAnimal is TDog then begin MyDog := TDog (MyAnimal); Text := MyDog.Eat; end;
This same operation can be accomplished directly by the second RTTI operator, as, which converts the object only if the requested class is compatible with the actual one. The parameters of the as operator are an object and a class type, and the result is an object converted to the new class type. You can write the following snippet:
MyDog := MyAnimal as TDog; Text := MyDog.Eat;
If you only want to call the Eat function, you might also use an even shorter notation:
(MyAnimal as TDog).Eat;
The result of this expression is an object of the TDog class data type, so you can apply to it any method of that class. The difference between the traditional cast and the use of the as cast is that the second approach raises an exception if the object type is incompatible with the type you are trying to cast it to. The exception raised is EInvalidCast (exceptions are described at the end of this chapter).
To avoid this exception, use the is operator and, if it succeeds, make a plain typecast (in fact, there is no reason to use is and as in sequence, doing the type check twice):
if MyAnimal is TDog then TDog(MyAnimal).Eat;
Both RTTI operators are very useful in Delphi because you often want to write generic code that can be used with several components of the same type or even of different types. When a component is passed as a parameter to an event-response method, a generic data type is used (TObject); so, you often need to cast it back to the original component type:
procedure TForm1.Button1Click(Sender: TObject); begin if Sender is TButton then ... end;
This is a common technique in Delphi, and I'll use it in examples throughout the book. The two RTTI operators, is and as, are extremely powerful, and you might be tempted to consider them as standard programming constructs. Although they are indeed powerful, you should probably limit their use to special cases. When you need to solve a complex problem involving several classes, try using polymorphism first. Only in special cases, where polymorphism alone cannot be applied, should you try using the RTTI operators to complement it. Do not use RTTI instead of polymorphism. This is bad programming practice, and it results in slower programs. RTTI has a negative impact on performance, because it must walk the hierarchy of classes to see whether the typecast is correct. As you have seen, virtual method calls require just a memory lookup, which is much faster.
Note |
Run-time type information (RTTI) involves more than the is and as operators. You can access detailed class and type information at run time, particularly for published properties, events, and methods. You'll find more on this topic in Chapter 4. |
When you define an abstract class to represent the base class of a hierarchy, you can come to a point at which the abstract class is so abstract that it only lists a series of virtual functions without providing any implementation. This kind of purely abstract class can also be defined using a specific technique: an interface. For this reason, we refer to these classes as interfaces.
Technically, an interface is not a class, although it may resemble one. Interfaces are not classes, because they are considered a totally separate element with distinctive features:
Note |
The base interface class was IUnknown until Delphi 5, but Delphi 6 introduced a new name for it—IInterface—to mark even more clearly the fact that this language feature is separate from Microsoft's COM (which uses IUnknown as its base interface). Delphi interfaces are available also in Kylix. |
It is important to note that interfaces support a slightly different OOP model than classes. Interfaces provide a less restricted implementation of polymorphism. Object reference polymorphism is based around a specific branch of a hierarchy. Interface polymorphism works across an entire hierarchy. Certainly, interfaces favor encapsulation and provide a looser connection between classes than inheritance. Notice that the most recent OOP languages, from Java to C#, have the notion of interfaces.
Here is the syntax of the declaration of an interface (which, by convention, starts with the letter I):
type ICanFly = interface ['{EAD9C4B4-E1C5-4CF4-9FA0-3B812C880A21}'] function Fly: string; end;
This interface has a GUID (Globally Unique Identifier)—a numeric ID following its declaration and based on Windows conventions. You can generate these identifiers by pressing Ctrl+Shift+G in the Delphi editor.
Note |
Although you can compile and use an interface without specifying a GUID for it, you'll generally want to generate the GUID because it is required to perform interface querying or dynamic as typecasts using that interface type. The whole point of interfaces is (usually) to take advantage of greatly extended type flexibility at run time; so, compared with class types, interfaces without GUIDs are not very useful. |
Once you've declared an interface, you can define a class to implement it:
type TAirplane = class (TInterfacedObject, ICanFly) function Fly: string; end;
The RTL already provides a few base classes to implement the basic behavior required by the IInterface interface. For internal objects, use the TInterfacedObject class I've used in this code.
You can implement interface methods with static methods (as in the previous code) or with virtual methods. You can override virtual methods in inherited classes by using the override directive. If you don't use virtual methods, you can still provide a new implementation in an inherited class by redeclaring the interface type in the inherited class, rebinding the interface methods to new versions of the static methods. At first sight, using virtual methods to implement interfaces seems to allow for smoother coding in inherited classes, but both approaches are equally powerful and flexible. However, the use of virtual methods affects code size and memory.
Note |
The compiler has to generate stub routines to fix up the interface call entry points to the matching method of the implementing class, and adjust the self pointer. The interface method stubs for static methods have to adjust self and jump to the real method in the class. The interface method stubs for virtual methods are much more complicated, requiring about four times more code (20 to 30 bytes) in each stub than the static case. Also, adding more virtual methods to the implementing class just bloats the virtual method table (VMT) that much more in the implementing class and all its descendents. An interface already has its own VMT, and redeclaring an interface in descendents to rebind the interface to new methods in the descendent is just as polymorphic as using virtual methods, but much smaller in code size. |
Now that you have defined an implementation of the interface, you can write some code to use an object of this class, through an interface-type variable:
var Flyer1: ICanFly; begin Flyer1 := TAirplane.Create; Flyer1.Fly; end;
As soon as you assign an object to an interface-type variable, Delphi automatically checks to see whether the object implements that interface, using the as operator. You can explicitly express this operation as follows:
Flyer1 := TAirplane.Create as ICanFly;
Note |
The compiler generates different code for the as operator when used with interfaces or with classes. With classes, the compiler introduces run-time checks to verify that the object is effectively "type-compatible" with the given class. With interfaces, the compiler sees at compile time that it can extract the necessary interface from the available class type, so it does. This operation is like a "compile-time as," not something that exists at run time. |
Whether you use the direct assignment or the as statement, Delphi does one extra thing: It calls the _AddRef method of the object (defined by IInterface). The standard implementation of this method, like the one provided by TInterfacedObject, is to increase the object's reference count. At the same time, as soon as the Flyer1 variable goes out of scope, Delphi calls the _ Release method (again part of IInterface). The TInterfacedObject's implementation of _Release decreases the reference count, checks whether the reference count is zero, and, if necessary, destroys the object. For this reason, the previous example doesn't include any code to free the object you've created.
In other words, in Delphi, objects referenced by interface variables are reference-counted, and they are automatically de-allocated when no interface variable refers to them any more.
Warning |
When using interface-based objects, you should generally access them only with object references or only with interface references. Mixing the two approaches breaks the reference counting scheme provided by Delphi and can cause memory errors that are extremely difficult to track. In practice, if you've decided to use interfaces, you should probably use exclusively interface-based variables. If you want to be able to mix them, instead, disable the reference counting by writing your own base class instead of using TInterfacedObject. |
Another key feature of Delphi is its support for exceptions. Exceptions make programs more robust by providing a standard way for notifying and handling errors and unexpected conditions. Exceptions make programs easier to write, read, and debug because they allow you to separate the error-handling code from your normal code, instead of intertwining the two. Enforcing a logical split between code and error handling and branching to the error handler automatically makes the actual logic cleaner and clearer. You end up writing code that is more compact and less cluttered by maintenance chores unrelated to the actual programming objective.
At run time, Delphi libraries raise exceptions when something goes wrong (in the run-time code, in a component, or in the operating system). From the point in the code at which it is raised, the exception is passed to its calling code, and so on. Ultimately, if no part of your code handles the exception, the VCL handles it, by displaying a standard error message and then trying to continue the program by handling the next system message or user request.
The whole mechanism is based on four keywords:
try Delimits the beginning of a protected block of code.
except Delimits the end of a protected block of code and introduces the exception-handling statements.
finally Specifies blocks of code that must always be executed, even when exceptions occur. This block is generally used to perform cleanup operations that should always be executed, such as closing files or database tables, freeing objects, and releasing memory and other resources acquired in the same program block.
raise Generates an exception. Most exceptions you'll encounter in your Delphi programming will be generated by the system, but you can also raise exceptions in your own code when it discovers invalid or inconsistent data at run time. The raise keyword can also be used inside a handler to re-raise an exception; that is, to propagate it to the next handler.
Tip |
Exception handling is no substitute for proper control flow within a program. Keep using if statements to test user input and other foreseeable error conditions. You should use exceptions only for abnormal or unexpected situations. |
The power of exceptions in Delphi relates to the fact that they are "passed" from a routine or method to the caller, up to a global handler (if the program provides one, as Delphi applications generally do), instead of following the standard execution path of the program. So the real problem you might have is not how to stop an exception but how to execute code even if an exception is raised.
Consider this code, which performs some time-consuming operations and uses the hourglass cursor to show the user that it's doing something:
Screen.Cursor := crHourglass; // long algorithm... Screen.Cursor := crDefault;
In case there is an error in the algorithm (as I've included on purpose in the TryFinally example's event handlers), the program will break, but it won't reset the default cursor. This is what a try/finally block is for:
Screen.Cursor := crHourglass; try // long algorithm... finally Screen.Cursor := crDefault; end;
When the program executes this function, it always resets the cursor, regardless of whether an exception (of any sort) occurs.
This code doesn't handle the exception; it merely makes the program robust in case an exception is raised. A try block can be followed by either an except or a finally statement, but not both of them at the same time; so, if you want to also handle the exception, the typical solution is to use two nested try blocks. You associate the internal block with a finally statement and the external block with an except statement, or vice versa as the situation requires. Here is the skeleton of the code for the third button in the TryFinally example:
Screen.Cursor := crHourglass; try try // long algorithm... finally Screen.Cursor := crDefault; end; except on E: EDivByZero do ... end;
Every time you have some finalization code at the end of a method, you should place the code in a finally block. You should always, invariably, and continuously (how can I stress this more?) protect your code with finally statements, to avoid resource or memory leaks in case an exception is raised.
Tip |
Handling the exception is generally much less important than using finally blocks, because Delphi can survive most exceptions. Too many exception-handling blocks in your code probably indicate errors in the program flow and possibly a misunderstanding of the role of exceptions in the language. In the examples in the rest of the book, you'll see many try/finally blocks, a few raise statements, and almost no try/except blocks. |
In the exception-handling statements shown earlier, you caught the EDivByZero exception, which is defined by Delphi's RTL. Other such exceptions refer to run-time problems (such as a wrong dynamic cast), Windows resource problems (such as out-of-memory errors), or component errors (such as a wrong index). Programmers can also define their own exceptions; you can create a new inherited class of the default exception class or one of its inherited classes:
type EArrayFull = class (Exception);
When you add a new element to an array that is already full (probably because of an error in the logic of the program), you can raise the corresponding exception by creating an object of this class:
if MyArray.Full then raise EArrayFull.Create ('Array full');
This Create constructor (inherited from the Exception class) has a string parameter to describe the exception to the user. You don't need to worry about destroying the object you have created for the exception, because it will be deleted automatically by the exception-handler mechanism.
The code presented in the previous excerpts is part of a sample program called Exception1. Some of the routines have been slightly modified, as in the following DivideTwicePlusOne function:
function DivideTwicePlusOne (A, B: Integer): Integer; begin try // error if B equals 0 Result := A div B; // do something else... skip if exception is raised Result := Result div B; Result := Result + 1; except on EDivByZero do begin Result := 0; MessageDlg ('Divide by zero corrected.', mtError, [mbOK], 0); end; on E: Exception do begin Result := 0; MessageDlg (E.Message, mtError, [mbOK], 0); end; end; // end except end;
Debugging and Exceptions
When you start a program from the Delphi environment (for example, by pressing the F9 key), you'll generally run it within the debugger. When an exception is encountered, the debugger will suspend the program by default. This result is normally what you want, of course, because you'll know where the exception took place and can see the call of the handler step by step. You can also use Delphi's Stack Trace feature to see the sequence of function and method calls that caused the program to raise an exception.
In the case of the Exception1 test program, however, this behavior will confuse a programmer not well aware of how Delphi's debugger works. Even if the code is prepared to properly handle the exception, the debugger will stop the program execution at the source code line closest to where the exception was raised. Then, moving step by step through the code, you can see how it is handled.
If you just want to let the program run when the exception is properly handled, run the program from Windows Explorer, or temporarily disable the Stop on Delphi Exceptions options in the Language Exceptions page of the Debugger Options dialog box (activated by the Tools ® Debugger Options command), shown in the Language Exceptions page of the Debugger Options dialog box shown here (as an alternative you can also disable the debugger):
In the Exception1 code, there are two different exception handlers after the same try block. You can have any number of these handlers, which are evaluated in sequence.
Using a hierarchy of exceptions, a handler is also called for the inherited classes of the type it refers to, as any procedure will do. For this reason, you need to place the broader handlers (the handlers of the ancestor Exception classes) at the end. But keep in mind that using a handler for every exception, such as the previous one, is not usually a good choice. It is better to leave unknown exceptions to Delphi. The default exception handler in the VCL displays the error message of the exception class in a message box, and then resumes normal program operation. You can modify the normal exception handler with the Application.OnException event or the OnException event of the ApplicationEvents component, as demonstrated in the ErrorLog example in the next section.
Another important element of the previous code is the use of the exception object in the handler (see on E: Exception do). The reference E of class Exception refers to the exception object passed by the raise statement. When you work with exceptions, remember this rule: You raise an exception by creating an object and handle it by indicating its type. This has an important benefit, because as you have seen, when you handle a type of exception, you are really handling exceptions of the type you specify as well as any descendant type.
Most of the time, you don't know which operation will raise an exception, and you cannot (and should not) wrap each and every piece of code in a try/except block. The general approach is to let Delphi handle all the exceptions and eventually pass them to you, by handling the OnException event of the global Application object. You can do so rather easily with the ApplicationEvents component.
In the ErrorLog example, I've added to the main form an instance of the ApplicationEvents component and written a handler for its OnException event:
procedure TFormLog.LogException(Sender: TObject; E: Exception); var Filename: string; LogFile: TextFile; begin // prepares log file Filename := ChangeFileExt (Application.Exename, '.log'); AssignFile (LogFile, Filename); if FileExists (FileName) then Append (LogFile) // open existing file else Rewrite (LogFile); // create a new one try // write to the file and show error Writeln (LogFile, DateTimeToStr (Now) + ':' + E.Message); if not CheckBoxSilent.Checked then Application.ShowException (E); finally // close the file CloseFile (LogFile); end; end;
Note |
The ErrorLog example uses the text file support provided by the traditional Turbo Pascal TextFile data type. You can assign a text file variable to an actual file and then read or write it. You can find more on TextFile operations in Chapter 12 of the e-book Essential Pascal, covered in Appendix C. |
In the global exceptions handler, you can write to the log, for example, the date and time of the event, and also decide whether to show the exception as Delphi usually does (executing the ShowException method of the TApplication class). By default, Delphi executes ShowException only if no OnException handler is installed. In Figure 2.8, you can see the ErrorLog program running and a sample exceptions log open in ConTEXT (a nice programmer's editor built with Delphi and available at www.fixedsys.com/context).
Figure 2.8: The ErrorLog example and the log it produces
The last language feature I'll discuss in this chapter is the use of class references, which implies the idea of manipulating classes themselves within your code. The first point to keep in mind is that a class reference isn't an object; it is a reference to a class type. A class reference type determines the type of a class reference variable. Sounds confusing? A few lines of code will make this concept a little clearer.
Suppose you have defined the class TMyClass. You can now define a new class reference type, related to that class:
type TMyClassRef = class of TMyClass;
Now you can declare variables of both types. The first variable refers to an object, the second to a class:
var AnObject: TMyClass; AClassRef: TMyClassRef; begin AnObject := TMyClass.Create; AClassRef := TMyClass;
You may wonder what class references are used for. In general, class references allow you to manipulate a class data type at run time. You can use a class reference in any expression where the use of a data type is legal. There are not many such expressions, but the few cases are interesting, like the creation of an object. You can rewrite the last line of the previous code as follows:
AnObject := AClassRef.Create;
This time, you apply the Create constructor to the class reference instead of to an actual class; you use a class reference to create an object of that class.
Class reference types wouldn't be as useful if they didn't support the same type-compatibility rule that applies to class types. When you declare a class reference variable, such as MyClassRef, you can then assign to it that specific class and any inherited class. So if TMyNewClass is an inherited class of TMyClass my class, you can also write
AClassRef := TMyNewClass;
Delphi declares a lot of class references in the run-time library and the VCL, such as the following:
TClass = class of TObject; TComponentClass = class of TComponent; TFormClass = class of TForm;
In particular, the TClass class reference type can be used to store a reference to any class you write in Delphi, because every class is ultimately derived from TObject. The TFormClass reference is used in the source code of most Delphi projects. The CreateForm method of the Application object requires as a parameter the class of the form to create:
Application.CreateForm(TForm1, Form1);
The first parameter is a class reference; the second is a variable that stores a reference to the created object instance.
Finally, when you have a class reference, you can apply to it the class methods of the related class. Considering that each class inherits from TObject, you can apply to each class reference some of the methods of TObject, as you'll see in Chapter 3.
What is the practical use of class references in Delphi? Being able to manipulate a data type at run time is a fundamental element of the Delphi environment. When you add a new component to a form by selecting it from the Component Palette, you select a data type and create an object of that data type. (Actually, that is what Delphi does for you behind the scenes.) In other words, class references give you polymorphism for object construction.
To give you a better idea of how class references work, I've built an example named ClassRef. The form displayed by this example has three radio buttons, placed inside a panel in the upper portion
of the form. When you select one of these radio buttons and click the form, you'll be able to create new components of the three types indicated by the button labels: radio buttons, push buttons, and edit boxes.
To make this program run properly, you need to change the names of the three components. The form must also have a class reference field, declared as ClassRef: TControlClass. It stores a new data type every time the user clicks one of the three radio buttons, with assignments like ClassRef := TEdit. The interesting part of the code is executed when the user clicks the form. Again, I've chosen the OnMouseDown event of the form to hold the position of the mouse click:
procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var NewCtrl: TControl; MyName: String; begin // create the control NewCtrl := ClassRef.Create (Self); // hide it temporarily, to avoid flickering NewCtrl.Visible := False; // set parent and position NewCtrl.Parent := Self; NewCtrl.Left := X; NewCtrl.Top := Y; // compute the unique name (and caption) Inc (Counter); MyName := ClassRef.ClassName + IntToStr (Counter); Delete (MyName, 1, 1); NewCtrl.Name := MyName; // now show it NewCtrl.Visible := True; end;
The first line of the code for this method is the key. It creates a new object of the class data type stored in the ClassRef field. You accomplish this by applying the Create constructor to the class reference. Now you can set the value of the Parent property, set the position of the new component, give the component a name (which is automatically used also as the value of Caption or Text), and make it visible. You can see an example of the output of this program in Figure 2.9.
Figure 2.9: An example of the output of the ClassRef example
Note |
For polymorphic construction to work, the base class type of the class reference must have a virtual constructor. If you use a virtual constructor (as in the example), the constructor call applied to the class reference will call the constructor of the type that the class reference variable currently refers to. But without a virtual constructor, your code will call the constructor of fixed class type indicated in the class reference declaration. Virtual constructors are required for polymorphic construction in the same way that virtual methods are required for polymorphism. |
In this chapter, we have discussed the foundations of object-oriented programming (OOP) in Delphi. We have considered the definition of classes and the use of methods, encapsulation, and memory management, but also some more advanced concepts such as properties and the dynamic creation of components. Then we moved to inheritance, virtual and abstract methods, polymorphism, safe typecasting, interfaces, exceptions, and class references.
This is certainly a lot of information if you are a newcomer. But if you are fluent in another OOP language or if you've already used past versions of Delphi, you should be able to apply the topics covered in this chapter to your programming.
Understanding the secrets of Delphi's language and library is vital for becoming an expert Delphi programmer. These topics form the foundation of working with the VCL and CLX class libraries; after exploring them in the next two chapters, we'll finally go on to explore the development of real applications using all the various components provided by Delphi.
In the meantime, Chapter 3 will give you an overview of the Delphi run-time library (mainly a collection of functions with little OOP involved). The RTL is a collection of assorted routines for performing basic tasks with Delphi. Chapter 4 will give you more information about the language, discussing features related to the structure of the Delphi class library, such as the effect of the published keyword and the role of events. That chapter, as a whole, will discuss the overall architecture of the component library.
Part I - Foundations
Part II - Delphi Object-Oriented Architectures
Part III - Delphi Database-Oriented Architectures
Part IV - Delphi, the Internet, and a .NET Preview