C# 6.0 – kurz & gut - Joseph Albahari - E-Book

C# 6.0 – kurz & gut E-Book

Joseph Albahari

0,0

Beschreibung

Dieses Buch ist für vielbeschäftigte Programmierer gedacht, die eine knappe, aber dennoch gut verständliche Beschreibung von C# 6.0 suchen. C# 6.0 – kurz & gut informiert Sie über genau das, was Sie wissen müssen, um schnell durchstarten zu können. Behandelt werden: - alle Aspekte der C#-Syntax, vordefinierte Typen, Ausdrücke und Operatoren - das Erstellen von Klassen, Structs, Delegates und Events, Enums, Generics und Constraints, Exception Handling und Iteratoren - die Feinheiten des Boxing, das Überladen von Operatoren, Lambda-Ausdrücke, die Delegate-Kovarianz oder das Auflösen von Erweiterungsmethoden - dynamische Bindung und asynchrone Funktionen - LINQ – von den Standard-Abfrageoperatoren bis zu einer vollständigen Referenz der Query-Syntax Trotz seines erstaunlich kompakten Formats bietet dieses Buch eine Fülle von Details. Es unterstützt Sie optimal, die konzeptionellen Herausforderungen beim Lernen von C# 6.0 schnell zu meistern. Wenn Sie bereits mit Java, C++ oder einer älteren Version von C# vertraut sind, ist C# 6.0 – kurz & gut die ideale Wahl.

Sie lesen das E-Book in den Legimi-Apps auf:

Android
iOS
von Legimi
zertifizierten E-Readern

Seitenzahl: 223

Das E-Book (TTS) können Sie hören im Abo „Legimi Premium” in Legimi-Apps auf:

Android
iOS
Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



C# 6.0

kurz & gut

Joseph AlbahariBen Albahari

Deutsche Übersetzung von Lars Schulten & Thomas Demmig

Joseph Albahari & Ben Albahari

Übersetzung: Lars Schulten & Thomas DemmigLektorat: Alexandra FolleniusKorrektorat: Sibylle FeldmannHerstellung: Susanne BröckelmannUmschlaggestaltung: Karen Montgomery & Michael Oréal, www.oreal.deSatz: Reemers Publishing Services GmbH, Krefeld, www.reemers.deDruck und Bindung: Media-Print Informationstechnologie, mediaprint-druckerei.de

Bibliografische Information Der Deutschen NationalbibliothekDie Deutsche Nationalbibliothek verzeichnet diese Publikation in der DeutschenNationalbibliografie; detaillierte bibliografische Daten sind im Internet überhttp://dnb.d-nb.de abrufbar.

ISBN:Print   978-3-96009-029-8PDF   978-3-96010-045-4ePub   978-3-96010-046-1mobi   978-3-96010-047-8

Dieses Buch erscheint in Kooperation mit O’Reilly Media, Inc. unter dem Imprint »O’REILLY«. O’REILLY ist ein Markenzeichen und eine eingetragene Marke von O’Reilly Media, Inc. und wird mit Einwilligung des Eigentümers verwendet.

4. AuflageCopyright © 2016 dpunkt.verlag GmbHWieblinger Weg 1769123 Heidelberg

Authorized German translation of the English edition of C# 6.0 Pocket Reference, ISBN 9781491927410 © 2016 Joseph Albahari, Ben Albahari. This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same.

Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen.

Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und Produktbezeichnungen der jeweiligen Firmen im Allgemeinen warenzeichen-, marken- oder patentrechtlichem Schutz unterliegen.

Die Informationen in diesem Buch wurden mit gröéter Sorgfalt erarbeitet. Dennoch können Fehler nicht vollständig ausgeschlossen werden. Verlag, Autoren und Übersetzer übernehmen keine juristische Verantwortung oder irgendeine Haftung für eventuell verbliebene Fehler und deren Folgen.

5 4 3 2 1 0

Inhalt

Inhalt

C# 6.0 – kurz & gut

Ein erstes C#-Programm

Syntax

Typgrundlagen

Numerische Typen

Der Typ bool und die booleschen Operatoren

Strings und Zeichen

Arrays

Variablen und Parameter

Ausdrücke und Operatoren

Null-Operatoren

Anweisungen

Namensräume

Klassen

Vererbung

Der Typ object

Structs

Zugriffsmodifikatoren

Interfaces

Enums

Eingebettete Typen

Generics

Delegates

Events

Lambda-Ausdrücke

Anonyme Methoden

try-Anweisungen und Exceptions

Enumeration und Iteratoren

Nullbare Typen

