|
Late Binding and PolymorphismPascal 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.
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. Overriding and Redefining MethodsAs 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;
Virtual versus Dynamic MethodsIn 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 HandlersA 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.
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. Abstract MethodsThe 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 <class name> 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;
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. |
|
Copyright © 2004-2024 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide |
|