Chapter 7
Classes and objects

A class, or class type, defines a structure consisting of fields, methods, and properties. Instances of a class type are called objects. The fields, methods, and properties of a class are called its components or members.

Objects are dynamically allocated blocks of memory whose structure is determined by their class type. Each object has a unique copy of every field defined in the class, but all instances of a class share the same methods. Objects are created and destroyed by special methods called constructors and destructors.

A variable of a class type is actually a pointer that references an object. Hence more than one variable can refer to the same object. Like other pointers, class-type variables can hold the value nil. But you don't have to explicitly dereference a class-type variable to access the object it points to. For example, SomeObject.Size := 100 assigns the value 100 to the Size property of the object referenced by SomeObject; you would not write this as SomeObject^.Size := 100.

Class types

A class type must be declared and given a name before it can be instantiated. (You cannot define a class type within a variable declaration.) Declare classes only in the outermost scope of a program or unit, not in a procedure or function declaration.

A class type declaration has the form

type className = class (ancestorClass)
  memberList
 end;

where className is any valid identifier, (ancestorClass) is optional, and memberList declares members--that is, fields, methods, and properties--of the class. If you omit (ancestorClass), then the new class inherits directly from the predefined TObject class. If you include (ancestorClass) and memberList is empty, you can omit end. A class type declaration can also include a list of interfaces implemented by the class; see "Implementing interfaces".

Methods appear in a class declaration as function or procedure headings, with no body. Defining declarations for each method occur elsewhere in the program.

For example, here is the declaration of the TListColumns class from the ComCtrls unit of Delphi's VCL.

type
  TListColumns = class(TCollection)
  private
    FOwner: TCustomListView;
    function GetItem(Index: Integer): TListColumn;
    procedure SetItem(Index: Integer; Value: TListColumn);
  protected
    function GetOwner: TPersistent; override;
    procedure Update(Item: TCollectionItem); override;
  public
    constructor Create(AOwner: TCustomListView);
    function Add: TListColumn;
    property Owner: TCustomListView read FOwner;
    property Items[Index: Integer]: TListColumn read GetItem write SetItem; default;
  end;

TListColumns descends from TCollection (in the Classes unit), inheriting most of its members. But it defines--or redefines--several methods and properties, including its constructor method, Create. Its destructor, Destroy, is inherited without change from TCollection, and so is not redeclared. Each member is declared as private, protected, or public (this class has no published members); for explanations of these terms, see "Visibility of class members".

Given this declaration, we can create a TListColumns with

var ListColumns: TListColumns;
ListColumns := TListColumns.Create(SomeListView);

where SomeListView is a variable that holds a TCustomListView object.

Inheritance and scope

When you declare a class, you can specify its immediate ancestor. For example,

type TSomeControl = class(TWinControl);

declares a class called TSomeControl that descends from TWinControl. A class type automatically inherits all of the members from its immediate ancestor. Each class can declare new members and can redefine inherited ones, but a class cannot remove members defined in an ancestor. Hence TSomeControl contains all of the members defined in TWinControl and in each of TWinControl's ancestors.

The scope of a member's identifier starts at the point where the member is declared, continues to the end of the class declaration, and extends over all descendants of the class and the blocks of all methods defined in the class and its descendants.

TObject and TClass

The TObject class, declared in the System unit, is the ultimate ancestor of all other classes. TObject defines only a handful of methods, including a basic constructor and destructor. In addition to TObject, the System unit declares the class-reference type TClass:

TClass = class of TObject;

For more information about TObject, see the online VCL reference. For more information about class-reference types, see "Class references".

If the declaration of a class type doesn't specify an ancestor, the class inherits directly from TObject. Thus

type TMyClass = class
...
end;

is equivalent to

type TMyClass = class(TObject)
...
end;

The latter form is recommended for readability.

Compatibility of class types

A class type is assignment-compatible with its ancestors. Hence a variable of a class type can reference an instance of any descendant type. For example, given the declarations

type
  TFigure = class(TObject);
  TRectangle = class(TFigure);
  TSquare = class(TRectangle);
var
  Fig: TFigure;

the variable Fig can be assigned values of type TFigure, TRectangle, and TSquare.

Object types

As an alternative to class types, you can declare object types using the syntax

type objectTypeName = object (ancestorObjectType)
  memberList
end;

where objectTypeName is any valid identifier, (ancestorObjectType) is optional, and memberList declares fields, methods, and properties. If (ancestorObjectType) is omitted, then the new type has no ancestor. Object types cannot have published members.

Since object types do not descend from TObject, they provide no built-in constructors, destructors, or other methods. You can create instances of an object type using the New procedure and destroy them with the Dispose procedure, or you can simply declare variables of an object type, just as you would with records.

Object types are supported for backward compatibility only. Their use is not recommended.

Visibility of class members

Every member of a class has an attribute called visibility, which is indicated by one of the reserved words private, protected, public, published, or automated. For example,

published property Color: TColor read GetColor write SetColor;

declares a published property called Color. Visibility determines where and how a member can be accessed, with private representing the least accessibility, protected representing an intermediate level of accessibility, and public, published, and automated representing the greatest accessibility.

If a member's declaration appears without its own visibility specifier, the member has the same visibility as the one that precedes it. Members at the beginning of a class declaration that don't have a specified visibility are by default published, provided the class is compiled in the {$M+} state or is derived from a class compiled in the {$M+} state; otherwise, such members are public.

For readability, it is best to organize a class declaration by visibility, placing all the private members together, followed by all the protected members, and so forth. This way each visibility reserved word appears at most once and marks the beginning of a new "section" of the declaration. So a typical class declaration should like this:

