Autor Beitrag
Martok
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 3661
Erhaltene Danke: 604

Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
BeitragVerfasst: Mo 03.02.14 20:06 
Interfaces in Delphi
Von Innen und von Außen



Abstract
Interfaces sind in Delphi seit Version 4 verfügbar. Damals eingeführt, um das (damals neue) Component Object Model (COM) in Delphi unterstützen zu können, sind Interfaces in Delphi nach wie vor sehr darauf zugeschnitten. Deswegen verhalten sie sich anders, als das in anderen Sprachen (z.B. C# oder PHP) der Fall ist. Das führt gerne zu Verwirrung, die dieser Artikel etwas auflösen soll. Dabei richtet er sich sowohl an Anfänger als auch an fortgeschrittene Interface-Nutzer. Die Grundlagen der objektorientierten Programmierung in Delphi (Klassen, Vererbung, Methoden, Eigenschaften vs. Felder) sollten aber bekannt sein. Im zweiten Teil (so er denn irgendwann mal erscheint...) werden einige praktische Verwendungszenarien beschrieben.
Explizit nicht Thema ist die Erstellung und Verwaltung von COM-Server-Projekten, das würde einfach den Rahmen sprengen und ist bei Doberstein/Rauter auch viel besser erklärt, als ich das könnte.

Überblick
  1. Grundlagen
  2. Implementation
  3. Anwendungen


Quellen
  1. Andreas Doberstein, Georg Rauter: Softwareentwicklung mit Delphi 4, Addison-Wesley, 1999; S.659ff
  2. Elmar Warken: Delphi 4, Addison-Wesley-Longman, 1999; S 260ff
  3. www.delphigroups.info/2/2/989989.htm
  4. Delphi 7-Hilfe: “Schnittstellen delegieren”
  5. Delphi 7 RTL Source Code: System.pas, Kommentare zu TInterfacedObject, TAggregateObject, TContainedObject



Anmerkungen & Ergänzungen?
Immer losposten ;)
Ich bin mir bei den meisten Sachen relativ sicher, aber ein so länglicher so technischer Artikel muss ja quasi Fehler haben...

_________________
"The phoenix's price isn't inevitable. It's not part of some deep balance built into the universe. It's just the parts of the game where you haven't figured out yet how to cheat."


Zuletzt bearbeitet von Martok am Mo 03.02.14 20:18, insgesamt 4-mal bearbeitet

Für diesen Beitrag haben gedankt: Narses, Nersgatt, Xion
Martok Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 3661
Erhaltene Danke: 604

Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
BeitragVerfasst: Mo 03.02.14 20:06 
Grundlagen

Deklaration
Interfaces sind in Delphi den Klassen sehr ähnlich, wenn man sie mit vollständig abstrakten Klassen vergleicht, wird der Unterschied sogar noch kleiner. Nehmen wir diese Beispielklasse:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
TWorker = class
public
 procedure DoWork; virtualabstract;
end;

Von dieser könnte man jetzt weitere Klassen ableiten, die verschiedene Arbeiten ausführen. Je nachdem, was man braucht, kann man dann die passende Klasse instantiieren:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
var
  Inst: TWorker;
begin
 case Task of 
   aCleanup: Inst:= TCleanupWorker.Create;
   aRunTest: Inst:= TTestWorker.Create;
 end;
 Inst.DoWork;
//...

Das funktioniert solange, wie wir nur genau eine Basisklasse brauchen. Was aber, wenn manche TWorker eine Methode zum Vorbereiten brauchen, und manche nicht? Die übliche Variante in reiner OOP wäre wohl, leere Methodenrümpfe zu verwenden. Das funktioniert, wenn es nur um wenige Methoden geht, aber irgendwann wird es unübersichtlich.

Für diesen Fall kann man nun Interfaces verwenden. Diese sind zunächst erstmal so etwas ähnliches wie Klassen (und werden auch so ähnlich deklariert), sind aber lediglich Beschreibungen der nach außen sichtbaren Schnittstelle - ohne Implementation und nur mit Methoden (also Prozeduren und Funktionen).
ausblenden Delphi-Quelltext
1:
2:
3:
IWorker = interface
 procedure DoWork;
end;

(Hinweis: per Konvention fangen Interface-Namen mit einem “I” an, so wie Typnamen mit einem “T”)
Im Code ändert sich dabei nicht viel, nur die Deklaration der Variablen. Wie genau die Klassen aussehen müssen, werden wir uns im Abschnitt Implementation ansehen.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
var
  Inst: IWorker;
