Difference between revisions of "Helper types"

From Lazarus wiki
m (Class helpers moved to Helper types: The topic should describe both class helpers and record helpers at once (as there are only subtle differences between them))
(Brought the article up to date)
Line 1: Line 1:
Class helpers allow you to define new functionality for classes. This is useful if you can't extend a class because it's sealed or you can't control of which final type the class instance is (e.g. Lines/Items properties in various LCL components).
+
Helper types allow you to extend the scope which the compiler uses to search symbols for. This is useful if you can't extend a class because it's sealed or you can't control of which final type the class instance is (e.g. Lines/Items properties in various LCL components are presented as TStrings, but could be every TStrings descendant).
This page describes class helpers in Object Pascal. For class helpers in Objective Pascal see [[FPC_PasCocoa#Category_declaration|here]].
+
This page describes helper types in Object Pascal. For the equivalent in Objective Pascal see [[FPC_PasCocoa#Category_declaration|here]].
  
 
==General==
 
==General==
  
If you want to test this feature you need to checkout a FPC branch using the following command:
+
If you want to test this feature you (currently) need to checkout a FPC branch using the following command:
 
   
 
   
 
  svn checkout http://svn.freepascal.org/svn/fpc/branches/svenbarth/classhelpers fpc-classhelpers
 
  svn checkout http://svn.freepascal.org/svn/fpc/branches/svenbarth/classhelpers fpc-classhelpers
  
You can build the compiler like a normal trunk compiler (you should use FPC 2.4.2 for building of course).
+
You can build the compiler like a normal trunk compiler (you should use FPC 2.4.2 or yet to be released 2.4.4 for building of course).
  
'''Important:'''<br />
+
''Note'': This feature is complete and awaits the review for the merge and the merge itself.
This is a Work In Progress and thus not everything described here might work that way in the compiler or even work at all.
 
  
==Syntax==
+
==Declaration==
The syntax for class helpers is the following:
+
Helper types allow you to extend a given class or record with methods that add more functionality to that type. So you can add default properties, enumerators and wrappers for existing functionality.
 +
===Syntax===
 +
Currently helper types can be used to extend classes (''class helper'') and records (''record helper'') as those are supported by Delphi as well. In theory they could be extended  to support interfaces, objects or even primitive types as well (although the latter would need bigger adjustments inside the compiler).
 +
 
 +
The general syntax for helper types is the following:
 
<delphi>
 
<delphi>
ClassHelperName = class helper[(BaseClassHelper)] for ExtendedClass
+
HelperName = class|record helper[(BaseHelper)] for ExtendedType
 
   [properties, procedures, functions, constructors]
 
   [properties, procedures, functions, constructors]
 
end [hint modifiers];
 
end [hint modifiers];
 
</delphi>
 
</delphi>
  
''ClassHelperName'', ''BaseClassHelper'' and ''ExtendedClass'' have to be valid Pascal identifiers, ''BaseClassHelper'' must be a class helper for  a super class of ''ClassHelperName'' and ''ExtendedClass'' must be an Object Pascal class or record (the latter is not supported yet in FPC).
+
The sequence ''class helper'' declares a helper for a class while ''record helper'' declares a helper for a record. The inheritance ''(BaseHelper)'' is optional.
The declarations inside a class helper are very similiar to a normal class declaration, but you must not use fields and destructors.
+
 
 +
''HelperName'', ''BaseHelper'' and ''ExtendedType'' have to be valid Pascal identifiers. In case of a class helper ''ExtendedType'' must be a class and ''BaseHelper'' must be another class helper that extends either the same class as ''ExtendedType'' or a parent class of it. For record helpers ''ExtendedType'' must be a record and ''BaseHelper'' must be a record helper that extend the same record type.
 +
The declarations inside a helper are very similiar to the declaration of a normal class, but you must not use fields, destructors and class constructors/destructors.
 +
 
 +
''Note'':
 +
In mode ObjFPC record helpers need the modeswitch ''advancedrecords'' to be declared; in mode Delphi no further modeswitch is necessary.
 +
===Implementation===
 +
The rules when implementing a helper are a bit differently then when implementing a normal class or record. First of the type of ''Self'' is always of the type of the extended type (e.g. ''TObject'' in case of a helper declared as ''class helper for TObject'').
 +
If a method is not defined in the helper type itself or it's prepended by "inherited", it is first searched in the extended type, then in all the parent helpers of the current helper and then (for class helpers) in the parent classes of the extended class. Please note that in the last case helpers for the parent classes are available as well.
 +
 
 +
===Restrictions===
 +
A helper type may not
 +
* contain (class) destructors
 +
* contain class constructors
 +
* contain (class) fields
 +
* contain abstract methods
 +
* "override" virtual methods of the extended class (they can be hidden by the helper though)
 +
* for record helpers: contain constructors (this is not a restriction of helper types themselves, but of FPC, as constructors are not yet supported inside advanced records)
 +
 
 +
Methods of the extended type can be overloaded (thus they are not hidden by the helper) by using the ''overload'' keyword.
 +
 
 +
Example:
 +
<delphi>
 +
TObjectHelper = class helper for TObject
 +
  function ToString(const aFormat: String): String; overload;
 +
end;
 +
 
 +
function TObjectHelper.ToString(const aFormat: String): String;
 +
begin
 +
  Result := Format(aFormat, [ToString]);
 +
end;
 +
 
 +
var
 +
  o: TObject;
 +
begin
 +
  Writeln(o.ToString('The object''s name is %s'));
 +
end.
 +
</delphi>
 +
 
 +
===Inheritance===
 +
Inheritance for helper types fullfills two purposes:
 +
* use the methods of multiple helpers at once
 +
* (class helpers only) bring methods that were added to a parent class to the topmost visiblilty (because equally named methods of helpers are hidden by subclasses)
 +
====class helper====
 +
A class helper can inherit from another class helper if it extends a subclass of the class or the same class which is extend by the parent class helper.
 +
 
 +
Example:
 +
<delphi>
 +
TObjectHelper = class helper for TObject
 +
  procedure SomeMethod;
 +
end;
 +
 
 +
TFoo = class(TObject)
 +
end;
 +
 
 +
TFooHelper = class helper(TObjectHelper) for TFoo
 +
end;
 +
</delphi>
 +
 
 +
====record helper====
 +
A record helper can inherit from another record helper if it extends the same record which is extend by the parent record helper.
 +
'''Important''':
 +
Inheritance for record helpers is not enabled in mode Delphi, because there this is not allowed.
 +
 
 +
Example:
 +
<delphi>
 +
TTest = record
 +
end;
 +
 
 +
TTestHelper = record helper for TTest
 +
  procedure SomeMethod;
 +
end;
 +
 
 +
TTestHelperSub = record helper(TTestHelper) for TTest
 +
end;
 +
</delphi>
 +
 
 +
===Example===
 +
A simple example for extending existing functionality is the following:
 +
The LCL's ''TCheckListBox'', which is a list with checkoxes, does not provide a property to get the count of all checked items. Using a class helper this can be added rather easily.
 +
<delphi>
 +
type
 +
  TCheckListBoxHelper = class helper for TCheckListBox
 +
  private
 +
    function GetCheckedCount: Integer;
 +
  public
 +
    property CheckedCount: Integer read GetCheckedCount;
 +
  end;
  
 +
function GetCheckedCount: Integer;
 +
var
 +
  i: Integer;
 +
begin
 +
  Result := 0;
 +
  for i := 0 to Items.Count - 1 do
 +
    if Checked[i] then
 +
      Inc(Result);
 +
end;
 +
 +
// somewhere else (TCheckListBoxHelper needs to be in scope)
 +
if MyCheckList.CheckedCount > 0 then ...
 +
</delphi>
 +
 +
Please note that it's not trivial to implement a cached variation, as helper types can't introduce fields.
 
==Usage==
 
==Usage==
You can not reference class helpers anywhere in the code except when inheriting from one. The methods are always available once its unit is used and can be called as if they'd belong to the extended class.
+
===Scope===
 +
The methods declared inside the helper are always available once it is in scope and can be called as if they'd belong to the extended type.
  
 
Example:
 
Example:
Line 45: Line 151:
 
</delphi>
 
</delphi>
  
==Restrictions==
+
Because of Delphi compatibility only one helper type can be active for a type at the same time. This helper is always the last one that is in scope (which follows the usual rules of Pascal).
A class helper may not
 
* contain (class) destructors
 
* contain (class) fields
 
* contain abstract methods
 
* "override" virtual methods of the extended class (they can be hidden by the helper though)
 
 
 
If a class helper has a constructor the first statement in that constructor must be a call to the parameterless constructor.<br />
 
:[[User:PascalDragon|PascalDragon]] 21:51, 28 January 2011 (CET): This statement needs to be verified. I've found an example where this was not the case.
 
  
 +
Example:
 
<delphi>
 
<delphi>
 
type
 
type
   TObjectHelper = class helper for TObject
+
   TTestRecord = record
     constructor SomeMagicCreate(aFoo: Integer);
+
  end;
 +
  TTestClass = class
 +
  end;
 +
 
 +
  TTestRecordHelper = record helper for TTestRecord
 +
    procedure Test;
 +
  end;
 +
 
 +
  TTestClassHelper1 = class helper for TTestClass
 +
    procedure Test1;
 +
  end;
 +
 
 +
  TTestClassHelper2 = class helper for TTestClass
 +
     procedure Test2;
 
   end;
 
   end;
  
constructor TObjectHelper.SomeMagicCreate(aFoo: Integer);
+
var
 +
  tr: TTestRecord;
 +
  tc: TTestClass;
 
begin
 
begin
   Create; // must be the first statement!
+
   tr.Test; // this compiles
end;
+
  tc.Test1; // this won't compile
 +
  tc.Test2; // this compiles
 +
end.
 
</delphi>
 
</delphi>
 +
===Method hiding===
 +
The methods of helper types hide the methods declared in the extended type as long as they are not declared with "overload".
  
Methods of the extended class can be overloaded (in mode ObjFPC this is enabled by default, in mode Delphi you must enable this by using the ''overload'' keyword).
+
If a class helper is in scope for a parent class then only he methods of the parent class are hidden while the methods of the subclass will stay visible.
:[[User:PascalDragon|PascalDragon]] 21:51, 28 January 2011 (CET): This is not yet working in any of both modes.
 
  
 +
Example:
 
<delphi>
 
<delphi>
TObjectHelper = class helper for TObject
+
type
   function ToString(const aFormat: String): String; overload;
+
  TTest = class
end;
+
    procedure Test;
 +
  end;
 +
   TTestSub = class(TTest)
 +
    procedure Test;
 +
  end;
  
function TObjectHelper.ToString(const aFormat: String): String;
+
   TTestHelper = class helper for TTest
begin
+
    procedure Test;
   Result := Format(aFormat, [ToString]);
+
  end;
end;
 
  
 
var
 
var
   o: TObject;
+
   t: TTest;
 +
  ts: TTestSub;
 
begin
 
begin
   Writeln(o.ToString('The object''s name is %s'));
+
   t.Test; // this calls "Test" of the helper
 +
  ts.Test; // this calls "Test" of TTestSub
 
end.
 
end.
 
</delphi>
 
</delphi>
 +
===Restrictions===
 +
You can not reference helper types anywhere in the code except for the following cases:
 +
* inheriting from one
 +
* using one in (Bit)SizeOf
 +
* using one in TypeInfo
 +
 +
==Differences==
 +
===Differences between mode Delphi and ObjFPC===
 +
In mode Delphi you can use ''virtual'', ''dynamic'', ''override'' and ''message'' identifiers. As the concepts behind those identifiers (virtual methods, message dispatching) isn't applicable for helpers, those keywords are ignored in mode Delphi and not allowed in mode ObjFPC.
  
==Inheritance==
+
In mode Delphi you can't inherit a record helper from another one and you can not use the "inherited" keyword (not even to call the method of the extended record).
A class helper can inherit for another class helper if it extends a subclass of the class which is extend by the parent class helper.
+
 
 +
===Differences between Delphi and FPC===
 +
While the helper feature was implemented as Delphi compatible as possible there is one difference between the two:<br />
 +
In Delphi a helper is basically a class which inherits from a class ''TClassHelperBase'' (both class and record helpers) which in turn inherits from ''TInterfacedObject''. In FPC helpers are a type for themself and don't have a basetype.<br />
 +
As this difference is only visible in the RTTI (which is seldomly used for helpers) this difference was considered neglectable.
 +
 
 +
Because helper are there own type in FPC they also have a custom type kind (''tkHelper'') and their own fields in TTypeData:
 +
* HelperParent: a PTypeInfo field that points to the type info of the parent helper (can be '''nil''')
 +
* ExtendedInfo: a PTypeInfo field that points to the type info of the extended type
 +
* HelperProps: contains the count of the (published) properties the helper contains
 +
* HelperUnit: contains the name of the unit the helper is defined in<br />
 +
The usual RTTI methods can be used to query for properties of the helper.
 +
 
 +
''Note'':
 +
''ExtendedInfo'' and ''HelperUnit'' don't have a Delphi equivalent.
  
 
Example:
 
Example:
 
<delphi>
 
<delphi>
TObjectHelper = class helper for TObject
+
type
   procedure SomeMethod;
+
  TObjectHelper = class helper for TObject
end;
+
   end;
  
TFoo = class(TObject)
+
var
 +
  ti: PTypeInfo;
 +
  td: PTypeInfo;
 +
begin
 +
  ti := TypeInfo(TObjectHelper);
 +
  td := GetTypeData(ti);
 +
{$ifdef fpc}
 +
// you must use this in FPC
 +
if ti^.Kind = tkHelper then begin
 +
  if Assigned(td^.HelperParent) then
 +
    Writeln(td^.HelperParent^.Name)
 +
  else
 +
    Writeln('no helper parent');
 +
  Writeln(td^.ExtendedInfo^.Name);
 +
  Writeln(td^.HelperProps);
 +
  Writeln(td^.HelperUnit);
 
end;
 
end;
 
+
{$else}
TFooHelper = class helper(TObjectHelper) for TFoo
+
// you must use this in Delphi
 +
if ti^.Kind = tkHelper then begin
 +
  if td^.ParentInfo <> TypeInfo(TClassHelperBase) then
 +
    Writeln(td^.HelperParent^.Name)
 +
  else
 +
    Writeln('no helper parent');
 +
  Writeln(td^.PropCount);
 
end;
 
end;
 +
{$endif} 
 +
end.
 
</delphi>
 
</delphi>
 
This allows you to use class methods that where added for a super class with a child class as well. E.g. you have a class helper that is defined for a ''TStrings'', but you have a ''TStringList'' variable and don't want to cast to ''TStrings'' every time you want to use the helper method.
 
 
==Differences between mode Delphi and ObjFPC==
 
In mode Delphi you can use ''virtual'', ''dynamic'', ''override'' and ''message'' identifiers. As the concepts behind those identifiers (virtual methods, message dispatching) isn't applicable for class helpers, those keywords are ignored in mode Delphi and not allowed in mode ObjFPC.
 
On the other hand you need to define "overload" in mode Delphi if you want to introduce a method with the same name as the original one without hiding it. In mode ObjFPC you don't need this (the same way as in normal classes).
 
  
 
==Code examples==
 
==Code examples==

Revision as of 09:22, 14 April 2011

Helper types allow you to extend the scope which the compiler uses to search symbols for. This is useful if you can't extend a class because it's sealed or you can't control of which final type the class instance is (e.g. Lines/Items properties in various LCL components are presented as TStrings, but could be every TStrings descendant). This page describes helper types in Object Pascal. For the equivalent in Objective Pascal see here.

General

If you want to test this feature you (currently) need to checkout a FPC branch using the following command:

svn checkout http://svn.freepascal.org/svn/fpc/branches/svenbarth/classhelpers fpc-classhelpers

You can build the compiler like a normal trunk compiler (you should use FPC 2.4.2 or yet to be released 2.4.4 for building of course).

Note: This feature is complete and awaits the review for the merge and the merge itself.

Declaration

Helper types allow you to extend a given class or record with methods that add more functionality to that type. So you can add default properties, enumerators and wrappers for existing functionality.

Syntax

Currently helper types can be used to extend classes (class helper) and records (record helper) as those are supported by Delphi as well. In theory they could be extended to support interfaces, objects or even primitive types as well (although the latter would need bigger adjustments inside the compiler).

The general syntax for helper types is the following: <delphi> HelperName = class|record helper[(BaseHelper)] for ExtendedType

 [properties, procedures, functions, constructors]

end [hint modifiers]; </delphi>

The sequence class helper declares a helper for a class while record helper declares a helper for a record. The inheritance (BaseHelper) is optional.

HelperName, BaseHelper and ExtendedType have to be valid Pascal identifiers. In case of a class helper ExtendedType must be a class and BaseHelper must be another class helper that extends either the same class as ExtendedType or a parent class of it. For record helpers ExtendedType must be a record and BaseHelper must be a record helper that extend the same record type. The declarations inside a helper are very similiar to the declaration of a normal class, but you must not use fields, destructors and class constructors/destructors.

Note: In mode ObjFPC record helpers need the modeswitch advancedrecords to be declared; in mode Delphi no further modeswitch is necessary.

Implementation

The rules when implementing a helper are a bit differently then when implementing a normal class or record. First of the type of Self is always of the type of the extended type (e.g. TObject in case of a helper declared as class helper for TObject). If a method is not defined in the helper type itself or it's prepended by "inherited", it is first searched in the extended type, then in all the parent helpers of the current helper and then (for class helpers) in the parent classes of the extended class. Please note that in the last case helpers for the parent classes are available as well.

Restrictions

A helper type may not

  • contain (class) destructors
  • contain class constructors
  • contain (class) fields
  • contain abstract methods
  • "override" virtual methods of the extended class (they can be hidden by the helper though)
  • for record helpers: contain constructors (this is not a restriction of helper types themselves, but of FPC, as constructors are not yet supported inside advanced records)

Methods of the extended type can be overloaded (thus they are not hidden by the helper) by using the overload keyword.

Example: <delphi> TObjectHelper = class helper for TObject

 function ToString(const aFormat: String): String; overload;

end;

function TObjectHelper.ToString(const aFormat: String): String; begin

 Result := Format(aFormat, [ToString]);

end;

var

 o: TObject;

begin

 Writeln(o.ToString('The objects name is %s'));

end. </delphi>

Inheritance

Inheritance for helper types fullfills two purposes:

  • use the methods of multiple helpers at once
  • (class helpers only) bring methods that were added to a parent class to the topmost visiblilty (because equally named methods of helpers are hidden by subclasses)

class helper

A class helper can inherit from another class helper if it extends a subclass of the class or the same class which is extend by the parent class helper.

Example: <delphi> TObjectHelper = class helper for TObject

 procedure SomeMethod;

end;

TFoo = class(TObject) end;

TFooHelper = class helper(TObjectHelper) for TFoo end; </delphi>

record helper

A record helper can inherit from another record helper if it extends the same record which is extend by the parent record helper. Important: Inheritance for record helpers is not enabled in mode Delphi, because there this is not allowed.

Example: <delphi> TTest = record end;

TTestHelper = record helper for TTest

 procedure SomeMethod;

end;

TTestHelperSub = record helper(TTestHelper) for TTest end; </delphi>

Example

A simple example for extending existing functionality is the following: The LCL's TCheckListBox, which is a list with checkoxes, does not provide a property to get the count of all checked items. Using a class helper this can be added rather easily. <delphi> type

 TCheckListBoxHelper = class helper for TCheckListBox
 private
   function GetCheckedCount: Integer;
 public
   property CheckedCount: Integer read GetCheckedCount;
 end;

function GetCheckedCount: Integer; var

 i: Integer;

begin

 Result := 0;
 for i := 0 to Items.Count - 1 do
   if Checked[i] then
     Inc(Result);

end;

// somewhere else (TCheckListBoxHelper needs to be in scope) if MyCheckList.CheckedCount > 0 then ... </delphi>

Please note that it's not trivial to implement a cached variation, as helper types can't introduce fields.

Usage

Scope

The methods declared inside the helper are always available once it is in scope and can be called as if they'd belong to the extended type.

Example: <delphi> type

 TObjectHelper = class helper for TObject
   function TheAnswer: Integer;
 end;

function TObjectHelper.TheAnswer: Integer; begin

 Result := 42;

end;

begin

 o := TObject.Create;
 o.TheAnswer;

end. </delphi>

Because of Delphi compatibility only one helper type can be active for a type at the same time. This helper is always the last one that is in scope (which follows the usual rules of Pascal).

Example: <delphi> type

 TTestRecord = record
 end;
 TTestClass = class
 end;
 TTestRecordHelper = record helper for TTestRecord
   procedure Test;
 end;
 TTestClassHelper1 = class helper for TTestClass
   procedure Test1;
 end;
 TTestClassHelper2 = class helper for TTestClass
   procedure Test2;
 end;

var

 tr: TTestRecord;
 tc: TTestClass;

begin

 tr.Test; // this compiles
 tc.Test1; // this won't compile
 tc.Test2; // this compiles

end. </delphi>

Method hiding

The methods of helper types hide the methods declared in the extended type as long as they are not declared with "overload".

If a class helper is in scope for a parent class then only he methods of the parent class are hidden while the methods of the subclass will stay visible.

Example: <delphi> type

 TTest = class
   procedure Test;
 end;
 TTestSub = class(TTest)
   procedure Test;
 end;
 TTestHelper = class helper for TTest
   procedure Test;
 end;

var

 t: TTest;
 ts: TTestSub;

begin

 t.Test; // this calls "Test" of the helper
 ts.Test; // this calls "Test" of TTestSub

end. </delphi>

Restrictions

You can not reference helper types anywhere in the code except for the following cases:

  • inheriting from one
  • using one in (Bit)SizeOf
  • using one in TypeInfo

Differences

Differences between mode Delphi and ObjFPC

In mode Delphi you can use virtual, dynamic, override and message identifiers. As the concepts behind those identifiers (virtual methods, message dispatching) isn't applicable for helpers, those keywords are ignored in mode Delphi and not allowed in mode ObjFPC.

In mode Delphi you can't inherit a record helper from another one and you can not use the "inherited" keyword (not even to call the method of the extended record).

Differences between Delphi and FPC

While the helper feature was implemented as Delphi compatible as possible there is one difference between the two:
In Delphi a helper is basically a class which inherits from a class TClassHelperBase (both class and record helpers) which in turn inherits from TInterfacedObject. In FPC helpers are a type for themself and don't have a basetype.
As this difference is only visible in the RTTI (which is seldomly used for helpers) this difference was considered neglectable.

Because helper are there own type in FPC they also have a custom type kind (tkHelper) and their own fields in TTypeData:

  • HelperParent: a PTypeInfo field that points to the type info of the parent helper (can be nil)
  • ExtendedInfo: a PTypeInfo field that points to the type info of the extended type
  • HelperProps: contains the count of the (published) properties the helper contains
  • HelperUnit: contains the name of the unit the helper is defined in

The usual RTTI methods can be used to query for properties of the helper.

Note: ExtendedInfo and HelperUnit don't have a Delphi equivalent.

Example: <delphi> type

 TObjectHelper = class helper for TObject
 end;

var

 ti: PTypeInfo;
 td: PTypeInfo;

begin

 ti := TypeInfo(TObjectHelper);
 td := GetTypeData(ti);

{$ifdef fpc} // you must use this in FPC if ti^.Kind = tkHelper then begin

 if Assigned(td^.HelperParent) then
   Writeln(td^.HelperParent^.Name)
 else
   Writeln('no helper parent');
 Writeln(td^.ExtendedInfo^.Name);
 Writeln(td^.HelperProps);
 Writeln(td^.HelperUnit);

end; {$else} // you must use this in Delphi if ti^.Kind = tkHelper then begin

 if td^.ParentInfo <> TypeInfo(TClassHelperBase) then
   Writeln(td^.HelperParent^.Name)
 else
   Writeln('no helper parent');
 Writeln(td^.PropCount);

end; {$endif} end. </delphi>

Code examples

The following table lists some examples for class helpers found on the web and whether they work with the current implementation.

URL State
Class helper to add for ... in support for TComponent.Components / ComponentCount ok
Class helper for Delphi's TStrings: Implemented Add(Variant) ok