Inhaltsverzeichnis         

Objektorientierte Programmierung in C#

Grundlagen

Die objektorientierte Programmierung ist eine Erweiterung der klassischen, prozeduralen Programmierung um Entwurfsmethoden und Ausdrucksmittel, durch welche

  1. die Abbildung von Geschäftsprozessen auf Programmstrukturen vereinfacht,

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

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

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

  1. Bankkonto

  2. Fotoalbum

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

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

  1. Die Überladung von Operatoren kann nur für selbstdefinierte Typen erfolgen.

  2. Die Überladung muss innerhalb der Klasse des selbstdefinierten Typs erfolgen

  3. Die Überladung muss als statische Methoden deklariert werden.

  4. 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:

  1. Color, zur Darstellung der Linienfarbe

  2. style, zur Darstellung der Strichart

  3. unit, zur Darstellung der Einheit, in der gezeichnet wird

  4. Methode draw zum Zeichnen der Figur auf einem Ausgabemedium

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

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:

  1. Konvertierung in object und explizite Konvertierung in einen Schnittstellentyp sind möglich

  2. Operatorenn != und == sind nicht anwendbar, da keine allgemeine Gerantie für deren Existenz gegeben ist

  3. 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");
}