type
  TMyClass = class(TControl)
  private
  ... { private declarations here}
  protected
  ... { protected declarations here }
  public
  ... { public declarations here }
  published
  ... { published declarations here }
  end;

You can increase the visibility of a member in a descendant class by redeclaring it, but you cannot decrease its visibility. For example, a protected property can be made public in a descendant, but not private. Moreover, published members cannot become public in a descendant class. For more information, see "Property overrides and redeclarations".

Private, protected, and public members

A private member is invisible outside of the unit or program where its class is declared. In other words, a private method cannot be called from another module, and a private field or property cannot be read or written to from another module. By placing related class declarations in the same module, you can give the classes access to one another's private members without making those members more widely accessible.

A protected member is visible anywhere in the module where its class is declared and from any descendant class, regardless of the module where the descendant class appears. In other words, a protected method can be called, and a protected field or property read or written to, from the definition of any method belonging to a class that descends from the one where the protected member is declared. Members that are intended for use only in the implementation of derived classes are usually protected.

A public member is visible wherever its class can be referenced.

Published members

Published members have the same visibility as public members. The difference is that runtime type information (RTTI) is generated for published members. RTTI allows an application to query the fields and properties of an object dynamically and to locate its methods. Delphi uses RTTI to access the values of properties when saving and loading form (.DFM) files, to display properties in the Object Inspector, and to associate specific methods (called event handlers) with specific properties (called events).

Published properties are restricted to certain data types. Ordinal, string, class, interface, and method-pointer types can be published. So can set types, provided the upper and lower bounds of the base type have ordinal values between 0 and 31. (In other words, the set must fit in a byte, word, or double word.) Any real type except Real48 can be published. Array properties cannot be published.

All methods are publishable, but a class cannot publish two or more overloaded methods with the same name. Fields can be published only if they are of a class or interface type.

A class cannot have published members unless it is compiled in the {$M+} state or descends from a class compiled in the {$M+} state. Most classes with published members derive from TPersistent, which is compiled in the {$M+} state, so it is seldom necessary to use the $M directive.

Automated members

Automated members have the same visibility as public members. The difference is that Automation type information (required for Automation servers) is generated for automated members. Automated members typically appear only in classes derived from the TAutoObject class in the OleAuto unit. This unit, and the automated reserved word itself, are maintained for backward compatibility. The TAutoObject class in the ComObj unit does not use automated.

The following restrictions apply to methods and properties declared as automated.

The declaration of an automated method or property can include a dispid directive, which must be followed by an integer constant that specifies an Automation dispatch ID for the member. Otherwise, the compiler automatically assigns the member a dispatch ID that is one larger than the largest dispatch ID used by any method or property in the class and its ancestors. Specifying an already used ID in a dispid directive causes an error.

For more information about Automation, see "Automation objects".

Forward declarations and mutually dependent classes

If the declaration of a class type ends with the word class and a semicolon--that is, if it has the form

type className = class;

with no ancestor or class members listed after the word class--then it is a forward declaration. A forward declaration must be resolved by a defining declaration of the same class within the same type declaration section. In other words, between a forward declaration and its defining declaration, nothing can occur except other type declarations.

Forward declarations allow mutually dependent classes. For example,

type
  TFigure = class;  // forward declaration
  TDrawing = class
    Figure: TFigure;
    ...
  end;

  TFigure = class  // defining declaration
    Drawing: TDrawing;
    ...
  end;

Do not confuse forward declarations with complete declarations of types that derive from TObject without declaring any class members.

type
  TFirstClass = class;           // this is a forward declaration

  TSecondClass = class           // this is a complete class declaration
    end                          //

  TThirdClass = class(TObject);  // this is a complete class declaration

Fields

A field is like a variable that belongs to an object. Fields can be of any type, including class types. (That is, fields can hold object references.) Fields are usually private.

To define a field member of a class, simply declare the field as you would a variable. All field declarations must occur before any property or method declarations. For example, the following declaration creates a class called TNumber whose only member, other than the methods is inherits from TObject, is an integer field called Int.

type TNumber = class
  Int: Integer;
end;

Fields are statically bound; that is, references to them are fixed at compile time. To see what this means, consider the following code.

type
  TAncestor = class
    Value: Integer;
  end;

  TDescendant = class(TAncestor)
    Value: string;  // hides the inherited Value field
  end;

var
  MyObject: TAncestor;

begin
  MyObject := TDescendant.Create;
  MyObject.Value := 'Hello!';  // error
  TDescendant(MyObject).Value := 'Hello!';  // works!
end;

Although MyObject holds an instance of TDescendant, it is declared as TAncestor. The compiler therefore interprets MyObject.Value as referring to the (integer) field declared in TAncestor. Both fields, however, exist in the TDescendant object; the inherited Value is hidden by the new one, and can be accessed through a typecast.

Methods

A method is a procedure or function associated with a class. A call to a method specifies the object (or, if it is a class method, the class) that the method should operate on. For example,

SomeObject.Free

calls the Free method in SomeObject.

Method implementations

Within a class declaration, methods appear as procedure and function headings, which work like forward declarations. Somewhere after the class declaration, but within the same module, each method must be implemented by a defining declaration. For example, suppose the declaration of TMyClass includes a method called DoSomething:

type
  TMyClass = class(TObject)
    ...
    procedure DoSomething;
    ...
  end;

A defining declaration for DoSomething must occur later in the module:

procedure TMyClass.DoSomething;
begin
...
end;

While a class can be declared in either the interface or the implementation section of a unit, defining declarations for a class's methods must be in the implementation section.

