Programmieren in C#: Einführung


Kapitel 2: Objektorientierung


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


2.1 Allgemeines

Die Objektorientierung als heute wichtigstes Programmierparadigma

Unter der Objektorientierung versteht man eine bestimmte Herangehensweise an ein Problem und eine bestimmte Organisationsform zur Lösung dieses. Herangehensweise und Organisationsform werden allgemein als Programmierparadigma bezeichnet. Die Objektorientierung ist dabei nur eines von vielen Paradigmen, die es in der Welt der Softwareentwicklung gibt. Die Objektorientierung ist für die Programmierung mit C# aber entscheidend, da alle C#-Programme immer objektorientiert sind. In C# ist die Objektorientierung zwingend vorgeschrieben. Es ist daher wichtig, dass Sie sich mit der Objektorientierung vertraut machen und verstehen, wie objektorientierte Programme in C# erstellt werden.


2.2 Paradigma der Objektorientierung

Sinn und Zweck der Objektorientierung

Die Grundannahme der Objektorientierung ist die, dass wir Menschen in Objekten denken. Wenn Sie sich umschauen und das beschreiben müssten, was Sie sehen, nennen Sie die Dinge beim Namen: Sie sehen zum Beispiel einen Schreibtisch vor sich, auf dem ein Computer steht. Hinter dem Schreibtisch ist möglicherweise ein Fenster, vor dem sich eventuell eine Pflanze befindet. Schreibtisch, Computer, Fenster und Pflanze sind grundsätzlich alles erstmal Objekte.

Wenn Sie objektorientiert programmieren, identifizieren Sie wesentliche Objekte, die für Sie und Ihr Programm eine entscheidende Rolle spielen. Möchten Sie zum Beispiel eine Software entwickeln, mit der eine Bibliothek ihren Bücherbestand verwalten kann, denken Sie wohlmöglich an Objekte wie Buch, Stellplatz im Regal, Ausleiher etc. Bei einer Software für eine Bank zur Verwaltung von Konten würden Ihnen Objekte wie Konto, Kontoinhaber, Bankauszug etc. einfallen. Würden Sie ein Rennspiel entwickeln, kämen Ihnen Objekte wie Rennauto, Rennstrecke, Boxengasse etc. in den Sinn. Die Objektorientierung versucht, unserer menschlichen Denkweise entgegen zu kommen, indem genau diese Objekte, an die wir denken, in unser Programm hinüber genommen werden können.

Wenn Sie noch keine Programmiererfahrung haben und C# Ihre erste Programmiersprache ist, wird Ihnen der Ansatz der Objektorientierung (hoffentlich) logisch vorkommen. Andere Programmiersprachen als C#, die auf anderen Paradigmen als der Objektorientierung basieren, erfordern jedoch eine andere Herangehensweise. So ist es möglich, sich einer Aufgabenstellung nicht über Objekte zu nähern, sondern über Abläufe. So kann man sich eine Verwaltungssoftware für eine Bibliothek auch so vorstellen, dass zuerst nach einem Buch gefragt wird, dann überprüft wird, ob es momentan verfügbar ist, und dann das Buch verliehen wird. Die Software für die Bank würde Aktionen wie Konto eröffnen, Geld anlegen, Geld abheben oder Kontoauszug drucken unterstützen. Auch das Rennspiel ließe sich als Aneinanderreihung von Abläufen darstellen.

In C# ist die Objektorientierung zwingend vorgeschrieben. Wollen Sie nicht objektorientiert programmieren, weil Ihnen ein anderes Paradigma logischer erscheint, müssen Sie die Programmiersprache wechseln. Heute ist es jedoch so, dass die wichtigsten und populärsten Programmiersprachen alle objektorientiert sind.

Wenn Sie Objekte identifiziert haben, die Sie für Ihr Programm als wichtig erachten, müssen Sie sie beschreiben. Denn ein Computer weiß natürlich nicht, was ein Buch, ein Konto oder ein Rennauto ist. Die Beschreibung erfolgt dabei in der Art, dass Sie Eigenschaften und Fähigkeiten der Objekte identifizieren. Eigenschaften und Fähigkeiten werden als Merkmale eines Objekts bezeichnet.

Ein Buch zum Beispiel hat einen Titel und kann ausgeliehen werden. Ein Konto hat einen Kontostand, der verringert oder erhöht werden kann. Ein Rennauto gehört einem Rennstall und kann beschleunigt und abgebremst werden. Titel, Kontostand und Rennstall wären in diesem Fall Eigenschaften der Objekte. Ausleihen, Ein- und Auszahlen und Beschleunigen und Abbremsen wären Fähigkeiten. Im Allgemeinen lassen sich Eigenschaften und Fähigkeiten recht einfach identifizieren: Während Eigenschaften statisch sind und den Zustand eines Objekts beschreiben, sind Fähigkeiten dynamisch und ermöglichen, den Zustand eines Objekts zu ändern.

Sind für ein Programm wichtige Objekte identifiziert worden und wurden deren Eigenschaften und Fähigkeiten herausgearbeitet, müssen sie in C# beschrieben werden. Eine derartige Beschreibung eines Objekts nennt man Klasse.


2.3 Klassen

Beschreibungen von Objekten

