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

C# 10 – kurz & gut E-Book

Joseph Albahari

0,0

Beschreibung

Konkurrenzlos knapp, aber vollständig: Alle Sprachfeatures von C# 10

  • Schneller Einstieg in C# 10 für erfahrene Programmierer:innen
  • Das handliche Format macht das Buch zum idealen Begleiter für alle, die mit C# programmieren
  • Auch in der Neuauflage eine zuverlässige Referenz der renommierten Autoren Joseph und Ben Albahari

Ideal für vielbeschäftigte Programmierer:innen, die eine knappe und zugleich verständliche Beschreibung von C# 10 und LINQ suchen: C# 10 – kurz & gut gibt Ihnen genau das Wissen an die Hand, das Sie benötigen, um schnell effektiv mit C# 10 arbeiten zu können.
Wer schon mit Java, C++ oder einer früheren Version von C# vertraut ist, trifft mit C# 10 – kurz & gut die optimale Wahl: Kein anderes Buch und keine Online-Ressource bietet so viel Inhalt auf so wenig Raum. Konzentriert auf das Wesentliche und sehr praxisorientiert, behandelt dieses Taschenbuch mehr Themen als viele der großen C#-Kompendien. So deckt es auch fortgeschrittene Themen ab wie das Überladen von Operatoren, Typ-Constraints, Iteratoren, nullbare Typen, Closures und asynchrone Funktionen.
Diese aktualisierte 7. Auflage wurde um die wichtigsten neuen Features der C#-Version 10 ergänzt.

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

Android
iOS
von Legimi
zertifizierten E-Readern
Kindle™-E-Readern
(für ausgewählte Pakete)

Seitenzahl: 269

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.



Zu diesem Buch – sowie zu vielen weiteren O’Reilly-Büchern – können Sie auch das entsprechende E-Book im PDF-Format herunterladen. Werden Sie dazu einfach Mitglied bei oreilly.plus+:

www.oreilly.plus

7. AUFLAGE

C# 10

kurz & gut

Joseph Albahari & Ben Albahari

Deutsche Übersetzung vonThomas Demmig

Joseph Albahari, Ben Albahari

Lektorat: Alexandra Follenius

Übersetzung: Thomas Demmig

Copy-Editing: Sibylle Feldmann, www.richtiger-text.de

Satz: III-Satz, www.drei-satz.de

Herstellung: Stefanie Weidner

Umschlaggestaltung: Karen Montgomery, Michael Oréal, www.oreal.de

Bibliografische Information der Deutschen Nationalbibliothek

Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.

ISBN:

Print

978-3-96009-200-1

PDF

978-3-96010-683-8

ePub

978-3-96010-684-5

mobi

978-3-96010-685-2

7., aktualisierte Auflage 2022

Translation Copyright für die deutschsprachige Ausgabe © 2022 dpunkt.verlag GmbH

Wieblinger Weg 17

69123 Heidelberg

Authorized German translation of the English edition of C# 10 Pocket Reference, ISBN 9781098122041 © 2022 Joseph Albahari and 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.

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.

Hinweis:

Dieses Buch wurde auf PEFC-zertifiziertem Papier aus nachhaltiger Waldwirtschaft gedruckt. Der Umwelt zuliebe verzichten wir auf die Einschweißfolie.

Schreiben Sie uns:

Falls Sie Anregungen, Wünsche und Kommentare haben, lassen Sie es uns wissen: [email protected].

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. Alle Angaben und Programme in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autor noch Verlag noch Übersetzer können jedoch für Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buches stehen.

5 4 3 2 1 0

Inhalt

C# 10 – 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 Werttypen

Nullbare Referenztypen

Erweiterungsmethoden

Anonyme Typen

Tupel

Datensätze

Muster

LINQ

Die dynamische Bindung

Überladen von Operatoren

Attribute

Aufrufer-Info-Attribute

Asynchrone Funktionen

Unsicherer Code und Zeiger

Präprozessordirektiven

XML-Dokumentation

Index

C# 10 – kurz & gut

C# ist eine allgemein anwendbare, typsichere, vorwiegend 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# 10 ist darauf ausgerichtet, mit der .NET Core 6-Runtime von Microsoft zusammenzuarbeiten (während C# 9 für .NET 5, C# 8 für .NET Core 3 und C# 7 für .NET Core 2 sowie Microsoft .NET Framework 4.6/4.7/4.8 gedacht war).

