
|
HomePage | About Us | HTML | DelpHi | Downloads | Credits | | BookMark This Site |
|
|
|
- 3 - Classes and Object-Oriented Programming
Today you get to the good stuff. In this chapter you will learn about classes. Classes are the heart of Object Pascal and a major part of object-oriented programming. Classes are also the heart of the Visual Component Library (VCL), which you will use when you start writing real Windows applications. (The VCL is discussed in detail on Day 5, "The Visual Component Model.") Today you will find out what a class is and how it's expected to be used. Along the way you will learn the meaning of Object Pascal buzzwords like inheritance, object, and data abstraction. Before you get to that, however, I want to cover a few more aspects of Object Pascal that I haven't yet covered.
Sets Sets are used frequently throughout Delphi, so you need to know what sets are and how they work. A set is a collecti on of values of one type. That description doesn't say too much, does it? An example that comes to mind is the Style property of a VCL font object. This property can include one or more of the following values:
A font can have any combination of these styles or none of them at all. A set of font styles, then, might have none of these values, it could have all of them, or it could have any combination. So how do you use a set? Let me use the Style property to illustrate. Typically, you turn the individual Style values for the font on or off at design time. Sometimes, however, you need to set the font's Style property at runtime. For example, let's say that you want to add the bold and italic attributes to the font style. One way is to declare a variable of type TFontStyles and then add the fsBold and fsItalic styles to the set. Here's how it looks:
var This code adds the elements fsBold and fsItalic to the Styles set. The elements are enclosed in brackets to indicate that you are adding elements to the set. The brackets, when used in this way, are called a set constructor. Notice that this code doesn't actually change a font's style; it just creates a set and adds two elements to it. To change a font's style, you have to assign this newly created set to the Font.Style property of some component:
Memo.Font.Style = Styles; Now, let's say that you want the font to be bold but not italic. In that case, you have to remove the italic style from the set:
Styles := Styles - [fsItalic]; The style now contains only the fsBold value because the fsItalic value has been removed. Often you want to know whether a particular item is in a set. Let's say you want to know whether the font is currently set to bold. You can find out whether the fsBold element is in the set by using the in keyword:
if fsBold in Styles then Sometimes you need to make sure you are starting with an empty set. You can clear a set of its contents by assigning an empty set to a set variable. This is done with an empty set constructor--for example,
{ start with an empty set
} In this example the font style is cleared of all contents, and then the bold and italic styles are added. This same thing can be accomplished in a slightly different way by just assigning directly to a set:
Styles := [fsBold, fsItalic]; You don't specifically have to create a TFontStyles variable to change a font's style. You can just work with the property directly--for example,
Memo.Font.Style := []; A set is declared using the set keyword. The TFontStyles property is declared in the VCL source file GRAPHICS.PAS like this:
TFontStyle = (fsBold,
fsItalic, fsUnderline, fsStrikeOut); TFontStyles = set of
TFontStyle; The first line here declares an enumeration type called TFontStyle. (An enumeration is a list of possible values.) The second line creates the TFontStyles set as a set of TFontStyle values. Sets are used often in VCL and in Delphi programming. Many component properties are defined as sets. You'll get the hang of sets quickly as you work with Delphi.
Casting New Term: Cast
means to tell the compiler to treat one data type as if it were a
different type. Another term for cast is typecast. Here's an example of a Char data type typecast to an Integer:
procedure
TForm1.Button1Click(Sender: TObject); In this example, the cast Integer(AChar) tells the compiler to convert the value of AChar to an Integer data type. The cast is necessary because you can't assign the value of a Char data type to an Integer type. If you attempt to make the assignment without the cast, the compiler will issue an error that reads Incompatible types: `Integer' and `Char'. By the way, when the preceding code executes, the label will display the text 65 (65 is the integer value of the character A). It is not always possible to cast one data type to another. Take this code, for example:
procedure
TForm1.Button1Click(Sender: TObject); In this case, I am trying
to cast a Double to an Integer. This is not a valid cast, so the
compiler will issue an error that reads Invalid typecast. To convert a
floating-point value to an integer value, use the Trunc, Floor, or Ceil
functions. Pointers can be cast from one type to another using the as operator. (Pointers are discussed in the next section.) I'll discuss the as operator later in the section "Class Keywords: is and as."
Pointers Pointers are one of the most confusing aspects of the Object Pascal language. So what is a pointer? It's a variable that holds the address of another variable. There, that wasn't so bad, was it? I wish it were that simple! Because a pointer holds the address of another variable, it is said to "point to" the second variable. This is called indirection because the pointer does not have a direct association with the actual data, but rather an indirect association. New Term: A pointe r is a variable that holds the address of another variable. Let's look at an example. Let's say you have a record, and you need to pass the address of that record to a procedure requiring a pointer. You take the address of a record instance using the @ operator. Here's how it looks:
var The APtr variable (which
is of type Pointer) is used to hold the memory address of the MLRecord
record. This type of pointer is called an untyped pointer because the
Pointer data type simply holds a memory address. Another type of pointer type The type PMailingListRecord is declared as a pointer to a TMailingListRecord. You will often see records and their corresponding pointers declared in this way. You might be wondering what the point is (no pun intended). Let's go on to the next section and I'll show you one way pointers are used.
Local Versus Dynamic Memo ry Usage Yesterday when you read about records, I showed you some examples. All of those examples used local allocation of objects. That is, the memory required for the record variable was obtained from the program's stack. New Term: Local allocation means that the memory required for a variable or object is obtained from the program's stack. New Term: The stack is an area of working memory set aside by the program when the program starts. Any memory the program needs for things such as local variables, function calls, and so on is taken from the program's stack. This memory is allocated as needed and then freed when it is no longer needed; usually this happens when the program enters a function or other local code block. Memory for any local variables the function uses is allocated when the function is entered. When the function returns, all the memory allocated for the function's use is freed. It all happens for you automatically; you don't have to give any thought to how the memory is freed or whether the memory is freed at all. Local allocation has its good points and its bad points. On the plus side, memory can be allocated from the stack very quickly. The negative side is that the stack is a fixed size and cannot be changed as the program runs. If your program runs out of stack space, weird things start to happen. Your program might crash, it might start behaving oddly, or it might seem to perform normally but crash when the program terminates. This is less of a problem in the 32-bit world than in 16-bit programming, but it's still a consideration. For things like variables of the built-in data types and small arrays, there is no point in doing anything other than local allocation. But if you are going to be using large records, you will probably want to use dynamic allocation from the heap. The heap amounts to your computer's free physical RAM plus all your free hard disk space. In other words, you can easily have 10 0MB of heap memory available on a typical Windows system. The good news here is that you have virtually unlimited memory available for your programs. The bad news is that memory allocated dynamically requires some additional overhead and, as such, is just a smidgen slower than memory allocated from the stack. In most programs the extra overhead is not noticed in the least. An additional drawback of dynamic allocation is that it requires more from the programmer--not a lot more, mind you, but a little. New Term: Dynamic allocation means that memory required for an object is allocated from the heap.
New Term: The heap
in a Windows program refers to all of your computer's virtual memory. Dynamic Allocation and Pointers In an Object Pascal program, memory can be allocated dynamically in several different ways. Perhaps the best way is to use the AllocMem function. AllocMem allocates memory and fills the allocated memory with zeros. (Other ways to dynamically allocate memory include the GetMem procedure and the New function.) All things considered, AllocMem probably provides the best way of allocating memory dynamically. Let's go back to the TMailingListRecord record. In previous examples, I allocated memory for one of these records from the stack like this:
var Now I'll create the record dynamically rather than locally:
var end; Notice that this time I declare a PMailingListRecord (a pointer to a TMailingListRecord) rather than a TMailingListRecord itself. Also notice that I allocate memory for the structure by calling the AllocMem function. The parameter passed to AllocMem is the amount of memory to allocate. The SizeOf function returns the size of the record, so I use that function to determine how much memory to allocate. The call to AllocMem allocates memory and initializes the pointer by creating a new instance of a TMailingListRecord dynamically. After the memory has been allocated, you can use the pointer variable just as you do a regular variable. Finally, notice that after I am done with the object, I free the memory associated with the object by using the FreeMem procedure. Failure to call FreeMem to free dynamically allocated objects will result in a program that leaks memory (uses up memory that it never releases). This is the process by which you dynamically create and access records in Object Pascal. You probably won't use dynamic allocation very much, but sometimes it's necessary, so you should know how it's done.
if SomePointer = nil then This code checks a
pointer to see whether it has been assigned a value. If it hasn't been
assigned a value, then memory is allocated for the pointer. Dereferencing a Pointer Sometimes you need to dereference a pointer. New Term: Dereferencing a pointer means retrieving the object that the pointer points to. Let's say that you dynamically created a mailing list record as descr ibed earlier. Now you want to assign the contents of that mailing list record to another mailing list record variable that was allocated from the stack. Here's what you have so far:
var Now let's say you want to copy the contents of APtr to the Rec variable. The APtr variable is a pointer to a TMailingListRecord and the Rec variable is a TMailingListRecord. You might try this:
Rec := APtr; That won't work, however,
because APtr contains a memory address, not a TMailingListRecord. In
order to make this assignment, you have to dereference the pointer
by using the pointer operator (^). It looks like this: Rec := APtr^; When you dereference a pointer, you are telling the compiler, "Give me the object pointed to by the pointer and not the value of the pointer itself."
What's a Class? A class is a collection of fields and methods (functions and procedures) that work together to accomplish a specific programming task. In this way a class is said to encapsulate the task. Classes have the following features:
Before diving into an
explanation of these features, let me give you a quick example of how a
class might be used. Let's use a typical Windows control as an
example--a check box, for instance. A class that represents a check box
would have fields var In this example, each instance of the class is a separate object. Each instance has its own fields, and the objects operate independently of one another. They are all objects of the same type but are separate instances in memory. With that brief introduction, you can roll up your sleeves once more and go to work on understanding classes.
Anatomy of a Class A class, like a record, has a declaration. The class declaration is always in a type section.
Class Access Levels Classes can have four levels of access:
Each of these access levels is defined in this section. Class access levels control how a class is used . As a single programmer, you might be not only the class's creator but also a user of the class. In team programming environments, one programmer might be the creator of the class and other programmers the users of the class. To understand the role that levels of access play in class operation, you first need to understand how classes are used. In any class there is the public part of the class, which the outside world has access to, and there is the private part of a class. The private part of a class is the internal implementation of the class--the inner workings, so to speak. Part of a well-designed class includes hiding anything from public view that the user of the class doesn't need to know. New Term: Data abstraction is the hiding of internal implementations within the class from outside views. Data abstraction prevents the user from knowing more than he or she needs to know about the class and also prevents the user from messing with things that shouldn't be messed with. For example, when you get in your car and turn the key to start it, do you want to know every detail about how the car operates? Of course not. You only want to know as much as you need to know to operate the car safely. In this analogy, the steering wheel, pedals, gear shift lever, speedometer, and so on represent the public interface between the car and the driver. The driver knows which of those components to manipulate to make the car perform the way he or she wants. Conversely, the engine, drive train, and electrical system of the car are hidden from public view. The engine is tucked neatly away where you never have to look at it if you don't want to. It's a detail that you don't need to know about, so it is hidden from you--kept private, if you prefer. Imagine how much trouble driving would be if you had to know everything the car was doing at all times: Is the carburetor getting enough gas? Does the differential have enough grease? Is the alternator producing adequate voltage for both the ignition and the radio to operate? Are the intake valves opening properly? Who needs it! In the same way, a class keeps its internal implementation private so the user of the class doesn't have to worry about what's going on under the hood. The internal workings of the class are kept private and the user interface is public. The protected access level is a little harder to explain. Protected class members, like private class members, cannot be accessed by users of the class. They can, however, be accessed by classes that are derived from this class. Continuing with the car analogy, let's say you want to extend the car (literally) by making it a stretch limousine. To do this, you need to know something about the underlying structure of the car. You need to know how to modify the drive shaft and frame of the car at the very minimum. In this case you would need to get your hands dirty and, as a limousine designer, get at the parts of the car that were previously unimportant to you (the protected parts). The internal workings of the engine are still kept private because you don't need to know how the engine works to extend the frame of the car. Similarly, most of the public parts of the car remain the same, but you might add some new public elements such as the controls for the intercom system. I've strayed a little here and given you a peek in to what is called inheritance, but I won't go in to further details right now. I will talk more about protected access a little later in the section "Methods," and about inheritance in the section "Inheritance." The point here is that the protected section of a class contains the parts of a class that someone extending the class will need to know about. The published access level is used when writing components. Any components declared in the published section will appear in the Object Inspector at design time. I'll talk more about the published section on Day 20, "Creating Compon ents." The Object Pascal language has four keywords that pertain to class access. The keywords are (not surprisingly) public, private, protected, and published. You specify a class member's access level when you declare the class. A class is declared with the class keyword. Here's an example:
TVehicle = class Speed : Integer;
procedure
StartElectricalSystem; Notice how you break the class organization down into the three access levels. You might not use all of the access levels in a given class. For example, I am not using the published access level in this example. You are not required to use any of the access levels if you don't want, but typically you will have a public and a private section at the least.
Constructors Classes in Object Pascal have a special method called the constructor. New Term: The constructor is a method that is used to create an instance of a class. The constructor is used to initialize any class member variables, allocate memory the class will need, or do any other startup tasks. The TVehicle example you just saw does not have a constructor. If you don't provide a constructor, you can use the base class's constructor when you create the class. (If not otherwise specified, all Object Pascal classes are derived from TObject. The TObject class has a constructor called Create, so it is this constructor that will be called if you don't provide a constructor. I'll discuss base classes and inheritance later in the section "Inheritance.") Although using the base class's constructor is fine for simple classes, you will almost always provide a constructor for classes of any significance. The constructor can be named anything, but it must be declared using the constructor keyword. This is what distinguishes it as a constructor. Given that, let's add a constructor declaration to the TVehicle class: TVehicle = class
private Notice that the constructor is a special type of method. It does not have a return type because a constructor cannot return a value. If you try to add a return type to the constructor declaration, you will get a compiler error. A class can have more than one constructor. This can be accomplished in two different ways. The first way is to simply give the constructor a different name--for example,
TVehicle = class This example shows two constructors, one called Create and the other called CreateModel. Another way to declare multiple constructors is through method overloading, which I discussed yesterday. Here is an example that uses constructors with the same name, but with different parameters: TVehicle = class
{ rest of class
deleted } Because method overloading is new in Delphi 4, I don't expect to see this way of declaring multiple constructors used very much in Delphi programs. The traditional method is to declare constructors with different names, an d I suspect that trend will continue for quite some time. Still, both methods are legal and either one can be used.
What's the point of multiple constructors? Multiple constructors provide different ways of creating a class. For instance, a class can have a constructor that takes no parameters and a constructor that takes one or more parameters to initialize fields to certain values. For example, let's say you have a class called TMyRect that encapsulates a rectangle (rectangles are frequently used in Windows programming). This class could have several constructors. It could have a default constructor that sets all the fields to 0, and another constructor that enables you to set the class's fields through the constructor. First, let's take a look at how the class declaration might look:
TMyRect = class end; The definitions for the constructors would look something like this:
constructor
TMyRect.Create; The first constructor simply initializes each field to 0. The second constructor takes the parameters passed and assigns them to the corresponding class fields. The variable names in the parameter list are local to the constructor, so each of the variable names begins with an A to differentiate between the local variables and the class fields (the use of the leading A is customary for Delphi programs). Notice the use of the inherited keyword in the constructors. I'll talk about the inherited keyword later in the section "Inheritance." I wanted to point it out here just so you would know I'm not leaving you in the dark.
New Term: Instantiation is the creation of an object, called an instance, of a class.
So how do you use one of
these constructors instead of the other? You do that when you
instantiate an instance of a class. The following code snippet creates
two instances of the TMyRect class. The first uses the Create
constructor and the second var You can have as many constructors as you like as long as they all have different names or, if overloaded, as long as they follow the rules of method overloading. There is one thing that I need to point out about the previous example: Both instances of the TMyRect class are allocated dynamically. Earlier I said that you allocate memory for an object dynamically by calling the GetMem procedure. Now I seem to be contradicting myself, but in truth I am not. The reason is that memory for Object Pascal classes is always allocate d dynamically. Although that is not true of records, it is true of classes. That also means that the previous code snippet leaks memory because I didn't free the memory associated with the two classes. I'll talk about that next. Because all Object Pascal classes are created on the heap, all class variables are, therefore, pointers. The Rect1 and Rect2 variables in the preceding example are both pointers to the TMyRect class.
Destructors New Term: The destructor is a special method that is automatically called just before the object is destroyed. The destructor can be considered the opposite of the constructor. It is usually used to free any memory allocated by the class or do any other cleanup chores. A class is not required to have a destructor because the base class's destructor can be used instead. Like a constructor, a destructor has no return value. Although a class can have multiple destructors, it is not something that is typically done. If you have just one destructor, you should name it Destroy. This is more than just tradition. When you free an instance of a class (remove it from memory), you call the Free method. Free is a method of the TObject class that calls the class's Destroy method just before the class is removed from memory. This is the typical way to free the memory associated with a class. Here's an example:
Rect1 := TMyRect.Create; The example in the
"Constructors" section would actually leak memory because the
two TMyRect objects were never freed. The following shows the updated code for the TMyRect class, complete with destructor (some code removed for brevity):
TMyRect = class The modified version of the TMyRect class allocates storage for a null-terminated string (a PChar) named Text in its constructor and frees that storage in the destructor. (I can't think of a good reason for a class that handles rectangles to have a text field, but you never know! It's just an example, after all.) Take a closer look at the declaration of the destructor in the TMyRect class declaration. It looks like this:
destructor Destroy;
override; Notice the override
keyword at the end of the declaration. This keyword tells the compiler
that you are overriding a method that is also found in the base class.
I'm getting ahead of myself again, so I'll continue this discussion
later in the
Data Fields Data fields of a class are simply variables that are declared in the class declaration; they can be considered as variables that have class scope. Fields in classes are essentially the same as fields in records except that their access can be controlled by declaring them as private, public, or protected. Regardless of a field's access, it is available for use in all methods of the class. Depending on the field's access level, it can be visible outside the class as well. Private and protected fields, for exampl e, are private to the class and cannot be seen outside the class. Public fields, however, can be accessed from outside the class but only through an object. Take the TMyRect class declared previously, for example. It has no public fields. You could try the following, but you'll get a compiler error:
Rect :=
TMyRect.CreateVal(0, 0, 100, 100); The compiler error will say Undeclared identifier: `Left'. The compiler is telling you that Left is a private field, and you can't get to it. If Left were in the public section of the class declaration, this code would compile.
Object Pascal uses
properties to control the access to private fields. A property can be
read/write, read-only, or write-only (although write-only properties are
rare). A property can have a read method that is called when the
property is read,
When you create an instance of a class, each class has its own data. You can assign a value to the variable Left in one instance of a class and assign a different value to the Left variable in a different instance of the class--for example,
Rect1 :=
TMyRect.CreateVal(100, 100, 500, 500); This code creates two instances of the TMyRect class. Although these two instances are identical in terms of their structure, they are completely separate in memory. Each instance has its own data. In the first case, the Left field would have a value of 100. In the second case it would have a value of 0. It's like new cars on the showroom floor: Every model of a particular car comes from the same mold, but they are all separate objects. They all vary in their color, upholstery style, features, and so on.
Methods Methods are functions and procedures that belong to your class. They are local to the class and don't exist outside the class. Methods can be called only from within the class itself or through an instance of the class. They have access to all public, protected, and private fields of the class. Methods can be declared in the private, protected, or public sections of your class. Good class design requires that you think about which of these sections your methods should go into. * Public methods, along with properties, represent the user interface to the class. It is through the public methods that users of the cl ass access the class to gain whatever functionality it provides. For example, let's say you have a class that plays and records waveform audio. Public methods might include methods named Open, Play, Record, Save, Rewind, and so on.
Methods can be declared as class methods. A class method operates more like a regular function or procedure than a method of a class. Specifically, a class method cannot access fields or other methods of the class. (In just a bit I'll tell you why this restriction exists.) Most of the time, you will not use class methods, so I won't go into any detail on them.
About Self New Term: All classes have a hidden field called Self. Self is a pointer to the instanc e of the class in memory. Obviously, this will require some explanation. First, let's take a look at how the TMyRect class would look if Self were not a hidden field: TMyRect = class
private This is effectively what the TMyRect class looks like to the compiler. When a class object is created, the Self pointer is automatically initialized to the address of the class in memory:
Rect :=
TMyRect.CreateVal(0, 0, 100, 100); "But," you ask, "what does Self mean?" Remember that each class instance gets its own copy of the class's fields. But all class instances share the same set of methods for the class (there's no point in duplicating that code for each instance of the class). How does the compiler figure out which instance goes with which method call? All class methods have a hidden Self parameter that goes with them. To illustrate, let's say you have a function for the TMyRect class called GetWidth. It would look like this: function TMyRect.GetWidth : Integer;
begin That's how the function looks to you and me. To the compiler, though, it looks something like this:
function TMyRect.GetWidth
: Integer; That's not exactly accurate from a technical perspective, but it's close enough for this discussion. In this code you can see that Self is working behind the scenes to keep everything straight for you. You don't have to worry abo ut how that happens, but you need to know that it does happen.
Although Self works
behind the scenes, it is still a variable that you can access from
within the class. As an illustration, let's take a quick peek into VCL.
Most of the time, you will create components in VCL by dropping them on
the form at procedure
TForm1.Button1Click(Sender: TObject); In this code, you can see that Self is used in the constructor (this sets the Owner property of the button, but I'll get into that later when I cover VCL components on Day 7, "VCL Components") and also that it is assigned to the Parent property of the newly created button. This is how you will use the Self pointer the vast majority of the time in your Delphi applications.
Don't worry too much about Self right now. When you begin to use VCL, it will quickly become clear when you are required to use Self in your Delphi applications.
A Class Example Right now it would be
nice if you could see an example of a class. Listing 3.1 shows a unit
that contains a class called TAirplane. This class could be used by an
aircraft controller program. The class enables you to command an
airplane by LISTING 3.1. AIRPLANU.PAS. unit AirplanU; function GetHeading : Integer;
function GetAltitude : Integer; Result := False;
end else ANALYSIS: Look first at the class declaration in the interface section. Notice that the TAirplane class has one overloaded function called GetStatus. When called with a string parameter, GetStatus will return a status string as well as the status data member (the string parameter is a variable parameter). When called without a parameter, it just returns Status. Note that the only way to access the private fields is via the public methods. For example, you can change the speed, altitude, and heading of an airplane only by sending it a message. To use an analogy, consider that an air traffic controller cannot physically change an aircraft's heading. The best he can do is send a message to the pilot and tell him to change to a new heading.
Now turn your attention to the definition of the TAirplane class in the interface section. The constructor performs some initialization chores. You have probably noticed that the SendMessage function does most of the work. A case statement determines which message w as sent and takes the appropriate action. Notice that the TakeOff and Land procedures cannot be called directly (they are protected) but rather are called through the SendMessage function. Again, as an air traffic controller, you can't make an aircraft take off or land, you can only send it a message telling it what you want it to do. There's something else here that I haven't discussed yet. Note the virtual keyword. This specifies that the function is a virtual method. New Term: A virtual method is a method that is automatically called if a method of that name exists in the derived class. I'll discuss virtual methods in the next section, but I wanted to point them out to you now. The book's code contains a program called Airport, which enables you to play air traffic controller. (You can find the book's code at the Web site http://www.mcp.com/info. Type in the book's ISBN: 0-672-31286-7.) The program first sets up an array of TAirplane classes and then creates three instances of the TAirplane class. You can send messages to any airplane by selecting the airplane, setting up the parameters for the message, and then clicking the Execute button. Clicking the button results in a call to the selected airplane's SendMessage function. When you send a message, you get a response back from the airplane, and that response is displayed in a memo component. Run the program and play with it to get a feel for how it works. Figure 3.1 shows the Airport program running.
Inheritance One of the most powerful features of classes in Object Pascal is that they can be extended through inheritance. New Term: Inheritance means taking an existing class and adding functionality by deriving a new class from it.
New Term: The class
you start with is called the base class or ancestor class, and the new
class you create is called the derived class. To illu strate, let's go back to the TAirplane class. The civilian and military worlds are quite different, as you know. To represent a military aircraft, I can derive a class from TAirplane and add functionality to it:
TMilitaryPlane =
class(TAirplane) A TMilitaryPlane has everything a TAirplane has, plus a few more goodies. Note the first line of the class declaration. The class name in parentheses after the class keyword is used to tell the compiler that I am inheriting from another class. The class from which I am deriving this class is the base class and, in this case, is the TAirplane class.
You'll notice that in the private section there is a line that declares a variable of the TMission class. The TMission class is not shown here, but it could encapsulate everything that deals with the mission of a military aircraft: the target, navigation waypoints, ingress and egress altitudes and headings, and so on. This illustrates the use of a field that is an instance of another class. In fact, you'll see that a lot when programming in Delphi.
Overriding Methods I want to take a moment here to discuss virtual methods. Note that the TakeOff procedure is a virtual method in the TAirplane class (refer to Listing 3.1). Notice that TakeOff is called by SendMessage in response to the MsgTakeOff message. If the TMilitaryPlane class did not provide its own TakeOff method, the base class's TakeOff method would be called. Because the TMilitaryPlane class does provide a TakeOff method, that method will be called rather than the method in the base class. New Term: Replacing a base class method in a derived class is called overriding the method. In order for overriding to work, the method signature must exactly match that of the method in the base class. In other words, the return type, method name, and parameter list must all be the same as the base class method. In addition, the method in the derived class must be declared with the override keyword.
You can override a method with the intention of replacing the base class method, or you can override a method to enhance the base class method. Consider the TakeOff method, for example. If you want to completely replace what the TakeOff method of TAirplane does, you would override it and supply whatever code you want:
procedure
TMilitaryPlane.TakeOff(Dir : Integer); But if you want your
method to take the functionality of the base class and add to it, you
would first call the base class method and then add new code. Calling
the base class method is done with the inherited keyword--for example, procedure
TMilitaryPlane.TakeOff(Dir : Integer); By calling the base class method, you get the original behavior of the method as written in the base class. You can then add code before or after the base class call to enhance the base class method. Note that the TakeOff method is declared in the protected section of the TAirplane class. If it were in the private section, this would not work because even a derived class cannot access the private members of its ancestor class. By making the TakeOff method protected, it is hidden from the outside world but still accessible to derived classes.
When you derive a class from another class, you must be sure to call the base class's constructor so that all ancestor classes are properly initialized. Calling the base class constructor is also done with the inherited keyword. Look again at the constructor for the TAirplane class:
constructor
TAirplane.Create(AName : string; AKind : Integer); end; Notice that the base class Create constructor is called to ensure that the class is properly created. "Hey, wait a minute!" I can hear some of you saying. "The TAirplane class doesn't have a base class!" Actually, it does. If no base class is specified when the class is declared, the base class is automatically TObject. Be sur e to call the base class constructor in your class constructor. Figure 3.2 illustrates the concept of inheritance. FIGURE 3.2. An example of inheritance. You can see in Figure 3.2 that the class called F16 is descended from the class called MilitaryFighter. Ultimately, F16 is derived from TAirplane because TAirplane is the base class for all of the airplane classes.
Class Keywords: is and as Object Pascal has two operators that pertain specifically to classes. The is operator is used to determine whether a class is of a specific type. Let's go back to the example of the TAirplane and TMilitaryPlane classes. Let's say you have an instance of a class called Plane. The class might be an instance of the TAirplane class, it might be an instance of the TMilitaryPlane class, or it might be an instance of a different class altogether. You can use the is operator to find out. For example:
if Plane is
TMilitaryPlane then The is operator returns a
Boolean value. If the variable is of the requested type, is returns
True. If the variable is not of the requested type, is returns False.
The is operator will also return True if the requested class type is an
ancestor if Plane is TAirplane
then
The as operator is usually used in conjunction with the with operator (yes, this conversation is a bit confusing). In this code snippet the Plane variable is a pointer that could be an instance of the TAirplane class, the TMilitaryPlane class, or neither. The as operator is used to cast the pointer to a TMilitaryPlane type and then the Attack method is called. If the Plane variable is not an instance of the TMilitaryPlane class (or one of its ancestor classes), this cast will fail and the Attack method will not be called. If, however, the Plane variable is a pointer to an instance of the TMilitaryPlane class, the cast will succeed and the Attack method will be called.
Summary Today you have learned about classes in Object Pascal. A well-designed class is easy to use and saves many programming hours. I'd even go so far as to say a well-designed class is a joy to use--especially when it's your own creation. The lessons of these first three days are important to understand as you progress through this book. If they don't make complete sense to you yet, don't despair. As you continue through the next days, you will see these concepts repeated and put to use in programs that have more practical application than the short, incomplete examples you've been working with thus far.
Workshop The Workshop contains quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you have learned. You can find the answers to quiz questions in Appendix A, "Answers to the Quiz Questions."
Q&A
Quiz
Exercises
|