søndag den 18. december 2011

Smart Pointers og function Invoke: T;

Smart pointers har, indtil for nyligt, ikke været muligt at implementere i Delphi grundet manglen på Generics. Generics kom som bekendt i Delphi 2009.

Lad mig først starte med at citere Wikipedia for at få defineret præcis, hvad smart pointers egentligt er:

"In computer science, a smart pointer is an abstract data type that simulates a pointer while providing additional features, such as automatic garbage collection or bounds checking. These additional features are intended to reduce bugs caused by the misuse of pointers while retaining efficiency. Smart pointers typically keep track of the objects they point to for the purpose of memory management."

Så med andre ord er smart pointers en datatype med garbage collection, altså noget hvor Delphi selv rydder op, når der ikke er flere referencer til klassen. Det smarte ved smart pointers er, at de transformerer sig selv, når man spørger på deres indhold. Det er det, som dette indlæg, handler om.

Siden Delphi 3 (tror jeg nok) har vi haft interfaces i Delphi. Fidusen ved interfaces er bl.a., at man har en reference counter. Ved samtidig at nedarve fra TInterfacedObject opnår man, at objektet bliver nedlagt, når reference counter er 0 - altså med andre ord: garbage collection.

Lad mig vise et simpelt eksempel :

uses
SysUtils;


type
  iMyClass = interface
    procedure HelloWorld;
  end;

  TMyClass = class(TInterfacedObject, iMyClass)
    procedure HelloWorld;
    destructor Destroy; override;
  end;

{ TMyClass }

procedure TMyClass.HelloWorld;
begin
  ShowMessage('Hello World');
end;

destructor TMyClass.Destroy;
begin
  ShowMessage('Goodbye World');
  inherited;
end;

procedure TForm1.Button1Click(Sender: TObject); var myClass: iMyClass; begin myClass := TMyClass.Create; myClass.HelloWorld;
end;

Ganske som forventet får man to beskeder ud af det: Én, når man kalder metoden HelloWorld, og én, når klassen nedlægges. Intet nyt her. Ulempen er bare, at man kun kan kalde de metoder, der ligger på interfacet, og at man således er nødt til at kopiere alle metoderne fra klassen til interfacet først. Generelt er det temmelig ufleksibelt.

Her kommer smart pointers ind i billedet, fordi det er et interface, hvor man kan tilgå alle metoderne fra klassen uden først at skulle kopiere dem op på interfacet.

Når man - i sin kode - skriver ordet "myClass" og sætter et efterfølgende punktum, så får man listen af tilgængelige metoder. Listen er afgjort af, hvad en metode, kaldet "Invoke", returnerer. Denne metode retunerer bare interfacet selv, og således er den afgørende for, hvad man kan se af metoder. Men hvis nu man overstyrer Invoke, så kan man selv bestemme, hvad Invoke skal returnere og dermed, hvad man vil blive tilbudt i listen...

Lad mig vise noget kode. Som sædvanligt har jeg skrevet kommentarer i koden, således at jeg slipper for en lang forklaring bagefter.

unit SmartPointerU;

interface

uses
  SysUtils;

type
  //En reference til en contructor
  //eller en anden funktion, der retunerer et object.
  ISmartPointer<T> = reference to function: T;

  //I definitionen på TSmartPointer angiver jeg at T er en klasse.
  //Jeg angiver også, at jeg vil oprette en ny instans af mit object af typen "T"
  //Hvis ikke jeg skriver <T: class, Contructor> får jeg følgende fejl:
  //[DCC Error] SmartPointerU.pas(31): E2568 Can't create new instance without CONSTRUCTOR constraint in type parameter declaration
  TSmartPointer<T: class, constructor> = class(TInterfacedObject, ISmartPointer<T>)
  private
    //Object'et der arbejdes på
    FValue: T;
  public
    //Default constructor
    constructor Create; overload;

    //Overloaded contructor som tager en instans ind som parameter
    constructor Create(AValue: T); overload;

    destructor Destroy; override;

    //Invoke-funktionen som bliver kaldt når man skriver 
    //myInterface efterfulgt af punktum. Der skal ikke kaldes 
    //overload på funktionen. Det finder compileren selv ud af.
    function Invoke: T;
  end;

implementation

{ TSmartPointer<T> }

constructor TSmartPointer<T>.Create;
begin
  inherited;
  FValue := T.Create;
end;

constructor TSmartPointer<T>.Create(AValue: T);
begin
  inherited Create;
  if AValue = nil then
    FValue := T.Create
  else
    FValue := AValue;
end;

destructor TSmartPointer<T>.Destroy;
begin
  FValue.Free;
  inherited;
end;

function TSmartPointer<T>.Invoke: T;
begin
  Result := FValue;
end;

end.

Så lidt kode skal der til. Så klarer Delphi resten. Herefter er det bare tage klassen i brug. Til det formål har jeg skrevet en testklasse. Den har bare to properties: StringProperty og IntegerProperty samt en TMemo til log af fx oprettelse og nedlæggelse af klasserne, mm. Jeg vil ikke vise implementeringen af klassen her, da den kan ses i demo projektet. Her vil jeg blot vise brugen af klassen, samt klasse definitionen:

Klasse definition : 


type
  TCustomTestClass = class abstract
  strict private
    procedure SetLoggingMemo(const Value: TMemo);
  protected
    FLoggingMemo: TMemo;
    function LogMessage(const aMessage: String): String;
  public
    constructor Create(const aLoggingMemo: TMemo);
    destructor Destroy; override;
  published
    property LoggingMemo: TMemo read FLoggingMemo write SetLoggingMemo;
  end;

  TTestClass = class sealed(TCustomTestClass)
  private
    FIntegerProperty: Integer;
    FStringProperty: String;
    procedure SetIntegerProperty(const Value: Integer);
    procedure SetStringProperty(const Value: String);
  public
    property StringProperty: String read FStringProperty write SetStringProperty;
    property IntegerProperty: Integer read FIntegerProperty write SetIntegerProperty;
  end;

Brugen af TTestClass:


procedure TMainU.Button1Click(Sender: TObject);
var
  Test: ISmartPointer<TTestClass>;
begin
  Test := TSmartPointer<TTestClass>.Create(TTestClass.Create(Memo1));
end; procedure TMainU.Button2Click(Sender: TObject); var Test: ISmartPointer<TTestClass>; begin Test := TSmartPointer<TTestClass>.Create(); Test.LoggingMemo := Memo1; Test.IntegerProperty := 7; end; procedure TMainU.Button3Click(Sender: TObject); var Test: ISmartPointer<TTestClass>; TestObj: TTestClass; begin TestObj := TTestClass.Create(Memo1); TestObj.StringProperty := 'Dette er en test'; Test := TSmartPointer<TTestClass>.Create(TestObj); Test.IntegerProperty := 8; end; procedure TMainU.FormCreate(Sender: TObject); begin Memo1.Clear; end;

Loggen, og skærmbilledet fra demo projektet:


Som det ses i demo projektet, så nedlægges klasserne af sig selv. Det ses også, at man har direkte adgang til alle metoderne på klasserne via interfacet - uden at skulle kopiere dem op på interfacet selv. De to forskellige constructors er også vist i demo projektet. 

Hermed har jeg dels vist, hvad en smart pointer er, men også hvordan man implementerer sådan en struktur i Delphi.

Husk det komplette eksempel med source kode kan hentes her.

Jens Borrisholt

Ingen kommentarer:

Send en kommentar