Die Programme und Codefragmente in diesem Buch entsprechen denen aus den Kapiteln 2 und 4 von C# 10 in a Nutshell und sind alle als interaktive Beispiele in LINQPad verfügbar. Das Durcharbeiten der 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.

Um die Beispiele herunterzuladen, klicken Sie in LINQPad auf den Samples-Tab und wählen dort Download more samples. 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.

Unser Programm besteht aus zwei Anweisungen. Anweisungen werden in C# sequenziell ausgeführt und durch ein Semikolon abgeschlossen. Die erste Anweisung berechnet den Ausdruck12 * 30 und speichert das Ergebnis in einer Variablen namens x, deren Typ ein 32-Bit-Integer ist (int). Die zweite Anweisung ruft die MethodeWriteLine einer Klasse namens Console auf, die in einem NamensraumSystem definiert ist. Damit wird die Variable x in einem Textfenster auf dem Bildschirm ausgegeben.

Eine Methode führt eine Funktion aus – eine Klasse führt Funktions-Member und Daten-Member zusammen, um einen objektorientierten Baustein zu bilden. Die Klasse Console fasst Member zusammen, die sich um die Ein- und Ausgabe (Input/Output, I/O) an der Befehlszeile kümmern, wie zum Beispiel die Methode WriteLine. Eine Klasse ist die Art eines Typs, den wir im Abschnitt »Typgrundlagen« auf Seite 14 behandeln.

Auf der obersten Ebene sind Typen in Namensräumen organisiert. Viele häufig genutzte Typen – einschließlich der Klasse Console – befinden sich im Namensraum System. Die .NET-Bibliotheken sind in verschachtelten Namensräumen organisiert. So enthält beispielsweise der Namensraum System.Text Typen für den Umgang mit Text, und System.IO enthält Typen für die Ein- und Ausgabe.

Muss man die Klasse Console bei jeder Verwendung durch den Namensraum System qualifizieren, wird das schnell unübersichtlich. Die using-Direktive ermöglicht Ihnen, das Ganze besser lesbar zu machen, indem ein Namensraum importiert wird:

Eine einfache Form der Wiederverwendung von Code ist das Schreiben von High-Level-Funktionen, die wiederum Low-Level-Funktionen aufrufen. Wir können unser Programm mit einer wiederverwendbaren Methode namens FeetToInchesrefaktorieren, die eine Ganzzahl mit 12 multipliziert:

Unsere Methode enthält eine Folge von Anweisungen, die durch ein Paar geschweifter Klammern umschlossen sind. Das wird als Anweisungsblock bezeichnet.

Eine Methode kann Eingabedaten vom Aufrufer erhalten, indem sie Parameter spezifiziert, und Daten an den Aufrufer zurückgeben, indem sie einen Rückgabetyp festlegt. Unsere Methode FeetToInches besitzt einen Parameter für die Übergabe von Feet und einen Rückgabetyp für die Ausgabe der Inches:

int FeetToInches (int feet)

...

Die Literale30 und 100 sind die Argumente, die an die Methode FeetToInches übergeben werden.

Erhält eine Methode keine Eingabewerte, nutzen Sie ein leeres Klammernpaar. Gibt sie nichts zurück, verwenden Sie das Schlüsselwort void:

using System;

SayHello();

void SayHello()

{

Console.WriteLine ("Hello, world");

}

Methoden sind eine von vielen Arten von Funktionen in C#. Eine andere Art von Funktionen, die wir in unserem Beispielprogramm genutzt haben, war der Operator*, der eine Multiplikation durchführt. Es gibt zudem Konstruktoren, Eigenschaften, Events, Indexer und Finalizer.

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, wobei eine Konsolen- oder Windows-Anwendung einen Einsprungpunkt besitzt, während das bei einer Bibliothek nicht der Fall ist. Der Zweck einer Bibliothek ist es, von einer Anwendung oder anderen Bibliotheken aufgerufen (referenziert) zu werden. .NET selbst ist ein Satz von Bibliotheken (und eine Laufzeitumgebung).