In the heading of a defining declaration, the method name is always qualified with the name of the class to which it belongs. The heading can repeat the parameter list from the class declaration; if it does so, the order, type, and names of the parameters must match exactly, and, if the method is a function, so must the return value.

Inherited

The reserved word inherited plays a special role in implementing polymorphic behavior. It can occur in method definitions, with or without an identifier after it.

If inherited is followed by a method identifier, it represents a normal method call, except that the search for the method begins with the immediate ancestor of the enclosing method's class. For example, when

inherited Create(...);

occurs in the definition of a method, it calls the inherited Create.

When inherited has no identifier after it, it refers to the inherited method with the same name as the enclosing method. In this case, inherited can appear with or without parameters; if no parameters are specified, it passes to the inherited method the same parameters with which the enclosing method was called. For example,

inherited;

occurs frequently in the implementation of constructors. It calls the inherited constructor with the same parameters that were passed to the descendant.

Self

Within the implementation of a method, the identifier Self references the object in which the method is called. For example, here is the implementation of TCollection's Add method in the Classes unit of the VCL.

function TCollection.Add: TCollectionItem;
begin
  Result := FItemClass.Create(Self);
end;

The Add method calls the Create method in the class referenced by the FItemClass field, which is always a TCollectionItem descendant. TCollectionItem.Create takes a single parameter of type TCollection, so Add passes it the TCollection instance object where Add is called. This is illustrated in the following code.

var MyCollection: TCollection;
...
MyCollection.Add  // MyCollection is passed to the TCollectionItem.Create method

Self is useful for a variety of reasons. For example, a member identifier declared in a class type might be redeclared in the block of one of the class's methods. In this case, you can access the original member identifier as Self.Identifier.

For information about Self in class methods, see "Class methods".

Method binding

Methods can be static (the default), virtual, or dynamic. Virtual and dynamic methods can be overridden, and they can be abstract. These designations come into play when a variable of one class type holds a value of a descendant class type. They determine which implementation is activated when a method is called.

Static methods

Methods are by default static. When a static method is called, the declared (compile-time) type of the class or object variable used in the method call determines which implementation to activate. In the following example, the Draw methods are static.

type
  TFigure = class
    procedure Draw;
  end;
  TRectangle = class(TFigure)
    procedure Draw;
  end;

Given these declarations, the following code illustrates the effect of calling a static method. In the second call to Figure.Draw, the Figure variable references an object of class TRectangle, but the call invokes the implementation of Draw in TFigure, because the declared type of the Figure variable is TFigure.

var
  Figure: TFigure;
  Rectangle: TRectangle;
begin
  Figure := TFigure.Create;
  Figure.Draw;  // calls TFigure.Draw
  Figure.Destroy;
  Figure := TRectangle.Create;
  Figure.Draw;  // calls TFigure.Draw
  TRectangle(Figure).Draw;  // calls TRectangle.Draw
  Figure.Destroy;
  Rectangle := TRectangle.Create;
  Rectangle.Draw;  // calls TRectangle.Draw
  Rectangle.Destroy;
end;

Virtual and dynamic methods

To make a method virtual or dynamic, include the virtual or dynamic directive in its declaration. Virtual and dynamic methods, unlike static methods, can be overridden in descendant classes. When an overridden method is called, the actual (runtime) type of the class or object used in the method call--not the declared type of the variable--determines which implementation to activate.

To override a method, redeclare it with the override directive. An override declaration must match the ancestor declaration in the order and type of its parameters and in its result type (if any).

In the following example, the Draw method declared in TFigure is overridden in two descendant classes.

type
  TFigure = class
    procedure Draw; virtual;
  end;
  TRectangle = class(TFigure)
    procedure Draw; override;
  end;
  TEllipse = class(TFigure)
    procedure Draw; override;
  end;

Given these declarations, the following code illustrates the effect of calling a virtual method through a variable whose actual type varies at runtime.

var
  Figure: TFigure;
begin
  Figure := TRectangle.Create;
  Figure.Draw;  // calls TRectangle.Draw
  Figure.Destroy;
  Figure := TEllipse.Create;
  Figure.Draw;  // calls TEllipse.Draw
  Figure.Destroy;
end;

Only virtual and dynamic methods can be overridden. All methods, however, can be overloaded; see "Overloading methods".

Virtual versus dynamic

Virtual and dynamic methods are semantically equivalent. They differ only in the implementation of method-call dispatching at runtime. Virtual methods optimize for speed, while dynamic methods optimize for code size.

In general, virtual methods are the most efficient way to implement polymorphic behavior. Dynamic methods are useful when a base class declares many overridable methods which are inherited by many descendant classes in an application, but only occasionally overridden.

Overriding versus hiding

If a method declaration specifies the same method identifier and parameter signature as an inherited method, but doesn't include override, the new declaration merely hides the inherited one without overriding it. Both methods exist in the descendant class, where the method name is statically bound. For example,

type
  T1 = class(TObject)
    procedure Act; virtual;
  end;
  T2 = class(T1)
    procedure Act;  // Act is redeclared, but not overridden
  end;

var
  SomeObject: T1;
begin
  SomeObject := T2.Create;
  SomeObject.Act;  // calls T1.Act
end;

Reintroduce

The reintroduce directive suppresses compiler warnings about hiding previously declared virtual methods. For example,

procedure DoSomething; reintroduce;  // the ancestor class also has a DoSomething method

Use reintroduce when you want to hide an inherited virtual method with a new one.

Abstract methods

An abstract method is a virtual or dynamic method that has no implementation in the class where it is declared. Its implementation is deferred to a descendant class. Abstract methods must be declared with the directive abstract after virtual or dynamic. For example,