Im Einführungskapitel hatten Sie erfahren, dass das .NET-Framework zahlreiche Klassen zur Verfügung stellt. Diese Klassen wurden von Microsoft entwickelt, um einen einfachen Zugriff auf Betriebssystemfunktionen zu ermöglichen. Eine Klasse hatten Sie im Beispielprogramm im Einführungskapitel auch bereits verwendet: Über die Klasse Console kann auf die Konsole zugegriffen werden.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    Console.Out.WriteLine("Hallo, Welt!"); 
  } 
} 

Der Vorteil des .NET-Frameworks ist es, dass Microsoft zahlreiche in der Praxis nützliche Klassen bereits entwickelt hat und Sie sie nicht selbst entwickeln müssen. Da sehr viele Programme Daten in irgendeiner Form ein- oder ausgeben müssen, ist es nur praktisch, wenn Entwickler auf eine fertige Klasse Console aus dem .NET-Framework zugreifen können.

Der Zugriff auf die Klasse Console im Beispielprogramm sah so aus, dass über einen Punkt, der hinter dem Klassennamen angegeben wurde, auf etwas namens Out zugegriffen wurde. Dieses Out stellt eine Eigenschaft der Klasse Console dar. Microsoft hat in der Klasse Console also eine Eigenschaft definiert, die Out heißt.

Die Eigenschaft Out repräsentiert die sogenannte Standardausgabe. Neben der Standardausgabe gibt es auch die Standardeingabe und die Standardfehlerausgabe. Diese werden durch die Eigenschaften In und Error repräsentiert. Diese Ein- und Ausgabemechanismen sind keine Erfindung Microsofts, sondern existieren auch auf anderen Betriebssystemen. Viele Entwickler sind mit diesem sehr grundlegenden Ein- und Ausgabemechanismus vertraut. Der Zusammenhang zwischen der Konsole und der Standardein-, Standardaus- und Standardfehlerausgabe wurde von Microsoft objektorientiert so ausgedrückt, dass die Klasse Console drei Eigenschaften In, Out und Error hat.

Im Beispielprogramm aus dem Einführungskapitel wurde auf die Standardausgabe Out zugegriffen, um Hallo, Welt! auszugeben. Dies erfolgte über den Aufruf einer Methode namens WriteLine(). Der Name dieser Methode wurde hinter Out und einem weiteren Punkt angegeben.

In einem anderen Beispielprogramm im Einführungskapitel wurde über In auf die Standardeingabe zugegriffen, um mit Hilfe einer Methode ReadLine() eine Zeile einzulesen.

Methoden sind der Mechanismus, den objektorientierte Programmiersprachen anbieten, um Fähigkeiten eines Objekts abzubilden. Sie können sich eine Methode als eine Ansammlung von Befehlen vorstellen, die nacheinander ausgeführt werden, wenn die Methode aufgerufen wird. Methoden können Eingabewerte akzeptieren, die innerhalb der Methode verwendet werden. Methoden können auch demjenigen, der die Methode aufruft, ein Ergebnis zurückliefern. Sie lernen Methoden im Detail im Abschnitt 3.3, „Methoden“ kennen.

Da Out die Standardausgabe repräsentiert und die Standardausgabe die Fähigkeit besitzt, Daten auszugeben, sollte Out eine entsprechende Methode besitzen, die genau diese Fähigkeit repräsentiert. Microsoft hat diese Methode WriteLine() genannt. Daten, die in runden Klammern als Parameter an WriteLine() übergeben werden, werden von dieser Methode auf die Standardausgabe ausgegeben.

Console ist eine typische Klasse aus dem .NET-Framework. Sie beschreibt ein bestimmtes Objekt - in diesem Fall also die Eingabeaufforderung unter Windows - und bietet Eigenschaften und Methoden an, um mit diesem Objekt zu arbeiten. Eigenschaften sind ihrerseits nichts anderes als Objekte, die ebenfalls wiederum Eigenschaften und Methoden anbieten können. Sie können sich über verschachtelte Objekte bis hin zu einer Methode vorhangeln, indem Sie jeweils einen Punkt und dahinter die entsprechende Eigenschaft oder Methode angeben.

Der Punkt heißt Zugriffsoperator, weil Sie über den Punkt auf Eigenschaften oder Methoden eines Objekts zugreifen. Da Eigenschaften ihrerseits Objekte sind, kann es durchaus vorkommen, dass mehrmals auf den Punkt zugegriffen wird - so wie bei Console.Out.WriteLine("Hallo, Welt!") geschehen.

Die Klasse Console mussten Sie nicht selbst definieren, weil sie von Microsoft im .NET-Framework zur Verfügung gestellt wird. Sie müssen aber immer mindestens eine eigene Klasse definieren, wenn Sie ein Programm mit C# entwickeln wollen.

Das Beispielprogramm aus dem Einführungskapitel besteht aus einer Klasse Program. Sie erkennen die Klassendefinition am Schlüsselwort class. Die Klasse heißt Program, weil sie die Beschreibung eines Programms sein soll. Während ich den Namen in diesem Beispiel so festgelegt habe, können Sie Ihre Klasse aber auch ganz anders nennen. C# gibt Ihnen nicht vor, wie Ihre Klasse zu heißen hat. Der Klassenname Program macht aber insofern Sinn als dass diese Klasse ein Programm beschreiben soll.

