© Martin Korneffel, Stuttgart 2005 +++ email: trac@n.zgs.de +++ web: www.s-line.de/homepages/trac
Objekt |
Ein Objekt steht für ein konkretes Ding (z.B. Der rote Ferrari von Fred Vollgas). In C# sind sind Objekte Bündel aus speziellen Daten und Prozeduren, die über eine gemeinsamme Hauptspeicherreferenz erreichbar sind (z.B. FredsFerrari.Farbe oder FredsFerrari.gasGeben(100);) In der objektorientierten Sichtweise werden die Prozeduren eines Objektes als Methoden, und die Daten als Elemente bezeichnet. |
Durch die Einführung des Objektbegriffes in C# können Systeme mit komplexer innerer Struktur direkt beschrieben werden. Jedes Objekt wird mit einem Namen gekennzeichnet, jeder Prozess in einem Objekt durch eine sogenannte Methode ausgedrückt, und jeder Zustand durch eine Eigenschaft dokumentiert. Beziehungen und Prozesse, an denen mehrere Objekte beteiligt sind, können durch Methoden, die Objekte als Parameter erwarten, und Eigenschaften, die wiederum Objekte sind, ausgedrückt werden.
Aufgaben:
Beschreiben Sie folgende Systeme durch Objektmengen. Stellen Sie die Objekte durch Objektdiagramme dar.
Bankkonto
Fotoalbum
Vektorrechnung
Klasse |
Eine Klasse ist eine Menge von Objekten, die einen gemeinsammen strukturellen Aufbau aus Daten und Prozeduren haben. Die Menge kann durch eine Klassendeklaration beschrieben werden. Eine Klassendeklaration listet Deklarationen von Datenelementen und Prozeduren auf, die die Objekte der Klasse enthalten. |
In C# werden Klassen wie folgt deklariert:
// class Klassenname : Basisklasse { ... } class CAuto { // Konstruktor CAuto(string name) { nameFahrer = name; } // Destruktor ~CAuto() { ... } // Felder private string nameFahrer = ""; // Eigenschaftsdefinitionen public Fahrer { get{ return nameFahrer; } } // Methoden public int fahren(char richtung, float v) { ... } }
Eigenschaften und Methoden einer Klasse, die unabhängig von einem konkreten Objekt sind, werden statische Klassenkomponenten genannt. Sie werden bei der Deklaration mit dem Schlüsselwort static ausgezeichnet.
class C { public static int anz_instanzen; }
Statische Klassenkomponeten sind nur über den Klassennamen erreichbar.
C.anz_instanzen;
Eine Klasse, die nur statische Member enthält, wird statische Klasse genannt. Sie besitzen folgende Merkmale
die Deklaration erfolgt durch static class ...
aus statischen Klassen können keine Objekte instanziiert werden
statische Klassen sind versiegelt
Die Deklaration einer Klasse kann auf mehrere Quelltextdateien verteilt werden. In jeder Quelltextdatei wird die Klasse mit dem zusätzlichen Schlüsselwort partial deklariert.
Konstruktoren |
Sind vom Anwender überschreibbare Prozeduren, die unmittelbar nach Allokation des Objektes der Klasse auf dem Heap gestartet werden. Konstruktoren können wie normale Prozeduren Eingaben verarbeiten, jedoch können sie keine Ausgaben vornehmen ! In C# werden Konstruktoren in Memberfunktionen ohne Rückgabetyp definiert. Diese Memberfunktionen müssen den Namen der Klasse tragen, z.B: class Lager { public static int instancecounter = 0; // Konstruktor public Lager(int pKapzitaet) { instancecounter ++; } : } |
Um statische Klassenkomponenten zu initialisieren gibt es spezielle Konstruktoren, genannt statische Konstruktoren. Diese werden beim erstmaligen gebrauch einer Klasse aufgerufen:
class C { public static int anz_instanzen; static C() { anz_instanzen = 1; } }
Das Implementieren von Konstruktoren, die alle möglichen Einsatzszenarien abdecken, ist häufig zeitraubende Routinearbeit. Ab .NET 3.5 bieten sich als bequeme Alternative Objektinitialiserer an. Hierbei kann eine in geschweiften Klammern gefasste Initialisierungsliste für Eigenschaften und Felder dem Konstruktor einer Klasse übergeben werden. Reihenfolge und Vollständigkeit spielen dabei keine Rolle.
Beispiel:
Die folgende Klasse verzichtet auf explizite Konstruktoren.
class Filedesccriptor { public string filename; public string filetype; public long sizeInBytes; }
Stattdessen instanziiert der Anwender, indem dem Konstruktor eine für den Anwendungsfall spezifische Initialisierungsliste übergeben wird.
// Klassische Initialisierung Filedesccriptor fd = new Filedesccriptor(); fd.filename = "C:\boot.ini"; //... // Neu: Objektinitialisierer anstelle Konstruktors Filedesccriptor fd2 = new Filedesccriptor{ filename = "boot.ini", filetype = ".ini", sizeInBytes = 999 };
Mit der Einführung von Linq ab .NET 3.5 wurden auch das Feature der anonyme Typen erforderlich. Das Ergebnis einer Linq- Abfrage muss wg. der strengen Typisierung in .NET einen Typen haben, der der Struktur des Ergebnisses entspricht. Da der Compiler aus der Linq- Abfrage die Typdeklaration selbständig ableiten kann, haben die C# Entwickler mit den anonymen Typen den Programmiere von der Pflicht entbunden, für jede Linq- Abfrage eine Typdeklaration für das Ergebnis anzufertigen.
// Anonyme Typen var fdAnonym = new { filename = "boot.ini", filetype = ".ini", sizeInBytes = 999 }; // Auch für anonyme Typen gelten strenge Typisierung fdAnonym.sizeInBytes = "Hallo Welt"; // Fehler
Destruktoren |
Spezielle Funktionen einer Klasse, die immer unmitterlbar vor dem Löschen eines Objektes durch den GC starten. Der C#- Compiler konvertiert den Destruktor einer Klasse automatisch in die Finalize – Methode von System.Object. class Lager { public static int instancecounter = 0; ... // Destruktor ~Lager(int pKapzitaet) { instancecounter --; } : } |
Dispose- Methode |
Jede Klasse kann die Schnittstelle IDisposable implementieren, welche die Methode Dispose() deklariert. Dispose wird in einer Klasse implementiert, um sofort Resourcen freizugeben, wenn das Objekt nicht mehr benötigt wird. Hat die Dispose- Methode bereits alle Aufräumarbeiten erledigt, dann kann der explizite Aufruf des Destruktors durch den GC unterdrückt werden wie folgt: GC.SuppressFinalize(this); |
In C# kann der Instanzierung, Nutzung und schließlich Aufruf der Mehtode Dispose durch den using- Block in eine strukturiete Form überführt werden:
using (ClassA obj = new ClassA()) { // Anweisungen } // Automatischer Aufruf von Dispose
Erweitern Sie die Klasse CAuto um Kontstruktor, Destruktor und Dispose- Methode. Testen Sie diese in einem Kommandozeilenprogramm.
Konstanten sind Felder, deren Wert zur Kompilationszeit definiert wird, und die anschließend nur über den Klassennamen referenzierbar sind.
const float e = 2.72;
Readonly- Member sind Felder, deren Wert zur Konstruktionszeit definiert werden. D.h. ihnen können im Konstruktor Werte zugewiesen werden. Zu späteren Zeitpunkten ist keine Zuweisung mehr möglich.
class Figur { readonly int max_anz_leben; Figur(int max_al) { max_anz_leben = max_al; } void ueberleben() { max_anz_leben ++; // Fehler, da readonly } : }
Bei der objektorientierten Programmierung fällt auf, das verschiedene Klassendekalrationen in einem Projekt Abschnitte mit identischen Deklarationen von Eigenschaften und Methoden enthalten können. Hier kann es sinvoll sein, diese Gemeinsamkeiten zentral an einer Stelle im Programm zu deklarieren zwecks Erhöhung der Übersichtlichkeit und Vereinfachung der Wartung.
Ein anderer Aspekt ist die Verwaltung von Objekten unterschiedlicher Typen in einer Liste wie z.B. bei der programmtechnischen Darstellung von CAD- Zeichnungen (Menge aus Linen, Kreisen etc). Ein Ausdruck der Zeichnung wird durch einen Durchlauf der Liste realisiert, wobei von jedem Objekt die draw- Methode aufgerufen wird.
Technische 2D Zeichungen sind aus elementaren geometrischen Primitiven wie Linien, Rechtecke, Kreise, Ellipsen, Splines etc.. Es bietet sich in bietet sich an, den Entwurf damit zu beginnen, für jedes Primitiv eine Klasse zu deklarieren:
Wie
aus dem Diagramm ersichtlich, haben alle Klassen einen Satz
gemeinsamer Eigenschaften und Methoden:
Color, zur Darstellung der Linienfarbe
style, zur Darstellung der Strichart
unit, zur Darstellung der Einheit, in der gezeichnet wird
Methode draw zum Zeichnen der Figur auf einem Ausgabemedium
Methoden translate, rotate und scale zum Verschieben, Drehen und Strecken der Figur
Die Eigenschaften color, style und unit sind in allen Klassen die gleiche. Sie können in einer gemeinsammen Basisklasse aller Primitivklassen zentral deklariert werden. Nennen wir diese Klasse CFigur. Durch Vererbung werden die von CFigur abgeleiteten Klassen mit den Eigenschaften von CFigur ausgestattet:
Die
Daklaratioen der Methoden draw, translate, rotate und scale
können nicht einfach in die Basisklasse CFigur verschoben
werden. Denn für jedes Klasse zu einem Grafikprimitv ist eine
individuelle Implentierung diesser notwendig.
Die Vererbung wird in der abgeleiteten Klasse wie folgt deklariert:
class CLinie : CFigur { : }
Bei der Vererbung kann es schnell zu Namenskonflikten kommen. Beispielsweise implementieren CLinie und CFigur jeweils einen Konstruktor new(). Zur korrekten Instanziierung eines CLinie- Objektes ist es notwendig, daß auch der Konstruktor der Basisklasse CFigur aufgerufen wird. Hier ist eine Unterscheidung im Kontext der abgeleiteten Klasse zwischen eigenen Membern und der der Basisklasse nötig. Dies geschieht durch das Schlüsselwort base
public class CLinie : CFigur { : public new() : base() // Aufruf des Konstruktors aus CFigur { : } : }
Mittels base können auch überdeckte Member aus Basisklassen in abgeleiteten Klassen wieder sichtbar gemacht werden:
In der Klassenbibliothek für 2D- Zeichnungen wurde für jedes Primitiv ein Satz von Transformationsfunktionen definiert (translate, rotate, scale). Nehmen wir die Translationsfunktion. Sie könnte zu. Beispiel wie folgt definiert werden:
void translate(float tx, float ty)
Anstelle von tx und ty könnte aber auch ein Punkt übergeben werden, der das Ende des Verschiebevektors kennzeichnet:
void translate(SPoint vec)
Um die erste, als auch zweite Variante zu ermöglichen, muß das Schlüsselwort Overloads eingesetzt werden (insbesondere bei Vererbung)
Overloads Sub translate(tx as Single, ty as Single) Overloads Sub translate(vec as SPoint)
Bei der Entwicklung großer Programmpakete hat es sich als sinvoll erwiesen, Implementierungsdetails von Bibliotheken vor dem Anwender zu verbergen. In VB.NET wird dieses sog. Kapselungsprinzip durch Zugriffmodifikatoren für Klassenmember und Schnittstellen realisiert.
Zugriffsmodifikator |
Beschreibung |
Nicht deklarierebar in |
---|---|---|
(keiner) |
nur im Block sichtbar, in dem deklariert wurde |
|
public |
überall sichtbar. Über Public- Methoden und Eigenschaften werden an die Objekte der Klasse Nachrichten gesendet. |
Prozeduren, Funktionen |
private |
nur in der Klasse sichtbar, in der Member deklariert wurde. Zur Kapselung von Implementationsdetails in einer Klasse |
Prozeduren, Funktionen |
protected |
nur von Membern der eigenen oder von Membern in abgeleiteten Klassen sichtbar. Zur Kapselung von Implementationsdetails in einer Klassenhierarchie |
Prozeduren, Funktione, Module |
internal |
ist überall innerhalb der Assembly sichtbar. Außerhalb der Assembly nicht sichtbar. Zur Kapselung von Implementationsdetails in einer Assembly |
Prozeduren, Funktionen |
Die Angabe des Zugriffsmodifizierers ist optional. Wird kein Zugriffsmodifizierer angegeben, dann gelten folgende Voreinstellungen:
Klasse |
internal |
Klassenmitglied |
private |
Module Module1 Class CFestung Public Shared mShared As Int16 Private mPrivat As Short Protected mProtected As Int16 Friend mFriend As Int16 Public mPublic As Int16 Sub New(ByVal init As Short) mShared = init mPrivat = init + 1S mProtected = init + 2S mFriend = init + 3S mPublic = init + 4S End Sub Public Sub tor_auf() ' Auf Private Elemente kann nur in Mathoden aus der Klasse ' selbst zugegriffen werden mPrivat = 1000 End Sub ' Ein Ereignis deklarieren Public Event treffer() ' Das Ereignis selbst auslösen Public Sub getroffen() RaiseEvent treffer() End Sub End Class ' Eine von Festung abgeleitete Klasse Class CBurg Inherits CFestung ' Konstruktoren werden nicht vererbt. Jede Klasse hat ihren ' eigenen Satz von Konstruktoren (Initialisierungsroutinen) Sub New(ByVal init As Int16) MyBase.New(init) ' Auf Protected- Member aus der Basisklasse kann in der abgeleiteten Klassen ' zugegriffen werden mprotected *= 10S End Sub Function gebe_protected_aus() As Int16 Return mprotected End Function End Class ' Ein Eventhandler für Objekte vom Typ Festung Sub festung_treffer() Console.WriteLine("Festung wurde getroffen") End Sub Sub Main() ' Eisenstein ist ein Objekt vom Typ CFestung Dim Eisenstein As New CFestung(10) Dim Dreistein As New CFestung(100) Dim Raubstein As New CBurg(1000) Console.WriteLine("Zuegriff auf Protected {0}", Raubstein.gebe_protected_aus()) ' Eventhandler registrieren AddHandler Eisenstein.treffer, AddressOf festung_treffer ' Zugriff auf die Member CFestung.mShared *= -1S Eisenstein.mShared *= 99S ' Nur shared Elemente können dierekt über die Klasse aufgerufen ' werden. Es mus kein Objekt/Instanz existieren, um mit dem ' Element zu arbeiten 'CFestung.mPublic *= 12S With Eisenstein '.mPrivat *= -1 '.mProtected *= -1S .mFriend *= -1S .mPublic *= -1S ' Ereignis auslösen .getroffen() .tor_auf() End With Console.ReadLine() End Sub End Module
Polymorphe Operation |
(Vielgestaltig) Sind Funktionen oder Prozeduren, die unter gleichem Namen und mit gleicher Parameterliste in verschiedenen Klassen deklariert, jedoch verschieden implementiert sind. Beim Aufruf über ein Objekt wird immer die Implementierung aus dem Typ des Objektes genommen. Erfolgt der Aufruf über eine Referenz vom Typ einer Basisklasse, dann wird zur Laufzeit die Implementierung über sog. virtuelle Funktionstabellen bestimmt. |
Beim Instanziieren werden die vtabels so initilisiert, daß ihre Einträge auf die korrekten Funktionen zeigen.
Im folgenden Beispiel wird das Erzeugen von Arbeitsfortschrittsmeldungen in der Methode MakeProgressInfo gekapselt. Diese wird zu bestimmten Zeitpunkten beim Scannen eines Verzeichnisses aufgerufen, um den Client von DirTree über den aktuellen Arbeitsstand zu informieren. In abgeleiteten Klassen können detailiertere Informationen zum Arbeitsfortschritt benötigt werden. In diesem Falle muß von der Klasse DirTreeProgressInfo eine speziellere Arbeitsfortschrittmeldung abgeleitet werden. In einer Methode, welche MakeProgressInfo überschreibt, muß die abgeleitete Arbeitsfortschrittmeldung dann erzeugt werden.
namespace DMS { // Basisklasse für alle Klassen, die einen rekursiven Durchlauf durch einen Dateibaum // durchführen public class DirTree { //----------------------------------------------------------------------------- // Member zur Ausgabe des Arbeitsfortschrittes // Klasse mit Informationen über den Arbeitsfortschritt. // Detailiertere Arbeitsfortschrittmeldungen müssen von dieser Klasse // abgeleitet werden. public class DirTreeProgressInfo : ProgressInfo { public int CountAllDirs; public int CountAllFiles; public DirTreeProgressInfo(int CountAllDirs, int CountAllFiles) { this.CountAllDirs = CountAllDirs; this.CountAllFiles = CountAllFiles; } } // Funktionszeigertyp von Handlern zur Behandlung des Arbeitsfortschritts- Event public delegate void DGEventProgress(DirTreeProgressInfo info); // Ereignis: Arbeitsfortschritt public event DGEventProgress EventProgress; // Generator für Arbeitsfortschrittmeldungen: Kann in abgeleiteten Klassen // überschrieben werden, um detailiertere Arbeitsfortschrittmeldungen, die von // DirTreeProgressInfo abgeleitet sind, zu erzeugen protected virtual DirTreeProgressInfo MakeProgressInfo() { return new DirTreeProgressInfo(m_dir_count, m_file_count); } } }
Schnittstellen von Basisklassen werden an Kindklassen vererbt
Schnittstellen können von Schnittstellen abgeleitet werden
In einer Klasse kann eine Schnittstelle implementiert werden, oder sie kann von einer abstrakten Klasse abgeleitet sein. Beides ist nicht das gleiche! Über eine Schnittstelle "steuern" wir eine BlackBox. Die BlackBox ist irgendein Objekt, dessen Klasse die Schnittstelle implementiert hat. Eine abstrakte Klasse ist ein Datentyp, der durch Ableitung verfeinert wird. Die Instanzen der abgeleiteten Typen lassen sich immer durch implizite Konvertierung wie Typen der abstrakten Basisklasse behandeln. Eine solche Verwandschaft besteht bei den über Schnittstellen steuerbaren Objekten im allg. nicht.
Alle Elemente einer Schnittstelle sind public. Es können nur Deklarationen für Eigenschaften, Methoden und Ereignisse Elemente von Schnittstellen sein. Datenfelder und Implementierungen von Eigenschaften und Methoden dürfen in Schnittstellendeklarationen nicht enthalten sein.
public interface IPlotter{ // Eigenschaft, über welche der Zeichenstil in Ausgabeoperationen beeinflusst wird CStyle style { get; set;} void print_line(SPoint a, SPoint b) void print_circle(SPoint m, double radius) // Ereignis, welches einen Fehler während des Zeichnens signalisiert event DGError Error; }
Die Elemente einer Schnittstelle könne als Member einer Klasse implementiert werden. Diese Form wird implizite Implementierung genannt. Bei der impliziten Implementierung können die Member der Schnittstelle wie alle anderen Member einer Klasse über die Instanzen der Klasse aufgerufen werden.
public Class CDxfPlotter : IPlotter public print_line(SPoint a, SPoint b, CStyle style) { : } } : CDxfPlotter dxfPlotter; // Der folgende Aufruf ist bei der expliziten Implementierung nicht mehr möglich // dxfPlotter.print(...);
Der Grundgedanke für Schnittstellen, die Steuerung einer BlackBox, wird bei der impliziten Implementierung etwas verwässert. Durch die sog. explizite Implementierung wird eine Schnittstelle klar vom Objekt separiert, indem der Zugriff auf die Schnittstellenmember nicht mehr über eine Instanz, sondern nur noch über einen Schnittstellenzeiger möglich ist:
public Class CDxfPlotter : IPlotter // Bei der expliziten Implementierung müssen die Schnittstellenmember private implementiert werden void IPlotter.print_line(SPoint a, SPoint b, CStyle style) { : } } : CDxfPlotter dxfPlotter; // Der folgende Aufruf ist bei der expliziten Implementierung nicht mehr möglich // dxfPlotter.print(...); // Um auf die Schnittstellenmember zuzugrifen, muß eine Referenz auf die Schnittstelle angelegt werden IPlotter iPlotter = dxfPlotter; // Über die Schnittstellenreferenz kann jetzt auf die Member zugegriffen werden iPlotter.print(....);
In vielen .Net Sprachen ist der foreach - Algorithmus ein Merkmal der Sprache. Alle Klassen, auf deren Objekte der foreach- Algo. anwendbar sein soll, müssen dabei die IEnumerable- Schnittstelle unterstützen, welche wiederum ein Objekt mit der IEnumerator- Schnittstelle liefert. Mittels der IEnumerator- Methoden diese Objektes kann dann der foreach- Algo. die Menge durchlaufen. Folgendes Objektsequenzdiagramm stellt den Ablauf dar:
Der
Aufwand zur Implementierung der IEnumerator- Schnittstelle in einer
Klasse kann in .NET 2.0 eingespaart werden durch Iteratoren.
Definition |
|
---|---|
Iterator |
Ein Iterator ist eine Liste, welche für jeden Schleifendurchlauf des foreach- Algorithmusses den einzusetzenden Wert aus der Menge, durch die iteriert wird, definiert. Aus dieser Liste generiert der C#- Kompiler Klassendeklarationen, die gemäß der Beschreibung die IEnumerator- Schnittstelle implementieren. |
Die
Werte, die ein Iterator bei einer Iteration liefert, werden durch
eine Folge von yield retun <Ausdruck> -definiert.
Wiederholen sich die yield- return Anweisungen nach bestimmten
Gesetzmäßiskeiten, so kann dies auch in einer for -
Schleifenkonstruktion ausgedrückt werden. Zu beachten ist
hierbei, das die for- Schleifenkonstruktion hier die Definition der
Liste aus yield Anweisungen darstellt, deren Ergebnisse für
jeden foreach- Schleifendurchlauf genutzt werden, und nicht wie
gewöhnlich eine Kontrollanweisung zur Steuerung des
Programmflusses ist.
Im Beispiel wird eine Klasse mit Iteratordeklarationen implementiert. Wie aus dem Beispiel ersichtlich, kann eine Klasse beliebig viele Iteratordeklarationen enthalten. Iteratoren können auch mit Parametern ausgestattet werden.
class CMengeMitIteratoren { int[] primz = { 2, 3, 5, 7, 11, 13, 17, 23 }; // Iterator, der die ersten 3 Eniräge durchläuft public System.Collections.IEnumerable ItErsteDrei() { yield return primz[0]; yield return primz[1]; yield return primz[2]; } // Iterator, welcher alle Einträge durchläuft public System.Collections.IEnumerable ItAlle() { for (int i = 0; i < primz.Length; i++) yield return primz[i]; } // Iterator, welcher die ersten n Einträge durchläuft public System.Collections.IEnumerable ItErsteN(int n) { for (int i = 0; i < Math.Min(n, primz.Length); i++) yield return primz[i]; } } // Anwendung von Iteratoren static void Main(string[] args) { CMengeMitIteratoren Menge = new CMengeMitIteratoren(); Console.WriteLine("Ausgabe der ersten drei"); foreach (int z in Menge.ItErsteDrei()) { Console.WriteLine(z); } Console.WriteLine("Nochmalige Ausgabe der ersten drei"); foreach (int z in Menge.ItErsteDrei()) { Console.WriteLine(z); } Console.WriteLine("Ausgabe aller"); foreach (int z in Menge.ItAlle()) { Console.WriteLine(z); } Console.WriteLine("Ausgabe der ersten 5"); foreach (int z in Menge.ItErsteN(5)) { Console.WriteLine(z); }
In der Parxis werden aus für Anwendungfall spezielle Datenstrukturen und Algorithmen entworfen und implementiert. Oft besteht das spezielle darin, daß die Algorithmen und Datenstrukturen an spezielle Datentypen gebunden sind. So kann eine Sortierroutinen für Arrays aus Integern entwickelt, und eine Koordinate als Wertepaar zweier floats dargestellt werden.
Der Abläufe in Sortierroutinen für int- und string- Arrays sind fast die gleichen. Wenn die Gemeinsamkeiten z.B. durch ein Code- Snippet in VS2005 dargestellt werden, in das die zu soriterenden Typen zur Entwurfszeit eingesetzt werden müssen, dann hat man mit dem Snippet einen sehr allgemein verwendbaren Sortieralgorithmus. Exakter ausgedrückt stellt das Snippet eine Definition für eine Familie von Sortiealgorithmen dar.
Diese Vorgehenssweise kann auf viele speziell entwickelten Algorithmen und Datenstrukturen ausgedehnt werden. Sie wird als generische Programmierung bezeichnet.
Definition |
|
---|---|
Generische Programmierung |
Generische Programmierung bezeichnet eine Form des Programmierens, bei der eine Datentyp- unabhängige Beschreibungen von Algorithmen und Klassen verfasst werden. |
C# unterstüzt die generische Programmierung dierekt. Durch Syntaxelemente wie Typparameter, die an C++ angelehnt sind, können schnell Generische Typen definiert werden. Der Compiler überwacht dabei die Typsicherheit in der Definition der Generischen Typen und bei der Instanziierung von konkreten Typen aus ihnen.
Insbesondere die Überwachung der Typsicherheit in der Definition der generischen Typen ist ein Unterschied zum Urvater C++. Unter Einschränkungen wird dies noch näher erläutert.
hochgradig wiederverwendbarer Code durch das hohe Abstraktionsniveau der datentypunabhängigen Definitionen von Algorithmen und Klassen auf der einen-, und die einfache Instanziierung konkretern Algorithmen und Klassen aus diesen abstrakten Definitionen zur Entwurfszeit auf der anderen Seite
Fördert die Typsicherheit im Code
vermindert die Systembelastung zur Laufzeit, da viele Boxing/Unboxing Operationen entfallen
Typparameter sind Platzhalter für Datentypen in Klassen-, Schnittstellen-, Mehtoden und Delegate- Deklarationen. Sie müssen in einer Typparameterliste deklariert werden, welche in spitzen Klammern eingeschlossen sind:
class CMeasure<TValue, TEnumUnit> { ... }
Die Bezeichnung von Typparametern sollte nach dem Schema TsprechenderParametername erfolgen.
Generische Typen entstehen durch Klassendeklarationen mit Typ- Parametern. Beispiel:
// Generische Klasse zur Darstellung von Maszen mit 2 Typparametern class CMeasure<TValue, TEnumUnit> { public TValue value; public TEnumUnit unit; }
Aus dem generischen Typen CPoint<T> könne zur Entwurfszeit konkrekte Typen instanziiert werden:
enum UnitS { mm, cm, dm, m, km } enum UnitT { ms, s, min, h } ... CMeasure<double, UnitS> WegX = new CMeasure<double, UnitS>(); CMeasure<int, UnitT> ZeitX = new CMeasure<int, UnitT>(); WegX.value = 100; WegX.unit = UnitS.km; ZeitX.value = 200; //ZeitX.unit = UnitS.cm; // Kompilationsfehler !!! ZeitX.unit = UnitT.s;
Analog den Klassen können auch generische Schnittstellen deklariert werden. Das Resultat sind Schnittstellemengen mit einheitlichem Aufbau.
Im Beispiel wird eine generische Schnittstelle deklariert, welche Konvertierungsfunktionen in einen Parametierbaren Typen anbietet.
// Schnittstelle, die einen Satz von Konvertierungsfunktionen // in einen parametrierbaren Typ anbietet public interface IValueConverter<TValue> { TValue ToValue(short val); TValue ToValue(int val); TValue ToValue(float val); TValue ToValue(double val); }
Die Instanziierung kann für eine Klasse erfolgen, wleche die Schnittstelle implementiert. Bei expliziter Implementierung der Schnitstellenmember sind dabei die Typparameter jeweils zu instanziieren.
Im Beispiel wird eine Klasse mit Konvertierungsfunktionen in Int32 implementiert, wobei die Konvertierungsfunktionen wierderum Implementierungen für die Instanz IValueConverter<int> aus dem generischen Typ IValueConverter<T> sind.
class CIntConverter : IValueConverter<int> { public CIntConverter() { } #region IValueConverter<int> Member int IValueConverter<int>.ToValue(short val) { return val; } int IValueConverter<int>.ToValue(int val) { return val; } int IValueConverter<int>.ToValue(float val) { return (int)val; } int IValueConverter<int>.ToValue(double val) { return (int)val; } #endregion }
Ohne weitere Informationen für einen Typparameter ist das Arbeiten mit diesen sehr beschränkt:
Definition |
|
---|---|
Ungebundene Typparameter |
Werden für einen Typparameter T keine Einschränkungen explizitdefiniert, dann ist T ein sog. ungebundener Typparameter. Für ungebundene Typparameter gelten folgende Regelen:
|
Der beschränkte Umgang mit den ungebundenen Typparametern kann sinnigerweise durch Einschränkungen (Constraints) überwunden werden. Dabei werden Merkmale definiert, die ein Typparameter mindestens besitzen muß. Die geschieht durch einen Constraint mit folgender Syntax:
where T : Merkmal [, Merkmal ...]
Der Besitz eines Merkmals stellt eine Enschränkung bei der Auswahl der Typen dar, mit der eine Typparameterlisteinstanziiert werden kann.
Folgende Merkmale können festgelegt werden:
Constraint |
Bedeutung |
---|---|
where T: struct |
T muß eine Wertetyp sein |
where T: class |
T muß ein Referenztyp sein |
where T: new() |
T muß einen Defaultkonstruktorbesitzen |
where T: <Basisklassenname> |
T muß von der genannten Basisklasse abgeleitet sein |
where T: <Schnittstellenname> |
T muß die genannten Schnittstelle implementieren |
where T: U |
Typparameter T muß gleich Typparameter U oder von diesem abgeleitet sein |
Das Beispie CMeasure<TValue, TEnumUnit> kann verfeinert werden, indem Methoden zum Umrechnen in die entsprechende SI- Standardeinheit angeboten werden. Für den Weg ist die Standardeinheit das Meter, und für die Zeit die Sekunde. Mittels dierser Umrechnugsnethoden können dann auch Wegmaße in beliebigen Einheiten miteinander verrechnet werden, indem sie vor der Operation in die SI Standardeinheit umgerechnet (normiert) werden.
Die Normierung von Zeitmaßen folgt nach anderen Regeln als die Normierung von Wegmaßen. Deshalb sind in speziellen Klassen für Zeit, wie auch für Wegmaße Implementierungen anzubieten. Um trozdem eine nahezu einheitliche Struktur für alle Varianten von Maßzahlen zu erreichen, könnte folgende Familie von Klassenhirarchien eingesetzt werden:
Die
Basisklasse ist ein generischer Typ und zudem abstrakt. TValue und
TEnumUnit legen wiederum die Typen des Wertes und der Einheit einer
Maßzahl fest. TValueConverter ist ein Typparameter für
Instanzen aus dem generischen Schnittstellentyp IValueConverter<T>.
Dieser wird aus noch zu erläuternden Gründen in der Methode
ToBaseUnit() benötigt.
Die Methoden FactToBaseUnit() und BaseUnit() sind abstrakt. Ihre Implementierung kann erst in einer abgeleiteten Klasse für eine spezielle SI Größe erfolgen. Im Diagramm sind es beispielhaft die Klassen CMeasureS<TValue, TValueConverter> und CMeasureT<TValue, TValueConverter>. Den Typparameterlisten dieser abgeleiteten Klassen fehlt der Parameter TEnumUnit. Diese wird in der Implementierung der abgeleiteten Klasse für die Basisklasse jeweils festgelegt. Von welchem Typ jedoch der Wert einer Maßzahl ist (z.B. int oder float) kann der Anwender der Klassenbibliothek aber immer noch frei wählen.
// Abstrakte Basisklasse für Typen von Maßzahlen public abstract class CMeasure<TValue, TValueConverter, TEnumUnit> // Durch einen Constraint wird definiert, daß TValueConverter eine Instanz der // generischen Schnittstelle IValueConverter<T> ist, sowie einen Defaultkonstruktor // besitzt where TValueConverter : IValueConverter<TValue>, new() { // Wert und Einheit einer Maßzahl werden in geschützen Feldern gepeichert protected TValue m_value; protected TEnumUnit m_unit; // Der lesende Zugriff aud die Partikel eines Maßes erfolgt über Eigenschaften public TValue value { get { return m_value; } } public TEnumUnit unit { get { return m_unit; } } // Mittels der Methode Set kann ein neus Maß gesetzt werden public void Set(TValue value, TEnumUnit unit) { m_value = value; m_unit = unit; } // Gibt den Umrechnugsfaktor in die Basiseinheit zurück // (z.B. 0.001 für mm in m) public abstract double FactToBaseUnit(); // Liefert die Basiseinheit zurück // (z.B. Meter für Maszeinheit Weg) public abstract TEnumUnit BaseUnit(); // Rechnet das in einer Instanz gespeicherte Masz in die SI- Basiseinheit um // (z.B. 5 cm in Meter) public TValue ToBaseUnit() { double value_base_unit = FactToBaseUnit() * Convert.ToDouble(m_value); // Achtung: Eine Konvertierung mittels (TValue)value_base_unit wird vom // Kompiler abgewiesen, da es keine Garantie gibt, daß ein solcher Konvertierungsoperator // existiert. Ein TValueConverter besitzt aber lt. Constraint eine Methode .ToValue(..) // die in jedem Fall einen TValue zurückgibt. Durch diesen Trick haben wir dem Compiler // die Konvertierung in einen generischen Typ beigebracht. return (new TValueConverter()).ToValue(value_base_unit); } } //--------------------------------------------------------------------------------------------------- // Aufzählungstyp für die SI Einheiten eines Weges public enum UnitS { mm, cm, dm, m, km } // Klasse zur Implementierung eines Wegmaßes. Über Vererbung wird auf die Strukturen der Klasse // CMeasure zurückgegriffen. Der Typparameter TEnumUnit wird dabei durch den Typen UnitS // festgelegt public class CMeasureS<TValue, TValueConverter> : CMeasure<TValue, TValueConverter, UnitS> where TValueConverter : IValueConverter<TValue>, new() { // Defaultkonstruktor public CMeasureS() { // Mittels des Schlüsselwortes default(T) wird dem Kompiler mitgeteilt // das hier ein 0- Wert passend zum Typ gesetzt werden muß (Wertetyp= 0, // Referenztyp = Null) m_value = default(TValue); m_unit = UnitS.m; } // Konstruktor public CMeasureS(TValue initVal, UnitS initUnit) { m_value = initVal; m_unit = initUnit; } // Implementation der Umrechnugstabelle für verschiedene Wegeinheiten // in Meter public override double FactToBaseUnit() { switch (m_unit) { case UnitS.mm: return 0.001; case UnitS.cm: return 0.01; case UnitS.dm: return 0.1; case UnitS.m: return 1; case UnitS.km: return 1000; default: throw new Exception("Unbekannte Wegeinheit"); } } // SI Basiseinheit für den Weg public override UnitS BaseUnit() { return UnitS.m; } }
Methoden implementieren irgendwelche Algorithmen. Sollen diese generisch verfasst werden, dann werden die Typen von Variablen und Operanden durch Typparameter ersetzt. Da mit ungebundenen Typen ein sehr eingeschränkter Umfang von Operationen möglich sind, ist hier das definieren von Constaints die Regel.
Als Beispiel soll eine statische Methode dienen, welche Maßzahlen miteinander addiert, indem sie sie zuvor normiert.
// Definition von 3 Zeitwerten // ZeitInt1 = 2 h CMeasureT<int, CIntConverter> ZeitInt1 = new CMeasureT<int, CIntConverter>(2, UnitT.h); // ZeitInt2 = 5 min CMeasureT<int, CIntConverter> ZeitInt2 = new CMeasureT<int, CIntConverter>(5, UnitT.min); // ZeitIntSum wird die Summe aufnehmen CMeasureT<int, CIntConverter> ZeitIntSum = new CMeasureT<int, CIntConverter>(); // ZeitIntSum = ZeitInt1 + ZeitInt2 CCalcMeasures.add(new CIntALU(), ZeitInt1, ZeitInt2, ZeitIntSum);
Die Addition kann als generische Methode wie folgt definiert werden:
public class CCalcMeasures { // Allgemeiner Algortihmus zum addieren von 2 Maßeinheiten public static void add<TValue, TValueConverter, TEnumUnit>( IALU<TValue> alu, // CMeasure<TValue, TValueConverter, TEnumUnit> a, CMeasure<TValue, TValueConverter, TEnumUnit> b, CMeasure<TValue, TValueConverter, TEnumUnit> sum) // Der Constraint ist notwendig, damit der TValueConverter- Parameter als // 2. Parameter bei der Instanziirung CMeasure verwendet werden kann where TValueConverter : IValueConverter<TValue>, new() { sum.Set(alu.add(a.ToBaseUnit(), b.ToBaseUnit()), sum.BaseUnit()); } ... }
Über die Normierungsfunktion ToBaseUnit() werden die Maßzahlen a und b in die Basiseinheit umgerechnet. Eine Addition mit dem + Operator würde vom Compiler jedoch nicht akzeptitiert werden, da die Existenz einer solchen Operation nicht allgemein vorausgesetzt werden kann. Deshalb wird hier die generische Schnittstelle IALU<TValue> mitgeliefert, welche wie folgt definiert ist:
public interface IALU<TValue> { // Grundrechenarten TValue add(TValue a, TValue b); TValue sub(TValue a, TValue b); TValue mul(TValue a, TValue b); TValue div(TValue a, TValue b); // Kleiner als Relation bool lt(TValue a, TValue b); }
Für jede Grundrechenart auf TValue bietet IALU eine Methode an. Die Methode IALU.add wird in der generischen Methode genutzt, um die beiden TValue Werte miteinander zu addieren.
throw new Exception("Fehlermeldung");
try { // Programmcode, der Fehlerobjekte werfen kann } catch (FileNotFoundException ex) { // Behandeln einer Speziellen Ausnahme } catch (Expception ex) { // Behandeln einer allgemeinen Ausnahme } finally { // Anweisungen, die in jedem Fall ausgeführt werden }
try { try { throw new Exception("Ex Innen"); } catch (Exception ex) { Console.WriteLine(ex.Message); throw new Exception("Ex Catch"); } finally { Console.WriteLine("Finally 2"); } Console.WriteLine("Nach InnerTry"); } catch(Exception ex) { Console.WriteLine(ex.Message); } finally { Console.WriteLine("Finally 1"); }
Namespace: System.Delegat, System.MulticastDelegate
Delegates sind typisierte Funktionspointer. Durch sie wird es möglich, Einsprungpunkte von Methoden als Parameter einer Methode zu übergeben oder in einer Liste zu verwalten. Anwendung finden Delegates in Callback- Methoden bzw. zur Implementierung von Ereignissen.
Im .NET Framework sind Delegates nichts weiter als Objekte spezieller Klassen. Da jeder Delegate eine individuelle Parameterliste aufweisen kann, können die Delegates nicht alle Objekte einer gemeinsamen Klasse sein. Der C#- Kompiler generiert für jeden Delegate automatisch eine versigelte Klasse die von der Klasse System.Delegate abgeleitet ist. Diese Klassen haben folgenden Aufbau:
Jeder Delegate muß vor seiner Verwendung deklariert werden:
[Zugriffsmodifikator] delegate Methodendeklaration z.B.: public delegate void DGProgress(int count_dirs, int count_files);
Im folgenden Beispiel wird mittels des Delegaten DGProgress für die Methode scanDir der Klasse CDirTree ein Callback implementiert, durch welche der Aufrufer fortlaufend über den Arbeitsfortschritt informiert wird.
class CDirTree { public DGProgress CallBackProgress; public void scanDir(string path) { : // Anzeige des Verarbeitungsstandes if (icount_files % 100 == 0) if (CallBackProgress != null) CallBackProgress(icount_dirs, icount_files); : } }
Die Zuweisung einer Methode geschieht durch einen Konstruktor. Dieser Prozess wird auch als Registrierung bezeichnet:
class Class1 { // Callback für Anzeige des Forschrittes static void Progress(int anz_dirs, int anz_files) { Console.WriteLine("Progress: Dirs = {0}, Files= {1}", anz_dirs, anz_files); } static void Main() { CDirTree dt = new CDirTree(); // Callbackroutine registrieren: #methode Progress wird dem Delegaten zugewiesen dt.CallBackProgress = new DGProgress(Progress); dt.traverse("c:\\"); }
Ein Delegate kann auch eine Liste von Funktionspointern speichern. Wird der Delegate aufgerufen, dann werden alle für den Delegate registrierten Methoden nacheinander ausgeführt. Die Registrierung erfolgt in diesem Fall mittels des += Operators:
class Class1 { // Callback für Anzeige des Forschrittes static void Progress(int anz_dirs, int anz_files) { Console.WriteLine("Progress: Dirs = {0}, Files= {1}", anz_dirs, anz_files); } // Callback für Anzeige des Forschrittes static void Progress2(int anz_dirs, int anz_files) { Console.WriteLine("Progress2: Dirs = {0}, Files= {1}", anz_dirs, anz_files); } static void Main() { CDirTree dt = new CDirTree(); // Zwei Callbackroutine registrieren: dt.CallBackProgress += new DGProgress(Progress); dt.CallBackProgress += new DGProgress(Progress2); dt.traverse("c:\\"); }
Bisher wurde davon ausgegangen, daß einem Delegate eine gewöhnliche sog. benannte Methode zugewiesen wird. In Fällen, wo ein Stück ausführbarer Kode nur an einer Stelle im Programm zugewiesen werden muß, kann der Overhead einer kompletten Methodendeklaration durch sog. Anonyme Methoden eingespart werden.
Eine anonyme Methode wird wie folgt definiert:
delegate(Parameterliste) { /* Methodenrumpf */ }
Im folgenden Beispiel wird dem Ereignis Error der Klasse CLog zwei Ereignishandler als anonyme Methoden zugewiesen:
public partial class Form1 : Form { CLog log = new CLog(); public Form1() { InitializeComponent(); // Zuweisen von anonymen Methoden an das Ereignis Error // Hiedurch wird der Quelltext kompakter, da zwei explizite // Methodendeklarationen entfallen log.Error += delegate(string msg) { statusStrip1.Items["toolStripStatusLabel1"].Text = msg; }; log.Error += delegate(string msg) { MessageBox.Show(msg); }; } private void btnFireEvent_Click(object sender, EventArgs e) { log.LogError("Ein Ereignis"); } }
Auch die anonymen Methoden bringen noch einen syntaktischen "Overhead" mit sich wie z.B. das Schlüsselwort delegate und die Typdeklarationen in den Parameterlisten. Insbesondere die Typdeklarationen in den Parameterlisten kann der Comnpiler in vielen Fällen aus dem Kontext bestimmen, in dem die anonyme Methode aufgerufen wird. Mit der Einführung von Linq ab .NET 3.5, das den regen Einsatz anonymer Methoden nach sich zieht, wurden die .NET Compiler wie C# befähigt, mit vereinfachten Deklarationen anonymer Methoden umzugehen, die auf Typdeklarationen in den Parameterlisten verzichten. Diese vereinfachten Deklarationen werden als Lambda- Ausdrücke bezeichnet.
delegate int dgUnFunc(int a); delegate int dgBinFunc(int a, int b); // ... // Lambdaausdrücke dgBinFunc bf = (a, b) => { return a + b; }; int summe = bf(2, 3); bf = (a, b) => { return a * b; }; int produkt = bf(2, 3); // Vereinfachte Syntax bei Lambdaausdrücken mit einem Parameter dgUnFunc uf = (a) => { return -a; }; // nicht vereinfachte Form int negI = uf(9); uf = a => a/2; // vereinfachte Form int aHalbe = uf(9);
Delegates ermöglichen Methoden ausynchron auszuführen. Dabei wird ein Thread aus dem Threadpool genommen, un in diesem die Methode gestartet. Die geschieht durch die BeginInvoke Methode
delegate.BeginInvoke([Parameterliste der Methode], AsyncCallback DelegateDerCallBackMethode, object ParameterDerCallBackMethode)
BeginInvoke werden dabei die Parameter der asynchron aufzurufenden Methode, und zusätzlich ein Delegate einer Callback- Methode und deren Parameter übergeben. Die Callback- Methode wird aufgerufen, wenn die asynchron aufgerufene Methode endet. Sie ist vom Typ (Delegate) AsyncCallback:
delegate void AsyncCallback(IAsyncResult ar);
Über BeginInvoke kann der Programmierer an die Callback Methode einen Parameter übergeben (letzter Parameter). Dieser wird in einem Objekt vom Typ IAsyncResult im Feld AsyncState verpackt. Das Objekt vom Typ IAsyncResult wird schließlich an die CallBack- Methode übergeben.
Ereignisse sind Delegates für die folgende Einschränkung gilt:
Ereignisse sind Delegates, die nur von Methoden der Klasse aufgerufen werden, in welcher das Ereignis definiert ist.
public delegate void DGEnterDir(string path);
public class CLog { // Signatur von Eventhandlern für Fehler- und Nachrichtenmeldungen public delegate void DGLog(int no, string msg); // Ereignis, an das Abonnenten Routinen zur Protokollierung und Darstellung von // Fehlermeldungen binden können public event DGLog EventError; // Ereignis, an das Abonnenten Routinen zur Protokollierung und Darstellung von // allgemeinen Meldungen binden können public event DGLog EventMsg; }
public class CLog { // Signatur von Eventhandlern für Fehler- und Nachrichtenmeldungen public delegate void DGLog(int no, string msg); // Ereignis, an das Abonnenten Routinen zur Protokollierung und Darstellung von // Fehlermeldungen binden können public event DGLog EventError; // Ereignis, an das Abonnenten Routinen zur Protokollierung und Darstellung von // allgemeinen Meldungen binden können public event DGLog EventMsg; // Allgemeine Methode, über die Protokollierung und Darstellung von // Fehlermeldungen angestoßen wird public void LogError(int errno, string msg) { if (EventError != null) EventError(errno, msg); } // Allgemeine Methode, über die Protokollierung und Darstellung von // allg. Meldungen angestoßen wird public void LogMsg(int msgno, string msg) { if(EventMsg != null) EventMsg(msgno, msg); } }
public class CLog { // Signatur von Eventhandlern für Fehler- und Nachrichtenmeldungen public delegate void DGLog(int no, string msg); // Ereignis, an das Abonnenten Routinen zur Protokollierung und Darstellung von // Fehlermeldungen binden können public event DGLog EventError; // Ereignis, an das Abonnenten Routinen zur Protokollierung und Darstellung von // allgemeinen Meldungen binden können public event DGLog EventMsg; // Allgemeine Methode, über die Protokollierung und Darstellung von // Fehlermeldungen angestoßen wird public void LogError(int errno, string msg) { if (EventError != null) EventError(errno, msg); } // Allgemeine Methode, über die Protokollierung und Darstellung von // allg. Meldungen angestoßen wird public void LogMsg(int msgno, string msg) { if(EventMsg != null) EventMsg(msgno, msg); } // Die Eventhandler sind als Schnittstellenmember, // welche in einer Eventhandlerklasse implementiert werden. public void registerLogHnd(ILogHnd iLogHnd) { EventError += new CLog.DGLog(iLogHnd.OnError); EventMsg += new CLog.DGLog(iLogHnd.OnMsg); } }
Eine Klasse wie CLog kann in vielen Projekten eingesetzt werden. Wenn der Autor von CLog in einer neuen Version die Parameterliste der Events ändern möchte, dann wäre diese Version mit einer Vielzahl existierender Eventhandler nicht mehr nutzbar. Einen Ausweg bietet hier der Einsatz der Basisklasse
System.EventArgs { // Repräsentiert die Leere Parameterliste public static readonly EventArgs Empty; // Statischer Defaultkonstruktor: // Generiert die leere Eventarg- Liste public static EventArgs() { Empty = new EventArgs(); } // Defaultkonstruktor public EventArgs() { } }
Die Signatur der Events kann dann wie folgt abgeändert werden:
public class CLog { // Signatur von Eventhandlern für Fehler- und Nachrichtenmeldungen public delegate void DGLog(System.EventArgs eventArgs); // Ereignis, an das Abonnenten Routinen zur Protokollierung und Darstellung von // Fehlermeldungen binden können public event DGLog EventError; // Ereignis, an das Abonnenten Routinen zur Protokollierung und Darstellung von // allgemeinen Meldungen binden können public event DGLog EventMsg; }
Die Events übergeben Objekte an die Eventhandler, deren Klasse von der Klasse EventArgs abgeleitete sind:
public class CLog { // Signatur von Eventhandlern für Fehler- und Nachrichtenmeldungen public delegate void DGLog(System.EventArgs eventArgs); // Ereignis, an das Abonnenten Routinen zur Protokollierung und Darstellung von // Fehlermeldungen binden können public event DGLog EventError; // Ereignis, an das Abonnenten Routinen zur Protokollierung und Darstellung von // allgemeinen Meldungen binden können public event DGLog EventMsg; public class CLogEventArgs : System.EventArgs { CLogEventArgs(int errno, string msg) { this.No = errno; this.msg = msg; } public int No; public int msg; } // Allgemeine Methode, über die Protokollierung und Darstellung von // Fehlermeldungen angestoßen wird public void LogError(int errno, string msg) { if (EventError != null) EventError(new CLogEventArgs(errno, msg)); } // Allgemeine Methode, über die Protokollierung und Darstellung von // allg. Meldungen angestoßen wird public void LogMsg(int msgno, string msg) { if(EventMsg != null) EventMsg(new CLogEventArgs(errno, msg)); } }
In den Eventhandlern muß dann das empfangene EventArgs- Objekt in die Klasse CLogEventArgs downgecastet werden.