begin
 case Task of 
   aCleanup: Inst:= TCleanupWorker.Create;
   aRunTest: Inst:= TTestWorker.Create;
 end;
 Inst.DoWork;
//...


Wie man sieht, kann das alles ganz einfach sein. Mit dieser Einschätzung fangen dann aber die Probleme an, denn es gibt ein paar Dinge, die man so nicht sieht.


Hintergrund: COM
Bevor wir uns aber dem Unbekannten (pun intended) zuwenden, ist es zweckmäßig, sich Gedanken darüber zu machen, woher das eigentlich alles kommt. So wie CORBA mal der Grund war, Delphi in einer Client/Server-Edition rauszubringen, ist Microsofts Component Object Model der Grund gewesen, Interfaces in die Sprache zu integrieren. COM ist die Antwort auf die Frage, wie man Funktionalität so bereitstellen kann, dass man sie in allen Sprachen nutzen kann. Das Ganze kann man als sehr großes Plugin-System mit Factory-Funktionen auffassen, bei dem man sich eine Klasse bestellen kann und dann eine Instanz davon bekommt. Nun sieht aber ein C++-Objekt im Speicher ganz anders aus als ein Delphi-Objekt (und wie man bei Packages aus verschiedenen Delphi-Versionen sieht, ist ja selbst das nicht immer gleich), und was tun erst Skriptsprachen (damals sicherlich am wichtigsten VB, heute z.B. die Powershell)?
Deswegen hat man Interfaces erfunden, als einen Weg, wie man unterschiedliche Speicherlayouts, Aufrufkonventionen und (in COM, das hat nichts mit Interfaces zu tun) Datentypen vereinheitlichen kann.
Das ganze funktioniert dann so, dass man beim COM per CreateComObject() ein Objekt bestellt, dieses dann in der Registry gesucht und nachdem der Server gestartet oder geladen wurde eine Instanz zurückgegeben wird. Und diese ist dann eben ein IIrgendwas, wie wir weiter oben auch deklariert haben.


IUnknown, IDispatch
Jede Klasse, welche ohne Angabe einer Basisklasse deklariert wird, erbt bekanntermaßen automatisch von TObject. Einen ähnlichen Automatismus hat Delphi wegen COM geerbt: jedes Interface erbt automatisch von IUnknown, dem Basis-COM-Objekt. Das ist auch der große Unterschied zu anderen Programmiersprachen wie Java oder PHP, denn durch dieses sind Interfaces in Delphi nicht nur reine Contracts über eine Schnittstelle, sondern immer etwas mehr. Dieses Interface definiert die drei Methoden QueryInterface, _AddRef und _Release, denen wir uns weiter unten noch zuwenden werden.

Damit auch Sprachen, die keine Methodenzeiger kennen (zum Beispiel Skriptsprachen oder Java), ein COM-Interface verwenden können, gibt es ein weiteres Basisinterface: IDispatch. Dieses bietet Möglichkeiten, um Methoden per Namen aufzurufen.
Auch, wenn man in Delphi OLE-Objekte verwendet, wird IDispatch verwendet:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
var
  AVar: OleVariant;
AVar := CreateComObject(CLASS_TMyAutoObj) as IDispatch;
AVar.DoThis;


Das bedeutet aber, dass jeder Aufruf zuerst den Namen der Methode in eine ID (die dispid) aufgelöst werden muss, um sie dann darüber aufzurufen. Da das etwas langsam sein kann, besteht in Delphi die Möglichkeit diesen numerischen Wert bei der Deklaration schon mit anzugeben, damit der Auflösungs-Schritt gespart werden kann. Damit das funktioniert, verwendet man statt interface das Schlüsselwort dispinterface. Das führt auch dazu, dass nicht mehr IUnknown sondern IDispatch als Basisklasse verwendet wird.
Die Sache hat aber einen Haken: ein IDispatch ist nie ein normales Interface - immer nur ein getarnter OleVariant. Man spart damit nur den Namens-Lookup.


GUIDs
Weiter oben schrieb ich, dass man "ein Objekt bestellt". Was aber ist die "Bestellnummer"? Der Name des Interfaces, also z.B. "IWorker" ist dabei nicht wirklich hilfreich, denn wie man an dem Beispiel sieht, ist dieser nicht wirklich eindeutig und man kann davon ausgehen, dass nicht zweifelsfrei geklärt werden kann welches IWorker denn nun gemeint ist. Daher werden Interfaces über eine sogenannte GUID identifiziert. GUID steht für Globally Unique IDentifier, also eine ID, die garantiert global eindeutig ist. Üblicherweise notiert man diese in hexadezimaler Darstellung:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
IMediaViewer = interface
  ['{400F017E-0D21-4DDF-B629-5841391C7465}']
  function SupportsFiletype(Filename: String):boolean;
