Nachwuchs für die C-Familie – C#

Veröffentlicht: 09. Dez 2001 | Aktualisiert: 18. Jun 2004

Von Ralf Morgenstern

Im Rahmen der .NET-Initiative hat Microsoft gleich eine neue Sprache vorgestellt: C# (C Sharp). Sie gehört zur C-Familie, das heißt, die Syntax ist an C und C++ angelehnt. Microsoft selbst bezeichnet C# als Komponenten-orientierte Sprache. Man könnte aber auch einfach sagen: C# vereint das Beste von C++, Java und Visual Basic.

Auf dieser Seite

 Eine erste Klasse
 C#-Datentypen sind .NET-Typen
 OOP à la C#
 C#-Extras
 Fazit

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild01

Wenn Sie die Syntax betrachten, werden Sie merken, dass C# zur C-Familie gehört. Doch obwohl C# Zeiger kennt, werden Sie sich in der Regel nicht damit auseinandersetzen, denn C# unterstützt wie alle .NET-Sprachen die automatische Speicherverwaltung (Garbage Collection). Viele Elemente werden Sie an Visual Basic erinnern. Wenn Sie mit Visual Studio.NET arbeiten, stellt sich das VB-Feeling ein, nur die Syntax ist etwas anders. Und ein paar Möglichkeiten mehr haben Sie auch noch.

Ein Unterschied zu VBA könnte Ihnen anfangs Schwierigkeiten bereiten: C# unterscheidet Groß- und Kleinschreibung genau. Anders als in VBA müssen Sie also auf die Schreibweise achten, da if etwas anderes ist als IF oder If oder iF.

Listing 1 zeigt ein sehr einfaches C#-Programm. Über die using-Anweisung macht es zunächst eine Bibliothek bekannt. Da das .NET-Framework objekt-orientiert ist, handelt es sich um eine Klassenbibliothek, die unter anderem Funktionen zur Ein- und Ausgabe auf der Konsole (Kommandozeile) zur Verfügung stellt. Die Sprache C# selbst enthält keine Funktionen dieser Art. Funktionen wie Print, InStr oder Show zum Anzeigen eines Formulars sind nicht unbedingt integraler Bestandteil einer Sprache, die oft nur elementare Anweisungen wie for oder if sowie grundlegende Datentypen enthalten. Funktionen für Dateioperationen, das GUI und so weiter, werden hingegen über Bibliotheken eingebunden. Das ist übrigens auch in VB.NET der Fall, allerdings fällt es durch einen von Visual Studio.NET standardmäßig eingebundenen Kompatibilitäts-Namespace und die enge Verbundenheit der Sprachen mit dem Framework nicht auf.

Listing 1: Ein einfaches C#-Programm.

using System; 
class HelloCSharp 
{ 
  public static void Main() 
  { 
    Console.WriteLine("Hallo C#"); 
  } 
}

using (sowie Imports in VB.NET) macht die Bibliothek bekannt, so dass Sie nicht den kompletten Namen inklusive aller Namensräume nennen müssen. Damit Sie ein Programm erfolgreich übersetzen können, müssen Sie aber eine Referenz setzen, vergleichbar zum Einfügen eines Verweises in der VB(A)-IDE. In Visual Studio.NET steht dafür ein Dialog zur Verfügung, für den Kommandozeilen-Compiler verwenden Sie die Option /r. Die wichtigsten Bibliotheken bindet Visual Studio.NET beim Anlegen eines neuen Projekts selbst ein.

Eine erste Klasse

Im Anschluss an das Einbinden der Bibliothek mit using wird die Klasse HelloCSharp definiert. Sie enthält eine einzige Methode: Main. Jedes C#-Programm benötigt eine Main-Funktion, denn es ist die vorgegebene Startfunktion, die von der Laufzeitumgebung aufgerufen wird. Die Funktion muss statisch sein, das heißt, alle Instanzen dieser Klasse teilen sich diese Funktion. Das Schlüsselwort static entspricht shared in Visual Basic.NET. Es wird auch auf Variablen angewandt, die dann für alle Instanzen gemeinsam gelten.

C# unterscheidet grundsätzlich nicht zwischen Funktionen und Sub-Routinen, wie es Visual Basic tut. Es gibt nicht einmal ein Schlüsselwort, um eine Funktion zu deklarieren, der Funktionscharakter ergibt sich aus dem Kontext. Da aber nicht in jedem Fall ein Rückgabewert erforderlich ist, können Sie den Pseudo-Datentyp void verwenden. Die Main-Funktion aus
Listing 1 ist deshalb eher mit einer Sub-Prozedur in Visual Basic vergleichbar.

