CSharp/All

Aus Das Sopra Wiki
Wechseln zu: Navigation, Suche


Geschichte

C# (lies C-Sharp) ist eine von Microsoft entwickelte Programmiersprache, die bei ECMA International[1] und der International Organization for Standards[2] als Standard registriert ist. Sie wird im Rahmen des Softwarepraktikums in Kombination mit dem XNA-Framework als Sprache zur Softwareentwicklung eingesetzt.

Während der Entwicklung von Microsofts .NET-Frameworks wurden die Klassenbibliotheken ursprünglich in einem Compilersystem namens Simple Managed C (SMC) geschrieben. 1999 wurde eine neue Sprache, Cool (C-Like Object Oriented Language), entwickelt. Aus lizenz- und urheberrechtlichen Überlegungen entschied sich Microsoft jedoch gegen den Namen Cool. Als im Jahr 2000 das .NET-Framework zum ersten Mal öffentlich vorgestellt wurde, ist die Sprache in C# umbenannt worden. Im gleichen Zuge wurden die Klassenbibliotheken und ASP.NET nach C# portiert.

Die Designprinzipien anderer Programmiersprachen wie C++, Java, Delphi und Smalltalk schufen die Grundlagen für die Common Language Runtime (CLR), die das Design von C# selbst angetrieben hat.

Versionshistorie

Version ECMA Language Specification ISO Spezifikation Microsoft Language Specification Release .NET Framework Visual Studio Version
C# 1.0 Dezember 2002 April 2003 Januar 2002 5. Januar 2002 .NET Framework 1.0 Visual Studio .NET 2002
C# 1.2 Dezember 2002 April 2003 Oktober 2003 3. April 2003 .NET Framework 1.1 Visual Studio .NET 2003
C# 2.0 Juni 2006 September 2006 September 2005 7. November 2005 .NET Framework 2.0 Visual Studio 2005
C# 3.0 Juni 2005 Es existiert noch kein offizielles ISO-Dokument zur Language Specification. August 2007 6. November 2006 .NET Framework 3.0 Visual Studio 2008
C# 3.5 Juni 2006 Es existiert noch kein offizielles ISO-Dokument zur Language Specification. August 2007 9. November 2007 .NET Framework 3.5 Visual Studio 2008
C# 3.5 SP1 Von der ECMA gibt es keine offizielle Language Specification. Es existiert noch kein offizielles ISO-Dokument zur Language Specification. August 2007 11. August 2008 .NET Framework 3.5 SP1 Visual Studio 2008
C# 4.0 Von der ECMA gibt es keine offizielle Language Specification. Es existiert noch kein offizielles ISO-Dokument zur Language Specification. April 2010 11. April 2010 .NET Framework 4 Visual Studio 2010


Zusammenfassung der Versionen

C# 2.0 C# 3.0 C# 4.0 C# 5.0 (geplant)
Neue
Features
  • Implicitly typed variables
  • Implicitly typed arrays
  • Anonymous types
  • Extension methods
  • Query expressions
  • Lambda-Ausdrücke
  • Expression trees
  • Dynamic binding
  • Named and optional arguments
  • Generic co- and contravariance
  • Asynchronous methods
  • Compiler As a Service

Konzepte

  • Die Sprache und die aus ihr resultierenden Programme sollen softwaretechnische Prinzipien, wie starke Typisierung, Prüfung von Array-Grenzen, Erkennung von Benutzung uninitialisierter Variablen und einer automatischen Garbage Collection unterstützen.
  • In C# wird auf Konzepte von Sprachen wie Java, C++ und Delphi zurückgegriffen.
  • Es unterstützt nicht nur die Entwicklung von .NET-Komponenten, sondern auch von COM-Komponenten, die von Win32-Applikationen verwendet werden können.
  • In C# entwickelte Programme sollen das Konzept der Plattformunabhängigkeit realisieren können.
  • Obwohl C#-Anwendungen von Natur aus sehr ökonomisch mit Speicherverbrauch und Rechenleistung umgehen, ist die Sprache nicht dazu gedacht, sich in Performanzbelangen mit Sprachen wie C oder Assembler zu messen.

Grundlagen

Datentypen

In Programmiersprachen gibt es unterschiedlichste Arten von Werten. Diese können Zahlen, Buchstaben, Zeichenketten usw. sein. Damit beliebige dieser Werte abgespeichert werden können, gibt es sogenannte Typen. Variablen werden von einem bestimmten Typ erstellt und können dann auch nur Werte dieses Typs enthalten. Typen sind ein sehr wichtiges Paradigma in Programmiersprachen. Über sie wird gesteuert, was z.B. bei einem Operator, wie dem "+"-Operator passiert, oder einfach, wie mit den Werten, die in Variablen gespeichert sind, umgegangen werden soll.

Die meisten Programmiersprachen bieten eine Reihe so genannter primitiver Datentypen. Dazu gehören beispielsweise Ganzzahl- oder Gleitkommatypen. Im Gegensatz zu Verweistypen (engl. „reference types“), die lediglich eine Referenz zu den Objekten speichern, werden in primitiven Datentypen tatsächliche Werte abgespeichert. Im .NET-Framework werden diese Datentypen Wertetypen (engl. „value types“) genannt. Es existieren zwei Arten von Wertetypen. Zum einen die, die schon in der .NET-Framework integriert sind, zum anderen die selbstdefinierten Datentypen. Alle Wertetypen in .NET werden von der Klasse ValueType abgeleitet.

Da C# als vollständig objektorientierte Sprache gilt, kann man sich die Frage stellen, warum nicht alle Datentypen als Referenztypen behandelt werden. Da ein Objekt ein Referenztyp ist und alles ein Objekt ist, müsste auch jeder Datentyp ein Referenztyp sein. Die Unterscheidung zwischen Referenz- und Wertetypen erfolgt intern durch die Verhaltensweise. Die Klasse ValueType ist direkt von Object (der Basisklasse aller Datentypen und Klassen in .NET) abgeleitet. Sie enthält jedoch andere Definitionen für die Verhaltensweise nach außen, also zum Programmierer hin, als Object.