end;

Durch diese ID ist das Interface eindeutig gekennzeichnet und wenn nun diese GUID verwendet wird (wie oben über die Konstante CLASS_TMyAutoObj) wissen alle beteiligten, dass exakt dieses Interface gemeint ist. In Delphi kann auch der Name des Interfaces verwendet werden, sofern es mit GUID deklariert ist. Bei der Version von IWorker weiter oben wäre das also nicht möglich, bei IMediaViewer schon.
Um eine solche GUID zu generieren existiert in Delphi der Shortcut Strg+Shift+G (und nur der Shortcut, dafür existiert kein Menü).


Begriffe
Nochmal zusammengefasst:
  • Interface
    Eine Sicht auf die Schnittstelle eines Objekts. Das Objekt kann andere Methoden haben, aber diese sind garantiert.
  • Interface-Vererbung
    Interfaces können wie Komponenten voneinander abgeleitet werden. Das neue Interface hat dann alle Methoden des Vorfahren und zusätzlich neue. Das neue Interface braucht dann eine neue GUID.
  • Co-Klasse
    Die (Delphi-)Klasse, die ein Interface deklariert. Dabei kann eine Klasse durchaus mehrere Interfaces implementieren, also meherere Schnittstellen zur Verfügung stellen. Ein TStream könnte zum Beispiel die Interfaces IReadable und IWriteable implementieren. Interface-Variablen können hin- und her gecastet werden, sofern ein Objekt mehr als ein Interface implementiert (und das tut es immer, nämlich mindestens IUnknown zusätzlich zum angegebenen).
  • Interface-Variable
    Variable vom Typ eines Interfaces, die ein Interface repräsentiert. Co-Klassen können an Interfacevariablen zugewiesen werden, das Objekt bekommt man allerdings nicht daraus zurück. Einmal Interface, immer Interface ;-)

_________________
"The phoenix's price isn't inevitable. It's not part of some deep balance built into the universe. It's just the parts of the game where you haven't figured out yet how to cheat."


Zuletzt bearbeitet von Martok am Mo 03.02.14 20:12, insgesamt 1-mal bearbeitet

Für diesen Beitrag haben gedankt: Nersgatt
Martok Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 3661
Erhaltene Danke: 604

Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
BeitragVerfasst: Mo 03.02.14 20:07 
Implementation

Interfaces implementieren
Um eine Co-Klasse zu erstellen, müssen wir Delphi sagen, dass ein bestimmtes Interface implementiert werden soll. Dazu geben wir als Quasi-Basisklasse das Interface an. Es ist auch möglich, mehrere Interfaces kommagetrennt anzugeben.
ausblenden Delphi-Quelltext
1:
2:
TTestWorker = class(IWorker)
end;

Nun weiß Delphi, welche Funktionen implementiert werden müssen, um dieses Interface zu erfüllen und kann fehlende Methoden beim compilieren erkennen. Dass DoWork fehlt ist ja noch klar, aber wie oben erwähnt sind da noch die 3 Methoden von IUnknown. Da jedes Interface in Delphi von IUnknown erbt und diese daher implementieren muss, liefert Delphi die Klasse TInterfacedObject mit, die genau diese 3 implementiert und uns damit Arbeit abnimmt:
ausblenden Delphi-Quelltext
1:
2:
3:
TTestWorker = class(TInterfacedObject, IWorker)
  procedure DoWork;
end;



TInterfacedObject
TInterfacedObject nimmt hier zwar einige Arbeit ab, aber das funktioniert eben nur, wenn die zu implementierende Klasse davon erben kann. Soll z.B. TStringList die Basisklasse sein, kann ja nicht noch eine Basisklasse existieren. Daher müssen die drei COM-Methoden direkt implementiert werden. Sehen wir uns also an, was diese tun und warum.


Referenzzählung
Einer der Kernpunkte von COM ist es, dass Objekte zwischen verschiedenen Sprachen ausgetauscht werden können. Nun hat aber jede Sprache eine andere Vorstellung von Speicherverwaltung. Selbst wenn das geklärt ist, stellt sich aber immer noch die Frage, wer dafür verantwortlich ist, eine Instanz wieder freizugeben. COM fordert daher Referenzzählung von jeder Sprache, die COM-Interfaces verwendet. Das funktioniert ähnlich wie Delphi-Strings: mit jeder Zuweisung an eine Variable wird ein Zähler erhöht, und wenn diese Variable nicht mehr verwendet wird (also z.B. am Ende des Blocks) erniedrigt. Ist dieser Zähler null, hat niemand mehr eine Referenz auf die Instanz und sie kann freigegeben werden.
In TInterfacedObject sieht das so aus:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
function TInterfacedObject._AddRef: Integer;
begin
  Result := InterlockedIncrement(FRefCount);