Die Klasse Program ist so definiert, dass sie eine einzige Methode namens Main() besitzt. Diese Methode mit dem Rückgabewert void und einem Parameter vom Typ string[] muss statisch sein, damit sie als Einstiegspunkt bei der Programmausführung verwendet werden kann. Ist in Ihrem Visual C#-Projekt diese Methode in einer einzigen Klasse definiert, verwendet der Compiler sie automatisch als Einstiegspunkt. Sollte Ihr Visual C#-Projekt aus mehr als einer Klasse bestehen und Sie in mehreren Klassen diese Main()-Methode definiert haben, müssen Sie über einen Kommandozeilenparameter /main dem Compiler mitteilen, welche Klasse als Startobjekt verwendet werden soll.

Das Beispielprogramm aus dem Einführungskapitel besteht also aus einer Klasse, die ein Programm beschreibt und daher Program genannt wurde. Diese Klasse hat genau eine Methode namens Main(), die zwingend notwendig ist, damit das Programm ausgeführt werden kann. Main() repräsentiert also die Fähigkeit der Klasse Program, als Startobjekt verwendet zu werden.

Um das Beispielprogramm vollständig zu verstehen, müssen wir uns anschauen, was void und string[] sind. Dazu müssen Sie aber erst lernen, wie Klassen eingesetzt werden und was Referenz- und Wertvariablen sind.


2.4 Instantiierung

Objekte erstellen und in Referenzvariablen verankern

Wenn Sie eine Klasse definieren, erstellen Sie eine Beschreibung eines Objekts. Die Klasse selbst ist kein Objekt, sondern nur die Beschreibung eines Objekts. Sie können das vergleichen mit dem Grundriss eines Hauses: Der Grundriss beschreibt ein Haus. Das heißt aber nicht, dass Sie bereits ein fertiges Haus haben, in das Sie einziehen können.

Nur weil Sie eine Klasse definiert haben, haben Sie also noch kein Objekt, mit dem Sie arbeiten können. Um ein derartiges Objekt zu erhalten, müssen Sie die Klasse instantiieren. Der Vorgang, ein Objekt basierend auf einer Klasse zu erzeugen, wird Instantiierung genannt. In C# geschieht dies mit dem Schlüsselwort new. Mit new wird ein Objekt erzeugt, das genau die Merkmale besitzt, die in der entsprechenden Klasse definiert sind.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    new Uri("http://www.highscore.de/"); 
  } 
} 

Im .NET-Framework existiert im Namensraum System eine Klasse Uri. Die Klasse Uri heißt so, weil sie URIs beschreibt. Wenn Sie ein erfahrener Internetnutzer sind, kennen Sie URIs seit langem: Über URIs werden unter anderem Webseiten identifiziert. So ist zum Beispiel http://www.highscore.de/ eine URI. Möglicherweise ist Ihnen der Begriff URL besser bekannt: URLs sind eine Sonderform von URIs.

Wie Sie inzwischen wissen ist eine Klasse eine Beschreibung eines Objekts, die erst instantiiert werden muss, um ein Objekt zu erhalten. Im obigen Programm wird Uri instantiiert, indem das Schlüsselwort new gefolgt vom Klassennamen angegeben wird.

Wie Sie sehen wird hinter dem Klassenamen außerdem in runden Klammern "http://www.highscore.de/" übergeben. Mit dieser Angabe wird das entsprechende Objekt initialisiert. Während die Instantiierung den Vorgang beschreibt, basierend auf einer Klasse ein Objekt zu erzeugen, ermöglicht die Initialisierung, das entsprechende Objekt auf einen bestimmten Startzustand zu setzen.

Da URIs Ressourcen wie beispielsweise Webseiten beschreiben, wäre es schön, wenn das entsprechende Objekt mit einer Adresse auf eine Webseite initialisiert werden kann. Die Klasse Uri unterstützt eine derartige Initialisierung. Genaugenommen erzwingt sie sie sogar. So muss bei der Instantiierung von Uri in runden Klammern eine Adresse angegeben werden, um das entsprechende Objekt zu initialisieren.

Sie wissen nun, wie Sie ein Objekt erstellen und es initialisieren können. Einmal erstellt möchte man mit dem Objekt natürlich irgendwie arbeiten - sonst würde es keinen Sinn machen, es zu erstellen. Man möchte also auf Merkmale des Objekts zugreifen, die in der entsprechenden Klasse, auf der das Objekt basiert, definiert sind. Nur wie macht man das?

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    Uri uriHighscore = new Uri("http://www.highscore.de/"); 
  } 
} 

Das Objekt, das mit new erstellt wird, muss irgendwie identifiziert werden können, damit es im Programm verwendet werden kann. Dazu muss es in einer sogenannten Referenzvariablen verankert werden. Im obigen Beispiel heißt die Referenzvariable uriHighscore.

Die Referenzvariable heißt so, weil sie ein Objekt referenziert. Weil jedes Objekt auf einer ganz bestimmten Klasse basiert, muss ihr der entsprechende Klassenname vorangestellt werden.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    var uriHighscore = new Uri("http://www.highscore.de/"); 
  } 
} 

Seit C# 3.0 kann der Referenzvariablen auch das Schlüsselwort var vorangestellt werden, da der Compiler in der Lage ist, die entsprechende Klasse abzuleiten - immerhin steht der Klassenname sowieso hinter new.