procedure DoSomething; virtual; abstract;

You can call an abstract method only in a class or instance of a class in which the method has been overridden.

Overloading methods

A method can be redeclared using the overload directive. In this case, if the redeclared method has a different parameter signature from its ancestor, it overloads the inherited method without hiding it. Calling the method in a descendant class activates whichever implementation matches the parameters in the call.

If you overload a virtual method, use the reintroduce directive when you redeclare it in descendant classes. For example,

type
  T1 = class(TObject)
    procedure Test(I: Integer); overload; virtual;
  end;
  T2 = class(T1)
    procedure Test(S: string); reintroduce; overload;
  end;
  ...
  SomeObject := T2.Create;
  SomeObject.Test('Hello!');  // calls T2.Test
  SomeObject.Test(7);   // calls T1.Test

Within a class, you cannot publish multiple overloaded methods with the same name. Maintenance of runtime type information requires a unique name for each published member.

type
  TSomeClass = class
  published
    function Func(P: Integer): Integer;
    function Func(P: Boolean): Integer  // error
    ...

Methods that serve as property read or write specifiers cannot be overloaded.

The implementation of an overloaded method must repeat the parameter list from the class declaration. For more information about overloading, see "Overloading procedures and functions".

Constructors

A constructor is a special method that creates and initializes instance objects. The declaration of a constructor looks like a procedure declaration, but it begins with the word constructor. Examples:

constructor Create;
constructor Create(AOwner: TComponent);

Constructors must use the default register calling convention. Although the declaration specifies no return value, when a constructor is called using a class reference, it returns a reference to the object it creates.

A class can have more than one constructor, but most have only one. It is conventional to call the constructor Create.

To create an object, call the constructor method in a class type. For example,

MyObject := TMyClass.Create;

This allocates storage for the new object on the heap, sets the values of all ordinal fields to zero, assigns nil to all pointer and class-type fields, and makes all string fields empty. Other actions specified in the constructor implementation are performed next; typically, objects are initialized based on values passed as parameters to the constructor. Finally, the constructor returns a reference to the newly allocated and initialized object. The type of the returned value is the same as the class type specified in the constructor call.

If an exception is raised during execution of a constructor that was invoked on a class reference, the Destroy destructor is automatically called to destroy the unfinished object.

When a constructor is called using an object reference (rather than a class reference), it does not create an object or return a value. Instead, the constructor operates on the specified object, executing only the statements in the constructor's implementation. A constructor is typically invoked on an object reference in conjunction with the reserved word inherited to execute an inherited constructor.

Here is an example of a class type and its constructor.

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 usually to call an inherited constructor to initialize the object's inherited fields. The constructor then initializes the fields introduced in the descendant class. Because a constructor always clears the storage it allocates for a new object, all fields start with a value of zero (ordinal types), nil (pointer and class types), empty (string types), or Unassigned (variants). Hence there is no need to initialize fields in a constructor's implementation except to nonzero or nonempty values.

When invoked through a class-type identifier, a constructor declared as virtual is equivalent to a static constructor. When combined with class-reference types, however, virtual constructors allow polymorphic construction of objects--that is, construction of objects whose types aren't known at compile time. (See "Class references".)

Destructors

A destructor is a special method that destroys the object where it is called and deallocates its memory. The declaration of a destructor looks like a procedure declaration, but it begins with the word destructor. Examples:

destructor Destroy;
destructor Destroy; override;

Destructors must use the default register calling convention. Although a class can have more than one destructor, it is recommended that each class override the inherited Destroy method and declare no other destructors.

To call a destructor, you must reference an instance object. For example,

MyObject.Destroy;

When a destructor is called, actions specified in the destructor implementation are performed first. Typically, these consist of destroying any embedded objects and freeing resources that were allocated by the object. Then the storage that was allocated for the object is disposed of.

Here is an example of a destructor implementation.

destructor TShape.Destroy;
begin
  FBrush.Free;
  FPen.Free;
  inherited Destroy;
end;

The last action in a destructor's implementation is typically to call the inherited destructor to destroy the object's inherited fields.

When an exception is raised during creation of an object, Destroy is automatically called to dispose of the unfinished object. This means that Destroy must be prepared to dispose of partially constructed objects. Because a constructor sets the fields of a new object to zero or empty values before performing other actions, class-type and pointer-type fields in a partially constructed object are always nil. A destructor should therefore check for nil values before operating on class-type or pointer-type fields. Calling the Free method (defined in TObject), rather than Destroy, offers a convenient way of checking for nil values before destroying an object.

Message handlers

Message handlers are methods that implement responses to dynamically dispatched messages. Delphi's VCL uses message handlers to respond to Windows messages.

A message handler is created by including the message directive in a method declaration, followed by an integer constant between 1 and 49151 which specifies the message ID. For message handlers in VCL controls, the integer constant must be one of the Windows message IDs defined, along with corresponding record types, in the Messages unit. For example,

type
  TTextBox = class(TCustomControl)
  private
    procedure WMChar(var Message: TWMChar); message WM_CHAR;
    ...
  end;

A message handler must be a procedure that takes a single var parameter.

A message handler does not have to include the override directive to override an inherited message handler. In fact, it doesn't have to specify the same method name or parameter type as the method it overrides. The message ID alone determines which message the method responds to and whether it is an override.

Implementing message handlers

The implementation of a message handler can call the inherited message handler, as in this example:

procedure TTextBox.WMChar(var Message: TWMChar);
begin
  if Chr(Message.CharCode) = #13 then
    ProcessEnter
  else
    inherited;
end;