end;
function TInterfacedObject._Release: Integer;
begin
  Result := InterlockedDecrement(FRefCount);
  if Result = 0 then
    Destroy;
end;

InterlockedIncrement/Decrement sind WinAPI-Funktionen, die Threadsicher eine Variable ändern und auslesen können, sodass in Result immer der neue Wert steht.
In bestimmten Fällen kann es sinnvoll sein, keine Referenzzählung zu verwenden. In diesem Fall sollten beide Funktionen -1 zurückgeben und sonst nichts weiter tun.


Generierter Code
Grundsätzlich werden Interfaces nur dann besonders behandelt, wenn Interfacevariablen verwendet werden. Wird ein Objekt, welches Interfaces implementiert, als normales Objekt verwendet, ändert sich nichts. Interfacevariablen hingegen haben ein anderes Verhalten bei Zuweisungen und beim Verlassen des Gültigkeitsbereichs.
Zuweisungen an eine Interfacevariable erfolgen intern immer mit der Prozedur IntfCopy. Diese ist (wie alle Funktionen der “Compiler Magic”) in System.pas deklariert und übernimmt die Aufgabe, die Referenzzählung bei Zuweisungen auszulösen und tut dabei drei Dinge:

  1. Sollte die Variable schon ein Interface enthalten, wird dieses dereferenziert
  2. Das neue Interface wird referenziert
  3. Der Wert der Variable wird auf das neue Interface geändert.

Der zweite Automatismus greift, wenn eine Variable den Sichtbarkeitsbereich (Scope) verlässt. Das passiert zum Beispiel, wenn eine Funktion verlassen wird - dann geraten alle dort deklarierten Variablen “Out of Scope”. Je nach Optimierung kann das auch durchaus passieren, nachdem eine Variable das letzte Mal benutzt wurde. Zu diesem Zeitpunkt wird ein in einer Interfacevariable gespeichertes Interface automatisch dereferenziert und dadurch ggf. freigegeben.. Das passiert auch, wenn die Methode durch eine Exception verlassen wird. Man könnte also sagen, dass die Aufräumarbeiten in einer Art implizitem try-finally-block, welcher die gesamte Methode umfasst, stattfinden.


Weak References
Wie bei weiter unten anhand TInterfaceList beschrieben, findet die Referenzzählung nur statt, wenn Interfacevariablen verwendet werden - nicht bei einfachen Pointern. Diese Eigenschaft kann man verwenden, um sogenannte “weak references” zu erzeugen, also “schwache Referenzen”. Dabei wird nicht eine Interfacevariable gespeichert, sondern eben ein Pointer. Das ist am Nützlichsten, wenn man einen Zirkelbezug braucht. Ein Beispiel:
In einer Baumstruktur kennt jeder Knoten seine untergeordneten Knoten, z.B. über eine TInterfaceList. Gleichzeitig soll er auch seinen direkt übergeordneten Knoten kennen. Würde man diese Referenzen direkt als Interfacevariablen speichern, würde der enstehende Zirkelbezug dazu führen, dass jeder Knoten immer mindestens einmal referenziert wäre - und daher nie freigegeben würde, auch wenn nirgendwo sonst im Programm mehr Referenzen existieren. Um das zu umgehen, könnte man den Elternknoten als Weak Reference ablegen und bei Verwendung in das Interface casten.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
TNode = class(..., INode)
//...
FParent: Pointer;
end;
//...
procedure TNode.TellParent(Messagestring);
begin
 INode(FParent).Tell(Message);
end;

Dabei wird eine temporäre Referenz auf das Interface angelegt, die direkt nach der Verwendung wieder “entsorgt” wird.
An diesem Beispiel wird auch eines deutlich: es gibt anders als sonst keine Garantie dafür, dass das Interface zum Zeitpunkt der Verwendung noch existiert. Man sollte also aufpassen, welche Art von Beziehungen man baut, so wie bei normalen Objekten üblich.


QueryInterface, as, Supports
Wie bereits oben erwähnt ist ein Interface nur eine Sicht auf ein Objekt. Nun kann eine Klasse aber mehrere Interfaces (oder z.B. mehrere Versionen, die jeweils neue Funktionen einführen) implementieren. Eine Interfacevariable stellt nun aber nur genau dieses da, woher bekommt man also andere Ansichten?
COM definiert dafür auf IUnknown die Funktion QueryInterface, welche ein Interface in ein anderes umwandeln kann. Sehen wir uns die Implementation in TInterfacedObject an:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
function TInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