Überladen von Operatoren

Erweiterungsmethoden

Anonyme Typen

LINQ

Die dynamische Bindung

Attribute

Aufrufer-Info-Attribute

Asynchrone Funktionen

Unsicherer Code und Zeiger

Präprozessordirektiven

XML-Dokumentation

Index

C# 6.0 – kurz & gut

C# ist eine allgemein anwendbare, typsichere, objektorientierte Programmiersprache, die die Produktivität des Programmierers erhöhen soll. Zu diesem Zweck versucht die Sprache, die Balance zwischen Einfachheit, Ausdrucksfähigkeit und Performance zu finden. Die Sprache C# ist plattformneutral, wurde aber geschrieben, um gut mit dem .NET Framework von Microsoft zusammenzuarbeiten. C# 6.0 ist auf das .NET Framework 4.6 ausgerichtet.

Die Programme und Codefragmente in diesem Buch entsprechen denen aus den Kapiteln 2 und 4 von C# 6.0 in a Nutshell und sind alle als interaktive Beispiele in LINQPad verfügbar. Das Durcharbeiten dieser Beispiele im Zusammenhang mit diesem Buch fördert den Lernvorgang, da Sie bei der Bearbeitung der Beispiele unmittelbar die Ergebnisse sehen können, ohne dass Sie in Visual Studio dazu Projekte und Projektmappen einrichten müssten.

Sie können die Beispiele unter http://bit.ly/linqpad_csharp6_samples herunterladen. LINQPad ist kostenlos – Sie finden es unter http://www.linqpad.net.

Ein erstes C#-Programm

Das hier ist ein Programm, das 12 mit 30 multipliziert und das Ergebnis ausgibt (360). Der doppelte Schrägstrich (Slash) gibt an, dass der Rest einer Zeile ein Kommentar ist.

Im Kern dieses Programms gibt es zwei Anweisungen. In C# werden Anweisungen nacheinander ausgeführt und jeweils durch ein Semikolon abgeschlossen. Die erste Anweisung berechnet den Ausdruck12 * 30 und speichert das Ergebnis in einer lokalen Variablen namens x, die einen ganzzahligen Wert repräsentiert. Die zweite Anweisung ruft die WriteLine-Methode der Klasse Console auf, um die Variable x in einem Textfenster auf dem Bildschirm auszugeben.

Eine Methode führt eine Aktion als Abfolge von Anweisungen aus, die als Anweisungsblock bezeichnet wird – ein (geschweiftes) Klammernpaar mit null oder mehr Anweisungen. Wir haben eine einzelne Methode mit dem Namen Main definiert.

Das Schreiben von High-Level-Funktionen, die Low-Level-Funktionen aufrufen, vereinfacht ein Programm. Wir können unser Programm refaktorieren, indem wir eine wiederverwendbare Methode schreiben, die einen Integer-Wert mit 12 multipliziert:

Eine Methode kann Eingabedaten vom Aufrufenden erhalten, indem sie Parameter spezifiziert, und Daten zurück an den Aufrufenden geben, indem sie einen Rückgabetyp festlegt. Wir haben eine Methode FeetToInches definiert, die einen Parameter für die Übergabe der Feet und einen Rückgabetyp für die berechneten Inches hat:

Die Literale30 und 100 sind die Argumente, die an die Methode FeetToInches übergeben wurden. Die Methode Main hat in unserem Beispiel leere Klammern, da sie keine Parameter besitzt, und sie ist void, weil sie keinen Wert an den Aufrufenden zurückliefert. C# erkennt eine Methode mit dem Namen Main als Angabe des Standardeinstiegspunkts für die Ausführung. Die Methode Main kann optional einen Integer-Wert zurückgeben (statt void), um der Ausführungsumgebung einen Wert zu übermitteln. Sie kann auch optional ein Array mit Strings als Parameter erwarten (das dann durch die Argumente gefüllt wird, die an die ausführbare Datei übergeben werden). Hier sehen Sie ein Beispiel:

static int Main (string[] args) {...}

Ein Array (wie zum Beispiel string[]) steht für eine feste Zahl an Elementen eines bestimmten Typs (siehe den Abschnitt »Arrays« auf Seite 32).

Methoden sind eine der vielen Arten von Funktionen in C#. Eine andere Art von Funktionen, die wir verwendet haben, war der *-Operator, der dazu dient, Multiplikationen auszuführen. Des Weiteren gibt es noch Konstruktoren, Eigenschaften, Events, Indexer und Finalizer.

In unserem Beispiel sind die beiden Methoden in einer Klasse zusammengefasst. Eine Klasse gruppiert Funktions-Member und Daten-Member zu einem objektorientierten Building-Block. Die Klasse Consolefasst Member zusammen, die Funktionalität zur Ein- und Ausgabe an der Befehlszeile bieten, zum Beispiel die Methode WriteLine. Unsere Klasse Test fasst zwei Methoden zusammen – Main und FeetToInches. Eine Klasse ist eine Art von Typ; das wird später im Abschnitt »Typgrundlagen« auf Seite 9 genauer erläutert.

Auf der obersten Ebene eines Programms werden Typen in Namensräume eingeteilt. Die using-Direktive wurde genutzt, um unserer Anwendung den Namensraum System verfügbar zu machen, damit sie die Klasse Console nutzen kann. Wir können alle von uns bislang definierten Klassen folgendermaßen im TestPrograms-Namensraum zusammenfassen:

using System;namespace TestPrograms{  class Test  {...}  class Test2 {...}}

Das .NET Framework ist in hierarchischen Namensräumen organisiert. Dazu gehört zum Beispiel der Namensraum, der die Typen für den Umgang mit Text enthält:

using System.Text;

Die Direktive using dient der Bequemlichkeit – Sie können einen Typ auch über seinen vollständig qualifizierten Namen ansprechen. Das ist der Name des Typs, dem sein Namensraum vorangestellt ist, zum Beispiel System.Text.StringBuilder.

Kompilation

Der C#-Compiler führt Quellcode, der in einer Reihe von Dateien mit der Endung .cs untergebracht ist, in einer Assembly zusammen. Eine Assembly ist die Verpackungs- und Auslieferungseinheit in .NET und kann entweder eine Anwendung oder eine Bibliothek sein. Eine normale Konsolen- oder Windows-Anwendung hat eine Main-Methode und ist eine .exe-Datei. Eine Bibliothek ist eine .dll-Datei – im Prinzip eine .exe-Datei ohne Einsprungpunkt. Ihr Zweck ist, von einer Anwendung oder anderen Bibliotheken aufgerufen (referenziert) zu werden. Das .NET Framework ist eine Sammlung von Bibliotheken.

Der Name des C#-Compilers ist csc.exe. Sie können entweder eine integrierte Entwicklungsumgebung (Integrated Development Environment, IDE) wie Visual Studio .NET nutzen, damit csc automatisch aufgerufen wird, oder den Compiler selbst per Hand über die Befehlszeile aufrufen. Um manuell zu kompilieren, speichern Sie ein Programm zunächst in einer Datei wie MyFirstProgram.cs und rufen dann csc auf (zu finden unter %ProgramFiles (X86)%\msbuild\14.0\bin):

csc MyFirstProgram.cs

Das erstellt eine Anwendung namens MyFirstProgram.exe.

Eine Bibliothek (.dll) erstellen Sie mit der folgenden Anweisung:

csc /target:library MyFirstProgram.cs

Eigenartigerweise bringt das .NET Framework 4.6 den C# 5-Compiler mit. Um den C# 6-Befehlszeilen-Compiler zu erhalten, müssen Sie Visual Studio oder MSBuild 14 installieren.

Syntax

Die Syntax von C# ist von der Syntax von C und C++ inspiriert. In diesem Abschnitt beschreiben wir die C#-Elemente der Syntax anhand des folgenden Programms:

Bezeichner und Schlüsselwörter

Bezeichner sind Namen, die Programmierer für ihre Klassen, Methoden, Variablen und so weiter wählen. Das hier sind die Bezeichner in unserem Beispielprogramm in der Reihenfolge ihres Auftretens:

System   Test   Main   x   Console   WriteLine

Ein Bezeichner muss ein ganzes Wort sein und aus Unicode-Zeichen bestehen, wobei der Anfang ein Buchstabe oder der Unterstrich ist. C#-Bezeichner unterscheiden Groß- und Kleinschreibung. Es ist üblich, Argumente, lokale Variablen und private Felder in Camel-Case zu schreiben (zum Beispiel myVariable) und alle anderen Bezeichner in Pascal-Schreibweise (zum Beispiel MyMethod).

Schlüsselwörter sind Namen, die für den Compiler eine bestimmte Bedeutung haben. Dies sind die Schlüsselwörter in unserem Beispielprogramm:

using   class   static   void   int

Die meisten Schlüsselwörter sind für den Compiler reserviert, Sie können sie nicht als Bezeichner verwenden. Hier ist eine vollständige Liste aller C#-Schlüsselwörter:

abstract         enum            long            stackallocas               event           namespace       staticbase             explicit        new             stringbool             extern          null            structbreak            false           object          switchbyte             finally         operator        thiscase             fixed           out             throwcatch            float           override        truechar             for             params          trychecked          foreach         private         typeofclass            goto            protected       uintconst            if              public          ulongcontinue         implicit        readonly        uncheckeddecimal          in              ref             unsafedefault          int             return          ushortdelegate         interface       sbyte           usingdo               internal        sealed          virtualdouble           is              short           voidelse             lock            sizeof          while

Konflikte vermeiden

Wenn Sie wirklich einen Bezeichner nutzen wollen, der mit einem reservierten Schlüsselwort in Konflikt geraten würde, müssen Sie ihn mit dem Präfix @ auszeichnen:

class class {...}     // illegalclass @class {...}    // legal

Das Zeichen @ gehört nicht zum Bezeichner selbst, daher ist @myVariable das Gleiche wie myVariable.

Kontextuelle Schlüsselwörter

Einige Schlüsselwörter sind kontextbezogen, d. h., sie können auch als Bezeichner eingesetzt werden – ohne ein vorangestelltes @-Zeichen. Das sind folgende:

add             equals         join           selectascending       from           let            setasync           get            nameof         valueawait           global         on             varby              group          orderby        whendescending      in             partial        

where

dynamic         into           remove         yield

Bei den kontextabhängigen Schlüsselwörtern kann es innerhalb des verwendeten Kontexts keine Mehrdeutigkeit geben.

Literale, Satzzeichen und Operatoren

Literale sind einfache Daten, die statisch im Programm verwendet werden. Die Literale in unserem Beispielprogramm sind 12 und 30. Satzzeichen helfen dabei, die Struktur des Programms abzugrenzen. Das hier sind die Satzzeichen in unserem Beispielprogramm: {, } und ;.

Die geschweiften Klammer gruppieren mehrere Anweisungen zu einem Anweisungsblock. Das Semikolon beendet eine Anweisung (die kein Block ist). Anweisungen können mehrere Zeilen übergreifen:

Console.WriteLine  (1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10);

Ein Operator verwandelt und kombiniert Ausdrücke. In C# werden die meisten Operatoren durch Symbole angezeigt, beispielsweise der Multiplikationsoperator *. Die Operatoren in unserem Programm sind folgende:

.   ()   *   =

Ein Punkt zeigt ein Member von etwas an (oder, in numerischen Literalen, den Dezimaltrenner). Klammern werden genutzt, wenn eine Methode aufgerufen oder deklariert wird; leere Klammern werden genutzt, wenn eine Methode keine Argumente akzeptiert. Das Gleichheitszeichen führt eine Zuweisung durch (ein doppeltes Gleichheitszeichen, , führt einen Vergleich auf Gleichheit durch).

Kommentare

C# bietet zwei verschiedene Arten von Quellcodekommentaren: einzeilige und mehrzeilige Kommentare. Ein einzeiliger Kommentar beginnt mit zwei Schrägstrichen und geht bis zum Zeilenende, zum Beispiel so:

Ein mehrzeiliger Kommentar beginnt mit /* und endet mit */, zum Beispiel so:

Kommentare können in XML-Dokumentations-Tags (siehe »XML-Dokumentation« auf Seite 210) eingebettet sein.

Typgrundlagen

Ein Typ definiert die Blaupause für einen Wert. In unserem Beispiel haben wir zwei Literale des Typs int mit den Werten 12 und 30 genutzt. Wir haben außerdem eine Variable des Typs int deklariert, deren Name x lautete.

Eine Variable zeigt einen Speicherort an, der mit der Zeit unterschiedliche Werte annehmen kann. Im Unterschied dazu repräsentiert eine Konstante immer den gleichen Wert (mehr dazu später).

Alle Werte sind in C# Instanzen eines spezifischen Typs. Die Bedeutung eines Werts und die Menge der möglichen Werte, die eine Variable aufnehmen kann, wird durch seinen bzw. ihren Typ bestimmt.

Vordefinierte Typen

Vordefinierte Typen (die auch als »eingebaute Typen« bezeichnet werden) Typen sind solche, die besonders vom Compiler unterstützt werden. Der Typ int ist ein vordefinierter Typ, der die Menge der Ganzzahlen darstellen kann, die in einen 32-Bit-Speicher passen – von –231 bis 231–1. Wir können zum Beispiel arithmetische Funktionen mit Instanzen des Typs int durchführen:

Ein weiterer vordefinierter Typ in C# ist string. Der Typ string repräsentiert eine Folge von Zeichen, zum Beispiel ».NET« oder »http://oreilly.com«. Wir können Strings bearbeiten, indem wir ihre Funktionen aufrufen:

Der vordefinierte Typ bool hat genau zwei mögliche Werte: true und false. bool wird häufig verwendet, um zusammen mit der if-Anweisung Befehle nur bedingt ausführen zu lassen:

Der Namensraum System im .NET Framework enthält viele wichtige Typen, die C# nicht vordefiniert (zum Beispiel DateTime).

Benutzerdefinierte Typen

So, wie wir komplexe Funktionen aus einfachen Funktionen aufbauen können, können wir auch komplexe Typen aus primitiven Typen aufbauen. In diesem Beispiel werden wir einen eigenen Typ namens UnitConverter definieren – eine Klasse, die als Vorlage für die Umwandlung von Einheiten dient:

Member eines Typs

Ein Typ enthält Daten-Member und Funktions-Member. Das Daten-Member von UnitConverter ist das Feld mit dem Namen ratio. Die Funktions-Member von UnitConverter sind die Methode Convert und der Konstruktor von UnitConverter

Symmetrie vordefinierter und benutzerdefinierter Typen

Das Schöne an C# ist, dass vordefinierte und selbst definierte Typen nur wenige Unterschiede aufweisen. Der primitive Typ int dient als Vorlage für Ganzzahlen (Integer). Er speichert Daten – 32 Bit – und stellt Funktions-Member bereit, die diese Daten verwenden, zum Beispiel ToString. Genauso dient unser selbst definierter Typ UnitConverter als Vorlage für die Einheitenumrechnung. Er enthält Daten – das Verhältnis zwischen den Einheiten – und stellt Funktions-Member bereit, die diese Daten nutzen.

Konstruktoren und Instanziierung

Daten werden erstellt, indem ein Typ instanziiert wird. Vordefinierte Typen können einfach mit einem Literal wie 12 oder "Hallo Welt" definiert werden.

Der new-Operator erstellt Instanzen von benutzerdefinierten Typen. Wir haben unsere Main-Methode damit begonnen, dass wir zwei Instanzen des Typs UnitConverter erstellten. Unmittelbar nachdem der new-Operator ein Objekt instanziiert, wird der Konstruktor des Objekts aufgerufen, um die Initialisierung durchzuführen. Ein Konstruktur wird wie eine Methode definiert, aber der Methodenname und der Rückgabetyp werden auf den Namen des einschließenden Typen reduziert:

Instanz-Member versus statische Member

Die Daten-Member und die Funktions-Member, die mit der Instanz des Typs arbeiten, werden als Instanz-Member bezeichnet. Die Methode Convert von UnitConverter und die Methode ToString von int sind Beispiele für solche Instanz-Member. Standardmäßig sind Member Instanz-Member.

Daten-Member und Funktions-Member, die nicht mit der Instanz des Typs arbeiten, sondern mit dem Typ selbst, müssen als static gekennzeichnet werden. Die Methoden Test.Main und Console.WriteLine sind statische Methoden. Die Klasse Console ist sogar eine statische Klasse, bei der alle Member statisch sind. Man erzeugt nie tatsächlich Instanzen von Console – eine einzige Konsole wird in der gesamten Anwendung verwendet.

Der Unterschied zwischen Instanz- und statischen Membern ist folgender: Das Instanz-Feld Name gehört zu einer Instanz eines bestimmten Panda, während Population zur Menge aller Panda-Instanzen gehört:

Der folgende Code erzeugt zwei Instanzen von Panda und gibt ihre Namen und dann die Gesamtpopulation aus:

Das Schlüsselwort public

Das Schlüsselwort public macht Member für andere Klassen zugänglich. Wenn in diesem Beispiel das Feld Name in Panda nicht als öffentlich markiert gewesen wäre, würde es sich um ein privates Feld handeln, und die Klasse Test hätte es nicht ansprechen können. Das »Öffentlichmachen« eines Members mit public lässt einen Typ sagen: »Das hier will ich andere Typen sehen lassen – alles andere sind meine privaten Implementierungsdetails.« In objektorientierten Begriffen sagen wir, dass die öffentlichen Member die privaten Member der Klasse kapseln.

Umwandlungen

C# kann Instanzen kompatibler Typen umwandeln. Eine Umwandlung erstellt immer einen neuen Wert für einen bestehenden Wert. Umwandlungen können entweder implizit oder explizit sein. Implizite Umwandlungen erfolgen automatisch, während explizite Umwandlungen einen Cast erfordern. Im folgenden Beispiel konvertieren wir implizit einen int in einen long (der doppelt so viel Kapazität an Bits wie ein int bietet) und casten explizit einen int auf einen short (der nur die halbe Bit-Kapazität eines int bietet):

