Folgendes ist die HTML-Aufbereitung einer Mail, die ich am 25.12.2001
in der Area Delphi.ger des Fidonet
gepostet habe. Ich packe sie hierher in der Hoffnung, dass sie dem ein
oder anderen hilft, der evtl. kein Fidonet hat.
Der eine oder andere hat sie vielleicht schonmal in der Hilfe gesehen,
meist im Zusammenhang mit COM und ActiveX und so'n Zeuch, aber Interfaces
sind auch ohne diesen Kram interessant.
Ok, stelle ma uns mal ganz dumm und fragen: "Watt is'nen Interface?"
Ein Interface ist sowas wie eine abstrakte Klasse, also nur Deklaration,
keine Implementation. Ein solches Interface muss dann von einer Klasse
implementiert werden. Ein Beispiel:
unit i_MyFunction;
interface
type
IMyFunction = interface
function Ergebnis: double;
function GetParameters(_Idx: integer): double;
procedure AddParameter(const _Param: double);
property Parameters[_Idx: integer]: double read GetParameters;
end;
implementation
end;
Sieht nicht besonders aufregend aus, gell? Folgendes sind die Unterschiede
zu einer normalen Klassendeklaration:
-
Das Schluesselwort 'interface' anstelle von 'class'.
-
Alle Methoden sind abstrakt, d.h. es gibt erstmal keine Implementation.
-
Properties muessen immer je eine Get- und eine Set-Methode haben (Ok, falls es readonly-Properties sind, reicht natuerlich eine Get-Methode.).
-
Alle Deklarationen sind implizit 'public', es gibt keine protected und private Deklarationen.
-
Es gibt keine Felder.
-
Es gibt keine Klassen-Methoden.
-
Es gibt keinen Konstruktor und keinen Destruktor.
Wie man sieht, sind das alles sehr subtile Unterschiede, die man auf
den ersten Blick gar nicht bemerkt.
Das Interface alleine nuetzt erstmal gar nichts, es muss durch eine
Klasse mit einer Implementation versehen werden:
unit u_MyFunctions;
interface
uses
i_MyFunction;
type
TSumme = class(TInterfacedObject, IMyFunction)
protected
fParameters: TList;
function Ergebnis: double;
function GetParameters(_Idx: integer): double;
procedure AddParameter(const _Param: double);
public
constructor Create;
destructor Destroy; override;
end;
Diese Klasse implementiert das Interface IMyFunction. Sie baut dazu auf der
Klasse TInterfacedObject (deklariert in System, glaube ich) auf, die ein paar
Grundfunktionen zur Verfuegung stellt, die jede Klasse haben muss, die ein
Interface implementiert, mehr dazu spaeter.
Wichtig ist, dass einen solche Klasse alle im Interface deklarierten Methoden
implementiert, dabei sind sind abstrakte Methoden zulaessig, aber das verfolge
ich hier nicht weiter.
Zusaetzlich gibt es einen Konstruktor und Destruktor sowie ein Feld
fParameters. Evtl. faellt dem ein oder anderen auf, dass die Methoden des
Interface als 'protected' deklariert sind. Ich mache das, um zu
verhindern, dass die Klasse als Klasse benutzt wird, sie soll nur
ueber das Interface benutzt werden, mehr dazu spaeter.
Ok, jetzt die Implementation:
type
PDouble = ^Double;
constructor TSumme.Create;
begin
inherited Create;
fParameters := TList.Create;
end;
destructor TSumme.Destroy;
var
i: integer;
begin
if Assigned(fParameters) then begin
for i:=0 to fParameters.Count-1 do begin
Dispose(PDouble(fParameters[i]));
end;
end;
fParameters.Free;
inherited;
end;
function TSumme.GetParameters(_Idx: integer): double;
begin
Result := PDouble(fParameters[_Idx])^;
end;
procedure TSumme.AddParameter(const _Param: double);
var
p: PDouble;
begin
New(p);
p^ := _Param;
fParameters.Add(p);
end;
function TSumme.Ergebnis: double;
var
i: integer;
begin
Result := 0;
for i := 0 to fParameters.Count - 1 do begin
Result := Result + PDouble(fParameters[i])^;
end;
end;
Wie man sieht, implementiert die Klasse ganz einfach eine Summenbildung
ueber alle ihre Parameter. Nicht wirklich aufregend, aber ein einfach
verstaendliches Beispiel. Man koennte sich jetzt noch andere Klassen
vorstellen, die dasselbe Interface implementieren, die ich aber als
Uebung dem geneigten Leser ueberlasse. Ich werde im folgenden annehmen,
dass es noch die Klassen TProdukt und TKehrsumme (Summe der Kehrwerte
1/Param) gibt.
Nur zur Anwendung:
procedure UseMyFunc(_MyFunc: IMyFunction);
begin
MyFunc.AddParam(5.0);
MyFunc.AddParam(1.0);
MyFunc.AddParam(2.5);
WriteLn(MyFunc.Ergebnis);
end;
var
MyFunc: IMyFunction;
begin
MyFunc := TSumme.Create;
UseMyFunc(MyFunc);
MyFunc := TProdukt.Create;
UseMyFunc(MyFunc);
end;
Wer mit Object Pascal vertraut ist, dem wird oben sofort auffallen, dass der
Aufruf des Destruktors fehlt, also das Programm ein dickes Speicherleck
zu haben scheint. Dem ist nicht so, mehr dazu spaeter. ;-)
Obiges zeigt, dass es moeglich ist, in einer Prozedur jede beliebige Klasse,
die das Interface IMyFunction implementiert zu verwenden, ohne wissen zu
muessen, um welche Klasse es sich handelt. Dies waere natuerlich auch moeglich,
indem man Klassen mit einem gemeinsamen (evtl. abstrakten) Vorfahren verwendet.
Die Implementation als Interface hat aber einen entscheidenden Vorteil: Die
Klassen brauchen eben *nicht* einen gemeinsamen Vorfahren zu haben. Auf diese
Weise bekommt man unter Object Pascal sowas aehnliches wie multiple Vererbung,
wenn auch nur der Deklaration und nicht der Implementation (zur "Vererbung"
der Implementation, das geht naemlich auch, siehe das Stichwort 'Delegation'
in der Online Hilfe).
Und nun zu den bereits mehrfach verschobenen Erklaerungen zu TInterfacedObject:
Jedes Interface stammt von einem speziellen Interface IUnknown ab, genauso, wie
jede Klasse von TObject abstammt. IUnknown deklariert (in System) drei
Funktionen:
type
IUnknown = interface
procedure _AddRef;
procedure _Release;
function QueryInterface(...)...; // hier nicht weiter behandelt
end;
Die beiden ersten Methoden sind notwendig fuer das sogenannte Reference
Counting, das der Compiler automatisch fuer alle Interfaces macht. Es
funktioniert genauso wie man es von den Ansi-Strings kennt: Bei einer Zuweisung
auf einen anderen String wird nicht der komplette String kopiert sondern
lediglich ein Pointer auf den vorhandenen String gesetzt und der Reference
Counter des Strings um eins erhoeht. Das ist wesentlich effizienter als
jedesmal den kompletten String zu kopieren und hat keine Nachteile, solange der
String nicht veraendert wird. Wenn einer der Verweise auf den String dann
ungueltig wird ('out of scope' geht), wird der Reference Counter wieder um eins
vermindert. Wenn der Reference Counter bei 0 ankommt, kann der Speicher des
Strings freigegeben werden, da keine Variable mehr darauf verweist.
All das geschieht im Hintergrund ohne dass der Programmierer sich dessen
bewusst ist.
Bei Interfaces ist es genauso: Jede Zuweisung auf eine Variable vom Type
Interface fuehrt dazu, dass der Compiler die _AddRef Methode aufruft. Wenn eine
solche Variable ungueltig wird oder einen anderen Wert (ein anderes Interface
oder NIL) zugewiesen bekommt, so ruft er die _Release Methode auf. Eine Klasse,
die diese beiden Methoden implementiert, sollte also intern einen Zaehler
haben, der durch _AddRef um eins erhoeht und durch _Release um eins vermindert
wird. Zusaetzlich muss, wenn der Zaehler 0 erreicht, der Destruktor der Klasse
aufgerufen werden.
Natuerlich koennte man das selbst implementieren und es ist evtl. sinnvoll, das
einmal zu tun, um zu sehen, was passiert, aber man kann auch ganz einfach seine
Klassen von TInterfacedObject ableiten, welches diese Implementation bereits
zur Verfuegung stellt.
Zu guter Letzt loest sich damit natuerlich das Raetsel des fehlenden
Destruktor-Aufrufs in meinem Beispielcode auf. Er ist nicht notwendig, da der
Compiler durch das Reference-Counting das selbst uebernimmt. Hier nochmal der
Code, man beachte die Kommentare:
procedure UseMyFunc(_MyFunc: IMyFunction);
begin
MyFunc.AddParam(5.0);
MyFunc.AddParam(1.0);
MyFunc.AddParam(2.5);
WriteLn(MyFunc.Ergebnis;
// (1) Hier endet die Gueltigkeit des Parameters _MyFunc,
// d.h. der Compiler ruft _Release auf.
end;
var
MyFunc: IMyFunction;
begin
// (2) Die naechste Zeile ist eine Zuweisung auf eine
// Interface-Variable, d.h. der Compiler ruft _AddRef auf.
MyFunc := TSumme.Create;
// (3) Die naechste Zeile ist eine implizite Zuweisung auf
// eine Interface-Variable, naemlich auf den Parameter der
// Prozedur, d.h. der Compiler ruft _AddRef auf.
UseMyFunc(MyFunc);
// (4) Die naechste Zeile ist eine Zuweisung auf eine
// Interface-Variable. In diesem Fall hat die Variable
// bereits einen Wert, d.h. der Compiler ruft zunaechst
// _Release fuer den alten Wert auf und dann _AddRef fuer
// den neuen.
MyFunc := TProdukt.Create;
// (5) Wiederholung von (3)
UseMyFunc(MyFunc);
// (6) Hier endet die Gueltigkeit der Variablen MyFunc, d.h.
// der Compiler ruft die _Release Methode auf.
end;
Wenn man dem obigen Programmablauf folgt, so wird man an den jeweils
nummerierten Punkten feststellen, dass der Reference-Counter wie folgt
manipuliert wird:
(2) TSumme-Objekt: 0 -> 1
(3) TSumme-Objekt: 1 -> 2
(1) TSumme-Objekt: 2 -> 1
(4) TSumme-Objekt: 1 -> 0 -> Destruktor-Auruf!
(4) TProdukt-Objekt: 0 -> 1
(5) TProdukt-Objekt: 1 -> 2
(1) TProdukt-Objekt: 2 -> 1
(6) TProdukt-Objekt: 1 -> 0 -> Destruktor-Aufruf!
Es entsteht also kein Speicherleck.
Ein wichtiger Hinweis noch: Man sollte niemals auf ein und dasselbe Objekt als
Objekt und als Interface gleichzeitig zugreifen. Wenn man dabei naemlich nicht
hoellisch aufpasst, wird einem das Objekt durch das Reference Counting des
Interfaces 'unter dem Hintern weg' freigegeben. Insbesondere betrifft
dies Komponenten, denn bei diesen gibt es noch eine weitere
Moeglichkeit, wann sie freigegeben werden, naemlich dann, wenn ihr
"Owner" freigegeben wird.
So, das sollte erstmal reichen. Wenn ich die Zeit finde, werde ich auf dieser
Mail aufbauend mich mal mit TInterfaceList beschaeftigen und einen boesen
Fallstrick bei dessen Verwendung aufzeigen.
(c) Copyright 2001 by Thomas Mueller, alle Rechte vorbehalten
This document was generated using AFT v5.096
|