ITMAGAZINE C-Sharp-Workshop, Teil 1: Einstieg in die C-Sharp-Welt 11. Juni 2001 - Im ersten Teil des C#-Workshops werden die grundlegenden Konzepte und Eigenheiten der Programmiersprache anhand einfacher Beispiele vorgestellt. Als die Fachwelt vor etwa einem Jahr zum ersten Mal hörte, Microsoft wolle eine neue Programmiersprache einführen, schien dieses vielen ein wenig befremdlich. C Sharp sollte der Name sein, geschrieben als C#. Es stellte sich bald die Frage, ob es sich dabei um einen Nachfolger von C++ handeln würde oder allenfalls auch um ein Gegenstück zu Java, proprietär auf die Microsoft-Welt abgestimmt. Ein paar Monate später lüftete sich dann ein weiteres Geheimnis: Microsoft stellte .Net vor. Zu diesem Zeitpunkt wurde klar, dass Microsoft nicht mehr die Frage der Programmiersprache in den Vordergrund stellt. .Net ist ein für Programmiersprachen offenes Konzept und C# ist eine mögliche Sprache zur Programmentwicklung unter .Net. Das Interesse vieler Entwickler wurde an C# voll und ganz geweckt, als zu erfahren war, wer hinter .Net steckt. Es ist Anders Hejlsberg, der Vater von Turbo Pascal und seit 1996 Microsoft-Mitarbeiter. Die Positionierung von C# Das Ziel von Microsoft war die Entwicklung der ersten komponentenorientierten Sprache für die C/C++-Familie. Softwareentwicklung heutzutage bedeutet immer weniger die Erstellung monolithischer Anwendungen. Vielmehr werden Komponenten entworfen, die sich in unterschiedliche Ausführungsumgebungen integrieren. Also beispielsweise transaktionale Geschäftsobjekte, Steuerelemente für Browser oder GUI-Oberflächen und Funktionsbibliotheken. Dieses verlangt nach einer Sprache, die Objekte mit Eigenschaften, Methoden, Ereignissen und beschreibenden Attributen in einfacher Form sowohl erstellen als auch verwenden kann. C# ist eine Sprache, in der alles auf Objekten basiert. C# ist die systemeigene Hochsprache von .Net. C# wurde konsequent auf die Erstellung von robusten und langlebigen Komponenten entwickelt. Im Gegensatz zu C++ ist die Sprache wesentlich stärker objektorientiert ausgelegt. C- oder C++-Entwickler können ihr vorhandenes Wissen jedoch weiterverwenden, da C# eine sehr starke Ähnlichkeit vor allem zu C++ enthält. Sie finden aber grosse Erleichterungen bei der Programmierung; Includes und Header-Dateien sind nun nicht mehr notwendig. Auch Java-Programmierer werden sich sehr schnell in C# zu Hause fühlen. Seine Leistungsfähigkeit bezieht C# vor allem durch die Nutzung der .Net-Laufzeitumgebung und des .Net Frameworks. Die .Net-Laufzeitumgebung Bevor die Programmiersprache nun im Detail vorgestellt wird, zuerst eine kurze Einführung in die grundsätzliche Funktionsweise der .Net-Laufzeitumgebung. Es würde den Rahmen dieses Workshops sprengen, alle Einzelheiten der .Net-Laufzeitumgebung zu besprechen. Mit einem herkömmlichen Compiler unter Windows werden eigenständig ausführbare Programme als .exe-Dateien oder Programmbibliotheken als .dll-Dateien erstellt. In den letzten Jahren gewann auch das Erstellen von Komponenten, die innerhalb einer anderen Ausführungsumgebung ausgeführt werden, eine immer grössere Bedeutung. Der Standard zur Erstellung solcher Komponenten wurde von Microsoft über das COM-Modell festgelegt. Allen gemeinsam ist, dass immer das Betriebssystem Windows in entsprechenden Versionen notwendig ist, sollen die erzeugten Programme, Bibliotheken oder Komponenten ausgeführt werden. Diese liegen nämlich in Maschinencode vor und sind in der Regel zusätzlich auf entsprechenden Laufzeitumgebungen, z.B. die MFC oder VisualBasic Runtime, angewiesen. Ein .Net Compiler wie C# erzeugt nun keine direkt ausführbaren Programme oder Komponenten, er erstellt vielmehr einen Code im Microsoft-eigenen Intermediate-Language-Format (IL). Es werden als Ziel der Kompilierung zwar auch weiterhin .exe oder .dll-Files angegeben, diese werden unter .Net jedoch als Assemblierung bezeichnet, die eben IL-Anweisungen statt Maschinencode enthalten. Prinzipiell kann eine Assemblierung sogar aus mehreren Dateien bestehen. Diese Assemblierungen sind dann von der .Net-Laufzeitumgebung ausführbar. Dazu wird ein Just In Time Compiler (JIT) verwendet, der eine Assemblierung endgültig in Maschinencode übersetzt. Dies geschieht zu dem Zeitpunkt, an dem die Assemblierung installiert wird, oder dynamisch bei der ersten Verwendung einer Assemblierung. Die Vorgehensweise erlaubt die Anpassung des ausführbaren Codes an unterschiedliche Prozessoren. Daneben schafft es jedoch auch die Unabhängigkeit vom Betriebssystem, entscheidend ist nur die Verfügbarkeit der .Net-Laufzeitumgebung für das Zielsystem. Da die Laufzeitumgebung immer optimierten Maschinencode ausführt, ergeben sich deutliche Geschwindigkeitsvorteile gegenüber anderen Konzepten virtueller Maschinen, die lediglich einen Zwischencode ausführen. Erweiterung per Metadaten Assemblierungen bestehen jedoch nicht nur aus IL-Code, sondern enthalten auch Metadaten, die ein Objekt so beschreiben, dass es innerhalb von .Net ohne zusätzliche Dateien oder Einträgen in die Registry eingesetzt werden kann. Zu diesen Objektinformationen zählen Objektname, alle Eigenschaften sowie die Namen der Mitgliedsfunktionen einschliesslich Parameterdefinition. Dieses Konzept vereinfacht sowohl die Entwicklung als auch die Benutzung von Assemblierungen. Im Gegensatz zur Entwicklung unter COM ist keine Registrierung mehr nötig, die Installation von Anwendungen per XCOPY-Befehl wird möglich. Über das sogenannte "Reflection" ermöglicht die .Net-Laufzeitumgebung das Auslesen dieser Metadaten zur Laufzeit. Die Definition von Attributen erlaubt die weitere Auszeichnung von Komponenten. Diese werden ebenfalls in den Metadaten gespeichert und bieten beispielsweise die Möglichkeit zur Festlegung des Transaktionsverhaltens einer Komponente. Mit C# entwickelte Programme und Komponenten können aufgrund der Integration in die Mit C# entwickelte Programme und Komponenten können aufgrund der Integration in die .Net-Laufzeitumgebung nun ohne jegliche Probleme von allen anderen Programmiersprachen unter .Net verwendet werden. Entscheidend hiefür ist vor allem die einheitliche Typendefinition unter .Net, die allen Programmiersprachen zur Verfügung steht. Da für .Net geschriebene Programme grundsätzlich innerhalb der .Net-eigenen Speicherverwaltung ablaufen, ist ein manuelles Speichermanagement nicht mehr notwendig und auch nicht mehr möglich. Freigegebener und unbenutzter Speicher wird durch den sogenannten Garbage Collector verwaltet. Das erste Programm in C# Traditionell stellt sich eine Programmiersprache immer über die Ausgabe des Textes "Hello World" vor. Das folgende Beispiel zeigt den in C# notwendigen Code. Das Programm ist als Konsolenanwendung realisiert. namespace HelloWorld { using System; public class Hello { public static void_ Main(string[] args) { //Textausgabe im //Konsolenfenster Console.WriteLine_ ("Hello World"); } } } //Codezeilen, die aus Layout//technischen Gründen umbrochen //werden mussten, werden an der //betreffenden Stelle mit einem //Unterstrich gekennzeichnet. Bevor die einzelnen Programmzeilen im Detail untersucht werden, hier noch drei grundsätzliche Syntaxdefinitionen: Codeblöcke werden generell in geschweifte Klammern eingeschlossen, Anweisungen werden mit einem Semikolon abgeschlossen und Kommentare durch zwei Slashes (//) eingeleitet. In der ersten Zeile wird ein Namespace definiert. Namespaces sind die Grundlage zur Benennung von Komponenten in .Net. Jede Komponente sollte einen eigenen Namespace besitzen. Namespaces können hierarchisch gegliedert und somit auch verschachtelt definiert werden. In der nächstfolgenden Zeile wird durch das Schlüsselwort Using der Namespace System aus dem .Net Framework verwendet. Namespaces werden in Assemblierungen festgelegt. Der Name der Assemblierung, der den entsprechenden Namespace enthält, muss dem Compiler als Referenz bekannt sein. So befindet sich beispielsweise der Namespace System in der Assemblierungsdatei system.dll. Die Klasse mit Namen Hello wird als mit public veröffentlicht, um anderen Programmen die Möglichkeit zu geben, diese zu benutzen. C# kennt keine globalen Funktionen. Daher wird innerhalb der Klasse Hello eine statische Funktion definiert, die als Startpunkt der Anwendung dient. Über die Definition von HelloWorld.Hello als Startobjekt wird diese Klasse beim Starten der Anwendung automatisch instanziert, worauf die Funktion Main ausgeführt wird. Diese Funktion gibt über Console.Writeline einen Text im Konsolenfenster aus. Die Klasse Console ist innerhalb des Namespace System definiert. Da über using die Metadaten des Namespace System bereits bekannt sind, kann hier die Kurzschreibweise verwendet werden. Die folgende Tabelle zeigt, wie sich der Einsatz von using auf den Programmieraufwand auswirkt. Kompilieren des Programms Es ist nicht notwendig, Visual Studio.Net als Tool zur Erstellung von Programmen zu verwenden. Da der Compiler auch als Kommandozeilenanwendung csc.exe zur Verfügung steht, reicht ein einfacher Texteditor aus. Über entsprechende Parameter können weitere Optionen gesetzt werden, beispielsweise die verwendeten Verweise oder eine Antwortdatei. Interessant ist an dieser Stelle noch, dass beispielsweise mehrere Klassen innerhalb einer einzigen Quellcodedatei definiert werden können. Datentypen in C# Bei der Verwendung von Datentypen greift C# auf die unter .Net grundsätzlich verfügbaren Datentypen zurück. So ist ein int in C# mit dem Typ System.Int32 aus der .Net-Laufzeitumgebung vorhanden. Datentypen werden in Werte- und Verweistypen unterteilt. Wertetypen werden dem Stack zugewiesen oder sind strukturintern vorhanden. Verweistypen werden dem Heap zugeordnet. Alle Datentypen sind von der Basisklasse object abgeleitet. Muss nun ein Wertetyp als Verweistyp fungieren, so wird ein Wrapper, der den Wertetyp als Verweisobjekt erscheinen lässt, dem Heap zugeordnet. Dieses wird als Boxing bezeichnet, der umgekehrte Vorgang heisst Unboxing. Erreicht wird hiermit, dass jeder beliebige Datentyp als Objekt behandelt werden kann. Der folgende Code zeigt dies anhand einer Ganzzahl, die als Literal 4711 im Sourcecode definiert wird. Obwohl dieses natürlich ein ganzzahliger Wertetyp ist, kann die Methode .ToString() durch Boxing angewandt werden. public class BoxingDemo { public static void_ ShowBoxing() { Console.WriteLine_ ("Boxing Demo:{0}",_ 4711.ToString()); } } Durch das Schlüsselwort class wird ein Verweistyp definiert. Im obigen Beispiel wird dieses also mit dem Namen BoxingDemo gemacht. Strukturen werden durch das Schlüsselwort struct eingeleitet. Im Gegensatz zu class werden hier Wertetypen deklariert. Strukturen sollten nur für schlanke Objekte eingesetzt werden, die wie integrierte Typen agieren. Ansonsten sind Klassen vorzuziehen. public struct Kreis { int Radius; string Farbe; } Für die Definition von Variablen ist es unerheblich, ob es sich um einen Verweis- oder einen Wertetyp handelt. Eine int-Variable ist beispielsweise ein Wertetyp, ein string hingegen ein Verweistyp. Variablen können bei der Definition gleich initialisiert werden. Der folgende Codeausschnitt verdeutlicht dieses noch einmal. int X = 0; string Name = "Frank Groth"; Die Programmsteuerung Bei der Benutzung von Operatoren haben alle diejenigen, die der Sprache C++ mächtig sind, keine Probleme, da die Ausdruckssyntax von C# mit der von C++ identisch ist. Für den Neueinsteiger in die C-Sprachenfamilie zeigt die folgende Tabelle einen Überblick über alle arithmetischen Operatoren. Der Operator + kann auch zur Zeichenfolgeverkettung eingesetzt werden. Operanten, die in einer solcher Anweisung nicht vom Typ string sind, können über den Aufruf der virtuellen Methode ToString() automatisch in eine Zeichenfolge umgewandelt werden. int X = 0; string Name = "Frank Groth"; string Result; Result = Name +_ " (" + X.ToString() + ")"; Über ein vorgestelltes Minus kann ein Wert negiert werden, sofern er einen Datentyp besitzt, der über eine gültige negative Darstellung verfügt. Ein Vergleich zweier Werte erfolgt mit relationalen Operatoren. Um einen boolschen Wert zu negieren, wird weiter der Operator ! eingesetzt. Die relationalen und logischen Operatoren von C# haben wir im Kasten auf der nächsten Seite zusammengestellt. Die Sprache C# stellt auch einen Bedingungsoperator als ?: zur Verfügung. Dieser wählt basierend auf einem boolschen Ausdruck aus zwei Ausdrücken aus. int X; string Result; … Result = (X < 0)?_ "X ist negativ": "X ist positiv"; In diesem Beispiel wird geprüft, ob x kleiner Null ist. Ist das der Fall, wird der Ausdruck hinter dem Fragezeichen zurückgegeben, andernfalls der Ausdruck hinter dem Doppelpunkt. Zuweisungen werden in C# mit dem Gleichheitszeichen geschrieben, dabei kann die rechte Seite einer Zuweisung auch einen komplexen Ausdruck enthalten. Auswahlanweisungen Die bekannteste Anweisung zur Steuerung des Programmablaufes ist sicherlich die if-then-Anweisung. Dabei wird die dem if folgende Bedingung als boolscher Ausdruck ausgewertet. int X; string Result; if (X < 0) { Result = "X ist negativ"; } else { Result = "X ist positiv"; } Die switch-Anweisung realisiert einen Verteiler. Dabei können dem auch Zeichenketten eingesetzt werden. Der switch-Anweisung sollte auf jeden Fall immer der Vorzug vor einer Reihe von if-Anweisungen gegeben werden. Intern ist die Verarbeitung eines solchen switch-Konstruktes sehr stark optimiert. int X; string Result; switch (X) { case 0: Result = "Null"; break; case 1: Result = "Eins"; break; default: Result = "Ungleich 0_ oder 1"; } Schleifenanweisungen Die for-Anweisung durchläuft den Schleifenkörper anhand der festgelegten Unter- und Obergrenze. Die Anzahl der Schleifendurchläufe wird durch die Schrittweite des Zählers festgelegt. for (int i = 0;i <= 100;i++) { Console.Writeline("i={0}",i); } Die while-Anweisung realisiert eine Schleife, deren Abbruchbedingung am Anfang der Schleife geprüft wird. Dadurch kann ein Eintreten in den Schleifenkörper generell verhindert werden. int n = 0; while (n < 10) { Console.Writeline("n={0}",n); n++; } Die do-while-Anweisung implementiert eine Schleifenkonstruktion, bei der die Abbruchbedingung am Ende der Schleife geprüft wird. Somit wird der Schleifenkörper auf jeden Fall ein Mal durchlaufen. int n = 0; do { Console.Writeline("n={0}",n); n++; }while (n < 10); Die for-each-Anweisung ist prinzipiell mit einer for-Schleife zu vergleichen. In beiden Konstrukten steht bereits vor dem Eintritt in die Schleife fest, wie oft diese durchlaufen werden soll. Im Gegensatz zur for-Schleife ermöglicht ein for-each-Konstrukt allerdings das Durchlaufen von einer Sammlung von Objekten. Dabei wird jedes Objekt der Sammlung genau ein Mal angesprochen. Gerade durch die konsequente Ausrichtung von C# als objektorientierte Sprache ermöglicht diese Anweisung ein sehr bequemes und fehlertolerantes Codieren. using System; using System.Collections; class Person { } class Personen { public static void_ ListePersonen(ArrayList arr) { foreach (Person_ current in arr) { Console.WriteLine_ ("Person: {0}",current); } } } Zur Optimierung der Programmablaufsteuerung stehen Sprunganweisungen wie break, continue und auch goto zur Verfügung. Um die Übersichtlichkeit zu gewährleisten, empfiehlt es sich allerdings, den Einsatz dieser Befehle nur sparsam oder gar nicht einzusetzen. Ausnahmebehandlung Die ordnungsgemässe Behandlung von Fehlern (Ausnahmen) und die Weitergabe von Fehlercodes aus Mitgliedsfunktionen an eine aufrufende Programmzeile tragen entscheidend zur Softwarequalität bei. In komponentenbasierenden Systemen ist sogar eine komponentenübergreifende Ausnahmebehandlung erforderlich. In der Regel steht hier nämlich der Quelltext nicht zu Verfügung. Viele Programmierer verwenden Rückgabewerte als Ergebnis einer Funktion. Im Minimalfall wird nur ein boolscher Wert zurückgeliefert, in der Regel jedoch ein ganzzahliger Wert, der die Nummer eines eventuell aufgetretenen Fehlers liefert. Dieses Verfahren hat jedoch den Nachteil, dass zusätzliche Informationen wie eine Fehlerbeschreibung nur über zusätzlichen Aufwand dem Aufrufer zur Verfügung gestellt werden können. Unter .Net ist die Ausnahmebehandlung fundamental in die Laufzeitumgebung integriert. Dieses Vorgehen erlaubt eine einheitliche Ausnahmebehandlung, ohne dass auf individuelle Konzepte zurückgegriffen werden muss. C# stellt hierfür eine try-catch-finally-Struktur zur Verfügung. Alle Anweisungen, die sich innerhalb des try-Blocks befinden, werden ausgeführt. Tritt hier eine Ausnahme bzw. ein Fehler auf, so sucht die .Net-Laufzeitumgebung nach einem passenden catch-Block, der die Ausnahme behandeln kann. Die Programmausführung wird dann dort fortgesetzt, während auf die Ausführung weiterer Zeilen im try-Block verzichtet wird. Ein abschliessender optional vorhandener finally-Block wird immer ausgeführt, unabhängig davon, ob eine Ausnahme aufgetreten ist. Innerhalb dieses Blocks können dann abschliessende Anweisungen wie das Freigeben von Ressourcen oder das Schliessen von Dateien ausgeführt werden. public static void_ ShowException() { int X; try { Console.WriteLine(4711 / X); } catch (DivideByZeroException e) { Console.WriteLine_ ("Fehler: {0}",e); } finally { Console.WriteLine("Finally:_ Dieses wird immer_ ausgeführt!"); } } Es ist möglich, mehrere catch-Blöcke zu definieren, um auf einzelne Fehler gezielt reagieren zu können. In einem allgemeinen catch-Block sollten dann alle sonstigen Ausnahmen abgefangen werden. Eine solche allgemeinen catch-Block sollten dann alle sonstigen Ausnahmen abgefangen werden. Eine solche Vorgehensweise erzeugt sichere Programme. Alle Ausnahmen sind Ableitungen der .Net-Basisklasse Exception. Es ist nun möglich, eigene Ausnahmen zu definieren und über die throw-Anweisung eine Ausnahme gezielt auszulösen. Zudem ist auch die Verschachtelung von Ausnahmebehandlungen möglich. So kann die Ausnahmebehandlung sehr detailliert gestaltet werden. Übersicht grundlegender Datentypen Typ Byte Sbyte Short Laufzeittyp Byte Sbyte Int16 Ushort Uint16 Int Int32 Uint Uint32 Beschreibung Byte-Wert ohne Vorzeichen Byte-Wert mit Vorzeichen Short-Wert mit Vorzeichen Short-Wert ohne Vorzeichen Integer-Wert mit Vorzeichen Integer-Wert ohne Vorzeichen Long Int64 Ulong Uint64 Float Single Double Double Decimal Decimal String Char Bool String Char Boolean Longinteger-Wert mit Vorzeichen Longinteger-Wert ohne Vorzeichen Gleitkommazahl Gleitkommazahl mit doppelter Genauigkeit Zahl mit fester Genauigkeit Unicode-Zeichenfolge Unicode-Zeichen Boolscher Wert Arithmetische Operatoren Operator + * / X % y << >> ++ -- Beschreibung Addition Subtraktion Multiplikation Division Restbetrag höherwertige Bits verwerfen, niederwertige Bits auf Null setzen oder umgekehrt inkrementiert eine Variable um den Wert 1 dekrementiert eine Variable um den Wert 1 Relationale und logische Operatoren Relationale A==b A!=b A<b (a<=b) A>b (a>=b) Logische & | ^ && || Beschreibung wahr, wenn a gleich b wahr, wenn a ungleich b wahr, wenn a kleiner (gleich) b wahr, wenn a grösser (gleich) b Beschreibung Bitweises AND der zwei Operatoren Bitweises OR der zwei Operatoren Bitweises XOR (exklusives OR) der beiden Operatoren Logisches AND der beiden Operatoren Logisches OR der beiden Operatoren Copyright by Swiss IT Media 2017