In der Regel sind implizite Umwandlungen dann zulässig, wenn der Compiler garantieren kann, dass sie immer gelingen werden, ohne dass dabei Informationen verloren gehen. Andernfalls müssen Sie einen expliziten Cast nutzen, um die Umwandlung zwischen kompatiblen Typen durchzuführen.

Werttypen vs. Referenztypen

C#-Typen können in Werttypen und Referenztypen eingeteilt werden.

Werttypen enthalten die meisten eingebauten Typen (genauer gesagt, alle numerischen Typen sowie die Typen char und bool), aber auch selbst definierte struct- und enum-Typen. Referenztypen enthalten alle Klassen-, Array-, Delegate- und Interface-Typen.

Der prinzipielle Unterschied zwischen Werttypen und Referenztypen ist ihre Behandlung im Arbeitsspeicher.

Werttypen

Der Inhalt einer Werttyp-Variablen oder -Konstanten ist einfach ein Wert. So besteht zum Beispiel der Inhalt des eingebauten Werttyps int aus 32 Bit mit Daten.

Sie können einen selbst definierten Werttyp mithilfe des Schlüsselworts struct definieren (siehe Abbildung 1-1):

public

struct

Point { public int X, Y; }

Abbildung 1-1: Eine Werttypinstanz im Speicher

Das Zuweisen einer Werttyp-Instanz kopiert immer die Instanz:

Abbildung 1-2 zeigt, dass p1 und p2 unabhängig voneinander gespeichert werden.

Abbildung 1-2: Zuweisung kopiert eine Werttypinstanz

Referenztypen

Ein Referenztyp ist komplexer als ein Werttyp. Er besteht aus zwei Teilen: einem Objekt und der Referenz auf dieses Objekt. Der Inhalt einer Referenztyp-Variablen oder -Konstanten ist eine Referenz auf ein Objekt, das den Wert enthält. Hier ist der Typ Point aus unserem vorigen Beispiel als Klasse umgeschrieben worden (siehe Abbildung 1-3):

public

class

Point { public int X, Y; }

Abbildung 1-3: Ein Referenztyp im Speicher

Durch das Zuweisen einer Referenztyp-Variablen wird die Referenz kopiert, nicht die Objektinstanz. Damit ist es möglich, mit mehreren Variablen auf dasselbe Objekt zu verweisen – etwas, das mit Werttypen normalerweise nicht geht. Wenn wir das vorige Beispiel wiederholen, diesmal aber mit Point als Klasse, beeinflusst eine Operation auf p1 auch p2:

Abbildung 1-4 zeigt, dass p1 und p2 zwei Referenzen sind, die auf dasselbe Objekt verweisen.

Abbildung 1-4: Eine Zuweisung kopiert eine Referenz

Null

Einer Referenz kann das Literal null zugewiesen werden, wodurch ausgesagt wird, dass die Referenz auf kein Objekt zeigt – vorausgesetzt, Point ist eine Klasse:

Der Versuch, auf ein Member einer Null-Referenz zuzugreifen, führt zu einem Laufzeitfehler:

Console.WriteLine (p.X);   // NullReferenceException

Im Gegensatz dazu kann einem Werttyp auf normalem Weg kein Null-Wert zugewiesen werden:

C# bietet nullbare Typen an, mit denen Werttypen auch Null-Werte repräsentieren können (siehe den Abschnitt »Nullbare Typen« auf Seite 142).

Die Einteilung der vordefinierten Typen

Die vordefinierten Typen in C# sind folgende:

Werttypen

– Numerisch

– Ganzzahl mit Vorzeichen (sbyte, short, int, long)

– Ganzzahl ohne Vorzeichen (byte, ushort, uint, ulong)

– Reelle Zahl (float, double, decimal)

– Logisch (bool)

– Zeichen (char)

Referenztypen

– String (string)

– Objekt (object)

Die vordefinierten Typen in C# sind Aliase für Framework-Typen aus dem Namensraum System. Zwischen den beiden folgenden Anweisungen gibt es nur syntaktische Unterschiede:

Die vordefinierten Werttypen (mit Ausnahme von decimal) werden in der Common Language Runtime (CLR) als elementare Typen bezeichnet. Sie heißen so, weil sie im kompilierten Code direkt über Anweisungen unterstützt werden, die üblicherweise auf eine unmittelbare Unterstützung durch den zugrunde liegenden Prozessor zurückgehen.