The inherited statement searches backward through the class hierarchy and invokes the first message handler with the same ID as the current method, automatically passing the message record to it. If no ancestor class implements a message handler for the given ID, inherited calls the DefaultHandler method originally defined in TObject.

The implementation of DefaultHandler in TObject simply returns without performing any actions. By overriding DefaultHandler, a class can implement its own default handling of messages. The DefaultHandler method for VCL controls calls the Windows DefWindowProc function.

Message dispatching

Message handlers are seldom called directly. Instead, messages are dispatched to an object using the Dispatch method inherited from TObject:

procedure Dispatch(var Message);

The Message parameter passed to Dispatch must be a record whose first entry is a field of type Cardinal containing a message ID. See the Messages unit for examples.

Dispatch searches backward through the class hierarchy (starting from the class of the object where it is called) and invokes the first message handler for the ID passed to it. If no message handler is found for the given ID, Dispatch calls DefaultHandler.

Properties

A property, like a field, defines an attribute of an object. But while a field is merely a storage location whose contents can be examined and changed, a property associates specific actions with reading or modifying its data. Properties provide control over access to an object's attributes, and they allow attributes to be computed.

The declaration of a property specifies a name and a type, and includes at least one access specifier. The syntax of a property declaration is

property propertyName[indexes]: type index integerConstant specifiers;

where

Properties are defined by their access specifiers. Unlike fields, properties cannot be passed as var parameters, nor can the @ operator be applied to a property. The reason is that a property doesn't necessarily exist in memory. It could, for instance, have a read method that retrieves a value from a database or generates a random value.

Property access

Every property has a read specifier, a write specifier, or both. These are called access specifiers and they have the form

read fieldOrMethod
write fieldOrMethod

where fieldOrMethod is the name of a field or method declared in the same class as the property or in an ancestor class.

For example, given the declaration

property Color: TColor read GetColor write SetColor;

the GetColor method must be declared as

function GetColor: TColor;

and the SetColor method must be declared as one of these:

procedure SetColor(Value: TColor);
procedure SetColor(const Value: TColor);

(The name of SetColor's parameter, of course, doesn't have to be Value.)

When a property is referenced in an expression, its value is read using the field or method listed in the read specifier. When a property is referenced in an assignment statement, its value is written using the field or method listed in the write specifier.

The example below declares a class called TCompass with a published property called Heading. The value of Heading is read through the FHeading field and written through the SetHeading procedure.

type
  THeading = 0..359;
  TCompass = class(TControl)
  private
    FHeading: THeading;
    procedure SetHeading(Value: THeading);
  published
    property Heading: THeading read FHeading write SetHeading;
    ...
  end;

Given this declaration, 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 consists of retrieving 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, presumably, stores the new value in the FHeading field as well as performing other actions. For example, SetHeading might be implemented like this:

procedure TCompass.SetHeading(Value: THeading);
begin
  if FHeading <> Value then
  begin
    FHeading := Value;
    Repaint;  // update user interface to reflect new value
  end;
end;

A property whose declaration includes only a read specifier is a read-only property, and one whose declaration includes only a write specifier is a write-only property. It is an error to assign a value to a read-only property or use a write-only property in an expression.

Array properties

Array properties are indexed properties. They can represent things like items in a list, child controls of a control, and pixels of a bitmap.

The declaration of an array property includes a parameter list that specifies the names and types of the indexes. 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's or function's parameter list, except that the parameter declarations are enclosed in brackets instead of parentheses. Unlike arrays, which can use only ordinal-type indexes, array properties allow indexes of any type.

For array properties, access specifiers must list methods rather than fields. The method in a read specifier must be a function that takes the number and type of parameters listed in the property's index parameter list, in the same order, and whose result type is identical to the property's type. The method in a write specifier must be a procedure that takes the number and type of parameters listed in the property's index parameter list, in the same order, plus an additional value or const parameter of the same type as the property.

For example, the access methods for the array properties above 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 indexing the property identifier. 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 the default directive, in which case the array property becomes the default property of the class. For example,

type
  TStringArray = class
  public
    property Strings[Index: Integer]: string ...; default;
    ...
  end;

If a class has a default property, you can access that property with the abbreviation object[index], which is equivalent to object.property[index]. For example, given the declaration above, StringArray.Strings[7] can be abbreviated to StringArray[7]. A class can have only one default property. Changing or hiding the default property in descendant classes may lead to unexpected behavior, since the compiler always determines an object's default property statically.

Index specifiers

Index specifiers allow several properties to share the same access method while representing different values. An index specifier consists of the directive index followed by an integer constant between -2147483647 and 2147483647. If a property has an index specifier, its read and write specifiers must list methods rather than fields. 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 method for a property with an index specifier must take an extra value parameter of type Integer. For a read function, it must be the last parameter; for a write procedure, it must be the second-to-last parameter (preceding the parameter that specifies the property value). When a program accesses the property, the property's integer constant is automatically passed to the access method.

Given the declaration above, if Rectangle is of type TRectangle, then

Rectangle.Right := Rectangle.Left + 100;

corresponds to

Rectangle.SetCoordinate(2, Rectangle.GetCoordinate(0) + 100);

Storage specifiers

The optional stored, default, and nodefault directives are called storage specifiers. They have no effect on program behavior, but control the way Delphi maintains runtime type information (RTTI). Specifically, storage specifiers determine whether Delphi saves the values of published properties in form (.DFM) files.

The stored directive must be followed by True, False, the name of a Boolean field, or the name of a parameterless method that returns a Boolean value. For example,

property Name: TComponentName read FName write SetName stored False;