E_NOINTERFACE wird zurückgegeben, wenn ein Interface nicht verfügbar ist. GetInterface ist eine Standardfunktion von TObject, die in der Interfacetabelle des Objekts nach der GUID sucht und ggf. das neue Interface in Obj schreibt.
Nehmen wir also ein hypothetisches Reader-Interface, dass einige Klassen aus java.io.* nachbildet:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
var reader: IReader;
   linereader: IBufferedReader;
//...
 if reader.QueryInterface(IBufferedReader, linereader)=0 then
   foo:= linereader.readln();

Nun ist aber der Vergleich auf 0 etwas unhandlich. Deswegen existiert die Funktion Supports, die das verpackt:
ausblenden Delphi-Quelltext
1:
if Supports(reader, IBufferedReader, linereader) then //...					

Außerdem ist der Operator as für Interfaces überladen, um auch QueryInterface aufzurufen. Wenn die Konvertierung nicht möglich ist, wir eine Exception EIntfCastError ausgelöst.
ausblenden Delphi-Quelltext
1:
foo:= (reader as IBufferedReader).readln();					



Properties und Sichtbarkeit
Interfaces definieren immer nur die nach außen sichtbaren Methoden. Das hat ein paar Auswirkungen darauf, was deklariert werden kann: nur Methoden, und nur sichtbare. Genauer: es gibt keine Sichtbarkeitsklassen, alles ist immer public. Außerdem sind Eigenschaften eingeschränkt: nur solche mit Getter-Funktionen und Setter-Prozeduren sind möglich (und diese müssen dann natürlich Public sein). Das schränkt ihren Nutzen durchaus ein, es ist meistens einfacher direkt Get/Set-Methoden zu verwenden wie in Sprachen, die keine Properties kennen.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
ITestIntf = interface
  ['{08CAF932-CAFD-487C-A210-24355A2A84A8}']
  function GetTest: integer;
  procedure SetTest(Value: integer);
  property Test: integer read GetTest write SetTest;
end;



Methodenzuordnung
Betrachten wir folgendes (zugegeben konstruiertes) Beispiel:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
IAddInt = interface
  function Add(A, B: integer): integer;
end;
IAddFloat = interface
  function Add(A, B: double): double;
end;
TSomeClass = class(TInterfacedObject, IAddInt, IAddFloat)
public
 // ???
end;

Um nun TSomeClass zu füllen, müsste die Methode Add beider Interfaces implementiert werden. Da offensichtlich nicht der gleiche Name für mehrere Methoden verwendet werden kann, besteht die Möglichkeit die Zuordnung der Methoden der Co-Klasse zu Methoden des Interfaces direkt anzugeben. Das sieht dann so aus:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
TSomeClass = class(TInterfacedObject, IAddInt, IAddFloat)
public
 function AddInt(A, B: integer): integer;
  function AddFloat(A, B: Double): Double;
  function IAddInt.Add = AddInt;
  function IAddFloat.Add = AddFloat;
end;

Es wird also jeweils nach dem Schema Interface.Name = NameInKlasse angegeben, welche Methode welche Interface-Methode implementiert. Wie immer müssen natürlich die Signaturen übereinstimmen.


Delegates
Es ist nicht zwangsweise notwendig, alle Interfaces in der Co-Klasse zu implementieren. Die Implementation einiger (oder sogar aller) Interfaces kann auch an eine Interface- oder Klassen-Eigenschaft der Co-Klasse delegiert werden. Diese Eigenschaft muss einen read-Bezeichner haben und darf keine indizierte Eigenschaft sein. Wird als Getter eine Funktion verwendet, muss diese die Aufrufkonvention register (also die Standard-Aufrufkonvention von Dephi) haben.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
  IDialogHandler = interface ['{590C606C-991B-46F3-900C-208C2305F726}']
        procedure SetType(SaveDialog: boolean);
        function Execute: boolean;
        function GetFilename: string;
  end;
  TDialogHandler = class(TInterfacedObject, IDialogHandler)
  private
        FDialog: TOpenDialog;
  public
        constructor Create;
        destructor Destroy; override;
        procedure SetType(SaveDialog: Boolean);
        function GetFilename: String;
        property Dialog: TOpenDialog read FDialog implements IDialogHandler;
  end;



