Startseite  Index  <<  >>  

5 - Objektorientierte Sprachgrundlagen

Nachdem Sie nun die allgemeinen Sprachgrundlagen von C# kennen gelernt haben, werden im nächsten Schritt die "Mechanismen" vorgestellt, wie Sie Anweisungen objektorientiert verpacken und gruppieren. Dazu werden in diesem Kapitel die objektorientierten Sprachgrundlagen, d.h. die Deklaration von Klassen und Methoden sowie das Erzeugen von Objekten, behandelt. Außerdem wird die Verwendung von partiellen Typen und Strukturen vorgestellt.

ACHTUNG

Dieses Kapitel stellt keine allgemeine Einführung in die objektorientierte Programmierung dar. Dazu gibt es spezielle Literatur, die häufig sogar unabhängig von einer konkreten Programmiersprache ist. Wie eine Anwendung optimal in Klassen, Methoden, Namespaces etc. aufgeteilt wird, ist einerseits mit viel Erfahrung verbunden und andererseits von dem zukünftigen Aufgabenbereich der Anwendung abhängig.

5.1 Klassen und Objekte

5.1.1 Klassen

Der Grundbaustein einer .NET-Anwendung sowie einer Klassenbibliothek sind Klassen. Sehr einfache Anwendungen (z.B. die Beispiele in diesem Buch) bestehen oft nur aus einer einzigen Klasse, größere Anwendungen dagegen aus mehreren Hundert oder Tausend Klassen. Eine Klasse besteht wiederum aus Datenelementen, z.B. Variablen (auch Felder oder Fields genannt) und aus Methoden. Die Methoden sind die ausführende Einheit, da sie die Anweisungen und damit die Programmlogik enthalten. Anweisungen befinden sich immer innerhalb einer Methode. Methoden befinden sich immer innerhalb einer Klasse. Methoden, die sich außerhalb einer Klasse befinden, wie z.B. von C/C++ oder Pascal/Delphi bekannt, gibt es in C# nicht.

Innerhalb einer Klasse lassen sich verschiedene Bereiche definieren, die z.B. privat oder öffentlich sein können (Abbildung 5.1). Private Bereiche (der obere Bereich in der Abbildung) lassen sich nur innerhalb der Klassendefinition verwenden. In den privaten Bereichen werden in der Regel Hilfsroutinen untergebracht, die Algorithmen oder Teilfunktionalitäten enthalten, die später in den öffentlichen Methoden verwendet werden. Öffentliche Bereiche (der untere Bereich) dienen als Schnittstelle. Sie bestehen meist nur aus Methoden und Eigenschaften. Auf sie kann auch außerhalb der Klassendeklaration zugegriffen werden, z.B. über ein Objekt der Klasse.

Aufbau einer Klasse mit privatem und öffentlichem Teil

Abbildung 5.1: Aufbau einer Klasse mit privatem und öffentlichem Teil

Wozu dienen nun Klassen? Angenommen Sie möchten eine Anwendung entwickeln, die zur Verwaltung Ihrer DVD-Sammlung dient. Jede DVD besitzt bestimmte Merkmale wie einen Titel, einen Preis, das Erscheinungsjahr und eine Kategorie. Diese Merkmale einer DVD können durch jeweils eine Variable repräsentiert werden. Weiterhin kann eine DVD ausgeliehen (durch einen Bekannten, der sie dann 3 Jahre später beim Umzug wiederfindet) oder verkauft werden. Diese Operationen werden durch Methoden implementiert. Eine Klasse definiert nun einen Bauplan für eine konkrete DVD mitsamt den darauf ausführbaren Operationen (der Schnittstelle).

Kapselung

Ein Grundprinzip der objektorientierten Programmierung ist die Kapselung. Damit ist gemeint, dass eine Klasse Datenelemente in Form von privaten Variablen enthält, auf die nur über öffentliche Schnittstellen, also Methoden zugegriffen werden kann. Wozu dieser Umweg? Beim direkten Zugriff auf eine Variable kann der Wert geändert werden, ohne dass es jemand mitbekommt und etwas dagegen tun kann. Die Variable könnte von Hunderten verschiedenen Stellen aus bearbeitet werden. Enthält die Variable dann einen ungültigen Wert (z.B. im Falle der DVD ein Erscheinungsjahr von 1960), lässt sich schwer prüfen, wodurch dies zustande kam. Ist dagegen der Zugriff auf eine Variable nur über eine Methode möglich, kann innerhalb dieser einen Methode der neue Wert überprüft und gegebenenfalls andere Programmelemente darüber informiert werden, dass sich ein Wert geändert hat. Unabhängig von der Anzahl der Stellen in einer Anwendung, welche den Wert der Variablen verändern wollen, ist immer der Aufruf der betreffenden Methode notwendig.

INFO

Die Kapselung sollte eigentlich auch die Sicherstellung des Geheimnisprinzips ermöglichen, d.h. man möchte den internen Aufbau einer Klasse vor deren Anwender verstecken. Dies ist unter .NET aber leider nicht möglich, da sich mittels Reflection (vgl. dazu später in diesem Buch) alle Informationen über den Aufbau einer Klasse ermitteln lassen. Dennoch trägt die Kapselung durch die Bereitstellung der öffentlichen Schnittstelle dazu bei, Programmierfehler zu vermeiden, diese beim Auftreten schneller zu finden und ein besseres Klassendesign zu schaffen.

5.1.2 Objekte

Da eine Klasse nur ein Bauplan ist, müssen irgendwie die konkreten Teile, z.B. die DVDs, erzeugt werden. Diese Teile heißen in der objektorientierten Programmierung Objekte. Man sagt auch, ein Objekt ist eine Instanz einer Klasse. Dabei besitzt jedes Objekt individuelle Eigenschaften bzw. einen internen Zustand, der sich aus den aktuellen Werten seiner Datenelemente zusammensetzt. Weiterhin besitzt jedes Objekt noch eine individuelle Identität, die z.B. durch seine Lage im Hauptspeicher festgelegt wird und es von anderen Objekten unterscheidet - auch wenn die Werte der Datenelemente zweier Objekte gleich sein können. Die Methoden sind jedoch für alle Objekte einer Klasse immer dieselben. Lediglich die dabei verwendeten Daten unterscheiden sich.

Beispiel: Mit Klassen und Objekte arbeiten

Die Klasse DVD besitzt zwei private Variablen und zwei öffentliche Methoden. Von der Klasse DVD werden nun zwei konkrete Objekte erzeugt. Jedes Objekt besitzt dabei individuelle Werte in den beiden Eigenschaften Erscheinungsjahr und Titel.

Objekterzeugung

Abbildung 5.2: Objekterzeugung

5.1.3 Klassen deklarieren

In einer C#-Datei können Sie eine oder auch mehrere Klassen deklarieren. Aus Gründen der Übersichtlichkeit wird meistens in einer Datei nur jeweils eine Klasse deklariert.

Beispiel: Eine einfache Klassendeklaration

Die Klasse DVD enthält die beiden Variablen Titelund Erscheinungsjahr sowie die Methode Ausleihen(). Allerdings enthält die Methode noch keine Funktionalität und auf die beiden Variablen kann nur innerhalb der Klasse zugegriffen werden.

public class DVD
{
  private string Titel;
  private int Erscheinungsjahr;
  public void Ausleihen(string name)
  {
    ...
  }
}
Listing 5.1: DVDSammlung.cs

