The Delphi Object Model (PART I)
|The Delphi Object Model (PART I)|
|Sample Chapter of|
Delphi all versions
||Last Update: |
delphi delphi3000 article Object Model OO interfaces
|Uploader: Stefan Walther
Company: bluestep.com IT-Consulting
|Reference: Ray Lischner - O'Reilly|
Delphi's support for object-oriented programming is rich and powerful. In addition to traditional classes and objects, Delphi also has interfaces (similar to those found in COM and Java), exception handling, and multithreaded programming. This chapter covers Delphi's object model in depth. You should already be familiar with standard Pascal and general principles of object-oriented programming.
Reprinted with permission from O'Reilly & Associates
Classes and Objects
Think of a class as a record on steroids. Like a record, a class describes a type that comprises any number of parts, called fields. Unlike a record, a class can also contain functions and procedures (called methods), and properties. A class can inherit from another class, in which case it inherits all the fields, methods, and properties of the ancestor class.
An object is a dynamic instance of a class. An object is always allocated dynamically, on the heap, so an object reference is like a pointer (but without the usual Pascal caret operator). When you assign an object reference to a variable, Delphi copies only the pointer, not the entire object. When your program finishes using an object, it must explicitly free the object. Delphi does not have any automatic garbage collection (but see the section "Interfaces," later in this chapter).
For the sake of brevity, the term object reference is often shortened to object, but in precise terms, the object is the chunk of memory where Delphi stores the values for all the object's fields. An object reference is a pointer to the object. The only way to use an object in Delphi is through an object reference. An object reference usually comes in the form of a variable, but it might also be a function or property that returns an object reference.
A class, too, is a distinct entity (as in Java, but unlike C++). Delphi's representation of a class is a read-only table of pointers to virtual methods and lots of information about the class. A class reference is a pointer to the table. (Chapter 3, Runtime Type Information, describes in depth the layout of the class tables.) The most common use for a class reference is to create objects or to test the type of an object reference, but you can use class references in many other situations, including passing class references as routine parameters or returning a class reference from a function. The type of a class reference is called a metaclass.
Example 2-1 shows several class declarations. A class declaration is a type declaration that starts with the keyword class. The class declaration contains field, method, and property declarations, ending with the end keyword. Each method declaration is like a forward declaration: you must implement the method in the same unit (except for abstract methods, which are discussed later in this chapter).
type TAccount = class private fCustomer: string; // name of customer fNumber: Cardinal; // account number fBalance: Currency; // current account balance end; TSavingsAccount = class(TAccount) private fInterestRate: Integer; // annual percentage rate, scaled by 1000 end; TCheckingAccount = class(TAccount) private fReturnChecks: Boolean; end; TCertificateOfDeposit = class(TSavingsAccount) private fTerm: Cardinal; // CD maturation term, in days end; var CD1, CD2: TAccount; begin CD1 := TCertificateOfDeposit.Create; CD2 := TCertificateOfDeposit.Create; ...
Figure 2-1 depicts the memory layout of the objects and classes from Example 2-1. The variables and their associated objects reside in read-write memory. Classes reside in read-only memory, along with the program code.
Delphi's object model is similar to those in other object-oriented languages, such as C++ and Java. Table 2-1 shows a quick comparison between Delphi and several other popular programming languages.
The following sections explain each of these language features in more detail.
A class declaration is a kind of type declaration. A class declaration describes the fields, methods, and properties of the class. You can declare a class in an interface or implementation section of a unit, but the methods--like any other function or procedure--are defined in the implementation section. You must implement a class's methods in the same unit as the class declaration.
A class declaration has one or more sections for different access levels (private, protected, public, published, or automated). Access levels are discussed later in this chapter. You can mix sections in any order and repeat sections with the same access level.
Within each section, you can have any number of fields, followed by method and property declarations. Method and property declarations can be mixed together, but all fields must precede all methods and properties within each section. Unlike Java and C++, you cannot declare any types nested inside a class declaration.
A class has a single base class, from which it inherits all the fields, properties, and methods. If you do not list an explicit base class, Delphi uses TObject. A class can also implement any number of interfaces. Thus, Delphi's object model most closely resembles that of Java, where a class can extend a single class and implement many interfaces.
A class reference is an expression that refers to a specific class. A class reference is not quite a first class object, as it is in Java or Smalltalk, but is used to create new objects, call class methods, and test or cast an object's type. A class reference is implemented as a pointer to a table of information about the class, especially the class's virtual method table (VMT). (See Chapter 3 for the complete details of what's inside a VMT.)
The most common use for a class reference is to create instances of that class by calling a constructor. You can also use a class reference to test the type of an object (with the is operator) or to cast an object to a particular type (with the as operator). Usually, the class reference is a class name, but it can also be a variable whose type is a metaclass, or a function or property that returns a class reference. Example 2-2 shows an example of a class declaration.
Example 2-2: Declaring a Class and Metaclass
type TComplexClass = class of TComplex; // metaclass type TComplex = class(TPersistent) private fReal, fImaginary: Double; public constructor Create(Re: Double = 0.0); overload; constructor Create(Re, Im: Double); overload; destructor Destroy; override; procedure Assign(Source: TPersistent); override; function AsString: string; published property Real: Double read fReal write fReal; property end;
An object is a dynamic instance of a class. The dynamic instance contains values for all the fields declared in the class and all of its ancestor classes. An object also contains a hidden field that stores a reference to the object's class.
Objects are always allocated dynamically, on the heap, so an object reference is really a pointer to the object. The programmer is responsible for creating objects and for freeing them at the appropriate time. To create an object, use a class reference to call a constructor, for example:
Obj := TSomeClass.Create;
Most constructors are named Create, but that is a convention, not a requirement of Delphi. You will sometimes find constructors with other names, especially older classes that were written before Delphi had method overloading. For maximum compatibility with C++ Builder, which does not let you name constructors, you should stick with Create for all your overloaded constructors.
To get rid of the object when your program no longer needs it, call the Free method. To ensure that the object is properly freed, even if an exception is raised, use a try-finally exception handler. (See Chapter 1, Delphi Pascal, for more information about try-finally.) For example:
Obj := TSomeOtherClass.Create; try Obj.DoSomethingThatMightRaiseAnException; Obj.DoSomethingElse; finally Obj.Free; end;
When freeing a global variable or field, always set the variable to nil when freeing the object so you are not left with a variable that contains an invalid pointer. You should take care to set the variable to nil before freeing the object. If the destructor, or a method called from the destructor, refers to that variable, you usually want the variable to be nil to avoid any potential problems. An easy way to do this is to call the FreeAndNil procedure (from the SysUtils unit):
GlobalVar := TFruitWigglies.Create; try GlobalVar.EatEmUp; finally FreeAndNil(GlobalVar); end;
Each object has a separate copy of all of its fields. A field cannot be shared among multiple objects. If you need to share a variable, declare the variable at the unit level or use indirection: many objects can hold separate pointers or object references that refer to common data.
A class can inherit from another class. The derived class inherits all the fields, methods, and properties of the base class. Delphi supports only single inheritance, so a class has one base class. That base class can have its own base class, and so on, so a class inherits the fields, properties, and methods of every ancestor class. A class can also implement any number of interfaces (which are covered later in this chapter). As in Java, but not C++, every class inherits from a single root class, TObject. If you do not specify an explicit base class, Delphi automatically uses TObject as the base class.
The TObject class declares several methods and one special, hidden field to store a reference to the object's class. This hidden field points to the class's virtual method table (VMT). Every class has a unique VMT and all objects of that class share the class's VMT. Chapter 5, Language Reference, covers the other details of the TObject class and its methods.
You can assign an object reference to a variable whose type is the object's class or any of its ancestor classes. In other words, the declared type of an object reference is not necessarily the same as the actual type of the object. Assignments that go the other way--assigning a base-class object reference to a derived-class variable--are not allowed because the object might not be of the correct type.
Delphi retains the strong type-checking of Pascal, so the compiler performs compile-time checks based on the declared type of an object reference. Thus, all methods must be part of the declared class, and the compiler performs the usual checking of function and procedure arguments. The compiler does not necessarily bind the method call to a specific method implementation. If the method is virtual, Delphi waits until runtime and uses the object's true type to determine which method implementation to call. See the section "Methods," later in this chapter for details.
Use the is operator to test the object's true class. It returns True if the class reference is the object's class or any of its ancestor classes. It returns False if the object reference is nil or of the wrong type. For example:
if Account is TCheckingAccount then ... // tests the class of Account if Account is TObject then ... // True when Account is not nil
You can also use a type cast to obtain an object reference with a different type. A type cast does not change an object; it just gives you a new object reference. Usually, you should use the as operator for type casts. The as operator automatically checks the object's type and raises a runtime error if the object's class is not a descendant of the target class. (The SysUtils unit maps the runtime error to an EInvalidCast exception.)
Another way to cast an object reference is to use the name of the target class in a conventional type cast, similar to a function call. This style of type cast does not check that the cast is valid, so use it only if you know it is safe, as shown in Example 2-3.
Example 2-3: Using Static Type Casts
var Account: TAccount; Checking: TCheckingAccount; begin Account := Checking; // Allowed Checking := Account; // Compile-time error Checking := Account as TCheckingAccount; // Okay Account as TForm; // Raises a runtime error Checking := TCheckingAccount(Account); // Okay, but not recommended if Account is TCheckingAccount then // Better Checking := TCheckingAccount(Account) else Checking := nil;
A field is a variable that is part of an object. A class can declare any number of fields, and each object has its own copy of every field declared in its class and in every ancestor class. In other languages, a field might be called a data member, an instance variable, or an attribute. Delphi does not have class variables, class instance variables, static data members, or the equivalent (that is, variables that are shared among all objects of the same class). Instead, you can usually use unit-level variables for a similar effect.
A field can be of any type unless the field is published. In a published section, a field must have a class type, and the class must have runtime type information (that is, the class or an ancestor class must use the $M+ directive). See Chapter 3 for more information.
When Delphi first creates an object, all of the fields start out empty, that is, pointers are initialized to nil, strings and dynamic arrays are empty, numbers have the value zero, Boolean fields are False, and Variants are set to Unassigned. (See NewInstance and InitInstance in Chapter 5 for details.)
A derived class can declare a field with the same name as a field in an ancestor class. The derived class's field hides the field of the same name in the ancestor class. Methods in the derived class refer to the derived class's field, and methods in the ancestor class refer to the ancestor's field.
Methods are functions and procedures that apply only to objects of a particular class and its descendants. In C++, methods are called "member functions." Methods differ from ordinary procedures and functions in that every method has an implicit parameter called Self, which refers to the object that is the subject of the method call. Self is similar to this in C++ and Java. Call a method the same way you would call a function or procedure, but preface the method name with an object reference, for example:
A class method applies to a class and its descendants. In a class method, Self refers not to an object but to the class. The C++ term for a class method is "static member function."
You can call a method that is declared in an object's class or in any of its ancestor classes. If the same method is declared in an ancestor class and in a derived class, Delphi calls the most-derived method, as shown in Example 2-4.
Example 2-4: Binding Static Methods
type TAccount = class public procedure Withdraw(Amount: Currency); end; TSavingsAccount = class(TAccount) public procedure Withdraw(Amount: Currency); end; var Savings: TSavingsAccount; Account: TAccount; begin ... Savings.Withdraw(1000.00); // Calls TSavingsAccount.Withdraw Account.Withdraw(1000.00); // Calls TAccount.Withdraw
An ordinary method is called a static method because the compiler binds the method call directly to a method implementation. In other words, the binding is static. In C++ this is an ordinary member function, and in Java it's called a "final method." Most Delphi programmers refrain from using the term static method, preferring the simple term, method or even non-virtual method.
A virtual method is a method that is bound at runtime instead of at compile time. At compile time, Delphi uses the declared type of an object reference to determine which methods you are allowed to call. Instead of compiling a direct reference to any specific method, the compiler stores an indirect method reference that depends on the object's actual class. At runtime, Delphi looks up the method in the class's runtime tables (specifically, the VMT), and calls the method for the actual class. The object's true class might be the compile-time declared class, or it might be a derived class--it doesn't matter because the VMT provides the pointer to the correct method.
To declare a virtual method, use the virtual directive in the base class, and use the override directive to provide a new definition of the method in a derived class. Unlike in Java, methods are static by default, and you must use the virtual directive to declare a virtual method. Unlike in C++, you must use the override directive to override a virtual method in a derived class.
Example 2-5 uses virtual methods.
Example 2-5: Binding Virtual Methods
type TAccount = class public procedure Withdraw(Amount: Currency); virtual; end; TSavingsAccount = class(TAccount) public procedure Withdraw(Amount: Currency); override; end; var Savings: TSavingsAccount; Account: TAccount; begin ... Savings.Withdraw(1000.00); // Calls TSavingsAccount.Withdraw Account := Savings; Account.Withdraw(1000.00); // Calls TSavingsAccount.Withdraw
Instead of using the virtual directive, you can also use the dynamic directive. The semantics are identical, but the implementation is different. Looking up a virtual method in a VMT is fast because the compiler generates an index directly into a VMT. Looking up a dynamic method is slower. Calling a dynamic method requires a linear search of a class's dynamic method table (DMT). If the class does not override that method, the search continues with the DMT of the base class. The search continues with ancestor classes until TObject is reached or the method is found. The tradeoff is that in a few circumstances, dynamic methods take up less memory than virtual methods. Unless you are writing a replacement for the VCL, you should use virtual methods, not dynamic methods. See Chapter 3 for a complete explanation of how dynamic and virtual methods are implemented.
A virtual or dynamic method can be declared with the abstract directive, in which case the class does not define the method. Instead, derived classes must override that method. The C++ term for an abstract method is a "pure virtual method." If you call a constructor for a class that has an abstract method, the compiler issues a warning, telling you that you probably made a mistake. You probably wanted to create an instance of a derived class that overrides and implements the abstract method. A class that declares one or more abstract methods is often called an abstract class, although some people reserve that term for a class that declares only abstract methods.
type TBaseAbstract = class procedure Method; virtual; abstract; end; TDerivedAbstract = class(TBaseAbsract) procedure Method; override; abstract; end; TConcrete = class(TDerivedAbstract) procedure Method; override; end;
A class method or constructor can also be virtual. In Delphi, class references are real entities that you can assign to variables, pass as parameters, and use as references for calling class methods. If a constructor is virtual, a class reference can have a static type of the base class, but you can assign to it a class reference for a derived class. Delphi looks up the virtual constructor in the class's VMT and calls the constructor for the derived class.
Methods (and other functions and procedures) can be overloaded, that is, multiple routines can have the same name, provided they take different arguments. Declare overloaded methods with the overload directive. A derived class can overload a method it inherits from a base class. In that case, only the derived class needs the overload directive. After all, the author of the base class cannot predict the future and know when other programmers might want to overload an inherited method. Without the overload directive in the derived class, the method in the derived class hides the method in the base class, as shown in Example 2-6.
Example 2-6: Overloading Methods
type TAuditKind = (auInternal, auExternal, auIRS, auNasty); TAccount = class public procedure Audit; end; TCheckingAccount = class(TAccount) public procedure Audit(Kind: TAuditKind); // Hides TAccount.Audit end; TSavingsAccount = class(TAccount) public // Can call TSavingsAccount.Audit and TAccount.Audit procedure Audit(Kind: TAuditKind); overload; end; var Checking: TCheckingAccount; Savings: TSavingsAccount; begin Checking := TCheckingAccount.Create; Savings := TSavingsAccount.Create; Checking.Audit; // Error because TAccount.Audit is hidden Savings.Audit; // Okay because Audit is overloaded Savings.Audit(auNasty); // Okay Checking.Audit(auInternal); // Okay
Every class has one or more constructors, possibly inherited from a base class. By convention, constructors are usually named Create, although you can use any name you like. Some constructor names start with Create, but convey additional information, such as CreateFromFile or CreateFromStream. Usually, though, the simple name Create is sufficient, and you can use method overloading to define multiple constructors with the same name. Another reason to overload the name Create is for compatibility with C++ Builder. C++ does not permit different constructor names, so you must use overloading to define multiple constructors.
Calling a constructor
A constructor is a hybrid of object and class methods. You can call it using an object reference or a class reference. Delphi passes an additional, hidden parameter to indicate how it was called. If you call a constructor using a class reference, Delphi calls the class's NewInstance method to allocate a new instance of the class. After calling NewInstance, the constructor continues and initializes the object. The constructor automatically sets up a try-except block, and if any exception occurs in the constructor, Delphi calls the destructor.
When you call a constructor with an object reference, Delphi does not set up the try-except block and does not call NewInstance. Instead, it calls the constructor the same way it calls any ordinary method. This lets you call an inherited constructor without unnecessary overhead.
var Account: TSavingsAccount; begin Account.Create; // wrong Account := TSavingsAccount.Create; // right
One of Delphi's features is that you have total control over when, how, and whether to call the inherited constructor. This lets you write some powerful and interesting classes, but also introduces an area where it is easy to make mistakes.
Delphi always constructs the derived class first, and only if the derived class calls the inherited constructor does Delphi construct the base class. C++ constructs classes in the opposite direction, starting from the ancestor class and constructing the derived class last. Thus, if class C inherits from B, which inherits from A, Delphi constructs C first, then B, and A last. C++ constructs A first, then B, and finally C.
Virtual methods and constructors
Another significant difference between C++ and Delphi is that in C++, a constructor always runs with the virtual method table of the class being constructed, but in Delphi, the virtual methods are those of the derived class, even when the base class is being constructed. As a result, you must be careful when writing any virtual method that might be called from a constructor. Unless you are careful, the object might not be fully constructed when the method is called. To avoid any problems, you should override the AfterConstruction method and use that for any code that needs to wait until the object is fully constructed. If you override AfterConstruction, be sure to call the inherited method, too.
One constructor can call another constructor. Delphi can tell the call is from an object reference (namely, Self), so it calls the constructor as an ordinary method. The most common reason to call another constructor is to put all the initialization code in a single constructor. Example 2-7 shows some different ways to define and call constructors.
Example 2-7: Declaring and Calling Constructors
type TCustomer = class ... end; TAccount = class private fBalance: Currency; fNumber: Cardinal; fCustomer: TCustomer; public constructor Create(Customer: TCustomer); virtual; destructor Destroy; override; end; TSavingsAccount = class(TAccount) private fInterestRate: Integer; // Scaled by 1000 public constructor Create(Customer: TCustomer); override; overload; constructor Create(Customer: TCustomer; InterestRate: Integer); overload; // Note that TSavingsAccount does not need a destructor. It simply // inherits the destructor from TAccount. end; var AccountNumber: Cardinal = 1; constructor TAccount.Create(Customer: TCustomer); begin inherited Create; // Call TObject.Create. fNumber := AccountNumber; // Assign a unique account number. Inc(AccountNumber); fCustomer := Customer; // Notify customer of new account. Customer.AttachAccount(Self); end; destructor TAccount.Destroy; begin // If the constructor fails before setting fCustomer, the field // will be nil. Release the account only if Customer is not nil. if Customer <> nil then Customer.ReleaseAccount(Self); // Call TObject.Destroy. inherited Destroy; end; const DefaultInterestRate = 5000; // 5%, scaled by 1000 constructor TSavingsAccount.Create(Customer: TCustomer); begin // Call a sibling constructor. Create(Customer, DefaultInterestRate); end; constructor TSavingsAccount(Customer: TCustomer; InterestRate:Integer); begin // Call TAccount.Create. inherited Create(Customer); fInterestRate := InterestRate; end;
Destructors, like constructors, take an extra hidden parameter. The first call to a destructor passes True for the extra parameter. This tells Delphi to call FreeInstance to free the object. If the destructor calls an inherited destructor, Delphi passes False as the hidden parameter to prevent the inherited destructor from trying to free the same object.
Before Delphi starts the body of the destructor, it calls the virtual method, BeforeDestruction. You can override BeforeDestruction to assert program state or take care of other business that must take place before any destructor starts. This lets you write a class safely without worrying about how or whether any derived classes will call the base class destructor.
If a constructor or AfterConstruction method raises an exception, Delphi automatically calls the object's destructor. When you write a destructor, you must remember that the object being destroyed might not have been completely constructed. Delphi ensures that all fields start out at zero, but if the exception occurs in the middle of your constructor, some fields might be initialized and some might still be zero. If the destructor just frees objects and pointers, you don't need to worry, because the Free method and FreeMem procedure both check for nil pointers. If the destructor calls other methods, though, always check first for a nil pointer.