Numerische Typen

C# bietet die folgenden vordefinierten numerischen Typen:

C#-Typ

Systemtyp

Suffix

Breite

Bereich

Ganzzahlig mit Vorzeichen

sbyte

SByte

 

8 Bit

–27 bis 27–1

short

Int16

 

16 Bit

–215 bis 215–1

int

Int32

 

32 Bit

–231 bis 231–1

long

Int64

L

64 Bit

–263 bis 263–1

Ganzzahlig ohne Vorzeichen

byte

Byte

 

8 Bit

0 bis 28–1

ushort

UInt16

 

16 Bit

0 bis 216–1

uint

UInt32

U

32 Bit

0 bis 232–1

ulong

UInt64

UL

64 Bit

0 bis 264–1

Reell

float

Single

F

32 Bit

± (~10–45 bis 1038)

double

Double

D

64 Bit

± (~10–324 bis 10308)

decimal

Decimal

M

128 Bit

± (~10–28 bis 1028)

Von den ganzzahligen Typen sind int und long Bürger erster Klasse und werden von C# und der Runtime bevorzugt. Die anderen ganzzahligen Typen werden üblicherweise im Dienste der Interoperabilität eingesetzt oder wenn eine effiziente Speicherplatznutzung wichtig ist.

Von den reellen Zahltypen werden float und double auch als Gleitkommatypen bezeichnet und üblicherweise für wissenschaftliche Berechnungen sowie im Grafikumfeld genutzt. Der Typ decimal wird üblicherweise für finanzmathematische Berechnungen verwendet, bei denen eine exakte Basis-10-Arithmetik und hohe Genauigkeit erforderlich sind. (Technisch betrachtet, ist decimal ebenfalls ein Gleitkommatyp, wird normalerweise aber nicht als solcher bezeichnet.)

Numerische Literale

Ganzzahlliterale können mit der Dezimal- oder der Hexadezimalnotation dargestellt werden; die Hexadezimalnotation wird mit dem Präfix 0x angezeigt (z. B. entspricht 0x7f dem Dezimalwert 127). Reelle Literale können die Dezimal- oder die Exponentialnotation nutzen, z. B. 1E06.

Typableitung bei numerischen Literalen

Standardmäßig geht der Compiler davon aus, dass ein numerisches Literal entweder einen double-Wert oder einen ganzzahligen Wert darstellt:

• Wenn das Literal einen Dezimaltrenner oder das Exponentialsymbol (E) enthält, ist der Typ double.

• Andernfalls ist der Typ des Literals der erste Typ aus der folgenen Liste, in den der Wert des Literals passt: int, uint, long und ulong.

Hier sehen Sie Beispiele dafür:

Console.Write (        1.0.GetType());  // Double

(double)

Console.Write (       1E06.GetType());  // Double

(double)

Console.Write (          1.GetType());  // Int32  

(int)

Console.Write ( 0xF0000000.GetType());  // UInt32

(uint)

Console.Write (0x100000000.GetType());  // Int64  

(long)

Numerische Suffixe

Die numerischen Suffixe, die in der vorangegangenen Tabelle aufgeführt sind, definieren den Typ eines Literals:

Die Suffixe U und L werden nur selten benötigt, weil die Typen uint, long und ulong fast immer erschlossen werden können oder intimplizit in sie umgewandelt werden kann.

Das Suffix D ist technisch gesehen redundant, da bei allen Literalen, die einen Dezimaltrenner enthalten, geschlossen wird, dass es double-Werte sind (und man einem numerischen Literal immer einen Dezimaltrenner anhängen kann). Die Suffixe F und M sind am nützlichsten und außerdem unumgänglich, wenn man float- oder decimal-Literale angeben will. Ohne Suffixe ließen sich die folgenden Anweisungen nicht kompilieren, da geschlossen würde, dass 4.5 den Typ double hat, der sich nicht implizit in float oder decimal umwandeln lässt.

Numerische Umwandlung

Ganzzahlige Umwandlungen

Ganzzahlige Umwandlungen sind implizit, wenn der Zieltyp jeden möglichen Wert des Ausgangstyps darstellen kann. Andernfalls ist eine explizite Umwandlung erforderlich, zum Beispiel so:

Reelle Umwandlungen

Ein float kann implizit in einen double umgewandelt werden, weil ein double jeden möglichen float-Wert darstellen kann. Die umgekehrte Umwandlung muss explizit sein.

Die Umwandlung zwischen decimal und den anderen reellen Typen muss explizit sein.