Führt man beispielsweise eine Vergleichsoperation durch, wird bei einem Wertetyp wirklich verglichen, ob beide Variablen den gleichen Wert haben. Bei Referenztypen nutzt man in der Regel Methoden wie Equals(), die zwei Objekte auf Gleichheit prüfen sollen. Ein weiterer Unterschied besteht in der internen Behandlung. Wertetypen werden auf dem Stack abgelegt, um einen schnellen und effizienten Zugriff zu gewährleisten, wohingegen Referenztypen im verwalteten Heap liegen, dessen Zugriff länger dauert. Lediglich der Verweis eines Referenztyps, also sozusagen der Offset im Heap, liegt auf dem Stack.

Um eine Unterscheidung von Referenz- und Wertetypen zu geben, können drei wichtige Eigenschaften betrachtet werden:

  • Wertetypen sind versiegelt (sealed). Von ihnen kann nicht abgeleitet werden. Neue Wertetypen müssen von ValueType abgeleitet, bzw. als Struct definiert werden. Von Referenztypen kann in der Regel immer abgeleitet werden. (Außer, es wurde anders definiert.)
  • Wenn Wertetypen verwendet werden, ist es nicht nötig, einen Konstruktor aufzurufen. Bei Referenztypen ist dies pflicht.
  • Jedem Wertetyp muss vor seiner erstmaligen Verwendung ein Wert zugewiesen werden.

Integrierte Datentypen


Wertetypen sind Datentypen, welche bei der Übergabe als Parameter - im Gegensatz zu Referenztypen - nicht als Referenz übergeben werden. Stattdessen wird der in ihnen gespeicherte Wert bei eienm Funktionsaufruf kopiert und steht der aufgerufenen Funktion zur Verfügung, ohne dass Änderungen an der Variable sich auf die "Ursprungsvariable" auswirken.

In C# existieren 13 Wertetypen, die man nicht extra zu definieren braucht. Dabei handelt es sich ausnahmslos um integrierte Datentypen. Außerdem werden Strukturen und Aufzählungen ebenfalls als Wertetypen behandelt:

Datentyp Größe Wertebereich Alias
bool 1 Bit true, false System.Boolean
byte 8 Bit <math>0</math> bis <math>255</math> System.Byte
sbyte 8 Bit <math>-128</math> bis <math>127</math> System.SByte
char 16 Bit ein Unicode-Zeichen System.Char
short 16 Bit <math>-32~768</math> bis <math>32~767</math> System.Int16
ushort 16 Bit <math>0</math> bis <math>65~535</math> System.UInt16
int 32 Bit <math>- 2~147~483~648</math> bis <math>2~147~483~647</math> System.Int32
uint 32 Bit <math>0</math> bis <math>4~294~967~295</math> System.UInt32
long 64 Bit <math>-9~223~372~036~854~775~808</math> bis <math>9~223~372~036~854~775~807</math> System.Int64
ulong 64 Bit <math>0</math> bis <math>18~446~744~073~709~551~615</math> System.UInt64
float 32 Bit <math>\pm 1.5 \cdot 10^{45}</math> bis <math>\pm 3.4 \cdot 10^{38}</math> System.Single
double 64 Bit <math>\pm 5.0 \cdot 10^{324}</math> bis <math>\pm 1.7 \cdot 10^{308}</math> System.Double
decimal 128 Bit <math>\pm 1.0 \cdot 10^{28}</math> bis <math>\pm 7.9 \cdot 10^{28}</math> System.Decimal
struct variabel variabel -
enum variabel variabel -

Der Datentyp string

Zeichenketten, in der Regel als Strings bezeichnet, sind einer der am häufigsten verwendeten Datentypen in fast allen Programmiersprachen. In .NET hat der Datentyp string eine besondere Bedeutung, da es sich bei ihm um eine Art "Zwitter" handelt. Eigentlich ist string als Referenztyp definiert, er verhält sich aber nach außen wie ein Wertetyp. In .NET heißt die Basisklasse System.String.

Wird einer Variable vom Typ string ein Wert zugewiesen, wird intern im Heap der entsprechende Speicherplatz bereitgestellt. Der Wert wird nicht auf dem Stack gespeichert, wie sonst bei Wertetypen üblich. Außerdem ist ein String immutable. Das bedeutet, er ist unveränderlich. Sobald einmal ein Wert zugewiesen wurde, kann dieser intern nicht mehr verändert werden. Nach außen - also zum Programmierer hin - ist dies aber dennoch möglich. Was intern zum Beispiel beim Anhängen eines Strings an einen anderen String geschrieht, wird im folgenden deutlich:

  • Die Gesamtlänge des neuen Strings wird ermittelt.
  • Im Heap wird entsprechend Speicher reserviert.
  • Die Daten werden an diesen neuen Speicherplatz kopiert.
  • Die Referenz wird auf den neuen Platz im Heap umgelegt.

Bei jeder Änderung eines Strings wird intern eine Kopie des alten Strings erzeugt. Dies beeinflusst die Performanz. Es ist wichtig, diese Eigenschaft bei Performanz-Überlegungen mit einzubeziehen.

Variablen

Variablen müssen vor ihrer Verwendung in C# wie in vielen anderen Sprachen auch zunächst deklariert werden. Jede Variable besitzt ihren eigenen Datentyp, der ihr bei der Deklaration zugewiesen wird. Im Allgemeinen kann man zwischen drei Typen von Variablen unterscheiden:
  • Lokale Variablen werden innerhalb von Methoden verwendet. Sie sind so lange gültig, wie der Block, in dem sie deklariert wurden, abgearbeitet wird. Danach werden sie aus dem Speicher entfernt. Auch Parameter von Methoden werden - bis auf Ausnahmefälle wie out- oder ref-Parameter - als lokale Variablen angesehen.
  • Instanzvariablen sind Bestandteil einer Klasse. Ihre Lebensdauer entspricht der Lebensdauer des Objektes, in dem sie definiert wurden. Sie werden häufig auch als Felder einer Klasse bzw. eines Objektes bezeichnet.
  • Klassenvariablen sind ebenfalls Bestandteil einer Klasse, allerdings nicht auf Instanz- bzw. Objektebene, sondern auf Klassenebene. Ihre Lebensdauer entspricht der des Programms, in dem die Klasse definiert wurde. Klassenvariablen existieren jeweils nur einmal im Bezug auf eine Klasse, nicht für jedes Objekt. Sie werden häufig als statische Variablen oder statische Felder bezeichnet.


Deklaration

In C# werden Variablen deklariert, indem zunächst ihr Typ und dann ihr Bezeichner angegeben wird:

int a;            // Deklariert die Variable a vom Typ int
System.Int32 b;   // Deklariert die Variable b vom gleichen Typ wie a, also int

An welcher Stelle eine Variable innerhalb eines Bereiches deklariert wird, ist unerheblich. Im Gegensatz zu anderen Sprachen, wie Delphi, in denen alle Variablen zu Beginn einer Methode deklariert werden müssen, kann in C# an beliebiger Stelle innerhalb einer Methode eine Variablendeklaration erfolgen. Wichtig ist nur, dass eine Deklaration erfolgt, bevor die Variable verwendet wird. Damit auf eine Variable zugegriffen werden kann, muss diese vor ihrem Zugriff außerdem noch initialisiert werden. Bei Wertetypen geschieht dies durch die Zuweisung eines Wertes. Lokal definierte Werte- und Referenztypen müssen instantiiert werden. Im globalen Kontext erhalten alle Referenztypen standardmäßig den Wert null, Wertetypen erhalten automatisch den Default-Wert des jeweiligen Typs.

// Deklaration und Initialisierung der Integer-Variable a
int a = 13;

// Deklaration und Initialisierung des Strings s
string s = "Hallo";

// Deklaration und Instantiierung des ComboBox-Objektes box
System.Windows.Forms.ComboBox box = new System.Windows.Forms.Combobox();

Bezeichner

Bezeichner, oder Namen einer Variablen identifizieren sie eindeutig und erlauben den Zugriff auf die Variable. Ein Bezeichner muss in C# mit einem Unterstrich (_) oder einem Buchstaben beginnen. Ziffern sind nur innerhalb des Namens erlaubt. Ein Variablenname darf ebenfalls keine Leerzeichen enthalten. Aufgrund der Unicode-Kompatibilität von C# und .NET können auch Sonderzeichen in Variablenbezeichnern verwendet werden. Davon wird allerdings abgeraten:

int _myValue;          // Korrekte Deklaration
double 1Wert;          // Fehler: Der Bezeichner der Variable beginnt mit einer Ziffer
double Wert1;          // Korrekte Deklaration
string Währung;        // Korrekte Deklaration, da Unicode-Zeichen unterstützt werden
int ein Wert;          // Fehler: Der Bezeichner beinhaltet ein Leerzeichen
int noch_ein_Wert;     // Korrekte Deklaration


Gültigkeitsbereich

Wie bereits erwähnt sind Variablen nur innerhalb des Blocks, in dem sie deklariert wurden, gültig. Das folgende Beispiel soll dies veranschaulichen:

class Class1
{
   static void Main(string[] args)
   {
      int i = 5;                   // i ist deklariert und initialisiert
      for (int u = 0; u < 10; u++)
      {
         i = i + u;
         Console.WriteLine(i);
      }
      Console.WriteLine(u);        // Fehler: u ist hier nicht mehr bekannt

      Console.ReadLine();
   }
}

Konstanten

Konstanten werden mit Hilfe des Schlüsselwortes const deklariert. Konstante Variablen können ihren Wert nicht mehr ändern. Sie müssen bereits bei der Deklaration initialisiert werden:

const int speedOfLight = 299792458;

Konvertierungen und Boxing

Implizite und explizite Konvertierung

Da es sich bei C# um eine typsichere Sprache handelt, kann jede Variable, die einen bestimmten Datentyp hat, auch nur die Werte enthalten, die diesem Datentyp entsprechen. Allerdings kann dies nicht immer vorausgesetzt werden. Aus diesem Grund gibt es die möglichkeit, verschiedene Konvertierungen von Datentypen vorzunehmen, nämlich

  • Implizite Konvertierung und
  • explizite Konvertierung (casting).

Wird eine implizite Konvertierung vorgenommen, bekommt der Programmierer davon meist nichts mit. Diese wird vom Compiler erledigt. Datentypen, die ohne Wert- oder Genauigkeitsverlust in einen anderen Typen umgewandelt werden können, werden implizit konvertiert. Beispielsweise kann ein Wert vom Typ int ohne Probleme in einen Wert vom Typ long konvertiert werden.

Wenn allerdings Verluste auftreten können, weigert sich der Compiler, eine Konvertierung vorzunehmen. Eine Konvertierung kann jedoch explizit erzwungen werden. Dies Geschieht mit Hilfe des sogenannten Casts. Dabei wird der neue Typ, in den konvertiert werden soll, vor den zu konvertierenden Wert oder die zu konvertierende Variable in Klammern geschrieben. Dies ist auch mit Zeichen und ihrer Repräsentation als Zahlenfolge möglich:

short s = 0;
int   i = 10;

s = i;        // Fehler: Der Wertebereich von i ist größer als der von s
s = (short)i; // OK: Explizite Konvertierung (cast) von int zu short

char c = (char)65;    // Konvertiert den Zahlenwert 65 in 'A'
int  z = (int)'A';    // Konvertiert 'A' in den Zahlenwert 65

Boxing und Unboxing

Obwohl sich in C# Werte- und Referenztypen unterschiedlich verhalten, sind sie innerhalb des .NET-Frameworks wie alles andere auch aus Klassen (bzw. Strukturen) implementiert. Wertetypen stammen von der Klasse ValueType ab, die wiederum von der Klasse Object abgeleitet ist. Da sich in .NET alles von Object ableitet, ist der Datentyp object quasi ein Platzhalter für jeden beliebigen anderen Datentyp.

Es gibt viele Methoden, die als Übergabeparameter Werte vom Typ Object erwarten. Diese Methoden werden auch als universelle Methoden bezeichnet, da sie mit jedem Datentyp umgehen können. Wenn einer solchen Methode ein Wert übergeben wird, wird dieser in einem Objekt "verpackt". Das Objekt dient sozusagen als "Hülle" um einen Datentyp, der eigentlich ein Wertetyp ist. Diese Art, einen Wertetyp zu verpacken, ihn also in eine "Box" zu stecken, bezeichnet man als Boxing.

Beim Boxing handelt es sich um eine implizite Konvertierung. Diese wird vom Compiler erledigt und braucht kein Zutun des Programmierers. Soll allerdings der "verpackte" Wert wieder entnommen werden, muss eine explizite Rückkonvertierung durchgeführt werden. Diese Rückkonvertierung bezeichnet man als Unboxing:

int i = 100;
object o = i;                    // Boxing
int u = (int)o;                  // Unboxing

Console.WriteLine(o.GetType());  // Ausgabe: System.Int32

Boxing und Unboxing sollte jedoch immer im Zusammenhang mit der Ausführungsgeschwindigkeit gesehen werden. Sowohl Boxing als auch Unboxing sind relativ rechenintensive Prozesse. Beim Boxing wird ein komplett neues Objekt erzeugt, das heißt, der Speicher muss reserviert und das neue Objekt muss instantiiert werden. Dieser Prozess kann bis zu 20 mal länger dauern, als eine gewöhnliche Wertezuweisung. Beim Unboxing kann der Prozess des Castens bis zu vier mal so lange dauern, wie eine Wertezuweisung. Aus diesem Grund sollte besonders bei performanztechnischen Aspekten sehr darauf geachtet werden, an welcher Stelle Boxing und Unboxing sinnvoll ist und verwendet werden kann.[3]

Andere Konvertierungsmethoden

.NET bietet neben den oben genannten Konvertierungsmöglichkeiten auch etliche andere Möglichkeiten, wie ein Wert in einen anderen Konvertiert werden kann. Die vermutlich meist benutzt Konvertierung ist die von einer Zahl in einen String und umgekehrt. Diese wird hier kurz behandelt, um das .NET-Paradigma der sonstigen Konvertierungen zu zeigen.

Um einen beliebigen Datentyp, also auch eine Ganzzahl, in einen String umzuwandeln, steht jedem Objekt die Methode ToString() zur Verfügung. Im Prinzip kann mit dieser Methode jedes Objekt und jeder Typ in einen String umgeandelt werden. Allerdings muss die Umwandlungsoperation manchmal von Hand programmiert werden, damit die Umwandlung in die gewünschte Ausgabe funktioniert.

Um eine Zahl in einen String umzuwandeln reicht es, wenn hinter den Bezeichner einer Zahl einfach der Aufruf von ToString() folgt. Für die Rückrichtung muss eine Parsing-Methode aufgerufen werden, da es auch Zeichenketten geben kann, in denen nicht nur Zahlen, sondern auch Buchstaben vorkommen. Die Parsing-Methode überprüft dabei, ob in der Zeichenkette eine Zahl vorhanden ist und wandelt diese in eine Zahl zurück.

int i = 15;
string s = i.ToString();            // Speichert in s den Wert "15"
int z = Int.Parse(s);               // Wandelt "15" zurück in 15 und speichert den Wert in z

In C# existieren noch viele weitere dieser Parsing-Funktionen.

Arrays

Arrays dienen dazu, mehrere Elemente eines Typs zusammen zu fassen. In vielen Programmiersprachen sind Arrays ein fester Bestandteil der Sprachen selbst. In C# sind Arrays allerdings Instanzen der Klasse System.Array. Ein Array ist also ein Objekt, ein Referenztyp. Die Elemente eines Arrays können jedoch sowohl Werte- als auch Referenztypen beinhalten.

Arrays werden deklariert, in dem nach der Angabe des Typs eine öffnende und eine schließende eckige Klammer ([]) folgen, gefolgt vom Bezeichner. Arrays können deklariert oder gleich mit einer festen Größe initialisiert werden. Die Größe von Arrays ist im Nachhinein nicht mehr änderbar. Neben eindimensionalen Arrays gibt es auch die Möglichkeit, mehrdimensionale Arrays zu erzeugen. Das folgende Codebeispiel soll dies verdeutlichen:

int[] arr1;                     // Deklaration
arr1 = new int[5];              // Initialisierung
int[] arr2 = new int[5];        // Deklaration und Initialisierung
arr1[0] = 1;                    // Initialisierung des Array-Wertes an der ersten Stelle
arr1[1] = 2;                    // Initialisierung des Array-Wertes an der zweiten Stelle

int[] arr = { 1, 2, 3, 4, 5 };  // Deklaration und Initialisierung eines Arrays der Länge 5 mit bereits gefüllten Werten.

int[,] twoDim = new int[5,7];   // Deklaration und Initialisierung eines Arrays mit zwei Dimensionen
int[...] multiArray = new int[2, 3, 2, 2]; // Deklaration und Initialisierung eines 4-dimensionalen Arrays

Das .NET-Framework bietet unterschiedlichste Operationen, die auf Arrays durchgeführt werden können. Zu einigen zählen das Sortieren, Kopieren, Löschen, Suchen usw. Für Details über diese Funktionen ist es am besten, die MSDN-Bibliothek zum Thema Array zu befragen.

Aufzählungstypen

Mit Aufzählungen (in C#: enum) ist es möglich, mehrere konstante Werte zu gruppieren. Der Vorteil bei dieser Methode ist, dass die Konstanten in einem Datentyp gekapselt sind und somit als Klartext in einem Programm verwendet werden können.

Aufzählungen, oder enums werden folgendermaßen deklariert:

enum WeekDays
{
   Monday,
   Tuesday,
   Wednesday,
   Thursday,
   Friday,
   Saturday,
   Sunday
};

[...]

WeekDays day = WeekDays.Monday;

if (day == WeekDays.Tuesday)
   Console.WriteLine("It's Tuesday!");

Intern wird zu jedem Eintrag der Enumeration ein Integer-Wert abgespeichert, der auch abgefragt werden kann. Es ist jedoch auch möglich, diesen Wert selbst zu vergeben:

enum Preise
{
   Tomaten = 12,
   Brot = 10,
   Chili = 20,
   Eier = 4
};

Um im Programm alle Namen einer Enumeration zu ermitteln, kann die Klasse Enum verwendet werden:

string[] names;
names = Enum.GetNames(typeof(Preise));

for (int i = 0; i < names.Length; i++)
   Console.WriteLine(names[i] + ": " + (int)Enum.Parse(typeof(Preise), names[i]));


Bitfelder

Bitfelder sind eine besondere Art von Aufzählungen. Sie werden dann benutzt, wenn nicht nur eine Eigenschaft einer Aufzählung zutreffen kann, sondern mehrere Eigenschaften zutreffen. Die Definition eines Bitfeldes erfolgt am einfachsten über Attribute. Zum Beispiel können Dateien die Eigenschaften Archiv, Versteckt, Nur Lesen und System haben. Eine Datei kann mehrere dieser Eigenschaften (bei Betriebssystemen ebenfalls Attribute genannt) gleichzeitig annehmen. In C# erfolgt die Definition und der Test dieser Eigenschaften folgendermaßen:

[Flags()] // Hier wird das C#-Attribut "Flags()" verwendet
enum FileAttributes
{
   Archive = 0,
   Hidden = 1,
   Readonly = 2,
   System = 4,
   All = Archive | Hidden | Readonly | System
};

FileAttributes attrib = FileAttributes.Archive;
attrib = attrib | System; // Fügt neben Archiv auch das Attribut System hinzu

if ((attrib & FileAttributes.Hidden) = FileAttributes.Hidden)  // Testet, ob attrib das Attribut Hidden beinhaltet
   Console.WriteLine("The file is hidden!");

Intern ist der Aufbau der Enumeration dann folgendermaßen:

Archive  = 0000
Hidden   = 0001
Readonly = 0010
System   = 0100
All      = 0111

Beim Test wird bitweise mit Hilfe des &-Operators geprüft, ob eine bestimmte Eigenschaft vorhanden ist (siehe Beispiel).

Programmdesign

Objektorientierung

Objektorientierte Programmierung (OOP) beschreibt die Abstraktion des Programmflusses auf der Ebene von Objekten. Dabei interagieren Objekte untereinander, können Daten austauschen und sich gegenseitig referenzieren. Außerdem können praktisch beliebig viele neue Objekte erzeugt werden. Das Paradigma der Objektorientierung verspricht eine besseren Überblick und eine sauberere Programmierung, vor allem in komplexen Programmen.

Es gibt viele Sprachen mit objektorientiertem Ansatz, jedoch nur wenige, die das Konzept von Objektorientierung vollständig umsetzen. Die erste dieser Sprachen war Java. Mit C#, bzw. dem .NET-Framework kam eine weitere vollständig objektorientierte Sprache hinzu.

Grundsätzlich gibt es vier Konzepte, die für objektorientierte Programmiersprachen gelten:

  • Abstraktion, die Trennung von Konzept und Umsetzung,
  • Kapselung, die Zusammenfassung von Daten und dazugehöriger Funktionalität,
  • Polymorphie, die Fähigkeit eines Objekts, eine Instanz einer von seiner Klasse abgeleiteten Klasse aufzunehmen und
  • Vererbung, die die Möglichkeit der Spezialisierung und die Erstellung einer Klassenhierarchie ermöglicht.

In der Tutorialsammlung befinden sich einige Quellen, die das Verstehen und den Einstieg in Objektorientierung erleichtern. Für eine deutlich umfangreichere Diskussion des Paradigmas Objektorientierung eignet sich der Wikipedia-Artikel Object-oriented programming] als Einstieg.

Namensräume

Namensräume oder Namespaces stellen eine Möglichkeit dar, ein Projekt zu strukturieren. Dabei werden unterschiedliche Komponenten eines Programms in einem Namensraum zusammengefasst. Der Vorteil von Namespaces besteht darin, dass es in zwei Namespaces Klassen mit demselben Namen geben kann, ohne dass Konflikte verursacht werden. Des Weiteren bieten Namespaces die Möglichkeit der Schachtelung, was die Strukturierung um ein Vielfaches erleichtert.

Das folgende Beispiel soll die Verwendung von Namespaces verdeutlichen:

namespace FileHandling
{
   namespace FileIO
   {
      class FileSearcher { ... }
      class FileDeleter { ... }
   }

   namespace FileAttributes
   {
      class FileAttributeConnector { ... }
   }

   class StandardFileHandler { ... }
}

namespace MyNamespace1
{
   Class Class1
   {
      void DoSomething()
      {
         FileHandling.FileIO.FileSearcher search = new FileHandling.FileIO.FileSearcher();
         FileHandling.StandardFileHandler fileHandler = new FileHandling.StandardFileHandler();
         FileHandling.FileIO.FileDeleter deleter = new FileHandling.FileIO.FileDeleter();
      }
   }
}

Durch Verwendung des using-Schlüsselworts können Klassen in anderen Namespaces auch ohne die Angabe ihrer Namensräume verwendet werden:

using FileHandling;
using FileHandling.FileIO;

namespace MyNamespace2
{
   class Class1
   {
      void DoSomething()
      {
         FileSearcher search = new FileSearcher();
         StandardFileHandler fileHandler = new StandardFileHandler();
         FileDeleter deleter = new FileDeleter();
      }
   }
}

Kommt es allerdings zu Ambiguitäten, also zu der Verwendung eines gleichen Namens aus zwei Namespaces, muss der Namespace der Referenzierung angegeben werden, damit die Eindeutigkeit gewahrt bleibt.

Klassen

In C#-Programmen wird die gesamte Funktionalität über Klassen gesteuert. Das liegt daran, dass in C# und .NET alles von der Klasse Object abstammt.Damit ein C#-Programm lauffähig ist, muss zumindest eine Klasse deklariert werden, in der sich die auszuführenden Anweisungen befinden. Klassen sind sozusagen Baupläne für jedes Objekt, dass in der Anwendung erzeugt werden kann.

Eine Klasse kann aus Feldern, Methoden, Konstruktoren und Destruktoren bestehen, wobei alle diese vier Eigenschaften weggelassen werden können, um eine Klasse zu deklarieren.

Wird ein Objekt einer Klasse erzeugt, man sagt, die Klasse wird instantiiert, so erhält das Objekt alle in der Klasse definierten Methoden und Felder. Es können beliebig viele Objekte aus einer Klasse erzeugt werden. Obwohl ihnen derselbe Bauplan zugrunde liegt, können sie alle vollkommen unabhängig voneinander agieren. (Ausnahmen bilden statische Methoden und Felder, auf die hier noch nicht eingegangen wird.)

Die Definition einer Klasse und die Erzeugung eines Objektes aus dieser Klasse sieht wie folgt aus:

class Cube
{
   // Felderdefinition der Klasse
   int width;
   int height;
   int depth;

   // Konstruktordefinition (Default-Constructor)
   public Cube()
   { 
      this.width = this.height = this.depth = 0;
   }

   // Konstruktordefinition (Überladener Constructor)
   public Cube(int width, int height, int depth)
   {
      this.width = width;
      this.height = height;
      this.depth = depth;
   }

   // Methodendefinition
   public int Volume()
   {
      return (this.width * this.height * this.depth);
   }
}

class Class1
{
   void DoSomething()
   {
      Cube cube1 = new Cube();          // Erzeugung eines Objektes / einer Instanz der Klasse Cube aus dem Standardkonstruktor
      Console.WriteLine("Das Volumen ist: " + cube1.Volume()); // Ausgabe: Das Volumen ist: 0

      Cube cube2 = new Cube(2, 2, 2);   // Erzeugung eines Objektes / einer Instanz der Klasse Cube aus dem überladenen Konstruktor
      Console.WriteLine("Das Volumen ist: " + cube2.Volume()); // Ausgabe: Das Volumen ist: 8
   }
}

Der Konstruktor einer Klasse wird immer dann aufgerufen, wenn eine Instanz der Klasse, also ein Objekt, erzeugt wird. Dabei werden alle Anweisungen, die im Rumpf des Konstruktors definiert wurden, ausgeführt. Er zeichnet sich dadurch aus, dass er nur den Namen der Klasse selbst mit einer öffnenden und schließenden Klammer ((...)), in der sich eventuelle Parameterdefinitionen befinden, beinhaltet. Ist kein Konstruktor definiert, wird zur Erzeugung eines Objektes der Klasse immer der Standardkonstruktor (Klasse()) aufgerufen.

Methoden

Methodendeklarationen einer Klasse sehen ähnlich aus, wie Konstruktordefinitionen, mit dem Unterschied, dass sie nicht denselben Namen, wie die Klasse selbst, haben dürfen und dass sie immer einen Rückgabewert besitzen. Entweder, dieser Wert ist void, also "nichts", oder der Rückgabewert entspricht einem beliebigen Typen. Mit der return-Anweisung innerhalb der Methode kann ein Wert zurückgegeben werden. void-Methoden benötigen kein return, bei Methoden mit Rückgabewert ist dies Pflicht.

Parameter

Parameter sind Variablen, die an eine Methode übergeben werden können. Mit ihnen kann man Methoden aufrufen und den Verlauf von Methoden beeinflussen. Parameter werden innerhalb der öffnenden und schließenden Klammer ((...)) einer Methodendefinition eingefügt und bestehen immer aus Übergabetyp und Bezeichner. Wenn keine Parameter übergeben werden sollen, bleibt der Bereich zwischen den Klammern leer.

void Method1()                  // Definition einer Methode ohne Parameter
{
   // Do something
}

void Method2(int a, int b)      // Definition einer Methode mit zwei Parametern vom Typ int
{
   for (int i = a; a < b; a++)  // Zugriff auf die übergebenen Parameter
      Method1();                // Aufruf der Methode 1 ohne Parameter
}


Optionale Parameter

In C# ist es nicht wirklich möglich, optionale Parameter in einer Methodendefinition anzugeben. Allerdings bietet das Schlüsselwort params die Möglichkeit, mehrere Parameter in einer Methodendeklaration anzugeben, wobei deren Anzahl egal ist. Es gilt jedoch zu beachten:

  • params darf nur einmal in der Methodendefinition vorkommen.
  • Nach params dürfen keine weiteren Parameter übergeben werden.