Die einzige Aktivität der Funktion besteht darin, den Text „Hello C#" auf der Konsole auszugeben. Die Anweisung wird mit einem Semikolon abgeschlossen, weil C# anders als VBA nicht zeilenorientiert ist. Deshalb gibt es ein spezielles Anweisungsendezeichen. Sie können mehrere Anweisungen in eine Zeile schreiben oder eine Anweisung beliebig über Zeilen verteilen, ohne ein Aufteilungszeichen wie den Unterstrich zu benutzen.

Der Text ist in doppelte Anführungszeichen eingeschlossen. C# kennt auch einzelne Zeichen, die dann in einfachen Anführungszeichen stehen.

Neben dem Datentyp string gibt es den Typ char für einzelne Zeichen. In jedem Fall handelt es sich um UNICODE-Zeichen. Folgende Anweisungen deklarieren eine String- und eine Character-Variable und initialisieren diese auch gleich:

string s = "Hallo"; 
char c = 'a';

Einzelne Zeichen eines Strings können Sie über einen Index ansprechen, allerdings nur lesend:

c = s[0];

Indizes werden in C# in eckigen Klammern angegeben, auch bei Arrays. Das ermöglicht eine klare Unterscheidung zwischen Funktionsaufrufen und Arrayzugriffen. Beachten Sie, dass folgende Anweisung falsch ist, da sie versucht, einem String einen Character zuzuweisen:

string s2 = s[1];

Strings können zusammengesetzt werden, wobei aber nur der +-Operator zulässig ist:

string s, t="Hallo"; 
s = t + " C#";

s und t sind Variablen von Typ string. Eine Variablendeklaration beginnt immer mit der Typangabe, darauf folgt eine durch Komma getrennte Liste von Variablennamen. Jede Variable kann individuell mit einem Wert vorbelegt werden, s ist noch nicht initialisiert. Nach der Zuweisung hat s den Wert „Hallo C#".

Listing 2: Eine Summen-Funktion in C#.

using System; 
class Sum 
{ 
  public static void Main(string[] args) 
  { 
    int i, n, x=0; 
    for(i=0; i<args.Length; i++) 
    { 
      n = args[i].ToInt32(); 
      x += n; 
    } 
    Console.WriteLine(x.ToString()); 
  } 
}

Listing 2 stellt ein etwas umfangreicheres C#-Programm vor. Es ist ebenfalls ein Konsolenprogramm, das aber die Kommandozeilenargumente verwendet. Sie werden als Zahlen interpretiert und addiert – die Klasse heißt daher Sum. Die Funktion Main erhält als Parameter ein String-Array mit variabler Dimensionierung. Sie können natürlich auch Arrays mit vorgegebenen Dimensionen deklarieren:

string[] a = new string[10]; 
string[] b = new string[2] {"a", "b"}; 
int c[] = {1, 2, 3};

a und b sind String-Arrays, c ist ein Integer-Array. Im Unterschied zu C++ stehen die Indexklammern beim Typ, nicht beim Variablennamen. Die Größe wird erst beim Erzeugen des Array-Objekts festgelegt, a hat zehn Elemente, das zweite String-Array hat zwei Elemente, die auch gleich vorbelegt werden. Wird ein Array bei der Deklaration mit Werten belegt, können Sie eine alternative Syntax verwenden, die Sie am Beispiel eines Integer-Arrays sehen können. Alle Beispiele zeigen, dass Arrays Objekte sind, die auch erzeugt werden müssen. Alle Arrays in .NET- und damit in C#-Programmen basieren auf dem Datentyp System.Array.
Die Funktion Main arbeitet mit drei int-Variablen, also 32-Bit-Ganzzahlen. Da nur x explizit den Wert 0 erhält, ist der Wert von i und n zu Beginn der Ausführung unbestimmt. Sie könnten diese beiden Variablen vor der ersten Zuweisung zum Beispiel nicht in einer WriteLine-Anweisung verwenden.

i wird als Laufvariable in der for-Schleife verwendet und nimmt Werte von 0 bis args.Length-1 an. Mit Length ermitteln Sie die Anzahl der Elemente eines Arrays, das letzte Element hat den Index Length-1. Die Laufvariable wird durch den Inkrement-Operator ++ für jeden Durchlauf der Schleife um 1 erhöht.

 

C#-Datentypen sind .NET-Typen

Da die Argumente als Strings übergeben werden, müssen sie vor der Addition in einen numerischen Wert umgewandelt werden. Das geschieht durch die Mitgliedsfunktion ToInt32 der string-Variablen. Beachten Sie auch die Konvertierung von x in eine Zeichenkette zur Ausgabe mit ToString(). Es zeigt einen wesentlichen Aspekt von C# beziehungsweise des .NET-Frameworks: Alles ist ein Objekt (Bild 1). Alle Datentypen basieren auf dem Ursprungstyp System.Object beziehungsweise object. Sie werden davon abgeleitet, übernehmen also die Funktionalität der Basisklasse und ergänzen sie um spezifische Funktionen. string ist direkt von object abgeleitet, int oder char nur indirekt. Diese Grundtypen sind als „endgültig" definiert, Sie können Sie also nicht erweitern, indem Sie neue Klassen davon ableiten (Tabelle 1).