If a property has no stored directive, it is treated as if stored True were specified.

The default directive must be followed by a constant of the same type as the property. For example,

property Tag: Longint read FTag write FTag default 0;

To override an inherited default value without specifying a new one, use the nodefault directive. The default and nodefault directives are supported only for ordinal types and for set types, provided the upper and lower bounds of the set's base type have ordinal values between 0 and 31; if such a property is declared without default or nodefault, it is treated as if nodefault were specified. For reals, pointers, and strings, there is an implicit default value of 0, nil, and '' (the empty string), respectively.

When Delphi saves a component's state, it checks the storage specifiers of the component's published properties. If a property's current value is different from its default value (or if there is no default value) and the stored specifier is True, then the property's value is saved. Otherwise, the property's value is not saved.

Note: Storage specifiers are not supported for array properties. The default directive has a different meaning when used in an array property declaration. See "Array properties".

Property overrides and redeclarations

A property declaration that doesn't specify a type is called a property override. Property overrides allow you to change a property's inherited visibility or specifiers. The simplest override consists only of the reserved word property followed by an inherited property identifier; this form is used to change a property's visibility. For example, if an ancestor class declares a property as protected, a derived class can redeclare it in a public or published section of the class. Property overrides can include read, write, stored, default, and nodefault directives; any such directive overrides the corresponding inherited directive. An override can replace an inherited access specifier, add a missing specifier, or increase a property's visibility, but it cannot remove an access specifier or decrease a property's visibility. An override can include an implements directive, which adds to the list of implemented interfaces without removing inherited ones.

The following declarations illustrate the use of property overrides.

type
  TAncestor = 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(TAncestor)
    ...
  protected
    property Size write SetSize;
  published
    property Text;
    property Color stored True default clBlue;
    ...
  end;

The override of Size adds a write specifier to allow the property to be modified. The overrides of Text and Color change the visibility of the properties from protected to published. The property override of Color also specifies that the property should be filed if its value isn't clBlue.

A redeclaration of a property that includes a type identifier hides the inherited property rather than overriding it. This means that a new property is created with the same name as the inherited one. Any property declaration that specifies a type must be a complete declaration, and must therefore include at least one access specifier.

Whether a property is hidden or overridden in a derived class, property look-up is always static. That is, the declared (compile-time) type of the variable used to identify an object determines the interpretation of its property identifiers. Hence, after the following code executes, reading or assigning a value to MyObject.Value invokes Method1 or Method2, even though MyObject holds an instance of TDescendant. But you can cast MyObject to TDescendant to access the descendant class's properties and their access specifiers.

type
  TAncestor = class
    ...
    property Value: Integer read Method1 write Method2;
  end;

  TDescendant = class(TAncestor)
    ...
    property Value: Integer read Method3 write Method4;
  end;

var MyObject: TAncestor;
...
MyObject := TDescendant.Create;

Class references

Sometimes operations are performed on a class itself, rather than on instances of a class (that is, objects). This happens, for example, when you call a constructor method using a class reference. You can always refer to a specific class using its name, but at times it is necessary to declare variables or parameters that take classes as values, and in these situations you need class-reference types.

Class-reference types

A class-reference type, sometimes called a metaclass, is denoted by a construction of the form

class of type

where type is any class type. The identifier type itself denotes a value whose type is class of type. If type1 is an ancestor of type2, then class of type2 is assignment-compatible with class of type1. Thus

type TClass = class of TObject;
var AnyObj: TClass;

declares a variable called AnyObj that can hold a reference to any class. (The definition of a class-reference type cannot occur directly in a variable declaration or parameter list.) You can assign the value nil to a variable of any class-reference type.