Jedes der Programme im vorherigen Abschnitt begann direkt mit einer Folge von Anweisungen (auch als Anweisungen auf oberster Ebene bezeichnet). Gibt es solche Anweisungen, wird implizit ein Einstiegspunkt für eine Konsolen- oder Windows-Anwendung erzeugt. (Ohne Anweisungen auf oberster Ebene stellt eine Main-Methode den Einstiegspunkt dar – siehe den Abschnitt »Symmetrie vordefinierter und benutzerdefinierter Typen« auf Seite 16.)

Um den Compiler aufzurufen, können Sie entweder eine integrierte Entwicklungsumgebung (Integrated Development Environment, IDE) wie Visual Studio oder Visual Studio Code nutzen oder ihn selbst per Hand über die Befehlszeile aufrufen. Um eine Konsolenanwendung manuell mit .NET zu kompilieren, laden Sie zunächst das .NET 6 SDK herunter und erzeugen dann ein neues Projekt:

dotnet new console -o MyFirstProgram

cd MyFirstProgram

Das erstellt einen Ordner namens MyFirstProgram mit der C#-Datei Program.cs, die Sie dann bearbeiten können. Zum Aufruf des Compilers rufen Sie dotnet build (oder dotnet run) auf, wodurch das Programm ausgeführt und dann gestartet wird. Die Ausgabe wird in ein Unterverzeichnis unter bin\debug geschrieben. Dort finden sich MyFirstProgram.dll (die Ausgabe-Assembly) und MyFirstProgram.exe (was das kompilierte Programm direkt aufruft).

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 x Console WriteLine

Ein Bezeichner muss ein ganzes Wort sein und aus Unicode-Zeichen bestehen, wobei den Anfang ein Buchstabe oder der Unterstrich bildet. 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. Die Schlüsselwörter in unserem Beispielprogramm sind using und 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

as

base

bool

break

byte

case

catch

char

checked

class

const

continue

decimal

default

delegate

do

double

else

enum

event

explicit

extern

false

finally

fixed

float

for

foreach

goto

if

implicit

in

int

interface

internal

is

lock

long

namespace

new

null

object

operator

out

override

params

private

protected

public

readonly

record

ref

return

sbyte

sealed

short

sizeof

stackalloc

static

string

struct

switch

this

throw

true

try

typeof

uint

ulong

unchecked

unsafe

ushort

using

virtual

volatile

void

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 {...} // illegal

class @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. Das heißt, sie können – auch ohne ein vorangestelltes @-Zeichen – als Bezeichner eingesetzt werden, und zwar folgende:

add

alias

alias

and

async

await

by

descending

dynamic

equals

from

get

global

group

into

init

join

let

managed

nameof

nameof

nint

not

notnull

nuint

or

orderby

partial

remove

select

set

unmanaged

value

var

when

where

with

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. Ein Beispiel ist das Semikolon, das eine Anweisung beendet. 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 mithilfe von Symbolen angezeigt, beispielsweise dem Multiplikationsoperator *. Die Operatoren in unserem Programm sind folgende:

Ein Punkt zeigt ein Member von etwas an (oder, in numerischen Literalen, den Dezimaltrenner). Die Klammern werden genutzt, wenn eine Methode aufgerufen oder deklariert wird; leere Klammern werden verwendet, wenn eine Methode keine Argumente akzeptiert. Das Gleichheitszeichen führt eine Zuweisung aus (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 Ende der aktuellen Zeile, 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 248) 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), 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 in .NET Core 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 unser Programm damit begonnen, dass wir zwei Instanzen des Typs UnitConverter erstellten. Unmittelbar nachdem der new-Operator ein Objekt instanziiert hat, wird der Konstruktor des Objekts aufgerufen, um die Initialisierung durchzuführen. Ein Konstruktor 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, können als static gekennzeichnet werden. Um sich außerhalb eines Typs auf einen statischen Member von ihm zu beziehen, geben Sie statt einer Instanz seinen Typnamen an. Ein Beispiel ist die Methode WriteLine der Klasse Console. Da sie statisch ist, rufen wir Console.WriteLine() und nicht new Console().WriteLine() auf.

Der Unterschied zwischen Instanz- und statischen Membern ist dieser: Im folgenden Beispielcode gehört das Instanzfeld Name zu einer Instanz eines bestimmten Panda, während Population zur Menge aller Panda-Instanzen gehört. Wir erzeugen zwei Instanzen von Panda, geben ihre Namen und dann die gesamte Population aus:

Versuchen Sie, p1.Population oder Panda.Name auszuwerten, wird das zu einem Fehler beim Kompilieren führen.

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 könnte nicht von außerhalb der Klasse angesprochen werden. 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.

Einen Namensraum erstellen

Insbesondere bei größeren Programmen ist es sinnvoll, Typen in Namensräumen zu organisieren. So definieren Sie die Klasse Panda innerhalb eines Namensraums Animals:

namespace Animals

{

public class Panda

{

...

}

}

Wir behandeln Namensräume detaillierter im Abschnitt »Namensräume« auf Seite 71.

Eine Main-Methode definieren

Alle unsere Beispiele haben bisher Anweisungen auf oberster Ebene genutzt – ein Feature, das in C# 9 eingeführt wurde. Ohne solche Anweisungen sieht eine einfache Konsolen- oder Windows-Anwendung wie folgt aus:

Gibt es keine Anweisungen auf oberster Ebene, sucht C# nach einer statischen Methode namens Main, die dann zum Einstiegspunkt wird. Die Methode Main kann innerhalb einer beliebigen Klasse definiert sein (und es kann nur eine Methode Main geben). Muss Ihre Main-Methode auf private Member einer bestimmten Klasse zugreifen können, kann es einfacher sein, eine Main-Methode innerhalb dieser Klasse zu definieren, statt Anweisungen auf oberster Ebene zu verwenden.

Die Main-Methode kann optional einen Integer-Wert zurückgeben (statt void), um der ausführenden Umgebung einen Wert mitzuteilen (wobei ein Wert ungleich null normalerweise für einen Fehler steht). Sie kann optional auch ein Array mit Strings als Parameter übernehmen (das mit allen Argumenten gefüllt wird, die an die ausführbare Datei übergeben werden) – zum Beispiel:

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

Ein Array (wie string[]) steht für eine feste Zahl an Elementen eines bestimmten Typs. Arrays werden spezifiziert, indem man eckige Klammern hinter den Typ des Elements schreibt. Wir beschreiben sie im Abschnitt »Arrays« auf Seite 38.

(Die Methode Main kann auch async deklariert werden und einen Task oder Task<int> zurückgeben, um asynchrone Programmierung zu unterstützen – siehe den Abschnitt »Asynchrone Funktionen« auf Seite 229.)

Anweisungen auf oberster Ebene

Anweisungen auf oberster Ebene ermöglichen Ihnen (seit C# 9), den Aufwand mit einer statischen Main-Methode und einer Klasse, die diese Methode enthält, zu vermeiden. Eine Datei mit Anweisungen auf oberster Ebene besteht aus drei Abschnitten in dieser Reihenfolge:

1. (optional)

using

-Direktiven

2. eine Folge von Anweisungen, optional vermischt mit Methodendeklarationen

3. (optional) Typ- und Namensraumdeklarationen

Alles in Abschnitt 2 landet in einer vom Compiler generierten Main-Methode innerhalb einer ebenso generierten Klasse. Das heißt, dass die Methoden in Ihren Anweisungen auf oberster Ebene zu lokalen Methoden werden (deren Feinheiten wir im Abschnitt »Lokale Methoden« auf Seite 78 beschreiben). Anweisungen auf oberster Ebene können optional einen Integer-Wert an den Aufrufenden zurückgeben und auf eine »magische« Variable namens args vom Typ string[] zugreifen, in der die vom Aufrufenden übergebenen Befehlszeilenargumente enthalten sind.

Da ein Programm nur einen Einstiegspunkt haben kann, kann es in einem C#-Projekt auch nur höchstens eine Datei mit Anweisungen auf oberster Ebene geben.

Typen und 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):

public struct Point { public int X, Y; }

Abbildung 1: Eine Werttyp-Instanz im Speicher

Das Zuweisen einer Werttyp-Instanz kopiert immer die Instanz:

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

Abbildung 2: Eine Zuweisung kopiert eine Werttyp-Instanz.

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 3):

public class Point { public int X, Y; }

Abbildung 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 4 zeigt, dass p1 und p2 zwei Referenzen sind, die auf dasselbe Objekt verweisen.

