Artikel als PDF anzeigen

Werbung
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 
Herunterladen