Advanced  Services Klassen und Objekte

Klassen als Funktionsbibliotheken sind im Grunde keine typischen Klassen. Die eigentlichen Fähigkeiten kommen dabei garnicht zum tragen. Das wird hiermit geändert.

Klassen als (nichtprimitive) Datentypen

Man kann Klassen sehr unterschiedlich erklären. Eine einfache Möglichkeit ist es, eine Klasse als Datentyp aufzufassen. Eine Klasse ist ein nichtprimitiver Datentyp und stellt damit keinen Werttyp dar, sondern einen Referenztyp wie etwa die Klasse ArrayList. Das Anlegen von Variablen verläuft daher nach dem schon bekannten Schema, wie das folgende Beispiel zeigt.

public class NeueDatentypen
{
   public static void Main(string[] args)
   {
      int a ;
      char arr[] ;
      String st;
      StringBuilder sb ;
      System.Collections.ArrayList al;
      System.Windows.Forms.Button butt;
      System.IO.StreamReader sr;
   }
}

Im obigen Beispiel ist nur int ein Werttyp, alle anderen Typen sind Referenztypen. Wir haben auch bereits erkannt, daß eine Arrayvariable, hier also arr, eine Referenz darstellt. Ebenso wie die Variable a sind auch alle anderen Variablen noch nicht initialisiert.

Wir wissen bereits, wie man eine Arrayreferenz initialisiert. Wie aber schaut es mit den anderen Referenzen aus. Den Datentyp String können wir uns noch recht gut vorstellen, "Hello C#" etwa ist ein String und vielleicht kann man einen String einfach mit st = "Aha" initialisieren. Es gibt jedoch mit der Klasse StringBuilder eine zusätzliche Möglichkeit Zeichenketten zu speichern. String und StringBuilder sind zwar thematisch verwandte Klassen, aber konzeptionell sehr unterschiedlich. Wie initialisiert man einen Objekt vom Typ StringBuilder ? Ein Buttonobjekt ist ein graphisches Objekt, das zunächst im Hauptspeicher realisiert wird und dann bei Bedarf auf dem Bildschirm angezeigt wird. Ein ArrayListobjekt ist eine Art dynamisches Array, in das man (praktisch) beliebig viele Elemente aufnehmen kann und das sich im Hintergrund bei Bedarf automatisch vergrößert. Ein StreamReaderObjekt stellt nach der Initialisierung eine zum Lesen geöffnete Datei dar. Jedes dieser Objekte hat einen eigenen Datentyp und damit eine eigene Initialisierungssituation. Und wie diese Initialisierung genau ausschaut weiß nur derjenige, der die Klasse geschrieben hat. Wie aber initialisiert man Objekte, von denen man garnicht weiß, wie man sie initialisieren soll ? Dies klärt der nächste Abschnitt.


Konstruktoren

Die Antwort ist verblüffend einfach. Der Entwickler der Klasse muß für seine Klasse einen entsprechenden Initialisierungsdienst bereitstellen. Nach einem genau festgelegten Schema stattet er seine Klasse mit speziellen Methoden aus, mit denen der Anwendungsprogrammierer Objekte der Klasse anlegen kann. Diese speziellen Methoden heißen Konstruktoren. Wir müssen also garnicht Bescheid wissen über das Innenleben der Objekte, wir initialisieren die Objekte mit Hilfe dieses Dienstes. Das sieht folgendermaßen aus :

using System.IO.*;
using System.Collections.*;
using System.Windows.Forms.*;

public class NeueDatentypen2
{
   public static void Main(string[] args)
   {
      int a ;
      char arr[] ;
      String st1, st2;
      StringBuilder sb ;
      System.Collections.ArrayList al;
      System.Windows.Forms.Button butt; // benötigt den Eintrag System.Windows.Forms unter References
      System.IO.StreamReader sr;

      arr = new char[] { 'a' , 'g' , 'a' , 'p' , 'i' } ;
            // Array arr wird initialisiert

      st1 = new String(arr) ;
            // String st1 wird mit Hilfe von arr initialisiert

      sb  = new StringBuilder(st1);
            // StringBuilder sb wird mit Hilfe von st1 initialisiert

      al = new ArrayList();
            // ein ArrayListobjekt wird mit Hilfe eines Defaultkonstruktors initialisiert

      butt = new Button();
            // ein Buttonobjekt wird initialisiert

      //StreamReader sr = new System.IO.StreamReader("c:\\eineDatei.dat");
      // ein StreamReaderObjekt wird mit Hilfe eines Konstruktors initialisiert, der
      // einen Dateinamen als String erhält (d.h. die Datei wird geöffnet)

      st2 = "Hier brauchen wir ausnahmsweise keinen Konstruktor" ;
   }
}

Im obigen Beispiel ist der Aufruf des Konstruktors der Klasse StreamReader auskommentiert. Der Aufruf an sich ist zwar korrekt, aber in diesem Falle kann es beim Ablauf des Programms zu einem Laufzeitfehler (Exception, Ausnahme) kommen, wenn die Datei nicht existiert und das ist hier noch nicht unser Thema.

Eine Klasse kann keinen, einen oder mehrere Konstruktoren zur Verfügung stellen. Stellt eine Klasse keinen Konstruktor bereit, so kann man von ihr keine Objekte anlegen. Allerdings gibt es eine Ausnahme von dieser Regel. Es gibt Klassen, die an Stelle eines Konstruktors eine statische Methode bereitstellen, die ein Objekt vom Typ der Klasse zurückliefert. Darauf gehen wir hier aber nicht näher ein. Bietet eine Klasse einen oder mehrere Konstruktoren an, so werden diese zur Initialisierung verwendet. Wie man oben sieht, erinnert die Syntax stark an die Initialisierung von Arrays. Nach dem new Operator kommt der Name des Datentyps bzw. der Klasse. Im Gegensatz zu Arrays werden dann aber runde Klammern, also Methodenklammern verwendet. Den Konstruktoren können Parameter übergeben werden, aber es gibt auch Konstruktoren ohne Parameter. Ein parameterloser Konstruktor wird Defaultkonstruktor genannt.