Bild03

Tabelle 1: Die Datentypen von C#.

Datentyp

Erklärung

sbyte, short, int, long

Ganzzahlen

byte, ushort, uint, ulong

Vorzeichenlose Ganzzahlen

float, double

Fließkommazahlen

decimal (28 signifikante Nachkommastellen)

Dezimalzahl

char

Ein einzelnes UNICODE-Zeichen

string

Eine UNICODE-Zeichenkette fester Länge

bool

Boolescher Wert

object

Generischer Basistyp

Die Addition erfolgt durch den kombinierten Zuweisungs-Additions-Operator +=. Zum Wert der links davon stehenden Variablen wird der Wert rechts addiert. Die Anweisung entspricht also dem bekannten x = x + n, ist aber effizienter und intuitiver. Diese Kombination gibt es für alle gängigen Operatoren und sie ist auch auf nicht-numerische Werte anwendbar.

Neben for stehen weitere Schleifenanweisungen bereit, zum Beispiel foreach:

foreach( string s in args ) 
{ 
x += s.ToInt32(); 
}

Sie müssen den Typ der Laufvariablen direkt in der Anweisung angeben, die nicht nur auf Arrays, sondern auf viele andere Datencontainer anwendbar ist.
Eine weitere Schleifenkonstruktion basiert auf dem Schlüsselwort while und prüft die in runden Klammern angegebene Bedingung vor Eintritt in die Schleife:

int i=0; 
while( i < args.Length ) 
{ 
x += args[i].ToInt32(); 
i++; 
}

Hier sind Sie selbst dafür verantwortlich, die Laufvariable i zu verwalten. Während Sie bei for die entsprechende Anweisung wahrscheinlich mehr oder weniger automatisch schreiben, vergessen Sie es bei Schleifen dieser Art möglicherweise öfters. Sie können die beiden Anweisungen im Schleifenblock auch zusammenfassen:

int i=0; 
while( i < args.Length ) 
x += args[i++].ToInt32();

Da jetzt nur noch eine Anweisung in der Schleife steht, können Sie die geschweiften Klammern weglassen.
Die Inkrementierung in die Indizierung hineinzuziehen, birgt Gefahren. In etwas komplexeren Anweisungsfolgen arbeiten Sie schnell mit dem falschen Wert, da Sie beim Umstellen einiger Anweisungen übersehen, dass i schon erhöht wurde. Noch schwieriger wird es dadurch, dass Sie den Inkrement- und den Dekrement-Operator sowohl als Postfix- als auch als Präfix-Operator verwenden können. Welche Ausgabe produzieren wohl die folgenden Anweisungen, welche Werte listet die erste Schleife auf, welche die zweite?

i=0; 
while( i++ < 10 ) WriteLine(i.ToString()); 
i=0; 
while( ++i < 10 ) WriteLine(i.ToString());

Die erste zählt von 1 bis 10, die zweite von 1 bis 9. Der erste ausgegebene Wert ist nicht 0, da i vor der Ausgabe schon einmal inkrementiert wird.
while wird auch noch in einem weiteren Fall verwendet:

bool b=true; 
do 
{ 
// tu was 
}while( b );

Die Schleife wird mindestens einmal durchlaufen und erst dann verlassen, wenn die Boolesche Variable b den Wert false hat. Die beiden Schrägstriche leiten einen Kommentar ein, der bis zum Ende der Zeile geht. Ergänzend gibt es den Blockkommentar, der mit /* beginnt, auch über mehrere Zeilen gehen kann und mit */ endet.

Sie können alle diese Schleifen mit der Anweisung break vorzeitig verlassen. Da das aber sicher nur in bestimmten Fällen geschehen soll, benötigen Sie eine Anweisung, um den Fall festzustellen:

for(;;) 
{ 
if( i == args.Length ) break; 
x += args[i++].ToInt(); 
}

Die if-Anweisung prüft eine Bedingung und führt eine Anweisung dementsprechend aus oder nicht. Die Bedingung muss in runden Klammern stehen wie auch schon bei while, dafür wird kein then benötigt. Auch hier gilt: Mehrere Anweisungen müssen durch geschweifte Klammern als Anweisungsblock zusammengefasst werden. Da im Beispiel nur das break ausgeführt werden soll, kann auf die Klammern verzichtet werden.

Der Vergleichsoperator == unterscheidet sich vom Zuweisungsoperator. Auf ungleich wird mit != geprüft, ansonsten stimmen die Vergleichsoperatoren mit denen von VBA überein.