Objekte und auch Referenzvariablen basieren in C# jeweils auf einer ganz bestimmten Klasse. Man spricht davon, dass Objekte und Referenzvariablen einen ganz bestimmten Datentyp haben. Der Datentyp beschreibt, welche Art von Information ein Objekt speichern und verarbeiten kann. So ist aufgrund des Datentyps Uri klar, dass die Referenzvariable uriHighscore auf ein Objekt verweist, das URIs speichern und verarbeiten kann.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    var uriHighscore = new Uri("http://www.highscore.de/"); 
    var uriMicrosoft = new Uri("http://www.microsoft.de/"); 
  } 
} 

Sie können eine Klasse beliebig oft verwenden, um so viele Objekte zu erstellen, wie Sie brauchen. Auch das können Sie mit dem Grundriss eines Hauses vergleichen: Mit einem gleichen Grundriss können Sie so viele Häuser bauen wie Sie möchten.


2.5 Zugriff auf Merkmale

Eigenschaften und Methoden eines Objekts verwenden

Wenn Sie Objekte erstellt und in einer Referenzvariablen verankert haben, können Sie über die Referenzvariable auf Eigenschaften und Methoden des Objekts zugreifen. Woher wissen Sie aber, welche Eigenschaften und Methoden ein Objekt zur Verfügung stellt?

Microsoft bietet eine riesige Dokumentation für alle Klassen im .NET-Framework an. In dieser Dokumentation ist selbstverständlich auch eine Beschreibung der Klasse Uri enthalten.

Wenn Sie sich die Dokumentation der Klasse Uri ansehen, sehen Sie unter anderem eine Übersicht über Eigenschaften und Methoden, die diese Klasse anbietet. So existiert zum Beispiel eine Eigenschaft namens Host, die den Servernamen zurückgibt.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    var uriHighscore = new Uri("http://www.highscore.de/csharp/einfuehrung/"); 
    Console.Out.WriteLine(uriHighscore.Host); 
  } 
} 

Obiges Programm gibt www.highscore.de aus.

In der Dokumentation von Uri ist auch angegeben, dass es eine Methode gibt, die IsBaseOf() heißt. Diese Methode ermittelt, ob eine URI Bestandteil einer anderen URI ist.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    var uriHighscore = new Uri("http://www.highscore.de/"); 
    var uriCSharp = new Uri("http://www.highscore.de/csharp/einfuehrung/"); 
    Console.Out.WriteLine(uriHighscore.IsBaseOf(uriCSharp)); 
  } 
} 

Weil http://www.highscore.de/ in http://www.highscore.de/csharp/einfuehrung/ enthalten ist, gibt obiges Programm True aus.

Der Zugriff auf Merkmale einer Klasse - egal, ob Eigenschaften oder Methoden - erfolgt wieder per Punkt. Es handelt sich hierbei um den Zugriffsoperator, den Sie bereits kennen. Dieser Zugriffsoperator wird laufend eingesetzt, um Eigenschaften und Methoden verwenden zu können.


2.6 Datentypen

Referenzvariablen und Wertvariablen

Sie wissen bereits, wie Sie Klassen verwenden, um Objekte zu erstellen. Sie haben ebenfalls Referenzvariablen kennengelernt, in denen Objekte verankert werden, um auf sie zugreifen zu können. Neben Referenzvariablen existieren in C# aber auch Wertvariablen. Es stellt sich die Frage, was Wertvariablen sind und wie Sie sie verwenden.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    var uriCSharp = new Uri("http://www.highscore.de/csharp/einfuehrung/"); 
    var i = new Int32(); 
  } 
} 

Im obigen Beispiel wird neben der bereits bekannten Klasse Uri eine weitere Klasse instantiiert: Int32 ist ebenfalls im Namensraum System definiert und kann verwendet werden, um eine Ganzzahl zu speichern.

Auch wenn die beiden Zeilen mit den Instantiierungen von Uri und Int32 völlig identisch aussehen: uriCSharp ist eine Referenzvariable und i eine Wertvariable.

Es hängt nicht von Ihnen oder Ihrem Programm ab, welche Variablen Referenz- oder Wertvariablen sind. Es hängt einzig und allein von den verwendeten Datentypen ab.

Wenn Sie sich die Dokumentation von Uri ansehen, stellen Sie fest, dass diese Klasse mit dem Schlüsselwort class definiert wurde. Wenn Sie die Dokumentation von Int32 öffnen, sehen Sie, dass diese Klasse mit dem Schlüsselwort struct definiert wurde. Bei Int32 handelt es sich also gar nicht um eine Klasse, sondern um eine Struktur. Und genau das macht den Unterschied zwischen Referenz- und Wertvariablen aus.

Die allermeisten Datentypen im .NET-Framework sind Klassen. Nur ein kleiner Teil der Datentypen ist als Struktur definiert. Was der Unterschied zwischen Klassen und Strukturen in der Praxis ist, soll Ihnen folgendes Beispielprogramm zeigen.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    var uriCSharp = new UriBuilder("http://www.highscore.de/csharp/einfuehrung/"); 
    var uriCSharp2 = uriCSharp; 
    uriCSharp.Path = ""; 
    Console.Out.WriteLine(uriCSharp2); 

    var i = new Int32(); 
    i = 10; 
    var i2 = i; 
    i = 20; 
    Console.Out.WriteLine(i2); 
  } 
} 