Reelle Typen in ganzzahlige Typen umwandeln

Die Umwandlung eines ganzzahligen Typs in einen reellen Typ ist immer implizit, während die umgekehrte Umwandlung immer explizit angegeben werden muss. Bei der Umwandlung eines Gleitkommatyps in einen ganzzahligen Typ wird immer der möglicherweise vorhandene Nachkommateil der Zahl abgeschnitten. Rundende Umwandlungen können Sie mit der statischen Klasse System.Convert durchführen.

Ein Nachteil der impliziten Umwandlung ist, dass das bei der Umwandlung großer ganzzahlige Werte in Gleitkommawerte zwar die Größenordnung erhalten bleibt, gelegentlich aber Genauigkeit verloren geht.

Arithmetische Operatoren

Die arithmetischen Operatoren (+, -, *, /, %) sind für alle numerischen Typen außer den 8- und 16-Bit-Ganzzahltypen definiert. Der %-Operator wird zum Rest nach der Division ausgewertet.

Inkrement- und Dekrementoperatoren

Die Inkrement- und Dekrement-Operatoren (++, --) erhöhen beziehungsweise verringern numerische Typen um eins. Der Operator kann entweder vor oder hinter der Variablen stehen – je nachdem, ob Sie die Variable vor oder nach dem Auswerten des Ausdrucks verändern wollen. Hier sehen Sie ein Beispiel:

Besondere integrale Operationen

Ganzzahlige Division

Divisionsoperationen mit ganzzahligen Operanden schneiden immer den Rest ab (runden auf null). Eine Division durch eine Variable, deren Wert null ist, führt zu einem Laufzeitfehler (eine DivideByZeroException). Eine Division durch das Literal 0 oder eine Konstante mit dem Wert 0 führt zu einem Kompiliationsfehler.

Ganzzahlüberlauf

Arithmetische Operationen auf Integer-Typen können zur Laufzeit zu einem Überlauf führen. Standardmäßig geschieht das still und leise – es wird keine Exception ausgelöst und das Ergebnis weist ein Umschlagverhalten auf, als ob die Berechnung mit einem größeren Ganzzahltypen durchgeführt und die zusätzlichen signifikanten Bits verworfen worden wären. Verringert man zum Beispiel den minimalen möglichen int-Wert um eins, erhält man den maximal möglichen int-Wert:

Die Operatoren checked und unchecked

Der Operator checked teilt der Laufzeitumgebung mit, eine OverflowException zu erzeugen, statt still und heimlich fehlzuschlagen, wenn ein ganzzahliger Ausdruck oder eine Anweisung die arithmetischen Grenzen dieses Typs überschreitet. Der Operator checked ist für Ausdrücke mit ++, -- (unär), +, -, *, / und expliziten Konvertierungsoperatoren zwischen ganzzahligen Typen nutzbar.

Sie können checked um einen Ausdruck oder einen Anweisungsblock verwenden, zum Beispiel so:

Sie können die Prüfung auf arithmetische Überläufe zum Standard für alle Ausdrücke in einem Programm machen, indem Sie sie mit dem Schalter /checked+ an der Befehlszeile kompilieren (in Visual Studio gehen Sie zu den Advanced Build Settings). Wenn Sie die Überlaufprüfung nur für bestimmte Ausdrücke oder Anweisungen abschalten wollen, können Sie das mit dem Operator unchecked erreichen.

Bitoperatoren

C# unterstützt die folgenden Bitoperatoren:

Operator

Bedeutung

Beispiel

Ergebnis

~

Komplement

~0xfU

0xfffffff0U

&

Und

0xf0 & 0x33

0x30

|

Oder

0xf0 | 0x33

0xf3

^

Exklusives Oder

0xff00 ^ 0x0ff0

0xf0f0

<< 

Verschiebung nach links

0x20 << 2

0x80

>> 

Verschiebung nach rechts

0x20 >> 1

0x10

8- und 16-Bit-Ganzzahlwerte

Die 8- und 16-Bit-Integer-Typen sind byte, sbyte, short und ushort. Sie haben keine eigenen arithmetischen Operatoren, daher konvertiert C# sie bei Bedarf implizit in größere Typen. Das kann zu einem Kompilierungsfehler führen, wenn man versucht, das Ergebnis wieder einem kleinen Integer-Typ zuzuweisen:

In diesem Fall werden x und y implizit nach int konvertiert, damit die Addition durchgeführt werden kann. Das bedeutet aber, dass das Ergebnis ebenfalls ein int ist, der nicht implizit in einen short