Die for-Schleife sieht unvollständig aus, ist es aber nicht. Alle drei Anweisungsteile (Ausgangswert zuweisen, Bedingung, Laufvariable ändern) können fehlen. Den Extremfall sehen Sie oben. Die Teile können aber auch mehrere Anweisungen enthalten, die durch Kommata getrennt werden:

for( i=0, j=a.Length-1, 
i < a.Length / 2;  
i++, j-- ) 
{ 
k=a[i]; a[i] = a[j]; a[j] = k; 
}

Das Beispiel dreht die Reihenfolge, in der die Elemente eines Arrays auftreten, um. Dazu wird eine Indexvariable von 0 an erhöht, eine andere vom letzten Element an heruntergezählt.

Ergänzend zu if gibt es die else-Anweisung, die die alternative(n) Anweisung(en) enthält. Da es kein elseif gibt, müssen Sie mehrere if-Anweisungen schachteln.

Eine Kurzform von if-else für Zuweisungen arbeitet mit speziellen Operatoren:

s = n<0 ? "kleiner 0" : "größer 0";

In Abhängigkeit davon, ob n kleiner 0 ist, wird s der entsprechende Wert zugewiesen. Das entspricht dem umfangreicheren, aber funktional gleichen Anweisungsblock:

if( n < 0 ) 
s = "kleiner 0"; 
else 
s = "größer 0";

Möchten Sie mehrere Fälle unterscheiden, ziehen Sie die switch-Anweisung in Betracht. Sie ist das Gegenstück zum Select Case von VBA:

switch( c ) 
{ 
case 'a': 
// Anweisungen 
goto case 'b'; 
case 'b': 
// Anweisungen 
break; 
default: 
// Standardfall 
}

Als Unterscheidungswert kommen Ganzzahlen, Character und Strings in Frage. Jede case-Anweisung kann nur einen Wert aufnehmen und muss mit einem break, return oder goto abgeschlossen werden. break verlässt die switch-Anweisung, return die Funktion und goto verzweigt zu einem anderen case-Teil. Mehrere case-Werte mit den gleichen Anweisungen zu behandeln wie in C++, ist nicht möglich. Sie müssen mit goto arbeiten.

Noch eine abschließende Bemerkung zu den Schleifen: Mit continue überspringen Sie in gewisser Weise einen Teil der Ausführung und setzen die Schleife sofort mit dem nächsten Laufwert fort. Denken Sie an ein Array mit Objekten, in dem einige Elemente noch nicht initialisiert sind, Sie also die Methodenaufrufe nicht ausführen können beziehungsweise dürfen. Ob eine Referenz gültig ist, prüfen Sie folgendermaßen:

