Westfälische Wilhelms-Universität Münster Ausarbeitung C# (Im Rahmen des Seminars – Programmiersprachen) Markus Nestvogel Themensteller: Prof. Dr. Herbert Kuchen Betreuer: Roger Müller Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft Inhaltsverzeichnis 1 Einleitung........................................................................................................................1 2 Microsofts .NET............................................................................................................. 1 2.1 Das .Net Framework............................................................................................... 2 2.1.1 Intermediate Language...................................................................................... 2 2.1.2 Common Type System...................................................................................... 4 2.1.3 Werttypen.......................................................................................................... 5 2.1.4 Verweistypen..................................................................................................... 6 2.1.5 Common Language Specification......................................................................7 2.2 Framework Class Library (FCL).............................................................................9 2.3 Common Language Runtime (CLR).......................................................................9 2.3.1 Typverwaltung und Assemblies........................................................................ 9 2.3.2 Codeverwaltung...............................................................................................11 3 C#..................................................................................................................................12 3.1 Klassen und Vererbung.........................................................................................13 3.2 Grundsätzlicher Aufbau........................................................................................ 15 3.3 Eigenschaften........................................................................................................16 3.4 Ereignisbehandlung und Delegaten...................................................................... 16 3.5 Quicksort...............................................................................................................17 4 Fazit.............................................................................................................................. 19 5 Literaturverzeichnis...................................................................................................... 20 6 Anhang..........................................................................................................................22 6.1 Quicksort in C#.....................................................................................................22 6.2 Quicksort in Java.................................................................................................. 24 II 1 Einleitung 1 Einleitung Im Juni 2000 stellte Microsoft erstmals C# als eigens auf .NET abgestimmte objektorientierte Programmiersprache der Öffentlichkeit vor und veröffentlichte gute 20 Monate später die finalen Versionen. Die Ausarbeitung versucht einen Überblick über die Besonderheiten von C#, insbesondere im Vergleich zu ihren direkten Verwandten C++ und Java, zu geben. Eine Betrachtung von C# unabhängig von .NET ist kaum möglich und auch nicht ratsam. Daher wird dieser Bereich im ersten Kapitel ausführlich besprochen. Darauf folgend geht der Text auf C# als solches und insbesondere auf die auffälligsten Neuerungen gegenüber anderen objektorientierten Programmiersprachen ein. Das zweite Kapitel endet mit einem Vergleich von Implementierungen des Quicksort-Algorithmus in C# und Java. Abschließend wirft die Arbeit noch einen Blick auf die Aufgabenbereiche der Sprache C# und versucht ihre Zukunftschancen zu bewerten. 2 Microsofts .NET Microsoft definiert sich und seine Unternehmensphilosophie in seinen Pressemitteilungen wie folgt: empower people through great software - any time, any place and on any device. [MS1] Das im Juli 2000 vorgestellte .NET folgt diesem übergeordneten Unternehmensziel in einer für Microsoft bisher für nicht möglich gehaltenen Konsequenz, es setzt sich die „Verknüpfung von Informationen, Menschen, Systeme und Geräte“ zum Ziel [MS2]. Zur Verwirklichung dieser Strategie richtet Microsoft seine gesamte Unternehmenspolitik auf .NET ab, im folgenden interessieren wir uns aber nur für das .NET Framework als technisches Fundament des Gesamtkonzeptes. Hinter dem groß vermarkteten Schlagwort .NET steckt nämlich weitaus mehr als zur Besprechung von C# nötig ist. 1 2 Microsofts .NET 2.1 Das .Net Framework Das .NET Framework von Microsoft besteht im wesentlichen aus zwei Elementen; zum einen ist eine Laufzeitumgebung namens Common Language Runtime (CLR) zu nennen, zum anderen eine Basisklassenbibliothek namens Framework Class Library (FCL). Diese zwei stellen Microsofts eigene Implementierung der ebenfalls im .NET festgelegten Spezifikationen der Common Language Specification (CLS), der Intermediate Language (IL) und des Common Type Systems (CTS) dar. Diese sind Bestandteil der Common Language Infrastruktur (CLI), die von der European Computer Manufactures Association [ECMA1] und der ISO [ISO1] standardisiert wurde. Dadurch ist es möglich das viele verschiedene Programmiersprachen auf .NET aufsetzen können, zur Zeit buhlen ca. 20 Sprachen um die Gunst des Programmierers [MS3]. Die wichtigsten sind wohl C++ mit verwalteten Erweiterungen, VisualBasic.NET, Delphi.NET, das Javaderivat J# und natürlich C#. Auch funktionale Programmiersprachen wie Haskell finden ihren Weg nach .NET. Grundsätzlich ist es möglich auch funktionale Sprachen in IL zu übersetzen, für eine noch bessere Unterstützung veröffentlichte Microsoft aber eine erweiterte Version der IL, die ILX. Dennoch bleibt der Code abwärtskompatibel zur IL [MSR]. Auch die .NET auf den Leib geschneiderte Sprache C# hat Microsoft bei der ECMA [ECMA2] und der ISO [ISO2] standardisieren lassen. Hierdurch ist es prinzipiell jedem möglich die gesamte .NET Umgebung auch auf andere Plattformen als Windows zu portieren und so einen wichtigen Vorteil von Java auszugleichen. Für FreeBSD hat Microsoft diese Aufgabe bereits selbst übernommen. Unter Linux ist für Mitte 2004 zum Beispiel das finale Release des Mono-Projektes geplant [MONO], die Entwickler haben sich das ehrgeizige Ziel gesetzt nicht nur die CLI und C# zu implementieren, sondern auch einen Großteil der FCL zu portieren. Ein weiteres Open Source Projekt stellt dotGNU dar, als Besonderheit soll hier vorerst kein Just-In-Time Kompiler (siehe 2.3.2) sondern nur ein Interpreter für die Codeausführung sorgen [DG]. 2 2 Microsofts .NET 2.1.1 Intermediate Language Die meisten herkömmlichen Compiler liefern Maschinencode, der auf eine bestimmte Prozessorarchitektur abgestimmt ist. .NET Compiler aber erzeugen Binärdateien (Assemblies) mit Metadaten und einem Zwischencode namens Intermediate Language (IL). Diese Assemblies können nicht direkt auf einem Prozessor ausgeführt werden, stattdessen laufen sie innerhalb der CLR. Die IL kann man als eine Art höheren Assemblercode ansehen, die Unterstützung objektorientierter Funktionalität wie etwa die Ideen von Klassen, Kapselung und Verbergen von Daten und auch die Vererbung sind fest in sie integriert. Dafür existieren in IL mehr als 220 Instruktionen [MS4], also wesentlich mehr als bei x86 Assembler. .locals init ([0] int32 int a = 2341; a, [1] int32 b, int b = 2; int c = a+b; [2] int32 c) IL_0000: IL_0005: IL_0006: IL_0007: IL_0008: IL_0009: IL_000a: IL_000b: ldc.i4 0x925 stloc.0 ldc.i4.2 stloc.1 ldloc.0 ldloc.1 add stloc.2 0 sipush 2341 3 istore_1 4 iconst_2 5 istore_2 6 iload_1 7 iload_2 8 iadd 9 istore_3 10 return Abbildung 1 Addition zweier Integer in IL und Java Bytecode Entgegen dem Assembler Code werden lokale Variablen in der CLR nicht auf dem Stack geladen, stattdessen liegen sie im Speicher. Erst durch einen Methodenaufruf werden Werte durch Ladebefehle vom Typ ld aus dem Speicher in den Stack geladen. Hat die Methode ihre Arbeit vollendet, werden die Werte aus dem Stack durch Speicherbefehle vom Typ st wieder in den Speicher geschrieben. Hier werden die Werte von einem Methodenaufruf zum nächsten gespeichert. Anders als im Java Bytecode sind die Typinformationen der einzelnen Argumente meistens nicht Teil der Instruktionen, sondern werden von dem Just In Time Compiler aus dem Stackinhalt bestimmt. Dadurch wird ganz im Sinne der Sprachunabhängigkeit 3 2 Microsofts .NET der Compilerbau erleichtert und die Typbestimmung der CLR überlassen. Da dies einen Interpreter sehr verlangsamen würde, verzichtet Microsofts .NET, im Gegensatz zu Java, auf die Möglichkeit des Interpretierens, also des Zeilenweisen Ablaufen des Codes. Ebenso wie Sun für seinen Bytecode stellt auch Microsoft für die IL ein Tool (ildasm.exe) zur Anzeige des Codes zur Verfügung, so kann man die Typdefinitionen und Namen von Methoden in schlecht oder gar nicht kommentierten Klassen herausfinden und leichter in eigenen Code weiterverwenden. Allerdings steigt so auch die Gefahr, dass Code geklaut werden kann und unrechtmäßig in fremden Programmen wiederverwendet wird. Programme wie der Salamander .NET Decompiler [SAL] können IL sogar komplett in Hochsprache zurückübersetzen. Durch Verschlüsselung können aber zumindest die Methoden- und Variablennamen unkenntlich gemacht werden. 2.1.2 Common Type System Normalerweise ist das Typsystem in den Compiler einer Sprache integriert; in .NET wandert es aber als Common Type System (CTS) in die CLR und steht daher allen darauf ablaufenden Sprachen zumindest grundsätzlich zur Verfügung. Dadurch gibt es keine verschiedenen Typdefinitionen mehr. So ist zum Beispiel der Datentyp short in C# und in VB.NET nicht nur per Definition gleich, tatsächlich benutzen beide Sprachen sogar dieselbe Implementierung dieses Typs. Typen, die innerhalb des CTS definiert sind, werden als verwaltete Typen (managed Types) bezeichnet, außerdem ist es möglich nicht verwaltete Typen zu verwenden, dann ist der Code aber weder sprachübergreifend einsetzbar noch ist die Typsicherheit innerhalb der CLR gesichert. Da alle Datentypen (Klassen) in IL vorliegen, wird eigener Quellcode nach Übersetzung automatisch selbst Teil des CTS und steht ebenfalls allen auf .NET aufsetzenden Sprachen zur Verfügung. 4 2 Microsofts .NET Datentyp (System.Object) Werttypen (System.ValueType) In die Sprache integrierte Werttypen (int, bool etc.) .NET-Werttypen (System.Int32,Point, Recangel etc.) benutzerdefinierte Werttypen (struct) Aufzählungsrypen (System.Enum) .NET-Aufzählungstypen (System.Int32, etc.) benutzerdefinierte Aufzählungstypen (enum) Verweistypen deklarierte Datentypen .NET-Schnittstellen (System.IFormtable etc.) benutzerdefinierte Schnittstellen (interface) benutzerdefinierte Zeigertypen (*-Operator; unsafe) Quelle: c’t 15/03 S. 212 implementierte Datentypen Arrays (System.Array) Delegaten (Funktionszeigerprotot.; System.Delegate) .NET-Delegaten (System.EventHandler etc.) benutzerdefinierte Delegaten (delegate, event) Klassen Boxing-Klassen (als Verweistypen verpackte Werttypen) .NET-Klassenhierarchie (für Einzelsprache sichtbar) benutzerdefinierte Klassen (class) systematische Kategorie .NET-Kategorie Benutzerdef. C#-Kategorie Abbildung 2 Das CTS aus der Sicht von C# Wie in Abbildung 2 zu sehen, sind alle Datentypen direkte beziehungsweise indirekte Ableitungen des „Urdatentyps“ System.Object. Also werden auch elementare Datentypen wie Integer als Klasse und ihre Werte als Objekte gehandhabt. Im Gegensatz zu den verbreitetsten objektorientierten Programmiersprachen ist dies ist etwas absolut neuartiges. Selbst in Java sind die primitiven Datentypen keine Klassen im Objektorientierten Sinn, denn sie besitzen weder Eigenschaften noch Methoden [C#K02, S.116]. Dort werden diese erst durch den Einsatz von Wrapper-Klassen bereitgestellt. Um die Performance möglichst hoch zu halten, sind die Klassen aber auch innerhalb .NET fest implementiert und mit dem Modifizierer sealed versehen, das heißt, sie können nicht für eigene Ableitungen verwendet werden, wohl aber beinhalten sie Methoden. Durch diese konsequente Umsetzung des objektorientierten Gedankens gibt es auf erster Ebene nur genau zwei Arten von Typen, Werttypen und Verweistypen. 2.1.3 Werttypen Wie es der Name schon sagt, repräsentieren Werttypen echte Werte. Hierzu gehören neben den elementaren Datentypen wie int, short oder double auch komplexe Werttypen 5 2 Microsofts .NET wie Aufzählungstypen (enums) und Strukturen (structs). Besonders auffallend ist der von Microsoft aus Visual Basic übernommene Typ decimal. Dieser eignet sich insbesondere für finanzmathematische Berechnungen, da er bis auf die 28. Dezimalstelle exakt berechnet. Dies wird durch eine 128-Bit-Repräsentation des Wertebereichs ±1,0 * 10-28 bis ±7,9 * 10-28 erreicht. Werttypen werden auf dem so genannten Evaluation Stack der CLR gespeichert und verbleiben dort bis zum Ende ihres Geltungsbereichs, also ihres Methodenaufrufs. Danach wird der Evaluation Stack wieder geleert. 2.1.4 Verweistypen Man kann Verweistypen auch als objektorientierte Zeiger verstehen. Bei der Definition einer Variablen eines Verweistyps, wird ein neues Objekt im Heap Speicher der CLR angelegt, das später mit Variablen gefüllt wird. Im Stack wird lediglich ein Verweis auf den zum Objekt gehörenden Speicherbereich im Heap abgelegt. In C# bemerkt man den Unterschied zwischen Wert- und Verweistypen an der Wirkung des Zuweisungsoperators >>=<<. Einem Verweistyp wird ein weiterer Verweis auf ein- und denselben Speicherbereich zugewiesen, wohingegen bei einem Werttypen eine weitere Kopie des Wertes angelegt wird. Eine Ausnahme bilden hier die Strings, die zwar Verweistypen sind, sich aber wie Werttypen verhalten. Dies geschieht, da der Stack durch seine einfacheren Organisation dem Heap in Sachen Geschwindigkeit einiges voraus ist. Laut Definition sind sowohl Wert als auch Verweistypen Objekte, aber nur die Verweistypen werden, wie bei Objekten üblich, auf dem Heap gespeichert. Nun stellt sich die Frage wie ein Werttyp denn eine Objektmethode aufrufen kann. Dies geschieht durch das so genannte Boxing und Unboxing. Ruft ein Werttyp eine Objektmethode auf, wird von der CLR automatisch ein temporäres Objekt auf dem Heap angelegt und der Inhalt des Werttyps dort hinein kopiert. Nach der Ausführung des Methodenaufrufs wird das Objekt wieder gelöscht und die CLR arbeitet mit dem Werttyp weiter. Möchte man das Arbeiten mit temporären Ojekten vermeiden, kann man durch explizites Boxing Werttypen manuell in Verweistypen umwandeln. Den 6 2 Microsofts .NET umgekehrten Effekt erreicht man durch manuelles Unboxing, wobei dieser Vorgang nur für Objekte erlaubt ist, die durch Boxing erzeugt wurden. Objekte vordefinierter oder selbst definierter Klassen können nicht in Werttypen umgewandelt werden.[C#21] // implizites Boxing string s = 7.GetType().ToString(); i 123 // explizites Boxing int i = 123; object 0 = i; int j = (int) o; o System.Int32 123 j 123 Quelle: Die .NET CLR, [MS5] Abbildung 3 Boxing und Unboxing Objekte werden in der Regel mit Hilfe des new-Operators erzeugt, ausgenommen Strings, hier übernimmt der Compiler automatisch das Erzeugen eines Objektes. Sobald es keine Verweise auf ein Objekt mehr gibt ist es unwiderruflich verloren. Damit der Speicher nicht voll mit solchen „Objektleichen“ wird, durchläuft die automatische Garbage Collection periodisch den Heap und entfernt alle Objekte für die kein Verweis im Stack gefunden wird. Innerhalb der Verweistypen fallen die Delegaten (delegates) ins Auge, da auf sie im Kapitel 3 noch näher eingegangen wird, sei hier nur der Vollständigkeit halber erwähnt, dass sie eine objektorientierte Version von Funktionszeigern darstellen. 2.1.5 Common Language Specification .NET versucht die Grundlage von möglichst vielen verschiedenen Programmiersprachen zu sein. Damit all diese Sprachen von der gemischtsprachigen Programmierung 7 2 Microsofts .NET profitieren können, müssen sie auf ein gemeinsames Fundament aufbauen. Dieser wird von Microsoft in Form der Common Language Specification (CLS) festgelegt. Wie in 2.1.2 besprochen, spezifiziert das CTS die Typen, die in Programmen verwendet werden dürfen, um auf der CLR zu laufen. Die CLS hingegen legt fest, wie das CTS innerhalb der Sprachen verwendet werden muss um kompatiblen, sprachübergreifenden Code zu erzeugen. Laut der ersten Regel [ECMA – 335, S. 26] betrifft die CLS alle öffentlichen Teile einer Typdefinition, wobei interne Teile sich nicht nach diesem Standard richten müssen. Im wesentlichen regelt die CLS die Menge der grundlegenden Datentypen und stellt einige Regeln für die Namensgebung auf. So dürfen sich Namen nicht nur durch Großund Kleinschreibung unterscheiden, da nicht alle auf .NET aufsetzenden Sprachen „Case Sensitive“ sind. Auch dürfen Methoden und Felder nicht die gleichen Namen besitzen, wenn sie CLS kompatibel sein wollen. Alle Regeln der CLS ergeben einen Kompatibilitätsstandard, der es ermöglicht, dass ein in einer anderen Sprache implementierter Datentyp so benutzt werden kann, als ob er in der eigenen geschrieben wäre. So wird erreicht, dass Programmteile aus verschiedenen Sprachen aneinander Variablen übergeben können. Also handelt es sich bei CLSkonformen Datentypen um echte Basisklassen im Sinne der objektorientierten Programmierung, die auf Wunsch auch abgeleitet werden können. Dadurch kann .NET allen Sprachen eine umfangreiche gemeinsame Basisklassenbibliothek zur Verfügung stellen und muss sie nicht für jede einzelne Sprache portieren. Sprachen die jeden CLS kompatiblen Typen verarbeiten können, nennt man CLS Customer und solche, die jeden vorhandenen CLS – konformen Typen erweitern können, nennt man CLS – Extender. C# ist, als Haus und Hof Sprache des .NET Systems, sowohl Consumer als auch Extender. 8 2 Microsofts .NET 2.2 Framework Class Library (FCL) Die FCL stellt die Basisklassenbibliothek des .NET Frameworks dar, ihre über 2500 Klassen bieten dem Entwickler einen reichhaltigen Fundus an grundlegenden Komponenten als Ausgangspunkt für eigene Programme. Im wesentlichen ermöglichen die Klassen den objektorientierten Zugriff auf die Funktionen des unter dem .NET Framework liegenden Betriebssystems. Insbesondere unter Microsofts eigenem Betriebssystem Windows scheint die FCL den Java Foundation Classes überlegen, da sie sich eng an die verbreiteten Windows Standards hält. Dies erlaubt dem Entwickler zum Beispiel ein leichtes verwenden von standardisierten Layouts. 2.3 Common Language Runtime (CLR) Die CLR ist für die eigentliche Ausführung des in IL vorliegenden Codes zuständig. Dazu obliegen ihr mehrere Aufgabenbereiche. Marshalling (Inter-Prozesskommunikation) COM-Interop API-Anbindung Prozessraumverwaltung Application Domains Freispeicherverwaltung Garbage Collection Common Language Runtime (CLR) Codeverwaltung Quelle: C# Kompendium S. 42 Typverwaltung Namensbereiche Ausführungskontrolle Gemeinsam genutzte Assemblies (Global Assembly Cache) Typsicherheit private Assemblies Assembly-Loader Codesicherheit JIT Manifest Abbildung 4 Aufgaben der CLR 2.3.1 Typverwaltung und Assemblies Ein Ziel von .NET ist die Abschaffung der in der klassischen Windowswelt nötigen Registrierung und der damit verbundenen „dll-Hölle“. 9 2 Microsofts .NET Die .NET Typverwaltung muss sich daher anders die Informationen über Codekomponenten verschaffen und selbstständig verwalten. Aus diesem Grund ist ein Blick auf die durch einen Compiler erzeugten Assemblies notwendig. Veranlasst man einen Compiler Quellcode in die IL zu übersetzen, so erhält man, falls eine Main() Methode vorhanden ist, eine .exe – Datei, fehlt diese, erhält man eine reine Klassenbibliothek mit der Dateierweiterung .dll. Beide haben zwar vertraute Namen, sind aber ohne installiertes .NET Framework nicht ausführbar. In einem Assembly ist neben dem reinen Codeabschnitt zusätzlich ein Ressourcenabschnitt, in dem Verweise auf andere Ressourcen wie Bilder, Dokumente und andere Klassen gespeichert werden, enthalten. Für die Typverwaltung interessanter ist aber das so genannte Manifest; es besteht aus Metadaten, die Informationen über die öffentlich zur Verfügung stehenden Typen, die Ressourcen- und Datendateien beinhalten. Ferner offerieren sie Versionsinformationen, ggf. zusätzlichen Informationen zum Autor und Copyright Bestimmungen und auch Informationen zur Verschlüsselung können Bestandteil sein. Möchte man ein Assembly auch anderen zugänglich machen, kann man es beim Global Assembly Cache (GAC) anmelden, standardmäßig sind Assemblies aber privat. Wird nun auf ein Assembly verwiesen, sucht die Typverwaltung zunächst im privaten Bereich der Anwendung, wird sie dort nicht fündig, so wird im GAC gesucht. Dort können nun, im Gegensatz zur klassischen .dll Registrierung, mehrere Versionen eines Assembly parallel existieren, da ja die benötigte Versionsnummer in den Metadaten gespeichert wird. Böse Überraschungen, dass Programme nach der Installation eines neuen nicht mehr funktionieren, sollten so der Vergangenheit angehören. In Java mussten bisher Deployment Deskriptoren erstellt werden, um Anwendungen während der Laufzeit mit Informationen zu versorgen. Ab der für Mitte 2004 angekündigten Java-Version 1.5 soll aber ein näher an .NET orientiertes Gerüst für die Metadaten eingeführt werden [CW]. Für den Entwickler ist dann der augenfälligste Unterschied, dass in einem Assembly, im Gegensatz zu einem Java Class File, mehrere Klassen gespeichert werden können. 10 2 Microsofts .NET 2.3.2 Codeverwaltung JIT – Kompilierung Die Assemblies liegen in IL vor und weil .NET auf einen Interpreter verzichtet, muss der Code zur Ausführung in nativen Maschinencode übersetzt werden. Diese Aufgabe übernimmt der in die CLR eingebettete JIT-Compiler, der während der Laufzeit den benötigten Codeabschnitt methodendweise übersetzt. Der übersetzte Code wird nach Verwendung im Cache zwischengespeichert, so dass häufig benutzte Methoden nicht ständig erneut kompiliert werden müssen. Bei Installation eines Assemblies in den GAC läuft der JIT-Compiler einmalig ab, gespeichert wird dann der optimierte Maschinencode. Application Domains Normalerweise isoliert das Betriebssystem parallel ablaufende Anwendungen in verschiedenen Prozessen, die alle über einen eigenen Adressraum verfügen, so dass sie sich nicht gegenseitig stören. Das Erzeugen der Prozesse, sowie ein ProgrammcodeAufruf zwischen ihnen und dem damit verbundenen „Marshalling“, ist sehr zeitaufwendig. Mit dem .NET Framework führt Microsoft das Konzept der Application Domains ein. Es sieht vor, dass sich mehrere Anwendungen einen einzigen Prozessraum teilen und darin als Thread ausgeführt werden. Dies hat den Vorteil, dass die Erzeugung einer Application Domain schneller erfolgt als das Erzeugen eines Prozesses. Auch ein Programmcode-Aufruf zwischen ihnen läuft schneller als zwischen Prozessen ab. Computer Prozess Prozess Verwaltete Umgebung Freispeicherverwaltung, Typsicherheit auf Basis von Laufzeittypen, Codesicherheit, Lizensierung Application Domain Anwendung Unverwaltete Umgebung Application Domain Anwendung Anwendung Objekt Objekt Objekt Objekt Objekt Objekt Objekt Objekt Objekt Objekt Objekt Objekt Direkter Speicherzugriff möglich, COM – Komponenten, Bibliotheken mit APIRoutinen Abbildung 5 Application Domains in verwalteter Umgebung 11 2 Microsofts .NET Um dieses Konzept durchführen zu können und um vor ungewollter oder böswilliger gewollter Störung zu schützen, muss der beteiligte Code typsicher sein. Der Code, der diese Bedingung erfüllt, wird verwalteter (managed) Code genannt. Im Gegensatz dazu steht der nicht verwaltete (unmanaged) Code, er wird wie bisher in externen Prozessen ausgeführt. Auf diese Weise können bisherige Bibliotheken auch ohne eine Portierung nach .NET weiter verwendet werden. C# Code ist grundsätzlich typsicher, ansonsten muss er explizit als „unsafe“ deklariert werden und der Compiler vor dem Übersetzungsvorgang in IL davon in Kenntnis gesetzt werden. Durch ein solches Vorgehen kann man sogar aus .NET heraus direkt auf den physischen Speicher zugreifen und beispielsweise sehr hardwarenah programmieren. Geschieht dies aber bei verteilten Anwendungen, so können solche nur von Benutzern mit Administratorrechten für das jeweilige Netzwerk ausgeführt werden. 3 C# Um die Funktionen des .NET Frameworks vollständig ausreizen zu können, hätte aller Sprachunabhängigkeit zum Trotz, jede bisherige Sprache mehr oder weniger stark verändert werden müssen. Aus diesem Grund schuf Microsoft eine sehr stark an Java erinnernde Sprache, die alle Möglichkeiten von .NET zu nutzen vermag. Offiziell wird Java niemals als Ahne angegeben, stattdessen heißt es über die neue Sprache immer, sie sei eine Sprache, die die Einfachheit von Visual Basic und die Mächtigkeit von C++ in sich vereint [MS6]. Obwohl man schon zweimal hingucken muss um ein C# von einem Java Programm zu unterscheiden, gibt es im Innern doch einige Veränderungen. All diese zu beleuchten würde den Rahmen des Kapitels sprengen, deshalb werden im Folgenden nur einige der auffälligsten Neuerungen gegenüber den klassischen objektorientierten Programmiersprachen besprochen. 12 3 C# 3.1 Klassen und Vererbung Ohne einen Mechanismus zur Vererbung ist keine objektorientierte Programmiersprache vorstellbar, erst recht nicht, wenn es sich um eine Sprache für .NET handelt und alle Klassen per Definition von System.Object abgeleitet werden. Wie dieses Konzept umgesetzt wird ist aber wiederum jeder Sprache selbst überlassen, so stellt C# ein ganz ähnliches Konstrukt wie Java zur Verfügung. Wie auch dort werden eigene Typen durch Klassen implementiert, sie können Methoden, Konstruktoren, Konstanten, usw. enthalten. Eine Klasse wird wie folgt deklariert: [Modifizierer] class Klassenname [: {Basisklasse | Schnittstelle[, ...]}] Der Modifizierer wird standardmäßig auf internal gesetzt, dadurch ist die Sichtbarkeit auf das aktuelle Projekt begrenzt. Ferner stehen dem Programmierer public, protected, internal protected und private zur Verfügung. Soll die Klasse von einer anderen Erben, so wird dies durch einen : und dem Basisklassennamen kenntlich gemacht. Vererbung Quelle: C# Kompendium S. 317 Einfachvererbung Implementierungs Vererbung class Mehrfachvererbung partielle Implemen Tierungsvererbung abstract class nicht polymorphe Implementierung new schnittstellenVererbung interface polymorphe Implementierung virtual, override Abbildung 6 Vererbung in C# Mehrfachvererbung ist in C# nicht vorgesehen, wohl aber die Verwendung von mehreren interfaces (Schnittstellen). Wie in Java repräsentieren diese das Pflichtenheft der ableitenden Klassen, sie definieren alles was die erbende Klasse implementieren muss. 13 3 C# Sollen übernommene Methoden überschrieben werden können, muss ihnen in der Basisklasse der Modifizierer virtual und der überschreibenden Methode override vorangestellt sein. Dadurch, und weil Methoden standardmäßig nicht überschrieben werden können, wird einem versehentlichen Überschreiben vorgebeugt. Klassentypen sind Verweistypen und werden dementsprechend auf dem Heap gespeichert. Hat man aber kleine Datenstrukturen mit Wertsemantik kann es aus Performancegründen durchaus gewünscht sein sie in den Stack zu laden. Zu diesem Zweck gibt es in C# den Datentyp struct, er wird wie eine class-Klasse verwendet, kann aber nicht als Basisklasse verwendet werden. Um überhaupt auf eine Klasse als Basisklasse zugreifen zu können, muss sie dem Compiler auch kenntlich gemacht werden. Dafür werden Klassen zu einzelnen namespaces (Namensräume) zusammengefasst, sie entsprechen in etwa den packages aus Java. Diese werden dann über das Schlüsselwort using in den eigenen Code eingebunden. Möchte man nur auf eine bestimmte Klasse zurückgreifen, muss ein Alias nach dem Muster using GewuenschteKlasse = Namensraum.Klasse; erzeugt werden. using System; namespace Biergarten { // Delegat für Ereignis public delegate void KundeEventHandler(); public class Kunde { // Ereignisdefinition public event KundeEventHandler Bestelle; int vorrat; public Kunde (int vorrat) { this.vorrat = vorrat; } // Variable mit get/set Eigenschaften public int Vorrat { get { return vorrat; } set { vorrat = value; if (vorrat < 1) { Console.WriteLine("Bier leer, Bestellung ausgelöst"); Bestelle(); // Ereignis auslösen } } } 14 3 C# public class Bedienung { Kunde k; public Bedienung (Kunde kunde) { k = kunde; // Ereigniss abonnieren kunde.Bestelle += new KundeEventHandler(kunde_Bestelle); } } // Wenn Ereigniss Eintritt ausführen private void kunde_Bestelle() { Console.WriteLine("Bed: Da ist es"); k.Vorrat++; } public class Rechnung { Kunde kunde; public Rechnung (Kunde k) { kunde = k; // Ereigniss abonnieren kunde.Bestelle += new KundeEventHandler(kunde_Bestelle); } } } } private void kunde_Bestelle(){ Console.WriteLine("1 Ereignis kann mehrere Aktionen auslösen"); } public class Test { public static void Main() { //Kunde sitzt mit 1 Bier im Biergarten Kunde kunde = new Kunde(1); //Eine Bedienung nimmt den Kunden wahr Bedienung bed = new Bedienung(kunde); //Für den Kunden wird eine Rechnung erstellt Rechnung k = new Rechnung(kunde); //Vorrat über set Eigenschaft verringert, Bestellung ausgelöst kunde.Vorrat--; Console.ReadLine(); } } Quelltext 1 Beispiel mit Delegaten und Eventbehandlung 3.2 Grundsätzlicher Aufbau Wie in Quelltext 1 zu sehen, sind die Unterschiede zwischen C# und Java im syntaktischen Aufbau eher gering. Methodennamen werden per CLS-Konvention groß 15 3 C# geschrieben und die Einstiegsmethode Main() kann verschiedene Signaturen haben. Bei der Kompilierung fällt auf, dass die Datei nicht zwangsläufig so heißen muss, wie die Klasse. Auch mehrere vollwertige public Klassen sind innerhalb einer einzelnen Datei erlaubt. 3.3 Eigenschaften Die objektorientierte Programmierung erlaubt eine leichte Wiederverwendung von einmal geschriebenen Code, daher sollten Datenfelder einer Klasse vor falschem Zugriff geschützt sein. In Java müssen spezielle get- und set-Methoden geschrieben werden, C# hat dafür das aus Visual Basic und Delphi bekannte Konstrukt der Properties verfeinert und stellt dem Programmierer Eigenschaften zur Verfügung. Ihre Implementierung ist gut in der Klasse Kunde zu sehen. Auf die private Integer Variable vorrat wird von außen über die get und set Eigenschaften des public property Vorrat zugegriffen. Wie der Zugriff funktioniert erkennt man in der Testklasse. Auch nur lesbare Properties sind möglich, zu diesem Zweck wird der set Teil einfach weggelassen. 3.4 Ereignisbehandlung und Delegaten Klassische Funktionszeiger haben keinen Platz mehr in einer typsicheren objektorientierten Programmiersprache. In Java gibt keinen äquivalenten Ersatz dafür, stattdessen muss der Programmierer inner classes und interfaces verwenden. C# geht einen eleganteren Weg und führt das Konstrukt der Delegaten ein, die als typsichere objektorientierte Funktionszeiger, die sogar im Stande sind mehrere Methoden zu delegieren, verstanden werden können. Delegaten dienen in den meisten Fällen als Platzhalter für die eigentlichen Callback-Methoden. Die wohl wichtigste Funktion haben die Delegaten bei der Ereignisbehandlung, speziell hierfür stellt C# zusätzlich noch den Verweistyp event zur Verfügung. Delegaten bilden in C# einen eigenen Datentyp (delegate), der sich ähnlich wie Klassen verhält - so wird in Quelltext 1 der Delegat KundeEventHandler mit Hilfe von new angelegt. Bei diesem melden sich im Verlauf die Methoden Bedienung und Rechnung 16 3 C# an und registrieren je eine ihrer Funktionen als Rückruffunktion. In der Auslösenden Methode des Kunden, wird ein Ereignis mit dem Delegaten verknüpft. Sobald dies eintritt werden automatisch die beim Delegaten registrierten Methoden ausgeführt. 3.5 Quicksort Mit Hilfe des Quicksort Algorithmus soll ein Vergleich zwischen Java und C# gezogen werden. Als Basis dienen die .NET Version 1.1 und die JRE 1.4.1_02. Für das Programmieren wird Visual Studio .NET verwendet, dieses ist speziell auf die Programmierung mit C# abgestimmt und erleichtert den Überblick zu behalten. Dieser Vorteil für C# muss bei einem Vergleich der Implementierungsgeschwindigkeit berücksichtigt werden, mit einer explizit auf Java abgestimmten Entwicklungsumgebung würde auch das Programmieren in dieser Sprache leichter fallen. Die eigentliche Quicksortmethode unterscheidet sich in Java und C# lediglich durch die in der CLS festgelegten Benennungskoventionen. static void Quicksort (int[] a, static void quicksort (int[] a, int L, int R) int L, int R) { { int m = a[(L + R) / 2]; int m = a[(L + R) / 2]; int i = L; int i = L; int j = R; int j = R; while (i <= j) while (i <= j) { { while (a[i] < m) i++; while (a[i] < m) i++; while (m < a[j]) j--; while (m < a[j]) j--; if (i <= j) if (i <= j) { { Swap(a, i, j); swap(a, i, j); i++; i++; j--; j--; } } } } if (L < j) Quicksort(a, L, j); if (L < j) quicksort(a, L, j); if (i < R) Quicksort(a, i, R); if (i < R) quicksort(a, i, R); } } Quelltext 2 Quicksort in C# Quelltext 3 Quicksort in Java Das Implementieren geht in beiden Sprachen schnell von Statten, nach wenigen Minuten sind die Quicksort Methoden fertiggestellt. Auch das Testen mit einer kleinen main-Methode läuft in beiden Sprachen in etwa gleich zügig ab, hierbei fällt in 17 3 C# VisualStudio der sehr gefällige Debug-Modus für C# ins Auge, man behält ständig den Überblick über jede einzelne Variable. Die gesamte Prozedur hat in C# in etwa zwei Stunden gedauert und noch eine weitere um den Code nach Java zu portieren, der zweite Schritt wäre aufgrund der geringen Unterschiede mit einer auf Java abgestimmten Entwicklungsumgebung sehr wahrscheinlich schneller bewältigt worden. Der vollständige Quellcode beider Programme ist im Anhang enthalten. Für einen Geschwindigkeitsvergleich zwischen beiden Sprachen erscheint es sinnvoll mehr als das Sortieren eines einzelnen Arrays zu betrachten, deshalb gilt es Methoden zum automatischen Erzeugen und Füllen von Arrays zu schreiben. Zur Zeitmessung wird die Differenz der Zeit vor und nach dem Quicksortalgorithmus genommen. Dabei fällt auf, dass kleine Arrays oft noch vor einem neuen Zeitsignal sortiert sind. Um dieser Messungenauigkeit etwas entgegenzuwirken, werden 10.000 Arrays verschiedener Länge getestet. Zusätzlich durchlaufen je 10.000 sortierte und unsortiere Arrays der Länge 100.000 die Quicksort-Methode. Quicksort Unsortierte Arrays unteschiedlicher Länge Unsortierte Arrays der Länge 100.000 C# Java Sortierte Arrays der Länge 100.000 0 5 10 15 20 25 30 35 40 45 50 Durchschnittliche Zeit pro Array im ms Abbildung 7 Geschwindigkeitsvergleich Quicksort auf einem Athlon 900 In Abbildung 4 lässt sich erkennen, dass C# gegenüber Java einen Geschwindigkeitsvorteil von ca. 30 % aufweisen kann. Diesen Eindruck bestätigt ebenfalls die Messung der gesamten Programmlaufzeit, in die auch das automatische Erzeugen der Arrays einfließt. Das Ergebnis geht konform mit Messungen der Zeitschrift c't, in 18 3 C# Geschwindigkeitsvergleichen kamen A. Schäpers und R. Huttary auf ähnliche Ergebnisse. [c't] 4 Fazit .NET und C# können für alle erdenklichen Anwendungen herangezogen werden, durch als unsafe deklarierten Code ist sogar Hardwarenahe Programmierung möglich. Beim momentanen Stand der Technik werden geschwindigkeitslastige Anwendungen wie 3DSpiele oder rechenintensive Simulationen wohl weiterhin in C++ geschrieben. Mit einer verbesserten Version der Laufzeitumgebung könnte der Performancenachteil aber weiter verringert werden. Ob es sich für eingefleischte Java-Programmierer lohnt auf C# umzusteigen ist noch nicht klar zu sagen, je nach Anwendungsgebiet hat mal die eine oder die andere Sprache die Nase vorn. Zudem werden auch in der für Sommer 2004 angekündigten JavaVersion 1.5 einige Nachteile gegenüber .NET ausgemerzt [CW]. Auch professionelle Entwickler sind sich zur Zeit nicht unbedingt einig, einige favorisieren C# und .NET, „weil sie neuer ist und sehen konnte, was bei den alten Technologien falsch war“ [DNM]. Andere vertrauen lieber auf das bekannte Java um nicht noch abhängiger von Microsoft und seiner Lizenzpolitik zu werden. Das Marktforschungsinstitut Garnter traut beiden Plattformen in den nächsten Jahren eine in etwa gleich große Verbreitung zu [IW]. Sollte Microsoft seine Ankündigungen wahr machen und in Zukunft die Anwendungsentwicklungen auf die Basis .NET und C# stellen, wird in der Windowswelt wohl kein Weg daran vorbei führen. In Puncto Plattformunabhängigkeit hat Java zur Zeit zwar noch eindeutig die Nase vorn, aber auch an diesem Vorsprung fängt .NET dank Projekten wie Mono langsam zu Knabbern an. 19 5 Literaturverzeichnis 5 Literaturverzeichnis [MS1] Microsoft: Chancery Software and Microsoft .NET Technology Empower Learners And Enhance Solutions for Schools, http://www.microsoft.com/presspass/press/2001/jun01/0625ChanceryPR.asp. [MS2] Microsoft: Definition der .NET Grundkonzepte, http://www.microsoft.com/germany/themen/net/grundkonzepte.mspx. [ECMA1] ECMA: Standard ECMA–335-Common Language Infrastructure, ECMA 2002. [ISO1] ISO: ISO/IEC 23271- Information technology -- Common Language Infrastructure, ISO 2003. [MS3] Microsoft: Technology Overview, http://msdn.microsoft.com/netframework/technologyinfo/Overview. [MSR] Don Syme: ILX: Extending the .NET Common IL for Functional Language Interoperability, Babel 2001, http://research.microsoft.com/projects/ilx/babel01.pdf. [ECMA2] ECMA: Standard ECMA–334-C# Language Specification, ECMA 2002. [ISO2] ISO: ISO/IEC 23270-Information technology -- C# Language Specification, ISO 2003. [MONO] The Mono Project, http://www.go-mono.org. [DG] DotGNU Project, http://www.dotgnu.org. [MS4] Erik Meijer, Jim Miller: Technical Overview of the Common Language Runtime, S. 4, http://research.microsoft.com/~emeijer/Papers/CLR.pdf. [SAL] Salamander .NET Decompiler, http://www.remotesoft.com/salamander/index.html. 20 5 Literaturverzeichnis [C#K02] A. Schäpers, R. Huttary, Dieter Bremers: C# Kompendium, Markt und Technik 2002. [C#21] Dirk Louis, Shinja Strasser: C# in 21 Tagen, S. 270, Markt und Technik 2002. [CW] Wolfgang Sommergut: Java 1.5 soll .NET-Entwickler ködern, http://www.computerwoche.de/index.cfm?pageid=255&artid=50356&main _id=50356&category=160&currpage=5&type=detail&kw=. [.NETFR] Dr. Holger Schwichtenberg: Das .NET Framework, http://www.dotnetframework.de/default2.aspx?start=http://www.dotnetfram ework.de/(ai5vtr45kjg1ivz20bb0kv45)/ dotnet/DOTNET_Framework_Einfuehrung.asp. [MS5] Microsoft: C#, J# und allgemeine Migrationsthemen, http://www.microsoft.com/germany/ms/msdnbiblio/show_all.asp?siteid=53 0229. [MS6] Michael Willers: Die .NET Common Language Runtime, http://www.microsoft.com/germany/ms/msdnbiblio/show_all.asp?siteid=47 6316. [c't] A. Schäpers, R. Huttary: C#, Java, C++ und Delphi im Effizienztest, c't 19/2003, S. 204-207 und c't 21/200, S. 222-227, Heise Verlag. [DNM] Stefan Wunderlich: Stefan Zill und Andraz Vrenko von Avanade im Gespräch, http://dotnetmag.de/itr/online_artikel/show.php3?nodeid=31&id=171. [IM] Michael Bauer, Werner Fritsch: Java oder .NET?, http://www.informationweek.de/index.php3?/channels/channel21/022620.ht m. [C#N02] Peter Drayton, Ben Albahari, Ted Neward: C# in a Nutshell, O'ReillyVerlag 2002. 21 6 Anhang 6 Anhang 6.1 Quicksort in C# using System; class AufgabeQuickSort { static long zeit = 0; static long zeit100000 = 0; static long zeitSort = 0; static long zeitUnsort = 0; static void Quicksort (int[] a, int L, int R) { int m = a[(L + R) / 2]; int i = L; int j = R; while (i <= j) { while (a[i] < m) i++; while (m < a[j]) j--; if (i <= j) { Swap(a, i, j); i++; j--; } } if (L < j) Quicksort(a, L, j); if (i < R) Quicksort(a, i, R); } static void Swap (int[] a, int L, int R) { int temp = a[L]; a[L] = a[R]; a[R] = temp; } static void ArrayAusgeben(int[] a) { foreach (int elem in a) { Console.Write("{0} ",elem); } Console.WriteLine("\n"); } static void ErzeugeArraySort() { int[] a = new int[100000]; for (int i = 0; i < 100000; i++) { a[i] = i; 22 6 Anhang } } long start = DateTime.Now.Ticks; //Ein Tick dauert 100ns Quicksort(a,0,a.Length-1); long dauer = DateTime.Now.Ticks - start; zeitSort += dauer; static void ErzeugeArray100000() { Random zufallszahlen = new Random(); int[] a = new int[100000]; for (int i = 0; i < 100000; i++) { a[i] = zufallszahlen.Next(1,1000000) - 500000; } long start = DateTime.Now.Ticks; Quicksort(a,0,a.Length-1); long dauer = DateTime.Now.Ticks - start; zeit100000 += dauer; } static void ErzeugeArrayZZ() { Random zufallszahlen = new Random(); int zz = zufallszahlen.Next(1,100001); int[] a = new int[zz]; for (int i = 0; i < zz; i++) { a[i] = zufallszahlen.Next(1,1000000) - 500000; } long start = DateTime.Now.Ticks; Quicksort(a,0,a.Length-1); long dauer = DateTime.Now.Ticks - start; zeit += dauer; } static void Main(string[] args) { int[] a = {-213,52,234,-7,13,4,661,-123,43,73,12,-1}; ArrayAusgeben(a); Quicksort(a,0,a.Length-1); ArrayAusgeben(a); int anzArrays = 10000; for (int i = 0; i<anzArrays;i++) { ErzeugeArray100000(); ErzeugeArrayZZ(); ErzeugeArraySort(); } Console.WriteLine("Quicksort wurde für {0} arrays unterschiedlicher Länge aufgerufen.\nDurchschnittlich dauerte das Sortieren {1} ms\n",anzArrays,zeit/anzArrays/10000); Console.WriteLine("Quicksort wurde für {0} arrays der Länge 100.000 aufgerufen.\nDurchschnittlich dauerte das Sortieren {1} ms\n",anzArrays,zeit100000/anzArrays/10000); Console.WriteLine("Quicksort wurde für {0} sortierte Arrays der Länge 100.000 aufgerufen.\nDurchschnittliche brauchte Quicksort {1} ms\n",anzArrays,zeitSort/anzArrays/10000); Console.ReadLine(); } } 23 6 Anhang 6.2 Quicksort in Java import java.util.*; class qs { static long zeit = 0; static long zeit100000 = 0; static long zeitSort = 0; static void quicksort (int[] a, int L, int R) { int m = a[(L + R) / 2]; int i = L; int j = R; while (i <= j) { while (a[i] < m) i++; while (m < a[j]) j--; if (i <= j) { swap(a, i, j); i++; j--; } } if (L < j) quicksort(a, L, j); if (i < R) quicksort(a, i, R); } static void swap (int[] a, int L, int R) { int temp = a[L]; a[L] = a[R]; a[R] = temp; } static void arrayAusgeben(int[] a) { for (int i = 0; i < a.length; i++) { System.out.print(a[i]+" "); } System.out.println("\n"); } static void erzeugeArraySort() { int[] a = new int[100000]; for (int i = 0; i < 100000; i++) { a[i] = i; } Date start = new Date(); quicksort(a,0,a.length-1); Date fertig = new Date(); zeitSort += fertig.getTime() - start.getTime(); } static void erzeugeArray100000() 24 6 Anhang { } Random zufallszahlen = new Random(); int[] a = new int[100000]; for (int i = 0; i < 100000; i++) { a[i] = zufallszahlen.nextInt(1000000) - 500000; } Date start = new Date(); quicksort(a,0,a.length-1); Date fertig = new Date(); zeit100000 += fertig.getTime() - start.getTime(); static void erzeugeArrayZZ() { Random zufallszahlen = new Random(); int zz = zufallszahlen.nextInt(100001); int[] a = new int[zz]; for (int i = 0; i < zz; i++) { a[i] = zufallszahlen.nextInt(1000000) - 500000; } Date start = new Date(); quicksort(a,0,a.length-1); Date fertig = new Date(); zeit += fertig.getTime() - start.getTime(); } public static void main(String[] args) { int[] a = {-213,52,234,-7,13,4,661,-123,43,73,12,-1}; arrayAusgeben(a); quicksort(a,0,a.length-1); arrayAusgeben(a); int anzArrays = 10000; for (int i = 0; i<anzArrays;i++) { erzeugeArray100000(); erzeugeArrayZZ(); erzeugeArraySort(); } System.out.println("Quicksort wurde für "+anzArrays+" Arrays unterschiedlicher Länge aufgerufen.\nDurchschnittlich dauerte das Sortieren "+zeit/anzArrays+" ms\n"); System.out.println("Quicksort wurde für "+anzArrays+" Arrays der Länge 100.000 aufgerufen.\nDurchschnittlich dauerte das Sortieren "+zeit100000/anzArrays+" ms\n"); System.out.println("Quicksort wurde für "+anzArrays+" sortierte Arrays der Länge 100.000 aufgerufen.\nDurchschnittlich dauerte das Sortieren "+zeitSort/anzArrays+" ms\n"); } } 25