Der Beispielcode in der MSDN zeigt, wie optionale Parameter verwendet werden können.

Modifizierer

Auf alle oben besprochenen Elemente kann nur zugegriffen werden, wenn eine Instanz der jeweiligen Klasse erzeugt worden ist. Dies gilt für Klassenfelder, Methoden und Klassen selbst. Es gibt jedoch auch die Möglichkeit, auf Methoden oder Felder zuzugreifen, ohne, dass die Klasse, in der sie sich befinden, instantiiert wurde. Diese Felder und Methoden heißen statisch. Diese Felder existieren nur einmal, nämlich auf Klassenebene.

Statische Elemente werden definiert, in dem vor ihre Deklaration das Schlüsselwort static hinzugefügt wird. Im folgenden Beispiel wird die Benutzung von statischen Feldern einer klasse deutlich:

class Fahrzeug : IDisposable
{
   public static int instances = 0;

   public Fahrzeug()
   {
      instances++;
   }

   public void Dispose()
   {
      instances--;
   }

   public void PrintFahrzeugCounter()
   {
      Console.WriteLine("Es gibt im Moment " + Fahrzeug.instances + " Fahrzeuge.");
   }
}

Wird hier ein Objekt vom Typ Fahrzeug erzeugt, so wird der Zähler für die Instanzen um eins erhöht. Jedes Fahrzeug hat dann die Möglichkeit, die aktuelle Anzahl aller vorhandenen Fahrzeuge auszugeben. Auf die Anzahl der Fahrzeuge können alle Fahrzeug-Objekte gleichzeitig zugreifen, da sie Statisch definiert ist. Vom Interface IDisposable mit seiner Methode Dispose() wurde lediglich abgeleitet, um den Zähler der Fahrzeuginstanzen herunter zu zählen, sobald ein Fahrzeugobjekt im Speicher zerstört wird.

Eine weitere nützliche Verwendung der statischen Eigenschaften zeigt sich im Singleton-Pattern.

Neben public, private und static gibt es noch weitere Modifizierer, die in C# verwendet werden können.

Modifizierer Verwendungszweck / Bedeutung
public Zugriff/Sichtbarkeit. Elemente, die als public deklariert sind, sind von außerhalb der Klasse sichtbar, d.h. auf diese Member kann zugegriffen werden.
private Zugriff/Sichtbarkeit. Auf Elemente, die als private deklariert sind, kann nur aus der Klasse heraus zugegriffen werden, in der sie deklariert sind. Der Zugriff aus von der Klasse abgeleiteten Klassen ist ebenfalls nicht möglich.
internal Zugriff/Sichtbarkeit. Der Zugriff auf ein als internal deklariertes Element ist nur aus Dateien der gleichen Assembly heraus möglich. Dieser Zugriffsmodifizierer kann mit protected kombiniert werden.
protected Zugriff/Sichtbarkeit. Der Zugriff auf ein als protected deklariertes Element ist nur von innerhalb der Klasse oder innerhalb einer abgeleiteten Klasse möglich. Dieser Modifizierer kann mit internal kombiniert werden.
abstract Als abstrakt werden unvollständige Member einer Klasse bezeichnet, beispielsweise Methoden ohne konkrete Implementierung. Dadurch wird die Klasse selbst automatisch ebenfalls abstrakt. Von abstrakten klassen kann/darf keine Instanz erzeugt werden, sie erzwingen, dass eine andere Klasse abgeleitet wird.
event Der Modifizierer event wird oftmals fälschlicherweise als herkömmliches Schlüsselwort bzw. reserviertes Wort bezeichnet. Tatsächlich ist es so, dass durch event aus Delegates Ereignisse deklariert werden. Der Modifizierer hat Einfluss auf die Verwendungsmöglichkeit des damit bezeichneten Delegates.
extern Der Modifizierer extern bezieht sich auf Methoden. Er gibt an, dass die Methode nicht innerhalb des aktuellen Projekts, sondern in einer DLL des Betriebssystems deklariert ist. Dieser Modifizierer wird für das so genannte P/Invoke, den Zugriff auf WinAPI-Funktionen, verwendet.
override override dient zum Überschreiben einer Methode der Basisklasse, die als virtual bezeichnet ist und sowohl gleiche Signatur als auch gleiche Sichtbarkeit aufweisen muss.
readonly Der Modifizierer readonly ermöglicht Nur-Lese-Zugriffe auf öffentliche Instanzvariablen. Normalerweise sollte es aber keine öffentlichen Instanzvariablen geben, sonder der Zugriff immer über Eigenschaften erfolgen.
sealed sealed ist sozusagen das Gegenteil von abstract. Von einer "versiegelten" Klasse kann nicht abgeleitet werden. Bekanntester Vertreter solcher Klassen ist (zum Leidwesen vieler Programmierer) die Klasse String.
static Der Modifizierer static legt fest, dass der damit bezeichnete Member einer Klasse Bestandteil auf Klassenebene und nicht auf Instanzebene ist.
unsafe Der Modifizierer unsafe dient zum Erzeugen von "unsicherem" Code, also Code, der nicht von der Laufzeitumgebung kontrolliert wird und auf den auch die Garbage-Collection keine Einwirkung hat.
virtual Der Modifizierer virtual legt fest, dass ein damit bezeichneter Member in einer abgeleiteten Klasse überschrieben werden kann.
volatile volatile wird für Felder (Instanzvariablen) verwendet. Das so bezeichnete Feld kann vom Betriebssystem ausgelesen werden.
new Achtung - hier ist der Modifizierer new gemeint, nicht der Operator new zum Erzeugen einer neuen Instanz. Der Modifizierer new dient dazu, Member einer Klasse in einer davon abgeleiteten Klasse zu verdecken.

Structs

