Generische Containerklasse aus TInterfaceList kann keine Procedures

    Generische Containerklasse aus TInterfaceList kann keine Procedures

    Hallo zusammen,

    Ich habe mir eine generische Containerklasse für Interfaces zusammengebastelt. Prinzipiell habe ich mich dabei sehr stark am Delphi-Treff-Tutorial zu TObjectList orientiert (Abschnitt TObjectList an Klasse anpassen), eben statt TObjectList TInterfaceList verwendet und Generics eingebaut. Hier der Methodenkopf, damit dürfte einigermaßen klar sein, was ich verändert habe (Ansonsten kann ich gerne auch noch den Implementationsteil nachliefern):

    Delphi-Code

    1. type TBasicInterfaceList<IInt: IInterface> = class(TInterfaceList)
    2. protected
    3. function getItem(Index: Integer): IInt; virtual;
    4. procedure setItem(Index: Integer; intfc: IInt); virtual;
    5. public
    6. constructor Create();
    7. function Add(intfc: IInt): Integer; virtual;
    8. procedure AddList(list: TBasicInterfaceList<IInt>);
    9. function IndexOf(Objekt: IInt): Integer; virtual;
    10. procedure Insert(Index: Integer; Objekt: IInt); virtual;
    11. function First: IInt; virtual;
    12. function Last: IInt; virtual;
    13. property Items[Index: Integer]: IInt read getItem write setItem; default;
    14. end;


    Das funktioniert weitestgehend. Allerdings gibt es folgendes Problem:

    Ich nehme ein Interface

    Delphi-Code

    1. type IIntfc = interface
    2. procedure Execute;
    3. end;

    und bastle mir damit eine Containerklasse

    Delphi-Code

    1. type TIntfcList = TBasicInterfaceList<IIntfc>;

    Jetzt baue ich eine Klasse, die von IIntfc abgeleitet ist und Execute implementiert.

    Delphi-Code

    1. type TKlasse1 = class(TInterfacedObject, IIntfc)
    2. procedure Execute; //Execute enthält nur die Anweisung showMessage('XXXXX');
    3. end;

    Wenn ich nun folgendes mache:

    Delphi-Code

    1. procedure TForm2.FormCreate(Sender: TObject);
    2. var klasse1: IIntfc;
    3. list: TIntfcList;
    4. begin
    5. list := TIntfcList.Create;
    6. klasse1 := TKlasse1.Create;
    7. list.Add(klasse1);
    8. list.Items[0].Execute;
    9. end;

    ...erhalte ich eine schöne EAccessViolation.
    Verändere ich aber die letzte Zeile (Zeile 8 ) zu

    Delphi-Code

    1. (list.Items[0] as TKlasse1).Execute;

    dann funktioniert es. Offenkundig muss ich also die Klasse angeben, was ich aber nicht so toll finde.

    Jetzt die Frage: Wie schaffe ich es, dass die obere FormCreate ohne EAccessViolation zum Laufen bekomme?
    Getter und Setter:

    Delphi-Code

    1. function TBasicInterfaceList<IInt>.getItem(Index: Integer): IInt;
    2. begin
    3. Result := IInt(inherited Items[Index]);
    4. end;
    5. procedure TBasicInterfaceList<IInt>.setItem(Index: Integer; intfc: IInt);
    6. begin
    7. inherited Items[Index] := intfc;
    8. end;


    Noch der Hinweis an der Stelle, dass ich offenbar schon über Items auslesen kann. Wenn ich ihn frage, ob Element 0 ein TKlasse1 ist, gibt er mir true.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „fsmman“ ()

    Das sieht sehr vielversprechend aus, jedoch kann ich nicht nachvollziehen, wie sie damals das Problem gelöst haben.

    Offensichtlich haben sie den Getter verändert, sodass er auf mein Problem übertragen folgendermaßen aussieht:

    Delphi-Code

    1. function TGenericInterfaceList<IInt>.Get(Index: Integer): IInt;
    2. begin
    3. (inherited Items[Index]).QueryInterface(GetTypeData(TypeInfo(IInt)).Guid, Result);
    4. end;


    Mein Delphi 2010 kennt leider einige dieser Befehle gar nicht (Oder ich kenne die Unit nicht, in denen sie zu finden sind; es geht um getTypeData().Guid).

    Es wurde weiter eine Möglichkeit erläutert, eine Art Klasse zu basteln, wobei mir weder klar ist, wie man diese einsetzen soll, noch, wie das mein Problem löst. Denn wenn ich mein Item zu TObject caste, so verliere ich ja die Information über die Execute-Prozedur. Oder mache ich einen Denkfehler?
    Diese Methode finde ich ziemlich übel. Wenn du mal überlegst, was da eigentlich gemacht wird:
    Im Prinzip will man nur einen Container haben, der die Interfaces verwaltet. Und ein Interface ist nichts weiter als ein Pointer auf eine spezifische Speicheranordnung.1
    Im Grunde genommen hast du nur einen Pointer. Und den willst du in einer Liste abspeichern. Um genau diesen Pointer dann aber wieder korrekt zu erhalten, musst du RTTI-Informationen auslesen und QueryInterface aufrufen. Das klingt schon irgendwie absurd.
    Vielleicht habe ich ja irgend etwas nicht bedacht, aber im Anhang mein Vorschlag für eine schnellere Implementierung der InterfaceList. Und auch wenn da viel hart gecastet wird, ist die von der Assembler-Seite her viel einfacher; man muss Delphi nur überzeugen, das genauso zu sehen.
    (@Thomas: .pas ist nicht als Dateierweiterung für Anhänge zugelassen. Das sollte vielleicht mal geändert werden, ebenso wie dfm und dpr!)

    Kleine Erklärung: Angenommen, ich füge ein Element hinzu, rufe also Add auf. Was passiert, zusammengeschrieben aus Add und Notify:

    Delphi-Code

    1. Function TBasicInterfaceList<IInt>.Add(Const Item: IInt): Integer;
    2. Begin
    3. Result := FList.Add(Pointer(IInterface(Item)));
    4. IInterface(Item)._AddRef;
    5. End;

    Diese harten Typecasts sind alle kostenlos - sie generieren keinen Assembler-Befehl und dienen nur, um den Compiler zu befriedigen. Ein Interface ist grundsätzlich intern ein Pointer, daher ist es „physikalisch“ möglich, jedes Interface in einen Pointer zu casten. Allerdings mag das Delphi nicht für IInt. Es gibt aber kein Problem damit, ein IInterface in einen Pointer zu casten, und natürlich kann ich ein IInt in ein IInterface casten. So komme ich zu meinem Pointer.
    Das ist notgedrungen etwas umständlich geschrieben, folgender Code macht im Prinzip das gleiche und erspart diese Castkaskade:2

    Delphi-Code

    1. Function TBasicInterfaceList<IInt>.Add(Const Item: IInt): Integer;
    2. Var
    3. ItemPointer: Pointer Absolute Item;
    4. Begin
    5. Result := FList.Add(ItemPointer);
    6. IInterface(ItemPointer)._AddRef;
    7. End;

    Das ist vielleicht leichter zu verstehen. Trotzdem habe ich hier aber immer noch einen Cast drin: nämlich in der letzten Zeile, wo ich _AddRef aufrufe (was ich aus Notify hier in die Add-Funktion kopiert habe).
    Wieso rufe ich nicht einfach Item._AddRef auf? Das könnte ich im Prinzip. Aber es wäre viel ineffizienter, denn Delphi weiß, dass _AddRef nicht zu IInt selbst gehört (außer IInt ist identisch mit IInterface; ich habe nicht überprüft, ob das dann besser optimiert wird), sondern dass diese Methode nur vererbt wird. Also castet Delphi intern über den Aufruf von _IntfCast das Item in ein IInterface; im Endeffekt steht da also:

    Delphi-Code

    1. Item._AddRef
    2. // ist äquivalent zu
    3. (Item As IInterface)._AddRef

    Im Allgemeinen ist das auch sinnvoll („gute Compiler-Magic“), weil es bei Interfaces ja Mehrfachvererbung gibt, d.h. es ist normalerweise nicht sofort klar, wo denn nur diese Prozedur nun implementiert ist. Aber _AddRef ist ein Spezialfall, wie auch QueryInterface und _Release. Denn diese Methoden gehören zu IInterface, also zu dem Ding, von dem jedes Interface erbt. Und deswegen weiß ich3, dass es in der VMT ganz am Anfang steht. Um Hagen zu zitieren:

    Delphi-Code

    1. type
    2. PIntfVTable = ^TIntfVTable;
    3. TIntfVTable = packed record
    4. QueryInterface: Pointer;
    5. _AddRef: Pointer;
    6. _Release: Pointer;
    7. end;
    8. // so sieht dann ein alloziertes minimal Interface aus
    9. // es enthält ähnlich wie ein TObject als erstes Feld einen Zeiger auf die VMT
    10. TIntf = packed record
    11. VTable: PIntfVTable;
    12. // Field1: Integer; // hier würden die Datenfelder eines allozierten Interfaces gespeichert
    13. end;

    Das heißt, ich kann mir den komplizieren dynamischen Cast sparen („böse Compiler-Magic“) und einen einfachen statischen Cast machen.
    Nach dem Prinzip ist mein gesamter Code aufgebaut. Ich muss mir nur noch an den entsprechenden Stellen Gedanken machen, wann ich manuell die Referenzverwaltung auslöse, weil ich ja das Interface nicht „direkt“, sondern als Pointer speichere, so dass Delphi nichts (oder nur wenig, und das unnütz) selbst macht. Hierfür verwende ich den Notify-Mechanismus von TList.
    Beispiel:

    Delphi-Code

    1. Procedure Test;
    2. Var
    3. klasse1: IIntfc;
    4. list: TIntfcList;
    5. x: IIntfc;
    6. Begin
    7. list := TIntfcList.Create;
    8. klasse1 := TKlasse1.Create;
    9. list.Add(klasse1);
    10. list.Add(klasse1);
    11. For x In list Do Begin
    12. x.Execute;
    13. End;
    14. x := NIL; // Nach einem For...in-Loop (der natürlich auch nicht das Optimum an Effizienz darstellt)
    15. // hält x noch eine Referenz. Die würde am Ende der Prozedur automatisch freigegeben werden, wenn
    16. // es nicht hier schon gemacht wird.
    17. list.Items[0].Execute;
    18. list.SortList(
    19. Function(A, B: IIntfc): Integer
    20. Begin
    21. Result := 0;
    22. End);
    23. klasse1 := list.Extract(klasse1);
    24. WriteLn(list.Remove(klasse1));
    25. list.Free;
    26. End;

    Das sollte wesentlich effizienter sein als jedes Hantieren mit QueryInterface und RTTI. Funktioniert ohne Exceptions und ohne Speicherlecks.

    1 Du kannst ein Interface prinzipiell als Record emulieren. So etwas wird gern gemacht, wenn man eine leichtgewichtige Referenzverwaltung will. Quasi Garbage-Collection ohne das ganze Zusatzgedöns, was durch TInterfacedObject noch dabei ist durch die ganze Polymorphie vom. negaH hat in der DP-Library da ein nettes Beispiel gegeben [auch wenn das dort ziemlicher Overkill ist]. Das wird ganz gerne etwa für numerische Bibliotheken gemacht; ich verwende das etwa in meiner sich in Entwicklung befindenden Delphi-Schnittstelle für GMP/MPFR. Denn so hat man das beste aus allen Welten: Automatisches Speichermanagement, überladene Operatoren und minimaler, präzise kontrollierter Speicherbedarf.
    2 Das wollte ich zuerst immer so verwenden, weil ich Absolute lieber mag als ein Haufen Rumgeschubse mit sich wiederholenden Casts. Aber das funktioniert in XE3 nicht immer, weil Absolute dort immer mal wieder zu internen Compilerfehlern führt. Unter Berlin ist das behoben und würde durchlaufen. (Andere Versionen habe ich hier nicht zum Testen.) Andererseits ist Berlin aber tatsächlich dazu in der Lage, die ganzen Funktionen zu inlinen, so wie ich es eigentlich vorgegeben habe (XE3 kann das nicht). Wenn die Funktion geinlined ist, dann wird der zweifache Cast von oben aber in effizienteren Maschinencode umgesetzt als die Absolute-Variante; für letztere kopiert Delphi zunächst den Interface-Pointer in den Stack, um ihn dann wieder von dort zu laden. Der zweifachen Cast kann direkt wegoptimiert werden zu einem einzigen MOV.
    3 Delphi könnte das eigentlich auch wissen; und normalerweise tut es das auch. Wenn du eine Variable vom Typ deines Interfaces hast, dann wird das der Aufruf von _AddRef direkt ohne dynamischen Typecast gehen. Das Problem sind hier die Generics: Aus irgendeinem Grund ist Delphi nicht in der Lage, das zu erkennen, wenn du den generischen Typ IInt verwendest. Übrigens: Was ich geschrieben habe von wegen „ist äquivalent zu“ ist eigentlich falsch, weil (Item As IInterface) nicht compiliert, wenn Item von einem generischen Typ ist. Das funktioniert nur, wenn es ein fest vorgegebener Typ ist.
    Dateien
    Master of the EDH ;)