Objektorientierte Programmierung in C#
Grundlagen
Die objektorientierte Programmierung ist eine
Erweiterung der klassischen, prozeduralen Programmierung um
Entwurfsmethoden und Ausdrucksmittel, durch welche
die Abbildung von Geschäftsprozessen auf
Programmstrukturen vereinfacht,
und der Programmcode transparenter wird.
Von zentraler Bedeutung sind dabei Objekte:
Definition
|
Objekt
|
Ein Objekt steht für ein konkretes Ding (z.B. Der rote
Ferrari von Fred Vollgas oder die Tabelle 1 im
Arbeitsblatt Umsätze).
Objekte besitzen einen inneren Zustand. Z.B. hat der rote
Ferrari von Fred Vollgas eine Geschwindigkeit, und die Zelle $A1
in Tabelle 1 des Wert 99. Der Zugriff auf den inneren Zustand
erfolgt über Eigenschaften.
Objekte kann man beeinflussen, indem man ihnen Nachrichten
sendet. Z.B. kann man den roten Ferrari in einer Simulation
auffordern, die Geschwindigkeit auf 100 km/h zu reduzieren, oder
dem Tabellenblatt mitteilen, den Hintergrund der Zelle $A1 rot zu
färben.
Zustandsänderungen in Objekten können Ereignisse
auslösen. Schert vor dem Ferrari urplötzlich aus der
rechten Spur eine grüne Ente aus, die den linken
Fahrstreifen mit Tempo 100 km/h blockiert, dann wird der Ferrari-
Fahrer wütend, und hupt (= löst das Ereignis Hupen)
aus. Durch dieses Ereignis können wiederum eine Reihe von
Nachrichten erzeugt werden, die an die umgebenden Objekte
gesendet werden (z.B. Nachricht Hupsignal an die grüne Ente
gerichtet).
|
Graphische Darstellung von Objekten mittels
Objektdiagramm
Die objektorientierte Sichtweise erleichtert die
die notwendige Formalisierung von Aufgaben- Problemstellungen bei der
Softwareentwicklung, indem sie eine 1:1 Abbildung der Realität
auf den Entwurf ermöglicht. 1:1 bedeutet nicht, das alle Details
der Realität im Entwurf nachgebildet werden. Der Entwurf
abstrahiert nach wie vor von der Realität, indem er die
Abbildung auf die für die Aufgabenstellung wesentlichen
Eigenschaften und Prozesse einschränkt. Jedoch ist weniger
Abstraktion notwendig als bei der rein Prozeduralen Programmierung,
da diese zusätzlich erfordert, alle Objekte in Mengen aus Daten
und diese manipulierende Prozeduren aufzulösen.
Objektdiagramm
|
Objekt
|
alternativ:
Objekt
|
+- Eigenschaft
|
+- M: Methode(Param1, Param2)
|
+- C: Collection
|
+- E: Event
|
|
Klassifizierung von Objektmengen
Die Klassenbildung ist
eine Begriffsbildung aus der Mengenlehre, und bezeichnet dort die
Aufteilung einer Menge in disjunkte Teilmengen bezüglich eines
Äquivalenzkriteriums. Alle Elemente, die bezüglich des
Kriteriums äquivalent sind, bilden dabei eine Teilmenge, die
auch Klasse genannt wird.
So kann die Menge der
Politiker bezüglich des Äquivalenzkriteriums "gehört
zur gleichen Partei" in disjunkte Teilmengen aufgeteilt werden,
wobei jede Teilmenge eine politische Partei darstellt.
Definition
|
Klasse
|
Eine Klasse ist eine Menge von Objekten, die
einen gemeinsamen strukturellen Aufbau
aus Eigenschaften, Methoden und Ereignissen haben.
Z.B. Alle Objekte mit der Struktur { string
Name; string Strasse; string PLZ } könnten zur Klasse
Adressen gehören.
Aus Sicht der Geschäftslogik von der
gleichen Art sind.
Z.B. Kundenadresse hat
die gleiche Struktur wie Lieferantenadresse.
Jedoch sind aus Sicht der Geschäftslogik beides
verschiedene Mengen von Objekten.
|
Die Klassenbildung in
der objektorientierten Programmierung sollte in erster Linie durch
die Äquivalenzen zwischen Objekten in der Geschäftslogik
bestimmt sein (Beispiel: jeweils separate Klasse für
Lieferantenadressen und Kundenadressen).
Objekte, die aus Sicht
der Geschäftslogik von der gleichen Art sind, müssen
zusätzlich noch bei der Abstraktion auf den gleichen
strukturellen Aufbaus aus Eigenschaften, Methoden und Ereignissen
reduzierbar sein. Diese Einschränkung spielt bei der Deklaration
und Implementierung von Klassen in objektorientierten Spachen wie C#
eine große Rolle.
Klassendeklaration in C#
In C# wird eine Klasse durch eine
Klassendeklaration definiert.
Eine Klassendeklaration ist ein Block, der mit dem
Schlüsselwort class beginnt,
und dem der Name der Klasse folgt. Innerhalb eines class- Blockes
wird der gemeinsame strukturelle Aufbau aller Objekte definiert, die
zur Klasse gehören, indem aller Eigenschaften, Methoden
und Ereignisse einer Klasse deklariert und implementiert werden.
// class Klassenname : Basisklasse { ... }
class Auto {
// Konstruktor
Auto(string nameFahrer) {
_nameFahrer = name;
}
// Destruktor
~Auto() {
...
}
// Felder
private string _nameFahrer = "";
// Eigenschaftsdefinitionen
public Fahrer {
get{
return _nameFahrer;
}
}
// Eigenschaft mit implizit definierten Speicherfeld
public double EntfernungVonStuttgartInKm { get; set; }
// Methoden
public int fahren(double v, doubel dt) {
...
}
}
Member einer Klasse
Mebertyp
|
Beschreibung
|
Beispiel
|
Eigenschaften
|
Beschreiben die Schnittstellen zum inneren
Zustand eines Objektes. Es gibt nur lesbare -, nur beschreibbare -
und lese- schreib-bare Eigenschaften.
Objekt
Neuer Zustand → set → Zustand → get → aktueller Zustand
|
|
Felder
|
Dienen zur Implementierung des inneren
Zustandes
|
|
Methoden
|
Berechnen aus Eingaben und dem Inneren Zustand
einen neuen inneren Zustand. Berechnungsergebnisse können
zurückgegeben werden.
|
|
Lebenszyklus eines Objektes
Objekte durchlaufen in einem C# Programm einen
Lebenszyklus. Dabei werden bestimmte Methoden zu bestimmten
Zeitpunkten ausgeführt.
Konstruktor und Destruktor
Definition
|
Konstruktoren
|
Konstruktoren beauftragen die Speicherverwaltung, Speicherplatz
für ein neues Objekt im Speicher anzulegen. Im
Konstruktorrumpf kann anschließend die Initialisierung des
neuen Objektes programmiert werden. Wie beim Aufruf von
Methoden können Parameter über Parameterlisten
übergeben und bei der Initialisierung verarbeitet werden.
Das neu angelegte und initialisierte Objekt
wird vom Konstruktor zurückgegeben.
In C# ist ein Konstruktor Block ähnlich
einer Methode, der den Namen der Klasse trägt und im
Unterschied zu einer Methode keinen Rückgabetyp hat.
class Lager
{
// statische Memeberdeklaration mit Anweisung zur Initialisierung im statischen
// Konstruktor (= 0)
public static int instancecounter = 0;
// Memeberdeklaration mit Anweisung zur Initialisierung im Konstruktor (= 0)
public int Kapazität = 0;
// Defaultkonstruktor
public Lager() {
instancecounter ++;
}
// Konstruktor mit Parameterliste
public Lager(int pKapzität)
{
// Konstruktorrumpf
Kapazität = pKapazität;
instancecounter ++;
}
:
}
|
Destruktoren
Definition
|
Destruktoren
|
Spezielle Funktionen einer Klasse, die immer unmittelbar 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 - Pattern
Definition
|
Dispose- Pattern
|
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);
|
using Block
In C# kann der Instanzierung, Nutzung und
schließlich Aufruf der Methode Dispose durch den using-
Block in eine strukturiete Form überführt werden:
using (ClassA obj = new ClassA()) {
// Anweisungen
} // Automatischer Aufruf von Dispose
Aufgabe
Erweitern Sie die Klasse Auto um
Kontstruktor, Destruktor und Dispose- Methode. Testen Sie diese in
einem Kommandozeilenprogramm.
Beispiel: Stoppuhr- Objekte
Als Beispiel für eine Klassendeklaration und
Objekte diene die Klasse Stoppuhr.
Mit einem Stoppuhr- Objekt kann in einem Programm die Rechenzeit für
aufwendige Berechnungen gemessen werden.
Beispielanwendung:
void MeinEventhandler(double Zeitlimit, double verstricheneZeitInMs)
{
Debug.WriteLine("Zeitlimit: " + Zeitlimit.ToString("N1") +
", verstrichene Zeit: " + verstricheneZeitInMs.ToString("N1"));
}
[TestMethod]
public void TestMethod1()
{
StoppUhr meineStoppuhr = new StoppUhr();
// Zugriff auf den inneren Zustand durch schreiben in eine Eigenschaft
meineStoppuhr.ZeitInMsEigenschaft = 1000;
// Lesen des inneren Zustandes durch lesen einer eigenschaft
double gestoppteZeit = meineStoppuhr.ZeitInMsEigenschaft;
// Events austesten
meineStoppuhr.ZeitLimitInMs = 1000;
// Eventhandler registrieren
meineStoppuhr.ZeitlimitUeberschrittenEvent += MeinEventhandler;
// wg. dem Schlüsselwort event kann der Delegate nicht
// mehr über die Objektinstanz direkt aufgeufen werden
//meineStoppuhr.ZeitlimitUeberschrittenEvent(1, 2);
meineStoppuhr.Start();
System.Threading.Thread.Sleep(2000);
meineStoppuhr.Stopp();
}
Innerer Zustand
Objekte können wie Variablen Informationen
speichern. Dieser Objekt- interne Informationsspeicher wird als
Innerer Zustand bezeichnet.
Implementiert
wird der innere Zustand durch Variablen, auch Felder genannt:
public class StoppUhr
{
// Innerer Zustand: hier werden Informationen im Objekt gespeichert
long _TicksBeimStart;
long _TicksBeimStopp;
}
Methoden
Methoden sind für
den externen Zugriff freigegebene Prozeduren (Sub's) oder Funktionen
(Function's). Aus der Perspektive der Objektorientierung sind
Methoden Nachrichten, die an ein Objekt gesendet werden. Diese
Nachrichten beeinflussen den inneren Zustand eines Objektes.
In einer etwas praktischeren Sichtweise können
wir Methoden als Operationen betrachten, die abhängig vom
inneren Zustand des Objektes sind.
In einem Klassenblock werden Methoden als
öffentliche Funktionen oder Unterprogramme definiert:
public class StoppUhr
{
// Innerer Zustand: hier werden Informationen im Objekt gespeichert
long _TicksBeimStart;
long _TicksBeimStopp;
// Methoden oder Nachrichten, die ein Objekt empfangen kann,
// und auf die es reagiert
/// <summary>
/// Zeitmessvorgang starten
/// </summary>
public void Start()
{
Reset();
}
/// <summary>
/// ... beenden
/// </summary>
public void Stopp()
{
_TicksBeimStopp = DateTime.Now.Ticks;
…
}
public void Reset()
{
_TicksBeimStart = DateTime.Now.Ticks;
_TicksBeimStopp = _TicksBeimStart;
}
// Hier werden Nachrichten eingesetzt, um den inneren Zustand abzurufen
public double ZeitInMs()
{
return new TimeSpan(_TicksBeimStopp - _TicksBeimStart).TotalMilliseconds;
}
}
Eigenschaften
Eigenschaften sind die Schnittstellen zum inneren
Zustand eines Objektes. Sie werden in VB.NET durch benannte Paare von
Methoden implementiert, die in einem Property-
Block eingeschlossen sind. Sie ermöglichen die
Manipulation des inneren Zustandes.
<Datentyp> <Name der Eigenschaft>
{
// Der Getter
get {
return <Ausdruck, der lesend auf inneren Zustand zugreift>;
}
// Der Setter
set {
// Schreibender Zugriff auf inneren Zustand. Dabei kann über das
// Schlüsselwort value auf den der Eigenschaft neu zugewiesenen Wert
// zugegriffen werden
}
}
Im allgemeinen ist der direkte
Zugriff auf die Felder verboten, welche den inneren Zustand eines
Objektes implementieren. In C# kann man jedoch Felder Public
deklarieren, wodurch der
direkten Zugriff auf den inneren Zustand möglich wird. Das
verletzt die Prinzipien der objektorientierten Programmierung und
kann als Relikt aus der "Computersteinzeit" betrachtet
werden !
Eine Methode des Paars wird Setter genannt.
Sie wird mit dem Schlüsselwort set markiert
und ermöglicht einen Schreibzugriff auf den inneren Zustand:
// Der Setter
set {
// Zugriff auf inneren Zustand
}
Aufgerufen wird der Setter, wenn
man der Eigenschaft einen neuen Wert zuweist:
Objekt_X.MyProp =
Neuer Wert;
Die andere Methode
ist der Getter. Er ermöglicht einen Lesezugriff auf den
inneren Zustand:
// Der Getter
get {
return <Ausdruck, der auf inneren Zustand zugreift>;
}
Aufgerufen wird der
Getter, wenn man den Wert aus der Eigenschaft ausliest:
int meineVariable = Objekt_X.MyProp;
Beispiele für Eigenschften in der Stoppuhrklasse:
public class StoppUhr
{
// Innerer Zustand speichert ein Zeitlimit
long _Zeitlimit_in_Ticks;
// Alternativer Zugriff auf den inneren Zustand über Eigenschaften
public double ZeitLimitInMs
{
// Getter dient zum Abruf der Informationen einer Eigenschaft
get
{
return _Zeitlimit_in_Ticks * 1e-4;
}
// Setter dient zum Setzen neuer Informationen in einer Eigenschaft
// Compiler wandelt set{ ... } um in void set(double value) {...}
set
{
_Zeitlimit_in_Ticks = (long)(value * 1e4);
}
}
}
Ereignisse
Wenn ein Objekt einen
besonderen Zustand annimmt, kann es abhängig von der
Aufgabenstellung erforderlich sein, andere Objekte darüber zu
informieren. Diese Situation wird Ereignis genannt.
Die Stoppuhr kann über die Eigenschaft
ZeitlimitInMs so konfiguriert
werden, dass sie ein ein Ereignis "feuert", wenn
eine Einstellbare Zeitspanne in der Messung überschritten wurde.
Das Feuern erfolgt mit RaiseEvent in
der Stopp- Methode:
public void Stopp()
{
_TicksBeimStopp = DateTime.Now.Ticks;
if (ZeitInMsEigenschaft >= ZeitLimitInMs)
{
// Ereignis feuern, was alle Benachrichtigt in der Umgebung, dass das
// Zeitlimit gerissen wurde
if (ZeitlimitUeberschrittenEvent != null)
{
//Eventhandler wurden registriert-> Event wird gefeuert
ZeitlimitUeberschrittenEvent(ZeitLimitInMs, ZeitInMsEigenschaft);
}
}
}
Ein "Ereignis feuern" kann man sich wie einen
Methodenaufruf vorstellen. Jedoch ist die Methode nicht fest beim
Implementieren der Stoppuhr definiert worden. Stattdessen kann die
Methode nachträglich an das Ereignis "gebunden" werden
mit dem AddHandler Befehl.
Man nennt die an das Ereignis gebundenen Methoden auch Eventhandler.
void MeinEventhandler(double Zeitlimit, double verstricheneZeitInMs)
{
Debug.WriteLine("Zeitlimit: " + Zeitlimit.ToString("N1")
+ ", verstrichene Zeit: "
+ verstricheneZeitInMs.ToString("N1"));
}
[TestMethod]
public void TestMethod1()
{
StoppUhr meineStoppuhr = new StoppUhr();
// Zugriff auf den inneren Zustand durch schreiben in eine Eigenschaft
meineStoppuhr.ZeitInMsEigenschaft = 1000;
// Lesen des inneren Zustandes durch lesen einer eigenschaft
double gestoppteZeit = meineStoppuhr.ZeitInMsEigenschaft;
// Events austesten
meineStoppuhr.ZeitLimitInMs = 1000;
// Eventhandler registrieren
meineStoppuhr.ZeitlimitUeberschrittenEvent += MeinEventhandler;
// wg. dem Schlüsselwort event kann der Delegate nicht
// mehr über die Objektinstanz direkt aufgeufen werden
//meineStoppuhr.ZeitlimitUeberschrittenEvent(1, 2);
meineStoppuhr.Start();
System.Threading.Thread.Sleep(2000);
meineStoppuhr.Stopp();
// Eventhandler wieder abkoppeln
meineStoppuhr.ZeitlimitUeberschrittenEvent -= MeinEventhandler;
}
Aus technischer Sicht ist ein Ereignis ein Speicherort, an dem andere
Objekte Einsprungadressen von Methoden (Eventhandler) hinterlegen,
die starten sollen, wenn das Ereignis eintritt bzw. "feuert".
Primzahlscanner objektorientiert implementieren
Teilaufgabe 1 wird durch Zahlenobjekte gelöst,
die eine IstPrimzahl- Methode besitzen. Wir senden bildlich
gesprochen dem Zahlenobjekt eine Anfrage, ob es eine Primzahl ist
oder nicht, indem wir seine IstPrimzahl- Methode aufrufen. Gibt diese
true zurück, dann ist das Zahlenobjekt eine Primzahl, sonst
nicht.
Teilaufgabe
2 wird durch zwei Objekte gelöst. Das eine (Zahlenbereich)
erzeugt für vorgegebene Grenzen eine Liste aus Zahlenobjekten,
die alle Zahlen innerhalb der Grenzen als Zahlenobjekte umfasst.
Das andere (Primzahlscanner) filtert die Liste,
indem es jeden Eintrag über die IstPrimzahl- Methode befragt, ob
es eine Primzahl ist. Objekte von Primzahlen werden in die
Ergebnisliste kopiert
Aufgaben:
Beschreiben Sie folgende Systeme durch
Objektmengen. Stellen Sie die Objekte durch Objektdiagramme dar.
Bankkonto
Fotoalbum
Vektorrechnung
Klassendeklarationen - Details
Statische Member
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;
Statische Klassen (NET 2.0)
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
Partielle Klassen (NET 2.0)
Die Deklaration einer Klasse kann auf mehrere
Quelltextdateien verteilt werden. In jeder Quelltextdatei wird die
Klasse mit dem zusätzlichen Schlüsselwort partial
deklariert.
Konstruktion/Destruktion - Details
Statische Konstruktoren
Um statische Klassenkomponenten zu initialisieren
gibt es spezielle Konstruktoren, genannt statische Konstruktoren.
Diese werden beim erstmaligen Zugriff auf eine Klasse aufgerufen:
class C {
public static int anz_instanzen;
static C() {
anz_instanzen = 1;
}
}
Objektinitialisierer (ab .NET 3.5)
Das Implementieren von Konstruktoren, die alle
möglichen Einsatzszenarien abdecken, ist häufig
zeitraubende Routinearbeit. Ab .NET 3.5 bieten sich als bequeme
Alternative der Objekt- Initialisierer 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 };
Anonyme Typen (ab .NET 3.5)
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
Konstanten und Readonly- Member
Konstanten sind Felder, deren Wert zur
Übersetzungszeit definiert wird, und die anschließend nur
über den Klassennamen referenzierbar sind.
const double 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
}
:
}
Ereignisse - Details
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.
Ereignishandler durch Delegates typisieren
public delegate void DGEnterDir(string path);
Ereignisse in der Klasse deklarieren
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;
}
Ereignisse auslösen
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);
}
}
Ereignis behandeln
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);
}
}
EventArgs- einheitliche Verpackung für Eventhandlerargumente
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 muss dann das empfangene EventArgs- Objekt in
die Klasse CLogEventArgs downgecastet werden.
Operator overloading
Eine Besonderheit von C# innerhalb der .NET
Sprachen ist die Operatorüberladung. Damit kann den bekannten
Operationen eine neue Bedeutung gegeben werden.
Im folgenden Beispiel wird eine Addition auf dem
selbstdefinierten Typ Romzahl ausgeführt, indem das gewohnte +
Symbol benutzt wird. Dank Operatorüberladung ist diese intuitive
Darstellung möglich.
[TestMethod]
public void OperatorüberladungTest()
{
var a = new Romzahl("MDCLXVI");
var b = new Romzahl("XIII");
// Explizite Wandlung mit Konvertierungsoperator
long MDCLXVI = (long)a;
Assert.AreEqual(MDCLXVI, 1666);
// Aufruf des selbstdefinierten Additionsoperators für Romzahlen
var RomSumme = a + b;
long LongSumme = (long)RomSumme;
Assert.AreEqual(LongSumme, 1679);
}
Die Anwendung des + Operators auf Romzahlen setzt allerding voraus,
dass in der Typdefinition für Romzahlen eine Operatorüberladung
erfolgt. Siehe wie folgt:
namespace Sprachkonzepte.Operatorüberladung
{
/// <summary>
/// Klasse zum Darstellen und Rechnen mit römischen Zahlen. Es wird bei der
/// Implementierung auf Funktionen aus der Bibliothek mko.Algo zurückgegriffen.
/// </summary>
public class Romzahl
{
public Romzahl(string Rom)
{
Value = Rom;
}
public Romzahl(long intRom)
{
Value = mko.Algo.Zahlentheorie.Zahlensysteme.ConvertToRom(intRom);
}
public string Value { get; set; }
public override string ToString()
{
return Value;
}
/// <summary>
/// Konvertierungsoperator: berechnet den Wert, der durch eine Romzahl dargestellt
/// wird, und gibt ihn als long zurück
/// </summary>
/// <param name="a"></param>
/// <returns></returns>
public static explicit operator long(Romzahl a) {
return mko.Algo.Zahlentheorie.Zahlensysteme.ConvertToInt(a.Value);
}
/// <summary>
/// Additionsoperator: Wandelt zunächst beide Summanden (Romzahlen) in longs um,
/// summiert diese und wandelt die Summe in eine Romzahl zurück
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static Romzahl operator +(Romzahl a, Romzahl b)
{
return new Romzahl((long)a + (long)b);
}
}
}
Voraussetzungen
Die Überladung von Operatoren kann nur
für selbstdefinierte Typen erfolgen.
Die Überladung muss innerhalb der Klasse
des selbstdefinierten Typs erfolgen
Die Überladung muss als statische
Methoden deklariert werden.
Die Parameterliste der Überladung muss
mindestens einen Parameter enthalten, der vom selbstdefinierten Typ
ist.
Syntax der Überladung
// Operator- Overloading (unäre Operatoren)
public static <ResultType> operator <OpSymbol> (Operand1)
// Operator- Overloading (binäre Operatoren)
public static <ResultType> operator <OpSymbol> (Operand1, Operand2)
Überladbare Operatoren
Nicht alle Operatoren sind überladbar.
Beispielsweise sind [] (siehe Indexer), /=, &&, || sind nicht
überladbar.
Folgende unäre Operatoren sind überladbar
Symbol
|
Bedeutung
|
Überladung
|
+
|
Positives Vorzeichen
|
static <Typ> operator +(<Typ>
value)
|
-
|
Negatives Vorzeichen
|
static <Typ> operator +(<Typ>
value)
|
!
|
Logische Negation
|
static <Typ> operator !(<Typ>
value)
|
~
|
bitweises Komplement
|
static <Typ> operator !(<Typ>
value)
|
++
|
Inkrement
|
static <Typ> operator ++(<Typ>
value)
|
- -
|
Dekrement
|
static <Typ> operator –(<Typ>
value)
|
true
|
Definiert eine Bedingung für die
Umwandlung in den Wahrheitswert true
class GeradeZal{
public Value { get; set;}
public ststic bool operator true(GeradeZahl z)
{
return Value % 2 == 0;
}
}
…
var z1 = new GeradeZahl() {Value = 28};
if(z1)
Debug.WriteLine("z1 ist eine gerade Zahl")
|
static bool operator true(<Typ> value)
|
false
|
Definiert eine Bedingung für die
Umwandlung in den Wahrheitswert true
|
static bool operator true(<Typ> value)
|
Folgende binäre Operatoren sind überladbar:
Symbol
|
Bedeutung
|
Überladung (Beispiel)
|
+, -, *, /, %
|
Addition, Subtraktion, Multiplikation,
Division, Modulo (Rest einer ganzzahligen Division)
|
static Romzahl operator +(Romzahl a, Romzahl b)
|
!=, ==, <, <=, >, >=
|
ungleich, gleich, kleiner, kleiner gleich,
größer, größer gleich
|
static bool operator ==(Romzahl a, Romzahl b)
|
&, !, ^
|
Logik: And, Or, Xor
|
static Fuzzy operator !(Fuzzy a, Fuzzy b)
|
<<, >>
|
Verschiebung, Links rechts
|
static Head operator <<(Tape backup, int
shiftleft)
|
Indexer
Indexer entsprechen der Operatorüberladung
von [] in C++. Sie sind stets an Instanzen gebunden, können also
nie statisch deklariert werden.
public <ResultType> this[<indexType> i]
Selbstdefinierte Konvertierungsoperatoren
Für selbsdefinierte Typen können
spezielle Konvertierungsoperatoren implementiert werden:
class CMy {
public int x;
// Deklaration eines impliziten Konvertierungsoperators
public static implicit operator int(CMy obj) {
return obj.x;
}
// Deklaration eines expliziten Konvertierungsoperators
public static explicit operator string(CMy obj) {
return obj.x.ToString();
}
}
:
// Einsatz
CMy mx = new CMy();
my.x = 99;
int i = my;
string txt_i = (string) my;
Vererbung
Beim Einführen in die Klassenbildung der
objektorientierten Programmierung wurde die Klassen Kundenadresse
und Lieferantenadresse
als Bespiele für aus Sicht der Geschäftslogik disjunkte
Klassen genannt. Dem Leser blieb dabei das Gefühl, zwei
konzeptionell sehr ähnliche Mengen komplett unabhängig
voneinander entwickeln zu müssen.
Abstraktion durch Oberbegriffe
Mit den Vererbungstechniken in C# können z.B.
Lieferantenadresse und
Kundenadresse zu
einer Klasse Adresse verallgemeinert
werden, die die semantischen und strukturellen Gemeinsamkeiten
ausdrückt, und so die Bildung
von Oberbegriffen in C# ermöglicht.
Lohn der
Vererbung: heterogene Objektmengen verwalten
Indem übergeordnete, abstraktere Klassen (die
Oberbegriffe) gebildet werden, gelingt die gemeinsame Verwaltung von
Objekten unterschiedlichen Typs.
Eine CAD- Zeichnung ist eine z.B. Menge aus
Linien, Kreisen etc. Diese kann durch eine Liste vom Typ Figur
(Oberbegriff von Linien und Kreisen) implementiert werden.
Da Kreise und Linien allgemein Figuren sind,
gelingt das Einfügen dieser unterschiedlichen Typen in die Liste
vom einheitlichen Typ Figur.
Beim Plotten der Zeichnung wird die Liste
durchlaufen, und von jedem Objekt die verallgemeinerte draw-
Methode der Klasse Figur aufgerufen. Die Vererbungsmechanismen
von C# sorgen dafür, dass beim Aufruf vom allgemeinen draw(...)
eine Weiterleitung an die speziellen draw()- Methoden des Linien-
oder Kreis- Objektes erfolgt. So werden Linien als Linien, und ein
Kreise als Kreise trotz der Abstraktion geplottet.
Definition
von Vererbungsbeziehungen
Beispiel: Entwickeln einer Klassenbibliothek zur
Bearbeitung von technischen 2D- Zeichnungen
Technische 2D Zeichnungen 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:
Klassen der Grafik-primitive
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
gemeinsamen 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:
Klassenhierarchie CFigur
Die
Deklarationen 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 Implementierung dieser notwendig.
Die Vererbung wird in der abgeleiteten Klasse wie
folgt deklariert:
class CLinie : CFigur {
:
}
Auflösen von Namenskonflikten bei der Vererbung
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:
Überladen von Eigenschaften und Methoden
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)
Das Kapselungsprinzip
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.
Zugriffsmodifikatoren für die Kapselung
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
|
Default- Zugriffsmodifikator
Die Angabe des Zugriffsmodifizierers ist optional.
Wird kein Zugriffsmodifizierer angegeben, dann gelten folgende
Voreinstellungen:
Klasse
|
internal
|
Klassenmitglied
|
private
|
Beispiel: Kapselung
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
Polymorphismus
Definition
|
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.
Beispiel
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
Merkmale
Unterschied Schnittstelle/Abstrakte Klasse
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.
Deklaration einer Schnittstelle
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;
}
Implizite und explizite Implementierung einer Schnittstelle
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(....);
Generische Programmierung (NET 2.0)
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.
Vorteile der generischen Programmierung
hochgradig wiederverwendbarer Code durch das
hohe Abstraktionsniveau der datentypunabhängigen Definitionen
von Algorithmen und Klassen auf der einen-, und die einfache
Instanziierung konkreter 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 und Typparameterlisten
Typparameter sind Platzhalter für Datentypen
in Klassen-, Schnittstellen-, Methoden 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
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;
}
Instanziierung
Aus dem generischen Typen CPoint<T> könne
zur Entwurfszeit konkrete 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;
Generische Schnittstellen
Analog den Klassen können auch generische
Schnittstellen deklariert werden. Das Resultat sind
Schnittstellenengen mit einheitlichem Aufbau.
Im Beispiel wird eine generische Schnittstelle
deklariert, welche Konvertierungsfunktionen in einen parametrierbaren
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);
}
Instanziierung
Die Instanziierung kann für eine Klasse
erfolgen, welche die Schnittstelle implementiert. Bei expliziter
Implementierung der Schnittstellen- Member 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
}
Einschränkungen (Constraints)
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:
Konvertierung
in object und explizite Konvertierung in einen
Schnittstellentyp sind möglich
Operatorenn !=
und == sind nicht anwendbar, da keine allgemeine Gerantie für
deren Existenz gegeben ist
Vergleiche mit
null sind möglich. Wird der Typparameter durch einen
Wertetyp instanziiert, dann liefert der Vergleich mit null immer
false
|
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 muss. Die
geschieht durch einen Constraint mit folgender Syntax:
where T : Merkmal [, Merkmal ...]
Der Besitz eines Merkmals stellt eine Einschränkung bei der
Auswahl der Typen dar, mit der eine Typparameterliste instanziiert
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
|
Generische Typen und Klassenhirarchien
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
dieser Umrechnungsmethoden 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 trotzdem eine nahezu einheitliche
Struktur für alle Varianten von Maßzahlen zu erreichen,
könnte folgende Familie von Klassenhierarchien 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;
}
}
Generische Methoden
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.
Fehlerbehandlung
Fehlerobjekt auswerfen
throw new Exception("Fehlermeldung");
Fehler behandeln
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
}
Hierarchie der Ausnahmen
Beispiel
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");
}