Delphi 2009 is op 25 augustus 2008 aangekondigd, en was vanaf maandag 8 september leverbaar (alhoewel mensen met een subscription minimaal een week en soms nog langer op hun upgrade moesten wachten). Wat betreft nieuwe features ging de meeste aandacht uit naar de Unicode ondersteuning, gevolgd door DataSnap (waar Pawel een artikel over zal schrijven), maar ook de andere taaluitbreidingen zoals Generics en Anonymous Methods zijn beslist de moeite waard. Probleem is een beetje dat de on-line help zoals gewoonlijk wat achterloopt, en dus zijn niet alle (on)mogelijkheden even goed gedocumenteerd. Maar ik hoop met dit artikel (en mijn sessie tijdens de SDC) een beetje licht in de schemering te kunnen werpen.
De syntax van Generics is grotendeels vergelijkbaar met Delphi for .NET
Generics
Laten we beginnen met Generics, of Parameterized Types, zoals ze ook wel in Delphi worden genoemd. Deze vinden hun oorsprong eerlijk gezegd in .NET, en waren vorig jaar beloofd als nieuwe feature toen het niet haalbaar bleek om Generics al toe te voegen aan Delphi 2007 for Win32. De syntax is grotendeels vergelijkbaar met die van de Delphi for .NET syntax, met als uitzondering dat het .NET Framework een aantal zaken regelt die onder Win32 niet mogelijk zijn (in .NET kun je alles naar een object boxen, en bestaan er al standaard interfaces om values met elkaar te vergelijken).
Een generic type definitie kan gebruikt worden om functionaliteit te presenteren waarbij je van te voren nog niet weet of je die voor integers, strings, floats, of de een-of-andere object class wilt laten gebruiken. Als syntax worden de vishaken gebruikt met een type aanduiding ertussen (meestal een T, maar je kan er alles voor gebruiken, behalve reserved keywords).
Om een verzameling van elementen van het type T weer te geven, gerepresenteerd door een array van T, kunnen we het volgende schrijven:
type
Verzameling<T> = Array of T;
We kunnen variabelen direct van het type Verzameling<T> declareren, waarbij we dan wel voor de T een daadwerkelijk type moeten aangeven (de instantie van het type parameter als het ware), bijvoorbeeld als volgt:
type
VerzamelingIntegers = Verzameling<integer>;
var
X: VerzamelingIntegers;
Y: Verzameling<integer>; // kan natuurlijk ook
Behalve de type parameter T kunnen we ook constraints toevoegen, die daarmee aangeven dat we bij de instantiatie van het type aan bepaalde voorwaarden moeten voldoen. Een constraint is ofwel een class type waar de T van afgeleid moet zijn, of een interface die de T moet implementeren.
Dus om een verzameling van elementen van een type T weer te geven waarbij T ten minste een TComponent moet zijn, kunnen we de volgende syntax gebruiken:
type
Verzameling<T: TComponent> = Array of T;
Het type Verzameling<integer> zal nu niet meer compileren, maar Verzameling<TComponent> wel, en dat geldt ook voor de elementen die dan minstens van type TComponent moeten zijn.
We kunnen behalve TComponent ook TPersistent gebruiken, maar het is niet mogelijk om TObject als constraint op te geven. In plaats daarvan moeten we dan het keyword class gebruiken:
type
Verzameling<T: class> = Array of T;
En dit zegt dat we een verzameling kunnen bouwen van elementen die class instanties zijn (en geen integer of strings bijvoorbeeld).
Generic Stack
Om eens een voorbeeld te geven van het gebruik van Generics is het noodzakelijk om ook functionaliteit toe te voegen. Alleen maar een Array van een type T is niet zo zinvol, maar als je er ook mee kunt manipuleren (of onderling vergelijken, zoals ik aan het eind van dit artikel ga doen), dan is het plotseling een stuk handiger.
Een voor de hand liggend voorbeeld is de Generic TStack, die als volgt gedefinieerd kan worden:
type
TStack<AnyType> = class
protected
FCount: Word;
FStack: Array of AnyType;//needs to grow when needed
public
constructor Create(Capacity: Word = 42);
destructor Destroy; override;
function Count: Integer;
function Push(const item: AnyType): Word;
function Peek: AnyType;
function Pop: AnyType;
end;
Omdat ik een array gebruik om de elementen van type T in op te slaan, moeten we zorgen dat de stack groot genoeg is (en blijft). Als eerste voorziening heb ik de constructor een default argument gegeven die de stack een opbergruimte van maximaal 42 elementen geeft (waar in het begin nog niks in zit natuurlijk, alleen de maximale ruimte is vast gereserveerd).
Als de elementen instanties van classes zijn, dan wordt hun destructor niet aangeroepen … dit kan dus tot een geheugenlek leiden als je de stack niet hebt leeggemaakt voor je hem weggooit
De destructor roept de SetLength weer aan om het array weer op te ruimen. Let op: als de elementen instanties van classes zijn, dan wordt hun destructor niet aangeroepen; dit kan dus tot een geheugenlek leiden als je de stack niet hebt leeggemaakt voor je hem weggooit. De TStack is dus niet de eigenaar van de items die je erin stopt (dat kun je er wel van maken, maar dat moet je bijvoorbeeld aangeven dat de elementen minstens classes zijn via de constraint syntax, en dan in de destructor eerst Free aanroepen van alle elementen voordat je de SetLength op het array aanroept – maar dat laat ik over als oefening voor de lezer).
constructor TStack<AnyType>.Create(Capacity: Word = 42);
begin
inherited Create;
FCount := 0;
SetLength(FStack, Capacity)
end;
destructor TStack<AnyType>.Destroy;
begin
SetLength(FStack,0);
inherited
end;
De Push methode moet eerst kijken of het interne array van de stack wel groot genoeg is om het nieuwe element op te slaan. Zo niet, dan moet SetLength opnieuw worden aangeroepen om het array te laten groeien. Hiertoe kunnen we SetLength weer opnieuw aanroepen (die er tevens voor zorgt dat alle bestaande elementen uit het array meegenomen worden naar de nieuwe lengte). In de code vergroot ik het array met stapjes van 1 (door de nieuwe benodigde count mee te geven), maar het is wellicht slimmer om dit in iets grotere stappen te doen, afhankelijk van de behoefte aan extra opslagruimte van de stack.
De implementatie van de Peek en Pop spreekt voor zich, neem ik aan. Merk op dat we hier steeds het AnyType gebruiken, waardoor de stack dus inderdaad van alles kan bevatten, van strings tot integers of TDataSets.
function TStack<AnyType>.Count: Integer;
begin
Result := FCount
end;
function TStack<AnyType>.Push(const item: AnyType): Word;
begin
Inc(FCount);
if FCount > Length(FStack) then
SetLength(FStack, FCount);
FStack[FCount-1] := item;
Result := FCount
end;
function TStack<AnyType>.Peek: AnyType;
begin
Result := FStack[FCount-1]
end;
function TStack<AnyType>.Pop: AnyType;
begin
Result := Peek;
Dec(FCount)
end;
We kunnen b.v de TStack gebruiken voor een lijst van strings die we in omgekeerde volgorde weer willen printen, en wel als volgt:
type
TStringStack = TStack<string>;
var
S: TStringStack;
begin
S := TStringStack.Create;
try
S.Push('Delphi 2009');
S.Push('C++Builder 2009');
writeln(S.Pop);
writeln(S.Pop);
finally
S.Free
end;
end.
Of we gebruiken hem als een stack van TPersistent classes, waarbij we bijvoorbeeld de Name en de ClassName maar ook de UnitName (ook een nieuwe property in Delphi 2009) kunnen laten zien:
procedure TFormMain.ButtonClick(Sender: TObject);
var
CompStack: TStack<TPersistent>;
Comp: TComponent;
begin
CompStack := TStack<TPersistent>.Create;
try
for Comp in Self do CompStack.Push(Comp);
while CompStack.Count > 0 do
begin
Comp := CompStack.Pop as TComponent;
ShowMessage(Comp.Name + ': ' + Comp.ClassName +
' (' + Comp.UnitName + ')') end
finally
CompStack.Free
end
end;
Voor dit voorbeeld is het maar goed dat de stack niet zijn elementen allemaal “free’t”, want anders zou het Form plotseling leeg zijn nadat we deze code hebben uitgevoerd (leuk voor een demo, maar niet leuk in praktijk).
Overigens zijn er al veel Generic types te vinden in de unit Generics.Collections, dus moeten we opletten niet het wiel opnieuw uit te vinden.
Generic Methods
Behalve Generic Types maak ik zelf ook veel gebruik van Generic Methods. Het enige jammere is dat ik hier geen gewone lokale routines voor kan gebruiken, maar dat in de vorm van class methods moet doen.
We kunnen hierbij de type parameter op twee plaatsen toevoegen: of bij de class of bij de method zelf. In de volgende code snippet staan beide alternatieven naast elkaar:
type
TGeneric<AnyType> = class
class procedure Swap(var X,Y: AnyType); static;
end;
type
TGeneric = class
class procedure Swap<AnyType>(var X,Y: AnyType);
static;
end;
Als het maar om één method gaat is het verschil niet zo groot, maar als er meerdere methods worden toegevoegd zul je al snel merken dat het flexibeler is om de type parameter toe te voegen aan de method en niet aan de class. Zeker als er methods komen die wellicht meer dan één type parameter hebben.
De implementatie van de Generic Swap methode zelf is redelijk eenvoudig: een derde variabele van type AnyType en klaar is kees:
class procedure TGeneric.Swap<AnyType>(var X,Y:AnyType);
var
Z: AnyType;
begin
Z := X;
X := Y;
Y := Z
end;
Bij het aanroepen hoeven we natuurlijk geen instantie van TGeneric aan te maken, maar kunnen we direct de class function Swap aanroepen, met daarbij een specificatie voor de type parameter en de juiste argumenten.
Voor een tweetal integer variabelen kunnen we dit als volgt doen:
var
A,B: Integer;
begin
A := 42;
B := 17;
TGeneric.Swap<integer>(A,B);
Op dezelfde manier kunnen we andere class methods bouwen zoals een IFF (afhankelijk van een expressie krijg je de TrueValue of the FalseValue terug) of een ChooseDef (kies een item uit een lijst, maar als de index buiten het bereik van de lijst is krijg je een default waarde terug).
type
TGeneric = class
class function IFF<AnyType>(
const Expression: Boolean;
TrueValue: AnyType; FalseValue: AnyType): AnyType;
class function ChooseDef<AnyType>(index: Integer;
const values: Array of AnyType; default: AnyType):
AnyType;
end;
Hier zien we een verschil met het .NET Framework, waar de default waarde van een type door het framework zelf wordt gegeven. Dat bestaat in Win32 niet, vandaar dat ik de default waarde van het type AnyType) als extra argument aan de generic method heb toegevoegd, en we hem bij iedere aanroep moeten meegeven. Niet zo elegant als de .NET implementatie, maar het werkt net zo goed.
De implementatie van beide generic methods is als volgt:
class function TGeneric.IFF<AnyType>(
const Expression: Boolean; TrueValue,
FalseValue: AnyType): AnyType;
begin
if Expression then Result := TrueValue
else
Result := FalseValue
end;
class function TGeneric.ChooseDef<AnyType>(
index: Integer; const values: array of AnyType;
default: AnyType): AnyType;
begin
if (index >= Low(values)) and (index <= High(values))
then Result := values[index]
else Result := default
end;
Voor ik verder ga met een laatste Generic Method (om het grootste of kleinste element van een lijst van type AnyType te vinden) wil ik eerst even een uitstapje maken naar een noodzakelijk bouwsteen daarvoor: de Anonymous Methods.
Anonymous Methods
Een Anonymous method is een methode zonder naam, maar wel een stuk code binnen een procedure of function sectie. Zoiets als een stuk code dat je via copy-en-paste ergens hebt neergezet maar met een “wrapper” eromheen. Ze zijn zeker niet overal even goed toepasbaar, en leiden ook niet altijd tot leesbare code, maar in sommige situaties kunnen ze een hulpmiddel zijn om de expressiemogelijkheden van de taal Delphi te vergroten. Aan de hand van een eerste eenvoudig voorbeeld zal ik tot slot een tweetal nuttige toepassingen van Anonymous Methods laten zien.
Een Anonymous Method geven we aan met de keywords “reference to procedure” (of function), inclusief de parameterlijst. Het invullen gaat dan in een blok code, waarbij er geen puntkomma komt tussen de header en de eerste begin.
type
TProc = reference to procedure(x: Integer);
procedure call(const proc: TProc);
begin
proc(42);
end;
var
TheAnonymousMethod: TProc;
begin
TheAnonymousMethod := procedure(a: Integer)
begin
Button1.Caption := IntToStr(a)
end; // einde van de Anonymous Method
call(TheAnonymousMethod)
end;
De assignment van het stukje code aan de variabele “TheAnonymousMethod” ziet er op het eerste gezicht niet zo leesbaar uit, vooral niet als het stuk code lang(er) wordt. Er zijn echter wel voordelen te halen uit het gebruik van Anonymous Methods, zoals ik in de volgende voorbeelden zal laten zien.
Een van de plaatsen waar Anonymous Methods van waarde kunnen zijn, is binnen de synchronize methode van een TThread component
Nuttige Anonymous Methods
Een van de plaatsen waar Anonymous Methods van waarde kunnen zijn, is binnen de synchronize methode van een TThread component. Dit zou anders een extra methode kosten, inclusief velden voor de parameters van deze methode. Dit is o.a. in meer detail besproken in de weblog van Allen Bauer (zie http://blogs.codegear.com/abauer/2008/09/08/38868), waar hij spreekt over het thrddemo project in de C:\Documents and Settings\All Users\Documents\RAD Studio\6.0\Demos\DelphiWin32\VCLWin32\Threads directory. Grappig genoeg is de daadwerkelijke demo niet aangepast volgens zijn suggesties, dus kunnen we dat zelf uitproberen. De oorspronkelijke code is terug te vinden in bovenstaande locatie, maar de aangepaste versie met Anonymous Methods zou er als volgt uit komen te zien:
procedure TSortThread.VisualSwap(A, B, I, J: Integer);
begin
{$IFDEF ANONYMOUS}
Synchronize(procedure
begin
with FBox do
begin
Canvas.Pen.Color := clBtnFace;
PaintLine(Canvas, I, A);
PaintLine(Canvas, J, B);
Canvas.Pen.Color := clRed;
PaintLine(Canvas, I, B);
PaintLine(Canvas, J, A)
end
end)
{$ELSE}
FA := A;
FB := B;
FI := I;
FJ := J;
Synchronize(DoVisualSwap);
{$ENDIF}
end;
De {$IFDEF ANONYMOUS} kan gebruikt worden om te switchen tussen de oorspronkelijke code (die de extra methode DoVisualSwap aanroept) en de aanroep van de Anonymous Method die de parameters A, B, I en J direct ontvangt.
Een tweede voorbeeld van een nuttig gebruik van Anonymous Methods heb ik gevonden bij het maken van generic methods voor het bepalen van het grootste of kleinste element uit een lijst van items van een bepaald type. Omdat hier de vergelijking tussen twee elementen cruciaal is, kunnen we dit niet zomaar implementeren. Daar hebben we iets extra’s voor nodig, wat we o.a. terugvinden in de Generics.Defaults unit.
Generics.Defaults
Delphi 2009 bevat de units Generics.Defaults en Generics.Collections die al een hoop voorgedefinieerde nuttige voorbeelden van Generics (en Anonymous Methods) bevatten. Zo bevat Generics.Defaults de definitie van het IComparer<T> interface dat we straks willen gebruiken, en ook een TComparison<T> Anonymous Method kunnen we hier terugvinden:
type
IComparer<T> = interface
function Compare(const Left, Right: T): Integer;
end;
TComparison<T> = reference to function(
const Left, Right: T): Integer;
Deze Anonymous Method kan o.a. gebruikt worden in de aanroep van de Construct methode van de TComparer<T> class, die het IComparer<T> interface implementeert.
TComparer<T> = class(TInterfacedObject, IComparer<T>)
public
class function Default: IComparer<T>;
class function Construct(const Comparison:
TComparison<T>): IComparer<T>;
function Compare(const Left, Right: T): Integer;
virtual; abstract;
end;
Daarnaast bevat de Generics.Defaults unit voorbeelden om de gelijkheid van generic values te bepalen, en een niet-reference counted IInterface implementatie in het type TSingletonImplementation, inclusief de implementaties van QueryInterface, _AddRef en _Release. Zie de source code voor details.
Combinatie van Anonymous Methods en Generics
Het laatste voorbeeld is er eentje dat ik zelf regelmatig gebruik: een generic method die de grootste – of kleinste – waarde uit een lijst van values van een bepaald type teruggeeft. Vergelijkbaar met de ChooseDef, maar dan inclusief een vergelijking zoals we die in het IComparer interface uit de Generics.Defaults unit zagen.
De definitie van mijn functions Min en Max is als volgt:
type
TGeneric = class
class function Min<AnyType>(const values:
array of AnyType;
const Comparer: IComparer<AnyType>): AnyType;
class function Max<AnyType>(const values:
array of AnyType;
const Comparer: IComparer<AnyType>): AnyType;
end;
Als eerste argument geef ik de lijst van values van type AnyType mee, en als tweede argument het IComparer interface van hetzelfde type. Hoe we aan deze IComparer komen is nog even niet van belang – dat wordt tijdens de aanroep geregeld (met een Anonymous Method).
Wat we eerst moeten bekijken is de implementatie van de Min en Max functies, waarbij we de Compare functie van het IComparer interface kunnen gebruiken om steeds twee values van het AnyType te vergelijken met elkaar. De Compare geeft een positief getal terug als links groter is dan rechts, en anders een negatief getal. In feite kun je het zien als “links min rechts” als het getallen zouden zijn.
class function TGeneric.Max<AnyType>(const values:
array of AnyType;
const Comparer: IComparer<AnyType>): AnyType;
var
item: AnyType;
begin
if length(values) >= 1 then
begin
Result := values[Low(values)];
for item in values do
if Comparer.Compare(item,Result) > 0 then
Result := item
end
end;
class function TGeneric.Min<AnyType>(const values:
array of AnyType;
const Comparer: IComparer<AnyType>): AnyType;
var
item: AnyType;
begin
if length(values) >= 1 then
begin
Result := values[Low(values)];
for item in values do
if Comparer.Compare(item,Result) < 0 then
Result := item
end
end;
Tot nu toe is de code nog goed te lezen en niet al te vreemd gezien de generic methods die ik eerder liet zien. De slagroom op de taart krijgen we wanneer we de Min of Max willen aanroepen, bijvoorbeeld door het grootste getal uit een dynamisch array van integers op te leveren. Het eerste deel van de aanroep van Max zou er als volgt uit kunnen zien:
TGeneric.Max<integer>([1,2,4,8,16,32,64,42,36,13],
Voor het tweede argument hebben we dan iets nodig dat het IComparer interface oplevert. En daarvoor moeten we even terugdenken aan de Generics.Defaults unit, met daarin de TComparer class met de Construct class function die het IComparer interface teruggeeft. Omdat de Construct method een class function is, kunnen we gewoon TComparer<integer>.Construct aanroepen om een IComparer<integer> terug te krijgen. Moeten we alleen nog het Comparison argument van de Construct meegeven, en dat is een TComparison<T> oftewel een reference to function(const Left, Right: T): Integer;
Om een lang verhaal kort te maken: bij de aanroep van Construct kunnen we dus meteen een Anonymous Method schrijven en die meegeven als argument, zoals in de volgende code te zien is:
TGeneric.Max<integer>([1,2,4,8,16,32,64,42,36,13],
TComparer<integer>.Construct(
function(const Left,Right: integer): integer
begin
Result := Left-Right
end))
De Anonymous Method doet in feite niets anders dan Rechts van Links aftrekken om zo het gewenste gedrag te implementeren dat binnen de Max functie verwacht wordt. Tijdens de aanroep van TGeneric.Max<integer> geef ik dus behalve het type en de lijst van elementen van dat type ook meteen een dynamische vergelijk-functie mee voor elementen van dat type (in de vorm van een Anonymous Method). En dat moet je misschien twee of drie keer lezen voor het duidelijk wordt, maar werkt wel perfect.
Als variatie op dit thema wil ik nog een laatste voorbeeld geven waarbij ik van een lijst van componenten de grootste wil hebben, en ik “grootste” definieer als het component met de langste string die de nieuwe ToString functie teruggeeft. Hiermee geef ik meteen aan dat ik van aanroep tot aanroep de implementatie van de vergelijking tussen twee elementen kan aanpassen door een andere Anonymous Method te schrijven.
Dit voorbeeld ziet er uiteindelijk als volgt uit, waarbij ik eerst een array van TComponents maak door de Self.Components van een TForm af te lopen:
procedure TFormX.ButtonClick(Sender: TObject);
var
SelfComponents: array of TComponent;
i: Integer;
begin
SetLength(SelfComponents, Self.ComponentCount);
for i:=0 to Self.ComponentCount-1 do
SelfComponents[i] := Self.Components[i];
ShowMessage(
TGeneric.Max<TComponent>(SelfComponents,
TComparer<TComponent>.Construct(
function(const Left,Right: TComponent): integer
begin
Result := Length(Left.ToString) –
Length(Right.ToString)
end)).ToString
)
end;
Met deze code in de OnClick van een TButton op een form krijg je als resultaat de inhoud van het component met de langste ToString waarde. Maar natuurlijk is het nu niet moeilijk meer om een eigen criterium te implementeren, of dit principe te gebruiken voor andere Generics en/of Anonymous Methods.
Conclusie
In dit artikel heb ik laten zien hoe de nieuwe Delphi 2009 taalelementen voor Generic Types en Generic Methods alsmede Anonymous Methods in elkaar zitten en hoe we die zelf kunnen maken en gebruiken. Vooral de combinatie van Generics en Anonymous Methods levert fijne nieuwe mogelijkheden die voorheen niet mogelijk waren zonder dit allemaal als vele losse routines uit te schrijven.
Wie nog vragen of opmerkingen heeft kan me altijd per e-mail bereiken of een bezoek brengen aan mijn nieuwe trainingsruimte in Helmond Brandevoort (zie www.eBob42.com voor details).
De sources die bij dit artikel horen kun je downloaden via Swart_Delphi2009Taaluitbreidingen_SRC.zip.