To see how class-reference types are used, look at the declaration of the constructor for TCollection (in the VCL's Classes unit):

type TCollectionItemClass = class of TCollectionItem;
...
constructor Create(ItemClass: TCollectionItemClass);

This declaration says that to create a TCollection instance object, you must pass to the constructor the name of a class descending from TCollectionItem.

Class-reference types are useful when you want to invoke a class method or virtual constructor on a class or object whose actual type is unknown at compile time.

Constructors and class references

A constructor can be called using a variable of a class-reference type. This allows construction of objects whose type isn't known at compile time.

For example,

type TControlClass = class of TControl;

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 requires a class-reference parameter to tell it what kind of control to create. It uses this parameter to call the class's constructor. Because class-type identifiers denote class-reference values, a call to CreateControl can specify the identifier of the class to create an instance of. For example,

CreateControl(TEdit, 'Edit1', 10, 10, 100, 20);

Constructors called using class references are usually virtual. The constructor implementation activated by the call depends on the runtime type of the class reference.

Class operators

Every class inherits from TObject methods called ClassType and ClassParent that return, respectively, a reference to the class of an object and of an object's immediate ancestor. Both methods return a value of type TClass (where TClass = class of TObject), which can be cast to a more specific type. Every class also inherits a method called InheritsFrom that tests whether the object where it is called descends from a specified class. These methods are used by the is and as operators, and it is seldom necessary to call them directly.

The is operator

The is operator, which performs dynamic type checking, is used to verify the actual runtime class of an object. The expression

object is class

returns True if object is an instance of the class denoted by class or one of its descendants, and False otherwise. (If object is nil, the result is False.) If the declared type of object is unrelated to class--that is, if the types are distinct and one is not an ancestor of the other--a compilation error results. For example,

if ActiveControl is TEdit then TEdit(ActiveControl).SelectAll;

This statement casts a variable to TEdit after first verifying that the object it references is an instance of TEdit or one of its descendants.

The as operator

The as operator performs checked typecasts. The expression

object as class

returns a reference to the same object as object, but with the type given by class. At runtime, object must be an instance of the class denoted by class or one of its descendants, or be nil; otherwise an exception is raised. If the declared type of object is unrelated to class--that is, if the types are distinct and one is not an ancestor of the other--a compilation error results. For example,

with Sender as TButton do
begin
  Caption := '&Ok';
  OnClick := OkClick;
end;

The rules of operator precedence often require as typecasts to be enclosed in parentheses. For example,

(Sender as TButton).Caption := '&Ok';

Class methods

A class method is a method (other than a constructor) that operates on classes instead of objects. The definition of a class method must begin with the reserved word class. 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 begin with 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 where the method is called (which could be a descendant of the class in which it is defined). If the method is called in the class C, then Self is of the type class of C. Thus you cannot use Self to access fields, properties, and normal (object) methods, but you can use it to call constructors and other class methods.

A class method can be called through a class reference or an object reference. When it is called through an object reference, the class of the object becomes the value of Self.

Exceptions

An exception is raised when an error or other event interrupts normal execution of a program. The exception transfers control to an exception handler, which allows you to separate normal program logic from error-handling. Because exceptions are objects, they can be grouped into hierarchies using inheritance, and new exceptions can be introduced without affecting existing code. An exception can carry information, such as an error message, from the point where it is raised to the point where it is handled.

When an application uses the SysUtils unit, all runtime errors are automatically converted into exceptions. Errors that would otherwise terminate an application--such as insufficient memory, division by zero, and general protection faults--can be caught and handled.

Declaring exception types

Exception types are declared just like other classes. In fact, it is possible to use an instance of any class as an exception, but it is recommended that exceptions be derived from the Exception class defined in SysUtils.

You can group exceptions into families using inheritance. For example, the following declarations in SysUtils define a family of exception types for math errors.

type
  EMathError = class(Exception);
  EInvalidOp = class(EMathError);
  EZeroDivide = class(EMathError);
  EOverflow = class(EMathError);
  EUnderflow = class(EMathError);

Given these declarations, you can define a single EMathError exception handler that also handles EInvalidOp, EZeroDivide, EOverflow, and EUnderflow.

Exception classes sometimes define fields, methods, or properties that convey additional information about the error. For example,

type EInOutError = class(Exception)
    ErrorCode: Integer;
  end;

Raising and handling exceptions

To create an exception object, call the exception class's constructor within a raise statement. For example,

raise EMathError.Create;

In general, the form of a raise statement is

raise object at address

where object and at address are both optional. If object is omitted, the statement re-raises the current exception; see "Re-raising exceptions". When an address is specified, it is usually a pointer to a procedure or function; use this option to raise the exception from an earlier point in the stack than the one where the error actually occurred.

When an exception is raised--that is, referenced in a raise statement--it is governed by special exception-handling logic. A raise statement never returns control in the normal way. Instead, it transfers control to the innermost exception handler that can handle exceptions of the given class. (The innermost handler is the one whose try...except block was most recently entered but has not yet exited.)

For example, the function below converts a string to an integer, raising an ERangeError exception if the resulting value is outside a specified range.

function StrToIntRange(const S: string; Min, Max: Longint): Longint;
begin
  Result := StrToInt(S);  // StrToInt is declared in SysUtils
  if (Result < Min) or (Result > Max) then
    raise ERangeError.CreateFmt(
      '%d is not within the valid range of %d..%d',
      [Result, Min, Max]);
end;

Notice the CreateFmt method called in the raise statement. Exception and its descendants have special constructors that provide alternative ways to create exception messages and context IDs. See the online Help for details.

A raised exception is destroyed automatically after it is handled. Never attempt to destroy a raised exception manually.

Note: Raising an exception in the initialization section of a unit may not produce the intended result. Normal exception support comes from the SysUtils unit, which must be initialized before such support is available. If an exception occurs during initialization, all initialized units--including SysUtils--are finalized and the exception is re-raised. Then the System unit catches the exception and handles it, usually by interrupting the program.

Try...except statements

Exceptions are handled within try...except statements. For example,

try
  X := Y/Z;
except
  on EZeroDivide do HandleZeroDivide;
end;

This statement attempts to divide Y by Z, but calls a routine named HandleZeroDivide if an EZeroDivide exception is raised.

The syntax of a try...except statement is

try statements except exceptionBlock end

where statements is a sequence of statements (delimited by semicolons) and exceptionBlock is either

An exception handler has the form

on identifier: type do statement

where identifier: is optional (if included, identifier can be any valid identifier), type is a type used to represent exceptions, and statement is any statement.

A try...except statement executes the statements in the initial statements list. If no exceptions are raised, the exception block (exceptionBlock) is ignored and control passes to the next part of the program.

If an exception is raised during execution of the initial statements list, either by a raise statement in the statements list or by a procedure or function called from the statements list, an attempt is made to "handle" the exception:

If none of the conditions above is satisfied, the search continues in the exception block of the next-most-recently entered try...except statement that has not yet exited. If no appropriate handler, else clause, or statement list is found there, the search propagates to the next-most-recently entered try...except statement, and so forth. If the outermost try...except statement is reached and the exception is still not handled, the program terminates.

When an exception is handled, the stack is traced back to the procedure or function containing the try...except statement where the handling occurs, and control is transferred to the executed exception handler, else clause, or statement list. This process discards all procedure and function calls that occurred after entering the try...except statement where the exception is handled. The exception object is then automatically destroyed through a call to its Destroy destructor and control is passed to the statement following the try...except statement. (If a call to the Exit, Break, or Continue standard procedure causes control to leave the exception handler, the exception object is still automatically destroyed.)

In the example below, the first exception handler handles division-by-zero exceptions, the second one handles overflow exceptions, and the final one handles all other math exceptions. EMathError appears last in the exception block because it is the ancestor of the other two exception classes; if it appeared first, the other two handlers would never be invoked.

try
...
except
  on EZeroDivide do HandleZeroDivide;
  on EOverflow do HandleOverflow;
  on EMathError do HandleMathError;
end;

An exception handler can specify an identifier before the name of the exception class. This declares the identifier to represent the exception object during execution of the statement that follows on...do. The scope of the identifier is limited to that statement. For example,

try
...
except
  on E: Exception do ErrorDialog(E.Message, E.HelpContext);
end;

If the exception block specifies an else clause, the else clause handles any exceptions that aren't handled by the block's exception handlers. For example,

try
...
except
  on EZeroDivide do HandleZeroDivide;
  on EOverflow do HandleOverflow;
  on EMathError do HandleMathError;
else
  HandleAllOthers;
end;

Here, the else clause handles any exception that isn't an EMathError.

An exception block that contains no exception handlers, but instead consists only of a list of statements, handles all exceptions. For example,

try
...
except
  HandleException;
end;

Here, the HandleException routine handles any exception that occurs as a result of executing the statements between try and except.

Re-raising exceptions

When the reserved word raise occurs in an exception block without an object reference following it, it raises whatever exception is handled by the block. This allows an exception handler to respond to an error in a limited way and then re-raise the exception. Re-raising is useful when a procedure or function has to clean up after an exception occurs but cannot fully handle the exception.

For example, the GetFileList function allocates a TStringList object and fills it with file names matching a specified search path:

function GetFileList(const Path: string): TStringList;
var
  I: Integer;
  SearchRec: TSearchRec;
begin
  Result := TStringList.Create;
  try
    I := FindFirst(Path, 0, SearchRec);
    while I = 0 do
    begin
      Result.Add(SearchRec.Name);
      I := FindNext(SearchRec);
    end;
  except
    Result.Free;
    raise;
  end;
end;

GetFileList creates a TStringList object, then uses the FindFirst and FindNext functions (defined in SysUtils) to initialize it. If the initialization fails--for example because the search path is invalid, or because there is not enough memory to fill in the string list--GetFileList needs to dispose of the new string list, since the caller does not yet know of its existence. For this reason, initialization of the string list is performed in a try...except statement. If an exception occurs, the statement's exception block disposes of the string list, then re-raises the exception.

Nested exceptions

Code executed in an exception handler can itself raise and handle exceptions. As long as these exceptions are also handled within the exception handler, they do not affect the original exception. However, once an exception raised in an exception handler propagates beyond that handler, the original exception is lost. This is illustrated by the Tan function below.

type
  ETrigError = class(EMathError);

function Tan(X: Extended): Extended;
begin
  try
    Result := Sin(X) / Cos(X);
  except
    on EMathError do
      raise ETrigError.Create('Invalid argument to Tan');
  end;
end;

If an EMathError exception occurs during execution of Tan, the exception handler raises an ETrigError. Since Tan does not provide a handler for ETrigError, the exception propagates beyond the original exception handler, causing the EMathError exception to be destroyed. To the caller, it appears as if the Tan function has raised an ETrigError exception.

Try...finally statements

Sometimes you want to ensure that specific parts of an operation are completed, whether or not the operation is interrupted by an exception. For example, when a routine acquires control of a resource, it is often important that the resource be released, regardless of whether the routine terminates normally. In these situations, you can use a try...finally statement.

The following example shows how code that opens and processes a file can ensure that the file is ultimately closed, even if an error occurs during execution.

Reset(F);
try
...  // process file F
finally
  CloseFile(F);
end;

The syntax of a try...finally statement is

try statementList1 finally statementList2 end

where each statementList is a sequence of statements delimited by semicolons. The try...finally statement executes the statements in statementList1 (the try clause). If statementList1 finishes without raising exceptions, statementList2 (the finally clause) is executed. If an exception is raised during execution of statementList1, control is transferred to statementList2; once statementList2 finishes executing, the exception is re-raised. If a call to the Exit, Break, or Continue procedure causes control to leave statementList1, statementList2 is automatically executed. Thus the finally clause is always executed, regardless of how the try clause terminates.

If an exception is raised but not handled in the finally clause, that exception is propagated out of the try...finally statement, and any exception already raised in the try clause is lost. The finally clause should therefore handle all locally raised exceptions, so as not to disturb propagation of other exceptions.

Standard exception classes and routines

The SysUtils unit declares several standard routines for handling exceptions, including ExceptObject, ExceptAddr, and ShowException. SysUtils and other VCL units also include dozens of exception classes, all which (aside from OutlineError) derive from Exception.

The Exception class has properties called Message and HelpContext that can be used to pass an error description and a context ID for context-sensitive online documentation. It also defines various constructor methods that allow you to specify the description and context ID in different ways. See the online Help for details.



Otros Links de Interés:

Cocina  -  Videos  -  Juegos Gratis  - Postales cachondas  - Cine  - Programas Gratis  -  Letras de Canciones

Listas de todos los Tutoriales Gratis. 1998- 2007 - -

Los tutoriales y cursos aquí reunidos son una recopilación de los mejores encontrados en Internet.
El crédito y copyright de los mismos si lo hubiere corresponde al autor de cada uno de ellos.
Si tu tutorial o curso está aquí, y deseas darlo de baja de esta recopilación o quieres añadir el tuyo, envíanos un mensaje desde
aquí

Publispain - Fun