Syntax einer Klassendeklaration

  • Eine Klasse wird über das Schlüsselwort class deklariert.
  • Optional kann eine Klasse mit den Zugriffsmodifizierern public oder internal versehen werden. Diese werden noch vor das Schlüsselwort class geschrieben. Geben Sie keinen Modifizierer an, ist internal die Standardeinstellung.
  • Nach dem Schlüsselwort class folgt der Name der Klasse, der ein gültiger Bezeichner sein muss.
  • In geschweiften Klammern wird der Inhalt der Klasse eingeschlossen. Eine Klasse kann unter anderem Variablen, Konstanten und Methoden beinhalten. Geben Sie keinen Zugriffsmodifizierer an, sind die Klassenelemente standardmäßig als private deklariert, d.h., auf sie kann nur innerhalb der Klassendeklaration zugegriffen werden.

    Instanzvariablen

    Variablen innerhalb einer Klasse werden auch als Instanzvariablen bezeichnet. Damit wird kenntlich gemacht, dass jedes Objekt der Klasse eine eigene Instanz der Variablen mit einem individuellen Wert besitzt. Instanzvariablen müssen nicht initialisiert werden. Sie erhalten in diesem Fall einen Standardwert, z.B. 0 für int-Typen. Eine Liste aller Standardwerte können Sie in der Hilfe unter Default Values Table einsehen. Häufig wird statt von Instanzvariablen auch von Feldern gesprochen (engl. fields). Da dies zu Verwirrung mit dem Begriff "Feld" im Zusammenhang mit Arrays führen kann, wird im Folgenden immer von Instanzvariablen gesprochen.

    Schreibgeschützte Variablen

    Instanzvariablen sollten eigentlich im privaten Teil einer Klasse deklariert werden. Der Zugriff erfolgt dann über Methoden oder Eigenschaften. Man kann aber Variablen auch im öffentlichen Teil einer Klasse deklarieren. Wenn deren Werte nur gelesen werden sollen, können diese über das Schlüsselwort readonly als schreibgeschützt deklariert werden. Jetzt kann die Variable nur noch bei ihrer Deklaration und in einem Konstruktor der Klasse initialisiert (geschrieben) werden. Selbst in einer anderen Methode der Klasse kann auf eine solche Variable nicht mehr schreibend zugegriffen werden. Der einzige Unterschied zu einer Konstanten besteht darin, dass eine als readonly markierte Variable auch im Konstruktor initialisiert werden kann. Durch die Verwendung mehrerer Konstruktoren kann sie dadurch unterschiedliche Werte erhalten.

    Beispiel: Variablen schreibgeschützt deklarieren

    Die beiden Variablen Computername und Benutzername werden im Konstruktor der Klasse Systeminfo initialisiert. Auf die Variablen kann aber von außen (über Instanzen der Klasse) nur lesend zugegriffen werden.

    public class SystemInfo
    {
      public readonly string Computername;
      public readonly string Benutzername;
      public void SystemInfo()
      {
        Computername = "MeinPC";
        Benutzername = "Nelly";
      }
    }

    Zugriffsmodifizierer

    Über Zugriffsmodifizierer steuern Sie, wer Typen, Methoden oder Variablen verwenden, d.h. darauf zugreifen, kann. So können Sie z.B. verhindern, dass ein Benutzer Ihrer Klasse, z.B. ein weiterer Programmierer, direkt auf eine Variable zugreifen kann. Die folgende Tabelle listet alle möglichen Zugriffsmodifizierer auf, auch wenn diese momentan noch nicht von Bedeutung sind. Sie dürfen einige davon nur in einem bestimmten Kontext verwenden. Dies wird in den betreffenden Abschnitten erläutert.
    ZugriffsmodiBeschreibung
    publicAuf diese Member kann innerhalb des Typs, von abgeleiteten Typen und von Objekten des Typs aus zugegriffen werden.
    protectedAuf diese Member kann innerhalb des Typs und von abgeleiteten Typen aus zugegriffen werden. Objekte können niemals auf protected-Member zugreifen.
    privateAuf diese Member kann nur innerhalb der Typdefinition (auch von verschachtelten Typen aus) zugegriffen werden.
    protected internalAuf diese Member kann innerhalb des Typs und von abgeleiteten Typen aus zugegriffen werden. Außerdem kann von allen Typdefinitionen der Assembly darauf zugegriffen werden, in welcher der Typ deklariert wurde.
    internalEs kann von allen Typdefinitionen innerhalb der jeweiligen Assembly darauf zugegriffen werden.
    Tabelle 5.1: Zulässige Zugriffsmodifizierer

    5.2 Objekte erzeugen

    Nachdem Sie nun schon einfache Baupläne, d.h. Klassen, deklarieren können, besteht die nächste Aufgabe darin, konkrete Objekte von einem bestimmten Typ zu erzeugen. Dazu wird in C# der Operator new verwendet. Bei seinem Aufruf stellt er zuerst Speicherplatz für ein Objekt des verwendeten Typs bereit, initialisiert ihn und gibt eine Referenz darauf zurück. Diese Referenz können Sie dann in einer Variablen speichern und darüber auf das Objekt zugreifen.

    Beispiel: Ein Objekt mit new erzeugen

    Es wird eine Variable Dvd1 (auch Referenzvariable genannt, da sie ein Objekt referenziert) vom Typ DVD deklariert. Über new DVD() wird ihr eine Referenz auf ein neues DVD-Objekt zugewiesen. Die Deklaration und die Erzeugung des Objekts können auch über zwei separate Anweisungen erfolgen.

    DVD Dvd1 = new DVD();
    // oder
    DVD Dvd1;
    Dvd1 = new DVD();

    INFO

    Eine Ausnahme bei der Objekterzeugung bilden die "primitiven" Datentypen und der Datentyp String. Diesen können Sie über ein Gleichheitszeichen einen neuen Wert zuweisen. Auf diese Weise wird ein Objekt erzeugt und der Wert des Typs festgelegt.
    string s = "Dies ist ein Schlüsselwort";
    int i = 10;
    Int32 i = 10;

    Syntax der Objekterzeugung
  • Zuerst geben Sie den Typ und danach den Bezeichner des Objekts an.
  • Weisen Sie dem Bezeichner eine Referenz auf ein neues Objekt zu. Geben Sie dazu den Operator new, den Typnamen und ein rundes Klammerpaar an.
  • Sie können die Deklaration und Objekterzeugung in einer Anweisung zusammenfassen.

    Anonyme Typen

    Im Zuge der Einführung von LINQ mit dem .NET Framework 3.5 wurden auch Spracherweiterungen geschaffen, die eigentlich nur in Zusammenhang mit LINQ sinnvoll eingesetzt werden können. Die Definition von anonymen Typen ist eine solche. Mit einem anonymen Typ wird ein unbenanntes Objekt erzeugt, welches öffentliche Eigenschaften besitzt, deren Datentyp vom Compiler aus den zugewiesenen Werten bestimmt wird (Eigenschaften werden im Kapitel 12 genauer besprochen). Die Werte der Eigenschaften sind dabei immer nur lesbar. In LINQ wird dies verwendet, um beispielsweise eine Ergebnismenge einer Abfrage bereitzustellen.

    Ein anonymer Typ wird dadurch erzeugt, dass hinter dem Schlüsselwort var sofort der Name des Objekts angegeben wird. Der Typ fehlt also und wird später vom Compiler generiert. Mittels new wird dann ein Objekt erzeugt. Anstatt hier wieder den Typnamen und runde Klammern zu verwenden, werden direkt hinter new in geschweiften Klammern Eigenschaft/Wert-Paare angegeben. Mehrere Paare werden durch Komma getrennt.
    var <name> = new { Eigenschaft1 = Wert1, ... };

    Beispiel: Einen anonymen Typ erstellen

    Vom Compiler wird durch die erste Anweisung ein unbenannter Typ erstellt. Des Weiteren wird ein Objekt von diesem Typ erzeugt, auf das über die Variable person zugegriffen werden kann. Der unbenannte Typ besitzt die beiden Eigenschaften Name und Vorname, auf die nur lesend zugegriffen werden kann.

    var person = new { Name = "Meier", Vorname = "Paule" };
    string n = person.Name;
    Console.WriteLine(person.Vorname);

    Nullwerte

    Wurde einer Referenzvariablen noch kein Wert, d.h. eine Referenz auf ein Objekt, zugewiesen, enthält diese den Wert null. Wenn Sie nicht wissen, ob eine Referenzvariable auf ein Objekt verweist, sollten Sie einen Test auf den Wert null durchführen. Ansonsten kann es beim Zugriff auf das nicht vorhandene Objekt zu einer NullReferenceException kommen.

    Beispiel: Prüfung einer Referenzvariablen auf null

    Die Klasse Person enthält ein Datenelement Name. Vor dem Zugriff wird geprüft, dass die Referenz nicht auf null verweist. Nur in diesem Fall wird auf das Datenelement Name zugegriffen.

    Person p;
    if(p != null)
      p.Name = "Meier";

    Nullbare (nullable) Typen

    Bis einschließlich des .NET Frameworks 1.1, konnten nur Referenztypen den Wert null annehmen. Ab der Version 2.0 können dies nun über einen neuen, speziellen Typ auch die Werttypen wie int oder double. Auf diese Weise kann man z.B. kennzeichnen, dass eine Variable von einem Werttyp noch nicht initialisiert wurde. Bei Datenbankanwendungen ist dies z.B. nützlich, da dort zwischen dem Wert null (überhaupt kein Wert vorhanden) oder dem Wert 0 oder "" (der tatsächliche Wert ist die Zahl 0 oder ein leerer String) unterschieden wird. Für nullbare Typen (in der Hilfe zu finden unter: auf NULL festlegbare Typen), d.h. Typen, die null-Werte zulassen, wurden eine spezielle Syntax und einige Hilfemethoden eingeführt. Dazu wird dem betreffenden Datentyp ein Fragezeichen angefügt. Nullbare Typen sind Instanzen der generischen Struktur System.Nullable<T>.

    Beispiel: Deklarieren von Typen, die den Wert null annehmen können

    int? i = null;
    double? d = 1.0;
    d = null;

    Nullbare Typen haben die folgenden Eigenschaften:
  • Nur Werttypen können als nullbarer Typ deklariert werden. Dazu wird bei der Deklaration dem Werttyp ein Fragezeichen angefügt, z.B. int?. Diese Syntax ist dabei die Kurzform von System.Nullable<T>, wobei T für den Typnamen steht.
  • Nullbare Typen besitzen die Eigenschaft HasValue die true zurückgibt, wenn ein Wert vorhanden ist, und false, wenn die Variable den Wert null enthält. Über die Eigenschaft Value wird der gespeicherte Wert zurückgeliefert. Ist beim Zugriff auf Value der Wert null, wird eine InvalidOperationException ausgelöst.
  • Ein Standardtyp kann implizit in einen nullbaren Typ konvertiert werden. Die umgekehrte Richtung geht nur explizit über einen Cast. Wird einem Standardtyp der Wert null zugewiesen, tritt eine InvalidOperationException auf.
  • Als ein zusätzlicher Operator wird ?? zur Verfügung gestellt (NULL-Sammeloperator). Dieser liefert den Wert eines nullbaren Typs, wenn er ungleich null ist, sonst den danach angegebenen Wert. In der Anweisung
    int? Nullwert = ...;
    int Variable = Nullwert ?? 0; 
    wird Variable entweder der Wert von Nullwert zugewiesen, wenn dieser nicht null ist, ansonsten der Wert 0.
  • Statt des Operators ?? können Sie auch die Methode System.Nullable.GetValueOrDefault() verwenden, die den Standardwert eines Typs liefert, wenn er null ist und einem nicht nullbaren Typ zugewiesen werden soll, z.B. int Variable = Nullwert.GetValueOrDefault();. Alternativ lässt sich der Methode auch ein individueller Standardwert als Parameter übergeben, der dann zugewiesen wird.
  • Die Verknüpfung von booleschen Werten wurde um den Wert null erweitert und entspricht den Ergebnissen der Verknüpfung in SQL (siehe Hilfe-Index VERWENDEN VON AUF NULL FESTLEGBAREN TYPEN).

    Beispiel: Anwendungsbeispiele für nullbare Typen

    Das Beispiel zeigt die verschiedenen Operationen mit nullbaren Typen für den Datentyp int. Eine Konvertierung von einem nullbaren in den nicht nullbaren Typ ist nur über einen Cast möglich (10). Der umgekehrte Fall geht auch implizit (11). Mittels der Eigenschaft HasValue wird in (13) geprüft, ob die Variable einen Wert enthält, der nicht null ist. Auf den Wert kann über die Eigenschaft Value (14) zugegriffen werden. Über den Operator ?? wird in Zeile (17) eine bedingte Zuweisung realisiert. Ist der Wert von zahl1 nicht null, wird dieser Wert verwendet, sonst der Wert -1. In (19) wird die Methode GetValueOrDefault() genutzt, um einem einfachen Werttyp den Wert eines nullbaren Typs oder im Falle von null den Standardwert des Datentyps zuzuweisen.

    01 using System;
    02 namespace CSharpBuch.Kap05
    03 {
    04   public class NullbareTypen
    05   {
    06     static void Main(string[] args)
    07     {
    08       int? zahl1 = null;
    09       int? zahl2 = 10;
    10       int zahl3 = (int)zahl2;
    11       zahl2 = zahl3;
    12 
    13       if(zahl1.HasValue) // oder (zahl1 != null)
    14         Console.WriteLine("zahl1 hat den Wert: " + zahl1.Value);
    15       else
    16         Console.WriteLine("zahl1 hat den Wert null.");
    17       zahl3 = zahl1 ?? -1;
    18       Console.WriteLine("zahl3 hat den Wert: " + zahl3);
    19       zahl3 = zahl1.GetValueOrDefault();
    20       Console.WriteLine("zahl3 hat den Wert: " + zahl3);
    21       Console.ReadLine();
    22     }
    23   }
    24 }
    Listing 5.2: NullbareTypen.cs

    5.3 Strukturen

    Eine Struktur ist einer Klasse sehr ähnlich. Wie mit einer Klasse können Sie auch mit einer Struktur einen Bauplan erzeugen, auf dessen Grundlage Objekte erzeugt werden. Zum Definieren einer Struktur verwenden Sie das Schlüsselwort struct. Objekte einer Struktur werden ebenfalls über new erzeugt.

    Beispiel: Deklarieren und Instanzieren einer Struktur

    struct DVD
    {
      private string titel;
      private int erscheinungsjahr;
      public void Ausleihen(string name)
      {}
    }
    ...
    DVD Dvd1 = new DVD();

    Die beiden wesentlichen Unterschiede zwischen einer Struktur und einer Klasse bestehen darin, dass Strukturen nicht vererbt werden können (siehe Kapitel 7) und dass sie keine Referenz-, sondern Werttypen sind. Dies wird auch deutlich, wenn man sich vor Augen führt, dass jede Struktur automatisch von der Klasse ValueType abgeleitet wird (von der auch die anderen Werttypen wie System.Int32 abgeleitet sind), welche wiederum die Klasse Object erweitert. Durch eine Zuweisung der Art
    Struktur1 = Struktur2;
    werden die Werte der Elemente der Struktur2 in die Elemente der Struktur1 kopiert. Da Strukturen Werttypen sind, werden sie nicht durch den Garbage Collector beseitigt. Dies kann in einigen Fällen zu Geschwindigkeitsvorteilen führen. Besonders dann, wenn eine Struktur hauptsächlich als Datencontainer dient.
    public struct Person
    {
      string name;
      int alter;
    }
    // Da Strukturen Werttypen sind, wird der Speicher für alle 
    // Elemente bereits beim Anlegen des Arrays bereitgestellt
    Person[] personen = new Person[10];
    // wäre Person eine Klasse, müssten Sie zuerst das Array und 
    // danach für jedes Element ein Person-Objekt erzeugen
    for(int personIndex = 0; personIndex < 10; personIndex++)
      personen[personIndex] = new Person();

    Eine Struktur kann neben den Feldern auch Methoden, wie z.B. Ausleihen() in der Struktur DVD, sowie Konstruktoren zum Initialisieren der Felder enthalten. Die Definition eines parameterlosen Standardkonstruktors oder eines Destruktors ist allerdings nicht möglich. Eine Struktur besitzt allerdings implizit einen parameterlosen Standardkonstruktor, der die Werttypen mit ihrem Standardwert (z.B. 0 bei int) und alle Referenztypen mit null initialisiert. Beachten Sie bei der Definition einer Struktur, dass die Feldvariablen nicht initialisiert werden dürfen. Entweder Sie verwenden die implizite Initialisierung über den Standardkonstruktor oder Sie stellen einen eigenen Konstruktor bereit (der mindestens einen Parameter enthält), welcher die Instanzvariablen initialisiert.

    Auch wenn Strukturen nicht von Klassen oder anderen Strukturen erben können, so ist es doch möglich, Interfaces zu implementieren.

    Beispiel: Klassen und Strukturen im Vergleich

    Im Beispiel wurde jeweils eine Klasse und eine Struktur mit dem gleichen öffentlichen Datenelement Name deklariert. Dann wird ein Objekt der Klasse KPerson erzeugt und der Variablen kp1 zugewiesen. Einer zweiten Variablen wird die erste Variable zugewiesen. Hier wird aber keine Kopie des Objekts erzeugt, sondern die Variable kp2 verweist nun auch auf das Objekt kp1. Über beide Variablen wird nun der Wert des Datenelements Name geändert. Wie die Ausgabe zeigt, hat die Änderung über kp1 den Wert von kp2 überschrieben. Die gleiche Vorgehensweise wird nun für den Typ SPerson vorgenommen, wobei es sich hier um eine Struktur handelt. Bei der Zuweisung sp2 = sp1; handelt es sich um eine echte Kopie der Datenelemente, so dass die Änderung des Namens über sp1 keine Auswirkungen auf sp2 hat.

    using System;
    namespace CSharpBuch.Kap05
    {
      public class StrukturenUndKlassen
      {
        static void Main(string[] args)
        {
          KPerson kp1 = new KPerson();
          KPerson kp2 = kp1;
          kp2.Name = "Müller";
          kp1.Name = "Meier";
          Console.WriteLine("Klasse => Name: " + kp2.Name);
          SPerson sp1 = new SPerson();
          SPerson sp2 = sp1;
          sp2.Name = "Müller";
          sp1.Name = "Meier";
          Console.WriteLine("Struktur => Name: " + sp2.Name);
          Console.ReadLine();
        }
      }
      public class KPerson
      {
        public string Name;
      }
      public struct SPerson
      {
        public string Name;
      }
    }
    Listing 5.3: StrukturenUndKlassen.cs

    Ausgabe:
    Klasse => Name: Meier
    Struktur => Name: Müller

    INFO

    Sind einige Datenelemente einer Struktur Referenzvariablen, werden hier ebenfalls Kopien erzeugt. Dabei ist aber zu beachten, dass diese Variablen selbst zwar unterschiedlich sind, aber auf das gleiche Objekt verweisen, da von dem Objekt selbst keine Kopie erzeugt wird (Beispiel StrukturRefKopie.cs).

    5.4 Methoden

    Die Funktionalität einer Anwendung machen die Methoden bzw. die Anweisungen darin aus. Methoden befinden sich immer innerhalb einer Klasse. In C# wird nicht zwischen Methoden mit oder ohne Rückgabewert unterschieden. Die Bezeichnung "Methode" kennzeichnet die Zugehörigkeit zu einer Klasse, einer Struktur oder einem Interface. In anderen Programmiersprachen entsprechen Methoden z.B. Funktionen oder Prozeduren.

    Eine Methode sollte immer eine ganz bestimmte Aufgabe erfüllen. Anstatt eine Anwendung mit nur einer oder wenigen Methoden zu implementieren, in die möglichst viel hineingepackt wird, sollten die Anweisungen in funktional zusammengehörige Blöcke gegliedert werden, die in Methoden verpackt werden. Dadurch wird ein Programm besser lesbar und wartbar. Außerdem kann die Funktionalität einer Methode eventuell an mehreren Stellen im Programm genutzt werden. Die Methode WriteLine() der Klasse Console ist ein Beispiel für eine Methode, die in einer (Konsolen-)Anwendung immer wieder zur Ausgabe von Text benötigt wird.

    Beispiel: Die Funktionalität einer Anwendung auf mehrere Methoden verteilen

    Die Funktionalität dieser Anwendung wurde auf vier Methoden verteilt. Damit diese innerhalb der Methode Main() verwendet werden können, müssen sie als static deklariert werden (da statische Methoden nur andere statische Methoden aufrufen können). Nach der Aufforderung, die Länge eines Passworts anzugeben, geben Sie eine Zahl auf der Konsole ein, ansonsten wird eine Exception ausgelöst. Dann wird ein Passwort "generiert". Dieses besteht einfach aus einer Folge der Buchstaben ABC... in der eingegebenen Länge (Methode PasswortErzeugen()). Auch wenn die Methoden in diesem Beispiel sehr wenige Anweisungen enthalten, ist die Aufteilung durchaus sinnvoll. Man könnte z.B. die Methode AnzahlZeichenEinlesen() dazu verwenden, beliebige Zahlen über die Konsole einzulesen. Es müsste nur dafür gesorgt werden, dass die Aufforderung zur Eingabe variabel ist. Die Methode PasswortErzeugen() ist natürlich in der Praxis so nicht brauchbar, zumindest was das generierte Passwort angeht. Damit es tatsächlich ein zufälliges Passwort wird, müssen Sie später aber nur diese Methode ändern, der Rest der Anwendung bleibt unberührt. Die Methode PasswortAusgeben() kann z.B. dahingehend geändert werden, dass sie das Passwort in eine Datei schreibt.

    using System;
    namespace CSharpBuch.Kap05
    {
      public class PasswortGeneratorProj
      {
        static void Main(string[] args)
        {
          int anzZeichen;
          string passwort;
    
          BegruessungAnzeigen();
          AnzZeichen = AnzahlZeichenEinlesen();
          passwort = PasswortErzeugen(anzZeichen);
          PasswortAusgeben(passwort);
        }
        private static void BegruessungAnzeigen()
        {
          Console.WriteLine("Es wird ein Passwort erzeugt, dass " + 
                            "die von Ihnen angegebene Länge besitze.");
        }
        private static int AnzahlZeichenEinlesen()
        {
          Console.WriteLine("Wie lang soll das Passwort sein?");
          return Convert.ToInt32(Console.ReadLine());
        }
        private static string PasswortErzeugen(int laenge)
        {
          string passwort = "";
          for(int i = 0; i < laenge; i++)
            passwort = passwort + (char)(i + 65);
          return passwort;
        }
        private static void PasswortAusgeben(string passwort)
        {
          Console.WriteLine("Das generierte Passwort ist: " + passwort);
          Console.ReadLine();
        }
      }
    }
    Listing 5.4: PasswortGenerator.cs

    5.4.1 Methoden ohne Parameterübergabe

    Die einfachste Form einer Methode besteht in einer Methode, die keine Parameter erwartet. Lediglich ein Rückgabewert kann angegeben werden. Die Verwendung einer Methode unterteilt sich weiterhin in die Deklaration der Methode und deren Aufruf.

    Syntax der Methodendeklaration

  • Methoden werden immer in einer Klasse (oder einer Struktur) deklariert.
  • Eine Methode besitzt immer einen Rückgabetyp und einen Namen. Neben den bekannten Datentypen kann eine Methode eine Referenz auf einen Typ oder void zurückgeben. Dabei kennzeichnet void die Tatsache, dass kein Wert zurückgegeben wird. Die Angabe von void ist in diesem Fall aber unbedingt notwendig.
  • Optional können Sie einer Methode einen der Zugriffsmodifizierer public, protected, private, protected internal oder internal voranstellen. Ohne Angabe eines Zugriffsmodifizierers ist private in Klassen voreingestellt.
  • Dem Namen der Methode folgt ein rundes Klammerpaar, in dem Parameter angegeben werden können, die an die Methode zu übergeben sind.
  • Im Methodenrumpf befinden sich die Anweisungen der Methode, welche deren Funktionalität realisieren. Sie sind in ein geschweiftes Klammerpaar eingeschlossen.
  • Mittels der return-Anweisung wird eine Methode sofort verlassen. Jeder mögliche Ausführungspfad der Methode (also z.B. ein if- und der else-Zweig, dem keine Anweisungen mehr folgen) muss über eine return-Anweisung verfügen. Ansonsten meldet der Compiler einen Fehler. Der return-Anweisung wird der Rückgabewert der Methode angefügt.
  • Die Namen von Methoden werden immer in Pascalschreibweise formuliert, d.h., jedes Hauptwort wird mit einem Großbuchstaben begonnen.

    Beispiel: Deklaration einer Methode

    [public | protected | private | internal]  Name()
    {
      // Anweisungen
      return [];
    }

    INFO

    Wenn der Compiler Anweisungen bemerkt, die niemals ausgeführt werden können, nimmt er bestimmte Prüfungen nicht vor. Im folgenden Codeauszug enthält der else-Zweig z.B. keine -Anweisung. Da er aber niemals aufgerufen wird, gibt der Compiler auch keine Fehlermeldung (allerdings eine Warnung) aus. Eine Syntaxprüfung nimmt der Compiler allerdings in jedem Fall vor.

    private static int GetZahl()
    {
      if(true)
        return 10;
      else
        Console.WriteLine("Hallo");
    }

    Syntax des Methodenaufrufs
  • Eine Methode wird über ihren Namen aufgerufen. Dem Methodennamen wird ein Klammerpaar angefügt.
  • Der Rückgabewert einer Methode kann optional einer Variablen zugewiesen werden. Es ist aber keine Pflicht, den Rückgabewert einer Methode zu verarbeiten.
  • Statische Methoden werden über den Klassen- und Methodennamen, getrennt durch einen Punkt, aufgerufen.
  • Alle anderen Methoden werden über den Namen einer Referenzvariablen und den Methodennamen, getrennt durch einen Punkt, aufgerufen.

    Beispiel: Eine Methode aufrufen

    Die Klasse Mathe deklariert die Methode GetPI(), die den (verkürzten) Wert der mathematischen Konstante Pi zurückliefert. Um die Methode zu nutzen, muss ein Objekt der Klasse Mathe erzeugt werden. Über die Referenzvariable m kann dann die Methode aufgerufen werden.

    public class Mathe
    {
      public double getPI()
      {
        return 3.14;
      }
    }
    public class Test
    {
      static void Main(string[] args)
      {
        Mathe m = new Mathe(); // Objekt erzeugen
        Console.WriteLine(m.GetPI());
      }
    }
    Listing 5.5: EinfacheMethoden.cs

    5.4.2 Methoden mit Parameterübergabe

    Meist werden den Methoden Parameter übergeben, die während der Ausführung der Methode ausgewertet und gegebenenfalls geändert werden. Die Anzahl und Reihenfolge der beim Methodenaufruf übergebenen aktuellen Parameter (auch Argumente genannt) muss mit der Anzahl und Reihenfolge der im Methodenkopf deklarierten formalen Parameter übereinstimmen. Es sei denn, Sie haben bei der Methodendeklaration eine variable Parameterliste, die eine beliebige Anzahl von Parametern entgegennehmen kann, deklariert. Die in der Parameterliste festgelegten Typen und die Anzahl der Parameter werden als Signatur der Methode bezeichnet. Der Rückgabetyp zählt in der Regel nicht zur Signatur.

    Parameter können auf zwei unterschiedliche Arten übergeben werden. Dies richtet sich danach, ob der Wert der als Parameter übergebenen Variablen nach der Ausführung der Methode erhalten bleiben soll oder in der Methode geändert werden kann. Dazu werden zwei verschiedene Möglichkeiten der Parameterübergabe unterschieden - Wertparameter und Referenzparameter.

    Diese Unterscheidung ist allerdings nur bei Werttypen notwendig. Bei Referenztypen ist es egal, ob sie per Referenz oder als Wert übergeben werden - es wird letzten Endes immer eine Referenz verwendet.

    INFO

    Die Namen der Parameter werden standardmäßig in Kamelschreibweise angegeben, d.h. das erste Hauptwort wird klein, und alle folgenden groß geschrieben.
    Syntax der Methodendeklaration mit Parametern
  • Die formalen Parameter, mehrere durch Komma getrennt, werden in Klammern hinter dem Methodennamen angegeben.
  • Jeder Parameter wird durch seinen Datentyp und seinen Namen deklariert.
  • Für die Parameterübergabe per Referenz (by reference) ist das Schlüsselwort ref oder out voranzustellen. Dabei kennzeichnet ref einen Parameter, der einen Wert an die Methode übergibt und in der Methode verändert werden kann. Ein out-Parameter muss vor der Übergabe an die Methode nicht initialisiert werden, da er nur einen Wert aus der Methode zurückgibt.
  • Variablen Parameterlisten kann das Schlüsselwort params vorangestellt werden. Als Datentyp ist ein eindimensionales Array anzugeben.
    Wertparameter
    Wertparameter werden erzeugt, wenn bei der Deklaration der formalen Parameter keine weitere Angaben (also kein ref oder out) vor dem Datentyp stehen. Bei der Parameterübergabe als Wert (by value) bekommt der formale Parameter zur Laufzeit eine Kopie des Wertes des aktuellen Parameters zugewiesen. Wird der Wert des formalen Parameters innerhalb der Methode geändert, hat dies keine Auswirkung auf die Variable, die als aktueller Parameter angegeben wurde (vgl. folgendes Beispiel: ParameterUebergeben.cs).
    Referenzparameter
    Mit den Schlüsselwörtern ref oder out werden Referenzparameter deklariert. Die Angabe ist bei Werttypen erforderlich, wenn Änderungen des Variablenwertes innerhalb der Methode auch nach der Beendigung der Methode zur Verfügung stehen sollen. Die als Parameter angegebene Variable kann also dauerhaft innerhalb der aufgerufenen Methode geändert werden.

    Parameterübergabe als Wert und als Referenz

    Abbildung 5.3: Parameterübergabe als Wert und als Referenz

    Beispiel: Parameterübergabe an Methoden

    Die Klasse ParameterUebergeben enthält zwei Methoden Add1() und Add2(), in denen jeweils der Wert des Parameters um eins erhöht wird. Der Unterschied zwischen beiden Methoden liegt lediglich in der Art der Parameterübergabe. Der Methode Add1() wird der Parameter als Wert und der Methode Add2() als Referenz übergeben. Nach Aufruf der beiden Methoden von Main() aus erfolgt die Ausgabe des Wertes der als Parameter übergebenen Variablen i. An den Ausgaben sehen Sie, dass der Aufruf von Add1() im Gegensatz zu Add2() keinen Einfluss auf den späteren Wert von i hat.

    using System;
    namespace CSharpBuch.Kap05
    {
      public class ParameterUebergeben
      { 
        static void Main(string[] args)
        {
          int i = 1;
          Add1(i);
          Console.WriteLine("i = " + i);
          Add2(ref i);
          Console.WriteLine("i = " + i);
          Console.ReadLine();
        }
        static void Add1(int x)
        {
          x++;
          Console.WriteLine("x = " + x);
        }
        static void Add2(ref int y)
        {
          y++;
          Console.WriteLine("y = " + y);
        }
      }
    }
    Listing 5.6: ParameterUebergeben.cs

    Verwenden Sie das Schlüsselwort ref, muss der Parameter vorher initialisiert werden. Bei der Verwendung von out ist dies nicht erforderlich. In beiden Fällen muss das Schlüsselwort aber sowohl in der Methodendeklaration, als auch beim Methodenaufruf vor dem entsprechenden Parameter angegeben werden.

    Rückgabeparameter

    Eine Methode kann über return maximal einen Wert zurückliefern. Oft reicht das jedoch nicht aus. Dieses Problem lässt sich leicht lösen, indem Sie der Methode Variablen für die Wertrückgabe als Referenzparameter übergeben. Wertänderungen dieser Parameter bleiben dann auch außerhalb der Methode erhalten. Definieren Sie Parameter, die nur für die Wertrückgabe benötigt werden, müssen die übergebenen Variablen nicht einmal initialisiert werden. Diese Parameter sind dann durch das Schlüsselwort out zu kennzeichnen. Sie werden als Rückgabe- oder auch als Ausgabeparameter bezeichnet.

    Beispiel: Ausgabeparameter zur Rückgabe mehrerer Werte nutzen

    Die Methode Init() nimmt zwei Parameter entgegen, die innerhalb der Methode mit Werten belegt werden. Da die Parameter mit out deklariert sind, müssen die übergebenen Variablen nicht initialisiert werden und die Werte sind nach Beendigung der Methode verfügbar. Dies ist aus der Ausgabe ersichtlich, die dem Methodenaufruf folgt.

    static void Init(out int x, out int y)
    {
      x = 5;
      y = 8;
    }
    ...
    // Aufruf in der Main-Methode:
    int j, k;
    Init(out j, out k);
    Console.WriteLine("j = " + j + ", k = " + k); 
    // Ausgabe: j = 5, k = 8

    INFO

    Da ein out-Parameter nicht initialisiert ist, wird er wie eine lokale Variable behandelt. Er muss also vor der Verwendung auf der rechten Seite initialisiert werden.

    static void Init(out int x, out int y)
    {
      int i =  x; // Compilerfehler
    Variable Parameterlisten
    Mit dem Schlüsselwort params ist es möglich, eine Parameterliste zu deklarieren, die beliebig viele Parameter entgegennehmen kann. Dieses Schlüsselwort wird nur in der Methodendeklaration angegeben, nicht beim Methodenaufruf. Beachten Sie, dass hinter einem params-Parameter kein weiterer Parameter folgen darf. Damit kann dieses Schlüsselwort auch nur einmal in der Methodendeklaration verwendet werden. Das übergebene Array darf nur eindimensional sein (Arrays werden später noch genauer vorgestellt) und es wird als Wert übergeben. Das heißt, die Werte innerhalb des Arrays können in einer Methode nicht dauerhaft verändert werden.

    Beispiel: Variable Parameterlisten nutzen

    Der Methode Anzahl() kann eine Liste von beliebig vielen Objekten übergeben werden. Sie liefert die Anzahl der tatsächlich übergebenen Objekte zurück. In der Main()-Methode wird die Methode Anzahl() mit vier Parametern aufgerufen. Da in C# Werttypen indirekt Objekte sind (d.h., in diese über Boxing konvertiert werden), kann als Parametertyp der allgemeinste Typ Object genutzt werden. Die zurückgegebene Anzahl der Objekte wird ausgegeben. Die Ausgabe lautet hier: 4 Objekte übergeben

    public class VariableParameterliste
    {
      static void Main(string[] args)
      {
        Console.WriteLine(Anzahl(1, 2.2, "drei", "vier") + 
                          " Objekte übergeben");
      }
      private static int Anzahl(params object[] liste)
      {
        return liste.Length;
      }
    }
    Listing 5.7: VariableParameterliste.cs

    Benannte Parameter

    Die bisherige Programmierung reiner C#-Anwendungen wird durch die Einführung der benannten Parameter (auch benannte Argumente) mit dem .NET Framework 4.0 nicht wesentlich verbessert. Als einzigen Vorteil kann die direkt beim Methodenaufruf erkennbare Verwendung der Parameter angesehen werden, da der Name des Parameters vor den betreffenden Wert geschrieben wird. Ansonsten sind benannte Parameter vielmehr ein Entgegenkommen an die Programmierung mit COM (Common Object Model), um beispielsweise Word oder Excel anzusteuern. Dort ist es von Vorteil wenn Parameter nicht mehr durch ihre Reihenfolge sondern durch ihren Namen beim Methodenaufruf festgelegt werden. Noch interessanter werden die optionalen Parameter die gleich besprochen werden, da es beider COM-Programmierung oft Methoden mit sehr vielen Parametern gibt die aber meist nicht alle benötigt werden.
  • Die Deklaration einer Methode ändert sich nicht, um benannte Argumente zu nutzen.
  • Geben Sie beim Aufruf der Methode vor dem Parameterwert den Namen des Parameters sowie einen Doppeltpunkt vor dem Parameterwert an, z.B. Rechne(zahl1: 100, zahl2: 300).
  • Die Reihenfolge der benannten Parameter ist beliebig, es dürfen aber keine Parameter weggelassen werden. Verwenden Sie auch unbenannte Positionsparameter, müssen diese zu Beginn stehen. Positionsparameter dürfen nicht benannten Parametern folgen.
    Rechne(100, zahl2: 300);        // Ok
    Rechne(zahl2: 300, zahl1: 100); // Ok
    Rechne(zahl1: 100, 300);        // nicht erlaubt

    Beispiel: Parameter mit Namen versehen

    Der Aufruf der beispielhaften Methode Ausgabe() erfolgt hier über drei verschiedene Varianten. Zuerst wird der Standardaufruf ohne die Verwendung von benannten Parametern genutzt. Im folgenden Aufruf werden die benannten Parameter genutzt und es wird ersichtlich, das die Reihenfolge keine Rolle spielt. Der dritte Aufruf zeigt schließlich das auch Positionsparameter mit benannten gemischt werden können, solange die Positionsparameter (also die normalen Parameter ohne Angabe des Namen) an ersten Stelle stehen.

    public class BenannteParameter
    {
      static void Main(string[] args)
      {
        BenannteParameter bPara = new BenannteParameter();
        bPara.Ausgabe(10, "Meier", 100.99);
        bPara.Ausgabe(name: "Schulze", betrag: 200.01, zahl: 100);
        bPara.Ausgabe(1000, betrag: 12.34, name: "Kunze");
      }
      public void Ausgabe(int zahl, string name, double betrag)
      {
        Console.WriteLine("zahl: " + zahl);
        Console.WriteLine("name: " + name);
        Console.WriteLine("betrag: " + betrag);
      }
    }
    Listing 5.8: BenannteParameter.cs

    Optionale Parameter

    Im Unterschied zu variablen Parameterlisten (definiert durch params) bei denen Sie für einen Parameter beliebig viele Werte angeben konnten gestattet die Verwendung optionaler Parameter, dass nicht für alle Parameter Werte beim Methodenaufruf übergeben werden müssen. Dazu werden den Parametern bei der Deklaration zusätzlich Standardwerte zugewiesen die diese annehmen, wenn sie nicht explizit angegeben werden.

    Speziell für die COM-Programmierung sind optionale Parameter sehr hilfreich, da hier meist nur wenige Parameter von sehr umfangreichen Parameterlisten (10 bis 20 Parameter) verwendet werden.

  • Optionale Parameter dürfen Sie bei Methoden, Konstruktoren, Indexern und Delagaten verwenden.
  • Verwenden Sie auch Positionsparameter in der Deklaration, müssen diese zu Beginn stehen. Positionsparameter dürfen nicht optionalen Parametern folgen.
  • Für alle optionalen Parameter muss ein Standardwert bei der Deklaration angegeben werden, der verwendet wird, wenn der Parameter beim Aufruf der Methode etc. weggelassen wird. Die Standardwerte müssen Konstanten bzw. Literale sein.
  • Sie können beim Aufruf Positionsparameter und optionale Parameter zugleich verwenden, allerdings müssen Positionsparameter zu Beginn angegeben werden.
  • Beim Aufruf muss beachtet werden, dass Sie entweder Parameterwerte für optionale Parameter am Ende weglassen, oder benannte Parameter für den Aufruf nutzen, um bestimmte optionale Parameterwerte nicht zu setzen. Das Verwenden von Lücken (also 2 Kommas hintereinander beim Methodenaufruf) sind nicht erlaubt (z.B. Rechne(10, , 20)).
  • Beim Aufruf müssen immer zuerst die Werte für die festen Positionsparameter angegeben werden. Optionale Parameter können danach weggelassen oder über benannte Parameter angegeben werden.

    Beispiel: Parameterwerte beim Aufruf weglassen

    Die Methode Ausgabe() besitzt in diesem Beispiel drei optionale Parameter, die jeweils mit null vorbelegt sind. Es wurden dazu nullbare Typen verwendete um tatsächlich später erkennen zu können, das diese nicht beim Aufruf mit einem Wert belegt wurden. Für die Ausgabe des Wertes wurde der NULL-Sammeloperator ?? verwendet der entweder den Wert des Parameters liefert wenn dieser nicht null ist oder im anderen Fall den nach dem ??-Operator angegebenen Wert. Alternativ könnte auch mit der Methode HasValue() der Parameter geprüft werden, ob dieser einen von null verschiedenen Wert besitzt und entsprechend darauf reagiert werden.

    public class OptionaleParameter
    {
      static void Main(string[] args)
      {
        OptionaleParameter oPara = new OptionaleParameter();
        oPara.Ausgabe();
        oPara.Ausgabe(name: "Meier");
        oPara.Ausgabe(100, betrag: 12.34);
      }
      public void Ausgabe(int? zahl = null, string name = null, 
                          double? betrag = null)
      {
        Console.WriteLine("zahl: " + (zahl ?? 0).ToString());
        Console.WriteLine("name: " + (name ?? "-"));
        Console.WriteLine("betrag: " + (betrag ?? 0.0).ToString());
      }
    }
    Listing 5.9: OptionaleParameter.cs

    5.4.3 Methoden überladen

    Häufig werden Methoden benötigt, die gleiche Aufgaben erfüllen, aber unterschiedliche Parameterlisten benötigen. Anstatt nun verschiedene Methodennamen zu vergeben, können Sie die Methoden überladen. Sie erstellen also mehrere Methoden, die den gleichen Namen aber unterschiedliche Parameter besitzen. Ein Beispiel, welches Sie bereits kennen, ist die Verwendung der Methode Console.WriteLine(). Ihr können sowohl verschiedene Werttypen als auch Objekte übergeben werden. Als Ergebnis gibt sie immer die Textrepräsentation der Werttypen oder des Objekts auf der Konsole aus. Beim Überladen von Methoden müssen Sie folgende Regeln beachten:
  • Der Name muss bei allen Methoden übereinstimmen.
  • Die Methoden müssen sich in der Anzahl und/oder dem Typ der Parameter unterscheiden.
  • Der Rückgabetyp wird nicht zur Unterscheidung der Methoden hinzugezogen, da Methoden auch ohne Verarbeitung des Rückgabewertes aufgerufen werden können und der Compiler somit keine Möglichkeit hat, diesen Typ zu ermitteln.

    Beispiel: Überladen von Methoden, welche die gleichen Aufgaben erfüllen

    Die Methode TextAusgeben() wird in diesem Beispiel dreimal deklariert. Sie kann sowohl einen int-, einen double-, als auch einen string-Parameter entgegennehmen. Der übergebene Parameter und sein Datentyp werden ausgegeben. Aufgrund der Überladung müssen nicht für jeden Parametertyp neue Namen erdacht werden, was die Anwendung der Methoden erleichtert.

    using System;
    namespace CSharpBuch.Kap05
    {
      public class MethodeUeberladen
      {
        static void Main(string[] args)
        {
          TextAusgeben("Hallo");
          TextAusgeben(23.456);
          TextAusgeben(25);
          Console.ReadLine();
        }
        static void TextAusgeben(int x)
        {
          Console.WriteLine(x + " ist eine ganze Zahl");
        }
        static void TextAusgeben(double x)
        {
          Console.WriteLine(x + " ist eine Gleitkommazahl");
        }
        static void TextAusgeben(string x)
        {
          Console.WriteLine(x + " ist eine Zeichenkette");
        }
      } 
    }
    Listing 5.10: MethodenUeberladen.cs

    Das Überladen einer Methode ist nicht möglich, wenn derselbe Parameter in einer Methodendeklaration mit ref und in einer anderen mit out deklariert wird. Die Überladung kann aber für Parameter erfolgen, die in einer Methode mit ref oder out deklariert sind, in einer weiteren Methode jedoch keiner von beiden verwendet wird.

    Nicht erlaubte Kombination:

    void methode(ref int i)
    void methode(out int i)
    // erlaubte Kombination:
    void methode(ref int i)
    void methode(int i)

    INFO

    Haben Sie eine Methode mit einer variablen Parameterliste überladen, wird diese nur dann verwendet, wenn keine der anderen Methoden passt, auch wenn dazu eine interne Typumwandlung durchgeführte werden muss.

    5.4.4 Externe Methoden

    Eine besondere Form von Methoden sind die externen Methoden, die aus anderen Bibliotheken in eine Anwendung importiert werden können. Bei diesen Bibliotheken handelt es sich um Dynamic Link Libraries (DLLs), die z.B. in C/C++ oder Delphi geschrieben wurden. An diese Methoden werden einige Anforderungen gestellt, die aber nicht Gegenstand dieses Buches sind - hier geht es ja um .NET, während diese DLLs mit dem Win32-API entwickelt werden.

    Damit eine externe Methode verwendet werden kann, benötigen Sie folgende Angaben:

  • Den Namen der DLL.
  • Den Namen der Methode inklusive der Parametertypen sowie deren "Übersetzung" in Typen des .NET Frameworks. In der Hilfe finden Sie dazu unter DATENTYPEN FÜR DEN PLATTFORMAUFRUF weitere Informationen.

    Zum Einbinden verwenden Sie das Attribut DllImport. Als Parameter übergeben Sie den Modulnamen der DLL ohne Endung. Es ist noch die Angabe weiterer Parameter möglich. Unter dem Attribut wird die Methode deklariert und die Modifizierer static und extern werden davor gesetzt. Dieser Abschnitt muss sich innerhalb einer Klasse befinden, über die dann die Methode aufgerufen werden kann.

    [DllImport("")]
    static extern <type> <Name>(<Parameter>);

    Beispiel: Externe Methoden nutzen

    Das Attribut DllImport befindet sich im Namespace System.Runtime.InteropServices, der zu Beginn mit eingebunden werden muss. Zum Aufruf der statischen Methode Sleep() ist der Namespace System.Threading erforderlich, da hieraus die Klasse Thread benötigt wird. Durch den Aufruf von Sleep() wird die Anwendung für die übergebene Anzahl Millisekunden angehalten. In der Klasse ExterneMethoden wird dann die Funktion MessageBeep() importiert (die einen Piepton erzeugt). Dieser wird ein Parameter vom Typ uint übergeben, der festlegt, welcher Sound-Typ erzeugt wird. Die Übergabe von 0xFFFFFFFF erzeugt den Standard-Piepser. Durch den "geschickten" Einsatz der for-Anweisung in Verbindung mit dem Modulo-Operator und der Methode Sleep() erhalten Sie einen Klingelton, der das Herz eines jeden Besitzers eines polyphonen Handys gefrieren lässt.

    using System;
    using System.Runtime.InteropServices;
    using System.Threading;
    namespace CSharpBuch.Kap05
    {
      public class ExterneMethoden
      {
        [DllImport("user32")]
        static extern bool MessageBeep(uint type);
        static void Main(string[] args)
        {
          for(int i = 0; i < 60; i++)
          {
            if(i % 10 == 0)
              Thread.Sleep(300);
            if(i % 4 == 0)
              Thread.Sleep(50);
            MessageBeep(0xFFFFFFFF);
          }
        }
      }
    }
    Listing 5.11: ExterneMethoden.cs

    TIPP

    Der Vorteil beim Einbinden externer Methoden bzw. Funktionen liegt natürlich in der Wiederverwendbarkeit bereits vorhandener Softwaremodule. Es sind ja noch nicht längst alle Funktionen des Windows APIs in .NET verfügbar. Weiterhin bieten einige Hersteller nur DLLs als Schnittstelle zu spezieller Hardware oder anderen Dingen an, so dass Sie diese in .NET-Anwendungen importieren müssen.

    Nachteilig ist, dass Sie Unmanaged Code verwenden (also Code der nicht durch die CLR überwacht wird) und dadurch unter anderem den Sicherheitsmechanismus von .NET aushebeln.

    5.5 Konstruktoren und Destruktoren

    5.5.1 Konstruktoren

    Ein Konstruktor ist eine spezielle Methode, die beim Erzeugen eines Objekts einer Klasse oder einer Struktur mit new aufgerufen wird. Er hat denselben Namen wie die Klasse und gibt keinen Wert zurück. Häufig gibt es mehrere Überladungen des Konstruktors (also mehrere Konstruktoren mit unterschiedlichen Parametern), um verschiedene Möglichkeiten zu schaffen, ein Objekt schon bei dessen Erzeugung zu initialisieren. Wird in einer Klasse oder Struktur kein Konstruktor definiert, wird automatisch ein Standardkonstruktor (Defaultkonstruktor) bereitgestellt, der keine Parameter besitzt. Der Konstruktor übernimmt normalerweise die Initialisierung der Felder einer Klasse.

    Syntax von Konstruktoren
  • Konstruktoren sind wie Methoden aufgebaut, sie haben allerdings keinen Rückgabetyp, auch nicht void. Konstruktoren können wie Methoden benannte und optionale Parameter besitzen.
  • Konstruktoren werden in der Regel als public vereinbart und tragen den Namen der Klasse.
  • Sie können beliebig oft überladen werden. Bei der Objekt-Erzeugung mit new wird der passende Konstruktor aufgerufen.
  • Auch wenn Sie keinen Konstruktor definieren, wird immer ein parameterloser Standardkonstruktor bereitgestellt, da dieser zum Erzeugen des Objekts notwendig ist. Der Standardkonstruktor initialisiert alle Instanzvariablen der Klasse mit ihren Standardwerten, z.B. int-Zahlen mit dem Wert 0.
  • Sobald Sie irgendeinen Konstruktor definiert haben, wird kein Standardkonstruktor mehr bereitgestellt. Ein parameterloser Konstruktor muss dann also bei Bedarf selbst definiert werden.
  • Es ist nicht möglich, einen Konstruktor direkt (ohne new) aufzurufen.
  • Spezielle Konstruktoren sind private-Konstruktoren. Durch die Verwendung eines privaten Konstruktors können Sie verhindern, dass eine Klasse mittels new instanziert werden kann. Diese Konstruktoren werden z.B. in Klassen verwendet, die nur statische Member besitzen. Hier ist es weder notwendig noch sinnvoll, eine Instanz zu erzeugen, vgl. später in diesem Kapitel.
  • Instanzvariablen werden noch vor der Ausführung des Konstruktors initialisiert.

    Beispiel: Verwenden verschiedener Konstruktoren bei der Objekterzeugung

    Die Klasse Buch besitzt drei Überladungen des Konstruktors, in denen die Instanzvariablen mit den übergebenen Werten initialisiert werden. In der Main()-Methode werden zwei der Konstruktoren bei der Verwendung des Operators new aufgerufen. Anschließend werden zur Überprüfung die Inhalte der Variablen eines Buch-Objekts über die Methode ObjektAnzeigen() ausgegeben. Über this wird hier Bezug auf die Instanzvariablen der Klasse genommen. Diese Angabe ist notwendig, da die Parameter den gleichen Namen wie diese Variablen besitzen.

    using System;
    namespace CSharpBuch.Kap05
    {
      public class ObjekteErzeugen
      {
        static void Main(string[] args)
        {
          Buch buch = new Buch("Märchen", "Gebrüder Grimm");
          Console.WriteLine(buch.ObjektAnzeigen());
          buch = new Buch("Mord im Orientexpress", "A. Christie", 1978);
          Console.WriteLine(buch.ObjektAnzeigen());
          Console.ReadLine();
        }
      }
      public class Buch
      {
        private string titel;
        private string autor;
        private int erscheinungsJahr;
        public Buch(string titel, int jahr)
        {
          this.titel = titel;
          this.erscheinungsJahr = jahr;
          this.autor = "";
        }
        public Buch(string titel, string autor)
        {
          this.titel = titel;
          this.autor = autor;
          this.erscheinungsJahr = 0;
        }
        public Buch(string titel, string autor, int jahr)
        {
          this.titel = titel;
          this.erscheinungsJahr = jahr;
          this.autor = autor;
        }
        public string ObjektAnzeigen()
        {
          if(erscheinungsJahr == 0)
            return (autor + ": \"" + titel + "\", Erscheinungsjahr unbekannt");
          else if(Autor == "")
            return ("\"" + titel + "\", " + erscheinungsJahr + " Autor unbekannt");
          else
            return (autor + ": \"" + titel + "\", " + erscheinungsJahr);
        }
      }
    }
    Listing 5.12: ObjekteErzeugen.cs

    Konstruktorinitialisierer

    Um über verschiedene Konstruktoren eine Initialisierung der Instanzvariablen zu erreichen, ist wie im gerade gezeigten Beispiel einiges an Schreibarbeit notwendig. In jedem Konstruktor müssen alle Variablen mit Werten versehen werden. Einfacher wäre es, wenn man beim Aufruf eines Konstruktors einfach den Aufruf an den umfangreichsten (oder einen anderen) Konstruktor weiterleiten könnte. Die fehlenden Parameterwerte werden dann mit Standardwerten versehen. Genau diese Funktionalität bieten Konstruktorinitialisierer (manchmal auch Konstruktorverkettung genannt).

    Dazu wird einem Konstruktor das reservierte Wort this mit einem Doppelpunkt angefügt und in Klammern werden die Parameter angegeben. In diesem Fall wird zuerst der Konstruktor aufgerufen, der die passenden Parameter zum Aufruf von this besitzt. Danach wird erst der eigentlich aufgerufene Konstruktor ausgeführt.

    INFO

    Die Bedeutung von this wird im nächsten Kapitel genauer erläutert.

    Beispiel: Konstruktorinitialisierer vereinfachen die Initialisierung eines Objekts

    In den weniger umfangreichen Konstruktoren der Klasse Buch werden die Standardwerte einfach an den "allumfassenden" Konstruktor übergeben. Nachdem dieser ausgeführt wurde, werden die Anweisungen im zuerst aufgerufenen Konstruktor ausgeführt.

    public Buch(string titel, int jahr): this(titel, "", jahr)
    {
      Console.WriteLine("Konstruktor (titel, jahr)"); 
    }
    public Buch(string titel, string autor, int jahr)
    {
      Console.WriteLine("Konstruktor (titel, autor, jahr)");
      this.titel = titel;
      this.erscheinungsJahr = jahr;
      this.autor = autor;
    }
    Listing 5.13: ObjekteErzeugen2.cs

    Ausgabe:

    Konstruktor (titel, autor, jahr)
    Konstruktor (titel, jahr)"

    5.5.2 Objektinitialisierer

    Besitzt eine Klasse zahlreiche öffentliche Eigenschaften (diese werden später noch genau erklärt) und Variablen (öffentliche Variablen sollten eher der Ausnahmefall sein) und soll es unterschiedliche Initialisierungsvarianten für diese geben, müsste für jede ein eigener Konstruktor oder eine komplizierte Initialisierungslogik geschaffen werden.

    Die mit C# 3.0 (.NET Framework 3.5) neu eingeführten Objektinitialisierer ermöglichen es, beim Erzeugen einer Instanz einer Klasse öffentlichen Variablen und Eigenschaften Werte zuzuweisen. Dazu wird hinter dem Typnamen keine runde, sondern eine geschweifte Klammer angegeben. Innerhalb der geschweiften Klammer werden Name/Wert-Paare angegeben, mehrere Paare werden durch Komma getrennt. Der Name entspricht dabei dem Namen einer Variablen oder einer Eigenschaft. Die Initialisiererliste wird mit einer geschweiften Klammer abgeschlossen. Im Gegensatz zur bisher üblichen Objekterzeugung wird kein rundes Klammerpaar benötigt.

    Möchten Sie zusätzlich einen parametrisierten Konstruktor nutzen, ist das runde Klammerpaar vor der Objektinitialisierung anzugeben. In diesem Fall muss gegebenenfalls auch der parameterlose Standardkonstruktor bereitgestellt werden, wenn Sie Objektinitialisierungen ohne weitere Parameter an Konstruktoren durchführen wollen.

    Typ t = new Typ{ Variable|Eigenschaft = , ...};
    Typ t = new Typ(){ Variable|Eigenschaft = , ...};

    Initialisieren Sie eine Variable bzw. Eigenschaft über den Konstruktor und zusätzlich über eine Initialisiererliste, wird zuerst der Konstruktor aufgerufen und danach der Wert durch die Initialisiererliste zugewiesen. Der Konstruktor wird also in jedem Fall zuerst ausgeführt.

    Beispiel: Objekte flexibel initialisieren

    Die Klasse Person besitzt eine öffentliche Variable (nur zu Demonstrationszwecken) und eine öffentliche Eigenschaft. Die Eigenschaft nutzt dabei die Spracherweiterung der automatisch implementierten Eigenschaften (Eigenschaften werden später noch ausführlich erläutert).

    Die Variable und die Eigenschaft werden nun auf drei unterschiedliche Weisen initialisiert (Name und Vorname, Name, Vorname). Normalerweise hätte man dazu drei Konstruktoren bereitstellen müssen. Durch die Verwendung der Objektinitialisierer entfällt dieser Aufwand.

    Zusätzlich zur Objektinitialisierung können auch andere Konstruktoren verwendet werden. Das runde Klammerpaar muss in diesem Fall vor der Objektinitialisierung stehen und es wird die explizite Angabe eines parameterlosen Konstruktors für die beiden anderen Objektinitialisierungen benötigt.

    namespace CSharpBuch.Kap05
    {
      public class ObjektInitialisierer
      {
        static void Main(string[] args)
        {
          Person p1 = new Person{ Name = "Meierle", Vorname = "Paule" };
          Person p2 = new Person{ Name = "Schulze" };
          Person p3 = new Person("Schmidt"){ Vorname = "Paula" };
        }
      }  
      public class Person
      {
        public string Name;
        public string Vorname { get; set; }
        public Person() {}
        public Person(string Name) { this.Name = Name; }
      }
    }
    Listing 5.14: ObjektInitialisierer.cs

    5.5.3 Destruktoren

    Der Destruktor wird aufgerufen, wenn ein Objekt zerstört wird. Er wird verwendet, um abschließende Arbeiten zu verrichten, beispielsweise Datenverbindungen zu beenden oder noch geöffnete Dateien zu schließen. Für die Freigabe des Speichers brauchen Sie bei Managed Code keinen Destruktor, dies erledigt der Garbage Collector automatisch im Hintergrund. Als Programmierer haben Sie keinen 100%igen Einfluss darauf, wann der Destruktor aufgerufen wird, dies ist unter anderem vom Ausführungszeitpunkt des Garbage Collectors abhängig. Wird ein Objekt nicht mehr benötigt, ruft der Garbage Collector den Destruktor auf und gibt den Speicherplatz frei. Dies geschieht spätestens dann, wenn das Programm beendet ist.
    Syntax von Destruktoren
  • Destruktoren können nur für Klassen, nicht für Strukturen bereitgestellt werden.
  • Sie können pro Klasse nur einen Destruktor verwenden. Dieser kann nicht vererbt werden.
  • Der Name des Destruktors entspricht dem Namen der Klasse mit einem vorangestellten Tilde-Zeichen (~), z.B. ~Buch().
  • Ein Destruktor besitzt keinen Modifizierer und keine Parameter (deshalb kann er also auch nicht überladen werden).
  • Der Destruktor wird automatisch aufgerufen, wenn das Objekt beseitigt wird.

    Beispiel: Aufruf des Destruktors beim Beseitigen eines Objekts

    Dem letzten Beispiel wird nun noch ein Destruktor für die Klasse Buch hinzugefügt. Dieser gibt eine Meldung auf der Konsole aus, wenn ein Objekt zerstört wird. Wenn Sie die Anwendung ausführen, werden Sie feststellen, dass der Destruktor erst nach dem Aufruf von ReadLine() aktiviert wird. Dies liegt daran, dass die Buch-Objekte zwar nicht mehr benötigt werden, der Garbage Collector aber noch nicht zum Einsatz gekommen ist.

    ~Buch()
    {
      Console.WriteLine("Buch-Objekt zerstört");
    }
    Listing 5.15: ObjekteErzeugen.cs

    TIPP

    Um den Zeitpunkt für die Freigabe von Ressourcen besser beeinflussen zu können, wird in .NET die Implementierung einer Dispose()-Methode vorgeschlagen, die aufgerufen wird, wenn ein Objekt nicht mehr benötigt wird. Mehr dazu erfahren Sie im Kapitel zu Interfaces, die in diesem Zusammenhang eine wichtige Rolle spielen.

    INFO

    Leere Destruktoren sollten nicht angegeben werden, sie führen zu Performanceverlusten. Es ist möglich, den Garbage Collector über GC.Collect() direkt aufzurufen. Dies ist jedoch nicht ratsam, da es meist ebenfalls zu Performance-Verlusten führt.

    5.6 Partielle Typdeklarationen

    Eine Typdeklaration, z.B. einer Klasse, muss in vielen Programmiersprachen immer am Stück erfolgen und befindet sich innerhalb einer einzigen Datei. Mit dem .NET Framework 2.0 wurden partielle Typdeklarationen eingeführt. Dazu wird eine Klasse, ein Interface oder eine Struktur nicht mehr an einer, sondern an mehreren Stellen "partiell" deklariert. Der Compiler übersetzt alle Sourcen und fügt dann "passende - gleichnamige" Typen zu einem einzelnen Typ zusammen. Dabei ist zu beachten, dass z.B. eine Methode mit der gleichen Signatur nicht in zwei partiellen Deklarationen derselben Klasse deklariert wird, da es dann zu einem Fehler kommt.

    Die Bereitstellung dieses Sprachfeatures hat folgende Vorteile:
  • Sie können nun umfangreiche Klassen in mehrere kleine Stücke aufteilen. Dadurch können z.B. mehrere Entwickler parallel an einer Klasse arbeiten, ohne dass es später Probleme bei der Zusammenführung des Sourcecodes gibt (unter der Voraussetzung, dass die Entwickler unterschiedliche Namen für ihre Methoden etc. verwenden).
  • Andere Anwendungsgebiete sind z.B. die Verknüpfung von automatisch generiertem Code mit manuell erzeugtem Sourcecode, ähnlich dem im vorigen Punkt erläuterten Beispiel. Es werden hier Probleme vermieden, die bei gleichzeitiger manueller und automatischer Bearbeitung ein und derselben Codedatei auftreten können.
  • Durch das Weglassen, Hinzufügen bzw. Austauschen von bestimmten, partiellen Dateien bei der Übersetzung können Sie auf einfache Weise Anwendungen mit unterschiedlicher Funktionalität erzeugen.
  • In Frameworks wie z.B. der Windows Presentation Foundation kann eine grafische Oberfläche in einer Datei, die zugehörige Logik in einer anderen bereitgestellt werden. Im "Hintergrund" werden beide auch mithilfe partieller Klassen zusammengeführt. In Windows Forms wird diese Vorgehensweise ebenfalls verwendet, um den automatisch generierten Code des Designers vom Code, den Sie als Programmierer schreiben, zu trennen.

    Beispiel: Eine partielle Klassendeklaration

    Einem partiellen Typ wird das Schlüsselwort partial vorangestellt.

    partial class Mathe
    {
    }

    Syntax der partiellen Deklaration
  • Eine partielle Deklaration kann aus beliebig vielen Teilen, d.h. auch nur aus einem Teil, bestehen. Sie kann für Klassen, Interfaces und Strukturen genutzt werden.
  • Die einzelnen partiellen Deklarationen dürfen sich nicht überschneiden, d.h., es darf beispielsweise nicht mehrere Methoden mit derselben Signatur geben.
  • Die einzelnen Teile eines Typs müssen sich im gleichen Namespace und der gleichen Assembly befinden sowie den gleichen Typ und Namen als auch denselben Zugriffsmodifizierer besitzen.
  • Alle Teile müssen im Sourcecode vorliegen, damit sie zusammen kompiliert werden können.
  • Jeder Teil einer Typdefinition muss mit dem Schlüsselwort partial versehen werden, das direkt vor class, interface oder struct anzugeben ist.
  • Sind einzelne Teile einer partiellen Typdefinition als abstract oder sealed gekennzeichnet, so gilt dies später auch für den gesamten Typ.
  • Ist ein partieller Typ von einer Klasse abgeleitet, so gilt dies später auch für den gesamten Typ. Dabei müssen alle Teile von dieser oder keiner Klasse abgeleitet sein.
  • Im zusammengefassten Typ werden außerdem alle Interfaces, Attribute und XML-Kommentare vereinigt. Hier ist zu beachten, dass die einzelnen partiellen Typen unterschiedliche Interfaces implementieren und unterschiedliche Attribute besitzen können.

    Beispiel: Aus mehreren partiellen Typdefinitionen wird ein Gesamttyp gebildet

    Das Hauptprogramm verwendet die Klasse Mathe, die wiederum durch zwei partielle Klassen in den Dateien Teil1.cs und Teil2.cs implementiert wird. Wichtig ist, dass für die Implementierung der Klasse Mathe immer der gleiche Namespace und die gleichen Zugriffsmodifizierer verwendet werden. Durch das Konzept der partiellen Klassen können nun z.B. komplexe Funktionen in eigene Dateien ausgelagert werden.

    using System;
    namespace CSharpBuch.Kap05
    {
      public class PartielleTypen
      {
        static void Main(string[] args)
        {
          Mathe m = new Mathe();
          Console.WriteLine("3 + 4 = " + m.Add(3, 4));
          Console.WriteLine("3 - 4 = " + m.Minus(3, 4));
          Console.ReadLine();
        }
      }
    }
    Listing 5.16: PartielleTypen.cs

    using System;
    namespace CSharpBuch.Kap05
    {
      public partial class Mathe
      {
        public int Add(int zahl1, int zahl2)
        {
          return zahl1 + zahl2;
        }
      }
    }
    Listing 5.17: Teil1.cs
    using System;
    namespace CSharpBuch.Kap05
    {
      public partial class Mathe
      {
        public int Minus(int zahl1, int zahl2)
        {
          return zahl1 - zahl2;
        }
      }
    }
    Listing 5.18: Teil2.cs

    5.7 Partielle Methoden

    Seit dem .NET Framework 3.5 gibt es neben partiellen Klassen auch partielle Methoden. Dabei werden Methoden in einer partiellen Klasse deklariert und in einer weiteren (oder derselben) implementiert. Allerdings gibt es bei partiellen Methoden einiges zu beachten:

  • Partielle Methoden sind implizit private. Die Angabe eines Zugriffsmodifizierers wie public oder protected ist nicht erlaubt.
  • Die Modifizierer abstract, extern, new, override, sealed und virtual sind nicht erlaubt. Allerdings dürfen partielle Methoden static sein.
  • Der Rückgabetyp ist immer void. Parameter darf die Methode zwar besitzen, allerdings sind keine out-Parameter erlaubt.
  • Wird eine partielle Methode nur deklariert, aber nicht implementiert, werden Deklaration und mögliche Verwendungen entfernt. Der letzte Punkt ist dann auch für das Haupteinsatzgebiet verantwortlich. Entwickler, Codegeneratoren u.a. können Klassen mit Methoden vorbereiten, inklusive deren Aufruf im Code. Ein weiterer Entwickler kann nun entscheiden, ob die Methode in einer weiteren partiellen Klasse implementiert - und damit auch verwendet wird. Wird sie nicht implementiert, hat dies allerdings keine Auswirkungen auf die Kompilierung und die Ausführbarkeit der Anwendung. Der Compiler entfernt einfach sämtliche Aufrufe und die Deklaration der Methode.

    Beispiel: Partielle Methoden (de)aktivieren

    Die Klasse Person wird über zwei partielle Klassen definiert. In der ersten Klasse befindet sich nur der Methodenrumpf, um eine Ausgabe durchzuführen. Außerdem besitzt sie eine öffentliche Methode AusgabeDurchfuehren(), welche die private Methode Ausgabe() aufruft.

    In einer zweiten partiellen Klasse Person wird nun die Methode Ausgabe() implementiert. Allerdings ist sie im Moment auskommentiert. Dies führt aber zu keinem Compiler- oder Laufzeitfehler. Es wird einfach die Methode vom Compiler ignoriert. Werden die Kommentarzeichen entfernt, wird die Konsolenausgabe durchgeführt.

    namespace CSharpBuch.Kap05
    {
      public class PartielleMethoden
      {
        static void Main(string[] args)
        {
          Person p = new Person();
          p.AusgabeDurchfuehren();
        }
      }  
      public partial class Person
      {
        public void AusgabeDurchfuehren()
        {
          Ausgabe();
        }    
        partial void Ausgabe(); // nur Deklaration
      }
      public partial class Person
      {
        /*** entfernen, um die Methode zu nutzen
        partial void Ausgabe()
        { 
          Console.WriteLine("Hallo");
        }
        */
      }
    }
    Listing 5.19: PartielleMethoden.cs

    5.8 Statische Klassen und Klassenelemente

    Sie haben bisher schon mehrfach Gebrauch von statischen Methoden gemacht, z.B. von der Methode Main(). Jetzt soll den statischen Teilen einer Anwendung etwas mehr Augenmerk geschenkt werden. Eine Klasse stellt ja so etwas wie einen Bauplan für Objekte dar. Jedes Objekt besitzt eine eigene Kopie der Datenelemente seiner Klasse (die nicht-statisch sind). Nicht-statische Methoden können nur über Objekte aufgerufen werden, da sie mit den nicht-statischen Datenelementen der Objekte arbeiten (d.h. mit ihren eigenen Kopien der Datenelemente). Sie existieren sozusagen nur in einem Objekt.

    In vielen Fällen ist es aber nicht notwendig bzw. sinnvoll, ein Objekt einer Klasse zu erzeugen, nur um deren Methoden zu nutzen. Allerdings zwingt das .NET Framework die Entwickler, alle Methoden in einer Klasse unterzubringen. Eine Lösung bieten statische Klassenelemente, welche direkt über eine Klasse, d.h. den Klassennamen, aufgerufen werden können, ohne dass eine Instanz der Klasse erstellt werden muss. Statische Methoden sind also vergleichbar mit den Funktionen und Prozeduren klassischer Programmiersprachen, mit der Ausnahme, dass sie sich in einer Klasse befinden.

    Beispiel: Statische Methoden nutzen

    Mathematische oder Zeichenkettenoperationen stellen ideale Kandidaten für statische Klassenelemente dar. So wird z.B. eine Methode zum Addieren zweier Zahlen mit den beiden Zahlen als Parameter aufgerufen und liefert die Summe als Ergebnis. Die Methode benötigt keine weiteren Informationen, d.h., sie benötigt insbesondere keinen Zugriff auf die Werte von Instanzvariablen, da kein Zustand über mehrere Methodenaufrufe hinweg gespeichert werden muss. Die Implementierung als statische Methode vermeidet, dass für den Aufruf der Methode erst ein Objekt erstellt werden muss.

    public class Mathe
    {
      public static int Add(int zahl1, int zahl2)
      {
        return zahl1 + zahl2;
      }
    }
    ... // und der Aufruf
    int ergebnis = Mathe.Add(10, 11);

    Statische Variablen werden auch als Klassenvariablen bezeichnet, da sie nur einmal innerhalb der Klasse existieren. Da statische Elemente immer genau einmal vorhanden sind, stehen sie auch in allen Objekten der Klasse zur Verfügung. Umgekehrt können statische Elemente nicht auf nicht-statische Klassenelemente zugreifen, da sie ja kein Objekt zur Verfügung haben, auf das die statischen Methoden zugreifen könnten. In statischen Methoden kann außerdem kein this-Zeiger (vgl. Kapitel 7) verwendet werden, da kein Objekt verfügbar ist, auf das this verweisen könnte.

    C# kennt drei verschiedene Einsatzgebiete des Schlüsselwortes static. Sie können es zusammen mit Variablen, Methoden und seit der Version 2.0 auch mit Klassen verwenden.
    Syntax und Verwendung
  • Statische Klassenelemente werden durch den Modifizierer static gekennzeichnet. Optional kann ein Zugriffsmodifizierer vorangestellt werden.
  • Der Rest der Deklaration entspricht der bisherigen Vorgehensweise.
  • Statische Methoden können nur auf statische Variablen zugreifen und andere statische Methoden direkt aufrufen.
  • Sie können static mit Methoden, Variablen, Eigenschaften, Operatoren, Ereignissen, Konstruktoren und Klassen verwenden.
  • Konstanten sind implizit statisch.
  • Wird eine Klasse mit static deklariert, müssen alle Klassenelemente statisch sein. Von statischen Klassen können keine anderen Klassen abgeleitet werden (sie sind automatisch sealed - vgl. Kapitel 7). Der Vorteil von statischen Klassen ist, dass von ihnen niemals Instanzen erstellt werden können und dies bereits durch den Compiler sichergestellt werden kann. Bisher konnten Sie dies nur über einen privaten Konstruktor verhindern. Der Vorteil dieser Vorgehensweise ist, dass Optimierungen durchgeführt werden können.

  • Statische Konstruktoren werden vor der ersten Verwendung der Klasse (Aufruf einer statischen Methode oder Erstellen einer Instanz) aufgerufen. Sie werden über den Modifizierer static und den Klassennamen, gefolgt von einem Klammerpaar, deklariert. Sie haben keine Parameter und keinen weiteren Zugriffsmodifizierer. Statische Klassenelemente werden initialisiert, bevor der statischen Konstruktor aufgerufen wird. Statische Konstruktoren können in statischen wie auch in "normalen" Klassen verwendet werden, um die statischen Bestandteile zu initialisieren.
  • Statische Methoden und Variablen werden direkt über den Klassennamen aufgerufen. Sie können keine statischen Elemente über eine Referenzvariable nutzen.
    [Modifizierer] static  Variablenname;
    [Modifizierer] static  Methodenname()
    [Modifizierer] static class Klassenname
    {}

    Beispiel: Statische Elemente werden direkt über eine Klasse verwendet

    Im Beispiel wird eine statische Klasse Mathe deklariert, welche nur statische Elemente enthält. Über die statische Variable aufrufe soll die Anzahl der Methodenaufrufe gezählt werden. Die Variable wird in einem statischen Konstruktor initialisiert. Über zwei statische Methoden wird die Addition durchgeführt und der Wert der Variablen aufrufe zurückgeliefert. Die Methoden der Klasse werden in der Methode Main() direkt über den Klassennamen aufgerufen. Die Verwendung einer solchen statischen Variablen ist z.B. für manuelle Statistik- und Durchsatzmessungen geeignet.

    Die statische Methode Format() der Klasse String dient der Ausgabe von formatierten Zeichenketten. Die verwendeten Platzhalter {0} werden durch die Werte der folgenden Parameter der Reihenfolge nach ersetzt.

    using System;
    namespace CSharpBuch.Kap05
    {
      public class StatischeElemente
      {
        static void Main(string[] args)
        {
          Console.WriteLine(String.Format("3 + 4 = {0}", Mathe.Add(3, 4)));
          Console.WriteLine(String.Format("6 + 5 = {0}", Mathe.Add(6, 5)));
          Console.WriteLine(String.Format("Mathe wurde {0} mal verwendet.", Mathe.GetAufrufe()));
        }
      }
      public static class Mathe
      {
        public const double PI = 3.14;
        private static int aufrufe;
        public static Mathe()
        {
          aufrufe = 0;
        }
        public static int Add(int zahl1, int zahl2)
        {
          aufrufe++;
          return zahl1 + zahl2;
        }
        public static int GetAufrufe()
        {
          return aufrufe;
        }
      }
    }
    Listing 5.20: StatischeElemente.cs

    Die Methode Main()

    Jetzt sollte auch klar geworden sein, warum die Methode Main() als static deklariert wird. Da noch keine Instanz der umgebenden Klasse existiert, wenn die .NET-Anwendung gestartet wird, muss diese Methode unabhängig von einem Objekt ausgeführt werden können. Die Angabe von public ist nicht notwendig, da die Anwendung über die Assembly gestartet wird, welche die Methode Main() enthält. Und da Main() implizit internal ist, kann die Methode auch aufgerufen werden. Als Rückgabetyp kommt neben void auch int in Frage. Weiterhin kann Main() mit einer Parameterliste string[] args aufgerufen werden. Dadurch ist es möglich, einer Anwendung Parameter zu übergeben (z.B. beim Aufruf über die Kommandozeile). Für die Implementierung der Methode Main() ergeben sich damit die folgenden vier Möglichkeiten:
    int Main()
    int Main(string[] args)
    void Main()
    void Main(string[] args)

    5.9 Erweiterungsmethoden

    Um einer Klasse eine Methode hinzuzufügen, muss die Methode normalerweise innerhalb der Klasse neu angelegt werden. Dies hat den Nachteil, dass die Klasse neu übersetzt werden muss und Änderungen wieder zum Neuübersetzen der Klasse führen. Erweiterungsmethoden weichen diese Vorgehensweise auf, indem einer Klasse Methoden über andere Klassen hinzugefügt werden können, selbst dann, wenn die zu erweiternde Klasse als sealed (nicht erweiterbar - vgl. Kapitel 7) gekennzeichnet ist. Der Vorteil der Erweiterungsmethoden ist, dass sie wie Methoden der erweiterten Klasse aufgerufen werden, obwohl sie an anderer Stelle bereitgestellt werden.

    Da Sie Erweiterungsmethoden durch weitere Assemblies bereitstellen können, bieten sich eventuell interessante Einsatzgebiete. Durch den Austausch der Assembly mit den Erweiterungsmethoden kann die alte Erweiterung z.B. durch eine neue Implementierung ausgetauscht werden oder Sie fügen neue Erweiterungen hinzu. Um eine Erweiterungsmethode zu erstellen, sind folgende Dinge notwendig:

  • Erstellen Sie eine neue statische Klasse. Wenn Sie die Klasse in einem anderen Namespace unterbringen, müssen Sie diesen später mit einbinden.
  • Jede Erweiterungsmethode muss statisch sein und als ersten Parameter ein Objekt vom Typ der zu erweiternden Klasse entgegennehmen. Diesem Parameter wird this vorangestellt. Die Erweiterungsmethode wird sozusagen auf ein Objekt vom Typ der zu erweiternden Klasse angewandt.
  • Befindet sich in einer Klasse bereits eine Methode, welche die gleiche Signatur wie eine Erweiterungsmethode besitzt (allerdings ohne den ersten Parameter und ohne static), hat diese Methode beim Aufruf immer Vorrang. Eine Fehlermeldung oder Warnung gibt es allerdings nicht.

    Sie können beispielsweise eine Erweiterung für die Klasse String erstellen, welche die Anzahl der Zeichen des Strings zurückliefert (also die Eigenschaft Length auswertet). Die Namen der Erweiterungsklasse und Erweiterungsmethode sind hierbei beliebig. Die Erweiterungsmethode kann nun wie jede andere Methode der Klasse String aufgerufen werden.
    public static class StringErweiterung
    {
      public static int ZeichenAnzahl(this string para)
      {
        return para.Length;
      }
    }
    ...
    string name = "Meierle";
    Console.WriteLine(name.ZeichenAnzahl());

    TIPP

    Auch wenn Erweiterungsmethoden ein interessantes Feature darstellen, sollten Sie damit sparsam umgehen. In großen Klassenbibliotheken kann die Trennung zwischen einem Typ und seinen Erweiterungsmethoden zu Unübersichtlichkeit und im schlechtesten Fall zu Fehlverhalten führen, wenn beispielsweise später eine Methode mit der Signatur der Erweiterungsmethode einer Klasse hinzugefügt wird (z.B. über Vererbung).

    Beispiel: Klassen um Methoden erweitern

    Die Klasse Person besitzt zwei öffentliche Instanzvariablen. Diese sollen mit einer Methode ausgegeben werden, die als Erweiterungsmethode implementiert wird. Dazu wird eine weitere statische Klasse PersonErweiterung bereitgestellt, welche die Methode Ausgabe() besitzt. Als erster Parameter muss ein Objekt vom Typ der zu erweiternden Klasse übergeben werden. Der zweite Parameter soll als Trennzeichen für die Ausgabe dienen. Zum Test wird ein Person-Objekt über einen Objektinitialisierer erzeugt und danach wird für das Objekt die Methode Ausgabe() aufgerufen.

    using System;
    namespace CSharpBuch.Kap05
    {
      public class ErweiterungsMethoden
      {
        static void Main(string[] args)
        {
          Person p = new Person { Name="Meierle", Vorname="Paule" };
          p.Ausgabe(", ");
        }
      }  
      public class Person
      {
        public string Name;
        public string Vorname;
      }  
      public static class PersonErweiterung
      {
        public static void Ausgabe(this Person pers, string trenner)
        {
          Console.WriteLine(pers.Name + trenner + pers.Vorname);
        }
      }
    }
    Listing 5.21: ErweiterungsMethoden.cs

    5.10 Überladen von Operatoren

    Die standardmäßig in C# vorhandenen Operatoren arbeiten nur mit einigen Datentypen. Es kann aber durchaus gewünscht sein, auch für eigene Typen bestimmte Operatoren zur Verfügung zu haben. So können z.B. für Klassen zur Bruchrechnung mathematische Operatoren definiert werden, damit mit Bruch-Objekten gerechnet werden kann. Eine andere (theoretische) Anwendungsmöglichkeit wäre, dass für eine Klasse Maschinenteil eine Addition definiert wird, so dass aus mehreren Maschinenteilen eine Maschine erzeugt werden kann (rein softwaretechnisch natürlich). Es lassen sich zwar nicht alle Operatoren überladen, aber die meisten. Die folgenden Operatoren können Sie dazu verwenden:
    + - * / % & | ^ ! ~ ++ -- true, false << >> == != < > <= >=
    Syntax zum Überladen von Operatoren
  • Das Überladen eines Operators geschieht durch die Implementierung einer Methode innerhalb der Klasse, für die der Operator verwendet werden soll.
  • Die Methode muss als public und static definiert werden.
  • Nach dem Rückgabetyp, der im Falle der arithmetischen Operatoren dem Typ der Operatorklasse entspricht, wird das Schlüsselwort operator angegeben, gefolgt vom Operatorsymbol. Zwischen beiden kann optional ein Leerzeichen stehen.
  • Als Parameter können keine ref- und out-Parameter verwendet werden. Mindestens einer der Parameter muss dem Typ entsprechen, in dem sich die Operatormethode befindet.
  • Benutzerdefinierte Operatoren haben immer Vorrang vor den vordefinierten Operatoren.
  • Beim Überladen einiger Operatoren ist darauf zu achten, dass diese immer paarweise überladen werden müssen, z.B. < und >. Für einige Operatoren ist es außerdem erforderlich, dass bestimmte Methoden wie Equals() und GetHashCode() überschrieben werden.
  • Neben den "normalen" Operatoren können Sie Typumwandlungsoperatoren mittels der Schlüsselwörter explicit und implicit definieren. Mehr zu den beiden Schlüsselwörtern finden Sie in der Hilfe.
    public static  operator [Symbol](Operand1, ...)
    {
    }

    Beispiel: Operatoren für eigene Typen bereitstellen

    Zur Bruchrechnung existiert im .NET Framework noch keine geeignete Klasse. Die folgende Klasse Bruch besitzt zwei private Instanzvariablen, welche den Wert des Zählers und des Nenners verwalten, einen Konstruktor, mit dem beide Werte initialisiert werden, zwei Get-Methoden, um den Zähler und Nenner des Bruchs abzufragen, einen überladenen Operator zur Addition zweier Brüche und eine Methode ToString(), welche eine geeignete Textdarstellung eines Bruches zurückliefert. In der Methode Main() werden zum Test zwei Brüche addiert und das Ergebnis wird ausgegeben.

    using System;
    namespace CSharpBuch.Kap05
    {
      public class OperatorenUeberladen
      {
        static void Main(string[] args)
        {
          Bruch b1 = new Bruch(3, 4);
          Bruch b2 = new Bruch(7, 5);
          Bruch b3 = b1 + b2;
          Console.WriteLine(b1.ToString() + "+" + 
                            b2.ToString() + "=" + 
                            b3.ToString());
        }
      }
      public class Bruch
      {
        private int zaehler;
        private int nenner;
        public Bruch(int zaehler, int nenner)
        {
          this.zaehler = zaehler;
          this.nenner = nenner;
        }
        public int GetZaehler()
        {
          return zaehler;
        }
        public int GetNenner()
        {
          return nenner;
        }
        public static Bruch operator +(Bruch op1, Bruch op2)
        {
          int nenner = op1.nenner * op2.nenner;
          int zaehler = nenner / op1.nenner * op1.zaehler +
                        nenner / op2.nenner * op2.zaehler;
          Bruch b = new Bruch(zaehler, nenner);
          return b;
        }
        public override string ToString()
        {
          return zaehler.ToString() + "/" + nenner.ToString();
        }
      }
    }
    Listing 5.22: OperatorenUeberladen.cs

    TIPP

    Beachten Sie beim Überladen eines Operators, dass Sie dabei seine Bedeutung nicht verschleiern. So kann man sich sicher schwer vorstellen, wie zwei DVD-Objekte voneinander abgezogen oder multipliziert werden sollen.

    5.11 Übungsaufgaben

    Aufgabe 1

    Erstellen Sie eine Klasse, welche das Einlesen von Zahlen von der Konsole vereinfachen soll. Die Klasse besitzt einen parameterlosen Standardkonstruktor sowie einen weiteren Konstruktor, dem die Eingabeaufforderungen für die Zahlen als Zeichenketten übergeben werden können.

    Aufgabe 2

    Erweitern Sie die Klasse Bruch um Operatoren für die Subtraktion, Multiplikation und Division. Testen Sie die Operatoren in einer Anwendung.

    Aufgabe 3

    Erstellen Sie in einer beliebigen Klasse eine Methode, welche als Parameter eine int-Zahl entgegennimmt. Weiterhin soll sie zwei out-Parameter besitzen, welche die Anzahl der Stellen dieser Zahl sowie die Quersumme zurückgeben.

    Die Zahl 12345 hat beispielsweise fünf Stellen und die Quersumme ist 15 (1+2+3+4+5).

    Aufgabe 4

    Implementieren Sie die Erweiterung der Klasse Bruch aus Aufgabe 2 über partielle Klassen. Erstellen Sie dazu zwei C#-Dateien, die jeweils einen Teil der Implementierung der Klasse Bruch enthalten.

    Aufgabe 5

    Erstellen Sie eine Struktur, welche zum Verwalten eines gekauften Warenartikels dient. Sie enthält Felder für den Artikelnamen, den Nettopreis, den Bruttopreis sowie den Rabatt. Erstellen Sie ein Array von einigen Elementen dieser Struktur und füllen Sie die Werte der Felder für den Namen und den Nettopreis (siehe das Beispiel zum Thema Strukturen bzw. Kapitel 10).

    Übergeben Sie das Array sowie einen double-Parameter per ref-Parameterübergabe an eine Methode. Diese Methode berechnet für jedes Element der Struktur den Bruttopreis und gibt 3% Rabatt, wenn der Nettopreis größer als 100 Euro ist. Im ref-Parameter wird der Gesamtbruttobetrag zurückgegeben.