Zitat von
.chicken:
Also wie schreibe ich ein Event für meinen Server, wenn ich ihn zur Laufzeit erstelle?
Willst Du hier wirklich das Event schreiben oder die Behandlung des Events?
Ich werde im folgenden einfach mal auf Beides eingehen. Das Ereignis an sich ist nichts, was Du wirklich programmieren musst. Du legst eigentlich nur fest, wann die Ereignisbehandlung ausgelöst wird. Das ist schon alles.
Bleibt das Problem der Ereignisbehandlung. Hier gibt es ganz verschiedene Ansätze. Der unter Delphi typische besteht in einem Callback. Callback kannst Du dabei wirklich als Rückruf übersetzen. Jmd. der sich für ein Ereignis interessiert hinterlässt seine "Nummer" (genau gesagt ist es eine Adresse) und wird "zurückgerufen", wenn das Ereignis eintritt.
An sich ist dies natürlich (mehr oder weniger) immer der Weg, den man verwendet. Der (hauptsächliche) Unterschied liegt darin, wie man nun die Rückrufnummer hinterlässt. Dabei ist der Delphi-typische Weg die Verwendung eines Methodenzeigers. Ein Methodenzeiger speichert die Adresse einer Methode. Das ist ein eigener Datentyp, der einfach eine Adresse speichert. Du weißt dann, dass eine solche Adresse auf eine Methode zeigt und wie deren Signatur aussieht. Du weißt also welche Parameter Du übergeben musst und was für ein Ergebnis Du bekommst (keins wenn es eine Procedure ist).
Ein Methodenzeiger wird wie folgt deklariert:
Delphi-Quellcode:
type
TMethodenZeiger = procedure(Argument1: Typ1; ... ArgumentN: TypN) of object;
// oder auch
TMethodenZeiger2 = function(Argument1: Typ1; ... ArgumentN: TypN): TRueckgabeTyp of object;
Wie Du hier siehst, werden Methodenzeiger als Datentyp deklariert. Du gibst einfach einen Namen an und die eigentliche Deklaration besteht aus dem Schlüsselwort
procedure oder
function gefolgt von den Argumenten (soweit vorhanden) und für Funktionen dem Rückgabetyp. Der deutliche Unterschied zu normalen Methoden besteht hier schon darin, dass Du keinen Namen der Methode angibst. Zudem wird immer das Suffix
of object angehangen.
Lässt Du Letzteres weg, so handelt es sich um einfach Funktionszeiger. Der Unterschied zwischen Funktions- und Methodenzeiger ist dabei recht einfach erklärt, Methoden gehören immer zu einem Objekt (Instanz einer Klasse), Funktionszeiger tun das nie! Beide sind in Delphi unterschiedlich aufgebaut und müssen deswegen unterschieden werden (Methodenzeiger verwenden ein verstecktes Argument, das Objekt).
Der Unterschied besteht also darin, dass Du einem Methodenzeiger nur Funktionen und Prozeduren (deren Adresse) zuweisen kannst, die zu der Instanz einer Klasse gehören. Umgekehrt kannst Du Prozeduren und Funktionen, die zu einer Klasse gehören aber nie einem Funktionszeiger zuordnen.
Wie gesagt, Delphi verwendet i.d.R. immer Methodenzeiger.
Wie man die deklariert hat, hast Du schon gesehen. Nach der Deklaration kannst Du die wie jeden anderen Datentypen auch verwenden und Variablen von diesem Typ anlegen. Diese Variablen können natürlich einen Wert annehmen und wie gesagt, dieser muss eine Adresse auf eine Methode (Prozedur oder Funktion eines Objekts) sein. Zudem muss die Signatur der Methode mit der deklarierten übereinstimmen (also müssen gleichviele Parameter in gleicher Reihenfolge und mit gleichem Typ übergeben werden).
Typischerweise legt man in Klassen alle Variablen mit geringster Sichtbarkeit (private) an (auf die Gründe gehe ich nicht näher ein, hier wäre ein Tutorial zur
OOP nötig). Die eigentlichen Lese- und Schreibzugriffe delegiert man (Stichwort property). Ein Delphi property wirkt nach außen wie eine Variable, der Zugriff kann jedoch an eine Methode oder Variable delegiert werden.
Delphi-Quellcode:
type
TKlasse = class(TObject)
private
FVariable: TTyp;
protected
procedure setVariable(const FValue: TTyp);
public
property Variable: TTyp read FVariable write setVariable;
property VariableRO: TTyp read FVariable;
end;
So ungefähr könnte das dann aussehen. Hier wird eine private Variable
FVariable vom Typ
TTyp angelegt. Den gleichen Typ haben die beiden properties (aber andere Namen). Dem Schlüsselwort property folgt etwas, dass wie eine normale Variablendeklaration aussieht (
property Variable: TTyp). Dem wiederum folgt die Delegation des Lesens und/oder Schreibens (mindestens eine Eigenschaft muss hier delegiert werden). Im ersten Fall würde beim Lesen von
Variable direkt die Instanzvariable
FVariable gelesen werden. Würdest Du
Variable einen Wert zuweisen (schreiben), dann würde hier die Methode setVariable aufgerufen werden. Der Parameter hat dabei automatisch den Wert, den Du setzen wolltest.
Hier siehst Du auch gleich warum man das macht, so ist
VariableRO ReadOnly. Hier wurde nur das Lesen delegiert. Würdest Du versuchen diesen Wert für eine Instanz zu schreiben, wird Dir ein Fehler (schon zur Compilezeit) gemeldet. Aber auch das Delegieren macht Sinn. So kann setVariable prüfen, ob der übergebene Wert überhaupt gültig ist. Dies kann wichtig sein, wenn es Einschränkungen der gültigen Werte gibt. Auch das Lesen eines Wertes kann natürlich an eine Funktion delegiert werden. So wäre z.B. eine Eigenschaft Mittelwert denkbar, die Du natürlich leicht bei Bedarf berechnen kannst (statt sie in einer Variable zu speichern und bei allen Änderungen zu aktualisieren).
Ja, das gleiche wird meist auch mit Methodenzeigern gemacht. Allerdings werden die Zugriffe hier meistens direkt an die Variable deklariert (nur damit Du dich im Folgenden nicht wunderst).
So, alles zusammen ergibt jetzt die Ereignisbehandlung. Du legst einfach eine Klasse an, die ein property OnEventX besitzt. Dieses ist vom Typ ein Methodenzeiger und speichert die Rückrufnummer für die Ereignisbehandlung (in einer Variable).
Tritt jetzt Dein Ereignis ein, so schaust Du nach, ob eine gültige Rückrufnummer hinterlegt wurde. Ist dies der Fall, rufst Du dort an und teilst das Ereignis mit. Ist dies nicht der Fall, so kannst Du niemanden rückrufen und musst entsprechend nichts machen.
Das ganze mal komplett:
Delphi-Quellcode:
type
TRückruf = procedure(const arg1: String) of object;
TCallMe = class(TObject)
public
procedure callMe(const t: String);
end;
TKlasseMitEreignis = class(TObject)
private
FRückrufAdresse: TRückruf;
...
public
procedure doFoo;
....
property OnEreignis: TRückruf read FRückrufAdresse write FRückrufAdresse;
end;
Das sind jetzt erstmal nur die Datentypen. Du hast einfach einen Datentypen als Methodenzeiger, eine Klasse, deren Instanzen zurückgerufen werden können und eine Klasse, die die Rückrufe durchführt, sobald etwas bestimmtes passiert. In der letzten ist die weitere Funktionalität durch eine Methode doFoo angedeutet. doFoo könnte z.B. Auslöser der Ereignisbehandlung sein.
Die Implementierung von TCallMe ist hier völlig egal. Stell Dir einfach vor, dass die Klasse ein ShowMessage mit dem Argument t macht, sobald die Methode callMe aufgerufen wird.
Bleibt noch der Aufruf der Ereignisbehandlung:
Delphi-Quellcode:
procedure TKlasseMitEreignis.doFoo;
begin
...
// Benachrichtigung
// prüfen ob eine gültige Rückrufadresse zugewiesen wurde
if assigned(OnEreignis) then
begin
// Rückruf einer Prozedur, die ein Argument vom Typ String bekommt
self.OnEreignis('WAS AUCH IMMER');
end; // if assigned(OnEreignis)
end;
So sieht dann einfach der Rückruf aus. Du kannst auf die Variable, die vom Typ ein Methodenzeiger ist wie auf eine normale Methode zurückgreifen. Mit dem assigned wird überprüft, ob die Adresse <> nil ist. Dies ist der Fall, sobald einmal eine gültige Adresse zugewiesen wurde. Nil (0) ist die einzigste verbotene Adresse. Auf einem 32-Bit Rechner bleiben damit (2^32 - 1) Adressen übrig (so gute 4,2 Mrd.). Das heißt aber nicht, dass jede Adresse <> nil gültig ist. Weißt Du eine Adresse zu, die zu einem Objekt gehört, so ist die nur gültig, bis Du das Objekt löscht. Du solltest Dich dann vor dem Löschen des Objekts darum kümmern, dass Du dort, wo es zurückgerufen wird die Rückrufnummer wieder auf nil setzt. Ansonsten bleibt die alte Rückrufnummer gespeichert, aber der Anschluss, der zurückgerufen wird wurde schon entfernt. Das gibt dann ein Problem (eine
Exception).
Zu guter Letzt bleibt noch das setzen der Variable. Das schauen wir uns gleich am Beispiel an:
Delphi-Quellcode:
var klasseMitEreignis: TKlassesMitEreignis;
rückrufKlasse: TRückruf;
begin
// neue Instanzen erstellen
klasseMitEreignis := TKlasseMitEreignis.Create;
rückrufKlasse := TRückruf.Create;
// eigentliches Registrieren der Rückruf-Adresse
klasseMitEreignis.OnEreignis := rückrufKlasse.callMe;
....
end;
Natürlich solltest Du die Instanzen nicht in lokalen Variablen speichern, wie es hier der Fall ist, sonst wären die nach dem Verlassen der Methode eh unerreichbar und sollten hier schon wieder frei gegeben werden (anders gesagt, das Beispiel zeigt etwas unsauberes, nicht so nachmachen
).
Das Prinzip sollte aber klar sein.
Eine Sache ist hier jetzt aber ganz schlecht! Wie gesagt, es ist der Delphi-Standardweg. Trotzdem hast Du das Problem, dass Du nur eine Variable hast, also nur eine Rückrufadresse speichern kannst. Natürlich kannst Du für jede mögliche Rückrufadresse eine eigene Variable vorsehen, aber das verschiebt nur das Problem (man weiß nicht immer wie viele Spieler wirklich am Spiel teilnehmen wollen). Besser ist das Observer-Pattern zu verwenden.
Dieses Entwurfsmuster beschreibt zwei Rollen, das Observable (beobachtbares Objekt) und die Observer (Beobachter). Ein Observable bietet dabei die Möglichkeit, dass sich Beobachter über eine bestimmte Schnittstelle registrieren und wieder deregistrieren können. Observer registrieren sich also beim Observable und alle registrierten Observer werden werden vom Observable zurückgerufen. Wie das genau geschieht (wie die registrierten Observer verwaltet und rückgerufen werden) ist dabei nicht durch das Pattern vorgegeben (Pattern definieren nur ein Problem und eine allgemeingültige Lösung, die unabhängig von der Implementierung ist!). Somit ist nur eindeutig bestimmt, was für eine Technik verwendet wird.
Wie Du es z.B. in Delphi realisieren kannst, kannst Du dem einen Thread (hatte den Link denke ich schon gepostet) entnehmen.