Strukturen, oder auch Structs sind Wertetypen. Sie verhalten sich anders als Klassen, können aber fast die gleichen Member aufnehmen. Es gibt jedoch einige Unterschiede zu Klassen:

  • Strukturen können nicht Basis einer Vererbungshierarchie sein, erben jedoch implizit von Object.
  • Der Standardkonstruktor ist in Strukturen implizit vorhanden und kann nicht explizit deklariert werden.
  • Strukturen können ohne Initialisierung durch new verwendet werden.

Alle Wertetypen (int, double usw.) sind intern Strukturen. Das Vorhandensein eines Konstruktors bei einem Wertetyp dient allerdings nur dazu, die Felder des struct mit Standardwerten zu initialisieren. Daher muss das Schlüsselwort new nicht verwendet werden. Verzichtet man auf die Verwendung von new, müssen die Felder der Struktur vor der erstmaligen Benutzung mit Werten belegt werden.

public struct Coords
{
   public int x;
   public int y;
   public int z;

   public Coords(int x, int y, int z)
   {
      this.x = x;
      this.y = y;
      this.z = z;
   }
}

[...]

void DoSomething()
{
   Coords coords;  // Der Standardkonstruktor ist immer vorhanden und muss nicht aufgerufen werden
   coords.x = 10;
   coords.y = 20;
   coords.z = -1;

   Coords new_coords = new Coords(10, 11, -5);  // Erzeugen des Structs new_coords mit Hilfe des eigenen Konstruktors
}

Attribute



Mit Attributen ist es möglich, Informationen zu Programmen hinzuzufügen. Es handelt sich bei diesen Informationen um Metadaten. Diese können von der CLR (Common Language Runtime) zur Laufzeit ausgewertet werden. Über Reflection ist es auch möglich, diese Metadaten innerhalb der eigenen Programme auszuwerten.

Eines dieser Attribute ist das Attribut Flags, das im Zusammenhang von Bitfeldern behandelt wird. Ohne Attribute müsste dem Programm zur Laufzeit mitgeteilt werden, dass es sich bei der Aufzählung um ein Bitfeld handelt. Üblicherweise geschieht das über ein eigenes Konstrukt, oder man definiert von vorne herein eine Aufzählung, die nur ein Bitfeld sein kann. Über Attribute ist man da wesentlich flexibler.

Ein anderes Beispiel ist das Attribut Obsolete. Mit diesem Attribut lässt sich ein bestimmter Programmteil als veraltet deklarieren. Der Compiler wird dann beim Kompilieren eine Warnung ausgeben. Der Warnungstext ist dabei selbst definierbar:

[System.Obsolete("This class is obsolete. Use Car")]
class Automobile
{
   public void CreateAutomobile()
   {
      // Do Something old   
   }
}

class Car
{
   [System.Obsolete("This method is obsolete. Use CreateCar(int numWheels, System.Drawing.Color color)", true]
   public void CreateCar(int numWheels)
   {
      // Create a car with some wheels
   }

   public void CreateCar(int numWheels, System.Drawing.Color color)
   {
      // Create a car with some wheels and a certain color
   }
}

Das Schlüsselwort true innerhalb des Obsolete-Schlüsselworts sorgt dafür, dass bei Benutzung dieser Methode ein Compiler-Fehler erzeugt wird. Dies ist dann von Vorteil, wenn ein Fehler innerhalb einer Methode (oder Klasse) entdeckt wurde und dieser in allen Programmen, die diese verwenden, korrigiert werden muss.

Attribute leiten sich, wie alles andere in .NET auch, von einer Oberklasse, in diesem Fall der Klasse System.Attribute ab. Aus der Namenskonvention heraus haben alle Attribute den Suffix Attribute. Dies hat den Grund, dass der Compiler zunächst nach den Klassennamen sucht, die verwendet werden. Im Beispiel von Flags sucht er zunächst nach einer Klasse Flags. Wird diese nicht gefunden, hängt der Compiler automatisch die Endung Attribute an den Namen an und sucht dann nach dieser Klasse, also in dem Fall nach FlagsAttribute. Diese Namenskonvention ist bei allen Attributen in .NET vertreten. Zum Beispiel auch bei Obsolete, dessen Klasse ObsoleteAttribute heißt oder auch bei Serializable mit SerializableAttribute.

Alle Felder und Eigenschaften von Attributen, die als benannte Parameter fungieren sollen, müssen öffentlich und les-/beschreibbar sein. Des Weiteren können die Elemente, für die ein Attribut angewendet werden kann, frei festgelegt werden.

Dieses Beispiel zeigt die Deklaration und Verwendung eines Todo-Attributs, adaptiert aus Frank Eller und Michael Kofler, "Visual C# - Grundlagen, Programmiertechniken, Windows-Programmierung", Addison-Wesley. ISBN 3-8273-2255-3.

Ein Attribut kennzeichnet in .NET eine Markierung, die auf

angewendet werden kann.

Um zum Beispiel eine Klasse als serialisierbar zu markieren markiert man sie mit dem Attribut [Serializable]

[Serializable]
public class Klasse
{ }

Siehe auch

  1. MSDN-Artikel zu Attributen

Literatur

In der C#-Tutorialsammlung in diesem Wiki stehen etliche Tutorials aufgelistet, die helfen, C# zu erlernen.

Dieser Artikel orientiert sich sehr an dem Buch Visual C# - Grundlagen, Programmiertechniken, Windows-Programmierung.[4].

Referenzen

  1. ECMA-Spezifikation der Sprache
  2. ISO - C# Language Specification
  3. http://msdn.microsoft.com/en-us/library/ms173196%28VS.80%29.aspx
  4. Frank Eller und Michael Kofler, "Visual C# - Grundlagen, Programmiertechniken, Windows-Programmierung", Addison-Wesley. ISBN 3-8273-2255-3