if( obj != null ) 
{ // ....

Oder Sie machen bei einer ungültigen Referenz mit dem nächsten Objekt weiter:

if( obj == null ) continue;

In C# hat eine nicht initialisierte Objektvariable den Wert null. Die Prüfung findet mit den normalen Vergleichsoperatoren anstelle der is-Anweisung statt.

Zeit für die erste Funktion: Add addiert zwei Fließkommazahlen. Der Rückgabetyp ist float. Er wird durch die return-Anweisung festgelegt, die die Funktion verlässt:

float Add(float f1, float f2) 
{ 
return f1 + f2; 
}

Und so rufen Sie die Funktion auf:

float f = Add( 11.9F, 0,1F );

Das angehängte F kennzeichnet die konstanten Werte als einfach-genaue Werte. Ohne es würden die Zahlen als doppelt-genaue Zahlen vom Typ double betrachtet, und der strenge C#-Compiler würde das Programm nicht übersetzen, da er versteckte Typkonvertierungen nicht zulässt.
Möchten Sie eine Additionsroutine schreiben, die mit einer unterschiedlichen Anzahl an Parametern zurechtkommt, verwenden Sie ein Parameter-Array. Das Schlüsselwort ist params (vergleiche ParamArray in VBA), das Array selbst ist zudem typisiert. Innerhalb der Funktion verwenden Sie es wie ein normales Array. Das Zwischenergebnis aller Additionen wird in dd gespeichert und das Ergebnis erst abschließend mit return zurückgegeben:

double Add(params double[] ds) 
{ 
double dd=0; 
foreach( double d in ds ) 
dd += d; 
return dd; 
} 
double x; 
x= Add( 1.3 ); 
x = Add( 1.3, 4.0, 0.7 );

Die beiden Versionen von Add können Sie in die gleiche Klasse schreiben, ohne dass sie sich ins Gehege kommen. Durch die unterschiedlichen Typen der Parameter sind sie deutlich unterschieden. Sie müssen den Um-stand, dass der Name Add überladen ist, nicht wie in VB.NET mit einem zusätzlichen Schlüsselwort hervorheben.

Sie können Parameter auch als Referenz übergeben. Dazu müssen Sie sie sowohl in der Funktionsdeklaration als auch beim Aufruf mit ref kennzeichnen:

void Swap( ref int a, ref int b) 
{ 
int temp=a; a=b; b=temp; 
} 
int x=10, y = 100; 
Swap( ref x, ref y);

Swap selbst gibt keinen Wert zurück, weshalb in der Deklaration der Pseudotyp void verwendet wird.

Aber C# kennt noch einen dritten Parametertyp, von dem man nur hoffen kann, dass er auch bald in VB.NET verfügbar sein wird. Bei einem Referenz-Parameter übergeben Sie einen Verweis auf eine existierende Variable. Beim out-Parameter stellt der Aufrufer nur eine Variable bereit, die ein Objekt aufnehmen soll, das Objekt selbst wird von der aufgerufenen Funktion erzeugt:

void Fill(out ArrayList al, int count) 
{ 
al = new ArrayList(count); 
for( int i=0; i < count; i++ ) 
al.Add( i ); 
} 
ArrayList arli; 
Fill( out arli, 100 );

Der erste Parameter von Fill ist eine nicht initialisierte Variable vom Typ ArrayList, einer Collection-Klasse aus dem .NET-Framework, die die Funktionalität eines Arrays und einer dynamischen Liste vereint. Die erste Aktion von Fill muss das Erzeugen einer Instanz sein. Das ist in der Sprachspezifikation so vorgegeben und wird vom Compiler überprüft. Wie bei ref gilt, dass out sowohl in der Deklaration als auch beim Aufruf angegeben werden muss.

Wo liegt der Vorteil? Stellen Sie sich vor, Sie verwenden an mehreren Stellen in Ihrem Programm eine Datenmenge, die von einer zentralen Funktion bereitgestellt wird. Arbeiten Sie mit ref, müssen Sie vor jedem Funktionsaufruf ein Objekt erzeugen, das die Daten aufnimmt und dieses an die Funktion übergeben. Zehn Aufrufe, zehn Anweisungen, um das Objekt zu erzeugen. Arbeiten Sie mit out, rufen Sie lediglich die Funktion auf.

 

OOP à la C#

Klassen vereinbaren Sie über das Schlüsselwort class. Eine Klasse kann Konstanten, Variablen, Eigenschaften und Methoden haben. Diese können mit verschiedenen Zugriffsrechten versehen und auch statisch sein. Letztlich treffen Sie all die Dinge an, die in [1] und [2] beschrieben wurden. An dieser Stelle sollen nicht noch einmal OOP-Begriffe und -Verfahren beschrieben werden, sondern lediglich C#-Spezifika.

Listing 3: Klassen, Schnittstellen und Vererbung in C#.

namespace OOP 
{ 
  using System; 
  public class Class1 
  { 
    protected int a; 
    public Class1(int a) 
    { 
      this.a = a;  
    } 
    public Class1():this(0) { } 
    public virtual void Print() 
    { 
      Console.WriteLine("a={0}", a.ToString()); 
    } 
  } //Class1 
  public class Class2:Class1 
  { 
    protected int b; 
    public Class2() 
    { 
      b=0; 
    } 
    public Class2(int b):base(1) 
    { 
      this.b = b; 
    } 
    public Class2(int b, int a):base(a) 
    { 
      this.b=b; 
    } 
    public override void Print() 
    { 
      base.Print(); 
      Console.WriteLine("b={0}", b.ToString()); 
    } 
  }//Class2 
  public interface Interface1 
  { 
    void Method1(); 
    void Method2(); 
  } 
  public class Class3:Class1, Interface1 
  { 
    private int c; 
    public Class3() 
    { 
      c = -1; 
    } 
    public override void Print() 
    { 
      base.Print(); 
      Console.WriteLine("c={0}", c.ToString()); 
    } 
    public void Method1() 
    { 
      Console.WriteLine("Method1"); 
    } 
    void Interface1.Method2() 
    { 
      Console.WriteLine("Method2"); 
    } 
  }//Class3 
  sealed class OOPApp 
  { 
    public static int Main(string[] args) 
    { 
      Class1 o1 = new Class1(); 
      Class1 o2 = new Class1(11); 
      Class2 o3 = new Class2(100); 
      Class2 o4 = new Class2(101, 102); 
      Class3 o5 = new Class3(); 
      o1.Print(); 
      o2.Print(); 
      o3.Print(); 
      o4.Print(); 
      o5.Print(); 
      o5.Method1(); 
      Interface1 i1 = o5; 
      i1.Method2(); 
      object o = new Class1(200); 
      // o.Print(); funktioniert nicht, object kennt kein Print 
      Class1 o6 = o as Class1; 
      o6.Print(); 
      Class2 o7 = new Class2(); 
      o7.Print(); 
      return 0; 
    } 
  } 
}

Listing 3 zeigt die drei Klassen Class1, Class2 und Class3. Class2 und Class3 sind von Class1 abgeleitet, Class3 implementiert zudem die Schnittstelle Interface1 (Bild 2).

Bild02

Während Ihnen das Schlüsselwort class sicher vertraut vorkommt, erkennen Sie den Konstruktor eventuell nicht auf Anhieb. Konstruktoren haben in C# den Namen der Klasse. Class1 implementiert zwei Konstruktoren, einen mit und einen ohne Parameter. Der parameterlose Konstruktor ruft über this den anderen auf, um die Variable a mit dem Wert 0 vorzubelegen. Mitgliedsvariablen werden im C#-Sprachgebrauch Felder (Fields) genannt. Die Konstruktor-Notation ist anfangs etwas gewöhnungsbedürftig, aber schon bald ist Ihnen eine Anweisung wie die folgende vertraut:

public Class1():this(0){}

this wird auch verwendet, um Felder (Variablen der Klasse) zu referenzieren. Es entspricht dem Me von VBA.

Class1 implementiert zudem eine überschreibbare (virtuelle) Methode namens Print. Das Schlüsselwort, um eine Funktion oder eine Eigenschaft als überschreibbar zu kennzeichnen, lautet virtual.

Class2 ist von Class1 abgeleitet. Dieses Verwandtschaftsverhältnis wird dadurch angezeigt, dass der Name der Basisklasse in der Deklaration durch einen Doppelpunkt getrennt aufgeführt wird. C# unterstützt Einfachvererbung, das heißt, Sie können an dieser Stelle immer nur eine Basisklasse angeben. Zusätzlich zu einer Basisklasse können Sie aber mehrere Schnittstellen benennen, die implementiert werden sollen.

Konstruktoren werden nicht vererbt, Sie können aber die der Basisklasse aufrufen, die Sie mit base referenzieren:

public Class2(int b):base(1)

base verweist generell auf die Basisklasse, Sie sprechen auch deren public oder protected deklarierte Funktionen und Felder darüber an. So kann die überschriebene Funktion Print die Basisroutine aufrufen, bevor sie ihren Teil der Aufgabe übernimmt. In Class2 ist Print mit override gekennzeichnet, um deutlich zu machen, dass damit die Funktion der Basisklasse überlagert wird. Am Rande bemerkt, zeigt dies, dass die einzelnen Teams bei Microsoft nicht miteinander reden. Anders wäre es doch eine Kleinigkeit gewesen, die Begriffe von VB.NET und C# aufeinander abzustimmen. VB.NET verwendet zu diesem Zweck overrides, also ein s mehr – für mehrsprachige Programmierer ein Ärgernis.

Sie definieren eine Schnittstelle über das Schlüsselwort interface. Eine Klasse, die eine Schnittstelle implementieren möchte, gibt das in der Klassendeklaration an. Die Methoden und Eigenschaften lassen sich implizit (siehe Method1) oder explizit (siehe Method2) implementieren. Eine implizit implementierte Methode können Sie wie eine „normale" Methode der Klasse aufrufen. Explizit implementierte Methoden können Sie nur über einen Schnittstellenzeiger aufrufen. Den Zeiger erhalten Sie durch einfache Zuweisung (Listing 3).

Eine Gruppe von Objekten wird oft über Variablen der Basisklasse verwaltet. Die Collection-Klassen arbeiten alle mit dem grundlegenden Basistyp object. Um klassenspezifische Funktionen aufzurufen, benötigen Sie aber einen typgenauen Verweis. Diesen können Sie mit as herstellen:

object o = new Class1(200); 
Class1 c = o as Class1;

Ob eine Variable auf einem bestimmten Datentyp (einer bestimmten Klasse) basiert, können Sie auf unterschiedliche Weise prüfen.

Listing 4: Eine generische Tabellen-Klasse mit Indexer.

namespace Grid 
{ 
  using System; 
  public class Grid 
  { 
    private int rows, cols; 
    private object[,] cells; 
    public Grid(int rows, int cols) 
    { 
      this.rows = rows; 
      this.cols = cols; 
      cells = new object[rows, cols]; 
    } 
    public Grid(int rc):this(rc,rc) {} 
    public object this[int row, int col] 
    { 
      get{ 
      if( (row < 0 || row >= rows)  
      || (col < 0 || col > cols ) ) 
      throw new System.Exception( 
      "Ungültiger Index"); 
      return cells[row,col]; 
    } 
    set{ // Indexprüfung 
    cells[row,col]=value; 
  } 
} 
public int Rows 
{ get{ return rows; } 
} 
public int Cols 
{ get{ return cols; } 
} 
public string RowString(int r) 
{ 
  string s=""; // Init. auf "" ist wichtig ... 
  for(int c=0; c<cols; c++) 
  { 
    if(cells[r,c] == null ) continue; 
    if(s.Length > 0 ) s+= "\t";  
    s += cells[r,c].ToString(); 
  } 
  return s; 
} 
public void Print1() 
{ 
  for(int r=0; r<rows; r++) 
  { for(int c=0; c<cols; c++) 
  { 
    if(cells[r,c] == null ) continue; 
    Console.WriteLine("[{0},{1}]={2}",  
    r,c,cells[r,c].ToString()); 
  } 
} 
}//Print1 
public void Print2() 
{ 
  System.DateTime dt; 
  for(int r=0; r<rows; r++) 
  { 
    for(int c=0; c<cols; c++) 
    { 
      if(cells[r,c] == null ) continue; 
      if( cells[r,c].GetType() == typeof(string) ) 
      { 
        // Strings direkt ausgeben 
        Console.WriteLine("[{0},{1}]={2}", 
        r,c,cells[r,c]); 
      } 
      else if(cells[r,c] is System.DateTime)  
      { 
        dt = (System.DateTime)cells[r,c]; 
        Console.WriteLine("[{0},{1}]={2}", 
        r,c, 
        dt.Format("dd.MM.yyyy", Globalization.DateTimeFormatInfo.InvariantInfo) 
        ); 
      } 
      else 
      { 
        // ToString gibt es immer 
        Console.WriteLine("[{0},{1}]={2}",  
        r,c,cells[r,c].ToString()); 
      } 
    } 
  } 
}// Print2 
}//Grid 
class GridApp 
{ 
  public static int Main(string[] args) 
  { 
    System.DateTime dt =  
    System.DateTime.Now; 
    Grid g = new Grid(3); 
    g[0,0] = "oben links"; 
    g[0,1] = "oben mitte"; 
    g[0,2] = "oben rechts"; 
    g[1,0] = 0; 
    g[1,1] = 1; 
    g[1,2] = 2; 
    g[2,0] = dt; 
    g[2,1] = dt.AddDays(1); 
    g.Print1(); 
    g.Print2(); 
    WriteLine("Cell[0,0]={0}", g[0,0]); 
    WriteLine("Row 0: {0}", g.RowString(0)); 
    WriteLine("Row 11: {0}", g.RowString(11)); 
    // Fehler provozieren und behandeln 
    try 
    { 
      g[4,4] = "ohje"; 
    } 
    catch( Exception e) 
    { 
      Console.WriteLine(e.ToString()); 
    } 
    return 0; 
  } 
}//GridApp 
}

Listing 4 zeigt dies in der Routine Print2. Die Grid-Klasse realisiert eine einfache Tabelle, in der Sie beliebige Werte ablegen können. Datumswerte und Strings sollen gesondert behandelt werden. Alle .NET-Typen führen aussagekräftige und umfassende Typinformationen mit sich. Da dies eine Basisfunktionalität ist, können Sie die Typinformationen über eine Funktion von object abrufen. Die C#-Anweisung typeof liefert die Typinformationen zu einer Klasse. Somit können Sie über folgenden Ausdruck feststellen, ob die Zelle ein String ist:

cells[r,c].GetType() == typeof(string)

Alternativ können Sie auch folgenden Ausdruck verwenden:

cells[r,c] is string

is stellt fest, ob eine Variable in einen Typ umgewandelt werden kann. Das gilt auch für Schnittstellen.

Datumswerte basieren im .NET-Framework auf dem Typ System.DateTime, C# verfügt nicht über einen eigenen Datumstyp.

Listing 4 verwendet den is-Operator, um einen Datumswert zu erkennen und die Ausgabe zu formatieren.

Listing 4 zeigt auch, wie Sie Eigenschaften (Properties) implementieren. Rows und Cols sind Nur-Lese-Eigenschaften und verfügen deshalb nur über einen get-Block. Wie schon bei den Funktionen gibt es für Eigenschaften kein Schlüsselwort, die Bedeutung ergibt sich aus dem Kontext. Beachten Sie, dass auf den Namen der Eigenschaft keine Klammern folgen.

Es gibt in C# das Schlüsselwort readonly, das jedoch nicht im Zusammenhang mit Eigenschaften verwendet wird. Sie können eine Variable als readonly kennzeichnen, so dass deren Wert nach der ersten Zuweisung – in der Regel innerhalb des Konstruktors – nicht mehr geändert werden kann. Das ist eher als eine Erweiterung des Konstantenkonzepts zu sehen und kann in einigen Situationen hilfreich sein.

 

C#-Extras

C# kennt eine besondere Eigenschaft: Den Indexer. Er erlaubt den indizierten Zugriff auf die Werte eines Objekts, und zwar direkt über den Klassennamen. Indexer treten an die Stelle der Default-Eigenschaften von VBA, zu deren berühmtesten Vertretern Caption gehört. Bei der Deklaration geben Sie this als Eigenschaftennamen an, ansonsten unterscheidet sich die Syntax durch nichts von der anderer Eigenschaftsroutinen. Der Indexer von Grid unterstützt den lesenden und schreibenden Zugriff auf die einzelnen Zellenwerte. Er verfügt daher über einen set- und einen get-Block. Der zugewiesene Wert ist über die Anweisung value verfügbar.

Der Indexer löst bei einem ungültigen Index mit throw eine Exception aus. Sie können auf diese Exception mit einer try-catch-Anweisung reagieren, wie

Listing 4 es zeigt. Auf der CD finden Sie das Projekt Simple3, das eine um die Ausnahmebehandlung erweiterte Version von Simple2 (Listing 2) ist. Sie wird immer dann aktiv, wenn Sie statt einer Zahl einen anderen Wert auf der Kommandozeile eingeben.

C# erlaubt das Überladen von Operatoren. Das bedeutet, dass Sie selbst festlegen können, wie Ihre Klasse Operatoren wie + oder == handhaben soll. Operator-Funktionen sind statische Funktionen, die mit dem Schlüsselwort operator gekennzeichnet werden. Auf der CD finden Sie die Klasse Point3D, die einige Operatoren überlädt, zum Beispiel den Additions-Operator:

public static Point3D operator + (Point3D p1, 
Point3D p2) 
{ 
return new Point3D( 
p1.x+p2.x, p1.y+p2.y, p1.z+p2.z); 
}

Sie können nicht alle Operatoren überladen, aber die wesentlichen. Zudem können Sie festlegen, wie Ihre Klasse in andere Typen konvertiert werden soll. Hier wird zwischen impliziten und expliziten Konvertierungen unterschieden, das heißt, einmal müssen Sie einen Konvertierungsbefehl angeben, das andere Mal geschieht die Konvertierung völlig transparent.

Neben Klassen kennt C# auch Strukturen, die fast alle Eigenschaften von Klassen haben, sich aber nicht durch Vererbung erweitern lassen. Der wesentliche Unterschied liegt in der Verwaltung: Instanzen von Klassen werden auf dem Heap erzeugt, Instanzen von Strukturen auf dem Stack. Die einfachen Datentypen wie System.Int32 oder System.Boolean (die den C#-Typen int und bool entsprechen) sind Strukturen.

Auch Aufzählungen basieren auf einer .NET-Klasse und werden in C# über enum vereinbart. Selbstverständlich können Sie auch Konstanten definieren:

const int EINS = 1;

Sie sind stets typisiert. Auch Aufzählungen sind streng typgebunden und können nicht so einfach wie in VBA mit Integer-Werten kombiniert werden. Dafür bieten .NET-Aufzählungen einige andere Vorteile [1]. In Zeichenketten sind die aus C/C++ oder auch JavaScript bekannten Escape-Zeichen verwendbar, wie zum Beispiel \n oder \t für Zeilenwechsel beziehungsweise Tabulator. Beachten Sie, dass Sie einen Backslash stets als \\ angeben.

Mit checked markieren Sie einen Anweisungsblock, in dem ein Überlauf nach einer arithmetischen Operation zu einer Ausnahmebehandlung führt. Mit unchecked können Sie das unterbinden, stattdessen erhalten Sie dann den Wert 0 zurück.

Eine als unsafe gekennzeichnete Funktion kann direkt auf den Speicher zugreifen, also mit Zeigern arbeiten. Mit fixed deklarierte Variablen werden von der automatischen Speicherverwaltung ausgenommen. Diese Mechanismen ermöglichen es auch, auf API-Funktionen zuzugreifen, für die Sie die Anweisungen DllImport und extern verwenden. Über Attribute können Sie dabei viel präziser als von VBA her bekannt festlegen, wie die C#-Typen in API-Typen umgewandelt werden.

 

Fazit

C# ist eine ernstzunehmende Programmiersprache, die sicher all denen gefallen wird, die gerne mit C++ und mit Visual Basic arbeiten. Die enge Verbundenheit mit dem .NET-Framework sorgt dafür, dass Sie in kurzer Zeit stabile Applikationen entwickeln können. Bei .NET tritt die Programmiersprache jedoch in den Hintergrund, da sich deren Funktionalität zu einem guten Teil aus den Framework-Bibliotheken ergibt. C# kommt mit einigen Schlüsselwörtern weniger aus als VB.NET und bietet mit out-Parametern und überladbaren Operatoren einige Extras. Vielleicht spielt für Sie auch die Option eine Rolle, unsicheren Code schreiben zu können.

Die wichtigste Erkenntnis ist jedoch folgende: Sie haben künftig die Wahl. Wählen Sie die bevorzugte Syntax und mischen Sie eventuell die Sprachen (auf Komponentenebene) in einer Weise, wie es einfacher noch nie möglich war.