"Microsoft has spoken. - Is anyone really listening?" [Sun Microsystems(8)]
C# und Java haben viele Gemeinsamkeiten, teilweise sind sich die Sprachen derart ähnlich, dass man schon ein zweites Mal auf den Programmcode schauen muss, um die Sprachen unterscheiden zu können. Jedoch sind unter C# zusätzliche Konzepte hinzu gekommen und so kommt es zu durchaus wesentlichen Unterschieden zwischen den Sprachen. Ich will hier zunächst die Gemeinsamkeiten auflisten, um später die Unterschiede aufzeigen zu können.
Properties sind bekannt aus Sprachen wie VisualBasic und Delphi. Sie geben die Möglichkeit, auf interne Daten und Code einer Klasse zuzugreifen. In Java wird dies durch Getter- und Setter-Methoden realisiert. Da diese Methoden oft angewandt werden stellt C# die Properties als zusätzliches Konzept zur Verfügung, was speziell bei RAD-Tools (Rapid Application Development) zu Vorteilen bei der Benutzung führt.
Java |
|
Bsp.setCounter( Bsp.getCounter() + 1); |
|
Aufruf get/set - Listing 4.1 |
C# | |
Bsp.Counter++; |
|
Aufruf Property - Listing 4.2 |
das Beispiel in C# ist intuitiver und offensichtlich besser lesbar - das spiegelt sich auch in zugehöriger nachfolgender Deklaration wieder:
Java |
|
public int getCounter() { return counter; } |
|
Deklaration Getter/Setter- Listing 4.3 |
C# | |
public int Counter { get { return counter; } set { |
|
Deklaration Property-
Listing 4.4 |
Besonders bei read/write-Properties ist C# etwas eleganter als Java, da die
get- und set-Methoden unmittelbar logisch zueinander gehören und dies so
nun auch optisch deutlich wird. Soll ein Property nur lesbar bzw. schreibbar
sein so lässt man einfach den set- bzw. get-Teil weg. Muss man beispielsweise
den Namen eines Properties ändern so geschieht dies in C# nur an einer
Stelle - in Java können die get- und set-Methoden theoretisch weit voneinander
entfernt auftauchen. Genauso sieht dies auch bei Kommentaren aus, die bei C#
so nur an einer Stelle auftauchen müssen.
Als einziges Problem könnte hier genannt werde, dass man als Nutzer der
Klasse nicht unterscheiden kann, ob es sich bei einem Wert um ein Feld oder
ein Property handelt. Allerdings sollte es generell selten vorkommen, dass Felder
als "public" deklariert werden.
Indexer sind ein Konzept in C# um Objekte wie Arrays zu behandeln, ähnlich
wie Properties nutzen auch sie get/set um Werte zu lesen bzw. zu schreiben.
Ein Beispiel:
C# | |
using System; //Indexer public int this[int index] { get { return anzPlaetze[index - 1]; } set { if ((index >= 1) && (index <=5)) anzPlaetze[index - 1] = value; } } |
|
Hoersaal als Indexer
- Listing 4.5 |
Event-Handling ist prinzipiell ein Prozess, bei dem ein Objekt ein anderes
Objekt informiert, dass ein Ereignis eingetreten ist. C# bietet eine direkte
Unterstützung von Events durch das Konzept der Delegates. Ein Delegate
kann man sich am ehesten als einen erweiterten Funktionszeiger vorstellen, wie
diese z.B. in C, C++ und Delphi (Closure) vorkommen. Mit 'event' deklariert
der Entwickler innerhalb einer Klasse ein Datenfeld zur Aufnahme eines Delegate,
also ein Funktionsobjekt. Mit dem Operator += kann nun einem Exemplar dieser
Klasse ein Funktionsobjekt zugeordnet und mit dem Operator -= wieder entfernt
werden. Das Prinzip ist im wesentlichen ein Registrieren und Entfernen einer
Callbak-Funktion einschließlich dessen Aufrufkontext.
Ein Beispiel mit einem Wecker als Event-Auslöser:
C# | |
using System; |
|
Event-Handling mit
Delegates am Beispiel eines Weckers - Listing 4.6 |
Man sieht hier, dass der event-auslösenden Klasse (Wecker) die event-behandelnden Klassen nicht bekannt sein müssen. Die Operatoren += bzw. -= in Bezug auf eine Event-Methode (i.Bsp. Klingeln) erlauben es, beliebig viele Event-Handler anzumelden bzw. zu entfernen also eine Abarbeitungskette aufzubauen. Das Einführen der Delegates durch Microsoft führte zu einer Auseinandersetzung über Sinn und Zweck dieses Prinzips, im Besonderen zwischen Sun und Microsoft. Sun beschreibt in einem veröffentlichten Whitepaper [Sun(9)] Delegates als unnötiges Konzept, das zudem der Objektorientierung wiederspricht. Microsoft reagierte und veröffentlichte seinerseits einen Artikel über Delegates in dem es klarstellt, dass deren Verwendung zu besser strukturiertem und so besser lesbarem Code führt. [Microsoft(10)]
Aufzählungstypen (Enums) sind Teil vieler gängiger Sprachen, wie Pascal, C, C++ und nun auch von C#. Java hat dieses Konzept nicht eingeführt, weil man Aufzählungstypen durch Klassen nachbilden kann. Die Philosophie bei Sun scheint zu sein, auf die Einführung neuer Sprachkonstrukte zu verzichten, wenn man das Ziel auch mit Klassen erreichen kann. Ob dies sinnvoll ist liegt wohl im Geschmack des Betrachters, denn die Sprachkonstrukte die einen spezifischen Fall beschreiben, können diesen u.U. präziser abbilden und somit in einem gewissen Rahmen das Programmieren erleichten.
C# | |
//Deklaration |
|
Aufzählungstyp
in C# - Listing 4.7 |
in Java wird der gleiche Sachverhalt folgendermaßen deklariert:
Java | ||
//Deklaration |
||
"Aufzählungstyp"
in Java - Listing 4.8 |
Der Typ der "ampel" ist im C#-Fall als "Ampelfarbe" gekennzeichnet
- in Java ist die ein ganz "normaler" int-Datentyp. Auch wenn intern
der Datentyp bei beiden gleich ist, so ist das C#-Beispiel eine bessere Abstraktion
des Problems und einfacher zu lesen. Im C#-Beispiel würde ein Ausgabe durch
"System.Console.WriteLine(ampel)" wirklich "gruen" ausgeben
und nicht den internen int-Wert.
In C# ist ein Wert aus einem enum auch nur über den Wertbezeichner anzusprechen
- ein Aufruf über Ordinalzahl ist nur durch explizite Typumwandlung möglich
(Ausnahme ist der Ordinalwert 0, der i.A. den Standardwert meint). Als Basistyp
für Enums ist neben dem Standard 'int' auch eine Reihe anderer Typen möglich
(byte, sbyte, short, ...), dieser muss nach dem enum-Bezeichner explizit angegeben
werden.
C# | |
//Deklaration public enum Ampelfarbe : byte |
|
Beispiel mit Angabe
des Ordinalwerts - Listing 4.9 |
In diesem Beispiel kann man auch sehen, dass es möglich ist mehreren Aufzählungswerten den gleichen Ordinalwert zuzuweisen. Bei Aufruf über den Ordinalwert wird dann der zuerst Gelistete benutzt. Enums sind ebenso wie die später vorgestellten Structs Werttypen.
In C# wurde ein foreach-Ausdruck eingeführt, der auf Objekten arbeitet, die Collections genannt werden. Collections sind Objekte, die folgendes Interface implementiert haben:
C# | |
interface IEnumerable |
|
Interface IEnumerable
- Listing 4.10 |
C# | |
interface IEnumerator |
|
Interface IEnumerator
- Listing 4.11 |
Im .NET Framework sind diese Klassen im Namensraum "System.Collections" zu finden. So ist zum Beispiel ein Array eine Collection über das man mit dem foreach-Ausdruck iterieren kann.
C# | |
string[] namen = {"Andreas", "Michael", "Uwe" }; //Iterieren über ein Collection (hier Array) foreach (string s in namen) |
|
foreach mit String-Array
- Listing 4.12 |
Neben Klassen gibt es in C# Strukturen, die durch das Schlüsselwort 'struct' eingeleitet werden. Structs sind in Sprachen wie Delphi und Visual Basic als zusammengesetzte Typen oder Records bekannt. Auch in C gibt es Structs. Die Möglichkeiten der Structs in C# sind jedoch umfangreicher. In C# sind diese mehr mit Klassen zu vergleichen, denn sie können auch Code (Methoden) und sogar einen Konstruktor enthalten. Trotz vieler Ähnlichkeiten gibt es aber wesentliche Unterschiede. Die Datentypen werden als Werttypen und nicht wie Klassen, Arrays oder Strings als Referenztypen angelegt. Somit werden Structs auf dem Stack erzeugt, im Gegensatz zu Klassen, die immer auf dem Heap angelegt werden. Sie eignen sich für Daten, die nicht zu groß sind, die in großer Zahl vorkommen und für die eine eigene Klasse zuviel Aufwand wäre. Ein weiterer Unterschied zu Klassen ist, dass Structs keinerlei Vererbung erlauben also weder beerbt werden noch vererben können. Mit Boxing- und Unboxing bietet C# einen wirksamen Mechanismus zur Umwandlung von Wert- und Referenztypen und wieder zurück. Microsoft bezeichnet dies als "unified type system". Die Wrapper-Klassen, die in Java nötig sind werden hier quasi vom Compiler automatisch erzeugt.
C# erlaubt mit dem Konzept der Structs den Satz der primitiven Typen zu erweitern. Es ist in der Tat so, dass in C# alle primitiven Typen als Structs angelegt sind, so ist "int" ein Alias für den Struct 'System.Int32'.
Mit virtuellen Methoden wird es objektorientierten Sprachen ermöglicht Polymorphismus auszudrücken. Als Vorgabe werden Methodenaufrufe im Gegensatz zu Java in C# früh gebunden (early-binding). Ist eine späte Bindung (late-binding) erfordert muss der Entwickler eine Methode ausdrücklich als 'virtual' kennzeichnen. In Java sind grundsätzlich alle Methoden erstmal virtuell, d.h. Methoden abgeleiteter Klassen mit gleicher Signatur können die Methoden aus der Basisklasse überschreiben. Wie erwähnt muss in C# ähnlich wie in C++ und Delphi eine Methode in der Basisklasse als 'virtual' und die überschreibende Methode mit 'override' (nicht in C++) deklariert werden.
C# | |
using System; |
|
Polymorphismus in
C# - Listing 4.13 |
Wird versucht eine nicht-virtuelle Methode zu überschreiben resultiert dies in einem Compile-Time-Error, es sei denn die überschreibende Methode wurde mit dem Schlüsselwort 'new' deklariert. In diesem Fall entsteht eine neue unabhängige Methode, die die Mehtode aus der Basisklasse verdeckt. Vergleicht man diese Schreibweise mit der aus Java dann ist im Quellcode besser zu erkennen welche Methoden überschrieben werden. Ein Vorteil für nicht-virtuelle Methoden ist die höhere Ausführgeschwindigkeit, da nicht zur Laufzeit überprüft wird zu welcher Methode gebunden werden muss. Wobei diese Möglichkeit der nicht-virtuellen Methoden auch in Java problemlos erreicht werden kann. Im Prinzip ist es also so, dass nur der Default bei C# und Java anders gesetzt ist.
Zwischen den Interfaces in Java und C# gibt es nur einen kleinen Unterschied. Die Nutzung ist in C# etwas flexibler, denn besitzt eine Klasse zwei oder mehr Interfaces muss in Java darauf geachtet werden, dass die jeweiligen Interfaces keine gleichen Methoden deklariert haben. In C# wird das Problem gelöst, indem man bei der Implementierung der Methode den Bezeichner des Interfaces voranstellt.
C# | |
public interface I { |
|
Interfaces und Implementierung
- Listing 4.14 |
In C# gibt es im Vergleich zu Java erweiterte Möglichkeiten zur Parameterübergabe. Ohne Angabe erfolgt die Übergabe als Wert. Nachfolgend sind die möglichen Parameter-Schlüsselworte erläutert. Microsoft hat sich an die Vorgabe der UML (Unified Modeling Language) gehalten, bei der die Parameterübergabe in den Formen 'in', 'out' und 'inout' erfolgen kann [Object Management Group(11)]. 'in' entspricht in C# der reinen Wertübergabe, 'inout' heißt in C# 'ref' und 'out' ist identisch benannt.
'ref':
Bei C# ist es im Vergleich zu Java möglich Refernzparameter festzulegen.
Der Parameter muss als Referenzparameter sowohl beim Deklarieren als auch beim
Übergeben als ein solcher gekennzeichnet werden (anders als z.B. in C++).
Dies geschieht mit dem Schlüsselwort 'ref'. Ein Parameter der so gekennzeichnet
ist muss zwingend vor der Übergabe initialisiert sein.
'out':
Bei Parametern, die als 'out' deklariert werden diese als nicht initialisiert
angenommen - die aufgerufene Methode muss dafür sorgen, dass die übergebene
Variable einen Wert zugewiesen bekommt.
'params':
Dieser Modifier kann nur in Verbindung mit dem letzten Parameter auftreten.
So können beliebig viele Parameter eines bestimmten Typs übergeben
werden.
In C# ist es möglich direkt mit Zeigern zu arbeiten. Hierzu müssen diese Bereiche mit dem Schlüsselwort 'unsafe' deklariert werden. Der Bezeichner 'unsafe' macht bereits deutlich, dass diese Möglichkeit nur mit Vorsicht verwendet und möglichst vermieden werden sollte. Nicht umsonst hat Sun diese Möglichkeit in Java bewusst unterbunden.
Durch die Zeigerarithmetik wird es prinzipiell auch ermöglicht z.B. Gerätetreiber in C# zu schreiben oder mit COM-Objekten zu kommunizieren. Bei Benutzung von Zeigern und unsafe-Bereichen ist darauf zu achten, dass hier keine Garbage-Collection durchgeführt wird, man sich also selbst um die Freigabe von Ressourcen kümmern muss.
Der Switch-Ausdruck in C# funktioniert im wesentlichen analog zu dem in Java, jedoch kann dieser in C# zusätzlich mit Enums und Strings umgehen. Ein beliebter Fehler in Sprachen wie C, C++ und Java ist das unbeabsichtigte Weglassen der 'break'-Anweisung in einem case-Block, wodurch oftmals ein unerwünschtes Programmverhalten entsteht. Um dies zu vermeiden müssen 'case'-Blöcke in C# nun zwingend mit 'break' abgeschlossen werden, worauf man ggf. durch einen Compile-Fehler aufmerksam gemacht wird.
C# | |
string str = "aaa"; switch (str) { case "aaa" : Console.WriteLine('aaa'); break; case "bbb": Console.WriteLine('bbb'); break; default: Console.WriteLine(str); break; } |
|
Case-Verteiler mit
String - Listing 4.15 |