Aus der Dokumentation (API) können wir z.B. entnehmen, daß ein Aufruf wie new Button() eine Schaltfläche anlegt. Wie der Konstruktor das im Detail macht, bleibt uns verborgen. In Analogie zu Arrays kann man sich jedoch folgendes Bild machen.

Im ersten Schritt wird nur die Referenz angelegt:    Frame  wnd ;


Nun legen wir das FrameObjekt mit Hilfe eines Konstruktors an:   wnd = new Frame("Ich bin ein Fenster") ;

Nach dem Konstruktoraufruf haben wir die folgende Situation:



Da wir nicht wissen, wie der Konstruktor ein Objekt anlegt, kennen wir auch den Inhalt eines Objekts nicht. Anders als bei einem Array können wir hier nicht direkt auf den Speicher zugreifen. Stattdessen bietet eine Klasse Methoden an. Mit diesen Methoden kann man nun auf das Objekt zugreifen. Je nachdem, mit welchen Methoden der Entwickler die Klasse ausgestattet hat, kann man etwa auf Teile des Objektes lesend oder schreibend zugreifen. So gibt es für ein Buttonobjekt etwa Möglichkeiten um den Titel zu setzen oder zu ändern, oder die Größe des Buttons oder die Farbe. Ganz andere Methoden stellt die Klasse String bereit. Mit diesen kann man etwa die Länge des StringObjekts erfahren, oder ein bestimmtes Zeichen im String suchen oder einen StringObjekt mit einem anderen (lexikalisch) vergleichen.

Wie die obige Graphik veranschaulicht, gibt es einen Unterschied zwischen der Referenz (dem Zeiger) und dem eigentlichen Objekt bzw. Instanz. Es ist gut, sich das einmal klarzumachen. In der Praxis werden die Begriffe Referenz, Objekt und Instanz aber synonym verwendet, ohne daß das zu Mißverständnissen führt.

Zusammenfassung


Nichtstatische versus statische Methoden

Eine Klasse kann sowohl statische als auch nichtstatische Methoden anbieten. Von den statischen Methoden wissen wir bereits, daß sie mit dem Namen der Klasse verwendet werden, sie haben also nichts mit den Objekten der Klasse zu tun. Ganz anders verhält es sich mit den nichtstatischen Methoden. Will man Eigenschaften eines Objektes darstellen oder verändern, so braucht man Methoden, die auf genau dieses Objekt wirken. Dazu dienen nichtstatische Methoden. Nichtstatische Methoden werden immer zu einem Objekt aufgerufen und können auf dieses Objekt wirken. Die gleiche Methode kann dann bei unterschiedlichen Objekten (der gleichen Klasse natürlich) auch unterschiedliche Wirkung haben.

Zusammenfassung


Die using-Anweisung

Es gibt in der Standardklassenbibliothek mittlerweile tausende von Klassen. Mal abgesehen davon, daß man diese nicht alle im Kopf haben kann und deswegen eine Dokumentation unumgänglich ist, ist es auch notwendig, diese große Anzahl zu ordnen. Dazu haben die Entwickler die Klassen in verschiedene Pakete gepackt. Ein Paket enthält eine Reihe von Klassen und kann auch selbst wieder ein Paket enthalten. Die Namen der Pakete sind Teil des Klassennamens, so heißt die Klasse String genau genommen System.String und die Klasse Random heißt mit ihrem vollständigen Namen System.Random . Spricht man eine Klasse mit ihrem vollständigen Namen an, so wird sie der Compiler immer als Standardklasse erkennen. Es ist jedoch sehr umständlich, mit diesen langen Klassennamen zu hantieren. Glücklicherweise ist das auch nicht notwendig. Mit Hilfe der using-Anweisung kann man dem Compiler Pakete bekanntmachen, sodaß man nicht mehr den vollständigen Klassennamen angeben muß.


Ohne Verwendung der using-Anweisung
class Program
{
   static void Main(string[] args)
   {
      System.String st = "hallo";
      string st2 = "helau";
      System.Collections.ArrayList arrayList  = new System.Collections.ArrayList();
      System.Collections.Generic.List<String> list  = new System.Collections.List<String>();
      System.Random rd = new System.Random();
      System.IO.StreamReader sr = new System.IO.StreamReader("c:\\eineDatei.dat");
   }
}

Mit Verwendung der using-Anweisung
using System;
using System.IO;  // für Streamreader
using System.Collections; // für ArrayList
using System.Collections.Generic; // für List

class Program
{
   static void Main(string[] args)
   {
      String st = "hallo";
      string st2 = "helau";
      Random rd = new Random();
      ArrayList al  = new ArrayList();
      List<String> list  = new List<String>();
      StreamReader sr = new StreamReader("c:\\eineDatei.dat");
   }
}

Man sieht hier nebenbei, daß string ein Alias für System.String ist und nicht für String alleine. Die Paketordnung mit using ist aufgebaut wie eine Verzeichnisstruktur, wobei die Unterverzeichnisse nicht automatisch bekannt sind sondern eigens bekanntgemacht werden müssen. So benötigt etwa ArrayList explizit ein using System.Collections; und StreamReader explizit ein using System.IO;.