Im obigen Programm wird anstelle der Klasse Uri die Klasse UriBuilder verwendet. Die beiden Klassen sind grundsätzlich gleich. Lediglich UriBuilder erlaubt jedoch, URIs zu ändern - Objekte vom Typ Uri speichern konstante URIs.

Wenn Sie obiges Programm ausführen, wird http://www.highscore.de:80/ und 10 ausgegeben. Wenn Sie sich das Programm genau anschauen, sollte Sie das verwundern.

Das Objekt vom Typ UriBuilder wird mit der URI http://www.highscore.de/csharp/einfuehrung/ initialisiert und in der Referenzvariablen uriCSharp verankert. In der nächste Zeile wird eine neue Referenzvariable uriCSharp2 erstellt, die auf uriCSharp gesetzt wird. Anschließend wird über uriCSharp auf die URI zugegriffen und der Pfad über die Eigenschaft Path auf eine leere Zeichenkette gesetzt. Das ist gleichbedeutend mit einer Löschung des Pfads. Dann wird uriCSharp2 auf die Standardausgabe ausgegeben.

Obwohl der URI über uriCSharp verändert wurde, wird bei Ausgabe von uriCSharp2 die geänderte URI angezeigt. Der Grund ist der, dass Referenzvariablen keine Objekte sind, sondern auf Objekte verweisen. Weil uriCSharp2 auf uriCSharp gesetzt wurde, verweisen beide Referenzvariablen auf das gleiche Objekt. Es gibt nur ein Objekt vom Typ UriBuilder. Es gibt aber zwei Referenzvariablen, die auf dieses eine Objekt verweisen.

Bei den Variablen i und i2 verhält es sich genau andersrum. Hierbei handelt es sich nicht um Verweise auf ein Objekt vom Typ Int32. Die beiden Variablen i und i2 sind Objekte vom Typ Int32. Bei der Zuweisung var i2 = i; wird der Inhalt der Variablen i in die Variable i2 kopiert. Wenn dann in der darauffolgenden Zeile i auf 20 gesetzt wird, ist in i2 immer noch die Zahl 10 gespeichert, die deswegen vom Programm ausgegeben wird.

Referenzvariablen verweisen auf Objekte, während Wertvariablen Objekte sind. Während bei einem Zugriff über eine Referenzvariable das Objekt verändert wird, auf das die Referenzvariable verweist, wird bei einem Zugriff über eine Wertvariable genau das Objekt geändert, das die Wertvariable ist.

Der Grund, warum überhaupt zwischen Referenz- und Wertvariablen unterschieden wird, ist Performance. Da Referenzvariablen keine Objekte sind, sondern auf Objekte verweisen, findet beim Zugriff ein kleiner Umweg statt. Für kleine Objekte wie zum Beispiel vom Typ Int32, die einfach nur eine Ganzzahl speichern, ist es besser, wenn diese als Wertvariable abgelegt werden können und auf den Umweg bei einem Zugriff verzichtet wird.

In der Praxis ist es wichtig, dass Sie wissen, ob eine Variable eine Referenz- oder Wertvariable ist. Ansonsten wundern Sie sich, wenn Änderungen in einem Objekt an einer anderen Stelle im Programm sichtbar oder eben nicht sichtbar sind. Sie können aber ganz leicht herausfinden, welche Variablen Referenz- und Wertvariablen sind, indem Sie sich einfach den Datentyp des entsprechenden Objekts anschauen: Ist der Datentyp mit class definiert worden, handelt es sich um eine Klasse, deren Objekte in Referenzvariablen verankert sind. Ist der Datentyp mit struct definiert worden, handelt es sich um eine Struktur, deren Objekte Wertvariablen sind.


2.7 Alias-Datentypen

Kürzere Schreibweise für wichtige Datentypen

Es gibt eine Reihe von Datentypen, die in der Praxis sehr häufig verwendet werden. Dazu zählt zum Beispiel ein Datentyp zum Speichern von Ganzzahlen. Sie hatten soeben die Struktur Int32 kennengelernt, mit der es möglich ist, Ganzzahlen in Objekten zu speichern.

Für sehr häufig verwendete Datentypen bietet C# Aliase an. Zum Teil ist damit eine kürzere Schreibweise möglich. Zum Teil sehen die Aliase Datentypen aus anderen Programmiersprachen ähnlich, was es Entwicklern, die andere Programmiersprachen kennen, einfacher macht, C#-Code zu verstehen.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    int i = 10; 
    Console.Out.WriteLine(i); 
  } 
} 

Der Datentyp int bedeutet, dass die Variable i eine Wertvariable vom Typ Int32 ist. Es handelt sich lediglich um eine abgekürzte Schreibweise, die in der Praxis auch so bevorzugt angewandt wird.

Neben int stehen weitere Aliase in C# zur Verfügung. Folgende Tabelle gibt Ihnen einen Überblick:

Tabelle 2.1. Alias-Datentypen
Alias Datentyp Beschreibung
bool System.Boolean Datentyp zum Speichern von true oder false
byte System.Byte 8-bit Ganzzahlen
sbyte System.SByte 8-bit Ganzzahlen mit Vorzeichen
short System.Int16 16-bit Ganzzahlen
int System.Int32 32-bit Ganzzahlen
long System.Int64 64-bit Ganzzahlen
ushort System.UInt16 16-bit Ganzzahlen ohne Vorzeichen
uint System.UInt32 32-bit Ganzzahlen ohne Vorzeichen
ulong System.UInt64 64-bit Ganzzahlen ohne Vorzeichen
float System.Single 32-bit Datentyp für Fließkommazahlen mit einfacher Genauigkeit
double System.Double 64-bit Datentyp für Fließkommazahlen mit höherer Genauigkeit
decimal System.Decimal 128-bit Datentyp für Dezimalzahlen
char System.Char 16-bit Zeichen
string System.String Datentyp zum Speichern von Zeichenketten (basiert auf char)
object System.Object Datentyp zum Umwandeln von Wertvariablen zu Referenzvariablen

Die meisten Aliase werden verwendet, um Wertvariablen zu definieren, die Ganz- oder Kommazahlen speichern können. Da Zahlen in Computerprogrammen sehr häufig verwendet werden, ist es nur praktisch, wenn es eine abgekürzte Schreibweise gibt.

Auch Zeichenketten müssen häufig in Programmen gespeichert oder verarbeitet werden. Das .NET-Framework definiert hierzu eine Klasse String. Auch diese Klasse muss nicht explizit mit new instantiiert werden. Stattdessen kann der Alias string verwendet werden.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    string s = "Hallo, Welt!"; 
    Console.Out.WriteLine(s); 
  } 
} 

Da String eine Klasse und keine Struktur ist, ist s eine Referenzvariable auf ein Objekt, das mit "Hallo, Welt!" initialisiert wird.

Der Alias object vereinfacht das Arbeiten mit der Klasse Object. Die Klasse Object ist eine allgemeine Klasse, die jedes beliebige .NET-Objekt beschreibt - deswegen heißt sie Object. Alle .NET-Klassen sind immer auch von Object abgeleitet. Was das genau heißt, werden Sie erfahren, wenn Sie die Vererbung kennenlernen.

An dieser Stelle ist lediglich wichtig zu wissen, dass object verwendet werden kann, um Wertvariablen in Referenzvariablen umzuwandeln. Schauen Sie sich dazu das folgende Beispiel an.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    int i = 10; 
    object o = i; 
    i = 20; 
    Console.Out.WriteLine(o); 
  } 
} 

o ist eine Referenzvariable und verweist auf ein Objekt vom Typ int. Dieses Objekt wird automatisch erstellt, wenn eine Wertvariable wie i einer Variablen vom Typ object zugewiesen wird. Obwohl o den Datentyp object hat, zeigt die Referenzvariable also auf ein Objekt vom Typ int. Diese int-Variable speichert die Zahl 20, weil sie als Kopie von i erstellt wurde.

Wenn in der nächsten Zeile i auf 20 gesetzt wird, ist im Objekt, auf das die Referenzvariable o zeigt, immer noch 10 gespeichert. Obiges Programm gibt somit 10 aus.

Die Umwandlung einer Wertvariablen in ein Objekt, das in einer Referenzvariablen verankert wird, heißt im Englischen Boxing. Die Wertvariable wird sozusagen in eine Box gesteckt, auf die mit einer Referenzvariablen verwiesen wird.

Boxing macht dann Sinn, wenn Daten nur einmal in einem Programm gespeichert werden sollen und die verschiedenen Stellen des Programms, an denen die Daten verarbeitet werden, alle auf die gleichen Daten zugreifen sollen. Da Referenzvariablen keine Objekte sind, sondern auf Objekte verweisen, kann durch Boxing eine Wertvariable in einem Objekt so abgelegt werden, dass über verschiedene Referenzvariablen im Programm auf die gleiche Wertvariable zugegriffen wird.


2.8 Arrays

Variablen-Gruppen

All das, was Sie bisher in diesem Kapitel kennengelernt haben, war notwendig, um das Beispielprogramm aus dem Einführungskapitel zu verstehen. Wenn Sie sich das Beispielprogramm ansehen, werden Sie feststellen, dass es aber immer noch etwas enthält, was Sie bisher nicht kennengelernt haben.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    Console.Out.WriteLine("Hallo, Welt!"); 
  } 
} 

Beim Start des Programms wird der Methode Main() ein Parameter übergeben, der nicht den mittlerweile bekannten Datentypen string hat, sondern string[]. Hinter string sind zwei eckige Klammern angegeben, deren Bedeutung Sie noch nicht kennen.

Während eine Variable vom Typ string eine Referenz auf ein Objekt darstellt, in dem genau eine Zeichenkette gespeichert werden kann, ist eine Variable vom Typ string[] eine Referenz auf ein Objekt, in dem beliebig viele Zeichenketten gespeichert werden können. Die eckigen Klammern machen also aus einem Objekt mit genau einem Speicherplatz ein Objekt mit beliebig vielen Speicherplätzen. Ein derartiges Objekt wird Array genannt.

Der Grund, warum Main() eine Referenz auf ein Array übergeben bekommt, liegt darin, dass Programmen beim Aufruf Kommandozeilenparameter übergeben werden können. Diese Kommandozeilenparameter werden dem Programm im Parameter von Main() zur Verfügung gestellt. Wenn das Programm ohne Kommandozeilenparameter gestartet wird, ist das Array leer.

Weil Arrays ganz normale Objekte sind, besitzen sie Eigenschaften und Methoden, auf die zugegriffen werden kann. So bieten Arrays zum Beispiel eine Eigenschaft Length an, die die Anzahl der Elemente im Array zurückgibt.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    Console.Out.WriteLine(args.Length); 
  } 
} 