Abbildung 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 Abschnitt »Nullbare Referenztypen« auf Seite 167 beschreiben wir ein Feature von C#, mit dem sich ungewollte NullReferenceException-Fehler verhindern lassen.

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 Werttypen« auf Seite 162).

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 .NET-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:

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. Die ganzzahligen Typen nint und nuint (eingeführt in C# 9) haben eine Größe, die zum Adressraum des Laufzeitprozesses passt. Sie können bei der Arbeit mit Zeiger-Arithmetik hilfreich sein. Wir beschreiben sie detailliert in Kapitel 4 von C# 10 in a Nutshell.

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 in der Regel 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-, Hexadezimal- oder Binärnotation dargestellt werden; die Hexadezimalnotation wird mit dem Präfix 0x angezeigt (z. B. entspricht 0x7f dem Dezimalwert 127), die Binärnotation mit dem Präfix 0b. Reelle Literale können die Dezimal- oder die Exponentialnotation nutzen, z. B. 1E06. Numerische Literale können für eine bessere Lesbarkeit durch Unterstriche ergänzt werden (zum Beispiel 1_000_000).

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 folgenden 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 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 Dekrementoperatoren (++, –) erhöhen bzw. 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

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 (einer DivideByZeroException). Eine Division durch das Literal 0 oder eine Konstante mit dem Wert 0 führt zu einem Kompiliationsfehler.

Ü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. Die Überlaufprüfung bringt ein wenig Performanceverlust mit sich.

Sie können checked um einen Ausdruck oder einen Anweisungsblock herum 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 :

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 shortkonvertiert werden kann (da es dabei zu Datenverlust kommen könnte). Damit sich diese Anweisung kompilieren lässt, müssen wir einen expliziten Cast hinzufügen:

Spezielle Float- und Double-Werte

Im Unterschied zu Integer-Typen haben Gleitkommatypen Werte, die bestimmte Operationen besonders behandeln. Diese speziellen Werte sind NaN (Not a Number, keine Zahl), +∞, –∞ und –0. Die Float- und Double-Klassen haben Konstanten für NaN, +∞ und –∞ und auch für andere Werte (MaxValue, MinValue und Epsilon). Hier sehen Sie ein Beispiel:

Console.Write (double.NegativeInfinity); // -Infinity

Die Division einer Zahl ungleich null durch null ergibt einen der Unendlich-Werte:

Console.WriteLine ( 1.0 / 0.0); // Infinity

Console.WriteLine (–1.0 / 0.0); // -Infinity

Console.WriteLine ( 1.0 / –0.0); // -Infinity

Console.WriteLine (–1.0 / –0.0); // Infinity

Null geteilt durch null oder unendlich minus unendlich ergibt NaN:

Console.Write ( 0.0 / 0.0); // NaN

Console.Write ((1.0 / 0.0) – (1.0 / 0.0)); // NaN

Verwenden Sie allerdings object.Equals, sind zwei NaN-Werte gleich:

double vs. decimal

Für wissenschaftliche Berechnungen (wie die Berechnung von Raumkoordinaten) eignet sich double. decimal ist für finanzmathematische Berechnungen geeignet und für Werte, die »menschengeschaffen« sind und nicht das Ergebnis echter Messungen. Hier sehen Sie eine Zusammenfassung der Unterschiede:

Eigenschaft

double

decimal

Interne Darstellung

Basis 2

Basis 10

Genauigkeit

15 bis 16 signifikante Ziffern

28 bis 29 signifikante Ziffern

Wertbereich

±(~10–324 bis ~10308)

±(~10–28 bis ~1028)

Besondere Werte

+0, –0, +∞, –∞ und NaN

keine

Geschwindigkeit

native CPU-Verarbeitung

keine native Verarbeitung auf der CPU (ungefähr 10-mal langsamer als double)

Rundungsfehler bei reellen Zahlen

float und double werden intern durch Zahlen zur Basis 2 repräsentiert. Aus diesem Grund werden die meisten Literale mit einem Nachkommateil (die zur Basis 10 sind) nicht genau abgebildet, was der Grund dafür ist, dass float und double für finanzmathematische Berechnungen nicht geeignet sind. Im Gegensatz dazu nutzt