Class types
A class type is a structure consisting of a fixed number of components. The possible components of a class are fields, methods, and properties. Unlike other types, a class type can be declared only in a type declaration part in the outermost scope of a program or unit. Therefore, a class type can't be declared in a variable declaration part or within a procedure, function, or method block.
object type
visibility specifier
heritage
component list
field definition
method definition
method heading
method directives
Instances and references
An instance of a class type is a dynamically allocated block of memory with a layout defined by the class type. Instances of a class type are also commonly referred to as objects. Objects are created using constructors, and destroyed using destructors.
Each object of a class type has a unique copy of the fields declared in the class type, but all share the same methods.
A variable of a class type contains a reference to an object of the class type. The variable doesn’t contain the object itself, but rather is a pointer to the memory block that has been allocated for the object. Like pointer variables, multiple class type variables can refer to the same object. Furthermore, a class type variable can contain the value nil, indicating that it doesn’t currently reference an object.
Note Unlike a pointer variable, it isn’t necessary to de-reference a class type variable to gain access to the referenced object. In other words, while it is necessary to write Ptr^.Field to access a field in a dynamically allocated record, the ^ operator is implied when accessing a component of an object, and the syntax is simply Instance.Field.
Throughout this book, the term object reference is used to denote a value of a class type. For example, a class-type variable contains an object reference, and a constructor returns an object reference.
In addition, the term class reference is used to denote a value of a class-reference type. For example, a class-type identifier is a class reference, and a variable of a class-reference type contains a class reference. Class-reference types are described in further detail on page 108.
Class components
The components of a class are fields, methods, and properties. Class components are sometimes referred to as members.
Fields
A field declaration in a class defines a data item that exists in each instance of the class. This is similar to a field of a record.
Methods
A method is a procedure or function that performs an operation on an object. Part of the call to a method specifies the object the method should operate on.
The declaration of a method within a class type corresponds to a forward declaration of that method. This means that somewhere after the class-type declaration, and within the same module, the method must be implemented by a defining declaration.
Within the implementation of a method, the identifier Self represents an implicit parameter that references the object for which the method was invoked.
Constructors and destructors are special methods that control construction and destruction of objects.
A constructor defines the actions associated with creating an object. When invoked, a constructor acts as a function that returns a reference to a newly allocated and initialized instance of the class type.
A destructor defines the actions associated with destroying an object. When invoked, a destructor will deallocate the memory that was allocated for the object.
A class method is a procedure or function that operates on a class reference instead of an object reference.
Properties
A property declaration in a class defines a named attribute for objects of the class and the actions associated with reading and writing the attribute. Properties are described in depth beginning on page 101
Inheritance
A class type can inherit components from another class type. If T2 inherits from T1, then T2 is a descendant of T1, and T1 is an ancestor of T2. Inheritance is transitive; that is, if T3 inherits from T2, and T2 inherits from T1, then T3 also inherits from T1. The domain of a class type consists of itself and all of its descendants.
A descendant class implicitly contains all the components defined by its ancestor classes. A descendant class can add new components to those it inherits. However, it can’t remove the definition of a component defined in an ancestor class.
The predefined class type TObject is the ultimate ancestor of all class types. If the declaration of a class type doesn’t specify an ancestor type (that is, if the heritage part of the class declaration is omitted), the class type will be derived from TObject. TObject is declared by the System unit, and defines a number of methods that apply to all classes. For a description of these methods, see “The TObject and TClass types” on page 111.
Components and scope
The scope of a component identifier declared in a class type extends from the point of declaration to the end of the class-type definition, and extends over all descendants of the class type and the blocks of all method declarations of the class type. Also, the scope of component identifiers includes field, method, and property designators, and with statements that operate on variables of the given class type.
A component identifier declared in a class type can be redeclared in the block of a method declaration of the class type. In that case, the Self parameter can be used to access the component whose identifier was redeclared.
A component identifier declared in an ancestor class type can be redeclared in a descendant of the class type. Such redeclaration effectively hides the inherited component, although the inherited keyword can be used to bring the inherited component back into scope.
Forward references
The declaration of a class type can specify the reserved word class and nothing else, in which case the declaration is a forward declaration. A forward declaration must be resolved by a normal declaration of the class within the same type declaration part. Forward declarations allow mutually dependent classes to be declared. For example:
type
TFigure = class; TDrawing = class
Figure: TFigure;
:
end;
TFigure = class
Drawing: TDrawing;
:
end;
Class type compatibility rules
A class type is assigment-compatible with any ancestor class type; therefore, during program execution, a class-type variable can reference an instance of that type or an instance of any descendant type. For example, given the declarations
type
TFigure = class
:
end;
TRectangle = class(TFigure)
:
end;
TRoundRect = class(TRectangle)
:
end;
TEllipse = class(TFigure)
:
end;
a value of type TRectangle can be assigned to variables of type TRectangle, TFigure, and TObject, and during execution of a program, a variable of type TFigure might be either nil or reference an instance of TFigure, TRectangle, TRoundRect, TEllipse, or any other instance of a descendant of TFigure.
Component visibility
The visibility of a component identifier is governed by the visibility attribute of the component section that declares the identifier. The four possible visibility attributes are published, public, protected, and private.
Component identifiers declared in the component list that immediately follows the class type heading have the published visibility attribute if the class type is compiled in the {$M+} state or is derived from a class that was compiled in the {$M+} state.
Otherwise, such component identifiers have the public visibility attribute.
Public components
Component identifiers declared in public sections have no special restrictions on their visibility.
Published components
The visibility rules for published components are identical to those of public components. The only difference between published and public components is that run-time type information is generated for fields and properties that are declared in a published section. This run-time type information enables an application to dynamically query the fields and properties of an otherwise unknown class type.
Note The Delphi Visual Component Library uses run-time type information to access the values of a component's properties when saving a loading form files. Also, the Delphi development environment uses a component's run-time type information to determine the list of properties shown in the Object Inspector.
A class type cannot have published sections unless it is compiled in the {$M+} state or is derived from a class that was compiled in the {$M+} state. The $M compiler directive controls the generation of run-time type information for a class. For further details on $M, see Appendix B.
Fields defined in a published section must be of a class type. Fields of all other types are restricted to public, protected, and private sections.
Properties defined in a published section cannot be array properties. Furthermore, the type of a property defined in a published section must be an ordinal type, a real type (Single, Double, Extended, or Comp, but not Real), a string type, a small set type, a class type, or a method pointer type. A small set type is a set type with a base type whose lower and upper bounds have ordinal values between 0 and 15. In other words, a small set type is a set that fits in a byte or a word.
Protected components
When accessing through a class type declared in the current module, the protected component identifiers of the class and its ancestors are visible. In all other cases, protected component identifiers are hidden.
Access to protected components of a class is restricted to the implementation of methods of the class and its descendants. Therefore, components of a class that are intended for use only in the implementation of derived classes are usually declared as protected.
Private components
The visibility of a component identifier declared in a private component section is restricted to the module that contains the class-type declaration. In other words, private component identifiers act like normal public component identifiers within the module that contains the class-type declaration, but outside the module, any private component identifiers are unknown and inaccessible. By placing related class types in the same module, these class types can gain access to each other's private components without making the private components known to other modules.
Static methods
Methods declared in a class type are by default static. When a static method is called, the declared (compile-time) type of the class or object used in the method call determines which method implementation to activate. In the following example, the Draw methods are static:
type
TFigure = class procedure Draw;
:
end;
TRectangle = class(TFigure) procedure Draw;
:
end;
The following section of code illustrates the effect of calling a static method. Even though in the second call to Figure.Draw the Figure variable references an object of
class TRectangle, the call invokes the implementation of TFigure.Draw because the declared type of the Figure variable is TFigure.
var
Figure: TFigure;
Rectangle: TRectangle;
begin
Figure := TFigure.Create;
Figure.Draw; { Invokes TFigure.Draw } Figure.Destroy;
Figure := TRectangle.Create;
Figure.Draw; { Invokes TFigure.Draw } Figure.Destroy;
Rectangle := TRectangle.Create;
Rectangle.Draw; { Invokes TRectangle.Draw } Rectangle.Destroy;
end;
Virtual methods
A method can be made virtual by including a virtual directive in its declaration. When a virtual method is called, the actual (run-time) type of the class or object used in the method call determines which method implementation to activate. The following is an example of a declaration of a virtual method:
type
TFigure = class
procedure Draw; virtual;
:
end;
A virtual method can be overridden in a descendant class. When an override directive is included in the declaration of a method, the method overrides the inherited implementation of the method. An override of a virtual method must match exactly the order and types of the parameters, and the function result type (if any), of the original method.
The only way a virtual method can be overridden is through the override directive. If a method declaration in a descendant class specifies the same method identifier as an inherited method, but doesn’t specify an override directive, the new method declaration will hide the inherited declaration, but not override it.
Assuming the declaration of class TFigure above, the following two descendant classes override the Draw method:
type
TRectangle = class(TFigure) procedure Draw; override;
:
end;
TEllipse = class(TFigure) procedure Draw; override;
:
end;
The following section of code illustrates the effect of calling a virtual method through a class-type variable whose actual type varies at run time:
var
Figure: TFigure;
begin
Figure := TRectangle.Create;
Figure.Draw; { Invokes TRectangle.Draw } Figure.Destroy;
Figure := TEllipse.Create;
Figure.Draw; { Invokes TEllipse.Draw } Figure.Destroy;
end;
Dynamic methods
A method is made dynamic by including a dynamic directive in its declaration. Dynamic methods are semantically identical to virtual methods. Virtual and dynamic methods differ only in the implementation of method call dispatching at run time; for all other purposes, the two types of methods can be considered equivalent.
In the implementation of virtual methods, the compiler favors speed of call dispatching over code size. The implementation of dynamic methods on the other hand favors code size over speed of call dispatching.
In general, virtual methods are the most efficient way to implement polymorphic behavior. Dynamic methods are useful only in situations where a base class declares a large number of virtual methods, and an application declares a large number of descendant classes with few overrides of the inherited virtual methods.
Abstract methods
An abstract method is a virtual or dynamic method whose implementation isn’t defined in the class declaration in which it appears; its definition is instead deferred to descendant classes. An abstract method in effect defines an interface, but not the underlying operation.
A method is abstract if an abstract directive is included in its declaration. A method can be declared abstract only if it is first declared virtual or dynamic. The following is an example of a declaration of an abstract method.
type
TFigure = class
procedure Draw; virtual; abstract;
:
end;
An override of an abstract method is identical to an override of a normal virtual or dynamic method, except that in the implementation of the overriding method, an inherited method isn’t available to call.
Calling an abstract method through an object that hasn't overridden the method will generate an exception at run time.
Method activations
A method is activated (or called) through a function call or procedure statement consisting of a method designator followed by an actual parameter list. This type of call is known as method activation.
method designator
The variable reference specified in a method designator must denote an object reference or a class reference, and the method identifier must denote a method of that class type.
The instance denoted by a method designator becomes an implicit actual parameter of the method; it corresponds to a formal parameter named Self that possesses the class type corresponding to the activated method.
Within a with statement that references an object or a class, the variable-reference part of a method designator can be omitted. In that case, the implicit Self parameter of the method activation becomes the instance referenced by the with statement.
Method implementations
The declaration of a method within a class type corresponds to a forward declaration of that method. Somewhere after the class-type declaration, and within the same module, the method must be implemented by a defining declaration. For example, given the class-type declaration
type
TFramedLabel = class(TLabel) protected
procedure Paint; override; end;
the Paint method must later be implemented by a defining declaration. For example,
procedure TFramedLabel.Paint;
begin
inherited Paint; with Canvas do begin
Brush.Color := clWindowText; Brush.Style := bsSolid; FrameRect(ClientRect);
end; end;
For procedure and function methods, the defining declaration takes the form of a normal procedure or function declaration, except that the procedure or function identifier in the heading is a qualified method identifier. A qualified method identifier consists of a class-type identifier followed by a period (.) and then by a method identifier.
For constructors and destructors, the defining declaration takes the form of a procedure method declaration, except that the procedure reserved word is replaced by constructor or destructor.
A method's defining declaration can optionally repeat the formal parameter list of the method heading in the class type. The defining declaration's method heading must match exactly the order, types, and names of the parameters, and the type of the function result, if any.
In the defining declaration of a method, there is always an implicit parameter with the identifier Self, corresponding to a formal parameter of the class type. Within the method block, Self represents the instance for which the method was activated.
The scope of a component identifier in a class type extends over any procedure, function, constructor, or destructor block that implements a method of the class type. The effect is the same as if the entire method block was embedded in a with statement of the form:
with Self do begin
:
:
end;
Within a method block, the reserved word inherited can be used to access redeclared and overridden component identifiers. For example, in the implementation of the TFramedLabel.Paint method above, inherited is used to invoke the inherited implementation of the Paint method. When an identifier is prefixed with inherited, the search for the identifier begins with the immediate ancestor of the enclosing method's class type.
Constructors and destructors
Constructors and destructors are special methods that control construction and destruction of objects. A class can have zero or more constructors and destructors for objects of the class type. Each is specified as a component of the class in the same way as a procedure or function method, except that the reserved words constructor and destructor begin each declaration instead of procedure and function. Like other methods, constructors and destructors can be inherited.
Constructors
Constructors are used to create and initialize new objects. Typically, the initialization is based on values passed as parameters to the constructor.
Unlike an ordinary method, which must be invoked on an object reference, a constructor can be invoked on either a class reference or an object reference.
To create a new object, a constructor must be invoked on a class reference. When a constructor is invoked on a class reference, the following actions take place:
-
Storage for a new object is allocated from the heap.
-
The allocated storage is cleared. This causes the ordinal value of
all ordinal type fields to become zero, the value of all pointer and class-type fields to become nil, and the value of all string fields to become empty.
-
The user-specified actions of the constructor are executed.
-
A reference to the newly allocated and initialized object is
returned from the constructor. The type of the returned value is the same as the class type specified in the constructor call.
When a constructor is invoked on an object reference, the constructor acts like a normal procedure method. This means that a new object is not allocated and cleared, and that the constructor call does not return an object reference. Instead, the constructor operates on the specified object reference, and only executes the user specified actions given in the constructor's statement part. A constructor is typically invoked on an object reference only in conjunction with the inherited keyword to execute an inherited constructor.
constructor declaration
constructor heading
An example of a class type and its associated constructor follows below:
type
TShape = class(TGraphicControl) private
FPen: TPen;
FBrush: TBrush;
procedure PenChanged(Sender: TObject);
procedure BrushChanged(Sender: TObject);
public
constructor Create(Owner: TComponent); override; destructor Destroy; override;
:
end;
constructor TShape.Create(Owner: TComponent);
begin
inherited Create(Owner); { Initialize inherited parts }
Width := 65; { Change inherited properties } Height := 65;
FPen := TPen.Create; { Initialize new fields } FPen.OnChange := PenChanged;
FBrush := TBrush.Create; FBrush.OnChange := BrushChanged;
end;
The first action of a constructor is almost always to call an inherited constructor to initialize the inherited fields of the object. Following that, the constructor then initializes the fields of the object that were introduced in the class. Because a constructor always clears the storage it allocates for a new object, all fields automatically have a default value of zero (ordinal types), nil (pointer and class types), or empty (string types). Unless a field's default value is non-zero, there is no need to initialize the field in a constructor.
If an exception occurs during execution of a constructor that was invoked on a class reference, the Destroy destructor is automatically called to destroy the unfinished object. The effect is the same as if the entire statement part of the constructor were embedded in a try...finally statement of this form:
try
: { User defined actions }
:
except { On any exception }
Destroy; { Destroy unfinished object }
raise; { Re-raise exception }
end;
Like other methods, constructors can be virtual. When invoked through a class-type identifier, as is usually the case, a virtual constructor is equivalent to a static constructor. When combined with class-reference types, however, virtual constructors allow polymorphic construction of objectsthat is, construction of objects whose types aren’t known at compile time, as described on page 109.
Destructors
Destructors are used to destroy objects. When a destructor is invoked, the user- defined actions of the destructor are executed, and then the storage that was allocated for the object is disposed of. The user-defined actions of a destructor typically consist of destroying any embedded objects and releasing any resources that were allocated by the object.
destructor declaration
destructor heading
The following example shows how the destructor that was declared for the TShape
class in the preceding section might be implemented.
destructor TShape.Destroy;
begin
FBrush.Free; FPen.Free; inherited Destroy;
end;
The last action of a destructor is typically to call the inherited destructor to destroy the inherited fields of the object.
While it is possible to declare multiple destructors for a class, it is recommended that classes only implement overrides of the inherited Destroy destructor. Destroy is a parameterless virtual destructor declared in TObject, and because TObject is the ultimate ancestor of every class, the Destroy destructor always available for any object.
As described in the preceding section on constructors, if an exception occurs during the execution of a constructor, the Destroy destructor is invoked to destroy the unfinished object. This means that destructors must be prepared to handle destruction of partially constructed objects. Because a constructor sets all fields of a new object to null values before executing any user defined actions, any class-type or pointer-type fields in a partially constructed object are always nil. A destructor should therefore always check for nil values before performing operations on class- type or pointer-type fields.
Referring to the TShape.Destroy destructor mentioned earlier, note that the Free method (which is inherited from TObject) is used to destroy the objects referenced by the FPen and FBrush fields. The implementation of the Free method is
procedure TObject.Free;
begin
if Self <> nil then Destroy;
end;
The Free method is a convenient way of checking for nil before invoking Destroy on an object reference. By calling Free instead of Destroy for any class-type fields, a destructor is automatically prepared to handle partially constructed objects resulting from constructor exceptions. For that same reason, direct calls to Destroy aren’t recommended.
Class operators
Object Pascal defines two operators, is and as, that operate on class and object references.
The is operator
The is operator is used to perform dynamic type checking. Using the is operator, you can check whether the actual (run-time) type of an object reference belongs to a particular class. The syntax of the is operator is
ObjectRef is ClassRef
where ObjectRef is an object reference and ClassRef is a class reference. The is operator returns a boolean value. The result is True if ObjectRef is an instance of the class denoted by ClassRef or an instance of a class derived from the class denoted by ClassRef. Otherwise, the result is False. If ObjectRef is nil, the result is always False. If the declared types of ObjectRef and ClassRef are known not to be relatedthat is if the declared type of ObjectRef is known not to be an ancestor of, equal to, or a descendant of ClassRefthe compiler reports a type-mismatch error.
The is operator is often used in conjunction with an if statement to perform a
guarded typecast. For example,
if ActiveControl is TEdit then TEdit(ActiveControl).SelectAll;
Here, if the is test is True, it is safe to typecast ActiveControl to be of class TEdit.
The rules of operator precedence group the is operator with the relational operators (=, <>, <, >, <=, >=, and in). This means that when combined with other boolean expressions using the and and or operators, is tests must be enclosed in parentheses:
if (Sender is TButton) and (TButton(Sender).Tag <> 0) then ...;
The as operator
The as operator is used to perform checked typecasts. The syntax of the as operator is
ObjectRef as ClassRef
where ObjectRef is an object reference and ClassRef is a class reference. The resulting value is a reference to the same object as ObjectRef, but with the type given by ClassRef. When evaluated at run time, ObjectRef must be nil, an instance of the class denoted by ClassRef, or an instance of a class derived from the class denoted by ClassRef. If none of these conditions are True, an exception is raised. If the declared types of ObjectRef and ClassRef are known not to be relatedthat is, if the declared type of ObjectRef is known not to be an ancestor of, equal to, or a descendant of ClassRefthe compiler reports a type-mismatch error.
The as operator is often used in conjunction with a with statement. For example,
with Sender as TButton do begin
Caption := '&Ok';
OnClick := OkClick;
end;
The rules of operator precedence group the as operator with the multiplying operators (*, /, div, mod, and, shl, and shr). This means that when used in a variable reference, an as typecast must be enclosed in parentheses:
(Sender as TButton).Caption := '&Ok';
Message handling
Message handler methods are used to implement user-defined responses to dynamically dispatched messages. Delphi's Visual Class Library uses message handler methods to implement Windows message handling.
Message handler declarations
A message handler method is defined by including a message directive in the method declaration.
type
TTextBox = class(TCustomControl) private
procedure WMChar(var Message: TWMChar); message WM_CHAR;
...
end;
A message handler method must be a procedure that takes a single variable parameter, and the message keyword must be followed by an integer constant between 0 and 32767 which specifies the message ID.
Note When declaring a message handler method for a VCL control, the integer constant specified in the message directive must be a Windows message ID. The Messages unit defines all Windows message IDs and their corresponding message records.
In contrast to a regular method, a message handler method does not have to specify an override directive in order to override an inherited message handler. In fact, a message handler override doesn't even have to specify the same method identifier and parameter name and type as the method it overrides. The message ID solely determines which message the method will respond to, and whether or not it is an override.
Message handler implementations
The implementation of a message handler method corresponds to that of a normal method. For example, the TTextBox.WMChar message handler method defined above might be implemented as follows
procedure TTextBox.WMChar(var Message: TWMChar);
begin
if Chr(Message.CharCode) = #13 then
ProcessEnter
else
inherited;
end;
The implementation of a message handler method can call the inherited implementation using an inherited statement as shown above. The inherited statement automatically passes the message record as a parameter to the inherited method. The inherited statement invokes the first message handler with the same message ID found in the most derived ancestor class (that is, the ancestor class that is closest to the class in the inheritance hierarchy). If none of the ancestor classes implement a message handler for the given message ID, the inherited statement instead calls the DefaultHandler virtual method, which is inherited from TObject and therefore present in any class.
The effect of the inherited statement is that for a particular message ID, a class does not need to know whether its parent classes implement a handler for the message– an inherited implementation always appears to be available.
Message dispatching
Message handler methods are typically not called directly. Instead, messages are dispatched to an object using the Dispatch method defined in the TObject class.
Dispatch is declared as
procedure TObject.Dispatch(var Message);
The Message parameter passed to Dispatch must be a record, and the first entry in the record must be a field of type Cardinal which contains the message ID of the message being dispatched. For example
type
TMessage = record
Msg: Cardinal;
...
end;
A message record can contain any number of additional fields that define message specific information.
A call to Dispatch invokes the most derived implementation of a message handler for the given message ID. In other words, Dispatch invokes the first message handler with a matching message ID found by examining the class itself, its ancestor, its ancestor's ancestor, and so on until TObject is reached. If the class and its ancestors do not define a handler for the given message ID, Dispatch will instead invoke the DefaultHandler method.
The DefaultHandler method is declared in TObject as follows
procedure DefaultHandler(var Message); virtual;
The implementation of DefaultHandler in TObject simply returns without performing any actions. By overriding DefaultHandler, a class can implement default handling of messages. As described above, DefaultHandler is invoked when Dispatch is called to
dispatch a message for which the class implements no message handler. DefaultHandler is also invoked when a message handler method executes an inherited statement for which no inherited message handler exists.
Note The DefaultHandler method for a VCL control invokes the DefWindowProc default message handling function defined by Windows.
Properties
A property definition in a class declares a named attribute for objects of the class and the actions associated with reading and writing the attribute. Examples of properties are the caption of a form, the size of a font, the name of a database table, and so on.
Properties are a natural extension of fields in an object. Both can be used to express attributes of an object, but whereas fields are merely storage locations which can be examined and modified at will, properties provide greater control over access to attributes, they provide a mechanism for associating actions with the reading and writing of attributes, and they allow attributes to be computed.
property definition
property interface
property parameter list
property specifiers
read specifier
write specifier
stored specifier
default specifier
field or method
Property definitions
The definition of a property specifies the name and type of the property, and the actions associated with reading (examining) and writing (modifying) the property. A property can be of any type except a file type.
The declarations below define an imaginary TCompass control which has a Heading property that can assume values from 0 to 359 degrees. The definition of the Heading property further states that its value is read from the FHeading field, and that its value is written using the SetHeading method.
type
THeading = 0..359; TCompass = class(TControl) private
FHeading: THeading;
procedure SetHeading(Value: THeading);
published
property Heading: THeading read FHeading write SetHeading;
:
end;
Property access
When a property is referenced in an expression, its value is read using the field or method listed in the read specifier, and when a property is referenced in an assignment statement, its value is written using the field or method listed in the write specifier. For example, assuming that Compass is an object reference of the TCompass type defined above, the statements
if Compass.Heading = 180 then GoingSouth; Compass.Heading := 135;
correspond to
if Compass.FHeading = 180 then GoingSouth; Compass.SetHeading(135);
In the TCompass class, no action is associated with reading the Heading property. The read operation simply consists of examining the value stored in the FHeading field. On the other hand, assigning a value to the Heading property translates into a call to the SetHeading method, which not only stores the new value in the FHeading field,
but also performs whatever actions are required to update the user interface of the compass control. For example, SetHeading might be implemented as this:
procedure TCompass.SetHeading(Value: THeading);
begin
if FHeading <> Value then begin
FHeading := Value;
Repaint;
end; end;
Note Unlike fields, properties can’t be passed as variable parameters, and it isn’t possible to take the address of a property using the @ operator. This is true even if the read and write specifiers both list a field identifier, thereby ensuring that a future implementation of the property is free to change one or both access specifiers to list a method.
Access specifiers
The read and write specifiers of a property definition control how a property is accessed. A property definition must include at least a read or a write specifier, but isn’t required to include both. If a property definition includes only a read specifer, then the property is said to be read-only property, and if a property definition includes only a write specifier, then the property is said to be write-only property. If a property definition includes both a read and a write specifier, the property is said to be read-write property. It is an error to assign a value to a read-only property.
Likewise, it is an error to use a write-only property in an expression.
The read or write keyword in an access specifier must be followed by a field identifier or a method identifier. The field or method can belong to the class type in which the property is defined, in which case the field or method definition must precede the property definition, or it can belong to an ancestor class, in which case the field or method must be visible in the class containing the property definition.
Fields and methods listed in access specifiers are governed by the following rules:
-
If an access specifier lists a field identifier, then the field type
must be identical to the property type.
-
If a read specifier lists a method identifier, then the method
must be a parameterless function method, and the function result type must be identical to the property type.
-
If a write specifier lists a method identifier, then the method
must be a procedure method that takes a single value or constant parameter of the same type as the property type.
For example, if a property is defined as
property Color: TColor read GetColor write SetColor;
then preceding the property definition, the GetColor method must be defined as
function GetColor: TColor;
and the SetColor method must be defined as one of these:
procedure SetColor(Value: TColor);
procedure SetColor(const Value: TColor);
Array properties
Array properties allow the implementation of indexed properties. Examples of array properties include the items of a list, the child controls of a control, and the pixels of a bitmap.
The definition of an array property includes an index parameter list which specifies the names and types of the indexes of the array property. For example,
property Objects[Index: Integer]: TObject
read GetObject write SetObject;
property Pixels[X, Y: Integer]: TColor
read GetPixel write SetPixel;
property Values[const Name: string]: string read GetValue write SetValue;
The format of an index parameter list is the same as that of a procedure or function's formal parameter list, except that the parameter declarations are enclosed in square brackets instead of parentheses. Note that unlike array types, which can only specify ordinal type indexes, array properties allow indexes of any type. For example, the Values property declared previously might represent a lookup table in which a string index is used to look up a string value.
An access specifier of an array property must list a method identifier. In other words, the read and write specifiers of an array property aren’t allowed to specify a field name. The methods listed in array property access specifiers are governed by the following rules:
-
The method listed in the read specifier of an array property
must be a function that takes the same number and types of parameters as are listed in the property's index parameter list, and the function result type must be identical to the property type.
-
The method listed in the write specifier of an array property
must be a procedure with the same number and types of parameters as are listed in the property's index parameter list, plus an additional value or constant parameter of the same type as the property type.
For example, for the array properties declared above, the access methods might be declared as
function GetObject(Index: Integer): TObject; function GetPixel(X, Y: Integer): TColor; function GetValue(const Name: string): string;
procedure SetObject(Index: Integer; Value: TObject); procedure SetPixel(X, Y: Integer; Value: TColor); procedure SetValue(const Name, Value: string);
An array property is accessed by following the property identifier with a list of actual parameters enclosed in square brackets. For example, the statements
if Collection.Objects[0] = nil then Exit; Canvas.Pixels[10, 20] := clRed; Params.Values['PATH'] := 'C:\DELPHI\BIN';
correspond to
if Collection.GetObject(0) = nil then Exit; Canvas.SetPixel(10, 20, clRed); Params.SetValue('PATH', 'C:\DELPHI\BIN');
The definition of an array property can be followed by a default directive, in which case the array property becomes the default array property of the class. For example
type
TStringArray = class public
property Strings[Index: Integer]: string ...; default;
:
end;
If a class has a default array property, then access to the array property of the form Instance.Property[...] can be abbreviated to Instance[...]. For example, given that StringArray is an object reference of the TStringArray class defined above, the construct
StringArray.Strings[Index]
can be abbreviated to
StringArray[Index]
When an object reference is followed by a list of indexes enclosed in square brackets, the compiler automatically selects the class type's default array property, or issues an error if the class type has no default array property.
If a class defines a default array property, derived classes automatically inherit the default array property. It isn’t possible for a derived class to redeclare or hide the default array property.
Index specifiers
The definition of a property can optionally include an index specifier. Index specifiers allow a number of properties to share the same access methods. An index specifier consists of the directive index followed by an integer constant with a value between
–32767 and 32767. For example
type
TRectangle = class private
FCoordinates: array[0..3] of Longint;
function GetCoordinate(Index: Integer): Longint;
procedure SetCoordinate(Index: Integer; Value: Longint);
public
property Left: Longint index 0
read GetCoordinate write SetCoordinate;
property Top: Longint index 1
read GetCoordinate write SetCoordinate;
property Right: Longint index 2
read GetCoordinate write SetCoordinate;
property Bottom: Longint index 3
read GetCoordinate write SetCoordinate;
property Coordinates[Index: Integer]: Longint
read GetCoordinate write SetCoordinate;
:
end;
An access specifier of a property with an index specifier must list a method identifier. In other words, the read and write specifiers of a property with an index specifier aren’t allowed to list a field name.
When accessing a property with an index specifier, the integer value specified in the property definition is automatically passed to the access method as an extra parameter. For that reason, an access method for a property with an index specifier must take an extra value parameter of type Integer. For a property read function, the extra parameter must be the last parameter. For a property write procedure, the extra parameter must be the second to last parameter, that is it must immediately precede the parameter that specifies the new property value.
Assuming that Rectangle is an object reference of the TRectangle type defined above, the statement
Rectangle.Right := Rectangle.Left + 100;
corresponds to
Rectangle.SetCoordinate(2, Rectangle.GetCoordinate(0) + 100);
Storage specifiers
The optional stored, default, and nodefault specifiers of a property definition are called storage specifiers. They control certain aspects of the run-time type information that is generated for published properties. Storage specifiers are only supported for normal (non-array) properties.
Storage specifiers have no semantic effects on a property, that is they do not affect how a property is used in program code. The Delphi Visual Component Library, however, uses the information generated by storage specifiers to control filing of a component's propertiesthe automatic saving and loading of a component's property values in a form file. The stored directive controls whether a property is filed, and the default and nodefault properties control the value that is considered a property's default value.
If present in a property definition, the stored keyword must be followed by a boolean constant (True or False), the identifier of a field of type Boolean, or the identifier of a parameterless function method with returns a value of type Boolean. If a property definition doesn’t include a stored specifier, the results are the same as if a stored True specifier were included.
The default and nodefault specifiers are supported only for properties of ordinal types and small set types. If present in a property definition, the default keyword must be followed by a constant of the same type as the property. If a property definition doesn’t (or can’t) include a default or nodefault specifier, the results are the same as if a nodefault specifier were included.
When saving a component's state, the Delphi Visual Component Library iterates over all of the component's published properties. For each property, the result of evaluating the boolean constant, field, or function method of the stored specifier controls whether the property is saved. If the result is False, the property isn’t saved. If the result is True, the property's current value is compared to the value given in the default specifier (if present). If the current value is equal to the default value, the property isn’t saved. Otherwise, if current value is different from the default value, or if the property has no default value, the property is saved.
Property overrides
A property definition that doesn’t include a property interface is called a property override. A property override allows a derived class to change the visibility, access specifiers, and storage specifiers of an inherited property.
In its simplest form, a property override specifies only the reserved word property followed by an inherited propery identifier. This form is used to change the visibility of a property. If, for example, a base class defines a property in a protected section, a derived class can raise the visibility of the property by declaring a property override in a public or published section.
A property override can include a read, write, stored, and default or nodefault specifier. Any such specifier overrides the corresponding inherited specifier. Note that a property override can change an inherited access specifier or add a missing access specifier, but it can’t remove an access specifier.
The following declarations illustrate the use of property overrides to change the visibility, access specifiers, and storage specifiers of inherited properties:
type
TBase = class
:
protected
property Size: Integer read FSize;
property Text: string read GetText write SetText;
property Color: TColor read FColor write SetColor stored False;
:
end;
type
TDerived = class(TBase)
:
protected
property Size write SetSize;
published property Text;
property Color stored True default clBlue;
:
end;
The property override of Size adds a write specifier to allow the Size property to be modified. The property overrides of Text and Color change the visibility of the properties from protected to published. In addition, the property override of Color specifies that the property should be filed if its value isn’t clBlue.
Class-reference types
Class-reference types allow operations to be performed directly on classes. This contrasts with class types, which allow operations to be performed on instances of classes. Class-reference types are sometimes referred to as metaclasses or metaclass types.
class reference type
Class-reference types are useful in the following situations:
-
With a virtual constructor to create an object whose actual type is
unknown at compile time
-
With a class method to perform an operation on a class whose actual
type is unknown at compile time
-
As the right operand of an is operator to perform a dynamic type
check with a type that is unknown at compile time
-
As the right operand of an as operator to perform a checked
typecast to a type that is unknown at compile time
The declaration of a class-reference type consists of the reserved words class of
followed by a class-type identifier. For example,
type
TComponent = class(TPersistent)
:
end;
TComponentClass = class of TComponent; TControl = class(TComponent)
:
end;
TControlClass = class of TControl;
var
ComponentClass: TComponentClass; ControlClass: TControlClass;
The previous declarations define TComponentClass as a type that can reference class TComponent, or any class that derives from TComponent, and TControlClass as a type that can reference class TControl, or any class that derives from TControl.
Class-type identifiers function as values of their corresponding class-reference types. For example, in addition to its other uses, the TComponent identifier functions as a value of type TComponentClass, and the TControl identifier functions as a value of type TControlClass.
A class-reference type value is assignment-compatible with any ancestor class- reference type. Therefore, during program execution, a class-reference type variable can reference the class it was defined for or any descendant class of the class it was defined for. Referring to the previous declarations, the assignments
ComponentClass := TComponent; { Valid } ComponentClass := TControl; { Valid }
are both valid. Of these assignments,
ControlClass := TComponent; { Invalid } ControlClass := TControl; { Valid }
only the second one is valid, however. The first assignment is an error because TComponent isn’t a descandant of TControl, and therefore not a value of type TControlClass.
A class-reference type variable can be nil, which indicates that the variable doesn’t currently reference a class.
Every class inherits (from TObject) a method function called ClassType, which returns a reference to the class of an object. The type of the value returned by ClassType is TClass, which is declared as class of TObject. This means that the value returned by ClassType may have to be typecast to a more specific descendant type before it can be used, for example
if Control <> nil then
ControlClass := TControlClass(Control.ClassType) else
ControlClass := nil;
Constructors and class references
A constructor can be invoked on a variable reference of a class-reference type. This allows polymorphic construction of objects, that is construction of objects whose actual type isn’t known at compile time. For example,
function CreateControl(ControlClass: TControlClass;
const ControlName: string; X, Y, W, H: Integer): TControl;
begin
Result := ControlClass.Create(MainForm);
with Result do begin
Parent := MainForm;
Name := ControlName;
SetBounds(X, Y, W, H);
Visible := True;
end; end;
The CreateControl function uses a class-reference type parameter to specify which class of control to create. It subsequently uses the class-reference type parameter to invoke the Create constructor of the class. Because class-type identifiers also function as class-reference type values, calls to CreateControl can simply specify the identifier of the class to create an instance for. For example,
CreateControl(TEdit, 'Edit1', 10, 10, 100, 20);
CreateControl(TButton, 'Button1', 120, 10, 80, 30);
A constructor can be invoked on a variable reference of a class-reference type. This allows polymorphic construction of objects, that is construction of objects whose actual type isn’t known at compile time.
Constructors that are invoked through class-reference types are usually virtual. That way, the constructor implementation that is called depends on the actual (run-time) class type selected by the class reference.
Class methods
A class method is a method that operates on a class reference instead of an object reference. The definition of a class method must include the reserved word class before the procedure or function keyword that starts the definition. For example,
type
TFigure = class public
class function Supports(Operation: string): Boolean; virtual; class procedure GetInfo(var Info: TFigureInfo); virtual;
:
end;
The defining declaration of a class method must also start with the reserved word
class. For example,
class procedure TFigure.GetInfo(var Info: TFigureInfo);
begin
:
end;
In the defining declaration of a class method, the identifier Self represents the class for which the method was activated. The type of Self in a class method is class of ClassType, where ClassType is the class type for which the method is implemented. Because Self doesn’t represent an object reference in a class method, it isn’t possible to use Self to access fields, properties, and normal methods. It is possible, however, to call constructors and other class methods through Self.
A class method can be invoked through a class reference or an object reference. When invoked through an object reference, the class of the given object reference is passed as the Self parameter.
The TObject and TClass types
The System unit defines two types, TObject and TClass, which serve as the root types for all class types and class-reference types. The declarations of the two types are shown below.
type
TObject = class;
TClass = class of TObject;
TObject = class constructor Create;
destructor Destroy; virtual;
class function ClassInfo: Pointer; class function ClassName: string; class function ClassParent: TClass; function ClassType: TClass;
procedure DefaultHandler(var Message); virtual; procedure Dispatch(var Message);
function FieldAddress(const Name: string): Pointer;
procedure Free;
procedure FreeInstance; virtual;
class function InheritsFrom(AClass: TClass): Boolean; class procedure InitInstance(Instance: Pointer): TObject; class function InstanceSize: Word;
class function NewInstance: TObject; virtual;
class function MethodAddress(const Name: string): Pointer;
class function MethodName(Address: Pointer): string; end;
C h a p t e r