Wenn Sie obiges Programm ohne Angabe von Kommandozeilenparametern starten, wird 0 ausgegeben. Rufen Sie es mit Kommandozeilenparametern auf, wird die Anzahl dieser ausgegeben.

Selbstverständlich gibt es nicht nur Arrays wie string[], in denen beliebig viele Zeichenketten gespeichert werden können. Sie können jeden beliebigen Datentyp verwenden, um ein Array zu erstellen. So wäre int[] eine Referenz auf ein int-Array und Uri[] eine Referenz auf ein Uri-Array.

Auch wenn sich die Arrays im Datentypen unterscheiden - das eine speichert Zeichenketten, das andere Ganzzahlen, das nächste URIs - so basieren alle Arrays auf einer Klasse namens Array. In dieser Klasse ist unter anderem die Eigenschaft Length definiert. Deswegen steht diese Eigenschaft für alle Arrays zur Verfügung - egal, ob es sich um ein string-, ein int- oder ein Uri-Array handelt. In der Dokumentation der Klasse Array finden Sie wie gewohnt eine Übersicht über verfügbare Eigenschaften und Methoden.

Referenzen sind nur dann nützlich, wenn sie auf ein Objekt verweisen. Entweder wird Ihnen eine Referenz auf ein Array übergeben, so wie es bei Main() geschieht. Oder Sie erstellen ein Array selbst - wie üblich mit new.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    int[] i = new int[3]; 
    i[0] = 1; 
    i[1] = 99; 
    i[2] = 1000; 
    Console.Out.WriteLine(i.Length); 
  } 
} 

So wie bei herkömlichen Objekten müssen Sie hinter new einen Datentyp angeben. Im obigen Beispiel ist das int. Wenn nicht ein einzelnes Objekt erstellt werden soll, sondern ein Array, muss hinter dem Datentyp ein Paar eckiger Klammern angegeben werden. Außerdem muss in den eckigen Klammern angegeben werden, für wie viele Elemente das Array Speicherplatz bereitstellen soll. Im obigen Fall können bis zu drei Ganzzahlen im Array gespeichert werden.

Ist das Array erstellt worden, kann wie gewohnt über die Referenzvariable darauf zugegriffen werden. Arrays bieten hierbei nicht nur Eigenschaften und Methoden an, sondern auch einen besonderen Operator, mit dem auf ein einzelnes Element an einer ganz bestimmten Position im Array zugegriffen werden kann. Der Index des entsprechenden Elements wird dabei zwischen zwei eckigen Klammern angegeben.

Beachten Sie, dass der Index des ersten Elements im Array 0 ist. Achten Sie außerdem darauf, dass Sie keinen ungültigen, weil zu großen Index verwenden. So dürfen Sie im obigen Beispielprogramm keinen Index größer als 2 verwenden, weil das Array lediglich aus 3 Elementen besteht.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    int[] numbers = new int[3]; 
    numbers[0] = 1; 
    numbers[1] = 99; 
    numbers[2] = 1000; 

    foreach (int i in numbers) 
    { 
      Console.Out.WriteLine(i); 
    } 
  } 
} 

C# bietet eine sogenannte foreach-Schleife an, um über alle Elemente in einem Array zu iterieren. So werden im obigen Programm die Zahlen im Array numbers nacheinander auf die Standardausgabe ausgegeben. Dabei muss in runden Klammern hinter dem Schlüsselwort foreach eine Variable angegeben werden, die in jedem Schleifendurchgang einen anderen Wert aus dem Array speichert, das hinter der Variablen und hinter dem Schlüsselwort in angegeben wird. Im nachfolgenden Block, der wie üblich durch ein Paar geschweifter Klammern gekennzeichnet wird, kann auf die im Schleifenkopf definierte Variable zugegriffen werden, um auf diese Weise alle Werte in einem Array zu verarbeiten.

Während die foreach-Schleife speziell für Arrays verwendet wird, um jedes Element im Array zu verarbeiten, steht mit der for-Schleife ein allgemeineres Instrument zum Iterieren zur Verfügung.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    int[] numbers = new int[3]; 
    numbers[0] = 1; 
    numbers[1] = 99; 
    numbers[2] = 1000; 

    for (int i = 0; i < numbers.Length; i += 2) 
    { 
      Console.Out.WriteLine(i); 
    } 
  } 
} 

Obiges Programm greift lediglich auf jedes zweite Element im Array numbers zu, um es auszugeben. Dazu verwendet das Programm eine for-Schleife, in dessen Schleifenkopf eine Zählvariable i definiert und mit 0 initialisiert wird. Diese Zählvariable wird nach jedem Schleifendurchgang um 2 erhöht. Dies geschieht mit Hilfe des +=-Operators. Anschließend wird eine Bedingung überprüft: Ist sie wahr, wird die Schleife erneut ausgeführt; ansonsten wird der Code nach der Schleife ausgeführt.

Während die for-Schleife der foreach-Schleife ähnlich sieht, wird der Schleifenkopf in runden Klammern durch zwei Semikolons in drei Bereiche geteilt: Im ersten Bereich wird üblicherweise eine Zählvariable definiert, die im zweiten Bereich in einer Bedingung verwendet wird. Im dritten Bereich wird die Zählvariable geändert. Während die foreach-Schleife dann beendet wird, wenn über alle Elemente im Array iteriert wurde, hängt dies bei der for-Schleife allein von der Zählvariablen und der Bedingung ab.

