twm's homepage logo
Von einem, der auszog die Heimat schätzen zu lernen ...
Object Pascal Interfaces
Deutsch English
Google
Search dummzeuch.de
Search WWW

Referenz

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.

Einleitung

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?"

Was ist ein 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:

  1. Das Schluesselwort 'interface' anstelle von 'class'.
  2. Alle Methoden sind abstrakt, d.h. es gibt erstmal keine Implementation.
  3. Properties muessen immer je eine Get- und eine Set-Methode haben (Ok, falls es readonly-Properties sind, reicht natuerlich eine Get-Methode.).
  4. Alle Deklarationen sind implizit 'public', es gibt keine protected und private Deklarationen.
  5. Es gibt keine Felder.
  6. Es gibt keine Klassen-Methoden.
  7. 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.

Implementation

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.

Anwendung

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).

TInterfacedObject

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.

Reference Counting

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.

Vorsicht Falle

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

letzte Änderung: 2012-10-14 twm
Post to del.icio.us Best Viewed With Open EyesValid XHTML 1.0!