Was tut dieser Code? IDialogHandler ist ein Interface, dass einen umschaltbaren Open- oder SaveDialog kapselt. Die Co-Klasse TDialogHandler implementiert nun nur die Methoden selbst, die notwendig sind: SetType, um den Dialog zu erzeugen und GetFilename, um auf die Filename-Eigenschaft zuzugreifen. TOpenDialog (und damit auch der abgeleitete TSaveDialog) kennen bereits Execute mit der passenden Signatur, daher überlassen wir dem Dialog die Implementation dieser Methode. Genau genommen sogar mehr, denn nach einer Implementation wird der Reihe nach gesucht: in der delegate-Klasse, deren Vorfahren, dann in der Co-Klasse und deren Vorfahren. Das bedeutet, würde TOpenDialog eine public-Methode namens GetFilename haben, würde diese verwendet. Um Mehrdeutigkeiten zu vermeiden, kann mit der manuellen Methodenzuordnung gearbeitet werden.
Interfaces können auch an Interface-Eigenschaften delegiert werden, dann ist es jedoch zwingend notwendig, alle Methoden dort zu implementieren - eine Mischung wie hier im Beispiel ist nicht möglich.


TAggregateObject
Da jedes als Delegate verwendbare Objekt mindestens ein Interface implementiert, implementiert es auch IUnknown - inklusive der Referenzzählung. Das kann zu Problemen führen, wenn das innere Objekt eine andere Lebensdauer hat als das äußere. Warum funktioniert dann das Beispiel oben?
Der Grund ist relativ einfach: TOpenDialog erbt von TComponent und TComponent implementiert IUnknown, um ActiveX-Komponenten zu ermöglichen. Wenn die Komponente aber kein ActiveX-Objekt ist, geht die Implementation einen anderen Weg und ignoriert Referenzzählung. Man könnte also sagen, das funktioniert nur zufällig.
Um diesem Problem abzuhelfen, gibt es TAggregatedObject und TContainedObject. Beide halten eine Referenz auf das übergeordnete Objekt (den Controller) und verwenden dessen Referenzzählung. Der Unterschied liegt darin, worauf sich QueryInterface bezieht. TAggregatedObject spiegelt auch das wieder an den Controller, TContainedObject tut das nicht. Das bedeutet, dass man von einem TAggregatedObject die Interfaces des Controllers bekommen kann (es also nicht selbstständig funktioniert - nur die Implementation eines Interfaces wird zentralisiert), während ein TContainedObject selbst andere Interfaces implementieren kann (aber nicht muss: ein Anwendungsfall sind Oneway-Konvertierungen: es kann vom übergeordneten Interface in das konvertiert werden, was delegiert wurde, aber es gibt keinen Weg zurück, da das TContainedObject die Interfaces des Containers nicht mehr kennt.


Neue Versionen
Da Interfaces zur Weitergabe von Objekten zwischen verschiedenen Modulen gedacht sind, sollten Interfaces nicht mehr nachträglich verändert werden: ist die gleiche GUID mit verschiedenen Methoden definiert, kann das sehr schiefgehen. Möchte man ein Interface erweitern, sollte man ein neues Interface mit neuer GUID definieren.
Windows verwendet so etwas bei den IContextMenu-Interfaces (hier sind die GUIDs durch Konstanten an anderer Stelle definiert), um in die MessageLoop des Menüs einsteigen zu können - eine Funktionalität, die das ursprüngliche IContextMenu nicht geboten hat.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
IContextMenu = interface(IUnknown)
  [SID_IContextMenu]
  function QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult; stdcall;
  function InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall;
  function GetCommandString(idCmd, uType: UINT; pwReserved: PUINT; pszName: LPSTR; cchMax: UINT): HResult; stdcall;
end;

IContextMenu2 = interface(IContextMenu)
  [SID_IContextMenu2]
  function HandleMenuMsg(uMsg: UINT; WParam, LParam: Integer): HResult; stdcall;
end;

IContextMenu3 = interface(IContextMenu2)
  [SID_IContextMenu3]
  function HandleMenuMsg2(uMsg: UINT; wParam, lParam: Integer; var lpResult: Integer): HResult; stdcall;
end;



TInterfaceList
Das Problem der Referenzzählung stellt sich auch, wenn man TList verwenden würde, um Interfaces zu speichern. TList speichert Pointer, und Pointer sind keine Interfaces, weswegen keine Referenz gezählt wird. Um das zu umgehen, existiert TInterfaceList, die die Referenzierung manuell macht und zwischen Pointer und korrekt referenziertem IUnknown konvertiert. Aus diesem kann dann wieder das passende Interface abgeleitet werden.

_________________
"The phoenix's price isn't inevitable. It's not part of some deep balance built into the universe. It's just the parts of the game where you haven't figured out yet how to cheat."


Zuletzt bearbeitet von Martok am Mo 03.02.14 20:13, insgesamt 1-mal bearbeitet

Für diesen Beitrag haben gedankt: Nersgatt
Martok Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 3661
Erhaltene Danke: 604

Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
BeitragVerfasst: Mo 03.02.14 20:09 
Anwendungen

Nachdem wir uns jetzt mit den Grundlagen auseinandergesetzt haben, wird es Zeit, an ein paar Anwendungsbeispielen zu zeigen, was das bedeutet. Hier gibt es dann auch wieder etwas mehr Quellcode. Dabei sind GUID-Angaben der Einfachheit halber gekürzt.


Objekte
Manchmal ist es notwendig oder einfacher, statt einem Interface das implementierende Objekt zu verwenden. Das sprachübergreifende Wesen von COM sorgt nun aber dafür, dass ein Interface nicht einfach wieder in ein Objekt konvertiert werden kann. Das kann man aber einfach lösen:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
IObject = interface
 [GUID]
 function GetObject: TObject;
end;

TSomeImpl = class(TInterfacedObject, IObject, ISomethingElse)
public
  //...
 function GetObject: TObject;
end;

function TSomeImpl.GetObject: TObject;
begin
 Result:= Self;
end;

//...
var
 intf: ISomethingElse;
 ob: IObject;
//...
 intf:= TSomeImpl.Create;
 if Supports(intf, IObject, ob) then
   ShowMessage(ob.GetObject.ClassName); // ‘TSomeImpl’


Dieser Teil ist noch nicht fertig, aber ich reserviere mir mal den Beitrag als Platzhalter

_________________
"The phoenix's price isn't inevitable. It's not part of some deep balance built into the universe. It's just the parts of the game where you haven't figured out yet how to cheat."
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19272
Erhaltene Danke: 1740

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: Mo 03.02.14 20:47 
In Delphiversionen, die dies haben, (ich glaube ab XE oder XE2) sollten statt den rein Windows API basierten Interlocked-Funktionen besser die in den Compiler integrierten Delphifunktionen wie AtomicIncrement, AtomicDecrement usw. verwendet werden.
Erstens sind diese plattformunabhängig und zweitens werden daraus nur drei Assemblerbefehle statt 14 wie z.B. bei InterlockedIncrement, so dass es auch performancemäßig sinnvoller ist.

user profile iconMartok hat folgendes geschrieben Zum zitierten Posting springen:
Manchmal ist es notwendig oder einfacher, statt einem Interface das implementierende Objekt zu verwenden. Das sprachübergreifende Wesen von COM sorgt nun aber dafür, dass ein Interface nicht einfach wieder in ein Objekt konvertiert werden kann.
Das war früher mal so. Seit Delphi 2010 kann man auch einfach mit as zurück von dem Interface in das Objekt casten. Siehe Dokumentation:
docwiki.embarcadero....eferences_to_Objects

// EDIT:
user profile iconMartok hat folgendes geschrieben Zum zitierten Posting springen:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
  IDialogHandler = interface ['{590C606C-991B-46F3-900C-208C2305F726}']
        procedure SetType(SaveDialog: boolean);
        function Execute: boolean;
        function GetFilename: string;
  end;
Um kompatibel mit anderen Sprachen zu sein und die Interfaces auch z.B. an DLLs übergeben zu können, macht es Sinn nur Datentypen zu benutzen, die dabei auch unterstützt werden. Sprich statt String besser WideString, statt Boolean besser LongBool, ...

Für diesen Beitrag haben gedankt: Martok
Martok Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 3661
Erhaltene Danke: 604

Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
BeitragVerfasst: Mo 03.02.14 23:08 
user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
In Delphiversionen, die dies haben, (ich glaube ab XE oder XE2) sollten statt den rein Windows API basierten Interlocked-Funktionen besser die in den Compiler integrierten Delphifunktionen wie AtomicIncrement, AtomicDecrement usw. verwendet werden.
Hm, keine gute Wahl von Embadingsda. Diese Funktionen heißen in allen Compilern und allen RTLs Interlocked*, auch dann wenn sie z.B. über compilier-intrinsics (siehe VS) implementiert sind. Man ist also mal wieder explizit den falschest möglichen Weg gegangen :wall:

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Das war früher mal so. Seit Delphi 2010 kann man auch einfach mit as zurück von dem Interface in das Objekt casten.
Supports kann das in FPC auch, stimmt. IObject oder so ähnlich, ich guck mal nach und ergänze das noch. Aber das ist eh der Anwedungsteil, der ist nicht so ganz toll ;)

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Um kompatibel mit anderen Sprachen zu sein und die Interfaces auch z.B. an DLLs übergeben zu können, macht es Sinn nur Datentypen zu benutzen, die dabei auch unterstützt werden. Sprich statt String besser WideString, statt Boolean besser LongBool, ...
Jupp. Das hatte ich mal unter dem Teil COM verbucht, dafür ist dieses Tutorial nicht gedacht ;-)