C# kennt zwei weitere Schleifen, die while- und do-while-Schleife genannt werden. Da diese Schleifen ähnlich eingesetzt werden wie die for-Schleife, wird an dieser Stelle auf die Dokumentation der Iterationsanweisungen im C#-Programmierhandbuch verwiesen.


2.9 Zusammenfassung

Ein erster Überblick

Sie haben in diesem Kapitel einen ersten Überblick über die Objektorientierung in C# bekommen. Ausgehend vom Beispielprogramm aus dem Einführungskapitel wurde Ihnen erklärt, was Klassen sind und welche Rolle sie in der Objektorientierung spielen. Sie haben gelernt, wie Sie mit new Klassen instantiieren können, um Objekte zu erstellen. Sie haben ebenfalls gelernt, wie Sie dann mit dem Zugriffsoperator auf Eigenschaften und Methoden von Objekten zugreifen können.

Klassen stellen eine Beschreibung dessen dar, was ein Objekt ist und tun kann. Weil es nicht nur Klassen, sondern auch Strukturen gibt, haben Sie den Unterschied zwischen Referenz- und Wertvariablen kennengelernt. Klassen und Strukturen werden gemeinsam als Datentypen bezeichnet.

Weil es einige Klassen gibt, die in der Praxis sehr häufig instantiiert werden, bietet C# Aliase an - abgekürzte Schreibweisen für einige Datentypen.

Abschließend haben Sie Arrays kennengelernt, die Speicherplatz für beliebig viele Objekte anbieten.

Mit all diesen Informationen ausgerüstet sollten Sie jetzt in der Lage sein, das Beispiel aus dem Einführungskapital besser zu verstehen.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    Console.Out.WriteLine("Hallo, Welt!"); 
  } 
} 

Einige Fragen bleiben dennoch offen. So haben Sie bisher nicht das Schlüsselwort static kennengelernt. Auch ist momentan nicht klar, wieso die Klasse Console verwendet werden kann, ohne dass von ihr mit new ein Objekt erstellt werden muss. Die Antwort auf diese Fragen erhalten Sie im Kapitel 3, Objektmerkmale.


2.10 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Erstellen Sie eine Konsolenanwendung, die den Anwender zur Eingabe einer WWW-Adresse auffordert. Speichern Sie die Adresse in einem Objekt vom Typ Uri und geben Sie anschließend den Servernamen in der Adresse auf die Konsole aus.

    Instantiieren Sie die Klasse Uri und initialisieren Sie das Objekt mit der Eingabe des Anwenders. Den Servernamen erhalten Sie über die Eigenschaft Host.

  2. Erweitern Sie Ihre Lösung zu Aufgabe 1, indem der Anwender angeben kann, wie viele WWW-Adressen er speichern möchte. Anschließend soll er in einer Schleife aufgefordert werden, die entsprechende Anzahl an WWW-Adressen einzugeben. Die Servernamen der gespeicherten Adressen sollen anschließend auf die Konsole ausgegeben werden.

    Wandeln Sie die Eingabe vom Anwender, die Sie über die Methode ReadLine() der Eigenschaft In von Console erhalten, mit Hilfe der Methode Parse() der Struktur Int32 in einen int-Wert um. Das ist notwendig, weil ReadLine() ein Objekt vom Typ String zurückgibt, zum Erstellen eines Arrays in den Klammern aber ein int-Wert angegeben werden muss. Greifen Sie dann in einer Schleife auf das Array zu, um es mit Uri-Objekten zu füllen. Sie müssen dabei eine for-Schleife verwenden, weil eine foreach-Schleife lediglich einen lesenden Zugriff auf ein Array zulässt.

  3. Erstellen Sie eine Konsolenanwendung, die den Anwender zur Eingabe eines Schemas, eines Servernamens und eines Pfads auffordert. Mit diesen Angaben soll mit Hilfe der Klasse UriBuilder eine URI erstellt werden. Diese soll anschließend auf die Konsole ausgegeben werden. Wenn ein Anwender zum Beispiel http, www.highscore.de und /csharp/einfuehrung/ eingibt, soll das Programm http://www.highscore.de/csharp/einfuehrung/ ausgeben.

    Erstellen Sie ein Objekt vom Typ UriBuilder und greifen Sie nacheinander auf die Eigenschaften Scheme, Host und Path zu, um die vom Anwender eingegebenen Daten zum Bilden der Adresse zu verwenden.

  4. Erstellen Sie eine Konsolenanwendung, die den Anwender zur Eingabe zweier Kommazahlen auffordert. Addieren, subtrahieren, multiplizieren und dividieren Sie die Kommazahlen und geben Sie die vier Ergebnisse auf die Konsole aus.

    Greifen Sie über die Eigenschaft In der Klasse Console auf die Standardeingabe zu und rufen Sie die Methode ReadLine() auf, um eine Zeile vom Anwender einzulesen. Weil ReadLine() einen String zurückgibt, Sie für die Rechnungen jedoch eine Zahl benötigen, parsen Sie den String mit Hilfe der Methode Parse() der Struktur Single. Diese Methode gibt die Zahl in dem String, der als Parameter übergeben wird, als float-Wert zurück.