_________________
"The phoenix's price isn't inevitable. It's not part of some deep balance built into the universe. It's just the parts of the game where you haven't figured out yet how to cheat."
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19272
Erhaltene Danke: 1740

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: Di 04.02.14 08:39 
Naja, hätte man die Interlocked-Funktionen genauso genannt, hätte man immer den Namespace davorscbreiben müssen, damit man die richtige Funktion benutzt. Zudem wäre die Verwechslungsgefahr hoch gewesen. Insofern finde ich, dass sie hier den richtigen Weg gegangen sind.

Und die sicheren Datentypen sollte man besser auch bei Interfaces benutzen, die eigentlich rein im Programm verwendet werden. Sonst muss man ggf. irgendwann ein neues Interface erfinden um das an neue Programmmodule in DLLs etc. zu übergeben und hätte dann ggf. zwei Interfaces mit gleichem Funktionsumfang redundant.

Deshalb sollte man das besser gleich einplanen.
Martok Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 3661
Erhaltene Danke: 604

Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
BeitragVerfasst: Di 04.02.14 17:58 
user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Naja, hätte man die Interlocked-Funktionen genauso genannt, hätte man immer den Namespace davorscbreiben müssen, damit man die richtige Funktion benutzt. Zudem wäre die Verwechslungsgefahr hoch gewesen. Insofern finde ich, dass sie hier den richtigen Weg gegangen sind.
Aber das ist doch Käse, wenn ich diese Funktionen verwende erwarte ich, dass ich die schnellste für die Zielplattform verfügbare Version reincompiliert bekomme (wie halt auch in allen andern Compilern). Ist immerhin das dokumentierte Verhalten. Ein IfDef für "Haben wir einen Assembler dafür? Okay, dann den nehmen, sonst API-Funktion" wäre jetzt nicht so schwer gewesen (Hint: FPC tut genau das Gleiche wie VS, rtl/inc/systemh.inc:1212ff). Man merkt schon, dass die das mit dem Multitarget-Code erst noch üben. Wird lustig, falls das mit dem LLVM-Frontend doch mal noch was wird und sie auf einmal identischen Code können, aber die RTL so plattform-fragmentiert haben dass es keinen mehr gibt.
Aber gut, darum geht es hier ja nicht :roll:


user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Und die sicheren Datentypen sollte man besser auch bei Interfaces benutzen, die eigentlich rein im Programm verwendet werden. Sonst muss man ggf. irgendwann ein neues Interface erfinden um das an neue Programmmodule in DLLs etc. zu übergeben und hätte dann ggf. zwei Interfaces mit gleichem Funktionsumfang redundant.
Das ist tatsächlich ein gutes Argument, ist aber meiner Meinung nach auch eine Abwägungsfrage. Der COM-korrekte Weg wäre dann auch über safecall (siehe Beispiel), und damit durch die versteckte Magic signifikant langsamer. Das kann egal sein, muss aber nicht. Ich persönlich würde das nur tun, wenn überhaupt externer Code geplant ist ;)

_________________
"The phoenix's price isn't inevitable. It's not part of some deep balance built into the universe. It's just the parts of the game where you haven't figured out yet how to cheat."
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19272
Erhaltene Danke: 1740

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: Di 04.02.14 23:30 
user profile iconMartok hat folgendes geschrieben Zum zitierten Posting springen:
wenn ich diese Funktionen verwende erwarte ich, dass ich die schnellste für die Zielplattform verfügbare Version reincompiliert bekomme (wie halt auch in allen andern Compilern). Ist immerhin das dokumentierte Verhalten.
Nein, bei Delphi ist das nicht dokumentiertes Verhalten. Da bekommt man normalerweise nicht automatisch plötzlich eine komplett andere Funktion untergeschoben. Es gibt entsprechende Compilerhinweise, dass die verwendeten Funktionen oder Units plattformspezifisch sind, aber mehr nicht. Und das ist auch gut so. Wenn ich eine API-Funktion verwende, möchte ich ganz sicher nicht, dass da plötzlich eine andere aufgerufen wird, nur weil der Compiler der Meinung ist, dass die besser ist... :roll: