0% fanden dieses Dokument nützlich (0 Abstimmungen)
659 Ansichten

C# Tutorial

Hochgeladen von

MannyCalavera
Copyright
© © All Rights Reserved
Wir nehmen die Rechte an Inhalten ernst. Wenn Sie vermuten, dass dies Ihr Inhalt ist, beanspruchen Sie ihn hier.
Verfügbare Formate
Als PDF, TXT herunterladen oder online auf Scribd lesen
0% fanden dieses Dokument nützlich (0 Abstimmungen)
659 Ansichten

C# Tutorial

Hochgeladen von

MannyCalavera
Copyright
© © All Rights Reserved
Wir nehmen die Rechte an Inhalten ernst. Wenn Sie vermuten, dass dies Ihr Inhalt ist, beanspruchen Sie ihn hier.
Verfügbare Formate
Als PDF, TXT herunterladen oder online auf Scribd lesen
Sie sind auf Seite 1/ 2543

Contents

C#-Dokumentation
Erste Schritte
Einführung
Typen
Programmbausteine
Wichtige Sprachbereiche
Tutorials
Auswählen der ersten Lektion
Browserbasierte Tutorials
Hello World
Zahlen in C#
Verzweigungen und Schleifen
Listensammlungen
Arbeiten in Ihrer lokalen Umgebung
Erstellen Ihrer Umgebung
Zahlen in C#
Verzweigungen und Schleifen
Listensammlungen
Grundlagen
Programmstruktur
Übersicht
Main-Methode
Top-Level-Anweisungen
Typsystem
Übersicht
Namespaces
Klassen
Datensätze
Schnittstellen
Generics
Anonyme Typen
Objektorientiertes Programmieren
Übersicht
erzwingen
Vererbung
Polymorphismus
Funktionale Techniken
Musterabgleich
Ausschuss
Dekonstruieren von Tupeln und anderen Typen
Ausnahmen und Fehler
Übersicht
Verwenden von Ausnahmen
Ausnahmebehandlung
Erstellen und Auslösen von Ausnahmen
Vom Compiler generierte Ausnahmen
Codierungsstil
Bezeichnernamen
Codekonventionen für C#
Tutorials
Anzeigen von Befehlszeilenargumenten
Einführung in Klassen
Objektorientiertes C#
Vererbung in C# und .NET
Konvertieren von Typen
Erstellen von datengesteuerten Algorithmen mit Musterabgleich
Behandeln einer Ausnahme mit try/catch
Ausführen von Bereinigungscode mit finally
Neues in C#
C# 10.0 (Vorschauversion 7)
C# 9.0
C# 8.0
C# 7.0–7.3
Wichtige Compileränderungen
C#-Versionsgeschichte
Beziehungen zur .NET-Bibliothek
Versionskompatibilität
Tutorials
Erkunden von Datensatztypen
Lesen von Top-Level-Anweisungen
Erkunden von Mustern in Objekten
Sicheres Aktualisieren von Schnittstellen mit Standardschnittstellenmethoden
Erstellen einer Mixin-Funktionalität mit Standardschnittstellenmethoden
Erkunden von Indizes und Bereichen
Arbeiten mit Verweistypen, die Nullwerte zulassen
Generieren und Nutzen asynchroner Streams
Tutorials
Erkunden von Zeichenfolgeninterpolation – interakt
Erkunden von Zeichenfolgeninterpolation – in Ihrer Umgebung
Erweiterte Szenarios für Zeichenfolgeninterpolation
Konsolenanwendung
REST-Client
Arbeiten mit LINQ
Verwenden von Attributen
C#-Konzepte
Nullwerte zulassende Verweistypen
Nullverweismigrationen
Auflösen von Nullable-Warnungen
Methoden
Eigenschaften
Indexer
Iterators
Delegaten und Ereignisse
Einführung in Delegaten
System.Delegate und das delegate-Schlüsselwort
Stark typisierte Delegate
Gängige Muster für Delegate
Einführung in Ereignisse
Standardereignismuster in .NET
Das aktualisierte .NET-Ereignismuster
Unterscheidung zwischen Delegaten und Ereignissen
Language-Integrated Query (LINQ)
Übersicht über LINQ
Grundlagen zu Abfrageausdrücken
LINQ in C#
Schreiben von LINQ-Abfragen in C#
Abfrage einer Auflistung von Objekten
Zurückgeben einer Abfrage aus einer Methode
Speichern der Ergebnisse einer Abfrage im Speicher
Gruppieren von Abfrageergebnissen
Erstellen einer geschachtelten Gruppe
Ausführen einer Unterabfrage für eine Gruppierungsoperation
Gruppieren von Ergebnissen nach zusammenhängenden Schlüsseln
Dynamisches Festlegen von Prädikatfiltern zur Laufzeit
Ausführen von inneren Verknüpfungen
Ausführen von Gruppenverknüpfungen
Ausführen von Left Outer Joins
Sortieren der Ergebnisse einer Join-Klausel
Verknüpfen mithilfe eines zusammengesetzten Schlüssels
Ausführen von benutzerdefinierten Verknüpfungsoperationen
Behandeln von NULL-Werten in Abfrageausdrücken
Behandeln von Ausnahmen in Abfrageausdrücken
Schreiben von sicherem und effizientem Code
Ausdrucksbaumstrukturen
Einführung in Ausdrucksbaumstrukturen
Ausdrucksbaumstrukturen mit Erläuterung
Framework-Typen, die Ausdrucksbaumstrukturen unterstützen
Ausführen von Ausdrücken
Interpretieren von Ausdrücken
Erstellen von Ausdrücken
Übersetzen von Ausdrücken
Zusammenfassung
Native Interoperabilität
Versionskontrolle
Artikel zu Vorgehensweisen in C#
Artikelindex
Aufteilen von Zeichenfolgen in Teilzeichenfolgen
Verketten von Zeichenfolgen
Suchzeichenfolgen
Ändern von Zeichenfolgeninhalten
Vergleichen von Zeichenfolgen
Abfangen einer Nicht-CLS-Ausnahme
Das .NET Compiler Platform SDK (Roslyn APIs)
Übersicht: das .NET Compiler Platform SDK (Roslyn APIs)
Grundlegendes zum API-Modell von Compilern
Arbeiten mit der Syntax
Arbeiten mit der Semantik
Arbeiten mit einem Arbeitsbereich
Untersuchen von Code mit der Syntaxschnellansicht
Quellgeneratoren
Schnellstarts
Syntaxanalyse
Semantikanalyse
Syntaxtransformation
Tutorials
Erstellen Ihres ersten Analysetools und Codefixes
C#-Programmierhandbuch
Übersicht
Übersicht
Programmierkonzepte
Übersicht
Asynchrone Programmierung
Übersicht
Asynchrone Programmierszenarios
Aufgabenbasiertes asynchrones Programmiermodell
Asynchrone Rückgabetypen
Abbrechen von Tasks
Abbrechen einer Aufgabenliste
Abbrechen asynchroner Tasks nach einem bestimmten Zeitraum
Verarbeiten asynchroner Aufgaben nach Abschluss
Asynchroner Dateizugriff
Attribute
Übersicht
Erstellen benutzerdefinierter Attribute
Zugriff auf Attribute mit Reflektion
Erstellen einer Union in C/C++ mit Attributen
Auflistungen
Kovarianz und Kontravarianz
Übersicht
Abweichungen bei generischen Schnittstellen
Erstellen von Varianten generischer Schnittstellen
Verwenden von Varianz in Schnittstellen für generische Sammlungen
Varianz bei Delegaten
Varianz bei Delegaten
Verwenden von Varianz für die generischen Delegaten Func und Action
Ausdrucksbaumstrukturen
Übersicht
Ausführen von Ausdrucksbaumstrukturen
Ändern von Ausdrucksbaumstrukturen
Verwenden von Ausdrucksbaumstrukturen zum Erstellen dynamischer Abfragen
Debuggen von Ausdrucksbäumen in Visual Studio
DebugView-Syntax
Iterators
Language-Integrated Query (LINQ)
Übersicht
Erste Schritte mit LINQ in C#
Einführung in LINQ-Abfragen
LINQ und generische Typen
Grundlegende LINQ-Abfragevorgänge
Datentransformationen mit LINQ
Typbeziehungen in LINQ-Abfragevorgängen
Abfragesyntax und Methodensyntax in LINQ
C#-Funktionen mit LINQ-Unterstützung
Exemplarische Vorgehensweise: Schreiben von Abfragen in C# (LINQ)
Übersicht über Standardabfrageoperatoren
Übersicht
Abfrageausdruckssyntax für Standardabfrageoperatoren
Klassifizierung von Standardabfrageoperatoren nach Ausführungsart
Sortieren von Daten
Mengenvorgänge
Filtern von Daten
Quantifizierervorgänge
Projektionsvorgänge
Partitionieren von Daten
Verknüpfungsvorgänge
Gruppieren von Daten
Generierungsvorgänge
Gleichheitsvorgänge
Elementvorgänge
Konvertieren von Datentypen
Verkettungsvorgänge
Aggregationsvorgänge
LINQ to Objects
Übersicht
LINQ und Zeichenfolgen
Artikeln zu Vorgehensweisen
Zählen der Vorkommen eines Worts in einer Zeichenfolge (LINQ)
Abfragen von Sätzen, die eine angegebene Gruppe von Wörtern (LINQ)
enthalten
Abfragen von Zeichen in einer Zeichenfolge (LINQ)
Kombinieren von LINQ-Abfragen mit regulären Ausdrücken
Suchen der festgelegten Differenz zwischen zwei Listen (LINQ)
Sortieren oder Filtern von Textdaten nach einem beliebigen Wort oder Feld
(LINQ)
Neuordnen der Felder einer Datei mit Trennzeichen (LINQ)
Verbinden und Vergleichen von Zeichenfolgenauflistungen (LINQ)
Füllen von Objektsammlungen aus mehreren Quellen (LINQ)
Teilen einer Datei in mehrere Dateien durch das Verwenden von Gruppen
(LINQ)
Verknüpfen des Inhalts unterschiedlicher Dateien (LINQ)
Berechnen von Spaltenwerten in einer CSV-Textdatei (LINQ)
LINQ und Reflection
Abfragen der Metadaten einer Assembly mit Reflektion (LINQ)
LINQ und Dateiverzeichnisse
Übersicht
Abfragen von Dateien mit einem angegebenen Attribut oder Namen
Gruppieren von Dateien nach Erweiterung (LINQ)
Abfragen der Gesamtzahl an Bytes in mehreren Ordnern (LINQ)
Vergleichen des Inhalts von zwei Ordnern (LINQ)
Abfragen der größten Datei oder der größten Dateien in einer
Verzeichnisstruktur (LINQ)
Abfragen von Dateiduplikaten in einer Verzeichnisstruktur (LINQ)
Abfragen des Inhalts von Dateien in einem Ordner (LINQ)
Abfragen von ArrayList mit LINQ
Hinzufügen von benutzerdefinierten Methoden zu LINQ-Abfragen
LINQ to ADO.NET (Portalseite)
Aktivieren einer Datenquelle für LINQ-Abfragen
Visual Studio-IDE und Toolunterstützung für LINQ
Spiegelung
Serialisierung (C#)
Übersicht
Schreiben von Objektdaten in eine XML-Datei
Lesen von Objektdaten aus einer XML-Datei
Exemplarische Vorgehensweise: Beibehalten eines Objekts in Visual Studio
Anweisungen, Ausdrücke und Operatoren
Übersicht
Anweisungen
Ausdruckskörpermember
Gleichheit und Gleichheitsvergleiche
Übereinstimmungsvergleiche
Definieren von Wertgleichheit für einen Typ
Überprüfen auf Verweisgleichheit (Identität)
Typen
Umwandlung und Typkonvertierungen
Boxing und Unboxing
Konvertieren eines Bytearrays in einen ganzzahligen Typ
Konvertieren einer Zeichenfolge in eine Zahl
Konvertieren zwischen Hexadezimalzeichenfolgen und numerischen Typen
Verwenden von dynamischen Typen
Exemplarische Vorgehensweise: Erstellen und Verwenden von dynamischen
Objekten (C# und Visual Basic)
Klassen, Strukturen und Datensätze
Polymorphie
Versionsverwaltung mit den Schlüsselwörtern "override" und "new"
Wann müssen die Schlüsselwörter "override" und "new" verwendet werden?
Überschreiben der ToString-Methode
Member
Member (Übersicht)
Abstrakte und versiegelte Klassen und Klassenmember
Statische Klassen und statische Klassenmember
Zugriffsmodifizierer
Felder
Konstanten
Definieren von abstrakten Eigenschaften
Definieren von Konstanten in C#
Eigenschaften
Übersicht über Eigenschaften
Verwenden von Eigenschaften
Schnittstelleneigenschaften
Einschränken des Zugriffsmethodenzugriffs
Deklarieren und Verwenden von Lese-/Schreibeigenschaften
Automatisch implementierte Eigenschaften
Implementieren einer einfachen Klasse mit automatisch implementierten
Eigenschaften
Methoden
Methoden (Übersicht)
Lokale Funktionen
Ref-Rückgaben und lokale ref-Variablen
Parameter
Übergeben von Parametern
Übergeben von Werttypparametern
Übergeben von Verweistypparametern
Unterschiede zwischen dem Übergeben einer Struktur und dem Übergeben
eines Klassenverweises an eine Methode
Implizit typisierte lokale Variablen
Verwenden von implizit typisierten lokalen Variablen und Arrays in einem
Abfrageausdruck
Erweiterungsmethoden
Implementieren und Aufrufen einer benutzerdefinierten Erweiterungsmethode
Erstellen einer neuen Methode für eine Enumeration
Benannte und optionale Argumente
Verwenden von benannten und optionalen Argumenten in der Office-
Programmierung
Konstruktoren
Konstruktoren (Übersicht)
Verwenden von Konstruktoren
Instanzenkonstruktoren
Private Konstruktoren
Statische Konstruktoren
Schreiben eines Kopierkonstruktors
Finalizer
Objekt- und Auflistungsinitialisierer
Initialisieren von Objekten mithilfe eines Objektinitialisierers
Initialisieren eines Wörterbuchs mit einem Sammlungsinitialisierer
Geschachtelte Typen
Partielle Klassen und Methoden
Zurückgeben von Teilmengen von Elementeigenschaften in einer Abfrage
Schnittstellen
Explizite Schnittstellenimplementierung
Explizites Implementieren von Schnittstellenmembern
Explizites Implementieren von Membern zweier Schnittstellen
Delegaten
Übersicht
Verwenden von Delegaten
Delegaten mit benannten im Vergleich zu Anonyme Methoden
Kombinieren von Delegaten (Multicastdelegaten) (C#-Programmierhandbuch)
Deklarieren, Instanziieren und Verwenden von Delegaten
Arrays
Übersicht
Eindimensionale Arrays
Mehrdimensionale Arrays
Verzweigte Arrays
Verwenden von „foreach“ mit Arrays
Übergeben von Arrays als Argumente
Implizit typisierte Arrays
Zeichenfolgen
Programmieren mit Zeichenfolgen
Bestimmen, ob eine Zeichenfolge einen numerischen Wert darstellt
Indexer
Übersicht
Verwenden von Indexern
Indexer in Schnittstellen
Vergleich zwischen Eigenschaften und Indexern
Ereignisse
Übersicht
Abonnieren von Ereignissen und Kündigen von Ereignisabonnements
Veröffentlichen von Ereignissen, die den .NET-Richtlinien entsprechen
Auslösen von Basisklassenereignissen in abgeleiteten Klassen
Implementieren von Schnittstellenereignissen
Implementieren benutzerdefinierter Ereignisaccessoren
Generics
Generische Typparameter
Einschränkungen für Typparameter
Generische Klassen
Generische Schnittstellen
Generische Methoden
Generics und Arrays
Generische Delegate
Unterschiede zwischen C++-Vorlagen und C#-Generics
Generics zur Laufzeit
Generics und Reflexion
Generische Typen und Attribute
Das Dateisystem und die Registrierung
Übersicht
Durchlaufen einer Verzeichnisstruktur
Abrufen von Informationen über Dateien, Ordner und Laufwerke
Erstellen einer Datei oder eines Ordners
Kopieren, Löschen und Verschieben von Dateien und Ordnern
Bereitstellen eines Statusdialogfelds für Dateioperationen
Schreiben in eine Textdatei
Lesen aus einer Textdatei
Zeilenweises Lesen einer Textdatei
Erstellen eines Schlüssels in der Registrierung
Interoperabilität
.NET-Interoperabilität
Überblick über die Interoperabilität
Zugreifen auf Office-Interop-Objekte mithilfe von Visual C#-Funktionen
Indizierte Eigenschaften bei der COM-Interop-Programmierung
Verwenden eines Plattformaufrufs zum Wiedergeben einer Wavedatei
Exemplarische Vorgehensweise: Office-Programmierung (C# und Visual Basic)
COM-Beispielklasse
Sprachreferenz
Übersicht
Angeben der Sprachversion
Typen
Werttypen
Übersicht
Integrale numerische Typen
Native ganzzahlige nint- und nuint-Typen
Numerische Gleitkommatypen
Integrierte numerischer Konvertierungen
bool
char
Enumerationstypen
Strukturtypen
Tupeltypen
Auf NULL festlegbare Werttypen
Verweistypen
Funktionen von Verweistypen
Integrierte Referenztypen
Datensatz (record)
class
interface
Nullwerte zulassende Verweistypen
void
var
Integrierte Typen
Nicht verwaltete Typen
Standardwerte
Keywords
Übersicht
Modifizierer
Zugriffsmodifizierer
Kurzreferenz
Zugriffsebenen
Zugriffsdomäne
Einschränkungen bei der Verwendung von Zugriffsebenen
internal
private
protected
public
protected internal
private protected
abstract
async
const
event
extern
in (generischer Modifizierer)
new (Membermodifizierer)
out (generischer Modifizierer)
override
readonly
sealed
static
unsafe
virtual
volatile
Anweisungsschlüsselwörter
Anweisungskategorien
Sprunganweisungen
break
continue
goto
return
Ausnahmebehandlungsanweisungen
throw
try-catch
try-finally
try-catch-finally
Checked und Unchecked
Übersicht
checked
unchecked
fixed-Anweisung
lock-Anweisung
Methodenparameter
Übergeben von Parametern
params
Modifizierer für in-Parameter
ref
Modifizierer für out-Parameter
Namespaceschlüsselwörter
namespace
using
Kontexte für using
using-Anweisung
using-Anweisung
extern-Alias
Generische Schlüsselwörter für die Typeinschränkung
new-Einschränkung
where
Zugriffsschlüsselwörter
base
this
Literalschlüsselwörter
NULL
„true” und „false”.
default
Kontextabhängige Schlüsselwörter
Kurzreferenz
add
get
init
partial (Typ)
partial (Methode)
remove
set
when (Filterbedingung)
value
yield
Abfrageschlüsselwörter
Kurzreferenz
from-Klausel
where-Klausel
select-Klausel
group-Klausel
into
orderby-Klausel
join-Klausel
let-Klausel
ascending
descending
on
equals
by
in
Operatoren und Ausdrücke
Übersicht
Arithmetische operatoren
Logische boolesche Operatoren
Bitweise und Schiebeoperatoren
Gleichheitsoperatoren
Vergleichsoperatoren
Operatoren und Ausdrücke für den Memberzugriff
Cast-Ausdrücke und Operatoren für Typtests
Benutzerdefinierte Konvertierungsoperatoren
Auf Zeiger bezogene Operatoren
Zuweisungsoperatoren
Lambdaausdrücke
Muster
+ +=-Operatoren
- -=-Operatoren
?:-Operator
! NULL-toleranter Operator
?? und ??=-Operatoren
=>-Operator
::-Operator
Await-Operator
Ausdrücke mit Standardwert
delegate-Operator
is-Operator
nameof-Ausdruck
new-Operator
sizeof-Operator
stackalloc-Ausdruck
switch-Ausdruck
true- und false-Operatoren
with-Ausdruck
Überladen von Operatoren
Anweisungen
Iterationsanweisungen
Auswahlanweisungen
Sonderzeichen
Übersicht
$ -- Zeichenfolgeninterpolation
@ -- ausführlicher Bezeichner
Vom Compiler gelesene Attribute
Globale Attribute
Aufruferinformationen
Statische Analysen, die NULL-Werte zulassen
Sonstiges
Unsicherer Code und Zeiger
Präprozessordirektiven
Compileroptionen
Übersicht
Sprachoptionen
Ausgabeoptionen
Eingabeoptionen
Fehler und Warnungsoptionen
Optionen für die Codegenerierung
Sicherheitsoptionen
Ressourcenoptionen
Sonstige Optionen
Erweiterte Optionen
XML-Dokumentationskommentare
Dokumentation zum Generieren von APIs
Recommended tags (Empfohlene Tags)
Beispiele
Compilermeldungen
Spezifikationen
C# 6.0 draft specification (C# 6.0 - Entwurfsspezifikationen)
Einführung
Lexikalische Struktur
Grundlegende Konzepte
Typen
Variablen
Konvertierungen
Ausdrücke
Anweisungen
Namespaces
Klassen
Strukturen
Arrays
Schnittstellen
Enumerationen
Delegaten
Ausnahmen
Attribute
Unsicherer Code
Dokumentationskommentare
C# 7.0- bis 10.0-Features
C# 7.0-Features
Musterabgleich
Lokale Funktionen
out-Variablendeklaration
Throw-Ausdrücke
Binäre Literale
Zahlentrennzeichen
Asynchrone Tasktypen
C# 7.1-Features
Async Main-Methode
Standardausdrücke
Ableiten von Tupelnamen
Musterabgleich mit Generics
C# 7.2-Features
Schreibgeschützte Verweise
Sicherheit zur Kompilierzeit für ref-ähnliche Typen
Nicht schließende benannte Argumente
Privat geschützt
Bedingter ref-Ausdruck
Trennzeichen für vorangestellte Ziffern
C# 7.3-Features
Nicht verwaltete generische Typeneinschränkungen
Für `fixed`-Felder zur Indizierung ist kein Anheften erforderlich unabhängig vom
Kontext „verschiebbar/nicht verschiebbar“
Musterbasierte `fixed`-Anweisung
Lokale ref-Neuzuweisung
Stackalloc-Arrayinitialisierer
Auf Felder ausgerichtete Attribute für automatisch implementierte Eigenschaft
Ausdrucksvariablen in Initialisierern
Tupelgleichheit (==) und -ungleichheit (! =)
Verbesserte Überladungskandidaten
C# 8.0-Features
Nullable-Verweistypen – Vorschlag
Rekursiver Musterabgleich
Standardschnittstellenmethoden
Asynchrone Datenströme
Bereiche
Musterbasierte Verwendung und using-Deklarationen
Statische lokale Funktionen
NULL-Sammelzuweisung
Schreibgeschützte Instanzmember
stackalloc bei Verschachtelungen
C# 9.0-Features
Datensätze
Top-Level-Anweisungen
Nullable-Verweistypen – Spezifikation
Verbesserungen am Musterabgleich:
init-only-Setter
Zieltypisierte neue Ausdrücke
Modulinitialisierer
Erweitern von partiellen Methoden
Statische anonyme Funktionen
Bedingter Ausdruck mit Zieltyp
Kovariante Rückgabetypen
Erweiterung GetEnumerator in foreach-Schleifen
Parameter zum Verwerfen von Lambdafunktion
Attribute in lokalen Funktionen
Integerwerte mit nativer Größe
Funktionszeiger
Unterdrücken der Ausgabe des Flags „localsinit“
Nicht eingeschränkte Typparameteranmerkungen
C# 10.0-Features
Datensatzstrukturen
Parameterlose Strukturkonstruktoren
Globale using-Anweisung
Dateibereichsnamespaces
Muster für erweiterte Eigenschaften
Verbesserte interpolierte Zeichenfolgen
Konstante interpolierte Zeichenfolgen
Lambdaverbesserungen
Aufruferargumentausdruck
Erweiterte #line-Direktiven
Generische Attribute
Verbesserte Analyse der definitiven Zuweisung
AsyncMethodBuilder-Überschreibung
Überblick über C#
04.11.2021 • 13 minutes to read

C# (Aussprache „C Sharp“) ist eine moderne, objektorientierte und typsichere Programmiersprache. C#


ermöglicht Entwicklern das Erstellen zahlreicher sicherer und robuster Anwendungen, die in .NET ausgeführt
werden. C# hat seine Wurzeln in der C-Sprachenfamilie und ist Programmierern, die mit C, C++, Java und
JavaScript arbeiten, sofort vertraut. Diese Einführung bietet einen Überblick über die wichtigsten Komponenten
der Sprache in C# 8 und früheren Versionen. Wenn Sie die Sprache anhand von interaktiven Beispielen
kennenlernen möchten, arbeiten Sie die Tutorials auf der Seite Einführung in C# durch.
C# ist eine objektorientierte, * komponentenorientier te _ Programmiersprache. C# bietet Sprachkonstrukte
zur direkten Unterstützung dieser Konzepte, was C# zu einer natürlichen Sprache macht, in der
Softwarekomponenten erstellt und verwendet werden. Seit Veröffentlichung wurden C# Features hinzugefügt,
um neue Workloads und Methoden zur Gestaltung von Software zu unterstützen. Im Wesentlichen ist C# eine _
objektorientierte* Programmiersprache. Sie definieren Typen und deren Verhalten.
Mehrere C#-Features helfen bei der Erstellung stabiler und dauerhafter Anwendungen. Die *Garbage
Collection _ gibt Arbeitsspeicher automatisch frei, der von unerreichbaren nicht verwendeten Objekten belegt
wird. Nullable-Typen bieten Schutz vor Variablen, die nicht auf zugeordnete Objekte verweisen. Die
Ausnahmebehandlung bietet einen strukturierten und erweiterbaren Ansatz zur Fehlererkennung und
Wiederherstellung. Lambdaausdrücke unterstützen funktionale Programmiertechniken. Die Language
Integrated Query-Syntax (LINQ) erstellt ein gängiges Muster für das Arbeiten mit Daten aus einer beliebigen
Quelle. Dank Sprachunterstützung für asynchrone Vorgänge wird eine Syntax für den Aufbau verteilter Systeme
bereitgestellt. C# bietet ein _ *einheitliches Typsystem**. Alle C#-Typen, einschließlich primitiver Typen wie int
und double , erben von einem einzelnen object -Stammtyp. Allen Typen teilen sich eine Reihe allgemeiner
Vorgänge. Werte jeglicher Art können einheitlich gespeichert, transportiert und bearbeitet werden. Darüber
hinaus unterstützt C# benutzerdefinierte Verweis- und Werttypen. C# ermöglicht die dynamische Zuteilung von
Objekten und die Inlinespeicherung schlanker Strukturen. C# unterstützt generische Methoden und Typen, die
eine bessere Typsicherheit und Leistung bieten. C# stellt Iteratoren bereit, mit denen Implementierer von
Auflistungsklassen benutzerdefinierte Verhaltensweisen für Clientcode definieren können.
In C# dient die Versionsver waltung zum Sicherstellen, dass sich Programme und Bibliotheken im Laufe der
Zeit kompatibel weiterentwickeln können. Zu den Aspekten der Entwicklung von C#, die direkt von
Überlegungen bei der Versionskontrolle beeinflusst wurden, gehören die separaten virtual - und override -
Modifizierer, die Regeln für die Überladungsauflösung und die Unterstützung für explizite
Schnittstellenmember-Deklarationen.

.NET-Architektur
C#-Programme werden auf Grundlage von .NET ausgeführt, ein virtuelles Ausführungssystem namens
Common Language Runtime (CLR) sowie Klassenbibliotheken. Die CLR ist die Implementierung der Common
Language Infrastructure (CLI) von Microsoft, ein internationaler Standard. Die CLI ist die Grundlage für das
Erstellen von Ausführungs- und Entwicklungsumgebungen, in denen Sprachen und Bibliotheken nahtlos
zusammenarbeiten.
Der in C# geschriebene Quellcode wird in eine Zwischensprache kompiliert, die konform mit der CLI-
Spezifikation ist. Der IL-Code wird zusammen mit Ressourcen wie z. B. Bitmaps und Zeichenfolgen in einer
Assembly gespeichert, die normalerweise die Erweiterung .dll aufweist. Eine Assembly enthält ein Manifest, das
Informationen über die Typen, die Version und die Kultur der Assembly bereitstellt.
Wenn das C#-Programm ausgeführt wird, wird die Assembly in die CLR geladen. Die CLR konvertiert den IL-
Code mithilfe der JIT-Kompilierung (Just-In-Time) in native Computeranweisungen. Die CLR stellt weitere
Dienste zur automatischen Garbage Collection, Ausnahmebehandlung und Ressourcenverwaltung bereit. Code,
der von der CLR ausgeführt wird, wird manchmal als „verwalteter Code“ bezeichnet. „Nicht verwalteter Code“
wird in native Computersprache kompiliert, die auf eine bestimmte Plattform zielt.
Eines der wichtigsten Features in .NET ist die Sprachinteroperabilität. Der vom C#-Compiler erzeugte IL-Code
entspricht dem allgemeinen Typsystem (CTS, Common Type Specification). Der über C# generierte IL-Code kann
mit Code interagieren, der über die .NET-Versionen von F#, Visual Basic oder C++ generiert wurde. Es gibt mehr
als 20 weitere CTS-kompatible Sprachen. Eine einzelne Assembly kann mehrere Module enthalten, die in
verschiedenen .NET-Sprachen geschrieben wurden. Die Typen können aufeinander verweisen, als wären sie in
der gleichen Sprache geschrieben.
Zusätzlich zu den Laufzeitdiensten enthält .NET auch umfangreiche Bibliotheken. Diese Bibliotheken
unterstützen viele verschieden Workloads. Sie sind in Namespaces organisiert, die eine große Bandbreite
nützlicher Funktionen bereitstellen. Die Bibliotheken beinhalten alles von der Dateiein- und -ausgabe über die
Bearbeitung von Zeichenfolgen, XML-Analyse und Webanwendungs-Frameworks bis hin zu Windows Forms-
Steuerelementen. Eine typische C#-Anwendung verwendet für die Ausführung von Routinevorgängen ausgiebig
die .NET-Klassenbibliothek.
Weitere Informationen zu .NET finden Sie in der Übersicht über .NET.

Hello World
Das Programm „Hello, World“ wird für gewöhnlich zur Einführung einer Programmiersprache verwendet. Hier
ist es in C#:

using System;

class Hello
{
static void Main()
{
Console.WriteLine("Hello, World");
}
}

Das Programm „Hello, World“ wird mit einer using -Richtlinie gestartet, die auf den System -Namespace
verweist. Namespaces bieten eine hierarchische Möglichkeit zum Organisieren von C#-Programmen und -
Bibliotheken. Namespaces enthalten Typen und andere Namespaces. Beispiel: Der System -Namespace enthält
eine Reihe von Typen, wie etwa die Console -Klasse, auf die im Programm verwiesen wird, und eine Reihe
anderer Namespaces, wie etwa IO und Collections . Eine using -Richtlinie, die auf einen bestimmten
Namespace verweist, ermöglicht die nicht qualifizierte Nutzung der Typen, die Member dieses Namespace sind.
Aufgrund der using -Direktive kann das Programm Console.WriteLine als Abkürzung für
System.Console.WriteLine verwenden.

Die Hello-Klasse, die vom Programm „Hello, World“ deklariert wird, verfügt über einen einzelnen Member: die
Main -Methode. Die Main -Methode wird mit dem Modifizierer static deklariert. Auch wenn Instanzmethoden
mit dem Schlüsselwort this auf eine bestimmte einschließende Objektinstanz verweisen können, agieren
statische Methoden ohne Verweis auf ein bestimmtes Objekt. Gemäß Konvention fungiert eine statische
Methode mit der Bezeichnung Main als Einstiegspunkt eines C#-Programms.
Die Ausgabe des Programms wird anhand der WriteLine -Methode der Console -Klasse im System -Namespace
generiert. Diese Klasse wird anhand der Standardklassenbibliotheken bereitgestellt, auf die standardmäßig
automatisch vom Compiler verwiesen wird.
Typen und Variablen
Ein Typ definiert die Struktur und das Verhalten von allen Daten in C#. Die Deklaration eines Typs kann seine
Member, seinen Basistyp, die Schnittstellen, die er implementiert, und die für diesen Typ zulässigen Operationen
enthalten. Eine Variable ist eine Bezeichnung, die auf einen bestimmten Instanzentyp verweist.
Es gibt zwei Arten von Typen in C#: Werttypen und Verweistypen. Variablen von Werttypen enthalten ihre
tatsächlichen Daten. Variablen von Verweistypen speichern hingegen Verweise auf ihre Daten – letztere werden
als Objekte bezeichnet. Dank Verweistypen können zwei Variablen auf das gleiche Objekt verweisen. So können
auf eine Variable angewendete Vorgänge das Objekt beeinflussen, auf das die andere Variable verweist. Bei
Werttypen besitzen die Variablen jeweils eigene Kopien der Daten. Auf eine Variable angewendete Vorgänge
können sich nicht auf die andere Variable auswirken (außer im Fall der Parametervariablen ref und out ).
Ein Bezeichner ist ein Variablenname. Ein Bezeichner ist eine Sequenz von Unicode-Zeichen ohne Leerzeichen.
Ein Bezeichner kann ein in C# reserviertes Wort sein, wenn ihm das Präfix @ vorangestellt ist. Die Verwendung
eines reservierten Worts als Bezeichner kann nützlich sein, wenn Sie mit anderen Sprachen interagieren.
C#-Werttypen sind weiter unterteilt in einfache Typen, Enumerationstypen, Strukturtypen, Nullable-Werttypen
und Tuple-Werttypen. C#-Verweistypen sind weiter unterteilt in Klassentypen, Schnittstellentypen, Arraytypen
und Delegattypen.
Im Folgenden finden Sie eine Übersicht des C#-Typsystems.
Werttypen
Einfache Typen
Integral mit Vorzeichen: sbyte , short , int , long
Integral ohne Vorzeichen: byte , ushort , uint , ulong
Unicode-Zeichen: char , das ein Zeichen als UTF-16-Codeeinheit darstellt
Binäres Gleitkomma (IEEE): float , double
Dezimale Gleitkommazahl mit hoher Genauigkeit: decimal
Boolesch: bool dient zur Darstellung boolescher Werte, die entweder true oder false sind
Enumerationstypen
Benutzerdefinierte Typen der Form enum E {...} . Ein enum -Typ ist ein eigenständiger Typ mit
einer benannten Konstante. Jeder enum -Typ verfügt über einen zugrunde liegenden Typ, der
einer der acht ganzzahligen Typen sein muss. Der Satz von Werten eines enum -Typs ist mit
dem Satz von Werten des zugrunde liegenden Typs identisch.
Strukturtypen
Benutzerdefinierte Typen der Form struct S {...}
Auf NULL festlegbare Werttypen
Erweiterungen aller anderen Werttypen mit einem null -Wert
Tupelwerttypen
Benutzerdefinierte Typen der Form (T1, T2, ...)
Verweistypen
Klassentypen
Ultimative Basisklasse aller anderen Typen: object
Unicode-Zeichenfolgen: string , die eine Sequenz von UTF-16-Codeeinheiten darstellt
Benutzerdefinierte Typen der Form class C {...}
Schnittstellentypen
Benutzerdefinierte Typen der Form interface I {...}
Array types (Arraytypen)
Eindimensionale Arrays, mehrdimensionale Arrays und Jagged Arrays, beispielsweise int[] ,
int[,] und int[][]
Delegattypen
Benutzerdefinierte Typen der Form delegate int D(...)

C#-Programme verwenden Typdeklarationen, um neue Typen zu erstellen. Eine Typdeklaration gibt den Namen
und die Member des neuen Typs an. Sechs Typkategorien von C# können von Benutzern definiert werden:
Klassentypen, Strukturtypen, Schnittstellentypen, Enumerationstypen, Delegattypen und Tuple-Werttypen. Sie
können außerdem record -Typen deklarieren, entweder record struct oder record class . Datensatztypen
verfügen über vom Compiler synthetisierte Member. Sie verwenden Datensätze in erster Linie zum Speichern
von Werten mit minimalem zugeordneten Verhalten.
Ein class -Typ definiert eine Datenstruktur, die Datenmember (Felder) und Funktionsmember (Methoden,
Eigenschaften usw.) enthält. Klassentypen unterstützen einzelne Vererbung und Polymorphie. Dies sind
Mechanismen, durch die abgeleitete Klassen erweitert und Basisklassen spezialisiert werden können.
Ein struct -Typ ähnelt einem Klassentyp, da er eine Struktur mit Datenmembern und Funktionsmembern
darstellt. Im Gegensatz zu Klassen sind Strukturen Werttypen, die in der Regel keine Heapzuordnung
erfordern. Strukturtypen unterstützen keine benutzerdefinierte Vererbung, und alle Strukturtypen erben
implizit vom Typ object .
Ein interface -Typ definiert einen Vertrag als benannte Gruppe öffentlicher Member. Ein class - oder
struct -Typ, der einen interface -Typ implementiert, muss Implementierungen der Member der
Schnittstelle bereitstellen. Eine interface kann von mehreren Basisschnittstellen erben, und eine class
oder struct kann mehrere Schnittstellen implementieren.
Ein delegate -Typ stellt Verweise auf Methoden mit einer bestimmten Parameterliste und dem Rückgabetyp
dar. Delegate ermöglichen die Behandlung von Methoden als Entitäten, die Variablen zugewiesen und als
Parameter übergeben werden können. Delegate werden analog zu Funktionstypen von funktionalen
Sprachen bereitgestellt. Außerdem ähneln sie konzeptionell Funktionszeigern, die es in einigen anderen
Sprachen gibt. Im Gegensatz zu Funktionszeigern sind Delegaten objektorientiert und typsicher.
Die Typen, class , struct , interface und delegate unterstützen Generics, wodurch sie mit anderen Typen
parametrisiert werden können.
C# unterstützt ein- und mehrdimensionale Arrays aller beliebigen Typen. Im Gegensatz zu den oben
aufgeführten Typen müssen Arraytypen nicht deklariert werden, bevor sie verwendet werden können.
Stattdessen werden Arraytypen erstellt, indem hinter einen Typnamen eckige Klammern gesetzt werden. int[]
ist beispielsweise ein eindimensionales Array aus int , int[,] ein zweidimensionales Array aus int , während
int[][] ein eindimensionales Array aus eindimensionalen Arrays oder ein Jagged Array aus int ist.

Nullable-Typen erfordern keine getrennte Definition. Für jeden Non-Nullable-Typ T gibt es einen
entsprechenden Nullable-Typ T? , der einen zusätzlichen Wert, null , enthalten kann. Beispielsweise ist int?
ein Typ, der jeden 32-Bit-Ganzzahlwert oder den Wert null enthalten kann. string? ist ein Typ, der beliebige
string -Typen oder den Wert null enthalten kann.

Das C#-Typsystem ist dahingehend vereinheitlicht, dass ein Wert eines beliebigen Typs als object behandelt
werden kann. Jeder Typ in C# ist direkt oder indirekt vom object -Klassentyp abgeleitet, und object ist die
ultimative Basisklasse aller Typen. Werte von Verweistypen werden als Objekte behandelt, indem die Werte
einfach als Typ object angezeigt werden. Werte von Werttypen werden durch Ausführen von Boxing- und
Unboxingvorgängen als Objekte behandelt. Im folgenden Beispiel wird ein int -Wert in ein object und wieder
in einen int -Wert konvertiert.

int i = 123;
object o = i; // Boxing
int j = (int)o; // Unboxing
Wenn ein Wert eines Werttyps einem object -Verweis zugewiesen wird, wird eine „Box“ zugeordnet, die den
Wert enthalten soll. Bei dieser Box handelt es sich um eine Instanz eines Verweistyps, und der Wert wird in diese
Box kopiert. Wenn umgekehrt ein object -Verweis in einen Werttyp umgewandelt wird, wird überprüft, ob der
object -Typ, auf den verwiesen wird, eine Box des korrekten Werttyps ist. Nach erfolgreicher Überprüfung wird
der Wert in der Box zum Werttyp kopiert.
Aus dem einheitlichen C#-Typensystem resultiert, dass Werttypen „bei Nachfrage“ als object -Verweise
behandelt werden. Aufgrund der Vereinheitlichung können Bibliotheken für allgemeine Zwecke, die den Typ
object verwenden, mit allen Typen verwendet werden können, die von object abgeleitet werden, wozu
sowohl Verweis- als auch Werttypen zählen.
Es gibt mehrere Arten von Variablen in C#, einschließlich Feldern, Arrayelementen, lokalen Variablen und
Parametern. Variablen stellen Speicherorte dar. Jede Variable hat, wie nachstehend gezeigt, einen Typ, der
bestimmt, welche Werte in der Variablen gespeichert werden können.
Nicht auf NULL festlegbarer Werttyp
Ein Wert genau dieses Typs
Auf NULL festlegbarer Werttyp
Ein null -Wert oder ein Wert genau dieses Typs
object
Ein null -Verweis, ein Verweis auf ein Objekt eines beliebigen Verweistyps oder ein Verweis auf einen
geschachtelten Wert eines beliebigen Werttyps
Klassentyp
Ein null -Verweis, ein Verweis auf eine Instanz dieses Klassentyps oder ein Verweis auf eine Instanz
einer Klasse, die von diesem Klassentyp abgeleitet ist
Schnittstellentyp
Ein null -Verweis, ein Verweis auf eine Instanz eines Klassentyps, der diesen Schnittstellentyp
implementiert, oder ein Verweis auf einen geschachtelten Wert eines Werttyps, der diesen
Schnittstellentyp implementiert
Arraytyp
Ein null -Verweis, ein Verweis auf eine Instanz dieses Arraytyps oder ein Verweis auf eine Instanz
eines kompatiblen Arraytyps
Delegattyp
Ein null -Verweis oder ein Verweis auf eine Instanz eines kompatiblen Delegattyp

Programmstruktur
Die wichtigsten Organisationskonzepte in C# sind *Programme _, Namespace, Typen, Member und Assemblys
Programme deklarieren Typen, die Member enthalten, und können in Namespaces organisiert werden. Klassen,
Strukturen und Schnittstellen sind Beispiele von Typen. Felder, Methoden, Eigenschaften und Ereignisse sind
Beispiele für Member. Wenn C#-Programme kompiliert werden, werden sie physisch in Assemblys gepackt.
Assemblys haben in der Regel die Erweiterung .exe oder .dll , je nachdem, ob sie Anwendungen oder
_*Bibliotheken** implementieren.
Nehmen Sie als einfaches Beispiel eine Assembly, die den folgenden Code enthält:
using System;

namespace Acme.Collections
{
public class Stack<T>
{
Entry _top;

public void Push(T data)


{
_top = new Entry(_top, data);
}

public T Pop()
{
if (_top == null)
{
throw new InvalidOperationException();
}
T result = _top.Data;
_top = _top.Next;

return result;
}

class Entry
{
public Entry Next { get; set; }
public T Data { get; set; }

public Entry(Entry next, T data)


{
Next = next;
Data = data;
}
}
}
}

Der vollqualifizierte Name dieser Klasse ist Acme.Collections.Stack . Die Klasse enthält mehrere Member: ein
Feld mit dem Namen top , zwei Methoden mit dem Namen Push und Pop sowie eine geschachtelte Klasse mit
dem Namen Entry . Die Entry -Klasse enthält weitere drei Member: ein Feld mit dem Namen next , ein Feld
mit dem Namen data und einen Konstruktor. Stack ist eine generische Klasse. Sie hat einen Typparameter, T ,
der bei seiner Verwendung durch einen konkreten Typ ersetzt wird.
Ein Stapel ist eine FILO-Sammlung (First In, Last Out). Neue Elemente werden am Anfang des Stapels
hinzugefügt. Das Entfernen eines Elements erfolgt von oben aus dem Stapel. Im vorherigen Beispiel wird der Typ
Stack deklariert, der den Speicher und das Verhalten für einen Stapel definiert. Sie können eine Variable
deklarieren, die auf eine Instanz des Typs Stack verweist, um diese Funktionalität zu verwenden.
Assemblys enthalten ausführbaren Code in Form von Zwischensprachenanweisungen (Intermediate Language,
IL) und symbolischen Informationen in Form von Metadaten. Vor der Ausführen konvertiert der JIT-Compiler
(Just-In-Time) der .NET Common Language Runtime den IL-Code in einer Assembly in prozessorspezifischen
Code.
Da eine Assembly eine selbstbeschreibende Funktionseinheit mit Code und Metadaten ist, besteht in C# keine
Notwendigkeit für #include -Direktiven und Headerdateien. Die öffentlichen Typen und Member, die in einer
bestimmten Assembly enthalten sind, werden einfach durch Verweisen auf die Assembly beim Kompilieren des
Programms in einem C#-Programm verfügbar gemacht. Dieses Programm verwendet z.B. die
Acme.Collections.Stack -Klasse aus der acme.dll -Assembly:
using System;
using Acme.Collections;

class Example
{
public static void Main()
{
var s = new Stack<int>();
s.Push(1); // stack contains 1
s.Push(10); // stack contains 1, 10
s.Push(100); // stack contains 1, 10, 100
Console.WriteLine(s.Pop()); // stack contains 1, 10
Console.WriteLine(s.Pop()); // stack contains 1
Console.WriteLine(s.Pop()); // stack is empty
}
}

Um dieses Programm zu kompilieren, müssen Sie auf die Assembly verweisen, die die im vorherigen Beispiel
definierte Stapelklasse enthält.
C#-Programme können in mehreren Quelldateien gespeichert werden. Bei der Kompilierung eines C#-
Programms werden alle Quelldateien zusammen verarbeitet. Die Quelldateien können frei aufeinander
verweisen. Konzeptionell ist das Ganze so, als ob alle Quelldateien vor der Verarbeitung zu einer großen Datei
verkettet worden wären. Vorwärtsdeklarationen sind in C# nie erforderlich, da die Reihenfolge der Deklaration
mit wenigen Ausnahmen unbedeutend ist. C# beschränkt eine Quelldatei weder auf die Deklaration eines
einzigen öffentlichen Typs, noch muss der Name der Quelldatei mit einem in der Quelldatei deklarierten Typ
übereinstimmen.
In weiteren Artikeln in dieser Einführung werden diese Organisationsblöcke erläutert.

N Ä C H S TE
Typen und Member
04.11.2021 • 6 minutes to read

Als objektorientierte Sprache unterstützt C# die Konzepte der Kapselung, Vererbung und Polymorphie. Eine
Klasse kann direkt von einer übergeordneten Klasse erben und eine beliebige Anzahl von Schnittstellen
implementieren. Methoden, die virtuelle Methoden in einer übergeordneten Klasse überschreiben, erfordern das
override -Schlüsselwort als Möglichkeit, eine versehentliche Neudefinition zu verhindern. In C# verhält sich
eine Struktur wie eine vereinfachte Klasse. Sie entspricht einem auf dem Stapel reservierten Typ, der
Schnittstellen implementieren kann, jedoch keine Vererbung unterstützt. C# bietet record class - und
record struct -Typen. Der Zweck dieser Typen besteht primär darin, Datenwerte zu speichern.

Klassen und Objekte


Klassen sind die grundlegendsten der C#-Typen. Eine Klasse ist eine Datenstruktur, die einen Zustand (Felder)
und Aktionen (Methoden und andere Funktionsmember) in einer einzigen Einheit kombiniert. Eine Klasse stellt
eine Definition für Instanzen der Klasse bereit, die auch Objekte genannt werden. Klassen unterstützen
Vererbung und Polymorphie. Dies sind Mechanismen, durch die abgeleitete Klassen erweitert und Basisklassen
spezialisiert werden können.
Neue Klassen werden mithilfe von Klassendeklarationen erstellt. Eine Klassendeklaration beginnt mit einem
Header. Der Header legt Folgendes fest:
Die Attribute und Modifizierer der Klasse
Den Namen der Klasse
Die Basisklasse (wenn von einer Basisklasse geerbt wird)
Die von der Klasse implementierten Schnittstellen
Auf den Header folgt der Klassenkörper. Dieser besteht aus einer Liste der Memberdeklarationen, die zwischen
den Trennzeichen { und } eingefügt werden.
Im folgenden Code wird die Deklaration einer einfachen Klasse namens Point veranschaulicht:

public class Point


{
public int X { get; }
public int Y { get; }

public Point(int x, int y) => (X, Y) = (x, y);


}

Instanzen von Klassen werden mit dem new -Operator erstellt. Dieser reserviert Speicher für eine neue Instanz,
ruft einen Konstruktor zum Initialisieren der Instanz auf und gibt einen Verweis auf die Instanz zurück. Mit den
folgenden Anweisungen werden zwei Point -Objekte erstellt und Verweise auf diese Objekte in zwei Variablen
gespeichert:

var p1 = new Point(0, 0);


var p2 = new Point(10, 20);

Der von einem Objekt belegte Speicher wird automatisch wieder freigegeben, wenn das Objekt nicht mehr
erreichbar ist. Es ist weder erforderlich noch möglich, die Zuweisung von Objekten in C# explizit aufzuheben.
Typparameter
Generische Typparameter definieren Typparameter . Typparameter sind eine Liste von Typparameternamen, die
in spitzen Klammern enthalten sind. Typparameter folgen auf den Klassennamen. Die Typparameter können
dann im Körper der Klassendeklarationen zum Definieren der Klassenmember verwendet werden. Im folgenden
Beispiel lauten die Typparameter von Pair``TFirst und TSecond :

public class Pair<TFirst, TSecond>


{
public TFirst First { get; }
public TSecond Second { get; }

public Pair(TFirst first, TSecond second) =>


(First, Second) = (first, second);
}

Ein Klassentyp, der zum Akzeptieren von Typparametern deklariert wird, wird als generischer Klassentyp
bezeichnet. Struktur-, Schnittstellen- und Delegattypen können auch generisch sein. Wenn die generische Klasse
verwendet wird, müssen für jeden der Typparameter Typargumente angegeben werden:

var pair = new Pair<int, string>(1, "two");


int i = pair.First; // TFirst int
string s = pair.Second; // TSecond string

Ein generischer Typ, für den Typargumente angegeben wurden (siehe Pair<int,string> oben), wird als
konstruierter Typ bezeichnet.
Basisklassen
Mit einer Klassendeklaration kann eine Basisklasse angegeben werden. Fügen Sie nach dem Klassennamen und
den Typparametern einen Doppelpunkt und den Namen der Basisklasse ein. Das Auslassen einer
Basisklassenspezifikation ist dasselbe wie eine Ableitung vom Typ object . Im folgenden Beispiel ist Point die
Basisklasse von Point3D . Im folgenden ersten Beispiel ist object die Basisklasse von Point :

public class Point3D : Point


{
public int Z { get; set; }

public Point3D(int x, int y, int z) : base(x, y)


{
Z = z;
}
}

Eine Klasse erbt die Member der zugehörigen Basisklasse. Vererbung bedeutet, dass eine Klasse implizit nahezu
alle Member der Basisklasse enthält. Eine Klasse erbt die Instanz- und statischen Konstruktoren sowie den
Finalizer nicht. Eine abgeleitete Klasse kann den geerbten Membern neue Member hinzufügen, aber die
Definition eines geerbten Members kann nicht entfernt werden. Im vorherigen Beispiel erbt Point3D die
Member X und Y von Point , und alle Point3D -Instanzen enthalten die drei Eigenschaften X , Y und Z .
Ein Klassentyp kann implizit in einen beliebigen zugehörigen Basisklassentyp konvertiert werden. Eine Variable
eines Klassentyps kann auf eine Instanz der Klasse oder eine Instanz einer beliebigen abgeleiteten Klasse
verweisen. Beispielsweise kann in den vorherigen Klassendeklarationen eine Variable vom Typ Point entweder
auf Point oder auf Point3D verweisen:
Point a = new Point(10, 20);
Point b = new Point3D(10, 20, 30);

Strukturen
Klassen definieren Typen, die die Vererbung und die Polymorphie unterstützen. Sie ermöglichen es Ihnen,
komplexe Verhaltensweise anhand von Hierarchien abgeleiteter Klassen zu erstellen. Im Gegensatz dazu sind
Struktur typen (struct) einfachere Typen, deren Hauptaufgabe darin besteht, Datenwerte zu speichern.
Strukturen können keinen Basistyp deklarieren, sie leiten implizit von System.ValueType ab. Sie können keine
anderen struct -Typen von einem struct -Typ ableiten. Sie werden implizit versiegelt.

public struct Point


{
public double X { get; }
public double Y { get; }

public Point(double x, double y) => (X, Y) = (x, y);


}

Schnittstellen
Eine *Schnittstelle _ definiert einen Vertrag, der von Klassen und Strukturen implementiert werden kann. Sie
definieren eine Schnittstelle, um Funktionen zu deklarieren, die von verschiedenen Typen gemeinsam genutzt
werden. Die System.Collections.Generic.IEnumerable<T>-Schnittstelle definiert beispielsweise eine konsistente
Methode zum Durchlaufen aller Elemente in einer Sammlung, z. B. in einem Array. Eine Schnittstelle kann
Methoden, Eigenschaften, Ereignisse und Indexer enthalten. Eine Schnittstelle stellt in der Regel keine
Implementierungen der von ihr definierten Member bereit. Sie gibt lediglich die Member an, die von Klassen
oder Strukturen bereitgestellt werden müssen, die die Schnittstelle implementieren.
Schnittstellen können Mehrfachvererbung einsetzen. Im folgenden Beispiel erbt die Schnittstelle IComboBox
sowohl von ITextBox als auch IListBox .

interface IControl
{
void Paint();
}

interface ITextBox : IControl


{
void SetText(string text);
}

interface IListBox : IControl


{
void SetItems(string[] items);
}

interface IComboBox : ITextBox, IListBox { }

Klassen und Strukturen können mehrere Schnittstellen implementieren. Im folgenden Beispiel implementiert die
Klasse EditBox sowohl IControl als auch IDataBound .
interface IDataBound
{
void Bind(Binder b);
}

public class EditBox : IControl, IDataBound


{
public void Paint() { }
public void Bind(Binder b) { }
}

Wenn eine Klasse oder Struktur eine bestimmte Schnittstelle implementiert, können Instanzen dieser Klasse
oder Struktur implizit in diesen Schnittstellentyp konvertiert werden. Beispiel:

EditBox editBox = new EditBox();


IControl control = editBox;
IDataBound dataBound = editBox;

Enumerationen
Enumerationstypen (Enum) definieren eine Gruppe konstanter Werte. Mit dem folgenden enum -Typ werden
Konstanten deklariert, die verschiedene Wurzelgemüsesorten definieren:

public enum SomeRootVegetable


{
HorseRadish,
Radish,
Turnip
}

Sie können einen enum -Typ definieren, der in Kombination als Flags verwendet werden sollen. In der folgenden
Deklaration werden Flags für die vier Jahreszeiten deklariert. Jede Kombination der Jahreszeiten kann
angewendet werden, einschließlich eines All -Werts, der alle Jahreszeiten umfasst:

[Flags]
public enum Seasons
{
None = 0,
Summer = 1,
Autumn = 2,
Winter = 4,
Spring = 8,
All = Summer | Autumn | Winter | Spring
}

Im folgenden Beispiel werden Deklaration der beiden Enumerationen veranschaulicht:

var turnip = SomeRootVegetable.Turnip;

var spring = Seasons.Spring;


var startingOnEquinox = Seasons.Spring | Seasons.Autumn;
var theYear = Seasons.All;

Nullable-Typen
Jede Art von Variable kann als non-nullable _ oder _nullable_ deklariert werden. Eine Nullable-Variable kann
einen zusätzlichen null -Wert enthalten, der angibt, dass kein Wert vorliegt. Nullable-Werttypen (Strukturen
oder Enumerationen) werden mit System.Nullable<T> dargestellt. Non-Nullable- und Nullable-Verweistypen
werden beide vom zugrunde liegenden Verweistyp repräsentiert. Der Unterschied wird von Metadaten
dargestellt, die vom Compiler und einigen Bibliotheken gelesen werden. Der Compiler gibt Warnungen aus,
wenn Nullable-Verweise dereferenziert werden, ohne dass ihr Wert zunächst auf null geprüft wird. Der
Compiler gibt auch Warnungen aus, wenn Non-Nullable-Verweisen ein Wert zugewiesen wird, der null sein
kann. Im folgenden Beispiel wird ein _nullable int_-Wert deklariert und mit null initialisiert. Dann wird der
Wert auf 5 festgelegt. Dasselbe Konzept wird mit _nullable string** veranschaulicht. Weitere Informationen
finden Sie unter Nullable-Werttypen und Nullable-Verweistypen.

int? optionalInt = default;


optionalInt = 5;
string? optionalText = default;
optionalText = "Hello World.";

Tupel
C# unterstützt Tupel , die eine kompakte Syntax zum Gruppieren mehrerer Datenelemente in einer einfachen
Datenstruktur bereitstellen. Sie können ein Tupel instanziieren, indem Sie die Typen und Namen der Member
zwischen ( und ) deklarieren. Dies wird im folgenden Beispiel veranschaulicht:

(double Sum, int Count) t2 = (4.5, 3);


Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
// Output:
// Sum of 3 elements is 4.5.

Tupel bieten eine Alternative für Datenstrukturen mit mehreren Membern, ohne dass die Bausteine verwendet
werden müssen, die im nächsten Artikel beschrieben werden.

ZU RÜ CK W E ITE R
Programmbausteine
04.11.2021 • 23 minutes to read

Die im vorherigen Artikel beschriebenen Typen werden mithilfe der folgenden Bausteine erstellt: *Member _,
Ausdrücke und _ *Anweisungen**.

Member
Die Member von class sind entweder statische Member _ oder _Instanzmember**. Statische Member
gehören zu Klassen, Instanzmember gehören zu Objekten (Instanzen von Klassen).
In der folgenden Liste finden Sie einen Überblick über die Memberarten, die eine Klasse enthalten kann.
Konstanten: Konstante Werte, die der Klasse zugeordnet sind
Felder : Variablen, die der Klasse zugeordnet sind.
Methods (Methoden): Aktionen, die von der Klasse ausgeführt werden
Proper ties: Aktionen im Zusammenhang mit dem Lesen und Schreiben von benannten Eigenschaften der
Klasse
Indexer : Aktionen im Zusammenhang mit dem Indizieren von Instanzen der Klasse, z.B. einem Array
Ereignisse: Benachrichtigungen, die von der Klasse generiert werden können
Operatoren: Operatoren für Konvertierungen und Ausdrücke, die von der Klasse unterstützt werden
Konstruktoren: Aktionen, die zum Initialisieren von Instanzen der Klasse oder der Klasse selbst benötigt
werden
Finalizer : Aktionen, die ausgeführt werden, bevor Instanzen der Klasse dauerhaft verworfen werden
Typen: Geschachtelte Typen, die von der Klasse deklariert werden

Barrierefreiheit
Jeder Member einer Klasse ist mit einem Zugriff verknüpft, der die Regionen des Programmtexts steuert, die auf
den Member zugreifen können. Es gibt sechs mögliche Formen des Zugriffs. Die Zugriffsmodifizierer werden im
Folgenden zusammengefasst.
public : Der Zugriff ist nicht eingeschränkt.
private : Der Zugriff ist auf diese Klasse beschränkt.
protected : Der Zugriff ist auf diese Klasse oder von dieser abgeleiteten Klassen beschränkt.
internal : Der Zugriff ist auf die aktuelle Assembly ( .exe oder .dll ) beschränkt.
protected internal : Der Zugriff ist auf diese Klasse, auf Klassen, die von dieser Klasse abgeleitet wurden,
oder auf Klassen innerhalb der gleichen Assembly beschränkt.
private protected : Der Zugriff ist auf diese Klasse und auf Klassen in derselben Assembly beschränkt, die
von diesem Typ abgeleitet wurden.

Felder
Ein Feld ist eine Variable, die einer Klasse oder einer Instanz einer Klasse zugeordnet ist.
Ein Feld, das mit dem static-Modifizierer deklariert wurde, definiert ein statisches Feld. Ein statisches Feld
identifiziert genau einen Speicherort. Unabhängig davon, wie viele Instanzen einer Klasse erstellt werden, gibt es
immer nur eine Kopie eines statischen Felds.
Ein Feld, das ohne den static-Modifizierer deklariert wurde, definiert ein Instanzfeld. Jede Instanz einer Klasse
enthält eine separate Kopie aller Instanzfelder dieser Klasse.
Im folgenden Beispiel weist jede Instanz der Color -Klasse eine separate Kopie der Instanzfelder R , G und B
auf, aber es gibt nur eine Kopie der statischen Felder Black , White , Red , Green und Blue :

public class Color


{
public static readonly Color Black = new Color(0, 0, 0);
public static readonly Color White = new Color(255, 255, 255);
public static readonly Color Red = new Color(255, 0, 0);
public static readonly Color Green = new Color(0, 255, 0);
public static readonly Color Blue = new Color(0, 0, 255);

public byte R;
public byte G;
public byte B;

public Color(byte r, byte g, byte b)


{
R = r;
G = g;
B = b;
}
}

Wie im vorherigen Beispiel gezeigt, können schreibgeschützte Felder mit einem readonly -Modifizierer
deklariert werden. Zuweisungen zu einem schreibgeschützten Feld können nur im Rahmen der Deklaration des
Felds oder in einem Konstruktor derselben Klasse auftreten.

Methoden
Eine Methode ist ein Member, das eine Berechnung oder eine Aktion implementiert, die durch ein Objekt oder
eine Klasse durchgeführt werden kann. Auf statische Methoden wird über die Klasse zugegriffen. Auf
Instanzmethoden wird über Instanzen der Klasse zugegriffen.
Methoden verfügen über eine Liste von Parametern, die Werte oder Variablenverweise darstellen, die an die
Methode übergeben werden. Methoden besitzen einen Rückgabetyp, der den Typ des Werts festlegt, der von
der Methode berechnet und zurückgegeben wird. Der Rückgabetyp einer Methode lautet void , wenn kein Wert
zurückgegeben wird.
Ebenso wie Typen können Methoden einen Satz an Typparametern aufweisen, für den beim Aufruf der Methode
Typargumente angegeben werden müssen. Im Gegensatz zu Typen können die Typargumente häufig aus den
Argumenten eines Methodenaufrufs abgeleitet werden und müssen nicht explizit angegeben werden.
Die Signatur einer Methode muss innerhalb der Klasse eindeutig sein, in der die Methode deklariert ist. Die
Signatur einer Methode besteht aus dem Namen der Methode, der Anzahl von Typparametern und der Anzahl,
den Modifizierern und den Typen der zugehörigen Parameter. Die Signatur einer Methode umfasst nicht den
Rückgabetyp.
Wenn es sich bei einem Methodenkörper um einen einzelnen Ausdruck handelt, kann die Methode mithilfe
eines kompakten Ausdrucksformat definiert werden. Dies wird im folgenden Beispiel veranschaulicht:

public override string ToString() => "This is an object";

Parameter
Parameter werden dazu verwendet, Werte oder Variablenverweise an Methoden zu übergeben. Die Parameter
einer Methode erhalten ihre tatsächlichen Werte über Argumente, die angegeben werden, wenn die Methode
aufgerufen wird. Es gibt vier Arten von Parametern: Wertparameter, Verweisparameter, Ausgabeparameter und
Parameterarrays.
Ein Wertparameter wird zum Übergeben von Eingabeargumenten verwendet. Ein Wertparameter entspricht
einer lokalen Variablen, die ihren Anfangswert von dem Argument erhält, das für den Parameter übergeben
wurde. Änderungen an einem Wertparameter wirken sich nicht auf das Argument aus, das für den Parameter
übergeben wurde.
Wertparameter können optional sein, indem ein Standardwert festgelegt wird, damit die zugehörigen
Argumente weggelassen werden können.
Ein Verweisparameter wird zum Übergeben von Argumenten als Verweis verwendet. Das für einen
Verweisparameter übergebene Argument muss eine Variable mit einem definitiven Wert sein. Währen der
Ausführung der Methode stellt der Verweisparameter denselben Speicherort wie die Argumentvariable dar. Ein
Verweisparameter wird mit dem ref -Modifizierer deklariert. Das folgende Beispiel veranschaulicht die
Verwendung des ref -Parameters.

static void Swap(ref int x, ref int y)


{
int temp = x;
x = y;
y = temp;
}

public static void SwapExample()


{
int i = 1, j = 2;
Swap(ref i, ref j);
Console.WriteLine($"{i} {j}"); // "2 1"
}

Ein Ausgabeparameter wird zum Übergeben von Argumenten als Verweis verwendet. Er ist einem
Verweisparameter ähnlich, außer dass er nicht erfordert, dass Sie explizit dem vom Aufrufer bereitgestellten
Argument einen Wert zuweisen. Ein Ausgabeparameter wird mit dem out -Modifizierer deklariert. Das
folgende Beispiel zeigt die Verwendung von out -Parametern mithilfe der in C# 7 eingeführten Syntax.

static void Divide(int x, int y, out int result, out int remainder)
{
result = x / y;
remainder = x % y;
}

public static void OutUsage()


{
Divide(10, 3, out int res, out int rem);
Console.WriteLine($"{res} {rem}"); // "3 1"
}

Ein Parameterarray ermöglicht es, eine variable Anzahl von Argumenten an eine Methode zu übergeben. Ein
Parameterarray wird mit dem params -Modifizierer deklariert. Nur der letzte Parameter einer Methode kann ein
Parameterarray sein, und es muss sich um ein eindimensionales Parameterarray handeln. Die Methoden Write
und WriteLine der Klasse System.Console sind gute Beispiele für die Nutzung eines Parameterarrays. Sie
werden folgendermaßen deklariert.
public class Console
{
public static void Write(string fmt, params object[] args) { }
public static void WriteLine(string fmt, params object[] args) { }
// ...
}

Innerhalb einer Methode mit einem Parameterarray verhält sich das Parameterarray wie ein regulärer Parameter
des Arraytyps. Beim Aufruf einer Methode mit einem Parameterarray ist es jedoch möglich, entweder ein
einzelnes Argument des Parameterarraytyps oder eine beliebige Anzahl von Argumenten des Elementtyps des
Parameterarrays zu übergeben. Im letzteren Fall wird automatisch eine Arrayinstanz erstellt und mit den
vorgegebenen Argumenten initialisiert. Dieses Beispiel:

int x, y, z;
x = 3;
y = 4;
z = 5;
Console.WriteLine("x={0} y={1} z={2}", x, y, z);

...entspricht dem folgenden Code:

int x = 3, y = 4, z = 5;

string s = "x={0} y={1} z={2}";


object[] args = new object[3];
args[0] = x;
args[1] = y;
args[2] = z;
Console.WriteLine(s, args);

Methodenkörper und lokale Variablen


Der Methodenkörper gibt die Anweisungen an, die beim Aufruf der Methode ausgeführt werden sollen.
Ein Methodenkörper kann Variablen deklarieren, die für den Aufruf der Methode spezifisch sind. Diese Variable
werden lokale Variablen genannt. Die Deklaration einer lokalen Variable gibt einen Typnamen, einen
Variablennamen und eventuell einen Anfangswert an. Im folgenden Beispiel wird eine lokale Variable i mit
einem Anfangswert von 0 und einer lokalen Variablen j ohne Anfangswert deklariert.

class Squares
{
public static void WriteSquares()
{
int i = 0;
int j;
while (i < 10)
{
j = i * i;
Console.WriteLine($"{i} x {i} = {j}");
i = i + 1;
}
}
}

In C# muss eine lokale Variable definitiv zugewiesen sein, bevor ihr Wert abgerufen werden kann. Wenn die
vorherige Deklaration von i beispielsweise keinen Anfangswert enthält, würde der Compiler bei der späteren
Verwendung von i einen Fehler melden, weil i zu diesen Zeitpunkten im Programm nicht definitiv
zugewiesen ist.
Eine Methode kann return -Anweisungen verwenden, um die Steuerung an den zugehörigen Aufrufer
zurückzugeben. In einer Methode, die void zurückgibt, können return -Anweisungen keinen Ausdruck
angeben. In einer Methode, die nicht „void“ zurückgibt, müssen return -Anweisungen einen Ausdruck enthalten,
der den Rückgabewert berechnet.
Statische Methoden und Instanzmethoden
Eine Methode, die mit einem static -Modifizierer deklariert wird, ist eine statische Methode. Eine statische
Methode führt keine Vorgänge für eine spezifische Instanz aus und kann nur direkt auf statische Member
zugreifen.
Eine Methode, die ohne einen static -Modifizierer deklariert wird, ist eine Instanzmethode. Eine
Instanzmethode führt Vorgänge für eine spezifische Instanz aus und kann sowohl auf statische Member als auch
auf Instanzmember zugreifen. Auf die Instanz, für die eine Instanzmethode aufgerufen wurde, kann explizit als
this zugegriffen werden. Es ist ein Fehler, in einer statischen Methode auf this zu verweisen.

Die folgende Entity -Klasse umfasst sowohl statische Member als auch Instanzmember.

class Entity
{
static int s_nextSerialNo;
int _serialNo;

public Entity()
{
_serialNo = s_nextSerialNo++;
}

public int GetSerialNo()


{
return _serialNo;
}

public static int GetNextSerialNo()


{
return s_nextSerialNo;
}

public static void SetNextSerialNo(int value)


{
s_nextSerialNo = value;
}
}

Jede Entity -Instanz enthält eine Seriennummer (und vermutlich weitere Informationen, die hier nicht
angezeigt werden). Der Entity -Konstruktor (der einer Instanzmethode ähnelt) initialisiert die neue Instanz mit
der nächsten verfügbaren Seriennummer. Da der Konstruktor ein Instanzmember ist, kann er sowohl auf das
_serialNo -Instanzfeld als auch auf das statische s_nextSerialNo -Feld zugreifen.

Die statischen Methoden GetNextSerialNo und SetNextSerialNo können auf das statische Feld s_nextSerialNo
zugreifen, aber es wäre ein Fehler, über diese Methoden direkt auf das Instanzfeld _serialNo zuzugreifen.
Im folgenden Beispiel wird die Verwendung der Entity -Klasse veranschaulicht.

Entity.SetNextSerialNo(1000);
Entity e1 = new Entity();
Entity e2 = new Entity();
Console.WriteLine(e1.GetSerialNo()); // Outputs "1000"
Console.WriteLine(e2.GetSerialNo()); // Outputs "1001"
Console.WriteLine(Entity.GetNextSerialNo()); // Outputs "1002"
Die statischen Methoden SetNextSerialNo und GetNextSerialNo werden für die Klasse aufgerufen, während die
GetSerialNo -Instanzmethode für Instanzen der Klasse aufgerufen wird.

Virtuelle, überschriebene und abstrakte Methoden


Sie verwenden virtuelle, Außerkraftsetzungs- und abstrakte Methoden, um das Verhalten für eine Hierarchie
von Klassentypen zu definieren. Da eine Klasse von einer Basisklasse abgeleitet werden kann, muss diese
abgeleitete Klasse möglicherweise das in der Basisklasse implementierte Verhalten ändern. Eine vir tuelle
Methode ist eine Methode, die in einer Basisklasse deklariert und implementiert wird, wobei jede abgeleitete
Klasse eine spezifischere Implementierung bereitstellen kann. Eine Außerkraftsetzungsmethode ist eine in einer
abgeleiteten Klasse implementierte Methode, die das Verhalten der Implementierung der Basisklasse ändert.
Eine abstrakte Methode ist eine Methode, die in einer Basisklasse deklariert ist, die in allen abgeleiteten Klassen
überschrieben werden muss. Tatsächlich definieren abstrakte Methoden keine Implementierung in der
Basisklasse.
Methodenaufrufe von Instanzmethoden können entweder in Basisklassenimplementierungen oder
Implementierungen abgeleiteter Klassen aufgelöst werden. Der Typ einer Variablen bestimmt ihren
Kompilierzeittyp. Der Kompilierzeittyp ist der Typ, den der Compiler verwendet, um seine Member zu
bestimmen. Eine Variable kann jedoch einer Instanz eines beliebigen Typs zugewiesen werden, der von seinem
Kompilierzeittyp abgeleitet ist. Der Laufzeittyp ist der Typ der tatsächlichen Instanz, auf die von dieser Variablen
verwiesen wird.
Beim Aufruf einer virtuellen Methode bestimmt der Laufzeittyp der Instanz, für die der Aufruf erfolgt, die
tatsächlich aufzurufende Methodenimplementierung. Beim Aufruf einer nicht virtuellen Methode ist der
Kompilierzeittyp der bestimmende Faktor.
Eine virtuelle Methode kann in einer abgeleiteten Klasse überschrieben werden. Wenn eine
Instanzmethodendeklaration einen override-Modifizierer enthält, überschreibt die Methode eine geerbte
virtuelle Methode mit derselben Signatur. Mit der virtuellen Methodendeklaration wird eine neue Methode
eingeführt. Eine Deklaration einer Überschreibungsmethode spezialisiert eine vorhandene geerbte virtuelle
Methode, indem eine neue Implementierung dieser Methode bereitgestellt wird.
Eine abstrakte Methode ist eine virtuelle Methode ohne Implementierung. Eine abstrakte Methode wird mit dem
Modifizierer abstract deklariert und ist nur in einer abstrakten Klasse zulässig. Eine abstrakte Methode muss in
jeder nicht abstrakten abgeleiteten Klasse überschrieben werden.
Im folgenden Beispiel wird die abstrakte Klasse Expression deklariert, die einen Ausdrucksbaumstrukturknoten
sowie drei abgeleitete Klassen repräsentiert: Constant , VariableReference und Operation . Diese
implementieren Ausdrucksbaumstrukturknoten für Konstanten, variable Verweise und arithmetische
Operationen. (Dieses Beispiel ähnelt den Ausdrucksbaumstrukturtypen, ist aber nicht mit diesen verwandt.)
public abstract class Expression
{
public abstract double Evaluate(Dictionary<string, object> vars);
}

public class Constant : Expression


{
double _value;

public Constant(double value)


{
_value = value;
}

public override double Evaluate(Dictionary<string, object> vars)


{
return _value;
}
}

public class VariableReference : Expression


{
string _name;

public VariableReference(string name)


{
_name = name;
}

public override double Evaluate(Dictionary<string, object> vars)


{
object value = vars[_name] ?? throw new Exception($"Unknown variable: {_name}");
return Convert.ToDouble(value);
}
}

public class Operation : Expression


{
Expression _left;
char _op;
Expression _right;

public Operation(Expression left, char op, Expression right)


{
_left = left;
_op = op;
_right = right;
}

public override double Evaluate(Dictionary<string, object> vars)


{
double x = _left.Evaluate(vars);
double y = _right.Evaluate(vars);
switch (_op)
{
case '+': return x + y;
case '-': return x - y;
case '*': return x * y;
case '/': return x / y;

default: throw new Exception("Unknown operator");


}
}
}

Die vorherigen vier Klassen können zum Modellieren arithmetischer Ausdrücke verwendet werden.
Beispielsweise kann mithilfe von Instanzen dieser Klassen der Ausdruck x + 3 folgendermaßen dargestellt
werden.

Expression e = new Operation(


new VariableReference("x"),
'+',
new Constant(3));

Die Evaluate -Methode einer Expression -Instanz wird aufgerufen, um den vorgegebenen Ausdruck
auszuwerten und einen double -Wert zu generieren. Die Methode verwendet ein Dictionary -Argument, das
Variablennamen (als Schlüssel der Einträge) und Werte (als Werte der Einträge) enthält. Da Evaluate eine
abstrakte Methode ist, müssen nicht-abstrakte Klassen, die von Expression abgeleitet sind, Evaluate außer
Kraft setzen.
Eine Implementierung von Constant für Evaluate gibt lediglich die gespeicherte Konstante zurück. Eine
Implementierung von VariableReference sucht im Wörterbuch nach dem Variablennamen und gibt den
Ergebniswert zurück. Eine Implementierung von Operation wertet zunächst (durch einen rekursiven Aufruf der
zugehörigen Evaluate -Methoden) den linken und rechten Operanden aus und führt dann die vorgegebene
arithmetische Operation aus.
Das folgende Programm verwendet die Expression -Klassen zum Auswerten des Ausdrucks x * (y + 2) für
verschiedene Werte von x und y .

Expression e = new Operation(


new VariableReference("x"),
'*',
new Operation(
new VariableReference("y"),
'+',
new Constant(2)
)
);
Dictionary<string, object> vars = new Dictionary<string, object>();
vars["x"] = 3;
vars["y"] = 5;
Console.WriteLine(e.Evaluate(vars)); // "21"
vars["x"] = 1.5;
vars["y"] = 9;
Console.WriteLine(e.Evaluate(vars)); // "16.5"

Methodenüberladung
Das Überladen von Methoden macht es möglich, dass mehrere Methoden in derselben Klasse denselben Namen
verwenden, solange sie eindeutige Signaturen aufweisen. Beim Kompilieren des Aufrufs einer überladenen
Methode verwendet der Compiler die Überladungsauflösung, um die spezifische Methode zu ermitteln, die
aufgerufen werden soll. Die Überladungsauflösung ermittelt die Methode, die den Argumenten am besten
entspricht. Wenn keine optimale Übereinstimmung gefunden wird, wird ein Fehler gemeldet. Das folgende
Beispiel zeigt die Verwendung der Überladungsauflösung. Der Kommentar für jeden Aufruf in der UsageExample
-Methode zeigt, welche Methode aufgerufen wird.
class OverloadingExample
{
static void F() => Console.WriteLine("F()");
static void F(object x) => Console.WriteLine("F(object)");
static void F(int x) => Console.WriteLine("F(int)");
static void F(double x) => Console.WriteLine("F(double)");
static void F<T>(T x) => Console.WriteLine("F<T>(T)");
static void F(double x, double y) => Console.WriteLine("F(double, double)");

public static void UsageExample()


{
F(); // Invokes F()
F(1); // Invokes F(int)
F(1.0); // Invokes F(double)
F("abc"); // Invokes F<string>(string)
F((double)1); // Invokes F(double)
F((object)1); // Invokes F(object)
F<int>(1); // Invokes F<int>(int)
F(1, 1); // Invokes F(double, double)
}
}

Wie im Beispiel gezeigt, kann eine bestimmte Methode immer ausgewählt werden, indem die Argumente
explizit in die exakten Parametertypen und Typargumente umgewandelt werden.

Andere Funktionsmember
Member, die ausführbaren Code enthalten, werden als Funktionsmember einer Klasse bezeichnet. Im vorherigen
Abschnitt wurden Methoden beschrieben, die die Haupttypen von Funktionsmembern sind. In diesem Abschnitt
werden die weiteren Funktionsmember behandelt, die C# unterstützt: Konstruktoren, Eigenschaften, Indexer,
Ereignisse, Operatoren und Finalizer.
Im folgenden Beispiel wird eine generische Klasse namens MyList<T> gezeigt, die eine wachsende Liste von
Objekten implementiert. Die Klasse enthält verschiedene Beispiele der gängigsten Arten von
Funktionsmembern.

public class MyList<T>


{
const int DefaultCapacity = 4;

T[] _items;
int _count;

public MyList(int capacity = DefaultCapacity)


{
_items = new T[capacity];
}

public int Count => _count;

public int Capacity


{
get => _items.Length;
set
{
if (value < _count) value = _count;
if (value != _items.Length)
{
T[] newItems = new T[value];
Array.Copy(_items, 0, newItems, 0, _count);
_items = newItems;
}
}
}

public T this[int index]


{
get => _items[index];
set
{
_items[index] = value;
OnChanged();
}
}

public void Add(T item)


{
if (_count == Capacity) Capacity = _count * 2;
_items[_count] = item;
_count++;
OnChanged();
}
protected virtual void OnChanged() =>
Changed?.Invoke(this, EventArgs.Empty);

public override bool Equals(object other) =>


Equals(this, other as MyList<T>);

static bool Equals(MyList<T> a, MyList<T> b)


{
if (Object.ReferenceEquals(a, null)) return Object.ReferenceEquals(b, null);
if (Object.ReferenceEquals(b, null) || a._count != b._count)
return false;
for (int i = 0; i < a._count; i++)
{
if (!object.Equals(a._items[i], b._items[i]))
{
return false;
}
}
return true;
}

public event EventHandler Changed;

public static bool operator ==(MyList<T> a, MyList<T> b) =>


Equals(a, b);

public static bool operator !=(MyList<T> a, MyList<T> b) =>


!Equals(a, b);
}

Konstruktoren
C# unterstützt sowohl Instanzkonstruktoren als auch statische Konstruktoren. Ein Instanzkonstruktor ist ein
Member, der die erforderlichen Aktionen zum Initialisieren einer Instanz einer Klasse implementiert. Ein
statischer Konstruktor ist ein Member, der die zum Initialisieren einer Klasse erforderlichen Aktionen
implementiert, um die Klasse beim ersten Laden selbst zu initialisieren.
Ein Konstruktor wird wie eine Methode ohne Rückgabetyp und mit demselben Namen wie die enthaltende
Klasse deklariert. Wenn eine Konstruktordeklaration einen static -Modifizierer enthält, deklariert diese einen
statischen Konstruktor. Andernfalls wird ein Instanzkonstruktor deklariert.
Instanzkonstruktoren können überladen werden und optionale Parameter verwenden. Die MyList<T> -Klasse
deklariert z.B. einen Instanzkonstruktor mit einem einzelnen optionalen int -Parameter. Instanzkonstruktoren
werden über den new -Operator aufgerufen. Die folgenden Anweisungen weisen zwei Instanzen von
MyList<string> unter Verwendung des Konstruktors der MyList -Klasse zu, mit dem optionalen Argument und
ohne das optionale Argument.
MyList<string> list1 = new MyList<string>();
MyList<string> list2 = new MyList<string>(10);

Im Gegensatz zu anderen Membern werden Instanzkonstruktoren nicht geerbt. Eine Klasse weist keine anderen
Instanzkonstruktoren auf als diejenigen, die tatsächlich in der Klasse deklariert wurden. Wenn kein
Instanzkonstruktor für eine Klasse angegeben ist, wird automatisch ein leerer Instanzkonstruktor ohne
Parameter bereitgestellt.
Eigenschaften
Eigenschaften sind eine natürliche Erweiterung der Felder. Beide sind benannte Member mit zugeordneten
Typen, und für den Zugriff auf Felder und Eigenschaften wird dieselbe Syntax verwendet. Im Gegensatz zu
Feldern geben Eigenschaften jedoch keine Speicherorte an. Stattdessen verfügen Eigenschaften über
Zugriffsmethoden, die die ausgeführten Anweisungen angeben, wenn ihre Werte gelesen oder geschrieben
werden. Ein get-Accessor liest den Wert. Ein set-Accessor schreibt den Wert.
Eine Eigenschaft wird wie ein Feld deklariert, abgesehen davon, dass die Deklaration nicht auf ein Semikolon,
sondern auf einen get- oder set-Accessor endet, der von den Trennzeichen { und } umschlossen wird. Eine
Eigenschaft, die eine Get-Zugriffsmethode und eine Set-Zugriffsmethode umfasst, ist eine Eigenschaft mit Lese-
/Schreibzugriff. Eine Eigenschaft, die nur eine Get-Zugriffsmethode umfasst, ist eine schreibgeschützte
Eigenschaft. Eine Eigenschaft, die nur eine Set-Zugriffsmethode umfasst, ist eine lesegeschützte Eigenschaft.
Ein get-Accessor entspricht einer Methode ohne Parameter mit einem Rückgabewert des Eigenschaftstyps. Ein
set-Accessor entspricht einer Methode mit einem einzigen Parameter namens „value“ ohne Rückgabetyp. Die
get-Zugriffsmethode berechnet den Wert der Eigenschaft. Die set-Zugriffsmethode stellt einen neuen Wert für
die Eigenschaft bereit. Wenn die Eigenschaft das Ziel einer Zuweisung oder der Operand von ++ oder -- ist,
wird die set-Zugriffsmethode aufgerufen. In anderen Fällen, in denen die Eigenschaft referenziert wird, wird die
get-Zugriffsmethode aufgerufen.
Die MyList<T> -Klasse deklariert die beiden Eigenschaften „ Count “ und „ Capacity “, von denen die eine
schreibgeschützt ist und die andere Lese- und Schreibzugriff besitzt. Im folgenden Beispielcode wird die
Verwendung dieser Eigenschaften veranschaulicht:

MyList<string> names = new MyList<string>();


names.Capacity = 100; // Invokes set accessor
int i = names.Count; // Invokes get accessor
int j = names.Capacity; // Invokes get accessor

Ähnlich wie bei Feldern und Methoden unterstützt C# sowohl Instanzeigenschaften als auch statische
Eigenschaften. Statische Eigenschaften werden mit dem static-Modifizierer, Instanzeigenschaften werden ohne
static-Modifizierer deklariert.
Die Accessors einer Eigenschaft können virtuell sein. Wenn eine Eigenschaftendeklaration einen virtual -,
abstract - oder override -Modifizierer enthält, wird dieser auf den Accessor der Eigenschaft angewendet.

Indexer
Ein Indexer ist ein Member, mit dem Objekte wie ein Array indiziert werden können. Ein Indexer wird wie eine
Eigenschaft deklariert, abgesehen davon, dass der Name des Members this ist, gefolgt von einer
Parameterliste, die zwischen die Trennzeichen [ und ] geschrieben wird. Die Parameter stehen im Accessor
des Indexers zur Verfügung. Ähnlich wie Eigenschaften können Indexer Lese-/Schreibzugriff besitzen,
schreibgeschützt und lesegeschützt sein und virtuelle Accessors verwenden.
Die MyList<T> -Klasse deklariert einen einzigen Indexer mit Lese-/Schreibzugriff, der einen int -Parameter
akzeptiert. Der Indexer ermöglicht es, Instanzen von MyList<T> mit int -Werten zu indizieren. Beispiel:
MyList<string> names = new MyList<string>();
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
for (int i = 0; i < names.Count; i++)
{
string s = names[i];
names[i] = s.ToUpper();
}

Indexer können überladen werden. Eine Klasse kann mehrere Indexer deklarieren, solange sich die Anzahl oder
die Typen ihrer Parameter unterscheiden.
Events
Ein Ereignis ist ein Member, der es einer Klasse oder einem Objekt ermöglicht, Benachrichtigungen
bereitzustellen. Ein Ereignis wird wie ein Feld deklariert, abgesehen davon, dass es ein event -Schlüsselwort
enthält und einen Delegattyp aufweisen muss.
Innerhalb einer Klasse, die einen Ereignismember deklariert, verhält sich das Ereignis wie ein Feld des
Delegattyps (vorausgesetzt, das Ereignis ist nicht abstrakt und deklariert keine Zugriffsmethoden). Das Feld
speichert einen Verweis auf einen Delegaten, der die Ereignishandler repräsentiert, die dem Ereignis
hinzugefügt wurden. Wenn keine Ereignishandler vorhanden sind, ist das Feld null .
Die MyList<T> -Klasse deklariert einen einzigen Ereignismember namens Changed , der angibt, dass der Liste ein
neues Element hinzugefügt wurde. Das Changed-Ereignis wird durch die virtuelle Methode OnChanged
ausgelöst, die zunächst prüft, ob das Ereignis null ist (d.h. nicht über Handler verfügt). Das Auslösen eines
Ereignisses entspricht exakt dem Aufrufen des Delegats, der durch das Ereignis repräsentiert wird. Es gibt keine
speziellen Sprachkonstrukte zum Auslösen von Ereignissen.
Clients reagieren über Ereignishandler auf Ereignisse. Ereignishandler werden unter Verwendung des += -
Operators angefügt und mit dem -= -Operator entfernt. Im folgenden Beispiel wird dem Changed -Ereignis von
MyList<string> ein Ereignishandler hinzugefügt.

class EventExample
{
static int s_changeCount;

static void ListChanged(object sender, EventArgs e)


{
s_changeCount++;
}

public static void Usage()


{
var names = new MyList<string>();
names.Changed += new EventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(s_changeCount); // "3"
}
}

In fortgeschrittenen Szenarios, in denen die zugrunde liegende Speicherung eines Ereignisses gesteuert werden
soll, kann eine Ereignisdeklaration explizit die Zugriffsmethoden add und remove bereitstellen, die der
Zugriffsmethode set einer Eigenschaft ähneln.
Operatoren
Ein Operator ist ein Member, der die Bedeutung der Anwendung eines bestimmten Ausdrucksoperators auf
Instanzen einer Klasse definiert. Es können drei Arten von Operatoren definiert werden: unäre Operatoren,
binäre Operatoren und Konvertierungsoperatoren. Alle Operatoren müssen als public und static deklariert
werden.
Die MyList<T> -Klasse deklariert zwei Operatoren: operator == und operator != . Diese überschriebenen
Operatoren verleihen Ausdrücken eine neue Bedeutung, die diese Operatoren auf MyList -Instanzen anwenden.
Insbesondere die Operatoren definieren die Gleichheit für zwei Instanzen von MyList<T> , indem alle
enthaltenen Objekte mithilfe ihrer Equals -Methoden verglichen werden. Im folgenden Beispiel wird der == -
Operator verwendet, um zwei Instanzen von MyList<int> zu vergleichen.

MyList<int> a = new MyList<int>();


a.Add(1);
a.Add(2);
MyList<int> b = new MyList<int>();
b.Add(1);
b.Add(2);
Console.WriteLine(a == b); // Outputs "True"
b.Add(3);
Console.WriteLine(a == b); // Outputs "False"

Die erste Methode Console.WriteLine gibt True aus, weil die zwei Listen dieselbe Anzahl von Objekten mit
denselben Werten in derselben Reihenfolge enthalten. Wenn MyList<T> nicht operator == definieren würde,
würde die Ausgabe der ersten Console.WriteLine -Methode False lauten, weil a und b auf unterschiedliche
MyList<int> -Instanzen verweisen.

Finalizer
Ein Finalizer ist ein Member, der die erforderlichen Aktionen zum Bereinigen einer Instanz einer Klasse
implementiert. In der Regel ist ein Finalizer erforderlich, um nicht verwaltete Ressourcen freizugeben. Finalizer
können weder Parameter noch Zugriffsmodifizierer aufweisen und können nicht explizit aufgerufen werden. Der
Finalizer für eine Instanz wird bei der Garbagecollection automatisch aufgerufen. Weitere Informationen finden
Sie im Artikel zu Finalizern.
Der Garbage Collector kann weitestgehend selbst über den Zeitpunkt der Objektbereinigung und die
Ausführung der Finalizer entscheiden. Insbesondere der Zeitpunkt für den Aufruf der Finalizer ist nicht
festgelegt, und Finalizer können für beliebige Threads ausgeführt werden. Aus diesen und weiteren Gründen
sollten Klassen Finalizer nur dann implementieren, wenn keine andere Lösung möglich ist.
Die using -Anweisung bietet einen besseren Ansatz für die Objektzerstörung.

Ausdrücke
Ausdrücke bestehen aus Operanden und Operatoren. Die Operatoren eines Ausdrucks geben an, welche
Operationen auf die Operanden angewendet werden. Beispiele für Operatoren sind + , - , * , / und new .
Beispiele für Operanden sind Literale, Felder, lokale Variablen und Ausdrücke.
Wenn ein Ausdruck mehrere Operatoren enthält, steuert die Rangfolge der Operatoren die Reihenfolge, in der
die einzelnen Operatoren ausgewertet werden. Der Ausdruck x + y * z wird z.B. als x + (y * z) ausgewertet,
da der * -Operator Vorrang vor dem + -Operator hat.
Tritt ein Operand zwischen zwei Operatoren mit gleicher Rangfolge auf, steuert die Assoziativität der Operatoren
die Reihenfolge, in der die Vorgänge ausgeführt werden:
Mit Ausnahme der Zuweisungs- und NULL-Sammeloperatoren sind alle binären Operatoren linksassoziativ,
was bedeutet, dass Vorgänge von links nach rechts ausgeführt werden. x + y + z wird beispielsweise als
(x + y) + z ausgewertet.
Die Zuweisungsoperatoren, die NULL-Sammeloperatoren ?? und ??= und der bedingte Operator ?: sind
rechtsassoziativ, d.h., die Operationen werden von rechts nach links ausgeführt. x = y = z wird
beispielsweise als x = (y = z) ausgewertet.
Rangfolge und Assoziativität können mit Klammern gesteuert werden. In x + y * z wird beispielsweise zuerst
y mit z multipliziert und dann das Ergebnis zu x addiert, aber in (x + y) * z werden zunächst x und y
addiert, und dann wird das Ergebnis mit z multipliziert.
Die meisten Operatoren können überladen werden. Das Überladen von Operatoren ermöglicht die Angabe
benutzerdefinierter Operatorimplementierungen für Vorgänge, in denen einer der Operanden oder beide einer
benutzerdefinierten Klasse oder einem benutzerdefinierten Strukturtyp angehören.
C# bietet Operatoren für arithmetische, logische, bitweise und Verschiebungsvorgänge sowie Vergleiche von
Gleichheit und Reihenfolge.
Die vollständige Liste der nach Rangfolgenebene sortierten C#-Operatoren finden Sie unter C#-Operatoren.

Anweisungen
Die Aktionen eines Programms werden mit Anweisungen ausgedrückt. C# unterstützt verschiedene Arten von
Anweisungen, von denen ein Teil als eingebettete Anweisungen definiert ist.
Ein Block ermöglicht, mehrere Anweisungen in Kontexten zu schreiben, in denen eine einzelne Anweisung
zulässig ist. Ein Block besteht aus einer Liste von Anweisungen, die zwischen den Trennzeichen { und }
geschrieben sind.
Deklarationsanweisungen werden verwendet, um lokale Variablen und Konstanten deklarieren.
Ausdrucksanweisungen werden zum Auswerten von Ausdrücken verwendet. Ausdrücke, die als
Anweisungen verwendet werden können, enthalten Methodenaufrufe, Objektzuordnungen mit dem new -
Operator, Zuweisungen mit = und den Verbundzuweisungsoperatoren, Inkrementier- und
Dekrementiervorgänge unter Verwendung des ++ - und -- -Operators und await -Ausdrücke.
Auswahlanweisungen werden verwendet, um eine Anzahl von möglichen Anweisungen für die Ausführung
anhand des Werts eines Ausdrucks auszuwählen. Diese Gruppe enthält die Anweisungen if und switch .
Iterationsanweisungen werden verwendet, um eine eingebettete Anweisung wiederholt auszuführen. Diese
Gruppe enthält die Anweisungen while , do , for und foreach .
Sprunganweisungen werden verwendet, um die Steuerung zu übertragen. Diese Gruppe enthält die
Anweisungen break , continue , goto , throw , return und yield .
Mit der try ... catch -Anweisung werden Ausnahmen abgefangen, die während der Ausführung eines Blocks
auftreten, und mit der try ... finally -Anweisung wird Finalisierungscode angegeben, der immer
ausgeführt wird, unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht.
Mit den Anweisungen checked und unchecked wird der Überlaufüberprüfungs-Kontext für arithmetische
Operationen für Ganzzahltypen und Konvertierungen gesteuert.
Die lock -Anweisung wird verwendet, um die Sperre für gegenseitigen Ausschluss für ein bestimmtes
Objekt abzurufen, eine Anweisung auszuführen und die Sperre aufzuheben.
Die using -Anweisung wird verwendet, um eine Ressource abzurufen, eine Anweisung auszuführen und
dann diese Ressource zu verwerfen.
In der folgenden Liste werden die Arten von Anweisungen aufgeführt, die verwendet werden können:
Deklaration lokaler Variablen
Deklaration lokaler Konstanten
Ausdrucksanweisung
if -Anweisung
switch -Anweisung
while -Anweisung
do -Anweisung
for -Anweisung
foreach -Anweisung
break -Anweisung
continue -Anweisung
goto -Anweisung
return -Anweisung
yield -Anweisung
throw -Anweisungen und try -Anweisungen
checked - und unchecked -Anweisungen
lock -Anweisung
using -Anweisung

ZU RÜ CK W E ITE R
Wichtige Sprachbereiche
04.11.2021 • 8 minutes to read

Arrays, Sammlungen und LINQ


C# und .NET stellen viele verschiedene Sammlungstypen bereit. Die Syntax von Arrays wird von der Sprache
definiert. Generische Sammlungstypen werden im Namespace System.Collections.Generic aufgeführt.
Spezialisierte Sammlungen umfassen System.Span<T> für den Zugriff auf kontinuierlichen Arbeitsspeicher im
Stapelrahmen und System.Memory<T> für den Zugriff auf kontinuierlichen Arbeitsspeicher im verwalteten
Heap. Alle Sammlungen, einschließlich Arrays, Span<T> und Memory<T> teilen ein vereinheitlichendes Prinzip
für die Iteration. Sie verwenden die Schnittstelle System.Collections.Generic.IEnumerable<T>. Dieses
vereinheitlichende Prinzip bedeutet, dass alle Sammlungstypen mit LINQ-Abfragen oder anderen Algorithmen
verwendet werden können. Diese Methoden schreiben Sie mit IEnumerable<T>, und die Algorithmen
funktionieren mit allen Sammlungen.
Arrays
Ein *Array _ ist eine Datenstruktur, die eine Anzahl von Variablen enthält, auf die über berechnete Indizes
zugegriffen wird. Die in einem Array enthaltenen Variablen, die auch als Elemente des Arrays bezeichnet werden,
weisen alle denselben Typ auf. Dieser Typ wird als Elementtyp des Arrays bezeichnet.
Arraytypen sind Verweistypen, und die Deklaration einer Arrayvariablen reserviert Speicher für einen Verweis
auf eine Arrayinstanz. Die tatsächlichen Arrayinstanzen werden unter Verwendung des new -Operators zur
Laufzeit dynamisch erstellt. Der new -Vorgang legt die Länge der neuen Arrayinstanz fest, die dann für die
Lebensdauer der Instanz beibehalten wird. Die Indizes der Arrayelemente reichen von 0 bis Length - 1 . Der
new -Operator initialisiert die Elemente eines Arrays automatisch mit ihren Standardwerten. Dieser lautet z.B.
für alle numerischen Typen 0 und für alle Verweistypen null .
Im folgenden Beispiel wird ein Array aus int -Elementen erstellt. Anschließend wird das Array initialisiert und
die Inhalte des Arrays werden gedruckt.

int[] a = new int[10];


for (int i = 0; i < a.Length; i++)
{
a[i] = i * i;
}
for (int i = 0; i < a.Length; i++)
{
Console.WriteLine($"a[{i}] = {a[i]}");
}

Mit diesem Beispiel wird ein eindimensionales Array erstellt und verwendet. C# unterstützt auch
mehrdimensionale Arrays. Die Anzahl von Dimensionen eines Arraytyps, auch als Rang des Arraytyps
bezeichnet, ist 1 plus die Anzahl von Kommas, die innerhalb der eckigen Klammern des Arraytyps angegeben ist.
Mit dem folgenden Beispiel wird respektive ein eindimensionales, ein zweidimensionales und ein
dreidimensionales Array erstellt.

int[] a1 = new int[10];


int[,] a2 = new int[10, 5];
int[,,] a3 = new int[10, 5, 2];

Das a1 -Array enthält 10 Elemente, das a2 -Array umfasst 50 (10 × 5) Elemente, und das a3 -Array enthält 100
(10 × 5 × 2) Elemente. Ein Array kann einen beliebigen Elementtyp verwenden, einschließlich eines Arraytyps.
Ein Array mit Elementen eines Arraytyps wird auch als Jagged Array bezeichnet, weil die Länge der
Elementarrays nicht identisch sein muss. Im folgenden Beispiel wird ein Array aus int -Arrays zugewiesen:

int[][] a = new int[3][];


a[0] = new int[10];
a[1] = new int[5];
a[2] = new int[20];

In der ersten Zeile wird ein Array mit drei Elementen erstellt, das jeweils den Typ int[] und einen Anfangswert
von null aufweist. In den nachfolgenden Zeilen werden die drei Elemente mit Verweisen auf einzelne
Arrayinstanzen unterschiedlicher Länge initialisiert.
Der new -Operator erlaubt es, die Anfangswerte der Arrayelemente unter Verwendung eines
Arrayinitialisierers anzugeben, bei dem es sich um eine Liste von Ausdrücken zwischen den Trennzeichen {
und } handelt. Mit dem folgenden Beispiel wird ein int[] mit drei Elementen zugewiesen und initialisiert.

int[] a = new int[] { 1, 2, 3 };

Die Länge des Arrays wird von der Anzahl der Ausdrücke zwischen { und } abgeleitet. Die Arrayinitialisierung
kann weiter so verkürzt werden, dass der Arraytyp nicht erneut angegeben werden muss.

int[] a = { 1, 2, 3 };

Die zwei vorherigen Beispiele entsprechen dem folgenden Code:

int[] t = new int[3];


t[0] = 1;
t[1] = 2;
t[2] = 3;
int[] a = t;

Die foreach -Anweisung kann für die Enumeration der Elemente von beliebigen Sammlungen verwendet
werden. Mit dem folgenden Code wird das Array aus dem vorherigen Beispiel aufgezählt:

foreach (int item in a)


{
Console.WriteLine(item);
}

Die foreach -Anweisung nutzt die IEnumerable<T>-Schnittstelle und kann daher mit jeder Sammlung
funktionieren.

Zeichenfolgeninterpolierung
Die Zeichenfolgeninterpolation von C# ermöglicht Ihnen das Formatieren von Zeichenfolgen durch
Definieren von Ausdrücken, deren Ergebnisse in eine Formatzeichenfolge platziert werden. Im folgenden
Beispiel wird die Temperatur für einen jeweiligen Tag aus Wetterdaten ausgegeben:
Console.WriteLine($"The low and high temperature on {weatherData.Date:MM-DD-YYYY}");
Console.WriteLine($" was {weatherData.LowTemp} and {weatherData.HighTemp}.");
// Output (similar to):
// The low and high temperature on 08-11-2020
// was 5 and 30.

Eine interpolierte Zeichenfolge wird mithilfe des $ -Tokens deklariert. Die Zeichenfolgeninterpolation wertet die
Ausdrücke zwischen { und } aus, konvertiert das Ergebnis in string und ersetzt den Text zwischen den
Klammern durch das Zeichenfolgenergebnis des Ausdrucks. Mit : im ersten Ausdruck gibt
{weatherData.Date:MM-DD-YYYY} die Formatzeichenfolge an. Im obigen Beispiel wird festgelegt, dass das Datum
im Format „MM-TT-JJJJ“ ausgegeben werden soll.

Musterabgleich
Die C#-Sprache stellt Musterabgleichsausdrücke bereit, um den Zustand eines Objekts abzufragen und Code
auf Grundlage dieses Zustands auszuführen. Sie können Typen und Werte von Eigenschaften und Feldern
untersuchen, um zu bestimmen, welche Aktion ausgeführt werden soll. Der switch -Ausdruck ist der primäre
Ausdruck für den Musterabgleich.

Delegate und Lambdaausdrücke


Ein Delegattyp stellt Verweise auf Methoden mit einer bestimmten Parameterliste und dem Rückgabetyp dar.
Delegate ermöglichen die Behandlung von Methoden als Entitäten, die Variablen zugewiesen und als Parameter
übergeben werden können. Delegaten ähneln konzeptionell Funktionszeigern, die es in einigen anderen
Sprachen gibt. Im Gegensatz zu Funktionszeigern sind Delegaten objektorientiert und typsicher.
Im folgenden Beispiel wird ein Delegattyp namens Function deklariert und verwendet.

delegate double Function(double x);

class Multiplier
{
double _factor;

public Multiplier(double factor) => _factor = factor;

public double Multiply(double x) => x * _factor;


}

class DelegateExample
{
static double[] Apply(double[] a, Function f)
{
var result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}

public static void Main()


{
double[] a = { 0.0, 0.5, 1.0 };
double[] squares = Apply(a, (x) => x * x);
double[] sines = Apply(a, Math.Sin);
Multiplier m = new Multiplier(2.0);
double[] doubles = Apply(a, m.Multiply);
}
}

Eine Instanz des Delegattyps Function kann auf jede Methode verweisen, die ein double -Argument und einen
double -Wert akzeptiert. Die Apply -Methode wendet eine jeweilige Function (Funktion) auf die Elemente einer
double[] -Methode an. Mit den Ergebnissen wird eine double[] -Methode zurückgegeben. In der Main -
Methode wird Apply verwendet, um drei verschiedene Funktionen auf ein double[] anzuwenden.
Ein Delegat kann entweder auf eine statische Methode verweisen (z.B. Square oder Math.Sin im vorherigen
Beispiel) oder eine Instanzmethode (z.B. m.Multiply im vorherigen Beispiel). Ein Delegat, der auf eine
Instanzmethode verweist, verweist auch auf ein bestimmtes Objekt, und wenn die Instanzmethode durch den
Delegaten aufgerufen wird, wird das Objekt this im Aufruf.
Delegaten können auch mithilfe anonymer Funktionen erstellt werden, bei denen es sich um „Inlinemethoden“
handelt, die bei der Deklaration erstellt werden. Anonyme Funktionen können die lokalen Variablen der
umgebenden Methoden sehen. Im folgenden Beispiel wird keine Klasse erstellt:

double[] doubles = Apply(a, (double x) => x * 2.0);

Ein Delegat weiß nicht und interessiert sich nicht dafür, welche Klasse der Methode er referenziert. Die Methode,
auf die verwiesen wird, muss über die gleichen Parameter und denselben Rückgabetyp wie der Delegat
verfügen.

async/await
C# unterstützt asynchrone Programme mit zwei Schlüsselwörtern: async und await . Sie fügen den async -
Modifizierer zu einer Methodendeklaration hinzu, um zu deklarieren, dass die Methode asynchron ist. Der
await -Operator weist den Compiler an, asynchron auf den Ergebnis zu warten. Die Kontrolle wird wieder dem
Aufrufer überlassen, und die Methode gibt eine Struktur zurück, die den Zustand der asynchronen Arbeitet
verwaltet. In der Regel handelt es sich um eine System.Threading.Tasks.Task<TResult>-Struktur, es kann jedoch
ein beliebiger Typ vorliegen, der das await-Muster unterstützt. Diese Features ermöglichen Ihnen das Schreiben
von Code, der sich wie das synchrone Gegenstück liest, aber asynchron ausgeführt wird. Mit dem folgenden
Code wird beispielsweise die Homepage für die Microsoft-Dokumentation heruntergeladen:

public async Task<int> RetrieveDocsHomePage()


{
var client = new HttpClient();
byte[] content = await client.GetByteArrayAsync("https://docs.microsoft.com/");

Console.WriteLine($"{nameof(RetrieveDocsHomePage)}: Finished downloading.");


return content.Length;
}

In diesem kleinen Beispiel werden die wichtigsten Features für die asynchrone Programmierung
veranschaulicht:
Die Methodendeklaration enthält den Modifizierer async .
Der Text der Methode await enthält die Rückgabe der GetByteArrayAsync -Methode.
Der in der return -Anweisung angegebene Typ stimmt mit dem Typargument in der Task<T> -Deklaration
für die Methode überein. (Eine Methode, die ein Task zurückgibt, würde return -Anweisungen ohne
Argumente verwenden.)

Attributes
Typen, Member und andere Entitäten in einem C#-Programm unterstützen Modifizierer, die bestimmte Aspekte
ihres Verhaltens steuern. Der Zugriff auf eine Methode wird beispielsweise mithilfe der Modifizierer public ,
protected , internal und private kontrolliert. C# generalisiert diese Funktionalität, indem benutzerdefinierte
Typen deklarativer Informationen an eine Programmentität angefügt und zur Laufzeit abgerufen werden
können. Programme geben diese deklarativen Informationen durch Definieren und Verwenden von Attributen
an.
Im folgenden Beispiel wird ein HelpAttribute -Attribut deklariert, dass in Programmentitäten platziert werden
kann, um Links zur zugehörigen Dokumentation bereitzustellen.

public class HelpAttribute : Attribute


{
string _url;
string _topic;

public HelpAttribute(string url) => _url = url;

public string Url => _url;

public string Topic


{
get => _topic;
set => _topic = value;
}
}

Alle Attributklassen werden von der Basisklasse Attribute abgeleitet, die von der .NET-Bibliothek bereitgestellt
wird. Attribute können durch Angabe ihres Namens angewendet werden, zusammen mit beliebigen
Argumenten. Diese müssen in eckigen Klammern genau vor der zugehörigen Deklaration eingefügt werden.
Wenn ein Attributname auf Attribute endet, kann dieser Teil des Namens beim Verweis auf das Attribut
weggelassen werden. Beispielsweise kann HelpAttribute wie folgt verwendet werden.

[Help("https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/features")]
public class Widget
{
[Help("https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/features",
Topic = "Display")]
public void Display(string text) { }
}

In diesem Beispiel wird HelpAttribute an die Widget -Klasse angefügt. Darüber hinaus wird der Display -
Methode in der Klasse ein weiteres HelpAttribute hinzugefügt. Die öffentlichen Konstruktoren einer
Attributklasse steuern die Informationen, die beim Anfügen des Attributs an eine Programmentität angegeben
werden müssen. Zusätzliche Informationen können angegeben werden, indem auf öffentliche Eigenschaften mit
Lese-/Schreibzugriff der Attributklasse verwiesen wird (z.B. wie der obige Verweis auf die Topic -Eigenschaft).
Die durch Attribute definierten Metadaten können zur Laufzeit mittels Reflektion gelesen und bearbeitet werden.
Wenn mithilfe dieser Technik ein bestimmtes Attribut angefordert wird, wird der Konstruktor für die
Attributklasse mit den in der Programmquelle angegebenen Informationen aufgerufen. Die resultierende
Attributinstanz wird zurückgegeben. Wenn zusätzliche Informationen über Eigenschaften bereitgestellt wurden,
werden diese Eigenschaften auf die vorgegebenen Werte festgelegt, bevor die Attributinstanz zurückgegeben
wird.
Das folgende Codebeispiel zeigt, wie die der Widget -Klasse zugeordneten HelpAttribute -Instanzen und die
dazugehörige Display -Methode abgerufen wird.
Type widgetType = typeof(Widget);

object[] widgetClassAttributes = widgetType.GetCustomAttributes(typeof(HelpAttribute), false);

if (widgetClassAttributes.Length > 0)
{
HelpAttribute attr = (HelpAttribute)widgetClassAttributes[0];
Console.WriteLine($"Widget class help URL : {attr.Url} - Related topic : {attr.Topic}");
}

System.Reflection.MethodInfo displayMethod = widgetType.GetMethod(nameof(Widget.Display));

object[] displayMethodAttributes = displayMethod.GetCustomAttributes(typeof(HelpAttribute), false);

if (displayMethodAttributes.Length > 0)
{
HelpAttribute attr = (HelpAttribute)displayMethodAttributes[0];
Console.WriteLine($"Display method help URL : {attr.Url} - Related topic : {attr.Topic}");
}

Erfahren Sie mehr


Weitere Informationen über C# finden Sie in den Tutorials.

ZU RÜ CK
Einführung in C#
04.11.2021 • 2 minutes to read

Willkommen bei der Einführung in die C#-Tutorials. Die Lektionen beginnen mit interaktivem Code, den Sie in
Ihrem Browser ausführen können. Bevor Sie diese interaktiven Lektionen starten, können Sie die Grundlagen
von C# in der C# 101-Videoreihe erlernen.

In der ersten Lektion werden C#-Konzepte anhand kleiner Codeausschnitte erläutert. Sie lernen die Grundlagen
der C#-Syntax kennen und wie Sie mit Datentypen wie Zeichenfolgen, Zahlen und booleschen Werten arbeiten.
Komplett interaktiv, und Ihr Code ist innerhalb weniger Minuten geschrieben und zur Ausführung bereit. Diese
erste Lektionen setzen keine Vorkenntnisse in der Programmierung oder C#-Sprache voraus.
Sie können diese Tutorials in verschiedenen Umgebungen ausprobieren. Die erlernten Konzepte bleiben gleich.
Es gibt jedoch Unterschiede bei den Umgebungen:
Im Browser auf der Microsoft Dokumentation-Plattform: Hier wird ein ausführbares C#-Codefenster in die
Dokumentationsseiten eingebunden. Sie schreiben C# Code im Browser und führen ihn dort aus.
Auf Microsoft Learn: Dieser Lernpfad enthält mehrere Module, die die Grundlagen von C# vermitteln.
In Jupyter auf Binder: Sie können mit C#-Code in einem Jupyter-Notebook auf Binder experimentieren.
Auf einem lokalen Computer: Sie können das .NET Core SDK herunterladen und die Programme auf Ihrem
Computer entwickeln.
Alle einführenden Tutorials, die auf die „Hallo Welt“-Lektion folgen, sind über einen Onlinebrowser oder in Ihrer
eigenen lokalen Entwicklungsumgebung verfügbar. Am Ende jedes Tutorials entscheiden Sie, ob Sie mit der
nächsten Lektion online oder auf Ihrem eigenen Computer fortfahren möchten. Es sind Links verfügbar, die
Ihnen helfen, Ihre Umgebung einzurichten und mit dem nächsten Tutorial auf Ihrem Computer fortzufahren.

Hallo Welt
Im Hallo Welt-Tutorial erstellen Sie das einfachste C#-Programm. Sie untersuchen den string -Typ und lernen,
mit Text zu arbeiten. Sie können auch den Pfad auf Microsoft Learn oder Jupyter auf Binder verwenden.

Zahlen in C#
Im Tutorial Zahlen in C# erfahren Sie, wie Computer Zahlen speichern und wie Sie Berechnungen mit
verschiedenen Zahlentypen ausführen. Sie lernen die Grundlagen der Rundung und das Ausführen
mathematischer Berechnungen mithilfe von C#. Dieses Tutorial ist auch für die lokale Ausführung auf Ihrem
Computer verfügbar.
Für dieses Tutorial wird vorausgesetzt, dass Sie die Lektion Hallo Welt abgeschlossen haben.

Verzweigungen und Schleifen


Im Tutorial Branches und Schleifen werden die Grundlagen der Auswahl verschiedener Codepfadausführungen
auf Basis der in Variablen gespeicherten Werte erläutert. Sie lernen die Grundlagen der Ablaufsteuerung
kennen, von der abhängt, wie Programme Entscheidungen treffen und verschiedene Aktionen auswählen.
Dieses Tutorial ist auch für die lokale Ausführung auf Ihrem Computer verfügbar.
In diesem Tutorial wird vorausgesetzt, dass Sie die Lektionen Hallo Welt und Zahlen in C# abgeschlossen haben.
Listensammlung
Die Lektion Listensammlung bietet Ihnen einen Überblick über den Listensammlungstyp, in dem
Datensequenzen speichert werden. Sie erfahren, wie Sie Elemente hinzufügen und entfernen, nach Elementen
suchen und die Listen sortieren. Sie werden verschiedene Arten von Listen erforschen. Dieses Tutorial ist auch
für die lokale Ausführung auf Ihrem Computer verfügbar.
Für dieses Tutorial wird vorausgesetzt, dass Sie die oben aufgeführten Lektionen abgeschlossen haben.

101 LINQ-Beispiele
Für dieses Beispiel ist das globale dotnet-try-Tool erforderlich. Sobald Sie das Tool installiert und das Repository
try-samples geklont haben, können Sie LINQ (Language Integrated Query) mithilfe von 101 Beispielen lernen,
die Sie interaktiv ausführen können. Sie können unterschiedliche Möglichkeiten zum Abfragen, Untersuchen
und Transformieren von Datensequenzen untersuchen.
Einrichten Ihrer lokalen Umgebung
04.11.2021 • 2 minutes to read

Der erste Schritt beim Ausführen eines Tutorials auf Ihrem Computer besteht darin, eine
Entwicklungsumgebung einzurichten. Wählen Sie eine der folgenden Alternativen:
Wie Sie die .NET CLI und den Text- oder Code-Editor Ihrer Wahl verwenden, erfahren Sie im .NET-Tutorial
Hallo Welt in 10 Minuten. Im Tutorial finden Sie eine Anleitung zum Einrichten Ihrer Entwicklungsumgebung
unter Windows, Linux oder macOS.
Um die .NET CLI und Visual Studio Code zu verwenden, installieren Sie das .NET SDK und Visual Studio Code.
Informationen zur Verwendung von Visual Studio 2019 finden Sie unter Tutorial: Erstellen einer einfachen
C#-Konsolen-App in Visual Studio.

Grundlegender Anwendungsentwicklungsworkflow
Bei den Anweisungen in diesen Tutorials wird davon ausgegangen, dass Sie die .NET CLI zum Entwerfen,
Erstellen und Ausführen von Anwendungen verwenden. Sie verwenden die folgenden Befehle:
dotnet new erstellt eine Anwendung. Mit diesem Befehl werden die für Ihre Anwendung erforderlichen
Dateien und Objekte generiert. Bei der Einführung in C#-Tutorials wird immer der Anwendungstyp console
verwendet. Wenn Sie mit den Grundlagen vertraut sind, können Sie mit den Anwendungstypen fortfahren.
dotnet build erstellt die ausführbare Datei.
dotnet run führt die ausführbare Datei aus.

Wenn Sie Visual Studio 2019 für diese Tutorials verwenden, wählen Sie eine Visual Studio-Menüauswahl aus,
wenn Sie von einem Tutorial aufgefordert werden, einen der folgenden CLI-Befehle auszuführen:
Datei > Neu > Projekt erstellt eine Anwendung.
Die Console Application -Projektvorlage wird empfohlen.
Sie erhalten die Möglichkeit, ein Zielframework anzugeben. Die folgenden Tutorials funktionieren am
besten für das Ziel .NET 5 oder höher.
Erstellen > Lösung erstellen erstellt die ausführbare Datei.
Debuggen > Ohne Debuggen star ten führt die ausführbare Datei aus.

Auswählen Ihres Tutorials


Sie können mit einem der folgenden Tutorials beginnen:

Zahlen in C#
Im Tutorial Zahlen in C# erfahren Sie, wie Computer Zahlen speichern und wie Sie Berechnungen mit
verschiedenen Zahlentypen ausführen. Sie lernen die Grundlagen der Rundung und das Ausführen
mathematischer Berechnungen mithilfe von C#.
Für dieses Tutorial wird vorausgesetzt, dass Sie die Hallo Welt-Lektion abgeschlossen haben.

Verzweigungen und Schleifen


Im Tutorial Branches und Schleifen werden die Grundlagen der Auswahl verschiedener Codepfadausführungen
auf Basis der in Variablen gespeicherten Werte erläutert. Sie lernen die Grundlagen der Ablaufsteuerung
kennen, von der abhängt, wie Programme Entscheidungen treffen und verschiedene Aktionen auswählen.
In diesem Tutorial wird vorausgesetzt, dass Sie die Lektionen Hallo Welt und Zahlen in C# abgeschlossen haben.

Listensammlung
Die Lektion Listensammlung bietet Ihnen einen Überblick über den Listensammlungstyp, in dem
Datensequenzen speichert werden. Sie erfahren, wie Sie Elemente hinzufügen und entfernen, nach Elementen
suchen und die Listen sortieren. Sie werden verschiedene Arten von Listen erforschen.
Für dieses Tutorial wird vorausgesetzt, dass Sie die oben aufgeführten Lektionen abgeschlossen haben.
Bearbeiten von Ganzzahlen und Gleitkommazahlen
in C#
04.11.2021 • 10 minutes to read

Dieses interaktive Tutorial erläutert die numerischen Typen in C#. Sie schreiben einen Teil des Codes,
kompilieren diesen und führen ihn aus. Das Tutorial enthält eine Reihe von Lektionen, in denen Zahlen und
mathematische Vorgänge in C# untersucht werden. In diesen Lektionen lernen Sie die Grundlagen der
Programmiersprache C# kennen.

Voraussetzungen
Für dieses Tutorial wird vorausgesetzt, dass Sie einen Computer für die lokale Entwicklung eingerichtet haben.
Unter Windows, Linux oder macOS können Sie die .NET CLI zum Programmieren, Erstellen und Ausführen von
Anwendungen verwenden. Auf dem Mac oder unter Windows können Sie Visual Studio 2019 verwenden.
Setupanweisungen finden Sie unter Einführung in .NET-Entwicklungstools.

Erkunden von arithmetischen Operationen mit ganzen Zahlen


Erstellen Sie ein Verzeichnis mit dem Namen numbers-quickstart. Legen Sie es als das aktuelle Verzeichnis fest,
und führen Sie den folgenden Befehl aus:

dotnet new console -n NumbersInCSharp -o .

IMPORTANT
Die C#-Vorlagen für .NET 6 verwenden Anweisungen der obersten Ebene. Ihre Anwendung passt möglicherweise nicht
zum Code in diesem Artikel, wenn Sie bereits ein Upgrade auf die .NET 6-Vorschauversionen durchgeführt haben. Weitere
Informationen finden Sie im Artikel Neue C#-Vorlagen generieren Anweisungen auf oberster Ebene.
Das .NET 6 SDK fügt auch eine Reihe impliziter global using -Anweisungen für Projekte hinzu, die die folgenden SDKs
verwenden:
Microsoft.NET.Sdk
Microsoft.NET.Sdk.Web
Microsoft.NET.Sdk.Worker
Diese impliziten global using -Anweisungen enthalten die gängigsten Namespaces für den Projekttyp.

Öffnen Sie Program.cs in Ihrem bevorzugten Editor, und ersetzen Sie den Inhalt der Datei durch folgenden Code:

using System;

int a = 18;
int b = 6;
int c = a + b;
Console.WriteLine(c);

Führen Sie diesen Code aus, indem Sie dotnet run in Ihr Befehlsfenster eingeben.
Sie haben eine der grundlegenden arithmetischen Operationen mit ganzen Zahlen kennengelernt. Der int -Typ
steht für integer , d. h. eine Null oder eine positive oder negative ganze Zahl. Sie verwenden zum Addieren das
+ -Symbol. Zu den anderen häufig verwendeten arithmetischen Operationen für ganze Zahlen zählen Folgende:

- zur Subtraktion
* zur Multiplikation
/ zur Division
Erkunden Sie zunächst die anderen Operationen. Fügen Sie diese Zeilen nach der Zeile hinzu, die den Wert von
c schreibt:

// subtraction
c = a - b;
Console.WriteLine(c);

// multiplication
c = a * b;
Console.WriteLine(c);

// division
c = a / b;
Console.WriteLine(c);

Führen Sie diesen Code aus, indem Sie dotnet run in Ihr Befehlsfenster eingeben.
Wenn Sie möchten, können Sie auch experimentieren, indem Sie mehrere arithmetische Operationen in dieselbe
Zeile schreiben. Testen Sie zum Beispiel c = a + b - 12 * 17; . Das Kombinieren von Variablen und konstanten
Zahlen ist erlaubt.

TIP
Bei Ihren ersten Schritten mit C# (oder einer anderen Programmiersprache) kann es zu Fehlern kommen, wenn Sie Codes
schreiben. Der Compiler findet diese Fehler und meldet diese. Sollten Fehlermeldungen vorliegen, sehen Sie sich den
Beispielcode und den Code in Ihrem Fenster an, um festzustellen, was korrigiert werden muss. Durch diese Übung lernen
Sie die Struktur eines C#-Codes kennen.

Sie haben den ersten Schritt abgeschlossen. Bevor Sie mit dem nächsten Abschnitt beginnen, verschieben wir
den aktuellen Code in eine separate Methode. Eine Methode ist eine Reihe von Anweisungen, die gruppiert
werden und einen Namen erhalten. Sie rufen eine Methode auf, indem Sie den Namen der Methode gefolgt von
() schreiben. Wenn Sie Ihren Code in Methoden organisieren, ist der Einstieg in die Arbeit mit einem neuen
Beispiel einfacher. Anschließend sollte der Code wie folgt aussehen:
using System;

WorkWithIntegers();

void WorkWithIntegers()
{
int a = 18;
int b = 6;
int c = a + b;
Console.WriteLine(c);

// subtraction
c = a - b;
Console.WriteLine(c);

// multiplication
c = a * b;
Console.WriteLine(c);

// division
c = a / b;
Console.WriteLine(c);
}

Die Zeile WorkWithIntegers(); ruft die Methode auf. Im folgenden Code wird die Methode deklariert und
definiert.

Erkunden der Reihenfolge der Operationen


Kommentieren Sie den Aufruf von WorkingWithIntegers() aus. Dadurch wird die Ausgabe weniger überladen,
wenn Sie in diesem Abschnitt arbeiten:

//WorkWithIntegers();

Mit // wird ein Kommentar in C# begonnen. Kommentare sind Texte, die Sie in Ihrem Quellcode beibehalten,
jedoch nicht als Code ausführen möchten. Der Compiler generiert keinen ausführbaren Code über die
Kommentare. Da WorkWithIntegers() eine Methode ist, müssen Sie nur eine Zeile auskommentieren.
Die Programmiersprache C# definiert anhand von Regeln, die Sie aus der Mathematik kennen, die Rangfolge
verschiedener arithmetischer Operationen. Multiplikation und Division haben gegenüber der Addition und
Subtraktion Vorrang. Überprüfen Sie dies, indem Sie nach dem Aufruf von WorkWithIntegers() den folgenden
Code hinzufügen und dotnet run ausführen:

int a = 5;
int b = 4;
int c = 2;
int d = a + b * c;
Console.WriteLine(d);

Die Ausgabe zeigt, dass vor der Addition die Multiplikation ausgeführt wurde.
Sie können eine andere Operationsreihenfolge erzwingen, indem Sie die Operation bzw. die Operationen, die
zuerst ausgeführt werden soll bzw. sollen, mit Klammern umschließen. Fügen Sie die folgenden Zeilen hinzu,
und führen Sie sie aus:
d = (a + b) * c;
Console.WriteLine(d);

Machen Sie sich damit vertraut, indem Sie viele verschiedene Operationen kombinieren. Fügen Sie in etwa die
folgenden Zeilen hinzu. Testen Sie erneut dotnet run .

d = (a + b) - 6 * c + (12 * 4) / 3 + 12;
Console.WriteLine(d);

Vielleicht haben Sie bereits ein interessantes Verhalten bei den ganzen Zahlen bemerkt. Bei der Division ganzer
Zahlen kommt immer ein ganzzahliges Ergebnis heraus, selbst wenn Sie als Ergebnis einen Dezimal- oder
Bruchteil erwarten würden.
Wenn Sie dieses Verhalten noch nicht beobachtet haben, testen Sie den folgenden Code:

int e = 7;
int f = 4;
int g = 3;
int h = (e + f) / g;
Console.WriteLine(h);

Geben Sie erneut dotnet run ein, um die Ergebnisse anzuzeigen.


Bevor Sie fortfahren, kopieren Sie den gesamten Code, den Sie in diesem Abschnitt geschrieben haben, und
fügen ihn in eine neue Methode ein. Rufen Sie die neue Methode OrderPrecedence auf. Ihr Code sollte in etwa
wie folgt aussehen:
using System;

// WorkWithIntegers();
OrderPrecedence();

void WorkWithIntegers()
{
int a = 18;
int b = 6;
int c = a + b;
Console.WriteLine(c);

// subtraction
c = a - b;
Console.WriteLine(c);

// multiplication
c = a * b;
Console.WriteLine(c);

// division
c = a / b;
Console.WriteLine(c);
}

void OrderPrecedence()
{
int a = 5;
int b = 4;
int c = 2;
int d = a + b * c;
Console.WriteLine(d);

d = (a + b) * c;
Console.WriteLine(d);

d = (a + b) - 6 * c + (12 * 4) / 3 + 12;
Console.WriteLine(d);

int e = 7;
int f = 4;
int g = 3;
int h = (e + f) / g;
Console.WriteLine(h);
}

Erkunden der Genauigkeit und Grenzwerte ganzer Zahlen


Im vorherigen Beispiel haben Sie gesehen, dass das Ergebnis bei der Division ganzer Zahlen abgeschnitten wird.
Sie erhalten den Restwert remainder mithilfe des Modulo -Operators, dem % -Zeichen. Versuchen Sie es nach
dem Methodenaufruf von OrderPrecedence() mit folgendem Code:

int a = 7;
int b = 4;
int c = 3;
int d = (a + b) / c;
int e = (a + b) % c;
Console.WriteLine($"quotient: {d}");
Console.WriteLine($"remainder: {e}");

Der integer-C#-Typ unterscheidet sich noch in einem weiteren Punkt von einer mathematischen ganzen Zahl:
Der int -Typ ist mit minimalen und maximalen Grenzwerten versehen. Fügen Sie diesen Code hinzu, um die
jeweiligen Grenzwerte zu sehen:

int max = int.MaxValue;


int min = int.MinValue;
Console.WriteLine($"The range of integers is {min} to {max}");

Wenn bei einer Berechnung ein Wert herauskommt, der diese Grenzwerte überschreitet, liegt eine Unterlauf-
oder Überlaufbedingung vor. Die Antwort gibt dann den Bereich der Grenzwerte an. Fügen Sie die folgenden
zwei Zeilen hinzu, um ein Beispiel anzuzeigen:

int what = max + 3;


Console.WriteLine($"An example of overflow: {what}");

Beachten Sie, dass die Antwort sehr nah an der minimalen (negativen) ganzen Zahl liegt. Sie entspricht min + 2 .
Die Additionsoperation hat die zulässigen Werte für ganze Zahlen überlaufen . Die Antwort enthält eine sehr
große negative Zahl, da ein Überlauf den größtmöglichen ganzzahligen Wert bis zum kleinstmöglichen Wert
umschließt.
Wenn der int -Typ nicht Ihren Anforderungen entspricht, so gibt es verschiedene numerische Typen mit
anderen Grenzwerten und Genauigkeitsgraden, die Sie verwenden können. Werfen wir im Folgenden einmal
einen Blick auf diese anderen Typen. Bevor Sie mit dem nächsten Abschnitt beginnen, verschieben Sie den in
diesem Abschnitt geschriebenen Code in eine separate Methode. Nennen Sie es TestLimits .

Arbeiten mit dem Double-Typ


Der numerische Typ double stellt eine Gleitkommazahl mit doppelter Genauigkeit dar. Falls Ihnen diese
Benennungen nichts sagen, beachten Sie die folgenden Erläuterungen: Eine Gleitkommazahl wird verwendet,
um sehr große oder sehr kleine Zahlen, die keine ganzen Zahlen sind, darzustellen. Doppelte Genauigkeit ist
ein relativer Begriff, der die Anzahl der binären Ziffern beschreibt, die zum Speichern des Werts verwendet
werden. Zahlen mit doppelter Genauigkeit besitzen die doppelte Anzahl von binären Ziffern wie Zahlen mit
einfacher Genauigkeit . Auf Bei modernen Computern werden häufiger Zahlen mit doppelter Genauigkeit
verwendet statt mit einfacher Genauigkeit. Zahlen mit einfacher Genauigkeit werden mit dem Schlüsselwort
float deklariert. Sehen wir uns dies einmal genauer an. Fügen Sie den folgenden Code hinzu, und zeigen Sie
das Ergebnis an:

double a = 5;
double b = 4;
double c = 2;
double d = (a + b) / c;
Console.WriteLine(d);

Beachten Sie, dass die Antwort die Dezimalzahl des Quotienten enthält. Testen Sie einen etwas komplizierteren
Ausdruck mit Werten vom Typ „double“:

double e = 19;
double f = 23;
double g = 8;
double h = (e + f) / g;
Console.WriteLine(h);

Der Bereich eines Werts vom Typ „double“ ist weitaus größer als bei ganzzahligen Werten. Fügen Sie den
folgenden Code unter dem Code hinzu, den Sie bereits geschrieben haben:
double max = double.MaxValue;
double min = double.MinValue;
Console.WriteLine($"The range of double is {min} to {max}");

Diese Werte werden in der wissenschaftlichen Schreibweise ausgegeben. Die Zahl links von E ist die Mantisse.
Die Zahl rechts ist der Exponent als Potenz von 10. Wie bei Dezimalzahlen in der Mathematik können double-
Werte in C# Rundungsfehler aufweisen. Testen Sie den folgenden Code:

double third = 1.0 / 3.0;


Console.WriteLine(third);

Denken Sie daran, dass 0.3 Periode nicht exakt 1/3 entspricht.
Übung
Testen Sie für den double -Typ andere Berechnungen mit großen und kleinen Zahlen sowie mit Multiplikation
und Division. Testen Sie kompliziertere Berechnungen. Nachdem Sie sich nun einige Zeit lang mit der Übung
auseinander gesetzt haben, kopieren Sie den von Ihnen geschriebenen Code, und fügen Sie ihn in eine neue
Methode ein. Vergeben Sie einen Namen für die neue Methode WorkWithDoubles .

Arbeiten mit Dezimaltypen


Sie haben die grundlegenden numerischen Typen in C# – „integer “ und „double“ – kennengelernt. Es gibt einen
weiteren Typ, den Sie kennen sollten: den decimal -Typ. Der decimal -Typ weist einen kleineren Bereich als
double auf, aber eine höhere Genauigkeit. Sehen wir uns das einmal genauer an:

decimal min = decimal.MinValue;


decimal max = decimal.MaxValue;
Console.WriteLine($"The range of the decimal type is {min} to {max}");

Beachten Sie, dass der Bereich kleiner ist als beim double -Typ. Sie können sehen, dass die Genauigkeit beim Typ
„decimal“ höher ist, wenn Sie den folgenden Code testen:

double a = 1.0;
double b = 3.0;
Console.WriteLine(a / b);

decimal c = 1.0M;
decimal d = 3.0M;
Console.WriteLine(c / d);

Mit dem Suffix M neben einer Zahl geben Sie an, dass eine Konstante den decimal -Typ verwenden soll.
Andernfalls nimmt der Compiler den Typ double an.

NOTE
Der Buchstabe M wurde als visuell eindeutigster Buchstabe zur Unterscheidung zwischen den Schlüsselwörtern double
und decimal ausgewählt.

Beachten Sie, dass der aus dieser arithmetischen Operation resultierende Wert vom Typ „decimal“ rechts neben
dem Dezimalpunkt mehr Ziffern enthält.
Übung
Nachdem Sie nun die verschiedenen numerischen Typen kennengelernt haben, schreiben Sie Code, der den
Flächeninhalt eines Kreises mit einem Radius von 2,5 cm berechnet. Denken Sie daran, dass der Flächeninhalt
eines Kreises durch das Quadrat des Radius multipliziert mit Pi gebildet wird. Hinweis: .NET bietet eine
Konstante für Pi (Math.PI), die Sie für die Berechnung dieses Werts verwenden können. Math.PI, wie alle im
System.Math -Namespace deklarierten Konstanten, ist ein double -Wert. Aus diesem Grund sollten Sie double
anstelle von decimal -Werten für diese Aufgabe verwenden.
Sie sollten eine Antwort zwischen 19 und 20 erhalten. Sie können Ihre Antwort anhand des fertig gestellten
Beispielcodes auf GitHub prüfen.
Wenn Sie möchten, testen Sie andere Formeln.
Sie haben den Schnellstart „Zahlen in C#“ abgeschlossen. Sie können mit dem Schnellstart Branches und
Schleifen in Ihrer eigenen Entwicklungsumgebung fortfahren.
Weitere Informationen zu Zahlen in C# finden Sie auch in folgenden Artikeln:
Integrale numerische Typen
Numerische Gleitkommatypen
Integrierte numerischer Konvertierungen
Bedingungen für Verzweigungs- und
Schleifenanweisungen
04.11.2021 • 10 minutes to read

In diesem Tutorial erfahren Sie, wie Sie Code schreiben, der Variablen untersucht und basierend auf diesen
Variablen den Ausführungspfad ändert. Sie schreiben einen C#-Code und sehen dort die Ergebnisse der
Kompilierung und Ausführung Ihres Codes. Dieses Tutorial enthält eine Reihe von Lektionen, in denen
Verzweigungs- und Schleifenkonstrukte in C# erkundet werden. In diesen Lektionen lernen Sie die Grundlagen
der Programmiersprache C# kennen.

Voraussetzungen
Für dieses Tutorial wird vorausgesetzt, dass Sie einen Computer für die lokale Entwicklung eingerichtet haben.
Unter Windows, Linux oder macOS können Sie die .NET CLI zum Programmieren, Erstellen und Ausführen von
Anwendungen verwenden. Auf dem Mac und unter Windows können Sie Visual Studio 2019 verwenden.
Setupanweisungen finden Sie unter Einführung in .NET-Entwicklungstools.

Treffen von Entscheidungen mithilfe der if -Anweisung


Erstellen Sie ein Verzeichnis mit dem Namen branches-tutorial. Legen Sie dieses als das aktuelle Verzeichnis fest,
und führen Sie den folgenden Befehl aus:

dotnet new console -n BranchesAndLoops -o .

IMPORTANT
Die C#-Vorlagen für .NET 6 verwenden Anweisungen der obersten Ebene. Ihre Anwendung passt möglicherweise nicht
zum Code in diesem Artikel, wenn Sie bereits ein Upgrade auf die .NET 6-Vorschauversionen durchgeführt haben. Weitere
Informationen finden Sie im Artikel Neue C#-Vorlagen generieren Anweisungen auf oberster Ebene.
Das .NET 6 SDK fügt auch eine Reihe impliziter global using -Anweisungen für Projekte hinzu, die die folgenden SDKs
verwenden:
Microsoft.NET.Sdk
Microsoft.NET.Sdk.Web
Microsoft.NET.Sdk.Worker
Diese impliziten global using -Anweisungen enthalten die gängigsten Namespaces für den Projekttyp.

Dieser Befehl erstellt im aktuellen Verzeichnis eine neue .NET-Konsolenanwendung. Öffnen Sie Program.cs in
Ihrem bevorzugten Editor, und ersetzen Sie den Inhalt durch folgenden Code:

using System;

int a = 5;
int b = 6;
if (a + b > 10)
Console.WriteLine("The answer is greater than 10.");
Testen Sie diesen Code, indem Sie dotnet run in Ihr Konsolenfenster eingeben. Es sollte folgende Meldung in
Ihrer Konsole angezeigt werden: „Die Antwort ist größer als 10.“ Ändern Sie die Deklaration von b so, dass die
Summe kleiner als 10 ist:

int b = 3;

Geben Sie erneut dotnet run ein. Da die Antwort kleiner als 10 ist, wird nichts ausgegeben. Die von Ihnen
getestete Bedingung ist falsch. Es ist kein Code auszuführen, da Sie lediglich eine der möglichen
Verzweigungen für eine if -Anweisung geschrieben haben: die true-Verzweigung.

TIP
Bei Ihren ersten Schritten mit C# (oder einer anderen Programmiersprache) kann es zu Fehlern kommen, wenn Sie Codes
schreiben. Der Compiler findet und meldet die Fehler. Sehen Sie sich die Fehlerausgabe und den Code, der den Fehler
erzeugt hat, genau an. Der Compilerfehler kann Ihnen in der Regel helfen, das Problem zu erkennen.

Das erste Beispiel veranschaulicht die Vorteile von if -Anweisungen und Boolean-Typen. Ein boolean-Typ ist
eine Variable, die einen der folgenden zwei Werte enthalten kann: true oder false . In C# ist ein besonderer
Typ für boolesche Variablen, bool , definiert. Die if -Anweisung überprüft den Wert eines bool -Typs. Wenn
der Wert true lautet, wird die nach if folgende Anweisung ausgeführt. Andernfalls wird sie übersprungen.
Dieser Vorgang zum Überprüfen von Bedingungen und Ausführen von Anweisungen basierend auf diesen
Bedingungen ist wirkungsvoll.

Kombinieren von if- und else-Anweisungen


Um einen anderen Code in den true- und false-Verzweigungen auszuführen, erstellen Sie eine else -
Verzweigung, die ausgeführt wird, wenn die Bedingung falsch ist. Versuchen Sie es mit einer else -
Verzweigung. Fügen Sie die letzten beiden Zeilen im nachfolgenden Code hinzu (die ersten vier sollten bereits
vorhanden sein):

int a = 5;
int b = 3;
if (a + b > 10)
Console.WriteLine("The answer is greater than 10");
else
Console.WriteLine("The answer is not greater than 10");

Die Anweisung, die nach dem Schlüsselwort else folgt, wird nur ausgeführt, wenn die zu testende Bedingung
false lautet. Wenn Sie if und else mit booleschen Bedingungen kombinieren, müssen Sie sowohl eine
true - als auch eine false -Bedingung verarbeiten.

IMPORTANT
Der Einzug unter den if - und else -Anweisungen dient zur besseren Lesbarkeit. In der Programmiersprache C#
werden Einzüge oder Leerräume nicht berücksichtigt. Die Anweisung nach dem Schlüsselwort if bzw. else wird
basierend auf der Bedingung ausgeführt. Alle Beispiele in diesem Tutorial folgen der gängigen Vorgehensweise, Zeilen
basierend auf der Ablaufsteuerung von Anweisungen mit einem Einzug zu versehen.

Da Einzüge nicht relevant sind, müssen Sie mit { und } angeben, dass Sie mehr als eine Anweisung im
Rahmen des bedingt ausgeführten Blocks verwenden möchten. C#-Programmierer verwenden solche
geschweifte Klammern in der Regel bei allen if - und else -Anweisungen. Das folgende Beispiel ist identisch
mit dem Inhalt, den Sie erstellt haben. Ändern Sie den obigen Code dahingehend, dass er mit dem folgenden
Code übereinstimmt:

int a = 5;
int b = 3;
if (a + b > 10)
{
Console.WriteLine("The answer is greater than 10");
}
else
{
Console.WriteLine("The answer is not greater than 10");
}

TIP
Im restlichen Tutorial enthalten alle Codebeispiele geschweifte Klammern gemäß den allgemein gültigen Vorgehensweisen.

Sie können kompliziertere Bedingungen testen. Fügen Sie den folgenden Code unter dem Code hinzu, den Sie
bereits geschrieben haben:

int c = 4;
if ((a + b + c > 10) && (a == b))
{
Console.WriteLine("The answer is greater than 10");
Console.WriteLine("And the first number is equal to the second");
}
else
{
Console.WriteLine("The answer is not greater than 10");
Console.WriteLine("Or the first number is not equal to the second");
}

Die == -Symboltests für Gleichheit. Die Verwendung von == unterscheidet den Test auf Gleichheit von der
Zuweisung, die Sie in a = 5 gesehen haben.
Das Zeichen && steht für „and“. Es bedeutet, dass beide Bedingungen „true“ lauten müssen, damit die
Anweisung in der true-Verzweigung ausgeführt wird. Diese Beispiele zeigen außerdem, dass Sie in jeder
bedingten Verzweigung mehrere Anweisungen verwenden können, sofern Sie sie mit { und } umschließen.
Sie können auch || für „or “ verwenden. Fügen Sie den folgenden Code unter dem Code hinzu, den Sie bereits
geschrieben haben:

if ((a + b + c > 10) || (a == b))


{
Console.WriteLine("The answer is greater than 10");
Console.WriteLine("Or the first number is equal to the second");
}
else
{
Console.WriteLine("The answer is not greater than 10");
Console.WriteLine("And the first number is not equal to the second");
}

Ändern Sie die Werte von a , b und c , und wechseln Sie zwischen && und || , um sie zu untersuchen. So
werden Sie besser verstehen, wie die Operatoren && und || funktionieren.
Sie haben den ersten Schritt abgeschlossen. Bevor Sie mit dem nächsten Abschnitt beginnen, verschieben wir
den aktuellen Code in eine separate Methode. Dies erleichtert das Arbeiten mit einem neuen Beispiel. Fügen Sie
den vorhandenen Code in eine Methode namens ExploreIf() ein. Rufen Sie sie am Anfang Ihres Programms
auf. Nach diesen Änderungen sollte der Code wie folgt aussehen:

using System;

ExploreIf();

void ExploreIf()
{
int a = 5;
int b = 3;
if (a + b > 10)
{
Console.WriteLine("The answer is greater than 10");
}
else
{
Console.WriteLine("The answer is not greater than 10");
}

int c = 4;
if ((a + b + c > 10) && (a > b))
{
Console.WriteLine("The answer is greater than 10");
Console.WriteLine("And the first number is greater than the second");
}
else
{
Console.WriteLine("The answer is not greater than 10");
Console.WriteLine("Or the first number is not greater than the second");
}

if ((a + b + c > 10) || (a > b))


{
Console.WriteLine("The answer is greater than 10");
Console.WriteLine("Or the first number is greater than the second");
}
else
{
Console.WriteLine("The answer is not greater than 10");
Console.WriteLine("And the first number is not greater than the second");
}
}

Kommentieren Sie den Aufruf von ExploreIf() aus. Dadurch wird die Ausgabe weniger überladen, wenn Sie in
diesem Abschnitt arbeiten:

//ExploreIf();

Mit // wird ein Kommentar in C# begonnen. Kommentare sind Texte, die Sie in Ihrem Quellcode beibehalten,
jedoch nicht als Code ausführen möchten. Der Compiler generiert keinen ausführbaren Code über die
Kommentare.

Wiederholen von Vorgängen durch Schleifen


In diesem Abschnitt verwenden Sie Schleifen , um Anweisungen zu wiederholen. Fügen Sie diesen Code nach
dem Aufruf von ExploreIf hinzu:
int counter = 0;
while (counter < 10)
{
Console.WriteLine($"Hello World! The counter is {counter}");
counter++;
}

Die while -Anweisung prüft eine Bedingung und führt die Anweisung oder der Anweisungsblock nach while
aus. Es wiederholt die Überprüfung der Bedingung und die Ausführung dieser Anweisungen, bis die Bedingung
„false“ lautet.
In diesem Beispiel kommt ein weiterer neuer Operator vor. Das ++ -Zeichen nach der counter -Variable ist der
increment -Operator. Er erhöht den Wert von counter um 1 und speichert diesen Wert in der Variable
counter .

IMPORTANT
Stellen Sie sicher, dass die Schleifenbedingung while zu „false“ wechselt, nachdem Sie den Code ausgeführt haben.
Erstellen Sie anderenfalls eine Endlosschleife , durch die das Programm niemals beendet wird. Das wird in diesem Beispiel
nicht gezeigt, weil Sie die programmseitige Verwendung von STRG+C oder anderen Mitteln unterbinden müssen.

Die while -Schleife testet die Bedingung, bevor der Code nach while ausgeführt wird. Die do ... while -
Schleife führt den Code zuerst aus und überprüft anschließend die Bedingung. Die do while-Schleife wird im
folgenden Code gezeigt:

int counter = 0;
do
{
Console.WriteLine($"Hello World! The counter is {counter}");
counter++;
} while (counter < 10);

Diese do -Schleife und die vorherige while -Schleife erzeugen die gleiche Ausgabe.

Arbeiten mit der for-Schleife


Die for -Schleife wird üblicherweise in C# verwendet. Testen Sie den folgenden Code:

for (int index = 0; index < 10; index++)


{
Console.WriteLine($"Hello World! The index is {index}");
}

Der vorherige Code funktioniert auf dieselbe Weise wie die while -Schleife und die do -Schleife, die Sie bereits
verwendet haben. Die for -Anweisung besteht aus drei Teilen, die steuern, wie sie ausgeführt wird.
Der erste Teil ist der for-Initialisierer : int index = 0; deklariert, dass index die Schleifenvariable ist, und legt
den Anfangswert auf 0 fest.
Der mittlere Teil ist die for-Bedingung : index < 10 deklariert, dass diese for -Schleife ausgeführt wird,
solange der Wert des Zählers kleiner als 10 ist.
Der letzte Teil ist der for-Iterator : index++ gibt an, wie die Schleifenvariable geändert wird, nachdem der Block
nach der for -Anweisung ausgeführt wurde. Hier gibt dieser an, dass index bei jeder Blockausführung um 1
erhöht werden soll.
Experimentieren Sie selbst. Testen Sie jede der folgenden Variationen:
Ändern Sie den Initialisierer, um mit einem anderen Wert zu beginnen.
Ändern Sie die Bedingung, um an einem anderen Wert anzuhalten.
Wenn Sie fertig sind, fahren Sie damit fort, mithilfe der erworbenen Kenntnisse selbst Codes zu schreiben.
Eine andere Schleifenanweisung wird in diesem Tutorial nicht behandelt: die foreach -Anweisung. Mit der
foreach -Anweisung wird die Anweisung für jedes Element in einer Sequenz von Elementen ausgeführt. Da sie
am häufigsten mit Sammlungen verwendet wird, wird sie im nächsten Tutorial behandelt.

Erstellte geschachtelte Schleifen


Eine while -, do - oder for -Schleife kann in einer anderen Schleife geschachtelt werden, um mithilfe der
Kombination aus jedem Element in der äußeren Schleife und jedem Element in der inneren Schleife eine Matrix
zu erstellen. Durch die Umsetzung dessen können Sie mehrere alphanumerische Paare erstellen, die Zeilen und
Spalten darstellen.
Eine for -Schleife kann die Zeilen generieren:

for (int row = 1; row < 11; row++)


{
Console.WriteLine($"The row is {row}");
}

Eine andere Schleife kann die Spalten generieren:

for (char column = 'a'; column < 'k'; column++)


{
Console.WriteLine($"The column is {column}");
}

Sie können eine Schleife innerhalb der anderen schachteln, um Paare zu bilden:

for (int row = 1; row < 11; row++)


{
for (char column = 'a'; column < 'k'; column++)
{
Console.WriteLine($"The cell is ({row}, {column})");
}
}

Sie können erkennen, dass sich die äußere Schleife für jede vollständige Ausführung der inneren Schleife einmal
erhöht. Kehren Sie die Schachtelung der Zeilen und Spalten um, und erkennen Sie selbst, was sich ändert. Wenn
Sie fertig sind, fügen Sie den Code aus diesem Abschnitt in eine Methode namens ExploreLoops() ein.

Kombinieren von Branches und Schleifen


Nachdem Sie nun die if -Anweisung und die Schleifenkonstrukte in der Programmiersprache C#
kennengelernt haben, versuchen Sie, einen C#-Code zu schreiben, der die Summe aller ganzen Zahlen von 1 bis
20 ermittelt, die durch 3 teilbar sind. Im Folgenden einige Tipps:
Der % -Operator ermittelt den Restwert einer Divisionsoperation.
Die if -Anweisung ermittelt die Bedingung, um festzustellen, ob eine Zahl in der Summe berücksichtigt
werden soll.
Die for -Schleife ermöglicht es, eine Reihe von Schritten für alle Zahlen von 1 bis 20 zu wiederholen.

Probieren Sie es selbst aus. Prüfen Sie dann, wie Sie abgeschnitten haben. Sie sollten 63 als Antwort erhalten.
Sie können eine mögliche Antwort sehen, indem Sie sich den fertig gestellten Code auf GitHub ansehen.
Sie haben das Tutorial „Verzweigungen und Schleifen“ abgeschlossen.
Sie können mit dem Tutorial Arrays und Auflistungen in Ihrer eigenen Entwicklungsumgebung fortfahren.
Weitere Informationen zu diesen Begriffen finden Sie in diesen Artikeln:
Auswahlanweisungen
Iterationsanweisungen
Informationen zum Verwalten von
Datensammlungen mithilfe des generischen
Listentyps
04.11.2021 • 6 minutes to read

Dieses Tutorial bietet eine Einführung in die Sprache C# und die Grundlagen der List<T>-Klasse.

Voraussetzungen
Für dieses Tutorial wird vorausgesetzt, dass Sie einen Computer für die lokale Entwicklung eingerichtet haben.
Unter Windows, Linux oder macOS können Sie die .NET CLI zum Programmieren, Erstellen und Ausführen von
Anwendungen verwenden. Auf dem Mac und unter Windows können Sie Visual Studio 2019 verwenden.
Setupanweisungen finden Sie unter Einführung in .NET-Entwicklungstools.

Beispiel für eine einfache Liste


Erstellen Sie ein Verzeichnis mit dem Namen list-tutorial. Machen Sie dieses Verzeichnis zum aktuellen
Verzeichnis, und führen Sie dotnet new console aus.

IMPORTANT
Die C#-Vorlagen für .NET 6 verwenden Anweisungen der obersten Ebene. Ihre Anwendung passt möglicherweise nicht
zum Code in diesem Artikel, wenn Sie bereits ein Upgrade auf die .NET 6-Vorschauversionen durchgeführt haben. Weitere
Informationen finden Sie im Artikel Neue C#-Vorlagen generieren Anweisungen auf oberster Ebene.
Das .NET 6 SDK fügt auch eine Reihe impliziter global using -Anweisungen für Projekte hinzu, die die folgenden SDKs
verwenden:
Microsoft.NET.Sdk
Microsoft.NET.Sdk.Web
Microsoft.NET.Sdk.Worker
Diese impliziten global using -Anweisungen enthalten die gängigsten Namespaces für den Projekttyp.

Öffnen Sie Program.cs in Ihrem bevorzugten Editor, und ersetzen Sie den vorhandenen Code durch Folgendes:

using System;
using System.Collections.Generic;

var names = new List<string> { "<name>", "Ana", "Felipe" };


foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}

Ersetzen Sie <name> durch Ihren eigenen Namen. Speichern Sie Program.cs. Geben Sie dotnet run in Ihrem
Konsolenfenster ein, um es zu testen.
Sie haben eine Liste mit Zeichenfolgen erstellt, dieser Liste drei Namen hinzugefügt und die Namen in
Großbuchstaben ausgegeben. Sie verwenden Konzepte, die Sie in früheren Tutorials kennengelernt haben, um
die Liste zu durchlaufen.
Im Code zum Anzeigen von Namen wird das Feature Zeichenfolgeninterpolation genutzt. Wenn Sie einem
string ein $ -Zeichen voranstellen, können Sie C#-Code in die Zeichenfolgendeklaration einbetten. Der Wert,
den dieser C#-Code generiert, ist eine Zeichenfolge, durch die der C#-Code ersetzt wird. In diesem Beispiel wird
{name.ToUpper()} mit dem jeweiligen in Großbuchstaben konvertierten Namen ersetzt, da Sie die ToUpper-
Methode aufgerufen haben.
Setzen wir nun unsere Forschungen fort.

Ändern von Listeninhalten


Die Sammlung, die Sie erstellt haben, nutzt den List<T>-Typ. Dieser Typ speichert Elementsequenzen. Sie geben
den Typ der Elemente zwischen den spitzen Klammern an.
Ein wichtiger Aspekt dieses List<T>-Typs ist, dass er wachsen oder schrumpfen kann, sodass Sie Elemente
hinzufügen oder entfernen können. Fügen Sie am Ende Ihres Programms den folgenden Code hinzu:

Console.WriteLine();
names.Add("Maria");
names.Add("Bill");
names.Remove("Ana");
foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}

Sie haben am Ende der Liste zwei weitere Namen hinzugefügt. Sie haben auch einen entfernt. Speichern Sie die
Datei, und geben Sie dotnet run zum Testen ein.
List<T> ermöglicht Ihnen auch, mithilfe des Indexes auf einzelne Elemente zu verweisen. Platzieren Sie den
Index hinter dem Listennamen zwischen den Zeichen [ und ] . C# verwendet 0 für den ersten Index. Fügen
Sie diesen Code direkt unterhalb des Codes hinzu, den Sie gerade hinzugefügt haben, und probieren Sie es aus:

Console.WriteLine($"My name is {names[0]}");


Console.WriteLine($"I've added {names[2]} and {names[3]} to the list");

Sie können nicht auf einen Index zugreifen, der hinter dem Ende der Liste liegt. Denken Sie daran, dass die
Indizes mit 0 (null) beginnen, sodass der größte gültige Index um eins kleiner ist als die Anzahl der Elemente in
der Liste. Sie können mit der Count-Eigenschaft überprüfen, wie lang die Liste ist. Fügen Sie am Ende Ihres
Programms den folgenden Code hinzu:

Console.WriteLine($"The list has {names.Count} people in it");

Speichern Sie die Datei, und geben Sie dotnet run erneut ein, um die Ergebnisse anzuzeigen.

Suchen in und Sortieren von Listen


In unseren Beispielen werden relativ kleine Listen verwendet, aber Ihre Anwendungen erstellen möglicherweise
häufig Listen mit viel mehr Elementen, die manchmal in die Tausende gehen. Um in diesen größeren
Sammlungen Elemente zu finden, müssen Sie die Liste nach verschiedenen Elementen durchsuchen. Die
IndexOf-Methode sucht nach einem Element und gibt den Index des Elements zurück. Wenn das Element nicht
in der Liste enthalten ist, gibt IndexOf -1 zurück. Fügen Sie am Ende Ihres Programms den folgenden Code
hinzu:
var index = names.IndexOf("Felipe");
if (index == -1)
{
Console.WriteLine($"When an item is not found, IndexOf returns {index}");
}
else
{
Console.WriteLine($"The name {names[index]} is at index {index}");
}

index = names.IndexOf("Not Found");


if (index == -1)
{
Console.WriteLine($"When an item is not found, IndexOf returns {index}");
}
else
{
Console.WriteLine($"The name {names[index]} is at index {index}");

Die Elemente in der Liste können auch sortiert werden. Die Sort-Methode sortiert alle Elemente in der Liste in
der normalen Reihenfolge (Zeichenfolgen alphabetisch). Fügen Sie am Ende Ihres Programms den folgenden
Code hinzu:

names.Sort();
foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}

Speichern Sie die Datei, und geben Sie dotnet run ein, um diese neueste Version zu testen.
Bevor Sie mit dem nächsten Abschnitt beginnen, verschieben wir den aktuellen Code in eine separate Methode.
Dies erleichtert das Arbeiten mit einem neuen Beispiel. Fügen Sie den gesamten Code, den Sie geschrieben
haben, in eine neue Methode namens WorkWithStrings() ein. Rufen Sie diese Methode am Anfang Ihres
Programms auf. Anschließend sollte der Code wie folgt aussehen:
using System;
using System.Collections.Generic;

WorkWithString();

void WorkWithString()
{
var names = new List<string> { "<name>", "Ana", "Felipe" };
foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}

Console.WriteLine();
names.Add("Maria");
names.Add("Bill");
names.Remove("Ana");
foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}

Console.WriteLine($"My name is {names[0]}");


Console.WriteLine($"I've added {names[2]} and {names[3]} to the list");

Console.WriteLine($"The list has {names.Count} people in it");

var index = names.IndexOf("Felipe");


if (index == -1)
{
Console.WriteLine($"When an item is not found, IndexOf returns {index}");
}
else
{
Console.WriteLine($"The name {names[index]} is at index {index}");
}

index = names.IndexOf("Not Found");


if (index == -1)
{
Console.WriteLine($"When an item is not found, IndexOf returns {index}");
}
else
{
Console.WriteLine($"The name {names[index]} is at index {index}");

names.Sort();
foreach (var name in names)
{
Console.WriteLine($"Hello {name.ToUpper()}!");
}
}

Listen mit anderen Typen


Sie haben bisher den string -Typ in Listen verwendet. Nun erstellen wir eine List<T> mithilfe eines anderen
Typs. Zunächst erstellen wir einen Satz von Zahlen.
Fügen Sie Ihrem Programm nach dem Aufruf von WorkWithStrings() Folgendes hinzu:

var fibonacciNumbers = new List<int> {1, 1};


Damit wird eine Liste von Ganzzahlen erstellt und für die ersten beiden Ganzzahlen der Wert 1 festgelegt. Dies
sind die ersten beiden Werte einer Fibonacci-Sequenz – einer Sequenz von Zahlen. Jede nächste Fibonacci-Zahl
wird ermittelt, indem die Summe der beiden vorherigen Zahlen gebildet wird. Fügen Sie den folgenden Code
hinzu:

var previous = fibonacciNumbers[fibonacciNumbers.Count - 1];


var previous2 = fibonacciNumbers[fibonacciNumbers.Count - 2];

fibonacciNumbers.Add(previous + previous2);

foreach (var item in fibonacciNumbers)


Console.WriteLine(item);

Speichern Sie die Datei, und geben Sie dotnet run ein, um die Ergebnisse anzuzeigen.

TIP
Um sich genau auf diesen Abschnitt zu konzentrieren, können Sie den Code auskommentieren, der
WorkingWithStrings(); aufruft. Setzen Sie einfach zwei / -Zeichen wie folgt vor den Aufruf:
// WorkingWithStrings(); .

Herausforderung
Versuchen Sie, einige dieser Konzepte aus dieser Lektion und früheren Lektionen in einen Zusammenhang zu
bringen. Erweitern Sie das, was Sie bisher bezüglich Fibonacci-Zahlen erstellt haben. Schreiben Sie den Code
zum Generieren der ersten 20 Zahlen der Sequenz. (Hinweis: Die 20. Fibonacci-Zahl lautet 6765.)

Übung abgeschlossen
Eine Beispiellösung finden Sie in Form eines fertiggestellten Beispielcodes auf GitHub.
Mit jeder Iteration der Schleife werden die letzten beiden Ganzzahlen in der Liste summiert, und dieser Wert
wird der Liste hinzugefügt. Die Schleife wird wiederholt, bis der Liste 20 Elemente hinzugefügt worden sind.
Herzlichen Glückwunsch, Sie haben das Listentutorial abgeschlossen. Sie können mit zusätzlichen Tutorials in
Ihrer eigenen Entwicklungsumgebung fortfahren.
Weitere Informationen zum Arbeiten mit dem List -Typ finden Sie im Artikel „.NET-Grundlagen“ unter
Sammlungen. Sie werden auch viele andere Sammlungstypen kennenlernen.
Allgemeine Struktur eines C#-Programms
04.11.2021 • 2 minutes to read

C#-Programme bestehen aus mindestens einer Datei. Jede Datei enthält null oder mehr Namespaces. Ein
Namespace enthält Typen, z. B. Klassen, Strukturen, Schnittstellen, Enumerationen und Delegaten, oder andere
Namespaces. Im folgenden Beispiel wird das Grundgerüst eines C#-Programms dargestellt, das all diese
Elemente enthält.

// A skeleton of a C# program
using System;

// Your program starts here:


Console.WriteLine("Hello world!");

namespace YourNamespace
{
class YourClass
{
}

struct YourStruct
{
}

interface IYourInterface
{
}

delegate int YourDelegate();

enum YourEnum
{
}

namespace YourNestedNamespace
{
struct YourStruct
{
}
}
}

Im vorherigen Beispiel werden Anweisungen der obersten Ebene für den Einstiegspunkt des Programms
verwendet. Dieses Feature wurde in C# 9 hinzugefügt. Vor C# 9 war der Einstiegspunkt eine statische Methode
namens Main , wie im folgenden Beispiel gezeigt:
// A skeleton of a C# program
using System;
namespace YourNamespace
{
class YourClass
{
}

struct YourStruct
{
}

interface IYourInterface
{
}

delegate int YourDelegate();

enum YourEnum
{
}

namespace YourNestedNamespace
{
struct YourStruct
{
}
}

class Program
{
static void Main(string[] args)
{
//Your program starts here...
Console.WriteLine("Hello world!");
}
}
}

Verwandte Abschnitte
Informationen zu diesen Programmelementen finden Sie im Abschnitt Typen im Leitfaden zu den Grundlagen:
Klassen
Strukturen
Namespaces
Schnittstellen
Enumerationen
Delegaten

C#-Programmiersprachenspezifikation
Weitere Informationen finden Sie in den grundlegenden Konzepten und derC#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.
Das C#-Typsystem
04.11.2021 • 13 minutes to read

C# ist eine stark typisierte Sprache. Jede Variable und jede Konstante verfügt über einen Typ, genau wie jeder
Ausdruck, der zu einem Wert ausgewertet wird. Jede Methodendeklaration gibt den Namen, den Typ und die
Art (Wert, Verweis oder Ausgabe) für jeden Eingabeparameter und Rückgabewert an. In der .NET-
Klassenbibliothek sind integrierte numerische Typen und komplexe Typen definiert, die für eine große
Bandbreite an Konstrukten stehen. Dazu gehören das Dateisystem, Netzwerkverbindungen, Sammlungen und
Arrays von Objekten sowie Datumsangaben. In einem typischen C#-Programm werden Typen aus der
Klassenbibliothek und benutzerdefinierte Typen verwendet, die die Konzepte für das Problemfeld des
Programms modellieren.
Die in einem Typ gespeicherten Informationen können die folgenden Elemente umfassen:
Der Speicherplatz, den eine Variable des Typs erfordert
Die maximalen und minimalen Werte, die diese darstellen kann
Die enthaltenen Member (Methoden, Felder, Ereignisse usw.)
Der Basistyp, von dem geerbt wird
Die Schnittstellen, die implementiert werden
Die Arten von zulässigen Vorgängen
Der Compiler verwendet Typinformationen, um sicherzustellen, dass alle im Code ausgeführten Vorgänge
typsicher sind. Wenn Sie z. B. eine Variable vom Typ int deklarieren, können Sie mit dem Compiler die Variable
für Additions- und Subtraktionsvorgänge verwenden. Wenn Sie dieselben Vorgänge für eine Variable vom Typ
bool ausführen möchten, generiert der Compiler einen Fehler, wie im folgenden Beispiel dargestellt:

int a = 5;
int b = a + 2; //OK

bool test = true;

// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;

NOTE
C- und C++-Entwickler sollten beachten, dass in C# bool nicht in int konvertiert werden kann.

Der Compiler bettet die Typinformationen als Metadaten in die ausführbare Datei ein. Die Common Language
Runtime (CLR) verwendet diese Metadaten zur Laufzeit, um die Typsicherheit zu gewährleisten, wenn
Speicherplatz belegt und freigegeben wird.

Angeben von Typen in Variablendeklarationen


Wenn Sie eine Variable oder Konstante in einem Programm deklarieren, müssen Sie ihren Typ festlegen oder
das var -Schlüsselwort verwenden, damit der Typ vom Compiler abgeleitet wird. Im folgenden Beispiel werden
einige Variablendeklarationen dargestellt, die sowohl integrierte numerische Typen als auch komplexe
benutzerdefinierte Typen verwenden:
// Declaration only:
float temperature;
string name;
MyClass myClass;

// Declaration with initializers (four examples):


char firstLetter = 'C';
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in source
where item <= limit
select item;

Die Methodenparameter- und Rückgabewerttypen werden in der Methodendeklaration angegeben. Die


folgende Signatur zeigt eine Methode, für die ein int als Eingabeargument benötigt wird und die eine
Zeichenfolge zurückgibt:

public string GetName(int ID)


{
if (ID < names.Length)
return names[ID];
else
return String.Empty;
}
private string[] names = { "Spencer", "Sally", "Doug" };

Nachdem Sie eine Variable deklariert haben, können Sie sie nicht erneut mit einem neuen Typ deklarieren, und
Sie können keinen Wert zuweisen, der nicht mit ihrem deklarierten Typ kompatibel ist. Beispielsweise können
Sie keinen Typ int deklarieren und diesem dann den booleschen Wert true zuweisen. Werte können jedoch
in andere Typen konvertiert werden, etwa wenn diese neuen Variablen zugewiesen oder als
Methodenargumente übergeben werden. Eine Typkonvertierung, die keinen Datenverlust verursacht, wird
automatisch vom Compiler ausgeführt. Eine Konvertierung, die möglicherweise Datenverlust verursacht,
erfordert eine Umwandlung in den Quellcode.
Weitere Informationen finden Sie unter Umwandlung und Typkonvertierungen.

Integrierte Typen
C# stellt einen Standardsatz integrierter Typen bereit. Diese stellen ganze Zahlen, Gleitkommawerte, boolesche
Ausdrücke, Textzeichen, Dezimalwerte und andere Datentypen dar. Es gibt auch integrierte string -Typen und
object -Typen. Diese Typen können Sie in jedem C#-Programm verwenden. Eine vollständige Liste der
integrierten Typen finden Sie unter Integrierte Typen.

Benutzerdefinierte Typen
Sie verwenden die Konstrukte struct , class , interface , enum und record zum Erstellen Ihrer eigenen
benutzerdefinierten Typen. Die .NET-Klassenbibliothek ist eine Sammlung benutzerdefinierter Typen, die Sie in
Ihren eigenen Anwendungen verwenden können. Standardmäßig sind die am häufigsten verwendeten Typen in
der Klassenbibliothek in jedem C#-Programm verfügbar. Andere stehen nur zur Verfügung, wenn Sie
ausdrücklich einen Projektverweis auf die Assembly hinzufügen, in der sie definiert sind. Wenn der Compiler
über einen Verweis auf die Assembly verfügt, können Sie Variablen (und Konstanten) des in dieser Assembly
deklarierten Typs im Quellcode deklarieren. Weitere Informationen finden Sie in der Dokumentation zur .NET-
Klassenbibliothek.

Das allgemeine Typsystem


Es ist wichtig, zwei grundlegende Punkte zum Typsystem in .NET zu verstehen:
Es unterstützt das Prinzip der Vererbung. Typen können von anderen Typen abgeleitet werden, die als
Basistypen bezeichnet werden. Der abgeleitete Typ erbt (mit einigen Beschränkungen) die Methoden,
Eigenschaften und anderen Member des Basistyps. Der Basistyp kann wiederum von einem anderen Typ
abgeleitet sein. In diesem Fall erbt der abgeleitete Typ die Member beider Basistypen in der
Vererbungshierarchie. Alle Typen, einschließlich integrierter numerischer Typen wie System.Int32 (C#-
Schlüsselwort: int ) werden letztendlich von einem einzelnen Basistyp abgeleitet, nämlich System.Object
(C#-Schlüsselwort: object ). Diese einheitliche Typhierarchie wird als Allgemeines Typsystem (CTS)
bezeichnet. Weitere Informationen zur Vererbung in C# finden Sie unter Vererbung.
Jeder Typ im CTS ist als Werttyp oder Referenztyp definiert. Diese Typen umfassen auch alle
benutzerdefinierten Typen in der .NET-Klassenbibliothek und Ihre eigenen benutzerdefinierten Typen. Typen,
die Sie mithilfe des struct -Schlüsselworts definieren, sind Werttypen. Alle integrierten numerischen Typen
sind structs . Typen, die Sie mithilfe des class - oder record -Schlüsselworts definieren, sind
Referenztypen. Für Referenztypen und Werttypen gelten unterschiedliche Kompilierzeitregeln und ein
anderes Laufzeitverhalten.
In der folgenden Abbildung wird die Beziehung zwischen Werttypen und Referenztypen im CTS dargestellt.

NOTE
Wie Sie sehen, sind die am häufigsten verwendeten Typen alle im System-Namespace organisiert. Jedoch ist es für den
Namespace, in dem ein Typ enthalten ist, unerheblich, ob es sich um einen Werttyp oder einen Referenztyp handelt.

Klassen und Strukturen sind zwei der grundlegenden Konstrukte des allgemeinen Typsystems in .NET. C# 9 fügt
Datensätze hinzu, bei denen es sich um eine Art von Klasse handelt. Bei beiden handelt es sich um eine
Datenstruktur, die einen als logische Einheit zusammengehörenden Satz von Daten und Verhalten kapselt. Die
Daten und Verhalten bilden die Member der Klasse, der Struktur oder des Datensatzes. Die Member beinhalten
ihre Methoden, Eigenschaften, Ereignisse usw. und sind weiter unten in diesem Artikel aufgeführt.
Die Deklaration einer Klasse, Struktur oder eines Datensatzes ist mit einer Blaupause vergleichbar, mit der zur
Laufzeit Instanzen oder Objekte erstellt werden. Wenn Sie eine Klasse, Struktur oder einen Datensatz namens
Person definieren, ist Person der Name des Typs. Wenn Sie eine Variable p vom Typ Person deklarieren und
initialisieren, wird p als Objekt oder Instanz von Person bezeichnet. Vom selben Typ Person können mehrere
Instanzen erstellt werden, und jede Instanz kann über unterschiedliche Werte in ihren Eigenschaften und Feldern
verfügen.
Eine Klasse ist ein Verweistyp. Wenn ein Objekt des Typs erstellt wird, enthält die Variable, der das Objekt
zugewiesen wurde, lediglich einen Verweis auf den entsprechenden Speicherort. Wenn der Objektverweis einer
neuen Variablen zugewiesen wird, verweist die neue Variable auf das ursprüngliche Objekt. Über eine Variable
vorgenommene Änderungen gelten auch für die andere Variable, da beide auf dieselben Daten verweisen.
Eine Struktur ist ein Werttyp. Wenn eine Struktur erstellt wird, enthält die Variable, der die Struktur zugewiesen
wird, die eigentlichen Daten der Struktur. Wenn die Struktur einer neuen Variable zugewiesen wird, wird sie
kopiert. Die neue Variable und die ursprüngliche Variable enthalten daher zwei separate Kopien der gleichen
Daten. Änderungen an einer Kopie wirken sich nicht auf die andere Kopie aus.
Datensatztypen können entweder Verweistypen ( record class ) oder Werttypen ( record struct ) sein.
Im Allgemeinen werden Klassen verwendet, um komplexeres Verhalten zu modellieren. Klassen speichern in der
Regel Daten, die geändert werden sollen, nachdem ein Klassenobjekt erstellt wurde. Strukturen eignen sich am
besten für kleine Datenstrukturen. In Strukturen sind in der Regel Daten gespeichert, deren Änderung nach dem
Erstellen der Struktur nicht beabsichtigt ist. Datensatztypen sind Datenstrukturen mit zusätzlichen vom Compiler
synthetisierten Membern. In Datensätzen sind in der Regel Daten gespeichert, deren Änderung nach dem
Erstellen des Objekts nicht beabsichtigt ist.
Werttypen
Werttypen werden von System.ValueType abgeleitet, was wiederum von System.Object abgeleitet wird. Typen,
die von System.ValueType abgeleitet werden, weisen ein besonderes Verhalten in der CLR auf. Werttypvariablen
enthalten ihre Werte direkt. Der Arbeitsspeicher für eine Struktur wird inline in dem Kontext zugeordnet, in dem
die Variable deklariert ist. Für Werttypvariablen erfolgt keine getrennte Heapzuordnung bzw. kein Mehraufwand
für Garbage Collection. Sie können record struct -Typen deklarieren, die Werttypen sind, und die
synthetisierten Member für Datensätze einschließen.
Zwei Kategorien von Werttypen sind verfügbar: struct und enum .
Die integrierten numerischen Typen sind Strukturen und verfügen über Felder und Methoden, auf die Sie
zugreifen können:

// constant field on type byte.


byte b = byte.MaxValue;

Sie deklarieren diese jedoch und weisen ihnen Werte zu, als wären es einfache, nicht aggregierte Typen:

byte num = 0xA;


int i = 5;
char c = 'Z';

Werttypen sind versiegelt. Sie können keinen Typ aus einem Werttyp ableiten, z. B. System.Int32. Sie können
keine Struktur definieren, die von einer benutzerdefinierten Klasse oder Struktur erben kann, weil eine Struktur
nur von System.ValueType erben kann. Eine Struktur kann jedoch eine oder mehrere Schnittstellen
implementieren. Sie können einen Strukturtyp in jeden beliebigen Schnittstellentyp umwandeln, den er
implementiert. Diese Umwandlung verursacht einen Boxing-Vorgang, mit dem die Struktur von einem
Referenztypobjekt im verwalteten Heap umschlossen wird. Boxing-Vorgänge werden auch ausgeführt, wenn Sie
einen Werttyp an eine Methode übergeben, die System.Object oder einen beliebigen Schnittstellentyp als
Eingabeparameter akzeptiert. Weitere Informationen finden Sie unter Boxing und Unboxing.
Sie können das struct-Schlüsselwort verwenden, um eigene benutzerdefinierte Werttypen zu erstellen. In der
Regel wird eine Struktur als Container für einen kleinen Satz verwandter Variablen verwendet, wie im folgenden
Beispiel dargestellt:
public struct Coords
{
public int x, y;

public Coords(int p1, int p2)


{
x = p1;
y = p2;
}
}

Weitere Informationen über Strukturen finden Sie unter Struktur-Typen. Weitere Informationen zu Werttypen
finden Sie unter Werttypen.
Die andere Kategorie von Werttypen ist enum . Eine Enumeration definiert einen Satz benannter ganzzahliger
Konstanten. So enthält z.B. die System.IO.FileMode-Enumeration in der .NET-Klassenbibliothek mehrere
benannte ganzzahlige Konstanten, die festlegen, wie eine Datei geöffnet werden soll. Die Definition erfolgt wie
im folgenden Beispiel:

public enum FileMode


{
CreateNew = 1,
Create = 2,
Open = 3,
OpenOrCreate = 4,
Truncate = 5,
Append = 6,
}

Die System.IO.FileMode.Create-Konstante besitzt den Wert 2. Der Name ist jedoch für Personen, die den
Quellcode lesen, viel aussagekräftiger. Aus diesem Grund ist es besser, anstelle von Konstantenliteralen
Enumerationen zu verwenden. Weitere Informationen finden Sie unter System.IO.FileMode.
Alle Enumerationen erben von System.Enum, was wiederum von System.ValueType erbt. Alle Regeln, die für
Strukturen gelten, gelten auch für Enumerationen. Weitere Informationen zu Enumerationen finden Sie unter
Enumerationstypen.
Verweistypen
Ein Typ, der als class , record , delegate , Array oder interface definiert ist, ist ein reference type .
Wenn Sie eine Variable eines reference type deklarieren, enthält sie den Wert null , bis Sie ihr eine Instanz
dieses Typs zuweisen oder über den new -Operator eine Instanz erstellen. Das folgende Beispiel veranschaulicht
die Erstellung und Zuweisung einer Klasse:

MyClass myClass = new MyClass();


MyClass myClass2 = myClass;

interface kann nicht direkt über den new -Operator instanziiert werden. Erstellen Sie stattdessen eine Instanz
einer Klasse, die die Schnittstelle implementiert, und weisen Sie sie zu. Betrachten Sie das folgenden Beispiel:
MyClass myClass = new MyClass();

// Declare and assign using an existing value.


IMyInterface myInterface = myClass;

// Or create and assign a value in a single statement.


IMyInterface myInterface2 = new MyClass();

Beim Erstellen des Objekts wird der Arbeitsspeicher auf dem verwalteten Heap zugewiesen. Die Variable enthält
nur einen Verweis auf den Speicherort des Objekts. Für Typen im verwalteten Heap ist sowohl bei der
Zuweisung als auch bei der Bereinigung Mehraufwand erforderlich. Garbage Collection ist die automatische
Speicherverwaltungsfunktion der CLR, die die Bereinigung ausführt. Die Garbage Collection ist jedoch auch
stark optimiert. In den meisten Szenarien führt sie nicht zu einem Leistungsproblem. Weitere Informationen zur
Garbage Collection finden Sie unter Automatische Speicherverwaltung.
Alle Arrays sind Referenztypen, selbst wenn ihre Elemente Werttypen sind. Arrays werden implizit von der
System.Array-Klasse abgeleitet. Sie deklarieren und verwenden diese jedoch mit der vereinfachten, von C#
bereitgestellten Syntax, wie im folgenden Beispiel dargestellt:

// Declare and initialize an array of integers.


int[] nums = { 1, 2, 3, 4, 5 };

// Access an instance property of System.Array.


int len = nums.Length;

Referenztypen bieten volle Vererbungsunterstützung. Beim Erstellen einer Klasse können Sie von jeder anderen
Schnittstelle oder Klasse erben, die nicht als versiegelt definiert ist. Andere Klassen können von Ihrer Klasse
erben und Ihre virtuellen Methoden überschreiben. Weitere Informationen zum Erstellen eigener Klassen finden
Sie unter Klassen, Strukturen und Datensätze. Weitere Informationen zur Vererbung und zu virtuellen Methoden
finden Sie unter Vererbung.

Typen von Literalwerten


In C# erhalten Literalwerte einen Typ vom Compiler. Sie können festlegen, wie ein numerisches Literal
eingegeben werden soll, indem Sie am Ende der Zahl einen Buchstaben anfügen. Um z. B. anzugeben, dass der
Wert 4.56 als float behandelt werden soll, fügen Sie nach der Zahl 4.56f ein "f" oder "F" an. Wenn kein
Buchstabe angefügt wird, leitet der Compiler einen Typ für das Literal ab. Weitere Informationen darüber, welche
Typen mit Buchstabensuffixen angegeben werden können, finden Sie unter Integrale numerische Typen und
unter Numerische Gleitkommatypen.
Da Literale typisiert sind und alle Typen letztlich von System.Object abgeleitet werden, können Sie Code der
folgenden Art erstellen und kompilieren:

string s = "The answer is " + 5.ToString();


// Outputs: "The answer is 5"
Console.WriteLine(s);

Type type = 12345.GetType();


// Outputs: "System.Int32"
Console.WriteLine(type);

Generische Typen
Ein Typ kann mit einem oder mehreren Typparametern deklariert werden, die als Platzhalter für den eigentlichen
Typ verwendet werden (den konkreten Typ). Der konkrete Typ wird beim Erstellen einer Instanz des Typs vom
Clientcode bereitgestellt. Solche Typen werden als generische Typen bezeichnet. Beispielsweise besitzt der .NET-
Typ System.Collections.Generic.List<T> einen Typparameter, der konventionsgemäß den Namen T erhält. Beim
Erstellen einer Instanz des Typs geben Sie den Typ der Objekte an, die die Liste enthalten soll, z. B. string :

List<string> stringList = new List<string>();


stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);

Die Verwendung des Typparameters ermöglicht die Wiederverwendung der Klasse für beliebige Elementtypen,
ohne die einzelnen Elemente in object konvertieren zu müssen. Generische Sammlungsklassen werden als stark
typisierte Sammlungen bezeichnet, weil der Compiler den jeweiligen Typ der Elemente in der Sammlung kennt
und zur Kompilierzeit einen Fehler auslösen kann, wenn Sie beispielsweise versuchen, dem stringList -Objekt
im vorherigen Beispiel eine ganze Zahl hinzuzufügen. Weitere Informationen finden Sie unter Generics.

Implizite Typen, anonyme Typen und Werttypen, die Nullwerte


zulassen
Sie können eine lokale Variable ( jedoch keine Klassenmember) implizit eingeben, indem Sie das var -
Schlüsselwort verwenden. Die Variable erhält weiterhin zur Kompilierzeit einen Typ, aber der Typ wird vom
Compiler bereitgestellt. Weitere Informationen zu finden Sie unter Implizit typisierte lokale Variablen.
Es kann unpraktisch sein, einen benannten Typ für einfache Sätze verwandter Werte zu erstellen, die nicht
außerhalb von Methodengrenzen gespeichert oder übergeben werden sollen. Sie können für diesen Zweck
anonyme Typen erstellen. Weitere Informationen finden Sie unter Anonyme Typen.
Gewöhnliche Werttypen können nicht den Wert null aufweisen. Sie können jedoch Nullwerte zulassende
Werttypen erstellen, indem Sie nach dem Typ ein ? anfügen. Zum Beispiel ist int? ein int -Typ, der auch den
Wert null aufweisen kann. Werttypen, die Nullwerte zulassen, sind Instanzen vom generischen Strukturtyp
System.Nullable<T>. Werttypen, die Nullwerte zulassen, sind besonders hilfreich, wenn Sie Daten an und aus
Datenbanken übergeben, in denen die numerischen Werte null sein können. Weitere Informationen finden Sie
unter Werttypen, die Nullwerte zulassen.

Typ zur Kompilierzeit und Typ zur Laufzeit


Eine Variable kann unterschiedliche Kompilierzeit- und Laufzeittypen aufweisen. Der Typ zur Kompilierzeit ist der
deklarierte oder abgeleitete Typ der Variablen im Quellcode. Der Typ zur Laufzeit ist der Typ der Instanz, auf die
von dieser Variablen verwiesen wird. Häufig sind diese beiden Typen identisch, wie im folgenden Beispiel
gezeigt:

string message = "This is a string of characters";

In anderen Fällen ist der Typ zur Kompilierzeit ein anderer, wie in den folgenden beiden Beispielen gezeigt:

object anotherMessage = "This is another string of characters";


IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

In den beiden vorherigen Beispielen ist der Typ zur Laufzeit ein Typ string . Der Typ zur Kompilierzeit ist
object in der ersten Zeile und IEnumerable<char> in der zweiten Zeile.

Wenn sich die beiden Typen für eine Variable unterscheiden, ist es wichtig zu verstehen, wann der Typ zur
Kompilierzeit und wann der Typ zur Laufzeit auftritt. Der Typ zur Kompilierzeit bestimmt alle Aktionen, die vom
Compiler ausgeführt werden. Diese Compileraktionen umfassen die Auflösung von Methodenaufrufen, die
Überladungsauflösung und verfügbare implizite und explizite Umwandlungen. Der Typ zur Laufzeit bestimmt
alle Aktionen, die zur Laufzeit aufgelöst werden. Diese Laufzeitaktionen umfassen das Verteilen virtueller
Methodenaufrufe, das Auswerten von is - und switch -Ausdrücken sowie andere Typtest-APIs. Um besser zu
verstehen, wie der Code mit Typen interagiert, ermitteln Sie, welche Aktion für welchen Typ gilt.

Verwandte Abschnitte
Weitere Informationen finden Sie in den folgenden Artikeln:
Integrierte Typen
Werttypen
Verweistypen

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
Objektorientiertes Programmieren
04.11.2021 • 4 minutes to read

Kapselung
Kapselung wird gelegentlich als erster Pfeiler oder als Prinzip der objektorientierten Programmierung
bezeichnet. Eine Klasse oder Struktur kann festlegen, inwieweit Code außerhalb der Klasse oder Struktur auf
deren Member zugreifen kann. Nicht für die Verwendung von außerhalb der Klasse oder Assembly vorgesehene
Methoden und Variablen können ausgeblendet werden, um die Wahrscheinlichkeit von Programmierfehlern
und böswilligen Angriffen zu verringern. Weitere Informationen finden Sie unter Objektorientierte
Programmierung.

Member
Die Member eines Typs umfassen alle Methoden, Felder, Konstanten, Eigenschaften und Ereignisse. In C# gibt es
im Gegensatz zu einigen anderen Sprachen keine globalen Variablen oder Methoden. Selbst der Einstiegspunkt
eines Programms, die Main -Methode, muss innerhalb einer Klasse oder Struktur deklariert werden (implizit im
Fall von Anweisungen der obersten Ebene).
In der folgenden Liste werden sämtliche Arten von Membern aufgeführt, die in einer Klasse, Struktur oder
einem Datensatz deklariert werden können.
Felder
Konstanten
Eigenschaften
Methoden
Konstruktoren
Ereignisse
Finalizer
Indexer
Operatoren
Geschachtelte Typen

Zugriff
Einige Methoden und Eigenschaften sind für den Aufruf oder Zugriff von als Clientcode bezeichnetem Code
außerhalb einer Klasse oder Struktur vorgesehen. Andere Methoden und Eigenschaften dienen nur der
Verwendung in der Klasse oder Struktur selbst. Es ist wichtig, den Zugriff auf den Code einzuschränken, damit
nur der Clientcode darauf zugreifen kann, der dafür vorgesehen ist. Inwieweit Clientcode auf die Typen und
deren Member zugreifen kann, können Sie mit folgenden Zugriffsmodifizierern festlegen:
public
protected
internal
protected internal
private
private protected.
Die Standardeinstellung für den Zugriff lautet private .
Vererbung
Klassen ( jedoch nicht Strukturen) unterstützen das Konzept der Vererbung. Eine Klasse, die von einer anderen
Klasse, der sogenannten Basisklasse, abgeleitet ist, enthält automatisch alle öffentlichen, geschützten und
internen Member der Basisklasse mit Ausnahme der Konstruktoren und Finalizer. Weitere Informationen finden
Sie unter Vererbung und Polymorphie.
Klassen können als abstrakt deklariert werden. Das bedeutet, dass mindestens eine ihrer Methoden nicht
implementiert ist. Obwohl abstrakte Klassen nicht direkt instanziiert werden können, können Sie als
Basisklassen für andere Klassen dienen, von denen die fehlende Implementierung bereitgestellt wird. Klassen
können auch als versiegelt deklariert werden, um zu verhindern, dass andere Klassen von ihnen erben.

Schnittstellen
Klassen, Strukturen und Datensätze können mehrere Schnittstellen erben. Von einer Schnittstelle erben
bedeutet, dass der Typ alle in der Schnittstelle definierten Methoden implementiert. Weitere Informationen
finden Sie unter Schnittstellen.

Generische Typen
Klassen, Strukturen und Datensätze können mit einem oder mehreren Typparametern definiert werden. Der Typ
wird beim Erstellen einer Instanz des Typs vom Clientcode bereitgestellt. Beispielsweise ist die Klasse List<T> im
Namespace System.Collections.Generic mit einem Typparameter definiert. Vom Clientcode wird eine Instanz von
List<string> oder List<int> erstellt, um den Typ anzugeben, den die Liste enthalten soll. Weitere
Informationen finden Sie unter Generics.

Statische Typen
Klassen (nicht jedoch Strukturen oder Datensätze) können als static deklariert werden. Eine statische Klasse
kann nur statische Member enthalten und nicht mit dem Schlüsselwort new instanziiert werden. Beim Laden
des Programms wird eine Kopie der Klasse in den Speicher geladen. Auf deren Member wird über den
Klassennamen zugegriffen. Klassen, Strukturen und Datensätze können statische Member enthalten.

Geschachtelte Typen
Eine Klasse, Struktur oder ein Datensatz kann innerhalb einer anderen Klasse, Struktur oder eines Datensatzes
geschachtelt werden.

Partielle Typen
Sie können einen Teil einer Klasse, Struktur oder Methode in einer Codedatei und einen anderen Teil in einer
separaten Codedatei definieren.

Objektinitialisierer
Sie können Klassen- oder Strukturobjekte sowie Auflistungen von Objekten instanziieren und initialisieren,
indem Sie ihren Eigenschaften Werte zuweisen.

Anonyme Typen
In Situationen, in denen es unkomfortabel oder nicht erforderlich ist, eine benannte Klasse zu erstellen,
verwenden Sie anonyme Typen. Anonyme Typen werden durch ihre benannten Datenmember definiert.
Erweiterungsmethoden
Sie können eine Klasse „erweitern“, ohne eine abgeleitete Klasse zu erstellen, indem Sie einen separaten Typ
erstellen. Dieser Typ enthält Methoden, die aufgerufen werden können, als ob sie zum ursprünglichen Typ
gehörten.

Implizit typisierte lokale Variablen


Innerhalb einer Klassen- oder Strukturmethode können Sie implizite Typisierung verwenden, um den Compiler
anzuweisen, den Typ einer Variablen während der Kompilierung zu bestimmen.

Datensätze
C# 9 führt den record -Typ ein. Dies ist ein Verweistyp, den Sie anstelle einer Klasse oder einer Struktur
erstellen können. Datensätze sind Klassen mit dem integrierten Verhalten zum Kapseln von Daten in
unveränderlichen Typen. Mit C# 10 wird der Werttyp record struct eingeführt. Ein Datensatz (entweder
record class oder record struct ) bietet die folgenden Features:

Präzise Syntax zum Erstellen eines Verweistyps mit unveränderlichen Eigenschaften


Wertgleichheit: Zwei Variablen eines Datensatztyps sind gleich, wenn sie den gleichen Typ aufweisen, und
wenn für jedes Feld die Werte in beiden Datensätzen gleich sind. Klassen nutzen Verweisgleichheit, also dass
zwei Variablen eines Klassentyps gleich sind, wenn sie auf dasselbe Objekt verweisen.
Präzise Syntax für die nicht destruktive Mutation: Sie können mit einem with -Ausdruck eine neue
Datensatzinstanz erstellen, die eine Kopie einer vorhandenen Instanz ist, bei der jedoch die angegebenen
Eigenschaftswerte geändert wurden.
Integrierte Formatierung für die Anzeige: Die ToString -Methode gibt den Namen des Datensatztyps sowie
die Namen und Werte öffentlicher Eigenschaften aus.
Unterstützung für Vererbungshierarchien in Datensatzklassen. Datensatzklassen unterstützen Vererbung.
Datensatzstrukturen unterstützen keine Vererbung.
Weitere Informationen finden Sie unter Datensätze.

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
Ausnahmen und Ausnahmebehandlung
04.11.2021 • 2 minutes to read

Die C#-Funktionen zur Ausnahmebehandlung helfen Ihnen dabei, unerwartete oder außergewöhnliche
Situationen zu verarbeiten, die beim Ausführen von Programmen auftreten können. Die Ausnahmebehandlung
verwendet die Schlüsselwörter try , catch und finally , um Aktionen zu testen, die möglicherweise nicht
erfolgreich sind, um Fehler zu behandeln, wenn Sie entscheiden, dass dies vernünftig ist, und um später
Ressourcen zu bereinigen. Ausnahmen können von der Common Language Runtime (CLR), von .NET bzw.
anderen Drittanbieterbibliotheken oder vom Anwendungscode generiert werden. Ausnahmen werden mit dem
Schlüsselwort throw erstellt.
In vielen Fällen wird eine Ausnahme nicht von einer Methode ausgelöst, die von Ihrem Code direkt aufgerufen
wurde, sondern von einer anderen Methode weiter unten in der Aufrufliste. Wenn eine Ausnahme ausgelöst
wird, entlädt die CLR die Liste, sucht nach einer Methode mit einem catch -Block für den spezifischen
Ausnahmetyp und führt den ersten gefundenen catch -Block aus. Wenn kein geeigneter catch -Block in der
Aufrufliste gefunden wird, beendet die CLR den Prozess und zeigt eine Meldung für den Benutzer an.
In diesem Beispiel testet eine Methode, ob eine Division durch Null möglich ist, und fängt den Fehler ab. Ohne
Ausnahmebehandlung würde dieses Programm mit dem Fehler DivideByZeroException wurde nicht
behandelt beendet.

public class ExceptionTest


{
static double SafeDivision(double x, double y)
{
if (y == 0)
throw new DivideByZeroException();
return x / y;
}

public static void Main()


{
// Input for test purposes. Change the values to see
// exception handling behavior.
double a = 98, b = 0;
double result;

try
{
result = SafeDivision(a, b);
Console.WriteLine("{0} divided by {1} = {2}", a, b, result);
}
catch (DivideByZeroException)
{
Console.WriteLine("Attempted divide by zero.");
}
}
}

Übersicht über Ausnahmen


Ausnahmen weisen folgende Eigenschaften auf:
Bei Ausnahmen handelt es sich um Typen, die alle letztlich von System.Exception abgeleitet werden.
Verwenden Sie einen try -Block um die Anweisungen, die möglicherweise Ausnahmen auslösen.
Sobald eine Ausnahme im try -Block auftritt, springt der Steuerungsfluss zum ersten zugeordneten
Ausnahmehandler, der sich an einer beliebigen Stelle in der Aufrufliste befindet. In C# wird das Schlüsselwort
catch zum Definieren eines Ausnahmehandlers verwendet.
Wenn für eine bestimmte Ausnahme kein Ausnahmehandler vorhanden ist, beendet das Programm die
Ausführung mit einer Fehlermeldung.
Fangen Sie Ausnahmen nur dann ab, wenn Sie sie behandeln und die Anwendung in einem bekannten
Zustand belassen können. Wenn Sie System.Exception abfangen, lösen Sie diese mithilfe des Schlüsselworts
throw am Ende des catch -Blocks erneut aus.
Wenn ein catch -Block eine Ausnahmevariable definiert, können Sie diese verwenden, um weitere
Informationen zum Typ der aufgetretenen Ausnahme abzurufen.
Ausnahmen können von einem Programm mithilfe des Schlüsselworts throw explizit generiert werden.
Ausnahmeobjekte enthalten detaillierte Informationen über den Fehler, z.B. den Zustand der Aufrufliste und
eine Textbeschreibung des Fehlers.
Code in einem finally -Block wird unabhängig davon ausgeführt, ob eine Ausnahme ausgelöst wurde.
Verwenden Sie einen finally -Block, um Ressourcen freizugeben, beispielsweise um Streams oder Dateien
zu schließen, die im try -Block geöffnet wurden.
Zusätzlich zum strukturierten Win32-Mechanismus zur Ausnahmebehandlung wurden in .NET verwaltete
Ausnahmen implementiert. Weitere Informationen finden Sie unter Structured Exception Handling (C/C++)
(Strukturierte Ausnahmebehandlung [C/C++]) und A Crash Course on the Depths of Win32 Structured
Exception Handling (Schnellkurs zu den Details der strukturierten Win32-Ausnahmebehandlung).

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter Ausnahmen in der C#-Sprachspezifikation. Die Sprachspezifikation ist
die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
SystemException
C#-Schlüsselwörter
throw
try-catch
try-finally
try-catch-finally
Ausnahmen
Neuerungen in C# 10.0
04.11.2021 • 3 minutes to read

IMPORTANT
In diesem Artikel werden die Features erläutert, die ab .NET 6 Vorschau 7 in C# 10.0 verfügbar sind. Zurzeit erfolgt die
Dokumentation der Verbesserungen für C# 10.0. Sie können den Fortschritt der Dokumentation in diesem Projekt
überprüfen.

Mit Version 10.0 wird die Sprache C# um die folgenden Features und Verbesserungen erweitert:
Datensatzstrukturen
Verbesserungen von Strukturtypen
Handler für interpolierte Zeichenfolgen
global using -Direktiven
Dateibezogene Namespacedeklaration
Muster für erweiterte Eigenschaften
Lässt interpolierte Zeichenfolgen vom Typ const zu
Datensatztypen können ToString() versiegeln
Lässt Zuweisung und Deklaration in derselben Dekonstruktion zu
Lässt AsyncMethodBuilder -Attribut für Methoden zu

Einige der Features, die Sie ausprobieren können, sind nur verfügbar, wenn Sie Ihre Sprachversion auf „preview“
festlegen. Diese Features werden möglicherweise in zukünftigen Vorschauversionen noch weiter verfeinert,
bevor .NET 6.0 veröffentlicht wird.
C# 10.0 wird in .NET 6 unterstützt. Weitere Informationen finden Sie unter C#-Sprachversionsverwaltung.
Sie können das neueste .NET 6.0 SDK über die .NET-Downloadseite herunterladen. Sie können auch die Version
Visual Studio 2022 Preview herunterladen, die das .NET 6.0 Preview SDK enthält.

Datensatzstrukturen
Sie können Werttyp-Datensätze deklarieren, indem Sie entweder die record struct - oder
readonly record struct - Deklaration verwenden. Sie können nun mit der Deklaration record class
verdeutlichen, dass record ein Referenztyp ist.

Verbesserungen von Strukturtypen


In C# 10.0 werden die folgenden Verbesserungen im Zusammenhang mit Strukturtypen eingeführt:
Sie können einen parameterlosen Konstruktor für eine Instanz in einem Strukturtyp deklarieren und ein
Instanzfeld oder eine Instanzeigenschaft in der zugehörigen Deklaration initialisieren. Weitere Informationen
finden Sie im Abschnitt Parameterlose Konstruktoren und Feldinitialisierer des Artikels Strukturtypen.
Der linke Operand des with -Ausdrucks kann einen beliebigen Strukturtyp aufweisen.

Handler für interpolierte Zeichenfolgen


Sie können einen Typ erstellen, der die resultierende Zeichenfolge aus einem interpolierten
Zeichenfolgenausdruck erstellt. Die .NET-Bibliotheken verwenden dieses Feature in vielen APIs. Sie können
einen Handler anhand dieses Tutorials erstellen.

Globale using-Anweisungen
Sie können den global -Modifizierer einer beliebigen using-Anweisung hinzufügen, um den Compiler
anzuweisen, dass die Direktive für alle Quelldateien in der Kompilierung gilt. Dies sind in der Regel alle
Quelldateien in einem Projekt.

Dateibezogene Namespacedeklaration
Sie können eine neue Form der namespace -Deklaration verwenden, um zu deklarieren, dass alle nachfolgenden
Deklarationen Member des deklarierten Namespace sind:

namespace MyNamespace;

Diese neue Syntax spart sowohl horizontal als auch vertikal Platz für die gängigsten namespace -Deklarationen.

Muster für erweiterte Eigenschaften


Ab C# 10.0 können Sie auf geschachtelte Eigenschaften oder Felder innerhalb eines Eigenschaftsmusters
verweisen. Beispielsweise ein Muster der Form

{ Prop1.Prop2: pattern }

in C# 10.0 und höher gültig und entspricht

{ Prop1: { Prop2: pattern } }

(In C# 8.0 und höher gültig)


Weitere Informationen finden Sie unter Eigenschaftsmuster im Hinweis zum Featurevorschlag. Weitere
Informationen zu Eigenschaftsmustern finden Sie im Abschnitt Eigenschaftsmuster des Artikels Muster.

Konstante interpolierte Zeichenfolgen


In C# 10.0 können Zeichenfolgen vom Typ const mithilfe der Zeichenfolgeninterpolation initialisiert werden,
wenn alle Platzhalter selbst konstante Zeichenfolgen sind. Durch Zeichenfolgeninterpolation können besser
lesbare konstante Zeichenfolgen erzeugt werden, während Sie konstante Zeichenfolgen zur Verwendung in
Ihrer Anwendung erstellen. Die Platzhalterausdrücke können keine numerischen Konstanten sein, da diese
Konstanten zur Laufzeit in Zeichenfolgen konvertiert werden. Die aktuelle Kultur kann sich auf die
Zeichenfolgendarstellung auswirken. Weitere Informationen finden Sie in der Sprachreferenz zu const -
Ausdrücken.

NOTE
Wenn Sie .NET 6.0 Preview 5 verwenden, muss für dieses Feature das <LangVersion> -Element in Ihrer CSPROJ-Datei auf
preview festgelegt werden.

Datensatztypen können ToString versiegeln


In C# 10.0 können Sie den sealed -Modifizierer hinzufügen, wenn Sie ToString in einem Datensatztyp
überschreiben. Das Versiegeln der ToString -Methode verhindert, dass der Compiler eine ToString -Methode
für abgeleitete Datensatztypen synthetisiert. Eine sealed ToString stellt sicher, dass alle abgeleiteten
Datensatztypen die in einem gemeinsamen Basisdatensatztyp die definierte ToString -Methode verwenden.
Weitere Informationen über dieses Feature finden Sie im Artikel zu Datensätzen.

NOTE
Wenn Sie .NET 6.0 Preview 5 verwenden, muss für dieses Feature das <LangVersion> -Element in Ihrer CSPROJ-Datei auf
preview festgelegt werden.

Zuweisung und Deklaration in derselben Dekonstruktion


Diese Änderung hebt eine Einschränkung aus früheren Versionen von C# auf. Zuvor konnte eine Dekonstruktion
alle Werte vorhandenen Variablen zuweisen oder neu deklarierte Variablen initialisieren:

// Initialization:
(int x, int y) = point;

// assignment:
int x1 = 0;
int y1 = 0;
(x1, y1) = point;

Diese Einschränkung wird in C# 10.0 aufgehoben:

int x = 0;
(x, int y) = point;

NOTE
Wenn Sie .NET 6.0 Preview 5 verwenden, muss für dieses Feature das <LangVersion> -Element in Ihrer CSPROJ-Datei auf
preview festgelegt werden.

AsyncMethodBuilder-Attribut für Methoden


In C# 10.0 und höher können Sie zusätzlich zur Angabe des Methodengeneratortyps für alle Methoden, die
einen bestimmten taskähnlichen Typ zurückgeben, einen anderen Generator für asynchrone Methoden für eine
einzelne Methode angeben. Ein benutzerdefinierter asynchroner Methoden-Generator ermöglicht erweiterte
Szenarios der Leistungsoptimierung, in denen eine bestimmte Methode von einem benutzerdefinierten
Generator profitieren kann.
Weitere Informationen finden Sie im Abschnitt zu AsyncMethodBuilder im Artikel zu vom Compiler gelesenen
Attributen.
Neuerungen in C# 9.0
04.11.2021 • 16 minutes to read

Mit Version 9.0 wird die Sprache C# um die folgenden Features und Verbesserungen erweitert:
Datensätze
init-only-Setter
Top-Level-Anweisungen
Verbesserungen am Musterabgleich:
Leistung und Interop
Integerwerte mit nativer Größe
Funktionszeiger
Unterdrücken der Ausgabe des Flags „localsinit“
Anpassen und Fertigstellen von Features
Zieltypisierte new -Ausdrücke
Anonyme static -Funktionen
Bedingter Ausdruck mit Zieltyp
Kovariante Rückgabetypen
Unterstützung für die Erweiterung GetEnumerator für foreach -Schleifen
Parameter zum Verwerfen von Lambdafunktion
Attribute in lokalen Funktionen
Unterstützung für Code-Generatoren
Modulinitialisierer
Neue Features für partielle Methoden
C# 9.0 wird in .NET 5 unterstützt. Weitere Informationen finden Sie unter C#-Sprachversionsverwaltung.
Sie können das neueste .NET SDK über die .NET-Downloadseite herunterladen.

Eintragstypen
In C# 9.0 wurden Datensatztypen (Eintragstypen) eingeführt. Sie verwenden das record -Schlüsselwort, um
einen Verweistyp zu definieren, der integrierte Funktionalität zum Kapseln von Daten bereitstellt. Sie können
Datensatztypen mit unveränderlichen Eigenschaften erstellen, indem Sie Positionsparameter oder
Standardeigenschaftensyntax verwenden:

public record Person(string FirstName, string LastName);

public record Person


{
public string FirstName { get; init; } = default!;
public string LastName { get; init; } = default!;
};

Sie können auch Datensatztypen mit änderbaren Eigenschaften und Feldern erstellen:
public record Person
{
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
};

Datensätze können zwar änderbar sein, sind jedoch primär dafür vorgesehen, unveränderliche Datenmodelle zu
unterstützen. Der Datensatztyp bietet die folgenden Funktionen:
Präzise Syntax zum Erstellen eines Verweistyps mit unveränderlichen Eigenschaften
Verhalten, das für einen datenzentrierten Verweistyp nützlich ist:
Wertgleichheit
Präzise Syntax für die nicht destruktive Mutation
Integrierte Formatierung für die Anzeige
Unterstützung für Vererbungshierarchien
Sie können Strukturtypen verwenden, um datenzentrierte Typen zu entwerfen, die Wertgleichheit und wenig
oder kein Verhalten bereitstellen. Bei relativ großen Datenmodellen haben Strukturtypen jedoch einige
Nachteile:
Sie unterstützen keine Vererbung.
Sie sind weniger effizient bei der Bestimmung der Wertgleichheit. Bei Werttypen verwendet die
ValueType.Equals-Methode Reflexion, um alle Felder zu suchen. Für Datensätze generiert der Compiler die
Equals -Methode. In der Praxis ist die Implementierung von Wertgleichheit in Datensätzen messbar
schneller.
In einigen Szenarien wird mehr Arbeitsspeicher verwendet, da jede Instanz über eine vollständige Kopie aller
Daten verfügt. Datensatztypen sind Verweistypen, sodass eine Datensatzinstanz nur einen Verweis auf die
Daten enthält.
Positionssyntax für die Eigenschaftendefinition
Sie können Positionsparameter verwenden, um die Eigenschaften eines Datensatzes zu deklarieren und die
Eigenschaftswerte zu initialisieren, wenn Sie eine Instanz erstellen:

public record Person(string FirstName, string LastName);

public static void Main()


{
Person person = new("Nancy", "Davolio");
Console.WriteLine(person);
// output: Person { FirstName = Nancy, LastName = Davolio }
}

Wenn Sie die Positionssyntax für die Eigenschaftendefinition verwenden, erstellt der Compiler Folgendes:
Eine öffentliche, automatisch implementierte Init-only-Eigenschaft für jeden Positionsparameter, der in der
Datensatzdeklaration bereitgestellt wird. Eine Init-only-Eigenschaft kann nur im Konstruktor oder mit einem
Eigenschafteninitialisierer festgelegt werden.
Ein primärer Konstruktor, dessen Parameter mit den Parametern mit fester Breite der Datensatzdeklaration
übereinstimmen
Eine Deconstruct -Methode mit einem out -Parameter für jeden Positionsparameter, der in der
Datensatzdeklaration bereitgestellt wird.
Weitere Informationen finden Sie unter Positionssyntax im C#-Sprachreferenzartikel zu Datensätzen (Records).
Unveränderlichkeit
Ein Datensatztyp ist nicht notwendigerweise unveränderlich. Sie können Eigenschaften mit set -
Zugriffsmethoden und Feldern deklarieren, die nicht readonly sind. Doch obwohl Datensätze änderbar sein
können, vereinfachen sie die Erstellung unveränderlicher Datenmodelle. Eigenschaften, die Sie mit
Positionssyntax erstellen, sind unveränderlich.
Unveränderlichkeit kann nützlich sein, wenn Sie möchten, dass ein datenzentrierter Typ threadsicher ist oder ein
Hashcode in einer Hashtabelle unverändert bleibt. Dadurch können Fehler verhindert werden, die auftreten,
wenn Sie ein Argument als Verweis an eine Methode übergeben und die Methode den Argumentwert
unerwartet ändert.
Die für Datensatztypen eindeutigen Features werden von durch den Compiler synthetisierte Methoden
implementiert, und keine dieser Methoden beeinträchtigt die Unveränderlichkeit durch Ändern des
Objektzustands.
Wertgleichheit
Wertgleichheit bedeutet, dass zwei Variablen eines Datensatztyps gleich sind, wenn die Typen sowie alle
Eigenschafts- und Feldwerte übereinstimmen. Bei anderen Verweistypen bedeutet Gleichheit Identität. Zwei
Variablen eines Verweistyps sind demnach gleich, wenn sie auf dasselbe Objekt verweisen.
Im folgenden Beispiel wird die Wertgleichheit von Datensatztypen veranschaulicht:

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()


{
var phoneNumbers = new string[2];
Person person1 = new("Nancy", "Davolio", phoneNumbers);
Person person2 = new("Nancy", "Davolio", phoneNumbers);
Console.WriteLine(person1 == person2); // output: True

person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True

Console.WriteLine(ReferenceEquals(person1, person2)); // output: False


}

In class -Typen könnten Sie Gleichheitsmethoden und -operatoren manuell überschreiben, um Wertgleichheit
zu erzielen, aber das Entwickeln und Testen dieses Codes wäre zeitaufwändig und fehleranfällig. Wenn diese
Funktionalität integriert ist, werden Fehler verhindert, die sich daraus ergeben würden, dass vergessen wird,
benutzerdefinierten Überschreibungscode zu aktualisieren, wenn Eigenschaften oder Felder hinzugefügt oder
geändert werden.
Weitere Informationen finden Sie unter Wertgleichheit im C#-Sprachreferenzartikel zu Datensätzen (Records).
Nichtdestruktive Mutation
Wenn Sie unveränderliche Eigenschaften einer Datensatzinstanz mutieren müssen, können Sie einen with -
Ausdruck verwenden, um eine nichtdestruktive Mutation zu erzielen. Ein with -Ausdruck erstellt eine neue
Datensatzinstanz, bei der es sich um eine Kopie einer vorhandenen Datensatzinstanz handelt, bei der bestimmte
Eigenschaften und Felder geändert wurden. Mit der Objektinitialisierersyntax können Sie die zu ändernden
Werte angeben, wie im folgenden Beispiel gezeigt:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; }
}

public static void Main()


{
Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
Console.WriteLine(person1);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }

Person person2 = person1 with { FirstName = "John" };


Console.WriteLine(person2);
// output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False

person2 = person1 with { PhoneNumbers = new string[1] };


Console.WriteLine(person2);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False

person2 = person1 with { };


Console.WriteLine(person1 == person2); // output: True
}

Weitere Informationen finden Sie unter Nichtdestruktive Mutation im C#-Sprachreferenzartikel zu Datensätzen


(Records).
Integrierte Formatierung für die Anzeige
Datensatztypen verfügen über eine vom Compiler generierte ToString-Methode, die die Namen und Werte der
öffentlichen Eigenschaften und Felder anzeigt. Die ToString -Methode gibt eine Zeichenfolge in folgendem
Format zurück:

<record type name> { <property name> = <value>, <property name> = <value>, ...}

Für Verweistypen wird der Typname des Objekts, auf das die Eigenschaft verweist, anstelle des
Eigenschaftenwerts angezeigt. Im folgenden Beispiel ist das Array ein Verweistyp, sodass System.String[]
anstelle der tatsächlichen Arrayelementwerte angezeigt wird:

Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }

Weitere Informationen finden Sie unter Integriert Formatierung im C#-Sprachreferenzartikel zu Datensätzen


(Records).
Vererbung
Ein Datensatz kann von einem anderen Datensatz erben. Ein Datensatz kann jedoch nicht von einer Klasse erben,
und eine Klasse kann nicht von einem Datensatz erben.
Im folgenden Beispiel wird die Vererbung mit Positionseigenschaftensyntax veranschaulicht:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Damit zwei Datensatzvariablen gleich sind, muss der Laufzeittyp gleich sein. Die Typen der enthaltenden
Variablen können unterschiedlich sein. Dies wird im folgenden Codebeispiel veranschaulicht:

public abstract record Person(string FirstName, string LastName);


public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Person student = new Student("Nancy", "Davolio", 3);
Console.WriteLine(teacher == student); // output: False

Student student2 = new Student("Nancy", "Davolio", 3);


Console.WriteLine(student2 == student); // output: True
}

Im Beispiel verfügen alle Instanzen über dieselben Eigenschaften und Eigenschaftswerte. student == teacher
gibt jedoch False zurück, obwohl beide Variablen den Typ Person haben. Und student == student2 gibt True
zurück, obwohl eine Instanz eine Person -Variable und eine Instanz eine Student -Variable ist.
Alle öffentlichen Eigenschaften und Felder sowohl von abgeleiteten als auch von Basistypen sind in der
ToString -Ausgabe enthalten, wie im folgenden Beispiel gezeigt:

public abstract record Person(string FirstName, string LastName);


public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);

public static void Main()


{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Weitere Informationen finden Sie unter Vererbung im C#-Sprachreferenzartikel zu Datensätzen (Records).

init-only-Setter
Nur-init-Setter bieten eine konsistente Syntax zum Initialisieren von Objektmembern.
Eigenschafteninitialisierer verdeutlichen, welcher Wert welche Eigenschaft festlegt. Der Nachteil ist, dass diese
Eigenschaften festlegbar sein müssen. Ab C# 9.0 können Sie init -Zugriffsmethoden anstelle von set -
Zugriffsmethoden für Eigenschaften und Indexer erstellen. Aufrufer können diese Werte mithilfe der Syntax von
Eigenschafteninitialisierern in Erstellungsausdrücken festlegen. Diese Eigenschaften sind jedoch nach Abschluss
der Erstellung schreibgeschützt. Nur-init-Setter bieten Ihnen die Möglichkeit, den Zustand innerhalb eines
bestimmten Zeitfensters zu ändern. Dieses Zeitfenster schließt sich nach Abschluss der Konstruktionsphase. Die
Konstruktionsphase endet effektiv, nachdem die gesamte Initialisierung, einschließlich aller
Eigenschafteninitialisierer und with-Ausdrücke, abgeschlossen wurde.
Sie können Nur- init -Setter in einem jedem Typ deklarieren, den Sie schreiben. Die folgende Struktur definiert
z. B. eine Struktur zur Wetterbeobachtung:

public struct WeatherObservation


{
public DateTime RecordedAt { get; init; }
public decimal TemperatureInCelsius { get; init; }
public decimal PressureInMillibars { get; init; }

public override string ToString() =>


$"At {RecordedAt:h:mm tt} on {RecordedAt:M/d/yyyy}: " +
$"Temp = {TemperatureInCelsius}, with {PressureInMillibars} pressure";
}

Aufrufer können die Werte mithilfe der Syntax von Eigenschafteninitialisierern festlegen und gleichzeitig die
Unveränderlichkeit wahren:

var now = new WeatherObservation


{
RecordedAt = DateTime.Now,
TemperatureInCelsius = 20,
PressureInMillibars = 998.0m
};

Ein Versuch, eine Beobachtung nach der Initialisierung zu ändern, führt zu einem Compilerfehler:

// Error! CS8852.
now.TemperatureInCelsius = 18;

Nur-init-Setter können nützlich sein, um Basisklasseneigenschaften von abgeleiteten Klassen festzulegen. Sie
können auch mithilfe von Hilfsprogrammen abgeleitete Eigenschaften in einer Basisklasse festlegen. Positionelle
Datensätze deklarieren Eigenschaften mithilfe von Nur-init-Settern. Diese Setter werden in with-Ausdrücken
verwendet. Sie können Nur-init-Setter für jedes class -, struct - oder record -Element deklarieren, das Sie
definieren.
Weitere Informationen finden Sie unter init (C#-Referenz).

Top-Level-Anweisungen
Mithilfe von allgemeinen Anweisungen lässt sich der Code in vielen Anwendungen stark verkürzen. Dies ist
das kanonische Hallo-Welt-Programm („Hello World“):
using System;

namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

Nur eine der Codezeilen ruft eine Aktion hervor. Mit allgemeinen Anweisungen können Sie all diese
Codebausteine durch die using -Anweisung und die eine Zeile ersetzen, die die Aktion verursacht:

using System;

Console.WriteLine("Hello World!");

Wenn Sie ein einzeiliges Programm schreiben möchten, können Sie die using -Anweisung auch entfernen und
den vollqualifizierten Typnamen verwenden:

System.Console.WriteLine("Hello World!");

Allgemeine Anweisungen dürfen nur einer Anwendungsdatei eingesetzt werden. Wenn der Compiler in
mehreren Quelldateien allgemeine Anweisungen findet, führt dies zu einem Fehler. Ein Fehler wird ebenfalls
zurückgegeben, wenn Sie allgemeine Anweisungen mit einer deklarierten Einstiegspunktmethode des
Programms kombinieren (in der Regel eine Main -Methode). Sie können sich dies vorstellen, als ob eine Datei
die Anweisungen enthält, die normalerweise in die Main -Methode einer Program -Klasse geschrieben werden.
Einer der häufigsten Anwendungsfälle für dieses Feature ist die Erstellung von Lehrmaterial. Angehende C#-
Entwickler können die kanonische „Hallo Welt“-Anwendung in einer oder zwei Codezeilen schreiben. Keiner der
zusätzlichen Codebausteine ist erforderlich. Aber auch erfahrene Entwickler werden viele
Verwendungsmöglichkeiten für dieses Feature finden. Allgemeine Anweisungen bieten skriptähnliche
Experimentierfunktionen, ähnlich wie Jupyter Notebook-Instanzen. Allgemeine Anweisungen eignen sich auch
hervorragend für kleine Konsolenprogramme und Hilfsprogramme. Azure Functions ist ein idealer
Anwendungsfall für allgemeine Anweisungen.
Vor allem schränken allgemeine Anweisungen weder den Umfang noch die Komplexität einer Anwendung ein.
Diese Anweisungen können auf jede beliebige .NET-Klasse zugreifen oder diese verwenden. Außerdem
schränken sie nicht die Verwendung von Befehlszeilenargumenten oder Rückgabewerten ein. Allgemeine (top-
level) Anweisungen können auf ein Zeichenfolgenarray namens args zugreifen. Wenn allgemeine
Anweisungen einen ganzzahligen Wert zurückgeben, wird dieser Wert zum ganzzahligen Rückgabecode einer
synthetisierten Main -Methode. Allgemeine Anweisungen können async-Ausdrücke enthalten. In diesem Fall
gibt der synthetisierte Einstiegspunkt Task oder Task<int> zurück.
Weitere Informationen finden Sie unter Top-Level-Anweisungen im C#-Programmierleithandbuch.

Verbesserungen am Musterabgleich:
C# 9 enthält neue Verbesserungen am Musterabgleich:
Typmuster gleichen eine Variable mit einem Typ ab.
In Klammern gesetzte Muster erzwingen den Vorrang von Musterkombinationen oder heben diesen
hervor.
In konjunktiven and -Mustern müssen beide Muster übereinstimmen.
In disjunktiven or -Mustern muss eines von beiden Mustern übereinstimmen.
In negier ten not -Mustern darf ein Muster nicht übereinstimmen.
In relationalen Mustern muss die Eingabe kleiner als, größer als, kleiner gleich oder größer gleich einer
angegebenen Konstante sein.
Diese Muster erweitern die Mustersyntax. Sehen Sie sich die folgenden Beispiele an:

public static bool IsLetter(this char c) =>


c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

Mit optionalen Klammern, die verdeutlichen, dass and Vorrang vor or hat:

public static bool IsLetterOrSeparator(this char c) =>


c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';

Einer der gängigsten Anwendungsfälle ist eine neue Syntax für NULL-Überprüfungen:

if (e is not null)
{
// ...
}

Jedes dieser Muster kann in jedem Kontext verwendet werden, in dem Muster zulässig sind: is -
Musterausdrücke, switch -Ausdrücke, geschachtelte Muster und das Muster einer case -Bezeichnung einer
switch -Anweisung.

Weitere Informationen finden Sie unter Muster (C#-Referenz).


Weitere Informationen finden Sie in den Abschnitten Relationale Muster und Logische Muster des Artikels
Muster.

Leistung und Interop


Diese drei neuen Features verbessern die Unterstützung für die native Interop und spezifische Bibliotheken, die
eine hohe Leistung erfordern: ganze Zahlen mit nativer Größe, Funktionszeiger und das Auslassen des
localsinit -Flags.

Ganze Zahlen mit nativer Größe, nint und nuint , sind ganzzahlige Typen. Sie werden durch die zugrunde
liegenden Typen System.IntPtr und System.UIntPtr ausgedrückt. Der Compiler gibt zusätzliche Konvertierungen
und Vorgänge für diese Typen als native ganze Zahlen aus. Integer mit nativer Größe definieren die
Eigenschaften für MaxValue oder MinValue . Diese Werte können nicht als Kompilierzeitkonstanten ausgedrückt
werden, da sie von der nativen Größe einer ganzen Zahl auf dem Zielcomputer abhängen. Diese Werte sind zur
Laufzeit schreibgeschützt. Konstantenwerte können für nint in folgendem Bereich verwendet werden: [
int.MinValue ... int.MaxValue ]. Konstantenwerte können für nuint in folgendem Bereich verwendet werden: [
uint.MinValue ... uint.MaxValue ]. Der Compiler führt eine konstante Faltung aller unären und binären
Operatoren mithilfe der Typen System.Int32 und System.UInt32 durch. Wenn das Ergebnis nicht in 32 Bit passt,
wird der Vorgang zur Laufzeit ausgeführt und nicht als Konstante angesehen. Ganze Zahlen mit nativer Größe
können die Leistung in Szenarios steigern, in denen ganzzahlige Mathematik intensiv angewendet und die
schnellstmögliche Leistung benötigt wird. Weitere Informationen finden Sie unter den nint - und nuint -Typen.
Funktionszeiger bieten eine einfache Syntax für den Zugriff auf die IL-Opcodes ldftn und calli . Sie können
Funktionszeiger mithilfe der neuen delegate* -Syntax deklarieren. Ein delegate* -Typ ist ein Typ von Zeiger. Bei
einem Aufruf des delegate* -Typs wird calli verwendet. Dies ist ein Unterschied zu einem Delegaten, der
callvirt für die Invoke() -Methode verwendet. Syntaktisch sind die Aufrufe identisch. Bei Aufrufen von
Funktionszeigern wird die managed -Aufrufkonvention verwendet. Wenn Sie deklarieren möchten, dass Sie die
unmanaged -Aufrufkonvention benötigen, müssen Sie nach der delegate* -Syntax das Schlüsselwort unmanaged
einfügen. Andere Aufrufkonventionen können mithilfe von Attributen in der delegate* -Deklaration angegeben
werden. Weitere Informationen finden Sie unter Unsicherer Code und Zeigertypen.
Schließlich können Sie System.Runtime.CompilerServices.SkipLocalsInitAttribute hinzufügen, um den Compiler
anzuweisen, das localsinit -Flag nicht auszugeben. Dieses Flag weist die Common Language Runtime an, alle
lokalen Variablen mit 0 (Null) zu initialisieren. Das localsinit -Flag ist das Standardverhalten von C# seit
Version 1.0. Die zusätzliche Nullinitialisierung kann jedoch in einigen Szenarios zu nachweisbaren
Leistungseinbußen führen, insbesondere wenn Sie stackalloc verwenden. In diesen Fällen können Sie
SkipLocalsInitAttribute hinzufügen. Sie können die Klasse einer einzelnen Methode oder Eigenschaft, zu class /
struct / interface oder sogar zu einem Modul hinzufügen. Dieses Attribut hat keine Auswirkung auf abstract
-Methoden. Es beeinflusst den für die Implementierung generierten Code. Weitere Informationen finden Sie
unter SkipLocalsInit -Attribut.
Diese Features können die Leistung in einigen Szenarios verbessern. Sie sollten jedoch nur nach einem
sorgfältigen Leistungsvergleich vor und nach der Einführung eingesetzt werden. Code, der ganze Zahlen in
nativer Größe enthält, muss auf mehreren Zielplattformen mit unterschiedlichen Größen von ganzen Zahlen
getestet werden. Die anderen Features erfordern unsicheren Code.

Anpassen und Fertigstellen von Features


Viele der anderen Features helfen Ihnen, Code effizienter zu schreiben. In C# 9.0 können Sie den Typ in einem
neuen new -Ausdruck weglassen, wenn der Typ des erstellten Objekts bereits bekannt ist. Die häufigste
Anwendungsfall hierfür sind Felddeklarationen:

private List<WeatherObservation> _observations = new();

Der Zieltyp new kann auch verwendet werden, wenn Sie ein neues Objekt erstellen müssen, das als Argument
an eine Methode übergeben werden soll. In diesem Fall können Sie eine ForecastFor() -Methode mit der
folgenden Signatur implementieren:

public WeatherForecast ForecastFor(DateTime forecastDate, WeatherForecastOptions options)

Sie können sie wie folgt aufrufen:

var forecast = station.ForecastFor(DateTime.Now.AddDays(2), new());

Ein weiterer nützlicher Anwendungsfall für dieses Feature ist die Kombination mit Nur-init-Eigenschaften, um
ein neues Objekt zu initialisieren:

WeatherStation station = new() { Location = "Seattle, WA" };

Mithilfe einer return new(); -Anweisung können Sie eine Instanz zurückgeben, die vom Standardkonstruktor
erstellt wurde.
Ein ähnliches Feature verbessert die Zieltypauflösung von bedingten Ausdrücken. Aufgrund dieser Änderung
müssen die beiden Ausdrücke keine implizite Konvertierung von einem in den anderen aufweisen, sondern
können beide über implizite Konvertierungen in einen Zieltyp verfügen. Diese Änderung wird Ihnen
wahrscheinlich nicht auffallen. Was Sie bemerken werden, ist, dass einige bedingte Ausdrücke, die zuvor eine
Umwandlung erforderten oder nicht kompiliert werden konnten, jetzt funktionieren.
Ab C# 9.0 können Sie Lambdaausdrücken oder anonymen Methoden den Modifizierer static hinzufügen.
Statische Lambdaausdrücke entsprechen den lokalen static -Funktionen: Eine statische Lambdafunktion oder
anonyme Methode kann weder lokale Variablen noch den Instanzzustand erfassen. Der Modifizierer static
verhindert, dass versehentlich andere Variablen erfasst werden.
Kovariante Rückgabetypen flexibilisieren die Rückgabetypen von override-Methoden. Eine override-Methode
kann einen Typ zurückgeben, der vom Rückgabetyp der überschriebenen Basismethode abgeleitet wurde. Dies
kann sowohl für Datensätze als auch für andere Typen nützlich sein, die virtuelle Klon- oder Factorymethoden
unterstützen.
Außerdem erkennen und verwenden foreach -Schleifen eine GetEnumerator -Erweiterungsmethode, die
ansonsten das foreach -Muster erfüllt. Diese Änderung bedeutet, dass foreach mit anderen musterbasierten
Konstruktionen, z. B. mit dem async-Muster, sowie der musterbasierten Dekonstruktion konsistent ist. In der
Praxis bedeutet diese Änderung, dass Sie jedem Typ foreach -Unterstützung hinzufügen können. Sie sollten die
Verwendung von „foreach“ jedoch auf die Fälle beschränken, in denen die Enumeration eines Objekts in Ihrem
Softwareentwurf sinnvoll ist.
Sie können auch Ausschussvariablen als Parameter für Lambdaausdrücke verwenden. So müssen Sie das
Argument nicht mehr benennen, und der Compiler muss es unter Umständen gar nicht verwenden. Sie nutzen
einfach _ für alle Argumente. Weitere Informationen finden Sie im Abschnitt Eingabeparameter eines
Lambdaausdrucks des Artikels Lambdaausdrücke.
Schließlich haben Sie nun die Möglichkeit, Attribute auf lokale Funktionen anzuwenden. Sie können
beispielsweise Nullable-Attributanmerkungen auf lokale Funktionen anwenden.

Unterstützung für Code-Generatoren


Die beiden letzten Features dienen der Unterstützung von C#-Code-Generatoren. C#-Code-Generatoren sind
eine Komponente, die Sie selbst schreiben können und die einem Roslyn-Analysetool oder einem Codefix ähnelt.
Der Unterschied besteht darin, dass Code-Generatoren Code analysieren und im Rahmen der Kompilierung
neue Quellcodedateien schreiben. Ein typischer Code-Generator durchsucht Code nach Attributen oder weiteren
Konventionen.
Ein Code-Generator liest Attribute oder andere Codeelemente mithilfe der Roslyn-Analyse-APIs. Auf Grundlage
dieser Informationen fügt er der Kompilierung neuen Code hinzu. Quell-Generatoren können nur Code
hinzufügen. Sie sind nicht berechtigt, vorhandenen Code während der Kompilierung zu ändern.
Bei den beiden Features für Code-Generatoren sind handelt es sich um Erweiterungen für par tielle
Methodensyntax und für Modulinitialisierer. Zuerst zu den Änderungen an partiellen Methoden: Vor C# 9.0
waren partielle Methoden privat ( private ) und konnten weder einen Zugriffsmodifizierer angeben noch über
eine leere Rückgabe ( void ) oder out -Parameter verfügen. Diese Einschränkungen führten dazu, dass der
Compiler alle Aufrufe von partiellen Methoden entfernte, wenn keine Methodenimplementierung bereitgestellt
wurde. In C# 9.0 werden diese Einschränkungen behoben. Deklarationen von partiellen Methoden müssen jetzt
jedoch implementiert werden. Code-Generatoren können diese Implementierung bereitstellen. Damit kein
Breaking Change eingeführt wird, befolgt der Compiler bei jeder partiellen Methode, die keinen
Zugriffsmodifizierer aufweist, die alten Regeln. Wenn die partielle Methode den Zugriffsmodifizierer private
enthält, unterliegt die partielle Methode den neuen Regeln. Weitere Informationen finden Sie unter Partielle
Methode (C#-Referenz).
Das zweite neue Feature für Code-Generatoren sind Modulinitialisierer . Modulinitialisierer sind Methoden, an
die das Attribut ModuleInitializerAttribute angefügt wurde. Diese Methoden werden von der Runtime vor jedem
anderen Feldzugriff oder Methodenaufruf innerhalb des gesamten Moduls aufgerufen. Ein Modulinitialisierer:
muss statisch sein
muss parameterlos sein
muss eine leere Rückgabe („void“) zurückgeben
darf keine generische Methode sein
darf nicht in einer generischen Klasse enthalten sein
muss für das Modul zugänglich sein, in dem er enthalten ist
Der letzte Aufzählungspunkt bedeutet, dass die Methode und die Klasse, in der die Methode enthalten ist, intern
oder öffentlich sein müssen. Diese Methode darf keine lokale Funktion sein. Weitere Informationen finden Sie
unter ModuleInitializer -Attribut.
Neues in C# 8.0
04.11.2021 • 16 minutes to read

C# 8.0 fügt der Sprache C# die folgenden Features und Verbesserungen hinzu:
Readonly-Member
Standardschnittstellenmethoden
Verbesserungen am Musterabgleich:
switch-Ausdrücke
Eigenschaftsmuster
Tupelmuster
Positionsmuster
using-Deklarationen
Statische lokale Funktionen
Verwerfbare Referenzstrukturen
Nullwerte zulassende Verweistypen
Asynchrone Streams
Asynchrone verwerfbare Typen
Indizes und Bereiche
NULL-Coalescing-Zuweisung
Nicht verwaltete konstruierte Typen
Stackalloc in geschachtelten Ausdrücken
Erweiterung von interpolierten ausführlichen Zeichenfolgen
C# 8.0 wird unter .NET Core 3.x und .NET Standard 2.1 unterstützt. Weitere Informationen finden Sie unter
C#-Sprachversionsverwaltung.
Der Rest dieses Artikels beschreibt diese Funktionen kurz. Wenn ausführliche Artikel verfügbar sind, werden
Links zu diesen Tutorials und Übersichten bereitgestellt. Sie können sich diese Funktionen in unserer Umgebung
mit dem globalen dotnet try -Tool näher ansehen:
1. Installieren Sie das globale dotnet-try-Tool.
2. Klonen Sie das dotnet/try-samples-Repository.
3. Legen Sie das aktuelle Verzeichnis auf das Unterverzeichnis csharp8 für das try-samples-Repository fest.
4. Führen Sie aus dotnet try .

Readonly-Member
Sie können den readonly -Modifizierer auf jeden Member einer Struktur anwenden. Damit wird angezeigt, dass
der Member den Zustand nicht ändert. Dies ist granularer als das Anwenden des readonly -Modifikators auf
eine struct -Deklaration. Betrachten Sie folgende veränderliche Struktur:
public struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Distance => Math.Sqrt(X * X + Y * Y);

public override string ToString() =>


$"({X}, {Y}) is {Distance} from the origin";
}

Wie bei den meisten Strukturen verändert die ToString() -Methode den Zustand nicht. Sie könnten dies durch
Hinzufügen des readonly -Modifikators zur Deklaration von ToString() angeben:

public readonly override string ToString() =>


$"({X}, {Y}) is {Distance} from the origin";

Die vorhergehende Änderung generiert eine Compilerwarnung, weil ToString auf die Distance -Eigenschaft
zugreift, die nicht als readonly markiert ist:

warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an
implicit copy of 'this'

Der Compiler warnt Sie, wenn er eine Defensivkopie erstellen muss. Die Distance -Eigenschaft verändert nicht
den Zustand, sodass Sie diese Warnung aufheben können, indem Sie der Deklaration den readonly -
Modifizierer hinzufügen:

public readonly double Distance => Math.Sqrt(X * X + Y * Y);

Beachten Sie, dass der readonly -Modifizierer bei einer schreibgeschützten Eigenschaft erforderlich ist. Der
Compiler geht nicht davon aus, dass get -Zugriffsmethoden den Zustand nicht ändern. Sie müssen readonly
explizit deklarieren. Automatisch implementierte Eigenschaften sind eine Ausnahme; der Compiler behandelt
alle automatisch implementierten Getter als readonly , sodass es hier nicht notwendig ist, den readonly -
Modifizierer zu den X - und Y -Eigenschaften hinzuzufügen.
Der Compiler erzwingt die Regel, dass readonly -Member den Status nicht ändern. Die folgende Methode wird
nicht kompiliert, es sei denn, Sie entfernen den readonly -Modifizierer:

public readonly void Translate(int xOffset, int yOffset)


{
X += xOffset;
Y += yOffset;
}

Mit diesem Feature können Sie Ihre Designabsicht angeben, damit der Compiler sie erzwingen und
Optimierungen basierend auf dieser Absicht vornehmen kann.
Weitere Informationen finden Sie im Abschnitt readonly -Instanzmember des Artikels Strukturtypen.

Standardschnittstellenmethoden
Sie können nun Member zu Schnittstellen hinzufügen und eine Implementierung für diese Member
bereitstellen. Dieses Sprachfeature ermöglicht es API-Autoren, in späteren Versionen Methoden zu einer
Schnittstelle hinzuzufügen, ohne die Quell- oder Binärkompatibilität mit bestehenden Implementierungen dieser
Schnittstelle zu beeinträchtigen. Bestehende Implementierungen erben die Standardimplementierung. Dieses
Feature ermöglicht zudem die Interaktion zwischen C# und APIs, die auf Android oder Swift abzielen und
ähnliche Funktionen unterstützen. Standardschnittstellenmethoden ermöglichen auch Szenarien, die einem
„Traits“-Sprachfeature ähneln.
Standardschnittstellenmethoden wirken sich auf viele Szenarien und Sprachelemente aus. Unser erstes Tutorial
behandelt die Aktualisierung einer Schnittstelle mit Standardimplementierungen.

Weitere Muster an mehr Orten


Musterabgleich bietet Tools zur Bereitstellung formabhängiger Funktionalität für verwandte, aber
unterschiedliche Datentypen. C# 7.0 führte eine Syntax für Typmuster und konstante Muster ein, indem der
Ausdruck is und die Anweisung switch verwendet wurden. Diese Funktionen stellten die ersten vorläufigen
Schritte zur Unterstützung von Programmierparadigmen dar, bei denen Daten und Funktionalität getrennt
voneinander sind. Da sich die Branche immer mehr auf Mikroservices und andere Cloud-basierte Architekturen
konzentriert, werden andere Sprachtools benötigt.
C# 8.0 erweitert dieses Vokabular, sodass Sie mehr Musterausdrücke an mehreren Stellen in Ihrem Code
verwenden können. Berücksichtigen Sie diese Funktionen, wenn Ihre Daten und Funktionen getrennt sind.
Berücksichtigen Sie den Musterabgleich, wenn Ihre Algorithmen von einer anderen Tatsache als dem
Runtimetyp eines Objekts abhängen. Diese Verfahren bieten eine weitere Möglichkeit, Entwürfe auszudrücken.
Zusätzlich zu neuen Mustern an neuen Orten fügt C# 8.0 rekursive Muster hinzu. Rekursive Muster sind
Muster, die andere Muster enthalten können.
Switch-Ausdrücke
Häufig liefert eine switch -Anweisung in jedem ihrer case -Blöcke einen Wert. Mit switch-Ausdrücken
können Sie eine präzisere Ausdruckssyntax verwenden. Es gibt weniger repetitive case - und break -
Schlüsselwörter und weniger geschweifte Klammern. Betrachten Sie als Beispiel die folgende Enumeration, die
die Farben des Regenbogens enumeriert:

public enum Rainbow


{
Red,
Orange,
Yellow,
Green,
Blue,
Indigo,
Violet
}

Wenn Ihre Anwendung einen RGBColor -Typ definiert hat, der aus den Komponenten R , G und B aufgebaut
ist, können Sie einen Rainbow -Wert in seine RGB-Werte konvertieren, indem Sie die folgende Methode
verwenden, die einen Switch-Ausdruck enthält:
public static RGBColor FromRainbow(Rainbow colorBand) =>
colorBand switch
{
Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00),
Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00),
Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF),
Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
_ => throw new ArgumentException(message: "invalid enum value", paramName:
nameof(colorBand)),
};

Es gibt hier verschiedene Syntaxverbesserungen:


Die Variable steht vor dem switch -Schlüsselwort. Die unterschiedliche Reihenfolge macht es optisch
einfach, den switch-Ausdruck von der switch-Anweisung zu unterscheiden.
Die case - und : -Elemente werden durch => ersetzt. Es ist präziser und intuitiver.
Der default -Fall wird durch eine _ -Ausschlussvariable ersetzt.
Die Textkörper sind Ausdrücke, keine Anweisungen.
Stellen Sie dies mit dem entsprechenden Code unter Verwendung der klassischen switch -Anweisung
gegenüber:

public static RGBColor FromRainbowClassic(Rainbow colorBand)


{
switch (colorBand)
{
case Rainbow.Red:
return new RGBColor(0xFF, 0x00, 0x00);
case Rainbow.Orange:
return new RGBColor(0xFF, 0x7F, 0x00);
case Rainbow.Yellow:
return new RGBColor(0xFF, 0xFF, 0x00);
case Rainbow.Green:
return new RGBColor(0x00, 0xFF, 0x00);
case Rainbow.Blue:
return new RGBColor(0x00, 0x00, 0xFF);
case Rainbow.Indigo:
return new RGBColor(0x4B, 0x00, 0x82);
case Rainbow.Violet:
return new RGBColor(0x94, 0x00, 0xD3);
default:
throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand));
};
}

Weitere Informationen finden Sie unter switch -Ausdruck.


Eigenschaftsmuster
Mit dem Eigenschaftsmuster können Sie die Eigenschaften des untersuchten Objekts anpassen. Denken Sie
an eine eCommerce-Website, die die Umsatzsteuer auf der Grundlage der Adresse des Käufers berechnen muss.
Diese Berechnung ist keine Kernaufgabe einer Address -Klasse. Sie wird sich im Laufe der Zeit ändern,
wahrscheinlich häufiger als Adressformatänderungen. Die Höhe der Umsatzsteuer hängt von der State -
Eigenschaft der Adresse ab. Die folgende Methode verwendet das Eigenschaftsmuster, um die Umsatzsteuer aus
der Adresse und dem Preis zu berechnen:
public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
location switch
{
{ State: "WA" } => salePrice * 0.06M,
{ State: "MN" } => salePrice * 0.075M,
{ State: "MI" } => salePrice * 0.05M,
// other cases removed for brevity...
_ => 0M
};

Ein Musterabgleich erstellt eine präzise Syntax,. um diesen Algorithmus auszudrücken.


Weitere Informationen finden Sie im Abschnitt Eigenschaftsmuster des Artikels Muster.
Tupelmuster
Einige Algorithmen sind von mehreren Eingaben abhängig. Tupelmuster erlauben Ihnen, auf Grundlage
mehrerer Werte, ausgedrückt als ein Tupel, zu wechseln („switch“). Der folgende Code zeigt einen switch-
Ausdruck für das Spiel Stein, Schere, Papier (Schnick, Schnack, Schnuck):

public static string RockPaperScissors(string first, string second)


=> (first, second) switch
{
("rock", "paper") => "rock is covered by paper. Paper wins.",
("rock", "scissors") => "rock breaks scissors. Rock wins.",
("paper", "rock") => "paper covers rock. Paper wins.",
("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
("scissors", "rock") => "scissors is broken by rock. Rock wins.",
("scissors", "paper") => "scissors cuts paper. Scissors wins.",
(_, _) => "tie"
};

Die Meldungen zeigen den Gewinner an. Der Fall „Verwerfen“ („case discard“) stellt die drei Kombinationen für
Unentschieden oder andere Texteingaben dar.
Positionsmuster
Einige Typen umfassen eine Deconstruct -Methode, die ihre Eigenschaften in diskrete Variablen dekonstruiert.
Wenn auf eine Deconstruct -Methode zugegriffen werden kann, können Sie Positionsmuster verwenden, um
Eigenschaften des Objekts zu untersuchen, und diese Eigenschaften für ein Muster verwenden. Beachten Sie die
folgende Point -Klasse, die eine Deconstruct -Methode umfasst, um diskrete Variablen für X und Y zu
erstellen:

public class Point


{
public int X { get; }
public int Y { get; }

public Point(int x, int y) => (X, Y) = (x, y);

public void Deconstruct(out int x, out int y) =>


(x, y) = (X, Y);
}

Beachten Sie zudem die folgende Enumeration, die verschiedene Positionen eines Quadranten darstellt:
public enum Quadrant
{
Unknown,
Origin,
One,
Two,
Three,
Four,
OnBorder
}

Die folgende Methode verwendet das Positionsmuster , um die Werte von x und y zu extrahieren. Dann
wird mit einer when -Klausel der Quadrant des Punktes bestimmt:

static Quadrant GetQuadrant(Point point) => point switch


{
(0, 0) => Quadrant.Origin,
var (x, y) when x > 0 && y > 0 => Quadrant.One,
var (x, y) when x < 0 && y > 0 => Quadrant.Two,
var (x, y) when x < 0 && y < 0 => Quadrant.Three,
var (x, y) when x > 0 && y < 0 => Quadrant.Four,
var (_, _) => Quadrant.OnBorder,
_ => Quadrant.Unknown
};

Das Ausschussmuster im vorherigen Switch stimmt überein, wenn entweder x oder y 0 ist, jedoch nicht
beide. Ein switch-Ausdruck muss entweder einen Wert erzeugen oder eine Ausnahme auslösen. Wenn keiner
der Fälle übereinstimmt, löst der switch-Ausdruck eine Ausnahme aus. Der Compiler erzeugt für Sie eine
Warnung, wenn Sie nicht alle möglichen Fälle in Ihrem Switch-Ausdruck abdecken.
In diesem erweiterten Tutorial zum Musterabgleich erhalten Sie weitere Informationen zu
Musterabgleichverfahren. Weitere Informationen zu einem Positionsmuster finden Sie im Abschnitt
Positionsmuster des Artikels Muster.

Using-Deklarationen
Eine using-Deklaration ist eine Variablendeklaration, der das Schlüsselwort using vorangestellt ist. Es teilt
dem Compiler mit, dass die zu deklarierende Variable am Ende des umschließenden Bereichs angeordnet
werden soll. Sehen Sie sich beispielsweise den folgenden Code an, der eine Textdatei schreibt:

static int WriteLinesToFile(IEnumerable<string> lines)


{
using var file = new System.IO.StreamWriter("WriteLines2.txt");
int skippedLines = 0;
foreach (string line in lines)
{
if (!line.Contains("Second"))
{
file.WriteLine(line);
}
else
{
skippedLines++;
}
}
// Notice how skippedLines is in scope here.
return skippedLines;
// file is disposed here
}
Im vorhergehenden Beispiel wird die Datei angeordnet, wenn die schließende Klammer für die Methode erreicht
ist. Dies ist das Ende des Bereichs, in dem file deklariert wird. Der obige Code entspricht dem folgenden Code
mit klassischer using-Anweisung:

static int WriteLinesToFile(IEnumerable<string> lines)


{
using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
{
int skippedLines = 0;
foreach (string line in lines)
{
if (!line.Contains("Second"))
{
file.WriteLine(line);
}
else
{
skippedLines++;
}
}
return skippedLines;
} // file is disposed here
}

Im vorhergehenden Beispiel wird die Datei angeordnet, wenn die der using -Anweisung zugeordnete
schließende Klammer erreicht ist.
In beiden Fällen generiert der Compiler den Aufruf von Dispose() . Der Compiler erzeugt einen Fehler, wenn der
Ausdruck in der using -Anweisung nicht verwerfbar ist.

Statische lokale Funktionen


Sie können nun den static -Modifikator zu lokalen Funktionen hinzufügen, um sicherzustellen, dass die lokale
Funktion keine Variablen aus dem umschließenden Bereich erfasst (referenziert). Dadurch wird CS8421 erzeugt,
„Eine statische lokale Funktion kann keine Referenz auf <variable> enthalten.“
Betrachten Sie folgenden Code. Die lokale Funktion LocalFunction greift auf die Variable y zu, die im
umschließenden Bereich (die Methode M ) deklariert ist. Daher kann LocalFunction nicht mit dem static -
Modifikator deklariert werden:

int M()
{
int y;
LocalFunction();
return y;

void LocalFunction() => y = 0;


}

Der folgende Code enthält eine statische lokale Funktion. Er kann statisch sein, da er nicht auf Variablen im
umschließenden Bereich zugreift:
int M()
{
int y = 5;
int x = 7;
return Add(x, y);

static int Add(int left, int right) => left + right;


}

Verwerfbare Referenzstrukturen
Ein mit dem ref -Modifizierer deklarierter struct darf keine Schnittstellen und damit auch keine IDisposable
implementieren. Aus diesem Grund Aktivieren einer ref struct um verworfen werden, muss eine zugängliche
void Dispose() Methode. Dieses Feature gilt auch für readonly ref struct -Deklarationen.

Nullwerte zulassende Verweistypen


In einem Nullwerte zulassenden Anmerkungskontext, wird jede Variable eines Referenztyps als keine
Nullwer te zulassender Referenztyp betrachtet. Wenn Sie angeben möchten, dass eine Variable Null sein
kann, müssen Sie den Typnamen mit ? ergänzen, um die Variable als keine Nullwer te zulassenden
Ver weistyp zu deklarieren.
Wenn der Verweistyp keine Nullwerte zulässt, verwendet der Compiler die Flussanalyse, um sicherzustellen,
dass lokale Variablen bei der Deklaration auf einen Nicht-Null-Wert initialisiert werden. Felder müssen während
der Erstellung initialisiert werden. Der Compiler erzeugt eine Warnung, wenn die Variable nicht durch einen
Aufruf eines der verfügbaren Konstruktoren oder durch einen Initialisierer festgelegt wird. Darüber hinaus kann
Verweistypen, die keine Nullwerte zulassen, kein Wert zugewiesen werden, der Null sein könnte.
Verweistypen, die keine Nullwerte zulassen werden nicht überprüft, um sicherzustellen, dass sie nicht mit Null
belegt oder initialisiert werden. Der Compiler verwendet jedoch die Flussanalyse, um sicherzustellen, dass jede
Variable eines Verweistypen, der Nullwerte zulässt, gegen null geprüft wird, bevor auf sie zugegriffen oder
einem nicht Verweistypen zugewiesen wird, der Nullwerte zulässt.
Mehr über die Funktion erfahren Sie in der Übersicht der Verweistypen, die Nullwerte zulassen. Probieren Sie es
selbst in einer neuen Anwendung in diesem Tutorial für Verweistypen, die Nullwerte zulassen aus. Weitere
Informationen über die Schritte zur Migration einer bestehenden Codebasis, um Verweistypen zu nutzen, die
Nullwerte zulassen, finden Sie im Artikel zum Upgrade auf Nullwerte zulassende Verweistypen.

Asynchrone Streams
Ab C# 8.0 können Sie Streams asynchron erstellen und nutzen. Eine Methode, die einen asynchronen Stream
zurückgibt, hat drei Eigenschaften:
1. Die Deklaration erfolgte mit dem async -Modifikator.
2. Es wird IAsyncEnumerable<T> zurückgegeben.
3. Das Verfahren enthält yield return -Anweisungen, um aufeinanderfolgende Elemente im asynchronen
Stream zurückzugeben.
Die Verwendung eines asynchronen Streams erfordert, dass Sie das Schlüsselwort await vor dem
Schlüsselwort foreach hinzufügen, wenn Sie die Elemente des Streams auflisten. Das Hinzufügen des
Schlüsselwortes await erfordert, dass die Methode, die den asynchronen Strom enumiert, mit dem Modifikator
async deklariert wird und einen für eine async -Methode zulässigen Typ zurückgibt. In der Regel ist dies die
Rückgabe von Task oder Task<TResult>. Es kann auch ValueTask oder ValueTask<TResult> sein. Eine Methode
kann einen asynchronen Stream sowohl verwenden als auch erzeugen, was bedeutet, dass sie
IAsyncEnumerable<T> zurückgeben würde. Der folgende Code erzeugt eine Sequenz von 0 bis 19 und wartet
100 ms zwischen der Generierung jeder Zahl:

public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()


{
for (int i = 0; i < 20; i++)
{
await Task.Delay(100);
yield return i;
}
}

Die Enumeration der Sequenz erfolgt mit der await foreach -Anweisung:

await foreach (var number in GenerateSequence())


{
Console.WriteLine(number);
}

Sie können asynchrone Streams selbst in unserem Tutorial zum Erstellen und Verwenden von asynchronen
Streams ausprobieren. Standardmäßig werden Streamelemente im erfassten Kontext verarbeitet. Wenn Sie die
Erfassung des Kontexts deaktivieren möchten, verwenden Sie die Erweiterungsmethode
TaskAsyncEnumerableExtensions.ConfigureAwait. Weitere Informationen über Synchronisierungskontexte und
die Erfassung des aktuellen Kontexts finden Sie im Artikel über das Verwenden des aufgabenbasierten
asynchronen Musters.

Asynchrone verwerfbare Typen


Ab C# 8.0 unterstützt die Sprache asynchrone verwerfbare Typen, die die System.IAsyncDisposable-Schnittstelle
implementieren. Verwenden Sie die Anweisung await using , um mit einem asynchron verwerfbaren Objekt zu
arbeiten. Weitere Informationen finden Sie im Artikel Implementieren einer DisposeAsync-Methode.

Indizes und Bereiche


Indizes und Bereiche bieten eine prägnante Syntax für den Zugriff auf einzelne Elemente oder Bereiche in einer
Sequenz.
Diese Sprachunterstützung basiert auf zwei neuen Typen und zwei neuen Operatoren:
System.Index: Stellt einen Index in einer Sequenz dar.
Der Index vom Endeoperator ^ , der angibt, dass ein Index relativ zum Ende der Sequenz ist.
System.Range: Stellt einen Unterbereich einer Sequenz dar.
Der Bereichsoperator .. , der den Beginn und das Ende eines Bereichs als seine Operanden angibt.
Beginnen wir mit den Regeln für Indizes. Betrachten Sie einen Array sequence . Der 0 -Index entspricht
sequence[0] . Der ^0 -Index entspricht sequence[sequence.Length] . Beachten Sie, dass sequence[^0] genau wie
sequence[sequence.Length] eine Ausnahme auslöst. Für eine beliebige Zahl n ist der Index ^n identisch mit
sequence.Length - n .

Ein Bereich gibt den Beginn und das Ende eines Bereichs an. Der Beginn des Bereichs ist inklusiv, das Ende des
Bereichs ist jedoch exklusiv. Das bedeutet, dass der Beginn im Bereich enthalten ist, das Ende aber nicht. Der
Bereich [0..^0] stellt ebenso wie [0..sequence.Length] den gesamten Bereich dar.
Schauen wir uns einige Beispiele an. Betrachten Sie das folgende Array, kommentiert mit seinem Index „from
the start“ und „from the end“:
var words = new string[]
{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5
"over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1
}; // 9 (or words.Length) ^0

Sie können das letzte Wort mit dem ^1 -Index abrufen:

Console.WriteLine($"The last word is {words[^1]}");


// writes "dog"

Der folgende Code erzeugt einen Teilbereich mit den Worten „quick“, „brown“ und „fox“. Er enthält words[1] bis
words[3] . Das Element words[4] befindet sich nicht im Bereich.

var quickBrownFox = words[1..4];

Der folgende Code erzeugt einen Teilbereich mit „lazy“ und „dog“. Dazu gehören words[^2] und words[^1] . Der
Endindex words[^0] ist nicht enthalten:

var lazyDog = words[^2..^0];

Die folgenden Beispiele erstellen Bereiche, die am Anfang, am Ende und auf beiden Seiten offen sind:

var allWords = words[..]; // contains "The" through "dog".


var firstPhrase = words[..4]; // contains "The" through "fox"
var lastPhrase = words[6..]; // contains "the", "lazy" and "dog"

Sie können Bereiche auch als Variablen deklarieren:

Range phrase = 1..4;

Der Bereich kann dann innerhalb der Zeichen [ und ] verwendet werden:

var text = words[phrase];

Indizes und Bereiche werden nicht nur von Arrays unterstützt. Indizes und Bereiche können auch mit string,
Span<T> oder ReadOnlySpan<T> verwendet werden. Weitere Informationen finden Sie unter Typunterstützung
für Indizes und Bereiche.
Weitere Informationen zu Indizes und Bereichen finden Sie im Tutorial zu Indizes und Bereichen.

NULL-Coalescing-Zuweisung
In C# 8.0 wird der NULL-Coalescing-Zuweisungsoperator ??= eingeführt. Sie können den ??= -Operator
verwenden, um den Wert des rechten Operanden dem linken Operanden nur dann zuzuweisen, wenn die
Auswertung des linken Operanden null ergibt.

List<int> numbers = null;


int? i = null;

numbers ??= new List<int>();


numbers.Add(i ??= 17);
numbers.Add(i ??= 20);

Console.WriteLine(string.Join(" ", numbers)); // output: 17 17


Console.WriteLine(i); // output: 17

Weitere Informationen finden Sie im Artikel zu den Operatoren ?? und ??=.

Nicht verwaltete konstruierte Typen


In C# 7.3 und früher darf ein konstruierter Typ (also ein Typ, der mindestens ein Typargument enthält) kein nicht
verwalteter Typ sein. Ab C# 8.0 ist ein konstruierter Werttyp nicht verwaltet, wenn er nur Felder von nicht
verwalteten Typen enthält.
Ein Beispiel: Ihr Code enthält die folgende Definition des generischen Coords<T> -Typs:

public struct Coords<T>


{
public T X;
public T Y;
}

Dann ist der Coords<int> -Typ in C# 8.0 und höher ein nicht verwalteter Typ. Wie bei allen nicht verwalteten
Typen können Sie einen Zeiger auf eine Variable dieses Typs erstellen oder Instanzen dieses Typs einen
Arbeitsspeicherblock im Stapel zuordnen:

Span<Coords<int>> coordinates = stackalloc[]


{
new Coords<int> { X = 0, Y = 0 },
new Coords<int> { X = 0, Y = 3 },
new Coords<int> { X = 4, Y = 0 }
};

Weitere Informationen finden Sie unter Nicht verwaltete Typen.

Stackalloc in geschachtelten Ausdrücken


Ab C# 8.0 können Sie, wenn das Ergebnis eines stackalloc-Ausdrucks vom Typ System.Span<T> oder
System.ReadOnlySpan<T> ist, den stackalloc -Ausdruck in anderen Ausdrücken verwenden:

Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };


var ind = numbers.IndexOfAny(stackalloc[] { 2, 4, 6, 8 });
Console.WriteLine(ind); // output: 1

Erweiterung von interpolierten ausführlichen Zeichenfolgen


$ - und @ -Token in interpolierten ausführlichen Zeichenfolgen können in beliebiger Reihenfolge vorliegen:
sowohl $@"..." als auch @$"..." sind gültige interpolierte ausführliche Zeichenfolgen. In früheren C#-
Versionen musste das Token $ vor dem Token @ vorhanden sein.
Neuerungen von C# 7.0 bis C# 7.3
04.11.2021 • 23 minutes to read

Von C# 7.0 bis C# 7.3 wurden zahlreiche Features für und inkrementelle Verbesserungen an den
Entwicklungsfunktionen von C# eingeführt. In diesem Artikel erhalten Sie einen Überblick über die neuen
Sprachfeatures und Compileroptionen. In den Beschreibungen wird das Verhalten für C# 7.3 erläutert. Dabei
handelt es sich um die aktuelle Version, die für auf dem .NET Framework basierende Anwendungen unterstützt
wird.
Das Konfigurationselement für die Sprachversionsauswahl wurde im Rahmen von C# 7.1 hinzugefügt. Damit
können Sie die Compilersprachenversion in Ihrer Projektdatei angeben.
Von C# 7.0 bis C# 7.3 wurden der C#-Sprache die folgenden Features und Designs hinzugefügt:
Tupel und Verwerfen
Sie können einfache, unbenannte Typen erstellen, die mehrere öffentliche Felder enthalten. Compiler
und IDE-Tools kennen die Semantik dieser Typen.
Ausschussvariablen (discards) sind temporäre, lesegeschützte Variablen, die in Zuweisungen
verwendet werden, wenn der zugewiesene Wert nicht weiter interessiert. Sie eignen sich besonders
zum Dekonstruieren von Tupeln und benutzerdefinierten Typen sowie beim Aufrufen von Methoden
mit out -Parametern.
Mustervergleich
Sie können Verzweigungslogik basierend auf beliebigen Typen und Werten der Member dieser Typen
erstellen.
async Main -Methode
Der Einstiegspunkt für eine Anwendung kann über den Modifizierer async verfügen.
Lokale Funktionen
Sie können Funktionen innerhalb von anderen Funktionen verschachteln, um deren Bereich und
Sichtbarkeit zu beschränken.
Mehr Ausdruckskörpermember
Die Liste der Member, die mithilfe von Ausdrücken erstellt werden können, ist länger geworden.
throw -Ausdrücke
Sie können Ausnahmen in Codekonstrukten auslösen, die vorher nicht zulässig waren, da throw eine
Anweisung war.
default Literale Ausdrücke
Sie können literale Standardausdrücke in Standardwertausdrücken verwenden, wenn der Zieltyp
abgeleitet werden kann.
Verbesserung der numerischen literalen Syntax
Neue Token verbessern die Lesbarkeit für numerische Konstanten.
out Variablen
Sie können out -Werte als Inlineargumente für die Methode deklarieren, wenn sie verwendet werden.
Nicht schließende benannte Argumente
Positionelle Argumente können auf benannte Argumente folgen.
private protected -Zugriffsmodifizierer
Der private protected -Zugriffsmodifizierer ermöglicht den Zugriff für abgeleitete Klassen innerhalb
der gleichen Assembly.
Verbesserte Überladungsauflösung
Neue Regeln sorgen nun für die Auflösung von Ambiguitäten bei Überladungsauflösungen.
Techniken zum Schreiben von sicherem, effizientem Code
Eine Kombination aus Verbesserungen der Syntax, die das Arbeiten mit Werttypen mithilfe von
Verweissemantik ermöglichen.
Schließlich verfügt der Compiler über neue Optionen:
-refout und -refonly , wodurch die Erstellung von Referenzassemblys gesteuert wird.
-publicsign , um das Signieren von Assemblys durch Open Source Software (OSS) zu ermöglichen.
-pathmap , um eine Zuordnung für Quellverzeichnisse bereitzustellen.

Dieser Artikel enthält im Folgenden eine Übersicht über die einzelnen Funktionen. Sie werden die Hintergründe
sowie die Syntax jedes einzelnen Features kennenlernen. Sie können sich diese Funktionen in unserer
Umgebung mit dem globalen dotnet try -Tool näher ansehen:
1. Installieren Sie das globale dotnet-try-Tool.
2. Klonen Sie das dotnet/try-samples-Repository.
3. Legen Sie das aktuelle Verzeichnis auf das Unterverzeichnis csharp7 für das try-samples-Repository fest.
4. Führen Sie aus dotnet try .

Tupel und Verwerfen


C# bietet eine umfangreiche Syntax für Klassen und Strukturen, die verwendet wird, um Ihre Entwurfsabsicht zu
erläutern. Aber manchmal erfordert diese umfangreiche Syntax zusätzliche Arbeit mit minimalem Nutzen.
Möglicherweise schreiben Sie häufig Methoden, die eine einfache Struktur erfordern, die mehr als ein
Datenelement enthält. Zur Unterstützung dieser Szenarios wurden C# Tupel hinzugefügt. Tupel sind einfache
Datenstrukturen, die mehrere Felder zur Darstellung der Datenmember enthalten. Die Felder werden nicht
überprüft, und Sie können keine eigenen Methoden definieren. C#-Tupeltypen unterstützen == und != .
Weitere Informationen.

NOTE
Tupel waren schon vor C# 7.0 verfügbar, sie waren jedoch ineffizient und hatten keine Sprachunterstützung. Das brachte
mit sich, dass auf Tupelelemente nur als Item1 , Item2 usw. verwiesen werden konnte. Mit C# 7.0 wird
Sprachunterstützung für Tupel eingeführt, wodurch semantische Namen für die Felder eines Tupels mit Einsatz neuer,
effizienterer Tupeltypen möglich werden.

Sie können ein Tupel erstellen, indem Sie jedem Member einen Wert zuweisen und ihnen optional auch
semantische Namen bereitstellen:

(string Alpha, string Beta) namedLetters = ("a", "b");


Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");

Das namedLetters -Tupel enthält Felder, die als Alpha und Beta bezeichnet werden. Diese Namen bestehen nur
zur Kompilierzeit und werden nicht beibehalten, wenn das Tupel beispielsweise zur Laufzeit mithilfe von
Reflektion untersucht wird.
In einer Tupelzuweisung können Sie auch die Namen der Felder auf der rechten Seite der Zuweisung angeben:

var alphabetStart = (Alpha: "a", Beta: "b");


Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");

Manchmal möchten Sie vielleicht die Member eines Tupels entpacken, die von einer Methode zurückgegeben
wurden. Sie können dazu für jeden Wert im Tupel separate Variablen deklarieren. Das Auspacken wird als
Dekonstruieren des Tupels bezeichnet:

(int max, int min) = Range(numbers);


Console.WriteLine(max);
Console.WriteLine(min);

Sie können auch eine ähnliche Dekonstruktion für alle Typen in .NET bereitstellen. Schreiben Sie eine
Deconstruct -Methode als Member der Klasse. Diese Deconstruct -Methode bietet eine Reihe von out -
Argumenten für jede der Eigenschaften, die Sie extrahieren möchten. Berücksichtigen Sie diese Point -Klasse,
die eine Dekonstruktionsmethode bereitstellt, die die X - und Y -Koordinaten extrahiert:

public class Point


{
public Point(double x, double y)
=> (X, Y) = (x, y);

public double X { get; }


public double Y { get; }

public void Deconstruct(out double x, out double y) =>


(x, y) = (X, Y);
}

Sie können die einzelnen Felder extrahieren, indem Sie einem Point ein Tupel zuweisen:

var p = new Point(3.14, 2.71);


(double X, double Y) = p;

Wenn Sie einen Tupel initialisieren, sind die Variablen, die für die rechte Seite der Zuweisung verwendet werden,
oft dieselben, wie die Namen, die Sie den Tupelelementen geben möchten. Die Namen von Tupelelementen
können von den Variablen abgeleitet werden, die zum Initialisieren der Tupel verwendet werden:

int count = 5;
string label = "Colors used in the map";
var pair = (count, label); // element names are "count" and "label"

Weitere Informationen zu diesem Feature finden Sie im Artikel Tupeltypen.


Beim Dekonstruieren eines Tupels oder dem Aufrufen einer Methode mit out -Parametern sind Sie gezwungen,
eine Variable zu definieren, deren Wert Sie nicht interessiert und die Sie nicht zu verwenden beabsichtigen. C#
verfügt jetzt über Unterstützung für Ausschussvariablen (discards), um diesem Szenario Rechnung zu tragen.
Eine Ausschussvariable ist eine lesegeschützte Variable mit dem Namen _ (dem Unterstrichzeichen); Sie
können der einzelnen Variablen alle Werte zuweisen, die Sie verwerfen möchten. Eine Ausschussvariable ist wie
eine nicht zugewiesene Variable; abgesehen von der Zuweisungsanweisung kann die Ausschussvariable nicht in
Code verwendet werden.
Ausschussvariablen werden in den folgenden Szenarien unterstützt:
Beim Dekonstruieren von Tupeln oder benutzerdefinierten Typen.
Beim Aufrufen von Methoden mit out-Parametern.
In einem Musterabgleichvorgang mit dem is -Operator und der switch -Anweisung.
Als eigenständiger Bezeichner, wenn Sie den Wert einer Zuweisung explizit als Ausschuss kennzeichnen
möchten.
Das folgende Beispiel definiert eine QueryCityData -Methode, die ein 3-Tupel zurückgibt, das ein Datum für eine
Stadt für zwei verschiedene Jahre enthält. Der Methodenaufruf im Beispiel befasst sich nur mit den zwei
Bevölkerungswerten, die von der Methode zurückgegeben werden und behandelt so die verbleibenden Werte
im Tupel beim Dekonstruieren des Tupels als Ausschuss.

using System;

public class Example


{
public static void Main()
{
var (_, pop, _) = QueryCityData("New York City");

// Do something with the data.


}

private static (string name, int pop, double size) QueryCityData(string name)
{
if (name == "New York City")
return (name, 8175133, 468.48);

return ("", 0, 0);


}
}

Weitere Informationen finden Sie unter Ausschuss.

Musterabgleich
Beim Musterabgleich handelt es sich um mehrere Features, die neue Möglichkeiten eröffnen, Ablaufsteuerung in
Ihrem Code auszudrücken. Sie können Variablen nach Typ, Werten oder den Werten ihrer Eigenschaften testen.
Dieses Vorgehen sorgt für einen besser lesbaren Codeflow.
Mustervergleich unterstützt is -Ausdrücke und switch -Ausdrücke. Jeder davon ermöglicht das Überprüfen
eines Objekts und dessen Eigenschaften, um zu bestimmen, ob das Objekt dem gesuchten Muster entspricht. Sie
verwenden das when -Schlüsselwort, um zusätzliche Regeln für das Muster anzugeben.
Der Musterausdruck is erweitert den vertrauten is -Operator, um eine Abfrage zum Typ eines Objekts
auszuführen und das Ergebnis in einer Anweisung zuzuweisen. Der folgende Code überprüft, ob es sich bei
einer Variablen um einen int -Wert handelt und fügt sie, wenn dies der Fall ist, der aktuellen Summe hinzu:

if (input is int count)


sum += count;

Das vorausgegangene kleine Beispiel verdeutlicht die Verbesserungen des is -Ausdrucks. Sie können für Wert-
und Verweistypen testen und das erfolgreiche Ergebnis einer neuen Variable des richtigen Typs zuweisen.
Der Switch-Vergleichsausdruck verfügt über eine vertraute Syntax basierend auf der switch -Anweisung, die
bereits Teil der C#-Sprache ist. Die aktualisierte Switch-Anweisung verfügt über mehrere neue Konstrukte:
Der maßgebliche Typ eines switch -Ausdrucks ist nicht mehr beschränkt auf ganzzahlige Typen, Enum -
Typen, string oder einen Nullable-Typ, der einem dieser Typen entspricht. Es kann jeder Typ verwendet
werden.
Sie können den Typ des switch -Ausdrucks in jeder case -Bezeichnung testen. Sie können diesem Typ wie
schon beim is -Ausdruck eine neue Variable zuweisen.
Wenn Sie die Bedingungen für diese Variable weiter testen möchten, können Sie eine when -Klausel
hinzufügen.
Die Reihenfolge der case -Bezeichnungen ist hierbei entscheidend. Die erste Verzweigung, für die eine
Übereinstimmung gefunden wird, wird ausgeführt. Alle weiteren werden übersprungen.
Im folgenden Code werden diese Funktionen veranschaulicht:

public static int SumPositiveNumbers(IEnumerable<object> sequence)


{
int sum = 0;
foreach (var i in sequence)
{
switch (i)
{
case 0:
break;
case IEnumerable<int> childSequence:
{
foreach(var item in childSequence)
sum += (item > 0) ? item : 0;
break;
}
case int n when n > 0:
sum += n;
break;
case null:
throw new NullReferenceException("Null found in sequence");
default:
throw new InvalidOperationException("Unrecognized type");
}
}
return sum;
}

case 0: ist ein Konstantenmuster.


case IEnumerable<int> childSequence: ist ein Deklarationsmuster.
case int n when n > 0: ist ein Deklarationsmuster mit einer zusätzlichen when -Bedingung.
case null: ist das null -Konstantenmuster.
default: ist die vertraute Standardanfrage.

Ab C# 7.1 kann der Musterausdruck für is und das switch -Typmuster den Typ eines generischen
Typparameters haben. Dies kann sehr nützlich sein, wenn Sie Typen überprüfen, die entweder struct - oder
class -Typen sein können, und Sie Boxing vermeiden möchten.

Weitere Informationen zum Mustervergleich finden Sie unter Pattern Matching in C# (Mustervergleich in C#).

Async Main
Mithilfe einer async main-Methode können Sie await in Ihrer Main -Methode verwenden. Vorher hätten Sie
das Folgende schreiben müssen:

static int Main()


{
return DoAsyncWork().GetAwaiter().GetResult();
}

Nun können Sie das Folgende schreiben:


static async Task<int> Main()
{
// This could also be replaced with the body
// DoAsyncWork, including its await expressions:
return await DoAsyncWork();
}

Wenn Ihr Programm keinen Exitcode zurückgibt, können Sie eine Main -Methode deklarieren, die einen Task
zurückgibt:

static async Task Main()


{
await SomeAsyncMethod();
}

Ausführliche Informationen finden Sie unter Async Main im Programmierhandbuch.

Lokale Funktionen
Viele Entwürfe für Klassen enthalten Methoden, die nur von einer Stelle aufgerufen werden. Durch diese
zusätzlichen privaten Methoden bleibt jede Methode klein und konzentriert. Mit lokalen Funktionen können Sie
Methoden innerhalb des Kontexts einer anderen Methode deklarieren. So können Leser der Klasse leichter
erkennen, dass die lokale Methode nur aus dem Kontext aufgerufen wird, in dem sie deklariert wird.
Es gibt zwei häufige Anwendungsfälle für lokale Funktionen: öffentliche Iteratormethoden und öffentliche
asynchrone Methoden. Beide Arten von Methoden generieren Code, der Fehler später meldet, als
Programmierer erwarten würden. Bei Iteratormethoden werden Ausnahmen nur festgestellt, wenn Code
aufgerufen wird, der die zurückgegebene Sequenz auflistet. Bei asynchronen Methoden werden Ausnahmen nur
festgestellt, wenn der zurückgegebene Task -Wert erwartet wird. Im folgenden Beispiel wird veranschaulicht,
wie mithilfe einer lokalen Funktion die Überprüfung der Parameter von der Iteratorimplementierung getrennt
wird:

public static IEnumerable<char> AlphabetSubset3(char start, char end)


{
if (start < 'a' || start > 'z')
throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
if (end < 'a' || end > 'z')
throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");

if (end <= start)


throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");

return alphabetSubsetImplementation();

IEnumerable<char> alphabetSubsetImplementation()
{
for (var c = start; c < end; c++)
yield return c;
}
}

Das gleiche Verfahren kann mit async -Methoden eingesetzt werden, um sicherzustellen, dass Ausnahmen
aufgrund der Argumentüberprüfung ausgelöst werden, bevor die asynchrone Arbeit beginnt:
public Task<string> PerformLongRunningWork(string address, int index, string name)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-
negative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

return longRunningWorkImplementation();

async Task<string> longRunningWorkImplementation()


{
var interimResult = await FirstWork(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}. Enjoy.";
}
}

Diese Syntax wird jetzt unterstützt:

[field: SomeThingAboutFieldAttribute]
public int SomeProperty { get; set; }

Das Attribut wird auf das vom Compiler generierte Unterstützungsfeld für
SomeThingAboutFieldAttribute
SomeProperty angewendet. Weitere Informationen finden Sie unter attributes im C#-Programmierhandbuch.

NOTE
Einige der Entwürfe, die von lokalen Funktionen unterstützt werden, können auch mithilfe von Lambdaausdrücken
erreicht werden. Weitere Informationen finden Sie unter Lokale Funktionen im Vergleich zu Lambdaausdrücken.

Mehr Ausdruckskörpermember
In C# 6 wurden Ausdruckskörpermember für Memberfunktionen und schreibgeschützte Eigenschaften
eingeführt. Mit C# 7.0 werden die zulässigen Member erweitert, die als Ausdrücke implementiert werden
können. In C# 7.0 können Sie Konstruktoren, Finalizer sowie get - und set -Zugriffsmethoden für
Eigenschaften und Indexer implementieren. Der folgende Code zeigt entsprechende Beispiele:

// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// Expression-bodied get / set accessors.


public string Label
{
get => label;
set => this.label = value ?? "Default label";
}
NOTE
In diesem Beispiel ist kein Finalizer erforderlich, aber damit soll die Syntax dargestellt werden. Sie sollten in Ihrer Klasse nur
einen Finalizer implementieren, wenn es notwendig ist, nicht verwaltete Ressourcen freizugeben. Sie sollten auch die
Verwendung der SafeHandle-Klasse in Betracht ziehen, anstatt nicht verwaltete Ressourcen direkt zu verwalten.

Diese neuen Speicherorte für Member mit Ausdruckskörper sind ein wichtiger Meilenstein für die Sprache C#:
Diese Features wurden von Community-Mitgliedern implementiert, die am Open Source-Projekt Roslyn
arbeiten.
Das Ändern einer Methode in ein Ausdruckskörpermember ist eine binärkompatible Änderung.

Throw-Ausdrücke
In C# ist throw schon immer eine Anweisung. Da throw eine Anweisung und kein Ausdruck ist, gab es C#-
Konstrukte, in denen diese Anweisung nicht verwendet werden konnte. Darunter waren bedingte Ausdrücke,
NULL-Sammelausdrücke und einige Lambdaausdrücke. Das Hinzufügen von Ausdruckskörpermembern fügt
mehr Speicherorte hinzu, bei denen throw -Ausdrücke nützlich wären. Damit Sie diese Konstrukte schreiben
können, wurden in C# 7.0 Throw-Ausdrücke eingeführt.
Diese Ergänzung erleichtert das Schreiben ausdrucksbasierteren Codes. Sie benötigen zur Fehlerüberprüfung
keine weiteren Anweisungen.

Literale Standardausdrücke
Literale Standardausdrücke sind eine Erweiterung von Ausdrücken mit Standardwert. Diese Ausdrücke
initialisieren eine Variable auf dem Standardwert. Dort, wo Sie vorher das Folgende geschrieben haben:

Func<string, bool> whereClause = default(Func<string, bool>);

Können Sie nun den Typ weglassen, der auf der rechten Seite der Initialisierung steht.

Func<string, bool> whereClause = default;

Weitere Informationen finden Sie im Abschnitt Standardliteral des Artikels zum default-Operator.

Verbesserung der numerischen literalen Syntax


Falsches Lesen numerischer Konstanten kann Code beim ersten Lesen schwerer verständlich machen. Bitmasken
oder andere symbolische Werte können Missverständnisse hervorrufen. C# 7.0 enthält zwei neue Funktionen,
mit denen Zahlen für den beabsichtigten Zweck so lesbar wie möglich geschrieben werden können: binäre
Literale und Zifferntrennzeichen.
Beim Erstellen von Bitmasken oder wenn die binäre Darstellung einer Zahl den Code besonders gut lesbar
macht, sollten Sie diese Zahl im Binärformat schreiben:

public const int Sixteen = 0b0001_0000;


public const int ThirtyTwo = 0b0010_0000;
public const int SixtyFour = 0b0100_0000;
public const int OneHundredTwentyEight = 0b1000_0000;

Das 0b am Anfang der Konstante gibt an, dass die Zahl als binäre Zahl geschrieben wird. Binäre Zahlen können
lang werden. Daher kann die Einführung von _ als Ziffertrennzeichen wie in der binären Konstante im
vorherigen Beispiel gezeigt die Bitmuster übersichtlicher machen. Das Zifferntrennzeichen kann überall in der
Konstante angezeigt werden. Für Zahlen der Basis 10 wird es üblicherweise als Tausendertrennzeichen
verwendet. Hexadezimale und binäre numerische Literale dürfen jetzt mit _ beginnen:

public const long BillionsAndBillions = 100_000_000_000;

Das Zifferntrennzeichen kann auch mit decimal -, float - und double -Typen verwendet werden:

public const double AvogadroConstant = 6.022_140_857_747_474e23;


public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;

Insgesamt können Sie numerische Konstanten viel leserlicher deklarieren.

out -Variablen
Die vorhandene Syntax zur Unterstützung von out -Parametern wurde in C# 7 verbessert. Nun können Sie out
-Variablen in der Argumentliste eines Methodenaufrufs deklarieren, anstatt eine separate
Deklarationsanweisung zu schreiben:

if (int.TryParse(input, out int result))


Console.WriteLine(result);
else
Console.WriteLine("Could not parse input");

Sie sollten den Typ der out -Variablen aus Gründen der Übersichtlichkeit angeben. Dies wird im vorherigen
Beispiel veranschaulicht. Die Sprache unterstützt jedoch die Verwendung einer implizit typisierten lokalen
Variable:

if (int.TryParse(input, out var answer))


Console.WriteLine(answer);
else
Console.WriteLine("Could not parse input");

Der Code ist einfacher zu lesen.


Sie deklarieren die out-Variable, wenn Sie sie verwenden, nicht in einer anderen Codezeile weiter
oben.
Sie müssen keinen Anfangswert zuweisen.
Durch das Deklarieren der out -Variable, wenn sie in einem Methodenaufruf verwendet wird, können
Sie diese nicht versehentlich verwenden, bevor sie zugewiesen wurde.
Die in C# 7.0 zum Zulassen von out -Variablendeklarationen hinzugefügte Syntax wurde erweitert, sodass sie
Feldinitialisierer, Eigenschafteninitialisierer, Konstruktorinitialisierer und Abfrageklauseln umfasst. Sie ermöglicht
Code wie im folgenden Beispiel:
public class B
{
public B(int i, out int j)
{
j = i;
}
}

public class D : B
{
public D(int i) : base(i, out var j)
{
Console.WriteLine($"The value of 'j' is {j}");
}
}

Nicht schließende benannte Argumente


Methodenaufrufe dürfen jetzt benannte Argumente verwenden, die positionellen Argumenten vorstehen, wenn
diese benannten Argumente in der richtigen Position verwendet werden. Weitere Informationen finden Sie
unter Benannte und optionale Argumente (C#-Programmierhandbuch).

Zugriffsmodifizierer private protected


Ein neuer zusammengesetzter Zugriffsmodifizierer: private protected zeigt an, dass auf ein Element durch eine
es enthaltende Klasse oder durch innerhalb der gleichen Assembly deklarierte abgeleitete Klassen zugegriffen
werden darf. Während protected internal den Zugriff durch abgeleitete Klassen oder Klassen, die sich in der
gleichen Assembly befinden, erlaubt, schränkt private protected den Zugriff auf innerhalb der gleichen
Assembly deklarierte abgeleitete Typen ein.
Weitere Informationen finden Sie unter Zugriffsmodifizierer (C#-Referenz) in der Sprachreferenz.

Verbesserte Überladungskandidaten
In jedem Release werden die Überladungsauflösungsregeln für Situationen aktualisiert, in denen mehrdeutige
Methodenaufrufe eine „naheliegende“ Auswahlmöglichkeit haben. In diesem Release werden drei neue Regeln
hinzufügt, damit der Compiler die naheliegende Auswahlmöglichkeit nutzt:
1. Wenn eine Methodengruppe sowohl Instanz- als auch statische Member enthält, verwirft der Compiler die
Instanzmember, wenn die Methode ohne Instanzempfänger oder -kontext aufgerufen wurde. Der Compiler
verwirft die statischen Member, wenn die Methode mit einem Instanzempfänger aufgerufen wurde. Wenn
kein Empfänger vorhanden ist, bezieht der Compiler nur statische Member in einen statischen Kontext ein –
andernfalls sowohl statische als auch Instanzmember. Wenn unklar ist, ob der Empfänger eine Instanz oder
ein Typ ist, bezieht der Compiler beides ein. Ein statischer Kontext, wo ein impliziter this -Instanzempfänger
nicht verwendet werden kann, enthält den Text von Membern, wo kein this definiert ist, z.B. statische
Member, sowie Orte, an denen this nicht verwendet werden kann, z.B. Feld- und Konstruktorinitialisierer.
2. Wenn eine Methodengruppe einige generische Methoden enthält, deren Typargumente ihre
Einschränkungen nicht erfüllen, werden diese Member aus dem Satz von Kandidaten entfernt.
3. Für eine Methodengruppenkonvertierung werden Kandidatenmethoden, deren Rückgabetyp nicht mit dem
des Delegaten übereinstimmt, aus dem Satz entfernt.
Sie werden diese Änderung bemerken, da Sie weniger Compilerfehler für mehrdeutige Methodenüberladungen
finden werden, wenn Sie sicher sind, welche Methode besser ist.

Ermöglichen von effizienterem sicherem Code


Sie sollten C#-Code sicher schreiben können, der so leistungsstark ist wie unsicherer Code. Sicherer Code
vermeidet Fehlerklassen, z.B. Pufferüberläufe, verirrte Zeiger und andere Fehler beim Arbeitsspeicherzugriff.
Diese neuen Features erweitern die Funktionen des überprüfbaren sicheren Codes. Schreiben Sie mithilfe
sicherer Konstrukte mehr Code. Diese Features erleichtern dies.
Die folgenden neuen Features unterstützen das Design der besseren Leistung für sicheren Code:
Sie können ohne Anheften auf feste Felder zugreifen.
Sie können ref erneut lokale Variablen zuweisen.
Sie können Initialisierer auf stackalloc -Arrays verwenden.
Sie können fixed -Anweisungen mit jedem Typ verwenden, der ein Muster unterstützt.
Sie können zusätzliche generische Einschränkungen verwenden.
Den in -Modifizierer für Parameter zur Angabe, dass ein Argument durch Verweis übergeben, von der
aufgerufenen Methode aber nicht verändert wird. Das Hinzufügen des Modifizierers in zu einem Argument
ist eine quellkompatible Änderung.
Der ref readonly -Modifizierer für die Methodenrückgabe, um anzugeben, dass eine Methode ihren Wert als
Verweis zurückgibt und keinen Schreibzugriff auf dieses Objekt zulässt. Das Hinzufügen des Modifizierers
ref readonly ist eine quellkompatible Änderung, wenn die Rückgabe einem Wert zugewiesen wird. Das
Hinzufügen des Modifizierers readonly zu einer vorhandenen ref -Rückgabeanweisung ist eine
inkompatible Änderung. Es verlangt von Aufrufern das Aktualisieren der lokalen ref -Variablen, um den
readonly -Modifizierer einzuschließen.
Die readonly struct -Deklaration, um anzugeben, dass eine Struktur unveränderlich ist und ihren
Membermethoden als in -Parameter übergeben werden sollte. Das Hinzufügen des Modifizierers readonly
zu einer vorhandenen Strukturdeklaration ist eine binärkompatible Änderung.
Die ref struct -Deklaration, um anzugeben, dass ein Strukturtyp direkt auf verwalteten Arbeitsspeicher
zugreift und immer per Stapel zugeordnet werden muss. Das Hinzufügen des Modifizierers ref zu einer
vorhandenen struct -Deklaration ist eine inkompatible Änderung. Ein ref struct kann kein Mitglied einer
Klasse sein oder an anderen Stellen verwendet werden, wo es auf dem Heap zugewiesen werden könnte.
Weitere Informationen zu all diesen Änderungen finden Sie unter Schreiben von sicherem und effizientem Code.
Lokale ref-Variablen und Rückgabetypen
Diese Funktion ermöglicht Algorithmen, die Verweise auf Variablen verwenden und zurückgeben, die an
anderer Stelle definiert sind. Ein Beispiel ist das Arbeiten mit großen Matrizen und die Suche nach einem
einzigen Ort mit bestimmten Eigenschaften. Die folgende Methode gibt einen Ver weis auf diesen Speicher in
der Matrix zurück:

public static ref int Find(int[,] matrix, Func<int, bool> predicate)


{
for (int i = 0; i < matrix.GetLength(0); i++)
for (int j = 0; j < matrix.GetLength(1); j++)
if (predicate(matrix[i, j]))
return ref matrix[i, j];
throw new InvalidOperationException("Not found");
}

Sie können den Rückgabewert als ref deklarieren und diesen Wert wie im folgenden Code gezeigt in der
Matrix ändern:

ref var item = ref MatrixSearch.Find(matrix, (val) => val == 42);


Console.WriteLine(item);
item = 24;
Console.WriteLine(matrix[4, 2]);
Die C#-Sprache verfügt über mehrere Regeln, die Sie vor einer falschen Verwendung der lokalen ref -Variablen
und Rückgabetypen schützen:
Sie müssen der Methodensignatur und allen return -Anweisungen einer Methode das ref -Schlüsselwort
hinzufügen.
Dadurch wird deutlich, dass Rückgaben in der gesamten Methode als Verweis erfolgen.
Eine ref return -Rückgabe kann einer Wert- oder einer ref -Variablen zugewiesen werden.
Der Aufrufer steuert, ob der Rückgabewert kopiert werden soll. Durch Auslassen des ref -
Modifizierers beim Zuweisen des Rückgabewerts gibt der Aufrufer an, dass der Wert kopiert werden
und nicht etwa ein Verweis auf den Speicher erfolgen soll.
Sie können einer lokalen ref -Variablen keinen Rückgabewert einer Standardmethode zuweisen.
Dadurch werden Aussagen wie ref int i = sequence.Count(); nicht zugelassen.
Sie können ref nicht an eine Variable zurückgeben, deren Lebensdauer nicht über die Ausführung der
Methode hinausgeht.
Das bedeutet, dass Sie keinen Verweis auf eine lokale Variable oder eine Variable mit einem ähnlichen
Bereich zurückgeben können.
Lokale ref -Variablen und Rückgabewerte können nicht in Verbindung mit asynchronen Methoden
verwendet werden.
Der Compiler kann nicht feststellen, ob die Variable, auf die verwiesen wird, bei der Rückgabe der
asynchronen Methode auf ihren endgültigen Wert festgelegt ist.
Das Hinzufügen von lokalen ref-Variablen und ref-Rückgaben ermöglicht effizientere Algorithmen, da Werte
nicht kopiert oder dereferenzierende Vorgänge nicht mehrmals ausgeführt werden.
Das Hinzufügen von ref zum Rückgabewert stellt eine quellkompatible Änderung dar. Vorhandener Code lässt
sich kompilieren, der ref-Rückgabewert wird aber bei der Zuweisung kopiert. Aufrufer müssen den Speicher für
den Rückgabewert in eine lokale ref -Variable aktualisieren, um die Rückgabe als Verweis zu speichern.
Lokale ref -Variablen werden jetzt möglicherweise neu zugewiesen, um nach der Initialisierung auf
verschiedene Instanzen zu verweisen. Der folgende Code wird jetzt kompiliert:

ref VeryLargeStruct refLocal = ref veryLargeStruct; // initialization


refLocal = ref anotherVeryLargeStruct; // reassigned, refLocal refers to different storage.

Weitere Informationen finden Sie im Artikel zu ref -Rückgaben und lokalen ref -Variablen und im Artikel zu
foreach .

Weitere Informationen finden Sie im Artikel Schlüsselwort „ref“.


Bedingte ref Ausdrücke
Schließlich kann ein bedingter Ausdruck als Ergebnis einen Verweis anstatt eines Werts erzeugen.
Beispielsweise würden Sie folgendes schreiben, um einen Verweis auf das erste Element in einem von zwei
Arrays abzurufen:

ref var r = ref (arr != null ? ref arr[0] : ref otherArr[0]);

Die Variable r ist ein Verweis auf den ersten Wert in arr oder otherArr .
Weitere Informationen finden Sie unter conditional-Operator (?:) in der Sprachreferenz.
Modifizierer für in -Parameter
Das in -Schlüsselwort ergänzt die vorhandenen Schlüsselwörter ref und out zum Übergeben von Argumenten
als Verweis. Durch das in -Schlüsselwort wird festgelegt, dass das Argument als Verweis übergeben wird, die
aufgerufene Methode aber nicht den Wert ändert.
Sie können Überladungen, die nach Wert oder nach schreibgeschütztem Verweis übergeben werden, wie im
folgenden Code gezeigt deklarieren:

static void M(S arg);


static void M(in S arg);

Die Überladung, die nach Wert übergeben wird (die erste im obigen Beispiel) ist besser als die Version, die nach
schreibgeschütztem Verweis übergeben wird. Um die Version mit dem readonly-Verweisargument aufzurufen,
müssen Sie auch den in -Modifizierer beim Aufrufen der Methode einbeziehen.
Weitere Informationen finden Sie im Artikel zum in -Parametermodifizierer.
Weitere Typen unterstützen die fixed -Anweisung
Die -Anweisung unterstützte eine begrenzte Anzahl von Typen. Ab C# 7.3 kann jeder Typ, der eine
fixed
GetPinnableReference() -Methode enthält, die eine ref T oder ref readonly T zurückgibt, fixed sein. Dieses
Feature hinzuzufügen, bedeutet, dass fixed mit System.Span<T> und verwandten Typen genutzt werden kann.
Weitere Informationen finden Sie im Artikel zur fixed -Anweisung in der Sprachreferenz.
Indizieren von fixed -Feldern erfordert kein Anheften
Betrachten Sie diese Struktur:

unsafe struct S
{
public fixed int myFixedField[10];
}

In früheren Versionen von C# mussten Sie eine Variable für den Zugriff auf eine der ganzen Zahlen anheften, die
Teil von myFixedField sind. Der folgende Code wird kompiliert, ohne die Variable p in einer separaten fixed -
Anweisung anzuheften:

class C
{
static S s = new S();

unsafe public void M()


{
int p = s.myFixedField[5];
}
}

Die Variable p greift auf ein Element in myFixedField zu. Sie müssen keine separate int* -Variable
deklarieren. Sie benötigen weiterhin einen unsafe -Kontext. In früheren Versionen von C# müssen Sie einen
zweiten festen Zeiger deklarieren:
class C
{
static S s = new S();

unsafe public void M()


{
fixed (int* ptr = s.myFixedField)
{
int p = ptr[5];
}
}
}

Weitere Informationen finden Sie im Artikel zur fixed -Anweisung.


stackalloc -Arrays unterstützen Initialisierer
Sie konnten schon die Werte für Elemente in einem Array angeben, wenn Sie es initialisierten:

var arr = new int[3] {1, 2, 3};


var arr2 = new int[] {1, 2, 3};

Nun kann die gleiche Syntax auf Arrays angewendet werden, die mit stackalloc deklariert werden:

int* pArr = stackalloc int[3] {1, 2, 3};


int* pArr2 = stackalloc int[] {1, 2, 3};
Span<int> arr = stackalloc [] {1, 2, 3};

Weitere Informationen finden Sie im Artikel zum stackalloc -Operator.


Verbesserte generische Einschränkungen
Sie können jetzt Typ System.Enum oder System.Delegate als Basisklasseneinschränkungen für einen
Typparameter angeben.
Sie können auch die neue unmanaged -Einschränkung nutzen, um anzugeben, dass der Typparameter ein nicht
verwalteter Non-Nullable-Typ sein muss.
Weitere Informationen finden Sie in den Artikeln zu generischen where -Einschränkungen und Einschränkungen
für Typparameter.
Das Hinzufügen dieser Einschränkungen zu vorhandenen Typen ist eine inkompatible Änderung. Geschlossene
generische Typen erfüllen diese neuen Einschränkungen möglicherweise nicht mehr.
Generalisierte asynchrone Rückgabetypen
Das Zurückgeben eines Task -Objekts von asynchronen Methoden kann Leistungsengpässe in bestimmten
Pfaden verursachen. Task ist ein Verweistyp, seine Verwendung bedeutet also das Zuordnen eines Objekts. In
Fällen, in denen eine mit dem async -Modifizierer deklarierte Methode ein zwischengespeichertes Ergebnis
zurückgibt oder synchron abschließt, können die zusätzlichen Zuordnungen viel Zeit bei der Ausführung
kritischer Codeabschnitte kosten. Es kann kostspielig werden, wenn diese Zuordnungen in engen Schleifen
auftreten.
Durch die neue Sprachfunktion sind die Rückgabetypen asynchroner Methoden nicht auf Task , Task<T> und
void beschränkt. Der zurückgegebene Typ muss noch immer das asynchrone Muster erfüllen, d.h. dass eine
GetAwaiter -Methode verfügbar sein muss. Als konkretes Beispiel wurde der ValueTask -Typ zu .NET
hinzugefügt, um diese neue Sprachfunktion zu nutzen:
public async ValueTask<int> Func()
{
await Task.Delay(100);
return 5;
}

NOTE
Sie müssen das NuGet-Paket System.Threading.Tasks.Extensions hinzufügen, um den Typ ValueTask<TResult>
verwenden zu können.

Diese Verbesserung ist besonders für Bibliotheksautoren hilfreich, um die Zuordnung von Task in
leistungskritischem Code zu verhindern.

Neue Compileroptionen
Neue Compileroptionen unterstützen neue Build- und DevOpsszenarien für C#-Programme.
Generierung der Referenzassembly
Es gibt zwei neue Compileroptionen, die reine Verweisassemblys generieren: ProduceReferenceAssembly
und ProduceOnlyReferenceAssembly . In den verlinkten Artikeln werden diese Optionen und
Referenzassemblys ausführlich beschrieben.
Öffentliche oder Open Source -Signierung
Die Compileroption PublicSign weist den Compiler an, die Assembly mit einem öffentlichen Schlüssel zu
signieren. Die Assembly wird als signiert markiert, aber die Signatur wird dem öffentlichen Schlüssel
entnommen. Mit dieser Option können Sie signierte Assemblys aus Open Source-Projekten mit einem
öffentlichen Schlüssel erstellen.
Weitere Informationen finden Sie im Artikel zur PublicSign -Compileroption.
pathmap
Die Compileroption PathMap weist den Compiler an, Quellpfade aus der Buildumgebung mit zugeordneten
Quellpfaden zu ersetzen. Die Option PathMap CallerFilePathAttribute steuer t den Quellpfad, der vom
Compiler in PDB-Dateien oder für geschrieben wird.
Weitere Informationen finden Sie im Artikel zur PathMap -Compileroption.
Informationen zu Breaking Changes im C#-
Compiler
04.11.2021 • 2 minutes to read

Das Roslyn-Team verwaltet eine Liste von Breaking Changes in den C#- und Visual Basic-Compilern.
Informationen zu diesen Änderungen finden Sie unter diesen Links im GitHub-Repository:
Breaking Changes in Roslyn nach .NET 5
Breaking Changes in VS2019 Version 16.8, eingeführt in .NET 5.0 und C# 9.0
Breaking Changes in VS2019 Update 1 oder höher im Vergleich zu VS2019
Breaking Changes seit VS2017 (C# 7)
Breaking Changes in Roslyn 3.0 (VS2019) im Vergleich zu Roslyn 2.* (VS2017)
Breaking Changes in Roslyn 2.0 (VS2017) im Vergleich zu Roslyn 1.* (VS2015) und zum nativen C#-Compiler
(VS2013 oder früher).
Breaking Changes in Roslyn 1.0 (VS2015) im Vergleich zum nativen C#-Compiler (VS2013 oder früher).
Änderung der Unicode-Version in C# 6
Die Geschichte von C#
04.11.2021 • 12 minutes to read

Dieser Artikel erläutert den Verlauf jeder Hauptversion der Sprache C#. Das C#-Team entwickelt und ergänzt
immer neue und innovative Features. Informationen zu den Status von Programmiersprachenfunktionen, so
auch Funktionen, die für zukünftige Versionen geplant sind, finden Sie im dotnet/roslyn-Repository auf GitHub.

IMPORTANT
Für einige der Features nutzt die Sprache C# Typen und Methoden in einer Struktur, die in der C#-Spezifikation als
Standardbibliothek bezeichnet wird. Die .NET-Plattform stellt diese Typen und Methoden in verschiedenen Paketen bereit.
Ein Beispiel ist die Ausnahmeverarbeitung. Alle throw -Anweisungen oder -Ausdrücke werden überprüft, um
sicherzustellen, dass das ausgelöste Objekt von Exception abgeleitet ist. Auf ähnliche Weise wird jedes catch überprüft,
um sicherzustellen, dass der abgefangen Typ von Exception abgeleitet ist. Jede Version kann neue Anforderungen
hinzufügen. Um die neuesten Sprachfunktionen in älteren Umgebungen verwenden zu können, müssen Sie vielleicht
bestimmte Bibliotheken installieren. Diese Abhängigkeiten werden auf der jeweiligen Seite für eine spezifische Version
dokumentiert. Sie können mehr über die Beziehungen zwischen Sprache und Bibliothek erfahren, um
Hintergrundinformationen zu dieser Abhängigkeit zu erhalten.

C# Version 1.0
Die mit Visual Studio .NET 2002 veröffentlichte C#-Version 1.0 ähnelte Java sehr stark. Als Teil der
vorgegebenen Entwurfsziele für ECMA sollte sie eine „einfache, moderne, objektorientierte Universalsprache
sein“. Zu diesem Zeitpunkt bedeuteten die Ähnlichkeiten mit Java, dass die frühen Entwurfsziele erreicht wurden.
Wenn Sie sich C# 1.0 jedoch heute ansehen, wird Ihnen schwindlig. Es fehlten die integrierten Async-Funktionen
und einige der cleveren Funktionen bezüglich Generics, die heute als selbstverständlich betrachtet werden. In
der Tat fehlten Generics vollständig. Und was ist mit LINQ? War noch nicht verfügbar. Bis zum Erscheinen dieser
Erweiterungen dauerte es noch einige Jahre.
Im Vergleich zu heute sieht C# Version 1.0 jedoch ziemlich minimalistisch aus. Man musste selbst ausführlichen
Code schreiben. Irgendwo musste man jedoch anfangen. C# Version 1.0 war eine brauchbare Alternative zu Java
auf der Windows-Plattform.
Die wichtigsten Features von C# 1.0 umfassten:
Klassen
Strukturen
Schnittstellen
Ereignisse
Eigenschaften
Delegaten
Operatoren und Ausdrücke
Anweisungen
Attribute

C# Version 1.2
C# Version 1.2 wird in Visual Studio. NET 2003 bereitgestellt. Sie enthielt einige kleine Verbesserungen der
Sprache. Die wichtigste Änderung betrifft, ab dieser Version, den in einer foreach -Schleife namens Dispose
generierten Code in IEnumerator, wenn IEnumeratorIDisposable implementiert hat.

C# Version 2.0
Nun wird es langsam interessant. Werfen wir einen Blick auf einige wichtige Features von C# 2.0, die im Jahr
2005 zusammen mit Visual Studio 2005 veröffentlicht wurden:
Generics
Partial types (Partielle Typen)
Anonyme Methoden
Auf NULL festlegbare Werttypen
Iteratoren
Kovarianz und Kontravarianz
Andere Features von C# 2.0 fügten vorhandenen Features Funktionen hinzu:
Separate Getter- und Setter-Zugriffsfunktionen
Konvertierung von Methodengruppen (Delegate)
Statische Klassen
Delegatrückschlüsse
C# mag als allgemein gehaltene, objektorientierte (OO) Sprache angefangen haben. Das änderte sich mit C#
Version 2.0 jedoch schnell. Sobald man diese im Griff hatte, widmete man sich einigen ernsten Problemfeldern
der Entwickler. Und zwar in bedeutungsvoller Art und Weise.
Mit Generics können Typen und Methoden für einen beliebigen Typ ausgeführt werden, und sie behalten
dennoch ihre Typsicherheit bei. Wenn Sie zum Beispiel List<T> haben, können Sie List<string> oder
List<int> verwenden und typsichere Vorgänge für diese Zeichenfolgen oder Ganzzahlen ausführen, während
Sie sie durchlaufen. Die Verwendung von Generics ist besser als das Erstellen eines ListInt -Typs, der für jeden
Vorgang von ArrayList abgeleitet oder aus Object umgewandelt wird.
C# Version 2.0 brachte Iteratoren mit sich. Kurz gesagt können Sie mit Iteratoren alle Elemente in einem List
(oder anderen Enumerable-Typen) mit einer foreach -Schleife untersuchen. Als erstklassiger Bestandteil der
Sprache verbesserten Iteratoren die Lesbarkeit der Sprache und die Möglichkeiten zum Erörtern des Codes
erheblich.
Trotzdem hinkte C# weiter ein wenig hinter Java her. Von Java gab es bereits Versionen mit Generics und
Iteratoren. Das sollte sich jedoch bald ändern, da sich die Sprachen weiter voneinander weg entwickelten.

C# Version 3.0
C# Version 3.0 wurde Ende 2007 zusammen mit Visual Studio 2008 veröffentlicht. Der volle Umfang an
Sprachfeatures kam tatsächlich jedoch erst mit .NET Framework Version 3.5. Diese Version markierte eine
wesentliche Veränderung in der Entwicklung von C#. Sie etablierte C# als eine wirklich beachtliche
Programmiersprache. Werfen wir einen Blick auf einige wichtige Features in dieser Version:
Automatisch implementierte Eigenschaften
Anonyme Typen
Query expressions (Abfrageausdrücke)
Lambda-Ausdrücke
Ausdrucksbaumstrukturen
Erweiterungsmethoden
Implizit typisierte lokale Variablen
Partielle Methoden
Objekt- und Elementinitialisierer
Rückblickend scheinen viele dieser Features unvermeidlich und untrennbar. Sie alle passen strategisch
zusammen. Im Allgemeinen wird angenommen, dass das durchschlagende Feature dieser Version von C# der
Abfrageausdruck war, auch bekannt als Language Integrated Query (LINQ).
Bei genauerem Hinsehen zeigt sich, dass Ausdrucksbaumstrukturen, Lambdaausdrücke und anonyme Typen die
Grundlage waren, auf der LINQ konstruiert wurde. In jedem Fall stellte C# 3.0 ein revolutionäres Konzept dar. C#
3.0 hatte mit dem Schaffen der Grundlagen begonnen, um C# in eine hybride objektorientierte/funktionale
Sprache zu verwandeln.
Konkret konnte man nun deklarative Abfragen im Stil von SQL schreiben, unter anderem um Vorgänge an
Sammlungen durchzuführen. Anstatt eine for -Schleife zu schreiben, um den Durchschnitt einer Liste von
ganzen Zahlen zu berechnen, konnte man dies nun einfach als list.Average() ausführen. Die Kombination aus
Abfrageausdrücken und Erweiterungsmethoden ließ es jedoch so aussehen, als wäre die Liste der ganzen
Zahlen sehr viel intelligenter geworden.
Es dauerte eine Weile, aber nach und nach verstanden die Menschen das Konzept wirklich und integrierten es.
Und nun, Jahre später, ist der Code sehr viel präziser, einfacher und funktionaler.

C# Version 4.0
Die C#-Version 4.0, die mit Visual Studio 2010 veröffentlicht wurde, konnte nicht an den Erfolg der Version 3.0
anknüpfen. Mit Version 3.0 bewegte sich C# deutlich aus dem Schatten von Java heraus und gewann an
Bedeutung. Die Sprache wurde schnell elegant.
Die nächste Version führte einige interessante neue Features ein:
Dynamische Bindung
Benannte/optionale Argumente
Generische Kovarianz und Kontravarianz
Eingebettete Interop-Typen
Durch eingebettete Interop-Typen wird die Bereitstellung von COM-Interopassemblys für Ihre Anwendung
vereinfacht. Generische Kovarianz und Kontravarianz bieten Ihnen mehr Möglichkeiten zum Verwenden von
Generics. Sie sind allerdings recht theoretisch und werden vermutlich von Framework- und Bibliotheksautoren
am meisten geschätzt. Mit benannten und optionalen Parametern können Sie Methodenüberladungen
eliminieren. Außerdem bieten sie Bequemlichkeit. Keines dieser Features war jedoch bahnbrechend.
Das wichtigste Feature war die Einführung des dynamic -Schlüsselworts. Das in C# Version 4.0 eingeführte
dynamic -Schlüsselwort gibt die Möglichkeit zum Überschreiben des Compilers bei Eingabe zur Kompilierzeit.
Durch die Verwendung des dynamischen Schlüsselworts können Sie Konstrukte schreiben, die dynamisch
typisierten Sprachen wie JavaScript ähneln. Sie können ein dynamic x = "a string" erstellen und dann sechs
hinzufügen, und überlassen Sie es der Runtime herauszufinden, was als Nächstes geschehen soll.
Die dynamische Bindung kann zu Fehlern führen, bietet aber gleichzeitig eine hohe Leistungsfähigkeit innerhalb
der Sprache.

C# Version 5.0
In der C#-Version 5.0, die mit Visual Studio 2012 veröffentlicht wurde, lag der Fokus auf ganz bestimmten
Sprachaspekten. Nahezu die gesamte Arbeit für diese Version war einem weiteren bahnbrechenden
Sprachkonzept gewidmet: den Modellen async und await für die asynchrone Programmierung. Hier ist die
Liste der wichtigsten Features:
Asynchrone Member
Attribute „CallerInfo“
Siehe auch
Code Project: Caller Info Attributes in C# 5.0 (Aufruferinformationsattribute in C# 5.0)
Mit dem Attribut „CallerInfo“ können Sie leicht Informationen über den Kontext erhalten, in dem Sie ausführen,
ohne auf Dutzende Reflektionscodebausteine zurückzugreifen. Es gibt viele Verwendungsmöglichkeiten für
Diagnose- und Protokollierungsaufgaben.
Die eigentlichen Stars dieser Version sind aber async und await . Als diese Features im Jahr 2012
herauskamen, veränderte C# noch einmal alles, indem Asynchronität als erstrangiger Bestandteil in die Sprache
eingearbeitet wurde. Wenn Sie schon einmal mit langlaufenden Vorgängen und der Implementierung von
Rückrufnetzen zu tun hatten, haben Sie dieses Sprachfeature vermutlich zu schätzen gelernt.

C# Version 6.0
Mit den Versionen 3.0 und 5.0 wurde C# um wichtige neue Features in einer objektorientierten Sprache
erweitert. In Version 6.0, die mit Visual Studio 2015 veröffentlicht wurde, lag der Fokus nicht auf einem
einzelnen Hauptfeature, sondern auf der Implementierung vieler kleiner Verbesserungen, die das
Programmieren mit C# noch effizienter machten. Hier sind einige davon:
Statische Importe
Ausnahmefilter
Initialisierer für automatische Eigenschaften
Ausdruckskörpermember
Nullpropagator
Zeichenfolgeninterpolation
nameof-Operator
Zu weiteren neuen Features gehören:
Indexinitialisierer
„Await“ in Catch- und Finally-Blöcken
Standardwerte für Getter-exklusive Eigenschaften
Jedes dieser Features ist auf seine eigene Weise interessant. Wenn Sie die Features jedoch zusammen
betrachten, erkennen Sie ein interessantes Muster. In dieser Version von C# wurden Codebausteine eliminiert,
um den Code knapper und besser lesbar zu machen. Für Anhänger von klarem, einfachem Code war diese
Sprachversion ein großer Gewinn.
Neben dieser Version wurde noch etwas anderes gemacht, auch wenn es sich nicht um ein herkömmliches
Sprachfeature handelt. Der Compiler Roslyn wurde als Dienst veröffentlicht. Der C#-Compiler ist nun in C#
geschrieben, und Sie können den Compiler als Teil Ihrer Programmierarbeiten verwenden.

C# Version 7.0
Die C#-Version 7.0 wurde mit Visual Studio 2017 veröffentlicht. Diese Version bietet einige evolutionäre und
tolle Aspekte im Stil von C# 6.0, aber ohne den Compiler als Dienst. Hier sind einige der neuen Features:
Out-Variablen
Tupel und Dekonstruktionen
Mustervergleich
Local functions (Lokale Funktionen)
Erweiterte Ausdruckskörpermember
Lokale ref-Variablen und Rückgaben
Weitere Features umfassten:
Verwerfen
Binäre Literale und Zahlentrennzeichen
Throw expressions (Throw-Ausdrücke)
Alle diese Features bieten tolle neue Möglichkeiten für Entwickler und erlauben es, einen noch saubereren Code
als je zuvor zu schreiben. Ein Highlight ist das Verdichten der Variablendeklaration zur Verwendung mit dem
out -Schlüsselwort und indem die Rückgabe mehrerer Werte über Tupel erlaubt wird.

C# kommt auf einem immer breiteren Feld zum Einsatz. .NET Core zielt nun auf alle Betriebssysteme ab und
konzentriert sich stark auf die Cloud und auf Portabilität. Diese neuen Funktionen nehmen zusätzlich zum
Entwickeln neuer Features sicherlich viel Arbeit und Zeit in Anspruch.

C#-Version 7.1
Die Punktreleases von C# wurden mit C# 7.1 eingeführt. Mit dieser Version wurde das Konfigurationselement
zur Auswahl der Sprachversion, drei neue Sprachfeatures und neues Compilerverhalten hinzugefügt.
Die neuen Sprachfeatures in diesem Release umfassen:
async Main -Methode
Der Einstiegspunkt für eine Anwendung kann über den Modifizierer async verfügen.
default Literale Ausdrücke
Sie können literale Standardausdrücke in Standardwertausdrücken verwenden, wenn der Zieltyp
abgeleitet werden kann.
Abgeleitete Tupelelementnamen
Die Namen von Tupelelementen können in den meisten Fällen von der Initialisierung eines Tupels
abgeleitet werden.
Musterabgleich für generische Typparameter
Sie können Musterabgleichsausdrücke für Variablen verwenden, deren Typ ein generischer
Typparameter ist.
Außerdem verfügt der Compiler über die zwei Optionen -refout und -refonly , mit denen die Generierung der
Referenzassembly gesteuert wird.

C#-Version 7.2
Mit C# 7.2 wurden einige kleine Sprachfeatures hinzugefügt:
Techniken zum Schreiben von sicherem, effizientem Code
Eine Kombination aus Verbesserungen der Syntax, die das Arbeiten mit Werttypen mithilfe von
Verweissemantik ermöglichen.
Nicht schließende benannte Argumente
Positionelle Argumente können auf benannte Argumente folgen.
Führende Unterstriche in numerischen Literalen
Numerische Literale dürfen jetzt führende Unterstriche vor aufgeführten Stellen aufweisen.
private protected -Zugriffsmodifizierer
Der private protected -Zugriffsmodifizierer ermöglicht den Zugriff für abgeleitete Klassen innerhalb
der gleichen Assembly.
Bedingte ref Ausdrücke
Das Ergebnis eines bedingten Ausdrucks ( ?: ) kann jetzt ein Verweis sein.

C#-Version 7.3
Es gibt zwei Hauptdesigns in der C# 7.3-Version. Ein Design bietet Features, die ermöglichen, dass sicherer Code
so leistungsfähig ist wie unsicherer Code. Das zweite Design bietet inkrementelle Verbesserungen vorhandener
Funktionen. Darüber hinaus wurden in diesem Release neue Compileroptionen hinzugefügt.
Die folgenden neuen Features unterstützen das Design der besseren Leistung für sicheren Code:
Sie können ohne Anpinnen auf fixierte Felder zugreifen.
Sie können lokale ref -Variablen neu zuweisen.
Sie können Initialisierer für stackalloc -Arrays verwenden.
Sie können fixed -Anweisungen mit jedem Typ verwenden, der ein Muster unterstützt.
Sie können generischere Einschränkungen verwenden.
Die folgenden Verbesserungen wurden an vorhandenen Features vorgenommen:
Sie können == und != mit Tupeltypen testen.
Sie können Ausdrucksvariablen an mehreren Standorten verwenden.
Sie können Attribute dem Unterstützungsfeld automatisch implementierter Eigenschaften anfügen.
Die Auflösung der Methode, wenn Argumente um in differieren, wurde verbessert.
Die Auflösung von Überladungen weist jetzt weniger mehrdeutige Fälle auf.
Die neuen Compileroptionen lauten:
-publicsign , um das Signieren von Assemblys durch Open Source Software (OSS) zu ermöglichen.
-pathmap , um eine Zuordnung für Quellverzeichnisse bereitzustellen.

C#-Version 8.0
C# 8.0 ist das erste Hauptrelease von C#, das speziell auf .NET Core ausgerichtet ist. Einige Features nutzen neue
CLR-Funktionen, während andere Bibliothekstypen nutzen, die nur in .NET Core hinzugefügt wurden. C# 8.0 fügt
der Sprache C# die folgenden Features und Verbesserungen hinzu:
Readonly-Member
Standardschnittstellenmethoden
Verbesserungen am Musterabgleich:
switch-Ausdrücke
Eigenschaftsmuster
Tupelmuster
Positionsmuster
using-Deklarationen
Statische lokale Funktionen
Verwerfbare Referenzstrukturen
Nullwerte zulassende Verweistypen
Asynchrone Streams
Indizes und Bereiche
NULL-Coalescing-Zuweisung
Nicht verwaltete konstruierte Typen
Stackalloc in geschachtelten Ausdrücken
Erweiterung von interpolierten ausführlichen Zeichenfolgen
Standardschnittstellenmember erfordern Verbesserungen in der CLR (Common Language Runtime). Diese
Features wurden in der CLR für .NET Core 3.0 hinzugefügt. Bereiche, Indizes und asynchrone Datenströme
erfordern neue Typen in den .NET Core 3.0-Bibliotheken. Nullable-Verweistypen, die im Compiler implementiert
sind, sind weitaus nützlicher, wenn Bibliotheken mit Anmerkungen versehen sind, um semantische
Informationen zum NULL-Status von Argumenten und Rückgabewerten anzugeben. Diese Anmerkungen
werden in den .NET Core-Bibliotheken hinzugefügt.

C#-Version 9.0
C# 9.0 wurde mit .NET 5 veröffentlicht. Es ist die Standardsprachversion für jede Assembly, die auf das .NET 5-
Release ausgelegt ist. Es enthält die folgenden neuen und verbesserten Features:
Mit Version 9.0 wird die Sprache C# um die folgenden Features und Verbesserungen erweitert:
Datensätze
init-only-Setter
Top-Level-Anweisungen
Verbesserungen am Musterabgleich:
Leistung und Interop
Integerwerte mit nativer Größe
Funktionszeiger
Unterdrücken der Ausgabe des Flags „localsinit“
Anpassen und Fertigstellen von Features
Zieltypisierte new -Ausdrücke
Anonyme static -Funktionen
Bedingter Ausdruck mit Zieltyp
Kovariante Rückgabetypen
Unterstützung für die Erweiterung GetEnumerator für foreach -Schleifen
Parameter zum Verwerfen von Lambdafunktion
Attribute in lokalen Funktionen
Unterstützung für Code-Generatoren
Modulinitialisierer
Neue Features für partielle Methoden
C# 9.0 setzt drei der Themen aus früheren Versionen fort: Entfernen von Aufwand, Trennen von Daten und
Algorithmen sowie Bereitstellen von mehr Mustern an mehr Stellen.
Anweisungen der obersten Ebene bedeuten, dass das Hauptprogramm einfacher zu lesen ist. Es besteht weniger
Aufwand: ein Namespace, die Program -Klasse und static void Main() sind nicht erforderlich.
Die Einführung von records ermöglicht eine kompakte Syntax für Verweistypen, die der Wertsemantik für
Gleichheit folgen. Mit diesen Typen definieren Sie Datencontainer, die typischerweise ein Mindestverhalten
festlegen. init-only-Setter ermöglichen nicht destruktive Mutationen with (Ausdrücke) in Datensätzen. C# 9.0
fügt auch kovariante Rückgabetypen hinzu, sodass abgeleitete Datensätze virtuelle Methoden überschreiben
und einen Typ zurückgeben können, der vom Rückgabetyp der Basismethode abgeleitet ist.
Die Möglichkeiten zum Musterabgleich wurden auf verschiedene Weisen erweitert. Numerische Typen
unterstützen jetzt Bereichsmuster. Muster können mit den Mustern and , or und not kombiniert werden.
Klammern können hinzugefügt werden, um komplexere Muster zu verdeutlichen.
Eine weitere Reihe von Features unterstützt High Performance Computing in C#:
Die Typen nint und nuint modellieren die Integertypen mit nativer Größe auf der Ziel-CPU.
Funktionszeiger stellen delegatähnliche Funktionen zur Verfügung und vermeiden gleichzeitig die
Zuteilungen, die zum Erstellen eines Delegatobjekts erforderlich sind.
Die Anweisung localsinit kann zum Speichern von Anweisungen weggelassen werden.

Eine weitere Reihe von Verbesserungen gilt für Szenarien, in denen Codegeneratoren Funktionalität hinzufügen:
Modulinitialisierer sind Methoden, die die Laufzeit beim Laden einer Assembly aufruft.
Partielle Methoden unterstützen neue zugängliche Modifizierer und Rückgabetypen des Typs „nicht-void“. In
diesen Fällen muss eine Implementierung bereitgestellt werden.
C# 9.0 wurden viele weitere kleine Features hinzugefügt, die die Produktivität von Entwicklern verbessern,
sowohl beim Schreiben als auch beim Lesen von Code:
new -Ausdrücke mit Zieltyp
Anonyme static -Funktionen
Bedingte Ausdrücke mit Zieltyp
Unterstützung für die Erweiterung GetEnumerator() in foreach -Schleifen
Lambdaausdrücke können das Verwerfen von Parametern deklarieren
Attribute können auf lokale Funktionen angewendet werden
Die C#-Version 9.0 ist eine Weiterentwicklung von C# als moderne, universell einsetzbare Programmiersprache.
Features unterstützen weiterhin moderne Workloads und Anwendungstypen.
Dieser Artikel wurde ursprünglich auf dem NDepend-Blog veröffentlicht und uns freundlicherweise von Erik
Dietrich und Patrick Smacchia zur Verfügung gestellt.
Die Beziehung zwischen Sprachfeatures und
Bibliothekstypen
04.11.2021 • 2 minutes to read

Die C#-Sprachdefinition erfordert eine Standardbibliothek, damit bestimmte Typen und bestimmte zugängliche
Members für diese Typen verfügbar sind. Der Compiler generiert Code, der die erforderlichen Typen und
Members für viele verschiedene Sprachfeatures verwendet. Beim Schreiben von Code für Umgebungen, für die
diese Typen und Members noch nicht bereitgestellt wurden, stehen bei Bedarf NuGet-Pakete zur Verfügung, die
Typen enthalten, die für neuere Versionen der Sprache erforderlich sind.
Diese Abhängigkeit von den Funktionen der Standardbibliothek ist seit der ersten Version Teil der Sprache „C#“.
In dieser Version sind folgende Beispiele enthalten:
Exception: wird für alle vom Compiler generierten Ausnahmen verwendet.
String: Der C#-Typ string stellt ein Synonym für String dar.
Int32: Synonym von int .
Diese erste Version war einfach, denn der Compiler und die Standardbibliothek waren beide enthalten, und von
beiden gab es nur eine Version.
Bei nachfolgenden Versionen von C# wurden den Abhängigkeiten gelegentlich neue Typen oder Members
hinzugefügt. Beispiele dafür sind INotifyCompletion, CallerFilePathAttribute und CallerMemberNameAttribute.
In C# 7.0 wird dies fortgesetzt, indem die Abhängigkeit von ValueTuple hinzugefügt wird, um das Sprachfeature
für Tupels hinzuzufügen.
Das Entwurfsteam für die Sprache arbeitet daran, die Oberfläche der Typen und Members zu verringern, die für
eine kompatible Standardbibliothek erforderlich sind. Das Ziel ist ein übersichtliches Design, bei dem neue
Bibliotheksfeatures nahtlos in die Sprache integriert werden können. In zukünftigen Versionen von C# werden
neue Features hinzugefügt, die neue Typen und Members in einer Standardbibliothek erfordern. Es ist wichtig,
ein Verständnis dafür zu entwickeln, wie Sie diese Abhängigkeiten in Ihrer Arbeit verwalten.

Verwalten Ihrer Abhängigkeiten


C#-Compilertools werden nun vom Releasezyklus der .NET-Bibliotheken auf den unterstützten Plattformen
entkoppelt. Unterschiedliche .NET-Bibliotheken weisen unterschiedliche Releasezyklen auf. So wird .NET
Framework unter Windows als Windows Update veröffentlicht, während .NET Core einem anderen Zeitplan folgt
und die Xamarin-Versionen der Bibliotheksupdates in den Xamarin-Tools für jede Zielplattform enthalten sind.
In den meisten Fällen werden Sie diese Änderungen nicht bemerken. Wenn Sie jedoch mit einer neueren
Version der Sprache arbeitet, die Features erfordert, die noch nicht in den .NET-Bibliotheken auf dieser Plattform
enthalten sind, können Sie auf die entsprechenden NuGet-Pakete verweisen, um diese Typen bereitzustellen. Da
die Plattformen, die Ihre App unterstützt, durch die Installation neuer Frameworks aktualisiert werden, können
Sie zusätzliche Verweise entfernen.
Diese Trennung bedeutet, dass Sie neue Sprachfeatures auch dann verwenden können, wenn Sie Computer
anzielen, die das entsprechende Framework nicht besitzen.
Versions- und Updateüberlegungen für C#-
Entwickler
04.11.2021 • 2 minutes to read

Kompatibilität ist ein sehr wichtiges Ziel, wenn der Sprache C# neue Features hinzugefügt werden. In nahezu
allen Fällen kann vorhandener Code problemlos mit einer neuen Compilerversion neu kompiliert werden.
Möglicherweise ist mehr Sorgfalt erforderlich, wenn Sie neue Sprachfeatures in einer Bibliothek übernehmen.
Sie erstellen möglicherweise eine neue Bibliothek mit Features, die sich in der neuesten Version befinden, und
müssen sicherstellen, dass Apps, die mit früheren Versionen des Compilers erstellt wurden, diese verwenden
können. Oder Sie nehmen ein Upgrade einer vorhandenen Bibliothek vor, und viele Ihrer Benutzer verfügen
möglicherweise noch nicht über Versionen mit dem Upgrade. Beim Treffen von Entscheidungen zur Übernahme
neuer Features müssen Sie zwei Varianten von Kompatibilität unterscheiden: quellkompatibel und
binärkompatibel.

Binärkompatible Änderungen
Änderungen an Ihrer Bibliothek sind binärkompatibel , wenn Ihre aktualisierte Bibliothek ohne Neuerstellung
der Anwendungen und Bibliotheken, die Sie nutzen, verwendet werden kann. Abhängige Assemblys müssen
nicht neu erstellt werden, und es sind keine Änderungen am Quellcode erforderlich.

Quellkompatible Änderungen
Änderungen an Ihrer Bibliothek sind quellkompatibel , wenn die Anwendungen und Bibliotheken, die Ihre
Bibliothek nutzen, keine Änderungen am Quellcode erfordern, die Quellen aber mit der neuen Version neu
kompiliert werden müssen, um ordnungsgemäß zu funktionieren.

Inkompatible Änderungen
Wenn eine Änderung weder quellkompatibel noch binärkompatibel ist, sind Änderungen am Quellcode in
Kombination mit einer erneuten Kompilierung in den abhängigen Bibliotheken und Anwendungen erforderlich.

Bewerten Ihrer Bibliothek


Diese Kompatibilitätskonzepte betreffen die öffentlichen und geschützten Deklarationen für Ihre Bibliothek,
nicht deren interne Implementierung. Die interne Übernahme beliebiger neuer Features ist immer
binärkompatibel .
Bei binärkompatiblen Änderungen steht neue Syntax zur Verfügung, die den gleichen kompilierten Code für
öffentliche Deklarationen erzeugt wie die ältere Syntax. Beispielsweise ist das Ändern einer Methode in ein
Ausdruckskörpermember eine binärkompatible Änderung:
Ursprünglicher Code:

public double CalculateSquare(double value)


{
return value * value;
}

Neuer Code:
public double CalculateSquare(double value) => value * value;

Mit quellkompatiblen Änderungen wird Syntax eingeführt, die den kompilierten Code für ein öffentliches
Member ändert, aber in einer Weise, die mit vorhandenen Aufrufsites kompatibel ist. Beispielsweise ist das
Ändern einer Methodensignatur von einem Wertparameter in einen in -Verweisparameter quellkompatibel,
aber nicht binärkompatibel:
Ursprünglicher Code:

public double CalculateSquare(double value) => value * value;

Neuer Code:

public double CalculateSquare(in double value) => value * value;

In den Neuigkeiten-Artikeln ist vermerkt, ob die Einführung eines Features, das sich auf öffentliche
Deklarationen auswirkt, quellkompatibel oder binärkompatibel ist.
Erstellen von Datensatztypen
04.11.2021 • 12 minutes to read

Mit C# 9 wurden Datensätze eingeführt. Dabei handelt es sich um einen neuen Verweistyp, den Sie anstelle von
Klassen oder Strukturen erstellen können. C# 10 führt Datensatzstrukturen ein, mit denen Sie Datensätze als
Werttypen definieren können. Datensätze unterscheiden sich insofern von Klassen, dass Datensatztypen
wertbasierte Gleichheit verwenden. Zwei Variablen eines Datensatztyps sind gleich, wenn die
Datensatztypdefinitionen identisch sind und wenn die Werte in beiden Datensätzen für alle Felder identisch sind.
Zwei Variablen eines Klassentyps sind identisch, wenn die Objekte, auf die verwiesen wird, denselben Klassentyp
aufweisen und die Variablen auf dasselbe Objekt verweisen. Die wertbasierte Gleichheit impliziert andere
Funktionen, die Sie wahrscheinlich in Datensatztypen benötigen. Der Compiler generiert viele dieser Member,
wenn Sie record anstelle von class deklarieren. Der Compiler generiert dieselben Methoden für
record struct -Typen.

In diesem Tutorial lernen Sie Folgendes:


Wann Sie class oder record deklarieren sollten
Deklarieren von Datensatztypen und Datensatztypen mit fester Breite
Ersetzen Ihrer Methoden für vom Compiler generierte Methoden in Datensätzen

Voraussetzungen
Sie müssen Ihren Computer für die Ausführung von .NET 6 oder höher einrichten, einschließlich des Compilers
für C# 10.0 oder höher. Der C# 9.0-Compiler steht ab der Vorschauversion von Visual Studio 2022 oder ab dem
.NET 6.0 SDK zur Verfügung.

Charakteristiken von Datensätzen


Sie definieren einen Datensatz, indem Sie einen Typ mit dem Schlüsselwort record deklarieren, anstatt mit dem
Schlüsselwort class oder struct . Optional können Sie ein record class -Element deklarieren, um zu
verdeutlichen, dass es sich um einen Verweistyp handelt. Ein Datensatz ist ein Verweistyp und unterliegt der
Semantik der wertbasierten Gleichheit. Sie können ein record struct -Element definieren, um einen Datensatz
zu erstellen, bei dem es sich um einen Werttyp handelt. Um Wertsemantik zu erzwingen, generiert der Compiler
mehrere Methoden für Ihren Datensatztyp (sowohl für den record class - als auch den record struct -Typ):
Eine Überschreibung von Object.Equals(Object)
Eine virtuelle Equals -Methode, deren Parameter der Datensatztyp ist
Eine Überschreibung von Object.GetHashCode()
Methoden für operator == und operator !=
Datensatztypen implementieren System.IEquatable<T>
Datensätze stellen auch eine Überschreibung von Object.ToString() zur Verfügung. Der Compiler synthetisiert
Methoden zum Anzeigen von Datensätzen mit Object.ToString(). Diese Member untersuchen Sie, während Sie
Code für dieses Tutorial schreiben. Datensätze unterstützen with -Ausdrücke, um nicht destruktive Änderungen
von Datensätzen zu ermöglichen.
Sie können auch Datensätze mit fester Breite mithilfe einer kürzeren Syntax deklarieren. Der Compiler
synthetisiert mehr Methoden für Sie, wenn Sie Datensätze mit fester Breite deklarieren:
Ein primärer Konstruktor, dessen Parameter mit den Parametern mit fester Breite der Datensatzdeklaration
übereinstimmen
Öffentliche Eigenschaften für jeden Parameter eines primären Konstruktors. Dabei handelt es sich um init-
only-Eigenschaften für record class - und readonly record struct -Typen. Für record struct -Typen sind es
read-write-Eigenschaften.
Eine Deconstruct -Methode zum Extrahieren von Eigenschaften aus dem Datensatz

Erstellen von Temperaturdaten


Daten und Statistiken gehören zu den Szenarios, in denen Sie Datensätze verwenden sollten. Für dieses Tutorial
erstellen Sie eine Anwendung, die Wärmesummen für verschiedene Verwendungszwecke berechnet.
Wärmesummen sind ein Maß für Temperaturwerte über einen gewissen Zeitraum von Tagen, Wochen oder
Monaten. Wärmesummen verfolgen den Energieverbrauch und prognostizieren diesen. Eine größere Anzahl von
Tagen mit hohen Temperaturen führt zu erhöhter Nutzung von Klimaanlagen, während eine größere Anzahl von
Tagen mit niedrigen Temperaturen zu erhöhter Nutzung von Heizkörpern führt. Wärmesummen helfen bei der
Verwaltung von Pflanzenbeständen und korrelieren mit dem Pflanzenwachstum im Wechsel der Jahreszeiten.
Wärmesummen werden außerdem zur Nachverfolgung von Tierwanderungen für Spezies verwendet, die sich
dem Klima entsprechend bewegen.
Die Formel basiert auf der Durchschnittstemperatur eines jeweiligen Tages und einer Baselinetemperatur. Zum
Berechnen von Wärmesummen über Zeit benötigen Sie die Höchst- und Mindesttemperaturen für jeden Tag
eines Zeitraums. Im Folgenden beginnen Sie mit der Erstellung einer neuen Anwendung. Erstellen Sie eine neue
Konsolenanwendung. Erstellen Sie einen neuen Datensatztyp in einer neuen Datei namens
„DailyTemperature.cs“:

public readonly record struct DailyTemperature(double HighTemp, double LowTemp);

Mit dem obigen Code wird ein Datensatz mit fester Breite definiert. Der DailyTemperature -Datensatz gehört
zum Typ readonly record struct , da er nicht vererben und unveränderlich sein sollte. Bei den Eigenschaften
HighTemp und LowTemp handelt es sich um init-only-Eigenschaften, d. h., sie können im Konstruktor oder
mithilfe eines Eigenschafteninitialisierers festgelegt werden. Wenn Lese-/Schreibzugriff auf positionale
Parameter bestehen soll, müssen Sie record struct anstelle von readonly record struct deklarieren. Der Typ
DailyTemperature verfügt ebenfalls über einen primären Konstruktor , der über zwei Parameter verfügt, die den
zwei Eigenschaften entsprechen. Sie verwenden den primären Konstruktor zum Initialisieren eines
DailyTemperature -Datensatzes:
private static DailyTemperature[] data = new DailyTemperature[]
{
new DailyTemperature(HighTemp: 57, LowTemp: 30),
new DailyTemperature(60, 35),
new DailyTemperature(63, 33),
new DailyTemperature(68, 29),
new DailyTemperature(72, 47),
new DailyTemperature(75, 55),
new DailyTemperature(77, 55),
new DailyTemperature(72, 58),
new DailyTemperature(70, 47),
new DailyTemperature(77, 59),
new DailyTemperature(85, 65),
new DailyTemperature(87, 65),
new DailyTemperature(85, 72),
new DailyTemperature(83, 68),
new DailyTemperature(77, 65),
new DailyTemperature(72, 58),
new DailyTemperature(77, 55),
new DailyTemperature(76, 53),
new DailyTemperature(80, 60),
new DailyTemperature(85, 66)
};

Sie können Ihre eigenen Eigenschaften oder Methoden zu Datensätzen hinzufügen, dazu zählen auch Datensätze
mit fester Breite. Sie müssen die Durchschnittstemperatur für jeden Tag berechnen. Diese Eigenschaft können
Sie zum Datensatz DailyTemperature hinzufügen:

public readonly record struct DailyTemperature(double HighTemp, double LowTemp)


{
public double Mean => (HighTemp + LowTemp) / 2.0;
}

Als Nächstes stellen Sie sicher, dass Sie diese Daten verwenden können. Fügen Sie der Main -Methode den
folgenden Code hinzu:

foreach (var item in data)


Console.WriteLine(item);

Führen Sie Ihre Anwendung aus. Daraufhin sollte Ihnen eine Ausgabe angezeigt werden, die der folgenden
Anzeige ähnelt (einige Zeilen wurden aus Platzgründen entfernt):

DailyTemperature { HighTemp = 57, LowTemp = 30, Mean = 43.5 }


DailyTemperature { HighTemp = 60, LowTemp = 35, Mean = 47.5 }

DailyTemperature { HighTemp = 80, LowTemp = 60, Mean = 70 }


DailyTemperature { HighTemp = 85, LowTemp = 66, Mean = 75.5 }

Der obige Code zeigt die Ausgabe der Überschreibung von ToString an, die vom Compiler synthetisiert wurde.
Wenn Sie einen anderen Text bevorzugen, können Sie Ihre eigene Version von ToString schreiben, die den
Compiler daran hindert, eine Version für Sie zu synthetisieren.

Berechnen von Wärmesummen


Zum Berechnen der Wärmesummen verwenden Sie die Differenz zwischen einer Baselinetemperatur und einer
Durchschnittstemperatur für einen Tag. Zum Berechnen der Wärmesumme über Zeit entfernen Sie alle Tage, an
denen die Durchschnittstemperatur unterhalb der Baselinetemperatur liegt. Zum Berechnen der Kältesumme
über Zeit entfernen Sie alle Tage, an denen die Durchschnittstemperatur über der Baselinetemperatur liegt.
Beispielsweise verwenden die USA 65 F als Baselinetemperatur für Wärme- und Kältesummen. Bei dieser
Temperatur ist weder Heizung noch Kühlung erforderlich. Ein Tag mit einer Durchschnittstemperatur von 70 F
weist eine Kältesumme von „5“und eine Wärmesumme von „0“ auf. Wenn die Durchschnittstemperatur also
55 F entspricht, weist der Tag dementsprechend eine Wärmesumme von „10“ und eine Kältesumme von „0“ auf.
Sie können diese Formeln in Form einer kleinen Hierarchie aus Datensatztypen ausdrücken: ein abstrakter
Temperatursummentyp und zwei konkrete Typen für Wärmesummen und Kältesummen. Bei diesen Typen kann
es sich außerdem um Datensätze mit fester Breite handeln. Sie verwenden eine Baselinetemperatur und eine
Reihe täglicher Temperaturdatensätze als Argumente für den primären Konstruktor:

public abstract record DegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords);

public sealed record HeatingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)


: DegreeDays(BaseTemperature, TempRecords)
{
public double DegreeDays => TempRecords.Where(s => s.Mean < BaseTemperature).Sum(s => BaseTemperature -
s.Mean);
}

public sealed record CoolingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)


: DegreeDays(BaseTemperature, TempRecords)
{
public double DegreeDays => TempRecords.Where(s => s.Mean > BaseTemperature).Sum(s => s.Mean -
BaseTemperature);
}

Der abstrakte Datensatz DegreeDays ist die gemeinsame Basisklasse für die beiden Datensätze
HeatingDegreeDays und CoolingDegreeDays . Die primären Konstruktordeklarationen der abgeleiteten Datensätze
zeigen, wie die Initialisierung des Basisdatensatzes verwaltet wird. Ihr abgeleiteter Datensatz deklariert
Parameter für alle Parameter im primären Konstruktor des Basisdatensatzes. Der Basisdatensatz deklariert und
initialisiert diese Eigenschaften. Der abgeleitete Datensatz blendet diese nicht aus. Stattdessen erstellt und
initialisiert er nur Eigenschaften für Parameter, die nicht in seinem Basisdatensatz deklariert sind. In diesem
Beispiel fügen die abgeleiteten Datensätze keine neuen primären Konstruktorparameter hinzu. Testen Sie Ihren
Code, indem Sie den folgenden Code zu Ihrer Main -Methode hinzufügen:

var heatingDegreeDays = new HeatingDegreeDays(65, data);


Console.WriteLine(heatingDegreeDays);

var coolingDegreeDays = new CoolingDegreeDays(65, data);


Console.WriteLine(coolingDegreeDays);

Daraufhin sollte eine Ausgabe ähnlich der folgenden angezeigt werden:

HeatingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 85 }


CoolingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 71.5 }

Definieren von durch den Compiler synthetisierten Methoden


Ihr Code berechnet die korrekte Anzahl von Wärme- und Kältesummen über den Zeitraum. Jedoch
veranschaulicht dieses Beispiel, wieso Sie einige der synthetisierten Methoden durch Datensätze ersetzen
sollten. Mit Ausnahme der Clone-Methode können Sie Ihre eigene Version von beliebigen durch den Compiler
synthetisierten Methoden in einem Datensatztyp deklarieren. Die Clone-Methode verfügt über einen vom
Compiler generierten Namen, und Sie können keine andere Implementierung bereitstellen. Die synthetisierten
Methoden umfassen einen Kopierkonstruktor, die Member der System.IEquatable<T>-Schnittstelle, Gleichheits-
und Ungleichheitstests sowie GetHashCode(). Zu diesem Zweck synthetisieren Sie PrintMembers . Sie könnten
auch Ihre eigene Version von ToString deklarieren, jedoch stellt PrintMembers eine bessere Option für
Vererbungsszenarios dar. Die Signatur muss mit der synthetisierten Methode übereinstimmen, wenn Sie Ihre
eigene Version einer synthetisierten Methode verwenden möchten.
Das Element TempRecords in der Konsolenausgabe ist nicht nützlich. Es zeigt den Typ an, erfüllt aber sonst
keinen Zweck. Sie können dieses Verhalten ändern, indem Sie Ihre eigene Implementierung der synthetisierten
PrintMembers -Methode angeben. Die Signatur hängt von den Modifizierern ab, die auf die record -Deklaration
angewendet werden:
Wenn ein Datensatztyp sealed oder record struct ist, lautet die Signatur
private bool PrintMembers(StringBuilder builder); .
Wenn ein Datensatztyp nicht sealed ist und von object abgeleitet wird (d. h., er deklariert keinen
Basisdatensatz), lautet die Signatur protected virtual bool PrintMembers(StringBuilder builder); .
Wenn ein Datensatztyp nicht sealed ist und von einem anderen Datensatz abgeleitet wird, lautet die
Signatur protected override bool PrintMembers(StringBuilder builder); .

Diese Regeln sind am einfachsten zu verstehen, wenn Sie den Zweck von PrintMembers verstehen.
PrintMembers fügt Informationen zu jeder Eigenschaft in einem Datensatztyp zu einer Zeichenfolge hinzu. Der
Vertrag erfordert, dass Basisdatensätze ihre Member zur Anzeige hinzufügen, und geht davon aus, dass
abgeleitete Member ihre Member hinzufügen. Jeder Datensatztyp synthetisiert eine ToString -Überschreibung,
die dem folgenden Beispiel für HeatingDegreeDays ähnelt:

public override string ToString()


{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("HeatingDegreeDays");
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}

Sie deklarieren eine PrintMembers -Methode im Datensatz DegreeDays , der den Typ der Sammlung nicht ausgibt:

protected virtual bool PrintMembers(StringBuilder stringBuilder)


{
stringBuilder.Append($"BaseTemperature = {BaseTemperature}");
return true;
}

Die Signatur deklariert eine virtual protected -Methode entsprechend der Version des Compilers. Sie müssen
sich keine Sorgen darüber machen, dass Sie die falschen Zugriffsmethoden verwenden, da die Sprache die
richtige Signatur erzwingt. Wenn Sie die richtigen Modifizierer für synthetisierte Methoden vergessen, gibt der
Compiler Warnungen oder Fehler aus, die Sie dabei unterstützen, die richtige Signatur zu verwenden.
Ab C# 10.0 können Sie die ToString -Methode im Datensatztyp als sealed deklarieren. Dadurch wird
verhindert, dass abgeleitete Datensätze eine neue Implementierung bereitstellen. Abgeleitete Datensätze
enthalten weiterhin die Überschreibung PrintMembers . Die ToString -Methode sollte versiegelt werden, wenn
der Laufzeittyp des Datensatzes nicht angezeigt werden soll. Im vorherigen Beispiel würde dabei die Information
verloren gehen, wo der Datensatz Wärme- oder Kältesummen gemessen hat.
Nicht destruktive Änderungen
Die synthetisierten Member in einer positionalen Datensatzklasse ändern den Zustand des Datensatzes nicht.
Das Ziel besteht darin, dass Sie unveränderliche Datensätze einfacher erstellen können. Denken Sie daran, dass
Sie ein readonly record struct -Element deklarieren, um eine unveränderliche Datensatzstruktur zu erstellen.
Sehen Sie sich noch einmal die vorherigen Deklarationen für HeatingDegreeDays und CoolingDegreeDays an. Die
hinzugefügten Member führen Berechnungen der Werte für den Datensatz durch, ändern aber nicht den
Zustand. Datensätze mit fester Breite vereinfachen das Erstellen unveränderlicher Verweistypen.
Das Erstellen unveränderlicher Verweistypen impliziert, dass Sie nicht destruktive Änderungen verwenden
sollten. Sie erstellen neue Datensatzinstanzen mit with -Ausdrücken, die den vorhandenen Datensatzinstanzen
ähneln. Diese Ausdrücke sind eine Kopierkonstruktion mit zusätzlichen Zuweisungen, die die Kopie ändern. Das
Ergebnis ist eine neue Datensatzinstanz, bei der alle Eigenschaften aus dem vorhandenen Datensatz kopiert und
optional geändert wurden. Der ursprüngliche Datensatz bleibt unverändert.
Als Nächstes fügen Sie zur Veranschaulichung von with -Ausdrücken einige Features zu Ihrem Programm
hinzu. Zuerst erstellen Sie einen neuen Datensatz zum Berechnen steigender Wärmesummen mithilfe derselben
Daten. Für steigende Wärmesummen werden in der Regel 41 F als Baselinetemperatur verwendet und sie
messen Temperaturen über der Baseline. Sie können einen neuen Datensatz erstellen, der coolingDegreeDays
ähnelt, aber eine andere Baselinetemperatur verwendet, um dieselben Daten zu verwenden:

// Growing degree days measure warming to determine plant growing rates


var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);

Sie können die Anzahl der berechneten Temperaturen mit den generierten Zahlen mit einer höheren
Baselinetemperatur vergleichen. Denken Sie daran, dass Datensätze Verweistypen sind und dass es sich bei
diesen Kopien um flache Kopien handelt. Das Array für die Daten wird nicht kopiert, aber beide Datensätze
beziehen sich auf dieselben Daten. Dies ist in einem anderen Szenario von Vorteil. Bei steigenden
Wärmesummen ist es nützlich, die Gesamtsumme der letzten fünf Tage zu überwachen. Mithilfe von with -
Ausdrücken können Sie neue Datensätze mit anderen Quelldaten erstellen. Mit dem folgenden Code wird eine
Sammlung dieser Akkumulationen erstellt, und anschließend werden die Werte angezeigt:

// showing moving accumulation of 5 days using range syntax


List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
Console.WriteLine(item);
}

Sie können auch with -Ausdrücke verwenden, um Kopien von Datensätzen zu erstellen. Geben Sie keine
Eigenschaften zwischen den geschweiften Klammern für den with -Ausdruck an. Das bedeutet, eine Kopie wird
erstellt und es werden keine Eigenschaften geändert:

var growingDegreeDaysCopy = growingDegreeDays with { };

Führen Sie die fertiggestellte Anwendung aus, um die Ergebnisse anzuzeigen.


Zusammenfassung
In diesem Tutorial wurden verschiedene Aspekte von Datensätzen vorgestellt. Datensätze bieten eine bündige
Syntax für Typen, deren grundlegender Zweck das Speichern von Daten ist. Bei objektorientierten Klassen ist der
grundlegende Zweck die Definition von Zuständigkeiten. Im Mittelpunkt dieses Tutorials standen positionale
Datensätze, in denen Sie die Eigenschaften eines Datensatzes mit einer bündigen Syntax deklarieren können.
Der Compiler synthetisiert einige Member des Datensatzes zum Kopieren und Vergleichen der Datensätze. Sie
können beliebige andere Member hinzufügen, die Sie für Ihre Datensatztypen benötigen. Sie können
unveränderliche Datensatztypen erstellen und sich gewiss sein, das kein vom Compiler generierter Member den
Zustand ändern würde. with -Ausdrücke vereinfachen zudem die Unterstützung nicht destruktiver Änderungen.
Datensätze bieten eine weitere Möglichkeit zum Definieren von Typen. Sie können class -Definitionen zum
Erstellen von objektorientierten Hierarchien verwenden, die sich auf die Zuständigkeiten und das Verhalten von
Objekten konzentrieren. Sie können struct -Typen für Datenstrukturen erstellen, die Daten speichern und klein
genug für effiziente Kopiervorgänge sind. Sie erstellen record -Typen, wenn Sie wertbasierte Gleichheit und
Vergleiche wünschen, keine Werte kopieren und Verweisvariablen verwenden möchten. Sie erstellen
record struct -Typen, wenn Sie die Merkmale von Datensätzen für einen Typ verwenden möchten, der klein
genug für ein effizientes Kopieren ist.
In der C#-Sprachreferenz zu Datensätzen, in der vorgeschlagenen Spezifikation des Datensatztyps sowie in der
Spezifikation zur Datensatzstruktur erfahren Sie mehr über Datensätze.
Tutorial: Schreiben von Code durch das Umsetzen
eigener Ideen mithilfe von Anweisungen der
obersten Ebene
04.11.2021 • 8 minutes to read

In diesem Tutorial lernen Sie Folgendes:


Kennenlernen der Regeln für die Verwendung von Anweisungen der obersten Ebene
Verwenden von Anweisungen der obersten Ebene zum Untersuchen von Algorithmen
Umwandeln von Erkenntnissen in wiederverwendbare Komponenten

Voraussetzungen
Sie müssen Ihren Computer zur Ausführung von .NET 6 einrichten, einschließlich des C# 10.0-Compilers. Der
C# 10.0-Compiler steht ab Visual Studio 2022 oder mit dem .NET 6.0 SDK zur Verfügung.
In diesem Tutorial wird vorausgesetzt, dass Sie C# und .NET, einschließlich Visual Studio oder die .NET Core-CLI
kennen.

Ausprobieren
Mithilfe von Anweisungen der obersten Ebene können Sie den zusätzlichen erforderlichen Schritt vermeiden,
indem Sie den Einstiegspunkt Ihres Programms in einer statischen Methode in einer Klasse platzieren. Der
typische Startpunkt für eine neue Konsolenanwendung sieht wie der folgende Code aus:

using System;

namespace Application
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

Der vorangehende Code ist das Ergebnis der Ausführung des dotnet new console -Befehls und der Erstellung
einer neuen Konsolenanwendung. Diese elf Zeilen enthalten nur eine Zeile ausführbaren Codes. Sie können
dieses Programm mit dem neuen Feature für Anweisungen der obersten Ebene vereinfachen. Dadurch können
Sie in diesem Programm bis auf zwei Zeilen alle anderen Zeilen entfernen.

// See https://aka.ms/new-console-template for more information


Console.WriteLine("Hello, World!");
IMPORTANT
Die C#-Vorlagen für .NET 6 verwenden Anweisungen der obersten Ebene. Ihre Anwendung passt möglicherweise nicht
zum Code in diesem Artikel, wenn Sie bereits ein Upgrade auf die .NET 6-Vorschauversionen durchgeführt haben. Weitere
Informationen finden Sie im Artikel Neue C#-Vorlagen generieren Anweisungen auf oberster Ebene.
Das .NET 6 SDK fügt auch eine Reihe impliziter global using -Anweisungen für Projekte hinzu, die die folgenden SDKs
verwenden:
Microsoft.NET.Sdk
Microsoft.NET.Sdk.Web
Microsoft.NET.Sdk.Worker
Diese impliziten global using -Anweisungen enthalten die gängigsten Namespaces für den Projekttyp.

Durch dieses Feature wird das Umsetzen neuer Ideen vereinfacht. Sie können Anweisungen der obersten Ebene
für Skripterstellungsszenarios oder zum Durchsuchen verwenden. Sobald Sie die Grundlagen beherrschen,
können Sie mit dem Umgestalten des Codes beginnen und Methoden, Klassen oder andere Assemblys für von
Ihnen entwickelte wiederverwendbare Komponenten erstellen. Anweisungen der obersten Ebene ermöglichen
das schnelle Testen von Code und bieten eine gute Grundlage für Einsteigertutorials. Außerdem bieten sie eine
praktische Möglichkeit für den Übergang von Experimenten mit Code hin zu vollständigen Programmen.
Anweisungen der obersten Ebene werden in der Reihenfolge ausgeführt, in der sie in der Datei aufgeführt sind.
Sie können nur in einer Quelldatei in der Anwendung verwendet werden. Der Compiler generiert einen Fehler,
wenn Sie die Anweisungen in mehr als einer Datei verwenden.

Entwickeln eines „magischen“ .NET-Programms zum Ausgeben von


Antworten
In diesem Tutorial erstellen Sie eine Konsolenanwendung, die eine Frage, die mit „Ja“ oder „Nein“ beantwortet
wird, zufällig beantwortet. Sie erstellen die Funktionalität Schritt für Schritt. Sie können sich auf Ihre Aufgabe
konzentrieren, anstatt sich mit dem Prozess zum Entwickeln der Struktur eines typischen Programms
beschäftigen zu müssen. Sobald Sie mit der Funktionalität zufrieden sind, können Sie die Anwendung nach
Bedarf umgestalten.
Es ist ein guter Startpunkt, die Frage wieder in die Konsole zu schreiben. Sie können beginnen, indem Sie den
folgenden Code schreiben:

Console.WriteLine(args);

Sie deklarieren keine args -Variable. Im Zusammenhang mit der einzelnen Quelldatei, die die Anweisungen der
obersten Ebene enthält, erkennt der Compiler, dass args für die Befehlszeilenargumente steht. Dies sind wie in
allen C#-Programmen Argumente vom Typ string[] .
Sie können den Code testen, indem Sie den folgenden dotnet run -Befehl ausführen:

dotnet run -- Should I use top level statements in all my programs?

Die Argumente nach -- in der Befehlszeile werden an das Programm weitergegeben. Sie können den Typ der
args -Variable sehen, da diese Information in der Konsole angezeigt wird:

System.String[]
Sie müssen die Argumente einzeln benennen und durch ein Leerzeichen trennen, um die Frage in die Konsole zu
schreiben. Ersetzen Sie den WriteLine -Aufruf durch den folgenden Code:

Console.WriteLine();
foreach(var s in args)
{
Console.Write(s);
Console.Write(' ');
}
Console.WriteLine();

Wenn Sie das Programm ausführen, wird die Frage nun ordnungsgemäß als Zeichenfolge von Argumenten
angezeigt.

Antworten mit einer zufälligen Antwort


Nachdem Sie die Frage wiederholt haben, können Sie den Code hinzufügen, um die zufällige Antwort zu
generieren. Fügen Sie zunächst ein Array möglicher Antworten hinzu:

string[] answers =
{
"It is certain.", "Reply hazy, try again.", "Don’t count on it.",
"It is decidedly so.", "Ask again later.", "My reply is no.",
"Without a doubt.", "Better not tell you now.", "My sources say no.",
"Yes – definitely.", "Cannot predict now.", "Outlook not so good.",
"You may rely on it.", "Concentrate and ask again.", "Very doubtful.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
};

Dieses Array umfasst zehn zustimmende, fünf zurückhaltende und fünf verneinende Antworten. Fügen Sie als
Nächstes den folgenden Code hinzu, um eine zufällige Antwort aus dem Array zu generieren und anzuzeigen:

var index = new Random().Next(answers.Length - 1);


Console.WriteLine(answers[index]);

Sie können die Anwendung noch mal ausführen, um die Ergebnisse anzuzeigen. Es sollte nun etwa die folgende
Ausgabe angezeigt werden:

dotnet run -- Should I use top level statements in all my programs?

Should I use top level statements in all my programs?


Better not tell you now.

Mit diesem Code werden die Fragen beantwortet, allerdings fügen Sie noch ein weiteres Feature hinzu. Ihre
Frage-App soll das Nachdenken vor dem Ausgeben der Antwort simulieren. Dies erreichen Sie, indem Sie eine
ASCII-Animation (American Standard Code for Information Interchange) hinzufügen und den restlichen Vorgang
während der Bearbeitung anhalten. Fügen Sie den folgenden Code nach der Zeile hinzu, die die Frage
wiederholt:
for (int i = 0; i < 20; i++)
{
Console.Write("| -");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("/ \\");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("- |");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("\\ /");
await Task.Delay(50);
Console.Write("\b\b\b");
}
Console.WriteLine();

Außerdem müssen Sie am Anfang der Quelldatei eine using -Anweisung hinzufügen:

using System.Threading.Tasks;

Die using -Anweisungen müssen sich vor allen anderen Anweisungen in der Datei befinden. Andernfalls tritt ein
Compilerfehler auf. Sie können das Programm noch mal ausführen und die Animation anzeigen. Dies
ermöglicht eine bessere Nutzung des Programms. Experimentieren Sie hinsichtlich der Länge der Verzögerung,
bis Sie mit dem Ergebnis zufrieden sind.
Der vorangehende Code erstellt eine Reihe von Zeilen, die durch ein Leerzeichen voneinander getrennt sind.
Durch das Hinzufügen des Schlüsselworts await wird der Compiler angewiesen, den Programmeinstiegspunkt
als Methode zu generieren, die den async -Modifizierer aufweist und die System.Threading.Tasks.Task-Klasse
zurückgibt. Dieses Programm gibt keinen Wert zurück, sodass der Programmeinstiegspunkt Task zurückgibt.
Wenn das Programm einen ganzzahligen Wert zurückgibt, fügen Sie eine return-Anweisung am Ende der
Anweisungen der obersten Ebene hinzu. Diese return-Anweisung gibt den ganzzahligen Wert an, der
zurückgegeben wird. Wenn die Anweisungen der obersten Ebene einen await -Ausdruck enthalten, wird der
Rückgabetyp zu einer System.Threading.Tasks.Task<TResult>-Klasse.

Umgestaltung für die Zukunft


Ihr Programm sollte wie der folgende Code aussehen:
Console.WriteLine();
foreach(var s in args)
{
Console.Write(s);
Console.Write(' ');
}
Console.WriteLine();

for (int i = 0; i < 20; i++)


{
Console.Write("| -");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("/ \\");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("- |");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("\\ /");
await Task.Delay(50);
Console.Write("\b\b\b");
}
Console.WriteLine();

string[] answers =
{
"It is certain.", "Reply hazy, try again.", "Don't count on it.",
"It is decidedly so.", "Ask again later.", "My reply is no.",
"Without a doubt.", "Better not tell you now.", "My sources say no.",
"Yes – definitely.", "Cannot predict now.", "Outlook not so good.",
"You may rely on it.", "Concentrate and ask again.", "Very doubtful.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
};

var index = new Random().Next(answers.Length - 1);


Console.WriteLine(answers[index]);

Der vorangehende Code ist nützlich. Er funktioniert. Er kann jedoch nicht erneut verwendet werden. Nachdem
die Anwendung jetzt funktioniert, ist es an der Zeit, wiederverwendbare Teile zu extrahieren.
Eine Option hierfür ist der Code, der die Animation beim Warten während des Vorgangs anzeigt. Aus diesem
Codeausschnitt kann eine Methode werden:
Erstellen Sie hierfür zunächst eine lokale Funktion in der Datei. Ersetzen Sie die aktuelle Animation durch den
folgenden Code:
await ShowConsoleAnimation();

static async Task ShowConsoleAnimation()


{
for (int i = 0; i < 20; i++)
{
Console.Write("| -");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("/ \\");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("- |");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("\\ /");
await Task.Delay(50);
Console.Write("\b\b\b");
}
Console.WriteLine();
}

Der vorangehende Code erstellt eine lokale Funktion in der Main-Methode. Diese ist immer noch nicht
wiederverwendbar. Extrahieren Sie den Code daher in eine Klasse. Erstellen Sie eine neue Datei namens
utilities.cs, und fügen Sie den folgenden Code hinzu:

namespace MyNamespace
{
public static class Utilities
{
public static async Task ShowConsoleAnimation()
{
for (int i = 0; i < 20; i++)
{
Console.Write("| -");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("/ \\");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("- |");
await Task.Delay(50);
Console.Write("\b\b\b");
Console.Write("\\ /");
await Task.Delay(50);
Console.Write("\b\b\b");
}
Console.WriteLine();
}
}
}

Eine Datei mit Anweisungen auf oberster Ebene kann auch Namespaces und Typen am Ende der Datei nach den
Anweisungen auf oberster Ebene enthalten. Für dieses Tutorial verwenden Sie die Animationsmethode jedoch in
einer separaten Datei, um sie leichter wiederverwenden zu können.
Zum Schluss können Sie den Animationscode bereinigen, um Duplizierungen zu entfernen:
foreach (string s in new[] { "| -", "/ \\", "- |", "\\ /", })
{
Console.Write(s);
await Task.Delay(50);
Console.Write("\b\b\b");
}

Nun verfügen Sie über eine komplette Anwendung, und Sie haben die wiederverwendbaren Teile für die spätere
Verwendung umgestaltet. Sie können die neue Hilfsprogrammmethode über Ihre Anweisungen der obersten
Ebene abrufen, wie unten in der fertigen Version des Hauptprogramms gezeigt:

using MyNamespace;

Console.WriteLine();
foreach(var s in args)
{
Console.Write(s);
Console.Write(' ');
}
Console.WriteLine();

await Utilities.ShowConsoleAnimation();

string[] answers =
{
"It is certain.", "Reply hazy, try again.", "Don’t count on it.",
"It is decidedly so.", "Ask again later.", "My reply is no.",
"Without a doubt.", "Better not tell you now.", "My sources say no.",
"Yes – definitely.", "Cannot predict now.", "Outlook not so good.",
"You may rely on it.", "Concentrate and ask again.", "Very doubtful.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
};

var index = new Random().Next(answers.Length - 1);


Console.WriteLine(answers[index]);

Im Beispiel oben wird der Aufruf Utilities.ShowConsoleAnimation hinzugefügt, und eine zusätzliche using -
Anweisung wird hinzugefügt.

Zusammenfassung
Anweisungen der obersten Ebene vereinfachen das Erstellen einfacher Programme, mit denen neue
Algorithmen untersucht werden können. Sie können mit Algorithmen experimentieren, indem Sie verschiedene
Codeausschnitte ausprobieren. Nachdem Sie gelernt haben, was funktioniert, können Sie den Code so
umgestalten, dass er leichter verwaltet werden kann.
Anweisungen der obersten Ebene vereinfachen Programme, die auf Konsolenanwendungen basieren. Hierzu
gehören Azure-Funktionen, GitHub-Aktionen und andere kleine Hilfsprogramme. Weitere Informationen finden
Sie unter Anweisungen auf oberster Ebene (C#-Programmierleitfaden).
Verwenden des Musterabgleichs zum Gestalten des
Verhaltens von Klassen für besseren Code
04.11.2021 • 10 minutes to read

Die Features für den Musterabgleich in C# stellen die Syntax zum Ausdrücken Ihrer Algorithmen bereit. Mithilfe
dieser Techniken können Sie das Verhalten in Ihren Klassen implementieren. Sie können einen objektorientierten
Klassenentwurf mit einer datenorientierten Implementierung kombinieren, um bei der Modellierung von
Objekten aus der realen Welt präzisen Code bereitzustellen.
In diesem Tutorial lernen Sie Folgendes:
Ausdrücken Ihrer objektorientierten Klassen mithilfe von Datenmustern
Implementieren dieser Muster mithilfe der C#-Features zum Musterabgleich
Nutzen der Compilerdiagnose zum Überprüfen Ihrer Implementierung

Voraussetzungen
Sie müssen Ihren Computer zur Ausführung von .NET 5 einrichten, einschließlich des C# 9.0-Compilers. Der
C# 9.0-Compiler steht ab Visual Studio 2019 Version 16.8 Preview oder .NET 5.0 SDK Preview zur Verfügung.

Erstellen einer Simulation einer Kanalschleuse


In diesem Tutorial erstellen Sie eine C#-Klasse, die eine Kanalschleuse simuliert. Eine Kanalschleuse ist, kurz
gesagt, eine Anlage, die Schiffe hebt und senkt, wenn sie sich zwischen zwei unterschiedlich hohen
Wasserständen bewegen. Eine Schleuse hat zwei Tore und einen Mechanismus zur Änderung des Wasserstands.
Im Normalbetrieb fährt ein Schiff in eines der Tore ein, wenn der Wasserstand in der Schleuse dem Wasserstand
auf der Seite entspricht, auf der das Schiff hineinfährt. Einmal in der Schleuse wird der Wasserstand
entsprechend dem Wasserstand geändert, bei dem das Boot die Schleuse verlässt. Sobald der Wasserstand mit
dieser Seite übereinstimmt, öffnet sich das Tor auf der Ausfahrtseite. Sicherheitsmaßnahmen stellen sicher, dass
ein Schiffsführer keine gefährliche Situation im Kanal herbeiführen kann. Der Wasserstand kann nur geändert
werden, wenn beide Tore geschlossen sind. Es darf höchstens ein Tor geöffnet sein. Um ein Tor zu öffnen, muss
der Wasserstand in der Schleuse mit dem Wasserstand außerhalb des zu öffnenden Tores übereinstimmen.
Sie können eine C#-Klasse erstellen, um dieses Verhalten zu modellieren. Eine CanalLock -Klasse muss Befehle
zum Öffnen oder Schließen eines der Tore bieten. Sie muss andere Befehle aufweisen, mit denen sich der
Wasserstand heben oder senken lässt. Die Klasse muss auch Eigenschaften zum Lesen der aktuellen Stellung der
beiden Tore und des Wasserstands unterstützen. Sicherheitsmaßnahmen werden mithilfe Ihrer Methoden
implementiert.

Definieren einer Klasse


Sie erstellen eine Konsolenanwendung, um Ihre CanalLock -Klasse zu testen. Erstellen Sie entweder in Visual
Studio oder mit der .NET CLI ein neues Konsolenprojekt für .NET 5. Fügen Sie dann eine neue Klasse hinzu, und
nennen Sie sie CanalLock . Als Nächstes entwerfen Sie Ihre öffentliche API, ohne jedoch die Methoden zu
implementieren:
public enum WaterLevel
{
Low,
High
}
public class CanalLock
{
// Query canal lock state:
public WaterLevel CanalLockWaterLevel { get; private set; } = WaterLevel.Low;
public bool HighWaterGateOpen { get; private set; } = false;
public bool LowWaterGateOpen { get; private set; } = false;

// Change the upper gate.


public void SetHighGate(bool open)
{
throw new NotImplementedException();
}

// Change the lower gate.


public void SetLowGate(bool open)
{
throw new NotImplementedException();
}

// Change water level.


public void SetWaterLevel(WaterLevel newLevel)
{
throw new NotImplementedException();
}

public override string ToString() =>


$"The lower gate is {(LowWaterGateOpen ? "Open" : "Closed")}. " +
$"The upper gate is {(HighWaterGateOpen ? "Open" : "Closed")}. " +
$"The water level is {CanalLockWaterLevel}.";
}

Der vorstehende Code initialisiert das Objekt so, dass beide Tore geschlossen sind und der Wasserstand niedrig
ist. Schreiben Sie als Nächstes den folgenden Testcode in Ihre Main -Methode, um Sie bei der Erstellung einer
ersten Implementierung der Klasse zu leiten:
// Create a new canal lock:
var canalGate = new CanalLock();

// State should be doors closed, water level low:


Console.WriteLine(canalGate);

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate: {canalGate}");

Console.WriteLine("Boat enters lock from lower gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate: {canalGate}");

canalGate.SetWaterLevel(WaterLevel.High);
Console.WriteLine($"Raise the water level: {canalGate}");
Console.WriteLine(canalGate);

canalGate.SetHighGate(open: true);
Console.WriteLine($"Open the higher gate: {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");


Console.WriteLine("Boat enters lock from upper gate");

canalGate.SetHighGate(open: false);
Console.WriteLine($"Close the higher gate: {canalGate}");

canalGate.SetWaterLevel(WaterLevel.Low);
Console.WriteLine($"Lower the water level: {canalGate}");

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate: {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate: {canalGate}");

Fügen Sie als Nächstes der CanalLock -Klasse eine erste Implementierung jeder Methode hinzu. Der folgende
Code implementiert die Methoden der Klasse, ohne dass die Sicherheitsregeln berücksichtigt werden.
Sicherheitstests fügen Sie später hinzu:

// Change the upper gate.


public void SetHighGate(bool open)
{
HighWaterGateOpen = open;
}

// Change the lower gate.


public void SetLowGate(bool open)
{
LowWaterGateOpen = open;
}

// Change water level.


public void SetWaterLevel(WaterLevel newLevel)
{
CanalLockWaterLevel = newLevel;
}

Die Tests, die Sie bisher geschrieben haben, werden bestanden. Sie haben die Grundlagen implementiert.
Schreiben Sie nun einen Test für die erste Fehlerbedingung. Am Ende der vorherigen Tests sind beide Tore
geschlossen, und der Wasserstand ist auf niedrig festgelegt. Fügen Sie einen Test hinzu, um zu versuchen, das
obere Tor zu öffnen:

Console.WriteLine("=============================================");
Console.WriteLine(" Test invalid commands");
// Open "wrong" gate (2 tests)
try
{
canalGate = new CanalLock();
canalGate.SetHighGate(open: true);
}
catch (InvalidOperationException)
{
Console.WriteLine("Invalid operation: Can't open the high gate. Water is low.");
}
Console.WriteLine($"Try to open upper gate: {canalGate}");

Dieser Test schlägt fehl, da das Tor geöffnet wird. Als erste Implementierung können Sie dies mit dem folgendem
Code beheben:

// Change the upper gate.


public void SetHighGate(bool open)
{
if (open && (CanalLockWaterLevel == WaterLevel.High))
HighWaterGateOpen = true;
else if (open && (CanalLockWaterLevel == WaterLevel.Low))
throw new InvalidOperationException("Cannot open high gate when the water is low");
}

Die Tests werden bestanden. Aber je mehr Tests Sie hinzufügen, desto mehr if -Klauseln werden Sie
hinzufügen und verschiedene Eigenschaften testen. Schon bald werden diese Methoden zu kompliziert, je mehr
Bedingungen Sie hinzufügen.

Implementieren der Befehle mit Mustern


Eine bessere Möglichkeit bieten Muster, um festzustellen, ob sich das Objekt in einem gültigen Zustand zur
Ausführung eines Befehls befindet. Sie können ausdrücken, ob ein Befehl in Abhängigkeit von drei Variablen
erlaubt ist: Stellung des Tores, Wasserstand und die neue Einstellung:

N EUE EIN ST EL L UN G ST EL L UN G DES TO RES WA SSERSTA N D ERGEB N IS

Geschlossen Geschlossen Hoch Geschlossen

Geschlossen Geschlossen Niedrig Geschlossen

Geschlossen Geöffnet Hoch Geöffnet

Geschlossen Geöffnet Niedrig Geschlossen

Öffnen Geschlossen Hoch Geöffnet

Geöffnet Geschlossen Niedrig Geschlossen (Fehler)

Geöffnet Geöffnet Hoch Geöffnet

Geöffnet Geöffnet Niedrig Geschlossen (Fehler)


In der vierten und letzten Zeile der Tabelle ist der Text durchgestrichen, weil sie ungültig ist. Der Code, den Sie
jetzt hinzufügen, muss sicherstellen, dass das Hochwassertor niemals bei niedrigem Wasserstand geöffnet wird.
Diese Zustände können als ein einziger Switch-Ausdruck programmiert werden (denken Sie daran, dass false
„Geschlossen“ bedeutet):

HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch


{
(false, false, WaterLevel.High) => false,
(false, false, WaterLevel.Low) => false,
(false, true, WaterLevel.High) => false,
(false, true, WaterLevel.Low) => false, // should never happen
(true, false, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the
water is low"),
(true, true, WaterLevel.High) => true,
(true, true, WaterLevel.Low) => false, // should never happen
};

Versuchen Sie diese Version. Die Tests werden bestanden und der Code bestätigt. Die vollständige Tabelle zeigt
die möglichen Kombinationen von Eingaben und Ergebnissen. Das bedeutet, dass Sie und andere Entwickler
schnell einen Blick auf die Tabelle werfen können, um festzustellen, dass Sie alle möglichen Eingaben abgedeckt
haben. Zur Vereinfachung kann auch der Compiler beitragen. Nachdem Sie den vorherigen Code hinzugefügt
haben, sehen Sie, dass der Compiler eine Warnung generiert: CS8524 bedeutet, dass der Switch-Ausdruck nicht
alle möglichen Eingaben abdeckt. Der Grund für diese Warnung ist, dass eine der Eingaben den Typ enum hat.
Der Compiler interpretiert „alle möglichen Eingaben“ als alle Eingaben des zugrunde liegenden Typs,
typischerweise int . Dieser switch -Ausdruck prüft nur die in enum deklarierten Werte. Um die Warnung zu
entfernen, können Sie für den letzten Teil des Ausdrucks ein Muster des Typs „Alle abfangen und entsorgen“
hinzufügen. Diese Bedingung löst eine Ausnahme aus, da sie eine ungültige Eingabe angibt:

_ => throw new InvalidOperationException("Invalid internal state"),

Der vorherige Switch-Arm muss der letzte in Ihrem switch -Ausdruck sein, da er mit allen Eingaben
übereinstimmt. Experimentieren Sie, indem Sie ihn in der Reihenfolge nach vorn verschieben. Dies verursacht
den Compilerfehler CS8510 für nicht erreichbaren Code in einem Muster. Die natürliche Struktur von Switch-
Ausdrücken ermöglicht es dem Compiler, Fehler und Warnungen für mögliche Fehlersituationen zu generieren.
Das „Sicherheitsnetz“ des Compilers erleichtert Ihnen, fehlerfreien Code in weniger Iterationen zu erstellen, und
bietet Ihnen die Möglichkeit, Switch-Arme mit Platzhaltern frei zu kombinieren. Der Compiler löst Fehler aus,
wenn Ihre Kombination zu unerreichbaren Armen führt, mit denen Sie nicht gerechnet haben, und Warnungen,
wenn Sie einen benötigten Arm entfernen.
Die erste Änderung besteht darin, alle Arme zu kombinieren, bei denen der Befehl lautet, das Tor zu schließen.
Das ist stets erlaubt. Fügen Sie den folgenden Code als ersten Arm in den Switch-Ausdruck ein:

(false, _, _) => false,

Nachdem Sie den vorherigen Switch-Arm hinzugefügt haben, erhalten Sie vier Compilerfehler, einen in jedem
der Arme, in denen der Befehl false ist. Diese Arme sind bereits durch den neu hinzugefügten Arm abgedeckt.
Sie können diese vier Zeilen sicher entfernen. Sie wollten mit diesem neuen Switch-Arm diese Bedingungen
ersetzen.
Als Nächstes können Sie die vier Arme vereinfachen, bei denen der Befehl lautet, das Tor zu öffnen. In beiden
Fällen, in denen der Wasserstand hoch ist, kann das Tor geöffnet werden. (In einem ist es bereits geöffnet.) Ein
Fall, in dem der Wasserstand niedrig ist, löst eine Ausnahme aus, der andere darf nicht passieren. Es sollte sicher
sein, die gleiche Ausnahme auszulösen, wenn sich die Schleuse bereits in einer ungültigen Stellung befindet. Sie
können die folgenden Vereinfachungen für diese Arme vornehmen:

(true, _, WaterLevel.High) => true,


(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water
is low"),
_ => throw new InvalidOperationException("Invalid internal state"),

Führen Sie die Tests erneut aus, die bestanden werden. Hier ist die endgültige Version der SetHighGate -
Methode:

// Change the upper gate.


public void SetHighGate(bool open)
{
HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
(false, _, _) => false,
(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when
the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}

Eigenständiges Implementieren der Muster


Nachdem Sie die Technik kennengelernt haben, füllen Sie die Methoden SetLowGate und SetWaterLevel selbst
aus. Beginnen Sie mit dem Hinzufügen des folgenden Codes, um ungültige Vorgänge in diesen Methoden zu
testen:
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetWaterLevel(WaterLevel.High);
canalGate.SetLowGate(open: true);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't open the lower gate. Water is high.");
}
Console.WriteLine($"Try to open lower gate: {canalGate}");
// change water level with gate open (2 tests)
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetLowGate(open: true);
canalGate.SetWaterLevel(WaterLevel.High);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't raise water when the lower gate is open.");
}
Console.WriteLine($"Try to raise water with lower gate open: {canalGate}");
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetWaterLevel(WaterLevel.High);
canalGate.SetHighGate(open: true);
canalGate.SetWaterLevel(WaterLevel.Low);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't lower water when the high gate is open.");
}
Console.WriteLine($"Try to lower water with high gate open: {canalGate}");

Führen Sie Ihre Anwendung erneut aus. Sie erleben, wie die neuen Tests fehlschlagen und die Kanalschleuse in
einen ungültigen Zustand gerät. Versuchen Sie, die restlichen Methoden selbst zu implementieren. Die Methode
zur Festlegung des unteren Tores sollte ähnlich wie die Methode zur Festlegung des oberen Tores sein. Die
Methode zum Ändern des Wasserstands hat unterschiedliche Prüfungen, sollte aber eine ähnliche Struktur
aufweisen. Möglicherweise finden Sie es hilfreich, dasselbe Verfahren auch für die Methode zu verwenden, mit
der der Wasserstand festgelegt wird. Beginnen Sie mit allen vier Eingaben: Stellung der beiden Tore, aktueller
Wasserstand und geforderter neuer Wasserstand. Der Switch-Ausdruck sollte wie folgt beginnen:

CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch


{
// elided
};

Es gibt insgesamt 16 auszufüllende Switch-Arme. Testen und vereinfachen Sie das Ganze im Anschluss.
Haben Sie Methoden in etwa wie folgt gestaltet?
// Change the lower gate.
public void SetLowGate(bool open)
{
LowWaterGateOpen = (open, LowWaterGateOpen, CanalLockWaterLevel) switch
{
(false, _, _) => false,
(true, _, WaterLevel.Low) => true,
(true, false, WaterLevel.High) => throw new InvalidOperationException("Cannot open high gate when
the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}

// Change water level.


public void SetWaterLevel(WaterLevel newLevel)
{
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
(WaterLevel.Low, WaterLevel.Low, true, false) => WaterLevel.Low,
(WaterLevel.High, WaterLevel.High, false, true) => WaterLevel.High,
(WaterLevel.Low, _, false, false) => WaterLevel.Low,
(WaterLevel.High, _, false, false) => WaterLevel.High,
(WaterLevel.Low, WaterLevel.High, false, true) => throw new InvalidOperationException("Cannot lower
water when the high gate is open"),
(WaterLevel.High, WaterLevel.Low, true, false) => throw new InvalidOperationException("Cannot raise
water when the low gate is open"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}

Die Tests sollten bestanden werden, und die Schleuse sollte sicher betrieben werden.

Zusammenfassung
In diesem Tutorial haben Sie gelernt, mithilfe eines Musterabgleichs den internen Zustand eines Objekts zu
überprüfen, ehe Änderungen an diesem Zustand vorgenommen werden. Sie können Kombinationen von
Eigenschaften überprüfen. Sobald Sie Tabellen für beliebige dieser Übergänge erstellt haben, testen Sie Ihren
Code, und vereinfachen ihn dann hinsichtlich Lesbarkeit und Wartbarkeit. Aus diesen anfänglichen Refactorings
können sich weitere Refactorings ergeben, die den internen Zustand validieren oder andere API-Änderungen
verwalten. In diesem Tutorial wurden Klassen und Objekte mit einem eher datenorientierten, musterbasierten
Ansatz kombiniert, um diese Klassen zu implementieren.
Tutorial: Aktualisieren von Schnittstellen mit
Standardschnittstellenmethoden in C# 8.0
04.11.2021 • 5 minutes to read

Ab C# 8.0 können Sie in .NET Core 3.0 eine Implementierung definieren, wenn Sie einen Member einer
Schnittstelle deklarieren. Das häufigste Szenario ist das sichere Hinzufügen von Membern zu einer Schnittstelle,
die bereits veröffentlicht ist und von unzähligen Clients verwendet wird.
In diesem Tutorial lernen Sie, wie die folgenden Aufgaben ausgeführt werden:
Erweitern Sie Schnittstellen problemlos durch Hinzufügen von Methoden mit Implementierungen.
Erstellen Sie parametrisierte Implementierungen, um größere Flexibilität zu bieten.
Ermöglichen Sie Implementierern, eine spezifischere Implementierung in Form einer Überschreibung zu
bieten.

Voraussetzungen
Sie müssen Ihren Computer zur Ausführung von .NET Core einrichten, einschließlich des C# 8.0-Compilers. Der
C# 8.0-Compiler steht ab Visual Studio 2019 Version 16.3 oder mit dem .NET Core 3.0 SDK zur Verfügung.

Übersicht über das Szenario


Dieses Tutorial beginnt mit Version 1 einer Kundenbeziehungsbibliothek. Sie erhalten die Startanwendung von
unserem Beispielerepository auf GitHub. Das Unternehmen, das diese Bibliothek erstellt hat, beabsichtigte, dass
Kunden mit vorhandenen Anwendungen seine Bibliothek verwenden. Minimale Schnittstellendefinitionen
wurden bereitgestellt, die Benutzer ihrer Bibliothek implementieren sollten. So sieht die Schnittstellendefinition
für einen Kunden aus:

public interface ICustomer


{
IEnumerable<IOrder> PreviousOrders { get; }

DateTime DateJoined { get; }


DateTime? LastOrder { get; }
string Name { get; }
IDictionary<DateTime, string> Reminders { get; }
}

Eine zweite Schnittstelle wurde definiert, die eine Bestellung darstellt:

public interface IOrder


{
DateTime Purchased { get; }
decimal Cost { get; }
}

Von diesen Schnittstellen aus konnte das Team eine Bibliothek für die Benutzer erstellen, um den Kunden eine
bessere Benutzererfahrung zu bieten. Das Ziel bestand darin, eine intensivere Beziehung zu Bestandskunden
aufzubauen und ihre Beziehungen zu neuen Kunden zu verbessern.
Jetzt ist es Zeit, die Bibliothek für das nächste Release zu aktualisieren. Eines der angeforderten Features
gewährt Kunden, die viele Bestellungen aufgeben, einen Treuerabatt. Dieser neue Treuerabatt wird angewendet,
wenn ein Kunde eine Bestellung aufgibt. Der spezifische Rabatt ist eine Eigenschaft jedes einzelnen Kunden. Jede
Implementierung von ICustomer kann andere Regeln für den Treuerabatt festlegen.
Die naheliegendste Methode zum Hinzufügen dieser Funktionalität ist die Verbesserung der ICustomer -
Schnittstelle mit einer Methode zur Anwendung eines Treuerabatts. Diese Entwurfsempfehlung löste bei
erfahrenen Entwicklern Bedenken aus: „Schnittstellen sind unveränderlich, sobald sie veröffentlicht sind! Dies ist
eine einschneidende Änderung!“ C# 8.0 fügt Standardschnittstellenimplementierungen zum Aktualisieren von
Schnittstellen hinzu. Die Autoren der Bibliothek können der Schnittstelle neue Member hinzufügen und eine
Standardimplementierung für diese Member bereitstellen.
Mit Implementierungen von Standardschnittstellen können Entwickler eine Schnittstelle aktualisieren, während
gleichzeitig alle Implementierer diese Implementierung überschreiben können. Benutzer der Bibliothek können
die standardmäßige Implementierung als eine nicht unterbrechende Änderung akzeptieren. Wenn ihre
Geschäftsregeln anders sind, können sie überschreiben.

Upgraden mit Standardschnittstellenmethoden


Das Team stimmte der wahrscheinlichsten Standardimplementierung zu: einem Treuerabatt für Kunden.
Das Upgrade sollte die Funktionalität zum Festlegen von zwei Eigenschaften bieten: die für den Rabatt
erforderliche Anzahl an Bestellungen sowie den Prozentsatz des Rabatts. Damit wird es zum idealen Szenario für
Standardschnittstellenmethoden. Sie können der ICustomer -Schnittstelle eine Methode hinzufügen und die
wahrscheinlichste Implementierung bereitstellen. Alle vorhandenen und alle neuen Implementierungen können
die Standardimplementierung verwenden oder ihre eigene angeben.
Fügen Sie zunächst die neue Methode einschließlich deren Text der Schnittstelle hinzu:

// Version 1:
public decimal ComputeLoyaltyDiscount()
{
DateTime TwoYearsAgo = DateTime.Now.AddYears(-2);
if ((DateJoined < TwoYearsAgo) && (PreviousOrders.Count() > 10))
{
return 0.10m;
}
return 0;
}

Der Bibliotheksautor schrieb einen ersten Test zum Überprüfen der Implementierung:

SampleCustomer c = new SampleCustomer("customer one", new DateTime(2010, 5, 31))


{
Reminders =
{
{ new DateTime(2010, 08, 12), "childs's birthday" },
{ new DateTime(1012, 11, 15), "anniversary" }
}
};

SampleOrder o = new SampleOrder(new DateTime(2012, 6, 1), 5m);


c.AddOrder(o);

o = new SampleOrder(new DateTime(2103, 7, 4), 25m);


c.AddOrder(o);

// Check the discount:


ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");
Beachten Sie den folgenden Teil des Tests:

// Check the discount:


ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

Diese Umwandlung von SampleCustomer zu ICustomer ist erforderlich. Die SampleCustomer -Klasse muss keine
Implementierung für ComputeLoyaltyDiscount bereitstellen; dies erfolgt über die ICustomer -Schnittstelle.
Allerdings erbt die SampleCustomer -Klasse keine Member von ihren Schnittstellen. Diese Regel hat sich nicht
geändert. Um jede in der Schnittstelle deklarierte und implementierte Methode aufrufen zu können, muss die
Variable vom Typ der Schnittstelle sein, in diesem Beispiel ICustomer .

Bereitstellen der Parametrisierung


Ein guter Anfang. Aber die Standardimplementierung ist zu restriktiv. Viele Nutzer dieses Systems könnten
unterschiedliche Schwellenwerte für die Anzahl der Käufe, eine andere Dauer der Mitgliedschaft oder einen
anderen Rabattprozentsatz auswählen. Sie können mehr Kunden eine bessere Upgradeerfahrung bieten, indem
Sie eine Möglichkeit zum Festlegen dieser Parameter bereitstellen. Wird fügen nun eine statische Methode
hinzu, die diese drei, die Standardimplementierung steuernden Parameter festlegt:

// Version 2:
public static void SetLoyaltyThresholds(
TimeSpan ago,
int minimumOrders = 10,
decimal percentageDiscount = 0.10m)
{
length = ago;
orderCount = minimumOrders;
discountPercent = percentageDiscount;
}
private static TimeSpan length = new TimeSpan(365 * 2, 0,0,0); // two years
private static int orderCount = 10;
private static decimal discountPercent = 0.10m;

public decimal ComputeLoyaltyDiscount()


{
DateTime start = DateTime.Now - length;

if ((DateJoined < start) && (PreviousOrders.Count() > orderCount))


{
return discountPercent;
}
return 0;
}

Dieses kleine Codefragment zeigt viele neue Sprachfunktionen. Schnittstellen können nun statische Member
einschließlich Feldern und Methoden enthalten. Verschiedene Zugriffsmodifizierer sind ebenfalls aktiviert. Die
zusätzlichen Felder sind privat, die neue Methode ist öffentlich. Beliebige der Modifizierer sind auf
Schnittstellenmembern erlaubt.
Anwendungen, die die allgemeine Formel zum Berechnen des Treuerabatts verwenden, aber andere Parameter,
müssen keine benutzerdefinierte Implementierung bereitstellen; sie können die Argumente über eine statische
Methode festlegen. Der folgende Code legt z.B. eine „Kundenwertschätzung“ fest, die jeden Kunden mit mehr als
einem Monat Mitgliedschaft belohnt:

ICustomer.SetLoyaltyThresholds(new TimeSpan(30, 0, 0, 0), 1, 0.25m);


Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");
Erweitern der Standardimplementierung
Der Code, den Sie bisher hinzugefügt haben, hat eine einfache Implementierung für diese Szenarien ermöglicht,
in denen Benutzer etwas wie die Standardimplementierung wünschen, oder um eine unzusammenhängende
Gruppe von Regeln bereitzustellen. Für ein finales Feature werden wir den Code ein wenig umgestalten, um
Szenarien zu ermöglichen, in denen Benutzer die Standardimplementierung erstellen möchten.
Stellen Sie sich ein Startupunternehmen vor, das neue Kunden gewinnen möchte. Es bietet einen Preisnachlass
von 50% für die erste Bestellung eines neuen Kunden. Andernfalls erhalten Bestandskunden den Standardrabatt.
Der Bibliotheksautor muss die Standardimplementierung in eine protected static -Methode verschieben,
sodass jede Klasse, die diese Schnittstelle implementiert, den Code in ihrer Implementierung wiederverwenden
kann. Die Standardimplementierung des Schnittstellenmembers ruft diese freigegebene Methode ebenfalls auf:

public decimal ComputeLoyaltyDiscount() => DefaultLoyaltyDiscount(this);


protected static decimal DefaultLoyaltyDiscount(ICustomer c)
{
DateTime start = DateTime.Now - length;

if ((c.DateJoined < start) && (c.PreviousOrders.Count() > orderCount))


{
return discountPercent;
}
return 0;
}

In einer Implementierung einer Klasse, die diese Schnittstelle implementiert, kann die Überschreibung die
statische Hilfsmethode aufrufen und diese Logik zum Bereitstellen des „Neuer Kunde“-Rabatts erweitern:

public decimal ComputeLoyaltyDiscount()


{
if (PreviousOrders.Any() == false)
return 0.50m;
else
return ICustomer.DefaultLoyaltyDiscount(this);
}

Den vollständigen Code finden Sie in unserem Beispielrepository auf GitHub. Sie erhalten die Startanwendung
von unserem Beispielerepository auf GitHub.
Diese neuen Features bedeuten, dass Schnittstellen problemlos aktualisiert werden können, wenn eine
vernünftige Standardimplementierung für diese neuen Member vorhanden ist. Entwerfen Sie Schnittstellen
sorgfältig, um einzelne funktionale Konzepte auszudrücken, die von mehreren Klassen implementiert werden
können. Dies erleichtert das Aktualisieren dieser Schnittstellendefinitionen, wenn neue Anforderungen für diese
gleichen funktionalen Konzept entdeckt werden.
Tutorial: Untermischen von Funktionalität beim
Erstellen von Klassen mithilfe von Schnittstellen mit
Standardschnittstellenmethoden
04.11.2021 • 8 minutes to read

Ab C# 8.0 können Sie in .NET Core 3.0 eine Implementierung definieren, wenn Sie einen Member einer
Schnittstelle deklarieren. Dieses Feature bietet neue Funktionen, mit denen Sie Standardimplementierungen für
Funktionen definieren können, die in Schnittstellen deklariert werden. Klassen können auswählen, wann die
Funktionalität überschrieben werden soll, wann die Standardfunktionalität verwendet werden soll und wann
keine Unterstützung für diskrete Features deklariert werden soll.
In diesem Tutorial lernen Sie, wie die folgenden Aufgaben ausgeführt werden:
Erstellen von Schnittstellen mit Implementierungen, die diskrete Features beschreiben.
Erstellen von Klassen, die die Standardimplementierungen verwenden.
Erstellen von Klassen, die einige oder alle Standardimplementierungen überschreiben.

Voraussetzungen
Sie müssen Ihren Computer zur Ausführung von .NET Core einrichten, einschließlich des C# 8.0-Compilers. Der
C# 8.0-Compiler steht ab Visual Studio 2019 Version 16.3 oder ab dem .NET Core 3.0 SDK zur Verfügung.

Einschränkungen von Erweiterungsmethoden


Eine Möglichkeit, Verhalten zu implementieren, das als Teil einer Schnittstelle auftritt, besteht darin,
Erweiterungsmethoden zu definieren, die das Standardverhalten bereitstellen. Schnittstellen deklarieren einen
minimalen Satz von Membern und bieten gleichzeitig eine größere Oberfläche für jede Klasse, die diese
Schnittstelle implementiert. Die Erweiterungsmethoden in Enumerable stellen beispielsweise die
Implementierung für jede beliebige Sequenz als Quelle einer LINQ-Abfrage bereit.
Erweiterungsmethoden werden zur Kompilierzeit mithilfe des deklarierten Typs der Variablen aufgelöst. Klassen,
die die Schnittstelle implementieren, können eine bessere Implementierung für jede beliebige
Erweiterungsmethode bereitstellen. Variablendeklarationen müssen dem implementierenden Typ entsprechen,
damit der Compiler diese Implementierung auswählen kann. Wenn der Kompilierzeittyp mit der Schnittstelle
übereinstimmt, werden Methodenaufrufe in die Erweiterungsmethode aufgelöst. Ein weiteres Problem bei
Erweiterungsmethoden ist, dass auf diese Methoden überall dort zugegriffen werden kann, wo auf die Klasse,
die die Erweiterungsmethoden enthält, zugegriffen werden kann. Klassen können nichts deklarieren, wenn sie in
Erweiterungsmethoden deklarierte Funktionen bereitstellen oder nicht bereitstellen sollten.
Ab C# 8.0 können Sie die Standardimplementierungen als Schnittstellenmethoden deklarieren. Anschließend
verwendet jede Klasse automatisch die Standardimplementierung. Jede Klasse, die eine bessere
Implementierung bereitstellen kann, kann die Definition der Schnittstellenmethode mit einem besseren
Algorithmus überschreiben. In gewisser Weise klingt diese Technik ähnlich wie die Verwendung von
Erweiterungsmethoden.
In diesem Artikel erfahren Sie, wie Standardschnittstellenimplementierungen neue Szenarien ermöglichen.

Entwerfen der Anwendung


Stellen Sie sich eine Anwendung für Smart Home-Automatisierung vor. Sie haben wahrscheinlich viele
verschiedene Arten von Leuchten und Indikatoren, die im gesamten Haus verwendet werden könnten. Jede
Leuchte muss APIs unterstützen, um sie ein- und auszuschalten und den aktuellen Zustand zu melden. Einige
Leuchten und Indikatoren unterstützen möglicherweise andere Funktionen, beispielsweise:
Einschalten der Leuchte und Ausschalten anhand eines Timers.
Blinklichtfunktion der Leuchte für einen bestimmten Zeitraum.
Einige dieser erweiterten Funktionen können auf Geräten emuliert werden, die den minimalen Satz
unterstützen. Dies bedeutet, dass eine Standardimplementierung bereitgestellt wird. Für Geräte, die über mehr
integrierte Funktionen verfügen, würde die Gerätesoftware die nativen Funktionen verwenden. Für andere
Leuchten können sie sich entscheiden, die Schnittstelle zu implementieren und die Standardimplementierung zu
verwenden.
Standardschnittstellenmember sind eine bessere Lösung für dieses Szenario als Erweiterungsmethoden.
Klassenautoren können steuern, welche Schnittstellen sie implementieren möchten. Diese Schnittstellen, die sie
auswählen, sind als Methoden verfügbar. Da Standardschnittstellenmethoden standardmäßig virtuell sind, wählt
die Methodenbindung außerdem immer die Implementierung in der-Klasse aus.
Erstellen wir den Code, um diese Unterschiede zu veranschaulichen.

Erstellen von Schnittstellen


Beginnen Sie, indem Sie die Schnittstelle erstellen, die das Verhalten für alle Leuchten definiert:

public interface ILight


{
void SwitchOn();
void SwitchOff();
bool IsOn();
}

Eine einfache Deckenleuchte könnte diese Schnittstelle wie im folgenden Code dargestellt implementieren:

public class OverheadLight : ILight


{
private bool isOn;
public bool IsOn() => isOn;
public void SwitchOff() => isOn = false;
public void SwitchOn() => isOn = true;

public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

In diesem Tutorial werden keine IoT-Geräte durch den Code gesteuert, sondern diese Aktivitäten werden
emuliert, indem Nachrichten in die Konsole geschrieben werden. Sie können den Code untersuchen, ohne Ihr
Haus zu automatisieren.
Definieren wir nun die Schnittstelle für eine Leuchte, die nach einem Timeout automatisch ausgeschaltet werden
kann:

public interface ITimerLight : ILight


{
Task TurnOnFor(int duration);
}

Sie könnten eine Basisimplementierung zur Deckenleuchte hinzufügen, aber eine bessere Lösung besteht darin,
diese Schnittstellendefinition zu ändern, um eine virtual -Standardimplementierung bereitzustellen:

public interface ITimerLight : ILight


{
public async Task TurnOnFor(int duration)
{
Console.WriteLine("Using the default interface method for the ITimerLight.TurnOnFor.");
SwitchOn();
await Task.Delay(duration);
SwitchOff();
Console.WriteLine("Completed ITimerLight.TurnOnFor sequence.");
}
}

Durch Hinzufügen dieser Änderung kann die OverheadLight -Klasse die Timerfunktion implementieren, indem
sie Unterstützung für die Schnittstelle deklariert:

public class OverheadLight : ITimerLight { }

Ein anderer Leuchtentyp unterstützt möglicherweise ein anspruchsvolleres Protokoll. Er kann seine eigene
Implementierung für TurnOnFor bereitstellen, wie im folgenden Code gezeigt:

public class HalogenLight : ITimerLight


{
private enum HalogenLightState
{
Off,
On,
TimerModeOn
}

private HalogenLightState state;


public void SwitchOn() => state = HalogenLightState.On;
public void SwitchOff() => state = HalogenLightState.Off;
public bool IsOn() => state != HalogenLightState.Off;
public async Task TurnOnFor(int duration)
{
Console.WriteLine("Halogen light starting timer function.");
state = HalogenLightState.TimerModeOn;
await Task.Delay(duration);
state = HalogenLightState.Off;
Console.WriteLine("Halogen light finished custom timer function");
}

public override string ToString() => $"The light is {state}";


}

Im Gegensatz zum Überschreiben von Methoden der virtuellen Klasse verwendet die Deklaration von
TurnOnFor in der HalogenLight -Klasse nicht das Schlüsselwort override .

Mix-und-Match-Funktionen
Die Vorteile von Standardschnittstellenmethoden werden deutlicher, wenn Sie erweiterte Funktionen einführen.
Durch die Verwendung von Schnittstellen können Sie Mix-und-Match-Funktionen verwenden. Außerdem kann
jeder Klassenautor zwischen der Standardimplementierung und einer benutzerdefinierten Implementierung
wählen. Fügen wir eine Schnittstelle mit einer Standardimplementierung für eine blinkende Leuchte hinzu:
public interface IBlinkingLight : ILight
{
public async Task Blink(int duration, int repeatCount)
{
Console.WriteLine("Using the default interface method for IBlinkingLight.Blink.");
for (int count = 0; count < repeatCount; count++)
{
SwitchOn();
await Task.Delay(duration);
SwitchOff();
await Task.Delay(duration);
}
Console.WriteLine("Done with the default interface method for IBlinkingLight.Blink.");
}
}

Die Standardimplementierung ermöglicht jeder Leuchte das Blinken. Die Deckenleuchte kann sowohl Timer- als
auch Blinkfunktionen mit der Standardimplementierung hinzufügen:

public class OverheadLight : ILight, ITimerLight, IBlinkingLight


{
private bool isOn;
public bool IsOn() => isOn;
public void SwitchOff() => isOn = false;
public void SwitchOn() => isOn = true;

public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

Ein neuer Leuchtentyp ( LEDLight ) unterstützt die Timerfunktion und die Blinkfunktion direkt. Dieser
Leuchtenstil implementiert sowohl die ITimerLight - als auch die IBlinkingLight -Schnittstelle und überschreibt
die Blink -Methode:

public class LEDLight : IBlinkingLight, ITimerLight, ILight


{
private bool isOn;
public void SwitchOn() => isOn = true;
public void SwitchOff() => isOn = false;
public bool IsOn() => isOn;
public async Task Blink(int duration, int repeatCount)
{
Console.WriteLine("LED Light starting the Blink function.");
await Task.Delay(duration * repeatCount);
Console.WriteLine("LED Light has finished the Blink funtion.");
}

public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

Ein ExtraFancyLight -Element unterstützt ggf. Blink- und Timerfunktionen direkt:


public class ExtraFancyLight : IBlinkingLight, ITimerLight, ILight
{
private bool isOn;
public void SwitchOn() => isOn = true;
public void SwitchOff() => isOn = false;
public bool IsOn() => isOn;
public async Task Blink(int duration, int repeatCount)
{
Console.WriteLine("Extra Fancy Light starting the Blink function.");
await Task.Delay(duration * repeatCount);
Console.WriteLine("Extra Fancy Light has finished the Blink function.");
}
public async Task TurnOnFor(int duration)
{
Console.WriteLine("Extra Fancy light starting timer function.");
await Task.Delay(duration);
Console.WriteLine("Extra Fancy light finished custom timer function");
}

public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

Das HalogenLight -Element, das Sie zuvor erstellt haben, unterstützt kein Blinken. Fügen Sie IBlinkingLight
daher nicht der Liste der unterstützten Schnittstellen dieses Elements hinzu.

Erkennen der Leuchtentypen mithilfe von Musterabgleich


Schreiben wir nun etwas Testcode. Sie können das Feature Musterabgleich von C# verwenden, um die
Funktionen einer Leuchte zu ermitteln, indem Sie untersuchen, welche Schnittstellen sie unterstützt. Die
folgende Methode gibt die unterstützten Fähigkeiten der einzelnen Leuchten aus:

private static async Task TestLightCapabilities(ILight light)


{
// Perform basic tests:
light.SwitchOn();
Console.WriteLine($"\tAfter switching on, the light is {(light.IsOn() ? "on" : "off")}");
light.SwitchOff();
Console.WriteLine($"\tAfter switching off, the light is {(light.IsOn() ? "on" : "off")}");

if (light is ITimerLight timer)


{
Console.WriteLine("\tTesting timer function");
await timer.TurnOnFor(1000);
Console.WriteLine("\tTimer function completed");
}
else
{
Console.WriteLine("\tTimer function not supported.");
}

if (light is IBlinkingLight blinker)


{
Console.WriteLine("\tTesting blinking function");
await blinker.Blink(500, 5);
Console.WriteLine("\tBlink function completed");
}
else
{
Console.WriteLine("\tBlink function not supported.");
}
}
Der folgende Code in der Main -Methode erstellt alle Leuchtentypen nacheinander und testet die einzelnen
Leuchten:

static async Task Main(string[] args)


{
Console.WriteLine("Testing the overhead light");
var overhead = new OverheadLight();
await TestLightCapabilities(overhead);
Console.WriteLine();

Console.WriteLine("Testing the halogen light");


var halogen = new HalogenLight();
await TestLightCapabilities(halogen);
Console.WriteLine();

Console.WriteLine("Testing the LED light");


var led = new LEDLight();
await TestLightCapabilities(led);
Console.WriteLine();

Console.WriteLine("Testing the fancy light");


var fancy = new ExtraFancyLight();
await TestLightCapabilities(fancy);
Console.WriteLine();
}

So ermittelt der Compiler die beste Implementierung


Dieses Szenario zeigt eine Basisschnittstelle ohne Implementierungen. Durch das Hinzufügen einer Methode zur
ILight -Schnittstelle werden neue Komplexitäten eingeführt. Die Sprachregeln, die für
Standardschnittstellenmethoden gelten, minimieren die Auswirkungen auf die konkreten Klassen, die mehrere
abgeleitete Schnittstellen implementieren. Erweitern wir die ursprüngliche Schnittstelle durch eine neue
Methode, um zu zeigen, wie sich dadurch ihre Verwendung ändert. Jede Indikatorleuchte kann ihren
Energiestatus als Enumerationswert melden:

public enum PowerStatus


{
NoPower,
ACPower,
FullBattery,
MidBattery,
LowBattery
}

Die Standardimplementierung geht von einer fehlenden Stromversorgung aus:

public interface ILight


{
void SwitchOn();
void SwitchOff();
bool IsOn();
public PowerStatus Power() => PowerStatus.NoPower;
}

Diese Änderungen werden ordnungsgemäß kompiliert, auch wenn ExtraFancyLight Unterstützung für die
ILight -Schnittstelle und die beiden abgeleiteten Schnittstellen ITimerLight und IBlinkingLight deklariert. Es
gibt nur eine „nächste“ Implementierung, die in der ILight -Schnittstelle deklariert ist. Jede Klasse, die eine
Überschreibung deklariert hat, würde zur „nächsten“ Implementierung werden. Sie haben in den
vorhergehenden Klassen Beispiele gesehen, die die Member anderer abgeleiteter Schnittstellen überschreiben.
Vermeiden Sie das Überschreiben derselben Methode in mehreren abgeleiteten Schnittstellen. Auf diese Weise
wird ein mehrdeutiger Methodenaufruf erstellt, wenn eine Klasse beide abgeleiteten Schnittstellen
implementiert. Der Compiler kann keine einzelne bessere Methode auswählen, sodass er einen Fehler ausgibt.
Wenn z.B. sowohl IBlinkingLight als auch ITimerLight eine Überschreibung von PowerStatus implementiert
hat, müsste OverheadLight eine spezifischere Überschreibung bereitstellen. Andernfalls kann der Compiler nicht
zwischen den Implementierungen in den beiden abgeleiteten Schnittstellen wählen. Sie können diese Situation
in der Regel vermeiden, indem Sie Schnittstellendefinitionen klein halten und sich auf eine Funktion
konzentrieren. In diesem Szenario ist jede Funktion einer Leuchte eine eigene Schnittstelle. Mehrere
Schnittstellen werden nur von Klassen geerbt.
Dieses Beispiel zeigt ein Szenario, in dem Sie diskrete Features definieren können, die in Klassen gemischt
werden können. Sie deklarieren einen beliebigen Satz unterstützter Funktionen, indem Sie deklarieren, welche
Schnittstellen eine Klasse unterstützt. Durch die Verwendung von virtuellen Standardschnittstellenmethoden
können Klassen eine andere Implementierung für beliebige oder alle Schnittstellenmethoden verwenden oder
definieren. Diese Sprachfunktion bietet neue Möglichkeiten zum Modellieren der realen Systeme, die Sie
entwickeln. Standardschnittstellenmethoden bieten eine bessere Möglichkeit, verwandte Klassen auszudrücken,
die Mix-and-Match-Funktionen verwenden, indem sie virtuelle Implementierungen dieser Funktionen
verwenden.
Indizes und Bereiche
04.11.2021 • 6 minutes to read

Bereiche und Indizes bieten eine prägnante Syntax für den Zugriff auf einzelne Elemente oder Bereiche in einer
Sequenz.
In diesem Tutorial lernen Sie, wie die folgenden Aufgaben ausgeführt werden:
Verwenden Sie die Syntax für Bereiche in einer Sequenz.
Lernen Sie die Entwurfsentscheidungen für Start und Ende jeder Sequenz kennen.
Lernen Sie Szenarien für die Typen Index und Range kennen.

Sprachunterstützung für Indizes und Bereiche


Diese Sprachunterstützung basiert auf zwei neuen Typen und zwei neuen Operatoren:
System.Index: Stellt einen Index in einer Sequenz dar.
Der Index vom Endeoperator ^ , der angibt, dass ein Index relativ zum Ende einer Sequenz ist.
System.Range: Stellt einen Unterbereich einer Sequenz dar.
Der Bereichsoperator .. , der den Beginn und das Ende eines Bereichs als seine Operanden angibt.
Beginnen wir mit den Regeln für Indizes. Betrachten Sie einen Array sequence . Der 0 -Index entspricht
sequence[0] . Der ^0 -Index entspricht sequence[sequence.Length] . Der Ausdruck sequence[^0] löst eine
Ausnahme aus, genau wie sequence[sequence.Length] . Für eine beliebige Zahl n ist der Index ^n identisch mit
sequence[sequence.Length - n] .

string[] words = new string[]


{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5
"over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1
}; // 9 (or words.Length) ^0

Sie können das letzte Wort mit dem ^1 -Index abrufen. Fügen Sie unter der Initialisierung folgenden Code
hinzu:

Console.WriteLine($"The last word is {words[^1]}");

Ein Bereich gibt den Beginn und das Ende eines Bereichs an. Bereiche sind exklusiv, d. h. das Ende ist nicht im
Bereich enthalten. Der Bereich [0..^0] stellt ebenso wie [0..sequence.Length] den gesamten Bereich dar.
Der folgende Code erzeugt einen Teilbereich mit den Worten „quick“, „brown“ und „fox“. Er enthält words[1] bis
words[3] . Das Element words[4] befindet sich nicht im Bereich. Fügen Sie derselben Methode den folgenden
Code hinzu. Kopieren Sie ihn, und fügen Sie ihn unten in das interaktive Fenster ein.
string[] quickBrownFox = words[1..4];
foreach (var word in quickBrownFox)
Console.Write($"< {word} >");
Console.WriteLine();

Der folgende Code gibt den Bereich mit „lazy“ und „dog“ zurück. Dazu gehören words[^2] und words[^1] . Der
Endindex words[^0] ist nicht enthalten. Fügen Sie den folgenden Code auch hinzu:

string[] lazyDog = words[^2..^0];


foreach (var word in lazyDog)
Console.Write($"< {word} >");
Console.WriteLine();

Die folgenden Beispiele erstellen Bereiche, die am Anfang, am Ende und auf beiden Seiten offen sind:

string[] allWords = words[..]; // contains "The" through "dog".


string[] firstPhrase = words[..4]; // contains "The" through "fox"
string[] lastPhrase = words[6..]; // contains "the, "lazy" and "dog"
foreach (var word in allWords)
Console.Write($"< {word} >");
Console.WriteLine();
foreach (var word in firstPhrase)
Console.Write($"< {word} >");
Console.WriteLine();
foreach (var word in lastPhrase)
Console.Write($"< {word} >");
Console.WriteLine();

Sie können Bereiche oder Indizes auch als Variablen deklarieren. Die Variable kann dann innerhalb der Zeichen
[ und ] verwendet werden:

Index the = ^3;


Console.WriteLine(words[the]);
Range phrase = 1..4;
string[] text = words[phrase];
foreach (var word in text)
Console.Write($"< {word} >");
Console.WriteLine();

Das folgende Beispiel zeigt viele der Gründe für diese Auswahl. Ändern Sie x , y und z , um verschiedene
Kombinationen zu testen. Verwenden Sie beim Experimentieren Werte, wo x kleiner ist als y und y kleiner
als z für gültige Kombinationen. Fügen Sie den folgenden Code in einer neuen Methode hinzu. Probieren Sie
verschiedene Kombinationen aus:
int[] numbers = Enumerable.Range(0, 100).ToArray();
int x = 12;
int y = 25;
int z = 36;

Console.WriteLine($"{numbers[^x]} is the same as {numbers[numbers.Length - x]}");


Console.WriteLine($"{numbers[x..y].Length} is the same as {y - x}");

Console.WriteLine("numbers[x..y] and numbers[y..z] are consecutive and disjoint:");


Span<int> x_y = numbers[x..y];
Span<int> y_z = numbers[y..z];
Console.WriteLine($"\tnumbers[x..y] is {x_y[0]} through {x_y[^1]}, numbers[y..z] is {y_z[0]} through
{y_z[^1]}");

Console.WriteLine("numbers[x..^x] removes x elements at each end:");


Span<int> x_x = numbers[x..^x];
Console.WriteLine($"\tnumbers[x..^x] starts with {x_x[0]} and ends with {x_x[^1]}");

Console.WriteLine("numbers[..x] means numbers[0..x] and numbers[x..] means numbers[x..^0]");


Span<int> start_x = numbers[..x];
Span<int> zero_x = numbers[0..x];
Console.WriteLine($"\t{start_x[0]}..{start_x[^1]} is the same as {zero_x[0]}..{zero_x[^1]}");
Span<int> z_end = numbers[z..];
Span<int> z_zero = numbers[z..^0];
Console.WriteLine($"\t{z_end[0]}..{z_end[^1]} is the same as {z_zero[0]}..{z_zero[^1]}");

Typunterstützung für Indizes und Bereiche


Indizes und Bereiche stellen eine klare, präzise Syntax für den Zugriff auf ein einzelnes Element oder einen
Bereich von Elementen in einer Sequenz bereit. Ein Indexausdruck gibt in der Regel den Typ der Elemente einer
Sequenz zurück. Ein Bereichsausdruck gibt in der Regel den gleichen Sequenztyp wie die Quellsequenz zurück.
Jeder Typ, der einen Indexer mit einem Index- oder Range-Parameter bereitstellt, unterstützt explizit Indizes bzw.
Bereiche. Ein Indexer, der einen einzelnen Range-Parameter annimmt, kann einen anderen Sequenztyp
zurückgeben, z. B. System.Span<T>.

IMPORTANT
Die Codeleistung bei Verwendung eines Bereichsoperators hängt vom Typ des Operanden der Sequenz ab.
Die Zeitkomplexität des Bereichsoperators hängt vom Sequenztyp ab. Wenn die Sequenz beispielsweise string oder ein
Array ist, ist das Ergebnis eine Kopie des angegebenen Abschnitts der Eingabe, die Zeitkomplexität ist also O(N) . N steht
dabei für die Länge des Bereichs. Wenn es sich andernfalls um System.Span<T> oder System.Memory<T> handelt,
verweist das Ergebnis auf denselben Sicherungsspeicher, d. h. es gibt keine Kopie, und für den Vorgang gilt O(1) .
Zusätzlich zur Zeitkomplexität führt dies zu weiteren Belegungen und Kopien, was sich auf die Leistung auswirkt. Bei
leistungsabhängigem Code sollten Sie als Sequenztyp Span<T> oder Memory<T> verwenden, da der Bereichsoperator
keine Belegungen dafür vornimmt.

Ein Typ ist zählbar , wenn er über eine Eigenschaft mit dem Namen Length oder Count mit einem zugreifbaren
Getter und einem Rückgabetyp von int verfügt. Ein zählbarer Typ, der Indizes oder Bereiche nicht explizit
unterstützt, kann implizite Unterstützung dafür bieten. Weitere Informationen finden Sie in den Abschnitten
Implizite Indexunterstützung und Implizite Bereichsunterstützung der Featurevorschläge. Bereiche, die die
implizite Bereichsunterstützung verwenden, geben denselben Sequenztyp wie die Quellsequenz zurück.
Beispielsweise unterstützen die folgenden .NET-Typen Indizes und Bereiche: String, Span<T> und
ReadOnlySpan<T>. List<T> unterstützt Indizes, jedoch keine Bereiche.
Array zeigt ein differenzierteres Verhalten. Eindimensionale Arrays unterstützen sowohl Indizes als auch
Bereiche. Mehrdimensionale Arrays unterstützen keine Indexer oder Bereiche. Der Indexer für ein
mehrdimensionales Array verfügt über mehrere Parameter, nicht über einen einzelnen Parameter. Jagged Arrays,
auch als Array von Arrays bezeichnet, unterstützen sowohl Bereiche als auch Indexer. Das folgende Beispiel zeigt,
wie ein rechteckiger Unterabschnitt eines Jagged Arrays durchlaufen wird. Es durchläuft den Abschnitt in der
Mitte, wobei die ersten und letzten drei Zeilen sowie die ersten und letzten zwei Spalten jeder ausgewählten
Zeile ausgeschlossen werden:

var jagged = new int[10][]


{
new int[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
new int[10] { 10,11,12,13,14,15,16,17,18,19},
new int[10] { 20,21,22,23,24,25,26,27,28,29},
new int[10] { 30,31,32,33,34,35,36,37,38,39},
new int[10] { 40,41,42,43,44,45,46,47,48,49},
new int[10] { 50,51,52,53,54,55,56,57,58,59},
new int[10] { 60,61,62,63,64,65,66,67,68,69},
new int[10] { 70,71,72,73,74,75,76,77,78,79},
new int[10] { 80,81,82,83,84,85,86,87,88,89},
new int[10] { 90,91,92,93,94,95,96,97,98,99},
};

var selectedRows = jagged[3..^3];

foreach (var row in selectedRows)


{
var selectedColumns = row[2..^2];
foreach (var cell in selectedColumns)
{
Console.Write($"{cell}, ");
}
Console.WriteLine();
}

In allen Fällen ordnet der Bereichsoperator für Array ein Array zu, um die zurückgegebenen Elemente zu
speichern.

Szenarien für Indizes und Bereiche


Sie werden oft Bereiche und Indizes verwenden, wenn Sie einen Teil einer größeren Sequenz analysieren
möchten. Aus der neuen Syntax lässt sich klarer herauslesen, welcher Teil der Sequenz beteiligt ist. Die lokale
Funktion MovingAverage nimmt einen Range als Argument entgegen. Die Methode listet dann genau diesen
Bereich bei der Berechnung von Minimum, Maximum und Durchschnitt auf. Probieren Sie den folgenden Code
in Ihrem Projekt aus:
int[] sequence = Sequence(1000);

for(int start = 0; start < sequence.Length; start += 100)


{
Range r = start..(start+10);
var (min, max, average) = MovingAverage(sequence, r);
Console.WriteLine($"From {r.Start} to {r.End}: \tMin: {min},\tMax: {max},\tAverage: {average}");
}

for (int start = 0; start < sequence.Length; start += 100)


{
Range r = ^(start + 10)..^start;
var (min, max, average) = MovingAverage(sequence, r);
Console.WriteLine($"From {r.Start} to {r.End}: \tMin: {min},\tMax: {max},\tAverage: {average}");
}

(int min, int max, double average) MovingAverage(int[] subSequence, Range range) =>
(
subSequence[range].Min(),
subSequence[range].Max(),
subSequence[range].Average()
);

int[] Sequence(int count) =>


Enumerable.Range(0, count).Select(x => (int)(Math.Sqrt(x) * 100)).ToArray();
Tutorial: Besseres Ausdrücken Ihrer Entwurfsabsicht
mit Verweistypen, die NULL-Werte zulassen und
nicht zulassen
04.11.2021 • 10 minutes to read

C# 8.0 führt Nullable-Verweistypen ein, die Verweistypen auf die gleiche Weise ergänzen, wie Nullable-
Werttypen Werttypen ergänzen. Sie deklarieren eine Variable zu einem Ver weistyp, der NULL-Wer te
zulässt , indem Sie ? an den Typen anfügen. Beispielsweise stellt string? eine string dar, die NULL-Werte
zulässt. Mit diesen neuen Typen können Sie Ihre Entwurfsabsicht besser zum Ausdruck bringen: Einige Variablen
müssen immer einen Wert haben, bei anderen kann ein Wert fehlen.
In diesem Tutorial lernen Sie, wie die folgenden Aufgaben ausgeführt werden:
Integrieren von Verweistypen, die NULL-Werte zulassen und nicht zulassen, in Ihre Entwürfe.
Aktivieren von Überprüfungen Ihres Verweistypen, der NULL-Werte zulässt, anhand Ihres Codes.
Schreiben von Code, bei dem ein Compiler diese Entwurfsentscheidungen erzwingt.
Verwenden des Verweisfeatures, das NULL-Werte zulässt, in Ihren eigenen Entwürfen.

Voraussetzungen
Sie müssen Ihren Computer zur Ausführung von .NET Core einrichten, einschließlich des C# 8.0-Compilers. Der
C# 8.0-Compiler ist mit Visual Studio 2019 oder .NET Core 3.0 verfügbar.
In diesem Tutorial wird vorausgesetzt, dass Sie C# und .NET, einschließlich Visual Studio oder die .NET Core-CLI
kennen.

Integrieren von Verweistypen, die NULL-Werte zulassen, in Ihre


Entwürfe
In diesem Tutorial erstellen Sie eine Bibliothek, die die Ausführung einer Umfrage modelliert. Der Code
verwendet Verweistypen, die NULL-Werte zulassen bzw. nicht zulassen, zur Darstellung der realen Konzepte. Die
Fragen in der Umfrage können nie NULL sein. Ein Befragter möchte eine Frage möglicherweise nicht
beantworten. In diesem Fall können die Antworten null sein.
Der Code, den Sie für dieses Beispiel schreiben, drückt diese Absicht aus, und der Compiler setzt sie durch.

Erstellen der Anwendung und Aktivieren der Verweistypen, die NULL-


Werte zulassen
Erstellen Sie eine neue Konsolenanwendung in Visual Studio oder über die Befehlszeile mit dotnet new console .
Nennen Sie die Anwendung NullableIntroduction . Nachdem Sie die Anwendung erstellt haben, müssen Sie
angeben, dass das gesamte Projekt in einem aktivierten Nullable-Anmerkungskontext kompiliert wird.
Öffnen Sie die CSPROJ-Datei, und fügen Sie dem PropertyGroup -Element ein Nullable -Element hinzu. Legen
Sie den Wert der Eigenschaft auf enable fest. Selbst in C# 8.0-Projekten müssen Sie das Feature für Nullable-
Ver weistypen aktivieren. Grund dafür ist, dass bestehende Verweisvariablendeklarationen nach dem
Aktivieren des Features zu Ver weistypen werden, die NULL-Wer te nicht zulassen . Diese Entscheidung ist
zwar hilfreich, um Probleme zu finden, bei denen bestehender Code möglicherweise keine ordnungsgemäßen
NULL-Überprüfungen aufweist, aber möglicherweise spiegelt sie nicht genau Ihre ursprüngliche
Entwurfsabsicht wider.

<Nullable>enable</Nullable>

Vor .NET 6 ist das -Element in neuen Projekten nicht enthalten. Ab .NET 6 enthalten neue Projekte das
Nullable
<Nullable>enable</Nullable> -Element in der Projektdatei.

Entwerfen der Typen für die Anwendung


Für diese Umfrageanwendung müssen einige Klassen erstellt werden:
Eine Klasse, die die Liste der Fragen modelliert.
Eine Klasse, die eine Liste der Personen modelliert, die für die Umfrage kontaktiert werden.
Eine Klasse, die die Antworten einer Person modelliert, die an der Umfrage teilgenommen hat.
Diese Typen verwenden Verweistypen, die NULL-Werte sowohl zulassen als auch nicht zulassen, um
auszudrücken, welche Member erforderlich und welche optional sind. Verweistypen, die NULL-Werte zulassen,
kommunizieren diese Entwurfsabsicht eindeutig:
Die Fragen, die Teil der Umfrage sind, können nie NULL sein. Es ist nicht sinnvoll, eine leere Frage zu stellen.
Die Befragten können nie NULL sein. Sie werden sicher die Personen nachverfolgen wollen, mit denen Sie
Kontakt aufgenommen haben, sogar die Personen, die die Teilnahme abgelehnt haben.
Jede Antwort auf eine Frage darf NULL sein. Befragten können es ablehnen, einige oder alle Fragen zu
beantworten.
Wenn Sie in C# programmiert haben, sind Sie vielleicht so sehr an Verweistypen gewöhnt, die null -Werte
zulassen, dass Sie andere Möglichkeiten verpasst haben, Instanzen zu deklarieren, die NULL-Werte nicht
zulassen:
Die Auflistung der Fragen sollte keine NULL-Werte zulassen.
Die Auflistung der Antworten sollte keine NULL-Werte zulassen.
Wenn Sie den Code schreiben, werden Sie Folgendes feststellen: Indem Sie für Verweise standardmäßig einen
Verweistyp verwenden, der keine NULL-Werte zulässt, lassen sich häufige Fehler vermeiden, die zu
NullReferenceException-Ergebnissen führen können. Eine Lektion aus diesem Tutorial ist, dass Sie entschieden
haben, welche Variablen null sein könnten oder nicht. Die Sprache bot keine Syntax, um diese Entscheidungen
auszudrücken. Jetzt ist es möglich.
Die von Ihnen erstellte App führt die folgenden Schritte aus:
1. Erstellen einer Umfrage und Hinzufügen von Fragen.
2. Erstellen eines pseudozufälligen Satzes von Befragten für die Umfrage.
3. Kontaktieren der Befragten, bis die Anzahl der abgeschlossen Umfragen den Zielwert erreicht hat.
4. Erstellen wichtiger Statistiken zu den Antworten.

Erstellen der Umfrage mit Nullable- und Nicht-Nullable-Verweistypen


Mit dem ersten geschriebenen Code erstellen Sie die Umfrage. Sie schreiben die Klassen, um eine Frage der
Umfrage und eine Ausführung zu modellieren. Ihre Umfrage umfasst drei Arten von Fragen, die sich durch das
Format der Antwort unterscheiden: Ja/Nein-Antworten, Zahlenantworten und Textantworten. Erstellen Sie eine
public SurveyQuestion -Klasse:
namespace NullableIntroduction
{
public class SurveyQuestion
{
}
}

Der Compiler interpretiert jede Verweistyp-Variablendeklaration für Code in einem aktivierten Nullable-
Anmerkungskontext als Nicht-Nullable-Ver weistyp . Sie können Ihre erste Warnung sehen, indem Sie
Eigenschaften für den Fragetext und die Art der Frage hinzufügen, wie im folgenden Code gezeigt:

namespace NullableIntroduction
{
public enum QuestionType
{
YesNo,
Number,
Text
}

public class SurveyQuestion


{
public string QuestionText { get; }
public QuestionType TypeOfQuestion { get; }
}
}

Da Sie QuestionText noch nicht initialisiert haben, gibt der Compiler eine Warnung aus, dass noch keine
Eigenschaft initialisiert wurde, nicht keine NULL-Werte zulässt. Ihr Entwurf verlangt, dass der Fragetext nicht null
ist. Also fügen Sie einen Konstruktor zur Initialisierung und den Wert QuestionType hinzu. Die fertige
Klassendefinition sieht wie im folgenden Code aus:

namespace NullableIntroduction
{
public enum QuestionType
{
YesNo,
Number,
Text
}

public class SurveyQuestion


{
public string QuestionText { get; }
public QuestionType TypeOfQuestion { get; }

public SurveyQuestion(QuestionType typeOfQuestion, string text) =>


(TypeOfQuestion, QuestionText) = (typeOfQuestion, text);
}
}

Durch das Hinzufügen des Konstruktors wird die Warnung entfernt. Das Konstruktorargument ebenfalls ein
Verweistyp, der keine NULL-Werte zulässt. Der Compiler gibt also keine Warnungen aus.
Erstellen Sie eine public -Klasse mit dem Namen SurveyRun . Diese Klasse enthält eine Liste von
SurveyQuestion -Objekten und Methoden, um Fragen zur Umfrage hinzuzufügen, wie im folgenden Code
gezeigt:
using System.Collections.Generic;

namespace NullableIntroduction
{
public class SurveyRun
{
private List<SurveyQuestion> surveyQuestions = new List<SurveyQuestion>();

public void AddQuestion(QuestionType type, string question) =>


AddQuestion(new SurveyQuestion(type, question));
public void AddQuestion(SurveyQuestion surveyQuestion) => surveyQuestions.Add(surveyQuestion);
}
}

Wie bisher müssen Sie das Listenobjekt Wert initialisieren, der keine NULL-Werte zulässt, oder der Compiler
gibt eine Warnung aus. Es gibt keine NULL-Überprüfungen in der zweiten Überladung von AddQuestion , da sie
nicht benötigt werden: Sie haben diese Variable als Typ deklariert, der keine NULL-Werte zulässt. Der Wert kann
nicht null sein.
Wechseln Sie in Ihrem Editor zu Program.cs, und ersetzen Sie den Inhalt von Main durch die folgenden
Codezeilen:

var surveyRun = new SurveyRun();


surveyRun.AddQuestion(QuestionType.YesNo, "Has your code ever thrown a NullReferenceException?");
surveyRun.AddQuestion(new SurveyQuestion(QuestionType.Number, "How many times (to the nearest 100) has that
happened?"));
surveyRun.AddQuestion(QuestionType.Text, "What is your favorite color?");

Da sich das gesamte Projekt in einem aktivierten Nullable-Anmerkungskontext befindet, erhalten Sie
Warnungen, wenn Sie null in Erwartung eines Nicht-Nullable-Verweistyps an eine Methode übergeben.
Probieren Sie es aus, indem Sie die folgende Zeile zu Main hinzufügen:

surveyRun.AddQuestion(QuestionType.Text, default);

Erstellen von Befragten und Erhalten von Antworten zur Umfrage


Schreiben Sie als Nächstes den Code, der Antworten für die Umfrage generiert. Dieser Prozess umfasst mehrere
kleine Aufgaben:
1. Erstellen Sie eine Methode, die Antwortobjekte generiert. Diese repräsentieren die Personen, die um das
Ausfüllen der Umfrage gebeten werden.
2. Erstellen Sie eine Logik, um zu simulieren, dass Sie die Fragen an einen Befragten stellen und Antworten
sammeln, oder feststellen, dass ein Befragter nicht geantwortet hat.
3. Wiederholen Sie den Vorgang, bis genügend Befragten an der Umfrage teilgenommen haben.
Sie benötigen eine Klasse zum Darstellen einer Umfrageantwort. Fügen Sie diese jetzt hinzu. Aktivieren Sie
Support für NULL-Werte. Fügen Sie eine Id -Eigenschaft und einen Konstruktor hinzu, der diese initialisiert, wie
in folgendem Code dargestellt:
namespace NullableIntroduction
{
public class SurveyResponse
{
public int Id { get; }

public SurveyResponse(int id) => Id = id;


}
}

Fügen Sie als Nächstes eine static -Methode hinzu, um neuen Teilnehmer zu erstellen, indem Sie eine Zufalls-
ID generieren:

private static readonly Random randomGenerator = new Random();


public static SurveyResponse GetRandomId() => new SurveyResponse(randomGenerator.Next());

Die Hauptaufgabe dieser Klasse besteht darin, die Antworten für einen Teilnehmer auf die Fragen der Umfrage
zu generieren. Diese Aufgabe umfasst einige Schritte:
1. Bitten Sie um die Teilnahme an der Umfrage. Wenn eine Person nicht einverstanden ist, geben Sie eine
fehlende (oder Null) Antwort zurück.
2. Stellen Sie die einzelnen Fragen, und notieren Sie die Antwort. Jede Antwort kann auch nicht vorhanden
(oder null) sein.
Fügen Sie der SurveyResponse -Klasse den folgenden Code hinzu:
private Dictionary<int, string>? surveyResponses;
public bool AnswerSurvey(IEnumerable<SurveyQuestion> questions)
{
if (ConsentToSurvey())
{
surveyResponses = new Dictionary<int, string>();
int index = 0;
foreach (var question in questions)
{
var answer = GenerateAnswer(question);
if (answer != null)
{
surveyResponses.Add(index, answer);
}
index++;
}
}
return surveyResponses != null;
}

private bool ConsentToSurvey() => randomGenerator.Next(0, 2) == 1;

private string? GenerateAnswer(SurveyQuestion question)


{
switch (question.TypeOfQuestion)
{
case QuestionType.YesNo:
int n = randomGenerator.Next(-1, 2);
return (n == -1) ? default : (n == 0) ? "No" : "Yes";
case QuestionType.Number:
n = randomGenerator.Next(-30, 101);
return (n < 0) ? default : n.ToString();
case QuestionType.Text:
default:
switch (randomGenerator.Next(0, 5))
{
case 0:
return default;
case 1:
return "Red";
case 2:
return "Green";
case 3:
return "Blue";
}
return "Red. No, Green. Wait.. Blue... AAARGGGGGHHH!";
}
}

Der Speicher für Umfrageantworten ist eine Dictionary<int, string>? . Damit wird angegeben, dass auch NULL
zulässig ist. Sie verwenden das neue Sprachfeature, um Ihre Entwurfsabsicht zu deklarieren, sowohl gegenüber
dem Compiler als auch gegenüber allen, die Ihren Code später lesen. Wenn Sie jemals surveyResponses
dereferenzieren, ohne zuerst nach dem null -Wert zu suchen, erhalten Sie eine Compilerwarnung. Sie erhalten
keine Warnung in der AnswerSurvey -Methode, da der Compiler bestimmen kann, dass die Variable
surveyResponses oben auf einen Wert gesetzt wurde, der NULL-Werte zulässt.

Die Verwendung von null für fehlende Antworten hebt einen wichtigen Punkt für die Arbeit mit NULL-Werte
zulassenden Verweistypen hervor: Ihr Ziel ist nicht, alle null -Werte aus Ihrem Programm zu entfernen. Ihr Ziel
ist eher, sicherzustellen, dass der Code, den Sie schreiben, Ihre Entwurfsabsicht ausdrückt. Fehlende Werte sind
ein notwendiges in Ihrem Code auszudrückendes Konzept. Der null -Wert ist eine klare Möglichkeit, diese
fehlenden Werte auszudrücken. Der Versuch, alle null -Werte zu entfernen, führt nur zum Definieren einer
anderen Möglichkeit, diese fehlenden Werte ohne null auszudrücken.
Als Nächstes müssen Sie die PerformSurvey -Methode in der SurveyRun -Klasse schreiben. Fügen Sie den
folgenden Code der SurveyRun -Klasse hinzu:

private List<SurveyResponse>? respondents;


public void PerformSurvey(int numberOfRespondents)
{
int respondentsConsenting = 0;
respondents = new List<SurveyResponse>();
while (respondentsConsenting < numberOfRespondents)
{
var respondent = SurveyResponse.GetRandomId();
if (respondent.AnswerSurvey(surveyQuestions))
respondentsConsenting++;
respondents.Add(respondent);
}
}

Auch hier geben Sie durch Ihre Auswahl von List<SurveyResponse>? , die NULL-Werte zulässt, dass die Antwort
Null sein kann. Damit wird angegeben, dass die Umfrage noch keinem Befragten zugewiesen wurde. Beachten
Sie, dass Personen hinzugefügt werden, bis genügend zugestimmt haben.
Der letzte Schritt zum Ausführen der Umfrage besteht darin, einen Aufruf zur Durchführung der Umfrage am
Ende der Main -Methode hinzuzufügen:

surveyRun.PerformSurvey(50);

Untersuchen von Umfrageantworten


Der letzte Schritt ist das Anzeigen von Umfrageergebnissen. Sie fügen Code zu vielen der Klassen hinzu, die Sie
geschrieben haben. Dieser Code demonstriert den Wert der Unterscheidung von Verweistypen, die NULL-Werte
zulassen bzw. nicht zulassen. Beginnen Sie, indem Sie die zwei folgenden Ausdruckskörpermember zur
SurveyResponse -Klasse hinzufügen:

public bool AnsweredSurvey => surveyResponses != null;


public string Answer(int index) => surveyResponses?.GetValueOrDefault(index) ?? "No answer";

Da surveyResponses ein Verweistyp ist, der NULL-Werte zulässt, sind vor dem Dereferenzieren NULL-
Überprüfungen erforderlich. Die Answer -Methode gibt eine Zeichenfolge zurück, die keine NULL-Werte zulässt.
Daher müssen wir durch die Verwendung des NULL-Sammeloperators den Fall abdecken, dass eine Antwort
fehlt.
Fügen Sie diese drei Ausdruckskörpermember zur SurveyRun -Klasse hinzu:

public IEnumerable<SurveyResponse> AllParticipants => (respondents ?? Enumerable.Empty<SurveyResponse>());


public ICollection<SurveyQuestion> Questions => surveyQuestions;
public SurveyQuestion GetQuestion(int index) => surveyQuestions[index];

Das AllParticipants Member muss berücksichtigen, dass die respondents -Variable Null sein kann, der
zurückgegebene Wert aber nicht Null sein darf. Wenn Sie diesen Ausdruck ändern, indem Sie ?? und die
folgende leere Sequenz entfernen, warnt Sie der Compiler, dass die Methode null zurückgeben könnte, und
ihre Rückgabesignatur gibt einen Typen zurück, der keine NULL-Werte zulässt.
Fügen Sie abschließend die folgende Schleife am Ende der Main -Methode hinzu:
foreach (var participant in surveyRun.AllParticipants)
{
Console.WriteLine($"Participant: {participant.Id}:");
if (participant.AnsweredSurvey)
{
for (int i = 0; i < surveyRun.Questions.Count; i++)
{
var answer = participant.Answer(i);
Console.WriteLine($"\t{surveyRun.GetQuestion(i).QuestionText} : {answer}");
}
}
else
{
Console.WriteLine("\tNo responses");
}
}

Sie benötigen keine null -Überprüfungen in diesem Code, das Sie die darunter liegenden Schnittstellen so
entworfen haben, dass sie alle einen Verweistypen zurückgeben, der keine NULL-Werte zulässt.

Abrufen des Codes


Sie können den Code für das abgeschlossene Tutorial aus unserem samples-Repository im Ordner
csharp/NullableIntroduction abrufen.
Experimentieren Sie, indem Sie bei der Typdeklaration zwischen Verweistypen wechseln, die NULL-Werte
zulassen bzw. nicht zulassen. Sehen Sie sich an, wie dabei verschiedene Warnungen erzeugt werden, um
sicherzustellen, dass Sie nicht versehentlich null dereferenzieren.

Nächste Schritte
Hier erfahren Sie, wie Sie Nullable-Verweistypen bei Verwendung von Entity Framework nutzen:

Grundlagen von Entity Framework Core: Verwenden von Nullable-Verweistypen


Tutorial: Generieren und Nutzen asynchroner
Datenströme mit C# 8.0 und .NET Core 3.0
04.11.2021 • 9 minutes to read

C# 8.0 führt asynchrone Streams ein, die eine Streamingdatenquelle modellieren. In Datenströmen werden
Elemente häufig asynchron abgerufen oder generiert. Asynchrone Streams basieren auf neuen Schnittstellen,
die in .NET Standard 2.1 eingeführt wurden. Diese Schnittstellen werden in .NET Core 3.0 und höher unterstützt.
Sie stellen ein intuitives Programmiermodell für asynchrone Streamingdatenquellen bereit.
In diesem Tutorial lernen Sie, wie die folgenden Aufgaben ausgeführt werden:
Erstellen einer Datenquelle, die eine Sequenz von Datenelementen asynchron generiert
Asynchrones Nutzen dieser Datenquelle
Unterstützung für Abbruchvorgänge und erfasste Kontexte für asynchrone Streams
Erkennen, wenn die neue Schnittstelle und Datenquelle früheren synchronen Datensequenzen vorgezogen
werden

Voraussetzungen
Sie müssen Ihren Computer zur Ausführung von .NET Core einrichten, einschließlich des C# 8.0-Compilers. Der
C# 8-Compiler steht ab Visual Studio 2019 Version 16.3 oder mit dem .NET Core 3.0 SDK zur Verfügung.
Sie müssen ein GitHub-Zugriffstoken erstellen, damit Sie auf den GitHub GraphQL-Endpunkt zugreifen können.
Wählen Sie die folgenden Berechtigungen für Ihr GitHub-Zugriffstoken aus:
repo:status
public_repo
Speichern Sie das Zugriffstoken an einem sicheren Ort, damit Sie es für den Zugriff auf den GitHub-API-
Endpunkt verwenden können.

WARNING
Schützen Sie Ihr persönliches Zugriffstoken. Jede Software mit Ihrem persönlichen Zugriffstoken kann mit Ihren
Zugriffsrechten GitHub-API-Aufrufe ausführen.

In diesem Tutorial wird vorausgesetzt, dass Sie C# und .NET, einschließlich Visual Studio oder die .NET Core-CLI
kennen.

Ausführen der Startanwendung


Sie können den Code für die in diesem Tutorial verwendete Startanwendung aus unserem Repository
dotnet/docs im Ordner csharp/whats-new/tutorials abrufen.
Die Startanwendung ist eine Konsolenanwendung, die die GitHub GraphQL-Schnittstelle zum Abrufen aktueller
Issues verwendet, die in das Repository dotnet/docs geschrieben wurden. Sehen Sie sich zunächst folgenden
Code für die Main -Methode der Starter-App an:
static async Task Main(string[] args)
{
//Follow these steps to create a GitHub Access Token
// https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/#creating-a-
token
//Select the following permissions for your GitHub Access Token:
// - repo:status
// - public_repo
// Replace the 3rd parameter to the following code with your GitHub access token.
var key = GetEnvVariable("GitHubKey",
"You must store your GitHub key in the 'GitHubKey' environment variable",
"");

var client = new GitHubClient(new Octokit.ProductHeaderValue("IssueQueryDemo"))


{
Credentials = new Octokit.Credentials(key)
};

var progressReporter = new progressStatus((num) =>


{
Console.WriteLine($"Received {num} issues in total");
});
CancellationTokenSource cancellationSource = new CancellationTokenSource();

try
{
var results = await runPagedQueryAsync(client, PagedIssueQuery, "docs",
cancellationSource.Token, progressReporter);
foreach(var issue in results)
Console.WriteLine(issue);
}
catch (OperationCanceledException)
{
Console.WriteLine("Work has been cancelled");
}
}

Sie können entweder eine GitHubKey -Umgebungsvariable auf Ihr persönliches Zugriffstoken festlegen, oder Sie
können das letzte Argument im Aufruf von GetEnvVariable durch Ihr persönliches Zugriffstoken ersetzen.
Fügen Sie Ihren Zugriffscode nicht in den Quellcode ein, wenn Sie die Quelle für andere freigeben. Laden Sie
Zugriffscodes niemals in ein freigegebenes Quellrepository hoch.
Nach dem Erstellen des GitHub-Clients werden durch den Code in Main ein Objekt für Fortschrittsberichte und
ein Abbruchtoken erstellt. Nachdem die Objekte erstellt wurden, wird runPagedQueryAsync durch Main
aufgerufen, um die neuesten 250 Issues abzurufen. Nach Abschluss dieser Aufgabe werden die Ergebnisse
angezeigt.
Bei Ausführung der Startanwendung können Sie einige wichtige Details zur Ausführung dieser Anwendung
beobachten. Für jede von GitHub zurückgegebene Seite wird der Fortschritt gemeldet. Sie können eine deutliche
Pause beobachten, bevor GitHub eine weitere neue Seite mit Issues zurückgibt. Die Issues werden erst angezeigt,
nachdem alle zehn Seiten aus GitHub abgerufen wurden.

Untersuchen der Implementierung


Die Implementierung zeigt, warum Sie das im vorherigen Abschnitt beschriebene Verhalten beobachten
konnten. Untersuchen Sie den Code für runPagedQueryAsync :
private static async Task<JArray> runPagedQueryAsync(GitHubClient client, string queryText, string repoName,
CancellationToken cancel, IProgress<int> progress)
{
var issueAndPRQuery = new GraphQLRequest
{
Query = queryText
};
issueAndPRQuery.Variables["repo_name"] = repoName;

JArray finalResults = new JArray();


bool hasMorePages = true;
int pagesReturned = 0;
int issuesReturned = 0;

// Stop with 10 pages, because these are large repos:


while (hasMorePages && (pagesReturned++ < 10))
{
var postBody = issueAndPRQuery.ToJsonText();
var response = await client.Connection.Post<string>(new Uri("https://api.github.com/graphql"),
postBody, "application/json", "application/json");

JObject results = JObject.Parse(response.HttpResponse.Body.ToString());

int totalCount = (int)issues(results)["totalCount"];


hasMorePages = (bool)pageInfo(results)["hasPreviousPage"];
issueAndPRQuery.Variables["start_cursor"] = pageInfo(results)["startCursor"].ToString();
issuesReturned += issues(results)["nodes"].Count();
finalResults.Merge(issues(results)["nodes"]);
progress?.Report(issuesReturned);
cancel.ThrowIfCancellationRequested();
}
return finalResults;

JObject issues(JObject result) => (JObject)result["data"]["repository"]["issues"];


JObject pageInfo(JObject result) => (JObject)issues(result)["pageInfo"];
}

Konzentrieren wir uns auf den Paginierungsalgorithmus und die asynchrone Struktur des obigen Codes. (Details
zur GitHub GraphQL-API finden Sie in der GitHub GraphQL-Dokumentation.) Die runPagedQueryAsync -Methode
listet die Issues vom neuesten zum ältesten auf. Sie fordert 25 Issues pro Seite an und untersucht die pageInfo -
Struktur der Antwort, um mit der vorherigen Seite fortzufahren. Dies entspricht der GraphQL-
Standardpaginierungsunterstützung für mehrseitige Antworten. Die Antwort enthält ein pageInfo -Objekt mit
einem hasPreviousPages -Wert und einem startCursor -Wert zum Anfordern der vorherigen Seite. Die Issues
befinden sich im nodes -Array. Die runPagedQueryAsync -Methode fügt diese Knoten einem Array an, das alle
Ergebnisse aus allen Seiten enthält.
Nach dem Abrufen und Wiederherstellen einer Seite mit Ergebnissen meldet runPagedQueryAsync den
Fortschritt und prüft auf Abbruch. Wenn ein Abbruch angefordert wurde, löst runPagedQueryAsync eine
OperationCanceledException aus.
Es gibt mehrere Elemente in diesem Code, die verbessert werden können. Vor allem muss runPagedQueryAsync
Speicherplatz für alle zurückgegebenen Issues zuordnen. In diesem Beispiel wird der Vorgang bei 250 Issues
beendet, weil das Abrufen aller offenen Issues wesentlich mehr Arbeitsspeicher zum Speichern aller
abgerufenen Issues erfordern würde. Der Algorithmus ist durch die Protokolle zur Unterstützung von
Fortschrittsberichten und Abbruchvorgängen beim ersten Lesen schwieriger zu verstehen. Es sind mehr Typen
und APIs beteiligt. Außerdem müssen Sie die Kommunikation über CancellationTokenSource und die zugehörige
CancellationToken-Struktur verfolgen, um nachzuvollziehen, wo der Abbruch angefordert und wo er gewährt
wird.

Bessere Möglichkeiten durch asynchrone Datenströme


Mit asynchronen Datenströmen und der zugehörigen Sprachunterstützung lassen sich diese Probleme beheben.
Der Code, der die Sequenz generiert, kann mit yield return jetzt Elemente in einer Methode zurückgeben, die
mit dem async -Modifizierer deklariert wurde. Sie können einen asynchronen Datenstrom mit einer
await foreach -Schleife genau so nutzen, wie Sie eine beliebige Sequenz mit einer foreach -Schleife einsetzen.

Diese neuen Sprachfeatures hängen von drei neuen Schnittstellen ab, die dem .NET Standard 2.1 hinzugefügt
und in .NET Core 3.0 implementiert wurden:
System.Collections.Generic.IAsyncEnumerable<T>
System.Collections.Generic.IAsyncEnumerator<T>
System.IAsyncDisposable
Diese drei Schnittstellen sollten den meisten C#-Entwicklern vertraut sein. Sie verhalten sich ähnlich wie ihre
synchronen Gegenstücke:
System.Collections.Generic.IEnumerable<T>
System.Collections.Generic.IEnumerator<T>
System.IDisposable
Ein möglicherweise weniger bekannter Typ ist System.Threading.Tasks.ValueTask. Die ValueTask -Struktur bietet
eine ähnliche API für die System.Threading.Tasks.Task-Klasse. ValueTask wird in diesen Schnittstellen aus
Leistungsgründen verwendet.

Konvertieren in asynchrone Datenströme


Als Nächstes konvertieren Sie die runPagedQueryAsync -Methode, um einen asynchronen Datenstrom zu
generieren. Ändern Sie zunächst die Signatur von runPagedQueryAsync so, dass ein IAsyncEnumerable<JToken>
zurückgegeben wird, und entfernen Sie das Abbruchtoken und die Fortschrittsobjekte aus der Parameterliste,
wie im folgenden Code gezeigt:

private static async IAsyncEnumerable<JToken> runPagedQueryAsync(GitHubClient client,


string queryText, string repoName)

Der Startcode verarbeitet die einzelnen Seiten, während sie abgerufen werden, wie im folgenden Code gezeigt:

finalResults.Merge(issues(results)["nodes"]);
progress?.Report(issuesReturned);
cancel.ThrowIfCancellationRequested();

Ersetzen Sie diese drei Zeilen durch den folgenden Code:

foreach (JObject issue in issues(results)["nodes"])


yield return issue;

Sie können auch die Deklaration von finalResults weiter oben in dieser Methode sowie die return -
Anweisung entfernen, die der von Ihnen geänderten Schleife folgt.
Sie haben die Änderungen zum Generieren eines asynchronen Datenstroms abgeschlossen. Die fertige Methode
sollte in etwa dem folgenden Code entsprechen:
private static async IAsyncEnumerable<JToken> runPagedQueryAsync(GitHubClient client,
string queryText, string repoName)
{
var issueAndPRQuery = new GraphQLRequest
{
Query = queryText
};
issueAndPRQuery.Variables["repo_name"] = repoName;

bool hasMorePages = true;


int pagesReturned = 0;
int issuesReturned = 0;

// Stop with 10 pages, because these are large repos:


while (hasMorePages && (pagesReturned++ < 10))
{
var postBody = issueAndPRQuery.ToJsonText();
var response = await client.Connection.Post<string>(new Uri("https://api.github.com/graphql"),
postBody, "application/json", "application/json");

JObject results = JObject.Parse(response.HttpResponse.Body.ToString());

int totalCount = (int)issues(results)["totalCount"];


hasMorePages = (bool)pageInfo(results)["hasPreviousPage"];
issueAndPRQuery.Variables["start_cursor"] = pageInfo(results)["startCursor"].ToString();
issuesReturned += issues(results)["nodes"].Count();

foreach (JObject issue in issues(results)["nodes"])


yield return issue;
}

JObject issues(JObject result) => (JObject)result["data"]["repository"]["issues"];


JObject pageInfo(JObject result) => (JObject)issues(result)["pageInfo"];
}

Als Nächstes ändern Sie den Code, der die Sammlung nutzt, um den asynchronen Datenstrom zu verwenden.
Suchen Sie in Main den folgenden Code, der die Sammlung der Issues verarbeitet:

var progressReporter = new progressStatus((num) =>


{
Console.WriteLine($"Received {num} issues in total");
});
CancellationTokenSource cancellationSource = new CancellationTokenSource();

try
{
var results = await runPagedQueryAsync(client, PagedIssueQuery, "docs",
cancellationSource.Token, progressReporter);
foreach(var issue in results)
Console.WriteLine(issue);
}
catch (OperationCanceledException)
{
Console.WriteLine("Work has been cancelled");
}

Ersetzen Sie den Code durch die folgende await foreach -Schleife:
int num = 0;
await foreach (var issue in runPagedQueryAsync(client, PagedIssueQuery, "docs"))
{
Console.WriteLine(issue);
Console.WriteLine($"Received {++num} issues in total");
}

Die neue Schnittstelle IAsyncEnumerator<T> leitet von IAsyncDisposable ab. Das bedeutet, dass die
vorhergehende Schleife den Stream asynchron löscht, wenn die Schleife beendet wird. Die Schleife ähnelt dem
folgenden Code:

int num = 0;
var enumerator = runPagedQueryAsync(client, PagedIssueQuery, "docs").GetEnumeratorAsync();
try
{
while (await enumerator.MoveNextAsync())
{
var issue = enumerator.Current;
Console.WriteLine(issue);
Console.WriteLine($"Received {++num} issues in total");
}
} finally
{
if (enumerator != null)
await enumerator.DisposeAsync();
}

Standardmäßig werden Streamelemente im erfassten Kontext verarbeitet. Wenn Sie die Erfassung des Kontexts
deaktivieren möchten, verwenden Sie die Erweiterungsmethode
TaskAsyncEnumerableExtensions.ConfigureAwait. Weitere Informationen über Synchronisierungskontexte und
die Erfassung des aktuellen Kontexts finden Sie im Artikel über das Verwenden des aufgabenbasierten
asynchronen Musters.
Asynchrone Streams unterstützen Abbruchvorgänge mithilfe desselben Protokolls wie andere async -
Methoden. Ändern Sie die Signatur für die asynchrone Iteratormethode folgendermaßen, damit
Abbruchvorgänge unterstützt werden:
private static async IAsyncEnumerable<JToken> runPagedQueryAsync(GitHubClient client,
string queryText, string repoName, [EnumeratorCancellation] CancellationToken cancellationToken =
default)
{
var issueAndPRQuery = new GraphQLRequest
{
Query = queryText
};
issueAndPRQuery.Variables["repo_name"] = repoName;

bool hasMorePages = true;


int pagesReturned = 0;
int issuesReturned = 0;

// Stop with 10 pages, because these are large repos:


while (hasMorePages && (pagesReturned++ < 10))
{
var postBody = issueAndPRQuery.ToJsonText();
var response = await client.Connection.Post<string>(new Uri("https://api.github.com/graphql"),
postBody, "application/json", "application/json");

JObject results = JObject.Parse(response.HttpResponse.Body.ToString());

int totalCount = (int)issues(results)["totalCount"];


hasMorePages = (bool)pageInfo(results)["hasPreviousPage"];
issueAndPRQuery.Variables["start_cursor"] = pageInfo(results)["startCursor"].ToString();
issuesReturned += issues(results)["nodes"].Count();

foreach (JObject issue in issues(results)["nodes"])


yield return issue;
}

JObject issues(JObject result) => (JObject)result["data"]["repository"]["issues"];


JObject pageInfo(JObject result) => (JObject)issues(result)["pageInfo"];
}

Das EnumeratorCancellationAttribute-Attribut bewirkt, dass der Compiler Code für IAsyncEnumerator<T>


generiert, der dazu führt, dass das an GetAsyncEnumerator übergebene Token für den Text des asynchronen
Iterators als dieses Argument sichtbar ist. In runQueryAsync können Sie den Status des Tokens überprüfen und
sich ggf. weitere Arbeit sparen.
Eine andere Erweiterungsmethode (WithCancellation) wird verwendet, um das Abbruchtoken an den
asynchronen Stream zu übergeben. Ändern Sie die Schleife, die die Issues enumeriert, folgendermaßen:

private static async Task EnumerateWithCancellation(GitHubClient client)


{
int num = 0;
var cancellation = new CancellationTokenSource();
await foreach (var issue in runPagedQueryAsync(client, PagedIssueQuery, "docs")
.WithCancellation(cancellation.Token))
{
Console.WriteLine(issue);
Console.WriteLine($"Received {++num} issues in total");
}
}

Sie können den Code für das abgeschlossene Tutorial aus dem Repository dotnet/docs im Ordner csharp/whats-
new/tutorials abrufen.

Ausführen der fertig gestellten Anwendung


Führen Sie die Anwendung erneut aus. Vergleichen Sie deren Verhalten mit dem Verhalten der Startanwendung.
Die erste Seite mit Ergebnissen wird aufgelistet, sobald sie verfügbar ist. Es gibt eine wahrnehmbare Pause,
wenn eine neue Seite angefordert und abgerufen wird, und dann werden die Ergebnisse der nächsten Seite
schnell aufgelistet. Der try / catch -Block ist zur Verarbeitung eines Abbruchs nicht erforderlich: Der Aufrufer
kann das Auflisten der Sammlung beenden. Der Fortschritt wird deutlich gemeldet, weil der asynchrone
Datenstrom die Ergebnisse generiert, während die einzelnen Seiten heruntergeladen werden. Der Status jedes
zurückgegebenen Problems ist nahtlos in der await foreach -Schleife enthalten. Sie benötigen kein
Rückrufobjekt, um den Fortschritt nachzuverfolgen.
Sie können Verbesserungen in der Arbeitsspeichernutzung erkennen, indem Sie den Code untersuchen. Sie
müssen eine Sammlung nicht mehr zuordnen, um alle Ergebnisse zu speichern, bevor sie aufgelistet werden.
Der Aufrufer kann festlegen, wie die Ergebnisse genutzt werden und ob eine Speichersammlung erforderlich ist.
Führen Sie sowohl die Startanwendung als auch die fertig gestellte Anwendung aus, um die Unterschiede
zwischen den Implementierungen selbst zu beobachten. Wenn Sie fertig sind, können Sie das zu Beginn des
Tutorials erstellte GitHub-Zugriffstoken löschen. Wenn ein Angreifer Zugriff auf dieses Token erlangt hat, kann er
mit Ihren Anmeldeinformationen auf GitHub-APIs zugreifen.
Erstellen formatierter Zeichenfolgen mit der
Zeichenfolgeninterpolation
04.11.2021 • 7 minutes to read

Dieses Tutorial erläutert, wie Sie die Zeichenfolgeninterpolation in C# verwenden, um Werte in eine einzelne
Ergebniszeichenfolge einzufügen. Sie schreiben einen C#-Code und sehen dort die Ergebnisse der Kompilierung
und Ausführung Ihres Codes. Dieses Tutorial enthält einige Lektionen, in denen Ihnen gezeigt wird, wie Werte in
eine Zeichenfolge eingefügt und auf verschiedene Weisen formatiert werden.
Für dieses Tutorial wird vorausgesetzt, dass Sie über einen Computer verfügen, den Sie für die Entwicklung
nutzen können. Im .NET-Tutorial Hello World in 10 minutes (Hallo Welt in zehn Minuten) finden Sie eine
Anleitung zum Einrichten Ihrer lokalen Entwicklungsumgebung unter Windows, Linux oder macOS. Sie können
auch die interaktive Version dieses Tutorials in Ihrem Browser durcharbeiten.

Erstellen einer interpolierten Zeichenfolge


Erstellen Sie ein Verzeichnis namens interpolated. Legen Sie dieses als das aktuelle Verzeichnis fest, und führen
Sie folgenden Befehl in einem Konsolenfenster aus:

dotnet new console

Dieser Befehl erstellt im aktuellen Verzeichnis eine neue .NET Core-Konsolenanwendung.


Öffnen Sie Program.cs in Ihrem bevorzugten Editor, und ersetzen Sie die Zeile
Console.WriteLine("Hello World!"); durch den folgenden Code, in dem Sie <name> durch Ihren Namen
ersetzen:

var name = "<name>";


Console.WriteLine($"Hello, {name}. It's a pleasure to meet you!");

Testen Sie diesen Code, indem Sie dotnet run in Ihr Konsolenfenster eingeben. Wenn Sie das Programm
ausführen, wird eine einzelne Zeichenfolge angezeigt, die Ihren Namen in der Begrüßung enthält. Die im
WriteLine-Methodenaufruf enthaltene Zeichenfolge ist ein interpolierter Zeichenfolgenausdruck. Dabei handelt
es sich um eine Vorlage, durch die Sie eine einzelne Zeichenfolge (als Ergebniszeichenfolge bezeichnet) aus
einer Zeichenfolge erstellen können, die eingebetteten Code enthält. Interpolierte Zeichenfolgen sind besonders
nützlich für das Einfügen von Werten in eine Zeichenfolge oder zum Verketten (bzw. Verknüpfen) von
Zeichenfolgen.
Dieses einfache Beispiel enthält die zwei Elemente, über die jede interpolierte Zeichenfolge verfügen muss:
Ein Zeichenfolgenliteral, das ein $ -Zeichen vor dem öffnenden Anführungszeichen enthält. Zwischen
dem $ -Symbol und dem Anführungszeichen darf kein Leerraum vorhanden sein. (Wenn Sie testen
möchten, was andernfalls geschieht, fügen Sie einen Leerraum nach dem $ -Zeichen ein, speichern Sie
die Datei, und führen Sie das Programm erneut aus, indem Sie dotnet run im Konsolenfenster eingeben.
Der C#-Compiler zeigt dann die Fehlermeldung „Fehler CS1056: Unerwartetes Zeichen '$'“ an.)
Mindestens ein Interpolationsausdruck. Ein Interpolationsausdruck wird durch eine öffnende und eine
schließende geschweifte Klammer ( { und } ) angegeben. Sie können jeden C#-Ausdruck in die
Klammern einfügen, der einen Wert (einschließlich null ) zurückgibt.
Im Folgenden finden Sie weitere Beispiele für die Zeichenfolgeninterpolation mit einigen anderen Datentypen.

Einschließen verschiedener Datentypen


Im vorherigen Abschnitt haben Sie die Zeichenfolgeninterpolation verwendet, um eine Zeichenfolge in eine
andere einzufügen. Das Ergebnis eines Interpolationsausdrucks kann jedoch jeden Datentyp aufweisen. Im
Folgenden werden mehrere Datentypen in eine interpolierte Zeichenfolge einbezogen.
Im folgenden Beispiel wird zunächst ein Klassen-Datentyp Vegetable definiert, der über die Eigenschaft Name
und die Methode ToString verfügt. Diese Methode überschreibt das Verhalten der Object.ToString()-Methode.
Der public -Zugriffsmodifizierer stellt diese Methode jedem Clientcode zur Verfügung, um die
Zeichenfolgendarstellung einer Vegetable -Instanz abzurufen. Im Beispiel gibt die Methode Vegetable.ToString
den Wert der Eigenschaft Name zurück, die beim Konstruktor Vegetable initialisiert wird:

public Vegetable(string name) => Name = name;

Dann wird eine Instanz der Klasse Vegetable mit dem Namen item mithilfe des new Operators erstellt und ein
Name für den Konstruktor Vegetable angegeben:

var item = new Vegetable("eggplant");

Zum Schluss wird die Variable item in eine interpolierte Zeichenfolge einbezogen, die auch einen DateTime-
Wert, Decimal-Wert und einen Enumerationswert Unit enthält. Ersetzen Sie sämtlichen C#-Code in Ihrem
Editor durch folgenden Code, und verwenden Sie dann den dotnet run -Befehl, um diesen auszuführen:

using System;

public class Vegetable


{
public Vegetable(string name) => Name = name;

public string Name { get; }

public override string ToString() => Name;


}

public class Program


{
public enum Unit { item, kilogram, gram, dozen };

public static void Main()


{
var item = new Vegetable("eggplant");
var date = DateTime.Now;
var price = 1.99m;
var unit = Unit.item;
Console.WriteLine($"On {date}, the price of {item} was {price} per {unit}.");
}
}

Beachten Sie, dass der Interpolationsausdruck item der interpolierten Zeichenfolge in der Ergebniszeichenfolge
zu dem Text „eggplant“ aufgelöst wird. Dies liegt daran, dass der Ausdruckergebnistyp keine Zeichenfolge ist.
Daher wird das Ergebnis auf folgende Weise zu einer Zeichenfolge aufgelöst:
Wenn der Interpolationsausdruck zu null ausgewertet wird, wird eine leere Zeichenfolge („“ oder
String.Empty) verwendet.
Wenn der Interpolationsausdruck nicht zu null ausgewertet wird, wird in der Regel die Methode
ToString des Ergebnistyps aufgerufen. Sie können dies testen, indem Sie die Implementierung der
Methode Vegetable.ToString aktualisieren. Sie müssen die Methode ToString nicht einmal
implementieren, da jeder Typ auf die eine oder andere Art über eine Implementierung dieser Methode
verfügt. Sie können dies testen, indem Sie die Definition der Methode Vegetable.ToString im Beispiel
auskommentieren (fügen Sie hierzu davor ein Kommentarsymbol // ein). In der Ausgabe wird die
Zeichenfolge „eggplant“ durch den vollqualifizierten Typnamen ersetzt (in diesem Beispiel „Vegetable“).
Dies stellt das Standardverhalten der Object.ToString()-Methode dar. Das Standardverhalten der Methode
ToString für einen Enumerationswert besteht darin, die Zeichenfolgendarstellung eines Werts
zurückzugeben.
Bei der Ausgabe dieses Beispielcodes ist das Datum zu genau angegeben (der Preis von Auberginen ändert sich
nicht sekündlich), und der Wert der Variablen „price“ gibt keine Währungsinformation an. Im nächsten Abschnitt
erfahren Sie, wie Sie diese Probleme beheben, indem Sie das Format der Zeichenfolgendarstellung der
Ergebnisse des Ausdrucks steuern.

Steuern der Formatierung von Interpolationsausdrücken


Im vorherigen Abschnitt wurden zwei fehlerhaft formatierte Zeichenfolgen in die Ergebniszeichenfolge
eingefügt. Bei einer davon handelte es sich um einen Datums- und Uhrzeitwert, bei dem nur das Datum relevant
war. Bei der zweiten handelte es sich um einen Preis, bei dem keine Währungseinheit angegeben wurde. Beide
Probleme sind einfach zu beheben. Mithilfe der Zeichenfolgeninterpolation können Sie Formatzeichenfolgen
angeben, die die Formatierung bestimmter Typen steuern. Ändern Sie den Aufruf von Console.WriteLine im
vorherigen Beispiel, damit die Formatzeichenfolgen für die Ausdrücke „date“ und „price,“ wie in der folgenden
Zeile dargestellt, enthalten sind:

Console.WriteLine($"On {date:d}, the price of {item} was {price:C2} per {unit}.");

Sie können eine Formatzeichenfolge angeben, indem Sie dem Interpolationsausdruck einen Doppelpunkt („:“)
und die Formatzeichenfolge anfügen. Bei „d“ handelt es sich um eine Zeichenfolge für das Standardformat für
Datum und Uhrzeit, die das kurze Datumsformat darstellt. Bei „C2“ handelt es sich um eine Zeichenfolge für das
numerische Standardformat, die eine Zahl als Währungswert mit zwei Ziffern nach dem Dezimaltrennzeichen
darstellt.
Einige Typen in den .NET-Bibliotheken unterstützen einen vordefinierten Satz von Formatzeichenfolgen. Darin
sind alle numerischen Typen sowie alle Datums- und Uhrzeittypen enthalten. Eine vollständige Liste der Typen,
die Formatzeichenfolgen unterstützen, finden Sie im Artikel Formatieren von Typen in .NET unter Format Strings
and .NET Class Library Types (Formatzeichenfolgen und .NET-Klassenbibliothekstypen).
Versuchen Sie, die Formatzeichenfolgen in Ihrem Text-Editor zu ändern, und führen Sie das Programm jedes Mal
erneut aus, wenn Sie eine Änderung vornehmen. So können Sie feststellen, wie sich die Änderungen auf die
Formatierung des Datums, der Uhrzeit und des numerischen Werts auswirken. Ändern Sie „d“ in {date:d} in „t“
(um das kurze Uhrzeitformat anzuzeigen) sowie in „y“ (um das Jahr und den Monat anzuzeigen) und in „yyyy“
(um das Jahr als vierstellige Zahl anzuzeigen). Ändern Sie „C2“ in {price:C2} in „e“ (für die
Exponentialschreibweise) und in „F3“ (für einen numerischen Wert mit drei Ziffern nach dem
Dezimaltrennzeichen).
Sie können zusätzlich zum Steuern der Formatierung auch die Feldbreite und die Ausrichtung der formatierten
Zeichenfolgen steuern, die in der Ergebniszeichenfolge enthalten sind. Im nächsten Abschnitt erfahren Sie mehr
zu diesem Thema.

Steuern der Feldbreite und der Ausrichtung von


Interpolationsausdrücken
Wenn das Ergebnis eines Interpolationsausdrucks als Zeichenfolge formatiert wird, wird diese Zeichenfolge
normalerweise in eine Ergebniszeichenfolge ohne führende oder nachgestellte Leerzeichen einbezogen. Die
Feldbreite und Ausrichtung des Texts steuern zu können, ist insbesondere bei der Arbeit mit Daten hilfreich, um
eine besser lesbare Ausgabe zu erzeugen. Ersetzen Sie zur Veranschaulichung den gesamten Code in Ihrem Text-
Editor durch folgenden Code, und geben Sie dann dotnet run ein, um das Programm auszuführen:

using System;
using System.Collections.Generic;

public class Example


{
public static void Main()
{
var titles = new Dictionary<string, string>()
{
["Doyle, Arthur Conan"] = "Hound of the Baskervilles, The",
["London, Jack"] = "Call of the Wild, The",
["Shakespeare, William"] = "Tempest, The"
};

Console.WriteLine("Author and Title List");


Console.WriteLine();
Console.WriteLine($"|{"Author",-25}|{"Title",30}|");
foreach (var title in titles)
Console.WriteLine($"|{title.Key,-25}|{title.Value,30}|");
}
}

Die Namen der Autoren sind linksbündig ausgerichtet, während deren Werke rechtsbündig ausgerichtet sind.
Sie können die Ausrichtung festlegen, indem Sie einem Interpolationsausdruck ein Komma („,“) anfügen und die
minimale Feldbreite angeben. Wenn der angegebene Wert eine positive Zahl ist, wird das Feld rechtsbündig
ausgerichtet. Wenn er eine negative Zahl ist, wird das Feld linksbündig ausgerichtet.
Versuchen Sie die negativen Vorzeichen aus dem Code {"Author",-25} und {title.Key,-25} wie im folgenden
Code zu entfernen, und führen Sie das Beispiel erneut aus:

Console.WriteLine($"|{"Author",25}|{"Title",30}|");
foreach (var title in titles)
Console.WriteLine($"|{title.Key,25}|{title.Value,30}|");

Diesmal sind die Informationen zum Autor rechtsbündig ausgerichtet.


Sie können einen Ausrichtungsspezifizierer und eine Formatzeichenfolge für einen einzigen
Interpolationsausdruck kombinieren. Geben Sie dazu zunächst die Ausrichtung gefolgt von einem Doppelpunkt
und der Formatzeichenfolge an. Ersetzen Sie sämtlichen Code innerhalb der Main -Methode durch folgenden
Code, der drei formatierte Zeichenfolgen mit definierten Feldbreiten anzeigt. Führen Sie das Programm
anschließend aus, indem Sie den Befehl dotnet run eingeben.

Console.WriteLine($"[{DateTime.Now,-20:d}] Hour [{DateTime.Now,-10:HH}] [{1063.342,15:N2}] feet");

Die Ausgabe sieht in etwa folgendermaßen aus:

[04/14/2018 ] Hour [16 ] [ 1,063.34] feet


Sie haben da Tutorial für die Zeichenfolgeninterpolation abgeschlossen.
Weitere Informationen finden Sie im Artikel zur Zeichenfolgeninterpolation und dem Tutorial
Zeichenfolgeninterpolation in C#.
Zeichenfolgeninterpolation in C#
04.11.2021 • 5 minutes to read

In diesem Tutorial erfahren Sie, wie Sie die Zeichenfolgeninterpolation verwenden, um Ausdrucksergebnisse zu
einer Ergebniszeichenfolge hinzuzufügen. Für diese Beispiele wird davon ausgegangen, dass Sie sich mit den
grundlegenden C#-Konzepten und der .NET-Typformatierung auskennen. Wenn Sie sich mit der
Zeichenfolgeninterpolation oder der .NET-Typformatierung nicht auskennen, absolvieren Sie zuerst das
interaktive Tutorial zur Zeichenfolgeninterpolation. Weitere Informationen zum Formatieren von Typen in .NET
finden Sie unter Formatieren von Typen in .NET.

NOTE
Die C#-Beispiele in diesem Artikel werden in der Inlinecodeausführung und dem Playground von Try.NET ausgeführt.
Klicken Sie auf die Schaltfläche Ausführen , um ein Beispiel in einem interaktiven Fenster auszuführen. Nachdem Sie den
Code ausgeführt haben, können Sie ihn ändern und den geänderten Code durch erneutes Anklicken der Schaltfläche
Ausführen ausführen. Der geänderte Code wird entweder im interaktiven Fenster ausgeführt, oder das interaktive
Fenster zeigt alle C#-Compilerfehlermeldungen an, wenn die Kompilierung fehlschlägt.

Einführung
Das Feature Zeichenfolgeninterpolation ist ein Zusatz zum Feature composite formating (Kombinierte
Formatierung) und ermöglicht eine Syntax, die lesbarer und zweckmäßiger ist, um formatierte
Ausdrucksergebnisse zu einer Ergebniszeichenfolge hinzuzufügen.
Wenn Sie ein Zeichenfolgenliteral als interpolierte Zeichenfolge ermitteln möchten, stellen Sie ihm ein $ -
Symbol voran. Sie können jeden gültigen C#-Ausdruck einbetten, der einen Wert in einer interpolierten
Zeichenfolge zurückgibt. Im folgenden Beispiel wird das Ergebnis eines Ausdrucks in eine Zeichenfolge
konvertiert und zu einer Ergebniszeichenfolge hinzugefügt, sobald ein Ausdruck berechnet wird:

double a = 3;
double b = 4;
Console.WriteLine($"Area of the right triangle with legs of {a} and {b} is {0.5 * a * b}");
Console.WriteLine($"Length of the hypotenuse of the right triangle with legs of {a} and {b} is
{CalculateHypotenuse(a, b)}");

double CalculateHypotenuse(double leg1, double leg2) => Math.Sqrt(leg1 * leg1 + leg2 * leg2);

// Expected output:
// Area of the right triangle with legs of 3 and 4 is 6
// Length of the hypotenuse of the right triangle with legs of 3 and 4 is 5

Wie in dem Beispiel dargestellt, fügen Sie einen Ausdruck zu einer interpolierten Zeichenfolge hinzu, indem Sie
diesen in Klammern setzen:

{<interpolationExpression>}

Interpolierte Zeichenfolgen unterstützen sämtliche Funktionen des Features kombinierte Formatierung von
Zeichenfolgen. Damit stellen sie eine lesbarere Alternative zur Verwendung der String.Format-Methode dar.

Angeben einer Formatierungszeichenfolge für einen


Interpolationsausdruck
Sie können eine Formatzeichenfolge angeben, die von der Art des Ausdrucksergebnisses unterstützt wird,
indem Sie den Interpolationsausdruck mit einem Doppelpunkt (:) und der Formatzeichenfolge versehen:

{<interpolationExpression>:<formatString>}

Im folgenden Beispiel wird veranschaulicht, wie Sie Standardformatzeichenfolgen und benutzerdefinierte


Formatzeichenfolgen für Ausdrücke angeben, die Datums- oder Uhrzeitergebnisse bzw. numerische Ergebnisse
ausgeben:

var date = new DateTime(1731, 11, 25);


Console.WriteLine($"On {date:dddd, MMMM dd, yyyy} Leonhard Euler introduced the letter e to denote
{Math.E:F5} in a letter to Christian Goldbach.");

// Expected output:
// On Sunday, November 25, 1731 Leonhard Euler introduced the letter e to denote 2.71828 in a letter to
Christian Goldbach.

Weitere Informationen finden Sie im Artikel Kombinierte Formatierung im Abschnitt


Formatzeichenfolgenkomponente. In diesem Abschnitt finden Sie Links zu den Artikeln, in denen
Standardzeichenfolgenkomponenten und benutzerdefinierte Zeichenfolgenkomponenten erläutert werden, die
von den .NET-Basistypen unterstützt werden.

Steuern der Feldbreite und -ausrichtung des formatierten


Interpolationsausdrucks
Sie können die Mindestbreite und die Ausrichtung für das formatierte Ausdrucksergebnis angeben, indem Sie
den Interpolationsausdruck mit einem Komma (,) und dem konstanten Ausdruck versehen:

{<interpolationExpression>,<alignment>}

Wenn der Wert der Ausrichtung positiv ist, wird das formatierte Ausdrucksergebnis rechtsbündig ausgerichtet.
Ist der Wert negativ, wird das Ergebnis linksbündig ausgerichtet.
Wenn Sie sowohl die Ausrichtung als auch die Formatzeichenfolge angeben müssen, beginnen Sie mit der
Ausrichtungskomponente:

{<interpolationExpression>,<alignment>:<formatString>}

Im folgenden Beispiel sehen Sie, wie Sie die Ausrichtung angeben. Es werden senkrechte Striche („|“) verwendet,
um Textfelder zu begrenzen:
const int NameAlignment = -9;
const int ValueAlignment = 7;

double a = 3;
double b = 4;
Console.WriteLine($"Three classical Pythagorean means of {a} and {b}:");
Console.WriteLine($"|{"Arithmetic",NameAlignment}|{0.5 * (a + b),ValueAlignment:F3}|");
Console.WriteLine($"|{"Geometric",NameAlignment}|{Math.Sqrt(a * b),ValueAlignment:F3}|");
Console.WriteLine($"|{"Harmonic",NameAlignment}|{2 / (1 / a + 1 / b),ValueAlignment:F3}|");

// Expected output:
// Three classical Pythagorean means of 3 and 4:
// |Arithmetic| 3.500|
// |Geometric| 3.464|
// |Harmonic | 3.429|

Wie Sie in der Beispielausgabe sehen können, wird der Wert Ausrichtung ignoriert, wenn die Länge des
formatierten Ausdrucksergebnisses über die angegebene Feldbreite hinausgeht.
Weitere Informationen finden Sie im Artikel Kombinierte Formatierung im Abschnitt Ausrichtungskomponente.

Verwenden von Escapesequenzen in einer interpolierten


Zeichenfolge
Interpolierte Zeichenfolgen unterstützen alle Escapesequenzen, die in gewöhnlichen Zeichenfolgenliteralen
verwendet werden können. Weitere Informationen finden Sie unter Zeichenfolgenescapesequenzen.
Verwenden Sie ein ausführliches Zeichenfolgenliteral, um Escapesequenzen wörtlich zu interpretieren.
Ausführliche interpolierte Zeichenfolgen beginnen mit dem Zeichen $ , gefolgt vom Zeichen @ . Ab C# 8.0
können Sie die Token $ und @ in beliebiger Reihenfolge verwenden: Sowohl $@"..." als auch @$"..." sind
gültige interpolierte ausführliche Zeichenfolgen.
Wenn Sie einer Ergebniszeichenfolge eine Klammer hinzufügen möchten „{“ oder „}“, sollten Sie jeweils zwei
Klammern verwenden: „{{“ oder „}}“. Weitere Informationen finden Sie im Artikel Kombinierte Formatierung im
Abschnitt Versehen von geschweiften Klammern mit Escapezeichen.
Im folgenden Beispiel wird dargestellt, wie Sie Klammern zu einer Ergebniszeichenfolge hinzufügen und eine
ausführliche interpolierte Zeichenfolge erstellen:

var xs = new int[] { 1, 2, 7, 9 };


var ys = new int[] { 7, 9, 12 };
Console.WriteLine($"Find the intersection of the {{{string.Join(", ",xs)}}} and {{{string.Join(", ",ys)}}}
sets.");

var userName = "Jane";


var stringWithEscapes = $"C:\\Users\\{userName}\\Documents";
var verbatimInterpolated = $@"C:\Users\{userName}\Documents";
Console.WriteLine(stringWithEscapes);
Console.WriteLine(verbatimInterpolated);

// Expected output:
// Find the intersection of the {1, 2, 7, 9} and {7, 9, 12} sets.
// C:\Users\Jane\Documents
// C:\Users\Jane\Documents

Verwenden eines ternären bedingten ?: -Operators in einem


Interpolationsausdruck
Da der Doppelpunkt (:) in einem Element mit einem Interpolationsausdruck eine besondere Funktion einnimmt,
müssen Sie diesen Ausdruck wie folgt in runde Klammern einschließen, um einen Bedingungsoperator
verwenden zu können:

var rand = new Random();


for (int i = 0; i < 7; i++)
{
Console.WriteLine($"Coin flip: {(rand.NextDouble() < 0.5 ? "heads" : "tails")}");
}

Erstellen einer kulturspezifischen Ergebniszeichenfolge mit einer


Zeichenfolgeninterpolation
Standardmäßig verwendet eine interpolierte Zeichenfolge die aktuelle Kultur, die von der
CultureInfo.CurrentCulture-Eigenschaft für alle Formatierungsvorgänge definiert ist. Verwenden Sie für eine
interpolierte Zeichenfolge einen impliziten Wechsel in eine System.FormattableString-Instanz, und rufen Sie
deren ToString(IFormatProvider)-Methode auf, um eine kulturspezifische Ergebniszeichenfolge zu erstellen. Das
folgende Beispiel zeigt, wie Sie dabei vorgehen müssen:

var cultures = new System.Globalization.CultureInfo[]


{
System.Globalization.CultureInfo.GetCultureInfo("en-US"),
System.Globalization.CultureInfo.GetCultureInfo("en-GB"),
System.Globalization.CultureInfo.GetCultureInfo("nl-NL"),
System.Globalization.CultureInfo.InvariantCulture
};

var date = DateTime.Now;


var number = 31_415_926.536;
FormattableString message = $"{date,20}{number,20:N3}";
foreach (var culture in cultures)
{
var cultureSpecificMessage = message.ToString(culture);
Console.WriteLine($"{culture.Name,-10}{cultureSpecificMessage}");
}

// Expected output is like:


// en-US 5/17/18 3:44:55 PM 31,415,926.536
// en-GB 17/05/2018 15:44:55 31,415,926.536
// nl-NL 17-05-18 15:44:55 31.415.926,536
// 05/17/2018 15:44:55 31,415,926.536

Wie in dem Beispiel dargestellt, können Sie eine FormattableString-Instanz verwenden, um mehrere
Ergebniszeichenfolgen für verschiedene Kulturen zu generieren.

Erstellen einer Ergebniszeichenfolge mithilfe der invarianten Kultur


Neben der FormattableString.ToString(IFormatProvider)-Methode können Sie die statische
FormattableString.Invariant-Methode verwenden, um eine interpolierte Zeichenfolge in eine
Ergebniszeichenfolge für InvariantCulture auszulösen. Das folgende Beispiel zeigt, wie Sie dabei vorgehen
müssen:
string messageInInvariantCulture = FormattableString.Invariant($"Date and time in invariant culture:
{DateTime.Now}");
Console.WriteLine(messageInInvariantCulture);

// Expected output is like:


// Date and time in invariant culture: 05/17/2018 15:46:24

Zusammenfassung
In diesem Tutorial wurden häufig auftretende Szenarios zur Verwendung der Zeichenfolgeninterpolation
beschrieben. Weitere Informationen zur Zeichenfolgeninterpolation finden Sie unter Zeichenfolgeninterpolation.
Weitere Informationen zum Formatieren von Typen in .NET finden Sie unter Formatieren von Typen in .NET und
Kombinierte Formatierung.

Siehe auch
String.Format
System.FormattableString
System.IFormattable
Zeichenfolgen
Konsolen-App
04.11.2021 • 12 minutes to read

In diesem Tutorial lernen Sie verschiedene Features in .NET Core und der Sprache C# kennen. Es werden die
folgenden Themen abgedeckt:
Die Grundlagen zur .NET Core-CLI
Die Struktur einer C#-Konsolenanwendung
Konsolen-E/A
Grundlagen der Datei-E/A-APIs in .NET
Grundlagen des taskbasierten asynchronen Programmiermodells in .NET
Sie erstellen eine Anwendung, die eine Textdatei liest und die Inhalte dieser Textdatei an die Konsole ausgibt. Das
Tempo der Ausgabe in der Konsole ist so festgelegt, dass ein lautes Mitlesen möglich ist. Sie können die
Ausgabe beschleunigen oder verlangsamen, indem Sie die Tasten „<“ (kleiner als) oder „>“ (größer als) drücken.
In diesem Tutorial werden viele Features abgedeckt. Gehen wir sie einzeln an.

Voraussetzungen
Richten Sie Ihren Computer für die Ausführung von .NET Core ein. Die Installationsanweisungen finden
Sie auf der Seite .NET Core-Downloads. Sie können diese Anwendung unter Windows, Linux, macOS oder
in einem Docker-Container ausführen.
Installieren Sie Ihren bevorzugten Code-Editor.

Erstellen der App


Im ersten Schritt wird eine neue Anwendung erstellt. Öffnen Sie eine Eingabeaufforderung, und erstellen Sie ein
neues Verzeichnis für Ihre Anwendung. Legen Sie das Verzeichnis als aktuelles Verzeichnis fest. Geben Sie an der
Eingabeaufforderung den Befehl dotnet new console ein. Hierdurch werden die Startdateien für eine einfache
„Hello World“-Anwendung erstellt.
Bevor Sie damit beginnen, Änderungen durchzuführen, gehen wir die Schritte zur Ausführung der einfachen
Hello World-Anwendung durch. Geben Sie nach dem Erstellen der Anwendung den Befehl dotnet restore an
der Eingabeaufforderung ein. Mit diesem Befehl wird der Prozess zur NuGet-Paketwiederherstellung ausgeführt.
NuGet ist ein .NET-Paket-Manager. Mit diesem Befehl werden alle fehlenden abhängigen Komponenten für Ihr
Projekt heruntergeladen. Da es sich um ein neues Projekt handelt, ist keine der abhängigen Komponenten
vorhanden, deshalb wird zunächst das .NET Core-Framework heruntergeladen. Nach diesem ersten Schritt
müssen Sie dotnet restore nur ausführen, wenn Sie neue abhängige Pakete hinzufügen oder die Versionen
abhängiger Komponenten aktualisieren.
Sie müssen dotnet restore nicht ausführen, da der Befehl implizit von allen Befehlen ausgeführt wird, die eine
Wiederherstellung erfordern. Zu diesen zählen z. B. dotnet new , dotnet build , dotnet run , dotnet test ,
dotnet publish und dotnet pack . Verwenden Sie die Option --no-restore , um die implizite Wiederherstellung
zu deaktivieren.
In bestimmten Fällen eignet sich der dotnet restore -Befehl dennoch. Dies ist etwa bei Szenarios der Fall, in
denen die explizite Wiederherstellung sinnvoll ist. Beispiele hierfür sind Continuous Integration-Builds in Azure
DevOps Services oder Buildsysteme, die den Zeitpunkt für die Wiederherstellung explizit steuern müssen.
Informationen zum Verwalten von NuGet-Feeds finden Sie in der dotnet restore Dokumentation.
Nach dem Wiederherstellen der Pakete führen Sie dotnet build aus. Hiermit wird die Build-Engine ausgeführt
und die ausführbare Datei für Ihre Anwendung erstellt. Abschließend führen Sie dotnet run aus, um Ihre
Anwendung zu starten.
Der gesamte Code für die einfache Hello World-Anwendung ist in „Program.cs“ enthalten. Öffnen Sie diese
Datei mit Ihrem bevorzugten Text-Editor. Wir werden jetzt die ersten Änderungen vornehmen. Am Anfang der
Datei sehen Sie eine using-Anweisung:

using System;

Diese Anweisung informiert den Compiler, dass alle Typen aus dem System -Namespace im Gültigkeitsbereich
liegen. Wie andere objektorientierte Sprachen, die Sie vielleicht schon verwendet haben, verwendet C#
Namespaces, um Typen zu organisieren. Dieses Hello World-Programm funktioniert genauso. Sie können sehen,
dass das Programm in den Namespace eingeschlossen ist, wobei der Name auf dem des aktuellen
Verzeichnisses basiert. Für dieses Tutorial ändern wir den Namen des Namespace in TeleprompterConsole :

namespace TeleprompterConsole

Lesen und Ausgeben der Datei


Das erste hinzuzufügende Feature ist die Möglichkeit zum Lesen einer Textdatei und zum Anzeigen des
gesamten Texts in der Konsole. Fügen wir zunächst eine Textdatei hinzu. Kopieren Sie die Datei sampleQuotes.txt
aus dem GitHub-Repository für dieses Beispiel in Ihr Projektverzeichnis. Diese Datei dient als Skript für Ihre
Anwendung. Wenn Sie Informationen dazu erhalten möchten, wie Sie die Beispiel-App für dieses Thema
herunterladen, finden Sie Anweisungen im Thema Beispiele und Tutorials.
Fügen Sie als Nächstes die folgende Methode in Ihre Program -Klasse ein (rechts neben der Main -Methode):

static IEnumerable<string> ReadFrom(string file)


{
string line;
using (var reader = File.OpenText(file))
{
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}

Diese Methode verwendet Typen aus zwei neuen Namespaces. Für die Kompilierung müssen Sie oben in der
Datei die folgenden zwei Zeilen einfügen:

using System.Collections.Generic;
using System.IO;

Die IEnumerable<T>-Schnittstelle ist im System.Collections.Generic-Namespace definiert. Die File-Klasse ist im


System.IO-Namespace definiert.
Diese Methode ist ein besonderer Typ von C#-Methode, der als Iteratormethode bezeichnet wird.
Enumeratormethoden geben Sequenzen zurück, die verzögert ausgewertet werden. Dies bedeutet, dass jedes
Element in der Sequenz dann generiert wird, wenn es vom Code angefordert wird, der die Sequenz verarbeitet.
Enumeratormethoden sind Methoden, die mindestens eine yield return -Anweisung enthalten. Das von der
ReadFrom -Methode zurückgegebene Objekt enthält den Code zum Generieren aller Elemente in der Sequenz. In
diesem Beispiel umfasst dies das Einlesen der nächsten Textzeile aus der Quelldatei und die Rückgabe dieser
Zeichenfolge. Jedes Mal, wenn der aufrufende Code die nächste Zeile aus der Sequenz anfordert, liest der Code
die nächste Zeile aus der Textdatei und gibt sie zurück. Wenn die Datei vollständig gelesen wurde, gibt die
Sequenz an, dass keine weiteren Elemente vorhanden sind.
Es gibt zwei weitere C#-Syntaxelemente, die möglicherweise neu für Sie sind. Die using -Anweisung in dieser
Methode verwaltet die Ressourcenbereinigung. Die Variable, die in der using -Anweisung initialisiert wird – in
diesem Beispiel reader –, muss die IDisposable-Schnittstelle implementieren. Diese Schnittstelle definiert eine
einzige Methode, Dispose , die aufgerufen werden muss, wenn die Ressource freigegeben werden soll. Der
Compiler generiert diesen Aufruf, wenn die Ausführung die schließende geschweifte Klammer der using -
Anweisung erreicht. Der vom Compiler generierte Code stellt sicher, dass die Ressource selbst dann freigegeben
wird, wenn der Code im durch die using-Anweisung definierten Block eine Ausnahme auslöst.
Die reader -Variable wird mit dem var -Schlüsselwort definiert. var definiert eine implizit typisierte lokale
Variable. Dies bedeutet, dass der Typ der Variablen durch den Kompilierzeittyp des Objekts bestimmt wird, das
der Variablen zugewiesen ist. Hier ist dies der Rückgabewert der OpenText(String)-Methode, bei dem es sich um
ein StreamReader-Objekt handelt.
Geben Sie jetzt den Code ein, um die Datei in der Main -Methode zu lesen:

var lines = ReadFrom("sampleQuotes.txt");


foreach (var line in lines)
{
Console.WriteLine(line);
}

Führen Sie das Programm (unter Verwendung von dotnet run ) aus. Jede Zeile wird einzeln an der Konsole
ausgegeben.

Hinzufügen von Verzögerungen und Formatieren der Ausgabe


Der Text wird momentan zu schnell ausgegeben, um ihn laut mitzulesen. Jetzt müssen Sie Verzögerungen in der
Ausgabe hinzufügen. Zu Beginn erstellen Sie einen Teil des grundlegenden Codes, der asynchrone Verarbeitung
ermöglicht. Auf diese ersten Schritte folgen jedoch einige Antimuster. Die Antimuster werden in Kommentaren
erläutert, die Sie im Code hinzufügen, und der Code wird in späteren Schritten aktualisiert.
Dieser Abschnitt umfasst zwei Schritte. Zunächst aktualisieren Sie die Iteratormethode, um anstelle von ganzen
Zeilen einzelne Wörter zurückzugeben. Dies wird durch diese Änderungen erreicht. Ersetzen Sie die
yield return line; -Anweisung durch den folgenden Code:

var words = line.Split(' ');


foreach (var word in words)
{
yield return word + " ";
}
yield return Environment.NewLine;

Als Nächstes ändern Sie die Art und Weise, in der die Dateizeilen verarbeitet werden und fügen nach jedem
geschriebenen Wort eine Verzögerung hinzu. Ersetzen Sie die Console.WriteLine(line) -Anweisung in der Main
-Methode durch den folgenden Block:
Console.Write(line);
if (!string.IsNullOrWhiteSpace(line))
{
var pause = Task.Delay(200);
// Synchronously waiting on a task is an
// anti-pattern. This will get fixed in later
// steps.
pause.Wait();
}

Die Task-Klasse ist im System.Threading.Tasks-Namespace enthalten, deshalb müssen Sie diese using -
Anweisung am Anfang der Datei hinzufügen:

using System.Threading.Tasks;

Führen Sie das Beispiel aus, und überprüfen Sie die Ausgabe. Jetzt folgt nach der Ausgabe jedes Worts eine
Pause von 200 ms. Die angezeigte Ausgabe ist jedoch fehlerhaft, weil der Quelltext mehrere Zeilen umfasst, die
mehr als 80 Zeichen ohne Zeilenumbruch aufweisen. Der Text ist möglicherweise schwer zu lesen, wenn er ohne
Umbrüche angezeigt wird. Dieses Problem kann einfach gelöst werden. Sie verfolgen einfach die Länge jeder
Zeile nach und generieren immer dann eine neue Zeile, wenn die Zeilenlänge einen bestimmten Schwellenwert
überschreitet. Deklarieren Sie nach der Deklaration von words in der ReadFrom -Methode eine lokale Variable,
die die Zeilenlänge enthält:

var lineLength = 0;

Fügen Sie dann nach der yield return word + " "; -Anweisung (vor der schließenden geschweiften Klammer)
den folgenden Code hinzu:

lineLength += word.Length + 1;
if (lineLength > 70)
{
yield return Environment.NewLine;
lineLength = 0;
}

Führen Sie das Beispiel aus. Jetzt sollten Sie in der Lage sein, den Text im festgelegten Tempo laut mitzulesen.

Asynchrone Tasks
In diesem letzten Schritt fügen Sie den Code hinzu, mit dem in einem Task die Ausgabe asynchron geschrieben
wird, während in einem weiteren Task Eingaben vom Benutzer gelesen werden, um ggf. die Geschwindigkeit der
Textanzeige zu erhöhen oder zu verringern oder die Textanzeige ganz zu beenden. Hierzu sind einige Schritte
erforderlich, damit Sie am Ende über alle benötigten Aktualisierungen verfügen. Im ersten Schritt erstellen Sie
eine asynchrone Task-Rückgabemethode, die den Code darstellt, den Sie bisher zum Lesen und Anzeigen der
Datei erstellt haben.
Fügen Sie diese Methode Ihrer Program -Klasse hinzu (diese stammt aus dem Textkörper Ihrer Main -Methode):
private static async Task ShowTeleprompter()
{
var words = ReadFrom("sampleQuotes.txt");
foreach (var word in words)
{
Console.Write(word);
if (!string.IsNullOrWhiteSpace(word))
{
await Task.Delay(200);
}
}
}

Sie werden zwei Änderungen bemerken. Zunächst wird im Methodenkörper anstelle eines Aufrufs von Wait()
zum synchronen Warten auf eine Taskbeendigung in dieser Version das Schlüsselwort await verwendet. Hierzu
müssen Sie der Methodensignatur den async -Modifizierer hinzufügen. Diese Methode gibt einen Task zurück.
Beachten Sie, dass es keine return-Anweisungen gibt, die ein Task -Objekt zurückgeben. Stattdessen wird dieses
Task -Objekt durch Code erstellt, den der Compiler beim Verwenden des await -Operators generiert. Sie
können sich dies so vorstellen, dass die Methode eine Rückgabe durchführt, wenn sie ein await -Schlüsselwort
erreicht. Der zurückgegebene Task gibt an, dass der Vorgang noch nicht abgeschlossen wurde. Die Methode
wird fortgesetzt, wenn der erwartete Task abgeschlossen ist. Nach Abschluss der Ausführung weist der
zurückgegebene Task darauf hin, dass er abgeschlossen wurde. Der aufrufende Code kann den
zurückgegebenen Task überwachen, um zu ermitteln, wann dieser abgeschlossen ist.
Sie können diese neue Methode in Ihrer Main -Methode aufrufen:

ShowTeleprompter().Wait();

Hier führt der Code in Main einen asynchronen Wartevorgang aus. Sie sollten nach Möglichkeit anstelle eines
synchronen Wartevorgangs immer den await -Operator verwenden. In der Main -Methode einer
Konsolenanwendung kann der await -Operator jedoch nicht verwendet werden. Dies würde dazu führen, dass
die Anwendung beendet wird, bevor alle Tasks abgeschlossen sind.

NOTE
Wenn Sie C# 7.1 oder höher verwenden, können Sie Konsolenanwendungen mit der async Main -Methode erstellen.

Als Nächstes müssen Sie die zweite asynchrone Methode schreiben, um Inhalte aus der Konsole zu lesen und
auf die Tasteneingaben „<“ (kleiner als) und „>“ (größer als) sowie „X“ und „x“ zu überwachen. Dies ist die
Methode, die Sie für diesen Task hinzufügen:
private static async Task GetInput()
{
var delay = 200;
Action work = () =>
{
do {
var key = Console.ReadKey(true);
if (key.KeyChar == '>')
{
delay -= 10;
}
else if (key.KeyChar == '<')
{
delay += 10;
}
else if (key.KeyChar == 'X' || key.KeyChar == 'x')
{
break;
}
} while (true);
};
await Task.Run(work);
}

Hiermit wird ein Lambdaausdruck zur Darstellung eines Action-Delegaten erstellt. Mit diesem wird ein Schlüssel
aus der Konsole gelesen und eine lokale Variable geändert, die die Verzögerung beim Drücken der Tasten „<“
(kleiner als) oder „>“ (größer als) durch den Benutzer darstellt. Die Delegatmethode wird beendet, wenn der
Benutzer die Tasten „X“ oder „x“ drückt, sodass der Benutzer die Textanzeige jederzeit beenden kann. Diese
Methode verwendet ReadKey() zum Blockieren und wartet darauf, dass der Benutzer eine Taste drückt.
Um dieses Feature abzuschließen, müssen Sie eine neue async Task -Rückgabemethode erstellen, die beide
Tasks ( GetInput und ShowTeleprompter ) startet und außerdem die von diesen Tasks gemeinsam verwendeten
Daten verwaltet.
Es muss eine neue Klasse erstellt werden, mit der die von diesen zwei Tasks gemeinsam verwendeten Daten
verarbeitet werden können. Diese Klasse enthält zwei öffentliche Eigenschaften: die Verzögerung und ein Flag
Done , mit dem angegeben wird, dass die Datei vollständig gelesen wurde:

namespace TeleprompterConsole
{
internal class TelePrompterConfig
{
public int DelayInMilliseconds { get; private set; } = 200;

public void UpdateDelay(int increment) // negative to speed up


{
var newDelay = Min(DelayInMilliseconds + increment, 1000);
newDelay = Max(newDelay, 20);
DelayInMilliseconds = newDelay;
}

public bool Done { get; private set; }

public void SetDone()


{
Done = true;
}
}
}

Platzieren Sie diese Klasse in einer neuen Datei, und fügen Sie diese Klasse in den TeleprompterConsole -
Namespace ein (wie oben gezeigt). Sie benötigen außerdem eine using static -Anweisung, damit Sie ohne die
Namen der übergeordneten Klasse oder des Namespace auf die Min - und Max -Methode verweisen können.
Eine using static -Anweisung importiert die Methoden einer Klasse. Dies steht in Kontrast zu den bisher
verwendeten using -Anweisungen, die alle Klassen aus einem Namespace importiert haben.

using static System.Math;

Im nächsten Schritt müssen Sie die ShowTeleprompter - und GetInput -Methoden zur Verwendung des neuen
config -Objekts aktualisieren. Schreiben Sie einen finalen Task , der die async -Methode zurückgibt, um beide
Tasks zu starten und den Vorgang zu beenden, sobald der erste Task beendet wird:

private static async Task RunTeleprompter()


{
var config = new TelePrompterConfig();
var displayTask = ShowTeleprompter(config);

var speedTask = GetInput(config);


await Task.WhenAny(displayTask, speedTask);
}

Die neue Methode hier ist der WhenAny(Task[])-Aufruf. Hiermit wird ein Task erstellt, der abgeschlossen wird,
sobald einer der Tasks in dieser Argumentliste beendet ist.
Im nächsten Schritt müssen Sie die ShowTeleprompter - und GetInput -Methoden zur Verwendung des neuen
config -Objekts aktualisieren:

private static async Task ShowTeleprompter(TelePrompterConfig config)


{
var words = ReadFrom("sampleQuotes.txt");
foreach (var word in words)
{
Console.Write(word);
if (!string.IsNullOrWhiteSpace(word))
{
await Task.Delay(config.DelayInMilliseconds);
}
}
config.SetDone();
}

private static async Task GetInput(TelePrompterConfig config)


{
Action work = () =>
{
do {
var key = Console.ReadKey(true);
if (key.KeyChar == '>')
config.UpdateDelay(-10);
else if (key.KeyChar == '<')
config.UpdateDelay(10);
else if (key.KeyChar == 'X' || key.KeyChar == 'x')
config.SetDone();
} while (!config.Done);
};
await Task.Run(work);
}

Diese neue Version von ShowTeleprompter ruft eine neue Methode in der TeleprompterConfig -Klasse auf. Jetzt
müssen Sie Main aktualisieren, um anstelle von ShowTeleprompter``RunTeleprompter aufzurufen:
RunTeleprompter().Wait();

Schlussbemerkung
In diesem Tutorial wurden verschiedene Features von C# und den .NET Core-Bibliotheken vorgestellt, die bei der
Arbeit in Konsolenanwendungen benötigt werden. Sie können auf diesem Wissen aufbauen, um C# und die hier
beschriebenen Klassen weiter zu erkunden. Sie haben die Grundlagen der Datei- und Konsolen-E/A
kennengelernt, und es wurden die blockierende und die nicht blockierende Verwendung der taskbasierten
asynchronen Programmierung vorgestellt. Außerdem haben Sie einen Überblick über die Sprache C# und die
Struktur von C#-Programmen erhalten, und Sie haben die .NET Core-CLI kennengelernt.
Weitere Informationen zur Datei-E/A finden Sie im Thema Datei- und Stream-E/A. Weitere Informationen zu
dem in diesem Tutorial verwendeten asynchronen Programmiermodell finden sie in den Themen
Aufgabenbasierte asynchrone Programmierung und Asynchrone Programmierung.
Tutorial: Übermitteln von HTTP-Anforderungen in
einer .NET-Konsolen-App mit C#
04.11.2021 • 8 minutes to read

In diesem Tutorial erstellen Sie eine App, die HTTP-Anforderungen an einen REST-Dienst in GitHub übermittelt.
Die App liest Informationen im JSON-Format und konvertiert den JSON-Code in C#-Objekte. Die Konvertierung
von JSON-Code in C#-Objekte wird als Deserialisierung bezeichnet.
In diesem Tutorial lernen Sie Folgendes:
Senden von HTTP-Anforderungen
Deserialisieren von JSON-Antworten
Konfigurieren der Deserialisierung mit Attributen
Wenn Sie lieber das vollständige Beispiel für dieses Tutorial befolgen möchten, können Sie es herunterladen.
Anweisungen zum Herunterladen finden Sie unter Beispiele und Lernprogramme.

Voraussetzungen
.NET SDK 5.0 oder höher
Ein Code-Editor wie Visual Studio Code, ein plattformübergreifender Open-Source-Editor Sie können die
Beispiel-App unter Windows, Linux oder macOS oder auch in einem Docker-Container ausführen.

Erstellen der Client-App


1. Öffnen Sie eine Eingabeaufforderung, und erstellen Sie ein neues Verzeichnis für Ihre App. Legen Sie das
Verzeichnis als aktuelles Verzeichnis fest.
2. Geben Sie den folgenden Befehl in ein Konsolenfenster ein:

dotnet new console --name WebAPIClient

Mit diesem Befehl werden die Startdateien für eine einfache „Hallo Welt“-App erstellt. Der Projektname
lautet „WebAPIClient“.
3. Navigieren Sie zum Verzeichnis „WebAPIClient“, und führen Sie die App aus.

cd WebAPIClient

dotnet run

Mit dotnet run wird automatisch dotnet restore ausgeführt, um alle Abhängigkeiten
wiederherzustellen, die von der App benötigt werden. Außerdem wird bei Bedarf dotnet build
ausgeführt.

Übermitteln von HTTP-Anforderungen


Diese App ruft die GitHub-API auf, um Informationen zu den Projekten unter dem Schirm der .NET Foundation
zu erhalten. Der Endpunkt ist https://api.github.com/orgs/dotnet/repos. Zum Abrufen von Informationen wird
eine HTTP GET-Anforderung gesendet. Browser verwenden ebenfalls HTTP GET-Anforderungen, daher können
Sie diese URL auf der Adressleiste Ihres Browsers einfügen, um zu sehen, welche Informationen abgerufen und
verarbeitet werden.
Verwenden Sie die HttpClient-Klasse, um HTTP-Anforderungen zu übermitteln. HttpClient unterstützt für die
APIs mit langer Ausführungszeit nur asynchrone Methoden. Daher wird mit den folgenden Schritten eine
asynchrone Methode erstellt und über die Main-Methode aufgerufen.
1. Öffnen Sie zunächst die Datei Program.cs in Ihrem Projektverzeichnis, und fügen Sie der Program -Klasse
die folgende asynchrone Methode hinzu:

private static async Task ProcessRepositories()


{
}

2. Fügen Sie eine using -Direktive am Anfang der Datei Program.cs hinzu, damit der C#-Compiler den Typ
Task erkennt:

using System.Threading.Tasks;

Wenn Sie dotnet build zu diesem Zeitpunkt ausführen, wird die Kompilierung zwar erfolgreich
abgeschlossen, Sie werden jedoch gewarnt, dass diese Methode keine await -Operatoren enthält und
daher synchron ausgeführt wird. Sie fügen await -Operatoren später hinzu, wenn Sie die Methode
vervollständigen.
3. Ersetzen Sie die Main -Methode durch folgenden Code:

static async Task Main(string[] args)


{
await ProcessRepositories();
}

Mit diesem Code wird Folgendes durchgeführt:


Ändern der Signatur von Main durch Hinzufügen des async -Modifizierers und Ändern des
Rückgabetyps in Task
Ersetzen der Console.WriteLine -Anweisung durch einen Aufruf von ProcessRepositories , der das
Schlüsselwort await verwendet
4. Erstellen Sie in der Program -Klasse eine statische Instanz von HttpClient, um Anforderungen und
Antworten zu verarbeiten.

namespace WebAPIClient
{
class Program
{
private static readonly HttpClient client = new HttpClient();

static async Task Main(string[] args)


{
//...
}
}
}
5. Rufen Sie in der ProcessRepositories -Methode den GitHub-Endpunkt auf, der eine Liste aller Repositorys
unter der Organisation der .NET Foundation zurückgibt:

private static async Task ProcessRepositories()


{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");

var stringTask = client.GetStringAsync("https://api.github.com/orgs/dotnet/repos");

var msg = await stringTask;


Console.Write(msg);
}

Mit diesem Code wird Folgendes durchgeführt:


Einrichten der HTTP-Header für alle Anforderungen:
Ein Accept -Header zum Akzeptieren von JSON-Antworten
Ein User-Agent -Header. Diese Header werden vom GitHub-Servercode überprüft und sind
erforderlich, um Informationen aus GitHub abzurufen.
Aufrufen von HttpClient.GetStringAsync(String), um eine Webanforderung zu erstellen und die
Antwort abzurufen: Diese Methode startet einen Task, der die Webanforderung durchführt. Wenn eine
Rückgabe durch die Anforderung erfolgt, liest der Task den Antwortstream und extrahiert den Inhalt
aus dem Stream. Der Text der Antwort wird als String zurückgegeben, der nach Abschluss der Aufgabe
verfügbar ist.
Warten auf den Task für die Antwortzeichenfolge und Ausgeben der Antwort an der Konsole
6. Fügen Sie am Anfang der Datei zwei using -Direktiven hinzu:

using System.Net.Http;
using System.Net.Http.Headers;

7. Erstellen Sie die App, und führen Sie sie aus.

dotnet run

Es wird keine Buildwarnung ausgegeben, da ProcessRepositories jetzt einen await -Operator enthält.
Die Ausgabe ist eine lange Darstellung von JSON-Text.

Deserialisieren des JSON-Ergebnisses


Mit den folgenden Schritten wird die JSON-Antwort in C#-Objekte konvertiert. Sie verwenden die
System.Text.Json.JsonSerializer-Klasse, um JSON-Code in Objekte zu deserialisieren.
1. Erstellen Sie eine Datei mit dem Namen repo.cs, und fügen Sie den folgenden Code hinzu:
using System;

namespace WebAPIClient
{
public class Repository
{
public string name { get; set; }
}
}

Mit diesem Code definieren Sie eine Klasse, die das von der GitHub-API zurückgegebene JSON-Objekt
darstellt. Sie verwenden diese Klasse, um eine Liste mit Repositorynamen anzuzeigen.
Der JSON-Code für ein Repositoryobjekt enthält Dutzende von Eigenschaften, aber nur die name -
Eigenschaft wird deserialisiert. Das Serialisierungsmodul ignoriert automatisch alle JSON-Eigenschaften,
für die keine Übereinstimmung in der Zielklasse vorliegt. Dieses Feature vereinfacht die Erstellung von
Typen, die nur mit einem Teilsatz der Felder in einem großen JSON-Paket funktionieren.
Laut C#-Konvention wird der erste Buchstabe von Eigenschaftennamen groß geschrieben, aber die name
-Eigenschaft beginnt hier mit einem Kleinbuchstaben, da dies genau dem JSON-Code entspricht. Später
erfahren Sie, wie Sie C#-Eigenschaftennamen verwenden, die nicht mit den JSON-Eigenschaftennamen
übereinstimmen.
2. Verwenden Sie das Serialisierungsmodul, um JSON-Code in C#-Objekte zu konvertieren. Ersetzen Sie
den Aufruf von GetStringAsync(String) in der ProcessRepositories -Methode durch die folgenden Zeilen:

var streamTask = client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos");


var repositories = await JsonSerializer.DeserializeAsync<List<Repository>>(await streamTask);

Im aktualisierten Code wird GetStringAsync(String) durch GetStreamAsync(String) ersetzt. Die Methode


des Serialisierungsmoduls nutzt anstelle einer Zeichenfolge einen Stream als Quelle.
Das erste Argument für JsonSerializer.DeserializeAsync<TValue>(Stream, JsonSerializerOptions,
CancellationToken) ist ein await -Ausdruck. await -Ausdrücke können fast überall in Ihrem Code stehen,
obwohl sie bisher nur als Teil einer Zuweisungsanweisung verwendet wurden. Die anderen beiden
Parameter JsonSerializerOptions und CancellationToken sind optional und werden im Codeausschnitt
ausgelassen.
Die DeserializeAsync -Methode ist generisch. Das bedeutet, dass Sie Typargumente für die Objekttypen
angeben müssen, die aus dem JSON-Text erstellt werden sollen. In diesem Beispiel deserialisieren Sie in
ein List<Repository> . Dabei handelt es sich um ein weiteres generisches Objekt,
System.Collections.Generic.List<T>. Die List<T> -Klasse speichert eine Sammlung von Objekten. Das
Typargument deklariert den Typ der in List<T> gespeicherten Objekte. Das Typargument ist Ihre
Repository -Klasse, da der JSON-Text eine Auflistung von Repositoryobjekten darstellt.

3. Fügen Sie Code hinzu, um die Namen der einzelnen Repositorys anzuzeigen. Ersetzen Sie diese Zeilen:

var msg = await stringTask;


Console.Write(msg);

durch den folgenden Code:

foreach (var repo in repositories)


Console.WriteLine(repo.name);
4. Fügen Sie die folgenden using -Direktiven am Anfang der Datei hinzu:

using System.Collections.Generic;
using System.Text.Json;

5. Führen Sie die App aus.

dotnet run

Die Ausgabe ist eine Liste der Namen der Repositorys, die zur .NET Foundation gehören.

Konfigurieren der Deserialisierung


1. Ändern Sie in repo.cs die name -Eigenschaft in Name , und fügen Sie ein [JsonPropertyName] -Attribut
hinzu, um anzugeben, wie diese Eigenschaft im JSON-Code angezeigt wird.

[JsonPropertyName("name")]
public string Name { get; set; }

2. Fügen Sie den using -Direktiven den System.Text.Json.Serialization-Namespace hinzu:

using System.Text.Json.Serialization;

3. Aktualisieren Sie in Program.cs den Code so, dass er die neue Groß-/Kleinschreibung der Name -
Eigenschaft verwendet:

Console.WriteLine(repo.Name);

4. Führen Sie die App aus.


Die Ausgabe ist identisch.

Gestalten Sie den Code um.


Die ProcessRepositories -Methode kann die asynchrone Arbeit erledigen und eine Auflistung der Repositorys
zurückgeben. Ändern Sie diese Methode, sodass sie List<Repository> zurückgibt, und verschieben Sie den
Code zum Schreiben dieser Informationen in die Main -Methode.
1. Ändern Sie die Signatur von ProcessRepositories , um einen Task zurückzugeben, dessen Ergebnis eine
Liste mit Repository -Objekten ist:

private static async Task<List<Repository>> ProcessRepositories()

2. Geben Sie die Repositorys zurück, nachdem die JSON-Antwort verarbeitet wurde:

var streamTask = client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos");


var repositories = await JsonSerializer.DeserializeAsync<List<Repository>>(await streamTask);
return repositories;

Der Compiler generiert das Task<T> -Objekt für den Rückgabewert, da Sie diese Methode als async
markiert haben.
3. Ändern Sie die Main -Methode, sodass die Ergebnisse erfasst werden und jeder Repositoryname an der
Konsole ausgegeben wird. Die Main -Methode sieht jetzt wie folgt aus:

public static async Task Main(string[] args)


{
var repositories = await ProcessRepositories();

foreach (var repo in repositories)


Console.WriteLine(repo.Name);
}

4. Führen Sie die App aus.


Die Ausgabe ist identisch.

Deserialisieren weiterer Eigenschaften


In den folgenden Schritten fügen Sie Code hinzu, um weitere Eigenschaften im empfangenen JSON-Paket zu
verarbeiten. Wahrscheinlich möchten Sie nicht jede Eigenschaft verarbeiten, aber das Hinzufügen einiger
weiterer veranschaulicht andere Features von C#.
1. Fügen Sie der Repository -Klassendefinition die folgenden Eigenschaften hinzu:

[JsonPropertyName("description")]
public string Description { get; set; }

[JsonPropertyName("html_url")]
public Uri GitHubHomeUrl { get; set; }

[JsonPropertyName("homepage")]
public Uri Homepage { get; set; }

[JsonPropertyName("watchers")]
public int Watchers { get; set; }

Die Typen Uri und int verfügen über integrierte Funktionen zum Konvertieren in und aus
Zeichenfolgendarstellungen. Es ist kein zusätzlicher Code erforderlich, um das JSON-
Zeichenfolgenformat in diese Zieltypen zu deserialisieren. Wenn das JSON-Paket Daten enthält, die nicht
in einen Zieltyp konvertiert werden können, löst die Serialisierungsaktion eine Ausnahme aus.
2. Aktualisieren Sie die Main -Methode, um die Eigenschaftswerte anzuzeigen:

foreach (var repo in repositories)


{
Console.WriteLine(repo.Name);
Console.WriteLine(repo.Description);
Console.WriteLine(repo.GitHubHomeUrl);
Console.WriteLine(repo.Homepage);
Console.WriteLine(repo.Watchers);
Console.WriteLine();
}

3. Führen Sie die App aus.


Die Liste enthält jetzt die zusätzlichen Eigenschaften.

Hinzufügen einer Datumseigenschaft


Das Datum des letzten Pushvorgangs wird in der JSON-Antwort wie folgt formatiert:

2016-02-08T21:27:00Z

Dieses Format entspricht der koordinierten Weltzeit (UTC), sodass das Ergebnis der Deserialisierung ein
DateTime-Wert ist, dessen Kind-Eigenschaft Utc lautet.
Wenn Sie ein Datum in Ihrer Zeitzone bevorzugen, müssen Sie eine benutzerdefinierte Methode für die
Konvertierung schreiben.
1. Fügen Sie in repo.cs eine public -Eigenschaft für die UTC-Darstellung von Datum und Uhrzeit und eine
LastPush readonly -Eigenschaft hinzu, die das konvertierte Datum in Ortszeit zurückgibt:

[JsonPropertyName("pushed_at")]
public DateTime LastPushUtc { get; set; }

public DateTime LastPush => LastPushUtc.ToLocalTime();

Die LastPush -Eigenschaft wird mit einem Ausdruckskörpermember für die get -Zugriffsmethode
definiert. Es gibt keine set -Zugriffsmethode. Das Auslassen der set -Zugriffsmethode ist eine
Möglichkeit, eine schreibgeschützte Eigenschaft in C# zu definieren. (Ja, Sie können lesegeschützte
Eigenschaften in C# erstellen, aber ihr Wert ist begrenzt.)
2. Fügen Sie in Program.cs eine weitere Ausgabeanweisung hinzu:

Console.WriteLine(repo.LastPush);

3. Führen Sie die App aus.


Die Ausgabe enthält das Datum und die Uhrzeit des jeweils letzten Pushs an jedes Repository.

Nächste Schritte
In diesem Tutorial haben Sie eine App erstellt, die Webanforderungen ausführt und die Ergebnisse analysiert.
Ihre Version der App sollte nun dem vollständigen Beispiel entsprechen.
Weitere Informationen zum Konfigurieren der JSON-Serialisierung finden Sie unter Serialisieren und
Deserialisieren (Marshallen und Rückgängigmachen des Marshallens) von JSON-Daten in .NET.
Arbeiten mit LINQ (Language-Integrated Query)
04.11.2021 • 15 minutes to read

Einführung
In diesem Tutorial lernen Sie Features in .NET Core und der Sprache C# kennen. Sie lernen Folgendes:
Generieren von Sequenzen mit LINQ.
Schreiben von Methoden, die sich problemlos in LINQ-Abfragen verwenden lassen.
Unterscheiden zwischen strenger und verzögerter Auswertung.
Sie lernen diese Techniken durch Erstellen einer Anwendung, die eine der grundlegenden Fertigkeiten jedes
Zauberkünstlers demonstriert: den Faro-Shuffle. Ein Faro-Shuffle ist eine Kartenmischtechnik, bei der zwei
Kartenpäckchen exakt so ineinander gefächert werden, dass auf eine Karte des einen Stapels stets eine Karte des
anderen Stapels folgt.
Zauberer verwenden diese Technik, weil sie damit nach jedem Mischen genau wissen, welche Karte sich wo
befindet, und die Reihenfolge ein sich wiederholendes Muster ist.
Im vorliegenden Artikel dient die Technik dazu, das Manipulieren von Datensequenzen mit einem anschaulichen
Beispiel zu zeigen. Die von Ihnen erstellte Anwendung stellt einen Kartenstapel zusammen und führt dann eine
Sequenz aus Mischvorgängen aus, wobei die Sequenz jedes Mal ausgegeben wird. Sie werden auch die
aktualisierte mit der ursprünglichen Reihenfolge vergleichen.
Dieses Tutorial besteht aus vielen Schritten. Sie können die Anwendung nach jedem Schritt ausführen und sich
den Fortschritt ansehen. Sie können sich auch das abgeschlossene Beispiel in unserem Repository
„dotnet/samples“ auf GitHub ansehen. Anweisungen zum Herunterladen finden Sie unter Beispiele und
Lernprogramme.

Voraussetzungen
Sie müssen Ihren Computer zur Ausführung von .NET Core einrichten. Die Installationsanweisungen finden Sie
auf der Downloadseite für .NET Core. Sie können diese Anwendung unter Windows, Ubuntu Linux, OS X oder in
einem Docker-Container ausführen. Sie müssen Ihren bevorzugten Code-Editor installieren. In den folgenden
Beschreibungen wird Visual Studio Code verwendet. Hierbei handelt es sich um einen plattformübergreifenden
Open-Source-Editor. Sie können jedoch auch ein beliebiges anderes Tool verwenden, mit dem Sie vertraut sind.

Erstellen der Anwendung


Im ersten Schritt wird eine neue Anwendung erstellt. Öffnen Sie eine Eingabeaufforderung, und erstellen Sie ein
neues Verzeichnis für Ihre Anwendung. Legen Sie das Verzeichnis als aktuelles Verzeichnis fest. Geben Sie an der
Eingabeaufforderung den Befehl dotnet new console ein. Hierdurch werden die Startdateien für eine einfache
„Hello World“-Anwendung erstellt.
Wenn Sie C# noch nie verwendet haben, erläutert dieses Tutorial die Struktur eines C#-Programms. Sie können
dieses Tutorial lesen und dann zu diesem Artikel zurückkehren, um mehr über LINQ zu erfahren.

Erstellen des Datasets


Bevor Sie beginnen, stellen Sie sicher, dass die folgenden Zeilen am Anfang der Datei Program.cs stehen, die
von dotnet new console generiert wurde:
// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;

Wenn diese drei Zeilen ( using -Anweisungen) nicht am Anfang der Datei stehen, wird unser Programm nicht
kompiliert.
Jetzt haben Sie alle nötigen Referenzen und können die Struktur eines Spielkartenstapels berücksichtigen. In der
Regel besteht ein Spielkartenstapel aus vier Farben, und jede hat dreizehn Werte. In der Regel würden Sie wohl
direkt eine Card -Klasse erstellen und eine Sammlung von Card -Objekten manuell füllen. Mit LINQ können Sie
einen Spielkartenstapel präziser als üblich erstellen. Statt eine Card -Klasse zu erstellen, können Sie zwei
Sequenzen zur Darstellung von Farben und Rängen erstellen. Sie erstellen ein einfaches Paar von
Iteratormethoden, das die Ränge und Farben als IEnumerable<T>s von Zeichenfolgen generiert:

// Program.cs
// The Main() method

static IEnumerable<string> Suits()


{
yield return "clubs";
yield return "diamonds";
yield return "hearts";
yield return "spades";
}

static IEnumerable<string> Ranks()


{
yield return "two";
yield return "three";
yield return "four";
yield return "five";
yield return "six";
yield return "seven";
yield return "eight";
yield return "nine";
yield return "ten";
yield return "jack";
yield return "queen";
yield return "king";
yield return "ace";
}

Platzieren Sie diese unterhalb der Main -Methode in Ihrer Program.cs -Datei. Diese Methoden nutzen beide die
yield return -Syntax, um während der Ausführung eine Sequenz zu erzeugen. Der Compiler erstellt ein Objekt,
das IEnumerable<T>implementiert und die Sequenz aus Zeichenfolgen erst dann generiert, wenn diese
angefordert werden.
Verwenden Sie nun diese Iteratormethoden, um den Spielkartenstapel zu erstellen. Sie platzieren die LINQ-
Abfrage in unserer Main -Methode. Sie sieht wie folgt aus:
// Program.cs
static void Main(string[] args)
{
var startingDeck = from s in Suits()
from r in Ranks()
select new { Suit = s, Rank = r };

// Display each card that we've generated and placed in startingDeck in the console
foreach (var card in startingDeck)
{
Console.WriteLine(card);
}
}

Die verschiedenen from -Klauseln erzeugen SelectMany, wodurch aus der Kombination jedes Elements in der
ersten Sequenz mit jedem Element in der zweiten Sequenz eine einzige Sequenz erstellt wird. Für unsere
Zwecke ist die Reihenfolge wichtig. Das erste Element in der ersten Quellsequenz (Farben) wird mit jedem
Element in der zweiten Sequenz (Ränge) kombiniert. Dadurch werden alle dreizehn Karten der ersten Farbe
erzeugt. Dieser Vorgang wird mit jedem Element in der ersten Sequenz (Farben) wiederholt. Das Ergebnis ist ein
Kartenstapel, der erst nach Farben und innerhalb der Farben nach Werten sortiert ist.
Beachten Sie unbedingt Folgendes: Ob Sie LINQ-Anweisungen in der oben verwendeten Abfragesyntax
schreiben oder stattdessen die Methodensyntax verwenden, Sie können immer von einer Form der Syntax zur
anderen wechseln. Die obige, in der Abfragesyntax geschriebene Abfrage kann in der Methodensyntax
geschrieben werden als:

var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => new { Suit = suit, Rank = rank }));

Der Compiler übersetzt mit der Abfragesyntax geschriebene LINQ-Anweisungen in die entsprechende
Methodenaufrufsyntax. Aus diesem Grund erzielen unabhängig von Ihrer Syntaxwahl die beiden Versionen der
Abfrage das gleiche Ergebnis. Wählen Sie die beste Syntax für Ihre Situation aus: Wenn Sie z.B. in einem Team
arbeiten, in dem einige Mitglieder mit der Methodensyntax Schwierigkeiten haben, versuchen Sie, die
Abfragesyntax zu bevorzugen.
Führen Sie jetzt das erstellte Beispiel aus. Es werden alle 52 Karten des Kartenstapels angezeigt. Es kann sehr
hilfreich sein, dieses Beispiel mit einem Debugger auszuführen, um zu beobachten, wie die Methoden Suits()
und Ranks() ausgeführt werden. Sie können deutlich erkennen, dass jede Zeichenfolge in jeder Sequenz erst
dann erstellt wird, wenn sie benötigt wird.
Ändern der Reihenfolge
Konzentrieren Sie sich nun darauf, wie die Karten im Kartenstapel gemischt werden sollen. Der erste Schritt bei
jedem guten Mischen ist das Aufteilen des Kartenstapels in zwei Hälften. Die zu den LINQ-APIs gehörenden
Methoden Take und Skip stellen Ihnen diese Funktion bereit. Platzieren Sie sie unterhalb der foreach -Schleife:

// Program.cs
public static void Main(string[] args)
{
var startingDeck = from s in Suits()
from r in Ranks()
select new { Suit = s, Rank = r };

foreach (var c in startingDeck)


{
Console.WriteLine(c);
}

// 52 cards in a deck, so 52 / 2 = 26
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
}

In der Standardbibliothek ist jedoch keine Mischmethode vorhanden, Sie müssen sie also selbst schreiben. Die
Mischmethode, die Sie erstellen, veranschaulicht verschiedene Techniken, die Sie mit LINQ-basierten
Programmen verwenden, sodass jeder Teil dieses Prozesses in Schritten erläutert wird.
Um Funktionalität für Ihre Interaktion mit den IEnumerable<T>-Formen, die Sie von einigen LINQ-Abfragen
erhalten, hinzuzufügen, müssen Sie einige besondere Arten von Methoden schreiben, die als
Erweiterungsmethoden bezeichnet werden. Eine Erweiterungsmethode ist im Wesentlichen eine statische
Methode für einen speziellen Zweck, die einem bereits vorhandenen Typ Funktionalität hinzufügt, ohne diesen
ursprünglichen Typ ändern zu müssen.
Fügen Sie für Ihre Erweiterungsmethoden eine neue statische Klassendatei namens Extensions.cs Ihrem
Programm hinzu, und beginnen Sie dann, die erste Erweiterungsmethode zu erstellen:

// Extensions.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqFaroShuffle
{
public static class Extensions
{
public static IEnumerable<T> InterleaveSequenceWith<T>(this IEnumerable<T> first, IEnumerable<T>
second)
{
// Your implementation will go here soon enough
}
}
}

Beachten Sie für einen Moment die Signatur der Methode, insbesondere die Parameter:

public static IEnumerable<T> InterleaveSequenceWith<T> (this IEnumerable<T> first, IEnumerable<T> second)

Sie sehen, dass dem ersten Argument der Methode der this -Modifizierer hinzugefügt wurde. Dies bedeutet,
dass Sie die Methode so aufrufen können, als wäre sie eine Membermethode vom Typ des ersten Arguments.
Diese Methodendeklaration folgt auch einem Standardidiom, bei dem die Eingabe- und Ausgabetypen
IEnumerable<T> sind. Auf diese Weise können LINQ-Methoden miteinander verkettet werden, um komplexere
Abfragen auszuführen.
Wenn Sie den Kartenstapel in zwei Hälften geteilt haben, müssen Sie diese Hälften natürlich auch wieder
zusammenführen. Im Code zählen Sie beide Sequenzen, die Sie durch Take und Skip erhalten haben, gleichzeitig
auf, führen ein interleaving der Elemente durch und erstellen eine einzige Sequenz: Ihr jetzt gemischter
Kartenstapel. Um eine LINQ-Methode schreiben zu können, die mit Sequenzen arbeitet, müssen Sie verstehen,
wie IEnumerable<T> funktioniert.
Die IEnumerable<T>-Schnittstelle weist eine einzige Methode auf: GetEnumerator. Das von GetEnumerator
zurückgegebene Objekt verfügt über eine Methode, um zum nächsten Element zu wechseln, und eine
Eigenschaft, die das aktuelle Element in der Sequenz abruft. Sie verwenden diese beiden Member, um die
Auflistung aufzuzählen und die Elemente zurückzugeben. Diese Interleavemethode ist eine Iteratormethode,
daher verwenden Sie die oben gezeigte yield return -Syntax, anstatt eine Auflistung zu erstellen und die
Auflistung zurückzugeben.
Hier sehen Sie die Implementierung dieser Methode:

public static IEnumerable<T> InterleaveSequenceWith<T>


(this IEnumerable<T> first, IEnumerable<T> second)
{
var firstIter = first.GetEnumerator();
var secondIter = second.GetEnumerator();

while (firstIter.MoveNext() && secondIter.MoveNext())


{
yield return firstIter.Current;
yield return secondIter.Current;
}
}

Nachdem Sie diese Methode geschrieben haben, kehren Sie jetzt zur Main -Methode zurück und mischen den
Kartenstapel ein Mal:

// Program.cs
public static void Main(string[] args)
{
var startingDeck = from s in Suits()
from r in Ranks()
select new { Suit = s, Rank = r };

foreach (var c in startingDeck)


{
Console.WriteLine(c);
}

var top = startingDeck.Take(26);


var bottom = startingDeck.Skip(26);
var shuffle = top.InterleaveSequenceWith(bottom);

foreach (var c in shuffle)


{
Console.WriteLine(c);
}
}

Vergleiche
Wie viele Mischvorgänge sind nötig, damit der Kartenstapel wieder in seiner ursprünglichen Reihenfolge
vorliegt? Um dies herauszufinden, müssen Sie eine Methode schreiben, die ermittelt, ob zwei Sequenzen gleich
sind. Nachdem Sie diese Methode erstellt haben, müssen Sie den Code, der den Stapel mischt, in eine Schleife
platzieren und dann prüfen, wann der Stapel wieder die ursprüngliche Reihenfolge aufweist.
Das Schreiben einer Methode, mit der ermittelt wird, ob die beiden Sequenzen gleich sind, sollte kein Problem
sein. Die Methode hat die gleiche Struktur wie die Methode, die Sie zum Mischen des Kartenstapels geschrieben
haben. Der Unterschied ist, dass dieses Mal nicht für jedes Element ein yield return ausgeführt wird, sondern
dass Sie die übereinstimmenden Elemente jeder Sequenz vergleichen. Wenn die gesamte Sequenz aufgezählt
wurde und alle Elemente übereinstimmen, sind die Sequenzen gleich:

public static bool SequenceEquals<T>


(this IEnumerable<T> first, IEnumerable<T> second)
{
var firstIter = first.GetEnumerator();
var secondIter = second.GetEnumerator();

while (firstIter.MoveNext() && secondIter.MoveNext())


{
if (!firstIter.Current.Equals(secondIter.Current))
{
return false;
}
}

return true;
}

Dies zeigt ein zweites LINQ-Idiom: Terminalmethoden. Diese Methoden akzeptieren eine Sequenz als Eingabe
(bzw. in diesem Fall zwei Sequenzen) und geben einen einzelnen Skalarwert zurück. Terminalmethoden sind
immer die endgültige Methode in einer Kette von Methoden für eine LINQ-Abfrage, daher der Namen
„Terminalmethode“.
Sie können diese Methode in Aktion sehen, wenn Sie sie verwenden, um zu ermitteln, wann der Kartenstapel
wieder die ursprüngliche Reihenfolge aufweist. Platzieren Sie den Code zum Mischen in einer Schleife, und
halten Sie diese an, wenn die Sequenz wieder die ursprüngliche Reihenfolge aufweist. Wenden Sie hierzu die
SequenceEquals() -Methode an. Hier sehen Sie, warum diese Methode immer die letzte in einer Abfrage sein
muss: sie gibt anstelle einer Sequenz einen einzelnen Wert zurück:
// Program.cs
static void Main(string[] args)
{
// Query for building the deck

// Shuffling using InterleaveSequenceWith<T>();

var times = 0;
// We can re-use the shuffle variable from earlier, or you can make a new one
shuffle = startingDeck;
do
{
shuffle = shuffle.Take(26).InterleaveSequenceWith(shuffle.Skip(26));

foreach (var card in shuffle)


{
Console.WriteLine(card);
}
Console.WriteLine();
times++;

} while (!startingDeck.SequenceEquals(shuffle));

Console.WriteLine(times);
}

Führen Sie den bisher erstellten Code aus, und notieren Sie sich, wie der Kartenstapel bei jedem Mischvorgang
neu angeordnet wird. Nach 8 Mischvorgängen (Iterationen der Do-while-Schleife) kehrt der Stapel zur
ursprünglichen Konfiguration zurück, die er hatte, als Sie ihn zum ersten Mal durch Starten der LINQ-Abfrage
erstellt haben.

Optimierungen
Das Beispiel, das Sie bisher erstellt haben, führt einen Mischvorgang nach außen aus, bei dem die oberste und
unterste Karte bei jedem Durchgang gleich bleiben. Als Nächstes führen wir stattdessen einen Mischvorgang
nach innen aus, bei dem alle 52 Karten ihre Position ändern. Hierbei werden die Hälften so ineinander gefächert,
dass die erste Karte der unteren Hälfte zur ersten Karte des Kartenstapels wird. Das bedeutet, dass die unterste
Karte der oberen Hälfte zur untersten Karte des Stapels wird. Dies ist eine einfache Änderung einer einzelnen
Codezeile. Aktualisieren Sie den derzeitigen Mischvorgang durch Wechseln der Positionen von Take und Skip.
Dies ändert die Reihenfolge der oberen und unteren Hälfte des Kartenstapels:

shuffle = shuffle.Skip(26).InterleaveSequenceWith(shuffle.Take(26));

Führen Sie das Programm erneut aus, Sie werden feststellen, dass 52 Iterationen nötig sind, um den
Kartenstapel wieder in die ursprüngliche Reihenfolge zu bringen. Sie werden auch einige erhebliche
Leistungsabfälle beim Ausführen des Programms bemerken.
Dafür gibt es eine Anzahl von Gründen. Sie können eine der wichtigsten Ursachen dieses Leistungsabfalls
bekämpfen: die ineffiziente Nutzung der verzögerten Auswertung.
Der wesentliche Aspekt der verzögerten Auswertung ist, dass eine Anweisung erst dann ausgewertet wird,
wenn der Wert benötigt wird. LINQ-Abfragen sind verzögert ausgewertete Anweisungen. Die Sequenzen
werden erst generiert, wenn die Elemente angefordert werden. Dies ist üblicherweise ein großer Vorteil von
LINQ. In Programmen wie diesem verursacht diese Art der Auswertung jedoch ein exponentielles Wachstum der
Ausführungszeit.
Denken Sie daran, dass wir den ursprünglichen Stapel mithilfe einer LINQ-Abfrage generiert haben. Jede
Mischung wird durch Ausführung dreier LINQ-Abfragen im vorherigen Kartenstapel generiert. All diese werden
verzögert ausgeführt. Das bedeutet auch, dass sie bei jeder Anforderung der Sequenz erneut ausgeführt
werden. Zum Zeitpunkt der 52. Iteration haben Sie den ursprünglichen Stapel also sehr häufig neu generiert.
Nun schreiben wir ein Protokoll, das dieses Verhalten veranschaulicht. Danach werden wir das Problem
beheben.
Geben Sie die folgende Methode in Ihre Extensions.cs -Datei ein, bzw. kopieren Sie sie hinein. Diese
Erweiterungsmethode erstellt eine neue Datei namens debug.log in Ihrem Projektverzeichnis, und zeichnet in
der Protokolldatei auf, welche Abfrage derzeit ausgeführt wird. Diese Erweiterungsmethode kann jeder Abfrage
angefügt werden, um diese Abfrage als „ausgeführt“ zu kennzeichnen.

public static IEnumerable<T> LogQuery<T>


(this IEnumerable<T> sequence, string tag)
{
// File.AppendText creates a new file if the file doesn't exist.
using (var writer = File.AppendText("debug.log"))
{
writer.WriteLine($"Executing Query {tag}");
}

return sequence;
}

Dann werden unter File rote Wellenlinien angezeigt. Das bedeutet, dass dieses Element nicht vorhanden ist. Es
erfolgt keine Kompilierung, da der Compiler nicht weiß, worum es sich bei File handelt. Sie können dieses
Problem lösen, indem Sie die folgende Codezeile unter die erste Zeile der Datei Extensions.cs einfügen:

using System.IO;

Dadurch sollten das Problem behoben und die roten Wellenlinien entfernt werden.
Als Nächstes instrumentieren Sie die Definition jeder Abfrage mit einer Protokollmeldung:
// Program.cs
public static void Main(string[] args)
{
var startingDeck = (from s in Suits().LogQuery("Suit Generation")
from r in Ranks().LogQuery("Rank Generation")
select new { Suit = s, Rank = r }).LogQuery("Starting Deck");

foreach (var c in startingDeck)


{
Console.WriteLine(c);
}

Console.WriteLine();
var times = 0;
var shuffle = startingDeck;

do
{
// Out shuffle
/*
shuffle = shuffle.Take(26)
.LogQuery("Top Half")
.InterleaveSequenceWith(shuffle.Skip(26)
.LogQuery("Bottom Half"))
.LogQuery("Shuffle");
*/

// In shuffle
shuffle = shuffle.Skip(26).LogQuery("Bottom Half")
.InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
.LogQuery("Shuffle");

foreach (var c in shuffle)


{
Console.WriteLine(c);
}

times++;
Console.WriteLine(times);
} while (!startingDeck.SequenceEquals(shuffle));

Console.WriteLine(times);
}

Beachten Sie, dass beim Zugreifen auf eine Abfrage kein Protokolleintrag erstellt wird. Ein Protokolleintrag wird
nur erstellt, wenn Sie die ursprüngliche Abfrage erstellen. Die Ausführung des Programms dauert immer noch
lang, aber nun sehen Sie den Grund dafür. Wenn Sie nicht warten möchten, bis das Mischen nach innen mit
aktivierter Protokollierung ausgeführt wurde, wechseln Sie zurück zum Mischen nach außen. Sie sehen immer
noch die Auswirkungen der verzögerten Auswertung. In einer Ausführung werden 2592 Abfragen ausgeführt,
einschließlich der Generierung aller Werte und Farben.
Sie können hier die Leistung des Codes verbessern, um die Anzahl der Ausführungen zu reduzieren, die Sie
vornehmen. Eine einfache Lösung ist das Zwischenspeichern der Ergebnisse der ursprünglichen LINQ-Abfrage,
die den Kartenstapel erstellt. Derzeit führen Sie die Abfragen jedes Mal erneut aus, wenn die Do-while-Schleife
eine Iteration durchläuft, wobei Sie jedes Mal den Kartenstapel neu erstellen und mischen. Zum
Zwischenspeichern des Kartenstapels können Sie die LINQ-Methoden ToArray und ToList nutzen; wenn Sie sie
an die Abfragen anfügen, führen sie die gleichen Aktionen aus, für die Sie sie programmiert haben, aber jetzt
speichern sie die Ergebnisse in einem Array oder einer Liste, je nachdem, welche Methode Sie auswählen. Fügen
Sie die LINQ-Methode ToArray an beide Abfragen an, und führen Sie das Programm erneut aus:
public static void Main(string[] args)
{
var startingDeck = (from s in Suits().LogQuery("Suit Generation")
from r in Ranks().LogQuery("Value Generation")
select new { Suit = s, Rank = r })
.LogQuery("Starting Deck")
.ToArray();

foreach (var c in startingDeck)


{
Console.WriteLine(c);
}

Console.WriteLine();

var times = 0;
var shuffle = startingDeck;

do
{
/*
shuffle = shuffle.Take(26)
.LogQuery("Top Half")
.InterleaveSequenceWith(shuffle.Skip(26).LogQuery("Bottom Half"))
.LogQuery("Shuffle")
.ToArray();
*/

shuffle = shuffle.Skip(26)
.LogQuery("Bottom Half")
.InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
.LogQuery("Shuffle")
.ToArray();

foreach (var c in shuffle)


{
Console.WriteLine(c);
}

times++;
Console.WriteLine(times);
} while (!startingDeck.SequenceEquals(shuffle));

Console.WriteLine(times);
}

Jetzt ist der Mischvorgang nach außen auf 30 Abfragen reduziert. Auch beim Mischen nach innen werden Sie
ähnliche Verbesserungen feststellen: Jetzt werden 162 Abfragen ausgeführt.
Bitte beachten Sie: Dieses Beispiel sollte nur Anwendungsfälle veranschaulichen, bei denen eine verzögerte
Auswertung zu Leistungsproblemen führen kann. Es ist wichtig, festzustellen, wo die verzögerte Auswertung die
Codeleistung beeinträchtigen kann, aber es ist gleichermaßen wichtig, zu verstehen, dass nicht alle Abfragen
strikt ausgeführt werden sollten. Ursache der Leistungseinbußen, die auftreten, wenn Sie ToArray nicht
verwenden, ist, dass jede neue Anordnung des Kartenstapels aus der vorherigen Anordnung erstellt wird. Bei
der verzögerten Auswertung bedeutet dies, dass jede neue Kartenstapelkonfiguration aus dem ursprünglichen
Kartenstapel erzeugt wird. Dabei wird sogar der Code ausgeführt, der den startingDeck -Stapel erstellt hat. Das
verursacht eine Menge zusätzlichen Aufwands.
In der Praxis werden manche Algorithmen gut mit der strikten Auswertung ausgeführt, für andere eignet sich
die verzögerte Auswertung besser. Für die routinemäßige Nutzung eignet sich die verzögerte Auswertung in
der Regel besser, wenn es sich bei der Datenquelle um einen separaten Prozess handelt, beispielsweise um eine
Datenbank-Engine. Bei Datenbanken ermöglicht die verzögerte Auswertung komplexere Abfragen, um nur
einen Roundtrip zum Datenbankprozess und zurück zu Ihrem übrigen Code auszuführen. LINQ ist flexibel,
unabhängig davon, ob Sie die verzögerte oder strikte Auswertung verwenden, also wägen Sie Ihre Prozesse ab,
und wählen Sie die Art der Auswertung aus, die Ihnen die beste Leistung bietet.

Zusammenfassung
In diesem Projekt wurde Folgendes behandelt:
Verwendung von LINQ-Abfragen zum Aggregieren von Daten in einer sinnvollen Abfolge
Schreiben von Erweiterungsmethoden zum Hinzufügen eigener benutzerdefinierter Funktionalität zu LINQ-
Abfragen
Lokalisieren von Bereichen in unserem Code, in denen LINQ-Abfragen zu Leistungsproblemen wie
Geschwindigkeitseinbußen führen können
verzögerte und strikte Auswertung im Hinblick auf die LINQ-Abfragen und die möglichen Auswirkungen auf
die Abfrageleistung
Abgesehen von LINQ haben Sie ein wenig über die Verfahren gelernt, die Zauberer für Kartentricks anwenden.
Zauberer verwenden den Faro-Shuffle, weil sie damit genau steuern können, wann sich welche Karte wo im
Stapel befindet. Verderben Sie mit Ihrem Wissen jetzt aber nicht anderen den Spaß!
Weitere Informationen zu LINQ finden Sie unter:
Language-Integrated Query (LINQ)
Einführung in LINQ
Grundlegende LINQ-Abfragevorgänge (C#)
Datentransformationen mit LINQ (C#)
Abfragesyntax und Methodensyntax in LINQ (C#)
C#-Funktionen mit LINQ-Unterstützung
Verwenden von Attributen in C#
04.11.2021 • 7 minutes to read

Attribute bieten die Möglichkeit, Informationen in deklarativer Form mit Code zu verknüpfen. Attribute können
außerdem als wiederverwendbare Elemente genutzt werden, die auf eine Vielzahl von Zielen angewendet
werden können.
Betrachten wir z.B. das [Obsolete] -Attribut. Es kann auf Klassen, Strukturen, Methoden, Konstruktoren und
mehr angewendet werden. Mit ihm wird das Element als veraltet deklariert. Anschließend ist es die Aufgabe des
C#-Compilers, nach diesem Attribut zu suchen und Aktionen auszuführen.
In diesem Tutorial erfahren Sie, wie Sie Attribute zu Ihrem Code hinzufügen sowie eigene Attribute erstellen und
verwenden, und Sie lernen einige der Attribute kennen, die in .NET Core integriert sind.

Voraussetzungen
Sie müssen Ihren Computer zur Ausführung von .NET Core einrichten. Die Installationsanweisungen finden Sie
auf der Seite .NET Core-Downloads. Sie können diese Anwendung unter Windows, Ubuntu Linux, macOS oder in
einem Docker-Container ausführen. Sie müssen Ihren bevorzugten Code-Editor installieren. In den folgenden
Beschreibungen wird Visual Studio Code verwendet. Hierbei handelt es sich um einen plattformübergreifenden
Open Source-Editor. Sie können jedoch auch ein beliebiges anderes Tool verwenden, mit dem Sie vertraut sind.

Erstellen der Anwendung


Nachdem Sie alle Tools installiert haben, erstellen Sie eine neue .NET Core-Anwendung. Um den
Befehlszeilengenerator zu verwenden, führen Sie den folgenden Befehl in Ihrer bevorzugten Shell aus:
dotnet new console

Mit diesem Befehl werden .NET Core-Basisprojektdateien erstellt. Sie müssen dotnet restore ausführen, um die
Abhängigkeiten wiederherzustellen, die zum Kompilieren dieses Projekts erforderlich sind.
Sie müssen dotnet restore nicht ausführen, da der Befehl implizit von allen Befehlen ausgeführt wird, die eine
Wiederherstellung erfordern. Zu diesen zählen z. B. dotnet new , dotnet build , dotnet run , dotnet test ,
dotnet publish und dotnet pack . Verwenden Sie die Option --no-restore , um die implizite Wiederherstellung
zu deaktivieren.
In bestimmten Fällen eignet sich der dotnet restore -Befehl dennoch. Dies ist etwa bei Szenarios der Fall, in
denen die explizite Wiederherstellung sinnvoll ist. Beispiele hierfür sind Continuous Integration-Builds in Azure
DevOps Services oder Buildsysteme, die den Zeitpunkt für die Wiederherstellung explizit steuern müssen.
Informationen zum Verwalten von NuGet-Feeds finden Sie in der dotnet restore Dokumentation.
Verwenden Sie zum Ausführen des Programms dotnet run . Es sollte „Hello, World“ auf der Konsole
ausgegeben werden.

Hinzufügen von Attributen zum Code


In C# sind Attribute Klassen, die von der Attribute -Basisklasse erben. Alle Klassen, die von Attribute erben,
können als eine Art von „Tag“ für andere Codeelemente verwendet werden. Beispielsweise gibt es das Attribut
ObsoleteAttribute . Mit diesem Attribut wird gekennzeichnet, dass der Code veraltet ist und nicht mehr
verwendet werden sollte. Sie können eine Klasse beispielsweise mit diesem Attribut markieren, indem Sie eckige
Klammern verwenden.

[Obsolete]
public class MyClass
{
}

Obwohl der Name der Klasse ObsoleteAttribute lautet, kann im Code nur [Obsolete] verwendet werden. Dies
ist eine Konvention von C#. Wenn Sie möchten, können Sie auch den vollständigen Namen,
[ObsoleteAttribute] , verwenden.

Wenn Sie eine Klasse als veraltet markieren, sollten Sie Informationen dazu geben, warum die Klasse veraltet ist
und/oder was stattdessen verwendet werden sollte. Um dies zu tun, übergeben Sie einen
Zeichenfolgenparameter an das Obsolete-Attribut.

[Obsolete("ThisClass is obsolete. Use ThisClass2 instead.")]


public class ThisClass
{
}

Die Zeichenfolge wird als Argument an einen -Konstruktor übergeben, so als ob Sie
ObsoleteAttribute
var attr = new ObsoleteAttribute("some string") schreiben würden.
Die Parameter für einen Attributkonstruktor sind auf einfache Typen/Literale beschränkt:
bool, int, double, string, Type, enums, etc und Arrays dieser Typen. Sie können keine Ausdrücke oder
Variablen verwenden. Es ist möglich, Positionsparameter oder benannte Parameter einzusetzen.

Erstellen eigener Attribute


Das Erstellen eines Attributs ist so einfach wie das Erben von der Attribute -Basisklasse.

public class MySpecialAttribute : Attribute


{
}

Durch den Code oben kann jetzt [MySpecial] (oder [MySpecialAttribute] ) als Attribut an anderer Stelle in der
Codebasis verwendet werden.

[MySpecial]
public class SomeOtherClass
{
}

Attribute in der .NET-Basisklassenbibliothek, z.B. ObsoleteAttribute , lösen ein bestimmtes Verhalten im


Compiler aus. Die erstellten Attribute dienen jedoch nur als Metadaten, sie führen nicht zu Code innerhalb der
Attributklasse, der ausgeführt wird. Es liegt an Ihnen, diese Metadaten an anderer Stelle im Code einzusetzen
(hierzu später mehr in diesem Tutorial).
Es gibt hierbei ein Problem, das beachtet werden muss. Wie oben erwähnt, können bei der Verwendung von
Attributen nur bestimmte Typen als Argumente übergeben werden. Wenn Sie aber einen Attributtyp erstellen,
hindert der C#-Compiler Sie nicht daran, diese Parameter zu erstellen. Im nachstehenden Beispiel habe ich ein
Attribut mit einem Konstruktor erstellt, der problemlos kompiliert werden kann.
public class GotchaAttribute : Attribute
{
public GotchaAttribute(Foo myClass, string str) {
}
}

Dieser Konstruktor kann aber nicht mit der Attributsyntax verwendet werden.

[Gotcha(new Foo(), "test")] // does not compile


public class AttributeFail
{
}

Der obige Code führt zu einem Compilerfehler wie diesem:


Attribute constructor parameter 'myClass' has type 'Foo', which is not a valid attribute parameter type

Einschränken der Attributverwendung


Attribute können für verschiedene Ziele verwendet werden. In den obigen Beispielen wurde die Verwendung für
Klassen gezeigt, aber Attribute können auch für folgende Elemente verwendet werden:
Assembly
Klasse
Konstruktor
Delegat
Enumeration
Ereignis
Feld
GenericParameter
Schnittstelle
Methode
Modul
Parameter
Eigenschaft
ReturnValue
Struktur
Wenn Sie eine Attributklasse erstellen, lässt C# standardmäßig die Verwendung dieses Attributs für alle
möglichen Attributziele zu. Wenn Sie Ihr Attribut auf bestimmte Ziele beschränken möchten, erreichen Sie dies
durch Verwendung von AttributeUsageAttribute für Ihre Attributklasse. Ganz richtig, ein Attribut für ein
Attribut!

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class MyAttributeForClassAndStructOnly : Attribute
{
}

Wenn Sie versuchen, das oben gezeigte Attribut für ein Element zu verwenden, bei dem es sich nicht um eine
Klasse oder eine Struktur handelt, erhalten Sie einen Compilerfehler wie diesen:
Attribute 'MyAttributeForClassAndStructOnly' is not valid on this declaration type. It is only valid on
'class, struct' declarations
public class Foo
{
// if the below attribute was uncommented, it would cause a compiler error
// [MyAttributeForClassAndStructOnly]
public Foo()
{ }
}

Verwenden von Attributen, die an Codeelemente angefügt sind


Attribute fungieren als Metadaten. Ohne Aktivität von außen bewirken sie zunächst nichts.
Um nach Attributen zu suchen und Aktionen auszuführen, ist im Allgemeinen eine Reflektion erforderlich. In
diesem Tutorial wird nicht ausführlich auf die Reflektion eingegangen. Die Grundidee hierbei ist jedoch folgende:
Mithilfe der Reflektion ist es möglich, Code in C# zu schreiben, mit dem anderer Code untersucht wird.
Sie können beispielsweise mithilfe der Reflektion Informationen zu einer Klasse abrufen (fügen Sie
using System.Reflection; im Kopf Ihres Codes hinzu):

TypeInfo typeInfo = typeof(MyClass).GetTypeInfo();


Console.WriteLine("The assembly qualified name of MyClass is " + typeInfo.AssemblyQualifiedName);

Es wird eine Ausgabe ähnlich der folgenden erzeugt:


The assembly qualified name of MyClass is ConsoleApplication.MyClass, attributes, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null

Sobald Sie über ein -Objekt (oder MemberInfo , FieldInfo usw.) verfügen, können Sie die
TypeInfo
GetCustomAttributes -Methode verwenden. Dadurch wird eine Auflistung von Attribute -Objekte
zurückgegeben. Sie können auch GetCustomAttribute verwenden und einen Attributtyp angeben.
Hier ein Beispiel zur Verwendung von GetCustomAttributes für eine MemberInfo -Instanz für MyClass (die, wie
zuvor gezeigt, über ein [Obsolete] -Attribut verfügt).

var attrs = typeInfo.GetCustomAttributes();


foreach(var attr in attrs)
Console.WriteLine("Attribute on MyClass: " + attr.GetType().Name);

Dies führt zur folgenden Konsolenausgabe: Attribute on MyClass: ObsoleteAttribute . Versuchen Sie, weitere
Attribute zu MyClass hinzuzufügen.
Es ist wichtig, darauf hinzuweisen, dass diese Attribute -Objekte verzögert instanziiert werden. Anders
ausgedrückt: Sie werden erst instanziiert, wenn Sie GetCustomAttribute oder GetCustomAttributes verwenden.
Sie werden außerdem jedes Mal instanziiert. Das zweimalige Aufrufen von GetCustomAttributes in einer Zeile
führt zur Rückgabe von zwei unterschiedlichen Instanzen von ObsoleteAttribute .

Allgemeine Attribute in der Basisklassenbibliothek


Attribute werden in vielen Tools und Frameworks verwendet. NUnit verwendet Attribute wie z.B. [Test] und
[TestFixture] , die vom NUnit Test Runner verwendet werden. ASP.NET MVC verwendet Attribute wie
[Authorize] und stellt ein Aktionsfilterframework bereit, um übergreifende Anforderungen (Cross-Cutting
Concerns) für MVC-Aktionen umzusetzen. PostSharp verwendet die Attributsyntax, um eine aspektorientierte
Programmierung in C# zu ermöglichen.
Nachfolgend werden einige wichtige Attribute aufgeführt, die in die .NET Core-Basisklassenbibliotheken
integriert sind:
[Obsolete] . Dieses Attribut wurde in den obigen Beispielen verwendet, es ist im System -Namespace
enthalten. Es ist nützlich, um eine deklarative Dokumentation für eine sich ändernde Codebasis
bereitzustellen. Eine Meldung kann in Form einer Zeichenfolge bereitgestellt werden, und ein weiterer,
boolescher Parameter kann für eine Eskalation von einer Compilerwarnung zu einem Compilerfehler
verwendet werden.
[Conditional] . Dieses Attribut ist im System.Diagnostics -Namespace enthalten. Es kann auf Methoden
(oder Attributklassen) angewendet werden. Sie müssen eine Zeichenfolge an den Konstruktor übergeben.
Wenn diese Zeichenfolge nicht einer #define -Anweisung entspricht, werden alle Aufrufe dieser Methode
(aber nicht die Methode selbst) durch den C#-Compiler entfernt. Dieses Attribut wird typischerweise zum
Debuggen (zu Diagnosezwecken) eingesetzt.
[CallerMemberName] . Dieses Attribut kann für Parameter verwendet werden und ist im
System.Runtime.CompilerServices -Namespace enthalten. Es handelt sich um ein Attribut, mit dem der
Name der Methode eingeführt wird, die eine andere Methode aufruft. So werden typischerweise
„magische Zeichenfolgen“ beim Implementieren von „INotifyPropertyChanged“ in verschiedenen UI-
Frameworks beseitigt. Beispiel:

public class MyUIClass : INotifyPropertyChanged


{
public event PropertyChangedEventHandler PropertyChanged;

public void RaisePropertyChanged([CallerMemberName] string propertyName = null)


{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

private string _name;


public string Name
{
get { return _name;}
set
{
if (value != _name)
{
_name = value;
RaisePropertyChanged(); // notice that "Name" is not needed here explicitly
}
}
}
}

Im Code oben ist keine literale "Name" -Zeichenfolge erforderlich. Dies kann dazu beitragen, Programmfehler
aufgrund von Rechtschreibfehlern zu vermeiden, und es erleichtert das Refactoring/Umbenennen.

Zusammenfassung
Attribute verschaffen C# ein deklaratives Potenzial, aber sie sind eine Metadatenform von Code und agieren
nicht selbst.
Nullwerte zulassende Verweistypen
04.11.2021 • 14 minutes to read

C# 8.0 führt Features ein, die Sie verwenden können, um die Wahrscheinlichkeit zu minimieren, dass Ihr Code
bewirkt, dass die Runtime System.NullReferenceException auslöst. Es gibt drei Features, mit denen Sie diese
Ausnahmen vermeiden können.
Verbesserte statische Flussanalyse, die bestimmt, ob eine Variable null sein kann, bevor sie dereferenziert
wird.
Attribute, die APIs mit Anmerkungen kommentieren, sodass die Flussanalyse den NULL-Status bestimmt.
Variablenanmerkungen, die Entwickler verwenden, um den beabsichtigten NULL-Zustand für eine Variable
explizit zu deklarieren.
Im restlichen Teil dieses Artikels wird beschrieben, wie diese drei Featurebereiche funktionieren, um Warnungen
zu generieren, wenn Ihr Code möglicherweise einen null -Wert dereferenzier t . Das Dereferenzieren einer
Variablen bedeutet, mithilfe des . -Operators (Punkt) auf einen ihrer Member zuzugreifen, wie im folgenden
Beispiel gezeigt:

string message = "Hello, World!";


int length = message.Length; // dereferencing "message"

Wenn Sie eine Variable dereferenzieren, deren Wert null ist, löst die Laufzeit eine
System.NullReferenceException aus.

Analyse des NULL-Status


*Analyse des NULL-Status verfolgt den NULL-Status von Verweisen nach. Diese statische Analyse gibt
Warnungen aus, wenn Ihr Code möglicherweise null dereferenziert. Sie können diese Warnungen behandeln,
um die Fälle zu minimieren, in denen die Laufzeit eine System.NullReferenceException auslöst. Der Compiler
verwendet statische Analyse, um den NULL-Status einer Variablen zu bestimmen. Eine Variable ist entweder not-
null oder maybe-null. Der Compiler bestimmt auf zwei Arten, dass eine Variable nicht not-null ist:
1. Die Variable wurde einem Wert zugewiesen, der bekanntermaßen nicht NULL ist.
2. Die Variable wurde mit null überprüft und seit dieser Überprüfung nicht mehr geändert.
Jede Variable, die der Compiler nicht als not-null bestimmt hat, wird als maybe-null betrachtet. Die Analyse
liefert Warnungen in Situationen, in denen Sie einen null -Wert versehentlich dereferenzieren können. Der
Compiler generiert Warnungen basierend auf dem NULL-Status.
Wenn eine Variable not-null ist, kann diese Variable sicher dereferenziert werden.
Wenn eine Variable maybe-null ist, muss diese Variable überprüft werden, um sicherzustellen, dass sie vor
der Deferenzierung nicht null ist.
Betrachten Sie das folgenden Beispiel:
string message = null;

// warning: dereference null.


Console.WriteLine($"The length of the message is {message.Length}");

var originalMessage = message;


message = "Hello, World!";

// No warning. Analysis determined "message" is not null.


Console.WriteLine($"The length of the message is {message.Length}");

// warning!
Console.WriteLine(originalMessage.Length);

Im vorherigen Beispiel bestimmt der Compiler, dass message maybe-null ist, wenn die erste Meldung
ausgegeben wird. Für die zweite Meldung wird keine Warnung angezeigt. Die letzte Codezeile generiert eine
Warnung, weil originalMessage möglicherweise NULL ist. Das folgende Beispiel zeigt eine praktischere
Verwendung, um eine Struktur von Knoten bis zum Stamm zu durchlaufen und jeden Knoten während des
Durchlaufs zu verarbeiten:

void FindRoot(Node node, Action<Node> processNode)


{
for (var current = node; current != null; current = current.Parent)
{
processNode(current);
}
}

Der Code oben generiert keine Warnungen zum Dereferenzieren der Variablen current . Die statische Analyse
bestimmt, dass current nie dereferenziert wird, wenn der Wert maybe-null ist. Die Variable current wird
anhand von null überprüft, bevor auf current.Parent zugegriffen und current an die Aktion ProcessNode
übergeben wird. Die vorherigen Beispiele zeigen, wie der Compiler den NULL-Status für lokale Variablen
bestimmt, wenn initialisiert, zugewiesen oder mit null verglichen wird.

Attribute für API-Signaturen


Die NULL-Statusanalyse benötigt Hinweise von Entwicklern, um die Semantik von APIs zu verstehen. Einige APIs
bieten NULL-Überprüfungen und sollten den null-state einer Variablen von maybe-null in not-null ändern.
Andere APIs geben Ausdrücke zurück, die not-null oder maybe-null sind, je nach dem null-state der
Eingabeargumente. Sehen Sie sich beispielsweise den folgenden Code an, der eine Meldung anzeigt:

public void PrintMessage(string message)


{
if (!string.IsNullOrWhiteSpace(message))
{
Console.WriteLine($"{DateTime.Now}: {message}");
}
}

Basierend auf der Überprüfung würde jeder Entwickler diesen Code als sicher betrachten, und er sollte keine
Warnungen generieren. Der Compiler weiß nicht, dass IsNullOrWhiteSpace eine NULL-Überprüfung bereitstellt.
Sie wenden Attribute an, um den Compiler darüber zu informieren, dass message not-null ist, wenn
IsNullOrWhiteSpace false zurückgibt (und nur dann). Im vorherigen Beispiel enthält die Signatur NotNullWhen ,
um den NULL-Status von message anzugeben:
public static bool IsNullOrWhiteSpace([NotNullWhen(false)] string message);

Attribute bieten ausführliche Informationen zum NULL-Status von Argumenten, Rückgabewerten und Membern
der Objektinstanz, die zum Aufrufen eines Members verwendet werden. Die Details zu den einzelnen Attributen
finden Sie im Sprachreferenzartikel zu Nullable-Verweisattributen. Alle .NET-Runtime-APIs wurden in .NET 5 mit
Anmerkungen kommentiert. Sie verbessern die statische Analyse, indem Sie Ihre APIs kommentieren, um
semantische Informationen zum null-state von Argumenten und Rückgabewerten zu liefern.

Nullable-Variablenanmerkungen
Die null-state-Analyse bietet stabile Analysen für die meisten Variablen. Der Compiler benötigt weitere
Informationen von Ihnen für Membervariablen. Der Compiler kann keine Annahmen über die Reihenfolge
treffen, in der auf öffentliche Member zugegriffen wird. Auf alle öffentlichen Member könnte in beliebiger
Reihenfolge zugegriffen werden. Jeder der zugreifbaren Konstruktoren kann verwendet werden, um das Objekt
zu initialisieren. Wenn ein Memberfeld jemals auf null festgelegt werden kann, muss der Compiler davon
ausgehen, dass sein null-status zu Beginn jeder Methode maybe-null ist.
Sie verwenden Anmerkungen, die deklarieren können, ob eine Variable ein Nullable-Ver weistyp oder ein
Nicht-Nullable-Ver weistyp ist. Diese Anmerkungen enthalten wichtige Anweisungen zum null-state für
Variablen:
Ein Ver weis darf nicht NULL sein . Der Standardstatus einer keine Nullwerte zulassenden Verweisvariable
ist nicht-null. Der Compiler erzwingt Regeln, die sicherstellen, dass das Dereferenzieren dieser Variablen
sicher ist, ohne zuerst zu überprüfen, ob sie nicht NULL sind:
Die Variable muss mit einem Wert ungleich NULL initialisiert werden.
Der Variablen kann nie der Wert null zugewiesen werden. Der Compiler gibt eine Warnung aus,
wenn der Code einen maybe-null-Ausdruck einer Variablen zuweist, die nicht NULL sein sollte.
Ein Ver weis darf NULL sein . Der Standardstatus einer Nullwerte zulassenden Verweisvariable ist maybe-
null. Der Compiler erzwingt Regeln, um sicherzustellen, dass Sie ordnungsgemäß auf einen null -Verweis
überprüft haben:
Die Variable kann nur dereferenziert werden, wenn der Compiler garantieren kann, dass der Wert
nicht null ist.
Diese Variablen können mit dem Standardwert null initialisiert und in anderem Code dem Wert
null zugewiesen werden.
Der Compiler gibt keine Warnungen aus, wenn Code einer Variablen, die NULL sein kann, einen
maybe-null-Ausdruck zuweist.
Jede Verweisvariable, die nicht null sein soll, weist den null-state not-null auf. Jede Verweisvariable, die
anfänglich null sein kann, hat den null-state maybe-null.
Ein Nullable-Ver weistyp wird mithilfe der gleichen Syntax wie Nullable-Werttypen aufgeführt: Ein ? wird an
den Variablentyp angefügt. Beispielsweise stellt die folgende Variablendeklaration eine Nullable-
Zeichenfolgenvariable, name , dar:

string? name;

Bei jeder Variable, bei der ? nicht an den Typnamen angefügt ist, handelt es sich um einen Non-Nullable-
Ver weistyp . Dies umfasst alle Verweistypvariablen in vorhandenem Code, wenn Sie dieses Feature aktiviert
haben. Alle implizit typisierten lokalen Variablen (die mit var deklariert wurden) sind Nullable-
Ver weistypen . Wie in den vorherigen Abschnitten gezeigt, bestimmt die statische Analyse den null-state
lokaler Variablen, um zu ermitteln, ob sie maybe-null sind.
Manchmal müssen Sie eine Warnung überschreiben, wenn Sie wissen, dass eine Variable nicht NULL ist, der
Compiler aber bestimmt, dass der null-state maybe-null ist. Sie verwenden den NULL-toleranten Operator !
nach einem Variablennamen, um zu erzwingen, dass der null-state not-null ist. Wenn Sie beispielsweise wissen,
dass die Variable name nicht null ist, der Compiler aber eine Warnung ausgibt, können Sie folgenden Code
schreiben, um die Analyse des Compilers zu überschreiben:

name!.Length;

Nullable-Verweistypen und Nullable-Werttypen bieten ein ähnliches semantisches Konzept: Eine Variable kann
einen Wert oder ein Objekt darstellen, oder diese Variable kann null sein. Nullable-Verweistypen und
Nullable-Werttypen werden jedoch unterschiedlich implementiert: Nullable-Werttypen werden mit
System.Nullable<T> implementiert, und Nullable-Verweistypen werden durch Attribute implementiert, die vom
Compiler gelesen werden. string? und string werden z. B. beide durch den gleichen Typ dargestellt:
System.String. int? und int werden jedoch durch System.Nullable<System.Int32> bzw. System.Int32
dargestellt.

Generics
Generics erfordern detaillierte Regeln zur Behandlung von T? für jeden Typparameter T . Die Regeln sind
aufgrund des bisherigen Verlaufs und der unterschiedlichen Implementierung für einen Nullwerte zulassende
Werttyp und einen Nullwerte zulassende Verweistyp notwendigerweise ausführlich. Nullwerte zulassende
Werttypen werden mit der Struktur System.Nullable<T> implementiert. Nullwerte zulassende Verweistypen
werden als Typ-Anmerkungen implementiert, die dem Compiler semantische Regeln vorgeben.
In C# 8.0 wurde die Verwendung von T? ohne die Einschränkung, dass T ein struct oder ein class sein
muss, nicht kompiliert. Dies ermöglichte es dem Compiler, T? eindeutig zu interpretieren. Diese Einschränkung
wurde in C# 9.0 aufgehoben, indem die folgenden Regeln für einen nicht eingeschränkten Typparameter T
definiert wurden:
Wenn das Typargument für T ein Verweistyp ist, verweist T? auf den entsprechenden Nullwerte
zulassenden Verweistyp. Wenn zum Beispiel T ein string ist, dann ist T? ein string? .
Wenn das Typargument für T ein Wertetyp ist, verweist T? auf denselben Wertetyp, T . Wenn zum
Beispiel T ein int ist, ist auch T? ein int .
Wenn das Typargument für T ein löschbarer Verweistyp ist, verweist T? auf denselben löschbaren
Verweistyp. Wenn zum Beispiel T ein string? ist, dann ist T? auch ein string? .
Wenn das Typargument für T ein löschbarer Werttyp ist, verweist T? auf denselben löschbaren Werttyp.
Wenn zum Beispiel T ein int? ist, dann ist T? auch ein int? .

Bei Rückgabewerten ist T? äquivalent zu [MaybeNull]T ; für Argumentwerte ist T? äquivalent zu [AllowNull]T
. Weitere Informationen finden Sie im Artikel Attribute für die Nullzustandsanalyse in der Sprachreferenz.
Mit Einschränkungen können Sie ein anderes Verhalten festlegen:
Die class -Einschränkung bedeutet, dass T ein keine Nullwerte zulassender Verweistyp sein muss (z. B.
string ). Der Compiler gibt eine Warnung aus, wenn Sie einen Nullwerte zulassenden Verweistyp
verwenden, z. B. string? für T .
Die class? -Einschränkung bedeutet, dass T ein Verweistyp sein muss, entweder ein keine Nullwerte
zulassender ( string ) oder ein Nullwerte zulassender Verweistyp (z. B. string? ). Wenn der Typparameter
ein löschbarer Verweistyp ist, z. B. string? , verweist ein Ausdruck von T? auf denselben löschbaren
Verweistyp, z. B. string? .
Die notnull -Einschränkung bedeutet, dass T ein Non-Nullable-Verweistyp oder ein Non-Nullable-Werttyp
sein muss. Wenn Sie einen Nullwerte zulassende Verweistyp oder einen Nullwerte zulassende Werttyp für
den Typparameter verwenden, generiert der Compiler eine Warnung. Wenn T ein Werttyp ist, ist der
Rückgabewert dieser Werttyp und nicht der entsprechende löschbare Werttyp.
Diese Einschränkungen helfen dem Compiler, weitere Informationen zur Verwendung von T zu erhalten. Dies
hilft, wenn Entwickler den Typ für T auswählen, und bietet eine bessere null-state-Analyse, wenn eine Instanz
des generischen Typs verwendet wird.

Nullable-Kontexte
Die neuen Features, die vor dem Auslösen von System.NullReferenceException schützen, können störend sein,
wenn sie in einer vorhandenen Codebasis aktiviert werden:
Alle explizit typisierten Verweisvariablen werden als Non-Nullable-Verweistypen interpretiert.
Die Bedeutung der class -Einschränkung in Generika wurde geändert, um einen Non-Nullable-Verweistyp
anzugeben.
Aufgrund dieser neuen Regeln werden neue Warnungen generiert.
Sie müssen sich ausdrücklich für die Nutzung dieser Funktionen in Ihren Projekten entscheiden. Dies bietet
einen Migrationspfad und bewahrt Abwärtskompatibilität. Nullable-Kontexte ermöglichen eine differenzierte
Steuerung der Interpretation von Verweistypvariablen durch den Compiler. Der Nullable-
Anmerkungskontext bestimmt das Verhalten des Compilers. Es gibt vier Werte für den Nullable-
Anmerkungskontext :
disabled: Der Compiler verhält sich ähnlich wie C# 7.3 und früher:
Nullable-Warnungen sind deaktiviert.
Alle Verweistypvariablen sind Nullable-Verweistypen.
Sie können eine Variable nicht als Nullable-Verweistyp deklarieren, indem Sie das Suffix ? für den
Typ verwenden.
Sie können den NULL-toleranten Operator ( ! ) verwenden, das hat aber keine Auswirkungen.
enabled: Der Compiler aktiviert alle NULL-Verweisanalysen und alle Sprachfeatures.
Alle neuen Nullable-Warnungen sind aktiviert.
Sie können das Suffix ? verwenden, um einen Nullable-Verweistyp zu deklarieren.
Alle anderen Verweistypvariablen sind Non-Nullable-Verweistypen.
Der NULL-tolerante Operator unterdrückt Warnungen für eine mögliche Zuweisung zu null .
warnings: Der Compiler führt alle NULL-Analysen durch und gibt Warnungen aus, wenn Code
möglicherweise null dereferenziert.
Alle neuen Nullable-Warnungen sind aktiviert.
Verwenden Sie das Suffix ? verwenden, um einen Nullable-Verweistyp zu deklarieren, der eine
Warnung generiert.
Alle Verweistypvariablen dürfen NULL sein. Member haben jedoch den null-state not-null an der
öffnenden geschweiften Klammer aller Methoden, es sei denn, sie werden mit dem Suffix ?
deklariert.
Sie können den NULL-toleranten Operator ( ! ) verwenden.
annotations: Der Compiler führt keine NULL-Analyse durch oder gibt Warnungen aus, wenn Code
möglicherweise null dereferenziert.
Alle neuen Nullable-Warnungen sind deaktiviert.
Sie können das Suffix ? verwenden, um einen Nullable-Verweistyp zu deklarieren.
Alle anderen Verweistypvariablen sind Non-Nullable-Verweistypen.
Sie können den NULL-toleranten Operator ( ! ) verwenden, das hat aber keine Auswirkungen.

Verweistypvariablen in Code, der vor C# 8 kompiliert wurde, oder in einem deaktivierten Kontext sind Nullwerte
zulassend-nicht beachtend. Sie können ein null -Literal oder eine maybe-null-Variable einer Variablen
zuweisen, die Nullwerte zulassend-nicht beachtend ist. Der Standardzustand einer Variable mit der Eigenschaft
Nullwerte zulassend-nicht beachtend ist jedoch nicht-null.
Sie können auswählen, welche Einstellung für Ihr Projekt am besten geeignet ist:
Wählen Sie disabled für Legacyprojekte aus, die Sie nicht basierend auf Diagnosen oder neuen Features
aktualisieren möchten.
Wählen Sie warnings aus, um zu bestimmen, wo Ihr Code möglicherweise System.NullReferenceExceptions
auslösen kann. Sie können diese Warnungen beheben, bevor Sie den Code ändern, um Non-Nullable-
Verweistypen zu aktivieren.
Wählen Sie annotations aus, um Ihre Entwurfsabsicht auszudrücken, bevor Sie Warnungen aktivieren.
Wählen Sie enabled für neue Projekte und aktive Projekte aus, die Sie vor NULL-Verweisausnahmen
schützen möchten.
Der Nullable-Anmerkungskontext und der Nullable-Warnungskontext können für ein Projekt festgelegt werden,
indem Sie das <Nullable> -Element in Ihrer CSPROJ-Datei verwenden. Dieses Element konfiguriert, wie der
Compiler die NULL-Zulässigkeit von Typen interpretiert und welche Warnungen generiert werden. Gültige
Einstellungen sind folgende:
enable
warnings
annotations
disable

Beispiel:

<Nullable>enable</Nullable>

Sie können auch Anweisungen verwenden, um diese gleichen Kontexte an beliebiger Stelle in Ihrem Quellcode
festzulegen. Diese sind besonders nützlich, wenn Sie eine große Codebasis migrieren.
#nullable enable : Legt den Nullable-Anmerkungskontext und den Nullable-Warnungskontext auf enabled
(aktiviert) fest.
#nullable disable : Legt den Nullable-Anmerkungskontext und den Nullable-Warnungskontext auf disabled
(deaktiviert) fest.
#nullable restore : Stellt die Projekteinstellungen für den Nullable-Anmerkungskontext und den Nullable-
Warnungskontext wieder her.
#nullable disable warnings : Legt den Nullable-Warnungskontext auf disabled (deaktiviert) fest.
#nullable enable warnings : Legt den Nullable-Warnungskontext auf enabled (aktiviert) fest.
#nullable restore warnings : Stellt die Projekteinstellungen für den Nullable-Warnungskontext wieder her.
#nullable disable annotations : Legt den Nullable-Anmerkungskontext auf disabled (deaktiviert) fest.
#nullable enable annotations : Legt den Nullable-Anmerkungskontext auf enabled (aktiviert) fest.
#nullable restore annotations : Stellt die Projekteinstellungen für den Anmerkungswarnungskontext wieder
her.
Für jede Codezeile können Sie eine der folgenden Kombinationen festlegen:

WA RN UN GSKO N T EXT A N M ERK UN GSKO N T EXT Z W EC K

Standardeinstellung des Projekts Standardeinstellung des Projekts Standard


WA RN UN GSKO N T EXT A N M ERK UN GSKO N T EXT Z W EC K

enabled deaktiviert Analysewarnungen korrigieren

enabled Standardeinstellung des Projekts Analysewarnungen korrigieren

Standardeinstellung des Projekts enabled Typanmerkungen hinzufügen

enabled enabled Bereits migrierter Code

deaktiviert enabled Kommentieren von Code vor dem


Beheben von Warnungen

deaktiviert deaktiviert Hinzufügen von Legacycode zum


migrierten Projekt

Standardeinstellung des Projekts deaktiviert Selten

deaktiviert Standardeinstellung des Projekts Selten

Mit diesen neun Kombinationen können Sie die Diagnosen, die der Compiler für Ihren Code ausgibt, detailliert
steuern. Sie können weitere Features in jedem Bereich aktivieren, den Sie aktualisieren, ohne zusätzliche
Warnungen zu erhalten, die Sie noch nicht beheben möchten.

IMPORTANT
Der globale Nullable-Kontext gilt nicht für generierte Codedateien. Der Nullable-Kontext ist unabhängig von der Strategie
für alle als generiert gekennzeichneten Quelldateien deaktiviert. Das bedeutet, dass alle in generierten Dateien
enthaltenen APIs nicht mit Anmerkungen versehen werden. Es gibt viel Möglichkeiten, eine Datei als generiert zu
markieren:
1. Geben Sie in der EDITORCONFIG-Datei generated_code = true in einem Abschnitt an, der für diese Datei gilt.
2. Fügen Sie <auto-generated> oder <auto-generated/> ganz oben in der Datei in einem Kommentar ein. Dabei
kann es sich um eine beliebige Zeile des Kommentars handeln, jedoch muss es sich beim Kommentarblock um das
erste Element in der Datei handeln.
3. Beginnen Sie den Dateinamen mit TemporaryGeneratedFile_ .
4. Enden Sie den Dateinamen mit .designer.cs, .generated.cs, .g.cs oder .g.i.cs.
Generatoren können die Präprozessoranweisung #nullable verwenden.

Standardmäßig sind die Nullable-Anmerkungs- und -Warnungskontexte deaktivier t . Dies bedeutet, dass Ihr
vorhandener Code ohne Änderungen und ohne Warnungen kompiliert wird. Ab .NET 6 enthalten neue Projekte
das <Nullable>enable</Nullable> -Element in allen Projektvorlagen.
Diese Optionen bieten zwei unterschiedliche Strategien zum Aktualisieren einer vorhandenen Codebasis, um
Nullable-Verweistypen zu verwenden.

Bekannte Fehlerquellen
Arrays und Strukturen, die Verweistypen enthalten, sind bekannte Fallstricke in Nullable-Verweisen und in der
statischen Analyse, die die NULL-Sicherheit bestimmt. In beiden Fällen kann ein Non-Nullable-Verweis mit
null initialisiert werden, ohne Warnungen zu generieren.

Strukturen
Strukturen, die Verweistypen enthalten, die keine NULL-Werte zulassen, können Sie default zuweisen, ohne
dass Warnungen ausgelöst werden. Betrachten Sie das folgenden Beispiel:

using System;

#nullable enable

public struct Student


{
public string FirstName;
public string? MiddleName;
public string LastName;
}

public static class Program


{
public static void PrintStudent(Student student)
{
Console.WriteLine($"First name: {student.FirstName.ToUpper()}");
Console.WriteLine($"Middle name: {student.MiddleName?.ToUpper()}");
Console.WriteLine($"Last name: {student.LastName.ToUpper()}");
}

public static void Main() => PrintStudent(default);


}

Im Beispiel oben gibt es in PrintStudent(default) keine Warnung, wenn die Non-Nullable-Verweistypen


FirstName und LastName NULL sind.

Bei der Verwendung von generischen Strukturen tritt ein weiteres häufigeres Problem auf. Betrachten Sie das
folgenden Beispiel:

#nullable enable

public struct Foo<T>


{
public T Bar { get; set; }
}

public static class Program


{
public static void Main()
{
string s = default(Foo<string>).Bar;
}
}

Im obigen Beispiel entspricht die Eigenschaft Bar zur Laufzeit null , und sie wird einer Zeichenfolge
zugewiesen, die keine NULL-Werte zulässt, ohne dass eine Warnung ausgelöst wird.
Arrays
Arrays stellen ebenfalls eine bekannte Fehlerquelle in Verweistypen dar, die NULL-Werte zulassen. Sehen Sie
sich das folgende Beispiel an, das keine Warnungen auslöst:
using System;

#nullable enable

public static class Program


{
public static void Main()
{
string[] values = new string[10];
string s = values[0];
Console.WriteLine(s.ToUpper());
}
}

Im Beispiel oben zeigt die Deklaration des Arrays, dass dieses Non-Nullable-Zeichenfolgen enthält, während alle
seine Elemente mit null initialisiert werden. Anschließend wird der Variablen s ein null -Wert (das erste
Element des Arrays) zugewiesen. Schließlich wird die Variable s dereferenziert, was zu einer Laufzeitausnahme
führt.

Siehe auch
Nullwerte zulassende Verweistypen – Vorschlag
Entwurf der Spezifikation für Nullable-Verweistypen
Nicht eingeschränkte Typparameteranmerkungen
Tutorial: Besseres Ausdrücken Ihrer Entwurfsabsicht mit Verweistypen, die NULL-Werte zulassen und nicht
zulassen
Nullable (C#-Compileroption)
Aktualisieren einer Codebasis mit Nullable-
Verweistypen zur Verbesserung von NULL-
Diagnosewarnungen
04.11.2021 • 6 minutes to read

Nullable-Verweistypen ermöglichen es zu deklarieren, ob Variablen eines Verweistyps ein null -Wert


zugewiesen werden soll oder nicht. Die statische Analyse des Compilers und Warnungen, wenn Ihr Code null
dereferenzieren kann, sind der wichtigste Vorteil dieses Features. Wenn diese Funktion aktiviert ist, erzeugt der
Compiler Warnungen, die Ihnen helfen, bei der Ausführung Ihres Codes ein Verwerfen von
System.NullReferenceException zu vermeiden.
Wenn Ihre Codebasis relativ klein ist, können Sie das -Feature in Ihrem Projekt aktivieren, Warnungen
bearbeiten und die Vorteile der verbesserten Diagnose nutzen. Größere Codebasen erfordern möglicherweise
einen strukturierteren Ansatz, um im Laufe der Zeit angezeigte Warnungen zu bearbeiten, indem das Feature
selektiv aktiviert wird, wenn Sie Warnungen in verschiedenen Typen oder Dateien bearbeiten. Dieser Artikel
beschreibt verschiedene Strategien zur Aktualisierung einer Codebasis und die mit diesen Strategien
verbundenen Kompromisse. Bevor Sie mit der Migration beginnen, lesen Sie den konzeptionellen Überblick
über Nullable-Verweistypen. Er betrifft die statische Analyse des Compilers, die null-state-Werte von maybe-null
und not-null sowie die Nullable-Anmerkungen. Sobald Sie mit diesen Konzepten und Begriffen vertraut sind,
können Sie Ihren Code migrieren.

Planen Ihrer Migration


Unabhängig davon, wie Sie Ihre Codebasis aktualisieren, müssen Nullable-Warnungen und Nullable-
Anmerkungen in Ihrem Projekt aktiviert werden. Sobald Sie dieses Ziel erreicht haben, haben Sie die
<nullable>Enable</nullable> -Einstellung in Ihrem Projekt. Sie brauchen keine Pragmas, um Einstellungen an
anderer Stelle vorzunehmen.
Die beste Option ist die Definition der Standardeinstellungen für das Projekt. Folgende Optionen stehen zur
Auswahl:
1. Nullable als Standard deaktivieren _: _disable ist die Standardeinstellung, wenn Sie kein Nullable -
Element zu Ihrer Projektdatei hinzufügen. Verwenden Sie diese Standardeinstellung, wenn Sie neue Dateien
nicht aktiv zur Codebasis hinzufügen. Die Hauptaufgabe ist die Aktualisierung der Bibliothek, um Nullable-
Verweistypen zu verwenden. Wenn Sie diese Standardeinstellung verwenden, fügen Sie jeder Datei ein
Nullable-Pragma hinzu, wenn Sie den Code aktualisieren.
2. Nullable als Standard aktivieren : Setzen Sie diesen Standard, wenn Sie aktiv neue Features entwickeln.
Sie möchten Nullable-Verweistypen und Nullable statischen Analysen für den gesamten neuen Code nutzen.
Wenn Sie diesen Standard verwenden, müssen Sie am Anfang jeder Datei ein #pragma nullable disable
hinzufügen. Sie entfernen dieses Pragma, wenn Sie mit der Bearbeitung der Warnungen in dieser Datei
beginnen.
3. Nullable-Warnungen als Standardeinstellung _: Wählen Sie diese Standardeinstellung für eine
Migration in zwei Phasen. In der ersten Phase werden Warnungen bearbeitet. In der zweiten Phase schalten
Sie die Anmerkungen zur Deklaration des erwarteten _null-state einer Variablen ein. Wenn Sie diesen
Standard verwenden, müssen Sie am Anfang jeder Datei ein #pragma nullable disable hinzufügen.
4. Nullable-Anmerkungen als Standard. Anmerkungen von Code vor dem Beheben von Warnungen.
Das Aktivieren von „Nullable“ als Standard erzeugt zunächst mehr Aufwand, um das Pragma zu jeder Datei
hinzuzufügen. Sie hat jedoch den Vorteil, dass jede neue Codedatei, die dem Projekt hinzugefügt wird, Nullable-
fähig ist. Jede neue Arbeit ist Nullable-fähig. Nur bereits vorhandener Code muss aktualisiert werden. Die
Deaktivierung von Nullable als Standard funktioniert besser, wenn die Bibliothek stabil ist und der
Hauptschwerpunkt der Entwicklung auf der Einführung von Nullable-Verweistypen liegt. Aktivieren Sie
Nullable-Verweistypen beim Kommentieren von APIs. Aktivieren Sie anschließend Nullable-Verweistypen für
das gesamte Projekt. Wenn Sie eine neue Datei erstellen, müssen Sie die Pragmas hinzufügen und "Nullable
aware" machen. Wenn Entwickler in Ihrem Team das vergessen, ist dieser neue Code nun im Arbeitsrückstand,
da der gesamte Code Nullable-fähig gemacht werden muss.
Für welche Strategie Sie sich entscheiden, hängt davon ab, wie viel aktive Entwicklung in Ihrem Projekt
stattfindet. Je ausgereifter und stabiler Ihr Projekt ist, umso besser eignet sich die zweite Strategie. Je mehr
Features entwickelt werden, umso besser eignet sich die erste Strategie.

IMPORTANT
Der globale Nullable-Kontext gilt nicht für generierte Codedateien. Der Nullable-Kontext ist unabhängig von der Strategie
für alle als generiert gekennzeichneten Quelldateien deaktiviert. Das bedeutet, dass alle in generierten Dateien
enthaltenen APIs nicht mit Anmerkungen versehen werden. Es gibt viel Möglichkeiten, eine Datei als generiert zu
markieren:
1. Geben Sie in der EDITORCONFIG-Datei generated_code = true in einem Abschnitt an, der für diese Datei gilt.
2. Fügen Sie <auto-generated> oder <auto-generated/> ganz oben in der Datei in einem Kommentar ein. Dabei
kann es sich um eine beliebige Zeile des Kommentars handeln, jedoch muss es sich beim Kommentarblock um das
erste Element in der Datei handeln.
3. Beginnen Sie den Dateinamen mit TemporaryGeneratedFile_ .
4. Enden Sie den Dateinamen mit .designer.cs, .generated.cs, .g.cs oder .g.i.cs.
Generatoren können die Präprozessoranweisung #nullable verwenden.

Verstehen von Zusammenhängen und Warnungen


Das Aktivieren von Warnungen und Anmerkungen steuert, wie der Compiler Verweistypen und Nullablility
betrachtet. Jeder Typ hat eine von drei Nullabilities:
oblivious: Alle Verweistypen sind Nullable oblivious, wenn der Anmerkungskontext deaktiviert ist.
nonnullable: Ein nicht mit Anmerkungen versehener Verweistyp C ist nonnullable, wenn der
Anmerkungskontext aktiviert ist.
Nullable: Ein mit Anmerkungen versehener Verweistyp C? ist nullable, es kann aber eine Warnung
ausgegeben werden, wenn der Anmerkungskontext deaktiviert ist. Mit var deklarierte Variablen sind
nullable, wenn der Anmerkungskontext aktiviert ist.
Der Compiler erzeugt anhand dieser Nullability Warnungen:
nonnullable-Typen erzeugen Warnungen, wenn ihnen ein potentieller null -Wert zugewiesen wird.
Nullable-Typen erzeugen Warnungen, wenn sie den Status maybe-null haben und dereferenziert werden.
oblivious-Typen verursachen Warnungen, wenn sie mit dem Status maybe-null dereferenziert werden und
der Warnkontext aktiviert ist.
Jede Variable hat den Standardzustand „Nullable“, der von ihrer NULL-Zulässigkeit abhängt:
Nullable-Variablen haben einen Standard-Status null-state für maybe-null.
Nicht-Nullable-Variablen haben einen Standard-Status null-state für not-null.
Nullable oblivious-Variablen haben einen Standardstatus null-state von not-null.
Bevor Sie Nullable-Verweistypen aktivieren, sind alle Deklarationen in Ihrer Codebasis nullable oblivious. Das ist
wichtig, weil damit alle Verweistypen einen Standardstatus null-state für not-null haben.

Bearbeiten von Warnungen


Wenn Ihr Projekt Entity Framework Core verwendet, sollten Sie die Anleitung zum Thema Arbeiten mit Nullable-
Verweistypen lesen.
Wenn Sie mit der Migration beginnen, sollten Sie zunächst nur Warnungen aktivieren. Alle Deklarationen
bleiben nullable oblivious, aber Sie werden Warnungen sehen, wenn Sie einen Wert dereferenzieren, nachdem
sich sein null-state auf maybe-null ändert. Wenn Sie diese Warnungen bearbeiten, werden Sie an weiteren
Stellen gegen NULL prüfen, und Ihre Codebasis wird fehlersicherer. Spezifische Techniken für verschiedene
Situationen finden Sie in dem Artikel Techniken zur Auflösung von Nullable-Warnungen.
Sie können Warnungen bearbeiten und Anmerkungen in jeder Datei oder Klasse aktivieren, bevor Sie mit
anderem Code fortfahren. Es ist jedoch oft effizienter, die Warnungen zu bearbeiten, die im Kontext Warnungen
generiert werden, bevor die Typanmerkungen aktiviert sind. Auf diese Weise sind alle Typen oblivious, bis Sie
die erste Gruppe von Warnungen bearbeitet haben.

Aktivieren von Typanmerkungen


Nachdem Sie den ersten Satz Warnungen bearbeitet haben, können Sie den Anmerkungskontext aktivieren.
Dadurch ändern sich die Verweistypen von oblivious auf nonnullable. Alle mit var deklarierten Variablen sind
nullable. Diese Änderung führt oft zu neuen Warnungen. Der erste Schritt zum Bearbeiten der Compiler-
Warnungen ist die Verwendung von ? -Anmerkungen für Parameter- und Rückgabetypen, um anzuzeigen,
wann Argumente oder Rückgabewerte null sein können. Bei dieser Aufgabe geht es nicht nur darum,
Warnungen zu behandeln. Vielmehr geht es darum, dafür zu sorgen, dass der Compiler versteht, warum NULL-
Werte verwendet werden sollen.

Attribute erweitern Typanmerkungen


Es wurden mehrere Attribute hinzugefügt, um zusätzliche Informationen über den NULL-Zustand von Variablen
auszudrücken. Die Regeln für Ihre APIs sind für alle Parameter und Rückgabewerte wahrscheinlich komplizierter
als not-null oder maybe-null. Viele Ihrer APIs umfassen komplexere Regeln in Bezug darauf, wann Variablen den
Wert null annehmen können oder nicht. In diesen Fällen verwenden Sie Attribute, um diese Regeln
auszudrücken. Die Attribute, die die Semantik Ihrer API beschreiben, finden Sie im Artikel Attributes that affect
nullable analysis (Attribute, die die Nullable-Analyse betreffen).

Nächste Schritte
Sobald Sie alle Warnungen nach der Aktivierung von Anmerkungen behoben haben, können Sie den
Standardkontext für Ihr Projekt auf aktiviert setzen. Wenn Sie in Ihrem Code Pragmas für die Nullable-
Anmerkung oder den Warnkontext hinzugefügt haben, können Sie diese entfernen. Im Laufe der Zeit werden Sie
möglicherweise neue Warnungen erhalten. Sie können Code schreiben, der Warnungen auslöst. Eine
Bibliotheksabhängigkeit für Nullable-Verweistypen kann aktualisiert werden. Durch diese Aktualisierungen
werden die Typen in dieser Bibliothek von nullable oblivious auf nonnullable oder nullable geändert.
Methoden in C#
04.11.2021 • 21 minutes to read

Eine Methode ist ein Codeblock, der eine Reihe von Anweisungen enthält. Ein Programm bewirkt die
Ausführung der Anweisungen, indem die Methode aufgerufen wird und alle erforderlichen
Methodenargumente angegeben werden. In C# werden alle Anweisungen im Kontext einer Methode ausgeführt.
Die Methode Main ist der Einstiegspunkt jeder C#-Anwendung und wird von der Common Language Runtime
(CLR) aufgerufen, wenn das Programm gestartet wird.

NOTE
In diesem Thema werden benannte Methoden erläutert. Weitere Informationen zu anonymen Funktionen finden Sie unter
Lambdaausdrücke.

Methodensignaturen
Methoden werden in class , record , oder struct durch folgende Angaben deklariert:
Eine optionale Zugriffsebene, z.B. public oder private . Der Standardwert ist private .
Optionale Modifizierer, z.B. abstract oder sealed .
Der Rückgabewert oder void , wenn die Methode keinen besitzt.
Der Methodenname.
Jede Methodenparameter. Methodenparameter werden in Klammern eingeschlossen und durch Kommas
getrennt. Leere Klammern geben an, dass für die Methode keine Parameter erforderlich sind.
Diese Teile bilden zusammen die Signatur der Methode.

IMPORTANT
Ein Rückgabetyp einer Methode ist nicht Teil der Signatur der Methode, wenn es um die Methodenüberladung geht. Er ist
jedoch Teil der Methodensignatur, wenn die Kompatibilität zwischen einem Delegaten und der Methode bestimmt wird,
auf die dieser verweist.

Im folgenden Beispiel wird eine Klasse mit dem Namen Motorcycle deklariert, die fünf Methoden enthält:
using System;

abstract class Motorcycle


{
// Anyone can call this.
public void StartEngine() {/* Method statements here */ }

// Only derived classes can call this.


protected void AddGas(int gallons) { /* Method statements here */ }

// Derived classes can override the base class implementation.


public virtual int Drive(int miles, int speed) { /* Method statements here */ return 1; }

// Derived classes can override the base class implementation.


public virtual int Drive(TimeSpan time, int speed) { /* Method statements here */ return 0; }

// Derived classes must implement this.


public abstract double GetTopSpeed();
}

Beachten Sie, dass die Motorcycle -Klasse eine überladene Methode, Drive , enthält. Zwei Methoden haben
denselben Namen, müssen aber durch ihre Parametertypen unterschieden werden.

Methodenaufruf
Methoden können entweder instance oder static sein. Das Aufrufen einer Instanzmethode erfordert, dass Sie ein
Objekt instanziieren und die Methode an diesem Objekt aufrufen; eine Instanzmethode funktioniert in dieser
Instanz und ihren Daten. Sie rufen eine statische Methode auf, indem Sie auf den Namen des Typs verweisen, zu
dem die Methode gehört. Statische Methoden funktionieren nicht in Instanzdaten. Bei dem Versuch eine
statische Methode über eine Objektinstanz aufzurufen, wird ein Compilerfehler erzeugt.
Das Aufrufen einer Methode ähnelt dem Zugreifen auf ein Feld. Fügen Sie nach dem Objektnamen (wenn Sie
eine Instanzmethode aufrufen) oder dem Typnamen (beim Aufrufen einer static -Methode) einen Punkt, den
Methodennamen und Klammern hinzu. Argumente werden innerhalb der Klammern aufgelistet und durch
Kommas getrennt.
Die Methodendefinition gibt die Namen und Typen aller ggf. erforderlichen Parameter an. Wenn ein Aufrufer die
Methode aufruft, werden für jeden Parameter konkrete Werte bereitgestellt, die als Argumente bezeichnet
werden. Die Argumente müssen mit dem Parametertyp kompatibel sein, aber der Name des Arguments (sofern
im aufzurufenden Code einer verwendet wird) muss nicht mit dem in der Methode definierten Parameternamen
identisch sein. Im folgenden Beispiel enthält die Square -Methode einen einzelnen Parameter vom Typ int mit
dem Namen i. Der erste Methodenaufruf übergibt der Square -Methode eine Variable vom Typ int mit dem
Namen num. Der zweite übergibt eine numerische Konstante und der dritte einen Ausdruck.
public class SquareExample
{
public static void Main()
{
// Call with an int variable.
int num = 4;
int productA = Square(num);

// Call with an integer literal.


int productB = Square(12);

// Call with an expression that evaluates to int.


int productC = Square(productA * 3);
}

static int Square(int i)


{
// Store input argument in a local variable.
int input = i;
return input * input;
}
}

Die häufigste Form des Methodenaufrufs verwendete Positionsargumente. Die Argumente werden in der
gleichen Reihenfolge wie Methodenparameter bereitgestellt. Die Methoden der Motorcycle -Klasse können
deshalb wie im folgenden Beispiel aufgerufen werden. Der Aufruf der Drive -Methode enthält z.B. zwei
Argumente, die den beiden Parametern in der Syntax der Methode entsprechen. Das erste Argument wird der
Wert des miles -Parameters, das zweite wird der Wert des speed -Parameters.

class TestMotorcycle : Motorcycle


{
public override double GetTopSpeed()
{
return 108.4;
}

static void Main()


{

TestMotorcycle moto = new TestMotorcycle();

moto.StartEngine();
moto.AddGas(15);
moto.Drive(5, 20);
double speed = moto.GetTopSpeed();
Console.WriteLine("My top speed is {0}", speed);
}
}

Sie können auch benannte Argumente anstelle von positionellen Argumenten verwenden, wenn Sie eine
Methode aufrufen. Wenn Sie benannte Argumente verwenden, geben Sie den Parameternamen, gefolgt von
einem Doppelpunkt („:“), und das Argument an. Argumente können für diese Methode in beliebiger Reihenfolge
erscheinen, solange alle benötigen Argumente vorhanden sind. Im folgenden Beispiel werden die benannten
Argumente zum Aufrufen der TestMotorcycle.Drive -Methode verwendet. In diesem Beispiel werden die
benannten Argumente in umgekehrter Reihenfolge aus der Parameterliste der Methode übergeben.
using System;

class TestMotorcycle : Motorcycle


{
public override int Drive(int miles, int speed)
{
return (int)Math.Round(((double)miles) / speed, 0);
}

public override double GetTopSpeed()


{
return 108.4;
}

static void Main()


{

TestMotorcycle moto = new TestMotorcycle();


moto.StartEngine();
moto.AddGas(15);
var travelTime = moto.Drive(speed: 60, miles: 170);
Console.WriteLine("Travel time: approx. {0} hours", travelTime);
}
}
// The example displays the following output:
// Travel time: approx. 3 hours

Sie können eine Methode aufrufen, indem Sie sowohl Positionsargumente als auch benannte Argumente
verwenden. Positionellen Argumenten können benannten Argumenten nur folgen, wenn diese an der richtigen
Position verwendet werden. Im folgenden Beispiel wird die TestMotorcycle.Drive -Methode des vorherigen
Beispiels aufgerufen, indem jeweils ein Positionsargument und ein benanntes Argument verwendet wird.

var travelTime = moto.Drive(170, speed: 55);

Geerbte und überschriebene Methoden


Zusätzlich zu den Elementen, die ausdrücklich in einem Typ definiert werden, erbt ein Typ Member, die in seiner
Basisklasse definiert wurden. Da alle Typen im verwalteten Typsystem direkt oder indirekt von der Object-Klasse
erben, erben alle Typen ihre Member, z.B. Equals(Object), GetType() und ToString(). Im folgenden Beispiel wird
eine Person -Klasse definiert, zwei Person -Objekte instanziiert, und es wird die Person.Equals -Methode
aufgerufen, um zu bestimmen, ob die zwei Objekte gleich sind. Jedoch ist die Equals -Methode nicht in der
Person -Klasse definiert; sie wird von Object vererbt.
using System;

public class Person


{
public String FirstName;
}

public class ClassTypeExample


{
public static void Main()
{
var p1 = new Person();
p1.FirstName = "John";
var p2 = new Person();
p2.FirstName = "John";
Console.WriteLine("p1 = p2: {0}", p1.Equals(p2));
}
}
// The example displays the following output:
// p1 = p2: False

Typen können geerbte Member überschreiben, indem das Schlüsselwort override verwendet und eine
Implementierung für die überschriebene Methode bereitgestellt wird. Die Signatur der Methode muss mit der
überschriebenen Methode identisch sein. Das folgende Beispiel ähnelt dem vorherigen Beispiel, außer dass es
die Methode Equals(Object) überschreibt. (Sie überschreibt auch die GetHashCode()-Methode, da die zwei
Methoden konsistente Ergebnisse bereitstellen sollen)

using System;

public class Person


{
public String FirstName;

public override bool Equals(object obj)


{
var p2 = obj as Person;
if (p2 == null)
return false;
else
return FirstName.Equals(p2.FirstName);
}

public override int GetHashCode()


{
return FirstName.GetHashCode();
}
}

public class Example


{
public static void Main()
{
var p1 = new Person();
p1.FirstName = "John";
var p2 = new Person();
p2.FirstName = "John";
Console.WriteLine("p1 = p2: {0}", p1.Equals(p2));
}
}
// The example displays the following output:
// p1 = p2: True
Übergeben von Parametern
Typen in C# sind entweder Werttypen oder Verweistypen. Eine Liste der integrierten Werttypen finden Sie unter
Typen. Sowohl Werttypen als auch Verweistypen werden standardmäßig als Wert an eine Methode übergeben.

Übergeben von Parametern als Wert


Wenn ein Werttyp an eine Methode als Wert übergeben wird, wird anstelle des eigentlichen Objekts
standardmäßig eine Kopie übergeben. Aus diesem Grund haben Änderungen am Objekt in der aufgerufenen
Methode keine Auswirkung auf das ursprüngliche Objekt, wenn das Steuerelement an den Aufrufer
zurückgegeben wird.
Im folgenden Beispiel wird ein Werttyp als Wert an eine Methode übergeben, und die aufgerufene Methode
versucht, den Wert des Werttyps zu ändern. Es definiert eine Variable des Typs int , die ein Werttyp ist,
initialisiert seine Werte auf 20 und übergibt ihn an eine Methode mit dem Namen ModifyValue , die den Wert
der Variable in 30 ändert. Wenn die Methode zurückgegeben wird, bleibt der Wert der Variable jedoch
unverändert.

using System;

public class ByValueExample


{
public static void Main()
{
int value = 20;
Console.WriteLine("In Main, value = {0}", value);
ModifyValue(value);
Console.WriteLine("Back in Main, value = {0}", value);
}

static void ModifyValue(int i)


{
i = 30;
Console.WriteLine("In ModifyValue, parameter value = {0}", i);
return;
}
}
// The example displays the following output:
// In Main, value = 20
// In ModifyValue, parameter value = 30
// Back in Main, value = 20

Wenn ein Objekt eines Verweistyps als Wert an eine Methode übergeben wird, wird ein Verweis als Wert auf
das Objekt übergeben. Das heißt, die Methode erhält nicht das Objekt selbst, sondern ein Argument, das den
Speicherort des Objekts angibt. Wenn Sie einen Member des Objekts unter Verwendung dieses Verweises
ändern, wird die Änderung im Objekt berücksichtigt, wenn das Steuerelement der aufrufenden Methode
zurückgegeben wird. Jedoch hat das Ersetzen des Objekts, das an die Methode übergeben wird, keine
Auswirkung auf das ursprüngliche Objekt, wenn das Steuerelement dem Aufrufer zurückgegeben wird.
Im folgenden Beispiel wird eine Klasse (die ein Verweistyp ist) mit dem Namen SampleRefType definiert. Sie
instanziiert ein SampleRefType -Objekt, weist seinem value -Feld 44 zu, und übergibt das Objekt der
ModifyObject -Methode. Dieses Beispiel entspricht im Wesentlichen dem vorherigen Beispiel und übergibt ein
Argument als Wert an eine Methode. Da jedoch ein Verweistyp verwendet wird, unterscheidet sich das Ergebnis.
Die Änderung, die in ModifyObject am obj.value -Feld vorgenommen wurden, ändern auch das value -Feld
des Arguments rt in der Main -Methode auf 33, wie die Ausgabe des Beispiels zeigt.
using System;

public class SampleRefType


{
public int value;
}

public class ByRefTypeExample


{
public static void Main()
{
var rt = new SampleRefType();
rt.value = 44;
ModifyObject(rt);
Console.WriteLine(rt.value);
}

static void ModifyObject(SampleRefType obj)


{
obj.value = 33;
}
}

Übergeben von Parametern durch einen Verweis


Sie übergeben einen Parameter durch einen Verweis, wenn Sie den Wert eines Arguments in einer Methode
ändern und diese Änderung berücksichtigen möchten, wenn die Steuerung an die aufrufende Methode
zurückgegeben wird. Verwenden Sie das Schlüsselwort ref oder out , um einen Parameter pro Verweis zu
übergeben. Außerdem können Sie einen Wert pro Verweis übergeben, um das Kopieren und Änderungen zu
vermeiden, wenn Sie das Schlüsselwort in verwenden.
Das folgende Beispiel ist identisch mit dem vorherigen Beispiel, außer dass der Wert durch einen Verweis an die
ModifyValue -Methode übergeben wird. Wenn der Wert des Parameters in der ModifyValue -Methode verändert
wird, wird die Wertänderung berücksichtigt, wenn das Steuerelement dem Aufrufer zurückgegeben wird.

using System;

public class ByRefExample


{
public static void Main()
{
int value = 20;
Console.WriteLine("In Main, value = {0}", value);
ModifyValue(ref value);
Console.WriteLine("Back in Main, value = {0}", value);
}

static void ModifyValue(ref int i)


{
i = 30;
Console.WriteLine("In ModifyValue, parameter value = {0}", i);
return;
}
}
// The example displays the following output:
// In Main, value = 20
// In ModifyValue, parameter value = 30
// Back in Main, value = 30

Ein häufiges Muster, das von ref-Parametern verwendet wird, umfasst das Tauschen der Werte der Variablen. Sie
übergeben durch einen Verweis zwei Variablen an eine Methode, und die Methode tauscht deren Inhalte. Im
folgenden Beispiel werden ganzzahlige Werte getauscht.
using System;

public class RefSwapExample


{
static void Main()
{
int i = 2, j = 3;
System.Console.WriteLine("i = {0} j = {1}" , i, j);

Swap(ref i, ref j);

System.Console.WriteLine("i = {0} j = {1}" , i, j);


}

static void Swap(ref int x, ref int y)


{
int temp = x;
x = y;
y = temp;
}
}
// The example displays the following output:
// i = 2 j = 3
// i = 3 j = 2

Durch das Übergeben eines Verweistyp-Parameters können Sie den eigentlichen Wert des Verweises anstatt
den Wert der einzelnen Elemente oder Felder ändern.

Parameterarrays
Manchmal ist die Voraussetzung, dass Sie die genaue Anzahl von Argumenten für Ihre Methode angeben,
restriktiv. Mithilfe des Schlüsselworts params wird angegeben, dass ein Parameter ein Parameterarray ist, und
Sie können Ihre Methode mit einer variablen Anzahl von Argumenten aufrufen. Der mit dem Schlüsselwort
params gekennzeichnete Parameter muss ein Arraytyp sein, und er muss der letzte Parameter in der
Parameterliste der Methode sein.
Ein Aufrufer kann anschließend die Methode auf vier verschiedene Arten aufrufen:
Durch das Übergeben eines Arrays des entsprechenden Typs, der die gewünschte Anzahl von Elementen
enthält
Durch das Übergeben einer mit Komma getrennten Liste eines einzelnen Arguments des entsprechenden
Typs der Methode
Durch Übergeben von null .
Durch keine Bereitstellung eines Arguments für das Parameterarray
Das folgende Beispiel definiert eine Methode namens GetVowels , die alle Vokale aus einem Parameterarray
zurückgibt. Die Methode Main zeigt alle vier Möglichkeiten zum Aufrufen der Methode. Aufrufer müssen keine
Argumente für Parameter angeben, die den Modifizierer params enthalten. In diesem Fall ist der Parameter ein
leeres Array.
using System;
using System.Linq;

class ParamsExample
{
static void Main()
{
string fromArray = GetVowels(new[] { "apple", "banana", "pear" });
Console.WriteLine($"Vowels from array: '{fromArray}'");

string fromMultipleArguments = GetVowels("apple", "banana", "pear");


Console.WriteLine($"Vowels from multiple arguments: '{fromMultipleArguments}'");

string fromNull = GetVowels(null);


Console.WriteLine($"Vowels from null: '{fromNull}'");

string fromNoValue = GetVowels();


Console.WriteLine($"Vowels from no value: '{fromNoValue}'");
}

static string GetVowels(params string[] input)


{
if (input == null || input.Length == 0)
{
return string.Empty;
}

var vowels = new char[] { 'A', 'E', 'I', 'O', 'U' };


return string.Concat(
input.SelectMany(
word => word.Where(letter => vowels.Contains(char.ToUpper(letter)))));
}
}

// The example displays the following output:


// Vowels from array: 'aeaaaea'
// Vowels from multiple arguments: 'aeaaaea'
// Vowels from null: ''
// Vowels from no value: ''

Optionale Parameter und Argumente


Eine Methodendefinition kann angeben, dass seine Parameter erforderlich oder optional sind. Parameter sind
standardmäßig erforderlich. Optionale Parameter werden einschließlich des Standardwerts des Parameters in
der Methodendefinition angegeben. Wird die Methode aufgerufen, wenn kein Argument für einen optionalen
Parameter angegeben wird, wird stattdessen der Standardwert verwendet.
Der Standardwert des Parameters muss von einer der folgenden Ausdrucksarten zugewiesen werden:
Eine Konstante, z.B. eine Zeichenfolgenliteral oder eine Zahl
Ein Ausdruck im Formular default(SomeType) , wobei SomeType entweder ein Werttyp oder ein
Verweistyp sein kann. Wenn es sich um einen Verweistyp handelt, ist dies praktisch identisch mit der
Angabe von null . Ab C# 7.1 können Sie das default -Literal verwenden, da der Compiler den Typ aus
der Parameterdeklaration ableiten kann.
Ein Ausdruck in Form von new ValType() , wobei ValType ein Werttyp ist. Beachten Sie, dass dies den
impliziten parameterlosen Konstruktor des Werttyps aufruft, der eigentlich kein Member des Typs ist.
NOTE
Wenn in C# 10.0 und höher ein Ausdruck der Form new ValType() den explizit definierten parameterlosen
Konstruktor eines Werttyps aufruft, generiert der Compiler einen Fehler, da der Standardparameterwert eine
Kompilierzeitkonstante sein muss. Verwenden Sie den default(ValType) -Ausdruck oder das default -Literal,
um den Standardparameterwert bereitzustellen. Weitere Informationen zu parameterlosen Konstruktoren finden
Sie im Abschnitt Parameterlose Konstruktoren und Feldinitialisierer des Artikels Strukturtypen.

Wenn eine Methode erforderliche und optionale Parameter enthält, werden optionale Parameter nach allen
benötigten Parametern am Ende der Parameterliste definiert.
Im folgenden Beispiel wird eine ExampleMethod -Methode definiert, die aus einem erforderlichen und zwei
optionalen Parametern besteht.

using System;

public class Options


{
public void ExampleMethod(int required, int optionalInt = default,
string? description = default)
{
var msg = $"{description ?? "N/A"}: {required} + {optionalInt} = {required + optionalInt}";
Console.WriteLine(msg);
}
}

Wenn eine Methode mit mehreren optionalen Argumenten mithilfe von Positionsargumenten aufgerufen wird,
muss der Aufrufer ein Argument für alle optionalen Parameter, vom ersten bis zum letzten, bereitstellen, für die
ein Argument bereitgestellt wird. Bei der ExampleMethod -Methode muss der Parameter z.B. auch ein Argument
für den description -Parameter bereitstellen, wenn er ein Argument für den optionalInt -Parameter bereitstellt.
opt.ExampleMethod(2, 2, "Addition of 2 and 2"); ist ein gültiger Methodenaufruf;
opt.ExampleMethod(2, , "Addition of 2 and 0"); erzeugt einen Compilerfehler: „Argument fehlt“.

Wenn eine Methode durch ein benanntes Argument oder einer Mischung aus benannten und
Positionsargumenten aufgerufen wird, kann der Aufrufer Argumente auslassen, die dem letzten
Positionsargument im Methodenaufruf folgen.
Im folgenden Beispiel wird die ExampleMethod -Methode dreimal aufgerufen. Die ersten zwei Methodenaufrufe
verwenden Positionsargumente. Der erste Methodenaufruf lässt beide optionale Argumente aus, während der
Zweite das letzte Argument auslässt. Der dritte Methodenaufruf stellt für die benötigten Parameter ein
positionelles Argument bereit, verwendet aber ein benanntes Argument, um einen Wert für den description -
Parameter bereitzustellen, während das optionalInt -Argument ausgelassen wird.

public class OptionsExample


{
public static void Main()
{
var opt = new Options();
opt.ExampleMethod(10);
opt.ExampleMethod(10, 2);
opt.ExampleMethod(12, description: "Addition with zero:");
}
}
// The example displays the following output:
// N/A: 10 + 0 = 10
// N/A: 10 + 2 = 12
// Addition with zero:: 12 + 0 = 12
Die Verwendung von zwei optionalen Parametern wirkt sich auf die Überladungsauflösung aus, oder die Art und
Weise, mit der der C#-Compiler bestimmt, welche besondere Überladung von einem Methodenaufruf wie folgt
aufgerufen werden sollte:
Eine Methode, ein Indexer oder ein Konstruktor ist ein Kandidat für die Ausführung, wenn jeder der
Parameter entweder optional ist oder über Namen oder Position auf ein einzelnes Argument in der
aufrufenden Anweisung reagiert. Dieses Argument kann in dem Typ des Parameters konvertiert werden.
Wenn mehr als ein Kandidat gefunden wird, werden die Regeln der Überladungsauflösung als bevorzugte
Konvertierungen auf die Argumente angewandt, die ausdrücklich angegeben sind. Ausgelassene Argumente
für optionale Parameter werden ignoriert.
Wenn zwei Kandidaten gleich gut geeignet sind, wird ein Kandidat bevorzugt, der keine optionalen Parameter
besitzt, für die Argumente im Aufruf ausgelassen wurden. Dies ist die Folge einer allgemeinen Präferenz bei
der Überladungsauflösung für Kandidaten, die weniger Parameter besitzen.

Rückgabewerte
Methoden können einen Wert an die aufrufende Funktion (den Aufrufer) zurückgeben. Wenn der Rückgabetyp
(der vor dem Methodennamen aufgeführte Typ) nicht void ist, kann die Methode den Wert mithilfe des
return -Schlüsselworts zurückgeben. Eine Anweisung mit dem Schlüsselwort return , gefolgt von einem Wert,
der dem Rückgabetyp entspricht, gibt diesen Wert an den Methodenaufrufer zurück. Methoden mit einem
anderen Rückgabetyp als „void“ müssen das return -Schlüsselwort verwenden, um einen Wert zurückzugeben.
Das return -Schlüsselwort beendet außerdem die Ausführung der Methode.
Wenn der Rückgabetyp void ist, ist eine return -Anweisung ohne Wert immer noch nützlich, um die
Ausführung der Methode zu beenden. Ohne das return -Schlüsselwort wird die Ausführung der Methode
beendet, wenn das Ende des Codeblocks erreicht ist.
Die folgenden beiden Methoden verwenden z. B. das return -Schlüsselwort, um ganze Zahlen zurückzugeben:

class SimpleMath
{
public int AddTwoNumbers(int number1, int number2)
{
return number1 + number2;
}

public int SquareANumber(int number)


{
return number * number;
}
}

Um einen von einer Methode zurückgegebenen Wert zu verwenden, kann die aufrufende Methode den
Methodenaufruf selbst an jeder Stelle verwenden, an der ein Wert des gleichen Typs ausreichend ist. Sie können
den Rückgabewert auch einer Variablen zuweisen. Beispielsweise wird mit den folgenden beiden Codebeispiele
das gleiche Ergebnis erzielt:

int result = obj.AddTwoNumbers(1, 2);


result = obj.SquareANumber(result);
// The result is 9.
Console.WriteLine(result);
result = obj.SquareANumber(obj.AddTwoNumbers(1, 2));
// The result is 9.
Console.WriteLine(result);

Die Verwendung einer lokalen Variablen, in diesem Fall result , zum Speichern eines Werts ist optional. Es kann
die Lesbarkeit des Codes verbessern, oder es kann notwendig sein, wenn Sie den ursprünglichen Wert des
Arguments für den gesamten Gültigkeitsbereich der Methode speichern müssen.
Manchmal möchten Sie, dass Ihre Methode mehr als einen Wert zurückgibt. Ab mit C#-7.0 können Sie dies
einfach mithilfe von Tupeltypen und Tupelliteralen erledigen. Der Tupeltyp definiert die Datentypen der Elemente
des Tupels. Tupelliterale stellen die tatsächlichen Werte des zurückgegebenen Tupels bereit. Im folgenden
Beispiel definiert (string, string, string, int) den Tupeltyp, der durch die GetPersonalInfo -Methode
zurückgegeben wird. Der (per.FirstName, per.MiddleName, per.LastName, per.Age) -Ausdruck ist das Tupelliteral.
Die Methode gibt den ersten, mittleren und letzten Namen zusammen mit dem Alter eines PersonInfo -Objekts
zurück.

public (string, string, string, int) GetPersonalInfo(string id)


{
PersonInfo per = PersonInfo.RetrieveInfoById(id);
return (per.FirstName, per.MiddleName, per.LastName, per.Age);
}

Der Aufrufer kann anschließend das zurückgegebene Tupel mit Code wie dem folgenden verwenden:

var person = GetPersonalInfo("111111111")


Console.WriteLine($"{person.Item1} {person.Item3}: age = {person.Item4}");

Namen können auch den Tupelelementen in der Typdefinition des Tupels zugewiesen werden. Das folgende
Beispiel zeigt eine alternative Version der GetPersonalInfo -Methode, die benannte Elemente verwendet:

public (string FName, string MName, string LName, int Age) GetPersonalInfo(string id)
{
PersonInfo per = PersonInfo.RetrieveInfoById(id);
return (per.FirstName, per.MiddleName, per.LastName, per.Age);
}

Der vorherige Aufruf der GetPersonInfo -Methode kann anschließend wie folgt geändert werden:

var person = GetPersonalInfo("111111111");


Console.WriteLine($"{person.FName} {person.LName}: age = {person.Age}");

Wenn eine Methode einem Array als Argument übergeben wird und den Wert der einzelnen Elemente ändert,
ist es nicht erforderlich, dass die Methode das Array zurückgibt, obwohl Sie sich entscheiden könnten, dies für
den funktionalen Fluss von Werten oder zu stilistischen Zwecken zu tun. Das liegt daran, dass C# alle
Verweistypen als Wert übergibt und der Wert eines Arrayverweises der Zeiger auf das Array ist. Im folgenden
Beispiel sind Änderungen an den Inhalten des values -Arrays, die in der DoubleValues -Methode ausgeführt
werden, von jedem Code beobachtbar, der einen Verweis auf das Array hat.
using System;

public class ArrayValueExample


{
static void Main(string[] args)
{
int[] values = { 2, 4, 6, 8 };
DoubleValues(values);
foreach (var value in values)
Console.Write("{0} ", value);
}

public static void DoubleValues(int[] arr)


{
for (int ctr = 0; ctr <= arr.GetUpperBound(0); ctr++)
arr[ctr] = arr[ctr] * 2;
}
}
// The example displays the following output:
// 4 8 12 16

Erweiterungsmethoden
Es gibt normalerweise zwei Möglichkeiten, einem vorhandenen Typ eine Methode hinzuzufügen:
Ändern Sie den Quellcode für diesen Typ. Sie können dies natürlich nicht tun, wenn Sie nicht den Quellcode
des Typs besitzen. Dies wird zudem eine bahnbrechende Änderung, wenn Sie auch private Datenfelder zur
Unterstützung der Methode hinzufügen.
Definieren Sie die neue Methode in einer abgeleiteten Klasse. Eine Methode kann nicht auf diese Weise für
andere Typen wie Strukturen oder Enumerationen mithilfe von Vererbung hinzugefügt werden. Sie kann
auch nicht verwendet werden, um einer versiegelten Klasse eine Methode „hinzuzufügen“.
Erweiterungsmethoden lassen Sie eine Methode einem vorhanden Typ „hinzufügen“, ohne den eigentlichen Typ
zu verändern oder die neue Methode in einem geerbten Typ zu implementieren. Die Erweiterungsmethode
muss sich auch nicht im gleichen Assembly wie der Typ befinden, den es erweitert. Sie rufen eine
Erweiterungsmethode auf, als ob es sich um einen definierten Member eines Typs handele.
Weitere Informationen finden Sie unter Erweiterungsmethoden.

Asynchrone Methoden
Mithilfe der Async-Funktion können Sie asynchrone Methoden aufrufen, ohne explizite Rückrufe verwenden
oder den Code manuell über mehrere Methoden oder Lambda-Ausdrücke teilen zu müssen.
Wenn Sie eine Methode mit dem Modifizierer async kennzeichnen, können Sie den Operator await in der
Methode verwenden. Wenn ein await -Ausdruck in der asynchronen Methode erreicht wird, wird die Steuerung
an den Aufrufer zurückgegeben, wenn die erwartete Aufgabe nicht fertig ist, und die Ausführung der Methode
mit dem Schlüsselwort await wird angehalten, bis die erwartete Aufgabe abgeschlossen ist. Wenn die Aufgabe
abgeschlossen ist, kann die Ausführung in der Methode fortgesetzt werden.

NOTE
Eine asynchrone Methode wird an den Aufrufer zurückgegeben, wenn sie entweder auf das erste erwartete Objekt trifft,
das noch nicht abgeschlossen wurde, oder das Ende der asynchronen Methode erreicht.
Eine asynchrone Methode weist üblicherweise den Rückgabetyp Task<TResult>, Task, IAsyncEnumerable<T>
oder void auf. Der Rückgabetyp void wird hauptsächlich zum Definieren von Ereignishandlern verwendet,
wobei ein void -Rückgabetyp erforderlich ist. Auf eine asynchrone Methode, die void zurückgibt, kann nicht
gewartet werden, und der Aufrufer einer Methode mit void-Rückgabe kann keine Ausnahmen abfangen, die die
Methode auslöst. Ab C# 7.0 kann eine asynchrone Methode jeden Task-ähnlichen Typ zurückgeben.
Im folgenden Beispiel ist DelayAsync eine asynchrone Methode, die eine Rückgabeanweisung besitzt, die eine
ganze Zahl zurückgibt. Da es sich um eine async-Methode handelt, muss die Methodendeklaration einen
Task<int> -Rückgabetyp haben. Da der Rückgabetyp Task<int> ist, ergibt die Auswertung des await -
Ausdrucks in DoSomethingAsync eine ganze Zahl, wie die folgende int result = await delayTask -Anweisung
veranschaulicht.

using System;
using System.Threading.Tasks;

class Program
{
static Task Main() => DoSomethingAsync();

static async Task DoSomethingAsync()


{
Task<int> delayTask = DelayAsync();
int result = await delayTask;

// The previous two statements may be combined into


// the following statement.
//int result = await DelayAsync();

Console.WriteLine($"Result: {result}");
}

static async Task<int> DelayAsync()


{
await Task.Delay(100);
return 5;
}
}
// Example output:
// Result: 5

Mit einer asynchronen Methode können keine in-, ref- oder out-Parameter deklariert, jedoch Methoden
aufgerufen werden, die solche Parameter aufweisen.
Weitere Informationen zu den asynchronen Methoden finden Sie unter Asynchrone Programmierung mit async
und await und Asynchrone Rückgabetypen (C#).

Ausdruckskörpermember
Es gibt häufig Methodendefinitionen, die einfach direkt das Ergebnis eines Ausdrucks zurückgeben oder eine
einzige Anweisung als Text der Methode aufweisen. Es ist eine Syntaxabkürzung zur Definition solcher
Methoden mithilfe von => verfügbar:

public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
public void Print() => Console.WriteLine(First + " " + Last);
// Works with operators, properties, and indexers too.
public static Complex operator +(Complex a, Complex b) => a.Add(b);
public string Name => First + " " + Last;
public Customer this[long id] => store.LookupCustomer(id);
Wenn die Methode void zurückgibt oder es sich um eine asynchrone Methode handelt, muss der Text der
Methode ein Anweisungsausdruck sein (wie bei Lambdas). Eigenschaften und Indexer müssen schreibgeschützt
sein. Verwenden Sie darüber hinaus nicht das get -Accessorschlüsselwort.

Iterators
Ein Iterator führt eine benutzerdefinierte Iteration durch eine Auflistung durch, z. B. eine Liste oder ein Array. Ein
Iterator verwendet die yield return -Anweisung, um jedes Element einzeln nacheinander zurückzugeben. Wenn
eine yield return -Anweisung erreicht wird, wird die aktuelle Position gespeichert, sodass der Aufrufer das
nächste Element in der Sequenz anfordern kann.
Der Rückgabetyp eines Iterators kann IEnumerable, IEnumerable<T>, IEnumerator oder IEnumerator<T> sein.
Weitere Informationen finden Sie unter Iteratoren.

Weitere Informationen
Zugriffsmodifizierer
Statische Klassen und statische Klassenmember
Vererbung
Abstrakte und versiegelte Klassen und Klassenmember
params
out
ref
in
Übergeben von Parametern
Eigenschaften
04.11.2021 • 9 minutes to read

Eigenschaften sind Bürger erster Klasse in C#. Die Sprache definiert die Syntax, mit der Entwickler Code
schreiben können, der genau ihre Entwurfsabsicht ausdrückt.
Eigenschaften verhalten sich wie Felder, wenn darauf zugegriffen wird. Jedoch sind Eigenschaften im Gegensatz
zu Feldern mit Accessoren implementiert, die die ausgeführten Anweisungen definieren, wenn auf eine
Eigenschaft zugegriffen oder sie zugewiesen wird.

Eigenschaftssyntax
Die Syntax für Eigenschaften ist eine natürliche Erweiterung von Feldern. Ein Feld definiert einen Speicherort:

public class Person


{
public string FirstName;
// remaining implementation removed from listing
}

Eine Eigenschaftendefinition enthält Deklarationen für einen get - und set -Accessor, der den Wert dieser
Eigenschaft abruft oder zuweist:

public class Person


{
public string FirstName { get; set; }

// remaining implementation removed from listing


}

Die oben dargestellte Syntax ist die Auto-Eigenschaft-Syntax. Der Compiler generiert den Speicherort für das
Feld, das die Eigenschaft sichert. Der Compiler implementiert außerdem den Text der get - und set -
Accessoren.
In einigen Fällen müssen Sie eine Eigenschaft auf einen anderen Wert als den Standardwert für seinen Datentyp
initialisieren. C# ermöglicht dies, indem nach der schließenden Klammer für die Eigenschaft ein Wert festgelegt
wird. Möglicherweise bevorzugen Sie den Anfangswert für die Eigenschaft FirstName als leere Zeichenfolge
und nicht null . Sie würden dies wie unten dargestellt angeben:

public class Person


{
public string FirstName { get; set; } = string.Empty;

// remaining implementation removed from listing


}

Die bestimmte Initialisierung eignet sich am besten für schreibgeschützte Eigenschaften, wie Sie später in
diesem Artikel sehen werden.
Sie können den Speicher auch selbst definieren, wie unten dargestellt:
public class Person
{
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
private string firstName;
// remaining implementation removed from listing
}

Wenn die Implementierung einer Eigenschaft ein einzelner Ausdruck ist, können Sie Ausdruckskörpermember
für die Getter oder Setter verwenden:

public class Person


{
public string FirstName
{
get => firstName;
set => firstName = value;
}
private string firstName;
// remaining implementation removed from listing
}

Diese vereinfachte Syntax wird in diesem Artikel immer dann verwendet, wenn es sich anbietet.
Die oben gezeigte Eigenschaftendefinition ist eine Schreib-Lese-Eigenschaft. Beachten Sie das Schlüsselwort
value im set-Accessor. Der set -Accessor verfügt immer über einen einzelnen Parameter namens value . Der
get -Accessor muss einen Wert zurückgeben, der in den Typ der Eigenschaft konvertiert werden kann ( string
in diesem Beispiel).
Das sind die Grundlagen der Syntax. Es gibt viele verschiedene Varianten, die eine Vielzahl von verschiedenen
Entwürfen unterstützen. Lassen Sie uns diese erforschen, und lernen Sie die Syntaxoptionen für jede kennen.

Szenarien
In den Beispielen oben wurde eine der einfachsten Fälle von Eigenschaftendefinition gezeigt: eine Schreib-Lese-
Eigenschaft ohne Überprüfung. Durch das Schreiben des Codes, den Sie in den get - und set -Accessoren
möchten, können Sie viele verschiedene Szenarios erstellen.
Validierung
Sie können Code im set -Accessor schreiben, um sicherzustellen, dass die durch eine Eigenschaft dargestellten
Werte immer gültig sind. Angenommen, eine Regel für die Person -Klasse besagt, dass der Name nicht leer sein
und keinen Leerraum enthalten darf. Sie würden das wie folgt schreiben:
public class Person
{
public string FirstName
{
get => firstName;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("First name must not be blank");
firstName = value;
}
}
private string firstName;
// remaining implementation removed from listing
}

Das vorausgehende Beispiel kann unter Verwendung eines throw -Ausdrucks als Teil der Validierung des
Eigenschaftensetters vereinfacht werden:

public class Person


{
public string FirstName
{
get => firstName;
set => firstName = (!string.IsNullOrWhiteSpace(value)) ? value : throw new ArgumentException("First
name must not be blank");
}
private string firstName;
// remaining implementation removed from listing
}

Das obige Beispiel erzwingt die Regel, dass der Vorname nicht leer sein und keinen Leerraum enthalten darf.
Wenn ein Entwickler schreibt

hero.FirstName = "";

Die Zuweisung löst eine ArgumentException aus. Da der set-Accessor einen void-Rückgabetyp aufweisen muss,
melden Sie Fehler im set-Accessor durch Auslösen einer Ausnahme.
Sie können die gleiche Syntax auf alles andere in Ihrem Szenario erweitern, was benötigt wird. Sie können die
Beziehungen zwischen unterschiedlichen Eigenschaften oder gegen externe Bedingungen überprüfen. Gültige
C#-Anweisungen sind in einem Eigenschaftenaccessor gültig.
Schreibgeschützt
Bis zu diesem Zeitpunkt sind alle Eigenschaftsdefinitionen, die Sie gesehen haben Lese-/Schreibeigenschaften
mit öffentlichen Accessoren. Dies sind nicht die einzige gültigen Eingabehilfen für Eigenschaften. Sie können
schreibgeschützte Eigenschaften erstellen, oder den set- und get-Accessoren verschiedene Eingabehilfen geben.
Angenommen, Ihre Person -Klasse sollte nur die Änderung des Werts der FirstName -Eigenschaft von anderen
Methoden in dieser Klasse ermöglichen. Sie konnten dem set-Accessor private -Eingabehilfen anstelle von
public geben:

public class Person


{
public string FirstName { get; private set; }

// remaining implementation removed from listing


}
Nun, kann auf die FirstName -Eigenschaft von einem beliebigen Code zugegriffen werden, aber es kann nur von
einem anderem Code in der Person -Klasse zugewiesen werden.
Sie können einen restriktiven Zugriffsmodifizierer zum set- oder get-Accessor hinzufügen. Jeder
Zugriffsmodifizierer, den Sie auf den einzelnen Accessor platzieren, muss eingeschränkter sein als der
Zugriffsmodifizierer für die Eigenschaftsdefinition. Das Obige ist zulässig, da die FirstName -Eigenschaft public
ist, aber der set-Accessor ist private . Sie können keine private -Eigenschaft mit einem public -Accessor
deklarieren. Eigenschaftendeklarationen können ebenfalls als protected , internal , protected internal oder
sogar private deklariert werden.
Es ist auch zulässig, den restriktiveren Modifizierer auf dem get -Accessor zu platzieren. Sie verfügen z.B. über
eine public -Eigenschaft, schränken jedoch den get -Accessor auf private ein. Dieses Szenario wird in der
Praxis nur selten ausgeführt.
Sie können auch Änderungen an einer Eigenschaft beschränken, sodass sie nur in einem Konstruktor oder
einem Eigenschafteninitialisierer festgelegt werden kann. Sie können die Person -Klasse daher wie folgt ändern:

public class Person


{
public Person(string firstName) => this.FirstName = firstName;

public string FirstName { get; }

// remaining implementation removed from listing


}

Diese Funktion wird am häufigsten für die Initialisierung von Auflistungen verwendet, die als schreibgeschützte
Eigenschaften verfügbar gemacht werden:

public class Measurements


{
public ICollection<DataPoint> points { get; } = new List<DataPoint>();
}

Berechnete Eigenschaften
Eine Eigenschaft muss nicht einfach den Wert eines Memberfelds zurückgeben. Sie können Eigenschaften
erstellen, die einen berechneten Wert zurückgeben. Lassen Sie uns das Person -Objekt so erweitern, dass es den
vollständigen Namen zurückgibt, berechnet durch die Verkettung des ersten und letzten Namens:

public class Person


{
public string FirstName { get; set; }

public string LastName { get; set; }

public string FullName { get { return $"{FirstName} {LastName}"; } }


}

Im Beispiel oben wird das Feature Zeichenfolgeninterpolation verwendet, um die formatierte Zeichenfolge für
den vollständigen Namen zu erstellen.
Sie können auch einen Ausdruckskörpermember verwenden, was eine kompaktere Möglichkeit zum Erstellen
der berechneten FullName -Eigenschaft darstellt:
public class Person
{
public string FirstName { get; set; }

public string LastName { get; set; }

public string FullName => $"{FirstName} {LastName}";


}

Ausdruckskörpermember verwenden die Syntax von Lambdaausdrücken zum Definieren einer Methode, die
einen einzelnen Ausdruck enthält. Hier gibt dieser Ausdruck den vollständigen Namen für das Person-Objekt
zurück.
Zwischengespeicherte ausgewertete Eigenschaften
Sie können das Konzept einer berechneten Eigenschaft mit dem Speicher mischen und eine
zwischengespeicherte ausgewertete Eigenschaft erstellen. Sie können z.B. die FullName -Eigenschaft so
aktualisieren, dass die Zeichenfolgenformatierung nur beim ersten Zugriff umgesetzt wird:

public class Person


{
public string FirstName { get; set; }

public string LastName { get; set; }

private string fullName;


public string FullName
{
get
{
if (fullName == null)
fullName = $"{FirstName} {LastName}";
return fullName;
}
}
}

Der obige Code enthält jedoch einen Fehler. Wenn der Code den Wert der FirstName - oder LastName -
Eigenschaft aktualisiert, ist das zuvor ausgewertete fullName -Feld ungültig. Sie müssen die set -Accessoren
der FirstName - und LastName -Eigenschaft aktualisieren, damit das fullName -Feld erneut berechnet wird:
public class Person
{
private string firstName;
public string FirstName
{
get => firstName;
set
{
firstName = value;
fullName = null;
}
}

private string lastName;


public string LastName
{
get => lastName;
set
{
lastName = value;
fullName = null;
}
}

private string fullName;


public string FullName
{
get
{
if (fullName == null)
fullName = $"{FirstName} {LastName}";
return fullName;
}
}
}

Diese endgültige Version wertet die FullName -Eigenschaft nur bei Bedarf aus. Wenn die zuvor berechnete
Version gültig ist, wird sie verwendet. Wenn eine andere Änderung des Zustands die zuvor berechnete Version
ungültig macht, wird sie neu berechnet. Entwickler, die diese Klasse verwenden, müssen die Details der
Implementierung nicht kennen. Keine dieser interne Änderungen hat Einfluss auf die Verwendung des Person-
Objekts. Das ist der Hauptgrund für die Verwendung von Eigenschaften, um Datenmember eines Objekts
verfügbar zu machen.
Anfügen von Attributen an automatisch implementierte Eigenschaften
Ab C# 7.3 können Feldattribute an das vom Compiler generierte Unterstützungsfeld in den automatisch
implementierten Eigenschaften angefügt werden. Sie sollten z.B. eine Überarbeitung der Person -Klasse
hinzufügen, die eine eindeutige Eigenschaft des Integers Id hinzufügt. Schreiben Sie die Id -Eigenschaft unter
Verwendung einer automatisch implementierten Eigenschaft. Für Ihren Entwurf ist es jedoch nicht erforderlich,
die Id -Eigenschaft zu speichern. Das NonSerializedAttribute-Attribut kann nur an Felder, aber nicht an
Eigenschaften angefügt werden. Sie können wie im folgenden Beispiel dargestellt das NonSerializedAttribute-
Attribut für die Id -Eigenschaft an das Unterstützungsfeld unter Verwendung des field: -Spezifizierers des
Attributs anfügen:
public class Person
{
public string FirstName { get; set; }

public string LastName { get; set; }

[field:NonSerialized]
public int Id { get; set; }

public string FullName => $"{FirstName} {LastName}";


}

Diese Technik funktioniert für jedes beliebige Attribut, das Sie an das Unterstützungsfeld für die automatisch
implementierte Eigenschaft anfügen.
Implementiert INotifyPropertyChanged
Ein abschließendes Szenario, in dem Sie Code in einem Eigenschaftenaccessor schreiben müssen, ist zur
Unterstützung der INotifyPropertyChanged-Schnittstelle, die verwendet wird, um Clients mit Datenbindung zu
benachrichtigen, dass ein Wert geändert wurde. Wenn sich der Wert einer Eigenschaft ändert, löst das Objekt
das INotifyPropertyChanged.PropertyChanged-Ereignis aus, um die Änderung anzuzeigen. Die Bibliotheken mit
Datenbindung wiederum aktualisieren die auf dieser Änderung basierenden Anzeigeelemente. Der folgende
Code zeigt, wie Sie INotifyPropertyChanged für die FirstName -Eigenschaft dieser Person-Klasse implementieren
würden.

public class Person : INotifyPropertyChanged


{
public string FirstName
{
get => firstName;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("First name must not be blank");
if (value != firstName)
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(FirstName)));
}
firstName = value;
}
}
private string firstName;

public event PropertyChangedEventHandler PropertyChanged;


// remaining implementation removed from listing
}

Der Operator ?. wird bedingter NULL-Operator genannt. Es sucht vor der Auswertung der rechten Seite des
Operators nach einem NULL-Verweis. Das Endergebnis bedeutet, dass, wenn sich keine Abonnenten im
PropertyChanged -Ereignis befinden, der Code zum Auslösen des Ereignisses nicht ausgeführt wird. Er würde
eine NullReferenceException auslösen, ohne diese in diesem Fall zu überprüfen. Weitere Informationen finden
Sie unter events . In diesem Beispiel wird auch der neue nameof -Operator verwendet, um vom Symbol des
Eigenschaftennamen in die Textdarstellung zu konvertieren. Mithilfe von nameof können Fehler reduziert
werden, bei denen Sie den Namen der Eigenschaft falsch eingegeben haben.
Erneut ist die Implementierung von INotifyPropertyChanged ein Beispiel für einen Fall, in dem Sie Code in Ihren
Accessoren schreiben können, um die benötigten Szenarios zu unterstützen.
Zusammenfassung
Eigenschaften sind eine Form intelligenter Felder in einer Klasse oder einem Objekt. Außerhalb des Objekts
wirken diese wie Felder in dem Objekt. Eigenschaften können jedoch mithilfe der vollständigen Palette der C#-
Funktionalität implementiert werden. Sie können Überprüfung, verschiedene Eingabehilfen, verzögerte
Auswertung oder alle Anforderungen, die Ihre Szenarios benötigen, bereitstellen.
Indexer
04.11.2021 • 9 minutes to read

Indexer sind ähnlich wie Eigenschaften. In vielen Aspekten bauen Indexer genauso wie Eigenschaften auf den
gleichen Sprachfunktionen auf. Indexer ermöglichen indizierte Eigenschaften: Eigenschaften, auf die von
mindestens einem Argument verwiesen wird. Diese Argumente stellen einen Index in einer Auflistung von
Werten bereit.

Indexersyntax
Sie können mithilfe eines Variablennamens und eckiger Klammern auf einen Indexer zugreifen. Geben Sie die
Indexerargumente innerhalb der Klammern ein:

var item = someObject["key"];


someObject["AnotherKey"] = item;

Sie können Indexer deklarieren, in dem sie das Schlüsselwort this als Eigenschaftenname verwenden und die
Argumente in eckigen Klammern deklarieren. Diese Deklaration stimmt mit dem Gebrauch in oben stehendem
Abschnitt überein:

public int this[string key]


{
get { return storage.Find(key); }
set { storage.SetAt(key, value); }
}

Dieses erste Beispiel veranschaulicht die Beziehung zwischen der Syntax der Eigenschaften und der Indexer.
Diese Analogie wirkt sich auf die meisten Syntaxregeln für Indexer auf. Indexer können beliebige gültige
Zugriffsmodifizierer aufweisen (public, protected internal, protected, internal, private oder private protected). Sie
können versiegelt, virtuell oder abstrakt sein. Bei Eigenschaften können Sie verschiedene Zugriffsmodifizierer
für die Get- und Set-Accessoren eines Indexers angeben. Außerdem können Sie schreibgeschützte Indexer
(indem Sie die set-Zugriffsmethode auslassen) oder lesegeschützte Indexer (indem Sie die get-Zugriffsmethode
auslassen) angeben.
Fast alles, was Sie von der Arbeit mit Eigenschaften kennen, können sie auch für Indexer anwenden. Die einzige
Ausnahme von dieser Regel sind automatisch implementierte Eigenschaften. Der Compiler kann nicht immer
angemessenen Speicher für einen Indexer generieren.
Indexer unterscheiden sich durch das Vorhandensein von Argumenten, die auf ein Element in einem Satz von
Elementen verweisen, von Eigenschaften. Sie können mehrere Indexer in einem Typ definieren, solange die
Argumentauflistung für jeden Indexer eindeutig ist. In den folgenden Szenarios erfahren Sie mehr über das
Verwenden von einem oder mehreren Indexern in einer Klassendefinition.

Szenarien
Sie definieren Indexer in Ihrem Typ, wenn dessen API eine Auflistung modelliert, in der Sie die Argumente der
Auflistung definieren. Ihre Indexer ordnen möglicherweise direkt zu Ihren Auflistungstypen zu, die Teil des .NET
Core Frameworks sind. Ihr Typ hat möglicherweise andere Verpflichtungen als nur das Modellieren einer
Auflistung. Mit Indexern können Sie die API bereitstellen, die mit der Abstraktion Ihres Typs übereinstimmt, ohne
die inneren Details offen zu legen, wie die Werte dieser Abstraktion gespeichert oder berechnet werden.
Die häufigsten Szenarios für das Verwenden von Indexern. Sie können auf den Beispielordner für Indexer
zugreifen. Anweisungen zum Herunterladen finden Sie unter Beispiele und Lernprogramme.
Arrays und Vektoren
Eines der häufigsten Szenarios beim Erstellen von Indexern ist, wenn Ihr Typ ein Array oder einen Vektor
modelliert. Sie können einen Indexer erstellen, um eine geordnete Datenliste zu modellieren.
Der Vorteil beim Erstellen eines eigenen Indexers ist die Tatsache, dass Sie den Speicher für diese Auflistung an
Ihre Bedürfnisse anpassen können. Stellen Sie sich ein Szenario vor, in dem Ihr Typ historische Daten modelliert,
die zu groß sind, um sie auf einmal in den Speicher zu laden. Sie müssen Abschnitte der Auflistung nach
Verbrauch laden und entladen. Das folgende Beispiel veranschaulicht dieses Verhalten. Es meldet, wie viele
Datenpunkte vorhanden sind. Es erstellt Seiten, die auf Abruf Abschnitte der Daten anzeigen. Es entfernt Seiten
aus dem Speicher, um Platz für Seiten zu schaffen, die für eine aktuelle Abfrage benötigt werden.

public class DataSamples


{
private class Page
{
private readonly List<Measurements> pageData = new List<Measurements>();
private readonly int startingIndex;
private readonly int length;
private bool dirty;
private DateTime lastAccess;

public Page(int startingIndex, int length)


{
this.startingIndex = startingIndex;
this.length = length;
lastAccess = DateTime.Now;

// This stays as random stuff:


var generator = new Random();
for(int i=0; i < length; i++)
{
var m = new Measurements
{
HiTemp = generator.Next(50, 95),
LoTemp = generator.Next(12, 49),
AirPressure = 28.0 + generator.NextDouble() * 4
};
pageData.Add(m);
}
}
public bool HasItem(int index) =>
((index >= startingIndex) &&
(index < startingIndex + length));

public Measurements this[int index]


{
get
{
lastAccess = DateTime.Now;
return pageData[index - startingIndex];
}
set
{
pageData[index - startingIndex] = value;
dirty = true;
lastAccess = DateTime.Now;
}
}

public bool Dirty => dirty;


public DateTime LastAccess => lastAccess;
}
}

private readonly int totalSize;


private readonly List<Page> pagesInMemory = new List<Page>();

public DataSamples(int totalSize)


{
this.totalSize = totalSize;
}

public Measurements this[int index]


{
get
{
if (index < 0)
throw new IndexOutOfRangeException("Cannot index less than 0");
if (index >= totalSize)
throw new IndexOutOfRangeException("Cannot index past the end of storage");

var page = updateCachedPagesForAccess(index);


return page[index];
}
set
{
if (index < 0)
throw new IndexOutOfRangeException("Cannot index less than 0");
if (index >= totalSize)
throw new IndexOutOfRangeException("Cannot index past the end of storage");
var page = updateCachedPagesForAccess(index);

page[index] = value;
}
}

private Page updateCachedPagesForAccess(int index)


{
foreach (var p in pagesInMemory)
{
if (p.HasItem(index))
{
return p;
}
}
var startingIndex = (index / 1000) * 1000;
var newPage = new Page(startingIndex, 1000);
addPageToCache(newPage);
return newPage;
}

private void addPageToCache(Page p)


{
if (pagesInMemory.Count > 4)
{
// remove oldest non-dirty page:
var oldest = pagesInMemory
.Where(page => !page.Dirty)
.OrderBy(page => page.LastAccess)
.FirstOrDefault();
// Note that this may keep more than 5 pages in memory
// if too much is dirty
if (oldest != null)
pagesInMemory.Remove(oldest);
}
pagesInMemory.Add(p);
}
}

Sie können diesem Entwurfsausdruck folgen, um jede beliebige Auflistungsart dort zu modellieren, wo es gute
Gründe gibt, nicht den gesamten Datensatz in eine Auflistung im Speicher zu laden. Beachten Sie, dass die
Klasse Page eine private geschachtelte Klasse ist, die nicht Teil der öffentlichen Schnittstelle ist. Diese
Informationen sind für jeden Benutzer der Klasse verborgen.
Wörterbücher
Ein weiteres häufiges Szenario ist, wenn Sie ein Wörterbuch oder eine Zuordnung modellieren möchten. In
diesem Szenario speichert Ihr Typ Werte nach Schlüsseln, typischerweise Textschlüssel. In diesem Beispiel wird
ein Wörterbuch erstellt, das Befehlszeilenargumente Lambdaausdrücken zuordnet, die diese Optionen
verwalten. In folgenden Beispiel werden zwei Klassen gezeigt: eine ArgsActions -Klasse, die eine
Befehlszeilenoption einem Action -Delegaten zuordnet, und ein ArgsProcessor , um mit jeder ArgsActions eine
Action auszuführen, wenn es auf diese Option trifft.

public class ArgsProcessor


{
private readonly ArgsActions actions;

public ArgsProcessor(ArgsActions actions)


{
this.actions = actions;
}

public void Process(string[] args)


{
foreach(var arg in args)
{
actions[arg]?.Invoke();
}
}

}
public class ArgsActions
{
readonly private Dictionary<string, Action> argsActions = new Dictionary<string, Action>();

public Action this[string s]


{
get
{
Action action;
Action defaultAction = () => {} ;
return argsActions.TryGetValue(s, out action) ? action : defaultAction;
}
}

public void SetOption(string s, Action a)


{
argsActions[s] = a;
}
}

In diesem Beispiel ordnet die ArgsAction -Auflistung nah an der zugrundeliegenden Auflistung zu. get legt fest,
ob eine gegebene Option konfiguriert wurde. Falls dem so ist, gibt es die mit dieser Option verknüpfte Action
zurück. Falls dem nicht so ist, gibt es eine Action zurück, die keine Auswirkungen hat. Die öffentliche
Zugriffsmethode enthält keine set -Zugriffsmethode. Stattdessen enthält Sie den Entwurf, der eine öffentliche
Methode für das Festlegen von Optionen verwendet.
Mehrdimensionale Zuordnungen
Sie können Indexer erstellen, die mehrere Argumente verwenden. Zusätzlich sind müssen diese Argumente
nicht denselben Typen aufweisen. Nachfolgend sehen Sie zwei Beispiele.
Das erste Beispiel zeigt eine Klasse, die Werte einer Mandelbrot-Menge generiert. Weitere Informationen zur
Mathematik der Menge finden Sie in diesem Artikel. Der Indexer verwendet zwei double-Werte, um einen Punkt
auf der X-Y-Ebene zu definieren. Die get-Zugriffsmethode berechnet die Anzahl der Iterationen, bis ein Punkt
festgelegt wird, der außerhalb der Menge liegt. Wenn die maximale Zahl an Iterationen erreicht ist, befindet sich
der Punkt in der Menge, und der Wert „maxIterations“ der Klasse wird zurückgegeben. (Die vom Computer
generierten Bilder, die durch die Mandelbrot-Menge bekannt wurden, definieren Farben für die Anzahl von
Iterationen, die benötigt werden, um festzustellen, das sich ein Punkt außerhalb der Menge befindet.)

public class Mandelbrot


{
readonly private int maxIterations;

public Mandelbrot(int maxIterations)


{
this.maxIterations = maxIterations;
}

public int this [double x, double y]


{
get
{
var iterations = 0;
var x0 = x;
var y0 = y;

while ((x*x + y * y < 4) &&


(iterations < maxIterations))
{
var newX = x * x - y * y + x0;
y = 2 * x * y + y0;
x = newX;
iterations++;
}
return iterations;
}
}
}

Die Mandelbrot-Menge definiert Werte an jeder (x, y)-Koordinaten für reelle Zahlenwerte. Dadurch wird ein
Wörterbuch definiert, das eine unendliche Anzahl von Werten enthalten könnte. Deshalb liegt kein Speicher
hinter der Menge. Stattdessen berechnet diese Klasse den Wert für jeden Punkt, wenn die get -Zugriffsmethode
von Code aufgerufen wird. Es wird kein zugrundeliegender Speicher verwendet.
Hier ist ein letztes Beispiel für den Gebrauch von Indexern, in dem der Indexer mehrere Argumente mit
unterschiedlichen Typen akzeptiert. Stellen Sie sich ein Programm vor, dass Daten zu historischen Temperaturen
verwaltet. Dieser Indexer verwendet eine Stadt und ein Datum, um die höchste und niedrigste Temperatur für
diesen Ort festzulegen oder abzurufen:
using DateMeasurements =
System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>;
using CityDataMeasurements =
System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<System.DateTime,
IndexersSamples.Common.Measurements>>;

public class HistoricalWeatherData


{
readonly CityDataMeasurements storage = new CityDataMeasurements();

public Measurements this[string city, DateTime date]


{
get
{
var cityData = default(DateMeasurements);

if (!storage.TryGetValue(city, out cityData))


throw new ArgumentOutOfRangeException(nameof(city), "City not found");

// strip out any time portion:


var index = date.Date;
var measure = default(Measurements);
if (cityData.TryGetValue(index, out measure))
return measure;
throw new ArgumentOutOfRangeException(nameof(date), "Date not found");
}
set
{
var cityData = default(DateMeasurements);

if (!storage.TryGetValue(city, out cityData))


{
cityData = new DateMeasurements();
storage.Add(city, cityData);
}

// Strip out any time portion:


var index = date.Date;
cityData[index] = value;
}
}
}

In diesem Beispiel wird ein Indexer erstellt, der Wetterdaten zwei Argumenten zuordnet: eine Stadt
(repräsentiert durch string ) und ein Datum (repräsentiert durch DateTime ). Der interne Speicher verwendet
zwei Dictionary -Klassen, die das zweidimensionale Wörterbuch repräsentieren. Die öffentliche API
repräsentiert nicht mehr den zugrundeliegenden Speicher. Stattdessen können Sie mit den Sprachfunktionen
der Indexer eine öffentliche Schnittstelle erstellen, die Ihre Abstraktion repräsentiert, auch wenn der
zugrundeliegende Speicher unterschiedliche Kernauflistungstypen verwenden muss.
Es gibt zwei Teile dieses Codes, die Entwicklern möglicherweise unbekannt sind. Diese beiden using -
Anweisungen:

using DateMeasurements = System.Collections.Generic.Dictionary<System.DateTime,


IndexersSamples.Common.Measurements>;
using CityDataMeasurements = System.Collections.Generic.Dictionary<string,
System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>>;

erstellen einen Alias für einen konstruierter generischen Typen. Durch diese Anweisungen kann ein Code später
die deskriptiveren Namen DateMeasurements und CityDateMeasurements statt der generischen Konstruktion von
Dictionary<DateTime, Measurements> und Dictionary<string, Dictionary<DateTime, Measurements> > verwenden.
Diese Konstruktion erfordert den Gebrauch des vollqualifizierten Typnamens auf der rechten Seite des = -
Zeichens.
In der zweiten Vorgehensweise entfernen Sie die Uhrzeitteile jedes DateTime -Objekts, das verwendet wird, um
in die Auflistungen zu indizieren. .NET bietet keinen reinen Datumstyp. Entwickler verwenden den DateTime -
Typ, aber die Date -Eigenschaft, um sicherzustellen, das jedes DateTime -Objekt dieses Tages gleich ist.

Schlussbemerkung
Sie sollten Indexer immer dann erstellen, wenn Sie ein Element, das einer Eigenschaft ähnelt, in Ihrer Klasse
vorliegen haben, und wenn diese Eigenschaft nicht einen einzelnen Wert repräsentiert, sondern eine Auflistung
von Werten, in der jedes einzelne Element durch einen Satz von Argumenten identifiziert wird. Diese Argumente
können eindeutig identifizieren, auf welches Element in der Auflistung verwiesen werden sollte. Indexer
erweitern das Konzept der Eigenschaften, bei denen ein Member wie ein Datenelement außerhalb der Klasse
behandelt wird, sondern wie eine Methode innerhalb der Klasse. Indexer ermöglichen es Argumenten, ein
einzelnes Element in einer Eigenschaft zu finden, das einen Satz von Elementen repräsentiert.
Iterators
04.11.2021 • 6 minutes to read

Bei fast jedem Programm, das Sie schreiben, muss eine Auflistung durchlaufen werden. Sie schreiben Code, der
jedes Element in einer Auflistung überprüft.
Sie erstellen auch Iteratormethoden, die einen Iterator für die Elemente dieser Klasse erzeugen. Ein Iterator ist
ein Objekt, das einen Container durchläuft, insbesondere Listen. Iteratoren können für Folgendes verwendet
werden:
Ausführen einer Aktion für jedes Element in einer Auflistung
Enumerieren einer benutzerdefinierten Auflistung
Erweitern von LINQ oder anderen Bibliotheken
Erstellen einer Datenpipeline, in der Daten Iteratormethoden effizient durchlaufen
Die Programmiersprache C# bietet Funktionen zum Generieren und Verarbeiten von Sequenzen. Diese
Sequenzen können synchron oder asynchron erzeugt und verarbeitet werden. Dieser Artikel enthält eine
Übersicht über diese Funktionen.

Durchlaufen mit foreach


Das Enumerieren einer Auflistung ist einfach: Das foreach -Schlüsselwort enumeriert eine Auflistung und führt
die eingebettete Anweisung einmal für jedes Element in der Auflistung aus:

foreach (var item in collection)


{
Console.WriteLine(item.ToString());
}

Das ist alles. Für das Durchlaufen aller Inhalte einer Auflistung benötigen Sie nur die foreach -Anweisung. Die
foreach -Anweisung ist jedoch nicht magisch. Sie beruht auf zwei generischen Schnittstellen, die in der .NET
Core-Bibliothek definiert sind, um den Code für das Durchlaufen einer Auflistung zu generieren: IEnumerable<T>
und IEnumerator<T> . Dieser Mechanismus wird unten ausführlich erläutert.
Für diese beiden Schnittstellen gibt es auch nicht generische Entsprechungen: IEnumerable und IEnumerator .
Die generischen Versionen werden für modernen Code bevorzugt.
Wenn eine Sequenz asynchron generiert wird, können Sie die await foreach -Anweisung verwenden, um die
Sequenz asynchron zu verarbeiten:

await foreach (var item in asyncSequence)


{
Console.WriteLine(item.ToString());
}

Wenn eine Sequenz ein System.Collections.Generic.IEnumerable<T> ist, verwenden Sie foreach . Wenn eine
Sequenz ein System.Collections.Generic.IAsyncEnumerable<T> ist, verwenden Sie await foreach . Im letzteren
Fall wird die Sequenz asynchron generiert.

Enumerationsquellen mit Iteratormethoden


Mit einer weiteren großartigen Funktion der Programmiersprache C# können Sie Methoden konstruieren, die
eine Quelle für eine Enumeration erstellen. Diese Methoden werden als Iteratormethoden bezeichnet. Eine
Iteratormethode definiert, wie die Objekte in einer Sequenz bei Anforderung generiert werden. Sie verwenden
die yield return -Kontextschlüsselwörter, um eine Iteratormethode zu definieren.
Sie könnten diese Methode schreiben, um die Sequenz von ganzen Zahlen von 0 bis 9 zu erstellen:

public IEnumerable<int> GetSingleDigitNumbers()


{
yield return 0;
yield return 1;
yield return 2;
yield return 3;
yield return 4;
yield return 5;
yield return 6;
yield return 7;
yield return 8;
yield return 9;
}

Der obige Code zeigt verschiedene yield return -Anweisungen, die verdeutlichen, dass Sie mehrere diskrete
yield return -Anweisungen in einer Iteratormethode verwenden können. Sie können (und werden auch oft)
andere Sprachkonstrukte zur Vereinfachung des Codes einer Iteratormethode verwenden. Die folgende
Methodendefinition erzeugt genau dieselbe Sequenz von Zahlen:

public IEnumerable<int> GetSingleDigitNumbersLoop()


{
int index = 0;
while (index < 10)
yield return index++;
}

Sie müssen sich nicht für eine davon entscheiden. Sie können so viele yield return -Anweisungen verwenden
wie für Ihre Methode erforderlich:

public IEnumerable<int> GetSetsOfNumbers()


{
int index = 0;
while (index < 10)
yield return index++;

yield return 50;

index = 100;
while (index < 110)
yield return index++;
}

Alle obigen Beispiele hätten eine asynchrone Entsprechung. In jedem Fall ersetzen Sie den Rückgabetyp von
IEnumerable<T> durch ein IAsyncEnumerable<T> . Das vorherige Beispiel würde beispielsweise die folgende
asynchrone Version aufweisen:
public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
int index = 0;
while (index < 10)
yield return index++;

await Task.Delay(500);

yield return 50;

await Task.Delay(500);

index = 100;
while (index < 110)
yield return index++;
}

Dies ist die Syntax für synchrone und asynchrone Iteratoren. Sehen wir uns ein reales Beispiel an. Angenommen,
Sie arbeiten an einem IoT-Projekt und die Gerätesensoren generieren einen sehr großen Datenstrom. Um ein
Gefühl für die Daten zu bekommen, können Sie eine Methode schreiben, die bei jedem n-ten Datenelement eine
Stichprobe durchführt. Diese kleine Iteratormethode ist der Trick dabei:

public static IEnumerable<T> Sample<T>(this IEnumerable<T> sourceSequence, int interval)


{
int index = 0;
foreach (T item in sourceSequence)
{
if (index++ % interval == 0)
yield return item;
}
}

Wenn der Messwert vom IoT-Gerät eine asynchrone Sequenz erzeugt, ändern Sie die Methode wie im
Folgenden dargestellt:

public static async IAsyncEnumerable<T> Sample<T>(this IAsyncEnumerable<T> sourceSequence, int interval)


{
int index = 0;
await foreach (T item in sourceSequence)
{
if (index++ % interval == 0)
yield return item;
}
}

Es gibt eine wichtige Einschränkung bei Iteratormethoden: Eine return -Anweisung und eine yield return -
Anweisung können nicht in derselben Methode enthalten sein. Der folgende Code wird nicht kompiliert:
public IEnumerable<int> GetSingleDigitNumbers()
{
int index = 0;
while (index < 10)
yield return index++;

yield return 50;

// generates a compile time error:


var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
return items;
}

Diese Einschränkung ist normalerweise kein Problem. Sie können in der Methode entweder durchgehend
yield return verwenden oder die ursprüngliche Methode in mehrere Methoden aufteilen, von denen einige
return und einige yield return verwenden.

Sie können die letzte Methode leicht ändern und so yield return überall verwenden:

public IEnumerable<int> GetFirstDecile()


{
int index = 0;
while (index < 10)
yield return index++;

yield return 50;

var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
foreach (var item in items)
yield return item;
}

Manchmal ist die beste Lösung, eine Iteratormethode in zwei verschiedene Methoden aufzuteilen. Die eine
verwendet dann return und die zweite verwendet yield return . Betrachten Sie eine Situation, in der Sie eine
leere Auflistung oder die ersten fünf ungeraden Zahlen basierend auf einem booleschen Argument zurückgeben
möchten. Sie können das mit diesen zwei Methoden schreiben:

public IEnumerable<int> GetSingleDigitOddNumbers(bool getCollection)


{
if (getCollection == false)
return new int[0];
else
return IteratorMethod();
}

private IEnumerable<int> IteratorMethod()


{
int index = 0;
while (index < 10)
{
if (index % 2 == 1)
yield return index;
index++;
}
}

Betrachten Sie die oben genannten Methoden. Die erste Methode verwendet die return -Standardanweisung,
um entweder eine leere Auflistung oder den Iterator, der durch die zweite Methode erstellt wurde,
zurückzugeben. Die zweite Methode verwendet die yield return -Anweisung, um die angeforderte Reihenfolge
zu erstellen.
Tieferer Einblick in foreach
Die -Anweisung wird in einen Standardausdruck erweitert, der die Schnittstellen IEnumerable<T> und
foreach
IEnumerator<T> für das Durchlaufen aller Elemente einer Auflistung verwendet. Außerdem werden Fehler
minimiert, die Entwickler durch falsche Ressourcenverwaltung verursachen.
Der Compiler übersetzt die im ersten Beispiel gezeigte foreach -Schleife in etwas wie dieses Konstrukt:

IEnumerator<int> enumerator = collection.GetEnumerator();


while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}

Der genaue vom Compiler generierte Code ist komplizierter und behandelt Situationen, in denen das von
GetEnumerator() zurückgegebene Objekt die IDisposable -Schnittstelle implementiert. Der durch vollständige
Erweiterung generierte Code sieht eher wie folgt aus:

{
var enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of enumerator.
}
}

Der Compiler übersetzt das erste asynchrone Beispiel in ein Konstrukt wie das folgende:

{
var enumerator = collection.GetEnumerator();
try
{
while (await enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of async enumerator.
}
}

Die Art und Weise, wie der Enumerator verworfen wird, hängt von den Merkmalen des Typs von enumerator ab.
Im allgemeinen synchronen Fall wird die finally -Klausel wie folgt erweitert:
finally
{
(enumerator as IDisposable)?.Dispose();
}

Im allgemeinen asynchronen Fall sieht die Erweiterung wie folgt aus:

finally
{
if (enumerator is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
}

Wenn es sich bei dem Typ von enumerator jedoch um einen versiegelten Typ handelt und es keine implizite
Konvertierung vom Typ von enumerator zu IDisposable oder IAsyncDisposable gibt, wird die finally -Klausel
zu einem leeren Block erweitert:

finally
{
}

Wenn es eine implizite Konvertierung des Typs von enumerator zu IDisposable gibt und enumerator keine
NULL-Werte zulässt, wird die finally -Klausel wie folgt erweitert:

finally
{
((IDisposable)enumerator).Dispose();
}

Glücklicherweise müssen Sie sich nicht alle diese Details merken. Die foreach -Anweisung kümmert sich für Sie
um alle diese Nuancen. Der Compiler generiert den korrekten Code für jedes dieser Konstrukte.
Einführung in Delegaten
04.11.2021 • 2 minutes to read

Delegaten bieten einen Mechanismus mit später Bindung in .NET. Späte Bindung bedeutet, dass Sie einen
Algorithmus erstellen, in dem der Aufrufer auch mindestens eine Methode bereitstellt, die einen Teil des
Algorithmus implementiert.
Sie können z.B. eine Liste von Sternen in einer Astronomie-Anwendung sortieren. Sie können diese Sterne nach
ihrer Entfernung von der Erde, nach der Größe des Sterns oder ihrer wahrgenommenen Helligkeit sortieren.
In all diesen Fällen macht die Sort()-Methode im Prinzip das Gleiche: Sie ordnet die Elemente in der Liste
basierend auf einen Vergleich. Der Code, der zwei Sterne vergleicht, ist bei jeder Sortierreihenfolge
unterschiedlich.
Diese Arten von Lösungen wurden in der Softwareentwicklung für ein halbes Jahrhundert verwendet. Das
Delegatenkonzept von C# bietet erstklassige Sprachunterstützung und Typsicherheit rund um das Konzept.
Wie Sie später in dieser Serie sehen werden, ist der C#-Code, den Sie für Algorithmen wie diesen schreiben,
typsicher und nutzt die Sprachregeln und den Compiler, um sicherzustellen, dass die Typen mit Argumenten und
Rückgabetypen übereinstimmen.
Funktionszeiger wurden C# 9 für ähnliche Szenarios hinzugefügt, in denen Sie mehr Kontrolle über die
Aufrufkonvention benötigen. Der einem Delegaten zugeordnete Code wird mithilfe einer virtuellen Methode
aufgerufen, die einem Delegattypen hinzugefügt wird. Mithilfe von Funktionszeigern können Sie
unterschiedliche Konventionen angeben.

Sprachliche Entwurfsziele für Delegaten


Die Sprachentwickler haben mehrere Ziele für die Funktion aufgezählt, die schließlich Delegaten geworden sind.
Das Team wollte ein allgemeines Sprachkonstrukt entwerfen, das für alle Algorithmen mit später Bindung
verwendet werden kann. Mit Delegaten können Softwareentwickler ein Konzept erlernen und dasselbe Konzept
bei vielen verschiedenen Softwareproblemen anwenden.
Außerdem sollten sowohl einzelne als auch Multicast-Methodenaufrufe unterstützt werden. (Multicastdelegaten
sind Delegaten, mit denen mehrere Methodenaufrufe verkettet werden. Beispiele hierfür finden Sie in einem der
folgenden Artikel dieser Reihe.)
Delegaten sollten dieselbe Typsicherheit unterstützen, die Entwickler von allen C#-Konstrukten erwarten.
Die Sprachentwickler haben schließlich erkannt, dass ein Ereignismuster ein bestimmtes Muster ist, für das
Delegaten – bzw. jeder beliebige Algorithmus mit später Bindung – sehr nützlich sind. Daher sollte sichergestellt
werden, dass der Code für den Delegaten die Basis für das .NET-Ereignismuster bereitstellen konnte.
Das Ergebnis dieser Arbeit war die Delegat- und Ereignisunterstützung in C# und .NET. Die übrigen Artikel in
diesem Abschnitt behandeln die Sprachfunktionen, die Bibliotheksunterstützung und die allgemeinen
Ausdrücke, die bei der Arbeit mit Delegaten verwendet werden.
Sie werden das delegate -Schlüsselwort kennenlernen und erfahren, welchen Code es generiert. Außerdem
werden Sie die Funktionen der System.Delegate -Klasse kennenlernen und erfahren, wie diese Funktionen
verwendet werden. Darüber hinaus erfahren Sie, wie Sie typsichere Delegaten erstellen und wie Sie Methoden
erstellen, die durch Delegaten aufgerufen werden können. Sie erfahren, wie Sie mithilfe von Lambdaausdrücken
mit Delegaten und Ereignissen arbeiten und an welcher Stelle Delegaten einer der Bausteine für LINQ werden.
Darüber hinaus erfahren Sie, warum Delegaten die Grundlage für das .NET-Ereignismuster sind und wie sie sich
unterscheiden.
Fangen wir also an.
Nächste
System.Delegate und das delegate -Schlüsselwort
04.11.2021 • 5 minutes to read

Zurück
Dieser Artikel enthält Informationen zu den Klassen in .NET, die Delegaten unterstützen, und wie diese dem
Schlüsselwort delegate zugeordnet werden.

Definieren von Delegattypen


Wir beginnen mit dem Schlüsselwort „delegate“, da Sie dieses beim Arbeiten mit Delegaten hauptsächlich
verwenden werden. Der Code, der vom Compiler bei Verwendung des delegate -Schlüsselworts generiert wird,
verweist auf Methodenaufrufe, die Member der Klassen Delegate und MulticastDelegate aufrufen.
Sie definieren einen Delegattyp, der Syntax verwendet, die dem Definieren einer Methodensignatur ähnlich ist.
Sie fügen das delegate -Schlüsselwort einfach der Definition hinzu.
Wir verwenden die List.Sort()-Methode weiterhin als Beispiel. Der erste Schritt ist die Erstellung eines Typs für
den Vergleichsdelegaten:

// From the .NET Core library

// Define the delegate type:


public delegate int Comparison<in T>(T left, T right);

Der Compiler generiert eine von System.Delegate abgeleitete Klasse, die der verwendeten Signatur entspricht
(in diesem Fall eine Methode, die eine ganze Zahl zurückgibt und über zwei Argumente verfügt). Der Typ des
Delegaten ist Comparison . Der Delegattyp Comparison ist ein generischer Typ. Weitere Informationen zu
Generics finden Sie hier.
Beachten Sie, dass es so scheinen kann, als ob die Syntax eine Variable deklariert, aber tatsächlich deklariert sie
einen Typ. Sie können Delegattypen innerhalb von Klassen, direkt in Namespaces oder sogar im globalen
Namespace definieren.

NOTE
Es wird nicht empfohlen, Delegattypen (oder andere Typen) direkt im globalen Namespace zu deklarieren.

Der Compiler generiert auch Handler zum Hinzufügen und Entfernen für diesen neuen Typ, damit Clients dieser
Klasse Methoden der Aufrufliste einer Instanz hinzufügen und entfernen können. Der Compiler erzwingt, dass
die Signatur der Methode, die hinzugefügt oder entfernt wird, der Signatur bei der Deklaration der Methode
entspricht.

Deklarieren von Instanzen von Delegaten


Nach dem Definieren des Delegats können Sie eine Instanz dieses Typs erstellen. Wie alle Variablen in C#
können Sie Delegatinstanzen nicht direkt in einem Namespace oder im globalen Namespace deklarieren.
// inside a class definition:

// Declare an instance of that type:


public Comparison<T> comparator;

Der Typ der Variable ist Comparison<T> , der zuvor definierte Delegattyp. Der Name der Variablen ist comparator .
Dieser obenstehende Codeausschnitt hat eine Membervariable innerhalb einer Klasse deklariert. Sie können
auch Delegatvariablen deklarieren, die lokale Variablen oder Argumente von Methoden sind.

Aufrufen von Delegaten


Sie rufen die Methoden in der Aufrufliste eines Delegaten auf, indem Sie diesen Delegaten aufrufen. In der
Sort() -Methode ruft der Code die Vergleichsmethode an, um zu bestimmen, in welcher Reihenfolge er die
Objekte anordnet:

int result = comparator(left, right);

In der Zeile darüber ruft der Code die Methode auf, die an den Delegaten angefügt ist. Sie behandeln die
Variable als Methodenname und rufen sie mit der üblichen Syntax für Methodenaufrufe auf.
Diese Codezeile macht eine unsichere Annahme: Es gibt keine Garantie, dass dem Delegaten ein Ziel
hinzugefügt wurde. Wenn keine Ziele angehängt wurden, löst die Zeile darüber eine NullReferenceException
aus. Die Ausdrücke, die verwendet werden, um dieses Problem zu beheben, sind komplizierter als eine einfache
NULL-Überprüfung und werden später in dieser Reihe behandelt.

Zuweisen, Hinzufügen und Entfernen von Aufrufzielen


So werden Delegattypen definiert und Delegatinstanzen deklariert und aufgerufen.
Entwickler, die die List.Sort() -Methode verwenden möchten, müssen eine Methode definieren, deren Signatur
der Definition des Delegattypen entspricht, und diese dem Delegaten zuweisen, der von der sort-Methode
verwendet wird. Diese Zuordnung fügt die Methode der Aufrufliste des Delegatobjekts hinzu.
Nehmen wir an, Sie möchten eine Liste von Zeichenfolgen nach Länge sortieren. Die Vergleichsfunktion kann
folgende sein:

private static int CompareLength(string left, string right) =>


left.Length.CompareTo(right.Length);

Die Methode wird als private Methode deklariert. Das ist in Ordnung. Möglicherweise möchten Sie nicht, dass
diese Methode Teil der öffentlichen Schnittstelle ist. Sie kann dennoch als Vergleichsmethode verwendet
werden, wenn sie an einen Delegaten angefügt wird. Der aufrufende Code wird diese Methode an die Zielliste
des Delegatobjekts anfügen lassen und kann über diesen Delegaten darauf zugreifen.
Sie erstellen diese Beziehung durch Übergeben dieser Methode an die List.Sort() -Methode:

phrases.Sort(CompareLength);

Beachten Sie, dass der Name der Methode ohne Klammern verwendet wird. Das Verwenden der Methode als
Argument teilt dem Compiler mit, dass er den Methodenverweis in einen Verweis konvertieren soll, der als
Delegataufrufziel verwendet werden kann, und diese Methode als Aufrufziel anfügen soll.
Sie könnten auch explizit eine Variable vom Typ Comparison<string> deklarieren und diese zuweisen:
Comparison<string> comparer = CompareLength;
phrases.Sort(comparer);

Wenn es sich bei der als Delegatziel verwendeten Methode um eine kleine Methode handelt, wird gewöhnlich
die Lambdaausdruck-Syntax für die Durchführung der Zuweisung verwendet:

Comparison<string> comparer = (left, right) => left.Length.CompareTo(right.Length);


phrases.Sort(comparer);

Die Verwendung von Lambdaausdrücken für Delegatziele wird in einem späteren Abschnitt ausführlicher
behandelt.
Das Sort()-Beispiel fügt dem Delegaten typischerweise eine einzelne Zielmethode an. Allerdings unterstützen
Delegatobjekte Aufruflisten mit mehreren Zielmethoden, die an ein Delegatobjekt angefügt sind.

Delegatklassen und MulticastDelegate-Klassen


Die oben beschriebene Sprachunterstützung bietet die Funktionen und Unterstützung, die Sie in der Regel bei
der Arbeit mit Delegaten benötigen. Diese Features beruhen auf zwei Klassen im .NET Core Framework:
Delegate und MulticastDelegate.
Die System.Delegate -Klasse und ihre einzige direkte untergeordnete Klasse System.MulticastDelegate bieten die
Framework-Unterstützung für das Erstellen von Delegaten, das Registrieren von Methoden als Delegatziele und
das Aufrufen aller Methoden, die als Delegatziel registriert sind.
Interessanterweise sind die Klassen System.Delegate und System.MulticastDelegate selbst keine Delegattypen.
Sie bieten die Grundlage für alle spezifischen Delegattypen. Derselbe Sprachentwurfsprozess verlangte, dass
keine Klasse deklariert werden kann, die von Delegate oder MulticastDelegate abgeleitet wird. Die C#-
Sprachregeln verbieten dies.
Stattdessen erstellt der C#-Compiler Instanzen einer von MulticastDelegate abgeleiteten Klasse, wenn Sie das
C#-Schlüsselwort verwenden, um Delegattypen zu deklarieren.
Dieser Entwurf hat seinen Ursprung in der ersten Version von C# und .NET. Ein Ziel des Entwurfsteams war
sicherzustellen, dass die Sprache bei der Verwendung von Delegaten Typsicherheit erzwingt. Dies bedeutete
sicherzustellen, dass Delegaten mit dem richtigen Typ und der richtigen Anzahl von Argumenten aufgerufen
werden. Zudem sollte jeder Rückgabetyp zur Kompilierzeit richtig angegeben werden. Delegaten waren Teil von
.NET Version 1.0, die es vor Generics gab.
Die beste Möglichkeit, diese Typsicherheit zu erzwingen, bestand darin, dass der Compiler die konkreten
Delegatklassen erstellt, die die verwendete Methodensignatur darstellten.
Obwohl Sie abgeleitete Klassen nicht direkt erstellen können, verwenden Sie die Methoden, die auf diesen
Klassen definiert sind. Nun sehen wir uns die am häufigsten verwendeten Methoden an, die Sie beim Arbeiten
mit Delegaten verwenden werden.
An erster Stelle müssen Sie beachten, dass jeder Delegat, mit dem Sie arbeiten, von MulticastDelegate
abgeleitet ist. Ein Multicastdelegat bedeutet, dass mehr als ein Methodenziel beim Aufrufen über einen
Delegaten aufgerufen werden kann. Der ursprüngliche Entwurf sah eine Unterscheidung vor zwischen
Delegaten, bei denen nur eine Zielmethode angefügt und aufgerufen werden konnte, und Delegaten, bei denen
mehrere Zielmethoden angefügt und aufgerufen werden konnten. Diese Unterscheidung erwies sich in der
Praxis jedoch als weniger nützlich als ursprünglich gedacht. Die zwei verschiedenen Klassen wurden bereits
erstellt und befinden sich seit der ersten Veröffentlichung im Framework.
Die Methoden, die Sie bei der Arbeit mit Delegaten am häufigsten verwenden, sind Invoke() und
BeginInvoke() / EndInvoke() . Invoke() ruft alle Methoden auf, die an eine bestimmte Delegatinstanz angefügt
wurden. Wie oben gezeigt, werden Delegaten in der Regel mit der Aufrufsyntax-Methode auf der
Delegatvariablen aufgerufen. Wie Sie später in dieser Reihe sehen werden, gibt es Muster, die direkt mit diesen
Methoden arbeiten.
Nun haben Sie die Sprachsyntax und die Klassen gesehen, die Delegaten unterstützen. Als Nächstes untersuchen
wir, wie stark typisierte Delegaten verwendet, erstellt und aufgerufen werden.
Nächste
Stark typisierte Delegate
04.11.2021 • 2 minutes to read

Zurück
Im vorherigen Artikel haben Sie gesehen, dass bestimmte Delegaten mithilfe des delegate -Schlüsselworts
erstellen.
Die abstrakte Klasse für den Delegaten stellt die Infrastruktur für die lose Kopplung und den losen Aufruf bereit.
Konkrete Delegattypen werden durch die Einführung und das Erzwingen von Typsicherheit für die Methoden,
die zur Aufrufliste für ein Delegatobjekt hinzugefügt werden, viel nützlicher. Wenn Sie das delegate -
Schlüsselwort verwenden und einen konkreten Delegattyp definieren, generiert der Compiler diese Methoden.
In der Praxis würde dies zur Erstellung neuer Delegattypen führen, wann immer Sie eine andere
Methodensignatur benötigen. Diese Arbeit könnte mit der Zeit ermüdend werden. Jede neue Funktion erfordert
neue Delegattypen.
Glücklicherweise ist dies nicht erforderlich. Der .NET Core Framework enthält verschiedene Typen, die Sie
wiederverwenden können, wenn Sie Delegattypen benötigen. Dies sind generische Definitionen, damit Sie
Anpassungen deklarieren können, wenn Sie neue Methodendeklarationen benötigen.
Der erste dieser Typen ist der Action-Typ, und verschiedene Varianten:

public delegate void Action();


public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
// Other variations removed for brevity.

Die in -Modifizierer im generischen Typargument wird in diesem Artikel in Kovarianz behandelt.


Es gibt Variationen des Action -Delegaten, der bis zu 16 Argumente enthält, wie z.B.
Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16>. Es ist wichtig, dass diese Definitionen andere
generische Argumente für jeden der Delegatargumente verwenden: Das bietet Ihnen maximale Flexibilität. Die
Methodenargumente müssen nicht, aber können der gleiche Typ sein.
Verwenden Sie einen der Action -Typen für jeden Delegattyp, der über einen void-Rückgabetyp verfügt.
Das Framework enthält auch mehrere generische Delegattypen, die Sie für Delegattypen verwenden können, die
Werte zurückgeben:

public delegate TResult Func<out TResult>();


public delegate TResult Func<in T1, out TResult>(T1 arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
// Other variations removed for brevity

Der out -Modifizierer im Ergebnisargument des generischen Typs wird in diesem Artikel in Kovarianz
behandelt.
Es gibt Variationen des Func -Delegaten mit bis zu 16 Eingabeargumenten, wie z.B.
Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult>. Der Typ des Ergebnisses ist immer der
letzte Typparameter in allen Func -Deklarationen, gemäß der Konvention.
Verwenden Sie einen der Func -Typen für jeden Delegattyp, der einen Wert zurückgibt.
Außerdem gibt es einen spezialisierten Predicate<T>-Typ für einen Delegaten, der einen Test für einen einzelnen
Wert zurückgibt:

public delegate bool Predicate<in T>(T obj);

Möglicherweise haben Sie bemerkt, dass für jeden Predicate -Typ ein strukturell äquivalenter Func -Typ
vorhanden ist, beispielsweise:

Func<string, bool> TestForString;


Predicate<string> AnotherTestForString;

Sie denken möglicherweise, dass diese beiden Typen äquivalent sind. Das sind sie nicht. Diese beiden Variablen
sind nicht austauschbar. Eine Variable eines Typs kann dem anderen Typ nicht zugewiesen werden. Das C#-
Typsystem verwendet die Namen der definierten Typen, nicht die Struktur.
Alle diese Definitionen von Delegattypen in der .NET Core-Bibliothek sollten bedeuten, dass Sie keinen neuen
Delegattypen für jede neue Funktion, die Sie erstellen und die Delegaten erfordert, definieren müssen. Diese
generischen Definitionen sollten alle Delegattypen bereitstellen, die Sie in den meisten Fällen benötigen. Sie
können einen dieser Typen mit den erforderlichen Parametern einfach instanziieren. Im Falle von Algorithmen,
die generisch vorgenommen werden können, können diese Delegaten als generische Typen verwendet werden.
Dies sollte Zeit sparen und die Anzahl neuer Typen minimieren, die Sie erstellen müssen, um mit Delegaten zu
arbeiten.
Im nächsten Artikel sehen Sie einige allgemeine Muster für die praktische Arbeit mit Delegaten.
Nächste
Gängige Muster für Delegate
04.11.2021 • 8 minutes to read

Vorherige
Delegaten bieten einen Mechanismus, der Software-Entwürfe ermöglicht, die minimale Kopplung zwischen
Komponenten umfassen.
Ein hervorragendes Beispiel für diese Art von Design ist LINQ. Das LINQ-Abfrageausdrucksmuster stützt sich
auf Delegaten für alle Funktionen. Betrachten Sie folgendes einfaches Beispiel:

var smallNumbers = numbers.Where(n => n < 10);

Die Sequenz von Zahlen wird auf nur die, die kleiner als der Wert 10 sind, gefiltert. Die Where -Methode
verwendet einen Delegaten, der bestimmt, welche Elemente einer Sequenz den Filter passieren. Wenn Sie eine
LINQ-Abfrage erstellen, geben Sie die Implementierung des Delegaten für diesen bestimmten Zweck an.
Der Prototyp für die Where-Methode ist:

public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool>


predicate);

Dieses Beispiel wird mit allen Methoden wiederholt, die Teil von LINQ sind. Alle basieren auf Delegaten für den
Code, der die jeweilige Abfrage verwaltet. Dieses API-Entwurfsmuster ist ein leistungsfähiges Muster zum
Erlernen und Verstehen.
In diesem einfachen Beispiel wird veranschaulicht, wie Delegaten nur wenig Kopplung zwischen Komponenten
erfordern. Sie müssen keine Klasse erstellen, die von einer bestimmten Basisklasse abgeleitet ist. Sie müssen
keine bestimmte Schnittstelle implementieren. Die einzige Voraussetzung ist die Bereitstellung der
Implementierung einer Methode, die für den jeweiligen Task unerlässlich ist.

Erstellen eigener Komponenten mit Delegaten


Erstellen Sie für dieses Beispiel mithilfe eines Entwurfs, das auf Delegaten basiert, eine Komponente.
Definieren wir eine Komponente, die in einem umfangreichen System für Protokollmeldungen verwendet
werden kann. Bibliothekskomponenten können in vielen verschiedenen Umgebungen auf mehreren
verschiedenen Plattformen verwendet werden. Es gibt zahlreiche allgemeine Funktionen in der Komponente, die
die Protokolle verwalten. Es muss Meldungen von jeder Komponente im System annehmen. Diese Meldungen
werden über unterschiedliche Prioritäten verfügen, die die Kernkomponente verwalten kann. Die Meldungen
sollten einen Zeitstempel in ihrer endgültigen archivierten Form aufweisen. Für komplexere Szenarios können
Sie Meldungen von der Quellkomponente filtern.
Es gibt ein Aspekt der Funktion, der sich häufig ändern wird: An welchem Standort Meldungen geschrieben
werden. In einigen Umgebungen können sie in der Fehlerkonsole geschrieben werden. In anderen Fällen in
einer Datei. Andere können beispielsweise Datenbankspeicher, Betriebssystem-Ereignisprotokolle oder andere
Dokumentspeicher sein.
Es gibt auch Ausgabekombinationen, die in verschiedenen Szenarios verwendet werden könnten.
Möglicherweise möchten Sie Meldungen an die Konsole und in eine Datei schreiben.
Ein Entwurf, basierend auf den Delegaten bietet ein hohes Maß an Flexibilität, und erleichtert Ihnen die
Unterstützung von Speichermechanismen, die in der Zukunft möglicherweise hinzugefügt werden.
Durch dieses Design kann die primäre Protokollkomponente eine nicht virtuelle, sogar eine versiegelte Klasse
sein. Sie können eine beliebige Anzahl von Delegaten angeben, um die Meldungen in unterschiedliche
Speichermedien zu schreiben. Die integrierte Unterstützung für Multicastdelegaten erleichtert die Unterstützung
von Szenarios, in denen Meldungen an mehreren Standorten (eine Datei und eine Konsole) geschrieben werden
müssen.

Erste Implementierung
Fangen wir klein an: Die anfängliche Implementierung akzeptiert neue Meldungen, und schreibt mithilfe von
angefügten Delegaten. Sie können mit einem Delegaten beginnen, der Meldungen in die Konsole schreibt.

public static class Logger


{
public static Action<string> WriteMessage;

public static void LogMessage(string msg)


{
WriteMessage(msg);
}
}

Die statische Klasse oben ist die einfachste Sache, die funktionieren kann. Wir müssen die einzelne
Implementierung für die Methode schreiben, die Meldungen in die Konsole schreibt:

public static class LoggingMethods


{
public static void LogToConsole(string message)
{
Console.Error.WriteLine(message);
}
}

Abschließend müssen Sie den Delegaten verknüpfen, indem Sie ihn an den WriteMessage-Delegaten anfügen,
der in der Protokollierung deklariert wurde:

Logger.WriteMessage += LoggingMethods.LogToConsole;

Methoden
Unser Beispiel ist bisher recht einfach, aber es veranschaulicht immer noch einige der wichtigen Richtlinien für
Entwürfe, die Delegaten umfassen.
Delegattypen zu verwenden, die im Kernframework definiert sind, erleichtert Benutzern die Arbeit mit den
Delegaten. Sie müssen keine neue Typen definieren, und Entwickler, die die Bibliothek nutzen, müssen keine
neuen, spezialisierten Delegattypen erlernen.
Die verwendeten Schnittstellen sind so minimal und so flexibel wie möglich: Um eine neue
Ausgabeprotokollierung zu erstellen, müssen Sie eine Methode erstellen. Diese Methode kann eine statische
Methode oder eine Instanzmethode sein. Es kann über jeden Zugriff verfügen.

Formatieren der Ausgabe


Lassen Sie uns die erste Version etwas stabiler machen und anschließend andere Protokollierungsmechanismen
erstellen.
Als Nächstes fügen wir einige Argumente in die LogMessage() -Methode ein, damit Ihre Protokollklasse mehr
strukturierte Meldungen erstellt:

public enum Severity


{
Verbose,
Trace,
Information,
Warning,
Error,
Critical
}

public static class Logger


{
public static Action<string> WriteMessage;

public static void LogMessage(Severity s, string component, string msg)


{
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
WriteMessage(outputMsg);
}
}

Als Nächstes verwenden wir dieses Severity -Argument, um die Meldungen zu filtern, die in das
Ausgabeprotokoll gesendet werden.

public static class Logger


{
public static Action<string> WriteMessage;

public static Severity LogLevel {get;set;} = Severity.Warning;

public static void LogMessage(Severity s, string component, string msg)


{
if (s < LogLevel)
return;

var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";


WriteMessage(outputMsg);
}
}

Methoden
Sie haben der Protokollierungsinfrastruktur neue Funktionen hinzugefügt. Da die Protokollierungskomponente
sehr lose an Ausgabemechanismen gekoppelt ist, können diese neuen Funktionen ohne Auswirkung auf den
Code, der die Protokollierungsdelegaten implementiert, hinzugefügt werden.
Solange Sie dies erstellen, sehen Sie weitere Beispiele, wie diese lose Verbindung mehr Flexibilität bei der
Aktualisierung von Teilen der Site ohne Änderungen an anderen Speicherorten ermöglicht. In der Tat könnten
die Klassen der Protokollierungsausgabe in einer umfangreicheren Anwendung in einer anderen Assembly sein.
Sie müssen auch nicht neu erstellt werden.

Erstellen einer zweiten Ausgabe-Engine


Die Protokollierungskomponente kommt gut voran. Fügen wir eine weitere Ausgabe-Engine hinzu, die
Meldungen in einer Datei protokolliert. Dies wird eine etwas komplexere Ausgabe-Engine werden. Es wird eine
Klasse sein, die die Dateivorgänge kapselt, und sicherstellt, dass die Datei nach jedem Schreibvorgang immer
geschlossen wird. Dadurch wird sichergestellt, dass alle Daten auf den Datenträger geschrieben werden,
nachdem jede Meldung generiert wurde.
Hier ist diese dateibasierte-Protokollierung:

public class FileLogger


{
private readonly string logPath;
public FileLogger(string path)
{
logPath = path;
Logger.WriteMessage += LogMessage;
}

public void DetachLog() => Logger.WriteMessage -= LogMessage;


// make sure this can't throw.
private void LogMessage(string msg)
{
try
{
using (var log = File.AppendText(logPath))
{
log.WriteLine(msg);
log.Flush();
}
}
catch (Exception)
{
// Hmm. We caught an exception while
// logging. We can't really log the
// problem (since it's the log that's failing).
// So, while normally, catching an exception
// and doing nothing isn't wise, it's really the
// only reasonable option here.
}
}
}

Wenn Sie diese Klasse erstellt haben, können Sie sie instanziieren und sie fügt ihre LogMessage-Methode in die
Protokollierungskomponente an:

var file = new FileLogger("log.txt");

Diese beiden schließen einander nicht aus. Sie können beide Protokollmethoden anfügen, und Meldungen in die
Konsole und eine Datei generieren:

var fileOutput = new FileLogger("log.txt");


Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized
earlier

Später können Sie auch in derselben Anwendung einen Delegaten ohne andere Probleme auf dem System
entfernen:

Logger.WriteMessage -= LoggingMethods.LogToConsole;

Methoden
Sie haben nun einen zweiten Ausgabehandler für das Protokollierungs-Subsystem hinzugefügt. Dieses Objekt
benötigt ein wenig mehr Infrastruktur, um das Dateisystem ordnungsgemäß zu unterstützen. Bei dem Delegat
handelt es sich um eine Instanzmethode. Es ist auch eine private Methode. Es besteht keine Notwendigkeit für
höhere Verfügbarkeit, da die Delegat-Infrastruktur die Delegaten verbinden kann.
Zweitens ermöglicht der delegatbasierte Entwurf mehrere Ausgabemethoden ohne zusätzlichen Code. Sie
müssen keine zusätzliche Infrastruktur zur Unterstützung von mehreren Ausgabemethoden erstellen. Sie
erhalten einfach eine andere Methode auf der Aufrufliste.
Achten Sie besonders auf den Code in der Ausgabemethode, die die Datei protokolliert. Es wird codiert, um
sicherzustellen, dass keine Ausnahmen ausgelöst werden. Obwohl dies nicht immer unbedingt erforderlich ist,
ist es häufig sinnvoll. Wenn eine der Delegatmethoden eine Ausnahme auslöst, werden die im Aufruf
verbleibenden Delegaten nicht aufgerufen werden.
Ein letzter Hinweis: Die Dateiprotokollierung muss ihre Ressourcen durch Öffnen und Schließen der Datei in
jeder Protokollmeldung verwalten. Sie können die Datei offen lassen und IDisposable implementieren, um die
Datei zu schließen, wenn Sie fertig sind. Beide Methoden haben Vor- und Nachteile. Beide erstellen etwas mehr
Kopplung zwischen den Klassen.
Kein Teil des Codes in der Logger -Klasse müsste aktualisiert werden, um beide Szenarios zu unterstützen.

Verarbeiten von NULL-Delegaten


Zum Schluss aktualisieren wir die LogMessage-Methode, damit es für jene Fälle stabil ist, wenn kein
Ausgabemechanismus ausgewählt wurde. Die aktuelle Implementierung löst eine NullReferenceException aus,
wenn der WriteMessage -Delegat nicht über eine angefügte Aufrufliste verfügt. Möglicherweise bevorzugen Sie
einen Entwurf, der im Hintergrund fortgesetzt wird, wenn keine Methoden angefügt wurden. Dies lässt sich
mithilfe des bedingten NULL-Operators, kombiniert mit der Delegate.Invoke() -Methode, leicht umsetzen:

public static void LogMessage(string msg)


{
WriteMessage?.Invoke(msg);
}

Der bedingte NULL-Operator ( ?. ) wird verkürzt, wenn der linke Operand ( WriteMessage in diesem Fall) NULL
ist, was bedeutet, dass nicht versucht wurde, eine Meldung zu protokollieren.
Sie werden die Invoke()-Methode nicht in der Dokumentation für System.Delegate oder
System.MulticastDelegate aufgelistet finden. Der Compiler generiert eine typsicherer Invoke -Methode für
jeden deklarierten Delegattyp. In diesem Beispiel bedeutet das, dass Invoke ein einzelnes string -Argument
nimmt und über einen void-Rückgabetyp verfügt.

Zusammenfassung der Methoden


Sie haben die Anfänge einer Protokollkomponente gesehen, die mit anderen Writern und anderen Funktionen
erweitert werden konnte. Mithilfe von Delegaten im Entwurf sind diese verschiedenen Komponenten lose
gekoppelt. Dies bietet mehrere Vorteile. Es ist einfach, neue Ausgabemechanismen zu erstellen und diese dem
System anzufügen. Diese Mechanismen benötigen nur eine Methode: Die Methode, die die Protokollmeldungen
schreibt. Es ist ein Entwurf, der stabil ist, wenn neue Funktionen hinzugefügt werden. Der für alle Writer
erforderliche Vertrag dient dazu, eine Methode zu implementieren. Diese Methode kann eine statische oder
Instanzmethode sein. Es kann sich um einen öffentlichen, privaten oder jeden anderen rechtlichen Zugriff
handeln.
Die Protokollierungsklasse kann eine beliebige Anzahl von Verbesserungen oder Änderungen vornehmen, ohne
wichtige Änderungen einzuführen. Wie jede Klasse können Sie die öffentliche API nicht ohne das Risiko von
wichtigen Änderungen ändern. Aber da die Kopplung zwischen der Protokollierung und den Ausgabe-Engines
nur über den Delegaten stattfindet, sind keine anderen Typen (z.B. Schnittstellen oder Basisklassen) beteiligt. Die
Kopplung ist so klein wie möglich.
Nächste
Einführung in Ereignisse
04.11.2021 • 3 minutes to read

Zurück
Ereignisse sind, wie Delegaten, ein Mechanismus mit später Bindung. In der Tat werden Ereignisse basierend auf
der Sprachunterstützung für Delegaten erstellt.
Ereignisse sind eine Möglichkeit für ein Objekt (für alle interessierten Komponenten im System) weiterzugeben,
dass etwas passiert ist. Jede andere Komponente kann das Ereignis abonnieren, und benachrichtigt werden,
wenn ein Ereignis ausgelöst wird.
Sie haben wahrscheinlich Ereignisse in einigen ihrer Programmierungen verwendet. Viele grafische Systeme
verfügen über ein Ereignismodell, um über Benutzerinteraktion zu berichten. Diese Ereignisse berichten über die
Mausbewegung, das Betätigen der Schaltflächen und ähnliche Interaktionen. Dies ist eines der am häufigsten
verwendeten, aber sicherlich nicht das einzige Szenario, in dem Ereignisse verwendet werden.
Sie können Ereignisse, die für Ihre Klassen ausgelöst werden sollen, definieren. Ein wichtiger Aspekt bei der
Arbeit mit Ereignissen ist, dass es möglicherweise kein registriertes Objekt für ein bestimmtes Ereignis gibt. Sie
müssen Ihren Code so schreiben, damit er keine Ereignisse auslöst, wenn keine Listener konfiguriert sind.
Das Abonnieren eines Ereignisses erstellt auch eine Kopplung zwischen zwei Objekten (die Ereignisquelle und
die Ereignissenke). Sie müssen sicherstellen, dass sich die Ereignissenke von der Ereignisquelle abmeldet, wenn
Sie nicht mehr an Ereignissen interessiert ist.

Entwurfsziele für die Ereignisunterstützung


Der Sprachentwurf für Ereignisse ist auf diese Ziele ausgerichtet:
Aktivieren Sie eine sehr geringe Kopplung zwischen der Ereignisquelle und einer Ereignissenke. Diese
beiden Komponenten können nicht von derselben Organisation geschrieben werden und werden
möglicherweise sogar auf völlig unterschiedliche Zeitpläne aktualisiert.
Es sollte sehr einfach sein, ein Ereignis zu abonnieren, und sich von demselben Ereignis abzumelden.
Ereignisquellen sollten mehrere Ereignisabonnenten unterstützen. Es sollte auch unterstützen, dass es
keine angefügten Ereignisabonnenten gibt.
Sie sehen, dass die Ziele für Ereignisse, den Zielen für Delegaten sehr ähnlich sind. Deshalb basiert die
Sprachunterstützung für Ereignisse auf der Sprachunterstützung für Delegaten.

Sprachunterstützung für Ereignisse


Die Syntax zum Definieren von Ereignissen, und zum Abonnieren oder Abmelden von Ereignissen, ist eine
Erweiterung der Syntax für Delegaten.
Zum Definieren eines Ereignisses verwenden Sie das event -Schlüsselwort:

public event EventHandler<FileListArgs> Progress;

Der Typ des Ereignisses ( EventHandler<FileListArgs> in diesem Beispiel) muss ein Delegattyp sein. Es gibt eine
Reihe von Konventionen, die Sie befolgen sollten, wenn Sie ein Ereignis deklarieren. Normalerweise verfügt der
Delegattyp des Ereignisses über einen „void“-Rückgabetyp. Ereignisdeklarationen sollten ein Verb oder eine
Verbalphrase sein. Verwenden Sie die Vergangenheitsform, wenn das Ereignis meldet, dass etwas geschehen ist.
Verwenden Sie ein Gegenwartsverb (z.B. Closing ) um etwas zu melden, das geschehen wird. Häufig gibt die
Verwendung der Gegenwartsform an, dass die Klasse eine Art der Anpassung des Verhaltens unterstützt. Eines
der häufigsten Szenarios ist die Unterstützung des Abbruchs. Angenommen, ein Closing -Ereignis enthält ein
Argument, das angeben würde, ob der Schließvorgang fortgesetzt werden soll oder nicht. Andere Szenarios
ermöglichen es Aufrufern, das Verhalten zu ändern, indem die Eigenschaften der Ereignisargumente aktualisiert
werden. Sie können ein Ereignis auslösen, um eine vorgeschlagene nächste Aktion anzugeben, die einen
Algorithmus auslösen wird. Der Ereignishandler kann eine andere Aktion vorgeben, indem er die Eigenschaften
des Ereignisarguments ändert.
Wenn Sie das Ereignis auslösen möchten, rufen Sie mithilfe der Aufrufsyntax des Delegaten den Ereignishandler
auf:

Progress?.Invoke(this, new FileListArgs(file));

Wie im Abschnitt unter Delegaten beschrieben, macht der ?. Operator es leicht, sicherzustellen, dass Sie nicht
versuchen das Ereignis auszulösen, wenn keine Abonnenten für dieses Ereignis vorhanden sind.
Sie abonnieren ein Ereignis mithilfe des += -Operators:

EventHandler<FileListArgs> onProgress = (sender, eventArgs) =>


Console.WriteLine(eventArgs.FoundFile);

fileLister.Progress += onProgress;

Die Handlermethode weist in der Regel das Präfix „On“ auf, gefolgt vom Ereignisnamen, wie oben gezeigt.
Sie melden sich mithilfe des -= -Operators ab:

fileLister.Progress -= onProgress;

Sie müssen eine lokale Variable für den Ausdruck deklarieren, der den Ereignishandler darstellt. Damit wird
sichergestellt, dass UNSUBSCRIBE den Handler entfernt. Wenn Sie stattdessen den Text des Lambdaausdrucks
verwendet haben, versuchen Sie einen Handler, der nicht angefügt wurde und nichts tut, zu entfernen.
Im nächsten Artikel erfahren Sie mehr über das typische Ereignismuster und verschiedene Varianten dieses
Beispiels.
Nächste
Standardereignismuster in .NET
04.11.2021 • 8 minutes to read

Zurück
.NET-Ereignisse folgen in der Regel einigen bekannten Mustern. Standardisierung auf diese Muster bedeutet,
dass Entwickler Kenntnisse über diese Standardmuster nutzen können, die auf ein beliebiges .NET
Ereignisprogramm angewendet werden können.
Schauen wir uns diese Standardmuster an, sodass Sie über alle Kenntnisse verfügen, die Sie benötigen, um
Standardereignisquellen zu erstellen und Standardereignisse in Ihrem Code zu abonnieren und zu verarbeiten.

Ereignisdelegatsignaturen
Die Standardsignatur für ein .NET-Ereignisdelegat lautet:

void OnEventRaised(object sender, EventArgs args);

Der Rückgabetyp ist void. Ereignisse basieren auf Delegaten und sind Multicastdelegaten. Dies unterstützt
mehrere Abonnenten für jede Ereignisquelle. Der einzelne Rückgabewert einer Methode wird nicht auf mehrere
Ereignisabonnenten skaliert. Welchen Rückgabewert findet die Ereignisquelle nach dem Auslösen eines
Ereignisses vor? In diesem Artikel sehen Sie später, wie Sie Ereignisprotokolle zur Unterstützung der
Ereignisabonnenten, die Informationen an die Ereignisquelle senden, erstellen.
Die Argumentliste enthält zwei Argumente: Den Absender und die Ereignisargumente. Der Kompilierzeittyp von
sender ist System.Object , obwohl Sie wahrscheinlich einen stärker abgeleiteten Typ kennen, die immer korrekt
wäre. Verwenden Sie gemäß der Konvention object .
Das zweite Argument ist in der Regel ein Typ, der von System.EventArgs abgeleitet ist. (Im nächsten Abschnitt
sehen Sie, dass diese Konvention nicht mehr erzwungen wird.) Wenn Ihr Ereignistyp keine weiteren Argumente
benötigt, geben Sie trotzdem beide Argumente an. Es gibt einen speziellen Wert, EventArgs.Empty , den Sie
verwenden sollten, um anzugeben, dass Ihr Ereignis keine weiteren Informationen enthält.
Erstellen Sie eine Klasse, die Dateien in einem Verzeichnis oder einem seiner Unterverzeichnisse, die einem
Muster folgen, auflistet. Diese Komponente löst ein Ereignis für jede gefundene Datei aus, die mit dem Muster
übereinstimmt.
Ein Ereignismodell bietet einige Vorteile beim Entwurf. Sie können mehrere Ereignislistener erstellen, die
verschiedene Aktionen ausführen, wenn eine gesuchte Datei gefunden wird. Das Kombinieren der
verschiedenen Listener kann robustere Algorithmen erstellen.
Hier ist die erste Ereignisargumentdeklaration für die Suche nach einer gesuchten Datei:

public class FileFoundArgs : EventArgs


{
public string FoundFile { get; }

public FileFoundArgs(string fileName)


{
FoundFile = fileName;
}
}
Obwohl dieser Typ wie ein kleiner, Nur-Daten-Typ aussieht, sollten Sie die Konvention einhalten und ihm einen
Verweis ( class )-Typ geben. Dies bedeutet, dass das Argumentobjekt als Verweis übergeben wird, und alle
Updates der Daten von allen Abonnenten gesehen werden. Die erste Version ist ein unveränderliches Objekt. Sie
sollten die Eigenschaften im Ereignisargumenttyp auf unveränderlich einstellen. Auf diese Weise kann ein
Abonnent die Werte nicht ändern, bevor ein anderer Abonnent sie sieht. (Es gibt Ausnahmen dafür, wie Sie
unten sehen werden.)
Als Nächstes müssen wir die Ereignisdeklaration in der FileSearcher-Klasse erstellen. Die Nutzung des
EventHandler<T> -Typ bedeutet, dass Sie nicht noch eine Typdefinition erstellen müssen. Sie verwenden einfach
eine generische Spezialisierung.
Füllen wir die FileSearcher-Klasse aus, um nach Dateien mit dem Muster zu suchen und das richtige Ereignis
auszulösen, wenn eine Übereinstimmung gefunden wird.

public class FileSearcher


{
public event EventHandler<FileFoundArgs> FileFound;

public void Search(string directory, string searchPattern)


{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
FileFound?.Invoke(this, new FileFoundArgs(file));
}
}
}

Definieren und Auslösen von feldähnlichen Ereignissen


Die einfachste Möglichkeit, Ihrer Klasse ein Ereignis hinzuzufügen, ist das Deklarieren des Ereignisses als
öffentliches Feld, wie im vorherigen Beispiel:

public event EventHandler<FileFoundArgs> FileFound;

Dies scheint ein öffentliches Feld zu deklarieren, was als schlechte objektorientierte Vorgehensweise erscheinen
würde. Sie möchten den Datenzugriff über Eigenschaften oder Methoden schützen. Während dies anscheinend
kein empfehlenswertes Verfahren ist, erstellt der vom Compiler generierte Code Wrapper, damit auf die
Ereignisobjekte nur auf sichere Weise zugegriffen werden kann. Die einzig verfügbaren Vorgänge für ein
feldähnliches Ereignis sind das Hinzufügen von Handlern:

EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>


{
Console.WriteLine(eventArgs.FoundFile);
filesFound++;
};

fileLister.FileFound += onFileFound;

und das Entfernen von Handlern:

fileLister.FileFound -= onFileFound;

Beachten Sie, dass eine lokale Variable für den Handler vorhanden ist. Wenn Sie den Text des Lambda-Ausdrucks
verwenden, würde das Entfernen nicht ordnungsgemäß funktionieren. Es wäre eine andere Instanz des
Delegaten, und es würde stillschweigend nichts geschehen.
Code außerhalb der Klasse kann das Ereignis nicht auslösen, noch kann er andere Vorgänge ausführen.

Rückgabe von Werten aus Ereignisabonnenten


Ihre einfache Version funktioniert einwandfrei. Fügen wir eine weitere Funktion hinzu: Abbruch.
Beim Auslösen des gefunden Ereignisses sollten Listener die weitere Verarbeitung beenden können, wenn diese
Datei die letzte gesuchte Datei ist.
Die Ereignishandler geben keinen Wert zurück. Daher müssen Sie dies auf andere Weise kommunizieren. Das
Standardereignismuster verwendet das EventArgs-Objekt, um Felder einzuschließen, die Ereignisabonnenten
zum Kommunizieren von „Abbrechen“ verwenden.
Es gibt zwei unterschiedliche Muster, die verwendet werden können, auf der Grundlage der Semantik des
Vertrags „Abbrechen“. In beiden Fällen werden Sie ein boolesches Feld zum EventArguments für das gefundene
Dateiereignis hinzufügen.
Ein Muster ermöglicht jedem Abonnenten, den Vorgang abzubrechen. Für dieses Muster wird ein neues Feld mit
false initialisiert. Jeder Abonnent kann es in true ändern. Nachdem alle Abonnenten das ausgelöste Ereignis
gesehen haben, untersucht die Komponente FileSearcher den booleschen Wert und ergreift Maßnahmen.
Das zweite Muster würde nur den Vorgang abbrechen, wenn alle Abonnenten den Vorgang abgebrochen haben
möchten. In diesem Muster wird das neue Feld initialisiert, um anzugeben, dass der Vorgang abgebrochen
werden soll, und jeder Abonnent kann dies ändern, um anzugeben, dass der Vorgang fortgesetzt werden soll.
Nachdem alle Abonnenten das ausgelöste Ereignis gesehen haben, untersucht die Komponente FileSearcher
den booleschen Wert und ergreift Maßnahmen. Es gibt einen zusätzlichen Schritt in diesem Muster: Die
Komponente muss wissen, ob jeder Abonnent das Ereignis gesehen hat. Wenn keine Abonnenten vorhanden
sind, würde das Feld fälschlicherweise einen Abbruch angeben.
Implementieren wir die erste Version für dieses Beispiel. Sie müssen ein boolesches Feld mit dem Namen
CancelRequested dem FileFoundArgs -Typ hinzufügen:

public class FileFoundArgs : EventArgs


{
public string FoundFile { get; }
public bool CancelRequested { get; set;}

public FileFoundArgs(string fileName)


{
FoundFile = fileName;
}
}

Dieses neue Feld wird automatisch mit false initialisiert, dem Standardwert für ein boolesches Feld, damit Sie
nicht versehentlich abbrechen. Die einzige andere Änderung der Komponente ist das Überprüfen des Flag nach
dem Auslösen des Ereignisses, um festzustellen, ob einer der Abonnenten einen Abbruch angefordert hat:

public void List(string directory, string searchPattern)


{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
var args = new FileFoundArgs(file);
FileFound?.Invoke(this, args);
if (args.CancelRequested)
break;
}
}
Ein Vorteil dieses Musters ist, dass es keine unterbrechende Änderung ist. Keiner der Abonnenten hat vorher
einen Abbruch angefordert und macht es noch immer nicht. Kein Teil des Abonnentencodes benötigt eine
Aktualisierung, sofern sie das neue Abbrechen-Protokoll nicht unterstützen möchten. Es ist sehr lose gekoppelt.
Aktualisieren wir den Abonnenten, damit ein Abbruch angefordert wird, sobald die erste ausführbare Datei
gefunden wird:

EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>


{
Console.WriteLine(eventArgs.FoundFile);
eventArgs.CancelRequested = true;
};

Hinzufügen einer anderen Ereignisdeklaration


Fügen wir eine weitere Funktion hinzu, und zeigen andere Sprachausdrücke für Ereignisse. Fügen wir eine
Überladung der Search -Methode, die alle Unterverzeichnisse auf der Suche nach Dateien durchsucht.
In einem Verzeichnis mit vielen Unterverzeichnisse könnte dies ein längerer Vorgang werden. Fügen wir ein
Ereignis hinzu, das zu Beginn jeder neuen Verzeichnissuche ausgelöst wird. Dies ermöglicht es Abonnenten, den
Fortschritt zu verfolgen und den Benutzer während des Fortschritts zu aktualisieren. Die Beispiele, die Sie bisher
erstellt haben, sind öffentlich. Dieses erstellen wir als internes Ereignis. Das bedeutet, dass Sie auch die Typen für
die Argumente intern erstellen können.
Sie beginnen mit dem Erstellen der neuen abgeleiteten EventArgs-Klasse für die Berichte des neuen
Verzeichnisses und Status.

internal class SearchDirectoryArgs : EventArgs


{
internal string CurrentSearchDirectory { get; }
internal int TotalDirs { get; }
internal int CompletedDirs { get; }

internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs)


{
CurrentSearchDirectory = dir;
TotalDirs = totalDirs;
CompletedDirs = completedDirs;
}
}

In diesem Fall können Sie erneut der Empfehlung für das Erstellen eines nicht änderbaren Verweistyps für die
Ereignisargumente folgen.
Definieren Sie als Nächstes das Ereignis. Dieses Mal werden Sie eine andere Syntax verwenden. Zusätzlich zur
Verwendung der Feldsyntax, können Sie die Eigenschaft explizit erstellen, mit dem Hinzufügen- und Entfernen-
Handler. In diesem Beispiel werden Sie für diese Handler keinen zusätzlichen Code benötigen, aber dies zeigt,
wie Sie sie erstellen würden.

internal event EventHandler<SearchDirectoryArgs> DirectoryChanged


{
add { directoryChanged += value; }
remove { directoryChanged -= value; }
}
private EventHandler<SearchDirectoryArgs> directoryChanged;

In vielerlei Hinsicht spiegelt der Code, den Sie hier schreiben, den vom Compiler generierten Code für die
Feldereignisdefinitionen wider, die Sie zuvor gesehen haben. Sie erstellen das Ereignis mithilfe der Syntax
ähnlich der für Eigenschaften. Beachten Sie, dass die Handler unterschiedliche Namen haben: add und remove .
Diese werden aufgerufen, um das Ereignis zu abonnieren oder sich vom Ereignis abzumelden. Beachten Sie,
dass Sie auch ein privates Unterstützungsfeld zum Speichern der Ereignisvariable deklarieren müssen. Es wird
mit NULL initialisiert.
Als Nächstes fügen wir die Überladung der Search -Methode hinzu, die Unterverzeichnisse durchsucht und
beide Ereignisse auslöst. Die einfachste Möglichkeit besteht darin, ein Standardargument zu verwenden, um
anzugeben, dass alle Verzeichnisse durchsucht werden sollen:

public void Search(string directory, string searchPattern, bool searchSubDirs = false)


{
if (searchSubDirs)
{
var allDirectories = Directory.GetDirectories(directory, "*.*", SearchOption.AllDirectories);
var completedDirs = 0;
var totalDirs = allDirectories.Length + 1;
foreach (var dir in allDirectories)
{
directoryChanged?.Invoke(this,
new SearchDirectoryArgs(dir, totalDirs, completedDirs++));
// Search 'dir' and its subdirectories for files that match the search pattern:
SearchDirectory(dir, searchPattern);
}
// Include the Current Directory:
directoryChanged?.Invoke(this,
new SearchDirectoryArgs(directory, totalDirs, completedDirs++));
SearchDirectory(directory, searchPattern);
}
else
{
SearchDirectory(directory, searchPattern);
}
}

private void SearchDirectory(string directory, string searchPattern)


{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
var args = new FileFoundArgs(file);
FileFound?.Invoke(this, args);
if (args.CancelRequested)
break;
}
}

An diesem Punkt können Sie die Anwendung durch das Aufrufen der Überladung für die Suche aller
Unterverzeichnisse ausführen. Es sind keine Abonnenten auf dem neuen ChangeDirectory -Ereignis vorhanden,
aber mit den ?.Invoke() -Ausdruck wird sichergestellt, dass es ordnungsgemäß funktioniert.
Fügen wir einen Handler hinzu, um eine Zeile zu schreiben, die den Status im Konsolenfenster anzeigt.

fileLister.DirectoryChanged += (sender, eventArgs) =>


{
Console.Write($"Entering '{eventArgs.CurrentSearchDirectory}'.");
Console.WriteLine($" {eventArgs.CompletedDirs} of {eventArgs.TotalDirs} completed...");
};

Sie haben Muster gesehen, die im .NET-Ökosystem eingehalten werden. Indem Sie diese Muster und
Konventionen erlernen, werden Sie schnell idiomatische C# und .NET schreiben können.
Als Nächstes sehen Sie einige Änderungen in diesen Mustern in der neuesten Version von .NET.
Nächste
Das aktualisierte .NET Core-Ereignismuster
04.11.2021 • 3 minutes to read

Zurück
Im vorherigen Artikel wurden die am häufigsten verwendeten Ereignismuster erläutert. .NET Core ist ein
lockereres Muster. Die EventHandler<TEventArgs> -Definition hat in dieser Version nicht länger die Einschränkung,
dass TEventArgs eine von System.EventArgs abgeleitete Klasse sein muss.
Dies erhöht die Flexibilität für Sie und ist abwärtskompatibel. Beginnen wir mit der Flexibilität. Die
System.EventArgs-Klasse leitet eine Methode ein: MemberwiseClone() , die eine flache Kopie des Objekts erstellt.
Diese Methode muss Reflektion verwenden, um ihre Funktionalität für jede von EventArgs abgeleitete Klasse zu
implementieren. Diese Funktionalität lässt sich in einer bestimmten abgeleiteten Klasse einfacher erstellen. Das
bedeutet, dass das Ableiten von System.EventArgs eine Einschränkung ist, die Ihre Entwürfe beschränkt, aber
keine zusätzlichen Vorteile bietet. Tatsächlich können Sie die Definitionen von FileFoundArgs und
SearchDirectoryArgs ändern, sodass sie nicht von EventArgs abgeleitet werden. Das Programm funktioniert
genauso.
Sie könnten auch SearchDirectoryArgs in eine Struktur ändern, wenn Sie eine weitere Änderung vornehmen:

internal struct SearchDirectoryArgs


{
internal string CurrentSearchDirectory { get; }
internal int TotalDirs { get; }
internal int CompletedDirs { get; }

internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs) : this()


{
CurrentSearchDirectory = dir;
TotalDirs = totalDirs;
CompletedDirs = completedDirs;
}
}

Die zusätzliche Änderung besteht darin, den parameterlosen Konstruktor aufzurufen, bevor der Konstruktor
eingegeben wird, der alle Felder initialisiert. Ohne diesen Zusatz würden die Regeln von C# berichten, dass auf
die Eigenschaften zugegriffen wird, bevor sie zugewiesen wurden.
Ändern Sie FileFoundArgs nicht von einer Klasse (Verweistyp) in eine Struktur (Werttyp). Das Protokoll zur
Behandlung von Abbrüchen erfordert, dass die Ereignisargumente als Verweis übergeben werden. Wenn Sie
diese Änderung vorgenommen hätten, könnte die Dateisuche-Klasse niemals Änderungen beobachten, die von
einem der Ereignisabonnenten vorgenommen wurden. Für jeden Abonnenten würde eine neue Kopie der
Struktur verwendet werden, und diese Kopie würde sich von derjenigen unterscheiden, die vom Dateisuche-
Objekt gesehen wird.
Schauen wir uns nun an, wie diese Änderung abwärtskompatibel gemacht werden kann. Das Entfernen der
Einschränkung wirkt sich nicht auf vorhandenen Code aus. Alle vorhandenen Ereignisargumenttypen werden
immer noch von System.EventArgs abgeleitet. Abwärtskompatibilität ist ein wichtiger Grund, weshalb sie
weiterhin von System.EventArgs ableiten. Vorhandene Ereignisabonnenten werden Abonnenten für ein Ereignis,
die dem klassische Muster folgen.
Nach derselben Logik würden alle erstellten Ereignisargumenttypen keine Abonnenten in vorhandenen
Codebases haben. Neue Ereignistypen, die nicht von System.EventArgs abgeleitet sind, unterbrechen diese
Codebases nicht.

Ereignisse mit asynchronen Abonnenten


Das letzte, in diesem Artikel behandelte Muster ist die richtige Schreibweise von Ereignisabonnenten, die
asynchronen Code aufrufen. Informationen hierzu finden Sie im Artikel zu Async und Await. Asynchrone
Methoden können einen Rückgabetyp „Void“ haben, aber davon ist dringend abzuraten. Wenn Ihr
Ereignisabonnent eine asynchrone Methode aufruft, haben Sie keine Wahl, außer eine async void -Methode zu
erstellen. Sie wird von der Signatur des Ereignishandlers benötigt.
Sie müssen diesen entgegengesetzten Leitfaden abstimmen. Sie müssen irgendwie eine sichere async void -
Methode erstellen. Die Grundlagen des Musters, das Sie implementieren müssen, finden Sie untenan:

worker.StartWorking += async (sender, eventArgs) =>


{
try
{
await DoWorkAsync();
}
catch (Exception e)
{
//Some form of logging.
Console.WriteLine($"Async task failure: {e.ToString()}");
// Consider gracefully, and quickly exiting.
}
};

Beachten Sie, dass der Handler als ein asynchroner Handler markiert ist. Da er einem Ereignishandler-
Delegattyp zugewiesen wurde, weist er den Rückgabetyp „Void“ auf. Das bedeutet, dass Sie dem im Handler
angezeigten Muster folgen müssen und nicht zulassen dürfen, dass Ausnahmen ausgelöst werden, die nicht
zum Kontext des asynchronen Handlers gehören. Da der Handler keine Aufgabe zurückgibt, gibt es keine
Aufgabe, die einen Fehler berichten kann, indem sie in den Fehlerzustand tritt. Da die Methode asynchron ist,
kann sie die Ausnahme nicht einfach auslösen. (Die aufrufende Methode hat die Ausführung fortgesetzt, weil sie
den Typ async aufweist.) Das tatsächliche Laufzeitverhalten wird für verschiedene Umgebungen unterschiedlich
definiert. Sie kann den Thread oder den Prozess, der den Thread besitzt, beenden oder den Prozess in einem
unbestimmten Zustand belassen. Alle diese potenziellen Ergebnisse sind jedoch unerwünscht.
Deshalb sollten Sie die Await-Anweisung für die asynchrone Aufgabe in Ihrem eigenen try-Block umschließen.
Wenn sie einen fehlerhaften Vorgang verursacht, können Sie den Fehler protokollieren. Wenn es sich um einen
Fehler handelt, aus dem sich Ihre Anwendung nicht wiederherstellen lässt, können Sie das Programm schnell
und problemlos beenden.
Dies sind die wichtigsten Updates für das .NET-Ereignismuster. In den Bibliotheken, mit denen Sie arbeiten, Sie
zahlreiche Beispiele für die früheren Versionen. Allerdings sollten Sie auch die neuesten Muster kennenlernen.
Der nächste Artikel dieser Reihe hilft Ihnen dabei, zwischen der Verwendung von delegates und events in
ihren Entwürfen zu unterscheiden. Da sich beide Konzepte ähneln, wird Ihnen dieser Artikel helfen, die beste
Entscheidung für Ihre Programme zu treffen.
Nächste
Unterscheidung zwischen Delegaten und
Ereignissen
04.11.2021 • 3 minutes to read

Vorherige
Entwickler, die die .NET Core-Plattform noch nicht kennen, haben oft mit der Entscheidung zwischen einem
Entwurf auf der Grundlage von delegates und einem Entwurf auf der Grundlage von events zu kämpfen. Die
Wahl zwischen Delegaten oder Ereignissen ist meist schwierig, da sich die beiden Sprachfeatures ähneln.
Ereignisse werden sogar mithilfe der Sprachunterstützung für Delegaten erstellt.
Beide bieten ein Szenario mit später Bindung: Sie ermöglichen Szenarios, in denen eine Komponente durch
Aufrufen einer Methode, die nur zur Laufzeit bekannt ist, kommuniziert. Beide Versionen unterstützen einzelne
und mehrerer Methoden des Abonnenten. Möglicherweise finden Sie dies unter der Bezeichnung Singlecast-
und Multicast-Unterstützung. Beide unterstützen eine ähnliche Syntax zum Hinzufügen und Entfernen von
Handlern. Schließlich verwenden das Auslösen eines Ereignisses und das Aufrufen eines Delegaten genau die
gleiche Methodensyntax zu Aufrufen. Auch unterstützen beide die gleiche Invoke() -Methodensyntax für die
Verwendung mit dem ?. -Operator.
Mit all diesen Ähnlichkeiten ist es einfach nur schwer zu bestimmen, wann welche Komponente benutzt werden
soll.

Das Überwachen von Ereignissen ist optional


Der wichtigste Aspekt bei der Bestimmung, welche Sprachfunktion verwendet werden soll, ist die Frage, ob ein
angefügter Abonnent vorhanden sein muss. Wenn der Code den vom Abonnenten bereitgestellten Code
aufrufen muss, sollten Sie einen delegatbasierten Entwurf werden, wenn ein Callback implementiert werden
muss. Wenn Ihr Code all seine Arbeit ohne Aufruf von Abonnenten abschließen kann, sollten Sie einen Entwurf
auf Grundlage von Ereignissen verwenden.
Betrachten Sie die Beispiele, die während dieses Abschnitts erstellt wurden. Dem Code, den Sie mithilfe von
List.Sort() erstellt haben, muss eine Vergleichsfunktion gegeben werden, um die Elemente ordnungsgemäß
zu sortieren. LINQ-Abfragen müssen mit Delegaten bereitgestellt werden, um zu bestimmen, welche Elemente
zurückgeben werden sollen. Beide verwenden einen mit Delegaten erstellten Entwurf.
Betrachten Sie das Progress -Ereignis. Es meldet den Status eines Tasks. Der Task wird fortgesetzt, unabhängig
davon, ob alle Listener vorhanden sind. Der FileSearcher ist ein weiteres Beispiel. Es würde weiterhin alle
gesuchten Dateien suchen und finden, auch ohne angefügte Ereignisabonnenten. UX-Steuerelemente
funktionieren weiterhin ordnungsgemäß, auch wenn keine Abonnenten die Ereignisse überwachen. Beide
verwenden Entwürfe auf der Grundlage von Ereignissen.

Rückgabewerte erfordern Delegaten


Ein weiterer Aspekt ist der Methodenprototyp, den Sie für die Delegatmethode haben sollten. Wie Sie gesehen
haben, verfügen die für Ereignisse verwendete Delegaten alle über einen void-Rückgabetyp. Sie haben auch
gesehen, dass Ausdrücke vorhanden sind, um Ereignishandler zu erstellen, die Informationen durch Ändern der
Eigenschaften des Ereignisargumentobjekts zurück an die Ereignisquellen geben. Während diese Ausdrücke
funktionieren, sind sie nicht so natürlich wie die Rückgabe eines Werts aus einer Methode.
Beachten Sie, dass häufig diese beiden Heuristiken vorhanden sein können: Wenn die Delegatmethode einen
Wert zurückgibt, wirkt sich dies wahrscheinlich auf irgendeine Weise auf den Algorithmus aus.

Privater Aufruf von Ereignissen


Klassen, in denen das Ereignis nicht enthalten ist, können nur Ereignislistener hinzufügen und entfernen. Nur die
Klasse, die das Ereignis enthält, kann das Ereignis aufrufen. Ereignisse sind normalerweise öffentliche
Klassenmember. Delegaten hingegen werden oft als Parameter übergeben und als private Klassenmember
gespeichert, sofern sie überhaupt gespeichert werden.

Ereignislistener haben häufig eine längere Lebensdauer


Dass Ereignislistener eine längere Lebensdauer besitzen, ist eine eher schlechtere Begründung. Jedoch können
Sie feststellen, dass ereignisbasierte Entwürfe natürlicher sind, wenn die Ereignisquelle über einen langen
Zeitraum Ereignisse auslösen wird. Beispiele für ereignisbasierte Designs für Steuerelemente der
Benutzeroberfläche können Sie in vielen Systemen finden. Sobald Sie ein Ereignis abonnieren, kann die
Ereignisquelle Ereignisse während der gesamten Lebensdauer des Programms auslösen. (Sie können das
Abonnement von Ereignissen kündigen, wenn Sie sie nicht mehr benötigen.)
Vergleichen Sie das mit vielen delegatbasierten Entwürfen, bei denen ein Delegat als Argument an eine Methode
verwendet wird, und der Delegat wird nicht verwendet, nachdem diese Methode zurückgegeben wird.

Sorgfältig bewerten
Die oben genannten Aspekte sind keine verbindlichen Regeln. Stattdessen stellen sie Leitfäden dar, mit denen Sie
entscheiden können, welche Auswahl für Ihre spezielle Verwendung am besten geeignet ist. Da sie sich ähneln,
können Sie sogar beide als Prototyp nutzen, und überlegen, welche beim Arbeiten natürlicher wäre. Beide
behandeln Szenarios mit später Bindung gut. Verwenden Sie die, die Ihren Entwurf am besten kommuniziert.
Sprachintegrierte Abfrage (Language-Integrated
Query, LINQ)
04.11.2021 • 2 minutes to read

Language Integrated Query (LINQ) bezeichnet einen Satz Technologien, die auf der direkten Integration der
Abfragefunktionen in die Sprache C# basieren. Abfragen von Daten werden gewöhnlich als einfache
Zeichenfolgen ohne Typüberprüfung zur Kompilierzeit und ohne IntelliSense-Unterstützung ausgedrückt.
Außerdem müssen Sie für jeden Datenquellentyp eine andere Abfragesprache lernen: SQL-Datenbanken, XML-
Dokumente, verschiedene Webdienste usw. Bei LINQ ist eine Abfrage ein erstklassiges Sprachkonstrukt,
genauso wie Klassen, Methoden oder Ereignisse.
Für einen Entwickler, der Abfragen schreibt, ist der sichtbarste „sprachintegrierte“ Teil von LINQ der
Abfrageausdruck. Abfrageausdrücke werden in einer deklarativen Abfragesyntax geschrieben. Mit der
Abfragesyntax können Sie mit minimalem Codeeinsatz Filter-, Sortier- und Gruppiervorgänge in Datenquellen
ausführen. Sie verwenden die gleichen grundlegenden Abfrageausdrucksmuster, um Daten in SQL-
Datenbanken, ADO. NET-Datasets, XML-Dokumenten und -Streams sowie .NET-Auflistungen abzufragen und zu
transformieren.
Das folgende Beispiel zeigt den vollständigen Abfragevorgang. Der vollständige Vorgang umfasst die Erstellung
einer Datenquelle, die Definition des Abfrageausdrucks und die Ausführung der Abfrage in einer foreach -
Anweisung.

class LINQQueryExpressions
{
static void Main()
{

// Specify the data source.


int[] scores = new int[] { 97, 92, 81, 60 };

// Define the query expression.


IEnumerable<int> scoreQuery =
from score in scores
where score > 80
select score;

// Execute the query.


foreach (int i in scoreQuery)
{
Console.Write(i + " ");
}
}
}
// Output: 97 92 81

Übersicht über Abfrageausdrücke


Abfrageausdrücke können verwendet werden, um Daten aus einer beliebigen LINQ-fähigen Datenquelle
abzufragen und zu transformieren. Mit einer einzigen Abfrage können z.B. Daten aus einer SQL-
Datenbank abgerufen und ein XML-Stream als Ausgabe generiert werden.
Die Arbeit mit Abfrageausdrücken ist einfach, da sie viele vertraute Konstrukte der Sprache C#
verwenden.
Alle Variablen in einem Abfrageausdruck sind stark typisiert, obwohl Sie den Typ in vielen Fällen nicht
explizit angeben müssen, da der Compiler ihn ableiten kann. Weitere Informationen finden Sie unter
Typbeziehungen in LINQ-Abfragevorgängen.
Eine Abfrage wird erst ausgeführt, wenn Sie die Abfragevariable durchlaufen, z.B. in einer foreach -
Anweisung. Weitere Informationen finden Sie unter Einführung in LINQ-Abfragen.
Zur Kompilierzeit werden Abfrageausdrücke gemäß den in der C#-Spezifikation festgelegten Regeln in
Methodenaufrufe des Standardabfrageoperators konvertiert. Jede Abfrage, die mithilfe der
Abfragesyntax ausgedrückt werden kann, kann auch mithilfe der Methodensyntax ausgedrückt werden. In
den meisten Fällen ist aber die Abfragesyntax präziser und besser lesbar. Weitere Informationen finden
Sie unter Spezifikation für die Sprache C# und Übersicht über Standardabfrageoperatoren.
Beim Schreiben von LINQ-Abfragen empfiehlt sich diese Faustregel: Verwenden Sie die Abfragesyntax,
wann immer es möglich ist, und verwenden Sie die Methodensyntax nur, wenn es nötig ist. Zwischen den
beiden Formen gibt es keine semantischen oder leistungsbezogenen Unterschiede. Abfrageausdrücke
sind oft besser lesbar als die entsprechenden in der Methodensyntax geschriebenen Ausdrücke.
Für einige Abfragevorgänge, wie z.B. Count oder Max, gibt es keine entsprechende
Abfrageausdrucksklausel, daher müssen diese als Methodenaufruf ausgedrückt werden. Die
Methodensyntax kann auf verschiedene Weise mit der Abfragesyntax kombiniert werden. Weitere
Informationen finden Sie unter Abfragesyntax und Methodensyntax in LINQ.
Abfrageausdrücke können in Ausdrucksbaumstrukturen oder Delegaten kompiliert werden, je nachdem,
auf welchen Typ die Abfrage angewendet wird. IEnumerable<T>-Abfragen werden zu Delegaten
kompiliert. IQueryable- und IQueryable<T>-Abfragen werden zu Ausdrucksbaumstrukturen kompiliert.
Weitere Informationen finden Sie unter Ausdrucksbaumstrukturen.

Nächste Schritte
Um mehr zu LINQ zu erfahren, machen Sie sich zunächst unter Grundlagen zu Abfrageausdrücken mit einigen
grundlegenden Konzepten vertraut, und lesen Sie dann die Dokumentation für die LINQ-Technologie, die Sie
interessiert:
XML-Dokumente: LINQ to XML
ADO.NET Entity Framework: LINQ to Entities
.NET-Collections, -Dateien, -Zeichenfolgen usw.: LINQ to Objects
Tiefer greifende Einblicke in LINQ im Allgemeinen erhalten Sie unter LINQ in C#.
Informationen zu den ersten Schritten mit LINQ in C# erhalten Sie im Tutorial Arbeiten mit LINQ.
Grundlagen zu Abfrageausdrücken
04.11.2021 • 11 minutes to read

In diesem Artikel werden die grundlegenden Konzepte für Abfrageausdrücke in C# vorgestellt.

Was ist eine Abfrage, und welche Funktion hat sie?


Eine Abfrage ist ein Satz von Anweisungen, der beschreibt, welche Daten aus einer bestimmten Datenquelle
(oder Quellen) abgerufen werden sollen, und welche Form und Organisation die zurückgegebenen Daten haben
sollen. Eine Abfrage unterscheidet sich von den Ergebnissen, die sie erzeugt.
Im Allgemeinen werden die Quelldaten logisch als Sequenz von Elementen der gleichen Art organisiert. Eine
SQL-Datenbanktabelle enthält z.B. eine Sequenz von Zeilen. In einer XML-Datei gibt es eine „Sequenz“ von XML-
Elementen (auch wenn diese hierarchisch in einer Baumstruktur organisiert sind). Eine Auflistung im
Arbeitsspeicher enthält eine Sequenz von Objekten.
Aus Sicht einer Anwendung ist der spezifische Typ und die Struktur der ursprünglichen Datenquelle nicht
wichtig. Die Anwendung sieht die Quelldaten immer als eine IEnumerable<T>- oder IQueryable<T>-Auflistung
an. In LINQ to XML werden die Quelldaten z.B. als ein IEnumerable <XElement sichtbar gemacht.
Wenn diese Quellsequenz vorliegt, kann eine Abfrage eine der folgenden drei Aktionen durchführen:
Abrufen einer Teilmenge der Elemente zum Erstellen einer neuen Sequenz ohne die einzelnen Elemente
zu verändern. Die Abfrage kann die zurückgegebenen Sequenzen dann auf verschiedene Arten sortieren
oder gruppieren, wie im folgenden Beispiel gezeigt wird (Annahme: scores ist int[] ):

IEnumerable<int> highScoresQuery =
from score in scores
where score > 80
orderby score descending
select score;

Abrufen einer Sequenz von Elementen wie im vorherigen Beispiel, aber mit Transformation der Elemente
in einen neuen Objekttyp. Eine Abfrage kann z.B. nur die Nachnamen aus bestimmten
Kundendatensätzen in einer Datenquelle abrufen. Sie kann möglicherweise auch den vollständigen
Datensatz abrufen und ihn zum Erstellen eines anderen Objekttyps im Arbeitsspeicher oder sogar XML-
Daten vor dem Generieren der endgültigen Ergebnissequenz verwenden. Im folgenden Beispiel wird eine
Projektion von int in string veranschaulicht. Beachten Sie den neuen Typ von highScoresQuery .

IEnumerable<string> highScoresQuery2 =
from score in scores
where score > 80
orderby score descending
select $"The score is {score}";

Abrufen eines Singleton-Werts zu den Quelldaten, z.B.:


Die Anzahl der Elemente, die eine bestimmte Bedingung erfüllen
Das Element, das den größten oder den niedrigsten Wert hat
Das erste Element, das einer Bedingung entspricht oder die Summe bestimmter Werte in einer
angegebenen Menge von Elementen Die folgende Abfrage gibt z.B. die Anzahl von Ergebnissen
aus dem scores -Ganzzahlarray zurück, die höher als 80 sind:

int highScoreCount =
(from score in scores
where score > 80
select score)
.Count();

Beachten Sie im vorherigen Beispiel die Verwendung von Klammern um den Abfrageausdruck vor
dem Aufruf der Count -Methode. Sie können dies auch mit einer neuen Variable ausdrücken, um
das konkrete Ergebnis zu speichern. Diese Technik ist besser lesbar, da die Variablen, die die
Abfrage speichern, von der Abfrage getrennt sind, die ein Ergebnis speichert.

IEnumerable<int> highScoresQuery3 =
from score in scores
where score > 80
select score;

int scoreCount = highScoresQuery3.Count();

Im vorherigen Beispiel wird die Abfrage im Aufruf von Count ausgeführt, da Count die Ergebnisse durchlaufen
muss, um die Anzahl der von highScoresQuery zurückgegebenen Elemente zu bestimmen.

Was ist ein Abfrageausdruck?


Ein Abfrageausdruck ist eine in der Abfragesyntax ausgedrückte Abfrage. Ein Abfrageausdruck ist ein
erstklassiges Sprachkonstrukt. Er verhält sich wie jeder andere Ausdruck und kann in jedem Kontext verwendet
werden, in dem ein C#-Ausdruck gültig ist. Ein Abfrageausdruck besteht aus einem Satz von in einer
deklarativen Syntax geschriebenen Klauseln, ähnlich wie SQL oder XQuery. Jede Klausel umfasst wiederum
einen oder mehrere C#-Ausdrücke. Diese Ausdrücke sind möglicherweise selbst Abfrageausdrücke oder
enthalten einen Abfrageausdruck.
Ein Abfrageausdruck muss mit einer from-Klausel beginnen und mit einer select- oder group-Klausel enden.
Zwischen der ersten from -Klausel und der letzten select - oder group -Klausel kann ein Abfrageausdruck eine
oder mehrere der folgenden optionalen Klauseln enthalten: where, orderby, join, let und sogar zusätzliche from-
Klauseln. Sie können auch das Schlüsselwort into verwenden, um zuzulassen, das das Ergebnis einer join -
oder group -Klausel als Quelle für zusätzliche Abfrageklauseln im selben Abfrageausdruck dient.
Abfragevariable
In LINQ ist eine Abfragevariable eine beliebige Variable, die eine Abfrage anstatt des Ergebnisses einer Abfrage
speichert. Genauer gesagt ist eine Abfragevariable immer ein Enumerable-Typ, der eine Sequenz von Elementen
erzeugt, wenn er in einer foreach -Anweisung oder einem direkten Aufruf der IEnumerator.MoveNext -Methode
durchlaufen wird.
Das folgende Codebeispiel zeigt einen einfachen Abfrageausdruck mit einer Datenquelle, einer Filtering-Klausel,
einer Ordering-Klausel und ohne Transformationen der Quellelemente. Die Klausel select beendet die Abfrage.
static void Main()
{
// Data source.
int[] scores = { 90, 71, 82, 93, 75, 82 };

// Query Expression.
IEnumerable<int> scoreQuery = //query variable
from score in scores //required
where score > 80 // optional
orderby score descending // optional
select score; //must end with select or group

// Execute the query to produce the results


foreach (int testScore in scoreQuery)
{
Console.WriteLine(testScore);
}
}
// Outputs: 93 90 82 82

Im vorherigen Beispiel ist scoreQuery eine Abfragevariable, die manchmal auch einfach als Abfrage bezeichnet
wird. Die Abfragevariable speichert keine tatsächlichen Ergebnisdaten, die in der foreach -Schleife erzeugt
werden. Wenn die foreach -Anweisung ausgeführt wird, werden die Ergebnisse der Abfrage nicht über die
Abfragevariable scoreQuery zurückgegeben. Stattdessen werden sie über die Iterationsvariable testScore
zurückgegeben. Die scoreQuery -Variable kann in einer zweiten foreach -Schleife durchlaufen werden. Die
gleichen Ergebnisse werden erzeugt, solange weder die Variable noch die Datenquelle geändert wurde.
Eine Abfragevariable kann eine Abfrage speichern, die in einer Abfragesyntax oder Methodensyntax oder einer
Kombination aus beiden ausgedrückt wird. In den folgenden Beispielen sind sowohl queryMajorCities als auch
queryMajorCities2 Abfragevariablen:

//Query syntax
IEnumerable<City> queryMajorCities =
from city in cities
where city.Population > 100000
select city;

// Method-based syntax
IEnumerable<City> queryMajorCities2 = cities.Where(c => c.Population > 100000);

Andererseits zeigen die beiden nächsten Beispiele Variablen, die keine Abfragevariablen sind, obwohl beide mit
einer Abfrage initialisiert werden. Sie sind keine Abfragevariablen, da sie Ergebnisse speichern:
int highestScore =
(from score in scores
select score)
.Max();

// or split the expression


IEnumerable<int> scoreQuery =
from score in scores
select score;

int highScore = scoreQuery.Max();


// the following returns the same result
int highScore = scores.Max();

List<City> largeCitiesList =
(from country in countries
from city in country.Cities
where city.Population > 10000
select city)
.ToList();

// or split the expression


IEnumerable<City> largeCitiesQuery =
from country in countries
from city in country.Cities
where city.Population > 10000
select city;

List<City> largeCitiesList2 = largeCitiesQuery.ToList();

Weitere Informationen zu den verschiedenen Verfahren zum Ausdrücken von Abfragen finden Sie unter Query
syntax and method syntax in LINQ (Abfragesyntax und Methodensyntax in LINQ).
Explizite und implizite Typisierung von Abfragevariablen
Diese Dokumentation enthält normalerweise den expliziten Typ der Abfragevariablen, um die Typbeziehung
zwischen der Abfrage und der select-Klausel darzustellen. Sie können aber auch das Schlüsselwort var
verwenden, um den Compiler anzuweisen, den Typ einer Abfragevariable (oder eine andere lokale Variable) zur
Kompilierzeit abzuleiten. Das Beispiel einer Abfrage, das vorher in diesem Thema gezeigt wurde, kann
beispielsweise auch durch implizierte Typisierung ausgedrückt werden:

// Use of var is optional here and in all queries.


// queryCities is an IEnumerable<City> just as
// when it is explicitly typed.
var queryCities =
from city in cities
where city.Population > 100000
select city;

Weitere Informationen finden Sie unter Implizit typisierte lokale Variablen und Type relationships in LINQ query
operations (Typbeziehungen in LINQ-Abfragevorgängen).
Starten eines Abfrageausdrucks
Ein Abfrageausdruck muss mit einer from -Klausel beginnen. Er gibt eine Datenquelle zusammen mit einer
Bereichsvariablen an. Die Bereichsvariable stellt jedes darauffolgende Element in der Quellsequenz dar, wenn
das Quellelement durchsucht wird. Die Bereichsvariable ist, basierend auf den Typen des Elements in der
Datenquelle, stark typisiert. Im folgenden Beispiel ist die Bereichsvariable auch als Country typisiert, da
countries ein Array von Country -Objekten ist. Da die Bereichsvariable stark typisiert ist, können Sie den
Punktoperator verwenden, um auf verfügbare Member des Typs zuzugreifen.
IEnumerable<Country> countryAreaQuery =
from country in countries
where country.Area > 500000 //sq km
select country;

Die Bereichsvariable befindet sich im Geltungsbereich, bis die Abfrage entweder mit einem Semikolon oder
einer continuation-Klausel beendet wird.
Ein Abfrageausdruck enthält möglicherweise mehrere from -Klauseln. Verwenden Sie zusätzliche from -
Klauseln, wenn jedes Element in der Quellsequenz selbst eine Auflistung ist oder eine Auflistung enthält.
Nehmen wir beispielsweise an, dass Sie über eine Auflistung von Country -Objekten verfügen, von der jedes
eine Auflistung von City -Objekten mit dem Namen Cities enthält. Verwenden Sie zwei from -Klauseln, um
die City -Objekte in jedem Country abzufragen, wie hier gezeigt:

IEnumerable<City> cityQuery =
from country in countries
from city in country.Cities
where city.Population > 10000
select city;

Weitere Informationen finden Sie unter from-Klausel.


Beenden eines Abfrageausdrucks
Ein Abfrageausdruck muss entweder mit einer group - oder einer select -Klausel enden.
group-Klausel
Verwenden Sie die group -Klausel, um eine Sequenz von Gruppen zu erzeugen, die von einem von Ihnen
angegebenen Schüssel organisiert wird. Der Schlüssel kann ein beliebiger Datentyp sein. Die folgende Abfrage
erstellt z. B. eine Sequenz von Gruppen, die ein oder mehrere Country -Objekte enthalten und deren Schlüssel
vom Typ char ist und als Wert den ersten Buchstaben von Ländern enthält.

var queryCountryGroups =
from country in countries
group country by country.Name[0];

Weitere Informationen zum Gruppieren finden Sie unter group-Klausel.


select-Klausel
Verwenden Sie die select -Klausel, um alle anderen Typen von Sequenzen zu erzeugen. Eine einfache select -
Klausel erzeugt nur eine Sequenz von Objekten desselben Typs wie die Objekte, die in der Datenquelle enthalten
sind. In diesem Beispiel enthält die Datenquelle Country -Objekte. Die orderby -Klausel sortiert die Elemente in
eine neue Reihenfolge, und die select -Klausel erzeugt eine Sequenz der neu angeordneten Country -Objekte.

IEnumerable<Country> sortedQuery =
from country in countries
orderby country.Area
select country;

Die select -Klausel kann zum Transformieren von Quelldaten in Sequenzen neuer Typen verwendet werden.
Diese Transformation wird auch als Projektion bezeichnet. Im folgenden Beispiel projiziert select die-Klausel
eine Sequenz anonymer Typen, die nur eine Teilmenge der Felder im originalen Element enthalten. Beachten Sie,
dass die neuen Objekte mit einem Objektinitialisierer initialisiert werden.
// Here var is required because the query
// produces an anonymous type.
var queryNameAndPop =
from country in countries
select new { Name = country.Name, Pop = country.Population };

Weitere Informationen zu allen Verfahren, in denen eine select -Klausel zum Transformieren von Daten
verwendet werden kann, finden Sie unter select-Klausel.
Fortsetzungen mit into
Sie können das Schlüsselwort into in einer select - oder group -Klausel verwenden, um einen temporären
Bezeichner zu erstellen, der eine Abfrage speichert. Dieses Vorgehen ist ratsam, wenn Sie zusätzliche
Abfragevorgänge nach einem grouping- oder einem select-Vorgang auf eine Abfrage ausführen müssen. Im
folgenden Beispiel werden countries gemäß der Bevölkerung in Bereiche von 10 Millionen gruppiert.
Nachdem diese Gruppen erstellt wurden, filtern zusätzliche Klauseln einige Gruppen heraus und sortieren die
Gruppen dann in aufsteigender Reihenfolge. Um diese zusätzlichen Vorgänge durchzuführen, wird die von
countryGroup dargestellte Fortsetzung benötigt.

// percentileQuery is an IEnumerable<IGrouping<int, Country>>


var percentileQuery =
from country in countries
let percentile = (int) country.Population / 10_000_000
group country by percentile into countryGroup
where countryGroup.Key >= 20
orderby countryGroup.Key
select countryGroup;

// grouping is an IGrouping<int, Country>


foreach (var grouping in percentileQuery)
{
Console.WriteLine(grouping.Key);
foreach (var country in grouping)
Console.WriteLine(country.Name + ":" + country.Population);
}

Weitere Informationen finden Sie unter into.


Filtern, Sortieren und Verknüpfen
Zwischen der from -Klausel am Anfang und der select - oder group -Klausel am Ende sind alle anderen
Klauseln ( where , join , orderby , from , let ) optional. Eine der optionalen Klauseln kann entweder überhaupt
nicht oder mehrfach in einem Abfragetext verwendet werden.
where-Klausel
Verwenden Sie die Klausel where zum Herausfiltern von Elementen aus den Quelldaten basierend auf einem
oder mehreren Prädikatausdrücken. Im folgenden Beispiel verfügt die Klausel where über ein Prädikat mit zwei
Bedingungen.

IEnumerable<City> queryCityPop =
from city in cities
where city.Population < 200000 && city.Population > 100000
select city;

Weitere Informationen finden Sie unter where-Klausel.


orderby-Klausel
Verwenden Sie die orderby -Klausel zum Sortieren der Ergebnisse in auf- oder absteigender Reihenfolge. Sie
können auch eine sekundäre Sortierreihenfolge angeben. Im folgenden Beispiel wird mit der Eigenschaft Area
eine primäre Sortierung der country -Objekte durchgeführt. Anschließend wird eine sekundäre Sortierung mit
der Eigenschaft Population durchgeführt.

IEnumerable<Country> querySortedCountries =
from country in countries
orderby country.Area, country.Population descending
select country;

Das Schlüsselwort ascending ist optional; wenn keine andere Reihenfolge angegeben ist, ist dies die
Standardreihenfolge. Weitere Informationen finden Sie unter orderby-Klausel.
join-Klausel
Verwenden Sie die join -Klausel zum Zuordnen und/oder Kombinieren von Elementen aus einer Datenquelle
mit Elementen aus einer anderen Datenquelle basierend auf einem Gleichheitsvergleich zwischen angegebenen
Schlüsseln in jedem Element. In LINQ werden Verknüpfungsvorgänge für Sequenzen von Objekten ausgeführt,
deren Elemente unterschiedliche Typen haben. Nachdem Sie zwei Segmente verknüpft haben, müssen Sie eine
select - oder group -Anweisung verwenden, um anzugeben, welches Element in der Ausgabesequenz
gespeichert werden soll. Sie können auch einen anonymen Typ verwenden, um Eigenschaften aus jedem Satz
der zugewiesenen Elemente in einem neuen Typ für die Ausgabesequenz zu kombinieren. Im folgenden Beispiel
werden prod -Objekte, deren Category -Eigenschaft einer der Kategorien im categories -Zeichenfolgearray
entspricht, zugewiesen. Produkte, deren Category keiner Zeichenfolge in categories entspricht, werden
herausgefiltert. Die select -Anweisung projiziert einen neuen Typ, dessen Eigenschaften sowohl aus cat als
auch aus prod übernommen werden.

var categoryQuery =
from cat in categories
join prod in products on cat equals prod.Category
select new { Category = cat, Name = prod.Name };

Sie können auch eine Gruppenverknüpfung durchführen, indem Sie die Ergebnisse des join -Vorgangs mithilfe
des Schlüsselworts into in eine temporäre Variable speichern. Weitere Informationen finden Sie unter join-
Klausel.
let-Klausel
Verwenden Sie die let -Klausel zum Speichern der Ergebnisse eines Ausdrucks, z.B. eines Methodenaufrufs, in
einer neuen Bereichsvariable. Im folgenden Beispiel speichert die Bereichsvariable firstName das erste
Elemente eines Arrays von Zeichenfolgen, das von Split zurückgegeben wird.

string[] names = { "Svetlana Omelchenko", "Claire O'Donnell", "Sven Mortensen", "Cesar Garcia" };
IEnumerable<string> queryFirstNames =
from name in names
let firstName = name.Split(' ')[0]
select firstName;

foreach (string s in queryFirstNames)


Console.Write(s + " ");
//Output: Svetlana Claire Sven Cesar

Weitere Informationen finden Sie unter let-Klausel.


Unterabfragen in einem Abfrageausdruck
Ein Abfrageausdruck selbst kann eine Abfrageklausel enthalten, die manchmal als Unterabfrage bezeichnet wird.
Jede Unterabfrage beginnt mit ihrer eigenen from -Klausel, die nicht unbedingt auf die gleiche Datenquelle in
der ersten from -Klausel zeigt. Die folgende Abfrage zeigt z.B. einen Abfrageausdruck, der in der Select-
Anweisung zum Abrufen der Ergebnisse eines Gruppierungsvorganges verwendet wird.
var queryGroupMax =
from student in students
group student by student.GradeLevel into studentGroup
select new
{
Level = studentGroup.Key,
HighestScore =
(from student2 in studentGroup
select student2.Scores.Average())
.Max()
};

Weitere Informationen finden Sie unter Ausführen einer Unterabfrage für einen Gruppierungsvorgang.

Siehe auch
C#-Programmierhandbuch
Language-Integrated Query (LINQ)
Abfrageschlüsselwörter (LINQ)
Standard query operators overview (Übersicht über Standardabfrageoperatoren)
LINQ in C#
04.11.2021 • 2 minutes to read

Dieser Abschnitt enthält Links zu Themen, die mehr Informationen zu LINQ bereitstellen.

In diesem Abschnitt
Introduction to LINQ queries (Einführung in LINQ-Abfragen)
Beschreibt die drei Bestandteile des grundlegenden LINQ-Abfragevorgangs, die bei allen Sprachen und
Datenquellen verwendet werden
LINQ and generic types (LINQ und generische Typen)
Bietet eine kurze Einführung in generische Typen, wie sie in LINQ verwendet werden
Datentransformationen mit LINQ (C#)
Beschreibt die verschiedenen Methoden, mit der Sie aus Abfragen abgerufene Daten transformieren können
Type relationships in LINQ query operations (Typbeziehungen in LINQ-Abfragevorgängen)
Beschreibt, wie Datentypen beibehalten bzw. in drei Teile eines LINQ-Abfragevorgangs transformiert werden
Query syntax and method syntax in LINQ (Abfragesyntax und Methodensyntax in LINQ)
Vergleicht Methodensyntax und Abfragesyntax als zwei Möglichkeiten, um eine LINQ-Abfrage auszudrücken
C# features that support LINQ (C#-Funktionen mit LINQ-Unterstützung)
Beschreibt die Konstrukte in C#, die LINQ unterstützen

Verwandte Abschnitte
LINQ-Abfrageausdrücke
Enthält eine Übersicht der Abfragen in LINQ und stellt Links zu weiteren Ressourcen bereit
Standard query operators overview (Übersicht über Standardabfrageoperatoren)
Führt die Standardmethoden ein, die in LINQ verwendet werden
Schreiben von LINQ-Abfragen in C#
04.11.2021 • 4 minutes to read

In diesem Artikel werden drei Möglichkeiten vorgestellt, mit denen eine LINQ-Abfrage in C# geschrieben
werden kann:
1. Verwenden der Abfragesyntax.
2. Verwenden der Methodensyntax.
3. Verwenden einer Kombination aus Abfragesyntax und Methodensyntax.
Die folgenden Beispiele veranschaulichen einige einfache LINQ-Abfragen anhand der oben aufgelisteten
Herangehensweisen. Im Allgemeinen gilt die Regel, wann immer möglich (1) zu verwenden, und (2) und (3)
wenn nötig.

NOTE
Diese Abfragen funktionieren auf Grundlage einfacher im Speicher enthaltener Auflistungen; die grundlegende Syntax
entspricht jedoch genau der in „LINQ to Entities“ und „LINQ to XML“ verwendeten Syntax.

Beispiel: Abfragesyntax
Die empfohlene Methode, die meisten Abfragen zu schreiben, ist die Verwendung der Abfragesyntax zum
Erstellen von Abfrageausdrücken. Im folgenden Beispiel werden drei Abfrageausdrücke gezeigt. Der erste
Abfrageausdruck veranschaulicht, wie man Ergebnisse durch Anwenden von Bedingungen mit einer where -
Klausel filtern und einschränken kann. Er gibt alle Elemente in der Quellsequenz zurück, deren Wert größer als 7
oder kleiner als 3 ist. Der zweite Ausdruck veranschaulicht, wie man die zurückgegebenen Ergebnisse sortiert.
Der dritte Ausdruck veranschaulicht, wie man Ergebnisse nach einem Schlüssel gruppiert. Diese Abfrage gibt
basierend auf dem ersten Buchstaben des Worts zwei Gruppen zurück.

// Query #1.
List<int> numbers = new List<int>() { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

// The query variable can also be implicitly typed by using var


IEnumerable<int> filteringQuery =
from num in numbers
where num < 3 || num > 7
select num;

// Query #2.
IEnumerable<int> orderingQuery =
from num in numbers
where num < 3 || num > 7
orderby num ascending
select num;

// Query #3.
string[] groupingQuery = { "carrots", "cabbage", "broccoli", "beans", "barley" };
IEnumerable<IGrouping<char, string>> queryFoodGroups =
from item in groupingQuery
group item by item[0];

Beachten Sie, dass der Typ der Abfrage IEnumerable<T> ist. Alle diese Abfragen können mithilfe von var
geschrieben werden, wie im folgenden Beispiel gezeigt wird:
var query = from num in numbers...

In allen vorherigen Beispielen werden die Abfragen nicht ausgeführt, bis Sie die Iteration über die
Abfragevariable in einer foreach - oder einer anderen Anweisung durchlaufen haben. Weitere Informationen
finden Sie unter Introduction to LINQ Queries (Einführung in LINQ-Abfragen).

Beispiel: Methodensyntax
Einige Abfragevorgänge müssen als Methodenaufruf ausgedrückt werden. Die häufigsten derartigen Methoden
sind die, die einzelne numerische Werte zurückgeben, wie z.B. Sum, Max, Min, Average usw. Diese Methoden
müssen in einer Abfrage immer zuletzt aufgerufen werden, da sie nur einen einzelnen Wert darstellen und nicht
als Quelle für einen zusätzlichen Abfragevorgang dienen können. Im folgenden Beispiel wird ein
Methodenaufruf in einem Abfrageausdruck dargestellt:

List<int> numbers1 = new List<int>() { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };


List<int> numbers2 = new List<int>() { 15, 14, 11, 13, 19, 18, 16, 17, 12, 10 };
// Query #4.
double average = numbers1.Average();

// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

Wenn die Methode über Aktions- oder Funktionsparameter verfügt, werden diese wie im folgenden Beispiel
dargestellt in Form eines Lambdaausdrucks zur Verfügung gestellt:

// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

Von den vorherigen Abfragen wird nur die vierte Abfrage sofort ausgeführt. Dies liegt daran, dass es einen
einzelnen Wert zurückgibt und keine generische IEnumerable<T>-Auflistung. Die Methode selbst muss
foreach verwenden, um einen Wert zu berechnen.

Alle vorherigen Abfragen können mithilfe von implizierter Typisierung mit var geschrieben werden. Dies wird
im folgenden Beispiel gezeigt:

// var is used for convenience in these queries


var average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);

Beispiel: Gemischte Abfrage und Methodensyntax


In diesem Beispiel wird veranschaulicht, wie Sie die Methodensyntax auf die Ergebnisse einer Abfrageklausel
anwenden können. Umschließen Sie einfach den Abfrageausdruck mit Klammern, wenden Sie anschließend den
Punktoperator an, und rufen Sie die Methode auf. Im folgenden Beispiel wird von der siebten Abfrage die Anzahl
der Zahlen zurückgegeben, deren Wert zwischen 3 und 7 liegt. Im Allgemeinen ist es jedoch besser, eine zweite
Variable zu verwenden, um das Ergebnis des zweiten Methodenaufrufs zu speichern. Auf diese Weise ist es
unwahrscheinlicher, dass dieses Ergebnis mit dem Ergebnis der Abfrage verwechselt wird.
// Query #7.

// Using a query expression with method syntax


int numCount1 =
(from num in numbers1
where num < 3 || num > 7
select num).Count();

// Better: Create a new variable to store


// the method call result
IEnumerable<int> numbersQuery =
from num in numbers1
where num < 3 || num > 7
select num;

int numCount2 = numbersQuery.Count();

Da Abfrage Nr.7 einen einzelnen Wert und keine Auflistung zurückgibt, wird die Abfrage sofort ausgeführt.
Die vorherige Abfrage kann mithilfe der implizierten Typisierung mit var wie folgt geschrieben werden:

var numCount = (from num in numbers...

Sie kann folgendermaßen in Methodensyntax geschrieben werden:

var numCount = numbers.Where(n => n < 3 || n > 7).Count();

Sie kann mithilfe der implizierten Typisierung wie folgt geschrieben werden:

int numCount = numbers.Where(n => n < 3 || n > 7).Count();

Weitere Informationen
Exemplarische Vorgehensweise: Schreiben von Abfragen in C#
Language-Integrated Query (LINQ)
where-Klausel
Abfrage einer Auflistung von Objekten
04.11.2021 • 2 minutes to read

In diesem Beispiel wird veranschaulicht, wie eine einfache Abfrage für eine Liste von Student -Objekten
ausgeführt wird. Jedes Student -Objekt enthält grundlegende Informationen über den Studenten und eine Liste,
die die Ergebnisse des Studenten aus vier Prüfungen darstellt.
Diese Anwendung dient als Rahmen für viele andere Beispiele in diesem Bereich, bei denen dieselbe students -
Datenquelle verwendet wird.

Beispiel
Die folgende Abfrage gibt die Studenten zurück, die ein Ergebnis von 90 oder höher in ihrer ersten Prüfung
erzielt haben.

public class Student


{
#region data
public enum GradeLevel { FirstYear = 1, SecondYear, ThirdYear, FourthYear };

public string FirstName { get; set; }


public string LastName { get; set; }
public int Id { get; set; }
public GradeLevel Year;
public List<int> ExamScores;

protected static List<Student> students = new List<Student>


{
new Student {FirstName = "Terry", LastName = "Adams", Id = 120,
Year = GradeLevel.SecondYear,
ExamScores = new List<int> { 99, 82, 81, 79}},
new Student {FirstName = "Fadi", LastName = "Fakhouri", Id = 116,
Year = GradeLevel.ThirdYear,
ExamScores = new List<int> { 99, 86, 90, 94}},
new Student {FirstName = "Hanying", LastName = "Feng", Id = 117,
Year = GradeLevel.FirstYear,
ExamScores = new List<int> { 93, 92, 80, 87}},
new Student {FirstName = "Cesar", LastName = "Garcia", Id = 114,
Year = GradeLevel.FourthYear,
ExamScores = new List<int> { 97, 89, 85, 82}},
new Student {FirstName = "Debra", LastName = "Garcia", Id = 115,
Year = GradeLevel.ThirdYear,
ExamScores = new List<int> { 35, 72, 91, 70}},
new Student {FirstName = "Hugo", LastName = "Garcia", Id = 118,
Year = GradeLevel.SecondYear,
ExamScores = new List<int> { 92, 90, 83, 78}},
new Student {FirstName = "Sven", LastName = "Mortensen", Id = 113,
Year = GradeLevel.FirstYear,
ExamScores = new List<int> { 88, 94, 65, 91}},
new Student {FirstName = "Claire", LastName = "O'Donnell", Id = 112,
Year = GradeLevel.FourthYear,
ExamScores = new List<int> { 75, 84, 91, 39}},
new Student {FirstName = "Svetlana", LastName = "Omelchenko", Id = 111,
Year = GradeLevel.SecondYear,
ExamScores = new List<int> { 97, 92, 81, 60}},
new Student {FirstName = "Lance", LastName = "Tucker", Id = 119,
Year = GradeLevel.ThirdYear,
ExamScores = new List<int> { 68, 79, 88, 92}},
new Student {FirstName = "Michael", LastName = "Tucker", Id = 122,
Year = GradeLevel.FirstYear,
Year = GradeLevel.FirstYear,
ExamScores = new List<int> { 94, 92, 91, 91}},
new Student {FirstName = "Eugene", LastName = "Zabokritski", Id = 121,
Year = GradeLevel.FourthYear,
ExamScores = new List<int> { 96, 85, 91, 60}}
};
#endregion

// Helper method, used in GroupByRange.


protected static int GetPercentile(Student s)
{
double avg = s.ExamScores.Average();
return avg > 0 ? (int)avg / 10 : 0;
}

public static void QueryHighScores(int exam, int score)


{
var highScores = from student in students
where student.ExamScores[exam] > score
select new {Name = student.FirstName, Score = student.ExamScores[exam]};

foreach (var item in highScores)


{
Console.WriteLine($"{item.Name,-15}{item.Score}");
}
}
}

public class Program


{
public static void Main()
{
Student.QueryHighScores(1, 90);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}

Diese Abfrage wurde absichtlich einfach gehalten, damit Sie damit experimentieren können. Sie können z.B.
weitere Bedingungen in der where -Klausel ausprobieren oder eine orderby -Klausel verwenden, um die
Ergebnisse zu sortieren.

Siehe auch
Language-Integrated Query (LINQ)
Zeichenfolgeninterpolation
Gewusst wie: Zurückgeben einer Abfrage aus einer
Methode (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

In diesem Beispiel wird gezeigt, wie Sie eine Abfrage aus einer Methode als Rückgabewert oder out -Parameter
zurückgeben.
Abfrageobjekte sind zusammensetzbar, das bedeutet, dass Sie eine Abfrage aus einer Methode zurückgeben
können. Objekte, die Abfragen darstellen, speichern nicht die resultierende Auflistung, sondern bei Bedarf die
Schritte zum Erzeugen der Ergebnisse. Der Vorteil des Zurückgebens von Abfrageobjekten aus Methoden ist,
dass diese weiter zusammengesetzt oder geändert werden können. Daher muss ein Rückgabewert oder ein
out -Parameter einer Methode, die eine Abfrage zurückgibt, über diesen Typ verfügen. Wenn eine Methode eine
Abfrage in einen konkreten List<T>- oder Array-Typ materialisiert, wird sie als Methode betrachtet, die die
Abfrageergebnisse zurückgibt, nicht die Abfrage selbst. Eine Abfragevariable, die aus einer Methode
zurückgegeben wird, kann noch zusammengesetzt oder geändert werden.

Beispiel
Im folgenden Beispiel gibt die erste Methode eine Abfrage als Rückgabewert zurück. Die zweite Methode gibt
eine Abfrage als out -Parameter zurück. Beachten Sie, dass es sich in beiden Fällen um eine Abfrage handelt, die
zurückgegeben wird, nicht um Abfrageergebnisse.

class MQ
{
// QueryMethhod1 returns a query as its value.
IEnumerable<string> QueryMethod1(ref int[] ints)
{
var intsToStrings = from i in ints
where i > 4
select i.ToString();
return intsToStrings;
}

// QueryMethod2 returns a query as the value of parameter returnQ.


void QueryMethod2(ref int[] ints, out IEnumerable<string> returnQ)
{
var intsToStrings = from i in ints
where i < 4
select i.ToString();
returnQ = intsToStrings;
}

static void Main()


{
MQ app = new MQ();

int[] nums = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// QueryMethod1 returns a query as the value of the method.


var myQuery1 = app.QueryMethod1(ref nums);

// Query myQuery1 is executed in the following foreach loop.


Console.WriteLine("Results of executing myQuery1:");
// Rest the mouse pointer over myQuery1 to see its type.
foreach (string s in myQuery1)
{
Console.WriteLine(s);
Console.WriteLine(s);
}

// You also can execute the query returned from QueryMethod1


// directly, without using myQuery1.
Console.WriteLine("\nResults of executing myQuery1 directly:");
// Rest the mouse pointer over the call to QueryMethod1 to see its
// return type.
foreach (string s in app.QueryMethod1(ref nums))
{
Console.WriteLine(s);
}

IEnumerable<string> myQuery2;
// QueryMethod2 returns a query as the value of its out parameter.
app.QueryMethod2(ref nums, out myQuery2);

// Execute the returned query.


Console.WriteLine("\nResults of executing myQuery2:");
foreach (string s in myQuery2)
{
Console.WriteLine(s);
}

// You can modify a query by using query composition. A saved query


// is nested inside a new query definition that revises the results
// of the first query.
myQuery1 = from item in myQuery1
orderby item descending
select item;

// Execute the modified query.


Console.WriteLine("\nResults of executing modified myQuery1:");
foreach (string s in myQuery1)
{
Console.WriteLine(s);
}

// Keep console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

Siehe auch
Language-Integrated Query (LINQ)
Speichern der Ergebnisse einer Abfrage im Speicher
04.11.2021 • 2 minutes to read

Eine Abfrage besteht im Grunde aus einer Reihe von Anweisungen für das Abrufen und Organisieren von Daten.
Abfragen werden verzögert ausgeführt, da jedes nachfolgende Element im Ergebnis angefordert wird. Wenn Sie
foreach zum Durchlaufen der Ergebnisse verwenden, werden Elemente so zurückgegeben, wie auf sie
zugegriffen wurde. Rufen Sie einfach eine der folgenden Methoden für die Abfragevariable auf, um eine Abfrage
auszuwerten und ihre Ergebnisse ohne das Ausführen einer foreach -Schleife zu speichern:
ToList
ToArray
ToDictionary
ToLookup
Es wird empfohlen, dass Sie die zurückgegebenen Auflistungsobjekte beim Speichern der Abfrageergebnisse
einer neuen Variable zuweisen, wie im folgenden Beispiel gezeigt wird:

Beispiel
class StoreQueryResults
{
static List<int> numbers = new List<int>() { 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
static void Main()
{

IEnumerable<int> queryFactorsOfFour =
from num in numbers
where num % 4 == 0
select num;

// Store the results in a new variable


// without executing a foreach loop.
List<int> factorsofFourList = queryFactorsOfFour.ToList();

// Iterate the list just to prove it holds data.


Console.WriteLine(factorsofFourList[2]);
factorsofFourList[2] = 0;
Console.WriteLine(factorsofFourList[2]);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key");
Console.ReadKey();
}
}

Siehe auch
Language-Integrated Query (LINQ)
Gruppieren von Abfrageergebnissen
04.11.2021 • 8 minutes to read

Das Gruppieren ist eine der leistungsstärksten Funktionen von LINQ. Die folgenden Beispiele zeigen Ihnen, wie
Sie Daten auf verschiedene Arten und Weisen gruppieren:
Nach einer einzelnen Eigenschaft
Nach dem ersten Buchstaben einer Zeichenfolgeneigenschaft
Nach einem berechneten numerischen Bereich
Nach einem booleschen Prädikat oder einem anderer Ausdruck
Nach einem Verbundschlüssel
Darüber hinaus projizieren die letzten beiden Abfragen die Ergebnisse in einen neuen anonymen Typ, der nur
die Vor- und Nachnamen der Studenten enthält. Weitere Informationen finden Sie unter group-Klausel.

Beispielhilfsklasse und -datenquelle


Alle Beispiele in diesem Thema verwenden die folgenden Hilfsklassen und Datenquellen.

public class StudentClass


{
#region data
protected enum GradeLevel { FirstYear = 1, SecondYear, ThirdYear, FourthYear };
protected class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int ID { get; set; }
public GradeLevel Year;
public List<int> ExamScores;
}

protected static List<Student> students = new List<Student>


{
new Student {FirstName = "Terry", LastName = "Adams", ID = 120,
Year = GradeLevel.SecondYear,
ExamScores = new List<int>{ 99, 82, 81, 79}},
new Student {FirstName = "Fadi", LastName = "Fakhouri", ID = 116,
Year = GradeLevel.ThirdYear,
ExamScores = new List<int>{ 99, 86, 90, 94}},
new Student {FirstName = "Hanying", LastName = "Feng", ID = 117,
Year = GradeLevel.FirstYear,
ExamScores = new List<int>{ 93, 92, 80, 87}},
new Student {FirstName = "Cesar", LastName = "Garcia", ID = 114,
Year = GradeLevel.FourthYear,
ExamScores = new List<int>{ 97, 89, 85, 82}},
new Student {FirstName = "Debra", LastName = "Garcia", ID = 115,
Year = GradeLevel.ThirdYear,
ExamScores = new List<int>{ 35, 72, 91, 70}},
new Student {FirstName = "Hugo", LastName = "Garcia", ID = 118,
Year = GradeLevel.SecondYear,
ExamScores = new List<int>{ 92, 90, 83, 78}},
new Student {FirstName = "Sven", LastName = "Mortensen", ID = 113,
Year = GradeLevel.FirstYear,
ExamScores = new List<int>{ 88, 94, 65, 91}},
new Student {FirstName = "Claire", LastName = "O'Donnell", ID = 112,
Year = GradeLevel.FourthYear,
ExamScores = new List<int>{ 75, 84, 91, 39}},
new Student {FirstName = "Svetlana", LastName = "Omelchenko", ID = 111,
Year = GradeLevel.SecondYear,
ExamScores = new List<int>{ 97, 92, 81, 60}},
new Student {FirstName = "Lance", LastName = "Tucker", ID = 119,
Year = GradeLevel.ThirdYear,
ExamScores = new List<int>{ 68, 79, 88, 92}},
new Student {FirstName = "Michael", LastName = "Tucker", ID = 122,
Year = GradeLevel.FirstYear,
ExamScores = new List<int>{ 94, 92, 91, 91}},
new Student {FirstName = "Eugene", LastName = "Zabokritski", ID = 121,
Year = GradeLevel.FourthYear,
ExamScores = new List<int>{ 96, 85, 91, 60}}
};
#endregion

//Helper method, used in GroupByRange.


protected static int GetPercentile(Student s)
{
double avg = s.ExamScores.Average();
return avg > 0 ? (int)avg / 10 : 0;
}

public void QueryHighScores(int exam, int score)


{
var highScores = from student in students
where student.ExamScores[exam] > score
select new {Name = student.FirstName, Score = student.ExamScores[exam]};

foreach (var item in highScores)


{
Console.WriteLine($"{item.Name,-15}{item.Score}");
}
}
}

public class Program


{
public static void Main()
{
StudentClass sc = new StudentClass();
sc.QueryHighScores(1, 90);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}

Beispiel für die Gruppierung nach einer einzelnen Eigenschaft


Das folgende Beispiel veranschaulicht die Gruppierung von Quellelementen mithilfe einer einzelnen Eigenschaft
des Elements als Gruppenschlüssel. In diesem Fall ist der Schlüssel ein string , der Nachname des Studenten. Es
ist auch möglich, eine Teilzeichenfolge als Schlüssel zu verwenden. Bei der Gruppierungsoperation wird der als
Standard für diesen Typ festgelegte Gleichheitsvergleich verwendet.
Fügen Sie die folgende Methode in die StudentClass -Klasse ein. Ändern Sie die aufrufende Anweisung in der
Methode Main in sc.GroupBySingleProperty() .
public void GroupBySingleProperty()
{
Console.WriteLine("Group by a single property in an object:");

// Variable queryLastNames is an IEnumerable<IGrouping<string,


// DataClass.Student>>.
var queryLastNames =
from student in students
group student by student.LastName into newGroup
orderby newGroup.Key
select newGroup;

foreach (var nameGroup in queryLastNames)


{
Console.WriteLine($"Key: {nameGroup.Key}");
foreach (var student in nameGroup)
{
Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
}
}
}
/* Output:
Group by a single property in an object:
Key: Adams
Adams, Terry
Key: Fakhouri
Fakhouri, Fadi
Key: Feng
Feng, Hanying
Key: Garcia
Garcia, Cesar
Garcia, Debra
Garcia, Hugo
Key: Mortensen
Mortensen, Sven
Key: O'Donnell
O'Donnell, Claire
Key: Omelchenko
Omelchenko, Svetlana
Key: Tucker
Tucker, Lance
Tucker, Michael
Key: Zabokritski
Zabokritski, Eugene
*/

Beispiel für die Gruppierung nach einem Wert


Das folgende Beispiel veranschaulicht die Gruppierung von Quellelemente ohne eine Objekteigenschaft als
Gruppenschlüssel zu verwenden. In diesem Beispiel ist der Schlüssel der erste Buchstabe des Nachnamens des
Studenten.
Fügen Sie die folgende Methode in die StudentClass -Klasse ein. Ändern Sie die aufrufende Anweisung in der
Methode Main in sc.GroupBySubstring() .
public void GroupBySubstring()
{
Console.WriteLine("\r\nGroup by something other than a property of the object:");

var queryFirstLetters =
from student in students
group student by student.LastName[0];

foreach (var studentGroup in queryFirstLetters)


{
Console.WriteLine($"Key: {studentGroup.Key}");
// Nested foreach is required to access group items.
foreach (var student in studentGroup)
{
Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
}
}
}
/* Output:
Group by something other than a property of the object:
Key: A
Adams, Terry
Key: F
Fakhouri, Fadi
Feng, Hanying
Key: G
Garcia, Cesar
Garcia, Debra
Garcia, Hugo
Key: M
Mortensen, Sven
Key: O
O'Donnell, Claire
Omelchenko, Svetlana
Key: T
Tucker, Lance
Tucker, Michael
Key: Z
Zabokritski, Eugene
*/

Beispiel für die Gruppierung nach einem Bereich


Das folgende Beispiel veranschaulicht die Gruppierung von Quellelementen mithilfe eines numerischen Bereichs
als Gruppenschlüssel. Die Abfrage projiziert dann die Ergebnisse in einen anonymen Typ, der nur den Vor- und
Nachnamen und den Prozentbereich enthält, dem der Student angehört. Ein anonymer Typ wird verwendet, da
es nicht notwendig ist, das gesamte Student -Objekt zu nutzen, um die Ergebnisse anzuzeigen. GetPercentile
ist eine Hilfsfunktion, die einen Prozentwert berechnet, der auf dem Durchschnittsergebnis des Studenten
basiert. Die Methode gibt eine ganze Zahl zwischen 0 und 10 zurück.

//Helper method, used in GroupByRange.


protected static int GetPercentile(Student s)
{
double avg = s.ExamScores.Average();
return avg > 0 ? (int)avg / 10 : 0;
}

Fügen Sie die folgende Methode in die StudentClass -Klasse ein. Ändern Sie die aufrufende Anweisung in der
Methode Main in sc.GroupByRange() .
public void GroupByRange()
{
Console.WriteLine("\r\nGroup by numeric range and project into a new anonymous type:");

var queryNumericRange =
from student in students
let percentile = GetPercentile(student)
group new { student.FirstName, student.LastName } by percentile into percentGroup
orderby percentGroup.Key
select percentGroup;

// Nested foreach required to iterate over groups and group items.


foreach (var studentGroup in queryNumericRange)
{
Console.WriteLine($"Key: {studentGroup.Key * 10}");
foreach (var item in studentGroup)
{
Console.WriteLine($"\t{item.LastName}, {item.FirstName}");
}
}
}
/* Output:
Group by numeric range and project into a new anonymous type:
Key: 60
Garcia, Debra
Key: 70
O'Donnell, Claire
Key: 80
Adams, Terry
Feng, Hanying
Garcia, Cesar
Garcia, Hugo
Mortensen, Sven
Omelchenko, Svetlana
Tucker, Lance
Zabokritski, Eugene
Key: 90
Fakhouri, Fadi
Tucker, Michael
*/

Beispiel für die Gruppierung nach einem Vergleich


Das folgende Beispiel veranschaulicht die Gruppierung von Vergleichselementen mithilfe eines booleschen
Vergleichsausdrucks. In diesem Beispiel testet der boolesche Ausdruck, ob die durchschnittliche Bewertung der
Tests eines Studenten größer als 75 ist. Wie in den vorherigen Beispielen werden die Ergebnisse in einen
anonymen Typ projiziert, da nicht das gesamte Quellelement benötigt wird. Beachten Sie, dass die Eigenschaften
im anonymen Typ Eigenschaften des Key -Members werden und dass über den Namen auf sie zugegriffen
werden kann, wenn die Abfrage ausgeführt wird.
Fügen Sie die folgende Methode in die StudentClass -Klasse ein. Ändern Sie die aufrufende Anweisung in der
Methode Main in sc.GroupByBoolean() .
public void GroupByBoolean()
{
Console.WriteLine("\r\nGroup by a Boolean into two groups with string keys");
Console.WriteLine("\"True\" and \"False\" and project into a new anonymous type:");
var queryGroupByAverages = from student in students
group new { student.FirstName, student.LastName }
by student.ExamScores.Average() > 75 into studentGroup
select studentGroup;

foreach (var studentGroup in queryGroupByAverages)


{
Console.WriteLine($"Key: {studentGroup.Key}");
foreach (var student in studentGroup)
Console.WriteLine($"\t{student.FirstName} {student.LastName}");
}
}
/* Output:
Group by a Boolean into two groups with string keys
"True" and "False" and project into a new anonymous type:
Key: True
Terry Adams
Fadi Fakhouri
Hanying Feng
Cesar Garcia
Hugo Garcia
Sven Mortensen
Svetlana Omelchenko
Lance Tucker
Michael Tucker
Eugene Zabokritski
Key: False
Debra Garcia
Claire O'Donnell
*/

Gruppieren nach einem anonymen Typ


Das folgende Beispiel veranschaulicht die Verwendung eines anonymen Typs für die Kapselung eines Schlüssels
mit mehreren Werten. In diesem Beispiel ist der erste Schlüsselwert der erste Buchstabe des Nachnamens des
Studenten. Der zweite Schlüsselwert ist ein boolescher Wert, der angibt, ob der Student in der ersten Prüfung
ein Ergebnis über 85 erzielt hat. Sie können die Gruppen anhand jeder Eigenschaft im Schlüssel sortieren.
Fügen Sie die folgende Methode in die StudentClass -Klasse ein. Ändern Sie die aufrufende Anweisung in der
Methode Main in sc.GroupByCompositeKey() .
public void GroupByCompositeKey()
{
var queryHighScoreGroups =
from student in students
group student by new { FirstLetter = student.LastName[0],
Score = student.ExamScores[0] > 85 } into studentGroup
orderby studentGroup.Key.FirstLetter
select studentGroup;

Console.WriteLine("\r\nGroup and order by a compound key:");


foreach (var scoreGroup in queryHighScoreGroups)
{
string s = scoreGroup.Key.Score == true ? "more than" : "less than";
Console.WriteLine($"Name starts with {scoreGroup.Key.FirstLetter} who scored {s} 85");
foreach (var item in scoreGroup)
{
Console.WriteLine($"\t{item.FirstName} {item.LastName}");
}
}
}

/* Output:
Group and order by a compound key:
Name starts with A who scored more than 85
Terry Adams
Name starts with F who scored more than 85
Fadi Fakhouri
Hanying Feng
Name starts with G who scored more than 85
Cesar Garcia
Hugo Garcia
Name starts with G who scored less than 85
Debra Garcia
Name starts with M who scored more than 85
Sven Mortensen
Name starts with O who scored less than 85
Claire O'Donnell
Name starts with O who scored more than 85
Svetlana Omelchenko
Name starts with T who scored less than 85
Lance Tucker
Name starts with T who scored more than 85
Michael Tucker
Name starts with Z who scored more than 85
Eugene Zabokritski
*/

Siehe auch
GroupBy
IGrouping<TKey,TElement>
Language-Integrated Query (LINQ)
group-Klausel
Anonyme Typen
Ausführen einer Unterabfrage für eine Gruppierungsoperation
Erstellen einer geschachtelten Gruppe
Gruppieren von Daten
Erstellen einer geschachtelten Gruppe
04.11.2021 • 2 minutes to read

Im folgenden Beispiel wird veranschaulicht, wie geschachtelte Gruppen in einem LINQ-Abfrageausdruck erstellt
werden. Jede Gruppe, die nach Studienjahr oder Klassenstufe erstellt wird, wird basierend auf den Namen der
einzelnen Personen weiter in Gruppen unterteilt.

Beispiel
NOTE
In diesem Beispiel sind Verweise auf Objekte enthalten, die im Beispielcode in Abfragen einer Auflistung von Objekten
definiert sind.
public void QueryNestedGroups()
{
var queryNestedGroups =
from student in students
group student by student.Year into newGroup1
from newGroup2 in
(from student in newGroup1
group student by student.LastName)
group newGroup2 by newGroup1.Key;

// Three nested foreach loops are required to iterate


// over all elements of a grouped group. Hover the mouse
// cursor over the iteration variables to see their actual type.
foreach (var outerGroup in queryNestedGroups)
{
Console.WriteLine($"DataClass.Student Level = {outerGroup.Key}");
foreach (var innerGroup in outerGroup)
{
Console.WriteLine($"\tNames that begin with: {innerGroup.Key}");
foreach (var innerGroupElement in innerGroup)
{
Console.WriteLine($"\t\t{innerGroupElement.LastName} {innerGroupElement.FirstName}");
}
}
}
}
/*
Output:
DataClass.Student Level = SecondYear
Names that begin with: Adams
Adams Terry
Names that begin with: Garcia
Garcia Hugo
Names that begin with: Omelchenko
Omelchenko Svetlana
DataClass.Student Level = ThirdYear
Names that begin with: Fakhouri
Fakhouri Fadi
Names that begin with: Garcia
Garcia Debra
Names that begin with: Tucker
Tucker Lance
DataClass.Student Level = FirstYear
Names that begin with: Feng
Feng Hanying
Names that begin with: Mortensen
Mortensen Sven
Names that begin with: Tucker
Tucker Michael
DataClass.Student Level = FourthYear
Names that begin with: Garcia
Garcia Cesar
Names that begin with: O'Donnell
O'Donnell Claire
Names that begin with: Zabokritski
Zabokritski Eugene
*/

Beachten Sie, das drei geschachtelte foreach -Schleifen für die Iteration über die inneren Elemente einer
geschachtelten Gruppe erforderlich sind.

Siehe auch
Language-Integrated Query (LINQ)
Ausführen einer Unterabfrage für eine
Gruppierungsoperation
04.11.2021 • 2 minutes to read

In diesem Artikel werden zwei verschiedene Möglichkeiten gezeigt, um eine Abfrage zu erstellen, die Quelldaten
in Gruppen sortiert und anschließend eine Unterabfrage für jede einzelne Gruppe ausführt. Die grundlegende
Technik in jedem Beispiel besteht darin, die Quellelemente mithilfe einer Fortsetzung namens newGroup zu
gruppieren und anschließend eine neue Unterabfrage für newGroup zu erzeugen. Diese Unterabfrage wird für
jede neue Gruppe ausgeführt, die von der äußeren Abfrage erstellt wurde. Beachten Sie, dass in diesem
speziellen Beispiel die endgültige Ausgabe keine Gruppe ist, sondern eine flache Sequenz von anonymen Typen.
Weitere Informationen zum Gruppieren finden Sie unter group-Klausel.
Weitere Informationen zu Fortsetzungen finden Sie unter into. Im folgenden Beispiel wird eine Datenstruktur im
Arbeitsspeicher als Datenquelle verwendet, jedoch gelten für jede Art von LINQ-Datenquelle die gleichen
Prinzipien.

Beispiel
NOTE
In diesem Beispiel sind Verweise auf Objekte enthalten, die im Beispielcode in Abfragen einer Auflistung von Objekten
definiert sind.

public void QueryMax()


{
var queryGroupMax =
from student in students
group student by student.Year into studentGroup
select new
{
Level = studentGroup.Key,
HighestScore =
(from student2 in studentGroup
select student2.ExamScores.Average()).Max()
};

int count = queryGroupMax.Count();


Console.WriteLine($"Number of groups = {count}");

foreach (var item in queryGroupMax)


{
Console.WriteLine($" {item.Level} Highest Score={item.HighestScore}");
}
}

Die Abfrage im oben stehenden Codeausschnitt können auch per Methodensyntax geschrieben werden. Der
folgende Codeausschnitt weist eine semantisch äquivalente Abfrage auf, die per Methodensyntax geschrieben
wurde.
public void QueryMaxUsingMethodSyntax()
{
var queryGroupMax = students
.GroupBy(student => student.Year)
.Select(studentGroup => new
{
Level = studentGroup.Key,
HighestScore = studentGroup.Select(student2 => student2.ExamScores.Average()).Max()
});

int count = queryGroupMax.Count();


Console.WriteLine($"Number of groups = {count}");

foreach (var item in queryGroupMax)


{
Console.WriteLine($" {item.Level} Highest Score={item.HighestScore}");
}
}

Siehe auch
Language-Integrated Query (LINQ)
Gruppieren von Ergebnissen nach
zusammenhängenden Schlüsseln
04.11.2021 • 7 minutes to read

Im folgende Beispiel wird veranschaulicht, wie Elemente in Segmenten gruppiert werden, die Untersequenzen
von zusammenhängenden Schlüsseln darstellen. Gehen wir beispielsweise von der folgenden Sequenz von
Schlüssel-Wert-Paaren aus:

SC H L ÜSSEL W ERT

Ein Wir

Ein finden

Ein dass

B LINQ

C is

Ein cool

B ist

B !

Die folgenden Gruppen werden in dieser Reihenfolge erstellt:


1. Wir, finden, dass
2. LINQ
3. is
4. cool
5. ist, !
Die Lösung wird als threadsichere Erweiterungsmethode implementiert, die die Ergebnisse streamend
zurückgibt. Das bedeutet, dass die Methode während dem Durchlaufen der Quellsequenz die Gruppen erzeugt.
Im Gegensatz zu den Operatoren group und orderby kann die Methode mit dem Zurückgeben der Gruppen an
den Aufrufer beginnen, bevor die ganze Sequenz gelesen wurde.
Um Threadsicherheit zu gewährleisten, wird bei der Iteration der Gruppe eine Kopie der Gruppe oder des
Abschnitts erstellt, wie in den Quellcodekommentaren erläutert. Wenn die Quellsequenz eine lange Sequenz
von zusammenhängenden Elementen enthält, löst die Common Language Runtime möglicherweise eine
OutOfMemoryException-Ausnahme aus.

Beispiel
Im folgenden Beispiel werden die Erweiterungsmethode und der Clientcode gezeigt, der sie verwendet:
using System;
using System.Collections.Generic;
using System.Linq;

namespace ChunkIt
{
// Static class to contain the extension methods.
public static class MyExtensions
{
public static IEnumerable<IGrouping<TKey, TSource>> ChunkBy<TSource, TKey>(this IEnumerable<TSource>
source, Func<TSource, TKey> keySelector)
{
return source.ChunkBy(keySelector, EqualityComparer<TKey>.Default);
}

public static IEnumerable<IGrouping<TKey, TSource>> ChunkBy<TSource, TKey>(this IEnumerable<TSource>


source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
{
// Flag to signal end of source sequence.
const bool noMoreSourceElements = true;

// Auto-generated iterator for the source array.


var enumerator = source.GetEnumerator();

// Move to the first element in the source sequence.


if (!enumerator.MoveNext()) yield break;

// Iterate through source sequence and create a copy of each Chunk.


// On each pass, the iterator advances to the first element of the next "Chunk"
// in the source sequence. This loop corresponds to the outer foreach loop that
// executes the query.
Chunk<TKey, TSource> current = null;
while (true)
{
// Get the key for the current Chunk. The source iterator will churn through
// the source sequence until it finds an element with a key that doesn't match.
var key = keySelector(enumerator.Current);

// Make a new Chunk (group) object that initially has one GroupItem, which is a copy of the
current source element.
current = new Chunk<TKey, TSource>(key, enumerator, value => comparer.Equals(key,
keySelector(value)));

// Return the Chunk. A Chunk is an IGrouping<TKey,TSource>, which is the return value of the
ChunkBy method.
// At this point the Chunk only has the first element in its source sequence. The remaining
elements will be
// returned only when the client code foreach's over this chunk. See Chunk.GetEnumerator for
more info.
yield return current;

// Check to see whether (a) the chunk has made a copy of all its source elements or
// (b) the iterator has reached the end of the source sequence. If the caller uses an inner
// foreach loop to iterate the chunk items, and that loop ran to completion,
// then the Chunk.GetEnumerator method will already have made
// copies of all chunk items before we get here. If the Chunk.GetEnumerator loop did not
// enumerate all elements in the chunk, we need to do it here to avoid corrupting the
iterator
// for clients that may be calling us on a separate thread.
if (current.CopyAllChunkElements() == noMoreSourceElements)
{
yield break;
}
}
}

// A Chunk is a contiguous group of one or more source elements that have the same key. A Chunk
// has a key and a list of ChunkItem objects, which are copies of the elements in the source
sequence.
class Chunk<TKey, TSource> : IGrouping<TKey, TSource>
{
// INVARIANT: DoneCopyingChunk == true ||
// (predicate != null && predicate(enumerator.Current) && current.Value == enumerator.Current)

// A Chunk has a linked list of ChunkItems, which represent the elements in the current chunk.
Each ChunkItem
// has a reference to the next ChunkItem in the list.
class ChunkItem
{
public ChunkItem(TSource value)
{
Value = value;
}
public readonly TSource Value;
public ChunkItem Next = null;
}

// The value that is used to determine matching elements


private readonly TKey key;

// Stores a reference to the enumerator for the source sequence


private IEnumerator<TSource> enumerator;

// A reference to the predicate that is used to compare keys.


private Func<TSource, bool> predicate;

// Stores the contents of the first source element that


// belongs with this chunk.
private readonly ChunkItem head;

// End of the list. It is repositioned each time a new


// ChunkItem is added.
private ChunkItem tail;

// Flag to indicate the source iterator has reached the end of the source sequence.
internal bool isLastSourceElement = false;

// Private object for thread syncronization


private object m_Lock;

// REQUIRES: enumerator != null && predicate != null


public Chunk(TKey key, IEnumerator<TSource> enumerator, Func<TSource, bool> predicate)
{
this.key = key;
this.enumerator = enumerator;
this.predicate = predicate;

// A Chunk always contains at least one element.


head = new ChunkItem(enumerator.Current);

// The end and beginning are the same until the list contains > 1 elements.
tail = head;

m_Lock = new object();


}

// Indicates that all chunk elements have been copied to the list of ChunkItems,
// and the source enumerator is either at the end, or else on an element with a new key.
// the tail of the linked list is set to null in the CopyNextChunkElement method if the
// key of the next element does not match the current chunk's key, or there are no more elements
in the source.
private bool DoneCopyingChunk => tail == null;

// Adds one ChunkItem to the current group


// REQUIRES: !DoneCopyingChunk && lock(this)
private void CopyNextChunkElement()
{
{
// Try to advance the iterator on the source sequence.
// If MoveNext returns false we are at the end, and isLastSourceElement is set to true
isLastSourceElement = !enumerator.MoveNext();

// If we are (a) at the end of the source, or (b) at the end of the current chunk
// then null out the enumerator and predicate for reuse with the next chunk.
if (isLastSourceElement || !predicate(enumerator.Current))
{
enumerator = null;
predicate = null;
}
else
{
tail.Next = new ChunkItem(enumerator.Current);
}

// tail will be null if we are at the end of the chunk elements


// This check is made in DoneCopyingChunk.
tail = tail.Next;
}

// Called after the end of the last chunk was reached. It first checks whether
// there are more elements in the source sequence. If there are, it
// Returns true if enumerator for this chunk was exhausted.
internal bool CopyAllChunkElements()
{
while (true)
{
lock (m_Lock)
{
if (DoneCopyingChunk)
{
// If isLastSourceElement is false,
// it signals to the outer iterator
// to continue iterating.
return isLastSourceElement;
}
else
{
CopyNextChunkElement();
}
}
}
}

public TKey Key => key;

// Invoked by the inner foreach loop. This method stays just one step ahead
// of the client requests. It adds the next element of the chunk only after
// the clients requests the last element in the list so far.
public IEnumerator<TSource> GetEnumerator()
{
//Specify the initial element to enumerate.
ChunkItem current = head;

// There should always be at least one ChunkItem in a Chunk.


while (current != null)
{
// Yield the current item in the list.
yield return current.Value;

// Copy the next item from the source sequence,


// if we are at the end of our local list.
lock (m_Lock)
{
if (current == tail)
{
CopyNextChunkElement();
}
}
}

// Move to the next ChunkItem in the list.


current = current.Next;
}
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>


GetEnumerator();
}
}

// A simple named type is used for easier viewing in the debugger. Anonymous types
// work just as well with the ChunkBy operator.
public class KeyValPair
{
public string Key { get; set; }
public string Value { get; set; }
}

class Program
{
// The source sequence.
public static IEnumerable<KeyValPair> list;

// Query variable declared as class member to be available


// on different threads.
static IEnumerable<IGrouping<string, KeyValPair>> query;

static void Main(string[] args)


{
// Initialize the source sequence with an array initializer.
list = new[]
{
new KeyValPair{ Key = "A", Value = "We" },
new KeyValPair{ Key = "A", Value = "think" },
new KeyValPair{ Key = "A", Value = "that" },
new KeyValPair{ Key = "B", Value = "Linq" },
new KeyValPair{ Key = "C", Value = "is" },
new KeyValPair{ Key = "A", Value = "really" },
new KeyValPair{ Key = "B", Value = "cool" },
new KeyValPair{ Key = "B", Value = "!" }
};

// Create the query by using our user-defined query operator.


query = list.ChunkBy(p => p.Key);

// ChunkBy returns IGrouping objects, therefore a nested


// foreach loop is required to access the elements in each "chunk".
foreach (var item in query)
{
Console.WriteLine($"Group key = {item.Key}");
foreach (var inner in item)
{
Console.WriteLine($"\t{inner.Value}");
}
}

Console.WriteLine("Press any key to exit");


Console.ReadKey();
}
}
}

Um die Erweiterungsmethode in Ihrem Projekt zu verwenden, kopieren Sie die statische MyExtensions -Klasse in
eine neue oder eine vorhandene Datenquelldatei, und fügen Sie, falls erforderlich, am Speicherort eine using -
Direktive für den Namespace hinzu.

Siehe auch
Language-Integrated Query (LINQ)
Dynamisches Festlegen von Prädikatfiltern zur
Laufzeit
04.11.2021 • 2 minutes to read

In einigen Fällen wissen Sie bis zur Laufzeit nicht, wie viele Prädikate Sie für die Quellelemente in die where -
Klausel übernehmen müssen. Eine Möglichkeit, mehrere Prädikatfilter dynamisch festzulegen, ist die
Verwendung der Methode Contains, wie im folgenden Beispiel gezeigt wird. Das Beispiel wird auf zwei Arten
erstellt. Zuerst wird das Projekt durch Filtern nach Werten, die im Programm bereitgestellt werden, ausgeführt.
Und dann wird das Projekt mithilfe der Eingabe, die zur Laufzeit bereitgestellt wird, erneut ausgeführt.

Filtern mithilfe der Contains-Methode


1. Erstellen Sie eine neue Konsolenanwendung und nennen Sie sie PredicateFilters .
2. Kopieren Sie die StudentClass -Klasse aus Query a collection of objects (Abfragen einer Auflistung von
Objekten) und fügen Sie ihn in Namespace PredicateFilters unter Klasse Program ein. StudentClass
stellt eine Liste von Student -Objekten bereit.
3. Kommentieren Sie die Main -Methode in StudentClass aus.
4. Ersetzen Sie das Klassen- Program durch den folgenden Code:

class DynamicPredicates : StudentClass


{
static void Main(string[] args)
{
string[] ids = { "111", "114", "112" };

Console.WriteLine("Press any key to exit.");


Console.ReadKey();
}

static void QueryByID(string[] ids)


{
var queryNames =
from student in students
let i = student.ID.ToString()
where ids.Contains(i)
select new { student.LastName, student.ID };

foreach (var name in queryNames)


{
Console.WriteLine($"{name.LastName}: {name.ID}");
}
}
}

5. Fügen Sie die folgende Zeile in die Main -Methode in Klasse DynamicPredicates unter der Deklaration
von ids ein.

QueryById(ids);

6. Führen Sie das Projekt aus.


7. In einem Konsolenfenster wird die folgende Ausgabe angezeigt werden:
Garcia: 114
O'Donnell: 112
Omelchenko: 111
8. Im nächsten Schritt wird das Projekt erneut ausgeführt, dieses Mal mit der Eingabe zur Laufzeit anstelle
von Array ids . Ändern Sie QueryByID(ids) zu QueryByID(args) in der Main -Methode.
9. Führen Sie das Projekt mit den Befehlszeilenargumenten 122 117 120 115 aus. Wenn das Projekt
ausgeführt wird, werden diese Werte Elemente von args , dem Parameter der Main -Methode.
10. In einem Konsolenfenster wird die folgende Ausgabe angezeigt werden:
Adams: 120
Feng: 117
Garcia: 115
Tucker: 122

Filtern mithilfe einer Switch-Anweisung


1. Sie können eine switch -Anweisung zum Auswählen zwischen vordefinierten alternativen Abfragen
verwenden. Im folgenden Beispiel verwendet studentQuery eine andere where -Klausel, abhängig davon,
welche Klassenstufe oder welches Jahr zur Laufzeit angegeben wird.
2. Kopieren Sie die folgende Methode, und fügen Sie sie in die Klasse DynamicPredicates ein.
// To run this sample, first specify an integer value of 1 to 4 for the command
// line. This number will be converted to a GradeLevel value that specifies which
// set of students to query.
// Call the method: QueryByYear(args[0]);

static void QueryByYear(string level)


{
GradeLevel year = (GradeLevel)Convert.ToInt32(level);
IEnumerable<Student> studentQuery = null;
switch (year)
{
case GradeLevel.FirstYear:
studentQuery = from student in students
where student.Year == GradeLevel.FirstYear
select student;
break;
case GradeLevel.SecondYear:
studentQuery = from student in students
where student.Year == GradeLevel.SecondYear
select student;
break;
case GradeLevel.ThirdYear:
studentQuery = from student in students
where student.Year == GradeLevel.ThirdYear
select student;
break;
case GradeLevel.FourthYear:
studentQuery = from student in students
where student.Year == GradeLevel.FourthYear
select student;
break;

default:
break;
}
Console.WriteLine($"The following students are at level {year}");
foreach (Student name in studentQuery)
{
Console.WriteLine($"{name.LastName}: {name.ID}");
}
}

3. Ersetzen Sie in der Main -Methode den Aufruf von QueryByID mit dem folgenden Aufruf, der das erste
Element aus dem args -Array als sein Argument sendet: QueryByYear(args[0]) .
4. Führen Sie das Projekt mit einem Befehlszeilenargument für einen ganzzahligen Wert zwischen 1 und 4
aus.

Siehe auch
Language-Integrated Query (LINQ)
where-Klausel
Ausführen von inneren Verknüpfungen
04.11.2021 • 10 minutes to read

Bei relationalen Datenbanken erzeugt eine innere Verknüpfung einen Ergebnissatz, in dem jedes Element der
ersten Aufzählung einmal für jedes übereinstimmende Element in der zweiten Auflistung erscheint. Wenn ein
Element in der ersten Auflistung keine übereinstimmenden Elemente besitzt, erscheint es nicht im Ergebnissatz.
Die Methode Join, die durch die join -Klausel in C# aufgerufen wird, implementiert eine innere Verknüpfung.
In diesem Artikel erfahren Sie, wie Sie vier Varianten eines inneren Joins ausführen:
Eine einfache innere Verknüpfung, die Elemente aus zwei Datenquellen anhand eines einfachen
Schlüssels verknüpft.
Eine innere Verknüpfung, die Elemente aus zwei Datenquellen anhand eines zusammengesetzten
Schlüssels verknüpft. Mit einem zusammengesetzten Schlüssel, der aus mehr als einem Wert besteht,
können Sie Elemente anhand mehr als einer Eigenschaft verknüpfen.
Eine Mehrfachverknüpfung, in der aufeinanderfolgende Verknüpfungsvorgänge aneinander gehängt
werden.
Eine innere Verknüpfung, die mithilfe einer Gruppenverknüpfung implementiert wird.

Beispiel: einfacher Schlüsseljoin


Im folgenden Beispiel werden zwei Auflistungen erstellt, die Objekte von zwei benutzerdefinierten Typen (
Person und Pet ) enthalten. Die Abfrage verwendet die join -Klausel in C#, um Person -Objekte mit Pet -
Objekten übereinzustimmen, dessen Owner diese Person ist. Die select -Klausel in C# definiert, wie die
resultierenden Objekte aussehen werden. In diesem Beispiel sind die resultierenden Objekte anonyme Typen,
die aus dem Vornamen des Besitzers und dem Haustiernamen bestehen.
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}

/// <summary>
/// Simple inner join.
/// </summary>
public static void InnerJoinExample()
{
Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
Person terry = new Person { FirstName = "Terry", LastName = "Adams" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" };
Person rui = new Person { FirstName = "Rui", LastName = "Raposo" };

Pet barley = new Pet { Name = "Barley", Owner = terry };


Pet boots = new Pet { Name = "Boots", Owner = terry };
Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte };
Pet bluemoon = new Pet { Name = "Blue Moon", Owner = rui };
Pet daisy = new Pet { Name = "Daisy", Owner = magnus };

// Create two lists.


List<Person> people = new List<Person> { magnus, terry, charlotte, arlene, rui };
List<Pet> pets = new List<Pet> { barley, boots, whiskers, bluemoon, daisy };

// Create a collection of person-pet pairs. Each element in the collection


// is an anonymous type containing both the person's name and their pet's name.
var query = from person in people
join pet in pets on person equals pet.Owner
select new { OwnerName = person.FirstName, PetName = pet.Name };

foreach (var ownerAndPet in query)


{
Console.WriteLine($"\"{ownerAndPet.PetName}\" is owned by {ownerAndPet.OwnerName}");
}
}

// This code produces the following output:


//
// "Daisy" is owned by Magnus
// "Barley" is owned by Terry
// "Boots" is owned by Terry
// "Whiskers" is owned by Charlotte
// "Blue Moon" is owned by Rui

Beachten Sie, dass das Person -Objekt, dessen LastName „Huff“ ist, nicht im Ergebnissatz erscheint, weil es kein
Pet -Objekt gibt, bei dem Pet.Owner gleich Person ist.

Beispiel: Join mit zusammengesetztem Schlüssel


Anstatt Elemente anhand nur einer Eigenschaft zu verknüpfen, können Sie einen zusammengesetzten Schlüssel
verwenden, um Elemente anhand mehreren Eigenschaften zu vergleichen. Geben Sie dazu die
Schlüsselauswahlfunktion für jede Auflistung an, um einen anonymen Typ zurückgegeben, der aus den zu
vergleichenden Eigenschaften besteht. Wenn Sie die Eigenschaften beschriften, müssen sie über die gleiche
Bezeichnung in jedem anonymen Typ des Schlüssels verfügen. Die Eigenschaften müssen auch in der gleichen
Reihenfolge angezeigt werden.
Im folgenden Beispiel wird eine Liste von Employee -Objekten und eine Liste von Student -Objekten verwendet,
um zu bestimmen, welche Angestellten auch Studenten sind. Diese beiden Typen haben eine FirstName - und
LastName -Eigenschaft vom Typ String. Die Funktion, die die Verknüpfungsschlüssel aus jedem Element der Liste
erstellt, gibt einen anonymen Typ zurück, der aus den Eigenschaften FirstName und LastName von jedem
Element besteht. Der Verknüpfungsvorgang vergleicht diese zusammengesetzten Schlüssel auf Gleichheit und
gibt Objektpaare aus jeder Liste zurück, in der der Vor- und Nachname übereinstimmen.

class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int EmployeeID { get; set; }
}

class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int StudentID { get; set; }
}

/// <summary>
/// Performs a join operation using a composite key.
/// </summary>
public static void CompositeKeyJoinExample()
{
// Create a list of employees.
List<Employee> employees = new List<Employee> {
new Employee { FirstName = "Terry", LastName = "Adams", EmployeeID = 522459 },
new Employee { FirstName = "Charlotte", LastName = "Weiss", EmployeeID = 204467 },
new Employee { FirstName = "Magnus", LastName = "Hedland", EmployeeID = 866200 },
new Employee { FirstName = "Vernette", LastName = "Price", EmployeeID = 437139 } };

// Create a list of students.


List<Student> students = new List<Student> {
new Student { FirstName = "Vernette", LastName = "Price", StudentID = 9562 },
new Student { FirstName = "Terry", LastName = "Earls", StudentID = 9870 },
new Student { FirstName = "Terry", LastName = "Adams", StudentID = 9913 } };

// Join the two data sources based on a composite key consisting of first and last name,
// to determine which employees are also students.
IEnumerable<string> query = from employee in employees
join student in students
on new { employee.FirstName, employee.LastName }
equals new { student.FirstName, student.LastName }
select employee.FirstName + " " + employee.LastName;

Console.WriteLine("The following people are both employees and students:");


foreach (string name in query)
Console.WriteLine(name);
}

// This code produces the following output:


//
// The following people are both employees and students:
// Terry Adams
// Vernette Price

Beispiel: Mehrfachjoin
Eine beliebige Anzahl von Verknüpfungsvorgängen kann aneinander gehängt werden, um eine
Mehrfachverknüpfung auszuführen. Jede join -Klausel in C# verknüpft eine angegebene Datenquelle mit den
Ergebnissen der vorherigen Verknüpfung.
Im folgenden Beispiel werden drei Auflistungen erstellt: eine Liste von Person -Objekten, eine Liste von Cat -
Objekten und eine Liste von Dog -Objekten.
Die erste -Klausel in C# stimmt Personen und Katzen überein, anhand eines Person -Objekts, das mit
join
Cat.Owner übereinstimmt. Es gibt eine Sequenz von anonymen Typen zurück, die das Person -Objekt und
Cat.Name enthält.

Die zweite join -Klausel in C# verknüpft die von der ersten Verknüpfung zurückgegebenen anonymen Typen
mit Dog -Objekten in der bereitgestellten Liste von Hunden anhand eines zusammengesetzten Schlüssels, der
aus der Owner -Eigenschaft des Person -Typs und dem ersten Buchstaben des Tiernamens besteht. Sie gibt eine
Sequenz von anonymen Typen zurück, die die Eigenschaft Cat.Name und Dog.Name von jedem übereinstimmen
Paar enthält. Da es sich um eine innere Verknüpfung handelt, werden nur die Elemente aus der ersten
Datenquelle zurückgegeben, die eine Übereinstimmung in der zweiten Datenquelle haben.

class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}

class Cat : Pet


{ }

class Dog : Pet


{ }

public static void MultipleJoinExample()


{
Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
Person terry = new Person { FirstName = "Terry", LastName = "Adams" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" };
Person rui = new Person { FirstName = "Rui", LastName = "Raposo" };
Person phyllis = new Person { FirstName = "Phyllis", LastName = "Harris" };

Cat barley = new Cat { Name = "Barley", Owner = terry };


Cat boots = new Cat { Name = "Boots", Owner = terry };
Cat whiskers = new Cat { Name = "Whiskers", Owner = charlotte };
Cat bluemoon = new Cat { Name = "Blue Moon", Owner = rui };
Cat daisy = new Cat { Name = "Daisy", Owner = magnus };

Dog fourwheeldrive = new Dog { Name = "Four Wheel Drive", Owner = phyllis };
Dog duke = new Dog { Name = "Duke", Owner = magnus };
Dog denim = new Dog { Name = "Denim", Owner = terry };
Dog wiley = new Dog { Name = "Wiley", Owner = charlotte };
Dog snoopy = new Dog { Name = "Snoopy", Owner = rui };
Dog snickers = new Dog { Name = "Snickers", Owner = arlene };

// Create three lists.


List<Person> people =
new List<Person> { magnus, terry, charlotte, arlene, rui, phyllis };
List<Cat> cats =
new List<Cat> { barley, boots, whiskers, bluemoon, daisy };
List<Dog> dogs =
new List<Dog> { fourwheeldrive, duke, denim, wiley, snoopy, snickers };
// The first join matches Person and Cat.Owner from the list of people and
// cats, based on a common Person. The second join matches dogs whose names start
// with the same letter as the cats that have the same owner.
var query = from person in people
join cat in cats on person equals cat.Owner
join dog in dogs on
new { Owner = person, Letter = cat.Name.Substring(0, 1) }
equals new { dog.Owner, Letter = dog.Name.Substring(0, 1) }
select new { CatName = cat.Name, DogName = dog.Name };

foreach (var obj in query)


{
Console.WriteLine(
$"The cat \"{obj.CatName}\" shares a house, and the first letter of their name, with \"
{obj.DogName}\".");
}
}

// This code produces the following output:


//
// The cat "Daisy" shares a house, and the first letter of their name, with "Duke".
// The cat "Whiskers" shares a house, and the first letter of their name, with "Wiley".

Beispiel: innerer Join mithilfe eines gruppierten Joins


In den folgenden Beispielen wird Ihnen gezeigt, wie eine innere Verknüpfung mithilfe einer
Gruppenverknüpfung implementiert wird.
Die Liste von Person -Objekten in query1 ist über eine Gruppenverknüpfung mit der Liste von Pet -Objekten
verknüpft, basierend auf der Person , die mit der Eigenschaft Pet.Owner übereinstimmt. Die
Gruppeverknüpfung erstellt eine Auflistung von Zwischengruppen, bei der jede Gruppe aus einem Person -
Objekt und einer Sequenz von übereinstimmenden Pet -Objekten besteht.
Durch das Hinzufügen einer zweiten from -Klausel zur Abfrage, wird diese Sequenz von Sequenzen in eine
längere Sequenz vereint (oder vereinfacht). Der Typ der Elemente der endgültigen Sequenz wird von der
select -Klausel festgelegt. In diesem Beispiel ist dieser Typ ein anonymer Typ, der aus der Eigenschaft
Person.FirstName und Pet.Name für jedes übereinstimmende Paar besteht.

Das Ergebnis von query1 entspricht dem Ergebnissatz, der mithilfe der join -Klausel abgerufen werden würde,
ohne dass die into -Klausel eine innere Verknüpfung ausführt. Die query2 -Variable veranschaulicht diese
entsprechende Abfrage.

class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}

/// <summary>
/// Performs an inner join by using GroupJoin().
/// </summary>
public static void InnerGroupJoinExample()
{
Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
Person terry = new Person { FirstName = "Terry", LastName = "Adams" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" };

Pet barley = new Pet { Name = "Barley", Owner = terry };


Pet boots = new Pet { Name = "Boots", Owner = terry };
Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte };
Pet bluemoon = new Pet { Name = "Blue Moon", Owner = terry };
Pet daisy = new Pet { Name = "Daisy", Owner = magnus };

// Create two lists.


List<Person> people = new List<Person> { magnus, terry, charlotte, arlene };
List<Pet> pets = new List<Pet> { barley, boots, whiskers, bluemoon, daisy };

var query1 = from person in people


join pet in pets on person equals pet.Owner into gj
from subpet in gj
select new { OwnerName = person.FirstName, PetName = subpet.Name };

Console.WriteLine("Inner join using GroupJoin():");


foreach (var v in query1)
{
Console.WriteLine($"{v.OwnerName} - {v.PetName}");
}

var query2 = from person in people


join pet in pets on person equals pet.Owner
select new { OwnerName = person.FirstName, PetName = pet.Name };

Console.WriteLine("\nThe equivalent operation using Join():");


foreach (var v in query2)
Console.WriteLine($"{v.OwnerName} - {v.PetName}");
}

// This code produces the following output:


//
// Inner join using GroupJoin():
// Magnus - Daisy
// Terry - Barley
// Terry - Boots
// Terry - Blue Moon
// Charlotte - Whiskers
//
// The equivalent operation using Join():
// Magnus - Daisy
// Terry - Barley
// Terry - Boots
// Terry - Blue Moon
// Charlotte - Whiskers

Weitere Informationen
Join
GroupJoin
Ausführen von Gruppenverknüpfungen
Ausführen linker äußerer Verknüpfungen
Anonyme Typen
Ausführen von Gruppenverknüpfungen
04.11.2021 • 5 minutes to read

Die Gruppenverknüpfung ist nützlich für das Erstellen hierarchischer Datenstrukturen. Sie verbindet jedes
Element aus der ersten Auflistung mit einem Satz von entsprechenden Elementen aus der zweiten Auflistung.
Eine Klasse oder relationale Datenbanktabelle namens Student kann z.B. zwei Felder enthalten: Id und Name .
Eine zweite Klasse oder relationale Datenbanktabelle namens Course kann zwei Felder enthalten: StudentId
und CourseTitle . Eine Gruppenverknüpfung dieser beiden Datenquellen, die auf der übereinstimmenden
Student.Id und Course.StudentId basiert, würde jeden Student mit einer Auflistung von Course -Objekten
gruppieren (die vielleicht leer sind).

NOTE
Jedes Element der ersten Auflistung erscheint im Ergebnissatz einer Gruppenverknüpfung, unabhängig davon, ob
entsprechende Elemente in der zweiten Auflistung gefunden werden. Sollten keine entsprechenden Elemente gefunden
werden, ist die Sequenz der entsprechenden Elemente für das Element leer. Die Ergebnisauswahl hat daher Zugriff auf
jedes Element der ersten Auflistung. Dies unterscheidet sich von der Ergebnisauswahl in einer Verknüpfung, bei der keine
Gruppen verknüpft werden. Diese kann nicht auf Elemente aus der ersten Auflistung zugreifen, die keine
Übereinstimmung in der zweiten Auflistung haben.

WARNING
Enumerable.GroupJoin hat keine direkte Entsprechung unter den Begriffen herkömmlicher relationaler Datenbanken.
Diese Methode implementiert jedoch eine Obermenge innerer Joins und linker äußerer Joins. Beide dieser Vorgänge
können im Hinblick auf einen gruppierten Join geschrieben werden. Weitere Informationen finden Sie unter
Verknüpfungsvorgänge (C#) und unter Entity Framework Core > GroupJoin.

Im ersten Beispiel in diesem Artikel wird das Ausführen eines gruppierten Joins gezeigt. Im zweiten Beispiel
wird gezeigt, wie eine Gruppenverknüpfung zum Erstellen von XML-Elementen verwendet wird.

Beispiel: Gruppierter Join


Das folgende Beispiel führt eine Gruppenverknüpfung von Objekten des Typs Person und Pet aus, die auf der
Person basiert und mit der Pet.Owner -Eigenschaft übereinstimmt. Bei einer Verknüpfung, bei der keine
Gruppen verknüpft werden, wird für jede Übereinstimmung ein Elementpaar erzeugt. Im Gegensatz dazu
erzeugt eine Gruppenverknüpfung nur ein resultierendes Objekt für jedes Element der ersten Auflistung, was in
diesem Beispiel ein Person -Objekt ist. Die entsprechenden Elemente aus der zweiten Auflistung, die in diesem
Beispiel Pet -Objekte sind, werden in einer Auflistung gruppiert. Die Ergebnisauswahlfunktion erstellt
schließlich einen anonymen Typ für jede Übereinstimmung, die aus Person.FirstName und einer Auflistung von
Pet -Objekten besteht.
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}

/// <summary>
/// This example performs a grouped join.
/// </summary>
public static void GroupJoinExample()
{
Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
Person terry = new Person { FirstName = "Terry", LastName = "Adams" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" };

Pet barley = new Pet { Name = "Barley", Owner = terry };


Pet boots = new Pet { Name = "Boots", Owner = terry };
Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte };
Pet bluemoon = new Pet { Name = "Blue Moon", Owner = terry };
Pet daisy = new Pet { Name = "Daisy", Owner = magnus };

// Create two lists.


List<Person> people = new List<Person> { magnus, terry, charlotte, arlene };
List<Pet> pets = new List<Pet> { barley, boots, whiskers, bluemoon, daisy };

// Create a list where each element is an anonymous type


// that contains the person's first name and a collection of
// pets that are owned by them.
var query = from person in people
join pet in pets on person equals pet.Owner into gj
select new { OwnerName = person.FirstName, Pets = gj };

foreach (var v in query)


{
// Output the owner's name.
Console.WriteLine($"{v.OwnerName}:");
// Output each of the owner's pet's names.
foreach (Pet pet in v.Pets)
Console.WriteLine($" {pet.Name}");
}
}

// This code produces the following output:


//
// Magnus:
// Daisy
// Terry:
// Barley
// Boots
// Blue Moon
// Charlotte:
// Whiskers
// Arlene:

Beispiel: Gruppierter Join zum Erstellen einer XML


Gruppenverknüpfungen lassen sich ideal für das Erstellen von XML mithilfe von LINQ to XML nutzen. Das
folgende Beispiel ähnelt dem vorherigen, nur dass die Ergebnisauswahlfunktion anstatt eines anonymen Typs
XML-Elemente erstellt, die die verknüpften Objekte darstellen.

class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}

/// <summary>
/// This example creates XML output from a grouped join.
/// </summary>
public static void GroupJoinXMLExample()
{
Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
Person terry = new Person { FirstName = "Terry", LastName = "Adams" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" };

Pet barley = new Pet { Name = "Barley", Owner = terry };


Pet boots = new Pet { Name = "Boots", Owner = terry };
Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte };
Pet bluemoon = new Pet { Name = "Blue Moon", Owner = terry };
Pet daisy = new Pet { Name = "Daisy", Owner = magnus };

// Create two lists.


List<Person> people = new List<Person> { magnus, terry, charlotte, arlene };
List<Pet> pets = new List<Pet> { barley, boots, whiskers, bluemoon, daisy };

// Create XML to display the hierarchical organization of people and their pets.
XElement ownersAndPets = new XElement("PetOwners",
from person in people
join pet in pets on person equals pet.Owner into gj
select new XElement("Person",
new XAttribute("FirstName", person.FirstName),
new XAttribute("LastName", person.LastName),
from subpet in gj
select new XElement("Pet", subpet.Name)));

Console.WriteLine(ownersAndPets);
}

// This code produces the following output:


//
// <PetOwners>
// <Person FirstName="Magnus" LastName="Hedlund">
// <Pet>Daisy</Pet>
// </Person>
// <Person FirstName="Terry" LastName="Adams">
// <Pet>Barley</Pet>
// <Pet>Boots</Pet>
// <Pet>Blue Moon</Pet>
// </Person>
// <Person FirstName="Charlotte" LastName="Weiss">
// <Pet>Whiskers</Pet>
// </Person>
// <Person FirstName="Arlene" LastName="Huff" />
// </PetOwners>

Siehe auch
Join
GroupJoin
Ausführen innerer Verknüpfungen
Ausführen linker äußerer Verknüpfungen
Anonyme Typen
Ausführen von Left Outer Joins
04.11.2021 • 2 minutes to read

Ein Left Outer Join ist eine Verknüpfung, die jedes Element der ersten Auflistung zurückgibt, unabhängig davon,
ob es entsprechende Elemente in der zweiten Auflistung gibt. Sie können LINQ verwenden, um eine linke
äußere Verknüpfung auszuführen, indem die Methode DefaultIfEmpty bei den Ergebnissen einer
Gruppenverknüpfung aufgerufen wird.

Beispiel
Im folgenden Beispiel wird veranschaulicht, wie die Methode DefaultIfEmpty bei den Ergebnissen einer
Gruppenverknüpfung verwendet wird, um eine linke äußere Verknüpfung auszuführen.
Der erste Schritt zum Erstellen eines Left Outer Join von zwei Auflistungen besteht darin, eine innere
Verknüpfung durch Gruppenverknüpfung auszuführen. (Eine Erklärung dieses Vorgangs finden Sie unter
Ausführen von inneren Verknüpfungen.) In diesem Beispiel wird die Liste der Person -Objekte über eine innere
Verknüpfung mit der Liste der Pet -Objekte verknüpft, die auf einem Person -Objekt basieren, dass mit
Pet.Owner übereinstimmt.

Der zweite Schritt besteht darin, jedes Element der ersten (linken) Liste in den Ergebnissatz einzuschließen,
selbst wenn das Element in der rechten Auflistung keine Übereinstimmungen hat. Dies wird durch den Aufruf
von DefaultIfEmpty für jede Sequenz von übereinstimmenden Elementen aus der Gruppenverknüpfung erreicht.
In diesem Beispiel wird DefaultIfEmpty für jede Sequenz von übereinstimmenden Pet -Objekten aufgerufen.
Diese Methode gibt eine Auflistung zurück, die einen einzelnen Standardwert enthält, wenn die Sequenz von
übereinstimmenden Pet -Objekten für jedes Person -Objekt leer ist, womit die Methode sicherstellt, dass jedes
Person -Objekt in der Ergebnisauflistung dargestellt wird.

NOTE
Der Standardwert für einen Verweistyp ist null ; deshalb wird im Beispiel nach einem NULL-Verweis gesucht, bevor auf
jedes Element jeder Pet -Auflistung zugegriffen wird.
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}

public static void LeftOuterJoinExample()


{
Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
Person terry = new Person { FirstName = "Terry", LastName = "Adams" };
Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" };
Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" };

Pet barley = new Pet { Name = "Barley", Owner = terry };


Pet boots = new Pet { Name = "Boots", Owner = terry };
Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte };
Pet bluemoon = new Pet { Name = "Blue Moon", Owner = terry };
Pet daisy = new Pet { Name = "Daisy", Owner = magnus };

// Create two lists.


List<Person> people = new List<Person> { magnus, terry, charlotte, arlene };
List<Pet> pets = new List<Pet> { barley, boots, whiskers, bluemoon, daisy };

var query = from person in people


join pet in pets on person equals pet.Owner into gj
from subpet in gj.DefaultIfEmpty()
select new { person.FirstName, PetName = subpet?.Name ?? String.Empty };

foreach (var v in query)


{
Console.WriteLine($"{v.FirstName+":",-15}{v.PetName}");
}
}

// This code produces the following output:


//
// Magnus: Daisy
// Terry: Barley
// Terry: Boots
// Terry: Blue Moon
// Charlotte: Whiskers
// Arlene:

Siehe auch
Join
GroupJoin
Ausführen innerer Verknüpfungen
Ausführen von Gruppenverknüpfungen
Anonyme Typen
Sortieren der Ergebnisse einer Join-Klausel
04.11.2021 • 2 minutes to read

Dieses Beispiel zeigt, wie Sie die Ergebnisse einer Verknüpfungsoperation sortieren. Beachten Sie, dass die
Sortierung nach der Verknüpfung ausgeführt wird. Obwohl Sie eine orderby -Klausel mit einer oder mehreren
Quellsequenzen verwenden können, wird dies normalerweise nicht empfohlen. Einige LINQ-Anbieter könnten
diese Sortierung nach der Verknüpfung vielleicht nicht beibehalten.

Beispiel
Diese Abfrage erstellt eine Gruppenverknüpfung und sortiert die Gruppen anschließend anhand des Elements
der Kategorie, das sich noch im Geltungsbereich befindet. Innerhalb des anonymen Typinitialisierers ordnet eine
Unterabfrage alle übereinstimmende Elemente aus der Produktsequenz.

class HowToOrderJoins
{
#region Data
class Product
{
public string Name { get; set; }
public int CategoryID { get; set; }
}

class Category
{
public string Name { get; set; }
public int ID { get; set; }
}

// Specify the first data source.


List<Category> categories = new List<Category>()
{
new Category(){Name="Beverages", ID=001},
new Category(){ Name="Condiments", ID=002},
new Category(){ Name="Vegetables", ID=003},
new Category() { Name="Grains", ID=004},
new Category() { Name="Fruit", ID=005}
};

// Specify the second data source.


List<Product> products = new List<Product>()
{
new Product{Name="Cola", CategoryID=001},
new Product{Name="Tea", CategoryID=001},
new Product{Name="Mustard", CategoryID=002},
new Product{Name="Pickles", CategoryID=002},
new Product{Name="Carrots", CategoryID=003},
new Product{Name="Bok Choy", CategoryID=003},
new Product{Name="Peaches", CategoryID=005},
new Product{Name="Melons", CategoryID=005},
};
#endregion
static void Main()
{
HowToOrderJoins app = new HowToOrderJoins();
app.OrderJoin1();

// Keep console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
Console.ReadKey();
}

void OrderJoin1()
{
var groupJoinQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
orderby category.Name
select new
{
Category = category.Name,
Products = from prod2 in prodGroup
orderby prod2.Name
select prod2
};

foreach (var productGroup in groupJoinQuery2)


{
Console.WriteLine(productGroup.Category);
foreach (var prodItem in productGroup.Products)
{
Console.WriteLine($" {prodItem.Name,-10} {prodItem.CategoryID}");
}
}
}
/* Output:
Beverages
Cola 1
Tea 1
Condiments
Mustard 2
Pickles 2
Fruit
Melons 5
Peaches 5
Grains
Vegetables
Bok Choy 3
Carrots 3
*/
}

Siehe auch
Language-Integrated Query (LINQ)
orderby-Klausel
join-Klausel
Verknüpfen mithilfe eines zusammengesetzten
Schlüssels
04.11.2021 • 2 minutes to read

In diesem Beispiel erfahren Sie, wie sie Verknüpfungsvorgänge durchführen können, in denen Sie mehr als
einen Schlüssel zur Definition einer Übereinstimmung verwenden möchten. Dies können Sie mithilfe eines
zusammengesetzten Schlüssels machen. Ein zusammengesetzter Schlüssel wird als anonymer oder benannter
Typ mit den Werten, die Sie vergleichen möchten, erstellt. Wenn die Abfragevariable
methodengrenzenübergreifend übergeben wird, verwenden Sie einen benannten Typ, der Equals und
GetHashCode für den Schlüssel außer Kraft setzt. Der Name der Eigenschaften und die Reihenfolge, in der diese
auftreten, muss in jedem Schlüssel identisch sein.

Beispiel
In folgendem Beispiel erfahren Sie, wie Sie einen zusammengesetzten Schlüssel verwenden können, um Daten
aus drei Tabellen zu verknüpfen:

var query = from o in db.Orders


from p in db.Products
join d in db.OrderDetails
on new {o.OrderID, p.ProductID} equals new {d.OrderID, d.ProductID} into details
from d in details
select new {o.OrderID, p.ProductID, d.UnitPrice};

Typrückschluss für zusammengesetzte Schlüssel hängt von den Namen der Eigenschaften in den Schlüsseln und
deren Reihenfolge ab. Wenn die Eigenschaften der Quellsequenz nicht dieselben Namen haben, müssen Sie
ihnen im Schlüssel neue Namen zuweisen. Wenn in den Tabellen Orders und OrderDetails beispielsweise
unterschiedliche Namen für die Spalten verwendet werden, können Sie zusammengesetzte Schlüssel erstellen,
indem Sie identische Namen in den anonymen Typen zuweisen:

join...on new {Name = o.CustomerName, ID = o.CustID} equals


new {Name = d.CustName, ID = d.CustID }

Zusammengesetzte Schlüssel können auch in einer group -Klausel verwendet werden.

Siehe auch
Language-Integrated Query (LINQ)
join-Klausel
group-Klausel
Ausführen von benutzerdefinierten
Verknüpfungsoperationen
04.11.2021 • 4 minutes to read

Dieses Beispiel veranschaulicht, wie Sie JOIN-Vorgänge ausführen, die nicht mit der join -Klausel ausgeführt
werden können. In einem Abfrageausdruck ist die join -Klausel auf Gleichheitsverknüpfungen, den bei weitem
häufigsten Typ einer Verknüpfungsoperation, beschränkt und dafür optimiert. Wenn eine
Gleichheitsverknüpfung ausgeführt wird, erhalten Sie die beste Leistung wahrscheinlich immer mit der join -
Klausel.
Die join -Klausel kann jedoch nicht in den folgenden Fällen verwendet werden:
Wenn die Verknüpfung auf einem Ausdruck der Ungleichheit basiert (eine Ungleichheitsverknüpfung)
Wenn die Verknüpfung auf mehr als einem Ausdruck der Gleich- oder Ungleichheit basiert
Wenn Sie eine temporäre Bereichsvariable für die rechte (innere) Sequenz vor der
Verknüpfungsoperation einführen müssen.
Wenn Sie Verknüpfungen ausführen möchten, die keine Gleichheitsverknüpfungen sind, können Sie mehrere
from -Klauseln verwenden, um jede Datenquelle einzeln einzuführen. Sie wenden anschließend einen
Prädikatsausdruck in einer where -Klausel auf die Bereichsvariable für jede Quelle an. Der Ausdruck kann auch
die Form eines Methodenaufrufs annehmen.

NOTE
Verwechseln Sie diese Art des benutzerdefinierten JOIN-Vorgangs nicht mit der Verwendung mehrerer from -Klauseln
für den Zugriff auf interne Sammlungen. Weitere Informationen finden Sie unter join-Klausel.

Beispiel 1
Die erste Methode im folgenden Beispiel zeigt eine einfache Kreuzverknüpfung. Kreuzverknüpfungen müssen
mit Bedacht verwendet werden, da sie einen sehr großen Ergebnissatz erzeugen können. Sie können jedoch in
manchen Szenarios nützlich sein, um Quellsequenzen zu erstellen, mit denen zusätzliche Abfragen ausgeführt
werden.
Die zweite Methode erzeugt eine Sequenz aller Produkte, deren Kategorie-ID in der Kategorieliste auf der linken
Seite aufgeführt ist. Beachten Sie, dass Sie mit der Verwendung der let -Klausel und der Contains -Methode
ein temporäres Array erstellen. Es ist auch möglich, das Array vor der Abfrage zu erstellen und die erste from -
Klausel zu löschen.

class CustomJoins
{

#region Data

class Product
{
public string Name { get; set; }
public int CategoryID { get; set; }
}

class Category
class Category
{
public string Name { get; set; }
public int ID { get; set; }
}

// Specify the first data source.


List<Category> categories = new List<Category>()
{
new Category(){Name="Beverages", ID=001},
new Category(){ Name="Condiments", ID=002},
new Category(){ Name="Vegetables", ID=003},
};

// Specify the second data source.


List<Product> products = new List<Product>()
{
new Product{Name="Tea", CategoryID=001},
new Product{Name="Mustard", CategoryID=002},
new Product{Name="Pickles", CategoryID=002},
new Product{Name="Carrots", CategoryID=003},
new Product{Name="Bok Choy", CategoryID=003},
new Product{Name="Peaches", CategoryID=005},
new Product{Name="Melons", CategoryID=005},
new Product{Name="Ice Cream", CategoryID=007},
new Product{Name="Mackerel", CategoryID=012},
};
#endregion

static void Main()


{
CustomJoins app = new CustomJoins();
app.CrossJoin();
app.NonEquijoin();

Console.WriteLine("Press any key to exit.");


Console.ReadKey();
}

void CrossJoin()
{
var crossJoinQuery =
from c in categories
from p in products
select new { c.ID, p.Name };

Console.WriteLine("Cross Join Query:");


foreach (var v in crossJoinQuery)
{
Console.WriteLine($"{v.ID,-5}{v.Name}");
}
}

void NonEquijoin()
{
var nonEquijoinQuery =
from p in products
let catIds = from c in categories
select c.ID
where catIds.Contains(p.CategoryID) == true
select new { Product = p.Name, CategoryID = p.CategoryID };

Console.WriteLine("Non-equijoin query:");
foreach (var v in nonEquijoinQuery)
{
Console.WriteLine($"{v.CategoryID,-5}{v.Product}");
}
}
}
/* Output:
/* Output:
Cross Join Query:
1 Tea
1 Mustard
1 Pickles
1 Carrots
1 Bok Choy
1 Peaches
1 Melons
1 Ice Cream
1 Mackerel
2 Tea
2 Mustard
2 Pickles
2 Carrots
2 Bok Choy
2 Peaches
2 Melons
2 Ice Cream
2 Mackerel
3 Tea
3 Mustard
3 Pickles
3 Carrots
3 Bok Choy
3 Peaches
3 Melons
3 Ice Cream
3 Mackerel
Non-equijoin query:
1 Tea
2 Mustard
2 Pickles
3 Carrots
3 Bok Choy
Press any key to exit.
*/

Beispiel 2
Im folgenden Beispiel muss die Abfrage zwei Sequenzen anhand übereinstimmender Schlüssel verknüpfen, die
bei der inneren (rechten) Sequenz nicht vor der eigentlichen Join-Klausel abgerufen werden können. Wenn
diese Verknüpfung mit einer join -Klausel ausgeführt werden würde, müsste anschließend die Split -Methode
für jedes Element aufgerufen werden. Mit der Verwendung von mehreren from -Klauseln kann die Abfrage den
Mehraufwand des wiederholten Methodenaufrufs vermeiden. Da join jedoch optimiert ist, könnte es in
diesem Sonderfall trotzdem schneller sein als mehrere from -Klauseln zu verwenden. Die Ergebnisse variieren
hauptsächlich nach Aufwand des Methodenaufrufs.

class MergeTwoCSVFiles
{
static void Main()
{
// See section Compiling the Code for information about the data files.
string[] names = System.IO.File.ReadAllLines(@"../../../names.csv");
string[] scores = System.IO.File.ReadAllLines(@"../../../scores.csv");

// Merge the data sources using a named type.


// You could use var instead of an explicit type for the query.
IEnumerable<Student> queryNamesScores =
// Split each line in the data files into an array of strings.
from name in names
let x = name.Split(',')
from score in scores
let s = score.Split(',')
// Look for matching IDs from the two data files.
where x[2] == s[0]
// If the IDs match, build a Student object.
select new Student()
{
FirstName = x[0],
LastName = x[1],
ID = Convert.ToInt32(x[2]),
ExamScores = (from scoreAsText in s.Skip(1)
select Convert.ToInt32(scoreAsText)).
ToList()
};

// Optional. Store the newly created student objects in memory


// for faster access in future queries
List<Student> students = queryNamesScores.ToList();

foreach (var student in students)


{
Console.WriteLine($"The average score of {student.FirstName} {student.LastName} is
{student.ExamScores.Average()}.");
}

//Keep console window open in debug mode


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int ID { get; set; }
public List<int> ExamScores { get; set; }
}

/* Output:
The average score of Omelchenko Svetlana is 82.5.
The average score of O'Donnell Claire is 72.25.
The average score of Mortensen Sven is 84.5.
The average score of Garcia Cesar is 88.25.
The average score of Garcia Debra is 67.
The average score of Fakhouri Fadi is 92.25.
The average score of Feng Hanying is 88.
The average score of Garcia Hugo is 85.75.
The average score of Tucker Lance is 81.75.
The average score of Adams Terry is 85.25.
The average score of Zabokritski Eugene is 83.
The average score of Tucker Michael is 92.
*/

Siehe auch
Language-Integrated Query (LINQ)
join-Klausel
Sortieren der Ergebnisse einer Join-Klausel
Behandeln von NULL-Werten in Abfrageausdrücken
04.11.2021 • 2 minutes to read

Dieses Beispiel zeigt, wie mögliche NULL-Werte in Quellauflistungen behandelt werden. Eine Objektauflistung
wie z.B. IEnumerable<T> kann Elemente enthalten, deren Wert NULL ist. Wenn eine Quellauflistung null ist
oder ein Element enthält, dessen Wert null ist, und die Abfrage keine null -Werte verarbeitet, wird eine
NullReferenceException ausgelöst, wenn Sie die Abfrage ausführen.
Sie können defensiv codieren, um eine Nullverweisausnahme wie im folgenden Beispiel dargestellt zu
vermeiden:

var query1 =
from c in categories
where c != null
join p in products on c.ID equals
p?.CategoryID
select new { Category = c.Name, Name = p.Name };

Im vorherigen Beispiel filtert die where -Klausel alle NULL-Elemente in der Reihenfolge der Kategorien heraus.
Diese Technik ist unabhängig von der NULL-Überprüfung in der join-Klausel. Der bedingte Ausdruck mit NULL
in diesem Beispiel funktioniert, da Products.CategoryID vom Typ int? ist, was eine Abkürzung für
Nullable<int> ist.

Wenn in einer join-Klausel nur einer der Vergleichsschlüssel ein Nullable-Werttyp ist, können Sie den anderen
Schlüssel im Abfrageausdruck in einen Nullable-Werttyp umwandeln. Im folgenden Beispiel wird angenommen,
dass EmployeeID eine Spalte mit Werten vom Typ int? ist:

void TestMethod(Northwind db)


{
var query =
from o in db.Orders
join e in db.Employees
on o.EmployeeID equals (int?)e.EmployeeID
select new { o.OrderID, e.FirstName };
}

In jedem der Beispiele wird das equals Abfragenschlüsselwort verwendet. C# 9 fügt einen Musterabgleich
hinzu,der Muster für is null und is not null enthält. Diese Muster werden in LINQ-Abfragen nicht
empfohlen, da Abfrageanbieter die neue C#-Syntax möglicherweise nicht richtig interpretieren. Ein
Abfrageanbieter ist eine Bibliothek, die C#-Abfrageausdrücke in ein natives Datenformat übersetzt, z. B. Entity
Framework Core. Abfrageanbieter implementieren die System.Linq.IQueryProvider -Schnittstelle, um
Datenquellen zu erstellen, die die -Schnittstelle System.Linq.IQueryable<T> implementieren.

Siehe auch
Nullable<T>
Language-Integrated Query (LINQ)
Auf NULL festlegbare Werttypen
Behandeln von Ausnahmen in Abfrageausdrücken
04.11.2021 • 2 minutes to read

Es ist möglich, jede Methode im Kontext eines Abfrageausdrucks aufzurufen. Es empfiehlt sich jedoch, Methoden
in einem Abfrageausdruck aufzurufen, die Nebeneffekte wie die Änderung des Inhalts der Datenquelle oder das
Auslösen einer Ausnahme erzeugen können. In diesem Beispiel wird veranschaulicht, wie Sie es beim Aufrufen
von Methoden in Abfrageausdrücken vermeiden, Ausnahmen auszulösen, ohne gegen die allgemeinen .NET-
Richtlinien für die Behandlung von Ausnahmen zu verstoßen. Gemäß dieser Richtlinien dürfen Sie eine
bestimmte Ausnahme abfangen, wenn Sie wissen, warum sie in einem bestimmten Kontext ausgelöst wird.
Weitere Informationen finden Sie unter Best Practices für Ausnahmen.
Im letzten Beispiel wird der Umgang mit diesen Fällen veranschaulicht, wenn Sie während der Ausführung einer
Abfrage eine Ausnahme auslösen müssen.

Beispiel 1
Im folgenden Beispiel wird veranschaulicht, wie Sie den Ausnahmebehandlungscode aus einem
Abfrageausdruck verschieben. Dies ist nur möglich, wenn die Methode von keiner für die Abfrage lokalen
Variablen abhängig ist.
class ExceptionsOutsideQuery
{
static void Main()
{
// DO THIS with a datasource that might
// throw an exception. It is easier to deal with
// outside of the query expression.
IEnumerable<int> dataSource;
try
{
dataSource = GetData();
}
catch (InvalidOperationException)
{
// Handle (or don't handle) the exception
// in the way that is appropriate for your application.
Console.WriteLine("Invalid operation");
goto Exit;
}

// If we get here, it is safe to proceed.


var query = from i in dataSource
select i * i;

foreach (var i in query)


Console.WriteLine(i.ToString());

//Keep the console window open in debug mode


Exit:
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}

// A data source that is very likely to throw an exception!


static IEnumerable<int> GetData()
{
throw new InvalidOperationException();
}
}

Beispiel 2
In einigen Fällen ist die möglicherweise beste Antwort auf eine Ausnahme, die von einer Abfrage ausgelöst wird,
die Ausführung der Abfrage sofort zu beenden. Im folgenden Beispiel wird der Umgang mit Ausnahmen
veranschaulicht, die innerhalb eines Abfragetexts ausgelöst werden können. Angenommen dass
SomeMethodThatMightThrow zu einer Ausnahme führen kann, derentwegen die Ausführung der Abfrage beenden
werden muss.
Beachten Sie, dass der try -Block die foreach -Schleife und nicht die Abfrage selbst enthält. Das liegt daran,
dass die foreach -Schleife der Punkt ist, an dem die Abfrage tatsächlich ausgeführt wird. Weitere Informationen
finden Sie unter Einführung in LINQ-Abfragen.
class QueryThatThrows
{
static void Main()
{
// Data source.
string[] files = { "fileA.txt", "fileB.txt", "fileC.txt" };

// Demonstration query that throws.


var exceptionDemoQuery =
from file in files
let n = SomeMethodThatMightThrow(file)
select n;

// Runtime exceptions are thrown when query is executed.


// Therefore they must be handled in the foreach loop.
try
{
foreach (var item in exceptionDemoQuery)
{
Console.WriteLine($"Processing {item}");
}
}

// Catch whatever exception you expect to raise


// and/or do any necessary cleanup in a finally block
catch (InvalidOperationException e)
{
Console.WriteLine(e.Message);
}

//Keep the console window open in debug mode


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}

// Not very useful as a general purpose method.


static string SomeMethodThatMightThrow(string s)
{
if (s[4] == 'C')
throw new InvalidOperationException();
return @"C:\newFolder\" + s;
}
}
/* Output:
Processing C:\newFolder\fileA.txt
Processing C:\newFolder\fileB.txt
Operation is not valid due to the current state of the object.
*/

Siehe auch
Language-Integrated Query (LINQ)
Schreiben von sicherem und effizientem C#-Code
04.11.2021 • 17 minutes to read

C# bietet Features, mit denen Sie nachweislich sicheren Code mit besserer Leistung schreiben können. Wenn Sie
diese Techniken mit Bedacht einsetzen, sind weniger Szenarios mit unsicherem Code erforderlich. Mit den neuen
Features können Verweise auf Werttypen als Methodenargumente und auf Rückgabewerte von Methoden
leichter verwendet werden. Wenn diese Strategien sicher angewendet werden, müssen weniger Werttypen
kopiert werden. Durch die Verwendung von Werttypen können Sie die Anzahl der Speicherbelegungen und
Garbage Collection-Vorgänge minimieren.
In vielen Codebeispielen in diesem Artikel werden die neuen Features von C# 7.2 verwendet. Um diese Features
nutzen zu können, vergewissern Sie sich, dass Ihr Projekt nicht für die Verwendung einer früheren Version
konfiguriert ist. Weitere Informationen finden Sie unter Konfigurieren der Sprachversion.
Ein Vorteil bei der Nutzung von Werttypen besteht darin, dass häufig eine Heapspeicherbelegung vermieden
wird. Der Nachteil ist, dass sie als Wert kopiert werden. Dieser Kompromiss macht es schwieriger, Algorithmen
zu optimieren, die mit großen Datenmengen arbeiten. Die in diesem Artikel vorgestellten Sprachfeatures bieten
Mechanismen, die sicheren, effizienten Code mit Verweisen auf Werttypen ermöglichen. Wenn Sie diese
Features geschickt einsetzen, können Sie sowohl Speicherbelegungen als auch Kopiervorgänge minimieren.
Einige der Anleitungen in diesem Artikel verweisen auf Programmierpraktiken, die stets ratsam sind, nicht nur
wegen des Leistungsvorteils. Verwenden Sie das Schlüsselwort readonly , wenn es die Entwurfsabsicht genau
ausdrückt:
Deklarieren Sie unveränderliche Strukturen als readonly .
Deklarieren Sie readonly -Member für veränderbare Strukturen.
Im Artikel werden auch einige Optimierungen auf untergeordneter Ebene erläutert, die ratsam sind, wenn Sie
einen Profiler ausgeführt und Engpässe ausgemacht haben:
Verwenden Sie den Parametermodifizierer in .
Verwenden Sie ref readonly return -Anweisungen.
Verwenden Sie ref struct -Typen.
Verwenden Sie die Typen nint und nuint .
Diese Vorgehensweisen bringen zwei konkurrierende Ziele in Einklang:
Minimieren von Zuteilungen für den Heap.
Als Verweistypen fungierende Variablen enthalten einen Verweis auf einen Ort im Arbeitsspeicher und
werden dem verwalteten Heap zugeteilt. Wenn ein Verweistyp als Argument an eine Methode übergeben
oder von einer Methode zurückgegeben wird, wird nur der Verweis kopiert. Jedes neue Objekt erfordert
eine neue Zuteilung und muss später wieder freigegeben werden. Garbage Collection nimmt Zeit in
Anspruch.
Minimieren des Kopierens von Werten.
Als Werttypen fungierende Variablen enthalten ihren Wert direkt. Der Wert wird in der Regel kopiert,
wenn er an eine Methode übergeben oder von einer Methode zurückgegeben wird. Dieses Verhalten
schließt das Kopieren des Werts von this beim Aufruf von Iteratoren und asynchronen
Instanzmethoden von Strukturen ein. Der Kopiervorgang nimmt abhängig von der Größe des Typs Zeit in
Anspruch.
In diesem Artikel wird im Folgenden beispielhaft eine Struktur für einen Punkt im dreidimensionalen Raum
verwendet, um die Empfehlungen zu erläutern:

public struct Point3D


{
public double X;
public double Y;
public double Z;
}

In verschiedenen Beispielen werden unterschiedliche Implementierungen vorgestellt.

Deklarieren unveränderlicher Strukturen als readonly


Deklarieren Sie eine readonly struct , um anzugeben, dass ein Typ unveränderlich ist. Mit dem Modifizierer
readonly wird dem Compiler signalisiert, dass ein unveränderlicher Typ erstellt werden soll. Der Compiler
erzwingt diese Entwurfsentscheidung mit den folgenden Regeln:
Alle Feldmember müssen schreibgeschützt sein.
Alle Eigenschaften, also auch automatisch implementierte, müssen schreibgeschützt sein.
Mit diesen beiden Regeln wird sichergestellt, dass kein Member von readonly struct den Zustand dieser
Struktur verändert. Der struct -Typ ist unveränderlich. Die Point3D -Struktur kann wie im folgenden Beispiel
gezeigt als unveränderlich definiert werden:

readonly public struct ReadonlyPoint3D


{
public ReadonlyPoint3D(double x, double y, double z)
{
this.X = x;
this.Y = y;
this.Z = z;
}

public double X { get; }


public double Y { get; }
public double Z { get; }
}

Sie sollten sich immer dann an diese Empfehlung halten, wenn die Entwurfsabsicht darin besteht, einen
unveränderlichen Werttyp zu erstellen. Ein weiterer Vorteil ergibt sich aus eventuellen
Leistungsverbesserungen. Die Schlüsselwörter readonly struct drücken Ihre Entwurfsabsicht
unmissverständlich aus.

Deklarieren von readonly -Membern für veränderbare Strukturen


Wenn ein Strukturtyp in C# 8.0 und höher veränderbar ist, deklarieren Sie Member, die den Zustand nicht als
readonly -Member ändern.

Erwägen Sie eine andere Anwendung, die eine Struktur für einen Punkt im dreidimensionalen Raum erfordert,
aber Veränderlichkeit unterstützt. Die folgende Version einer solchen Struktur fügt den readonly -Modifizierer
nur den Membern hinzu, die die Struktur nicht ändern. Folgen Sie diesem Beispiel, wenn Ihr Entwurf
Änderungen an der Struktur durch einige Member unterstützen muss, Sie aber dennoch die Vorteile der
Erzwingung von readonly für andere Member nutzen möchten:
public struct Point3D
{
public Point3D(double x, double y, double z)
{
_x = x;
_y = y;
_z = z;
}

private double _x;


public double X
{
readonly get => _x;
set => _x = value;
}

private double _y;


public double Y
{
readonly get => _y;
set => _y = value;
}

private double _z;


public double Z
{
readonly get => _z;
set => _z = value;
}

public readonly double Distance => Math.Sqrt(X * X + Y * Y + Z * Z);

public readonly override string ToString() => $"{X}, {Y}, {Z}";


}

Das vorhergehende Beispiel zeigt viele der Stellen, an denen Sie den readonly -Modifizierer anwenden können:
Methoden, Eigenschaften und Eigenschaftenaccessoren. Wenn Sie automatisch implementierte Eigenschaften
verwenden, fügt der Compiler dem get -Zugriff den readonly -Modifizierer für Lese- und Schreibeigenschaften
hinzu. Der Compiler fügt den readonly -Modifizierer zu den automatisch implementierten
Eigenschaftendeklarationen für Eigenschaften mit nur einem get -Zugriff hinzu.
Das Hinzufügen des readonly -Modifizierers zu Membern, die den Zustand nicht verändern, bietet zwei damit
verbundene Vorteile: Der erste Vorteil ist, dass Sie mithilfe des Compilers die Umsetzung Ihrer Absicht
erzwingen können. Dieser Member kann den Zustand der Struktur nicht ändern. Der zweite Vorteil ist, dass der
Compiler beim Zugriff auf einen readonly -Member keine defensiven Kopien von in -Parametern erstellt. Der
Compiler kann diese Optimierung sicher durchführen, da er garantiert, dass der struct -Member nicht von
einem readonly -Member verändert wird.

Verwenden von ref readonly return -Anweisungen


Verwenden Sie eine Rückgabe des Typs ref readonly , wenn die beiden folgenden Bedingungen erfüllt sind:
Der Rückgabewert ist struct größer als IntPtr.Size.
Die Speicherlebensdauer ist größer als die Methode, die den Wert zurückgibt.
Sie können Werte als Verweis zurückgeben, wenn diese in der Rückgabemethode nicht lokal verwendet werden.
Bei dieser Vorgehensweise wird nur der Verweis und nicht die Struktur kopiert. Im folgenden Beispiel kann für
die Origin -Eigenschaft kein ref -Verweisrückgabewert verwendet werden, da der Rückgabewert eine lokale
Variable ist:
public Point3D Origin => new Point3D(0,0,0);

Die folgende Eigenschaftendefinition kann hingegen durchaus als Verweis zurückgegeben werden, weil der
Rückgabewert ein statischer Member ist:

public struct Point3D


{
private static Point3D origin = new Point3D(0,0,0);

// Dangerous! returning a mutable reference to internal storage


public ref Point3D Origin => ref origin;

// other members removed for space


}

Damit aufrufende Funktionen nicht den Ursprung verändern, sollten Sie den Wert als ref readonly
zurückgeben:

public struct Point3D


{
private static Point3D origin = new Point3D(0,0,0);

public static ref readonly Point3D Origin => ref origin;

// other members removed for space


}

Durch die Rückgabe von ref readonly vermeiden Sie, dass größere Strukturen kopiert werden. Zusätzlich wird
die Unveränderlichkeit interner Datenmember beibehalten.
An der Aufrufstelle können die aufrufenden Funktionen die Origin -Eigenschaft entweder als ref readonly
oder als Wert verwenden:

var originValue = Point3D.Origin;


ref readonly var originReference = ref Point3D.Origin;

Die erste Zuweisung im vorhergehenden Code erstellt eine Kopie der Konstanten Origin und weist diese Kopie
zu. Die zweite weist einen Verweis zu. Beachten Sie, dass der Modifizierer readonly Teil der Variablendeklaration
sein muss. Der referenzierte Verweis kann nicht geändert werden. Derartige Versuche führen zu einem
Kompilierzeitfehler.
Der readonly -Modifizierer ist für die Deklaration von originReference erforderlich.
Der Compiler erzwingt, dass die aufrufende Funktion den Verweis nicht ändern kann. Versuche einer direkten
Wertzuweisung führen zu einem Kompilierzeitfehler. In anderen Fällen weist der Compiler eine defensive Kopie
zu, es sei denn, er kann den schreibgeschützten Verweis sicher verwenden. Durch statische Analyseregeln wird
bestimmt, ob die Struktur geändert werden kann. Der Compiler erstellt keine defensive Kopie, wenn die Struktur
ein readonly struct oder der Member ein readonly der Struktur ist. Defensive Kopien sind nicht erforderlich,
um die Struktur als in -Argument zu übergeben.

Verwenden des Parametermodifizierers in


In den folgenden Abschnitten wird erklärt, was der Modifizierer in bewirkt, wie er zu verwenden ist und wann
er zur Leistungsoptimierung eingesetzt werden sollte:
Die Schlüsselwörter out , ref und in
Verwenden von in -Parametern für große Strukturen
Optionale Verwendung von in an der Aufrufstelle
Vermeiden defensiver Kopien
Die Schlüsselwörter out , ref und in

Das Schlüsselwort in ergänzt die Schlüsselwörter ref und out , um Argumente per Verweis zu übergeben.
Durch das Schlüsselwort in wird angegeben, dass das Argument per Verweis übergeben wird, die aufgerufene
Methode aber nicht den Wert ändert. Der Modifizierer in kann auf beliebige Member angewendet werden, die
Parameter akzeptieren: Methoden, Delegaten, Lambdas, lokale Funktionen, Indexer und Operatoren.
Mit Hinzufügung des Schlüsselworts in bietet C# ein vollständiges Vokabular zum Ausdrücken Ihrer
Entwurfsabsicht. Werttypen werden bei der Übergabe an eine aufgerufene Methode kopiert, wenn Sie keinen
der folgenden Modifizierer in den Methodensignaturen angeben. Jeder dieser Modifizierer legt fest, dass eine
Variable als Verweis übergeben wird, wodurch ein Kopieren vermieden wird. Jeder Modifizierer drückt eine
andere Absicht aus:
out : Diese Methode legt den Wert des Arguments fest, das als dieser Parameter verwendet wird.
ref : Diese Methode kann den Wert des Arguments ändern, das als dieser Parameter verwendet wird.
in : Diese Methode ändert den Wert des Arguments nicht, das als dieser Parameter verwendet wird.

Wenn Sie den Modifizierer in zur Übergabe eines Arguments als Verweis hinzufügen, legen Sie als
Entwurfsabsicht fest, dass Argumente als Verweis übergeben werden sollen, um unnötiges Kopieren zu
vermeiden. Das als Argument verwendete Objekt soll nicht geändert werden.
Der Modifizierer in ergänzt out und ref auch in anderer Weise. Sie können keine überladenen Methoden
erstellen, die sich nur durch das Vorhandensein von in , out oder ref unterscheiden. Diese neuen Regeln
erweitern dasselbe Verhalten, das stets für out - und ref -Parameter definiert wurde. Für Werttypen kann
ebenso wie für die Modifizierer out und ref kein Boxing durchgeführt werden, da der in -Modifizierer
angewendet wird. Von in -Parametern wird ein weiteres Feature bereitgestellt, durch das Sie Literalwerte oder
Konstanten als Argument für einen in -Parameter verwenden können.
Der Modifizierer in kann auch mit Verweistypen oder numerischen Werten verwendet werden. Allerdings ist
der Nutzen in diesen Fällen, wenn überhaupt, nur minimal.
Der Compiler kann auf verschiedene Weise den Schreibschutz eines in -Arguments erzwingen. Zunächst kann
die aufgerufene Methode nicht direkt einem in -Parameter zugewiesen werden. Eine direkte Zuweisung zu
einem Feld eines in -Parameters ist nicht möglich, wenn dieser Wert vom Typ struct ist. Außerdem können
Sie keinen in -Parameter an eine Methode übergeben, die den Modifizierer ref oder out verwendet. Diese
Regeln gelten für jedes Feld eines in -Parameters, falls das Feld vom Typ struct und auch der Parameter vom
Typ struct ist. Tatsächlich gelten diese Regeln für mehrere Memberzugriffsebenen, falls die Typen auf allen
Ebenen structs sind. Der Compiler erzwingt, dass struct -Typen als in -Argumente übergeben werden und
deren struct -Member schreibgeschützte Variablen sind, wenn sie als Argumente an andere Methoden
übergeben werden.
Verwenden von in -Parametern für große Strukturen
Sie können den Modifizierer in auf jeden readonly struct -Parameter anwenden, aber durch diese
Vorgehensweise wird die Leistung wahrscheinlich nur bei Werttypen verbessert, die wesentlich größer als
IntPtr.Size sind. Bei einfachen Typen (wie sbyte , byte , short , ushort , int , uint , long , ulong , char ,
float , double , decimal und bool sowie enum ) fallen potenzielle Leistungssteigerungen hingegen minimal
aus. Einige einfache Typen, wie z. B. decimal mit der Größe 16 Byte, sind größer als 4-Byte- oder 8-Byte-
Verweise, aber nicht so groß, dass in den meisten Szenarien ein messbarer Leistungsunterschied zu verzeichnen
ist. Wenn für Typen, die kleiner als IntPtr.Size sind, Werte als Verweis übergeben werden, kann dies zu
Leistungseinbußen führen.
Der folgende Code zeigt als Beispiel eine Methode, die den Abstand zwischen Punkten in einem 3D-Raum
berechnet.

private static double CalculateDistance(in Point3D point1, in Point3D point2)


{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;

return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);


}

Die Argumente sind zwei Strukturen, die jeweils drei double-Werte enthalten. Ein double-Wert ist 8 Byte groß,
also umfasst jedes Argument 24 Byte. Durch Angabe des Modifizierers in übergeben Sie – abhängig von der
Architektur des Computers – einen 4-Byte- oder 8-Byte-Verweis an diese Argumente. Die Differenz ist gering,
kann sich aber summieren, wenn Ihre Anwendung diese Methode in einer kurzen Schleife mit vielen
unterschiedlichen Werten aufruft.
Die Auswirkungen von Optimierungen auf untergeordneter Ebene, wie z. B. die Verwendung des Modifizierers
in , sollten jedoch gemessen werden, um einen Leistungsvorteil zu bestätigen. Sie könnten zum Beispiel
meinen, dass die Verwendung von in bei einem Guid-Parameter von Vorteil wäre. Der Typ Guid hat eine
Größe von 16 Byte und ist damit doppelt so groß wie ein 8-Byte-Verweis. Aber eine solch geringfügige Differenz
wird wahrscheinlich nicht zu einem messbaren Leistungsvorteil führen, es sei denn, es handelt sich um eine
Methode, die sich in einem zeitkritischen langsamsten Pfad für Ihre Anwendung befindet.
Optionale Verwendung von in an der Aufrufstelle
Im Gegensatz zu einem ref - oder out -Parameter müssen Sie den Modifizierer in nicht an der Aufrufstelle
anwenden. Der folgende Code zeigt zwei Beispiele zum Aufrufen der CalculateDistance -Methode. Im ersten
Beispiel werden zwei lokale Variablen als Verweis übergeben. Im zweiten Beispiel wird als Teil des
Methodenaufrufs eine temporäre Variable erstellt.

var distance = CalculateDistance(pt1, pt2);


var fromOrigin = CalculateDistance(pt1, new Point3D());

Wenn Sie den in -Modifizierer an der Aufrufstelle weglassen, wird dem Compiler mitgeteilt, dass es aus einem
der folgenden Gründen zulässig ist, eine Kopie des Arguments zu erstellen:
Ein Argumenttyp wird implizit in einen Parametertyp konvertiert; eine Identitätskonvertierung findet jedoch
nicht statt.
Das Argument ist ein Ausdruck, verfügt jedoch nicht über eine bekannte Speichervariable.
Eine überladene Methode unterscheidet sich nur durch die (fehlende) Angabe von in . In diesem Fall wird
die überladene Methode verwendet, für die Argumente als Wert übergeben werden.
Diese Regeln sind nützlich, wenn Sie vorhandenen Code anpassen und schreibgeschützte Verweisargumente
verwenden. In der aufgerufenen Methode können Sie alle Instanzmethoden aufrufen, die als Wert übergebene
Parameter verwenden. In diesen Instanzen wird eine Kopie des in -Parameters erstellt.
Da der Compiler eine temporäre Variable für jeden in -Parameter erstellen kann, können Sie auch
Standardwerte für jeden in -Parameter angeben. Im folgenden Codebeispiel wird der Ursprung (Punkt 0,0,0)
als Standardwert für den zweiten Punkt verwendet:
private static double CalculateDistance2(in Point3D point1, in Point3D point2 = default)
{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;

return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);


}

Wenn der Compiler dazu gezwungen werden soll, schreibgeschützte Argumente als Verweis zu übergeben,
müssen Sie wie im folgenden Beispiel gezeigt den in -Modifizierer für Argumente an der Aufrufstelle angeben:

distance = CalculateDistance(in pt1, in pt2);


distance = CalculateDistance(in pt1, new Point3D());
distance = CalculateDistance(pt1, in Point3D.Origin);

Durch dieses Verhalten können in -Parameter leichter über einen gewissen Zeitraum in große Codebasen, in
denen Leistungssteigerungen möglich sind, eingeführt werden. Zuerst fügen Sie den in -Modifizierer den
Methodensignaturen hinzu. Anschließend ergänzen Sie die Aufrufstellen um den in -Modifizierer und erstellen
readonly struct -Typen, damit der Compiler keine defensive Kopien von in -Parametern an mehreren
Speicherorten erstellt.
Vermeiden defensiver Kopien
Übergeben Sie struct als Argument für einen in -Parameter nur dann, wenn dieser mit dem Modifizierer
readonly deklariert ist oder die Methode nur auf readonly -Member der Struktur zugreift. Andernfalls muss der
Compiler in vielen Situationen defensive Kopien erstellen, um sicherzustellen, dass Argumente nicht mutiert
werden. Betrachten Sie das folgende Beispiel, in dem im dreidimensionalen Raum der Abstand eines Punkts
vom Ursprung berechnet wird:

private static double CalculateDistance(in Point3D point1, in Point3D point2)


{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;

return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);


}

Die Point3D -Struktur ist keine schreibgeschützte Struktur. Im Methodenkörper werden sechs Aufrufe zum
Zugriff auf die Eigenschaften ausgeführt. Auf den ersten Blick mögen Sie denken, dass diese Zugriffe sicher sind.
Die get -Zugriffsmethode sollte schließlich nicht den Zustand eines Objekts ändern. Dies wird jedoch von
keiner Programmiersprachregel erzwungen. Es handelt sich nur um eine Konvention. Jeder Typ könnte eine
get -Zugriffsmethode implementieren, die den internen Zustand ändert.

Ohne eine Garantie der Programmiersprache muss der Compiler eine temporäre Kopie des Arguments
erstellen, bevor er einen Member aufruft, der nicht mit dem readonly -Modifizierer gekennzeichnet ist. Der
temporäre Speicher wird auf dem Stapel erstellt, die Werte des Arguments werden in den temporären Speicher
kopiert, und der Wert wird bei jedem Memberzugriff als this -Argument in den Stapel kopiert. In vielen
Situationen beeinträchtigen die Kopien die Leistung so, dass die Übergabe als Wert schneller ist als die
Übergabe als schreibgeschützter Verweis, wenn der Argumenttyp nicht readonly struct lautet und die
Methode Member aufruft, die nicht als readonly markiert sind. Wenn Sie alle Methoden als readonly
markieren, die keine Änderung am Strukturstatus durchführen, kann der Compiler sicher bestimmen, dass der
Strukturzustand nicht geändert wird und eine defensive Kopie nicht erforderlich ist.
Wenn zur Abstandsberechnung die unveränderliche Struktur ReadonlyPoint3D verwendet wird, werden
temporäre Objekte nicht benötigt:

private static double CalculateDistance3(in ReadonlyPoint3D point1, in ReadonlyPoint3D point2 = default)


{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;

return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);


}

Der Compiler generiert effizienteren Code, wenn Member des Typs readonly struct aufgerufen werden: Im
Gegensatz zur Kopie des Empfängers ist der this -Verweis immer ein in -Parameter, der als Verweis der
Membermethode übergeben wird. Durch diese Optimierung werden Kopien vermieden, wenn Sie
readonly struct als in -Argument verwenden.

Übergeben Sie als in -Argument keinen Nullwerte zulassenden Werttyp. Der Typ Nullable<T> ist nicht als
schreibgeschützte Struktur deklariert. Dies bedeutet, dass der Compiler defensive Kopien für jedes Nullwerte
zulassende Typargument generieren muss, das einer Methode mit dem in -Modifizierer in der
Parameterdeklaration übergeben wird.
Im Beispielrepository auf GitHub finden Sie ein Beispielprogramm, das die Leistungsunterschiede mithilfe von
BenchmarkDotNet veranschaulicht. Dabei wird die Übergabe einer veränderlichen Struktur als Wert und
Verweis mit der Übergabe einer unveränderlichen Struktur als Wert und Verweis verglichen. Am schnellsten ist
die Übergabe einer unveränderlichen Struktur als Verweis.

Verwenden von ref struct -Typen


Verwenden Sie einen ref struct - oder readonly ref struct -Typ wie Span<T> oder ReadOnlySpan<T>, um
mit Arbeitsspeicherblöcken als Bytesequenz zu arbeiten. Der vom Bereich verwendete Arbeitsspeicher ist auf
einen einzigen Stapelrahmen beschränkt. Durch die Einschränkung kann der Compiler mehrere Optimierungen
vornehmen. Der primäre Beweggrund für dieses Feature waren Span<T> und zugehörige Strukturen. Wenn Sie
die neuen und aktualisierten .NET-APIs verwenden, in denen der Span<T>-Typ verwendet wird, werden Sie
Leistungsverbesserungen erzielen, die diese Optimierungen mit sich bringen.
Das Deklarieren einer Struktur als readonly ref vereint die Vorteile und Einschränkungen der ref struct - und
readonly struct -Deklarationen. Der von der schreibgeschützten Span-Struktur verwendete Speicher ist auf
einen einzelnen Stapelrahmen beschränkt und kann nicht geändert werden.
Möglicherweise haben Sie ähnliche Anforderungen, wenn Sie mit Speicher arbeiten, der mit stackalloc erstellt
wurde, oder wenn Sie Speicher aus Interop-APIs verwenden. Sie können für diese Anforderungen eigene
ref struct -Typen definieren.

Verwenden der Typen nint und nuint


Integer-Typen mit nativer Größe sind 32-Bit-Integers in einem 32-Bit-Prozess oder 64-Bit-Integers in einem 64-
Bit-Prozess. Sie können für Interop-Szenarien, Low-Level-Bibliotheken und zur Optimierung der Leistung in
Szenarien verwendet werden, in denen arithmetischen Operationen mit ganzen Zahlen ausgiebig genutzt
werden.

Zusammenfassung
Durch das Verwenden von Werttypen wird die Anzahl der Speicherbelegungen minimiert:
Speicher für Werttypen wird für lokale Variablen und Methodenargumente vom Stapel zugeteilt.
Speicher für Werttypen, die Member von anderen Objekten sind, wird als Teil dieses Objekts belegt. Eine
separate Speicherbelegung findet nicht statt.
Speicher für Rückgabewerte des Werttyps wird auf dem Stapel belegt.
Für Verweistypen ist in denselben Situationen ein anderes Verhalten zu beobachten:
Speicher für Verweistypen wird für lokale Variablen und Argumente vom Heap zugeteilt. Der Verweis wird
auf dem Stapel gespeichert.
Speicher für Werttypen, die Member von anderen Objekten sind, wird separat belegt. Das enthaltende Objekt
speichert den Verweis.
Speicher für Rückgabewerte des Verweistyps wird auf dem Heap belegt. Der Verweis auf den Speicher wird
auf dem Stapel gespeichert.
Das Minimieren von Speicherbelegungen hat auch Nachteile. Wenn die Größe des struct -Typs die des
Verweises übersteigt, wird mehr Speicher kopiert. Ein Verweis ist in der Regel 64 Bit oder 32 Bit lang und von
der CPU des Zielcomputers abhängig.
Diese Nachteile wirken sich in der Regel nur minimal auf die Leistung aus. Bei größeren Strukturen oder
Sammlungen können jedoch höhere Leistungseinbußen auftreten. In Programmen können die Auswirkungen
bei kurzen Schleifen und langsamen Pfaden gravierend sein.
Diese Erweiterungen von C# wurden für leistungskritische Algorithmen entwickelt, bei denen die Minimierung
von Speicherbelegungen entscheidend ist, um die erforderliche Leistung zu erzielen. Sie werden feststellen, dass
Sie diese Features möglicherweise nicht oft in dem Code verwenden, den Sie schreiben. Die Verbesserungen
wurden jedoch global in .NET umgesetzt. Da immer mehr APIs diese Features nutzen, werden Sie feststellen,
dass sich die Leistung Ihrer Anwendungen verbessert.

Siehe auch
Modifizierer für in-Parameter (C#-Verweis)
ref (C#-Referenz)
Ref-Rückgabetypen und lokale ref-Variablen
Ausdrucksbaumstrukturen
04.11.2021 • 2 minutes to read

Wenn Sie LINQ verwendet haben, verfügen Sie über Erfahrung mit einer umfangreichen Bibliothek, in der die
Func -Typen Teil des API-Satzes sind. (Wenn Sie mit LINQ nicht vertraut sind, sollten Sie zuerst das LINQ-
Tutorial und den Artikel zu Lambdaausdrücken lesen.) Ausdrucksbaumstrukturen bieten eine umfassendere
Interaktion mit den Argumenten, die Funktionen darstellen.
Sie schreiben die Argumente der Funktion in der Regel mithilfe von Lambdaausdrücken, wenn Sie LINQ-
Abfragen erstellen. In einer normalen LINQ-Abfrage werden die Funktionsargumente in einen Delegaten
transformiert, den der Compiler erstellt.
Wenn Sie eine umfangreichere Interaktion haben möchten, müssen Sie Ausdrucksbaumstrukturen verwenden.
Ausdrucksbaumstrukturen stellen Code wie eine Struktur dar, die Sie untersuchen, ändern oder ausführen
können. Diese Tools bieten Ihnen die Möglichkeit, Code während der Laufzeit zu ändern. Sie können Code
schreiben, der ausgeführte Algorithmen untersucht oder neue Funktionen einfügt. In erweiterten Szenarios
können Sie ausgeführte Algorithmen ändern und sogar C#-Ausdrücke für die Ausführung in einer anderen
Umgebung in eine andere Form übersetzen.
Sie haben wahrscheinlich bereits Code geschrieben, der Ausdrucksbaumstrukturen verwendet. LINQ-APIs von
Entity Framework akzeptieren Ausdrucksbaumstrukturen als Argumente für das LINQ-Abfrageausdrucksmuster.
So kann Entity Framework die Abfrage übersetzen, die Sie in C# in SQL geschrieben haben, die in der
Datenbank-Engine ausgeführt wird. Ein weiteres Beispiel ist Moq, was ein beliebtes Mockingframework für .NET
ist.
In den verbleibenden Abschnitten dieses Tutorials wird untersucht, was Ausdrucksbaumstrukturen sind, es
werden die Framework-Klassen untersucht, die Ausdrucksbaumstrukturen unterstützen, und es wird gezeigt, wie
Sie mit Ausdrucksbaumstrukturen arbeiten. Sie erfahren, wie Sie Ausdrucksbaumstrukturen lesen und diese
erstellen, wie Sie geänderte Ausdrucksbaumstrukturen erstellen und wie Sie Code von dargestellten
Ausdrucksbaumstrukturen ausführen. Nach dem Lesen können Sie mit diesen Strukturen umfangreiche
adaptive Algorithmen erstellen.
1. Ausdrucksbaumstrukturen mit Erläuterung
Verstehen der Struktur und der Konzepte hinter Ausdrucksbaumstrukturen.
2. Framework-Typen, die Ausdrucksbaumstrukturen unterstützen
Erfahren Sie mehr über die Strukturen und Klassen, die Ausdrucksbaumstrukturen definieren und
bearbeiten.
3. Ausführen von Ausdrücken
Erfahren Sie, wie Sie eine Ausdrucksbaumstruktur, die als Lambdaausdruck dargestellt wird, in einen
Delegaten konvertieren, und wie Sie den resultierenden Delegaten ausführen.
4. Interpretieren von Ausdrücken
Erfahren Sie, wie Sie Ausdrucksbaumstrukturen durchlaufen und untersuchen, um zu verstehen, welchen
Code die Ausdrucksbaumstruktur darstellt.
5. Erstellen von Ausdrücken
Erfahren Sie, wie Sie die Knoten für eine Ausdrucksbaumstruktur erstellen, und wie Sie
Ausdrucksbaumstrukturen erstellen.
6. Übersetzen von Ausdrücken
Erfahren Sie, wie Sie eine geänderte Kopie einer Ausdrucksbaumstruktur erstellen oder wie Sie eine
Ausdrucksbaumstruktur in ein anderes Format übersetzen.
7. Zusammenfassung
Überprüfen Sie die Informationen zu Ausdrucksbaumstrukturen.
Ausdrucksbaumstrukturen mit Erläuterung
04.11.2021 • 4 minutes to read

Vorheriges – Übersicht
Eine Ausdrucksbaumstruktur ist eine Datenstruktur, die Code darstellt. Sie basiert auf den gleichen Strukturen,
die ein Compiler verwendet, um Code zu analysieren und die kompilierte Ausgabe zu generieren. Wenn Sie
dieses Tutorial lesen, werden Sie feststellen, dass eine Ähnlichkeit zwischen Ausdrucksbaumstrukturen und den
Typen in den Roslyn-APIs vorhanden ist, um Analyzers and CodeFixes (Analyzer und CodeFixes) zu erstellen.
(Analysetools und CodeFixes sind NuGet-Pakete, die statische Analysen für Code durchführen und potenzielle
Korrekturen für einen Entwickler vorschlagen können.) Die Konzepte sind ähnlich, das Endergebnis ist eine
Datenstruktur, die eine sinnvolle Untersuchung des Quellcodes ermöglicht. Ausdrucksbaumstrukturen basieren
jedoch auf einem völlig anderen Satz von Klassen und APIs als die Roslyn-APIs.
Sehen wir uns ein einfaches Beispiel an. Hier ist eine Codezeile:

var sum = 1 + 2;

Würden Sie dies als eine Ausdrucksbaumstruktur analysieren, enthält die Struktur mehrere Knoten. Der äußerste
Knoten ist eine Variablendeklaration-Anweisung mit der Zuordnung ( var sum = 1 + 2; ). Dieser äußerste Knoten
enthält mehrere untergeordnete Knoten: eine Variablendeklaration, ein Zuweisungsoperator und ein Ausdruck,
der die rechte Seite des Gleichheitszeichens darstellt. Dieser Ausdruck wird weiter unterteilt in Ausdrücke, die
den Additionsvorgang und linken und rechten Operanden der Addition darstellen.
Lassen Sie uns die Ausdrücke etwas genauer ansehen, die die rechte Seite neben dem Gleichheitszeichen bilden.
Der Ausdruck ist 1 + 2 . Das ist ein binärer Ausdruck. Genauer gesagt ist es ein binärer Additionsausdruck. Ein
binäre Additionsausdruck verfügt über zwei untergeordnete Elemente, die den linken und rechten Knoten des
Additionsausdrucks darstellen. Hier handelt es sich bei beiden Knoten um konstante Ausdrücke: Der linke
Operand ist der Wert 1 , und der rechte Operand ist der Wert 2 .
Visuell ist die gesamte Anweisung eine Struktur: Sie können beim Stammknoten beginnen und zu jedem Knoten
in der Struktur navigieren, um den Code anzuzeigen, der die Anweisung bildet:
Variablendeklaration-Anweisung mit der Zuordnung ( var sum = 1 + 2; )
Implizite Typdeklaration von Variablen ( var sum )
Implizites var-Schlüsselwort ( var )
Namensdeklaration von Variablen ( sum )
Zuweisungsoperator ( = )
Binärer Additionsausdruck ( 1 + 2 )
Linker Operand ( 1 )
Additionsoperator ( + )
Rechter Operand ( 2 )
Dies mag kompliziert aussehen, aber es ist sehr leistungsfähig. Mit den gleichen Verfahren können Sie
wesentlich kompliziertere Ausdrücke zerlegen. Betrachten Sie diesen Ausdruck:
var finalAnswer = this.SecretSauceFunction(
currentState.createInterimResult(), currentState.createSecondValue(1, 2),
decisionServer.considerFinalOptions("hello")) +
MoreSecretSauce('A', DateTime.Now, true);

Der obige Ausdruck ist auch die Variablendeklaration mit einer Zuordnung. In dieser Instanz ist die rechte Seite
der Zuordnung eine wesentlich kompliziertere Struktur. Ich werde diesen Ausdruck nicht zerlegen, aber
beachten Sie, was die verschiedenen Knoten sein könnten. Es sind Methodenaufrufe vorhanden, die das aktuelle
Objekt als Empfänger verwenden, einen, der einen expliziten this -Empfänger hat und einen, der das nicht hat.
Es sind Methodenaufrufe vorhanden, die andere Empfängerobjekte verwenden. Es sind konstante Argumente
verschiedener Typen vorhanden. Und schließlich gibt es einen binären Additionsoperator. Je nach Rückgabetyp
von SecretSauceFunction() oder MoreSecretSauce() , kann dieser binäre Additionsoperator ein Methodenaufruf
an einen überschriebenen Additionsoperator sein, der einen statischen Methodenaufruf des binären
Additionsoperators, der für eine Klasse definiert ist auflöst.
Trotz dieser spürbaren Komplexität erstellt der obige Ausdruck eine Baumstruktur, die so einfach wie das erste
Beispiel navigiert werden kann. Sie können untergeordnete Knoten durchlaufen lassen, um Endknoten im
Ausdruck zu finden. Übergeordnete Knoten verfügen über Verweise auf ihre untergeordneten Elemente, und
jeder Knoten verfügt über eine Eigenschaft, die beschreibt, welche Art von Knoten es ist.
Die Struktur einer Ausdrucksbaumstruktur ist sehr konsistent. Sobald Sie mit den Grundlagen vertraut sind,
können Sie auch den komplexesten Code verstehen, wenn er als Ausdrucksbaumstruktur dargestellt wird. Die
Eleganz in der Datenstruktur erläutert, wie der C#-Compiler die komplexesten C#-Programme analysieren und
eine korrekte Ausgabe aus diesem komplizierten Quellcode erstellen kann.
Sobald Sie mit der Struktur von Ausdrucksbaumstrukturen vertraut sind, werden Sie feststellen, dass dieses
Wissen es Ihnen ermöglicht, mit mehr und mehr erweiterten Szenarios zu arbeiten. Dies bedeutet eine
unglaubliche Leistung für Ausdrucksbaumstrukturen.
Zusätzlich zum Übersetzen von Algorithmen, die in anderen Umgebungen ausgeführt werden sollen, können
Ausdrucksbaumstrukturen verwendet werden, um das Schreiben von Algorithmen zu erleichtern, der Code
überprüft, bevor Sie ihn ausführen. Sie können eine Methode schreiben, deren Argumente Ausdrücke sind, und
anschließend diese Ausdrücke vor dem Ausführen des Codes überprüfen. Die Ausdrucksbaumstruktur ist eine
vollständige Darstellung des Codes: Sie können Werte von beliebigen Unterausdrücken sehen. Sie können
Methoden- und Eigenschaftsnamen sehen. Sie können den Wert jedes konstanten Ausdrucks sehen. Sie können
auch eine Ausdrucksbaumstruktur in einen ausführbaren Delegaten konvertieren und den Code ausführen.
Mit den APIs für Ausdrucksbaumstrukturen können Sie Strukturen erstellen, die fast jeden gültigen
Codekonstrukt darstellen. Um die Dinge so einfach wie möglich zu halten, können jedoch einige C#-Ausdrücke
nicht in einer Ausdrucksbaumstruktur erstellt werden. Ein Beispiel sind asynchrone Ausdrücke (mithilfe der
async - und await -Schlüsselwörter). Wenn Ihre Bedürfnisse asynchrone Algorithmen erfordern, müssten Sie
die Task -Objekte direkt bearbeiten, anstatt sich auf die Unterstützung des Compilers zu verlassen. Ein weiteres
Beispiel ist die Erstellung von Schleifen. Normalerweise erstellen Sie diese mithilfe der Schleifen for , foreach ,
while oder do . Wie Sie später in dieser Serie sehen werden, unterstützen die APIs für
Ausdrucksbaumstrukturen einen einzelnen Schleifenausdruck mit break - und continue -Ausdrücken, die die
Wiederholung der Schleife steuern.
Eine Sache, die für Sie nicht möglich ist, ist die Änderung einer Ausdrucksbaumstruktur.
Ausdrucksbaumstrukturen sind unveränderliche Datenstrukturen. Wenn Sie eine Ausdrucksbaumstruktur
ändern möchten, müssen Sie eine neue Struktur erstellen, die eine Kopie des Originals ist, aber mit den
gewünschten Änderungen.
Weiter – Framework-Typen, die Ausdrucksbaumstrukturen unterstützen
Framework-Typen, die Ausdrucksbaumstrukturen
unterstützen
04.11.2021 • 2 minutes to read

Vorherige – Ausdrucksbaumstrukturen mit Erläuterung


Es gibt eine umfangreiche Liste von Klassen im .NET Core Framework, die mit Ausdrucksbaumstrukturen
arbeiten. Die vollständige Liste finden Sie unter System.Linq.Expressions. Anstatt die vollständige Liste zu
durchlaufen, sehen wir uns an, wie die Framework-Klassen entwickelt wurden.
Im Sprachentwurf ist ein Ausdruck ein Teil eines Codes, der ausgewertet wird und einen Wert zurückgibt.
Ausdrücke können sehr einfach sein: Der konstante Ausdruck 1 gibt den konstanten Wert 1 zurück. Sie sind
möglicherweise komplizierter: Der Ausdruck (-B + Math.Sqrt(B*B - 4 * A * C)) / (2 * A) gibt ein
Stammverzeichnis für eine quadratische Gleichung zurück (in dem Fall, in dem die Gleichung eine Lösung hat).

Es beginnt alles mit System.Linq.Expression


Eine der Komplexitäten der Arbeit mit Ausdrucksbaumstrukturen ist, dass viele verschiedene Arten von
Ausdrücken an vielen Stellen in Programmen gültig sind. Betrachten Sie einen Zuweisungsausdruck. Die rechte
Seite einer Zuweisung kann ein konstanter Wert, eine Variable, ein Methodenaufrufausdruck oder anderes sein.
Die Sprachflexibilität bedeutet, dass Sie viele unterschiedliche Ausdruckstypen an beliebigen Stellen in den
Knoten einer Struktur erhalten können, wenn Sie eine Ausdrucksbaumstruktur durchlaufen. Wenn Sie mit dem
Typ des Ausdrucks arbeiten können, ist dies daher der einfachste Weg zum Arbeiten. Allerdings müssen Sie
manchmal mehr wissen. Die Basisausdrucksklasse enthält eine NodeType -Eigenschaft für diesen Zweck. Es gibt
einen ExpressionType zurück, der eine Enumeration möglicher Ausdruckstypen ist. Wenn Sie den Typ des
Knotens kennen, können Sie es in diesen Typ umwandeln, und bestimmte Aktionen, die den Typ des
Ausdrucksknotens kennen, durchführen. Sie können nach bestimmten Knoten suchen, und anschließend mit den
bestimmten Eigenschaften dieser Art von Ausdruck arbeiten.
Dieser Code wird z.B. den Namen einer Variablen für einen Variablenzugriff des Ausdrucks ausgeben. Ich habe
den Knotentyp überprüft, ihn anschließend in einen Variablenzugriff des Ausdrucks umgewandelt und die
Eigenschaften des bestimmten Ausdruckstyps überprüft:

Expression<Func<int, int>> addFive = (num) => num + 5;

if (addFive.NodeType == ExpressionType.Lambda)
{
var lambdaExp = (LambdaExpression)addFive;

var parameter = lambdaExp.Parameters.First();

Console.WriteLine(parameter.Name);
Console.WriteLine(parameter.Type);
}

Erstellen von Ausdrucksbaumstrukturen


Die System.Linq.Expression -Klasse enthält außerdem viele statische Methoden zum Erstellen von Ausdrücken.
Diese Methoden erstellen mithilfe der Argumente für seine untergeordneten Elemente einen Ausdrucksknoten.
Auf diese Weise erstellen Sie einen Ausdruck über seine Endknoten. Dieser Code erstellt z.B. einen Add-
Ausdruck:
// Addition is an add expression for "1 + 2"
var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));
var addition = Expression.Add(one, two);

Sie können in diesem einfachen Beispiel erkennen, dass viele Typen bei der Erstellung und beim Arbeiten mit
Ausdrucksbaumstrukturen beteiligt sind. Diese Komplexität ist erforderlich, um die Funktionalität des rich-
Vokabulars der C#-Sprache bereitzustellen.

Navigieren in den APIs


Es gibt Ausdrucksknotentypen, die fast alle Elemente der Syntax der C#-Sprache zuordnen. Jeder Typ verfügt
über spezielle Methoden für diese Art von Sprachelement. Es ist viel, was Sie sich gleichzeitig merken müssen.
Anstatt sich alles zu merken, finden Sie hier die Techniken, die ich zum Arbeiten mit Ausdrucksbaumstrukturen
verwende:
1. Betrachten Sie die Member des ExpressionType -enum, um mögliche Knoten zu bestimmen, die Sie
untersuchen sollten. Dies ist wirklich hilfreich, wenn Sie eine Ausdrucksbaumstruktur durchlaufen und
verstehen möchten.
2. Betrachten Sie die statischen Member der Expression -Klasse, um einen Ausdruck zu erstellen. Diese
Methoden können jeden Ausdruckstyp aus einer Reihe von untergeordneten Knoten erstellen.
3. Sehen Sie sich die ExpressionVisitor -Klasse an, um eine geänderte Ausdrucksbaumstruktur zu erstellen.
Sie werden mehr finden, wenn Sie sich jeden dieser drei Bereiche ansehen. Unweigerlich, werden Sie feststellen,
was Sie benötigen, wenn Sie mit einem dieser drei Schritte starten.
Weiter -- Ausführen von Ausdrucksbaumstrukturen
Ausführen von Ausdrucksbaumstrukturen
04.11.2021 • 5 minutes to read

Vorheriges – Framework-Typen, die Ausdrucksbaumstrukturen unterstützen


Eine Ausdrucksbaumstruktur ist eine Datenstruktur, die Code darstellt. Es ist ein nicht kompilierter und
ausführbarer Code. Wenn Sie den .NET-Code ausführen möchten, der durch eine Ausdrucksbaumstruktur
dargestellt wird, müssen Sie ihn in ausführbare IL-Anweisungen konvertieren.

Lambdaausdrücke zu Funktionen
Sie können jede LambdaExpression oder jeden Typ, der von LambdaExpression in eine ausführbare IL abgeleitet
wurde, konvertieren. Andere Ausdruckstypen können nicht direkt in Code konvertiert werden. Diese
Einschränkung wirkt sich kaum auf die Praxis aus. Lambdaausdrücke sind die einzigen Typen von Ausdrücken,
die Sie ausführen möchten, indem Sie sie in eine ausführbare Zwischensprache (Intermediate Language, IL)
konvertieren. (Denken Sie darüber nach, was es bedeuten würde, eine ConstantExpression direkt auszuführen.
Wäre das sinnvoll?) Jede beliebige Ausdrucksbaumstruktur, die eine LambdaExpression oder ein von
LambdaExpression abgeleiteter Typ ist, kann in eine IL konvertiert werden. Der Ausdruckstyp
Expression<TDelegate> ist das einzige konkrete Beispiel in den .NET Core-Bibliotheken. Hiermit wird ein
Ausdruck dargestellt, der jedem Delegattyp zugeordnet ist. Da dieser Typ einem Delegattyp zugeordnet ist, kann
.NET den Ausdruck untersuchen, und die IL für einen entsprechenden Delegaten generieren, der der Signatur
des Lambdaausdrucks entspricht.
In den meisten Fällen erstellt dies eine einfache Zuordnung zwischen einem Ausdruck und seinem
entsprechenden Delegaten. Angenommen, eine Ausdrucksbaumstruktur, die durch Expression<Func<int>>
dargestellt wird, wird in einen Delegaten des Typs Func<int> konvertiert. Für einen Lambdaausdruck mit
beliebigem Rückgabetyp und Argumentliste besteht ein Delegattyp, der der Zieltyp für den ausführbaren Code
ist, der von diesem Lambdaausdruck dargestellt wird.
Der LambdaExpression -Typ enthält Compile - und CompileToMethod -Member, die Sie verwenden würden, um
eine Ausdrucksbaumstruktur in ausführbaren Code zu konvertieren. Die Compile -Methode erstellt einen
Delegaten. Die CompileToMethod -Methode aktualisiert ein MethodBuilder -Objekt mit der IL, das die kompilierte
Ausgabe der Ausdrucksbaumstruktur darstellt. Beachten Sie, dass CompileToMethod nur im Desktop-Framework
verfügbar ist, nicht in .NET Core.
Optional können Sie auch einen DebugInfoGenerator angeben, der das Symbol „Debuginformationen“ für das
generierte Delegatobjekt empfängt. Dies ermöglicht es Ihnen, die Ausdrucksbaumstruktur in ein Delegatobjekt
zu konvertieren, und über vollständige Debuginformationen über die generierten Delegate zu verfügen.
Sie würden einen Ausdruck mithilfe des folgenden Code in einen Delegaten konvertieren:

Expression<Func<int>> add = () => 1 + 2;


var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);

Beachten Sie, dass der Delegattyp auf den Ausdruckstyp basiert. Sie müssen den Rückgabetyp und die
Argumentliste kennen, wenn Sie das Delegatobjekt mit strikter Typzuordnung verwenden möchten. Die
LambdaExpression.Compile() -Methode gibt den Delegate -Typ zurück. Sie müssen es in den richtigen Delegattyp
umwandeln, um Kompilierzeittools die Argumentliste oder den Rückgabetyp überprüfen zu lassen.
Ausführung und Lebensdauer
Sie führen den Code durch Aufrufen des Delegaten aus, den Sie beim Aufrufen von LambdaExpression.Compile()
erstellt haben. Dies sehen Sie oben, wo add.Compile() einen Delegaten zurückgibt. Rufen Sie diesen Delegaten
durch Aufrufen von func() aus, der den Code ausführt.
Dieser Delegat stellt den Code in der Ausdrucksbaumstruktur dar. Sie können das Handle für diesen Delegaten
beibehalten und es später aufrufen. Sie müssen die Ausdrucksbaumstruktur nicht jedes Mal kompilieren, wenn
Sie den Code ausführen möchten, den sie darstellt. (Beachten Sie, dass Ausdrucksbaumstrukturen
unveränderlich sind und dass das spätere Kompilieren der gleichen Ausdrucksbaumstruktur einen Delegaten
erstellt, der den gleichen Code ausführt.)
Ich rate Ihnen davon ab, zu versuchen, mehr anspruchsvollere Zwischenspeicherungsmechanismen zu erstellen,
um die Leistungsfähigkeit durch Vermeiden unnötiger Kompilieraufrufe zu erhöhen. Das Vergleichen von zwei
beliebigen Ausdrucksbaumstrukturen um festzustellen, ob sie den gleichen Algorithmus darstellen, wird auch
zum Ausführen sehr zeitaufwändig sein. Sie werden wahrscheinlich feststellen, dass die Rechenzeit, die Sie
durch Vermeiden zusätzlicher Aufrufe von LambdaExpression.Compile() sichern, durch die Zeit für das Ausführen
von Code, der zwei verschiedene Ausdrucksbaumstrukturen bestimmt, die zum gleichen ausführbaren Code
führen, mehr als verbraucht sein wird.

Vorbehalte
Das Kompilieren eines Lambdaausdrucks in einen Delegaten und das Aufrufen dieses Delegaten, ist einer der
einfachsten Vorgänge, die Sie mit einer Ausdrucksbaumstruktur ausführen können. Trotz dieses einfachen
Vorgangs gibt es jedoch Hinweise, die Sie beachten müssen.
Lambdaausdrücke erstellen Closures über lokale Variablen, auf die im Ausdruck verwiesen wird. Sie müssen
sicherstellen, dass alle Variablen, die Teil des Delegaten wären, am Speicherort verwendet werden können, wo
Sie Compile aufrufen sowie beim Ausführen des resultierenden Delegats.
Im Allgemeinen stellt der Compiler sicher, dass dies der Fall ist. Wenn jedoch der Ausdruck auf eine Variable
zugreift, die IDisposable implementiert, ist es möglich, dass der Code das Objekt löschen kann, während es
weiterhin in der Ausdrucksbaumstruktur aufrechterhalten wird.
Angenommen, dieser Code funktioniert gut, da int nicht IDisposable implementiert:

private static Func<int, int> CreateBoundFunc()


{
var constant = 5; // constant is captured by the expression tree
Expression<Func<int, int>> expression = (b) => constant + b;
var rVal = expression.Compile();
return rVal;
}

Der Delegat hat einen Verweis auf die lokale Variable constant erfasst. Auf diese Variable wird später
zugegriffen, wenn die von CreateBoundFunc zurückgegebene Funktion ausgeführt wird.
Jedoch sollten Sie diese (ausgedachte) Klasse beachten, die IDisposable implementiert:
public class Resource : IDisposable
{
private bool isDisposed = false;
public int Argument
{
get
{
if (!isDisposed)
return 5;
else throw new ObjectDisposedException("Resource");
}
}

public void Dispose()


{
isDisposed = true;
}
}

Wenn Sie es in einem Ausdruck verwenden, wie unten dargestellt, erhalten Sie eine ObjectDisposedException
beim Ausführen von Code, auf den die Resource.Argument -Eigenschaft verweist:

private static Func<int, int> CreateBoundResource()


{
using (var constant = new Resource()) // constant is captured by the expression tree
{
Expression<Func<int, int>> expression = (b) => constant.Argument + b;
var rVal = expression.Compile();
return rVal;
}
}

Der Delegat, der von dieser Methode zurückgegeben wurde, wurde über das constant -Objekt geschlossen, das
verworfen wurde. (Es wurde verworfen, da es in einer using -Anweisung deklariert wurde.)
Nun, wenn Sie den von dieser Methode zurückgegebenen Delegaten ausführen, müssen Sie eine
ObjectDisposedException haben, die zum Zeitpunkt der Ausführung ausgelöst wird.

Es scheint sonderbar, einen Laufzeitfehler zu haben, der ein Kompilierzeitkonstrukt darstellt, aber das ist nun mal
gang und gäbe, wenn wir mit Ausdrucksbaumstrukturen arbeiten.
Es gibt viele Permutationen dieses Problem, daher ist es schwierig, eine allgemeine Anleitung zu bieten, um dies
zu vermeiden. Seien Sie mit dem Zugriff auf lokale Variablen beim Definieren von Ausdrücken vorsichtig. Dies
gilt auch beim Zugreifen auf Status im aktuellen Objekt (dargestellt durch this ), wenn Sie eine
Ausdrucksbaumstruktur erstellen, die durch eine öffentliche API zurückgegeben werden kann.
Der Code in einem Ausdruck kann auf Methoden oder Eigenschaften in anderen Assemblys verweisen. Auf diese
Assembly muss zugegriffen werden können, wenn der Ausdruck definiert ist, und wenn sie kompiliert wird und
das resultierende Delegat aufgerufen wird. Sie werden einer ReferencedAssemblyNotFoundException in Fällen
begegnen, in denen es nicht vorhanden ist.

Zusammenfassung
Ausdrucksbaumstrukturen, die Lambdaausdrücke darstellen, können kompiliert werden, um einen Delegaten zu
erstellen, den Sie ausführen können. Dies bietet einen Mechanismus, um den durch eine
Ausdrucksbaumstruktur dargestellten Code auszuführen.
Die Ausdrucksbaumstruktur stellt den Code dar, der für jedes angegebene Konstrukt ausgeführt werden würde,
das Sie erstellen. Solange die Umgebung, in der Sie den Code kompilieren und ausführen, der Umgebung
entspricht, in der Sie den Ausdruck erstellen, funktioniert alles wie erwartet. Wenn dies nicht der Fall ist, sind die
Fehler sehr berechenbar, und sie werden in den ersten Tests von Code mithilfe der Ausdrucksbaumstrukturen
abgefangen werden.
Weiter – Interpretieren von Ausdrücken
Interpretieren von Ausdrücken
04.11.2021 • 14 minutes to read

Vorheriges – Ausführen von Ausdrücken


Lassen Sie uns nun Code schreiben, um die Struktur einer Ausdrucksbaumstruktur zu untersuchen. Jeder Knoten
in einer Ausdrucksbaumstruktur ist ein Objekt einer Klasse, die von Expression abgeleitet ist.
Dieser Entwurf macht den Zugriff auf alle Knoten in einer Ausdrucksbaumstruktur zu einem relativ
unkomplizierten rekursiven Vorgang. Die allgemeine Strategie besteht darin, im Stammknoten zu starten und zu
bestimmen, welche Art von Knoten es ist.
Wenn der Knotentyp untergeordnete Elemente besitzt, greifen Sie rekursiv auf die untergeordneten Elemente
zu. Wiederholen Sie den beim Stammknoten verwendeten Prozess bei jedem untergeordneten Knoten:
Bestimmen Sie den Typ, und wenn er untergeordnete Elemente aufweist, greifen Sie auf jedes der
untergeordneten Elemente zu.

Untersuchen eines Ausdrucks ohne untergeordnete Elemente


Beginnen wir damit, auf jeden Knoten in einer einfachen Ausdrucksbaumstruktur zuzugreifen. Hier ist der Code,
der einen konstanten Ausdruck erstellt und anschließend seine Eigenschaften überprüft:

var constant = Expression.Constant(24, typeof(int));

Console.WriteLine($"This is a/an {constant.NodeType} expression type");


Console.WriteLine($"The type of the constant value is {constant.Type}");
Console.WriteLine($"The value of the constant value is {constant.Value}");

Dadurch wird Folgendes zurückgegeben:

This is an Constant expression type


The type of the constant value is System.Int32
The value of the constant value is 24

Jetzt schreiben wir den Code, der diesen Ausdruck untersuchen und einige wichtige Eigenschaften darüber
schreiben würde. Hier ist dieser Code:

Untersuchen eines einfachen Additionsausdrucks


Beginnen wir mit dem Hinzufügen-Beispiel aus der Einführung zu diesem Abschnitt.

Expression<Func<int>> sum = () => 1 + 2;

Ich verwende kein var , um diese Ausdrucksbaumstruktur zu deklarieren, da es nicht möglich ist, weil die
rechte Seite der Zuweisung implizit typisiert ist.

Der Stammknoten ist ein LambdaExpression . Um interessanten Code auf der rechten Seite des => -Operators zu
erhalten, müssen Sie eines der untergeordneten Elemente des LambdaExpression finden. Wir werden dies mit
allen Ausdrücken in diesem Abschnitt durchführen. Der übergeordnete Knoten hilft uns beim Finden des
Rückgabetyps des LambdaExpression .
Wir müssen rekursiv auf eine Anzahl von Knoten zugreifen, um jeden Knoten in diesem Ausdruck zu
untersuchen. Hier ist eine einfache erste Implementierung:

Expression<Func<int, int, int>> addition = (a, b) => a + b;

Console.WriteLine($"This expression is a {addition.NodeType} expression type");


Console.WriteLine($"The name of the lambda is {((addition.Name == null) ? "<null>" : addition.Name)}");
Console.WriteLine($"The return type is {addition.ReturnType.ToString()}");
Console.WriteLine($"The expression has {addition.Parameters.Count} arguments. They are:");
foreach(var argumentExpression in addition.Parameters)
{
Console.WriteLine($"\tParameter Type: {argumentExpression.Type.ToString()}, Name:
{argumentExpression.Name}");
}

var additionBody = (BinaryExpression)addition.Body;


Console.WriteLine($"The body is a {additionBody.NodeType} expression");
Console.WriteLine($"The left side is a {additionBody.Left.NodeType} expression");
var left = (ParameterExpression)additionBody.Left;
Console.WriteLine($"\tParameter Type: {left.Type.ToString()}, Name: {left.Name}");
Console.WriteLine($"The right side is a {additionBody.Right.NodeType} expression");
var right= (ParameterExpression)additionBody.Right;
Console.WriteLine($"\tParameter Type: {right.Type.ToString()}, Name: {right.Name}");

Dieses Beispiel gibt die folgende Ausgabe zurück:

This expression is a/an Lambda expression type


The name of the lambda is <null>
The return type is System.Int32
The expression has 2 arguments. They are:
Parameter Type: System.Int32, Name: a
Parameter Type: System.Int32, Name: b
The body is a/an Add expression
The left side is a Parameter expression
Parameter Type: System.Int32, Name: a
The right side is a Parameter expression
Parameter Type: System.Int32, Name: b

Sie werden viele Wiederholungen im obigen Codebeispiel sehen. Lassen Sie uns dies bereinigen und einen
Ausdrucksknoten für Besucher für eine allgemeinere Verwendung erstellen. Dafür müssen wir einen rekursiven
Algorithmus schreiben. Jeder Knoten kann ein Typ sein, der möglicherweise untergeordnete Elemente aufweist.
Jeder Knoten mit untergeordneten Elementen erfordert es, dass Sie auf diese untergeordneten Elemente
zugreifen und bestimmen, was dieser Knoten ist. Hier finden Sie die bereinigte Version, die Rekursion
verwendet, um auf die Additionsvorgänge zuzugreifen:

// Base Visitor class:


public abstract class Visitor
{
private readonly Expression node;

protected Visitor(Expression node)


{
this.node = node;
}

public abstract void Visit(string prefix);

public ExpressionType NodeType => this.node.NodeType;


public static Visitor CreateFromExpression(Expression node)
{
switch(node.NodeType)
{
case ExpressionType.Constant:
case ExpressionType.Constant:
return new ConstantVisitor((ConstantExpression)node);
case ExpressionType.Lambda:
return new LambdaVisitor((LambdaExpression)node);
case ExpressionType.Parameter:
return new ParameterVisitor((ParameterExpression)node);
case ExpressionType.Add:
return new BinaryVisitor((BinaryExpression)node);
default:
Console.Error.WriteLine($"Node not processed yet: {node.NodeType}");
return default(Visitor);
}
}
}

// Lambda Visitor
public class LambdaVisitor : Visitor
{
private readonly LambdaExpression node;
public LambdaVisitor(LambdaExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This expression is a {NodeType} expression type");
Console.WriteLine($"{prefix}The name of the lambda is {((node.Name == null) ? "<null>" :
node.Name)}");
Console.WriteLine($"{prefix}The return type is {node.ReturnType.ToString()}");
Console.WriteLine($"{prefix}The expression has {node.Parameters.Count} argument(s). They are:");
// Visit each parameter:
foreach (var argumentExpression in node.Parameters)
{
var argumentVisitor = Visitor.CreateFromExpression(argumentExpression);
argumentVisitor.Visit(prefix + "\t");
}
Console.WriteLine($"{prefix}The expression body is:");
// Visit the body:
var bodyVisitor = Visitor.CreateFromExpression(node.Body);
bodyVisitor.Visit(prefix + "\t");
}
}

// Binary Expression Visitor:


public class BinaryVisitor : Visitor
{
private readonly BinaryExpression node;
public BinaryVisitor(BinaryExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This binary expression is a {NodeType} expression");
var left = Visitor.CreateFromExpression(node.Left);
Console.WriteLine($"{prefix}The Left argument is:");
left.Visit(prefix + "\t");
var right = Visitor.CreateFromExpression(node.Right);
Console.WriteLine($"{prefix}The Right argument is:");
right.Visit(prefix + "\t");
}
}

// Parameter visitor:
public class ParameterVisitor : Visitor
{
private readonly ParameterExpression node;
public ParameterVisitor(ParameterExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This is an {NodeType} expression type");
Console.WriteLine($"{prefix}Type: {node.Type.ToString()}, Name: {node.Name}, ByRef:
{node.IsByRef}");
}
}

// Constant visitor:
public class ConstantVisitor : Visitor
{
private readonly ConstantExpression node;
public ConstantVisitor(ConstantExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This is an {NodeType} expression type");
Console.WriteLine($"{prefix}The type of the constant value is {node.Type}");
Console.WriteLine($"{prefix}The value of the constant value is {node.Value}");
}
}

Dieser Algorithmus ist die Grundlage für einen Algorithmus, der jeden beliebigen LambdaExpression besuchen
kann. Es gibt zahlreiche Löcher, da der Code, den ich erstellt habe, nur nach einem kleinen Teil der möglichen
Sätze von Knoten in der Ausdrucksbaumstruktur sucht, die auftreten können. Allerdings können Sie dennoch
etwas von dem lernen, was er produziert. (Der Standardfall in der Visitor.CreateFromExpression -Methode gibt
eine Meldung an die Fehlerkonsole, wenn ein neuer Knotentyp gefunden wird. Auf diese Weise wissen Sie, dass
Sie einen neuen Ausdruck hinzufügen können.)
Beim Ausführen dieser Besucher auf dem oben gezeigten Additionsausdruck erhalten Sie die folgende Ausgabe:

This expression is a/an Lambda expression type


The name of the lambda is <null>
The return type is System.Int32
The expression has 2 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False
The expression body is:
This binary expression is a Add expression
The Left argument is:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False

Nun, da Sie eine allgemeinere Besucherimplementierung erstellt haben, können Sie auf mehr verschiedene
Ausdruckstypen zugreifen und diese verarbeiten.

Untersuchen eines Additionsausdrucks mit vielen Ebenen


Wir versuchen ein etwas komplizierteres Beispiel, aber trotzdem beschränken wir die Knotentypen auf Addition:
Expression<Func<int>> sum = () => 1 + 2 + 3 + 4;

Bevor Sie dies auf dem Besucheralgorithmus ausführen, versuchen Sie eine Denkübung, um herauszufinden,
was die Ausgabe sein könnte. Beachten Sie, dass der + -Operator ein binärer Operator ist: Er muss über zwei
untergeordnete Elemente verfügen, die die linken und rechten Operanden darstellen. Es gibt mehrere
Möglichkeiten, eine Struktur zu erstellen, die richtig sein könnte:

Expression<Func<int>> sum1 = () => 1 + (2 + (3 + 4));


Expression<Func<int>> sum2 = () => ((1 + 2) + 3) + 4;

Expression<Func<int>> sum3 = () => (1 + 2) + (3 + 4);


Expression<Func<int>> sum4 = () => 1 + ((2 + 3) + 4);
Expression<Func<int>> sum5 = () => (1 + (2 + 3)) + 4;

Sie sehen die Aufteilung in zwei mögliche Antworten, um die vielversprechendste zu markieren. Die erste stellt
rechtsassoziative Ausdrücke dar. Die zweite stellt linksassoziative Ausdrücke dar. Der Vorteil dieser beiden
Formate ist, dass das Format auf jede beliebige Anzahl von Additionsausdrücken skaliert.
Wenn Sie diesen Ausdruck über die Besucher ausführen, sehen Sie diese Ausgabe, die überprüft, ob der einfache
Additionsausdruck linksassoziativ ist.
Um dieses Beispiel auszuführen und die vollständige Ausdrucksbaumstruktur anzuzeigen, musste ich eine
Änderung an der Quelle der Ausdrucksbaumstruktur vornehmen. Wenn die Ausdrucksbaumstruktur alle
Konstanten enthält, enthält die resultierende Struktur einfach den konstanten Wert von 10 . Der Compiler führt
alle Additionen aus und reduziert den Ausdruck auf seine einfachste Form. Das Hinzufügen einer Variablen im
Ausdruck ist ausreichend, um die ursprüngliche Struktur anzuzeigen:

Expression<Func<int, int>> sum = (a) => 1 + a + 3 + 4;

Erstellen Sie einen Besucher für diese Summe, und führen Sie den Besucher aus, für den Sie diese Ausgabe
sehen:
This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 1 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The expression body is:
This binary expression is a Add expression
The Left argument is:
This binary expression is a Add expression
The Left argument is:
This binary expression is a Add expression
The Left argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 1
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The Right argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 3
The Right argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 4

Sie können auch eines der anderen Beispiele über den Besuchercode ausführen und sehen, welche Struktur es
darstellt. Hier ist ein Beispiel für den oben stehenden sum3 -Ausdruck (mit einem zusätzlichen Parameter, um zu
verhindern, dass der Compiler die Konstante berechnet):

Expression<Func<int, int, int>> sum3 = (a, b) => (1 + a) + (3 + b);

Dies ist die Ausgabe vom Besucher:


This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False
The expression body is:
This binary expression is a Add expression
The Left argument is:
This binary expression is a Add expression
The Left argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 1
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The Right argument is:
This binary expression is a Add expression
The Left argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 3
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False

Beachten Sie, dass die Klammern nicht Teil der Ausgabe sind. Es sind keine Knoten in der
Ausdrucksbaumstruktur vorhanden, die die Klammern im eingegebenen Ausdruck darstellen. Die Struktur der
Ausdrucksbaumstruktur enthält alle Informationen, die erforderlich sind, um die Rangfolge zu kommunizieren.

Erweiterungen aus diesem Beispiel


Das Beispiel behandelt nur die elementarsten Ausdrucksbaumstrukturen. Der Code, den Sie in diesem Abschnitt
gesehen haben, behandelt nur konstante ganze Zahlen und den binären + -Operator. Als letztes Beispiel
aktualisieren wir den Besucher, um einen komplizierteren Ausdruck zu behandeln. Lassen Sie uns dafür sorgen,
dass es hierfür funktioniert:

Expression<Func<int, int>> factorial = (n) =>


n == 0 ?
1 :
Enumerable.Range(1, n).Aggregate((product, factor) => product * factor);

Dieser Code stellt eine mögliche Implementierung für die mathematische Fakultät-Funktion dar. So wie ich
diesen Code geschrieben habe, werden zwei Einschränkungen beim Erstellen von Ausdrucksbaumstrukturen
durch die Zuweisung von Lambdaausdrücken an Ausdrücke hervorgehoben. Erstens sind Anweisungslambdas
nicht zulässig. Das bedeutet, ich kann keine Schleifen, Blöcke, if/else-Anweisungen und anderen allgemeinen
Steuerungsstrukturen in C# verwenden. Ich kann nur Ausdrücke verwenden. Zweitens kann ich nicht denselben
Ausdruck rekursiv aufrufen. Ich könnte dies, wenn er bereits ein Delegat wäre, aber ich kann ihn nicht in seiner
Form der Ausdrucksbaumstruktur aufrufen. Im Abschnitt zum Erstellen von Ausdrucksbaumstrukturen werden
Sie Techniken erlernen, um diese Einschränkung zu umgehen.
In diesem Ausdruck können Knoten all dieser Typen auftreten:
1. Gleich (binärer Ausdruck)
2. Multiplizieren (binärer Ausdruck)
3. Bedingt (der ? : Ausdruck)
4. Ausdruck des Methodenaufrufs (Aufrufen von Range() und Aggregate() )

Eine Möglichkeit zum Ändern des Besucheralgorithmus besteht darin, ihn auszuführen, und den Knotentyp jedes
Mal, wenn Sie Ihre default -Klausel erreichen, zu schreiben. Nach einigen Iterationen haben Sie alle möglichen
Knoten gesehen. Dann haben Sie alles, was Sie benötigen. Das Ergebnis würde in etwa wie folgt aussehen:

public static Visitor CreateFromExpression(Expression node)


{
switch(node.NodeType)
{
case ExpressionType.Constant:
return new ConstantVisitor((ConstantExpression)node);
case ExpressionType.Lambda:
return new LambdaVisitor((LambdaExpression)node);
case ExpressionType.Parameter:
return new ParameterVisitor((ParameterExpression)node);
case ExpressionType.Add:
case ExpressionType.Equal:
case ExpressionType.Multiply:
return new BinaryVisitor((BinaryExpression)node);
case ExpressionType.Conditional:
return new ConditionalVisitor((ConditionalExpression)node);
case ExpressionType.Call:
return new MethodCallVisitor((MethodCallExpression)node);
default:
Console.Error.WriteLine($"Node not processed yet: {node.NodeType}");
return default(Visitor);
}
}

ConditionalVisitor und MethodCallVisitor verarbeiten diese zwei Knoten:


public class ConditionalVisitor : Visitor
{
private readonly ConditionalExpression node;
public ConditionalVisitor(ConditionalExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This expression is a {NodeType} expression");
var testVisitor = Visitor.CreateFromExpression(node.Test);
Console.WriteLine($"{prefix}The Test for this expression is:");
testVisitor.Visit(prefix + "\t");
var trueVisitor = Visitor.CreateFromExpression(node.IfTrue);
Console.WriteLine($"{prefix}The True clause for this expression is:");
trueVisitor.Visit(prefix + "\t");
var falseVisitor = Visitor.CreateFromExpression(node.IfFalse);
Console.WriteLine($"{prefix}The False clause for this expression is:");
falseVisitor.Visit(prefix + "\t");
}
}

public class MethodCallVisitor : Visitor


{
private readonly MethodCallExpression node;
public MethodCallVisitor(MethodCallExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This expression is a {NodeType} expression");
if (node.Object == null)
Console.WriteLine($"{prefix}This is a static method call");
else
{
Console.WriteLine($"{prefix}The receiver (this) is:");
var receiverVisitor = Visitor.CreateFromExpression(node.Object);
receiverVisitor.Visit(prefix + "\t");
}

var methodInfo = node.Method;


Console.WriteLine($"{prefix}The method name is {methodInfo.DeclaringType}.{methodInfo.Name}");
// There is more here, like generic arguments, and so on.
Console.WriteLine($"{prefix}The Arguments are:");
foreach(var arg in node.Arguments)
{
var argVisitor = Visitor.CreateFromExpression(arg);
argVisitor.Visit(prefix + "\t");
}
}
}

Und die Ausgabe für die Ausdrucksbaumstruktur wäre:


This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 1 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: n, ByRef: False
The expression body is:
This expression is a Conditional expression
The Test for this expression is:
This binary expression is a Equal expression
The Left argument is:
This is an Parameter expression type
Type: System.Int32, Name: n, ByRef: False
The Right argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 0
The True clause for this expression is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 1
The False clause for this expression is:
This expression is a Call expression
This is a static method call
The method name is System.Linq.Enumerable.Aggregate
The Arguments are:
This expression is a Call expression
This is a static method call
The method name is System.Linq.Enumerable.Range
The Arguments are:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 1
This is an Parameter expression type
Type: System.Int32, Name: n, ByRef: False
This expression is a Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 arguments. They are:
This is an Parameter expression type
Type: System.Int32, Name: product, ByRef: False
This is an Parameter expression type
Type: System.Int32, Name: factor, ByRef: False
The expression body is:
This binary expression is a Multiply expression
The Left argument is:
This is an Parameter expression type
Type: System.Int32, Name: product, ByRef: False
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: factor, ByRef: False

Erweitern Sie die Beispielbibliothek


Die Beispiele in diesem Abschnitt zeigen die Kerntechniken, um Knoten in einer Ausdrucksbaumstruktur zu
besuchen und zu untersuchen. Ich habe viele Aktionen ausgelassen, die Sie möglicherweise benötigen, um sich
auf die wichtigsten Aufgaben beim Zugriff auf die Knoten in einer Ausdrucksbaumstruktur zu konzentrieren.
Erstens behandelt der Besucher nur Konstanten, die ganze Zahlen sind. Konstante Werte können jeder andere
numerische Typ sein, und die C#-Sprache unterstützt Konvertierungen und Werbeaktionen zwischen diesen
Typen. Eine robustere Version dieses Codes würde alle diese Funktionen widerspiegeln.
Sogar das letzte Beispiel erkennt eine Teilmenge der möglichen Knotentypen. Sie können weiterhin viele
Ausdrücke eingeben, die Fehler verursachen werden. Eine vollständige Implementierung ist in .NET Standard
unter dem Namen ExpressionVisitor enthalten und kann alle möglichen Knotentypen verarbeiten.
Schließlich wurde die Bibliothek, die ich in diesem Artikel verwendet habe, für Demo- und Lernzwecke erstellt.
Sie ist nicht optimiert. Ich habe sie geschrieben, um die verwendeten Strukturen klar zu machen und um die
verwendeten Techniken für den Zugriff auf die Knoten hervorzuheben und zu analysieren, was sich dort
befindet. Eine Produktionsimplementierung würde mehr Aufmerksamkeit auf die Leistung legen, als ich es habe.
Selbst mit diesen Einschränkungen sollten Sie sich auf dem richtigen Weg zum Schreiben von Algorithmen
befinden, die Ausdrucksbaumstrukturen lesen und verstehen.
Weiter – Erstellen von Ausdrücken
Erstellen von Ausdrucksbaumstrukturen
04.11.2021 • 5 minutes to read

Vorheriges - Interpretieren von Ausdrücken


Alle Ausdrucksbaumstrukturen, die Sie bisher gesehen haben, wurden vom C#-Compiler erstellt. Sie müssen
einfach einen Lambdaausdruck erstellen, der einer typisierten Variable als Expression<Func<T>> oder als einen
ähnlichen Typ zugewiesen wurde. Das ist nicht die einzige Möglichkeit, eine Ausdrucksbaumstruktur zu erstellen.
Für viele Szenarios stellen Sie möglicherweise fest, dass Sie einen Ausdruck im Arbeitsspeicher zur Laufzeit
erstellen müssen.
Das Erstellen von Ausdrucksbaumstrukturen ist kompliziert, da diese Ausdrucksbaumstrukturen unveränderlich
sind. Unveränderlich bedeutet, dass Sie die Struktur von den Blättern bis zum Stamm erstellen müssen. Die APIs,
die Sie zum Erstellen von Ausdrucksbaumstrukturen verwenden spiegeln diese Tatsache wider: Die Methoden,
die Sie verwenden, um einen Knoten zu erstellen, werden alle ihre untergeordneten Elemente als Argumente
verwenden. Betrachten wir einige Beispiele, die Ihnen die Techniken zeigen.

Erstellen von Knoten


Beginnen wir erneut relativ einfach. Wir verwenden den Additionsausdruck, mit dem ich in diesen Abschnitten
arbeiten werde:

Expression<Func<int>> sum = () => 1 + 2;

Um diese Ausdrucksbaumstruktur zu erstellen, müssen Sie die Endknoten erstellen. Die Endknoten sind
Konstanten, damit Sie die Expression.Constant -Methode verwenden können, um die Knoten zu erstellen:

var one = Expression.Constant(1, typeof(int));


var two = Expression.Constant(2, typeof(int));

Als Nächstes erstellen Sie den Additionsausdruck:

var addition = Expression.Add(one, two);

Sobald Sie den Additionsausdruck haben, können Sie den Lambdaausdruck erstellen:

var lambda = Expression.Lambda(addition);

Dies ist ein sehr einfacher Lambdaausdruck, da er keine Argumente enthält. In diesem Abschnitt erfahren Sie
später, wie Sie Parametern Argumente zuordnen und kompliziertere Ausdrücke erstellen.
Für Ausdrücke, die so einfach wie dieser sind, können Sie alle Aufrufe in einer einzelnen Anweisung
kombinieren:
var lambda = Expression.Lambda(
Expression.Add(
Expression.Constant(1, typeof(int)),
Expression.Constant(2, typeof(int))
)
);

Erstellen einer Struktur


Das sind die Grundlagen der Erstellung einer Ausdrucksbaumstruktur im Arbeitsspeicher. Komplexere
Strukturen bedeuten im Allgemeinen mehr Knotentypen und weitere Knoten in der Struktur. Lassen Sie uns ein
weiteres Beispiel ausführen, und zwei weitere Knotentypen, die Sie in der Regel beim Erstellen von
Ausdrucksbaumstrukturen erstellen, anzeigen: Die Argumentknoten und Methodenaufrufknoten.
Wir erstellen eine Ausdrucksbaumstruktur, um diesen Ausdruck zu erstellen:

Expression<Func<double, double, double>> distanceCalc =


(x, y) => Math.Sqrt(x * x + y * y);

Sie beginnen mit dem Erstellen der Parameterausdrücke für x und y :

var xParameter = Expression.Parameter(typeof(double), "x");


var yParameter = Expression.Parameter(typeof(double), "y");

Das Erstellen der Multiplikations- und Additionsausdrücke folgt dem Muster, das Sie bereits gesehen haben:

var xSquared = Expression.Multiply(xParameter, xParameter);


var ySquared = Expression.Multiply(yParameter, yParameter);
var sum = Expression.Add(xSquared, ySquared);

Anschließend müssen Sie einen Ausdruck des Methodenaufrufs für den Aufruf von Math.Sqrt erstellen.

var sqrtMethod = typeof(Math).GetMethod("Sqrt", new[] { typeof(double) });


var distance = Expression.Call(sqrtMethod, sum);

Und anschließend legen Sie den Methodenaufruf in einen Lambdaausdruck, und stellen sicher, dass Sie die
Argumente für den Lambdaausdruck definieren:

var distanceLambda = Expression.Lambda(


distance,
xParameter,
yParameter);

In diesem komplizierteren Beispiel sehen Sie ein paar weitere Verfahren, die Sie häufig benötigen, um
Ausdrucksbaumstrukturen zu erstellen.
Zunächst müssen Sie die Objekte erstellen, die Parameter oder lokale Variablen darstellen, bevor Sie sie
verwenden. Wenn Sie diese Objekte erstellt haben, können Sie diese in Ihrer Ausdrucksbaumstruktur
verwenden, wo immer Sie sie benötigen.
Zweitens müssen Sie einen Teil der Reflektions-APIs verwenden, um ein MethodInfo -Objekt zu erstellen, sodass
Sie eine Ausdrucksbaumstruktur für den Zugriff auf diese Methode erstellen können. Sie müssen sich auf die
Teilmenge der Reflektions-APIs begrenzen, die auf der .NET Core-Plattform verfügbar sind. In diesem Fall
werden diese Techniken auf andere Ausdrucksbaumstrukturen erweitert.

Erstellen von Code im Detail


Sie sind nicht darin beschränkt, was Sie mithilfe dieser APIs erstellen können. Je komplizierter jedoch die
Ausdrucksbaumstruktur ist, die Sie erstellen möchten, desto schwieriger ist der Code zu verwalten und zu lesen.
Wir erstellen eine Ausdrucksbaumstruktur, die diesem Code entspricht:

Func<int, int> factorialFunc = (n) =>


{
var res = 1;
while (n > 1)
{
res = res * n;
n--;
}
return res;
};

Beachten Sie oben, dass ich nicht die Ausdrucksbaumstruktur, aber einfach den Delegaten erstellt habe. Mithilfe
der Expression -Klasse können Sie keine Anweisungslambdas erstellen. Hier ist der Code, der erforderlich ist,
um die gleiche Funktionalität zu erstellen. Es ist etwas kompliziert, dass es keine API zum Erstellen einer while -
Schleife gibt. Stattdessen müssen Sie eine Schleife, die einen bedingten Test enthält, und ein Bezeichnungsziel
erstellen, um die Schleife zu unterbrechen.

var nArgument = Expression.Parameter(typeof(int), "n");


var result = Expression.Variable(typeof(int), "result");

// Creating a label that represents the return value


LabelTarget label = Expression.Label(typeof(int));

var initializeResult = Expression.Assign(result, Expression.Constant(1));

// This is the inner block that performs the multiplication,


// and decrements the value of 'n'
var block = Expression.Block(
Expression.Assign(result,
Expression.Multiply(result, nArgument)),
Expression.PostDecrementAssign(nArgument)
);

// Creating a method body.


BlockExpression body = Expression.Block(
new[] { result },
initializeResult,
Expression.Loop(
Expression.IfThenElse(
Expression.GreaterThan(nArgument, Expression.Constant(1)),
block,
Expression.Break(label, result)
),
label
)
);

Der Code zum Erstellen der Baumstruktur für die Fakultätsfunktion ist etwas länger, komplizierter, und er ist voll
von Bezeichnungen und Break-Anweisungen und anderen Elemente, die wir in unseren täglichen
Codieraufgaben vermeiden möchten.
Für diesen Abschnitt habe ich auch den Besuchercode aktualisiert, um jeden Knoten in dieser
Ausdrucksbaumstruktur zu finden, und Informationen zu den Knoten, die in diesem Beispiel erstellt wurden, zu
schreiben. Sie können den Beispielcode vom Repository „dotnet/docs“ auf GitHub anzeigen oder herunterladen.
Experimentieren Sie selbst, indem Sie die Beispiele erstellen und ausführen. Anweisungen zum Herunterladen
finden Sie unter Beispiele und Lernprogramme.

Untersuchen der APIs


Die Ausdrucksbaumstruktur-APIs sind einige der Schwierigeren zum Navigieren in .NET Core, aber das ist in
Ordnung. Ihr Zweck ist ein recht komplexes Unterfangen: Schreiben von Code, der Code zur Laufzeit generiert.
Sie sind notwendigerweise kompliziert, um ein Gleichgewicht zwischen der Unterstützung aller verfügbaren
Steuerungsstrukturen in der C#-Sprache bereitzustellen und um die Oberfläche der APIs so klein wie
angemessenen zu halten. Diese Balance bedeutet, dass viele Steuerungsstrukturen nicht über die C#-Konstrukte
dargestellt werden, jedoch über Konstrukte, die die zugrunde liegende Logik darstellen, die der Compiler von
diesen Konstrukten mit höherer Ebene generiert.
Außerdem sind zu diesem Zeitpunkt C#-Ausdrücke vorhanden, die nicht direkt mit Expression -
Klassenmethoden erstellt werden können. Im Allgemeinen werden dies die neuesten Operatoren und Ausdrücke
sein, die in C# 5 und C# 6 hinzugefügt werden. (Z.B. async -Ausdrücke können nicht erstellt werden, und der
neue ?. -Operator kann nicht direkt erstellt werden.)
Weiter– Übersetzen von Ausdrücken
Übersetzen von Ausdrucksbaumstrukturen
04.11.2021 • 6 minutes to read

Vorheriges – Erstellen von Ausdrücken


In diesem letzten Abschnitt erfahren Sie, wie Sie auf jeden Knoten in einer Ausdrucksbaumstruktur zugreifen,
während eine geänderte Kopie dieser Ausdrucksbaumstruktur erstellt wird. Dies sind die Techniken, die Sie in
zwei wichtigen Szenarios verwenden werden. Die erste besteht darin, die Algorithmen zu verstehen, die durch
eine Ausdrucksbaumstruktur ausgedrückt werden, sodass sie in eine andere Umgebung übersetzt werden
können. Die zweite ist, wenn Sie den erstellten Algorithmus ändern möchten. Dies könnte zum Beispiel dazu
dienen, Protokollierung hinzuzufügen oder Methodenaufrufe abzufangen und zu verfolgen.

Übersetzen bedeutet Zugreifen


Der Code, den Sie erstellen, um eine Ausdrucksbaumstruktur zu übersetzen, ist eine Erweiterung von dem, was
Sie bereits gesehen haben, um auf alle Knoten in einer Struktur zuzugreifen. Wenn Sie eine
Ausdrucksbaumstruktur übersetzen, greifen Sie auf alle Knoten zu, und erstellen während des Zugriffs auf diese
die neue Struktur. Die neue Struktur enthält Verweise auf den ursprünglichen Knoten oder neue Knoten, die Sie
in der Struktur platziert haben.
Betrachten wir dies in Aktion durch Zugriff auf eine Ausdrucksbaumstruktur und Erstellen einer neuen Struktur
mit einigen Knoten zum Ersetzen. Ersetzen wir in diesem Beispiel jede Konstante durch eine Konstante, die
zehnmal so groß ist. Ansonsten werden wir die Ausdrucksbaumstruktur intakt lassen. Anstatt den Wert der
Konstante zu lesen und sie durch eine neue Konstante zu ersetzen, führen wir diese Ersetzung durch Ersetzen
des konstanten Knotens durch einen neuen Knoten durch, der die Multiplikation ausführt.
Hier erstellen Sie, sobald Sie einen konstanten Knoten gefunden haben, einen neuen Multiplikationsknoten,
dessen untergeordnete Elemente die ursprünglichen Konstanten sind, und die Konstante 10 :

private static Expression ReplaceNodes(Expression original)


{
if (original.NodeType == ExpressionType.Constant)
{
return Expression.Multiply(original, Expression.Constant(10));
}
else if (original.NodeType == ExpressionType.Add)
{
var binaryExpression = (BinaryExpression)original;
return Expression.Add(
ReplaceNodes(binaryExpression.Left),
ReplaceNodes(binaryExpression.Right));
}
return original;
}

Durch Ersetzen des ursprünglichen Knotens durch den Ersatz wird eine neue Struktur gebildet, die unsere
Änderungen enthält. Wir können dies durch Kompilieren und Ausführen der ersetzten Struktur überprüfen.
var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));
var addition = Expression.Add(one, two);
var sum = ReplaceNodes(addition);
var executableFunc = Expression.Lambda(sum);

var func = (Func<int>)executableFunc.Compile();


var answer = func();
Console.WriteLine(answer);

Das Erstellen einer neuen Struktur ist eine Kombination aus dem Zugriff auf die Knoten in der vorhandenen
Struktur und dem Erstellen und Einfügen neuer Knoten in die Struktur.
Dieses Beispiel zeigt, wie wichtig es ist, dass Ausdrucksbaumstrukturen unveränderlich sind. Beachten Sie, dass
die weiter oben erstellte neue Struktur eine Mischung aus neu erstellten Knoten und Knoten aus der
vorhandenen Struktur enthält. Dies ist sicher, da die Knoten in der vorhandenen Struktur nicht geändert werden
können. Dies kann zu beträchtlicher Effizienz beim Speicherplatz führen. Die gleichen Knoten können in der
gesamten Struktur oder in mehreren Ausdrucksbaumstrukturen verwendet werden. Da Knoten nicht geändert
werden können, kann der gleiche Knoten wiederverwendet werden, wenn er benötigt wird.

Durchlaufen und Ausführen einer Addition


Lassen Sie uns dies überprüfen, indem Sie einen zweiten Besucher erstellen, der die Struktur der
Additionsknoten durchläuft und das Ergebnis berechnet. Hierzu können Sie einige Änderungen an dem
Besucher vornehmen, den Sie bisher gesehen haben. In dieser neuen Version wird der Besucher die partielle
Summe des Additionsvorgangs bis zu diesem Punkt zurückgeben. Für einen konstanten Ausdruck ist dies
einfach der Wert des konstanten Ausdrucks. Für einen Additionsausdruck ist das Ergebnis die Summe der linken
und rechten Operanden, sobald diese Strukturen durchlaufen wurden.

var one = Expression.Constant(1, typeof(int));


var two = Expression.Constant(2, typeof(int));
var three= Expression.Constant(3, typeof(int));
var four = Expression.Constant(4, typeof(int));
var addition = Expression.Add(one, two);
var add2 = Expression.Add(three, four);
var sum = Expression.Add(addition, add2);

// Declare the delegate, so we can call it


// from itself recursively:
Func<Expression, int> aggregate = null;
// Aggregate, return constants, or the sum of the left and right operand.
// Major simplification: Assume every binary expression is an addition.
aggregate = (exp) =>
exp.NodeType == ExpressionType.Constant ?
(int)((ConstantExpression)exp).Value :
aggregate(((BinaryExpression)exp).Left) + aggregate(((BinaryExpression)exp).Right);

var theSum = aggregate(sum);


Console.WriteLine(theSum);

Es ist einiges an Code, aber die Konzepte sind sehr zugänglich. Dieser Code besucht untergeordnete Elemente in
einer ersten tiefgründigen Suche. Wenn er einen konstanten Knoten erkennt, gibt der Besucher den Wert der
Konstanten zurück. Nachdem der Besucher auf beide untergeordnete Elemente zugegriffen hat, werden diese
untergeordneten Elemente die Summe für die Unterstruktur berechnet haben. Der Additionsknoten kann jetzt
seine Summe berechnen. Sobald alle Knoten in der Ausdrucksbaumstruktur aufgerufen wurden, wird die
Summe berechnet worden sein. Sie können die Ausführung verfolgen, indem Sie das Beispiel im Debugger
ausführen und die Ausführung verfolgen.
Wir erleichtern die Verfolgung, wie die Knoten analysiert werden und wie die Summe berechnet wird, indem wir
die Struktur durchlaufen. Hier ist eine aktualisierte Version der Aggregatmethode, die ziemlich viel
Ablaufverfolgungsinformationen enthält:

private static int Aggregate(Expression exp)


{
if (exp.NodeType == ExpressionType.Constant)
{
var constantExp = (ConstantExpression)exp;
Console.Error.WriteLine($"Found Constant: {constantExp.Value}");
return (int)constantExp.Value;
}
else if (exp.NodeType == ExpressionType.Add)
{
var addExp = (BinaryExpression)exp;
Console.Error.WriteLine("Found Addition Expression");
Console.Error.WriteLine("Computing Left node");
var leftOperand = Aggregate(addExp.Left);
Console.Error.WriteLine($"Left is: {leftOperand}");
Console.Error.WriteLine("Computing Right node");
var rightOperand = Aggregate(addExp.Right);
Console.Error.WriteLine($"Right is: {rightOperand}");
var sum = leftOperand + rightOperand;
Console.Error.WriteLine($"Computed sum: {sum}");
return sum;
}
else throw new NotSupportedException("Haven't written this yet");
}

Es auf demselben Ausdruck auszuführen, ergibt die folgende Ausgabe:

10
Found Addition Expression
Computing Left node
Found Addition Expression
Computing Left node
Found Constant: 1
Left is: 1
Computing Right node
Found Constant: 2
Right is: 2
Computed sum: 3
Left is: 3
Computing Right node
Found Addition Expression
Computing Left node
Found Constant: 3
Left is: 3
Computing Right node
Found Constant: 4
Right is: 4
Computed sum: 7
Right is: 7
Computed sum: 10
10

Verfolgen Sie die Ausgabe und folgen Sie dem obigen Code. Sie sollten in der Lage sein zu ermitteln, wie der
Code auf jeden Knoten zugreift und die Summe berechnet, während er die Struktur durchläuft und die Summe
ermittelt.
Jetzt sehen wir uns eine andere Ausführung mit dem von sum1 zur Verfügung gestellten Ausdruck an:
Expression<Func<int> sum1 = () => 1 + (2 + (3 + 4));

Hier ist die Ausgabe von der Untersuchung dieses Ausdrucks:

Found Addition Expression


Computing Left node
Found Constant: 1
Left is: 1
Computing Right node
Found Addition Expression
Computing Left node
Found Constant: 2
Left is: 2
Computing Right node
Found Addition Expression
Computing Left node
Found Constant: 3
Left is: 3
Computing Right node
Found Constant: 4
Right is: 4
Computed sum: 7
Right is: 7
Computed sum: 9
Right is: 9
Computed sum: 10
10

Während die endgültige Antwort identisch ist, ist das Durchlaufen der Struktur unterschiedlich. Die Knoten
werden in einer anderen Reihenfolge zurückgelegt, da die Struktur mit verschiedenen Vorgängen zuerst erstellt
wurde.

Weitere Informationen
Dieses Beispiel zeigt eine kleine Teilmenge des Codes, den Sie erstellen möchten, um die durch eine
Ausdrucksbaumstruktur dargestellten Algorithmen zu durchlaufen und zu interpretieren. Für eine vollständige
Beschreibung aller notwendigen Arbeiten zur Erstellung einer allgemeinen Bibliothek, die
Ausdrucksbaumstrukturen in eine andere Sprache übersetzt, lesen Sie diese Serie von Matt Warren. Es wird bis
ins Detail erklärt, wie jeder Code übersetzt wird, den Sie in einer Ausdrucksbaumstruktur finden könnten.
Ich hoffe, dass Sie jetzt die tatsächliche Leistungsfähigkeit von Ausdrucksbaumstrukturen gesehen haben. Sie
können einen Satz von Code untersuchen, alle Änderungen vornehmen, die Sie für diesen Code haben möchten,
und die geänderte Version ausführen. Da die Ausdrucksbaumstrukturen unveränderlich sind, können Sie neue
Strukturen mithilfe der Komponenten von vorhandenen Strukturen erstellen. Dies minimiert die Menge an
erforderlichem Arbeitsspeicher, um geänderte Ausdrucksbaumstrukturen zu erstellen.
Weiter – Schlussbemerkung
Zusammenfassung der Ausdrucksbaumstrukturen
04.11.2021 • 2 minutes to read

Vorheriges – Übersetzen von Ausdrücken


In dieser Serie haben Sie erfahren, wie Sie Ausdrucksbaumstrukturen verwenden können, um dynamische
Programme zu erstellen, die Code wie Daten interpretieren, und um neue Funktionen basierend auf diesem
Code zu erstellen.
Sie können Ausdrucksbaumstrukturen untersuchen, um die Absicht eines Algorithmus zu verstehen. Sie können
nicht nur diesen Code überprüfen. Sie können neue Ausdrucksbaumstrukturen erstellen, die geänderte
Versionen des ursprünglichen Codes darstellen.
Sie können auch mithilfe von Ausdrucksbaumstrukturen einen Algorithmus betrachten und den Algorithmus in
eine andere Sprache oder Umgebung übersetzen.

Einschränkungen
Es gibt einige neuere C#-Sprachelemente, die nicht gut in Ausdrucksbaumstrukturen übersetzt werden.
Ausdrucksbaumstrukturen können keine await -Ausdrücke oder async -Lambdaausdrücke enthalten. Viele der
in der C# 6-Version hinzugefügten Funktionen werden nicht genau wie in den Ausdrucksbaumstrukturen
geschrieben angezeigt. Stattdessen werden neuere Funktionen in Ausdrucksbaumstrukturen in der
entsprechenden früheren Syntax verfügbar gemacht. Diese sind möglicherweise nicht so sehr von einer
Einschränkung betroffen, wie Sie vielleicht denken. In der Tat bedeutet dies, dass Ihr Code, der
Ausdrucksbaumstrukturen interpretiert, wahrscheinlich immer noch genauso funktioniert, wenn neue
Sprachfunktionen eingeführt werden.
Trotz dieser Einschränkungen können Ausdrucksbaumstrukturen es Ihnen ermöglichen, dynamische
Algorithmen zu erstellen, die sich auf das Interpretieren und Bearbeiten von Code verlassen, der als
Datenstruktur dargestellt wird. Es ist ein leistungsfähiges Tool und eine der Funktionen des .NET-Ökosystems,
die es umfangreichen Bibliotheken wie Entity Framework ermöglichen, zu erreichen, was sie tun.
Interoperabilität (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Interoperabilität ermöglicht es Ihnen, vorhandene Investitionen in nicht verwalteten Code zu schützen und
weiter zu nutzen. Code, der unter der Steuerung der Common Language Runtime (CLR) ausgeführt wird, wird
als verwalteter Code bezeichnet. Code, der außerhalb der CLR ausgeführt wird, wird als nicht verwalteter Code
bezeichnet. COM, COM+, C++-Komponenten, ActiveX-Komponenten und die Microsoft Windows-API sind
Beispiele für nicht verwalteten Code.
.NET ermöglicht Interoperabilität mit nicht verwaltetem Code über Plattformaufrufdienste, dem
System.Runtime.InteropServices-Namespace, C++-Interoperabilität und COM-Interoperabilität (COM-Interop).

In diesem Abschnitt
Überblick über die Interoperabilität
Dieser Artikel beschreibt Methoden zum Ermöglichen der Interoperabilität zwischen von C#-verwaltetem und
nicht verwaltetem Code.
Zugreifen auf Office-Interop-Objekte mithilfe von Visual C#-Funktionen
Dieser Artikel beschreibt Funktionen, die in Visual C# zur Erleichterung der Office-Programmierung eingeführt
wurden.
Indizierte Eigenschaften bei der COM-Interop-Programmierung
Dieser Artikel beschreibt die Verwendung von indizierten Eigenschaften zum Zugriff auf COM-Eigenschaften,
die über Parameter verfügen.
Verwenden eines Plattformaufrufs zum Wiedergeben einer Wavedatei
Dieser Artikel beschreibt die Verwendung von Plattformaufrufdiensten zum Abspielen einer WAV-Audiodatei im
Windows-Betriebssystem.
Exemplarische Vorgehensweise: Office-Programmierung
Dieser Artikel zeigt das Erstellen einer Excel-Arbeitsmappe und eines Word-Dokuments, das einen Link zur
Arbeitsmappe enthält.
COM-Beispielklasse
Dieser Artikel veranschaulicht, wie eine C#-Klasse als COM-Objekt verfügbar gemacht wird.

C#-Programmiersprachenspezifikation
Weitere Informationen finden Sie in den grundlegenden Konzepten und derC#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
Marshal.ReleaseComObject
C#-Programmierhandbuch
Interoperabilität mit nicht verwaltetem Code
Exemplarische Vorgehensweise: Office-Programmierung
Versionierung in C#
04.11.2021 • 5 minutes to read

In diesem Tutorial erfahren Sie, welche Rolle die Versionierung in .NET spielt. Außerdem erfahren Sie, welche
Faktoren Sie berücksichtigen müssen, wenn Sie Ihre Bibliothek versionieren oder ein Upgrade auf eine neue
Version einer Bibliothek durchführen.

Erstellen von Bibliotheken


Als Entwickler, der bereits .NET-Bibliotheken für die öffentliche Verwendung erstellt hat, waren Sie bestimmt
schon in Situationen, in denen Sie neue Updates implementieren mussten. Die richtige Vorgehensweise ist hier
sehr wichtig, da Sie sicherstellen müssen, dass es einen nahtlosen Übergang von vorhandenem Code auf die
neue Version Ihrer Bibliothek gibt. Folgendes Punkte spielen eine Rolle, wenn Sie eine neue Version erstellen:
Semantische Versionskontrolle
Semantic versioning (Semantische Versionierung, kurz SemVer) ist eine Namenskonvention, die auf Versionen
Ihrer Bibliothek angewendet wird, um bestimmte Meilensteinereignisse zu kennzeichnen. Im Idealfall sollten es
die Versionsinformationen Ihrer Bibliothek Entwicklern erleichtern, festzustellen, ob ihre Projekte, die ältere
Versionen dieser Bibliothek verwenden, kompatibel sind.
Die einfachste Herangehensweise an SemVer ist das Format MAJOR.MINOR.PATCH mit drei Komponenten, bei dem:
MAJOR inkrementiert wird, wenn Sie nicht kompatible API-Änderungen durchführen
MINOR inkrementiert wird, wenn Sie abwärtskompatible Funktionalität hinzufügen
PATCH inkrementiert wird, wenn Sie abwärtskompatible Fehlerkorrekturen durchführen
Es besteht auch die Möglichkeit, andere Szenarios wie Vorabversionen usw. anzugeben, wenn Sie die
Versionsinformationen auf Ihre .NET-Bibliothek anwenden.
Abwärtskompatibilität
Die Abwärtskompatibilität mit früheren Versionen ist wahrscheinlich eine Ihrer Hauptsorgen, wenn Sie neue
Versionen Ihrer Bibliothek veröffentlichen. Bei einer neuen Version Ihrer Bibliothek besteht
Quellenkompatibilität mit einer früheren Version, wenn Code, der von früheren Versionen abhängt, durch
erneutes Kompilieren mit der neuen Version funktionieren kann. Bei einer neuen Version Ihrer Bibliothek besteht
binäre Kompatibilität, wenn eine Anwendung, die von der alten Versionen abhängt, ohne erneutes Kompilieren
mit der neuen Version arbeiten kann.
Folgende Punkte sollten Sie beachten, wenn Sie versuchen die Abwärtskompatibilität mit älteren Versionen ihrer
Bibliothek aufrechtzuerhalten:
Virtuelle Methoden: Wenn Sie eine virtuelle Methode in Ihrer neuen Version in eine nicht-virtuelle Methode
umwandeln, bedeutet das, dass die Projekte, die diese Methode außer Kraft setzen, aktualisiert werden
müssen. Es handelt sich um eine schwere grundlegende Änderung, von der stark abgeraten wird.
Methodensignaturen: Wenn Sie beim Aktualisieren des Verhaltens einer Methode ebenfalls deren Signatur
ändern müssen, sollten Sie stattdessen eine Überladung erstellen, damit der Code, der diese Methode aufruft,
weiterhin funktioniert. Sie können die alte Methodensignatur immer so ändern, dass sie die neue
Methodensignatur aufruft. Dadurch bleibt die Implementierung konsistent.
Obsolete-Attribut: Sie können dieses Attribut in Ihrem Code verwenden, um Klassen oder Klassenmember
anzugeben, die veraltet sind und in zukünftigen Versionen vermutlich gelöscht werden. Dadurch sind
Entwickler, die Ihre Bibliothek verwenden, besser auf grundlegende Änderungen vorbereitet.
Optionale Methodenargumente: Wenn sie vorher optionale in obligatorische Methodenargumente
umwandeln oder ihren Standardwert ändern, muss der gesamte Code, der diese Argumente nicht bereitstellt,
aktualisiert werden.

NOTE
Die Umwandlung von obligatorischen Argumenten in optionale sollte nur geringe Auswirkungen haben, besonders, wenn
dadurch das Verhalten der Methode nicht geändert wird.

Je leichter Sie es Ihren Benutzern machen, auf die neue Version Ihrer Bibliothek zu aktualisieren, desto früher
werden sie dieses Upgrade durchführen.
Anwendungskonfigurationsdatei
Als .NET-Entwickler haben Sie sehr wahrscheinlich schon einmal mit der app.config -Datei gearbeitet, die in den
meisten Projekttypen enthalten ist. Diese einfache Konfigurationsdatei kann beim Verbessern der Einführung
neuer Updates einen großen Unterschied machen. In der Regel sollten Sie Ihre Bibliotheken so anlegen, dass die
Informationen, die sich wahrscheinlich häufiger ändern, in der Datei app.config gespeichert werden. Auf diese
Weise muss die Konfigurationsdatei früherer Versionen nur durch die neue Datei ersetzt werden, wenn solche
Informationen aktualisiert werden. Eine erneute Kompilierung der Bibliothek ist nicht mehr erforderlich.

Verarbeiten von Bibliotheken


Als Entwickler, der .NET-Bibliotheken verarbeitet, die von anderen Entwicklern erstellt wurden, sind Sie sich
sicher der Tatsache bewusst, dass eine neue Version einer Bibliothek möglicherweise nicht voll kompatibel mit
Ihrem Projekt ist, und Sie Ihren Code oft aktualisieren müssen, um auf diese Änderungen zu reagieren.
Glücklicherweise gibt es in C#-und dem .NET-Ökosystem Features und Techniken, mit denen Sie Apps leicht
aktualisieren können, damit sie mit neuen Versionen von Bibliotheken funktionieren, mit denen möglicherweise
Breaking Changes eingeführt werden.
Assemblybindungsumleitung
Sie können die Datei app.config zum Aktualisieren der Version einer von Ihrer App verwendeten Bibliothek
verwenden. Durch Hinzufügen einer sogenannten Bindungsumleitung können Sie die neue Version der
Bibliothek verwenden, ohne dass Sie Ihre App erneut kompilieren müssen. Im folgenden Beispiel wird gezeigt,
wie Sie die app.config-Datei Ihrer App aktualisieren, um die Patchversion 1.0.1 von ReferencedLibrary zu
verwenden, statt der Version 1.0.0 , mit der sie ursprünglich kompiliert wurde.

<dependentAssembly>
<assemblyIdentity name="ReferencedLibrary" publicKeyToken="32ab4ba45e0a69a1" culture="en-us" />
<bindingRedirect oldVersion="1.0.0" newVersion="1.0.1" />
</dependentAssembly>

NOTE
Dieser Ansatz funktioniert nur, wenn die neue Version von ReferencedLibrary binär kompatibel mit Ihrer Anwendung
ist. Im Abschnitt Abwärtskompatibilität finden Sie Änderungen, die Sie beachten müssen, wenn Sie die Kompatibilität
bestimmen.

neu
Sie können den new -Modifizierer verwenden, um geerbte Member einer Basisklasse auszublenden. Dies ist
eine Möglichkeit, wie abgeleitete Klassen auf Updates in Basisklassen reagieren können.
Betrachten Sie das folgende Beispiel:
public class BaseClass
{
public void MyMethod()
{
Console.WriteLine("A base method");
}
}

public class DerivedClass : BaseClass


{
public new void MyMethod()
{
Console.WriteLine("A derived method");
}
}

public static void Main()


{
BaseClass b = new BaseClass();
DerivedClass d = new DerivedClass();

b.MyMethod();
d.MyMethod();
}

Ausgabe

A base method
A derived method

Im obigen Beispiel können Sie sehen, wie DerivedClass die Methode MyMethod ausblendet, die sich in
BaseClass befindet. Das bedeutet, dass Sie einfach den new -Modifizierer auf Ihre abgeleiteten Klassenmember
anwenden können, um die Member der Basisklasse auszublenden, wenn von einer Basisklasse in der neuen
Version einer Bibliothek Member hinzugefügt werden, die sich bereits in Ihrer abgeleiteten Klasse befinden.
Wenn kein new -Modifizierer angegeben ist, blendet eine abgeleitete Klasse standardmäßig in Konflikt stehende
Member in der Basisklasse aus. Obwohl eine Compilerwarnung generiert wird, kompiliert der Code weiter. Das
bedeutet, dass die neue Version Ihrer Bibliothek durch das Hinzufügen neuer Member zu einer vorhanden
Klasse sowohl quellen- als auch binär kompatibel mit dem Code wird, der von ihr abhängt.
override
Der override -Modifizierer bedeutet, dass eine abgeleitete Implementierung die Implementierung eines
Basisklassenmembers erweitert, anstatt sie auszublenden. Der virtual -Modifizierer muss auf den
Basisklassenmember angewendet sein.
public class MyBaseClass
{
public virtual string MethodOne()
{
return "Method One";
}
}

public class MyDerivedClass : MyBaseClass


{
public override string MethodOne()
{
return "Derived Method One";
}
}

public static void Main()


{
MyBaseClass b = new MyBaseClass();
MyDerivedClass d = new MyDerivedClass();

Console.WriteLine("Base Method One: {0}", b.MethodOne());


Console.WriteLine("Derived Method One: {0}", d.MethodOne());
}

Ausgabe

Base Method One: Method One


Derived Method One: Derived Method One

Der override -Modifizierer wird beim Kompilieren ausgewertet, und der Compiler löst einen Fehler aus, wenn
kein virtueller Member gefunden wird, der außer Kraft gesetzt werden kann.
Ihre Kenntnis der besprochenen Techniken und Ihr Wissen, in welchen Situationen diese angewendet werden,
wird wesentlich dazu beitragen, den Übergang zwischen den Versionen einer Bibliothek zu erleichtern.
Vorgehensweise (C#)
04.11.2021 • 3 minutes to read

Im Abschnitt zu den Vorgehensweisen des C#-Handbuchs finden Sie Antworten auf häufig gestellte Fragen. In
manchen Fällen können Artikel in mehreren Abschnitten aufgeführt sein. Diese Artikel sollten somit über
verschiedene Suchwege einfach zu finden sein.

Allgemeine C#-Konzepte
Die meisten C#-Entwickler nutzen verschiedene Tipps und Tricks, wie diese hier:
Initialisieren von Objekten mithilfe eines Objektinitialisierers.
Unterschiede zwischen dem Übergeben einer Struktur und dem Übergeben eines Klassenverweises an eine
Methode.
Verwenden der Operatorüberladung.
Implementieren und Aufrufen einer benutzerdefinierten Erweiterungsmethode.
Erstellen einer neuen Methode für einen enum -Typ mithilfe von Erweiterungsmethoden.
Klassen-, Datensatz- und Strukturmember
Sie erstellen Klassen, Datensätze und Strukturen, um Ihr Programm zu implementieren. Diese Verfahren werden
häufig beim Schreiben von Klassen, Datensätzen oder Strukturen verwendet.
Deklarieren automatisch implementierter Eigenschaften.
Deklarieren und Verwenden von Lese-/Schreibeigenschaften.
Definieren von Konstanten.
Überschreiben der ToString -Methode zum Bereitstellen von Zeichenfolgenausgaben.
Definieren von abstrakten Eigenschaften.
Verwenden der XML-Dokumentationsfunktionen zum Dokumentieren Ihres Codes.
Explizites Implementieren von Schnittstellenmembern, um Ihre öffentliche Schnittstelle bündig zu halten.
Explizites Implementieren von Membern zweier Schnittstellen.
Arbeiten mit Sammlungen
Diese Artikel helfen Ihnen bei der Arbeit mit Datensammlungen.
Initialisieren eines Wörterbuchs mit einem Auflistungsinitialisierer.

Arbeiten mit Zeichenfolgen


Zeichenfolgen sind der grundlegende Datentyp für das Anzeigen oder Bearbeiten von Text. Diese Artikel
veranschaulichen gängige Vorgehensweisen zu Zeichenfolgen.
Vergleichen von Zeichenfolgen.
Ändern des Inhalts einer Zeichenfolge.
Bestimmen, ob eine Zeichenfolge einen numerischen Wert darstellt.
Benutzen von String.Split zum Trennen von Zeichenfolgen.
Vereinen von mehreren Zeichenfolgen.
Suchen von Text in einer Zeichenfolge.

Konvertieren zwischen Typen


Möglicherweise müssen Sie ein Objekt in einen anderen Typ konvertieren.
Bestimmen, ob eine Zeichenfolge einen numerischen Wert darstellt.
Konvertieren zwischen Zeichenfolgen, die einen Hexadezimalwert darstellen, und numerischen Typen.
Konvertieren einer Zeichenfolge in den Wert DateTime .
Konvertieren eines Bytearrays in einen ganzzahligen Typ.
Konvertieren einer Zeichenfolge in eine Zahl.
Verwenden des Musterabgleichs sowie der Operatoren as und is zur sicheren Umwandlung in einen
anderen Typ
Definieren benutzerdefinierter Typkonvertierungen
Identifizieren, ob ein Typ ein Typ ist, der NULL-Werte zulässt.
Konvertieren zwischen Typen, die NULL zulassen, und Typen, die NULL nicht zulassen.

Übereinstimmungs- und Reihenfolgenvergleiche


Sie können Typen erstellen, die ihre eigenen Übereinstimmungsregeln, oder eine natürliche Reihenfolge von
Objekten ihres Typs definieren.
Überprüfen auf Verweisgleichheit.
Definieren von Wertgleichheit für einen Typ.

Ausnahmebehandlung
.NET Programme melden Methoden, die ihre Arbeit nicht erfolgreich abgeschlossen haben, indem sie
Ausnahmen auslösen. In den folgenden Artikeln erfahren Sie, wie Sie mit Ausnahmen arbeiten.
Behandeln von Ausnahmen mit try und catch .
Bereinigen von Ressourcen mit finally -Klauseln.
Abfangen von Nicht-CLS Ausnahmen (Common Language Specification).

Delegaten und Ereignisse


Delegaten und Ereignisse sind Funktionen für Strategien, die lose angeordnete Codeblöcke verwenden.
Deklarieren, Instanziieren und Verwenden von Delegaten.
Kombinieren von Multicastdelegaten.
Ereignisse sind Funktionen zum Veröffentlichen oder Abonnieren von Benachrichtigungen.
Abonnieren von Ereignissen und Kündigen von Ereignisabonnements.
Implementieren von Schnittstellenereignissen.
Veröffentlichen von mit den .NET-Richtlinien konformen Ereignissen (C#-Programmierhandbuch)
Auslösen von Basisklassenereignissen in abgeleiteten Klassen.
Implementieren von benutzerdefinierten Ereigniszugriffsmethoden.

LINQ-Methoden
LINQ ermöglicht Ihnen Code zu schreiben, mit dem Sie Datenquellen abfragen können, die das LINQ-
Abfrageausdrucksmuster unterstützen. In den folgenden Artikeln finden Sie Informationen zum Verstehen des
Musters, und zum Arbeiten mit verschiedenen Datenquellen.
Abfragen einer Auflistung.
Verwenden von var in Abfrageausdrücken.
Zurückgeben von Teilmengen von Elementeigenschaften aus einer Abfrage.
Schreiben von Abfragen mit komplexer Filterung.
Sortieren von Elementen einer Datenquelle.
Sortieren von Elementen nach mehreren Schlüsseln.
Steuern des Typs einer Projektion.
Zählen der Vorkommnisse eines Werts in einer Quellsequenz.
Berechnen von Zwischenwerten.
Zusammenführen von Daten aus mehreren Quellen.
Suchen der Unterschiedsmenge zwischen zwei Sequenzen.
Debuggen von leeren Abfrageergebnissen.
Hinzufügen von benutzerdefinierten Methoden zu LINQ-Abfragen.

Mehrere Threads und asynchrone Verarbeitung


Moderne Programme verwenden häufig asynchrone Vorgänge. In den folgenden Artikeln erfahren Sie, wie Sie
mit diesen Methoden arbeiten.
Verbessern der asynchronen Leistung mit System.Threading.Tasks.Task.WhenAll .
Paralleles Erstellen mehrerer Webanforderungen mit async und await .
Verwenden von Threadpools.

Befehlszeilenargumente für Ihr Programm


C#-Programme haben in der Regel Befehlszeilenargumente. In den folgenden Artikeln erfahren Sie, wie Sie auf
Befehlszeilenargumente zugreifen und sie verarbeiten.
Abrufen aller Befehlszeilenargumente mit for .
Teilen von Zeichenfolgen mithilfe von String.Split in
C#
04.11.2021 • 2 minutes to read

Die String.Split-Methode erstellt ein Array mit Teilzeichenfolgen, indem die Eingabezeichenfolge von mindestens
einem Trennzeichen geteilt wird. Sie stellt in der Regel die einfachste Möglichkeit dar, eine Zeichenfolge an
Wortgrenzen zu teilen. Sie wird außerdem verwendet, um Zeichenfolgen nach anderen bestimmten Zeichen
und Zeichenfolgen zu trennen.

NOTE
Die C#-Beispiele in diesem Artikel werden in der Inlinecodeausführung und dem Playground von Try.NET ausgeführt.
Klicken Sie auf die Schaltfläche Ausführen , um ein Beispiel in einem interaktiven Fenster auszuführen. Nachdem Sie den
Code ausgeführt haben, können Sie ihn ändern und den geänderten Code durch erneutes Anklicken der Schaltfläche
Ausführen ausführen. Der geänderte Code wird entweder im interaktiven Fenster ausgeführt, oder das interaktive
Fenster zeigt alle C#-Compilerfehlermeldungen an, wenn die Kompilierung fehlschlägt.

Mithilfe des folgenden Codes wird ein häufig verwendeter Ausdruck in ein Array mit Zeichenfolgen für jedes
Wort unterteilt.

string phrase = "The quick brown fox jumps over the lazy dog.";
string[] words = phrase.Split(' ');

foreach (var word in words)


{
System.Console.WriteLine($"<{word}>");
}

Jede Instanz eines Trennzeichens gibt einen Wert in dem zurückgegebenen Array zurück. Aufeinander folgende
Trennzeichen geben eine leere Zeichenfolge als Wert in einem zurückgegebenen Array zurück. Im folgenden
Beispiel sehen Sie, wie ein leere Zeichenfolge erstellt wird. Hier wird das Leerzeichen als Trennzeichen
verwendet.

string phrase = "The quick brown fox jumps over the lazy dog.";
string[] words = phrase.Split(' ');

foreach (var word in words)


{
System.Console.WriteLine($"<{word}>");
}

Dieses Verhalten vereinfacht den Vorgang für Formate wie CSV (Comma Separated Values, durch Trennzeichen
getrennte Werte), die Tabellendaten darstellen. Aufeinander folgende Kommas stellen eine leere Spalte dar.
Sie können einen optionalen StringSplitOptions.RemoveEmptyEntries-Parameter übergeben, um jegliche leeren
Zeichenfolgen aus dem zurückgegebenen Array auszuschließen. Für kompliziertere Prozesse der
zurückgegebenen Auflistung können Sie LINQ verwenden, um die Ergebnissequenz zu manipulieren.
String.Split kann mehrere Trennzeichen verwenden. In diesem Beispiel werden Leerräume, Kommas, Punkte,
Doppelpunkte und Tabstopps verwendet, die an Split in einem Array übergeben werden. Die Schleife am Ende
des Codes zeigt sämtliche Wörter im zurückgegeben Array an.
char[] delimiterChars = { ' ', ',', '.', ':', '\t' };

string text = "one\ttwo three:four,five six seven";


System.Console.WriteLine($"Original text: '{text}'");

string[] words = text.Split(delimiterChars);


System.Console.WriteLine($"{words.Length} words in text:");

foreach (var word in words)


{
System.Console.WriteLine($"<{word}>");
}

Aufeinander folgende Instanzen einer beliebigen Trennlinie geben die leere Zeichenfolge im Ausgabearray
zurück:

char[] delimiterChars = { ' ', ',', '.', ':', '\t' };

string text = "one\ttwo :,five six seven";


System.Console.WriteLine($"Original text: '{text}'");

string[] words = text.Split(delimiterChars);


System.Console.WriteLine($"{words.Length} words in text:");

foreach (var word in words)


{
System.Console.WriteLine($"<{word}>");
}

String.Split kann ein Array mit Zeichenfolgen aufnehmen (Zeichenfolgen, die als Trennzeichen für die Analyse
der Zielzeichenfolge fungieren, statt einzelne Zeichen).

string[] separatingStrings = { "<<", "..." };

string text = "one<<two......three<four";


System.Console.WriteLine($"Original text: '{text}'");

string[] words = text.Split(separatingStrings, System.StringSplitOptions.RemoveEmptyEntries);


System.Console.WriteLine($"{words.Length} substrings in text:");

foreach (var word in words)


{
System.Console.WriteLine(word);
}

Siehe auch
Extrahieren von Elementen aus einer Zeichenfolge
C#-Programmierhandbuch
Zeichenfolgen
Reguläre Ausdrücke in .NET
Vorgehensweise: Verketten mehrerer Zeichenfolgen
(C#-Handbuch)
04.11.2021 • 4 minutes to read

Verkettung ist der Prozess, eine Zeichenfolge ans Ende einer anderen Zeichenfolge anzufügen. Sie können
Zeichenfolgen mithilfe des + -Operators verketten. Bei Zeichenfolgenliteralen und Zeichenfolgenkonstanten
erfolgt die Verkettung beim Kompilieren. Es erfolgt keine Verkettung zur Laufzeit. Bei Zeichenfolgenvariablen
erfolgt die Verkettung nur zur Laufzeit.

NOTE
Die C#-Beispiele in diesem Artikel werden in der Inlinecodeausführung und dem Playground von Try.NET ausgeführt.
Klicken Sie auf die Schaltfläche Ausführen , um ein Beispiel in einem interaktiven Fenster auszuführen. Nachdem Sie den
Code ausgeführt haben, können Sie ihn ändern und den geänderten Code durch erneutes Anklicken der Schaltfläche
Ausführen ausführen. Der geänderte Code wird entweder im interaktiven Fenster ausgeführt, oder das interaktive
Fenster zeigt alle C#-Compilerfehlermeldungen an, wenn die Kompilierung fehlschlägt.

Zeichenfolgenliterale
Im folgenden Beispiel wird ein langes Zeichenfolgenliteral in kleinere Zeichenfolgen aufgeteilt, um die
Lesbarkeit im Quellcode zu verbessern. Der Code verkettet die kleineren Zeichenfolgen, um das lange
Zeichenfolgenliteral zu erstellen. Die Teile werden zur Kompilierzeit zu einer einzelnen Zeichenfolge verkettet. Es
entstehen unabhängig von der Anzahl an Zeichenfolgen keine Leistungseinbußen zur Laufzeit.

// Concatenation of literals is performed at compile time, not run time.


string text = "Historically, the world of data and the world of objects " +
"have not been well integrated. Programmers work in C# or Visual Basic " +
"and also in SQL or XQuery. On the one side are concepts such as classes, " +
"objects, fields, inheritance, and .NET Framework APIs. On the other side " +
"are tables, columns, rows, nodes, and separate languages for dealing with " +
"them. Data types often require translation between the two worlds; there are " +
"different standard functions. Because the object world has no notion of query, a " +
"query can only be represented as a string without compile-time type checking or " +
"IntelliSense support in the IDE. Transferring data from SQL tables or XML trees to " +
"objects in memory is often tedious and error-prone.";

System.Console.WriteLine(text);

Die Operatoren + und +=


Zum Verketten von Zeichenfolgenvariablen können Sie die Operatoren + und += , die
Zeichenfolgeninterpolation oder die Methoden String.Format, String.Concat, String.Join oder
StringBuilder.Append verwenden. Der Operator + ist einfach zu verwenden und gut für intuitiv verständlichen
Code geeignet. Auch wenn Sie mehrere + -Operatoren in einer Anweisung verwenden, wird der Inhalt der
Zeichenfolge nur einmal kopiert. Der folgende Code zeigt Beispiele für die Verwendung der Operatoren + und
+= zum Verketten von Zeichenfolgen:
string userName = "<Type your name here>";
string dateString = DateTime.Today.ToShortDateString();

// Use the + and += operators for one-time concatenations.


string str = "Hello " + userName + ". Today is " + dateString + ".";
System.Console.WriteLine(str);

str += " How are you today?";


System.Console.WriteLine(str);

Zeichenfolgeninterpolierung
In einigen Ausdrücken ist es einfacher, Zeichenfolgen mithilfe der Zeichenfolgeninterpolation zu verketten, wie
in folgendem Beispiel gezeigt wird:

string userName = "<Type your name here>";


string date = DateTime.Today.ToShortDateString();

// Use string interpolation to concatenate strings.


string str = $"Hello {userName}. Today is {date}.";
System.Console.WriteLine(str);

str = $"{str} How are you today?";


System.Console.WriteLine(str);

NOTE
Bei der Zeichenfolgenverkettung behandelt der C#-Compiler eine NULL-Zeichenfolge wie eine leere Zeichenfolge.

Ab C# 10 können Sie die Zeichenfolgeninterpolation verwenden, um eine konstante Zeichenfolge zu


initialisieren, wenn alle für Platzhalter verwendeten Ausdrücke auch konstante Zeichenfolgen sind.

String.Format
Eine andere Methoden zum Verketten von Zeichenfolgen ist String.Format. Diese Methode funktioniert gut,
wenn Sie eine Zeichenfolge mit einer kleinen Anzahl von Zeichenfolgenkomponenten erstellen.

StringBuilder
In anderen Fällen kann es passieren, dass Sie Zeichenfolgen in einer Schleife kombinieren, bei der Sie nicht
wissen, wie viele Quellzeichenfolgen kombiniert werden, und die tatsächliche Anzahl an Quellzeichenfolgen
kann in solchen Fällen sehr groß sein. Für solche Szenarios wurde die Klasse StringBuilder entwickelt. Im
folgenden Code werden Zeichenfolgen mit der Methode Append der Klasse StringBuilder verkettet.

// Use StringBuilder for concatenation in tight loops.


var sb = new System.Text.StringBuilder();
for (int i = 0; i < 20; i++)
{
sb.AppendLine(i.ToString());
}
System.Console.WriteLine(sb.ToString());

Erfahren Sie mehr über die Gründe für das Verketten von Zeichenfolgen oder die StringBuilder -Klasse.
String.Concat oder String.Join
Eine weitere Option zum Verketten von Zeichenfolgen aus einer Sammlung ist die Verwendung der Methode
String.Concat. Verwenden Sie die String.Join-Methode, wenn Quellzeichenfolgen durch ein Trennzeichen
getrennt werden sollen. Der folgenden Code kombiniert ein Array aus Wörtern mithilfe beider Methoden:

string[] words = { "The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog." };

var unreadablePhrase = string.Concat(words);


System.Console.WriteLine(unreadablePhrase);

var readablePhrase = string.Join(" ", words);


System.Console.WriteLine(readablePhrase);

LINQ und Enumerable.Aggregate


Die letzte Option zum Verketten von Zeichenfolgen aus einer Sammlung ist die Verwendung von LINQ und der
Methode Enumerable.Aggregate. Diese Methode kombiniert die Quellzeichenfolgen mithilfe eines
Lambdaausdrucks. Der Lambdaausdruck fügt jede Zeichenfolge der vorhandenen Akkumulation zu. Im
folgenden Beispiel wird ein Array von Worten kombiniert, indem zwischen jedem Wort im Array Leerzeichen
hinzugefügt werden:

string[] words = { "The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog." };

var phrase = words.Aggregate((partialPhrase, word) =>$"{partialPhrase} {word}");


System.Console.WriteLine(phrase);

Diese Option kann mehr Zuordnungen verursachen als andere Methoden zum Verketten von Auflistungen, da
sie eine Zwischenzeichenfolge für jede Iteration erstellt. Wenn die Optimierung der Leistung entscheidend ist,
sollten Sie die StringBuilder -Klasse oder die String.Concat - bzw. String.Join -Methode in Betracht ziehen,
um eine Auflistung zu verketten (antelle von Enumerable.Aggregate ).

Siehe auch
String
StringBuilder
C#-Programmierhandbuch
Zeichenfolgen
Vorgehensweise: Durchsuchen von Zeichenfolgen
04.11.2021 • 4 minutes to read

Sie können zwei Strategien verwenden, um in Zeichenfolgen nach Text zu suchen. Methoden der String-Klasse
suchen nach einem bestimmten Text. Reguläre Ausdrücke suchen nach Mustern im Text.

NOTE
Die C#-Beispiele in diesem Artikel werden in der Inlinecodeausführung und dem Playground von Try.NET ausgeführt.
Klicken Sie auf die Schaltfläche Ausführen , um ein Beispiel in einem interaktiven Fenster auszuführen. Nachdem Sie den
Code ausgeführt haben, können Sie ihn ändern und den geänderten Code durch erneutes Anklicken der Schaltfläche
Ausführen ausführen. Der geänderte Code wird entweder im interaktiven Fenster ausgeführt, oder das interaktive
Fenster zeigt alle C#-Compilerfehlermeldungen an, wenn die Kompilierung fehlschlägt.

Der Zeichenfolgen-Typ, der ein Alias für die Klasse System.String ist, enthält eine Reihe nützlicher Methoden, um
den Inhalt einer Zeichenfolge zu durchsuchen. Dazu zählen Contains, StartsWith, EndsWith, IndexOf,
LastIndexOf. Die System.Text.RegularExpressions.Regex-Klasse bietet ein umfangreiches Vokabular zum Suchen
nach Mustern im Text. In diesem Artikel lernen Sie diese Techniken kennen und erfahren, wie Sie die am besten
geeignete Methode für Ihre Anforderungen auswählen.

Enthält eine Zeichenfolge Text?


Die Methoden String.Contains, String.StartsWith und String.EndsWith durchsuchen eine Zeichenfolge nach
einem bestimmten Text. Das folgende Beispiel zeigt jede dieser Methoden und eine Variante, die einen
Suchvorgang verwendet, bei dem die Groß-/Kleinschreibung nicht beachtet wird:

string factMessage = "Extension methods have all the capabilities of regular static methods.";

// Write the string and include the quotation marks.


Console.WriteLine($"\"{factMessage}\"");

// Simple comparisons are always case sensitive!


bool containsSearchResult = factMessage.Contains("extension");
Console.WriteLine($"Contains \"extension\"? {containsSearchResult}");

// For user input and strings that will be displayed to the end user,
// use the StringComparison parameter on methods that have it to specify how to match strings.
bool ignoreCaseSearchResult = factMessage.StartsWith("extension",
System.StringComparison.CurrentCultureIgnoreCase);
Console.WriteLine($"Starts with \"extension\"? {ignoreCaseSearchResult} (ignoring case)");

bool endsWithSearchResult = factMessage.EndsWith(".", System.StringComparison.CurrentCultureIgnoreCase);


Console.WriteLine($"Ends with '.'? {endsWithSearchResult}");

Das obige Beispiel veranschaulicht eine wichtigen Aspekt bei der Verwendung dieser Methoden. Bei
Suchvorgängen wird standardmäßig die Groß-/Kleinschreibung beachtet . Verwenden Sie den
Enumerationswert StringComparison.CurrentCultureIgnoreCase, um einen Suchvorgang anzugeben, bei dem
die Groß-/Kleinschreibung nicht beachtet wird.

An welcher Stelle einer Zeichenfolge befindet sich der gesuchte Text ?


Die Methoden IndexOf und LastIndexOf suchen ebenfalls nach Text in Zeichenfolgen. Diese Methoden geben die
Position des gesuchten Texts zurück. Wenn der Text nicht gefunden wird, geben sie -1 zurück. Das folgende
Beispiel sucht nach dem ersten und letzten Vorkommen des Worts „methods“ und zeigt den Text dazwischen an.

string factMessage = "Extension methods have all the capabilities of regular static methods.";

// Write the string and include the quotation marks.


Console.WriteLine($"\"{factMessage}\"");

// This search returns the substring between two strings, so


// the first index is moved to the character just after the first string.
int first = factMessage.IndexOf("methods") + "methods".Length;
int last = factMessage.LastIndexOf("methods");
string str2 = factMessage.Substring(first, last - first);
Console.WriteLine($"Substring between \"methods\" and \"methods\": '{str2}'");

Suchen nach einem bestimmten Text mithilfe von regulären


Ausdrücken
Die System.Text.RegularExpressions.Regex-Klasse kann zum durchsuchen von Zeichenfolgen verwendet werden.
Die Komplexität dieser Suchvorgänge reicht von sehr einfachen bis hin zu komplizierten Textmustern.
Das folgende Codebeispiel sucht nach dem Wort „the“ oder „their “ in einem Satz, die Groß-/Kleinschreibung
wird ignoriert. Die statische Methode Regex.IsMatch führt die Suche aus. Die zu durchsuchende Zeichenfolge
und ein Suchmuster werden an die Methode übergeben. In diesem Fall gibt ein drittes Argument an, dass die
Groß-/Kleinschreibung nicht beachtet werden soll. Weitere Informationen finden Sie unter
System.Text.RegularExpressions.RegexOptions.
Das Suchmuster beschreibt den Text, nach dem Sie suchen. In der folgenden Tabelle werden die einzelnen
Elemente des Suchmusters beschrieben. (In der folgenden Tabelle wird ein einzelnes \ -Zeichen verwendet.
Dieses muss in einer C#-Zeichenfolge durch das Escapezeichen \\ ersetzt werden.)

M UST ER B EDEUT UN G

the Findet den Text „the“.

(eir)? Findet 0 oder 1 Vorkommen von „eir “.

\s Findet Zeichen für Leerraum.


string[] sentences =
{
"Put the water over there.",
"They're quite thirsty.",
"Their water bottles broke."
};

string sPattern = "the(ir)?\\s";

foreach (string s in sentences)


{
Console.Write($"{s,24}");

if (System.Text.RegularExpressions.Regex.IsMatch(s, sPattern,
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
Console.WriteLine($" (match for '{sPattern}' found)");
}
else
{
Console.WriteLine();
}
}

TIP
Die string -Methoden sind bei der Suche nach einer genauen Zeichenfolge in der Regel die bessere Wahl. Reguläre
Ausdrücke eignen sich besser, wenn Sie nach einem bestimmten Muster in einer Quellzeichenfolge suchen.

Folgt eine Zeichenfolge einem Muster?


Der folgende Code verwendet reguläre Ausdrücke, um das Format jeder Zeichenfolge in einem Array zu
überprüfen. Für die Überprüfung ist erforderlich, dass jede Zeichenfolge die Form einer Telefonnummer
aufweist, in der drei Gruppen von Ziffern durch Bindestriche getrennt sind, wobei die ersten beiden Gruppen je
drei Ziffern enthalten und die dritte Gruppe vier Ziffern umfasst. Das Suchmuster verwendet den regulären
Ausdruck ^\\d{3}-\\d{3}-\\d{4}$ . Weitere Informationen finden Sie unter Sprachelemente für reguläre
Ausdrücke – Kurzübersicht.

M UST ER B EDEUT UN G

^ Findet den Anfang der Zeichenfolge.

\d{3} Findet genau 3 Ziffernzeichen.

- Findet das Zeichen „-“.

\d{4} Findet genau 4 Ziffernzeichen.

$ Findet das Ende der Zeichenfolge.


string[] numbers =
{
"123-555-0190",
"444-234-22450",
"690-555-0178",
"146-893-232",
"146-555-0122",
"4007-555-0111",
"407-555-0111",
"407-2-5555",
"407-555-8974",
"407-2ab-5555",
"690-555-8148",
"146-893-232-"
};

string sPattern = "^\\d{3}-\\d{3}-\\d{4}$";

foreach (string s in numbers)


{
Console.Write($"{s,14}");

if (System.Text.RegularExpressions.Regex.IsMatch(s, sPattern))
{
Console.WriteLine(" - valid");
}
else
{
Console.WriteLine(" - invalid");
}
}

Dieses einzelne Suchmuster findet alle gültigen Zeichenfolgen. Reguläre Ausdrücke eignen sich besser zum
Suchen nach Mustern oder zum Überprüfen von Zeichenfolgen anhand eines Musters als für einzelne
Zeichenfolgen.

Siehe auch
C#-Programmierhandbuch
Zeichenfolgen
LINQ und Zeichenfolgen
System.Text.RegularExpressions.Regex
Reguläre Ausdrücke in .NET
Sprachelemente für reguläre Ausdrücke – Kurzübersicht
Empfohlene Vorgehensweisen für die Verwendung von Zeichenfolgen in .NET
Vorgehensweise: Ändern von Zeichenfolgeninhalten
in C#
04.11.2021 • 5 minutes to read

In diesem Artikel werden verschiedene Methoden zum Erzeugen einer string durch Modifizieren einer
vorhandenen string erläutert. Alle diese gezeigten Methoden geben das Ergebnis der Modifizierung als neues
string -Objekt zurück. Die Beispiele speichern das Ergebnis in einer neuen Variable, um zu zeigen, dass es sich
bei den ursprünglichen und den bearbeiteten Zeichenfolgen um unterschiedliche Instanzen handelt. Sie können
die ursprüngliche string und die neue, bearbeitete string untersuchen, wenn Sie die einzelnen Beispiele
ausführen.

NOTE
Die C#-Beispiele in diesem Artikel werden in der Inlinecodeausführung und dem Playground von Try.NET ausgeführt.
Klicken Sie auf die Schaltfläche Ausführen , um ein Beispiel in einem interaktiven Fenster auszuführen. Nachdem Sie den
Code ausgeführt haben, können Sie ihn ändern und den geänderten Code durch erneutes Anklicken der Schaltfläche
Ausführen ausführen. Der geänderte Code wird entweder im interaktiven Fenster ausgeführt, oder das interaktive
Fenster zeigt alle C#-Compilerfehlermeldungen an, wenn die Kompilierung fehlschlägt.

In diesem Artikel werden mehrere Methoden aufgezeigt. Sie können vorhandenen Text ersetzen. Sie können
nach Mustern suchen und übereinstimmenden Text durch anderen Text ersetzen. Sie können Zeichenfolgen als
Zeichensequenzen behandeln. Sie können zudem Hilfsmethoden verwenden, die Leerräume entfernen.
Verwenden Sie diejenigen Methoden, die am besten zu Ihrem Szenario passen.

Ersetzen von Text


Der folgende Code erstellt eine neue Zeichenfolge, indem er vorhandenen Text ersetzt.

string source = "The mountains are behind the clouds today.";

// Replace one substring with another with String.Replace.


// Only exact matches are supported.
var replacement = source.Replace("mountains", "peaks");
Console.WriteLine($"The source string is <{source}>");
Console.WriteLine($"The updated string is <{replacement}>");

Der vorherige Code veranschaulicht diese unveränderliche Eigenschaft von Zeichenfolgen. Im vorherigen
Beispiel können Sie sehen, dass die ursprüngliche Zeichenfolge source nicht modifiziert wird. Die Methode
String.Replace erstellt eine neue string , die die Modifizierungen enthält.
Die Methode Replace kann entweder Zeichenfolgen oder einzelne Zeichen ersetzen. In beiden Fällen wird jedes
Vorkommen des gesuchten Texts ersetzt. In folgendem Beispiel werden alle „ “-Zeichen durch _ ersetzt:

string source = "The mountains are behind the clouds today.";

// Replace all occurrences of one char with another.


var replacement = source.Replace(' ', '_');
Console.WriteLine(source);
Console.WriteLine(replacement);
Die Quellzeichenfolge bleibt unverändert, und es wird eine neue Zeichenfolge mit der Ersetzung zurückgegeben.

Entfernen von Leerräumen


Sie können die Methoden String.Trim, String.TrimStart und String.TrimEnd verwenden, um führende oder
nachfolgende Leerräume zu entfernen. Im folgenden Code ist ein Beispiel für jede Methode dargestellt. Die
Quellzeichenfolge bleibt unverändert. Diese Methoden geben eine neue Zeichenfolge mit modifiziertem Inhalt
zurück.

// Remove trailing and leading white space.


string source = " I'm wider than I need to be. ";
// Store the results in a new string variable.
var trimmedResult = source.Trim();
var trimLeading = source.TrimStart();
var trimTrailing = source.TrimEnd();
Console.WriteLine($"<{source}>");
Console.WriteLine($"<{trimmedResult}>");
Console.WriteLine($"<{trimLeading}>");
Console.WriteLine($"<{trimTrailing}>");

Entfernen von Text


Mit der Methode String.Remove können Sie Text aus einer Zeichenfolge entfernen. Diese Methode entfernt
mehrere Zeichen ab einem spezifischen Index. In folgendem Beispiel wird gezeigt, wie Sie String.IndexOf gefolgt
von Remove verwenden können, um Text aus einer Zeichenfolge zu entfernen:

string source = "Many mountains are behind many clouds today.";


// Remove a substring from the middle of the string.
string toRemove = "many ";
string result = string.Empty;
int i = source.IndexOf(toRemove);
if (i >= 0)
{
result= source.Remove(i, toRemove.Length);
}
Console.WriteLine(source);
Console.WriteLine(result);

Ersetzen von übereinstimmenden Mustern


Sie können reguläre Ausdrücke verwenden, um Text, der mit Mustern übereinstimmt, durch neuen Text zu
ersetzen, der auch durch ein Muster definiert werden kann. In folgendem Beispiel wird die Klasse
System.Text.RegularExpressions.Regex verwendet, um ein Muster in einer Quellzeichenfolge zu finden und
dieses durch Text mit korrekter Großschreibung zu ersetzen. Die Methode Regex.Replace(String, String,
MatchEvaluator, RegexOptions) akzeptiert eine Funktion, die die Logik der Ersetzung als eines ihrer Argumente
bereitstellt. In diesem Beispiel ist diese Funktion LocalReplaceMatchCase eine lokale Funktion , die in der
Beispielmethode deklariert wird. LocalReplaceMatchCase verwendet die System.Text.StringBuilder-Klasse zum
Erstellen der Ersatzzeichenfolge mit korrekter Großschreibung.
Reguläre Ausdrücke sind besonders beim Suchen und Ersetzen von Text nützlich, der einem bestimmten Muster
folgt, und nicht so sehr bei bekanntem Text. Weitere Informationen finden Sie unter Vorgehensweise:
Durchsuchen von Zeichenfolgen. Das Suchmuster „the\s“ sucht nach dem Wort „the“ gefolgt von einem
Leerzeichen. Der Teil des Musters stellt sicher, das es nicht „there“ als Übereinstimmung in der Quellzeichenfolge
ansieht. Weitere Informationen zur Sprache für reguläre Ausdrücke finden Sie unter Sprachelemente für
reguläre Ausdrücke – Kurzübersicht.
string source = "The mountains are still there behind the clouds today.";

// Use Regex.Replace for more flexibility.


// Replace "the" or "The" with "many" or "Many".
// using System.Text.RegularExpressions
string replaceWith = "many ";
source = System.Text.RegularExpressions.Regex.Replace(source, "the\\s", LocalReplaceMatchCase,
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
Console.WriteLine(source);

string LocalReplaceMatchCase(System.Text.RegularExpressions.Match matchExpression)


{
// Test whether the match is capitalized
if (Char.IsUpper(matchExpression.Value[0]))
{
// Capitalize the replacement string
System.Text.StringBuilder replacementBuilder = new System.Text.StringBuilder(replaceWith);
replacementBuilder[0] = Char.ToUpper(replacementBuilder[0]);
return replacementBuilder.ToString();
}
else
{
return replaceWith;
}
}

Die Methode StringBuilder.ToString gibt eine unveränderliche Zeichenfolge zurück, die den Inhalt im
StringBuilder-Objekt enthält.

Modifizieren einzelner Zeichen


Sie können ein Zeichenarray aus einer Zeichenfolge erzeugen, den Inhalt des Arrays modifizieren und dann eine
neue Zeichenfolge aus dem modifizierten Inhalt des Arrays erstellen.
In folgendem Beispiel wird gezeigt, wie Sie mehrere Zeichen in einer Zeichenfolge ersetzen. Zunächst wird die
Methode String.ToCharArray() verwendet, um ein Zeichenarray zu erstellen. Die Methode IndexOf wird
verwendet, um den Startindex des Worts „fox“ zu finden. Die folgenden drei Zeichen werden durch ein anderes
Wort ersetzt. Zum Schluss wird eine neue Zeichenfolge aus dem aktualisierten Zeichenarray erstellt.

string phrase = "The quick brown fox jumps over the fence";
Console.WriteLine(phrase);

char[] phraseAsChars = phrase.ToCharArray();


int animalIndex = phrase.IndexOf("fox");
if (animalIndex != -1)
{
phraseAsChars[animalIndex++] = 'c';
phraseAsChars[animalIndex++] = 'a';
phraseAsChars[animalIndex] = 't';
}

string updatedPhrase = new string(phraseAsChars);


Console.WriteLine(updatedPhrase);

Programmgesteuertes Erstellen von Zeichenfolgeninhalten


Da Zeichenfolgen unveränderlich sind, erzeugen die vorherigen Beispiele alle temporäre Zeichenfolgen oder -
arrays. In Hochleistungsszenarien ist es möglicherweise wünschenswert, diese Heapzuordnungen zu vermeiden.
.NET Core bietet die Methode String.Create, mit der Sie den Zeicheninhalt einer Zeichenfolge über einen Rückruf
programmgesteuert füllen können, wobei die zwischengeschalteten temporären Zeichenfolgenzuordnungen
vermieden werden.

// constructing a string from a char array, prefix it with some additional characters
char[] chars = { 'a', 'b', 'c', 'd', '\0' };
int length = chars.Length + 2;
string result = string.Create(length, chars, (Span<char> strContent, char[] charArray) =>
{
strContent[0] = '0';
strContent[1] = '1';
for (int i = 0; i < charArray.Length; i++)
{
strContent[i + 2] = charArray[i];
}
});

Console.WriteLine(result);

Sie können eine Zeichenfolge in einem festen Block mit unsicherem Code ändern. Es wird jedoch stark davon
abgeraten, den Inhalt der Zeichenfolge nach deren Erstellung zu ändern. Dadurch können Abläufe auf
unvorhersehbare Weise beeinträchtigt werden. Wenn jemand beispielsweise eine Zeichenfolge internalisiert, die
den gleichen Inhalt wie Ihre hat, erhält er Ihre Kopie und erwartet nicht, dass Sie seine Zeichenfolge ändern.

Siehe auch
Reguläre Ausdrücke in .NET
Sprachelemente für reguläre Ausdrücke – Kurzübersicht
Vergleichen von Zeichenfolgen in C#
04.11.2021 • 11 minutes to read

Durch den Vergleich zweier Zeichenfolgen können Sie entweder feststellen, ob diese gleich sind, oder in welche
Reihenfolge sie beim Sortieren gebracht werden sollen.
Beide Vorgänge werden durch folgende Faktoren erschwert:
Sie können einen Ordinalvergleich oder einen linguistischen Vergleich durchführen.
Sie können festlegen, ob die Groß/-Kleinschreibung berücksichtigt werden soll.
Sie können kulturspezifische Vergleiche durchführen.
Linguistische Vergleiche sind kultur- und plattformabhängig.

NOTE
Die C#-Beispiele in diesem Artikel werden in der Inlinecodeausführung und dem Playground von Try.NET ausgeführt.
Klicken Sie auf die Schaltfläche Ausführen , um ein Beispiel in einem interaktiven Fenster auszuführen. Nachdem Sie den
Code ausgeführt haben, können Sie ihn ändern und den geänderten Code durch erneutes Anklicken der Schaltfläche
Ausführen ausführen. Der geänderte Code wird entweder im interaktiven Fenster ausgeführt, oder das interaktive
Fenster zeigt alle C#-Compilerfehlermeldungen an, wenn die Kompilierung fehlschlägt.

Wenn Sie Zeichenfolgen vergleichen, definieren Sie für diese eine Reihenfolge. Bei einigen Vergleichen wird eine
Zeichenfolgensequenz sortiert. Sobald sich die Sequenz in einer bestimmten Reihenfolge befindet, können
Menschen und Softwareanwendungen leichter darin suchen. Bei anderen Vergleichen kann überprüft werden,
ob Zeichenfolgen identisch sind. Identitätsprüfungen sind mit Gleichheitsprüfungen zu vergleichen, doch einige
Unterschiede wie die Groß-/Kleinschreibung werden möglicherweise ignoriert.

Standardordinalvergleich
Standardmäßig gilt für die gängigsten Vorgänge Folgendes:
String.Equals
String.Equality und String.Inequality (d.h. equality-Operatoren == bzw. != )
führen einen Ordinalvergleich durch, bei dem die Groß-/Kleinschreibung beachtet wird, und im Fall von
String.Equals ein StringComparison-Argument bereitgestellt werden kann, um die Sortierregeln zu ändern. Dies
wird im folgenden Beispiel veranschaulicht:

string root = @"C:\users";


string root2 = @"C:\Users";

bool result = root.Equals(root2);


Console.WriteLine($"Ordinal comparison: <{root}> and <{root2}> are {(result ? "equal." : "not equal.")}");

result = root.Equals(root2, StringComparison.Ordinal);


Console.WriteLine($"Ordinal comparison: <{root}> and <{root2}> are {(result ? "equal." : "not equal.")}");

Console.WriteLine($"Using == says that <{root}> and <{root2}> are {(root == root2 ? "equal" : "not
equal")}");

Bei dem Standardordinalvergleich werden keine linguistischen Regeln berücksichtigt, wenn Zeichenfolgen
verglichen werden. In ihm wird der binäre Wert jedes Char-Objekts in zwei Zeichenfolgen verglichen. Dies
bedingt, dass im Standardordinalvergleich ebenfalls die Groß-/Kleinschreibung beachtet wird.
Der Test auf Gleichheit mit String.Equals und den Operatoren == und != unterscheidet sich von einem
Zeichenfolgenvergleich mit den Methoden String.CompareTo und Compare(String, String). Während in den Tests
auf Gleichheit ein Ordinalvergleich mit Beachtung der Groß-/Kleinschreibung ausgeführt wird, wird in den
Vergleichsmethoden ein Vergleich unter Beachtung der Groß-/Kleinschreibung und der aktuellen Kultur
ausgeführt. Weil die Standardvergleichsmethoden häufig unterschiedliche Arten von Vergleichen ausführen,
sollten Sie die Absicht Ihres Codes immer verdeutlichen, indem Sie eine Überladung aufrufen, in der die Art des
auszuführenden Vergleichs explizit angegeben ist.

Ordinalvergleich ohne Beachtung der Groß- und Kleinschreibung


Mit der String.Equals(String, StringComparison)-Methode können Sie einen StringComparison-Wert von
StringComparison.OrdinalIgnoreCase angeben, um einen Ordinalvergleich ohne Beachtung der Groß-
/Kleinschreibung auszuführen. Es gibt auch eine statische String.Compare(String, String, StringComparison)-
Methode, in der ein Ordinalvergleich ohne Beachtung der Groß-/Kleinschreibung ausgeführt wird, wenn Sie den
Wert StringComparison.OrdinalIgnoreCase für das StringComparison-Argument angeben. Diese Methoden
werden im folgenden Codebeispiel verwendet:

string root = @"C:\users";


string root2 = @"C:\Users";

bool result = root.Equals(root2, StringComparison.OrdinalIgnoreCase);


bool areEqual = String.Equals(root, root2, StringComparison.OrdinalIgnoreCase);
int comparison = String.Compare(root, root2, comparisonType: StringComparison.OrdinalIgnoreCase);

Console.WriteLine($"Ordinal ignore case: <{root}> and <{root2}> are {(result ? "equal." : "not equal.")}");
Console.WriteLine($"Ordinal static ignore case: <{root}> and <{root2}> are {(areEqual ? "equal." : "not
equal.")}");
if (comparison < 0)
Console.WriteLine($"<{root}> is less than <{root2}>");
else if (comparison > 0)
Console.WriteLine($"<{root}> is greater than <{root2}>");
else
Console.WriteLine($"<{root}> and <{root2}> are equivalent in order");

Wenn Sie einen Ordinalvergleich ohne Beachtung der Groß-/Kleinschreibung ausführen, verwenden Sie die
Schreibungskonventionen der invarianten Kultur.

Linguistische Vergleiche
Zeichenfolgen können auch mit linguistischen Regeln für die aktuelle Kultur sortiert werden. Dies wird
manchmal als „Wortsortierreihenfolge“ bezeichnet. Beim Ausführen eines linguistischen Vergleichs werden
möglicherweise einigen nicht alphanumerischen Unicode-Zeichen bestimmte Gewichtungen zugewiesen.
Beispielsweise wird dem Bindestrich („-“) ggf. eine geringe Gewichtung zugeordnet, sodass „coop“ und „co-op“
in einer Sortierreihenfolge nebeneinander angezeigt werden. Darüber hinaus können einige Unicode-Zeichen
einer Sequenz von Char-Instanzen entsprechen. Im folgenden Beispiel werden die Sätze „Sie tanzen auf der
Straße.“ und „Sie tanzen auf der Strasse.“ verwendet, in Deutsch mit „ss“ (U+0073 U+0073) in einer Zeichenfolge
und „ß“ (U+00DF) in einer anderen. Unter Windows entspricht in den Kulturen „en-US“ und „de-DE“ die
Zeichenfolge „ss“ linguistisch dem scharfen S/Eszett („ß“).
string first = "Sie tanzen auf der Straße.";
string second = "Sie tanzen auf der Strasse.";

Console.WriteLine($"First sentence is <{first}>");


Console.WriteLine($"Second sentence is <{second}>");

bool equal = String.Equals(first, second, StringComparison.InvariantCulture);


Console.WriteLine($"The two strings {(equal == true ? "are" : "are not")} equal.");
showComparison(first, second);

string word = "coop";


string words = "co-op";
string other = "cop";

showComparison(word, words);
showComparison(word, other);
showComparison(words, other);
void showComparison(string one, string two)
{
int compareLinguistic = String.Compare(one, two, StringComparison.InvariantCulture);
int compareOrdinal = String.Compare(one, two, StringComparison.Ordinal);
if (compareLinguistic < 0)
Console.WriteLine($"<{one}> is less than <{two}> using invariant culture");
else if (compareLinguistic > 0)
Console.WriteLine($"<{one}> is greater than <{two}> using invariant culture");
else
Console.WriteLine($"<{one}> and <{two}> are equivalent in order using invariant culture");
if (compareOrdinal < 0)
Console.WriteLine($"<{one}> is less than <{two}> using ordinal comparison");
else if (compareOrdinal > 0)
Console.WriteLine($"<{one}> is greater than <{two}> using ordinal comparison");
else
Console.WriteLine($"<{one}> and <{two}> are equivalent in order using ordinal comparison");
}

Im folgenden Beispiel wird veranschaulicht, dass linguistische Vergleiche betriebssystemabhängig sind. Der Host
für das interaktive Fenster wird unter Linux ausgeführt. Der linguistische Vergleich und der Ordinalvergleich
führen zu identischen Ergebnissen. Wenn Sie dasselbe Beispiel auf einem Windows-Host ausführen, wird
Folgendes ausgegeben:

<coop> is less than <co-op> using invariant culture


<coop> is greater than <co-op> using ordinal comparison
<coop> is less than <cop> using invariant culture
<coop> is less than <cop> using ordinal comparison
<co-op> is less than <cop> using invariant culture
<co-op> is less than <cop> using ordinal comparison

Unter Windows ändert sich die Sortierreihenfolge von „cop“, „coop“ und „co-op“, wenn Sie anstelle eines
Ordinalvergleichs einen linguistischen Vergleich verwenden. Die unterschiedlichen Vergleichsarten führen auch
bei den beiden deutschen Beispielsätzen zu unterschiedlichen Ergebnissen.

Vergleiche unter Berücksichtigung bestimmter Kulturen


In diesem Beispiel werden CultureInfo-Objekte für die Kulturen „en-US“ und „de-DE“ gespeichert. Damit
tatsächlich ein kulturabhängiger Vergleich verwendet wird, werden die Vergleiche mit einem CultureInfo-Objekt
ausgeführt.
Die verwendete Kultur wirkt sich auf linguistische Vergleiche aus. Im folgenden Beispiel werden die beiden
deutsche Sätze unter Berücksichtigung der Kulturen „en-US“ und „de-DE“ verglichen und die Ergebnisse
angezeigt:
string first = "Sie tanzen auf der Straße.";
string second = "Sie tanzen auf der Strasse.";

Console.WriteLine($"First sentence is <{first}>");


Console.WriteLine($"Second sentence is <{second}>");

var en = new System.Globalization.CultureInfo("en-US");

// For culture-sensitive comparisons, use the String.Compare


// overload that takes a StringComparison value.
int i = String.Compare(first, second, en, System.Globalization.CompareOptions.None);
Console.WriteLine($"Comparing in {en.Name} returns {i}.");

var de = new System.Globalization.CultureInfo("de-DE");


i = String.Compare(first, second, de, System.Globalization.CompareOptions.None);
Console.WriteLine($"Comparing in {de.Name} returns {i}.");

bool b = String.Equals(first, second, StringComparison.CurrentCulture);


Console.WriteLine($"The two strings {(b ? "are" : "are not")} equal.");

string word = "coop";


string words = "co-op";
string other = "cop";

showComparison(word, words, en);


showComparison(word, other, en);
showComparison(words, other, en);
void showComparison(string one, string two, System.Globalization.CultureInfo culture)
{
int compareLinguistic = String.Compare(one, two, en, System.Globalization.CompareOptions.None);
int compareOrdinal = String.Compare(one, two, StringComparison.Ordinal);
if (compareLinguistic < 0)
Console.WriteLine($"<{one}> is less than <{two}> using en-US culture");
else if (compareLinguistic > 0)
Console.WriteLine($"<{one}> is greater than <{two}> using en-US culture");
else
Console.WriteLine($"<{one}> and <{two}> are equivalent in order using en-US culture");
if (compareOrdinal < 0)
Console.WriteLine($"<{one}> is less than <{two}> using ordinal comparison");
else if (compareOrdinal > 0)
Console.WriteLine($"<{one}> is greater than <{two}> using ordinal comparison");
else
Console.WriteLine($"<{one}> and <{two}> are equivalent in order using ordinal comparison");
}

Mit kulturabhängigen Vergleichen werden üblicherweise Zeichenfolgeneingaben unterschiedlicher Benutzer


verglichen und sortiert. Die Zeichen und Sortierungskonventionen dieser Zeichenfolgen können allerdings je
nach Gebietsschema des Benutzercomputers variieren. Sogar Zeichenfolgen, die identische Zeichen enthalten,
sortieren je nach Kultur des aktuellen Threads möglicherweise unterschiedlich. Wenn der folgende Beispielcode
lokal auf einem Windows-Computer ausgeführt wird, werden die folgenden Ergebnisse angezeigt:

<coop> is less than <co-op> using en-US culture


<coop> is greater than <co-op> using ordinal comparison
<coop> is less than <cop> using en-US culture
<coop> is less than <cop> using ordinal comparison
<co-op> is less than <cop> using en-US culture
<co-op> is less than <cop> using ordinal comparison

Linguistische Vergleiche sind sowohl von der aktuellen Kultur als auch vom Betriebssystem abhängig.
Berücksichtigen Sie dies bei Zeichenfolgenvergleichen.

Linguistisches Sortieren und Suchen nach Zeichenfolgen in Arrays


In den folgenden Beispielen wird gezeigt, wie Sie mit einem linguistischen Vergleich, der abhängig von der
aktuellen Kultur ist, Zeichenfolgen in einem Array sortieren und nach diesen suchen. Dabei werden die
statischen Array-Methoden verwendet, die einen System.StringComparer-Parameter akzeptieren.
Im nächsten Beispiel wird demonstriert, wie Sie Zeichenfolgen in einem Array unter Berücksichtigung der
aktuellen Kultur sortieren können:

string[] lines = new string[]


{
@"c:\public\textfile.txt",
@"c:\public\textFile.TXT",
@"c:\public\Text.txt",
@"c:\public\testfile2.txt"
};

Console.WriteLine("Non-sorted order:");
foreach (string s in lines)
{
Console.WriteLine($" {s}");
}

Console.WriteLine("\n\rSorted order:");

// Specify Ordinal to demonstrate the different behavior.


Array.Sort(lines, StringComparer.CurrentCulture);

foreach (string s in lines)


{
Console.WriteLine($" {s}");
}

Nach der Sortierung des Arrays können Sie mit einer Binärsuche nach Einträgen suchen. Bei einer Binärsuche
wird als Startposition die Mitte der Collection gewählt, um zu bestimmen, welche Hälfte der Collection die
gesuchte Zeichenfolge enthält. Jeder nachfolgende Vergleich unterteilt den verbleibenden Teil der Collection in
zwei weitere Teile. Das Array wird mit der StringComparer.CurrentCulture-Eigenschaft sortiert. Die lokale
Funktion ShowWhere gibt Informationen zur Position aus, an der die Zeichenfolge gefunden wurde. Wenn die
Zeichenfolge nicht gefunden wurde, gibt der zurückgegebene Wert an, an welcher Stelle sich die Zeichenfolge
befände, wenn diese vorhanden wäre.
string[] lines = new string[]
{
@"c:\public\textfile.txt",
@"c:\public\textFile.TXT",
@"c:\public\Text.txt",
@"c:\public\testfile2.txt"
};
Array.Sort(lines, StringComparer.CurrentCulture);

string searchString = @"c:\public\TEXTFILE.TXT";


Console.WriteLine($"Binary search for <{searchString}>");
int result = Array.BinarySearch(lines, searchString, StringComparer.CurrentCulture);
ShowWhere<string>(lines, result);

Console.WriteLine($"{(result > 0 ? "Found" : "Did not find")} {searchString}");

void ShowWhere<T>(T[] array, int index)


{
if (index < 0)
{
index = ~index;

Console.Write("Not found. Sorts between: ");

if (index == 0)
Console.Write("beginning of sequence and ");
else
Console.Write($"{array[index - 1]} and ");

if (index == array.Length)
Console.WriteLine("end of sequence.");
else
Console.WriteLine($"{array[index]}.");
}
else
{
Console.WriteLine($"Found at index {index}.");
}
}

Ordinales Sortieren und Suchen in Collections


Im folgenden Codebeispiel wird die System.Collections.Generic.List<T>-Collectionklasse zum Speichern von
Zeichenfolgen verwendet. Die Zeichenfolgen werden mit der List<T>.Sort-Methode sortiert. Diese Methode
benötigt einen Delegaten, der zwei Zeichenfolgen vergleicht und sortiert. Die String.CompareTo-Methode stellt
diese Vergleichsfunktion bereit. Führen Sie das Beispiel aus, und beachten Sie die Reihenfolge. Bei diesem
Sortiervorgang werden Zeichenfolgen ordinal unter Berücksichtigung der Groß-/Kleinschreibung sortiert. Mit
den statischen String.Compare-Methoden können Sie unterschiedliche Vergleichsregeln angeben.
List<string> lines = new List<string>
{
@"c:\public\textfile.txt",
@"c:\public\textFile.TXT",
@"c:\public\Text.txt",
@"c:\public\testfile2.txt"
};

Console.WriteLine("Non-sorted order:");
foreach (string s in lines)
{
Console.WriteLine($" {s}");
}

Console.WriteLine("\n\rSorted order:");

lines.Sort((left, right) => left.CompareTo(right));


foreach (string s in lines)
{
Console.WriteLine($" {s}");
}

Nach der Sortierung kann die Liste der Zeichenfolgen mit einer Binärsuche durchsucht werden. Im folgenden
Beispiel wird gezeigt, wie Sie die sortierte Liste mit derselben Vergleichsfunktion durchsuchen. Die lokale
Funktion ShowWhere zeigt an, wo sich der gesuchte Text befindet oder befände, wenn er vorhanden wäre:
List<string> lines = new List<string>
{
@"c:\public\textfile.txt",
@"c:\public\textFile.TXT",
@"c:\public\Text.txt",
@"c:\public\testfile2.txt"
};
lines.Sort((left, right) => left.CompareTo(right));

string searchString = @"c:\public\TEXTFILE.TXT";


Console.WriteLine($"Binary search for <{searchString}>");
int result = lines.BinarySearch(searchString);
ShowWhere<string>(lines, result);

Console.WriteLine($"{(result > 0 ? "Found" : "Did not find")} {searchString}");

void ShowWhere<T>(IList<T> collection, int index)


{
if (index < 0)
{
index = ~index;

Console.Write("Not found. Sorts between: ");

if (index == 0)
Console.Write("beginning of sequence and ");
else
Console.Write($"{collection[index - 1]} and ");

if (index == collection.Count)
Console.WriteLine("end of sequence.");
else
Console.WriteLine($"{collection[index]}.");
}
else
{
Console.WriteLine($"Found at index {index}.");
}
}

Achten Sie darauf, zum Sortieren und Suchen von Zeichenfolgen immer dieselbe Vergleichsart zu verwenden.
Andernfalls werden unerwartete Ergebnisse angezeigt.
Auflistungsklassen wie System.Collections.Hashtable, System.Collections.Generic.Dictionary<TKey,TValue> und
System.Collections.Generic.List<T> verfügen über Konstruktoren, die einen System.StringComparer-Parameter
übernehmen, wenn der Typ des Elements oder der Schlüssel string ist. Im Allgemeinen sollten Sie nach
Möglichkeit diese Konstruktoren verwenden und entweder StringComparer.Ordinal oder
StringComparer.OrdinalIgnoreCase angeben.

Verweisgleichheit und Internalisieren von Zeichenfolgen


Bisher wurde für kein Beispiel ReferenceEquals verwendet. Diese Methode bestimmt, ob zwei Zeichenfolgen
dasselbe Objekt darstellen. Dies kann zu inkonsistenten Ergebnissen in Zeichenfolgenvergleichen führen. Im
folgenden Beispiel wird das C#-Feature Zeichenfolgeninternalisierung demonstriert. Wenn ein Programm zwei
oder mehr identische Zeichenfolgenvariablen deklariert, speichert der Compiler alle am selben Speicherort.
Durch Aufrufen der ReferenceEquals-Methode können Sie sehen, dass die beiden Zeichenfolgen tatsächlich auf
das gleiche Objekt im Arbeitsspeicher verweisen. Mit der String.Copy-Methode können Sie eine Internalisierung
verhindern. Nach der Erstellung der Kopie verfügen beide Zeichenfolgen über unterschiedliche Speicherorte,
obwohl sie denselben Wert besitzen. Wenn Sie das folgende Beispiel ausführen, werden die Zeichenfolgen a
und b internalisiert – für beide wird also derselbe Speicherort verwendet. Für die Zeichenfolgen a und c ist
dies hingegen nicht der Fall.
string a = "The computer ate my source code.";
string b = "The computer ate my source code.";

if (String.ReferenceEquals(a, b))
Console.WriteLine("a and b are interned.");
else
Console.WriteLine("a and b are not interned.");

string c = String.Copy(a);

if (String.ReferenceEquals(a, c))
Console.WriteLine("a and c are interned.");
else
Console.WriteLine("a and c are not interned.");

NOTE
Wenn Sie Zeichenfolgen auf Gleichheit überprüfen, sollten Sie Methoden verwenden, die explizit angeben, welche Art von
Vergleich Sie durchführen möchten. Dadurch kann Ihr Code viel besser verwaltet und gelesen werden. Verwenden Sie
dabei die überladenen Methoden der Klassen System.String und System.Array, die einen StringComparison-
Enumerationsparameter akzeptieren. Dadurch geben Sie die Art des Vergleichs an, der ausgeführt werden soll. Vermeiden
Sie die Verwendung der Operatoren == und != , wenn Sie Zeichenfolgen auf Gleichheit überprüfen. Die
String.CompareTo-Instanzmethoden führen immer einen Ordinalvergleich unter Berücksichtigung der Groß-
/Kleinschreibung durch. Sie eignen sich in erster Linie für die Sortierung von Zeichenfolgen in alphabetischer Reihenfolge.

Sie können eine Zeichenfolge in den Internpool schreiben oder auf eine im Internpool vorhandene Zeichenfolge
verweisen, indem Sie die String.Intern-Methode aufrufen. Um zu bestimmen, ob eine Zeichenfolge im Internpool
vorhanden ist, rufen Sie die String.IsInterned-Methode auf.

Siehe auch
System.Globalization.CultureInfo
System.StringComparer
Zeichenfolgen
Vergleichen von Zeichenfolgen
Globalisieren und Lokalisieren von Anwendungen
Abfangen einer Nicht-CLS-Ausnahme
04.11.2021 • 2 minutes to read

Einige .NET-Sprachen, einschließlich C++/CLI, erlauben Objekten, Ausnahmen auszulösen, die nicht aus
Exception stammen. Diese Ausnahmen werden als Nicht-CLS-Ausnahmen oder Nicht-Ausnahmen bezeichnet. In
C# können Sie keine Nicht-CLS-Ausnahmen auslösen. Sie können sie jedoch auf zwei Arten abfangen:
Innerhalb eines catch (RuntimeWrappedException e) -Blocks
In der Standardeinstellung fängt eine Visual C#-Assembly Nicht-CLS-Ausnahmen als umschlossene
Ausnahmen ab. Verwenden Sie diese Methode, wenn Sie Zugriff auf die ursprüngliche Ausnahme
benötigen, auf die über die Eigenschaft RuntimeWrappedException.WrappedException zugegriffen
werden kann. Weiter unten in diesem Thema wird erläutert, wie Abfragen auf diese Art und Weise
abgefangen werden können.
Innerhalb eines allgemeinen Catch-Blocks (ein Catch-Block ohne angegebenen Ausnahmetyp), der nach
allen anderen catch -Blocks eingefügt wird
Verwenden Sie diese Methode, wenn Sie eine Aktion (z.B. das Schreiben in eine Protokolldatei) als
Reaktion auf Nicht-CLS-Ausnahmen ausführen möchten und Sie keinen Zugriff auf die
Ausnahmeinformationen benötigen. Standardmäßig umschließt die Common Language Runtime alle
Ausnahmen. Um dieses Verhalten zu deaktivieren, fügen Sie dieses Attribut auf Assemblyebene Ihrem
Code hinzu, in der Regel in die Datei „AssemblyInfo.cs“:
[assembly: RuntimeCompatibilityAttribute(WrapNonExceptionThrows = false)] .

So fangen Sie eine Nicht-CLS -Ausnahme ab


Greifen Sie in einem catch(RuntimeWrappedException e) -Block über die Eigenschaft
RuntimeWrappedException.WrappedException auf die ursprüngliche Ausnahme zu.

Beispiel
Im folgenden Beispiel wird gezeigt, wie Sie eine Nicht-CLS-Ausnahme abfangen, die von einer in C++/CLI
geschriebenen Klassenbibliothek ausgelöst wurde. Beachten Sie, dass in diesem Beispiel der C#-Clientcode im
Voraus weiß, dass der Ausnahmetyp, der ausgelöst wird, System.String lautet. Wandeln Sie die Eigenschaft
RuntimeWrappedException.WrappedException in den ursprünglichen Typ um, solange dieser Typ über Ihren
Code erreicht werden kann.
// Class library written in C++/CLI.
var myClass = new ThrowNonCLS.Class1();

try
{
// throws gcnew System::String(
// "I do not derive from System.Exception!");
myClass.TestThrow();
}
catch (RuntimeWrappedException e)
{
String s = e.WrappedException as String;
if (s != null)
{
Console.WriteLine(s);
}
}

Siehe auch
RuntimeWrappedException
Ausnahmen und Ausnahmebehandlung
.NET Compiler Platform-SDK
04.11.2021 • 6 minutes to read

Compiler erstellen bei der Validierung der Syntax und Semantik eines Anwendungscodes ein detailliertes
Modell davon. Basierend auf diesem Modell wird die ausführbare Ausgabe aus dem Quellcode erstellt. Zugriff
auf dieses Modell wird durch das .NET Compiler Platform-SDK ermöglicht. Um die Produktivität zu steigern,
greifen wir vermehrt auf Funktionen der integrierten Entwicklungsumgebung (IDE) zurück, wie IntelliSense,
Refactoring, intelligentes Umbenennen, „Alle Verweise suchen“ und „Gehe zu Definition“. Zur Verbesserung
unserer Codequalität nehmen wir Codeanalysetools und zur Unterstützung der Anwendungsentwicklung
Codegeneratoren zu Hilfe. Je intelligenter diese Tools werden, desto umfangreicher wird auch der erforderliche
Zugriff auf das Modell, das nur von Compilern bei der Verarbeitung des Anwendungscodes erstellt wird. Genau
darin besteht das Hauptziel der Roslyn-APIs: Das Geheimnis um die Blackboxes zu lüften und Tools wie auch
Endbenutzern die Nutzung der umfassenden Informationen zu ermöglichen, die bezüglich unseres Codes in den
Compilern enthalten sind. Anstatt eine nicht transparente Übersetzung von Quellcode in Objektcode
bereitzustellen, werden Compiler durch Roslyn zu Plattformen: APIs, die Sie für codebezogene Aufgaben in Ihren
Tools und Anwendungen einsetzen können.

.NET Compiler Platform-SDK – Begriffe


Das .NET Compiler Platform-SDK vereinfacht die Erstellung von codeabhängigen Tools und Anwendungen um
ein Vielfaches. Dieses schafft viele Möglichkeiten für Innovationen in Bereichen wie Metaprogrammierung,
Codegenerierung und -transformation, die interaktive Verwendung der Programmiersprachen C# und Visual
Basic und die Einbettung von C# und Visual Basic in domänenspezifische Sprachen.
Mit dem .NET Compiler Platform-SDK können Sie Analysetools und Codefehlerbehebungen erstellen, die
Programmierungsfehler finden und korrigieren. _ Analysetools* analysieren die Syntax (Struktur des Codes) und
Semantiken, um Praktiken zu erkennen, die korrigiert werden sollten. Codefehlerbehebungen stellen
empfohlene Updates zur Behebung von Codierungsfehlern bereit, die von Analysetools und Compilerdiagnosen
gefunden werden. In der Regel werden ein Analysetool und die zugehörigen Codefehlerbehebungen in ein
einzelnes Projekt gepackt.
Analysetools und Codefehlerbehebungen verwenden zur Codeanalyse statische Analysen. Sie führen den Code
nicht aus oder bieten andere Vorteile in Bezug auf Tests. Sie können jedoch auf Verfahren hinweisen, die häufig
zu Fehlern, zu Problemen bei der Codeverwaltung oder zu Verstößen gegen Standardrichtlinien führen.
In Ergänzung zu Analysetools und Codefehlerbehebungen ermöglicht Ihnen das .NET Compiler Platform-SDK,
Coderefactorings zu erstellen. Es stellt eine einzelne Gruppe von APIs zur Verfügung, mit denen Sie eine C#-
oder Visual Basic-Codebasis untersuchen und analysieren können. Die Verwendung einer einzelnen Codebasis
ermöglicht ein einfacheres Schreiben von Analysetools und Codefehlerbehebungen, indem die Syntax- und
Semantikanalyse-APIs des .NET Compiler Platform-SDK genutzt werden. Durch den Wegfall der umfangreichen
Aufgabe, die vom Compiler durchgeführte Analyse zu replizieren, können Sie sich auf die spezifischere Aufgabe
konzentrieren, häufige Codierungsfehler für Ihr Projekt oder Ihre Bibliothek zu finden und zu beheben.
Ein weiterer nachrangiger Vorteil: Beim Laden in Visual Studio sind Ihre Analysetools und
Codefehlerbehebungen kleiner und belegen deutlich weniger Speicher, als wenn Sie eine eigene Codebasis für
die Analyse des Codes in einem Projekt schreiben würden. Da dieselben Klassen verwendet werden, die beim
Compiler und in Visual Studio zum Einsatz kommen, können Sie eigene statische Analysetools erstellen. Dies
bedeutet, dass Ihr Team Analysetools und Codefehlerbehebungen ohne spürbare Auswirkungen auf die Leistung
von IDE verwenden kann.
Für das Schreiben von Analysetools und Codefehlerbehebungen gibt es drei Hauptszenarien:
1. Erzwingen von Codierungsstandards im Team
2. Bereitstellen von Leitfäden mit Bibliothekspaketen
3. Bereitstellen allgemeiner Leitfäden

Erzwingen von Codierungsstandards im Team


Viele Teams verfügen über Codierungsstandards, die durch Code Reviews bei anderen Teammitgliedern
erzwungen werden. Die Effizienz dieses Prozesses kann durch Analysetools und Codefehlerbehebungen um ein
Vielfaches erhöht werden. Code Reviews erfolgen, wenn ein Entwickler seine Arbeit mit anderen Mitgliedern des
Teams teilt. In der Regel hat er viel Zeit in die Fertigstellung eines neuen Features investiert, bevor er
Kommentare erhält. Dabei können Wochen vergehen, in denen er Gewohnheiten stärkt, die nicht den Praktiken
des Teams entsprechen.
Während ein Entwickler Codes schreibt, werden Analysetools ausgeführt. Er erhält ein unmittelbares Feedback,
das ihn direkt dazu animiert, dem Leitfaden zu folgen. Sobald er mit der Prototyperstellung beginnt, schafft er
so Gewohnheiten zum Schreiben von konformen Codes. Wenn ein Review für das Feature durchgeführt werden
kann, wurden sämtliche Standardrichtlinien erzwungen.
Teams können Analysetools und Codefehlerbehebungen erstellen, die nach den am häufigsten verwendeten
Praktiken suchen, die gegen die Codierungspraktiken des Teams verstoßen. Diese können zur Erzwingung von
Standards auf jedem Entwicklungscomputer installiert werden.

TIP
Bevor Sie Ihr eigenes Analysetool erstellen, prüfen Sie die integrierten Tools. Weitere Informationen finden Sie unter
Codeformatregeln.

Bereitstellen von Leitfäden mit Bibliothekspaketen


NuGet bietet .NET-Entwicklern eine Vielzahl von Bibliotheken. Diese stammen von Microsoft, Drittanbietern
sowie von Communitymitgliedern und Freiwilligen. Diese Bibliotheken erhalten mehr Akzeptanz und höhere
Bewertungen, wenn Entwickler mit diesen Bibliotheken erfolgreich sein können.
Neben der Dokumentation können Sie Analysetools und Codefehlerbehebungen bereitstellen, die häufige
Verstöße gegen Ihre Bibliothek aufspüren und korrigieren. Diese unmittelbaren Korrekturen verhelfen
Entwickler schneller zum Erfolg.
Sie können Analysetools und Codefehlerbehebungen mit Ihrer Bibliothek in NuGet packen. In diesem Szenario
installiert jeder Entwickler, der Ihr NuGet-Paket installiert, auch das Paket des Analysetools. Alle Entwickler, die
Ihre Bibliothek verwenden, erhalten sofort Richtlinien von Ihrem Team in Form eines unmittelbaren Feedbacks
zu Fehlern und von Korrekturvorschlägen.

Bereitstellen von allgemeinen Codierungsleitfäden


Die .NET-Entwicklercommunity hat basierend auf ihrem Erfahrungsschatz Muster ermittelt, die gut
funktionieren, und solche, die am besten vermieden werden sollten. Mehrere Communitymitglieder haben
Analysetools erstellt, die diese empfohlenen Muster erzwingen. Mit fortschreitender Entwicklung haben wir
erfahren, dass dem Ideenreichtum keine Grenzen gesetzt sind.
Diese Analysetools können im Visual Studio Marketplace hochgeladen und von Entwicklern über Visual Studio
heruntergeladen werden. Benutzer, die mit der Sprache und Plattform noch nicht vertraut sind, lernen schnell
akzeptierte Praktiken und erzielen so früher Produktivität, während sie Erfahrungen mit der .NET-Sprache
sammeln. Mit der zunehmenden Verbreitung steigt auch die Wahrscheinlichkeit, dass diese Praktiken von der
Community übernommen werden.
Nächste Schritte
Das .NET Compiler Platform-SDK enthält die neuesten Sprachobjektmodelle für die Codegenerierung, die
Analyse und das Refactoring. Dieser Abschnitt enthält einen konzeptionellen Überblick über das .NET Compiler
Platform-SDK. Einzelheiten finden Sie in den Abschnitten „Schnellstarts“, „Beispiele“ und „Tutorials“.
In den folgenden fünf Themenbereichen erfahren Sie mehr über die Konzepte im .NET Compiler Platform-SDK:
Sehen Sie sich Code mit der Syntaxschnellansicht an.
Grundlegendes zum API-Modell von Compilern
Arbeiten mit der Syntax
Arbeiten mit der Semantik
Arbeiten mit einem Arbeitsbereich
Installieren Sie zunächst das SDK für die .NET Compiler Platform :

Installationsanweisungen: Visual Studio-Installer


Es gibt zwei verschiedene Möglichkeiten, das .NET Compiler Platform SDK im Visual Studio-Installer zu
finden:
Installation mithilfe des Visual Studio -Installers: Workloads im Überblick
Das .NET Compiler Platform SDK wird nicht automatisch als Teil der Workload „Visual Studio-
Extensionentwicklung“ ausgewählt. Sie müssen sie als optionale Komponente auswählen.
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Aktivieren Sie die Workload Visual Studio-Extensionentwicklung .
4. Öffnen Sie den Knoten Visual Studio-Extensionentwicklung in der Zusammenfassungsstruktur.
5. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an letzter Stelle
unter den optionalen Komponenten.
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Öffnen Sie den Knoten Einzelne Komponenten in der Zusammenfassungsstruktur.
2. Aktivieren Sie das Kontrollkästchen für den DGML-Editor .
Installation mithilfe des Visual Studio -Installers: Registerkarte „Einzelne Komponenten“
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Klicken Sie auf die Registerkarte Einzelne Komponenten .
4. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an oberster Stelle
im Abschnitt Compiler, Buildtools und Laufzeiten .
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Aktivieren Sie das Kontrollkästchen für den DGML-Editor . Sie finden es im Abschnitt Codetools .
Verstehen des .NET Compiler Platform SDK-Modells
04.11.2021 • 3 minutes to read

Compiler verarbeiten den von Ihnen geschriebenen Code, indem sie sich an strenge Regeln halten, die sich
häufig von der Weise unterscheiden, wie Menschen Code lesen und verstehen. Es ist wichtig, dass Sie über
grundlegende Kenntnisse zur Funktionsweise des vom Compiler verwendeten Modells verfügen, damit Sie ein
Verständnis über die APIs erlangen, die Sie zum Erstellen von Roslyn-basierten Tools verwenden.

Funktionsbereiche der Compilerpipeline


Das .NET Compiler Platform SDK stellt Ihnen als Consumer die Codeanalyse des Compilers für C# und Visual
Basic zur Verfügung, indem es eine API-Ebene bereitstellt, die eine traditionelle Compilerpipeline nachahmt.

Jede Phase dieser Pipeline stellt eine separate Komponente dar. Zunächst wird in der Analysephase Quelltext in
Token zerlegt und analysiert, sodass eine Syntax entsteht, die der Grammatik der Sprache entspricht. Dann
werden in der Deklarationsphase die Quelle und importierte Metadaten analysiert, sodass sie benannte
Symbole formen. In der Bindungsphase werden Bezeichner im Code Symbolen zugeordnet. Zuletzt werden in
der Ausgabephase Assemblys mit sämtlichen Informationen ausgegeben, die vom Compiler erstellt wurden.

Für jede dieser Phasen stellt das .NET Compiler Platform SDK ein Objektmodell zur Verfügung, das Zugriff auf
die Informationen zu der jeweiligen Phase gewährt. In der Analysephase wird eine Syntaxstruktur zur Verfügung
gestellt, in der Deklarationsphase eine hierarchische Symboltabelle und in der Bindungsphase das Ergebnis der
semantischen Analyse des Compilers. Die Ausgabephase besteht aus einer API, die IL-Bytecode erstellt.
Jeder Compiler vereint diese Komponenten zu einem End-to-End-Objekt.
Bei diesen APIs handelt es sich um dieselben, die von Visual Studio verwendet werden. Beispielsweise
verwenden die Features zur Codegliederung und -formatierung die Syntaxstrukturen. Der Objektkatalog und
die Navigationsfeatures verwenden die Symboltabelle, beim Refactoring und bei Zu Definition wechseln wird
das Semantikmodell verwendet, und Bearbeiten und For tfahren verwendet all diese Elemente, einschließlich
der Ausgabe-API.

API-Ebenen
Das .NET-Compiler-SDK besteht aus mehreren Ebenen von APIs: Compiler-APIs, Diagnose-APIs, Skript-APIs und
Arbeitsbereich-APIs.
Compiler-APIs
Die Compilerebene enthält die Objektmodelle, die mit den Informationen syntaktisch und semantisch
übereinstimmen, die in den einzelnen Phasen der Compilerpipeline zur Verfügung gestellt werden. Die
Compilerebene enthält außerdem eine nicht veränderbare Momentaufnahme eines einzelnen Aufrufs eines
Compilers, einschließlich Assemblyverweisen, Compileroptionen und Quellcodedateien. Es gibt zwei
unterschiedliche APIs, die die Sprachen C# und Visual Basic darstellen. Diese beiden APIs verfügen zwar über
eine ähnliche Form, sind aber jeweils auf einen hohen Treuegrad in Bezug auf die einzelnen Sprachen ausgelegt.
Diese Ebene ist unabhängig von Visual Studio-Komponenten.
Diagnose -APIs
Im Rahmen einer Analyse erstellt der Compiler möglicherweise mehrere Diagnosen, die angefangen bei Syntax,
Semantik und eindeutigen Zuweisungsfehlern bis hin zu Warnungen und informativen Diagnosen alles
abdecken. Die API-Ebene des Compilers stellt über eine erweiterbare API Diagnosen zur Verfügung, über die
benutzerdefinierte Analysetools an den Kompilierungsprozess angeschlossen werden. Außerdem können
dadurch neben vom Compiler definierten Diagnosen benutzerdefinierte Diagnosen erstellt werden, die
beispielsweise von Tools wie StyleCop erstellt werden. Wenn Diagnosen so erstellt werden, hat dies den Vorteil,
dass diese auf natürliche Weise in Tools wie MSBuild und Visual Studio integriert werden. Diese Tools sind von
Diagnosen abhängig, um z. B. einen Build basierend auf Richtlinien anzuhalten oder Wellenlinien direkt im
Editor anzuzeigen und Behebungen für Codefehler vorzuschlagen.
Erstellung von API -Skripts
Das Hosten und Scripting von APIs geschieht auf Compilerebene. Sie können sie verwenden, um
Codeausschnitte auszuführen und um Ausführungskontext zur Runtime zu sammeln. Die interaktive C#-REPL
(read–eval–print loop) verwendet diese APIs. Mithilfe der REPL können Sie C# als Skriptsprache verwenden und
den Code beim Schreiben interaktiv ausführen.
Arbeitsbereich-APIs
Die Arbeitsbereich-API ist auf Arbeitsbereichebene enthalten. Sie stellt einen Startpunkt für die Codeanalyse und
das Refactoring ganzer Projektmappen dar. Sie unterstützt Sie bei der Aufteilung der Informationen zu den
Projekten in einer Projektmappe in einzelne Objektmodelle, gewährt Ihnen direkten Zugriff auf die
Objektmodelle auf Compilerebene, ohne dabei Dateien zu analysieren, Optionen zu konfigurieren oder
Abhängigkeit zwischen einzelnen Projekten zu verwalten.
Außerdem befinden sich auf Arbeitsbereichsebene mehrere APIs, die bei der Implementierung von
Codeanalysen und beim Refactoring von Tools verwendet werden, die in einer Hostumgebung wie der Visual
Studio-IDE funktionieren. Nehmen Sie „Alle Verweise suchen“-, „Formatieren“- und Codegenerierungs-APIs als
Beispiele.
Diese Ebene ist unabhängig von Visual Studio-Komponenten.
Arbeiten mit der Syntax
04.11.2021 • 7 minutes to read

Die Syntaxstruktur ist eine grundlegende unveränderliche Datenstruktur, die von Compiler-APIs verfügbar
gemacht wird. Diese Strukturen stellen die lexikalische und syntaktische Struktur des Quellcodes dar. Sie erfüllen
zwei wichtige Aufgaben:
Damit Tools wie zum Beispiel eine IDE, Add-Ins, Codeanalysetools und Refactorings die syntaktische Struktur
von Quellcode in dem Projekt eines Benutzers anzeigen und verarbeiten können.
Damit Tools wie zum Beispiel Refactorings und eine IDE ermöglicht wird, Quellcode auf natürliche Weise zu
erstellen, zu ändern und neu anzuordnen, ohne den Text direkt zu bearbeiten. Durch Erstellen und Bearbeiten
von Strukturen können Tools einfach Quellcode erstellen und neu anordnen.

Syntaxstrukturen
Syntaxstrukturen sind die primär verwendete Struktur für Kompilierung, Codeanalyse, Bindung, Refactoring,
IDE-Funktionen und Codegenerierung. Kein Teil des Quellcodes kann verstanden werden, ohne zunächst als
eines von vielen bekannten strukturellen Sprachelementen identifiziert und kategorisiert worden zu sein.
Syntaxstrukturen verfügen über drei wichtige Attribute:
Sie enthalten alle Quellinformationen in voller Genauigkeit. Volle Genauigkeit bedeutet: Die Syntaxstruktur
enthält jede Information aus dem Quelltext, jedes grammatische Konstrukt, jedes lexikalische Token und alles,
was dazwischen liegt – einschließlich Leerzeichen, Kommentaren und Präprozessordirektiven. Zum Beispiel
wird jedes Literal, das in der Quelle erwähnt wird, so dargestellt, wie es eingegeben wurde. Syntaxstrukturen
erfassen auch Fehler im Quellcode, wenn das Programm unvollständig oder falsch formatiert ist, indem
übersprungene oder fehlende Token angezeigt werden.
Sie können den exakten Text, aus dem sie analysiert wurden, hervorrufen. Es ist möglich, die im Knoten
verankerte Textdarstellung der Teilstruktur aus einem beliebigen Syntaxknoten zu erhalten. Diese Fähigkeit
bedeutet, dass Syntaxstrukturen verwendet werden können, um Quelltext zu erstellen und zu bearbeiten.
Durch das Erstellen einer Struktur erstellen Sie gleichzeitig auch den entsprechenden Text, und durch das
Erstellen einer neuen Struktur anhand von Änderungen einer vorhandenen Struktur nehmen Sie Änderungen
am Text vor.
Sie sind unveränderlich und threadsicher. Nach dem Erstellen ist eine Struktur eine Momentaufnahme des
aktuellen Zustands des Codes, die sich nie ändert. Dadurch können mehrere Benutzer gleichzeitig mit der
selben Syntaxstruktur in verschiedenen Threads interagieren, ohne etwas sperren oder duplizieren zu
müssen. Da Strukturen unveränderlich sind, und keine direkten Änderungen an ihnen vorgenommen werden
können, sind Factorymethoden hilfreich beim Erstellen und Bearbeiten von Syntaxstrukturen, weil sie
zusätzliche Momentaufnahmen von der Struktur erstellen. Strukturen sind bei der Wiederverwendung von
grundlegenden Knoten effizient, weshalb eine neue Version schnell und mit geringem zusätzlichem
Speicherplatz neu erstellt werden kann.
Eine Syntaxstruktur ist eine Datenstruktur, in der nichtterminale Strukturelemente anderen Elementen
übergeordnet sind. Jede Syntaxstruktur besteht aus Knoten, Token und Trivia.

Syntaxknoten
Syntaxknoten gehören zu den Hauptelementen der Syntaxstrukturen. Diese Knoten stellen Syntaxkonstrukte wie
Deklarationen, Anweisungen, Klauseln und Ausdrücke dar. Jede Kategorie der Syntaxknoten wird durch eine
separate Klasse dargestellt, die von Microsoft.CodeAnalysis.SyntaxNode abgeleitet wird. Die Anzahl von
Knotenklassen ist nicht erweiterbar.
Alle Syntaxknoten sind nichtterminale Knoten in der Syntaxstruktur, was bedeutet, dass ihnen immer andere
Knoten und Token untergeordnet sind. Als untergeordnetes Element eines anderen Knotens hat jeder Knoten
einen übergeordneten Knoten, auf den mit der Eigenschaft SyntaxNode.Parent zugegriffen werden kann. Da
Knoten und Strukturen unveränderlich sind, ändert sich das übergeordnete Element eines Knotens nie. Das
übergeordnete Element des Stamms der Struktur ist NULL.
Jeder Knoten verfügt über die Methode SyntaxNode.ChildNodes(), die die untergeordneten Knoten sequenziell,
je nach ihrer Position im Quelltext, auflistet. Diese Auflistung enthält keine Token. Alle Knoten verfügen auch
über Methoden zum Untersuchen von Nachfolgern, wie DescendantNodes, DescendantTokens oder
DescendantTrivia, die eine Liste aller Knoten, Token oder Trivia darstellen, die in der Unterstruktur vorhanden
sind, die von dem Knoten abstammt.
Darüber hinaus macht jede Unterklasse der Syntaxknoten alle identischen untergeordneten Elemente durch
stark typisierte Eigenschaften verfügbar. Zum Beispiel verfügt eine Knotenklasse BinaryExpressionSyntax über
drei zusätzliche Eigenschaften, die auf binäre Operatoren beschränkt sind: Left, OperatorToken und Right. Der
Typ von Left und Right ist ExpressionSyntax, und der Typ von OperatorToken ist SyntaxToken.
Einige Syntaxknoten haben optionale untergeordnete Elemente. Zum Beispiel hat IfStatementSyntax ein
optionales ElseClauseSyntax. Die Eigenschaft gibt NULL zurück, wenn das untergeordnete Element nicht
vorhanden ist.

Syntaxtoken
Syntaxtoken sind terminale Elemente der Grammatik und repräsentieren die kleinsten syntaktischen Elemente
des Codes. Sie sind nie übergeordnete Elemente von anderen Knoten oder Token. Syntaxtoken bestehen aus
Schlüsselwörtern, Bezeichnern, Literalen und Interpunktion.
Der Typ SyntaxToken ist zugunsten der Effizienz ein CLR-Werttyp. Im Gegensatz zu Syntaxknoten gibt es deshalb
nur eine Struktur für alle Arten von Token mit verschiedenen Eigenschaften, deren Bedeutung von der Art des
dargestellten Tokens abhängt.
Zum Beispiel stellt ein Token für ein Integer-Literal einen numerischen Wert dar. Zusätzlich zu dem
unformatierten Quelltext den das Token umfasst, verfügt das Literal-Token über eine Value-Eigenschaft, die den
genauen Ganzzahlwert dekodiert angibt. Diese Eigenschaft wird mit Object typisiert, weil sie eine von vielen
primitiven Typen sein kann.
Die Eigenschaft ValueText gibt die selben Informationen an wie die Eigenschaft Value an, sie wird jedoch immer
als String typisiert. Ein Bezeichner in einem C#-Quelltext kann Escapezeichen in Unicode enthalten, aber die
Syntax der Escapesequenz selbst zählt nicht als Teil des Namens des Bezeichners. Obwohl der unformatierte
Text, den das Token umfasst, die Escapesequenz einschließt, tut die Eigenschaft ValueText das nicht. Stattdessen
enthält sie die Unicode-Zeichen, die von dem Escapezeichen identifiziert werden. Wenn der Quelltext zum
Beispiel einen Bezeichner enthält, der als \u03C0 geschrieben wird, dann gibt die Eigenschaft ValueText dieses
Tokens π zurück.

Syntaxtrivia
Syntaxtrivia stellen hauptsächlich die Bestandteile des Quelltexts dar, die für das normale Verständnis des Codes
nicht wichtig sind, z.B. Leerzeichen, Kommentare und Präprozessordirektiven. Syntaxtrivia sind, wie Syntaxtoken,
Werttypen. Der einzelne Typ Microsoft.CodeAnalysis.SyntaxTrivia wird dazu verwendet, alle Arten von Trivia zu
beschreiben.
Trivia werden nicht als untergeordnetes Element eines Knotens in der Syntaxstruktur eingeschlossen, weil sie
kein Teil der normalen Sprachsyntax sind, und überall zwischen zwei Token auftauchen können. Dennoch sind
sie als Teil der Syntaxstruktur vorhanden, weil sie beim Implementieren einer Funktion, wie dem Refactoring und
der genauen Beibehaltung des Quelltexts, wichtig sind.
Sie können auf Trivia zugreifen, indem Sie die Sammlungen SyntaxToken.LeadingTrivia oder
SyntaxToken.TrailingTrivia eines Tokens überprüfen. Triviasequenzen werden beim Analysieren von Quelltext den
Token zugeordnet. Im Allgemeinen sind einem Token alle darauffolgenden Trivia in der selben Zeile bis zum
nächsten Token zugeordnet. Alle Trivia nach dieser Zeile sind dem nächsten Token zugeordnet. Dem ersten Token
in der Quelldatei werden alle anfänglichen Trivia zugeordnet, und die letzte Triviasequenz der Datei wird dem
Dateiende-Token zugewiesen, das andernfalls eine Breite von 0 (null) hat.
Im Gegensatz zu Syntaxknoten und Token haben Syntaxtrivia keine übergeordneten Elemente. Da sie Teil der
Struktur sind und jeweils einem einzelnen Token zugeordnet sind, können Sie auf dieses Token mit der
SyntaxTrivia.Token-Eigenschaft zugreifen.

Span-Eigenschaften
Alle Knoten, Token und Trivia kennen ihre Position im Quelltext und die Anzahl der von ihnen enthaltenen
Zeichen. Eine Position im Text wird von einer 32-Bit-Ganzzahl, also einem nullbasierten char -Index, dargestellt.
Ein TextSpan-Objekt ist eine Anfangsposition und eine Anzahl von Zeichen, die jeweils beide als ganze Zahl
dargestellt werden. Wenn TextSpan die Länge 0 (null) hat, bezieht es sich auf eine Position zwischen zwei
Zeichen.
Jeder Knoten verfügt über zwei TextSpan-Eigenschaften: Span und FullSpan.
Die Eigenschaft Span bezeichnet die Textspanne vom ersten Token in der Unterstruktur des Knotens bis zum
Ende des letzten Tokens. Diese Spanne umfasst keine führenden oder nachgestellten Trivia.
Die Eigenschaft FullSpan bezeichnet die Textspanne, die die normale Spanne des Knotens und die Spannen
jeglicher führenden oder nachgestellten Trivia enthält.
Zum Beispiel:

if (x > 3)
{
|| // this is bad
|throw new Exception("Not right.");| // better exception?||
}

Der Anweisungsknoten im Block verfügt über eine Spanne, die von einzelnen senkrechten Strichen (|)
ausgezeichnet wird. Sie umfasst die Zeichen throw new Exception("Not right."); . Die vollständige Spanne wird
mit doppelten senkrechten Strichen (||) ausgezeichnet. Sie enthält die gleichen Zeichen wie die Spanne und
Zeichen, die den führenden und nachfolgenden Trivia zugeordnet sind.

Kind-Eigenschaften
Alle Knoten, Token und Trivia über die Eigenschaft SyntaxNode.RawKind vom Typ System.Int32, die das genaue
Syntax-Element identifiziert, das dargestellt wird. Dieser Wert kann in eine sprachspezifische Enumeration
umgewandelt werden. Jede Programmiersprache (C# oder Visual Basic) verfügt über eine einzelne SyntaxKind -
Enumeration (Microsoft.CodeAnalysis.CSharp.SyntaxKind bzw. Microsoft.CodeAnalysis.VisualBasic.SyntaxKind),
die alle möglichen Knoten, Token und Trivia in der Grammatik auflistet. Diese Konvertierung kann automatisch
erfolgen, indem Sie auf die Erweiterungsmethoden CSharpExtensions.Kind oder VisualBasicExtensions.Kind
zugreifen.
Die RawKind-Eigenschaft ermöglicht einfache Mehrdeutigkeitsvermeidung für Syntaxknotentypen, die die
gleiche Knotenklasse nutzen. Für Token und Trivia ist diese Eigenschaft die einzige Möglichkeit, verschiedene
Elementtypen voneinander zu unterscheiden.
Angenommen, eine einzelne BinaryExpressionSyntax-Klasse verfügt über die untergeordneten Elemente Left,
OperatorToken und Right. Dann unterscheidet die Kind-Eigenschaft, ob es sich um einen Syntaxknoten mit
AddExpression, SubtractExpression oder MultiplyExpression handelt.

TIP
Es wird empfohlen, die Arten mithilfe der Erweiterungsmethoden IsKind (für C#) oder IsKind (für VB) zu überprüfen.

Fehler
Auch wenn der Quelltext Syntaxfehler enthält, wird eine vollständige Syntaxstruktur verfügbar gemacht, die auf
die Quelle zurückführbar ist. Wenn der Parser auf Code trifft, der nicht der definierten Syntax der
Programmiersprache entspricht, nutzt er eines von zwei Verfahren, um eine Syntaxstruktur zu erstellen:
Wenn der Parser eine bestimmte Art von Token erwartet, es aber nicht vorhanden ist, kann er dort, wo er
das Token erwartet hat, ein fehlendes Token in die Syntaxstruktur einfügen. Ein fehlendes Token stellt das
Token dar, das vom Parser erwartet wurde, es besitzt jedoch eine leere Span-Eigenschaft, und die
zugehörige SyntaxNode.IsMissing-Eigenschaft gibt true an.
Der Parser kann Token überspringen, bis er ein Token findet, bei dem er mit der Analyse fortfahren kann.
Wenn das der Fall ist, werden die übersprungenen Token als Triviaknoten mit der Eigenschaft
SkippedTokensTrivia angefügt.
Arbeiten mit der Semantik
04.11.2021 • 3 minutes to read

Syntaxstrukturen stellen die lexikalische und syntaktische Struktur des Quellcodes dar. Diese Informationen
reichen zwar schon aus, um alle Deklarationen und die Logik in der Quelle zu beschreiben, jedoch reichen sie
nicht aus, um Verweise zu ermitteln. Ein Name kann Folgendes darstellen:
einen Typ
ein Feld
eine Methode
eine lokale Variable
Zwar sind diese Elemente alle eindeutig, jedoch sind häufig tiefgreifende Kenntnisse über die Sprachregeln
erforderlich, um zu bestimmen, auf welches Element ein Bezeichner verweist.
Im Quellcode werden Programmelemente dargestellt. Außerdem können Programme auf zuvor erstellte
Bibliotheken verweisen, die in Assemblydateien gepackt wurden. Zwar ist kein Quellcode für Assemblys
verfügbar, und daher auch keine Syntaxknoten oder -strukturen, jedoch können Programme auf Elemente
innerhalb der Assemblys verweisen.
Für Aufgaben wie diese benötigen Sie das Semantikmodell .
Neben einem syntaktischen Modell des Quellcodes kapselt ein Semantikmodell die Sprachregeln, sodass Sie
Bezeichner problemlos dem richtigen Programmelement zuordnen können, auf das verwiesen wird.

Kompilierung
Bei einer Kompilierung handelt es sich um eine Darstellung sämtlicher Elemente, die zum Kompilieren eines C#-
oder Visual Basic-Programms benötigt werden. Dies umfasst alle Assemblyverweise, Compileroptionen und
Quelldateien.
Da all diese Informationen an demselben Ort gespeichert sind, können die Elemente, die im Quellcode enthalten
sind, detaillierter beschrieben werden. Die Kompilierung stellt sämtliche deklarierten Typen, Member oder
Variablen als verschiedene Symbole dar. Sie enthält verschiedene Methoden, die Sie dabei unterstützen,
Symbole, die entweder im Quellcode deklariert sind oder als Metadaten aus einer Assembly importiert wurden,
zu finden und Beziehungen dazu herzustellen.
Ähnlich wie Syntaxstrukturen sind Kompilierungen unveränderlich. Nachdem Sie eine Kompilierung erstellt
haben, kann diese weder von Ihnen noch von anderen Personen verändert werden, mit denen Sie sie teilen.
Allerdings können Sie eine neue Kompilierung aus einer bereits vorhandenen erstellen und dabei Änderungen
vornehmen. So können Sie z.B. eine Kompilierung erstellen, die einer bereits vorhandenen Kompilierung
weitestgehend entspricht, aber eine zusätzliche Quelldatei oder einen zusätzlichen Quellverweis enthält.

Symbole
Ein Symbol stellt ein eindeutiges Element dar, das vom Quellcode deklariert wird oder aus einer Assembly als
Metadatenelement importiert wird. Jeder einzelne Namespace, Typ, Parameter, jede einzelne Methode,
Eigenschaft, lokale Variable und jedes einzelne Feld oder Ereignis wird von einem eigenen Symbol dargestellt.
Mithilfe verschiedener Methoden und Eigenschaften des Compilation-Typs können Sie Symbole finden.
Beispielsweise können Sie ein Symbol für einen deklarierten Typ über einen allgemeinen Metadatennamen
finden. Außerdem können Sie auf die gesamte Symboltabelle als Symbolstruktur zugreifen, deren Stamm der
globale Namespace ist.
Außerdem enthalten Symbole zusätzliche Informationen, die der Compiler über die Quelle oder über Metadaten
wie andere Symbole bestimmt, auf die verwiesen wird. Jede Art von Symbol wird von einer separaten
Schnittstelle dargestellt, die von ISymbol abgeleitet wird. Dabei verfügt jedes Symbol über eigene Methoden
und Eigenschaften, die die vom Compiler erfassten Informationen enthalten. Viele dieser Eigenschaften
enthalten direkte Verweise auf andere Symbole. Beispielsweise verweist die Eigenschaft
IMethodSymbol.ReturnType auf das tatsächliche Symbol, das von der Methode zurückgegeben wird.
Symbole stehen für eine allgemeine Darstellungen von Namespaces, Typen und Mitgliedern zwischen Quellcode
und Metadaten. Beispielsweise werden sowohl Methoden, die im Quellcode deklariert wurden, als auch
Methoden, die aus Metadaten importiert wurden, von einem IMethodSymbol mit denselben Eigenschaften
dargestellt.
Symbole haben ein ähnliches Konzept wie CLR-Typsysteme, die von der System.Reflection-API dargestellt
werden. Allerdings sind sie umfangreicher, da sie mehr als nur Typen modellieren. Namespaces, lokale Variablen
und Bezeichnungen sind Symbole. Außerdem stellen Symbole keine CLR-Konzepte, sondern Sprachkonzepte
dar. Es gibt also einige Überlappungen, aber es werden auch wichtige Unterschiede deutlich. Beispielsweise stellt
eine Iteratormethode in C# oder Visual Basic ein einzelnes Symbol dar. Wenn die Iteratormethode allerdings in
CLR-Metadaten übersetzt, handelt es sich dann um einen Typ und mehrere Methoden.

Semantikmodell
In einem Semantikmodell werden alle semantischen Informationen einer einzelnen Quelldatei dargestellt. Sie
können es verwenden, um Folgendes zu ermitteln:
Die Symbole, auf die an einer bestimmten Stelle in der Quelle verwiesen wird.
Der resultierende Typ eines beliebigen Ausdrucks.
Alle Diagnosen, bei denen es sich um Fehler oder Warnungen handelt.
Die Art und Weise, wie Variablen in bestimmte Bereiche der Quelle hinein und wieder aus ihnen heraus
fließen.
Antworten auf Fragen mit spekulativem Charakter.
Arbeiten mit einem Arbeitsbereich
04.11.2021 • 2 minutes to read

Die Ebene des Arbeitsbereichs stellt einen Startpunkt für die Codeanalyse und das Refactoring ganzer
Projektmappen dar. Auf dieser Ebene unterstützt Sie die Arbeitsbereich-API bei der Aufteilung der Informationen
zu den Projekten in einer Projektmappe in einzelne Objektmodelle. Sie gewährt Ihnen direkten Zugriff auf die
Objektmodelle auf Compilerebene wie Quelltext, Syntaxstrukturen, Semantikmodelle und Kompilierungen, ohne
dabei Dateien zu analysieren, Optionen zu konfigurieren oder Abhängigkeit zwischen einzelnen Projekten zu
verwalten.
Hostumgebungen wie eine IDE stellen einen Arbeitsbereich für zugehörige geöffnete Projektmappen zur
Verfügung. Außerdem kann dieses Modell außerhalb einer IDE verwendet werden, wenn Sie eine
Projektmappendatei laden.

Arbeitsbereich
Bei einem Arbeitsbereich handelt es sich um eine aktive Darstellung Ihrer Projektmappe als Auflistung von
Projekten, die alle Auflistungen von Dokumenten enthalten. Arbeitsbereiche werden in der Regel an
Hostumgebungen gebunden, die sich stetig verändern, während ein Benutzer eine Eingabe macht oder
Eigenschaften verändert.
Der Workspace stellt Zugriff auf das aktuelle Projektmappenmodell zur Verfügung. Wenn in der Hostumgebung
eine Änderung vorgenommen wird, löst der Arbeitsbereich passende Ereignisse aus, und die
Workspace.CurrentSolution-Eigenschaft wird aktualisiert. Wenn z.B. der Benutzer in einem Text-Editor eine
Eingabe macht, die einem der Quelldokumente entspricht, verwendet der Arbeitsbereich ein Ereignis, um zu
signalisieren, dass das gesamte Modell einer Projektmappe geändert wurde und welches Dokument geändert
wurde. Sie können dann mit diesen Änderungen umgehen, indem Sie das neue Modell auf Richtigkeit prüfen,
wobei wichtige Bereiche hervorgehoben werden oder Sie Vorschläge für Codeänderungen machen.
Außerdem können Sie eigenständige Arbeitsbereiche erstellen, deren Verbindung mit der Hostumgebung
getrennt wird, oder die in einer Anwendung ohne Hostumgebung verwendet werden.

Lösungen, Projekte und Dokumente


Obwohl Arbeitsbereiche sich jedes Mal verändern können, wenn eine Taste gedrückt wird, können Sie mit einem
separaten Modell der Projektmappe arbeiten.
Bei einer Projektmappe handelt es sich um ein nicht veränderbares Modell des Projekts und der Dokumente.
Das bedeutet, dass das Modell ohne Sperren oder Duplikate freigegeben werden kann. Sobald Sie eine
Projektmappeninstanz von der Workspace.CurrentSolution-Eigenschaft erhalten haben, wird diese Instanz nicht
mehr verändert. Trotzdem können Sie wie bei Syntaxstrukturen und Kompilierungen Projektmappen verändern,
indem Sie neue Instanzen basierend auf bereits vorhandenen Projektmappen und bestimmten Änderungen
erstellen. Damit der Arbeitsbereich Ihre Änderungen anzeigt, müssen Sie die veränderte Projektmappe explizit
auf den Arbeitsbereich anwenden.
Projekte sind Bestandteile des gesamten nicht veränderbaren Projektmappenmodells. Sie stellen alle
Quellcodedokumente, Analysen und Kompilierungsoptionen sowie Assemblyverweise und Verweise zwischen
den einzelnen Projekten dar. Über ein Projekt können Sie auf die zugehörige Kompilierung zugreifen. Dabei
müssen Sie keine Projektabhängigkeiten festlegen oder Quelldateien analysieren.
Dokumente sind ebenfalls Bestandteile des gesamten nicht veränderbaren Projektmappenmodells. Dokumente
stellen einzelne Quelldateien dar, über die Sie auf den Text in der Datei, die Syntaxstruktur und das
Semantikmodell zugreifen können.
Im folgenden Diagramm wird deutlich, in welcher Beziehung der Arbeitsbereich zu der Hostumgebung und den
Tools steht und wie Bearbeitungen vorgenommen werden.

Zusammenfassung
Roslyn stellt mehrere Compiler-APIs und Arbeitsbereich-APIs zur Verfügung, die umfangreiche Informationen
über Ihren Quellcode zur Verfügung stellen und uneingeschränkt mit den Sprachen C# und Visual Basic
zusammenarbeiten. Das .NET Compiler Platform SDK vereinfacht die Erstellung von codeabhängigen Tools und
Anwendungen um ein Vielfaches. Dieses schafft viele Möglichkeiten für Innovationen in Bereichen wie
Metaprogrammierung, Codegenerierung und -transformation, die interaktive Verwendung der
Programmiersprachen C# und Visual Basic und die Einbettung von C# und Visual Basic in domänenspezifische
Sprachen.
Untersuchen von Code mit der Roslyn-
Syntaxschnellansicht in Visual Studio Code
04.11.2021 • 9 minutes to read

Dieser Artikel enthält eine Übersicht über das Syntaxschnellansichtstool, das als Teil des .NET Compiler Platform
(„Roslyn“) SDK geliefert wird. Die Syntaxschnellansicht ist ein Toolfenster, das Ihnen beim Prüfen und
Untersuchen von Syntaxstrukturen hilft. Es ist ein grundlegendes Tool, um die Modelle für den Code zu
verstehen, die Sie analysieren möchten. Es ist auch eine Hilfe beim Debuggen, wenn Sie Ihre eigenen
Anwendungen unter Verwendung des .NET Compiler Platform („Roslyn“) SDK entwickeln. Öffnen Sie dieses Tool,
wenn Sie Ihre ersten Analysetools erstellen. Die Schnellansicht hilft Ihnen, die von den APIs verwendeten
Modelle zu verstehen. Sie können auch Tools wie SharpLab oder LINQPad verwenden, um Code zu untersuchen
und Syntaxstrukturen zu verstehen.

Installationsanweisungen: Visual Studio-Installer


Es gibt zwei verschiedene Möglichkeiten, das .NET Compiler Platform SDK im Visual Studio-Installer zu
finden:
Installation mithilfe des Visual Studio -Installers: Workloads im Überblick
Das .NET Compiler Platform SDK wird nicht automatisch als Teil der Workload „Visual Studio-
Extensionentwicklung“ ausgewählt. Sie müssen sie als optionale Komponente auswählen.
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Aktivieren Sie die Workload Visual Studio-Extensionentwicklung .
4. Öffnen Sie den Knoten Visual Studio-Extensionentwicklung in der Zusammenfassungsstruktur.
5. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an letzter Stelle
unter den optionalen Komponenten.
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Öffnen Sie den Knoten Einzelne Komponenten in der Zusammenfassungsstruktur.
2. Aktivieren Sie das Kontrollkästchen für den DGML-Editor .
Installation mithilfe des Visual Studio -Installers: Registerkarte „Einzelne Komponenten“
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Klicken Sie auf die Registerkarte Einzelne Komponenten .
4. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an oberster Stelle
im Abschnitt Compiler, Buildtools und Laufzeiten .
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Aktivieren Sie das Kontrollkästchen für den DGML-Editor . Sie finden es im Abschnitt Codetools .
Lesen Sie den Artikel zur Übersicht, und machen Sie sich mit den Konzepten vertraut, die im .NET Compiler
Platform SDK verwendet werden. Er gibt eine Übersicht über Syntaxstrukturen, Knoten, Tokens und Trivia.

Syntaxschnellansicht
Die Syntaxschnellansicht ermöglicht die Untersuchung der Syntaxstruktur für die C#- oder Visual Basic-
Codedatei im aktuellen aktiven Editor-Fenster in der integrierten Entwicklungsumgebung (IDE) von Visual
Studio. Die Schnellansicht kann gestartet werden, indem Sie auf Ansicht > Andere Fenster >
Syntaxschnellansicht klicken. Sie können auch die Schnellstar t -Symbolleiste in der oberen rechten Ecke
verwenden. Geben Sie „syntax“ ein, und der Befehl zum Öffnen der Syntaxschnellansicht sollte erscheinen.
Dieser Befehl öffnet die Syntaxschnellansicht als unverankertes Toolfenster. Wenn Sie kein Code-Editor-Fenster
geöffnet haben, ist die Anzeige leer, wie in der folgenden Abbildung dargestellt.

Docken Sie dieses Toolfenster an einem geeigneten Ort innerhalb von Visual Studio an, z.B. an die linke Seite.
Die Schnellansicht zeigt Informationen über die aktuelle Codedatei.
Erstellen Sie ein neues Projekt mit dem Befehl Datei > Neues Projekt . Sie können ein Visual Basic- oder ein
C#-Projekt erstellen. Wenn Visual Studio die Haupt-Codedatei für dieses Projekt öffnet, zeigt die Schnellansicht
die entsprechende Syntaxstruktur an. Sie können eine beliebige C#-/Visual Basic-Datei in dieser Instanz von
Visual Studio öffnen, und die Schnellansicht zeigt die Syntaxstruktur dieser Datei an. Wenn Sie mehrere
Codedateien in Visual Studio geöffnet haben, zeigt die Schnellansicht die Syntaxstruktur für die derzeit aktive
Codedatei (die Codedatei, die über den Tastaturfokus verfügt) an.

C#
Visual Basic
Wie in den vorherigen Abbildungen dargestellt wird, zeigt das Schnellansichts-Toolfenster die Syntaxstruktur
oben und ein Eigenschaftenraster unten an. Das Eigenschaftenraster zeigt die Eigenschaften des Elements an,
das derzeit in der Struktur ausgewählt ist, einschließlich .NET-Typ und Art (SyntaxKind) des Elements.
Syntaxstrukturen umfassen drei Typen von Elementen: Knoten, Token, und Trivia. Sie können mehr über diese
Typen im Artikel zum Arbeiten mit Syntax erfahren. Elemente der einzelnen Typen werden mit einer bestimmten
Farbe dargestellt. Klicken Sie für einen Überblick über die verwendeten Farben auf die Schaltfläche „Legende“.
Jedes Element in der Struktur zeigt außerdem seinen eigenen Bereich an. Der Bereich umfasst die Indizes
(Start- und Endposition) des Knotens in der Textdatei. Im vorherigen C#-Beispiel hat das ausgewählte Token
„UsingKeyword [0..5)“ einen Bereich , der fünf Zeichen breit ist, [0..5). Die Notation „[..)“ bedeutet, dass der
Startindex Teil des Bereichs ist, der Endindex allerdings nicht.
Es gibt zwei Möglichkeiten, in der Struktur zu navigieren:
Erweitern oder klicken Sie auf Elemente in der Struktur. Die Schnellansicht wählt automatisch den
entsprechenden Text aus, der zum Bereich des Elements im Code-Editor gehört.
Klicken Sie auf oder wählen Sie Text im Code-Editor. Wenn Sie im vorangegangenen Visual Basic-Beispiel die
Zeile mit „Module Module1“ im Code-Editor auswählen, navigiert die Schnellansicht automatisch zum
entsprechenden ModuleStatement-Knoten in der Struktur.
Die Schnellansicht markiert das Element in der Struktur, dessen Bereich am besten zum Bereich des im Editor
ausgewählten Texts passt.
Die Schnellansicht aktualisiert die Struktur, um Änderungen in der aktiven Codedatei abzubilden. Fügen Sie
einen Aufruf von Console.WriteLine() innerhalb von Main() hinzu. Während der Eingabe aktualisiert die
Schnellansicht die Struktur.
Unterbrechen Sie die Eingabe, nachdem Sie Console. eingegeben haben. Die Struktur besitzt einige rosa
gefärbte Elemente. Zu diesem Zeitpunkt bestehen Fehler (auch als „Diagnose“ bezeichnet) in dem eingegebenen
Code. Diese Fehler hängen mit Knoten, Tokens und Trivia in der Syntaxstruktur zusammen. Die Schnellansicht
zeigt Ihnen, welche Elemente fehlerbehaftet sind, indem sie den Hintergrund rosa markiert. Sie können die
Fehler an jedem rosa gefärbten Element überprüfen, indem Sie mit dem Mauszeiger auf das Element zeigen. Die
Schnellansicht zeigt nur syntaktische Fehler an (das sind Fehler im Zusammenhang mit der Syntax des
eingegebenen Codes); sie zeigt keine semantischen Fehler an.
Syntaxdiagramme
Klicken Sie mit der rechten Maustaste auf ein beliebiges Element in der Struktur, und klicken Sie auf
Gerichtetes Syntax-Diagramm anzeigen .

C#
Visual Basic

Die Schnellansicht zeigt eine grafische Darstellung des Teilbaums, der von dem ausgewählten Element
abstammt. Versuchen Sie diese Schritte für den MethodDeclaration -Knoten, der der Main() -Methode im C#-
Beispiel entspricht. Die Schnellansicht zeigt ein Syntaxdiagramm an, das wie folgt aussieht:

Der Syntaxdiagramm-Viewer verfügt über eine Option zum Anzeigen einer Legende für sein Farbschema. Sie
können auch mit dem Mauszeiger auf einzelne Elemente im Syntaxdiagramm zeigen, um die Eigenschaften des
jeweiligen Elements anzuzeigen.
Sie können Syntaxdiagramme für verschiedene Elemente in der Struktur wiederholt anzeigen lassen. Die
Diagramme werden immer im gleichen Fenster innerhalb von Visual Studio angezeigt. Sie können dieses
Fenster an einem geeigneten Ort innerhalb von Visual Studio andocken, damit Sie zum Anzeigen eines neuen
Syntaxdiagramms nicht zwischen Registerkarten hin- und herwechseln müssen. Der untere Bereich, unter dem
Fenster des Code-Editors, bietet sich oft an.
So sieht das Docking-Layout für die Verwendung mit dem Schnellansichts-Toolfenster und dem
Syntaxdiagramm aus:
Bei einem Setup mit zwei Monitoren kann das Syntaxdiagramm auch auf einen zweiten Monitor gelegt werden.

Überprüfen der Semantik


Die Syntaxschnellansicht ermöglicht die rudimentäre Überprüfung von Symbolen und semantischen
Informationen. Geben Sie double x = 1 + 1; innerhalb von Main() im C#-Beispiel ein. Wählen Sie dann den
Ausdruck 1 + 1 im Code-Editor-Fenster. Die Schnellansicht hebt den AddExpression -Knoten in der
Schnellansicht hervor. Klicken Sie mit der rechten Maustaste auf AddExpression , und klicken Sie dann auf
View Symbol (if any) (Symbol anzeigen (sofern vorhanden)). Beachten Sie, dass die meisten Menüelemente
über den Qualifizierer „if any“ (sofern vorhanden) verfügen. Die Syntaxschnellansicht untersucht Eigenschaften
eines Knotens, einschließlich Eigenschaften, die möglicherweise nicht bei allen Knoten vorhanden sind.
Das Eigenschaftenraster in der Schnellansicht wird wie in der folgenden Abbildung gezeigt aktualisiert: Das
Symbol für den Ausdruck ist ein SynthesizedIntrinsicOperatorSymbol mit Kind = Method .

Versuchen Sie View TypeSymbol (if any) (TypeSymbol anzeigen (sofern vorhanden)) für den gleichen
AddExpression -Knoten. Das Eigenschaftenraster in der Schnellansicht wird wie in der folgenden Abbildung
aktualisiert und zeigt an, dass der Typ des gewählten Ausdrucks Int32 ist.
Versuchen Sie View Conver ted TypeSymbol (if any) (Konvertiertes TypeSymbol anzeigen (sofern
vorhanden)) für den gleichen AddExpression -Knoten. Das aktualisierte Eigenschaftenraster zeigt an, dass zwar
der Typ des Ausdrucks Int32 ist, der konvertierte Typ des Ausdrucks aber Double ist, wie in der folgenden
Abbildung gezeigt. Dieser Knoten beinhaltet konvertierte TypeSymbol-Eigenschaften, da der Int32 -Ausdruck in
einem Kontext erscheint, in dem er zu Double konvertiert werden muss. Diese Konvertierung erfüllt den
Double -Typ für die Variable x auf der linken Seite des Zuweisungsoperators.

Versuchen Sie als Letztes View Constant Value (if any) (Konstanten Wert anzeigen (sofern vorhanden)) für
den gleichen AddExpression -Knoten. Das Eigenschaftenraster zeigt an, dass der Wert des Ausdrucks eine
kompilierte Zeitkonstante mit dem Wert 2 ist.
Das vorherige Beispiel kann auch in Visual Basic repliziert werden. Geben Sie Dim x As Double = 1 + 1 in einer
Visual Basic-Datei ein. Wählen Sie den Ausdruck 1 + 1 im Code-Editor-Fenster. Die Schnellansicht hebt den
entsprechenden AddExpression -Knoten in der Schnellansicht hervor. Wiederholen Sie die vorherigen Schritte
für AddExpression , und Sie sollten identische Ergebnisse erhalten.
Untersuchen Sie weiteren Code in Visual Basic. Aktualisieren Sie Ihre Visual Basic-Hauptdatei mit dem folgenden
Code:

Imports C = System.Console

Module Program
Sub Main(args As String())
C.WriteLine()
End Sub
End Module

Dieser Code führt einen Alias mit dem Namen C ein, der dem Typ System.Console am Anfang der Datei
zugeordnet ist und verwendet diesen Alias in Main() . Wählen Sie die Verwendung dieses Alias aus, der C in
C.WriteLine() , innerhalb der Main() -Methode. Die Schnellansicht wählt den entsprechenden
IdentifierName -Knoten in der Schnellansicht aus. Klicken Sie mit der rechten Maustaste auf diesen Knoten,
und klicken Sie auf Symbol anzeigen (sofern vorhanden) . Das Eigenschaftenraster zeigt an, dass dieser
Bezeichner an den Typ System.Console gebunden ist, wie in der nachfolgenden Abbildung gezeigt:
Versuchen Sie View AliasSymbol (if any) (AliasSymbol anzeigen (sofern vorhanden)) für den gleichen
IdentifierName -Knoten. Das Eigenschaftenraster zeigt an, dass der Bezeichner ein Alias mit dem Namen C ist,
der an das Ziel System.Console gebunden ist. Mit anderen Worten: Das Eigenschaftenraster liefert
Informationen in Bezug auf das AliasSymbol , das zum Bezeichner C gehört.

Untersuchen Sie das Symbol auf deklarierten Typ, Methode, Eigenschaft. Wählen Sie den entsprechenden
Knoten in der Schnellansicht aus, und klicken Sie auf Symbol anzeigen (sofern vorhanden) . Wählen Sie die
Methode Sub Main() , einschließlich des Textkörpers der Methode. Klicken Sie auf Symbol anzeigen (sofern
vorhanden) für den entsprechenden SubBlock -Knoten in der Schnellansicht. Das Eigenschaftenraster zeigt,
dass das MethodSymbol für diesen SubBlock den Namen Main mit Rückgabetyp Void hat.
Die oben genannten Visual Basic-Beispiele können problemlos in C# reproduziert werden. Geben Sie
using C = System.Console; anstelle von Imports C = System.Console für den Alias ein. Die vorhergehenden
Schritte in C# ergeben identische Ergebnisse im Schnellansichtsfenster.
Semantische Überprüfungsvorgänge sind nur an Knoten verfügbar. Sie sind nicht an Tokens oder Trivia
verfügbar. Nicht alle Knoten haben relevante semantische Informationen, die überprüft werden können. Wenn
ein Knoten keine relevanten semantischen Informationen hat, wird beim Klicken auf Symbol * anzeigen
(sofern vorhanden) ein leeres Eigenschaftenraster angezeigt.
Sie können mehr über APIs für die semantische Analyse im Übersichtsdokument Arbeiten mit der Semantik
erfahren.

Schließen der Syntaxschnellansicht


Sie können das Schnellansichtsfenster schließen, wenn Sie es nicht zum Untersuchen von Quellcode verwenden.
Die Syntaxschnellansicht aktualisiert ihre Darstellung, während Sie durch Code navigieren und ihn bearbeiten
oder verändern. Das kann störend sein, wenn die Schnellansicht nicht verwendet wird.
Quellcode-Generatoren
04.11.2021 • 5 minutes to read

Dieser Artikel bietet eine Übersicht über Quellcode-Generatoren, die als Teil des .NET Compiler Platform
(„Roslyn“) SDK bereitgestellt werden. Quellcode-Generatoren sind ein Feature des C#-Compilers, mit dem C#-
Entwickler den Code des Benutzers während der Kompilierung untersuchen und neue C#-Quelldateien
dynamisch generieren können, die der Kompilierung des Benutzers hinzugefügt werden.
Ein Quellcode-Generator ist ein Teil des Codes, der während der Kompilierung ausgeführt wird und Ihr
Programm untersuchen kann, um zusätzliche Quelldateien zu erzeugen, die zusammen mit dem Rest Ihres
Codes kompiliert werden.
Ein Quellcode-Generator ist eine neue Art von Komponente, die C#-Entwickler schreiben und mit der Sie zwei
wichtige Aufgaben ausführen können:
1. Abrufen eines Kompilierungsobjekts, das den gesamten Benutzercode darstellt, der gerade kompiliert
wird. Dieses Objekt kann untersucht werden, und Sie können Code schreiben, der mit den Syntax- und
semantischen Modellen für den zu kompilierenden Code arbeitet, genau wie bei heutigen Analysetools.
2. Generieren Sie C#-Quelldateien, die einem Kompilierungsobjekt während der Kompilierung hinzugefügt
werden können. Mit anderen Worten können Sie zusätzlichen Quellcode als Eingabe für eine
Kompilierung bereitstellen, während der Code kompiliert wird.
In der Kombination sind es diese beiden Aspekte, die Quellcode-Generatoren so nützlich machen. Sie können
Benutzercode mit all den reichhaltigen Metadaten untersuchen, die der Compiler während der Kompilierung
generiert, und dann C#-Code in dieselbe Kompilierung zurückgeben, die auf den von Ihnen analysierten Daten
basiert! Wenn Sie mit Roslyn-Analysetools vertraut sind, können Sie sich Quellcode-Generatoren als
Analysetools vorstellen, die C#-Quellcode ausgeben können.
Quellcode-Generatoren werden in einer unten dargestellten Kompilierungsphase ausgeführt:

Ein Quellcode-Generator ist eine .NET Standard 2.0-Assembly, die vom Compiler zusammen mit allen
Analysetools geladen wird. Er lässt sich in Umgebungen einsetzen, in denen .NET Standard-Komponenten
geladen und ausgeführt werden können.

Häufige Szenarios
Aktuell gibt es drei allgemeine Ansätze zur Untersuchung von Benutzercode und zum Generieren von
Informationen oder Code auf Grundlage dieser Analyse, die von den heutigen Technologien verwendet werden:
Reflexion zur Laufzeit, IL Weaving und Jonglieren mit MSBuild-Tasks. Quellcode-Generatoren können eine
Verbesserung gegenüber den einzelnen Ansätzen sein. Die Reflexion zur Laufzeit ist eine leistungsstarke
Technologie, die .NET vor langer Zeit hinzugefügt wurde. Es gibt unzählige Szenarien für ihren Einsatz. Ein sehr
gängiges Szenario ist die Analyse des Benutzercodes beim Starten einer App und die Verwendung dieser Daten,
um etwas zu generieren.
ASP.NET Core verwendet beispielsweise Reflexion, wenn Ihr Webdienst zum ersten Mal ausgeführt wird, um
Konstrukte zu erkennen, die Sie definiert haben, damit er Elemente wie Controller und Razor Pages miteinander
verbinden kann. Obwohl Sie auf diese Weise einfachen Code mit leistungsstarken Abstraktionen schreiben
können, geht dies zur Laufzeit mit einer Leistungseinbuße einher: Wenn Ihr Webdienst oder Ihre App zum
ersten Mal gestartet wird, kann er/sie keine Anforderungen empfangen, bis der gesamte Code für die Reflexion
zur Laufzeit, der Informationen über Ihren Code ermittelt, ausgeführt wurde! Obwohl diese Leistungseinbuße
nicht enorm ist, handelt es sich um eine Art Fixkosten, die Sie in Ihrer eigenen App nicht selbst beeinflussen
können.
Mit einem Quellcode-Generator kann die Phase der Ermittlung des Controllers beim Start stattdessen zur
Kompilierzeit erfolgen, indem Ihr Quellcode analysiert und der Code generiert wird, der zum Einrichten Ihrer
App benötigt wird. Dies kann zu schnelleren Startzeiten führen, da eine Aktion, die heute zur Laufzeit stattfindet,
in die Kompilierzeit verlagert werden kann. Quellcode-Generatoren können die Leistung auf eine Weise
verbessern, die sich nicht auf Reflexion zur Laufzeit beschränkt, um auch Typen zu ermitteln. In einigen
Szenarien wird die MSBuild-C#-Aufgabe (genannt CSC) mehrfach aufgerufen, um Daten aus einer Kompilierung
zu prüfen. Wie Sie sich vorstellen können, wirkt sich der mehrfache Aufruf des Compilers auf die Gesamtzeit
aus, die für die Erstellung Ihrer App benötigt wird! Wir untersuchen, wie Quellcode-Generatoren eingesetzt
werden können, um das Jonglieren mit MSBuild-Aufgaben wie dieser zu vermeiden, da Quellcode-Generatoren
nicht nur einige Leistungsvorteile bieten, sondern es auch Tools ermöglichen, auf der richtigen
Abstraktionsebene zu arbeiten.
Eine weitere Fähigkeit von Quellcode-Generatoren ist die Vermeidung der Verwendung einiger APIs des Typs
„String“, z. B. wie das ASP.NET Core-Routing zwischen Controllern und Razor Pages funktioniert. Mit einem
Quellcode-Generator kann das Routing stark typisiert werden, wobei die erforderlichen Zeichenfolgen als Detail
zur Kompilierzeit generiert werden. Dies würde die Anzahl der Fälle reduzieren, in denen eine falsch
eingegebene Zeichenfolge dazu führt, dass eine Anforderung nicht den richtigen Controller erreicht.

Erste Schritte mit Quellcode-Generatoren


In diesem Leitfaden untersuchen Sie die Erstellung eines Quellcode-Generators mithilfe der ISourceGenerator-
API.
1. Erstellen einer .NET-Konsolenanwendung
2. Ersetzen Sie die Klasse „Program“ durch folgenden Code:

partial class Program


{
static void Main(string[] args)
{
HelloFrom("Generated Code");
}

static partial void HelloFrom(string name);


}

NOTE
Sie können dieses Beispiel so ausführen, wie es ist, aber es wird noch nichts passieren.
3. Als Nächstes erstellen wir einen Quellcode-Generator, der den Inhalt der HelloFrom -Methode auffüllt.
4. Erstellen Sie ein .NET Standard-Bibliotheksprojekt, das wie folgt aussieht:

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all"
/>
</ItemGroup>

</Project>

5. Ändern oder erstellen Sie eine C#-Datei, die Ihren eigenen Quellcode-Generator wie folgt angibt:

using Microsoft.CodeAnalysis;

namespace MyGenerator
{
[Generator]
public class MySourceGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Code generation goes here
}

public void Initialize(GeneratorInitializationContext context)


{
// No initialization required for this one
}
}
}

6. Ersetzen Sie den Inhalt der Execute -Methode, sodass sie wie folgt aussieht:
public void Execute(GeneratorExecutionContext context)
{
// find the main method
var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);

// build up the source code


string source = $@"
using System;

namespace {mainMethod.ContainingNamespace.ToDisplayString()}
{{
public static partial class {mainMethod.ContainingType.Name}
{{
static partial void HelloFrom(string name)
{{
Console.WriteLine($""Generator says: Hi from '{{name}}'"");
}}
}}
}}
";
// add the source code to the compilation
context.AddSource("generatedSource", source);
}

7. Wir verfügen nun über einen funktionierenden Generator, müssen ihn aber mit unserer
Konsolenanwendung verbinden. Bearbeiten Sie das ursprüngliche Konsolenanwendungsprojekt, und
fügen Sie Folgendes hinzu. Ersetzen Sie dabei den Projektpfad durch den Pfad im .NET Standard-Projekt,
das Sie zuvor erstellt haben:

<!-- Add this as a new ItemGroup, replacing paths and names appropriately -->
<ItemGroup>
<!-- Note that this is not a "normal" ProjectReference.
It needs the additional 'OutputItemType' and 'ReferenceOutputAssembly' attributes. -->
<ProjectReference Include="path-to-sourcegenerator-project.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

8. Wenn Sie nun die Konsolenanwendung ausführen, sollten Sie sehen, dass der generierte Code
ausgeführt und auf dem Bildschirm ausgegeben wird.

NOTE
Derzeit müssen Sie Visual Studio neu starten, um IntelliSense zu sehen und Fehler dank der frühzeitigen
Verwendung der Tools zu beseitigen.

Nächste Schritte
Im Source Generators Cookbook werden einige dieser Beispiele mit empfohlenen Lösungsansätzen behandelt.
Zusätzlich bieten wir eine Reihe von Beispielen auf GitHub, die Sie selbst ausprobieren können.
Weitere Informationen zu Quellcode-Generatoren finden Sie in folgenden Themen:
Entwurfsdokument „Source Generators“
Source Generators Cookbook
Erste Schritte mit der Syntaxanalyse
04.11.2021 • 13 minutes to read

In diesem Tutorial lernen Sie die Syntax-API kennen. Die Syntax-API bietet Zugriff auf die Datenstrukturen, die
ein C#- oder Visual Basic-Programm beschreiben. Diese Datenstrukturen sind so detailliert, dass sie ein
Programm jeder Größe vollständig darstellen können. Diese Strukturen können komplette Programme
beschreiben, die sich fehlerfrei kompilieren und ausführen lassen. Sie können auch unvollständige Programme
beschreiben, während Sie diese im Editor schreiben.
Um diese umfangreiche Ausdrucksmöglichkeit zu unterstützen, müssen die Datenstrukturen und APIs, aus
denen die Syntax-API besteht, notwendigerweise komplex sein. Als Einstieg sehen wir uns die Datenstruktur für
ein typisches Hello World-Programm an:

using System;
using System.Collections.Generic;
using System.Linq;

namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

Sehen Sie sich den Text des oben stehenden Programms an. Sie werden bekannte Elemente erkennen. Der
gesamte Text repräsentiert eine einzelne Quelldatei, eine Kompilierungseinheit . Die ersten drei Zeilen dieser
Quelldatei sind using-Direktiven . Der Rest der Quelle ist in einer Namespacedeklaration enthalten. Die
Namespacedeklaration enthält eine untergeordnete Klassendeklaration . Die Klassendeklaration enthält eine
Methodendeklaration .
Die Syntax-API erstellt eine Baumstruktur, deren Stamm die Kompilierungseinheit repräsentiert. Knoten in der
Struktur repräsentieren die using-Direktiven, die Namespacedeklaration und alle anderen Elemente des
Programms. Die Baumstruktur setzt sich bis zur untersten Ebene fort: Die Zeichenfolge „Hello World!“ ist ein
Token für ein Zeichenfolgenliteral , das eine Ableitung eines Arguments ist. Die Syntax-API bietet Zugriff
auf die Struktur des Programms. Sie können Abfragen nach bestimmten Codemethoden erstellen, die gesamte
Struktur schrittweise durchlaufen, um den Code zu verstehen und durch Modifizieren der vorhandenen Struktur
neue Strukturen erstellen.
Diese kurze Beschreibung bietet einen Überblick über die Art der Informationen, auf die Sie mithilfe der Syntax-
API zugreifen können. Die Syntax-API ist nichts anderes als eine formale API, die die vertrauten Codekonstrukte
beschreibt, die Sie aus C# kennen. Sie bietet Informationen zur Formatierung des Codes, z.B. zu
Zeilenumbrüchen, Leerräumen und Einzügen. Mit diesen Informationen können Sie den Code genau so
darstellen, wie er von Programmierern oder dem Compiler geschrieben und gelesen wird. Dank dieser Struktur
gewinnt der Quellcode eine ganz neue Bedeutungsebene, auf der Sie interagieren können. Sie sehen nicht nur
Zeichenfolgen, sondern Daten, die die Struktur eines C#-Programms repräsentieren.
Installieren Sie zunächst das SDK für die .NET Compiler Platform :
Installationsanweisungen: Visual Studio-Installer
Es gibt zwei verschiedene Möglichkeiten, das .NET Compiler Platform SDK im Visual Studio-Installer zu
finden:
Installation mithilfe des Visual Studio -Installers: Workloads im Überblick
Das .NET Compiler Platform SDK wird nicht automatisch als Teil der Workload „Visual Studio-
Extensionentwicklung“ ausgewählt. Sie müssen sie als optionale Komponente auswählen.
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Aktivieren Sie die Workload Visual Studio-Extensionentwicklung .
4. Öffnen Sie den Knoten Visual Studio-Extensionentwicklung in der Zusammenfassungsstruktur.
5. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an letzter Stelle
unter den optionalen Komponenten.
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Öffnen Sie den Knoten Einzelne Komponenten in der Zusammenfassungsstruktur.
2. Aktivieren Sie das Kontrollkästchen für den DGML-Editor .
Installation mithilfe des Visual Studio -Installers: Registerkarte „Einzelne Komponenten“
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Klicken Sie auf die Registerkarte Einzelne Komponenten .
4. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an oberster Stelle
im Abschnitt Compiler, Buildtools und Laufzeiten .
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Aktivieren Sie das Kontrollkästchen für den DGML-Editor . Sie finden es im Abschnitt Codetools .

Grundlegendes zu Syntaxstrukturen
Verwenden Sie die Syntax-API zur Analyse der Struktur von C#-Code. Die Syntax-API stellt die Parser, die
Syntaxstrukturen und die Hilfsprogramme zur Verfügung, die Sie zum Analysieren und Konstruieren von
Syntaxstrukturen benötigen. So können Sie im Code nach bestimmten Syntaxelementen suchen oder den Code
für ein Programm lesen.
Eine Syntaxstruktur ist eine Datenstruktur, die von den C#- und Visual Basic-Compilern verwendet wird, um C#-
und Visual Basic-Programme zu verstehen. Syntaxstrukturen werden durch denselben Parser erzeugt, der
ausgeführt wird, wenn ein Projekt erstellt wird oder ein Entwickler die Taste F5 drückt. Die Syntaxstrukturen
weisen vollständige Datentreue mit der Sprache auf; jedes Informationselement in einer Codedatei wird in der
Struktur dargestellt. Das Schreiben einer Syntaxstruktur als Text reproduziert exakt den ursprünglichen Text, der
analysiert wurde. Syntaxstrukturen sind zudem unveränderlich – nach dem Erstellen kann eine Syntaxstruktur
nicht mehr geändert werden. Consumer der Strukturen können die Strukturen ohne Sperren oder andere
Parallelitätsmaßnahmen in mehreren Threads analysieren, weil sie wissen, dass sich die Daten niemals ändern.
Sie können APIs verwenden, um neue Strukturen zu erstellen, die durch Modifizieren einer vorhandenen
Struktur entstehen.
Syntaxstrukturen bestehen aus den folgenden vier primären Bausteinen:
Die Microsoft.CodeAnalysis.SyntaxTree-Klasse: Eine Instanz dieser Klasse repräsentiert eine vollständige
Analysestruktur. SyntaxTree ist eine abstrakte Klasse mit sprachspezifischen Ableitungen. Sie verwenden die
Analysemethoden der Klasse Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree (oder
Microsoft.CodeAnalysis.VisualBasic.VisualBasicSyntaxTree), um Text in C# (oder Visual Basic) zu analysieren.
Die Microsoft.CodeAnalysis.SyntaxNode-Klasse: Instanzen dieser Klasse repräsentieren syntaktische
Konstrukte wie Deklarationen, Anweisungen, Klauseln und Ausdrücke.
Die Microsoft.CodeAnalysis.SyntaxToken-Struktur: Diese Struktur repräsentiert ein einzelnes Schlüsselwort
oder Interpunktionszeichen oder einen einzelnen Bezeichner oder Operator.
Die Microsoft.CodeAnalysis.SyntaxTrivia-Struktur: Diese Struktur repräsentiert syntaktisch unwichtige
Informationen wie Leerräume zwischen Token, Vorverarbeitungsdirektiven und Kommentare.
Trivia, Token und Knoten werden hierarchisch zusammengestellt und bilden eine Struktur, die alle Elemente in
einem Visual Basic- oder C#-Codefragment vollständig repräsentiert. Sie können diese Struktur im Fenster
Syntaxschnellansicht anzeigen. Wählen Sie in Visual Studio Ansicht > Weitere Fenster >
Syntaxschnellansicht aus. Die oben gezeigte C#-Quelldatei sieht in der Syntaxschnellansicht ungefähr wie
folgt aus:
SyntaxNode : Blau | SyntaxToken : Grün | SyntaxTrivia : Rot

Durch Navigieren in dieser Struktur lässt sich jede Anweisung, jeder Ausdruck, jedes Token und jeder Leerraum
in einer Codedatei auffinden.
Sie können zwar in einer Codedatei mithilfe der Syntax-APIs alles finden, in den meisten Szenarien müssen Sie
jedoch kleine Codeausschnitte untersuchen oder nach bestimmten Anweisungen oder Fragmenten suchen. Die
beiden folgenden Beispiele zeigen typische Vorgehensweisen beim Durchsuchen der Codestruktur oder beim
Suchen nach einzelnen Anweisungen.

Durchlaufen von Strukturen


Sie können die Knoten in einer Syntaxstruktur auf zwei Arten untersuchen. Sie können die Struktur durchlaufen
und jeden einzelnen Knoten untersuchen oder Abfragen für bestimmte Elemente oder Knoten ausführen.
Manuelles Durchlaufen
Den fertig gestellten Code für dieses Beispiel finden Sie in unserem GitHub-Repository.

NOTE
Die Syntaxstrukturtypen verwenden Vererbung, um die verschiedenen Syntaxelemente zu beschreiben, die an
verschiedenen Positionen im Programm gültig sind. Bei der Verwendung dieser APIs müssen häufig Eigenschaften oder
Sammlungsmember in bestimmte abgeleitete Typen umgewandelt werden. In den folgenden Beispielen sind die
Zuweisung und die Umwandlung separate Anweisungen, bei denen explizit typisierte Variablen verwendet werden. Sie
können den Code lesen, um die Rückgabetypen der API und den Laufzeittyp der zurückgegebenen Objekte zu sehen. In
der Praxis ist es eher üblich, implizit typisierte Variablen zu verwenden und die Typen der zu untersuchenden Objekte
mithilfe von API-Namen zu beschreiben.

Erstellen Sie ein neues Stand-Alone Code Analysis Tool -Projekt für C#:
Wählen Sie in Visual Studio Datei > Neu > Projekt aus, um das Dialogfeld „Neues Projekt“ anzuzeigen.
Wählen Sie unter Visual C# > Er weiterbarkeit die Option Stand-Alone Code Analysis Tool aus.
Nennen Sie Ihr Projekt SyntaxTreeManualTraversal , und klicken Sie auf „OK“.
Sie werden das einfache Hello World!- Programm analysieren, das weiter oben in diesem Artikel gezeigt wurde.
Fügen Sie den Text für das Hello World-Programm als Konstante in Ihre Program -Klasse ein:

const string programText =


@"using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
}";

Anschließend fügen Sie den folgenden Code hinzu, um die Syntaxstruktur für den Codetext in der
programText -Konstante zu erstellen. Fügen Sie Ihrer Main -Methode die folgende Zeile hinzu:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);


CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Diese beiden Zeilen erstellen die Struktur und rufen den Stammknoten dieser Struktur ab. Jetzt können Sie die
Knoten in der Struktur untersuchen. Fügen Sie diese Zeilen zu Ihrer Main -Methode hinzu, um einige der
Eigenschaften des Stammknotens in der Struktur anzuzeigen:
WriteLine($"The tree is a {root.Kind()} node.");
WriteLine($"The tree has {root.Members.Count} elements in it.");
WriteLine($"The tree has {root.Usings.Count} using statements. They are:");
foreach (UsingDirectiveSyntax element in root.Usings)
WriteLine($"\t{element.Name}");

Führen Sie die Anwendung aus, um festzustellen, was Ihr Code in Zusammenhang mit dem Stammknoten in
dieser Struktur erkannt hat.
In der Regel würden Sie die Struktur durchlaufen, um mehr über den Code zu erfahren. In diesem Beispiel
analysieren Sie bekannten Code, um die APIs zu erkunden. Fügen Sie folgenden Code hinzu, um den ersten
Member des root -Knotens zu untersuchen:

MemberDeclarationSyntax firstMember = root.Members[0];


WriteLine($"The first member is a {firstMember.Kind()}.");
var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;

Dieser Member ist eine Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax. Er repräsentiert


alle Elemente im Bereich der namespace HelloWorld -Deklaration. Fügen Sie folgenden Code hinzu, um zu
ermitteln, welche Knoten im HelloWorld -Namespace deklariert sind:

WriteLine($"There are {helloWorldDeclaration.Members.Count} members declared in this namespace.");


WriteLine($"The first member is a {helloWorldDeclaration.Members[0].Kind()}.");

Führen Sie das Programm aus, um das Ergebnis anzuzeigen.


Da Sie jetzt wissen, dass die Deklaration eine Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax ist,
deklarieren Sie eine neue Variable dieses Typs, um die Klassendeklaration zu untersuchen. Diese Klasse enthält
nur einen Member: die Main -Methode. Fügen Sie folgenden Code hinzu, um die Main -Methode zu suchen und
in eine Microsoft.CodeAnalysis.CSharp.Syntax.MethodDeclarationSyntax umzuwandeln.

var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];


WriteLine($"There are {programDeclaration.Members.Count} members declared in the
{programDeclaration.Identifier} class.");
WriteLine($"The first member is a {programDeclaration.Members[0].Kind()}.");
var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members[0];

Der Knoten mit der Methodendeklaration enthält alle syntaktischen Informationen zu der Methode. Jetzt zeigen
wir den Rückgabetyp der Main -Methode, die Anzahl und Typen der Argumente und den Textkörper der
Methode an. Fügen Sie den folgenden Code hinzu:

WriteLine($"The return type of the {mainDeclaration.Identifier} method is {mainDeclaration.ReturnType}.");


WriteLine($"The method has {mainDeclaration.ParameterList.Parameters.Count} parameters.");
foreach (ParameterSyntax item in mainDeclaration.ParameterList.Parameters)
WriteLine($"The type of the {item.Identifier} parameter is {item.Type}.");
WriteLine($"The body text of the {mainDeclaration.Identifier} method follows:");
WriteLine(mainDeclaration.Body.ToFullString());

var argsParameter = mainDeclaration.ParameterList.Parameters[0];

Führen Sie das Programm aus, um alle Informationen anzuzeigen, die Sie zu diesem Programm ermittelt haben:
The tree is a CompilationUnit node.
The tree has 1 elements in it.
The tree has 4 using statements. They are:
System
System.Collections
System.Linq
System.Text
The first member is a NamespaceDeclaration.
There are 1 members declared in this namespace.
The first member is a ClassDeclaration.
There are 1 members declared in the Program class.
The first member is a MethodDeclaration.
The return type of the Main method is void.
The method has 1 parameters.
The type of the args parameter is string[].
The body text of the Main method follows:
{
Console.WriteLine("Hello, World!");
}

Abfragemethoden
Zusätzlich zum Durchlaufen von Strukturen können Sie die Syntaxstruktur auch mithilfe der in
Microsoft.CodeAnalysis.SyntaxNode definierten Abfragemethoden untersuchen. Diese Methoden werden Ihnen
vertraut vorkommen, wenn Sie XPath kennen. Sie können diese Methoden mit LINQ verwenden, um Elemente
in einer Struktur schnell zu finden. SyntaxNode weist Abfragemethoden wie z.B. DescendantNodes,
AncestorsAndSelf und ChildNodes auf.
Sie können diese Abfragemethoden verwenden, um anstelle des Navigierens in der Struktur das Argument für
die Main -Methode zu suchen. Fügen Sie am Ende Ihrer Main -Methode folgenden Code hinzu:

var firstParameters = from methodDeclaration in root.DescendantNodes()


.OfType<MethodDeclarationSyntax>()
where methodDeclaration.Identifier.ValueText == "Main"
select methodDeclaration.ParameterList.Parameters.First();

var argsParameter2 = firstParameters.Single();

WriteLine(argsParameter == argsParameter2);

Die erste Anweisung verwendet einen LINQ-Ausdruck und die DescendantNodes-Methode, um denselben
Parameter zu suchen wie im vorherigen Beispiel.
Führen Sie das Programm aus, und Sie werden feststellen, dass der LINQ-Ausdruck denselben Parameter
gefunden hat wie beim manuellen Navigieren durch die Struktur.
Das Beispiel verwendet WriteLine -Anweisungen, um während des Durchlaufens Informationen zu den
Syntaxstrukturen anzuzeigen. Sie erhalten auch mehr Informationen, wenn Sie das fertig gestellte Programm im
Debugger ausführen. Sie können die Eigenschaften und Methoden, die zu der für das Hello World-Programm
erstellten Syntaxstruktur gehören, genauer untersuchen.

Syntaxwalker
Es gibt häufig Situationen, in denen Sie in einer Syntaxstruktur alle Knoten eines bestimmten Typs finden
möchten, z.B. jede Eigenschaftendeklaration in einer Datei. Indem Sie die
Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker-Klasse erweitern und die
VisitPropertyDeclaration(PropertyDeclarationSyntax)-Methode überschreiben, können Sie jede
Eigenschaftendeklaration in einer Syntaxstruktur verarbeiten, ohne die Struktur vorher kennen zu müssen.
CSharpSyntaxWalker ist eine bestimmte Art von CSharpSyntaxVisitor, der rekursiv einen Knoten und jedes
seiner untergeordneten Elemente besucht.
Dieses Beispiel implementiert einen CSharpSyntaxWalker, der eine Syntaxstruktur untersucht. Gefundene
using -Direktiven, die keinen System -Namespace importieren, werden gesammelt.

Erstellen Sie ein neues Stand-Alone Code Analysis Tool -Projekt für C#, und nennen Sie es SyntaxWalker .
Den fertig gestellten Code für dieses Beispiel finden Sie in unserem GitHub-Repository. Das Beispiel in GitHub
enthält beide in diesem Tutorial beschriebenen Projekte.
Wie im vorherigen Beispiel können Sie eine Zeichenfolgenkonstante definieren, die den Text des Programms
enthält, das Sie analysieren möchten:

const string programText =


@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace TopLevel
{
using Microsoft;
using System.ComponentModel;

namespace Child1
{
using Microsoft.Win32;
using System.Runtime.InteropServices;

class Foo { }
}

namespace Child2
{
using System.CodeDom;
using Microsoft.CSharp;

class Bar { }
}
}";

Dieser Quelltext enthält using -Direktiven, die an vier Stellen verteilt sind: auf Dateiebene, im Namespace der
obersten Ebene und in den beiden geschachtelten Namespaces. Dieses Beispiel zeigt ein grundlegendes
Szenario für die Verwendung der CSharpSyntaxWalker-Klasse zum Abfragen von Code. Es wäre sehr
umständlich, jeden Knoten in der Stammsyntaxstruktur besuchen zu müssen, um using-Deklarationen zu finden.
Stattdessen erstellen Sie eine abgeleitete Klasse und überschreiben die Methoden, die nur aufgerufen werden,
denn der aktuelle Knoten in der Struktur eine using-Direktive ist. Ihr Besucher führt keine Aktionen für einen
andern Knotentyp aus. Diese Einzelmethode untersucht jede der using -Anweisungen und erstellt eine
Sammlung der Namespaces, die sich nicht im System -Namespace befinden. Sie erstellen einen
CSharpSyntaxWalker, der alle using -Anweisungen untersucht, und zwar nur die using -Anweisungen.
Nachdem Sie den Programmtext definieren haben, müssen Sie eine SyntaxTree erstellen und das
Stammelement dieser Struktur abrufen:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);


CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Erstellen Sie als Nächstes eine neue Klasse. Wählen Sie in Visual Studio Projekt > Neues Element
hinzufügen aus. Geben Sie im Dialogfeld Neues Element hinzufügen den Namen UsingCollector.cs als
Dateinamen ein.
Sie implementieren die using -Besucherfunktionalität in der UsingCollector -Klasse. Beginnen Sie, indem Sie
die UsingCollector -Klasse aus CSharpSyntaxWalker ableiten.

class UsingCollector : CSharpSyntaxWalker

Sie benötigen Speicherplatz, um die gesammelten Namespaceknoten zu speichern. Deklarieren Sie eine
öffentliche schreibgeschützte Eigenschaft in der UsingCollector -Klasse, und verwenden Sie diese Variable, um
die gefundenen UsingDirectiveSyntax-Knoten zu speichern:

public ICollection<UsingDirectiveSyntax> Usings { get; } = new List<UsingDirectiveSyntax>();

Die Basisklasse CSharpSyntaxWalker implementiert die Logik, gemäß der jeder Knoten in der Syntaxstruktur
besucht wird. Die abgeleitete Klasse überschreibt die Methoden, die für die Knoten aufgerufen werden, für die
Sie sich interessieren. In diesem Fall gilt Ihr Interesse allen using -Direktiven. Das bedeutet, dass Sie die
VisitUsingDirective(UsingDirectiveSyntax)-Methode überschreiben müssen. Das einzige Argument in dieser
Methode ist ein Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax-Objekt. Das ist ein wichtiger Vorteil
gegenüber der Verwendung der Besucher: Diese rufen die überschriebenen Methoden mit Argumenten auf, die
bereits in den bestimmten Knotentyp umgewandelt wurden. Die
Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax-Klasse weist eine Name-Eigenschaft auf, die den
Namen des zu importierenden Namespace speichert. Es handelt sich um eine
Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax. Fügen Sie folgenden Code in die
VisitUsingDirective(UsingDirectiveSyntax)-Überschreibung ein:

public override void VisitUsingDirective(UsingDirectiveSyntax node)


{
WriteLine($"\tVisitUsingDirective called with {node.Name}.");
if (node.Name.ToString() != "System" &&
!node.Name.ToString().StartsWith("System."))
{
WriteLine($"\t\tSuccess. Adding {node.Name}.");
this.Usings.Add(node);
}
}

Wie beim oben aufgeführten Beispiel haben Sie eine Vielzahl von WriteLine -Anweisungen hinzugefügt, die
dabei helfen, diese Methode zu verstehen. Sie sehen, wann die Methode aufgerufen wird und welche Argumente
bei jedem Aufruf an die Methode übergeben werden.
Zum Schluss müssen Sie zwei Codezeilen hinzufügen, um den UsingCollector zu erstellen und dafür zu sorgen,
dass dieser den Stammknoten besucht und alle using -Anweisungen sammelt. Fügen Sie dann eine foreach -
Schleife ein, um alle using -Anweisungen anzuzeigen, die der Collector gefunden hat:

var collector = new UsingCollector();


collector.Visit(root);
foreach (var directive in collector.Usings)
{
WriteLine(directive.Name);
}

Kompilieren Sie das Projekt, und führen Sie es aus. Die folgende Ausgabe wird angezeigt:
VisitUsingDirective called with System.
VisitUsingDirective called with System.Collections.Generic.
VisitUsingDirective called with System.Linq.
VisitUsingDirective called with System.Text.
VisitUsingDirective called with Microsoft.CodeAnalysis.
Success. Adding Microsoft.CodeAnalysis.
VisitUsingDirective called with Microsoft.CodeAnalysis.CSharp.
Success. Adding Microsoft.CodeAnalysis.CSharp.
VisitUsingDirective called with Microsoft.
Success. Adding Microsoft.
VisitUsingDirective called with System.ComponentModel.
VisitUsingDirective called with Microsoft.Win32.
Success. Adding Microsoft.Win32.
VisitUsingDirective called with System.Runtime.InteropServices.
VisitUsingDirective called with System.CodeDom.
VisitUsingDirective called with Microsoft.CSharp.
Success. Adding Microsoft.CSharp.
Microsoft.CodeAnalysis
Microsoft.CodeAnalysis.CSharp
Microsoft
Microsoft.Win32
Microsoft.CSharp
Press any key to continue . . .

Herzlichen Glückwunsch! Sie haben die Syntax-API verwendet, um bestimmte Arten von C#-Anweisungen und
-Deklarationen in C#-Quellcode zu ermitteln.
Erste Schritte mit der semantischen Analyse
04.11.2021 • 8 minutes to read

Bei diesem Tutorial wird davon ausgegangen, dass Sie mit der Syntax-API vertraut sind. Der Artikel Erste Schritte
mit der Syntaxanalyse bietet eine Einführung zu diesem Thema.
Im vorliegenden Tutorial erkunden Sie die APIs für Symbol und Bindung . Diese APIs bieten Informationen zur
semantischen Bedeutung eines Programms. Sie ermöglichen es Ihnen, Fragen zu den Typen zu stellen und zu
beantworten, die durch ein Symbol in Ihrem Programm dargestellt werden.
Die Installation des SDK für die .NET Compiler Platform ist erforderlich.

Installationsanweisungen: Visual Studio-Installer


Es gibt zwei verschiedene Möglichkeiten, das .NET Compiler Platform SDK im Visual Studio-Installer zu
finden:
Installation mithilfe des Visual Studio -Installers: Workloads im Überblick
Das .NET Compiler Platform SDK wird nicht automatisch als Teil der Workload „Visual Studio-
Extensionentwicklung“ ausgewählt. Sie müssen sie als optionale Komponente auswählen.
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Aktivieren Sie die Workload Visual Studio-Extensionentwicklung .
4. Öffnen Sie den Knoten Visual Studio-Extensionentwicklung in der Zusammenfassungsstruktur.
5. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an letzter Stelle
unter den optionalen Komponenten.
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Öffnen Sie den Knoten Einzelne Komponenten in der Zusammenfassungsstruktur.
2. Aktivieren Sie das Kontrollkästchen für den DGML-Editor .
Installation mithilfe des Visual Studio -Installers: Registerkarte „Einzelne Komponenten“
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Klicken Sie auf die Registerkarte Einzelne Komponenten .
4. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an oberster Stelle
im Abschnitt Compiler, Buildtools und Laufzeiten .
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Aktivieren Sie das Kontrollkästchen für den DGML-Editor . Sie finden es im Abschnitt Codetools .

Grundlegendes zu Kompilierungen und Symbolen


Wenn Sie eine Weile mit dem .NET Compiler SDK arbeiten, werden Ihnen die Unterschiede zwischen der Syntax-
API und der Semantik-API immer vertrauter. Die Syntax-API ermöglicht es Ihnen, die Struktur eines Programms
zu untersuchen. In vielen Fällen benötigen Sie jedoch weitere Informationen zur Semantik bzw. Bedeutung eines
Programms. Sie können zwar eine einzelne, losgelöste Codedatei oder einen isolierten Visual Basic- oder C#-
Codeausschnitt syntaktisch analysieren, aber es ist wenig sinnvoll, Fragen wie „Welchen Typ weist diese Variable
auf?“ sozusagen im luftleeren Raum zu stellen. Die Bedeutung eines Typnamens kann von Assemblyverweisen,
Namespaceimporten oder anderen Codedateien abhängig sein. Diese Fragen werden mithilfe der Semantik-
API , insbesondere der Microsoft.CodeAnalysis.Compilation-Klasse, beantwortet.
Eine Instanz von Compilation entspricht einem einzelnen Projekt aus Sicht des Compilers und repräsentiert alle
Elemente, die zum Kompilieren eines Visual Basic- oder C#-Programms erforderlich sind. Die Kompilierung
umfasst die Quelldateien, die kompiliert werden sollen, sowie Assemblyverweise und Compileroptionen. Sie
können alle weiteren Informationen in diesem Kontext heranziehen, um die Bedeutung des Codes genau zu
verstehen. Eine Compilation ermöglicht es Ihnen, Symbole zu suchen: Entitäten wie z.B. Typen, Namespaces,
Member und Variablen, auf die Namen und andere Ausdrücke verweisen. Der Prozess der Verknüpfung von
Namen und Ausdrücken mit Symbolen wird als Bindung bezeichnet.
Ebenso wie Microsoft.CodeAnalysis.SyntaxTree ist Compilation eine abstrakte Klasse mit sprachspezifischen
Ableitungen. Wenn Sie eine Instanz von „Compilation“ erstellen, müssen Sie eine Factorymethode in der Klasse
Microsoft.CodeAnalysis.CSharp.CSharpCompilation (oder
Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation) aufrufen.

Abfragen von Symbolen


In diesem Tutorial betrachten wir noch einmal das Hello World-Programm. Diesmal fragen Sie die Symbole im
Programm ab, um zu verstehen, welche Typen durch diese Symbole repräsentiert werden. Sie fragen die Typen
in einem Namespace ab und lernen, wie Sie die verfügbaren Methoden in einem Typ finden.
Den fertig gestellten Code für dieses Beispiel finden Sie in unserem GitHub-Repository.

NOTE
Die Syntaxstrukturtypen verwenden Vererbung, um die verschiedenen Syntaxelemente zu beschreiben, die an
verschiedenen Positionen im Programm gültig sind. Bei der Verwendung dieser APIs müssen häufig Eigenschaften oder
Sammlungsmember in bestimmte abgeleitete Typen umgewandelt werden. In den folgenden Beispielen sind die
Zuweisung und die Umwandlung separate Anweisungen, bei denen explizit typisierte Variablen verwendet werden. Sie
können den Code lesen, um die Rückgabetypen der API und den Laufzeittyp der zurückgegebenen Objekte zu sehen. In
der Praxis ist es eher üblich, implizit typisierte Variablen zu verwenden und die Typen der zu untersuchenden Objekte
mithilfe von API-Namen zu beschreiben.

Erstellen Sie ein neues Stand-Alone Code Analysis Tool -Projekt für C#:
Wählen Sie in Visual Studio Datei > Neu > Projekt aus, um das Dialogfeld „Neues Projekt“ anzuzeigen.
Wählen Sie unter Visual C# > Er weiterbarkeit die Option Stand-Alone Code Analysis Tool aus.
Nennen Sie Ihr Projekt SemanticQuickStar t , und klicken Sie auf „OK“.
Sie werden das einfache Hello World!- Programm analysieren, das weiter oben in diesem Artikel gezeigt wurde.
Fügen Sie den Text für das Hello World-Programm als Konstante in Ihre Program -Klasse ein:
const string programText =
@"using System;
using System.Collections.Generic;
using System.Text;

namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
}";

Anschließend fügen Sie den folgenden Code hinzu, um die Syntaxstruktur für den Codetext in der programText -
Konstante zu erstellen. Fügen Sie Ihrer Main -Methode die folgende Zeile hinzu:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);

CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Anschließend erstellen Sie aus der bereits erstellten Struktur eine CSharpCompilation. Das Hello World-Beispiel
basiert auf den Typen String und Console. Sie müssen auf die Assembly verweisen, die diese beiden Typen in
Ihrer Kompilierung deklariert. Fügen Sie Ihrer Main -Methode folgende Zeile hinzu, um eine Kompilierung Ihrer
Syntaxstruktur einschließlich des Verweises auf die entsprechende Assembly zu erstellen:

var compilation = CSharpCompilation.Create("HelloWorld")


.AddReferences(MetadataReference.CreateFromFile(
typeof(string).Assembly.Location))
.AddSyntaxTrees(tree);

Die CSharpCompilation.AddReferences-Methode fügt Verweise zur Kompilierung hinzu. Die


MetadataReference.CreateFromFile-Methode lädt eine Assembly als Verweis.

Abfragen des semantischen Modells


Sobald Sie über eine Compilation verfügen, können Sie diese nach einem SemanticModel für jede SyntaxTree in
dieser Compilation abfragen. Sie können sich das Semantikmodell als die Quelle aller Informationen vorstellen,
die Sie normalerweise aus IntelliSense erhalten. Ein SemanticModel kann Fragen wie die folgenden
beantworten: „Welche Namen befinden sich an diesem Ort im Gültigkeitsbereich?“, „Auf welche Member kann
von dieser Methode aus zugegriffen werden?“, „Welche Variablen werden in diesem Textblock verwendet?“ und
„Vorauf verweist dieser Name/dieser Ausdruck?“. Fügen Sie diese Anweisung hinzu, um das semantische Modell
zu erstellen:

SemanticModel model = compilation.GetSemanticModel(tree);

Binden eines Namens


Die Compilation erstellt das SemanticModel aus der SyntaxTree. Nach dem Erstellen des Modells können Sie es
abfragen, um die erste using -Direktive zu suchen und die Symbolinformationen für den System -Namespace
abzurufen. Fügen Sie die folgenden beiden Zeilen zu Ihrer Main -Methode hinzu, um das Semantikmodell zu
erstellen und das Symbol für die erste using-Anweisung abzurufen:
// Use the syntax tree to find "using System;"
UsingDirectiveSyntax usingSystem = root.Usings[0];
NameSyntax systemName = usingSystem.Name;

// Use the semantic model for symbol information:


SymbolInfo nameInfo = model.GetSymbolInfo(systemName);

Nachdem das Modell abgerufen wurde, wird der Name in der ersten using -Anweisung gebunden, um eine
Microsoft.CodeAnalysis.SymbolInfo für den System -Namespace abzurufen. Dieser Code verdeutlicht auch, dass
Sie das Syntaxmodell verwenden, um die Struktur des Codes zu ermitteln. Das Semantikmodell verwenden
Sie, um die Bedeutung des Codes zu verstehen. Das Syntaxmodell sucht die Zeichenfolge System in der using-
Anweisung. Das Semantikmodell verfügt über alle Informationen zu den im System -Namespace definierten
Typen.
Aus dem SymbolInfo-Objekt können Sie mithilfe der SymbolInfo.Symbol-Eigenschaft das
Microsoft.CodeAnalysis.ISymbol abrufen. Diese Eigenschaft gibt das Symbol zurück, auf das dieser Ausdruck
verweist. Bei Ausdrücken, die auf nichts verweisen (wie z.B. numerische Literale), lautet diese Eigenschaft null .
Wenn SymbolInfo.Symbol nicht null ist, bezeichnet ISymbol.Kind den Typ des Symbols. In diesem Beispiel ist die
ISymbol.Kind-Eigenschaft ein SymbolKind.Namespace. Fügen Sie der Main -Methode den folgenden Code
hinzu. Der Code ruft das Symbol für den System -Namespace ab und zeigt alle untergeordneten Namespaces
an, die im System -Namespace deklariert sind:

var systemSymbol = (INamespaceSymbol)nameInfo.Symbol;


foreach (INamespaceSymbol ns in systemSymbol.GetNamespaceMembers())
{
Console.WriteLine(ns);
}

Führen Sie das Programm aus. Folgende Ausgabe sollte angezeigt werden:

System.Collections
System.Configuration
System.Deployment
System.Diagnostics
System.Globalization
System.IO
System.Numerics
System.Reflection
System.Resources
System.Runtime
System.Security
System.StubHelpers
System.Text
System.Threading
Press any key to continue . . .

NOTE
Die Ausgabe umfasst nicht jeden Namespace, der ein untergeordneter Namespace des System -Namespace ist. Sie zeigt
jeden Namespace an, der in dieser Kompilierung vorhanden ist. Diese verweist nur auf die Assembly, in der
System.String deklariert ist. Namespaces, die in anderen Assemblys deklariert wurden, sind dieser Kompilierung
unbekannt.

Binden eines Ausdrucks


Der oben gezeigte Code veranschaulicht, wie Sie ein Symbol suchen, indem Sie es an einen Namen binden. Es
gibt weitere Ausdrücke in einem C#-Programm, die gebunden werden können, aber keine Namen sind. Um
diese Funktion zu demonstrieren, sehen wir uns die Bindung an ein einfaches Zeichenfolgenliteral an.
Das Hello World-Programm enthält eine Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax: die
Hello, World!- Zeichenfolge, die in der Konsole angezeigt wird.
Sie finden die Hello, World!- Zeichenfolge, indem Sie das einzelne Zeichenfolgenliteral im Programm suchen.
Wenn Sie den Syntaxknoten gefunden haben, rufen Sie die Typinformationen für diesen Knoten aus dem
Semantikmodell ab. Fügen Sie der Main -Methode den folgenden Code hinzu:

// Use the syntax model to find the literal string:


LiteralExpressionSyntax helloWorldString = root.DescendantNodes()
.OfType<LiteralExpressionSyntax>()
.Single();

// Use the semantic model for type information:


TypeInfo literalInfo = model.GetTypeInfo(helloWorldString);

Die Microsoft.CodeAnalysis.TypeInfo-Struktur enthält eine TypeInfo.Type-Eigenschaft, die Zugriff auf die


semantischen Informationen zum Typ des Literals erlaubt. In diesem Beispiel handelt es sich um den Typ string
. Fügen Sie eine Deklaration hinzu, die diese Eigenschaft einer lokalen Variable zuweist:

var stringTypeSymbol = (INamedTypeSymbol)literalInfo.Type;

Zum Abschluss dieses Tutorials erstellen wir eine LINQ-Abfrage, die eine Sequenz aller öffentlichen Methoden
erstellt, die im string -Typ deklariert sind und eine string zurückgeben. Diese Abfrage wird ziemlich komplex
– wir bauen sie daher Zeile für Zeile auf und rekonstruieren sie dann als Einzelabfrage. Die Quelle dieser Abfrage
ist die Sequenz aller Member, die im string -Typ deklariert sind:

var allMembers = stringTypeSymbol.GetMembers();

Diese Quellsequenz enthält alle Member, einschließlich Eigenschaften und Feldern. Filtern Sie die Sequenz daher
mit der ImmutableArray<T>.OfType-Methode, um die Elemente zu finden, bei denen es sich um
Microsoft.CodeAnalysis.IMethodSymbol-Objekte handelt:

var methods = allMembers.OfType<IMethodSymbol>();

Danach fügen Sie einen weiteren Filter hinzu, um nur die Methoden zurückzugeben, die öffentlich sind und eine
string zurückgeben:

var publicStringReturningMethods = methods


.Where(m => m.ReturnType.Equals(stringTypeSymbol) &&
m.DeclaredAccessibility == Accessibility.Public);

Wählen Sie nur die name-Eigenschaft und nur eindeutige Namen aus, indem Sie alle Überladungen entfernen:

var distinctMethods = publicStringReturningMethods.Select(m => m.Name).Distinct();

Sie können auch mithilfe der LINQ-Abfragesyntax die vollständige Abfrage erstellen und dann alle
Methodennamen in der Konsole anzeigen:
foreach (string name in (from method in stringTypeSymbol
.GetMembers().OfType<IMethodSymbol>()
where method.ReturnType.Equals(stringTypeSymbol) &&
method.DeclaredAccessibility == Accessibility.Public
select method.Name).Distinct())
{
Console.WriteLine(name);
}

Erstellen Sie das Programm, und führen Sie es aus. Die folgende Ausgabe wird angezeigt.

Join
Substring
Trim
TrimStart
TrimEnd
Normalize
PadLeft
PadRight
ToLower
ToLowerInvariant
ToUpper
ToUpperInvariant
ToString
Insert
Replace
Remove
Format
Copy
Concat
Intern
IsInterned
Press any key to continue . . .

Sie haben die Semantik-API verwendet, um Informationen zu den Symbolen zu finden und anzuzeigen, die zu
diesem Programm gehören.
Erste Schritte mit der Syntaxtransformation
04.11.2021 • 13 minutes to read

Dieses Tutorial baut auf Konzepten und Methoden auf, die in den Schnellstarts Erste Schritte mit der
Syntaxanalyse und Erste Schritte mit der semantischen Analyse erläutert werden. Wenn nicht bereits geschehen,
sollten Sie diese Schnellstarts durchlaufen, bevor Sie mit diesem beginnen.
In diesem Schnellstart erforschen Sie Methoden zum Erstellen und Transformieren von Syntaxstrukturen. In
Kombination mit den Methoden, die Sie in früheren Schnellstarts gelernt haben, erstellen Sie Ihr erstes
Kommandozeilenrefactoring.

Installationsanweisungen: Visual Studio-Installer


Es gibt zwei verschiedene Möglichkeiten, das .NET Compiler Platform SDK im Visual Studio-Installer zu
finden:
Installation mithilfe des Visual Studio -Installers: Workloads im Überblick
Das .NET Compiler Platform SDK wird nicht automatisch als Teil der Workload „Visual Studio-
Extensionentwicklung“ ausgewählt. Sie müssen sie als optionale Komponente auswählen.
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Aktivieren Sie die Workload Visual Studio-Extensionentwicklung .
4. Öffnen Sie den Knoten Visual Studio-Extensionentwicklung in der Zusammenfassungsstruktur.
5. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an letzter Stelle
unter den optionalen Komponenten.
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Öffnen Sie den Knoten Einzelne Komponenten in der Zusammenfassungsstruktur.
2. Aktivieren Sie das Kontrollkästchen für den DGML-Editor .
Installation mithilfe des Visual Studio -Installers: Registerkarte „Einzelne Komponenten“
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Klicken Sie auf die Registerkarte Einzelne Komponenten .
4. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an oberster Stelle
im Abschnitt Compiler, Buildtools und Laufzeiten .
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Aktivieren Sie das Kontrollkästchen für den DGML-Editor . Sie finden es im Abschnitt Codetools .

Unveränderlichkeit und die .NET Compiler Platform


Unveränderlichkeit ist ein Grundprinzip der .NET Compiler Platform. Unveränderliche Datenstrukturen
können nach ihrer Erstellung nicht mehr geändert werden. Sie können von mehreren Consumern gleichzeitig
auf sichere Weise genutzt und analysiert werden. Es besteht kein Risiko, dass ein Consumer einen anderen auf
unvorhersehbare Weise beeinträchtigt. Für Ihr Analysetool sind keine Sperren oder andere Maßnahmen für die
gleichzeitige Verwendung erforderlich. Diese Regel gilt für Syntaxstrukturen, Kompilierungen, Symbole,
semantische Modelle und jede andere Datenstruktur. Anstatt bestehende Strukturen zu modifizieren, werden
durch APIs neue Objekte basierend auf den angegebenen Unterschieden zu den alten Objekten erstellt. Sie
wenden dieses Konzept auf Syntaxstrukturen an, um mithilfe von Transformationen neue Strukturen zu erstellen.

Erstellen und Transformieren von Strukturen


Sie wählen eine von zwei Strategien für Syntaxtransformationen. Factor ymethoden werden am besten
verwendet, wenn Sie nach bestimmten Knoten suchen, die ersetzt werden sollen, oder nach bestimmten Stellen,
an denen Sie neuen Code einfügen möchten. Rewriter sind am besten geeignet, wenn Sie ein ganzes Projekt
nach Codemustern durchsuchen möchten, die Sie ersetzen möchten.
Erstellen von Knoten mit Factorymethoden
Durch die erste Syntaxtransformation werden die Factorymethoden demonstriert. Sie werden eine
using System.Collections; -Anweisung durch eine using System.Collections.Generic; -Anweisung ersetzen.
Dieses Beispiel zeigt, wie Sie Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode-Objekte mit den
Microsoft.CodeAnalysis.CSharp.SyntaxFactory-Factorymethoden erstellen. Für jede Art von Knoten , Token oder
Trivia gibt es eine Factorymethode, die eine Instanz dieses Typs erstellt. Sie erstellen Syntaxstrukturen, indem
Sie Knoten hierarchisch von unten nach oben zusammensetzen. Anschließend transformieren Sie das
vorhandene Programm, indem Sie bestehende Knoten durch die neue, von Ihnen erstellte Struktur ersetzen.
Starten Sie Visual Studio, und erstellen Sie ein neues C#-Projekt namens Stand-Alone Code Analysis Tool .
Wählen Sie in Visual Studio Datei > Neu > Projekt aus, um das Dialogfeld „Neues Projekt“ anzuzeigen. Wählen
Sie unter Visual C# > Er weiterbarkeit die Option Stand-Alone Code Analysis Tool aus. Dieser Schnellstart
enthält zwei Beispielprojekte. Nennen Sie daher die Lösung SyntaxTransformationQuickStar t und das
Projekt ConstructionCS . Klicken Sie auf OK .
Dieses Projekt verwendet die Methoden der Klasse Microsoft.CodeAnalysis.CSharp.SyntaxFactory, um ein
Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax-Element zu erstellen, das den System.Collections.Generic -
Namespace repräsentiert.
Fügen Sie am Anfang der Datei Program.cs die folgende using-Direktive hinzu.

using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;


using static System.Console;

Sie werden Namenssyntaxknoten erstellen, um die Struktur aufzubauen, die die


using System.Collections.Generic; -Anweisung repräsentiert. NameSyntax ist die Basisklasse für vier Typen von
Namen, die in C# verwendet werden. Sie setzen diese vier Typen von Namen zusammen, um einen beliebigen
Namen zu erstellen, der in der Programmiersprache C# enthalten sein kann:
Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax steht für einfache einzelne Bezeichner wie System und
Microsoft .
Microsoft.CodeAnalysis.CSharp.Syntax.GenericNameSyntax steht für einen generischen Typ oder
Methodennamen wie List<int> .
Microsoft.CodeAnalysis.CSharp.Syntax.QualifiedNameSyntax steht für einen qualifizierten Namen der Form
<left-name>.<right-identifier-or-generic-name> wie z.B. System.IO .
Microsoft.CodeAnalysis.CSharp.Syntax.AliasQualifiedNameSyntax steht für einen Namen unter Verwendung
eines externen Assemblyalias wie LibraryV2::Foo .
Sie verwenden die IdentifierName(String)-Methode, um einen NameSyntax-Knoten zu erstellen. Fügen Sie den
folgenden Code in Ihrer Main -Methode in Program.cs hinzu:

NameSyntax name = IdentifierName("System");


WriteLine($"\tCreated the identifier {name}");
Der vorangehende Code erstellt ein IdentifierNameSyntax-Objekt und weist es der Variablen name zu. Viele der
Roslyn-APIs geben Basisklassen zurück, um die Arbeit mit verwandten Typen zu erleichtern. Die Variable name ,
eine NameSyntax, kann beim Erstellen von QualifiedNameSyntax wiederverwendet werden. Verwenden Sie für
das Beispiel keinen Typrückschluss. Sie werden diesen Schritt in diesem Projekt automatisieren.
Sie haben nun den Namen erstellt. Jetzt ist es an der Zeit, die Struktur mit zusätzlichen Knoten zu erweitern,
indem Sie ein QualifiedNameSyntax-Element erstellen. In der neuen Struktur wird name als linker Namensteil
und ein neues IdentifierNameSyntax-Element für den Collections -Namespace als rechter Teil von
QualifiedNameSyntax verwendet. Fügen Sie den folgenden Code zu program.cs hinzu:

name = QualifiedName(name, IdentifierName("Collections"));


WriteLine(name.ToString());

Führen Sie den Code erneut aus, und sehen Sie sich die Ergebnisse an. Sie bauen eine Knotenstruktur auf, die
den Code darstellt. Sie werden dieses Muster fortsetzen, um das QualifiedNameSyntax-Element für den
System.Collections.Generic -Namespace zu erstellen. Fügen Sie den folgenden Code zu Program.cs hinzu:

name = QualifiedName(name, IdentifierName("Generic"));


WriteLine(name.ToString());

Starten Sie das Programm erneut, um zu sehen, dass Sie die Struktur für den hinzuzufügenden Code erstellt
haben.
Erstellen einer geänderten Struktur
Sie haben eine kleine Syntaxstruktur aufgebaut, die eine Anweisung enthält. Die APIs zum Erstellen neuer
Knoten sind die richtige Wahl, um einzelne Anweisungen oder andere kleine Codeblöcke zu erstellen. Um jedoch
größere Codeblöcke zu erstellen, sollten Sie Methoden verwenden, die Knoten ersetzen oder Knoten in eine
bestehende Struktur einfügen. Denken Sie daran, dass Syntaxstrukturen unveränderlich sind. Die Syntax-API
bietet keinen Mechanismus, um eine bestehende Syntaxstruktur nach deren Aufbau zu ändern. Stattdessen stellt
sie Methoden zur Verfügung, die neue Strukturen auf der Grundlage von Änderungen an bestehenden
Strukturen erzeugen. With* -Methoden werden in konkreten Klassen definiert, die von SyntaxNode abgeleitet
sind, oder in Erweiterungsmethoden, die in der Klasse SyntaxNodeExtensions deklariert sind. Diese Methoden
erstellen einen neuen Knoten, indem sie Änderungen an den untergeordneten Eigenschaften eines vorhandenen
Knotens vornehmen. Zusätzlich kann die ReplaceNode-Erweiterungsmethode verwendet werden, um einen
untergeordneten Knoten in einer Teilstruktur zu ersetzen. Diese Methode aktualisiert auch das übergeordnete
Element, sodass dieses auf das neu erstellte untergeordnete Element zeigt. Die Methode wiederholt diesen
Prozess für die gesamte Struktur – dies ist auch als Umlaufen der Struktur bekannt.
Der nächste Schritt besteht darin, eine Struktur zu erstellen, die ein ganzes (kleines) Programm darstellt, und
diese dann zu ändern. Fügen Sie den folgenden Code am Anfang der Program -Klasse hinzu:
private const string sampleCode =
@"using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
}";

NOTE
Der Beispielcode verwendet den System.Collections -Namespace und nicht den System.Collections.Generic -
Namespace.

Als Nächstes fügen Sie den folgenden Code am Ende der Main -Methode hinzu, um den Text zu analysieren und
eine Struktur zu erstellen:

SyntaxTree tree = CSharpSyntaxTree.ParseText(sampleCode);


var root = (CompilationUnitSyntax)tree.GetRoot();

In diesem Beispiel wird die WithName(NameSyntax)-Methode verwendet, um den Namen in einem


UsingDirectiveSyntax-Knoten durch den im vorangehenden Code konstruierten zu ersetzen.
Erstellen Sie einen neuen UsingDirectiveSyntax-Knoten mit der WithName(NameSyntax)-Methode, um den
System.Collections -Namen mit dem Namen zu aktualisieren, den Sie im vorangehenden Code erstellt haben.
Fügen Sie den folgenden Code am Ende der Main -Methode hinzu:

var oldUsing = root.Usings[1];


var newUsing = oldUsing.WithName(name);
WriteLine(root.ToString());

Starten Sie das Programm, und schauen Sie sich die Ausgabe genau an. Das newUsing -Element wurde nicht in
der Stammstruktur platziert. Die ursprüngliche Struktur wurde nicht geändert.
Fügen Sie den folgenden Code mit der ReplaceNode-Erweiterungsmethode hinzu, um eine neuen Struktur zu
erstellen. Die neue Struktur hat sich durch das Ersetzen des bestehenden Imports durch den aktualisierten
newUsing -Knoten ergeben. Sie weisen diese neue Struktur der vorhandenen root -Variablen zu:

root = root.ReplaceNode(oldUsing, newUsing);


WriteLine(root.ToString());

Führen Sie das Programm erneut aus. Diesmal wird der System.Collections.Generic -Namespace korrekt in die
Struktur importiert.
Transformieren von Strukturen mit SyntaxRewriters

Die Methoden With* und ReplaceNode bieten komfortable Möglichkeiten, einzelne Branches einer
Syntaxstruktur zu transformieren. Die Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter-Klasse führt
mehrere Transformationen in einer Syntaxstruktur durch. Bei der
Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter-Klasse handelt es sich um eine Unterklasse von
Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor<TResult>. Der CSharpSyntaxRewriter wendet eine
Transformation auf einen bestimmten Typ von SyntaxNode an. Sie können Transformationen auf mehrere Typen
von SyntaxNode-Objekten anwenden, unabhängig von deren Stelle in der Syntaxstruktur. Beim zweiten Projekt
in diesem Schnellstart erstellen Sie ein Kommandozeilenrefactoring, das explizite Typen in lokalen
Variablendeklarationen überall dort entfernt, wo Typrückschlüsse verwendet werden könnten.
Erstellen Sie ein neues C#-Projekt namens Stand-Alone Code Analysis Tool . Klicken Sie in Visual Studio mit
der rechten Maustaste auf den Lösungsknoten SyntaxTransformationQuickStart . Wählen Sie Hinzufügen >
Neues Projekt , um das Dialogfeld Neues Projekt anzuzeigen. Wählen Sie unter Visual C# >
Er weiterbarkeit die Option Stand-Alone Code Analysis Tool aus. Nennen Sie Ihr Projekt TransformationCS ,
und klicken Sie auf „OK“.
Der erste Schritt besteht in der Erstellung einer Klasse, die von CSharpSyntaxRewriter abgeleitet ist, um Ihre
Transformationen durchzuführen. Fügen Sie zum Projekt eine neue Klassendatei hinzu. Wählen Sie in Visual
Studio Projekt > Klasse hinzufügen... aus. Geben Sie im Dialogfeld Neues Element hinzufügen
TypeInferenceRewriter.cs als Dateinamen ein.

Fügen Sie der Datei TypeInferenceRewriter.cs die folgenden using-Anweisungen hinzu:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Als Nächstes nutzen Sie die TypeInferenceRewriter -Klasse zur Erweiterung der CSharpSyntaxRewriter-Klasse:

public class TypeInferenceRewriter : CSharpSyntaxRewriter

Fügen Sie den folgenden Code hinzu, um ein privates schreibgeschütztes Feld mit einem SemanticModel-
Element zu deklarieren, und initialisieren Sie es im Konstruktor. Sie werden dieses Feld später benötigen, um zu
bestimmen, wo ein Typrückschluss verwendet werden kann:

private readonly SemanticModel SemanticModel;

public TypeInferenceRewriter(SemanticModel semanticModel) => SemanticModel = semanticModel;

Überschreiben Sie die VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax)-Methode:

public override SyntaxNode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node)


{

NOTE
Viele der Roslyn-APIs deklarieren Rückgabetypen, die Basisklassen der tatsächlich zurückgegebenen Runtimetypen sind. In
vielen Szenarien kann eine Art von Knoten vollständig durch eine andere Art von Knoten ersetzt werden. Manchmal ist es
sogar möglich, eine Art von Knoten zu entfernen. In diesem Beispiel gibt die
VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax)-Methode, anstelle des abgeleiteten Typs von
LocalDeclarationStatementSyntax, ein SyntaxNode-Element zurück. Dieser Rewriter gibt einen neuen
LocalDeclarationStatementSyntax-Knoten zurück, der auf dem vorhandenen Knoten basiert.
In diesem Schnellstart werden lokale Variablendeklarationen behandelt. Die diesbezüglichen Informationen
können Sie auch für andere Deklarationen wie foreach -Schleifen, for -Schleifen, LINQ-Ausdrücke und
Lambdaausdrücke nutzen. Außerdem transformiert der hier gezeigte Rewriter nur Deklarationen der
einfachsten Form:

Type variable = expression;

Wenn Sie selbst die verschiedenen Möglichkeiten erkunden möchten, ist es empfehlenswert, das fertig gestellte
Beispiel für diese Typen von Variablendeklarationen einfach zu erweitern:

// Multiple variables in a single declaration.


Type variable1 = expression1,
variable2 = expression2;
// No initializer.
Type variable;

Fügen Sie den folgenden Code zum Text der VisitLocalDeclarationStatement -Methode hinzu, um das
Umschreiben dieser Deklarationsformen zu überspringen:

if (node.Declaration.Variables.Count > 1)
{
return node;
}
if (node.Declaration.Variables[0].Initializer == null)
{
return node;
}

Die Methode gibt an, dass kein Umschreiben stattfindet, indem der Parameter node unverändert
zurückgegeben wird. Wenn keiner dieser if -Ausdrücke „TRUE“ ist, stellt der Knoten eine mögliche Deklaration
mit Initialisierung dar. Fügen Sie diese Anweisungen hinzu, um den in der Deklaration angegebenen Typnamen
zu extrahieren, und binden Sie ihn mit dem SemanticModel-Feld, um ein Typsymbol zu erhalten:

var declarator = node.Declaration.Variables.First();


var variableTypeName = node.Declaration.Type;

var variableType = (ITypeSymbol)SemanticModel


.GetSymbolInfo(variableTypeName)
.Symbol;

Fügen Sie nun diese Anweisung hinzu, um den Initialisierungsausdruck zu binden:

var initializerInfo = SemanticModel.GetTypeInfo(declarator.Initializer.Value);

Abschließend fügen Sie die folgende if -Anweisung hinzu, um den vorhandenen Typnamen durch das
Schlüsselwort var zu ersetzen, wenn der Typ des Initialisierungsausdrucks dem angegebenen Typ entspricht:
if (SymbolEqualityComparer.Default.Equals(variableType, initializerInfo.Type))
{
TypeSyntax varTypeName = SyntaxFactory.IdentifierName("var")
.WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
.WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

return node.ReplaceNode(variableTypeName, varTypeName);


}
else
{
return node;
}

Die Bedingung ist erforderlich, da die Deklaration den Initialisierungsausdruck in eine Basisklasse oder eine
Schnittstelle umwandeln kann. Wenn das gewünscht wird, stimmen die Typen auf der linken und rechten Seite
der Zuweisung nicht überein. Das Entfernen des expliziten Typs würde in diesen Fällen die Semantik eines
Programms verändern. var wird als Bezeichner und nicht als Schlüsselwort angegeben, da var ein
kontextbezogenes Schlüsselwort ist. Die führenden und nachgestellten Trivia (Leerzeichen) werden vom alten
Typnamen in das Schlüsselwort var übertragen, um vertikale Leerzeichen und Einrückungen beizubehalten. Es
ist einfacher, ReplaceNode anstelle von With* für die Transformation der LocalDeclarationStatementSyntax zu
verwenden, da der Typname eigentlich ein untergeordnetes Element der zweiten Ebene der
Deklarationsanweisung ist.
Sie haben den TypeInferenceRewriter abgeschlossen. Kehren Sie nun zu Ihrer Program.cs -Datei zurück, um das
Beispiel fertig zu stellen. Erstellen Sie einen Compilation-Test, und rufen Sie daraus das SemanticModel ab.
Verwenden Sie dieses SemanticModel, um Ihren TypeInferenceRewriter auszuprobieren. Diesen Schritt führen
Sie zuletzt aus. In der Zwischenzeit deklarieren Sie eine Platzhaltervariable, die Ihre Testkompilierung
repräsentiert:

Compilation test = CreateTestCompilation();

Nach einer kurzen Unterbrechung sollten Sie eine Wellenlinie für einen Fehler sehen, die darauf hinweist, dass
die CreateTestCompilation -Methode nicht vorhanden ist. Drücken Sie Strg+Punkt , um die Glühbirnenmeldung
zu öffnen, und drücken Sie dann die EINGABETASTE, um den Befehl Methodenstub generieren aufzurufen.
Dieser Befehl erzeugt einen Methodenstub für die CreateTestCompilation -Methode in der Program -Klasse. Sie
werden diese Methode zu einem späteren Zeitpunkt ausfüllen:
Schreiben Sie den folgenden Code, um jede SyntaxTree-Klasse im Compilation-Test zu durchlaufen. Initialisieren
Sie für jeden einen neuen TypeInferenceRewriter mit dem SemanticModel für diese Struktur:

foreach (SyntaxTree sourceTree in test.SyntaxTrees)


{
SemanticModel model = test.GetSemanticModel(sourceTree);

TypeInferenceRewriter rewriter = new TypeInferenceRewriter(model);

SyntaxNode newSource = rewriter.Visit(sourceTree.GetRoot());

if (newSource != sourceTree.GetRoot())
{
File.WriteAllText(sourceTree.FilePath, newSource.ToFullString());
}
}

Fügen Sie innerhalb der von Ihnen erstellten foreach -Anweisung den folgenden Code hinzu, um die
Transformation für jede Quellstruktur durchzuführen. Dieser Code schreibt die neu transformierte Struktur
bedingt aus, falls Änderungen vorgenommen wurden. Ihr Rewriter sollte eine Struktur nur dann ändern, wenn er
auf eine oder mehrere lokale Variablendeklarationen stößt, die durch einen Typrückschluss vereinfacht werden
könnten:

SyntaxNode newSource = rewriter.Visit(sourceTree.GetRoot());

if (newSource != sourceTree.GetRoot())
{
File.WriteAllText(sourceTree.FilePath, newSource.ToFullString());
}

Der Code File.WriteAllText sollte mit einer Wellenlinie unterstrichen sein. Wählen Sie die Glühbirne aus, und
fügen Sie die erforderliche using System.IO; -Anweisung hinzu.
Sie haben es fast geschafft! Es gibt noch einen Schritt: das Erstellen eines Compilation-Tests. Da Sie bei diesem
Schnellstart überhaupt keinen Typrückschluss verwendet haben, wäre das ein perfekter Testfall gewesen. Leider
würde das Erstellen einer Kompilierung aus einer C#-Projektdatei außerhalb des Rahmens dieser
exemplarischen Vorgehensweise liegen. Aber glücklicherweise gibt es, wenn Sie die Anweisungen genau befolgt
haben, auch eine gute Nachricht. Ersetzen Sie den Inhalt der CreateTestCompilation -Methode durch folgenden
Code. Es wird eine Testkompilierung erstellt, die zufällig mit dem in diesem Schnellstart beschriebenen Projekt
übereinstimmt:
String programPath = @"..\..\..\Program.cs";
String programText = File.ReadAllText(programPath);
SyntaxTree programTree =
CSharpSyntaxTree.ParseText(programText)
.WithFilePath(programPath);

String rewriterPath = @"..\..\..\TypeInferenceRewriter.cs";


String rewriterText = File.ReadAllText(rewriterPath);
SyntaxTree rewriterTree =
CSharpSyntaxTree.ParseText(rewriterText)
.WithFilePath(rewriterPath);

SyntaxTree[] sourceTrees = { programTree, rewriterTree };

MetadataReference mscorlib =
MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
MetadataReference codeAnalysis =
MetadataReference.CreateFromFile(typeof(SyntaxTree).Assembly.Location);
MetadataReference csharpCodeAnalysis =
MetadataReference.CreateFromFile(typeof(CSharpSyntaxTree).Assembly.Location);

MetadataReference[] references = { mscorlib, codeAnalysis, csharpCodeAnalysis };

return CSharpCompilation.Create("TransformationCS",
sourceTrees,
references,
new CSharpCompilationOptions(OutputKind.ConsoleApplication));

Drücken Sie die Daumen, und führen Sie das Projekt aus. Wählen Sie in Visual Studio Debuggen > Debugging
star ten . Visual Studio informiert Sie darüber, dass sich die Dateien in Ihrem Projekt geändert haben. Klicken Sie
auf „Ja, alle “, um die geänderten Dateien neu zu laden. Schauen Sie sich Ihr Werk etwas genauer an. Beachten
Sie, wie viel sauberer der Code ohne all diese expliziten und redundanten Typspezifizierer aussieht.
Herzlichen Glückwunsch! Sie haben die Compiler-APIs verwendet, um Ihr eigenes Refactoring zu schreiben,
das alle Dateien in einem C#-Projekt nach bestimmten syntaktischen Mustern durchsucht, die Semantik des
Quellcodes in Übereinstimmung mit diesen Mustern analysiert und diese transformiert. Sie sind jetzt offiziell
Refactoringautor!
Tutorial: Schreiben Ihres ersten Analysetools und
Codefixes
04.11.2021 • 24 minutes to read

Das .NET Compiler Platform SDK bietet die Tools, die Sie benötigen, um benutzerdefinierte Diagnosefunktionen
(Analysetools), Codekorrekturen, Coderefactoring und Diagnoseunterdrückungsfunktionen zu erstellen, die auf
C#- oder Visual Basic-Code abzielen. Ein Analysetool enthält Code, der Verstöße gegen Ihre Regel erkennt. Ihr
Codefix enthält den Code, der den Verstoß behebt. Die Regeln, die Sie implementieren, können sich auf alles
Mögliche beziehen, von der Codestruktur über das Codeformat bis zu Benennungskonventionen usw. Die .NET
Compiler Platform stellt das Framework zum Ausführen der Analyse bereit, während die Entwickler Code
schreiben, mit allen Features der Visual Studio-Benutzeroberfläche für das Beheben von Codeproblemen:
Anzeigen von Wellenlinien im Editor, Auffüllen der Fehlerliste von Visual Studio, Erstellen der "Glühbirnen"-
Vorschläge und Darstellung der umfassenden Vorschau vorgeschlagener Fixe.
In diesem Tutorial lernen Sie die Erstellung eines Analysetools und eines begleitenden Codefixes unter
Verwendung der Roslyn-APIs kennen. Ein Analysetool ist eine Möglichkeit, Quellcodeanalyse auszuführen und
dem Benutzer ein Problem zu melden. Optional kann eine Codekorrektur mit dem Analysetool verknüpft
werden, um eine Änderung am Quellcode des Benutzers darzustellen. In diesem Tutorial wird ein Analysetool
erstellt, das Deklarationen von lokalen Variablen enthält, die mithilfe des const -Modifizierers deklariert werden
könnten, es aber nicht sind. Der begleitende Codefix ändert diese Deklarationen, indem er den const -
Modifizierer hinzufügt.

Voraussetzungen
Visual Studio 2019, Version 16.8 oder höher
Installieren Sie zunächst über den Visual Studio-Installer das SDK für die .NET Compiler Platform :

Installationsanweisungen: Visual Studio-Installer


Es gibt zwei verschiedene Möglichkeiten, das .NET Compiler Platform SDK im Visual Studio-Installer zu
finden:
Installation mithilfe des Visual Studio -Installers: Workloads im Überblick
Das .NET Compiler Platform SDK wird nicht automatisch als Teil der Workload „Visual Studio-
Extensionentwicklung“ ausgewählt. Sie müssen sie als optionale Komponente auswählen.
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Aktivieren Sie die Workload Visual Studio-Extensionentwicklung .
4. Öffnen Sie den Knoten Visual Studio-Extensionentwicklung in der Zusammenfassungsstruktur.
5. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an letzter Stelle
unter den optionalen Komponenten.
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Öffnen Sie den Knoten Einzelne Komponenten in der Zusammenfassungsstruktur.
2. Aktivieren Sie das Kontrollkästchen für den DGML-Editor .
Installation mithilfe des Visual Studio -Installers: Registerkarte „Einzelne Komponenten“
1. Führen Sie den Visual Studio-Installer aus.
2. Klicken Sie auf Ändern .
3. Klicken Sie auf die Registerkarte Einzelne Komponenten .
4. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK . Sie finden es an oberster Stelle
im Abschnitt Compiler, Buildtools und Laufzeiten .
Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:
1. Aktivieren Sie das Kontrollkästchen für den DGML-Editor . Sie finden es im Abschnitt Codetools .
Das Erstellen und Überprüfen Ihres Analysetools erfolgt in mehreren Schritten:
1. Erstellen der Projektmappe
2. Registrieren von Name und Beschreibung des Analysetools
3. Melden von Warnungen und Empfehlungen des Analysetools
4. Implementieren des Codefixes zum Übernehmen der Empfehlungen
5. Verbessern der Analyse durch Komponententests

Erstellen der Projektmappe


Wählen Sie in Visual Studio Datei > Neu > Projekt... aus, um das Dialogfeld „Neues Projekt“ anzuzeigen.
Wählen Sie unter Visual C# > Er weiterbarkeit Analyzer with code fix (.NET Standard) (Analysetool
mit Codefix (.NET Standard)) aus.
Benennen Sie Ihr Projekt "MakeConst ", und klicken Sie auf „OK“.

Kennenlernen der Vorlage für das Analysetool


Das Analysetool Analyzer mit der Codekorrekturvorlage erstellt fünf Projekte:
MakeConst : Enthält das Analysetool.
MakeConst.CodeFixes : Enthält die Codekorrektur.
MakeConst.Package : Wird verwendet, um das NuGet-Paket für das Analysetool und die Codekorrektur zu
generieren.
MakeConst.Test : Ein Komponententestprojekt.
MakeConst.Vsix : Das Standardstartprojekt, das eine zweite Instanz von Visual Studio startet, die das neue
Analysetool geladen hat. Drücken Sie F5, um das VSIX-Projekt zu starten.

NOTE
Analysetools sollten .NET Standard 2.0 als Ziel verwenden, da sie in der .NET Core-Umgebung (Befehlszeilenbuilds) und
.NET Framework-Umgebung (Visual Studio) ausgeführt werden können.
TIP
Wenn Sie das Analysetool ausführen, wird eine zweite Instanz von Visual Studio gestartet. Diese zweite Kopie verwendet
eine andere Registrierungsstruktur zum Speichern von Einstellungen. Dadurch können Sie in beiden Instanzen von Visual
Studio verschiedene Anzeigeeinstellungen verwenden. Sie können für die Testinstanz von Visual Studio ein anderes Design
auswählen. Achten Sie außerdem darauf, Ihre Einstellungen oder Ihre Anmeldung nicht mithilfe der Testinstanz von Visual
Studio als mobiler Benutzer auf Ihr Visual Studio-Konto zu übertragen. Auf diese Weise bleiben die unterschiedlichen
Einstellungen erhalten.
Die Struktur enthält nicht nur das Analysetool in der Entwicklung, sondern auch alle vorherigen geöffneten Analysetools.
Sie müssen die Roslyn-Struktur manuell unter %LocalAppData%\Microsoft\VisualStudio löschen, um sie zurückzusetzen.
Der Ordnername der Roslyn-Struktur endet auf Roslyn , z. B. 16.0_9ae182f9Roslyn . Beachten Sie, dass Sie die
Projektmappe möglicherweise bereinigen und nach dem Löschen der Struktur neu erstellen müssen.

Erstellen Sie in der zweiten Visual Studio-Instanz, die Sie gerade gestartet haben, ein neues Projekt für eine C#-
Konsolenanwendung (beliebiges Zielframework, Analysetools arbeiten auf Quellebene). Zeigen Sie auf das
wellenförmig unterstrichene Token, dann wird der vom Analysetool bereitgestellte Warnungstext angezeigt.
Die Vorlage erstellt ein Analysetool, das für jede Typdeklaration, deren Typname Kleinbuchstaben enthält, eine
Warnung meldet, wie in der folgenden Abbildung dargestellt:

Die Vorlage stellt darüber hinaus einen Codefix bereit, der jeden Typnamen, der Kleinbuchstaben enthält, in
durchgängig Großbuchstaben ändert. Sie können auf die zusammen mit der Warnung angezeigte Glühbirne
klicken, um die vorgeschlagenen Änderungen anzuzeigen. Wenn Sie die vorgeschlagenen Änderungen
akzeptieren, werden der Typname und alle Verweise auf den betreffenden Typ in der Projektmappe aktualisiert.
Nachdem Sie jetzt das anfängliche Analysetool in Aktion gesehen haben, schließen Sie die zweite Visual Studio-
Instanz, und kehren Sie zu Ihrem Analysetoolprojekt zurück.
Sie brauchen keine zweite Instanz von Visual Studio zu starten und neuen Code zu erstellen, um jede Änderung
an Ihrem Analysetool zu testen. Die Vorlage erstellt für Sie außerdem ein Komponententestprojekt. Dieses
Projekt enthält zwei Tests. TestMethod1 zeigt das typische Format eines Tests, der Code analysiert, ohne eine
Diagnose auszulösen. TestMethod2 zeigt das Format eines Tests, der eine Diagnose auslöst und dann einen
vorgeschlagenen Codefix anwendet. Beim Erstellen Ihres Analysetools und Codefixes werden Sie Tests für
verschiedene Codestrukturen schreiben, um Ihre Arbeit zu überprüfen. Komponententests für Analysetools sind
viel schneller als interaktive Tests in Visual Studio.

TIP
Analysetool-Komponententests sind ein hervorragendes Werkzeug, wenn Sie wissen, welche Codekonstrukte Ihr
Analysetool auslösen sollten und welche nicht. Das Laden Ihres Analysetools in einer weiteren Visual Studio-Instanz ist ein
tolles Hilfsmittel, um Konstrukte zu finden und zu untersuchen, die Ihnen möglicherweise noch nicht in den Sinn
gekommen sind.

In diesem Tutorial schreiben Sie ein Analysetool, das dem Benutzer alle lokalen Variablendeklarationen meldet,
die in lokale Konstanten konvertiert werden können. Beachten Sie z. B. folgenden Code:
int x = 0;
Console.WriteLine(x);

Im Code oben ist x ein konstanter Wert zugewiesen, der nie geändert wird. Er kann mithilfe des const -
Modifizierers deklariert werden:

const int x = 0;
Console.WriteLine(x);

Dies bringt die Analyse mit sich, mit der bestimmt wird, ob eine Variable zu einer Konstanten gemacht werden
kann, wozu Syntaxanalyse, Konstantenanalyse des Initialisiererausdrucks und eine Datenflussanalyse
erforderlich sind, um sicherzustellen, dass zu keinem Zeitpunkt in die Variable geschrieben wird. Die .NET
Compiler Platform stellt APIs zur Verfügung, die das Durchführen dieser Analyse erleichtern.

Erstellen von Analysetoolregistrierungen


Die Vorlage erstellt die anfängliche DiagnosticAnalyzer -Klasse in der Datei MakeConstAnalyzer.cs. Dieses
anfängliche Analysetool zeigt zwei wichtige Eigenschaften jedes Analysetools.
Jedes Diagnoseanalysetool muss ein [DiagnosticAnalyzer] -Attribut bereitstellen, das die Sprache beschreibt,
in der es arbeitet.
Jedes Diagnoseanalysetool muss (direkt oder indirekt) von der DiagnosticAnalyzer-Klasse abgeleitet sein.
Die Vorlage zeigt außerdem die grundlegenden Features, die jedes Analysetool auszeichnen:
1. Registrieren von Aktionen. Die Aktionen stellen Codeänderungen dar, die Ihr Analysetool auslösen sollten,
um den Code auf Verstöße hin zu untersuchen. Wenn Visual Studio Codebearbeitungen erkennt, die mit
einer registrierten Aktion übereinstimmen, ruft es die registrierte Methode Ihres Analysetools auf.
2. Erstellen von Diagnosen. Wenn Ihr Analysemodul einen Verstoß erkennt, erstellt es ein Diagnoseobjekt, das
von Visual Studio verwendet wird, um den Benutzer vom Verstoß zu benachrichtigen.
Sie registrieren Aktionen in Ihrer Überschreibung der DiagnosticAnalyzer.Initialize(AnalysisContext)-Methode. In
diesem Tutorial suchen Sie auf der Suche nach lokalen Deklarationen Syntaxknoten auf und sehen, welche von
ihnen konstante Werte aufweisen. Wenn eine Deklaration eine Konstante vorsehen könnte, erstellt und meldet
Ihr Analysetool eine Diagnose.
Der erste Schritt besteht darin, die Registrierungskonstanten und die Initialize -Methode zu aktualisieren,
damit diese Konstanten Ihr „Make Const“-Analysetool anzeigen. Die meisten der Zeichenfolgenkonstanten sind
in der Zeichenfolgen-Ressourcendatei definiert. Sie sollten sich zwecks einfacherer Lokalisierung auch an diese
Praxis halten. Öffnen Sie die Resources.resx-Datei für das MakeConst -Analysetoolprojekt. Dadurch wird der
Ressourcen-Editor angezeigt. Aktualisieren Sie die Zeichenfolgenressourcen wie folgt:
Ändern Sie AnalyzerDescription in „Variables that are not modified should be made constants.“.
Ändern Sie AnalyzerMessageFormat in „Variable '{0}' can be made constant“.
Ändern Sie AnalyzerTitle in „Variable can be made constant“.

Wenn Sie fertig sind, sollte der Ressourcen-Editor wie in der folgenden Abbildung gezeigt aussehen:

Die verbleibenden Änderungen finden sich in der Analysetooldatei. Öffnen Sie MakeConstAnalyzer.cs in Visual
Studio. Ändern Sie die registrierte Aktion von einer Aktion, die für Symbole aktiv ist, in eine, die mit Syntax
agiert. Suchen Sie in der MakeConstAnalyzerAnalyzer.Initialize -Methode die Zeile, die die Aktion für Symbole
registriert:

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

Ersetzen Sie sie durch die folgende Zeile:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

Nach dieser Änderung können Sie die AnalyzeSymbol -Methode löschen. Dieses Analysetool untersucht
SyntaxKind.LocalDeclarationStatement-Anweisungen, keine SymbolKind.NamedType-Anweisungen. Beachten
Sie, dass AnalyzeNode mit roten Wellenlinien unterstrichen ist. Der Code, den Sie soeben hinzugefügt haben,
verweist auf eine AnalyzeNode -Methode, die noch nicht deklariert wurde. Deklarieren Sie diese Methode
mithilfe des folgenden Codes:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)


{
}

Ändern Sie Category wie im folgenden Code gezeigt in „Usage“ in MakeConstAnalyzer.cs:

private const string Category = "Usage";

Suchen von lokalen Deklarationen, die „const“ lauten könnten


Es ist Zeit, die erste Version der AnalyzeNode -Methode zu schreiben. Sie soll nach einer einzelnen lokalen
Deklaration suchen, die const sein könnte, es aber nicht ist, wie der folgende Code:

int x = 0;
Console.WriteLine(x);

Der erste Schritt besteht darin, nach lokalen Deklarationen zu suchen. Fügen Sie in MakeConstAnalyzer.cs den
folgenden Code zu AnalyzeNode hinzu:

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

Diese Umwandlung funktioniert immer, da Ihr Analysetool für Änderungen an lokalen Deklarationen und nur an
lokalen Deklarationen registriert wurde. Kein anderer Knotentyp löst einen Aufruf Ihrer AnalyzeNode -Methode
aus. Überprüfen Sie als Nächstes die Deklaration für alle const -Modifizierer. Wenn Sie sie finden, geben Sie
sofort zurück. Der folgende Code sucht nach allen const -Modifizierern in der lokalen Deklaration:

// make sure the declaration isn't already const:


if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
return;
}

Schließlich müssen Sie überprüfen, ob die Variable const sein könnte. Das bedeutet, sicherzustellen, dass sie
nach der Initialisierung nie zugewiesen wird.
Sie führen semantische Analysen mithilfe von SyntaxNodeAnalysisContext durch. Sie verwenden das context -
Argument, um festzustellen, ob die lokale Variablendeklaration als const festgelegt werden kann. Ein
Microsoft.CodeAnalysis.SemanticModel stellt sämtliche semantischen Informationen in einer einzelnen
Quelldatei dar. Mehr können Sie im Artikel über semantische Modelle erfahren. Sie verwenden das
Microsoft.CodeAnalysis.SemanticModel zum Durchführen einer Datenflussanalyse für die lokale
Deklarationsanweisung. Anschließend nutzen Sie die Ergebnisse dieser Datenflussanalyse, um sicherzustellen,
dass zu keiner Zeit irgendwo ein neuer Wert in die lokale Variable geschrieben wird. Rufen Sie die
GetDeclaredSymbol-Erweiterungsmethode auf, um das ILocalSymbol für die Variable abzurufen und zu
sicherzustellen, dass sie nicht in der DataFlowAnalysis.WrittenOutside-Sammlung der Datenflussanalyse
enthalten ist. Fügen Sie am Ende der AnalyzeNode -Methode den folgenden Code hinzu:

// Perform data flow analysis on the local declaration.


DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
return;
}

Der Code, den Sie soeben hinzugefügt haben, stellt sicher, dass die Variable nicht verändert wird und daher als
const deklariert werden kann. Es ist Zeit, die Diagnose zu stellen. Fügen Sie in AnalyzeNode den folgenden
Code als letzte Zeile hinzu:

context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(),
localDeclaration.Declaration.Variables.First().Identifier.ValueText));

Sie können Ihren Fortschritt überprüfen, indem Sie F5 drücken, um Ihr Analysetool auszuführen. Sie können die
Konsolenanwendung laden, die Sie zuvor erstellt haben, und dann den folgenden Testcode hinzufügen:

int x = 0;
Console.WriteLine(x);

Die Glühbirne sollte angezeigt werden, und Ihr Analysetool sollte eine Diagnose melden. Allerdings verwendet
die Glühbirne noch den aus der Vorlage erzeugten Codefix und informiert Sie, dass die Variable in
Großbuchstaben geschrieben werden kann. Im nächsten Abschnitt erfahren Sie, wie der Codefix geschrieben
wird.

Schreiben des Codefixes


Ein Analysetool kann einen Codefix oder mehrere zur Verfügung stellen. Ein Codefix definiert eine Bearbeitung,
die das gemeldete Problem angeht. Für das Analysetool, das Sie erstellt haben, können Sie einen Codefix
bereitstellen, der das Schlüsselwort „const“ einfügt:

- int x = 0;
+ const int x = 0;
Console.WriteLine(x);

Der Benutzer wählt es im Editor in der Benutzeroberfläche der Glühbirne aus, und Visual Studio ändert den
Code.
Öffnen Sie die Datei CodeFixResources.resx, und ändern Sie CodeFixTitle in „Make constant“.
Öffnen Sie die von der Vorlage hinzugefügte Datei MakeConstCodeFixProvider.cs. Dieser Codefix ist bereits mit
der Diagnose-ID verschaltet, die von Ihrem Diagnoseanalysetool erzeugt wird, er implementiert jedoch noch
nicht die gewünschte Codetransformation.
Löschen Sie als Nächstes die MakeUppercaseAsync -Methode. Sie trifft nicht mehr zu.
Alle Codefixanbieter werden von CodeFixProvider abgeleitet. Sie alle überschreiben
CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext), um verfügbare Codefixe zu melden. Ändern Sie in
RegisterCodeFixesAsync den Typ des Vorgängerknotens, nach dem Sie suchen, in
LocalDeclarationStatementSyntax, damit er mit der Diagnose übereinstimmt:

var declaration =
root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>
().First();

Ändern Sie dann die letzte Zeile, um einen Codefix zu registrieren. Ihr Fix erstellt ein neues Dokument, das sich
daraus ergibt, dass einer vorhandenen Deklaration der const -Modifizierer hinzugefügt wird:

// Register a code action that will invoke the fix.


context.RegisterCodeFix(
CodeAction.Create(
title: CodeFixResources.CodeFixTitle,
createChangedDocument: c => MakeConstAsync(context.Document, declaration, c),
equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
diagnostic);

Sie werden rote Wellenlinien in dem soeben hinzugefügten Code unter dem Symbol MakeConstAsync bemerken.
Fügen Sie eine Deklaration für MakeConstAsync hinzu, wie etwa den folgenden Code:

private static async Task<Document> MakeConstAsync(Document document,


LocalDeclarationStatementSyntax localDeclaration,
CancellationToken cancellationToken)
{
}

Ihre neue MakeConstAsync -Methode transformiert das Document, das die Quelldatei des Benutzers darstellt, in
ein neues Document, das jetzt eine const -Deklaration enthält.
Sie erstellen ein neues const -Schlüsselworttoken, um es am Anfang der Deklarationsanweisung einzufügen.
Achten Sie darauf, zuerst alle führenden Trivia aus dem ersten Token der Deklarationsanweisung zu entfernen
und sie an das const -Token anzufügen. Fügen Sie der MakeConstAsync -Methode folgenden Code hinzu:

// Remove the leading trivia from the local declaration.


SyntaxToken firstToken = localDeclaration.GetFirstToken();
SyntaxTriviaList leadingTrivia = firstToken.LeadingTrivia;
LocalDeclarationStatementSyntax trimmedLocal = localDeclaration.ReplaceToken(
firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty));

// Create a const token with the leading trivia.


SyntaxToken constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword,
SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker));

Fügen Sie als Nächstes der Deklaration das const -Token mithilfe des folgenden Codes hinzu:
// Insert the const token into the modifiers list, creating a new modifiers list.
SyntaxTokenList newModifiers = trimmedLocal.Modifiers.Insert(0, constToken);
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal
.WithModifiers(newModifiers)
.WithDeclaration(localDeclaration.Declaration);

Formatieren Sie anschließend die neue Deklaration gemäß den C#-Formatierungsregeln. Das Formatieren Ihrer
Änderungen in Anlehnung an den vorhandenen Code macht einen besseren Eindruck. Fügen Sie unmittelbar
hinter dem vorhandenen Code die folgende Anweisung hinzu:

// Add an annotation to format the new local declaration.


LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);

Für diesen Code ist ein neuer Namespace erforderlich. Fügen Sie am Anfang der Datei die folgende using -
Anweisung hinzu:

using Microsoft.CodeAnalysis.Formatting;

Der letzte Schritt besteht im Ausführen Ihrer Bearbeitung. Dieser Prozess besteht aus drei Schritten:
1. Abrufen eines Handles zu dem vorhandenen Dokument.
2. Erstellen eines neuen Dokuments durch Ersetzen der vorhandenen Deklaration durch die neue Deklaration.
3. Zurückgeben des neuen Dokuments.
Fügen Sie den folgenden Code am Ende der MakeConstAsync -Methode hinzu:

// Replace the old local declaration with the new local declaration.
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);

// Return document with transformed tree.


return document.WithSyntaxRoot(newRoot);

Ihr Codefix ist nun bereit, ausprobiert zu werden. Drücken Sie F5, um das Analysetoolprojekt in einer zweiten
Instanz von Visual Studio auszuführen. Erstellen Sie in der zweiten Visual Studio-Instanz ein neues
Konsolenanwendungsprojekt in C#, und fügen Sie der Methode „Main“ einige lokale Variablendeklarationen
hinzu, die mit konstanten Werten initialisiert sind. Sie sehen, dass sie als Warnungen gemeldet werden, wie
unten dargestellt.

Sie haben große Fortschritte erzielt. Unter den Deklarationen, die in const umgewandelt werden können,
werden Wellenlinien angezeigt. Aber die Arbeit ist noch nicht abgeschlossen. Alles hier funktioniert, wenn Sie
const den Deklarationen beginnend bei i , dann weiter mit j und schließlich k hinzufügen. Wenn Sie den
const -Modifizierer aber in einer anderen Reihenfolge zu i zuweisen, beginnend mit k , erzeugt Ihr Analysetool
Fehler: k kann nicht const deklariert werden, sofern nicht i und j beide bereits const sind. Sie müssen
weitere Analysen durchführen, um sicherzustellen, dass Sie mit den verschiedenen Weisen umgehen können, in
denen Variablen deklariert und initialisiert werden können.
Erstellen von Komponententests
Ihr Analysetool und der Codefix funktionieren beim einfachen Fall einer einzelnen Deklaration, die als „const“
deklariert werden kann. Es gibt eine Vielzahl von möglichen Deklarationsanweisungen, bei denen diese
Implementierung zu Fehlern führt. Sie tragen diesen Fällen Rechnung, indem Sie mit der
Komponententestbibliothek arbeiten, die von der Vorlage erstellt wurde. Das funktioniert viel schneller, als
wiederholt eine zweite Instanz von Visual Studio zu öffnen.
Öffnen Sie die MakeConstUnitTests.cs-Datei im Komponententestprojekt. Die Vorlage hat zwei Tests erstellt, die
den zwei allgemeinen Mustern für einen Komponententest für Analysetools und Codefixe folgen. TestMethod1
zeigt das Muster für einen Test, der sicherstellt, dass das Analysetool keine Diagnose meldet, wenn es das nicht
sollte. TestMethod2 zeigt das Muster für das Melden einer Diagnose und das Ausführen des Codefixes.
Die Vorlage verwendet Microsoft.CodeAnalysis.Testing-Pakete für Komponententests.

TIP
Die Testbibliothek unterstützt eine spezielle Markupsyntax, einschließlich der folgenden Optionen:
[|text|] : Gibt an, dass eine Diagnosemeldung für text vorliegt. Standardmäßig darf diese Form nur zum Testen
von Analysetools mit genau einem DiagnosticDescriptor verwendet werden, der von
DiagnosticAnalyzer.SupportedDiagnostics bereitgestellt wird.
{|ExpectedDiagnosticId:text|} : Gibt an, dass eine Diagnosemeldung mit Id ExpectedDiagnosticId für text
vorliegt.

Ersetzen Sie die Vorlagentests in der Klasse MakeConstUnitTest durch die folgende Testmethode:

[TestMethod]
public async Task LocalIntCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
static void Main()
{
[|int i = 0;|]
Console.WriteLine(i);
}
}
", @"
using System;

class Program
{
static void Main()
{
const int i = 0;
Console.WriteLine(i);
}
}
");
}

Führen Sie diesen Test aus, um zu überprüfen, ob er erfolgreich ist. Öffnen Sie in Visual Studio den Test-
Explorer , indem Sie Test > Windows > Test-Explorer auswählen. Wählen Sie anschließend Alle ausführen
aus.
Erstellen von Tests für gültige Deklarationen
Ganz allgemein sollten Analysetool so schnell wie möglich beendet werden und nur minimale Arbeit verrichten.
Visual Studio ruft registrierte Analysetools auf, während der Benutzer den Code bearbeitet. Reaktionsfähigkeit ist
eine wichtige Anforderung. Es gibt mehrere Testfälle für Code, der Ihre Diagnose nicht auslösen soll. Ihr
Analysetool verarbeitet bereits einen dieser Tests, den Fall, in dem eine Variable nach der Initialisierung
zugewiesen wird. Fügen Sie die folgende Testmethode hinzu, um diesen Fall darzustellen:

[TestMethod]
public async Task VariableIsAssigned_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
static void Main()
{
int i = 0;
Console.WriteLine(i++);
}
}
");
}

Dieser Test wird ebenfalls bestanden. Fügen Sie dann Testmethoden für Bedingungen hinzu, die Sie noch nicht
behandelt haben:
Deklarationen, die bereits const sind, da sie bereits Konstanten sind:

[TestMethod]
public async Task VariableIsAlreadyConst_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
static void Main()
{
const int i = 0;
Console.WriteLine(i);
}
}
");
}

Deklarationen, die keinen Initialisierer besitzen, weil es keinen zu verwendenden Wert gibt:
[TestMethod]
public async Task NoInitializer_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
static void Main()
{
int i;
i = 0;
Console.WriteLine(i);
}
}
");
}

Deklarationen, bei denen der Initialisierer keine Konstante ist, da sie zur Kompilierzeit keine Konstanten
sein können:

[TestMethod]
public async Task InitializerIsNotConstant_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
static void Main()
{
int i = DateTime.Now.DayOfYear;
Console.WriteLine(i);
}
}
");
}

Es kann sogar noch komplizierter sein, da C# mehrere Deklarationen in Form einer Anweisung zulässt.
Betrachten Sie die folgende Zeichenfolgenkonstante aus einem Testfall:

[TestMethod]
public async Task MultipleInitializers_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
static void Main()
{
int i = 0, j = DateTime.Now.DayOfYear;
Console.WriteLine(i);
Console.WriteLine(j);
}
}
");
}

Die Variable i kann als Konstante deklariert werden, die Variable j jedoch nicht. Daher kann aus dieser
Anweisung keine const-Deklaration gemacht werden.
Führen Sie Ihre Tests erneut aus – Sie werden feststellen, dass bei den neuen Testfällen Fehler auftreten.

Aktualisieren Ihres Analysetools, damit korrekte Deklarationen


ignoriert werden
Sie benötigen noch einige Verbesserungen an der AnalyzeNode -Methode Ihres Analysetools, um Code
herauszufiltern, der den Bedingungen entspricht. Es handelt sich bei allen um verwandte Bedingungen, daher
lassen sich auch alle mit ähnlichen Änderungen beheben. Nehmen Sie an AnalyzeNode die folgenden
Änderungen vor:
Ihre semantische Analyse hat eine einzelne Variablendeklaration untersucht. Dieser Code muss sich in einer
foreach -Schleife befinden, die alle innerhalb der gleichen Anweisung deklarierten Variablen untersucht.
Jede deklarierte Variable muss über einen Initialisierer verfügen.
Der Initialisierer jeder deklarierten Variable muss zur Kompilierzeit eine Konstante sein.
Ersetzen Sie in Ihrer AnalyzeNode -Methode die ursprüngliche semantische Analyse:

// Perform data flow analysis on the local declaration.


DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
return;
}

durch den folgenden Codeausschnitt:


// Ensure that all variables in the local declaration have initializers that
// are assigned with constant values.
foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
EqualsValueClauseSyntax initializer = variable.Initializer;
if (initializer == null)
{
return;
}

Optional<object> constantValue = context.SemanticModel.GetConstantValue(initializer.Value,


context.CancellationToken);
if (!constantValue.HasValue)
{
return;
}
}

// Perform data flow analysis on the local declaration.


DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)


{
// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
return;
}
}

Die erste foreach -Schleife untersucht jede Variablendeklaration mithilfe der Syntaxanalyse. Die erste Prüfung
stellt sicher, dass die Variable einen Initialisierer aufweist. Die zweite Prüfung stellt sicher, dass der Initialisierer
eine Konstante ist. Die zweite Schleife enthält die ursprüngliche semantische Analyse. Die semantischen
Prüfungen befinden sich in einer separaten Schleife, da sie einen größeren Einfluss auf die Leistung haben.
Führen Sie Ihre Tests erneut aus – sie sollten alle bestanden werden.

Der letzte Schliff


Sie haben es fast geschafft! Es gibt noch ein paar weitere Bedingungen, mit denen Ihr Analysetool umgehen
muss. Visual Studio ruft Analysetools auf, während der Benutzer Code schreibt. Es tritt häufig der Fall ein, dass
Ihr Analysetool für Code aufgerufen wird, der sich nicht kompilieren lässt. Die Methode AnalyzeNode des
Diagnoseanalysetools überprüft nicht, ob der Konstantenwert in den Variablentyp konvertiert werden kann. Die
derzeitige Implementierung wandelt also eine fehlerhafte Deklaration wie int i = "abc" bereitwillig in eine
lokale Konstante um. Fügen Sie eine Testmethode für diesen Fall hinzu:
[TestMethod]
public async Task DeclarationIsInvalid_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
static void Main()
{
int x = {|CS0029:""abc""|};
}
}
");
}

Darüber hinaus werden Verweistypen nicht ordnungsgemäß behandelt. Der einzige konstante Wert, der für
einen Verweistyp zulässig ist, ist null . Nur für System.String sind Zeichenfolgenliterale zulässig. Das heißt,
const string s = "abc" ist zulässig, const object s = "abc" aber nicht. Diese Bedingung wird mit diesem
Codeausschnitt überprüft:

[TestMethod]
public async Task DeclarationIsNotString_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
static void Main()
{
object s = ""abc"";
}
}
");
}

Wenn Sie gründlich sein wollen, müssen Sie einen weiteren Test hinzufügen, um sicherzustellen, dass Sie für
eine Zeichenfolge eine Konstantendeklaration erstellen können. Der folgende Codeausschnitt definiert sowohl
den Code, der die Diagnose auslöst, als auch den Code nach der Anwendung des Fixes:
[TestMethod]
public async Task StringCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
static void Main()
{
[|string s = ""abc"";|]
}
}
", @"
using System;

class Program
{
static void Main()
{
const string s = ""abc"";
}
}
");
}

Schließlich ergreift bei einer Variablen, die mit dem Schlüsselwort var deklariert ist, der Codefix die falsche
Aktion und generiert eine const var -Deklaration, was von der Sprache C# nicht unterstützt wird. Um diesen
Fehler zu beheben, muss der Codefix das Schlüsselwort var durch den Namen des abgeleiteten Typs ersetzen:
[TestMethod]
public async Task VarIntDeclarationCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
static void Main()
{
[|var item = 4;|]
}
}
", @"
using System;

class Program
{
static void Main()
{
const int item = 4;
}
}
");
}

[TestMethod]
public async Task VarStringDeclarationCouldBeConstant_Diagnostic()
{
await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
static void Main()
{
[|var item = ""abc"";|]
}
}
", @"
using System;

class Program
{
static void Main()
{
const string item = ""abc"";
}
}
");
}

Glücklicherweise lassen sich alle oben aufgeführten Fehler mithilfe genau der Techniken beheben, die Sie
gerade gelernt haben.
Zum Beheben des ersten Fehlers öffnen Sie zuerst MakeConstAnalyzer.cs und suchen die foreach-Schleife, in der
jeder der Initialisierer der lokalen Deklaration überprüft wird, um sicherzustellen, dass allen konstante Werte
zugewiesen wurden. Rufen Sie unmittelbar vor der ersten foreach-Schleife context.SemanticModel.GetTypeInfo()
auf, um Detailinformationen über den deklarierten Typ der lokalen Deklaration abzurufen:

TypeSyntax variableTypeName = localDeclaration.Declaration.Type;


ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName,
context.CancellationToken).ConvertedType;
Überprüfen Sie anschließend innerhalb Ihrer foreach -Schleife jeden Initialisierer, um sicherzustellen, dass er in
den Variablentyp konvertiert werden kann. Fügen Sie die folgende Überprüfung hinzu, nachdem sichergestellt
wurde, dass der Initialisierer eine Konstante ist:

// Ensure that the initializer value can be converted to the type of the
// local declaration without a user-defined conversion.
Conversion conversion = context.SemanticModel.ClassifyConversion(initializer.Value, variableType);
if (!conversion.Exists || conversion.IsUserDefined)
{
return;
}

Die nächste Änderung baut auf der letzten auf. Fügen Sie vor der schließenden geschweiften Klammer der
ersten foreach-Schleife den folgenden Code hinzu, um den Typ der lokalen Deklaration zu überprüfen, wenn die
Konstante eine Zeichenfolge oder NULL ist.

// Special cases:
// * If the constant value is a string, the type of the local declaration
// must be System.String.
// * If the constant value is null, the type of the local declaration must
// be a reference type.
if (constantValue.Value is string)
{
if (variableType.SpecialType != SpecialType.System_String)
{
return;
}
}
else if (variableType.IsReferenceType && constantValue.Value != null)
{
return;
}

Sie müssen etwas mehr Code in Ihrem Codefixanbieter schreiben, um das Schlüsselwort var durch den
korrekten Typnamen zu ersetzen. Kehren Sie zu MakeConstCodeFixProvider.cs zurück. Der Code, den Sie
hinzufügen, führt die folgenden Schritte aus:
Überprüft, ob die Deklaration eine var -Deklaration ist, und wenn dies zutrifft:
Erstellt einen neuen Typ für den abgeleiteten Typ.
Vergewissert sich, dass die Typdeklaration kein Alias ist. Ist das der Fall, ist eine Deklaration als const var
zulässig.
Stellt sicher, dass var kein Typname in diesem Programm ist. (In dem Fall ist const var zulässig).
Vereinfacht den vollständigen Typnamen
Das hört sich nach ziemlich viel Code an. Ist es aber nicht. Ersetzen Sie die Zeile, in der newLocal deklariert und
initialisiert wird, durch den folgenden Code. Er gehört direkt hinter die Initialisierung von newModifiers :
// If the type of the declaration is 'var', create a new type name
// for the inferred type.
VariableDeclarationSyntax variableDeclaration = localDeclaration.Declaration;
TypeSyntax variableTypeName = variableDeclaration.Type;
if (variableTypeName.IsVar)
{
SemanticModel semanticModel = await
document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

// Special case: Ensure that 'var' isn't actually an alias to another type
// (e.g. using var = System.String).
IAliasSymbol aliasInfo = semanticModel.GetAliasInfo(variableTypeName, cancellationToken);
if (aliasInfo == null)
{
// Retrieve the type inferred for var.
ITypeSymbol type = semanticModel.GetTypeInfo(variableTypeName, cancellationToken).ConvertedType;

// Special case: Ensure that 'var' isn't actually a type named 'var'.
if (type.Name != "var")
{
// Create a new TypeSyntax for the inferred type. Be careful
// to keep any leading and trailing trivia from the var keyword.
TypeSyntax typeName = SyntaxFactory.ParseTypeName(type.ToDisplayString())
.WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
.WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

// Add an annotation to simplify the type name.


TypeSyntax simplifiedTypeName = typeName.WithAdditionalAnnotations(Simplifier.Annotation);

// Replace the type in the variable declaration.


variableDeclaration = variableDeclaration.WithType(simplifiedTypeName);
}
}
}
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal.WithModifiers(newModifiers)
.WithDeclaration(variableDeclaration);

Sie müssen eine using -Anweisung hinzufügen, um den Simplifier-Typ zu verwenden:

using Microsoft.CodeAnalysis.Simplification;

Führen Sie Ihre Tests aus – sie sollten alle bestanden werden. Gratulieren Sie sich, indem Sie Ihr fertiges
Analysetool ausführen. Drücken Sie STRG+F5, um das Analysetoolprojekt in einer zweiten Instanz von Visual
Studio mit geladener Roslyn-Vorschauerweiterung auszuführen.
Erstellen Sie in der zweiten Visual Studio-Instanz ein neues C#-Konsolenanwendungsprojekt, und fügen Sie
int x = "abc"; zur Methode „Main“ hinzu. Dank der ersten Fehlerbehebung sollte keine Warnung für diese
lokale Variablendeklaration gemeldet werden (obwohl es erwartungsgemäß einen Compilerfehler gibt).
Fügen Sie als Nächstes object s = "abc"; zur Methode „Main“ hinzu. Aufgrund der zweiten Fehlerbehebung
sollte keine Warnung gemeldet werden.
Fügen Sie schließlich eine weitere lokale Variable hinzu, die das Schlüsselwort var verwendet. Sie sehen,
dass eine Warnung gemeldet und ein Vorschlag links unterhalb der Meldung angezeigt wird.
Bewegen Sie den Textcursor des Editors über die Wellenlinien-Unterstreichung, und drücken Sie STRG+., um
den vorgeschlagenen Codefix anzuzeigen. Beachten Sie beim Auswählen des Codefixes, dass das
Schlüsselwort var jetzt ordnungsgemäß verarbeitet wird.
Fügen Sie schließlich den folgenden Code hinzu:
int i = 2;
int j = 32;
int k = i + j;

Nach diesen Änderungen erhalten Sie rote Wellenlinien nur unter den ersten zwei Variablen. Fügen Sie const
sowohl zu i als auch zu j hinzu, und Sie erhalten eine neue Warnung zu k , da das nun als const deklariert
werden kann.
Herzlichen Glückwunsch! Sie haben Ihre erste Erweiterung für die .NET Compiler Platform erstellt, die
dynamische Codeanalyse durchführt, um ein Problem zu erkennen, und eine schnelle Problembehebung zu
seiner Korrektur bereitstellt. Auf diesem Weg haben Sie viele der Code-APIs kennengelernt, die Teil der .NET
Compiler Platform SDKs sind (Roslyn-APIs). Sie können Ihre Arbeit anhand des fertiggestellten Beispiels in
unserem GitHub-Beispielrepository überprüfen.

Weitere Ressourcen
Erste Schritte mit der Syntaxanalyse
Erste Schritte mit der semantischen Analyse
C#-Programmierhandbuch
04.11.2021 • 2 minutes to read

Dieser Abschnitt bietet ausführliche Informationen zu wichtigen Funktionen von C# sowie zu Funktionen, die
über .NET für C# verfügbar sind.
In diesem Abschnitt wird größtenteils davon ausgegangen, dass Sie bereits über einige Kenntnisse zu C# und
allgemeinen Programmierkonzepten verfügen. Wenn Sie Einsteiger beim Programmieren oder bei C# sind, sind
die Tutorials zur Einführung in C# oder das Tutorial zu .NET im Browser am besten für Sie geeignet, da für diese
keine Programmierkenntnisse erforderlich sind.
Informationen zu bestimmten Schlüsselwörtern, Operatoren und Präprozessordirektiven finden Sie in der C#-
Referenz. Informationen zur Spezifikation für C# finden Sie unter C#-Sprachspezifikation.

Programmabschnitte
Einblicke in ein C#-Programm
Main() und Befehlszeilenargumente

Abschnitte zur Sprache


Anweisungen, Ausdrücke und Operatoren
Typen
Objektorientiertes Programmieren
Schnittstellen
Delegaten
Arrays
Zeichenfolgen
Eigenschaften
Indexer
Ereignisse
Generics
Iteratoren
LINQ-Abfrageausdrücke
Namespaces
Unsicherer Code und Zeiger
XML-Dokumentationskommentare

Abschnitte zur Plattform


Anwendungsdomänen
Assemblys in .NET
Attribute
Sammlungen
Ausnahmen und Ausnahmebehandlung
Das Dateisystem und die Registrierung (C#-Programmierhandbuch)
Interoperabilität
Reflexion

Siehe auch
C#-Referenz
Programmierkonzepte (C#)
04.11.2021 • 2 minutes to read

Dieser Abschnitt erläutert die Programmierkonzepte der Sprache C#.

In diesem Abschnitt
T IT EL B ESC H REIB UN G

Assemblys in .NET Hier erfahren Sie, wie Sie Assemblys erstellen und
verwenden.

Asynchrone Programmierung mit „async“ und „await“ (C#) Beschreibt, wie Sie asynchrone Projektmappen mithilfe der
Schlüsselwörter async und await in C# schreiben. Enthält
eine exemplarische Vorgehensweise.

Attribute (C#) Beschreibt, wie zusätzliche Informationen über


Programmierelemente, wie Typen, Felder, Methoden und
Eigenschaften, mit Attributen bereitgestellt werden können.

Auflistungen (C#) Beschreibt einige der Auflistungstypen, die von .NET


bereitgestellt werden. Veranschaulicht, wie einfache
Auflistungen und Auflistungen von Schlüssel-Wert-Paaren
verwendet werden.

Kovarianz und Kontravarianz (C#) Zeigt, wie die implizite Konvertierung von generischen
Typparametern in Schnittstellen und Delegaten aktiviert
wird.

Ausdrucksbaumstrukturen (C#) Erläutert, wie Sie Ausdrucksbaumstrukturen für die


dynamische Änderung von ausführbarem Codes verwenden
können.

Iteratoren (C#) Beschreibt Iteratoren, die verwendet werden, um


Auflistungen zu durchlaufen und um Elemente einzeln
zurückzugeben.

Language Integrated Query (LINQ) (C#) Behandelt die leistungsstarken Abfragefunktionen in der
Sprachsyntax von C# sowie das Abfragemodell für relationale
Datenbanken, XML-Dokumente, Datasets und
speicherinterne Auflistungen.

Reflektion (C#) Erläutert, wie Sie mithilfe von Reflektion Instanzen von Typen
dynamisch erzeugen, Typen an ein vorhandenes Objekt
binden und Typinformationen von vorhandenen Objekten
abfragen können. Ebenso wird erläutert wie die Methoden
vorhandener Objekte aufgerufen und auf ihre Felder und
Eigenschaften zugegriffen werden kann.

Serialisierung (C#) Beschreibt wichtige Konzepte der binären, XML- und SOAP-
Serialisierung.
Verwandte Abschnitte
Tipps zur Leistungssteigerung
Erläutert verschiedene grundlegende Regeln, die Ihnen helfen können, die Leistung der Anwendung zu
verbessern.
Asynchrone Programmierung mit async und await
04.11.2021 • 16 minutes to read

Das aufgabenbasierte asynchrone Programmiermodell stellt eine Abstraktion über asynchronen Code bereit. Sie
können Code in gewohnter Weise als eine Folge von Anweisungen schreiben. Sie können diesen Code so lesen,
als ob jede Anweisung abgeschlossen wäre, bevor die nächste Anweisung beginnt. Der Compiler führt mehrere
Transformationen durch, da möglicherweise einige dieser Anweisungen gestartet werden und dann einen Task
zurückgeben, der die derzeit ausgeführte Arbeit darstellt.
Dies ist das Ziel dieser Syntax: Code zu aktivieren, der sich wie eine Folge von Anweisungen liest, aber in einer
deutlich komplizierteren Reihenfolge ausgeführt wird, die auf einer externen Ressourcenzuordnung und dem
Abschluss von Aufgaben basiert. Vergleichbar ist dies mit der Art und Weise, wie Menschen Anweisungen für
Prozesse erteilen, die asynchrone Aufgaben enthalten. In diesem Artikel verwenden Sie als Beispiel
Anweisungen für die Zubereitung eines Frühstücks, um zu lernen, wie die Schlüsselwörter async und await
die Analyse von Code erleichtern, der eine Reihe asynchroner Anweisungen enthält. Um die Zubereitung eines
Frühstücks zu erläutern, würden Sie Anweisungen schreiben, und Ihre Liste sähe ungefähr so aus:
1. Schenken Sie sich eine Tasse Kaffee ein.
2. Erhitzen Sie eine Pfanne, und braten Sie darin zwei Eier.
3. Braten Sie drei Scheiben Frühstücksspeck.
4. Toasten Sie zwei Scheiben Brot.
5. Bestreichen Sie das getoastete Brot mit Butter und Marmelade.
6. Schenken Sie sich ein Glas Orangensaft ein.
Wenn Sie über Erfahrung im Kochen verfügen, würden Sie diese Anweisungen asynchron ausführen. Sie
würden zunächst die Pfanne für die Eier erhitzen und dann mit dem Frühstücksspeck beginnen. Sie würden das
Brot in den Toaster stecken und danach mit den Eiern beginnen. Bei jedem Schritt des Prozesses würden Sie eine
Aufgabe starten und dann Ihre Aufmerksamkeit auf Aufgaben lenken, die für Ihre Aufmerksamkeit bereit sind.
Die Zubereitung eines Frühstücks ist ein gutes Beispiel für asynchrone Arbeiten, die nicht parallel ausgeführt
werden. Eine Person (oder ein Thread) kann alle diese Aufgaben erledigen. Um beim Beispiel des Frühstücks zu
bleiben: Eine Person kann das Frühstück asynchron zubereiten, indem sie die nächste Aufgabe startet, bevor die
erste Aufgabe abgeschlossen ist. Die Zubereitung schreitet voran, und zwar unabhängig davon, ob jemand eine
Auge darauf hat oder nicht. Sobald Sie damit beginnen, die Pfanne für die Eier zu erhitzen, können Sie mit dem
Braten des Frühstücksspecks beginnen. Nachdem Sie das Braten des Frühstücksspecks begonnen haben,
können Sie das Brot in den Toaster stecken.
Für einen parallelen Algorithmus bräuchten Sie mehrere Köche (bzw. Threads). Ein Koch würde sich um die Eier
kümmern, ein weiterer um den Frühstücksspeck usw. Jeder Koch würde sich nur auf diese eine Aufgabe
konzentrieren. Jeder Koch (bzw. Thread) würde synchron blockiert, während er darauf wartet, dass der
Frühstücksspeck gewendet werden muss oder der Toaster das Brot auswirft.
Sehen Sie sich nun dieselben Anweisungen als C#-Anweisungen an:

using System;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
class Program
{
static void Main(string[] args)
{
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");

Egg eggs = FryEggs(2);


Console.WriteLine("eggs are ready");

Bacon bacon = FryBacon(3);


Console.WriteLine("bacon is ready");

Toast toast = ToastBread(2);


ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");

Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}

private static Juice PourOJ()


{
Console.WriteLine("Pouring orange juice");
return new Juice();
}

private static void ApplyJam(Toast toast) =>


Console.WriteLine("Putting jam on the toast");

private static void ApplyButter(Toast toast) =>


Console.WriteLine("Putting butter on the toast");

private static Toast ToastBread(int slices)


{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");

return new Toast();


}

private static Bacon FryBacon(int slices)


{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
Task.Delay(3000).Wait();
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
Task.Delay(3000).Wait();
Console.WriteLine("Put bacon on plate");

return new Bacon();


}

private static Egg FryEggs(int howMany)


{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}

private static Coffee PourCoffee()


{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}

Es hat ungefähr 30 Minuten gedauert, das Frühstück synchron zuzubereiten. Diese Dauer entspricht der Summe
der einzelnen Aufgaben.

NOTE
Die Klassen Coffee , Egg , Bacon , Toast und Juice sind leer. Sie sind lediglich Markerklassen für
Demonstrationszwecke und enthalten keine Eigenschaften.

Computer interpretieren diese Anweisungen anders als Menschen. Nach jeder Anweisung blockiert der
Computer das weitere Vorgehen, bis die Arbeit abgeschlossen ist. Erst danach fährt er mit der nächsten
Anweisung fort. So käme kein schmackhaftes Frühstück zustande. Die späteren Aufgaben würden erst gestartet,
wenn die früheren Aufgaben abgeschlossen sind. Die Zubereitung des Frühstücks würde wesentlich länger
dauern, und einige Komponenten wären bereits wieder kalt, bis sie serviert werden.
Wenn der Computer die obigen Anweisungen asynchron ausführen soll, müssen Sie asynchronen Code
schreiben.
Diese Überlegungen sind wichtig für die Programme, die Sie heutzutage schreiben. Wenn Sie Client-Programme
schreiben, möchten Sie, dass die Benutzeroberfläche auf Benutzereingaben reagiert. Ihre Anwendung sollte
nicht den Eindruck erwecken, dass sich das Smartphone aufgehängt hat, während es Daten aus dem Web
herunterlädt. Wenn Sie Serverprogramme schreiben, möchten Sie nicht, dass Threads blockiert werden. Diese
Threads könnten für andere Anforderungen benötigt werden. Die Verwendung von synchronem Code, wenn
asynchrone Alternativen vorhanden sind, beeinträchtigt Ihre Möglichkeiten für günstigere Erweiterungen. Sie
bezahlen für die blockierten Threads.
Erfolgreiche moderne Anwendungen erfordern asynchronen Code. Ohne Sprachunterstützung erforderte das
Schreiben von asynchronem Code Rückrufe, Abschlussereignisse oder andere Methoden, die die ursprüngliche
Absicht des Codes verdeckten. Der Vorteil des synchronen Codes besteht darin, dass durch das schrittweise
Ausführen der Aktionen das Scannen und Verstehen erleichtert werden. Bei traditionellen asynchronen
Modellen mussten Sie sich auf die asynchronen Eigenschaften des Codes und nicht auf die grundlegenden
Aktionen des Codes konzentrieren.

Nicht blockieren, stattdessen „await“ verwenden


Der obige Code zeigt eine schlechte Praxis: das Erstellen von synchronem Code zum Ausführen asynchroner
Vorgänge. In der vorliegenden Form hindert dieser Code den Thread an der Ausführung aller anderen Arbeiten.
Es wird nicht unterbrochen, während eine der anderen Aufgaben ausgeführt wird. Dies wäre so, als würden Sie
den Toaster anstarren, nachdem Sie das Brot hineingesteckt haben. Sie wären so lange für niemanden
ansprechbar, bis das getoastete Brot ausgeworfen wurde.
Aktualisieren wir also diesen Code so, dass der Thread nicht blockiert wird, während Aufgaben ausgeführt
werden. Das Schlüsselwort await bietet die Möglichkeit, eine Aufgabe zu starten und dann die Ausführung
fortzusetzen, wenn diese Aufgabe abgeschlossen ist, ohne dass es dabei zu einer Blockierung kommt. Eine
einfache asynchrone Version des Codes für die Frühstückszubereitung sähe daher wie der folgende
Codeausschnitt aus:

static async Task Main(string[] args)


{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");

Egg eggs = await FryEggsAsync(2);


Console.WriteLine("eggs are ready");

Bacon bacon = await FryBaconAsync(3);


Console.WriteLine("bacon is ready");

Toast toast = await ToastBreadAsync(2);


ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");

Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}

IMPORTANT
Die insgesamt verstrichene Zeit entspricht ungefähr der anfänglichen synchronen Version. Der Code muss noch darauf
ausgelegt werden, einige wichtige Features der asynchronen Programmierung zu nutzen.

TIP
Die Methodenkörper von FryEggsAsync , FryBaconAsync und ToastBreadAsync wurden aktualisiert, sodass sie jetzt
Task<Egg> , Task<Bacon> und Task<Toast> zurückgeben. Die Methoden werden umbenannt und enthalten dann das
Suffix „Async“. Ihre Implementierungen werden als Teil der endgültigen Version weiter unten in diesem Artikel gezeigt.

Dieser Code verursacht keine Blockierung, während die Eier oder der Frühstücksspeck gebraten werden. Dieser
Code startet jedoch keine anderen Aufgaben. Sie würden weiterhin das Brot in den Toaster stecken und das
Gerät so lange anstarren, bis das Brot ausgeworfen wird. Aber Sie wären zumindest für andere Personen
ansprechbar, die Ihre Aufmerksamkeit wünschen. In einem Restaurant, in dem mehrere Bestellungen
aufgegeben werden, könnte der Koch mit der Zubereitung eines weiteren Frühstücks beginnen, während das
erste Frühstück zubereitet wird.
Jetzt wird der Thread für die Frühstückszubereitung nicht blockiert, während er auf gestartete Aufgaben wartet,
die noch nicht abgeschlossen sind. Bei einigen Anwendungen reicht diese Änderung bereits aus. Alleine diese
Änderung führt bereits dazu, dass eine GUI-Anwendung weiterhin auf den Benutzer reagiert. In diesem Szenario
möchten Sie jedoch mehr. Die einzelnen Komponenten bzw. Aufgaben sollen nicht sequenziell ausgeführt
werden. Es ist besser, die einzelnen Komponenten/Aufgaben zu starten, ohne auf den Abschluss der vorherigen
Aufgabe zu warten.

Aufgaben gleichzeitig starten


In vielen Szenarios möchten Sie mehrere voneinander unabhängige Aufgaben unverzüglich starten. Sobald eine
der Aufgabe abgeschlossen ist, können Sie dann andere Aufgaben fortsetzen, die bereit sind. Um beim Beispiel
des Frühstücks zu bleiben, würden Sie dieses sehr viel schneller zubereiten können. Außerdem können Sie alle
Aufgaben nahezu gleichzeitig fertigstellen. Und erhalten so ein warmes Frühstück.
System.Threading.Tasks.Task und verwandte Typen sind Klassen, mit denen Sie Aufgaben analysieren können,
die gerade ausgeführt werden. So können Sie Code schreiben, der sehr viel stärker der Art und Weise ähnelt,
wie Sie wirklich ein Frühstück zubereiten. Sie würden gleichzeitig mit der Zubereitung von Eiern,
Frühstücksspeck und Toast beginnen. Da hierfür jeweils eine Aktion erforderlich ist, würden Sie Ihre
Aufmerksamkeit auf diese Aufgabe lenken, sich um die nächste Aktion kümmern und dann auf etwas anderes
warten, das Ihre Aufmerksamkeit erfordert.
Sie starten eine Aufgabe und behalten dann das Task-Objekt bei, das die Arbeit repräsentiert. Sie warten jede
Aufgabe ab („ await “), bevor Sie mit ihrem Ergebnis arbeiten.
Lassen Sie uns nun die entsprechenden Änderungen an dem Code für das Frühstück vornehmen. Der erste
Schritt besteht darin, die Aufgaben für Vorgänge bei deren Start zu speichern, anstatt auf sie zu warten:

Coffee cup = PourCoffee();


Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);


Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");

Task<Bacon> baconTask = FryBaconAsync(3);


Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");

Task<Toast> toastTask = ToastBreadAsync(2);


Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");

Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Console.WriteLine("Breakfast is ready!");

Als Nächstes können Sie die await -Anweisungen für den Frühstücksspeck und die Eier an das Ende der
Methode, vor dem Servieren des Frühstücks, verschieben:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);


Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);

Toast toast = await toastTask;


ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");

Egg eggs = await eggsTask;


Console.WriteLine("Eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");

Console.WriteLine("Breakfast is ready!");

Es hat ungefähr 20 Minuten gedauert, das Frühstück asynchron zuzubereiten. Diese Zeitersparnis kann damit
begründet werden, dass einige Tasks gleichzeitig ausgeführt wurden.
Der obige Code funktioniert besser. Sie starten alle asynchronen Aufgaben gleichzeitig. Sie verwenden „await“
nur für Aufgaben, wenn Sie deren Ergebnisse benötigen. Der obige Code ähnelt beispielsweise Code in einer
Webanwendung, die verschiedene Microservices anfordert und dann die Ergebnisse auf einer einzigen Seite
zusammenfasst. Sie führen alle Anforderungen sofort aus, warten dann aber mit await auf alle diese Aufgaben
und stellen die Webseite zusammen.

Kombination mit Aufgaben


Sie haben alles, was zum Frühstück benötigt wird, gleichzeitig fertig, mit Ausnahme des Toasts. Die Zubereitung
des Toasts ist eine Kombination aus einem asynchronen Vorgang (das Toasten des Brotes) und synchronen
Vorgängen (das Bestreichen mit Butter und Marmelade). Die Aktualisierung dieses Codes veranschaulicht ein
wichtiges Konzept:
IMPORTANT
Die Kombination aus einem asynchronen Vorgang gefolgt von einer synchronen Tätigkeit ergibt einen asynchronen
Vorgang. Mit anderen Worten: Wenn ein Teil eines Vorgangs asynchron ist, ist der gesamte Vorgang asynchron.

Der obige Code zeigt, dass Sie Task- oder Task<TResult>-Objekte verwenden können, um laufende Aufgaben
beizubehalten. Sie warten mit „ await “ auf jede Aufgabe, bevor Sie deren Ergebnis verwenden. Der nächste
Schritt besteht im Erstellen von Methoden, die die Kombination anderer Tätigkeiten darstellen. Bevor Sie das
Frühstück servieren, möchten Sie auf die Aufgabe warten, die für das Toasten des Brotes steht, bevor Sie das
getoastete Brot mit Butter und Marmelade bestreichen. Dies können Sie mit dem folgenden Code darstellen:

static async Task<Toast> MakeToastWithButterAndJamAsync(int number)


{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);

return toast;
}

Die obige Methode verfügt in ihrer Signatur über den Modifizierer async . Dies signalisiert dem Compiler, dass
diese Methode eine await -Anweisung enthält. Sie enthält also asynchrone Vorgänge. Diese Methode steht für
die Aufgabe, bei der das Brot getoastet und dann mit Butter und Marmelade bestrichen wird. Diese Methode
gibt das Ergebnis Task<TResult> aus, d. h. die Kombination dieser drei Vorgänge. Der Hauptcodeblock sieht jetzt
wie folgt aus:

static async Task Main(string[] args)


{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");

var eggsTask = FryEggsAsync(2);


var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);

var eggs = await eggsTask;


Console.WriteLine("eggs are ready");

var bacon = await baconTask;


Console.WriteLine("bacon is ready");

var toast = await toastTask;


Console.WriteLine("toast is ready");

Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}

Die obige Änderung veranschaulicht eine wichtige Technik für das Arbeiten mit asynchronem Code. Sie erstellen
Aufgaben, indem Sie die Vorgänge in eine neue Methode unterteilen, die eine Aufgabe zurückgibt. Sie können
entscheiden, wann auf diese Aufgabe gewartet werden soll. Sie können andere Aufgaben gleichzeitig starten.

Asynchrone Ausnahmen
Bis zu diesem Punkt haben Sie implizit angenommen, dass alle diese Tasks erfolgreich abgeschlossen wurden.
Asynchrone Methoden lösen wie ihre synchronen Gegenstücke Ausnahmen aus. Die asynchrone Unterstützung
von Ausnahmen und der Fehlerbehandlung hat im Allgemeinen dieselben Ziele wie die asynchrone
Unterstützung: Sie sollten Code schreiben, der sich wie eine Reihe synchroner Anweisungen liest. Tasks lösen
Ausnahmen aus, wenn sie nicht erfolgreich abgeschlossen werden können. Der Clientcode kann diese
Ausnahmen abfangen, wenn ein gestarteter Task den Status awaited aufweist. Angenommen, der Toaster fängt
Feuer, während der Toast getoastet wird. Sie können dies simulieren, indem Sie die ToastBreadAsync -Methode
so ändern, dass sie dem folgenden Code entspricht:

private static async Task<Toast> ToastBreadAsync(int slices)


{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(2000);
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
await Task.Delay(1000);
Console.WriteLine("Remove toast from toaster");

return new Toast();


}

NOTE
Sie erhalten eine Warnung, wenn Sie den vorangehenden Code im Zusammenhang mit nicht erreichbarem Code
kompilieren. Dies ist beabsichtigt, da keine Vorgänge normal fortgesetzt werden können, sobald der Toaster Feuer
gefangen hat.

Führen Sie die Anwendung aus, nachdem Sie diese Änderungen vorgenommen haben. Die Ausgabe ähnelt dem
folgenden Text:

Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 slices of bacon in the pan
Cooking first side of bacon...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a slice of bacon
Flipping a slice of bacon
Flipping a slice of bacon
Cooking the second side of bacon...
Cracking 2 eggs
Cooking the eggs ...
Put bacon on plate
Put eggs on plate
Eggs are ready
Bacon is ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
at AsyncBreakfast.Program.<Main>(String[] args)

Beachten Sie, dass zwischen dem Entzünden des Toasters und dem Erkennen der Ausnahme einige Tasks
abgeschlossen werden. Wenn ein asynchron ausgeführter Task eine Ausnahme auslöst, ist dieser Task
fehlerhaft . Das Taskobjekt enthält die Ausnahme, die in der Task.Exception-Eigenschaft ausgelöst wird.
Fehlerhafte Tasks lösen eine Ausnahme aus, wenn sie erwartet werden.
Es gibt zwei wichtige Mechanismen, die Sie verstehen müssen: Wie wird eine Ausnahme in einem fehlerhaften
Task gespeichert? Wie wird eine Ausnahme entpackt und erneut ausgelöst, wenn Code einen fehlerhaften Task
erwartet?
Wenn asynchron ausgeführter Code eine Ausnahme auslöst, wird diese Ausnahme im Task gespeichert. Die
Task.Exception-Eigenschaft ist eine System.AggregateException-Klasse, da während asynchronen Vorgängen
möglicherweise mehr als eine Ausnahme ausgelöst wird. Alle ausgelösten Ausnahmen werden der
AggregateException.InnerExceptions-Sammlung hinzugefügt. Wenn diese Exception -Eigenschaft NULL ist, wird
eine neue AggregateException -Klasse erstellt, und die ausgelöste Ausnahme ist das erste Element in der
Sammlung.
Das gängigste Szenario für einen fehlerhaften Task besteht darin, dass die Exception -Eigenschaft genau eine
Ausnahme enthält. Wenn Code einen fehlerhaften Task erwartet ( awaits ), wird die erste Ausnahme in der
AggregateException.InnerExceptions-Sammlung erneut ausgelöst. Aus diesem Grund wird in der Ausgabe
dieses Beispiels anstelle einer InvalidOperationException -Klasse eine AggregateException -Klasse angezeigt. Das
Extrahieren der ersten inneren Ausnahme bewirkt, dass das Arbeiten mit asynchronen Methoden ähnlich
möglich ist wie das Arbeiten mit ihren synchronen Entsprechungen. Sie können die Exception -Eigenschaft in
Ihrem Code überprüfen, wenn das Szenario möglicherweise mehrere Ausnahmen generiert.
Kommentieren Sie diese beiden Zeilen in der ToastBreadAsync -Methode aus, bevor Sie fortfahren. Sie möchten
ein weiteres Feuer verhindern:

Console.WriteLine("Fire! Toast is ruined!");


throw new InvalidOperationException("The toaster is on fire");

Effizient auf Aufgaben warten


Die Reihe der await -Anweisungen am Ende des obigen Codes kann mithilfe der Methoden der Task -Klasse
verbessert werden. Eine dieser APIs ist WhenAll, sie gibt eine Task zurück, die abgeschlossen wird, wenn alle
Aufgaben in ihrer Argumentenliste abgeschlossen sind. Dies zeigt der folgende Code:

await Task.WhenAll(eggsTask, baconTask, toastTask);


Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");

Eine weitere Möglichkeit ist die Verwendung von WhenAny. Dies gibt eine Task<Task> zurück, die
abgeschlossen wird, wenn eines ihrer Argumente abgeschlossen wird. Sie können mit „await“ auf die
zurückgegebene Aufgabe warten – in dem Wissen, dass sie bereits abgeschlossen ist. Der folgende Code zeigt,
wie Sie WhenAny verwenden können, um mit „await“ auf den Abschluss der ersten Aufgabe zu warten und
dann deren Ergebnis zu verarbeiten. Nach dem Verarbeiten des Ergebnisses der abgeschlossenen Aufgabe
können Sie diese abgeschlossene Aufgabe aus der Liste der an WhenAny übergebenen Aufgaben entfernen.
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("Eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("Bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("Toast is ready");
}
breakfastTasks.Remove(finishedTask);
}

Nach allen diesen Änderungen sieht der endgültige Code folgendermaßen aus:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");

var eggsTask = FryEggsAsync(2);


var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);

var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };


while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
breakfastTasks.Remove(finishedTask);
}

Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}

static async Task<Toast> MakeToastWithButterAndJamAsync(int number)


{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);

return toast;
}

private static Juice PourOJ()


{
Console.WriteLine("Pouring orange juice");
return new Juice();
}

private static void ApplyJam(Toast toast) =>


Console.WriteLine("Putting jam on the toast");

private static void ApplyButter(Toast toast) =>


Console.WriteLine("Putting butter on the toast");

private static async Task<Toast> ToastBreadAsync(int slices)


{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");

return new Toast();


}

private static async Task<Bacon> FryBaconAsync(int slices)


{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");

return new Bacon();


}

private static async Task<Egg> FryEggsAsync(int howMany)


{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");

return new Egg();


}

private static Coffee PourCoffee()


{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
Die endgültige Version des asynchron zubereiteten Frühstücks hat etwa 15 Minuten in Anspruch genommen, da
einige Aufgaben gleichzeitig ausgeführt wurden und der Code mehrere Tasks gleichzeitig überwachen konnte
und nur bei Bedarf eingreifen musste.
Dieser letzte Code ist asynchron. Er spiegelt genauer wieder, wie ein Mensch ein Frühstück zubereiten würde.
Vergleichen Sie den obigen Code mit dem ersten Codebeispiel in diesem Artikel. Die Kernaktionen sind beim
Lesen des Codes noch immer deutlich erkennbar. Sie können diesen Code in derselben Weise lesen wie die
Anweisungen für die Zubereitung eines Frühstücks am Anfang dieses Artikels. Die Sprachfunktionen für async
und await stellen die Übersetzung bereit, die jede Person vornimmt, um diese schriftlichen Anweisungen zu
befolgen: Starten Sie Aufgaben, sobald Sie dies können, und blockieren Sie nicht den weiteren Fortgang, indem
Sie auf den Abschluss von Aufgaben warten.

Nächste Schritte
Weitere reale Szenarios für asynchrone Programme erkunden
Asynchrone Programmierung
04.11.2021 • 10 minutes to read

Wenn Sie E/A-gebundene Anforderungen haben (z. B. Daten aus einem Netzwerk anfordern, auf eine Datenbank
zugreifen oder aus einem Dateisystem lesen oder hineinschreiben), sollten Sie asynchrone Programmierung
verwenden. Sie könnten auch CPU-gebundenen Code haben, z.B. eine teure Berechnung, bei der es sich auch
um ein gutes Szenario zum Schreiben von asynchronem Code handelt.
C# verfügt über ein asynchrones Programmiermodell auf Sprachebene, das ein problemloses Schreiben von
asynchronem Code ermöglicht, ohne dass Sie Rückrufe koordinieren oder eine Bibliothek nutzen müssen, die
Asynchronität unterstützt. Es folgt das so genannte Aufgabenbasierte asynchrone Muster (TAP).

Grundlegende Übersicht über das asynchrone Modell


Der Kern der asynchronen Programmierung sind die Task - und Task<T> -Objekte, die asynchrone Vorgänge
modellieren. Sie werden von den async - und await -Schlüsselwörtern unterstützt. Das Modell ist in den
meisten Fällen recht einfach:
Für E/A-gebundenen Code erwarten Sie einen Vorgang, der einen Task oder Task<T> innerhalb einer
async -Methode zurückgibt.
Für CPU-gebundenen Code erwarten Sie einen Vorgang, der in einem Hintergrundthread mit der Task.Run-
Methode gestartet wird.
Das Schlüsselwort await ist sozusagen der Zauberstab. Es übergibt die Steuerung an den Aufrufer der
Methode, die await durchgeführt hat. Somit können Benutzeroberflächen letztendlich reaktionsfähig und
Dienste elastisch werden. Obwohl es Möglichkeiten gibt, sich mit anderem asynchronem Code als async und
await zu befassen, konzentriert sich dieser Artikel auf die Konstrukte auf Sprachebene.

E/A -gebundenes Beispiel: Herunterladen von Daten von einem Webdienst


Möglicherweise müssen Sie einige Daten aus einem Webdienst herunterladen, wenn auf eine Schaltfläche
geklickt wird, möchten aber den UI-Thread nicht blockieren. Das können Sie wie folgt erreichen:

private readonly HttpClient _httpClient = new HttpClient();

downloadButton.Clicked += async (o, e) =>


{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await _httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};

Der Code gibt die Absicht (Daten asynchron herunterladen) an, ohne durch Interaktion mit Task -Objekten
vereitelt zu werden.
CPU -gebundenes Beispiel: Ausführen einer Berechnung für ein Spiel
Angenommen, Sie schreiben ein mobiles Spiel, in dem ein Knopfdruck vielen Feinden auf dem Bildschirm
Schaden zufügen könnte. Das Durchführen der Schadensberechnung kann teuer sein, und sie auf dem UI-
Thread durchzuführen, hält das Spiel scheinbar an, wenn die Berechnung ausgeführt wird!
Die beste Möglichkeit ist, einen Hintergrundthread zu starten, der die Arbeit mit Task.Run erledigt, und das
Ergebnis mit await zu erwarten. So wird die Benutzeroberfläche nicht gestört, wenn die Berechnung
durchgeführt wird.

private DamageResult CalculateDamageDone()


{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
}

calculateButton.Clicked += async (o, e) =>


{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};

Dieser Code drückt die Absicht des Klickereignisses der Schaltfläche deutlich aus, und zwar in einer nicht
blockierenden Weise, und erfordert kein manuelles Verwalten eines Hintergrundthreads.
Was im Hintergrund geschieht
Es gibt viele bewegliche Bestandteile bei asynchronen Vorgängen. Wenn Sie neugierig sind, was hinter den
Kulissen von Task und Task<T> geschieht, dann lesen Sie den Artikel Async im Detail.
Auf der C#-Seite transformiert der Compiler Ihren Code in einen Zustandsautomaten, der z.B. die Rückgabe der
Ausführung protokolliert, wenn await erreicht wird, und das Fortsetzen der Ausführung, wenn ein
Hintergrundauftrag abgeschlossen ist.
Für theorieinteressierte Benutzer: Dies ist eine Implementierung des Promise-Modells der Asynchronie.

Wichtigste Bestandteile
Async-Code kann für E/A-gebundenen und CPU-gebundene Code, aber für jedes Szenario anders verwendet
werden.
Async-Code verwendet die Konstrukte Task<T> und Task , die als Modell für Arbeit im Hintergrund
verwendet werden können.
Das async -Schlüsselwort wandelt eine Methode in eine asynchrone Methode um, mit der Sie das await -
Schlüsselwort in ihrem Nachrichtentext verwenden können.
Wenn das await -Schlüsselwort angewendet wird, hält es die aufrufende Methode an, und gibt die
Steuerung wieder an den Aufrufer zurück, bis die Aufgabe abgeschlossen ist.
await kann nur innerhalb einer Async-Methode verwendet werden.

Erkennen von CPU-gebundener und E/A-gebundener Arbeit


Die ersten beiden Beispiele in diesem Leitfaden zeigen, wie Sie async und await für E/A-gebundene und CPU-
gebundene Arbeit verwenden können. Es ist wichtig, dass Sie erkennen können, wenn ein zu erledigender
Auftrag E/A-gebunden oder CPU-gebunden ist, da dies die Leistung Ihres Codes erheblich beeinflussen und
möglicherweise zum fälschlichen Gebrauch bestimmter Konstrukte führen kann.
Hier sind zwei Fragen, die Sie stellen sollten, bevor Sie Code schreiben:
1. Wird Ihr Code auf etwas „warten“, z.B. auf Daten aus einer Datenbank?
Wenn Ihre Antwort „Ja“ lautet, ist Ihre Arbeit E/A-gebunden .
2. Wird Ihr Code eine umfangreiche Berechnung durchführen?
Wenn Ihre Antwort „Ja“ lautet, ist Ihre Arbeit CPU-gebunden .
Falls Ihre Arbeit E/A-gebunden ist, verwenden Sie async und await ohne Task.Run . Sie sollten nicht die Task
Parallel Library verwenden. Der Grund dafür wird in Async ausführlich dargelegt.
Falls Ihre Arbeit CPU-gebunden ist und Sie sich für Reaktionsfähigkeit interessieren, dann verwenden Sie
async und await , aber übertragen Sie die Arbeit mit Task.Run auf einen anderen Thread. Wenn die Arbeit für
Parallelität und Konkurrenz geeignet ist, erwägen Sie auch die Verwendung der Task Parallel Library.
Darüber hinaus sollten Sie immer die Ausführung Ihres Codes messen. Sie könnten z.B. in eine Situation
geraten, in der Ihre CPU-gebundene Arbeit im Vergleich zum Aufwand der Kontextwechsel beim Multithreading
nicht kostspielig genug ist. Jede Entscheidung hat Nachteile, und Sie sollten die Nachteile je nach Ihrer Situation
auswählen.

Weitere Beispiele
Die folgenden Beispiele veranschaulichen verschiedene Möglichkeiten, wie Sie asynchronen Code in C#
schreiben können. Diese decken einige andere Szenarios ab, auf die Sie möglicherweise stoßen.
Extrahieren von Daten aus einem Netzwerk
Dieser Ausschnitt lädt den HTML-Code von der Homepage https://dotnetfoundation.org herunter und zählt, wie
oft die Zeichenfolge „.NET“ darin vorkommt. Er verwendet ASP.NET zur Definition einer Web-API-
Controllermethode, die diesen Task ausführt und die Zahl zurückgibt.

NOTE
Wenn Sie eine HTML-Analyse im Produktionscode durchführen möchten, nutzen Sie dafür nicht die regulären Ausdrücke.
Verwenden Sie stattdessen eine Analysebibliothek.

private readonly HttpClient _httpClient = new HttpClient();

[HttpGet, Route("DotNetCount")]
public async Task<int> GetDotNetCount()
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org");

return Regex.Matches(html, @"\.NET").Count;


}

Hier sehen Sie das gleiche Szenario, das für eine universelle Windows-App geschrieben wurde, die die gleiche
Aufgabe ausführt, wenn auf eine Schaltfläche geklickt wird:
private readonly HttpClient _httpClient = new HttpClient();

private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)


{
// Capture the task handle here so we can await the background task later.
var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");

// Any other work on the UI thread can be done here, such as enabling a Progress Bar.
// This is important to do here, before the "await" call, so that the user
// sees the progress bar before execution of this method is yielded.
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible;

// The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.


// This is what allows the app to be responsive and not block the UI thread.
var html = await getDotNetFoundationHtmlTask;
int count = Regex.Matches(html, @"\.NET").Count;

DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visibility = Visibility.Collapsed;
}

Warten auf das Abschließen mehrerer Tasks


Sie könnten sich in einer Situation befinden, in der Sie mehrere Daten gleichzeitig abrufen müssen. Die Task -
API enthält die Methoden Task.WhenAll und Task.WhenAny, mit denen Sie asynchronen Code schreiben können,
der einen nicht blockierenden Wartevorgang für mehrere Hintergrundaufträge durchführt.
Dieses Beispiel zeigt, wie Sie einen User -Datensatz für einen Satz von userId nehmen könnten.

public async Task<User> GetUserAsync(int userId)


{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
}

public static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)


{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}

return await Task.WhenAll(getUserTasks);


}

Hier sehen Sie eine weitere Möglichkeit, dies mithilfe von LINQ präziser zu schreiben:
public async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
}

public static async Task<User[]> GetUsersAsync(IEnumerable<int> userIds)


{
var getUserTasks = userIds.Select(id => GetUserAsync(id));
return await Task.WhenAll(getUserTasks);
}

Auch wenn es weniger Code ist, sollten Sie trotzdem vorsichtig sein, wenn Sie LINQ mit asynchronem Code
mischen. Da LINQ verzögerte (lazy) Ausführung verwendet, werden asynchrone Aufrufe nicht sofort ausgeführt,
so wie in einer foreach -Schleife, es sei denn, Sie erzwingen, dass die generierte Sequenz einen Aufruf von
.ToList() oder .ToArray() durchläuft.

Wichtige Informationen und Hinweise


Bei asynchroner Programmierung sind einige Details zu berücksichtigen, durch die ein unerwartetes Verhalten
verhindert werden kann.
async -Methoden benötigen ein Schlüsselwor t await in Ihrem Textkörper, andernfalls erfolgt
niemals eine Rückgabe!
Berücksichtigen Sie dies. Wenn await nicht im Textkörper einer async -Methode verwendet wird,
generiert der C#-Compiler eine Warnung, aber der Code wird kompiliert und ausgeführt, als ob es sich
um eine normale Methode handeln würde. Dies ist unglaublich ineffizient, da der Zustandsautomat, der
vom C#-Compiler für die asynchrone Methode generiert wird, nichts erreicht.
Fügen Sie „Async“ als Suffix an die Namen aller async-Methoden an, die Sie schreiben.
Mit dieser in .NET verwendeten Konvention kann leichter zwischen synchronen und asynchronen
Methoden unterschieden werden. Bestimmte, von Ihrem Code nicht explizit aufgerufene Methoden (z. B.
Ereignishandler oder Webcontrollermethoden) werden nicht unbedingt angewendet. Da sie von Ihrem
Code nicht explizit aufgerufen werden, ist es nicht wichtig, ihre Namen explizit anzugeben.
async void sollte nur für Ereignishandler ver wendet werden.
async void ist die einzige Möglichkeit, mit der asynchrone Ereignishandler ausgeführt werden können,
da Ereignisse keine Rückgabetypen haben (und somit Task und Task<T> nicht verwenden können). Jede
andere Verwendung der async void folgt nicht dem TAP-Modell und kann schwierig zu verwenden sein,
wie beispielsweise:
Ausnahmen in einer async void -Methode können nicht außerhalb der Methode abgefangen werden.
async void -Methoden sind schwierig zu testen.
async void -Methoden können schlechte Nebeneffekte verursachen, wenn der Aufrufende nicht
erwartet, dass sie asynchron sind.
Gehen Sie bei der Ver wendung von asynchronen Lambdaausdrücken in LINQ-Ausdrücken
sorgfältig vor
Lambdaausdrücke in LINQ verwenden verzögerte Ausführung. Das bedeutet, dass Code zu einem
Zeitpunkt ausgeführt werden kann, zu dem Sie es nicht erwarten. Die Einführung von blockierenden
Aufgaben kann schnell zu einem Deadlock führen, wenn diese nicht ordnungsgemäß geschrieben
werden. Darüber hinaus kann die Schachtelung von asynchronem Code die Ausführung des Codes
erschweren. Async und LINQ sind leistungsstark, sollten zusammen aber so sorgfältig und deutlich wie
möglich verwendet werden.
Schreiben Sie Code, der Aufgaben in einer nicht blockierenden Ar t und Weise er war tet
Wenn Sie den aktuellen Thread blockieren, um auf den Abschluss eines Task zu warten, kann dies zu
Deadlocks und blockierten Kontextthreads führen, und eine komplexere Fehlerbehandlung kann
erforderlich sein. Die folgende Tabelle enthält Anleitungen zum nicht-blockierenden Warten auf Tasks:

VERW EN DEN SIE. . . A N STAT T. . . W EN N SIE DIES T UN M Ö C H T EN . . .

await Task.Wait oder Task.Result Abrufen des Ergebnisses einer


Hintergrundaufgabe

await Task.WhenAny Task.WaitAny Warten auf das Abschließen einer


Aufgabe

await Task.WhenAll Task.WaitAll Warten auf das Abschließen aller


Aufgaben

await Task.Delay Thread.Sleep Warten auf einen Zeitraum

Ver wenden Sie ggf. ValueTask .


Das Zurückgeben eines Task -Objekts von asynchronen Methoden kann Leistungsengpässe in
bestimmten Pfaden verursachen. Task ist ein Verweistyp, seine Verwendung bedeutet also das
Zuordnen eines Objekts. In Fällen, in denen eine mit dem async -Modifizierer deklarierte Methode ein
zwischengespeichertes Ergebnis zurückgibt oder synchron abschließt, können die zusätzlichen
Zuordnungen viel Zeit bei der Ausführung kritischer Codeabschnitte kosten. Es kann kostspielig werden,
wenn diese Zuordnungen in engen Schleifen auftreten. Weitere Informationen finden Sie unter
Generalisierte asynchrone Rückgabetypen.
Ver wenden Sie ggf. ConfigureAwait(false) .
Eine häufige Frage ist: „Wann sollte ich die Task.ConfigureAwait(Boolean)-Methode verwenden?“. Die
Methode ermöglicht einer Task -Instanz, ihren Awaiter zu konfigurieren. Dies ist ein wichtiger Aspekt,
und die falsche Festlegung kann möglicherweise Auswirkungen auf die Leistung und sogar Deadlocks
zur Folge haben. Weitere Informationen zu ConfigureAwait finden Sie im ConfigureAwait-FAQ.
Schreiben eines weniger statusbehafteten Codes
Machen Sie sich nicht abhängig vom Zustand globaler Objekte oder der Ausführung bestimmter
Methoden. Seien Sie stattdessen nur abhängig von Rückgabewerten der Methoden. Warum?
Code wird leichter verständlich sein.
Code wird leichter zu testen sein.
Das Kombinieren von asynchronem und synchronem Code ist wesentlich einfacher.
Racebedingungen können in der Regel ganz vermieden werden.
Je nach Rückgabewerten ist das Koordinieren von asynchronem Code einfach.
(Bonus) funktioniert hervorragend mit Abhängigkeitsinjektion.
Ein empfohlenes Ziel ist das vollständige oder nahezu vollständige Erreichen referenzieller Transparenz in Ihrem
Code. Dies führt zu einer vorhersagbaren, überprüfbaren und verwaltbaren Codebasis.

Weitere Ressourcen
Async ausführlich enthält weitere Informationen zur Funktionsweise von Aufgaben.
Das taskbasierte asynchrone Programmiermodell (C#)
Six Essential Tips for Async (Sechs wichtige Tipps für Async) von Lucian Wischik ist eine hervorragende
Ressource für asynchrone Programmierung.
Aufgabenbasiertes asynchrones Programmiermodell
04.11.2021 • 13 minutes to read

Sie können Leistungsengpässe vermeiden und die Reaktionsfähigkeit der Anwendung insgesamt verbessern,
indem Sie asynchrone Programmierung verwenden. Allerdings können herkömmliche Verfahren zum Schreiben
von asynchronen Anwendungen kompliziert sein, weshalb es schwierig ist, diese Anwendungen zu schreiben, zu
debuggen und zu verwalten.
C# 5 führte den vereinfachten Ansatz der asynchronen Programmierung ein, der die asynchrone Unterstützung
in .NET Framework 4.5 und höher, .NET Core sowie in der Windows-Runtime nutzt. Der Compiler übernimmt die
schwierige Arbeit, die zuvor vom Entwickler ausgeführt wurde, und die Anwendung behält eine logische
Struktur bei, die synchronem Code ähnelt. So erhalten Sie alle Vorteile der asynchronen Programmierung mit
einem Bruchteil des Aufwands.
Dieses Thema enthält eine Übersicht über die Verwendungsmöglichkeiten der asynchronen Programmierung
sowie Links zu Supportthemen, die weitere Informationen und Beispiele enthalten.

Async verbessert die Reaktionsfähigkeit


Asynchronie ist wesentlich für potenziell blockierende Aktivitäten, z.B. Webzugriff. Der Zugriff auf eine
Webressource ist manchmal langsam oder verzögert. Wenn eine solche Aktivität innerhalb eines synchronen
Prozesses blockiert wird, muss die gesamte Anwendung warten. In einem asynchronen Prozess kann die
Anwendung mit anderer Arbeit fortfahren, die nicht von der Webressource abhängig ist, bis die potenziell
blockierende Aufgabe beendet ist.
In der folgenden Tabelle werden typische Bereichen angezeigt, in denen die asynchrone Programmierung die
Reaktionsfähigkeit verbessert. Die aufgelisteten APIs von .NET und der Windows-Runtime enthalten Methoden,
die die asynchrone Programmierung unterstützen.

. N ET - T Y P EN M IT A SY N C H RO N EN W IN DO W S- RUN T IM E- T Y P EN M IT
A N W EN DUN GSB EREIC H M ET H O DEN A SY N C H RO N EN M ET H O DEN

Webzugriff HttpClient Windows.Web.Http.HttpClient


SyndicationClient

Arbeiten mit Dateien JsonSerializer StorageFile


StreamReader
StreamWriter
XmlReader
XmlWriter

Arbeiten mit Bildern MediaCapture


BitmapEncoder
BitmapDecoder

WCF-Programmierung Synchrone und asynchrone Vorgänge

Asynchronität erweist sich als besonders nützlich bei Prozessen, die durch den UI-Thread ausgeführt werden, da
sämtliche auf die Benutzeroberfläche bezogenen Aktivitäten sich gemeinhin diesen Thread teilen. Ist ein Prozess
in einer synchronen Anwendung blockiert, werden alle blockiert. Die Anwendung reagiert nicht mehr, und Sie
denken möglicherweise, es sei ein Fehler aufgetreten, wenn die Anwendung tatsächlich nur wartet.
Wenn Sie asynchrone Methoden verwenden, reagiert die Anwendung weiterhin auf die Benutzeroberfläche. Sie
können beispielsweise die Fenstergröße ändern, das Fenster minimieren oder die Anwendung schließen, wenn
Sie nicht warten möchten, bis sie fertig ist.
Durch den auf Asynchronie basierenden Ansatz wird eine Entsprechung für die automatische Übertragung an
die Liste der Optionen hinzugefügt, die beim Entwerfen von asynchronen Vorgängen zur Auswahl stehen. Sie
erhalten also alle Vorteile der herkömmlichen asynchronen Programmierung, der Aufwand des Entwicklers ist
jedoch wesentlich geringer.

Das Schreiben asynchroner Methoden ist einfach


Die Schlüsselwörter async und await in C# sind das Kernstück der asynchronen Programmierung. Durch
Verwendung dieser beiden Schlüsselwörter können Sie mithilfe von .NET Framework-, .NET Core- oder
Windows-Runtime-Ressourcen asynchrone Methoden fast genauso einfach erstellen wie synchrone Methoden.
Asynchrone Methoden, die Sie unter Verwendung des Schlüsselworts async definieren, werden als async-
Methoden bezeichnet.
Das folgende Beispiel zeigt eine Async-Methode. Fast der gesamte Code sollte Ihnen bekannt vorkommen.
Ein vollständiges WPF-Beispiel (Windows Presentation Foundation) können Sie unter Asynchrones
Programmieren mit async und await in C# herunterladen.

public async Task<int> GetUrlContentLengthAsync()


{
var client = new HttpClient();

Task<string> getStringTask =
client.GetStringAsync("https://docs.microsoft.com/dotnet");

DoIndependentWork();

string contents = await getStringTask;

return contents.Length;
}

void DoIndependentWork()
{
Console.WriteLine("Working...");
}

Sie können dem vorherigen Beispiel mehrere Methoden entnehmen. Starten Sie mit der Methodensignatur. Sie
enthält den async -Modifizierer. Der Rückgabetyp lautet Task<int> (weitere Optionen finden Sie im Abschnitt
„Rückgabetypen“). Der Methodenname endet auf Async . Im Text der Methode gibt GetStringAsync einen
Task<string> -Wert zurück. Das bedeutet, dass Sie string ( contents ) erhalten, wenn Sie auf die Aufgabe
warten ( await ). Bevor Sie auf die Aufgabe warten, können Sie Arbeiten ausführen, die nicht auf einem string -
Wert von GetStringAsync basieren.
Achten Sie besonders auf den await -Operator. GetUrlContentLengthAsync wird angehalten:
GetUrlContentLengthAsync kann erst fortgesetzt werden, wenn getStringTask abgeschlossen ist.
In der Zwischenzeit wird die Steuerung an den Aufrufer von GetUrlContentLengthAsync zurückgegeben.
Die Steuerung wird hier wieder aufgenommen, sobald getStringTask abgeschlossen ist.
Der await -Operator ruft nun das string -Ergebnis von getStringTask ab.

Die return-Anweisung gibt ein ganzzahliges Ergebnis an. Alle Methoden, die auf GetUrlContentLengthAsync
warten, rufen den Längenwert ab.
Wenn für GetUrlContentLengthAsync zwischen dem Aufrufen von GetStringAsync und dem Warten auf die
Beendigung keine Arbeit ansteht, können Sie den Code durch Aufrufen und Warten in der folgenden Anweisung
vereinfachen.

string contents = await client.GetStringAsync("https://docs.microsoft.com/dotnet");

Die folgenden Merkmale fassen zusammen, wodurch das vorherige Beispiel sich als asynchrone Methode
auszeichnet:
Die Methodensignatur enthält einen async -Modifizierer.
Der Name einer Async-Methode endet mit dem Suffix "Async".
Der Rückgabetyp ist einer der folgenden Typen:
Task<TResult>, wenn die Methode über eine return-Anweisung verfügt, in der der Operand dem Typ
TResult angehört.
Task, wenn die Methode keine return-Anweisung hat oder eine return-Anweisung ohne Operanden
hat.
void , wenn Sie einen asynchronen Ereignishandler schreiben.
Jeder andere Typ, der über eine GetAwaiter -Methode verfügt (ab C# 7.0).
Weitere Informationen finden Sie im Abschnitt Rückgabetypen und -parameter.
Die Methode umfasst normalerweise mindestens einen await -Ausdruck, der einen Punkt kennzeichnet,
an dem die Methode erst nach Abschluss des erwarteten asynchronen Vorgangs fortgesetzt werden
kann. In der Zwischenzeit wird die Methode angehalten, und die Steuerung kehrt zum Aufrufer der
Methode zurück. Im nächsten Abschnitt dieses Themas wird veranschaulicht, was am
Unterbrechungspunkt geschieht.
In Asynch-Methoden verwenden Sie die bereitgestellten Schlüsselwörter und Typen, um die gewünschten
Aktionen anzugeben. Der Compiler übernimmt den Rest, er verfolgt auch, was geschehen muss, wenn die
Steuerung zu einem await-Punkt in einer angehaltenen Methode zurückkehrt. Einige Routinenprozesse,
beispielsweise Schleifen und Ausnahmebehandlung, sind im herkömmlichen asynchronen Code
möglicherweise schwierig zu handhaben. In einer Async-Methode schreiben Sie diese Elemente fast so wie in
einer synchronen Lösung, und das Problem ist gelöst.
Weitere Informationen zur Asynchronität in vorherigen Versionen von .NET Framework finden Sie unter TPL und
herkömmliche asynchrone .NET Framework-Programmierung.

Was geschieht in einer asynchronen Methode?


Bei der asynchronen Programmierung ist es sehr wichtig zu verstehen, wie die Ablaufsteuerung von Methode zu
Methode springt. In dem folgenden Diagramm werden Sie durch den Prozess geführt:
Die Zahlen in diesem Diagramm entsprechen den folgenden Schritten, die initiiert werden, wenn eine
aufrufende Methode die asynchrone Methode aufruft.
1. Eine aufrufende Methode ruft die asynchrone Methode GetUrlContentLengthAsync auf und wartet auf sie.
2. GetUrlContentLengthAsync erstellt eine HttpClient-Instanz und ruft die asynchrone Methode
GetStringAsync auf, um den Inhalt einer Website als Zeichenfolge herunterzuladen.
3. In GetStringAsync geschieht etwas, durch das die Ausführung angehalten wird. Möglicherweise muss
gewartet werden, bis eine Website heruntergeladen oder eine andere blockierende Aktivität ausgeführt
wurde. Um blockierende Ressourcen zu vermeiden, übergibt die Methode GetStringAsync die Steuerung
an ihren Aufrufer GetUrlContentLengthAsync .
GetStringAsync gibt Task<TResult> zurück, wobei TResult eine Zeichenfolge ist.
GetUrlContentLengthAsync weist der getStringTask -Variablen die Aufgabe zu. Die Aufgabe stellt den
laufenden Prozess für den Aufruf von GetStringAsync dar, mit der Festlegung, dass bei Abschluss der
Arbeit ein tatsächlicher Zeichenfolgenwert erzeugt wurde.
4. Da getStringTask noch nicht abgewartet wurde, kann GetUrlContentLengthAsync mit anderer Arbeit
fortfahren, die nicht vom Endergebnis von GetStringAsync abhängt. Diese Aufgaben werden durch einen
Aufruf der synchronen Methode DoIndependentWork dargestellt.
5. DoIndependentWork ist eine synchrone Methode, die ihre Arbeit ausführt und zu ihrem Aufrufer
zurückkehrt.
6. Für GetUrlContentLengthAsync steht keine Arbeit mehr an, die die Methode ohne ein Ergebnis von
getStringTask ausführen kann. GetUrlContentLengthAsync möchte als Nächstes die Länge der
heruntergeladenen Zeichenfolge berechnen und zurückgeben, aber die Methode kann diesen Wert erst
berechnen, wenn sie über die Zeichenfolge verfügt.
Daher verwendet GetUrlContentLengthAsync einen await-Operator, um die Ausführung anzuhalten und
die Steuerung an die Methode zu übergeben, von der GetUrlContentLengthAsync aufgerufen wurde.
GetUrlContentLengthAsync gibt ein Task<int> -Element an den Aufrufer zurück. Die Aufgabe stellt eine
Zusicherung dar, ein Ganzzahlergebnis zu erzeugen, das die Länge der heruntergeladenen Zeichenfolge
ist.

NOTE
Wenn GetStringAsync (und daher getStringTask ) abgeschlossen ist, bevor GetUrlContentLengthAsync
darauf wartet, verbleibt die Steuerung bei GetUrlContentLengthAsync . Der Aufwand des Anhaltens und der
Rückkehr zu GetUrlContentLengthAsync wäre überflüssig, wenn der aufgerufene asynchrone Prozess
getStringTask bereits abgeschlossen ist und GetUrlContentLengthAsync nicht auf das Endergebnis warten
muss.

In der aufrufenden Methode wird das Verarbeitungsmuster fortgesetzt. Der Aufrufer führt
möglicherweise andere Arbeit aus, die nicht von dem Ergebnis von GetUrlContentLengthAsync abhängt,
bevor er auf dieses Ergebnis wartet, oder der Aufrufer wartet sofort auf das Ergebnis. Die aufrufende
Methode wartet auf GetUrlContentLengthAsync , und GetUrlContentLengthAsync wartet auf GetStringAsync
.
7. GetStringAsync wird abgeschlossen und erstellt ein Zeichenfolgenergebnis. Das Zeichenfolgenergebnis
wird von dem Aufruf GetStringAsync nicht auf die Art zurückgegeben, die Sie möglicherweise erwarten.
(Beachten Sie, dass die Methode bereits in Schritt 3 eine Aufgabe zurückgegeben hat.) Stattdessen wird
das Zeichenfolgenergebnis in dem Task gespeichert, der den Abschluss der Methode darstellt:
getStringTask . Der await-Operator ruft das Ergebnis von getStringTask ab. Die Zuweisungsanweisung
weist contents das abgerufene Ergebnis zu.
8. Wenn GetUrlContentLengthAsync über das Zeichenfolgenergebnis verfügt, kann die Methode die Länge
der Zeichenfolge berechnen. Dann ist die Arbeit von GetUrlContentLengthAsync auch abgeschlossen, und
der wartende Ereignishandler kann fortfahren. Im vollständigen Beispiel am Ende des Themas können Sie
überprüfen, ob der Ereignishandler den Wert des Längenergebnisses abruft und druckt. Wenn die
asynchrone Programmierung für Sie neu ist, nehmen Sie sich einen Moment Zeit, um den Unterschied
zwischen synchronem und asynchronem Verhalten zu untersuchen. Eine synchrone Methode kehrt zum
Aufrufer zurück, wenn ihre Arbeit abgeschlossen ist (Schritt 5). Eine asynchrone Methode hingegen gibt
einen Aufgabenwert zurück, wenn die Verarbeitung angehalten wird (Schritt 3 und 6). Wenn die
asynchrone Methode schließlich ihre Arbeit abgeschlossen hat, wird die Aufgabe als abgeschlossen
markiert und das Ergebnis ggf. in der Aufgabe gespeichert.

API-Async-Methoden
Sie fragen sich möglicherweise, wo Methoden wie GetStringAsync zu finden sind, die die asynchrone
Programmierung unterstützen. .NET Framework 4.5 oder höher und .NET Core enthalten viele Member, die mit
async und await funktionieren. Sie können sie am Suffix „Async“ erkennen, das an den Membernamen
angefügt wird, sowie am Rückgabetyp Task oder Task<TResult>. Beispielsweise enthält die System.IO.Stream -
Klasse Methoden wie CopyToAsync, ReadAsync und WriteAsync sowie die synchronen Methoden CopyTo, Read
und Write.
Die Windows-Runtime enthält außerdem viele Methoden, die Sie mit async und await in Windows-Apps
verwenden können. Weitere Informationen finden Sie unter Threading and async programming (Threading und
asynchrone Programmierung) für die UWP-Entwicklung, Asynchronous programming (Windows Store apps)
(Asynchrone Programmierung für Windows Store-Apps) und Quickstart: Calling asynchronous APIs in C# or
Visual Basic (Schnellstart: Aufrufen von asynchronen APIs in C# oder Visual Basic), wenn Sie frühere Versionen
der Windows-Runtime verwenden.

Threads
Async-Methoden sollen nicht blockierende Vorgänge sein. Ein await -Ausdruck in einer asynchronen Methode
blockiert den aktuellen Thread nicht, während der Task, auf den gewartet wurde, ausgeführt wird. Stattdessen
registriert der Ausdruck den Rest der Methode als Fortsetzung und gibt die Steuerung an den Aufrufer der
Async-Methode zurück.
Durch die Schlüsselwörter async und await werden keine zusätzlichen Threads erstellt. Async-Methoden
erfordern kein Multithreading, da eine Async-Methode nicht in einem eigenen Thread ausgeführt wird. Die
Methode wird im aktuellen Synchronisierungskontext ausgeführt und verwendet Zeit im Thread nur, wenn sie
aktiv ist. Sie können Task.Run zur Verschiebung CPU-gebundener Arbeit in einen Hintergrundthread verwenden,
aber ein Hintergrundthread nützt nichts bei einem Prozess, der wartet, dass Ergebnisse zur Verfügung gestellt
werden.
Der auf Asynchronie basierende Ansatz der asynchronen Programmierung ist vorhandenen Ansätzen in nahezu
jedem Fall vorzuziehen. Insbesondere eignet sich dieser Ansatz besser für E/A-gebundene Vorgänge als die
BackgroundWorker-Klasse, da der Code einfacher und kein Schutz vor Racebedingungen erforderlich ist. In
Kombination mit der Methode Task.Run eignet sich die asynchrone Programmierung besser als
BackgroundWorker für CPU-gebundene Vorgänge, da die asynchrone Programmierung Koordinationsdetails
der Ausführung des Codes von der Arbeit trennt, die Task.Run an den Threadpool überträgt.

„async“ und „await“


Wenn Sie angeben, dass eine Methode eine asynchrone Methode ist, indem Sie den async-Modifizierer
verwenden, aktivieren Sie die folgenden zwei Funktionen.
Die markierte async-Methode kann await verwenden, um Unterbrechungspunkte festzulegen. Der await
-Operator informiert den Compiler, dass die Async-Methode erst über diesen Punkt hinaus fortgesetzt
werden kann, wenn der abgewartete asynchrone Prozess abgeschlossen ist. In der Zwischenzeit kehrt die
Steuerung zum Aufrufer der Async-Methode zurück.
Die Unterbrechung einer async-Methode bei einem await -Ausdruck stellt keine Beendigung der
Methode dar, und finally -Blöcke werden nicht ausgeführt.
Auf die markierte Async-Methode können auch Methoden warten, die sie aufrufen.
Eine asynchrone Methode enthält in der Regel mindestens ein Vorkommen eines await -Operators, die
Abwesenheit von await -Ausdrücken verursacht jedoch keinen Compilerfehler. Wenn eine asynchrone Methode
keinen await -Operator verwendet, um einen Unterbrechungspunkt zu markieren, wird die Methode
ungeachtet des async -Modifizierers wie eine synchrone Methode ausgeführt. Der Compiler gibt eine Warnung
für solche Methoden aus.
async und await sind kontextbezogene Schlüsselwörter. Weitere Informationen und Beispiele finden Sie unter
den folgenden Themen:
async
await

Rückgabetypen und Parameter


Eine async-Methode gibt normalerweise eine Task oder Task<TResult> zurück. Innerhalb einer async-Methode
wird ein await -Operator auf einen Task angewendet, der in einem Aufruf einer anderen async-Methode
zurückgegeben wurde.
Geben Sie Task<TResult> als Rückgabetyp an, wenn die Methode eine return -Anweisung enthält, die einen
Operanden vom Typ TResult angibt.
Sie verwenden Task als Rückgabetyp, wenn die Methode keine return-Anweisung hat oder über eine return-
Anweisung verfügt, die keinen Operanden zurückgibt.
Ab C# 7.0 können Sie auch andere Rückgabetypen angeben, vorausgesetzt, der Typ enthält eine GetAwaiter -
Methode. Ein Beispiel eines solchen Typs ist ValueTask<TResult>. Er ist im NuGet-Paket
System.Threading.Tasks.Extension verfügbar.
Im folgenden Beispiel wird gezeigt, wie Sie eine Methode deklarieren und aufrufen, die Task<TResult> oder Task
zurückgibt:

async Task<int> GetTaskOfTResultAsync()


{
int hours = 0;
await Task.Delay(0);

return hours;
}

Task<int> returnedTaskTResult = GetTaskOfTResultAsync();


int intResult = await returnedTaskTResult;
// Single line
// int intResult = await GetTaskOfTResultAsync();

async Task GetTaskAsync()


{
await Task.Delay(0);
// No return statement needed
}

Task returnedTask = GetTaskAsync();


await returnedTask;
// Single line
await GetTaskAsync();

Jede zurückgegebene Aufgabe stellt derzeit ausgeführte Arbeit dar. Eine Aufgabe kapselt Informationen über
den Zustand des asynchronen Prozesses und schließlich entweder das Endergebnis aus dem Prozess oder die
Ausnahme, die durch einen Prozessfehler verursacht wird.
Eine asynchrone Methode kann auch den Rückgabetyp void aufweisen. Dieser Rückgabetyp wird hauptsächlich
zum Definieren von Ereignishandlern verwendet, wobei ein void -Rückgabetyp erforderlich ist. Asynchrone
Ereignishandler dienen häufig als Ausgangspunkt für asynchrone Programme.
Auf eine asynchrone Methode, die einen void -Rückgabetyp aufweist, kann nicht gewartet werden, und der
Aufrufer einer Methode, die „void“ zurückgibt, kann keine Ausnahmen abfangen, die von der Methode ausgelöst
werden.
Eine asynchrone Methode kann keine in-, ref- oder out-Parameter deklarieren. Die Methode kann jedoch
Methoden aufrufen, die solche Parameter aufweisen. Genauso kann eine async-Methode keinen Wert nach
Verweis zurückgeben, obwohl sie Methoden mit ref-Rückgabewerten aufrufen kann.
Weitere Informationen und Beispiele finden Sie unter Asynchrone Rückgabetypen (C#). Weitere Informationen
zum Abfangen von Ausnahmen in async-Methoden finden Sie unter try-catch.
Asynchrone APIs in der Windows-Runtime-Programmierung weisen einen der folgenden Rückgabetypen auf,
die Tasks ähnlich sind:
IAsyncOperation<TResult>, was Task<TResult> entspricht.
IAsyncAction, was Task entspricht.
IAsyncActionWithProgress<TProgress>
IAsyncOperationWithProgress<TResult,TProgress>
Benennungskonvention
Üblicherweise sollten die Namen von Methoden, die in der Regel awaitable-Typen zurückgeben (z. B. Task ,
Task<T> , ValueTask , ValueTask<T> ), auf „Async“ enden. Methoden, die einen asynchronen Vorgang starten,
aber keinen awaitable-Typ zurückgeben, sollten nicht auf „Async“ enden, sondern mit „Begin“, „Start“ oder einem
ähnlichen Verb beginnen, sodass eindeutig ist, dass diese Methode nicht das Ergebnis des Vorgangs zurückgibt
bzw. auslöst.
Sie können die Konvention ignorieren, wenn ein Ereignis, eine Basisklasse oder ein Schnittstellenvertrag einen
anderen Namen vorsieht. Beispielsweise sollten Sie allgemeine Ereignishandler wie OnButtonClick nicht
umbenennen.

Verwandte Themen und Beispiele (Visual Studio)


T IT EL B ESC H REIB UN G B EISP IEL

Vorgehensweise: Paralleles Erstellen Veranschaulicht, wie mehrere Async Sample: Make Multiple Web
mehrerer Webanforderungen mit Aufgaben gleichzeitig gestartet Requests in Parallel (Asynchrones
async und await (C#) werden. Beispiel: Paralleles Erstellen mehrerer
Webanforderungen)

Asynchrone Rückgabetypen (C#) Es werden die Typen veranschaulicht,


die asynchrone Methoden
zurückgeben können, und es wird
erklärt, wann die einzelnen Typen
geeignet sind.

Abbrechen von Aufgaben mit einem Zeigt, wie die folgenden Funktionen
Abbruchtoken als der asynchronen Lösung hinzugefügt
Signalisierungsmechanismus werden:

- Abbrechen einer Aufgabenliste (C#)


- Abbrechen von Aufgaben nach
einem bestimmten Zeitraum (C#)
- Verarbeiten asynchroner Aufgaben
nach Abschluss (C#)

Verwenden von Async für den Listet die Vorteile der Verwendung von
Dateizugriff (C#) "async" und "await" für den Zugriff auf
Dateien auf und veranschaulicht sie.

Aufgabenbasiertes asynchrones In diesem Artikel wird ein asynchrones


Muster Muster beschrieben. Dieses Muster
basiert auf den Typen Task und
Task<TResult>.

Videos zur asynchronen Stellt Links zu einer Vielzahl von Videos


Programmierung auf Channel 9 über die asynchrone Programmierung
bereit.

Siehe auch
async
await
Asynchrone Programmierung
Async (Übersicht)
Asynchrone Rückgabetypen (C#)
04.11.2021 • 9 minutes to read

Asynchrone Methoden können folgende Rückgabetypen haben:


Task für eine asynchrone Methode, die einen Vorgang ausführt, aber keinen Wert zurückgibt.
Task<TResult> für eine asynchrone Methode, die einen Wert zurückgibt.
void für einen Ereignishandler.
Ab C# 7.0: jeder Typ, der über eine zugängliche GetAwaiter -Methode verfügt. Das von der GetAwaiter -
Methode zurückgegebene Objekt muss die System.Runtime.CompilerServices.ICriticalNotifyCompletion-
Schnittstelle implementieren.
Ab C# 8.0 IAsyncEnumerable<T> für eine asynchrone Methode, die einen asynchronen Datenstrom
zurückgibt.
Weitere Informationen über async-Methoden finden Sie unter Asynchrone Programmierung mit async und
await (C#).
Es gibt auch einige weitere Typen, die spezifisch für Windows-Workloads gelten:
DispatcherOperation für asynchrone Vorgänge, die auf Windows beschränkt sind
IAsyncAction für asynchrone Aktionen in UWP, die keinen Wert zurückgeben
IAsyncActionWithProgress<TProgress> für asynchrone Aktionen in UWP, die den Fortschritt melden, aber
keinen Wert zurückgeben
IAsyncOperation<TResult> für asynchrone Vorgänge in UWP, die einen Wert zurückgeben
IAsyncOperationWithProgress<TResult,TProgress> für asynchrone Vorgänge in UWP, die den Fortschritt
melden und einen Wert zurückgeben

Task-Rückgabetyp
Asynchrone Methoden, die keine return -Anweisung enthalten oder eine return -Anweisung enthalten, die
keinen Operanden zurückgibt, haben normalerweise einen Rückgabetyp von Task. Solche Methoden geben
void zurück, wenn sie synchron ausgeführt werden. Wenn Sie einen Task-Rückgabetyp für eine asynchrone
Methode verwenden, kann ein aufrufende Methode einen await -Operator verwenden, um den Abschluss des
Aufrufers anzuhalten, bis die aufgerufene asynchrone Methode beendet ist.
Im folgenden Beispiel enthält die WaitAndApologizeAsync -Methode keine return -Anweisung, weshalb die
Methode ein Task-Objekt zurückgibt. Durch Rückgabe von Task wird ermöglicht, dass WaitAndApologizeAsync
erwartet wird. Der Typ Task enthält keine Result -Eigenschaft, da er nicht über einen Rückgabewert verfügt.
public static async Task DisplayCurrentInfoAsync()
{
await WaitAndApologizeAsync();

Console.WriteLine($"Today is {DateTime.Now:D}");
Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
Console.WriteLine("The current temperature is 76 degrees.");
}

static async Task WaitAndApologizeAsync()


{
await Task.Delay(2000);

Console.WriteLine("Sorry for the delay...\n");


}
// Example output:
// Sorry for the delay...
//
// Today is Monday, August 17, 2020
// The current time is 12:59:24.2183304
// The current temperature is 76 degrees.

WaitAndApologizeAsync wird erwartet, indem eine „await“-Anweisung anstelle eines „await“-Ausdrucks


verwendet wird, ähnlich der Aufrufanweisung einer Methode, die „void“ zurückgibt. Die Anwendung eines
Erwartungsoperators erzeugt in diesem Fall keinen Wert. Wenn der rechte Operand von await Task<TResult>
ist, generiert der Ausdruck await ein Ergebnis von T . Wenn der rechte Operand von await Task ist, sind
await und sein Operand eine Anweisung.

Sie können den WaitAndApologizeAsync -Aufruf eines await-Operators von der Anwendung trennen (dies wird im
folgenden Code veranschaulicht). Beachten Sie jedoch, dass Task über keine Result -Eigenschaft verfügt und
dass kein Wert erzeugt wird, wenn ein Erwartungsoperator auf Task angewendet wird.
Der folgende Code trennt Aufrufe der Methode WaitAndApologizeAsync vom Erwarten der Aufgabe, die die
Methode zurückgibt.

Task waitAndApologizeTask = WaitAndApologizeAsync();

string output =
$"Today is {DateTime.Now:D}\n" +
$"The current time is {DateTime.Now.TimeOfDay:t}\n" +
"The current temperature is 76 degrees.\n";

await waitAndApologizeTask;
Console.WriteLine(output);

Task-Rückgabetyp <TResult>
Der Rückgabetyp Task<TResult> wird für eine asynchrone Methode verwendet, die eine return-Anweisung
enthält, in der der Operand TResult lautet.
Im folgenden Beispiel enthält die GetLeisureHoursAsync -Methode eine return -Anweisung, die eine ganze Zahl
zurückgibt. Die Methodendeklaration muss den Rückgabetyp Task<int> angeben. Die asynchrone Methode
FromResult ist ein Platzhalter für einen Vorgang, der einen DayOfWeek-Wert zurückgibt.
public static async Task ShowTodaysInfoAsync()
{
string message =
$"Today is {DateTime.Today:D}\n" +
"Today's hours of leisure: " +
$"{await GetLeisureHoursAsync()}";

Console.WriteLine(message);
}

static async Task<int> GetLeisureHoursAsync()


{
DayOfWeek today = await Task.FromResult(DateTime.Now.DayOfWeek);

int leisureHours =
today is DayOfWeek.Saturday || today is DayOfWeek.Sunday
? 16 : 5;

return leisureHours;
}
// Example output:
// Today is Wednesday, May 24, 2017
// Today's hours of leisure: 5

Wenn GetLeisureHoursAsync aus einem „await“-Ausdruck in der Methode ShowTodaysInfo aufgerufen wird, ruft
der „await“-Ausdruck den ganzzahligen Wert ab (der Wert von GetLeisureHours ), der in der Aufgabe
gespeichert wird, die von der Methode leisureHours zurückgegeben wird. Weitere Informationen zu await-
Ausdrücken finden Sie unter await.
Wie await das Ergebnis aus einem Task<T> abruft, lässt sich besser erkennen, wenn Sie den Aufruf von
GetLeisureHoursAsync von der Anwendung von await trennen, wie der folgende Code zeigt. Ein Aufruf der
GetLeisureHoursAsync -Methode, die nicht sofort eine Antwort erwartet, gibt ein Task<int> zurück, wie Sie es
von der Deklaration der Methode erwarten. Die Aufgabe wird im Beispiel der getLeisureHoursTask -Variablen
zugewiesen. Da getLeisureHoursTask eine Task<TResult> ist, enthält es eine Result-Eigenschaft des Typs
TResult . In diesem Fall stellt TResult einen Integertyp dar. Wenn await auf getLeisureHoursTask angewendet
wird, wertet der „await“-Ausdruck den Inhalt der Eigenschaft Result von getLeisureHoursTask aus. Der Wert
wird der ret -Variablen zugewiesen.

IMPORTANT
Die Result-Eigenschaft ist eine Blocking-Eigenschaft. Wenn Sie darauf zuzugreifen versuchen, bevor seine Aufgabe beendet
ist, wird der momentan aktive Thread blockiert, bis die Aufgabe abgeschlossen und der Wert verfügbar ist. In den meisten
Fällen sollten Sie auf den Wert zugreifen, indem Sie await verwenden, anstatt direkt auf die Eigenschaft zuzugreifen.
Im vorherigen Beispiel wurde der Wert der Result-Eigenschaft abgerufen, um den Hauptthread zu blockieren, damit die
Main -Methode message an die Konsole ausgeben konnte, bevor die Anwendung beendet wurde.

var getLeisureHoursTask = GetLeisureHoursAsync();

string message =
$"Today is {DateTime.Today:D}\n" +
"Today's hours of leisure: " +
$"{await getLeisureHoursTask}";

Console.WriteLine(message);

Rückgabetyp „Void“
Der Rückgabetyp void wird in asynchronen Ereignishandlern verwendet, die den Rückgabetyp void
erfordern. Für andere Methoden als Ereignishandler, die keinen Wert zurückgeben, sollten Sie stattdessen Task
zurückgeben, da eine asynchrone Methode, die void zurückgibt, nicht erwartet werden kann. Jeder Aufrufer
einer solchen Methode muss bis zum Abschluss ausgeführt werden, ohne darauf zu warten, dass die
aufgerufene asynchrone Methode abgeschlossen wird. Der Aufrufer muss von allen Werten oder Ausnahmen
unabhängig sein, die von der asynchronen Methode generiert werden.
Der Aufrufer einer asynchronen Methode, die „void“ zurückgibt, kann keine von der Methode ausgelösten
Ausnahmen abfangen. Solche nicht behandelten Ausnahmen führen wahrscheinlich zu Fehlern in der
Anwendung. Wenn eine Methode, die Task oder Task<TResult> zurückgibt, eine Ausnahme auslöst, wird die
Ausnahme im zurückgegebenen Task gespeichert. Die Ausnahme wird erneut ausgelöst, wenn auf den Task
gewartet wird. Stellen Sie sicher, dass jede asynchrone Methode, die eine Ausnahme erzeugen kann, den
Rückgabetyp Task oder Task<TResult> aufweist, und dass Aufrufe der Methode erwartet werden.
Weitere Informationen zum Auffangen von Ausnahmen in asynchronen Methoden finden Sie im Abschnitt
Ausnahmen in async-Methoden des Artikels try-catch (C#-Referenz).
Im folgenden Beispiel wird das Verhalten eines asynchronen Ereignishandlers dargestellt. Im Beispielcode muss
ein asynchroner Ereignishandler dem Hauptthread mitteilen, dass er abgeschlossen wurde. Anschließend kann
der Hauptthread darauf warten, dass ein asynchroner Ereignishandler abgeschlossen wird, bevor das Programm
beendet wird.

using System;
using System.Threading.Tasks;

public class NaiveButton


{
public event EventHandler? Clicked;

public void Click()


{
Console.WriteLine("Somebody has clicked a button. Let's raise the event...");
Clicked?.Invoke(this, EventArgs.Empty);
Console.WriteLine("All listeners are notified.");
}
}

public class AsyncVoidExample


{
static readonly TaskCompletionSource<bool> s_tcs = new TaskCompletionSource<bool>();

public static async Task MultipleEventHandlersAsync()


{
Task<bool> secondHandlerFinished = s_tcs.Task;

var button = new NaiveButton();

button.Clicked += OnButtonClicked1;
button.Clicked += OnButtonClicked2Async;
button.Clicked += OnButtonClicked3;

Console.WriteLine("Before button.Click() is called...");


button.Click();
Console.WriteLine("After button.Click() is called...");

await secondHandlerFinished;
}

private static void OnButtonClicked1(object? sender, EventArgs e)


{
Console.WriteLine(" Handler 1 is starting...");
Task.Delay(100).Wait();
Console.WriteLine(" Handler 1 is done.");
}

private static async void OnButtonClicked2Async(object? sender, EventArgs e)


{
Console.WriteLine(" Handler 2 is starting...");
Task.Delay(100).Wait();
Console.WriteLine(" Handler 2 is about to go async...");
await Task.Delay(500);
Console.WriteLine(" Handler 2 is done.");
s_tcs.SetResult(true);
}

private static void OnButtonClicked3(object? sender, EventArgs e)


{
Console.WriteLine(" Handler 3 is starting...");
Task.Delay(100).Wait();
Console.WriteLine(" Handler 3 is done.");
}
}
// Example output:
//
// Before button.Click() is called...
// Somebody has clicked a button. Let's raise the event...
// Handler 1 is starting...
// Handler 1 is done.
// Handler 2 is starting...
// Handler 2 is about to go async...
// Handler 3 is starting...
// Handler 3 is done.
// All listeners are notified.
// After button.Click() is called...
// Handler 2 is done.

Generalisierte asynchrone Rückgabetypen und ValueTask<TResult>


Ab C# 7.0 kann eine asynchrone Methode jeden Typ zurückgeben, der eine zugängliche GetAwaiter -Methode
hat, die eine Instanz eines awaiter-Typs zurückgibt. Außerdem muss der von der GetAwaiter -Methode
zurückgegebene Typ über das System.Runtime.CompilerServices.AsyncMethodBuilderAttribute-Attribut
verfügen. Weitere Informationen finden Sie im Artikel Vom Compiler gelesene Attribute oder in der
Featurespezifikation für Taskähnliche Rückgabetypen.
Diese Funktion ist das Gegenstück zu awaitable Ausdrücken, die die Anforderungen an den Operanden von
await beschreibt. Generalisierte asynchrone Rückgabetypen ermöglichen dem Compiler das Generieren von
async -Methoden, die unterschiedliche Typen zurückgeben. Generalisierte asynchrone Rückgabetypen
ermöglichten Leistungsverbesserungen in den .NET-Bibliotheken. Da es sich bei Task und Task<TResult> um
Verweistypen handelt, kann bei der Speicherbelegung in leistungskritischen Pfaden, besonders bei
Zuordnungen in engen Schleifen, die Leistung beeinträchtigt werden. Die Unterstützung für generalisierte
Rückgabetypen bedeutet, dass Sie einen einfachen Werttyp statt eines Verweistypen zurückgeben können, um
zusätzliche Speicherbelegungen zu vermeiden.
.NET stellt die Struktur System.Threading.Tasks.ValueTask<TResult> als einfache Implementierung eines
generalisierten Werts bereit, der eine Aufgabe zurückgibt. Sie müssen das NuGet-Paket
System.Threading.Tasks.Extensions zu Ihrem Projekt hinzufügen, um den Typ
System.Threading.Tasks.ValueTask<TResult> zu verwenden. Im folgenden Beispiel wird die Struktur
ValueTask<TResult> verwendet, um den Wert von zwei Würfelvorgängen abzurufen.
using System;
using System.Threading.Tasks;

class Program
{
static readonly Random s_rnd = new Random();

static async Task Main() =>


Console.WriteLine($"You rolled {await GetDiceRollAsync()}");

static async ValueTask<int> GetDiceRollAsync()


{
Console.WriteLine("Shaking dice...");

int roll1 = await RollAsync();


int roll2 = await RollAsync();

return roll1 + roll2;


}

static async ValueTask<int> RollAsync()


{
await Task.Delay(500);

int diceRoll = s_rnd.Next(1, 7);


return diceRoll;
}
}
// Example output:
// Shaking dice...
// You rolled 8

Das Schreiben eines generalisierten asynchronen Rückgabetyps ist ein fortgeschrittenes Szenario und zur
Verwendung in spezialisierten Umgebungen gedacht. Erwägen Sie stattdessen die Verwendung der Typen
Task , Task<T> und ValueTask<T> , die die meisten Szenarios für asynchronen Code abdecken.

Ab C# 10.0 können Sie das AsyncMethodBuilder -Attribut auf eine asynchrone Methode (anstelle der Deklaration
des asynchronen Rückgabetyps) anwenden, um den Generator für diesen Typ zu überschreiben. Normalerweise
wenden Sie dieses Attribut an, um einen anderen Generator zu verwenden, der in der .NET-Runtime
bereitgestellt wird.

Asynchrone Datenströme mit IAsyncEnumerable<T>


Ab C# 8.0 kann eine asynchrone Methode einen asynchronen Datenstrom zurückgeben, der durch
IAsyncEnumerable<T> dargestellt wird. Ein asynchroner Datenstrom bietet eine Möglichkeit zur Aufzählung von
Elementen, die aus einem Datenstrom gelesen wurden, wenn die Elemente mit wiederholten asynchronen
Aufrufen in Blöcken generiert werden. Das folgende Beispiel zeigt eine asynchrone Methode, die einen
asynchronen Datenstrom generiert:
static async IAsyncEnumerable<string> ReadWordsFromStreamAsync()
{
string data =
@"This is a line of text.
Here is the second line of text.
And there is one more for good measure.
Wait, that was the penultimate line.";

using var readStream = new StringReader(data);

string line = await readStream.ReadLineAsync();


while (line != null)
{
foreach (string word in line.Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
yield return word;
}

line = await readStream.ReadLineAsync();


}
}

Das oben gezeigte Beispiel liest asynchron Zeilen aus einer Zeichenfolge. Sobald eine Zeile gelesen wurde, zählt
der Code jedes Wort in der Zeichenfolge auf. Aufrufen zählen die einzelnen Wörter mit der await foreach -
Anweisung auf. Die Methode wartet, wenn sie die nächste Zeile aus der Quellzeichenfolge asynchron lesen
muss.

Siehe auch
FromResult
Verarbeiten asynchroner Aufgaben nach Abschluss
Asynchrone Programmierung mit Async und Await (C#)
async
await
Abbrechen einer Aufgabenliste (C#)
04.11.2021 • 4 minutes to read

Sie können eine asynchrone Konsolenanwendung abbrechen, wenn Sie nicht auf den Abschluss ihrer
Ausführung warten möchten. Anhand des in diesem Artikel veranschaulichten Beispiels können Sie einen
Abbruch zu einer Anwendung hinzufügen, die die Inhalte von einer Liste von Websites herunterlädt. Sie können
viele Aufgaben abbrechen, indem Sie die CancellationTokenSource-Instanz jeder Aufgabe zuordnen. Wenn Sie
die EINGABETASTE drücken, brechen Sie alle Aufgaben ab, die noch nicht abgeschlossen wurden.
In diesem Lernprogramm wird Folgendes behandelt:
Erstellen einer .NET-Konsolenanwendung
Schreiben einer asynchronen Anwendung, die einen Abbruch unterstützt
Veranschaulichung der Signalisierung eines Abbruchs

Voraussetzungen
Für dieses Lernprogramm ist Folgendes erforderlich:
.NET SDK 5.0 oder höher
integrierte Entwicklungsumgebung (Integrated Development Environment, IDE)
Es wird empfohlen, Visual Studio, Visual Studio Code oder Visual Studio für Mac zu verwenden.
Erstellen einer Beispielanwendung
Erstellen Sie eine neue .NET Core-Konsolenanwendung. Sie können eine solche Anwendung mithilfe des Befehls
dotnet new console oder über Visual Studio erstellen. Öffnen Sie die Program.cs-Datei in Ihrem bevorzugten
Code-Editor.
Ersetzen von using-Anweisungen
Ersetzen Sie die vorhandenen using-Anweisungen durch diese Deklarationen:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

Hinzufügen von Feldern


Fügen Sie die folgenden drei Felder in der Program -Klassendefinition hinzu:
static readonly CancellationTokenSource s_cts = new CancellationTokenSource();

static readonly HttpClient s_client = new HttpClient


{
MaxResponseContentBufferSize = 1_000_000
};

static readonly IEnumerable<string> s_urlList = new string[]


{
"https://docs.microsoft.com",
"https://docs.microsoft.com/aspnet/core",
"https://docs.microsoft.com/azure",
"https://docs.microsoft.com/azure/devops",
"https://docs.microsoft.com/dotnet",
"https://docs.microsoft.com/dynamics365",
"https://docs.microsoft.com/education",
"https://docs.microsoft.com/enterprise-mobility-security",
"https://docs.microsoft.com/gaming",
"https://docs.microsoft.com/graph",
"https://docs.microsoft.com/microsoft-365",
"https://docs.microsoft.com/office",
"https://docs.microsoft.com/powershell",
"https://docs.microsoft.com/sql",
"https://docs.microsoft.com/surface",
"https://docs.microsoft.com/system-center",
"https://docs.microsoft.com/visualstudio",
"https://docs.microsoft.com/windows",
"https://docs.microsoft.com/xamarin"
};

CancellationTokenSource wird dazu verwendet, einen angeforderten Abbruch für CancellationToken zu


signalisieren. HttpClient ermöglicht das Senden und Empfangen von HTTP-Antworten. s_urlList enthält alle
URLs, die von der Anwendung verarbeitet werden sollen.

Aktualisieren des Einstiegspunkts der Anwendung


Der Haupteinstiegspunkt in die Konsolenanwendung ist die Main -Methode. Ersetzen Sie die vorhandene
Methode durch Folgendes:

static async Task Main()


{
Console.WriteLine("Application started.");
Console.WriteLine("Press the ENTER key to cancel...\n");

Task cancelTask = Task.Run(() =>


{
while (Console.ReadKey().Key != ConsoleKey.Enter)
{
Console.WriteLine("Press the ENTER key to cancel...");
}

Console.WriteLine("\nENTER key pressed: cancelling downloads.\n");


s_cts.Cancel();
});

Task sumPageSizesTask = SumPageSizesAsync();

await Task.WhenAny(new[] { cancelTask, sumPageSizesTask });

Console.WriteLine("Application ending.");
}
Die aktualisierte Main -Methode wird jetzt als Async main-Methode betrachtet, die einen asynchronen
Einstiegspunkt in die ausführbare Datei ermöglicht. Sie schreibt einige Anweisungsnachrichten an die Konsole
und deklariert dann eine Task-Instanz namens cancelTask , die Tastatureingaben in die Konsole liest. Wenn die
EINGABETASTE gedrückt wird, wird ein Aufruf an CancellationTokenSource.Cancel() ausgeführt. Dies signalisiert
den Abbruch. Dann wird eine sumPageSizesTask -Variable der SumPageSizesAsync -Methode zugewiesen. Beide
Aufgaben werden dann an Task.WhenAny(Task[]) übergeben, was fortgesetzt wird, wenn eine der beiden
Aufgaben abgeschlossen wird.

Erstellen der SumPageSizesAsync-Methode


Fügen Sie unter der Main -Methode die SumPageSizesAsync -Methode hinzu:

static async Task SumPageSizesAsync()


{
var stopwatch = Stopwatch.StartNew();

int total = 0;
foreach (string url in s_urlList)
{
int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
total += contentLength;
}

stopwatch.Stop();

Console.WriteLine($"\nTotal bytes returned: {total:#,#}");


Console.WriteLine($"Elapsed time: {stopwatch.Elapsed}\n");
}

Die Methode beginnt mit dem Instanziieren und Starten einer Stopwatch-Klasse. Anschließend durchläuft sie alle
URLs in s_urlList und ruft ProcessUrlAsync auf. Mit jeder Iteration wird s_cts.Token an die ProcessUrlAsync -
Methode übergeben, und der Code gibt Task<TResult> zurück, wobei TResult ein Integer ist:

int total = 0;
foreach (string url in s_urlList)
{
int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
total += contentLength;
}

Hinzufügen der Prozessmethode


Fügen Sie die folgende ProcessUrlAsync -Methode unterhalb der SumPageSizesAsync -Methode hinzu:

static async Task<int> ProcessUrlAsync(string url, HttpClient client, CancellationToken token)


{
HttpResponseMessage response = await client.GetAsync(url, token);
byte[] content = await response.Content.ReadAsByteArrayAsync(token);
Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

return content.Length;
}

Für eine beliebige URL verwendet die Methode die client -Instanz, die bereitgestellt wird, um die Antwort als
byte[] zu erhalten. Die CancellationToken-Instanz wird an die Methoden HttpClient.GetAsync(String,
CancellationToken) und HttpContent.ReadAsByteArrayAsync() übergeben. token wird dazu verwendet, den
angeforderten Abbruch zu registrieren. Die Länge wird zurückgegeben, nachdem die URL und die Länge in die
Konsole geschrieben wurden.
Ausgabe der Beispielanwendung

Application started.
Press the ENTER key to cancel...

https://docs.microsoft.com 37,357
https://docs.microsoft.com/aspnet/core 85,589
https://docs.microsoft.com/azure 398,939
https://docs.microsoft.com/azure/devops 73,663
https://docs.microsoft.com/dotnet 67,452
https://docs.microsoft.com/dynamics365 48,582
https://docs.microsoft.com/education 22,924

ENTER key pressed: cancelling downloads.

Application ending.

Vollständiges Beispiel
Der folgende Code besteht aus dem vollständigen Text der Program.cs-Datei für das Beispiel.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static readonly CancellationTokenSource s_cts = new CancellationTokenSource();

static readonly HttpClient s_client = new HttpClient


{
MaxResponseContentBufferSize = 1_000_000
};

static readonly IEnumerable<string> s_urlList = new string[]


{
"https://docs.microsoft.com",
"https://docs.microsoft.com/aspnet/core",
"https://docs.microsoft.com/azure",
"https://docs.microsoft.com/azure/devops",
"https://docs.microsoft.com/dotnet",
"https://docs.microsoft.com/dynamics365",
"https://docs.microsoft.com/education",
"https://docs.microsoft.com/enterprise-mobility-security",
"https://docs.microsoft.com/gaming",
"https://docs.microsoft.com/graph",
"https://docs.microsoft.com/microsoft-365",
"https://docs.microsoft.com/office",
"https://docs.microsoft.com/powershell",
"https://docs.microsoft.com/sql",
"https://docs.microsoft.com/surface",
"https://docs.microsoft.com/system-center",
"https://docs.microsoft.com/visualstudio",
"https://docs.microsoft.com/windows",
"https://docs.microsoft.com/xamarin"
};

static async Task Main()


{
Console.WriteLine("Application started.");
Console.WriteLine("Press the ENTER key to cancel...\n");
Console.WriteLine("Press the ENTER key to cancel...\n");

Task cancelTask = Task.Run(() =>


{
while (Console.ReadKey().Key != ConsoleKey.Enter)
{
Console.WriteLine("Press the ENTER key to cancel...");
}

Console.WriteLine("\nENTER key pressed: cancelling downloads.\n");


s_cts.Cancel();
});

Task sumPageSizesTask = SumPageSizesAsync();

await Task.WhenAny(new[] { cancelTask, sumPageSizesTask });

Console.WriteLine("Application ending.");
}

static async Task SumPageSizesAsync()


{
var stopwatch = Stopwatch.StartNew();

int total = 0;
foreach (string url in s_urlList)
{
int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
total += contentLength;
}

stopwatch.Stop();

Console.WriteLine($"\nTotal bytes returned: {total:#,#}");


Console.WriteLine($"Elapsed time: {stopwatch.Elapsed}\n");
}

static async Task<int> ProcessUrlAsync(string url, HttpClient client, CancellationToken token)


{
HttpResponseMessage response = await client.GetAsync(url, token);
byte[] content = await response.Content.ReadAsByteArrayAsync(token);
Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

return content.Length;
}
}

Weitere Informationen
CancellationToken
CancellationTokenSource
Asynchrone Programmierung mit Async und Await (C#)

Nächste Schritte
Abbrechen asynchroner Aufgaben nach einem bestimmten Zeitraum (C#)
Asynchrone Aufgaben nach einer Zeitperiode
abbrechen (C#)
04.11.2021 • 2 minutes to read

Sie können einen asynchronen Vorgang nach einem gewissen Zeitraum mithilfe der
CancellationTokenSource.CancelAfter-Methode abbrechen, wenn Sie nicht auf das Ende des Vorgangs warten
möchten. Diese Methode plant den Abbruch aller zugeordneten Aufgaben, die innerhalb des vom CancelAfter -
Ausdruck festgelegten Zeitraums nicht abgeschlossen sind.
Dieses Beispiel fügt dem in Abbrechen einer asynchronen Aufgabe oder Aufgabenliste (C#) entwickelten Code
Funktionen zum Herunterladen einer Liste von Websites und Anzeigen der Länge der Inhalte jeder Site hinzu.
In diesem Lernprogramm wird Folgendes behandelt:
Aktualisieren einer vorhandenen .NET-Konsolenanwendung
Planen eines Abbruchs

Voraussetzungen
Für dieses Lernprogramm ist Folgendes erforderlich:
Erstellen einer Anwendung im Tutorial Abbrechen einer asynchronen Aufgabe oder Aufgabenliste (C#)
.NET SDK 5.0 oder höher
integrierte Entwicklungsumgebung (Integrated Development Environment, IDE)
Es wird empfohlen, Visual Studio, Visual Studio Code oder Visual Studio für Mac zu verwenden.

Aktualisieren des Einstiegspunkts der Anwendung


Ersetzen Sie die vorhandene Main -Methode durch Folgendes:

static async Task Main()


{
Console.WriteLine("Application started.");

try
{
s_cts.CancelAfter(3500);

await SumPageSizesAsync();
}
catch (TaskCanceledException)
{
Console.WriteLine("\nTasks cancelled: timed out.\n");
}
finally
{
s_cts.Dispose();
}

Console.WriteLine("Application ending.");
}

Mit der aktualisierten Main -Methode werden einige Anweisungsnachrichten in die Konsole geschrieben. In der
try catch-Anweisung plant ein Aufruf von CancellationTokenSource.CancelAfter(Int32) einen Abbruch. Dadurch
wird ein Abbruch nach einem bestimmten Zeitraum signalisiert.
Als Nächstes wird die SumPageSizesAsync -Methode erwartet. Wenn die Verarbeitung aller URLs schneller erfolgt
als der geplante Abbruch, wird die Anwendung beendet. Wenn jedoch der geplante Abbruch vor der
Verarbeitung aller URLs ausgelöst wird, wird eine TaskCanceledException-Klasse ausgelöst.
Ausgabe der Beispielanwendung

Application started.

https://docs.microsoft.com 37,357
https://docs.microsoft.com/aspnet/core 85,589
https://docs.microsoft.com/azure 398,939
https://docs.microsoft.com/azure/devops 73,663

Tasks cancelled: timed out.

Application ending.

Vollständiges Beispiel
Der folgende Code besteht aus dem vollständigen Text der Program.cs-Datei für das Beispiel.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static readonly CancellationTokenSource s_cts = new CancellationTokenSource();

static readonly HttpClient s_client = new HttpClient


{
MaxResponseContentBufferSize = 1_000_000
};

static readonly IEnumerable<string> s_urlList = new string[]


{
"https://docs.microsoft.com",
"https://docs.microsoft.com/aspnet/core",
"https://docs.microsoft.com/azure",
"https://docs.microsoft.com/azure/devops",
"https://docs.microsoft.com/dotnet",
"https://docs.microsoft.com/dynamics365",
"https://docs.microsoft.com/education",
"https://docs.microsoft.com/enterprise-mobility-security",
"https://docs.microsoft.com/gaming",
"https://docs.microsoft.com/graph",
"https://docs.microsoft.com/microsoft-365",
"https://docs.microsoft.com/office",
"https://docs.microsoft.com/powershell",
"https://docs.microsoft.com/sql",
"https://docs.microsoft.com/surface",
"https://docs.microsoft.com/system-center",
"https://docs.microsoft.com/visualstudio",
"https://docs.microsoft.com/windows",
"https://docs.microsoft.com/xamarin"
};

static async Task Main()


{
Console.WriteLine("Application started.");
Console.WriteLine("Application started.");

try
{
s_cts.CancelAfter(3500);

await SumPageSizesAsync();
}
catch (TaskCanceledException)
{
Console.WriteLine("\nTasks cancelled: timed out.\n");
}
finally
{
s_cts.Dispose();
}

Console.WriteLine("Application ending.");
}

static async Task SumPageSizesAsync()


{
var stopwatch = Stopwatch.StartNew();

int total = 0;
foreach (string url in s_urlList)
{
int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
total += contentLength;
}

stopwatch.Stop();

Console.WriteLine($"\nTotal bytes returned: {total:#,#}");


Console.WriteLine($"Elapsed time: {stopwatch.Elapsed}\n");
}

static async Task<int> ProcessUrlAsync(string url, HttpClient client, CancellationToken token)


{
HttpResponseMessage response = await client.GetAsync(url, token);
byte[] content = await response.Content.ReadAsByteArrayAsync(token);
Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

return content.Length;
}
}

Weitere Informationen
CancellationToken
CancellationTokenSource
Asynchrone Programmierung mit Async und Await (C#)
Abbrechen einer Aufgabenliste (C#)
Verarbeiten asynchroner Aufgaben nach Abschluss
(C#)
04.11.2021 • 4 minutes to read

Mit Task.WhenAny können Sie mehrere Aufgaben gleichzeitig starten und diese nicht in der Reihenfolge, in der
sie gestartet wurden, sondern zu dem Zeitpunkt, zu dem sie abgeschlossen werden, verarbeiten.
Im folgenden Beispiel wird eine Abfrage verwendet, um eine Auflistung von Aufgaben zu erstellen. Jede
Aufgabe lädt den Inhalt einer angegebenen Website herunter. In jeder Iteration einer While-Schleife gibt ein
erwarteter Aufruf von WhenAny die Aufgabe in der Auflistung von Aufgaben zurück, die ihren Download zuerst
beendet. Diese Aufgabe wird aus der Auflistung entfernt und verarbeitet. Die Ausführung der Schleife wird
wiederholt, bis die Auflistung keine Aufgaben mehr enthält.

Erstellen einer Beispielanwendung


Erstellen Sie eine neue .NET Core-Konsolenanwendung. Sie können mithilfe des dotnet new console-Befehls
oder über Visual Studio eine solche Anwendung erstellen. Öffnen Sie die Program.cs-Datei in Ihrem
bevorzugten Code-Editor.
Ersetzen von using-Anweisungen
Ersetzen Sie die vorhandenen using-Anweisungen durch diese Deklarationen:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

Hinzufügen von Feldern


Fügen Sie in der Program -Klassendefinition die folgenden beiden Felder hinzu:
static readonly HttpClient s_client = new HttpClient
{
MaxResponseContentBufferSize = 1_000_000
};

static readonly IEnumerable<string> s_urlList = new string[]


{
"https://docs.microsoft.com",
"https://docs.microsoft.com/aspnet/core",
"https://docs.microsoft.com/azure",
"https://docs.microsoft.com/azure/devops",
"https://docs.microsoft.com/dotnet",
"https://docs.microsoft.com/dynamics365",
"https://docs.microsoft.com/education",
"https://docs.microsoft.com/enterprise-mobility-security",
"https://docs.microsoft.com/gaming",
"https://docs.microsoft.com/graph",
"https://docs.microsoft.com/microsoft-365",
"https://docs.microsoft.com/office",
"https://docs.microsoft.com/powershell",
"https://docs.microsoft.com/sql",
"https://docs.microsoft.com/surface",
"https://docs.microsoft.com/system-center",
"https://docs.microsoft.com/visualstudio",
"https://docs.microsoft.com/windows",
"https://docs.microsoft.com/xamarin"
};

Der HttpClient ermöglicht das Senden von HTTP-Anforderungen und das Empfangen von HTTP-Antworten.
s_urlList enthält alle URLs, die von der Anwendung verarbeitet werden sollen.

Aktualisieren des Einstiegspunkts der Anwendung


Der Haupteinstiegspunkt in die Konsolenanwendung ist die Main -Methode. Ersetzen Sie die vorhandene
Methode durch Folgendes:

static Task Main() => SumPageSizesAsync();

Die aktualisierte Main -Methode wird jetzt als Async main-Methode betrachtet, die einen asynchronen
Einstiegspunkt in die ausführbare Datei ermöglicht. Sie wird mit einem Aufruf von SumPageSizesAsync
angegeben.

Erstellen der SumPageSizesAsync-Methode


Fügen Sie unter der Main -Methode die SumPageSizesAsync -Methode hinzu:
static async Task SumPageSizesAsync()
{
var stopwatch = Stopwatch.StartNew();

IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);

List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

int total = 0;
while (downloadTasks.Any())
{
Task<int> finishedTask = await Task.WhenAny(downloadTasks);
downloadTasks.Remove(finishedTask);
total += await finishedTask;
}

stopwatch.Stop();

Console.WriteLine($"\nTotal bytes returned: {total:#,#}");


Console.WriteLine($"Elapsed time: {stopwatch.Elapsed}\n");
}

Die Methode beginnt mit dem Instanziieren und Starten einer Stopwatch-Klasse. Dann ist eine Abfrage
enthalten, die eine Auflistung von Aufgaben erstellt, wenn sie ausgeführt wird. Jeder Aufruf von
ProcessUrlAsync im folgenden Code gibt Task<TResult> zurück, wobei TResult eine ganze Zahl ist:

IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);

Aufgrund der verzögerten Ausführung mit LINQ wird Enumerable.ToList aufgerufen, um die einzelnen Aufgaben
zu starten.

List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

Die while -Schleife führt die folgenden Schritte für jede Aufgabe in der Auflistung aus:
1. Es wird ein Aufruf von WhenAny erwartet, um die erste Aufgabe in der Auflistung zu identifizieren, die
ihren Download beendet hat.

Task<int> finishedTask = await Task.WhenAny(downloadTasks);

2. Entfernt die entsprechende Aufgabe aus der Auflistung.

downloadTasks.Remove(finishedTask);

3. Erwartet , das durch einen Aufruf von ProcessUrlAsync zurückgegeben wird. Die Variable
finishedTask
finishedTask ist eine Task<TResult>, wobei TResult eine ganze Zahl ist. Die Aufgabe ist bereits
abgeschlossen, aber es darauf gewartet, dass von ihr die Länge der heruntergeladenen Website
abgerufen wird, wie im folgenden Beispiel dargestellt. Wenn für die Aufgabe ein Fehler auftritt, löst
await im Gegensatz zum Lesen der Task<TResult>.Result-Eigenschaft, die die AggregateException
auslösen würde, die erste untergeordnete Ausnahme aus, die in der AggregateException gespeichert ist.
total += await finishedTask;

Hinzufügen der Prozessmethode


Fügen Sie die folgende ProcessUrlAsync -Methode unterhalb der SumPageSizesAsync -Methode hinzu:

static async Task<int> ProcessUrlAsync(string url, HttpClient client)


{
byte[] content = await client.GetByteArrayAsync(url);
Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

return content.Length;
}

Für eine beliebige URL verwendet die Methode die client -Instanz, die bereitgestellt wird, um die Antwort als
byte[] zu erhalten. Die Länge wird zurückgegeben, nachdem die URL und die Länge in die Konsole
geschrieben wurden.
Führen Sie das Programm mehrmals aus, um zu bestätigen, dass die heruntergeladenen Längen nicht immer in
der gleichen Reihenfolge angezeigt werden.
Cau t i on

Sie können WhenAny in einer Schleife gemäß der Beschreibung im Beispiel verwenden, um Problemlösungen zu
entwickeln, die nur wenige Aufgaben umfassen. Andere Ansätze sind jedoch effizienter, wenn viele Aufgaben
verarbeitet werden müssen. Weitere Informationen und Beispiele finden Sie unter Processing Tasks as they
complete (Verarbeitung von Aufgaben nach deren Abschluss).

Vollständiges Beispiel
Der folgende Code besteht aus dem vollständigen Text der Program.cs-Datei für das Beispiel.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace ProcessTasksAsTheyFinish
{
class Program
{
static readonly HttpClient s_client = new HttpClient
{
MaxResponseContentBufferSize = 1_000_000
};

static readonly IEnumerable<string> s_urlList = new string[]


{
"https://docs.microsoft.com",
"https://docs.microsoft.com/aspnet/core",
"https://docs.microsoft.com/azure",
"https://docs.microsoft.com/azure/devops",
"https://docs.microsoft.com/dotnet",
"https://docs.microsoft.com/dynamics365",
"https://docs.microsoft.com/education",
"https://docs.microsoft.com/enterprise-mobility-security",
"https://docs.microsoft.com/gaming",
"https://docs.microsoft.com/graph",
"https://docs.microsoft.com/microsoft-365",
"https://docs.microsoft.com/office",
"https://docs.microsoft.com/powershell",
"https://docs.microsoft.com/sql",
"https://docs.microsoft.com/surface",
"https://docs.microsoft.com/system-center",
"https://docs.microsoft.com/visualstudio",
"https://docs.microsoft.com/windows",
"https://docs.microsoft.com/xamarin"
};

static Task Main() => SumPageSizesAsync();

static async Task SumPageSizesAsync()


{
var stopwatch = Stopwatch.StartNew();

IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);

List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

int total = 0;
while (downloadTasks.Any())
{
Task<int> finishedTask = await Task.WhenAny(downloadTasks);
downloadTasks.Remove(finishedTask);
total += await finishedTask;
}

stopwatch.Stop();

Console.WriteLine($"\nTotal bytes returned: {total:#,#}");


Console.WriteLine($"Elapsed time: {stopwatch.Elapsed}\n");
}

static async Task<int> ProcessUrlAsync(string url, HttpClient client)


{
byte[] content = await client.GetByteArrayAsync(url);
Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

return content.Length;
}
}
}
// Example output:
// https://docs.microsoft.com/windows 25,513
// https://docs.microsoft.com/gaming 30,705
// https://docs.microsoft.com/dotnet 69,626
// https://docs.microsoft.com/dynamics365 50,756
// https://docs.microsoft.com/surface 35,519
// https://docs.microsoft.com 39,531
// https://docs.microsoft.com/azure/devops 75,837
// https://docs.microsoft.com/xamarin 60,284
// https://docs.microsoft.com/system-center 43,444
// https://docs.microsoft.com/enterprise-mobility-security 28,946
// https://docs.microsoft.com/microsoft-365 43,278
// https://docs.microsoft.com/visualstudio 31,414
// https://docs.microsoft.com/office 42,292
// https://docs.microsoft.com/azure 401,113
// https://docs.microsoft.com/graph 46,831
// https://docs.microsoft.com/education 25,098
// https://docs.microsoft.com/powershell 58,173
// https://docs.microsoft.com/aspnet/core 87,763
// https://docs.microsoft.com/sql 53,362

// Total bytes returned: 1,249,485


// Elapsed time: 00:00:02.7068725
Weitere Informationen
WhenAny
Asynchrone Programmierung mit Async und Await (C#)
Asynchroner Dateizugriff (C#)
04.11.2021 • 5 minutes to read

Sie können die Async-Funktion verwenden, um auf Dateien zuzugreifen. Mithilfe der Async-Funktion können Sie
asynchrone Methoden aufrufen, ohne Rückrufe zu verwenden oder den Code über mehrere Methoden oder
Lambdaausdrücke teilen zu müssen. Um synchronen Code asynchron auszuführen, rufen Sie einfach eine
asynchrone Methode anstelle einer synchronen Methode auf und fügen Sie dem Code einige Schlüsselwörter
hinzu.
Sie sollten die folgenden Gründe für das Hinzufügen von Asynchronie zu Dateizugriffsaufrufen in Betracht
ziehen:
Durch Asynchronie kann die Reaktionsfähigkeit von UI-Anwendungen verbessert werden, da der UI-Thread,
der den Vorgang startet, andere Aufgaben durchführen kann. Wenn der UI-Thread Code ausführt, der viel
Zeit benötigt (z.B. mehr als 50 Millisekunden), kann die UI einfrieren, bis der E/A-Vorgang abgeschlossen ist
und der UI-Thread wieder Tastatur- und Mauseingaben und andere Ereignisse verarbeiten kann.
Asynchronie verbessert die Skalierbarkeit von ASP.NET und anderen serverbasierten Anwendungen, indem
die Notwendigkeit von Threads reduziert wird. Wenn die Anwendung einen dedizierten Thread pro Antwort
verwendet und tausend Anforderungen gleichzeitig behandelt werden, werden auch tausend Threads
benötigt. Asynchrone Vorgänge benötigen während der Wartezeit oft keinen Thread. Sie verwenden den
vorhandenen E/A-Abschlussthread kurz am Ende.
Die Latenz von Dateizugriffsvorgängen kann unter bestimmten Umständen sehr niedrig sein, aber sie kann
in Zukunft stark ansteigen. Eine Datei kann z.B. auf einen Server auf der anderen Seite der Erde verschoben
werden.
Der zusätzliche Mehraufwand durch Verwendung der Async-Funktion ist gering.
Asynchrone Aufgaben können problemlos parallel ausgeführt werden.

Verwenden geeigneter Klassen


Die in diesem Thema gezeigten einfachen Beispiele veranschaulichen File.WriteAllTextAsync und
File.ReadAllTextAsync. Für begrenzte Kontrolle über die E/A-Vorgänge der Datei verwenden Sie die FileStream-
Klasse, die über eine Option verfügt, die asynchrone E/A-Vorgänge auf Betriebssystemebene auslöst. Mit dieser
Option können Sie das Blockieren eines ThreadPool-Threads in vielen Fällen vermeiden. Geben Sie zum
Aktivieren dieser Option das Argument useAsync=true oder options=FileOptions.Asynchronous im
Konstruktoraufruf an.
Sie können diese Option nicht mit StreamReader und StreamWriter verwenden, wenn Sie sie direkt öffnen,
indem Sie einen Dateipfad angeben. Allerdings können Sie diese Option verwenden, wenn Sie ihnen ein Stream
geben, das die FileStream-Klasse geöffnet hat. Asynchrone Aufrufe in Anwendungsbenutzeroberflächen sind
schneller, selbst wenn ein ThreadPool-Thread blockiert wird, da der UI-Thread während der Wartezeit nicht
blockiert wird.

Schreiben von Text


In den folgenden Beispielen wird Text in eine Datei geschrieben. Bei jeder await-Anweisung wird die Methode
sofort beendet. Wenn die Datei-E/A abgeschlossen ist, wird die Methode bei der Anweisung hinter der await-
Anweisung fortgesetzt. Der async-Modifizierer befindet sich in der Definition der Methoden, die die await-
Anweisung verwenden.
Einfaches Beispiel
public async Task SimpleWriteAsync()
{
string filePath = "simple.txt";
string text = $"Hello World";

await File.WriteAllTextAsync(filePath, text);


}

Beispiel für begrenzte Kontrolle

public async Task ProcessWriteAsync()


{
string filePath = "temp.txt";
string text = $"Hello World{Environment.NewLine}";

await WriteTextAsync(filePath, text);


}

async Task WriteTextAsync(string filePath, string text)


{
byte[] encodedText = Encoding.Unicode.GetBytes(text);

using var sourceStream =


new FileStream(
filePath,
FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 4096, useAsync: true);

await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);


}

Das ursprüngliche Beispiel verfügt über die Anweisung


await sourceStream.WriteAsync(encodedText, 0, encodedText.Length); , bei der es sich um eine Kontraktion der
folgenden zwei Anweisungen handelt:

Task theTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);


await theTask;

Die erste Anweisung gibt eine Aufgabe zurück und löst die Dateiverarbeitung aus. Die zweite await-Anweisung
führt dazu, dass die Methode sofort beendet wird und eine andere Aufgabe zurückgibt. Wenn die
Dateiverarbeitung später abgeschlossen wird, wird die Ausführung bei der Anweisung fortgesetzt, die auf die
„await“-Anweisung folgt.

Lesen von Text


In den folgenden Beispielen wird Text aus einer Datei gelesen.
Einfaches Beispiel

public async Task SimpleReadAsync()


{
string filePath = "simple.txt";
string text = await File.ReadAllTextAsync(filePath);

Console.WriteLine(text);
}

Beispiel für begrenzte Kontrolle


Der Text wird gepuffert und in diesem Fall in StringBuilder abgelegt. Anders als im vorherigen Beispiel generiert
die Auswertung von „await“ einen Wert. Die Methode ReadAsync gibt ein Task<Int32>-Element zurück, sodass
die Auswertung der await-Anweisung einen Int32 -Wert von Typ numRead erzeugt, nachdem der Vorgang
abgeschlossen wurde. Weitere Informationen finden Sie unter Async Return Types (C#) (Asynchrone
Rückgabetypen (C#)).

public async Task ProcessReadAsync()


{
try
{
string filePath = "temp.txt";
if (File.Exists(filePath) != false)
{
string text = await ReadTextAsync(filePath);
Console.WriteLine(text);
}
else
{
Console.WriteLine($"file not found: {filePath}");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

async Task<string> ReadTextAsync(string filePath)


{
using var sourceStream =
new FileStream(
filePath,
FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096, useAsync: true);

var sb = new StringBuilder();

byte[] buffer = new byte[0x1000];


int numRead;
while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
string text = Encoding.Unicode.GetString(buffer, 0, numRead);
sb.Append(text);
}

return sb.ToString();
}

Parallele und asynchrone E/A-Vorgänge


In den folgenden Beispielen wird die parallele Verarbeitung durch Schreiben von zehn Textdateien
veranschaulicht.
Einfaches Beispiel
public async Task SimpleParallelWriteAsync()
{
string folder = Directory.CreateDirectory("tempfolder").Name;
IList<Task> writeTaskList = new List<Task>();

for (int index = 11; index <= 20; ++ index)


{
string fileName = $"file-{index:00}.txt";
string filePath = $"{folder}/{fileName}";
string text = $"In file {index}{Environment.NewLine}";

writeTaskList.Add(File.WriteAllTextAsync(filePath, text));
}

await Task.WhenAll(writeTaskList);
}

Beispiel für begrenzte Kontrolle


Die Methode WriteAsync gibt für jede Datei eine Aufgabe zurück, die anschließend zu einer Liste von Aufgaben
hinzugefügt wird. Die await Task.WhenAll(tasks); -Anweisung beendet die Methode und wird innerhalb der
Methode fortgesetzt, wenn die Dateiverarbeitung für alle Aufgaben abgeschlossen ist.
Im Beispiel werden alle FileStream-Instanzen in einem finally -Block geschlossen, nachdem die Aufgaben
abgeschlossen sind. Wenn jeder FileStream stattdessen in einer using -Anweisung erstellt wurde, kann
FileStream verworfen werden, bevor die Aufgabe abgeschlossen ist.

Jegliche Leistungsverbesserungen stammen nahezu ausschließlich von der parallelen Verarbeitung und nicht
von der asynchronen Verarbeitung. Die Vorteile der asynchronen Vorgänge bestehen darin, dass nicht mehrere
Threads und nicht der Benutzeroberflächenthread gebunden werden.
public async Task ProcessMulitpleWritesAsync()
{
IList<FileStream> sourceStreams = new List<FileStream>();

try
{
string folder = Directory.CreateDirectory("tempfolder").Name;
IList<Task> writeTaskList = new List<Task>();

for (int index = 1; index <= 10; ++ index)


{
string fileName = $"file-{index:00}.txt";
string filePath = $"{folder}/{fileName}";

string text = $"In file {index}{Environment.NewLine}";


byte[] encodedText = Encoding.Unicode.GetBytes(text);

var sourceStream =
new FileStream(
filePath,
FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 4096, useAsync: true);

Task writeTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);


sourceStreams.Add(sourceStream);

writeTaskList.Add(writeTask);
}

await Task.WhenAll(writeTaskList);
}
finally
{
foreach (FileStream sourceStream in sourceStreams)
{
sourceStream.Close();
}
}
}

Bei Verwendung der Methoden WriteAsync und ReadAsync können Sie einen CancellationToken angeben, mit
dem Sie den Vorgang in der Mitte des Streams beenden können. Weitere Informationen finden Sie unter
Abbruch in verwalteten Threads.

Weitere Informationen
Asynchrone Programmierung mit Async und Await (C#)
Asynchrone Rückgabetypen (C#)
Attribute (C#)
04.11.2021 • 5 minutes to read

Attribute stellen eine effiziente Methode dar, Metadaten oder deklarative Informationen Code (Assemblys, Typen,
Methoden, Eigenschaften usw.) zuzuordnen. Nach dem Zuordnen eines Attributs zu einer Programmentität kann
das Attribut zur Laufzeit mit einer Technik namens Reflektion abgefragt werden. Weitere Informationen finden
Sie unter Reflektion (C#).
Attribute verfügen über die folgenden Eigenschaften:
Attribute fügen Metadaten zu Ihrem Programm hinzu. Metadaten sind Informationen zu den Typen, die in
einem Programm definiert sind. Alle .NET-Assemblys enthalten einen festgelegten Satz von Metadaten, der
die Typen und Typmember beschreibt, die in der Assembly definiert sind. Sie können benutzerdefinierte
Attribute hinzufügen, um zusätzliche erforderliche Informationen anzugeben. Weitere Informationen finden
Sie unter Erstellen benutzerdefinierter Attribute (C#).
Sie können eines oder mehrere Attribute auf ganze Assemblys, Module oder kleinere Programmelemente
wie Klassen und Eigenschaften anwenden.
Attribute können Argumente auf die gleiche Art wie Methoden und Eigenschaften akzeptieren.
Das Programm kann seine eigenen Metadaten oder die Metadaten in anderen Programmen mithilfe der
Reflektion untersuchen. Weitere Informationen finden Sie unter Accessing Attributes by Using Reflection (C#)
(Zugriff auf Attribute mit Reflektion (C#)).

Verwenden von Attributen


Attribute können in nahezu jeder Deklaration platziert werden, allerdings schränkt möglicherweise ein
bestimmtes Attribut die Typen der Deklarationen ein, für die es gültig ist. In C# geben Sie ein Attribut durch
Angabe des Namens des Attributs an, eingeschlossen in eckigen Klammern ([]), oberhalb der Deklaration der
Entität, auf die es angewendet wird.
In diesem Beispiel wird das Attribut SerializableAttribute benutzt, um ein spezifisches Merkmal auf eine Klasse
anzuwenden:

[Serializable]
public class SampleClass
{
// Objects of this type can be serialized.
}

Eine Methode mit dem Attribut DllImportAttribute wird wie im folgenden Beispiel deklariert:

[System.Runtime.InteropServices.DllImport("user32.dll")]
extern static void SampleMethod();

Mehrere Attribute können in einer Deklaration platziert werden, wie im folgenden Beispiel dargestellt:

using System.Runtime.InteropServices;
void MethodA([In][Out] ref double x) { }
void MethodB([Out][In] ref double x) { }
void MethodC([In, Out] ref double x) { }

Einige Attribute können für eine bestimmte Entität mehrmals angegeben werden. Ein Beispiel für ein solches
mehrfach verwendbares Attribut ist ConditionalAttribute:

[Conditional("DEBUG"), Conditional("TEST1")]
void TraceMethod()
{
// ...
}

NOTE
Alle Attributnamen enden laut Konvention mit dem Wort „Attribute“, um sie von anderen Elementen in .NET-Bibliotheken
zu unterscheiden. Sie müssen das Attributsuffix allerdings nicht angeben, wenn Sie Attribute im Code verwenden. Beispiel:
[DllImport] entspricht [DllImportAttribute] , aber DllImportAttribute ist der tatsächliche Name des Attributs in
der .NET-Klassenbibliothek.

Attributparameter
Viele Attribute weisen Parameter auf, die positional, unbenannt der benannt sein können. Alle positionalen
Parameter müssen in einer bestimmten Reihenfolge angegeben werden und können nicht weggelassen werden.
Benannte Parameter sind optional und können in beliebiger Reihenfolge angegeben werden. Positionale
Parameter werden zuerst angegeben. Die folgenden drei Attribute sind beispielsweise äquivalent:

[DllImport("user32.dll")]
[DllImport("user32.dll", SetLastError=false, ExactSpelling=false)]
[DllImport("user32.dll", ExactSpelling=false, SetLastError=false)]

Der erste Parameter, der DLL-Name, ist positional und kommt immer an erster Stelle. Die anderen sind benannt.
In diesem Fall werden beide benannten Parameter standardmäßig auf „false“ festgelegt und können daher
ausgelassen werden. Positionale Parameter entsprechen den Parametern des Attributkonstruktors. Benannte
oder optionale Parameter entsprechen den Eigenschaften oder Feldern des Attributs. In der Dokumentation des
individuellen Attributs finden Sie Informationen zu Standardparameterwerten.
Weitere Informationen zu zulässigen Parametertypen finden Sie im Abschnitt Attribute der C#-
Sprachspezifikation.
Attributziele
Das Ziel eines Attributs ist die Entität, auf die das Attribut angewendet wird. Ein Attribut kann beispielsweise auf
eine Klasse, eine bestimmte Methode oder eine ganze Assembly angewendet werden. Standardmäßig wird ein
Attribut auf das darauffolgende Element angewendet. Sie können allerdings auch explizit festlegen, dass ein
Attribut beispielsweise auf eine Methode, ihren Parameter oder ihren Rückgabewert angewendet wird.
Verwenden Sie die folgende Syntax, um ein Attributziel explizit zu kennzeichnen:

[target : attribute-list]

Die Liste der möglichen target -Werte wird in der folgenden Tabelle gezeigt:
Z IEL W ERT B ET RIF F T

assembly Gesamte Assembly

module Aktuelle Assemblymodul

field Feld in einer Klasse oder Struktur

event event

method Methode oder get - und set -Eigenschaftenaccessors

param Methodenparameter oder set -Parameter des


Eigenschaftenaccessors

property Eigenschaft

return Rückgabewert einer Methode, Eigenschaftenindexer oder


get -Eigenschaftenaccessor

type Struktur, Klasse, Schnittstelle, Enumeration oder Delegat

Geben Sie den field -Zielwert an, um ein Attribut auf das für eine automatisch implementierte Eigenschaft
erstellte Unterstützungsfeld anzuwenden.
Im folgenden Beispiel wird veranschaulicht, wie Attribute auf Assemblys und Module angewendet werden.
Weitere Informationen finden Sie unter Allgemeine Attribute (C#).

using System;
using System.Reflection;
[assembly: AssemblyTitleAttribute("Production assembly 4")]
[module: CLSCompliant(true)]

Im folgenden Beispiel wird gezeigt, wie Attribute auf Methoden, Methodenparameter und
Methodenrückgabewerte in C# angewendet werden.

// default: applies to method


[ValidatedContract]
int Method1() { return 0; }

// applies to method
[method: ValidatedContract]
int Method2() { return 0; }

// applies to parameter
int Method3([ValidatedContract] string contract) { return 0; }

// applies to return value


[return: ValidatedContract]
int Method4() { return 0; }
NOTE
Unabhängig von den Zielen, auf denen ValidatedContract definiert wird, um gültig zu sein, muss das Ziel return
angegeben werden, selbst wenn ValidatedContract nur für die Rückgabewerte definiert wurde. Das heißt, der Compiler
verwendet keine AttributeUsage -Informationen zum Auflösen von mehrdeutigen Attributzielen. Weitere Informationen
finden Sie unter AttributeUsage (C#).

Häufige Verwendungsmöglichkeiten für Attribute


Die folgende Liste enthält einige häufige Verwendungsmöglichkeiten von Attributen im Code:
Kennzeichnen von Methoden mit dem WebMethod -Attribut in Webdiensten, um anzugeben, dass die Methode
über das SOAP-Protokoll aufrufbar sein sollte. Weitere Informationen finden Sie unter WebMethodAttribute.
Beschreiben, wie Methodenparameter bei der Interaktion mit systemeigenem Code gemarshallt werden
sollen. Weitere Informationen finden Sie unter MarshalAsAttribute.
Beschreiben der COM-Eigenschaften für Klassen, Methoden und Schnittstellen.
Aufrufen von nicht verwaltetem Code mithilfe der Klasse DllImportAttribute.
Beschreiben der Assembly im Hinblick auf Titel, Version, Beschreibung oder Marke.
Beschreiben, welche Member einer Klasse zur Verbesserung der Dauerhaftigkeit serialisiert werden müssen.
Beschreiben der Zuordnung zwischen Klassenmembern und XML-Knoten für die XML-Serialisierung.
Beschreiben der Sicherheitsanforderungen für Methoden.
Angeben von Eigenschaften zum Erzwingen der Sicherheit.
Steuern der vom JIT-Compiler (Just-In-Time-Compiler) ausgeführten Optimierungen, damit der Code
weiterhin problemlos debuggt werden kann.
Abrufen von Informationen zum Aufrufer einer Methode.

Verwandte Abschnitte
Weitere Informationen finden Sie unter:
Erstellen benutzerdefinierter Attribute (C#)
Accessing Attributes by Using Reflection (C#) (Zugriff auf Attribute mit Reflektion (C#))
Erstellen einer Union in C/C++ mit Attributen (C#)
Allgemeine Attribute (C#)
Aufruferinformationen (C#)

Siehe auch
C#-Programmierhandbuch
Reflektion (C#)
Attribute
Verwenden von Attributen in C#
Erstellen benutzerdefinierter Attribute (C#)
04.11.2021 • 2 minutes to read

Sie können eigene benutzerdefinierte Attribute erstellen, indem Sie eine Attributklasse definieren. Dies ist eine
Klasse, die direkt oder indirekt von Attribute abgeleitet wird, was es einfach macht, schnell Attributdefinitionen
in Metadaten zu identifizieren. Angenommen, Sie möchten Typen mit dem Namen des Programmierers
markieren, der den Typ geschrieben hat. Sie definieren möglicherweise eine benutzerdefinierte Author -
Attributklasse:

[System.AttributeUsage(System.AttributeTargets.Class |
System.AttributeTargets.Struct)
]
public class AuthorAttribute : System.Attribute
{
private string name;
public double version;

public AuthorAttribute(string name)


{
this.name = name;
version = 1.0;
}
}

Der Klassenname AuthorAttribute setzt sich aus dem Attributnamen Author und dem Suffix Attribute
zusammen. Er ist von System.Attribute abgeleitet, ist also eine benutzerdefinierte Attributklasse. Die Parameter
des Konstruktors sind die Positionsparameter des benutzerdefinierten Attributs. In diesem Beispiel ist name ein
Positionsparameter. Alle öffentlichen Lese-/Schreibfelder oder -Eigenschaften werden Parameter genannt. In
diesem Fall ist version der einzige Parameter mit Namen. Beachten Sie die Verwendung des AttributeUsage -
Attributs, um das Author -Attribut ausschließlich für Klassen- und struct -Deklarationen gültig zu machen.
Sie könnten dieses neue Attribut wie folgt verwenden:

[Author("P. Ackerman", version = 1.1)]


class SampleClass
{
// P. Ackerman's code goes here...
}

AttributeUsage verfügt über einen Parameter, AllowMultiple , mit dem Sie ein benutzerdefiniertes Attribut zur
einmaligen oder mehrfachen Nutzung erstellen können. Im folgenden Codebeispiel wird ein mehrfach
verwendbares Attribut erstellt.

[System.AttributeUsage(System.AttributeTargets.Class |
System.AttributeTargets.Struct,
AllowMultiple = true) // multiuse attribute
]
public class AuthorAttribute : System.Attribute

Im folgenden Codebeispiel werden mehrere Attribute desselben Typs auf eine Klasse angewendet.
[Author("P. Ackerman", version = 1.1)]
[Author("R. Koch", version = 1.2)]
class SampleClass
{
// P. Ackerman's code goes here...
// R. Koch's code goes here...
}

Siehe auch
System.Reflection
C#-Programmierhandbuch
Verfassen von benutzerdefinierten Attributen
Reflektion (C#)
Attribute (C#)
Accessing Attributes by Using Reflection (C#) (Zugriff auf Attribute mit Reflektion (C#))
AttributeUsage (C#)
Zugriff auf Attribute mit Reflektion (C#)
04.11.2021 • 2 minutes to read

Die Tatsache, dass Sie benutzerdefinierte Attribute definieren und diese in Ihren Quellcode platzieren können,
hätte keinen Nutzen, wenn Sie diese Informationen nicht abrufen und darauf reagieren könnten. Mithilfe der
Reflektion können Sie die Informationen abrufen, die mit benutzerdefinierten Attributen definiert wurden. Die
Schlüsselmethode ist GetCustomAttributes , die ein Array von Objekten zurück gibt, die die Laufzeitäquivalente
der Quellcodeattribute sind. Diese Methode hat mehrere überladene Versionen. Weitere Informationen finden
Sie unter Attribute.
Eine Attributspezifikation wie z.B.:

[Author("P. Ackerman", version = 1.1)]


class SampleClass

ist mit Folgendem vergleichbar:

Author anonymousAuthorObject = new Author("P. Ackerman");


anonymousAuthorObject.version = 1.1;

Der Code wird allerdings erst ausgeführt, wenn SampleClass nach Attributen abgefragt wird. Wenn
GetCustomAttributes in SampleClass aufgerufen wird, führt dies zur Erstellung und Initialisierung eines Author
-Objekt wie oben aufgeführt. Wenn die Klasse andere Attribute aufweist, werden so auch andere Attributobjekte
erstellt. GetCustomAttributes gibt anschließend das Author -Objekt zurück sowie jedes andere Attributobjekt
eines Arrays. Dann können Sie dieses Array durchlaufen, anhand der Type der Arrayelemente feststellen, welche
Attribute gelten, und Informationen jedes Attributobjekts erhalten.

Beispiel
Im Folgenden sehen Sie ein vollständiges Beispiel. Ein benutzerdefiniertes Attribut wird definiert, auf mehrere
Entitäten angewendet und mithilfe der Reflektion abgerufen.

// Multiuse attribute.
[System.AttributeUsage(System.AttributeTargets.Class |
System.AttributeTargets.Struct,
AllowMultiple = true) // Multiuse attribute.
]
public class Author : System.Attribute
{
string name;
public double version;

public Author(string name)


{
this.name = name;

// Default value.
version = 1.0;
}

public string GetName()


{
return name;
}
}
}

// Class with the Author attribute.


[Author("P. Ackerman")]
public class FirstClass
{
// ...
}

// Class without the Author attribute.


public class SecondClass
{
// ...
}

// Class with multiple Author attributes.


[Author("P. Ackerman"), Author("R. Koch", version = 2.0)]
public class ThirdClass
{
// ...
}

class TestAuthorAttribute
{
static void Test()
{
PrintAuthorInfo(typeof(FirstClass));
PrintAuthorInfo(typeof(SecondClass));
PrintAuthorInfo(typeof(ThirdClass));
}

private static void PrintAuthorInfo(System.Type t)


{
System.Console.WriteLine("Author information for {0}", t);

// Using reflection.
System.Attribute[] attrs = System.Attribute.GetCustomAttributes(t); // Reflection.

// Displaying output.
foreach (System.Attribute attr in attrs)
{
if (attr is Author)
{
Author a = (Author)attr;
System.Console.WriteLine(" {0}, version {1:f}", a.GetName(), a.version);
}
}
}
}
/* Output:
Author information for FirstClass
P. Ackerman, version 1.00
Author information for SecondClass
Author information for ThirdClass
R. Koch, version 2.00
P. Ackerman, version 1.00
*/

Siehe auch
System.Reflection
Attribute
C#-Programmierhandbuch
Abrufen von Informationen aus Attributen
Reflektion (C#)
Attribute (C#)
Erstellen benutzerdefinierter Attribute (C#)
Erstellen einer Union in C/C++ mithilfe von
Attributen (C#)
04.11.2021 • 2 minutes to read

Mithilfe von Attributen können Sie anpassen, wie Strukturen im Arbeitsspeicher angeordnet werden. Sie können
z.B. das erstellen, was als eine Union in C/C++ bekannt ist, indem Sie die mit StructLayout(LayoutKind.Explicit)
- und FieldOffset -Attribute verwenden.

Beispiele
In diesem Codesegment beginnen alle Felder von TestUnion an derselben Position im Arbeitsspeicher.

// Add a using directive for System.Runtime.InteropServices.

[System.Runtime.InteropServices.StructLayout(LayoutKind.Explicit)]
struct TestUnion
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int i;

[System.Runtime.InteropServices.FieldOffset(0)]
public double d;

[System.Runtime.InteropServices.FieldOffset(0)]
public char c;

[System.Runtime.InteropServices.FieldOffset(0)]
public byte b;
}

Im Folgenden finden Sie ein weiteres Beispiel, in dem Felder an verschiedenen, explizit festgelegten Orten
beginnen.
// Add a using directive for System.Runtime.InteropServices.

[System.Runtime.InteropServices.StructLayout(LayoutKind.Explicit)]
struct TestExplicit
{
[System.Runtime.InteropServices.FieldOffset(0)]
public long lg;

[System.Runtime.InteropServices.FieldOffset(0)]
public int i1;

[System.Runtime.InteropServices.FieldOffset(0)]
public int i2;

[System.Runtime.InteropServices.FieldOffset(8)]
public double d;

[System.Runtime.InteropServices.FieldOffset(12)]
public char c;

[System.Runtime.InteropServices.FieldOffset(14)]
public byte b;
}

Die zwei Ganzzahlfelder i1 und i2 teilen die gleichen Speicheradressen wie lg . Diese Art der Kontrolle über
das Strukturlayout ist nützlich, wenn Sie Plattformaufrufe nutzen.

Siehe auch
System.Reflection
Attribute
C#-Programmierhandbuch
Attribute
Reflektion (C#)
Attribute (C#)
Erstellen benutzerdefinierter Attribute (C#)
Accessing Attributes by Using Reflection (C#) (Zugriff auf Attribute mit Reflektion (C#))
Auflistungen (C#)
04.11.2021 • 13 minutes to read

Für eine Vielzahl von Anwendungen sollten Sie Gruppen von miteinander verwandten Objekten erstellen und
verwalten. Zum Gruppieren von Objekten gibt es zwei Möglichkeiten: das Erstellen von Objektarrays und das
Erstellen von Auflistungen von Objekten.
Arrays eignen sich bestens zum Erstellen und Arbeiten mit einer festen Anzahl von Objekten mit starkem Typ.
Weitere Informationen zu Arrays finden Sie unter Arrays.
Auflistungen ermöglichen ein flexibleres Arbeiten mit Objektgruppen. Im Gegensatz zu Arrays kann sich die
Gruppe von Objekten, mit denen Sie arbeiten, in Abhängigkeit von den sich ändernden Anforderungen der
Anwendung dynamisch vergrößern bzw. verkleinern. Bei einigen Auflistungen können Sie jedem Objekt, das Sie
in die Auflistung einfügen, einen Schlüssel zuweisen, sodass das Objekt anhand des Schlüssels schnell
abgerufen werden kann.
Eine Auflistung ist eine Klasse. Daher müssen Sie eine Instanzen der Klasse deklarieren, bevor Sie dieser
Auflistung Elemente hinzufügen können.
Wenn die Auflistung Elemente eines Datentyps enthält, können Sie eine der Klassen im
System.Collections.Generic-Namespace verwenden. Eine generische Auflistung erzwingt Typsicherheit, sodass
der Auflistung kein anderer Datentyp hinzugefügt werden kann. Wenn Sie ein Element aus einer generischen
Auflistung abrufen, brauchen Sie dessen Datentyp nicht zu bestimmen oder zu konvertieren.

NOTE
Schließen Sie bei den Beispielen in diesem Thema using-Anweisungen für die System.Collections.Generic - und
System.Linq -Namespaces ein.

Inhalt
Verwenden einer einfachen Auflistung
Arten von Auflistungen
System.Collections.Generic-Klassen
System.Collections.Concurrent-Klassen
System.Collections-Klassen
Implementieren einer Auflistung von Schlüssel-Wert-Paaren
Verwenden von LINQ zum Zugriff auf eine Auflistung
Sortieren einer Auflistung
Definieren einer benutzerdefinierten Auflistung
Iteratoren

Verwenden einer einfachen Auflistung


In den Beispielen in diesem Abschnitt wird die generische Klasse List<T> verwendet, die es Ihnen ermöglicht,
mit einer stark typisierten Liste von Objekten zu arbeiten.
Im folgenden Beispiel wird eine Liste von Zeichenfolgen erstellt, und die Zeichenfolgen werden unter
Verwendung einer foreach-Anweisung durchlaufen.

// Create a list of strings.


var salmons = new List<string>();
salmons.Add("chinook");
salmons.Add("coho");
salmons.Add("pink");
salmons.Add("sockeye");

// Iterate through the list.


foreach (var salmon in salmons)
{
Console.Write(salmon + " ");
}
// Output: chinook coho pink sockeye

Wenn der Inhalt einer Auflistung im Voraus bekannt ist, können Sie einen Auflistungsinitialisierer verwenden,
um die Auflistung zu initialisieren. Weitere Informationen finden Sie unter Objekt- und Auflistungsinitialisierer.
Das folgende Beispiel entspricht dem vorherigen Beispiel, außer dass ein Auflistungsinitialisierer verwendet
wird, um der Auflistung Elemente hinzuzufügen.

// Create a list of strings by using a


// collection initializer.
var salmons = new List<string> { "chinook", "coho", "pink", "sockeye" };

// Iterate through the list.


foreach (var salmon in salmons)
{
Console.Write(salmon + " ");
}
// Output: chinook coho pink sockeye

Sie können anstelle einer for-Anweisung eine foreach -Anweisung verwenden, um eine Auflistung zu
durchlaufen. Sie erreichen dies, indem Sie durch die Indexposition auf die Auflistungselemente zugreifen. Der
Index der Elemente beginnt mit 0 und endet an der Elementanzahl minus 1.
Im folgenden Beispiel werden die Elemente einer Auflistung unter Verwendung von for anstelle von foreach
durchlaufen.

// Create a list of strings by using a


// collection initializer.
var salmons = new List<string> { "chinook", "coho", "pink", "sockeye" };

for (var index = 0; index < salmons.Count; index++)


{
Console.Write(salmons[index] + " ");
}
// Output: chinook coho pink sockeye

Im folgenden Beispiel wird ein Element aus der Auflistung entfernt, indem das zu entfernende Objekt
angegeben wird.
// Create a list of strings by using a
// collection initializer.
var salmons = new List<string> { "chinook", "coho", "pink", "sockeye" };

// Remove an element from the list by specifying


// the object.
salmons.Remove("coho");

// Iterate through the list.


foreach (var salmon in salmons)
{
Console.Write(salmon + " ");
}
// Output: chinook pink sockeye

Im folgenden Beispiel werden alle Elemente aus einer generischen Liste entfernt. Anstelle einer foreach -
Anweisung wird eine for -Anweisung verwendet, die die Elemente in absteigender Reihenfolge durchläuft. Dies
liegt daran, dass die RemoveAt-Methode dazu führt, dass Elemente nach einem entfernten Element einen
niedrigeren Indexwert haben.

var numbers = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// Remove odd numbers.


for (var index = numbers.Count - 1; index >= 0; index--)
{
if (numbers[index] % 2 == 1)
{
// Remove the element by specifying
// the zero-based index in the list.
numbers.RemoveAt(index);
}
}

// Iterate through the list.


// A lambda expression is placed in the ForEach method
// of the List(T) object.
numbers.ForEach(
number => Console.Write(number + " "));
// Output: 0 2 4 6 8

Für den Typ der Elemente in List<T> können Sie auch eine eigene Klasse definieren. Im folgenden Beispiel wird
die Galaxy -Klasse, die von List<T> verwendet wird, im Code definiert.
private static void IterateThroughList()
{
var theGalaxies = new List<Galaxy>
{
new Galaxy() { Name="Tadpole", MegaLightYears=400},
new Galaxy() { Name="Pinwheel", MegaLightYears=25},
new Galaxy() { Name="Milky Way", MegaLightYears=0},
new Galaxy() { Name="Andromeda", MegaLightYears=3}
};

foreach (Galaxy theGalaxy in theGalaxies)


{
Console.WriteLine(theGalaxy.Name + " " + theGalaxy.MegaLightYears);
}

// Output:
// Tadpole 400
// Pinwheel 25
// Milky Way 0
// Andromeda 3
}

public class Galaxy


{
public string Name { get; set; }
public int MegaLightYears { get; set; }
}

Arten von Auflistungen


Viele häufig verwendete Sammlungen werden von .NET bereitgestellt. Jeder Auflistungstyp ist für einen
speziellen Zweck ausgelegt.
Einige der häufig verwendeten Auflistungsklassen werden in diesem Abschnitt beschrieben:
System.Collections.Generic-Klassen
System.Collections.Concurrent-Klassen
System.Collections-Klassen

System.Collections.Generic-Klassen
Zum Erstellen einer generischen Auflistung verwenden Sie eine der Klassen im System.Collections.Generic-
Namespace. Eine generische Auflistung ist sinnvoll, wenn jedes Element der Auflistung zum gleichen Datentyp
gehört. Eine generische Auflistung erzwingt eine starke Typisierung, da ihr nur Elemente des gewünschten
Datentyps hinzugefügt werden können.
In der folgenden Tabelle werden einige der häufig verwendeten Klassen des System.Collections.Generic-
Namespace aufgelistet:

K L A SSE B ESC H REIB UN G

Dictionary<TKey,TValue> Stellt eine Auflistung von Schlüssel-Wert-Paaren dar, deren


Reihenfolge anhand des Schlüssels bestimmt wird.

List<T> Stellt eine Liste von Objekten dar, auf die über einen Index
zugegriffen werden kann. Stellt Methoden zum Durchsuchen,
Sortieren und Bearbeiten von Listen bereit.
K L A SSE B ESC H REIB UN G

Queue<T> Stellt eine FIFO-Auflistung (First In, First Out) von Objekten
dar.

SortedList<TKey,TValue> Stellt eine Auflistung von Schlüssel-Wert-Paaren dar, die auf


Grundlage der zugeordneten IComparer<T>-
Implementierung nach den Schlüsseln sortiert sind.

Stack<T> Stellt eine LIFO-Auflistung (Last In, First Out) von Objekten
dar.

Weitere Informationen finden Sie unter Häufig verwendete Auflistungstypen, Auswählen einer Auflistungsklasse
und System.Collections.Generic.

System.Collections.Concurrent-Klassen
In .NET Framework 4 oder höher stellen die Sammlungen im Namespace System.Collections.Concurrent
effiziente, threadsichere Vorgänge für den Zugriff auf Sammlungselemente über mehrere Threads bereit.
Die Klassen im System.Collections.Concurrent-Namespace sollten anstelle von entsprechenden Typen in
System.Collections.Generic- und System.Collections-Namespaces verwendet werden, wenn mehrere Threads
gleichzeitig auf die Auflistung zugreifen. Weitere Informationen finden Sie unter Threadsichere Auflistungen und
System.Collections.Concurrent.
Einige der in die System.Collections.Concurrent-Namespaces aufgenommenen Klassen sind
BlockingCollection<T>, ConcurrentDictionary<TKey,TValue>, ConcurrentQueue<T> und ConcurrentStack<T>.

System.Collections-Klassen
Bei den Klassen im System.Collections-Namespace werden Elemente nicht als speziell typisierte Objekte,
sondern als Objekte vom Typ Object gespeichert.
Sofern möglich sollten die generischen Auflistungen im System.Collections.Generic-Namespace oder
System.Collections.Concurrent-Namespace anstelle der älteren Typen im System.Collections -Namespace
verwendet werden.
In der folgenden Tabelle werden einige der häufig verwendeten Klassen im System.Collections -Namespace
aufgelistet:

K L A SSE B ESC H REIB UN G

ArrayList Stellt ein Array von Objekten dar, das je nach Bedarf
dynamisch vergrößert wird.

Hashtable Stellt eine Auflistung von Schlüssel-Wert-Paaren dar, die auf


Grundlage des Hashcodes des Schlüssels geordnet sind.

Queue Stellt eine FIFO-Auflistung (First In, First Out) von Objekten
dar.

Stack Stellt eine LIFO-Auflistung (Last In, First Out) von Objekten
dar.

Der System.Collections.Specialized-Namespace bietet spezialisierte und stark typisierte Auflistungsklassen,


beispielsweise für Zeichenfolgenauflistungen sowie für Wörterbücher mit verketteten Listen und
Hybridwörterbücher.
Implementieren einer Auflistung von Schlüssel/Wert-Paaren
Die generische Auflistung Dictionary<TKey,TValue> ermöglicht es Ihnen, unter Verwendung des Schlüssels der
einzelnen Elemente auf die Elemente einer Auflistung zuzugreifen. Jede Hinzufügung zum Wörterbuch besteht
aus einem Wert und dem zugeordneten Schlüssel. Ein Wert kann anhand des zugehörigen Schlüssels schnell
abgerufen werden, da die Dictionary -Klasse in Form einer Hashtabelle implementiert ist.
Das folgende Beispiel erstellt eine Dictionary -Auflistung und durchläuft das Wörterbuch unter Verwendung
einer foreach -Anweisung.

private static void IterateThruDictionary()


{
Dictionary<string, Element> elements = BuildDictionary();

foreach (KeyValuePair<string, Element> kvp in elements)


{
Element theElement = kvp.Value;

Console.WriteLine("key: " + kvp.Key);


Console.WriteLine("values: " + theElement.Symbol + " " +
theElement.Name + " " + theElement.AtomicNumber);
}
}

private static Dictionary<string, Element> BuildDictionary()


{
var elements = new Dictionary<string, Element>();

AddToDictionary(elements, "K", "Potassium", 19);


AddToDictionary(elements, "Ca", "Calcium", 20);
AddToDictionary(elements, "Sc", "Scandium", 21);
AddToDictionary(elements, "Ti", "Titanium", 22);

return elements;
}

private static void AddToDictionary(Dictionary<string, Element> elements,


string symbol, string name, int atomicNumber)
{
Element theElement = new Element();

theElement.Symbol = symbol;
theElement.Name = name;
theElement.AtomicNumber = atomicNumber;

elements.Add(key: theElement.Symbol, value: theElement);


}

public class Element


{
public string Symbol { get; set; }
public string Name { get; set; }
public int AtomicNumber { get; set; }
}

Wenn stattdessen ein Auflistungsinitialisierer zum Erstellen der Dictionary -Auflistung verwendet werden soll,
können Sie die BuildDictionary - und AddToDictionary -Methoden durch die folgende Methode ersetzen.
private static Dictionary<string, Element> BuildDictionary2()
{
return new Dictionary<string, Element>
{
{"K",
new Element() { Symbol="K", Name="Potassium", AtomicNumber=19}},
{"Ca",
new Element() { Symbol="Ca", Name="Calcium", AtomicNumber=20}},
{"Sc",
new Element() { Symbol="Sc", Name="Scandium", AtomicNumber=21}},
{"Ti",
new Element() { Symbol="Ti", Name="Titanium", AtomicNumber=22}}
};
}

Im folgenden Beispiel werden die ContainsKey-Methode und die Item[]-Eigenschaft von Dictionary verwendet,
um anhand des Schlüssels schnell nach einem Element zu suchen. Die Item -Eigenschaft ermöglicht den Zugriff
auf ein Element in der elements -Auflistung unter Verwendung des elements[symbol] -Codes in C#.

private static void FindInDictionary(string symbol)


{
Dictionary<string, Element> elements = BuildDictionary();

if (elements.ContainsKey(symbol) == false)
{
Console.WriteLine(symbol + " not found");
}
else
{
Element theElement = elements[symbol];
Console.WriteLine("found: " + theElement.Name);
}
}

Im folgenden Beispiel wird stattdessen die TryGetValue-Methode verwendet, um anhand des Schlüssels schnell
nach einem Element zu suchen.

private static void FindInDictionary2(string symbol)


{
Dictionary<string, Element> elements = BuildDictionary();

Element theElement = null;


if (elements.TryGetValue(symbol, out theElement) == false)
Console.WriteLine(symbol + " not found");
else
Console.WriteLine("found: " + theElement.Name);
}

Verwenden von LINQ zum Zugriff auf eine Auflistung


LINQ (Language-Integrated Query) kann verwendet werden, um auf Auflistungen zuzugreifen. LINQ-Abfragen
stellen Filter-, Sortier- und Gruppierungsfunktionen bereit. Weitere Informationen finden Sie unter Erste Schritte
mit LINQ in C#.
Im folgenden Beispiel wird eine LINQ-Abfrage für eine generische List ausgeführt. Die LINQ-Abfrage gibt eine
andere Auflistung zurück, die die Ergebnisse enthält.
private static void ShowLINQ()
{
List<Element> elements = BuildList();

// LINQ Query.
var subset = from theElement in elements
where theElement.AtomicNumber < 22
orderby theElement.Name
select theElement;

foreach (Element theElement in subset)


{
Console.WriteLine(theElement.Name + " " + theElement.AtomicNumber);
}

// Output:
// Calcium 20
// Potassium 19
// Scandium 21
}

private static List<Element> BuildList()


{
return new List<Element>
{
{ new Element() { Symbol="K", Name="Potassium", AtomicNumber=19}},
{ new Element() { Symbol="Ca", Name="Calcium", AtomicNumber=20}},
{ new Element() { Symbol="Sc", Name="Scandium", AtomicNumber=21}},
{ new Element() { Symbol="Ti", Name="Titanium", AtomicNumber=22}}
};
}

public class Element


{
public string Symbol { get; set; }
public string Name { get; set; }
public int AtomicNumber { get; set; }
}

Sortieren einer Auflistung


Das folgende Beispiel zeigt ein Verfahren zum Sortieren einer Auflistung. In dem Beispiel werden Instanzen der
Car -Klasse sortiert, die in List<T> gespeichert sind. Die Car -Klasse implementiert die IComparable<T>-
Schnittstelle, die die Implementierung der CompareTo-Methode erfordert.
Jeder Aufruf der CompareTo-Methode führt einen einzelnen Vergleich aus, der für die Sortierung verwendet
wird. Vom Benutzer erstellter Code in der CompareTo -Methode gibt einen Wert für jeden Vergleich des aktuellen
Objekts mit einem anderen Objekt zurück. Der zurückgegebene Wert ist kleiner als Null, wenn das aktuelle
Objekt kleiner ist als das andere Objekt, größer als Null, wenn das aktuelle Objekt größer als das andere Objekt
ist und Null, wenn beide Objekt gleich groß sind. Dies ermöglicht es Ihnen, in dem Code die Kriterien für größer
als, kleiner als und gleich zu definieren.
In der ListCars -Methode sortiert die cars.Sort() -Anweisung die Liste. Dieser Aufruf der Sort-Methode von
List<T> führt dazu, dass die CompareTo -Methode für die Car -Objekte in der List automatisch aufgerufen
wird.

private static void ListCars()


{
var cars = new List<Car>
{
{ new Car() { Name = "car1", Color = "blue", Speed = 20}},
{ new Car() { Name = "car2", Color = "red", Speed = 50}},
{ new Car() { Name = "car2", Color = "red", Speed = 50}},
{ new Car() { Name = "car3", Color = "green", Speed = 10}},
{ new Car() { Name = "car4", Color = "blue", Speed = 50}},
{ new Car() { Name = "car5", Color = "blue", Speed = 30}},
{ new Car() { Name = "car6", Color = "red", Speed = 60}},
{ new Car() { Name = "car7", Color = "green", Speed = 50}}
};

// Sort the cars by color alphabetically, and then by speed


// in descending order.
cars.Sort();

// View all of the cars.


foreach (Car thisCar in cars)
{
Console.Write(thisCar.Color.PadRight(5) + " ");
Console.Write(thisCar.Speed.ToString() + " ");
Console.Write(thisCar.Name);
Console.WriteLine();
}

// Output:
// blue 50 car4
// blue 30 car5
// blue 20 car1
// green 50 car7
// green 10 car3
// red 60 car6
// red 50 car2
}

public class Car : IComparable<Car>


{
public string Name { get; set; }
public int Speed { get; set; }
public string Color { get; set; }

public int CompareTo(Car other)


{
// A call to this method makes a single comparison that is
// used for sorting.

// Determine the relative order of the objects being compared.


// Sort by color alphabetically, and then by speed in
// descending order.

// Compare the colors.


int compare;
compare = String.Compare(this.Color, other.Color, true);

// If the colors are the same, compare the speeds.


if (compare == 0)
{
compare = this.Speed.CompareTo(other.Speed);

// Use descending order for speed.


compare = -compare;
}

return compare;
}
}

Definieren einer benutzerdefinierten Auflistung


Sie können eine Auflistung definieren, indem Sie die IEnumerable<T>- oder IEnumerable-Schnittstelle
implementieren.
Sie können zwar eine benutzerdefinierte Sammlung definieren, in der Regel ist es aber besser, die in .NET
enthaltenen Sammlungen zu verwenden. Diese werden weiter oben unter Arten von Sammlungen beschrieben.
Im folgenden Beispiel wird die benutzerdefinierte Auflistungsklasse AllColors definiert. Diese Klasse
implementiert die IEnumerable-Schnittstelle, die die Implementierung der GetEnumerator-Methode erfordert.
Die GetEnumerator -Methode gibt eine Instanz der ColorEnumerator -Klasse zurück. ColorEnumerator
implementiert die IEnumerator-Schnittstelle, die die Implementierung der Current-Eigenschaft, der MoveNext-
Methode und der Reset-Methode erfordert.

private static void ListColors()


{
var colors = new AllColors();

foreach (Color theColor in colors)


{
Console.Write(theColor.Name + " ");
}
Console.WriteLine();
// Output: red blue green
}

// Collection class.
public class AllColors : System.Collections.IEnumerable
{
Color[] _colors =
{
new Color() { Name = "red" },
new Color() { Name = "blue" },
new Color() { Name = "green" }
};

public System.Collections.IEnumerator GetEnumerator()


{
return new ColorEnumerator(_colors);

// Instead of creating a custom enumerator, you could


// use the GetEnumerator of the array.
//return _colors.GetEnumerator();
}

// Custom enumerator.
private class ColorEnumerator : System.Collections.IEnumerator
{
private Color[] _colors;
private int _position = -1;

public ColorEnumerator(Color[] colors)


{
_colors = colors;
}

object System.Collections.IEnumerator.Current
{
get
{
return _colors[_position];
}
}

bool System.Collections.IEnumerator.MoveNext()
{
_position++;
return (_position < _colors.Length);
}
}

void System.Collections.IEnumerator.Reset()
{
_position = -1;
}
}
}

// Element class.
public class Color
{
public string Name { get; set; }
}

Iterators
Ein Iterator wird verwendet, um eine benutzerdefinierte Iteration durch eine Auflistung auszuführen. Ein Iterator
kann eine Methode oder ein get -Accessor sein. Ein Iterator verwendet eine yield return-Anweisung, um jedes
Element der Auflistung separat zurückzugeben.
Sie rufen einen Iterator mithilfe einer foreach-Anweisung auf. Jede Iteration der foreach -Schleife ruft den
Iterator auf. Wenn eine yield return -Anweisung im Iterator erreicht ist, wird ein Ausdruck zurückgegeben, und
die aktuelle Position im Code wird beibehalten. Wenn der Iterator das nächste Mal aufgerufen wird, wird die
Ausführung von dieser Position neu gestartet.
Weitere Informationen finden Sie unter Iteratoren (C#).
Im folgenden Beispiel wird eine Iteratormethode verwendet. Die Iteratormethode verfügt über eine
yield return -Anweisung, die sich innerhalb einer for -Schleife befindet. In der ListEvenNumbers -Methode
erstellt jede Iteration des foreach -Anweisungstexts einen Aufruf der Iteratormethode, der zur nächsten
yield return -Anweisung übergeht.

private static void ListEvenNumbers()


{
foreach (int number in EvenSequence(5, 18))
{
Console.Write(number.ToString() + " ");
}
Console.WriteLine();
// Output: 6 8 10 12 14 16 18
}

private static IEnumerable<int> EvenSequence(


int firstNumber, int lastNumber)
{
// Yield even numbers in the range.
for (var number = firstNumber; number <= lastNumber; number++)
{
if (number % 2 == 0)
{
yield return number;
}
}
}

Siehe auch
Objekt- und Auflistungsinitialisierer
Programmierkonzepte (C#)
Option Strict-Anweisung
LINQ to Objects (C#)
Parallel LINQ (PLINQ) (Paralleles LINQ (PLINQ))
Sammlungen und Datenstrukturen
Auswählen einer Auflistungsklasse
Vergleiche und Sortierungen innerhalb von Auflistungen
Verwenden von generischen Auflistungen
Iterationsanweisungen
Kovarianz und Kontravarianz (C#)
04.11.2021 • 3 minutes to read

Kovarianz und Kontravarianz in C# ermöglichen die implizite Referenzkonvertierung für Arraytypen,


Delegattypen und generische Typargumente. Die Kovarianz behält die Zuweisungskompatibilität bei und die
Kontravarianz kehrt sie um.
Der folgende Code veranschaulicht den Unterschied zwischen Zuweisungskompatibilität, Kovarianz und
Kontravarianz.

// Assignment compatibility.
string str = "test";
// An object of a more derived type is assigned to an object of a less derived type.
object obj = str;

// Covariance.
IEnumerable<string> strings = new List<string>();
// An object that is instantiated with a more derived type argument
// is assigned to an object instantiated with a less derived type argument.
// Assignment compatibility is preserved.
IEnumerable<object> objects = strings;

// Contravariance.
// Assume that the following method is in the class:
// static void SetObject(object o) { }
Action<object> actObject = SetObject;
// An object that is instantiated with a less derived type argument
// is assigned to an object instantiated with a more derived type argument.
// Assignment compatibility is reversed.
Action<string> actString = actObject;

Die Kovarianz für Arrays ermöglicht die implizite Konvertierung eines Arrays mit einem stärker abgeleiteten Typ
zu einem Array mit einem schwächer abgeleiteten Typ. Dieser Vorgang ist jedoch nicht typsicher, wie im
folgenden Codebeispiel gezeigt wird.

object[] array = new String[10];


// The following statement produces a run-time exception.
// array[0] = 10;

Die Unterstützung von Kovarianz und Kontravarianz für Methodengruppen ermöglicht es, Methodensignaturen
mit Delegattypen abzugleichen. Das bedeutet, dass Sie Delegaten nicht nur Methoden mit übereinstimmenden
Signaturen zuweisen können, sondern auch Methoden, die mehrere abgeleitete Typen zurückgeben (Kovarianz)
oder die Parameter akzeptieren, die über weniger abgeleitete Typen verfügen, als durch den Delegattyp
angegeben wurde (Kontravarianz). Weitere Informationen finden Sie unter Variance in Delegates (C#) (Varianz in
Delegaten (C#)) und Using Variance in Delegates (C#) (Verwenden von Varianz in Delegaten (C#)).
Im folgenden Codebeispiel wird die Unterstützung von Methodengruppen durch Kovarianz und Kontravarianz
veranschaulicht.
static object GetObject() { return null; }
static void SetObject(object obj) { }

static string GetString() { return ""; }


static void SetString(string str) { }

static void Test()


{
// Covariance. A delegate specifies a return type as object,
// but you can assign a method that returns a string.
Func<object> del = GetString;

// Contravariance. A delegate specifies a parameter type as string,


// but you can assign a method that takes an object.
Action<string> del2 = SetObject;
}

In .NET Framework 4 und höheren Versionen unterstützt C# die Kovarianz und Kontravarianz in generischen
Schnittstellen und Delegaten und ermöglicht die implizite Konvertierung von generischen Typparametern.
Weitere Informationen finden Sie unter Variance in Generic Interfaces (C#) (Varianz in generischen Schnittstellen
(C#)) und Variance in Delegates (C#) (Varianz in Delegaten (C#)).
Das folgende Codebeispiel zeigt die implizite Verweiskonvertierung bei generischen Schnittstellen.

IEnumerable<String> strings = new List<String>();


IEnumerable<Object> objects = strings;

Generische Schnittstellen oder Delegate werden als variant bezeichnet, wenn deren generische Parameter als
kovariant oder kontravariant deklariert sind. C# ermöglicht es Ihnen, eigene variante Schnittstellen oder
Delegate zu erstellen. Weitere Informationen finden Sie unter Creating Variant Generic Interfaces (C#) (Erstellen
von varianten generischen Schnittstellen (C#)) und Variance in Delegates (C#) (Varianz in Delegaten (C#)).

Verwandte Themen
T IT EL B ESC H REIB UN G

Varianz in generischen Schnittstellen Erläutert die Ko- und Kontravarianz in generischen


Schnittstellen und enthält eine Liste der verschiedenen
generischen Schnittstellen in .NET

Creating Variant Generic Interfaces (C#) (Erstellen von Zeigt, wie benutzerdefinierte variante Schnittstellen erstellt
varianten generischen Schnittstellen (C#)) werden.

Using Variance in Interfaces for Generic Collections (C#) Zeigt, wie die Unterstützung durch Kovarianz und
(Verwenden von Varianz in Schnittstellen für generische Kontravarianz bei IEnumerable<T>- und IComparable<T>-
Auflistungen (C#)) Schnittstellen bei der Wiederverwendung Ihres Codes helfen
kann.

Variance in Delegates (C#) (Varianz bei Delegaten (C#)) Erläutert die Ko- und Kontravarianz in generischen und nicht
generischen Delegaten und enthält eine Liste der
unterschiedlichen generischen Delegate in .NET

Using Variance in Delegates (C#) (Verwenden von Varianz in Zeigt, wie die Unterstützung durch Kovarianz und
Delegaten (C#)) Kontravarianz in nicht generischen Delegaten verwendet
werden kann, um Methodensignaturen mit Delegattypen
abzugleichen.
T IT EL B ESC H REIB UN G

Verwenden von Varianz für die generischen Delegaten Func Zeigt, wie die Unterstützung durch Kovarianz und
und Action (C#) Kontravarianz bei Func - und Action -Delegaten bei der
Wiederverwendung Ihres Codes helfen kann.
Varianz in generischen Schnittstellen (C#)
04.11.2021 • 2 minutes to read

In .NET Framework 4 wurde die Varianzunterstützung für mehrere vorhandene generische Schnittstellen
eingeführt. Die Varianzunterstützung lässt eine implizite Konvertierung von Klassen zu, die diese Schnittstellen
implementieren.
Ab .NET Framework 4 sind die folgenden Schnittstellen Varianten:
IEnumerable<T> (T ist kovariant)
IEnumerator<T> (T ist kovariant)
IQueryable<T> (T ist kovariant)
IGrouping<TKey,TElement> ( TKey und TElement sind kovariant)
IComparer<T> (T ist kontravariant)
IEqualityComparer<T> (T ist kontravariant)
IComparable<T> (T ist kontravariant)
Ab .NET Framework 4.5 sind die folgenden Schnittstellen Varianten:
IReadOnlyList<T> (T ist kovariant)
IReadOnlyCollection<T> (T ist kovariant)
Kovarianz ermöglicht einer Methode, stärker abgeleitete Rückgabetypen zu verwenden, als durch die
generischen Typparameter der Schnittstelle definiert sind. Betrachten Sie diese generischen Schnittstellen zur
Veranschaulichung der Kovarianzfunktionen: IEnumerable<Object> und IEnumerable<String> . Die Schnittstelle
IEnumerable<Object> wird nicht von der Schnittstelle IEnumerable<String> geerbt. Allerdings erbt der Typ
String den Typ Object . In einigen Fällen können Sie vielleicht auch die Objekte dieser Schnittstellen einander
zuweisen. Dies wird im folgenden Codebeispiel gezeigt.

IEnumerable<String> strings = new List<String>();


IEnumerable<Object> objects = strings;

In früheren Versionen des.NET-Frameworks verursacht dieser Code in Visual Basic einen Kompilierungsfehler in
C# und, wenn Option Strict aktiviert ist. Jetzt können Sie aber strings anstelle von objects verwenden, wie
im vorherigen Beispiel gezeigt wurde, da die IEnumerable<T>-Schnittstelle kovariant ist.
Kontravarianz ermöglicht einer Methode, Argumenttypen zu verwenden, die weniger stark abgeleitet sind als
durch die generischen Parameter der Schnittstelle angegeben. Nehmen Sie zur Veranschaulichung der
Kontravarianz an, dass Sie eine BaseComparer -Klasse zum Vergleich von Instanzen der BaseClass -Klasse erstellt
haben. Die BaseComparer -Klasse implementiert die IEqualityComparer<BaseClass> -Schnittstelle. Da die
Schnittstelle IEqualityComparer<T> jetzt kontravariant ist, können Sie BaseComparer verwenden, um Instanzen
von Klassen zu vergleichen, die die Klasse BaseClass erben. Dies wird im folgenden Codebeispiel gezeigt.
// Simple hierarchy of classes.
class BaseClass { }
class DerivedClass : BaseClass { }

// Comparer class.
class BaseComparer : IEqualityComparer<BaseClass>
{
public int GetHashCode(BaseClass baseInstance)
{
return baseInstance.GetHashCode();
}
public bool Equals(BaseClass x, BaseClass y)
{
return x == y;
}
}
class Program
{
static void Test()
{
IEqualityComparer<BaseClass> baseComparer = new BaseComparer();

// Implicit conversion of IEqualityComparer<BaseClass> to


// IEqualityComparer<DerivedClass>.
IEqualityComparer<DerivedClass> childComparer = baseComparer;
}
}

Weitere Beispiele finden Sie unter Using Variance in Interfaces for Generic Collections (C#) (Verwenden von
Varianz in Schnittstellen für generische Auflistungen (C#)).
Varianz in generischen Typparametern wird nur für Referenztypen unterstützt. Werttypen unterstützen keine
Varianz. Beispielweise kann IEnumerable<int> nicht implizit in IEnumerable<object> konvertiert werden, da
Ganzzahlen durch Werttypen dargestellt werden.

IEnumerable<int> integers = new List<int>();


// The following statement generates a compiler error,
// because int is a value type.
// IEnumerable<Object> objects = integers;

Es ist auch wichtig zu beachten, dass Klassen, die variante Schnittstellen implementieren, trotzdem noch
invariant sind. Obwohl List<T> beispielsweise die kovariante Schnittstelle IEnumerable<T> implementiert,
können Sie List<String> implizit in List<Object> konvertieren. Dies wird im folgenden Codebeispiel
veranschaulicht.

// The following line generates a compiler error


// because classes are invariant.
// List<Object> list = new List<String>();

// You can use the interface object instead.


IEnumerable<Object> listObjects = new List<String>();

Siehe auch
Using Variance in Interfaces for Generic Collections (C#) (Verwenden von Varianz in Schnittstellen für
generische Auflistungen (C#))
Creating Variant Generic Interfaces (C#) (Erstellen von varianten generischen Schnittstellen (C#))
Generische Schnittstellen
Variance in Delegates (C#) (Varianz bei Delegaten (C#))
Erstellen varianter generischer Schnittstellen (C#)
04.11.2021 • 4 minutes to read

Sie können generische Typparameter in Schnittstellen als Kovariante oder als Kontravariante deklarieren.
Kovarianz ermöglicht Schnittstellenmethoden, stärker abgeleitete Rückgabetypen zu verwenden, als durch die
generischen Typparameter definiert. Kontravarianz ermöglicht Schnittstellenmethoden, Argumenttypen zu
verwenden, die weniger stark abgeleitet sind, als durch die generischen Parameter angegeben. Eine generische
Schnittstelle mit ko- oder kontravarianten generischen Typparametern wird als variant bezeichnet.

NOTE
In .NET Framework 4 wurde die Varianzunterstützung für mehrere vorhandene generische Schnittstellen eingeführt. Die
Liste der varianten Schnittstellen in .NET finden Sie unter Varianz in generischen Schnittstellen (C#).

Deklarieren von varianten generischen Schnittstellen


Sie können variante generische Schnittstellen deklarieren, indem Sie die in - und out -Schlüsselwörter für
generische Typparameter verwenden.

IMPORTANT
ref -, in - und out -Parameter in C# können nicht variant sein. Auch Werttypen unterstützen keine Varianz.

Sie können einen generischen Typparameter mithilfe des Schlüsselworts out als Kovariante deklarieren. Der
kovariante Typ muss die folgenden Bedingungen erfüllen:
Der Typ wird nur als Rückgabetyp von Schnittstellenmethoden verwendet, und nicht als Typ von
Methodenargumenten. Dies wird im folgenden Beispiel veranschaulicht, in dem der Typ R als Kovariante
deklariert wird.

interface ICovariant<out R>


{
R GetSomething();
// The following statement generates a compiler error.
// void SetSomething(R sampleArg);

Es gibt allerdings eine Ausnahme zu dieser Regel. Wenn Sie einen kontravarianten generischen Delegaten
als Methodenparameter angegeben haben, können Sie den Typ als generischen Typparameter für den
Delegaten verwenden. Dies wird im folgenden Beispiel von Typ R veranschaulicht. Weitere
Informationen finden Sie unter Varianz in Delegaten (C#) und Verwenden von Varianz für die generischen
Delegaten Func und Action (C#).

interface ICovariant<out R>


{
void DoSomething(Action<R> callback);
}

Der Typ wird nicht als generische Einschränkung für die Schnittstellenmethoden verwendet. Der folgende
Code veranschaulicht dies.

interface ICovariant<out R>


{
// The following statement generates a compiler error
// because you can use only contravariant or invariant types
// in generic constraints.
// void DoSomething<T>() where T : R;
}

Sie können einen generischen Typparameter mithilfe des Schlüsselworts in als Kovariante deklarieren. Der
kontravariante Typ kann nur als Typ von Methodenargumenten und nicht von Schnittstellenmethoden
verwendet werden. Der kontravariante Typ kann auch für generische Einschränkungen verwendet werden. Der
folgende Code zeigt, wie eine kontravariante Schnittstelle deklariert und eine generische Einschränkung für eine
ihrer Methoden verwendet wird.

interface IContravariant<in A>


{
void SetSomething(A sampleArg);
void DoSomething<T>() where T : A;
// The following statement generates a compiler error.
// A GetSomething();
}

Bei unterschiedlichen Typparametern ist jedoch auch die Verwendung verschiedener Ko- und Kontravarianzen in
der gleichen Schnittstelle möglich, wie im folgenden Codebeispiel veranschaulicht wird.

interface IVariant<out R, in A>


{
R GetSomething();
void SetSomething(A sampleArg);
R GetSetSomethings(A sampleArg);
}

Implementieren von varianten generischen Schnittstellen


Sie können variante generische Schnittstellen in Klassen implementieren, indem Sie die gleiche Syntax
verwenden, die für invariante Schnittstellen verwendet wird. Im folgenden Codebeispiel wird veranschaulicht,
wie eine kovariante Schnittstelle in einer generischen Klasse implementiert wird.

interface ICovariant<out R>


{
R GetSomething();
}
class SampleImplementation<R> : ICovariant<R>
{
public R GetSomething()
{
// Some code.
return default(R);
}
}

Klassen, die variante Schnittstellen implementieren, sind nicht variant. Betrachten Sie hierzu den folgenden
Beispielcode:
// The interface is covariant.
ICovariant<Button> ibutton = new SampleImplementation<Button>();
ICovariant<Object> iobj = ibutton;

// The class is invariant.


SampleImplementation<Button> button = new SampleImplementation<Button>();
// The following statement generates a compiler error
// because classes are invariant.
// SampleImplementation<Object> obj = button;

Erweitern von varianten generischen Schnittstellen


Wenn Sie eine variante generische Schnittstelle erweitern, müssen Sie mit den in - und out -Schlüsselwörtern
explizit angeben, ob Varianz von der abgeleiteten Schnittstelle unterstützt wird. Der Compiler leitet eine Varianz
nicht von der Schnittstelle ab, die erweitert wird. Beachten Sie z.B. die folgenden Schnittstellen.

interface ICovariant<out T> { }


interface IInvariant<T> : ICovariant<T> { }
interface IExtCovariant<out T> : ICovariant<T> { }

In der IInvariant<T>-Schnittstelle ist der generische Typparameter T nicht variant, während in


IExtCovariant<out T> der Typparameter kovariant ist, obwohl beide Schnittstellen die gleiche Schnittstelle
erweitern. Die gleiche Regel gilt für kontravariante generische Typparameter.
Sie können eine Schnittstelle erstellen, die sowohl die Schnittstelle mit dem kovarianten generischen
Typparameter T als auch die Schnittstelle mit dem kontravarianten Typparameter implementiert, wenn der
generische Typparameter T in der erweiternden Schnittstelle nicht variant ist. Dies wird im folgenden
Codebeispiel veranschaulicht.

interface ICovariant<out T> { }


interface IContravariant<in T> { }
interface IInvariant<T> : ICovariant<T>, IContravariant<T> { }

Wenn der generische Typparameter T jedoch in einer Schnittstelle als Kovariante deklariert wird, können Sie
ihn nicht in der erweiternden Schnittstelle als Kontravariante deklarieren oder umgekehrt. Dies wird im
folgenden Codebeispiel veranschaulicht.

interface ICovariant<out T> { }


// The following statement generates a compiler error.
// interface ICoContraVariant<in T> : ICovariant<T> { }

Vermeiden von Mehrdeutigkeiten


Wenn Sie variante generische Schnittstellen implementieren, kann Varianz manchmal zu Mehrdeutigkeit führen.
Diese Mehrdeutigkeit sollte vermieden werden.
Wenn Sie z.B. die gleiche variante generische Schnittstelle explizit mit unterschiedlichen generischen
Typparametern in einer Klasse implementieren, kann dies zu Mehrdeutigkeit führen. Der Compiler erzeugt in
diesem Fall keinen Fehler. Es wird jedoch nicht angegeben, welche Schnittstellenimplementierung zur Laufzeit
ausgewählt wird. Diese Mehrdeutigkeit kann zu schwer erkennbaren Fehlern im Code führen. Betrachten Sie
folgendes Codebeispiel.
// Simple class hierarchy.
class Animal { }
class Cat : Animal { }
class Dog : Animal { }

// This class introduces ambiguity


// because IEnumerable<out T> is covariant.
class Pets : IEnumerable<Cat>, IEnumerable<Dog>
{
IEnumerator<Cat> IEnumerable<Cat>.GetEnumerator()
{
Console.WriteLine("Cat");
// Some code.
return null;
}

IEnumerator IEnumerable.GetEnumerator()
{
// Some code.
return null;
}

IEnumerator<Dog> IEnumerable<Dog>.GetEnumerator()
{
Console.WriteLine("Dog");
// Some code.
return null;
}
}
class Program
{
public static void Test()
{
IEnumerable<Animal> pets = new Pets();
pets.GetEnumerator();
}
}

In diesem Beispiel ist nicht angegeben, wie die pets.GetEnumerator -Methode zwischen Cat und Dog auswählt.
Dies könnte Probleme im Code verursachen.

Siehe auch
Varianz in generischen Schnittstellen
Verwenden von Varianz für die generischen Delegaten Func und Action (C#)
Verwenden von Varianz in Schnittstellen für
generische Auflistungen (C#)
04.11.2021 • 2 minutes to read

Eine kovariante Schnittstelle ermöglicht den zugehörigen Methoden, mehr abgeleitete Typen zurückzugeben, als
in der Schnittstelle angegeben sind. Eine kontravariante Schnittstelle ermöglicht den zugehörigen Methoden,
Parameter von weniger abgeleiteten Typen anzunehmen, als in der Schnittstelle angegeben sind.
In .NET Framework 4 wurden mehrere vorhandene Schnittstellen kovariant und kontravariant. Dazu gehören
IEnumerable<T> und IComparable<T>. Dadurch können Sie Methoden wiederverwenden, die mit generischen
Auflistungen von Basistypen für Sammlungen von abgeleiteten Typen verwendet werden.
Die Liste der varianten Schnittstellen in .NET finden Sie unter Varianz in generischen Schnittstellen (C#).

Konvertieren von generischen Auflistungen


Das folgende Beispiel veranschaulicht die Vorteile der Unterstützung von Kovarianz in der IEnumerable<T>-
Schnittstelle. Die PrintFullName -Methode akzeptiert eine Auflistung vom Typ IEnumerable<Person> als
Parameter. Sie können dies jedoch für eine Auflistung des Typs IEnumerable<Employee> wiederverwenden, da
Employee``Person erbt.

// Simple hierarchy of classes.


public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

public class Employee : Person { }

class Program
{
// The method has a parameter of the IEnumerable<Person> type.
public static void PrintFullName(IEnumerable<Person> persons)
{
foreach (Person person in persons)
{
Console.WriteLine("Name: {0} {1}",
person.FirstName, person.LastName);
}
}

public static void Test()


{
IEnumerable<Employee> employees = new List<Employee>();

// You can pass IEnumerable<Employee>,


// although the method expects IEnumerable<Person>.

PrintFullName(employees);

}
}

Vergleichen von generischen Auflistungen


Das folgende Beispiel veranschaulicht die Vorteile der Unterstützung von Kontravarianz in der
IEqualityComparer<T>-Schnittstelle. Die PersonComparer -Klasse implementiert die IEqualityComparer<Person> -
Schnittstelle. Sie können diese Klasse jedoch zum Vergleich einer Sequenz von Objekten des Typs Employee
wiederverwenden, da Employee``Person erbt.

// Simple hierarchy of classes.


public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

public class Employee : Person { }

// The custom comparer for the Person type


// with standard implementations of Equals()
// and GetHashCode() methods.
class PersonComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
if (Object.ReferenceEquals(x, y)) return true;
if (Object.ReferenceEquals(x, null) ||
Object.ReferenceEquals(y, null))
return false;
return x.FirstName == y.FirstName && x.LastName == y.LastName;
}
public int GetHashCode(Person person)
{
if (Object.ReferenceEquals(person, null)) return 0;
int hashFirstName = person.FirstName == null
? 0 : person.FirstName.GetHashCode();
int hashLastName = person.LastName.GetHashCode();
return hashFirstName ^ hashLastName;
}
}

class Program
{

public static void Test()


{
List<Employee> employees = new List<Employee> {
new Employee() {FirstName = "Michael", LastName = "Alexander"},
new Employee() {FirstName = "Jeff", LastName = "Price"}
};

// You can pass PersonComparer,


// which implements IEqualityComparer<Person>,
// although the method expects IEqualityComparer<Employee>.

IEnumerable<Employee> noduplicates =
employees.Distinct<Employee>(new PersonComparer());

foreach (var employee in noduplicates)


Console.WriteLine(employee.FirstName + " " + employee.LastName);
}
}

Siehe auch
Varianz in generischen Schnittstellen
Varianz bei Delegaten (C#)
04.11.2021 • 5 minutes to read

Mit .NET Framework 3.5 wurde die Unterstützung von Varianz eingeführt, um Methodensignaturen und
Delegattypen in allen Delegaten in C# vergleichen zu können. Das bedeutet, dass Sie Delegaten nicht nur
Methoden mit übereinstimmenden Signaturen zuweisen können, sondern auch Methoden, die mehrere
abgeleitete Typen zurückgeben (Kovarianz) oder die Parameter akzeptieren, die über weniger abgeleitete Typen
verfügen, als durch den Delegattyp angegeben wurde (Kontravarianz). Dies umfasst generische und nicht
generische Delegaten.
Betrachten Sie beispielsweise folgenden Code, der zwei Klassen und zwei Delegaten aufweist: generisch und
nicht generisch.

public class First { }


public class Second : First { }
public delegate First SampleDelegate(Second a);
public delegate R SampleGenericDelegate<A, R>(A a);

Beim Erstellen von Delegaten vom Typ SampleDelegate oder SampleGenericDelegate<A, R> können Sie diesen
Delegaten eine der folgenden Methoden zuweisen.

// Matching signature.
public static First ASecondRFirst(Second second)
{ return new First(); }

// The return type is more derived.


public static Second ASecondRSecond(Second second)
{ return new Second(); }

// The argument type is less derived.


public static First AFirstRFirst(First first)
{ return new First(); }

// The return type is more derived


// and the argument type is less derived.
public static Second AFirstRSecond(First first)
{ return new Second(); }

Das folgende Codebeispiel veranschaulicht die implizite Konvertierung zwischen der Methodensignatur und
dem Delegattyp.
// Assigning a method with a matching signature
// to a non-generic delegate. No conversion is necessary.
SampleDelegate dNonGeneric = ASecondRFirst;
// Assigning a method with a more derived return type
// and less derived argument type to a non-generic delegate.
// The implicit conversion is used.
SampleDelegate dNonGenericConversion = AFirstRSecond;

// Assigning a method with a matching signature to a generic delegate.


// No conversion is necessary.
SampleGenericDelegate<Second, First> dGeneric = ASecondRFirst;
// Assigning a method with a more derived return type
// and less derived argument type to a generic delegate.
// The implicit conversion is used.
SampleGenericDelegate<Second, First> dGenericConversion = AFirstRSecond;

Weitere Beispiele finden Sie unter Using Variance in Delegates (C#) (Verwenden von Varianz bei Delegaten (C#))
und Using Variance for Func and Action Generic Delegates (C#) (Verwenden von Varianz für die generischen
Delegaten Func und Action (C#)).

Varianz in generischen Typparametern


In .NET Framework 4 oder höher können Sie die implizierte Konvertierung zwischen Delegaten aktivieren. Das
bedeutet, dass generische Delegaten, die über verschiedene von generischen Typparametern angegebene Typen
verfügen, sich gegenseitig zugewiesen werden können, wenn die Typen voneinander geerbt werden. Dies ist für
die Varianz erforderlich.
Sie müssen einen generischen Parameter in einem Delegaten mithilfe der Schlüsselwörter in oder out
explizit als kovariant oder kontravariant deklarieren, um die implizite Konvertierung zu aktivieren.
Im folgenden Codebeispiel wird veranschaulicht, wie Sie einen Delegaten erstellen können, der über einen
kovarianten generischen Typparameter verfügt.

// Type T is declared covariant by using the out keyword.


public delegate T SampleGenericDelegate <out T>();

public static void Test()


{
SampleGenericDelegate <String> dString = () => " ";

// You can assign delegates to each other,


// because the type T is declared covariant.
SampleGenericDelegate <Object> dObject = dString;
}

Wenn Sie die Unterstützung von Varianz nur verwenden, um Methodensignaturen mit Delegaten zu vergleichen
und nicht die Schlüsselwörter in und out verwenden, kann es möglicherweise passieren, dass Sie zwar
Delegate mit identischen Lambdaausdrücken oder -Methoden instanziieren, aber keinen Delegaten einem
anderen zuweisen können.
Im folgenden Codebeispiel kann SampleGenericDelegate<String> nicht expliziert in
SampleGenericDelegate<Object> konvertiert werden, obwohl String``Object erbt. Sie können dieses Problem
beheben, indem Sie den generischen Parameter T mit dem Schlüsselwort out markieren.
public delegate T SampleGenericDelegate<T>();

public static void Test()


{
SampleGenericDelegate<String> dString = () => " ";

// You can assign the dObject delegate


// to the same lambda expression as dString delegate
// because of the variance support for
// matching method signatures with delegate types.
SampleGenericDelegate<Object> dObject = () => " ";

// The following statement generates a compiler error


// because the generic type T is not marked as covariant.
// SampleGenericDelegate <Object> dObject = dString;

Generische Delegaten mit varianten Typparametern in .NET


Mit .NET Framework 4 wurde die Unterstützung von Varianz für generische Typparameter in verschiedenen
vorhandenen generischen Delegaten eingeführt:
Action -Delegaten aus dem System-Namespace, z.B. Action<T> und Action<T1,T2>
Func -Delegaten aus dem System-Namespace, z.B. Func<TResult> und Func<T,TResult>
Der Predicate<T>-Delegat.
Der Comparison<T>-Delegat.
Der Converter<TInput,TOutput>-Delegat.
Weitere Informationen und Beispiele finden Sie unter Using Variance for Func and Action Generic Delegates (C#)
(Verwenden von Varianz für die generischen Delegaten Func und Action (C#)).
Angeben varianter Typparameter in generischen Delegaten
Wenn ein generischer Delegat über kovariante oder kontravariante generische Typparameter verfügt, kann er
als varianter generischer Delegat bezeichnet werden.
Sie können einen generischen Typparameter mithilfe des Schlüsselworts out in einem generischen Delegaten
als kovariant deklarieren. Der kovariante Typ kann nur als Typ von Methodenrückgaben und nicht von
Methodenargumenten verwendet werden. Das folgende Codebeispiel zeigt, wie Sie einen kovarianten
generischen Delegaten deklarieren.

public delegate R DCovariant<out R>();

Sie können einen generischen Typparameter mithilfe des Schlüsselworts in in einem generischen Delegaten
als kontravariant deklarieren. Der kontravariante Typ kann nur als Typ von Methodenrückgaben und nicht von
Methodenargumenten verwendet werden. Das folgende Codebeispiel zeigt, wie Sie einen kontravarianten
generischen Delegaten deklarieren.

public delegate void DContravariant<in A>(A a);

IMPORTANT
Die Parameter ref , in und out in C# können nicht als variant markiert werden.
Es ist auch möglich, Varianz und Kovarianz im gleichen Delegaten, aber für verschiedene Typparameter, zu
unterstützen. Dies wird im folgenden Beispiel gezeigt.

public delegate R DVariant<in A, out R>(A a);

Instanziieren und Aufrufen von varianten generischen Delegaten


Sie können variante Delegaten genau wie invariante Delegaten instanziieren und aufrufen. Im folgenden Beispiel
wird der Delegat durch einen Lambdaausdruck instanziiert.

DVariant<String, String> dvariant = (String str) => str + " ";


dvariant("test");

Kombinieren von varianten generischen Delegaten


Kombinieren Sie keine varianten Delegaten. Die Methode Combine unterstützt keine Konvertierung von
varianten Delegaten und erwartet, dass Delegaten vom exakt gleichen Typ sind. Es kann zu einer
Laufzeitausnahme führen, wenn Sie Delegaten entweder mit der Methode Combine oder dem Operator +
kombinieren, wie im folgenden Codebeispiel gezeigt wird.

Action<object> actObj = x => Console.WriteLine("object: {0}", x);


Action<string> actStr = x => Console.WriteLine("string: {0}", x);
// All of the following statements throw exceptions at run time.
// Action<string> actCombine = actStr + actObj;
// actStr += actObj;
// Delegate.Combine(actStr, actObj);

Varianz in generischen Typparametern für Wert- und Referenztypen


Varianz für generische Typparameter wird nur für Referenztypen unterstützt. DVariant<int> kann z.B. nicht
implizit in DVariant<Object> oder DVariant<long> konvertiert werden, da es sich bei „Integer “ um einen
Werttyp handelt.
Das folgende Beispiel veranschaulicht, dass Varianz in generischen Typparametern für Werttypen nicht
unterstützt wird.

// The type T is covariant.


public delegate T DVariant<out T>();

// The type T is invariant.


public delegate T DInvariant<T>();

public static void Test()


{
int i = 0;
DInvariant<int> dInt = () => i;
DVariant<int> dVariantInt = () => i;

// All of the following statements generate a compiler error


// because type variance in generic parameters is not supported
// for value types, even if generic type parameters are declared variant.
// DInvariant<Object> dObject = dInt;
// DInvariant<long> dLong = dInt;
// DVariant<Object> dVariantObject = dVariantInt;
// DVariant<long> dVariantLong = dVariantInt;
}
Siehe auch
Generics
Verwenden von Varianz für die generischen Delegaten Func und Action (C#)
Kombinieren von Delegaten (Multicastdelegaten)
Verwenden von Varianz bei Delegaten (C#)
04.11.2021 • 2 minutes to read

Wenn Sie einem Delegat eine Methode zuweisen, bieten Kovarianz und Kontravarianz Flexibilität für das
Abgleichen eines Delegattyps mit einer Methodensignatur. Kovarianz lässt die Verfügung einer Methode über
einen Rückgabetyp zu, der stärker abgeleitet ist als der im Delegat definierte Typ. Kontravarianz lässt eine
Methode zu, die über Typen verfügt, die weniger abgeleitet sind als die im Delegattyp.

Beispiel 1: Kovarianz
Beschreibung
In diesem Beispiel wird veranschaulicht, wie Delegaten mit Methoden verwendet werden können, die über
Rückgabetypen verfügen, die von den Rückgabetypen in der Delegatsignatur abgeleitet sind. Der von
DogsHandler zurückgegebene Datentyp ist vom Typ Dogs , der vom im Delegat definierten Typ Mammals
abhängt.
Code

class Mammals {}
class Dogs : Mammals {}

class Program
{
// Define the delegate.
public delegate Mammals HandlerMethod();

public static Mammals MammalsHandler()


{
return null;
}

public static Dogs DogsHandler()


{
return null;
}

static void Test()


{
HandlerMethod handlerMammals = MammalsHandler;

// Covariance enables this assignment.


HandlerMethod handlerDogs = DogsHandler;
}
}

Beispiel 2: Kontravarianz
Beschreibung
In diesem Beispiel wird veranschaulicht, wie Delegaten mit Methoden verwendet werden können, die über
Parameter eines Typs verfügen, die Basistypen von den Parametertypen der Delegatsignatur sind. Mithilfe von
Kontravarianz können Sie einen Ereignishandler anstelle getrennter Handler verwenden. Im folgenden Beispiel
werden zwei Delegaten verwendet:
Ein KeyEventHandler-Delegat, der die Signatur des Ereignisses Button.KeyDown definiert. Die Signatur
lautet:

public delegate void KeyEventHandler(object sender, KeyEventArgs e)

Ein MouseEventHandler-Delegat, der die Signatur des Ereignisses Button.MouseClick definiert. Die
Signatur lautet:

public delegate void MouseEventHandler(object sender, MouseEventArgs e)

Das Beispiel definiert einen Ereignishandler mit einem EventArgs-Parameter und verwendet diesen, um die
Ereignisse Button.KeyDown und Button.MouseClick zu bearbeiten. Dies ist möglich, weil EventArgs der Basistyp
sowohl von KeyEventArgs als auch von MouseEventArgs ist.
Code

// Event handler that accepts a parameter of the EventArgs type.


private void MultiHandler(object sender, System.EventArgs e)
{
label1.Text = System.DateTime.Now.ToString();
}

public Form1()
{
InitializeComponent();

// You can use a method that has an EventArgs parameter,


// although the event expects the KeyEventArgs parameter.
this.button1.KeyDown += this.MultiHandler;

// You can use the same method


// for an event that expects the MouseEventArgs parameter.
this.button1.MouseClick += this.MultiHandler;

Siehe auch
Variance in Delegates (C#) (Varianz bei Delegaten (C#))
Verwenden von Varianz für die generischen Delegaten Func und Action (C#)
Verwenden von Varianz für die generischen
Delegaten Func und Action (C#)
04.11.2021 • 2 minutes to read

Diese Beispiele veranschaulichen, wie Sie Kovarianz und Kontravarianz in den generischen Delegaten Func und
Action verwenden, um die Wiederverwendung von Methoden zu ermöglichen und mehr Flexibilität in Ihrem
Code zu bieten.
Weitere Informationen zu Ko- und Kontravarianz finden Sie unter Varianz bei Delegaten (C#).

Verwendung von Delegaten mit kovarianten Typparametern


Das folgende Beispiel veranschaulicht die Vorteile der Unterstützung von Kovarianz in generischen Func -
Delegaten. Die Methode FindByTitle nimmt einen Parameter vom Typ String entgegen und gibt ein Objekt
vom Typ Employee zurück. Allerdings können Sie diese Methode dem Delegaten Func<String, Person>
zuweisen, da Employee``Person erbt.

// Simple hierarchy of classes.


public class Person { }
public class Employee : Person { }
class Program
{
static Employee FindByTitle(String title)
{
// This is a stub for a method that returns
// an employee that has the specified title.
return new Employee();
}

static void Test()


{
// Create an instance of the delegate without using variance.
Func<String, Employee> findEmployee = FindByTitle;

// The delegate expects a method to return Person,


// but you can assign it a method that returns Employee.
Func<String, Person> findPerson = FindByTitle;

// You can also assign a delegate


// that returns a more derived type
// to a delegate that returns a less derived type.
findPerson = findEmployee;

}
}

Verwendung von Delegaten mit kontravarianten Typparametern


Im folgenden Beispiel werden die Vorteile der Unterstützung von Kontravarianz in generischen Action -
Delegaten veranschaulicht. Die AddToContacts -Methode nimmt einen Parameter vom Typ Person entgegen.
Allerdings können Sie diese Methode dem Delegaten Action<Employee> zuweisen, da Employee``Person erbt.
public class Person { }
public class Employee : Person { }
class Program
{
static void AddToContacts(Person person)
{
// This method adds a Person object
// to a contact list.
}

static void Test()


{
// Create an instance of the delegate without using variance.
Action<Person> addPersonToContacts = AddToContacts;

// The Action delegate expects


// a method that has an Employee parameter,
// but you can assign it a method that has a Person parameter
// because Employee derives from Person.
Action<Employee> addEmployeeToContacts = AddToContacts;

// You can also assign a delegate


// that accepts a less derived parameter to a delegate
// that accepts a more derived parameter.
addEmployeeToContacts = addPersonToContacts;
}
}

Siehe auch
Kovarianz und Kontravarianz (C#)
Generics
Ausdrucksbaumstrukturen (C#)
04.11.2021 • 4 minutes to read

Ausdrucksbaumstrukturen stellen Code in einer baumähnlichen Datenstruktur dar, in denen jeder Knoten ein
Ausdruck ist, z. B. ein Methodenaufruf oder eine binäre Operation wie x < y .
Sie können Code kompilieren und ausführen, der von Ausdrucksbaumstrukturen dargestellt wird. Dies
ermöglicht dynamische Änderungen des ausführbaren Codes, die Ausführung von LINQ-Abfragen in
verschiedenen Datenbanken und die Erstellung von dynamischen Abfragen. Weitere Informationen zu
Ausdrucksbaumstrukturen in LINQ finden Sie unter Verwenden von Ausdrucksbaumstrukturen zum Erstellen
von dynamischen Abfragen (C#).
Ausdrucksbaumstrukturen werden auch in der Dynamic Language Runtime (DLR) verwendet, um
Interoperabilität zwischen dynamischen Sprachen und .NET zu gewährleisten. Dies ermöglicht Entwicklern von
Compilern, Ausdrucksbaumstrukturen anstelle der Microsoft Intermediate Language (MSIL) auszugeben.
Weitere Informationen zur DLR finden Sie unter Übersicht über die Dynamic Language Runtime.
Sie können den C#- oder Visual Basic-Compiler zur Erstellung einer Ausdrucksbaumstruktur veranlassen,
basierend auf einem anonymen Lambda-Ausdruck oder Sie können Ausdrucksbaumstrukturen durch
Verwendung des Namespace System.Linq.Expressions manuell erstellen.

Erstellen von Ausdrucksbaumstrukturen aus Lambda-Ausdrücken


Wenn ein Lambda-Ausdruck einer Variablen vom Typ Expression<TDelegate> zugewiesen ist, gibt der Compiler
Code aus, um eine Ausdrucksbaumstruktur zu erstellen, die den Lambda-Ausdruck verkörpert.
Der C#-Compiler kann Ausdrucksbaumstrukturen nur aus Ausdrucklambdas (oder einzeiligen Lambdas)
erstellen. Es kann keine Anweisungslambdas (oder mehrzeiligen Lambdas) analysieren. Weitere Informationen
zu Lambdaausdrücken in C# finden Sie unter Lambdaausdrücke.
In den folgenden Codebeispielen wird veranschaulicht, wie Sie C#-Compiler dazu veranlassen, eine
Ausdrucksbaumstruktur zu erstellen, die den Lambdaausdruck num => num < 5 verkörpert.

Expression<Func<int, bool>> lambda = num => num < 5;

Erstellen von Ausdrucksbaumstrukturen mit der API


Verwenden Sie die Klasse Expression, um Ausdrucksbaumstrukturen mit der API zu erstellen. Diese Klasse
enthält statische Factorymethoden, die bestimmte Ausdrucksstruktur-Knotentypen erstellen können, z. B.
ParameterExpression, der eine Variable oder einen Parameter darstellt oder MethodCallExpression, der einen
Methodenaufruf darstellt. ParameterExpression, MethodCallExpression, und die anderen ausdrucksspezifischen
Ausdrucksbaumstruktur-Typen werden auch im Namespace System.Linq.Expressions definiert. Diese Typen
werden vom abstrakten Typ Expression abgeleitet.
Im folgenden Codebeispiel wird veranschaulicht, wie eine Ausdrucksbaumstruktur erstellt wird, die den
Lambdaausdruck num => num < 5 mithilfe der API verkörpert.
// Add the following using directive to your code file:
// using System.Linq.Expressions;

// Manually build the expression tree for


// the lambda expression num => num < 5.
ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression five = Expression.Constant(5, typeof(int));
BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
Expression<Func<int, bool>> lambda1 =
Expression.Lambda<Func<int, bool>>(
numLessThanFive,
new ParameterExpression[] { numParam });

In .NET Framework 4 oder höher unterstützt die Ausdrucksbaumstruktur-API auch Zuweisungen und
Ablaufsteuerungsausdrücke wie Schleifen, bedingte Blöcke und try-catch -Blöcke. Mithilfe der API können Sie
Ausdrucksbaumstrukturen erstellen, die komplexer sind als diejenigen, die von Lambda-Ausdrücken vom C#-
Compiler erstellt werden. Im folgenden Beispiel wird veranschaulicht, wie eine Ausdrucksbaumstruktur erstellt
wird, welche die Fakultät einer Zahl berechnet.

// Creating a parameter expression.


ParameterExpression value = Expression.Parameter(typeof(int), "value");

// Creating an expression to hold a local variable.


ParameterExpression result = Expression.Parameter(typeof(int), "result");

// Creating a label to jump to from a loop.


LabelTarget label = Expression.Label(typeof(int));

// Creating a method body.


BlockExpression block = Expression.Block(
// Adding a local variable.
new[] { result },
// Assigning a constant to a local variable: result = 1
Expression.Assign(result, Expression.Constant(1)),
// Adding a loop.
Expression.Loop(
// Adding a conditional block into the loop.
Expression.IfThenElse(
// Condition: value > 1
Expression.GreaterThan(value, Expression.Constant(1)),
// If true: result *= value --
Expression.MultiplyAssign(result,
Expression.PostDecrementAssign(value)),
// If false, exit the loop and go to the label.
Expression.Break(label, result)
),
// Label to jump to.
label
)
);

// Compile and execute an expression tree.


int factorial = Expression.Lambda<Func<int, int>>(block, value).Compile()(5);

Console.WriteLine(factorial);
// Prints 120.

Weitere Informationen finden Sie unter Generating Dynamic Methods with Expression Trees in Visual Studio
2010 (Generieren dynamischer Methoden mit Ausdrucksbaumstrukturen in Visual Studio 2010). Dieser Artikel
gilt auch für höhere Versionen von Visual Studio.
Analysieren von Ausdrucksbaumstrukturen
Im folgenden Codebeispiel wird veranschaulicht, wie die Ausdrucksbaumstruktur, die den Lambdaausdruck
num => num < 5 darstellt, in seine Bestandteile zerlegt werden kann.

// Add the following using directive to your code file:


// using System.Linq.Expressions;

// Create an expression tree.


Expression<Func<int, bool>> exprTree = num => num < 5;

// Decompose the expression tree.


ParameterExpression param = (ParameterExpression)exprTree.Parameters[0];
BinaryExpression operation = (BinaryExpression)exprTree.Body;
ParameterExpression left = (ParameterExpression)operation.Left;
ConstantExpression right = (ConstantExpression)operation.Right;

Console.WriteLine("Decomposed expression: {0} => {1} {2} {3}",


param.Name, left.Name, operation.NodeType, right.Value);

// This code produces the following output:

// Decomposed expression: num => num LessThan 5

Unveränderlichkeit von Ausdrucksbaumstrukturen


Ausdrucksbaumstrukturen sollten unveränderlich sein. Das heißt, wenn Sie eine Ausdrucksbaumstruktur ändern
möchten, müssen Sie einen neuen Knoten konstruieren, indem Sie einen vorhandenen Knoten kopieren und die
enthaltenen Knoten ersetzen. Sie können einen Ausdrucksbaumstruktur-Besucher verwenden, um die
vorhandene Ausdrucksbaumstruktur zu durchlaufen. Weitere Informationen finden Sie unter Ändern von
Ausdrucksbaumstrukturen (C#).

Kompilieren von Ausdrucksbaumstrukturen


Der Typ Expression<TDelegate> bietet die Methode Compile, welche den durch eine Ausdrucksbaumstruktur
dargestellten Code in einen ausführbaren Delegaten kompiliert.
Im folgenden Codebeispiel wird veranschaulicht, wie eine Ausdrucksbaumstruktur kompiliert und der daraus
resultierende Code ausgeführt wird.

// Creating an expression tree.


Expression<Func<int, bool>> expr = num => num < 5;

// Compiling the expression tree into a delegate.


Func<int, bool> result = expr.Compile();

// Invoking the delegate and writing the result to the console.


Console.WriteLine(result(4));

// Prints True.

// You can also use simplified syntax


// to compile and run an expression tree.
// The following line can replace two previous statements.
Console.WriteLine(expr.Compile()(4));

// Also prints True.

Weitere Informationen finden Sie unter Ausführen von Ausdrucksbaumstrukturen (C#).


Siehe auch
System.Linq.Expressions
Ausführen von Ausdrucksbaumstrukturen (C#)
Ändern von Ausdrucksbaumstrukturen (C#)
Lambda-Ausdrücke
Übersicht über die Dynamic Language Runtime
Programmierkonzepte (C#)
Ausführen von Ausdrucksbaumstrukturen (C#)
04.11.2021 • 2 minutes to read

In diesem Thema erfahren Sie, wie eine Ausdrucksbaumstruktur ausgeführt wird. Die Ausführung einer
Ausdrucksbaumstruktur gibt möglicherweise einen Wert zurück. Es kann jedoch auch nur eine Aktion
ausgeführt werden, z.B. das Aufrufen einer Methode.
Nur Ausdrucksbaumstrukturen, die Lambdaausdrücke darstellen, können ausgeführt werden.
Ausdrucksbaumstrukturen, die Lambdaausdrücke darstellen, sind vom Typ LambdaExpression oder
Expression<TDelegate>. Um diese Ausdrucksbaumstruktur auszuführen, rufen Sie die Compile-Methode auf,
um einen ausführbaren Delegaten zu erstellen und diesen anschließend aufzurufen.

NOTE
Wenn der Typ des Delegaten nicht bekannt ist, d.h. wenn der Lambdaausdruck vom Typ LambdaExpression und nicht
Expression<TDelegate> ist, müssen Sie die DynamicInvoke-Methode auf dem Delegaten aufrufen, anstatt sie direkt
aufzurufen.

Wenn eine Ausdrucksbaumstruktur keinen Lambdaausdruck darstellt,können Sie einen neuen Lambdaausdruck
erstellen, der die ursprüngliche Ausdrucksbaumstruktur als Textkörper hat, indem Sie die Lambda<TDelegate>
(Expression, IEnumerable<ParameterExpression>)-Methode aufrufen. Anschließend können Sie den
Lambdaausdruck ausführen, wie weiter oben in diesem Abschnitt beschrieben.

Beispiel
Im folgenden Codebeispiel wird veranschaulicht, wie eine Ausdrucksbaumstruktur ausgeführt wird, die das
Potenzieren darstellt, indem ein Lambdaausdruck erstellt und ausgeführt wird. Das Ergebnis, das die potenzierte
Zahl darstellt, wird angezeigt.

// The expression tree to execute.


BinaryExpression be = Expression.Power(Expression.Constant(2D), Expression.Constant(3D));

// Create a lambda expression.


Expression<Func<double>> le = Expression.Lambda<Func<double>>(be);

// Compile the lambda expression.


Func<double> compiledExpression = le.Compile();

// Execute the lambda expression.


double result = compiledExpression();

// Display the result.


Console.WriteLine(result);

// This code produces the following output:


// 8

Kompilieren des Codes


Binden Sie den System.Linq.Expressions-Namespace ein.

Siehe auch
Ausdrucksbaumstrukturen (C#)
Ändern von Ausdrucksbaumstrukturen (C#)
Ändern von Ausdrucksbaumstrukturen (C#)
04.11.2021 • 2 minutes to read

In diesem Thema erfahren Sie, wie Sie eine Ausdrucksbaumstruktur ändern können. Ausdrucksbaumstrukturen
sind unveränderlich, d.h. sie können nicht direkt modifiziert werden. Um eine Ausdrucksbaumstruktur zu
verändern, müssen Sie eine Kopie eines vorhandenen Ausdrucksbaumstruktur erstellen und währenddessen die
erforderlichen Änderungen vornehmen. Sie können die ExpressionVisitor-Klasse verwenden, um einen
vorhandenen Ausdrucksbaum zu durchlaufen und jeden Knoten zu kopieren, der durchlaufen wird.
So ändern Sie Ausdrucksbaumstrukturen
1. Erstellen Sie ein neues Konsolenanwendungsprojekt .
2. Fügen Sie der Datei eine using -Anweisung für den System.Linq.Expressions -Namespace hinzu.
3. Fügen Sie die AndAlsoModifier -Klasse in Ihr Projekt ein.

public class AndAlsoModifier : ExpressionVisitor


{
public Expression Modify(Expression expression)
{
return Visit(expression);
}

protected override Expression VisitBinary(BinaryExpression b)


{
if (b.NodeType == ExpressionType.AndAlso)
{
Expression left = this.Visit(b.Left);
Expression right = this.Visit(b.Right);

// Make this binary expression an OrElse operation instead of an AndAlso operation.


return Expression.MakeBinary(ExpressionType.OrElse, left, right, b.IsLiftedToNull,
b.Method);
}

return base.VisitBinary(b);
}
}

Diese Klasse erbt die ExpressionVisitor-Klasse und ist darauf spezialisiert, Ausdrücke zu verändern, die
bedingte AND -Vorgänge darstellen. Es ändert diese Vorgänge von einem bedingten AND in ein bedingtes
OR . Zu diesem Zweck setzt die Klasse die VisitBinary-Methode der Basisklasse außer Kraft, weil bedingte
AND -Ausdrücke als binäre Ausdrücke dargestellt werden. Für die VisitBinary -Methode gilt Folgendes:
Wenn der an die Methode übergebene Ausdruck eine bedingte AND -Operation darstellt, erstellt der Code
einen neuen Ausdruck, der den bedingten Operator OR anstelle des bedingten Operators AND enthält.
Wenn der an VisitBinary übergebene Ausdruck keinen bedingten AND -Vorgang darstellt, verzögert die
Methode die Implementierung der Basisklasse. Die Basisklassenmethode erstellt Knoten, die den
übergebenen Ausdrucksbaumstrukturen gleichen. In diesem Fall sind die Teilstrukturen der Knoten
jedoch durch die Ausdrucksbaumstrukturen ersetzt, die vom Besucher rekursiv erstellt werden.
4. Fügen Sie der Datei eine using -Anweisung für den System.Linq.Expressions -Namespace hinzu.
5. Fügen Sie der Main -Methode in der Datei „Program.cs“ Code hinzu, um eine Ausdrucksbaumstruktur zu
erstellen, und übergeben Sie diese Struktur an die Methode, die sie ändert.
Expression<Func<string, bool>> expr = name => name.Length > 10 && name.StartsWith("G");
Console.WriteLine(expr);

AndAlsoModifier treeModifier = new AndAlsoModifier();


Expression modifiedExpr = treeModifier.Modify((Expression) expr);

Console.WriteLine(modifiedExpr);

/* This code produces the following output:

name => ((name.Length > 10) && name.StartsWith("G"))


name => ((name.Length > 10) || name.StartsWith("G"))
*/

Der Code erstellt einen Ausdruck, der einen bedingten AND -Vorgang enthält. Er erstellt anschließend eine
Instanz der AndAlsoModifier -Klasse und übergibt den Ausdruck an die Modify -Methode dieser Klasse.
Sowohl der ursprüngliche als auch der geänderte Ausdrucksbaum werden ausgegeben, um die
Änderungen zu zeigen.
6. Kompilieren Sie die Anwendung, und führen Sie sie aus.

Siehe auch
Ausführen von Ausdrucksbaumstrukturen (C#)
Ausdrucksbaumstrukturen (C#)
Durchführen von Abfragen auf Basis des
Laufzeitzustands (C#)
04.11.2021 • 8 minutes to read

Der folgende Code definiert eine IQueryable- oder IQueryable<T>-Schnittstelle für eine Datenquelle:

var companyNames = new[] {


"Consolidated Messenger", "Alpine Ski House", "Southridge Video",
"City Power & Light", "Coho Winery", "Wide World Importers",
"Graphic Design Institute", "Adventure Works", "Humongous Insurance",
"Woodgrove Bank", "Margie's Travel", "Northwind Traders",
"Blue Yonder Airlines", "Trey Research", "The Phone Company",
"Wingtip Toys", "Lucerne Publishing", "Fourth Coffee"
};

// We're using an in-memory array as the data source, but the IQueryable could have come
// from anywhere -- an ORM backed by a database, a web request, or any other LINQ provider.
IQueryable<string> companyNamesSource = companyNames.AsQueryable();
var fixedQry = companyNames.OrderBy(x => x);

Bei jeder Ausführung dieses Codes wird genau dieselbe Abfrage ausgeführt. Dies ist häufig nicht sehr nützlich,
da Ihr Code je nach den Bedingungen zur Laufzeit verschiedene Abfragen ausführen soll. In diesem Artikel wird
beschrieben, wie Sie auf Grundlage des Laufzeitzustands eine andere Abfrage ausführen können.

IQueryable/IQueryable<T> und Ausdrucksbaumstrukturen


Eine IQueryable-Schnittstelle verfügt im Grunde über zwei Komponenten:
Expression: eine sprach- und datenquellenagnostische Darstellung der Komponenten der aktuellen Abfrage
in Form einer Ausdrucksbaumstruktur
Provider: eine Instanz eines LINQ-Anbieters, die weiß, wie die aktuelle Abfrage in einem Wert oder einer
Wertegruppe materialisiert werden soll
Im Kontext dynamischer Abfragen bleibt der Anbieter normalerweise immer gleich. Es ist die
Ausdrucksbaumstruktur, die sich von Abfrage zu Abfrage unterscheidet.
Ausdrucksbaumstrukturen sind unveränderlich. Wenn Sie eine andere Ausdrucksbaumstruktur (und somit eine
andere Abfrage) möchten, müssen Sie die bestehende Struktur in eine neue Ausdrucksbaumstruktur und somit
in eine neue IQueryable-Schnittstelle übersetzen.
In den folgenden Abschnitten werden die genauen Verfahren für das unterschiedliche Abfragen in Abhängigkeit
vom Laufzeitzustand beschrieben:
Verwenden des Laufzeitzustands innerhalb der Ausdrucksbaumstruktur
Aufrufen weiterer LINQ-Methoden
Variieren der Ausdrucksbaumstruktur, die an die LINQ-Methoden übergeben wird
Erstellen einer Expression<TDelegate>-Ausdrucksbaumstruktur mithilfe der Factorymethoden in Expression
Hinzufügen von Methodenaufrufknoten zu einer Ausdrucksbaumstruktur von IQueryable
Erstellen von Zeichenfolgen und Verwenden der dynamischen LINQ-Bibliothek

Verwenden des Laufzeitzustands innerhalb der


Ausdrucksbaumstruktur
Wenn der LINQ-Anbieter dies unterstützt, besteht der einfachste Weg, eine dynamische Abfrage durchzuführen,
darin, direkt in der Abfrage über eine geschlossene Variable, wie length im folgenden Codebeispiel, auf den
Laufzeitzustand zu verweisen:

var length = 1;
var qry = companyNamesSource
.Select(x => x.Substring(0, length))
.Distinct();

Console.WriteLine(string.Join(",", qry));
// prints: C, A, S, W, G, H, M, N, B, T, L, F

length = 2;
Console.WriteLine(string.Join(",", qry));
// prints: Co, Al, So, Ci, Wi, Gr, Ad, Hu, Wo, Ma, No, Bl, Tr, Th, Lu, Fo

Die interne Ausdrucksbaumstruktur (und damit die Abfrage) wurde nicht geändert. Die Abfrage gibt nur andere
Werte zurück, weil der Wert von length modifiziert wurde.

Aufrufen weiterer LINQ-Methoden


Im Allgemeinen führen die integrierten LINQ-Methoden in Queryable zwei Schritte aus:
Umschließen der aktuellen Ausdrucksbaumstruktur in einem MethodCallExpression-Objekt, das den
Methodenaufruf darstellt
Zurückübergeben der umschließenden Ausdrucksbaumstruktur an den Anbieter, um entweder über die
Methode IQueryProvider.Execute des Anbieters einen Wert oder über die Methode
IQueryProvider.CreateQuery ein übersetztes Abfrageobjekt zurückzugeben
Sie können die ursprüngliche Abfrage durch das Ergebnis einer Methode ersetzen, die IQueryable<T>
zurückgibt, um eine neue Abfrage zu erhalten. Dies kann abhängig vom Laufzeitzustand erfolgen, wie im
folgenden Beispiel gezeigt:

// bool sortByLength = /* ... */;

var qry = companyNamesSource;


if (sortByLength)
{
qry = qry.OrderBy(x => x.Length);
}

Variieren der Ausdrucksbaumstruktur, die an die LINQ-Methoden


übergeben wird
Abhängig vom Laufzeitzustand können Sie verschiedene Ausdrücke an die LINQ-Methoden übergeben:
// string? startsWith = /* ... */;
// string? endsWith = /* ... */;

Expression<Func<string, bool>> expr = (startsWith, endsWith) switch


{
("" or null, "" or null) => x => true,
(_, "" or null) => x => x.StartsWith(startsWith),
("" or null, _) => x => x.EndsWith(endsWith),
(_, _) => x => x.StartsWith(startsWith) || x.EndsWith(endsWith)
};

var qry = companyNamesSource.Where(expr);

Sie sollten die verschiedenen Unterausdrücke außerdem mit einer Drittanbieterbibliothek wie PredicateBuilder
von LinqKit erstellen:

// This is functionally equivalent to the previous example.

// using LinqKit;
// string? startsWith = /* ... */;
// string? endsWith = /* ... */;

Expression<Func<string, bool>>? expr = PredicateBuilder.New<string>(false);


var original = expr;
if (!string.IsNullOrEmpty(startsWith))
{
expr = expr.Or(x => x.StartsWith(startsWith));
}
if (!string.IsNullOrEmpty(endsWith))
{
expr = expr.Or(x => x.EndsWith(endsWith));
}
if (expr == original)
{
expr = x => true;
}

var qry = companyNamesSource.Where(expr);

Erstellen von Ausdrucksbaumstrukturen und Abfragen mit


Factorymethoden
In allen bisherigen Beispielen waren der Elementtyp zur Kompilierzeit, string , und somit auch der Typ der
Abfrage, IQueryable<string> , bekannt. Möglicherweise müssen Sie einer Abfrage eines beliebigen Elementtyps
Komponenten hinzufügen bzw. je nach Elementtyp verschiedene Komponenten verwenden. Sie können
Ausdrucksbaumstrukturen mithilfe der Factorymethoden in System.Linq.Expressions.Expression von Grund auf
neu erstellen und so den Ausdruck zur Laufzeit auf einen bestimmten Elementtyp zuschneiden.
Erstellen eines Expression<TDelegate>-Objekts
Wenn Sie einen Ausdruck erstellen, der an eine der LINQ-Methoden übergeben werden soll, erstellen Sie
tatsächlich eine Instanz von Expression<TDelegate>, wobei TDelegate ein Delegattyp wie Func<string, bool>
oder Action oder ein benutzerdefinierter Delegattyp ist.
Expression<TDelegate> erbt von einem LambdaExpression-Objekt, das einen kompletten Lambdaausdruck wie
den folgenden darstellt:

Expression<Func<string, bool>> expr = x => x.StartsWith("a");


LambdaExpression verfügt über zwei Komponenten:
eine Parameterliste, (string x) , die von der Parameters-Eigenschaft dargestellt wird
einen Körper, x.StartsWith("a") , der durch die Body-Eigenschaft dargestellt wird

Die grundlegenden Schritte zum Erstellen eines Expression<TDelegate>-Objekts lauten wie folgt:
Definieren Sie ParameterExpression-Objekte für jeden der Parameter (sofern vorhanden) im
Lambdaausdruck mithilfe der Parameter-Factorymethode.

ParameterExpression x = Parameter(typeof(string), "x");

Erstellen Sie den Text von LambdaExpression mit den von Ihnen definierten ParameterExpressions und
den Factorymethoden unter Expression. Ein Ausdruck, der x.StartsWith("a") darstellt, könnte
beispielsweise wie folgt erstellt werden:

Expression body = Call(


x,
typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!,
Constant("a")
);

Umschließen Sie die Parameter und den Körper mithilfe der Überladung der Lambda-Factorymethode
mit einem zur Kompilierzeit typisierten Expression<TDelegate>-Objekt:

Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);

In den folgenden Abschnitten wird ein Szenario beschrieben, in dem Sie ein Expression<TDelegate>-Objekt
erstellen, das an eine LINQ-Methode übergeben werden soll. Außerdem wird ein umfassendes Beispiel für die
Erstellung des Objekts mithilfe der Factorymethoden gegeben.
Szenario
Angenommen, Sie haben mehrere Entitätstypen:

record Person(string LastName, string FirstName, DateTime DateOfBirth);


record Car(string Model, int Year);

Diese Entitätstypen sollen nun gefiltert und nur die Entitäten zurückgegeben werden, die einen bestimmten Text
in einem der string -Felder aufweisen. Für Person sollen die Eigenschaften FirstName und LastName
durchsucht werden:

string term = /* ... */;


var personsQry = new List<Person>()
.AsQueryable()
.Where(x => x.FirstName.Contains(term) || x.LastName.Contains(term));

Für Car soll jedoch nur die Eigenschaft Model durchsucht werden:

string term = /* ... */;


var carsQry = new List<Car>()
.AsQueryable()
.Where(x => x.Model.Contains(term));
Sie können zwar eine benutzerdefinierte Funktion für IQueryable<Person> und eine weitere für IQueryable<Car>
schreiben, die folgende Funktion fügt diese Filter jedoch unabhängig vom jeweiligen Elementtyp zu jeder
vorhandenen Abfrage hinzu.
Beispiel

// using static System.Linq.Expressions.Expression;

IQueryable<T> TextFilter<T>(IQueryable<T> source, string term)


{
if (string.IsNullOrEmpty(term)) { return source; }

// T is a compile-time placeholder for the element type of the query.


Type elementType = typeof(T);

// Get all the string properties on this specific type.


PropertyInfo[] stringProperties =
elementType.GetProperties()
.Where(x => x.PropertyType == typeof(string))
.ToArray();
if (!stringProperties.Any()) { return source; }

// Get the right overload of String.Contains


MethodInfo containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) })!;

// Create a parameter for the expression tree:


// the 'x' in 'x => x.PropertyName.Contains("term")'
// The type of this parameter is the query's element type
ParameterExpression prm = Parameter(elementType);

// Map each property to an expression tree node


IEnumerable<Expression> expressions = stringProperties
.Select(prp =>
// For each property, we have to construct an expression tree node like
x.PropertyName.Contains("term")
Call( // .Contains(...)
Property( // .PropertyName
prm, // x
prp
),
containsMethod,
Constant(term) // "term"
)
);

// Combine all the resultant expression nodes using ||


Expression body = expressions
.Aggregate(
(prev, current) => Or(prev, current)
);

// Wrap the expression body in a compile-time-typed lambda expression


Expression<Func<T, bool>> lambda = Lambda<Func<T, bool>>(body, prm);

// Because the lambda is compile-time-typed (albeit with a generic parameter), we can use it with the
Where method
return source.Where(lambda);
}

Da die TextFilter -Funktion eine IQueryable<T>-Schnittstelle (und nicht nur eine IQueryable-Schnittstelle)
nimmt und zurückgibt, können Sie weitere zur Kompilierzeit typisierte Abfrageelemente nach dem Textfilter
hinzufügen.
var qry = TextFilter(
new List<Person>().AsQueryable(),
"abcd"
)
.Where(x => x.DateOfBirth < new DateTime(2001, 1, 1));

var qry1 = TextFilter(


new List<Car>().AsQueryable(),
"abcd"
)
.Where(x => x.Year == 2010);

Hinzufügen von Methodenaufrufknoten zur Ausdrucksbaumstruktur


von IQueryable
Wenn Sie IQueryable anstelle von IQueryable<T> verwenden, können Sie die generischen LINQ-Methoden
nicht direkt aufrufen. Eine Alternative besteht darin, die innere Ausdrucksbaumstruktur wie oben zu erstellen
und mithilfe der Reflexion die entsprechenden LINQ-Methode aufzurufen, während die Ausdrucksbaumstruktur
übergeben wird.
Sie können auch die Funktionalität der LINQ-Methode duplizieren, indem Sie die gesamte Struktur in einem
MethodCallExpression-Objekt umschließen, das einen Aufruf der LINQ-Methode darstellt:

IQueryable TextFilter_Untyped(IQueryable source, string term)


{
if (string.IsNullOrEmpty(term)) { return source; }
Type elementType = source.ElementType;

// The logic for building the ParameterExpression and the LambdaExpression's body is the same as in the
previous example,
// but has been refactored into the constructBody function.
(Expression? body, ParameterExpression? prm) = constructBody(elementType, term);
if (body is null) {return source;}

Expression filteredTree = Call(


typeof(Queryable),
"Where",
new[] { elementType},
source.Expression,
Lambda(body, prm!)
);

return source.Provider.CreateQuery(filteredTree);
}

Beachten Sie, dass in diesem Fall zur Kompilierzeit kein generischer T -Platzhalter verfügbar ist. Aus diesem
Grund verwenden Sie die Lambda-Überladung, die keine Typinformationen zur Kompilierzeit benötigt und
LambdaExpression anstelle eines Expression<TDelegate>-Objekts erzeugt.

Die dynamische LINQ-Bibliothek


Das Erstellen von Ausdrucksbaumstrukturen mit Factorymethoden ist relativ komplex. Es ist einfacher,
Zeichenfolgen zu verfassen. Die dynamische LINQ-Bibliothek macht eine Reihe von Erweiterungsmethoden für
IQueryable verfügbar, die den LINQ-Standardmethoden in Queryable entsprechen und die Zeichenfolgen in
einer besonderen Syntax anstelle von Ausdrucksbaumstrukturen akzeptieren. Die Bibliothek generiert die
entsprechende Ausdrucksbaumstruktur aus der Zeichenfolge und kann die resultierende übersetzte Schnittstelle
IQueryable zurückgeben.
Das vorherige Beispiel könnte z. B. wie folgt umgeschrieben werden:

// using System.Linq.Dynamic.Core

IQueryable TextFilter_Strings(IQueryable source, string term) {


if (string.IsNullOrEmpty(term)) { return source; }

var elementType = source.ElementType;

// Get all the string property names on this specific type.


var stringProperties =
elementType.GetProperties()
.Where(x => x.PropertyType == typeof(string))
.ToArray();
if (!stringProperties.Any()) { return source; }

// Build the string expression


string filterExpr = string.Join(
" || ",
stringProperties.Select(prp => $"{prp.Name}.Contains(@0)")
);

return source.Where(filterExpr, term);


}

Siehe auch
Ausdrucksbaumstrukturen (C#)
Ausführen von Ausdrucksbaumstrukturen (C#)
Dynamisches Festlegen von Prädikatfiltern zur Laufzeit
Debuggen von Ausdrucksbäumen in Visual Studio
(C#)
04.11.2021 • 2 minutes to read

Sie können die Struktur und den Inhalt von Ausdrucksbaumstrukturen beim Debuggen Ihrer Anwendung
analysieren. Um eine Übersicht über die Ausdrucksbaumstruktur zu erhalten, können Sie die DebugView -
Eigenschaft verwenden, die Ausdrucksbaumstrukturen mit einer speziellen Syntax darstellt. (Beachten Sie, dass
DebugView nur im Debugmodus verfügbar ist.)

Da DebugView eine Zeichenfolge ist, können Sie die integrierte Text-Schnellansicht verwenden, um eine Ansicht
mit mehreren Zeilen zu erhalten. Wählen Sie hierzu über das Lupensymbol neben DebugView die Option Text-
Schnellansicht .

Alternativ dazu können Sie eine benutzerdefinierte Schnellansicht für Ausdrucksbaumstrukturen installieren
und verwenden, wie z.B.:
Mit lesbaren Ausdrücken (MIT-Lizenz, die über den Visual Studio Marketplace verfügbar ist) wird die
Ausdrucksbaumstruktur als C#-Code, der ein Design zugewiesen werden kann, und mit verschiedenen
Renderingoptionen gerendert:

Bei der Schnellansicht für lesbare Ausdrücke (MIT-Lizenz) wird eine Strukturansicht der
Ausdrucksbaumstruktur und der zugehörigen einzelnen Knoten bereitgestellt:
So öffnen Sie eine Schnellansicht für eine Ausdrucksbaumstruktur
1. Klicken Sie auf das Lupensymbol, das in DataTips neben einer Ausdrucksbaumstruktur, in einem
Über wachungsfenster , einem Auto - oder einem Lokalfenster angezeigt wird.
Eine Liste mit den verfügbaren Schnellansichten wird angezeigt:

2. Klicken Sie auf die gewünschte Schnellansicht.

Siehe auch
Ausdrucksbaumstrukturen (C#)
Debuggen in Visual Studio
Erstellen benutzerdefinierter Schnellansichten
DebugView -Syntax
DebugView-Syntax
04.11.2021 • 2 minutes to read

Die Eigenschaft DebugView (nur beim Debuggen verfügbar) bietet eine Zeichenfolgendarstellung von
Ausdrucksbaumstrukturen. Der größte Teil der Syntax ist recht einfach zu verstehen; einige Sonderfälle werden
in den folgenden Abschnitten beschrieben.
Jedem Beispiel folgt ein Blockkommentar, der die DebugView enthält.

ParameterExpression
Namen von ParameterExpression-Variablen werden mit einem $ -Symbol am Anfang angezeigt.
Wenn ein Parameter nicht über einen Namen verfügt, wird ihm ein automatisch generierter Name zugewiesen,
z.B. $var1 oder $var2 .
Beispiele

ParameterExpression numParam = Expression.Parameter(typeof(int), "num");


/*
$num
*/

ParameterExpression numParam = Expression.Parameter(typeof(int));


/*
$var1
*/

ConstantExpression
Für ConstantExpression-Objekte, die ganzzahlige Werte, Zeichenfolgen und null darstellen, wird der Wert der
Konstante angezeigt.
Für numerische Typen, die über Standardsuffixe als C#-Literale verfügen, wird das Suffix zum Wert hinzugefügt.
Die folgende Tabelle zeigt die verschiedenen numerischen Typen und ihre zugeordneten Suffixe.

TYPE SC H L ÜSSEL W O RT SUF F IX

System.UInt32 uint U

System.Int64 long L

System.UInt64 ulong UL

System.Double double D

System.Single float F

System.Decimal decimal M

Beispiele
int num = 10;
ConstantExpression expr = Expression.Constant(num);
/*
10
*/

double num = 10;


ConstantExpression expr = Expression.Constant(num);
/*
10D
*/

BlockExpression
Wenn sich der Typ eines BlockExpression-Objekts vom Typ des letzten Ausdrucks im Block unterscheidet, wird
der Typ in spitzen Klammern ( < und > ) angezeigt. Andernfalls wird der Typ des BlockExpression-Objekts nicht
angezeigt.
Beispiele

BlockExpression block = Expression.Block(Expression.Constant("test"));


/*
.Block() {
"test"
}
*/

BlockExpression block = Expression.Block(typeof(Object), Expression.Constant("test"));


/*
.Block<System.Object>() {
"test"
}
*/

LambdaExpression.
LambdaExpression-Objekte werden zusammen mit ihren Delegattypen angezeigt.
Wenn ein Lambda-Ausdruck über keinen Namen verfügt, wird ihm ein automatisch generierter Name
zugewiesen, z.B. #Lambda1 oder #Lambda2 .
Beispiele

LambdaExpression lambda = Expression.Lambda<Func<int>>(Expression.Constant(1));


/*
.Lambda #Lambda1<System.Func'1[System.Int32]>() {
1
}
*/

LambdaExpression lambda = Expression.Lambda<Func<int>>(Expression.Constant(1), "SampleLambda", null);


/*
.Lambda #SampleLambda<System.Func'1[System.Int32]>() {
1
}
*/

LabelExpression
Wenn Sie einen Standardwert für das LabelExpression-Objekt angeben, wird dieser Wert vor dem LabelTarget-
Objekt angezeigt.
Das .Label -Token gibt den Anfang der Bezeichnung an. Das .LabelTarget -Token gibt den Zielort an, zu dem
gewechselt werden soll.
Wenn eine Bezeichnung nicht über einen Namen verfügt, wird ihr ein automatisch generierter Name
zugewiesen (z.B. #Label1 oder #Label2 ).
Beispiele

LabelTarget target = Expression.Label(typeof(int), "SampleLabel");


BlockExpression block = Expression.Block(
Expression.Goto(target, Expression.Constant(0)),
Expression.Label(target, Expression.Constant(-1))
);
/*
.Block() {
.Goto SampleLabel { 0 };
.Label
-1
.LabelTarget SampleLabel:
}
*/

LabelTarget target = Expression.Label();


BlockExpression block = Expression.Block(
Expression.Goto(target),
Expression.Label(target)
);
/*
.Block() {
.Goto #Label1 { };
.Label
.LabelTarget #Label1:
}
*/

Überprüfte Operatoren
Überprüften Operatoren wird in der Ansicht das # -Symbol vorangestellt. Der überprüfte Additionsoperator
wird z.B. als #+ angezeigt.
Beispiele

Expression expr = Expression.AddChecked( Expression.Constant(1), Expression.Constant(2));


/*
1 #+ 2
*/

Expression expr = Expression.ConvertChecked( Expression.Constant(10.0), typeof(int));


/*
#(System.Int32)10D
*/
Iteratoren (C#)
04.11.2021 • 7 minutes to read

Ein Iterator kann verwendet werden, um Auflistungen wie Listen und Arrays schrittweise durchzugehen.
Eine Iteratormethode oder eine get -Zugriffsmethode führt eine benutzerdefinierte Iteration einer Auflistung
durch. Eine Iteratormethode verwendet die yield return-Anweisung, um jedes Element einzeln nacheinander
zurückzugeben. Wenn eine yield return -Anweisung erreicht wird, wird die aktuelle Position im Code
gespeichert. Wenn die Iteratorfunktion das nächste Mal aufgerufen wird, wird die Ausführung von dieser
Position neu gestartet.
Sie erzeugen einen Iterator aus einem Clientcode, indem Sie eine foreach-Anweisung oder eine LINQ-Abfrage
verwenden.
In folgendem Beispiel führt die erste Iteration der foreach -Schleife dazu, dass die Ausführung solange in der
Iteratormethode SomeNumbers fortschreitet, bis die erste yield return -Anweisung erreicht wird. Diese Iteration
gibt den Wert 3 zurück, und die aktuelle Postion in der Iteratormethode wird beibehalten. In der nächsten
Iteration der Schleife wird die Ausführung in der Iteratormethode da fortgesetzt, wo sie beendet wurde, und
endet dann wieder an einem yield return -Ausdruck. Diese Iteration gibt den Wert 5 zurück, und die aktuelle
Postion in der Iteratormethode wird beibehalten. Die Schleife wird beendet, wenn das Ende der Iteratormethode
erreicht wird.

static void Main()


{
foreach (int number in SomeNumbers())
{
Console.Write(number.ToString() + " ");
}
// Output: 3 5 8
Console.ReadKey();
}

public static System.Collections.IEnumerable SomeNumbers()


{
yield return 3;
yield return 5;
yield return 8;
}

Der Rückgabetyp einer Iteratormethode oder einer get -Zugriffsmethode kann IEnumerable, IEnumerable<T>,
IEnumerator oder IEnumerator<T> sein.
Sie verwenden eine yield break -Anweisung, um die Iteration zu beenden.

NOTE
Verwenden Sie in allen Beispielen dieses Themas, außer Einfacher Iterator, die Direktiven using für die Namespaces
System.Collections und System.Collections.Generic .

Einfacher Iterator
In folgendem Beispiel wird eine yield return -Anweisung verwendet, die sich innerhalb einer for-Schleife
befindet. In der Main -Methode erstellt jede Iteration des foreach -Anweisungstexts einen Aufruf der
Iteratorfunktion, die zur nächsten yield return -Anweisung übergeht.

static void Main()


{
foreach (int number in EvenSequence(5, 18))
{
Console.Write(number.ToString() + " ");
}
// Output: 6 8 10 12 14 16 18
Console.ReadKey();
}

public static System.Collections.Generic.IEnumerable<int>


EvenSequence(int firstNumber, int lastNumber)
{
// Yield even numbers in the range.
for (int number = firstNumber; number <= lastNumber; number++)
{
if (number % 2 == 0)
{
yield return number;
}
}
}

Erstellen einer Auflistungsklasse


In folgendem Beispiel implementiert die DaysOfTheWeek -Klasse die IEnumerable-Schnittstelle, die eine
GetEnumerator-Methode erfordert. Der Compiler ruft die Methode GetEnumerator implizit auf, die IEnumerator
zurückgibt.
Die Methode GetEnumerator gibt jede Zeichenfolge einzeln nacheinander mithilfe der Anweisung yield return
zurück.

static void Main()


{
DaysOfTheWeek days = new DaysOfTheWeek();

foreach (string day in days)


{
Console.Write(day + " ");
}
// Output: Sun Mon Tue Wed Thu Fri Sat
Console.ReadKey();
}

public class DaysOfTheWeek : IEnumerable


{
private string[] days = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

public IEnumerator GetEnumerator()


{
for (int index = 0; index < days.Length; index++)
{
// Yield each day of the week.
yield return days[index];
}
}
}

Im folgenden Beispiel wird eine Zoo -Klasse erstellt, die eine Auflistung von Tieren enthält.
Die Anweisung foreach , die auf die Instanz der Klasse verweist ( theZoo ), ruft die Methode GetEnumerator
implizit auf. Die Anweisung foreach , die auf die Eigenschaften Birds und Mammals verweist, verwenden die
Iteratormethode AnimalsForType .

static void Main()


{
Zoo theZoo = new Zoo();

theZoo.AddMammal("Whale");
theZoo.AddMammal("Rhinoceros");
theZoo.AddBird("Penguin");
theZoo.AddBird("Warbler");

foreach (string name in theZoo)


{
Console.Write(name + " ");
}
Console.WriteLine();
// Output: Whale Rhinoceros Penguin Warbler

foreach (string name in theZoo.Birds)


{
Console.Write(name + " ");
}
Console.WriteLine();
// Output: Penguin Warbler

foreach (string name in theZoo.Mammals)


{
Console.Write(name + " ");
}
Console.WriteLine();
// Output: Whale Rhinoceros

Console.ReadKey();
}

public class Zoo : IEnumerable


{
// Private members.
private List<Animal> animals = new List<Animal>();

// Public methods.
public void AddMammal(string name)
{
animals.Add(new Animal { Name = name, Type = Animal.TypeEnum.Mammal });
}

public void AddBird(string name)


{
animals.Add(new Animal { Name = name, Type = Animal.TypeEnum.Bird });
}

public IEnumerator GetEnumerator()


{
foreach (Animal theAnimal in animals)
{
yield return theAnimal.Name;
}
}

// Public members.
public IEnumerable Mammals
{
get { return AnimalsForType(Animal.TypeEnum.Mammal); }
}
public IEnumerable Birds
{
get { return AnimalsForType(Animal.TypeEnum.Bird); }
}

// Private methods.
private IEnumerable AnimalsForType(Animal.TypeEnum type)
{
foreach (Animal theAnimal in animals)
{
if (theAnimal.Type == type)
{
yield return theAnimal.Name;
}
}
}

// Private class.
private class Animal
{
public enum TypeEnum { Bird, Mammal }

public string Name { get; set; }


public TypeEnum Type { get; set; }
}
}

Verwenden von Iteratoren mit einer generischen Liste


In folgendem Beispiel implementiert die generische Klasse Stack<T> die generische Schnittstelle
IEnumerable<T>. Die Methode Push weist Werte an Arrays des Typs T zu. Die GetEnumerator-Methode gibt
die Arraywerte mit der yield return -Anweisung zurück.
Zusätzlich zur generischen Methode GetEnumerator muss auch die nicht generische Methode GetEnumerator
implementiert werden. Dies liegt daran, dass IEnumerable<T> von IEnumerable erbt. Die nicht generische
Implementierung verzögert die generische Implementierung.
In diesem Beispiel werden benannte Iteratoren verwendet, um verschiedene Arten der Iteration in der selben
Datenauflistung zu unterstützen. Diese benannten Iteratoren sind die Eigenschaften TopToBottom und
BottomToTop und die Methode TopN .

Die Eigenschaft BottomToTop verwendet einen Iterator in einer get -Zugriffsmethode.

static void Main()


{
Stack<int> theStack = new Stack<int>();

// Add items to the stack.


for (int number = 0; number <= 9; number++)
{
theStack.Push(number);
}

// Retrieve items from the stack.


// foreach is allowed because theStack implements IEnumerable<int>.
foreach (int number in theStack)
{
Console.Write("{0} ", number);
}
Console.WriteLine();
// Output: 9 8 7 6 5 4 3 2 1 0

// foreach is allowed, because theStack.TopToBottom returns IEnumerable(Of Integer).


foreach (int number in theStack.TopToBottom)
foreach (int number in theStack.TopToBottom)
{
Console.Write("{0} ", number);
}
Console.WriteLine();
// Output: 9 8 7 6 5 4 3 2 1 0

foreach (int number in theStack.BottomToTop)


{
Console.Write("{0} ", number);
}
Console.WriteLine();
// Output: 0 1 2 3 4 5 6 7 8 9

foreach (int number in theStack.TopN(7))


{
Console.Write("{0} ", number);
}
Console.WriteLine();
// Output: 9 8 7 6 5 4 3

Console.ReadKey();
}

public class Stack<T> : IEnumerable<T>


{
private T[] values = new T[100];
private int top = 0;

public void Push(T t)


{
values[top] = t;
top++;
}
public T Pop()
{
top--;
return values[top];
}

// This method implements the GetEnumerator method. It allows


// an instance of the class to be used in a foreach statement.
public IEnumerator<T> GetEnumerator()
{
for (int index = top - 1; index >= 0; index--)
{
yield return values[index];
}
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

public IEnumerable<T> TopToBottom


{
get { return this; }
}

public IEnumerable<T> BottomToTop


{
get
{
for (int index = 0; index <= top - 1; index++)
{
yield return values[index];
}
}
}
}

public IEnumerable<T> TopN(int itemsFromTop)


{
// Return less than itemsFromTop if necessary.
int startIndex = itemsFromTop >= top ? 0 : top - itemsFromTop;

for (int index = top - 1; index >= startIndex; index--)


{
yield return values[index];
}
}

Syntaxinformationen
Ein Iterator kann als Methode oder als get -Zugriffsmethode vorkommen. Ein Iterator kann nicht in einem
Ereignis, Instanzenkonstruktor, statischen Konstruktor oder statischen Finalizer vorkommen.
Es muss eine implizite Konvertierung vom Typ des Ausdrucks in der yield return -Anweisung in das
Typargument für das vom Iterator zurückgegebene IEnumerable<T> vorhanden sein.
In C# kann eine Iteratormethode nicht die Parameter in , ref oder out aufweisen.
In C# ist yieldkein reserviertes Wort, und es hat nur besondere Bedeutung, wenn es vor den Schlüsselwörtern
return und break verwendet wird.

Technische Implementierung
Auch wenn Sie einen Iterator als Methode schreiben, führt der Compiler für diesen eine Translation in eine
geschachtelte Klasse durch, die tatsächlich ein Zustandsautomat ist. Diese Klasse überwacht die Position des
Iterators solange, wie die foreach -Schleife im Clientcode weiter ausgeführt wird.
Wenn Sie sehen möchten, was der Compiler macht, können Sie das Tool Ildasm.exe verwenden, um den
Intermediate Language-Code von Microsoft anzuzeigen, der für eine Iteratormethode generiert wird.
Wenn Sie einen Iterator für eine Klasse oder Struktur erstellen, müssen Sie die gesamte IEnumerator-
Schnittstelle implementieren. Wenn der Compiler einer Iterator erkennt, generiert er automatisch die Methoden
Current , MoveNext und Dispose der IEnumerator- und IEnumerator<T>-Schnittstelle.

In jeder aufeinanderfolgenden Iteration der foreach -Schleife (oder im direkten Aufruf von
IEnumerator.MoveNext ) setzt der nächste Iteratorcodetext den Prozess nach der letzten yield return -Anweisung
fort. Er springt dann zur nächsten yield return -Anweisung weiter, bis das Ende des Iteratortexts erreicht ist,
oder bis er auf eine yield break -Anweisung trifft.
Iteratoren unterstützen die IEnumerator.Reset-Methode nicht. Wenn der Prozess erneut von Anfang an
durchlaufen werden soll, müssen Sie einen neuen Iterator erstellen. Durch das Aufrufen von Reset für den von
einer Iteratormethode zurückgegebenen Iterator wird NotSupportedException ausgelöst.
Weitere Informationen finden Sie unter C#-Programmiersprachenspezifikation.

Verwendung von Iteratoren


Mit Iteratoren können Sie die Einfachheit einer foreach -Schleife beibehalten, wenn Sie komplexen Code
verwenden müssen, um eine Listensequenz aufzufüllen. Das kann für Folgende Aktionen nützlich sein:
Das Modifizieren der Listensequenz nach der ersten Iteration einer foreach -Schleife.
Das Vermeiden des kompletten Ladens einer großen Liste vor der ersten Iteration einer foreach -Schleife.
Ein Beispiel dafür ist das ausgelagerte Abrufen, um einen Batch von Tabellenzeilen zu laden. Ein anderes
Beispiel ist die EnumerateFiles-Methode, die Iteratoren in .NET implementiert.
Das Einschließen des Erstellens der Liste im Iterator. In der Iteratormethode können Sie die Liste erstellen
und anschließend jedes Ergebnis in eine Schleife liefern.

Siehe auch
System.Collections.Generic
IEnumerable<T>
foreach, in
yield
Verwenden von foreach mit Arrays
Generics
Language Integrated Query (LINQ) (C#)
04.11.2021 • 3 minutes to read

Language Integrated Query (LINQ) bezeichnet einen Satz Technologien, die auf der direkten Integration der
Abfragefunktionen in die Sprache C# basieren. Abfragen von Daten werden gewöhnlich als einfache
Zeichenfolgen ohne Typüberprüfung zur Kompilierzeit und ohne IntelliSense-Unterstützung ausgedrückt.
Außerdem müssen Sie für jeden Datenquellentyp eine andere Abfragesprache lernen: SQL-Datenbanken, XML-
Dokumente, verschiedene Webdienste usw. Bei LINQ ist eine Abfrage ein erstklassiges Sprachkonstrukt,
genauso wie Klassen, Methoden oder Ereignisse. Sie schreiben Abfragen für stark typisierte Auflistungen von
Objekten mithilfe von Sprachschlüsselwörtern und bekannten Operatoren. Die LINQ-Technologiefamilie bietet
ein konsistentes Abfrageerlebnis für Objekte (LINQ to Objects), relationale Datenbanken (LINQ to SQL) und XML
(LINQ to XML).
Für einen Entwickler, der Abfragen schreibt, ist der sichtbarste „sprachintegrierte“ Teil von LINQ der
Abfrageausdruck. Abfrageausdrücke werden in einer deklarativen Abfragesyntax geschrieben. Mit der
Abfragesyntax können Sie mit minimalem Codeeinsatz Filter-, Sortier- und Gruppiervorgänge in Datenquellen
ausführen. Sie verwenden die gleichen grundlegenden Abfrageausdrucksmuster, um Daten in SQL-Datenbank-
Instanzen, ADO.NET-Datasets, XML-Dokumenten und -Streams sowie .NET-Sammlungen abzufragen und zu
transformieren.
Sie können LINQ-Abfragen in C# für SQL Server-Datenbanken, XML-Dokumente, ADO.NET-Datasets und jede
Auflistung von Objekten schreiben, die IEnumerable oder die generische IEnumerable<T>-Schnittstelle
unterstützt. LINQ-Unterstützung wird auch von Drittanbietern für viele Webdienste und andere
Datenbankimplementierungen bereitgestellt.
Das folgende Beispiel zeigt den vollständigen Abfragevorgang. Der vollständige Vorgang umfasst die Erstellung
einer Datenquelle, die Definition des Abfrageausdrucks und die Ausführung der Abfrage in einer foreach -
Anweisung.

class LINQQueryExpressions
{
static void Main()
{

// Specify the data source.


int[] scores = new int[] { 97, 92, 81, 60 };

// Define the query expression.


IEnumerable<int> scoreQuery =
from score in scores
where score > 80
select score;

// Execute the query.


foreach (int i in scoreQuery)
{
Console.Write(i + " ");
}
}
}
// Output: 97 92 81

Die folgende Abbildung aus Visual Studio zeigt eine teilweise abgeschlossene LINQ-Abfrage für eine SQL
Server-Datenbank in C# und Visual Basic mit vollständiger Typüberprüfung und IntelliSense-Unterstützung:
Übersicht über Abfrageausdrücke
Abfrageausdrücke können verwendet werden, um Daten aus einer beliebigen LINQ-fähigen Datenquelle
abzufragen und zu transformieren. Mit einer einzigen Abfrage können z.B. Daten aus einer SQL-Datenbank
abgerufen und ein XML-Stream als Ausgabe generiert werden.
Die Arbeit mit Abfrageausdrücken ist einfach, da sie viele vertraute Konstrukte der Sprache C# verwenden.
Alle Variablen in einem Abfrageausdruck sind stark typisiert, obwohl Sie den Typ in vielen Fällen nicht explizit
angeben müssen, da der Compiler ihn ableiten kann. Weitere Informationen finden Sie unter
Typbeziehungen in LINQ-Abfragevorgängen.
Eine Abfrage wird erst ausgeführt, wenn Sie die Abfragevariable durchlaufen, z.B. in einer foreach -
Anweisung. Weitere Informationen finden Sie unter Einführung in LINQ-Abfragen.
Zur Kompilierzeit werden Abfrageausdrücke gemäß den in der C#-Spezifikation festgelegten Regeln in
Methodenaufrufe des Standardabfrageoperators konvertiert. Jede Abfrage, die mithilfe der Abfragesyntax
ausgedrückt werden kann, kann auch mithilfe der Methodensyntax ausgedrückt werden. In den meisten
Fällen ist aber die Abfragesyntax präziser und besser lesbar. Weitere Informationen finden Sie unter
Spezifikation für die Sprache C# und Übersicht über Standardabfrageoperatoren.
Beim Schreiben von LINQ-Abfragen empfiehlt sich diese Faustregel: Verwenden Sie die Abfragesyntax, wann
immer es möglich ist, und verwenden Sie die Methodensyntax nur, wenn es nötig ist. Zwischen den beiden
Formen gibt es keine semantischen oder leistungsbezogenen Unterschiede. Abfrageausdrücke sind oft
besser lesbar als die entsprechenden in der Methodensyntax geschriebenen Ausdrücke.
Für einige Abfragevorgänge, wie z.B. Count oder Max, gibt es keine entsprechende Abfrageausdrucksklausel,
daher müssen diese als Methodenaufruf ausgedrückt werden. Die Methodensyntax kann auf verschiedene
Weise mit der Abfragesyntax kombiniert werden. Weitere Informationen finden Sie unter Abfragesyntax und
Methodensyntax in LINQ.
Abfrageausdrücke können in Ausdrucksbaumstrukturen oder Delegaten kompiliert werden, je nachdem, auf
welchen Typ die Abfrage angewendet wird. IEnumerable<T>-Abfragen werden zu Delegaten kompiliert.
IQueryable- und IQueryable<T>-Abfragen werden zu Ausdrucksbaumstrukturen kompiliert. Weitere
Informationen finden Sie unter Ausdrucksbaumstrukturen.

Nächste Schritte
Um mehr zu LINQ zu erfahren, machen Sie sich zunächst unter Grundlagen zu Abfrageausdrücken mit einigen
grundlegenden Konzepten vertraut, und lesen Sie dann die Dokumentation für die LINQ-Technologie, die Sie
interessiert:
XML-Dokumente: LINQ to XML
ADO.NET Entity Framework: LINQ to Entities
.NET-Collections, -Dateien, -Zeichenfolgen usw.: LINQ to Objects
Tiefer greifende Einblicke in LINQ im Allgemeinen erhalten Sie unter LINQ in C#.
Informationen zu den ersten Schritten mit LINQ in C# erhalten Sie im Tutorial Arbeiten mit LINQ.
Einführung in LINQ-Abfragen (C#)
04.11.2021 • 5 minutes to read

Eine Abfrage ist ein Ausdruck, der Daten von einer Datenquelle abruft. Abfragen werden normalerweise in einer
spezialisierten Abfragesprache ausgedrückt. Im Laufe der Zeit wurden verschiedene Sprachen für die
verschiedenen Datenquellen entwickelt, beispielsweise SQL für relationale Datenbanken und XQuery für XML.
Aus diesem Grund mussten Entwickler für jeden Typ von Datenquelle oder Datenformat, den sie unterstützen
müssen, eine neue Abfragesprache erlernen. LINQ vereinfacht diese Situation durch die Bereitstellung eines
konsistenten Modells zum Arbeiten mit Daten in verschiedenen Arten von Datenquellen und Formaten. In einer
LINQ-Abfrage arbeiten Sie immer mit Objekten. Sie verwenden dieselben grundlegenden Codierungsmuster für
die Abfrage und Transformation von Daten in XML-Dokumenten, SQL-Datenbanken, ADO.NET-Datasets, .NET-
Auflistungen sowie allen anderen Quellen und Formaten, für die ein LINQ-Anbieter verfügbar ist.

Drei Teile einer Abfrageoperation


Alle LINQ-Abfrageoperationen bestehen aus drei unterschiedlichen Aktionen:
1. Abrufen der Datenquelle
2. Erstellen der Abfrage
3. Ausführen der Abfrage
Im folgenden Beispiel wird gezeigt, wie die drei Teile einer Abfrageoperation in Quellcode ausgedrückt werden.
Das Beispiel verwendet aus praktischen Gründen ein Array von Ganzzahlen als Datenquelle. Dieselben Konzepte
gelten jedoch auch für andere Datenquellen. Auf dieses Beispiel wird im Rest dieses Themas Bezug genommen.

class IntroToLINQ
{
static void Main()
{
// The Three Parts of a LINQ Query:
// 1. Data source.
int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };

// 2. Query creation.
// numQuery is an IEnumerable<int>
var numQuery =
from num in numbers
where (num % 2) == 0
select num;

// 3. Query execution.
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num);
}
}
}

Die folgende Abbildung zeigt die vollständige Abfrageoperation. In LINQ unterscheidet sich die Ausführung der
Abfrage von der Abfrage selbst. Anders ausgedrückt: Sie haben keine Daten abgerufen, indem Sie nur eine
Abfragevariable erstellen.
Die Datenquelle
Da es sich bei der Datenquelle im vorherigen Beispiel um ein Array handelt, unterstützt sie implizit die
generische IEnumerable<T>-Schnittstelle. Das bedeutet, dass sie mit L abgefragt werden kann. Eine Abfrage
wird in einer foreach -Anweisung ausgeführt, und foreach erfordert IEnumerable oder IEnumerable<T>.
Typen, die IEnumerable<T> unterstützen oder eine abgeleitete Schnittstelle, wie z.B. der generische Typ
IQueryable<T>, werden als abfragbare Typen bezeichnet.
Für abfragbare Typen ist keine Änderung oder besondere Behandlung notwendig, um sie als LINQ-Datenquelle
zu verwenden. Wenn die Quelldaten nicht bereits als abfragbarer Typ im Arbeitsspeicher vorhanden sind, muss
der LINQ-Anbieter diese als solcher darstellen. Zum Beispiel lädt LINQ to XML ein XML-Dokument in einen
abfragbaren XElement-Typ:

// Create a data source from an XML document.


// using System.Xml.Linq;
XElement contacts = XElement.Load(@"c:\myContactList.xml");

Mit LINQ to SQL erstellen Sie zuerst eine objektrelationale Zuordnung zur Entwurfszeit, entweder manuell oder
mit den LINQ to SQL-Tools in Visual Studio. Sie schreiben die Abfragen anhand der Objekte, und zur Laufzeit
übernimmt LINQ to SQL die Kommunikation mit der Datenbank. Im folgenden Beispiel stellt Customers eine
bestimmte Tabelle in der Datenbank dar, und der Typ des Abfrageergebnisses, IQueryable<T>, wird von
IEnumerable<T> abgeleitet.

Northwnd db = new Northwnd(@"c:\northwnd.mdf");

// Query for customers in London.


IQueryable<Customer> custQuery =
from cust in db.Customers
where cust.City == "London"
select cust;

Weitere Informationen zum Erstellen bestimmter Typen von Datenquellen finden Sie in der Dokumentation der
verschiedenen LINQ-Anbieter. Die Grundregel ist jedoch sehr einfach: Eine LINQ-Datenquelle ist jedes Objekt,
das die generische IEnumerable<T>-Schnittstelle oder eine Schnittstelle unterstützt, die davon erbt.
NOTE
Typen wie ArrayList, die die nicht generische IEnumerable-Schnittstelle unterstützen, können ebenso als LINQ-Datenquelle
verwendet werden. Weitere Informationen finden Sie unter Vorgehensweise: Abfragen von ArrayList mit LINQ (C#).

Die Abfrage
Die Abfrage gibt an, welche Informationen aus der Datenquelle oder den Datenquellen abgerufen werden sollen.
Optional kann eine Abfrage auch angeben, wie diese Informationen vor der Rückgabe sortiert, gruppiert und
strukturiert werden sollen. Eine Abfrage wird in einer Abfragevariablen gespeichert und mit einem
Abfrageausdruck initialisiert. Um das Schreiben von Abfragen zu erleichtern, hat C# eine neue Abfragesyntax
eingeführt.
Die Abfrage im vorherigen Beispiel gibt alle geraden Zahlen aus einem Ganzzahlen-Array zurück. Der
Abfrageausdruck enthält drei Klauseln: from , where und select . (Wenn Sie mit SQL vertraut sind, ist Ihnen
wahrscheinlich aufgefallen, dass die Klauseln umgekehrt wie in SQL angeordnet sind.) Die from -Klausel gibt die
Datenquelle an, die where -Klausel wendet den Filter an, und die select -Klausel gibt den Typ der
zurückgegebenen Elemente an. Diese und die anderen Abfrageklauseln werden im Abschnitt Language
Integrated Query (LINQ) ausführlich besprochen. Wichtig ist hier, dass die Abfragevariable selbst in LINQ keine
Aktion ausführt und keine Daten zurückgibt. Sie speichert nur die Informationen, die erforderlich sind, um
Ergebnisse zu erzeugen, wenn die Abfrage zu einem späteren Zeitpunkt ausgeführt wird. Weitere Informationen
zum Erstellen von Abfragen hinter den Kulissen finden Sie unter Übersicht über Standardabfrageoperatoren
(C#).

NOTE
Abfragen können auch unter Verwendung der Methodensyntax ausgedrückt werden. Weitere Informationen finden Sie
unter Abfragesyntax und Methodensyntax in LINQ.

Abfrageausführung
Verzögerte Ausführung
Wie bereits erwähnt, speichert die Abfragevariable selbst nur die Abfragebefehle. Die tatsächliche Ausführung
der Abfrage wird so lange verzögert, bis Sie die Abfragevariable in einer foreach -Anweisung durchlaufen.
Dieses Konzept wird als verzögerte Ausführung bezeichnet und wird im folgenden Beispiel veranschaulicht:

// Query execution.
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num);
}

In der foreach -Anweisung werden auch die Abfrageergebnisse abgerufen. So ist beispielsweise in der
vorherigen Abfrage in der Iterationsvariablen num jeder Wert ( jeweils einzeln) der zurückgegebenen Sequenz
enthalten.
Da die Abfragevariable selbst nie die Abfrageergebnisse enthält, können Sie sie beliebig oft ausführen. Sie
könnten beispielsweise über eine Datenbank verfügen, die ständig durch eine separate Anwendung aktualisiert
wird. Sie könnten in Ihrer Anwendung eine Abfrage erstellen, die die neuesten Daten abruft, und Sie könnten
diese Abfrage in bestimmten Abständen wiederholt ausführen, um bei jeder Ausführung andere Ergebnisse
abzurufen.
Erzwingen der unmittelbaren Ausführung
Abfragen, die Aggregationsfunktionen für einen Bereich von Quellelementen ausführen, müssen zuerst diese
Elemente durchlaufen. Beispiele für solche Abfragen sind Count , Max , Average und First . Diese Abfragen
werden ohne explizite foreach -Anweisung ausgeführt, da die Abfrage selbst foreach verwenden muss, um ein
Ergebnis auszugeben. Beachten Sie auch, dass diese Typen von Abfragen einen einzelnen Wert und keine
IEnumerable -Auflistung zurückgeben. Die folgende Abfrage gibt eine Anzahl der geraden Zahlen im Quellarray
zurück:

var evenNumQuery =
from num in numbers
where (num % 2) == 0
select num;

int evenNumCount = evenNumQuery.Count();

Um die unmittelbare Ausführung einer Abfrage zu erzwingen und ihre Ergebnisse zwischenzuspeichern, können
Sie die ToList-Methode oder die ToArray-Methode aufrufen.

List<int> numQuery2 =
(from num in numbers
where (num % 2) == 0
select num).ToList();

// or like this:
// numQuery3 is still an int[]

var numQuery3 =
(from num in numbers
where (num % 2) == 0
select num).ToArray();

Sie können die Ausführung auch erzwingen, indem Sie die foreach-Schleife unmittelbar nach dem
Abfrageausdruck setzen. Durch Aufrufen von ToList oder ToArray speichern Sie jedoch auch alle Daten in
einem einzelnen Auflistungsobjekt zwischen.

Siehe auch
Erste Schritte mit LINQ in C#
Exemplarische Vorgehensweise: Schreiben von Abfragen in C#
Language-Integrated Query (LINQ)
foreach, in
Abfrageschlüsselwörter (LINQ)
LINQ und generische Typen (C#)
04.11.2021 • 2 minutes to read

LINQ-Abfragen basieren auf generischen Typen, die mit Version 2.0 von .NET Framework eingeführt wurden. Sie
benötigen kein ausführliches Wissen über Generics, um Abfragen schreiben zu können. Dennoch sollten Sie
zwei grundlegende Konzepte verstehen:
1. Wenn Sie eine Instanz einer generischen Auflistungsklasse wie etwa List<T> erstellen, ersetzen Sie das
„T“ durch den Objekttyp, den die Liste enthalten wird. Eine Liste von Zeichenfolgen wird z.B. als
List<string> und eine Liste von Customer -Objekten als List<Customer> ausgedrückt. Eine generische
Liste ist stark typisiert und hat gegenüber Auflistungen, die ihre Elemente als Object speichern, viele
Vorzüge. Wenn Sie versuchen einen Customer in eine List<string> einzufügen, erhalten Sie zur Laufzeit
eine Fehlermeldung. Es ist sehr leicht, generische Auflistungen zu verwenden, da Sie keine
Laufzeitumwandlung von Typen durchführen müssen.
2. IEnumerable<T> ist die Schnittstelle, die es ermöglicht, dass generische Auflistungsklassen mithilfe der
Anweisung foreach aufgelistet werden. Generische Auflistungsklassen unterstützen IEnumerable<T>,
während nicht generische Auflistungsklassen, wie etwa ArrayList, IEnumerable unterstützen.
Weitere Informationen zu Generics finden Sie unter Generics.

IEnumerable<T>-Variablen in LINQ-Abfragen
LINQ-Abfragevariablen werden als IEnumerable<T> typisiert oder als ein abgeleiteter Typ, etwa IQueryable<T>.
Wenn Sie eine Abfragevariable des Typs IEnumerable<Customer> sehen, bedeutet dies nur, dass die Abfrage bei
der Ausführung eine Folge von null oder mehr Customer -Objekten produziert.

IEnumerable<Customer> customerQuery =
from cust in customers
where cust.City == "London"
select cust;

foreach (Customer customer in customerQuery)


{
Console.WriteLine(customer.LastName + ", " + customer.FirstName);
}

Weitere Informationen finden Sie unter Typbeziehungen in LINQ-Abfragevorgängen.

Verarbeiten von generischen Typdeklaration durch den Compiler


Falls Sie dies möchten, können Sie generische Syntax umgehen, indem Sie das Schlüsselwort var verwenden.
Das Schlüsselwort var weist den Compiler an, den Typ der Abfragevariablen abzuleiten, indem er sich an der in
der from -Klausel angegebenen Datenquelle orientiert. Das folgende Beispiel erstellt den gleichen kompilierten
Code wie das vorherige Beispiel:
var customerQuery2 =
from cust in customers
where cust.City == "London"
select cust;

foreach(var customer in customerQuery2)


{
Console.WriteLine(customer.LastName + ", " + customer.FirstName);
}

Das Schlüsselwort var ist nützlich, wenn der Typ der Variablen offensichtlich ist, oder wenn es nicht so wichtig
ist, die geschachtelten generischen Typen explizit festzulegen, wie z.B. diejenigen, die in Gruppenabfragen erstellt
werden. Allgemein wird darauf hingewiesen, dass das Verwenden von var das Lesen Ihres Codes durch andere
erschwert. Weitere Informationen zu finden Sie unter Implizit typisierte lokale Variablen.

Siehe auch
Generics
Grundlegende LINQ-Abfragevorgänge (C#)
04.11.2021 • 4 minutes to read

Dieses Thema gibt einen kurzen Überblick über LINQ-Abfrageausdrücke und einige der geläufigsten Vorgänge,
die Sie in einer Abfrage durchführen können. Ausführlichere Informationen finden Sie unter den folgenden
Themen:
LINQ-Abfrageausdrücke
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Exemplarische Vorgehensweise: Schreiben von Abfragen in C#

NOTE
Wenn Sie sich bereits mit einer Abfragesprache wie SQL oder XQuery auskennen, können Sie einen Großteil dieses
Themas überspringen. Im nächsten Abschnitt erfahren Sie mehr über die from -Klausel und die Reihenfolge von Klauseln
in einem LINQ-Abfrageausdruck.

Abrufen einer Datenquelle


Der erste Schritt für eine LINQ-Abfrage ist das Festlegen einer Datenquelle. Wie in den meisten
Programmiersprachen muss eine Variable in C# vor ihrer Verwendung deklariert werden. In einer LINQ-Abfrage
steht die from -Klausel an erster Stelle; sie führt die Datenquelle ( customers ) und die Bereichsvariable ( cust )
ein.

//queryAllCustomers is an IEnumerable<Customer>
var queryAllCustomers = from cust in customers
select cust;

Die Bereichsvariable befindet sich wie die Iterationvariable in einer foreach -Schleife, mit dem Unterschied, dass
in einem Abfrageausdruck keine Iterationen vorkommen. Wenn die Abfrage ausgeführt wird, fungiert die
Bereichsvariable als Verweis auf jedes aufeinanderfolgende Element in customers . Es ist nicht notwendig, diese
explizit anzugeben, da der Compiler den Typ von cust ableitet. Weitere Bereichsvariablen können mithilfe der
let -Klausel eingefügt werden. Weitere Informationen finden Sie unter let-Klausel.

NOTE
Die Bereichsvariable muss für nicht generische Datenquellen wie ArrayList explizit typisiert sein. Weitere Informationen
finden Sie unter Vorgehensweise: Abfragen von ArrayList mit LINQ (C#) und from-Klausel.

Filtern
Die wahrscheinlich üblichste Abfrageoperation ist das Anwenden eines Filters in Form eines booleschen
Ausdrucks. Das Filtern bewirkt, dass die Abfrage nur jene Elemente zurückgibt, für die der Ausdruck den Wert
TRUE hat. Das Ergebnis wird durch Verwendung der where -Klausel erzeugt. Faktisch gibt der Filter an, welche
Elemente nicht in die Quellsequenz eingeschlossen werden sollen. In folgendem Beispiel werden nur die
customers zurückgegeben, die eine Londoner Adresse haben.
var queryLondonCustomers = from cust in customers
where cust.City == "London"
select cust;

Sie können die geläufigen logischen C#-Operatoren AND und OR verwenden, um so viele Filterausdrücke wie
benötigt in der where -Klausel anzuwenden. Wenn Sie z.B. nur Kunden aus „London“ zurückgeben möchten,
deren Name „Devon“ ist, können Sie dazu AND verwenden wie in folgendem Codebeispiel:

where cust.City == "London" && cust.Name == "Devon"

Um Kunden aus London oder Paris zurückzugeben, verwenden Sie folgenden Code:

where cust.City == "London" || cust.City == "Paris"

Weitere Informationen finden Sie unter where-Klausel.

Sortieren
Es kann häufig praktisch sein, die zurückgegebenen Daten zu sortieren. Die orderby -Klausel sortiert die
Elemente in der zurückgegebenen Sequenz anhand des Standardcomparers für den zu sortierenden Typ. Die
folgende Abfrage kann z.B. so erweitert werden, dass Sie die Ergebnisse nach der Eigenschaft Name sortiert. Der
Standardcomparer nimmt eine Sortierung in alphabetischer Reihenfolge (A bis Z) vor, das es sich bei Name um
eine Zeichenfolge handelt.

var queryLondonCustomers3 =
from cust in customers
where cust.City == "London"
orderby cust.Name ascending
select cust;

Wenn Sie die Ergebnisse andersherum, von Z bis A, sortieren möchten, können Sie die orderby…descending -
Klausel verwenden.
Weitere Informationen finden Sie unter orderby-Klausel.

Gruppierung
Die group -Klausel ermöglicht Ihnen, die Ergebnisse auf Grundlage eines von Ihnen angegebenen Schlüssels zu
gruppieren. Sie können z.B. angeben, dass die Ergebnisse nach City gruppiert werden sollen, sodass sich alle
Kunden aus London oder Paris in einer einzelnen Gruppe befinden. In diesem Fall ist cust.City der Schlüssel.
// queryCustomersByCity is an IEnumerable<IGrouping<string, Customer>>
var queryCustomersByCity =
from cust in customers
group cust by cust.City;

// customerGroup is an IGrouping<string, Customer>


foreach (var customerGroup in queryCustomersByCity)
{
Console.WriteLine(customerGroup.Key);
foreach (Customer customer in customerGroup)
{
Console.WriteLine(" {0}", customer.Name);
}
}

Wenn Sie eine Abfrage mit einer group -Klausel beenden, werden Ihre Ergebnisse in einer Liste aus Listen
zurückgegeben. Jedes Element auf der Liste ist ein Objekt, dass über einen Key -Member und eine Liste von
Elementen verfügt, die anhand dieses Schlüssels gruppiert wurden. Wenn Sie eine Abfrage durchlaufen, die eine
Sequenz von Gruppen erzeugt, müssen Sie eine geschachtelte foreach -Schleife verwenden. Die äußere Schleife
durchläuft jede Gruppe, und die innere Schleife durchläuft alle Member jeder Gruppe.
Wenn Sie auf die Ergebnisse eines Gruppenvorgangs verweisen müssen, könne Sie das Schlüsselwort into
verwenden, um einen Bezeichner zu erstellen, der weiter abgefragt werden kann. Die folgende Abfrage gibt nur
die Gruppen zurück, die mehr als zwei Kunden enthalten:

// custQuery is an IEnumerable<IGrouping<string, Customer>>


var custQuery =
from cust in customers
group cust by cust.City into custGroup
where custGroup.Count() > 2
orderby custGroup.Key
select custGroup;

Weitere Informationen finden Sie unter group-Klausel.

Verknüpfen
Verknüpfungsvorgänge erstellen Verknüpfungen zwischen Sequenzen, die nicht explizit in der Datenquelle
modelliert werden. Sie können z.B. eine Verknüpfung erstellen, um alle Kunden und Händler mit demselben
Standort ausfindig zu machen. In LINQ arbeitet die join -Klausel immer mit Objektsammlungen statt direkt mit
Datenbanktabellen.

var innerJoinQuery =
from cust in customers
join dist in distributors on cust.City equals dist.City
select new { CustomerName = cust.Name, DistributorName = dist.Name };

In L müssen Sie nicht join so häufig wie in SQL verwenden, da Fremdschlüssel in LINQ im Objektmodell als
Eigenschaften dargestellt werden, die eine Elementauflistung enthalten. Ein Customer -Objekt enthält z.B. eine
Auflistung von Order -Objekten. Sie können auf die Bestellungen zugreifen, indem Sie Punktnotation statt einer
Verknüpfung verwenden:

from order in Customer.Orders...

Weitere Informationen finden Sie unter join-Klausel.


Auswählen (Projektionen)
Die select -Klausel erzeugt die Ergebnisse der Abfrage und gibt die „Form“ oder den Typ jedes
zurückgegebenen Elements an. Sie können z.B. bestimmen, ob Ihre Ergebnisse aus vollständigen Customer -
Objekte, aus lediglich einem Member, aus einer Teilmenge von Membern oder aus einem ganz anderen
Ergebnistyp, der auf einer Berechnung oder einem neu erstellten Objekt basiert, bestehen sollen. Wenn die
select -Klausel etwas anderes als eine Kopie des Quellelements erzeugt, wird dieser Vorgang als Projektion
bezeichnet. Das Verwenden von Projektionen zur Datentransformation ist eine leistungsfähige Funktion von
LINQ-Abfrageausdrücken. Weitere Informationen finden Sie unter Datentransformationen mit LINQ (C#) und
select-Klausel.

Siehe auch
LINQ-Abfrageausdrücke
Exemplarische Vorgehensweise: Schreiben von Abfragen in C#
Abfrageschlüsselwörter (LINQ)
Anonyme Typen
Datentransformationen mit LINQ (C#)
04.11.2021 • 5 minutes to read

Bei Language Integrated Query (LINQ) geht es nicht nur um das Abrufen von Daten. Es ist auch ein
leistungsstarkes Tool zur Datentransformation. Mithilfe einer LINQ-Abfrage können Sie eine Quellsequenz als
Eingabe verwenden und sie auf viele Arten zum Erstellen einer neuen Ausgabesequenz ändern. Sie können die
Sequenz selbst durch Sortieren und Gruppieren ändern, ohne die Elemente zu ändern. Aber das vielleicht
leistungsstärkste Feature von LINQ-Abfragen ist die Fähigkeit, neue Typen zu erstellen. Dies erfolgt in der select-
Klausel. Sie können z. B. folgende Aufgaben ausführen:
Führen Sie mehrere Eingabesequenzen in einer einzelnen Ausgabesequenz, die einen neuen Typ hat,
zusammen.
Erstellen Sie Ausgabesequenzen, deren Elemente aus nur einer oder mehreren Eigenschaften jedes
Elements in der Quellsequenz bestehen.
Erstellen Sie Ausgabesequenzen, deren Elemente aus Ergebnissen der Vorgänge für die Quelldaten
bestehen.
Erstellen Sie Ausgabesequenzen in einem anderen Format. Beispielsweise können Sie Daten aus SQL-
Zeilen oder Textdateien in XML umwandeln.
Dies sind nur einige Beispiele. Natürlich können diese Transformationen auf verschiedene Weise in der gleichen
Abfrage kombiniert werden. Darüber hinaus kann die Ausgabesequenz einer Abfrage als Eingabesequenz für
eine neue Abfrage verwendet werden.

Verknüpfen mehrerer Eingaben in eine Ausgabesequenz


Sie können eine LINQ-Abfrage verwenden, um eine Ausgabesequenz zu erstellen, die Elemente von mehr als
einer Eingabesequenz enthält. Im folgenden Beispiel wird gezeigt, wie zwei Datenstrukturen im Arbeitsspeicher
kombiniert werden können, aber die gleichen Prinzipien können angewendet werden, um Daten von XML- oder
SQL- oder DataSet-Quellen zu kombinieren. Nehmen Sie die beiden folgenden Klassentypen an:

class Student
{
public string First { get; set; }
public string Last {get; set;}
public int ID { get; set; }
public string Street { get; set; }
public string City { get; set; }
public List<int> Scores;
}

class Teacher
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public string City { get; set; }
}

Das folgende Beispiel gibt die Abfrage an:


class DataTransformations
{
static void Main()
{
// Create the first data source.
List<Student> students = new List<Student>()
{
new Student { First="Svetlana",
Last="Omelchenko",
ID=111,
Street="123 Main Street",
City="Seattle",
Scores= new List<int> { 97, 92, 81, 60 } },
new Student { First="Claire",
Last="O’Donnell",
ID=112,
Street="124 Main Street",
City="Redmond",
Scores= new List<int> { 75, 84, 91, 39 } },
new Student { First="Sven",
Last="Mortensen",
ID=113,
Street="125 Main Street",
City="Lake City",
Scores= new List<int> { 88, 94, 65, 91 } },
};

// Create the second data source.


List<Teacher> teachers = new List<Teacher>()
{
new Teacher { First="Ann", Last="Beebe", ID=945, City="Seattle" },
new Teacher { First="Alex", Last="Robinson", ID=956, City="Redmond" },
new Teacher { First="Michiyo", Last="Sato", ID=972, City="Tacoma" }
};

// Create the query.


var peopleInSeattle = (from student in students
where student.City == "Seattle"
select student.Last)
.Concat(from teacher in teachers
where teacher.City == "Seattle"
select teacher.Last);

Console.WriteLine("The following students and teachers live in Seattle:");


// Execute the query.
foreach (var person in peopleInSeattle)
{
Console.WriteLine(person);
}

Console.WriteLine("Press any key to exit.");


Console.ReadKey();
}
}
/* Output:
The following students and teachers live in Seattle:
Omelchenko
Beebe
*/

Weitere Informationen finden Sie unter join-Klausel und select-Klausel.

Auswählen einer Teilmenge jedes Quellelements


Es gibt zwei Hauptmethoden, eine Teilmenge jedes Elements in der Quellsequenz auszuwählen:
1. Um nur einen Member des Quellelements auszuwählen, verwenden Sie die Punktoperation. Im
folgenden Beispiel wird angenommen, dass ein Customer -Objekt verschiedene öffentliche Eigenschaften
enthält, einschließlich der Zeichenfolge City . Bei der Ausführung erzeugt diese Abfrage eine
Ausgabesequenz von Zeichenfolgen.

var query = from cust in Customers


select cust.City;

2. Zum Erstellen von Elementen, die mehr als eine Eigenschaft aus dem Quellelement enthalten, können Sie
einen Objektinitialisierer mit einem benannten Objekt oder einen anonymen Typ verwenden. Das
folgende Beispiel zeigt die Verwendung eines anonymen Typs zum Kapseln zweier Eigenschaften von
jedem Customer -Element:

var query = from cust in Customer


select new {Name = cust.Name, City = cust.City};

Weitere Informationen finden Sie unter Objekt- und Auflistungsinitialisierer und Anonyme Typen.

Transformieren von Objekten im Speicher in XML


LINQ-Abfragen machen es einfacher, Daten zwischen Datenstrukturen im Speicher, SQL-Datenbanken,
ADO.NET-Datasets und XML-Streams oder XML-Dokumenten zu transformieren. Im folgenden Beispiel werden
Objekte in einer Datenstruktur im Arbeitsspeicher in XML-Elemente transformiert.

class XMLTransform
{
static void Main()
{
// Create the data source by using a collection initializer.
// The Student class was defined previously in this topic.
List<Student> students = new List<Student>()
{
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores = new List<int>{97, 92, 81,
60}},
new Student {First="Claire", Last="O’Donnell", ID=112, Scores = new List<int>{75, 84, 91, 39}},
new Student {First="Sven", Last="Mortensen", ID=113, Scores = new List<int>{88, 94, 65, 91}},
};

// Create the query.


var studentsToXML = new XElement("Root",
from student in students
let scores = string.Join(",", student.Scores)
select new XElement("student",
new XElement("First", student.First),
new XElement("Last", student.Last),
new XElement("Scores", scores)
) // end "student"
); // end "Root"

// Execute the query.


Console.WriteLine(studentsToXML);

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

Der Code erzeugt die folgende XML-Ausgabe:


<Root>
<student>
<First>Svetlana</First>
<Last>Omelchenko</Last>
<Scores>97,92,81,60</Scores>
</student>
<student>
<First>Claire</First>
<Last>O'Donnell</Last>
<Scores>75,84,91,39</Scores>
</student>
<student>
<First>Sven</First>
<Last>Mortensen</Last>
<Scores>88,94,65,91</Scores>
</student>
</Root>

Weitere Informationen finden Sie unter Erstellen von XML-Strukturen in C# (LINQ to XML).

Ausführen von Operationen für Quellelemente


Eine Ausgabesequenz enthält möglicherweise keine Elemente oder Elementeigenschaften aus der Quellsequenz.
Die Ausgabe kann möglicherweise stattdessen eine Sequenz von Werten enthalten, die durch Verwendung der
Quellelemente als Eingabeargumente berechnet wird.
Die folgende Abfrage übernimmt eine Zahlensequenz, die die Radien der Kreise darstellt, den Bereich für jeden
Radius berechnet und eine Ausgabesequenz zurückgibt, die Zeichenfolgen enthält, die mit dem berechneten
Bereich formatiert sind.
Jede Zeichenfolge für die Ausgabesequenz wird mithilfe der Zeichenfolgeninterpolation formatiert. Eine
interpolierte Zeichenfolge enthält $ vor dem öffnenden Anführungszeichen der Zeichenfolge, und Vorgänge
können in geschweiften Klammern innerhalb der interpolierten Zeichenfolge durchgeführt werden. Nachdem
diese Vorgänge ausgeführt wurden, werden die Ergebnisse verkettet.

NOTE
Aufrufen von Methoden in Abfrageausdrücken wird nicht unterstützt, wenn die Abfrage in eine andere Domäne übersetzt
wird. Sie können beispielsweise keine normale C#-Methode in LINQ to SQL aufrufen, da SQL Server über keinen Kontext
dafür verfügt. Sie können jedoch gespeicherte Prozeduren Methoden zuordnen und diese aufrufen. Weitere
Informationen finden Sie unter Gespeicherte Prozeduren.
class FormatQuery
{
static void Main()
{
// Data source.
double[] radii = { 1, 2, 3 };

// LINQ query using method syntax.


IEnumerable<string> output =
radii.Select(r => $"Area for a circle with a radius of '{r}' = {r * r * Math.PI:F2}");

/*
// LINQ query using query syntax.
IEnumerable<string> output =
from rad in radii
select $"Area for a circle with a radius of '{rad}' = {rad * rad * Math.PI:F2}";
*/

foreach (string s in output)


{
Console.WriteLine(s);
}

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Area for a circle with a radius of '1' = 3.14
Area for a circle with a radius of '2' = 12.57
Area for a circle with a radius of '3' = 28.27
*/

Siehe auch
Language Integrated Query (LINQ) (C#)
LINQ to SQL
LINQ to DataSet
LINQ to XML (C#)
LINQ-Abfrageausdrücke
select-Klausel
Typbeziehungen in LINQ-Abfragevorgängen (C#)
04.11.2021 • 2 minutes to read

Um Abfragen effektiv erstellen zu können, ist es wichtig, dass Sie verstehen, wie die Variablentypen in einer
vollständigen Abfrageoperation miteinander zusammenhängen. Wenn Sie diese Beziehungen verstehen,
können Sie die LINQ-Beispiele und die Codebeispiele in der Dokumentation besser nachvollziehen. Weiterhin
können Sie dann besser verstehen, was im Hintergrund abläuft, wenn Variablen implizit mithilfe von var
typisiert werden.
LINQ-Abfrageoperationen sind in der Datenquelle, in der Abfrage selbst und in der Abfrageausführung stark
typisiert. Die Variablentypen in der Abfrage müssen mit den Elementtypen in der Datenquelle und mit dem Typ
der Iterationsvariablen in der foreach -Anweisung kompatibel sein. Diese starke Typisierung stellt sicher, dass
Typfehler zur Kompilierzeit abgefangen werden, sodass sie korrigiert werden können, bevor die Benutzer sie
ausführen.
Um diese Typbeziehungen zu veranschaulichen, wird in den meisten folgenden Beispielen explizite Typisierung
für alle Variablen angewendet. Im letzten Beispiel wird gezeigt, wie diese Prinzipien auch dann gelten, wenn Sie
die implizite Typisierung mithilfe von var verwenden.

Abfragen, bei denen die Quelldaten nicht transformiert werden


Die folgende Abbildung zeigt eine LINQ to Objects-Abfrageoperation für Objekte, die keine Transformationen
der Daten ausführt. Die Quelle enthält eine Sequenz von Zeichenfolgen, und die Abfrageausgabe ist ebenfalls
eine Sequenz von Zeichenfolgen.

1. Das Typargument der Datenquelle bestimmt den Typ der Bereichsvariablen.


2. Der Typ des Objekts, das ausgewählt ist, bestimmt den Typ der Abfragevariablen. In diesem Beispiel ist
name eine Zeichenfolge. Daher ist die Abfragevariable ein IEnumerable<string> .

3. Die Abfragevariable durchläuft in der foreach -Anweisung verschiedene Iterationen. Da die


Abfragevariable eine Sequenz von Zeichenfolgen ist, ist die Iterationsvariable ebenfalls eine Zeichenfolge.

Abfragen, bei denen die Quelldaten transformiert werden


Die folgende Abbildung zeigt eine LINQ to SQL-Abfrageoperation, die eine einfache Datentransformation
ausführt. Die Abfrage verwendet eine Sequenz von Customer -Objekten als Eingabe und wählt nur die Name -
Eigenschaft im Ergebnis aus. Da Name eine Zeichenfolge ist, erzeugt die Abfrage eine Sequenz von
Zeichenfolgen als Ausgabe.
1. Das Typargument der Datenquelle bestimmt den Typ der Bereichsvariablen.
2. Die select -Anweisung gibt die Name -Eigenschaft statt des vollständigen Customer -Objekts zurück. Da
Name eine Zeichenfolge ist, lautet das Typargument von custNameQuery nicht Customer , sondern string .
3. Da custNameQuery eine Sequenz von Zeichenfolgen ist, muss die Iterationsvariable der foreach -Schleife
auch ein string sein.
Die folgende Abbildung zeigt eine etwas komplexere Transformation. Die select -Anweisung gibt einen
anonymen Typ zurück, der nur zwei Member des ursprünglichen Customer -Objekts erfasst.

1. Das Typargument der Datenquelle ist immer der Typ der Bereichsvariablen in der Abfrage.
2. Da die select -Anweisung einen anonymen Typ erzeugt, muss die Abfragevariable mithilfe von var
implizit typisiert werden.
3. Da der Typ der Abfragevariablen implizit ist, muss die Iterationsvariable in der foreach -Schleife auch
implizit sein.

Ableiten von Typinformationen durch den Compiler


Auch wenn Sie die Typbeziehungen einer Abfrageoperation grundsätzlich verstehen sollten, haben Sie die
Option, den Compiler alle Arbeitsschritte ausführen zu lassen. Das var-Schlüsselwort kann in einer
Abfrageoperation für jede lokale Variable verwendet werden. Die folgende Abbildung ähnelt Beispiel Nummer
2, das zuvor erläutert wurde. Allerdings stellt der Compiler den starken Typ für jede Variable in der
Abfrageoperation bereit.
Weitere Informationen zu var finden Sie unter Implizit typisierte lokale Variablen.
Abfragesyntax und Methodensyntax in LINQ (C#)
04.11.2021 • 4 minutes to read

Die meisten Abfragen in der einführenden Dokumentation zu LINQ (Language Integrated Query) wurden
mithilfe der deklarativen Abfragesyntax von LINQ geschrieben. Die Abfragesyntax muss jedoch in
Methodenaufrufe für die .NET Common Language Runtime (CLR) übersetzt werden, wenn der Code kompiliert
wird. Diese Methodenaufrufe rufen die Standardabfrageoperatoren auf, die z.B. folgende Namen haben: Where ,
Select , GroupBy , Join , Max und Average . Sie können sie direkt mithilfe der Methodensyntax anstatt der
Abfragesyntax aufrufen.
Abfragesyntax und Methodensyntax sind semantisch identisch, aber viele Benutzer finden die Abfragesyntax
einfacher und leichter zu lesen. Einige Abfragen müssen als Methodenaufrufe ausgedrückt werden. Sie müssen
z.B. einen Methodenaufruf verwenden, um eine Abfrage auszudrücken, die die Anzahl der Elemente abruft, die
einer angegebenen Bedingung entsprechen. Sie müssen einen Methodenaufruf auch für eine Abfrage
verwenden, die das Element abruft, das den Maximalwert in der Quellsequenz hat. In der
Referenzdokumentation für die Standardabfrageoperatoren im System.Linq-Namespace wird im Allgemeinen
die Methodensyntax verwendet. Daher ist es sinnvoll, sich mit der Verwendung der Methodensyntax in
Abfragen und Abfrageausdrücken selbst vertraut zu machen, auch wenn mit dem Schreiben von LINQ-Abfragen
erst begonnen wird.

Erweiterungsmethoden von Standardabfrageoperatoren


Im folgenden Beispiel wird ein einfacher Abfrageausdruck und die semantisch äquivalente Abfrage gezeigt, die
als methodenbasierte Abfrage geschrieben ist.
class QueryVMethodSyntax
{
static void Main()
{
int[] numbers = { 5, 10, 8, 3, 6, 12};

//Query syntax:
IEnumerable<int> numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;

//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

foreach (int i in numQuery1)


{
Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
Console.Write(i + " ");
}

// Keep the console open in debug mode.


Console.WriteLine(System.Environment.NewLine);
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
/*
Output:
6 8 10 12
6 8 10 12
*/

Die Ausgabe der beiden Beispiele ist identisch. Sie sehen, dass der Typ der Abfragevariable in beiden Formen
der gleiche ist: IEnumerable<T>.
Betrachten Sie die methodenbasierte Abfrage genauer, um sie besser zu verstehen. Beachten Sie, dass die
where -Klausel auf der rechten Seite des Ausdrucks jetzt als Instanzmethode des numbers -Objekts ausgedrückt
wird. Sie erinnern sich sicher, dass diese über einen IEnumerable<int> -Typ verfügt. Wenn Sie mit der
generischen IEnumerable<T>-Schnittstelle vertraut sind, wissen Sie, dass sie über keine Where -Methode
verfügt. Wenn Sie jedoch die IntelliSense-Vervollständigungsliste in der Visual Studio IDE aufrufen, sehen Sie
nicht nur eine Where -Methode, sondern viele andere Methoden, z.B. Select , SelectMany , Join und Orderby .
Sie sind alle Standardabfrageoperatoren.

Obwohl es so scheint, als sei IEnumerable<T> neu definiert worden, um diese zusätzlichen Methoden zu
enthalten, ist dies tatsächlich nicht der Fall. Die Standardabfrageoperatoren werden als eine neue Methodenart,
die als Erweiterungsmethoden bezeichnet werden, implementiert. Erweiterungsmethoden „erweitern“ einen
vorhandenen Typ; sie können aufgerufen werden, als wären sie Instanzmethoden für den Typ. Die
Standardabfrageoperatoren erweitern IEnumerable<T>, weshalb Sie numbers.Where(...) schreiben können.
Um mit der Verwendung von LINQ zu beginnen, müssen Sie nur wissen, wie Sie Erweiterungsmethoden in Ihrer
Anwendung mithilfe der richtigen using -Anweisungen in den Geltungsbereich einbinden können. Aus Sicht
Ihrer Anwendung sind eine Erweiterungsmethode und eine reguläre Instanzmethode identisch.
Weitere Informationen zu Erweiterungsmethoden finden Sie unter Extension Methods (Erweiterungsmethoden).
Weitere Informationen über Standardabfrageoperatoren finden Sie unter Standard Query Operators Overview
(C#) (Übersicht über Standardabfrageoperatoren (C#)). Einige LINQ-Anbieter (z. B. LINQ to SQL und LINQ to
XML) implementieren ihre eigenen Standardabfrageoperatoren und zusätzliche Erweiterungsmethoden für
andere Typen als IEnumerable<T>.

Lambda-Ausdrücke
Beachten Sie im vorherigen Beispiel, dass der bedingte Ausdruck num % 2 == 0 als Inlineargument an die
Where -Methode übergeben wird: Where(num => num % 2 == 0). . Dieser Inlineausdruck wird als Lambdaausdruck
bezeichnet. Dies ist eine einfache Möglichkeit, Code zu schreiben, der sonst auf einem unpraktischeren Weg als
anonyme Methode, generischer Delegat oder Ausdrucksbaumstruktur geschrieben werden müsste. In C# ist der
Lambdaoperator => , der als „wird zu“ gelesen wird. num auf der linken Seite des Operators ist die
Eingabevariable, die num im Eingabeausdruck entspricht. Der Compiler kann den Typ von num ableiten, da er
weiß, dass es sich bei numbers um einen generischen IEnumerable<T>-Typ handelt. Der Text des
Lambdaausdrucks entspricht genau dem Ausdruck in der Abfragesyntax, in einem anderen Ausdruck oder in
einer Anweisung in C#; er kann Methodenaufrufe und andere komplexe Logik enthalten. Der „Rückgabewert“ ist
nur das Ergebnis des Ausdrucks.
Sie müssen Lambdaausdrücke nicht häufig verwenden, wenn Sie mit der Verwendung von LINQ beginnen.
Allerdings können bestimme Abfragen nur in der Methodensyntax ausgedrückt werden, und einige von ihnen
benötigen Lambdaausdrücke. Wenn Sie sich mit Lambdaausdrücken besser vertraut gemacht haben, werden
Sie sehen, dass sie ein leistungsstarkes und flexibles Tool in Ihrer LINQ-Toolbox sind. Weitere Informationen
finden Sie unter Lambdaausdrücke.

Zusammensetzbarkeit von Abfragen


Beachten Sie im vorherigen Codebeispiel, dass die OrderBy -Methode durch Verwendung des Punktoperators
auf dem Aufruf von Where aufgerufen wird. Where erzeugt eine gefilterte Sequenz, und Orderby bearbeitet sie
anschließend durch Sortierung. Da Abfragen IEnumerable zurückgeben, erstellen Sie sie in der Methodensyntax
durch Verkettung von Methodenaufrufen miteinander. Das führt auch der Compiler im Hintergrund aus, wenn
Sie über die Abfragesyntax Abfragen erstellen. Da die Abfragevariable die Ergebnisse der Abfrage nicht
speichert, können Sie sie jederzeit ändern oder als Basis für eine neue Abfrage verwenden, sogar, wenn sie
bereits ausgeführt wurde.
C#-Funktionen mit LINQ-Unterstützung
04.11.2021 • 3 minutes to read

Im folgenden Abschnitt werden neue Sprachkonstrukte, die in C# 3.0 eingeführt werden, vorgestellt. Obwohl
diese neuen Funktionen zu einem gewissen Grad mit LINQ-Abfragen verwendet werden, sind sie nicht
beschränkt auf LINQ und können in jedem Kontext, in dem Sie sie nützlich finden, verwendet werden.

Abfrageausdrücke
Abfrageausdrücke verwenden eine deklarative Syntax wie SQL oder XQuery, um eine Abfrage über
IEnumerable-Sammlungen zu erstellen. Zur Kompilierzeit wird die Abfragesyntax in Methodenaufrufe an eine
LINQ-Implementierung des Anbieters der Erweiterungsmethode des Standardabfrageoperators kompiliert.
Applikationen steuern die Standardabfrageoperatoren, die sich durch Angabe des entsprechenden Namespace
mit einer using -Anweisung innerhalb des Bereichs befinden. Der folgende Abfrageausdruck nimmt ein Array
von Zeichenfolgen, gruppiert sie nach dem ersten Zeichen in der Zeichenfolge und sortiert die Gruppen.

var query = from str in stringArray


group str by str[0] into stringGroup
orderby stringGroup.Key
select stringGroup;

Weitere Informationen finden Sie unter LINQ-Abfrageausdrücke.

Implizit typisierte Variablen (var)


Anstatt beim Deklarieren und Initialisieren einer Variablen einen Typ explizit anzugeben, können Sie den var-
Modifizierer verwenden, um den Compiler zum Ableiten und Zuweisen des Typs anzuweisen, wie hier gezeigt:

var number = 5;
var name = "Virginia";
var query = from str in stringArray
where str[0] == 'm'
select str;

Variablen, die als var deklariert werden, sind ebenso stark typisiert wie Variablen, deren Typ Sie explizit
angeben. Die Verwendung von var macht es möglich, anonyme Typen zu erstellen, dies ist jedoch nur für
lokale Variablen möglich. Arrays können auch mit impliziter Typisierung deklariert werden.
Weitere Informationen finden Sie unter Implizit typisierte lokale Variablen.

Objekt- und Auflistungsinitialisierer


Objekt- und Auflistungsinitialisierer ermöglichen das Initialisieren von Objekten ohne expliziten Aufruf eines
Konstruktors für das Objekt. Initialisierer werden in der Regel in Abfrageausdrücken verwendet, wenn sie die
Quelldaten in einen neuen Datentyp projizieren. In einer Klasse namens Customer mit öffentlichen Name - und
Phone -Eigenschaften können Objektinitialisierer wie im folgenden Code verwendet werden:

var cust = new Customer { Name = "Mike", Phone = "555-1212" };

Wenn Sie mit der Customer -Klasse fortfahren, gehen Sie davon aus, dass eine Datenquelle namens
IncomingOrders vorhanden ist, und dass für jede Bestellung mit einem großen OrderSize -Wert ein neues
Customer -Element basierend auf dieser Bestellung erstellt werden soll. Eine LINQ-Abfrage kann auf dieser
Datenquelle ausgeführt werden und verwendet die Objektinitialisierung, um eine Auflistung zu füllen:

var newLargeOrderCustomers = from o in IncomingOrders


where o.OrderSize > 5
select new Customer { Name = o.Name, Phone = o.Phone };

Die Datenquelle kann mehr Eigenschaften im Hintergrund haben als die Klasse Customer , z.B. OrderSize , aber
bei der Objektinitialisierung werden die von der Abfrage zurückgegebenen Daten in den gewünschten Datentyp
umgewandelt. Wählen Sie die Daten aus, die für Ihre Klasse relevant sind. Daher verfügen wir jetzt über ein
IEnumerable -Element, das mit den neuen gewünschten Customer -Informationen gefüllt ist. Das oben
Geschilderte kann auch in der Methodensyntax von LINQ geschrieben werden:

var newLargeOrderCustomers = IncomingOrders.Where(x => x.OrderSize > 5).Select(y => new Customer { Name =
y.Name, Phone = y.Phone });

Weitere Informationen finden Sie unter:


Objekt- und Auflistungsinitialisierer
Abfrageausdruckssyntax für Standardabfrageoperatoren

Anonyme Typen
Ein anonymer Typ wird vom Compiler erstellt, und der Typname ist nur für den Compiler verfügbar. Anonyme
Typen stellen eine bequeme Möglichkeit zum vorübergehenden Gruppieren einer Reihe von Eigenschaften in
einem Abfrageergebnis bereit, ohne einen separaten benannten Typ definieren zu müssen. Anonyme Typen
werden mit einem neuen Ausdruck und einem Objektinitialisierer initialisiert, wie hier gezeigt:

select new {name = cust.Name, phone = cust.Phone};

Weitere Informationen finden Sie unter Anonyme Typen.

Erweiterungsmethoden
Eine Erweiterungsmethode ist eine statische Methode, die einem Typ zugeordnet werden kann, sodass sie
aufgerufen werden kann, als ob es sich um eine Instanzmethode für den Typ handeln würde. Diese Funktion
ermöglicht es Ihnen, neue Methoden zu vorhandenen Typen „hinzuzufügen“, ohne sie tatsächlich zu ändern. Die
Standardabfrageoperatoren sind eine Reihe von Erweiterungsmethoden, die LINQ-Abfragefunktionen für jeden
Typ bieten, der IEnumerable<T> implementiert.
Weitere Informationen finden Sie unter Erweiterungsmethoden.

Lambda-Ausdrücke
Ein Lambdaausdruck ist eine Inlinefunktion, die den Operator => verwendet, um Eingabeparameter vom
Funktionstext zu trennen, und die zur Kompilierzeit in einen Delegaten oder eine Ausdrucksbaumstruktur
konvertiert werden kann. In der LINQ-Programmierung erhalten Sie Lambdaausdrücke, wenn Sie direkte
Methodenaufrufe für die Standardabfrageoperatoren vornehmen.
Weitere Informationen finden Sie unter
Lambda-Ausdrücke
Ausdrucksbaumstrukturen (C#)

Siehe auch
Language Integrated Query (LINQ) (C#)
Exemplarische Vorgehensweise: Schreiben von
Abfragen in C# (LINQ)
04.11.2021 • 10 minutes to read

Diese exemplarische Vorgehensweise veranschaulicht die C#-Sprachfunktionen, die zum Schreiben von LINQ-
Abfrageausdrücke verwendet werden.

Erstellen eines C#-Projekts


NOTE
Die folgenden Anweisungen gelten für Visual Studio. Wenn Sie eine andere Entwicklungsumgebung verwenden, erstellen
Sie ein Konsolenprojekt mit einem Verweis auf „System.Core.dll“ und eine using -Direktive für den Namespace
System.Linq.

So erstellen Sie ein Projekt in Visual Studio


1. Starten Sie Visual Studio.
2. Wählen Sie in der Menüleiste Datei , Neu , Projekt aus.
Das Dialogfeld Neues Projekt wird angezeigt.
3. Erweitern Sie nacheinander Installier t , Vorlagen , Visual C# , und wählen Sie dann
Konsolenanwendung aus.
4. Geben Sie im Textfeld Name einen anderen Namen ein, oder übernehmen Sie den Standardnamen, und
wählen Sie dann die Schaltfläche OK aus.
Das neue Projekt wird im Projektmappen-Explorer angezeigt.
5. Beachten Sie, dass Ihr Projekt einen Verweis auf „System.Core.dll“ und eine using -Direktive für den
Namespace System.Linq enthält.

Erstellen einer In-Memory-Datenquelle


Die Datenquelle für die Abfragen ist eine einfache Liste mit Student -Objekten. Jeder Student -Datensatz
umfasst einen Vornamen, einen Nachnamen und ein Array von Ganzzahlen, das die Testergebnisse der
einzelnen Studenten in der Klasse darstellt. Kopieren Sie diesen Code in Ihr Projekt. Beachten Sie die folgenden
Eigenschaften:
Die Student -Klasse besteht aus automatisch implementierten Eigenschaften.
Jeder Student in der Liste wird mit einem Objektinitialisierer initialisiert.
Die Liste selbst wird mit einem Auflistungsinitialisierer initialisiert.
Die gesamte Datenstruktur wird ohne explizite Aufrufe eines beliebigen Konstruktors oder expliziten
Memberzugriff initialisiert und instanziiert. Weitere Informationen zu diesen neuen Funktionen finden Sie unter
Automatisch implementierte Eigenschaften und Objekt- und Auflistungsinitialisierer.
So fügen Sie die Datenquelle hinzu
Fügen Sie die Student -Klasse und die initialisierte Liste der Studenten zur Program -Klasse in Ihrem
Projekt hinzu.

public class Student


{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public List<int> Scores;
}

// Create a data source by using a collection initializer.


static List<Student> students = new List<Student>
{
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int> {97, 92, 81,
60}},
new Student {First="Claire", Last="O'Donnell", ID=112, Scores= new List<int> {75, 84, 91, 39}},
new Student {First="Sven", Last="Mortensen", ID=113, Scores= new List<int> {88, 94, 65, 91}},
new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int> {97, 89, 85, 82}},
new Student {First="Debra", Last="Garcia", ID=115, Scores= new List<int> {35, 72, 91, 70}},
new Student {First="Fadi", Last="Fakhouri", ID=116, Scores= new List<int> {99, 86, 90, 94}},
new Student {First="Hanying", Last="Feng", ID=117, Scores= new List<int> {93, 92, 80, 87}},
new Student {First="Hugo", Last="Garcia", ID=118, Scores= new List<int> {92, 90, 83, 78}},
new Student {First="Lance", Last="Tucker", ID=119, Scores= new List<int> {68, 79, 88, 92}},
new Student {First="Terry", Last="Adams", ID=120, Scores= new List<int> {99, 82, 81, 79}},
new Student {First="Eugene", Last="Zabokritski", ID=121, Scores= new List<int> {96, 85, 91, 60}},
new Student {First="Michael", Last="Tucker", ID=122, Scores= new List<int> {94, 92, 91, 91}}
};

So fügen Sie einen neuen Studenten zur Liste der Studenten hinzu
1. Fügen Sie einen neuen Student zur Students -Liste hinzu, und verwenden Sie einen Namen und ein
Testergebnis Ihrer Wahl. Geben Sie alle Informationen für den neuen Studenten ein, um sich mit der Syntax
für den Objektinitialisierer vertraut zu machen.

Erstellen der Abfrage


So erstellen Sie eine einfache Abfrage
Erstellen Sie in der Main -Methode der Anwendung eine einfache Abfrage, die bei Ausführung eine Liste
aller Studenten erzeugt, deren Ergebnis im ersten Test höher als 90 war. Beachten Sie, dass der Typ der
Abfrage Student ist, da das gesamte IEnumerable<Student> -Objekt ausgewählt wird. Obwohl der Code
auch die implizite Typisierung mithilfe des Schlüsselworts var verwenden könnte, wird die explizite
Typisierung verwendet, um die Ergebnisse deutlich darzustellen. (Weitere Informationen zu var finden
Sie unter Implizit typisierte lokale Variablen.)
Beachten Sie auch, dass student , die Bereichsvariable der Abfrage, als Verweis auf jeden Student in der
Quelle dient und Memberzugriff auf jedes Objekt bietet.

// Create the query.


// The first line could also be written as "var studentQuery ="
IEnumerable<Student> studentQuery =
from student in students
where student.Scores[0] > 90
select student;

Ausführen der Abfrage


So führen Sie die Abfrage aus
1. Schreiben Sie jetzt die foreach -Schleife, die das Ausführen der Abfrage auslöst. Beachten Sie Folgendes
beim Code:
Auf jedes Element in der zurückgegebenen Sequenz wird über die Iterationsvariable in der
foreach -Schleife zugegriffen.

Der Typ dieser Variable ist Student , und der Typ der Abfragevariable, IEnumerable<Student> , ist
kompatibel.
2. Erstellen Sie die Anwendung, und führen Sie sie aus, nachdem Sie diesen Code hinzugefügt haben, um
die Ergebnisse im Fenster Konsole anzeigen zu lassen.

// Execute the query.


// var could be used here also.
foreach (Student student in studentQuery)
{
Console.WriteLine("{0}, {1}", student.Last, student.First);
}

// Output:
// Omelchenko, Svetlana
// Garcia, Cesar
// Fakhouri, Fadi
// Feng, Hanying
// Garcia, Hugo
// Adams, Terry
// Zabokritski, Eugene
// Tucker, Michael

So fügen Sie eine weitere Filterbedingung hinzu


1. Sie können mehrere boolesche Bedingungen in der where -Klausel kombinieren, um eine Abfrage weiter
zu optimieren. Der folgende Code fügt eine Bedingung hinzu, sodass die Abfrage die Studenten
zurückgibt, deren erstes Ergebnis höher als 90 und das letzte Ergebnis niedriger als 80 war. Die where -
Klausel sollte in etwa dem folgenden Code entsprechen.

where student.Scores[0] > 90 && student.Scores[3] < 80

Weitere Informationen finden Sie unter where-Klausel.

Ändern der Abfrage


So sortieren Sie die Ergebnisse
1. Es ist einfacher, die Ergebnisse zu überprüfen, wenn sie geordnet dargestellt werden. Sie können die
zurückgegebene Sequenz nach einem beliebigen zugänglichen Feld in den Quellelementen sortieren. Die
folgende orderby -Klausel ordnet die Ergebnisse z.B. in alphabetischer Reihenfolge (A bis Z) nach dem
Nachnamen jedes Studenten. Fügen Sie die folgende orderby -Klausel direkt nach der where -Anweisung
und vor der select -Anweisung zu Ihrer Abfrage hinzu:

orderby student.Last ascending

2. Ändern Sie nun die orderby -Klausel so, dass sie die Ergebnisse in umgekehrter Reihenfolge gemäß dem
Ergebnis im ersten Test sortiert, vom höchsten zum niedrigsten Ergebnis.

orderby student.Scores[0] descending

3. Ändern Sie die WriteLine -Formatzeichenfolge so, dass die Ergebnisse angezeigt werden:
Console.WriteLine("{0}, {1} {2}", student.Last, student.First, student.Scores[0]);

Weitere Informationen finden Sie unter orderby-Klausel.


So gruppieren Sie die Ergebnisse
1. Die Gruppierung ist eine leistungsstarke Funktion in Abfrageausdrücken. Eine Abfrage mit einer group-
Klausel erzeugt eine Sequenz von Gruppen, in der jede Gruppe einen Key und eine Sequenz enthält, die
aus allen Mitgliedern dieser Gruppe besteht. Die folgende neue Abfrage gruppiert die Studenten mit dem
ersten Buchstaben ihres Nachnamens als Schlüssel.

// studentQuery2 is an IEnumerable<IGrouping<char, Student>>


var studentQuery2 =
from student in students
group student by student.Last[0];

2. Beachten Sie, dass sich der Typ der Abfrage jetzt geändert ist. Sie erzeugt nun eine Sequenz von Gruppen
mit einem char -Typ als Schlüssel und eine Sequenz von Student -Objekten. Da der Typ der Abfrage
geändert wurde, ändert folgender Code ebenfalls die foreach -Ausführungsschleife:

// studentGroup is a IGrouping<char, Student>


foreach (var studentGroup in studentQuery2)
{
Console.WriteLine(studentGroup.Key);
foreach (Student student in studentGroup)
{
Console.WriteLine(" {0}, {1}",
student.Last, student.First);
}
}

// Output:
// O
// Omelchenko, Svetlana
// O'Donnell, Claire
// M
// Mortensen, Sven
// G
// Garcia, Cesar
// Garcia, Debra
// Garcia, Hugo
// F
// Fakhouri, Fadi
// Feng, Hanying
// T
// Tucker, Lance
// Tucker, Michael
// A
// Adams, Terry
// Z
// Zabokritski, Eugene

3. Führen Sie die Anwendung aus, und zeigen Sie die Ergebnisse im Fenster Konsole an.
Weitere Informationen finden Sie unter group-Klausel.
So legen Sie Variablen als implizit typisiert fest
1. Das explizite Codieren von IEnumerables von IGroupings kann schnell mühsam werden. Sie können
dieselbe Abfrage und foreach -Schleife viel einfacher mithilfe von var schreiben. Das Schlüsselwort
var ändert die Typen Ihres Objekts nicht; es weist nur den Compiler an, die Typen abzuleiten. Ändern Sie
den Typ von studentQuery und der Iterationsvariable group zu var , und führen Sie die Abfrage erneut
aus. Beachten Sie, dass die Iterationsvariable in der inneren foreach -Schleife immer noch als Student
typisiert ist und die Abfrage so ausgeführt wird wie zuvor. Ändern Sie die Iterationsvariable student zu
var , und führen Sie die Abfrage erneut aus. Sie sehen, dass Sie genau die gleichen Ergebnisse erhalten.

var studentQuery3 =
from student in students
group student by student.Last[0];

foreach (var groupOfStudents in studentQuery3)


{
Console.WriteLine(groupOfStudents.Key);
foreach (var student in groupOfStudents)
{
Console.WriteLine(" {0}, {1}",
student.Last, student.First);
}
}

// Output:
// O
// Omelchenko, Svetlana
// O'Donnell, Claire
// M
// Mortensen, Sven
// G
// Garcia, Cesar
// Garcia, Debra
// Garcia, Hugo
// F
// Fakhouri, Fadi
// Feng, Hanying
// T
// Tucker, Lance
// Tucker, Michael
// A
// Adams, Terry
// Z
// Zabokritski, Eugene

Weitere Informationen zu var finden Sie unter Implizit typisierte lokale Variablen.
So sortieren Sie die Gruppen nach ihren Schlüsselwerten
1. Wenn Sie die vorherige Abfrage ausführen, werden Sie feststellen, dass die Gruppen nicht alphabetischer
angeordnet sind. Um dies zu ändern, müssen Sie eine orderby -Klausel nach der group -Klausel angeben.
Sie benötigen aber zuerst einen Bezeichner, der als Verweis auf die durch die group -Klausel erstellte
Gruppe dient, bevor Sie eine orderby -Klausel verwenden können. Geben Sie den Bezeichner mithilfe des
Schlüsselworts into wie folgt an:
var studentQuery4 =
from student in students
group student by student.Last[0] into studentGroup
orderby studentGroup.Key
select studentGroup;

foreach (var groupOfStudents in studentQuery4)


{
Console.WriteLine(groupOfStudents.Key);
foreach (var student in groupOfStudents)
{
Console.WriteLine(" {0}, {1}",
student.Last, student.First);
}
}

// Output:
//A
// Adams, Terry
//F
// Fakhouri, Fadi
// Feng, Hanying
//G
// Garcia, Cesar
// Garcia, Debra
// Garcia, Hugo
//M
// Mortensen, Sven
//O
// Omelchenko, Svetlana
// O'Donnell, Claire
//T
// Tucker, Lance
// Tucker, Michael
//Z
// Zabokritski, Eugene

Wenn Sie diese Abfrage ausführen, werden Sie feststellen, dass die Gruppen nun in alphabetischer
Reihenfolge sortiert sind.
So führen Sie einen Bezeichner mit „let“ ein
1. Sie können das Schlüsselwort let verwenden, um einen Bezeichner für ein beliebiges
Ausdrucksergebnis in den Abfrageausdruck einzuführen. Dieser Bezeichner ist sehr praktisch, wie im
folgenden Beispiel zu sehen ist; er kann auch die Leistung verbessern, da die Ergebnisse eines Ausdrucks
gespeichert werden und nicht mehrfach berechnet werden müssen.
// studentQuery5 is an IEnumerable<string>
// This query returns those students whose
// first test score was higher than their
// average score.
var studentQuery5 =
from student in students
let totalScore = student.Scores[0] + student.Scores[1] +
student.Scores[2] + student.Scores[3]
where totalScore / 4 < student.Scores[0]
select student.Last + " " + student.First;

foreach (string s in studentQuery5)


{
Console.WriteLine(s);
}

// Output:
// Omelchenko Svetlana
// O'Donnell Claire
// Mortensen Sven
// Garcia Cesar
// Fakhouri Fadi
// Feng Hanying
// Garcia Hugo
// Adams Terry
// Zabokritski Eugene
// Tucker Michael

Weitere Informationen finden Sie unter let-Klausel.


So verwenden Sie die Methodensyntax in einem Abfrageausdruck
1. Wie unter Abfragesyntax und Methodensyntax in LINQ beschrieben, können einige Abfrageoperationen
nur durch Verwendung der Methodensyntax ausgedrückt werden. Der folgende Code berechnet das
Gesamtergebnis für jeden Student in der Quellsequenz und ruft dann die Average() -Methode für die
Ergebnisse der Abfrage auf, um die durchschnittliche Punktzahl der Klasse zu berechnen.

var studentQuery6 =
from student in students
let totalScore = student.Scores[0] + student.Scores[1] +
student.Scores[2] + student.Scores[3]
select totalScore;

double averageScore = studentQuery6.Average();


Console.WriteLine("Class average score = {0}", averageScore);

// Output:
// Class average score = 334.166666666667

So transformieren oder projizieren Sie in die select-Klausel


1. Es kommt sehr häufig vor, dass eine Abfrage eine Sequenz erzeugt, deren Elemente sich von den
Elementen in den Quellsequenzen unterscheiden. Löschen Sie oder kommentieren Sie die vorherige
Abfrage und Ausführungsschleife aus, und ersetzen Sie sie durch den folgenden Code. Beachten Sie, dass
die Abfrage eine Sequenz von Zeichenfolgen zurückgibt (nicht Students ). Dies spiegelt sich in der
foreach -Schleife wider.
IEnumerable<string> studentQuery7 =
from student in students
where student.Last == "Garcia"
select student.First;

Console.WriteLine("The Garcias in the class are:");


foreach (string s in studentQuery7)
{
Console.WriteLine(s);
}

// Output:
// The Garcias in the class are:
// Cesar
// Debra
// Hugo

2. Der vorherige Code in dieser exemplarischen Vorgehensweise hat gezeigt, dass das durchschnittliche
Ergebnis der Klasse ungefähr 334 beträgt. Zum Erstellen einer Sequenz von Students mit ihrer
Student ID , deren Ergebnis höher ist als der Klassendurchschnitt, können Sie einen anonymen Typ in der
select -Anweisung verwenden:

var studentQuery8 =
from student in students
let x = student.Scores[0] + student.Scores[1] +
student.Scores[2] + student.Scores[3]
where x > averageScore
select new { id = student.ID, score = x };

foreach (var item in studentQuery8)


{
Console.WriteLine("Student ID: {0}, Score: {1}", item.id, item.score);
}

// Output:
// Student ID: 113, Score: 338
// Student ID: 114, Score: 353
// Student ID: 116, Score: 369
// Student ID: 117, Score: 352
// Student ID: 118, Score: 343
// Student ID: 120, Score: 341
// Student ID: 122, Score: 368

Nächste Schritte
Nachdem Sie nun mit den grundlegenden Aspekten der Arbeit mit Abfragen in C# vertraut sind, sind Sie nun
bereit, die Dokumentation und Beispiele für bestimmte LINQ-Anbieter zu lesen, an denen Sie interessiert sind:
LINQ to SQL
LINQ to DataSet
LINQ to XML (C#)
LINQ to Objects (C#)

Siehe auch
Language Integrated Query (LINQ) (C#)
LINQ-Abfrageausdrücke
Übersicht über Standardabfrageoperatoren (C#)
04.11.2021 • 3 minutes to read

Die Standardabfrageoperatoren sind die Methoden, die das LINQ-Muster bilden. Die meisten dieser Methoden
bearbeiten Sequenzen. Eine Sequenz ist hier ein Objekt, dessen Typ die IEnumerable<T>-Schnittstelle oder die
IQueryable<T>-Schnittstelle implementiert. Die Standardabfrageoperatoren stellen Abfragefunktionen wie
Filterung, Projektion, Aggregation, Sortierung und weitere bereit.
Es gibt zwei Gruppen von LINQ-Standardabfrageoperatoren: Eine Gruppe funktioniert auf Grundlage von
Objekten vom Typ IEnumerable<T>, die andere auf Grundlage von Objekten vom Typ IQueryable<T>. Die
Methoden, die eine Gruppe bilden, sind statische Member der Klassen Enumerable und Queryable. Sie werden
als Erweiterungsmethoden des Typs, auf dessen Grundlage sie funktionieren, definiert. Erweiterungsmethoden
können entweder mit der Syntax für statische Methoden oder mit der für Instanzmethoden aufgerufen werden.
Darüber hinaus funktionieren mehrere Standardabfrageoperator-Methoden auf Grundlage anderer Typen als
die, die auf IEnumerable<T> oder IQueryable<T> basieren. Der Typ Enumerable definiert zwei Methoden, die
beide auf Grundlage von Objekten vom Typ IEnumerable funktionieren. Die Methoden Cast<TResult>
(IEnumerable) und OfType<TResult>(IEnumerable) ermöglichen es Ihnen, eine nicht parametrisierte oder nicht
generische Auflistung im LINQ-Muster abfragen zu lassen. Dies erfolgt durch Erstellen einer stark typisierten
Sammlung von Objekten. Die Klasse Queryable definiert zwei ähnliche Methoden, Cast<TResult>(IQueryable)
und OfType<TResult>(IQueryable), die auf Grundlage von Objekten vom Typ IQueryable funktionieren.
Die Standardabfrageoperatoren unterscheiden sich im Zeitpunkt ihrer Ausführung. Dies hängt davon ab, ob sie
einen Singleton-Wert oder eine Sequenz von Werten zurückgeben. Die Methoden, die einen Singleton-Wert
zurückgeben (z.B. Average und Sum), werden sofort ausgeführt. Methoden, die eine Sequenz zurückgeben,
verzögern die Abfrageausführung und geben ein auflistbares Objekt zurück.
Bei Methoden, die auf In-Memory-Sammlungen angewendet werden, d. h. den Methoden, die IEnumerable<T>
erweitern, erfasst das zurückgegebene auflistbare Objekt die Argumente, die an die Methode übergeben
wurden. Wenn dieses Objekt auflistbar ist, tritt die Logik des Abfrageoperators in Kraft, und die
Abfrageergebnisse werden zurückgegeben.
Im Gegensatz dazu implementieren Methoden, die IQueryable<T> erweitern, kein Abfrageverhalten. Sie
erstellen eine Ausdrucksbaumstruktur, die die auszuführende Abfrage darstellt. Die Verarbeitung von Abfragen
wird vom Quellobjekt IQueryable<T> übernommen.
Aufrufe der Abfragemethode können miteinander in eine Abfrage verkettet werden. Dadurch können Abfragen
von beliebiger Komplexität sein.
Im folgenden Codebeispiel wird veranschaulicht, wie die Standardabfrageoperatoren verwendet werden
können, um Informationen zu einer Sequenz zu erhalten.
string sentence = "the quick brown fox jumps over the lazy dog";
// Split the string into individual words to create a collection.
string[] words = sentence.Split(' ');

// Using query expression syntax.


var query = from word in words
group word.ToUpper() by word.Length into gr
orderby gr.Key
select new { Length = gr.Key, Words = gr };

// Using method-based query syntax.


var query2 = words.
GroupBy(w => w.Length, w => w.ToUpper()).
Select(g => new { Length = g.Key, Words = g }).
OrderBy(o => o.Length);

foreach (var obj in query)


{
Console.WriteLine("Words of length {0}:", obj.Length);
foreach (string word in obj.Words)
Console.WriteLine(word);
}

// This code example produces the following output:


//
// Words of length 3:
// THE
// FOX
// THE
// DOG
// Words of length 4:
// OVER
// LAZY
// Words of length 5:
// QUICK
// BROWN
// JUMPS

Abfrageausdruckssyntax
Einige der häufiger verwendeten Standardabfrageoperatoren verfügen über eine dedizierte Schlüsselwortsyntax
von C# und Visual Basic-Sprache, wodurch sie als Teil eines query-Ausdrucks aufgerufen werden können.
Weitere Informationen über Standardabfrageoperatoren, die über dedizierte Schlüsselwörter und deren
entsprechende Syntax verfügen, finden Sie unter Query Expression Syntax for Standard Query Operators (C#)
(Abfrageausdruckssyntax für Standardabfrageoperatoren (C#)).

Erweitern der Standardabfrageoperatoren


Sie können die Gruppe von Standardabfrageoperatoren durch Erstellen von domänenspezifischen Methoden
erweitern, die für Ihre Zieldomäne oder -technologie geeignet sind. Sie können die Standardabfrageoperatoren
auch mit ihren eigenen Implementierungen ersetzen, die zusätzliche Dienste bieten, z.B. Remote-Auswertung,
Abfrageübersetzung und Optimierung. Ein Beispiel finden Sie unter AsEnumerable.

Verwandte Abschnitte
Über die folgenden Links gelangen Sie zu Artikeln, die Ihnen weitere Informationen über die verschiedenen
Standardabfrageoperatoren basierend auf deren Funktionen bieten.
Sorting Data (C#) (Sortieren von Daten (C#))
Set Operations (C#) (Set-Vorgänge (C#))
Filtering Data (C#) (Filtern von Daten (C#))
Quantifier Operations (C#) (Quantifizierer-Vorgänge (C#))
Projection Operations (C#) (Projektionsvorgänge (C#))
Partitioning Data (C#) (Partitionieren von Daten (C#))
Join Operations (C#) (Verknüpfungsvorgänge (C#))
Grouping Data (C#) (Gruppieren von Daten (C#))
Generation Operations (C#) (Generierungsvorgänge (C#))
quality Operations (C#) (Gleichheitsvorgänge (C#))
Element Operations (C#) (Elementvorgänge (C#))
Converting Data Types (C#) (Konvertieren von Datentypen (C#))
Concatenation Operations (C#) (Verkettungsvorgänge (C#))
Aggregation Operations (C#) (Aggregationsvorgänge (C#))

Siehe auch
Enumerable
Queryable
Introduction to LINQ Queries (C#) (Einführung in LINQ-Abfragen (C#))
Abfrageausdruckssyntax für Standardabfrageoperatoren (C#)
Classification of Standard Query Operators by Manner of Execution (C#) (Klassifizierung von
Standardabfrageoperatoren nach Ausführungsarten (C#))
Erweiterungsmethoden
Abfrageausdruckssyntax für
Standardabfrageoperatoren (C#)
04.11.2021 • 2 minutes to read

Einige der häufiger verwendeten Standardabfrageoperatoren verfügen über eine dedizierte Schlüsselwortsyntax
von C#, wodurch sie als Teil eines Abfrageausdrucks aufgerufen werden können. Mit einem Abfrageausdruck
kann eine Abfrage besser lesbar ausgedrückt werden als mit dessen methodenbasierter Entsprechung. Die
Abfrageausdrucksklauseln werden bei der Kompilierung in Aufrufe der Abfragemethoden übersetzt.

Tabelle: Abfrageausdruckssyntax
In der folgenden Tabelle finden Sie eine Liste von Standardabfrageoperatoren, die über äquivalente
Abfrageausdrucksklauseln verfügen.

M ET H O DE C #- A B F RA GEA USDRUC K SSY N TA X

Cast Verwenden Sie eine explizit typisierte Bereichsvariable, z.B.:

from int i in numbers

(Weitere Informationen finden Sie unter from-Klausel.)

GroupBy group … by

- oder -

group … by … into …

(Weitere Informationen finden Sie unter group-Klausel.)

GroupJoin<TOuter,TInner,TKey,TResult> join … in … on … equals … into …


(IEnumerable<TOuter>, IEnumerable<TInner>,
Func<TOuter,TKey>, Func<TInner,TKey>, (Weitere Informationen finden Sie unter join-Klausel.)
Func<TOuter,IEnumerable<TInner>,TResult>)

Join<TOuter,TInner,TKey,TResult>(IEnumerable<TOuter>, join … in … on … equals …


IEnumerable<TInner>, Func<TOuter,TKey>,
Func<TInner,TKey>, Func<TOuter,TInner,TResult>) (Weitere Informationen finden Sie unter join-Klausel.)

OrderBy<TSource,TKey>(IEnumerable<TSource>, orderby
Func<TSource,TKey>)
(Weitere Informationen finden Sie unter orderby-Klausel.)

OrderByDescending<TSource,TKey> orderby … descending


(IEnumerable<TSource>, Func<TSource,TKey>)
(Weitere Informationen finden Sie unter orderby-Klausel.)

Select select

(Weitere Informationen finden Sie unter select-Klausel.)


M ET H O DE C #- A B F RA GEA USDRUC K SSY N TA X

SelectMany Mehrere from -Klauseln.

(Weitere Informationen finden Sie unter from-Klausel.)

ThenBy<TSource,TKey>(IOrderedEnumerable<TSource>, orderby …, …
Func<TSource,TKey>)
(Weitere Informationen finden Sie unter orderby-Klausel.)

ThenByDescending<TSource,TKey> orderby …, … descending


(IOrderedEnumerable<TSource>, Func<TSource,TKey>)
(Weitere Informationen finden Sie unter orderby-Klausel.)

Where where

(Weitere Informationen finden Sie unter where-Klausel.)

Siehe auch
Enumerable
Queryable
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Classification of Standard Query Operators by Manner of Execution (C#) (Klassifizierung von
Standardabfrageoperatoren nach Ausführungsarten (C#))
Klassifizierung von Standardabfrageoperatoren
nach Ausführungsarten (C#)
04.11.2021 • 2 minutes to read

Die LINQ to Objects-Implementierungen des Standardabfrageoperators werden mit einer von zwei möglichen
Arten ausgeführt: direkt oder zurückgestellt. Abfrageoperatoren, die die verzögerte Ausführung verwenden,
können darüber hinaus in zwei Kategorien unterteilt werden: Streaming und Nicht-Streaming. Wenn Sie wissen,
wie die einzelnen Abfrageoperatoren ausgeführt werden, erleichtert dies das Verständnis der Ergebnisse, die Sie
von einer Abfrage erhalten. Dies ist insbesondere dann der Fall, wenn die Datenquelle geändert wird oder wenn
Sie eine Abfrage auf Grundlage einer anderen Abfrage erstellen. In diesem Thema werden die
Standardabfrageoperatoren gemäß ihrer Ausführungsarten klassifiziert.

Arten der Ausführung


Direkt
Direkte Ausführung bedeutet, dass die Datenquelle gelesen wird und die Operation an dem Zeitpunkt im Code
ausgeführt wird, an dem die Abfrage deklariert wird. Alle Standardabfrageoperatoren, die ein einzelnes, nicht
aufzählbares Ergebnis zurückgeben, werden sofort ausgeführt.
Zurückgestellt
Zurückgestellte Ausführung bedeutet, dass der Vorgang nicht zum Zeitpunkt im Code ausgeführt wird, an dem
die Abfrage deklariert wird. Der Vorgang erfolgt nur, wenn die Abfragevariable aufgezählt wird, z.B. durch
Verwendung einer foreach -Anweisung. Dies bedeutet, dass die Ergebnisse der Ausführung der Abfrage vom
Inhalt der Datenquelle zum Zeitpunkt der Abfrageausführung, nicht der Abfragedefinition abhängen. Wenn die
Abfragevariable mehrfach aufgezählt wird, können die Ergebnisse jedes Mal abweichen. Fast alle
Standardabfrageoperatoren, deren Rückgabetyp IEnumerable<T> oder IOrderedEnumerable<TElement> ist,
werden verzögert ausgeführt.
Abfrageoperatoren, die die verzögerte Ausführung verwenden, können zusätzlich als Streaming und Nicht-
Streaming klassifiziert werden.
Streaming
Streaming-Operatoren müssen nicht alle Quelldaten lesen, bevor sie Elemente liefern. Zum Zeitpunkt der
Ausführung führt ein Streaming-Operator seine Operation auf jedem Quellelement aus, während es gelesen
wird, und liefert ggf. die Elemente. Ein Streaming-Operator liest weiterhin Quellelemente, bis ein
Ergebniselement erzeugt werden kann. Dies bedeutet, dass mehr als ein Quellelement womöglich gelesen
werden kann, um ein Ergebniselement zu erzeugen.
Nicht-Streaming
Nicht-Streaming-Operatoren müssen alle Quelldaten lesen, bevor sie ein Ergebniselement liefern können.
Vorgänge wie das Sortieren oder Gruppieren fallen unter diese Kategorie. Zum Zeitpunkt der Ausführung lesen
Nicht-Streaming-Operatoren alle Quelldaten, fügen sie in eine Datenstruktur ein, führen den Vorgang aus und
liefern die Elemente, die sich ergeben.

Klassifizierungstabelle
In der folgenden Tabelle wird jede Standardabfrageoperator-Methode laut der Ausführungsmethode
klassifiziert.
NOTE
Wenn ein Operator in zwei Spalten gekennzeichnet ist, werden zwei Eingabesequenzen in den Vorgang einbezogen, und
jede Sequenz wird unterschiedlich ausgewertet. In diesen Fällen ist es immer die erste Sequenz in der Parameterliste, die
verzögert und mit der Nicht-Straming-Methode ausgewertet wird.

VERZ Ö GERT E VERZ Ö GERT E N IC H T -


STA N DA RDA B F RA GE SO F O RT IGE ST REA M IN G- ST REA M IN G-
O P ERATO R RÜC KGA B ET Y P A USF ÜH RUN G A USF ÜH RUN G A USF ÜH RUN G

Aggregate TSource X

All Boolean X

Any Boolean X

AsEnumerable IEnumerable<T> X

Average Einzelner X
numerischer Wert

Cast IEnumerable<T> X

Concat IEnumerable<T> X

Contains Boolean X

Count Int32 X

DefaultIfEmpty IEnumerable<T> X

Distinct IEnumerable<T> X

ElementAt TSource X

ElementAtOrDefault TSource X

Empty IEnumerable<T> X

Except IEnumerable<T> X X

First TSource X

FirstOrDefault TSource X

GroupBy IEnumerable<T> X

GroupJoin IEnumerable<T> X X

Intersect IEnumerable<T> X X

Join IEnumerable<T> X X
VERZ Ö GERT E VERZ Ö GERT E N IC H T -
STA N DA RDA B F RA GE SO F O RT IGE ST REA M IN G- ST REA M IN G-
O P ERATO R RÜC KGA B ET Y P A USF ÜH RUN G A USF ÜH RUN G A USF ÜH RUN G

Last TSource X

LastOrDefault TSource X

LongCount Int64 X

Max Einzelner X
numerischer Wert,
TSource oder TResult

Min Einzelner X
numerischer Wert,
TSource oder TResult

OfType IEnumerable<T> X

OrderBy IOrderedEnumerable X
<TElement>

OrderByDescending IOrderedEnumerable X
<TElement>

Range IEnumerable<T> X

Repeat IEnumerable<T> X

Reverse IEnumerable<T> X

Select IEnumerable<T> X

SelectMany IEnumerable<T> X

SequenceEqual Boolean X

Single TSource X

SingleOrDefault TSource X

Skip IEnumerable<T> X

SkipWhile IEnumerable<T> X

Sum Einzelner X
numerischer Wert

Take IEnumerable<T> X

TakeWhile IEnumerable<T> X
VERZ Ö GERT E VERZ Ö GERT E N IC H T -
STA N DA RDA B F RA GE SO F O RT IGE ST REA M IN G- ST REA M IN G-
O P ERATO R RÜC KGA B ET Y P A USF ÜH RUN G A USF ÜH RUN G A USF ÜH RUN G

ThenBy IOrderedEnumerable X
<TElement>

ThenByDescending IOrderedEnumerable X
<TElement>

ToArray TSource-Array X

ToDictionary Dictionary<TKey,TVal X
ue>

ToList IList<T> X

ToLookup ILookup<TKey,TElem X
ent>

Union IEnumerable<T> X

Where IEnumerable<T> X

Siehe auch
Enumerable
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Abfrageausdruckssyntax für Standardabfrageoperatoren (C#)
LINQ to Objects (C#)
Sortieren von Daten (C#)
04.11.2021 • 2 minutes to read

Bei einem Sortiervorgang werden die Elemente einer Sequenz auf Grundlage eines oder mehrerer Attribute
sortiert. Mit dem ersten Sortierkriterium wird eine primäre Sortierung der Elemente ausgeführt. Sie können die
Elemente innerhalb jeder primären Sortiergruppe sortieren, indem Sie ein zweites Sortierkriterium angeben.
Die folgende Abbildung zeigt das Ergebnis eines alphabetischen Sortiervorgangs bei einer Zeichensequenz:

Die Methoden des Standardabfrageoperators, die Daten sortieren, sind im folgenden Abschnitt aufgeführt.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

OrderBy Sortiert Werte in orderby Enumerable.OrderBy


aufsteigender Reihenfolge
Queryable.OrderBy

OrderByDescending Sortiert Werte in orderby … descending Enumerable.OrderByDescen


absteigender Reihenfolge ding

Queryable.OrderByDescendi
ng

ThenBy Führt eine sekundäre orderby …, … Enumerable.ThenBy


Sortierung in aufsteigender
Reihenfolge durch Queryable.ThenBy

ThenByDescending Führt eine sekundäre orderby …, … Enumerable.ThenByDescend


Sortierung in absteigender descending ing
Reihenfolge durch
Queryable.ThenByDescendi
ng

Reverse Kehrt die Reihenfolge der Nicht zutreffend. Enumerable.Reverse


Elemente in einer Auflistung
um Queryable.Reverse

Beispiele für die Abfrageausdruckssyntax


Primäre Sortierungsbeispiele
Primäre aufsteigende Sortierung
Im folgenden Beispiel wird veranschaulicht, wie man die orderby -Klausel in einer LINQ-Abfrage verwendet, um
die Zeichenfolgen in einem Array in aufsteigender Reihenfolge nach der Länge der Zeichenfolgen zu sortieren.
string[] words = { "the", "quick", "brown", "fox", "jumps" };

IEnumerable<string> query = from word in words


orderby word.Length
select word;

foreach (string str in query)


Console.WriteLine(str);

/* This code produces the following output:

the
fox
quick
brown
jumps
*/

Primäre absteigende Sortierung


Im nächsten Beispiel wird veranschaulicht, wie man die orderby descending -Klausel in einer LINQ-Abfrage
verwendet, um die Zeichenfolgen in absteigender Reihenfolge nach ihrem ersten Buchstaben zu sortieren.

string[] words = { "the", "quick", "brown", "fox", "jumps" };

IEnumerable<string> query = from word in words


orderby word.Substring(0, 1) descending
select word;

foreach (string str in query)


Console.WriteLine(str);

/* This code produces the following output:

the
quick
jumps
fox
brown
*/

Sekundäre Sortierungsbeispiele
Sekundäre aufsteigende Sortierung
Im folgenden Beispiel wird veranschaulicht, wie man die orderby -Klausel in einer LINQ-Abfrage verwendet, um
eine primäre und eine sekundäre Sortierung der Zeichenfolgen in einem Array durchzuführen. Die
Zeichenfolgen werden primär nach der Länge und sekundär nach dem ersten Buchstaben der Zeichenfolge
sortiert, beide Male in aufsteigender Reihenfolge.
string[] words = { "the", "quick", "brown", "fox", "jumps" };

IEnumerable<string> query = from word in words


orderby word.Length, word.Substring(0, 1)
select word;

foreach (string str in query)


Console.WriteLine(str);

/* This code produces the following output:

fox
the
brown
jumps
quick
*/

Sekundäre absteigende Sortierung


Im nächsten Beispiel wird gezeigt, wie man die orderby descending -Klausel in einer LINQ-Abfrage verwendet,
um eine primäre Sortierung in aufsteigender Reihenfolge und eine sekundäre Sortierung in absteigender
Reihenfolge durchzuführen. Die Zeichenfolgen werden primär nach der Länge und sekundär nach dem ersten
Buchstaben der Zeichenfolge sortiert.

string[] words = { "the", "quick", "brown", "fox", "jumps" };

IEnumerable<string> query = from word in words


orderby word.Length, word.Substring(0, 1) descending
select word;

foreach (string str in query)


Console.WriteLine(str);

/* This code produces the following output:

the
fox
quick
jumps
brown
*/

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
orderby-Klausel
Sortieren der Ergebnisse einer Join-Klausel
Vorgehensweise: Sortieren oder Filtern von Textdaten nach einem beliebigen Wort oder Feld (LINQ) (C#)
Mengenvorgänge (C#)
04.11.2021 • 6 minutes to read

Mengenvorgänge in LINQ sind Abfrageoperationen, die ein Satz von Ergebnissen erzeugen, der auf der Existenz
oder Abwesenheit äquivalenter Elemente in derselben oder in einer getrennten Auflistung (oder einem Satz)
basiert.
Die Methoden des Standardabfrageoperators, die Mengenoperationen ausführen, sind im folgenden Abschnitt
aufgeführt.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M EN B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

Distinct oder DistinctBy Entfernt doppelte Werte Nicht zutreffend. Enumerable.Distinct


aus einer Auflistung Enumerable.DistinctBy
Queryable.Distinct
Queryable.DistinctBy

Except oder ExceptBy Gibt die festgelegte Nicht zutreffend. Enumerable.Except


Differenz zurück, was Enumerable.ExceptBy
bedeutet, dass die Elemente Queryable.Except
in einer Auflistung nicht in Queryable.ExceptBy
einer zweiten Auflistung
angezeigt werden

Intersect oder IntersectBy Gibt die Schnittmenge Nicht zutreffend. Enumerable.Intersect


zurück, d.h. die Elemente, Enumerable.IntersectBy
die in beiden Auflistungen Queryable.Intersect
angezeigt werden Queryable.IntersectBy

Union oder UnionBy Gibt die Nicht zutreffend. Enumerable.Union


Vereinigungsmenge zurück, Enumerable.UnionBy
d.h. eindeutige Elemente, Queryable.Union
die in einer der beiden Queryable.UnionBy
Auflistungen angezeigt
werden

Beispiele
Einige der folgenden Beispiele basieren auf einem record -Typ, der die Planeten in unserem Sonnensystem
darstellt.
namespace SolarSystem;

record Planet(
string Name,
PlanetType Type,
int OrderFromSun)
{
public static readonly Planet Mercury =
new(nameof(Mercury), PlanetType.Rock, 1);

public static readonly Planet Venus =


new(nameof(Venus), PlanetType.Rock, 2);

public static readonly Planet Earth =


new(nameof(Earth), PlanetType.Rock, 3);

public static readonly Planet Mars =


new(nameof(Mars), PlanetType.Rock, 4);

public static readonly Planet Jupiter =


new(nameof(Jupiter), PlanetType.Gas, 5);

public static readonly Planet Saturn =


new(nameof(Saturn), PlanetType.Gas, 6);

public static readonly Planet Uranus =


new(nameof(Uranus), PlanetType.Liquid, 7);

public static readonly Planet Neptune =


new(nameof(Neptune), PlanetType.Liquid, 8);

// Yes, I know... not technically a planet anymore


public static readonly Planet Pluto =
new(nameof(Pluto), PlanetType.Ice, 9);
}

record Planet ist ein Positionsdatensatz, der zur Instanziierung die Argumente Name , Type und OrderFromSun
erfordert. Es gibt mehrere static readonly -Planetinstanzen für den Planet -Typ. Dies sind zweckdienliche
Definitionen für bekannte Planeten. Das Type -Member identifiziert den Planetentyp.

namespace SolarSystem;

enum PlanetType
{
Rock,
Ice,
Gas,
Liquid
};

Distinct und DistinctBy


Das folgende Beispiel veranschaulicht das Verhalten der Enumerable.Distinct-Methode in einer Sequenz aus
Zeichenfolgen. Die zurückgegebene Sequenz enthält die eindeutigen Elemente aus der Eingabesequenz.
string[] planets = { "Mercury", "Venus", "Venus", "Earth", "Mars", "Earth" };

IEnumerable<string> query = from planet in planets.Distinct()


select planet;

foreach (var str in query)


{
Console.WriteLine(str);
}

/* This code produces the following output:


*
* Mercury
* Venus
* Earth
* Mars
*/

DistinctBy ist ein alternativer Ansatz für die Distinct -Methode, die ein keySelector -Element akzeptiert. Der
keySelector wird als Vergleichsdiskriminator des Quelltyps verwendet. Betrachten Sie das folgende Array aus
Planeten:

Planet[] planets =
{
Planet.Mercury,
Planet.Venus,
Planet.Earth,
Planet.Mars,
Planet.Jupiter,
Planet.Saturn,
Planet.Uranus,
Planet.Neptune,
Planet.Pluto
};

Planeten werden anhand ihres PlanetType -Elements unterschieden, und der erste Planet jedes Typs wird
angezeigt:

foreach (Planet planet in planets.DistinctBy(p => p.Type))


{
Console.WriteLine(planet);
}

// This code produces the following output:


// Planet { Name = Mercury, Type = Rock, OrderFromSun = 1 }
// Planet { Name = Jupiter, Type = Gas, OrderFromSun = 5 }
// Planet { Name = Uranus, Type = Liquid, OrderFromSun = 7 }
// Planet { Name = Pluto, Type = Ice, OrderFromSun = 9 }

Im oben stehenden C#-Code ist Folgendes passiert:


Das Planet -Array wird nach dem ersten Vorkommen jedes eindeutigen Planetentyps gefiltert.
Die resultierende planet -Instanz wird in die Konsole geschrieben.

Except und ExceptBy


Im folgenden Beispiel wird das Verhalten von Enumerable.Except veranschaulicht. Die zurückgegebene Sequenz
enthält nur die Elemente aus der ersten Eingabesequenz, die sich nicht in der zweiten Eingabesequenz befinden.
string[] planets1 = { "Mercury", "Venus", "Earth", "Jupiter" };
string[] planets2 = { "Mercury", "Earth", "Mars", "Jupiter" };

IEnumerable<string> query = from planet in planets1.Except(planets2)


select planet;

foreach (var str in query)


{
Console.WriteLine(str);
}

/* This code produces the following output:


*
* Venus
*/

ExceptBy ist ein alternativer Ansatz für die Except -Methode, die zwei Sequenzen heterogener Typen und ein
keySelector -Element akzeptiert. Der keySelector ist der gleiche Typ wie der zweite Auflistungstyp und wird als
Vergleichsdiskriminator des Quelltyps verwendet. Betrachten Sie die folgenden Arrays aus Planeten:

Planet[] planets =
{
Planet.Mercury,
Planet.Venus,
Planet.Earth,
Planet.Jupiter
};

Planet[] morePlanets =
{
Planet.Mercury,
Planet.Earth,
Planet.Mars,
Planet.Jupiter
};

Um Planeten in der ersten Auflistung zu finden, die sich nicht in der zweiten Sammlung Auflistung, können Sie
die Namen des Planeten als zweite ( second ) Auflistung projizieren und dasselbe keySelector -Element
bereitstellen:

// A shared "keySelector"
static string PlanetNameSelector(Planet planet) => planet.Name;

foreach (Planet planet in


planets.ExceptBy(
morePlanets.Select(PlanetNameSelector), PlanetNameSelector))
{
Console.WriteLine(planet);
}

// This code produces the following output:


// Planet { Name = Venus, Type = Rock, OrderFromSun = 2 }

Im oben stehenden C#-Code ist Folgendes passiert:


Der keySelector wird als lokale Funktion vom Typ static definiert, die nach Planetennamen unterscheidet.
Das erste Planetenarray wird basierend auf Planetennamen nach Planeten gefiltert, die im zweiten
Planetenarray nicht gefunden werden.
Die resultierende planet -Instanz wird in die Konsole geschrieben.

Intersect und IntersectBy


Im folgenden Beispiel wird das Verhalten von Enumerable.Intersect veranschaulicht. Die zurückgegebene
Sequenz enthält die Elemente, die in beiden Eingabesequenzen verwendet werden.

string[] planets1 = { "Mercury", "Venus", "Earth", "Jupiter" };


string[] planets2 = { "Mercury", "Earth", "Mars", "Jupiter" };

IEnumerable<string> query = from planet in planets1.Intersect(planets2)


select planet;

foreach (var str in query)


{
Console.WriteLine(str);
}

/* This code produces the following output:


*
* Mercury
* Earth
* Jupiter
*/

IntersectBy ist ein alternativer Ansatz für die Intersect -Methode, die zwei Sequenzen heterogener Typen und
ein keySelector -Element akzeptiert. Der keySelector wird als Vergleichsdiskriminator des Typs der zweiten
Auflistung verwendet. Betrachten Sie die folgenden Arrays aus Planeten:

Planet[] firstFivePlanetsFromTheSun =
{
Planet.Mercury,
Planet.Venus,
Planet.Earth,
Planet.Mars,
Planet.Jupiter
};

Planet[] lastFivePlanetsFromTheSun =
{
Planet.Mars,
Planet.Jupiter,
Planet.Saturn,
Planet.Uranus,
Planet.Neptune
};

Es gibt zwei Arrays mit Planeten: Das eine repräsentiert die fünf Planeten, die der Sonne am nächsten sind. Das
andere repräsentiert die fünf Planeten, die von der Sonne am weitesten entfernt sind. Da der Planet -Typ ein
positionsbezogener record -Typ ist, können Sie seine Wertvergleichssemantik in Form des keySelector -
Elements verwenden:
foreach (Planet planet in
firstFivePlanetsFromTheSun.IntersectBy(
lastFivePlanetsFromTheSun, planet => planet))
{
Console.WriteLine(planet);
}

// This code produces the following output:


// Planet { Name = Mars, Type = Rock, OrderFromSun = 4 }
// Planet { Name = Jupiter, Type = Gas, OrderFromSun = 5 }

Im oben stehenden C#-Code ist Folgendes passiert:


Die beiden Planet -Arrays überschneiden sich durch ihre Wertvergleichssemantik.
Nur Planeten, die sich in beiden Arrays finden, sind in der resultierenden Sequenz vorhanden.
Die resultierende planet -Instanz wird in die Konsole geschrieben.

Union und UnionBy


Das folgende Beispiel veranschaulicht einen Vereinigungsvorgang zweier Sequenzen aus Zeichenfolgen. Die
zurückgegebene Sequenz enthält die eindeutigen Elemente aus beiden Eingabesequenzen.

string[] planets1 = { "Mercury", "Venus", "Earth", "Jupiter" };


string[] planets2 = { "Mercury", "Earth", "Mars", "Jupiter" };

IEnumerable<string> query = from planet in planets1.Union(planets2)


select planet;

foreach (var str in query)


{
Console.WriteLine(str);
}

/* This code produces the following output:


*
* Mercury
* Venus
* Earth
* Jupiter
* Mars
*/

UnionBy ist ein alternativer Ansatz für die Union -Methode, die zwei Sequenzen desselben Typs und ein
keySelector -Element akzeptiert. Der keySelector wird als Vergleichsdiskriminator des Quelltyps verwendet.
Betrachten Sie die folgenden Arrays aus Planeten:
Planet[] firstFivePlanetsFromTheSun =
{
Planet.Mercury,
Planet.Venus,
Planet.Earth,
Planet.Mars,
Planet.Jupiter
};

Planet[] lastFivePlanetsFromTheSun =
{
Planet.Mars,
Planet.Jupiter,
Planet.Saturn,
Planet.Uranus,
Planet.Neptune
};

Um diese beiden Auflistungen in einer einzigen Sequenz zu vereinigen, geben Sie den keySelector an:

foreach (Planet planet in


firstFivePlanetsFromTheSun.UnionBy(
lastFivePlanetsFromTheSun, planet => planet))
{
Console.WriteLine(planet);
}

// This code produces the following output:


// Planet { Name = Mercury, Type = Rock, OrderFromSun = 1 }
// Planet { Name = Venus, Type = Rock, OrderFromSun = 2 }
// Planet { Name = Earth, Type = Rock, OrderFromSun = 3 }
// Planet { Name = Mars, Type = Rock, OrderFromSun = 4 }
// Planet { Name = Jupiter, Type = Gas, OrderFromSun = 5 }
// Planet { Name = Saturn, Type = Gas, OrderFromSun = 6 }
// Planet { Name = Uranus, Type = Liquid, OrderFromSun = 7 }
// Planet { Name = Neptune, Type = Liquid, OrderFromSun = 8 }

Im oben stehenden C#-Code ist Folgendes passiert:


Die beiden Planet -Arrays werden mithilfe ihrer Vergleichssemantik für den record -Wert miteinander
verwoben.
Die resultierende planet -Instanz wird in die Konsole geschrieben.

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Vorgehensweise: Verbinden und Vergleichen von Zeichenfolgenauflistungen (LINQ) (C#)
Vorgehensweise: Suchen der Unterschiedsmenge zwischen zwei Listen (LINQ) (C#)
Filtern von Daten (C#)
04.11.2021 • 2 minutes to read

Mit Filtern wird die Einschränkung des Resultsets auf Elemente bezeichnet, die eine bestimmte Bedingung
erfüllen. Es ist auch bekannt als Auswahl.
Die folgende Abbildung zeigt die Ergebnisse des Filterns einer Zeichenfolge. Das Prädikat für den Filtervorgang
gibt an, dass das Zeichen A sein muss.

Die Methoden des Standardabfrageoperators, die Auswahl ausführen, sind im folgenden Abschnitt aufgeführt.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

OfType Wählt Werte aus, je nach Nicht zutreffend. Enumerable.OfType


ihrer Fähigkeit, in einen
angegebenen Typ Queryable.OfType
umgewandelt zu werden.

Where Wählt Werte aus, die auf where Enumerable.Where


einer Prädikatfunktion
basieren. Queryable.Where

Beispiel für die Abfrageausdruckssyntax


Im folgenden Beispiel wird die where -Klausel verwendet, um die Zeichenfolgen aus einem Array zu filtern, die
eine bestimmte Länge aufweisen.

string[] words = { "the", "quick", "brown", "fox", "jumps" };

IEnumerable<string> query = from word in words


where word.Length == 3
select word;

foreach (string str in query)


Console.WriteLine(str);

/* This code produces the following output:

the
fox
*/
Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
where-Klausel
Dynamisches Festlegen von Prädikatfiltern zur Laufzeit
Vorgehensweise: Abfragen der Metadaten einer Assembly mit Reflektion (LINQ) (C#)
Vorgehensweise: Abfragen von Dateien mit einem angegebenen Attribut oder Namen (C#)
Vorgehensweise: Sortieren oder Filtern von Textdaten nach einem beliebigen Wort oder Feld (LINQ) (C#)
Quantifizierer-Vorgänge (C#)
04.11.2021 • 2 minutes to read

Quantifizierer-Vorgänge geben einen Boolean-Wert zurück, der angibt, ob einige oder alle Elemente in einer
Sequenz eine Bedingung erfüllen.
Die folgende Abbildung zeigt zwei verschiedene Quantifizierer-Vorgänge bei zwei verschiedenen
Quellsequenzen. Der erste Vorgang fragt, ob eines oder mehrere der Elemente das Zeichen „A“ sind. Das
Ergebnis ist true . Der zweite Vorgang fragt, ob alle Elemente das Zeichen „A“ sind. Das Ergebnis ist true .

Die Methoden des Standardabfrageoperators, die Quantifizierer-Vorgänge ausführen, sind im folgenden


Abschnitt aufgeführt.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

Alle Bestimmt, ob alle Elemente Nicht zutreffend. Enumerable.All


in einer Sequenz eine
Bedingung erfüllen. Queryable.All

Beliebig Bestimmt, ob Elemente Nicht zutreffend. Enumerable.Any


einer Sequenz eine
Bedingung erfüllen. Queryable.Any

Enthält Bestimmt, ob eine Sequenz Nicht zutreffend. Enumerable.Contains


ein angegebenes Element
enthält. Queryable.Contains

Beispiele für die Abfrageausdruckssyntax


Alle
Im folgenden Beispiel wird All verwendet, um zu überprüfen, ob alle Zeichenfolgen eine bestimmte Länge
besitzen.
class Market
{
public string Name { get; set; }
public string[] Items { get; set; }
}

public static void Example()


{
List<Market> markets = new List<Market>
{
new Market { Name = "Emily's", Items = new string[] { "kiwi", "cheery", "banana" } },
new Market { Name = "Kim's", Items = new string[] { "melon", "mango", "olive" } },
new Market { Name = "Adam's", Items = new string[] { "kiwi", "apple", "orange" } },
};

// Determine which market have all fruit names length equal to 5


IEnumerable<string> names = from market in markets
where market.Items.All(item => item.Length == 5)
select market.Name;

foreach (string name in names)


{
Console.WriteLine($"{name} market");
}

// This code produces the following output:


//
// Kim's market
}

Beliebig
Im folgenden Beispiel wird Any verwendet, um zu überprüfen, ob es Zeichenfolgen gibt, die mit „o“ beginnen.

class Market
{
public string Name { get; set; }
public string[] Items { get; set; }
}

public static void Example()


{
List<Market> markets = new List<Market>
{
new Market { Name = "Emily's", Items = new string[] { "kiwi", "cheery", "banana" } },
new Market { Name = "Kim's", Items = new string[] { "melon", "mango", "olive" } },
new Market { Name = "Adam's", Items = new string[] { "kiwi", "apple", "orange" } },
};

// Determine which market have any fruit names start with 'o'
IEnumerable<string> names = from market in markets
where market.Items.Any(item => item.StartsWith("o"))
select market.Name;

foreach (string name in names)


{
Console.WriteLine($"{name} market");
}

// This code produces the following output:


//
// Kim's market
// Adam's market
}
Enthält
Im folgenden Beispiel wird Contains verwendet, um zu überprüfen, ob ein Array ein bestimmtes Element
enthält.

class Market
{
public string Name { get; set; }
public string[] Items { get; set; }
}

public static void Example()


{
List<Market> markets = new List<Market>
{
new Market { Name = "Emily's", Items = new string[] { "kiwi", "cheery", "banana" } },
new Market { Name = "Kim's", Items = new string[] { "melon", "mango", "olive" } },
new Market { Name = "Adam's", Items = new string[] { "kiwi", "apple", "orange" } },
};

// Determine which market contains fruit names equal 'kiwi'


IEnumerable<string> names = from market in markets
where market.Items.Contains("kiwi")
select market.Name;

foreach (string name in names)


{
Console.WriteLine($"{name} market");
}

// This code produces the following output:


//
// Emily's market
// Adam's market
}

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Dynamisches Festlegen von Prädikatfiltern zur Laufzeit
Vorgehensweise: Abfragen von Sätzen, die bestimmte Wörter enthalten (LINQ) (C#)
Projektionsvorgänge (C#)
04.11.2021 • 5 minutes to read

Projektion bezieht sich auf einen Vorgang, bei dem ein Objekt in eine neue Form transformiert wird, die häufig
nur aus den Eigenschaften besteht, die anschließend verwendet werden. Mithilfe der Projektion können Sie
einen neuen Typ erstellen, der aus den einzelnen Objekten erstellt wird. Sie können eine Eigenschaft projizieren
und eine mathematische Funktion für sie ausführen. Sie können auch das ursprüngliche Objekt projizieren, ohne
es zu ändern.
Die Methoden des Standardabfrageoperators, die Projektion ausführen, sind im folgenden Abschnitt aufgeführt.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M EN B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

Auswählen Projektwerte, die auf einer select Enumerable.Select


Transform-Funktion Queryable.Select
basieren.

SelectMany Projiziert Sequenzen von Mehrere from -Klauseln Enumerable.SelectMany


Werten, die auf einer verwenden Queryable.SelectMany
Transform-Funktion
basieren, und fasst diese
dann in eine Sequenz
zusammen.

Zip Erzeugt eine Sequenz von Nicht zutreffend Enumerable.Zip


Tupeln mit Elementen aus Queryable.Zip
angegebenen Sequenzen
und einem optionalen
Ergebnisselektor.

Select
Im folgenden Beispiel wird die select -Klausel verwendet, um den ersten Buchstaben jeder Zeichenfolge in
einer Liste von Zeichenfolgen zu projizieren.

List<string> words = new() { "an", "apple", "a", "day" };

var query = from word in words


select word.Substring(0, 1);

foreach (string s in query)


Console.WriteLine(s);

/* This code produces the following output:

a
a
a
d
*/
SelectMany
Im folgenden Beispiel werden mehrere from -Klauseln verwendet, um die einzelnen Wörter aus den einzelnen
Zeichenfolgen in eine Liste von Zeichenfolgen zu projizieren.

List<string> phrases = new() { "an apple a day", "the quick brown fox" };

var query = from phrase in phrases


from word in phrase.Split(' ')
select word;

foreach (string s in query)


Console.WriteLine(s);

/* This code produces the following output:

an
apple
a
day
the
quick
brown
fox
*/

Zip
Für den Zip -Projektionsoperator gibt es mehrere Überladungen. Alle Zip -Methoden werden auf Sequenzen
aus mindestens zwei heterogenen Typen angewendet. Die ersten beiden Überladungen geben Tupel mit dem
entsprechenden Positionstyp aus den angegebenen Sequenzen zurück.
Betrachten Sie die folgenden Sammlungen:

// An int array with 7 elements.


IEnumerable<int> numbers = new[]
{
1, 2, 3, 4, 5, 6, 7
};
// A char array with 6 elements.
IEnumerable<char> letters = new[]
{
'A', 'B', 'C', 'D', 'E', 'F'
};

Um diese Sequenzen zusammen zu projizieren, verwenden Sie den Operator Enumerable.Zip<TFirst,TSecond>


(IEnumerable<TFirst>, IEnumerable<TSecond>):

foreach ((int number, char letter) in numbers.Zip(letters))


{
Console.WriteLine($"Number: {number} zipped with letter: '{letter}'");
}
// This code produces the following output:
// Number: 1 zipped with letter: 'A'
// Number: 2 zipped with letter: 'B'
// Number: 3 zipped with letter: 'C'
// Number: 4 zipped with letter: 'D'
// Number: 5 zipped with letter: 'E'
// Number: 6 zipped with letter: 'F'
IMPORTANT
Die aus einem ZIP-Vorgang resultierende Sequenz ist nie länger als die kürzeste Sequenz. Anders ausgedrückt: Wenn Sie
sich die Sammlungen numbers und letters ansehen, stellen Sie fest, dass sie sich in ihrer Länge unterscheiden. In der
resultierenden Sequenz wird das letzte Element aus der Sammlung letters ausgelassen, weil ihm eine Entsprechung
fehlt.

Die zweite Überladung akzeptiert eine third -Sequenz. Wir erstellen eine weitere Sammlung namens emoji :

// A string array with 8 elements.


IEnumerable<string> emoji = new[]
{
" ", " ", " ", " ", " ", " ", "✔", " "
};

Um diese Sequenzen zusammen zu projizieren, verwenden Sie den Operator


Enumerable.Zip<TFirst,TSecond,TThird>(IEnumerable<TFirst>, IEnumerable<TSecond>, IEnumerable<TThird>):

foreach ((int number, char letter, string em) in numbers.Zip(letters, emoji))


{
Console.WriteLine(
$"Number: {number} is zipped with letter: '{letter}' and emoji: {em}");
}
// This code produces the following output:
// Number: 1 is zipped with letter: 'A' and emoji:
// Number: 2 is zipped with letter: 'B' and emoji:
// Number: 3 is zipped with letter: 'C' and emoji:
// Number: 4 is zipped with letter: 'D' and emoji:
// Number: 5 is zipped with letter: 'E' and emoji:
// Number: 6 is zipped with letter: 'F' and emoji:

Ähnlich wie bei der vorherigen Überladung projiziert die Zip -Methode ein Tupel, dieses Mal jedoch mit drei
Elementen. Die endgültige Überladung macht Func<TFirst, TSecond, TResult> als Ergebnisselektor verfügbar.
Anhand der beiden Typen aus den gezippten Sequenzen können Sie eine neue resultierende Sequenz
projizieren.

foreach (string result in


numbers.Zip(letters, (number, letter) => $"{number} = {letter} ({(int)letter})"))
{
Console.WriteLine(result);
}
// This code produces the following output:
// 1 = A (65)
// 2 = B (66)
// 3 = C (67)
// 4 = D (68)
// 5 = E (69)
// 6 = F (70)

Mit der obigen Zip -Überladung wird die angegebene Funktion auf die entsprechenden Elemente numbers und
letter angewendet, wodurch eine Sequenz der string -Ergebnisse erzeugt wird.

Select im Vergleich mit SelectMany


Die Arbeit von jeweils Select und SelectMany besteht darin, einen Ergebniswert (oder Werte) aus den
Quellwerten zu erstellen. Select generiert einen Ergebniswert für jeden Quellwert. Das Ergebnis ist daher eine
Auflistung, die über die gleiche Anzahl von Elementen wie die Quellauflistung verfügt. Im Gegensatz dazu
erzeugt SelectMany ein einziges Gesamtergebnis, das verkettete untergeordnete Auflistungen aus jedem
Quellwert enthält. Die Transform-Funktion, die als Argument an SelectMany übergeben wird, muss eine
aufzählbare Sequenz von Werten für jeden Quellwert zurückgeben. Diese aufzählbaren Sequenzen werden
anschließend von SelectMany zu einer großen Sequenz verkettet.
Die folgenden zwei Abbildungen zeigen den konzeptionellen Unterschied zwischen den Aktionen der beiden
Methoden. In jedem Fall wird davon ausgegangen, dass die Auswahlfunktion (Transform) das Array von Blumen
aus jedem Quellwert auswählt.
Die Abbildung zeigt, wie Select eine Auflistung zurückgibt, die über die gleiche Anzahl von Elementen wie die
Quellauflistung verfügt.

Diese Abbildung zeigt, wie SelectMany die Zwischenmodus-Sequenz von Arrays in einem Endergebniswert
verkettet, der jeden Wert aus jedem Zwischenmodus-Array enthält.

Codebeispiel
Im folgenden Beispiel wird das Verhalten von Select und SelectMany verglichen. Der Code erstellt eine
„Bouquet“ von Blumen, indem er die ersten beiden Elemente aus jeder Liste der Blumennamen in der
Quellauflistung aufführt. In diesem Beispiel ist der „einzelne Wert“, den die Transformationsfunktion
Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,TResult>) verwendet, selbst eine Auflistung
von Werten. Dies erfordert die zusätzliche foreach -Schleife, um jede Zeichenfolge in den einzelnen
Untersequenzen aufzulisten.

class Bouquet
{
public List<string> Flowers { get; set; }
}

static void SelectVsSelectMany()


{
List<Bouquet> bouquets = new()
{
new Bouquet { Flowers = new List<string> { "sunflower", "daisy", "daffodil", "larkspur" }},
new Bouquet { Flowers = new List<string> { "sunflower", "daisy", "daffodil", "larkspur" }},
new Bouquet { Flowers = new List<string> { "tulip", "rose", "orchid" }},
new Bouquet { Flowers = new List<string> { "gladiolis", "lily", "snapdragon", "aster", "protea" }},
new Bouquet { Flowers = new List<string> { "larkspur", "lilac", "iris", "dahlia" }}
};

IEnumerable<List<string>> query1 = bouquets.Select(bq => bq.Flowers);

IEnumerable<string> query2 = bouquets.SelectMany(bq => bq.Flowers);

Console.WriteLine("Results by using Select():");


// Note the extra foreach loop here.
foreach (IEnumerable<String> collection in query1)
foreach (string item in collection)
Console.WriteLine(item);

Console.WriteLine("\nResults by using SelectMany():");


foreach (string item in query2)
Console.WriteLine(item);

/* This code produces the following output:

Results by using Select():


sunflower
daisy
daffodil
larkspur
tulip
rose
orchid
gladiolis
lily
snapdragon
aster
protea
larkspur
lilac
iris
dahlia

Results by using SelectMany():


sunflower
daisy
daffodil
larkspur
tulip
rose
orchid
gladiolis
lily
snapdragon
aster
protea
larkspur
lilac
iris
dahlia
*/
}

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
select-Klausel
Vorgehensweise: Auffüllen von Objektsammlungen mit Daten aus mehreren Quellen (LINQ) (C#)
Vorgehensweise: Aufteilen einer Datei in mehrere Dateien durch das Verwenden von Gruppen (LINQ) (C#)
Partitionieren von Daten (C#)
04.11.2021 • 2 minutes to read

Partitionieren in LINQ bezieht sich auf den Vorgang, bei dem eine Eingabesequenz in zwei Abschnitte unterteilt
wird, ohne die Elemente dabei neu anzuordnen, und bei dem anschließend einer der Abschnitte zurückzugeben
wird.
Die folgende Abbildung zeigt das Ergebnis von drei verschiedenen Partitionierungsvorgängen einer
Zeichensequenz. Der erste Vorgang gibt die ersten drei Elemente in der Sequenz zurück. Der zweite Vorgang
überspringt die ersten drei Elemente und gibt die übrigen Elemente zurück. Der dritte Vorgang überspringt die
ersten beiden Elemente in der Sequenz und gibt die nächsten drei Elemente zurück.

Die Methoden des Standardabfrageoperators, die Sequenzen partitionieren, sind im folgenden Abschnitt
aufgeführt.

Operatoren
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M EN B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

Skip Überspringt Elemente bis zu Nicht zutreffend. Enumerable.Skip


einer angegebenen Position Queryable.Skip
in einer Sequenz

SkipWhile Überspringt Elemente, die Nicht zutreffend. Enumerable.SkipWhile


auf einer Prädikatfunktion Queryable.SkipWhile
basieren, bis ein Element die
Bedingung nicht erfüllt

Take Übernimmt Elemente bis zu Nicht zutreffend. Enumerable.Take


einer angegebenen Position Queryable.Take
in einer Sequenz

TakeWhile Übernimmt Elemente, die Nicht zutreffend. Enumerable.TakeWhile


auf einer Prädikatfunktion Queryable.TakeWhile
basieren, bis ein Element
der Bedingung nicht erfüllt

Block Teilen Sie die Elemente einer Nicht zutreffend Enumerable.Chunk


Sequenz in Blöcke Queryable.Chunk
maximaler Größe auf.
Beispiel
Der Operator Chunk wird verwendet, um Elemente einer Folge auf der Grundlage eines bestimmten size
aufzuteilen.

int chunkNumber = 1;
foreach (int[] chunk in Enumerable.Range(0, 8).Chunk(3))
{
Console.WriteLine($"Chunk {chunkNumber++}:");
foreach (int item in chunk)
{
Console.WriteLine($" {item}");
}

Console.WriteLine();
}
// This code produces the following output:
// Chunk 1:
// 0
// 1
// 2
//
//Chunk 2:
// 3
// 4
// 5
//
//Chunk 3:
// 6
// 7

Für den C#-Code oben gilt:


Verlässt sich auf Enumerable.Range(Int32, Int32) um eine Zahlenfolge erzeugen.
Wendet den Operator Chunk an, der die Folge in Abschnitte mit der maximalen Größe drei aufspaltet.

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Join-Vorgänge (C#)
04.11.2021 • 4 minutes to read

Eine Verknüpfung zweier Datenquellen entspricht der Zuordnung von Objekten einer Datenquelle zu den
Objekten einer anderen Datenquelle, die ein Attribut gemeinsam haben.
Der Join-Vorgang ist für Abfragen sehr wichtig. Er ist auf Datenquellen ausgerichtet, deren Beziehungen
zueinander nicht direkt verfolgt werden können. Bei der objektorientierten Programmierung könnte dies eine
nicht modellierte Korrelation zwischen Objekten bedeuten, z. B. die entgegengesetzte Richtung einer
unidirektionalen Beziehung. Ein Beispiel einer unidirektionalen Beziehung ist die Klasse "Kunde" mit der
Eigenschaft vom Typ "Ort", während die Klasse "Ort" keine Eigenschaft besitzt, die einer Auflistung von "Kunde"-
Objekten entspricht. Wenn Sie eine Liste von "Ort"-Objekten besitzen und alle Kunden in den einzelnen Orten
finden möchten, könnten Sie eine Join-Operation verwenden, um diese zu finden.
Die im LINQ-Framework bereitgestellten Join-Methoden sind Join und GroupJoin. Diese Methoden führen
Gleichheitsverknüpfungen oder Verknüpfungen durch, bei denen zwei Datenquellen auf Basis der Gleichheit
ihrer Schlüssel verglichen werden. (Zum Vergleich: Transact-SQL unterstützt auch andere Join-Operatoren als
"ist gleich", z. B. den Operator "kleiner als".) Für relationale Datenbanken bedeutet dies, dass Join eine innere
Verknüpfung implementiert, d.h. eine Art von Verknüpfung, bei der nur die Objekte zurückgegeben werden, die
über eine Entsprechung im anderen Datensatz verfügen. Für die GroupJoin-Methode hat bei relationalen
Datenbanken kein direktes Äquivalent. Sie implementieren eine übergeordnete Menge innerer und linker
äußerer Joins. Ein Left Outer Join ist eine Verknüpfung, die jedes Element der ersten (linken) Datenquelle
zurückgibt, selbst wenn die andere Datenquelle keine zugehörigen Elemente enthält.
Die folgende Abbildung zeigt eine Konzeptansicht zweier Sätze sowie der Elemente innerhalb dieser Sätze, die
entweder in eine innere oder linke äußere Verknüpfung einbezogen sind.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

Join Die Join-Methode join … in … on … Enumerable.Join


verknüpft zwei Sequenzen equals …
auf Basis von Queryable.Join
Schlüsselauswahlfunktionen
und extrahiert Wertepaare.
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

GroupJoin Die Methode verknüpft join … in … on … Enumerable.GroupJoin


(Join) zwei Sequenzen auf equals … into …
Basis von Queryable.GroupJoin
Schlüsselauswahlfunktionen
und gruppiert die daraus
resultierenden
Übereinstimmungen für die
einzelnen Elemente.

Beispiele für die Abfrageausdruckssyntax


Join
Im folgenden Beispiel wird die join … in … on … equals … -Klausel verwendet, um zwei Sequenzen auf der
Grundlage eines bestimmten Werts zu verknüpfen:
class Product
{
public string Name { get; set; }
public int CategoryId { get; set; }
}

class Category
{
public int Id { get; set; }
public string CategoryName { get; set; }
}

public static void Example()


{
List<Product> products = new List<Product>
{
new Product { Name = "Cola", CategoryId = 0 },
new Product { Name = "Tea", CategoryId = 0 },
new Product { Name = "Apple", CategoryId = 1 },
new Product { Name = "Kiwi", CategoryId = 1 },
new Product { Name = "Carrot", CategoryId = 2 },
};

List<Category> categories = new List<Category>


{
new Category { Id = 0, CategoryName = "Beverage" },
new Category { Id = 1, CategoryName = "Fruit" },
new Category { Id = 2, CategoryName = "Vegetable" }
};

// Join products and categories based on CategoryId


var query = from product in products
join category in categories on product.CategoryId equals category.Id
select new { product.Name, category.CategoryName };

foreach (var item in query)


{
Console.WriteLine($"{item.Name} - {item.CategoryName}");
}

// This code produces the following output:


//
// Cola - Beverage
// Tea - Beverage
// Apple - Fruit
// Kiwi - Fruit
// Carrot - Vegetable
}

GroupJoin
Im folgenden Beispiel wird die join … in … on … equals … into … -Klausel verwendet, um zwei Sequenzen auf
der Grundlage eines bestimmten Werts zu verknüpfen und die resultierenden Übereinstimmungen für jedes
Element zu gruppieren:
class Product
{
public string Name { get; set; }
public int CategoryId { get; set; }
}

class Category
{
public int Id { get; set; }
public string CategoryName { get; set; }
}

public static void Example()


{
List<Product> products = new List<Product>
{
new Product { Name = "Cola", CategoryId = 0 },
new Product { Name = "Tea", CategoryId = 0 },
new Product { Name = "Apple", CategoryId = 1 },
new Product { Name = "Kiwi", CategoryId = 1 },
new Product { Name = "Carrot", CategoryId = 2 },
};

List<Category> categories = new List<Category>


{
new Category { Id = 0, CategoryName = "Beverage" },
new Category { Id = 1, CategoryName = "Fruit" },
new Category { Id = 2, CategoryName = "Vegetable" }
};

// Join categories and product based on CategoryId and grouping result


var productGroups = from category in categories
join product in products on category.Id equals product.CategoryId into productGroup
select productGroup;

foreach (IEnumerable<Product> productGroup in productGroups)


{
Console.WriteLine("Group");
foreach (Product product in productGroup)
{
Console.WriteLine($"{product.Name,8}");
}
}

// This code produces the following output:


//
// Group
// Cola
// Tea
// Group
// Apple
// Kiwi
// Group
// Carrot
}

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Anonyme Typen
Formulieren von Joins und produktübergreifenden Abfragen
join-Klausel
Join mithilfe zusammengesetzter Schlüssel
Vorgehensweise: Verknüpfen des Inhalts unterschiedlicher Dateien (LINQ) (C#)
Sortieren der Ergebnisse einer Join-Klausel
Ausführen von benutzerdefinierten Verknüpfungsoperationen
Ausführen von Gruppenverknüpfungen
Ausführen innerer Verknüpfungen
Ausführen linker äußerer Verknüpfungen
Vorgehensweise: Auffüllen von Objektsammlungen mit Daten aus mehreren Quellen (LINQ) (C#)
Gruppieren von Daten (C#)
04.11.2021 • 2 minutes to read

Als „Gruppieren“ wird das Anordnen von Daten in Gruppen bezeichnet, sodass die Elemente in jeder Gruppe
über ein gemeinsames Attribut verfügen.
Die folgende Abbildung zeigt die Ergebnisse der Gruppierung einer Zeichenfolge. Der Schlüssel für jede Gruppe
ist das Zeichen.

Die Methoden des Standardabfrageoperators, die Datenelemente gruppieren, sind im folgenden Abschnitt
aufgeführt.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

GroupBy Gruppenelemente, die über group … by Enumerable.GroupBy


ein gemeinsames Attribut
verfügen. Jede Gruppe wird - oder - Queryable.GroupBy
durch ein
IGrouping<TKey,TElement> group … by … into …
-Objekt dargestellt.

ToLookup Fügt Elemente basierend Nicht zutreffend. Enumerable.ToLookup


auf einer
Schlüsselauswahlfunktion in
eine
Lookup<TKey,TElement>-
Klasse (one-to-many-
Wörterbuch) ein.

Beispiel für die Abfrageausdruckssyntax


Im folgenden Codebeispiel wird die group by -Klausel angewandt, um die Gruppe ganzer Zahlen in Listen mit
geraden und ungeraden Zahlen zu aufzuteilen.
List<int> numbers = new List<int>() { 35, 44, 200, 84, 3987, 4, 199, 329, 446, 208 };

IEnumerable<IGrouping<int, int>> query = from number in numbers


group number by number % 2;

foreach (var group in query)


{
Console.WriteLine(group.Key == 0 ? "\nEven numbers:" : "\nOdd numbers:");
foreach (int i in group)
Console.WriteLine(i);
}

/* This code produces the following output:

Odd numbers:
35
3987
199
329

Even numbers:
44
200
84
4
446
208
*/

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
group-Klausel
Erstellen einer geschachtelten Gruppe
Vorgehensweise: Gruppieren von Dateien nach Erweiterung (LINQ) (C#)
Gruppieren von Abfrageergebnissen
Ausführen einer Unterabfrage für eine Gruppierungsoperation
Vorgehensweise: Aufteilen einer Datei in mehrere Dateien durch das Verwenden von Gruppen (LINQ) (C#)
Generierungsvorgänge (C#)
04.11.2021 • 2 minutes to read

Die Generierung bezieht sich auf das Erstellen einer neuen Sequenz von Werten.
Die Methoden des Standardabfrageoperators, die Generierung ausführen, sind im folgenden Abschnitt
aufgeführt.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

DefaultIfEmpty Ersetzt eine leere Auflistung Nicht zutreffend. Enumerable.DefaultIfEmpty


durch eine Standardwert-
Singeltonauflistung. Queryable.DefaultIfEmpty

Empty Gibt eine leere Auflistung Nicht zutreffend. Enumerable.Empty


zurück.

Bereich Generiert eine Auflistung, Nicht zutreffend. Enumerable.Range


die eine Sequenz von
Zahlen enthält.

Wiederholen Generiert eine Auflistung, Nicht zutreffend. Enumerable.Repeat


die einen wiederholten
Wert enthält.

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Gleichheitsvorgänge (C#)
04.11.2021 • 2 minutes to read

Zwei Sequenzen, deren entsprechende Elemente gleich sind und die gleiche Anzahl von Elementen aufweisen,
werden als gleich betrachtet werden.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

SequenceEqual Bestimmt, ob zwei Nicht zutreffend. Enumerable.SequenceEqual


Sequenzen gleich sind,
indem Elemente paarweise Queryable.SequenceEqual
verglichen werden.

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Vorgehensweise: Vergleichen des Inhalts von zwei Ordnern (LINQ) (C#)
Elementvorgänge (C#)
04.11.2021 • 2 minutes to read

Bei Elementvorgängen werden einzelne, spezifische Elemente aus einer Sequenz zurückgegeben.
Die Methoden des Standardabfrageoperators, die Elementvorgänge ausführen, sind im folgenden Abschnitt
aufgeführt.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

ElementAt Gibt das Element an einen Nicht zutreffend. Enumerable.ElementAt


angegebenen Index in einer Queryable.ElementAt
Auflistung zurück.

ElementAtOrDefault Gibt das Element an einen Nicht zutreffend. Enumerable.ElementAtOrDe


angegebenen Index in einer fault
Auflistung oder einen Queryable.ElementAtOrDef
Standardwert zurück, wenn ault
der Index außerhalb des
gültigen Bereichs liegt.

First Gibt das erste Element einer Nicht zutreffend. Enumerable.First


Auflistung oder das erste Queryable.First
Element, das eine
Bedingung erfüllt, zurück.

FirstOrDefault Gibt das erste Element einer Nicht zutreffend. Enumerable.FirstOrDefault


Auflistung oder das erste Queryable.FirstOrDefault
Element, das eine Queryable.FirstOrDefault<T
Bedingung erfüllt, zurück. Source>
Gibt einen Standardwert (IQueryable<TSource>)
zurück, wenn kein solches
Element vorhanden ist.

Letzter Gibt das letzte Element Nicht zutreffend. Enumerable.Last


einer Auflistung oder das Queryable.Last
letzte Element, das eine
Bedingung erfüllt, zurück.

LastOrDefault Gibt das letzte Element Nicht zutreffend. Enumerable.LastOrDefault


einer Auflistung oder das Queryable.LastOrDefault
letzte Element, das eine
Bedingung erfüllt, zurück.
Gibt einen Standardwert
zurück, wenn kein solches
Element vorhanden ist.
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

Single Gibt das einzige Element Nicht zutreffend. Enumerable.Single


einer Auflistung oder das Queryable.Single
einzige Element, das eine
Bedingung erfüllt, zurück.
Löst eine
InvalidOperationException
aus, wenn kein oder mehr
als ein Element für die
Rückgabe vorhanden ist.

SingleOrDefault Gibt das einzige Element Nicht zutreffend. Enumerable.SingleOrDefault


einer Auflistung oder das Queryable.SingleOrDefault
einzige Element, das eine
Bedingung erfüllt, zurück.
Gibt einen Standardwert
zurück, wenn kein Element
zurückgegeben werden
kann. Löst eine
InvalidOperationException
aus, wenn mehr als ein
Element für die Rückgabe
vorhanden ist.

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Vorgehensweise: Abfragen der größten Datei oder der größten Dateien in einer Verzeichnisstruktur (LINQ)
(C#)
Konvertieren von Datentypen (C#)
04.11.2021 • 2 minutes to read

Konvertierungsmethoden ändern den Typ von Eingabeobjekten.


Konvertierungsvorgänge in LINQ-Abfragen sind in vielen Anwendungen nützlich. Nachstehend sind einige
Beispiele aufgeführt:
Die Enumerable.AsEnumerable-Methode kann zum Ausblenden einer benutzerdefinierten
Implementierung eines Standardabfrageoperators eines Typs verwendet werden.
Die Enumerable.OfType-Methode kann verwendet werden, um nicht parametrisierte Auflistungen für
LINQ-Abfragen zu ermöglichen.
Die Methoden Enumerable.ToArray, Enumerable.ToDictionary, Enumerable.ToList und
Enumerable.ToLookup können verwendet werden, um die sofortige Ausführung einer Abfrage zu
erzwingen, statt sie zu verzögern, bis die Abfrage enumeriert wurde.

Methoden
Die folgende Tabelle enthält die Standardabfrageoperator-Methoden, die Datentypumwandlungen ausführen.
Die Konvertierungsmethoden in dieser Tabelle, deren Namen mit „As“ beginnen, ändern den statischen Typ der
Quellauflistung, listen ihn jedoch nicht auf. Die Methoden, deren Namen mit „To“ anfangen, listen die
Quellauflistung auf und verschieben die Elemente in den entsprechenden Auflistungstyp.

C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

AsEnumerable Gibt die Eingabe als Nicht zutreffend. Enumerable.AsEnumerable


IEnumerable<T> typisiert
zurück

AsQueryable Konvertiert ein Nicht zutreffend. Queryable.AsQueryable


(generisches) IEnumerable-
Element in ein (generisches)
IQueryable-Element

Typumwandlung Kopiert die Elemente einer Verwenden Sie eine explizit Enumerable.Cast
Auflistung in einen typisierte Bereichsvariable.
bestimmten Typ. Zum Beispiel: Queryable.Cast

from string str in


words

OfType Filtert Werte, je nach ihrer Nicht zutreffend. Enumerable.OfType


Fähigkeit, die in einen
angegebenen Typ Queryable.OfType
umgewandelt werden
sollen.
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

ToArray Konvertiert eine Auflistung Nicht zutreffend. Enumerable.ToArray


in ein Array. Diese Methode
erzwingt die Ausführung
der Abfrage.

ToDictionary Platziert Elemente in ein Nicht zutreffend. Enumerable.ToDictionary


Dictionary<TKey,TValue>
auf Grundlage einer
Schlüsselauswahlfunktion.
Diese Methode erzwingt die
Ausführung der Abfrage.

ToList Konvertiert eine Auflistung Nicht zutreffend. Enumerable.ToList


in eine List<T>. Diese
Methode erzwingt die
Ausführung der Abfrage.

ToLookup Platziert Elemente, Nicht zutreffend. Enumerable.ToLookup


basierend auf einer
Schlüsselauswahlfunktion, in
ein
Lookup<TKey,TElement>
(one-to-many-Wörterbuch)
ein. Diese Methode
erzwingt die Ausführung
der Abfrage.

Beispiel für die Abfrageausdruckssyntax


Das folgende Codebeispiel verwendet eine explizit typisierte Bereichsvariable, um einen Typ vor dem Zugriff auf
ein Element, das nur im Untertyp verfügbar ist, in einen Untertyp umzuwandeln.
class Plant
{
public string Name { get; set; }
}

class CarnivorousPlant : Plant


{
public string TrapType { get; set; }
}

static void Cast()


{
Plant[] plants = new Plant[] {
new CarnivorousPlant { Name = "Venus Fly Trap", TrapType = "Snap Trap" },
new CarnivorousPlant { Name = "Pitcher Plant", TrapType = "Pitfall Trap" },
new CarnivorousPlant { Name = "Sundew", TrapType = "Flypaper Trap" },
new CarnivorousPlant { Name = "Waterwheel Plant", TrapType = "Snap Trap" }
};

var query = from CarnivorousPlant cPlant in plants


where cPlant.TrapType == "Snap Trap"
select cPlant;

foreach (Plant plant in query)


Console.WriteLine(plant.Name);

/* This code produces the following output:

Venus Fly Trap


Waterwheel Plant
*/
}

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
from-Klausel
LINQ-Abfrageausdrücke
Vorgehensweise: Abfragen von ArrayList mit LINQ (C#)
Verkettungsvorgänge (C#)
04.11.2021 • 2 minutes to read

Verkettung bezieht sich auf das Anhängen einer Sequenz an eine andere.
Die folgende Abbildung stellt einen Verkettungsvorgang zweier Zeichensequenzen dar.

Die Methoden des Standardabfrageoperators, die die Verkettung ausführen, sind im folgenden Abschnitt
aufgeführt.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E B ESC H REIB UN G AX W EIT ERE IN F O RM AT IO N EN

Concat Verkettet zwei Sequenzen Nicht zutreffend. Enumerable.Concat


zu einer einzigen Sequenz.
Queryable.Concat

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Vorgehensweise: Verbinden und Vergleichen von Zeichenfolgenauflistungen (LINQ) (C#)
Aggregationsvorgänge (C#)
04.11.2021 • 2 minutes to read

Während eines Aggregationsvorgangs wird aus einer Auflistung von Werten ein einzelner Wert berechnet. Ein
Beispiel für einen Aggregationsvorgang ist die Berechnung der durchschnittlichen Tagestemperatur aus den
Tagestemperaturen eines Monats.
Die folgende Abbildung zeigt das Ergebnis von zwei verschiedenen Aggregationsvorgängen bei einer
Zahlensequenz. Der erste Vorgang zählt die Zahlen zusammen. Der zweite Vorgang gibt den höchsten Wert der
Sequenz zurück.

Die Methoden des Standardabfrageoperators, die Aggregationsvorgänge ausführen, sind im folgenden


Abschnitt aufgeführt.

Methoden
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E DESC RIP T IO N AX W EIT ERE IN F O RM AT IO N EN

Aggregat Führt einen Nicht zutreffend. Enumerable.Aggregate


benutzerdefinierten Queryable.Aggregate
Aggregationsvorgang für
die Werte einer Auflistung
durch.

Average Berechnet den Nicht zutreffend. Enumerable.Average


Durchschnittswert einer Queryable.Average
Auflistung von Werten.

Anzahl Zählt die Elemente einer Nicht zutreffend. Enumerable.Count


Auflistung, optional auch Queryable.Count
nur die Elemente, die eine
Prädikatfunktion erfüllen.

LongCount Zählt die Elemente einer Nicht zutreffend. Enumerable.LongCount


großen Auflistung, optional Queryable.LongCount
auch nur die Elemente, die
eine Prädikatfunktion
erfüllen.

Max oder MaxBy Bestimmt den Maximalwert Nicht zutreffend. Enumerable.Max


einer Auflistung. Enumerable.MaxBy
Queryable.Max
Queryable.MaxBy
C #-
A B F RA GEA USDRUC K SSY N T
M ET H O DEN N A M E DESC RIP T IO N AX W EIT ERE IN F O RM AT IO N EN

Min oder MinBy Bestimmt den Minimalwert Nicht zutreffend. Enumerable.Min


einer Auflistung. Enumerable.MinBy
Queryable.Min
Queryable.MinBy

Summe Berechnet die Summe der Nicht zutreffend. Enumerable.Sum


Werte einer Auflistung. Queryable.Sum

Siehe auch
System.Linq
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
Vorgehensweise: Berechnen von Spaltenwerten in einer CSV-Textdatei (LINQ) (C#)
Vorgehensweise: Abfragen der größten Datei oder der größten Dateien in einer Verzeichnisstruktur (LINQ)
(C#)
Vorgehensweise: Abfragen der Gesamtzahl an Bytes in einem Ordnersatz (LINQ) (C#)
LINQ to Objects (C#)
04.11.2021 • 2 minutes to read

Die Bezeichnung „LINQ to Objects“ bezieht sich auf die direkte Verwendung von LINQ-Abfragen mit einer
beliebigen IEnumerable- oder IEnumerable<T>-Auflistung, ohne einen LINQ-Zwischenanbieter oder eine API
wie LINQ to SQL oder LINQ to XML zu verwenden. Sie können LINQ zur Abfrage beliebiger aufzählbarer
Auflistungen wie List<T>, Array oder Dictionary<TKey,TValue> verwenden. Die Sammlung kann
benutzerdefiniert sein oder von einer .NET-API zurückgegeben werden.
Im Grunde stellt LINQ to Objects einen neuen Ansatz für Auflistungen dar. Bisher mussten Sie komplexe
foreach -Schleifen erstellen, die angegeben haben, wie Daten aus einer Auflistung abgerufen werden. Im LINQ-
Ansatz verfassen Sie einen deklarativen Code, in dem beschrieben wird, was Sie abrufen möchten.
Zudem bieten LINQ-Abfragen drei wesentliche Vorteile gegenüber herkömmlichen foreach -Schleifen:
Sie sind präziser und lesbarer, insbesondere beim Filtern mehrerer Bedingungen.
Sie bieten mit minimalem Anwendungscode leistungsstarke Filter-, Sortier- und Gruppierungsfunktionen.
Sie können mit geringfügigen oder ohne Änderungen zu anderen Datenquellen portiert werden.
Je komplexer der für die Daten durchzuführende Vorgang, desto größer ist im Allgemeinen der Vorteil, den Sie
durch die Verwendung von LINQ anstelle der herkömmlichen Iterationsverfahren haben.
Der Zweck dieses Abschnitts ist es, den LINQ-Ansatz anhand einiger Beispiele zu veranschaulichen. Er ist nicht
als vollständig zu betrachten.

In diesem Abschnitt
LINQ and Strings (C#) (LINQ und Zeichenfolgen (C#))
Erläutert, wie LINQ zum Abfragen und Transformieren von Zeichenfolgen und Auflistungen von Zeichenfolgen
verwendet werden kann. Dieser Abschnitt umfasst auch Links zu Artikeln, die diese Prinzipien veranschaulichen.
LINQ and Reflection (C#) (LINQ und Reflektion (C#))
Verweist auf ein Beispiel, das darstellt, wie LINQ die Reflektion verwendet.
LINQ and File Directories (C#) (LINQ und Dateiverzeichnisse (C#))
Erläutert, wie LINQ für die Interaktion mit Dateisystemen verwendet werden kann. Dieser Abschnitt umfasst
auch Links zu Artikeln, die diese Konzepte veranschaulichen.
Vorgehensweise: Abfragen von ArrayList mit LINQ (C#)
Veranschaulicht die Abfrage einer ArrayList in Visual Basic und C#.
Vorgehensweise: Hinzufügen von benutzerdefinierten Methoden zu LINQ-Abfragen (C#)
Erläutert die Erweiterung des Methodensatzes, den Sie durch Hinzufügen von Erweiterungsmethoden zur
IEnumerable<T>-Schnittstelle verwenden können.
Language Integrated Query (LINQ) (C#)
In diesem Artikel werden Links zu Artikeln bereitgestellt, die LINQ erläutern und Codebeispiele enthalten, die
Abfragen durchführen.
LINQ und Zeichenfolgen (C#)
04.11.2021 • 2 minutes to read

LINQ kann zum Abfragen und Transformieren von Zeichenfolgen und Auflistungen von Zeichenfolgen
verwendet werden. Dieses Verfahren ist bei halbstrukturierten Daten in Textdateien besonders nützlich. LINQ-
Abfragen können mit herkömmlichen Zeichenfolgenfunktionen und regulären Ausdrücken verbunden werden.
Beispielsweise können Sie die Methode String.Split oder Regex.Split verwenden, um ein Array von
Zeichenfolgen zu erstellen, das Sie anschließend mit LINQ abfragen oder ändern können. Sie können die
Methode Regex.IsMatch in der where -Klausel einer LINQ-Abfrage verwenden. Sie können LINQ außerdem zum
Abfragen und Ändern der MatchCollection-Ergebnisse, die von einem regulären Ausdruck zurückgegeben
werden, verwenden.
Sie können auch die Techniken verwenden, die in diesem Abschnitt beschrieben werden, um halbstrukturierte
Textdaten in XML zu transformieren. Weitere Informationen finden Sie unter Vorgehensweise: Generieren von
XML aus CSV-Dateien.
Die Beispiele in diesem Abschnitt gehören zu einer der folgenden beiden Kategorien:

Abfragen eines Textblocks


Sie können Textblöcke abfragen, analysieren und ändern, indem Sie sie mithilfe der String.Split- oder der
Regex.Split-Methode in ein abfragbares Array von kleineren Zeichenfolgen aufteilen. Sie können den Quelltext in
Wörter, Sätze, Absätze, Paragraphen, Seiten oder andere Kriterien unterteilen und anschließend andere
Unterteilungen ausführen, wenn sie in Ihrer Abfrage benötigt werden.
Vorgehensweise: Zählen der Vorkommen eines Worts in einer Zeichenfolge (LINQ) (C#)
Zeigt die Verwendung von LINQ für einfache Textabfragen.
Vorgehensweise: Abfragen von Sätzen, die bestimmte Wörter enthalten (LINQ) (C#)
Zeigt, wie Textdateien an beliebigen Grenzen unterteilt wird und wie Abfragen mit jedem Teil ausgeführt
werden.
Vorgehensweise: Abfragen von Zeichen in einer Zeichenfolge (LINQ) (C#)
Veranschaulicht, dass eine Zeichenfolge ein abfragbarer Typ ist.
Vorgehensweise: Kombinieren von LINQ-Abfragen mit regulären Ausdrücken (C#)
Zeigt, wie reguläre Ausdrücke in LINQ-Abfragen für komplexe Musterabgleiche bei gefilterten
Abfrageergebnissen verwendet werden.

Abfragen halbstrukturierter Daten im Textformat


Viele verschiedene Typen von Textdateien bestehen aus einer Reihe von Zeilen, die häufig mit ähnlicher
Formatierung, z.B. durch Tabstopps oder Kommas getrennten Dateien oder Zeilen mit fester Länge. Nachdem
Sie solch eine Textdatei in den Arbeitsspeicher gelesen haben, können Sie LINQ zum Abfragen bzw. Ändern der
Zeilen verwenden. LINQ-Abfragen vereinfachen zudem die Aufgabe, Daten aus mehreren Quellen zu
kombinieren.
Vorgehensweise: Suchen der Unterschiedsmenge zwischen zwei Listen (LINQ) (C#)
Zeigt, wie alle Zeichenfolgen gesucht werden, die in einer Liste, aber nicht in der anderen, vorhanden sind
Vorgehensweise: Sortieren oder Filtern von Textdaten nach einem beliebigen Wort oder Feld (LINQ) (C#)
Zeigt, wie Textzeilen anhand eines beliebigen Worts oder Felds sortiert werden.
Vorgehensweise: Neuanordnen der Felder einer Datei mit Trennzeichen (LINQ) (C#)
Zeigt, wie Felder in einer Zeile in einer CSV-Datei neu angeordnet werden
Vorgehensweise: Verbinden und Vergleichen von Zeichenfolgenauflistungen (LINQ) (C#)
Zeigt, wie Zeichenfolgenlisten auf verschiedene Weise verbunden werden
Vorgehensweise: Auffüllen von Objektsammlungen mit Daten aus mehreren Quellen (LINQ) (C#)
Zeigt, wie Auflistungen erstellt werden, indem Sie mehrere Textdateien als Datenquellen verwenden
Vorgehensweise: Verknüpfen des Inhalts unterschiedlicher Dateien (LINQ) (C#)
Zeigt, wie Zeichenfolgen in zwei Listen mit einem übereinstimmenden Schlüssel in einer einzigen
Zeichenfolge vereint werden
Vorgehensweise: Teilen einer Datei in mehrere Dateien durch das Verwenden von Gruppen (LINQ) (C#)
Veranschaulicht, wie neue Dateien mithilfe einer einzelnen Datei als Datenquelle erstellt werden
Vorgehensweise: Berechnen von Spaltenwerten in einer CSV-Textdatei (LINQ) (C#)
Zeigt, wie mathematische Berechnungen bei Textdaten in CSV-Dateien ausgeführt werden

Siehe auch
Language Integrated Query (LINQ) (C#)
Generieren von XML aus CSV-Dateien
Vorgehensweise: Zählen der Vorkommen eines
Worts in einer Zeichenfolge (LINQ) (C#)
04.11.2021 • 2 minutes to read

Dieses Beispiel zeigt, wie Sie eine LINQ-Abfrage verwenden, um die Vorkommen eines angegebenen Worts in
einer Zeichenfolge zu zählen. Beachten Sie, dass zuerst die Split-Methode aufgerufen wird, um ein Array von
Worten zu erstellen, damit die Zählung ausgeführt werden kann. Für die Split-Methode werden Leistungskosten
berechnet. Wenn der einzige Vorgang in der Zeichenfolge die Zählung der Wörter ist, überlegen Sie sich, ob Sie
nicht stattdessen die Matches- oder IndexOf-Methode verwenden möchten. Wenn jedoch die Leistung kein
zentrales Problem ist oder Sie die Sequenz in eine Reihenfolge gebracht haben, um andere Abfragetypen darauf
auszuführen, macht es Sinn, auch LINQ zum Zählen der Wörter oder Sätze zu verwenden.

Beispiel
class CountWords
{
static void Main()
{
string text = @"Historically, the world of data and the world of objects" +
@" have not been well integrated. Programmers work in C# or Visual Basic" +
@" and also in SQL or XQuery. On the one side are concepts such as classes," +
@" objects, fields, inheritance, and .NET APIs. On the other side" +
@" are tables, columns, rows, nodes, and separate languages for dealing with" +
@" them. Data types often require translation between the two worlds; there are" +
@" different standard functions. Because the object world has no notion of query, a" +
@" query can only be represented as a string without compile-time type checking or" +
@" IntelliSense support in the IDE. Transferring data from SQL tables or XML trees to" +
@" objects in memory is often tedious and error-prone.";

string searchTerm = "data";

//Convert the string into an array of words


string[] source = text.Split(new char[] { '.', '?', '!', ' ', ';', ':', ',' },
StringSplitOptions.RemoveEmptyEntries);

// Create the query. Use ToLowerInvariant to match "data" and "Data"


var matchQuery = from word in source
where word.ToLowerInvariant() == searchTerm.ToLowerInvariant()
select word;

// Count the matches, which executes the query.


int wordCount = matchQuery.Count();
Console.WriteLine("{0} occurrences(s) of the search term \"{1}\" were found.", wordCount,
searchTerm);

// Keep console window open in debug mode


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
/* Output:
3 occurrences(s) of the search term "data" were found.
*/

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ und Zeichenfolgen (C#)
Vorgehensweise: Abfragen von Sätzen, die
bestimmte Wörter enthalten (LINQ) (C#)
04.11.2021 • 2 minutes to read

Dieses Beispiel zeigt, wie Sie Sätze in einer Textdatei suchen, die Übereinstimmungen für jedes Wort einer
bestimmten Gruppe von Wörtern enthält. Obwohl das Array von Suchbegriffen in diesem Beispiel hartcodiert
ist, kann es auch zur Laufzeit dynamisch aufgefüllt werden. In diesem Beispiel gibt die Abfrage die Sätze zurück,
die die Wörter „Historically“ (ursprünglich), „data“ (Daten) und „integrated“ (integriert) enthalten.

Beispiel
class FindSentences
{
static void Main()
{
string text = @"Historically, the world of data and the world of objects " +
@"have not been well integrated. Programmers work in C# or Visual Basic " +
@"and also in SQL or XQuery. On the one side are concepts such as classes, " +
@"objects, fields, inheritance, and .NET APIs. On the other side " +
@"are tables, columns, rows, nodes, and separate languages for dealing with " +
@"them. Data types often require translation between the two worlds; there are " +
@"different standard functions. Because the object world has no notion of query, a " +
@"query can only be represented as a string without compile-time type checking or " +
@"IntelliSense support in the IDE. Transferring data from SQL tables or XML trees to " +
@"objects in memory is often tedious and error-prone.";

// Split the text block into an array of sentences.


string[] sentences = text.Split(new char[] { '.', '?', '!' });

// Define the search terms. This list could also be dynamically populated at runtime.
string[] wordsToMatch = { "Historically", "data", "integrated" };

// Find sentences that contain all the terms in the wordsToMatch array.
// Note that the number of terms to match is not specified at compile time.
var sentenceQuery = from sentence in sentences
let w = sentence.Split(new char[] { '.', '?', '!', ' ', ';', ':', ',' },
StringSplitOptions.RemoveEmptyEntries)
where w.Distinct().Intersect(wordsToMatch).Count() == wordsToMatch.Count()
select sentence;

// Execute the query. Note that you can explicitly type


// the iteration variable here even though sentenceQuery
// was implicitly typed.
foreach (string str in sentenceQuery)
{
Console.WriteLine(str);
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
/* Output:
Historically, the world of data and the world of objects have not been well integrated
*/

Die Abfrage funktioniert, indem der Text zuerst in Sätze aufgeteilt wird. Diese Sätze werden wiederum in ein
Array von Zeichenfolgen aufgeteilt, das jedes Wort enthält. Für jedes dieser Arrays entfernt die Distinct-Methode
alle Wortduplikate, und anschließend führt die Abfrage einen Intersect-Vorgang für das Wortarray und das
wordsToMatch -Array durch. Wenn die Anzahl der Schnittmenge identisch mit der Anzahl des wordsToMatch -
Arrays ist, werden alle gefundenen Wörter in den Wörtern und der ursprüngliche Satz zurückgegeben.
Im Aufruf von Split werden die Satzzeichen als Trennlinien verwendet, damit sie aus der Zeichenfolge entfernt
werden können. Wenn Sie dies nicht getan haben, haben Sie z.B. eine „ursprüngliche“ Zeichenfolge, die
„usprünglich“ nicht mit dem wordsToMatch -Array übereinstimmen würde. Sie müssen möglicherweise
zusätzliche Trennzeichen verwenden, abhängig von den Satzzeichen, die im Quelltext vorkommen.

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ und Zeichenfolgen (C#)
Vorgehensweise: Abfragen von Zeichen in einer
Zeichenfolge (LINQ) (C#)
04.11.2021 • 2 minutes to read

Da die String-Klasse die generische IEnumerable<T>-Schnittstelle implementiert, kann jede Zeichenfolge als
Folge von Zeichen abgefragt werden. Dies ist allerdings kein üblicher Einsatz von LINQ. Verwenden Sie für
komplex Musterabgleichvorgänge die Regex-Klasse.

Beispiel
In folgendem Beispiel wird eine Zeichenfolge abgefragt, um die Zahl von enthaltenen numerischen Ziffern zu
bestimmen. Beachten Sie, dass die Abfrage „wieder verwendet“ wird, nachdem sie zum ersten Mal ausgeführt
wurde. Dies ist möglich, da die Abfrage selbst keine Ergebnisse speichert.

class QueryAString
{
static void Main()
{
string aString = "ABCDE99F-J74-12-89A";

// Select only those characters that are numbers


IEnumerable<char> stringQuery =
from ch in aString
where Char.IsDigit(ch)
select ch;

// Execute the query


foreach (char c in stringQuery)
Console.Write(c + " ");

// Call the Count method on the existing query.


int count = stringQuery.Count();
Console.WriteLine("Count = {0}", count);

// Select all characters before the first '-'


IEnumerable<char> stringQuery2 = aString.TakeWhile(c => c != '-');

// Execute the second query


foreach (char c in stringQuery2)
Console.Write(c);

Console.WriteLine(System.Environment.NewLine + "Press any key to exit");


Console.ReadKey();
}
}
/* Output:
Output: 9 9 7 4 1 2 8 9
Count = 8
ABCDE99F
*/

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.
Siehe auch
LINQ und Zeichenfolgen (C#)
Vorgehensweise: Kombinieren von LINQ-Abfragen mit regulären Ausdrücken (C#)
Vorgehensweise: Kombinieren von LINQ-Abfragen
mit regulären Ausdrücken (C#)
04.11.2021 • 2 minutes to read

In diesem Beispiel wird gezeigt, wie Sie die Regex-Klasse verwenden, um einen regulären Ausdruck für
komplexere Übereinstimmungen in Textzeichenfolgen erstellen. Die LINQ-Abfrage erleichtert das Filtern nach
genau den Dateien, die Sie mithilfe des regulären Ausdrucks suchen möchten, sowie das Formen der Ergebnisse.

Beispiel
class QueryWithRegEx
{
public static void Main()
{
// Modify this path as necessary so that it accesses your version of Visual Studio.
string startFolder = @"C:\Program Files (x86)\Microsoft Visual Studio 14.0\";
// One of the following paths may be more appropriate on your computer.
//string startFolder = @"C:\Program Files (x86)\Microsoft Visual Studio\2017\";

// Take a snapshot of the file system.


IEnumerable<System.IO.FileInfo> fileList = GetFiles(startFolder);

// Create the regular expression to find all things "Visual".


System.Text.RegularExpressions.Regex searchTerm =
new System.Text.RegularExpressions.Regex(@"Visual (Basic|C#|C\+\+|Studio)");

// Search the contents of each .htm file.


// Remove the where clause to find even more matchedValues!
// This query produces a list of files where a match
// was found, and a list of the matchedValues in that file.
// Note: Explicit typing of "Match" in select clause.
// This is required because MatchCollection is not a
// generic IEnumerable collection.
var queryMatchingFiles =
from file in fileList
where file.Extension == ".htm"
let fileText = System.IO.File.ReadAllText(file.FullName)
let matches = searchTerm.Matches(fileText)
where matches.Count > 0
select new
{
name = file.FullName,
matchedValues = from System.Text.RegularExpressions.Match match in matches
select match.Value
};

// Execute the query.


Console.WriteLine("The term \"{0}\" was found in:", searchTerm.ToString());

foreach (var v in queryMatchingFiles)


{
// Trim the path a bit, then write
// the file name in which a match was found.
string s = v.name.Substring(startFolder.Length - 1);
Console.WriteLine(s);

// For this file, write out all the matching strings


foreach (var v2 in v.matchedValues)
{
Console.WriteLine(" " + v2);
Console.WriteLine(" " + v2);
}
}

// Keep the console window open in debug mode


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}

// This method assumes that the application has discovery


// permissions for all folders under the specified path.
static IEnumerable<System.IO.FileInfo> GetFiles(string path)
{
if (!System.IO.Directory.Exists(path))
throw new System.IO.DirectoryNotFoundException();

string[] fileNames = null;


List<System.IO.FileInfo> files = new List<System.IO.FileInfo>();

fileNames = System.IO.Directory.GetFiles(path, "*.*", System.IO.SearchOption.AllDirectories);


foreach (string name in fileNames)
{
files.Add(new System.IO.FileInfo(name));
}
return files;
}
}

Beachten Sie, dass Sie auch das MatchCollection-Objekt abfragen können, das von einer RegEx -Suche
zurückgegeben wird. In diesem Beispiel wird nur der Wert jeder Übereinstimmung in den Ergebnissen
angezeigt. Allerdings ist es auch möglich, LINQ zu verwenden, um alle Arten von Filtern, Sortieren und
Gruppieren für diese Auflistung ausführen. Da MatchCollection eine nicht generische IEnumerable-Auflistung ist,
müssen Sie den Typ der Bereichsvariable in der Abfrage explizit angeben.

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ und Zeichenfolgen (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Ermitteln der Unterschiedsmenge
zwischen zwei Listen (LINQ) (C#)
04.11.2021 • 2 minutes to read

In diesem Beispiel wird veranschaulicht, wie Sie mit LINQ zwei Listen mit Zeichenfolgen vergleichen und die
Zeilen ausgeben, die in names1.txt, aber nicht in names2.txt enthalten sind.
So erstellen Sie die Datendateien
1. Kopieren Sie names1.txt und names2.txt in den Projektmappenordner, sowie in Vorgehensweise:
Kombinieren und Vergleichen Zeichenfolgenauflistungen (LINQ) (C#).

Beispiel
class CompareLists
{
static void Main()
{
// Create the IEnumerable data sources.
string[] names1 = System.IO.File.ReadAllLines(@"../../../names1.txt");
string[] names2 = System.IO.File.ReadAllLines(@"../../../names2.txt");

// Create the query. Note that method syntax must be used here.
IEnumerable<string> differenceQuery =
names1.Except(names2);

// Execute the query.


Console.WriteLine("The following lines are in names1.txt but not names2.txt");
foreach (string s in differenceQuery)
Console.WriteLine(s);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
/* Output:
The following lines are in names1.txt but not names2.txt
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra
*/

Einige Arten der Abfragevorgänge in C# wie Except, Distinct, Union und Concat können nur in
methodenbasierter Syntax ausgedrückt werden.

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ und Zeichenfolgen (C#)
Vorgehensweise: Sortieren oder Filtern von
Textdaten nach einem beliebigen Wort oder Feld
(LINQ) (C#)
04.11.2021 • 2 minutes to read

Das folgende Beispiel zeigt, wie Sie Zeilen aus strukturiertem Text nach jedem Feld in der Zeile sortieren können,
wie z.B. durch Trennzeichen getrennte Werte. Das Feld kann dynamisch zur Laufzeit festgelegt werden. Gehen
Sie davon aus, dass die Felder in scores.csv die Matrikelnummer eines Studenten repräsentieren, gefolgt von
einer Reihe aus vier Testergebnissen.
So erstellen Sie eine Datei, die Daten enthält
1. Kopieren Sie die Daten von „scores.csv“ aus dem Thema Vorgehensweise: Verknüpfen des Inhalts
unterschiedlicher Dateien (LINQ) (C#), und speichern Sie diese anschließen in Ihren Projektmappenordner.

Beispiel
public class SortLines
{
static void Main()
{
// Create an IEnumerable data source
string[] scores = System.IO.File.ReadAllLines(@"../../../scores.csv");

// Change this to any value from 0 to 4.


int sortField = 1;

Console.WriteLine("Sorted highest to lowest by field [{0}]:", sortField);

// Demonstrates how to return query from a method.


// The query is executed here.
foreach (string str in RunQuery(scores, sortField))
{
Console.WriteLine(str);
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}

// Returns the query variable, not query results!


static IEnumerable<string> RunQuery(IEnumerable<string> source, int num)
{
// Split the string and sort on field[num]
var scoreQuery = from line in source
let fields = line.Split(',')
orderby fields[num] descending
select line;

return scoreQuery;
}
}
/* Output (if sortField == 1):
Sorted highest to lowest by field [1]:
116, 99, 86, 90, 94
120, 99, 82, 81, 79
111, 97, 92, 81, 60
114, 97, 89, 85, 82
121, 96, 85, 91, 60
122, 94, 92, 91, 91
117, 93, 92, 80, 87
118, 92, 90, 83, 78
113, 88, 94, 65, 91
112, 75, 84, 91, 39
119, 68, 79, 88, 92
115, 35, 72, 91, 70
*/

Dieses Beispiel zeigt außerdem, wie Sie eine Abfragevariable aus einer Methode zurückgeben können.

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ und Zeichenfolgen (C#)
Vorgehensweise: Neuordnen der Felder einer Datei
mit Trennzeichen (LINQ) (C#)
04.11.2021 • 2 minutes to read

Eine Datei mit kommagetrennten Werten (CSV) ist eine Textdatei, die häufig verwendet wird, um
Tabellenkalkulationsdaten oder andere Tabellendaten zu speichern, die durch Zeilen und Spalten dargestellt
werden. Mithilfe der Methode Split zum Trennen von Feldern ist es sehr einfach, CSV-Dateien mithilfe von LINQ
abzufragen und zu bearbeiten. Tatsächlich kann das gleiche Verfahren verwendet werden, um die Teile von
beliebigen strukturierten Textzeilen neu anzuordnen. Es ist nicht auf CSV-Dateien beschränkt.
Im folgenden Beispiel wird davon ausgegangen, dass die drei Spalten den „Nachnamen“, „Vornamen“ und die
„ID“ von Schülern darstellen. Die Felder sind in alphabetischer Reihenfolge nach Nachnamen der Schüler
angeordnet. Die Abfrage erzeugt eine neue Sequenz, in der die ID-Spalte zuerst angezeigt wird, gefolgt von
einer zweiten Spalte, die Vornamen und Nachnamen des Schülers kombiniert. Die Zeilen werden gemäß dem
ID-Feld neu angeordnet. Die Ergebnisse werden in einer neuen Datei gespeichert, und die ursprünglichen Daten
werden nicht geändert.
So erstellen Sie die Datendatei
1. Kopieren Sie die folgenden Zeilen in eine Nur-Text-Datei mit dem Namen „spreadsheet1.csv“. Speichern
Sie die Datei im Projektordner.

Adams,Terry,120
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Cesar,114
Garcia,Debra,115
Garcia,Hugo,118
Mortensen,Sven,113
O'Donnell,Claire,112
Omelchenko,Svetlana,111
Tucker,Lance,119
Tucker,Michael,122
Zabokritski,Eugene,121

Beispiel
class CSVFiles
{
static void Main(string[] args)
{
// Create the IEnumerable data source
string[] lines = System.IO.File.ReadAllLines(@"../../../spreadsheet1.csv");

// Create the query. Put field 2 first, then


// reverse and combine fields 0 and 1 from the old field
IEnumerable<string> query =
from line in lines
let x = line.Split(',')
orderby x[2]
select x[2] + ", " + (x[1] + " " + x[0]);

// Execute the query and write out the new file. Note that WriteAllLines
// takes a string[], so ToArray is called on the query.
System.IO.File.WriteAllLines(@"../../../spreadsheet2.csv", query.ToArray());

Console.WriteLine("Spreadsheet2.csv written to disk. Press any key to exit");


Console.ReadKey();
}
}
/* Output to spreadsheet2.csv:
111, Svetlana Omelchenko
112, Claire O'Donnell
113, Sven Mortensen
114, Cesar Garcia
115, Debra Garcia
116, Fadi Fakhouri
117, Hanying Feng
118, Hugo Garcia
119, Lance Tucker
120, Terry Adams
121, Eugene Zabokritski
122, Michael Tucker
*/

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ und Zeichenfolgen (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Generieren von XML aus CSV-Dateien (C#)
Vorgehensweise: Verbinden und Vergleichen von
Zeichenfolgenauflistungen (LINQ) (C#)
04.11.2021 • 2 minutes to read

In diesem Beispiel wird veranschaulicht, wie Sie Dateien mit Textzeilen zusammenführen und die Ergebnisse
anschließend sortieren. Insbesondere wird gezeigt, wie eine einfache Verkettung, eine Vereinigung und eine
Schnittmenge von zwei Gruppen von Textzeilen ausgeführt wird.
So richten Sie das Projekt und die Textdateien ein
1. Kopieren Sie diese Namen in eine Textdatei namens „names1.txt“, und speichern Sie sie in Ihrem
Projektordner:

Bankov, Peter
Holm, Michael
Garcia, Hugo
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra

2. Kopieren Sie diese Namen in eine Textdatei namens „names2.txt“, und speichern Sie sie in Ihrem
Projektordner. Beachten Sie, dass die zwei Dateien einige Namen gemeinsam haben.

Liu, Jinghao
Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
Gilchrist, Beth
Myrcha, Jacek
Giakoumakis, Leo
McLin, Nkenge
El Yassir, Mehdi

Beispiel
class MergeStrings
{
static void Main(string[] args)
{
//Put text files in your solution folder
string[] fileA = System.IO.File.ReadAllLines(@"../../../names1.txt");
string[] fileB = System.IO.File.ReadAllLines(@"../../../names2.txt");

//Simple concatenation and sort. Duplicates are preserved.


IEnumerable<string> concatQuery =
fileA.Concat(fileB).OrderBy(s => s);

// Pass the query variable to another function for execution.


OutputQueryResults(concatQuery, "Simple concatenate and sort. Duplicates are preserved:");

// Concatenate and remove duplicate names based on


// Concatenate and remove duplicate names based on
// default string comparer.
IEnumerable<string> uniqueNamesQuery =
fileA.Union(fileB).OrderBy(s => s);
OutputQueryResults(uniqueNamesQuery, "Union removes duplicate names:");

// Find the names that occur in both files (based on


// default string comparer).
IEnumerable<string> commonNamesQuery =
fileA.Intersect(fileB);
OutputQueryResults(commonNamesQuery, "Merge based on intersect:");

// Find the matching fields in each list. Merge the two


// results by using Concat, and then
// sort using the default string comparer.
string nameMatch = "Garcia";

IEnumerable<String> tempQuery1 =
from name in fileA
let n = name.Split(',')
where n[0] == nameMatch
select name;

IEnumerable<string> tempQuery2 =
from name2 in fileB
let n2 = name2.Split(',')
where n2[0] == nameMatch
select name2;

IEnumerable<string> nameMatchQuery =
tempQuery1.Concat(tempQuery2).OrderBy(s => s);
OutputQueryResults(nameMatchQuery, $"Concat based on partial name match \"{nameMatch}\":");

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}

static void OutputQueryResults(IEnumerable<string> query, string message)


{
Console.WriteLine(System.Environment.NewLine + message);
foreach (string item in query)
{
Console.WriteLine(item);
}
Console.WriteLine("{0} total names in list", query.Count());
}
}
/* Output:
Simple concatenate and sort. Duplicates are preserved:
Aw, Kam Foo
Bankov, Peter
Bankov, Peter
Beebe, Ann
Beebe, Ann
El Yassir, Mehdi
Garcia, Debra
Garcia, Hugo
Garcia, Hugo
Giakoumakis, Leo
Gilchrist, Beth
Guy, Wey Yuan
Holm, Michael
Holm, Michael
Liu, Jinghao
McLin, Nkenge
Myrcha, Jacek
Noriega, Fabricio
Potra, Cristina
Toyoshima, Tim
Toyoshima, Tim
20 total names in list

Union removes duplicate names:


Aw, Kam Foo
Bankov, Peter
Beebe, Ann
El Yassir, Mehdi
Garcia, Debra
Garcia, Hugo
Giakoumakis, Leo
Gilchrist, Beth
Guy, Wey Yuan
Holm, Michael
Liu, Jinghao
McLin, Nkenge
Myrcha, Jacek
Noriega, Fabricio
Potra, Cristina
Toyoshima, Tim
16 total names in list

Merge based on intersect:


Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
4 total names in list

Concat based on partial name match "Garcia":


Garcia, Debra
Garcia, Hugo
Garcia, Hugo
3 total names in list
*/

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ und Zeichenfolgen (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Auffüllen von Objektsammlungen
mit Daten aus mehreren Quellen (LINQ) (C#)
04.11.2021 • 3 minutes to read

In diesem Beispiel erfahren Sie, wie Sie Daten aus unterschiedlichen Quellen in einer Sequenz aus neuen Typen
zusammenführen können.

NOTE
Versuchen Sie nicht, Daten im Arbeitsspeicher oder Daten im Dateisystem mit Daten, die sich noch in der Datenbank
befinden, zusammenzuführen. Derartige domänenübergreifende Verknüpfungen können aufgrund von möglichen
unterschiedlichen Definitionen von Verknüpfungsvorgänge für Datenbankabfragen und anderen Quelltypen zu
undefinierten Ergebnissen führen. Zusätzlich kann eine derartige Verknüpfung eine Ausnahme außerhalb des Speichers
verursachen, wenn die Datenmenge in der Datenbank groß genug ist. Um Daten aus einer Datenbank mit Daten im
Arbeitsspeicher zu verknüpfen, rufen Sie zuerst ToList oder ToArray in der Datenbankabfrage auf, und führen Sie
dann die Verknüpfung in der zurückgegebenen Auflistung durch.

So erstellen Sie die Datendatei


Kopieren Sie die Dateien „names.csv“ und „scores.csv“ in Ihren Projektordner wie unter Vorgehensweise:
Verknüpfen des Inhalts unterschiedlicher Dateien (LINQ) (C#) beschrieben.

Beispiel
In folgendem Beispiel erfahren Sie, wie Sie einen benannten Typ Student zum Speichern zusammengeführter
Daten aus zwei Zeichenfolgeauflistungen, die Arbeitsblätter im .csv-Format simulieren, im Arbeitsspeicher
verwenden könne. Die erste Zeichenfolgeauflistung stellt die Namen der Studenten und deren Matrikelnummer
dar, und die zweite Zeichenfolgeauflistung stellt die Matrikelnummer (in der ersten Spalte) und vier
Prüfungsergebnisse dar. Die Matrikelnummer wird als Fremdschlüssel verwendet.

using System;
using System.Collections.Generic;
using System.Linq;

class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int ID { get; set; }
public List<int> ExamScores { get; set; }
}

class PopulateCollection
{
static void Main()
{
// These data files are defined in How to join content from
// dissimilar files (LINQ).

// Each line of names.csv consists of a last name, a first name, and an


// ID number, separated by commas. For example, Omelchenko,Svetlana,111
string[] names = System.IO.File.ReadAllLines(@"../../../names.csv");

// Each line of scores.csv consists of an ID number and four test


// Each line of scores.csv consists of an ID number and four test
// scores, separated by commas. For example, 111, 97, 92, 81, 60
string[] scores = System.IO.File.ReadAllLines(@"../../../scores.csv");

// Merge the data sources using a named type.


// var could be used instead of an explicit type. Note the dynamic
// creation of a list of ints for the ExamScores member. The first item
// is skipped in the split string because it is the student ID,
// not an exam score.
IEnumerable<Student> queryNamesScores =
from nameLine in names
let splitName = nameLine.Split(',')
from scoreLine in scores
let splitScoreLine = scoreLine.Split(',')
where Convert.ToInt32(splitName[2]) == Convert.ToInt32(splitScoreLine[0])
select new Student()
{
FirstName = splitName[0],
LastName = splitName[1],
ID = Convert.ToInt32(splitName[2]),
ExamScores = (from scoreAsText in splitScoreLine.Skip(1)
select Convert.ToInt32(scoreAsText)).
ToList()
};

// Optional. Store the newly created student objects in memory


// for faster access in future queries. This could be useful with
// very large data files.
List<Student> students = queryNamesScores.ToList();

// Display each student's name and exam score average.


foreach (var student in students)
{
Console.WriteLine("The average score of {0} {1} is {2}.",
student.FirstName, student.LastName,
student.ExamScores.Average());
}

//Keep console window open in debug mode


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
The average score of Omelchenko Svetlana is 82.5.
The average score of O'Donnell Claire is 72.25.
The average score of Mortensen Sven is 84.5.
The average score of Garcia Cesar is 88.25.
The average score of Garcia Debra is 67.
The average score of Fakhouri Fadi is 92.25.
The average score of Feng Hanying is 88.
The average score of Garcia Hugo is 85.75.
The average score of Tucker Lance is 81.75.
The average score of Adams Terry is 85.25.
The average score of Zabokritski Eugene is 83.
The average score of Tucker Michael is 92.
*/

In der select-Klausel wird ein Objektinitialisierer zur Instanziierung jedes neuen Student -Objekts mithilfe der
Daten aus den beiden Quellen verwendet.
Wenn Sie die Ergebnisse einer Abfrage nicht speichern müssen, können anonyme Typen praktischer als
benannte Typen sein. Benannte Typen sind für die Übergabe von Abfrageergebnissen außerhalb der Methode, in
der die Abfrage ausgeführt wird, erforderlich. Im folgenden Beispiel wird die gleiche Aufgabe wie im vorherigen
Beispiel ausgeführt. Allerdings werden statt benannter anonyme Typen verwendet:
// Merge the data sources by using an anonymous type.
// Note the dynamic creation of a list of ints for the
// ExamScores member. We skip 1 because the first string
// in the array is the student ID, not an exam score.
var queryNamesScores2 =
from nameLine in names
let splitName = nameLine.Split(',')
from scoreLine in scores
let splitScoreLine = scoreLine.Split(',')
where Convert.ToInt32(splitName[2]) == Convert.ToInt32(splitScoreLine[0])
select new
{
First = splitName[0],
Last = splitName[1],
ExamScores = (from scoreAsText in splitScoreLine.Skip(1)
select Convert.ToInt32(scoreAsText))
.ToList()
};

// Display each student's name and exam score average.


foreach (var student in queryNamesScores2)
{
Console.WriteLine("The average score of {0} {1} is {2}.",
student.First, student.Last, student.ExamScores.Average());
}

Siehe auch
LINQ und Zeichenfolgen (C#)
Objekt- und Auflistungsinitialisierer
Anonyme Typen
Vorgehensweise: Aufteilen einer Datei in mehrere
Dateien durch Verwenden von Gruppen (LINQ)
(C#)
04.11.2021 • 2 minutes to read

Dieses Beispiel zeigt eine Möglichkeit, den Inhalt von zwei Dateien zusammenführen und dann einen Satz von
neuen Dateien zu erstellen, die die Daten auf neue Weise organisieren.
So erstellen Sie die Datendateien
1. Kopieren Sie diese Namen in eine Textdatei namens „names1.txt“, und speichern Sie sie in Ihrem
Projektordner:

Bankov, Peter
Holm, Michael
Garcia, Hugo
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra

2. Kopieren Sie diese Namen in eine Textdatei namens „names2.txt“, und speichern Sie sie in Ihrem
Projektordner: Beachten Sie, dass die beiden Dateien einige Namen gemeinsam haben.

Liu, Jinghao
Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
Gilchrist, Beth
Myrcha, Jacek
Giakoumakis, Leo
McLin, Nkenge
El Yassir, Mehdi

Beispiel
class SplitWithGroups
{
static void Main()
{
string[] fileA = System.IO.File.ReadAllLines(@"../../../names1.txt");
string[] fileB = System.IO.File.ReadAllLines(@"../../../names2.txt");

// Concatenate and remove duplicate names based on


// default string comparer
var mergeQuery = fileA.Union(fileB);

// Group the names by the first letter in the last name.


var groupQuery = from name in mergeQuery
let n = name.Split(',')
group name by n[0][0] into g
group name by n[0][0] into g
orderby g.Key
select g;

// Create a new file for each group that was created


// Note that nested foreach loops are required to access
// individual items with each group.
foreach (var g in groupQuery)
{
// Create the new file name.
string fileName = @"../../../testFile_" + g.Key + ".txt";

// Output to display.
Console.WriteLine(g.Key);

// Write file.
using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fileName))
{
foreach (var item in g)
{
sw.WriteLine(item);
// Output to console for example purposes.
Console.WriteLine(" {0}", item);
}
}
}
// Keep console window open in debug mode.
Console.WriteLine("Files have been written. Press any key to exit");
Console.ReadKey();
}
}
/* Output:
A
Aw, Kam Foo
B
Bankov, Peter
Beebe, Ann
E
El Yassir, Mehdi
G
Garcia, Hugo
Guy, Wey Yuan
Garcia, Debra
Gilchrist, Beth
Giakoumakis, Leo
H
Holm, Michael
L
Liu, Jinghao
M
Myrcha, Jacek
McLin, Nkenge
N
Noriega, Fabricio
P
Potra, Cristina
T
Toyoshima, Tim
*/

Das Programm schreibt eine separate Datei für jede Gruppe im gleichen Ordner wie die Datendateien.

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.
Siehe auch
LINQ und Zeichenfolgen (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Verknüpfen des Inhalts
unterschiedlicher Dateien (LINQ) (C#)
04.11.2021 • 2 minutes to read

In diesem Beispiel wird veranschaulicht, wie Daten aus zwei durch Trennzeichen getrennten Dateien mit
gemeinsamem Wert, der als übereinstimmender Schlüssel verwendet wird, verknüpft werden. Diese Technik
kann hilfreich sein, wenn Sie Daten aus zwei Arbeitsblättern oder aus einem Arbeitsblatt und einer Datei, die ein
anderes Format aufweist, in einer neuen Datei kombinieren möchten. Sie können das Beispiel auch abändern,
damit es mit jeder Art von strukturiertem Text funktioniert.

So erstellen Sie die Datendateien


1. Kopieren Sie die folgenden Zeilen in eine Datei namens scores.csv, und speichern Sie sie in Ihrem
Projektordner. Diese Datei stellt das Arbeitsblatt dar. Spalte 1 enthält die ID des Studierenden und die
Spalten 2 bis 5 enthalten die Testergebnisse.

111, 97, 92, 81, 60


112, 75, 84, 91, 39
113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91

2. Kopieren Sie die folgenden Zeilen in eine Datei namens names.csv, und speichern Sie sie in Ihrem
Projektordner. Die Datei stellt ein Arbeitsblatt dar, das den Nachnamen, den Vornamen und die ID des
Studierenden enthält.

Omelchenko,Svetlana,111
O'Donnell,Claire,112
Mortensen,Sven,113
Garcia,Cesar,114
Garcia,Debra,115
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Hugo,118
Tucker,Lance,119
Adams,Terry,120
Zabokritski,Eugene,121
Tucker,Michael,122

Beispiel
using System;
using System.Collections.Generic;
using System.Linq;

class JoinStrings
class JoinStrings
{
static void Main()
{
// Join content from dissimilar files that contain
// related information. File names.csv contains the student
// name plus an ID number. File scores.csv contains the ID
// and a set of four test scores. The following query joins
// the scores to the student names by using ID as a
// matching key.

string[] names = System.IO.File.ReadAllLines(@"../../../names.csv");


string[] scores = System.IO.File.ReadAllLines(@"../../../scores.csv");

// Name: Last[0], First[1], ID[2]


// Omelchenko, Svetlana, 11
// Score: StudentID[0], Exam1[1] Exam2[2], Exam3[3], Exam4[4]
// 111, 97, 92, 81, 60

// This query joins two dissimilar spreadsheets based on common ID value.


// Multiple from clauses are used instead of a join clause
// in order to store results of id.Split.
IEnumerable<string> scoreQuery1 =
from name in names
let nameFields = name.Split(',')
from id in scores
let scoreFields = id.Split(',')
where Convert.ToInt32(nameFields[2]) == Convert.ToInt32(scoreFields[0])
select nameFields[0] + "," + scoreFields[1] + "," + scoreFields[2]
+ "," + scoreFields[3] + "," + scoreFields[4];

// Pass a query variable to a method and execute it


// in the method. The query itself is unchanged.
OutputQueryResults(scoreQuery1, "Merge two spreadsheets:");

// Keep console window open in debug mode.


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}

static void OutputQueryResults(IEnumerable<string> query, string message)


{
Console.WriteLine(System.Environment.NewLine + message);
foreach (string item in query)
{
Console.WriteLine(item);
}
Console.WriteLine("{0} total names in list", query.Count());
}
}
/* Output:
Merge two spreadsheets:
Omelchenko, 97, 92, 81, 60
O'Donnell, 75, 84, 91, 39
Mortensen, 88, 94, 65, 91
Garcia, 97, 89, 85, 82
Garcia, 35, 72, 91, 70
Fakhouri, 99, 86, 90, 94
Feng, 93, 92, 80, 87
Garcia, 92, 90, 83, 78
Tucker, 68, 79, 88, 92
Adams, 99, 82, 81, 79
Zabokritski, 96, 85, 91, 60
Tucker, 94, 92, 91, 91
12 total names in list
*/
Weitere Informationen
LINQ und Zeichenfolgen (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Berechnen von Spaltenwerten in
einer CSV-Textdatei (LINQ) (C#)
04.11.2021 • 3 minutes to read

In diesem Beispiel wird veranschaulicht, wie Sie Aggregatberechnungen wie „Sum“, „Average“, „Min“ und „Max“
für die Spalten einer CSV-Datei ausführen. Die hier gezeigten Beispielprinzipien können auf andere Typen von
strukturiertem Text angewendet werden.

So erstellen Sie die Quelldatei


1. Kopieren Sie die folgenden Zeilen in eine Datei namens „scores.csv“, und speichern Sie sie in Ihrem
Projektordner. Angenommen, die erste Spalte enthält eine Schüler-ID und die nachfolgende Spalten
stellen die Noten aus vier Prüfungen dar.

111, 97, 92, 81, 60


112, 75, 84, 91, 39
113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91

Beispiel
class SumColumns
{
static void Main(string[] args)
{
string[] lines = System.IO.File.ReadAllLines(@"../../../scores.csv");

// Specifies the column to compute.


int exam = 3;

// Spreadsheet format:
// Student ID Exam#1 Exam#2 Exam#3 Exam#4
// 111, 97, 92, 81, 60

// Add one to exam to skip over the first column,


// which holds the student ID.
SingleColumn(lines, exam + 1);
Console.WriteLine();
MultiColumns(lines);

Console.WriteLine("Press any key to exit");


Console.ReadKey();
}

static void SingleColumn(IEnumerable<string> strs, int examNum)


{
Console.WriteLine("Single Column Query:");
// Parameter examNum specifies the column to
// run the calculations on. This value could be
// passed in dynamically at runtime.

// Variable columnQuery is an IEnumerable<int>.


// The following query performs two steps:
// 1) use Split to break each row (a string) into an array
// of strings,
// 2) convert the element at position examNum to an int
// and select it.
var columnQuery =
from line in strs
let elements = line.Split(',')
select Convert.ToInt32(elements[examNum]);

// Execute the query and cache the results to improve


// performance. This is helpful only with very large files.
var results = columnQuery.ToList();

// Perform aggregate calculations Average, Max, and


// Min on the column specified by examNum.
double average = results.Average();
int max = results.Max();
int min = results.Min();

Console.WriteLine("Exam #{0}: Average:{1:##.##} High Score:{2} Low Score:{3}",


examNum, average, max, min);
}

static void MultiColumns(IEnumerable<string> strs)


{
Console.WriteLine("Multi Column Query:");

// Create a query, multiColQuery. Explicit typing is used


// to make clear that, when executed, multiColQuery produces
// nested sequences. However, you get the same results by
// using 'var'.

// The multiColQuery query performs the following steps:


// 1) use Split to break each row (a string) into an array
// of strings,
// 2) use Skip to skip the "Student ID" column, and store the
// rest of the row in scores.
// 3) convert each score in the current row from a string to
// an int, and select that entire sequence as one row
// in the results.
IEnumerable<IEnumerable<int>> multiColQuery =
from line in strs
let elements = line.Split(',')
let scores = elements.Skip(1)
select (from str in scores
select Convert.ToInt32(str));

// Execute the query and cache the results to improve


// performance.
// ToArray could be used instead of ToList.
var results = multiColQuery.ToList();

// Find out how many columns you have in results.


int columnCount = results[0].Count();

// Perform aggregate calculations Average, Max, and


// Min on each column.
// Perform one iteration of the loop for each column
// of scores.
// You can use a for loop instead of a foreach loop
// because you already executed the multiColQuery
// query by calling ToList.
for (int column = 0; column < columnCount; column++)
for (int column = 0; column < columnCount; column++)
{
var results2 = from row in results
select row.ElementAt(column);
double average = results2.Average();
int max = results2.Max();
int min = results2.Min();

// Add one to column because the first exam is Exam #1,


// not Exam #0.
Console.WriteLine("Exam #{0} Average: {1:##.##} High Score: {2} Low Score: {3}",
column + 1, average, max, min);
}
}
}
/* Output:
Single Column Query:
Exam #4: Average:76.92 High Score:94 Low Score:39

Multi Column Query:


Exam #1 Average: 86.08 High Score: 99 Low Score: 35
Exam #2 Average: 86.42 High Score: 94 Low Score: 72
Exam #3 Average: 84.75 High Score: 91 Low Score: 65
Exam #4 Average: 76.92 High Score: 94 Low Score: 39
*/

Die Abfrage nutzt die Methode Split, um jede Textzeile in ein Array zu konvertieren. Jedes Arrayelement stellt
eine Spalte dar. Schließlich wird der Text in jeder Spalte in die entsprechend numerische Darstellung konvertiert.
Wenn es sich bei der Datei um eine tabstoppgetrennte Datei handelt, aktualisieren Sie einfach das Argument in
der Split -Methode auf \t .

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ und Zeichenfolgen (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Abfragen der Metadaten einer
Assembly mit Reflexion (LINQ) (C#)
04.11.2021 • 2 minutes to read

Die Reflektions-APIs von .NET können verwendet werden, um die Metadaten in einer .NET-Assembly zu
untersuchen und Sammlungen von Typen, Typmembern, Parametern usw. zu erstellen, die sich in der Assembly
befinden. Da diese Auflistungen die generische IEnumerable<T>-Schnittstelle unterstützen, können sie mithilfe
von LINQ abgefragt werden.
Das folgende Beispiel zeigt, wie LINQ mit Reflektion verwendet werden kann, um bestimmte Metadaten über
Methoden abzurufen, die einem angegebenen Suchkriterium entsprechen. In diesem Fall findet die Abfrage die
Namen aller Methoden in der Assembly, die aufzählbare Typen zurückgeben wie z.B. Arrays.

Beispiel
using System;
using System.Linq;
using System.Reflection;

class ReflectionHowTO
{
static void Main()
{
Assembly assembly = Assembly.Load("System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=
b77a5c561934e089");
var pubTypesQuery = from type in assembly.GetTypes()
where type.IsPublic
from method in type.GetMethods()
where method.ReturnType.IsArray == true
|| ( method.ReturnType.GetInterface(
typeof(System.Collections.Generic.IEnumerable<>).FullName ) != null
&& method.ReturnType.FullName != "System.String" )
group method.ToString() by type.ToString();

foreach (var groupOfMethods in pubTypesQuery)


{
Console.WriteLine("Type: {0}", groupOfMethods.Key);
foreach (var method in groupOfMethods)
{
Console.WriteLine(" {0}", method);
}
}

Console.WriteLine("Press any key to exit... ");


Console.ReadKey();
}
}

Im Beispiel wird die Assembly.GetTypes-Methode verwendet, um ein Array von Typen in der angegebenen
Assembly zurückzugeben. Der where-Filter wird angewendet, sodass nur öffentliche Typen zurückgegeben
werden. Für jeden öffentlichen Typ wird mit dem MethodInfo-Array eine Unterabfrage generiert, die vom
Type.GetMethods-Aufruf zurückgegeben wird. Diese Ergebnisse werden gefiltert, damit nur die Methoden
zurückgegeben werden, deren Rückgabetyp ein Array oder ein Typ ist, der IEnumerable<T> implementiert.
Abschließend werden die Ergebnisse mithilfe des Typnamens als Schlüssel gruppiert.
Siehe auch
LINQ to Objects (C#)
Vorgehensweise: Abfragen der Metadaten einer
Assembly mit Reflexion (LINQ) (C#)
04.11.2021 • 2 minutes to read

Die Reflektions-APIs von .NET können verwendet werden, um die Metadaten in einer .NET-Assembly zu
untersuchen und Sammlungen von Typen, Typmembern, Parametern usw. zu erstellen, die sich in der Assembly
befinden. Da diese Auflistungen die generische IEnumerable<T>-Schnittstelle unterstützen, können sie mithilfe
von LINQ abgefragt werden.
Das folgende Beispiel zeigt, wie LINQ mit Reflektion verwendet werden kann, um bestimmte Metadaten über
Methoden abzurufen, die einem angegebenen Suchkriterium entsprechen. In diesem Fall findet die Abfrage die
Namen aller Methoden in der Assembly, die aufzählbare Typen zurückgeben wie z.B. Arrays.

Beispiel
using System;
using System.Linq;
using System.Reflection;

class ReflectionHowTO
{
static void Main()
{
Assembly assembly = Assembly.Load("System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=
b77a5c561934e089");
var pubTypesQuery = from type in assembly.GetTypes()
where type.IsPublic
from method in type.GetMethods()
where method.ReturnType.IsArray == true
|| ( method.ReturnType.GetInterface(
typeof(System.Collections.Generic.IEnumerable<>).FullName ) != null
&& method.ReturnType.FullName != "System.String" )
group method.ToString() by type.ToString();

foreach (var groupOfMethods in pubTypesQuery)


{
Console.WriteLine("Type: {0}", groupOfMethods.Key);
foreach (var method in groupOfMethods)
{
Console.WriteLine(" {0}", method);
}
}

Console.WriteLine("Press any key to exit... ");


Console.ReadKey();
}
}

Im Beispiel wird die Assembly.GetTypes-Methode verwendet, um ein Array von Typen in der angegebenen
Assembly zurückzugeben. Der where-Filter wird angewendet, sodass nur öffentliche Typen zurückgegeben
werden. Für jeden öffentlichen Typ wird mit dem MethodInfo-Array eine Unterabfrage generiert, die vom
Type.GetMethods-Aufruf zurückgegeben wird. Diese Ergebnisse werden gefiltert, damit nur die Methoden
zurückgegeben werden, deren Rückgabetyp ein Array oder ein Typ ist, der IEnumerable<T> implementiert.
Abschließend werden die Ergebnisse mithilfe des Typnamens als Schlüssel gruppiert.
Siehe auch
LINQ to Objects (C#)
LINQ und Dateiverzeichnisse (C#)
04.11.2021 • 2 minutes to read

Viele Dateisystemvorgänge sind im Wesentlichen Abfragen und sind deshalb für die LINQ-Vorgehensweise gut
geeignet.
Die Abfragen in diesem Abschnitt sind nicht destruktiv. Sie werden nicht verwendet, um den Inhalt der
ursprünglichen Dateien und Ordner zu ändern. Dies entspricht der Regel, nach der Abfragen keine Nebeneffekte
haben sollen. Allgemein gilt, dass jeder Code (einschließlich Abfragen, die Operatoren zum
Erstellen/Aktualisieren/Löschen ausführen), der Quelldaten modifiziert, von Code getrennt sein sollte, der Daten
lediglich abfragt.
Dieser Abschnitt enthält die folgenden Themen:
Vorgehensweise: Abfragen von Dateien mit einem angegebenen Attribut oder Namen (C#)
Hier erfahren Sie, wie Sie nach Dateien suchen, indem Sie eine oder mehrere Eigenschaften des Objekts FileInfo
betrachten.
Vorgehensweise: Gruppieren von Dateien nach Erweiterung (LINQ) (C#)
Hier erfahren Sie, wie Sie Gruppen des Objekts FileInfo nach deren Dateinamenerweiterung zurückgeben.
Vorgehensweise: Abfragen der Gesamtzahl an Bytes in einem Ordnersatz (LINQ) (C#)
Hier erfahren Sie, wie Sie die Gesamtzahl von Bytes in allen Dateien einer angegebenen Verzeichnisstruktur
zurückgeben.
Vorgehensweise: Vergleichen des Inhalts von zwei Ordnern (LINQ) (C#)
Hier erfahren Sie, wie Sie alle Dateien, die sich ein zwei angegebenen Ordnern befinden, zurückgeben können;
ebenso erfahren Sie, wie Sie die Dateien, die sich nur in einem der zwei Ordner befinden, zurückgeben können.
Vorgehensweise: Abfragen der größten Datei oder der größten Dateien in einer Verzeichnisstruktur (LINQ) (C#)
Hier erfahren Sie, wie Sie die größte oder kleinste Datei oder eine angegebene Anzahl von Dateien in einer
Verzeichnisstruktur zurückgeben können.
Vorgehensweise: Abfragen von Dateiduplikaten in einer Verzeichnisstruktur (LINQ) (C#)
Hier erfahren Sie, wie Sie die Dateien nach Dateinamen gruppieren können, die an mehr als einer Stelle in einer
angegebenen Verzeichnisstruktur vorkommen. Ebenfalls lernen Sie, wie sie komplexere Vergleiche mit einer
benutzerdefinierten Vergleichsfunktion durchführen können.
Vorgehensweise: Abfragen des Inhalts von Dateien in einem Ordner (LINQ) (C#)
Hier erfahren Sie, wie Sie Ordner in einer Struktur durchlaufen, jede Datei öffnen und die Inhalte der Datei
abfragen können.

Kommentare
Das Erstellen einer Datenquelle, die die Inhalte des Dateisystems angemessen repräsentieren, kommt mit einer
gewissen Komplexität. Die Beispiele in diesem Abschnitt geben Ihnen einen ersten Überblick über FileInfo-
Objekte, der alle Dateien in einem angegebenen Stammordner mit all seinen Unterordnern repräsentiert. Der
tatsächliche Zustand jedes FileInfo verändert sich möglicherweise in der Zeit zwischen Beginn und Ende der
Ausführung einer Abfrage. Sie können beispielsweise eine Liste von FileInfo-Objekten erstellen, um diese als
Datenquelle zu verwenden. Wenn Sie versuchen, auf die Eigenschaft Length einer Abfrage zuzugreifen,
versucht das FileInfo-Objekt, auf das Dateisystem zuzugreifen, um den Wert von Length zu aktualisieren. Wenn
die Datei nicht mehr existiert, erhalten Sie eine FileNotFoundException in Ihrer Abfrage, obwohl Sie keine direkte
Abfrage an das Dateisystem vornehmen. Einige Abfragen in diesem Bereich verwenden eine getrennte Methode,
die diese bestimmten Ausnahmen in gewissen Fällen verarbeitet. Des Weiteren können Sie Ihre Datenquelle auf
dem neuesten Stand halten, indem Sie den FileSystemWatcher verwenden.

Siehe auch
LINQ to Objects (C#)
Vorgehensweise: Abfragen von Dateien mit einem
angegebenen Attribut oder Namen (C#)
04.11.2021 • 2 minutes to read

In diesem Beispiel wird veranschaulicht, wie nach allen Dateien mit einem angegebenen Suffix (z.B. „.txt“) in
einer angegebenen Verzeichnisstruktur gesucht wird. Es wird auch gezeigt, wie basierend auf dem Zeitpunkt der
Erstellung entweder die neueste oder älteste Datei in der Struktur zurückgegeben wird.

Beispiel
class FindFileByExtension
{
// This query will produce the full path for all .txt files
// under the specified folder including subfolders.
// It orders the list according to the file name.
static void Main()
{
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\";

// Take a snapshot of the file system.


System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(startFolder);

// This method assumes that the application has discovery permissions


// for all folders under the specified path.
IEnumerable<System.IO.FileInfo> fileList = dir.GetFiles("*.*",
System.IO.SearchOption.AllDirectories);

//Create the query


IEnumerable<System.IO.FileInfo> fileQuery =
from file in fileList
where file.Extension == ".txt"
orderby file.Name
select file;

//Execute the query. This might write out a lot of files!


foreach (System.IO.FileInfo fi in fileQuery)
{
Console.WriteLine(fi.FullName);
}

// Create and execute a new query by using the previous


// query as a starting point. fileQuery is not
// executed again until the call to Last()
var newestFile =
(from file in fileQuery
orderby file.CreationTime
select new { file.FullName, file.CreationTime })
.Last();

Console.WriteLine("\r\nThe newest .txt file is {0}. Creation time: {1}",


newestFile.FullName, newestFile.CreationTime);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
Kompilieren des Codes
Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ to Objects (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Gruppieren von Dateien nach
Erweiterung (LINQ) (C#)
04.11.2021 • 2 minutes to read

In dieses Beispiel wird veranschaulicht, wie Sie mithilfe von LINQ erweiterte Gruppierungs- und
Sortiervorgänge mit Datei- oder Ordnerlisten ausführen können. Es zeigt auch, wie der Bildlauf für die Ausgabe
im Konsolenfenster mithilfe der Methoden Skip und Take durchgeführt wird.

Beispiel
Die folgende Abfrage zeigt, wie Sie den Inhalt einer angegebenen Verzeichnisstruktur nach der
Dateierweiterung gruppieren.

class GroupByExtension
{
// This query will sort all the files under the specified folder
// and subfolder into groups keyed by the file extension.
private static void Main()
{
// Take a snapshot of the file system.
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\Common7";

// Used in WriteLine to trim output lines.


int trimLength = startFolder.Length;

// Take a snapshot of the file system.


System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(startFolder);

// This method assumes that the application has discovery permissions


// for all folders under the specified path.
IEnumerable<System.IO.FileInfo> fileList = dir.GetFiles("*.*",
System.IO.SearchOption.AllDirectories);

// Create the query.


var queryGroupByExt =
from file in fileList
group file by file.Extension.ToLower() into fileGroup
orderby fileGroup.Key
select fileGroup;

// Display one group at a time. If the number of


// entries is greater than the number of lines
// in the console window, then page the output.
PageOutput(trimLength, queryGroupByExt);
}

// This method specifically handles group queries of FileInfo objects with string keys.
// It can be modified to work for any long listings of data. Note that explicit typing
// must be used in method signatures. The groupbyExtList parameter is a query that produces
// groups of FileInfo objects with string keys.
private static void PageOutput(int rootLength,
IEnumerable<System.Linq.IGrouping<string, System.IO.FileInfo>>
groupByExtList)
{
// Flag to break out of paging loop.
bool goAgain = true;

// "3" = 1 line for extension + 1 for "Press any key" + 1 for input cursor.
int numLines = Console.WindowHeight - 3;
int numLines = Console.WindowHeight - 3;

// Iterate through the outer collection of groups.


foreach (var filegroup in groupByExtList)
{
// Start a new extension at the top of a page.
int currentLine = 0;

// Output only as many lines of the current group as will fit in the window.
do
{
Console.Clear();
Console.WriteLine(filegroup.Key == String.Empty ? "[none]" : filegroup.Key);

// Get 'numLines' number of items starting at number 'currentLine'.


var resultPage = filegroup.Skip(currentLine).Take(numLines);

//Execute the resultPage query


foreach (var f in resultPage)
{
Console.WriteLine("\t{0}", f.FullName.Substring(rootLength));
}

// Increment the line counter.


currentLine += numLines;

// Give the user a chance to escape.


Console.WriteLine("Press any key to continue or the 'End' key to break...");
ConsoleKey key = Console.ReadKey().Key;
if (key == ConsoleKey.End)
{
goAgain = false;
break;
}
} while (currentLine < filegroup.Count());

if (goAgain == false)
break;
}
}
}

Die Ausgabe dieses Programms kann je nach den Details des lokalen Dateisystems und der Einstellung für
startFolder lang sein. Um alle Ergebnisse anzuzeigen, wird in diesem Beispiel gezeigt, wie Sie Ergebnisse
seitenweise anzeigen. Die gleichen Techniken können auf Windows- und Webanwendungen angewendet
werden. Beachten Sie, dass eine geschachtelte foreach -Schleife erforderlich ist, da der Code die Elemente in
einer Gruppe seitenweise anzeigt. Es ist auch sinnvoll, die aktuelle Position in der Liste berechnen zu können und
es dem Benutzer zu ermöglichen, das seitenweise Anzeigen anzuhalten und das Programm zu beenden. In
diesem speziellen Fall wird die Abfrage zum seitenweise Anzeigen gegen die zwischengespeicherten Ergebnisse
aus der ursprünglichen Abfrage ausgeführt. In anderen Kontexten wie LINQ to SQL ist ein solches
Zwischenspeichern nicht erforderlich.

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ to Objects (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Abfragen der Gesamtzahl an Bytes
in einem Ordnersatz (LINQ) (C#)
04.11.2021 • 2 minutes to read

Dieses Beispiel zeigt, wie die Gesamtanzahl der Bytes, die von allen Dateien in einem angegebenen Ordner und
allen Unterordnern verwendet werden, abgerufen wird.

Beispiel
Die Sum-Methode fügt die Werte aller Elemente hinzu, die in der select -Klausel ausgewählt wurden. Sie
können diese Abfrage leicht modifizieren, um die größte oder kleinste Datei in der angegebenen
Verzeichnisstruktur abzurufen, indem Sie die Min- oder Max-Methode statt der Sum-Methode verwenden.
class QuerySize
{
public static void Main()
{
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\VC#";

// Take a snapshot of the file system.


// This method assumes that the application has discovery permissions
// for all folders under the specified path.
IEnumerable<string> fileList = System.IO.Directory.GetFiles(startFolder, "*.*",
System.IO.SearchOption.AllDirectories);

var fileQuery = from file in fileList


select GetFileLength(file);

// Cache the results to avoid multiple trips to the file system.


long[] fileLengths = fileQuery.ToArray();

// Return the size of the largest file


long largestFile = fileLengths.Max();

// Return the total number of bytes in all the files under the specified folder.
long totalBytes = fileLengths.Sum();

Console.WriteLine("There are {0} bytes in {1} files under {2}",


totalBytes, fileList.Count(), startFolder);
Console.WriteLine("The largest files is {0} bytes.", largestFile);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}

// This method is used to swallow the possible exception


// that can be raised when accessing the System.IO.FileInfo.Length property.
static long GetFileLength(string filename)
{
long retval;
try
{
System.IO.FileInfo fi = new System.IO.FileInfo(filename);
retval = fi.Length;
}
catch (System.IO.FileNotFoundException)
{
// If a file is no longer present,
// just add zero bytes to the total.
retval = 0;
}
return retval;
}
}

Wenn Sie nur die Anzahl der Bytes in einer angegebenen Verzeichnisstruktur ermitteln wollen, können Sie dies
ohne LINQ-Abfrage effizienter tun, da diese zunächst die Listenauflistung als Datenquelle erstellt, was
Mehraufwand verursacht. Die Nützlichkeit des LINQ-Ansatzes wird erhöht, wenn die Abfrage komplexer wird
oder wenn Sie mehrere Abfragen für dieselbe Datenquelle ausführen.
Die Abfrage ruft eine separate Methode auf, um die Dateilänge zu erhalten. Dadurch wird die mögliche
Ausnahme abgefangen, die ausgelöst wird, wenn die Datei auf einem anderen Thread gelöscht wurde, nachdem
das FileInfo-Objekt im Aufruf von GetFiles erstellt wurde. Obwohl das FileInfo-Objekt bereits erstellt wurde,
kann die Ausnahme auftreten, weil ein FileInfo-Objekt versucht, seine Length-Eigenschaft mit der aktuellsten
Länge beim ersten Zugriff auf die Eigenschaft zu aktualisieren. Indem dieser Vorgang in einen Try-Catch-Block
außerhalb der Abfrage erfolgt, folgt der Code der Regel zum Vermeiden von Vorgängen in Abfragen, die
Nebeneffekte verursachen können. Im Allgemeinen ist beim Abfangen von Ausnahmen große Sorgfalt geboten,
um sicherzustellen, dass Anwendungen nicht in einem unbekannten Zustand verbleiben.

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ to Objects (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Vergleichen des Inhalts von zwei
Ordnern (LINQ) (C#)
04.11.2021 • 2 minutes to read

Dieses Beispiel zeigt drei Verfahren zum Vergleichen von zwei Dateilisten:
Durch Abfragen eines booleschen Werts, der angibt, ob die zwei Dateilisten identisch sind.
Durch Abfragen der Schnittmenge, um die Dateien abzurufen, die sich in beiden Ordnern befinden.
Durch Abfragen der Unterschiedsmenge, um die Dateien abzurufen, die sich in einem Ordner befinden,
aber nicht im anderen.

NOTE
Die hier gezeigten Verfahren können zum Vergleichen von Sequenzen von Objekten eines beliebigen Typs
angepasst werden.

Die hier gezeigte FileComparer -Klasse veranschaulicht, wie eine benutzerdefinierte Vergleichsklasse zusammen
mit den Standardabfrageoperatoren verwendet wird. Die Klasse ist nicht für die Verwendung in realen Szenarios
vorgesehen. Sie verwendet nur den Namen und die Länge jeder Datei in Bytes, um zu bestimmen, ob die Inhalte
der einzelnen Ordner identisch sind. In einem realen Szenario sollten Sie diese Vergleichsklasse ändern, um eine
gründlichere Überprüfung auf Gleichheit durchzuführen.

Beispiel
namespace QueryCompareTwoDirs
{
class CompareDirs
{

static void Main(string[] args)


{

// Create two identical or different temporary folders


// on a local drive and change these file paths.
string pathA = @"C:\TestDir";
string pathB = @"C:\TestDir2";

System.IO.DirectoryInfo dir1 = new System.IO.DirectoryInfo(pathA);


System.IO.DirectoryInfo dir2 = new System.IO.DirectoryInfo(pathB);

// Take a snapshot of the file system.


IEnumerable<System.IO.FileInfo> list1 = dir1.GetFiles("*.*",
System.IO.SearchOption.AllDirectories);
IEnumerable<System.IO.FileInfo> list2 = dir2.GetFiles("*.*",
System.IO.SearchOption.AllDirectories);

//A custom file comparer defined below


FileCompare myFileCompare = new FileCompare();

// This query determines whether the two folders contain


// identical file lists, based on the custom file comparer
// that is defined in the FileCompare class.
// The query executes immediately because it returns a bool.
bool areIdentical = list1.SequenceEqual(list2, myFileCompare);
bool areIdentical = list1.SequenceEqual(list2, myFileCompare);

if (areIdentical == true)
{
Console.WriteLine("the two folders are the same");
}
else
{
Console.WriteLine("The two folders are not the same");
}

// Find the common files. It produces a sequence and doesn't


// execute until the foreach statement.
var queryCommonFiles = list1.Intersect(list2, myFileCompare);

if (queryCommonFiles.Any())
{
Console.WriteLine("The following files are in both folders:");
foreach (var v in queryCommonFiles)
{
Console.WriteLine(v.FullName); //shows which items end up in result list
}
}
else
{
Console.WriteLine("There are no common files in the two folders.");
}

// Find the set difference between the two folders.


// For this example we only check one way.
var queryList1Only = (from file in list1
select file).Except(list2, myFileCompare);

Console.WriteLine("The following files are in list1 but not list2:");


foreach (var v in queryList1Only)
{
Console.WriteLine(v.FullName);
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

// This implementation defines a very simple comparison


// between two FileInfo objects. It only compares the name
// of the files being compared and their length in bytes.
class FileCompare : System.Collections.Generic.IEqualityComparer<System.IO.FileInfo>
{
public FileCompare() { }

public bool Equals(System.IO.FileInfo f1, System.IO.FileInfo f2)


{
return (f1.Name == f2.Name &&
f1.Length == f2.Length);
}

// Return a hash that reflects the comparison criteria. According to the


// rules for IEqualityComparer<T>, if Equals is true, then the hash codes must
// also be equal. Because equality as defined here is a simple value equality, not
// reference identity, it is possible that two or more objects will produce the same
// hash code.
public int GetHashCode(System.IO.FileInfo fi)
{
string s = $"{fi.Name}{fi.Length}";
return s.GetHashCode();
}
}
}
}

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ to Objects (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Abfragen der größten Datei oder
der größten Dateien in einer Verzeichnisstruktur
(LINQ) (C#)
04.11.2021 • 3 minutes to read

Dieses Beispiel zeigt fünf Abfragen mit Bezug auf die Dateigröße in Bytes:
So rufen Sie die Größe in Bytes der größten Datei ab
So rufen Sie die Größe in Bytes der kleinsten Datei ab
So rufen Sie das FileInfo-Objekt der größten oder kleinsten Datei aus einem oder mehreren Ordnern
unter einem bestimmten Stammordner ab
So rufen Sie eine Sequenz ab, wie z.B. die 10 größten Dateien
So ordnen Sie Dateien in Gruppen auf Grundlage ihrer Dateigröße in Bytes und ignorieren dabei die
Dateien, die kleiner als eine angegebene Größe sind

Beispiel
Das folgende Beispiel enthält fünf separate Abfragen, die zeigen, wie Sie Dateien je nach ihrer Dateigröße in
Bytes abfragen und gruppieren. Sie können diese Beispiele ganz einfach verändern, um die Abfrage auf eine
andere Eigenschaft des FileInfo-Objekts anzuwenden.

class QueryBySize
{
static void Main(string[] args)
{
QueryFilesBySize();
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}

private static void QueryFilesBySize()


{
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\";

// Take a snapshot of the file system.


System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(startFolder);

// This method assumes that the application has discovery permissions


// for all folders under the specified path.
IEnumerable<System.IO.FileInfo> fileList = dir.GetFiles("*.*",
System.IO.SearchOption.AllDirectories);

//Return the size of the largest file


long maxSize =
(from file in fileList
let len = GetFileLength(file)
select len)
.Max();

Console.WriteLine("The length of the largest file under {0} is {1}",


startFolder, maxSize);

// Return the FileInfo object for the largest file


// by sorting and selecting from beginning of list
System.IO.FileInfo longestFile =
(from file in fileList
let len = GetFileLength(file)
where len > 0
orderby len descending
select file)
.First();

Console.WriteLine("The largest file under {0} is {1} with a length of {2} bytes",
startFolder, longestFile.FullName, longestFile.Length);

//Return the FileInfo of the smallest file


System.IO.FileInfo smallestFile =
(from file in fileList
let len = GetFileLength(file)
where len > 0
orderby len ascending
select file).First();

Console.WriteLine("The smallest file under {0} is {1} with a length of {2} bytes",
startFolder, smallestFile.FullName, smallestFile.Length);

//Return the FileInfos for the 10 largest files


// queryTenLargest is an IEnumerable<System.IO.FileInfo>
var queryTenLargest =
(from file in fileList
let len = GetFileLength(file)
orderby len descending
select file).Take(10);

Console.WriteLine("The 10 largest files under {0} are:", startFolder);

foreach (var v in queryTenLargest)


{
Console.WriteLine("{0}: {1} bytes", v.FullName, v.Length);
}

// Group the files according to their size, leaving out


// files that are less than 200000 bytes.
var querySizeGroups =
from file in fileList
let len = GetFileLength(file)
where len > 0
group file by (len / 100000) into fileGroup
where fileGroup.Key >= 2
orderby fileGroup.Key descending
select fileGroup;

foreach (var filegroup in querySizeGroups)


{
Console.WriteLine(filegroup.Key.ToString() + "00000");
foreach (var item in filegroup)
{
Console.WriteLine("\t{0}: {1}", item.Name, item.Length);
}
}
}

// This method is used to swallow the possible exception


// that can be raised when accessing the FileInfo.Length property.
// In this particular case, it is safe to swallow the exception.
static long GetFileLength(System.IO.FileInfo fi)
{
long retval;
try
{
retval = fi.Length;
}
}
catch (System.IO.FileNotFoundException)
{
// If a file is no longer present,
// just add zero bytes to the total.
retval = 0;
}
return retval;
}

Um mindestens ein abgeschlossenes FileInfo-Objekt zurückzugeben, muss die Abfrage jedes in der Datenquelle
untersuchen und die Objekte dann nach Wert ihrer Length-Eigenschaft sortieren. Anschließend kann das
einzelne Objekt oder die Sequenz mit den größten Längen zurückgegeben werden. Verwenden Sie First, um das
erste Element in einer Liste zurückzugeben. Verwenden Sie Take, um die ersten n Elemente zurückzugeben.
Geben Sie eine absteigende Sortierreihenfolge an, um die kleinsten Elemente zu Beginn der Liste anzuzeigen.
Die Abfrage ruft eine separate Methode zum Abrufen der Dateigröße in Bytes auf, um die mögliche Ausnahme
zu verwenden, die in dem Fall ausgelöst wird, wenn eine Datei auf einem anderen Thread im Zeitraum, in dem
das FileInfo-Objekt im Aufruf an GetFiles erstellt wurde, gelöscht wurde. Obwohl das FileInfo-Objekt bereits
erstellt wurde, kann die Ausnahme auftreten, weil ein FileInfo-Objekt versucht, seine Length-Eigenschaft mithilfe
der aktuellsten Größe in Bytes beim ersten Zugriff auf die Eigenschaft zu aktualisieren. Indem dieser Vorgang in
einen Try-Catch-Block außerhalb der Abfrage erfolgt, folgt der Code der Regel zum Vermeiden von Vorgängen
in Abfragen, die Nebeneffekte verursachen können. Im Allgemeinen ist beim Abfangen von Ausnahmen große
Sorgfalt geboten, damit einen Anwendung nicht in einem unbekannten Zustand gelassen wird.

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ to Objects (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Abfragen von Dateiduplikaten in
einer Verzeichnisstruktur (LINQ) (C#)
04.11.2021 • 3 minutes to read

Manchmal können sich Dateien mit demselben Namen in mehr als einem Ordner befinden. Beispielsweise
enthalten mehrere Ordner unter dem Installationsordner von Visual Studio eine Datei „readme.htm“. Dieses
Beispiel zeigt, wie solche mehrfach vorkommenden Dateinamen in einem angegebenen Stammordner abgefragt
werden können. Das zweite Beispiel zeigt, wie Dateien abgefragt werden können, deren Größe und LastWrite-
Zeiten ebenfalls übereinstimmen.

Beispiel
class QueryDuplicateFileNames
{
static void Main(string[] args)
{
// Uncomment QueryDuplicates2 to run that query.
QueryDuplicates();
// QueryDuplicates2();

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}

static void QueryDuplicates()


{
// Change the root drive or folder if necessary
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\";

// Take a snapshot of the file system.


System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(startFolder);

// This method assumes that the application has discovery permissions


// for all folders under the specified path.
IEnumerable<System.IO.FileInfo> fileList = dir.GetFiles("*.*",
System.IO.SearchOption.AllDirectories);

// used in WriteLine to keep the lines shorter


int charsToSkip = startFolder.Length;

// var can be used for convenience with groups.


var queryDupNames =
from file in fileList
group file.FullName.Substring(charsToSkip) by file.Name into fileGroup
where fileGroup.Count() > 1
select fileGroup;

// Pass the query to a method that will


// output one page at a time.
PageOutput<string, string>(queryDupNames);
}

// A Group key that can be passed to a separate method.


// Override Equals and GetHashCode to define equality for the key.
// Override ToString to provide a friendly name for Key.ToString()
class PortableKey
{
public string Name { get; set; }
public string Name { get; set; }
public DateTime LastWriteTime { get; set; }
public long Length { get; set; }

public override bool Equals(object obj)


{
PortableKey other = (PortableKey)obj;
return other.LastWriteTime == this.LastWriteTime &&
other.Length == this.Length &&
other.Name == this.Name;
}

public override int GetHashCode()


{
string str = $"{this.LastWriteTime}{this.Length}{this.Name}";
return str.GetHashCode();
}
public override string ToString()
{
return $"{this.Name} {this.Length} {this.LastWriteTime}";
}
}
static void QueryDuplicates2()
{
// Change the root drive or folder if necessary.
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\Common7";

// Make the lines shorter for the console display


int charsToSkip = startFolder.Length;

// Take a snapshot of the file system.


System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(startFolder);
IEnumerable<System.IO.FileInfo> fileList = dir.GetFiles("*.*",
System.IO.SearchOption.AllDirectories);

// Note the use of a compound key. Files that match


// all three properties belong to the same group.
// A named type is used to enable the query to be
// passed to another method. Anonymous types can also be used
// for composite keys but cannot be passed across method boundaries
//
var queryDupFiles =
from file in fileList
group file.FullName.Substring(charsToSkip) by
new PortableKey { Name = file.Name, LastWriteTime = file.LastWriteTime, Length = file.Length
} into fileGroup
where fileGroup.Count() > 1
select fileGroup;

var list = queryDupFiles.ToList();

int i = queryDupFiles.Count();

PageOutput<PortableKey, string>(queryDupFiles);
}

// A generic method to page the output of the QueryDuplications methods


// Here the type of the group must be specified explicitly. "var" cannot
// be used in method signatures. This method does not display more than one
// group per page.
private static void PageOutput<K, V>(IEnumerable<System.Linq.IGrouping<K, V>> groupByExtList)
{
// Flag to break out of paging loop.
bool goAgain = true;

// "3" = 1 line for extension + 1 for "Press any key" + 1 for input cursor.
int numLines = Console.WindowHeight - 3;

// Iterate through the outer collection of groups.


foreach (var filegroup in groupByExtList)
foreach (var filegroup in groupByExtList)
{
// Start a new extension at the top of a page.
int currentLine = 0;

// Output only as many lines of the current group as will fit in the window.
do
{
Console.Clear();
Console.WriteLine("Filename = {0}", filegroup.Key.ToString() == String.Empty ? "[none]" :
filegroup.Key.ToString());

// Get 'numLines' number of items starting at number 'currentLine'.


var resultPage = filegroup.Skip(currentLine).Take(numLines);

//Execute the resultPage query


foreach (var fileName in resultPage)
{
Console.WriteLine("\t{0}", fileName);
}

// Increment the line counter.


currentLine += numLines;

// Give the user a chance to escape.


Console.WriteLine("Press any key to continue or the 'End' key to break...");
ConsoleKey key = Console.ReadKey().Key;
if (key == ConsoleKey.End)
{
goAgain = false;
break;
}
} while (currentLine < filegroup.Count());

if (goAgain == false)
break;
}
}
}

Die erste Abfrage verwendet einen einfachen Schlüssel, um eine Übereinstimmung zu ermitteln. Somit werden
Dateien gefunden, die den gleichen Namen haben, deren Inhalt aber unterschiedlich sein kann. Die zweite
Abfrage verwendet einen zusammengesetzten Schlüssel für den Abgleich von drei Eigenschaften des FileInfo-
Objekts. Diese Abfrage hat eine größere Wahrscheinlichkeit, Dateien zu finden, die denselben Namen und
ähnliche oder identische Inhalte aufweisen.

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ to Objects (C#)
LINQ und Dateiverzeichnisse (C#)
Vorgehensweise: Abfragen des Inhalts von
Textdateien in einem Ordner (LINQ) (C#)
04.11.2021 • 2 minutes to read

Dieses Beispiel zeigt, wie Sie die Abfrage über alle Dateien in einer angegebenen Verzeichnisstruktur
durchführen, jede Datei öffnen, und ihre Inhalte überprüfen. Diese Technik kann zum Erstellen von Indizes oder
umgekehrten Indizes des Inhalts einer Verzeichnisstruktur verwendet werden. In diesem Beispiel wird eine
einfache Zeichenfolgensuche ausgeführt. Komplexere Formen des Mustervergleichs können jedoch mit einem
regulären Ausdruck ausgeführt werden. Weitere Informationen finden Sie unter Vorgehensweise: Verbinden von
LINQ-Abfragen mit regulären Ausdrücken (C#).

Beispiel
class QueryContents
{
public static void Main()
{
// Modify this path as necessary.
string startFolder = @"c:\program files\Microsoft Visual Studio 9.0\";

// Take a snapshot of the file system.


System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(startFolder);

// This method assumes that the application has discovery permissions


// for all folders under the specified path.
IEnumerable<System.IO.FileInfo> fileList = dir.GetFiles("*.*",
System.IO.SearchOption.AllDirectories);

string searchTerm = @"Visual Studio";

// Search the contents of each file.


// A regular expression created with the RegEx class
// could be used instead of the Contains method.
// queryMatchingFiles is an IEnumerable<string>.
var queryMatchingFiles =
from file in fileList
where file.Extension == ".htm"
let fileText = GetFileText(file.FullName)
where fileText.Contains(searchTerm)
select file.FullName;

// Execute the query.


Console.WriteLine("The term \"{0}\" was found in:", searchTerm);
foreach (string filename in queryMatchingFiles)
{
Console.WriteLine(filename);
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit");
Console.ReadKey();
}

// Read the contents of the file.


static string GetFileText(string name)
{
string fileContents = String.Empty;

// If the file has been deleted since we took


// the snapshot, ignore it and return the empty string.
if (System.IO.File.Exists(name))
{
fileContents = System.IO.File.ReadAllText(name);
}
return fileContents;
}
}

Kompilieren des Codes


Erstellen Sie ein C#-Konsolenanwendungsprojekt mit using -Anweisungen für die Namespaces „System.Linq“
und „System.IO“.

Siehe auch
LINQ und Dateiverzeichnisse (C#)
LINQ to Objects (C#)
Vorgehensweise: Abfragen von ArrayList mit LINQ
(C#)
04.11.2021 • 2 minutes to read

Bei Verwendung von LINQ zum Abfragen nicht generischer IEnumerable-Auflistungen wie z.B. ArrayList müssen
Sie den Typ der Bereichsvariablen entsprechend dem spezifischen Typ der Objekte in der Auflistung explizit
deklarieren. Wenn Sie zum Beispiel eine ArrayList mit Student -Objekten haben, sollte die from-Klausel wie folgt
aussehen:

var query = from Student s in arrList


//...

Indem Sie den Typ der Bereichsvariablen angeben, wandeln Sie jedes Element in der ArrayList in ein Student
um.
Die Verwendung einer explizit typisierten Bereichsvariablen in einem Abfrageausdruck entspricht dem Aufrufen
der Cast-Methode. Cast löst eine Ausnahme aus, wenn bei der Umwandlung ein Fehler auftritt. Cast und OfType
sind zwei Standardabfrageoperator-Methoden, die mit nicht generischen IEnumerable-Typen arbeiten. Weitere
Informationen finden Sie unter Typbeziehungen in LINQ-Abfragevorgängen.

Beispiel
Im folgenden Beispiel wird eine einfache Abfrage von ArrayList veranschaulicht. Beachten Sie, dass in diesem
Beispiel Objektinitialisierer verwendet werden, wenn der Code die Add-Methode aufruft, aber dies ist keine
Voraussetzung.
using System;
using System.Collections;
using System.Linq;

namespace NonGenericLINQ
{
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int[] Scores { get; set; }
}

class Program
{
static void Main(string[] args)
{
ArrayList arrList = new ArrayList();
arrList.Add(
new Student
{
FirstName = "Svetlana", LastName = "Omelchenko", Scores = new int[] { 98, 92, 81, 60
}
});
arrList.Add(
new Student
{
FirstName = "Claire", LastName = "O’Donnell", Scores = new int[] { 75, 84, 91, 39 }
});
arrList.Add(
new Student
{
FirstName = "Sven", LastName = "Mortensen", Scores = new int[] { 88, 94, 65, 91 }
});
arrList.Add(
new Student
{
FirstName = "Cesar", LastName = "Garcia", Scores = new int[] { 97, 89, 85, 82 }
});

var query = from Student student in arrList


where student.Scores[0] > 95
select student;

foreach (Student s in query)


Console.WriteLine(s.LastName + ": " + s.Scores[0]);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
}
/* Output:
Omelchenko: 98
Garcia: 97
*/

Siehe auch
LINQ to Objects (C#)
Vorgehensweise: Hinzufügen von
benutzerdefinierten Methoden zu LINQ-Abfragen
(C#)
04.11.2021 • 5 minutes to read

Sie erweitern die Methoden, die Sie für LINQ-Abfragen durch Hinzufügen von Erweiterungsmethoden zur
IEnumerable<T>-Schnittstelle verwenden. Zusätzlich zu den durchschnittlichen oder maximalen
Standardvorgängen erstellen Sie eine benutzerdefinierte Aggregatmethode, um einen einzelnen Wert aus einer
Sequenz von Werten zu berechnen. Sie erstellen auch eine Methode, die als benutzerdefinierter Filter oder
spezifische Datentransformation für eine Sequenz von Werten agiert und eine neue Sequenz zurückgibt.
Beispiele für solche Methoden sind Distinct, Skip und Reverse.
Beim Erweitern der IEnumerable<T>-Schnittstelle können Sie die benutzerdefinierten Methoden auf jede
aufzählbare Auflistung anwenden. Weitere Informationen finden Sie unter Erweiterungsmethoden.

Hinzufügen einer Aggregatmethode


Eine aggregierte Methode berechnet einen einzelnen Wert aus einer Gruppe von Werten. LINQ stellt mehrere
Aggregatmethoden bereit, einschließlich Average, Min und Max. Sie können Ihre eigene Aggregatmethode
erstellen, indem Sie der IEnumerable<T>-Schnittstelle eine Erweiterungsmethode hinzufügen.
Im folgenden Codebeispiel wird veranschaulicht, wie eine Erweiterungsmethode namens Median erstellt wird,
um einen Median für eine Zahlensequenz des Typs double zu berechnen.

public static class EnumerableExtension


{
public static double Median(this IEnumerable<double>? source)
{
if (source is null || !source.Any())
{
throw new InvalidOperationException("Cannot compute median for a null or empty set.");
}

var sortedList =
source.OrderBy(number => number).ToList();

int itemIndex = sortedList.Count / 2;

if (sortedList.Count % 2 == 0)
{
// Even number of items.
return (sortedList[itemIndex] + sortedList[itemIndex - 1]) / 2;
}
else
{
// Odd number of items.
return sortedList[itemIndex];
}
}
}

Sie können diese Erweiterungsmethode für jede aufzählbare Auflistung genau so aufrufen, wie Sie andere
Aggregatmethoden aus der IEnumerable<T>-Schnittstelle aufrufen.
Im folgenden Codebeispiel wird die Verwendung der Median -Methode für ein Array des Typs double
veranschaulicht.

double[] numbers = { 1.9, 2, 8, 4, 5.7, 6, 7.2, 0 };


var query = numbers.Median();

Console.WriteLine($"double: Median = {query}");


// This code produces the following output:
// double: Median = 4.85

Überladen einer Aggregatmethode zum Akzeptieren verschiedener Typen


Sie können die Aggregatmethode überladen, sodass diese Sequenzen verschiedener Typen akzeptiert. Die
Standardmethode ist die Erstellung einer Überladung für jeden Typ. Ein anderer Ansatz ist das Erstellen einer
Überladung, die einen generischen Typ annimmt und diesen mit einem Delegaten in einen bestimmten Typ
konvertiert. Sie können auch beide Methoden kombinieren.
Erstellen einer Überladung für jeden Typ
Sie können eine bestimmte Überladung für jeden Typ erstellen, den Sie unterstützen möchten. Im folgenden
Codebeispiel wird eine Überladung der Median -Methode für den int -Typ veranschaulicht.

// int overload
public static double Median(this IEnumerable<int> source) =>
(from number in source select (double)number).Median();

Sie können nun die Median -Überladungen für die integer - und double -Typen aufrufen, so wie im folgenden
Code gezeigt:

double[] numbers1 = { 1.9, 2, 8, 4, 5.7, 6, 7.2, 0 };


var query1 = numbers1.Median();

Console.WriteLine($"double: Median = {query1}");

int[] numbers2 = { 1, 2, 3, 4, 5 };
var query2 = numbers2.Median();

Console.WriteLine($"int: Median = {query2}");


// This code produces the following output:
// double: Median = 4.85
// int: Median = 3

Erstellen einer generischen Überladung


Sie können auch eine Überladung erstellen, die eine Sequenz generischer Objekte akzeptiert. Diese Überladung
nimmt einen Delegaten als Parameter und verwendet ihn, um eine Sequenz von Objekten eines generischen
Typs in einen bestimmten Typ zu konvertieren.
Der folgende Code zeigt eine Überladung der Median -Methode, die den Func<T,TResult>-Delegaten als
Parameter akzeptiert. Dieser Delegat übernimmt ein Objekt des generischen Typs „T“ und gibt ein Objekt vom
Typ double zurück.

// generic overload
public static double Median<T>(
this IEnumerable<T> numbers, Func<T, double> selector) =>
(from num in numbers select selector(num)).Median();

Sie können nun die Median -Methode für eine Sequenz von Objekten beliebigen Typs aufrufen. Wenn der Typ
nicht über eine eigene Methodenüberladung verfügt, müssen Sie einen Delegatenparameter übergeben. In C#
können Sie zu diesem Zweck einen Lambdaausdruck verwenden. Wenn Sie die Aggregate - oder Group By -
Klausel anstatt des Methodenaufrufs verwenden, können Sie auch einen beliebigen Wert oder Ausdruck
übergeben, der im Bereich dieser Klausel vorhanden ist. Diese Methode ist nur in Visual Basic verfügbar.
Der folgende Beispielcode veranschaulicht, wie eine Median -Methode für ein Array aus ganzen Zahlen und ein
Array aus Zeichenfolgen aufgerufen wird. Für Zeichenfolgen wird der Median für die Längen der Zeichenfolgen
im Array berechnet. Das Beispiel zeigt, wie der Delegatparameter Median an die Func<T,TResult>-Methode für
jeden Fall übergeben wird.

int[] numbers3 = { 1, 2, 3, 4, 5 };

/*
You can use the num => num lambda expression as a parameter for the Median method
so that the compiler will implicitly convert its value to double.
If there is no implicit conversion, the compiler will display an error message.
*/
var query3 = numbers3.Median(num => num);

Console.WriteLine($"int: Median = {query3}");

string[] numbers4 = { "one", "two", "three", "four", "five" };

// With the generic overload, you can also use numeric properties of objects.
var query4 = numbers4.Median(str => str.Length);

Console.WriteLine($"string: Median = {query4}");


// This code produces the following output:
// int: Median = 3
// string: Median = 4

Hinzufügen einer Methode, die eine Sequenz zurückgibt


Sie können die Schnittstelle IEnumerable<T> mit einer benutzerdefinierten Abfragemethode erweitern, die eine
Sequenz von Werten zurückgibt. In diesem Fall muss die Methode eine Auflistung des Typs IEnumerable<T>
zurückgeben. Solche Methoden können verwendet werden, um Filter oder Datentransformationen auf eine
Sequenz von Werten anzuwenden.
Das folgende Beispiel zeigt, wie eine Erweiterungsmethode mit dem Namen AlternateElements erstellt wird, die
jedes andere Element in einer Auflistung zurückgibt, beginnend mit dem ersten Element.

// Extension method for the IEnumerable<T> interface.


// The method returns every other element of a sequence.
public static IEnumerable<T> AlternateElements<T>(this IEnumerable<T> source)
{
int index = 0;
foreach (T element in source)
{
if (index % 2 == 0)
{
yield return element;
}

index++;
}
}

Sie können diese Erweiterungsmethode für jede aufzählbare Auflistung genau so aufrufen, wie Sie andere
Methoden aus der Schnittstelle IEnumerable<T> aufrufen, so wie im folgenden Code dargestellt:
string[] strings = { "a", "b", "c", "d", "e" };

var query5 = strings.AlternateElements();

foreach (var element in query5)


{
Console.WriteLine(element);
}
// This code produces the following output:
// a
// c
// e

Siehe auch
IEnumerable<T>
Erweiterungsmethoden
LINQ to ADO.NET (Portalseite)
04.11.2021 • 2 minutes to read

Mit LINQ to ADO.NET können Sie in ADO.NET mithilfe des LINQ-Programmiermodells (Language Integrated
Query) jedes aufzählbare Objekt abfragen.

NOTE
Die LINQ to ADO.NET-Dokumentation befindet sich im Abschnitt „ADO.NET“ des .NET Framework SDK: LINQ und
ADO.NET.

Es gibt drei separate ADO.NET LINQ-Technologien (Language Integrated Query): LINQ to DataSet, LINQ to SQL
und LINQ to Entities. LINQ to DataSet ermöglicht umfangreichere, optimierte Abfragen von DataSet. LINQ to
SQL ermöglicht es Ihnen, SQL Server-Datenbankschemas direkt abzufragen, und mit LINQ to Entities können
Sie ein Entity Data Model abfragen.

LINQ to DataSet
DataSet ist eine der am häufigsten verwendeten Komponenten in ADO.NET und ein Schlüsselelement des
getrennten Programmiermodells, auf dem ADO.NET aufgebaut ist. Trotz seiner Bedeutung sind die
Abfragefunktionen des DataSet begrenzt.
Mit LINQ to DataSet können Sie umfangreichere Abfragefunktionen in DataSet integrieren, indem Sie die
gleiche Abfragefunktionalität verwenden, die für viele andere Datenquellen verfügbar ist.
Weitere Informationen finden Sie unter LINQ to DataSet.

LINQ to SQL
LINQ to SQL stellt eine Laufzeitinfrastruktur zum Verwalten relationaler Daten als Objekte bereit. In LINQ to SQL
wird das Datenmodell einer relationalen Datenbank einem Objektmodell zugeordnet, das in der
Programmiersprache des Entwicklers ausgedrückt ist. Wenn Sie die Anwendung ausführen, übersetzt LINQ to
SQL die sprachintegrierten Abfragen im Objektmodell in SQL und sendet sie zur Ausführung an die Datenbank.
Wenn die Datenbank die Ergebnisse zurückgibt, übersetzt LINQ to SQL die Ergebnisse zurück in Objekte, die Sie
bearbeiten können.
LINQ to SQL bietet Unterstützung für gespeicherte Prozeduren und benutzerdefinierte Funktionen in der
Datenbank sowie für die Vererbung im Objektmodell.
Weitere Informationen finden Sie unter LINQ to SQL.

LINQ to Entities
Durch das Entity Data Model werden relationale Daten als Objekte in der .NET-Umgebung verfügbar gemacht.
Dadurch wird die Objektebene zu einem optimalen Ziel für die LINQ-Unterstützung, die es Entwicklern
ermöglicht, Abfragen an die Datenbank in der Sprache der Geschäftlogik zu formulieren. Dies wird auch als
LINQ to Entities bezeichnet. Weitere Informationen Sie unter LINQ to Entities.

Siehe auch
LINQ und ADO.NET
Language Integrated Query (LINQ) (C#)
Aktivieren einer Datenquelle für LINQ-Abfragen
04.11.2021 • 2 minutes to read

Es gibt verschiedene Möglichkeiten, LINQ zu erweitern, um die Abfrage einer beliebigen Datenquelle im LINQ-
Muster zu ermöglichen. Bei der Datenquelle kann es sich u. a. um eine Datenstruktur, einen Webdienst, ein
Dateisystem oder eine Datenbank handeln. Das LINQ-Muster erleichtert Clients das Ausführen von Abfragen für
eine Datenquelle mit aktivierter LINQ-Abfrage, da die Syntax und das Muster der Abfrage unverändert bleiben.
LINQ kann u.a. auf folgende Weisen für die Verwendung dieser Datenquellen erweitert werden:
Implementieren der IEnumerable<T>-Schnittstelle in einem Typ, um LINQ to Objects-Abfragen für diesen
Typ zu aktivieren.
Erstellen von Standardabfrageoperator-Methoden wie Where und Select, die einen Typ erweitern, um
benutzerdefinierte LINQ-Abfragen für diesen Typ zu aktivieren.
Erstellen eines Anbieters für die Datenquelle, die die IQueryable<T>-Schnittstelle implementiert. Ein
Anbieter, der diese Schnittstelle implementiert, empfängt LINQ-Abfragen in Form von
Ausdrucksbaumstrukturen, die er benutzerdefiniert ausführen kann, beispielsweise im Remotemodus.
Erstellen eines Anbieters für die Datenquelle, die eine vorhandene LINQ-Technologie nutzt. Durch einen
solchen Anbieter werden nicht nur Abfragen ermöglicht, sondern zusätzlich Einfüge-, Aktualisierungs-
und Löschvorgänge sowie Zuordnungen für benutzerdefinierte Typen.
In diesem Thema werden diese Möglichkeiten erläutert.

Aktivieren von LINQ-Abfragen für eine Datenquelle


Daten im Arbeitsspeicher
Es gibt zwei Möglichkeiten, LINQ-Abfragen von Daten im Arbeitsspeicher zu aktivieren. Wenn die Daten über
einen Typ verfügen, der IEnumerable<T> implementiert, können Sie sie mithilfe von LINQ to Objects abfragen.
Wenn es nicht sinnvoll ist, die Enumeration des Typs durch Implementierung der IEnumerable<T>-Schnittstelle
zu aktivieren, können Sie LINQ-Standardabfrageoperator-Methoden in diesem Typ definieren oder LINQ-
Standardabfrageoperator-Methoden erstellen, die den Typ erweitern. Benutzerdefinierte Implementierungen der
Standardabfrageoperatoren sollten zur Rückgabe der Ergebnisse eine verzögerte Ausführung verwenden.
Remotedaten
Die beste Möglichkeit zur Aktivierung von LINQ-Abfragen für eine Remotedatenquelle besteht darin, die
IQueryable<T>-Schnittstelle zu implementieren. Dieser Ansatz unterscheidet sich jedoch vom Erweitern eines
Anbieters wie LINQ to SQL für eine Datenquelle.

LINQ-Anbieter "IQueryable"
LINQ-Anbieter, die IQueryable<T> implementieren, können in ihrer Komplexität große Unterschiede aufweisen.
In diesem Abschnitt werden die verschiedenen Komplexitätsstufen erläutert.
Ein weniger komplexer IQueryable -Anbieter erstellt möglicherweise eine Schnittstelle mit einer einzelnen
Methode eines Webdiensts. Dieser Anbietertyp ist sehr spezifisch, da er erwartet, dass in den von ihm
verarbeiteten Abfragen bestimmte Informationen enthalten sind. Er verfügt über ein geschlossenes Typsystem
und macht möglicherweise nur einen einzelnen Ergebnistyp verfügbar. Ein Großteil der Abfrageverarbeitung
erfolgt lokal, beispielsweise unter Verwendung der Enumerable-Implementierungen der
Standardabfrageoperatoren. Ein weniger komplexer Anbieter überprüft u. U. nur einen Ausdruck des
Methodenaufrufs in der Ausdrucksbaumstruktur, die die Abfrage darstellt, und lässt die übrige Abfragelogik von
einer anderen Instanz behandeln.
Ein IQueryable -Anbieter mittlerer Komplexität verwendet möglicherweise eine Datenquelle als Ziel, die über
eine teilweise ausdrucksbasierte Abfragesprache verfügt. Wenn ein Webdienst als Ziel verwendet wird, stellt der
Anbieter vielleicht Verbindungen zu mehr als einer Webdienstmethode her und wählt die aufzurufende
Methode auf der Grundlage der Frage aus, die von der Abfrage gestellt wird. Ein Anbieter mittlerer Komplexität
verfügt über ein umfangreicheres Typsystem als ein einfacher Anbieter, das jedoch weiterhin ein festes
Typsystem darstellt. Der Anbieter kann beispielsweise Typen verfügbar machen, die über 1:n-Beziehungen
verfügen, die wiederum traversiert werden können, ohne eine Zuordnungstechnologie für benutzerdefinierte
Typen bereitzustellen.
Ein komplexer IQueryable -Anbieter, wie z. B. der LINQ to SQL-Anbieter, kann möglicherweise vollständige
LINQ-Abfragen in eine ausdrucksbasierte Abfragesprache wie SQL übersetzen. Ein komplexer Anbieter arbeitet
universeller als ein weniger komplexer Anbieter, da er eine größere Bandbreite von Fragen in der Abfrage
verarbeiten kann. Außerdem verfügt er über ein offenes Typsystem und benötigt daher eine umfassende
Infrastruktur für die Zuordnung benutzerdefinierter Typen. Die Entwicklung eines komplexen Anbieters ist
ziemlich arbeitsaufwändig.

Siehe auch
IQueryable<T>
IEnumerable<T>
Enumerable
Standard Query Operators Overview (C#) (Übersicht über Standardabfrageoperatoren (C#))
LINQ to Objects (C#)
Visual Studio-IDE und Toolunterstützung für LINQ
(C#)
04.11.2021 • 2 minutes to read

Die integrierte Entwicklungsumgebung (Integrated Development Environment, IDE) von Visual Studio bietet die
folgenden Features, die die LINQ-Anwendungsentwicklung unterstützen:

Object Relational Designer


Der objektrelationale Designer ist ein visuelles Designtool, das Sie in LINQ to SQL-Anwendungen zum Erstellen
von Klassen in C# verwenden können, die die relationalen Daten in einer zugrunde liegenden Datenbank
darstellen. Weitere Informationen finden Sie unter LINQ to SQL-Tools in Visual Studio.

SQLMetal-Befehlszeilentool
SQLMetal ist ein Befehlszeilentool, das in Buildprozessen zum Generieren von Klassen aus vorhandenen
Datenbanken verwendet werden kann, die in LINQ to SQL-Anwendungen verwendet werden sollen. Weitere
Informationen finden Sie unter SqlMetal.exe (Tool zur Codegenerierung).

LINQ-fähiger Code-Editor
Der C#-Code-Editor bietet umfassende Unterstützung für LINQ durch IntelliSense- und
Formatierungsfunktionen.

Unterstützung für Visual Studio-Debugger


Der Visual Studio-Debugger unterstützt das Debuggen von Abfrageausdrücken. Weitere Informationen finden
Sie unter Debuggen von LINQ.

Siehe auch
Language Integrated Query (LINQ) (C#)
Reflektion (C#)
04.11.2021 • 2 minutes to read

Reflektion bietet Objekte (des Typs Type), die Assemblys, Module und Typen beschreiben. Mithilfe von Reflektion
können Sie dynamisch eine Instanz eines Typs erzeugen, den Typ an ein vorhandenes Objekt binden und
Typinformationen von vorhandenen Objekten abfragen. Ebenso wird erläutert wie die Methoden vorhandener
Objekte aufgerufen und auf ihre Felder und Eigenschaften zugegriffen werden kann. Wenn Sie Attribute in
Ihrem Code verwenden, können Sie mit Reflektion auf diese zugreifen. Weitere Informationen finden Sie unter
Attribute.
Hier sehen Sie ein einfaches Beispiel für Reflektion mit der Methode GetType(), die von allen Typen der
Basisklasse Object geerbt wird, zum Abrufen von Typinformationen einer Variable:

NOTE
Stellen Sie sicher, dass Sie using System; und using System.Reflection; am Anfang Ihrer .cs-Datei hinzufügen.

// Using GetType to obtain type information:


int i = 42;
Type type = i.GetType();
Console.WriteLine(type);

Die Ausgabe ist: System.Int32 .


Im folgenden Beispiel wird Reflektion verwendet, um den vollständigen Namen der geladenen Assembly
abzurufen.

// Using Reflection to get information of an Assembly:


Assembly info = typeof(int).Assembly;
Console.WriteLine(info);

Die Ausgabe ist: System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e .

NOTE
Die C#-Schlüsselwörter protected und internal haben in Zwischensprache (Intermediate Language, IL) keine
Bedeutung und werden nicht in Reflexions-APIs verwendet. Die entsprechenden Begriffe in IL sind Family und Assembly.
Verwenden Sie zum Identifizieren einer internal -Methode mithilfe von Reflektion die Eigenschaft IsAssembly.
Verwenden Sie IsFamilyOrAssembly zum Identifizieren einer Methode protected internal .

Übersicht über Reflektion


Reflektion ist in folgenden Situationen nützlich:
Wenn Sie in den Metadaten Ihres Programms auf Attribute zugreifen müssen Weitere Informationen finden
Sie unter Abrufen von Informationen aus Attributen.
Zum Untersuchen und Instanziieren von Typen in einer Assembly
Zum Erstellen neuer Typen zur Laufzeit Verwenden Sie Klassen in System.Reflection.Emit.
Zum Ausführen von spätem Binden mit Zugriff auf Methoden der zur Laufzeit erstellten Typen Weitere
Informationen finden Sie im Thema Dynamisches Laden und Verwenden von Typen.

Verwandte Abschnitte
Weitere Informationen finden Sie unter:
Reflexion
Anzeigen von Typinformationen
Reflektion und generische Typen
System.Reflection.Emit
Abrufen von Informationen aus Attributen

Weitere Informationen
C#-Programmierhandbuch
Assemblys in .NET
Serialisierung (C#)
04.11.2021 • 4 minutes to read

Serialisierung ist der Prozess der Konvertierung eines Objekts in einen Bytestream, um das Objekt zu speichern
oder in den Arbeitsspeicher, eine Datenbank oder eine Datei zu übertragen. Hauptzweck ist es, den Zustand
eines Objekts zu speichern, um es bei Bedarf neu erstellen zu können. Der umgekehrte Vorgang wird als
Deserialisierung bezeichnet.

Funktionsweise der Serialisierung


Diese Abbildung zeigt den gesamten Ablauf der Serialisierung:

Das Objekt wird in einem Stream serialisiert, der die Daten enthält. Der Stream kann auch Informationen zum
Objekttyp enthalten, z. B. zur Version, Kultur und zum Assemblynamen. Aus diesem Stream kann das Objekt in
einer Datenbank, einer Datei oder einem Arbeitsspeicher gespeichert werden.
Verwendungszwecke der Serialisierung
Mit der Serialisierung kann der Entwickler den Zustand eines Objekts speichern und das Objekt bei Bedarf neu
erstellen. Die Serialisierung stellt sowohl Objektspeicher als auch Datenaustausch bereit. Mithilfe der
Serialisierung kann ein Entwickler die folgenden Aktionen ausführen:
Senden des Objekts an eine Remoteanwendung mithilfe eines Webdiensts
Übergeben eines Objekts von einer Domäne zu einer anderen
Übergeben eines Objekts über eine Firewall als JSON- oder XML-Zeichenfolge
Verwalten von Sicherheits- und benutzerspezifischen Informationen über Anwendungen

JSON-Serialisierung
Der System.Text.Json-Namespace enthält Klassen für die JSON-Serialisierung und -Deserialisierung (JavaScript
Object Notation). JSON ist ein offener Standard, der häufig zum Freigeben von Daten über das Internet
verwendet wird.
Bei der JSON-Serialisierung werden die öffentlichen Eigenschaften eines Objekts in eine Zeichenfolge, ein
Bytearray oder Stream serialisiert, die der RFC 8259 JSON-Spezifikation entsprechen. Zur Steuerung der Art und
Weise, wie JsonSerializer eine Instanz der Klasse serialisiert oder deserialisiert:
Verwenden Sie ein JsonSerializerOptions-Objekt.
Wenden Sie die Attribute des System.Text.Json.Serialization-Namespaces auf Klassen oder Eigenschaften an.
Implementieren Sie benutzerdefinierte Konverter.

Binär- und XML-Serialisierung


Der System.Runtime.Serialization-Namespace enthält Klassen für die binäre Serialisierung und Deserialisierung
sowie die XML-Serialisierung und -Deserialisierung.
Die binäre Serialisierung verwendet zum Generieren einer kompakten Serialisierung die binäre Codierung für
den Speicher oder für socketbasierte Netzwerkstreams. Bei der binären Serialisierung werden alle Member
einschließlich der schreibgeschützten Member serialisiert, und die Leistung wird verbessert.

WARNING
Die binäre Serialisierung kann gefährlich sein. Weitere Informationen finden Sie im BinaryFormatter-Sicherheitshandbuch.

Bei der XML-Serialisierung werden die öffentlichen Felder und Eigenschaften eines Objekts bzw. die Parameter
und Rückgabewerte von Methoden in einen XML-Stream serialisiert, der einem bestimmtem XSD-Dokument
(XML Schema Definition) entspricht. Die XML-Serialisierung führt zu stark typisierten Klassen mit öffentlichen
Eigenschaften und Feldern, die in XML konvertiert werden. System.Xml.Serialization enthält Klassen zum
Serialisieren und Deserialisieren in XML. Sie wenden Attribute auf Klassen und Klassenmember an, um die Art
der Serialisierung oder Deserialisierung einer Instanz der Klasse durch XmlSerializer zu steuern.
Aktivieren der Serialisierbarkeit eines Objekts
Für die Binär- oder XML-Serialisierung benötigen Sie:
das zu serialisierende Objekt
einen Stream mit dem serialisierten Objekt
eine System.Runtime.Serialization.Formatter-Instanz
Wenden Sie das Attribut SerializableAttribute auf einen Typ an, um anzugeben, dass Instanzen des Typs
serialisiert werden können. Eine Ausnahme wird ausgelöst, wenn Sie versuchen zu serialisieren, aber der Typ
nicht über das Attribut SerializableAttribute verfügt.
Wenden Sie das NonSerializedAttribute-Attribut an, um zu verhindern, dass ein Feld serialisiert wird. Wenn ein
Feld eines serialisierbaren Typs einen Zeiger, ein Handle oder eine andere Datenstruktur enthält, der/die für eine
bestimmte Umgebung spezifisch ist, und das Feld in keiner anderen Umgebung sinnvoll wiederhergestellt
werden kann, sollten Sie den Typ als nicht serialisierbar markieren.
Wenn eine serialisierte Klasse Verweise auf Objekte anderer Klassen enthält, die mit SerializableAttribute
markiert sind, werden diese Objekte ebenfalls serialisiert.
Einfache und benutzerdefinierte Serialisierung
Die Binär- und XML-Serialisierung kann auf zwei Arten ausgeführt werden: einfach und benutzerdefiniert.
Die einfache Serialisierung verwendet .NET, um ein Objekt automatisch zu serialisieren. Die einzige
Voraussetzung ist, dass die Klasse über das SerializableAttribute-Attribut verfügt. Das Attribut
NonSerializedAttribute kann verwendet werden, um die Serialisierung bestimmter Felder zu verhindern.
Wenn Sie einfache Serialisierung verwenden, kann die Versionsverwaltung von Objekten Probleme bereiten. Sie
sollten benutzerdefinierte Serialisierung verwenden, wenn die Versionsverwaltung wichtig ist. Die einfache
Serialisierung ist die einfachste Möglichkeit zur Serialisierung, bietet allerdings nicht viel
Steuerungsmöglichkeiten für den Prozess.
Bei der benutzerdefinierten Serialisierung können Sie genau angeben, welche Objekte serialisiert werden und
wie die Serialisierung erfolgt. Die Klasse muss als SerializableAttribute markiert und in die ISerializable-
Schnittstelle implementiert sein. Wenn das Objekt auch benutzerdefiniert deserialisiert werden soll, verwenden
Sie einen benutzerdefinierten Konstruktor.

Designerserialisierung
Die Designerserialisierung ist eine besondere Form der Serialisierung, die auch die Art der Objektpersistenz
einbezieht, die mit Entwicklungstools verknüpft ist. Bei der Designerserialisierung handelt es sich um den
Prozess der Konvertierung eines Objektdiagramms in eine Quelldatei, die später zum Wiederherstellen des
Objektdiagramms verwendet werden kann. Eine Quelldatei kann Code-, Markup- oder sogar SQL-
Tabelleninformationen enthalten.

Verwandte Themen und Beispiele


Die System.Text.Json-Übersicht verdeutlicht, wie Sie die System.Text.Json -Bibliothek erhalten.
Serialisieren und Deserialisieren von JSON-Daten in .NET: Veranschaulicht, wie Objektdaten in und aus JSON
mithilfe der JsonSerializer-Klasse gelesen und geschrieben werden.
Exemplarische Vorgehensweise: Beibehalten eines Objekts in Visual Studio
Veranschaulicht, wie die Serialisierung verwendet werden kann, um die Daten eines Objekts zwischen Instanzen
beizubehalten. Dadurch können Sie Werte speichern und abrufen, wenn das Objekt das nächste Mal instanziiert
wird.
Lesen von Objektdaten aus einer XML-Datei (C#)
Zeigt, wie Objektdaten gelesen werden, die zuvor mithilfe der XmlSerializer-Klasse in eine XML-Datei
geschrieben wurden.
Schreiben von Objektdaten in eine XML-Datei (C#)
Zeigt, wie ein Objekt aus einer Klasse mithilfe der XmlSerializer-Klasse in eine XML-Datei geschrieben wird.
Schreiben von Objektdaten in eine XML-Datei (C#)
04.11.2021 • 2 minutes to read

Dieses Beispiel verwendet die XmlSerializer-Klasse, um das Objekt aus einer Klasse in eine XML-Datei zu
schreiben.

Beispiel
public class XMLWrite
{

static void Main(string[] args)


{
WriteXML();
}

public class Book


{
public String title;
}

public static void WriteXML()


{
Book overview = new Book();
overview.title = "Serialization Overview";
System.Xml.Serialization.XmlSerializer writer =
new System.Xml.Serialization.XmlSerializer(typeof(Book));

var path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) +


"//SerializationOverview.xml";
System.IO.FileStream file = System.IO.File.Create(path);

writer.Serialize(file, overview);
file.Close();
}
}

Kompilieren des Codes


Die zu serialisierende Klasse muss über einen öffentlichen Konstruktor ohne Parameter verfügen.

Stabile Programmierung
Die folgenden Bedingungen können einen Ausnahmefehler verursachen:
Die zu serialisierende Klasse verfügt nicht über einen öffentlichen, parameterlosen Konstruktor.
Die Datei ist bereits vorhanden und schreibgeschützt (IOException).
Der Pfad ist zu lang (PathTooLongException).
Der Datenträger ist voll (IOException).

.NET-Sicherheit
Mit diesem Beispiel wird eine neue Datei erstellt, wenn diese noch nicht vorhanden ist. Wenn eine Anwendung
eine Datei erstellen muss, benötigt sie eine Create -Berechtigung für den Ordner. Wenn die Datei bereits
vorhanden ist, benötigt die Anwendung lediglich die Berechtigung für den Write -Zugriff, also eine geringere
Berechtigung. Aus Sicherheitsgründen sollte die Datei nach Möglichkeit erst im Verlauf der Bereitstellung erstellt
werden. Außerdem sollte nur die Read -Berechtigung für eine einzelne Datei erteilt werden (anstatt Create -
Berechtigungen für den gesamten Ordner zu gewähren).

Siehe auch
StreamWriter
Lesen von Objektdaten aus einer XML-Datei (C#)
Serialisierung (C#)
Vorgehensweise: Lesen von Objektdaten aus einer
XML-Datei (C#)
04.11.2021 • 2 minutes to read

In diesem Beispiel werden Objektdaten gelesen, die zuvor mithilfe der XmlSerializer-Klasse in eine XML-Datei
geschrieben wurden.

Beispiel
public class Book
{
public String title;
}

public void ReadXML()


{
// First write something so that there is something to read ...
var b = new Book { title = "Serialization Overview" };
var writer = new System.Xml.Serialization.XmlSerializer(typeof(Book));
var wfile = new System.IO.StreamWriter(@"c:\temp\SerializationOverview.xml");
writer.Serialize(wfile, b);
wfile.Close();

// Now we can read the serialized book ...


System.Xml.Serialization.XmlSerializer reader =
new System.Xml.Serialization.XmlSerializer(typeof(Book));
System.IO.StreamReader file = new System.IO.StreamReader(
@"c:\temp\SerializationOverview.xml");
Book overview = (Book)reader.Deserialize(file);
file.Close();

Console.WriteLine(overview.title);

Kompilieren des Codes


Ersetzen Sie den Dateinamen „c:\temp\SerializationOverview.xml“ durch den Namen der Datei, die die
serialisierten Daten enthält. Weitere Informationen zum Serialisieren von Daten finden Sie unter
Vorgehensweise: Schreiben von Objektdaten in eine XML-Datei (C#).
Die Klasse muss über einen öffentlichen Konstruktor ohne Parameter verfügen.
Nur die öffentlichen Eigenschaften und Felder werden deserialisiert.

Stabile Programmierung
Die folgenden Bedingungen können einen Ausnahmefehler verursachen:
Die zu serialisierende Klasse verfügt nicht über einen öffentlichen, parameterlosen Konstruktor.
Die Daten in der Datei stellen keine Daten aus der zu deserialisierenden Klasse dar.
Die Datei ist nicht vorhanden (IOException).
.NET-Sicherheit
Überprüfen Sie immer die Eingaben, und deserialisieren Sie keine Daten aus einer nicht vertrauenswürdigen
Quelle. Das neu erstellte Objekt wird auf einem lokalen Computer mit den Berechtigungen des Codes
ausgeführt, der es deserialisiert hat. Überprüfen Sie alle Eingaben, bevor Sie die Daten in der Anwendung
verwenden.

Siehe auch
StreamWriter
Schreiben von Objektdaten in eine XML-Datei (C#)
Serialisierung (C#)
C#-Programmierhandbuch
Exemplarische Vorgehensweise: Beibehalten eines
Objekts unter Verwendung von C#
04.11.2021 • 5 minutes to read

Sie können die Serialisierung verwenden, um die Daten eines Objekts zwischen Instanzen beizubehalten.
Dadurch können Sie Werte speichern und abrufen, wenn das Objekt das nächste Mal instanziiert wird.
In dieser exemplarischen Vorgehensweise erstellen Sie ein einfaches Loan -Objekt und behalten dessen Daten in
einer Datei bei. Anschließend rufen Sie die Daten aus der Datei ab, wenn Sie das Objekt neu erstellen.

IMPORTANT
Mit diesem Beispiel wird eine neue Datei erstellt, wenn diese noch nicht vorhanden ist. Wenn eine Anwendung eine Datei
erstellen muss, muss Sie über die Create -Berechtigung für den Ordner verfügen. Berechtigungen werden mithilfe von
Zugriffssteuerungslisten festgelegt. Wenn die Datei bereits vorhanden ist, benötigt die Anwendung lediglich die
Berechtigung Write , was einer geringeren Berechtigung entspricht. Aus Sicherheitsgründen sollte die Datei nach
Möglichkeit erst im Verlauf der Bereitstellung erstellt werden. Außerdem sollte die Read -Berechtigung nur für eine
einzelne Datei erteilt werden (anstatt „Create“-Berechtigungen für den gesamten Ordner zu gewähren). Darüber hinaus
ist es sicherer, Daten in Benutzerordner statt in Stammordner oder den Ordner „Programmdateien“ zu schreiben.

IMPORTANT
In diesem Beispiel werden Daten in einer Datei im Binärformat gespeichert. Diese Formate sollten nicht für sensible Daten
wie Kennwörter oder Kreditkarteninformationen verwendet werden.

Voraussetzungen
Installieren Sie zum Erstellen und Ausführen von Builds das .NET Core SDK.
Installieren Sie Ihren bevorzugten Code-Editor, wenn Sie dies nicht bereits erledigt haben.

TIP
Benötigen Sie einen Code-Editor? Testen Sie Visual Studio.

Dieses Beispiel erfordert C# 7.3. Siehe Auswählen der C#-Sprachversion.


Sie können den Beispielcode online im GitHub-Repository für .NET-Beispiele untersuchen.

Erstellen des Loan-Objekts


Der erste Schritt ist das Erstellen einer Loan -Klasse und einer Konsolenanwendung, die die Klasse verwendet:
1. Erstellen Sie eine neue Anwendung. Geben Sie dotnet new console -o serialization ein, um eine neue
Konsolenanwendung in einem Unterverzeichnis mit dem Namen serialization zu erstellen.
2. Öffnen Sie die Anwendung in Ihrem Editor, und fügen Sie eine neue Klasse mit dem Namen Loan.cs hinzu.
3. Fügen Sie der Loan -Klasse den folgenden Code hinzu:
public class Loan : INotifyPropertyChanged
{
public double LoanAmount { get; set; }
public double InterestRatePercent { get; set; }

[field:NonSerialized()]
public DateTime TimeLastLoaded { get; set; }

public int Term { get; set; }

private string customer;


public string Customer
{
get { return customer; }
set
{
customer = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(Customer)));
}
}

[field: NonSerialized()]
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

public Loan(double loanAmount,


double interestRate,
int term,
string customer)
{
this.LoanAmount = loanAmount;
this.InterestRatePercent = interestRate;
this.Term = term;
this.customer = customer;
}
}

Sie müssen ebenfalls eine einfache Anwendung erstellen, die die Loan -Klasse verwendet.

Serialisieren des Loan-Objekts


1. Öffnen Sie Program.cs . Fügen Sie den folgenden Code hinzu:

Loan TestLoan = new Loan(10000.0, 7.5, 36, "Neil Black");

Fügen Sie für das PropertyChanged -Ereignis einen Ereignishandler und einige Zeilen hinzu, um das Loan -Objekt
zu bearbeiten und die Änderungen anzuzeigen. Im folgenden Code können Sie die Änderungen sehen:

TestLoan.PropertyChanged += (_, __) => Console.WriteLine($"New customer value: {TestLoan.Customer}");

TestLoan.Customer = "Henry Clay";


Console.WriteLine(TestLoan.InterestRatePercent);
TestLoan.InterestRatePercent = 7.1;
Console.WriteLine(TestLoan.InterestRatePercent);

Zu diesem Zeitpunkt können Sie den Code ausführen und die aktuelle Ausgabe sehen:
New customer value: Henry Clay
7.5
7.1

Wenn Sie diese Anwendung wiederholt ausführen, werden immer dieselben Werte geschrieben. Jedes Mal,
wenn Sie das Programm ausführen, wird ein neues Loan-Objekt erstellt. In der Praxis ändern sich Zinssätze
regelmäßig, aber nicht unbedingt jedes Mal wenn die Anwendung ausgeführt wird. Mithilfe von
Serialisierungscode speichern Sie die aktuellsten Zinssätze zwischen den einzelnen Anwendungsinstanzen. Im
nächsten Schritt werden Sie durch Hinzufügen von Serialisierung zur Loan-Klasse genau das tun.

Verwenden von Serialisierung zum Beibehalten des Objekts


Sie müssen die Klasse zuerst mit dem Attribut Serializable markieren, um die Werte für die Loan-Klasse
beizubehalten. Fügen Sie den folgenden Code oberhalb der Definition der Loan-Klasse hinzu.

[Serializable()]

Der Compiler wird vom Attribut SerializableAttribute darüber informiert, dass der Inhalt der Klasse in einer
Datei beibehalten werden kann. Da das PropertyChanged -Ereignis keinen Teil des Objektgraphen darstellt, der
gespeichert werden sollte, sollte es nicht serialisiert werden. Bei einer Serialisierung würden alle Objekte
serialisiert werden, die diesem Ereignis zugeordnet sind. Sie können das NonSerializedAttribute-Attribut zu der
Felddeklaration für den Ereignishandler PropertyChanged hinzufügen.

[field: NonSerialized()]
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

Ab C# 7.3 können Sie Attribute an das Unterstützungsfeld einer automatisch implementierten Eigenschaft unter
Verwendung des Zielwerts field anfügen. Über den folgenden Code fügen Sie eine TimeLastLoaded -
Eigenschaft hinzu und markieren diese als „nicht serialisierbar “:

[field:NonSerialized()]
public DateTime TimeLastLoaded { get; set; }

Fügen Sie als nächstes den Serialisierungscode zur LoanApp-Anwendung hinzu. Um die Klasse zu serialisieren
und in eine Datei zu schreiben, verwenden Sie die Namespaces System.IO und
System.Runtime.Serialization.Formatters.Binary. Sie können wie im folgenden Codebeispiel dargestellt Verweise
zu den notwendigen Namespaces hinzufügen, damit Sie die vollqualifizierten Namen nicht eingeben müssen:

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

Fügen Sie als nächstes Code hinzu, um das Objekt aus der Datei zu deserialisieren wenn das Objekt erstellt wird.
Fügen Sie wie im folgenden Codebeispiel dargestellt eine Konstante zur Klasse für den Dateinamen der
serialisierten Daten hinzu:

const string FileName = @"../../../SavedLoan.bin";

Fügen Sie dann im Anschluss an die Zeile, die das TestLoan -Objekt erstellt, den folgenden Code hinzu:
if (File.Exists(FileName))
{
Console.WriteLine("Reading saved file");
Stream openFileStream = File.OpenRead(FileName);
BinaryFormatter deserializer = new BinaryFormatter();
TestLoan = (Loan)deserializer.Deserialize(openFileStream);
TestLoan.TimeLastLoaded = DateTime.Now;
openFileStream.Close();
}

Sie müssen erst sicherstellen, ob die Datei vorhanden ist. Wenn sie vorhanden ist, erstellen Sie eine Stream-
Klasse zum Lesen der Binärdatei und eine BinaryFormatter-Klasse zum Übersetzen der Datei. Sie müssen
ebenfalls vom Streamtyp in den Loan-Objekttyp konvertieren.
Als nächstes müssen Sie Code hinzufügen, um die Klasse in eine Datei zu serialisieren. Fügen Sie dem
vorhandenen Code den folgenden Code in der Main -Methode hinzu:

Stream SaveFileStream = File.Create(FileName);


BinaryFormatter serializer = new BinaryFormatter();
serializer.Serialize(SaveFileStream, TestLoan);
SaveFileStream.Close();

Nun können Sie die Anwendung erneut erstellen und ausführen. Beachten Sie, dass die Zinssätze bei der ersten
Ausführung bei 7,5 beginnen und dann in 7,1 umgewandelt werden. Schließen Sie die Anwendung, und führen
Sie sie dann erneut aus. Die Anwendung druckt dann die Meldung aus, die sie aus der gespeicherten Datei
gelesen hat. Der Zinssatz liegt dann sogar vor dem Code, der diese ändert, bei 7,1.

Siehe auch
Serialisierung (C#)
C#-Programmierhandbuch
Anweisungen, Ausdrücke und Operatoren (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Der C#-Code, der eine Anwendung umfasst, besteht aus Anweisungen, die aus Schlüsselwörtern, Ausdrücken
und Operatoren bestehen. Dieser Abschnitt enthält Informationen bezüglich dieser wichtigen Elemente eines
C#-Programms.
Weitere Informationen finden Sie unter:
Anweisungen
Operatoren und Ausdrücke
Ausdruckskörpermember
Übereinstimmungsvergleiche

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Umwandlung und Typkonvertierungen
Anweisungen (C#-Programmierhandbuch)
04.11.2021 • 6 minutes to read

Von einer Anwendung ausgeführte Aktionen werden in Anweisungen ausgedrückt. Häufig verwendete Aktionen
umfassen das Deklarieren von Variablen, Zuweisen von Werten, Aufrufen von Methoden, Durchlaufen von
Auflistungen und das Verzweigen auf einen oder einen anderen Codeblock, je nach gegebener Bedingung. Die
Reihenfolge, in der Anweisungen in einem Programm ausgeführt werden, wird „Ablaufsteuerung“ oder
„Ausführungsablauf“ genannt. Die Ablaufsteuerung kann jedes Mal, wenn ein Programm ausgeführt wird,
variieren, je nachdem, wie die Anwendung auf eine Eingabe reagiert, die sie während der Laufzeit empfängt.
Eine Anweisung kann aus einer einzelnen Codezeile, die mit einem Semikolon endet, oder aus einer Reihe von
einzeiligen Anweisungen in einem Block bestehen. Ein Anweisungsblock ist in geschweiften Klammern ({})
eingeschlossen und kann geschachtelte Blöcke enthalten. Der folgende Code zeigt zwei Beispiele für einzeilige
Anweisungen und einen mehrzeiligen Anweisungsblock:

static void Main()


{
// Declaration statement.
int counter;

// Assignment statement.
counter = 1;

// Error! This is an expression, not an expression statement.


// counter + 1;

// Declaration statements with initializers are functionally


// equivalent to declaration statement followed by assignment statement:
int[] radii = { 15, 32, 108, 74, 9 }; // Declare and initialize an array.
const double pi = 3.14159; // Declare and initialize constant.

// foreach statement block that contains multiple statements.


foreach (int radius in radii)
{
// Declaration statement with initializer.
double circumference = pi * (2 * radius);

// Expression statement (method invocation). A single-line


// statement can span multiple text lines because line breaks
// are treated as white space, which is ignored by the compiler.
System.Console.WriteLine("Radius of circle #{0} is {1}. Circumference = {2:N2}",
counter, radius, circumference);

// Expression statement (postfix increment).


counter++;
} // End of foreach statement block
} // End of Main method body.
} // End of SimpleStatements class.
/*
Output:
Radius of circle #1 = 15. Circumference = 94.25
Radius of circle #2 = 32. Circumference = 201.06
Radius of circle #3 = 108. Circumference = 678.58
Radius of circle #4 = 74. Circumference = 464.96
Radius of circle #5 = 9. Circumference = 56.55
*/
Typen von Anweisungen
Die folgende Tabelle enthält die verschiedenen Typen von Anweisungen in C# und die zugehörigen
Schlüsselwörter, mit Links zu Themen, die weitere Informationen enthalten:

K AT EGO RIE C #- SC H L ÜSSEL W Ö RT ER / H IN W EISE

Declaration statements (Deklarationsanweisungen) Eine Deklarationsanweisung führt eine neue Variable oder
Konstante ein. Eine Deklaration einer Variable kann der
Variable optional einen Wert zuweisen. Bei einer Deklaration
einer Konstante ist die Zuweisung erforderlich.

Expression statements (Ausdrucksanweisungen) Ausdrucksanweisungen, die einen Wert berechnen, müssen


den Wert in einer Variablen speichern.

Auswahlanweisungen Auswahlanweisungen helfen Ihnen in unterschiedliche


Codeabschnitte zu verzweigen, abhängig von einer oder
mehreren angegebenen Bedingungen. Weitere
Informationen finden Sie unter den folgenden Themen:
if
switch

Iterationsanweisungen Iterationsanweisungen helfen Ihnen, Auflistungen, wie z.B.


Arrays, zu durchlaufen oder den selben Satz von
Anweisungen solange zu wiederholen, bis eine angegebene
Bedingung erfüllt ist. Weitere Informationen finden Sie unter
den folgenden Themen:
do
for
foreach
while

Sprunganweisungen Sprunganweisungen übertragen Steuerelemente zu einem


anderen Codeabschnitt. Weitere Informationen finden Sie
unter den folgenden Themen:
break
continue
goto
return
yield

Ausnahmebehandlungsanweisungen Mit Ausnahmebehandlungsanweisungen können Sie


ordnungsgemäß von Ausnahmebedingungen
wiederherstellen, die zur Laufzeit auftreten. Weitere
Informationen finden Sie unter den folgenden Themen:
throw
try-catch
try-finally
try-catch-finally

Checked und unchecked Die Anweisungen „checked“ und „unchecked“ helfen Ihnen
anzugeben, ob numerische Vorgänge einen Überlauf
verursachen dürfen, wenn das Ergebnis in einer Variable
gespeichert ist, die zu klein ist, um den resultierenden Wert
zu speichern. Weitere Informationen finden Sie unter
checked und unchecked.
K AT EGO RIE C #- SC H L ÜSSEL W Ö RT ER / H IN W EISE

Die Anweisung await Wenn Sie eine Methode mit dem async -Modifizierer
kennzeichnen, können Sie den await Operator in der
Methode verwenden. Wenn ein Ausdruck await in der
asynchronen Methode erreicht wird, wird die Steuerung an
den Aufrufer zurückgegeben, und die Ausführung der
Methode wird angehalten, bis die erwartete Aufgabe
abgeschlossen ist. Wenn die Aufgabe abgeschlossen ist,
kann die Ausführung in der Methode fortgesetzt werden.

Ein einfaches Beispiel finden Sie im Abschnitt „Async-


Methoden“ unter Methoden. Weitere Informationen finden
Sie unter Asynchrone Programmierung mit Async und Await.

Die Anweisung yield return Ein Iterator führt eine benutzerdefinierte Iteration durch eine
Auflistung durch, z. B. eine Liste oder ein Array. Ein Iterator
verwendet die yield return -Anweisung, um jedes Element
einzeln nacheinander zurückzugeben. Wenn eine
yield return -Anweisung erreicht wird, wird die aktuelle
Position im Code gespeichert. Wenn der Iterator das nächste
Mal aufgerufen wird, wird die Ausführung von dieser
Position neu gestartet.

Weitere Informationen finden Sie unter Iteratoren.

Die Anweisung fixed Die fixed-Anweisung verhindert, dass der Garbage Collector
eine bewegliche Variable verschiebt. Weitere Informationen
finden Sie unter fixed.

Die Anweisung lock Die lock-Anweisung hilft Ihnen, den Zugriff auf Codeblöcke
auf jeweils einen Thread zu beschränken. Weitere
Informationen finden Sie unter lock.

Anweisungen mit Bezeichnung Sie können einer Anweisung eine Bezeichnung geben, und
anschließend mit dem Schlüsselwort goto zu der Anweisung
mit Bezeichnung springen. (Ein Beispiel finden Sie in der
folgenden Zeile.)

Die leere Anweisung Die leere Anweisung besteht aus einem einzelnen Semikolon.
Sie führt keine Aktion aus und kann an Orten verwendet
werden, an denen eine Anweisung erforderlich ist, aber keine
Aktion ausgeführt werden muss.

Deklarationsanweisungen
Der folgende Code zeigt Beispiele für Variablendeklarationen mit und ohne anfängliche Zuweisung sowie eine
Konstantendeklaration mit der erforderlichen Initialisierung.

// Variable declaration statements.


double area;
double radius = 2;

// Constant declaration statement.


const double pi = 3.14159;

Ausdrucksanweisungen
Der folgende Code zeigt Beispiele für Ausdrucksanweisungen, einschließlich Zuweisung, Objekterstellung mit
Zuweisung und Methodenaufruf.

// Expression statement (assignment).


area = 3.14 * (radius * radius);

// Error. Not statement because no assignment:


//circ * 2;

// Expression statement (method invocation).


System.Console.WriteLine();

// Expression statement (new object creation).


System.Collections.Generic.List<string> strings =
new System.Collections.Generic.List<string>();

Die leere Anweisung


Die folgenden Beispiele zeigen zwei Verwendungen für eine leere Anweisung:

void ProcessMessages()
{
while (ProcessMessage())
; // Statement needed here.
}

void F()
{
//...
if (done) goto exit;
//...
exit:
; // Statement needed here.
}

Eingebettete Anweisungen
Einige Anweisungen, z. B. Iterationsanweisungen, haben immer eine eingebettete Anweisung, die diesen folgt.
Diese eingebettete Anweisung kann entweder eine einzelne Anweisung oder mehrere Anweisungen sein, die
durch geschweifte Klammern ({}) in einem Anweisungsblock umschlossen werden. In geschweiften Klammern
({}) können sogar einzeilige eingebettete Anweisungen wie im folgenden Beispiel gezeigt eingeschlossen
werden:

// Recommended style. Embedded statement in block.


foreach (string s in System.IO.Directory.GetDirectories(
System.Environment.CurrentDirectory))
{
System.Console.WriteLine(s);
}

// Not recommended.
foreach (string s in System.IO.Directory.GetDirectories(
System.Environment.CurrentDirectory))
System.Console.WriteLine(s);

Eine eingebettete Anweisung, die nicht in geschweifte Klammern ({}) eingeschlossen ist, darf keine
Deklarationsanweisung oder eine Anweisung mit Bezeichnung sein. Dies wird im folgenden Beispiel gezeigt:
if(pointB == true)
//Error CS1023:
int radius = 5;

Fügen Sie die eingebettete Anweisung in einen Block ein, um den Fehler zu beheben:

if (b == true)
{
// OK:
System.DateTime d = System.DateTime.Now;
System.Console.WriteLine(d.ToLongDateString());
}

Geschachtelte Anweisungsblöcke
Anweisungsblöcke können geschachtelt werden, wie im folgenden Code gezeigt wird:

foreach (string s in System.IO.Directory.GetDirectories(


System.Environment.CurrentDirectory))
{
if (s.StartsWith("CSharp"))
{
if (s.EndsWith("TempFolder"))
{
return s;
}
}
}
return "Not found.";

Nicht erreichbare Anweisungen


Wenn der Compiler festlegt, dass der Steuerungsablauf nie auf eine bestimmte Anweisung unter jeglichen
Umständen zugreifen kann, wird die Warnung CS0162 erzeugt, wie im folgenden Beispiel gezeigt wird:

// An over-simplified example of unreachable code.


const int val = 5;
if (val < 4)
{
System.Console.WriteLine("I'll never write anything."); //CS0162
}

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Anweisungen der C#-Sprachspezifikation.

Weitere Informationen
C#-Programmierhandbuch
Anweisungsschlüsselwörter
C#-Operatoren und -Ausdrücke
Ausdruckskörpermember (C#-
Programmierhandbuch)
04.11.2021 • 3 minutes to read

Mit Ausdruckstextdefinitionen können Sie die Implementierung eines Members in einer sehr präzisen und
lesbaren Form darstellen. Sie können eine Ausdruckstextdefinition verwenden, wann immer die Logik für einen
unterstützten Member, z.B. eine Methode oder Eigenschaft, aus einem einzelnen Ausdruck besteht. Eine
Ausdruckstextdefinition hat die folgende allgemeine Syntax:

member => expression;

wobei expression ein gültiger Ausdruck ist.


Die Unterstützung für Ausdruckskörperdefinitionen wurde für Methoden und schreibgeschützte Eigenschaften
in C# 6 eingeführt und in C# 7.0 erweitert. Ausdruckstextdefinitionen können mit Typmember verwendet
werden, die in der folgenden Tabelle aufgeführt sind:

M EM B ER UN T ERST ÜT Z T A B . . .

Methode C# 6

Schreibgeschützte Eigenschaft C# 6

Property C# 7.0

Konstruktor C# 7.0

Finalizer C# 7.0

Indexer C# 7.0

Methoden
Eine Ausdruckskörpermethode besteht aus einem einzelnen Ausdruck, der einen Wert zurückgibt, dessen Typ
mit dem Rückgabetyp der Methode übereinstimmt bzw. für Methoden, die void zurückgeben, das einige
Vorgänge ausführt. Beispielsweise enthalten Typen, die die ToString-Methode außer Kraft setzen normalerweise
einen einzelnen Ausdruck, der die Zeichenfolgendarstellung des aktuellen Objekts zurückgibt.
Das folgende Beispiel definiert eine Person -Klasse, die die ToString-Methode mit einer Ausdruckstextmethode
außer Kraft setzt. Es wird auch eine DisplayName -Methode definiert, die einen Namen für die Konsole anzeigt.
Beachten Sie, dass das Schlüsselwort return nicht in der Ausdruckstextmethode ToString verwendet wird.
using System;

public class Person


{
public Person(string firstName, string lastName)
{
fname = firstName;
lname = lastName;
}

private string fname;


private string lname;

public override string ToString() => $"{fname} {lname}".Trim();


public void DisplayName() => Console.WriteLine(ToString());
}

class Example
{
static void Main()
{
Person p = new Person("Mandy", "Dejesus");
Console.WriteLine(p);
p.DisplayName();
}
}

Weitere Informationen finden Sie unter Methoden (C#-Programmierhandbuch).

Schreibgeschützte Eigenschaften
Ab C# 6 können Sie Ausdruckskörperdefinitionen zum Implementieren von schreibgeschützten Eigenschaften
verwenden. Verwenden Sie hierzu die folgende Syntax:

PropertyType PropertyName => expression;

Im folgenden Beispiel wird eine Location -Klasse definiert, deren schreibgeschützte Eigenschaft Name als
Ausdruckskörperdefinition implementiert wird, die den Wert des privaten locationName -Felds zurückgibt:

public class Location


{
private string locationName;

public Location(string name)


{
locationName = name;
}

public string Name => locationName;


}

Weitere Informationen zu Eigenschaften finden Sie unter Eigenschaften (C#-Programmierhandbuch).

Eigenschaften
Ab C# 7.0 können Sie Ausdruckskörperdefinitionen zum Implementieren von get -Eigenschaften und set -
Accessoren verwenden. Im folgenden Beispiel wird die dafür erforderliche Vorgehensweise veranschaulicht:
public class Location
{
private string locationName;

public Location(string name) => Name = name;

public string Name


{
get => locationName;
set => locationName = value;
}
}

Weitere Informationen zu Eigenschaften finden Sie unter Eigenschaften (C#-Programmierhandbuch).

Konstruktoren
Eine Ausdruckstextdefinition für einen Konstruktor enthält in der Regel einen einzelnen Zuweisungsausdruck
oder einen Methodenaufruf, der die Argumente des Konstruktors behandelt oder den Instanzzustand initialisiert.
Im folgenden Beispiel wird eine Location -Klasse definiert, deren Klassenkonstruktor einen einzelnen
Zeichenfolgenparameter namens name enthält. Die Ausdruckstextdefinition weist das Argument für die
Eigenschaft Name zu.

public class Location


{
private string locationName;

public Location(string name) => Name = name;

public string Name


{
get => locationName;
set => locationName = value;
}
}

Weitere Informationen finden Sie unter Konstruktoren (C#-Programmierhandbuch).

Finalizer
Eine Ausdruckstextdefinition für einen Finalizer enthält normalerweise Bereinigungsanweisungen, z.B.
Anweisungen, die nicht verwaltete Ressourcen veröffentlichen.
Im folgenden Beispiel wird ein Finalizer definiert, der eine Ausdruckstextdefinition verwendet, um anzugeben,
dass der Finalizer aufgerufen wurde.

using System;

public class Destroyer


{
public override string ToString() => GetType().Name;

~Destroyer() => Console.WriteLine($"The {ToString()} finalizer is executing.");


}

Weitere Informationen finden Sie unter Finalizer (C#-Programmierhandbuch).


Indexer
Genauso wie Eigenschaften bestehen die get - und set -Accessoren des Indexers aus
Ausdruckstextdefinitionen, wenn der get -Accessor aus einem einzelnen Ausdruck besteht, der einen Wert
zurückgibt, oder der set -Accessor führt eine einfache Zuweisung aus.
Im folgenden Beispiel wird eine Klasse namens Sports definiert, die ein internes String-Array enthält, das aus
den Namen von verschiedenen Sportarten besteht. Die get - und set -Accessoren des Indexers werden als
Ausdruckstextdefinitionen implementiert.

using System;
using System.Collections.Generic;

public class Sports


{
private string[] types = { "Baseball", "Basketball", "Football",
"Hockey", "Soccer", "Tennis",
"Volleyball" };

public string this[int i]


{
get => types[i];
set => types[i] = value;
}
}

Weitere Informationen finden Sie unter Indexer (C#-Programmierhandbuch).


Übereinstimmungsvergleiche (C#-
Programmierhandbuch)
04.11.2021 • 3 minutes to read

Unter bestimmten Umständen ist es erforderlich, die Gleichheit zweier Werte zu prüfen. In einigen Fällen prüfen
Sie die Wertgleichheit, die auch als Äquivalenz bezeichnet wird. Das bedeutet, Sie prüfen, ob die in zwei
Variablen enthaltenen Werte gleich sind. In anderen Fällen müssen Sie ermitteln, ob zwei Variablen auf das
gleiche zugrunde liegende Objekt im Arbeitsspeicher verweisen. Diese Art von Gleichheit wird als
Verweisgleichheit oder Identität bezeichnet. In diesem Thema werden diese zwei Arten der Gleichheit
beschrieben. Außerdem finden Sie hier Links zu verwandten Themen mit weiteren Informationen.

Verweisgleichheit
Verweisgleichheit ist gegeben, wenn zwei Objektverweise auf dasselbe zugrunde liegende Objekt verweisen.
Die Ursache hierfür kann eine einfache Zuweisung sein, wie im folgenden Beispiel gezeigt.

using System;
class Test
{
public int Num { get; set; }
public string Str { get; set; }

static void Main()


{
Test a = new Test() { Num = 1, Str = "Hi" };
Test b = new Test() { Num = 1, Str = "Hi" };

bool areEqual = System.Object.ReferenceEquals(a, b);


// False:
System.Console.WriteLine("ReferenceEquals(a, b) = {0}", areEqual);

// Assign b to a.
b = a;

// Repeat calls with different results.


areEqual = System.Object.ReferenceEquals(a, b);
// True:
System.Console.WriteLine("ReferenceEquals(a, b) = {0}", areEqual);

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

In diesem Code werden zwei Objekte erstellt, nach der Zuweisungsanweisung verweisen jedoch beide Verweise
auf das gleiche Objekt. Es liegt eine Verweisgleichheit vor. Verwenden Sie die ReferenceEquals-Methode, um zu
ermitteln, ob zwei Verweise auf das gleiche Objekt verweisen.
Das Konzept der Verweisgleichheit gilt nur für Verweistypen. Bei Werttypobjekten kann keine Verweisgleichheit
vorliegen, da bei Zuweisung einer Werttypinstanz zu einer Variablen eine Kopie des Werts erstellt wird. Aus
diesem Grund ist es unmöglich, dass zwei nicht geschachtelte Strukturen vorhanden sind, die auf die gleiche
Position im Arbeitsspeicher verweisen. Wenn Sie zwei Werttypen mit der ReferenceEquals-Methode vergleichen,
ist das Ergebnis immer false , selbst wenn die in den Objekten enthaltenen Werte alle identisch sind. Der
Grund hierfür ist, dass jede Variable in einer eigenen Objektinstanz geschachtelt ist. Weitere Informationen
finden Sie unter Vorgehensweise: Überprüfen auf Verweisgleichheit (Identität).

Wertgleichheit
Eine Wertgleichheit liegt dann vor, wenn zwei Objekte den gleichen Wert bzw. die gleichen Werte enthalten. Die
Prüfung auf Wertgleichheit für primitive Werttypen wie int oder bool ist einfach. Sie können den ==-Operator
verwenden, wie im folgenden Beispiel gezeigt.

int a = GetOriginalValue();
int b = GetCurrentValue();

// Test for value equality.


if (b == a)
{
// The two integers are equal.
}

Für die meisten anderen Typen ist die Prüfung auf Wertgleichheit komplexer, da es darauf ankommt, wie die
Wertgleichheit für den jeweiligen Typ definiert wird. Für Klassen und Strukturen mit mehreren Feldern oder
Eigenschaften wird Wertgleichheit oft so definiert, dass alle Felder oder Eigenschaften den gleichen Wert
aufweisen. Zwei Point -Objekte werden z. B. als äquivalent definiert, wenn pointA.X gleich pointB.X und pointA.Y
gleich pointB.Y ist. Für Datensätze bedeutet Wertgleichheit, dass zwei Variablen eines Datensatztyps gleich sind,
wenn die Typen sowie alle Eigenschafts- und Feldwerte übereinstimmen.
Die Äquivalenz muss jedoch nicht unbedingt auf allen Feldern in einem Typ basieren. Die Basis kann auch eine
Teilmenge sein. Wenn Sie Typen von einem anderen Besitzer vergleichen, vergewissern Sie sich, wie die
Gleichheit für den jeweiligen Typ definiert ist. Informationen zum Definieren von Wertgleichheit für Ihre eigenen
Klassen und Strukturen finden Sie unter Vorgehensweise: Definieren von Wertgleichheit für einen Typ.
Wertgleichheit für Gleitkommawerte
Übereinstimmungsvergleiche für Gleitkommawerte (double und float) sind aufgrund der Ungenauigkeit der
Gleitkommaarithmetik auf Computern mit Binärlogik problematisch. Weitere Informationen finden Sie in den
Hinweisen im Thema System.Double.

Verwandte Themen
T IT EL B ESC H REIB UN G

Vorgehensweise: Überprüfen auf Verweisgleichheit (Identität) Beschreibt, wie zwei Variablen auf Verweisgleichheit geprüft
werden.

Vorgehensweise: Definieren von Wertgleichheit für einen Typ Beschreibt, wie eine benutzerdefinierte Definition der
Wertgleichheit für einen Typ erstellt wird.

C#-Programmierhandbuch Links zu ausführlichen Informationen zu wichtigen Features


der C#-Sprache sowie zu Features, die über .NET für C#
verfügbar sind.

Typen Informationen zum C#-Typsystem und Links zu weiteren


Informationen.

Datensätze Stellt Informationen zu Datensatztypen bereit, die


standardmäßig auf Wertgleichheit testen.
Siehe auch
C#-Programmierhandbuch
Definieren von Wertgleichheit für eine Klasse oder
eine Struktur (C#-Programmierleitfaden)
04.11.2021 • 9 minutes to read

Datensätze implementieren automatisch Wertgleichheit. Erwägen Sie, einen record anstelle einer class zu
definieren, wenn Ihr Typ Daten modelliert und Wertgleichheit implementieren soll.
Wenn Sie eine Klasse oder Struktur definieren, entscheiden Sie, ob es sinnvoll ist, eine benutzerdefinierte
Definition der Wertgleichheit (oder Äquivalenz) für den Typ zu erstellen. In der Regel implementieren Sie
Wertgleichheit, wenn Objekte des Typs zu einer Auflistung hinzugefügt werden sollen oder wenn ihr
Hauptzweck im Speichern einer Reihe von Feldern oder Eigenschaften besteht. Sie können die Definition der
Wertgleichheit auf einem Vergleich aller Felder und Eigenschaften im Typ oder auf einer Teilmenge aufbauen.
In beiden Fällen und in beiden Klassen und Strukturen sollte Ihre Implementierung den fünf Garantien der
Äquivalenz folgen (für die folgenden Regeln wird angenommen, dass x , y und z nicht NULL sind):
1. Die reflexive Eigenschaft: x.Equals(x) gibt true zurück.
2. Die symmetrische Eigenschaft: x.Equals(y) gibt denselben Wert wie y.Equals(x) zurück.
3. Die transitive Eigenschaft: wenn (x.Equals(y) && y.Equals(z)) true zurückgibt, gibt x.Equals(z) true
zurück.
4. Aufeinander folgende Aufrufe von x.Equals(y) geben immer denselben Wert zurück, es sei denn, die
Objekte, auf die x und y verweisen, werden geändert.
5. Ein Nicht-NULL-Wert ist nicht gleich NULL. x.Equals(y) löst jedoch eine Ausnahme aus, wenn x NULL
ist. Dadurch wird Regel 1 oder 2 gebrochen, abhängig vom Argument für Equals .
Jede Struktur, die Sie definieren, besitzt bereits eine Standardimplementierung der Wertgleichheit, die von der
System.ValueType-Außerkraftsetzung der Object.Equals(Object)-Methode geerbt wurde. Diese Implementierung
verwendet Reflektion, um alle Felder und Eigenschaften im Typ zu untersuchen. Obwohl diese Implementierung
richtige Ergebnisse produziert, ist sie relativ langsam im Vergleich zu einer benutzerdefinierten
Implementierung, die Sie speziell für den Typ schreiben.
Die Implementierungsdetails für Wertgleichheit unterscheiden sich für Klassen und Strukturen. Sowohl Klassen
als auch Strukturen erfordern dieselben grundlegenden Schritte zur Implementierung der Gleichheit:
1. Überschreiben Sie die virtuelle Methode Object.Equals(Object). In den meisten Fällen sollte Ihre
Implementierung von bool Equals( object obj ) nur die typspezifische Equals -Methode aufrufen, die
die Implementierung der Schnittstelle System.IEquatable<T> darstellt. (Siehe Schritt 2)
2. Implementieren Sie die Schnittstelle System.IEquatable<T>, indem Sie eine typspezifische Equals -
Methode bereitstellen. An dieser Stelle wird der eigentliche Äquivalenzvergleich ausgeführt. Sie könnten
Gleichheit z.B. nur über den Vergleich von ein oder zwei Feldern in Ihrem Typ definieren. Lösen Sie keine
Ausnahmen aus Equals aus. Für Klassen, die durch Vererbung verwandt sind:
Diese Methode sollte nur Felder prüfen, die in der Klasse deklariert sind. Sie sollte base.Equals
aufrufen, um Felder in der Basisklasse zu prüfen. (Rufen Sie nicht base.Equals auf, wenn der Typ
direkt von Object erbt, da die Object-Implementierung von Object.Equals(Object) eine
Überprüfung der Verweisgleichheit durchführt.)
Zwei Variablen sollten nur dann als gleich betrachtet werden, wenn die Laufzeittypen der
verglichenen Variablen identisch sind. Stellen Sie außerdem sicher, dass die IEquatable -
Implementierung der Equals -Methode für den Laufzeittyp verwendet wird, wenn sich die
Laufzeit- und Kompilierzeittypen einer Variablen unterscheiden. Eine Strategie, mit der
sichergestellt wird, dass Laufzeittypen immer ordnungsgemäß verglichen werden, besteht darin,
IEquatable nur in sealed -Klassen zu implementieren. Weitere Informationen finden Sie weiter
unten in diesem Artikel im Klassenbeispiel.
3. Optional, aber empfohlen: Überladen Sie die Operatoren == und !=.
4. Überschreiben Sie Object.GetHashCode, damit zwei Objekte, die Wertgleichheit besitzen, den gleichen
Hashcode erzeugen.
5. Optional: Implementieren Sie zur Unterstützung von Definitionen für „größer als“ oder „kleiner als“ die
Schnittstelle IComparable<T> für Ihren Typ, und überladen Sie auch die Operatoren <= und >=.

NOTE
Ab C# 9.0 können Sie Datensätze verwenden, um ohne unnötige Codebausteine eine Semantik mit Wertgleichheit zu
erzielen.

Klassenbeispiel
Im folgenden Beispiel wird veranschaulicht, wie Sie Wertgleichheit in einer Klasse (Referenztyp) implementieren.

using System;

namespace ValueEqualityClass
{
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }

public TwoDPoint(int x, int y)


{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.X = x;
this.Y = y;
}

public override bool Equals(object obj) => this.Equals(obj as TwoDPoint);

public bool Equals(TwoDPoint p)


{
if (p is null)
{
return false;
}

// Optimization for a common success case.


if (Object.ReferenceEquals(this, p))
{
return true;
}

// If run-time types are not exactly the same, return false.


if (this.GetType() != p.GetType())
{
return false;
}

// Return true if the fields match.


// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}

public override int GetHashCode() => (X, Y).GetHashCode();

public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)


{
if (lhs is null)
{
if (rhs is null)
{
return true;
}

// Only the left side is null.


return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}

public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}

// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.


class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
public int Z { get; private set; }

public ThreeDPoint(int x, int y, int z)


: base(x, y)
{
if ((z < 1) || (z > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.Z = z;
}

public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint);

public bool Equals(ThreeDPoint p)


{
if (p is null)
{
return false;
}

// Optimization for a common success case.


if (Object.ReferenceEquals(this, p))
{
return true;
}

// Check properties that this class declares.


if (Z == p.Z)
{
// Let base class check its own fields
// and do the run-time type comparison.
return base.Equals((TwoDPoint)p);
}
else
{
return false;
}
}

public override int GetHashCode() => (X, Y, Z).GetHashCode();

public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)


{
if (lhs is null)
{
if (rhs is null)
{
// null == null = true.
return true;
}

// Only the left side is null.


return false;
}
// Equals handles the case of null on right side.
return lhs.Equals(rhs);
}

public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}

class Program
{
static void Main(string[] args)
{
ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointC = null;
int i = 5;

Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));


Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
Console.WriteLine("null comparison = {0}", pointA.Equals(pointC));
Console.WriteLine("Compare to some other type = {0}", pointA.Equals(i));

TwoDPoint pointD = null;


TwoDPoint pointE = null;

Console.WriteLine("Two null TwoDPoints are equal: {0}", pointD == pointE);

pointE = new TwoDPoint(3, 4);


Console.WriteLine("(pointE == pointA) = {0}", pointE == pointA);
Console.WriteLine("(pointA == pointE) = {0}", pointA == pointE);
Console.WriteLine("(pointA != pointE) = {0}", pointA != pointE);

System.Collections.ArrayList list = new System.Collections.ArrayList();


list.Add(new ThreeDPoint(3, 4, 5));
Console.WriteLine("pointE.Equals(list[0]): {0}", pointE.Equals(list[0]));

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
null comparison = False
Compare to some other type = False
Two null TwoDPoints are equal: True
(pointE == pointA) = False
(pointA == pointE) = False
(pointA != pointE) = True
pointE.Equals(list[0]): False
pointE.Equals(list[0]): False
*/
}

Die standardmäßige Implementierung beider Object.Equals(Object)-Methoden führt bei Klassen (Referenztypen)


einen Verweisgleichheitsvergleich, keine Wertgleichheitsprüfung aus. Wenn eine Implementierer die virtuelle
Methode überschreibt, dient dies dazu, ihr Wertgleichheitssemantik zu verleihen.
Die Operatoren == und != können mit Klassen verwendet werden, selbst wenn die Klasse sie nicht überlädt.
Allerdings ist das Standardverhalten eine Ausführung einer Verweisgleichheitsprüfung. Wenn Sie die Equals -
Methode in einer Klasse überladen, sollten Sie die Operatoren == und != überladen, obwohl dies nicht
erforderlich ist.

IMPORTANT
Der vorherige Beispielcode verarbeitet möglicherweise nicht jedes Vererbungsszenario, wie Sie es erwarten. Betrachten Sie
folgenden Code:

TwoDPoint p1 = new ThreeDPoint(1, 2, 3);


TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True

Mit diesem Code wird gemeldet, dass p1 trotz des Unterschieds bei den z -Werten gleich p2 ist. Der Unterschied
wird ignoriert, da der Compiler die TwoDPoint -Implementierung von IEquatable auf Grundlage des Kompilierzeittyps
auswählt.
Die integrierte Wertgleichheit von record -Typen verarbeitet solche Szenarien ordnungsgemäß. Wenn TwoDPoint und
ThreeDPoint record -Typen wären, lautete das Ergebnis von p1.Equals(p2) False . Weitere Informationen finden
Sie unter Gleichheit in Vererbungshierarchien von record -Typen.

Strukturbeispiel
Im folgenden Beispiel wird veranschaulicht, wie Sie Wertgleichheit in einer Struktur (Werttyp) implementieren:

using System;

namespace ValueEqualityStruct
{
struct TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }

public TwoDPoint(int x, int y)


: this()
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
X = x;
Y = y;
}

public override bool Equals(object obj) => obj is TwoDPoint other && this.Equals(other);

public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y;

public override int GetHashCode() => (X, Y).GetHashCode();

public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}

class Program
{
static void Main(string[] args)
{
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
int i = 5;

// True:
Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
// True:
Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
// True:
Console.WriteLine("object.Equals(pointA, pointB) = {0}", object.Equals(pointA, pointB));
// False:
Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));
// False:
Console.WriteLine("(pointA == null) = {0}", pointA == null);
// True:
Console.WriteLine("(pointA != null) = {0}", pointA != null);
// False:
Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));
// CS0019:
// Console.WriteLine("pointA == i = {0}", pointA == i);

// Compare unboxed to boxed.


System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new TwoDPoint(3, 4));
// True:
Console.WriteLine("pointA.Equals(list[0]): {0}", pointA.Equals(list[0]));

// Compare nullable to nullable and to non-nullable.


TwoDPoint? pointC = null;
TwoDPoint? pointD = null;
// False:
Console.WriteLine("pointA == (pointC = null) = {0}", pointA == pointC);
// True:
Console.WriteLine("pointC == pointD = {0}", pointC == pointD);

TwoDPoint temp = new TwoDPoint(3, 4);


pointC = temp;
// True:
Console.WriteLine("pointA == (pointC = 3,4) = {0}", pointA == pointC);

pointD = temp;
// True:
Console.WriteLine("pointD == (pointC = 3,4) = {0}", pointD == pointC);

Console.WriteLine("Press any key to exit.");


Console.ReadKey();
}
}

/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
Object.Equals(pointA, pointB) = True
pointA.Equals(null) = False
(pointA == null) = False
(pointA != null) = True
pointA.Equals(i) = False
pointE.Equals(list[0]): True
pointA == (pointC = null) = False
pointC == pointD = True
pointA == (pointC = 3,4) = True
pointD == (pointC = 3,4) = True
*/
}

Für Strukturen führt die Standardimplementierung von Object.Equals(Object) (was die außer Kraft gesetzte
Version in System.ValueType ist) eine Überprüfung der Wertgleichheit durch, indem sie Reflektion zum
Vergleichen der Werte jedes Felds im Typ verwendet. Wenn ein Implementierer die virtuelle Equals -Methode
in einer Struktur überschreibt, dient dies der Bereitstellung effizienterer Mittel für die Ausführung der
Wertgleichheitsprüfung und optional dazu, den Vergleich auf einer Teilmenge des Felds oder der Eigenschaften
der Struktur zu basieren.
Die Operatoren == und != können nicht auf Strukturen operieren, es sei denn, die Struktur überlädt sie explizit.

Siehe auch
Übereinstimmungsvergleiche
C#-Programmierhandbuch
Vorgehensweise: Überprüfen auf Verweisgleichheit
(Identität) (C#-Programmierleitfaden)
04.11.2021 • 3 minutes to read

Sie müssen zur Unterstützung von Verweisgleichheitsprüfungen in Ihren Typen keine benutzerdefinierte Logik
implementieren. Diese Funktionalität wird für alle Typen mit der statischen Object.ReferenceEquals-Methode
bereitgestellt.
Im folgenden Beispiel wird gezeigt, wie Sie zwei Variablen auf Verweisgleichheit prüfen, d.h. ob diese auf das
gleiche Objekt im Arbeitsspeicher verweisen.
Das Beispiel veranschaulicht außerdem, warum Object.ReferenceEquals immer false für Wertetypen
zurückgibt und warum ReferenceEquals nicht zur Prüfung der Übereinstimmung von Zeichenfolgen geeignet ist.

Beispiel
using System;
using System.Text;

namespace TestReferenceEquality
{
struct TestStruct
{
public int Num { get; private set; }
public string Name { get; private set; }

public TestStruct(int i, string s) : this()


{
Num = i;
Name = s;
}
}

class TestClass
{
public int Num { get; set; }
public string Name { get; set; }
}

class Program
{
static void Main()
{
// Demonstrate reference equality with reference types.
#region ReferenceTypes

// Create two reference type instances that have identical values.


TestClass tcA = new TestClass() { Num = 1, Name = "New TestClass" };
TestClass tcB = new TestClass() { Num = 1, Name = "New TestClass" };

Console.WriteLine("ReferenceEquals(tcA, tcB) = {0}",


Object.ReferenceEquals(tcA, tcB)); // false

// After assignment, tcB and tcA refer to the same object.


// They now have reference equality.
tcB = tcA;
Console.WriteLine("After assignment: ReferenceEquals(tcA, tcB) = {0}",
Object.ReferenceEquals(tcA, tcB)); // true
// Changes made to tcA are reflected in tcB. Therefore, objects
// that have reference equality also have value equality.
tcA.Num = 42;
tcA.Name = "TestClass 42";
Console.WriteLine("tcB.Name = {0} tcB.Num: {1}", tcB.Name, tcB.Num);
#endregion

// Demonstrate that two value type instances never have reference equality.
#region ValueTypes

TestStruct tsC = new TestStruct( 1, "TestStruct 1");

// Value types are copied on assignment. tsD and tsC have


// the same values but are not the same object.
TestStruct tsD = tsC;
Console.WriteLine("After assignment: ReferenceEquals(tsC, tsD) = {0}",
Object.ReferenceEquals(tsC, tsD)); // false
#endregion

#region stringRefEquality
// Constant strings within the same assembly are always interned by the runtime.
// This means they are stored in the same location in memory. Therefore,
// the two strings have reference equality although no assignment takes place.
string strA = "Hello world!";
string strB = "Hello world!";
Console.WriteLine("ReferenceEquals(strA, strB) = {0}",
Object.ReferenceEquals(strA, strB)); // true

// After a new string is assigned to strA, strA and strB


// are no longer interned and no longer have reference equality.
strA = "Goodbye world!";
Console.WriteLine("strA = \"{0}\" strB = \"{1}\"", strA, strB);

Console.WriteLine("After strA changes, ReferenceEquals(strA, strB) = {0}",


Object.ReferenceEquals(strA, strB)); // false

// A string that is created at runtime cannot be interned.


StringBuilder sb = new StringBuilder("Hello world!");
string stringC = sb.ToString();
// False:
Console.WriteLine("ReferenceEquals(stringC, strB) = {0}",
Object.ReferenceEquals(stringC, strB));

// The string class overloads the == operator to perform an equality comparison.


Console.WriteLine("stringC == strB = {0}", stringC == strB); // true

#endregion

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
}

/* Output:
ReferenceEquals(tcA, tcB) = False
After assignment: ReferenceEquals(tcA, tcB) = True
tcB.Name = TestClass 42 tcB.Num: 42
After assignment: ReferenceEquals(tsC, tsD) = False
ReferenceEquals(strA, strB) = True
strA = "Goodbye world!" strB = "Hello world!"
After strA changes, ReferenceEquals(strA, strB) = False
ReferenceEquals(stringC, strB) = False
stringC == strB = True
*/
Die Implementierung von Equals in der universellen Basisklasse System.Object führt auch eine
Verweisgleichheitsprüfung aus. Diese Methode wird jedoch nicht empfohlen, da es zu unerwarteten
Ergebnissen kommen kann, wenn eine Klasse die Methode überschreibt. Dasselbe gilt für den == -Operator und
den != -Operator. Bei Anwendung auf Verweistypen wird mit == und != standardmäßig eine
Verweisgleichheitsprüfung ausgeführt. Der Operator kann aber von abgeleiteten Klassen überladen werden und
eine Wertgleichheitsprüfung ausführen. Um Fehler möglichst zu vermeiden, verwenden Sie am besten immer
ReferenceEquals zur Prüfung der Verweisgleichheit zweier Objekte.
Konstantenzeichenfolgen innerhalb der gleichen Assembly werden durch die Laufzeit immer intern gespeichert.
Das heißt, es wird nur eine Instanz jedes eindeutigen Zeichenfolgenliterals beibehalten. Es gibt jedoch keine
Garantie dafür, dass zur Laufzeit erstellte Zeichenfolgen oder zwei gleiche Konstantenzeichenfolgen in
unterschiedlichen Assemblys intern gespeichert werden.

Siehe auch
Übereinstimmungsvergleiche
Umwandlung und Typkonvertierungen (C#-
Programmierhandbuch)
04.11.2021 • 5 minutes to read

Weil C# zur Kompilierzeit statisch typisiert ist, kann eine Variable, nachdem sie deklariert wurde, nicht erneut
deklariert werden oder einen Wert von einem anderen Typ zugewiesen bekommen, es sei denn, dieser Typ ist
implizit in den Typ der Variable konvertierbar. string kann beispielsweise nicht implizit in int konvertiert
werden. Deshalb können Sie, nachdem Sie i als int deklariert haben, nicht die Zeichenfolge „Hello“ zuweisen.
Dies wird im folgenden Beispiel veranschaulicht:

int i;

// error CS0029: Cannot implicitly convert type 'string' to 'int'


i = "Hello";

Manchmal müssen Sie möglicherweise einen Wert in eine Variable oder einen Methodenparameter eines
anderen Typen kopieren. Sie haben z.B. möglicherweise eine Ganzzahlvariable, die Sie an eine Methode
übergeben müssen, deren Parameter vom Type double ist. Möglicherweise müssen Sie auch einer Variablen
eines Schnittstellentyps eine Klassenvariable zuweisen. Derartige Vorgänge werden als Typkonvertierungen
bezeichnet. In C# können Sie folgende Arten von Konvertierungen durchführen:
Implizite Konver tierungen : Es ist keine besondere Syntax erforderlich, da die Konvertierung immer
erfolgreich ist und keine Daten verloren gehen. Beispiele sind Konvertierungen von kleinere in größere
Ganzzahltypen und Konvertierungen von abgeleiteten in Basisklassen.
Explizite Konver tierungen (Umwandlungen) : Für explizite Konvertierungen ist ein Cast-Ausdruck
erforderlich. Eine Umwandlung ist erforderlich, wenn Informationen bei einer Konvertierung verloren
gehen können oder wenn die Konvertierung aus anderen Gründen fehlschlagen könnte. Häufig
auftretende Beispiele sind u.a. numerische Konvertierungen in einen Typen, der eine geringere
Genauigkeit oder einen kleineren Bereich aufweist, oder Konvertierungen einer Instanz einer Basisklasse
in eine abgeleitete Klasse.
Benutzerdefinier te Konver tierungen : Benutzerdefinierte Konvertierungen werden anhand spezieller
Methoden durchgeführt, die Sie definieren können, um explizite und implizite Konvertierungen zwischen
benutzerdefinierten Typen zu ermöglichen, die nicht in einer Beziehung „Basisklasse – abgeleitete Klasse“
zueinander stehen. Weitere Informationen finden Sie unter Benutzerdefinierte Konvertierungsoperatoren.
Konver tierungen mit Hilfsklassen : Für eine Konvertierung von nicht kompatiblen Typen, z.B. von
ganzen Zahlen und System.DateTime-Objekten, oder von Hexadezimalzeichenfolgen und Bytearrays
können Sie die System.BitConverter-Klasse, die System.Convert-Klasse und die Parse -Methoden der
integrierten numerischen Typen (z.B. Int32.Parse) verwenden. Weitere Informationen finden Sie unter
Vorgehensweise: Konvertieren eines Bytearrays in einen ganzzahligen Typ, Vorgehensweise: Konvertieren
einer Zeichenfolge in eine Zahl und Vorgehensweise: Konvertieren zwischen Hexadezimalzeichenfolgen
und numerischen Typen.

Implizite Konvertierungen
Eine implizite Konvertierung kann für integrierte numerische Typen durchgeführt werden, wenn der zu
speichernde Wert in die Variable passt, ohne abgeschnitten oder abgerundet zu werden. Für integrale Typen
bedeutet dies, dass der Bereich des Quelltyps eine korrekte Teilmenge des Bereichs für den Zieltyp ist. Zum
Beispiel kann eine Variable vom Typ long (64-Bit-Integer) jeden Wert speichern, den eine Variable vom Typ int
(32-Bit-Integer) speichern kann. Im folgenden Beispiel konvertiert der Compiler den Wert von num auf der
rechten Seite implizit in einen long -Typ, bevor er ihn bigNum zuweist.

// Implicit conversion. A long can


// hold any value an int can hold, and more!
int num = 2147483647;
long bigNum = num;

Eine vollständige Liste aller impliziten numerischen Konvertierungen finden Sie unter Implizite numerische
Konvertierungen im Artikel Integrierte numerische Konvertierungen (C#-Referenz).
Eine implizite Konvertierungen für Verweistypen von einer Klasse in jede ihrer direkten oder indirekten
Basisklassen oder Schnittstellen ist immer möglich. Es ist keine spezielle Syntax erforderlich, da eine abgeleitete
Klasse immer alle Member der Basisklasse enthält.

Derived d = new Derived();

// Always OK.
Base b = d;

Explizite Konvertierungen
Wenn allerdings eine Konvertierung nicht ohne möglichen Informationsverlust durchgeführt werden kann,
fordert der Compiler eine explizite Konvertierung; diese wird als Umwandlung bezeichnet. Eine Umwandlung
bietet die Möglichkeit, den Compiler explizit zu verständigen, dass Sie eine Konvertierung vornehmen möchten,
und dass es Ihnen bewusst ist, dass dies einen Datenverlust zur Folge haben kann. oder die Umwandlung
schlägt zur Laufzeit fehl. Wenn Sie eine Umwandlung durchführen möchten, geben Sie den Typ, in den
umgewandelt werden soll, in Klammern am Anfang des zu konvertierenden Wertes oder der zu
konvertierenden Variablen an. Das folgende Programm wandelt ein double in ein int um. Das Programm führt
ohne die Umwandlung keine Kompilierung durch.

class Test
{
static void Main()
{
double x = 1234.7;
int a;
// Cast double to int.
a = (int)x;
System.Console.WriteLine(a);
}
}
// Output: 1234

Eine vollständige Liste der expliziten numerischen Konvertierungen finden Sie unter Explicit numeric
conversions (Explizite numerische Konvertierungen) im Artikel Built-in numeric conversions (Integrierte
numerische Konvertierungen).
Eine explizite Umwandlung ist für Verweistypen erforderlich, wenn Sie von einer Basisklasse in eine abgeleitete
Klasse konvertieren möchten:
// Create a new derived type.
Giraffe g = new Giraffe();

// Implicit conversion to base type is safe.


Animal a = g;

// Explicit conversion is required to cast back


// to derived type. Note: This will compile but will
// throw an exception at run time if the right-side
// object is not in fact a Giraffe.
Giraffe g2 = (Giraffe)a;

Ein Umwandlungsvorgang zwischen Verweistypen ändert nicht den Laufzeittypen des zugrunde liegenden
Objekts; er ändert lediglich den Typ des Wertes, der als Verweis auf das Objekt verwendet wird. Weitere
Informationen finden Sie unter Polymorphie.

Typkonvertierungsausnahmen zur Laufzeit


In manchen Verweistypkonvertierungen kann der Compiler nicht bestimmen, ob eine Umwandlung zulässig
wäre. Es ist möglich, dass ein Umwandlungsvorgang, der ordnungsgemäß kompiliert, zur Laufzeit fehlschlägt.
Eine fehlgeschlagene Typumwandlung zur Laufzeit löst wie in folgendem Beispiel dargestellt eine
InvalidCastException aus.

class Animal
{
public void Eat() => System.Console.WriteLine("Eating.");

public override string ToString() => "I am an animal.";


}

class Reptile : Animal { }


class Mammal : Animal { }

class UnSafeCast
{
static void Main()
{
Test(new Mammal());

// Keep the console window open in debug mode.


System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}

static void Test(Animal a)


{
// System.InvalidCastException at run time
// Unable to cast object of type 'Mammal' to type 'Reptile'
Reptile r = (Reptile)a;
}
}

Die Methode Test verfügt über einen Parameter Animal , sodass es eine gefährliche Annahme ist, das
Argument a explizit in ein Reptile -Element umzuwandeln. Es ist sicherer, keine Annahmen zu verwenden,
sondern stattdessen den Typ zu überprüfen. C# stellt den is-Operator bereit, sodass Sie die Kompatibilität prüfen
können, bevor Sie die Umwandlung tatsächlich ausführen. Weitere Informationen finden Sie unter
Vorgehensweise: Sicheres Umwandeln mit Musterabgleich und den Operatoren „as“ und „is“.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Konvertierungen der C#-Sprachspezifikation.

Siehe auch
C#-Programmierhandbuch
Typen
Cast-Ausdruck
Benutzerdefinierte Konvertierungsoperatoren
Verallgemeinerte Typkonvertierung
Konvertieren einer Zeichenfolge in eine Zahl
Boxing und Unboxing (C#-Programmierhandbuch)
04.11.2021 • 5 minutes to read

Beim Boxing handelt es sich um die Konvertierung eines Werttyps in den Typ object oder in einen beliebigen
anderen Schnittstellentyp, der durch diesen Werttyp implementiert wird. Wenn die Common Language Runtime
(CLR) für einen Werttyp das Boxing durchführt, wird der Wert mit einer System.Object-Instanz umschlossen
und im verwalteten Heap gespeichert. Durch Unboxing wird der Werttyp aus dem Objekt extrahiert. Boxing ist
implizit, Unboxing ist explizit. Das Konzept von Boxing und Unboxing unterliegt der einheitlichen C#-Ansicht des
Typsystems, in dem ein Wert eines beliebigen Typs als Objekt behandelt werden kann.
Im folgenden Beispiel wird die ganzzahlige Variable i mittels Boxing konvertiert und dem Objekt o
zugewiesen.

int i = 123;
// The following line boxes i.
object o = i;

Das Objekt o kann dann mittels Unboxing zurückkonvertiert und der ganzzahligen Variablen i zugewiesen
werden:

o = 123;
i = (int)o; // unboxing

Die folgenden Beispiele veranschaulichen, wie Boxing in C# verwendet wird.

// String.Concat example.
// String.Concat has many versions. Rest the mouse pointer on
// Concat in the following statement to verify that the version
// that is used here takes three object arguments. Both 42 and
// true must be boxed.
Console.WriteLine(String.Concat("Answer", 42, true));

// List example.
// Create a list of objects to hold a heterogeneous collection
// of elements.
List<object> mixedList = new List<object>();

// Add a string element to the list.


mixedList.Add("First Group:");

// Add some integers to the list.


for (int j = 1; j < 5; j++)
{
// Rest the mouse pointer over j to verify that you are adding
// an int to a list of objects. Each element j is boxed when
// you add j to mixedList.
mixedList.Add(j);
}

// Add another string and more integers.


mixedList.Add("Second Group:");
for (int j = 5; j < 10; j++)
{
mixedList.Add(j);
}

// Display the elements in the list. Declare the loop variable by


// Display the elements in the list. Declare the loop variable by
// using var, so that the compiler assigns its type.
foreach (var item in mixedList)
{
// Rest the mouse pointer over item to verify that the elements
// of mixedList are objects.
Console.WriteLine(item);
}

// The following loop sums the squares of the first group of boxed
// integers in mixedList. The list elements are objects, and cannot
// be multiplied or added to the sum until they are unboxed. The
// unboxing must be done explicitly.
var sum = 0;
for (var j = 1; j < 5; j++)
{
// The following statement causes a compiler error: Operator
// '*' cannot be applied to operands of type 'object' and
// 'object'.
//sum += mixedList[j] * mixedList[j]);

// After the list elements are unboxed, the computation does


// not cause a compiler error.
sum += (int)mixedList[j] * (int)mixedList[j];
}

// The sum displayed is 30, the sum of 1 + 4 + 9 + 16.


Console.WriteLine("Sum: " + sum);

// Output:
// Answer42True
// First Group:
// 1
// 2
// 3
// 4
// Second Group:
// 5
// 6
// 7
// 8
// 9
// Sum: 30

Leistung
Im Verhältnis zu einfachen Zuweisungen sind Boxing und Unboxing rechentechnisch aufwändige Prozesse.
Wenn ein Werttyp mittels Boxing konvertiert wird, muss ein neues Objekt zugeordnet und erstellt werden. Die
für Unboxing erforderliche Umwandlung ist ebenfalls, jedoch in geringerem Maße rechentechnisch aufwändig.
Weitere Informationen finden Sie unter Leistung.

Boxing
Boxing wird verwendet, um Werttypen im Heap der Garbage Collection zu speichern. Beim Boxing handelt es
sich um die implizite Konvertierung eines Werttyps in den Typ object oder in einen beliebigen anderen
Schnittstellentyp, der durch diesen Werttyp implementiert wird. Beim Boxing eines Werttyps wird auf dem Heap
eine Objektinstanz zugeordnet. Anschließend wird der Wert in das neue Objekt kopiert.
Beachten Sie die folgende Deklaration einer Werttypvariablen:

int i = 123;
Mit der folgenden Anweisung wird der Boxing-Vorgang implizit auf die Variable i angewendet:

// Boxing copies the value of i into object o.


object o = i;

Diese Anweisung bewirkt, dass der Objektverweis o auf dem Stapel erstellt wird, der auf einen Wert vom Typ
int auf dem Heap verweist. Dieser Wert ist eine Kopie des Werttyps, der der Variablen i zugewiesen ist. In
der folgenden Abbildung der Boxing-Konversation ist der Unterschied zwischen den Variablen i und o
dargestellt.

Es auch möglich, das Boxing wie im folgenden Beispiel explizit auszuführen. Explizites Boxing ist jedoch nie
erforderlich:

int i = 123;
object o = (object)i; // explicit boxing

Beispiel
In diesem Beispiel wird die Ganzzahlvariable i mittels Boxing in das Objekt o konvertiert. Anschließend wird
der in der Variablen i gespeicherte Wert von 123 in 456 geändert. Das Beispiel veranschaulicht, dass der
ursprüngliche Werttyp und das durch Boxing entstehende Objekt unterschiedliche Speicherorte verwenden und
daher verschiedene Werte speichern können.

class TestBoxing
{
static void Main()
{
int i = 123;

// Boxing copies the value of i into object o.


object o = i;

// Change the value of i.


i = 456;

// The change in i doesn't affect the value stored in o.


System.Console.WriteLine("The value-type value = {0}", i);
System.Console.WriteLine("The object-type value = {0}", o);
}
}
/* Output:
The value-type value = 456
The object-type value = 123
*/

Unboxing
Beim Unboxing handelt es sich um eine explizite Konvertierung vom object -Typ in einen Werttyp bzw. von
einem Schnittstellentyp in einen Werttyp, durch den die Schnittstelle implementiert wird. Ein Unboxing-Vorgang
umfasst folgende Schritte:
Überprüfen der Objektinstanz, um sicherzustellen, dass es sich um einen mittels Boxing "verpackten"
Wert des jeweiligen Werttyps handelt.
Kopieren des Werts aus der Instanz in die Werttypvariable.
Anhand der folgenden Anweisungen werden sowohl Boxing-Vorgänge als auch Unboxing-Vorgänge
veranschaulicht:

int i = 123; // a value type


object o = i; // boxing
int j = (int)o; // unboxing

In der folgenden Abbildung ist das Ergebnis der vorherigen Anweisungen dargestellt:

Damit das Unboxing eines Werttypen zur Laufzeit erfolgreich verläuft, muss das zu konvertierende Element ein
Verweis auf ein Objekt sein, das zuvor durch Boxing einer Instanz dieses Werttyps erstellt wurde. Der Versuch,
ein Unboxing durchzuführen, null löst ein NullReferenceException aus. Der Versuch, einen Verweis auf einen
nicht kompatiblen Werttyp mittels Unboxing zu konvertieren, führt zu einer InvalidCastException.
Beispiel
Im folgenden Beispiel wird ein Fall von ungültigem Unboxing und der sich daraus ergebenden
InvalidCastException veranschaulicht. Bei Verwendung von try und catch wird eine Fehlermeldung
angezeigt, wenn der Fehler auftritt.

class TestUnboxing
{
static void Main()
{
int i = 123;
object o = i; // implicit boxing

try
{
int j = (short)o; // attempt to unbox

System.Console.WriteLine("Unboxing OK.");
}
catch (System.InvalidCastException e)
{
System.Console.WriteLine("{0} Error: Incorrect unboxing.", e.Message);
}
}
}

Dieses Programm gibt Folgendes aus:


Specified cast is not valid. Error: Incorrect unboxing.

Wenn Sie die Anweisung


int j = (short)o;

in:

int j = (int)o;

ändern, wird die Konvertierung ausgeführt und Folgendes ausgegeben.


Unboxing OK.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Verweistypen
Werttypen
Vorgehensweise: Konvertieren eines Bytearrays in
einen ganzzahligen Typ (C#-Programmierleitfaden)
04.11.2021 • 2 minutes to read

In diesem Beispiel wird veranschaulicht, wie Sie die BitConverter-Klasse dazu verwenden, einen Bytearray in
einen int-Typ und wieder zurück in ein Bytearray zu konvertieren. Sie müssen möglicherweise Bytes in einen
integrierten Datentyp konvertieren, wenn Sie z.B. Bytes aus dem Netzwerk gelesen haben. Die folgende Tabelle
enthält zusätzlich zu der Methode ToInt32(Byte[], Int32) aus dem Beispiel auch die Methoden in der
BitConverter-Klasse, die Bytes (aus einem Bytearray) in andere integrierte Typen konvertiert.

Z URÜC KGEGEB EN ER T Y P M ET H O DE

bool ToBoolean(Byte[], Int32)

char ToChar(Byte[], Int32)

double ToDouble(Byte[], Int32)

short ToInt16(Byte[], Int32)

int ToInt32(Byte[], Int32)

long ToInt64(Byte[], Int32)

float ToSingle(Byte[], Int32)

ushort ToUInt16(Byte[], Int32)

uint ToUInt32(Byte[], Int32)

ulong ToUInt64(Byte[], Int32)

Beispiele
In diesem Beispiel wird ein Bytearray initialisiert und umgekehrt, wenn die Computerarchitektur Little-Endian
entspricht (das kleinstwertige Byte wird am Anfang gespeichert). Anschließend wird die Methode ToInt32(Byte[],
Int32) aufgerufen, um vier Bytes im Array in einen int zu konvertieren. Das zweite Argument für
ToInt32(Byte[], Int32) gibt den Startindex des Bytearrays an.

NOTE
Die Ausgabe kann sich je nach der Bytereihenfolge der Architektur Ihres Computers unterscheiden.
byte[] bytes = { 0, 0, 0, 25 };

// If the system architecture is little-endian (that is, little end first),


// reverse the byte array.
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);

int i = BitConverter.ToInt32(bytes, 0);


Console.WriteLine("int: {0}", i);
// Output: int: 25

In diesem Beispiel wird die Methode GetBytes(Int32) der Klasse BitConverter aufgerufen, um int in ein
Bytearray zu konvertieren.

NOTE
Die Ausgabe kann sich je nach der Bytereihenfolge der Architektur Ihres Computers unterscheiden.

byte[] bytes = BitConverter.GetBytes(201805978);


Console.WriteLine("byte array: " + BitConverter.ToString(bytes));
// Output: byte array: 9A-50-07-0C

Siehe auch
BitConverter
IsLittleEndian
Typen
Vorgehensweise: Konvertieren einer Zeichenfolge in
eine Zahl (C#-Programmierleitfaden)
04.11.2021 • 4 minutes to read

string wird in eine Zahl umgewandelt, indem Sie die Methode Parse oder TryParse für numerische Typen
(z. B. int , long und double ) oder Methoden in der System.Convert-Klasse aufrufen.
Es ist etwas effizienter und einfacher, eine TryParse -Methode (beispielsweise int.TryParse("11", out number) )
oder eine Parse -Methode (beispielsweise var number = int.Parse("11") ) aufzurufen. Die Verwendung einer
Convert-Methode eignet sich eher für allgemeine Objekte, die IConvertible implementieren.
Sie können die Methode Parse oder TryParse für den numerischen Typ verwenden, den Sie in der
Zeichenfolge erwarten, z. B. den Typ System.Int32. Die Convert.ToInt32-Methode verwendet Parse intern. Die
Parse -Methode gibt die konvertierte Zahl zurück, und die TryParse -Methode gibt einen booleschen Wert
zurück, der angibt, ob die Konvertierung erfolgreich war. Anschließend wird die konvertierte Zahl in einem out
-Parameter zurückgegeben. Wenn die Zeichenfolge in keinem gültigen Format vorliegt, löst Parse eine
Ausnahme aus, jedoch gibt TryParse den Wert false zurück. Beim Aufrufen einer Parse -Methode sollten Sie
grundsätzlich die Ausnahmebehandlung zum Abfangen einer FormatException verwenden, falls beim
Analysevorgang ein Fehler auftritt.

Aufrufen der Parse- oder TryParse-Methode


Die Methoden Parse und TryParse ignorieren Leerraum am Anfang und am Ende der Zeichenfolge. Alle
anderen Zeichen müssen jedoch Zeichen sein, die den entsprechenden numerischen Typ bilden ( int , long ,
ulong , float , decimal usw.). Leerraum innerhalb der Zeichenfolge, die die Zahl bildet, führt zu einem Fehler.
Beispielsweise können Sie decimal.TryParse verwenden, um „10“, „10.3“ oder „ 10 “ zu analysieren. Sie können
diese Methode jedoch nicht verwenden, um 10 aus „10X“, „1 0“ (beachten Sie das eingebettete Leerzeichen), „10
.3“ (beachten Sie das eingebettete Leerzeichen), „10e1“ ( float.TryParse funktioniert in diesem Fall) und so
weiter zu analysieren. Eine Zeichenfolge, deren Wert null oder String.Empty lautet, wird nicht erfolgreich
analysiert. Durch Aufruf der String.IsNullOrEmpty-Methode können Sie auf eine NULL-Zeichenfolge oder eine
leere Zeichenfolge prüfen, bevor Sie den Analyseversuch starten.
Das folgende Beispiel zeigt sowohl erfolgreiche als auch nicht erfolgreiche Aufrufe von Parse und TryParse .
using System;

public static class StringConversion


{
public static void Main()
{
string input = String.Empty;
try
{
int result = Int32.Parse(input);
Console.WriteLine(result);
}
catch (FormatException)
{
Console.WriteLine($"Unable to parse '{input}'");
}
// Output: Unable to parse ''

try
{
int numVal = Int32.Parse("-105");
Console.WriteLine(numVal);
}
catch (FormatException e)
{
Console.WriteLine(e.Message);
}
// Output: -105

if (Int32.TryParse("-105", out int j))


{
Console.WriteLine(j);
}
else
{
Console.WriteLine("String could not be parsed.");
}
// Output: -105

try
{
int m = Int32.Parse("abc");
}
catch (FormatException e)
{
Console.WriteLine(e.Message);
}
// Output: Input string was not in a correct format.

const string inputString = "abc";


if (Int32.TryParse(inputString, out int numValue))
{
Console.WriteLine(numValue);
}
else
{
Console.WriteLine($"Int32.TryParse could not parse '{inputString}' to an int.");
}
// Output: Int32.TryParse could not parse 'abc' to an int.
}
}

Das folgende Beispiel veranschaulicht einen Ansatz zum Analysieren einer Zeichenfolge, von der erwartet wird,
dass sie führende numerische Zeichen (einschließlich Hexadezimalzeichen) und nachstehende nicht numerische
Zeichen enthält. Vor dem Aufruf der TryParse-Methode werden gültige Zeichen vom Anfang einer Zeichenfolge
einer neuen Zeichenfolge zugewiesen. Da die zu analysierenden Zeichenfolgen einige Zeichen enthalten, wird im
Beispiel die String.Concat-Methode aufgerufen, um einer neuen Zeichenfolge gültige Zeichen zuzuweisen. Für
eine umfangreichere Zeichenfolge kann stattdessen die StringBuilder-Klasse verwendet werden.

using System;

public static class StringConversion


{
public static void Main()
{
var str = " 10FFxxx";
string numericString = string.Empty;
foreach (var c in str)
{
// Check for numeric characters (hex in this case) or leading or trailing spaces.
if ((c >= '0' && c <= '9') || (char.ToUpperInvariant(c) >= 'A' && char.ToUpperInvariant(c) <=
'F') || c == ' ')
{
numericString = string.Concat(numericString, c.ToString());
}
else
{
break;
}
}

if (int.TryParse(numericString, System.Globalization.NumberStyles.HexNumber, null, out int i))


{
Console.WriteLine($"'{str}' --> '{numericString}' --> {i}");
}
// Output: ' 10FFxxx' --> ' 10FF' --> 4351

str = " -10FFXXX";


numericString = "";
foreach (char c in str)
{
// Check for numeric characters (0-9), a negative sign, or leading or trailing spaces.
if ((c >= '0' && c <= '9') || c == ' ' || c == '-')
{
numericString = string.Concat(numericString, c);
}
else
{
break;
}
}

if (int.TryParse(numericString, out int j))


{
Console.WriteLine($"'{str}' --> '{numericString}' --> {j}");
}
// Output: ' -10FFXXX' --> ' -10' --> -10
}
}

Aufrufen von Convert-Methoden


In der folgenden Tabelle werden einige der Methoden aus der Convert-Klasse aufgelistet, die Sie zum
Konvertieren einer Zeichenfolge in eine Zahl verwenden können.

N UM ERISC H ER T Y P M ET H O DE

decimal ToDecimal(String)
N UM ERISC H ER T Y P M ET H O DE

float ToSingle(String)

double ToDouble(String)

short ToInt16(String)

int ToInt32(String)

long ToInt64(String)

ushort ToUInt16(String)

uint ToUInt32(String)

ulong ToUInt64(String)

In diesem Beispiel wird die Convert.ToInt32(String)-Methode aufgerufen, um eine Eingabezeichenfolge in einen


int-Typ zu konvertieren. Das Beispiel fängt die zwei häufigsten Ausnahmen ab, die von dieser Methode
ausgelöst werden können: FormatException und OverflowException. Wenn die resultierende Zahl schrittweise
erhöht werden kann, ohne Int32.MaxValue zu überschreiten, fügt der Beispielcode dem Ergebnis 1 hinzu und
zeigt die Ausgabe an.
using System;

public class ConvertStringExample1


{
static void Main(string[] args)
{
int numVal = -1;
bool repeat = true;

while (repeat)
{
Console.Write("Enter a number between −2,147,483,648 and +2,147,483,647 (inclusive): ");

string input = Console.ReadLine();

// ToInt32 can throw FormatException or OverflowException.


try
{
numVal = Convert.ToInt32(input);
if (numVal < Int32.MaxValue)
{
Console.WriteLine("The new value is {0}", ++numVal);
}
else
{
Console.WriteLine("numVal cannot be incremented beyond its current value");
}
}
catch (FormatException)
{
Console.WriteLine("Input string is not a sequence of digits.");
}
catch (OverflowException)
{
Console.WriteLine("The number cannot fit in an Int32.");
}

Console.Write("Go again? Y/N: ");


string go = Console.ReadLine();
if (go.ToUpper() != "Y")
{
repeat = false;
}
}
}
}
// Sample Output:
// Enter a number between -2,147,483,648 and +2,147,483,647 (inclusive): 473
// The new value is 474
// Go again? Y/N: y
// Enter a number between -2,147,483,648 and +2,147,483,647 (inclusive): 2147483647
// numVal cannot be incremented beyond its current value
// Go again? Y/N: y
// Enter a number between -2,147,483,648 and +2,147,483,647 (inclusive): -1000
// The new value is -999
// Go again? Y/N: n
Vorgehensweise: Konvertieren zwischen
Hexadezimalzeichenfolgen und numerischen Typen
(C#-Programmierleitfaden)
04.11.2021 • 3 minutes to read

In diesen Beispielen wird gezeigt, wie Sie die folgenden Aufgaben ausführen:
Abrufen des Hexadezimalwerts jedes Zeichens in einer Zeichenfolge
Abrufen des char, das jedem Wert in einer Hexadezimalzeichenfolge entspricht
Konvertieren eines hexadezimalen string in int
Konvertieren eines hexadezimalen string in float
Konvertieren eines Byte-Arrays in einen hexadezimalen string

Beispiele
In diesem Beispiel wird der Hexadezimalwert jedes Zeichens in einem string ausgegeben. Zuerst wird der
string in ein Array von Zeichen aufgegliedert. Danach wird ToInt32(Char) auf jedem Zeichen aufgerufen, um
dessen numerischen Wert zu erhalten. Abschließend wird die Zahl als Hexadezimaldarstellung in einem string
formatiert.

string input = "Hello World!";


char[] values = input.ToCharArray();
foreach (char letter in values)
{
// Get the integral value of the character.
int value = Convert.ToInt32(letter);
// Convert the integer value to a hexadecimal value in string form.
Console.WriteLine($"Hexadecimal value of {letter} is {value:X}");
}
/* Output:
Hexadecimal value of H is 48
Hexadecimal value of e is 65
Hexadecimal value of l is 6C
Hexadecimal value of l is 6C
Hexadecimal value of o is 6F
Hexadecimal value of is 20
Hexadecimal value of W is 57
Hexadecimal value of o is 6F
Hexadecimal value of r is 72
Hexadecimal value of l is 6C
Hexadecimal value of d is 64
Hexadecimal value of ! is 21
*/

In diesem Beispiel wird ein string von Hexadezimalwerten analysiert, und die den einzelnen
Hexadezimalwerten entsprechenden Zeichen werden ausgegeben. Zuerst wird die Split(Char[])-Methode
aufgerufen, um jeden Hexadezimalwert als einzelnen string in einem Array abzurufen. Anschließend wird
ToInt32(String, Int32) aufgerufen, damit der Hexadezimalwert in einen Dezimalwert konvertiert, der als int
dargestellt wird. Es werden zwei verschiedene Arten gezeigt, um das diesem Zeichencode entsprechende
Zeichen abzurufen. Die erste Methode verwendet ConvertFromUtf32(Int32), wodurch das Zeichen
zurückgegeben wird, das dem ganzzahligen Argument als string entspricht. Die zweite Methode wandelt int
explizit in ein char um.

string hexValues = "48 65 6C 6C 6F 20 57 6F 72 6C 64 21";


string[] hexValuesSplit = hexValues.Split(' ');
foreach (string hex in hexValuesSplit)
{
// Convert the number expressed in base-16 to an integer.
int value = Convert.ToInt32(hex, 16);
// Get the character corresponding to the integral value.
string stringValue = Char.ConvertFromUtf32(value);
char charValue = (char)value;
Console.WriteLine("hexadecimal value = {0}, int value = {1}, char value = {2} or {3}",
hex, value, stringValue, charValue);
}
/* Output:
hexadecimal value = 48, int value = 72, char value = H or H
hexadecimal value = 65, int value = 101, char value = e or e
hexadecimal value = 6C, int value = 108, char value = l or l
hexadecimal value = 6C, int value = 108, char value = l or l
hexadecimal value = 6F, int value = 111, char value = o or o
hexadecimal value = 20, int value = 32, char value = or
hexadecimal value = 57, int value = 87, char value = W or W
hexadecimal value = 6F, int value = 111, char value = o or o
hexadecimal value = 72, int value = 114, char value = r or r
hexadecimal value = 6C, int value = 108, char value = l or l
hexadecimal value = 64, int value = 100, char value = d or d
hexadecimal value = 21, int value = 33, char value = ! or !
*/

Diese Beispiel stellt eine andere Möglichkeit dar, eine hexadezimale string in einen Integer zu konvertieren,
indem die Parse(String, NumberStyles)-Methode aufgerufen wird.

string hexString = "8E2";


int num = Int32.Parse(hexString, System.Globalization.NumberStyles.HexNumber);
Console.WriteLine(num);
//Output: 2274

Das folgende Beispiel zeigt, wie eine hexadezimale string in float konvertiert wird, indem die
System.BitConverter-Klasse und die UInt32.Parse-Methode verwendet wird.

string hexString = "43480170";


uint num = uint.Parse(hexString, System.Globalization.NumberStyles.AllowHexSpecifier);

byte[] floatVals = BitConverter.GetBytes(num);


float f = BitConverter.ToSingle(floatVals, 0);
Console.WriteLine("float convert = {0}", f);

// Output: 200.0056

Das folgende Beispiel zeigt, wie Sie ein Byte-Array mithilfe der System.BitConverter-Klasse in eine hexadezimale
Zeichenfolge konvertieren.
byte[] vals = { 0x01, 0xAA, 0xB1, 0xDC, 0x10, 0xDD };

string str = BitConverter.ToString(vals);


Console.WriteLine(str);

str = BitConverter.ToString(vals).Replace("-", "");


Console.WriteLine(str);

/*Output:
01-AA-B1-DC-10-DD
01AAB1DC10DD
*/

Im folgenden Beispiel wird gezeigt, wie Sie ein Byte-Array in eine hexadezimale Zeichenfolge konvertieren,
indem Sie die in .NET 5.0 eingeführte Convert.ToHexString-Methode aufrufen.

byte[] array = { 0x64, 0x6f, 0x74, 0x63, 0x65, 0x74 };

string hexValue = Convert.ToHexString(array);


Console.WriteLine(hexValue);

/*Output:
646F74636574
*/

Siehe auch
Standardmäßige Zahlenformatzeichenfolgen
Typen
Bestimmen, ob eine Zeichenfolge einen numerischen Wert darstellt
Verwenden des Typs „dynamic“ (C#-
Programmierhandbuch)
04.11.2021 • 4 minutes to read

C# 4 führt einen neuen Typ ein: dynamic . Bei diesem Typ handelt es sich um einen statischen Typ. Ein Objekt des
Typs dynamic umgeht aber die Überprüfung statischer Typen. In den meisten Fällen entspricht es der
Funktionsweise des Typs object . Bei einem Element, das zur Kompilierzeit als dynamic typisiert wird, wird
davon ausgegangen, dass es alle Vorgänge unterstützt. Daher müssen Sie sich keine Gedanken darüber machen,
ob das Objekt seinen Wert von einer COM-API, einer dynamischen Sprache wie IronPython, vom HTML-DOM
(Document Object Model), aus der Reflektion oder von einer anderen Quelle im Programm erhält. Wenn der
Code jedoch nicht gültig ist, werden Fehler zur Laufzeit abgefangen.
Wenn z.B. die Instanzmethode exampleMethod1 im folgenden Code nur einen Parameter hat, erkennt der
Compiler, dass der erste Aufruf der Methode, ec.exampleMethod1(10, 4) , nicht gültig ist, da er zwei Argumente
enthält. Dieser Aufruf löst einen Compilerfehler aus. Der zweite Aufruf der Methode,
dynamic_ec.exampleMethod1(10, 4) , wird vom Compiler nicht überprüft, da der Typ von dynamic_ec``dynamic ist.
Daher wird kein Compilerfehler gemeldet. Allerdings bleibt der Fehler nicht unbegrenzt unbemerkt. Er wird zur
Laufzeit abgefangen und führt zu einer Laufzeitausnahme.

static void Main(string[] args)


{
ExampleClass ec = new ExampleClass();
// The following call to exampleMethod1 causes a compiler error
// if exampleMethod1 has only one parameter. Uncomment the line
// to see the error.
//ec.exampleMethod1(10, 4);

dynamic dynamic_ec = new ExampleClass();


// The following line is not identified as an error by the
// compiler, but it causes a run-time exception.
dynamic_ec.exampleMethod1(10, 4);

// The following calls also do not cause compiler errors, whether


// appropriate methods exist or not.
dynamic_ec.someMethod("some argument", 7, null);
dynamic_ec.nonexistentMethod();
}

class ExampleClass
{
public ExampleClass() { }
public ExampleClass(int v) { }

public void exampleMethod1(int i) { }

public void exampleMethod2(string str) { }


}

In diesen Beispielen ist es die Rolle des Compilers, Informationen darüber zusammenzustellen, was jede
Anweisung für die Behandlung des Objekts oder des Ausdrucks vorschlägt, die als dynamic typisiert sind. Die
gespeicherten Informationen werden zur Laufzeit überprüft, und jede ungültige Anweisung verursacht eine
Laufzeitausnahme.
Das Ergebnis der meisten dynamischen Vorgänge ist wiederum dynamic . Wenn Sie z.B. den Mauszeiger im
folgenden Beispiel auf die Verwendung von testSum halten, zeigt IntelliSense den Typ (local variable)
dynamic testSum an.

dynamic d = 1;
var testSum = d + 3;
// Rest the mouse pointer over testSum in the following statement.
System.Console.WriteLine(testSum);

Vorgänge, in denen das Ergebnis nicht dynamic lautet, sind z.B.:


Konvertierungen von dynamic in einen anderen Typ
Konstruktoraufrufe, die Argumente des Typs dynamic enthalten
In der folgenden Deklaration ist der Typ von testInstance z.B. ExampleClass , nicht dynamic :

var testInstance = new ExampleClass(d);

Im folgenden Abschnitt „Konvertierungen“ finden Sie Konvertierungsbeispiele.

Konvertierungen
Konvertierungen zwischen dynamischen Objekten und anderen Typen sind sehr einfach. Dadurch kann der
Entwickler zwischen dynamischem und nicht dynamischem Verhalten wechseln.
Jedes Objekt kann implizit zu einem dynamischen Typ konvertiert werden, wie in den folgenden Beispielen
gezeigt.

dynamic d1 = 7;
dynamic d2 = "a string";
dynamic d3 = System.DateTime.Today;
dynamic d4 = System.Diagnostics.Process.GetProcesses();

Umgekehrt kann eine implizite Konvertierung dynamisch auf einen Ausdruck vom Typ dynamic angewendet
werden.

int i = d1;
string str = d2;
DateTime dt = d3;
System.Diagnostics.Process[] procs = d4;

Überladungsauflösung mit Argumenten vom Typ „dynamic“


Überladungsauflösung erfolgt zur Laufzeit anstatt zur Kompilierzeit, wenn eines oder mehrere der Argumente in
einem Methodenaufruf vom Typ dynamic sind, oder wenn der Empfänger des Methodenaufrufs vom Typ
dynamic ist. Im folgenden Beispiel wird durch das Senden von d1 als Argument kein Compilerfehler ausgelöst,
wenn die einzige zugängliche exampleMethod2 -Methode so definiert wird, dass sie ein Zeichenfolgenargument
akzeptiert. Allerdings wird eine Laufzeitausnahme ausgelöst. Die Überladungsauflösung schlägt zur Laufzeit
fehl, da der Laufzeittyp von d1``int ist und exampleMethod2 eine Zeichenfolge benötigt.
// Valid.
ec.exampleMethod2("a string");

// The following statement does not cause a compiler error, even though ec is not
// dynamic. A run-time exception is raised because the run-time type of d1 is int.
ec.exampleMethod2(d1);
// The following statement does cause a compiler error.
//ec.exampleMethod2(7);

Dynamic Language Runtime (DLR)


Die Dynamic Language Runtime (DLR) ist eine API, die in .NET Framework 4 eingeführt wurde. Sie bietet die
Infrastruktur, die den Typ dynamic in C# und die Implementierung von dynamischen Programmiersprachen wie
IronPython und IronRuby unterstützt. Weitere Informationen zur DLR finden Sie unter Übersicht über die
Dynamic Language Runtime.

COM-Interop
C# 4 enthält mehrere Features, die die Interoperabilität mit COM-APIs wie den Automatisierungs-APIs in Office
verbessern. Zu diesen Verbesserungen gehören die Verwendung des Typs dynamic und von benannten und
optionalen Argumenten.
Viele COM-Methoden ermöglichen die Variation von Argument- und Rückgabetypen durch Festlegen der Typen
als object . Dadurch wird das explizite Umwandeln der Werte für die Koordination mit stark typisierten
Variablen in C# notwendig. Wenn Sie mit der Option EmbedInteropTypes (C#-Compileroptionen) kompilieren,
ermöglicht Ihnen die Einführung des Typs dynamic , die Vorkommen von object in COM-Signaturen so zu
behandeln, als wären sie vom Typ dynamic . Dadurch können Sie einen Großteil der Umwandlung vermeiden.
Die folgenden Anweisungen unterscheiden z.B., wie Sie auf eine Zelle in einem Arbeitsblatt von Microsoft Office
Excel mit dem Typ dynamic und ohne den Typ dynamic zugreifen.

// Before the introduction of dynamic.


((Excel.Range)excelApp.Cells[1, 1]).Value2 = "Name";
Excel.Range range2008 = (Excel.Range)excelApp.Cells[1, 1];

// After the introduction of dynamic, the access to the Value property and
// the conversion to Excel.Range are handled by the run-time COM binder.
excelApp.Cells[1, 1].Value = "Name";
Excel.Range range2010 = excelApp.Cells[1, 1];

Verwandte Themen
T IT EL B ESC H REIB UN G

dynamic Beschreibt die Verwendung des Schlüsselworts dynamic .

Übersicht über die Dynamic Language Runtime Bietet eine Übersicht über die Dynamic Language Runtime
(DLR), eine Laufzeitumgebung, die der Common Language
Runtime (CLR) eine Reihe von Diensten für dynamische
Sprachen hinzufügt.

Exemplarische Vorgehensweise: Erstellen und Verwenden Bietet eine ausführliche Anleitung zum Erstellen eines
von dynamischen Objekten benutzerdefinierten dynamischen Objekts und zum Erstellen
eines Projekts, das auf eine IronPython -Bibliothek zugreift.
T IT EL B ESC H REIB UN G

Zugreifen auf Office-Interop-Objekte mithilfe von Visual C#- Veranschaulicht, wie Sie ein Projekt erstellen, das benannte
Funktionen und optionale Argumente, den Typ dynamic und andere
Verbesserungen verwendet, die den Zugriff auf Office-API-
Objekte vereinfachen.
Exemplarische Vorgehensweise: Erstellen und
Verwenden von dynamischen Objekten (C# und
Visual Basic)
04.11.2021 • 12 minutes to read

Dynamische Objekte machen Member wie etwa Eigenschaften und Methoden zur Laufzeit anstatt zur
Kompilierzeit verfügbar. Dadurch können Sie Objekte erstellen, mit denen Sie mit Strukturen arbeiten können,
die keinem statischen Typ oder Format entsprechen. Sie können z.B. ein dynamisches Objekt verwenden, um auf
das HTML-DOM (Document Object Model) zu verweisen, das eine Kombination aus gültigen HTML-
Markupelementen und Attributen enthalten kann. Da jedes HTML-Dokument eindeutig ist, werden die Elemente
für ein bestimmtes HTML-Dokument zur Laufzeit bestimmt. Eine gängige Methode zum Verweisen eines
Attributes auf ein HTML-Element ist, den Namen des Attributs an die GetProperty -Methode des Elements
weiterzugeben. Rufen Sie zum Verweisen des id -Attributs auf das HTML-Element <div id="Div1"> zuerst
einen Verweis auf das <div> -Element ab, und verwenden Sie anschließend divElement.GetProperty("id") .
Wenn Sie ein dynamisches Objekt verwenden, können Sie auf das id -Attribut als divElement.id verweisen.
Dynamische Objekte bieten außerdem einfachen Zugriff auf dynamische Sprachen wie IronPython und
IronRuby. Sie können ein dynamisches Objekt verwenden, um sich auf ein dynamisches Skript zu beziehen, das
zur Laufzeit ausgeführt wird.
Sie verweisen auf ein dynamisches Objekt mithilfe der späten Bindung. Geben Sie in C# den Typ eines spät
gebundenen Objekts als dynamic an. Geben Sie in Visual Basic den Typ eines spät gebundenen Objekts als
Object an. Weitere Informationen finden Sie unter dynamic und Frühes und spätes Binden.

Sie können benutzerdefinierte dynamische Objekte erstellen, indem Sie die Klassen im System.Dynamic-
Namespace verwenden. Sie können z.B. ein ExpandoObject erstellen und die Member dieses Objekts zur
Laufzeit angeben. Sie können auch einen eigenen Typ erstellen, der die DynamicObject-Klasse erbt. Sie können
die Member dieser DynamicObject-Klasse anschließend außer Kraft setzen, um dynamische Funktionen zur
Laufzeit bereitzustellen.
Dieser Artikel enthält zwei voneinander unabhängige exemplarische Vorgehensweisen:
Erstellen Sie ein benutzerdefiniertes Objekt, das die Inhalte einer Textdatei als Eigenschaften eines Objekts
weitergibt.
Erstellen Sie ein Objekt, das die IronPython -Bibliothek verwendet.
Sie können wählen, ob Sie eine Vorgehensweise oder beide absolvieren möchten (dabei spielt die Reihenfolge
keine Rolle).

Voraussetzungen
Visual Studio 2019 Version 16.9 oder höher mit installierter Workload .NET Desktop-Entwicklung . Das
.NET 5.0 SDK wird automatisch installiert, wenn Sie diese Workload auswählen.
NOTE
Auf Ihrem Computer werden möglicherweise andere Namen oder Speicherorte für die Benutzeroberflächenelemente von
Visual Studio angezeigt als die in den folgenden Anweisungen aufgeführten. Diese Elemente sind von der jeweiligen Visual
Studio-Version und den verwendeten Einstellungen abhängig. Weitere Informationen finden Sie unter Personalisieren der
IDE.

Wenn Sie die zweite exemplarische Vorgehensweise absolvieren möchten, installieren Sie IronPython für
.NET. Wechseln Sie zur Downloadseite, um die neueste Version zu erhalten.

Erstellen eines benutzerdefinierten dynamischen Objekts


In der ersten exemplarischen Vorgehensweise wird ein benutzerdefiniertes dynamisches Objekt definiert, mit
dem der Inhalt einer Textdatei durchsucht wird. Mit einer dynamischen Eigenschaft wird der Text angegeben,
nach dem gesucht werden soll. Wenn aufrufender Code z.B. dynamicFile.Sample angibt, gibt die dynamische
Klasse eine generische Liste von Zeichenfolgen zurück, die alle Zeilen aus der Datei enthält, die mit „Sample“
anfangen. Die Groß- und Kleinschreibung wird bei der Suche nicht berücksichtigt. Die dynamische Klasse
unterstützt auch zwei optionale Argumente. Das erste Argument ist ein Enumerationswert einer Suchoption, der
angibt, dass die dynamische Klasse am Beginn, am Ende oder an einer beliebigen Stelle der Zeile nach
Übereinstimmungen suchen soll. Das zweite Argument gibt an, dass die dynamische Klasse führende und
nachfolgende Leerzeichen vor dem Suchvorgang aus jeder Zeile entfernen soll. Wenn aufrufender Code z.B.
dynamicFile.Sample(StringSearchOption.Contains) angibt, sucht die dynamische Klasse an einer beliebigen Stelle
in der Zeile nach „Sample“. Wenn der aufrufende Code
dynamicFile.Sample(StringSearchOption.StartsWith, false) angibt, sucht die dynamische Klasse am Beginn jeder
Zeile nach „Sample“ und entfernt keine führenden oder nachfolgenden Leerzeichen. Standardmäßig sucht die
dynamische Klasse nach Übereinstimmungen am Beginn jeder Zeile und entfernt führende oder nachfolgende
Leerzeichen.
So erstellen Sie eine benutzerdefinierte dynamische Klasse
1. Starten Sie Visual Studio.
2. Wählen Sie Neues Projekt erstellen aus.
3. Wählen Sie im Dialogfeld Neues Projekt erstellen die Option „C#“ oder „Visual Basic“ aus, klicken Sie
auf Konsolenanwendung und anschließend auf Weiter .
4. Geben Sie im Dialogfeld Neues Projekt konfigurieren für Projektname den Text DynamicSample ein,
und klicken Sie dann auf Weiter .
5. Wählen Sie im Dialogfeld Zusätzliche Informationen für Zielframework die Option .NET 5.0
(aktuell) aus, und klicken Sie dann auf Erstellen .
Das neue Projekt wird erstellt.
6. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt „DynamicSample“,
und klicken Sie anschließend auf Hinzufügen > Klasse . Geben Sie im Feld Name den Text
ReadOnlyFile ein, und klicken Sie dann auf Hinzufügen .

Eine neue Datei wird hinzugefügt, die die Klasse „ReadOnlyFile“ enthält.
7. Fügen Sie am Anfang der Datei ReadOnlyFile.cs oder ReadOnlyFile.vb den folgenden Code hinzu, um die
Namespaces System.IO und System.Dynamic zu importieren.

using System.IO;
using System.Dynamic;
Imports System.IO
Imports System.Dynamic

8. Das benutzerdefinierte dynamische Objekt verwendet einen Enumerationswert, um die Suchkriterien zu


bestimmen. Fügen Sie vor der class-Anweisung die folgende Enumerationsdefinition ein.

public enum StringSearchOption


{
StartsWith,
Contains,
EndsWith
}

Public Enum StringSearchOption


StartsWith
Contains
EndsWith
End Enum

9. Aktualisieren Sie die class-Anweisung wie im folgenden Beispiel, um die DynamicObject -Klasse zu erben.

class ReadOnlyFile : DynamicObject

Public Class ReadOnlyFile


Inherits DynamicObject

10. Fügen Sie den folgenden Code zur ReadOnlyFile -Klasse hinzu, um ein privates Feld für den Dateipfad
und einen Konstruktor für die ReadOnlyFile -Klasse zu definieren.

// Store the path to the file and the initial line count value.
private string p_filePath;

// Public constructor. Verify that file exists and store the path in
// the private variable.
public ReadOnlyFile(string filePath)
{
if (!File.Exists(filePath))
{
throw new Exception("File path does not exist.");
}

p_filePath = filePath;
}
' Store the path to the file and the initial line count value.
Private p_filePath As String

' Public constructor. Verify that file exists and store the path in
' the private variable.
Public Sub New(ByVal filePath As String)
If Not File.Exists(filePath) Then
Throw New Exception("File path does not exist.")
End If

p_filePath = filePath
End Sub

11. Fügen Sie die folgende GetPropertyValue -Methode zu der ReadOnlyFile -Klasse hinzu. Die
GetPropertyValue -Methode akzeptiert Suchkriterien als Eingabe und gibt die Zeilen aus einer Textdatei
zurück, die den Suchkriterien entsprechen. Die von der ReadOnlyFile -Klasse bereitgestellten Methoden
rufen die GetPropertyValue -Methode auf, um ihre entsprechenden Ergebnisse abzurufen.
public List<string> GetPropertyValue(string propertyName,
StringSearchOption StringSearchOption =
StringSearchOption.StartsWith,
bool trimSpaces = true)
{
StreamReader sr = null;
List<string> results = new List<string>();
string line = "";
string testLine = "";

try
{
sr = new StreamReader(p_filePath);

while (!sr.EndOfStream)
{
line = sr.ReadLine();

// Perform a case-insensitive search by using the specified search options.


testLine = line.ToUpper();
if (trimSpaces) { testLine = testLine.Trim(); }

switch (StringSearchOption)
{
case StringSearchOption.StartsWith:
if (testLine.StartsWith(propertyName.ToUpper())) { results.Add(line); }
break;
case StringSearchOption.Contains:
if (testLine.Contains(propertyName.ToUpper())) { results.Add(line); }
break;
case StringSearchOption.EndsWith:
if (testLine.EndsWith(propertyName.ToUpper())) { results.Add(line); }
break;
}
}
}
catch
{
// Trap any exception that occurs in reading the file and return null.
results = null;
}
finally
{
if (sr != null) {sr.Close();}
}

return results;
}
Public Function GetPropertyValue(ByVal propertyName As String,
Optional ByVal StringSearchOption As StringSearchOption =
StringSearchOption.StartsWith,
Optional ByVal trimSpaces As Boolean = True) As List(Of String)

Dim sr As StreamReader = Nothing


Dim results As New List(Of String)
Dim line = ""
Dim testLine = ""

Try
sr = New StreamReader(p_filePath)

While Not sr.EndOfStream


line = sr.ReadLine()

' Perform a case-insensitive search by using the specified search options.


testLine = UCase(line)
If trimSpaces Then testLine = Trim(testLine)

Select Case StringSearchOption


Case StringSearchOption.StartsWith
If testLine.StartsWith(UCase(propertyName)) Then results.Add(line)
Case StringSearchOption.Contains
If testLine.Contains(UCase(propertyName)) Then results.Add(line)
Case StringSearchOption.EndsWith
If testLine.EndsWith(UCase(propertyName)) Then results.Add(line)
End Select
End While
Catch
' Trap any exception that occurs in reading the file and return Nothing.
results = Nothing
Finally
If sr IsNot Nothing Then sr.Close()
End Try

Return results
End Function

12. Fügen Sie nach der GetPropertyValue -Methode den folgenden Code hinzu, um die TryGetMember-
Methode der DynamicObject-Klasse zu überschreiben. Die Methode TryGetMember wird aufgerufen,
wenn ein Member einer dynamischen Klasse angefordert wird und keine Argumente angegeben werden.
Das Argument binder enthält Informationen über den Member, auf den verwiesen wurde. Das
Argument result verweist auf das Ergebnis, das für den angegebenen Member zurückgegeben wird.
Die Methode TryGetMember gibt einen booleschen Wert zurück, der true zurückgibt, wenn der
angeforderte Member vorhanden ist; andernfalls wird false zurückgegeben.

// Implement the TryGetMember method of the DynamicObject class for dynamic member calls.
public override bool TryGetMember(GetMemberBinder binder,
out object result)
{
result = GetPropertyValue(binder.Name);
return result == null ? false : true;
}

' Implement the TryGetMember method of the DynamicObject class for dynamic member calls.
Public Overrides Function TryGetMember(ByVal binder As GetMemberBinder,
ByRef result As Object) As Boolean
result = GetPropertyValue(binder.Name)
Return If(result Is Nothing, False, True)
End Function
13. Fügen Sie nach der TryGetMember -Methode den folgenden Code hinzu, um die TryInvokeMember-
Methode der DynamicObject-Klasse zu überschreiben. Die TryInvokeMember-Methode wird aufgerufen,
wenn ein Mitglied einer dynamischen Klasse mit Argumenten angefordert wird. Das Argument binder
enthält Informationen über den Member, auf den verwiesen wurde. Das Argument result verweist auf
das Ergebnis, das für den angegebenen Member zurückgegeben wird. Das Argument args enthält ein
Array von Argumenten, die an den Member weitergegeben werden. Die Methode TryInvokeMember gibt
einen booleschen Wert zurück, der true zurückgibt, wenn der angeforderte Member vorhanden ist;
andernfalls wird false zurückgegeben.
Die benutzerdefinierte Version der Methode TryInvokeMember erwartet, dass es sich beim ersten
Argument um einen Wert der StringSearchOption -Enumeration handelt, die Sie in einem vorherigen
Schritt definiert haben. Die Methode TryInvokeMember erwartet, dass es sich beim zweiten Argument um
einen booleschen Wert handelt. Wenn es sich bei einem oder beiden Argumenten um gültige Werte
handelt, werden sie an die Methode GetPropertyValue zum Abrufen der Werte weitergegeben.

// Implement the TryInvokeMember method of the DynamicObject class for


// dynamic member calls that have arguments.
public override bool TryInvokeMember(InvokeMemberBinder binder,
object[] args,
out object result)
{
StringSearchOption StringSearchOption = StringSearchOption.StartsWith;
bool trimSpaces = true;

try
{
if (args.Length > 0) { StringSearchOption = (StringSearchOption)args[0]; }
}
catch
{
throw new ArgumentException("StringSearchOption argument must be a StringSearchOption enum
value.");
}

try
{
if (args.Length > 1) { trimSpaces = (bool)args[1]; }
}
catch
{
throw new ArgumentException("trimSpaces argument must be a Boolean value.");
}

result = GetPropertyValue(binder.Name, StringSearchOption, trimSpaces);

return result == null ? false : true;


}
' Implement the TryInvokeMember method of the DynamicObject class for
' dynamic member calls that have arguments.
Public Overrides Function TryInvokeMember(ByVal binder As InvokeMemberBinder,
ByVal args() As Object,
ByRef result As Object) As Boolean

Dim StringSearchOption As StringSearchOption = StringSearchOption.StartsWith


Dim trimSpaces = True

Try
If args.Length > 0 Then StringSearchOption = CType(args(0), StringSearchOption)
Catch
Throw New ArgumentException("StringSearchOption argument must be a StringSearchOption enum
value.")
End Try

Try
If args.Length > 1 Then trimSpaces = CType(args(1), Boolean)
Catch
Throw New ArgumentException("trimSpaces argument must be a Boolean value.")
End Try

result = GetPropertyValue(binder.Name, StringSearchOption, trimSpaces)

Return If(result Is Nothing, False, True)


End Function

14. Speichern und schließen Sie die Datei.


So erstellen Sie eine Beispieltextdatei
1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt „DynamicSample“,
und klicken Sie anschließend auf Hinzufügen > Neues Element . Wählen Sie im Bereich Installier te
Vorlagen Allgemein aus, und wählen Sie anschließend die Vorlage Textdatei aus. Übernehmen Sie im
Feld Name den Standardnamen der Datei TextFile1.txt, und klicken Sie dann auf Hinzufügen . Eine neue
Textdatei wird zum Projekt hinzugefügt.
2. Kopieren Sie den folgenden Text in die Datei TextFile1.txt.

List of customers and suppliers

Supplier: Lucerne Publishing (https://www.lucernepublishing.com/)


Customer: Preston, Chris
Customer: Hines, Patrick
Customer: Cameron, Maria
Supplier: Graphic Design Institute (https://www.graphicdesigninstitute.com/)
Supplier: Fabrikam, Inc. (https://www.fabrikam.com/)
Customer: Seubert, Roxanne
Supplier: Proseware, Inc. (http://www.proseware.com/)
Customer: Adolphi, Stephan
Customer: Koch, Paul

3. Speichern und schließen Sie die Datei.


So erstellen Sie eine Beispielanwendung, die das benutzerdefinierte dynamische Objekt enthält
1. Doppelklicken Sie im Projektmappen-Explorer auf die Datei Program.vb (wenn Sie Visual Basic
verwenden) oder auf die Datei Program.cs (wenn Sie Visual C# verwenden).
2. Fügen Sie der Main -Prozedur den folgenden Code hinzu, um eine Instanz der Klasse ReadOnlyFile für
die Datei TextFile1.txt zu erstellen. Der Code verwendet spätes Binden, um dynamische Member
aufzurufen und die Textzeilen abzurufen, die die Zeichenfolge „Customer “ (Kunde) enthalten.
dynamic rFile = new ReadOnlyFile(@"..\..\..\TextFile1.txt");
foreach (string line in rFile.Customer)
{
Console.WriteLine(line);
}
Console.WriteLine("----------------------------");
foreach (string line in rFile.Customer(StringSearchOption.Contains, true))
{
Console.WriteLine(line);
}

Dim rFile As Object = New ReadOnlyFile("..\..\..\TextFile1.txt")


For Each line In rFile.Customer
Console.WriteLine(line)
Next
Console.WriteLine("----------------------------")
For Each line In rFile.Customer(StringSearchOption.Contains, True)
Console.WriteLine(line)
Next

3. Speichern Sie die Datei, und drücken Sie STRG+F5, um die Anwendung zu erstellen und
auszuführen.

Aufrufen einer Bibliothek in einer dynamischen Sprache


In der folgenden exemplarischen Vorgehensweise wird ein Projekt erstellt, das auf eine Bibliothek zugreift, die in
der dynamischen Sprache IronPython geschrieben ist.
So erstellen Sie eine benutzerdefinierte dynamische Klasse
1. Klicken Sie in Visual Studio auf Datei > Neu > Projekt .
2. Wählen Sie im Dialogfeld Neues Projekt erstellen die Option „C#“ oder „Visual Basic“ aus, klicken Sie
auf Konsolenanwendung und anschließend auf Weiter .
3. Geben Sie im Dialogfeld Neues Projekt konfigurieren für Projektname den Text
DynamicIronPythonSample ein, und klicken Sie dann auf Weiter .

4. Wählen Sie im Dialogfeld Zusätzliche Informationen für Zielframework die Option .NET 5.0
(aktuell) aus, und klicken Sie dann auf Erstellen .
Das neue Projekt wird erstellt.
5. Installieren Sie das NuGet-Paket IronPython.
6. Wenn Sie Visual Basic verwenden, bearbeiten Sie die Datei Program.vb. Wenn Sie Visual C# verwenden,
bearbeiten Sie die Datei Program.cs.
7. Fügen Sie am Anfang der Datei den folgenden Code hinzu, um die Namespaces
Microsoft.Scripting.Hosting und IronPython.Hosting aus den IronPython-Bibliotheken zu importieren
sowie den Namespace System.Linq .

using System.Linq;
using Microsoft.Scripting.Hosting;
using IronPython.Hosting;
Imports Microsoft.Scripting.Hosting
Imports IronPython.Hosting
Imports System.Linq

8. Fügen Sie in der Main-Methode den folgenden Code zum Erstellen eines neuen
Microsoft.Scripting.Hosting.ScriptRuntime -Objekts zum Hosten der IronPython-Bibliotheken hinzu. Das
ScriptRuntime -Objekt lädt das IronPython-Bibliotheksmodul „random.py“.

// Set the current directory to the IronPython libraries.


System.IO.Directory.SetCurrentDirectory(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) +
@"\IronPython 2.7\Lib");

// Create an instance of the random.py IronPython library.


Console.WriteLine("Loading random.py");
ScriptRuntime py = Python.CreateRuntime();
dynamic random = py.UseFile("random.py");
Console.WriteLine("random.py loaded.");

' Set the current directory to the IronPython libraries.


System.IO.Directory.SetCurrentDirectory(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) &
"\IronPython 2.7\Lib")

' Create an instance of the random.py IronPython library.


Console.WriteLine("Loading random.py")
Dim py = Python.CreateRuntime()
Dim random As Object = py.UseFile("random.py")
Console.WriteLine("random.py loaded.")

9. Fügen Sie nach dem Code zum Laden des Moduls „random.py“ den folgenden Code zum Erstellen eines
Arrays von ganzen Zahlen hinzu. Das Array wird an die shuffle -Methode des Moduls „random.py“
übergeben, das die Werte im Array per Zufall sortiert.

// Initialize an enumerable set of integers.


int[] items = Enumerable.Range(1, 7).ToArray();

// Randomly shuffle the array of integers by using IronPython.


for (int i = 0; i < 5; i++)
{
random.shuffle(items);
foreach (int item in items)
{
Console.WriteLine(item);
}
Console.WriteLine("-------------------");
}
' Initialize an enumerable set of integers.
Dim items = Enumerable.Range(1, 7).ToArray()

' Randomly shuffle the array of integers by using IronPython.


For i = 0 To 4
random.shuffle(items)
For Each item In items
Console.WriteLine(item)
Next
Console.WriteLine("-------------------")
Next

10. Speichern Sie die Datei, und drücken Sie STRG+F5, um die Anwendung zu erstellen und
auszuführen.

Siehe auch
System.Dynamic
System.Dynamic.DynamicObject
Verwenden von dynamischen Typen
Frühes und spätes Binden
dynamic
Implementing Dynamic Interfaces (downloadable PDF from Microsoft TechNet) (Implementieren von
dynamischen Schnittstellen (PDF von Microsoft TechNet zum Download))
Versionsverwaltung mit den Schlüsselwörtern
"override" und "new" (C#-Programmierhandbuch)
04.11.2021 • 5 minutes to read

Die C#-Sprache wurde entwickelt, damit die Versionierung von base- (Basis-) und abgeleiteten Klassen in
unterschiedlichen Bibliotheken weiterentwickelt und die Abwärtskompatibilität aufrechterhalten werden kann.
Das bedeutet z.B., dass die Einführung eines neuen Members in einer Basisklasse mit demselben Name wie ein
Member in einer abgeleiteten Klasse von C# vollständig unterstützt wird und nicht zu unerwartetem Verhalten
führt. Das bedeutet auch, dass eine Klasse explizit angeben muss, ob eine Methode für das außer Kraft setzen
einer geerbten Methode vorgesehen ist, oder ob eine Methode eine neue Methode ist, die eine Methode mit
ähnlichem Namen verbirgt.
In C# können abgeleitete Klassen Methoden mit dem gleichen Namen wie Basisklassen-Methoden enthalten.
Wenn der Methode in der abgeleiteten Klasse nicht die Schlüsselwörter new oder override vorangestellt
sind, gibt der Compiler eine Warnung aus, und die Methode verhält sich, als ob das Schlüsselwort new
vorhanden wäre.
Wenn der Methode in der abgeleiteten Klasse das Schlüsselwort new vorangestellt ist, wird die Methode
als unabhängig von der Methode in der Basisklasse definiert.
Wenn der Methode in der abgeleiteten Klasse das Schlüsselwort override vorangestellt ist, rufen
Objekte der abgeleiteten Klasse diese Methode anstatt der Methode der Basisklasse auf.
Um das override -Schlüsselwort auf die Methode in der abgeleiteten Klasse anzuwenden, muss die
Basisklassenmethode virtuell definiert werden.
Die Methode der Basisklasse kann mithilfe des Schlüsselworts base aus der Basisklasse heraus
aufgerufen werden.
Die Schlüsselwörter override , virtual und new können auch auf Eigenschaften, Indexer und Ereignisse
angewendet werden.
Standardmäßig sind C#-Methoden nicht virtuell. Wenn eine Methode als virtuell deklariert wird, kann jede
Klasse, die die Methode erbt, ihre eigene Version implementieren. Um eine Methode in eine virtuelle Methode
zu transformieren, wird der Modifizierer virtual in der Methodendeklaration der Basisklasse verwendet. Die
abgeleitete Klasse kann anschließend die virtuelle Methode der Basisklasse mithilfe des Schlüsselworts
override überschreiben oder die virtuelle Methode in der Basisklasse mithilfe des Schlüsselworts new
verbergen. Wenn weder das Schlüsselwort override noch das Schlüsselwort new angegeben ist, gibt der
Compiler eine Warnung aus, und die Methode in der abgeleiteten Klasse verbirgt die Methode in der
Basisklasse.
Nehmen wir zur Veranschaulichung dieser Vorgehensweise für einen Moment an, dass die Firma A eine Klasse
mit dem Namen GraphicsClass erstellt, die Ihr Programm benutzt. Die folgende Datei ist GraphicsClass :

class GraphicsClass
{
public virtual void DrawLine() { }
public virtual void DrawPoint() { }
}

Ihr Unternehmen verwendet diese Klasse, und Sie verwenden sie zum Ableiten einer Klasse oder zum
Hinzufügen einer neuen Methode:

class YourDerivedGraphicsClass : GraphicsClass


{
public void DrawRectangle() { }
}

Ihre Anwendung wird ohne Probleme verwendet, bis Firma A eine neue Version von GraphicsClass herausgibt,
die dem folgenden Code ähnelt:

class GraphicsClass
{
public virtual void DrawLine() { }
public virtual void DrawPoint() { }
public virtual void DrawRectangle() { }
}

Die neue Version von GraphicsClass enthält jetzt eine Methode namens DrawRectangle . Anfänglich geschieht
nichts. Die neue Version ist immer noch binärkompatibel mit der alten Version. Jede Software, die Sie entwickelt
haben, funktioniert weiterhin, sogar wenn die neue Klasse auf diesen Computersystemen installiert ist.
Aufgrund vorhandener Aufrufe der Methode verweist DrawRectangle weiterhin auf Ihre Version in Ihrer
abgeleiteten Klasse.
Sobald Sie Ihre Anwendung aber mit der neuen Version von GraphicsClass neu kompilieren, erhalten Sie vom
Compiler eine Warnung, CS0108. Diese Warnung informiert Sie darüber, dass Sie das gewünschte Verhalten der
DrawRectangle -Methode in Ihrer Anwendung bestimmen müssen.

Wenn Sie möchten, dass Ihre Methode die neue Basisklassenmethode außer Kraft setzt, verwenden Sie das
Schlüsselwort override :

class YourDerivedGraphicsClass : GraphicsClass


{
public override void DrawRectangle() { }
}

Das Schlüsselwort override stellt sicher, dass alle Objekte, die von YourDerivedGraphicsClass abgeleitet sind,
die Version von DrawRectangle der abgeleiteten Klasse verwenden. Objekte, die von YourDerivedGraphicsClass
abgeleitet sind, können auf die Basisklassenversion von DrawRectangle mithilfe des base-Schlüsselworts
zugreifen:

base.DrawRectangle();

Wenn Sie nicht möchten, dass Ihre Methode die neue Basisklassenmethode außer Kraft setzt, gelten die
folgenden Überlegungen. Sie können Ihre Methode umbenennen, um Verwechslungen zwischen den beiden
Methoden zu vermeiden. Dies kann zeitaufwändig und fehleranfällig sein und ist in einigen Fällen einfach nicht
praktikabel. Wenn das Projekt aber relativ klein ist, können Sie die Refactoring-Optionen von Visual Studio
verwenden, um die Methode umzubenennen. Weitere Informationen finden Sie unter Refactoring von Klassen
und Typen (Klassen-Designer).
Alternativ können Sie die Warnung vermeiden, indem Sie in der Definition Ihrer abgeleiteten Klasse das
Schlüsselwort new verwenden:
class YourDerivedGraphicsClass : GraphicsClass
{
public new void DrawRectangle() { }
}

Mit dem Schlüsselwort new teilt der Compiler mit, dass Ihre Definition die Definition ausblendet, die in der
Basisklasse enthalten ist. Dies ist das Standardverhalten.

Überschreiben und Methodenauswahl


Wenn eine Methode in einer Klasse benannt wird, wählt der C#-Compiler die beste Methode zum Aufrufen aus,
wenn mehr als eine Methode mit dem Aufruf kompatibel ist, z.B. wenn es zwei Methoden mit dem gleichen
Namen und Parameter gibt, die mit dem übergebenen Parameter kompatibel sind. Die folgenden Methoden
wären kompatibel:

public class Derived : Base


{
public override void DoWork(int param) { }
public void DoWork(double param) { }
}

Wenn DoWork für eine Instanz von Derived aufgerufen wird, versucht der C#-Compiler zuerst, den Aufruf mit
den Versionen von DoWork kompatibel zu machen, die ursprünglich für Derived deklariert wurden. Override-
Methoden werden nicht als Methoden angesehen, die für eine Klasse deklariert sind. Stattdessen sind sie neue
Implementierungen einer Methode, die für eine Basisklasse deklariert wurde. Nur wenn der C#-Compiler keine
Übereinstimmung des Methodenaufrufs mit dem Aufruf einer ursprünglichen Methode in Derived feststellen
kann, versucht er, den Aufruf mit einer überschriebenen Methode mit dem gleichen Namen und kompatiblen
Parametern übereinzustimmen. Zum Beispiel:

int val = 5;
Derived d = new Derived();
d.DoWork(val); // Calls DoWork(double).

Da die Variable valimplizit in einen Double-Wert konvertiert werden kann, ruft der C#-Compiler
DoWork(double) anstelle von DoWork(int) auf. Es gibt zwei Möglichkeiten, das zu vermeiden. Vermeiden Sie
zuerst das Deklarieren neuer Methoden mit dem gleichen Namen wie virtuelle Methoden. Zweitens können Sie
den C#-Compiler anweisen, die virtuelle Methode aufzurufen, indem Sie eine Suche nach der Liste der
Basisklassenmethode durchführen lassen. Dies geschieht durch umwandeln der Instanz von Derived in Base .
Da es sich um eine virtuelle Methode handelt, wird die Implementierung von DoWork(int) auf Derived
aufgerufen. Zum Beispiel:

((Base)d).DoWork(val); // Calls DoWork(int) on Derived.

Weitere Beispiele für new und override finden Sie unter Wann müssen die Schlüsselwörter „override“ und
„new“ verwendet werden?.

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Methoden
Vererbung
Wann müssen die Schlüsselwörter "override" und
"new" verwendet werden? (C#-
Programmierhandbuch)
04.11.2021 • 10 minutes to read

In C# kann eine Methode in einer abgeleiteten Klasse den gleichen Namen wie eine Methode in einer
Basisklasse haben. Sie können mit den Schlüsselwörtern new und override festlegen, wie die Methoden
interagieren. Der override -Modifizierer erweitert die virtual -Methode der Basisklasse, und der new -
Modifizierer verbirgt eine zugängliche Methode der Basisklasse. Der Unterschied wird in den Beispielen in
diesem Thema veranschaulicht.
Deklarieren Sie in einer Konsolenanwendung die folgenden beiden Klassen: BaseClass und DerivedClass .
DerivedClass erbt von BaseClass .

class BaseClass
{
public void Method1()
{
Console.WriteLine("Base - Method1");
}
}

class DerivedClass : BaseClass


{
public void Method2()
{
Console.WriteLine("Derived - Method2");
}
}

Deklarieren Sie in der Main -Methode die Variablen bc , dc und bcdc .


bc ist vom Typ BaseClass , und sein Wert ist vom Typ BaseClass .
dc ist vom Typ DerivedClass , und sein Wert ist vom Typ DerivedClass .
bcdc ist vom Typ BaseClass , und sein Wert ist vom Typ DerivedClass . Auf diese Variable müssen Sie
achten.
Da bc und bcdc vom Typ BaseClass sind, können sie nur direkt auf Method1 zugreifen, es sei denn, sie
verwenden Umwandlungen. Die Variable dc kann sowohl auf Method1 als auch auf Method2 zugreifen. Diese
Beziehungen werden im folgenden Code gezeigt.
class Program
{
static void Main(string[] args)
{
BaseClass bc = new BaseClass();
DerivedClass dc = new DerivedClass();
BaseClass bcdc = new DerivedClass();

bc.Method1();
dc.Method1();
dc.Method2();
bcdc.Method1();
}
// Output:
// Base - Method1
// Base - Method1
// Derived - Method2
// Base - Method1
}

Fügen Sie als Nächstes die folgende Method2 -Methode in BaseClass hinzu. Die Signatur dieser Methode
entspricht der Signatur der Method2 -Methode in DerivedClass .

public void Method2()


{
Console.WriteLine("Base - Method2");
}

Weil BaseClass jetzt über eine Method2 -Methode verfügt, kann eine zweite aufrufende Anweisung für die
BaseClass -Variablen bc und bcdc hinzugefügt werden, wie in folgendem Code gezeigt.

bc.Method1();
bc.Method2();
dc.Method1();
dc.Method2();
bcdc.Method1();
bcdc.Method2();

Wenn Sie das Projekt erstellen, stellen sie fest, dass das Hinzufügen der Method2 -Methode in BaseClass zu
einer Warnmeldung führt. In der Warnmeldung steht, dass die Method2 -Methode in DerivedClass die Method2 -
Methode in BaseClass verbirgt. Es wird empfohlen, dass Sie das new -Schlüsselwort in der Method2 -Definition
verwenden, wenn Sie möchten, dass es zu diesem Ergebnis kommt. Alternativ können Sie eine der Method2 -
Methoden umbenennen, um die Warnung aufzulösen. Dies muss jedoch nicht immer praktikabel sein.
Führen Sie das Programm aus, bevor Sie new hinzufügen, um die Ausgabe der zusätzlichen aufrufenden
Anweisungen zu überprüfen. Die folgenden Ergebnisse werden angezeigt.

// Output:
// Base - Method1
// Base - Method2
// Base - Method1
// Derived - Method2
// Base - Method1
// Base - Method2

Das new -Schlüsselwort erhält die Beziehungen, die diese Ausgabe verursachen, aber es unterdrückt die
Warnmeldung. Die Variablen vom Typ BaseClass greifen weiterhin auf die Member von BaseClass zu, und die
Variable des Typs DerivedClass greift weiterhin zuerst auf Member in DerivedClass zu, und kümmert sich dann
um von BaseClass geerbte Member.
Fügen sie den new -Modifizierer in die Definition von Method2 in DerivedClass ein, wie in folgendem Code
gezeigt, um die Warnmeldung zu unterdrücken. Der Modifizierer kann vor oder hinter public eingefügt
werden.

public new void Method2()


{
Console.WriteLine("Derived - Method2");
}

Führen Sie das Programm erneut aus, um sicherzustellen, dass die Ausgabe gleich geblieben ist. Überprüfen Sie
außerdem, ob die Warnung weiterhin angezeigt wird. Wenn Sie new verwenden, bestätigen Sie, dass Ihnen
bewusst ist, dass der davon modifizierte Member einen Member verbirgt, der von der Basisklasse vererbt wird.
Weitere Informationen zum Verbergen von Namen durch die Vererbung finden Sie unter new-Modifizierer.
Fügen Sie die folgende Methode in DerivedClass ein, um dieses Verhalten den Auswirkungen durch das
Verwenden von override gegenüberzustellen. Der override -Modifizierer kann vor oder hinter public
eingefügt werden.

public override void Method1()


{
Console.WriteLine("Derived - Method1");
}

Fügen Sie der Definition von Method1 in BaseClass den virtual -Modifizierer hinzu. Der virtual -Modifizierer
kann vor oder hinter public eingefügt werden.

public virtual void Method1()


{
Console.WriteLine("Base - Method1");
}

Führen Sie das Projekt erneut aus. Achten Sie besonders auf die letzten beiden Zeilen der folgenden Ausgabe.

// Output:
// Base - Method1
// Base - Method2
// Derived - Method1
// Derived - Method2
// Derived - Method1
// Base - Method2

Durch den override -Modifizierer kann bcdc auf die in DerivedClass definierte Method1 -Methode zugreifen.
Normalerweise ist dies das gewünschte Verhalten in Vererbungshierarchien. Objekte sollten Werte aufweisen,
die aus der abgeleiteten Klasse erzeugt werden, um die Methoden, die in der abgeleiteten Klasse definiert sind,
verwenden zu können. Dieses Verhalten erzielen Sie, indem Sie override verwenden, um die Methode der
Basisklasse zu erweitern.
Der folgende Code umfasst das vollständige Beispiel.
using System;
using System.Text;

namespace OverrideAndNew
{
class Program
{
static void Main(string[] args)
{
BaseClass bc = new BaseClass();
DerivedClass dc = new DerivedClass();
BaseClass bcdc = new DerivedClass();

// The following two calls do what you would expect. They call
// the methods that are defined in BaseClass.
bc.Method1();
bc.Method2();
// Output:
// Base - Method1
// Base - Method2

// The following two calls do what you would expect. They call
// the methods that are defined in DerivedClass.
dc.Method1();
dc.Method2();
// Output:
// Derived - Method1
// Derived - Method2

// The following two calls produce different results, depending


// on whether override (Method1) or new (Method2) is used.
bcdc.Method1();
bcdc.Method2();
// Output:
// Derived - Method1
// Base - Method2
}
}

class BaseClass
{
public virtual void Method1()
{
Console.WriteLine("Base - Method1");
}

public virtual void Method2()


{
Console.WriteLine("Base - Method2");
}
}

class DerivedClass : BaseClass


{
public override void Method1()
{
Console.WriteLine("Derived - Method1");
}

public new void Method2()


{
Console.WriteLine("Derived - Method2");
}
}
}
Das folgende Beispiel veranschaulicht ähnliches Verhalten in unterschiedlichen Kontexten. Im Beispiel werden
drei Klassen definiert: eine Basisklasse mit dem Namen Car und zwei Klassen, die von dieser Basisklasse
abgeleitet werden, ConvertibleCar und Minivan . Die Basisklasse enthält eine DescribeCar -Methode. Die
Methode zeigt eine grundlegende Beschreibung eines Autos und ruft dann ShowDetails auf, um zusätzliche
Informationen bereitzustellen. Jede der drei Klassen definiert eine ShowDetails -Methode. Der new -Modifizierer
wird für die Definition von ShowDetails in der ConvertibleCar -Klasse verwendet. Der override -Modifizierer
wird für die Definition von ShowDetails in der Minivan -Klasse verwendet.

// Define the base class, Car. The class defines two methods,
// DescribeCar and ShowDetails. DescribeCar calls ShowDetails, and each derived
// class also defines a ShowDetails method. The example tests which version of
// ShowDetails is selected, the base class method or the derived class method.
class Car
{
public void DescribeCar()
{
System.Console.WriteLine("Four wheels and an engine.");
ShowDetails();
}

public virtual void ShowDetails()


{
System.Console.WriteLine("Standard transportation.");
}
}

// Define the derived classes.

// Class ConvertibleCar uses the new modifier to acknowledge that ShowDetails


// hides the base class method.
class ConvertibleCar : Car
{
public new void ShowDetails()
{
System.Console.WriteLine("A roof that opens up.");
}
}

// Class Minivan uses the override modifier to specify that ShowDetails


// extends the base class method.
class Minivan : Car
{
public override void ShowDetails()
{
System.Console.WriteLine("Carries seven people.");
}
}

In dem Beispiel wird geprüft, welche Version von ShowDetails aufgerufen wird. Die folgende Methode,
TestCars1 , deklariert eine Instanz jeder Klasse und ruft dann DescribeCar auf jeder Instanz auf.
public static void TestCars1()
{
System.Console.WriteLine("\nTestCars1");
System.Console.WriteLine("----------");

Car car1 = new Car();


car1.DescribeCar();
System.Console.WriteLine("----------");

// Notice the output from this test case. The new modifier is
// used in the definition of ShowDetails in the ConvertibleCar
// class.

ConvertibleCar car2 = new ConvertibleCar();


car2.DescribeCar();
System.Console.WriteLine("----------");

Minivan car3 = new Minivan();


car3.DescribeCar();
System.Console.WriteLine("----------");
}

TestCars1 erzeugt die folgende Ausgabe. Achten Sie besonders auf die Ergebnis für car2 , die wahrscheinlich
nicht dem entsprechen, was Sie erwartet haben. Der Typ des Objekts ist ConvertibleCar . Allerdings greift
DescribeCar nicht auf die Version von ShowDetails zu, die in der ConvertibleCar -Klasse definiert wurde, da
diese Methode mit dem new -Modifizierer und nicht dem override -Modifizierer deklariert wird. Deshalb zeigt
ein ConvertibleCar -Objekt die gleiche Beschreibung wie ein Car -Objekt an. Stellen Sie die Ergebnis von car3
gegenüber, das ein Minivan -Objekt ist. In diesem Fall setzt die ShowDetails -Methode, die in der Minivan -Klasse
deklariert wurde, die ShowDetails -Methode, die in der Car -Klasse deklariert wurde, außer Kraft, und es wird
eine Beschreibung eines Minivans angezeigt.

// TestCars1
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Carries seven people.
// ----------

TestCars2 erstellt eine Liste von Objekten des Typs Car . Die Werte der Objekte werden von den Klassen Car ,
ConvertibleCar und Minivan instanziiert. DescribeCar wird auf jedem Element in der Liste aufgerufen. Der
folgende Code veranschaulicht die Definition von TestCars2 .
public static void TestCars2()
{
System.Console.WriteLine("\nTestCars2");
System.Console.WriteLine("----------");

var cars = new List<Car> { new Car(), new ConvertibleCar(),


new Minivan() };

foreach (var car in cars)


{
car.DescribeCar();
System.Console.WriteLine("----------");
}
}

Die folgende Ausgabe wird angezeigt. Beachten Sie, dass sie der Ausgabe, die von TestCars1 angezeigt wird,
entspricht. Die ShowDetails -Methode der ConvertibleCar -Klasse wird nicht aufgerufen, unabhängig davon, ob
der Typ des Objekts ConvertibleCar , wie in TestCars1 , oder Car , wie in TestCars2 , ist. Umgekehrt ruft car3
in beiden Fällen die ShowDetails -Methode der Minivan -Klasse auf, egal, ob es vom Typ Minivan oder Car ist.

// TestCars2
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Carries seven people.
// ----------

Die Methoden TestCars3 und TestCars4 schließen das Beispiel ab. Diese Methoden rufen ShowDetails direkt
auf. Zuerst von Objekte, die mit den Typen ConvertibleCar und Minivan ( TestCars3 ) deklariert wurden, und
anschließend von Objekten, die mit dem Typ Car ( TestCars4 ) deklariert wurden. Der folgende Code definiert
diese beiden Methoden.

public static void TestCars3()


{
System.Console.WriteLine("\nTestCars3");
System.Console.WriteLine("----------");
ConvertibleCar car2 = new ConvertibleCar();
Minivan car3 = new Minivan();
car2.ShowDetails();
car3.ShowDetails();
}

public static void TestCars4()


{
System.Console.WriteLine("\nTestCars4");
System.Console.WriteLine("----------");
Car car2 = new ConvertibleCar();
Car car3 = new Minivan();
car2.ShowDetails();
car3.ShowDetails();
}

Die Methoden erzeugen folgende Ausgabe, die den Ergebnissen des ersten Beispiels in diesem Thema
entspricht.
// TestCars3
// ----------
// A roof that opens up.
// Carries seven people.

// TestCars4
// ----------
// Standard transportation.
// Carries seven people.

Der folgende Code veranschaulicht das vollständige Projekt und dessen Ausgabe.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace OverrideAndNew2
{
class Program
{
static void Main(string[] args)
{
// Declare objects of the derived classes and test which version
// of ShowDetails is run, base or derived.
TestCars1();

// Declare objects of the base class, instantiated with the


// derived classes, and repeat the tests.
TestCars2();

// Declare objects of the derived classes and call ShowDetails


// directly.
TestCars3();

// Declare objects of the base class, instantiated with the


// derived classes, and repeat the tests.
TestCars4();
}

public static void TestCars1()


{
System.Console.WriteLine("\nTestCars1");
System.Console.WriteLine("----------");

Car car1 = new Car();


car1.DescribeCar();
System.Console.WriteLine("----------");

// Notice the output from this test case. The new modifier is
// used in the definition of ShowDetails in the ConvertibleCar
// class.
ConvertibleCar car2 = new ConvertibleCar();
car2.DescribeCar();
System.Console.WriteLine("----------");

Minivan car3 = new Minivan();


car3.DescribeCar();
System.Console.WriteLine("----------");
}
// Output:
// TestCars1
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Carries seven people.
// ----------

public static void TestCars2()


{
System.Console.WriteLine("\nTestCars2");
System.Console.WriteLine("----------");

var cars = new List<Car> { new Car(), new ConvertibleCar(),


new Minivan() };

foreach (var car in cars)


{
car.DescribeCar();
System.Console.WriteLine("----------");
}
}
// Output:
// TestCars2
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Standard transportation.
// ----------
// Four wheels and an engine.
// Carries seven people.
// ----------

public static void TestCars3()


{
System.Console.WriteLine("\nTestCars3");
System.Console.WriteLine("----------");
ConvertibleCar car2 = new ConvertibleCar();
Minivan car3 = new Minivan();
car2.ShowDetails();
car3.ShowDetails();
}
// Output:
// TestCars3
// ----------
// A roof that opens up.
// Carries seven people.

public static void TestCars4()


{
System.Console.WriteLine("\nTestCars4");
System.Console.WriteLine("----------");
Car car2 = new ConvertibleCar();
Car car3 = new Minivan();
car2.ShowDetails();
car3.ShowDetails();
}
// Output:
// TestCars4
// ----------
// Standard transportation.
// Carries seven people.
}

// Define the base class, Car. The class defines two virtual methods,
// DescribeCar and ShowDetails. DescribeCar calls ShowDetails, and each derived
// class also defines a ShowDetails method. The example tests which version of
// ShowDetails is used, the base class method or the derived class method.
// ShowDetails is used, the base class method or the derived class method.
class Car
{
public virtual void DescribeCar()
{
System.Console.WriteLine("Four wheels and an engine.");
ShowDetails();
}

public virtual void ShowDetails()


{
System.Console.WriteLine("Standard transportation.");
}
}

// Define the derived classes.

// Class ConvertibleCar uses the new modifier to acknowledge that ShowDetails


// hides the base class method.
class ConvertibleCar : Car
{
public new void ShowDetails()
{
System.Console.WriteLine("A roof that opens up.");
}
}

// Class Minivan uses the override modifier to specify that ShowDetails


// extends the base class method.
class Minivan : Car
{
public override void ShowDetails()
{
System.Console.WriteLine("Carries seven people.");
}
}

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Versionsverwaltung mit den Schlüsselwörtern "override" und "new"
base
abstract
Gewusst wie: Überschreiben der ToString-Methode
(C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

In C# erben alle Klassen oder Strukturen implizit die Object-Klasse. Deshalb erhält jedes Objekt in C# die
ToString-Methode, die eine Zeichenfolgenrepräsentation dieses Objekts zurückgibt. Alle Variablen des Typs int
verfügen z.B über eine ToString -Methode, die es ihnen ermöglicht, ihren Inhalt als Zeichenfolge
zurückzugeben:

int x = 42;
string strx = x.ToString();
Console.WriteLine(strx);
// Output:
// 42

Wenn Sie eine benutzerdefinierte Klasse oder Struktur erstellen, sollten Sie die ToString-Methode außer Kraft
setzen, um Informationen zum Typ an den Clientcode bereitzustellen.
Weitere Informationen zum Verwenden von Formatzeichenfolgen und anderen Arten von benutzerdefinierten
Formaten mit der ToString -Methode finden Sie unter Formatierung von Typen.

IMPORTANT
Wenn Sie sich entschieden haben, welche Informationen Sie über diese Methode bereitstellen möchten, beziehen Sie auch
mit ein, ob Ihre Klasse oder Struktur von nicht vertrauenswürdigem Code verwendet wird. Achten Sie darauf, dass Sie
keine Informationen bereitstellen, die von böswilligem Code ausgenutzt werden könnten.

Außerkraftsetzen der ToString -Methode in Ihrer Klasse oder Struktur:


1. Deklarieren Sie eine ToString -Methode mit folgenden Modifizierern und Rückgabetypen:

public override string ToString(){}

2. Implementieren Sie die Methode, sodass sie eine Zeichenfolge zurückgibt.


Im folgenden Beispiel wird der Name der Klasse neben den für eine bestimmte Instanz der Klasse
spezifischen Daten zurückgegeben.

class Person
{
public string Name { get; set; }
public int Age { get; set; }

public override string ToString()


{
return "Person: " + Name + " " + Age;
}
}

Sie können die ToString -Methode auch wie im folgenden Beispiel gezeigt prüfen:
Person person = new Person { Name = "John", Age = 12 };
Console.WriteLine(person);
// Output:
// Person: John 12

Siehe auch
IFormattable
C#-Programmierhandbuch
Das C#-Typsystem
Zeichenfolgen
string
override
virtual
Formatierung von Typen
Member (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Klassen und Strukturen verfügen über Member, die ihre Daten und ihr Verhalten darstellen. Die Member einer
Klasse umfassen alle Member, die in dieser Klasse deklariert sind, sowie alle Member (mit Ausnahme von
Konstruktoren und Finalizer), die in den Klassen der Vererbungshierarchie deklariert sind. Private Member in
Basisklassen werden geerbt. Aus abgeleiteten Klassen kann jedoch nicht darauf zugegriffen werden.
In der folgenden Tabelle sind die Arten von Membern aufgeführt, die in einer Klasse oder Struktur enthalten sein
können:

M EM B ER B ESC H REIB UN G

Felder Felder sind im Gültigkeitsbereich einer Klasse deklarierte


Variablen. Ein Feld kann ein integrierter numerischer Typ
oder eine Instanz einer anderen Klasse sein. So kann zum
Beispiel eine Kalenderklasse über ein Feld verfügen, das das
aktuelle Datum enthält.

Konstanten Konstanten sind Felder, deren Wert bei der Kompilierung


festgelegt wird und nicht geändert werden kann.

Eigenschaften Eigenschaften sind Methoden einer Klasse, auf die


zugegriffen wird, als ob sie Felder dieser Klasse wären. Eine
Eigenschaft kann ein Klassenfeld davor schützen, ohne das
Wissen des Objekts geändert zu werden.

Methoden Methoden definieren die Aktionen, die von einer Klasse


ausgeführt werden können. Methoden können Parameter
entgegennehmen, die Eingabedaten bereitstellen, und
mithilfe von Parametern Ausgabedaten zurückgeben.
Methoden können auch direkt einen Wert zurückgeben,
ohne einen Parameter zu verwenden.

Ereignisse Ereignisse stellen für andere Objekte Benachrichtigungen


darüber bereit, dass bestimmte Vorgänge (z. B. das Klicken
auf eine Schaltfläche oder die erfolgreiche Beendigung einer
Methode) eingetreten sind. Ereignisse werden mithilfe von
Delegaten definiert und ausgelöst.

Operatoren Überladene Operatoren werden als Typmember betrachtet.


Wenn Sie einen Operator überladen, definieren Sie diesen als
öffentliche statische Methode in einem Typ. Weitere
Informationen finden Sie unter Operatorüberladung.

Indexer Indexer ermöglichen es einem Objekt, ähnlich wie ein Array


indiziert zu werden.

Konstruktoren Konstruktoren sind Methoden, die beim ersten Erstellen von


Objekten aufgerufen werden. Sie werden häufig verwendet,
um die Daten der Objekte zu initialisieren.
M EM B ER B ESC H REIB UN G

Finalizer Finalizer werden in C# sehr selten verwendet. Bei


Destruktoren handelt es sich um Methoden, die von der
Ausführungs-Engine der Laufzeit aufgerufen werden, wenn
das Objekt aus dem Speicher entfernt werden soll. In der
Regel werden sie verwendet, um sicherzustellen, dass
Ressourcen, die freigegeben werden müssen, angemessen
verarbeitet werden.

Geschachtelte Typen Geschachtelte Typen sind Typen, die in einem anderen Typ
deklariert sind. Geschachtelte Typen werden häufig
verwendet, um Objekte zu beschreiben, die nur von den
Typen verwendet werden, in denen sie enthalten sind.

Siehe auch
C#-Programmierhandbuch
Klassen
Abstrakte und versiegelte Klassen und
Klassenmember (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Das Schlüsselwort abstract ermöglicht die Erstellung von Klassen und Klassenmembern, die unvollständig sind
und in einer abgeleiteten Klasse implementiert werden müssen.
Mit dem Schlüsselwort sealed können Sie die Vererbung von Klassen oder bestimmten Klassenmembern
unterbinden, die zuvor als virtuell gekennzeichnet wurden.

Abstrakte Klassen und Klassenmember


Klassen können durch Festlegen des Schlüsselworts abstract vor der Klassendefinition als abstrakt deklariert
werden. Zum Beispiel:

public abstract class A


{
// Class members here.
}

Eine abstrakte Klasse darf nicht instanziiert werden. Der Zweck einer abstrakten Klasse ist die Bereitstellung
einer allgemeinen Definition einer Basisklasse, die für mehrere abgeleitete Klassen freigegeben ist. Eine
Klassenbibliothek kann beispielsweise eine abstrakte Klasse definieren, die als Parameter für ihre Funktionen
verwendet wird. Programmierer, die diese Bibliothek verwenden, benötigen ihre eigene Implementierung der
Klasse, die sie von ihr ableiten können.
Abstrakte Klassen können auch abstrakte Methoden definieren. Zu diesem Zweck wird das Schlüsselwort
abstract vor dem Rückgabetyp der Methode einfügt. Zum Beispiel:

public abstract class A


{
public abstract void DoWork(int i);
}

Abstrakte Methoden verfügen über keine Implementierung, deshalb folgt der Methodendefinition ein
Semikolon statt eines normalen Methodenblocks. Von der abstrakten Klasse abgeleitete Klassen müssen alle
abstrakten Methoden implementieren. Wenn eine abstrakte Klasse eine virtuelle Methode von einer Basisklasse
erbt, kann sie die virtuelle Methode mit einer abstrakten Methode überschreiben. Zum Beispiel:
// compile with: -target:library
public class D
{
public virtual void DoWork(int i)
{
// Original implementation.
}
}

public abstract class E : D


{
public abstract override void DoWork(int i);
}

public class F : E
{
public override void DoWork(int i)
{
// New implementation.
}
}

Wenn eine virtual -Methode als abstract deklariert ist, bleibt sie für alle Klassen virtuell, die von der
abstrakten Klasse erben. Eine Klasse, die eine abstrakte Methode erbt, kann nicht auf die ursprüngliche
Implementierung der Methode zugreifen. Im vorherigen Beispiel konnte DoWork auf Klasse F DoWork auf Klasse
D nicht aufrufen. So kann eine abstrakte Klasse abgeleitete Klassen dazu zwingen, neue
Methodenimplementierungen für virtuelle Methoden bereitzustellen.

Versiegelte Klassen und Klassenmember


Klassen können durch Festlegen des Schlüsselworts sealed vor der Klassendefinition als sealed deklariert
werden. Zum Beispiel:

public sealed class D


{
// Class members here.
}

Eine versiegelte Klasse kann nicht als Basisklasse verwendet werden. Aus diesem Grund kann sie auch keine
abstrakte Klasse sein. Von versiegelten Klassen kann nicht abgeleitet werden. Weil sie nicht als Basisklasse
verwendet werden können, können Aufrufe an versiegelte Klassenmember durch Laufzeitoptimierungen etwas
beschleunigt werden.
Methoden, Indexer, Eigenschaften oder Ereignisse einer abgeleiteten Klasse, die einen virtuellen Member der
Basisklasse überschreiben, können diesen Member als versiegelt deklarieren. Damit wird der virtuelle Aspekt
des Members für jede weitere abgeleitete Klasse aufgehoben. Dazu wird in die Klassenmemberdeklaration das
Schlüsselwort sealed vor dem Schlüsselwort override eingefügt. Beispiel:

public class D : C
{
public sealed override void DoWork() { }
}

Weitere Informationen
C#-Programmierhandbuch
Das C#-Typsystem
Vererbung
Methoden
Felder
Definieren von abstrakten Eigenschaften
Statische Klassen und statische Klassenmember (C#-
Programmierhandbuch)
04.11.2021 • 5 minutes to read

Eine statische Klasse ist im Grunde identisch mit einer nicht statischen Klasse, aber es gibt einen Unterschied:
Eine statische Klasse kann nicht instanziiert werden. Das heißt, Sie können der new-Operator nicht verwenden,
um eine Variable des Klassentyps zu erstellen. Da keine Instanzvariable vorhanden ist, greifen Sie auf die
Member einer statischen Klasse mit dem Klassennamen selbst zu. Wenn Sie z.B. eine statische Klasse dem
Namen UtilityClass haben, die eine öffentliche statische Methode mit dem Namen MethodA besitzt, rufen Sie
die Methode wie im folgenden Beispiel gezeigt auf:

UtilityClass.MethodA();

Eine statische Klasse kann als geeigneter Container für Reihen von Methoden verwendet werden, die nur
Eingabeparameter verarbeiten und keine internen Instanzfelder haben oder festlegen müssen. In der .NET
Framework-Klassenbibliothek enthält z. B. die statische Klasse System.Math Methoden, die mathematische
Operationen durchführen, ohne dass Daten gespeichert oder abgerufen werden müssen, die in einer
bestimmten Instanz der Klasse Math einzigartig sind. Sie wenden also die Member der Klasse an, indem der
Klassen- und Methodennamen angegeben wird, wie im folgenden Beispiel gezeigt wird.

double dub = -3.14;


Console.WriteLine(Math.Abs(dub));
Console.WriteLine(Math.Floor(dub));
Console.WriteLine(Math.Round(Math.Abs(dub)));

// Output:
// 3.14
// -4
// 3

Wie bei allen Klassentypen lädt die .NET-Runtime die Typinformationen für statische Klassen beim Laden des auf
die Klasse verweisenden Programms. Das Programm kann nicht genau angeben, wann die Klasse geladen wird.
Jedoch wird sichergestellt, dass es geladen wird, und dass seine Felder initialisiert sowie seine statischen
Konstruktoren aufgerufen sind, bevor zum ersten Mal auf die Klasse in Ihrem Programm verwiesen wird. Ein
statischer Konstruktor wird nur einmal aufgerufen, und eine statische Klasse verbleibt im Speicher für die
Lebensdauer der Anwendungsdomäne, in der sich das Programm befindet.

NOTE
Wie Sie eine nicht statische Klasse erstellen, die es erlaubt, dass nur eine Instanz von ihr selbst erstellt wird, finden Sie
unter Implementierung von Singleton in C#.

Die folgende Liste stellt die Haupteigenschaften einer statischen Klasse dar:
Enthält nur statische Member
Kann nicht instanziiert werden
Ist versiegelt
Darf keine Instanzkonstruktoren enthalten
Daher ist das Erstellen einer statischen Klasse grundsätzlich dasselbe wie das Erstellen einer Klasse, die nur
statische Member und einen privaten Konstruktor enthält. Ein privater Konstruktor verhindert, dass die Klasse
instanziiert wird. Der Vorteil bei der Verwendung einer statischen Klasse ist, dass der Compiler überprüfen und
dadurch sicherstellen kann, dass keine Instanzmember versehentlich hinzugefügt werden. Der Compiler
garantiert, dass Instanzen dieser Klasse nicht erstellt werden können.
Statische Klassen sind versiegelt und können nicht vererbt werden. Sie können nur von der Klasse Object erben.
Statische Klassen können keinen Instanzkonstruktor enthalten. Sie können jedoch einen statischen Konstruktor
enthalten. Nicht statische Klassen sollten auch einen statischen Konstruktor definieren, wenn die Klasse statische
Member enthält, die eine nicht triviale Initialisierung erfordern. Weitere Informationen finden Sie unter Statische
Konstruktoren.

Beispiel
Dies ist ein Beispiel für eine statische Klasse, die zwei Methoden enthält, die die Temperatur von Grad Celsius in
Fahrenheit und umgekehrt konvertieren:

public static class TemperatureConverter


{
public static double CelsiusToFahrenheit(string temperatureCelsius)
{
// Convert argument to double for calculations.
double celsius = Double.Parse(temperatureCelsius);

// Convert Celsius to Fahrenheit.


double fahrenheit = (celsius * 9 / 5) + 32;

return fahrenheit;
}

public static double FahrenheitToCelsius(string temperatureFahrenheit)


{
// Convert argument to double for calculations.
double fahrenheit = Double.Parse(temperatureFahrenheit);

// Convert Fahrenheit to Celsius.


double celsius = (fahrenheit - 32) * 5 / 9;

return celsius;
}
}

class TestTemperatureConverter
{
static void Main()
{
Console.WriteLine("Please select the convertor direction");
Console.WriteLine("1. From Celsius to Fahrenheit.");
Console.WriteLine("2. From Fahrenheit to Celsius.");
Console.Write(":");

string selection = Console.ReadLine();


double F, C = 0;

switch (selection)
{
case "1":
Console.Write("Please enter the Celsius temperature: ");
F = TemperatureConverter.CelsiusToFahrenheit(Console.ReadLine());
Console.WriteLine("Temperature in Fahrenheit: {0:F2}", F);
break;
case "2":
Console.Write("Please enter the Fahrenheit temperature: ");
C = TemperatureConverter.FahrenheitToCelsius(Console.ReadLine());
Console.WriteLine("Temperature in Celsius: {0:F2}", C);
break;

default:
Console.WriteLine("Please select a convertor.");
break;
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Example Output:
Please select the convertor direction
1. From Celsius to Fahrenheit.
2. From Fahrenheit to Celsius.
:2
Please enter the Fahrenheit temperature: 20
Temperature in Celsius: -6.67
Press any key to exit.
*/

Statische Member
Eine nicht statische Klasse kann statische Methoden, Felder, Eigenschaften oder Ereignisse enthalten. Der
statische Member ist für eine Klasse aufrufbar, selbst wenn keine Instanz der Klasse erstellt wurde. Auf den
statischen Member wird immer vom Klassennamen, nicht vom Namen der Instanz, zugegriffen. Es ist nur eine
Kopie eines statischen Members vorhanden, unabhängig davon, wie viele Instanzen der Klasse erstellt werden.
Statische Methoden und Eigenschaften können nicht auf nicht statische Felder und Ereignisse in ihrem
enthaltenden Typ zugreifen. Zudem können sie nicht auf eine Instanzvariable für ein beliebiges Objekt zugreifen,
sofern es nicht ausdrücklich in einem Methodenparameter übergeben wird.
Es ist üblicher, eine nicht statische Klasse mit einigen statischen Membern zu deklarieren, statt eine ganze Klasse
als statisch zu deklarieren. Zwei häufige Verwendungen von statischen Feldern bestehen daraus, dass die Anzahl
von instanziierten Objekten mitgezählt wird, oder dass ein Wert gespeichert wird, der für alle Instanzen
freigegeben werden muss.
Statische Methoden können überladen, aber nicht überschrieben werden, da sie zu der Klasse gehören und nicht
zu einer Instanz der Klasse.
Obwohl ein Feld nicht als static const deklariert werden kann, ist ein const-Feld in seinem Verhalten
grundsätzlich statisch. Es gehört zum Typ und nicht zu Instanzen des Typs. Daher kann auf const -Felder
mithilfe der gleichen ClassName.MemberName -Notation zugegriffen werden, die für statische Felder verwendet
wird. Es wird keine Objektinstanz benötigt.
C# unterstützt keine statischen lokalen Variablen (d. h. Variablen, die im Methodenbereich deklariert werden).
Deklarieren Sie statische Klassenmember mithilfe des Schlüsselworts static vor dem Rückgabetyp des
Members, wie im folgenden Beispiel gezeigt wird:
public class Automobile
{
public static int NumberOfWheels = 4;

public static int SizeOfGasTank


{
get
{
return 15;
}
}

public static void Drive() { }

public static event EventType RunOutOfGas;

// Other non-static fields and properties...


}

Statische Member werden initialisiert, bevor auf die statischen Member zum ersten Mal zugegriffen wird, und
bevor der statische Konstruktor, sofern vorhanden, aufgerufen wird. Verwenden Sie zum Angeben des
Speicherorts des Members den Namen der Klasse anstelle des Variablennamens, um auf einen statischen
Klassenmember zuzugreifen, wie im folgenden Beispiel gezeigt wird:

Automobile.Drive();
int i = Automobile.NumberOfWheels;

Wenn Ihre Klasse statische Felder enthält, stellen Sie einen statischen Konstruktor bereit, der sie beim Laden der
Klasse initialisiert.
Ein Aufruf einer statischen Methode erzeugt eine Aufrufanweisung in Microsoft Intermediate Language (MSIL),
während ein Aufruf einer Instanzmethode eine callvirt -Anweisung erzeugt, die auch auf Verweise auf ein
NULL-Objekt überprüft. Jedoch ist in den meisten Fällen der Leistungsunterschied zwischen den beiden nicht
bedeutend genug.

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter Statische Klassen und Statische und Instanzmember in der C#-
Sprachspezifikation. Die Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
static
Klassen
class
Statische Konstruktoren
Instanzkonstruktoren
Zugriffsmodifizierer (C#-Programmierhandbuch)
04.11.2021 • 4 minutes to read

Alle Typen und Typmember verfügen über eine Zugriffsebene. Diese Zugriffsebene steuert, ob sie von anderem
Code in Ihrer Assembly oder anderen Assemblys verwendet werden können. Mithilfe der folgenden
Zugriffsmodifizierer können Sie den Zugriff auf einen Typ oder Member festlegen, wenn Sie ihn deklarieren:
public: Auf den Typ oder Member kann von jedem Code in der gleichen Assembly oder einer anderen
Assembly, die darauf verweist, zugegriffen werden. Die Zugriffsebene öffentlicher Member eines Typs wird
durch die Zugriffsebene des Typs selbst gesteuert.
private: Der Zugriff auf den Typ oder Member kann nur über Code innerhalb derselben class - oder struct
-Instanz erfolgen.
protected: Der Zugriff auf den Typ oder Member kann nur über Code in derselben class - oder in einer
class -Instanz erfolgen, die von dieser class -Instanz abgeleitet wurde.
internal: Auf den Typ oder Member kann von jedem Code in der gleichen Assembly zugegriffen werden,
jedoch nicht von Code in einer anderen Assembly.
protected internal: Der Zugriff auf den Typ oder Member kann über beliebigen Code in der Assembly, in der
er deklariert wurde, oder innerhalb einer abgeleiteten class -Instanz in einer anderen Assembly erfolgen.
private protected: Auf den Typ oder Member kann nur innerhalb der deklarierenden Assembly, von Code in
derselben class -Instanz oder in einem Typ zugegriffen werden, der von dieser class -Instanz abgeleitet ist.

Zusammenfassungstabelle
STA N DO RT
DES PROTECTED PRIVATE
A UF RUF ERS PUBLIC INTERNAL PROTECTED INTERNAL PROTECTED PRIVATE

Innerhalb der ️
✔ ️
✔ ️
✔ ️
✔ ️
✔ ️

Klasse

Abgeleitete ️
✔ ️
✔ ️
✔ ️
✔ ️
✔ ❌
Klasse (selbe
Assembly)

Nicht ️
✔ ️
✔ ❌ ️
✔ ❌ ❌
abgeleitete
Klasse (selbe
Assembly)

Abgeleitete ️
✔ ️
✔ ️
✔ ❌ ❌ ❌
Klasse
(andere
Assembly)

Nicht ️
✔ ❌ ❌ ❌ ❌ ❌
abgeleitete
Klasse
(andere
Assembly)

Die folgenden Beispiele veranschaulichen, wie Zugriffsmodifizierer für einen Typ und Member angegeben
werden:
public class Bicycle
{
public void Pedal() { }
}

Nicht alle Zugriffsmodifizierer sind für alle Typen oder Member in allen Kontexten gültig. In manchen Fällen wird
der Zugriff auf ein Typmember durch den Zugriff des enthaltenden Typs eingeschränkt.

Zugriff auf Klassen, Datensätze und Strukturen


Klassen, Datensätze und Strukturen, die direkt innerhalb eines Namespace deklariert werden (also nicht in
anderen Klassen oder Strukturen geschachtelt sind), können die Zugriffsebene public oder internal
aufweisen. Wenn kein Zugriffsmodifizierer angegeben ist, wird standardmäßig internal verwendet.
Strukturmember, einschließlich geschachtelter Klassen und Strukturen, können als public , internal oder
private deklariert werden. Für Klassenmember, einschließlich geschachtelter Klassen und Strukturen, kann
public , protected internal , protected , internal , private protected oder private deklariert werden.
Klassen und Strukturmember, einschließlich geschachtelter Klassen und Strukturen, weisen standardmäßig
private -Zugriff auf. Auf private geschachtelte Typen kann nicht von außerhalb des enthaltenden Typs
zugegriffen werden.
Abgeleitete Klassen und Datensätze können keine höhere Zugänglichkeit als ihre Basistypen aufweisen. Sie
können keine öffentliche B -Klasse deklarieren, die von einer internen A -Klasse abgeleitet wird. Wenn dies
zulässig wäre, würde A als öffentlich festgelegt werden, da der Zugriff auf alle protected - oder internal -
Member von A über die abgeleitete Klasse möglich wäre.
Mithilfe von InternalsVisibleToAttribute können Sie den Zugriff auf Ihre internen Typen durch spezifische
andere Assemblys ermöglichen. Weitere Informationen finden Sie unter Friend-Assemblys.

Zugriff auf Klassen-, Datensatz- und Strukturmember


Klassen- und Datensatzmember (einschließlich geschachtelter Klassen, Datensätze und Strukturen) können mit
einem der sechs Zugriffstypen deklariert werden. Strukturmember können nicht als protected ,
protected internal oder private protected deklariert werden, da Strukturen die Vererbung nicht unterstützen.

Normalerweise ist der Zugriff auf einen Member nicht höher als der Zugriff des Typs, der ihn enthält. Allerdings
kann auf einen public -Member einer internen Klasse möglicherweise von außerhalb der Assembly zugegriffen
werden, wenn der Member Schnittstellenmethoden implementiert oder virtuelle Methoden überschreibt, die in
einer öffentlichen Basisklasse definiert sind.
Der Typ aller Memberfelder, -eigenschaften oder -ereignisse muss mindestens über den Member selbst
zugänglich sein. Ebenso müssen der Rückgabetyp und die Parametertypen von Methoden, Indexer oder
Delegaten mindestens so zugänglich wie der Member selbst sein. Sie können beispielsweise keine M -Methode
mit der Zugriffsebene public verwenden, die eine C -Klasse zurückgibt, es sei denn, C verfügt ebenfalls über
die Zugriffsebene public . Ebenso können Sie keine protected -Eigenschaft vom Typ A verwenden, wenn A
als private deklariert ist.
Benutzerdefinierte Operatoren müssen immer als public und static deklariert werden. Weitere
Informationen finden Sie unter Operatorüberladung.
Finalizer können nicht über Zugriffsmodifizierer verfügen.
Fügen Sie der Memberdeklaration wie im folgenden Beispiel gezeigt das entsprechende Schlüsselwort hinzu,
um die Zugriffsebene für einen class -, record - oder struct -Member festzulegen.
// public class:
public class Tricycle
{
// protected method:
protected void Pedal() { }

// private field:
private int wheels = 3;

// protected internal property:


protected internal int Wheels
{
get { return wheels; }
}
}

Andere Typen
Schnittstellen, die direkt in einem Namespace deklariert wurden, können die Zugriffsebene public oder
internal aufweisen. Außerdem verfügen Schnittstellen wie Klassen und Strukturen standardmäßig über
internal -Zugriff. Für Schnittstellenmember ist standardmäßig public festgelegt, da es der Zweck einer
Schnittstelle ist, anderen Typen den Zugriff auf eine Klasse oder Struktur zu ermöglichen.
Schnittstellenmemberdeklarationen können einen beliebigen Zugriffsmodifizierer aufweisen. Das ist für
statische Methoden besonders nützlich, um allgemeine Implementierungen bereitzustellen, die von allen
Implementierern einer Klasse benötigt werden.
Enumerationsmember weisen immer die Zugriffsebene public auf, und es können keine Zugriffsmodifizierer
angewendet werden.
Delegaten verhalten sich wie Klassen und Strukturen. Sie verfügen standardmäßig über internal -Zugriff, wenn
sie in einem Namespace deklariert wurden, sie verfügen über private -Zugriff, wenn sie geschachtelt sind.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Schnittstellen
private
public
internal
protected
protected internal
private protected
class
struct
interface
Felder (C#-Programmierhandbuch)
04.11.2021 • 3 minutes to read

Ein Feld ist eine Variable eines beliebigen Typs, die direkt in einer class oder struct deklariert ist. Felder sind
Member Ihres enthaltenden Typs.
Eine Klasse oder Struktur kann über Instanzfelder, statische Felder oder beides verfügen. Instanzenfelder sind für
eine Instanz eines Typs spezifisch. Wenn Sie eine Klasse T mit einem Instanzenfeld F haben, können Sie zwei
Objekte vom Typ T erstellen und den Wert von F in jedem Objekt ändern, ohne dabei den Wert in dem anderen
Objekt zu beeinflussen. Im Gegensatz dazu gehört ein statisches Feld zum Typ selbst und wird in allen Instanzen
dieses Typs gemeinsam verwendet. Sie können nur auf das statische Feld zugreifen, indem Sie den Typnamen
verwenden. Wenn Sie über einen Instanznamen auf das statische Feld zugreifen, erhalten Sie den
Kompilierzeitfehler CS0176.
Im Allgemeinen sollten Sie Felder nur für Variablen verwenden, die private oder geschützte
Zugriffsmöglichkeiten haben. Daten, die Ihr Typ für Clientcode bereitstellt, sollten über Methoden, Eigenschaften
und Indexer bereitgestellt werden. Sie können sich mit diesen Konstrukten für indirekten Zugriff auf interne
Felder vor ungültigen Eingabewerten schützen. Ein privates Feld, das über eine öffentliche Eigenschaft
bereitgestellte Daten speichert, wird als Sicherungsspeicher oder Unterstützungsfeld bezeichnet.
Felder speichern in der Regel die Daten, die über Zugriff auf mehrere Typmethodem verfügen müssen und
länger als die Lebensdauer einer einzelnen Methode gespeichert werden müssen. Beispielsweise verfügt ein Typ,
der ein Kalenderdatum darstellt, möglicherweise über drei Felder: Jeweils eines für Monat, Tag und Jahr.
Variablen, die außerhalb des Bereichs einer einzelnen Methode nicht verwendet werden, sollten als lokale
Variablen innerhalb des Methodentexts selbst deklariert werden.
Felder werden innerhalb des Klassen- oder Strukturblocks deklariert, indem Sie die Zugriffsebene des Felds,
gefolgt vom Typ des Felds, gefolgt vom Namen des Felds angeben. Beispiel:
public class CalendarEntry
{

// private field (Located near wrapping "Date" property).


private DateTime _date;

// Public property exposes _date field safely.


public DateTime Date
{
get
{
return _date;
}
set
{
// Set some reasonable boundaries for likely birth dates.
if (value.Year > 1900 && value.Year <= DateTime.Today.Year)
{
_date = value;
}
else
{
throw new ArgumentOutOfRangeException();
}
}
}

// public field (Generally not recommended).


public string Day;

// Public method also exposes _date field safely.


// Example call: birthday.SetDate("1975, 6, 30");
public void SetDate(string dateString)
{
DateTime dt = Convert.ToDateTime(dateString);

// Set some reasonable boundaries for likely birth dates.


if (dt.Year > 1900 && dt.Year <= DateTime.Today.Year)
{
_date = dt;
}
else
{
throw new ArgumentOutOfRangeException();
}
}

public TimeSpan GetTimeSpan(string dateString)


{
DateTime dt = Convert.ToDateTime(dateString);

if (dt.Ticks < _date.Ticks)


{
return _date - dt;
}
else
{
throw new ArgumentOutOfRangeException();
}
}
}

Um auf ein Feld in einer Instanz zuzugreifen, fügen Sie einen Punkt hinter dem Instanznamen ein, gefolgt vom
Namen des Felds, wie in instancename._fieldName . Zum Beispiel:
CalendarEntry birthday = new CalendarEntry();
birthday.Day = "Saturday";

Einem Feld kann ein Anfangswert durch Verwendung des Zuweisungsoperators zugewiesen werden, wenn das
Feld deklariert wird. Sie würden z.B. Day wie im folgenden Beispiel deklarieren, um das Day -Feld automatisch
"Monday" zuzuweisen:

public class CalendarDateWithInitialization


{
public string Day = "Monday";
//...
}

Felder werden umgehend initialisiert, bevor der Konstruktor für die Objektinstanz aufgerufen wird. Wenn der
Konstruktor den Wert eines Felds zuweist, überschreibt er jeden während der Felddeklaration angegebenen
Wert. Weitere Informationen finden Sie unter Verwenden von Konstruktoren.

NOTE
Ein Feldinitialisierer kann nicht auf andere Instanzfelder verweisen.

Felder können als public, private, protected, internal, protected internal oder private protected markiert werden.
Diese Zugriffsmodifizierer definieren, wie Benutzer des Typs auf die Felder zugreifen können. Weitere
Informationen finden Sie unter Zugriffsmodifizierer.
Ein Feld kann optional als static deklariert werden. Dadurch steht das Feld Aufrufern jederzeit zur Verfügung,
auch wenn keine Instanz des Typs vorhanden ist. Weitere Informationen finden Sie unter Statische Klassen und
statische Klassenmember.
Ein Feld kann als readonly deklariert werden. Einem schreibgeschützten Feld kann nur ein Wert während der
Initialisierung oder in einem Konstruktor zugewiesen werden. Ein static readonly -Feld ist einer Konstanten
sehr ähnlich, außer dass der C#-Compiler nicht auf den Wert eines statischen schreibgeschützten Felds zur
Kompilierzeit, sondern nur zur Laufzeit zugreifen kann. Weitere Informationen finden Sie unter Konstanten.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Verwenden von Konstruktoren
Vererbung
Zugriffsmodifizierer
Abstrakte und versiegelte Klassen und Klassenmember
Konstanten (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Konstanten sind unveränderliche Werte, die zur Kompilierzeit bekannt sind und sich während der Lebensdauer
des Programms nicht ändern. Konstanten werden mit dem const-Modifizierer deklariert. Nur die in C#
integrierten Typen (außer System.Object) können als const deklariert werden. Benutzerdefinierte Typen
einschließlich Klassen, Strukturen und Arrays können nicht const sein. Verwenden Sie den readonly-
Modifizierer zum Erstellen einer Klasse, einer Struktur oder eines Arrays, die/das zur Laufzeit einmal initialisiert
wird (z.B. in einem Konstruktor) und danach nicht geändert werden kann.
C# unterstützt keine const -Methoden, -Eigenschaften oder -Ereignisse.
Der Enumerationstyp ermöglicht Ihnen das Definieren benannter Konstanten für ganzzahlige integrierte Typen
(z.B. int , uint , long usw.). Weitere Informationen finden Sie unter enum.
Konstanten müssen initialisiert werden, wenn sie deklariert werden. Zum Beispiel:

class Calendar1
{
public const int Months = 12;
}

In diesem Beispiel ist die Konstante Months immer 12 und kann nicht einmal durch die Klasse selbst geändert
werden. Wenn der Compiler einen konstanten Bezeichner im C#-Quellcode erkennt (z.B. Months ), ersetzt er den
Literalwert direkt durch Code der Zwischensprache (Intermediate Language, IL), den er produziert. Da zur
Laufzeit keine Variablenadresse einer Konstanten zugeordnet ist, können const -Felder nicht durch Verweis
übergeben werden und können nicht als l-Wert in einem Ausdruck stehen.

NOTE
Seien Sie vorsichtig beim Verweisen auf konstante Werte, die in anderem Code wie z.B. DLLs definiert sind. Wenn eine
neue Version der DLL einen neuen Wert für die Konstante definiert, enthält das Programm weiterhin den alten
Literalwert, bis es mit der neuen Version erneut kompiliert wird.

Mehrere Konstanten desselben Typs können zur gleichen Zeit deklariert werden wie zum Beispiel:

class Calendar2
{
public const int Months = 12, Weeks = 52, Days = 365;
}

Der Ausdruck, mit dem eine Konstante initialisiert wird, kann auf eine andere Konstante verweisen, wenn
dadurch kein Zirkelverweis entsteht. Zum Beispiel:
class Calendar3
{
public const int Months = 12;
public const int Weeks = 52;
public const int Days = 365;

public const double DaysPerWeek = (double) Days / (double) Weeks;


public const double DaysPerMonth = (double) Days / (double) Months;
}

Konstanten können als public, private, protected, internal, protected internal oder private protected markiert
werden. Diese Zugriffsmodifizierer definieren, wie Benutzer der Klasse auf die Konstante zugreifen können.
Weitere Informationen finden Sie unter Zugriffsmodifizierer.
Auf Konstanten wird zugegriffen, als wären sie statische Felder, da der Wert der Konstante für alle Instanzen des
Typs identisch ist. Sie verwenden nicht das static -Schlüsselwort, um sie zu deklarieren. Ausdrücke, die nicht in
der Klasse enthalten sind, die die Konstante definiert, müssen den Klassennamen, einen Punkt und den Namen
der Konstante verwenden, um auf die Konstante zuzugreifen. Zum Beispiel:

int birthstones = Calendar.Months;

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Eigenschaften
Typen
readonly
Unveränderlichkeit in C#, Teil 1: Arten von Unveränderlichkeit
Gewusst wie: Definieren von abstrakten
Eigenschaften (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Das folgende Beispiel veranschaulicht, wie Sie abstrakte Eigenschaften definieren: Eine abstrakte
Eigenschaftendeklaration stellt keine Implementierung des Eigenschaftenaccessors bereit, sondern deklariert,
dass die Klasse Eigenschaften unterstützt, die Accessorenimplementierung jedoch abgeleiteten Klassen
überlässt. Das folgende Beispiel veranschaulicht das Implementieren von abstrakten Eigenschaften, die von
einer Basisklasse geerbt wurden.
Dieses Beispiel besteht aus drei Dateien, von denen jede einzeln kompiliert wird, und auf die daraus entstehende
Assembly von der nächsten Kompilierung verwiesen wird.
abstractshape.cs: die Shape -Klasse, die eine abstrakte Area -Eigenschaft enthält.
shapes.cs: die Unterklassen der Shape -Klasse.
shapetest.cs: ein Testprogramm zum Anzeigen der Bereiche einiger von Shape abgeleiteter Objekte.

Verwenden Sie den folgenden Befehl, um das Beispiel zu kompilieren:


csc abstractshape.cs shapes.cs shapetest.cs

Dadurch wird die ausführbare Datei „shapetest.exe“ erstellt.

Beispiele
Diese Datei deklariert die Shape -Klasse, die die Area -Eigenschaft des Typs double enthält.
// compile with: csc -target:library abstractshape.cs
public abstract class Shape
{
private string name;

public Shape(string s)
{
// calling the set accessor of the Id property.
Id = s;
}

public string Id
{
get
{
return name;
}

set
{
name = value;
}
}

// Area is a read-only property - only a get accessor is needed:


public abstract double Area
{
get;
}

public override string ToString()


{
return $"{Id} Area = {Area:F2}";
}
}

Modifizierer der Eigenschaft sind in der Deklaration der Eigenschaft selbst platziert. Zum Beispiel:

public abstract double Area

Wenn Sie eine abstrakte Eigenschaft deklarieren (z.B. Area in diesem Beispiel), geben Sie lediglich an,
welche Eigenschaftenaccessoren verfügbar sind, implementieren diese jedoch nicht. In diesem Beispiel ist
nur ein get-Accessor verfügbar, die Eigenschaft ist also schreibgeschützt.
Der folgende Code zeigt drei Unterklassen von Shape und wie sie die Area -Eigenschaft überschreiben, um ihre
eigene Implementierung bereitzustellen.
// compile with: csc -target:library -reference:abstractshape.dll shapes.cs
public class Square : Shape
{
private int side;

public Square(int side, string id)


: base(id)
{
this.side = side;
}

public override double Area


{
get
{
// Given the side, return the area of a square:
return side * side;
}
}
}

public class Circle : Shape


{
private int radius;

public Circle(int radius, string id)


: base(id)
{
this.radius = radius;
}

public override double Area


{
get
{
// Given the radius, return the area of a circle:
return radius * radius * System.Math.PI;
}
}
}

public class Rectangle : Shape


{
private int width;
private int height;

public Rectangle(int width, int height, string id)


: base(id)
{
this.width = width;
this.height = height;
}

public override double Area


{
get
{
// Given the width and height, return the area of a rectangle:
return width * height;
}
}
}

Der folgende Code zeigt ein Testprogramm, das eine Reihe abgeleiteter Shape -Objekte erstellt und deren
Bereiche ausdruckt.
// compile with: csc -reference:abstractshape.dll;shapes.dll shapetest.cs
class TestClass
{
static void Main()
{
Shape[] shapes =
{
new Square(5, "Square #1"),
new Circle(3, "Circle #1"),
new Rectangle( 4, 5, "Rectangle #1")
};

System.Console.WriteLine("Shapes Collection");
foreach (Shape s in shapes)
{
System.Console.WriteLine(s);
}
}
}
/* Output:
Shapes Collection
Square #1 Area = 25.00
Circle #1 Area = 28.27
Rectangle #1 Area = 20.00
*/

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Abstrakte und versiegelte Klassen und Klassenmember
Eigenschaften
Definieren von Konstanten in C#
04.11.2021 • 2 minutes to read

Konstanten sind Felder, deren Wert bei der Kompilierung festgelegt wird und nie geändert werden kann.
Verwenden Sie Konstanten, um aussagekräftige Namen anstelle numerischer Literale („magische Zahlen“) für
spezielle Werte bereitzustellen.

NOTE
In C# kann die Präprozessoranweisung #define nicht verwendet werden, um Konstanten zu definieren, so wie Sie es
normalerweise in C und C++ tun würden.

Verwenden Sie zum Definieren von konstanten Werten integraler Typen ( int , byte usw.) einen enumerierten
Typ. Weitere Informationen finden Sie unter enum.
Zum Definieren nicht-integraler Konstanten ist eine Methode, diese in einer einzelnen statischen Klasse namens
Constants zu gruppieren. Dies erfordert, dass allen Verweise auf die Konstanten, so wie im folgenden Beispiel
gezeigt, der Klassenname vorangestellt wird.

Beispiel
using System;

static class Constants


{
public const double Pi = 3.14159;
public const int SpeedOfLight = 300000; // km per sec.
}

class Program
{
static void Main()
{
double radius = 5.3;
double area = Constants.Pi * (radius * radius);
int secsFromSun = 149476000 / Constants.SpeedOfLight; // in km
Console.WriteLine(secsFromSun);
}
}

Die Verwendung des Klassennamenqualifizierers hilft Ihnen sicherzustellen, dass Sie und andere Benutzer der
Konstante verstehen, dass diese konstant ist und nicht verändert werden kann.

Siehe auch
Das C#-Typsystem
Eigenschaften (C#-Programmierhandbuch)
04.11.2021 • 4 minutes to read

Eine Eigenschaft ist ein Member, das einen flexiblen Mechanismus zum Lesen, Schreiben oder Berechnen des
Werts eines privaten Felds bietet. Eigenschaften können wie öffentliche Datenmember verwendet werden, es
sind jedoch spezielle Methoden namens Accessoren. Dies ermöglicht den problemlosen Datenzugriff und
unterstützt weiterhin die Sicherheit und Flexibilität der Methoden.

Übersicht über Eigenschaften


Mithilfe von Eigenschaften kann eine Klasse eine öffentliche Methode zum Abrufen und Festlegen von
Werten verfügbar machen und dabei den Implementierungs- oder Verifizierungscode ausblenden.
Eine get-Eigenschaftenaccessor wird verwendet, um den Wert der Eigenschaft zurückzugeben. Ein set-
Eigenschaftenaccessor wird verwendet, um einen neuen Wert zuzuweisen. In C# 9 und höher wird eine
init-Eigenschaftenzugriffsmethode verwendet, um nur während der Objekterstellung einen neuen Wert
zuzuweisen. Diese Zugriffsmethoden können über verschiedene Zugriffsebenen verfügen. Weitere
Informationen finden Sie unter Einschränken des Accessorzugriffs.
Das value-Schlüsselwort wird verwendet, um den Wert zu definieren, der von der set - oder init -
Zugriffsmethode zugewiesen wird.
Eigenschaften können sein: Lesen/Schreiben (beide verfügen über einen get - und set -Accessor),
schreibgeschützt (verfügen über einen get -Accessor, jedoch keinen set -Accessor), oder lesegeschützt
(verfügen über einen set -Accessor, jedoch keinen get Accessor). Lesegeschützte Eigenschaften sind
selten und werden am häufigsten verwendet, um den Zugriff auf vertrauliche Daten einzuschränken.
Einfache Eigenschaften, die keinen benutzerdefinierten Accessorcode erfordern können implementiert
werden, entweder als Ausdruckstextdefinitionen oder als automatisch implementierte Eigenschaften.

Eigenschaften mit Unterstützungsfeldern


Ein grundlegendes Muster zum Implementieren einer Eigenschaft umfasst ein privates Unterstützungsfeld zum
Festlegen und Abrufen des Eigenschaftswerts. Der get -Accessor gibt den Wert des privaten Felds zurück, und
der set -Accessor kann die Validierung einiger Daten ausführen, bevor er dem privaten Feld einen Wert
zuweist. Beide Accessoren führen möglicherweise eine Konvertierung oder eine Berechnung der Daten aus,
bevor sie gespeichert oder zurückgegeben werden.
Dieses Muster wird anhand des folgenden Beispiels veranschaulicht. In diesem Beispiel stellt die TimePeriod -
Klasse ein Zeitintervall dar. Intern speichert die Klasse das Zeitintervall in Sekunden in einem privaten Feld mit
dem Namen _seconds . Eine Schreib-Lese-Eigenschaft mit dem Namen Hours ermöglicht dem Kunden, das
Zeitintervall in Stunden anzugeben. Die get - und set -Accessoren führen jeweils die notwendige
Konvertierung zwischen Stunden und Sekunden durch. Darüber hinaus prüft der set -Accessor die Daten, und
löst eine ArgumentOutOfRangeException aus, wenn die Anzahl von Stunden ungültig ist.
using System;

class TimePeriod
{
private double _seconds;

public double Hours


{
get { return _seconds / 3600; }
set {
if (value < 0 || value > 24)
throw new ArgumentOutOfRangeException(
$"{nameof(value)} must be between 0 and 24.");

_seconds = value * 3600;


}
}
}

class Program
{
static void Main()
{
TimePeriod t = new TimePeriod();
// The property assignment causes the 'set' accessor to be called.
t.Hours = 24;

// Retrieving the property causes the 'get' accessor to be called.


Console.WriteLine($"Time in hours: {t.Hours}");
}
}
// The example displays the following output:
// Time in hours: 24

Ausdruckstextdefinitionen
Häufig bestehen Eigenschaftenaccessoren aus einzeilige Anweisungen, die gerade das Ergebnis eines Ausdrucks
zuweisen oder zurückgeben. Sie können diese Eigenschaften als Ausdruckskörpermember implementieren.
Ausdruckstextdefinitionen bestehen aus dem => -Symbol, gefolgt von dem Ausdruck, der der Eigenschaft
zugewiesen oder aus dieser abgerufen werden soll.
Beginnend mit dem C# 6, können schreibgeschützte Eigenschaften den get -Accessor als
Ausdruckskörpermember implementieren. In diesem Fall werden weder das get -Accessorschlüsselwort noch
das return -Schlüsselwort verwendet. Das folgende Beispiel implementiert die schreibgeschützte Name -
Eigenschaft als ein Ausdruckskörpermember.
using System;

public class Person


{
private string _firstName;
private string _lastName;

public Person(string first, string last)


{
_firstName = first;
_lastName = last;
}

public string Name => $"{_firstName} {_lastName}";


}

public class Example


{
public static void Main()
{
var person = new Person("Magnus", "Hedlund");
Console.WriteLine(person.Name);
}
}
// The example displays the following output:
// Magnus Hedlund

Ab C# 7.0 können sowohl der get - als auch der set -Accessor als Ausdruckskörpermember implementiert
werden. In diesem Fall müsse die get - und set -Schlüsselwörter vorhanden sein. Das folgende Beispiel
veranschaulicht die Verwendung von Ausdruckstextdefinitionen für beide Accessoren. Beachten Sie, dass das
return -Schlüsselwort nicht mit dem get -Accessor verwendet wird.
using System;

public class SaleItem


{
string _name;
decimal _cost;

public SaleItem(string name, decimal cost)


{
_name = name;
_cost = cost;
}

public string Name


{
get => _name;
set => _name = value;
}

public decimal Price


{
get => _cost;
set => _cost = value;
}
}

class Program
{
static void Main(string[] args)
{
var item = new SaleItem("Shoes", 19.95m);
Console.WriteLine($"{item.Name}: sells for {item.Price:C2}");
}
}
// The example displays output like the following:
// Shoes: sells for $19.95

Automatisch implementierte Eigenschaften


In einigen Fällen weisen die Eigenschaft get - und set -Accessoren nur einen Wert zu oder rufen einen Wert
aus einem Unterstützungsfeld ab, ohne zusätzliche Logik. Mithilfe von automatisch implementierten
Eigenschaften können Sie Ihren Code vereinfachen, während der C#-Compiler das Unterstützungsfeld für Sie
transparent bereitstellt.
Wenn eine Eigenschaft über eine get - und eine set -Zugriffsmethode (oder eine get - und init -
Zugriffsmethode) verfügt, müssen beide Methoden automatisch implementiert werden. Definieren Sie eine
automatisch implementierte Eigenschaft mithilfe der get - und set -Schlüsselwörter ohne jede
Implementierung. Im folgenden Beispiel wird das vorherige Beispiel wiederholt, außer das Name und Price
automatisch implementierte Eigenschaften sind. Das Beispiel entfernt auch den parametrisierten Konstruktor,
damit SaleItem -Objekte jetzt mit einem Aufruf des parameterlosen Konstruktors und eines Objektinitialisierers
initialisiert werden.
using System;

public class SaleItem


{
public string Name
{ get; set; }

public decimal Price


{ get; set; }
}

class Program
{
static void Main(string[] args)
{
var item = new SaleItem{ Name = "Shoes", Price = 19.95m };
Console.WriteLine($"{item.Name}: sells for {item.Price:C2}");
}
}
// The example displays output like the following:
// Shoes: sells for $19.95

Verwandte Abschnitte
Verwenden von Eigenschaften
Schnittstelleneigenschaften
Vergleich zwischen Eigenschaften und Indexern
Einschränken des Zugriffsmethodenzugriffs
Automatisch implementierte Eigenschaften

C#-Programmiersprachenspezifikation
Weitere Informationen finden Sie unter Eigenschaften in der C#-Sprachspezifikation. Die Sprachspezifikation ist
die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Verwenden von Eigenschaften
Indexer
get-Schlüsselwort
set-Schlüsselwort
Verwenden von Eigenschaften (C#-
Programmierhandbuch)
04.11.2021 • 8 minutes to read

Eigenschaften kombinieren Aspekte der Felder und der Methoden. Für den Benutzer eines Objekts erscheint
eine Eigenschaft wie ein Feld; der Zugriff auf die Eigenschaft erfordert dieselbe Syntax. Für den Implementierer
einer Klasse, besteht eine Eigenschaft aus einem oder zwei Codeblöcken, die einen get-Accessor und/oder einen
set-Accessor darstellen. Der Codeblock für den get -Accessor wird ausgeführt, wenn die Eigenschaft gelesen
wird; der Codeblock für den set -Accessor wird ausgeführt, wenn der Eigenschaft ein neuer Wert zugewiesen
wird. Eine Eigenschaft ohne einen set -Accessor ist schreibgeschützt. Eine Eigenschaft ohne einen get -
Accessor ist lesegeschützt. Eine Eigenschaft, die beide Accessoren umfasst, ermöglicht Lese-/ Schreibzugriff. In
C# 9 und höher können Sie eine init -Zugriffsmethode anstelle einer set -Zugriffsmethode verwenden, um
die Eigenschaft als schreibgeschützt zu kennzeichnen.
Im Gegensatz zu Feldern, werden Eigenschaften nicht als Variablen klassifiziert. Aus diesem Grund können Sie
Eigenschaften nicht als ref oder out-Parameter übergeben.
Eigenschaften sind vielseitig verwendbar: Sie können Daten überprüfen, bevor sie eine Änderung zulassen. Sie
können Daten in einer Klasse transparent verfügbar machen, in denen die Daten in Wirklichkeit von einer
anderen Quelle abgerufen werden, z.B. einer Datenbank. Sie können eine Aktion ausführen, wenn Daten
geändert werden, z.B. ein Ereignis auslösen, oder den Wert anderer Felder verändern.
Eigenschaften werden im Klassenblock deklariert, indem die Zugriffsebene des Felds angegeben wird, gefolgt
vom Typ der Eigenschaft, gefolgt vom Namen der Eigenschaft und gefolgt von einem Codeblock, der einen get
-Accessor und/oder einen set Accessor deklariert. Zum Beispiel:

public class Date


{
private int _month = 7; // Backing store

public int Month


{
get => _month;
set
{
if ((value > 0) && (value < 13))
{
_month = value;
}
}
}
}

In diesem Beispiel wird Month als Eigenschaft so deklariert, dass der set -Accessor dafür sorgen kann, dass der
Month -Wert zwischen 1 und 12 festgelegt wird. Die Month -Eigenschaft verwendet ein privates Feld, um den
tatsächlichen Wert nachzuverfolgen. Der tatsächliche Speicherort der Daten für eine Eigenschaft wird häufig als
„Sicherungsspeicher “ der Eigenschaft bezeichnet. Es ist üblich für Eigenschaften, die privaten Felder als
Sicherungsspeicher zu verwenden. Das Feld wird als privat gekennzeichnet um sicherzustellen, dass es nur
durch Aufrufen der Eigenschaft geändert werden kann. Weitere Informationen zu öffentlichen und privaten
Zugriffsbeschränkungen finden Sie unter Zugriffsmodifizierer.
Automatisch implementierte Eigenschaften stellen eine vereinfachte Syntax für einfache
Eigenschaftendeklarationen bereit. Weitere Informationen finden Sie unter Automatisch implementierte
Eigenschaften.

Die get-Zugriffsmethode
Der Text des get Accessors ähnelt dem einer Methode. Er muss einen Wert des Eigenschaftentyps zurückgeben.
Die Ausführung des get Accessors entspricht dem Lesen des Wert des Felds. Wenn Sie z.B. die private Variable
vom get -Accessor zurückgeben und Optimierungen aktiviert sind, wird der Aufruf an die get -Accessor-
Methode vom Compiler eingebettet, damit kein zusätzlicher Aufwand an Methodenaufrufen entsteht. Allerdings
kann eine get -Accessor-Methode kann nicht eingebettet werden, da der Compiler zum Zeitpunkt der
Kompilierung nicht erkennt, welche Methode zur Laufzeit tatsächlich aufgerufen wird. Im folgenden finden Sie
einen get -Accessor, der den Wert eines privaten Felds zurückgibt _name :

class Person
{
private string _name; // the name field
public string Name => _name; // the Name property
}

Wenn Sie auf die Eigenschaft, außer als Ziel einer Zuweisung, verweisen, wird der get -Accessor aufgerufen, um
den Wert der Eigenschaft zu lesen. Zum Beispiel:

Person person = new Person();


//...

System.Console.Write(person.Name); // the get accessor is invoked here

Der get Accessor muss mit einer return- oder einer throw-Anweisung enden, und die Steuerung darf nicht
über den Accessortext hinausgehen.
Es ist ein unzulässiger Programmierstil den Zustand des Objekts mithilfe des get -Accessors zu verändern. Der
folgende Accessor hat z.B. den Nebeneffekt, dass der Zustand des Objekts, bei jedem Zugriff auf das _number -
Feld verändert wird.

private int _number;


public int Number => _number++; // Don't do this

Der get -Accessor kann verwendet werden, um den Wert des Felds zurückzugeben, oder um den Wert des
Felds zu berechnen und zurückgeben. Zum Beispiel:

class Employee
{
private string _name;
public string Name => _name != null ? _name : "NA";
}

Im vorherigen Codesegment wird, wenn Sie der Name -Eigenschaft keinen Wert zuweisen, der Wert NA
zurückgegeben.

Die set-Zugriffsmethode
Der set -Accessor ähnelt einer Methode, deren Rückgabetyp void ist. Er verwendet einen impliziten Parameter
mit dem Namen value , dessen Typ der Typ der Eigenschaft ist. Im folgenden Beispiel wird ein set -Accessor
der Name -Eigenschaft hinzugefügt.

class Person
{
private string _name; // the name field
public string Name // the Name property
{
get => _name;
set => _name = value;
}
}

Wenn Sie der Eigenschaft einen Wert zuweisen, wird der set -Accessor mit einem Argument aufgerufen, das
den neuen Wert bereitstellt. Zum Beispiel:

Person person = new Person();


person.Name = "Joe"; // the set accessor is invoked here

System.Console.Write(person.Name); // the get accessor is invoked here

Das Verwenden des impliziten Parameternamens, value , für die Deklaration einer lokalen Variablen in einem
set -Accessor ist ein Fehler.

Die init-Zugriffsmethode
Der Code zum Erstellen einer init -Zugriffsmethode ist derselbe wie der Code zum Erstellen einer set -
Zugriffsmethode, mit der Ausnahme, dass Sie das Schlüsselwort init anstelle von set verwenden. Der
Unterschied besteht darin, dass die init -Zugriffsmethode nur im Konstruktor oder mithilfe eines
Objektinitialisierers verwendet werden kann.

Hinweise
Eigenschaften können als public , private , protected , internal , protected internal oder private protected
gekennzeichnet werden. Diese Zugriffsmodifizierer definieren, wie Benutzer der Klasse auf die Eigenschaft
zugreifen können. Die get - und set -Accessoren für die gleiche Eigenschaft haben möglicherweise
verschiedene Zugriffsmodifizierer. Z.B. kann get möglicherweise public sein, um den schreibgeschützten
Zugriff von außerhalb des Typs zu ermöglichen, und set kann möglicherweise private oder protected sein.
Weitere Informationen finden Sie unter Zugriffsmodifizierer.
Eine Eigenschaft kann als statische Eigenschaft deklariert werden, mithilfe des static -Schlüsselworts. Dadurch
steht das Feld Aufrufern jederzeit zur Verfügung, auch wenn keine Instanz der Klasse vorhanden ist. Weitere
Informationen finden Sie unter Statische Klassen und statische Klassenmember.
Ein Ereignis kann mithilfe des virtual-Schlüsselworts als virtuelles Ereignis gekennzeichnet werden. Dies
ermöglicht abgeleiteten Klassen, das Ereignisverhalten mithilfe des override-Schlüsselworts zu überschreiben.
Weitere Informationen zu diesen Optionen finden Sie unter Vererbung.
Ein Ereignis, das ein virtuelles Ereignis überschreibt, kann auch sealed(verschlossen) sein, was angibt, dass es für
abgeleitete Klassen nicht mehr virtuell ist. Schließlich kann eine Eigenschaft als abstract(abstrakt) deklariert
werden. Dies bedeutet, dass es keine Implementierung in der Klasse gibt, und abgeleitete Klassen müssen eine
eigene Implementierung schreiben. Weitere Informationen zu abstrakten Klassen finden Sie unter Abstrakte und
versiegelte Klassen und Klassenmember.
NOTE
Das Verwenden eines virtual(virtuell)-, abstract(abstrakt)- oder override(außer Kraft setzen)- Modifizierers für einen
Accessor einer statischen Eigenschaft ist ein Fehler.

Beispiele
Dieses Beispiel zeigt,Instanz-, statische- und schreibgeschützte Eigenschaften. Dieser Parameter akzeptiert den
Namen des Mitarbeiters auf der Tastatur, Inkremente NumberOfEmployees durch 1, und zeigt den
Mitarbeiternamen und die Nummer an.

public class Employee


{
public static int NumberOfEmployees;
private static int _counter;
private string _name;

// A read-write instance property:


public string Name
{
get => _name;
set => _name = value;
}

// A read-only static property:


public static int Counter => _counter;

// A Constructor:
public Employee() => _counter = ++NumberOfEmployees; // Calculate the employee's number:
}

class TestEmployee
{
static void Main()
{
Employee.NumberOfEmployees = 107;
Employee e1 = new Employee();
e1.Name = "Claude Vige";

System.Console.WriteLine("Employee number: {0}", Employee.Counter);


System.Console.WriteLine("Employee name: {0}", e1.Name);
}
}
/* Output:
Employee number: 108
Employee name: Claude Vige
*/

Beispiel für eine ausgeblendete Eigenschaft


In diesem Beispiel wird veranschaulicht, wie auf eine Eigenschaft in einer Basisklasse zugegriffen werden kann,
die von einer anderen Eigenschaft ausgeblendet ist, die in einer abgeleiteten Klasse den gleichen Namen hat:
public class Employee
{
private string _name;
public string Name
{
get => _name;
set => _name = value;
}
}

public class Manager : Employee


{
private string _name;

// Notice the use of the new modifier:


public new string Name
{
get => _name;
set => _name = value + ", Manager";
}
}

class TestHiding
{
static void Main()
{
Manager m1 = new Manager();

// Derived class property.


m1.Name = "John";

// Base class property.


((Employee)m1).Name = "Mary";

System.Console.WriteLine("Name in the derived class is: {0}", m1.Name);


System.Console.WriteLine("Name in the base class is: {0}", ((Employee)m1).Name);
}
}
/* Output:
Name in the derived class is: John, Manager
Name in the base class is: Mary
*/

Die folgenden Punkte im vorherigen Beispiel sind wichtig:


Die Eigenschaft Name in der abgeleiteten Klasse blendet die Eigenschaft Name in der Basisklasse aus. In
solch einem Fall wird der new -Modifizierer in der Deklaration der Eigenschaft in der abgeleiteten Klasse
verwendet:

public new string Name

Die Umwandlung (Employee) wird für den Zugriff auf die ausgeblendete Eigenschaft in der Basisklasse
verwendet:

((Employee)m1).Name = "Mary";

Weitere Informationen zum Ausblenden von Mitgliedern finden Sie unter new-Modifizierer.

Beispiel für Überschreibungseigenschaft


In diesem Beispiel implementieren zwei Klassen Cube und Square eine abstrakte Klasse Shape , und
überschreiben die abstrakte Area -Eigenschaft. Beachten Sie die Verwendung des überschreiben-Modifizierers
in den Eigenschaften. Das Programm akzeptiert die Seite als Eingabe und berechnet die Bereiche für das
Quadrat und den Cube. Das Programm akzeptiert auch den Bereich als Eingabe und berechnet die
entsprechende Seite für das Quadrat und den Cube.

abstract class Shape


{
public abstract double Area
{
get;
set;
}
}

class Square : Shape


{
public double side;

//constructor
public Square(double s) => side = s;

public override double Area


{
get => side * side;
set => side = System.Math.Sqrt(value);
}
}

class Cube : Shape


{
public double side;

//constructor
public Cube(double s) => side = s;

public override double Area


{
get => 6 * side * side;
set => side = System.Math.Sqrt(value / 6);
}
}

class TestShapes
{
static void Main()
{
// Input the side:
System.Console.Write("Enter the side: ");
double side = double.Parse(System.Console.ReadLine());

// Compute the areas:


Square s = new Square(side);
Cube c = new Cube(side);

// Display the results:


System.Console.WriteLine("Area of the square = {0:F2}", s.Area);
System.Console.WriteLine("Area of the cube = {0:F2}", c.Area);
System.Console.WriteLine();

// Input the area:


System.Console.Write("Enter the area: ");
double area = double.Parse(System.Console.ReadLine());

// Compute the sides:


s.Area = area;
c.Area = area;
// Display the results:
System.Console.WriteLine("Side of the square = {0:F2}", s.side);
System.Console.WriteLine("Side of the cube = {0:F2}", c.side);
}
}
/* Example Output:
Enter the side: 4
Area of the square = 16.00
Area of the cube = 96.00

Enter the area: 24


Side of the square = 4.90
Side of the cube = 2.00
*/

Siehe auch
C#-Programmierhandbuch
Eigenschaften
Schnittstelleneigenschaften
Automatisch implementierte Eigenschaften
Schnittstelleneigenschaften (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Eigenschaften können für eine Schnittstelle deklariert werden. Im folgenden Beispiel wird eine Zugriffsmethode
für Schnittstelleneigenschaften deklariert:

public interface ISampleInterface


{
// Property declaration:
string Name
{
get;
set;
}
}

Schnittstelleneigenschaften verfügen in der Regel über keinen Text. Die Zugriffsmethoden geben an, ob Lese-
/Schreibzugriff auf die Eigenschaft besteht oder ob sie schreib- oder lesegeschützt ist. Anders als bei Klassen
und Strukturen wird beim Deklarieren von Zugriffsmethoden ohne Text keine automatisch implementierte
Eigenschaft deklariert. Ab C# 8.0 kann eine Schnittstelle eine Standardimplementierung für Member,
einschließlich Eigenschaften, definieren. Standardimplementierungen für Eigenschaften in einer Schnittstelle
sind selten, weil Schnittstellen möglicherweise keine Instanzdatenfelder definieren.

Beispiel
In diesem Beispiel besitzt die Schnittstelle IEmployee eine Lese-/Schreibzugriff-Eigenschaft Name und eine
Lesezugriff-Eigenschaft Counter . Die Klasse Employee implementiert die Schnittstelle IEmployee und
verwendet diese beiden Eigenschaften. Der Name eines neuen Mitarbeiters und die aktuelle Anzahl der
Mitarbeiter werden vom Programm gelesen, und der Mitarbeitername sowie die berechnete Mitarbeiteranzahl
werden angezeigt.
Sie können den vollqualifizierten Namen der Eigenschaft verwenden, der auf die Schnittstelle verweist, in der
der Member deklariert wird. Zum Beispiel:

string IEmployee.Name
{
get { return "Employee Name"; }
set { }
}

Im vorangehenden Beispiel wird die Explizite Schnittstellenimplementierung veranschaulicht. Wenn z.B. die
Klasse Employee die beiden Schnittstellen ICitizen und IEmployee implementiert und beide Schnittstellen die
Eigenschaft Name besitzen, ist die explizite Implementierung des Schnittstellenmembers erforderlich. Das
bedeutet, dass die folgende Eigenschaftendeklaration:
string IEmployee.Name
{
get { return "Employee Name"; }
set { }
}

die Eigenschaft Name für die Schnittstelle IEmployee implementiert. Dahingegen implementiert die folgende
Deklaration:

string ICitizen.Name
{
get { return "Citizen Name"; }
set { }
}

die Eigenschaft Name für die Schnittstelle ICitizen .

interface IEmployee
{
string Name
{
get;
set;
}

int Counter
{
get;
}
}

public class Employee : IEmployee


{
public static int numberOfEmployees;

private string _name;


public string Name // read-write instance property
{
get => _name;
set => _name = value;
}

private int _counter;


public int Counter // read-only instance property
{
get => _counter;
}

// constructor
public Employee() => _counter = ++numberOfEmployees;
}
System.Console.Write("Enter number of employees: ");
Employee.numberOfEmployees = int.Parse(System.Console.ReadLine());

Employee e1 = new Employee();


System.Console.Write("Enter the name of the new employee: ");
e1.Name = System.Console.ReadLine();

System.Console.WriteLine("The employee information:");


System.Console.WriteLine("Employee number: {0}", e1.Counter);
System.Console.WriteLine("Employee name: {0}", e1.Name);

Beispielausgabe
Enter number of employees: 210
Enter the name of the new employee: Hazem Abolrous
The employee information:
Employee number: 211
Employee name: Hazem Abolrous

Weitere Informationen
C#-Programmierhandbuch
Eigenschaften
Verwenden von Eigenschaften
Vergleich zwischen Eigenschaften und Indexern
Indexer
Schnittstellen
Einschränken des Accessorzugriffs (C#-
Programmierhandbuch)
04.11.2021 • 4 minutes to read

Die Get- und Set-Teile einer Eigenschaft oder eines Indexers werden Zugriffsmethoden oder Accessoren
genannt. Standardmäßig weisen diese Zugriffsmethoden dieselbe Sichtbarkeit oder Zugriffsebene auf: nämlich
die der Eigenschaft oder des Indexers, zu dem sie gehören. Weitere Informationen finden Sie unter
Zugriffsebenen. Allerdings ist es manchmal sinnvoll, den Zugriff auf eine der beiden Zugriffsmethoden
einzuschränken. Dies bedeutet in der Regel, dass der Zugriff auf den set -Accessor eingeschränkt wird,
während der get -Accessor öffentlich zugänglich bleibt. Zum Beispiel:

private string _name = "Hello";

public string Name


{
get
{
return _name;
}
protected set
{
_name = value;
}
}

In diesem Beispiel definiert die Eigenschaft Name einen get - und einen set -Accessor. Der get -Accessor
erhält die Zugriffsebene der Eigenschaft selbst, in diesem Fall public . Der set -Accessor wird jedoch explizit
durch Anwenden des Protected-Zugriffsmodifizierers auf den Accessor selbst eingeschränkt.

Einschränkungen für Zugriffsmodifizierer für Accessoren


Bei der Verwendung von Accessormodifizierern auf Eigenschaften oder Indexer muss Folgendes beachtet
werden:
Accessormodifizierer können nicht bei einer Schnittstelle oder einer expliziten Schnittstellen-Member-
Implementierung verwendet werden.
Accessormodifizierer können nur verwendet werden, wenn die Eigenschaft oder der Indexer sowohl
einen set - als auch einen get -Accessor besitzt. In diesem Fall ist der Modifizierer nur für einen der
beiden Accessoren zulässig.
Besitzt die Eigenschaft oder der Indexer einen Override-Modifizierer, muss der Accessormodifizierer mit
dem eventuell vorhandenen Accessor des überschriebenen Accessors übereinstimmen.
Die Zugriffsebene des Accessors muss restriktiver sein als die Zugriffsebene der Eigenschaft oder des
Indexers selbst.

Zugriffsmodifizierer für Override-Accessoren


Beim Überschreiben einer Eigenschaft oder eines Indexers müssen die überschriebenen Accessoren für den
überschreibenden Code zugänglich sein. Außerdem müssen der Zugriff der Eigenschaft/des Indexers als auch
der Accessoren mit der entsprechenden überschriebenen Eigenschaft/dem Indexer und den Accessoren
übereinstimmen. Zum Beispiel:

public class Parent


{
public virtual int TestProperty
{
// Notice the accessor accessibility level.
protected set { }

// No access modifier is used here.


get { return 0; }
}
}
public class Kid : Parent
{
public override int TestProperty
{
// Use the same accessibility level as in the overridden accessor.
protected set { }

// Cannot use access modifier here.


get { return 0; }
}
}

Implementieren von Schnittstellen


Bei der Verwendung eines Accessors zur Implementierung einer Schnittstelle darf der Accessor keinen
Zugriffsmodifizierer besitzen. Wird jedoch zur Implementierung der Schnittstelle nur ein Accessor verwendet,
z.B. get , kann der andere Accessor einen Zugriffsmodifizierer besitzen. Hier ein Beispiel:

public interface ISomeInterface


{
int TestProperty
{
// No access modifier allowed here
// because this is an interface.
get;
}
}

public class TestClass : ISomeInterface


{
public int TestProperty
{
// Cannot use access modifier here because
// this is an interface implementation.
get { return 10; }

// Interface property does not have set accessor,


// so access modifier is allowed.
protected set { }
}
}

Zugriffsdomäne des Accessors


Bei Verwendung eines Zugriffsmodifizierers für den Accessor wird die Zugriffsdomäne des Accessors durch
diesen Modifizierer bestimmt.
Andernfalls wird die Zugriffsdomäne des Accessors durch die Zugriffsebene der Eigenschaft oder des Indexers
bestimmt.

Beispiel
Das folgende Beispiel enthält drei Klassen: BaseClass , DerivedClass und MainClass . Für BaseClass gibt es
zwei Eigenschaften, Name und Id für beide Klassen. Das Beispiel veranschaulicht, wie die Eigenschaft Id in
DerivedClass durch die Eigenschaft Id in BaseClass ausgeblendet werden kann, wenn ein restriktiver
Zugriffsmodifizierer, wie z.B. protected oder private, verwendet wird. Daher wird beim Zuweisen von Werten zu
dieser Eigenschaft stattdessen die Eigenschaft für die Klasse BaseClass aufgerufen. Wird der
Zugriffsmodifizierer durch public ersetzt, kann auf die Eigenschaft zugegriffen werden.
Das Beispiel zeigt auch, dass die Verwendung eines restriktiven Zugriffsmodifizierers, wie z.B. private oder
protected , auf den set -Accessor der Eigenschaft Name in DerivedClass den Zugriff auf den Accessor
verhindert. Mit dem Zuweisen zu dem Accessor wird ein Fehler generiert.

public class BaseClass


{
private string _name = "Name-BaseClass";
private string _id = "ID-BaseClass";

public string Name


{
get { return _name; }
set { }
}

public string Id
{
get { return _id; }
set { }
}
}

public class DerivedClass : BaseClass


{
private string _name = "Name-DerivedClass";
private string _id = "ID-DerivedClass";

new public string Name


{
get
{
return _name;
}

// Using "protected" would make the set accessor not accessible.


set
{
_name = value;
}
}

// Using private on the following property hides it in the Main Class.


// Any assignment to the property will use Id in BaseClass.
new private string Id
{
get
{
return _id;
}
set
{
_id = value;
}
}
}
}

class MainClass
{
static void Main()
{
BaseClass b1 = new BaseClass();
DerivedClass d1 = new DerivedClass();

b1.Name = "Mary";
d1.Name = "John";

b1.Id = "Mary123";
d1.Id = "John123"; // The BaseClass.Id property is called.

System.Console.WriteLine("Base: {0}, {1}", b1.Name, b1.Id);


System.Console.WriteLine("Derived: {0}, {1}", d1.Name, d1.Id);

// Keep the console window open in debug mode.


System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/* Output:
Base: Name-BaseClass, ID-BaseClass
Derived: John, ID-BaseClass
*/

Kommentare
Beachten Sie, dass beim Ersetzen der Deklaration new private string Id durch new public string Id
Folgendes ausgegeben wird:
Name and ID in the base class: Name-BaseClass, ID-BaseClass

Name and ID in the derived class: John, John123

Siehe auch
C#-Programmierhandbuch
Eigenschaften
Indexer
Zugriffsmodifizierer
Gewusst wie: Deklarieren und Verwenden von Lese-
/Schreibeigenschaften (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Eigenschaften stellen die Vorteile von öffentlichen Datenmembern bereit, ohne die mit dem ungeschützten,
nicht kontrollierten und nicht geprüften Zugriff auf die Daten eines Objekts verknüpften Risiken aufzuweisen.
Dies wird durch Accessoren erreicht: Dies sind besondere Methoden, die den zugrunde liegenden
Datenmembern Werte zuweisen bzw. diese Werte abrufen. Der set-Accessor ermöglicht das Zuweisen von
Datenmembern, und der get-Accessor ruft Datenmemberwerte ab.
In diesem Beispiel wird eine Person -Klasse mit zwei Eigenschaften dargestellt: Name (Zeichenfolge) und Age
(ganze Zahl). Beide Eigenschaften stellen einen get - und einen set -Accessor bereit, es handelt sich also um
Lese- bzw. Schreibeigenschaften.

Beispiel
class Person
{
private string _name = "N/A";
private int _age = 0;

// Declare a Name property of type string:


public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}

// Declare an Age property of type int:


public int Age
{
get
{
return _age;
}

set
{
_age = value;
}
}

public override string ToString()


{
return "Name = " + Name + ", Age = " + Age;
}
}

class TestPerson
{
static void Main()
{
{
// Create a new Person object:
Person person = new Person();

// Print out the name and the age associated with the person:
Console.WriteLine("Person details - {0}", person);

// Set some values on the person object:


person.Name = "Joe";
person.Age = 99;
Console.WriteLine("Person details - {0}", person);

// Increment the Age property:


person.Age += 1;
Console.WriteLine("Person details - {0}", person);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Person details - Name = N/A, Age = 0
Person details - Name = Joe, Age = 99
Person details - Name = Joe, Age = 100
*/

Stabile Programmierung
Im vorherigen Beispiel sind die Name - und Age -Eigenschaften public (öffentlich) und besitzen jeweils einen
get - und einen set -Accessor. Auf diese Weise können diese Eigenschaften von allen Objekten gelesen und
überschrieben werden. Manchmal ist es jedoch wünschenswert, einen der Accessoren auszuschließen. Indem
Sie den set -Accessor auslassen, wandeln Sie die Eigenschaft beispielsweise in eine schreibgeschützte
Eigenschaft um:

public string Name


{
get
{
return _name;
}
set
{
_name = value;
}
}

Alternativ können Sie einen Accessor öffentlich verfügbar machen, während der andere als privat oder geschützt
festgelegt wird. Weitere Informationen finden Sie unter Asymmetric Accessor Accessibility (Asymmetrischer
Accessorzugriff).
Nachdem die Eigenschaften deklariert wurden, können Sie genauso wie Felder der Klasse verwendet werden.
Daher kann sowohl beim Abrufen als auch beim Festlegen von Eigenschaftswerten eine relativ natürliche Syntax
verwendet werden. Siehe dazu folgende Anweisungen:

person.Name = "Joe";
person.Age = 99;

Beachten Sie, dass in der set -Methode einer Eigenschaft eine spezielle value -Variable verfügbar ist. Diese
Variable enthält den vom Benutzer angegebenen Wert. Beispiel:
_name = value;

Beachten Sie die einfache Syntax zum Erhöhen des Age -Eigenschaftswerts eines Person -Objekts:

person.Age += 1;

Wenn getrennte set - und get -Methoden verwendet wurden, um Eigenschaften zu modellieren, könnte der
entsprechende Code folgendermaßen aussehen:

person.SetAge(person.GetAge() + 1);

Die ToString -Methode wird in diesem Beispiel überschrieben:

public override string ToString()


{
return "Name = " + Name + ", Age = " + Age;
}

Beachten Sie, dass ToString im Programm nicht explizit verwendet wird. Es wird standardmäßig durch die
WriteLine -Aufrufe aufgerufen.

Siehe auch
C#-Programmierhandbuch
Eigenschaften
Das C#-Typsystem
Automatisch implementierte Eigenschaften (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

In C# 3.0 und höher werden Eigenschaftsdeklarationen durch automatisch implementierte Eigenschaften


präziser, wenn in den Eigenschaftenzugriffsmethoden keine zusätzliche Logik erforderlich ist. Zudem
ermöglichen sie Clientcode das Erstellen von Objekten. Wenn Sie eine Eigenschaft wie im folgenden Beispiel
gezeigt deklarieren, erstellt der Compiler ein privates, anonymes, dahinter liegendes Feld, auf das nur über get
und set -Accessoren zugegriffen werden kann. In C# 9 und höher können init -Zugriffsmethoden auch als
automatisch implementierte Eigenschaften deklariert werden.

Beispiel
Das folgende Beispiel zeigt eine einfache Klasse, die über einige automatisch implementierte Eigenschaften
verfügt:

// This class is mutable. Its data can be modified from


// outside the class.
class Customer
{
// Auto-implemented properties for trivial get and set
public double TotalPurchases { get; set; }
public string Name { get; set; }
public int CustomerId { get; set; }

// Constructor
public Customer(double purchases, string name, int id)
{
TotalPurchases = purchases;
Name = name;
CustomerId = id;
}

// Methods
public string GetContactInfo() { return "ContactInfo"; }
public string GetTransactionHistory() { return "History"; }

// .. Additional methods, events, etc.


}

class Program
{
static void Main()
{
// Intialize a new object.
Customer cust1 = new Customer(4987.63, "Northwind", 90108);

// Modify a property.
cust1.TotalPurchases += 499.99;
}
}

Sie können keine automatisch implementierten Eigenschaften in Schnittstellen deklarieren. Automatisch


implementierte Eigenschaften deklarieren ein Unterstützungsfeld für die private Instanz. Schnittstellen
deklarieren möglicherweise keine Instanzfelder. Beim Deklarieren einer Eigenschaft in einer Schnittstelle, ohne
einen Text zu definieren, wird eine Eigenschaft mit Zugriffsmethoden deklariert, die von allen Typen
implementiert werden muss, die die Schnittstelle implementieren.
Ab C# 6 können Sie automatisch implementierte Eigenschaften ähnlich wie Felder initialisieren:

public string FirstName { get; set; } = "Jane";

Die im vorherigen Beispiel gezeigte Klasse kann geändert werden. Der Clientcode kann die Werte in Objekten
nach der Erstellung ändern. In komplexen Klassen mit signifikantem Verhalten (Methoden) sowie Daten, ist es
oft notwendig, öffentliche Eigenschaften zu haben. Für kleine Klassen oder Strukturen, die nur einen Satz von
Werten (Daten) kapseln und wenig oder kein Verhalten aufweisen, sollten Sie jedoch eine der folgenden
Optionen verwenden, um die Objekte unveränderlich zu machen:
Deklarieren Sie nur eine get -Zugriffsmethode (überall unveränderlich mit Ausnahme des Konstruktors).
Deklarieren Sie eine get -Zugriffsmethode und eine init -Zugriffsmethode (überall unveränderlich außer
während der Objekterstellung).
Deklarieren Sie die set -Zugriffsmethode als privat (unveränderlich für Consumer).

Weitere Informationen finden Sie unter Vorgehensweise: Implementieren einer einfachen Klasse mit
automatisch implementierten Eigenschaften.

Siehe auch
Eigenschaften
Modifizierer
Gewusst wie: Implementieren einer einfachen Klasse
mit automatisch implementierten Eigenschaften
(C#-Programmierhandbuch)
04.11.2021 • 3 minutes to read

In diesem Beispiel wird das Erstellen einer unveränderlichen einfachen Klasse gezeigt, die nur zum Kapseln einer
Reihe von automatisch implementierten Eigenschaften dient. Verwenden Sie diese Konstruktart anstelle einer
Struktur, wenn Sie eine Verweistypsemantik verwenden müssen.
Für das Erstellen einer unveränderlichen Eigenschaft gibt es die folgenden Möglichkeiten:
Deklarieren Sie nur die get-Zugriffsmethode. Auf diese Weise wird die Eigenschaft überall (außer im
Konstruktor des Typs) unveränderlich.
Deklarieren Sie eine init-Zugriffsmethode anstelle einer set -Zugriffsmethode. Die Eigenschaft kann
dann nur im Konstruktor oder mithilfe eines Objektinitialisierers festgelegt werden.
Deklarieren Sie die set-Zugriffsmethode als privat. Die Eigenschaft kann im Typ festgelegt werden, ist
aber für Consumer unveränderlich.
Beim Deklarieren eines privaten set -Accessors können Sie einen Objektinitialisierer nicht verwenden,
um die Eigenschaft zu initialisieren. Sie müssen eine Konstruktor oder eine Factorymethode verwenden.
Das folgende Beispiel zeigt, wie eine Eigenschaft, die nur eine get-Zugriffsmethode aufweist, sich von einer
Eigenschaft mit „get“ und „private set“ unterscheidet.

class Contact
{
public string Name { get; }
public string Address { get; private set; }

public Contact(string contactName, string contactAddress)


{
// Both properties are accessible in the constructor.
Name = contactName;
Address = contactAddress;
}

// Name isn't assignable here. This will generate a compile error.


//public void ChangeName(string newName) => Name = newName;

// Address is assignable here.


public void ChangeAddress(string newAddress) => Address = newAddress;
}

Beispiel
Im folgenden Beispiel werden zwei Möglichkeiten für die Implementierung einer unveränderlichen Klasse
gezeigt, die über automatisch implementierte Eigenschaften verfügt. Mit beiden Möglichkeiten wird jeweils eine
der Eigenschaften mit einer privaten set und eine der Eigenschaft mit einer get deklariert. Die erste Klasse
verwendet einen Konstruktor nur zum Initialisieren der Eigenschaften, und die zweite Klasse verwendet eine
statische Factorymethode, die einen Konstruktor aufruft.
// This class is immutable. After an object is created,
// it cannot be modified from outside the class. It uses a
// constructor to initialize its properties.
class Contact
{
// Read-only property.
public string Name { get; }

// Read-write property with a private set accessor.


public string Address { get; private set; }

// Public constructor.
public Contact(string contactName, string contactAddress)
{
Name = contactName;
Address = contactAddress;
}
}

// This class is immutable. After an object is created,


// it cannot be modified from outside the class. It uses a
// static method and private constructor to initialize its properties.
public class Contact2
{
// Read-write property with a private set accessor.
public string Name { get; private set; }

// Read-only property.
public string Address { get; }

// Private constructor.
private Contact2(string contactName, string contactAddress)
{
Name = contactName;
Address = contactAddress;
}

// Public factory method.


public static Contact2 CreateContact(string name, string address)
{
return new Contact2(name, address);
}
}

public class Program


{
static void Main()
{
// Some simple data sources.
string[] names = {"Terry Adams","Fadi Fakhouri", "Hanying Feng",
"Cesar Garcia", "Debra Garcia"};
string[] addresses = {"123 Main St.", "345 Cypress Ave.", "678 1st Ave",
"12 108th St.", "89 E. 42nd St."};

// Simple query to demonstrate object creation in select clause.


// Create Contact objects by using a constructor.
var query1 = from i in Enumerable.Range(0, 5)
select new Contact(names[i], addresses[i]);

// List elements cannot be modified by client code.


var list = query1.ToList();
foreach (var contact in list)
{
Console.WriteLine("{0}, {1}", contact.Name, contact.Address);
}

// Create Contact2 objects by using a static factory method.


var query2 = from i in Enumerable.Range(0, 5)
select Contact2.CreateContact(names[i], addresses[i]);
select Contact2.CreateContact(names[i], addresses[i]);

// Console output is identical to query1.


var list2 = query2.ToList();

// List elements cannot be modified by client code.


// CS0272:
// list2[0].Name = "Eugene Zabokritski";

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

/* Output:
Terry Adams, 123 Main St.
Fadi Fakhouri, 345 Cypress Ave.
Hanying Feng, 678 1st Ave
Cesar Garcia, 12 108th St.
Debra Garcia, 89 E. 42nd St.
*/

Der Compiler erstellt Unterstützungsfelder für jede automatisch implementierte Eigenschaft. Es ist nicht
möglich, über den Quellcode direkt auf die Felder zuzugreifen.

Siehe auch
Eigenschaften
struct
Objekt- und Auflistungsinitialisierer
Methoden (C#-Programmierhandbuch)
04.11.2021 • 10 minutes to read

Eine Methode ist ein Codeblock, der eine Reihe von Anweisungen enthält. Ein Programm bewirkt die
Ausführung der Anweisungen, indem die Methode aufgerufen wird und alle erforderlichen
Methodenargumente angegeben werden. In C# werden alle Anweisungen im Kontext einer Methode ausgeführt.
Die Methode Main ist der Einstiegspunkt jeder C#-Anwendung und wird beim Start des Programms von der
Common Language Runtime (CLR) aufgerufen. In einer Anwendung, die Anweisungen auf oberster Ebene
verwendet, wird die Methode Main vom Compiler generiert und enthält alle Anweisungen auf oberster Ebene.

NOTE
In diesem Artikel werden benannte Methoden erläutert. Weitere Informationen zu anonymen Funktionen finden Sie unter
Lambdaausdrücke.

Methodensignaturen
Methoden werden in einer Klasse, Struktur oder Schnittstelle deklariert, indem die Zugriffsebene wie z. B.
public oder private , optionale Modifizierer wie z. B. abstract oder sealed , der Rückgabewert, der Name
der Methode und die Methodenparameter angegeben werden. Diese Teile bilden zusammen die Signatur der
Methode.

IMPORTANT
Ein Rückgabetyp einer Methode ist nicht Teil der Signatur der Methode, wenn es um die Methodenüberladung geht. Er ist
jedoch Teil der Methodensignatur, wenn die Kompatibilität zwischen einem Delegaten und der Methode bestimmt wird,
auf die dieser verweist.

Methodenparameter werden in Klammern eingeschlossen und durch Kommas getrennt. Leere Klammern geben
an, dass für die Methode keine Parameter erforderlich sind. Diese Klasse enthält vier Methoden:

abstract class Motorcycle


{
// Anyone can call this.
public void StartEngine() {/* Method statements here */ }

// Only derived classes can call this.


protected void AddGas(int gallons) { /* Method statements here */ }

// Derived classes can override the base class implementation.


public virtual int Drive(int miles, int speed) { /* Method statements here */ return 1; }

// Derived classes must implement this.


public abstract double GetTopSpeed();
}

Methodenzugriff
Das Aufrufen einer Methode für ein Objekt ähnelt dem Zugreifen auf ein Feld. Fügen Sie nach dem
Objektnamen einen Punkt, den Namen der Methode und Klammern hinzu. Argumente werden innerhalb der
Klammern aufgelistet und durch Kommas getrennt. Die Methoden der Motorcycle -Klasse können also wie im
folgenden Beispiel gezeigt aufgerufen werden:

class TestMotorcycle : Motorcycle


{

public override double GetTopSpeed()


{
return 108.4;
}

static void Main()


{

TestMotorcycle moto = new TestMotorcycle();

moto.StartEngine();
moto.AddGas(15);
moto.Drive(5, 20);
double speed = moto.GetTopSpeed();
Console.WriteLine("My top speed is {0}", speed);
}
}

Methodenparameter und Argumente


Die Methodendefinition gibt die Namen und Typen aller ggf. erforderlichen Parameter an. Wenn aufrufender
Code die Methode aufruft, werden für jeden Parameter konkrete Werte bereitgestellt, die als Argumente
bezeichnet werden. Die Argumente müssen mit dem Parametertyp kompatibel sein, aber der im aufrufenden
Code verwendete Name des Arguments (sofern vorhanden) muss nicht mit dem in der Methode definierten
Parameternamen identisch sein. Zum Beispiel:

public void Caller()


{
int numA = 4;
// Call with an int variable.
int productA = Square(numA);

int numB = 32;


// Call with another int variable.
int productB = Square(numB);

// Call with an integer literal.


int productC = Square(12);

// Call with an expression that evaulates to int.


productC = Square(productA * 3);
}

int Square(int i)
{
// Store input argument in a local variable.
int input = i;
return input * input;
}

Übergeben nach Verweis und Übergeben nach Wert


Wenn eine Instanz eines Werttyps an eine Methode übergeben wird, wird standardmäßig die zugehörige Kopie
anstelle der Instanz selbst übermittelt. Änderungen am Argument haben daher keine Auswirkungen auf die
originale Instanz in der aufrufenden Methode. Verwenden Sie das Schlüsselwort ref , um eine Werttypinstanz
als Verweis zu übergeben. Weitere Informationen finden Sie unter Übergeben von Werttypparametern.
Wenn ein Objekt eines Verweistyps an eine Methode übergeben wird, wird ein Verweis auf das Objekt
übergeben. Das heißt, die Methode erhält nicht das Objekt selbst, sondern ein Argument, das den Speicherort
des Objekts angibt. Wenn Sie einen Member des Objekts unter Verwendung dieses Verweises ändern, wird die
Änderung im Argument in der aufrufenden Methode berücksichtigt, selbst wenn Sie das Objekt als Wert
übergeben.
Sie erstellen einen Verweistyp mithilfe des class -Schlüsselworts, wie im folgenden Beispiel gezeigt:

public class SampleRefType


{
public int value;
}

Wenn Sie jetzt ein Objekt, das auf diesem Typ basiert, an eine Methode übergeben, wird ein Verweis auf das
Objekt übergeben. Das folgende Beispiel übergibt ein Objekt des SampleRefType -Typs an die ModifyObject -
Methode:

public static void TestRefType()


{
SampleRefType rt = new SampleRefType();
rt.value = 44;
ModifyObject(rt);
Console.WriteLine(rt.value);
}

static void ModifyObject(SampleRefType obj)


{
obj.value = 33;
}

Das Beispiel entspricht im Wesentlichen dem vorherigen Beispiel und übergibt ein Argument als Wert an eine
Methode. Aber da ein Verweistyp verwendet wird, unterscheidet sich das Ergebnis. Die Änderung, die in
ModifyObject am value -Feld des Parameters ( obj ) vorgenommen wird, ändert auch das value -Feld des
Arguments ( rt ) in der TestRefType -Methode. Die TestRefType -Methode zeigt 33 als Ausgabe an.
Weitere Informationen zum Übergeben von Verweistypen als Verweis oder als Wert finden Sie unter Übergeben
von Verweistypparametern und Verweistypen.

Rückgabewerte
Methoden können einen Wert an die aufrufende Funktion (den Aufrufer) zurückgeben. Wenn der Rückgabetyp
(der vor dem Methodennamen aufgeführte Typ) nicht void ist, kann die Methode den Wert mithilfe des
return -Schlüsselworts zurückgeben. Eine Anweisung mit der return -Schlüsselwort, gefolgt von einem Wert,
der dem Rückgabetyp entspricht, gibt diesen Wert an den Methodenaufrufer zurück.
Der Wert kann an den Aufrufer nach Wert oder, ab C# 7.0, nach Verweis zurückgegeben werden. Werte werden
an den Aufrufer nach Verweis zurückgegeben, wenn das Schlüsselwort ref in der Methodensignatur
verwendet wird und auf jedes return -Schlüsselwort folgt. Die folgende Methodensignatur und
Rückgabeanweisung geben an, dass die Methode eine Variable mit dem Namen estDistance nach Verweis an
den Aufrufer zurückgibt.
public ref double GetEstimatedDistance()
{
return ref estDistance;
}

Das return -Schlüsselwort beendet außerdem die Ausführung der Methode. Wenn der Rückgabetyp void ist,
ist eine return -Anweisung ohne Wert immer noch nützlich, um die Ausführung der Methode zu beenden.
Ohne das return -Schlüsselwort wird die Ausführung der Methode beendet, wenn das Ende des Codeblocks
erreicht ist. Methoden mit einem anderen Rückgabetyp als „void“ müssen das return -Schlüsselwort
verwenden, um einen Wert zurückzugeben. Die folgenden beiden Methoden verwenden z. B. das return -
Schlüsselwort, um ganze Zahlen zurückzugeben:

class SimpleMath
{
public int AddTwoNumbers(int number1, int number2)
{
return number1 + number2;
}

public int SquareANumber(int number)


{
return number * number;
}
}

Um einen von einer Methode zurückgegebenen Wert zu verwenden, kann die aufrufende Methode den
Methodenaufruf selbst an jeder Stelle verwenden, an der ein Wert des gleichen Typs ausreichend ist. Sie können
den Rückgabewert auch einer Variablen zuweisen. Beispielsweise wird mit den folgenden beiden Codebeispiele
das gleiche Ergebnis erzielt:

int result = obj.AddTwoNumbers(1, 2);


result = obj.SquareANumber(result);
// The result is 9.
Console.WriteLine(result);

result = obj.SquareANumber(obj.AddTwoNumbers(1, 2));


// The result is 9.
Console.WriteLine(result);

Die Verwendung einer lokalen Variablen, in diesem Fall result , zum Speichern eines Werts ist optional. Es kann
die Lesbarkeit des Codes verbessern, oder es kann notwendig sein, wenn Sie den ursprünglichen Wert des
Arguments für den gesamten Gültigkeitsbereich der Methode speichern müssen.
Um einen Wert zu verwenden, der nach Verweis von einer Methode zurückgegeben wurde, müssen Sie die
Variable ref local deklarieren, wenn Sie diesen Wert modifizieren möchten. Wenn die
Planet.GetEstimatedDistance -Methode z.B. einen Double-Wert nach Verweis zurückgibt, können Sie ihn mit
Code wie dem folgenden als ref local-Variable definieren:

ref int distance = Planet.GetEstimatedDistance();

Die Zurückgabe eines mehrdimensionalen Arrays aus einer Methode, M , die den Inhalt eines Arrays modifiziert,
ist nicht vonnöten, wenn die aufrufende Funktion das Array an M übergeben hat. Sie können das resultierende
Array aus M für den funktionalen Fluss der Werte oder aus stilistischen Gründen zurückgeben. Dies ist jedoch
nicht nötig, da C# alle Referenztypen nach Wert übergibt und der Wert eines Arrayverweises ein Zeiger auf das
Array ist. In der Methode M werden Änderungen am Inhalt des Arrays durch jeden Code berücksichtigt, der
einen Verweis auf das Array enthält, wie im folgenden Beispiel gezeigt:

static void Main(string[] args)


{
int[,] matrix = new int[2, 2];
FillMatrix(matrix);
// matrix is now full of -1
}

public static void FillMatrix(int[,] matrix)


{
for (int i = 0; i < matrix.GetLength(0); i++)
{
for (int j = 0; j < matrix.GetLength(1); j++)
{
matrix[i, j] = -1;
}
}
}

Weitere Informationen finden Sie unter return.

Asynchrone Methoden
Mithilfe der Async-Funktion können Sie asynchrone Methoden aufrufen, ohne explizite Rückrufe verwenden
oder den Code manuell über mehrere Methoden oder Lambda-Ausdrücke teilen zu müssen.
Wenn Sie eine Methode mit dem Modifizierer async kennzeichnen, können Sie den Operator await in der
Methode verwenden. Wenn ein await-Ausdruck in der asynchronen Methode erreicht wird, wird die Steuerung
an den Aufrufer zurückgegeben, und die Ausführung der Methode wird angehalten, bis die erwartete Aufgabe
abgeschlossen ist. Wenn die Aufgabe abgeschlossen ist, kann die Ausführung in der Methode fortgesetzt
werden.

NOTE
Eine asynchrone Methode wird an den Aufrufer zurückgegeben, wenn sie entweder auf das erste erwartete Objekt trifft,
das noch nicht abgeschlossen wurde, oder das Ende der asynchronen Methode erreicht.

Eine asynchrone Methode weist üblicherweise den Rückgabetyp Task<TResult>, Task, IAsyncEnumerable<T>
oder void auf. Der Rückgabetyp void wird hauptsächlich zum Definieren von Ereignishandlern verwendet,
wobei ein void -Rückgabetyp erforderlich ist. Auf eine asynchrone Methode, die void zurückgibt, kann nicht
gewartet werden, und der Aufrufer einer Methode mit void-Rückgabe kann keine Ausnahmen abfangen, die die
Methode auslöst. Ab C# 7.0 kann eine asynchrone Methode jeden Task-ähnlichen Typ zurückgeben.
Im folgenden Beispiel ist DelayAsync eine asynchrone Methode mit dem Rückgabetyp Task<TResult>.
DelayAsync enthält eine return -Anweisung, die eine ganze Zahl zurückgibt. Aus diesem Grund muss die
Methodendeklaration von DelayAsync den Rückgabetyp Task<int> haben. Da der Rückgabetyp Task<int> ist,
ergibt die Auswertung des await -Ausdrucks in DoSomethingAsync eine ganze Zahl, wie die folgende Anweisung
veranschaulicht: int result = await delayTask .
Die Main-Methode ist ein Beispiel für eine asynchrone Methode mit dem Task-Rückgabetyp. Es folgt die
DoSomethingAsync -Methode, und da sie mit einer einzelnen Zeile ausgedrückt wird, können Sie die
Schlüsselwörter async und await weglassen. Da DoSomethingAsync eine asynchrone Methode ist, muss die
Aufgabe für den Aufruf von DoSomethingAsync abgewartet werden, wie in der folgenden Anweisung dargestellt:
await DoSomethingAsync(); .
using System;
using System.Threading.Tasks;

class Program
{
static Task Main() => DoSomethingAsync();

static async Task DoSomethingAsync()


{
Task<int> delayTask = DelayAsync();
int result = await delayTask;

// The previous two statements may be combined into


// the following statement.
//int result = await DelayAsync();

Console.WriteLine($"Result: {result}");
}

static async Task<int> DelayAsync()


{
await Task.Delay(100);
return 5;
}
}
// Example output:
// Result: 5

Mit einer asynchronen Methode können keine ref - oder out -Parameter deklariert, jedoch Methoden aufgerufen
werden, die solche Parameter aufweisen.
Weitere Informationen zu den asynchronen Methoden finden Sie unter Asynchrone Programmierung mit async
und await und Asynchrone Rückgabetypen (C#).

Ausdruckstextdefinitionen
Es gibt häufig Methodendefinitionen, die einfach direkt das Ergebnis eines Ausdrucks zurückgeben oder eine
einzige Anweisung als Text der Methode aufweisen. Es ist eine Syntaxabkürzung zur Definition solcher
Methoden mithilfe von => verfügbar:

public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
public void Print() => Console.WriteLine(First + " " + Last);
// Works with operators, properties, and indexers too.
public static Complex operator +(Complex a, Complex b) => a.Add(b);
public string Name => First + " " + Last;
public Customer this[long id] => store.LookupCustomer(id);

Wenn die Methode void zurückgibt oder es sich um eine asynchrone Methode handelt, muss der Text der
Methode ein Anweisungsausdruck sein (wie bei Lambdas). Eigenschaften und Indexer müssen schreibgeschützt
sein. Verwenden Sie darüber hinaus nicht das get -Accessorschlüsselwort.

Iterators
Ein Iterator führt eine benutzerdefinierte Iteration durch eine Auflistung durch, z. B. eine Liste oder ein Array. Ein
Iterator verwendet die yield return -Anweisung, um jedes Element einzeln nacheinander zurückzugeben. Wenn
eine yield return -Anweisung erreicht wird, wird die aktuelle Position im Code gespeichert. Wenn der Iterator
das nächste Mal aufgerufen wird, wird die Ausführung von dieser Position neu gestartet.
Sie rufen einen Iterator im Clientcode mithilfe einer foreach Anweisung auf.
Der Rückgabetyp eines Iterators kann IEnumerable, IEnumerable<T>, IEnumerator oder IEnumerator<T> sein.
Weitere Informationen finden Sie unter Iteratoren.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Zugriffsmodifizierer
Statische Klassen und statische Klassenmember
Vererbung
Abstrakte und versiegelte Klassen und Klassenmember
params
return
out
ref
Übergeben von Parametern
Lokale Funktionen (C#-Programmierhandbuch)
04.11.2021 • 9 minutes to read

Ab C#-7.0 unterstützt C# lokale Funktionen. Lokale Funktionen sind private Methoden eines Typs, die in einem
anderen Member geschachtelt sind. Sie können nur aus ihrem enthaltenden Member aufgerufen werden. Lokale
Funktionen können deklariert und aufgerufen werden aus:
Methoden, insbesondere Iteratormethoden und Async-Methoden
Konstruktoren
Eigenschaftenaccessoren
Ereignisaccessoren
Anonymen Methoden
Lambdaausdrücke
Finalizer
Anderen lokalen Funktionen
Lokale Funktionen können jedoch nicht in einem Ausdruckskörpermember deklariert werden.

NOTE
In einigen Fällen können Sie einen Lambdaausdruck zum Implementieren von Funktionen verwenden, die auch von einer
lokalen Funktion unterstützt werden. Einen Vergleich finden Sie unter Lokale Funktionen im Vergleich zu
Lambdaausdrücken.

Lokale Funktionen machen den Zweck Ihres Codes deutlich. Beim Lesen des Codes wird deutlich, dass die
Methode nur von der enthaltenden Methode aufgerufen werden kann. Bei Teamprojekten wird auch verhindert,
dass ein anderer Entwickler die Methode versehentlich direkt an anderer Stelle in der Klasse oder Struktur
aufruft.

Syntax einer lokalen Funktion


Eine lokale Funktion wird definiert als eine geschachtelte Methode in einem enthaltenden Member. Ihre
Definition besitzt die folgende Syntax:

<modifiers> <return-type> <method-name> <parameter-list>

Sie können die folgenden Modifizierer mit einer lokalen Funktion verwenden:
async
unsafe
static (in C# 8.0 und höher). Eine statische lokale Funktion kann keine lokalen Variablen oder den
Instanzzustand erfassen.
extern (in C# 9.0 und höher). Eine externe lokale Funktion muss static sein.

Alle im enthaltenden Member definierten lokalen Variablen, einschließlich der Methodenparameter, sind in einer
nicht statischen lokalen Funktion zugänglich.
Im Gegensatz zu einer Methodendefinition kann die Definition einer lokalen Funktion keinen
Memberzugriffsmodifizierer enthalten. Da alle lokale Funktionen privat sind, generiert das Verwenden eines
Zugriffsmodifizierers wie etwa das Schlüsselwort private den Compilerfehler CS0106 „Der Modifizierer
‚private‘ ist für dieses Element nicht gültig“.
Das folgende Beispiel definiert eine lokale Funktion mit dem Namen AppendPathSeparator , die für eine Methode
mit dem Namen GetText privat ist:

private static string GetText(string path, string filename)


{
var reader = File.OpenText($"{AppendPathSeparator(path)}{filename}");
var text = reader.ReadToEnd();
return text;

string AppendPathSeparator(string filepath)


{
return filepath.EndsWith(@"\") ? filepath : filepath + @"\";
}
}

Ab C# 9.0 können Sie Attribute auf eine lokale Funktion, ihre Parameter und Typparameter anwenden. Sehen Sie
sich dazu das folgende Beispiel an:

#nullable enable
private static void Process(string?[] lines, string mark)
{
foreach (var line in lines)
{
if (IsValid(line))
{
// Processing logic...
}
}

bool IsValid([NotNullWhen(true)] string? line)


{
return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
}
}

Im vorherigen Beispiel wird ein spezielles Attribut verwendet, um den Compiler bei der statischen Analyse in
einem Nullable-Kontext zu unterstützen.

Lokale Funktionen und Ausnahmen


Eine nützliche Funktion von lokalen Funktionen ist die Tatsache, dass sie Ausnahmen sofort verfügbar machen
können. Bei Methodeniteratoren werden Ausnahmen erst eingeblendet, wenn die zurückgegebene Sequenz
aufgelistet wird, und nicht, wenn der Iterator abgerufen wird. Bei async-Methoden werden Ausnahmen
festgestellt, wenn die zurückgegebene Aufgabe erwartet wird.
Das folgende Beispiel definiert eine OddSequence -Methode, die ungerade Zahlen in einem angegebenen Bereich
aufzählt. Da eine Zahl größer als 100 an die OddSequence -Enumeratormethode übergeben wird, wird
ArgumentOutOfRangeException ausgelöst. Die Ausgabe des Beispiels zeigt, dass die Ausnahme erst beim
Durchlaufen der Zahlen und nicht beim Abrufen des Enumerators eingeblendet wird.
using System;
using System.Collections.Generic;

public class IteratorWithoutLocalExample


{
public static void Main()
{
IEnumerable<int> xs = OddSequence(50, 110);
Console.WriteLine("Retrieved enumerator...");

foreach (var x in xs) // line 11


{
Console.Write($"{x} ");
}
}

public static IEnumerable<int> OddSequence(int start, int end)


{
if (start < 0 || start > 99)
throw new ArgumentOutOfRangeException(nameof(start), "start must be between 0 and 99.");
if (end > 100)
throw new ArgumentOutOfRangeException(nameof(end), "end must be less than or equal to 100.");
if (start >= end)
throw new ArgumentException("start must be less than end.");

for (int i = start; i <= end; i++)


{
if (i % 2 == 1)
yield return i;
}
}
}
// The example displays the output like this:
//
// Retrieved enumerator...
// Unhandled exception. System.ArgumentOutOfRangeException: end must be less than or equal to 100.
(Parameter 'end')
// at IteratorWithoutLocalExample.OddSequence(Int32 start, Int32 end)+MoveNext() in
IteratorWithoutLocal.cs:line 22
// at IteratorWithoutLocalExample.Main() in IteratorWithoutLocal.cs:line 11

Wenn Sie Iteratorlogik in eine lokale Funktion platzieren, werden Ausnahmen bei der Argumentvalidierung
ausgelöst, wenn Sie den Enumerator abrufen. Sehen Sie sich dazu das folgende Beispiel an:
using System;
using System.Collections.Generic;

public class IteratorWithLocalExample


{
public static void Main()
{
IEnumerable<int> xs = OddSequence(50, 110); // line 8
Console.WriteLine("Retrieved enumerator...");

foreach (var x in xs)


{
Console.Write($"{x} ");
}
}

public static IEnumerable<int> OddSequence(int start, int end)


{
if (start < 0 || start > 99)
throw new ArgumentOutOfRangeException(nameof(start), "start must be between 0 and 99.");
if (end > 100)
throw new ArgumentOutOfRangeException(nameof(end), "end must be less than or equal to 100.");
if (start >= end)
throw new ArgumentException("start must be less than end.");

return GetOddSequenceEnumerator();

IEnumerable<int> GetOddSequenceEnumerator()
{
for (int i = start; i <= end; i++)
{
if (i % 2 == 1)
yield return i;
}
}
}
}
// The example displays the output like this:
//
// Unhandled exception. System.ArgumentOutOfRangeException: end must be less than or equal to 100.
(Parameter 'end')
// at IteratorWithLocalExample.OddSequence(Int32 start, Int32 end) in IteratorWithLocal.cs:line 22
// at IteratorWithLocalExample.Main() in IteratorWithLocal.cs:line 8

Lokale Funktionen im Vergleich zu Lambdaausdrücken


Auf den ersten Blick sind lokale Funktionen und Lambdaausdrücke sehr ähnlich. In vielen Fällen ist die
Entscheidung zwischen Lamdaausdrücken und lokalen Funktionen eine Frage des Formats und persönlicher
Präferenz. Es gibt allerdings tatsächliche Unterschiede, wann das eine oder das andere verwendet werden kann.
Diese sollten Ihnen bekannt sein.
Sehen wir uns die Unterschiede zwischen der Implementierungen des Fakultätsalgorithmus als lokale Funktion
und als Lambdaausdruck an. Dies ist die Version mit einer lokalen Funktion:

public static int LocalFunctionFactorial(int n)


{
return nthFactorial(n);

int nthFactorial(int number) => number < 2


? 1
: number * nthFactorial(number - 1);
}
Diese Version verwendet Lambdaausdrücke:

public static int LambdaFactorial(int n)


{
Func<int, int> nthFactorial = default(Func<int, int>);

nthFactorial = number => number < 2


? 1
: number * nthFactorial(number - 1);

return nthFactorial(n);
}

Benennung
Lokale Funktionen werden explizit wie Methoden benannt. Lambdaausdrücke sind anonyme Methoden und
müssen Variablen eines delegate -Typs zugewiesen werden. In der Regel handelt es sich entweder um Action -
oder Func -Typen. Die Deklaration einer lokalen Funktion erfolgt so ähnlich wie das Schreiben einer normalen
Methode. Sie müssen dazu einen Rückgabetyp und eine Funktionssignatur deklarieren.
Funktionssignaturen und Typen für Lambdaausdrücke
Beim Bestimmen der Argument- und Rückgabetypen sind Lambdaausdrücke auf den Typ der Action / Func -
Variablen angewiesen, der sie zugewiesen werden. Da die Syntax in lokalen Funktionen stark einer normalen
Methode ähnelt, sind die Argumenttypen und der Rückgabetyp bereits Teil der Funktionsdeklaration.
Definite assignment (Festgelegte Zuweisung)
Lambdaausdrücke sind Objekte, die zur Laufzeit deklariert und zugewiesen werden. Damit ein Lambdaausdruck
verwendet werden kann, muss er definitiv zugewiesen werden: die Action / Func -Variable, der er zugewiesen
wird, muss deklariert werden. Anschließend muss der Lambdaausdruck der Variablen zugewiesen werden.
Beachten Sie, dass LambdaFactorial den Lambdaausdruck nthFactorial deklarieren und initialisieren muss,
bevor dieser definiert wird. Wird das nicht gemacht, führt dies zu einem Kompilierzeitfehler, weil auf
nthFactorial verwiesen wurde, bevor es zugewiesen wurde.

Lokale Funktionen werden zur Kompilierzeit definiert. Da sie keinen Variablen zugewiesen werden, kann an
jeder Stelle im Code innerhalb des Gültigkeitsbereichs der Funktion darauf verwiesen werden. Im ersten
Beispiel LocalFunctionFactorial konnten Sie die lokale Funktion entweder oberhalb oder unterhalb der return
-Anweisung deklarieren, ohne Compilerfehler auszulösen.
Diese Unterschiede bedeuten, dass rekursive Algorithmen mit lokalen Funktionen leichter erstellt werden
können. Sie können eine lokale Funktion deklarieren und definieren, die sich selbst aufruft. Lambdaausdrücke
müssen deklariert werden, und dann muss ihnen ein Standardwert zugewiesen werden, bevor sie erneut einem
Text zugewiesen werden können, der auf den gleichen Lambdaausdruck verweist.
Implementierung als Delegat
Lambdaausdrücke werden bei der Deklaration in Delegate konvertiert. Lokale Funktionen sind flexibler, da sie
wie eine herkömmliche Methode oder als Delegat geschrieben werden können. Lokale Funktionen werden nur
dann in Delegate konvertiert, wenn sie als Delegate ver wendet werden.
Wenn Sie eine lokale Funktion deklarieren und nur darauf verweisen, indem Sie sie wie eine Methode aufrufen,
wird sie nicht in einen Delegaten konvertiert.
Erfassung von Variablen
Die Regeln für definitive Zuweisungen gelten auch für alle Variablen, die von der lokalen Funktion oder dem
Lambdaausdruck erfasst werden. Zudem kann der Compiler statische Analysen durchführen, mit denen lokale
Funktionen erfasste Variablen im einschließenden Gültigkeitsbereich definitiv zuweisen können. Betrachten Sie
das folgende Beispiel:
int M()
{
int y;
LocalFunction();
return y;

void LocalFunction() => y = 0;


}

Der Compiler kann festlegen, dass LocalFunction``y bei Aufruf definitiv zuweist. Da LocalFunction vor der
return -Anweisung aufgerufen wird, wird y definitiv bei der return -Anweisung zugewiesen.

Beachten Sie, dass die lokale Funktion als Delegattyp implementiert wird, wenn diese lokale Funktion Variablen
im einschließenden Gültigkeitsbereich erfasst.
Heapzuweisungen
Je nach Verwendung können lokale Funktionen Heapzuweisungen vermeiden, die immer für Lambdaausdrücke
erforderlich sind. Wenn eine lokale Funktion nie in einen Delegaten konvertiert wird und keine der von der
lokalen Funktion erfassten Variablen von anderen Lambdaausdrücken oder lokalen Funktionen, die in Delegate
konvertiert werden, erfasst wird, kann der Compiler Heapzuweisungen vermeiden.
Betrachten Sie das folgende asynchrone Beispiel:

public async Task<string> PerformLongRunningWorkLambda(string address, int index, string name)


{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-
negative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

Func<Task<string>> longRunningWorkImplementation = async () =>


{
var interimResult = await FirstWork(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}. Enjoy.";
};

return await longRunningWorkImplementation();


}

Der Abschluss dieses Lambdaausdrucks enthält die Variablen address , index und name . Im Fall von lokalen
Funktionen ist das Objekt, das den Abschluss implementiert, möglicherweise vom Typ struct . Dieser struct-Typ
würde per Verweis an die lokale Funktion übergeben. Dieser Unterschied bei der Implementierung würde bei
einer Zuweisung gespart.
Die für Lambdaausdrücke erforderliche Instanziierung bedeutet zusätzliche Speicherbelegung, was ein
Leistungsfaktor in zeitkritischen Codepfaden sein kann. Lokale Funktionen erfordern diesen Mehraufwand nicht.
Im obigen Beispiel hat die Version mit der lokalen Funktion zwei Zuordnungen weniger als die Version mit dem
Lambdaausdruck.
Wenn Sie wissen, dass Ihre lokale Funktion nicht in einen Delegaten konvertiert wird und dass keine der von ihr
erfassten Variablen auch von anderen Lambdaausdrücken oder lokalen Funktionen, die in Delegate konvertiert
werden, erfasst wird, können Sie durch Deklarieren der lokalen Funktion als statisch ( static ) verhindern, dass
Ihre lokale Funktion im Heap zugewiesen wird. Beachten Sie, dass dieses Feature in C# 8.0 und höher verfügbar
ist.
NOTE
Die Entsprechung dieser Methode mit der lokalen Funktion verwendet auch eine Klasse für den Abschluss. Ob der
Abschluss für eine lokale Funktion als class oder struct implementiert wird, ist ein Implementierungsdetail. Eine
lokale Funktion verwendet möglicherweise struct , während ein Lambdaausdruck immer class nutzt.

public async Task<string> PerformLongRunningWork(string address, int index, string name)


{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-
negative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

return await longRunningWorkImplementation();

async Task<string> longRunningWorkImplementation()


{
var interimResult = await FirstWork(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}. Enjoy.";
}
}

Verwendung des Schlüsselworts yield

Eine letzter Vorteil, der in diesem Beispiel zu kurz gekommen ist, besteht darin, dass lokale Funktionen mithilfe
der yield return -Syntax als Iteratoren implementiert werden können, um eine Sequenz von Werten zu
erzeugen.

public IEnumerable<string> SequenceToLowercase(IEnumerable<string> input)


{
if (!input.Any())
{
throw new ArgumentException("There are no items to convert to lowercase.");
}

return LowercaseIterator();

IEnumerable<string> LowercaseIterator()
{
foreach (var output in input.Select(item => item.ToLower()))
{
yield return output;
}
}
}

Die yield return -Anweisung ist in Lambdaausdrücken unzulässig. Weitere Informationen finden Sie unter
Compilerfehler CS1621.
Während lokale Funktionen für Lambdaausdrücke als überflüssig erscheinen, dienen sie tatsächlich anderen
Zwecken und haben unterschiedliche Verwendungen. Lokale Funktionen sind effizienter, im Fall dass Sie eine
Funktion schreiben möchten, die nur aus dem Kontext einer anderen Methode abgerufen wird.

Siehe auch
Methoden
Ref-Rückgaben und lokale ref-Variablen
04.11.2021 • 7 minutes to read

Ab C# 7.0 unterstützt C# Verweisrückgabewerte (ref-Rückgaben). Mit einem Verweisrückgabewert kann eine


Methode einen Verweis auf eine Variable statt eines Werts an eine aufrufende Funktion zurückgeben. Die
aufrufende Funktion kann dann wählen, ob sie die zurückgegebene Variable behandelt, als wäre sie als Wert
oder als Verweis zurückgegeben worden. Die aufrufende Funktion kann eine neue Variable erstellen, die selbst
einen Verweis auf den zurückgegebenen Wert (als lokaler Verweis bezeichnet) darstellt.

Was ist ein Verweisrückgabewert?


Die meisten Entwickler sind mit dem Übergeben eines Arguments als Verweis an eine aufgerufene Methode
vertraut. Die Argumentliste einer aufgerufenen Methode enthält eine Variable, die als Verweis übergeben wird.
Änderungen, die an diesem Wert von der aufgerufenen Methode vorgenommen werden, werden vom Aufrufer
überwacht. Ein Verweisrückgabewert bedeutet, dass eine Methode einen Verweis (oder Alias) an einige
Variablen zurückgibt. Der Geltungsbereich der Variable muss die Methode enthalten. Die Lebensdauer dieser
Variable muss über die Rückgabe der Methode hinausgehen. Änderungen am Methodenrückgabewert durch die
aufrufende Funktion werden auf die Variable angewendet, die von der Methode zurückgegeben wird.
Die Deklaration der Rückgabe eines Verweisrückgabewerts durch eine Methode weist darauf hin, dass die
Methode einen Alias an eine Variable zurückgibt. Das Entwurfsziel dahinter besteht häufig darin, dem
aufrufenden Code über den Alias Zugriff auf diese Variable sowie die Möglichkeit zu deren Änderung zu
gewähren. Daraus folgt, dass vom Verweis zurückgegebene Methoden nicht den Rückgabetyp void aufweisen
dürfen.
Es gibt einige Einschränkungen für den Ausdruck, den eine Methode als Verweisrückgabewert zurückgeben
kann. Es gelten folgende Beschränkungen:
Der Rückgabewert muss eine Lebensdauer aufweisen, die über die Ausführung der Methode hinausgeht.
Mit anderen Worten: Er darf in der Methode, die diesen zurückgibt, keine lokale Variable sein. Es kann
sich dabei um eine Instanz oder ein statisches Feld einer Klasse oder um ein Argument handeln, das an
die Methode übergeben wurde. Beim Versuch, eine lokale Variable zurückzugeben, tritt der
Compilerfehler CS8168 „Cannot return local 'obj' by reference because it is not a ref local.“ (Der lokale
Wert „obj“ kann nicht als Verweis zurückgegeben werden, da er kein lokaler ref-Wert ist.) auf.
Der Rückgabewert darf nicht das null -Literal sein. Bei der Rückgabe von null tritt der Compilerfehler
CS8156 „Ein Ausdruck kann in diesem Kontext nicht verwendet werden, weil er ggf. nicht als Verweis
zurückgegeben wird.“ auf.
Eine Methode mit einer Verweisrückgabe kann einen Alias an eine Variable zurückgeben, deren Wert
derzeit NULL ist (nicht instanziiert) oder ein Nullable-Werttyp für einen Werttyp darstellt.
Der Rückgabewert darf keine Konstante, kein Enumerationsmember, nicht der by-value-Rückgabewert
einer Eigenschaft und keine Methode einer class oder struct sein. Wenn gegen diese Regel verstoßen
wird, tritt der Compilerfehler CS8156 „Ein Ausdruck kann in diesem Kontext nicht verwendet werden,
weil er ggf. nicht als Verweis zurückgegeben wird.“ auf.
Zusätzlich sind Verweisrückgabewerte nicht für asynchrone Methoden zulässig. Eine asynchrone Methode wird
möglicherweise zurückgegeben, bevor die Ausführung abgeschlossen ist, wobei der Rückgabewert noch
unbekannt ist.
Definieren eines ref-Rückgabewerts
Eine Methode, die einen Verweisrückgabewert zurückgibt, muss die beiden folgenden Bedingungen erfüllen:
Die Methodensignatur enthält das ref-Schlüsselwort vor den Rückgabetyp.
Jede return-Anweisung im Hauptteil der Methode enthält das ref-Schlüsselwort vor den Namen der
zurückgegebenen Instanz.
Das folgende Beispiel zeigt eine Methode, die die Bedingungen erfüllt und einen Verweis auf ein Person -Objekt
mit dem Namen p zurückgibt:

public ref Person GetContactInformation(string fname, string lname)


{
// ...method implementation...
return ref p;
}

Verarbeiten eines ref-Rückgabewerts


Der Verweisrückgabewert ist ein Alias für eine andere Variable im Bereich der aufgerufenen Methode. Sie
können jede Verwendung der Verweisrückgabe als Verwendung der Variable interpretieren, für die diese als
Alias fungiert:
Wenn Sie den jeweiligen Wert zuweisen, weisen Sie der Variable, für die diese als Alias fungiert, einen Wert
zu.
Wenn Sie dessen Wert lesen, lesen Sie den Wert der Variable, für die diese als Alias fungiert.
Wenn Sie diesen nach Verweis zurückgeben, geben Sie einen Alias für diese Variable zurück.
Wenn Sie ihn nach Verweis an eine andere Methode übergeben, übergeben Sie einen Verweis auf die
Variable, für die diese als Alias fungiert.
Wenn Sie einen Alias für einen lokalen Verweis erstellen, erstellen Sie einen neuen Alias für diese Variable.

Lokale ref-Variablen
Nehmen Sie an, dass die GetContactInformation -Methode als Verweisrückgabe deklariert ist:

public ref Person GetContactInformation(string fname, string lname)

Eine by-value-Zuweisung liest den Wert einer Variable und weist diesen einer neuen Variable zu:

Person p = contacts.GetContactInformation("Brandie", "Best");

Die vorangehende Zuweisung deklariert p als lokale Variable. Der ursprüngliche Wert wird durch Lesen des
Rückgabewerts kopiert, der von GetContactInformation zurückgegeben wurde. Keine der zukünftigen
Zuweisungen zu p ändern den Wert der von GetContactInformation zurückgegebenen Variable. Die Variable
p fungiert nicht mehr als Alias für die zurückgegebene Variable.

Sie deklarieren eine lokale Verweisvariable, um den Alias in den ursprünglichen Wert zu kopieren. In der
folgenden Zuweisung ist p ein Alias für die von GetContactInformation zurückgegebene Variable.

ref Person p = ref contacts.GetContactInformation("Brandie", "Best");

Die nachfolgende Verwendung von p ist mit der Verwendung der von GetContactInformation
zurückgegebenen Variable identisch, da p ein Alias für diese Variable darstellt. Durch Änderungen an p wird
auch die von GetContactInformation zurückgegebene Variable geändert.
Das Schlüsselwort ref wird sowohl vor der Deklaration lokaler Variablen als auch vor dem Methodenaufruf
verwendet.
Auch auf Werte können Sie per Verweis zugreifen. In einigen Fällen erhöht dies die Leistung, da ein
möglicherweise aufwendiger Kopiervorgang vermieden wird. In der folgenden Anweisung wird z.B. gezeigt, wie
ein lokaler Verweiswert definiert wird, mit dem auf einen Wert verwiesen wird.

ref VeryLargeStruct reflocal = ref veryLargeStruct;

Das Schlüsselwort ref wird sowohl vor der Deklaration lokaler Variablen als auch vor dem Wert im zweiten
Beispiel verwendet. Wenn nicht in beiden Beispielen beide ref -Schlüsselwörter in den Ergebnissen der
Variablendeklaration und der Zuweisung enthalten sind, tritt der Compilerfehler CS8172 "Cannot initialize a by-
reference variable with a value." (Eine by-reference-Variable kann nicht mit einem Wert initialisiert werden) auf.
Vor C# 7.3 konnten lokalen ref-Variablen nach der Initialisierung nicht neu zugewiesen werden, um auf einen
anderen Speicher zu verweisen. Diese Einschränkung wurde entfernt. Im folgenden Beispiel wird eine
Neuzuweisung veranschaulicht:

ref VeryLargeStruct reflocal = ref veryLargeStruct; // initialization


refLocal = ref anotherVeryLargeStruct; // reassigned, refLocal refers to different storage.

Lokale ref-Variablen müssen noch immer initialisiert werden, wenn sie deklariert werden.

Ref-Rückgaben und lokale ref-Variablen: ein Beispiel


Das folgende Beispiel definiert eine NumberStore -Klasse, die ein Array von Integer-Werten speichert. Die
FindNumber -Methode gibt die erste Anzahl, die größer als oder gleich der Anzahl ist, die als Argument
übergeben wurde, als Verweis zurück. Wenn keine Anzahl größer als oder gleich dem Argument ist, gibt die
Methode die Anzahl im Index 0 zurück.

using System;

class NumberStore
{
int[] numbers = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023 };

public ref int FindNumber(int target)


{
for (int ctr = 0; ctr < numbers.Length; ctr++)
{
if (numbers[ctr] >= target)
return ref numbers[ctr];
}
return ref numbers[0];
}

public override string ToString() => string.Join(" ", numbers);


}

Im folgenden Beispiel wird die NumberStore.FindNumber -Methode aufgerufen, um den ersten Wert abzurufen,
der größer als oder gleich 16 ist. Die aufrufende Funktion verdoppelt dann den von der Methode
zurückgegebenen Wert. Die Ausgabe aus dem Beispiel zeigt die Änderung, die im Wert der Arrayelemente der
NumberStore -Instanz wiedergegeben wird.
var store = new NumberStore();
Console.WriteLine($"Original sequence: {store.ToString()}");
int number = 16;
ref var value = ref store.FindNumber(number);
value *= 2;
Console.WriteLine($"New sequence: {store.ToString()}");
// The example displays the following output:
// Original sequence: 1 3 7 15 31 63 127 255 511 1023
// New sequence: 1 3 7 15 62 63 127 255 511 1023

Ohne Unterstützung für Verweisrückgabewerte wird ein solcher Vorgang durchgeführt, indem der Index des
Arrayelements zusammen mit seinem Wert zurückgegeben wird. Die aufrufende Funktion kann diesen Index
dann dazu verwenden, den Wert in einem separaten Methodenaufruf zu ändern. Die aufrufende Funktion kann
den Index jedoch auch ändern, um auf andere Arraywerte zuzugreifen und diese zu bearbeiten.
Im folgenden Beispiel wird gezeigt, wie die FindNumber -Methode nach C# 7.3 neu geschrieben werden kann,
damit sie die lokale ref-Neuzuweisung verwendet:

using System;

class NumberStore
{
int[] numbers = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023 };

public ref int FindNumber(int target)


{
ref int returnVal = ref numbers[0];
var ctr = numbers.Length - 1;
while ((ctr >= 0) && numbers[ctr] >= target)
{
returnVal = ref numbers[ctr];
ctr--;
}
return ref returnVal;
}

public override string ToString() => string.Join(" ", numbers);


}

Diese zweite Version ist effizienter mit längeren Sequenzen in Szenarios, in denen die gesuchte Zahl näher am
Ende des Arrays liegt, da das Array vom Ende zum Anfang hin iteriert wird, sodass weniger Elemente untersucht
werden.

Siehe auch
ref (C#-Referenz)
Schreiben von sicherem und effizientem Code
Übergeben von Parametern (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

In C# können Argumente an Parameter entweder als Wert oder als Verweis übergeben werden. Durch die
Übergabe als Verweis können Funktionsmember, Methoden, Eigenschaften, Indexer, Operatoren und
Konstruktoren den Wert der Parameter ändern und behalten diese Änderung in der aufrufenden Umgebung bei.
Wenn Sie den Wert ändern und darum den Parameter pro Verweis übergeben möchten, verwenden Sie die
Schlüsselwörter ref oder out . Wenn Sie den Wert zwar ändern, aber keine Kopie erstellen möchten, und
darum den Parameter pro Verweis übergeben möchten, verwenden Sie den Modifizierer in . Der Einfachheit
halber wird in den Beispielen in diesem Thema nur das Schlüsselwort ref verwendet. Weitere Informationen
zum Unterschied zwischen in , ref und out finden Sie unter in, ref und out.
Das folgende Beispiel veranschaulicht die Unterschiede zwischen Wert- und Verweisparametern.

class Program
{
static void Main(string[] args)
{
int arg;

// Passing by value.
// The value of arg in Main is not changed.
arg = 4;
squareVal(arg);
Console.WriteLine(arg);
// Output: 4

// Passing by reference.
// The value of arg in Main is changed.
arg = 4;
squareRef(ref arg);
Console.WriteLine(arg);
// Output: 16
}

static void squareVal(int valParameter)


{
valParameter *= valParameter;
}

// Passing by reference
static void squareRef(ref int refParameter)
{
refParameter *= refParameter;
}
}

Weitere Informationen finden Sie unter den folgenden Themen:


Übergeben von Werttypparametern
Übergeben von Verweistypparametern

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie in den Argumentlisten in der C#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Methoden
Übergeben von Werttypparametern (C#-
Programmierhandbuch)
04.11.2021 • 3 minutes to read

In einer Werttypvariablen sind die Daten direkt enthalten, während eine Verweistypvariable einen Verweis auf
die Daten enthält. Wenn eine Werttypvariable als Wert an eine Methode übergeben wird, bedeutet dies die
Übergabe einer Kopie der Variablen an die Methode. Alle Änderungen am Parameter, die innerhalb der Methode
erfolgen, haben keine Auswirkung auf die ursprünglichen Daten, die in der Argumentvariable gespeichert sind.
Wenn Sie möchten, dass mit der aufgerufenen Methode der Wert des Arguments geändert wird, müssen Sie ihn
als Verweis übergeben, unter Verwendung des Schlüsselworts ref oder out. Sie können auch das Schlüsselwort
in verwenden, um einen Wertparameter pro Verweis zu übergeben, um einerseits zwar eine Kopie zu
vermeiden, andererseits aber zu garantieren, dass der Wert nicht verändert wird. Der Einfachheit halber wird im
folgenden Beispiel ref verwendet.

Übergeben von Werttypen als Wert


Im folgenden Beispiel wird gezeigt, wie Werttypparameter als Wert übergeben werden. Die Variable n wird als
Wert an die SquareIt -Methode übergeben. Alle Änderungen, die innerhalb der Methode vorgenommen
werden, wirken sich nicht auf den ursprünglichen Wert der Variablen aus.

class PassingValByVal
{
static void SquareIt(int x)
// The parameter x is passed by value.
// Changes to x will not affect the original value of x.
{
x *= x;
System.Console.WriteLine("The value inside the method: {0}", x);
}
static void Main()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);

SquareIt(n); // Passing the variable by value.


System.Console.WriteLine("The value after calling the method: {0}", n);

// Keep the console window open in debug mode.


System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/* Output:
The value before calling the method: 5
The value inside the method: 25
The value after calling the method: 5
*/

Die Variable n ist eine Werttypvariable. Sie enthält Daten, den Wert 5 . Beim Aufruf von SquareIt werden die
Inhalte von n in den Parameter x kopiert, der in der Methode quadriert wird. In Main ist der Wert von n
jedoch auch nach dem Aufruf der SquareIt -Methode derselbe wie zuvor. Die Änderung, die innerhalb der
Methode stattfindet, wirkt sich nur auf die lokale Variable x aus.
Übergeben von Werttypen als Verweis
Das folgende Beispiel entspricht dem vorhergehenden Beispiel, mit dem Unterschied, dass das Argument als
ref -Parameter übergeben wird. Der Wert des zugrunde liegenden Arguments, n , wird geändert, wenn x in
der Methode geändert wird.

class PassingValByRef
{
static void SquareIt(ref int x)
// The parameter x is passed by reference.
// Changes to x will affect the original value of x.
{
x *= x;
System.Console.WriteLine("The value inside the method: {0}", x);
}
static void Main()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);

SquareIt(ref n); // Passing the variable by reference.


System.Console.WriteLine("The value after calling the method: {0}", n);

// Keep the console window open in debug mode.


System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/* Output:
The value before calling the method: 5
The value inside the method: 25
The value after calling the method: 25
*/

In diesem Beispiel wird nicht der Wert von n , sondern ein Verweis auf n übergeben. Der Parameter x ist kein
int, sondern ein Verweis auf int – in diesem Fall ein Verweis auf n . Beim Quadrieren von x innerhalb der
Methode wird deshalb tatsächlich das Element quadriert, auf das x verweist: n .

Austauschen von Werttypen


Ein allgemeines Beispiel zum Ändern der Werte von Argumenten ist eine swap-Methode, bei der Sie zwei
Variablen an die Methode übergeben und die Methode deren Inhalte austauscht. Sie müssen die Argumente als
Verweis an die swap-Methode übergeben. Andernfalls tauschen Sie lokale Kopien der Parameter innerhalb der
Methode aus, und es erfolgt keine Änderung in der aufrufenden Methode. Im folgenden Beispiel werden
ganzzahlige Werte getauscht.

static void SwapByRef(ref int x, ref int y)


{
int temp = x;
x = y;
y = temp;
}

Wenn Sie die SwapByRef -Methode aufrufen, verwenden Sie das Schlüsselwort ref im Aufruf, wie im folgenden
Beispiel gezeigt.
static void Main()
{
int i = 2, j = 3;
System.Console.WriteLine("i = {0} j = {1}" , i, j);

SwapByRef (ref i, ref j);

System.Console.WriteLine("i = {0} j = {1}" , i, j);

// Keep the console window open in debug mode.


System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
/* Output:
i = 2 j = 3
i = 3 j = 2
*/

Siehe auch
C#-Programmierhandbuch
Übergeben von Parametern
Übergeben von Verweistypparametern
Übergeben von Verweistypparametern (C#-
Programmierhandbuch)
04.11.2021 • 3 minutes to read

Eine Variable eines Verweistyps enthält direkt keine Daten. Sie enthält einen Verweis auf die Daten. Wenn Sie
einen Verweistypparameter nach Wert übergeben, ist es möglich, die Daten zu ändern, die zu dem Objekt
gehören, auf das verwiesen wird, z.B. den Wert eines Klassenmembers. Sie können jedoch nicht den Wert des
Verweises selbst ändern.Zum Beispiel. können Sie nicht mit dem gleichen Verweis eines neuen Objekts
Arbeitsspeicher zuweisen und ihn außerhalb der Methode beibehalten. Dazu müssen Sie den Parameter mit den
Schlüsselwörtern ref oder out übergeben. Der Einfachheit halber wird im folgenden Beispiel ref verwendet.

Übergeben von Verweistypen nach Wert


Im folgenden Beispiel wird gezeigt, wie Verweistypparameter, arr , nach Wert an eine Methode, Change ,
übergeben werden. Da der Parameter ein Verweis auf arr ist, ist es möglich, die Werte der Arrayelemente zu
ändern. Der Versuch, den Parameter einem anderen Speicherort zuzuweisen, funktioniert aber nur innerhalb der
Methode und wirkt sich nicht auf die ursprüngliche Variable arr aus.

class PassingRefByVal
{
static void Change(int[] pArray)
{
pArray[0] = 888; // This change affects the original element.
pArray = new int[5] {-3, -1, -2, -3, -4}; // This change is local.
System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}

static void Main()


{
int[] arr = {1, 4, 5};
System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr
[0]);

Change(arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr
[0]);
}
}
/* Output:
Inside Main, before calling the method, the first element is: 1
Inside the method, the first element is: -3
Inside Main, after calling the method, the first element is: 888
*/

Im vorherigen Beispiel, wurde das Verweistyp-Array arr ohne den ref -Parameter an die Methode übergeben.
In diesem Fall wird eine Kopie des Verweises, die auf arr zeigt, an die Methode übergeben. Die Ausgabe zeigt,
dass die Methode den Inhalt eines Arrayelements ändern kann, in diesem Fall von 1 in 888 . Durch das
Zuweisen eines neuen Teils des Arbeitsspeichers mithilfe des Operators new innerhalb der Change -Methode
verweist die Variable pArray jedoch auf ein neues Array. Alle nachfolgenden Änderungen wirken sich daher
nicht auf das ursprüngliche Array arr aus, das in Main erstellt wird. Tatsächlich werden in diesem Beispiel zwei
Arrays erstellt, eines in der Main , und das andere in der Change -Methode.
Übergeben von Verweistypen nach Verweis
Das folgende Beispiel entspricht dem vorherigen, mit dem Unterschied, dass dem Methodenheader und -aufruf
das ref -Schlüsselwort hinzugefügt wird. Alle Änderungen in der Methode haben Auswirkungen auf die
ursprüngliche Variable im aufrufenden Programm.

class PassingRefByRef
{
static void Change(ref int[] pArray)
{
// Both of the following changes will affect the original variables:
pArray[0] = 888;
pArray = new int[5] {-3, -1, -2, -3, -4};
System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}

static void Main()


{
int[] arr = {1, 4, 5};
System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}",
arr[0]);

Change(ref arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}",
arr[0]);
}
}
/* Output:
Inside Main, before calling the method, the first element is: 1
Inside the method, the first element is: -3
Inside Main, after calling the method, the first element is: -3
*/

Alle Änderungen innerhalb der Methode haben Auswirkungen auf das ursprüngliche Array in Main . Das
ursprüngliche Array wird in der Tat mit dem new -Operator neu zugewiesen. Nach dem Aufruf der Change -
Methode zeigt jeder Verweis auf arr auf die fünf Elemente umfassendes Array, das in der Change -Methode
erstellt wird.

Austauschen von zwei Zeichenfolgen


Das Austauschen von Zeichenfolgen ist ein gutes Beispiel für das Übergeben von Verweistypparametern nach
Verweis. Im Beispiel werden zwei Zeichenfolgen, str1 und str2 , in Main initialisiert und der SwapStrings -
Methode als Parameter übergeben, die vom ref -Schlüsselwort geändert wurden. Die beiden Zeichenfolgen
werden innerhalb der Methode und auch in Main ausgetauscht.
class SwappingStrings
{
static void SwapStrings(ref string s1, ref string s2)
// The string parameter is passed by reference.
// Any changes on parameters will affect the original variables.
{
string temp = s1;
s1 = s2;
s2 = temp;
System.Console.WriteLine("Inside the method: {0} {1}", s1, s2);
}

static void Main()


{
string str1 = "John";
string str2 = "Smith";
System.Console.WriteLine("Inside Main, before swapping: {0} {1}", str1, str2);

SwapStrings(ref str1, ref str2); // Passing strings by reference


System.Console.WriteLine("Inside Main, after swapping: {0} {1}", str1, str2);
}
}
/* Output:
Inside Main, before swapping: John Smith
Inside the method: Smith John
Inside Main, after swapping: Smith John
*/

In diesem Beispiel müssen die Parameter nach Verweis übergeben werden, damit die Variablen im aufrufenden
Programm beeinflusst werden. Wenn Sie das ref -Schlüsselwort aus dem Methodenheader und dem
Methodenaufruf entfernen, erfolgen keine Änderungen im aufrufenden Programm.
Weitere Informationen zu Zeichenfolgen finden Sie unter string.

Siehe auch
C#-Programmierhandbuch
Übergeben von Parametern
ref
in
out
Verweistypen
Gewusst wie: Unterschiede zwischen dem
Übergeben einer Struktur und dem Übergeben
eines Klassenverweises an eine Methode (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Das folgende Beispiel stellt dar, wie die Übergabe einer Struktur an eine Methode sich von der Übergabe einer
Klasseninstanz an eine Methode unterscheidet. Im Beispiel werden beide Argumente (Struktur und
Klasseninstanz) nach Wert übergeben, und beide Methoden ändern den Wert eines Felds des Arguments.
Allerdings sind die Ergebnisse der beiden Methoden nicht identisch, denn wenn Sie eine Struktur übergeben,
unterscheidet sich dies von dem, wenn Sie eine Instanz einer Klasse übergeben.
Da eine Struktur ein Werttyp ist, wenn Sie eine Struktur nach Wert an eine Methode übergeben, erhält und
funktioniert die Methode nur mit einer Kopie des Strukturarguments. Die Methode hat keinen Zugriff auf die
ursprüngliche Struktur in der aufrufenden Methode und kann sie deshalb nicht ändern. Die Methode kann nur
die Kopie ändern.
Eine Instanz der Klasse ist ein Verweistyp, kein Werttyp. Wenn ein Verweistyp als Wert an eine Methode
übergeben wird, erhält die Methode eine Kopie des Verweises auf die Klasseninstanz. Die aufgerufene Methode
erhält also eine Kopie der Adresse der Instanz, während die aufrufende Methode die ursprüngliche Adresse der
Instanz beibehält. Die Klasseninstanz in der aufrufenden Methode verfügt über eine Adresse, der Parameter in
der aufgerufenen Methode verfügt über eine Kopie der Adresse und beide Adressen verweisen auf dasselbe
Objekt. Da der Parameter nur eine Kopie der Adresse enthält, kann die aufgerufene Methode nicht die Adresse
der Klasseninstanz in der aufrufenden Methode ändern. Allerdings kann die aufgerufene Methode die Kopie der
Adresse verwenden, um auf die Klassenmember zuzugreifen, auf die die ursprüngliche Adresse und die Kopie
der Adresse verweisen. Wenn die aufgerufene Methode einen Klassenmember ändert, ändert sich auch die
ursprüngliche Klasseninstanz in der aufrufenden Methode.
Die Ausgabe des folgenden Beispiels veranschaulicht den Unterschied. Der Wert des Felds willIChange der
Klasseninstanz wird durch den Aufruf auf die Methode ClassTaker geändert, da die Methode die Adresse im
Parameter verwendet, um das angegebene Feld der Klasseninstanz zu finden. Das Feld willIChange der Struktur
in der aufrufenden Methode wird nicht durch den Aufruf auf die Methode StructTaker geändert, da der Wert
des Arguments eine Kopie der Struktur selbst ist und keine Kopie deren Adresse. StructTaker ändert die Kopie,
und die Kopie wird abgebrochen, wenn der Aufruf auf StructTaker abgeschlossen ist.

Beispiel
using System;

class TheClass
{
public string willIChange;
}

struct TheStruct
{
public string willIChange;
}

class TestClassAndStruct
{
static void ClassTaker(TheClass c)
{
c.willIChange = "Changed";
}

static void StructTaker(TheStruct s)


{
s.willIChange = "Changed";
}

static void Main()


{
TheClass testClass = new TheClass();
TheStruct testStruct = new TheStruct();

testClass.willIChange = "Not Changed";


testStruct.willIChange = "Not Changed";

ClassTaker(testClass);
StructTaker(testStruct);

Console.WriteLine("Class field = {0}", testClass.willIChange);


Console.WriteLine("Struct field = {0}", testStruct.willIChange);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Class field = Changed
Struct field = Not Changed
*/

Weitere Informationen
C#-Programmierhandbuch
Klassen
Strukturtypen
Übergeben von Parametern
Implizit typisierte lokale Variablen (C#-
Programmierhandbuch)
04.11.2021 • 5 minutes to read

Lokale Variablen können deklariert werden, ohne einen expliziten Typ anzugeben. Das var -Schlüsselwort weist
den Compiler an, den Typ der Variablen vom Ausdruck auf der rechten Seite der Initialisierungsanweisung
abzuleiten. Der hergeleitete Typ ist möglicherweise ein integrierter Typ, ein anonymer Typ, ein
benutzerdefinierter Typ oder ein Typ, der in der .NET-Klassenbibliothek definiert wurde. Weitere Informationen
zur Initialisierung von Arrays mithilfe von var finden Sie unter Implizit typisierte Arrays.
Die folgenden Beispiele veranschaulichen verschiedene Arten, wie Sie lokale Variablen mit var deklarieren
können:

// i is compiled as an int
var i = 5;

// s is compiled as a string
var s = "Hello";

// a is compiled as int[]
var a = new[] { 0, 1, 2 };

// expr is compiled as IEnumerable<Customer>


// or perhaps IQueryable<Customer>
var expr =
from c in customers
where c.City == "London"
select c;

// anon is compiled as an anonymous type


var anon = new { Name = "Terry", Age = 34 };

// list is compiled as List<int>


var list = new List<int>();

Es ist von großer Bedeutung zu verstehen, dass das var -Schlüsselwort nicht „variant“ bedeutet, und das es
nicht darauf hinweist, dass die Variable schwach typisiert oder spät gebunden ist. Es bedeutet nur, dass der
Compiler den angemessensten Typen bestimmt und zuweist.
Das var -Schlüsselwort kann in folgendem Kontext verwendet werden:
Für lokale Variablen (Variablen, die im Geltungsbereich der Methode deklariert wurden), wie in
vorherigem Beispiel gezeigt.
In einer for-Initialisierungsanweisung.

for (var x = 1; x < 10; x++)

In einer foreach-Initialisierungsanweisung.

foreach (var item in list) {...}

In einer using-Anweisung.
using (var file = new StreamReader("C:\\myfile.txt")) {...}

Weitere Informationen finden Sie unter Vorgehensweise: Verwenden von implizit typisierten lokalen Variablen
und Arrays in einem Abfrageausdruck.

var und anonyme Typen


In vielen Fällen ist der Einsatz von var optional und nur eine praktische Syntax. Wenn eine Variable allerdings
mit einem anonymen Typ initialisiert wird, müssen Sie die Variable als var deklarieren, wenn Sie zu einem
späteren Zeitpunkt auf die Eigenschaften des Objekts zugreifen möchten. Das ist ein häufiges Szenario in LINQ-
Abfrageausdrücken. Weitere Informationen finden Sie unter Anonyme Typen.
Aus der Perspektive Ihres Quellcodes hat ein anonymer Typ keinen Namen. Wenn eine Abfragevariable mit var
initialisiert wurde, ist es deshalb nur möglich, auf die Eigenschaften in der zurückgegebenen Objektsequenz
zuzugreifen, wenn Sie var in der foreach -Anweisung als Typen der Iterationvariablen verwenden.

class ImplicitlyTypedLocals2
{
static void Main()
{
string[] words = { "aPPLE", "BlUeBeRrY", "cHeRry" };

// If a query produces a sequence of anonymous types,


// then use var in the foreach statement to access the properties.
var upperLowerWords =
from w in words
select new { Upper = w.ToUpper(), Lower = w.ToLower() };

// Execute the query


foreach (var ul in upperLowerWords)
{
Console.WriteLine("Uppercase: {0}, Lowercase: {1}", ul.Upper, ul.Lower);
}
}
}
/* Outputs:
Uppercase: APPLE, Lowercase: apple
Uppercase: BLUEBERRY, Lowercase: blueberry
Uppercase: CHERRY, Lowercase: cherry
*/

Hinweise
Die folgenden Einschränkungen gelten für implizit typisierte Variablendeklarationen:
var kann nur verwendet werden, wenn eine lokale Variable deklariert und in derselben Anweisung
initialisiert wird; die Variable kann weder mit NULL noch mit einer Methodengruppe oder einer
anonymen Funktion initialisiert werden.
var kann nicht für Felder im Klassenbereich verwendet werden.
Variablen, die mit var deklariert wurden, können nicht im Initialisierungsausdruck verwendet werden.
Sprich, dieser Ausdruck ist gültig: int i = (i = 20); , aber dieser Ausdruck führt zu einem
Kompilierzeitfehler: var i = (i = 20);
Es können nicht mehrere implizit typisierte Variablen in derselben Anweisung initialisiert werden.
Wenn sich ein Typ mit dem Namen var im Geltungsbereich befindet, löst sich das var -Schlüsselwort
zu diesem Typnamen auf und wird nicht als Teil der Deklaration einer implizit typisierten lokalen
Variablen behandelt.
Implizite Typisierung mit dem var -Schlüsselwort kann nur auf Variablen im Gültigkeitsbereich der lokalen
Methode angewendet werden. Implizite Typisierung ist für Klassenfelder nicht verfügbar, weil der C#-Compiler
während der Verarbeitung des Codes auf ein logisches Paradox treffen würde: Der Compiler muss den Typ des
Felds kennen, aber er kann den Typ erst bestimmen, wenn der Zuordnungsausdruck analysiert ist, und der
Ausdruck kann nicht ohne Kenntnis des Typs ausgewertet werden. Betrachten Sie folgenden Code:

private var bookTitles;

bookTitles ist ein Klassenfeld, das den Typ var erhält. Da das Feld keinen Ausdruck zur Auswertung enthält,
kann der Compiler nicht ableiten, von welchem Typ bookTitles ist. Darüber hinaus ist es auch nicht
ausreichend, dem Feld einen Ausdruck hinzuzufügen (wie für eine lokale Variable):

private var bookTitles = new List<string>();

Wenn der Compiler während der Codekompilierung auf Felder trifft, zeichnet er den Typ jedes Felds auf, bevor
er dem Feld zugeordnete Ausdrücke verarbeitet. Der Compiler erkennt das gleiche Paradox bei dem Versuch,
bookTitles zu analysieren: Er muss den Typ des Felds kennen, aber der Compiler würde den Typ von var
normalerweise durch Analysieren des Ausdrucks bestimmen, was nicht möglich ist, ohne vorher den Typ zu
kennen.
var erweist sich auch bei Abfrageausdrücken als nützlich, deren genauer konstruierter Typ der Abfragevariable
schwer ermittelbar ist. Dies kann bei Gruppierungs- und Sortierungsvorgängen auftreten.
Das var -Schlüsselwort erweist sich auch als nützlich, wenn es umständlich ist, den Variablentypen mit der
Tastatur einzugeben – oder wenn der Typ offensichtlich ist, oder nicht zur Lesbarkeit des Codes beiträgt. var ist
z.B. bei geschachtelten generischen Typen wie die, die in Gruppenvorgängen verwendet werden, auf diese Weise
nützlich. In der folgenden Abfrage ist der Typ der Abfragevariablen IEnumerable<IGrouping<string, Student>> .
Solange dies Ihnen und denen, die Ihren Code verwalten müssen, klar ist, spricht nichts dagegen, implizite
Typisierung aus Gründen der Zweckmäßigkeit und der Kürze zu verwenden.

// Same as previous example except we use the entire last name as a key.
// Query variable is an IEnumerable<IGrouping<string, Student>>
var studentQuery3 =
from student in students
group student by student.Last;

Die Verwendung von var trägt zur Vereinfachung Ihres Codes bei, aber die Verwendung sollte auf die Fälle
beschränkt werden, in denen sie erforderlich ist oder wenn sie Ihren Code leichter lesbar macht. Weitere
Informationen zur ordnungsgemäßen Verwendung von var finden Sie im Abschnitt Implizit typisierte lokale
Variablen im Artikel zu „C#-Richtlinien für das Codieren“.

Siehe auch
C#-Referenz
Implizit typisierte Arrays
Verwenden von implizit typisierten lokalen Variablen und Arrays in einem Abfrageausdruck
Anonyme Typen
Objekt- und Auflistungsinitialisierer
var
LINQ in C#
LINQ (Language Integrated Query)
Iterationsanweisungen
Using-Anweisung
Gewusst wie: Verwenden von implizit typisierten
lokalen Variablen und Arrays in einem
Abfrageausdruck (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Sie können implizit typisierte lokale Variablen immer dann verwenden, wenn Sie möchten, dass der Compiler
den Typ einer lokalen Variablen bestimmt. Sie müssen implizit typisierte lokale Variablen verwenden, um
anonyme Typen zu speichern, die häufig in Abfrageausdrücken verwendet werden. In den folgenden Beispielen
werden sowohl optionale als auch erforderliche Einsatzmöglichkeiten von implizit typisierte lokale Variablen in
einer Abfrage veranschaulicht.
Implizit typisierte lokale Variablen werden mit dem kontextuellen Schlüsselwort var deklariert. Weitere
Informationen zu finden Sie unter Implizit typisierte lokale Variablen und Implizit typisierte Arrays.

Beispiele
Im folgenden Beispiel wird ein häufiges Szenario gezeigt, in dem das Schlüsselwort var erforderlich ist: ein
Abfrageausdruck, der eine Folge anonymer Typen erzeugt. In diesem Szenario müssen sowohl die
Abfragevariable als auch die Iterationsvariable in der foreach -Anweisung mit var implizit typisiert werden, da
Sie keinen Zugriff auf einen Typnamen für den anonymen Typ haben. Weitere Informationen zu anonymen
Typen finden Sie unter Anonyme Typen.

private static void QueryNames(char firstLetter)


{
// Create the query. Use of var is required because
// the query produces a sequence of anonymous types:
// System.Collections.Generic.IEnumerable<????>.
var studentQuery =
from student in students
where student.FirstName[0] == firstLetter
select new { student.FirstName, student.LastName };

// Execute the query and display the results.


foreach (var anonType in studentQuery)
{
Console.WriteLine("First = {0}, Last = {1}", anonType.FirstName, anonType.LastName);
}
}

In folgendem Beispiel wird das Schlüsselwort var in einer ähnlichen Situation verwendet, wobei der Gebrauch
von var jedoch optional ist. Da student.LastName eine Zeichenfolge ist, gibt die Ausführung der Abfrage eine
Sequenz von Zeichenfolgen zurück. Deshalb kann der Typ von queryID als
System.Collections.Generic.IEnumerable<string> statt als var deklariert werden. Das Schlüsselwort var wird
aus praktischen Gründen verwendet. In dem Beispiel wird die Iterationsvariable in der foreach -Anweisung
explizit als Zeichenfolge typisiert. Sie könnte aber auch alternativ mit var deklariert werden. Da der Typ der
Iterationsvariablen kein anonymer Typ ist, ist das Verwenden von var optional aber nicht verpflichtend. Denken
Sie daran, das var selbst kein Typ ist, sondern eine Anweisung an den Compiler, um den Typen abzuleiten und
zuzuweisen.
// Variable queryId could be declared by using
// System.Collections.Generic.IEnumerable<string>
// instead of var.
var queryId =
from student in students
where student.Id > 111
select student.LastName;

// Variable str could be declared by using var instead of string.


foreach (string str in queryId)
{
Console.WriteLine("Last name: {0}", str);
}

Siehe auch
C#-Programmierhandbuch
Erweiterungsmethoden
LINQ (Language Integrated Query)
var
LINQ in C#
Erweiterungsmethoden (C#-
Programmierhandbuch)
04.11.2021 • 9 minutes to read

Mit Erweiterungsmethoden können Sie vorhandenen Typen Methoden hinzufügen, ohne einen neuen
abgeleiteten Typ zu erstellen und ohne den ursprünglichen Typ neu kompilieren oder auf andere Weise
bearbeiten zu müssen. Erweiterungsmethoden sind statische Methoden, die Sie jedoch wie Instanzmethoden für
den erweiterten Typ aufrufen können. Für in C#, F# und Visual Basic geschriebenen Clientcode gibt es keinen
sichtbaren Unterschied zwischen dem Aufrufen einer Erweiterungsmethode und den in einem Typ definierten
Methoden.
Die häufigsten Erweiterungsmethoden sind die LINQ-Standardabfrageoperatoren, die vorhandenen
System.Collections.IEnumerable- und System.Collections.Generic.IEnumerable<T>-Typen Funktionalität
hinzufügen. Um die Standardabfrageoperatoren zu verwenden, müssen Sie sie zuerst mit einer
using System.Linq -Direktive einbinden. Jeder Typ, der IEnumerable<T> implementiert, scheint
Instanzmethoden zu haben, wie z. B. GroupBy, OrderBy, Average. Sie können diese zusätzlichen Methoden in der
IntelliSense-Anweisungsvervollständigung sehen, wenn Sie nach einer Instanz eines IEnumerable<T>-Typs, z.B.
List<T> oder Array, "dot" eingeben.
OrderBy-Beispiel
Das folgende Beispiel zeigt, wie Sie die Standardabfrageoperator-Methode OrderBy für ein Ganzzahlarray
aufrufen können. Der Ausdruck in Klammern ist ein Lambda-Ausdruck. Viele Standardabfrageoperatoren
verwenden Lambda-Ausdrücke als Parameter, dies ist jedoch keine Voraussetzung für Erweiterungsmethoden.
Weitere Informationen finden Sie unter Lambdaausdrücke.

class ExtensionMethods2
{

static void Main()


{
int[] ints = { 10, 45, 15, 39, 21, 26 };
var result = ints.OrderBy(g => g);
foreach (var i in result)
{
System.Console.Write(i + " ");
}
}
}
//Output: 10 15 21 26 39 45

Erweiterungsmethoden werden als statische Methoden definiert, jedoch mithilfe einer Instanzmethodensyntax
aufgerufen. Ihr erster Parameter gibt an, für welchen Typ die Methode aufgerufen wird. Dem Parameter wird der
this-Modifizierer vorangestellt. Erweiterungsmethoden befinden sich nur dann im Bereich, wenn Sie den
Namespace explizit mit einer using -Direktive in Ihren Quellcode importieren.
Im folgenden Beispiel wird eine für die System.String-Klasse definierte Erweiterungsmethode veranschaulicht.
Die Definition erfolgt in einer nicht geschachtelten, nicht generischen statischen Klasse:
namespace ExtensionMethods
{
public static class MyExtensions
{
public static int WordCount(this String str)
{
return str.Split(new char[] { ' ', '.', '?' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}

Die WordCount -Erweiterungsmethode kann mit dieser using -Direktive eingebunden werden:

using ExtensionMethods;

Sie kann darüber hinaus mit dieser Syntax von einer Anwendung aufgerufen werden:

string s = "Hello Extension Methods";


int i = s.WordCount();

Sie rufen die Erweiterungsmethode im Code mit Instanzmethodensyntax auf. Die vom Compiler erstellte
Intermediate Language (IL) übersetzt Ihren Code in einen Aufruf der statischen Methode. Es wird nicht wirklich
gegen das Prinzip der Kapselung verstoßen. Erweiterungsmethoden können nicht auf private Variablen im Typ
zugreifen, den sie erweitern.
Sowohl die MyExtensions -Klasse als auch die WordCount -Methode sind static , und auf sie kann wie auf alle
anderen static -Member zugegriffen werden. Die WordCount -Methode kann wie andere static -Methoden
wie folgt aufgerufen werden:

string s = "Hello Extension Methods";


int i = MyExtensions.WordCount(s);

Für den C#-Code oben gilt:


Er deklariert ein neues string -Element namens s mit dem Wert "Hello Extension Methods" und weist es
zu.
Er ruft das angegebene MyExtensions.WordCount -Argument s auf.

Weitere Informationen finden Sie unter Vorgehensweise: Implementieren und Aufrufen einer
benutzerdefinierten Erweiterungsmethode.
Im Allgemeinen rufen Sie vermutlich sehr viel häufiger Erweiterungsmethoden auf, als eigene Methoden zu
implementieren. Da Erweiterungsmethoden mit der Instanzmethodensyntax aufgerufen werden, sind für ihren
Einsatz aus dem Clientcode keine besonderen Kenntnisse erforderlich. Um Erweiterungsmethoden für einen
bestimmten Typ zu aktivieren, fügen Sie eine using -Direktive für den Namespace, in dem die Methoden
definiert werden, hinzu. Um beispielsweise die Standardabfrageoperatoren zu verwenden, fügen Sie diese
using -Direktive dem Code hinzu:

using System.Linq;

(Möglicherweise müssen Sie auch einen Verweis auf System.Core.dll hinzufügen.) Wie Sie sehen, werden die
Standardabfrageoperatoren in IntelliSense jetzt für die meisten IEnumerable<T>-Typen als zusätzliche
Methoden angezeigt.
Binden von Erweiterungsmethoden während der Kompilierung
Sie können Erweiterungsmethoden verwenden, um eine Klasse oder eine Schnittstelle zu erweitern, jedoch
nicht, um sie zu überschreiben. Eine Erweiterungsmethode mit dem gleichen Namen und der gleichen Signatur
wie eine Schnittstellen- oder Klassenmethode wird nie aufgerufen. Bei der Kompilierung verfügen
Erweiterungsmethoden immer über niedrigere Priorität als im Typ selbst definierte Instanzmethoden. Das heißt,
wenn ein Typ eine Methode mit dem Namen Process(int i) hat und Sie über eine Erweiterungsmethode mit
der gleichen Signatur verfügen, stellt der Compiler immer eine Bindung mit der Instanzmethode her. Wenn der
Compiler einen Methodenaufruf erkennt, sucht er zuerst nach einer Entsprechung in den Instanzmethoden des
Typs. Wenn keine Entsprechung gefunden wird, sucht er nach Erweiterungsmethoden, die für den Typ definiert
wurden, und stellt eine Bindung mit der ersten gefundenen Erweiterungsmethode her. Im folgenden Beispiel
wird veranschaulicht, wie der Compiler bestimmt, mit welcher Erweiterungsmethode oder Instanzmethode die
Bindung erfolgen soll.

Beispiel
Das folgende Beispiel veranschaulicht die Regeln, denen der C#-Compiler folgt, um zu bestimmen, ob ein
Methodenaufruf an eine Instanzmethode für den Typ oder an eine Erweiterungsmethode gebunden werden soll.
Die statische Klasse Extensions enthält Erweiterungsmethoden, die für jeden Typ definiert wurden, der
IMyInterface implementiert. Die Klassen A , B und C implementieren alle die Schnittstelle.

Die MethodB -Erweiterungsmethode wird nie aufgerufen, da ihr Name und die Signatur genau mit Methoden
übereinstimmen, die bereits von den Klassen implementiert wurden.
Wenn der Compiler keine Instanzmethode mit einer entsprechenden Signatur findet, stellt er eine Bindung mit
einer entsprechenden Erweiterungsmethode her (sofern vorhanden).

// Define an interface named IMyInterface.


namespace DefineIMyInterface
{
using System;

public interface IMyInterface


{
// Any class that implements IMyInterface must define a method
// that matches the following signature.
void MethodB();
}
}

// Define extension methods for IMyInterface.


namespace Extensions
{
using System;
using DefineIMyInterface;

// The following extension methods can be accessed by instances of any


// class that implements IMyInterface.
public static class Extension
{
public static void MethodA(this IMyInterface myInterface, int i)
{
Console.WriteLine
("Extension.MethodA(this IMyInterface myInterface, int i)");
}

public static void MethodA(this IMyInterface myInterface, string s)


{
Console.WriteLine
("Extension.MethodA(this IMyInterface myInterface, string s)");
}
// This method is never called in ExtensionMethodsDemo1, because each
// of the three classes A, B, and C implements a method named MethodB
// that has a matching signature.
public static void MethodB(this IMyInterface myInterface)
{
Console.WriteLine
("Extension.MethodB(this IMyInterface myInterface)");
}
}
}

// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
namespace ExtensionMethodsDemo1
{
using System;
using Extensions;
using DefineIMyInterface;

class A : IMyInterface
{
public void MethodB() { Console.WriteLine("A.MethodB()"); }
}

class B : IMyInterface
{
public void MethodB() { Console.WriteLine("B.MethodB()"); }
public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
}

class C : IMyInterface
{
public void MethodB() { Console.WriteLine("C.MethodB()"); }
public void MethodA(object obj)
{
Console.WriteLine("C.MethodA(object obj)");
}
}

class ExtMethodDemo
{
static void Main(string[] args)
{
// Declare an instance of class A, class B, and class C.
A a = new A();
B b = new B();
C c = new C();

// For a, b, and c, call the following methods:


// -- MethodA with an int argument
// -- MethodA with a string argument
// -- MethodB with no argument.

// A contains no MethodA, so each call to MethodA resolves to


// the extension method that has a matching signature.
a.MethodA(1); // Extension.MethodA(IMyInterface, int)
a.MethodA("hello"); // Extension.MethodA(IMyInterface, string)

// A has a method that matches the signature of the following call


// to MethodB.
a.MethodB(); // A.MethodB()

// B has methods that match the signatures of the following


// method calls.
b.MethodA(1); // B.MethodA(int)
b.MethodB(); // B.MethodB()

// B has no matching method for the following call, but


// class Extension does.
b.MethodA("hello"); // Extension.MethodA(IMyInterface, string)

// C contains an instance method that matches each of the following


// method calls.
c.MethodA(1); // C.MethodA(object)
c.MethodA("hello"); // C.MethodA(object)
c.MethodB(); // C.MethodB()
}
}
}
/* Output:
Extension.MethodA(this IMyInterface myInterface, int i)
Extension.MethodA(this IMyInterface myInterface, string s)
A.MethodB()
B.MethodA(int i)
B.MethodB()
Extension.MethodA(this IMyInterface myInterface, string s)
C.MethodA(object obj)
C.MethodA(object obj)
C.MethodB()
*/

Gängige Verwendungsmuster
Auflistungsfunktionen
In der Vergangenheit war es üblich, „Sammlungsklassen“ zu erstellen, mit denen die
System.Collections.Generic.IEnumerable<T>-Schnittstelle für einen bestimmten Typ implementiert und
Funktionalität für Sammlungen dieses Typs bereitgestellt wurde. Es ist zwar nichts Falsches daran, diese Art von
Sammlungsobjekten zu erstellen, aber die gleiche Funktionalität kann durch eine Erweiterung von
System.Collections.Generic.IEnumerable<T> erreicht werden. Erweiterungen haben den Vorteil, dass die
Funktionalität aus einer beliebigen Sammlung – z. B. System.Array oder System.Collections.Generic.List<T> –
aufgerufen werden kann, die System.Collections.Generic.IEnumerable<T> für diesen Typ implementiert. Ein
entsprechendes Beispiel mit Verwendung eines Int32-Arrays finden Sie weiter oben in diesem Artikel.
Ebenenspezifische Funktionalität
Bei Verwendung einer Zwiebelarchitektur oder eines anderen geschichteten Anwendungsdesigns ist es üblich,
einen Satz mit Domänenentitäten oder Datenübertragungsobjekten für die Kommunikation über
Anwendungsgrenzen hinweg zu verwenden. Diese Objekte umfassen in der Regel keine oder nur minimale
Funktionalität, die für alle Ebenen der Anwendung gilt. Erweiterungsmethoden können verwendet werden, um
Funktionalität hinzuzufügen, die für jede Anwendungsschicht spezifisch ist, ohne das Objekt mit Methoden zu
überladen, die in anderen Ebenen nicht benötigt werden oder unerwünscht sind.

public class DomainEntity


{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}

static class DomainEntityExtensions


{
static string FullName(this DomainEntity value)
=> $"{value.FirstName} {value.LastName}";
}

Erweiterung von vordefinierten Typen


Wenn wiederverwendbare Funktionalität erstellt werden muss, kann statt der Erstellung neuer Objekte häufig
ein bereits vorhandener Typ erweitert werden, z. B. ein .NET- oder CLR-Typ. Wenn beispielsweise keine
Erweiterungsmethoden verwendet werden, könnten wir eine Engine - oder Query -Klasse zum Ausführen einer
Abfrage auf einer SQL Server-Instanz erstellen, die über mehrere Stellen in unserem Code aufgerufen werden
kann. Stattdessen können wir jedoch die Klasse System.Data.SqlClient.SqlConnection mithilfe von
Erweiterungsmethoden erweitern, damit diese Abfrage von jedem beliebigen Ort ausgeführt werden kann, an
dem eine Verbindung mit einer SQL Server-Instanz vorhanden ist. Weitere Beispiele wären das Hinzufügen
gemeinsamer Funktionalität zur Klasse System.String, eine Erweiterung der Datenverarbeitungsfunktionalität
der Objekte System.IO.File und System.IO.Stream sowie System.Exception-Objekte für eine spezifische
Fehlerbehandlungsfunktionalität. Der Verwendung dieser Anwendungsfälle sind praktisch keine Grenzen
gesetzt.
Das Erweitern vordefinierter Typen kann bei struct -Typen schwierig sein, da sie als Wert an Methoden
übergeben werden. Dies bedeutet, dass alle Änderungen an der Struktur an einer Kopie der Struktur
vorgenommen werden. Diese Änderungen sind nicht sichtbar, nachdem die Erweiterungsmethode beendet
wurde. Ab C# 7.2 können Sie dem ersten Argument einer Erweiterungsmethode den ref -Modifizierer
hinzufügen. Durch das Hinzufügen des ref -Modifizierers wird das erste Argument als Verweis übergeben. Auf
diese Weise können Sie Erweiterungsmethoden schreiben, die den Status der erweiterten Struktur ändern.

Allgemeine Richtlinien
Wenngleich es weiterhin vorzuziehen ist, Funktionalität durch Änderung des Codes eines Objekts oder durch
Ableitung eines neuen Typs hinzuzufügen – sofern dies sinnvoll und möglich ist –, sind Erweiterungsmethoden
zu einer wichtigen Option zum Bereitstellen wiederverwendbarer Funktionalität im gesamten .NET-Ökosystem
geworden. In Fällen, in denen die ursprüngliche Quelle nicht Ihrer Kontrolle unterliegt, ein abgeleitetes Objekt
ungeeignet ist oder nicht verwendet werden kann oder die Funktionalität nicht über den anwendbaren Bereich
hinaus offengelegt werden soll, stellen Erweiterungsmethoden eine ausgezeichnete Wahl dar.
Weitere Informationen zu abgeleiteten Typen finden Sie unter Vererbung.
Wenn Sie eine Erweiterungsmethode zum Erweitern eines Typs verwenden, dessen Quellcode Sie nicht ändern
können, laufen Sie Gefahr, dass eine Änderung an der Implementierung des Typs zu einer Beschädigung Ihrer
Erweiterungsmethode führt.
Wenn Sie Erweiterungsmethoden für einen gegebenen Typ implementieren, beachten Sie die folgenden Punkte:
Eine Erweiterungsmethode wird nie aufgerufen, wenn sie die gleiche Signatur wie eine im Typ definierte
Methode hat.
Erweiterungsmethoden werden auf Namespace-Ebene eingebunden. Wenn Sie z. B. über mehrere statische
Klassen verfügen, die Erweiterungsmethoden in einem einzelnen Namespace mit dem Namen Extensions
enthalten, werden sie alle mit der using Extensions; -Direktive eingebunden.
Für eine Klassenbibliothek, die Sie implementiert haben, sollten Sie keine Erweiterungsmethoden verwenden,
um zu vermeiden, dass die Versionsnummer einer Assembly erhöht wird. Wenn Sie zu einer Bibliothek, deren
Quellcode Sie besitzen, wichtige Funktionalität hinzufügen möchten, befolgen Sie die .NET-Richtlinien für
Assemblyversionierung. Weitere Informationen dazu finden Sie unter Assemblyversionen.

Siehe auch
C#-Programmierhandbuch
Parallel Programming Samples (Beispiele für parallele Programmierung (dazu gehören viele
Beispielerweiterungsmethoden))
Lambda-Ausdrücke
Übersicht über Standardabfrageoperatoren
Conversion rules for Instance parameters and their impact (Konvertierungsregeln für Instanzenparameter
und ihre Auswirkungen)
Extension methods Interoperability between languages (Erweiterungsmethoden-Interoperabilität zwischen
Sprachen)
Extension methods and Curried Delegates (Erweiterungsmethoden und Curry-Delegaten)
Extension method Binding and Error reporting (Binden von Erweiterungsmethoden und Problemberichten)
Gewusst wie: Implementieren und Aufrufen einer
benutzerdefinierten Erweiterungsmethode (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

In diesem Artikel wird das Implementieren Ihrer eigenen Erweiterungsmethoden für jeden .NET-Typ behandelt.
Der Clientcode kann Ihre Erweiterungsmethoden verwenden, wenn ein Verweis auf die DLL, die die Methoden
enthält, und eine using-Direktive hinzugefügt werden, die den Namespace angibt, in dem die
Erweiterungsmethoden definiert sind.

So definieren Sie die Erweiterungsmethode und rufen Sie auf


1. Definieren Sie eine statische Klasse, die die Erweiterungsmethode enthalten soll.
Die Klasse muss für den Clientcode sichtbar sein. Weitere Informationen finden Sie unter
Zugriffsmodifizierer.
2. Implementieren Sie die Erweiterungsmethode als statische Methode mit mindestens die gleichen
Sichtbarkeit wie die enthaltende Klasse.
3. Der erste Parameter der Methode gibt den Typ an, auf den die Methode angewendet wird. Ihm muss ein
this-Modifizierer vorangehen.
4. Fügen Sie im aufrufenden Code eine using -Direktive hinzu, um den Namespace anzugeben, der die
Erweiterungsklassemethode enthält.
5. Rufen Sie die Methoden auf, als wären sie Instanzmethoden für den Typ.
Beachten Sie, dass der erste Parameter nicht durch einen Aufruf von Code angegeben wird, da er den Typ
darstellt, auf den der Operator angewendet wird, und der Compiler bereits den Typ des Objekts kennt. Sie
müssen nur Argumente für Parameter 2 bis n bereitstellen.

Beispiel
Im folgenden Beispiel wird eine Erweiterungsmethode namens WordCount in der
CustomExtensions.StringExtension -Klasse implementiert. Die Methode wird auf die String-Klasse angewendet,
die als erster Methodenparameter angegeben wird. Der CustomExtensions -Namespace wird in den
Anwendungsnamespace importiert, und die Methode wird in der Main -Methode aufgerufen.
using System.Linq;
using System.Text;
using System;

namespace CustomExtensions
{
// Extension methods must be defined in a static class.
public static class StringExtension
{
// This is the extension method.
// The first parameter takes the "this" modifier
// and specifies the type for which the method is defined.
public static int WordCount(this String str)
{
return str.Split(new char[] {' ', '.','?'}, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
namespace Extension_Methods_Simple
{
// Import the extension method namespace.
using CustomExtensions;
class Program
{
static void Main(string[] args)
{
string s = "The quick brown fox jumped over the lazy dog.";
// Call the method as if it were an
// instance method on the type. Note that the first
// parameter is not specified by the calling code.
int i = s.WordCount();
System.Console.WriteLine("Word count of s is {0}", i);
}
}
}

.NET-Sicherheit
Erweiterungsmethoden enthalten keine speziellen Sicherheitslücken. Sie können nicht dazu verwendet werden,
die Identität vorhandener Methoden für einen Typ anzunehmen, da alle Namenskonflikte zugunsten der Instanz
oder eine statische Methode gelöst werden, die der Typ selbst definiert. Erweiterungsmethoden können in der
erweiterten Klasse nicht auf private Daten zugreifen.

Siehe auch
C#-Programmierhandbuch
Erweiterungsmethoden
LINQ (Language Integrated Query)
Statische Klassen und statische Klassenmember
protected
internal
public
this
namespace
Gewusst wie: Erstellen einer neuen Methode für
eine Enumeration (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Sie können Erweiterungsmethoden verwenden, um für einen bestimmten Enumerationstyp spezifische


Funktionen hinzuzufügen.

Beispiel
Im folgenden Beispiel stellt die Grades -Enumeration die möglichen Noten in Buchstaben dar, die ein Schüler im
Unterricht erhalten kann. Eine Erweiterungsmethode mit dem Namen Passing wird dem Grades -Typ
hinzugefügt, sodass jede Instanz dieses Typs nun „weiß“, ob sie eine Note darstellt, mit der der Schüler
bestanden hat.

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;

namespace EnumExtension
{
// Define an extension method in a non-nested static class.
public static class Extensions
{
public static Grades minPassing = Grades.D;
public static bool Passing(this Grades grade)
{
return grade >= minPassing;
}
}

public enum Grades { F = 0, D=1, C=2, B=3, A=4 };


class Program
{
static void Main(string[] args)
{
Grades g1 = Grades.D;
Grades g2 = Grades.F;
Console.WriteLine("First {0} a passing grade.", g1.Passing() ? "is" : "is not");
Console.WriteLine("Second {0} a passing grade.", g2.Passing() ? "is" : "is not");

Extensions.minPassing = Grades.C;
Console.WriteLine("\r\nRaising the bar!\r\n");
Console.WriteLine("First {0} a passing grade.", g1.Passing() ? "is" : "is not");
Console.WriteLine("Second {0} a passing grade.", g2.Passing() ? "is" : "is not");
}
}
}
/* Output:
First is a passing grade.
Second is not a passing grade.

Raising the bar!

First is not a passing grade.


Second is not a passing grade.
*/
Beachten Sie, dass die Extensions -Klasse auch eine statische Variable enthält, die dynamisch aktualisiert wird,
und dass der Rückgabewert der Erweiterungsmethode den aktuellen Wert der Variablen darstellt. Dies zeigt,
dass hinter den Kulissen Erweiterungsmethoden direkt auf der statischen Klasse aufgerufen werden, in der sie
definiert sind.

Siehe auch
C#-Programmierhandbuch
Erweiterungsmethoden
Benannte und optionale Argumente (C#-
Programmierhandbuch)
04.11.2021 • 7 minutes to read

C# 4 führt benannte und optionale Argumente ein. Mithilfe von benannten Argumenten können Sie ein
Argument für einen Parameter angeben, indem Sie das Argument mit dem Namen des Parameters anstatt mit
seiner Position in der Parameterliste abgleichen. Optionale Argumente ermöglichen es Ihnen, Argumente für
einige Parameter auszulassen. Beide Techniken können mit Methoden, Indexern, Konstruktoren und Delegaten
verwendet werden.
Wenn Sie benannte und optionale Argumente verwenden, werden die Argumente in der Reihenfolge
ausgewertet, in der sie in der Argumentliste, nicht in der Parameterliste, erscheinen.
Mit benannten und optionalen Parametern können Sie Argumente für ausgewählte Parameter bereitstellen.
Diese Funktion erleichtert den Zugriff auf COM-Schnittstellen wie etwa die Automation-APIs in Microsoft Office
erheblich.

Benannte Argumente
Bei Verwendung benannter Argumente bleibt es Ihnen erspart, die Reihenfolge von Parametern in den
Parameterlisten von aufgerufenen Methoden abzugleichen. Der Parameter für jedes Argument kann vom
Parameternamen angegeben werden. Eine Funktion, die beispielsweise Details zu einer Bestellung ausgibt (z. B.
Verkäufername, Bestellnummer und Produktname), kann aufgerufen werden, indem anhand der Position
Argumente in der Reihenfolge gesendet werden, die von der Funktion definiert wurde.

PrintOrderDetails("Gift Shop", 31, "Red Mug");

Wenn Sie sich nicht an die Reihenfolge der Parameter erinnern, aber deren Namen kennen, können Sie die
Argumente in einer beliebigen Reihenfolge senden.

PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");


PrintOrderDetails(productName: "Red Mug", sellerName: "Gift Shop", orderNum: 31);

Benannte Argumente verbessern auch die Lesbarkeit des Codes, indem sie identifizieren, was jedes Argument
darstellt. In der folgenden Beispielmethode darf sellerName nicht NULL sein oder Leerzeichen enthalten. Da es
sich bei sellerName und productName um Zeichenfolgentypen handelt, sollten Sie benannte Argumente
verwenden, anstatt Argumente nach Position zu senden, um die beiden Parameter zu unterscheiden und die
Leser des Codes nicht zu verwirren.
Benannte Argumente sind länger gültig, wenn sie mit positionellen Argumenten verwendet werden, da
ihnen keine positionellen Argumente folgen, bzw. da

PrintOrderDetails("Gift Shop", 31, productName: "Red Mug");

sie ab C# 7.2 in der richtigen Position verwendet werden. Im folgenden Beispiel befindet sich der
Parameter orderNum in der richtigen Position, ist jedoch nicht explizit benannt.
PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug");

Positionelle Argumente, die auf fehlerhafte benannte Argumente folgen, sind ungültig.

// This generates CS1738: Named argument specifications must appear after all fixed arguments have been
specified.
PrintOrderDetails(productName: "Red Mug", 31, "Gift Shop");

Beispiel
Der folgende Code implementiert die Beispiele in diesem Abschnitt sowie einige zusätzliche Beispiele.

class NamedExample
{
static void Main(string[] args)
{
// The method can be called in the normal way, by using positional arguments.
PrintOrderDetails("Gift Shop", 31, "Red Mug");

// Named arguments can be supplied for the parameters in any order.


PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");
PrintOrderDetails(productName: "Red Mug", sellerName: "Gift Shop", orderNum: 31);

// Named arguments mixed with positional arguments are valid


// as long as they are used in their correct position.
PrintOrderDetails("Gift Shop", 31, productName: "Red Mug");
PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug"); // C# 7.2 onwards
PrintOrderDetails("Gift Shop", orderNum: 31, "Red Mug"); // C# 7.2 onwards

// However, mixed arguments are invalid if used out-of-order.


// The following statements will cause a compiler error.
// PrintOrderDetails(productName: "Red Mug", 31, "Gift Shop");
// PrintOrderDetails(31, sellerName: "Gift Shop", "Red Mug");
// PrintOrderDetails(31, "Red Mug", sellerName: "Gift Shop");
}

static void PrintOrderDetails(string sellerName, int orderNum, string productName)


{
if (string.IsNullOrWhiteSpace(sellerName))
{
throw new ArgumentException(message: "Seller name cannot be null or empty.", paramName:
nameof(sellerName));
}

Console.WriteLine($"Seller: {sellerName}, Order #: {orderNum}, Product: {productName}");


}
}

Optionale Argumente
Die Definition einer Methode, eines Konstruktors, eines Indexers oder eines Delegaten kann angeben, dass deren
Parameter benötigt werden oder optional sind. Jeder Aufruf muss Argumente für alle benötigten Parameter
bereitstellen, kann aber Argumente für optionale Parameter auslassen.
Jeder optionale Parameter hat einen Standardwert als Teil seiner Definition. Wenn für diesen Parameter kein
Argument gesendet wird, wird der Standardwert verwendet. Ein Standardwert muss einer der folgenden Typen
von Ausdrücken sein:
Ein konstanter Ausdruck
Ein Ausdruck der Form new ValType() , wobei ValType ein Werttyp wie enum oder struct ist
Ein Ausdruck in Form von default(ValType), wobei ValType ein Werttyp ist

Optionale Parameter werden am Ende der Parameterliste nach den erforderlichen Parametern definiert. Wenn
der Aufrufer ein Argument für einen beliebigen Parameter aus einer Folge von optionalen Parametern
bereitstellt, muss er Argumente für alle vorherigen optionalen Parameter bereitstellen. Durch Trennzeichen
getrennte Lücken in der Argumentliste werden nicht unterstützt. Im folgenden Code wird z.B. die
Instanzmethode ExampleMethod mit einem erforderlichen und zwei optionalen Parametern definiert.

public void ExampleMethod(int required, string optionalstr = "default string",


int optionalint = 10)

Der folgende Aufruf von ExampleMethod verursacht einen Compilerfehler, da ein Argument für den dritten
Parameter, aber nicht für den zweiten, bereitgestellt wird.

//anExample.ExampleMethod(3, ,4);

Wenn Sie den Namen des dritten Parameters kennen, können Sie ein benanntes Argument zum Ausführen der
Aufgabe verwenden.

anExample.ExampleMethod(3, optionalint: 4);

IntelliSense verwendet wie in der folgenden Abbildung gezeigt zum Anzeigen von optionalen Parametern
Klammern:

NOTE
Sie können auch optionale Parameter mit der .NET-Klasse OptionalAttribute deklarieren. OptionalAttribute -Parameter
erfordern keinen Standardwert.

Beispiel
Im folgenden Beispiel hat der Konstruktor für ExampleClass einen Parameter, der optional ist. Instanzmethode
ExampleMethod hat einen erforderlichen Parameter ( required ) und zwei optionale Parameter ( optionalstr und
optionalint ). Der Code in Main veranschaulicht die unterschiedlichen Methoden, in denen der Konstruktor
und die Methode aufgerufen werden können.

namespace OptionalNamespace
{
class OptionalExample
{
static void Main(string[] args)
{
// Instance anExample does not send an argument for the constructor's
// optional parameter.
ExampleClass anExample = new ExampleClass();
anExample.ExampleMethod(1, "One", 1);
anExample.ExampleMethod(2, "Two");
anExample.ExampleMethod(3);

// Instance anotherExample sends an argument for the constructor's


// optional parameter.
ExampleClass anotherExample = new ExampleClass("Provided name");
ExampleClass anotherExample = new ExampleClass("Provided name");
anotherExample.ExampleMethod(1, "One", 1);
anotherExample.ExampleMethod(2, "Two");
anotherExample.ExampleMethod(3);

// The following statements produce compiler errors.

// An argument must be supplied for the first parameter, and it


// must be an integer.
//anExample.ExampleMethod("One", 1);
//anExample.ExampleMethod();

// You cannot leave a gap in the provided arguments.


//anExample.ExampleMethod(3, ,4);
//anExample.ExampleMethod(3, 4);

// You can use a named parameter to make the previous


// statement work.
anExample.ExampleMethod(3, optionalint: 4);
}
}

class ExampleClass
{
private string _name;

// Because the parameter for the constructor, name, has a default


// value assigned to it, it is optional.
public ExampleClass(string name = "Default name")
{
_name = name;
}

// The first parameter, required, has no default value assigned


// to it. Therefore, it is not optional. Both optionalstr and
// optionalint have default values assigned to them. They are optional.
public void ExampleMethod(int required, string optionalstr = "default string",
int optionalint = 10)
{
Console.WriteLine(
$"{_name}: {required}, {optionalstr}, and {optionalint}.");
}
}

// The output from this example is the following:


// Default name: 1, One, and 1.
// Default name: 2, Two, and 10.
// Default name: 3, default string, and 10.
// Provided name: 1, One, and 1.
// Provided name: 2, Two, and 10.
// Provided name: 3, default string, and 10.
// Default name: 3, default string, and 4.
}

Der obige Code zeigt eine Reihe von Beispielen, in denen optionale Parameter nicht ordnungsgemäß
angewendet werden. Das erste Beispiel veranschaulicht, dass für den ersten Parameter, der erforderlich ist, ein
Argument angegeben werden muss.

COM-Schnittstellen
Benannte und optionale Argumente verbessern zusammen mit der Unterstützung von dynamischen Objekten
deutlich die Interoperabilität mit COM-APIs wie Office Automation-APIs.
Beispielsweise verfügt die Methode AutoFormat in der Range-Schnittstelle von Microsoft Office Excel über
sieben Parameter, von denen alle optional sind. Diese Parameter sind in der folgenden Abbildung dargestellt:
In C# 3.0 und früheren Versionen wird ein Argument für jeden Parameter benötigt, wie im folgenden Beispiel
gezeigt wird.

// In C# 3.0 and earlier versions, you need to supply an argument for


// every parameter. The following call specifies a value for the first
// parameter, and sends a placeholder value for the other six. The
// default values are used for those parameters.
var excelApp = new Microsoft.Office.Interop.Excel.Application();
excelApp.Workbooks.Add();
excelApp.Visible = true;

var myFormat =
Microsoft.Office.Interop.Excel.XlRangeAutoFormat.xlRangeAutoFormatAccounting1;

excelApp.get_Range("A1", "B4").AutoFormat(myFormat, Type.Missing,


Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing);

Sie können jedoch den Aufruf an AutoFormat erheblich vereinfachen, indem Sie benannte und optionale
Argumente verwenden, die in C# 4.0 eingeführt wurden. Benannte und optionale Parameter helfen Ihnen dabei,
das Argument für einen optionalen Parameter auszulassen, wenn Sie den Standardwert des Parameters nicht
ändern möchten. Im folgenden Aufruf wird ein Wert für nur einen der sieben Parameter angegeben.

// The following code shows the same call to AutoFormat in C# 4.0. Only
// the argument for which you want to provide a specific value is listed.
excelApp.Range["A1", "B4"].AutoFormat( Format: myFormat );

Weitere Informationen und Beispiele finden Sie unter Vorgehensweise: Verwenden von benannten und
optionalen Argumenten in der Office-Programmierung und Vorgehensweise: Zugreifen auf Office Interop-
Objekte mithilfe von C#-Funktionen.

Überladungsauflösung
Das Verwenden von benannten und optionalen Argumenten wirkt sich auf die Überladungsauflösung
folgendermaßen aus:
Eine Methode, ein Indexer oder ein Konstruktor ist ein Kandidat für die Ausführung, wenn jeder der
Parameter entweder optional ist oder über Namen oder Position auf ein einzelnes Argument in der
aufrufenden Anweisung reagiert. Dieses Argument kann in dem Typ des Parameters konvertiert werden.
Wenn mehr als ein Kandidat gefunden wird, werden die Regeln der Überladungsauflösung als bevorzugte
Konvertierungen auf die Argumente angewandt, die ausdrücklich angegeben sind. Ausgelassene Argumente
für optionale Parameter werden ignoriert.
Wenn zwei Kandidaten gleich gut geeignet sind, wird ein Kandidat bevorzugt, der keine optionalen Parameter
besitzt, für die Argumente im Aufruf ausgelassen wurden. Bei der Überladungsauflösung werden
normalerweise Kandidaten mit weniger Parametern bevorzugt.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
Gewusst wie: Verwenden von benannten und
optionalen Argumenten in der Office-
Programmierung (C#-Programmierhandbuch)
04.11.2021 • 5 minutes to read

Benannte und optionale Argumente, die in C# 4 eingeführt wurden, optimieren die Praxistauglichkeit, Flexibilität
und Lesbarkeit in der C#-Programmierung. Diese Funktionen erleichtern zusätzlich den Zugriff auf COM-
Schnittstellen wie etwa die Automatisierungs-APIs in Microsoft Office.
In folgendem Beispiel hat die Methode ConvertToTable sechzehn Parameter, die Eigenschaften einer Tabelle
repräsentieren, wie z.B. die Zeilen- und Spaltenanzahl, das Format, die Rahmen, Schriftarten und Farben. Alle
sechzehn Parameter sind optional, weil Sie oftmals keine bestimmten Werte für sie festlegen möchten. Ohne
benannte und optionale Argumente muss aber trotzdem ein Wert oder Platzhalterwert für jeden Parameter
angegeben werden. Mit benannten und optionalen Argumenten geben Sie nur für die Parameter Werte an, die
für Ihr Projekt erforderlich sind.
Microsoft Office Word muss auf Ihrem Computer installiert sein, damit Sie diesen Vorgang abschließen können.

NOTE
Auf Ihrem Computer werden möglicherweise andere Namen oder Speicherorte für die Benutzeroberflächenelemente von
Visual Studio angezeigt als die in den folgenden Anweisungen aufgeführten. Diese Elemente sind von der jeweiligen Visual
Studio-Version und den verwendeten Einstellungen abhängig. Weitere Informationen finden Sie unter Personalisieren der
IDE.

So erstellen Sie eine Konsolenanwendung


1. Starten Sie Visual Studio.
2. Zeigen Sie im Menü Datei auf Neu , und klicken Sie dann auf Projekt .
3. Erweitern Sie im Bereich Templates Categories (Vorlagenkategorien) den Eintrag Visual C# , und
klicken Sie dann auf Windows .
4. Sehen Sie am oberen Rand des Bereichs Vorlagen nach , um sicherzustellen, dass .NET Framework 4
im Feld Zielframework angezeigt wird.
5. Klicken Sie im Bereich Vorlagen auf Konsolenanwendung .
6. Geben Sie einen Namen für das Projekt im Feld Name ein.
7. Klicken Sie auf OK .
Das neue Projekt wird im Projektmappen-Explorer angezeigt.

So fügen Sie einen Verweis hinzu


1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Projektnamen, und klicken
Sie dann auf Ver weis hinzufügen . Das Dialogfeld Ver weis hinzufügen wird angezeigt.
2. Wählen Sie auf der Seite .NET Microsoft.Office.Interop.Word in der Liste Komponentenname aus.
3. Klicken Sie auf OK .

So fügen Sie erforderliche using-Anweisungen hinzu


1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf die Datei Program.cs und dann
auf Code anzeigen .
2. Fügen Sie am Anfang der Codedatei die folgenden using -Anweisungen hinzu:

using Word = Microsoft.Office.Interop.Word;

So zeigen Sie Text in einem Word-Dokument an


1. Fügen Sie in der Klasse Program in Program.cs die folgende Methode hinzu, um eine Word-Anwendung
und ein Word-Dokument zu erstellen. Die Methode Hinzufügen verfügt über vier optionale Parameter. In
diesem Beispiel werden ihre Standardwerte verwendet. Deshalb sind in der aufrufenden Anweisung
keine Argumente erforderlich.

static void DisplayInWord()


{
var wordApp = new Word.Application();
wordApp.Visible = true;
// docs is a collection of all the Document objects currently
// open in Word.
Word.Documents docs = wordApp.Documents;

// Add a document to the collection and name it doc.


Word.Document doc = docs.Add();
}

2. Fügen Sie den folgenden Code am Ende der Methode hinzu, um zu definieren, wo der Text im Dokument
angezeigt werden und um welchen Text es sich dabei handeln soll:

// Define a range, a contiguous area in the document, by specifying


// a starting and ending character position. Currently, the document
// is empty.
Word.Range range = doc.Range(0, 0);

// Use the InsertAfter method to insert a string at the end of the


// current range.
range.InsertAfter("Testing, testing, testing. . .");

So führen Sie die Anwendung aus


1. Fügen Sie die folgende Anweisung in Main hinzu:

DisplayInWord();

2. Drücken Sie STRG+F5, um das Projekt auszuführen. Ein Word-Dokument mit dem angegebenen Text wird
angezeigt.

So ändern Sie Text in eine Tabelle


1. Verwenden Sie die ConvertToTable -Methode, um den Text in eine Tabelle einzuschließen. Die Methode
verfügt über sechzehn optionale Parameter. IntelliSense schließt optionale Parameter in Klammern ein,
wie in folgender Abbildung veranschaulicht.

Benannte und optionale Argumente ermöglichen es Ihnen, nur Werte für die Parameter anzugeben, die
Sie auch ändern möchten. Fügen Sie den folgenden Code am Ende der DisplayInWord -Methode hinzu,
um eine einfache Tabelle zu erstellen. Das Argument gibt an, dass die Kommas in der Textzeichenfolge in
range die Zelle der Tabelle trennen.

// Convert to a simple table. The table will have a single row with
// three columns.
range.ConvertToTable(Separator: ",");

In früheren Versionen von C# erfordert der Aufruf von ConvertToTable ein Verweisargument für jeden
Parameter, wie in folgendem Codebeispiel gezeigt:

// Call to ConvertToTable in Visual C# 2008 or earlier. This code


// is not part of the solution.
var missing = Type.Missing;
object separator = ",";
range.ConvertToTable(ref separator, ref missing, ref missing,
ref missing, ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing, ref missing,
ref missing);

2. Drücken Sie STRG+F5, um das Projekt auszuführen.

So können Sie sich auch an anderen Parametern versuchen


1. Um die Tabelle so zu ändern, dass sie nur noch eine Spalte und drei Zeilen hat, ersetzen Sie die letzte Zeile
in DisplayInWord durch folgende Anweisung, und geben Sie dann STRG+F5 ein.

range.ConvertToTable(Separator: ",", AutoFit: true, NumColumns: 1);

2. Um ein vordefiniertes Format für die Tabelle anzugeben, ersetzen Sie die letzte Zeile in DisplayInWord
durch folgende Anweisung, und geben Sie dann STRG+F5 ein. Das Format kann jede der
WdTableFormat-Konstanten sein.

range.ConvertToTable(Separator: ",", AutoFit: true, NumColumns: 1,


Format: Word.WdTableFormat.wdTableFormatElegant);

Beispiel
Der folgende Code enthält das vollständige Beispiel:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Word = Microsoft.Office.Interop.Word;

namespace OfficeHowTo
{
class WordProgram
{
static void Main(string[] args)
{
DisplayInWord();
}

static void DisplayInWord()


{
var wordApp = new Word.Application();
wordApp.Visible = true;
// docs is a collection of all the Document objects currently
// open in Word.
Word.Documents docs = wordApp.Documents;

// Add a document to the collection and name it doc.


Word.Document doc = docs.Add();

// Define a range, a contiguous area in the document, by specifying


// a starting and ending character position. Currently, the document
// is empty.
Word.Range range = doc.Range(0, 0);

// Use the InsertAfter method to insert a string at the end of the


// current range.
range.InsertAfter("Testing, testing, testing. . .");

// You can comment out any or all of the following statements to


// see the effect of each one in the Word document.

// Next, use the ConvertToTable method to put the text into a table.
// The method has 16 optional parameters. You only have to specify
// values for those you want to change.

// Convert to a simple table. The table will have a single row with
// three columns.
range.ConvertToTable(Separator: ",");

// Change to a single column with three rows..


range.ConvertToTable(Separator: ",", AutoFit: true, NumColumns: 1);

// Format the table.


range.ConvertToTable(Separator: ",", AutoFit: true, NumColumns: 1,
Format: Word.WdTableFormat.wdTableFormatElegant);
}
}
}

Siehe auch
Benannte und optionale Argumente
Konstruktoren (C#-Programmierleitfaden)
04.11.2021 • 2 minutes to read

Wenn eine class oder struct erstellt wird, wird deren Konstruktor aufgerufen. Eine Klasse oder Struktur verfügt
möglicherweise über mehrere Konstruktoren, die andere Argumente verwenden. Mit Konstruktoren können
Programmierer Standardwerte festlegen, Instanziierungen einschränken und Code schreiben, der flexibel und
leicht zu lesen ist. Weitere Informationen und Beispiele finden Sie unter Instanzkonstruktoren und Verwenden
von Konstruktoren.

Konstruktorsyntax
Ein Konstruktor ist eine Methode, dessen Name derselbe ist wie der seines Typs. Die Methodensignatur enthält
nur den Methodennamen und die Parameterliste; ein Rückgabetyp ist nicht enthalten. Im folgenden Beispiel
wird der Konstruktor für eine Klasse mit dem Namen Person gezeigt.

public class Person


{
private string last;
private string first;

public Person(string lastName, string firstName)


{
last = lastName;
first = firstName;
}

// Remaining implementation of Person class.


}

Wenn ein Konstruktor als einzelne Anweisung implementiert werden kann, können Sie eine expression body
definition (Ausdruckstextdefinition) verwenden. Im folgenden Beispiel wird eine Location -Klasse definiert,
deren Klassenkonstruktor einen einzelnen Zeichenfolgenparameter namens name enthält. Die
Ausdruckstextdefinition weist das Argument dem Feld locationName zu.

public class Location


{
private string locationName;

public Location(string name) => Name = name;

public string Name


{
get => locationName;
set => locationName = value;
}
}

Statische Konstruktoren
Die vorherigen Beispiele haben alle Instanzkonstruktoren gezeigt, die ein neues Objekt erstellen. Eine Klasse
oder Struktur kann auch einen statischen Konstruktor haben, der statische Member dieses Typs initialisiert.
Statische Konstruktoren sind parameterlos. Wenn Sie keinen statischen Konstruktor zum Initialisieren von
statischen Feldern angeben, initialisiert der C#-Compiler statische Felder mit ihrem Standardwert, wie unter
Standardwerte der C#-Typen aufgeführt.
Im folgenden Beispiel wird ein statischer Konstruktor verwendet, um ein statisches Feld zu initialisieren.

public class Adult : Person


{
private static int minimumAge;

public Adult(string lastName, string firstName) : base(lastName, firstName)


{ }

static Adult()
{
minimumAge = 18;
}

// Remaining implementation of Adult class.


}

Sie können einen statischen Konstruktor auch mit einer Ausdruckstextdefinition definieren, wie im folgenden
Beispiel gezeigt.

public class Child : Person


{
private static int maximumAge;

public Child(string lastName, string firstName) : base(lastName, firstName)


{ }

static Child() => maximumAge = 18;

// Remaining implementation of Child class.


}

Weitere Informationen und Beispiele finden Sie unter Statische Konstruktoren.

In diesem Abschnitt
Verwenden von Konstruktoren
Instanzkonstruktoren
Private Konstruktoren
Statische Konstruktoren
Schreiben eines Kopierkonstruktors

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Finalizer
static
Why Do Initializers Run In The Opposite Order As Constructors? Part One (Warum werden Initialisierer In der
entgegengesetzten Reihenfolge ausgeführt wie Konstruktoren? Teil Eins)
Verwenden von Konstruktoren (C#-
Programmierhandbuch)
04.11.2021 • 4 minutes to read

Wenn eine Klasse oder Struktur erstellt wird, wird deren Konstruktor aufgerufen. Konstruktoren haben den
gleichen Namen wie die Klasse oder Struktur, und sie initialisieren normalerweise die Datenmember des neuen
Objekts.
Im folgenden Beispiel wird eine Klasse mit dem Namen Taxi durch Verwendung eines einfachen Konstruktors
definiert. Die Klasse wird dann mit dem new-Operator instanziiert. Der Taxi -Konstruktor wird sofort durch den
new -Operator aufgerufen, nachdem Speicher für das neue Objekt zugewiesen wurde.

public class Taxi


{
public bool IsInitialized;

public Taxi()
{
IsInitialized = true;
}
}

class TestTaxi
{
static void Main()
{
Taxi t = new Taxi();
Console.WriteLine(t.IsInitialized);
}
}

Ein Konstruktor, der keine Parameter akzeptiert, wird parameterloser Konstruktor genannt. Parameterlose
Konstruktoren werden aufgerufen, wenn ein Objekt durch Verwendung des Operators new instanziiert wird
und keine Argumente für new bereitgestellt werden. Weitere Informationen finden Sie unter
Instanzkonstruktoren.
Klassen ohne Konstruktoren erhalten vom C#-Compiler einen öffentlichen parameterlosen Konstruktor, um die
Instanziierung der Klasse zuzulassen, außer die Klasse ist static. Weitere Informationen finden Sie unter Statische
Klassen und statische Klassenmember.
Sie können verhindern, dass eine Klasse instanziiert wird, indem Sie den Konstruktor auf „privat“ einstellen; dazu
müssen Sie wie folgt vorgehen:

class NLog
{
// Private Constructor:
private NLog() { }

public static double e = Math.E; //2.71828...


}

Weitere Informationen finden Sie unter Private Konstruktoren.


Konstruktoren für struct-Typen ähneln Klassenkonstruktoren, structs können aber keinen expliziten
parameterlosen Konstruktor enthalten, da er automatisch vom Compiler bereitgestellt wird. Dieser Konstruktor
initialisiert alle Felder in struct auf die Standardwerte. Dieser parameterlose Konstruktor wird jedoch nur
aufgerufen, wenn struct mit new instanziiert wird. Dieser Code verwendet den parameterlosen Konstruktor
z.B. für Int32, damit sichergestellt ist, dass der ganzzahlige Typ initialisiert wird:

int i = new int();


Console.WriteLine(i);

NOTE
Ab C# 10.0 kann ein Strukturtyp einen expliziten parameterlosen Konstruktor enthalten. Weitere Informationen finden Sie
im Abschnitt Parameterlose Konstruktoren und Feldinitialisierer des Artikels Strukturtypen.

Der folgende Code verursacht jedoch einen Compilerfehler, da nicht new verwendet wird, und da versucht wird,
ein Objekt zu verwenden, das nicht initialisiert wurde:

int i;
Console.WriteLine(i);

Alternativ können auch Objekte, die auf structs basieren, – einschließlich aller integrierten numerischen Typen
– initialisiert oder zugewiesen und anschließend verwendet werden, so wie im folgenden Beispiel:

int a = 44; // Initialize the value type...


int b;
b = 33; // Or assign it before using it.
Console.WriteLine("{0}, {1}", a, b);

Es ist also nicht erforderlich, einen parameterlosen Konstruktor für einen Werttyp aufzurufen.
Sowohl Klassen als auch structs können Konstruktoren definieren, die Parameter annehmen. Konstruktoren,
die Parameter annehmen, müssen über eine new - oder base-Anweisung aufgerufen werden. Klassen und
structs können also mehrere Konstruktoren definieren, und keine von beiden wird zum Definieren eines
parameterlosen Konstruktors benötigt. Zum Beispiel:

public class Employee


{
public int Salary;

public Employee() { }

public Employee(int annualSalary)


{
Salary = annualSalary;
}

public Employee(int weeklySalary, int numberOfWeeks)


{
Salary = weeklySalary * numberOfWeeks;
}
}

Diese Klasse kann mithilfe aller folgenden Anweisungen erstellt werden:


Employee e1 = new Employee(30000);
Employee e2 = new Employee(500, 52);

Ein Konstruktor kann das Schlüsselwort base verwenden, um den Konstruktor einer Basisklasse aufzurufen.
Zum Beispiel:

public class Manager : Employee


{
public Manager(int annualSalary)
: base(annualSalary)
{
//Add further instructions here.
}
}

In diesem Beispiel wird der Konstruktor der Basisklasse aufgerufen, bevor der Block eines Konstruktors
ausgeführt wird. Das Schlüsselwort base kann mit oder ohne Parameter verwendet werden. Alle Parameter des
Konstruktors können als Parameter für base oder als Teil eines Ausdrucks verwendet werden. Weitere
Informationen finden Sie unter base.
Wenn ein Konstruktor der Basisklasse in einer abgeleiteten Klasse nicht explizit durch das Schlüsselwort base
aufgerufen wird, wird der parameterlose Konstruktor (falls vorhanden) implizit aufgerufen. Dies bedeutet, dass
die folgenden Deklarationen identisch sind:

public Manager(int initialData)


{
//Add further instructions here.
}

public Manager(int initialData)


: base()
{
//Add further instructions here.
}

Wenn eine Basisklasse keinen parameterlosen Konstruktor bereitstellt, muss die abgeleitete Klasse einen
expliziten Aufruf an den Basiskonstruktor mithilfe von base durchführen.
Ein Konstruktor kann einen anderen Konstruktor in demselben Objekt über das Schlüsselwort this aufrufen.
Genau wie base kann this mit oder ohne Parameter verwendet werden, und alle Parameter im Konstruktor
sind als Parameter für this oder als Teil eines Ausdrucks verfügbar. Der zweite Konstruktor im vorherigen
Beispiel kann z.B. über this neu geschrieben werden:

public Employee(int weeklySalary, int numberOfWeeks)


: this(weeklySalary * numberOfWeeks)
{
}

Die Verwendung des Schlüsselworts this im vorherigen Beispiel bewirkt, dass dieser Konstruktor aufgerufen
wird:
public Employee(int annualSalary)
{
Salary = annualSalary;
}

Konstruktoren können als public, private, protected, internal, protected internal oder private protected markiert
werden. Diese Zugriffsmodifizierer definieren, wie Benutzer der Klasse die Klasse konstruieren können. Weitere
Informationen finden Sie unter Zugriffsmodifizierer.
Ein Konstruktor kann mithilfe des Schlüsselworts static als statisch deklariert werden. Statische Konstruktoren
werden automatisch aufgerufen, unmittelbar bevor auf ein statisches Feld zugegriffen wird, und werden
generell zum Initialisieren statischer Klassenmember verwendet. Weitere Informationen finden Sie unter
Statische Konstruktoren.

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter Instanzkonstruktoren und Statische Konstruktoren in der C#-
Sprachspezifikation. Die Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Konstruktoren
Finalizer
Instanzkonstruktoren (C#-Programmierleitfaden)
04.11.2021 • 2 minutes to read

Sie deklarieren einen Instanzkonstruktor, um den Code anzugeben, der ausgeführt wird, wenn Sie eine neue
Instanz eines Typs mit dem new -Ausdruck erstellen. Um eine statische Klasse oder eine statische Variable in
einer nicht statischen Klasse zu initialisieren, können Sie einen statischen Konstruktor definieren.
Wie im folgenden Beispiel gezeigt, können Sie mehrere Instanzkonstruktoren in einem Typ deklarieren:

using System;

class Coords
{
public Coords()
: this(0, 0)
{ }

public Coords(int x, int y)


{
X = x;
Y = y;
}

public int X { get; set; }


public int Y { get; set; }

public override string ToString() => $"({X},{Y})";


}

class Example
{
static void Main()
{
var p1 = new Coords();
Console.WriteLine($"Coords #1 at {p1}");
// Output: Coords #1 at (0,0)

var p2 = new Coords(5, 3);


Console.WriteLine($"Coords #2 at {p2}");
// Output: Coords #2 at (5,3)
}
}

Im vorherigen Beispiel ruft der erste parameterlose Konstruktor den zweiten Konstruktor auf, wobei beide
Argumente gleich 0 sind. Verwenden Sie dazu das Schlüsselwort this .
Wenn Sie einen Instanzkonstruktor in einer abgeleiteten Klasse deklarieren, können Sie einen Konstruktor einer
Basisklasse aufrufen. Verwenden Sie dazu das Schlüsselwort base , wie im folgenden Beispiel gezeigt:
using System;

abstract class Shape


{
public const double pi = Math.PI;
protected double x, y;

public Shape(double x, double y)


{
this.x = x;
this.y = y;
}

public abstract double Area();


}

class Circle : Shape


{
public Circle(double radius)
: base(radius, 0)
{ }

public override double Area() => pi * x * x;


}

class Cylinder : Circle


{
public Cylinder(double radius, double height)
: base(radius)
{
y = height;
}

public override double Area() => (2 * base.Area()) + (2 * pi * x * y);


}

class Example
{
static void Main()
{
double radius = 2.5;
double height = 3.0;

var ring = new Circle(radius);


Console.WriteLine($"Area of the circle = {ring.Area():F2}");
// Output: Area of the circle = 19.63

var tube = new Cylinder(radius, height);


Console.WriteLine($"Area of the cylinder = {tube.Area():F2}");
// Output: Area of the cylinder = 86.39
}
}

Parameterlose Konstruktoren
Wenn eine Klasse keine expliziten Instanzkonstruktoren hat, stellt C# einen parameterlosen Konstruktor bereit,
mit dem Sie eine Instanz dieser Klasse instanziieren können. Dies wird im folgenden Beispiel gezeigt:
using System;

public class Person


{
public int age;
public string name = "unknown";
}

class Example
{
static void Main()
{
var person = new Person();
Console.WriteLine($"Name: {person.name}, Age: {person.age}");
// Output: Name: unknown, Age: 0
}
}

Dieser Konstruktor initialisiert Instanzfelder und -eigenschaften gemäß den entsprechenden Initialisierern.
Wenn ein Feld oder eine Eigenschaft keinen Initialisierer hat, wird der Wert auf den Standardwert des Feld- oder
Eigenschaftstyps festgelegt. Wenn Sie mindestens einen Instanzkonstruktor in einer Klasse deklarieren, stellt C#
keinen parameterlosen Konstruktor bereit.
Ein Strukturtyp stellt einen parameterlosen Konstruktor immer wie folgt bereit:
In C# 9.0 und früher ist dies ein impliziter parameterloser Konstruktor, der den Standardwert eines Typs
erzeugt.
In C# 10.0 und höher ist dies entweder ein impliziter parameterloser Konstruktor, der den Standardwert eines
Typs erzeugt, oder ein explizit deklarierter parameterloser Konstruktor. Weitere Informationen finden Sie im
Abschnitt Parameterlose Konstruktoren und Feldinitialisierer des Artikels Strukturtypen.

Siehe auch
C#-Programmierhandbuch
Klassen, Strukturen und Datensätze
Konstruktoren
Finalizer
base
this
Private Konstruktoren (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Ein privater Konstruktor ist ein spezieller Instanzenkonstruktor. Er wird im Allgemeinen in Klassen verwenden,
die nur statische Member enthalten. Wenn eine Klasse einen oder mehrere private und keine öffentlichen
Konstruktoren besitzt, können andere Klassen (außer geschachtelte Klassen) keine Instanzen dieser Klasse
erstellen. Zum Beispiel:

class NLog
{
// Private Constructor:
private NLog() { }

public static double e = Math.E; //2.71828...


}

Die Deklaration des leeren Konstruktors verhindert die automatische Erstellung eines parameterlosen
Konstruktors. Beachten Sie, dass wenn Sie keinen Zugriffsmodifizierer mit dem Konstruktor verwenden, der
Konstruktor standardmäßig weiterhin privat ist. Allerdings werden die privaten Modifizierer normalerweise
ausdrücklich verwendet, um klarzustellen, dass die Klasse nicht instanziiert werden kann.
Private Konstruktoren werden verwendet, um das Erstellen von Instanzen einer Klasse zu verhindern, wenn es
keine Instanzfelder oder -methoden wie die Math-Klasse gibt oder wenn eine Methode aufgerufen wird, um eine
Instanz einer Klasse abzurufen. Wenn alle Methoden in der Klasse statisch sind, erwägen Sie es, die komplette
Klasse statisch zu machen. Weitere Informationen finden Sie unter Statische Klassen und statische
Klassenmember.

Beispiel
Folgendes ist ein Beispiel einer Klasse, die einen privaten Konstruktor verwendet.
public class Counter
{
private Counter() { }

public static int currentCount;

public static int IncrementCount()


{
return ++currentCount;
}
}

class TestCounter
{
static void Main()
{
// If you uncomment the following statement, it will generate
// an error because the constructor is inaccessible:
// Counter aCounter = new Counter(); // Error

Counter.currentCount = 100;
Counter.IncrementCount();
Console.WriteLine("New count: {0}", Counter.currentCount);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
// Output: New count: 101

Beachten Sie, dass wenn Sie die Auskommentierung der folgenden Anweisung aus dem Beispiel aufheben, ein
Fehler erzeugt wird, weil auf den Konstruktor aufgrund seiner Schutzebene nicht zugegriffen werden kann:

// Counter aCounter = new Counter(); // Error

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter Private Konstruktoren in der C#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Konstruktoren
Finalizer
private
public
Statische Konstruktoren (C#-
Programmierhandbuch)
04.11.2021 • 4 minutes to read

Ein statischer Konstruktor wird verwendet, um static-Daten zu initialisieren oder um eine bestimmte Aktion
auszuführen, die nur einmal ausgeführt werden muss. Er wird automatisch aufgerufen, bevor die erste Instanz
erstellt oder auf irgendwelche statischen Member verwiesen wird.

class SimpleClass
{
// Static variable that must be initialized at run time.
static readonly long baseline;

// Static constructor is called at most one time, before any


// instance constructor is invoked or member is accessed.
static SimpleClass()
{
baseline = DateTime.Now.Ticks;
}
}

Hinweise
Statische Konstruktoren verfügen über folgende Eigenschaften:
Ein statischer Konstruktor akzeptiert keine Zugriffsmodifizierer oder Parameter.
Eine Klasse oder Struktur kann nur einen statischen Konstruktor besitzen.
Statische Konstruktoren können nicht vererbt oder überladen werden.
Ein statischer Konstruktor kann nicht direkt aufgerufen werden und ist nur für den Aufruf durch die
Common Language Runtime (CLR) gedacht. Er wird automatisch aufgerufen.
Der Benutzer hat keine Kontrolle, wenn der statische Konstruktor im Programm ausgeführt wird.
Ein statischer Konstruktor wird automatisch aufgerufen. Er initialisiert die Klasse, bevor die erste Instanz
erstellt oder auf einen statischen Member verwiesen wird. Ein statischer Konstruktor wird vor einem
Instanzkonstruktor ausgeführt. Der statische Konstruktor eines Typs wird aufgerufen, wenn eine statische
Methode, die einem Ereignis oder Delegaten zugewiesen ist, aufgerufen wird. Dies erfolgt nicht während
der Zuweisung. Wenn Variableninitialisierer für statische Felder in der Klasse des statischen Konstruktors
vorhanden sind, werden sie in der Textreihenfolge ausgeführt, in der sie in der Klassendeklaration
vorhanden sind. Die Initialisierer werden direkt vor Ausführung des statischen Konstruktors ausgeführt.
Wenn Sie keinen statischen Konstruktor zum Initialisieren von statischen Feldern angeben, werden alle
statischen Felder mit ihrem Standardwert initialisiert, wie unter Standardwerte für C#-Typen aufgeführt.
Wenn ein statischer Konstruktor eine Ausnahme auslöst, ruft ihn die Laufzeit kein zweites Mal auf, und
der Typ bleibt für die Lebensdauer der Anwendungsdomäne nicht initialisiert. Eine
TypeInitializationException-Ausnahme wird in aller Regel ausgelöst, wenn ein statischer Konstruktor
keinen Typ instanziieren kann oder wenn in einem statischen Konstruktor ein Ausnahmefehler auftritt. Bei
statischen Konstruktoren, die im Quellcode nicht explizit definiert sind, ist zur Problembehandlung
möglicherweise eine Prüfung des IL-Codes (Intermediate Language, Zwischensprache) erforderlich.
Die Existenz eines statischen Konstruktors verhindert die Hinzufügung des BeforeFieldInit-Typattributs.
Dadurch wird die Laufzeitoptimierung eingeschränkt.
Ein als static readonly deklariertes Feld darf nur als Teil der Deklaration oder in einem statischen
Konstruktor zugewiesen werden. Wenn ein expliziter statischer Konstruktor nicht erforderlich ist,
initialisieren Sie statische Felder in der Deklaration anstatt über einen statischen Konstruktor, um die
Laufzeitoptimierung zu verbessern.
Die Runtime ruft einen statischen Konstruktor höchstens einmal in einer einzelnen Anwendungsdomäne
auf. Dieser Aufruf erfolgt in einem gesperrten Bereich, der auf dem spezifischen Typ der Klasse basiert. Im
Text eines statischen Konstruktors sind keine zusätzlichen Sperrmechanismen erforderlich. Um das Risiko
von Deadlocks zu vermeiden, blockieren Sie den aktuellen Thread in statischen Konstruktoren und
Initialisierern nicht. Warten Sie z. B. nicht auf Aufgaben, Threads, Wait-Handles oder Ereignisse, richten Sie
keine Sperren ein, und führen Sie keine blockierenden parallelen Vorgänge wie parallele Schleifen,
Parallel.Invoke und parallele LINQ-Abfragen aus.

NOTE
Auch wenn auf einen expliziten statischen Konstruktor nicht direkt zugegriffen werden kann, sollte seine Existenz dennoch
dokumentiert werden, um bei der Fehlerbehebung von Initialisierungsausnahmen zu helfen.

Verwendung
Eine typische Verwendung statischer Konstruktoren besteht darin, wenn die Klasse eine Protokolldatei
verwendet und der Konstruktor verwendet wird, um Einträge in dieser Datei zu schreiben.
Statische Konstruktoren sind auch beim Erstellen von Wrapperklassen für nicht verwalteten Code
nützlich, wenn der Konstruktor die LoadLibrary -Methode aufrufen kann.
Statische Konstruktoren sind auch eine praktische Möglichkeit, um Laufzeitüberprüfungen des
Typparameters über Typparametereinschränkungen zu erzwingen, die zur Kompilierzeit nicht überprüft
werden können.

Beispiel
In diesem Beispiel verfügt die Klasse Bus über einen statischen Konstruktor. Wenn die erste Instanz von Bus
erstellt wird ( bus1 ), wird der statische Konstruktor zur Initialisierung der Klasse aufgerufen. Die Beispielausgabe
überprüft, ob der statische Konstruktor nur einmal ausgeführt wird, obwohl zwei Instanzen von Bus erstellt
werden, und dass er vor dem Ausführen des Instanzkonstruktors ausgeführt wird.

public class Bus


{
// Static variable used by all Bus instances.
// Represents the time the first bus of the day starts its route.
protected static readonly DateTime globalStartTime;

// Property for the number of each bus.


protected int RouteNumber { get; set; }

// Static constructor to initialize the static variable.


// It is invoked before the first instance constructor is run.
static Bus()
{
globalStartTime = DateTime.Now;

// The following statement produces the first line of output,


// and the line occurs only once.
Console.WriteLine("Static constructor sets global start time to {0}",
globalStartTime.ToLongTimeString());
}

// Instance constructor.
public Bus(int routeNum)
{
RouteNumber = routeNum;
Console.WriteLine("Bus #{0} is created.", RouteNumber);
}

// Instance method.
public void Drive()
{
TimeSpan elapsedTime = DateTime.Now - globalStartTime;

// For demonstration purposes we treat milliseconds as minutes to simulate


// actual bus times. Do not do this in your actual bus schedule program!
Console.WriteLine("{0} is starting its route {1:N2} minutes after global start time {2}.",
this.RouteNumber,
elapsedTime.Milliseconds,
globalStartTime.ToShortTimeString());
}
}

class TestBus
{
static void Main()
{
// The creation of this instance activates the static constructor.
Bus bus1 = new Bus(71);

// Create a second bus.


Bus bus2 = new Bus(72);

// Send bus1 on its way.


bus1.Drive();

// Wait for bus2 to warm up.


System.Threading.Thread.Sleep(25);

// Send bus2 on its way.


bus2.Drive();

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Sample output:
Static constructor sets global start time to 3:57:08 PM.
Bus #71 is created.
Bus #72 is created.
71 is starting its route 6.00 minutes after global start time 3:57 PM.
72 is starting its route 31.00 minutes after global start time 3:57 PM.
*/

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Statische Konstruktoren der C#-Sprachspezifikation.

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Konstruktoren
Statische Klassen und statische Klassenmember
Finalizer
Entwurfsrichtlinien für Konstruktoren
Sicherheitswarnung – CA2121: Statische Konstruktoren sollten privat sein.
Gewusst wie: Schreiben eines Kopierkonstruktors
(C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

C#-Datensätze bieten einen Kopierkonstruktor für Objekte, jedoch müssen Sie für Klassen selbst einen
schreiben.

Beispiel
Im folgenden Beispiel wird von der Person -Klasse ein Kopierkonstruktor definiert, der eine Instanz von Person
als sein Argument annimmt. Die Werte der Eigenschaften des Arguments werden den Eigenschaften der neuen
Instanz von Person zugewiesen. Der Code enthält einen alternativen Kopierkonstruktor, der die Name - und Age
-Eigenschaften der Instanz sendet, die Sie in den Instanzenkonstruktor der Klasse kopieren möchten.
using System;

class Person
{
// Copy constructor.
public Person(Person previousPerson)
{
Name = previousPerson.Name;
Age = previousPerson.Age;
}

//// Alternate copy constructor calls the instance constructor.


//public Person(Person previousPerson)
// : this(previousPerson.Name, previousPerson.Age)
//{
//}

// Instance constructor.
public Person(string name, int age)
{
Name = name;
Age = age;
}

public int Age { get; set; }

public string Name { get; set; }

public string Details()


{
return Name + " is " + Age.ToString();
}
}

class TestPerson
{
static void Main()
{
// Create a Person object by using the instance constructor.
Person person1 = new Person("George", 40);

// Create another Person object, copying person1.


Person person2 = new Person(person1);

// Change each person's age.


person1.Age = 39;
person2.Age = 41;

// Change person2's name.


person2.Name = "Charles";

// Show details to verify that the name and age fields are distinct.
Console.WriteLine(person1.Details());
Console.WriteLine(person2.Details());

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
// Output:
// George is 39
// Charles is 41

Siehe auch
ICloneable
Datensätze
C#-Programmierhandbuch
Das C#-Typsystem
Konstruktoren
Finalizer
Finalizer (C#-Programmierhandbuch)
04.11.2021 • 4 minutes to read

Mit Finalizern (früher als Destruktoren bezeichnet) werden alle erforderlichen endgültigen Bereinigungen
durchgeführt, wenn eine Klasseninstanz vom Garbage Collector gesammelt wird. In den meisten Fällen können
Sie das Schreiben eines Finalizers vermeiden, indem Sie System.Runtime.InteropServices.SafeHandle oder
abgeleitete Klassen verwenden, um alle nicht verwalteten Handle einzuschließen.

Hinweise
Finalizer können nicht in Strukturen definiert werden. Sie werden nur mit Klassen verwendet.
Eine Klasse kann nur über einen Finalizer verfügen.
Finalizer können nicht vererbt oder überladen werden.
Finalizer können nicht aufgerufen werden. Sie werden automatisch aufgerufen.
Ein Finalizer kann nicht über Modifizierer oder Parameter verfügen.
Folgendes ist z.B. eine Deklaration eines Finalizers für die Klasse Car :

class Car
{
~Car() // finalizer
{
// cleanup statements...
}
}

Ein Finalizer kann auch als Ausdruckstextdefinition implementiert werden, wie im folgenden Beispiel gezeigt.

using System;

public class Destroyer


{
public override string ToString() => GetType().Name;

~Destroyer() => Console.WriteLine($"The {ToString()} finalizer is executing.");


}

Der Finalizer ruft Finalize implizit auf der Basisklasse des Objekts auf. Daher wird der Aufruf eines Finalizers
implizit in den folgenden Code übersetzt:

protected override void Finalize()


{
try
{
// Cleanup statements...
}
finally
{
base.Finalize();
}
}

Dies bedeutet, dass die Finalize -Methode für alle Instanzen in der Vererbungskette rekursiv aufgerufen wird,
von der am meisten bis zu der am wenigsten abgeleiteten.

NOTE
Leere Finalizer sollten nicht verwendet werden. Wenn eine Klasse einen Finalizer enthält, wird ein Eintrag in der Finalize
-Warteschlange erstellt. Diese Warteschlange wird vom Garbage Collector verarbeitet. Während der Verarbeitung der
Warteschlange ruft der GC jeden Finalizer auf. Unnötige Finalizer, darunter leere Finalizer, Finalizer, die nur den Finalizer der
Basisklasse aufrufen, oder Finalizer, die nur bedingt ausgegebene Methoden aufrufen, führen zu einem unnötigen
Leistungsverlust.

Der Programmierer hat keine Kontrolle darüber, wann der Finalizer aufgerufen wird, da der Garbage Collector
den Zeitpunkt des Aufrufs bestimmt. Der Garbage Collector sucht nach Objekten, die von der Anwendung nicht
mehr verwendet werden. Wenn er ein Objekt als abschließbar angesehen wird, ruft er den Finalizer auf (sofern
vorhanden) und fordert den Arbeitsspeicher, der zum Speichern des Objekts verwendet wurde, zurück. Es ist
möglich, die Garbage Collection durch Aufrufen von Collect zu erzwingen. Dieser Aufruf sollte jedoch meistens
vermieden werden, da er Leistungsprobleme hervorrufen kann.

NOTE
Ob Finalizer im Rahmen der Beendigung der Anwendung ausgeführt werden, hängt von der jeweiligen Implementierung
von .NET ab. Wenn eine Anwendung beendet wird, bemüht sich .NET Framework nach Kräften, Finalizer für Objekte
aufzurufen, für die noch keine Garbage Collection durchgeführt wurde, es sei denn, eine solche Bereinigung wurde
unterdrückt (z. B. durch einen Aufruf der Bibliotheksmethode GC.SuppressFinalize ). .NET 5 (einschließlich .NET Core)
und höhere Versionen rufen Finalizer nicht als Teil der Anwendungsbeendigung auf. Weitere Informationen finden Sie
unter dem GitHub-Issue dotnet/csharpstandard #291.

Wenn beim Beenden einer Anwendung eine Bereinigung zuverlässig ausgeführt werden muss, registrieren Sie
einen Handler für das Ereignis System.AppDomain.ProcessExit. Durch diesen Handler wird sichergestellt, dass
IDisposable.Dispose() (oder IAsyncDisposable.DisposeAsync()) für alle Objekte aufgerufen wurde, die vor dem
Beenden der Anwendung eine Bereinigung erfordern. Da Sie Finalize nicht direkt aufrufen können und nicht
garantieren können, dass der Garbage Collector vor dem Beenden alle Finalizer aufruft, müssen Sie Dispose
oder DisposeAsync verwenden, um sicherzustellen, dass Ressourcen freigegeben werden.

Verwenden von Finalizern zum Freigeben von Ressourcen


Im Allgemeinen erfordert C# nicht so viel Speicherverwaltung seitens des Entwicklers wie Sprachen, die mit der
Garbage Collection nicht auf eine Laufzeit abzielen. Der Garbage Collector von .NET verwaltet implizit die
Belegung und Freigabe von Arbeitsspeicher für Ihre Objekte. Wenn Ihre Anwendung nicht verwaltete
Ressourcen wie z. B. Fenster, Dateien und Netzwerkverbindungen kapselt, sollten Sie Finalizer verwenden, um
die Ressourcen freizugeben. Wenn das Objekt abgeschlossen werden kann, führt der Garbage Collector die
Finalize -Methode des Objekts aus.

Explizite Freigabe von Ressourcen


Wenn Ihre Anwendung eine umfangreiche externe Ressource verwendet, wird außerdem empfohlen, dass Sie
eine Möglichkeit bieten, die Ressource explizit freizugeben, bevor der Garbage Collector das Objekt freigibt.
Implementieren Sie zum Freigeben der Ressource eine Dispose -Methode aus der Schnittstelle IDisposable, die
die erforderliche Bereinigung für das Objekt durchführt. Dies kann die Leistung der Anwendung erheblich
verbessern. Trotz dieser expliziten Kontrolle über Ressourcen wird der Finalizer Ressourcen sicher bereinigen,
wenn der Aufruf der Dispose -Methode fehlschlägt.
Weitere Informationen zum Bereinigen von Ressourcen finden Sie in den folgenden Artikeln:
Bereinigen von nicht verwalteten Ressourcen
Implementieren einer Dispose-Methode
Implementieren einer DisposeAsync-Methode
Using-Anweisung

Beispiel
Das folgende Beispiel erstellt drei Klassen, die eine Vererbungskette bilden. Die Klasse First ist die Basisklasse,
Second wird von First abgeleitet, und Third wird von Second abgeleitet. Alle drei verfügen über Finalizer. In
Main wird eine Instanz der am meisten abgeleiteten Klasse erstellt. Die Ausgabe dieses Codes hängt davon ab,
für welche Implementierung von .NET die Anwendung bestimmt ist:
.NET Framework: Die Ausgabe zeigt, dass die Finalizer für die drei Klassen automatisch aufgerufen werden,
wenn die Anwendung beendet wird (in der Reihenfolge von der am meisten abgeleiteten bis zur am
wenigsten abgeleiteten Klasse).
.NET 5 (einschließlich .NET Core) oder höhere Version: Es gibt keine Ausgabe, da diese Implementierung von
.NET keine Finalizer aufruft, wenn die Anwendung beendet wird.

class First
{
~First()
{
System.Diagnostics.Trace.WriteLine("First's finalizer is called.");
}
}

class Second : First


{
~Second()
{
System.Diagnostics.Trace.WriteLine("Second's finalizer is called.");
}
}

class Third : Second


{
~Third()
{
System.Diagnostics.Trace.WriteLine("Third's finalizer is called.");
}
}

/*
Test with code like the following:
Third t = new Third();
t = null;

When objects are finalized, the output would be:


Third's finalizer is called.
Second's finalizer is called.
First's finalizer is called.
*/

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Destruktoren der C#-Sprachspezifikation.

Siehe auch
IDisposable
C#-Programmierhandbuch
Konstruktoren
Garbage Collection
Objekt- und Auflistungsinitialisierer (C#-
Programmierhandbuch)
04.11.2021 • 9 minutes to read

Mit C# können Sie ein Objekt oder eine Sammlung instanziieren und Memberzuweisungen in einer einzigen
Anweisung durchführen.

Objektinitialisierer
Mit Objektinitialisierern können Sie allen verfügbaren Feldern oder Eigenschaften eines Objekts zum
Erstellungszeitpunkt Werte zuweisen, ohne einen Konstruktor gefolgt von Zeilen mit Zuweisungsanweisungen
aufrufen zu müssen. Mit der Objektinitialisierersyntax können Sie Argumente für einen Konstruktor angeben
oder die Argumente (und Klammersyntax) weglassen. Das folgende Beispiel zeigt, wie Sie einen
Objektinitialisierer mit einem benannten Typ ( Cat ) verwenden und den parameterlosen Konstruktor aufrufen.
Beachten Sie die Verwendung automatisch implementierter Eigenschaften in der Cat -Klasse. Weitere
Informationen finden Sie unter Automatisch implementierte Eigenschaften.

public class Cat


{
// Auto-implemented properties.
public int Age { get; set; }
public string Name { get; set; }

public Cat()
{
}

public Cat(string name)


{
this.Name = name;
}
}

Cat cat = new Cat { Age = 10, Name = "Fluffy" };


Cat sameCat = new Cat("Fluffy"){ Age = 10 };

Die Syntax von Objektinitialisierern ermöglicht Ihnen die Erstellung einer Instanz und weist danach das neu
erstellte Objekt mit seinen zugewiesenen Eigenschaften der Variable in der Zuweisung zu.
Ab C# 6 können die Objektinitialisierer nicht nur Felder und Eigenschaften zuweisen, sondern auch zusätzlich
Indexer festlegen. Betrachten Sie diese grundlegende Matrix -Klasse:
public class Matrix
{
private double[,] storage = new double[3, 3];

public double this[int row, int column]


{
// The embedded array will throw out of range exceptions as appropriate.
get { return storage[row, column]; }
set { storage[row, column] = value; }
}
}

Sie können die Identitätsmatrix mit folgendem Code initialisieren:

var identity = new Matrix


{
[0, 0] = 1.0,
[0, 1] = 0.0,
[0, 2] = 0.0,

[1, 0] = 0.0,
[1, 1] = 1.0,
[1, 2] = 0.0,

[2, 0] = 0.0,
[2, 1] = 0.0,
[2, 2] = 1.0,
};

Alle zugänglichen Indexer, die zugängliche Setter enthalten, können unabhängig von der Anzahl und Typen der
Argumente als einer der Ausdrücke in einem Objektinitialisierer verwendet werden. Die Indexargumente bilden
die linke Seite der Zuweisung, und der Wert befindet sich auf der rechten Seite des Ausdrucks. Diese sind
beispielsweise alle gültig, wenn IndexersExample über die entsprechenden Indexer verfügt:

var thing = new IndexersExample {


name = "object one",
[1] = '1',
[2] = '4',
[3] = '9',
Size = Math.PI,
['C',4] = "Middle C"
}

Der IndexersExample -Typ muss für das Erstellen des obigen Codes die folgenden Member vorweisen:

public string name;


public double Size { set { ... }; }
public char this[int i] { set { ... }; }
public string this[char c, int i] { set { ... }; }

Objektinitialisierer mit anonymen Typen


Obwohl Objektinitialisierer in jedem Kontext verwendet werden können, sind sie vor allem in LINQ-
Abfrageausdrücken nützlich. Abfrageausdrücke verwenden häufig anonyme Typen, die nur mit einem
Objektinitialisierer initialisiert werden können, wie in der folgenden Deklaration veranschaulicht.
var pet = new { Age = 10, Name = "Fluffy" };

Anonyme Typen ermöglichen der select -Klausel in einem LINQ-Abfrageausdruck, Objekte der ursprünglichen
Sequenz in Objekte zu transformieren, deren Wert und Form sich vom Original unterscheiden können. Dies ist
nützlich, wenn Sie nur einen Teil der Informationen aus jedem Objekt in einer Sequenz speichern möchten. Im
folgenden Beispiel wird angenommen, dass ein Produktobjekt ( p ) viele Felder und Methoden enthält und dass
Sie nur eine Sequenz von Objekten erstellen möchten, die den Produktnamen und den Einzelpreis enthalten.

var productInfos =
from p in products
select new { p.ProductName, p.UnitPrice };

Wenn diese Abfrage ausgeführt wird, enthält die productInfos -Variable eine Sequenz von Objekten, auf die Sie
über eine foreach -Anweisung, wie im folgenden Beispiel gezeigt, zugreifen können:

foreach(var p in productInfos){...}

Jedes Objekt im neuen anonymen Typ weist zwei öffentliche Eigenschaften auf, die die gleichen Namen wie die
Eigenschaften oder Felder im ursprünglichen Objekt erhalten. Sie können ein Feld auch umbenennen, wenn Sie
einen anonymen Typ erstellen. Im folgenden Beispiel wird das UnitPrice -Feld in Price umbenannt.

select new {p.ProductName, Price = p.UnitPrice};

Auflistungsinitialisierer
Mit Auflistungsinitialisierern können Sie einen oder mehrere Elementinitialisierer festlegen, wenn Sie einen
Auflistungstyp initialisieren, der IEnumerable implementiert und über Add mit der entsprechenden Signatur als
Instanzmethode oder Erweiterungsmethode verfügt. Die Elementinitialisierer können ein einfacher Wert, ein
Ausdruck oder ein Objektinitialisierer sein. Wenn Sie einen Sammlungsinitialisierer verwenden, ist es nicht
nötig, selbst Aufrufe anzugeben, da der Compiler diese automatisch hinzufügt.
Im folgenden Beispiel werden zwei einfache Sammlungsinitialisierer dargestellt:

List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };


List<int> digits2 = new List<int> { 0 + 1, 12 % 3, MakeInt() };

Der folgende Auflistungsinitialisierer verwendet Objektinitialisierer, um Objekte der Cat -Klasse, die in einem
vorherigen Beispiel definiert wurden, zu initialisieren. Beachten Sie, dass die einzelnen Objektinitialisierer in
geschweifte Klammern eingeschlossen und durch Kommas voneinander getrennt werden.

List<Cat> cats = new List<Cat>


{
new Cat{ Name = "Sylvester", Age=8 },
new Cat{ Name = "Whiskers", Age=2 },
new Cat{ Name = "Sasha", Age=14 }
};

Sie können NULL als ein Element in einem Auflistungsinitialisierer festlegen, wenn die Add -Methode der
Auflistung dies zulässt.
List<Cat> moreCats = new List<Cat>
{
new Cat{ Name = "Furrytail", Age=5 },
new Cat{ Name = "Peaches", Age=4 },
null
};

Sie können indizierte Elemente angeben, wenn die Sammlung das Lesen oder Schreiben von Indizierungen
unterstützt.

var numbers = new Dictionary<int, string>


{
[7] = "seven",
[9] = "nine",
[13] = "thirteen"
};

Im vorherigen Beispiel wurde Code generiert, der die Item[TKey]-Eigenschaft zum Festlegen der Werte aufruft.
Mit Versionen vor C# 6 können Sie Wörterbücher und andere assoziative Container mithilfe der folgenden
Syntax initialisieren. Es gilt zu beachten, dass anstelle einer Indexersyntax mit Klammern und einer Zuweisung
ein Objekt mit mehreren Werten verwendet wird:

var moreNumbers = new Dictionary<int, string>


{
{19, "nineteen" },
{23, "twenty-three" },
{42, "forty-two" }
};

Bei diesem Beispiel eines Initialisierers wird Add(TKey, TValue) aufgerufen, um die drei Elemente dem
Wörterbuch hinzuzufügen. Diese zwei verschiedenen Arten des Initialisierens assoziativer Sammlungen
verhalten sich aufgrund der vom Compiler generierten Methodenaufrufe etwas unterschiedlich. Beide Varianten
unterstützen aber die Dictionary -Klasse. Bei anderen Typen wird abhängig von deren öffentlicher API
möglicherweise nur eine der beiden unterstützt.

Objektinitialisierer mit schreibgeschützter


Auflistungseigenschaftsinitialisierung
Einige Klassen verfügen möglicherweise über Auflistungseigenschaften, bei denen die Eigenschaft
schreibgeschützt ist, z. B. die Cats -Eigenschaft von CatOwner im folgenden Fall:

public class CatOwner


{
public IList<Cat> Cats { get; } = new List<Cat>();
}

Sie können die bisher erläuterte Auflistungsinitialisierersyntax nicht verwenden, da der Eigenschaft keine neue
Liste zugewiesen werden kann:
CatOwner owner = new CatOwner
{
Cats = new List<Cat>
{
new Cat{ Name = "Sylvester", Age=8 },
new Cat{ Name = "Whiskers", Age=2 },
new Cat{ Name = "Sasha", Age=14 }
}
};

Allerdings können Cats mithilfe der Initialisierungssyntax trotzdem neue Einträge hinzugefügt werden, indem
die Listenerstellung ( new List<Cat> ) weggelassen wird, wie im Folgenden gezeigt:

CatOwner owner = new CatOwner


{
Cats =
{
new Cat{ Name = "Sylvester", Age=8 },
new Cat{ Name = "Whiskers", Age=2 },
new Cat{ Name = "Sasha", Age=14 }
}
};

Die hinzuzufügenden Einträge werden einfach in geschweiften Klammern angezeigt. Das Obenstehende ist mit
dem folgenden Code identisch:

CatOwner owner = new CatOwner();


owner.Cats.Add(new Cat{ Name = "Sylvester", Age=8 });
owner.Cats.Add(new Cat{ Name = "Whiskers", Age=2 });
owner.Cats.Add(new Cat{ Name = "Sasha", Age=14 });

Beispiele
Im folgenden Beispiel werden die Konzepte des Objekt- und Auflistungsinitialisierers verbunden.
public class InitializationSample
{
public class Cat
{
// Auto-implemented properties.
public int Age { get; set; }
public string Name { get; set; }

public Cat() { }

public Cat(string name)


{
Name = name;
}
}

public static void Main()


{
Cat cat = new Cat { Age = 10, Name = "Fluffy" };
Cat sameCat = new Cat("Fluffy"){ Age = 10 };

List<Cat> cats = new List<Cat>


{
new Cat { Name = "Sylvester", Age = 8 },
new Cat { Name = "Whiskers", Age = 2 },
new Cat { Name = "Sasha", Age = 14 }
};

List<Cat> moreCats = new List<Cat>


{
new Cat { Name = "Furrytail", Age = 5 },
new Cat { Name = "Peaches", Age = 4 },
null
};

// Display results.
System.Console.WriteLine(cat.Name);

foreach (Cat c in cats)


System.Console.WriteLine(c.Name);

foreach (Cat c in moreCats)


if (c != null)
System.Console.WriteLine(c.Name);
else
System.Console.WriteLine("List element has null value.");
}
// Output:
//Fluffy
//Sylvester
//Whiskers
//Sasha
//Furrytail
//Peaches
//List element has null value.
}

Im folgenden Beispiel wird ein Objekt veranschaulicht, das IEnumerable implementiert und eine Add -Methode
mit mehreren Parametern enthält. Es wird ein Sammlungsinitialisierer mit mehreren Elementen pro Element in
der Liste verwendet, die der Signatur der Add -Methode entsprechen.
public class FullExample
{
class FormattedAddresses : IEnumerable<string>
{
private List<string> internalList = new List<string>();
public IEnumerator<string> GetEnumerator() => internalList.GetEnumerator();

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>


internalList.GetEnumerator();

public void Add(string firstname, string lastname,


string street, string city,
string state, string zipcode) => internalList.Add(
$@"{firstname} {lastname}
{street}
{city}, {state} {zipcode}"
);
}

public static void Main()


{
FormattedAddresses addresses = new FormattedAddresses()
{
{"John", "Doe", "123 Street", "Topeka", "KS", "00000" },
{"Jane", "Smith", "456 Street", "Topeka", "KS", "00000" }
};

Console.WriteLine("Address Entries:");

foreach (string addressEntry in addresses)


{
Console.WriteLine("\r\n" + addressEntry);
}
}

/*
* Prints:

Address Entries:

John Doe
123 Street
Topeka, KS 00000

Jane Smith
456 Street
Topeka, KS 00000
*/
}

Add -Methoden können durch das Schlüsselwort params eine variable Anzahl von Argumenten akzeptieren,
wie im folgenden Beispiel gezeigt wird. In diesem Beispiel wird die benutzerdefinierte Implementierung eines
Indexers sowie die Initialisierung einer Sammlung mithilfe von Indizes veranschaulicht.

public class DictionaryExample


{
class RudimentaryMultiValuedDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, List<TValue>>>
{
private Dictionary<TKey, List<TValue>> internalDictionary = new Dictionary<TKey, List<TValue>>();

public IEnumerator<KeyValuePair<TKey, List<TValue>>> GetEnumerator() =>


internalDictionary.GetEnumerator();

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>


internalDictionary.GetEnumerator();
public List<TValue> this[TKey key]
{
get => internalDictionary[key];
set => Add(key, value);
}

public void Add(TKey key, params TValue[] values) => Add(key, (IEnumerable<TValue>)values);

public void Add(TKey key, IEnumerable<TValue> values)


{
if (!internalDictionary.TryGetValue(key, out List<TValue> storedValues))
internalDictionary.Add(key, storedValues = new List<TValue>());

storedValues.AddRange(values);
}
}

public static void Main()


{
RudimentaryMultiValuedDictionary<string, string> rudimentaryMultiValuedDictionary1
= new RudimentaryMultiValuedDictionary<string, string>()
{
{"Group1", "Bob", "John", "Mary" },
{"Group2", "Eric", "Emily", "Debbie", "Jesse" }
};
RudimentaryMultiValuedDictionary<string, string> rudimentaryMultiValuedDictionary2
= new RudimentaryMultiValuedDictionary<string, string>()
{
["Group1"] = new List<string>() { "Bob", "John", "Mary" },
["Group2"] = new List<string>() { "Eric", "Emily", "Debbie", "Jesse" }
};
RudimentaryMultiValuedDictionary<string, string> rudimentaryMultiValuedDictionary3
= new RudimentaryMultiValuedDictionary<string, string>()
{
{"Group1", new string []{ "Bob", "John", "Mary" } },
{ "Group2", new string[]{ "Eric", "Emily", "Debbie", "Jesse" } }
};

Console.WriteLine("Using first multi-valued dictionary created with a collection initializer:");

foreach (KeyValuePair<string, List<string>> group in rudimentaryMultiValuedDictionary1)


{
Console.WriteLine($"\r\nMembers of group {group.Key}: ");

foreach (string member in group.Value)


{
Console.WriteLine(member);
}
}

Console.WriteLine("\r\nUsing second multi-valued dictionary created with a collection initializer


using indexing:");

foreach (KeyValuePair<string, List<string>> group in rudimentaryMultiValuedDictionary2)


{
Console.WriteLine($"\r\nMembers of group {group.Key}: ");

foreach (string member in group.Value)


{
Console.WriteLine(member);
}
}
Console.WriteLine("\r\nUsing third multi-valued dictionary created with a collection initializer
using indexing:");

foreach (KeyValuePair<string, List<string>> group in rudimentaryMultiValuedDictionary3)


{
Console.WriteLine($"\r\nMembers of group {group.Key}: ");
foreach (string member in group.Value)
{
Console.WriteLine(member);
}
}
}

/*
* Prints:

Using first multi-valued dictionary created with a collection initializer:

Members of group Group1:


Bob
John
Mary

Members of group Group2:


Eric
Emily
Debbie
Jesse

Using second multi-valued dictionary created with a collection initializer using indexing:

Members of group Group1:


Bob
John
Mary

Members of group Group2:


Eric
Emily
Debbie
Jesse

Using third multi-valued dictionary created with a collection initializer using indexing:

Members of group Group1:


Bob
John
Mary

Members of group Group2:


Eric
Emily
Debbie
Jesse
*/
}

Siehe auch
C#-Programmierhandbuch
LINQ in C#
Anonyme Typen
Gewusst wie: Initialisieren von Objekten mit einem
Objektinitialisierer (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Mit Objektinitialisierern können Sie deklarativ Typenobjekte initialisieren, ohne explizit einen Konstruktor für
den Typ aufzurufen.
Die folgenden Beispiele zeigen, wie Objektinitialisierer mit benannten Objekten verwendet werden. Der
Compiler verarbeitet Objektinitialisierer, indem zuerst auf den parameterlosen Instanzinstruktor zugegriffen
wird und anschließend die Memeberinitialisierungen verarbeitet werden. Daher tritt bei Objektinitialisierern, die
öffentlichen Zugriff benötigen, ein Fehler auf, wenn der parameterlose Konstruktor in der Klasse als private
deklariert wird.
Sie müssen einen Objektinitialisierer verwenden, wenn Sie einen anonymen Typ definieren. Weitere
Informationen finden Sie unter Vorgehensweise: Zurückgeben von Teilmengen von Elementeigenschaften in
einer Abfrage.

Beispiel
Im folgenden Beispiel wird das Initialisieren eines neuen StudentName -Typs mithilfe eines Objektinitialisierers
gezeigt. Dieses Beispiel legt Eigenschaften für den Typ StudentName fest:

public class HowToObjectInitializers


{
public static void Main()
{
// Declare a StudentName by using the constructor that has two parameters.
StudentName student1 = new StudentName("Craig", "Playstead");

// Make the same declaration by using an object initializer and sending


// arguments for the first and last names. The parameterless constructor is
// invoked in processing this declaration, not the constructor that has
// two parameters.
StudentName student2 = new StudentName
{
FirstName = "Craig",
LastName = "Playstead"
};

// Declare a StudentName by using an object initializer and sending


// an argument for only the ID property. No corresponding constructor is
// necessary. Only the parameterless constructor is used to process object
// initializers.
StudentName student3 = new StudentName
{
ID = 183
};

// Declare a StudentName by using an object initializer and sending


// arguments for all three properties. No corresponding constructor is
// defined in the class.
StudentName student4 = new StudentName
{
FirstName = "Craig",
LastName = "Playstead",
ID = 116
};
Console.WriteLine(student1.ToString());
Console.WriteLine(student2.ToString());
Console.WriteLine(student3.ToString());
Console.WriteLine(student4.ToString());
}
// Output:
// Craig 0
// Craig 0
// 183
// Craig 116

public class StudentName


{
// This constructor has no parameters. The parameterless constructor
// is invoked in the processing of object initializers.
// You can test this by changing the access modifier from public to
// private. The declarations in Main that use object initializers will
// fail.
public StudentName() { }

// The following constructor has parameters for two of the three


// properties.
public StudentName(string first, string last)
{
FirstName = first;
LastName = last;
}

// Properties.
public string FirstName { get; set; }
public string LastName { get; set; }
public int ID { get; set; }

public override string ToString() => FirstName + " " + ID;


}
}

Objektinitialisierer können dafür verwendet werden, Indexer in einem Objekt festzulegen. Das folgende Beispiel
definiert eine BaseballTeam -Klasse, die einen Indexer verwendet, um Player an verschiedenen Positionen
abzurufen und festzulegen. Der Initialisierer kann Player auf Grundlage der Abkürzung für die Position oder der
Zahl, die für jede einzelne Position auf einem Baseball-Scoresheet verwendet wird, zuweisen:
public class HowToIndexInitializer
{
public class BaseballTeam
{
private string[] players = new string[9];
private readonly List<string> positionAbbreviations = new List<string>
{
"P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"
};

public string this[int position]


{
// Baseball positions are 1 - 9.
get { return players[position-1]; }
set { players[position-1] = value; }
}
public string this[string position]
{
get { return players[positionAbbreviations.IndexOf(position)]; }
set { players[positionAbbreviations.IndexOf(position)] = value; }
}
}

public static void Main()


{
var team = new BaseballTeam
{
["RF"] = "Mookie Betts",
[4] = "Jose Altuve",
["CF"] = "Mike Trout"
};

Console.WriteLine(team["2B"]);
}
}

Siehe auch
C#-Programmierhandbuch
Objekt- und Auflistungsinitialisierer
Initialisieren eines Wörterbuchs mit einem
Auflistungsinitialisierer (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Eine Dictionary<TKey,TValue> enthält eine Sammlung von Schlüssel-Wert-Paaren. Die Add-Methode nimmt
zwei Parameter an, einen für den Schlüssel und einen für den Wert. Eine Möglichkeit, um eine
Dictionary<TKey,TValue>-Klasse oder eine beliebige Sammlung zu initialisieren, deren Add -Methode mehrere
Parameter annimmt, ist es, jedes Parameterpaar in Klammern einzuschließen, so wie im folgenden Beispiel
dargestellt. Eine andere Möglichkeit besteht darin, einen Index-Initialisierer zu verwenden, wie im nächsten
Beispiel gezeigt wird.

Beispiel
Im folgenden Codebeispiel wird Dictionary<TKey,TValue> mit Instanzen vom Typ StudentName initialisiert. Die
erste Initialisierung verwendet die Add -Methode mit zwei Argumenten. Der Compiler führt für Add einen
Aufruf für jedes der Paare von int -Schlüsseln und StudentName -Werten durch. Bei der zweiten Initialisierung
wird eine öffentliche Indexermethode der Dictionary -Klasse für Lese- und Schreibvorgänge verwendet:
public class HowToDictionaryInitializer
{
class StudentName
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int ID { get; set; }
}

public static void Main()


{
var students = new Dictionary<int, StudentName>()
{
{ 111, new StudentName { FirstName="Sachin", LastName="Karnik", ID=211 } },
{ 112, new StudentName { FirstName="Dina", LastName="Salimzianova", ID=317 } },
{ 113, new StudentName { FirstName="Andy", LastName="Ruth", ID=198 } }
};

foreach(var index in Enumerable.Range(111, 3))


{
Console.WriteLine($"Student {index} is {students[index].FirstName} {students[index].LastName}");
}
Console.WriteLine();

var students2 = new Dictionary<int, StudentName>()


{
[111] = new StudentName { FirstName="Sachin", LastName="Karnik", ID=211 },
[112] = new StudentName { FirstName="Dina", LastName="Salimzianova", ID=317 } ,
[113] = new StudentName { FirstName="Andy", LastName="Ruth", ID=198 }
};

foreach (var index in Enumerable.Range(111, 3))


{
Console.WriteLine($"Student {index} is {students2[index].FirstName}
{students2[index].LastName}");
}
}
}

Beachten Sie die zwei Paare geschweifter Klammern in jedem Element der Collection in der ersten Deklaration.
Die inneren Klammern umschließen den Objektinitialisierer für StudentName , und die äußeren Klammern
umschließen den Initialisierer für das Schlüssel-Wert-Paar, das students Dictionary<TKey,TValue> hinzugefügt
wird. Schließlich wird der ganze Auflistungsinitialisierer für das Wörterbuch in geschweifte Klammern
eingeschlossen. Bei der zweiten Initialisierung ist die linke Seite des Arbeitsauftrags der Schlüssel und die rechte
Seite ist der Wert. Für StudentName wird ein Objektinitialisierer verwendet.

Siehe auch
C#-Programmierhandbuch
Objekt- und Auflistungsinitialisierer
Geschachtelte Typen (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Ein innerhalb einer Klasse, Struktur oder Schnittstelle definierter Typ wird als geschachtelter Typ bezeichnet.
Beispiel:

public class Container


{
class Nested
{
Nested() { }
}
}

Unabhängig davon, ob es sich beim äußeren Typ um eine Klasse, Schnittstelle oder Struktur handelt, sind
geschachtelte Typen standardmäßig privat, d. h. sie sind nur über den enthaltenden Typ zugänglich. Im
vorherigen Beispiel konnten externe Typen nicht auf die Nested -Klasse zugreifen.
Sie können auch einen Zugriffsmodifizierer angeben, um den Zugriff auf einen geschachtelten Typ wie folgt zu
definieren:
Geschachtelte Typen von class können public, protected, internal, protected internal, private oder private
protected sein.
Durch das Definieren einer geschachtelten protected -, protected internal - oder private protected -
Klasse innerhalb einer versiegelten Klasse wird jedoch die Compilerwarnung CS0628, „Neuer geschützter
Member in versiegelter Klasse deklariert“, generiert.
Beachten Sie auch, dass das externe Sichtbarmachen eines geschachtelten Typs gegen die
Codequalitätsregel CA1034 „Geschachtelte Typen sollten nicht sichtbar sein“ verstößt.
Geschachtelte Typen einer Struktur können öffentlich (public), intern (internal) oder privat (private) sein.
Im folgenden Beispiel wird die Nested -Klasse öffentlich gemacht:

public class Container


{
public class Nested
{
Nested() { }
}
}

Der geschachtelte oder innere Typ kann auf den enthaltenden oder äußeren Typ zugreifen. Um auf den
enthaltenden Typ zuzugreifen, übergeben Sie ihn als Argument dem Konstruktor des geschachtelten Typs. Zum
Beispiel:
public class Container
{
public class Nested
{
private Container parent;

public Nested()
{
}
public Nested(Container parent)
{
this.parent = parent;
}
}
}

Ein geschachtelter Typ hat Zugriff auf alle Member, die für ihren äußeren (enthaltenden) Typ zugreifbar sind. Er
kann auf die Member des äußeren (enthaltenden) Typs zugreifen, die "private" oder "protected" sind, ebenso auf
alle geerbten Member, die "private" oder "protected" sind.
In der vorherigen Deklaration ist Nested der vollständige Name der Klasse Container.Nested . Mit diesem
Namen wird wie folgt eine neue Instanz der geschachtelten Klasse erstellt:

Container.Nested nest = new Container.Nested();

Siehe auch
C#-Programmierhandbuch
Das C#-Typsystem
Zugriffsmodifizierer
Konstruktoren
CA1034-Regel
Partielle Klassen und Methoden (C#-
Programmierhandbuch)
04.11.2021 • 6 minutes to read

Es ist möglich, die Definition einer Klasse, einer Struktur, einer Schnittstelle oder einer Methode auf zwei oder
mehr Quelldateien aufzuteilen. Jede Quelldatei enthält einen Abschnitt der Typ- oder Methodendefinition. Die
Teile werden bei der Kompilierung der Anwendung miteinander kombiniert.

Teilklassen
Es gibt mehrere Situationen, bei denen das Aufteilen einer Klassendefinition wünschenswert ist:
Wenn Sie an großen Projekten arbeiten, können durch das Verteilen einer Klasse auf mehrere Dateien
mehrere Programmierer gleichzeitig daran arbeiten.
Wenn Sie mit automatisch erzeugten Quellen arbeiten, kann Code einer Klasse hinzugefügt werden, ohne die
Quelldatei neu zu erstellen. Visual Studio verwendet diesen Ansatz, wenn es Windows Forms, Webdienst-
Wrappercode usw. erstellt. Sie können Code erstellen, der diese Klassen verwendet, ohne die von Visual
Studio erstellte Datei ändern zu müssen.
Wenn Sie Quellgeneratoren verwenden, um zusätzliche Funktionen in einer Klasse zu generieren.
Um eine Klassendefinition aufzuteilen, verwenden Sie den Schlüsselwortmodifizierer partial, wie hier gezeigt
wird:

public partial class Employee


{
public void DoWork()
{
}
}

public partial class Employee


{
public void GoToLunch()
{
}
}

Das Schlüsselwort partial gibt an, dass andere Teile der Klasse, Struktur oder Schnittstelle im Namespace
definiert werden können. Alle Teile müssen das Schlüsselwort partial verwenden. Alle Teile müssen zur
Kompilierzeit verfügbar sein, um den endgültigen Typ zu bilden. Alle Teile müssen die gleiche Zugriffsebene
haben, z.B. public , private usw.
Wenn ein beliebiger Teil als abstrakt deklariert wird, wird anschließend der ganze Typ als abstrakt betrachtet.
Wenn ein beliebiges Teil als versiegelt deklariert wird, wird anschließend der ganze Typ als versiegelt betrachtet.
Wenn ein beliebiger Teil einen Basistyp deklariert, erbt der ganze Typ diese Klasse.
Alle Teile, die eine Basisklasse angeben, müssen übereinstimmen, aber Teile, die eine Basisklasse auslassen,
erben weiterhin den Basistyp. Teile können unterschiedliche Basisschnittstellen angeben, und der letzte Typ
implementiert alle Schnittstellen, die von allen Teildeklarationen aufgeführt sind. Alle Klassen-, Struktur- und
Schnittstellenmember, die in einer Teildefinition deklariert wurden, sind für alle anderen Teile verfügbar. Der
endgültige Typ besteht aus allen Teilen zur Kompilierzeit.
NOTE
Der partial -Modifizierer ist nicht für Delegat- oder Enumerationsdeklarationen verfügbar.

Im folgenden Beispiel wird gezeigt, dass geschachtelte Typen eine Teilausführung sein können, selbst wenn der
Typ, in dem sie geschachtelt sind, selbst keine ist.

class Container
{
partial class Nested
{
void Test() { }
}

partial class Nested


{
void Test2() { }
}
}

Zur Kompilierzeit werden Attribute von partiellen Typdefinitionen zusammengeführt. Betrachten Sie
beispielsweise die folgenden Deklarationen:

[SerializableAttribute]
partial class Moon { }

[ObsoleteAttribute]
partial class Moon { }

Sie entsprechen den folgenden Deklarationen:

[SerializableAttribute]
[ObsoleteAttribute]
class Moon { }

Die folgenden werden aus allen partiellen Typdefinitionen zusammengeführt:


XML-Kommentare
Schnittstellen
Generische Parameterattribute
Klassenattribute
Member
Betrachten Sie beispielsweise die folgenden Deklarationen:

partial class Earth : Planet, IRotate { }


partial class Earth : IRevolve { }

Sie entsprechen den folgenden Deklarationen:

class Earth : Planet, IRotate, IRevolve { }

Beschränkungen
Es sind mehrere Regeln zu beachten, wenn Sie partielle Klassendefinitionen verwenden:
Alle partiellen Typdefinitionen, die als Teile des gleichen Typs vorgesehen sind, müssen mit partial
geändert werden. Beispielsweise erzeugen die folgenden Klassendeklarationen einen Fehler: [!code-
csharpAllDefinitionsMustBePartials#7]
Der partial -Modifizierer kann nur unmittelbar vor den Schlüsselwörtern class , struct oder interface
erscheinen.
Geschachtelte partielle Typen sind in partiellen Typdefinitionen zulässig, wie im folgenden Beispiel dargestellt
wird: [!code-csharpNestedPartialTypes#8]
Alle partiellen Typdefinitionen, die als Teile des gleichen Typs vorgesehen sind, müssen in der gleichen
Assembly und demselben Modul (EXE- oder DLL-Datei) definiert werden. Partielle Definitionen können nicht
mehrere Module umfassen.
Der Klassenname und die generischen Typparameter müssen mit allen partiellen Typdefinitionen
übereinstimmen. Generische Typen können partiell sein. Jede partielle Definition muss die gleichen
Parameternamen in der gleichen Reihenfolge aufweisen.
Die nachfolgenden Schlüsselwörter für eine partielle Typdefinition sind optional, wenn sie aber für eine
partielle Definition vorhanden sind, können sie nicht mit anderen Schlüsselwörtern in Konflikt treten, die in
anderen partiellen Definitionen desselben Typs angegeben wurden:
public
private
protected
internal
abstract
sealed
Basisklasse
new-Modifizierer (geschachtelte Teile)
Generische Einschränkungen
Weitere Informationen finden Sie unter Einschränkungen für Typparameter.

Beispiele
Im folgenden Beispiel werden die Felder und der Konstruktor der Klasse ( Coords ) in einer partiellen
Klassendefinition deklariert, und der Member ( PrintCoords ) wird in einer anderen partiellen Klassendefinition
deklariert.
public partial class Coords
{
private int x;
private int y;

public Coords(int x, int y)


{
this.x = x;
this.y = y;
}
}

public partial class Coords


{
public void PrintCoords()
{
Console.WriteLine("Coords: {0},{1}", x, y);
}
}

class TestCoords
{
static void Main()
{
Coords myCoords = new Coords(10, 15);
myCoords.PrintCoords();

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
// Output: Coords: 10,15

Im folgenden Beispiel wird gezeigt, dass Sie auch partielle Strukturen und Schnittstellen entwickeln können.

partial interface ITest


{
void Interface_Test();
}

partial interface ITest


{
void Interface_Test2();
}

partial struct S1
{
void Struct_Test() { }
}

partial struct S1
{
void Struct_Test2() { }
}

Partielle Methoden
Eine partielle Klasse oder Struktur kann eine partielle Methode enthalten. Ein Teil der Klasse enthält die Signatur
der Methode. Eine Implementierung kann im gleichen oder in einem anderen Teil definiert werden. Wenn die
Implementierung nicht bereitgestellt wird, werden anschließend die Methode sowie alle Aufrufe an die Methode
zur Kompilierzeit entfernt. Je nach Methodensignatur kann eine Implementierung erforderlich sein. Eine partielle
Methode erfordert in den folgenden Fällen keine Implementierung:
Sie verfügt über keine Zugriffsmodifizierer (einschließlich der Standardeinstellung private).
Der Rückgabewert ist void.
Sie verfügt über keine out-Parameter.
Sie verfügt über keinen der folgenden Modifizierer: virtual, override, sealed, new oder extern.
Jede Methode, die nicht allen Einschränkungen entspricht (z. B. die public virtual partial void -Methode),
muss eine Implementierung bereitstellen. Diese Implementierung kann von einem Quellgenerator bereitgestellt
werden.
Mit partiellen Methoden kann der Implementierer des einen Teils einer Klasse eine Methode deklarieren. Der
Implementierer eines anderen Teils der Klasse kann die betreffende Methode definieren. Es gibt zwei Szenarien,
in denen dies nützlich ist: Vorlagen, die Codebausteine generieren, und Quellgeneratoren.
Vorlagencode : Die Vorlage reserviert einen Methodennamen und eine Signatur, damit generierter Code die
Methode aufrufen kann. Diese Methode folgt den Einschränkungen, die es einem Entwickler ermöglichen, zu
entscheiden, ob sie implementiert werden soll. Wenn die Methode nicht implementiert wird, kann der
Compiler anschließend die Methodensignatur und alle Aufrufe an die Methode entfernen. Die Aufrufe an die
Methode, einschließlich Ergebnissen, die bei der Auswertung eines Arguments im Aufruf auftreten würden,
haben zur Laufzeit keine Auswirkung. Deshalb kann jeder Code in der partiellen Klasse eine partielle
Methode frei verwenden, selbst wenn die Implementation nicht bereitgestellt wird. Es ergeben sich keine
Laufzeit- oder Kompilierzeitfehler, wenn die Methode aufgerufen, aber nicht implementiert wird.
Quellgeneratoren : Quellgeneratoren stellen eine Implementierung für Methoden bereit. Der menschliche
Entwickler kann die Methodendeklaration hinzufügen (häufig mit Attributen, die vom Quellgenerator gelesen
werden). Der Entwickler kann Code schreiben, der diese Methoden aufruft. Der Quellgenerator wird während
der Kompilierung ausgeführt und stellt die Implementierung bereit. In diesem Szenario werden die
Einschränkungen für partielle Methoden, die möglicherweise nicht implementiert werden, häufig nicht
befolgt.

// Definition in file1.cs
partial void OnNameChanged();

// Implementation in file2.cs
partial void OnNameChanged()
{
// method body
}

Partielle Methodendeklarationen müssen mit dem Kontextschlüsselwort partial beginnen.


Partielle Methodensignaturen in beiden Teilen des partiellen Typs müssen übereinstimmen.
Partielle Methoden können statische und unsichere Modifizierer besitzen.
Partielle Methoden können generisch sein. Einschränkungen gelten für die definierende partielle
Methodendeklaration und können optional für die implementierende wiederholt werden. Parameter- und
Typparameternamen müssen in der implementierenden und definierenden Deklaration nicht gleich sein.
Sie können einen Delegaten für eine partielle Methode erstellen, die definiert und implementiert wurde. Dies
geht jedoch nicht für partielle Methoden, die nur definiert wurden.

C#-Programmiersprachenspezifikation
Weitere Informationen finden Sie unter Partielle Datentypen in der C#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Klassen
Strukturtypen
Schnittstellen
partial (Typ)
Gewusst wie: Zurückgeben von Teilmengen von
Elementeigenschaften in einer Abfrage (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Verwenden Sie einen anonymen Typ in einem Abfrageausdruck, wenn die folgenden Bedingungen beide erfüllt
sind:
Sie möchte nur manche der Eigenschaften jedes Quellelements zurückgeben.
Sie müssen die Abfrageergebnisse nicht außerhalb des Geltungsbereichs der Methode speichern, in der
die Abfrage ausgeführt wird.
Wenn Sie nur eine Eigenschaft oder ein Feld jeder Quelldatei zurückgeben möchten, können Sie dazu den
Punktoperator in der select -Klausel verwenden. Wenn Sie z.B. die ID jedes student zurückgeben möchten,
schreiben Sie die select -Klausel wie folgt:

select student.ID;

Beispiel
Im folgenden Beispiel erfahren Sie, wie Sie einen anonymen Typ verwenden können, um lediglich eine
Teilmenge der Eigenschaften jedes Quellelements zurückzugeben, das die angegebenen Bedingungen erfüllt.

private static void QueryByScore()


{
// Create the query. var is required because
// the query produces a sequence of anonymous types.
var queryHighScores =
from student in students
where student.ExamScores[0] > 95
select new { student.FirstName, student.LastName };

// Execute the query.


foreach (var obj in queryHighScores)
{
// The anonymous type's properties were not named. Therefore
// they have the same names as the Student properties.
Console.WriteLine(obj.FirstName + ", " + obj.LastName);
}
}
/* Output:
Adams, Terry
Fakhouri, Fadi
Garcia, Cesar
Omelchenko, Svetlana
Zabokritski, Eugene
*/

Beachten Sie, dass der anonyme Typ den Namen des Quellelements für dessen Eigenschaften verwendet, wenn
keine Namen angegeben sind. Um die Eigenschaften im anonymen Typen zu benennen, schreiben Sie die
select -Anweisung wie folgt:
select new { First = student.FirstName, Last = student.LastName };

Wenn Sie dies mit vorherigem Beispiel durchführen möchten, müssen Sie die Console.WriteLine -Anweisung
anpassen:

Console.WriteLine(student.First + " " + student.Last);

Kompilieren des Codes


Zum Ausführen dieses Codes müssen Sie die Klasse kopieren und in eine C#-Konsolenanwendung mit einer
using -Anweisung für „System.Linq“ einfügen.

Siehe auch
C#-Programmierhandbuch
Anonyme Typen
LINQ in C#
Explizite Schnittstellenimplementierung (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Wenn eine Klasse zwei Schnittstellen implementiert, die einen Member mit derselben Signatur enthalten,
bewirkt die Implementierung dieses Members in der Klasse, dass beide Schnittstellen diesen Member als ihre
Implementierung verwenden. Im folgenden Beispiel rufen alle Aufrufe von Paint dieselbe Methode auf. In
diesem ersten Beispiel werden die Typen definiert:

public interface IControl


{
void Paint();
}
public interface ISurface
{
void Paint();
}
public class SampleClass : IControl, ISurface
{
// Both ISurface.Paint and IControl.Paint call this method.
public void Paint()
{
Console.WriteLine("Paint method in SampleClass");
}
}

Im folgenden Beispiel werden die Methoden aufgerufen:

SampleClass sample = new SampleClass();


IControl control = sample;
ISurface surface = sample;

// The following lines all call the same method.


sample.Paint();
control.Paint();
surface.Paint();

// Output:
// Paint method in SampleClass
// Paint method in SampleClass
// Paint method in SampleClass

Aber Sie möchten vielleicht nicht, dass für beide Schnittstellen die gleiche Implementierung aufgerufen wird.
Um eine andere Implementierung aufzurufen, je nachdem welche Schnittstelle verwendet wird, können Sie
einen Schnittstellenmember explizit implementieren. Eine explizite Schnittstellenimplementierung ist ein
Klassenmember, der nur über die angegebene Schnittstelle aufgerufen wird. Benennen Sie den Klassenmember,
indem Sie ihm den Namen der Schnittstelle und einen Punkt voranstellen. Zum Beispiel:
public class SampleClass : IControl, ISurface
{
void IControl.Paint()
{
System.Console.WriteLine("IControl.Paint");
}
void ISurface.Paint()
{
System.Console.WriteLine("ISurface.Paint");
}
}

Der Klassenmember IControl.Paint ist nur über die Schnittstelle IControl verfügbar, und ISurface.Paint ist
nur über ISurface verfügbar. Beide Methodenimplementierungen sind voneinander getrennt, und sie sind nicht
direkt in der Klasse verfügbar. Zum Beispiel:

SampleClass sample = new SampleClass();


IControl control = sample;
ISurface surface = sample;

// The following lines all call the same method.


//sample.Paint(); // Compiler error.
control.Paint(); // Calls IControl.Paint on SampleClass.
surface.Paint(); // Calls ISurface.Paint on SampleClass.

// Output:
// IControl.Paint
// ISurface.Paint

Die explizite Implementierung wird auch zum Lösen von Konflikten verwendet, bei denen zwei Schnittstellen
verschiedene Member mit demselben Namen deklarieren, z. B. eine Eigenschaft und eine Methode. Zum
Implementieren beider Schnittstellen muss eine Klasse die explizite Implementierung für die Eigenschaft P , die
Methode P oder beide verwenden, um einen Compilerfehler zu vermeiden. Zum Beispiel:

interface ILeft
{
int P { get;}
}
interface IRight
{
int P();
}

class Middle : ILeft, IRight


{
public int P() { return 0; }
int ILeft.P { get { return 0; } }
}

Eine explizite Schnittstellenimplementierung besitzt keinen Zugriffsmodifizierer, da auf sie nicht als Member des
Typs zugegriffen werden kann, in dem sie definiert ist. Sie ist nur verfügbar, wenn sie über eine Instanz der
Schnittstelle aufgerufen wird. Wenn Sie einen Zugriffsmodifizierer für eine explizite
Schnittstellenimplementierung angeben, erhalten Sie den Compilerfehler CS0106. Weitere Informationen finden
Sie unter interface (C#-Referenz).
Ab C# 8.0 können Sie eine Implementierung für in einer Schnittstelle deklarierte Mitglieder definieren. Wenn
eine Klasse eine Methodenimplementierung von einer Schnittstelle erbt, ist diese Methode nur über einen
Verweis des Schnittstellentyps zugänglich. Der geerbte Member wird nicht als Teil der öffentlichen Schnittstelle
angezeigt. Im folgenden Beispiel wird eine Standardimplementierung für eine Schnittstellenmethode definiert:
public interface IControl
{
void Paint() => Console.WriteLine("Default Paint method");
}
public class SampleClass : IControl
{
// Paint() is inherited from IControl.
}

Im folgenden Beispiel wird die Standardimplementierung aufgerufen:

var sample = new SampleClass();


//sample.Paint();// "Paint" isn't accessible.
var control = sample as IControl;
control.Paint();

Alle Klassen, die die IControl -Schnittstelle implementieren, können die Standardmethode Paint
überschreiben: entweder als öffentliche Methode oder als explizite Schnittstellenimplementierung.

Weitere Informationen
C#-Programmierhandbuch
Objektorientiertes Programmieren
Schnittstellen
Vererbung
Vorgehensweise: Explizites Implementieren von
Schnittstellenmembern (C#-Programmierleitfaden)
04.11.2021 • 2 minutes to read

Dieses Beispiel deklariert eine Schnittstelle, IDimensions , und eine Klasse, Box , die explizit die
Schnittstellenmember GetLength und GetWidth implementiert. Die Member werden über eine
Schnittstelleninstanz, dimensions , erreicht.

Beispiel
interface IDimensions
{
float GetLength();
float GetWidth();
}

class Box : IDimensions


{
float lengthInches;
float widthInches;

Box(float length, float width)


{
lengthInches = length;
widthInches = width;
}
// Explicit interface member implementation:
float IDimensions.GetLength()
{
return lengthInches;
}
// Explicit interface member implementation:
float IDimensions.GetWidth()
{
return widthInches;
}

static void Main()


{
// Declare a class instance box1:
Box box1 = new Box(30.0f, 20.0f);

// Declare an interface instance dimensions:


IDimensions dimensions = box1;

// The following commented lines would produce compilation


// errors because they try to access an explicitly implemented
// interface member from a class instance:
//System.Console.WriteLine("Length: {0}", box1.GetLength());
//System.Console.WriteLine("Width: {0}", box1.GetWidth());

// Print out the dimensions of the box by calling the methods


// from an instance of the interface:
System.Console.WriteLine("Length: {0}", dimensions.GetLength());
System.Console.WriteLine("Width: {0}", dimensions.GetWidth());
}
}
/* Output:
Length: 30
Width: 20
*/

Stabile Programmierung
Beachten Sie, dass die folgenden Zeilen in der Main -Methode auskommentiert werden, da sie
Kompilierungsfehler verursachen würden. Auf ein Schnittstellenmember, das explizit implementiert wird,
kann nicht von einer class-Instanz zugegriffen werden.

//System.Console.WriteLine("Length: {0}", box1.GetLength());


//System.Console.WriteLine("Width: {0}", box1.GetWidth());

Beachten Sie auch, dass die folgenden Zeilen in der Main -Methode erfolgreich die Dimensionen des
Felds ausdrucken, da die Methoden von einer Instanz der Schnittstelle aufgerufen werden:
System.Console.WriteLine("Length: {0}", dimensions.GetLength());
System.Console.WriteLine("Width: {0}", dimensions.GetWidth());

Weitere Informationen
C#-Programmierhandbuch
Objektorientiertes Programmieren
Schnittstellen
Explizites Implementieren von Membern zweier Schnittstellen
Vorgehensweise: Explizites Implementieren von
Membern zweier Schnittstellen (C#-
Programmierleitfaden)
04.11.2021 • 2 minutes to read

Die explizite Schnittstellenimplementierung erlaubt dem Programmierer auch, zwei Schnittstellen zu


implementieren, die über die gleichen Membernamen verfügen, sowie jedem Schnittstellenmember eine
separate Implementierung zu geben. Dieses Beispiel zeigt die Dimensionen eines Felds in jeweils der metrischen
und der englischen Einheit. Das Feld class implementiert zwei Schnittstellen, „IEnglishDimensions“ und
„IMetricDimensions“, die die unterschiedlichen Messsysteme darstellen. Beide Schnittstellen verfügen über
identische Elementnamen, sowie unterschiedliche Länge und Breite.

Beispiel
// Declare the English units interface:
interface IEnglishDimensions
{
float Length();
float Width();
}

// Declare the metric units interface:


interface IMetricDimensions
{
float Length();
float Width();
}

// Declare the Box class that implements the two interfaces:


// IEnglishDimensions and IMetricDimensions:
class Box : IEnglishDimensions, IMetricDimensions
{
float lengthInches;
float widthInches;

public Box(float lengthInches, float widthInches)


{
this.lengthInches = lengthInches;
this.widthInches = widthInches;
}

// Explicitly implement the members of IEnglishDimensions:


float IEnglishDimensions.Length() => lengthInches;

float IEnglishDimensions.Width() => widthInches;

// Explicitly implement the members of IMetricDimensions:


float IMetricDimensions.Length() => lengthInches * 2.54f;

float IMetricDimensions.Width() => widthInches * 2.54f;

static void Main()


{
// Declare a class instance box1:
Box box1 = new Box(30.0f, 20.0f);

// Declare an instance of the English units interface:


IEnglishDimensions eDimensions = box1;

// Declare an instance of the metric units interface:


IMetricDimensions mDimensions = box1;

// Print dimensions in English units:


System.Console.WriteLine("Length(in): {0}", eDimensions.Length());
System.Console.WriteLine("Width (in): {0}", eDimensions.Width());

// Print dimensions in metric units:


System.Console.WriteLine("Length(cm): {0}", mDimensions.Length());
System.Console.WriteLine("Width (cm): {0}", mDimensions.Width());
}
}
/* Output:
Length(in): 30
Width (in): 20
Length(cm): 76.2
Width (cm): 50.8
*/

Stabile Programmierung
Wenn Sie die Standardmessungen in englischen Einheiten durchführen möchten, implementieren Sie die
Methoden Länge und Breite normal, und implementieren Sie explizit die Methoden Länge und Breite aus der
„IMetricDimensions“-Schnittstelle.

// Normal implementation:
public float Length() => lengthInches;
public float Width() => widthInches;

// Explicit implementation:
float IMetricDimensions.Length() => lengthInches * 2.54f;
float IMetricDimensions.Width() => widthInches * 2.54f;

In diesem Fall können Sie auf die englischen Einheiten von der Klasseninstanz aus zugreifen und auf die
metrischen Einheiten von der Schnittstelleninstanz aus.

public static void Test()


{
Box box1 = new Box(30.0f, 20.0f);
IMetricDimensions mDimensions = box1;

System.Console.WriteLine("Length(in): {0}", box1.Length());


System.Console.WriteLine("Width (in): {0}", box1.Width());
System.Console.WriteLine("Length(cm): {0}", mDimensions.Length());
System.Console.WriteLine("Width (cm): {0}", mDimensions.Width());
}

Weitere Informationen
C#-Programmierhandbuch
Objektorientiertes Programmieren
Schnittstellen
Explizites Implementieren von Schnittstellenmembern
Delegaten (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Ein Delegat ist ein Typ, der Verweise auf Methoden mit einer bestimmten Parameterliste und dem Rückgabetyp
darstellt. Nach Instanziierung eines Delegaten können Sie die Instanz mit einer beliebigen Methode verknüpfen,
die eine kompatible Signatur und einen kompatiblen Rückgabetyp aufweist. Sie können die Methode über die
Delegatinstanz aufrufen.
Delegaten werden verwendet, um Methoden als Argumente an anderen Methoden zu übergeben.
Ereignishandler sind nichts weiter als Methoden, die durch Delegaten aufgerufen werden. Wenn Sie eine
benutzerdefinierte Methode erstellen, kann eine Klasse wie das Windows-Steuerelement diese Methode
aufrufen, sobald ein bestimmtes Ereignis eintritt. Das folgende Beispiel veranschaulicht die Deklaration eines
Delegaten:

public delegate int PerformCalculation(int x, int y);

Jede Methode einer beliebigen verfügbaren Klasse oder Struktur, die mit dem Delegattyp übereinstimmt, kann
dem Delegaten zugewiesen werden. Bei der Methode kann es sich um eine statische Methode oder um eine
Instanzenmethode handeln. Diese Flexibilität ermöglicht das programmgesteuerte Ändern von
Methodenaufrufen oder die Integration von neuem Code in bereits vorhandene Klassen.

NOTE
In Verbindung mit dem Überladen von Methoden schließt die Signatur einer Methode den Rückgabewert nicht ein. Bei
Delegaten ist der Rückgabewert jedoch in die Signatur eingeschlossen. Mit anderen Worten muss eine Methode
denselben Rückgabetyp haben wie der Delegat.

Diese Fähigkeit, als Parameter auf eine Methode zu verweisen, macht Delegaten ideal für das Definieren von
Rückrufmethoden. Sie können eine Methode schreiben, die zwei Objekte in Ihrer Anwendung vergleicht. Diese
Methode kann in einem Delegaten für einen Sortieralgorithmus verwendet werden. Da der Vergleichscode von
der Bibliothek getrennt ist, kann die Sortiermethode allgemeiner sein.
Funktionszeiger wurden C# 9 für ähnliche Szenarios hinzugefügt, in denen Sie mehr Kontrolle über die
Aufrufkonvention benötigen. Der einem Delegaten zugeordnete Code wird mithilfe einer virtuellen Methode
aufgerufen, die einem Delegattypen hinzugefügt wird. Mithilfe von Funktionszeigern können Sie
unterschiedliche Konventionen angeben.

Übersicht über Delegaten


Delegaten verfügen über folgende Eigenschaften:
Delegaten ähneln C++-Funktionszeigern, sind aber vollständig objektorientiert, und im Gegensatz zu C++-
Zeigern auf Memberfunktionen kapseln Delegaten sowohl eine Objektinstanz als auch eine Methode.
Delegaten ermöglichen es, Methoden als Parameter zu übergeben.
Delegaten können zum Definieren von Rückrufmethoden verwendet werden.
Delegaten können miteinander verkettet werden. So können beispielsweise mehrere Methoden für ein
einziges Ereignis aufgerufen werden.
Methoden müssen nicht exakt mit dem Typ des Delegaten übereinstimmen. Weitere Informationen finden
Sie unter Verwenden von Varianz in Delegaten.
Lambdaausdrücke sind eine präzisere Methode zum Schreiben von Inlinecodeblöcken. Lambdaausdrücke
werden (in bestimmten Kontexten) in Delegattypen kompiliert. Weitere Informationen zu Lambdaausdrücken
finden Sie unter Lambdaausdrücke.

In diesem Abschnitt
Verwenden von Delegaten
Verwenden von Delegaten anstelle Schnittstellen (C#-Programmierhandbuch)
Delegate mit benannten im Vergleich zu anonymen Methoden
Verwenden von Varianz bei Delegaten
Kombinieren von Delegaten (Multicastdelegaten)
Deklarieren, Instanziieren und Verwenden von Delegaten

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter Delegaten in der C#-Sprachspezifikation. Die Sprachspezifikation ist
die verbindliche Quelle für die Syntax und Verwendung von C#.

Enthaltene Buchkapitel
Delegates, Events, and Lambda Expressions (Delegaten, Ereignisse und Lambda-Ausdrücke) in C# 3.0
Cookbook, Third Edition: More than 250 solutions for C# 3.0 programmers (C# 3.0-Cookbook, 3. Auflage:
Mehr als 250 Lösungen für C# 3.0-Programmierer)
Delegates and Events (Delegaten und Ereignisse) in Learning C# 3.0: Master the Fundamentals of C# 3.0
(Erlernen von C# 3.0: Die Grundlagen von C# 3.0)

Siehe auch
Delegate
C#-Programmierhandbuch
Ereignisse
Verwenden von Delegaten (C#-
Programmierhandbuch)
04.11.2021 • 5 minutes to read

Ein Delegat ist ein Typ, der ähnlich einem Funktionszeiger in C und C++ eine Methode sicher kapselt. Im
Gegensatz zu C-Funktionszeigern sind Delegate objektorientiert, typsicher und sicher. Der Typ eines Delegaten
wird durch den Namen des Delegaten definiert. Im folgenden Beispiel wird ein Delegat mit dem Namen Del
deklariert, der eine Methode kapseln kann, die eine Zeichenfolge als Argument übernimmt und void zurückgibt:

public delegate void Del(string message);

Ein Delegatobjekt wird normalerweise durch Angabe des Namens der Methode, die der Delegat umschließt,
oder mit einem Lambdaausdruck erstellt. Sobald ein Delegat instanziiert ist, wird vom Delegaten ein
Methodenaufruf an den Delegaten an diese Methode übergeben. Die vom Aufrufer an den Delegaten
übergebenen Parameter werden an die Methode übergeben, und der Rückgabewert von der Methode wird ggf.
durch den Delegaten an den Aufrufer zurückgegeben. Dies wird als Aufrufen des Delegaten bezeichnet. Ein
instanziierter Delegat kann wie die eingeschlossene Methode selbst aufgerufen werden. Zum Beispiel:

// Create a method for a delegate.


public static void DelegateMethod(string message)
{
Console.WriteLine(message);
}

// Instantiate the delegate.


Del handler = DelegateMethod;

// Call the delegate.


handler("Hello World");

Delegattypen werden von der Delegate-Klasse in .NET abgeleitet. Delegattypen sind versiegelt – von Ihnen kann
nicht abgeleitet werden – und es ist nicht möglich benutzerdefinierte Klassen von Delegate abzuleiten. Da der
instanziierte Delegat ein Objekt ist, kann er als Parameter übergeben oder einer Eigenschaft zugewiesen werden.
Dies ermöglicht es einer Methode, einen Delegaten als Parameter zu akzeptieren und den Delegaten zu einem
späteren Zeitpunkt aufzurufen. Dies wird als asynchroner Rückruf bezeichnet und ist eine häufig verwendete
Methode, um einen Aufrufer darüber zu benachrichtigen, dass ein langer Prozess abgeschlossen wurde. Wenn
ein Delegat auf diese Weise verwendet wird, benötigt der Code, der den Delegaten verwendet, keine Kenntnisse
über die Implementierung der verwendeten Methode. Die Funktion ähnelt den bereitgestellten
Kapselungsschnittstellen.
Ein weiterer häufiger Einsatzbereich von Rückrufen ist die Definition einer benutzerdefinierten
Vergleichsmethode und die Übergabe dieses Delegaten an eine Sortiermethode. Dadurch kann der Code des
Aufrufers Teil des Sortieralgorithmus werden. Im folgenden Beispiel wird der Typ Del als Parameter verwendet:

public static void MethodWithCallback(int param1, int param2, Del callback)


{
callback("The number is: " + (param1 + param2).ToString());
}
Sie können anschließend den oben erstellten Delegaten an diese Methode übergeben:

MethodWithCallback(1, 2, handler);

woraufhin die folgende Ausgabe auf der Konsole angezeigt wird:

The number is: 3

Wenn Sie den Delegaten als Abstraktion verwenden, muss MethodWithCallback die Konsole nicht direkt
aufrufen, d. h., bei der Entwicklung muss keine Konsole berücksichtigt werden. MethodWithCallback bereitet
einfach eine Zeichenfolge vor und übergibt sie an eine andere Methode. Dies ist besonders leistungsstark, da
eine delegierte Methode eine beliebige Anzahl Parameter verwenden kann.
Wenn ein Delegat erstellt wurde, um eine Instanzenmethode zu umschließen, verweist der Delegat sowohl auf
die Instanz als auch auf die Methode. Ein Delegat kennt nicht den Instanzentyp, abgesehen von der
umschlossenen Methode, sodass ein Delegat auf jede Art von Objekt verweisen kann, sofern es eine Methode
für das Objekt gibt, die mit der Signatur des Delegaten übereinstimmt. Wenn ein Delegat erstellt wurde, um eine
statische Methode zu umschließen, verweist er nur auf die Methode. Betrachten Sie hierzu die folgenden
Deklarationen:

public class MethodClass


{
public void Method1(string message) { }
public void Method2(string message) { }
}

Zusammen mit der zuvor dargestellten statischen DelegateMethod verfügen Sie jetzt über drei Methoden, die
von einer Del -Instanz umschlossen werden können.
Ein Delegat kann bei Aufruf mehr als eine Methode aufrufen. Dies wird als Multicasting bezeichnet. Um der Liste
an Methoden des Delegaten, sprich der Aufrufliste, eine weitere Methode hinzuzufügen, müssen lediglich zwei
Delegaten mithilfe der Additions- oder Additionszuweisungsoperatoren ('+' oder '+=') hinzugefügt werden.
Zum Beispiel:

var obj = new MethodClass();


Del d1 = obj.Method1;
Del d2 = obj.Method2;
Del d3 = DelegateMethod;

//Both types of assignment are valid.


Del allMethodsDelegate = d1 + d2;
allMethodsDelegate += d3;

Zu diesem Zeitpunkt enthält allMethodsDelegate drei Methoden in der Aufrufliste: Method1 , Method2 und
DelegateMethod . Die ursprünglichen drei Delegaten, d1 , d2 und d3 , bleiben unverändert. Wenn
allMethodsDelegate aufgerufen wird, werden alle drei Methoden nacheinander aufgerufen. Wenn der Delegat
Verweisparameter verwendet, wird der Verweis wiederum nacheinander an jede der drei Methoden übergeben,
und alle Änderungen einer Methode sind für die nächste Methode sichtbar. Wenn eine der Methoden eine
Ausnahme auslöst, die nicht innerhalb der Methode abgefangen wird, wird diese Ausnahme an den Aufrufer des
Delegaten übergeben und keine der nachfolgenden Methoden in der Aufrufliste wird aufgerufen. Wenn der
Delegat über einen Rückgabewert und/oder out-Parameter verfügt, gibt er den Rückgabewert und die
Parameter der letzten aufgerufenen Methode zurück. Um eine Methode aus der Aufrufliste zu entfernen,
verwenden Sie die Subtraktions- oder Subtraktionszuweisungsoperatoren ( - oder -= ). Zum Beispiel:
//remove Method1
allMethodsDelegate -= d1;

// copy AllMethodsDelegate while removing d2


Del oneMethodDelegate = allMethodsDelegate - d2;

Da Delegattypen von System.Delegate abgeleitet werden, können die Methoden und Eigenschaften, die durch
diese Klasse definiert werden, für den Delegaten aufgerufen werden. Beispiel: Schreiben Sie Folgendes, um die
Anzahl der Methoden in der Aufrufliste eines Delegaten zu ermitteln:

int invocationCount = d1.GetInvocationList().GetLength(0);

Delegaten mit mehr als einer Methode in der Aufrufliste werden von MulticastDelegate, einer Unterklasse von
System.Delegate , abgeleitet. Der obige Code funktioniert in jedem Fall, da beide Klassen GetInvocationList
unterstützen.
Multicastdelegaten werden ausgiebig bei der Ereignisbehandlung verwendet. Ereignisquellobjekte senden
Ereignisbenachrichtigungen an Empfängerobjekte, die für den Erhalt dieses Ereignisses registriert wurden. Um
sich für ein Ereignis zu registrieren, erstellt der Empfänger eine Methode zur Behandlung des Ereignisses, dann
erstellt er einen Delegaten für die Methode und übergibt den Delegaten an die Ereignisquelle. Die Quelle ruft
den Delegaten auf, wenn das Ereignis eintritt. Der Delegat ruft dann die Ereignisbehandlungsmethode für den
Empfänger auf und übermittelt die Ereignisdaten. Der Delegattyp für ein bestimmtes Ereignis wird von der
Ereignisquelle definiert. Weitere Informationen finden Sie unter Ereignisse.
Beim Vergleichen von zwei unterschiedlichen zugewiesenen Typen zur Kompilierzeit kommt es zu einem
Kompilierungsfehler. Falls die Delegatinstanzen statisch vom Typ System.Delegate sind, dann ist der Vergleich
zulässig, gibt jedoch zur Laufzeit "False" zurück. Beispiel:

delegate void Delegate1();


delegate void Delegate2();

static void method(Delegate1 d, Delegate2 e, System.Delegate f)


{
// Compile-time error.
//Console.WriteLine(d == e);

// OK at compile-time. False if the run-time type of f


// is not the same as that of d.
Console.WriteLine(d == f);
}

Siehe auch
C#-Programmierhandbuch
Delegaten
Verwenden von Varianz bei Delegaten
Varianz bei Delegaten
Verwenden von Varianz für die generischen Delegaten Func und Action
Ereignisse
Delegaten mit benannten im Vergleich zu Anonyme
Methoden (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Ein Delegat kann einer benannten Methode zugeordnet werden. Wenn Sie einen Delegaten mit einer benannten
Methode instanziieren, wird die Methode als Parameter übergeben:

// Declare a delegate.
delegate void Del(int x);

// Define a named method.


void DoWork(int k) { /* ... */ }

// Instantiate the delegate using the method as a parameter.


Del d = obj.DoWork;

Dies wird mit einer benannten Methode aufgerufen. Mit einer benannten Methode erstellte Delegaten können
entweder eine statische Methode oder eine Instanzmethode kapseln. Benannte Methoden sind die einzige
Möglichkeit, einen Delegaten in früheren Versionen von C# zu instanziieren. Wenn das Erstellen einer neuen
Methode jedoch einen unerwünschten Aufwand für Sie darstellt, können Sie in C# einen Delegaten instanziieren
und sofort einen Codeblock bestimmen, den der Delegat bei seinem Aufruf verarbeitet. Der Block kann
entweder einen Lambdaausdruck oder eine anonyme Methode enthalten.

Hinweise
Die Methode, die Sie als Delegatparameter übergeben, muss die gleiche Signatur wie die Delegatdeklaration
aufweisen.
Eine Delegatinstanz kann entweder eine statische Methode oder eine Instanzmethode kapseln.
Obwohl der Delegat einen out-Parameter verwenden kann, wird empfohlen, ihn nicht mit Multicast-
Ereignisdelegaten zu verwenden, da Sie nicht wissen können, welcher Delegat aufgerufen wird.

Beispiel 1
In diesem einfachen Beispiel wird ein Delegat deklariert und verwendet. Beachten Sie, dass der Delegat Del
und die zugeordnete Methode MultiplyNumbers dieselbe Signatur aufweisen.
// Declare a delegate
delegate void Del(int i, double j);

class MathClass
{
static void Main()
{
MathClass m = new MathClass();

// Delegate instantiation using "MultiplyNumbers"


Del d = m.MultiplyNumbers;

// Invoke the delegate object.


Console.WriteLine("Invoking the delegate using 'MultiplyNumbers':");
for (int i = 1; i <= 5; i++)
{
d(i, 2);
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}

// Declare the associated method.


void MultiplyNumbers(int m, double n)
{
Console.Write(m * n + " ");
}
}
/* Output:
Invoking the delegate using 'MultiplyNumbers':
2 4 6 8 10
*/

Beispiel 2
Im folgenden Beispiel wird ein Delegat sowohl einer statischen Methode als auch einer Instanzmethode
zugeordnet und gibt von jeder spezifische Informationen zurück.
// Declare a delegate
delegate void Del();

class SampleClass
{
public void InstanceMethod()
{
Console.WriteLine("A message from the instance method.");
}

static public void StaticMethod()


{
Console.WriteLine("A message from the static method.");
}
}

class TestSampleClass
{
static void Main()
{
var sc = new SampleClass();

// Map the delegate to the instance method:


Del d = sc.InstanceMethod;
d();

// Map to the static method:


d = SampleClass.StaticMethod;
d();
}
}
/* Output:
A message from the instance method.
A message from the static method.
*/

Weitere Informationen
C#-Programmierhandbuch
Delegaten
Kombinieren von Delegaten (Multicastdelegaten)
Ereignisse
Kombinieren von Delegaten (Multicastdelegaten)
(C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Das folgende Beispiel veranschaulicht das Erstellen von Multicastdelegaten. Eine nützliche Eigenschaft von
delegate-Objekten besteht darin, dass einer Delegatinstanz mithilfe des Operators + mehrere Objekte
zugewiesen werden können. Der Multicastdelegat enthält eine Liste der zugewiesenen Delegaten. Wenn der
Multicastdelegat aufgerufen wird, ruft er die Delegaten in der Liste nacheinander auf. Es können nur Delegaten
desselben Typs kombiniert werden.
Mithilfe des Operators - kann ein Komponentendelegat aus einem Multicastdelegaten entfernt werden.

Beispiel
using System;

// Define a custom delegate that has a string parameter and returns void.
delegate void CustomDel(string s);

class TestClass
{
// Define two methods that have the same signature as CustomDel.
static void Hello(string s)
{
Console.WriteLine($" Hello, {s}!");
}

static void Goodbye(string s)


{
Console.WriteLine($" Goodbye, {s}!");
}

static void Main()


{
// Declare instances of the custom delegate.
CustomDel hiDel, byeDel, multiDel, multiMinusHiDel;

// In this example, you can omit the custom delegate if you


// want to and use Action<string> instead.
//Action<string> hiDel, byeDel, multiDel, multiMinusHiDel;

// Create the delegate object hiDel that references the


// method Hello.
hiDel = Hello;

// Create the delegate object byeDel that references the


// method Goodbye.
byeDel = Goodbye;

// The two delegates, hiDel and byeDel, are combined to


// form multiDel.
multiDel = hiDel + byeDel;

// Remove hiDel from the multicast delegate, leaving byeDel,


// which calls only the method Goodbye.
multiMinusHiDel = multiDel - hiDel;

Console.WriteLine("Invoking delegate hiDel:");


hiDel("A");
Console.WriteLine("Invoking delegate byeDel:");
byeDel("B");
Console.WriteLine("Invoking delegate multiDel:");
multiDel("C");
Console.WriteLine("Invoking delegate multiMinusHiDel:");
multiMinusHiDel("D");
}
}
/* Output:
Invoking delegate hiDel:
Hello, A!
Invoking delegate byeDel:
Goodbye, B!
Invoking delegate multiDel:
Hello, C!
Goodbye, C!
Invoking delegate multiMinusHiDel:
Goodbye, D!
*/
Weitere Informationen
MulticastDelegate
C#-Programmierhandbuch
Ereignisse
Vorgehensweise: Deklarieren, Instanziieren und
Verwenden von Delegaten (C#-
Programmierleitfaden)
04.11.2021 • 4 minutes to read

In C# 1.0 und höher können Delegaten wie im folgenden Beispiel gezeigt deklariert werden.

// Declare a delegate.
delegate void Del(string str);

// Declare a method with the same signature as the delegate.


static void Notify(string name)
{
Console.WriteLine($"Notification received for: {name}");
}

// Create an instance of the delegate.


Del del1 = new Del(Notify);

C# 2.0 bietet eine einfachere Möglichkeit zum Schreiben der vorangegangenen Deklaration, siehe folgendes
Beispiel.

// C# 2.0 provides a simpler way to declare an instance of Del.


Del del2 = Notify;

In C# 2.0 und höher ist es außerdem möglich, eine anonyme Methode zum Deklarieren und Initialisieren eines
Delegaten zu verwenden. Dies wird im nachstehenden Beispiel gezeigt.

// Instantiate Del by using an anonymous method.


Del del3 = delegate(string name)
{ Console.WriteLine($"Notification received for: {name}"); };

In C# 3.0 und höher können Delegaten auch mithilfe eines Lamdaausdrucks deklariert und instanziiert werden,
wie im folgenden Beispiel dargestellt.

// Instantiate Del by using a lambda expression.


Del del4 = name => { Console.WriteLine($"Notification received for: {name}"); };

Weitere Informationen finden Sie unter Lambdaausdrücke.


Im folgenden Beispiel wird verdeutlicht, wie Sie einen Delegaten deklarieren, instanziieren und verwenden. In
der BookDB -Klasse ist eine Buchhandlungsdatenbank gekapselt, die eine Buchtiteldatenbank enthält. Sie stellt
eine ProcessPaperbackBooks -Methode zur Verfügung, durch die alle Taschenbücher in der Datenbank gefunden
werden und für jedes Taschenbuch ein Delegat aufgerufen wird. Der verwendete delegate -Typ hat den Namen
ProcessBookCallback . Die Test -Klasse verwendet diese Klasse, um die Buchtitel und Durchschnittspreise der
Taschenbücher auszugeben.
Die Verwendung von Delegaten beinhaltet eine sinnvolle Trennung der Funktionalitäten von
Buchhandlungsdatenbank und Clientcode. Der Clientcode hat keine Kenntnis darüber, wie die Bücher archiviert
werden oder wie der Buchhandlungscode Taschenbücher sucht. Der Buchhandlungscode wiederum hat keine
Kenntnis darüber, wie ein Taschenbuchtitel weiterverarbeitet wird, nachdem er gefunden wurde.

Beispiel
// A set of classes for handling a bookstore:
namespace Bookstore
{
using System.Collections;

// Describes a book in the book list:


public struct Book
{
public string Title; // Title of the book.
public string Author; // Author of the book.
public decimal Price; // Price of the book.
public bool Paperback; // Is it paperback?

public Book(string title, string author, decimal price, bool paperBack)


{
Title = title;
Author = author;
Price = price;
Paperback = paperBack;
}
}

// Declare a delegate type for processing a book:


public delegate void ProcessBookCallback(Book book);

// Maintains a book database.


public class BookDB
{
// List of all books in the database:
ArrayList list = new ArrayList();

// Add a book to the database:


public void AddBook(string title, string author, decimal price, bool paperBack)
{
list.Add(new Book(title, author, price, paperBack));
}

// Call a passed-in delegate on each paperback book to process it:


public void ProcessPaperbackBooks(ProcessBookCallback processBook)
{
foreach (Book b in list)
{
if (b.Paperback)
// Calling the delegate:
processBook(b);
}
}
}
}

// Using the Bookstore classes:


namespace BookTestClient
{
using Bookstore;

// Class to total and average prices of books:


class PriceTotaller
{
int countBooks = 0;
decimal priceBooks = 0.0m;
internal void AddBookToTotal(Book book)
{
countBooks += 1;
priceBooks += book.Price;
}

internal decimal AveragePrice()


{
return priceBooks / countBooks;
}
}

// Class to test the book database:


class Test
{
// Print the title of the book.
static void PrintTitle(Book b)
{
Console.WriteLine($" {b.Title}");
}

// Execution starts here.


static void Main()
{
BookDB bookDB = new BookDB();

// Initialize the database with some books:


AddBooks(bookDB);

// Print all the titles of paperbacks:


Console.WriteLine("Paperback Book Titles:");

// Create a new delegate object associated with the static


// method Test.PrintTitle:
bookDB.ProcessPaperbackBooks(PrintTitle);

// Get the average price of a paperback by using


// a PriceTotaller object:
PriceTotaller totaller = new PriceTotaller();

// Create a new delegate object associated with the nonstatic


// method AddBookToTotal on the object totaller:
bookDB.ProcessPaperbackBooks(totaller.AddBookToTotal);

Console.WriteLine("Average Paperback Book Price: ${0:#.##}",


totaller.AveragePrice());
}

// Initialize the book database with some test books:


static void AddBooks(BookDB bookDB)
{
bookDB.AddBook("The C Programming Language", "Brian W. Kernighan and Dennis M. Ritchie", 19.95m,
true);
bookDB.AddBook("The Unicode Standard 2.0", "The Unicode Consortium", 39.95m, true);
bookDB.AddBook("The MS-DOS Encyclopedia", "Ray Duncan", 129.95m, false);
bookDB.AddBook("Dogbert's Clues for the Clueless", "Scott Adams", 12.00m, true);
}
}
}
/* Output:
Paperback Book Titles:
The C Programming Language
The Unicode Standard 2.0
Dogbert's Clues for the Clueless
Average Paperback Book Price: $23.97
*/
Stabile Programmierung
Deklarieren von Delegaten
Mit der folgenden Anweisung wird ein neuer Delegattyp deklariert.

public delegate void ProcessBookCallback(Book book);

Durch die einzelnen Delegattypen werden Anzahl und Typen von Argumenten sowie Rückgabewerte von
Methoden beschrieben, die gekapselt werden können. Sobald neue Argumenttypen benötigt werden
oder ein neuer Rückgabewerttyp erforderlich ist, muss ein neuer Delegattyp deklariert werden.
Instanziieren von Delegaten
Nachdem ein Delegattyp deklariert wurde, muss ein Delegatobjekt erstellt und einer bestimmten
Methode zugeordnet werden. Im vorherigen Beispiel übergeben Sie dazu die PrintTitle -Methode an
die ProcessPaperbackBooks -Methode, wie im folgenden Beispiel gezeigt:

bookDB.ProcessPaperbackBooks(PrintTitle);

Hierdurch wird ein neues Delegatobjekt erstellt, das der statischen Methode Test.PrintTitle zugeordnet
ist. Auf ähnliche Weise wird die nicht statische Methode AddBookToTotal für das Objekt totaller
übergeben, wie im folgenden Beispiel gezeigt:

bookDB.ProcessPaperbackBooks(totaller.AddBookToTotal);

In beiden Fällen wird ein neues Delegatobjekt an die ProcessPaperbackBooks -Methode übergeben.
Nach der Erstellung eines Delegaten wird die diesem zugeordnete Methode nicht mehr geändert.
Delegatobjekte sind unveränderlich.
Aufrufen von Delegaten
Nachdem ein Delegatobjekt erstellt wurde, wird es normalerweise an anderen Code übergeben, durch
den der Delegat aufgerufen wird. Ein Delegatobjekt wird über seinen Namen aufgerufen. Auf den Namen
folgen (in Klammern gesetzte) Argumente, die an den Delegaten übergeben werden sollen. Es folgt ein
Beispiel für einen Delegataufruf:

processBook(b);

Ein Delegat kann entweder (wie in diesem Beispiel) synchron oder mithilfe der BeginInvoke -Methode
und der EndInvoke -Methode asynchron aufgerufen werden.

Weitere Informationen
C#-Programmierhandbuch
Ereignisse
Delegaten
Arrays (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Sie können mehrere Variablen des gleichen Typs in einer Arraydatenstruktur speichern. Ein Array wird
deklariert, indem der Typ seiner Elemente angegeben wird. Wenn Sie möchten, dass das Array Element jedes
Typs speichert, können Sie object als dessen Typ angeben. Im vereinheitlichen Typsystem von C# erben alle
Typen, vordefiniert und benutzerdefiniert sowie Verweis- und Werttypen, direkt oder indirekt von Object.

type[] arrayName;

Beispiel
In den folgenden Beispiel wird ein eindimensionales, ein mehrdimensionales und ein verzweigtes Array erstellt:

class TestArraysClass
{
static void Main()
{
// Declare a single-dimensional array of 5 integers.
int[] array1 = new int[5];

// Declare and set array element values.


int[] array2 = new int[] { 1, 3, 5, 7, 9 };

// Alternative syntax.
int[] array3 = { 1, 2, 3, 4, 5, 6 };

// Declare a two dimensional array.


int[,] multiDimensionalArray1 = new int[2, 3];

// Declare and set array element values.


int[,] multiDimensionalArray2 = { { 1, 2, 3 }, { 4, 5, 6 } };

// Declare a jagged array.


int[][] jaggedArray = new int[6][];

// Set the values of the first array in the jagged array structure.
jaggedArray[0] = new int[4] { 1, 2, 3, 4 };
}
}

Übersicht über Arrays


Ein Array verfügt über die folgenden Eigenschaften:
Ein Array kann eindimensional, mehrdimensional oder verzweigt sein.
Die Anzahl der Dimensionen und die Länge der einzelnen Dimensionen werden festgelegt, wenn die
Arrayinstanz erstellt wird. Diese Werte können während der Lebensdauer der Instanz nicht geändert werden.
Numerische Arrayelemente sind standardmäßig auf 0 (null) festgelegt, Verweiselemente auf null .
Ein verzweigtes Array ist ein Array von Arrays, und deshalb sind seine Elemente Referenztypen und werden
mit null initialisiert.
Arrays sind nullbasiert: Der Index eines Arrays mit n Elementen beginnt bei 0 und endet bei n-1 .
Arrayelemente können einen beliebigen Typ aufweisen, z. B. auch einen Arraytyp.
Arraytypen sind Referenztypen, die vom abstrakten Basistyp Array abgeleitet werden. Alle Arrays werden als
IList und IEnumerable implementiert. Sie können die foreach-Anweisung verwenden, um ein Array zu
durchlaufen. Eindimensionale Arrays implementieren ebenfalls IList<T> und IEnumerable<T>.
Verhalten von Standardwerten
Bei Werttypen werden die Arrayelemente mit dem Standardwert initialisiert (0-Bit-Muster). Die Elemente
weisen den Wert 0 auf.
Alle Verweistypen (einschließlich der Non-Nullable-Typen) weisen den Wert null auf.
Für Nullable-Werttypen wird HasValue auf false festgelegt, und die Elemente werden auf null festgelegt.
Arrays als Objekte
In C# sind Arrays tatsächlich Objekte und nicht nur adressierbare Regionen zusammenhängender Speicher wie
in C und C++. Array ist der abstrakte Basistyp aller Typen von Arrays. Sie können die Eigenschaften und die
anderen Klassenmember verwenden, über die Array verfügt. Ein Beispiel dafür ist die Verwendung der Length-
Eigenschaft, um die Länge eines Arrays zu erhalten. Der folgende Code weist die Länge des numbers -Arrays, die
5 beträgt, einer Variablen mit dem Namen lengthOfNumbers zu:

int[] numbers = { 1, 2, 3, 4, 5 };
int lengthOfNumbers = numbers.Length;

Die Array-Klasse bietet viele weitere nützliche Methoden und Eigenschaften zum Sortieren, Durchsuchen und
Kopieren von Arrays. Im folgenden Beispiel wird die Rank-Eigenschaft verwendet, um die Anzahl der
Dimensionen eines Arrays anzuzeigen.

class TestArraysClass
{
static void Main()
{
// Declare and initialize an array.
int[,] theArray = new int[5, 10];
System.Console.WriteLine("The array has {0} dimensions.", theArray.Rank);
}
}
// Output: The array has 2 dimensions.

Weitere Informationen
Verwenden eindimensionaler Arrays
Verwenden mehrdimensionaler Arrays
Verwenden verzweigter Arrays
Verwenden von foreach mit Arrays
Übergeben von Arrays als Argumente
Implizit typisierte Arrays
C#-Programmierhandbuch
Sammlungen
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
Eindimensionale Arrays (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Sie erstellen ein eindimensionales Array mit dem Operator new, indem Sie den Arrayelementtyp und die Anzahl
der Elemente angeben. Im folgenden Beispiel wird ein Array aus fünf Integern deklariert:

int[] array = new int[5];

Dieses Array enthält die Elemente array[0] bis array[4] . Die Elemente des Arrays werden mit dem
Standardwert des Elementtyps initialisiert ( 0 für ganze Zahlen).
Arrays können jeden beliebigen Elementtyp speichern, den Sie angeben. Dies wird im folgenden Beispiel
veranschaulicht, in dem ein Array aus Zeichenfolgen deklariert wird:

string[] stringArray = new string[6];

Arrayinitialisierung
Sie können die Elemente eines Arrays initialisieren, wenn Sie das Array deklarieren. Der Spezifizierer für die
Länge ist nicht erforderlich, weil dieser von der Anzahl der Elemente in der Initialisierungsliste abgeleitet wird.
Zum Beispiel:

int[] array1 = new int[] { 1, 3, 5, 7, 9 };

Im folgenden Code wird eine Deklaration eines Zeichenfolgenarrays veranschaulicht, in dem jedes Arrayelement
durch den Namen eines Wochentags initialisiert wird:

string[] weekDays = new string[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

Wie im folgenden Code gezeigt, können Sie den new -Ausdruck und den Arraytyp bei der Deklaration
vermeiden, wenn Sie ein Array initialisieren. Diese Art von Array wird als implizit typisiertes Array bezeichnet:

int[] array2 = { 1, 3, 5, 7, 9 };
string[] weekDays2 = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

Sie können eine Arrayvariable deklarieren, ohne sie zu erstellen. Sie müssen jedoch den Operator new
verwenden, wenn Sie dieser Variable ein neues Array zuweisen. Zum Beispiel:

int[] array3;
array3 = new int[] { 1, 3, 5, 7, 9 }; // OK
//array3 = {1, 3, 5, 7, 9}; // Error

Werttyp- und Verweistyparrays


Betrachten Sie die folgende Arraydeklaration:
SomeType[] array4 = new SomeType[10];

Das Ergebnis dieser Anweisung hängt davon ab, ob SomeType ein Werttyp oder ein Verweistyp ist. Wenn es sich
um einen Werttyp handelt, erstellt die Anweisung ein Array aus 10 Elementen, die alle den Typ SomeType
aufweisen. Stellt SomeType einen Verweistyp dar, wird durch die Anweisung ein Array aus 10 Elementen erstellt,
von denen jedes mit einem NULL-Verweis initialisiert wird. In beiden Instanzen werden die Elemente mit dem
Standardwert für den Elementtyp initialisiert. Weitere Informationen zu Werttypen und Verweistypen finden Sie
unter Werttypen und Verweistypen.

Abrufen von Daten aus einem Array


Sie können die Daten eines Arrays mithilfe eines Indexes abrufen. Zum Beispiel:

string[] weekDays2 = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

Console.WriteLine(weekDays2[0]);
Console.WriteLine(weekDays2[1]);
Console.WriteLine(weekDays2[2]);
Console.WriteLine(weekDays2[3]);
Console.WriteLine(weekDays2[4]);
Console.WriteLine(weekDays2[5]);
Console.WriteLine(weekDays2[6]);

/*Output:
Sun
Mon
Tue
Wed
Thu
Fri
Sat
*/

Siehe auch
Array
Arrays
Mehrdimensionale Arrays
Verzweigte Arrays
Mehrdimensionale Arrays (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Arrays können mehr als eine Dimension aufweisen. Die folgende Deklaration erstellt z.B. ein zweidimensionales
Array mit vier Zeilen und zwei Spalten.

int[,] array = new int[4, 2];

Die folgende Deklaration erstellt ein Array mit drei Dimensionen 4, 2 und 3.

int[,,] array1 = new int[4, 2, 3];

Arrayinitialisierung
Sie können das Array wie im folgenden Beispiel gezeigt nach der Deklaration initialisieren.
// Two-dimensional array.
int[,] array2D = new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
// The same array with dimensions specified.
int[,] array2Da = new int[4, 2] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
// A similar array with string elements.
string[,] array2Db = new string[3, 2] { { "one", "two" }, { "three", "four" },
{ "five", "six" } };

// Three-dimensional array.
int[,,] array3D = new int[,,] { { { 1, 2, 3 }, { 4, 5, 6 } },
{ { 7, 8, 9 }, { 10, 11, 12 } } };
// The same array with dimensions specified.
int[,,] array3Da = new int[2, 2, 3] { { { 1, 2, 3 }, { 4, 5, 6 } },
{ { 7, 8, 9 }, { 10, 11, 12 } } };

// Accessing array elements.


System.Console.WriteLine(array2D[0, 0]);
System.Console.WriteLine(array2D[0, 1]);
System.Console.WriteLine(array2D[1, 0]);
System.Console.WriteLine(array2D[1, 1]);
System.Console.WriteLine(array2D[3, 0]);
System.Console.WriteLine(array2Db[1, 0]);
System.Console.WriteLine(array3Da[1, 0, 1]);
System.Console.WriteLine(array3D[1, 1, 2]);

// Getting the total count of elements or the length of a given dimension.


var allLength = array3D.Length;
var total = 1;
for (int i = 0; i < array3D.Rank; i++)
{
total *= array3D.GetLength(i);
}
System.Console.WriteLine("{0} equals {1}", allLength, total);

// Output:
// 1
// 2
// 3
// 4
// 7
// three
// 8
// 12
// 12 equals 12

Sie können das Array auch ohne Angabe des Rangs initialisieren.

int[,] array4 = { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };

Wenn Sie eine Arrayvariable ohne Initialisierung deklarieren möchten, müssen Sie der Variable mit dem
Operator new ein Array zuweisen. Die Verwendung von new wird im folgenden Beispiel gezeigt.

int[,] array5;
array5 = new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } }; // OK
//array5 = {{1,2}, {3,4}, {5,6}, {7,8}}; // Error

Im folgende Beispiel wird einem bestimmten Arrayelement ein Wert zugewiesen.

array5[2, 1] = 25;

Auf ähnliche Weise wird im folgenden Beispiel der Wert eines bestimmten Arrayelements abgerufen und der
Variablen elementValue zugewiesen.

int elementValue = array5[2, 1];

Im folgenden Codebeispiel werden die Arrayelemente auf Standardwerte initialisiert (mit Ausnahme von
verzweigten Arrays).

int[,] array6 = new int[10, 10];

Siehe auch
C#-Programmierhandbuch
Arrays
Eindimensionale Arrays
Verzweigte Arrays
Verzweigte Arrays (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Ein Jagged Array ist ein Array, dessen Elemente Arrays möglicherweise verschiedener Größe sind. Ein
verzweigtes Array wird auch „Array aus Arrays“ genannt. Die folgenden Beispiele zeigen, wie Sie verzweigte
Arrays deklarieren, initialisieren und auf sie zugreifen können.
Die folgende Deklaration veranschaulicht ein eindimensionales Array mit drei Elementen, von denen jedes
wiederum ein eindimensionales Array aus ganzen Zahlen darstellt:

int[][] jaggedArray = new int[3][];

Vor der Verwendung von jaggedArray müssen seine Elemente initialisiert werden. Sie können die Elemente
folgendermaßen initialisieren:

jaggedArray[0] = new int[5];


jaggedArray[1] = new int[4];
jaggedArray[2] = new int[2];

Jedes Element ist ein eindimensionales Array aus ganzen Zahlen. Das erste Element ist ein Array aus 5 ganzen
Zahlen, das zweite aus 4 und das dritte aus 2.
Sie können auch Initialisierer verwenden, um die Arrayelemente mit Werten zu füllen. In diesem Fall wird die
Arraygröße nicht benötigt. Zum Beispiel:

jaggedArray[0] = new int[] { 1, 3, 5, 7, 9 };


jaggedArray[1] = new int[] { 0, 2, 4, 6 };
jaggedArray[2] = new int[] { 11, 22 };

Sie können das Array auch nach der Deklaration folgendermaßen initialisieren:

int[][] jaggedArray2 = new int[][]


{
new int[] { 1, 3, 5, 7, 9 },
new int[] { 0, 2, 4, 6 },
new int[] { 11, 22 }
};

Sie können folgende Kurzformen verwenden. Beachten Sie, dass der Operator new bei der Initialisierung der
Elemente nicht ausgelassen werden kann, da es für die Elemente keine Standardinitialisierung gibt:

int[][] jaggedArray3 =
{
new int[] { 1, 3, 5, 7, 9 },
new int[] { 0, 2, 4, 6 },
new int[] { 11, 22 }
};

Ein verzweigtes Array ist ein Array von Arrays, und deshalb sind seine Elemente Referenztypen und werden mit
null initialisiert.
In den folgenden Beispielen wird veranschaulicht, wie Sie auf einzelne Arrayelemente zugreifen können:

// Assign 77 to the second element ([1]) of the first array ([0]):


jaggedArray3[0][1] = 77;

// Assign 88 to the second element ([1]) of the third array ([2]):


jaggedArray3[2][1] = 88;

Es ist möglich, Jagged Arrays und mehrdimensionale Arrays zu mischen. Das folgende Beispiel zeigt die
Deklaration und Initialisierung eines eindimensionalen verzweigten Arrays, das drei zweidimensionale
Arrayelemente unterschiedlicher Größe enthält. Weitere Informationen finden Sie unter Mehrdimensionale
Arrays.

int[][,] jaggedArray4 = new int[3][,]


{
new int[,] { {1,3}, {5,7} },
new int[,] { {0,2}, {4,6}, {8,10} },
new int[,] { {11,22}, {99,88}, {0,9} }
};

Im folgenden Beispiel wird dargestellt, wie Sie auf einzelne Elemente zugreifen können. Der Wert des Elements
[1,0] des ersten Arrays (Wert 5 ) wird angezeigt:

System.Console.Write("{0}", jaggedArray4[0][1, 0]);

Die Methode Length gibt die Anzahl der Arrays zurück, die im verzweigten Array enthalten sind. Wenn Sie
beispielsweise das vorherige Array deklariert haben, dann gibt die folgende Zeile:

System.Console.WriteLine(jaggedArray4.Length);

den Wert 3 zurück.

Beispiel
In diesem Beispiel wird ein Array erstellt, dessen Elemente wiederum selbst Arrays sind. Jedes Arrayelement hat
eine unterschiedliche Größe.
class ArrayTest
{
static void Main()
{
// Declare the array of two elements.
int[][] arr = new int[2][];

// Initialize the elements.


arr[0] = new int[5] { 1, 3, 5, 7, 9 };
arr[1] = new int[4] { 2, 4, 6, 8 };

// Display the array elements.


for (int i = 0; i < arr.Length; i++)
{
System.Console.Write("Element({0}): ", i);

for (int j = 0; j < arr[i].Length; j++)


{
System.Console.Write("{0}{1}", arr[i][j], j == (arr[i].Length - 1) ? "" : " ");
}
System.Console.WriteLine();
}
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/* Output:
Element(0): 1 3 5 7 9
Element(1): 2 4 6 8
*/

Siehe auch
Array
C#-Programmierhandbuch
Arrays
Eindimensionale Arrays
Mehrdimensionale Arrays
Verwenden von foreach mit Arrays (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Die Anweisung foreach stellt eine einfache, klare Methode bereit, um die Elemente eines Arrays zu durchlaufen.
Bei eindimensionalen Arrays verarbeitet die Anweisung foreach Elemente in aufsteigender Indexreihenfolge,
beginnend bei Index 0 und endend bei Index Length - 1 :

int[] numbers = { 4, 5, 6, 1, 2, 3, -2, -1, 0 };


foreach (int i in numbers)
{
System.Console.Write("{0} ", i);
}
// Output: 4 5 6 1 2 3 -2 -1 0

Bei mehrdimensionalen Arrays werden Elemente so durchlaufen, dass die Indizes der äußersten rechten
Dimension zuerst erhöht werden, dann die der links daneben liegenden Dimension und so weiter, bis die linke
Seite erreicht ist:

int[,] numbers2D = new int[3, 2] { { 9, 99 }, { 3, 33 }, { 5, 55 } };


// Or use the short form:
// int[,] numbers2D = { { 9, 99 }, { 3, 33 }, { 5, 55 } };

foreach (int i in numbers2D)


{
System.Console.Write("{0} ", i);
}
// Output: 9 99 3 33 5 55

Bei mehrdimensionalen Arrays haben Sie jedoch eine größere Kontrolle über die Reihenfolge bei der
Verarbeitung von Arrayelementen, wenn Sie eine geschachtelte for-Schleife verwenden.

Siehe auch
Array
C#-Programmierhandbuch
Arrays
Eindimensionale Arrays
Mehrdimensionale Arrays
Verzweigte Arrays
Übergeben von Arrays als Argumente (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Arrays können als Argumente an Methodenparameter übergeben werden. Da es sich bei Arrays um
Verweistypen handelt, kann die Methode den Wert der Elemente ändern.

Übergeben von eindimensionalen Arrays als Argumente


Sie können ein initialisiertes eindimensionales Array an eine Methode übergeben. Die folgende Anweisung
sendet z.B. ein Array an eine print-Methode.

int[] theArray = { 1, 3, 5, 7, 9 };
PrintArray(theArray);

Im folgenden Code wird eine partielle Implementierung der print-Methode veranschaulicht.

void PrintArray(int[] arr)


{
// Method code.
}

Sie können einen neuen Array in einem Schritt initialisieren und übergeben, wie im folgenden Beispiel gezeigt.

PrintArray(new int[] { 1, 3, 5, 7, 9 });

Beispiel
Im folgenden Beispiel wird ein Array von Zeichenfolgen initialisiert und als Argument an eine DisplayArray -
Methode für Zeichenfolgen übergeben. Die Methode zeigt die Elemente des Arrays an. Als Nächstes kehrt die
ChangeArray -Methode die Elemente des Arrays um, und die ChangeArrayElements -Methode ändert die ersten
drei Elemente des Arrays. Nachdem jede Methode zurückgegeben wird, zeigt die DisplayArray -Methode an,
dass das Übergeben eines Arrays nach Wert keine Änderungen an den Arrayelementen verhindert.
using System;

class ArrayExample
{
static void DisplayArray(string[] arr) => Console.WriteLine(string.Join(" ", arr));

// Change the array by reversing its elements.


static void ChangeArray(string[] arr) => Array.Reverse(arr);

static void ChangeArrayElements(string[] arr)


{
// Change the value of the first three array elements.
arr[0] = "Mon";
arr[1] = "Wed";
arr[2] = "Fri";
}

static void Main()


{
// Declare and initialize an array.
string[] weekDays = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
// Display the array elements.
DisplayArray(weekDays);
Console.WriteLine();

// Reverse the array.


ChangeArray(weekDays);
// Display the array again to verify that it stays reversed.
Console.WriteLine("Array weekDays after the call to ChangeArray:");
DisplayArray(weekDays);
Console.WriteLine();

// Assign new values to individual array elements.


ChangeArrayElements(weekDays);
// Display the array again to verify that it has changed.
Console.WriteLine("Array weekDays after the call to ChangeArrayElements:");
DisplayArray(weekDays);
}
}
// The example displays the following output:
// Sun Mon Tue Wed Thu Fri Sat
//
// Array weekDays after the call to ChangeArray:
// Sat Fri Thu Wed Tue Mon Sun
//
// Array weekDays after the call to ChangeArrayElements:
// Mon Wed Fri Wed Tue Mon Sun

Übergeben von mehrdimensionalen Arrays als Argumente


Sie können ein initialisiertes mehrdimensionales Array genauso wie Sie ein eindimensionales Array an eine
Methode übergeben.

int[,] theArray = { { 1, 2 }, { 2, 3 }, { 3, 4 } };
Print2DArray(theArray);

Der folgende Code zeigt eine partielle Deklaration einer print-Methode, die ein zweidimensionales Array als
Argument akzeptiert.
void Print2DArray(int[,] arr)
{
// Method code.
}

Wie im folgenden Beispiel dargestellt können Sie ein Array in einem Schritt initialisieren und übergeben.

Print2DArray(new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } });

Beispiel
Im folgenden Beispiel wird ein zweidimensionales Array von ganzen Zahlen initialisiert und an die Print2DArray
-Methode übergeben. Die Methode zeigt die Elemente des Arrays an.

class ArrayClass2D
{
static void Print2DArray(int[,] arr)
{
// Display the array elements.
for (int i = 0; i < arr.GetLength(0); i++)
{
for (int j = 0; j < arr.GetLength(1); j++)
{
System.Console.WriteLine("Element({0},{1})={2}", i, j, arr[i, j]);
}
}
}
static void Main()
{
// Pass the array as an argument.
Print2DArray(new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } });

// Keep the console window open in debug mode.


System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/* Output:
Element(0,0)=1
Element(0,1)=2
Element(1,0)=3
Element(1,1)=4
Element(2,0)=5
Element(2,1)=6
Element(3,0)=7
Element(3,1)=8
*/

Siehe auch
C#-Programmierhandbuch
Arrays
Eindimensionale Arrays
Mehrdimensionale Arrays
Verzweigte Arrays
Implizit typisierte Arrays (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Sie können ein implizit typisiertes Array erstellen, in dem der Typ der Arrayinstanz von den im Arrayinitialisierer
angegebenen Elementen abgeleitet wird. Die Regeln für implizit typisierte Variablen gelten auch für implizit
typisierte Arrays. Weitere Informationen zu finden Sie unter Implizit typisierte lokale Variablen.
Implizit typisierte Arrays werden in der Regel in Abfrageausdrücken zusammen mit anonymen Typen sowie
Objekt- und Auflistungsinitialisierern verwendet.
Die folgenden Beispiele zeigen, wie ein implizit typisiertes Array erstellt wird:

class ImplicitlyTypedArraySample
{
static void Main()
{
var a = new[] { 1, 10, 100, 1000 }; // int[]
var b = new[] { "hello", null, "world" }; // string[]

// single-dimension jagged array


var c = new[]
{
new[]{1,2,3,4},
new[]{5,6,7,8}
};

// jagged array of strings


var d = new[]
{
new[]{"Luca", "Mads", "Luke", "Dinesh"},
new[]{"Karen", "Suma", "Frances"}
};
}
}

Beachten Sie im vorherigen Beispiel, dass bei implizit typisierten Arrays auf der linken Seite der
Initialisierungsanweisung keine eckigen Klammern verwendet werden. Beachten Sie auch, dass verzweigte
Arrays ebenso wie eindimensionale Arrays mithilfe von new [] initialisiert werden.

Implizit typisierte Arrays in Objektinitialisierern


Beim Erstellen eines anonymen Typs, der ein Array enthält, muss das Array im Objektinitialisierer des Typs
implizit typisiert werden. Im folgenden Beispiel ist contacts ein implizit typisiertes Array aus anonymen Typen,
von denen jeder ein Array mit dem Namen PhoneNumbers enthält. Beachten Sie, dass das Schlüsselwort var
nicht in den Objektinitialisierern verwendet wird.
var contacts = new[]
{
new {
Name = " Eugene Zabokritski",
PhoneNumbers = new[] { "206-555-0108", "425-555-0001" }
},
new {
Name = " Hanying Feng",
PhoneNumbers = new[] { "650-555-0199" }
}
};

Siehe auch
C#-Programmierhandbuch
Implizit typisierte lokale Variablen
Arrays
Anonyme Typen
Objekt- und Auflistungsinitialisierer
var
LINQ in C#
Zeichenfolgen (C#-Programmierhandbuch)
04.11.2021 • 12 minutes to read

Eine Zeichenfolge ist ein Objekt des Typs String, dessen Wert Text ist. Intern wird der Text als sequenzielle
schreibgeschützte Auflistung von Char-Objekten gespeichert. Es gibt kein mit NULL endendes Zeichen am Ende
einer C#-Zeichenfolge. Deshalb kann eine C#-Zeichenfolge eine beliebige Anzahl eingebetteter NULL-Zeichen
(„\0“) enthalten. Die Eigenschaft Length einer Zeichenfolge stellt die Anzahl von Char -Objekten dar, die darin
enthalten sind, nicht die Anzahl der Unicode-Zeichen. Verwenden Sie für den Zugriff auf einzelne Unicode-
Codepunkte in einer Zeichenfolge das Objekt StringInfo.

String im Vergleich zu System.String


In C# ist das Schlüsselwort string ein Alias für String. Daher sind String und string gleichwertig, es wird
jedoch weiterhin empfohlen, den bereitgestellten Alias string zu verwenden, da dieser auch ohne
using System; funktioniert. Die String -Klasse bietet viele Methoden zum sicheren Erstellen, Bearbeiten und
Vergleichen von Zeichenfolgen. Außerdem überlädt die Programmiersprache C# einige Operatoren, um
allgemeine Zeichenfolgenoperationen zu vereinfachen. Weitere Informationen über das Schlüsselwort finden
Sie unter String. Weitere Informationen zum Typ und dessen Methoden finden Sie unter String.

Deklarieren und Initialisieren von Zeichenfolgen


Sie können Zeichenfolgen auf verschiedene Weise deklarieren und Initialisieren, wie im folgenden Beispiel
gezeigt:
// Declare without initializing.
string message1;

// Initialize to null.
string message2 = null;

// Initialize as an empty string.


// Use the Empty constant instead of the literal "".
string message3 = System.String.Empty;

// Initialize with a regular string literal.


string oldPath = "c:\\Program Files\\Microsoft Visual Studio 8.0";

// Initialize with a verbatim string literal.


string newPath = @"c:\Program Files\Microsoft Visual Studio 9.0";

// Use System.String if you prefer.


System.String greeting = "Hello World!";

// In local variables (i.e. within a method body)


// you can use implicit typing.
var temp = "I'm still a strongly-typed System.String!";

// Use a const string to prevent 'message4' from


// being used to store another string value.
const string message4 = "You can't get rid of me!";

// Use the String constructor only when creating


// a string from a char*, char[], or sbyte*. See
// System.String documentation for details.
char[] letters = { 'A', 'B', 'C' };
string alphabet = new string(letters);

Beachten Sie, dass Sie nicht den neuen Operator zum Erstellen eines Zeichenfolgenobjekts verwenden, außer
wenn Sie die Zeichenfolge mit einem Array von Chars initialisieren.
Initialisieren Sie eine Zeichenfolge mit dem konstanten Wert Empty, um ein neues String-Objekt zu erstellen,
dessen Zeichenfolge eine Länge von 0 hat. Die Darstellung des Zeichenfolgenliterals einer Zeichenfolge mit
einer Länge von 0 ist "". Indem Zeichenfolgen mit dem Wert Empty anstatt NULL initialisiert werden, können Sie
die Chancen einer auftretenden NullReferenceException reduzieren. Verwenden Sie die statische Methode
IsNullOrEmpty(String), um den Wert einer Zeichenfolge zu überprüfen, bevor Sie versuchen, auf sie zuzugreifen.

Unveränderlichkeit von Zeichenfolgenobjekten


Zeichenfolgenobjekte sind unveränderlich: sie können nicht geändert werden, nachdem sie erstellt wurden. Alle
String-Methoden und C#-Operatoren, die eine Zeichenfolge scheinbar verändern, geben in Wirklichkeit die
Ergebnisse in einem neuen Zeichenfolgenobjekt zurück. Im folgenden Beispiel werden die beiden
ursprünglichen Zeichenfolgen nicht geändert, wenn die Inhalte von s1 und s2 verkettet werden, um eine
einzelne Zeichenfolge zu bilden. Der += -Operator erstellt eine neue Zeichenfolge, die die kombinierten Inhalte
enthält. Das neue Objekt wird der Variablen s1 zugewiesen, und das ursprüngliche Objekt, das s1 zugewiesen
wurde, wird für die Garbage Collection freigegeben, da keine andere Variable einen Verweis darauf enthält.
string s1 = "A string is more ";
string s2 = "than the sum of its chars.";

// Concatenate s1 and s2. This actually creates a new


// string object and stores it in s1, releasing the
// reference to the original object.
s1 += s2;

System.Console.WriteLine(s1);
// Output: A string is more than the sum of its chars.

Da eine „Zeichenfolgenänderung“ in Wirklichkeit eine neue Erstellung von Zeichenfolgen ist, müssen Sie
vorsichtig sein, wenn Sie Verweise auf Zeichenfolgen erstellen. Wenn Sie einen Verweis auf eine Zeichenfolge
erstellen und dann die ursprüngliche Zeichenfolge „ändern“, wird der Verweis weiterhin auf das ursprüngliche
Objekt anstelle des neuen Objekts zeigen, das erstellt wurde, als die Zeichenfolge geändert wurde. Der folgende
Code veranschaulicht dieses Verhalten:

string s1 = "Hello ";


string s2 = s1;
s1 += "World";

System.Console.WriteLine(s2);
//Output: Hello

Weitere Informationen zum Erstellen neuer Zeichenfolgen, die auf Änderungen wie beispielsweise Vorgängen
zum Suchen und Ersetzen auf der ursprüngliche Zeichenfolge basieren, finden Sie unter Ändern von
Zeichenfolgeninhalten.

Reguläre und ausführliche Zeichenfolgenliterale


Verwenden Sie reguläre Zeichenfolgenliterale, wenn Sie von C# bereitgestellte Escapezeichen einbetten müssen,
wie im folgenden Beispiel gezeigt:

string columns = "Column 1\tColumn 2\tColumn 3";


//Output: Column 1 Column 2 Column 3

string rows = "Row 1\r\nRow 2\r\nRow 3";


/* Output:
Row 1
Row 2
Row 3
*/

string title = "\"The \u00C6olean Harp\", by Samuel Taylor Coleridge";


//Output: "The Æolean Harp", by Samuel Taylor Coleridge

Verwenden Sie ausführliche Zeichenfolgen der Einfachheit und Lesbarkeit halber, wenn der Text der
Zeichenfolge umgekehrte Schrägstriche enthält, z.B. in Dateipfaden. Da ausführliche Zeichenfolgen neue
Zeilenzeichen als Teil des Texts der Zeichenfolge beibehalten, können sie verwendet werden, um mehrzeilige
Zeichenfolgen zu initialisieren. Verwenden Sie doppelte Anführungszeichen, um ein Anführungszeichen in eine
ausführliche Zeichenfolge einzubetten. Im folgenden Beispiel werden einige gängige Verwendungszwecke für
allgemeine Zeichenfolgen dargestellt:
string filePath = @"C:\Users\scoleridge\Documents\";
//Output: C:\Users\scoleridge\Documents\

string text = @"My pensive SARA ! thy soft cheek reclined


Thus on mine arm, most soothing sweet it is
To sit beside our Cot,...";
/* Output:
My pensive SARA ! thy soft cheek reclined
Thus on mine arm, most soothing sweet it is
To sit beside our Cot,...
*/

string quote = @"Her name was ""Sara.""";


//Output: Her name was "Sara."

Zeichenfolgen-Escapesequenzen
ESC A P ESEQ UEN Z Z EIC H EN N A M E UN IC O DE- C O DIERUN G

\' Einfaches Anführungszeichen 0x0027

\" Doppeltes Anführungszeichen 0x0022

\\ Umgekehrter Schrägstrich 0x005C

\0 Null 0x0000

\a Warnung 0x0007

\b Rückschritt 0x0008

\f Seitenvorschub 0x000C

\n Zeilenwechsel 0x000A

\r Wagenrücklauf 0x000D

\t Horizontaler Tabulator 0x0009

\v Vertikaler Tabulator 0x000B

\u Unicode-Escapesequenz (UTF-16) \uHHHH (Bereich: 0000 - FFFF;


Beispiel: \u00E7 = "ç")

\U Unicode-Escapesequenz (UTF-32) \U00HHHHHH (Bereich: 000000 -


10FFFF; Beispiel: \U0001F47D = " ")

\x Unicode-Escapesequenz, die ähnlich \xH[H][H][H] (Bereich: 0 - FFFF;


wie "\u" ist, außer mit variabler Länge Beispiel: \x00E7 or \x0E7 oder
\xE7 = "ç")
WARNING
Wenn Sie die Escapesequenz \x verwenden, weniger als vier Hexadezimalziffern angeben und es sich bei den Zeichen,
die der Escapesequenz unmittelbar folgen, um gültige Hexadezimalziffern handelt (z. B. 0–9, A–F und a–f), werden diese als
Teil der Escapesequenz interpretiert. Die Angabe \xA1 erzeugt beispielsweise den Wert "¡", der dem Codepunkt U+00A1
entspricht. Wenn das nächste Zeichen jedoch „A“ oder „a“ ist, wird die Escapesequenz stattdessen als \xA1A interpretiert
und erzeugt den Wert " ", der dem Codepunkt U+0A1A entspricht. In solchen Fällen können Fehlinterpretationen
vermieden werden, indem Sie alle vier Hexadezimalziffern (z. B. \x00A1 ) angeben.

NOTE
Zum Zeitpunkt der Kompilierung werden ausführliche Zeichenfolgen in normale Zeichenfolgen mit gleichen
Escapesequenzen konvertiert. Aus diesem Grund sehen Sie die Escapezeichen, die vom Compiler hinzugefügt werden, und
nicht die ausführliche Version aus Ihrem Sourcecode, wenn Sie eine ausführliche Zeichenfolge in Debugger-
Überwachungsfenster anzeigen. Die ausführliche Zeichenfolge @"C:\files.txt" wird im Überwachungsfenster z.B. als
„C:\\files.txt“ angezeigt.

Formatzeichenfolgen
Eine Formatzeichenfolge ist eine Zeichenfolge, deren Inhalt zur Laufzeit dynamisch bestimmt wird.
Formatzeichenfolgen werden erstellt, indem interpolierte Ausdrücke oder Platzhalter in geschweifte Klammern
innerhalb einer Zeichenfolge eingebettet werden. Der in den Klammern ( {...} ) eingeschlossene Inhalt wird in
einen Wert aufgelöst und zur Laufzeit als formatierte Zeichenfolge ausgegeben. Es gibt zwei Methoden zum
Erstellen von Formatzeichenfolgen: Zeichenfolgeninterpolation und kombinierte Formatierung.
Zeichenfolgeninterpolierung
In C# 6.0 und höheren Versionen werden interpolierte Zeichenfolgen durch das Sonderzeichen $ identifiziert
und interpolierte Ausdrücke in geschweifte Klammern eingeschlossen. Wenn Sie noch nicht mit der
Zeichenfolgeninterpolation vertraut sind, erhalten Sie im interaktiven Tutorial Zeichenfolgeninterpolation in C#
eine kurze Übersicht.
Verwenden Sie die Zeichenfolgeninterpolation, um die Lesbarkeit und die Wartungsfähigkeit Ihres Codes zu
verbessern. Mit der Zeichenfolgeninterpolation werden die gleichen Ergebnisse erzielt wie mit der
String.Format -Methode, es wird jedoch die Benutzerfreundlichkeit und Übersichtlichkeit verbessert.

var jh = (firstName: "Jupiter", lastName: "Hammon", born: 1711, published: 1761);


Console.WriteLine($"{jh.firstName} {jh.lastName} was an African American poet born in {jh.born}.");
Console.WriteLine($"He was first published in {jh.published} at the age of {jh.published - jh.born}.");
Console.WriteLine($"He'd be over {Math.Round((2018d - jh.born) / 100d) * 100d} years old today.");

// Output:
// Jupiter Hammon was an African American poet born in 1711.
// He was first published in 1761 at the age of 50.
// He'd be over 300 years old today.

Ab C# 10 können Sie die Zeichenfolgeninterpolation verwenden, um eine konstante Zeichenfolge zu


initialisieren, wenn alle für Platzhalter verwendeten Ausdrücke auch konstante Zeichenfolgen sind.
Kombinierte Formatierung
String.Format verwendet Platzhalter in geschweiften Klammern, um eine Formatzeichenfolge zu erstellen.
Dieses Beispiel liefert als Ergebnis eine ähnliche Ausgabe wie die oben verwendete Methode zur
Zeichenfolgeninterpolation.
var pw = (firstName: "Phillis", lastName: "Wheatley", born: 1753, published: 1773);
Console.WriteLine("{0} {1} was an African American poet born in {2}.", pw.firstName, pw.lastName, pw.born);
Console.WriteLine("She was first published in {0} at the age of {1}.", pw.published, pw.published -
pw.born);
Console.WriteLine("She'd be over {0} years old today.", Math.Round((2018d - pw.born) / 100d) * 100d);

// Output:
// Phillis Wheatley was an African American poet born in 1753.
// She was first published in 1773 at the age of 20.
// She'd be over 300 years old today.

Weitere Informationen zum Formatieren von .NET-Typen finden Sie unter Formatieren von Typen in .NET.

Teilzeichenfolgen
Eine Teilzeichenfolge ist eine beliebige Sequenz von Zeichen, die in einer Zeichenfolge enthalten ist. Verwenden
Sie die Substring-Methode, um eine neue Zeichenfolge aus einem Teil der ursprünglichen Zeichenfolge zu
erstellen. Sie können nach einem oder mehreren Vorkommnissen einer Teilzeichenfolge suchen, indem Sie die
IndexOf-Methode verwenden. Verwenden Sie die Replace-Methode, um alle Vorkommnisse einer angegebenen
Teilzeichenfolge mit einer neuen Zeichenfolge zu ersetzen. Genauso wie die Substring-Methode gibt Replace
tatsächlich eine neue Zeichenfolge zurück und ändert nicht die ursprüngliche Zeichenfolge. Weitere
Informationen finden Sie unter Suchen von Zeichenfolgen mithilfe von Zeichenfolgenmethoden und Ändern
von Zeichenfolgeninhalten.

string s3 = "Visual C# Express";


System.Console.WriteLine(s3.Substring(7, 2));
// Output: "C#"

System.Console.WriteLine(s3.Replace("C#", "Basic"));
// Output: "Visual Basic Express"

// Index values are zero-based


int index = s3.IndexOf("C");
// index = 7

Zugreifen auf einzelne Zeichen


Sie können die Arraynotation mit einem Indexwert verwenden, um schreibgeschützten Zugriff auf einzelne
Zeichen zu erhalten, so wie in folgendem Beispiel gezeigt:

string s5 = "Printing backwards";

for (int i = 0; i < s5.Length; i++)


{
System.Console.Write(s5[s5.Length - i - 1]);
}
// Output: "sdrawkcab gnitnirP"

Wenn die String-Methoden nicht die Funktionen bieten, die Sie zum Bearbeiten einzelner Zeichen in einer
Zeichenfolge benötigen, können Sie ein StringBuilder-Objekt verwenden, um die einzelnen Chars „direkt“ zu
bearbeiten und anschließend eine neue Zeichenfolge zu StringBuilder-Methoden zu speichern. Im folgenden
Beispiel wird davon ausgegangen, dass Sie die ursprüngliche Zeichenfolge auf eine bestimmte Weise ändern
und die Ergebnisse für die zukünftige Verwendung speichern müssen:
string question = "hOW DOES mICROSOFT wORD DEAL WITH THE cAPS lOCK KEY?";
System.Text.StringBuilder sb = new System.Text.StringBuilder(question);

for (int j = 0; j < sb.Length; j++)


{
if (System.Char.IsLower(sb[j]) == true)
sb[j] = System.Char.ToUpper(sb[j]);
else if (System.Char.IsUpper(sb[j]) == true)
sb[j] = System.Char.ToLower(sb[j]);
}
// Store the new string.
string corrected = sb.ToString();
System.Console.WriteLine(corrected);
// Output: How does Microsoft Word deal with the Caps Lock key?

NULL-Zeichenfolgen und leere Zeichenfolgen


Eine leere Zeichenfolge ist eine Instanz eines System.String-Objekts, dass 0 Zeichen enthält. Leere Zeichenfolgen
werden häufig in verschiedenen Programmierszenarios verwendet, um ein leeres Textfeld darzustellen. Sie
können Methoden für leere Zeichenfolgen aufrufen, da sie gültige System.String-Objekte sind. Leere
Zeichenfolgen werden wie folgt initialisiert:

string s = String.Empty;

Im Gegensatz dazu, verweist eine NULL-Zeichenfolge nicht auf eine Instanz eines System.String-Objekts und
jeder Versuch, eine Methode für eine NULL-Zeichenfolge aufzurufen, löst eine NullReferenceException aus.
Allerdings können Sie NULL-Zeichenfolgen in Verkettungs- und Vergleichsoperationen mit anderen
Zeichenfolgen verwenden. Die folgenden Beispiele veranschaulichen einige Fälle, in denen ein Verweis auf eine
NULL-Zeichenfolge nicht dazu führt, dass eine Ausnahme ausgelöst wird:
static void Main()
{
string str = "hello";
string nullStr = null;
string emptyStr = String.Empty;

string tempStr = str + nullStr;


// Output of the following line: hello
Console.WriteLine(tempStr);

bool b = (emptyStr == nullStr);


// Output of the following line: False
Console.WriteLine(b);

// The following line creates a new empty string.


string newStr = emptyStr + nullStr;

// Null strings and empty strings behave differently. The following


// two lines display 0.
Console.WriteLine(emptyStr.Length);
Console.WriteLine(newStr.Length);
// The following line raises a NullReferenceException.
//Console.WriteLine(nullStr.Length);

// The null character can be displayed and counted, like other chars.
string s1 = "\x0" + "abc";
string s2 = "abc" + "\x0";
// Output of the following line: * abc*
Console.WriteLine("*" + s1 + "*");
// Output of the following line: *abc *
Console.WriteLine("*" + s2 + "*");
// Output of the following line: 4
Console.WriteLine(s2.Length);
}

Verwenden von StringBuilder für die schnelle Erstellung von


Zeichenfolgen
Zeichenfolgenoperationen in .NET werden hochgradig optimiert und wirken sich in den meisten Fällen nicht
erheblich auf die Leistung aus. In einigen Szenarios, z.B. in engen Schleifen, die Hunderttausende Male
ausgeführt werden, können sich Zeichenfolgenoperationen auf die Leistung auswirken. Die StringBuilder-Klasse
erstellt einen Zeichenfolgenpuffer, der eine verbesserte Leistung mit sich bringt, wenn Ihr Programm viele
Zeichenfolgenbearbeitungen durchführt. Mit der StringBuilder-Zeichenfolge können Sie auch einzelne Zeichen
erneut zuweisen, was der integrierte String-Datentyp nicht unterstützt. Dieser Code ändert z.B. den Inhalt einer
Zeichenfolge ohne eine neue Zeichenfolge zu erstellen:

System.Text.StringBuilder sb = new System.Text.StringBuilder("Rat: the ideal pet");


sb[0] = 'C';
System.Console.WriteLine(sb.ToString());
System.Console.ReadLine();

//Outputs Cat: the ideal pet

In diesem Beispiel wird ein StringBuilder-Objekt zum Erstellen einer Zeichenfolge aus einem Satz von
numerischen Typen verwendet:
using System;
using System.Text;

namespace CSRefStrings
{
class TestStringBuilder
{
static void Main()
{
var sb = new StringBuilder();

// Create a string composed of numbers 0 - 9


for (int i = 0; i < 10; i++)
{
sb.Append(i.ToString());
}
Console.WriteLine(sb); // displays 0123456789

// Copy one character of the string (not possible with a System.String)


sb[0] = sb[9];

Console.WriteLine(sb); // displays 9123456789


Console.WriteLine();
}
}
}

Zeichenfolgen, Erweiterungsmethoden und LINQ


Da der String-Typ IEnumerable<T> implementiert, können Sie die Erweiterungsmethode verwenden, die in der
Enumerable-Klasse auf Zeichenfolgen definiert ist. Um „visuelle Überfrachtung“ zu vermeiden, werden diese
Methode für den String-Typ aus IntelliSense ausgeschlossen, nichtsdestotrotz sind sie weiterhin verfügbar. Sie
können auch LINQ-Abfrageausdrücke in Zeichenfolgen verwenden. Weitere Informationen finden Sie unter
LINQ und Zeichenfolgen.

Verwandte Themen
T H EM A B ESC H REIB UN G

Ändern von Zeichenfolgeninhalten Veranschaulicht Methoden zum Transformieren von


Zeichenfolgen und Modifizieren von Zeichenfolgeninhalten.

Vergleichen von Zeichenfolgen So führen Sie ordinale und kulturspezifische


Zeichenfolgenvergleiche durch.

Verketten mehrerer Zeichenfolgen Veranschaulicht verschiedene Möglichkeiten, mehrere


Zeichenfolgen zu einer einzigen zu verknüpfen.

Analysieren von Zeichenfolgen mithilfe von String.Split Enthält Codebeispiele, die veranschaulichen, wie Sie die
String.Split -Methode zum Analysieren von
Zeichenfolgen verwenden.

Suchen von Zeichenfolgen Erläutert, wie Sie mit der Suche Zeichenfolgen nach
spezifischem Text oder spezifischen Mustern durchsuchen
können.
T H EM A B ESC H REIB UN G

Bestimmen, ob eine Zeichenfolge einen numerischen Wert Zeigt, wie Sie sicher eine Zeichenfolge analysieren, um zu
darstellt sehen, ob diese über einen gültigen numerischen Wert
verfügt

Zeichenfolgeninterpolation Beschreibt die Funktion zur Zeichenfolgeninterpolation, die


eine zweckmäßige Syntax zum Formatieren von
Zeichenfolgen bietet.

Grundlegende Zeichenfolgenoperationen Stellt Links zu Themen bereit, die System.String- und


System.Text.StringBuilder-Methoden verwenden, um
grundlegende Zeichenfolgenoperationen durchzuführen

Analysieren von Zeichenfolgen in .NET Beschreibt das Konvertieren von Zeichenfolgendarstellungen


der .NET-Basistypen in Instanzen der entsprechenden Typen.

Analysieren von Zeichenfolgen für Datum und Uhrzeit in Zeigt, wie eine Zeichenfolge wie "01/24/2008" in ein
.NET System.DateTime-Objekt konvertiert wird

Vergleichen von Zeichenfolgen Enthält Informationen, wie Zeichenfolgen verglichen werden,


und gibt Beispiele in C# und Visual Basic

Verwenden der StringBuilder-Klasse Beschreibt das Erstellen und Ändern dynamischer


Zeichenfolgenobjekte mithilfe der StringBuilder-Klasse

LINQ und Zeichenfolgen Enthält Informationen zum Ausführen verschiedener


Zeichenfolgenoperationen mithilfe von LINQ-Abfragen

C#-Programmierhandbuch Enthält Links zu Themen, in denen die Konstrukte der


Programmierung in C# beschrieben werden
Vorgehensweise: Bestimmen, ob eine Zeichenfolge
einen numerischen Wert darstellt (C#-
Programmierleitfaden)
04.11.2021 • 2 minutes to read

Verwenden Sie die statische TryParse -Methode, die von allen primitiven numerischen Typen sowie von Typen
wie z.B. DateTime und IPAddress implementiert wird, um zu bestimmen, ob eine Zeichenfolge eine gültige
Darstellung eines angegebenen numerischen Typs ist. In folgendem Beispiel wird gezeigt, wie Sie bestimmen
können, ob „108“ eine zulässige ganze Zahl ist.

int i = 0;
string s = "108";
bool result = int.TryParse(s, out i); //i now = 108

Wenn die Zeichenfolge nicht numerische Zeichen enthält oder der numerische Wert für den Typ, den Sie
angegeben haben, zu groß oder zu klein ist, gibt TryParse „FALSE“ zurück und legt den Parameter auf 0 (null)
fest. Andernfalls wird „TRUE“ zurückgegeben und der Parameter auf den numerischen Wert der Zeichenfolge
festgelegt.

NOTE
Eine Zeichenfolge kann auch nur numerische Zeichen enthalten und trotzdem für den Typ unzulässig sein, dessen
TryParse -Methode Sie verwenden. Beispielsweise ist „256“ kein zulässiger Wert für byte , aber zulässig für int .
„98,6“ ist kein zulässiger Wert für int , aber zulässig für decimal .

Beispiel
In folgendem Beispiel wird gezeigt, wie Sie TryParse mit einer Zeichenfolgenrepräsentation von den Werten
long , byte und decimal verwenden können.
string numString = "1287543"; //"1287543.0" will return false for a long
long number1 = 0;
bool canConvert = long.TryParse(numString, out number1);
if (canConvert == true)
Console.WriteLine("number1 now = {0}", number1);
else
Console.WriteLine("numString is not a valid long");

byte number2 = 0;
numString = "255"; // A value of 256 will return false
canConvert = byte.TryParse(numString, out number2);
if (canConvert == true)
Console.WriteLine("number2 now = {0}", number2);
else
Console.WriteLine("numString is not a valid byte");

decimal number3 = 0;
numString = "27.3"; //"27" is also a valid decimal
canConvert = decimal.TryParse(numString, out number3);
if (canConvert == true)
Console.WriteLine("number3 now = {0}", number3);
else
Console.WriteLine("number3 is not a valid decimal");

Stabile Programmierung
Primitive numerische Typen implementieren auch die statische Parse -Methode, die eine Ausnahme auslöst,
wenn die Zeichenfolge keine zulässige Zahl ist. TryParse ist für gewöhnlich effizienter, da es einfach „FALSE“
zurückgibt, wenn die Zahl unzulässig ist.

.NET-Sicherheit
Verwenden Sie immer die Methoden TryParse oder Parse , um Benutzereingaben von Steuerelementen wie
Text- oder Kombinationsfeldern zu überprüfen.

Siehe auch
Konvertieren eines Bytearrays in einen Typ „int“
Konvertieren einer Zeichenfolge in eine Zahl
Konvertieren zwischen Hexadezimalzeichenfolgen und numerischen Typen
Verarbeiten numerischer Zeichenfolgen
Formatierung von Typen
Indexer (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Indexer ermöglichen, dass Instanzen einer Klasse oder Struktur wie Arrays indiziert werden. Der indizierte Wert
kann festgelegt oder ohne explizite Angabe eines Typs oder Instanzmembers abgerufen werden. Indexer ähneln
Eigenschaften. Der Unterschied besteht jedoch darin, dass ihre Zugriffsmethoden Parameter verwenden.
Im folgenden Beispiel wird eine generische Klasse mit einfachen get- und set-Accessormethoden zum Zuweisen
und Abrufen von Werten definiert. Die Program -Klasse erstellt eine Instanz dieser Klasse für das Speichern von
Zeichenfolgen.

using System;

class SampleCollection<T>
{
// Declare an array to store the data elements.
private T[] arr = new T[100];

// Define the indexer to allow client code to use [] notation.


public T this[int i]
{
get { return arr[i]; }
set { arr[i] = value; }
}
}

class Program
{
static void Main()
{
var stringCollection = new SampleCollection<string>();
stringCollection[0] = "Hello, World";
Console.WriteLine(stringCollection[0]);
}
}
// The example displays the following output:
// Hello, World.

NOTE
Weitere Beispiele finden Sie unter Verwandte Abschnitte.

Ausdruckstextdefinitionen
Get- oder Set-Accessoren eines Indexers bestehen häufig aus einer einzelnen Anweisung, die einen Wert
zurückgibt oder festlegt. Ausdruckskörpermember bieten eine vereinfachte Syntax zur Unterstützung dieses
Szenarios. Ab C# 6 kann ein schreibgeschützter Indexer als Ausdruckskörpermember implementiert werden, wie
im folgenden Beispiel gezeigt.
using System;

class SampleCollection<T>
{
// Declare an array to store the data elements.
private T[] arr = new T[100];
int nextIndex = 0;

// Define the indexer to allow client code to use [] notation.


public T this[int i] => arr[i];

public void Add(T value)


{
if (nextIndex >= arr.Length)
throw new IndexOutOfRangeException($"The collection can hold only {arr.Length} elements.");
arr[nextIndex++] = value;
}
}

class Program
{
static void Main()
{
var stringCollection = new SampleCollection<string>();
stringCollection.Add("Hello, World");
System.Console.WriteLine(stringCollection[0]);
}
}
// The example displays the following output:
// Hello, World.

Beachten Sie, dass => den Ausdruckstext vorstellt und dass das get -Schlüsselwort nicht verwendet wird.
Ab C# 7.0 können die Get- und die Set-Zugriffsmethode als Ausdruckskörpermember implementiert werden. In
diesem Fall müssen die Schlüsselwörter get und set verwendet werden. Zum Beispiel:

using System;

class SampleCollection<T>
{
// Declare an array to store the data elements.
private T[] arr = new T[100];

// Define the indexer to allow client code to use [] notation.


public T this[int i]
{
get => arr[i];
set => arr[i] = value;
}
}

class Program
{
static void Main()
{
var stringCollection = new SampleCollection<string>();
stringCollection[0] = "Hello, World.";
Console.WriteLine(stringCollection[0]);
}
}
// The example displays the following output:
// Hello, World.
Übersicht über Indexer
Indexer ermöglichen es Objekten, in ähnlicher Weise wie Arrays indiziert zu werden.
Ein get -Accessor gibt einen Wert zurück. Ein set -Accessor weist einen Wert zu.
Das this-Schlüsselwort wird zum Definieren des Indexers verwendet.
Das value-Schlüsselwort wird verwendet, um den Wert zu definieren, der vom set -Accessor
zugewiesen wird.
Indexer müssen nicht durch einen Ganzzahlwert indiziert werden. Sie können entscheiden, wie Sie den
spezifischen Suchmechanismus definieren möchten.
Indexer können überladen werden.
Indexer können mehr als einen formalen Parameter aufweisen, beispielsweise beim Zugreifen auf ein 2D-
Array.

Verwandte Abschnitte
Verwenden von Indexern
Indexer in Schnittstellen
Vergleich zwischen Eigenschaften und Indexern
Einschränken des Zugriffsmethodenzugriffs

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter Indexer in der C#-Sprachspezifikation. Die Sprachspezifikation ist die
verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Programmierhandbuch
Eigenschaften
Verwenden von Indexern (C#-
Programmierhandbuch)
04.11.2021 • 6 minutes to read

Indexer sind ein syntaktisches Hilfsmittel, um Ihnen die Erstellung einer Klasse, Struktur oder Schnittstelle zu
ermöglichen, auf die Clientanwendungen als Array zugreifen können. Der Compiler generiert die Eigenschaft
Item (oder eine anders benannte Eigenschaft, wenn IndexerNameAttribute vorhanden ist) und die
entsprechenden Zugriffsmethoden. Indexer werden am häufigsten in Typen implementiert, deren Hauptzweck
darin besteht, eine interne Auflistung oder ein Array zu kapseln. Angenommen, Sie verfügen beispielsweise über
eine Klasse namens TempRecord zur Darstellung der Temperatur in Fahrenheit zu 10 verschiedenen Zeitpunkten
während eines 24-stündigen Zeitraums. Die Klasse enthält das Array temps vom Typ float[] zum Speichern
der Temperaturwerte. Durch die Implementierung eines Indexers in dieser Klasse können Clients auf die
Temperaturen in einer TempRecord -Instanz als float temp = tempRecord[4] zugreifen, statt als
float temp = tempRecord.temps[4] . Die Indexernotation vereinfacht nicht nur die Syntax für Clientanwendungen.
Sie ermöglicht es auch anderen Entwicklern, die Klasse und deren Zweck intuitiver zu verstehen.
Um einen Indexer in einer Klasse oder Struktur zu deklarieren, verwenden Sie das this -Schlüsselwort, wie im
folgenden Beispiel gezeigt:

// Indexer declaration
public int this[int index]
{
// get and set accessors
}

IMPORTANT
Beim Deklarieren eines Indexers wird automatisch eine Eigenschaft mit dem Namen Item für das Objekt generiert. Auf
die Eigenschaft Item kann nicht direkt über den Memberzugriffsausdruck der Instanz zugegriffen werden. Wenn Sie Ihre
eigene Eigenschaft Item zu einem Objekt mit einem Indexer hinzufügen, wird außerdem der Compilerfehler CS0102
angezeigt. Verwenden Sie IndexerNameAttribute, und benennen Sie den Indexer wie unten beschrieben um, um diesen
Fehler zu vermeiden.

Hinweise
Der Typ eines Indexers und der Typ seiner Parameter müssen zumindest dieselben Zugriffsmöglichkeiten bieten
wie der Indexer selbst. Weitere Informationen zu den Zugriffsebenen finden Sie unter Zugriffsmodifizierer.
Weitere Informationen zum Verwenden von Indexern mit einer Schnittstelle finden Sie unter
Schnittstellenindexer.
Die Signatur eines Indexers besteht aus der Anzahl und den Typen seiner formalen Parameter. Sie umfasst weder
den Indexertyp noch die Namen der formalen Parameter. Wenn Sie mehrere Indexer in derselben Klasse
deklarieren, müssen sie verschiedene Signaturen aufweisen.
Ein Indexerwert wird nicht als Variable klassifiziert. Aus diesem Grund können Sie keinen Indexerwert als Ref-
oder out-Parameter übergeben.
Um für den Indexer einen Namen anzugeben, den andere Sprachen verwenden können, verwenden Sie
System.Runtime.CompilerServices.IndexerNameAttribute, wie im folgenden Beispiel gezeigt:
// Indexer declaration
[System.Runtime.CompilerServices.IndexerName("TheItem")]
public int this[int index]
{
// get and set accessors
}

Dieser Indexer hat den Namen TheItem , da er vom Indexernamensattribut überschrieben wird. Standardmäßig
ist der Indexername Item .

Beispiel 1
Im folgenden Beispiel wird die Deklaration eines öffentlichen Arrayfelds, temps , und eines Indexers dargestellt.
Der Indexer ermöglicht den direkten Zugriff auf die Instanz tempRecord[i] . Als Alternative zur Verwendung des
Indexers, kann das Array als öffentliches Mitglied deklariert und direkt auf dessen Mitglieder,
tempRecord.temps[i] , zugegriffen werden.

public class TempRecord


{
// Array of temperature values
float[] temps = new float[10]
{
56.2F, 56.7F, 56.5F, 56.9F, 58.8F,
61.3F, 65.9F, 62.1F, 59.2F, 57.5F
};

// To enable client code to validate input


// when accessing your indexer.
public int Length => temps.Length;

// Indexer declaration.
// If index is out of range, the temps array will throw the exception.
public float this[int index]
{
get => temps[index];
set => temps[index] = value;
}
}

Beachten Sie, dass, wenn der Zugriff eines Indexers, z.B. in einer Console.Write -Anweisung ausgewertet wird,
der get-Accessor aufgerufen wird. Wenn kein get -Accessor vorhanden ist, tritt deshalb ein Kompilierzeitfehler
auf.
using System;

class Program
{
static void Main()
{
var tempRecord = new TempRecord();

// Use the indexer's set accessor


tempRecord[3] = 58.3F;
tempRecord[5] = 60.1F;

// Use the indexer's get accessor


for (int i = 0; i < 10; i++)
{
Console.WriteLine($"Element #{i} = {tempRecord[i]}");
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
/* Output:
Element #0 = 56.2
Element #1 = 56.7
Element #2 = 56.5
Element #3 = 58.3
Element #4 = 58.8
Element #5 = 60.1
Element #6 = 65.9
Element #7 = 62.1
Element #8 = 59.2
Element #9 = 57.5
*/
}

Indizieren mit anderen Werten


C# beschränkt den Indexer-Parametertyp nicht auf Integer. Beispielsweise kann es sinnvoll sein, eine
Zeichenfolge mit einem Indexer zu verwenden. Ein solcher Indexer kann implementiert werden, indem die
Zeichenfolge in der Auflistung gesucht und der richtige Wert zurückgegeben wird. Da Zugriffsmethoden
überladen werden können, können die Zeichenfolgen- und die Integer-Version gleichzeitig vorhanden sein.

Beispiel 2
Das folgende Beispiel deklariert eine Klasse, die die Wochentage speichert. Ein get -Accessor akzeptiert eine
Zeichenfolge – den Namen eines Tages – und gibt den entsprechenden Integer-Wert zurück. „Sunday“ gibt
beispielsweise 0 zurück, „Monday“ 1 usw.
using System;

// Using a string as an indexer value


class DayCollection
{
string[] days = { "Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat" };

// Indexer with only a get accessor with the expression-bodied definition:


public int this[string day] => FindDayIndex(day);

private int FindDayIndex(string day)


{
for (int j = 0; j < days.Length; j++)
{
if (days[j] == day)
{
return j;
}
}

throw new ArgumentOutOfRangeException(


nameof(day),
$"Day {day} is not supported.\nDay input must be in the form \"Sun\", \"Mon\", etc");
}
}

Verwendungsbeispiel 2

using System;

class Program
{
static void Main(string[] args)
{
var week = new DayCollection();
Console.WriteLine(week["Fri"]);

try
{
Console.WriteLine(week["Made-up day"]);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine($"Not supported input: {e.Message}");
}
}
// Output:
// 5
// Not supported input: Day Made-up day is not supported.
// Day input must be in the form "Sun", "Mon", etc (Parameter 'day')
}

Beispiel 3
Im folgenden Beispiel wird eine Klasse deklariert, die die Wochentage mithilfe der Enumeration
System.DayOfWeek speichert. Eine get -Zugriffsmethode akzeptiert DayOfWeek (den Wert für einen Tag) und
gibt den entsprechenden Integer-Wert zurück. Bei DayOfWeek.Sunday wird beispielsweise 0 zurückgegeben, bei
DayOfWeek.Monday 1 und so weiter.
using System;
using Day = System.DayOfWeek;

class DayOfWeekCollection
{
Day[] days =
{
Day.Sunday, Day.Monday, Day.Tuesday, Day.Wednesday,
Day.Thursday, Day.Friday, Day.Saturday
};

// Indexer with only a get accessor with the expression-bodied definition:


public int this[Day day] => FindDayIndex(day);

private int FindDayIndex(Day day)


{
for (int j = 0; j < days.Length; j++)
{
if (days[j] == day)
{
return j;
}
}
throw new ArgumentOutOfRangeException(
nameof(day),
$"Day {day} is not supported.\nDay input must be a defined System.DayOfWeek value.");
}
}

Verwendungsbeispiel 3

using System;

class Program
{
static void Main()
{
var week = new DayOfWeekCollection();
Console.WriteLine(week[DayOfWeek.Friday]);

try
{
Console.WriteLine(week[(DayOfWeek)43]);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine($"Not supported input: {e.Message}");
}
}
// Output:
// 5
// Not supported input: Day 43 is not supported.
// Day input must be a defined System.DayOfWeek value. (Parameter 'day')
}

Stabile Programmierung
Es gibt zwei grundlegende Möglichkeiten, mit denen die Sicherheit und die Zuverlässigkeit von Indexern
verbessert werden kann:
Integrieren Sie irgendeine Form von Fehlerbehandlungsstrategie, für den Fall, dass Clientcode in einem
ungültigen Indexwert übergeben wird. Im ersten Beispiel weiter oben in diesem Thema stellt die
TempRecord-Klasse eine Length-Eigenschaft bereit, die dem Clientcode ermöglicht, die Eingabe vor der
Übergabe an den Indexer zu überprüfen. Sie können den Fehlerbehandlungscode auch in den Indexer
selbst einfügen. Achten Sie darauf alle Ausnahmen, die Sie innerhalb eines Indexeraccessors auslösen, für
Benutzer zu dokumentieren.
Schränken Sie den Zugriff auf die get- und set-Accessors so weit wie möglich ein. Dies ist insbesondere
wichtig für den set -Accessor. Weitere Informationen finden Sie unter Einschränken des Accessorzugriffs.

Siehe auch
C#-Programmierhandbuch
Indexer
Eigenschaften
Indexer in Schnittstellen (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Indexer können für eine Schnittstelle deklariert werden. Accessoren für Schnittstellenindexer unterscheiden sich
von den Accessoren für Klassen-Indexer in den folgenden Punkten:
Schnittstellenaccessoren verwenden keine Modifizierer.
Schnittstellenzugriffsmethoden weisen in der Regel keinen Text auf.
Der Zweck einer Zugriffsmethode besteht darin, anzugeben, ob der Indexer gleichzeitig Lese- und Schreibzugriff,
nur Lesezugriff oder nur Schreibzugriff besitzt. In seltenen Fällen müssen Sie eine Implementierung für einen in
einer Schnittstelle definierten Indexer angeben. Indexer definieren üblicherweise eine API für den Zugriff auf
Datenfelder, und Datenfelder können nicht in einer Schnittstelle definiert werden.
Das folgende Beispiel zeigt den Accessor für einen Schnittstellenindexer:

public interface ISomeInterface


{
//...

// Indexer declaration:
string this[int index]
{
get;
set;
}
}

Die Signatur eines Indexers muss sich von den Signaturen aller anderen in derselben Schnittstelle deklarierten
Indexer unterscheiden.

Beispiel
Das folgende Beispiel zeigt, wie Schnittstellenindexer implementiert werden.
// Indexer on an interface:
public interface IIndexInterface
{
// Indexer declaration:
int this[int index]
{
get;
set;
}
}

// Implementing the interface.


class IndexerClass : IIndexInterface
{
private int[] arr = new int[100];
public int this[int index] // indexer declaration
{
// The arr object will throw IndexOutOfRange exception.
get => arr[index];
set => arr[index] = value;
}
}

IndexerClass test = new IndexerClass();


System.Random rand = new System.Random();
// Call the indexer to initialize its elements.
for (int i = 0; i < 10; i++)
{
test[i] = rand.Next();
}
for (int i = 0; i < 10; i++)
{
System.Console.WriteLine($"Element #{i} = {test[i]}");
}

/* Sample output:
Element #0 = 360877544
Element #1 = 327058047
Element #2 = 1913480832
Element #3 = 1519039937
Element #4 = 601472233
Element #5 = 323352310
Element #6 = 1422639981
Element #7 = 1797892494
Element #8 = 875761049
Element #9 = 393083859
*/

Im vorherigen Beispiel könnte der Schnittstellenmember durch Verwendung des vollqualifizierten Namens des
Schnittstellenmembers explizit implementiert werden. Beispiel:

string IIndexInterface.this[int index]


{
}

Der vollqualifizierte Name ist jedoch nur erforderlich, um Mehrdeutigkeiten zu vermeiden, wenn mehr als eine
Schnittstelle mit derselben Indexersignatur von der Klasse implementiert wird. Wenn z.B. eine Employee -Klasse
die beiden Schnittstellen ICitizen und IEmployee implementiert und beide Schnittstellen dieselbe
Indexersignatur besitzen, ist die explizite Implementierung des Schnittstellenmembers erforderlich. Das
bedeutet, dass die folgende Indexerdeklaration:
string IEmployee.this[int index]
{
}

den Indexer für die Schnittstelle IEmployee implementiert. Dahingegen implementiert die folgende Deklaration:

string ICitizen.this[int index]


{
}

den Indexer für die Schnittstelle ICitizen .

Siehe auch
C#-Programmierhandbuch
Indexer
Eigenschaften
Schnittstellen
Vergleich zwischen Eigenschaften und Indexern (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Indexer sind wie Eigenschaften. Mit Ausnahme der in der folgenden Tabelle aufgeführten Unterschiede gelten
alle für Eigenschaftenaccessoren definierten Regeln auch für Indexeraccessoren.

EIGEN SC H A F T IN DEXER

Damit können Methoden wie allgemein zugängliche Damit können Elemente einer internen Auflistung eines
Datenmember aufgerufen werden. Objekts durch die Anwendung der Arraynotation auf das
Objekt aufgerufen werden.

Der Zugriff erfolgt über einen einfachen Namen. Der Zugriff erfolgt über einen Index.

Kann ein statischer Member oder ein Instanzmember sein. Muss ein Instanzmember sein.

Ein get-Accessor einer Eigenschaft weist keine Parameter auf. Ein get -Accessor eines Indexers hat dieselbe Liste formaler
Parameter wie der Indexer.

Ein set-Accessor einer Eigenschaft enthält den impliziten Ein set -Accessor eines Indexers enthält neben dem value-
value -Parameter. Parameter auch dieselbe Liste formaler Parameter wie der
Indexer.

Unterstützt Kurzsyntax mit Auto-Implemented Properties Unterstützt Ausdruckskörpermember für nur abrufende
(Automatisch implementierte Eigenschaften). Indexer.

Weitere Informationen
C#-Programmierhandbuch
Indexer
Eigenschaften
Ereignisse (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Ereignisse aktivieren eine Klasse oder ein Objekt, um Informationen über Aktionen von Interesse an andere
Klassen oder Objekte zu übermitteln. Die Klasse, die das Ereignis sendet (oder auslöst), wird als Herausgeber
bezeichnet, und die Klassen, die das Ereignis empfangen (oder behandeln), werden als Abonnenten bezeichnet.
In einer typischen C#-Windows Forms oder Web-Anwendung abonnieren Sie Ereignisse, die von
Steuerelementen wie Schaltflächen und Listenfelder ausgelöst werden. Sie können die integrierte
Entwicklungsumgebung (IDE) von Visual C# zum Durchsuchen der Ereignisse verwenden, die ein Steuerelement
auslöst, und diejenigen wählen, die Sie behandeln möchten. Mithilfe der IDE können eine leere
Ereignishandlermethode und der Code ganz einfach automatisch zu den Abonnenten des Ereignisses
hinzugefügt werden. Weitere Informationen finden Sie unter Abonnieren von Ereignissen und Kündigen von
Ereignisabonnements.

Übersicht über Ereignisse


Ereignisse verfügen über folgende Eigenschaften:
Der Herausgeber wird bestimmt, wenn ein Ereignis ausgelöst wird. Die Abonnenten bestimmen, welche
Aktion als Reaktion auf das Ereignis ausgeführt wird.
Ein Ereignis kann mehrere Abonnenten haben. Ein Abonnent kann mehrere Ereignisse von mehreren
Herausgebern behandeln.
Ereignisse, die keine Abonnenten haben, werden nie ausgelöst.
Ereignisse werden in der Regel verwendet, um Benutzeraktionen wie Mausklicks oder Menüauswahlen in
GUI-Schnittstellen zu signalisieren.
Wenn ein Ereignis mehrere Abonnenten hat, werden die Ereignishandler synchron aufgerufen, wenn ein
Ereignis ausgelöst wird. Informationen zum asynchronen Aufrufen von Ereignissen finden Sie unter
Asynchrones Aufrufen von synchronen Methoden.
In der .NET-Klassenbibliothek basieren Ereignisse auf dem Delegaten EventHandler und der Basisklasse
EventArgs.

Verwandte Abschnitte
Weitere Informationen finden Sie unter:
Abonnieren von Ereignissen und Kündigen von Ereignisabonnements
Veröffentlichen von Ereignissen, die den .NET-Richtlinien entsprechen
Auslösen von Basisklassenereignissen in abgeleiteten Klassen
Implementieren von Schnittstellenereignissen
Implementieren benutzerdefinierter Ereignisaccessoren

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter Ereignisse in der C#-Sprachspezifikation. Die Sprachspezifikation ist
die verbindliche Quelle für die Syntax und Verwendung von C#.

Enthaltene Buchkapitel
Delegates, Events, and Lambda Expressions (Delegaten, Ereignisse und Lambda-Ausdrücke) in C# 3.0 Cookbook,
Third Edition: More than 250 solutions for C# 3.0 programmers (C# 3.0-Cookbook, 3. Auflage: Mehr als 250
Lösungen für C# 3.0-Programmierer)
Delegates and Events (Delegaten und Ereignisse) in Learning C# 3.0: Master the Fundamentals of C# 3.0
(Erlernen von C# 3.0: Die Grundlagen von C# 3.0)

Siehe auch
EventHandler
C#-Programmierhandbuch
Delegaten
Erstellen von Ereignishandlern in Windows Forms
Vorgehensweise: Abonnieren von Ereignissen und
Kündigen von Ereignisabonnements (C#-
Programmierleitfaden)
04.11.2021 • 3 minutes to read

Wenn Sie benutzerdefinierten Code schreiben möchten, der aufgerufen wird, wenn dieses Ereignis ausgelöst
wird, können Sie ein Ereignis abonnieren, das von einer anderen Klasse veröffentlicht wurde. Sie können z.B. das
click -Ereignis einer Schaltfläche abonnieren, damit Ihre Anwendung etwas nützliches macht, wenn ein
Benutzer auf die Schaltfläche klickt.
So abonnieren Sie Ereignisse mit der Visual Studio IDE
1. Wenn Sie in der Ansicht Entwurf das Fenster Eigenschaften nicht sehen können, klicken Sie mit der
rechten Maustaste auf das Formular oder das Kontrollelement, für das Sie einen Ereignishandler erstellen
möchten, und wählen Sie anschließen Eigenschaften aus.
2. Klicken Sie oben im Fenster Eigenschaften auf das Symbol Ereignisse .
3. Doppelklicken Sie auf das Ereignis, das Sie erstellen möchten, z.B. das Load -Ereignis.
Visual C# erstellt eine leere Ereignishandlermethode, und fügt diese in den Code ein. Alternativ können
Sie den Code auch manuell in der Codeansicht einfügen. Die folgenden Codezeilen deklarieren
beispielsweise eine Eventhandlermethode, die aufgerufen wird, wenn die Klasse Form das Load -Ereignis
auslöst.

private void Form1_Load(object sender, System.EventArgs e)


{
// Add your form load event handling code here.
}

Die Codezeile, die für das Abonnement des Ereignisses erforderlich ist, wird auch automatisch in der
InitializeComponent -Methode in der Datei „Form1.Designer.cs“ in Ihrem Projekt generiert. Sie sieht
ungefähr so aus:

this.Load += new System.EventHandler(this.Form1_Load);

So abonnieren Sie Ereignisse programmgesteuert


1. Definieren Sie eine Ereignishandlermethode, deren Signatur mit der Delegatsignatur des Ereignisses
übereinstimmt. Wenn das Ereignis z.B. auf dem Delegattyp EventHandler basiert, repräsentiert der
folgende Code den Methodenstub:

void HandleCustomEvent(object sender, CustomEventArgs a)


{
// Do something useful here.
}

2. Verwenden Sie den Additionszuweisungsoperator ( += ), um Ihrem Ereignis einen Ereignishandler


anzufügen. Gehen Sie in folgendem Beispiel davon aus, dass ein Objekt mit dem Namen publisher ein
Ereignis mit dem Namen RaiseCustomEvent aufweist. Beachten Sie, dass die Abonnementklasse einen
Verweis auf die Herausgeberklasse benötigt, um deren Ereignis abonnieren zu können.

publisher.RaiseCustomEvent += HandleCustomEvent;

Sie können auch einen Lambdaausdruck zum Angeben eines Ereignishandlers verwenden:

public Form1()
{
InitializeComponent();
this.Click += (s,e) =>
{
MessageBox.Show(((MouseEventArgs)e).Location.ToString());
};
}

So abonnieren Sie Ereignisse mit einer anonymen Funktion


Wenn Sie das Abonnement eines Ereignisses später nicht kündigen müssen, können Sie den
Additionszuweisungsoperator ( += ) verwenden, um eine anonyme Funktion als Ereignishandler anzufügen.
Gehen Sie in folgendem Beispiel davon aus, dass ein Objekt mit dem Namen publisher ein Ereignis mit dem
Namen RaiseCustomEvent aufweist, und dass eine CustomEventArgs -Klasse so definiert wurde, dass Sie eine Art
von spezialisierter Ereignisinformation enthält. Beachten Sie, dass die Abonnementklasse einen Verweis auf die
publisher benötigt, um deren Ereignis abonnieren zu können.

publisher.RaiseCustomEvent += (object o, CustomEventArgs e) =>


{
string s = o.ToString() + " " + e.ToString();
Console.WriteLine(s);
};

Sie können das Abonnement eines Ereignisses nicht ohne Weiteres kündigen, wenn Sie eine anonyme Funktion
zum Abonnieren verwendet haben. Um in einem derartigen Szenario das Abonnement kündigen zu können,
kehren Sie zu dem Code zurück, mit dem Sie das Ereignis abonniert haben, speichern Sie anschließend die
anonyme Funktion in einer Delegatvariablen, und fügen Sie den Delegaten dann dem Ereignis hinzu. Es wird
empfohlen, keine anonymen Funktionen für das Abonnieren von Ereignissen zu verwenden, wenn Sie das
Abonnement später in Ihrem Code noch einmal kündigen müssen. Weitere Informationen zu anonymen
Funktionen finden Sie unter Lambdaausdrücke.

Kündigen des Abonnements


Kündigen Sie das Ereignisabonnement, um ein Abrufen des Ereignishandlers beim Auslösen des Ereignisses zu
verhindern. Sie sollten das Ereignisabonnement kündigen, bevor Sie ein Abonnentenobjekt verwerfen, um
Ressourcenverluste zu verhindern. Bis zur Kündigung Ihres Ereignisabonnements verweist der Multicastdelegat,
der dem Ereignis im Veröffentlichungsobjekt zugrunde liegt, auf einen Delegaten, der den Ereignishandler des
Abonnenten einkapselt. Solange das Veröffentlichungsobjekt diesen Verweis enthält, wird Ihr Abonnentenobjekt
bei der automatische Speicherbereinigung nicht gelöscht.
So kündigen Sie ein Ereignisabonnement
Verwenden Sie den Subtraktionszuweisungsoperator ( -= ), um ein Ereignisabonnement zu kündigen:

publisher.RaiseCustomEvent -= HandleCustomEvent;

Wenn alle Abonnenten ihr Ereignisabonnement gekündigt haben, wird die Ereignisinstanz in der
Herausgeberklasse auf null festgelegt.
Siehe auch
Ereignisse
event
Veröffentlichen von Ereignissen, die den .NET-Richtlinien entsprechen
Operatoren „-“ und „-=“ (C#-Referenz)
Operatoren „+“ und „+=“ (C#-Referenz)
Veröffentlichen von mit den .NET-Richtlinien
konformen Ereignissen (C#-Programmierhandbuch)
04.11.2021 • 3 minutes to read

Im Folgenden erfahren Sie, wie Sie Ereignisse, die dem .NET-Standardmuster folgen, zu Ihren Klassen und
Strukturen hinzufügen. Alle Ereignisse in der .NET-Klassenbibliothek basieren auf dem EventHandler-Delegaten,
der wie folgt definiert ist:

public delegate void EventHandler(object sender, EventArgs e);

NOTE
.NET Framework 2.0 führt eine generische Version dieses Delegaten ein: EventHandler<TEventArgs>. Die folgenden
Beispiele zeigen, wie Sie beide Versionen verwenden können.

Auch wenn Ereignisse in von Ihnen definierten Klassen auf jedem gültigen Delegattyp basieren können (sogar
Delegaten, die einen Wert zurückgeben), wird allgemein empfohlen, für Ihre Ereignisse das .NET-Muster mit
EventHandler zu verwenden, wie im folgenden Beispiel zu sehen.
Der Name EventHandler kann etwas Verwirrung stiften, da das Ereignis nicht tatsächlich verarbeitet wird.
EventHandler und der generische EventHandler<TEventArgs> sind Delegattypen. Eine Methode oder eine
anonyme Funktion, deren Signatur mit der Delegatdefinition übereinstimmt, ist der Ereignishandler und wird
aufgerufen, wenn das Ereignis ausgelöst wird.

Veröffentlichen von auf dem EventHandler-Muster basierenden


Ereignissen
1. (Überspringen Sie diesen Schritt, und gehen Sie direkt zu Schritt 3a, wenn Sie keine benutzerdefinierten
Daten mit Ihrem Ereignis senden müssen.) Deklarieren Sie die Klasse Ihrer benutzerdefinierten Daten in
einem für Ihre Herausgeber- und Ihre Abonnentenklasse sichtbaren Geltungsbereich. Fügen Sie dann die
erforderlichen Member hinzu, die Ihre benutzerdefinierten Ereignisdaten enthalten sollen. In diesem
Beispiel wird eine einzelne Zeichenfolge zurückgegeben.

public class CustomEventArgs : EventArgs


{
public CustomEventArgs(string message)
{
Message = message;
}

public string Message { get; set; }


}

2. (Überspringen Sie diesen Schritt, wenn Sie eine generische Version von EventHandler<TEventArgs>
verwenden.) Deklarieren Sie einen Delegaten in Ihrer Herausgeberklasse. Geben Sie ihm einen Namen,
der auf EventHandler endet. Der zweite Parameter gibt Ihren benutzerdefinierten EventArgs -Typ an.
public delegate void CustomEventHandler(object sender, CustomEventArgs args);

3. Deklarieren Sie das Ereignis in Ihrer Herausgeberklasse, indem Sie einen der folgenden Schritte
verwenden.
a. Wenn Sie keine benutzerdefinierte EventArgs-Klasse haben, ist Ihr Ereignistyp der nicht generische
EventHandler-Delegat. Sie müssen den Delegaten nicht deklarieren, da er schon im System-
Namespace deklariert wurde, der bei der Erstellung Ihres C#-Projekts erstellt wird. Fügen Sie den
folgenden Code in Ihre Herausgeberklasse ein.

public event EventHandler RaiseCustomEvent;

b. Wenn Sie die nicht generische Version von EventHandler verwenden, und Sie eine
benutzerdefinierte Klasse haben, die von EventArgs abgeleitet ist, deklarieren Sie Ihr Ereignis
innerhalb Ihrer Herausgeberklasse, und verwenden Sie Ihren Delegaten aus Schritt 2 als Typ.

public event CustomEventHandler RaiseCustomEvent;

c. Wenn sie die generische Version verwenden, benötigen Sie keinen benutzerdefinierten Delegaten.
Stattdessen geben Sie in Ihrer Herausgeberklasse Ihren Ereignistypen als
EventHandler<CustomEventArgs> an und fügen den Namen Ihrer eigenen Klasse in die eckigen
Klammern ein.

public event EventHandler<CustomEventArgs> RaiseCustomEvent;

Beispiel
Das folgende Beispiel veranschaulicht die oben genannten Schritte durch das Verwenden von einer
benutzerdefinierten EventArgs-Klasse und von EventHandler<TEventArgs> als Ereignistyp.

using System;

namespace DotNetEvents
{
// Define a class to hold custom event info
public class CustomEventArgs : EventArgs
{
public CustomEventArgs(string message)
{
Message = message;
}

public string Message { get; set; }


}

// Class that publishes an event


class Publisher
{
// Declare the event using EventHandler<T>
public event EventHandler<CustomEventArgs> RaiseCustomEvent;

public void DoSomething()


{
// Write some code that does something useful here
// then raise the event. You can also raise an event
// before you execute a block of code.
OnRaiseCustomEvent(new CustomEventArgs("Event triggered"));
OnRaiseCustomEvent(new CustomEventArgs("Event triggered"));
}

// Wrap event invocations inside a protected virtual method


// to allow derived classes to override the event invocation behavior
protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
{
// Make a temporary copy of the event to avoid possibility of
// a race condition if the last subscriber unsubscribes
// immediately after the null check and before the event is raised.
EventHandler<CustomEventArgs> raiseEvent = RaiseCustomEvent;

// Event will be null if there are no subscribers


if (raiseEvent != null)
{
// Format the string to send inside the CustomEventArgs parameter
e.Message += $" at {DateTime.Now}";

// Call to raise the event.


raiseEvent(this, e);
}
}
}

//Class that subscribes to an event


class Subscriber
{
private readonly string _id;

public Subscriber(string id, Publisher pub)


{
_id = id;

// Subscribe to the event


pub.RaiseCustomEvent += HandleCustomEvent;
}

// Define what actions to take when the event is raised.


void HandleCustomEvent(object sender, CustomEventArgs e)
{
Console.WriteLine($"{_id} received this message: {e.Message}");
}
}

class Program
{
static void Main()
{
var pub = new Publisher();
var sub1 = new Subscriber("sub1", pub);
var sub2 = new Subscriber("sub2", pub);

// Call the method that raises the event.


pub.DoSomething();

// Keep the console window open


Console.WriteLine("Press any key to continue...");
Console.ReadLine();
}
}
}

Siehe auch
Delegate
C#-Programmierhandbuch
Ereignisse
Delegaten
Vorgehensweise: Auslösen von
Basisklassenereignissen in abgeleiteten Klassen (C#-
Programmierleitfaden)
04.11.2021 • 3 minutes to read

Das folgende einfache Beispiel zeigt das Standardverfahren zum Deklarieren von Ereignissen in einer
Basisklasse, sodass sie auch von abgeleiteten Klassen ausgelöst werden können. Dieses Muster wird häufig in
Windows Forms-Klassen in .NET-Klassenbibliotheken verwendet.
Wenn Sie eine Klasse erstellen, die als Basisklasse für andere Klassen verwendet werden kann, sollten Sie den
Fakt bedenken, dass Ereignisse ein spezieller Typ von Delegaten sind, die nur von innerhalb der Klasse, die sie
deklariert hat, aufgerufen werden können. Abgeleitete Klassen können nicht direkt Ereignisse aufrufen, die
innerhalb der Basisklasse deklariert werden. Mitunter kann es zwar erwünscht sein, dass ein Ereignis nur von
der Basisklasse ausgelöst werden kann, in den meisten Fällen sollten Sie jedoch der abgeleiteten Klasse das
Aufrufen von Basisklassenereignissen ermöglichen. Zu diesem Zweck können Sie eine geschützte aufrufende
Methode in der Basisklasse erstellen, die das Ereignis umschließt. Durch Aufrufen oder Überschreiben dieser
aufrufenden Methode, können abgeleitete Klassen das Ereignis indirekt aufrufen.

NOTE
Deklarieren Sie keine virtuellen Ereignisse in einer Basisklasse, und überschreiben Sie sie nicht in einer abgeleiteten Klasse.
Der C#-Compiler verarbeitet diese nicht ordnungsgemäß, und es nicht vorhersehbar, ob ein Abonnent des abgeleiteten
Ereignisses tatsächlich das Ereignis der Basisklasse abonnieren wird.

Beispiel
namespace BaseClassEvents
{
// Special EventArgs class to hold info about Shapes.
public class ShapeEventArgs : EventArgs
{
public ShapeEventArgs(double area)
{
NewArea = area;
}

public double NewArea { get; }


}

// Base class event publisher


public abstract class Shape
{
protected double _area;

public double Area


{
get => _area;
set => _area = value;
}

// The event. Note that by using the generic EventHandler<T> event type
// we do not need to declare a separate delegate type.
public event EventHandler<ShapeEventArgs> ShapeChanged;
public event EventHandler<ShapeEventArgs> ShapeChanged;

public abstract void Draw();

//The event-invoking method that derived classes can override.


protected virtual void OnShapeChanged(ShapeEventArgs e)
{
// Safely raise the event for all subscribers
ShapeChanged?.Invoke(this, e);
}
}

public class Circle : Shape


{
private double _radius;

public Circle(double radius)


{
_radius = radius;
_area = 3.14 * _radius * _radius;
}

public void Update(double d)


{
_radius = d;
_area = 3.14 * _radius * _radius;
OnShapeChanged(new ShapeEventArgs(_area));
}

protected override void OnShapeChanged(ShapeEventArgs e)


{
// Do any circle-specific processing here.

// Call the base class event invocation method.


base.OnShapeChanged(e);
}

public override void Draw()


{
Console.WriteLine("Drawing a circle");
}
}

public class Rectangle : Shape


{
private double _length;
private double _width;

public Rectangle(double length, double width)


{
_length = length;
_width = width;
_area = _length * _width;
}

public void Update(double length, double width)


{
_length = length;
_width = width;
_area = _length * _width;
OnShapeChanged(new ShapeEventArgs(_area));
}

protected override void OnShapeChanged(ShapeEventArgs e)


{
// Do any rectangle-specific processing here.

// Call the base class event invocation method.


base.OnShapeChanged(e);
}

public override void Draw()


{
Console.WriteLine("Drawing a rectangle");
}
}

// Represents the surface on which the shapes are drawn


// Subscribes to shape events so that it knows
// when to redraw a shape.
public class ShapeContainer
{
private readonly List<Shape> _list;

public ShapeContainer()
{
_list = new List<Shape>();
}

public void AddShape(Shape shape)


{
_list.Add(shape);

// Subscribe to the base class event.


shape.ShapeChanged += HandleShapeChanged;
}

// ...Other methods to draw, resize, etc.

private void HandleShapeChanged(object sender, ShapeEventArgs e)


{
if (sender is Shape shape)
{
// Diagnostic message for demonstration purposes.
Console.WriteLine($"Received event. Shape area is now {e.NewArea}");

// Redraw the shape here.


shape.Draw();
}
}
}

class Test
{
static void Main()
{
//Create the event publishers and subscriber
var circle = new Circle(54);
var rectangle = new Rectangle(12, 9);
var container = new ShapeContainer();

// Add the shapes to the container.


container.AddShape(circle);
container.AddShape(rectangle);

// Cause some events to be raised.


circle.Update(57);
rectangle.Update(7, 7);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
}
}
/* Output:
Received event. Shape area is now 10201.86
Drawing a circle
Received event. Shape area is now 49
Drawing a rectangle
*/

Weitere Informationen
C#-Programmierhandbuch
Ereignisse
Delegaten
Zugriffsmodifizierer
Erstellen von Ereignishandlern in Windows Forms
Vorgehensweise: Implementieren von
Schnittstellenereignissen (C#-Programmierleitfaden)
04.11.2021 • 3 minutes to read

Eine Schnittstelle kann ein Ereignis deklarieren. Das folgende Beispiel zeigt, wie Schnittstellenereignisse in einer
Klasse implementiert werden. Die Regeln sind mit den Regeln zum Implementieren von Schnittstellenmethoden
oder -eigenschaften weitestgehend identisch.

So implementieren Sie Schnittstellenereignisse in einer Klasse


Deklarieren Sie das Ereignis in der Klasse, und rufen Sie es dann an den entsprechenden Stellen auf.

namespace ImplementInterfaceEvents
{
public interface IDrawingObject
{
event EventHandler ShapeChanged;
}
public class MyEventArgs : EventArgs
{
// class members
}
public class Shape : IDrawingObject
{
public event EventHandler ShapeChanged;
void ChangeShape()
{
// Do something here before the event…

OnShapeChanged(new MyEventArgs(/*arguments*/));

// or do something here after the event.


}
protected virtual void OnShapeChanged(MyEventArgs e)
{
ShapeChanged?.Invoke(this, e);
}
}

Beispiel
Das folgende Beispiel zeigt die ungewöhnlichere Vorgehensweise, wenn die Klasse von zwei oder mehr
Schnittstellen erbt und jede Schnittstelle über ein Ereignis mit dem gleichen Namen verfügt. In dieser Situation
müssen Sie eine explizite Schnittstellenimplementierung für mindestens eines der Ereignisse bereitstellen. Wenn
Sie eine explizite Schnittstellenimplementierung für ein Ereignis schreiben, müssen Sie auch den add -
Ereignisaccessor und den remove -Ereignisaccessor schreiben. Normalerweise werden diese vom Compiler
bereitgestellt, was in diesem Fall jedoch nicht möglich ist.
Durch das Bereitstellen Ihrer eigenen Accessoren können Sie festlegen, ob die beiden Ereignisse durch das
gleiche Ereignis oder durch verschiedene Ereignisse in der Klasse dargestellt werden sollen. Wenn z. B. die
Ereignisse gemäß den Schnittstellenspezifikationen zu unterschiedlichen Zeiten ausgelöst werden sollen,
können Sie jedes Ereignis einer separaten Implementierung in der Klasse zuordnen. Im folgenden Beispiel
bestimmen Abonnenten, welches OnDraw -Ereignis sie erhalten, indem sie den Formverweis entweder in eine
IShape oder ein IDrawingObject umwandeln.

namespace WrapTwoInterfaceEvents
{
using System;

public interface IDrawingObject


{
// Raise this event before drawing
// the object.
event EventHandler OnDraw;
}
public interface IShape
{
// Raise this event after drawing
// the shape.
event EventHandler OnDraw;
}

// Base class event publisher inherits two


// interfaces, each with an OnDraw event
public class Shape : IDrawingObject, IShape
{
// Create an event for each interface event
event EventHandler PreDrawEvent;
event EventHandler PostDrawEvent;

object objectLock = new Object();

// Explicit interface implementation required.


// Associate IDrawingObject's event with
// PreDrawEvent
#region IDrawingObjectOnDraw
event EventHandler IDrawingObject.OnDraw
{
add
{
lock (objectLock)
{
PreDrawEvent += value;
}
}
remove
{
lock (objectLock)
{
PreDrawEvent -= value;
}
}
}
#endregion
// Explicit interface implementation required.
// Associate IShape's event with
// PostDrawEvent
event EventHandler IShape.OnDraw
{
add
{
lock (objectLock)
{
PostDrawEvent += value;
}
}
remove
{
lock (objectLock)
{
{
PostDrawEvent -= value;
}
}
}

// For the sake of simplicity this one method


// implements both interfaces.
public void Draw()
{
// Raise IDrawingObject's event before the object is drawn.
PreDrawEvent?.Invoke(this, EventArgs.Empty);

Console.WriteLine("Drawing a shape.");

// Raise IShape's event after the object is drawn.


PostDrawEvent?.Invoke(this, EventArgs.Empty);
}
}
public class Subscriber1
{
// References the shape object as an IDrawingObject
public Subscriber1(Shape shape)
{
IDrawingObject d = (IDrawingObject)shape;
d.OnDraw += d_OnDraw;
}

void d_OnDraw(object sender, EventArgs e)


{
Console.WriteLine("Sub1 receives the IDrawingObject event.");
}
}
// References the shape object as an IShape
public class Subscriber2
{
public Subscriber2(Shape shape)
{
IShape d = (IShape)shape;
d.OnDraw += d_OnDraw;
}

void d_OnDraw(object sender, EventArgs e)


{
Console.WriteLine("Sub2 receives the IShape event.");
}
}

public class Program


{
static void Main(string[] args)
{
Shape shape = new Shape();
Subscriber1 sub = new Subscriber1(shape);
Subscriber2 sub2 = new Subscriber2(shape);
shape.Draw();

// Keep the console window open in debug mode.


System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
}
/* Output:
Sub1 receives the IDrawingObject event.
Drawing a shape.
Sub2 receives the IShape event.
*/
Weitere Informationen
C#-Programmierhandbuch
Ereignisse
Delegaten
Explizite Schnittstellenimplementierung
Auslösen von Basisklassenereignissen in abgeleiteten Klassen
Vorgehensweise: Implementieren
benutzerdefinierter Ereignisaccessoren (C#-
Programmierleitfaden)
04.11.2021 • 2 minutes to read

Ein Ereignis ist eine besondere Art eines Multitaskdelegaten, der nur aus der Klasse , in der er deklariert ist,
aufgerufen werden kann. Der Client abonniert das Ereignis durch Bereitstellen eines Verweises auf eine
Methode, die aufgerufen werden soll, wenn das Ereignis ausgelöst wird. Diese Methoden werden der Aufrufliste
des Delegaten über Ereignisaccessoren hinzugefügt, die Eigenschaftenaccessoren ähneln, mit der Ausnahme,
dass Ereignisaccessoren mit add und remove bezeichnet werden. In den meisten Fällen müssen Sie keine
benutzerdefinierten Ereignisaccessoren angeben. Wenn keine benutzerdefinierten Ereignisaccessoren im Code
angegeben werden, werden sie durch den Compiler automatisch hinzugefügt. In einigen Fällen müssen Sie
möglicherweise jedoch benutzerdefiniertes Verhalten bereitstellen. Einen solchen Fall enthält das Thema
Vorgehensweise: Implementieren von Schnittstellenereignissen.

Beispiel
Das folgende Beispiel veranschaulicht das Implementieren von Ereignisaccessoren zum Hinzufügen und
Entfernen. Obwohl Sie Code innerhalb der Accessoren ersetzen können, wird empfohlen, dass Sie das Ereignis
sperren, bevor Sie eine neue Ereignishandlermethode hinzufügen oder entfernen.

event EventHandler IDrawingObject.OnDraw


{
add
{
lock (objectLock)
{
PreDrawEvent += value;
}
}
remove
{
lock (objectLock)
{
PreDrawEvent -= value;
}
}
}

Siehe auch
Ereignisse
event
Generische Typparameter (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Bei der Definition eines generischen Typs oder einer Methode ist ein Typparameter ein Platzhalter für einen
bestimmten Typ, den ein Client angibt, wenn eine Instanz des generischen Typs erstellt wird. Eine generische
Klasse, z.B. die unter Einführung in Generics aufgelistete Klasse GenericList<T> , kann nicht ohne Anpassung
verwendet werden, denn sie ist nicht wirklich ein Typ, sondern mehr wie die Kopie eines Typs. Um
GenericList<T> verwenden zu können, muss der Clientcode einen konstruierten Typ deklarieren und
instanziieren, indem er ein Typargument in spitzen Klammern angibt. Das Typargument für diese spezielle Klasse
kann jeder Typ sein, der vom Compiler erkannt wird. Instanzen von konstruierten Typen können in beliebiger
Zahl erstellt werden, wobei jede Instanz ein anderes Typargument verwendet, z.B.:

GenericList<float> list1 = new GenericList<float>();


GenericList<ExampleClass> list2 = new GenericList<ExampleClass>();
GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();

Bei jeder dieser Instanzen von GenericList<T> wird jedes Vorkommen von T in der Klasse zur Laufzeit durch
das Typargument ersetzt. Aufgrund dieser Ersetzung werden drei separate typsichere und effiziente Objekte
mithilfe einer einzigen Klassendefinition erstellt. Weitere Informationen dazu, wie diese Ersetzung von der CLR
ausgeführt wird, finden Sie unter Generics zur Laufzeit.

Richtlinien für die Benennung von Typparametern


Ver wenden Sie zur Benennung von generischen Typparametern beschreibende Namen, es sei denn, ein
Name aus einem einzelnen Buchstaben reicht als Erklärung völlig aus, und ein beschreibender Name
würde das Verständnis für den Namen nicht wirklich erhöhen.

public interface ISessionChannel<TSession> { /*...*/ }


public delegate TOutput Converter<TInput, TOutput>(TInput from);
public class List<T> { /*...*/ }

Ver wenden Sie T als Typparametername für Typen, die einen einzelnen Buchstaben als Typparameter
haben.

public int IComparer<T>() { return 0; }


public delegate bool Predicate<T>(T item);
public struct Nullable<T> where T : struct { /*...*/ }

Ver wenden Sie das Präfix „T“ für beschreibende Typparameternamen.

public interface ISessionChannel<TSession>


{
TSession Session { get; }
}

Überlegen Sie, ob Sie Einschränkungen, die für einen Typparameter gelten, im Namen des Parameters
angeben möchten. Ein auf ISession eingeschränkter Parameter könnte z.B. TSession genannt werden.
Die Codeanalyseregel CA1715 kann verwendet werden, um sicherzustellen, dass Typparameter entsprechend
benannt werden.

Weitere Informationen
System.Collections.Generic
C#-Programmierhandbuch
Generics
Unterschiede zwischen C++-Vorlagen und C#-Generics
Einschränkungen für Typparameter (C#-
Programmierhandbuch)
04.11.2021 • 10 minutes to read

Einschränkungen informieren den Compiler über die Funktionen, über die ein Typargument verfügen muss.
Ohne Einschränkungen könnte das Typargument jedes beliebige Argument sein. Der Compiler kann nur die
System.Object-Elemente annehmen. Dies ist die übergeordnete Basisklasse für jeden beliebigen .NET-Typ.
Weitere Informationen finden Sie unter Weshalb Einschränkungen?. Wenn Clientcode einen Typ verwendet, der
einen Constraint nicht erfüllt, gibt der Compiler einen Fehler aus. Constraints werden mit dem kontextuellen
Schlüsselwort where angegeben. In der folgenden Tabelle werden die verschiedenen Einschränkungstypen
aufgelistet:

C O N ST RA IN T B ESC H REIB UN G

where T : struct Das Typargument muss ein Non-Nullable-Werttyp sein.


Weitere Informationen zu Nullable-Werttypen finden Sie
unter Nullable-Werttypen. Da alle Werttypen einen
parameterlosen Konstruktor aufweisen, auf den zugegriffen
werden kann, impliziert die struct -Einschränkung die
new() -Einschränkung und kann nicht mit der new() -
Einschränkung kombiniert werden. Ferner kann der struct
-Constraint nicht mit dem unmanaged -Constraint
kombiniert werden.

where T : class Das Typargument muss ein Verweistyp sein. Diese


Einschränkung gilt auch für jede Klasse, Schnittstelle, jeden
Delegaten oder Arraytyp. In einem Nullable-Kontext in
C# 8.0 oder höher muss T ein Non-Nullable-Verweistyp
sein.

where T : class? Das Typargument muss ein Nullable- oder ein Non-Nullable-
Verweistyp sein. Diese Einschränkung gilt auch für jede
Klasse, Schnittstelle, jeden Delegaten oder Arraytyp.

where T : notnull Das Typargument muss ein Nicht-Nullable-Typ sein. Das


Argument kann ein Non-Nullable-Verweistyp in C# 8.0 oder
höher oder ein Non-Nullable-Werttyp sein.

where T : default Diese Einschränkung beseitigt die Mehrdeutigkeit, wenn Sie


einen uneingeschränkten Typparameter angeben müssen,
wenn Sie eine Methode überschreiben oder eine explizite
Schnittstellenimplementierungen bereitstellen. Die default
Einschränkung impliziert die Basismethode ohne die class
oder struct -Einschränkung. Weitere Informationen finden
Sie unter default Hinweis zum Featurevorschlag.

where T : unmanaged Das Typargument muss ein nicht verwalteter Non-Nullable-


Typ sein. Die unmanaged -Einschränkung impliziert die
struct -Einschränkung und kann weder mit der struct -
noch mit der new() -Einschränkung kombiniert werden.
C O N ST RA IN T B ESC H REIB UN G

where T : new() Das Typargument muss einen öffentlichen, parameterlosen


Konstruktor aufweisen. Beim gemeinsamen Verwenden
anderen Constraints muss der new() -Constraint zuletzt
angegeben werden. Die new() -Einschränkung kann nicht
mit der struct - oder unmanaged -Einschränkung
kombiniert werden.

where T : <base class name> Das Typargument muss die angegebene Basisklasse sein
oder von dieser abgeleitet werden. In einem Nullable-
Kontext in C# 8.0 und höher muss T ein Non-Nullable-
Verweistyp sein, der von der angegebenen Basisklasse
abgeleitet ist.

where T : <base class name>? Das Typargument muss die angegebene Basisklasse sein
oder von dieser abgeleitet werden. In einem Nullable-
Kontext in C# 8.0 und höher kann T entweder ein
Nullable- oder ein Non-Nullable-Typ sein, der von der
angegebenen Basisklasse abgeleitet ist.

where T : <interface name> Das Typargument muss die angegebene Schnittstelle sein
oder diese implementieren. Es können mehrere
Schnittstelleneinschränkungen angegeben werden. Die
einschränkende Schnittstelle kann auch generisch sein. In
einem Nullable-Kontext in C# 8.0 und höher muss T ein
Non-Nullable-Typ sein, der die angegebenen Schnittstelle
implementiert.

where T : <interface name>? Das Typargument muss die angegebene Schnittstelle sein
oder diese implementieren. Es können mehrere
Schnittstelleneinschränkungen angegeben werden. Die
einschränkende Schnittstelle kann auch generisch sein. In
einem Nullable-Kontext in C# 8.0 kann T ein Nullable-
Verweistyp, ein Non-Nullable-Verweistyp oder ein Werttyp
sein. T darf kein Nullable-Werttyp sein.

where T : U Das Typargument, das für T angegeben wird, muss das für
U angegebene Argument sein oder von diesem abgeleitet
werden. Wenn in einem Nullable-Kontext U ein Non-
Nullable-Verweistyp ist, muss T ein Non-Nullable-
Verweistyp sein. Wenn U ein Nullable-Verweistyp ist, kann
T entweder ein Nullable- oder ein Non-Nullable-Typ sein.

Weshalb Einschränkungen?
Constraints geben die Funktionen und Erwartungen eines Typparameters an. Das Deklarieren von Constraints
bedeutet, dass Sie die Vorgänge und Methodenaufrufe des einschränkenden Typs verwenden können. Wenn
Ihre generischen Klassen oder Methoden Vorgänge für generische Teilnehmer durchführen sollen, die über das
einfache Zuweisen und Aufrufen von Methoden hinausgehen und die nicht von System.Object unterstützt
werden, müssen Sie Constraints auf den Typparameter anwenden. Der Basisklassenconstraint sagt dem
Compiler z.B., dass nur Objekte dieses Typs oder Objekte, die von diesem Typ abgeleitet werden, als
Typargumente verwendet werden. Sobald der Compiler diese Garantie hat, kann er erlauben, dass Methoden
dieses Typs in der generischen Klasse aufgerufen werden können. Im folgenden Codebeispiel wird die
Funktionalität veranschaulicht, die der GenericList<T> -Klasse durch das Anwenden einer
Basisklasseneinschränkung hinzugefügt werden kann (in Einführung in Generics).
public class Employee
{
public Employee(string name, int id) => (Name, ID) = (name, id);
public string Name { get; set; }
public int ID { get; set; }
}

public class GenericList<T> where T : Employee


{
private class Node
{
public Node(T t) => (Next, Data) = (null, t);

public Node Next { get; set; }


public T Data { get; set; }
}

private Node head;

public void AddHead(T t)


{
Node n = new Node(t) { Next = head };
head = n;
}

public IEnumerator<T> GetEnumerator()


{
Node current = head;

while (current != null)


{
yield return current.Data;
current = current.Next;
}
}

public T FindFirstOccurrence(string s)
{
Node current = head;
T t = null;

while (current != null)


{
//The constraint enables access to the Name property.
if (current.Data.Name == s)
{
t = current.Data;
break;
}
else
{
current = current.Next;
}
}
return t;
}
}

Die Einschränkung ermöglicht der generischen Klasse, die Employee.Name -Eigenschaft zu verwenden. Die
Einschränkung gibt an, dass alle Elemente des Typs T entweder ein Employee -Objekt oder ein Objekt sind, das
von Employee erbt.
Mehrere Constraints können wie folgt auf den gleichen Typenparameter angewendet werden, und die
Contraints können selbst generische Typen sein:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
// ...
}

Wenn Sie den Constraint where T : class anwenden, vermeiden Sie das Verwenden der Operatoren == und
!= mit dem Typparameter, da diese nur auf Verweisidentität und nicht auf Wertgleichheit prüfen. Dieses
Verhalten tritt auch auf, wenn diese Operatoren in einem Typ überladen werden, der als Argument verwendet
wird. Der folgende Code veranschaulicht diesen Aspekt. Die Ausgabe ist FALSE, obwohl die String-Klasse den
== -Operator überlädt.

public static void OpEqualsTest<T>(T s, T t) where T : class


{
System.Console.WriteLine(s == t);
}

private static void TestStringEquality()


{
string s1 = "target";
System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
string s2 = sb.ToString();
OpEqualsTest<string>(s1, s2);
}

Der Compiler weiß erst zur Kompilierzeit, dass T ein Verweistyp ist und die für alle Verweistypen zulässigen
Standardoperatoren verwendet werden müssen. Wenn Sie auf Wertgleichheit prüfen müssen, wird empfohlen,
dass Sie die where T : IEquatable<T> - oder where T : IComparable<T> -Einschränkung anwenden und die
Schnittstelle in jeder Klasse implementieren, die verwendet wird, um die generische Klasse zu erstellen.

Einschränken mehrerer Parameter


Sie können wie im folgenden Beispiel gezeigt Constraints auf mehrere Parameter und mehrere Constraints auf
einen einzelnen Parameter anwenden:

class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{ }

Ungebundene Typparameter
Typparameter, auf die keine Constraints angewendet wurden, wie z.B. T in der öffentlichen Klasse
SampleClass<T>{} , werden als ungebundene Typparameter bezeichnet. Für ungebundene Typparameter gelten
die folgenden Regeln:
Die Operatoren != und == können nicht verwendet werden, weil es keine Garantie dafür gibt, dass das
jeweilige Typargument diese auch unterstützt.
Sie können in und aus System.Object oder implizit in einen Schnittstellentyp konvertiert werden.
Sie können sie mit NULL vergleichen. Wenn ein ungebundener Parameter mit null verglichen wird, gibt der
Vergleich immer FALSE zurück, wenn das Typargument ein Werttyp ist.

Typparameter als Einschränkungen


Es ist nützlich, einen Typparameter wie in folgendem Beispiel gezeigt als Constraint zu verwenden, wenn eine
Memberfunktion mit ihren eigenen Typparametern diesen Parameter auf den Typparameter des enthaltenden
Typs einschränken muss:

public class List<T>


{
public void Add<U>(List<U> items) where U : T {/*...*/}
}

Im vorherigen Beispiel ist T ein Typconstraint im Kontext der Add -Methode und ein ungebundener
Typparameter im Kontext der List -Klasse.
Typparameter können auch in generischen Klassendefinitionen als Constraints verwendet werden. Der
Typparameter in spitzen Klammern muss zusammen mit allen anderen Typparametern deklariert werden:

//Type parameter V is used as a type constraint.


public class SampleClass<T, U, V> where T : V { }

Das Verwenden von Typparametern als Einschränkungen für generische Klassen ist nur bis zu einem gewissen
Punkt nützlich, da der Compiler keine Informationen über den Typparameter annehmen kann, nur dass er von
System.Object abgeleitet ist. Sie sollten Typparameter als Constraints dann verwenden, wenn Sie eine
Vererbungsbeziehung zwischen zwei Typparametern erzwingen möchten.

notnull -Einschränkung
Ab C# 8.0 können Sie die notnull -Einschränkung verwenden, um anzugeben, dass das Typargument ein Nicht-
Nullable-Wert- oder -Verweistyp sein muss. Im Gegensatz zu den meisten anderen Einschränkungen generiert
der Compiler eine Warnung statt eines Fehlers, wenn ein Typargument die notnull -Einschränkung verletzt.
Die notnull -Einschränkung wirkt sich nur aus, wenn sie in einem Nullable-Kontext verwendet wird. Wenn Sie
die notnull -Einschränkung in einem Nullable-Kontext hinzufügen, generiert der Compiler keine Warnungen
oder Fehler für Verstöße gegen die Einschränkung.

class -Einschränkung
Ab C# 8.0 gibt die class -Einschränkung in einem Nullable-Kontext an, dass das Typargument ein Non-
Nullable-Verweistyp sein muss. In einem Nullable-Kontext generiert der Compiler eine Warnung, wenn ein
Typargument ein Nullable-Verweistyp ist.

default -Einschränkung
Das Hinzufügen von Nullable-Verweistypen erschwert die Verwendung von T? in einem generischen Typ oder
einer generischen Methode. Vor C# 8 könnte T? nur verwendet werden, wenn die Einschränkung struct auf T
angewendet wurde. In diesem Kontext bezieht sich T? auf den Nullable<T> Typ für T . Ab C# 8 kann T?
entweder mit der struct - oder class - Einschränkung verwendet werden, aber eine von beiden muss
vorliegen. Wenn die class Einschränkung verwendet wurde, T? verweist auf den Nullable-Verweistyp für T .
Ab C# 9 kann T? verwendet werden, wenn keine Einschränkung angewendet wird. In diesem Fall wird T? für
Werttypen und Verweistypen genauso interpretiert wie in C# 8. Wenn jedoch T eine Instanz von Nullable<T>
oder T? ist, ist sie mit T identisch. Anders ausgedrückt: Es wird nicht zu T?? .
Da T? jetzt ohne die class - oder struct -Einschränkung verwendet werden kann, können Mehrdeutigkeiten
in Überschreibungen oder expliziten Schnittstellenimplementierungen auftreten. In beiden Fällen enthält die
Außerkraftsetzung nicht die Einschränkungen, sondern erhält sie von der Basisklasse. Wenn die Basisklasse
weder die class - noch die struct -Einschränkung anwendet, müssen abgeleitete Klassen auf irgendeine Weise
angeben, dass eine Außerkraftsetzung für die Basismethode ohne Einschränkung gilt. Dies ist der Zeitpunkt, an
dem die abgeleitete Methode die default -Einschränkung anwendet. Die default -Einschränkung verdeutlicht,
dass weder die class weder noch die struct -Einschränkung.

Nicht verwaltete Einschränkungen


Ab C# 7.3 können Sie die unmanaged -Einschränkung nutzen, um anzugeben, dass der Typparameter ein nicht
verwalteter Non-Nullable-Typ sein muss. Die unmanaged -Einschränkung ermöglicht Ihnen das Schreiben von
wiederverwendbarer Routinen zum Arbeiten mit Typen, die als Speicherblöcke bearbeitet werden können, wie
im folgenden Beispiel gezeigt:

unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged


{
var size = sizeof(T);
var result = new Byte[size];
Byte* p = (byte*)&argument;
for (var i = 0; i < size; i++)
result[i] = *p++;
return result;
}

Die vorherige Methode muss in einen unsafe -Kontext kompiliert werden, da sie den sizeof -Operator für
einen nicht bekannten Typ verwendet, der ein integrierter Typ ist. Ohne die unmanaged -Einschränkung ist der
sizeof -Operator nicht verfügbar.

Die unmanaged -Einschränkung impliziert die struct -Einschränkung und kann nicht mit ihr kombiniert werden.
Da die struct -Einschränkung die new() -Einschränkung impliziert, kann die unmanaged -Einschränkung ebenso
wenig mit der new() -Einschränkung kombiniert werden.

Delegieren von Einschränkungen


Ab C# 7.3 können Sie auch System.Delegate oder System.MulticastDelegate als Basisklasseneinschränkung
verwenden. Die CLR lässt diese Einschränkung immer zu, aber die C#-Sprache lässt sie nicht zu. Die
System.Delegate -Einschränkung ermöglicht es Ihnen, Code zu schreiben, der mit Delegaten in einer typsicheren
Weise funktioniert. Der folgende Code definiert eine Erweiterungsmethode, die zwei Delegaten kombiniert,
sofern diese vom gleichen Typ sind:

public static TDelegate TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)


where TDelegate : System.Delegate
=> Delegate.Combine(source, target) as TDelegate;

Sie können die oben dargestellte Methode verwenden, um Delegaten vom selben Typ zu kombinieren:

Action first = () => Console.WriteLine("this");


Action second = () => Console.WriteLine("that");

var combined = first.TypeSafeCombine(second);


combined();

Func<bool> test = () => true;


// Combine signature ensures combined delegates must
// have the same type.
//var badCombined = first.TypeSafeCombine(test);

Wenn Sie den Kommentar der letzten Zeile entfernen, findet die Kompilation nicht statt. Sowohl first als auch
test sind Delegattypen, aber sie sind unterschiedlich.

Enumerationseinschränkungen
Ab C# 7.3 können Sie auch den System.Enum-Typ als Basisklasseneinschränkung angeben. Die CLR lässt diese
Einschränkung immer zu, aber die C#-Sprache lässt sie nicht zu. Generics, die System.Enum verwenden, bieten
typsichere Programmierung zum Zwischenspeichern von Ergebnissen aus der Verwendung der statischen
Methoden in System.Enum . Im folgenden Beispiel werden alle gültigen Werte für einen Enumerationstyp
gefunden, und dann ein Wörterbuch erstellt, das diese Werte ihrer Zeichenfolgendarstellung zuordnet.

public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum


{
var result = new Dictionary<int, string>();
var values = Enum.GetValues(typeof(T));

foreach (int item in values)


result.Add(item, Enum.GetName(typeof(T), item));
return result;
}

Enum.GetValues und Enum.GetName nutzen Reflektion, die Auswirkungen auf die Leistung hat. Sie können
EnumNamedValues aufrufen, um eine Sammlung zu erstellen, die zwischengespeichert und wiederverwendet
wird, anstatt die Aufrufe zu wiederholen, die eine Reflektion erfordern.
Sie könnten dies wie im folgenden Beispiel gezeigt verwenden, um eine Enumeration und ein Wörterbuch der
Werte und Namen zu erstellen:

enum Rainbow
{
Red,
Orange,
Yellow,
Green,
Blue,
Indigo,
Violet
}

var map = EnumNamedValues<Rainbow>();

foreach (var pair in map)


Console.WriteLine($"{pair.Key}:\t{pair.Value}");

Siehe auch
System.Collections.Generic
C#-Programmierhandbuch
Einführung in Generics
Generische Klassen
new-Einschränkung
Generische Klassen (C#-Programmierhandbuch)
04.11.2021 • 3 minutes to read

Generische Klassen kapseln Operationen, die nicht spezifisch für einen bestimmten Datentyp sind. Generische
Klassen werden am häufigsten bei Auflistungen verwendet, z.B. bei verknüpften Listen, Hashtabellen, Stapeln,
Warteschlangen, Strukturen usw. Vorgänge wie das Hinzufügen oder Entfernen von Elementen aus der
Auflistung werden nahezu auf die gleiche Art und Weise ausgeführt, unabhängig vom Typ der gespeicherten
Daten.
Für die meisten Szenarios, die Auflistungsklassen erfordern, wird empfohlen, die in der Klassenbibliothek von
.NET bereitgestellten Auflistungsklassen zu verwenden. Weitere Informationen zur Verwendung dieser Klassen
finden Sie unter Generic Collections in .NET (Generische Auflistungen in .NET).
In der Regel erstellen Sie generische Klassen, indem Sie von einer vorhandenen konkreten Klasse ausgehen und
Typen in Typparameter ändern (einen nach dem anderen), bis das optimale Verhältnis zwischen
Verallgemeinerung und Verwendbarkeit gefunden ist. Wenn Sie eigene generische Klassen erstellen möchten,
sollten Sie die folgenden wichtigen Aspekte beachten:
Welche Typen sollen in Typparameter verallgemeinert werden.
Im Allgemeinen gilt: Je mehr Typen Sie parametrisieren können, umso flexibler und besser
wiederverwendbar ist Ihr Code. Jedoch kann ein Zuviel an Verallgemeinerung zu Code führen, der von
anderen Entwicklern nur schwer gelesen oder verstanden wird.
Welche Einschränkungen sollen ggf. auf die Typparameter angewendet werden (siehe Constraints on
Type Parameters (Einschränkungen für Typparameter)).
Es empfiehlt sich, alle Einschränkungen anzuwenden, die maximal möglich sind, und bei denen Sie
dennoch sämtliche Typen, die Sie behandeln müssen, auch behandeln können. Wenn Sie zum Beispiel
wissen, dass die generische Klasse nur mit Referenztypen verwendet werden soll, dann können Sie die
Klasseneinschränkung anwenden. Dadurch wird verhindert, dass die Klasse unbeabsichtigt mit
Werttypen verwendet wird. Gleichzeitig können Sie den Operator as für T verwenden und nach NULL-
Werten suchen.
Ob das generische Verhalten in Basisklassen und Unterklassen zerlegt werden soll.
Da generische Klassen als Basisklassen dienen können, sind hier beim Entwurf die gleichen Aspekte zu
berücksichtigen wie bei nicht generischen Klassen. Weitere Informationen bieten die Regeln zum Erben
von generischen Basisklassen weiter unten in diesem Thema.
Ob eine oder mehrere generische Schnittstellen implementiert werden soll.
Wenn Sie beispielsweise eine Klasse entwerfen, die zum Erstellen von Elementen in einer Generics-
basierten Auflistung verwendet wird, müssen Sie unter Umständen eine Schnittstelle implementieren, z.B.
IComparable<T>, wobei T der Typ Ihrer Klasse ist.
Ein Beispiel für eine einfache generische Klasse finden Sie unter Introduction to Generics (Einführung in
Generics).
Die Regeln für Einschränkungen und Typparameter haben eine Reihe von Auswirkungen auf das Verhalten einer
generischen Klasse, besonders im Hinblick auf Vererbung und Zugriff der Member. Bevor Sie fortfahren, sollten
Sie einige Begriffe verstehen. Bei einer generischen Klasse kann der Clientcode Node<T>, auf die Klasse
verweisen, indem er ein Typargument angibt, um einen geschlossenen konstruierten Typ ( Node<int> ) zu
erstellen. Die Alternative besteht darin, den Typparameter nicht anzugeben, z.B. wenn Sie eine generische
Basisklasse angeben, um einen offenen konstruierten Typ ( Node<T> ) zu erstellen. Generische Klassen können
von konkreten, geschlossenen konstruierten oder offenen konstruierten Basisklassen erben:

class BaseNode { }
class BaseNodeGeneric<T> { }

// concrete type
class NodeConcrete<T> : BaseNode { }

//closed constructed type


class NodeClosed<T> : BaseNodeGeneric<int> { }

//open constructed type


class NodeOpen<T> : BaseNodeGeneric<T> { }

Nicht generische, also konkrete Klassen können von geschlossenen konstruierten Basisklassen erben, aber nicht
von offenen konstruierten Klassen oder Typparametern, denn während der Laufzeit ist es für den Clientcode
nicht möglich, das erforderliche Typargument bereitzustellen, das zum Instanziieren der Basisklasse benötigt
wird.

//No error
class Node1 : BaseNodeGeneric<int> { }

//Generates an error
//class Node2 : BaseNodeGeneric<T> {}

//Generates an error
//class Node3 : T {}

Generische Klassen, die von offenen konstruierten Typen erben, müssen für sämtliche Basisklassen-
Typparameter, die von der erbenden Klasse nicht verwendet werden, Typargumente bereitstellen. Der folgende
Code stellt ein Beispiel dafür dar:

class BaseNodeMultiple<T, U> { }

//No error
class Node4<T> : BaseNodeMultiple<T, int> { }

//No error
class Node5<T, U> : BaseNodeMultiple<T, U> { }

//Generates an error
//class Node6<T> : BaseNodeMultiple<T, U> {}

Generische Klassen, die von offenen konstruierten Typen erben, müssen Einschränkungen angeben, die den
Einschränkungen des Basistyps übergeordnet sind oder diese implizieren:

class NodeItem<T> where T : System.IComparable<T>, new() { }


class SpecialNodeItem<T> : NodeItem<T> where T : System.IComparable<T>, new() { }

Generische Typen können mehrere Typparameter und Einschränkungen wie folgt verwenden:

class SuperKeyType<K, V, U>


where U : System.IComparable<U>
where V : new()
{ }
Offene konstruierte und geschlossene konstruierte Typen können als Methodenparameter verwendet werden:

void Swap<T>(List<T> list1, List<T> list2)


{
//code to swap items
}

void Swap(List<int> list1, List<int> list2)


{
//code to swap items
}

Wenn eine generische Klasse eine Schnittstelle implementiert, können alle Instanzen dieser Klasse in diese
Schnittstelle umgewandelt werden.
Generische Klassen sind unveränderlich. Wenn also ein Eingabeparameter eine List<BaseClass> angibt, wird
Ihnen ein Kompilierungsfehler angezeigt, falls Sie versuchen, eine List<DerivedClass> bereitzustellen.

Weitere Informationen
System.Collections.Generic
C#-Programmierhandbuch
Generics
Saving the State of Enumerators (Speichern des Zustands von Enumeratoren)
An Inheritance Puzzle, Part One (Ein Vererbungs-Puzzle, Teil 1)
Generische Schnittstellen (C#-
Programmierhandbuch)
04.11.2021 • 4 minutes to read

Es ist häufig sinnvoll, Schnittstellen entweder für generische Auflistungsklassen zu definieren oder für die
generischen Klassen, die Elemente in der Auflistung darstellen. Die Einstellung für generische Klassen ist, dass
generische Schnittstellen verwendet werden sollen, z.B. IComparable<T> anstelle von IComparable. Dadurch
werden Boxing- und Unboxingoperationen für Werttypen vermieden. Durch die .NET-Klassenbibliothek werden
einige generische Schnittstellen definiert, die zusammen mit den Sammlungsklassen im Namespace
System.Collections.Generic verwendet werden können.
Wenn als Einschränkung für einen Typparameter eine Schnittstelle angegeben ist, können nur Typen verwendet
werden, die diese Schnittstelle implementieren. Der folgende Code zeigt eine Klasse SortedList<T> , die von der
Klasse GenericList<T> abgeleitet wird. Weitere Informationen finden Sie unter Introduction to Generics
(Einführung in Generika). SortedList<T> fügt die Einschränkung where T : IComparable<T> hinzu. Dadurch kann
die Methode BubbleSort in SortedList<T> die generische Methode CompareTo für Listenelemente verwenden.
In diesem Beispiel sind Listenelemente eine einfache Klasse, Person , die IComparable<Person> implementiert.

//Type parameter T in angle brackets.


public class GenericList<T> : System.Collections.Generic.IEnumerable<T>
{
protected Node head;
protected Node current = null;

// Nested class is also generic on T


protected class Node
{
public Node next;
private T data; //T as private member datatype

public Node(T t) //T used in non-generic constructor


{
next = null;
data = t;
}

public Node Next


{
get { return next; }
set { next = value; }
}

public T Data //T as return type of property


{
get { return data; }
set { data = value; }
}
}

public GenericList() //constructor


{
head = null;
}

public void AddHead(T t) //T as method parameter type


{
Node n = new Node(t);
n.Next = head;
n.Next = head;
head = n;
}

// Implementation of the iterator


public System.Collections.Generic.IEnumerator<T> GetEnumerator()
{
Node current = head;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}

// IEnumerable<T> inherits from IEnumerable, therefore this class


// must implement both the generic and non-generic versions of
// GetEnumerator. In most cases, the non-generic method can
// simply call the generic method.
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

public class SortedList<T> : GenericList<T> where T : System.IComparable<T>


{
// A simple, unoptimized sort algorithm that
// orders list elements from lowest to highest:

public void BubbleSort()


{
if (null == head || null == head.Next)
{
return;
}
bool swapped;

do
{
Node previous = null;
Node current = head;
swapped = false;

while (current.next != null)


{
// Because we need to call this method, the SortedList
// class is constrained on IComparable<T>
if (current.Data.CompareTo(current.next.Data) > 0)
{
Node tmp = current.next;
current.next = current.next.next;
tmp.next = current;

if (previous == null)
{
head = tmp;
}
else
{
previous.next = tmp;
}
previous = tmp;
swapped = true;
}
else
{
previous = current;
current = current.next;
}
}
}
} while (swapped);
}
}

// A simple class that implements IComparable<T> using itself as the


// type argument. This is a common design pattern in objects that
// are stored in generic lists.
public class Person : System.IComparable<Person>
{
string name;
int age;

public Person(string s, int i)


{
name = s;
age = i;
}

// This will cause list elements to be sorted on age values.


public int CompareTo(Person p)
{
return age - p.age;
}

public override string ToString()


{
return name + ":" + age;
}

// Must implement Equals.


public bool Equals(Person p)
{
return (this.age == p.age);
}
}

public class Program


{
public static void Main()
{
//Declare and instantiate a new generic SortedList class.
//Person is the type argument.
SortedList<Person> list = new SortedList<Person>();

//Create name and age values to initialize Person objects.


string[] names = new string[]
{
"Franscoise",
"Bill",
"Li",
"Sandra",
"Gunnar",
"Alok",
"Hiroyuki",
"Maria",
"Alessandro",
"Raul"
};

int[] ages = new int[] { 45, 19, 28, 23, 18, 9, 108, 72, 30, 35 };

//Populate the list.


for (int x = 0; x < 10; x++)
{
list.AddHead(new Person(names[x], ages[x]));
}
//Print out unsorted list.
foreach (Person p in list)
{
System.Console.WriteLine(p.ToString());
}
System.Console.WriteLine("Done with unsorted list");

//Sort the list.


list.BubbleSort();

//Print out sorted list.


foreach (Person p in list)
{
System.Console.WriteLine(p.ToString());
}
System.Console.WriteLine("Done with sorted list");
}
}

Mehrere Schnittstellen können als Einschränkungen für einen einzelnen Typ wie folgt angegeben werden:

class Stack<T> where T : System.IComparable<T>, IEnumerable<T>


{
}

Eine Schnittstelle kann mehr als einen Typparameter wie folgt definieren:

interface IDictionary<K, V>


{
}

Für Klassen gelten die gleichen Vererbungsregeln wie für Schnittstellen:

interface IMonth<T> { }

interface IJanuary : IMonth<int> { } //No error


interface IFebruary<T> : IMonth<int> { } //No error
interface IMarch<T> : IMonth<T> { } //No error
//interface IApril<T> : IMonth<T, U> {} //Error

Generische Schnittstellen können von nicht generischen Schnittstellen erben, sofern die generische Schnittstelle
kovariant ist, also die generische Schnittstelle ausschließlich den eigenen Typparameter als Rückgabewert
verwendet. In der Klassenbibliothek von .NET Framework erbt IEnumerable<T> von IEnumerable, da
IEnumerable<T> nur T im Rückgabewert von GetEnumerator und im Eigenschaftengetter Current verwendet.
Konkrete Klassen können geschlossene konstruierte Schnittstellen wie folgt implementieren:

interface IBaseInterface<T> { }

class SampleClass : IBaseInterface<string> { }

Generische Klassen können generische Schnittstellen oder geschlossene konstruierte Schnittstellen


implementieren, sofern die Klassenparameterliste wie nachfolgend gezeigt sämtliche Argumente bereitstellt, die
von der Schnittstelle benötigt werden:
interface IBaseInterface1<T> { }
interface IBaseInterface2<T, U> { }

class SampleClass1<T> : IBaseInterface1<T> { } //No error


class SampleClass2<T> : IBaseInterface2<T, string> { } //No error

Die Regeln, die das Überladen von Methoden steuern, sind für die Methoden innerhalb von generischen Klassen,
generischen Strukturen oder generischen Schnittstellen gleich. Weitere Informationen finden Sie unter Generic
Methods (Generische Methoden).

Weitere Informationen
C#-Programmierhandbuch
Einführung in Generics
interface
Generics
Generische Methoden (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Bei einer generischen Methode handelt es sich um eine mit Typparametern deklarierte Methode, wie folgt:

static void Swap<T>(ref T lhs, ref T rhs)


{
T temp;
temp = lhs;
lhs = rhs;
rhs = temp;
}

Im folgenden Codebeispiel wird eine Möglichkeit gezeigt, die Methode mit int für das Typargument
aufzurufen:

public static void TestSwap()


{
int a = 1;
int b = 2;

Swap<int>(ref a, ref b);


System.Console.WriteLine(a + " " + b);
}

Sie können das Typargument auch weglassen, dann wird es vom Compiler abgeleitet. Der folgende Aufruf von
Swap bewirkt das gleiche wie der vorherige Aufruf:

Swap(ref a, ref b);

Für statische Methoden und Instanzmethoden gelten die gleichen Typrückschlussregeln. Der Compiler kann
Typparameter auf der Grundlage der übergebenen Methodenargumente ableiten. Eine Einschränkung oder ein
Rückgabewert genügen ihm zur Ableitung des Typparameters nicht. Infolgedessen ist ein Typrückschluss bei
Methoden ohne Parameter nicht möglich. Der Typrückschluss tritt beim Kompilieren auf, bevor der Compiler
versucht, die Signaturen von überladenen Methoden aufzulösen. Der Compiler wendet Typrückschlusslogik auf
alle generischen Methoden gleichen Namens an. Im Schritt zur Überladungsauflösung schließt der Compiler nur
die generischen Methoden ein, bei denen der Typrückschluss erfolgreich war.
Innerhalb einer generischen Klasse können nicht generische Methoden die Typparameter auf Klassenebene
folgendermaßen zugreifen:

class SampleClass<T>
{
void Swap(ref T lhs, ref T rhs) { }
}

Wenn eine generische Methode definiert wird, die die gleichen Typparameter wie die übergeordnete Klasse
akzeptiert, gibt der Compiler die Warnung CS0693 aus, weil innerhalb des Gültigkeitsbereichs der Methode das
Argument für das innere T das Argument für das innere T verdeckt. Falls Sie die Flexibilität benötigen, eine
generische Klassenmethode mit anderen als den bei der Instanziierung der Klasse bereitgestellten
Typargumenten aufrufen zu können, sollten Sie einen anderen Bezeichner für den Typparameter der Methode
erwägen, wie in GenericList2<T> im folgenden Beispiel dargestellt.

class GenericList<T>
{
// CS0693
void SampleMethod<T>() { }
}

class GenericList2<T>
{
//No warning
void SampleMethod<U>() { }
}

Verwenden Sie Einschränkungen, um spezialisiertere Operationen bei Typparametern in Methoden zu


ermöglichen. Die Version von Swap<T> , jetzt SwapIfGreater<T> genannt, kann nur mit Typargumenten
verwendet werden, die IComparable<T> implementieren.

void SwapIfGreater<T>(ref T lhs, ref T rhs) where T : System.IComparable<T>


{
T temp;
if (lhs.CompareTo(rhs) > 0)
{
temp = lhs;
lhs = rhs;
rhs = temp;
}
}

Generische Methoden können auf mehrere Typparameter überladen werden. Beispielsweise können sich
folgende Methoden alle in derselben Klasse befinden:

void DoWork() { }
void DoWork<T>() { }
void DoWork<T, U>() { }

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation.

Siehe auch
System.Collections.Generic
C#-Programmierhandbuch
Einführung in Generics
Methoden
Generics und Arrays (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

In C# 2.0 oder höher implementieren eindimensionale Arrays, die als untere Grenze 0 (null) aufweisen, IList<T>
automatisch. So können Sie generische Methoden erstellen, die zum Durchlaufen von Arrays und anderen
Auflistungstypen den gleichen Code verwenden können. Diese Technik ist vor allem zum Lesen der Daten in
Auflistungen nützlich. Die Schnittstelle IList<T> kann nicht verwendet werden, um Elemente einem Array
hinzuzufügen oder daraus zu entfernen. Bei dem Versuch, eine IList<T>-Methode wie RemoveAt für ein Array in
diesem Kontext aufzurufen, wird eine Ausnahme ausgelöst.
Das folgende Codebeispiel veranschaulicht, wie eine einzelne generische Methode, die einen IList<T>-
Eingabeparameter verwendet, sowohl eine Liste als auch ein Array (in diesem Fall ein Array mit ganzen Zahlen)
durchlaufen kann.

class Program
{
static void Main()
{
int[] arr = { 0, 1, 2, 3, 4 };
List<int> list = new List<int>();

for (int x = 5; x < 10; x++)


{
list.Add(x);
}

ProcessItems<int>(arr);
ProcessItems<int>(list);
}

static void ProcessItems<T>(IList<T> coll)


{
// IsReadOnly returns True for the array and False for the List.
System.Console.WriteLine
("IsReadOnly returns {0} for this collection.",
coll.IsReadOnly);

// The following statement causes a run-time exception for the


// array, but not for the List.
//coll.RemoveAt(4);

foreach (T item in coll)


{
System.Console.Write(item.ToString() + " ");
}
System.Console.WriteLine();
}
}

Weitere Informationen
System.Collections.Generic
C#-Programmierhandbuch
Generics
Arrays
Generics
Generische Delegaten (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Ein Delegat kann seine eigenen Typparameter definieren. Wie im folgenden Beispiel gezeigt, kann Code, der auf
den generischen Delegaten verweist, das Typargument zum Erstellen eines geschlossenen konstruierten Typs
angeben, genau wie wenn eine generische Klasse instanziiert oder eine generische Methode aufgerufen wird:

public delegate void Del<T>(T item);


public static void Notify(int i) { }

Del<int> m1 = new Del<int>(Notify);

C# 2.0 verfügt über ein neues Feature. Dieses Feature wird als Methodengruppenkonvertierung bezeichnet und
gilt sowohl für konkrete als auch für generische Delegattypen. Damit können Sie die vorhergehende Zeile mit
folgender vereinfachter Syntax schreiben:

Del<int> m2 = Notify;

Delegaten, die innerhalb einer generischen Klasse definiert sind, können die Typparameter der generischen
Klasse auf die gleiche Weise wie Klassenmethoden verwenden.

class Stack<T>
{
T[] items;
int index;

public delegate void StackDelegate(T[] items);


}

Code, der auf den Delegaten verweist, muss das Typargument der enthaltenden Klasse wie folgt angeben:

private static void DoWork(float[] items) { }

public static void TestStack()


{
Stack<float> s = new Stack<float>();
Stack<float>.StackDelegate d = DoWork;
}

Generische Delegaten sind besonders nützlich zum Definieren von Ereignissen, die auf dem typischen
Entwurfsmuster basieren, denn das Absenderargument kann mit starker Typisierung versehen werden und
muss nicht länger in das bzw. aus dem Object umgewandelt werden.
delegate void StackEventHandler<T, U>(T sender, U eventArgs);

class Stack<T>
{
public class StackEventArgs : System.EventArgs { }
public event StackEventHandler<Stack<T>, StackEventArgs> stackEvent;

protected virtual void OnStackChanged(StackEventArgs a)


{
stackEvent(this, a);
}
}

class SampleClass
{
public void HandleStackChange<T>(Stack<T> stack, Stack<T>.StackEventArgs args) { }
}

public static void Test()


{
Stack<double> s = new Stack<double>();
SampleClass o = new SampleClass();
s.stackEvent += o.HandleStackChange;
}

Weitere Informationen
System.Collections.Generic
C#-Programmierhandbuch
Einführung in Generics
Generische Methoden
Generische Klassen
Generische Schnittstellen
Delegaten
Generics
Unterschiede zwischen C++-Vorlagen und C#-
Generics (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

C#-Generics und C++-Vorlagen sind Sprachfunktionen, die Unterstützung für parametrisierte Typen
ermöglichen. Es gibt jedoch viele Unterschiede zwischen den beiden. Auf der Syntaxebene sind C#-Generics ein
einfacherer Ansatz für parametrisierte Typen ohne die Komplexität von C++-Vorlagen. Darüber hinaus versucht
C# nicht alle Funktionen bereitzustellen, die C++-Vorlagen bereitstellen. Auf der Ebene der Implementierung ist
der wichtigste Unterschied, dass C#-Ersetzungen des generischen Typs zur Laufzeit durchgeführt werden und
allgemeine Informationen für die instanziierten Objekte beibehalten werden. Weitere Informationen finden Sie
unter Generics zur Laufzeit.
Die folgenden sind die Hauptunterschiede zwischen C#-Generics und C++-Vorlagen:
C#-Generics bieten nicht die gleiche Menge an Flexibilität wie C++-Vorlagen. Es ist z.B. nicht möglich,
arithmetische Operatoren in einer generischen Klasse von C# aufzurufen, obwohl es möglich ist,
benutzerdefinierte Operatoren aufzurufen.
C# lässt keine Nichttyp-Vorlagenparameter zu, wie z.B. template C<int i> {} .
C# unterstützt keine explizite Spezialisierung; d.h. eine benutzerdefinierte Implementierung einer Vorlage
für einen bestimmten Typ.
C# unterstützt keine partielle Spezialisierung: Eine benutzerdefinierte Implementierung für eine
Teilmenge der Typargumente.
C# lässt nicht zu, dass der Typparameter als Basisklasse für den generischen Typ verwendet wird.
C# lässt nicht zu, dass Typparameter über Standardtypen verfügen.
In C# kann kein generischer Typparameter selbst generisch sein, obwohl konstruierte Typen als Generics
verwendet werden können. C++ lässt Vorlagenparameter zu.
C++ lässt Code zu, der möglicherweise nicht für alle Typparameter in der Vorlage gültig ist, die
anschließend auf den spezifischen Typ, der als Typparameter verwendet wird, geprüft wird. In C# muss
Code in einer Klasse in einer Weise geschrieben werden, dass er mit einem beliebigen Typ arbeiten kann,
der die Einschränkungen erfüllt. Zum Beispiel kann in C++ eine Funktion geschrieben werden, die die
arithmetischen Operatoren + und - für Objekte des Typparameters verwendet, was einen Fehler bei
der Instanziierung der Vorlage mit einem Typ, der diese Operatoren nicht unterstützt, erzeugt. C# lässt
dies zu; die einzigen zulässigen Sprachkonstrukte sind die, die von den Einschränkungen abgeleitet
werden können.

Weitere Informationen
C#-Programmierhandbuch
Einführung in Generics
Vorlagen
Generics zur Laufzeit (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Beim Kompilieren eines generischen Typs oder einer generischen Methode in Microsoft Intermediate Language
(MSIL) wird über Metadaten auf das Vorkommen von Typparametern hingewiesen. Die Art der Verwendung von
MSIL für einen generischen Typ hängt davon ab, ob es sich bei dem übergebenen Typparameter um einen
Werttyp oder einen Referenztyp handelt.
Wenn das erste Mal ein generischer Typ mit einem Werttyp als Parameter erstellt wird, erstellt die Laufzeit einen
spezialisierten generischen Typ, bei dem übergebene Parameter an den entsprechenden Stellen in MSIL ersetzt
werden. Spezialisierte generische Typen werden für jeden eindeutigen Werttyp, der als Parameter verwendet
wird, einmal erstellt.
Angenommen, im Programmcode wurde ein Stapel deklariert, der aus ganzen Zahlen erstellt wurde:

Stack<int> stack;

An diesem Punkt generiert die Laufzeit eine spezialisierte Version der Klasse Stack<T>, in der der Parameter
durch die entsprechende ganze Zahl ersetzt wird. Bei Verwendung eines Stapels aus ganzen Zahlen wird jetzt
immer die generierte spezialisierte Klasse Stack<T> verwendet. Im folgenden Beispiel werden zwei Instanzen
eines Stapels aus ganzen Zahlen erstellt, die eine Instanz des Stack<int> -Codes gemeinsam nutzen:

Stack<int> stackOne = new Stack<int>();


Stack<int> stackTwo = new Stack<int>();

Angenommen, dass an anderer Stelle im Code jedoch eine weitere Stack<T>-Klasse erstellt wird, mit einem
anderen Werttyp, z.B. long , oder einer benutzerdefinierten Struktur als Parameter. Daraufhin generiert die
Laufzeit eine andere Version des generischen Typs und ersetzt long an den entsprechenden Stellen in MSIL.
Konvertierungen sind nicht mehr notwendig, da jede spezialisierte generische Klasse den Werttyp nativ enthält.
Bei Referenztypen unterscheidet sich die Funktionsweise von Generics geringfügig. Wenn das erste Mal ein
generischer Typ mit einem beliebigem Referenztyp erstellt wird, erstellt die Laufzeit einen spezialisierten
generischen Typ, bei dem die Parameter durch Objektverweise in MSIL ersetzt werden. Wenn jetzt ein
konstruierter Typ mit einem Referenztyp als Parameter instanziiert wird (unabhängig davon, um welchen Typ es
sich dabei handelt), wird die zuvor erstellte spezialisierte Version des generischen Typs verwendet. Dies ist
möglich, da alle Verweise die gleiche Größe haben.
Angenommen, Sie verfügen über zwei Referenztypen, eine Customer -Klasse und eine Order -Klasse, und Sie
haben einen Stapel von Customer -Typen erstellt:

class Customer { }
class Order { }

Stack<Customer> customers;

An dieser Stelle generiert die Laufzeit eine spezialisierte Version der Klasse Stack<T>, die anstelle von Daten
Objektverweise speichert, die zu einem späteren Zeitpunkt mit Daten gefüllt werden. Angenommen, die nächste
Codezeile erstellt einen Stapel eines anderen Referenztyps mit dem Namen Order :
Stack<Order> orders = new Stack<Order>();

Anders als bei Werttypen wird für den Typ Stack<T> keine weitere spezialisierte Version der Klasse Order
erstellt. Stattdessen wird eine Instanz der spezialisierten Version der Klasse Stack<T> erstellt und die Variable
orders so festgelegt, dass sie darauf verweist. Angenommen, Sie würden dann auf eine Codezeile stoßen, die
einen Stapel des Typs Customer erstellt:

customers = new Stack<Customer>();

Wie auch bei der vorherigen Verwendung der mit dem Typ Order erstellten Klasse Stack<T> wird eine weitere
Instanz der spezialisierten Klasse Stack<T> erstellt. Die darin enthaltenen Zeiger werden so festgelegt, dass sie
auf einen Arbeitsspeicherbereich von der Größe eines Customer -Typs verweisen. Da die Anzahl der
Referenztypen von Programm zu Programm sehr unterschiedlich sein kann, wird bei der C#-Implementierung
von Generics eine übermäßige Zunahme des Codeumfangs dadurch verhindert, dass die Anzahl der
spezialisierten Klassen, die vom Compiler für generische Klassen von Referenztypen erstellt werden, auf eine
reduziert wird.
Weiterhin gilt, dass eine mit einem Werttyp- oder Referenztypparameter instanziierte generische C#-Klasse zur
Laufzeit mittels Reflektion abgefragt werden kann. Dabei können sowohl der tatsächliche Typ als auch der
Typparameter ermittelt werden.

Weitere Informationen
System.Collections.Generic
C#-Programmierhandbuch
Einführung in Generics
Generics
Generics und Reflektion (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Da die Common Language Runtime (CLR) Zugriff auf generische Typinformationen zur Laufzeit verfügt, können
Sie die Reflektion zum Abrufen von Informationen über generische Typen genauso wie für nicht generische
Typen verwenden. Weitere Informationen finden Sie unter Generics zur Laufzeit.
In .NET Framework 2.0 wurden mehrere neue Member zur Type-Klasse hinzugefügt, um Laufzeitinformationen
für generische Typen zu ermöglichen. Weitere Informationen zur Verwendung dieser Methoden und
Eigenschaften finden Sie in der Dokumentation zu diesen Klassen. Der System.Reflection.Emit-Namespace
enthält auch neue Member, die Generics unterstützen. Weitere Informationen finden Sie unter How to:
Definieren eines generischen Typs mit Reflektionsausgabe.
Eine Liste der invarianten Bedingungen für Begriffe, für Begriffe, die für die Reflektion mit generischen
Methoden verwendet werden, finden Sie in den Hinweisen zur Eigenschaft IsGenericType.

SY ST EM . T Y P E- M EM B ERN A M E B ESC H REIB UN G

IsGenericType Gibt TRUE zurück, wenn ein Typ generisch ist

GetGenericArguments Gibt ein Array von Type -Objekten zurück, die die
bereitgestellten Typargumente für einen konstruierten Typ
oder die Typparameter einer generischen Typdefinition
darstellen

GetGenericTypeDefinition Gibt die zugrunde liegende generische Typdefinition für den


aktuellen konstruierten Typ zurück

GetGenericParameterConstraints Gibt ein Array von Type -Objekten zurück, die die
Einschränkungen für den aktuellen generischen
Typparameter darstellen.

ContainsGenericParameters Gibt TRUE zurück, wenn der Typ oder einer seiner
einschließenden Typen oder Methoden Typparameter
enthalten, für die keine bestimmten Typen angegeben
wurden

GenericParameterAttributes Ruft eine Kombination von GenericParameterAttributes -


Flags ab, die die speziellen Einschränkungen des aktuellen
generischen Typparameters beschreiben

GenericParameterPosition Ruft für ein Type -Objekt, das einen Typparameter darstellt,
die Position des Typparameters in der Typparameterliste des
generischen Typs oder der generischen Methode, die den
Typparameter deklariert

IsGenericParameter Ruft einen Wert ab, der angibt, ob der aktuelle Type einen
Typparameter einer generischen Typ- oder
Methodendefinition darstellt
SY ST EM . T Y P E- M EM B ERN A M E B ESC H REIB UN G

IsGenericTypeDefinition Ruft einen Wert ab, der angibt, ob der aktuelle Type eine
generische Typdefinition darstellt, aus der andere generische
Typen konstruiert werden können. Gibt TRUE zurück, wenn
der Typ die Definition eines generischen Typs darstellt

DeclaringMethod Gibt die generische Methode, die den aktuellen generischen


Typparameter definiert, oder null zurück, wenn der
Typparameter nicht von einer generischen Methode definiert
wurde

MakeGenericType Ersetzt die Typparameter der aktuellen generischen


Typdefinition durch die Elemente eines Arrays von Typen und
gibt ein Type-Objekt zurück, das den resultierenden
konstruierten Typ darstellt.

Zusätzlich ermöglichen neue Member der MethodInfo-Klasse Laufzeitinformationen für generische Methoden.
Eine Liste der invarianten Bedingungen für Begriffe, die für die Reflektion mit generischen Methoden verwendet
werden, finden Sie unter den Hinweisen zur Eigenschaft IsGenericMethod.

SY ST EM . REF L EC T IO N . M EM B ERIN F O - M EM B ERN A M E B ESC H REIB UN G

IsGenericMethod Gibt TRUE zurück, wenn eine Methode generisch ist

GetGenericArguments Gibt ein Array von Type-Objekten zurück, die die


Typargumente einer konstruierten generischen Methode
oder die Typparameter einer generischen
Methodendefinition darstellen

GetGenericMethodDefinition Gibt die zugrunde liegende generische Methodendefinition


für die aktuelle konstruierte Methode zurück

ContainsGenericParameters Gibt TRUE zurück, wenn die Methode oder einer ihrer
einschließenden Typen Typparameter enthält, für die keine
bestimmten Typen angegeben wurden

IsGenericMethodDefinition Gibt TRUE zurück, wenn die aktuelle MethodInfo die


Definition eines generischen Typs darstellt

MakeGenericMethod Ersetzt die Typparameter der aktuellen generischen


Methodendefinition durch die Elemente eines Arrays von
Typen und gibt ein MethodInfo-Objekt zurück, das die sich
ergebende konstruierte Methode darstellt.

Siehe auch
C#-Programmierhandbuch
Generics
Reflektion und generische Typen
Generics
Generics und Attribute (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Das Anwenden von Attributen auf generische Typen erfolgt in derselben Weise wie auf nicht generische Typen.
Weitere Informationen zum Anwenden von Attributen finden Sie unter Attribute.
Benutzerdefinierte Attribute dürfen nur auf offene generische Typen (generische Typen, für die keine
Typargumente bereitgestellt werden) und auf geschlossene konstruierte generische Typen (generische Typen,
die Argumente für alle Typparameter bereitstellen) verweisen.
In den folgenden Beispielen wird dieses benutzerdefinierte Attribut verwendet:

class CustomAttribute : System.Attribute


{
public System.Object info;
}

Ein Attribut kann auf einen offenen generischen Typ verweisen:

public class GenericClass1<T> { }

[CustomAttribute(info = typeof(GenericClass1<>))]
class ClassA { }

Geben Sie mehrere Typparameter mit der entsprechenden Anzahl von Kommas an. In diesem Beispiel verfügt
GenericClass2 über zwei Typparameter:

public class GenericClass2<T, U> { }

[CustomAttribute(info = typeof(GenericClass2<,>))]
class ClassB { }

Ein Attribut kann auf einen geschlossenen, konstruierten generischen Typ verweisen:

public class GenericClass3<T, U, V> { }

[CustomAttribute(info = typeof(GenericClass3<int, double, string>))]


class ClassC { }

Ein Attribut, das auf einen generischen Typparameter verweist, verursacht einen Kompilierungsfehler:

//[CustomAttribute(info = typeof(GenericClass3<int, T, string>))] //Error


class ClassD<T> { }

Ein generischer Typ kann nicht von Attribute erben:

//public class CustomAtt<T> : System.Attribute {} //Error

Um zur Laufzeit Informationen zu einem generischen Typ oder Typparameter abzurufen, können Sie die
Methoden von System.Reflection verwenden. Weitere Informationen finden Sie unter Generics und Reflektion.
Weitere Informationen
C#-Programmierhandbuch
Generics
Attribute
Das Dateisystem und die Registrierung (C#-
Programmierhandbuch)
04.11.2021 • 2 minutes to read

Die folgenden Artikel zeigen die Verwendung von C# und dem .NET zum Ausführen verschiedener
grundlegender Vorgänge in Dateien, Ordnern und der Registrierung.

In diesem Abschnitt
T IT EL B ESC H REIB UN G

Durchlaufen einer Verzeichnisstruktur Zeigt, wie eine Verzeichnisstruktur manuell durchlaufen wird.

Abrufen von Informationen über Dateien, Ordner und Zeigt, wie Sie Informationen zu Dateien, Ordnern und
Laufwerke Laufwerken abrufen, beispielsweise die Uhrzeit der Erstellung
und die Größe.

Erstellen einer Datei oder eines Ordners Zeigt, wie Sie eine neue Datei oder einen neuen Ordner
erstellen.

Kopieren, Löschen und Verschieben von Dateien und Zeigt, wie Sie Dateien und Ordner kopieren, löschen und
Ordnern (C#-Programmierhandbuch) verschieben.

Bereitstellen eines Statusdialogfelds für Dateioperationen Zeigt, wie Sie ein standardmäßiges Windows-
Fortschrittsdialogfeld für bestimmte Dateivorgänge
anzeigen.

Schreiben in eine Textdatei Zeigt, wie Sie in eine Textdatei schreiben.

Lesen aus einer Textdatei Zeigt, wie Sie aus einer Textdatei lesen.

Zeilenweises Lesen einer Textdatei Zeigt, wie Sie Text zeilenweise aus einer Datei abrufen.

Erstellen eines Schlüssels in der Registrierung Zeigt, wie Sie einen Schlüssel in die Systemregistrierung
schreiben.

Verwandte Abschnitte
Datei- und Stream-E/A
Kopieren, Löschen und Verschieben von Dateien und Ordnern (C#-Programmierhandbuch)
C#-Programmierhandbuch
System.IO
Vorgehensweise: Durchlaufen einer
Verzeichnisstruktur (C#-Programmierleitfaden)
04.11.2021 • 6 minutes to read

Der Ausdruck „Durchlaufen einer Verzeichnisstruktur “ bedeutet, dass auf jede Datei in jedem verschachtelten
Unterverzeichnis in einem angegebenen Stammordner in einer beliebigen Tiefe zugegriffen wird. Sie müssen
nicht unbedingt jede Datei öffnen. Sie können einfach den Namen der Datei oder dem Unterverzeichnis als
string abrufen, oder Sie können zusätzliche Informationen eines System.IO.FileInfo oder
System.IO.DirectoryInfo-Objekts abrufen.

NOTE
In Windows sind die Begriffe „Verzeichnis“ und „Ordner “ austauschbar. Die meisten Dokumentationen und der Text der
Benutzeroberfläche verwenden den Begriff „Ordner “, aber die .NET-Klassenbibliotheken verwenden den Begriff
„Verzeichnis“.

Im einfachsten Fall, in dem Sie ganz sicher sind, dass Sie über die Zugriffsberechtigungen für alle Verzeichnisse
in einem angegebenen Stamm verfügen, können Sie das System.IO.SearchOption.AllDirectories -Flag
verwenden. Dieses Flag gibt alle geschachtelten Unterverzeichnisse zurück, die mit dem angegebenen Muster
übereinstimmen. Im folgenden Beispiel wird die Verwendung dieses Flags veranschaulicht.

root.GetDirectories("*.*", System.IO.SearchOption.AllDirectories);

Der Schwachpunkt bei diesem Ansatz ist, dass die Methode fehlschlägt und keine Verzeichnisse zurückgibt,
wenn eines der Unterverzeichnisse im angegebenen Stamm eine DirectoryNotFoundException oder
UnauthorizedAccessException bewirkt. Dasselbe gilt bei Verwendung der GetFiles-Methode. Wenn Sie diese
Ausnahmen für bestimmte Unterverzeichnisse behandeln müssen, müssen Sie die Verzeichnisstruktur manuell
durchlaufen, wie in den folgenden Beispielen gezeigt.
Wenn Sie eine Verzeichnisstruktur manuell durchlaufen, können Sie zuerst die Dateien (Durchlauf vor der
Sortierung) oder die Unterverzeichnisse (Durchlauf nach der Sortierung) behandeln. Wenn Sie einen Durchlauf
vor der Sortierung ausführen, besuchen Sie Dateien direkt unter diesem Ordner selbst und durchlaufen dann
die gesamte Struktur unter dem aktuellen Ordner. Der Durchlauf nach der Sortierung erfolgt umgekehrt, wobei
Sie die gesamte untergeordnete Struktur durchlaufen, bevor Sie zu den Dateien des aktuellen Ordners gelangen.
In den Beispielen weiter unten in diesem Dokument wird ein Durchlauf vor der Sortierung ausgeführt, aber Sie
können die Beispiele problemlos abändern, um einen Durchlauf nach der Sortierung auszuführen.
Eine andere Option ist, entweder Rekursion oder einen stapelbasierten Durchlauf zu verwenden. In den
Beispielen weiter unten in diesem Dokument werden beide Ansätze gezeigt.
Wenn Sie mehrere Vorgänge für Dateien und Ordner ausführen müssen, können Sie diese Beispiele
modularisieren, indem Sie den Vorgang in separate Funktionen umgestalten, die Sie mit einem einzelnen
Delegaten aufrufen können.
NOTE
NTFS-Dateisysteme können Analysepunkte in Form von Verknüpfungspunkten, symbolischen Verknüpfungen und festen
Links enthalten. .NET wie z. B. GetFiles und GetDirectories geben keine Unterverzeichnisse unter einem Analysepunkt
zurück. Dieses Verhalten schützt vor dem Risiko einer Endlosschleife, wenn zwei Analysepunkte aufeinander verweisen. Im
Allgemeinen sollten Sie bei Analysepunkten äußerst vorsichtig sein, um sicherzustellen, dass Sie nicht versehentlich
Dateien ändern oder löschen. Wenn Sie genaue Kontrolle über Analysepunkte benötigen, verwenden Sie einen
Plattformaufruf oder nativen Code, um die entsprechenden Win32-Dateisystemmethoden direkt aufzurufen.

Beispiele
Das folgende Beispiel zeigt, wie Sie eine Verzeichnisstruktur mit Rekursion direkt durchlaufen. Der rekursive
Ansatz ist elegant, kann aber eine Stapelüberlaufausnahme verursachen, wenn die Verzeichnisstruktur groß und
tief verschachtelt ist.
Die besonderen Ausnahmen, die verarbeitet werden, und die besonderen Aktionen, die für jede Datei und jeden
Ordner ausgeführt werden, werden nur als Beispiele angegeben. Sie sollten diesen Code für Ihre speziellen
Anforderungen ändern. Weitere Informationen finden Sie in den Kommentaren im Code.

public class RecursiveFileSearch


{
static System.Collections.Specialized.StringCollection log = new
System.Collections.Specialized.StringCollection();

static void Main()


{
// Start with drives if you have to search the entire computer.
string[] drives = System.Environment.GetLogicalDrives();

foreach (string dr in drives)


{
System.IO.DriveInfo di = new System.IO.DriveInfo(dr);

// Here we skip the drive if it is not ready to be read. This


// is not necessarily the appropriate action in all scenarios.
if (!di.IsReady)
{
Console.WriteLine("The drive {0} could not be read", di.Name);
continue;
}
System.IO.DirectoryInfo rootDir = di.RootDirectory;
WalkDirectoryTree(rootDir);
}

// Write out all the files that could not be processed.


Console.WriteLine("Files with restricted access:");
foreach (string s in log)
{
Console.WriteLine(s);
}
// Keep the console window open in debug mode.
Console.WriteLine("Press any key");
Console.ReadKey();
}

static void WalkDirectoryTree(System.IO.DirectoryInfo root)


{
System.IO.FileInfo[] files = null;
System.IO.DirectoryInfo[] subDirs = null;

// First, process all the files directly under this folder


try
{
{
files = root.GetFiles("*.*");
}
// This is thrown if even one of the files requires permissions greater
// than the application provides.
catch (UnauthorizedAccessException e)
{
// This code just writes out the message and continues to recurse.
// You may decide to do something different here. For example, you
// can try to elevate your privileges and access the file again.
log.Add(e.Message);
}

catch (System.IO.DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
}

if (files != null)
{
foreach (System.IO.FileInfo fi in files)
{
// In this example, we only access the existing FileInfo object. If we
// want to open, delete or modify the file, then
// a try-catch block is required here to handle the case
// where the file has been deleted since the call to TraverseTree().
Console.WriteLine(fi.FullName);
}

// Now find all the subdirectories under this directory.


subDirs = root.GetDirectories();

foreach (System.IO.DirectoryInfo dirInfo in subDirs)


{
// Resursive call for each subdirectory.
WalkDirectoryTree(dirInfo);
}
}
}
}

Im folgenden Beispiel wird gezeigt, wie Sie Dateien und Ordner in einer Verzeichnisstruktur ohne Rekursion
durchlaufen. Diese Technik verwendet den generischen Stack<T>-Auflistungstyp, bei dem es sich um einen
LIFO-Stapel handelt.
Die besonderen Ausnahmen, die verarbeitet werden, und die besonderen Aktionen, die für jede Datei und jeden
Ordner ausgeführt werden, werden nur als Beispiele angegeben. Sie sollten diesen Code für Ihre speziellen
Anforderungen ändern. Weitere Informationen finden Sie in den Kommentaren im Code.

public class StackBasedIteration


{
static void Main(string[] args)
{
// Specify the starting folder on the command line, or in
// Visual Studio in the Project > Properties > Debug pane.
TraverseTree(args[0]);

Console.WriteLine("Press any key");


Console.ReadKey();
}

public static void TraverseTree(string root)


{
// Data structure to hold names of subfolders to be
// examined for files.
Stack<string> dirs = new Stack<string>(20);
if (!System.IO.Directory.Exists(root))
{
throw new ArgumentException();
}
dirs.Push(root);

while (dirs.Count > 0)


{
string currentDir = dirs.Pop();
string[] subDirs;
try
{
subDirs = System.IO.Directory.GetDirectories(currentDir);
}
// An UnauthorizedAccessException exception will be thrown if we do not have
// discovery permission on a folder or file. It may or may not be acceptable
// to ignore the exception and continue enumerating the remaining files and
// folders. It is also possible (but unlikely) that a DirectoryNotFound exception
// will be raised. This will happen if currentDir has been deleted by
// another application or thread after our call to Directory.Exists. The
// choice of which exceptions to catch depends entirely on the specific task
// you are intending to perform and also on how much you know with certainty
// about the systems on which this code will run.
catch (UnauthorizedAccessException e)
{
Console.WriteLine(e.Message);
continue;
}
catch (System.IO.DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}

string[] files = null;


try
{
files = System.IO.Directory.GetFiles(currentDir);
}

catch (UnauthorizedAccessException e)
{

Console.WriteLine(e.Message);
continue;
}

catch (System.IO.DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}
// Perform the required action on each file here.
// Modify this block to perform your required task.
foreach (string file in files)
{
try
{
// Perform whatever action is required in your scenario.
System.IO.FileInfo fi = new System.IO.FileInfo(file);
Console.WriteLine("{0}: {1}, {2}", fi.Name, fi.Length, fi.CreationTime);
}
catch (System.IO.FileNotFoundException e)
{
// If file was deleted by a separate application
// or thread since the call to TraverseTree()
// then just continue.
Console.WriteLine(e.Message);
continue;
}
}

// Push the subdirectories onto the stack for traversal.


// This could also be done before handing the files.
foreach (string str in subDirs)
dirs.Push(str);
}
}
}

Es ist im Allgemeinen zu zeitaufwändig, bei allen Ordnern zu testen, ob die Anwendung über die Berechtigung
zum Öffnen verfügt. Im Codebeispiel wird daher nur dieser Teil der Operation in einen try/catch -Block
eingeschlossen. Sie können den catch -Block ändern, sodass Sie bei verweigertem Zugriff auf einen Ordner
versuchen, Ihre Berechtigungen zu erhöhen, und dann erneut darauf zugreifen. In der Regel sollten Sie nur die
Ausnahmen abfangen, die Sie behandeln können, ohne Ihre Anwendung in einem unbekannten Status zu lassen.
Wenn Sie den Inhalt einer Verzeichnisstruktur entweder im Arbeitsspeicher oder auf dem Datenträger speichern
müssen, speichern Sie am besten nur die FullName-Eigenschaft (vom Typ string ) für jede Datei. Anschließend
können Sie diese Zeichenfolge nach Bedarf zum Erstellen eines neuen FileInfo- oder DirectoryInfo-Objekts
verwenden, oder eine beliebige Datei öffnen, für die zusätzliche Verarbeitung erforderlich ist.

Stabile Programmierung
Bei stabilem Dateiiterationscode müssen viele komplexe Zusammenhänge des Dateisystems berücksichtigt
werden. Weitere Informationen zum Windows-Dateisystem finden Sie in der Übersicht zu NTFS.

Siehe auch
System.IO
LINQ und Dateiverzeichnisse
Das Dateisystem und die Registrierung (C#-Programmierhandbuch)
Vorgehensweise: Abrufen von Informationen zu
Dateien, Ordnern und Laufwerken (C#-
Programmierleitfaden)
04.11.2021 • 2 minutes to read

Sie können in .NET auf die Dateisysteminformationen mithilfe folgender Klassen zugreifen:
System.IO.FileInfo
System.IO.DirectoryInfo
System.IO.DriveInfo
System.IO.Directory
System.IO.File
Die Klassen FileInfo und DirectoryInfo stehen für eine Datei oder ein Verzeichnis, und sie enthalten
Eigenschaften, die viele der Dateiattribute offen legen, die vom NTFS-Dateisystem unterstützt werden. Sie
enthalten zusätzlich Methoden zum Öffnen, Schließen, Bewegen und Löschen von Dateien und Ordnern. Sie
können Instanzen dieser Klassen erstellen, indem Sie eine Zeichenfolge in den Konstruktor übergeben, die den
Namen der Datei, des Ordners oder des Laufwerks repräsentiert:

System.IO.DriveInfo di = new System.IO.DriveInfo(@"C:\");

Sie können auch die Namen von Dateien, Ordnern oder Laufwerken abrufen, indem Sie Aufrufe auf
DirectoryInfo.GetDirectories, DirectoryInfo.GetFiles und DriveInfo.RootDirectory verwenden.
Die Klassen System.IO.Directory und System.IO.File bieten statische Methoden zum Abrufen von Informationen
über Verzeichnisse und Dateien.

Beispiel
Das folgende Beispiel veranschaulicht verschiedene Arten des Zugriffs auf Datei- und Ordnerinformationen.

class FileSysInfo
{
static void Main()
{
// You can also use System.Environment.GetLogicalDrives to
// obtain names of all logical drives on the computer.
System.IO.DriveInfo di = new System.IO.DriveInfo(@"C:\");
Console.WriteLine(di.TotalFreeSpace);
Console.WriteLine(di.VolumeLabel);

// Get the root directory and print out some information about it.
System.IO.DirectoryInfo dirInfo = di.RootDirectory;
Console.WriteLine(dirInfo.Attributes.ToString());

// Get the files in the directory and print out some information about them.
System.IO.FileInfo[] fileNames = dirInfo.GetFiles("*.*");

foreach (System.IO.FileInfo fi in fileNames)


{
Console.WriteLine("{0}: {1}: {2}", fi.Name, fi.LastAccessTime, fi.Length);
Console.WriteLine("{0}: {1}: {2}", fi.Name, fi.LastAccessTime, fi.Length);
}

// Get the subdirectories directly that is under the root.


// See "How to: Iterate Through a Directory Tree" for an example of how to
// iterate through an entire tree.
System.IO.DirectoryInfo[] dirInfos = dirInfo.GetDirectories("*.*");

foreach (System.IO.DirectoryInfo d in dirInfos)


{
Console.WriteLine(d.Name);
}

// The Directory and File classes provide several static methods


// for accessing files and directories.

// Get the current application directory.


string currentDirName = System.IO.Directory.GetCurrentDirectory();
Console.WriteLine(currentDirName);

// Get an array of file names as strings rather than FileInfo objects.


// Use this method when storage space is an issue, and when you might
// hold on to the file name reference for a while before you try to access
// the file.
string[] files = System.IO.Directory.GetFiles(currentDirName, "*.txt");

foreach (string s in files)


{
// Create the FileInfo object only when needed to ensure
// the information is as current as possible.
System.IO.FileInfo fi = null;
try
{
fi = new System.IO.FileInfo(s);
}
catch (System.IO.FileNotFoundException e)
{
// To inform the user and continue is
// sufficient for this demonstration.
// Your application may require different behavior.
Console.WriteLine(e.Message);
continue;
}
Console.WriteLine("{0} : {1}",fi.Name, fi.Directory);
}

// Change the directory. In this case, first check to see


// whether it already exists, and create it if it does not.
// If this is not appropriate for your application, you can
// handle the System.IO.IOException that will be raised if the
// directory cannot be found.
if (!System.IO.Directory.Exists(@"C:\Users\Public\TestFolder\"))
{
System.IO.Directory.CreateDirectory(@"C:\Users\Public\TestFolder\");
}

System.IO.Directory.SetCurrentDirectory(@"C:\Users\Public\TestFolder\");

currentDirName = System.IO.Directory.GetCurrentDirectory();
Console.WriteLine(currentDirName);

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
Stabile Programmierung
Beim Verarbeiten von benutzerdefinierten Pfadzeichenfolgen, sollten Sie auch die Ausnahmen für folgende
Bedingungen bearbeiten:
Der Dateiname ist falsch formatiert. Er enthält beispielsweise ungültige Zeichen oder besteht nur aus
Leerzeichen.
Der Dateiname ist NULL.
Der Dateiname übersteigt die vom System definierte Maximallänge.
Der Dateiname enthält einen Doppelpunkt (:).
Wenn die Anwendung nicht die notwendigen Leseberechtigungen für die angegebene Datei hat, gibt die
Exists -Methode false zurück, unabhängig davon, ob ein Pfad vorhanden ist; die Methode löst keine
Ausnahme aus.

Siehe auch
System.IO
C#-Programmierhandbuch
Das Dateisystem und die Registrierung (C#-Programmierhandbuch)
Vorgehensweise: Erstellen einer Datei oder eines
Ordners (C#-Programmierleitfaden)
04.11.2021 • 3 minutes to read

Sie können einen Ordner auf dem Computer programmgesteuert erstellen, einen Unterordner erstellen, eine
Datei im Unterordner erstellen und Daten in die Datei schreiben.

Beispiel
public class CreateFileOrFolder
{
static void Main()
{
// Specify a name for your top-level folder.
string folderName = @"c:\Top-Level Folder";

// To create a string that specifies the path to a subfolder under your


// top-level folder, add a name for the subfolder to folderName.
string pathString = System.IO.Path.Combine(folderName, "SubFolder");

// You can write out the path name directly instead of using the Combine
// method. Combine just makes the process easier.
string pathString2 = @"c:\Top-Level Folder\SubFolder2";

// You can extend the depth of your path if you want to.
//pathString = System.IO.Path.Combine(pathString, "SubSubFolder");

// Create the subfolder. You can verify in File Explorer that you have this
// structure in the C: drive.
// Local Disk (C:)
// Top-Level Folder
// SubFolder
System.IO.Directory.CreateDirectory(pathString);

// Create a file name for the file you want to create.


string fileName = System.IO.Path.GetRandomFileName();

// This example uses a random string for the name, but you also can specify
// a particular name.
//string fileName = "MyNewFile.txt";

// Use Combine again to add the file name to the path.


pathString = System.IO.Path.Combine(pathString, fileName);

// Verify the path that you have constructed.


Console.WriteLine("Path to my file: {0}\n", pathString);

// Check that the file doesn't already exist. If it doesn't exist, create
// the file and write integers 0 - 99 to it.
// DANGER: System.IO.File.Create will overwrite the file if it already exists.
// This could happen even with random file names, although it is unlikely.
if (!System.IO.File.Exists(pathString))
{
using (System.IO.FileStream fs = System.IO.File.Create(pathString))
{
for (byte i = 0; i < 100; i++)
{
fs.WriteByte(i);
}
}
}
}
else
{
Console.WriteLine("File \"{0}\" already exists.", fileName);
return;
}

// Read and display the data from your file.


try
{
byte[] readBuffer = System.IO.File.ReadAllBytes(pathString);
foreach (byte b in readBuffer)
{
Console.Write(b + " ");
}
Console.WriteLine();
}
catch (System.IO.IOException e)
{
Console.WriteLine(e.Message);
}

// Keep the console window open in debug mode.


System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
// Sample output:

// Path to my file: c:\Top-Level Folder\SubFolder\ttxvauxe.vv0

//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
//30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
// 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 8
//3 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
}

Falls der Ordner bereits vorhanden ist, führt CreateDirectory keine Aktion aus, und es wird keine Ausnahme
ausgelöst. Allerdings wird mit File.Create eine vorhandene Datei durch eine neue Datei ersetzt. Im Beispiel wird
eine if - else -Anweisung verwendet, um zu verhindern, dass eine vorhandene Datei ersetzt wird.
Wenn Sie die folgenden Änderungen im Beispiel vornehmen, können Sie, je nachdem, ob eine Datei mit einem
bestimmten Namen bereits vorhanden ist, unterschiedliche Ergebnisse erzeugen. Wenn eine solche Datei nicht
vorhanden ist, erstellt der Code sie. Wenn eine solche Datei vorhanden ist, fügt der Code Daten an diese Datei
an.
Geben Sie einen nicht zufälligen Dateinamen an.

// Comment out the following line.


//string fileName = System.IO.Path.GetRandomFileName();

// Replace that line with the following assignment.


string fileName = "MyNewFile.txt";

Ersetzen Sie im folgenden Code die if - else -Anweisung durch die using -Anweisung.
using (System.IO.FileStream fs = new System.IO.FileStream(pathString, FileMode.Append))
{
for (byte i = 0; i < 100; i++)
{
fs.WriteByte(i);
}
}

Führen Sie das Beispiel mehrmals aus, um zu überprüfen, dass der Datei jedes Mal Daten hinzugefügt werden.
Weitere FileMode -Werte, die Sie ausprobieren können, finden Sie unter FileMode.
Die folgenden Bedingungen können einen Ausnahmefehler verursachen:
Der Ordnername ist falsch formatiert. Er enthält beispielsweise unzulässige Zeichen oder besteht nur aus
Leerzeichen (ArgumentException-Klasse). Verwenden Sie die Path-Klasse, um gültige Pfadnamen zu
erstellen.
Der übergeordnete Ordner des zu erstellenden Ordners ist schreibgeschützt (IOException-Klasse).
Der Ordnername ist null (ArgumentNullException-Klasse).
Der Ordnername ist zu lang (PathTooLongException-Klasse).
Der Ordnername besteht nur aus einem Doppelpunkt ":" (PathTooLongException-Klasse).

.NET-Sicherheit
Eine Instanz der SecurityException-Klasse kann in nur teilweise vertrauenswürdigen Umgebungen ausgelöst
werden.
Wenn Sie keine Berechtigung zum Erstellen des Ordners haben, wird in dem Beispiel eine Instanz der
UnauthorizedAccessException-Klasse ausgelöst.

Siehe auch
System.IO
C#-Programmierhandbuch
Das Dateisystem und die Registrierung (C#-Programmierhandbuch)
Vorgehensweise: Kopieren, Löschen und
Verschieben von Dateien und Ordnern (C#-
Programmierleitfaden)
04.11.2021 • 3 minutes to read

Die folgenden Beispiele veranschaulichen, wie Dateien und Ordner mithilfe der Klassen System.IO.File,
System.IO.Directory, System.IO.FileInfo und System.IO.DirectoryInfo aus dem Namespace System.IO synchron
kopiert, verschoben und gelöscht werden können. Diese Beispiele stellen keine Statusanzeige oder irgendeine
andere Benutzeroberfläche bereit. Informationen zur Bereitstellung eines standardmäßigen
Fortschrittsdialogfelds finden Sie unter Vorgehensweise: Bereitstellen eines Statusdialogfelds für
Dateioperationen.
Verwenden Sie System.IO.FileSystemWatcher zum Bereitstellen von Ereignissen, mit denen Sie bei Vorgängen
auf mehreren Dateien den Status berechnen können. Eine weitere Möglichkeit ist die Verwendung eines
Plattformaufrufs, um die relevanten dateibezogenen Methoden in der Windows-Shell aufzurufen. Weitere
Informationen dazu, wie Sie diese Dateivorgänge asynchron ausführen, finden Sie unter Asynchrone Datei-E/A.

Beispiele
Im folgenden Beispiel wird gezeigt, wie Sie Dateien und Verzeichnisse kopieren.
// Simple synchronous file copy operations with no user interface.
// To run this sample, first create the following directories and files:
// C:\Users\Public\TestFolder
// C:\Users\Public\TestFolder\test.txt
// C:\Users\Public\TestFolder\SubDir\test.txt
public class SimpleFileCopy
{
static void Main()
{
string fileName = "test.txt";
string sourcePath = @"C:\Users\Public\TestFolder";
string targetPath = @"C:\Users\Public\TestFolder\SubDir";

// Use Path class to manipulate file and directory paths.


string sourceFile = System.IO.Path.Combine(sourcePath, fileName);
string destFile = System.IO.Path.Combine(targetPath, fileName);

// To copy a folder's contents to a new location:


// Create a new target folder.
// If the directory already exists, this method does not create a new directory.
System.IO.Directory.CreateDirectory(targetPath);

// To copy a file to another location and


// overwrite the destination file if it already exists.
System.IO.File.Copy(sourceFile, destFile, true);

// To copy all the files in one directory to another directory.


// Get the files in the source folder. (To recursively iterate through
// all subfolders under the current directory, see
// "How to: Iterate Through a Directory Tree.")
// Note: Check for target path was performed previously
// in this code example.
if (System.IO.Directory.Exists(sourcePath))
{
string[] files = System.IO.Directory.GetFiles(sourcePath);

// Copy the files and overwrite destination files if they already exist.
foreach (string s in files)
{
// Use static Path methods to extract only the file name from the path.
fileName = System.IO.Path.GetFileName(s);
destFile = System.IO.Path.Combine(targetPath, fileName);
System.IO.File.Copy(s, destFile, true);
}
}
else
{
Console.WriteLine("Source path does not exist!");
}

// Keep console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

Im folgenden Beispiel wird gezeigt, wie Sie Dateien und Verzeichnisse verschieben.
// Simple synchronous file move operations with no user interface.
public class SimpleFileMove
{
static void Main()
{
string sourceFile = @"C:\Users\Public\public\test.txt";
string destinationFile = @"C:\Users\Public\private\test.txt";

// To move a file or folder to a new location:


System.IO.File.Move(sourceFile, destinationFile);

// To move an entire directory. To programmatically modify or combine


// path strings, use the System.IO.Path class.
System.IO.Directory.Move(@"C:\Users\Public\public\test\", @"C:\Users\Public\private");
}
}

Im folgenden Beispiel wird gezeigt, wie Sie Dateien und Verzeichnisse löschen.

// Simple synchronous file deletion operations with no user interface.


// To run this sample, create the following files on your drive:
// C:\Users\Public\DeleteTest\test1.txt
// C:\Users\Public\DeleteTest\test2.txt
// C:\Users\Public\DeleteTest\SubDir\test2.txt

public class SimpleFileDelete


{
static void Main()
{
// Delete a file by using File class static method...
if(System.IO.File.Exists(@"C:\Users\Public\DeleteTest\test.txt"))
{
// Use a try block to catch IOExceptions, to
// handle the case of the file already being
// opened by another process.
try
{
System.IO.File.Delete(@"C:\Users\Public\DeleteTest\test.txt");
}
catch (System.IO.IOException e)
{
Console.WriteLine(e.Message);
return;
}
}

// ...or by using FileInfo instance method.


System.IO.FileInfo fi = new System.IO.FileInfo(@"C:\Users\Public\DeleteTest\test2.txt");
try
{
fi.Delete();
}
catch (System.IO.IOException e)
{
Console.WriteLine(e.Message);
}

// Delete a directory. Must be writable or empty.


try
{
System.IO.Directory.Delete(@"C:\Users\Public\DeleteTest");
}
catch (System.IO.IOException e)
{
Console.WriteLine(e.Message);
}
// Delete a directory and all subdirectories with Directory static method...
if(System.IO.Directory.Exists(@"C:\Users\Public\DeleteTest"))
{
try
{
System.IO.Directory.Delete(@"C:\Users\Public\DeleteTest", true);
}

catch (System.IO.IOException e)
{
Console.WriteLine(e.Message);
}
}

// ...or with DirectoryInfo instance method.


System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(@"C:\Users\Public\public");
// Delete this dir and all subdirs.
try
{
di.Delete(true);
}
catch (System.IO.IOException e)
{
Console.WriteLine(e.Message);
}
}
}

Weitere Informationen
System.IO
C#-Programmierhandbuch
Das Dateisystem und die Registrierung (C#-Programmierhandbuch)
Bereitstellen eines Statusdialogfelds für Dateioperationen
Datei- und Stream-E/A
Allgemeine E/A-Aufgaben
Vorgehensweise: Bereitstellen eines
Statusdialogfelds für Dateioperationen (C#-
Programmierleitfaden)
04.11.2021 • 2 minutes to read

Sie können ein Standarddialogfeld bereitstellen, das den Verlauf bei Dateivorgängen in Windows anzeigt, wenn
Sie die Methode CopyFile(String, String, UIOption) im Microsoft.VisualBasic-Namespace verwenden.

NOTE
Auf Ihrem Computer werden möglicherweise andere Namen oder Speicherorte für die Benutzeroberflächenelemente von
Visual Studio angezeigt als die in den folgenden Anweisungen aufgeführten. Diese Elemente sind von der jeweiligen Visual
Studio-Version und den verwendeten Einstellungen abhängig. Weitere Informationen finden Sie unter Personalisieren der
IDE.

So fügen Sie einen Verweis in Visual Studio hinzu


1. Wählen Sie in der Menüleiste die Optionen Projekt und Ver weis hinzufügen aus.
Das Dialogfeld Ver weis-Manager wird angezeigt.
2. Wählen Sie im Bereich Assemblys die Option Framework aus, wenn sie nicht bereits ausgewählt ist.
3. Aktivieren Sie in der Namensliste das Kontrollkästchen Microsoft.VisualBasic , und schließen Sie dann
das Dialogfeld durch Auswählen der Schaltfläche OK .

Beispiel
Im folgenden Code wird das von sourcePath angegebene Verzeichnis in das von destinationPath angegebene
Verzeichnis kopiert. Mit diesem Code wird auch ein Standarddialogfeld bereitgestellt, in dem die geschätzte Zeit
angezeigt wird, die bis zum Abschluss des Vorgangs verbleibt.

// The following using directive requires a project reference to Microsoft.VisualBasic.


using Microsoft.VisualBasic.FileIO;

class FileProgress
{
static void Main()
{
// Specify the path to a folder that you want to copy. If the folder is small,
// you won't have time to see the progress dialog box.
string sourcePath = @"C:\Windows\symbols\";
// Choose a destination for the copied files.
string destinationPath = @"C:\TestFolder";

FileSystem.CopyDirectory(sourcePath, destinationPath,
UIOption.AllDialogs);
}
}

Siehe auch
Das Dateisystem und die Registrierung (C#-Programmierhandbuch)
Vorgehensweise: Schreiben in eine Textdatei (C#-
Programmierleitfaden)
04.11.2021 • 2 minutes to read

Dieser Artikel beinhaltet verschiedene Beispiele für das Schreiben von Text in eine Datei. In den ersten beiden
Beispielen werden statische Hilfsmethoden für die System.IO.File-Klasse verwendet, um jedes Element von
IEnumerable<string> und string -Inhalt in eine Textdatei zu schreiben. Im dritten Beispiel wird dargestellt, wie
Text einer Datei hinzugefügt wird, wenn jede Zeile beim Schreiben in die Datei einzeln verarbeitet werden muss.
In den ersten drei Beispielen wird der gesamte vorhandene Inhalt der Datei überschrieben. Im letzten Beispiel
wird veranschaulicht, wie einer vorhandenen Datei Text angefügt wird.
In jedem Beispiel werden Zeichenfolgenliterale in Dateien geschrieben. Wenn Sie Text formatieren wollen, der in
eine Datei geschrieben wird, verwenden Sie die Format-Methode oder das C#-Feature
Zeichenfolgeninterpolation.

Schreiben von mehreren Zeichenfolgen in eine Datei


using System.IO;
using System.Threading.Tasks;

class WriteAllLines
{
public static async Task ExampleAsync()
{
string[] lines =
{
"First line", "Second line", "Third line"
};

await File.WriteAllLinesAsync("WriteLines.txt", lines);


}
}

Das vorangehende Quellcodebeispiel erfüllt die folgenden Funktionen:


Es instanziiert ein Zeichenfolgenarray mit drei Werten.
Es erwartet einen Aufruf von File.WriteAllLinesAsync, der diese Aktionen ausführt:
Er erstellt asynchron eine Datei namens WriteLines.txt. Wenn die Datei bereits vorhanden ist, wird sie
überschrieben.
Er schreibt die angegebenen Zeilen in die Datei.
Er schließt die Datei und leert/löscht sie bei Bedarf.

Schreiben einer Zeichenfolge in eine Datei


using System.IO;
using System.Threading.Tasks;

class WriteAllText
{
public static async Task ExampleAsync()
{
string text =
"A class is the most powerful data type in C#. Like a structure, " +
"a class defines the data and behavior of the data type. ";

await File.WriteAllTextAsync("WriteText.txt", text);


}
}

Das vorangehende Quellcodebeispiel erfüllt die folgenden Funktionen:


Es instanziiert eine Zeichenfolge, die dem zugewiesenen Zeichenfolgenliteral entspricht.
Es erwartet einen Aufruf von File.WriteAllTextAsync, der diese Aktionen ausführt:
Er erstellt asynchron eine Datei namens WriteText.txt. Wenn die Datei bereits vorhanden ist, wird sie
überschrieben.
Er schreibt den angegebenen Text in die Datei.
Er schließt die Datei und leert/löscht sie bei Bedarf.

Schreiben ausgewählter Zeichenfolgen aus einem Array in eine Datei


using System.IO;
using System.Threading.Tasks;

class StreamWriterOne
{
public static async Task ExampleAsync()
{
string[] lines = { "First line", "Second line", "Third line" };
using StreamWriter file = new("WriteLines2.txt");

foreach (string line in lines)


{
if (!line.Contains("Second"))
{
await file.WriteLineAsync(line);
}
}
}
}

Das vorangehende Quellcodebeispiel erfüllt die folgenden Funktionen:


Es instanziiert ein Zeichenfolgenarray mit drei Werten.
Es instanziiert StreamWriter mit dem Dateipfad WriteLines2.txt als using-Deklaration.
Es durchläuft alle Zeilen.
Es erwartet bedingt einen Aufruf von StreamWriter.WriteLineAsync(String), der die Zeile in die Datei schreibt,
sofern die Zeile nicht "Second" enthält.

Anfügen von Text an eine vorhandene Datei


using System.IO;
using System.Threading.Tasks;

class StreamWriterTwo
{
public static async Task ExampleAsync()
{
using StreamWriter file = new("WriteLines2.txt", append: true);
await file.WriteLineAsync("Fourth line");
}
}

Das vorangehende Quellcodebeispiel erfüllt die folgenden Funktionen:


Es instanziiert ein Zeichenfolgenarray mit drei Werten.
Es instanziiert StreamWriter mit dem Dateipfad WriteLines2.txt als using-Deklaration und übergibt den
anzufügenden Wert true .
Es erwartet einen Aufruf von StreamWriter.WriteLineAsync(String), der die Zeichenfolge als angefügte Zeile
in die Datei schreibt.

Ausnahmen
Die folgenden Bedingungen können einen Ausnahmefehler verursachen:
InvalidOperationException: Die Datei ist bereits vorhanden und ist schreibgeschützt.
PathTooLongException: Der Pfadname ist möglicherweise zu lang.
IOException: Der Datenträger ist möglicherweise voll.
Es gibt noch weitere Bedingungen, die bei der Arbeit mit dem Dateisystem zu Ausnahmen führen können. Daher
empfiehlt sich eine defensive Programmierung.

Siehe auch
C#-Programmierhandbuch
Das Dateisystem und die Registrierung (C#-Programmierhandbuch)
Beispiel: Speichern einer Auflistung im Anwendungsspeicher
Vorgehensweise: Lesen aus einer Textdatei (C#-
Programmierleitfaden)
04.11.2021 • 2 minutes to read

In diesem Beispiel wird der Inhalt von Textdateien gelesen, indem die statische Methoden ReadAllText und
ReadAllLines aus der System.IO.File-Klasse verwendet werden.
Ein Beispiel, in dem StreamReader verwendet wird, finden Sie unter Vorgehensweise: Zeilenweises Lesen einer
Textdatei.

NOTE
Die Dateien, die in diesem Beispiel verwendet werden, werden im Thema Vorgehensweise: Schreiben in eine Textdatei
erstellt.

Beispiel
class ReadFromFile
{
static void Main()
{
// The files used in this example are created in the topic
// How to: Write to a Text File. You can change the path and
// file name to substitute text files of your own.

// Example #1
// Read the file as one string.
string text = System.IO.File.ReadAllText(@"C:\Users\Public\TestFolder\WriteText.txt");

// Display the file contents to the console. Variable text is a string.


System.Console.WriteLine("Contents of WriteText.txt = {0}", text);

// Example #2
// Read each line of the file into a string array. Each element
// of the array is one line of the file.
string[] lines = System.IO.File.ReadAllLines(@"C:\Users\Public\TestFolder\WriteLines2.txt");

// Display the file contents by using a foreach loop.


System.Console.WriteLine("Contents of WriteLines2.txt = ");
foreach (string line in lines)
{
// Use a tab to indent each line of the file.
Console.WriteLine("\t" + line);
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}

Kompilieren des Codes


Kopieren Sie den Code, und fügen Sie ihn in eine C#-Konsolenanwendung ein.
Wenn Sie nicht die Textdateien aus Vorgehensweise: Schreiben in eine Textdatei verwenden, ersetzen Sie das
Argument für ReadAllText und ReadAllLines durch die entsprechenden Pfad- und Dateinamen auf Ihrem
Computer.

Stabile Programmierung
Die folgenden Bedingungen können einen Ausnahmefehler verursachen:
Die Datei ist nicht oder nicht am angegebenen Speicherort vorhanden. Überprüfen Sie die Schreibweise des
Dateinamens und -pfads.

.NET-Sicherheit
Schließen Sie nicht vom Namen einer Datei auf deren Inhalt. Bei der Datei myFile.cs handelt es sich zum
Beispiel nicht unbedingt um eine C#-Quelldatei.

Siehe auch
System.IO
C#-Programmierhandbuch
Das Dateisystem und die Registrierung (C#-Programmierhandbuch)
Vorgehensweise: Zeilenweises Lesen einer Textdatei
(C#-Programmierleitfaden)
04.11.2021 • 2 minutes to read

Dieses Beispiel liest den Inhalt einer Textdatei Zeile pro Zeile in eine Zeichenfolge mithilfe der ReadLines -
Methode der File -Klasse. Jede Textzeile wird in der Zeichenfolge line gespeichert und auf dem Bildschirm
angezeigt.

Beispiel
int counter = 0;

// Read the file and display it line by line.


foreach (string line in System.IO.File.ReadLines(@"c:\test.txt"))
{
System.Console.WriteLine(line);
counter++;
}

System.Console.WriteLine("There were {0} lines.", counter);


// Suspend the screen.
System.Console.ReadLine();

Kompilieren des Codes


Kopieren Sie den Code, und fügen Sie ihn in die Main -Methode einer Konsolenanwendung ein.
Ersetzen Sie "c:\test.txt" durch den tatsächlichen Dateinamen.

Stabile Programmierung
Die folgenden Bedingungen können einen Ausnahmefehler verursachen:
Die Datei ist möglicherweise nicht vorhanden.

.NET-Sicherheit
Beurteilen Sie den Inhalt der Datei nicht anhand des Dateinamens. Bei der Datei myFile.cs handelt es sich
möglicherweise nicht um eine C#-Quelldatei.

Siehe auch
System.IO
C#-Programmierhandbuch
Das Dateisystem und die Registrierung (C#-Programmierhandbuch)
Vorgehensweise: Erstellen eines Schlüssels in der
Registrierung (C#- Programmierleitfaden)
04.11.2021 • 2 minutes to read

Dieses Beispiel fügt der Registrierung des aktuellen Benutzers unter dem Schlüssel "Names" das Wertepaar
"Name" und "Isabella" hinzu.

Beispiel
Microsoft.Win32.RegistryKey key;
key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey("Names");
key.SetValue("Name", "Isabella");
key.Close();

Kompilieren des Codes


Kopieren Sie den Code, und fügen Sie ihn in die Main -Methode einer Konsolenanwendung ein.
Ersetzen Sie den Names -Parameter durch den Namen eines Schlüssels, der sich in der Registrierung
direkt unter dem HKEY_CURRENT_USER-Knoten befindet.
Ersetzen Sie den Name -Parameter durch den Namen eines Werts, der sich direkt unterhalb des Names-
Knotens befindet.

Stabile Programmierung
Untersuchen Sie die Registrierungsstruktur, um eine adäquate Position für den Schlüssel zu ermitteln. Sie
können beispielsweise den Schlüssel Software des aktuellen Benutzers öffnen und einen Schlüssel mit dem
Namen Ihres Unternehmens erstellen. Anschließend fügen Sie die Registrierungswerte dem Schlüssel für das
Unternehmen hinzu.
Die folgenden Bedingungen können einen Ausnahmefehler verursachen:
Der Name des Schlüssels ist NULL.
Der Benutzer ist nicht zum Erstellen von Registrierungsschlüsseln berechtigt.
Der Name des Schlüssels ist länger als 255 Zeichen.
Der Schlüssel ist geschlossen.
Der Registrierungsschlüssel ist schreibgeschützt.

.NET-Sicherheit
Es ist sicherer, die Daten in den Benutzerordner ( Microsoft.Win32.Registry.CurrentUser ) anstatt auf den lokalen
Computer ( Microsoft.Win32.Registry.LocalMachine ) zu schreiben.
Wenn Sie einen Registrierungswert erstellen, müssen Sie festlegen, was geschehen soll, wenn der Wert bereits
vorhanden ist. Möglicherweise wurde der Wert bereits von einem bösartigen Prozess erstellt, der nun darauf
zugreifen kann. Wenn Sie dem Registrierungswert Daten hinzufügen, kann der andere Prozess darauf zugreifen.
Um dies zu verhindern, verwenden Sie die Overload:Microsoft.Win32.RegistryKey.GetValue - -Methode. Die
Methode gibt NULL zurück, wenn der Schlüssel noch nicht vorhanden ist.
Es ist nicht sicher, geheime Daten wie Kennwörter in der Registrierung als Klartext zu speichern. Dies gilt auch,
wenn der Registrierungsschlüssel durch Zugriffssteuerungslisten (ACLs) geschützt ist.

Siehe auch
System.IO
C#-Programmierhandbuch
Das Dateisystem und die Registrierung (C#-Programmierhandbuch)
Read, write and delete from the registry with C# (Ausführen von Aktionen in der Registrierung mit C#: Lesen,
Schreiben und Löschen)
Interoperabilität (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Interoperabilität ermöglicht es Ihnen, vorhandene Investitionen in nicht verwalteten Code zu schützen und
weiter zu nutzen. Code, der unter der Steuerung der Common Language Runtime (CLR) ausgeführt wird, wird
als verwalteter Code bezeichnet. Code, der außerhalb der CLR ausgeführt wird, wird als nicht verwalteter Code
bezeichnet. COM, COM+, C++-Komponenten, ActiveX-Komponenten und die Microsoft Windows-API sind
Beispiele für nicht verwalteten Code.
.NET ermöglicht Interoperabilität mit nicht verwaltetem Code über Plattformaufrufdienste, dem
System.Runtime.InteropServices-Namespace, C++-Interoperabilität und COM-Interoperabilität (COM-Interop).

In diesem Abschnitt
Überblick über die Interoperabilität
Dieser Artikel beschreibt Methoden zum Ermöglichen der Interoperabilität zwischen von C#-verwaltetem und
nicht verwaltetem Code.
Zugreifen auf Office-Interop-Objekte mithilfe von Visual C#-Funktionen
Dieser Artikel beschreibt Funktionen, die in Visual C# zur Erleichterung der Office-Programmierung eingeführt
wurden.
Indizierte Eigenschaften bei der COM-Interop-Programmierung
Dieser Artikel beschreibt die Verwendung von indizierten Eigenschaften zum Zugriff auf COM-Eigenschaften,
die über Parameter verfügen.
Verwenden eines Plattformaufrufs zum Wiedergeben einer Wavedatei
Dieser Artikel beschreibt die Verwendung von Plattformaufrufdiensten zum Abspielen einer WAV-Audiodatei im
Windows-Betriebssystem.
Exemplarische Vorgehensweise: Office-Programmierung
Dieser Artikel zeigt das Erstellen einer Excel-Arbeitsmappe und eines Word-Dokuments, das einen Link zur
Arbeitsmappe enthält.
COM-Beispielklasse
Dieser Artikel veranschaulicht, wie eine C#-Klasse als COM-Objekt verfügbar gemacht wird.

C#-Programmiersprachenspezifikation
Weitere Informationen finden Sie in den grundlegenden Konzepten und derC#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
Marshal.ReleaseComObject
C#-Programmierhandbuch
Interoperabilität mit nicht verwaltetem Code
Exemplarische Vorgehensweise: Office-Programmierung
Überblick über die Interoperabilität (C#-
Programmierhandbuch)
04.11.2021 • 3 minutes to read

Dieses Thema beschreibt Methoden zur Gewährleistung der Interoperabilität zwischen von C#-verwaltetem und
nicht verwaltetem Code.

Plattformaufruf
Der Plattformaufruf ist ein Dienst, durch den verwalteter Code nicht verwaltete Funktionen aufrufen kann, die in
DLLs (Dynamic Link Library) implementiert sind, z.B. die in der Microsoft Windows-API enthaltenen Funktionen.
Es sucht eine exportierte Funktion, ruft diese auf und marshallt ihre Argumente (ganze Zahlen, Zeichenfolgen,
Arrays, Strukturen usw.) bei Bedarf über die Grenzen des dialogfähigen Betriebs hinaus.
Weitere Informationen finden Sie unter Nutzen nicht verwalteter DLL-Funktionen und Vorgehensweise:
Verwenden eines Plattformaufrufs zum Wiedergeben einer WAV-Datei.

NOTE
Die Common Language Runtime (CLR) verwaltet den Zugriff auf Systemressourcen. Das Aufrufen von nicht verwaltetem
Code, der sich außerhalb der CLR befindet, umgeht diesen Sicherheitsmechanismus; deshalb stellt er ein Sicherheitsrisiko
dar. Nicht verwalteter Code kann z.B. Ressourcen in nicht verwaltetem Code direkt aufrufen und umgeht damit die
Sicherheitsmechanismen der CLR. Weitere Informationen finden Sie unter Sicherheit in .NET.

C++ Interop
Mit C++-Interop – auch als It Just Works (IJW) bezeichnet – können Sie eine native C++-Klasse umschließen,
sodass diese von in C# oder einer anderen .NET-Programmiersprache geschriebenem Code genutzt werden
kann. Dafür schreiben Sie C++-Code, der eine native DLL- oder COM-Komponente umschließen kann. Im
Gegensatz zu anderen .NET-Programmiersprachen bietet Visual C++ eine Interoperabilitätsunterstützung, dank
der verwalteter und nicht verwalteter Code in derselben Anwendung und sogar in derselben Datei verwendet
werden kann. Anschließend erstellen Sie den C++-Code mithilfe des /clr -Compilerschalters, um eine verwaltete
Assembly zu erzeugen. Zuletzt fügen Sie der Assembly in Ihrem C#-Projekt einen Verweis hinzu und verwenden
das umschlossene Objekt genauso wie eine andere verwaltete Klasse.

Verfügbarmachen von COM-Komponenten in C#


Sie können eine COM-Komponente aus einem C#-Projekt nutzen. Dies sind die Hauptschritte:
1. Suchen Sie eine COM-Komponenten, die Sie verwenden möchten, und registrieren Sie diese. Verwenden
Sie „regsvr32.exe“ zur Registrierung und Aufhebung der Registrierung einer COM-DLL.
2. Fügen Sie dem Projekt einen Verweis auf eine COM-Komponente oder eine Typbibliothek hinzu.
Visual Studio verwendet beim Hinzufügen des Verweises den Typbibliothekimporter (Tlbimp.exe). Dieser
akzeptiert eine Typbibliothek als Eingabe und gibt eine .NET-Interopassembly aus. Die Assembly – auch
als Runtime Callable Wrapper (RCW) bezeichnet – enthält verwaltete Klassen und Schnittstellen, die die
COM-Klassen und -Schnittstellen umschließen, die sich in der Typbibliothek befinden. Visual Studio fügt
dem Projekt einen Verweis auf die generierte Assembly hinzu.
3. Erstellen Sie eine Instanz einer im RCW definierten Klasse. Dadurch wird wiederum eine Instanz des
COM-Objekts erstellt.
4. Verwenden Sie das Objekt genauso, wie Sie andere verwaltete Objekte verwenden. Wenn das Objekt von
der automatische Speicherbereinigung freigegeben wird, wird auch die Instanz des COM-Objekts aus
dem Speicher freigestellt.
Weitere Informationen finden Sie unter Verfügbarmachen von COM-Komponenten für .NET Framework.

Verfügbarmachen von C# für COM


COM-Clients können C#-Typen nutzen, die ordnungsgemäß verfügbar gemacht wurden. Dies sind die
grundlegenden Schritte für das Verfügbarmachen von C#-Typen:
1. Fügen Sie Ihrem C#-Projekt Interop-Attribute hinzu.
Durch das Ändern der Visual C#-Projekteigenschaften können Sie eine Assembly für COM sichtbar
machen. Weitere Informationen finden Sie unter Dialogfeld „Assemblyinformationen“.
2. Generieren Sie eine COM-Typbibliothek, und registrieren Sie diese für den Gebrauch mit COM.
Sie können die Visual C#-Projekteigenschaften so modifizieren, dass die C#-Assembly automatisch für
COM-Interop registriert wird. Visual Studio verwendet Regasm.exe (das Assembly Registration-Tool)
mithilfe des Befehlszeilenschalters /tlb , das die verwaltete Assembly als Eingabe akzeptiert, um eine
Typbibliothek zu generieren. Diese Typbibliothek beschreibt den Typ public in der Assembly und fügt
Verzeichniseinträge hinzu, um COM-Clients das Erstellen verwalteter Klassen zu ermöglichen.
Weitere Informationen finden Sie unter Verfügbarmachen von .NET Framework-Komponenten in COM und
COM-Beispielklasse.

Siehe auch
Improving Interop Performance (Optimieren der Interopleistung)
Einführung in die Interoperabilität zwischen COM und .NET
Einführung in COM-Interop (Visual Basic)
Marshaling between Managed and Unmanaged Code (Marshalling zwischen verwaltetem und nicht
verwaltetem Code)
Interoperabilität mit nicht verwaltetem Code
C#-Programmierhandbuch
Vorgehensweise: Zugreifen auf Office-Interop-
Objekte (C#-Programmierleitfaden)
04.11.2021 • 12 minutes to read

C# verfügt über Funktionen, die den Zugriff auf Office-API-Objekte vereinfachen. Zu den neuen Funktionen
zählen benannte und optionale Argumente, ein neuer Typ namens dynamic und die Möglichkeit, Argumente an
Verweisparameter in COM-Methoden zu übergeben, als handele es sich um Werteparameter.
In diesem Thema verwenden Sie die neuen Funktionen, um Code zu schreiben, mit dem ein Microsoft Office
Excel-Arbeitsblatt erstellt und angezeigt wird. Dann schreiben Sie Code, um ein Office Word-Dokument
hinzuzufügen, das ein Symbol enthält, das mit dem Excel-Arbeitsblatt verknüpft ist.
Um diese exemplarische Vorgehensweise auszuführen, müssen Microsoft Office Excel 2007 und Microsoft Office
Word 2007 oder neuere Versionen auf Ihrem Computer installiert sein.

NOTE
Auf Ihrem Computer werden möglicherweise andere Namen oder Speicherorte für die Benutzeroberflächenelemente von
Visual Studio angezeigt als die in den folgenden Anweisungen aufgeführten. Diese Elemente sind von der jeweiligen Visual
Studio-Version und den verwendeten Einstellungen abhängig. Weitere Informationen finden Sie unter Personalisieren der
IDE.

So erstellen Sie eine Konsolenanwendung


1. Starten Sie Visual Studio.
2. Zeigen Sie im Menü Datei auf Neu , und klicken Sie dann auf Projekt . Das Dialogfeld Neues Projekt
wird angezeigt.
3. Erweitern Sie im Bereich Installier te Vorlagen den Eintrag Visual C# , und klicken Sie dann auf
Windows .
4. Sehen Sie sich den oberen Rand des Dialogfelds Neues Projekt an, um sicherzustellen, dass .NET
Framework 4 (oder höher) als Zielframework ausgewählt ist.
5. Klicken Sie im Bereich Vorlagen auf Konsolenanwendung .
6. Geben Sie einen Namen für das Projekt im Feld Name ein.
7. Klicken Sie auf OK .
Das neue Projekt wird im Projektmappen-Explorer angezeigt.

So fügen Sie Verweise hinzu


1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Projektnamen, und klicken
Sie dann auf Ver weis hinzufügen . Das Dialogfeld Ver weis hinzufügen wird angezeigt.
2. Wählen Sie auf der Seite Assemblys in der Liste Komponentenname den Eintrag
Microsoft.Office.Interop.Word aus, und wählen Sie bei gedrückter STRG-Taste
Microsoft.Office.Interop.Excel aus. Wenn keine Assemblys zu sehen sind, müssen Sie unter
Umständen sicherstellen, dass sie installiert sind und angezeigt werden. Weitere Informationen finden Sie
unter How to: Installieren von primären Interopassemblys für Office.
3. Klicken Sie auf OK .

So fügen Sie erforderliche using-Anweisungen hinzu


1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf die Datei Program.cs und dann
auf Code anzeigen .
2. Fügen Sie am Anfang der Codedatei die folgenden using -Anweisungen hinzu:

using Excel = Microsoft.Office.Interop.Excel;


using Word = Microsoft.Office.Interop.Word;

So erstellen Sie eine Liste mit Bankkonten


1. Fügen Sie die folgende Klassendefinition in Program.cs unter der Program -Klasse ein.

public class Account


{
public int ID { get; set; }
public double Balance { get; set; }
}

2. Fügen Sie den folgenden Code der Main -Methode hinzu, um eine bankAccounts -Liste mit zwei Konten
zu erstellen.

// Create a list of accounts.


var bankAccounts = new List<Account> {
new Account {
ID = 345678,
Balance = 541.27
},
new Account {
ID = 1230221,
Balance = -127.44
}
};

So deklarieren Sie eine Methode, mit der Kontoinformationen in Excel


exportiert werden
1. Fügen Sie die folgende Methode der Program -Klasse hinzu, um ein Excel-Arbeitsblatt einzurichten.
Die Methode Add hat einen optionalen Parameter zur Angabe einer bestimmten Vorlage. Optionale
Parameter – neu in C# 4 – ermöglichen es Ihnen, das Argument für diesen Parameter auszulassen, wenn
Sie den Standardwert des Parameters verwenden möchten. Da im folgenden Beispiel kein Argument
gesendet wird, verwendet Add die Standardvorlage und erstellt eine neue Arbeitsmappe. Die
entsprechende Anweisung in früheren Versionen von C# erfordert ein Platzhalterargument:
ExcelApp.Workbooks.Add(Type.Missing) .
static void DisplayInExcel(IEnumerable<Account> accounts)
{
var excelApp = new Excel.Application();
// Make the object visible.
excelApp.Visible = true;

// Create a new, empty workbook and add it to the collection returned


// by property Workbooks. The new workbook becomes the active workbook.
// Add has an optional parameter for specifying a praticular template.
// Because no argument is sent in this example, Add creates a new workbook.
excelApp.Workbooks.Add();

// This example uses a single workSheet. The explicit type casting is


// removed in a later procedure.
Excel._Worksheet workSheet = (Excel.Worksheet)excelApp.ActiveSheet;
}

2. Fügen Sie den folgenden Code am Ende von DisplayInExcel hinzu. Der Code fügt Werte in die ersten
beiden Spalten der ersten Zeile des Arbeitsblatts ein.

// Establish column headings in cells A1 and B1.


workSheet.Cells[1, "A"] = "ID Number";
workSheet.Cells[1, "B"] = "Current Balance";

3. Fügen Sie den folgenden Code am Ende von DisplayInExcel hinzu. Die foreach -Schleife fügt die
Informationen aus der Liste der Konten in die ersten beiden Spalten der nachfolgenden Zeilen des
Arbeitsblattes ein.

var row = 1;
foreach (var acct in accounts)
{
row++;
workSheet.Cells[row, "A"] = acct.ID;
workSheet.Cells[row, "B"] = acct.Balance;
}

4. Fügen Sie den folgenden Code am Ende von DisplayInExcel hinzu, um die Spaltenbreite an den Inhalt
anzupassen.

workSheet.Columns[1].AutoFit();
workSheet.Columns[2].AutoFit();

Frühere Versionen von C# erfordern eine explizite Umwandlung für diese Vorgänge, da
ExcelApp.Columns[1] ein Object zurückgibt und AutoFit eine Range-Methode von Excel ist. Die
folgenden Zeilen verdeutlichen die Umwandlung.

((Excel.Range)workSheet.Columns[1]).AutoFit();
((Excel.Range)workSheet.Columns[2]).AutoFit();

C# 4 und höhere Versionen konvertieren das zurückgegebene Object automatisch in dynamic , wenn die
Compileroption EmbedInteropTypes auf die Assembly verweist, oder wenn die Excel-Eigenschaft
Interop-Typen einbetten auf TRUE festgelegt ist. Der Standardwert für diese Eigenschaft ist "True".

So führen Sie das Projekt aus


1. Fügen Sie die folgende Zeile am Ende von Main hinzu.

// Display the list in an Excel spreadsheet.


DisplayInExcel(bankAccounts);

2. Drücken Sie STRG+F5.


Ein Excel-Arbeitsblatt mit den Daten der beiden Konten wird angezeigt.

So fügen Sie ein Word-Dokument hinzu


1. Um zusätzliche Möglichkeiten zu veranschaulichen, wie C# 4 und höhere Versionen die Office-
Programmierung verbessern, öffnet der folgende Code eine Word-Anwendung und erstellt ein Symbol,
das mit dem Excel-Arbeitsblatt verknüpft ist.
Fügen Sie die Methode CreateIconInWordDoc , die später in diesem Schritt bereitgestellt wird, in die
Program -Klasse ein. CreateIconInWordDoc verwendet benannte und optionale Argumente, um die
Komplexität der Methodenaufrufe von Add und PasteSpecial zu verringern. Diese Aufrufe beinhalten zwei
weitere neue Features, die in C# 4 eingeführt wurden und die Aufrufe von COM-Methoden mit
Verweisparametern vereinfachen. Erstens können Sie Argumente an die Verweisparameter senden als
handele es sich um Werteparameter. Das heißt, Sie können Werte direkt senden, ohne eine Variable für
jeden Verweisparameter zu erstellen. Der Compiler generiert temporäre Variablen, die die
Argumentwerte enthalten, und verwirft diese Variablen bei Rückkehr vom Aufruf. Zweitens können Sie
das ref -Schlüsselwort in der Argumentliste auslassen.
Die Add -Methode verfügt über vier Verweisparameter, die alle optional sind. In C# 4.0 und höheren
Versionen können Sie Argumente für einen oder alle Parameter weglassen, wenn Sie deren
Standardwerte verwenden möchten. In C# 3.0 und Vorgängerversionen muss ein Argument für jeden
Parameter bereitgestellt werden, und das Argument muss eine Variable sein, da es sich bei den
Parametern um Verweisparameter handelt.
Die PasteSpecial -Methode fügt den Inhalt aus der Zwischenablage ein. Die Methode verfügt über sieben
Verweisparameter, die alle optional sind. Der folgende Code gibt Argumente für zwei dieser Parameter
an: Link , um eine Verknüpfung mit der Quelle des Zwischenablage-Inhalts zu erstellen, und
DisplayAsIcon , um die Verknüpfung als Symbol anzuzeigen. In C# 4.0 und höheren Versionen können
Sie benannte Argumente für diese zwei verwenden und die anderen weglassen. Obwohl es sich um
Verweisparameter handelt, müssen Sie nicht das ref -Schlüsselwort verwenden oder Variablen erstellen,
die als Argumente gesendet werden. Sie können die Werte direkt senden. In C# 3.0 und
Vorgängerversionen müssen Sie für jeden Verweisparameter ein Variablenargument angeben.
static void CreateIconInWordDoc()
{
var wordApp = new Word.Application();
wordApp.Visible = true;

// The Add method has four reference parameters, all of which are
// optional. Visual C# allows you to omit arguments for them if
// the default values are what you want.
wordApp.Documents.Add();

// PasteSpecial has seven reference parameters, all of which are


// optional. This example uses named arguments to specify values
// for two of the parameters. Although these are reference
// parameters, you do not need to use the ref keyword, or to create
// variables to send in as arguments. You can send the values directly.
wordApp.Selection.PasteSpecial( Link: true, DisplayAsIcon: true);
}

In C# 3.0 und früheren Sprachversionen ist der folgende komplexere Code erforderlich.

static void CreateIconInWordDoc2008()


{
var wordApp = new Word.Application();
wordApp.Visible = true;

// The Add method has four parameters, all of which are optional.
// In Visual C# 2008 and earlier versions, an argument has to be sent
// for every parameter. Because the parameters are reference
// parameters of type object, you have to create an object variable
// for the arguments that represents 'no value'.

object useDefaultValue = Type.Missing;

wordApp.Documents.Add(ref useDefaultValue, ref useDefaultValue,


ref useDefaultValue, ref useDefaultValue);

// PasteSpecial has seven reference parameters, all of which are


// optional. In this example, only two of the parameters require
// specified values, but in Visual C# 2008 an argument must be sent
// for each parameter. Because the parameters are reference parameters,
// you have to contruct variables for the arguments.
object link = true;
object displayAsIcon = true;

wordApp.Selection.PasteSpecial( ref useDefaultValue,


ref link,
ref useDefaultValue,
ref displayAsIcon,
ref useDefaultValue,
ref useDefaultValue,
ref useDefaultValue);
}

2. Fügen Sie die folgende Anweisung am Ende von Main hinzu.

// Create a Word document that contains an icon that links to


// the spreadsheet.
CreateIconInWordDoc();

3. Fügen Sie die folgende Anweisung am Ende von DisplayInExcel hinzu. Die Copy -Methode fügt das
Arbeitsblatt der Zwischenablage hinzu.
// Put the spreadsheet contents on the clipboard. The Copy method has one
// optional parameter for specifying a destination. Because no argument
// is sent, the destination is the Clipboard.
workSheet.Range["A1:B3"].Copy();

4. Drücken Sie STRG+F5.


Ein Word-Dokument mit einem Symbol wird angezeigt. Doppelklicken Sie auf das Symbol, um das
Arbeitsblatt in den Vordergrund zu bringen.

So legen Sie die Eigenschaft "Interop-Typen einbetten" fest


1. Weitere Verbesserungen sind möglich, wenn Sie einen COM-Typ aufrufen, der keine primäre Interop-
Assembly (PIA) zur Laufzeit benötigt. Das Entfernen der Abhängigkeit von PIAs führt zu einer
Versionsunabhängigkeit und einfacheren Bereitstellung. Weitere Informationen zu den Vorteilen der
Programmierung ohne PIAs finden Sie unter Exemplarische Vorgehensweise: Einbetten von Typen aus
verwalteten Assemblys.
Darüber hinaus ist die Programmierung einfacher, da die Typen, die erforderlich sind und von COM-
Methoden zurückgegeben werden, mithilfe des Typs dynamic anstelle von Object dargestellt werden
können. Variablen vom Typ dynamic werden erst zur Laufzeit ausgewertet, wodurch die Notwendigkeit
der expliziten Umwandlung entfällt. Weitere Informationen finden Sie unter Verwenden von
dynamischen Typen.
In C# 4 ist das Einbetten von Typinformationen anstelle der Verwendung von PIAs das Standardverhalten.
Aufgrund dieses Standardverhaltens werden mehrere der vorherigen Beispiele vereinfacht, da keine
explizite Umwandlung erforderlich ist. Beispiel: Die Deklaration von worksheet in DisplayInExcel lautet
Excel._Worksheet workSheet = excelApp.ActiveSheet und nicht
Excel._Worksheet workSheet = (Excel.Worksheet)excelApp.ActiveSheet . Die Aufrufe von AutoFit in
derselben Methode würden ebenfalls eine explizite Umwandlung ohne den Standard erfordern, da
ExcelApp.Columns[1] ein Object zurückgibt und AutoFit eine Excel-Methode ist. Im folgenden Code
sind alle Umwandlungen dargestellt.

((Excel.Range)workSheet.Columns[1]).AutoFit();
((Excel.Range)workSheet.Columns[2]).AutoFit();

2. Wenn Sie den Standard ändern und PIAS verwenden möchten, anstatt die Typinformationen einzubetten,
erweitern Sie im Projektmappen-Explorer den Knoten Ver weise , und wählen Sie dann
Microsoft.Office.Interop.Excel oder Microsoft.Office.Interop.Word aus.
3. Wenn Sie das Fenster Eigenschaften nicht sehen, drücken Sie F4 .
4. Suchen Sie in der Liste der Eigenschaften nach Interop-Typen einbetten , und ändern Sie dessen Wert
in FALSE . Äquivalent dazu können Sie kompilieren, indem Sie die Compileroption References anstelle
von EmbedInteropTypes an einer Eingabeaufforderung verwenden.

So fügen Sie der Tabelle zusätzliche Formatierungen hinzu


1. Ersetzen Sie die beiden Aufrufe von AutoFit in DisplayInExcel mit der folgenden Anweisung.

// Call to AutoFormat in Visual C# 2010.


workSheet.Range["A1", "B3"].AutoFormat(
Excel.XlRangeAutoFormat.xlRangeAutoFormatClassic2);
Die Methode AutoFormat verfügt über sieben Wertparameter, die alle optional sind. Benannte und
optionale Argumente ermöglichen es Ihnen, Argumente für keine, einige oder alle von ihnen
bereitzustellen. In der vorherigen Anweisung wurde ein Argument für nur einen der Parameter
angegeben, nämlich für Format . Da Format der erste Parameter in der Parameterliste ist, müssen Sie
nicht den Parameternamen angeben. Die Anweisung ist jedoch möglicherweise leichter zu verstehen,
wenn der Parametername eingeschlossen wird, wie im folgenden Code gezeigt.

// Call to AutoFormat in Visual C# 2010.


workSheet.Range["A1", "B3"].AutoFormat(Format:
Excel.XlRangeAutoFormat.xlRangeAutoFormatClassic2);

2. Drücken Sie STRG + F5, um das Ergebnis anzuzeigen. Weitere Formate sind in der XlRangeAutoFormat-
Enumeration aufgelistet.
3. Vergleichen Sie die Anweisung in Schritt 1 mit dem folgenden Code, der die Argumente zeigt, die in C#
3.0 und Vorgängerversionen erforderlich sind.

// The AutoFormat method has seven optional value parameters. The


// following call specifies a value for the first parameter, and uses
// the default values for the other six.

// Call to AutoFormat in Visual C# 2008. This code is not part of the


// current solution.
excelApp.get_Range("A1", "B4").AutoFormat(Excel.XlRangeAutoFormat.xlRangeAutoFormatTable3,
Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing);

Beispiel
Der folgende Code veranschaulicht das vollständige Beispiel.

using System;
using System.Collections.Generic;
using System.Linq;
using Excel = Microsoft.Office.Interop.Excel;
using Word = Microsoft.Office.Interop.Word;

namespace OfficeProgramminWalkthruComplete
{
class Walkthrough
{
static void Main(string[] args)
{
// Create a list of accounts.
var bankAccounts = new List<Account>
{
new Account {
ID = 345678,
Balance = 541.27
},
new Account {
ID = 1230221,
Balance = -127.44
}
};

// Display the list in an Excel spreadsheet.


DisplayInExcel(bankAccounts);

// Create a Word document that contains an icon that links to


// the spreadsheet.
// the spreadsheet.
CreateIconInWordDoc();
}

static void DisplayInExcel(IEnumerable<Account> accounts)


{
var excelApp = new Excel.Application();
// Make the object visible.
excelApp.Visible = true;

// Create a new, empty workbook and add it to the collection returned


// by property Workbooks. The new workbook becomes the active workbook.
// Add has an optional parameter for specifying a praticular template.
// Because no argument is sent in this example, Add creates a new workbook.
excelApp.Workbooks.Add();

// This example uses a single workSheet.


Excel._Worksheet workSheet = excelApp.ActiveSheet;

// Earlier versions of C# require explicit casting.


//Excel._Worksheet workSheet = (Excel.Worksheet)excelApp.ActiveSheet;

// Establish column headings in cells A1 and B1.


workSheet.Cells[1, "A"] = "ID Number";
workSheet.Cells[1, "B"] = "Current Balance";

var row = 1;
foreach (var acct in accounts)
{
row++;
workSheet.Cells[row, "A"] = acct.ID;
workSheet.Cells[row, "B"] = acct.Balance;
}

workSheet.Columns[1].AutoFit();
workSheet.Columns[2].AutoFit();

// Call to AutoFormat in Visual C#. This statement replaces the


// two calls to AutoFit.
workSheet.Range["A1", "B3"].AutoFormat(
Excel.XlRangeAutoFormat.xlRangeAutoFormatClassic2);

// Put the spreadsheet contents on the clipboard. The Copy method has one
// optional parameter for specifying a destination. Because no argument
// is sent, the destination is the Clipboard.
workSheet.Range["A1:B3"].Copy();
}

static void CreateIconInWordDoc()


{
var wordApp = new Word.Application();
wordApp.Visible = true;

// The Add method has four reference parameters, all of which are
// optional. Visual C# allows you to omit arguments for them if
// the default values are what you want.
wordApp.Documents.Add();

// PasteSpecial has seven reference parameters, all of which are


// optional. This example uses named arguments to specify values
// for two of the parameters. Although these are reference
// parameters, you do not need to use the ref keyword, or to create
// variables to send in as arguments. You can send the values directly.
wordApp.Selection.PasteSpecial(Link: true, DisplayAsIcon: true);
}
}

public class Account


{
public int ID { get; set; }
public int ID { get; set; }
public double Balance { get; set; }
}
}

Siehe auch
Type.Missing
dynamic
Verwenden von dynamischen Typen
Benannte und optionale Argumente
Vorgehensweise: Verwenden von benannten und optionalen Argumenten in der Office-Programmierung
Vorgehensweise: Verwenden indizierter
Eigenschaften bei der COM-Interop-
Programmierung (C#-Programmierleitfaden)
04.11.2021 • 2 minutes to read

Indizierte Eigenschaften verbessern die Verarbeitung von COM-Eigenschaften mit Parametern in der C#-
Programmierung. Indizierte Eigenschaften arbeiten zusammen mit anderen Funktionen in Visual C#, wie z.B.
benannte und optionale Argumente und neuen Typinformationen (dynamisch) und eingebettete
Typinformationen, um die Microsoft Office-Programmierung zu verbessern.
In früheren Versionen von C# waren Methoden nur als Eigenschaften zugänglich, wenn die get -Methode keine
Parameter und die set -Methode nur einen Wertparameter hatte. Allerdings erfüllen nicht alle COM-
Eigenschaften diese Einschränkungen. Die Eigenschaft Range[] in Excel verfügt über eine get -Zugriffsmethode,
für die ein Parameter für den Namen des Bereichs erforderlich ist. Früher mussten Sie stattdessen die
get_Range -Methode verwenden, weil Sie nicht direkt auf die Range -Methode zugreifen konnten. Dies wird in
folgendem Beispiel veranschaulicht.

// Visual C# 2008 and earlier.


var excelApp = new Excel.Application();
// . . .
Excel.Range targetRange = excelApp.get_Range("A1", Type.Missing);

Mit indizierte Eigenschaften können Sie stattdessen Folgendes schreiben:

// Visual C# 2010.
var excelApp = new Excel.Application();
// . . .
Excel.Range targetRange = excelApp.Range["A1"];

NOTE
Im vorherigen Beispiel wurde auch die Funktion Optionale Argumente verwendet, mit der Sie Type.Missing weglassen
können.

Für das Festlegen der Value -Eigenschaft eines Range-Objekts in C# 3.0 und früher sind zwei Argumente
erforderlich. Eines dieser Argumente stellt ein Argument für einen optionalen Parameter bereit, der den Typ des
Bereichswerts angibt. Das andere Argument stellt den Wert für die Value -Eigenschaft bereit. In den folgenden
Beispielen werden diese Techniken veranschaulicht. Beide legen den Wert der Zelle A1 auf Name fest.

// Visual C# 2008.
targetRange.set_Value(Type.Missing, "Name");
// Or
targetRange.Value2 = "Name";

Mit indizierte Eigenschaften können Sie stattdessen folgenden Code schreiben.


// Visual C# 2010.
targetRange.Value = "Name";

Sie können nicht Ihre eigenen indizierten Eigenschaften erstellen. Die Funktion unterstützt nur die Nutzung
vorhandener indizierter Eigenschaften.

Beispiel
Der folgende Code veranschaulicht das vollständige Beispiel. Weitere Informationen zum Einrichten eines
Projekts, das auf die Office-API zugreift, finden Sie unter Vorgehensweise: Zugreifen auf Office-Interop-Objekte
mithilfe von C#-Funktionen.

// You must add a reference to Microsoft.Office.Interop.Excel to run


// this example.
using System;
using Excel = Microsoft.Office.Interop.Excel;

namespace IndexedProperties
{
class Program
{
static void Main(string[] args)
{
CSharp2010();
//CSharp2008();
}

static void CSharp2010()


{
var excelApp = new Excel.Application();
excelApp.Workbooks.Add();
excelApp.Visible = true;

Excel.Range targetRange = excelApp.Range["A1"];


targetRange.Value = "Name";
}

static void CSharp2008()


{
var excelApp = new Excel.Application();
excelApp.Workbooks.Add(Type.Missing);
excelApp.Visible = true;

Excel.Range targetRange = excelApp.get_Range("A1", Type.Missing);


targetRange.set_Value(Type.Missing, "Name");
// Or
//targetRange.Value2 = "Name";
}
}
}

Siehe auch
Benannte und optionale Argumente
dynamic
Verwenden von dynamischen Typen
Verwenden von benannten und optionalen Argumenten in der Office-Programmierung
Zugreifen auf Office-Interop-Objekte mithilfe von Visual C#-Funktionen
Exemplarische Vorgehensweise: Office-Programmierung
Vorgehensweise: Verwenden eines Plattformaufrufs
zum Wiedergeben einer WAV-Datei (C#-
Programmierleitfaden)
04.11.2021 • 2 minutes to read

Im folgenden C#-Codebeispiel wird die Verwendung von Plattformaufrufdiensten zum Wiedergeben einer
WAV-Audiodatei unter dem Betriebssystem Windows veranschaulicht.

Beispiel
In diesem Beispielcode wird DllImportAttribute verwendet, um den Einstiegspunkt der PlaySound -Methode von
winmm.dll als Form1 PlaySound() zu importieren. Das Beispiel hat ein einfaches Windows-Formular mit einer
Schaltfläche. Wenn Sie auf die Schaltfläche klicken, öffnet sich das Windows-Standarddialogfeld OpenFileDialog,
von wo aus Sie eine Datei zur Wiedergabe öffnen können. Wenn Sie eine Wavedatei auswählen, wird diese mit
der PlaySound() -Methode der wimm.dll-Bibliothek wiedergegeben. Weitere Informationen zu dieser Methode
finden Sie unter Verwenden der Funktion „PlaySound“ mit Wave-Audiodateien. Suchen Sie nach einer Datei und
wählen Sie eine aus, die über eine WAV-Erweiterung verfügt, und klicken Sie anschließend auf Öffnen , um die
WAVE-Datei mit einem Plattformaufruf wiederzugeben. Der vollständige Pfad der ausgewählten Datei wird in
einem Textfeld angezeigt.
Das Dialogfeld Open Files (Dateien öffnen) wird so gefiltert, dass es nur Dateien mit WAV-Erweiterung anzeigt
werden. Dazu werden folgende Filtereinstellungen verwendet:

dialog1.Filter = "Wav Files (*.wav)|*.wav";


using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WinSound
{
public partial class Form1 : Form
{
private TextBox textBox1;
private Button button1;

public Form1() // Constructor.


{
InitializeComponent();
}

[DllImport("winmm.DLL", EntryPoint = "PlaySound", SetLastError = true, CharSet = CharSet.Unicode,


ThrowOnUnmappableChar = true)]
private static extern bool PlaySound(string szSound, System.IntPtr hMod, PlaySoundFlags flags);

[System.Flags]
public enum PlaySoundFlags : int
{
SND_SYNC = 0x0000,
SND_ASYNC = 0x0001,
SND_NODEFAULT = 0x0002,
SND_LOOP = 0x0008,
SND_NOSTOP = 0x0010,
SND_NOWAIT = 0x00002000,
SND_FILENAME = 0x00020000,
SND_RESOURCE = 0x00040004
}

private void button1_Click(object sender, System.EventArgs e)


{
var dialog1 = new OpenFileDialog();

dialog1.Title = "Browse to find sound file to play";


dialog1.InitialDirectory = @"c:\";
dialog1.Filter = "Wav Files (*.wav)|*.wav";
dialog1.FilterIndex = 2;
dialog1.RestoreDirectory = true;

if (dialog1.ShowDialog() == DialogResult.OK)
{
textBox1.Text = dialog1.FileName;
PlaySound(dialog1.FileName, new System.IntPtr(), PlaySoundFlags.SND_SYNC);
}
}

private void Form1_Load(object sender, EventArgs e)


{
// Including this empty method in the sample because in the IDE,
// when users click on the form, generates code that looks for a default method
// with this name. We add it here to prevent confusion for those using the samples.
}
}
}

Kompilieren des Codes


1. Erstellen Sie in Visual Studio ein neues C#-Windows Forms-Anwendungsprojekt, und nennen Sie es
WinSound .
2. Kopieren Sie den oben stehenden Code, und fügen Sie diesen über dem Inhalt der Datei Form1.cs ein.
3. Kopieren Sie den folgenden Code, und fügen Sie ihn in der Form1.Designer.cs-Datei in der
InitializeComponent() -Methode immer nach vorhandenem Code ein.

this.button1 = new System.Windows.Forms.Button();


this.textBox1 = new System.Windows.Forms.TextBox();
this.SuspendLayout();
//
// button1
//
this.button1.Location = new System.Drawing.Point(192, 40);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(88, 24);
this.button1.TabIndex = 0;
this.button1.Text = "Browse";
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// textBox1
//
this.textBox1.Location = new System.Drawing.Point(8, 40);
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System.Drawing.Size(168, 20);
this.textBox1.TabIndex = 1;
this.textBox1.Text = "FIle path";
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(5, 13);
this.ClientSize = new System.Drawing.Size(292, 266);
this.Controls.Add(this.textBox1);
this.Controls.Add(this.button1);
this.Name = "Form1";
this.Text = "Platform Invoke WinSound C#";
this.ResumeLayout(false);
this.PerformLayout();

4. Kompilieren Sie den Code, und führen Sie diesen aus.

Weitere Informationen
C#-Programmierhandbuch
Überblick über die Interoperabilität
Genauere Betrachtung von Plattformaufrufen
Marshallen von Daten mit Plattformaufruf
Exemplarische Vorgehensweise: Office-
Programmierung (C# und Visual Basic)
04.11.2021 • 11 minutes to read

Visual Studio bietet Funktionen in C# und Visual Basic, die die Microsoft Office-Programmierung verbessern. Zu
nützlichen C#-Funktionen gehören benannte und optionale Argumente und Rückgabewerte des Typs dynamic .
Bei der COM-Programmierung können Sie das ref -Schlüsselwort weglassen und Zugriff auf indizierte
Eigenschaften erhalten. Funktionen in Visual Basic umfassen automatisch implementierte Eigenschaften,
Anweisungen in Lambdaausdrücken sowie Auflistungsinitialisierer.
Beide Sprachen ermöglichen das Einbetten von Typinformationen, wodurch Assemblys bereitgestellt werden
können, die mit COM-Komponenten interagieren, ohne primäre Interop-Assemblys (PIAs) auf dem Computer
des Benutzers bereitzustellen. Weitere Informationen finden Sie unter Exemplarische Vorgehensweise: Einbetten
von Typen aus verwalteten Assemblys.
Diese exemplarische Vorgehensweise veranschaulicht diese Funktionen im Kontext der Office-Programmierung,
aber viele dieser Funktionen sind auch bei der allgemeinen Programmierung nützlich. In der exemplarischen
Vorgehensweise verwenden Sie eine Excel-Add-In-Anwendung, um eine Excel-Arbeitsmappe zu erstellen. Als
nächstes erstellen Sie ein Word-Dokument, das einen Link zur Arbeitsmappe enthält. Zum Schluss sehen Sie,
wie Sie die PIA-Abhängigkeit aktivieren und deaktivieren können.

Voraussetzungen
Auf Ihrem Computer müssen Microsoft Office Excel und Microsoft Office Word oder neuere Versionen installiert
sein, um diese exemplarische Vorgehensweise ausführen zu können.

NOTE
Auf Ihrem Computer werden möglicherweise andere Namen oder Speicherorte für die Benutzeroberflächenelemente von
Visual Studio angezeigt als die in den folgenden Anweisungen aufgeführten. Diese Elemente sind von der jeweiligen Visual
Studio-Version und den verwendeten Einstellungen abhängig. Weitere Informationen finden Sie unter Personalisieren der
IDE.

So richten Sie eine Excel-Add-In-Anwendung ein


1. Starten Sie Visual Studio.
2. Zeigen Sie im Menü Datei auf Neu , und klicken Sie dann auf Projekt .
3. Erweitern Sie im Bereich Installier te Vorlagen die Option Visual Basic oder Visual C# , erweitern Sie
dann Office , und klicken Sie auf die Jahreszahl der Version des Office-Produkts.
4. Klicken Sie im Bereich Vorlagen auf Excel <version> Add-in .
5. Sehen Sie am oberen Rand des Bereichs Vorlagen nach, um sicherzustellen, dass .NET Framework 4
oder eine höhere Version im Feld Zielframework angezeigt wird.
6. Geben Sie, wenn gewünscht, einen Namen für das Projekt in das Feld Name ein.
7. Klicken Sie auf OK .
8. Das neue Projekt wird im Projektmappen-Explorer angezeigt.
So fügen Sie Verweise hinzu
1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Projektnamen, und klicken
Sie dann auf Ver weis hinzufügen . Das Dialogfeld Ver weis hinzufügen wird angezeigt.
2. Wählen Sie auf der Registerkarte Assemblys die Option Microsoft.Office.Interop.Excel , Version
<version>.0.0.0 (einen Schlüssel für die Versionsnummer des Office-Produkts finden Sie unter
Microsoft Versions (in englischer Sprache)), in der Liste Komponentenname aus, und halten Sie dann
die STRG-Taste gedrückt, während Sie Microsoft.Office.Interop.Word , version <version>.0.0.0
auswählen. Wenn keine Assemblys sichtbar sind, müssen Sie unter Umständen sicherstellen, dass sie
installiert sind und angezeigt werden (siehe Vorgehensweise: Installieren von primären Interopassemblys
für Office).
3. Klicken Sie auf OK .
So fügen Sie erforderliche Imports-Anweisungen oder using-Anweisungen hinzu
1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf die Datei ThisAddIn.vb oder
ThisAddIn.cs , und klicken Sie dann auf Code anzeigen .
2. Fügen Sie die folgenden Imports -Anweisungen (Visual Basic) oder using -Direktiven (C#) am Anfang
der Codedatei ein, wenn sie noch nicht vorhanden sind.

using System.Collections.Generic;
using Excel = Microsoft.Office.Interop.Excel;
using Word = Microsoft.Office.Interop.Word;

Imports Microsoft.Office.Interop

So erstellen Sie eine Liste mit Bankkonten


1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Projektnamen, klicken Sie
auf Hinzufügen und dann auf Klasse . Benennen Sie die Klasse "Account.vb", wenn Sie Visual Basic
verwenden, oder "Account.cs", wenn Sie C# verwenden. Klicken Sie auf Hinzufügen .
2. Ersetzen Sie die Definition der Account -Klasse durch den folgenden Code. Die Klassendefinitionen
verwenden automatisch implementierte Eigenschaften. Weitere Informationen finden Sie unter
Automatisch implementierte Eigenschaften.

class Account
{
public int ID { get; set; }
public double Balance { get; set; }
}

Public Class Account


Property ID As Integer = -1
Property Balance As Double
End Class

3. Fügen Sie den folgenden Code in die ThisAddIn_Startup -Methode in ThisAddIn.vb oder ThisAddIn.cs ein,
um eine bankAccounts -Liste mit zwei Konten zu erstellen. Die Listendeklarationen verwenden
Auflistungsinitialisierer. Weitere Informationen finden Sie unter Auflistungsinitialisierer.
var bankAccounts = new List<Account>
{
new Account
{
ID = 345,
Balance = 541.27
},
new Account
{
ID = 123,
Balance = -127.44
}
};

Dim bankAccounts As New List(Of Account) From {


New Account With {
.ID = 345,
.Balance = 541.27
},
New Account With {
.ID = 123,
.Balance = -127.44
}
}

So exportieren Sie Daten nach Excel


1. Fügen Sie in der gleichen Datei die folgende Methode der ThisAddIn -Klasse hinzu. Die Methode richtet
eine Excel-Arbeitsmappe ein, in die die Daten exportiert werden.

void DisplayInExcel(IEnumerable<Account> accounts,


Action<Account, Excel.Range> DisplayFunc)
{
var excelApp = this.Application;
// Add a new Excel workbook.
excelApp.Workbooks.Add();
excelApp.Visible = true;
excelApp.Range["A1"].Value = "ID";
excelApp.Range["B1"].Value = "Balance";
excelApp.Range["A2"].Select();

foreach (var ac in accounts)


{
DisplayFunc(ac, excelApp.ActiveCell);
excelApp.ActiveCell.Offset[1, 0].Select();
}
// Copy the results to the Clipboard.
excelApp.Range["A1:B3"].Copy();
}
Sub DisplayInExcel(ByVal accounts As IEnumerable(Of Account),
ByVal DisplayAction As Action(Of Account, Excel.Range))

With Me.Application
' Add a new Excel workbook.
.Workbooks.Add()
.Visible = True
.Range("A1").Value = "ID"
.Range("B1").Value = "Balance"
.Range("A2").Select()

For Each ac In accounts


DisplayAction(ac, .ActiveCell)
.ActiveCell.Offset(1, 0).Select()
Next

' Copy the results to the Clipboard.


.Range("A1:B3").Copy()
End With
End Sub

Bei dieser Methode werden zwei neue C#-Funktionen verwendet. Beide Funktionen existieren bereits in
Visual Basic.
Die Methode Add hat einen optionalen Parameter zum Angeben einer bestimmten Vorlage.
Optionale Parameter – neu in C# 4 – ermöglichen es Ihnen, das Argument für diesen Parameter
auszulassen, wenn Sie den Standardwert des Parameters verwenden möchten. Da im vorherigen
Beispiel kein Argument gesendet wurde, verwendet Add die Standardvorlage und erstellt eine
neue Arbeitsmappe. Die entsprechende Anweisung in früheren Versionen von C# erfordert ein
Platzhalterargument: excelApp.Workbooks.Add(Type.Missing) .
Weitere Informationen finden Sie unter Benannte und optionale Argumente.
Die Eigenschaften Range und Offset des range-Objekts verwenden die Funktion Indizierte
Eigenschaften. Diese Funktion ermöglicht es Ihnen, diese Eigenschaften von COM-Typen zu nutzen,
indem Sie die folgende typische C#-Syntax verwenden. Indizierte Eigenschaften ermöglichen es
Ihnen außerdem, die Value -Eigenschaft des Range -Objekts zu verwenden, sodass Sie die Value2
-Eigenschaft nicht mehr verwenden müssen. Die Value -Eigenschaft ist indiziert, der Index ist
jedoch optional. Optionale Argumente und indizierte Eigenschaften arbeiten im folgenden Beispiel
zusammen.

// Visual C# 2010 provides indexed properties for COM programming.


excelApp.Range["A1"].Value = "ID";
excelApp.ActiveCell.Offset[1, 0].Select();

In früheren Versionen der Sprache ist die folgende spezielle Syntax erforderlich.

// In Visual C# 2008, you cannot access the Range, Offset, and Value
// properties directly.
excelApp.get_Range("A1").Value2 = "ID";
excelApp.ActiveCell.get_Offset(1, 0).Select();

Sie können nicht Ihre eigenen indizierten Eigenschaften erstellen. Die Funktion unterstützt nur die
Nutzung vorhandener indizierter Eigenschaften.
Weitere Informationen finden Sie unter Vorgehensweise: Indizierte Eigenschaften bei der COM-
Interop-Programmierung.
2. Fügen Sie den folgenden Code am Ende von DisplayInExcel hinzu, um die Spaltenbreite an den Inhalt
anzupassen.

excelApp.Columns[1].AutoFit();
excelApp.Columns[2].AutoFit();

' Add the following two lines at the end of the With statement.
.Columns(1).AutoFit()
.Columns(2).AutoFit()

Diese Ergänzungen veranschaulichen eine weitere Funktion in C#: die Behandlung von Object -Werten,
die von COM-Hosts wie z.B. Office zurückgegeben wurden, als wären sie vom Typ dynamic. Dies
geschieht automatisch, wenn Einbetten von Interop-Typen auf den Standardwert True festgelegt ist,
und ebenfalls, wenn die Compileroption EmbedInteropTypes auf die Assembly verweist. Der Typ
dynamic ermöglicht eine späte Bindung, bereits in Visual Basic verfügbar, und vermeidet die in C# 3.0
und in früheren Sprachversionen erforderliche explizite Umwandlung.
excelApp.Columns[1] gibt z.B. Object zurück, und AutoFit ist eine Range-Methode von Excel. Ohne
dynamic müssen Sie das von excelApp.Columns[1] zurückgegebene Objekt als eine Instanz von Range
umwandeln, bevor Sie die Methode AutoFit aufrufen.

// Casting is required in Visual C# 2008.


((Excel.Range)excelApp.Columns[1]).AutoFit();

// Casting is not required in Visual C# 2010.


excelApp.Columns[1].AutoFit();

Weitere Informationen zum Einbetten von Interop-Typen finden Sie in den Verfahren "So suchen Sie den
PIA-Verweis" und "So stellen Sie die PIA-Abhängigkeit wieder her" weiter unten in diesem Thema.
Weitere Informationen zu dynamic finden Sie unter dynamic oder Verwenden von dynamischen Typen.
So rufen Sie DisplayInExcel auf
1. Fügen Sie den folgenden Code am Ende der ThisAddIn_StartUp -Methode hinzu. Der Aufruf von
DisplayInExcel enthält zwei Argumente. Das erste Argument ist der Name der Liste mit Konten, die
verarbeitet werden sollen. Das zweite Argument ist ein mehrzeiliger Lambda-Ausdruck, der definiert, wie
die Daten verarbeitet werden. Die ID - und balance -Werte für jedes Konto werden in angrenzenden
Zellen angezeigt, und die Zeile wird rot dargestellt, wenn der Saldo kleiner als Null ist. Weitere
Informationen finden Sie unter Lambdaausdrücke.

DisplayInExcel(bankAccounts, (account, cell) =>


// This multiline lambda expression sets custom processing rules
// for the bankAccounts.
{
cell.Value = account.ID;
cell.Offset[0, 1].Value = account.Balance;
if (account.Balance < 0)
{
cell.Interior.Color = 255;
cell.Offset[0, 1].Interior.Color = 255;
}
});
DisplayInExcel(bankAccounts,
Sub(account, cell)
' This multiline lambda expression sets custom
' processing rules for the bankAccounts.
cell.Value = account.ID
cell.Offset(0, 1).Value = account.Balance

If account.Balance < 0 Then


cell.Interior.Color = RGB(255, 0, 0)
cell.Offset(0, 1).Interior.Color = RGB(255, 0, 0)
End If
End Sub)

2. Drücken Sie F5, um das Programm auszuführen. Ein Excel-Arbeitsblatt wird mit den Kontendaten
angezeigt.
So fügen Sie ein Word-Dokument hinzu
1. Fügen Sie den folgenden Code am Ende der ThisAddIn_StartUp -Methode hinzu, um ein Word-Dokument
zu erstellen, das einen Link zur Excel-Arbeitsmappe enthält.

var wordApp = new Word.Application();


wordApp.Visible = true;
wordApp.Documents.Add();
wordApp.Selection.PasteSpecial(Link: true, DisplayAsIcon: true);

Dim wordApp As New Word.Application


wordApp.Visible = True
wordApp.Documents.Add()
wordApp.Selection.PasteSpecial(Link:=True, DisplayAsIcon:=True)

Dieser Code veranschaulicht mehrere der neuen Funktionen in C#: die Möglichkeit, das ref -
Schlüsselwort in der COM-Programmierung auszulassen, benannte Argumente sowie optionale
Argumente. Diese Funktionen sind bereits in Visual Basic vorhanden. Die Methode PasteSpecial verfügt
über sieben Parameter, die als optionale Verweisparameter definiert sind. Benannte und optionale
Argumente ermöglichen es Ihnen, die Parameter festzulegen, auf die Sie namentlich zugreifen möchten,
und Argumente nur an diese Parameter zu senden. In diesem Beispiel werden Argumente gesendet, um
anzugeben, dass ein Link zur Arbeitsmappe in der Zwischenablage erstellt werden soll (Parameter Link )
und dass der Link im Word-Dokument als Symbol angezeigt werden soll (Parameter DisplayAsIcon ).
Visual C# ermöglicht auch das Weglassen des ref -Schlüsselworts für diese Argumente.
So führen Sie die Anwendung aus
1. Drücken Sie F5, um die Anwendung auszuführen. Excel wird gestartet und zeigt eine Tabelle mit den
Informationen der beiden Konten in bankAccounts an. Anschließend wird ein Word-Dokument angezeigt, das
einen Link zur Excel-Tabelle enthält.
So bereinigen Sie das abgeschlossene Projekt
1. Klicken Sie in Visual Studio auf Projektmappe bereinigen im Menü Erstellen . Andernfalls wird das Add-In
jedes Mal ausgeführt, wenn Sie Excel auf Ihrem Computer öffnen.
So suchen Sie den PIA -Verweis
1. Führen Sie die Anwendung erneut aus, klicken Sie jedoch nicht auf Projektmappe bereinigen .
2. Wählen Sie Star t aus. Suchen Sie Microsoft Visual Studio <version> , und öffnen Sie eine
Entwicklereingabeaufforderung.
3. Geben Sie in der Developer-Eingabeaufforderung für Visual Studio ildasm ein, und drücken Sie dann die
EINGABETASTE. Das IL DASM-Fenster wird angezeigt.
4. Klicken Sie im IL DASM-Fenster im Menü Datei auf Datei > Öffnen . Doppelklicken Sie nacheinander auf
Visual Studio<version> und Projekte . Öffnen Sie den Ordner für das Projekt, und suchen Sie im
Ordner „bin/Debug“ nach der Datei Projektname.dll. Doppelklicken Sie auf Projektname.dll. In einem
neuen Fenster werden die Attribute Ihres Projekts sowie Verweise auf andere Module und Assemblys
angezeigt. Beachten Sie, dass die Namespaces Microsoft.Office.Interop.Excel und
Microsoft.Office.Interop.Word in der Assembly enthalten sind. Standardmäßig importiert der Compiler
in Visual Studio die benötigten Typen aus einer referenzierten PIA in Ihre Assembly.
Weitere Informationen finden Sie unter Vorgehensweise: View Assembly Contents (Vorgehensweise:
Anzeigen von Assemblyinhalt).
5. Doppelklicken Sie auf das Symbol MANIFEST . Es wird ein Fenster angezeigt, das eine Liste von
Assemblys enthält, die vom Projekt referenzierte Elemente enthalten. Microsoft.Office.Interop.Excel
und Microsoft.Office.Interop.Word sind nicht in der Liste enthalten. Da die Typen, die das Projekt
benötigt, in die Assembly importiert wurden, sind keine Verweise auf eine PIA erforderlich. Dadurch wird
die Bereitstellung vereinfacht. Die PIAs müssen nicht auf dem Computer des Benutzers vorhanden sein,
und da für eine Anwendung keine bestimmte PIA-Version bereitgestellt werden muss, können die
Anwendungen so konzipiert sein, dass sie mit mehreren Versionen von Office funktionieren, sofern die
erforderlichen APIs in allen Versionen vorhanden sind.
Da die Bereitstellung von primären Interop-Assemblys nicht mehr benötigt wird, können Sie eine
Anwendung in erweiterten Szenarien erstellen, bei denen mehrere Versionen von Office, einschließlich
früherer Versionen, verwendet werden. Dies funktioniert jedoch nur, wenn Ihr Code keine APIs
verwendet, die nicht in der Version von Office verfügbar sind, mit der Sie arbeiten. Es ist nicht immer klar,
ob eine bestimmte API in einer früheren Version verfügbar war; daher wird die Arbeit mit früheren
Office-Versionen nicht empfohlen.

NOTE
Office hat vor Office 2003 keine PIAs veröffentlicht. Aus diesem Grund besteht die einzige Möglichkeit zum
Generieren einer Interop-Assembly für Office 2002 oder früheren Versionen darin, den COM-Verweis zu
importieren.

6. Schließen Sie das Manifest-Fenster und das Assembly-Fenster.


So stellen Sie die PIA -Abhängigkeit wieder her
1. Klicken Sie im Projektmappen-Explorer auf die Schaltfläche Alle Dateien anzeigen . Erweitern Sie
den Ordner Ver weise , und wählen Sie Microsoft.Office.Interop.Excel aus. Drücken Sie F4, um das
Fenster Eigenschaften anzuzeigen.
2. Ändern Sie im Fenster Eigenschaften die Eigenschaft Einbetten von Interop-Typen von True zu
False .
3. Wiederholen Sie die Schritte 1 und 2 in dieser Prozedur für Microsoft.Office.Interop.Word .
4. Kommentieren Sie in C# die beiden Aufrufe von Autofit am Ende der DisplayInExcel -Methode aus.
5. Drücken Sie F5, um sicherzustellen, dass das Projekt immer noch ordnungsgemäß ausgeführt wird.
6. Wiederholen Sie die Schritte 1 bis 3 der vorherigen Prozedur, um das Assembly-Fenster zu öffnen.
Beachten Sie, dass Microsoft.Office.Interop.Word und Microsoft.Office.Interop.Excel nicht mehr in der
Liste der eingebetteten Assemblys sind.
7. Doppelklicken Sie auf das Symbol MANIFEST , und führen Sie einen Bildlauf durch die Liste der
referenzierten Assemblys durch. Microsoft.Office.Interop.Word und Microsoft.Office.Interop.Excel
befinden sich in der Liste. Da die Anwendung auf die Excel- und Word-PIAs verweist und die Eigenschaft
Einbetten von Interop-Typen auf False gesetzt ist, müssen beide Assemblys auf dem Computer des
Endbenutzers vorhanden sein.
8. Klicken Sie in Visual Studio im Menü Erstellen auf Projektmappe bereinigen , um das abgeschlossene
Projekt zu bereinigen.

Siehe auch
Automatisch implementierte Eigenschaften (Visual Basic)
Automatisch implementierte Eigenschaften (C#)
Auflistungsinitialisierer
Objekt- und Auflistungsinitialisierer
Optionale Parameter
Übergeben von Argumenten nach Position und Name
Benannte und optionale Argumente
Frühes und spätes Binden
dynamic
Verwenden von dynamischen Typen
Lambdaausdrücke (Visual Basic)
Lambdaausdrücke (C#)
Indizierte Eigenschaften bei der COM-Interop-Programmierung
Exemplarische Vorgehensweise: Einbetten von Typinformationen aus Microsoft Office-Assemblys in Visual
Studio
Exemplarische Vorgehensweise: Einbetten von Typen aus verwalteten Assemblys in Visual Studio
Exemplarische Vorgehensweise: Creating Your First VSTO Add-in for Excel (Exemplarische Vorgehensweise:
Erstellen Ihres ersten VSTO-Add-Ins für Excel)
COM-Interop
Interoperabilität
COM-Beispielklasse (C#-Programmierhandbuch)
04.11.2021 • 2 minutes to read

Das Folgende ist ein Beispiel für eine Klasse, die Sie als COM-Objekt offenlegen würden. Nachdem dieser Code
in eine CS-Datei platziert und Ihrem Projekt hinzugefügt wurde, legen Sie die Eigenschaft für COM-Interop
registrieren auf TRUE fest. Weitere Informationen finden Sie unter Vorgehensweise: Registrieren einer
Komponente für COM-Interop.
Das Verfügbarmachen von Visual C#-Objekten für COM erfordert die Deklaration einer Klassenschnittstelle,
einer Ereignisschnittstelle (wenn dies erforderlich ist) und die Klasse selbst. Klassenmember müssen diesen
Regeln folgen, um für COM sichtbar zu sein:
Die Klasse muss öffentlich sein.
Eigenschaften, Methoden und Ereignisse müssen öffentlich sein.
Eigenschaften und Methoden müssen auf der Klassenschnittstelle deklariert werden.
Ereignisse müssen in der Ereignisschnittstelle deklariert werden.
Andere öffentliche Member in der Klasse, die nicht in diesen Schnittstellen deklariert sind, werden für COM nicht
sichtbar sein, aber für andere .NET-Objekte werden sie sichtbar sein.
Um Eigenschaften und Methoden für COM verfügbar zu machen, müssen Sie diese auf der Klassenschnittstelle
deklarieren, sie mit einem DispId -Attribut markieren und sie in der Klasse implementieren. Die Reihenfolge, in
der die Elemente in der Schnittstelle deklariert werden, ist die für die COM-Vtable verwendete Reihenfolge.
Um Ereignisse aus Ihrer Klasse verfügbar zu machen, müssen Sie diese auf der Ereignisschnittstelle deklarieren
und sie mit einem DispId -Attribut markieren. Die Klasse sollte diese Schnittstelle nicht implementieren.
Die Klasse implementiert die Klassenschnittstelle. Es kann mehr als eine Schnittstelle implementieren, aber die
erste Implementierung ist die Standard-Klassenschnittstelle. Implementieren Sie die Methoden und
Eigenschaften hier, die Sie für COM verfügbar gemacht haben. Sie müssen als öffentlich markiert sein und den
Deklarationen in der Klassenschnittstelle entsprechen. Deklarieren Sie außerdem die von der Klasse hier
ausgelösten Ereignisse. Sie müssen als öffentlich markiert sein und den Deklarationen in der Klassenschnittstelle
entsprechen.

Beispiel
using System.Runtime.InteropServices;

namespace project_name
{
[Guid("EAA4976A-45C3-4BC5-BC0B-E474F4C3C83F")]
public interface ComClass1Interface
{
}

[Guid("7BD20046-DF8C-44A6-8F6B-687FAA26FA71"),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ComClass1Events
{
}

[Guid("0D53A3E8-E51A-49C7-944E-E72A2064F938"),
ClassInterface(ClassInterfaceType.None),
ComSourceInterfaces(typeof(ComClass1Events))]
public class ComClass1 : ComClass1Interface
{
}
}

Weitere Informationen
C#-Programmierhandbuch
Interoperabilität
Seite „Erstellen“, Projekt-Designer (C#)
C#-Referenz
04.11.2021 • 3 minutes to read

Dieser Abschnitt enthält Referenzmaterial zu Schlüsselwörtern, Operatoren, Sonderzeichen,


Präprozessordirektiven, Compileroptionen sowie Compilerfehlern und -warnmeldungen von C#.

In diesem Abschnitt
C#-Schlüsselwörter
Enthält Links zu Informationen über C#-Schlüsselwörtern und zur Syntax.
C#-Operatoren
Enthält Links zu Informationen über C#-Operatoren und zur Syntax.
C#-Sonderzeichen
Enthält Links zu Informationen über kontextbezogene Sonderzeichen in C# und deren Verwendung.
C#-Präprozessoranweisungen
Enthält Links zu Informationen über die Compilerbefehle zum Einbetten in C#-Quellcode.
C#-Compileroptionen
Enthält Informationen über Compileroptionen und ihre Verwendung.
C#-Compilerfehler
Enthält Codeausschnitte, die die Ursache und Korrektur der C#-Compilerfehler und -warnungen
veranschaulichen.
C#-Programmiersprachenspezifikation
Die C# 6.0-Sprachspezifikation. Dies ist ein Entwurfsvorschlag für die C# 6.0-Sprache. Dieses Dokument wird
durch die Zusammenarbeit mit dem Ecma International-Ausschuss für C#-Standards weiterentwickelt. Version
5.0 wurde im Dezember 2017 als Dokument Standard ECMA-334, 5. Edition veröffentlicht.
Die Features, die in C#-Versionen nach 6.0 implementiert wurden, werden in Sprachspezifikationsvorschlägen
dargestellt. Diese Dokumente beschreiben die Deltas der Sprachspezifikation, um diese neuen Features
hinzuzufügen. Sie liegen in Entwurfsform vor. Diese Spezifikationen werden weiterentwickelt und zur formellen
Prüfung und Implementierung in eine zukünftige Version des C#-Standards an den Ecma International-
Normungsausschuss weitergeleitet.
Vorschläge für die C# 7.0-Spezifikation
Es gibt eine Reihe neuer Features, die in C# 7.0 implementiert wurden. Dazu gehören der Musterabgleich, lokale
Funktionen, out-Variablendeklarationen, throw-Ausdrücke, binäre Literale und Zahlentrennzeichen. Dieser
Ordner enthält die Spezifikationen für jedes dieser Features.
Vorschläge für die C# 7.1-Spezifikation
In C# 7.1 wurden neue Features hinzugefügt. Erstens können Sie eine Main -Methode schreiben, die Task oder
Task<int> zurückgibt. Dadurch können Sie den async -Modifizierer Main hinzufügen. Der default -Ausdruck
kann ohne Typ an Stellen verwendet werden, an denen der Typ abgeleitet werden kann. Darüber hinaus können
Tupelmembernamen abgeleitet werden. Schließlich kann Mustervergleich mit Generics verwendet werden.
Vorschläge für die C# 7.2-Spezifikation
C# 7.2 hat eine Reihe von kleinen Features hinzugefügt. Sie können Argumente durch einen schreibgeschützten
Verweis mit dem Schlüsselwort in übergeben. Es gibt eine Reihe von Low-Level-Änderungen, die die
Sicherheit zur Kompilierzeit für Span und verwandte Typen unterstützen. Sie können benannte Argumente
verwenden, bei denen spätere Argumente in einigen Situationen positional sind. Mit dem private protected -
Zugriffsmodifizierer können Sie festlegen, dass Aufrufer auf abgeleitete Typen beschränkt sind, die in derselben
Assembly implementiert sind. Der ?: -Operator kann in einen Verweis auf eine Variable aufgelöst werden. Sie
können auch hexadezimale und binäre Zahlen mit einem führenden Zifferntrennzeichen formatieren.
Vorschläge für die C# 7.3-Spezifikation
C# 7.3 ist eine weitere Unterversion, die mehrere kleine Aktualisierungen enthält. Sie können neue
Einschränkungen für generische Typparameter verwenden. Weitere Änderungen erleichtern das Arbeiten mit
fixed -Feldern, einschließlich der Verwendung von stackalloc -Zuordnungen. Lokale Variablen, die mit dem
Schlüsselwort ref deklariert wurden, können neu zugewiesen werden, um sich auf neuen Speicher zu
beziehen. Sie können Attribute für automatisch implementierte Eigenschaften festlegen, die auf das vom
Compiler generierte Sicherungsfeld abzielen. Ausdrucksvariablen können in Initialisierern verwendet werden.
Tupel können hinsichtlich ihrer Gleichheit (oder Ungleichheit) verglichen werden. Außerdem wurden einige
Verbesserungen an der Überladungsauflösung vorgenommen.
Vorschläge für die C# 8.0-Spezifikation
C# 8.0 wird mit .NET Core 3.0 zur Verfügung gestellt. Zu den Features gehören Nullable-Verweistypen,
rekursiver Musterabgleich, Standardschnittstellenmethoden, asynchrone Streams, Bereiche und Indizes,
musterbasierte using-Anweisung und using-Deklarationen, NULL-Sammelzuweisungen sowie
schreibgeschützte Instanzmember.
Vorschläge für die C# 9.0-Spezifikation
C# 9.0 wird mit .NET 5.0 zur Verfügung gestellt. Zu den Features zählen Datensätze, Anweisungen auf oberster
Ebene, Erweiterungen zum Musterabgleich, Init-Only-Setter, zieltypisierte neue Ausdrücke, Modulinitialisierer,
Erweiterungen für partielle Methoden, statische anonyme Funktionen, zieltypisierte bedingte Ausdrücke,
kovariante Rückgabetypen, Erweiterungen für GetEnumerator in foreach-Schleifen, Parameter zum Verwerfen
von Lambdaausdrücken, Attribute in lokalen Funktionen, Integer mit nativer Größe, Funktionszeiger, die
Unterdrückung der Ausgabe des localsinit-Flags und nicht eingeschränkte Typparameteranmerkungen.

Verwandte Abschnitte
Verwenden der Visual Studio-Entwicklungsumgebung für C#
Enthält Links zu konzeptionellen und aufgabenspezifischen Themen, in denen IDE und Editor beschrieben
werden.
C#-Programmierhandbuch
Enthält Informationen zur Verwendung der C#-Programmiersprache.
Verwaltung der C#-Sprachversion
04.11.2021 • 4 minutes to read

Der C#-Compiler bestimmt eine Standardsprachversion, die auf den Zielframeworks Ihres Projekts basiert. In
Visual Studio gibt es keine Benutzeroberfläche zum Ändern dieses Werts, aber Sie können ihn ändern, indem Sie
die CSPROJ-Datei bearbeiten. Mit der Auswahl des Standardwerts wird sichergestellt, dass Sie die neueste
Sprachversion nutzen, die mit Ihrem Zielframework kompatibel ist. So profitieren Sie vom Zugriff auf die
neuesten Sprachfeatures, die mit dem Zielframework Ihres Projekts kompatibel sind. Mit dem Standardwert
wird außerdem sichergestellt, dass Sie keine Sprache verwenden, die Typen oder Runtimeverhalten erfordert,
die nicht im Zielframework verfügbar sind. Wenn Sie eine Sprachversion auswählen, die neuer als die
Standardeinstellung ist, können Kompilierzeit- und Runtimefehler auftreten, die schwer zu diagnostizieren sind.
Die in diesem Artikel aufgeführten Regeln gelten für den Compiler, der in Visual Studio 2019 oder dem .NET
SDK enthalten ist. Die C#-Compiler, die Teil der Visual Studio 2017-Installation oder von früheren .NET
Core SDK-Versionen sind, sind standardmäßig auf C# 7.0 ausgerichtet.
C# 8.0 wird nur in .NET Core 3.x und höher unterstützt. Viele der neuesten Features erfordern Bibliotheks- und
Runtimefeatures, die mit .NET Core 3.x eingeführt wurden:
Die Implementierung von Standardschnittstellen erfordert neue Features in der .NET Core 3.0-CLR.
Für asynchrone Datenströme sind die neuen Typen System.IAsyncDisposable,
System.Collections.Generic.IAsyncEnumerable<T> und System.Collections.Generic.IAsyncEnumerator<T>
erforderlich.
Für Indizes und Bereiche sind die neuen Typen System.Index und System.Range erforderlich.
Verweistypen, die NULL-Werte zulassen, nutzen mehrere Attribute, um bessere Warnungen bereitzustellen.
Diese Attribute wurden in .NET Core 3.0 hinzugefügt. Andere Zielframeworks wurden nicht mit diesen
Attributen versehen. Das bedeutet, dass Warnungen, die NULL-Werte zulassen, mögliche Probleme nicht
exakt widerspiegeln.
C# 9.0 wird nur in .NET 5 und höher unterstützt.

der Arbeitszeittabelle
Der Compiler bestimmt basierend auf den folgenden Regeln eine Standardversion:

Z IEL F RA M EW O RK VERSIO N C #- SP RA C H VERSIO N SSTA N DA RD

.NET 6.x C# 10.0

.NET 5.x C# 9.0

.NET Core 3.x C# 8.0

.NET Core 2.x C# 7.3

.NET Standard 2.1 C# 8.0

.NET-Standard 2.0 C# 7.3

.NET-Standard 1.x C# 7.3


Z IEL F RA M EW O RK VERSIO N C #- SP RA C H VERSIO N SSTA N DA RD

.NET Framework alle C# 7.3

Wenn Ihr Projekt auf eine Vorschauframework abzielt, das eine entsprechende Vorschausprachversion besitzt,
wird die Vorschausprachversion als Sprachversion verwendet. In dieser Vorschau können Sie die neuesten
Features in beliebigen Umgebungen verwenden, ohne Auswirkungen auf Projekte für eine veröffentlichte .NET
Core-Version zu haben.

IMPORTANT
Visual Studio 2017 hat allen erstellten Projektdateien einen <LangVersion>latest</LangVersion> -Eintrag hinzugefügt.
Dieser lautete C# 7.0, als er hinzugefügt wurde. Wenn Sie jedoch ein Upgrade auf Visual Studio 2019 durchführen, ist dies
unabhängig vom Zielframework die neueste veröffentlichte Version. Diese Projekte setzen jetzt das Standardverhalten
außer Kraft. Bearbeiten Sie die Projektdatei, und entfernen Sie den Knoten. Anschließend verwendet das Projekt die für
das Zielframework empfohlene Compilerversion.

Überschreiben eines Standardwerts


Wenn Sie Ihre C#-Version explizit angeben müssen, haben Sie verschiedene Möglichkeiten:
Manuelles Bearbeiten der Projektdatei
Festlegen der Sprachversion für mehrere Projekte in einem Unterverzeichnis
Konfigurieren der LangVersion -Compileroption

TIP
Wenn Sie wissen möchten, welche Sprachversion Sie derzeit verwenden, fügen Sie #error version (Groß-
/Kleinschreibung beachten) in Ihren Code ein. Dadurch meldet der Compiler einen Compilerfehler (CS8304) mit einer
Meldung, die die verwendete Compilerversion und die zurzeit ausgewählte Sprachversion enthält. Weitere Informationen
finden Sie unter #error (C#-Referenz).

Bearbeiten der Projektdatei


Sie können die Sprachversion in der Projektdatei festlegen. Wenn Sie beispielsweise expliziten Zugriff auf
Previewfunktionen wünschen, fügen Sie ein Element wie folgt hinzu:

<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>

Der Wert preview verwendet die neueste verfügbare Vorschauversion der Sprache C#, die Ihr Compiler
unterstützt.
Konfigurieren mehrerer Projekte
Sie können eine Director y.Build.props -Datei erstellen, die das Element <LangVersion> enthält, um mehrere
Projekte zu konfigurieren. In der Regel führen Sie dies im Projektmappenverzeichnis durch. Fügen Sie Folgendes
in eine Director y.Build.props -Datei in Ihrem Projektmappenverzeichnis ein:
<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>

Builds in allen Unterverzeichnissen des Verzeichnisses, das diese Datei enthält, verwenden die C#-
Vorschauversion. Weitere Informationen finden Sie unter Anpassen des Builds.

Referenz zur C#-Sprachversion


In der folgenden Tabelle sind alle aktuellen C#-Sprachversionen enthalten. Wenn Ihr Compiler älter ist, versteht
er möglicherweise nicht alle Werte. Wenn Sie das neueste .NET SDK installieren, erhalten Sie Zugriff auf alle
aufgelisteten Syntaxversionen.

W ERT B EDEUT UN G

preview Der Compiler akzeptiert jede gültige Sprachsyntax der


letzten Vorschauversion.

latest Der Compiler akzeptiert die Syntax der neuesten


veröffentlichte Version des Compilers (einschließlich
Nebenversionen).

latestMajor ( default ) Der Compiler akzeptiert die Syntax der neuesten


veröffentlichte Hauptversion des Compilers.

10.0 Der Compiler akzeptiert nur Syntax, die in C# 10.0 oder


niedriger enthalten ist.

9.0 Der Compiler akzeptiert nur Syntax, die in C# 9.0 oder


niedriger enthalten ist.

8.0 Der Compiler akzeptiert nur Syntax, die in C# 8.0 oder


niedriger enthalten ist.

7.3 Der Compiler akzeptiert nur Syntax, die in C# 7.3 oder früher
enthalten ist.

7.2 Der Compiler akzeptiert nur Syntax, die in C# 7.2 oder früher
enthalten ist.

7.1 Der Compiler akzeptiert nur Syntax, die in C# 7.1 oder früher
enthalten ist.

7 Der Compiler akzeptiert nur Syntax, die in C# 7.0 oder früher


enthalten ist.

6 Der Compiler akzeptiert nur Syntax, die in C# 6.0 oder früher


enthalten ist.

5 Der Compiler akzeptiert nur Syntax, die in C# 5.0 oder früher


enthalten ist.
W ERT B EDEUT UN G

4 Der Compiler akzeptiert nur Syntax, die in C# 4.0 oder früher


enthalten ist.

3 Der Compiler akzeptiert nur Syntax, die in C# 3.0 oder früher


enthalten ist.

ISO-2 (oder 2 ) Der Compiler akzeptiert nur Syntax, die in ISO/IEC


23270:2006 C# (2.0) enthalten ist.

ISO-1 (oder 1 ) Der Compiler akzeptiert nur Syntax, die in ISO/IEC


23270:2003 C# (1.0/1.2) enthalten ist.
Werttypen (C#-Referenz)
04.11.2021 • 3 minutes to read

Werttypen und Verweistypen sind die beiden Hauptkategorien von C#-Typen. Eine Variable eines Werttyps
enthält eine Instanz des Typs. Dies unterscheidet sich von einer Variablen eines Verweistyps, die einen Verweis
auf eine Instanz des Typs enthält. Standardmäßig werden die Variablenwerte bei der Zuweisung kopiert, dabei
handelt es sich um die Übergabe eines Arguments an eine Methode oder die Rückgabe eines
Methodenergebnisses. Im Fall von Werttypvariablen werden die entsprechenden Typinstanzen kopiert. Das
folgende Beispiel veranschaulicht dieses Verhalten:

using System;

public struct MutablePoint


{
public int X;
public int Y;

public MutablePoint(int x, int y) => (X, Y) = (x, y);

public override string ToString() => $"({X}, {Y})";


}

public class Program


{
public static void Main()
{
var p1 = new MutablePoint(1, 2);
var p2 = p1;
p2.Y = 200;
Console.WriteLine($"{nameof(p1)} after {nameof(p2)} is modified: {p1}");
Console.WriteLine($"{nameof(p2)}: {p2}");

MutateAndDisplay(p2);
Console.WriteLine($"{nameof(p2)} after passing to a method: {p2}");
}

private static void MutateAndDisplay(MutablePoint p)


{
p.X = 100;
Console.WriteLine($"Point mutated in a method: {p}");
}
}
// Expected output:
// p1 after p2 is modified: (1, 2)
// p2: (1, 200)
// Point mutated in a method: (100, 200)
// p2 after passing to a method: (1, 200)

Wie das vorhergehende Beispiel zeigt, wirken sich Vorgänge auf eine Werttypvariable nur auf die in der Variable
gespeicherte Instanz des Werttyps aus.
Wenn ein Werttyp einen Datenmember eines Verweistyps enthält, wird beim Kopieren einer Werttypinstanz nur
der Verweis auf die Instanz des Verweistyps kopiert. Sowohl die kopierte als auch die ursprüngliche
Werttypinstanz haben Zugriff auf dieselbe Verweistypinstanz. Das folgende Beispiel veranschaulicht dieses
Verhalten:
using System;
using System.Collections.Generic;

public struct TaggedInteger


{
public int Number;
private List<string> tags;

public TaggedInteger(int n)
{
Number = n;
tags = new List<string>();
}

public void AddTag(string tag) => tags.Add(tag);

public override string ToString() => $"{Number} [{string.Join(", ", tags)}]";


}

public class Program


{
public static void Main()
{
var n1 = new TaggedInteger(0);
n1.AddTag("A");
Console.WriteLine(n1); // output: 0 [A]

var n2 = n1;
n2.Number = 7;
n2.AddTag("B");

Console.WriteLine(n1); // output: 0 [A, B]


Console.WriteLine(n2); // output: 7 [A, B]
}
}

NOTE
Definieren und verwenden Sie unveränderliche Werttypen, um Ihren Code weniger fehleranfällig und stabiler zu machen.
In diesem Artikel werden veränderbare Werttypen nur zur Veranschaulichung verwendet.

Arten von Werttypen und Typeinschränkungen


Ein Werttyp kann einer der zwei folgenden Varianten sein:
ein Strukturtyp, der Daten und zugehörige Funktionen einschließt
ein Enumerationstyp, der durch mehrere benannte Konstanten definiert wird und eine Auswahl oder
Auswahlmöglichkeiten darstellt
Ein Werttyp, der NULL zulässt wie T? , stellt alle Werte des zugrunde liegenden Werttyps T und einen
zusätzlichen NULL-Wert dar. Sie können einer Variablen eines Werttyps nicht null zuweisen, es sei denn, es
handelt sich um einen Werttyp,der NULL zulässt.
Sie können die struct -Einschränkung verwenden, um anzugeben, dass ein Typparameter ein Non-Nullable-
Werttyp ist. Sowohl Struktur- als auch Enumerationstypen erfüllen die struct -Einschränkung. Ab C# 7.3
können Sie System.Enum in einer Basisklasseneinschränkung (die als enum-Einschränkung bezeichnet wird)
verwenden, um anzugeben, dass ein Typparameter ein Enumerationstyp ist.

Integrierte Werttypen
C# bietet die folgenden integrierten Werttypen, die auch als einfache Typen bekannt sind:
Integrale numerische Typen
Numerische Gleitkommatypen
bool, der einen booleschen Wert darstellt
char, der ein Unicode-Zeichen in UTF-16-Codierung darstellt
Alle einfachen Typen sind Strukturtypen und unterscheiden sich von anderen Strukturtypen dadurch, dass sie
bestimmte zusätzliche Vorgänge erlauben:
Sie können Literale verwenden, um einen Wert eines einfachen Typs bereitzustellen. 'A' ist
beispielsweise ein Literal vom Typ char , und 2001 ist ein Literal vom Typ int .
Konstanten der einfachen Typen können Sie mit dem Schlüsselwort const deklarieren. Es ist nicht
möglich, Konstanten anderer Strukturtypen zu haben.
Konstante Ausdrücke, deren Operanden alle Konstanten der einfachen Typen sind, werden zur
Kompilierzeit ausgewertet.
Ab C# 7.0 unterstützt C# Werttupel. Ein Werttupel ist ein Werttyp, aber kein einfacher Typ.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Werttypen
Einfache Typen
Variablen

Weitere Informationen
C#-Referenz
System.ValueType
Verweistypen
Integrale numerische Typen (C#-Referenz)
04.11.2021 • 4 minutes to read

Die integralen numerischen Typen stellen ganze Zahlen dar. Alle integralen numerischen Typen sind Werttypen.
Sie sind auch einfache Typen und können mit Literalen initialisiert werden. Alle integralen numerischen Typen
unterstützen arithmetic-, bitwise logical-, comparison- and equality-Operatoren.

Merkmale der integralen Typen


C# unterstützt die folgenden vordefinierten integralen Typen:

C #- T Y P / SC H L ÜSSEL W O RT B EREIC H GRÖ SSE . N ET - T Y P

sbyte –128 bis 127 Ganze 8-Bit-Zahl mit System.SByte


Vorzeichen

byte 0 bis 255 8-Bit-Ganzzahl ohne System.Byte


Vorzeichen

short –32.768 bis 32.767 Ganze 16-Bit-Zahl mit System.Int16


Vorzeichen

ushort 0 bis 65.535 16-Bit-Ganzzahl ohne System.UInt16


Vorzeichen

int -2,147,483,648 bis Eine 32-Bit-Ganzzahl mit System.Int32


2,147,483,647 Vorzeichen

uint 0 bis 4.294.967.295 32-Bit Ganzzahl ohne System.UInt32


Vorzeichen

long - 64-Bit-Ganzzahl mit System.Int64


9,223,372,036,854,775,808 Vorzeichen
bis
9,223,372,036,854,775,807

ulong 0 bis 64-Bit-Ganzzahl ohne System.UInt64


18.446.744.073.709.551.61 Vorzeichen
5

nint Plattformabhängig 32-Bit- oder 64-Bit- System.IntPtr


Integerwerte mit
Vorzeichen

nuint Plattformabhängig 32-Bit- oder 64-Bit- System.UIntPtr


Integerwerte ohne
Vorzeichen

In allen Tabellenzeilen mit Ausnahme der letzten beiden ist jedes Schlüsselwort des C#-Typs aus der Spalte ganz
links ein Alias für den entsprechenden .NET-Typ. Das Schlüsselwort und der .NET-Typname sind austauschbar. In
den folgenden Deklarationen werden beispielsweise Variablen des gleichen Typs deklariert:
int a = 123;
System.Int32 b = 123;

Die nint - und nuint -Typen in den letzten beiden Zeilen der Tabelle sind Integerwerte mit nativer Größe. Sie
werden intern durch die angegebenen .NET-Typen dargestellt, aber in jedem Fall sind das Schlüsselwort und der
.NET-Typ nicht austauschbar. Der Compiler stellt für nint und nuint als Integertypen Vorgänge und
Konvertierungen zur Verfügung, die er für die Zeigertypen System.IntPtr und System.UIntPtr nicht bereitstellt.
Weitere Informationen finden Sie unter den nint - und nuint -Typen.
Der Standardwert jedes integralen Typs ist Null ( 0 ). Die einzelnen integralen Typen mit Ausnahme der Typen
mit nativer Größe verfügen jeweils über die Konstanten MinValue und MaxValue , die den minimalen und
maximalen Wert des Typs angeben.
Verwenden Sie die System.Numerics.BigInteger-Struktur, um eine ganze Zahl mit Vorzeichen ohne obere oder
untere Grenzen darzustellen.

Ganzzahlenliteral
Ganzzahlenliterale können die folgenden Typen aufweisen:
Dezimal : ohne Präfix
Hexadezimal: mit dem Präfix 0x oder 0X
Binär: mit dem Präfix 0b oder 0B (verfügbar in C# 7.0 und höher)
Der folgende Code zeigt ein Beispiel für jeden Typ:

var decimalLiteral = 42;


var hexLiteral = 0x2A;
var binaryLiteral = 0b_0010_1010;

Das vorherige Beispiel zeigt auch die Verwendung von _ als Zifferntrennzeichen, das ab C# 7.0 unterstützt
wird. Sie können das Zifferntrennzeichen mit allen Arten numerischer Literale verwenden.
Der Typ eines integralen Literals wird wie folgt durch sein Suffix bestimmt:
Wenn das Literal kein Suffix besitzt, ist sein Typ der erste dieser Typen, in dem sein Wert dargestellt
werden kann: int , uint , long , ulong .

NOTE
Literale werden als positive Werte interpretiert. Beispielsweise stellt das Literal 0xFF_FF_FF_FF die Nummer
4294967295 des Typs uint dar, obwohl es die gleiche Bitdarstellung wie die Nummer -1 des Typs int hat.
Wenn Sie einen Wert eines bestimmten Typs benötigen, können Sie ein Literal in diesen Typ umwandeln.
Verwenden Sie den unchecked -Operator, wenn ein Literalwert nicht im Zieltyp dargestellt werden kann.
Beispielsweise erzeugt unchecked((int)0xFF_FF_FF_FF) den Wert -1 .

Wenn das Literal das Suffix U oder u aufweist, ist sein Typ der erste dieser Typen, in dem sein Wert
dargestellt werden kann: uint , ulong .
Wenn das Literal das Suffix L oder l aufweist, ist sein Typ der erste dieser Typen, in dem sein Wert
dargestellt werden kann: long , ulong .
NOTE
Sie können den Kleinbuchstaben l als Suffix verwenden. Allerdings erzeugt dies eine Compilerwarnung, weil der
Buchstabe l leicht mit der Zahl 1 verwechselt werden kann. Verwenden Sie aus Gründen der Klarheit L .

Wenn das Literal das Suffix UL , Ul , uL , ul , LU , Lu , lU oder lu aufweist, ist sein Typ ulong .

Wenn der von einem Integer-Literal dargestellte Wert UInt64.MaxValue überschreitet, tritt der Compilerfehler
CS1021 auf.
Wenn der festgelegte Typ eines Integerliterals int lautet, und der vom Literal dargestellte Wert innerhalb des
Bereichs des Zieltyps liegt, kann der Wert implizit in sbyte , byte , short , ushort , uint , ulong , nint oder
nuint konvertiert werden:

byte a = 17;
byte b = 300; // CS0031: Constant value '300' cannot be converted to a 'byte'

Wie das vorherige Beispiel zeigt, tritt der Compilerfehler CS0031 auf, wenn der Wert des Literals nicht innerhalb
des Bereichs des Zieltyps liegt.
Sie können auch eine Umwandlung verwenden, um den Wert, der durch ein Ganzzahlliteral dargestellt wird, in
einen anderen Typ als den festgelegten Literaltyp zu konvertieren:

var signedByte = (sbyte)42;


var longVariable = (long)42;

Konvertierungen
Sie können beliebige ganzzahlige numerische Typen in beliebige andere ganzzahlige numerische Typen
konvertieren. Wenn der Zieltyp alle Werte des Quelltyps speichern kann, handelt es sich um eine implizite
Konvertierung. Andernfalls müssen Sie einen Cast-Ausdruck verwenden, um eine explizite Konvertierung
durchzuführen. Weitere Informationen finden Sie unter Integrierte numerische Konvertierungen (C#-Referenz)
(Integrierte numerische Konvertierungen).

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Integrale Typen
Ganzzahlenliterale

Siehe auch
C#-Referenz
Werttypen
Gleitkommatypen
Standardmäßige Zahlenformatzeichenfolgen
Numerische Ausdrücke in .NET
nint - und nuint -Typen (C#-Referenz)
04.11.2021 • 2 minutes to read

Ab C# 9.0 können Sie die Schlüsselwörter nint und nuint verwenden, um Integerwerte in nativer Größe zu
definieren. Dabei handelt es sich um 32-Bit-Integerwerte bei Ausführung in einem 32-Bit-Prozess oder um 64-
Bit-Integerwerte bei Ausführung in einem 64-Bit-Prozess. Sie können für Interop-Szenarien, Low-Level-
Bibliotheken und zur Optimierung der Leistung in Szenarien verwendet werden, in denen Integermathematik
ausgiebig genutzt wird.
Die Integertypen mit nativer Größe werden intern als die .NET-Typen System.IntPtr und System.UIntPtr
dargestellt. Im Gegensatz zu anderen numerischen Typen sind die Schlüsselwörter nicht einfach Aliase für die
Typen. Die folgenden Anweisungen sind gleichwertig:

nint a = 1;
System.IntPtr a = 1;

Der Compiler stellt Vorgänge und Konvertierungen für nint und nuint zur Verfügung, die für Integertypen
geeignet sind.

Native Integergröße zur Laufzeit


Um die Größe eines Integerwerts mit nativer Größe zur Laufzeit abzurufen, können Sie sizeof() verwenden.
Der Code muss jedoch in einem unsicheren Kontext kompiliert werden. Beispiel:

Console.WriteLine($"size of nint = {sizeof(nint)}");


Console.WriteLine($"size of nuint = {sizeof(nuint)}");

// output when run in a 64-bit process


//size of nint = 8
//size of nuint = 8

// output when run in a 32-bit process


//size of nint = 4
//size of nuint = 4

Sie können den entsprechenden Wert auch aus den statischen Eigenschaften IntPtr.Size und UIntPtr.Size abrufen.

MinValue und MaxValue


Um die Mindest- und Höchstwerte von Integerwerten mit nativer Größe zur Laufzeit abzurufen, verwenden Sie
MinValue und MaxValue als statische Eigenschaften mit den Schlüsselwörtern nint und nuint , wie im
folgenden Beispiel gezeigt:
Console.WriteLine($"nint.MinValue = {nint.MinValue}");
Console.WriteLine($"nint.MaxValue = {nint.MaxValue}");
Console.WriteLine($"nuint.MinValue = {nuint.MinValue}");
Console.WriteLine($"nuint.MaxValue = {nuint.MaxValue}");

// output when run in a 64-bit process


//nint.MinValue = -9223372036854775808
//nint.MaxValue = 9223372036854775807
//nuint.MinValue = 0
//nuint.MaxValue = 18446744073709551615

// output when run in a 32-bit process


//nint.MinValue = -2147483648
//nint.MaxValue = 2147483647
//nuint.MinValue = 0
//nuint.MaxValue = 4294967295

Konstanten
Sie können konstante Werte in den folgenden Bereichen verwenden:
Für nint : Int32.MinValue bis Int32.MaxValue.
Für nuint : UInt32.MinValue bis UInt32.MaxValue.

Konvertierungen
Der Compiler stellt implizite und explizite Konvertierungen in andere numerische Typen bereit. Weitere
Informationen finden Sie unter Integrierte numerische Konvertierungen (C#-Referenz) (Integrierte numerische
Konvertierungen).

Literale
Es gibt keine direkte Syntax für Integerliterale mit nativer Größe. Es gibt kein Suffix, das angibt, dass es sich bei
einem Literal um einen Integerwert mit nativer Größe handelt (wie L , um long anzugeben). Sie können
stattdessen implizite oder explizite Umwandlungen von anderen Integerwerten verwenden. Beispiel:

nint a = 42
nint a = (nint)42;

Nicht unterstützte IntPtr/UIntPtr-Member


Die folgenden Member von IntPtr und UIntPtr werden für nint - und nuint -Typen nicht unterstützt:
Parametrisierte Konstruktoren
Add(IntPtr, Int32)
CompareTo
Size: Verwenden Sie stattdessen sizeOf(). Obwohl nint.Size nicht unterstützt wird, können Sie IntPtr.Size
verwenden, um einen entsprechenden Wert zu erhalten.
Subtract(IntPtr, Int32)
ToInt32
ToInt64
ToPointer
Zero: Verwenden Sie stattdessen 0.
C#-Sprachspezifikation
Weitere Informationen finden Sie in der C#-Sprachspezifikation und im Abschnitt Integerwerte mit nativer
Größe in den C# 9.0-Hinweisen zu Featurevorschlägen.

Siehe auch
C#-Referenz
Werttypen
Integrale numerische Typen
Integrierte numerischer Konvertierungen
Numerische Gleitkommatypen (C#-Referenz)
04.11.2021 • 3 minutes to read

Die numerischen Gleitkommatypen stellen reelle Zahlen dar. Alle numerischen Gleitkommatypen sind
Werttypen. Sie sind auch einfache Typen und können mit Literalen initialisiert werden. Alle numerischen
Gleitkommatypen unterstützen arithmetic-, comparison- und equality-Operatoren.

Merkmale der Gleitkommatypen


C# unterstützt die folgenden vordefinierten Gleitkommatypen:

C #-
T Y P / SC H L ÜSSEL W O R UN GEFÄ H RER
T B EREIC H GEN A UIGK EIT GRÖ SSE . N ET - T Y P

float ±1.5 × 10− 45 zu ~6–9 Stellen 4 Bytes System.Single


±3.4 × 1038

double ±5,0 × 10− 324 bis ~15–17 Stellen 8 Bytes System.Double


±1,7 × 10308

decimal ±1.0 × 10-28 to 28-29 Stellen 16 Bytes System.Decimal


±7.9228 × 1028

In der obigen Tabelle ist jedes C#-Typschlüsselwort aus der äußerst linken Spalte ein Alias für den
entsprechenden .NET-Typ. Sie können synonym verwendet werden. In den folgenden Deklarationen werden
beispielsweise Variablen des gleichen Typs deklariert:

double a = 12.3;
System.Double b = 12.3;

Der Standardwert jedes Gleitkommatyps ist Null ( 0 ). Die einzelnen Gleitkommatypen verfügen jeweils über die
Konstanten MinValue und MaxValue , die den minimalen und maximalen Endwert des Typs angeben. Die Typen
float und double verfügen auch über Konstanten, die nicht numerische Werte und Unendlichkeitswerte
darstellen. Der Typ double verfügt beispielsweise über folgende Konstanten: Double.NaN,
Double.NegativeInfinity und Double.PositiveInfinity.
Der decimal -Typ ist geeignet, wenn der erforderliche Genauigkeitsgrad durch die Anzahl der Ziffern rechts vom
Dezimaltrennzeichen bestimmt wird. Solche Zahlen werden häufig in Finanzanwendungen, für
Währungsbeträge (z. B. $1,00), bei Zinssätzen (z. B. 2,625 %) usw. verwendet. Gerade Zahlen, die nur auf eine
Dezimalstelle genau sind, werden vom decimal -Typ genauer behandelt: 0,1 kann z. B. durch eine decimal -
Instanz genau dargestellt werden, während keine double - oder float -Instanz 0,1 genau darstellt. Aufgrund
dieses Unterschieds bei numerischen Typen können unerwartete Rundungsfehler in arithmetischen
Berechnungen auftreten, wenn Sie double oder float für Dezimaldaten verwenden. Sie können double
anstelle von decimal verwenden, wenn die Optimierung der Leistung wichtiger ist, als die Genauigkeit
sicherzustellen. Allerdings würde jeder Unterschied in der Leistung bei allen Anwendungen außer den
berechnungsintensivsten unbemerkt bleiben. Ein weiterer möglicher Grund, decimal zu vermeiden, ist das
Minimieren der Speicheranforderungen. Beispielsweise verwendet ML.NET float , da der Unterschied zwischen
4 Bytes und 16 Bytes bei sehr großen Datasets ins Gewicht fällt. Weitere Informationen finden Sie unter
System.Decimal.
Sie können integrale Typen sowie die Typen float und double in einem Ausdruck kombinieren. In diesem Fall
werden integrale Typen implizit in einen der Gleitkommatypen konvertiert. Bei Bedarf wird der float -Typ
implizit in double konvertiert. Der Ausdruck wird wie folgt ausgewertet:
Wenn der double -Typ im Ausdruck vorhanden ist, wird der Ausdruck in double oder in relationalen
Vergleichen oder Vergleichen auf Gleichheit in bool ausgewertet.
Wenn der double -Typ im Ausdruck vorhanden ist, wird der Ausdruck in float oder in relationalen
Vergleichen oder Vergleichen auf Gleichheit in bool ausgewertet.
Sie können integrale Typen und den decimal -Typ auch in einem Ausdruck miteinander kombinieren. In diesem
Fall werden integrale Typen implizit in den decimal -Typ konvertiert, und der Ausdruck wird als decimal oder
bool in relationalen Vergleichen und Gleichheitsvergleichen ausgewertet.

In einem Ausdruck können Sie den decimal -Typ nicht mit den Typen float und double kombinieren. Wenn
Sie aber einen arithmetischen Vorgang, einen Vergleich oder einen Gleichheitsvorgang durchführen möchten,
müssen Sie, wie nachfolgend dargestellt, in diesem Fall die Operanden explizit aus dem oder in den decimal -
Typ konvertieren

double a = 1.0;
decimal b = 2.1m;
Console.WriteLine(a + (double)b);
Console.WriteLine((decimal)a + b);

Zum Formatieren eines Gleitkommawerts können Sie standardmäßige Zahlenformatzeichenfolgen oder


benutzerdefinierte Zahlenformatzeichenfolgen verwenden.

Real-Literale
Der Typ eines Real-Literals wird wie folgt durch sein Suffix bestimmt:
Das Literal ohne Suffix oder mit dem Suffix d oder D ist vom Typ double .
Das Literal mit dem Suffix f oder F ist vom Typ float .
Das Literal mit dem Suffix m oder M ist vom Typ decimal .

Der folgende Code zeigt ein Beispiel für jeden Typ:

double d = 3D;
d = 4d;
d = 3.934_001;

float f = 3_000.5F;
f = 5.4f;

decimal myMoney = 3_000.5m;


myMoney = 400.75M;

Das vorherige Beispiel zeigt auch die Verwendung von _ als Zifferntrennzeichen, das ab C# 7.0 unterstützt
wird. Sie können das Zifferntrennzeichen mit allen Arten numerischer Literale verwenden.
Sie können auch die wissenschaftliche Notation verwenden, d. h. einen exponentiellen Teil eines Real-Literals
angeben, wie das folgende Beispiel zeigt:
double d = 0.42e2;
Console.WriteLine(d); // output 42

float f = 134.45E-2f;
Console.WriteLine(f); // output: 1.3445

decimal m = 1.5E6m;
Console.WriteLine(m); // output: 1500000

Konvertierungen
Es gibt nur eine implizite Konvertierung zwischen numerischen Gleitkommatypen: von float zu double .
Allerdings können Sie einen Gleitkommatyp mit der expliziten Umwandlungin beliebige andere
Gleitkommatypen konvertieren. Weitere Informationen finden Sie unter Integrierte numerische
Konvertierungen (C#-Referenz) (Integrierte numerische Konvertierungen).

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Gleitkommatypen
Der Dezimaltyp
Real-Literale

Siehe auch
C#-Referenz
Werttypen
Integrale Typen
Standardmäßige Zahlenformatzeichenfolgen
Numerische Ausdrücke in .NET
System.Numerics.Complex
Integrierte numerische Konvertierungen (C#-
Referenz)
04.11.2021 • 4 minutes to read

C# bietet eine Reihe von integralen numerischen Typen und numerischen Gleitkommatypen. Es gibt eine
Konvertierung zwischen zwei beliebigen numerischen Typen, entweder implizit oder explizit. Sie müssen einen
Cast-Ausdruck verwenden, um eine explizite Konvertierung durchzuführen.

Implizite numerische Konvertierungen


Folgende Tabelle veranschaulicht vordefinierte implizite Konvertierungen zwischen integrierten numerischen
Typen:

VO N B ESC H REIB UN G

sbyte short , int , long , float , double , decimal oder


nint

byte short , ushort , int , uint , long , ulong , float ,


double , decimal , nint oder nuint

short int , long , float , double oder decimal oder nint

ushort int , uint , long , ulong , float , double oder


decimal , nint oder nuint

int long , float , double oder decimal , nint

uint long , ulong , float , double oder decimal oder


nuint

long float , double oder decimal

ulong float , double oder decimal

float double

nint long , float , double oder decimal

nuint ulong , float , double oder decimal

NOTE
Die impliziten Konvertierungen aus int , uint , long , ulong , nint oder nuint in float und aus long ,
ulong , nint oder nuint in double können einen Verlust an Genauigkeit verursachen, aber niemals einen Verlust in
einer Größenordnung. Die anderen impliziten numerischen Konvertierungen verlieren nie Informationen.

Beachten Sie außerdem:


Jeder integrale numerische Typ ist implizit in jeden numerischen Gleitkommatypen konvertierbar.
Es gibt keine impliziten Konvertierungen in die Typen byte und sbyte . Es gibt keine impliziten
Konvertierungen aus den Typen double und decimal .
Es gibt keine impliziten Konvertierungen zwischen dem Typ decimal und dem Typ float oder double .
Ein Wert eines konstanten Ausdrucks vom Typ int (z. B. ein Wert, der durch ein ganzzahliges Literal
dargestellt wird), kann implizit in sbyte , byte , short , ushort , uint , ulong , nint oder nuint
konvertiert werden, wenn er sich im Bereich des Zieltyps befindet:

byte a = 13;
byte b = 300; // CS0031: Constant value '300' cannot be converted to a 'byte'

Wie das vorherige Beispiel zeigt, tritt der Compilerfehler CS0031 auf, wenn der konstante Wert nicht
innerhalb des Bereichs des Zieltyps liegt.

Explizite numerische Konvertierungen


Folgende Tabelle veranschaulicht vordefinierte explizite Konvertierungen zwischen integrierten numerischen
Typen, für die es keine implizite Konvertierung gibt:

VO N B ESC H REIB UN G

sbyte byte , ushort , uint oder ulong oder nuint

byte sbyte

short sbyte , byte , ushort , uint , ulong oder nuint

ushort sbyte , byte oder short

int sbyte , byte , short , ushort , uint , ulong oder


nuint

uint sbyte , byte , short , ushort oder int

long sbyte , byte , short , ushort , int , uint , ulong ,


nint oder nuint

ulong sbyte , byte , short , ushort , int , uint , long ,


nint oder nuint

float sbyte , byte , short , ushort , int , uint , long ,


ulong , decimal , nint oder nuint

double sbyte , byte , short , ushort , int , uint , long ,


ulong , float , decimal , nint oder nuint

decimal sbyte , byte , short , ushort , int , uint , long ,


ulong , float , double , nint oder nuint
VO N B ESC H REIB UN G

nint sbyte , byte , short , ushort , int , uint , ulong


oder nuint

nuint sbyte , byte , short , ushort , int , uint , long


oder nint

NOTE
Eine explizite numerische Konvertierung kann zu Datenverlust führen oder eine Ausnahme auslösen, typischerweise ein
OverflowException.

Beachten Sie außerdem:


Wenn Sie einen Wert von einem integralen Typ in einen anderen integralen Typ konvertieren, ist das
Ergebnis vom Kontext der Überlaufprüfung abhängig. Die Konvertierung in einem geprüften Kontext ist
erfolgreich, wenn der Quellwert sich innerhalb des Bereichs des Zieltyps befindet. Andernfalls wird eine
OverflowException ausgelöst. In einem ungeprüften Kontext ist die Konvertierung immer erfolgreich, und
sie verläuft wie folgt:
Wenn der Quelltyp größer als der Zieltyp ist, wird der Quellwert abgeschnitten, indem die
wichtigsten „zusätzlichen“ Teile verworfen werden. Das Ergebnis wird dann als Wert des Zieltyps
behandelt.
Wenn der Quelltyp kleiner als der Zieltyp ist, ist der Quellwert entweder signaturerweitert oder
mit Null erweitert, sodass er die gleiche Größe wie der Zieltyp hat. Die Vorzeichenerweiterung
wird verwendet, wenn der Quelltyp mit einem Vorzeichen versehen ist. Die Erweiterung mit Nullen
(0) wird verwendet, wenn der Quelltyp mit keinem Vorzeichen versehen ist. Das Ergebnis wird
dann als Wert des Zieltyps behandelt.
Wenn der Quelltyp die gleiche Größe wie der Zieltyp aufweist, wird der Quellwert als Wert vom
Zieltyp behandelt.
Wenn Sie einen decimal -Wert in einen integralen Typ konvertieren, wird dieser Wert Richtung 0 (null)
auf den nächsten Integralwert gerundet. Wenn der erzeugte Integralwert sich außerhalb des Bereichs des
Zieltyps befindet, wird eine OverflowException ausgelöst.
Wenn Sie einen double - oder float -Wert in einen integralen Typ konvertieren, wird dieser Wert
Richtung 0 (null) auf den nächsten Integralwert gerundet. Wenn der resultierende Integralwert sich
außerhalb des Bereichs des Zieltyps befindet, hängt das Ergebnis vom Kontext der Überlaufprüfung ab. In
einem geprüften Kontext wird eine OverflowException ausgelöst, während das Ergebnis in einem
ungeprüften Kontext ein nicht angegebener Wert des Zieltyps ist.
Wenn Sie double in float konvertieren, wird der double -Wert auf den nächsten float -Wert
gerundet. Wenn der double -Wert zu klein oder zu groß ist, um in den float -Typ zu passen, ist das
Ergebnis 0 (null) oder unendlich.
Wenn Sie float oder double in decimal konvertieren, wird der Quellwert in eine decimal -Darstellung
konvertiert und bei Bedarf auf die nächste Zahl nach der achtundzwanzigsten Dezimalstelle gerundet. Je
nach Wert des Quellwerts kann eines der folgenden Ergebnisse auftreten:
Wenn der Quellwert zu klein ist, als dass er als decimal dargestellt werden könnte, ist das
Ergebnis 0 (null).
Wenn der Quellwert NaN (nicht numerisch), unendlich oder zu groß ist, um als decimal
dargestellt zu werden, wird eine OverflowException ausgelöst.
Wenn Sie decimal in float oder double konvertieren, wird der Quellwert in den nächsten float -
oder double -Wert konvertiert.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Implizite numerische Konvertierungen
Explizite numerische Konvertierungen

Siehe auch
C#-Referenz
Umwandlung und Typkonvertierungen
bool (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort vom Typ bool ist ein Alias für den .NET-Strukturtyp System.Boolean, der einen booleschen
Wert ( true oder false ) darstellt.
Um logische Operationen mit Werten vom Typ bool durchzuführen, verwenden Sie die booleschen
Logikoperatoren. Der Typ bool ist der Ergebnistyp von Vergleichs- und Gleichheitsoperatoren. Ein bool -
Ausdruck kann ein steuernder bedingter Ausdruck in if-, do-, while- und for-Anweisungen und im bedingten
Operator ?: sein.
Der Standardwert des Typs bool ist false .

Literale
Sie können die Literale true und false verwenden, um eine bool -Variable zu initialisieren oder einen bool -
Wert zu übergeben:

bool check = true;


Console.WriteLine(check ? "Checked" : "Not checked"); // output: Checked

Console.WriteLine(false ? "Checked" : "Not checked"); // output: Not checked

Dreiwertige boolesche Logik


Verwenden Sie den Nullable-Typ bool? , wenn Sie dreiwertige Logik unterstützen müssen (wenn Sie
beispielsweise mit Datenbanken arbeiten, die einen dreiwertigen booleschen Typ unterstützen). Für die bool? -
Operanden unterstützen die vordefinierten & - und | -Operatoren die dreiwertige Logik. Weitere
Informationen finden Sie im Abschnitt Boolesche logische Operatoren, die NULL-Werte zulassen im Artikel
Boolesche logische Operatoren.
Weitere Informationen zu Nullable-Werttypen finden Sie unter Nullable-Werttypen.

Konvertierungen
C# bietet nur zwei Konvertierungen, die den Typ bool beinhalten. Dabei handelt es sich um eine implizite
Konvertierung in den entsprechenden Nullable-Typ bool? und eine explizite Konvertierung aus dem bool? -
Typ. .NET bietet jedoch zusätzliche Methoden, die Sie verwenden können, um in den oder aus dem Typ bool zu
konvertieren. Weitere Informationen finden Sie im Abschnitt Konvertieren in boolesche Werte und aus
booleschen Werten auf der System.Boolean-API-Referenzseite.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Der Typ „bool“ in der C#-Sprachspezifikation.

Weitere Informationen
C#-Referenz
Werttypen
true- und false-Operatoren
char (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort vom Typ char ist ein Alias für den .NET-System.Char-Strukturtyp, der ein Unicode-UTF-16-
Zeichen darstellt.

TYP B EREIC H GRÖ SSE . N ET - T Y P

char U+0000 in U+FFFF 16 Bit System.Char

Der Standardwert des char -Typs ist \0 , d. h. U+0000.


Der -Typ unterstützt Vergleichs-, Gleichheits-, Inkrement- und Dekrement-Operatoren. Außerdem wird für
char
char -Operanden, arithmetische und bitweise logische Operatoren ein Vorgang für die entsprechenden
Zeichencodes durchgeführt und ein Ergebnis des int -Typs erzeugt.
Der string-Typ stellt Text als Sequenz von char -Werten dar.

Literale
Sie können einen char -Wert mit Folgendem angeben:
einem Zeichenliteral.
einer Escapesequenz für Unicodezeichen, d. h. \u gefolgt von der aus vier Symbolen bestehenden
Hexadezimaldarstellung eines Zeichencodes.
einer Escapesequenz für Hexadezimalzahlen, d. h. \x gefolgt von der Hexadezimaldarstellung eines
Zeichencodes.

var chars = new[]


{
'j',
'\u006A',
'\x006A',
(char)106,
};
Console.WriteLine(string.Join(" ", chars)); // output: j j j j

Wie das obige Beispiel zeigt, können Sie den Wert eines Zeichencodes auch in den entsprechenden char -Wert
umwandeln.

NOTE
Im Falle einer Escapesequenz für Unicodezeichen müssen Sie alle vier Hexadezimalziffern angeben. \u006A ist also eine
gültige Escapesequenz, \u06A und \u6A sind hingegen nicht gültig.
Bei einer Escapesequenz für Hexadezimalzahlen können Sie die führenden Nullen weglassen. Die Escapesequenzen
\x006A , \x06A und \x6A sind also gültig und entsprechen demselben Zeichen.

Konvertierungen
Der char -Typ kann implizit in die folgenden ganzzahligen Typen konvertiert werden: ushort , int , uint ,
long und ulong . Zudem lässt er sich auch implizit in diese integrierten numerischen Gleitkommatypen
konvertieren: float , double und decimal . Er kann explizit in die ganzzahligen Typen sbyte , byte und short
konvertiert werden.
Es gibt keine impliziten Konvertierungen anderen Typen in Typ char . Alle ganzzahligen numerischen Typen
oder numerischen Gleitkommatypen lassen sich jedoch explizit in char konvertieren.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Integrale Typen der C#-Sprachspezifikation.

Siehe auch
C#-Referenz
Werttypen
Zeichenfolgen
System.Text.Rune
Zeichencodierung in .NET
Enumerationstypen (C#-Referenz)
04.11.2021 • 3 minutes to read

Ein Enumerationstyp (oder enum-Typ) ist ein Werttyp, der durch eine Reihe benannter Konstanten des zugrunde
liegenden integralen numerischen Typs definiert wird. Um einen Enumerationstyp zu definieren, verwenden Sie
das enum -Schlüsselwort und geben die Namen von Enumerationsmembern an:

enum Season
{
Spring,
Summer,
Autumn,
Winter
}

Standardmäßig sind die zugeordneten Konstantenwerte von Enumerationsmembern vom Typ int . Sie
beginnen mit null und erhöhen sich um eins gemäß der Definitionstextreihenfolge. Sie können einen beliebigen
anderen integralen numerischen Typ als zugrunde liegenden Typ eines Enumerationstyps explizit angeben. Sie
können auch explizit die zugeordneten Konstantenwerte angeben, wie im folgenden Beispiel gezeigt:

enum ErrorCode : ushort


{
None = 0,
Unknown = 1,
ConnectionLost = 100,
OutlierReading = 200
}

In der Definition eines Enumerationstyps kann keine Methode definiert werden. Zum Hinzufügen von
Funktionen zu einem Enumerationstyp erstellen Sie eine Erweiterungsmethode.
Der Standardwert eines Enumerationstyps E ist der Wert, der vom Ausdruck (E)0 generiert wird, auch wenn
NULL nicht über den entsprechenden Enumerationsmember verfügt.
Sie verwenden einen Enumerationstyp, um eine Auswahl aus einer Reihe von sich gegenseitig ausschließenden
Werten oder eine Kombination aus Auswahlmöglichkeiten darzustellen. Um eine Kombination aus
Auswahlmöglichkeiten darzustellen, definieren Sie einen Enumerationstyp als Bitflags.

Enumerationstypen als Bitflags


Wenn ein Enumerationstyp eine Kombination aus Auswahlmöglichkeiten darstellen soll, definieren Sie
Enumerationsmember für diese Auswahlmöglichkeiten, sodass eine einzelne Auswahl ein Bitfeld ist. Das heißt,
die zugeordneten Werte dieser Enumerationsmember sollten Zweierpotenzen sein. Anschließend können Sie
die bitweisen logischen Operatoren | oder & verwenden, um Auswahlmöglichkeiten bzw. Schnittmengen von
Auswahlmöglichkeiten zu kombinieren. Um anzugeben, dass ein Enumerationstyp Bitfelder deklariert, wenden
Sie das Attribut Flags darauf an. Wie im folgenden Beispiel gezeigt, können Sie auch einige typische
Kombinationen in die Definition eines Enumerationstyps einschließen.
[Flags]
public enum Days
{
None = 0b_0000_0000, // 0
Monday = 0b_0000_0001, // 1
Tuesday = 0b_0000_0010, // 2
Wednesday = 0b_0000_0100, // 4
Thursday = 0b_0000_1000, // 8
Friday = 0b_0001_0000, // 16
Saturday = 0b_0010_0000, // 32
Sunday = 0b_0100_0000, // 64
Weekend = Saturday | Sunday
}

public class FlagsEnumExample


{
public static void Main()
{
Days meetingDays = Days.Monday | Days.Wednesday | Days.Friday;
Console.WriteLine(meetingDays);
// Output:
// Monday, Wednesday, Friday

Days workingFromHomeDays = Days.Thursday | Days.Friday;


Console.WriteLine($"Join a meeting by phone on {meetingDays & workingFromHomeDays}");
// Output:
// Join a meeting by phone on Friday

bool isMeetingOnTuesday = (meetingDays & Days.Tuesday) == Days.Tuesday;


Console.WriteLine($"Is there a meeting on Tuesday: {isMeetingOnTuesday}");
// Output:
// Is there a meeting on Tuesday: False

var a = (Days)37;
Console.WriteLine(a);
// Output:
// Monday, Wednesday, Saturday
}
}

Weitere Informationen und Beispiele finden Sie auf der Referenzseite zur System.FlagsAttribute-API und im
Abschnitt Nicht exklusive Member und das Flags-Attribut der Referenzseite zur System.Enum-API.

Der System.Enum-Typ und die enum-Einschränkung


Der System.Enum-Typ ist die abstrakte Basisklasse aller Enumerationstypen. Er bietet eine Reihe von Methoden,
um Informationen zu einem Enumerationstyp und seinen Werten abzurufen. Weitere Informationen und
Beispiele finden Sie auf der Referenzseite zur System.Enum-API.
Ab C# 7.3 können Sie System.Enum in einer Basisklasseneinschränkung (die als enum-Einschränkung bezeichnet
wird) verwenden, um anzugeben, dass ein Typparameter ein Enumerationstyp ist. Jeder Enumerationstyp erfüllt
auch die struct -Einschränkung, die verwendet wird, um anzugeben, dass ein Typparameter ein Non-Nullable-
Werttyp ist.

Konvertierungen
Für jeden Enumerationstyp gibt es explizite Konvertierungen zwischen dem Enumerationstyp und dem
zugrunde liegenden integralen Typ. Wenn Sie einen Enumerationswert in den zugrunde liegenden Typ
umwandeln, ist das Ergebnis der zugeordnete integrale Wert eines Enumerationsmembers.
public enum Season
{
Spring,
Summer,
Autumn,
Winter
}

public class EnumConversionExample


{
public static void Main()
{
Season a = Season.Autumn;
Console.WriteLine($"Integral value of {a} is {(int)a}"); // output: Integral value of Autumn is 2

var b = (Season)1;
Console.WriteLine(b); // output: Summer

var c = (Season)4;
Console.WriteLine(c); // output: 4
}
}

Verwenden Sie die Enum.IsDefined-Methode, um zu ermitteln, ob ein Enumerationstyp einen


Enumerationsmember mit dem bestimmten zugeordneten Wert enthält.
Für jeden Enumerationstyp gibt es Boxing- und Unboxing-Konvertierungen in bzw. aus dem System.Enum-Typ.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Enumerationen
Enum values and operations (Enumerationswerte und -vorgänge)
Logische Enumerationsoperatoren
Enumerationsvergleichsoperatoren
Explizite Enumerationskonvertierungen
Implizite Enumerationskonvertierungen

Siehe auch
C#-Referenz
Enumerationsformatzeichenfolgen
Entwurfsrichtlinien: Enumerationsentwurf
Entwurfsrichtlinien: Namenskonventionen für Enumerationen
switch -Ausdruck
switch -Anweisung
Strukturtypen (C#-Referenz)
04.11.2021 • 10 minutes to read

Ein Strukturtyp (oder struct type) ist ein Werttyp, der Daten und zugehörige Funktionen kapseln kann.
Verwenden Sie das struct -Schlüsselwort, um einen Strukturtyp zu definieren:

public struct Coords


{
public Coords(double x, double y)
{
X = x;
Y = y;
}

public double X { get; }


public double Y { get; }

public override string ToString() => $"({X}, {Y})";


}

Strukturtypen verfügen über eine Wertsemantik. Das heißt, eine Variable eines Strukturtyps enthält eine Instanz
des Typs. Standardmäßig werden die Variablenwerte bei der Zuweisung kopiert, dabei handelt es sich um die
Übergabe eines Arguments an eine Methode oder die Rückgabe eines Methodenergebnisses. Bei
Strukturtypvariablen wird eine Instanz des Typs kopiert. Weitere Informationen finden Sie unter Werttypen.
In der Regel werden Strukturtypen zum Entwerfen kleiner datenorientierter Typen verwendet, die wenig oder
gar kein Verhalten bereitstellen. Beispielsweise verwendet .NET Strukturtypen, um Zahlen (sowohl Integer als
auch reelle Zahlen), boolesche Werte, Unicode-Zeichen und Zeitinstanzen darzustellen. Wenn Sie das Verhalten
eines Typs verwenden möchten, sollten Sie eine Klasse definieren. Klassentypen verfügen über Verweissemantik.
Das heißt, eine Variable eines Klassentyps enthält einen Verweis auf eine Instanz des Typs, nicht die Instanz
selbst.
Da Strukturtypen eine Wertsemantik nutzen, wird die Definition von unveränderlichen Strukturtypen
empfohlen.

readonly -Struktur
Ab C# 7.2 können Sie mit dem readonly -Modifizierer einen Strukturtyp als unveränderlich deklarieren. Alle
Datenmember einer readonly -Struktur müssen als schreibgeschützt gekennzeichnet sein:
Alle Felddeklarationen müssen den readonly -Modifizierer aufweisen.
Alle Eigenschaften, auch automatisch implementierte, müssen schreibgeschützt sein. In C# 9.0 und höher
kann eine Eigenschaft einen init -Accessor aufweisen.
Auf diese Weise ist garantiert, dass kein Member einer readonly -Struktur den Status der Struktur ändert. In
C# 8.0 und höher bedeutet dies, dass andere Instanzmember mit Ausnahme von Konstruktoren implizit
readonly werden.

NOTE
In einer readonly -Struktur kann ein Datenmember eines änderbaren Verweistyps weiterhin den eigenen Status ändern.
Beispielsweise können Sie eine List<T>-Instanz nicht ersetzen, aber neue Elemente zur Instanz hinzufügen.
Der folgende Code definiert eine readonly -Struktur mit Nur-init-Eigenschaftensettern, die in C# 9.0 und höher
verfügbar sind:

public readonly struct Coords


{
public Coords(double x, double y)
{
X = x;
Y = y;
}

public double X { get; init; }


public double Y { get; init; }

public override string ToString() => $"({X}, {Y})";


}

readonly -Instanzmember
Ab C# 8.0 können Sie auch den readonly -Modifizierer verwenden, um zu deklarieren, dass ein Instanzmember
den Zustand einer Struktur nicht ändert. Wenn Sie nicht den gesamten Strukturtyp als readonly deklarieren
können, verwenden Sie den readonly -Modifizierer, um die Instanzmember zu markieren, die den Zustand der
Struktur nicht ändern.
Innerhalb eines readonly -Instanzmembers können Sie den Instanzfeldern einer Struktur nichts zuweisen. Ein
readonly -Member kann jedoch ein Nicht- readonly -Member aufrufen. In diesem Fall erstellt der Compiler eine
Kopie der Strukturinstanz und ruft den Nicht- readonly -Member in dieser Kopie auf. Folglich wird die
ursprüngliche Strukturinstanz nicht geändert.
In der Regel wenden Sie den readonly -Modifizierer auf die folgenden Arten von Instanzmembern an:
Methoden:

public readonly double Sum()


{
return X + Y;
}

Sie können den readonly -Modifizierer auch auf Methoden anwenden, die in System.Object deklarierte
Methoden überschreiben:

public readonly override string ToString() => $"({X}, {Y})";

Eigenschaften und Indexer:

private int counter;


public int Counter
{
readonly get => counter;
set => counter = value;
}

Wenn Sie den readonly -Modifizierer auf die Accessoren sowohl einer Eigenschaft als auch eines
Indexers anwenden müssen, wenden Sie ihn in der Deklaration der Eigenschaft bzw. des Indexers an.
NOTE
Der Compiler deklariert einen get -Accessor einer automatisch implementierten Eigenschaft als readonly ,
unabhängig davon, ob der readonly -Modifizierer in einer Eigenschaftsdeklaration vorhanden ist.

In C# 9.0 und höher können Sie den readonly -Modifizierer auf eine Eigenschaft oder einen Indexer mit
einem init -Accessor anwenden:

public readonly double X { get; init; }

Sie können den readonly -Modifizierer nicht auf statische Member eines Strukturtyps anwenden.
Der Compiler kann den readonly -Modifizierer für Leistungsoptimierungen verwenden. Weitere Informationen
finden Sie unter Schreiben von sicherem und effizientem C#-Code.

Nichtdestruktive Mutation
Ab C# 10.0 können Sie den with -Ausdruck verwenden, wenn Sie unveränderliche Eigenschaften oder Felder
einer Strukturtypinstanz verändern müssen. Ein with -Ausdruck erstellt eine Kopie seines Operanden mit
angegebenen Eigenschaften und geänderten Feldern. Sie verwenden die Objektinitialisierersyntax, um
anzugeben, welche Member bearbeitet und welche neuen Werte dazu verwendet werden sollen, wie im
folgenden Beispiel gezeigt:

public readonly struct Coords


{
public Coords(double x, double y)
{
X = x;
Y = y;
}

public double X { get; init; }


public double Y { get; init; }

public override string ToString() => $"({X}, {Y})";


}

public static void Main()


{
var p1 = new Coords(0, 0);
Console.WriteLine(p1); // output: (0, 0)

var p2 = p1 with { X = 3 };
Console.WriteLine(p2); // output: (3, 0)

var p3 = p1 with { X = 1, Y = 4 };
Console.WriteLine(p3); // output: (1, 4)
}

Einschränkungen beim Entwerfen eines Strukturtyps


Beim Entwerfen eines Strukturtyps verfügen Sie über dieselben Funktionen wie bei einem Klassentyp mit
folgenden Ausnahmen:
Sie können keinen parameterlosen Konstruktor deklarieren. Jeder Strukturtyp stellt bereits einen
impliziten parameterlosen Konstruktor bereit, der den Standardwert des Typs erzeugt.
NOTE
Ab C# 10.0 können Sie einen parameterlosen Konstruktor in einem Strukturtyp deklarieren. Weitere
Informationen finden Sie im Abschnitt Parameterlose Konstruktoren und Feldinitialisierer.

Sie können kein Instanzfeld und keine Eigenschaft in der Deklaration initialisieren. Sie können jedoch ein
static- oder const-Feld oder eine statische Eigenschaft in der Deklaration initialisieren.

NOTE
Ab C# 10.0 können Sie ein Instanzfeld oder eine Eigenschaft in der Deklaration initialisieren. Weitere
Informationen finden Sie im Abschnitt Parameterlose Konstruktoren und Feldinitialisierer.

Der Konstruktor einer Strukturtyps muss alle Instanzfelder des Typs initialisieren.
Ein Strukturtyp kann nicht von einer anderen Klasse oder einem anderen Strukturtyp erben, und er kann
nicht die Basis einer Klasse sein. Allerdings kann ein Strukturtyp Schnittstellen implementieren.
Innerhalb eines Strukturtyps können Sie keinen Finalizer deklarieren.

Parameterlose Konstruktoren und Feldinitialisierer


Ab C# 10.0 können Sie einen parameterlosen Instanzkonstruktor in einem Strukturtyp deklarieren, wie im
folgenden Beispiel gezeigt:

public readonly struct Measurement


{
public Measurement()
{
Value = double.NaN;
Description = "Undefined";
}

public Measurement(double value, string description)


{
Value = value;
Description = description;
}

public double Value { get; init; }


public string Description { get; init; }

public override string ToString() => $"{Value} ({Description})";


}

public static void Main()


{
var m1 = new Measurement();
Console.WriteLine(m1); // output: NaN (Undefined)

var m2 = default(Measurement);
Console.WriteLine(m2); // output: 0 ()

var ms = new Measurement[2];


Console.WriteLine(string.Join(", ", ms)); // output: 0 (), 0 ()
}

Wie das vorherige Beispiel zeigt, ignoriert der Standardwertausdruck einen parameterlosen Konstruktor und
erzeugt den Standardwert eines Strukturtyps. Dieser Wert wird generiert, indem alle Werttypfelder auf ihre
Standardwerte (das 0-Bit-Muster) und alle Verweistypfelder auf null festgelegt werden. Bei der Instanziierung
des Strukturtyparrays wird ein parameterloser Konstruktor ebenfalls ignoriert und ein Array erzeugt, das mit
den Standardwerten eines Strukturtyps aufgefüllt wird.
Ab C# 10.0 können Sie auch ein Instanzfeld oder eine Instanzeigenschaft in der Deklaration initialisieren, wie im
folgenden Beispiel gezeigt:

public readonly struct Measurement


{
public Measurement(double value)
{
Value = value;
}

public Measurement(double value, string description)


{
Value = value;
Description = description;
}

public double Value { get; init; }


public string Description { get; init; } = "Ordinary measurement";

public override string ToString() => $"{Value} ({Description})";


}

public static void Main()


{
var m1 = new Measurement(5);
Console.WriteLine(m1); // output: 5 (Ordinary measurement)

var m2 = new Measurement();


Console.WriteLine(m2); // output: 0 ()
}

Wenn Sie einen parameterlosen Konstruktor nicht explizit deklarieren, stellt ein Strukturtyp einen
parameterlosen Konstruktor mit folgendem Verhalten bereit:
Wenn ein Strukturtyp über explizite Instanzkonstruktoren oder über keine Feldinitialisierer verfügt,
erzeugt ein impliziter parameterloser Konstruktor den Standardwert eines Strukturtyps, unabhängig von
Feldinitialisierern, wie im vorherigen Beispiel gezeigt.
Wenn ein Strukturtyp keine expliziten Instanzkonstruktoren, aber Feldinitialisierer aufweist, synthetisiert
der Compiler einen öffentlichen parameterlosen Konstruktor, der die angegebenen Feldinitialisierungen
ausführt, wie im folgenden Beispiel gezeigt:
public struct Coords
{
public double X = double.NaN;
public double Y = double.NaN;

public override string ToString() => $"({X}, {Y})";


}

public static void Main()


{
var p1 = new Coords();
Console.WriteLine(p1); // output: (NaN, NaN)

var p2 = default(Coords);
Console.WriteLine(p2); // output: (0, 0)

var ps = new Coords[3];


Console.WriteLine(string.Join(", ", ps)); // output: (0, 0), (0, 0), (0, 0)
}

Wie im vorherigen Beispiel gezeigt, werden Feldinitialisierer vom Standardwertausdruck und von der
Arrayinstanziierung ignoriert.
Weitere Informationen finden Sie im Featurevorschlagshinweis für parameterlose Strukturkonstruktoren.

Instanziierung eines Strukturtyps


In C# müssen Sie eine deklarierte Variable initialisieren, bevor sie verwendet werden kann. Da eine
Strukturtypvariable nicht den Wert null aufweisen kann (es sei denn, es handelt sich um eine Variable eines
Nullable-Werttyps), müssen Sie eine Instanz des entsprechenden Typs instanziieren. Dafür stehen verschiedene
Möglichkeiten zur Verfügung.
Normalerweise instanziieren Sie einen Strukturtyp, indem Sie einen entsprechenden Konstruktor mit dem
Operator new aufrufen. Jeder Strukturtyp verfügt über mindestens einen Konstruktor. Dabei handelt es sich um
einen impliziten parameterlosen Konstruktor, der den Standardwert des Typs erzeugt. Sie können einen
Standardwertausdruck auch zum Erzeugen des Standardwerts für einen Typ verwenden.
Wenn alle Instanzfelder eines Strukturtyps zugänglich sind, können Sie ihn auch ohne den new -Operator
instanziieren. In diesem Fall müssen Sie alle Instanzfelder vor der ersten Verwendung der Instanz initialisieren.
Das folgende Beispiel zeigt, wie Sie dabei vorgehen müssen:

public static class StructWithoutNew


{
public struct Coords
{
public double x;
public double y;
}

public static void Main()


{
Coords p;
p.x = 3;
p.y = 4;
Console.WriteLine($"({p.x}, {p.y})"); // output: (3, 4)
}
}

Verwenden Sie für integrierte Werttypen die entsprechenden Literale, um den Wert des Typs festzulegen.
Übergeben von Strukturtypvariablen als Verweis
Wenn Sie eine Strukturtypvariable als Argument an eine Methode übergeben oder einen Strukturtypvariable
einer Methode zurückgeben, wird die gesamte Instanz des Strukturtyps kopiert. Dies kann sich in
Hochleistungsszenarios mit großen Strukturtypen auf die Leistung Ihres Codes auswirken. Sie können das
Kopieren von Werten vermeiden, indem Sie eine Strukturtypvariable als Verweis übergeben. Verwenden Sie die
Methodenparametermodifizierer ref , out oder in , um anzugeben, dass ein Argument als Verweis
übergeben werden muss. Verwenden Sie ref returns, um ein Methodenergebnis als Verweis zurückzugeben.
Weitere Informationen finden Sie unter Schreiben von sicherem und effizientem C#-Code.

ref -Struktur
Ab C# 7.2 können Sie bei der Deklaration eines Strukturtyps den Modifizierer ref verwenden. Instanzen eines
ref -Strukturtyps werden auf dem Stapel zugeordnet und können nicht in den verwalteten Heap überwechseln.
Der Compiler grenzt die Nutzung von ref -Strukturtypen wie folgt ein, um dies sicherzustellen:
Eine ref -Struktur kann nicht der Elementtyp eines Arrays sein.
Eine ref -Struktur kann kein deklarierter Typ eines Felds einer Klasse oder einer Struktur sein, bei der es sich
um keine ref -Struktur handelt.
Eine ref -Struktur kann keine Schnittstellen implementieren.
Für eine ref -Struktur kann kein Boxing in System.ValueType oder System.Object durchgeführt werden.
Eine ref -Struktur kann kein Typargument sein.
Eine ref -Strukturvariable kann nicht von einem Lambdaausdruck oder einer lokalen Funktion erfasst
werden.
Eine ref -Strukturvariable kann nicht in einer async -Methode verwendet werden. Sie können ref -
Strukturvariablen jedoch in synchronen Methoden verwenden, z. B. in solchen, die Task oder Task<TResult>
zurückgeben.
Eine ref -Strukturvariable kann nicht in Iteratoren verwendet werden.

In der Regel definieren Sie einen ref -Strukturtyp, wenn Sie einen Typ benötigen, der auch Datenmember von
ref -Strukturtypen enthält:

public ref struct CustomRef


{
public bool IsValid;
public Span<int> Inputs;
public Span<int> Outputs;
}

Kombinieren Sie die readonly - und ref -Modifizierer in der Typdeklaration (der readonly -Modifizierer muss
vor dem ref -Modifizierer stehen), um eine ref -Struktur als readonly zu deklarieren:

public readonly ref struct ConversionRequest


{
public ConversionRequest(double rate, ReadOnlySpan<double> values)
{
Rate = rate;
Values = values;
}

public double Rate { get; }


public ReadOnlySpan<double> Values { get; }
}
In .NET sind System.Span<T> und System.ReadOnlySpan<T> Beispiele für eine ref -Struktur.

struct-Einschränkung
Sie können auch das struct -Schlüsselwort in der struct -Einschränkung verwenden, um anzugeben, dass ein
Typparameter ein Non-Nullable-Werttyp ist. Sowohl Struktur- als auch Enumerationstypen erfüllen die struct -
Einschränkung.

Konvertierungen
Für alle Strukturtypen (außer ref struct-Typen) gibt es Boxing- und Unboxing-Umwandlungen in und aus den
Typen System.ValueType und System.Object. Außerdem gibt es Boxing- und Unboxing-Umwandlungen
zwischen einem Strukturtyp und der Schnittstelle, die ihn implementiert.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Strukturen der C#-Sprachspezifikation.
Weitere Informationen zu in C# 7.2 und höher eingeführten Features finden Sie in den folgenden
Featurevorschlägen:
Schreibgeschützte Strukturen
Schreibgeschützte Instanzmember
Kompilierzeitsicherheit für ref-ähnliche Typen
Parameterlose Strukturkonstruktoren
Zulassen von with -Ausdruck für Strukturen

Weitere Informationen
C#-Referenz
Entwurfsrichtlinien: Auswählen zwischen Klasse und Struktur
Entwurfsrichtlinien: Strukturentwurf
Das C#-Typsystem
Tupeltypen (C#-Referenz)
04.11.2021 • 7 minutes to read

Das Feature Tupel ist in C# 7.0 und höher verfügbar und bietet eine überschaubare Syntax zum Gruppieren von
mehreren Datenelementen in einer einfachen Datenstruktur. Im folgenden Beispiel wird veranschaulicht, wie Sie
eine Tupelvariable deklarieren, initialisieren und dafür auf die zugehörigen Datenmember zugreifen können:

(double, int) t1 = (4.5, 3);


Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}.");
// Output:
// Tuple with elements 4.5 and 3.

(double Sum, int Count) t2 = (4.5, 3);


Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
// Output:
// Sum of 3 elements is 4.5.

Wie im obigen Beispiel zu sehen ist, geben Sie zum Definieren eines Tupeltyps die Typen aller Datenmember
und optional die Feldnamen an. Sie können keine Methoden in einem Tupeltyp definieren, aber Sie können die
von .NET bereitgestellten Methoden verwenden. Dies wird im folgenden Beispiel veranschaulicht:

(double, int) t = (4.5, 3);


Console.WriteLine(t.ToString());
Console.WriteLine($"Hash code of {t} is {t.GetHashCode()}.");
// Output:
// (4.5, 3)
// Hash code of (4.5, 3) is 718460086.

Ab C# 7.3 unterstützen Tupeltypen die Gleichheitsoperatoren == und != . Weitere Informationen finden Sie im
Abschnitt Tupelgleichheit.
Tupeltypen sind Werttypen, und Tupelelemente sind öffentliche Felder. Bei Tupeln handelt es sich also um
veränderliche Werttypen.

NOTE
Für das Feature „Tupel“ werden der Typ System.ValueTuple und die zugehörigen generischen Typen (z. B.
System.ValueTuple<T1,T2>) benötigt. Sie sind in .NET Core sowie in .NET Framework 4.7 und höher verfügbar. Fügen Sie
dem Projekt das NuGet-Paket System.ValueTuple hinzu, wenn Sie Tupel in einem Projekt nutzen möchten, das auf .NET
Framework 4.6.2 oder früher ausgerichtet ist.

Sie können Tupel mit einer beliebig großen Anzahl von Elementen definieren:

var t =
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26);
Console.WriteLine(t.Item26); // output: 26

Anwendungsfälle von Tupeln


Einer der häufigsten Anwendungsfälle für Tupel ist die Verwendung als Methodenrückgabetyp. Anstatt out -
Methodenparameter zu definieren, können Sie also Methodenergebnisse in einem Tupelrückgabetyp
gruppieren. Dies ist im folgenden Beispiel veranschaulicht:

var xs = new[] { 4, 7, 9 };
var limits = FindMinMax(xs);
Console.WriteLine($"Limits of [{string.Join(" ", xs)}] are {limits.min} and {limits.max}");
// Output:
// Limits of [4 7 9] are 4 and 9

var ys = new[] { -9, 0, 67, 100 };


var (minimum, maximum) = FindMinMax(ys);
Console.WriteLine($"Limits of [{string.Join(" ", ys)}] are {minimum} and {maximum}");
// Output:
// Limits of [-9 0 67 100] are -9 and 100

(int min, int max) FindMinMax(int[] input)


{
if (input is null || input.Length == 0)
{
throw new ArgumentException("Cannot find minimum and maximum of a null or empty array.");
}

var min = int.MaxValue;


var max = int.MinValue;
foreach (var i in input)
{
if (i < min)
{
min = i;
}
if (i > max)
{
max = i;
}
}
return (min, max);
}

Wie Sie im obigen Beispiel sehen, können Sie direkt mit der zurückgegebenen Tupelinstanz arbeiten oder sie in
separate Variablen dekonstruieren.
Sie können Tupeltypen auch anstelle von anonymen Typen verwenden, z. B. in LINQ-Abfragen. Weitere
Informationen finden Sie unter Auswählen zwischen anonymen Typen und Tupeltypen.
Normalerweise verwenden Sie Tupel, um lose zusammengehörende Datenelemente zu gruppieren. Dies ist
normalerweise bei privaten und internen Hilfsmethoden hilfreich. Erwägen Sie bei Verwendung einer
öffentlichen API, den Typ Klasse oder Struktur zu definieren.

Tupelfeldnamen
Sie können die Namen von Tupelfeldern entweder in einem Ausdruck für die Tupelinitialisierung oder in der
Definition eines Tupeltyps explizit angeben. Dies wird im folgenden Beispiel veranschaulicht:

var t = (Sum: 4.5, Count: 3);


Console.WriteLine($"Sum of {t.Count} elements is {t.Sum}.");

(double Sum, int Count) d = (4.5, 3);


Console.WriteLine($"Sum of {d.Count} elements is {d.Sum}.");

Ab C# 7.1 gilt Folgendes: Wenn Sie keinen Feldnamen angeben, wird er ggf. vom Namen der entsprechenden
Variablen in einem Ausdruck für die Tupelinitialisierung abgeleitet (wie im folgenden Beispiel):

var sum = 4.5;


var count = 3;
var t = (sum, count);
Console.WriteLine($"Sum of {t.count} elements is {t.sum}.");

Dies wird als Tupel-Projektionsinitialisierer bezeichnet. Der Name einer Variablen wird in den folgenden Fällen
nicht auf einen Tupelfeldnamen projiziert:
Der Kandidatenname ist ein Membername eines Tupeltyps, z. B. Item3 , ToString oder Rest .
Beim Namen des Kandidaten handelt es sich um das Duplikat des expliziten oder impliziten Feldnamens
eines anderen Tupels.
In diesen Fällen geben Sie entweder explizit den Namen eines Felds an oder greifen über den Standardnamen
auf ein Feld zu.
Die Standardnamen von Tupelfeldern lauten Item1 , Item2 , Item3 usw. Den Standardnamen eines Felds
können Sie immer verwenden. Dies gilt auch, wenn ein Feldname explizit angegeben oder abgeleitet wird (wie
im folgenden Beispiel):

var a = 1;
var t = (a, b: 2, 3);
Console.WriteLine($"The 1st element is {t.Item1} (same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2} (same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}.");
// Output:
// The 1st element is 1 (same as 1).
// The 2nd element is 2 (same as 2).
// The 3rd element is 3.

Bei der Tupelzuweisung und bei Vergleichen der Tupelgleichheit werden Feldnamen nicht berücksichtigt.
Zur Kompilierzeit ersetzt der Compiler die nicht dem Standard entsprechenden Felder durch die jeweiligen
Standardnamen. Daher sind explizit angegebene oder abgeleitete Feldnamen zur Laufzeit nicht verfügbar.

Tupelzuweisung und -dekonstruktion


C# unterstützt die Zuweisung zwischen Tupeltypen, die die beiden folgenden Bedingungen erfüllen:
Beide Tupeltypen haben die gleiche Anzahl von Elementen.
Für jede Tupelposition ist der Typ des rechten Tupelelements mit dem Typ des entsprechenden linken
Tupelelements identisch oder implizit konvertierbar.
Die Werte von Tupelelementen werden gemäß der Reihenfolge der Tupelelemente zugewiesen. Die Namen von
Tupelfeldern werden ignoriert und nicht zugewiesen. Dies wird im folgenden Beispiel veranschaulicht:
(int, double) t1 = (17, 3.14);
(double First, double Second) t2 = (0.0, 1.0);
t2 = t1;
Console.WriteLine($"{nameof(t2)}: {t2.First} and {t2.Second}");
// Output:
// t2: 17 and 3.14

(double A, double B) t3 = (2.0, 3.0);


t3 = t2;
Console.WriteLine($"{nameof(t3)}: {t3.A} and {t3.B}");
// Output:
// t3: 17 and 3.14

Sie können auch den Zuweisungsoperator = verwenden, um eine Tupelinstanz in separate Variablen zu
dekonstruieren. Hierfür können Sie eine der folgenden Vorgehensweisen wählen:
Explizites Deklarieren des Typs jeder Variablen in Klammern:

var t = ("post office", 3.6);


(string destination, double distance) = t;
Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
// Output:
// Distance to post office is 3.6 kilometers.

Verwenden des Schlüsselworts var außerhalb der Klammern, um implizit typisierte Variablen zu
deklarieren und die Typen vom Compiler ableiten zu lassen:

var t = ("post office", 3.6);


var (destination, distance) = t;
Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
// Output:
// Distance to post office is 3.6 kilometers.

Verwenden von vorhandenen Variablen:

var destination = string.Empty;


var distance = 0.0;

var t = ("post office", 3.6);


(destination, distance) = t;
Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
// Output:
// Distance to post office is 3.6 kilometers.

Weitere Informationen zur Dekonstruktion von Tupeln und anderen Typen finden Sie unter Dekonstruieren von
Tupeln und anderen Typen.

Tupelgleichheit
Ab C# 7.3 unterstützen Tupeltypen die Operatoren == und != . Mit diesen Operatoren werden Member des
linken Operanden gemäß der Reihenfolge der Tupelelemente mit den entsprechenden Membern des rechten
Operanden verglichen.
(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right); // output: True
Console.WriteLine(left != right); // output: False

var t1 = (A: 5, B: 10);


var t2 = (B: 5, A: 10);
Console.WriteLine(t1 == t2); // output: True
Console.WriteLine(t1 != t2); // output: False

Wie im obigen Beispiel zu sehen ist, werden Tupelfeldnamen bei Operationen mit == und != nicht
berücksichtigt.
Zwei Tupel sind vergleichbar, wenn die beiden folgenden Bedingungen erfüllt sind:
Beide Tupel weisen die gleiche Anzahl von Elementen auf. t1 != t2 wird beispielsweise nicht kompiliert,
wenn t1 und t2 über eine unterschiedliche Anzahl von Elementen verfügen.
Für jede Tupelposition können die entsprechenden Elemente der linken und rechten Tupeloperanden mit den
Operatoren == und != verglichen werden. (1, (2, 3)) == ((1, 2), 3) wird beispielsweise nicht
kompiliert, weil 1 nicht mit (1, 2) vergleichbar ist.

Die Operatoren == und != vergleichen Tupel per „Kurzschluss“. Dies bedeutet, dass eine Operation sofort
angehalten wird, wenn ein Paar mit ungleichen Elementen erkannt oder das Ende von Tupeln erreicht wird.
Bevor ein Vergleich durchgeführt wird, werden aber alle Tupelelemente ausgewertet. Dies wird im folgenden
Beispiel veranschaulicht:

Console.WriteLine((Display(1), Display(2)) == (Display(3), Display(4)));

int Display(int s)
{
Console.WriteLine(s);
return s;
}
// Output:
// 1
// 2
// 3
// 4
// False

Tupel als out-Parameter


Normalerweise gestalten Sie eine Methode, die out -Parameter enthält, in eine Methode um, die ein Tupel
zurückgibt. Es gibt aber auch Fälle, in denen ein out -Parameter einen Tupeltyp aufweisen kann. Im folgenden
Beispiel wird veranschaulicht, wie Sie Tupel als out -Parameter verwenden:
var limitsLookup = new Dictionary<int, (int Min, int Max)>()
{
[2] = (4, 10),
[4] = (10, 20),
[6] = (0, 23)
};

if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits))


{
Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}");
}
// Output:
// Found limits: min is 10, max is 20

Vergleich von Tupeln und System.Tuple


C#-Tupel, die auf System.ValueTuple-Typen basieren, unterscheiden sich von Tupeln, die auf System.Tuple-Typen
basieren. Es gibt die folgenden Hauptunterschiede:
Bei -Typen handelt es sich um Werttypen. Tuple -Typen sind Verweistypen.
ValueTuple
ValueTuple -Typen sind veränderlich. Tuple -Typen sind unveränderlich.
Datenmember von ValueTuple -Typen sind Felder. Datenmember von Tuple -Typen sind Eigenschaften.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Hinweisen zu Featurevorschlägen:
Ableiten von Tupelnamen (Tupel-Projektionsinitialisierer)
Unterstützung für == und != in Tupeltypen

Siehe auch
C#-Referenz
Werttypen
Auswählen zwischen anonymen Typen und Tupeltypen
System.ValueTuple
Nullable-Werttypen (C#-Referenz)
04.11.2021 • 7 minutes to read

Ein Werttyp, der NULL zulässt (wie T? ) stellt alle Werte des zugrunde liegenden Werttyps T und einen
zusätzlichen NULL-Wert dar. Beispielsweise können Sie einer bool? -Variablen einen der folgenden drei Werte
zuweisen: true , false oder null . Ein zugrunde liegender Werttyp T darf selbst kein Nullable-Werttyp sein.

NOTE
C# 8.0 führt das Feature der Nullable-Verweistypen ein. Weitere Informationen finden Sie unter Nullable-Verweistypen.
Nullable-Werttypen sind ab C# 2 verfügbar.

Jeder Nullable-Werttyp ist eine Instanz der generischen Struktur System.Nullable<T>. Sie können auf einen
Nullable-Werttyp mit einem zugrunde liegenden Typ T in einer der folgenden austauschbaren Formen
verweisen: Nullable<T> oder T? .
Sie verwenden in der Regel einen Nullable-Werttyp, wenn Sie den nicht definierten Wert eines zugrunde
liegenden Werttyps darstellen müssen. Beispielsweise kann ein boolescher Wert eine bool -Variable nur true
oder false sein. In einigen Anwendungen kann ein Variablenwert jedoch nicht definiert sein oder fehlen.
Beispielsweise kann ein Datenbankfeld true oder false enthalten, oder es enthält gar keinen Wert, ist also
NULL . In diesem Szenario können Sie den bool? -Typ verwenden.

Deklaration und Zuweisung


Da ein Werttyp implizit in den entsprechenden Nullable-Werttyp konvertiert werden kann, können Sie einer
Variablen eines Nullable-Werttyps auf die gleiche Weise wie seinem zugrunde liegenden Werttyp einen Wert
zuweisen. Sie können auch den null -Wert zuweisen. Zum Beispiel:

double? pi = 3.14;
char? letter = 'a';

int m2 = 10;
int? m = m2;

bool? flag = null;

// An array of a nullable value type:


int?[] arr = new int?[10];

Der Standardwert eines Nullable-Werttyps stellt null dar, es handelt sich also um eine-Instanz, deren
Nullable<T>.HasValue-Eigenschaft false zurückgibt.

Untersuchung einer Instanz eines Nullable-Werttyps


Ab C# 7.0 können Sie den is -Operator mit einem Typmuster verwenden, um eine Instanz eines Nullable-
Werttyps für null zu untersuchen und einen Wert eines zugrunde liegenden Typs abzurufen:
int? a = 42;
if (a is int valueOfA)
{
Console.WriteLine($"a is {valueOfA}");
}
else
{
Console.WriteLine("a does not have a value");
}
// Output:
// a is 42

Sie können immer die folgenden schreibgeschützten Eigenschaften verwenden, um einen Wert einer Variablen
des Nullable-Werttyps zu untersuchen und abzurufen:
Nullable<T>.HasValue gibt an, ob eine Instanz des Nullable-Werttyps über einen Wert des zugrunde
liegenden Typs verfügt.
Nullable<T>.Value ruft den Wert eines zugrunde liegenden Typs ab, wenn HasValue true ist. Wenn
HasValue false ist, löst die Value-Eigenschaft eine InvalidOperationException aus.

Im folgenden Beispiel wird mit der HasValue -Eigenschaft überprüft, ob die Variable einen Wert enthält. Erst
danach erfolgt die Anzeige:

int? b = 10;
if (b.HasValue)
{
Console.WriteLine($"b is {b.Value}");
}
else
{
Console.WriteLine("b does not have a value");
}
// Output:
// b is 10

Sie können eine Variable eines Nullable-Werttyps wie im folgenden Beispiel gezeigt auch mit null anstelle der
HasValue -Eigenschaft vergleichen:

int? c = 7;
if (c != null)
{
Console.WriteLine($"c is {c.Value}");
}
else
{
Console.WriteLine("c does not have a value");
}
// Output:
// c is 7

Konvertieren eines Nullable-Werttyps in einen zugrunde liegenden


Typ
Wenn Sie einen Wert eines Nullable-Werttyps einer Variablen eines Nicht-Nullable-Werttyps zuweisen
möchten, müssen Sie möglicherweise den zuzuweisenden Wert anstelle von null angeben. Verwenden Sie zu
diesem Zweck den NULL-Sammeloperator ?? (Sie können auch die Nullable<T>.GetValueOrDefault(T)-
Methode für denselben Zweck verwenden):
int? a = 28;
int b = a ?? -1;
Console.WriteLine($"b is {b}"); // output: b is 28

int? c = null;
int d = c ?? -1;
Console.WriteLine($"d is {d}"); // output: d is -1

Wenn Sie den Standardwert des zugrunde liegenden Werttyps anstelle von null verwenden möchten,
verwenden Sie die Nullable<T>.GetValueOrDefault()-Methode.
Sie können einen Nullable-Werttyp wie im folgenden Beispiel gezeigt auch explizit in einen Non-Nullable-Typ
umwandeln:

int? n = null;

//int m1 = n; // Doesn't compile


int n2 = (int)n; // Compiles, but throws an exception if n is null

Wenn der Wert eines Nullable-Typs zur Laufzeit null ist, wird durch die explizite Umwandlung eine
InvalidOperationException ausgelöst.
Ein Nicht-Nullable-Werttyp T kann implizit in den entsprechenden Nullable-Werttyp T? konvertiert werden.

„Lifted“ Operatoren
Die vordefinierten unären und binären Operatoren oder alle überladenen Operatoren, die von einem Werttyp
T unterstützt werden, werden auch vom entsprechenden Werttyp T? unterstützt, der NULL zulässt. Durch
diese Operatoren (auch als „lifted“ Operatoren bezeichnet) wird null generiert, wenn mindestens ein Operand
null ist. Andernfalls verwendet der Operator die enthaltenen Werte seiner Operanden zur Berechnung des
Ergebnisses. Zum Beispiel:

int? a = 10;
int? b = null;
int? c = 10;

a++; // a is 11
a = a * c; // a is 110
a = a + b; // a is null

NOTE
Für den bool? -Typ gelten für die vordefinierten Operatoren & und | nicht die in diesem Abschnitt beschriebenen
Regeln. Die Auswertung eines Operators kann auch dann einen anderen Wert als NULL ergeben, wenn einer der beiden
Operanden null ist. Weitere Informationen finden Sie im Abschnitt Boolesche logische Operatoren, die NULL-Werte
zulassen im Artikel Boolesche logische Operatoren.

Für die Vergleichsoperatoren < , > , <= und >= ist das Ergebnis false , wenn einer oder beide Operanden
null sind. Andernfalls werden die enthaltenen Werte von Operanden verglichen. Falsch wäre die Annahme,
dass im Fall des Rückgabewerts false für einen bestimmten Vergleich (beispielsweise <= ) der gegenteilige
Vergleich ( > ) true zurückgibt. Das folgende Beispiel zeigt, dass 10
weder größer als oder gleich null
noch kleiner als null ist.
int? a = 10;
Console.WriteLine($"{a} >= null is {a >= null}");
Console.WriteLine($"{a} < null is {a < null}");
Console.WriteLine($"{a} == null is {a == null}");
// Output:
// 10 >= null is False
// 10 < null is False
// 10 == null is False

int? b = null;
int? c = null;
Console.WriteLine($"null >= null is {b >= c}");
Console.WriteLine($"null == null is {b == c}");
// Output:
// null >= null is False
// null == null is True

Für den Gleichheitsoperator == ist das Ergebnis true , wenn beide Operanden null sind. Das Ergebnis ist
false , wenn nur einer der Operanden null ist. Andernfalls werden die enthaltenen Werte von Operanden
verglichen.
Für den Ungleichheitsoperator != ist das Ergebnis false , wenn beide Operanden null sind. Das Ergebnis ist
true , wenn nur einer der Operanden null ist. Andernfalls werden die enthaltenen Werte von Operanden
verglichen.
Wenn eine benutzerdefinierte Konvertierung zwischen zwei Werttypen vorhanden ist, kann die gleiche
Konvertierung auch zwischen den entsprechenden Nullable-Werttypen verwendet werden.

Boxing und Unboxing


Das Boxing einer Instanz eines Nullable-Werttyps T? erfolgt folgendermaßen:
Wenn HasValue false zurückgibt, wird der Nullverweis erstellt.
Wenn HasValue true zurückgibt, wird nicht für eine Instanz von Nullable<T>, sondern für den
entsprechenden Wert des zugrunde liegenden Werttyps T Boxing durchgeführt.

Sie können den Werttyp, für den das Boxing durchgeführt wurde, vom Werttyp T mittels Unboxing in den
entsprechenden Nullable-Werttyp T? konvertieren, wie im folgenden Beispiel gezeigt wird:

int a = 41;
object aBoxed = a;
int? aNullable = (int?)aBoxed;
Console.WriteLine($"Value of aNullable: {aNullable}");

object aNullableBoxed = aNullable;


if (aNullableBoxed is int valueOfA)
{
Console.WriteLine($"aNullableBoxed is boxed int: {valueOfA}");
}
// Output:
// Value of aNullable: 41
// aNullableBoxed is boxed int: 41

Identifizieren eines Nullable-Werttyps


Das folgende Beispiel zeigt, wie Sie bestimmen können, ob eine System.Type-Instanz einen konstruierten
Nullable-Werttyp darstellt, d.h. den System.Nullable<T>-Typ mit einem angegebenen Typparameter T :
Console.WriteLine($"int? is {(IsNullable(typeof(int?)) ? "nullable" : "non nullable")} value type");
Console.WriteLine($"int is {(IsNullable(typeof(int)) ? "nullable" : "non-nullable")} value type");

bool IsNullable(Type type) => Nullable.GetUnderlyingType(type) != null;

// Output:
// int? is nullable value type
// int is non-nullable value type

Wie Sie im Beispiel sehen können, wird der Operator typeof verwendet, um eine System.Type-Instanz zu
erstellen.
Wenn Sie bestimmen möchten, ob eine Instanz einen Nullable-Werttyp aufweist, verwenden Sie nicht die
Object.GetType-Methode, um eine Type-Instanz abzurufen, die mit dem vorangehenden Code überprüft werden
soll. Wenn Sie die Object.GetType-Methode in einer Instanz eines Nullable-Werttyps aufrufen, wird die Instanz in
Objectgeschachtelt. Da das Schachteln einer nicht-NULL-Instanz von einem Nullable-Werttyp dem Schachteln
eines Werts des zugrunde liegenden Typs entspricht, gibt GetType eine Type-Instanz zurück, die den zugrunde
liegenden Typ eines Nullable-Werttyps darstellt:

int? a = 17;
Type typeOfA = a.GetType();
Console.WriteLine(typeOfA.FullName);
// Output:
// System.Int32

Verwenden Sie außerdem nicht den Operator is, um zu ermitteln, ob eine Instanz einem Nullable-Werttyp
entspricht. Wie im folgenden Beispiel veranschaulicht wird, können Sie die Typen einer Instanz eines Nullable-
Werttyps und die zugrunde liegende Typinstanz nicht mithilfe des is -Operators unterscheiden:

int? a = 14;
if (a is int)
{
Console.WriteLine("int? instance is compatible with int");
}

int b = 17;
if (b is int?)
{
Console.WriteLine("int instance is compatible with int?");
}
// Output:
// int? instance is compatible with int
// int instance is compatible with int?

Sie können den Code verwenden, der im folgenden Beispiel dargestellt wird, um zu ermitteln, ob eine Instanz
einen Nullable-Werttyp aufweist:

int? a = 14;
Console.WriteLine(IsOfNullableType(a)); // output: True

int b = 17;
Console.WriteLine(IsOfNullableType(b)); // output: False

bool IsOfNullableType<T>(T o)
{
var type = typeof(T);
return Nullable.GetUnderlyingType(type) != null;
}
NOTE
Die in diesem Abschnitt beschriebenen Methoden sind im Fall von Nullable-Verweistypen nicht anwendbar.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Nullable-Typen
„Lifted“ Operatoren
Implizite Nullable-Konvertierungen
Explizite Nullable-Konvertierungen
„Lifted“ Konvertierungsoperatoren

Siehe auch
C#-Referenz
What exactly does 'lifted' mean? (Was bedeutet „Lifted“ genau?)
System.Nullable<T>
System.Nullable
Nullable.GetUnderlyingType
Nullwerte zulassende Verweistypen
Verweistypen (C#-Referenz)
04.11.2021 • 2 minutes to read

Es gibt zwei Arten von Typen in C#: Verweistypen und Werttypen. Variablen von Verweistypen speichern
Verweise auf ihre Daten (Objekte), während Variablen von Werttypen ihre Daten direkt enthalten. Bei
Verweistypen können zwei Variablen auf dasselbe Objekt verweisen. Daher können auf eine Variable
angewendete Operationen das Objekt beeinflussen, auf das von der anderen Variablen verwiesen wird. Bei
Werttypen besitzt jede Variable eine eigene Kopie der Daten, und auf eine Variable angewendete Operationen
können die andere Variable nicht beeinflussen (außer im Fall von ref- und out-Parametervariablen, siehe in, ref
und out-Parametermodifizierer).
Die folgenden Schlüsselwörter werden verwendet, um Verweistypen zu deklarieren:
class
interface
delegate
record
C# enthält auch die folgenden integrierten Referenztypen:
dynamic
object
string

Siehe auch
C#-Referenz
C#-Schlüsselwörter
Zeigertypen
Werttypen
Integrierte Verweistypen (C#-Referenz)
04.11.2021 • 7 minutes to read

C# enthält eine Reihe von integrierten Verweistypen. Diese enthalten Schlüsselwörter oder Operatoren, die
Synonyme für einen Typ in der .NET-Bibliothek sind.

den Objekttyp
Der object -Typ ist ein Alias für System.Object in .NET. Im vereinheitlichen Typsystem von C# erben alle Typen,
vordefiniert und benutzerdefiniert sowie Verweis- und Werttypen, direkt oder indirekt von System.Object. Sie
können Werte eines beliebigen Typs Variablen des Typs object zuweisen. Diesem Standardwert kann mithilfe
des Literals null eine beliebige object -Variable zugewiesen werden. Wenn eine Variable eines Werttyps in ein
Objekt konvertiert wird, gilt es als geschachtelt. Wenn eine Variable des Typs object in ein Wertobjekt
konvertiert wird, gilt es als nicht geschachtelt. Weitere Informationen finden Sie unter Boxing und Unboxing.

Der Zeichenfolgentyp
Der Typ string stellt eine Sequenz von Null oder mehr Unicode-Zeichen dar. string ist ein Alias für
System.String in .NET.
Obwohl string ein Verweistyp ist, werden die Gleichheitsoperatoren == und != zum Vergleichen der Werte
von string -Objekten, nicht von Verweisen, definiert. Dadurch wird das Testen auf Zeichenfolgengleichheit
intuitiver. Beispiel:

string a = "hello";
string b = "h";
// Append to contents of 'b'
b += "ello";
Console.WriteLine(a == b);
Console.WriteLine(object.ReferenceEquals(a, b));

Dies zeigt TRUE und anschließend FALSE an, weil der Inhalt der Zeichenfolgen gleich sind. Jedoch verweisen a
und b nicht auf die gleiche Zeichenfolgeninstanz.
Der Operator „+“ verkettet Zeichenfolgen:

string a = "good " + "morning";

Dadurch wird ein Zeichenfolgenobjekt erstellt, das „Guten Morgen“ enthält.


Zeichenfolgen sind unveränderlich. Die Inhalte eines Zeichenfolgenobjekts können nicht geändert werden,
nachdem ein Objekt erstellt wurde, obwohl die Syntax den Eindruck erweckt, dass es machbar wäre. Wenn Sie
z. B. diesen Code schreiben, erstellt der Compiler tatsächlich ein neues Zeichenfolgenobjekt, um die neue
Zeichensequenz zu speichern. Das neue Objekt wird b zugewiesen. Der Speicherplatz, der b zugeordnet
wurde (sofern die Zeichenfolge „h“ enthalten war), hat dann Anspruch auf die Garbage Collection.

string b = "h";
b += "ello";

Der - [] Operator kann für schreibgeschützten Zugriff auf einzelne Zeichen einer Zeichenfolge verwendet
werden: Gültige Indexwerte beginnen bei 0 und müssen kleiner als die Länge der Zeichenfolge sein:

string str = "test";


char x = str[2]; // x = 's';

Auf gleiche Weise kann der [] -Operator auch für das Durchlaufen jedes Zeichens in der Zeichenfolge
verwendet werden:

string str = "test";

for (int i = 0; i < str.Length; i++)


{
Console.Write(str[i] + " ");
}
// Output: t e s t

Zeichenfolgenliterale sind Typ string und können in zwei Formaten geschrieben werden: in
Anführungszeichen und mit @ . Zeichenfolgenliterale in Anführungszeichen werden in doppelte
Anführungszeichen (") eingeschlossen:

"good morning" // a string literal

Zeichenfolgenliterale können jeden Zeichenliteral enthalten. Escapesequenzen sind enthalten. Im folgenden


Beispiel wird die Escapesequenz \\ für den umgekehrten Schrägstrich, \u0066 für den Buchstaben „f“ und \n
für den Zeilenumbruch verwendet.

string a = "\\\u0066\n F";


Console.WriteLine(a);
// Output:
// \f
// F

NOTE
Der Escapecode \udddd (wobei dddd eine vierstellige Zahl ist) stellt das Unicode-Zeichen U+ dddd dar. Escapecodes
aus achtstelligen Unicode werden auch erkannt: \Udddddddd .

Wörtliche Zeichenfolgenliterale beginnen mit @ und sind ebenfalls in doppelte Anführungszeichen


eingeschlossen. Beispiel:

@"good morning" // a string literal

Der Vorteil von wörtlichen Zeichenfolgen besteht darin, dass Escapesequenzen nicht verarbeitet werden,
wodurch z. B. das Schreiben eines vollqualifizierten Windows-Dateinamens erleichtert wird:

@"c:\Docs\Source\a.txt" // rather than "c:\\Docs\\Source\\a.txt"

Verdoppeln Sie das doppelte Anführungszeichen, um es in einer @-quoted-Zeichenfolge aufzunehmen:

@"""Ahoy!"" cried the captain." // "Ahoy!" cried the captain.


Der Delegattyp
Die Deklaration eines Delegattyps ähnelt einer Methodensignatur. Er verfügt über einen Rückgabewert und eine
beliebige Anzahl Parameter eines beliebigen Typs:

public delegate void MessageDelegate(string message);


public delegate int AnotherDelegate(MyType m, long num);

In .NET stellen die Typen System.Action und System.Func generische Definitionen für zahlreiche allgemeine
Delegaten bereit. Sie werden sehr wahrscheinlich keine neuen benutzerdefinierten Delegattypen definieren
müssen. Stattdessen können Sie Instanziierungen der bereitgestellten generischen Typen erstellen.
Ein delegate ist ein Verweistyp, der verwendet werden kann, um eine benannte oder anonyme Methode zu
kapseln. Delegaten entsprechen den Funktionszeigern in C++, sind jedoch typsicher und geschützt.
Anwendungsmöglichkeiten von Delegaten finden Sie unter Delegaten und Generische Delegaten. Delegaten
bilden die Grundlage für Ereignisse. Ein Delegat kann instanziiert werden, entweder durch Zuordnen mit einer
benannten oder einer anonymen Methode.
Der Delegat muss mit einer Methode oder einem Lambda-Ausdruck instanziiert werden, der über einen
kompatiblen Rückgabetypen und Eingabeparameter verfügt. Weitere Informationen zum Grad der Varianz, der
in der Methodensignatur zulässig ist, finden Sie unter Varianz bei Delegaten. Für die Verwendung mit anonymen
Methoden werden der Delegat und der Code, der mit ihm zugeordnet werden soll, zusammen deklariert.
Das Kombinieren und Entfernen von Delegaten schlägt mit einer Laufzeitausnahme fehl, wenn die zur Laufzeit
beteiligten Delegattypen aufgrund der Variantenkonvertierung unterschiedlich sind. Im folgenden Beispiel wird
eine Situation veranschaulicht, in der ein Fehler auftritt:

Action<string> stringAction = str => {};


Action<object> objectAction = obj => {};

// Valid due to implicit reference conversion of


// objectAction to Action<string>, but may fail
// at runtime.
Action<string> combination = stringAction + objectAction;

Sie können einen Delegaten mit dem richtigen Laufzeittyp erstellen, indem Sie ein neues Delegatobjekt erstellen.
Im folgenden Beispiel wird veranschaulicht, wie diese Problemumgehung auf das vorherige Beispiel
angewendet werden kann.

Action<string> stringAction = str => {};


Action<object> objectAction = obj => {};

// Creates a new delegate instance with a runtime type of Action<string>.


Action<string> wrappedObjectAction = new Action<string>(objectAction);

// The two Action<string> delegate instances can now be combined.


Action<string> combination = stringAction + wrappedObjectAction;

Ab C# 9 können Sie Funktionszeiger deklarieren, die eine ähnliche Syntax verwenden. Ein Funktionszeiger
verwendet die calli -Anweisung, anstatt einen Delegattyp zu instanziieren und die virtuelle Invoke -Methode
aufzurufen.

Der dynamische Typ


Der dynamic -Typ gibt an, dass durch die Verwendung der Variablen und Verweise auf die entsprechenden
Member die Prüfung des Kompilierzeittyps umgangen wird. Stattdessen werden diese Vorgänge zur Laufzeit
aufgelöst. Der dynamic -Typ vereinfacht den Zugriff auf COM-APIs, z. B. die Office Automation-APIs, auf
dynamische APIs, beispielsweise IronPython-Bibliotheken und auf das HTML-Dokumentobjektmodell
(Document Object Model, DOM).
Der Typ dynamic verhält sich in den meisten Fällen wie Typ object . Insbesondere können alle Ausdrücke, die
nicht NULL sind, in den dynamic -Typ konvertiert werden. Der dynamic -Typ unterscheidet sich jedoch von
object insofern, als dass Vorgänge, die Ausdrücke des Typs dynamic enthalten, nicht aufgelöst oder durch den
Compiler typgeprüft werden. Der Compiler packt Informationen über den Vorgang. Diese Informationen werden
später zur Evaluierung des Vorgangs zur Laufzeit verwendet. Als Teil dieses Prozesses werden Variablen des
Typs dynamic in Variablen des Typs object kompiliert. Deshalb existiert der Typ dynamic nur zur Kompilierzeit
und nicht zur Laufzeit.
Das folgende Beispiel vergleicht den Unterschied einer Variable des Typs dynamic mit einer Variable des Typs
object . Um den Typ jeder Variable zur Kompilierzeit zu überprüfen, zeigen Sie mit dem Mauszeiger auf dyn
oder obj in den WriteLine -Anweisungen. Kopieren Sie den folgenden Code in einen Editor, in dem IntelliSense
verfügbar ist. IntelliSense zeigt dynamic für dyn und object für obj .

class Program
{
static void Main(string[] args)
{
dynamic dyn = 1;
object obj = 1;

// Rest the mouse pointer over dyn and obj to see their
// types at compile time.
System.Console.WriteLine(dyn.GetType());
System.Console.WriteLine(obj.GetType());
}
}

Die WriteLine-Anweisungen zeigen die Laufzeittypen von dyn und obj . Zu diesem Zeitpunkt verfügen beide
denselben Typ, Integer. Es wird die folgende Ausgabe generiert:

System.Int32
System.Int32

Um den Unterschied zwischen dyn und obj zur Kompilierzeit anzuzeigen, fügen Sie die folgenden zwei Zeilen
zwischen die Deklarationen und die WriteLine -Anweisungen im vorherigen Beispiel ein.

dyn = dyn + 3;
obj = obj + 3;

Es wird ein Kompilierfehler für den Versuch, einen Integer und ein Objekt im Ausdruck obj + 3 einzufügen,
ausgegeben. Es wird jedoch kein Fehler für dyn + 3 gemeldet. Der Ausdruck, der dyn enthält, wird nicht zur
Kompilierzeit überprüft, da der Typ von dyn``dynamic ist.
Das folgende Beispiel verwendet dynamic in einigen Deklarationen. Die Main -Methode unterscheidet auch die
Typüberprüfung zur Kompilierzeit und die Laufzeittypüberprüfung.
using System;

namespace DynamicExamples
{
class Program
{
static void Main(string[] args)
{
ExampleClass ec = new ExampleClass();
Console.WriteLine(ec.exampleMethod(10));
Console.WriteLine(ec.exampleMethod("value"));

// The following line causes a compiler error because exampleMethod


// takes only one argument.
//Console.WriteLine(ec.exampleMethod(10, 4));

dynamic dynamic_ec = new ExampleClass();


Console.WriteLine(dynamic_ec.exampleMethod(10));

// Because dynamic_ec is dynamic, the following call to exampleMethod


// with two arguments does not produce an error at compile time.
// However, it does cause a run-time error.
//Console.WriteLine(dynamic_ec.exampleMethod(10, 4));
}
}

class ExampleClass
{
static dynamic field;
dynamic prop { get; set; }

public dynamic exampleMethod(dynamic d)


{
dynamic local = "Local variable";
int two = 2;

if (d is int)
{
return local;
}
else
{
return two;
}
}
}
}
// Results:
// Local variable
// 2
// Local variable

Siehe auch
C#-Referenz
C#-Schlüsselwörter
Ereignisse
Verwenden von dynamischen Typen
Empfohlene Vorgehensweisen für die Verwendung von Zeichenfolgen
Grundlegende Zeichenfolgenoperationen
Erstellen neuer Zeichenfolgen
Typtest- und Umwandlungsoperatoren
Vorgehensweise: Sicheres Umwandeln mit Musterabgleich mit den Operatoren „as“ und „is“
Exemplarische Vorgehensweise: Erstellen und Verwenden von dynamischen Objekten
System.Object
System.String
System.Dynamic.DynamicObject
Datensätze (C#-Referenz)
04.11.2021 • 16 minutes to read

Ab C# 9 verwenden Sie das record -Schlüsselwort, um einen Verweistyp zu definieren, der integrierte
Funktionen zum Kapseln von Daten bereitstellt. Sie können Datensatztypen mit unveränderlichen Eigenschaften
erstellen, indem Sie Positionsparameter oder Standardeigenschaftensyntax verwenden:

public record Person(string FirstName, string LastName);

public record Person


{
public string FirstName { get; init; } = default!;
public string LastName { get; init; } = default!;
};

Sie können auch Datensatztypen mit änderbaren Eigenschaften und Feldern erstellen:

public record Person


{
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
};

Datensätze können zwar änderbar sein, sind jedoch primär dafür vorgesehen, unveränderliche Datenmodelle zu
unterstützen. Der Datensatztyp bietet die folgenden Funktionen:
Präzise Syntax zum Erstellen eines Verweistyps mit unveränderlichen Eigenschaften
Integriertes Verhalten, das für einen datenzentrierten Verweistyp nützlich ist:
Wertgleichheit
Präzise Syntax für die nicht destruktive Mutation
Integrierte Formatierung für die Anzeige
Unterstützung für Vererbungshierarchien
Sie können auch Strukturtypen verwenden, um datenzentrierte Typen zu entwerfen, die Wertgleichheit und
wenig oder kein Verhalten bereitstellen. In C# 10.0 und höher können Sie record struct -Typen mithilfe von
Positionsparametern oder Standardeigenschaftssyntax definieren:

public readonly record struct Point(double X, double Y, double Z);

public record struct Point


{
public double X { get; init; }
public double Y { get; init; }
public double Z { get; init; }
}

Datensatzstrukturen können ebenfalls veränderlich sein, sowohl Positionsdatensatzstrukturen als auch


Datensatzstrukturen ohne Positionsparameter:
public record struct DataMeasurement(DateTime TakenAt, double Measurement);

public record struct Point


{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}

Die Beispiele oben zeigen einige Unterschiede zwischen Datensätzen, die Verweistypen sind, und Datensätzen,
die Werttypen sind:
Das record - oder record class -Element deklariert einen Verweistyp. Das class -Schlüsselwort ist optional,
kann aber für Leser mehr Klarheit schaffen. Das record struct -Element deklariert einen Werttyp.
Positionseigenschaften sind in record class - und readonly record struct -Elementen unveränderlich. Sie
sind in record struct -Elementen veränderbar.

Im weiteren Verlauf dieses Artikels werden sowohl record class - als auch record struct -Typen erläutert. Die
Unterschiede werden in den einzelnen Abschnitten ausführlich beschrieben. Sie sollten sich zwischen
record class - und record struct -Elementen entscheiden, ähnlich wie bei der Entscheidung zwischen class -
und struct -Elementen. Der Begriff Datensatz wird verwendet, um Verhalten zu beschreiben, das für alle
Datensatztypen gilt. record struct oder record class wird verwendet, um das Verhalten zu beschreiben, das
nur für Struktur- bzw. Klassentypen gilt.

Positionssyntax für die Eigenschaftendefinition


Sie können Positionsparameter verwenden, um die Eigenschaften eines Datensatzes zu deklarieren und die
Eigenschaftswerte zu initialisieren, wenn Sie eine Instanz erstellen:

public record Person(string FirstName, string LastName);

public static void Main()


{
Person person = new("Nancy", "Davolio");
Console.WriteLine(person);
// output: Person { FirstName = Nancy, LastName = Davolio }
}

Wenn Sie die Positionssyntax für die Eigenschaftendefinition verwenden, erstellt der Compiler Folgendes:
Eine öffentliche, automatisch implementierte Init-only-Eigenschaft für jeden Positionsparameter, der in der
Datensatzdeklaration bereitgestellt wird.
Für record - und readonly record struct -Typen: Eine init-only-Eigenschaft kann nur im Konstruktor
oder mit einem Eigenschafteninitialisierer festgelegt werden.
Für record struct -Typen: Eine Lese-/Schreibeigenschaft, die in einem Konstruktor, einem
Eigenschafteninitialisierer oder einer Zuweisung nach der Erstellung festgelegt werden kann.
Ein primärer Konstruktor, dessen Parameter mit den Parametern mit fester Breite der Datensatzdeklaration
übereinstimmen
Bei Datensatzstrukturtypen ein parameterloser Konstruktor, der jedes Feld auf seinen Standardwert festlegt.
Eine Deconstruct -Methode mit einem out -Parameter für jeden Positionsparameter, der in der
Datensatzdeklaration bereitgestellt wird. Diese Methode wird nur bereitgestellt, wenn zwei oder mehr
Positionsparameter vorhanden sind. Die Methode dekonstruiert Eigenschaften, die mithilfe der
Positionssyntax definiert wurden. Es werden Eigenschaften ignoriert, die mithilfe der
Standardeigenschaftensyntax definiert werden.
Sie sollten einem dieser Elemente, die der Compiler aus der Datensatzdefinition erstellt, Attribute hinzufügen.
Sie können einem beliebigen Attribut, das Sie auf die Eigenschaften des Datensatzes mit Feldern fester Breite
anwenden, ein Ziel hinzufügen. Im folgenden Beispiel wird
System.Text.Json.Serialization.JsonPropertyNameAttribute auf jede Eigenschaft des Person -Datensatzes
angewendet. Das property: -Ziel gibt an, dass das Attribut auf die vom Compiler generierte Eigenschaft
angewendet wird. Andere Werte sind beispielsweise field: zum Anwenden des Attributs auf das Feld und
param: zum Anwenden des Attributs auf den Parameter.

/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")]string FirstName,
[property: JsonPropertyName("lastName")]string LastName);

Das vorherige Beispiel zeigt auch, wie XML-Dokumentationskommentare für den Datensatz erstellt werden. Sie
können das <param> -Tag hinzufügen, um die Dokumentation für die Parameter des primären Konstruktors
hinzuzufügen.
Wenn die generierte, automatisch implementierte Eigenschaftendefinition nicht das Gewünschte ist, können Sie
eine eigene Eigenschaft mit demselben Namen definieren. Wenn Sie dies tun, verwenden der generierte
Konstruktor und Dekonstruktor Ihre Eigenschaftendefinition. Im folgenden Beispiel werden beispielsweise die
FirstName - und LastName -Eigenschaften eines Positionsdatensatzes public deklariert, der Positionsparameter
Id wird jedoch auf internal beschränkt. Sie können diese Syntax für Datensätze und Datensatzstrukturtypen
verwenden. Sie müssen die explizite Zuweisung der deklarierten Eigenschaft dem entsprechenden
Positionsparameter hinzufügen.

public record Person(string FirstName, string LastName, string Id)


{
internal string Id { get; init; } = Id;
}

public static void Main()


{
Person person = new("Nancy", "Davolio", "12345");
Console.WriteLine(person.FirstName); //output: Nancy

Ein Datensatztyp muss keine Positionseigenschaften deklarieren. Sie können einen Datensatz ohne
Positionseigenschaften deklarieren, und Sie können andere Felder und Eigenschaften deklarieren, wie im
folgenden Beispiel gezeigt:

public record Person(string FirstName, string LastName)


{
public string[] PhoneNumbers { get; init; } = Array.Empty<string>();
};
Wenn Sie Eigenschaften mit der Standardeigenschaftensyntax definieren, aber den Zugriffsmodifizierer
weglassen, sind die Eigenschaften implizit private .

Unveränderlichkeit
Ein Positionsdatensatz und eine schreibgeschützte Positionsdatensatzstruktur deklarieren init-only-
Eigenschaften. Eine Positionsdatensatzstruktur deklariert Lese-/Schreibeigenschaften. Sie können diese
Standardwerte überschreiben, wie im vorherigen Abschnitt gezeigt.
Die Unveränderlichkeit kann hilfreich sein, wenn Sie einen threadsicheren datenzentrierten Typ benötigen oder
davon abhängig sind, dass ein Hashcode in einer Hashtabelle unverändert bleibt. Die Unveränderlichkeit ist
jedoch nicht für alle Datenszenarios geeignet. Entity Framework Core unterstützt beispielsweise keine Updates
mit unveränderlichen Entitätstypen.
Unabhängig davon, ob init-only-Eigenschaften aus Positionsparametern ( record class oder
readonly record struct ) oder durch Angabe von init -Zugriffsmethoden erstellt wurden, weisen sie eine
flache Unveränderlichkeit auf. Nach der Initialisierung können Sie den Wert der Werttypeigenschaften oder den
Verweis der Verweistypeigenschaften nicht ändern. Allerdings können die Daten, auf die eine
Verweistypeigenschaft verweist, geändert werden. Das folgende Beispiel zeigt, dass der Inhalt einer
unveränderlichen Verweistypeigenschaft (in diesem Fall ein Array) änderbar ist:

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()


{
Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234

person.PhoneNumbers[0] = "555-6789";
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}

Die für Datensatztypen eindeutigen Features werden von durch den Compiler synthetisierte Methoden
implementiert, und keine dieser Methoden beeinträchtigt die Unveränderlichkeit durch Ändern des
Objektzustands. Sofern nicht angegeben, werden die synthetisierten Methoden für record -, record struct -
und readonly record struct -Deklarationen generiert.

Wertgleichheit
Für jeden Typ, den Sie definieren, können Sie Object.Equals(Object) überschreiben und operator == überladen.
Wenn Sie Equals nicht überschreiben oder operator == überladen, bestimmt der von Ihnen deklarierte Typ,
wie Gleichheit definiert wird:
Bei class -Typen sind zwei Objekte gleich, wenn sie auf das gleiche Objekt im Arbeitsspeicher verweisen.
Bei record -Typen sind zwei Objekte gleich, wenn sie die gleichen Werte speichern und denselben Typ
aufweisen.
Bei struct -Typen sind zwei Objekte gleich, wenn sie die gleichen Werte speichern.
Bei record struct - und readonly record struct -Typen sind zwei Objekte gleich, wenn sie die gleichen
Werte speichern.
Die Definition von Gleichheit für record struct entspricht der Definition für struct . Der Unterschied besteht
darin, dass sich die Implementierung für struct in ValueType.Equals(Object) befindet und auf Reflexion basiert.
Bei record struct wird die Implementierung vom Compiler synthetisiert und verwendet die deklarierten
Datenmember.
Verweisgleichheit ist für einige Datenmodelle erforderlich. Entity Framework Core hängt beispielsweise von der
Verweisgleichheit ab, um sicherzustellen, dass für konzeptionell eine Entität nur eine Instanz eines Entitätstyps
verwendet wird. Aus diesem Grund sind Datensätze und Datensatzstrukturen für die Verwendung als
Entitätstypen in Entity Framework Core nicht geeignet.
Im folgenden Beispiel wird die Wertgleichheit von Datensatztypen veranschaulicht:

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()


{
var phoneNumbers = new string[2];
Person person1 = new("Nancy", "Davolio", phoneNumbers);
Person person2 = new("Nancy", "Davolio", phoneNumbers);
Console.WriteLine(person1 == person2); // output: True

person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True

Console.WriteLine(ReferenceEquals(person1, person2)); // output: False


}

Zum Implementieren von Wertgleichheit synthetisiert der Compiler die folgenden Methoden:
Eine Überschreibung von Object.Equals(Object)
Diese Methode wird als Grundlage für die statische Object.Equals(Object, Object)-Methode verwendet,
wenn beide Parameter nicht NULL sind.
Eine virtuelle Equals -Methode, deren Parameter der Datensatztyp ist Diese Methode implementiert
IEquatable<T>.
Eine Überschreibung von Object.GetHashCode()
Überschreibungen der Operatoren == und != .
Sie können eigene Implementierungen schreiben, um diese synthetisierten Methoden zu ersetzen. Wenn ein
Datensatztyp über eine Methode verfügt, die mit der Signatur einer beliebigen synthetisierten Methode
übereinstimmt, wird diese Methode vom Compiler nicht synthetisiert.
Wenn Sie Ihre eigene Implementierung von Equals in einem Datensatztyp bereitstellen, stellen Sie auch eine
Implementierung von GetHashCode bereit.

Nichtdestruktive Mutation
Wenn eine Instanz mit einigen Änderungen kopieren müssen, können Sie einen with -Ausdruck verwenden, um
eine nichtdestruktive Mutation zu erzielen. Ein with -Ausdruck erstellt eine neue Datensatzinstanz, bei der es
sich um eine Kopie einer vorhandenen Datensatzinstanz handelt, bei der bestimmte Eigenschaften und Felder
geändert wurden. Mit der Objektinitialisierersyntax können Sie die zu ändernden Werte angeben, wie im
folgenden Beispiel gezeigt:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; }
}

public static void Main()


{
Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
Console.WriteLine(person1);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }

Person person2 = person1 with { FirstName = "John" };


Console.WriteLine(person2);
// output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False

person2 = person1 with { PhoneNumbers = new string[1] };


Console.WriteLine(person2);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False

person2 = person1 with { };


Console.WriteLine(person1 == person2); // output: True
}

Der with -Ausdruck kann Positionseigenschaften festlegen oder Eigenschaften, die mithilfe der
Standardeigenschaftensyntax erstellt werden. Damit Nicht-Positionseigenschaften in einem with -Ausdruck
geändert werden können, müssen sie eine init - oder set -Zugriffsmethode aufweisen.
Das Ergebnis eines with -Ausdrucks ist eine flache Kopie. Dies bedeutet, dass bei einer Verweiseigenschaft nur
der Verweis auf eine Instanz kopiert wird. Sowohl der ursprüngliche Datensatz als auch die Kopie verfügen über
einen Verweis auf dieselbe Instanz.
Um dieses Feature für record class -Typen zu implementieren, synthetisiert der Compiler eine Klonmethode
und einen Kopierkonstruktor. Die virtuelle Klonmethode gibt einen neuen Datensatz zurück, der vom
Kopierkonstruktor initialisiert wurde. Wenn Sie einen with -Ausdruck verwenden, erstellt der Compiler Code,
der die Klonmethode aufruft, und legt dann die Eigenschaften fest, die im with -Ausdruck angegeben werden.
Wenn Sie ein anderes Kopierverhalten benötigen, können Sie einen eigenen Kopierkonstruktor in record class
schreiben. Wenn Sie dies tun, synthetisiert der Compiler keinen Kopierkonstruktor. Legen Sie den Konstruktor
als private fest, wenn der Datensatz sealed ist, und legen Sie ihn andernfalls als protected fest. Der Compiler
synthetisiert keinen Kopierkonstruktor für record struct -Typen. Sie können einen solchen schreiben, aber der
Compiler generiert keine entsprechenden Aufrufe für with -Ausdrücke. Stattdessen verwendet der Compiler
Zuweisung.
Sie können die Klonmethode nicht überschreiben, und Sie können keinen Member mit dem Namen Clone in
einem Datensatztyp erstellen. Der tatsächliche Name der Klonmethode wird vom Compiler generiert.

Integrierte Formatierung für die Anzeige


Datensatztypen verfügen über eine vom Compiler generierte ToString-Methode, die die Namen und Werte der
öffentlichen Eigenschaften und Felder anzeigt. Die ToString -Methode gibt eine Zeichenfolge in folgendem
Format zurück:

<record type name> { <property name> = <value>, <property name> = <value>, ...}

Für Verweistypen wird der Typname des Objekts, auf das die Eigenschaft verweist, anstelle des
Eigenschaftenwerts angezeigt. Im folgenden Beispiel ist das Array ein Verweistyp, sodass System.String[]
anstelle der tatsächlichen Arrayelementwerte angezeigt wird:

Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }

Um dieses Feature zu implementieren, synthetisiert der Compiler in record class -Typen eine virtuelle
PrintMembers -Methode und eine ToString-Überschreibung. In record struct -Typen ist dieser Member private
. Mit der ToString -Überschreibung wird ein StringBuilder-Objekt mit dem Typnamen gefolgt von einer
öffnenden eckigen Klammer erstellt. Es ruft PrintMembers auf, um Eigenschaftennamen und Werte
hinzuzufügen, und fügt dann die schließende Klammer hinzu. Das folgende Beispiel zeigt Code, der mit dem
Inhalt der synthetisierten Überschreibung vergleichbar ist:

public override string ToString()


{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Teacher"); // type name
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}

Sie können Ihre eigene Implementierung von PrintMembers oder der ToString -Überschreibung bereitstellen.
Beispiele finden Sie weiter unten in diesem Artikel im Abschnitt PrintMembers -Formatierung in abgeleiteten
Datensätzen. In C# 10 und höheren Versionen kann Ihre Implementierung von ToString den sealed -
Modifizierer enthalten, der den Compiler daran hindert, eine ToString -Implementierung für abgeleitete
Datensätze zu synthetisieren. Das bedeutet, dass die ToString -Ausgabe keine Laufzeittypinformationen enthält.
(Alle Member und Werte werden angezeigt, da für abgeleitete Datensätze weiterhin eine PrintMembers-
Methode generiert wird.)

Vererbung
Dieser Abschnitt gilt nur für record class -Typen.
Ein Datensatz kann von einem anderen Datensatz erben. Ein Datensatz kann jedoch nicht von einer Klasse erben,
und eine Klasse kann nicht von einem Datensatz erben.
Positionsparameter in abgeleiteten Datensatztypen
Der abgeleitete Datensatz deklariert Positionsparameter für alle Parameter im primären Konstruktor des
Basisdatensatzes. Der Basisdatensatz deklariert und initialisiert diese Eigenschaften. Der abgeleitete Datensatz
blendet diese nicht aus. Stattdessen erstellt und initialisiert er nur Eigenschaften für Parameter, die nicht in
seinem Basisdatensatz deklariert sind.
Im folgenden Beispiel wird die Vererbung mit Positionseigenschaftensyntax veranschaulicht:

public abstract record Person(string FirstName, string LastName);


public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
Gleichheit in Vererbungshierarchien
Dieser Abschnitt gilt für record class -Typen, aber nicht für record struct -Typen. Damit zwei
Datensatzvariablen gleich sind, muss der Laufzeittyp gleich sein. Die Typen der enthaltenden Variablen können
unterschiedlich sein. Der Vergleich geerbter Gleichheit wird im folgenden Codebeispiel veranschaulicht:

public abstract record Person(string FirstName, string LastName);


public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Person student = new Student("Nancy", "Davolio", 3);
Console.WriteLine(teacher == student); // output: False

Student student2 = new Student("Nancy", "Davolio", 3);


Console.WriteLine(student2 == student); // output: True
}

Im Beispiel werden alle Variablen als Person deklariert, auch wenn die Instanz ein abgeleiteter Typ von Student
oder Teacher ist. Die Instanzen verfügen alle über dieselben Eigenschaften und Eigenschaftswerte.
student == teacher gibt jedoch False zurück, obwohl beide Person -Typvariablen sind, und
student == student2 gibt True zurück, obwohl die eine eine Person -Variable und die andere eine Student -
Variable ist. Der Gleichheitstest hängt vom Laufzeittyp des tatsächlichen Objekts ab, nicht vom deklarierten Typ
der Variablen.
Um dieses Verhalten zu implementieren, synthetisiert der Compiler eine EqualityContract -Eigenschaft, die ein
Type-Objekt zurückgibt, das mit dem Typ des Datensatzes übereinstimmt. Das EqualityContract -Element
ermöglicht den Gleichheitsmethoden, den Laufzeittyp von Objekten bei der Überprüfung auf Gleichheit zu
vergleichen. Wenn der Basistyp eines Datensatzes object ist, ist diese Eigenschaft virtual . Wenn der Basistyp
einem anderen Datensatztyp entspricht, ist diese Eigenschaft eine Überschreibung. Wenn der Datensatztyp
sealed ist, lautet diese Eigenschaft sealed .

Beim Vergleichen von zwei Instanzen eines abgeleiteten Typs überprüfen die synthetisierten
Gleichheitsmethoden alle Eigenschaften der Basis- und abgeleiteten Typen auf Gleichheit. Die synthetisierte
GetHashCode -Methode verwendet die GetHashCode -Methode aus allen im Basistyp und im abgeleiteten
Datensatztyp deklarierten Eigenschaften und Feldern.
with -Ausdrücke in abgeleiteten Datensätzen
Das Ergebnis eines with -Ausdrucks weist denselben Laufzeittyp auf wie der Operand des Ausdrucks. Alle
Eigenschaften des Laufzeittyps werden kopiert, Sie können jedoch nur Eigenschaften des Kompilierzeittyps
festlegen, wie im folgenden Beispiel gezeigt:
public record Point(int X, int Y)
{
public int Zbase { get; set; }
};
public record NamedPoint(string Name, int X, int Y) : Point(X, Y)
{
public int Zderived { get; set; }
};

public static void Main()


{
Point p1 = new NamedPoint("A", 1, 2) { Zbase = 3, Zderived = 4 };

Point p2 = p1 with { X = 5, Y = 6, Zbase = 7 }; // Can't set Name or Zderived


Console.WriteLine(p2 is NamedPoint); // output: True
Console.WriteLine(p2);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = A, Zderived = 4 }

Point p3 = (NamedPoint)p1 with { Name = "B", X = 5, Y = 6, Zbase = 7, Zderived = 8 };


Console.WriteLine(p3);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = B, Zderived = 8 }
}

PrintMembers -Formatierung in abgeleiteten Datensätzen


Die synthetisierte PrintMembers -Methode eines abgeleiteten Datensatztyps ruft die Basisimplementierung auf.
Das Ergebnis ist, dass alle öffentlichen Eigenschaften und Felder sowohl von abgeleiteten als auch Basistypen in
der ToString -Ausgabe enthalten sind, wie im folgenden Beispiel gezeigt:

public abstract record Person(string FirstName, string LastName);


public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);

public static void Main()


{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Sie können Ihre eigene Implementierung der PrintMembers -Methode bereitstellen. Wenn Sie dies tun,
verwenden Sie die folgende Signatur:
Für einen sealed -Datensatz, der von abgeleitet wird (deklariert keinen Basisdatensatz):
object
private bool PrintMembers(StringBuilder builder) ;
Für einen sealed -Datensatz, der von einem anderen Datensatz abgeleitet wird:
protected sealed override bool PrintMembers(StringBuilder builder) ;
Für einen Datensatz, der nicht sealed ist und von einem Objekt abgeleitet wird:
protected virtual bool PrintMembers(StringBuilder builder);
Für einen Datensatz, der nicht sealed ist und von einem anderen Datensatz abgeleitet wird:
protected override bool PrintMembers(StringBuilder builder);

Im Folgenden finden Sie ein Beispiel für Code, der die synthetisierten PrintMembers -Methoden ersetzt, ein
Beispiel für einen Datensatztyp, der vom Objekt abgeleitet wird, und ein Beispiel für einen Datensatztyp, der von
einem anderen Datensatz abgeleitet wird:
public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
return true;
}
}

public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
: Person(FirstName, LastName, PhoneNumbers)
{
protected override bool PrintMembers(StringBuilder stringBuilder)
{
if (base.PrintMembers(stringBuilder))
{
stringBuilder.Append(", ");
};
stringBuilder.Append($"Grade = {Grade}");
return true;
}
};

public static void Main()


{
Person teacher = new Teacher("Nancy", "Davolio", new string[2] { "555-1234", "555-6789" }, 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, PhoneNumber1 = 555-1234, PhoneNumber2 = 555-
6789, Grade = 3 }
}

NOTE
In C# 10.0 und höheren Versionen synthetisiert der Compiler PrintMembers , wenn ein Basisdatensatz die ToString -
Methode versiegelt hat. Sie können auch eine eigene Implementierung von PrintMembers erstellen.

Dekonstruktorverhalten in abgeleiteten Datensätzen


Die Deconstruct -Methode eines abgeleiteten Datensatzes gibt die Werte aller Positionseigenschaften des
Kompilierzeittyps zurück. Wenn der Variablentyp ein Basisdatensatz ist, werden nur die
Basisdatensatzeigenschaften dekonstruiert, es sei denn, das Objekt wird in den abgeleiteten Typ umgewandelt.
Das folgende Beispiel zeigt, wie ein Dekonstruktor für einen abgeleiteten Datensatz aufgerufen wird.

public abstract record Person(string FirstName, string LastName);


public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);

public static void Main()


{
Person teacher = new Teacher("Nancy", "Davolio", 3);
var (firstName, lastName) = teacher; // Doesn't deconstruct Grade
Console.WriteLine($"{firstName}, {lastName}");// output: Nancy, Davolio

var (fName, lName, grade) = (Teacher)teacher;


Console.WriteLine($"{fName}, {lName}, {grade}");// output: Nancy, Davolio, 3
}
Generische Einschränkungen
Es gibt keine generische Einschränkung, die erfordert, dass ein Typ ein Datensatz ist. Datensätze erfüllen
entweder die class - oder die struct -Einschränkung. Um eine Einschränkung für eine bestimmte Hierarchie
von Datensatztypen festzulegen, wenden Sie die Einschränkung wie eine Basisklasse auf den Basisdatensatz an.
Weitere Informationen finden Sie unter Einschränkungen für Typparameter (C#-Programmierhandbuch).

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Klassen der C#-Sprachspezifikation.
Weitere Informationen zu in C# 9 und höher eingeführten Features finden Sie in den folgenden
Featurevorschlägen:
Datensätze
Init-Only-Setter
Covariante Rückgaben

Siehe auch
C#-Referenz
Entwurfsrichtlinien: Auswählen zwischen Klasse und Struktur
Entwurfsrichtlinien: Strukturentwurf
Das C#-Typsystem
with -Ausdruck
class (C#-Referenz)
04.11.2021 • 2 minutes to read

Klassen werden mithilfe des Schlüsselworts class deklariert, wie im folgenden Beispiel dargestellt:

class TestClass
{
// Methods, properties, fields, events, delegates
// and nested classes go here.
}

Hinweise
In C# ist nur die einfache Vererbung zulässig. Eine Klasse kann also Implementierungen aus nur einer
Basisklasse erben. Es kann allerdings mehr als eine Schnittstelle implementiert werden. Die folgende Tabelle
zeigt Beispiele für Klassenvererbung und Implementierung der Schnittstelle:

VERERB UN G B EISP IEL

Keine class ClassA { }

Single class DerivedClass : BaseClass { }

Keine, Implementierung von zwei Schnittstellen class ImplClass : IFace1, IFace2 { }

Single, Implementierung einer Schnittstelle class ImplDerivedClass : BaseClass, IFace1 { }

Klassen, die Sie direkt innerhalb eines Namespace und nicht in anderen Klassen geschachtelt deklarieren,
können entweder public oder internal sein. Klassen sind standardmäßig internal .
Klassenmember, einschließlich geschachtelter Klassen, können öffentlich, intern geschützt, geschützt, intern,
privat geschützt oder privat sein. Member sind standardmäßig private .
Weitere Informationen finden Sie unter Zugriffsmodifizierer.
Sie können generische Klassen deklarieren, die über Typparameter verfügen. Weitere Informationen finden Sie
unter Generische Klassen.
Eine Klasse kann Deklarationen der folgenden Member enthalten:
Konstruktoren
Konstanten
Fields
Finalizer
Methoden
Eigenschaften
Indexer
Operatoren
Ereignisse
Delegaten
Klassen
Schnittstellen
Strukturtypen
Enumerationstypen

Beispiel
Das folgende Beispiel zeigt das Deklarieren von Klassenfeldern, Konstruktoren und Methoden. Darüber hinaus
veranschaulicht es Objektinstanziierung und Ausgabe von Instanzdaten. In diesem Beispiel werden zwei Klassen
deklariert. Die erste Klasse, Child , enthält zwei private Felder ( name und age ), zwei öffentliche Konstruktoren
und eine öffentliche Methode. Die zweite Klasse, StringTest , enthält Main .
class Child
{
private int age;
private string name;

// Default constructor:
public Child()
{
name = "N/A";
}

// Constructor:
public Child(string name, int age)
{
this.name = name;
this.age = age;
}

// Printing method:
public void PrintChild()
{
Console.WriteLine("{0}, {1} years old.", name, age);
}
}

class StringTest
{
static void Main()
{
// Create objects by using the new operator:
Child child1 = new Child("Craig", 11);
Child child2 = new Child("Sally", 10);

// Create an object using the default constructor:


Child child3 = new Child();

// Display results:
Console.Write("Child #1: ");
child1.PrintChild();
Console.Write("Child #2: ");
child2.PrintChild();
Console.Write("Child #3: ");
child3.PrintChild();
}
}
/* Output:
Child #1: Craig, 11 years old.
Child #2: Sally, 10 years old.
Child #3: N/A, 0 years old.
*/

Kommentare
Beachten Sie, dass im vorherigen Beispiel nur über die öffentliche Methode der Klasse Child auf die privaten
Felder ( name und age ) zugegriffen werden kann. Sie können z.B. den Namen des untergeordneten Elements
nicht aus der Main -Methode mit einer Anweisung wie folgt drucken:

Console.Write(child1.name); // Error

Zugriff auf private Member der Child von Main wäre nur möglich, wenn Main ein Member der Klasse wäre.
Typen, die innerhalb einer Klasse ohne Zugriffsmodifizierer deklariert werden, sind standardmäßig private ,
sodass die Datenmember in diesem Beispiel dennoch private wären, wenn das Schlüsselwort entfernt worden
wäre.
Beachten Sie schließlich, dass für das Objekt, das mit dem parameterlosen Konstruktor ( child3 ) erstellt wurde,
das Feld age standardmäßig auf 0 (null) initialisiert wurde.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Verweistypen
interface (C#-Referenz)
04.11.2021 • 2 minutes to read

Eine Schnittstelle definiert einen Vertrag. Jede class oder struct , die diesen Vertrag implementiert, muss eine
Implementierung der in der Schnittstelle definierten Member bereitstellen. Ab C# 8.0 kann eine Schnittstelle
eine Standardimplementierung für Member definieren. Sie kann auch static -Member definieren, um eine
einzelne Implementierung für allgemeine Funktionen bereitzustellen.
Im folgenden Beispiel muss die Klasse ImplementationClass eine Methode mit dem Namen SampleMethod
implementieren, die keine Parameter hat und void zurückgibt.
Weitere Informationen und Beispiele finden Sie unter Schnittstellen.

Beispielschnittstelle
interface ISampleInterface
{
void SampleMethod();
}

class ImplementationClass : ISampleInterface


{
// Explicit interface member implementation:
void ISampleInterface.SampleMethod()
{
// Method implementation.
}

static void Main()


{
// Declare an interface instance.
ISampleInterface obj = new ImplementationClass();

// Call the member.


obj.SampleMethod();
}
}

Eine Schnittstelle kann ein Member eines Namespaces oder einer Klasse sein. Eine Schnittstellendeklaration
kann Deklarationen der folgenden Member enthalten (Signaturen ohne Implementierungen):
Methoden
Eigenschaften
Indexer
Ereignisse
Diese vorangehenden Memberdeklarationen enthalten in der Regel keinen Text. Ab C# 8.0 kann ein
Schnittstellenmember einen Text deklarieren. Dies wird als Standardimplementierung bezeichnet. Member mit
Text ermöglichen der Schnittstelle, eine „Standardimplementierung“ für Klassen und Strukturen bereitzustellen,
die keine überschreibende Implementierung bereitstellen. Außerdem kann eine Schnittstelle ab C# 8.0
Folgendes umfassen:
Konstanten
Operatoren
Statischer Konstruktor
Geschachtelte Typen
Statische Felder, Methoden, Eigenschaften, Indexer und Ereignisse
Memberdeklarationen, die die Syntax der explizite Schnittstellenimplementierung verwenden
Explizite Zugriffsmodifizierer (der Standardzugriff ist public )
Schnittstellen dürfen keinen Instanzstatus enthalten. Obwohl statische Felder jetzt zulässig sind, sind
Instanzfelder in Schnittstellen nicht zulässig. Automatische Eigenschaften von Instanzen werden in Schnittstellen
nicht unterstützt, da sie implizit ein ausgeblendetes Feld deklarieren würden. Diese Regel hat eine fast
unmerkliche Auswirkung auf Eigenschaftsdeklarationen. In einer Schnittstellendeklaration deklariert der
folgende Code anders als bei class und struct keine automatisch implementierte Eigenschaft. Stattdessen
wird eine Eigenschaft deklariert, die keine Standardimplementierung hat, sondern in jedem Typ implementiert
werden muss, der die Schnittstelle implementiert:

public interface INamed


{
public string Name {get; set;}
}

Eine Schnittstelle kann von einer oder mehreren Basisschnittstellen erben. Wenn eine Schnittstelle eine Methode
überschreibt die in einer Basisschnittstelle implementiert ist, muss sie die Syntax der expliziten
Schnittstellenimplementierung verwenden.
Wenn eine Basistypliste sowohl eine Basisklasse als auch Schnittstellen umfasst, muss die Basisklasse zuerst in
der Liste stehen.
Eine Klasse, die eine Schnittstelle implementiert, kann Member dieser Schnittstelle explizit implementieren. Auf
einen explizit implementierten Member kann nicht durch eine Klasseninstanz zugegriffen werden, sondern nur
durch eine Schnittstelleninstanz. Außerdem kann auf Standardschnittstellenmember nur über eine Instanz der
Schnittstelle zugegriffen werden.
Weitere Informationen zur expliziten Implementierung finden Sie unter Explizite Schnittstellenimplementierung.

Beispielschnittstellenimplementierungen
Das folgende Beispiel veranschaulicht die Schnittstellenimplementierung. In diesem Beispiel enthält die
Schnittstelle die Eigenschaftendeklaration, und die Klasse enthält die Implementierung. Eine beliebige Instanz
einer Klasse, die IPoint implementiert, hat die ganzzahligen Eigenschaften x und y .
interface IPoint
{
// Property signatures:
int X
{
get;
set;
}

int Y
{
get;
set;
}

double Distance
{
get;
}
}

class Point : IPoint


{
// Constructor:
public Point(int x, int y)
{
X = x;
Y = y;
}

// Property implementation:
public int X { get; set; }

public int Y { get; set; }

// Property implementation
public double Distance =>
Math.Sqrt(X * X + Y * Y);

class MainClass
{
static void PrintPoint(IPoint p)
{
Console.WriteLine("x={0}, y={1}", p.X, p.Y);
}

static void Main()


{
IPoint p = new Point(2, 3);
Console.Write("My Point: ");
PrintPoint(p);
}
}
// Output: My Point: x=2, y=3

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Schnittstellen der C# -Sprachspezifikation und in der
Featurespezifikation für Standardschnittstellenmember – C# 8.0.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Verweistypen
Schnittstellen
Verwenden von Eigenschaften
Verwenden von Indexern
Nullable-Verweistypen (C#-Referenz)
04.11.2021 • 5 minutes to read

NOTE
Dieser Artikel behandelt Nullable-Verweistypen. Sie können auch Nullable-Werttypen deklarieren.

Nullable-Verweistypen sind ab C# 8.0 verfügbar, sofern der Code für NULL-kompatiblen Kontext aktiviert
wurde. Nullable-Verweistypen, Warnungen bei statischer Analyse des NULL-Status und der NULL-tolerante
Operator sind optionale Sprachfeatures. Alle sind standardmäßig deaktiviert. Ein NULL-kompatibler Kontext
wird auf Projektebene mithilfe von Buildeinstellungen oder im Code mithilfe von Pragmas festgelegt.
In einem NULL-kompatiblen Kontext gilt Folgendes:
Eine Variable mit dem Verweistyp T muss ohne NULL-Werte initialisiert werden, und ihr darf kein Wert
zugewiesen werden, der null sein kann.
Eine Variable mit dem Verweistyp T? kann mit null initialisiert oder null zugewiesen werden, muss
jedoch vor der Dereferenzierung auf null geprüft werden.
Eine m -Variable vom Typ T? wird als ungleich NULL betrachtet, wenn Sie den NULL-toleranten Operator
wie in m! anwenden.
Die Unterschiede zwischen dem Non-Nullable-Verweistyp T und dem Nullable-Verweistyp T? werden so
erzwungen, wie der Compiler die vorhergehenden Regeln interpretiert. Eine Variable vom Typ T und eine
Variable vom Typ T? werden durch denselben .NET-Typ dargestellt. Im folgenden Beispiel werden eine Non-
Nullable-Zeichenfolge und eine Nullable-Zeichenfolge deklariert. Anschließend wird der NULL-tolerante
Operator verwendet, um einer Non-Nullable-Zeichenfolge einen Wert zuzuweisen:

string notNull = "Hello";


string? nullable = default;
notNull = nullable!; // null forgiveness

Die Variablen notNull und nullable werden beide durch den String-Typ dargestellt. Da Non-Nullable- und
Nullable-Typen als derselbe Typ gespeichert werden, gibt es mehrere Speicherorte, an denen Nullable-
Verweistypen nicht verwendet werden dürfen. Ein Nullable-Verweistyp kann generell nicht als Basisklasse oder
implementierte Schnittstelle verwendet werden. Ein Nullable-Verweistyp kann nicht in Ausdrücken für
Objekterstellung oder Typtests verwendet werden. Ein Nullable-Verweistyp kann nicht der Typ eines
Memberzugriffsausdrucks sein. In den folgenden Beispielen werden diese Konstrukte veranschaulicht:
public MyClass : System.Object? // not allowed
{
}

var nullEmpty = System.String?.Empty; // Not allowed


var maybeObject = new object?(); // Not allowed
try
{
if (thing is string? nullableString) // not allowed
Console.WriteLine(nullableString);
} catch (Exception? e) // Not Allowed
{
Console.WriteLine("error");
}

Nullable-Verweise und statische Analyse


Die Beispiele im vorherigen Abschnitt veranschaulichen den Charakter von Nullable-Verweistypen. Nullable-
Verweistypen sind keine neuen Klassentypen, sondern eher Annotationen zu vorhandenen Verweistypen. Der
Compiler verwendet diese Annotationen, um potenzielle NULL-Verweisfehler in Ihrem Code zu finden. Für Non-
Nullable-Verweistypen und Nullable-Verweistypen werden keine unterschiedlichen Runtimes verwendet. Der
Compiler fügt keine Runtime hinzu, die auf Non-Nullable-Verweistypen prüft. Die Vorteile liegen in der Analyse
zur Kompilierzeit. Der Compiler generiert Warnungen, die Ihnen helfen, potenzielle NULL-Fehler in Ihrem Code
zu finden und zu beheben. Sie deklarieren Ihre Absicht, und der Compiler warnt Sie, wenn Ihr Code gegen diese
Absicht verstößt.
In NULL-kompatiblem Kontext führt der Compiler eine statische Analyse für Variablen für Nullable- und Non-
Nullable-Verweistypen durch. Der Compiler erfasst den null-state jeder Verweisvariablen entweder als not-null
(nicht NULL) oder als maybe-null (vielleicht NULL). Der Standardstatus eines nicht Nullwerte zulassenden
Verweises ist not null (nicht NULL). Der Standardstatus eines Nullwerte zulassenden Verweises ist maybe null
(vielleicht NULL).
Keine Nullwerte zulassende Verweistypen müssen immer sicher dereferenziert werden, da ihr NULL-Status not-
null (nicht NULL) ist. Der Compiler erzwingt diese Regel, indem Warnungen ausgegeben werden, wenn ein Non-
Nullable-Verweistyp nicht in einen Wert ungleich NULL initialisiert wird. Lokale Variablen müssen dort
zugewiesen werden, wo sie auch deklariert werden. Jedem Feld muss in einem Feldinitialisierer bzw. in jedem
Konstruktor ein Wert zugewiesen werden, der not-null (nicht Null) ist. Der Compiler gibt Warnungen aus, wenn
ein keine Nullwerte zulassender Verweis einem Verweis zugeordnet wird, dessen Status maybe null (vielleicht
NULL) ist. Da ein keine Nullwerte zulassender Verweis jedoch den Status not null (nicht NULL) hat, werden keine
Warnungen ausgegeben, wenn diese Variablen dereferenziert werden.

NOTE
Wenn Sie einen maybe-null-Ausdruck einem keine Nullwerte zulassenden Verweistyp zuweisen, erzeugt der Compiler eine
Warnung. Der Compiler erzeugt dann Warnungen für diese Variable, bis sie einem Ausdruck zugewiesen wird, der not-null
(nicht null) ist.

Nullable-Verweistypen können initialisiert oder null zugewiesen werden. Daher muss bei der statischen
Analyse bestätigt werden, dass eine Variable not null (nicht NULL) ist, bevor sie dereferenziert wird. Wenn ein
Nullwerte zulassender Verweis als maybe-nullable (vielleicht NULL) bestimmt wird, erzeugt die Zuweisung an
eine keine Nullwerte zulassende Verweisvariable eine Compilerwarnung. In der folgenden Klasse werden
Beispiele für diese Warnungen veranschaulicht:
public class ProductDescription
{
private string shortDescription;
private string? detailedDescription;

public ProductDescription() // Warning! short description not initialized.


{
}

public ProductDescription(string productDescription) =>


this.shortDescription = productDescription;

public void SetDescriptions(string productDescription, string? details=null)


{
shortDescription = productDescription;
detailedDescription = details;
}

public string GetDescription()


{
if (detailedDescription.Length == 0) // Warning! dereference possible null
{
return shortDescription;
}
else
{
return $"{shortDescription}\n{detailedDescription}";
}
}

public string FullDescription()


{
if (detailedDescription == null)
{
return shortDescription;
}
else if (detailedDescription.Length > 0) // OK, detailedDescription can't be null.
{
return $"{shortDescription}\n{detailedDescription}";
}
return shortDescription;
}
}

Im folgenden Codeausschnitt sehen Sie, an welcher Stelle der Compiler bei Verwendung dieser Klasse
Warnungen ausgibt:

string shortDescription = default; // Warning! non-nullable set to null;


var product = new ProductDescription(shortDescription); // Warning! static analysis knows shortDescription
maybe null.

string description = "widget";


var item = new ProductDescription(description);

item.SetDescriptions(description, "These widgets will do everything.");

In den vorangehenden Beispielen wird die statische Analyse des Compilers veranschaulicht, mit der der NULL-
Status von Verweisvariablen bestimmt wird. Der Compiler wendet Sprachregeln auf NULL-Prüfungen und -
Zuweisungen an, um die Analyse mit Informationen zu versorgen. Der Compiler kann keine Annahmen zur
Semantik von Methoden oder Eigenschaften treffen. Wenn Sie Methoden aufzurufen, die NULL-Prüfungen
durchführen, kann der Compiler nicht wissen, welche Methoden den NULL-Status einer Variablen beeinflussen.
Es gibt einige Attribute, die Sie Ihren APIs hinzufügen können, um den Compiler über die Semantik von
Argumenten und Rückgabewerten zu informieren. Diese Attribute wurden auf viele gängige APIs in den .NET
Core-Bibliotheken angewendet. Beispielsweise wurde IsNullOrEmpty aktualisiert, und der Compiler interpretiert
diese Methode ordnungsgemäß als NULL-Prüfung. Weitere Informationen zu den Attributen für die statische
Analyse des null-state finden Sie im Artikel zu Nullwerte zulassenden Attributen.

Festlegen des NULL-kompatiblen Kontexts


Es gibt zwei Möglichkeiten, den NULL-kompatiblen Kontext festzulegen. Auf Projektebene können Sie die
Projekteinstellung <Nullable>enable</Nullable> hinzufügen. In einer einzelnen C#-Quelldatei können Sie das
#nullable enable -Pragma hinzufügen, um den NULL-kompatiblen Kontext zu aktivieren. Weitere Informationen
finden Sie im Artikel zum Festlegen einer NULL-kompatiblen Strategie. Vor .NET 6 wurde für neue Projekte der
Standardwert <Nullable>disable</Nullable> verwendet. Ab .NET 6 enthalten neue Projekte das
<Nullable>enable</Nullable> -Element in allen Projektvorlagen.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Vorschlägen für die C#-Sprachspezifikation:
Nullwerte zulassende Verweistypen
Entwurf der Spezifikation für Nullable-Verweistypen

Siehe auch
C#-Referenz
Auf NULL festlegbare Werttypen
void (C#-Referenz)
04.11.2021 • 2 minutes to read

Sie verwenden void als Rückgabetyp einer Methode (oder einer lokalen Funktion), um anzugeben, dass die
Methode keinen Wert zurückgibt.

public static void Display(IEnumerable<int> numbers)


{
if (numbers is null)
{
return;
}

Console.WriteLine(string.Join(" ", numbers));


}

Sie können auch void als Referenztyp verwenden, um einen Zeiger auf einen unbekannten Typ zu deklarieren.
Weitere Informationen finden Sie unter Zeigertypen.
void kann nicht als Typ einer Variablen verwendet werden.

Weitere Informationen
C#-Referenz
System.Void
var (C#-Referenz)
04.11.2021 • 2 minutes to read

Ab C# 3 können Variablen, die im Methodenbereich deklariert wurden, den impliziten „Typ“ var haben. Eine
implizit typisierte lokale Variable ist stark typisiert, als hätten Sie den Typ selbst deklariert. Tatsächlich legt der
Compiler den Typ fest. Die folgenden beiden i -Aktivitäten sind funktional äquivalent:

var i = 10; // Implicitly typed.


int i = 10; // Explicitly typed.

IMPORTANT
Wenn var mit aktivierten Verweistypen verwendet wird, die Nullwerte zulassen, impliziert dies immer einen Verweistyp,
der Nullwerte zulässt, auch wenn der Ausdruckstyp diese zulässt. Die NULL-Statusanalyse des Compilers schützt vor dem
Dereferenzieren eines potenziellen null -Werts. Wenn die Variable nie einem Ausdruck zugewiesen wird, der
möglicherweise NULL ist, gibt der Compiler keine Warnungen aus. Wenn Sie die Variable einem Ausdruck zuweisen, der
möglicherweise NULL ist, müssen Sie testen, ob er NULL ist, bevor Sie ihn dereferenzieren, um Warnungen zu vermeiden.

Das var -Schlüsselwort wird häufig verwendet, wenn Konstruktoraufrufausdrücke verwendet werden. Durch
die Verwendung von var können Sie einen Typnamen in einer Variablendeklaration und Objektinstanziierung
nicht wiederholen, wie im folgenden Beispiel gezeigt:

var xs = new List<int>();

Ab C# 9.0 können Sie als Alternative einen als Ziel typisierten new -Ausdruck verwenden:

List<int> xs = new();
List<int>? ys = new();

Bei einem Musterabgleich wird das var -Schlüsselwort in einem var -Muster verwendet.

Beispiel
Im folgenden Beispiel werden zwei Abfrageausdrücke gezeigt. Im ersten Ausdruck in das Verwenden von var
erlaubt aber nicht erforderlich, da der Typ des Abfrageergebnisses explizit als IEnumerable<string> angegeben
werden kann. Im zweiten Ausdruck ermöglicht var , dass das Ergebnis eine Auflistung von anonymen Typen ist
und dass auf die Namen dieser Typen nicht zugegriffen werden kann. Nur der Compiler kann darauf zugreifen.
Durch die Verwendung von var wird die Voraussetzung beseitigt, eine neue Klasse für das Ergebnis erstellen
zu müssen. Beachten Sie, dass die foreach -Iterationsvariable item im zweiten Beispiel auch implizit typisiert
sein muss.
// Example #1: var is optional when
// the select clause specifies a string
string[] words = { "apple", "strawberry", "grape", "peach", "banana" };
var wordQuery = from word in words
where word[0] == 'g'
select word;

// Because each element in the sequence is a string,


// not an anonymous type, var is optional here also.
foreach (string s in wordQuery)
{
Console.WriteLine(s);
}

// Example #2: var is required because


// the select clause specifies an anonymous type
var custQuery = from cust in customers
where cust.City == "Phoenix"
select new { cust.Name, cust.Phone };

// var must be used because each item


// in the sequence is an anonymous type
foreach (var item in custQuery)
{
Console.WriteLine("Name={0}, Phone={1}", item.Name, item.Phone);
}

Siehe auch
C#-Referenz
Implizit typisierte lokale Variablen
Type relationships in LINQ query operations (Typbeziehungen in LINQ-Abfragevorgängen)
Integrierte Typen (C#-Referenz)
04.11.2021 • 2 minutes to read

In der folgenden Tabelle werden die in C# integrierten Werttypen aufgelistet:

C #- T Y P SC H L ÜSSEL W O RT . N ET - T Y P

bool System.Boolean

byte System.Byte

sbyte System.SByte

char System.Char

decimal System.Decimal

double System.Double

float System.Single

int System.Int32

uint System.UInt32

nint System.IntPtr

nuint System.UIntPtr

long System.Int64

ulong System.UInt64

short System.Int16

ushort System.UInt16

In der folgenden Tabelle werden die in C# integrierten Verweistypen aufgelistet:

C #- T Y P SC H L ÜSSEL W O RT . N ET - T Y P

object System.Object

string System.String

dynamic System.Object

In den Tabellen oben ist jedes C#-Typschlüsselwort aus der linken Spalte (mit Ausnahme von nint und nuint
sowie dynamic) ein Alias für den entsprechenden .NET-Typ. Sie können synonym verwendet werden. In den
folgenden Deklarationen werden beispielsweise Variablen des gleichen Typs deklariert:

int a = 123;
System.Int32 b = 123;

Die Typen nint und nuint sind Integerwerte mit nativer Größe. Sie werden intern durch die angegebenen
.NET-Typen dargestellt, aber in jedem Fall sind das Schlüsselwort und der .NET-Typ nicht austauschbar. Der
Compiler stellt für nint und nuint als Integertypen Vorgänge und Konvertierungen zur Verfügung, die er für
die Zeigertypen System.IntPtr und System.UIntPtr nicht bereitstellt. Weitere Informationen finden Sie unter
den nint - und nuint -Typen.
Das Schlüsselwort void markiert das Fehlen eines Typs. Sie können es als Rückgabetyp einer Methode
verwenden, die keinen Wert zurückgibt.

Siehe auch
C#-Referenz
Standardwerte der C#-Typen
Nicht verwaltete Typen (C#-Verweis)
04.11.2021 • 2 minutes to read

Ein Typ ist ein nicht ver walteter Typ , wenn es sich um einen der folgenden Typen handelt:
sbyte , byte , short , ushort , int , uint , long , ulong , char , float , double , decimal oder bool
Beliebiger Enumerationstyp
Beliebiger Zeigertyp
Jeder benutzerdefinierte Strukturtyp, der nur Felder mit nicht verwalteten Typen enthält und – in C# 7.3 und
früher – kein konstruierter Typ ist (ein konstruierter Typ ist ein Typ, der mindestens ein Typargument enthält)
Ab C# 7.3 können Sie die unmanaged -Einschränkung verwenden, um anzugeben, dass ein Typparameter ein
nicht verwalteter Nicht-Nullable-Nichtzeigertyp ist.
Ab C# 8.0 ist ein konstruierter Strukturtyp, der Felder mit nicht verwalteten Typen enthält, ebenfalls nicht
verwaltet, wie das folgende Beispiel zeigt:

using System;

public struct Coords<T>


{
public T X;
public T Y;
}

public class UnmanagedTypes


{
public static void Main()
{
DisplaySize<Coords<int>>();
DisplaySize<Coords<double>>();
}

private unsafe static void DisplaySize<T>() where T : unmanaged


{
Console.WriteLine($"{typeof(T)} is unmanaged and its size is {sizeof(T)} bytes");
}
}
// Output:
// Coords`1[System.Int32] is unmanaged and its size is 8 bytes
// Coords`1[System.Double] is unmanaged and its size is 16 bytes

Eine generische Struktur kann als Quelle sowohl für nicht verwaltete als auch für verwaltete konstruierte Typen
dienen. Das oben stehende Beispiel definiert die generische Struktur Coords<T> und zeigt Beispiele nicht
verwalteter konstruierter Typen. Ein Beispiel für einen verwalteten Typ ist Coords<object> . Der Typ ist verwaltet,
weil er Felder des Typs object enthält, der verwaltet ist. Wenn alle konstruierten Typen nicht verwaltet sein
sollen, verwenden Sie die unmanaged -Einschränkung in der Definition einer generischen Struktur:

public struct Coords<T> where T : unmanaged


{
public T X;
public T Y;
}
C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Zeigertypen der Spezifikation für die Sprache C#.

Siehe auch
C#-Referenz
Zeigertypen
Memory- und Span-bezogene Typen
sizeof (Operator)
stackalloc
Standardwerte der C#-Typen (C#-Referenz)
04.11.2021 • 2 minutes to read

In der folgenden Tabelle werden die Standardwerte von C#-Typen gezeigt:

TYP STA N DA RDW ERT

Ein beliebiger Verweistyp null

Beliebiger integrierter integraler numerischer Typ 0 (null)

Beliebiger integrierter numerischer Gleitkommatyp 0 (null)

bool false

char '\0' (U+0000)

enum Der Wert, der vom Ausdruck (E)0 erzeugt wird, bei dem
E der Enumerationsbezeichner ist.

struct Der Wert, der erzeugt wird, indem alle Werttypfelder auf ihre
Standardwerte festgelegt werden und alle Verweistypfelder
auf null .

Ein Werttyp, der NULL-Werte zulässt. Eine Instanz, für die die HasValue-Eigenschaft false und
die Value-Eigenschaft nicht definiert ist. Dieser Standardwert
wird auch als NULL-Wert eines Nullable-Werttyps
bezeichnet.

Ausdrücke mit Standardwert


Verwenden Sie den default -Operator, um wie im folgenden Beispiel den Standardwert eines Typs zu erzeugen:

int a = default(int);

Ab C# 7.1 können Sie das default -Literal verwenden, um eine Variable mit dem Standardwert des Typs zu
initialisieren:

int a = default;

Parameterloser Konstruktor eines Werttyps


Für einen Werttyp generiert der implizite parameterlose Konstruktor auch den Standardwert des Typs, wie das
folgende Beispiel zeigt:

var n = new System.Numerics.Complex();


Console.WriteLine(n); // output: (0, 0)
Wenn die System.Type-Instanz zur Laufzeit einen Werttyp darstellt, können Sie die
Activator.CreateInstance(Type)-Methode verwenden, um den parameterlosen Konstruktor aufzurufen, um den
Standardwert des Typs abzurufen.

NOTE
In C# 10.0 und höher kann ein Strukturtyp (also ein Werttyp) über einen expliziten parameterlosen Konstruktor verfügen,
der möglicherweise einen Nicht-Standardwert des Typs erzeugt. Daher wird empfohlen, den default -Operator oder das
default -Literal zu verwenden, um den Standardwert eines Typs zu erzeugen.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Standardwerte
Standardkonstruktoren

Siehe auch
C#-Referenz
Konstruktoren
C#-Schlüsselwörter
04.11.2021 • 2 minutes to read

Bei Schlüsselwörtern handelt es sich um vordefinierte reservierte Bezeichner, die eine besondere Bedeutung für
den Compiler haben. Sie können nur dann als Bezeichner in einem Programm verwendet werden, wenn @ als
Präfix vorangestellt wird. Beispiel: @if ist ein gültiger Bezeichner, aber if nicht, da if ein Schlüsselwort ist.
Die erste Tabelle in diesem Thema enthält die Schlüsselwörter, bei denen es sich in jedem Teil eines C#-
Programms um reservierte Bezeichner handelt. Die zweite Tabelle in diesem Thema enthält die
kontextabhängigen Schlüsselwörter in C#. Kontextabhängige Schlüsselwörter haben nur in einem beschränkten
Programmkontext eine besondere Bedeutung und können als Bezeichner außerhalb dieses Kontexts verwendet
werden. Im Allgemeinen werden neue Schlüsselwörter als Kontextschlüsselwörter zur C#-Sprache hinzugefügt,
um Programme, die mit früheren Versionen geschrieben wurden, nicht zu beschädigen.
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
ref
return
sbyte
sealed
short
sizeof
stackalloc
static
string
struct
switch
this
throw
true
try
typeof
uint
ulong
unchecked
unsafe
ushort
using
virtual
void
volatile
while

Kontextabhängige Schlüsselwörter
Ein Kontextschlüsselwort wird verwendet, um eine spezifische Bedeutung im Code bereitzustellen, es ist jedoch
kein reserviertes Wort in C#. Einige kontextabhängige Schlüsselwörter, wie partial und where , haben eine
besondere Bedeutung in mindestens zwei Kontexten.
add
and
alias
ascending
async
await
by
descending
dynamic
equals
from
get
global
group
init
into
join
let
verwaltet (Aufrufkonvention für Funktionszeiger)
nameof
nint
not
notnull
nuint
on
or
orderby
partial (Typ)
partial (Methode)
record
remove
select
set
nicht verwaltet (Aufrufkonvention für Funktionszeiger)
unmanaged (Einschränkung eines generischen Typs)
value
var
when (Filterbedingung)
where (Einschränkung eines generischen Typs)
where (Abfrageklausel)
mit
yield

Siehe auch
C#-Referenz
Zugriffsmodifizierer (C#-Referenz)
04.11.2021 • 2 minutes to read

Zugriffsmodifizierer sind Schlüsselwörter, die verwendet werden, um die deklarierte Zugriffsart eines Members
oder Typs anzugeben. In diesem Abschnitt werden die vier Zugriffsmodifizierer beschrieben:
public
protected
internal
private

Die folgenden sechs Zugriffsebenen können mit den Zugriffsmodifizierern angegeben werden:
public : Der Zugriff ist nicht beschränkt.
protected : Der Zugriff ist auf die enthaltende Klasse oder auf Typen beschränkt, die von der
enthaltenden Klasse abgeleitet sind.
internal : Der Zugriff ist auf die aktuelle Assembly beschränkt.
protected internal : Der Zugriff ist auf die aktuelle Assembly oder auf Typen beschränkt, die von der
enthaltenden Klasse abgeleitet sind.
private : Der Zugriff ist auf die enthaltende Klasse beschränkt.
private protected : Der Zugriff ist auf die enthaltende Klasse oder auf Typen beschränkt, die von der
enthaltenden Klasse innerhalb der aktuellen Assembly abgeleitet sind.
In diesem Abschnitt wird Folgendes beschrieben:
Zugriffsebenen: Deklarieren von sechs Zugriffsebenen mithilfe der vier Zugriffsmodifizierer.
Zugriffsdomäne: Gibt an, in welche Teile des Programms ein Member verwiesen werden kann.
Einschränkungen bei der Verwendung von Zugriffsebenen: Ein Überblick über die Einschränkungen bei
der Verwendung deklarierter Zugriffsebenen.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Zugriffsmodifizierer
Zugriffsschlüsselwörter
Modifizierer
Zugriffsebenen (C#-Referenz)
04.11.2021 • 2 minutes to read

Verwenden Sie die Zugriffsmodifizierer public , protected , internal oder private , um eine der folgenden
deklarierten Zugriffsebenen für Member anzugeben.

DEK L A RIERT ER Z UGRIF F B EDEUT UN G

public Der Zugriff ist nicht beschränkt.

protected Der Zugriff ist auf die enthaltende Klasse oder auf Typen
beschränkt, die von der enthaltenden Klasse abgeleitet sind.

internal Der Zugriff ist auf die aktuelle Assembly beschränkt.

protected internal Der Zugriff ist auf die aktuelle Assembly oder auf Typen
beschränkt, die von der enthaltenden Klasse abgeleitet sind.

private Der Zugriff ist auf die enthaltende Klasse beschränkt.

private protected Der Zugriff ist auf die enthaltende Klasse oder auf Typen
beschränkt, die von der enthaltenden Klasse innerhalb der
aktuellen Assembly abgeleitet sind. Verfügbar seit C# 7.2.

Es ist nur ein Zugriffsmodifizierer für einen Member oder Typ zulässig, außer wenn Sie die protected internal -
oder private protected -Kombination verwenden.
Zugriffsmodifizierer sind bei Namespaces nicht zulässig. Namespaces haben uneingeschränkten Zugriff.
Abhängig vom Kontext einer Memberdeklaration sind nur bestimmte deklarierte Zugriffe zulässig. Wenn in
einer Memberdeklaration kein Zugriffsmodifizierer angegeben ist, wird ein Standardzugriff verwendet.
Typen der obersten Ebene, die nicht in anderen Typen geschachtelt sind, können nur Zugriff der Art internal
oder public haben. Der Standardzugriff für diese Typen ist internal .
Geschachtelte Typen, die Member von anderen Typen sind, können deklarierte Zugriffe haben, wie in der
folgenden Tabelle angegeben.

Z UL Ä SSIGER DEK L A RIERT ER


M EM B ER VO N STA N DA RD- M EM B ERZ UGRIF F Z UGRIF F ST Y P DES M EM B ERS

enum public Keiner


Z UL Ä SSIGER DEK L A RIERT ER
M EM B ER VO N STA N DA RD- M EM B ERZ UGRIF F Z UGRIF F ST Y P DES M EM B ERS

class private public

protected

internal

private

protected internal

private protected

interface public public

protected

internal

private *

protected internal

private protected

struct private public

internal

private

* Ein interface Member mit private -Zugriff muss über eine Standard Implementierung verfügen.
Der Zugriff auf einen geschachtelten Typ hängt von seiner Zugriffsdomäne ab, die sowohl durch den
deklarierten Zugriffstyp des Members als auch durch die Zugriffsdomäne des direkt enthaltenden Typs
bestimmt wird. Die Zugriffsdomäne eines geschachtelten Typs kann jedoch nicht über die des enthaltenden Typs
hinausgehen.

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Zugriffsmodifizierer
Zugriffsdomäne
Einschränkungen bei der Verwendung von Zugriffsebenen
Zugriffsmodifizierer
public
private
protected
internal
Zugriffsdomäne (C#-Referenz)
04.11.2021 • 2 minutes to read

Die Zugriffsdomäne eines Members gibt an, in welche Teile des Programms ein Member verwiesen werden
kann. Wenn der Member in einem anderen Typ geschachtelt ist, wird seine Zugriffsdomäne sowohl durch das
Zugriffslevel des Members als auch durch die Zugriffsdomäne des direkt enthaltenden Typs bestimmt.
Die Zugriffsdomäne eines Typs der obersten Ebene ist mindestens der Programmtext des Projekts, in dem er
deklariert ist. Das bedeutet, dass die Domäne alle Quelldateien des Projekts enthält. Die Zugriffsdomäne eines
geschachtelten Typs ist mindestens der Programmtext des Typs, in dem er deklariert ist. Das bedeutet, dass die
Domäne der Typkörper ist, der alle geschachtelten Typen enthält. Die Zugriffsdomäne eines geschachtelten Typs
geht nie über die des enthaltenden Typs hinaus. Diese Konzepte werden im folgenden Beispiel dargestellt.

Beispiel
Dieses Beispiel enthält einen Typ der obersten Ebene T1 , und zwei geschachtelte Klassen M1 und M2 . Die
Klassen enthalten Felder mit unterschiedlichen deklarierten Zugriffen. In der Main -Methode folgt jeder
Anweisung ein Kommentar, der die Zugriffsdomäne jedes Members angibt. Beachten Sie, dass die Anweisungen,
die versuchen, auf die Member zu verweisen, auf die nicht zugegriffen werden kann, auskommentiert werden.
Wenn Sie die Compilerfehler anzeigen möchten, die durch Verweisen auf einen Member verursacht werden, auf
den nicht zugegriffen werden kann, entfernen Sie die Kommentare nacheinander.

public class T1
{
public static int publicInt;
internal static int internalInt;
private static int privateInt = 0;

static T1()
{
// T1 can access public or internal members
// in a public or private (or internal) nested class.
M1.publicInt = 1;
M1.internalInt = 2;
M2.publicInt = 3;
M2.internalInt = 4;

// Cannot access the private member privateInt


// in either class:
// M1.privateInt = 2; //CS0122
}

public class M1
{
public static int publicInt;
internal static int internalInt;
private static int privateInt = 0;
}

private class M2
{
public static int publicInt = 0;
internal static int internalInt = 0;
private static int privateInt = 0;
}
}

class MainClass
class MainClass
{
static void Main()
{
// Access is unlimited.
T1.publicInt = 1;

// Accessible only in current assembly.


T1.internalInt = 2;

// Error CS0122: inaccessible outside T1.


// T1.privateInt = 3;

// Access is unlimited.
T1.M1.publicInt = 1;

// Accessible only in current assembly.


T1.M1.internalInt = 2;

// Error CS0122: inaccessible outside M1.


// T1.M1.privateInt = 3;

// Error CS0122: inaccessible outside T1.


// T1.M2.publicInt = 1;

// Error CS0122: inaccessible outside T1.


// T1.M2.internalInt = 2;

// Error CS0122: inaccessible outside M2.


// T1.M2.privateInt = 3;

// Keep the console open in debug mode.


System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Zugriffsmodifizierer
Zugriffsebenen
Einschränkungen bei der Verwendung von Zugriffsebenen
Zugriffsmodifizierer
public
private
protected
internal
Einschränkungen bei der Verwendung von
Zugriffsebenen (C#-Referenz)
04.11.2021 • 2 minutes to read

Wenn Sie in einer Deklaration einen Typ angeben, überprüfen Sie, ob die Zugriffsebene dieses Typs von der
Zugriffsebene eines Members oder eines anderen Typs abhängt. Auf die direkte Basisklasse muss z.B.
mindestens genauso zugegriffen werden können wie auf die abgeleitete Klasse. Die folgende Deklaration
verursacht einen Compilerfehler, da die Basisklasse BaseClass eine stärkere Zugriffsbeschränkung hat als
MyClass :

class BaseClass {...}


public class MyClass: BaseClass {...} // Error

In der folgenden Tabelle werden die Einschränkungen für deklarierte Zugriffsebenen zusammengefasst.

KO N T EXT H IN W EISE

Klassen Die direkte Basisklasse eines Klassentyps muss mindesten


dieselben Zugriffsmöglichkeiten wie der Klassentyp selbst
bieten.

Schnittstellen Die explizite Basisschnittstelle eines Schnittstellentyps muss


mindesten dieselben Zugriffsmöglichkeiten bieten wie der
Schnittstellentyp selbst.

Delegaten Die Rückgabe- und Parametertypen eines Delegattyps


müssen mindestens dieselben Zugriffsmöglichkeiten wie der
Delegattyp selbst bieten.

Konstanten Der Typ einer Konstante muss mindestens dieselben


Zugriffsmöglichkeiten wie die Konstante selbst bieten.

Fields Der Typ eines Felds muss mindestens dieselben


Zugriffsmöglichkeiten bieten wie das Feld selbst.

Methoden Die Rückgabe- und Parametertypen einer Methode müssen


mindestens dieselben Zugriffsmöglichkeiten bieten wie die
Methode selbst.

Eigenschaften Der Typ einer Eigenschaft muss mindestens dieselben


Zugriffsmöglichkeiten bieten wie die Eigenschaft selbst.

Ereignisse Der Typ eines Ereignisses muss mindestens dieselben


Zugriffsmöglichkeiten bieten wie das Ereignis selbst.

Indexer Der Typ und die Parametertypen eines Indexers müssen


mindestens dieselben Zugriffsmöglichkeiten bieten wie der
Indexer selbst.
KO N T EXT H IN W EISE

Operatoren Die Rückgabe- und Parametertypen eines Operators müssen


mindestens dieselben Zugriffsmöglichkeiten bieten wie der
Operator selbst.

Konstruktoren Die Parametertypen eines Konstruktors müssen mindestens


dieselben Zugriffsmöglichkeiten bieten wie der Konstruktor
selbst.

Beispiel
Das folgende Beispiel enthält fehlerhafte Deklarationen verschiedener Typen. Der Kommentar nach jeder
Deklaration gibt den erwarteten Compilerfehler an.
// Restrictions on Using Accessibility Levels
// CS0052 expected as well as CS0053, CS0056, and CS0057
// To make the program work, change access level of both class B
// and MyPrivateMethod() to public.

using System;

// A delegate:
delegate int MyDelegate();

class B
{
// A private method:
static int MyPrivateMethod()
{
return 0;
}
}

public class A
{
// Error: The type B is less accessible than the field A.myField.
public B myField = new B();

// Error: The type B is less accessible


// than the constant A.myConst.
public readonly B myConst = new B();

public B MyMethod()
{
// Error: The type B is less accessible
// than the method A.MyMethod.
return new B();
}

// Error: The type B is less accessible than the property A.MyProp


public B MyProp
{
set
{
}
}

MyDelegate d = new MyDelegate(B.MyPrivateMethod);


// Even when B is declared public, you still get the error:
// "The parameter B.MyPrivateMethod is not accessible due to
// protection level."

public static B operator +(A m1, B m2)


{
// Error: The type B is less accessible
// than the operator A.operator +(A,B)
return new B();
}

static void Main()


{
Console.Write("Compiled successfully");
}
}

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Zugriffsmodifizierer
Zugriffsdomäne
Zugriffsebenen
Zugriffsmodifizierer
public
private
protected
internal
internal (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort internal ist ein Zugriffsmodifizierer für Typen und Typmember.

Auf dieser Seite wird der Zugriff auf internal behandelt. Das Schlüsselwort internal ist auch Teil des
Zugriffsmodifizierers protected internal .

Auf interne Typen oder Member kann nur innerhalb einer Datei in derselben Assembly zugegriffen werden, so
wie in diesem Beispiel:

public class BaseClass


{
// Only accessible within the same assembly.
internal static int x = 0;
}

Einen Vergleich von internal mit den anderen Zugriffsmodifizierern finden Sie unter Zugriffsebenen und
Zugriffsmodifizierer.
Weitere Informationen zu Assemblys finden Sie unter Assemblys in .NET.
Ein interner Zugriff wird häufig bei komponentenbasierten Entwicklungen verwendet, da auf diese Weise
Gruppen von Komponenten privat kooperieren können, ohne dass sie für den Rest des Anwendungscodes
verfügbar gemacht werden. Ein Framework könnte z.B für das Erstellen grafischer Benutzeroberflächen Control
- und Form -Klassen zur Verfügung stellen, die kooperieren, indem sie Member mit internen Zugriff verwenden.
Da diese Member intern sind, werden sie nicht für Code verfügbar gemacht, der das Framework verwendet.
Es ist unzulässig, auf einen Typen oder einen Member mit internem Zugriff außerhalb der Assembly zu
verweisen, in der sie definiert wurden.

Beispiel 1
Dieses Beispiel enthält zwei Dateien, Assembly1.cs und Assembly1_a.cs . Die erste Datei enthält eine interne
Basisklasse, BaseClass . In der zweiten Datei führt der Versuch, BaseClass zu instanziieren zu einem Fehler.

// Assembly1.cs
// Compile with: /target:library
internal class BaseClass
{
public static int intM = 0;
}
// Assembly1_a.cs
// Compile with: /reference:Assembly1.dll
class TestAccess
{
static void Main()
{
var myBase = new BaseClass(); // CS0122
}
}

Beispiel 2
Verwenden Sie in diesem Beispiel dieselbe Datei, die Sie im ersten Beispiel verwendet haben, und ändern Sie die
Zugriffsebene von BaseClass in public . Ändern Sie außerdem die Zugriffsebene des Members intM in
internal . Jetzt können Sie die Klasse instanziieren, aber Sie können nicht auf den internen Member zugreifen.

// Assembly2.cs
// Compile with: /target:library
public class BaseClass
{
internal static int intM = 0;
}

// Assembly2_a.cs
// Compile with: /reference:Assembly2.dll
public class TestAccess
{
static void Main()
{
var myBase = new BaseClass(); // Ok.
BaseClass.intM = 444; // CS0117
}
}

C#-Programmiersprachenspezifikation
Weitere Informationen finden Sie unter Deklarierte Barrierefreiheit in der C#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Zugriffsmodifizierer
Zugriffsebenen
Modifizierer
public
private
protected
private (C#-Referenz)
04.11.2021 • 2 minutes to read

Das private -Schlüsselwort ist ein Zugriffsmodifizierer für Member.

Auf dieser Seite wird der Zugriff auf private behandelt. Das Schlüsselwort private ist auch Teil des
Zugriffsmodifizierers private protected .

Privater-Zugriff ist die am wenigsten eingeschränkte Zugriffsebene. Private Member sind nur innerhalb der
Klasse oder Struktur, in der sie, wie im folgenden Beispiel, deklariert werden:

class Employee
{
private int _i;
double _d; // private access by default
}

Geschachtelte Typen im gleichen Text können auch auf diese privaten Member zugreifen.
Es ist ein Kompilierzeitfehler auf einen privaten Member außerhalb der Klasse oder Struktur, in der sie deklariert
ist, zu verweisen.
Einen Vergleich von private mit den anderen Zugriffsmodifizierern finden Sie unter Zugriffsebenen und
Zugriffsmodifizierer.

Beispiel
In diesem Beispiel enthält die Employee -Klasse zwei private Datenmember, _name und _salary . Als private
Member können nur Membermethoden auf sie zugreifen. Die öffentlichen Methoden GetName und Salary
werden hinzugefügt, um gesteuerten Zugriff auf die privaten Member zu ermöglichen. Auf den _name -Member
wird über eine öffentliche Methode zugegriffen, und auf den _salary -Member wird über eine öffentliche
schreibgeschützte Eigenschaft zugegriffen. (Weitere Informationen finden Sie unter Eigenschaften.)
class Employee2
{
private readonly string _name = "FirstName, LastName";
private readonly double _salary = 100.0;

public string GetName()


{
return _name;
}

public double Salary


{
get { return _salary; }
}
}

class PrivateTest
{
static void Main()
{
var e = new Employee2();

// The data members are inaccessible (private), so


// they can't be accessed like this:
// string n = e._name;
// double s = e._salary;

// '_name' is indirectly accessed via method:


string n = e.GetName();

// '_salary' is indirectly accessed via property


double s = e.Salary;
}
}

C#-Sprachspezifikation
Weitere Informationen finden Sie unter Deklarierte Barrierefreiheit in der C#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Zugriffsmodifizierer
Zugriffsebenen
Modifizierer
public
protected
internal
protected (C#-Referenz)
04.11.2021 • 2 minutes to read

Das protected -Schlüsselwort ist ein Zugriffsmodifizierer für Member.

NOTE
Auf dieser Seite wird der Zugriff auf protected behandelt. Das Schlüsselwort protected ist auch Teil der
Zugriffsmodifizierer protected internal und private protected .

Auf einen geschützten Member kann innerhalb seiner Klasse und von Instanzen abgeleiteter Klasse zugegriffen
werden.
Einen Vergleich von protected mit den anderen Zugriffsmodifizierern finden Sie unter Zugriffsebenen.

Beispiel 1
Auf einen geschützten Member einer Basisklasse kann in einer abgeleiteten Klasse zugegriffen werden, nur
wenn der Zugriff über den Typ der abgeleiteten Klasse erfolgt. Sehen Sie sich z.B. folgenden Codeabschnitt an:

class A
{
protected int x = 123;
}

class B : A
{
static void Main()
{
var a = new A();
var b = new B();

// Error CS1540, because x can only be accessed by


// classes derived from A.
// a.x = 10;

// OK, because this class derives from A.


b.x = 10;
}
}

Die Anweisung a.x = 10 generiert einen Fehler, da sie innerhalb der statischen Main-Methode erstellt wird und
keine Instanz der Klasse B ist.
Strukturmember können nicht geschützt werden, da die Struktur nicht vererbt werden kann.

Beispiel 2
In diesem Beispiel wird die DerivedPoint -Klasse von Point abgeleitet. Daher können Sie direkt von der
abgeleiteten Klasse auf die geschützten Member der Basisklasse zugreifen.
class Point
{
protected int x;
protected int y;
}

class DerivedPoint: Point


{
static void Main()
{
var dpoint = new DerivedPoint();

// Direct access to protected members.


dpoint.x = 10;
dpoint.y = 15;
Console.WriteLine($"x = {dpoint.x}, y = {dpoint.y}");
}
}
// Output: x = 10, y = 15

Wenn Sie die Zugriffsebenen von x und y auf private ändern, wird der Compiler die Fehlermeldungen
anzeigen:
'Point.y' is inaccessible due to its protection level.

'Point.x' is inaccessible due to its protection level.

C#-Sprachspezifikation
Weitere Informationen finden Sie unter Deklarierte Barrierefreiheit in der C#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Zugriffsmodifizierer
Zugriffsebenen
Modifizierer
public
private
internal
Sicherheitsaspekte für interne virtuelle Schlüsselwörter
public (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort public ist ein Zugriffsmodifizierer für Typen und Typmember. Der öffentlicher Zugriff ist die
eingeschränkteste Zugriffsebene. Es gibt keine Einschränkungen für den Zugriff auf öffentliche Member, wie im
folgenden Beispiel veranschaulicht:

class SampleClass
{
public int x; // No access restrictions.
}

Unter Access Modifiers (Zugriffsmodifizierer) und Accessibility Levels (Zugriffsebenen) finden Sie weitere
Informationen.

Beispiel
Im folgenden Beispiel werden zwei Klassen deklariert, PointTest und Program . Auf die öffentlichen Member x
und y von PointTest wird direkt von Program zugegriffen.

class PointTest
{
public int x;
public int y;
}

class Program
{
static void Main()
{
var p = new PointTest();
// Direct access to public members.
p.x = 10;
p.y = 15;
Console.WriteLine($"x = {p.x}, y = {p.y}");
}
}
// Output: x = 10, y = 15

Wenn Sie die Zugriffsebene public auf private (privat) oder protected (geschützt) festlegen, wird die folgende
Fehlermeldung angezeigt:
'PointTest.y' is inaccessible due to its protection level (Der Zugriff auf ‚PointTest.y‘ ist aufgrund der
Sicherheitsebene nicht möglich).

C#-Sprachspezifikation
Weitere Informationen finden Sie unter Deklarierte Barrierefreiheit in der C#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
Zugriffsmodifizierer
C#-Schlüsselwörter
Zugriffsmodifizierer
Zugriffsebenen
Modifizierer
private
protected
internal
protected internal (C#-Referenz)
04.11.2021 • 2 minutes to read

Die Schlüsselwortkombination protected internal ist ein Zugriffsmodifizierer für Member. Ein Member vom
Typ „protected internal“ kann von der aktuellen Assembly oder von Typen aus zugegriffen werden, die von der
enthaltenden Klasse abgeleitet werden. Einen Vergleich von protected internal mit den anderen
Zugriffsmodifizierern finden Sie unter Zugriffsebenen.

Beispiel
Ein Member vom Typ „protected internal“ einer Basisklasse kann von jedem Typ innerhalb seiner enthaltenden
Assembly aus zugegriffen werden. Sie kann auch von einer abgeleiteten Klasse zugegriffen werden, die sich in
einer anderen Assembly befindet, jedoch nur, wenn der Zugriff über eine Variable des abgeleiteten Klassentyps
erfolgt. Sehen Sie sich z.B. folgenden Codeabschnitt an:

// Assembly1.cs
// Compile with: /target:library
public class BaseClass
{
protected internal int myValue = 0;
}

class TestAccess
{
void Access()
{
var baseObject = new BaseClass();
baseObject.myValue = 5;
}
}

// Assembly2.cs
// Compile with: /reference:Assembly1.dll
class DerivedClass : BaseClass
{
static void Main()
{
var baseObject = new BaseClass();
var derivedObject = new DerivedClass();

// Error CS1540, because myValue can only be accessed by


// classes derived from BaseClass.
// baseObject.myValue = 10;

// OK, because this class derives from BaseClass.


derivedObject.myValue = 10;
}
}

Dieses Beispiel enthält zwei Dateien, Assembly1.cs und Assembly2.cs . Die erste Datei enthält eine öffentliche
Basisklasse, BaseClass , und eine weitere Klasse, TestAccess . BaseClass besitzt einen Member vom Typ
„protected internal“, myValue , auf den über den Typ TestAccess zugegriffen wird. In der zweiten Datei
verursacht ein Versuch, über eine BaseClass -Instanz auf myValue zuzugreifen, einen Fehler, während ein Zugriff
auf diesen Member über eine Instanz einer abgeleiteten Klasse DerivedClass gelingt.
Strukturmember können nicht vom Typ protected internal sein, da die Struktur nicht vererbt werden kann.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Zugriffsmodifizierer
Zugriffsebenen
Modifizierer
public
private
internal
Sicherheitsaspekte für interne virtuelle Schlüsselwörter
private protected (C#-Referenz)
04.11.2021 • 2 minutes to read

Die Schlüsselwortkombination private protected ist ein Zugriffsmodifizierer für Member. Ein Member vom Typ
„private protected“ kann von der von Typen aus zugegriffen werden, die von der enthaltenden Klasse abgeleitet
werden, jedoch nur innerhalb der enthaltenden Assembly. Einen Vergleich von private protected mit den
anderen Zugriffsmodifizierern finden Sie unter Zugriffsebenen.

NOTE
Der Zugriffsmodifizierer private protected ist in C# 7.2 und höher gültig.

Beispiel
Ein Member vom Typ „private protected“ einer Basisklasse kann nur dann von abgeleiteten Typen innerhalb
seiner enthaltenden Assembly aus zugegriffen werden, wenn der statische Typ der Variable der abgeleitete
Klassentyp ist. Sehen Sie sich z.B. folgenden Codeabschnitt an:

public class BaseClass


{
private protected int myValue = 0;
}

public class DerivedClass1 : BaseClass


{
void Access()
{
var baseObject = new BaseClass();

// Error CS1540, because myValue can only be accessed by


// classes derived from BaseClass.
// baseObject.myValue = 5;

// OK, accessed through the current derived class instance


myValue = 5;
}
}

// Assembly2.cs
// Compile with: /reference:Assembly1.dll
class DerivedClass2 : BaseClass
{
void Access()
{
// Error CS0122, because myValue can only be
// accessed by types in Assembly1
// myValue = 10;
}
}

Dieses Beispiel enthält zwei Dateien, Assembly1.cs und Assembly2.cs . Die erste Datei enthält eine öffentliche
Basisklasse, BaseClass , und einen davon abgeleiteten Typ, DerivedClass1 . BaseClass besitzt einen Member
vom Typ „private protected“, myValue , auf den DerivedClass1 auf zwei Arten zuzugreifen versucht. Der erste
Versuch, über eine Instanz von BaseClass auf myValue zuzugreifen, führt zu einem Fehler. Der Versuch, es als
geerbten Member in DerivedClass1 zu verwenden, gelingt jedoch.
In der zweiten Datei wird ein Versuch, auf myValue als geerbtes Mitglied von DerivedClass2 zuzugreifen, einen
Fehler erzeugen, da nur von abgeleiteten Typen in Assembly1 darauf zugegriffen werden kann.
Wenn Assembly1.csein InternalsVisibleToAttribute namens Assembly2 enthält, hat die abgeleitete Klasse
DerivedClass2 Zugriff auf private protected -Member, die in BaseClass deklariert sind. InternalsVisibleTo
macht private protected -Member für abgeleitete Klassen in anderen Assemblys sichtbar.
Strukturmember können nicht vom Typ private protected sein, da die Struktur nicht vererbt werden kann.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Zugriffsmodifizierer
Zugriffsebenen
Modifizierer
public
private
internal
Sicherheitsaspekte für interne virtuelle Schlüsselwörter
abstract (C#-Referenz)
04.11.2021 • 3 minutes to read

Der abstract -Modifizierer gibt an, dass dem modifizierten Objekt eine Implementierung fehlt oder dass diese
unvollständig ist. Der abstract-Modifizierer kann für Klassen, Methoden, Eigenschaften, Indexer und Ereignisse
verwendet werden. Verwenden Sie den abstract -Modifizierer in einer Klassendeklaration, um anzugeben, dass
die Klasse nur die Basisklasse für andere Klassen sein und nicht selbst instanziiert werden soll. Als abstrakt
markierte Member müssen von Klassen, die von nicht abstrakten Klassen abgeleitet wurden, implementiert
werden.

Beispiel 1
In diesem Beispiel muss die Klasse Square eine Implementierung von GetArea bereitstellen, da sie von Shape
abgeleitet ist:

abstract class Shape


{
public abstract int GetArea();
}

class Square : Shape


{
private int _side;

public Square(int n) => _side = n;

// GetArea method is required to avoid a compile-time error.


public override int GetArea() => _side * _side;

static void Main()


{
var sq = new Square(12);
Console.WriteLine($"Area of the square = {sq.GetArea()}");
}
}
// Output: Area of the square = 144

Abstrakte Klassen weisen die folgenden Funktionen auf:


Eine abstrakte Klasse darf nicht instanziiert werden.
Eine abstrakte Klasse enthält möglicherweise abstrakte Methode und Accessoren.
Eine abstrakte Klasse kann nicht mit dem sealed-Modifizierer geändert werden, da sich die beiden
Modifizierer gegenseitig ausschließen. Der sealed -Modifizierer verhindert das Vererben einer Klasse,
und der abstract -Modifizierer erfordert das Vererben einer Klasse.
Eine nicht abstrakte Klasse, die von einer abstrakten Klasse abgeleitet wurde, muss Implementierungen
aller geerbten abstrakten Methoden und Accessoren enthalten.
Verwenden Sie den abstract -Modifizierer in einer Methoden- oder Eigenschaftendeklaration, um anzugeben,
dass die Methode oder Eigenschaft keine Implementierung enthalten.
Abstrakte Methoden weisen die folgenden Funktionen auf:
Eine abstrakte Methode ist implizit eine virtuelle Methode.
Abstrakte Methodendeklarationen sind nur in abstrakten Klassen zulässig.
Es gibt keinen Methodenkörper, da eine abstrakte Methodendeklaration keine Implementierungen bietet;
die Methodendeklaration enden ganz einfach mit einem Semikolon; auf die Signatur folgen keine
geschweiften Klammern ({ }). Beispiel:

public abstract void MyMethod();

Die Implementierung wird von der Methode override zur Verfügung gestellt, die ein Member einer nicht
abstrakten Klasse ist.
Es ist unzulässig, die Modifizierer static oder virtual in einer abstrakten Methodendeklaration zu
verwenden.
Abstrakte Eigenschaften verhalten sich wie abstrakte Methoden – sie unterscheiden sich lediglich in der
Deklarations- und Aufrufsyntax.
Es ist ein unzulässig, den abstract -Modifizierer für eine statische Eigenschaft zu verwenden.
Eine abstrakte vererbte Eigenschaft kann in einer abgeleiteten Klasse mithilfe der
Eigenschaftendeklaration, die den Modifizierer override verwendet, außer Kraft gesetzt werden.
Weitere Informationen zu abstrakten Klassen finden Sie unter Abstrakte und versiegelte Klassen und
Klassenmember.
Eine abstrakte Klasse muss eine Implementierung für alle Schnittstellenmember bereitstellen.
Eine abstrakte Klasse, die eine Schnittstelle implementiert, ordnet die Schnittstellenmethoden möglicherweise
abstrakten Methoden zu. Zum Beispiel:

interface I
{
void M();
}

abstract class C : I
{
public abstract void M();
}

Beispiel 2
In diesem Beispiel wird die DerivedClass -Klasse von der abstrakten Klasse BaseClass abgeleitet. Die abstrakte
Klasse enthält eine abstrakte Methode, AbstractMethod , und zwei abstrakte Eigenschaften, X und Y .
// Abstract class
abstract class BaseClass
{
protected int _x = 100;
protected int _y = 150;

// Abstract method
public abstract void AbstractMethod();

// Abstract properties
public abstract int X { get; }
public abstract int Y { get; }
}

class DerivedClass : BaseClass


{
public override void AbstractMethod()
{
_x++;
_y++;
}

public override int X // overriding property


{
get
{
return _x + 10;
}
}

public override int Y // overriding property


{
get
{
return _y + 10;
}
}

static void Main()


{
var o = new DerivedClass();
o.AbstractMethod();
Console.WriteLine($"x = {o.X}, y = {o.Y}");
}
}
// Output: x = 111, y = 161

Wenn Sie beim vorherigen Beispiel versuchen, die abstrakte Klasse mithilfe des folgenden Anweisungsbeispiels
zu instanziieren:

BaseClass bc = new BaseClass(); // Error

Sie erhalten eine Fehlermeldung, dass der Compiler keine Instanz der abstrakten Klasse „BaseClass“ erstellen
kann.

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
Modifizierer
virtual
override
C#-Schlüsselwörter
async (C#-Referenz)
04.11.2021 • 4 minutes to read

Mit dem async -Modifizierer können Sie angeben, dass eine Methode, ein Lambdaausdruck oder eine anonyme
Methode asynchron ist. Wenn Sie diesen Modifizierer auf Methoden oder Ausdrücke anwenden, wird dies als
asynchrone Methode bezeichnet. Im folgenden Beispiel wird eine asynchrone Methode mit dem Namen
ExampleMethodAsync definiert:

public async Task<int> ExampleMethodAsync()


{
//...
}

Wenn Sie mit der asynchronen Programmierung noch nicht vertraut sind oder nicht wissen, wie eine Async-
Methode den await -Operator verwendet, um Aufgaben mit potenziell langer Laufzeit auszuführen, ohne den
Thread des Aufrufers zu blockieren, können Sie sich die Einführung unter Asynchrone Programmierung mit
async und await durchlesen. Der folgende Code befindet sich in einer asynchronen Methode und ruft die
HttpClient.GetStringAsync-Methode auf:

string contents = await httpClient.GetStringAsync(requestUrl);

Eine asynchrone Methode wird bis zum ersten await -Ausdruck synchron ausgeführt. Dann wird die Methode
angehalten, bis die erwartete Aufgabe abgeschlossen ist. In der Zwischenzeit wird die Steuerung an den
Aufrufer der Methode zurückgegeben, wie das Beispiel in nächsten Thema zeigt.
Wenn die Methode, die mit dem async -Schlüsselwort geändert wird, keinen await -Ausdruck oder keine
await-Anweisung enthält, wird die Methode synchron ausgeführt. Mit einer Compilerwarnung werden Sie auf
alle asynchronen Methoden hingewiesen, die keine await -Anweisungen enthalten, da dies möglicherweise auf
einen Fehler hindeutet. Siehe Compilerwarnung (Stufe 1) CS4014.
Das async -Schlüsselwort ist insofern kontextabhängig, dass es nur dann ein Schlüsselwort ist, wenn mit ihm
eine Methode, ein Lambda-Ausdruck oder eine anonyme Methode geändert wird. In allen anderen Kontexten
wird es als Bezeichner interpretiert.

Beispiel
Im folgenden Beispiel werden die Struktur und Ablaufsteuerung zwischen einem asynchronen Ereignishandler,
StartButton_Click , und einer asynchronen Methode, ExampleMethodAsync , veranschaulicht. Das Ergebnis der
asynchronen Methode ist die Anzahl von Zeichen einer Webseite. Der Code ist für eine Windows Presentation
Foundation (WPF)- oder Windows Store-Anwendung geeignet, die Sie in Visual Studio erstellen. Informationen
zum Einrichten der Anwendung finden Sie in den Codekommentaren.
Sie können diesen Code in Visual Studio als Windows Presentation Foundation (WPF)-App oder Windows Store-
App ausführen. Sie benötigen ein Schaltflächen-Steuerelement mit dem Namen StartButton und ein
Textfeldsteuerelement mit dem Namen ResultsTextBox . Denken Sie daran, die Namen und den Handler so
festzulegen, dass es in etwa wie folgt aussieht:
<Button Content="Button" HorizontalAlignment="Left" Margin="88,77,0,0" VerticalAlignment="Top" Width="75"
Click="StartButton_Click" Name="StartButton"/>
<TextBox HorizontalAlignment="Left" Height="137" Margin="88,140,0,0" TextWrapping="Wrap"
Text="&lt;Enter a URL&gt;" VerticalAlignment="Top" Width="310" Name="ResultsTextBox"/>

So führen Sie den Code als WPF-App aus:


Fügen Sie diesen Code in die MainWindow -Klasse in „MainWindow.xaml.cs“ ein.
Fügen Sie einen Verweis auf „System.Net.Http“ hinzu.
Fügen Sie eine using -Anweisung für „System.Net.Http“ hinzu.
So führen Sie den Code als Windows Store-App aus:
Fügen Sie diesen Code in die MainPage -Klasse in „MainPage.xaml.cs“ ein.
Fügen Sie using-Anweisungen für „System.Net.Http“ und „System.Threading.Tasks“ hinzu.

private async void StartButton_Click(object sender, RoutedEventArgs e)


{
// ExampleMethodAsync returns a Task<int>, which means that the method
// eventually produces an int result. However, ExampleMethodAsync returns
// the Task<int> value as soon as it reaches an await.
ResultsTextBox.Text += "\n";

try
{
int length = await ExampleMethodAsync();
// Note that you could put "await ExampleMethodAsync()" in the next line where
// "length" is, but due to when '+=' fetches the value of ResultsTextBox, you
// would not see the global side effect of ExampleMethodAsync setting the text.
ResultsTextBox.Text += String.Format("Length: {0:N0}\n", length);
}
catch (Exception)
{
// Process the exception if one occurs.
}
}

public async Task<int> ExampleMethodAsync()


{
var httpClient = new HttpClient();
int exampleInt = (await httpClient.GetStringAsync("http://msdn.microsoft.com")).Length;
ResultsTextBox.Text += "Preparing to finish ExampleMethodAsync.\n";
// After the following return statement, any method that's awaiting
// ExampleMethodAsync (in this case, StartButton_Click) can get the
// integer result.
return exampleInt;
}
// The example displays the following output:
// Preparing to finish ExampleMethodAsync.
// Length: 53292

IMPORTANT
Weitere Informationen zu Aufgaben und zum Code, der während des Wartens auf eine Aufgabe ausgeführt wird, finden
Sie unter Asynchrone Programmierung mit async und await. Ein vollständiges Konsolenbeispiel, das ähnliche Elemente
verwendet, finden Sie unter Mehrere asynchrone Aufgaben starten und nach Abschluss verarbeiten (C#).

Rückgabetypen
Eine asynchrone Methode kann folgende Rückgabetypen haben:
Task
Task<TResult>
void. Von den async void -Methoden wird außer für Code für Ereignishandler allgemein abgeraten, da
aufrufende Funktionen für diese Methoden await nicht verwenden können und einen anderen
Mechanismus implementieren müssen, um den erfolgreichen Abschluss oder Fehler zu melden.
Ab C# 7.0: jeder Typ, der über eine zugängliche GetAwaiter -Methode verfügt. Der Typ
System.Threading.Tasks.ValueTask<TResult> ist eine solche Implementierung. Er ist verfügbar, wenn Sie das
NuGet-Paket System.Threading.Tasks.Extensions hinzufügen.
Mit der asynchronen Methode können keine in-, ref- oder out-Parameter deklariert werden, und sie kann auch
keinen Verweisrückgabewert aufweisen, es können mit ihr jedoch Methoden aufgerufen werden, die solche
Parameter aufweisen.
Task<TResult> wird als Rückgabetyp einer Async-Methode angegeben, wenn mit der return-Anweisung der
Methode ein Operand vom Typ TResult angegeben wird. Task wird verwendet, falls kein sinnvoller Wert
zurückgegeben wird, wenn die Methode abgeschlossen ist. Das bedeutet, dass ein Aufruf der Methode einen
Task zurückgibt. Wenn der Task aber abgeschlossen ist, wird jeder await -Ausdruck, der auf den Task
wartet, als void ausgewertet.
Der Rückgabetyp void wird hauptsächlich zum Definieren von Ereignishandlern verwendet, die diesen
Rückgabetyp erfordern. Der Aufrufer einer Async-Methode, die void zurückgibt, kann auf ihn nicht warten und
keine Ausnahmen auffangen, die von der Methode ausgelöst werden.
Ab C# 7.0 wird ein anderer Typ zurückgegeben, üblicherweise ein Werttyp, der über eine GetAwaiter -Methode
verfügt, um Speicherzuteilungen in Codeabschnitten zu minimieren, die für die Leistung entscheidend sind.
Weitere Informationen und Beispiele finden Sie unter Asynchrone Rückgabetypen.

Siehe auch
AsyncStateMachineAttribute
await
Asynchrone Programmierung mit async und await
Verarbeiten asynchroner Aufgaben nach Abschluss
const (C#-Referenz)
04.11.2021 • 2 minutes to read

Sie verwenden das const -Schlüsselwort, um ein konstantes Feld oder eine konstante lokale Variable zu
deklarieren. Konstante Felder und lokale Felder sind keine Variablen und können daher nicht geändert werden.
Konstanten können Nummern, boolesche Werte, Zeichenfolgen oder ein NULL-Verweis sein. Erstellen Sie keine
Konstante, um Informationen darzustellen, bei denen Sie davon ausgehen, dass sie sich einmal ändern.
Verwenden Sie beispielsweise kein konstantes Feld, um den Preis einer Dienstleistung, einer
Produktversionsnummer oder den Markennamen eines Unternehmens zu speichern. Diese Werte können sich
im Laufe der Zeit ändern, und da Compiler Konstanten weitergeben, muss anderer Code, der mit Ihren
Bibliotheken kompiliert wird, neu kompiliert werden, damit die Änderungen sichtbar werden. Weitere
Informationen finden Sie auch unter dem readonly-Schlüsselwort. Beispiel:

const int X = 0;
public const double GravitationalConstant = 6.673e-11;
private const string ProductName = "Visual C#";

Ab C# 10 können interpolierte Zeichenfolgen Konstanten sein, wenn alle verwendeten Ausdrücke auch
konstante Zeichenfolgen sind. Dieses Feature kann den Code verbessern, der konstante Zeichenfolgen erstellt:

const string Language = "C#";


const string Platform = ".NET";
const string Version = "10.0";
const string FullProductName = $"{Platform} - Language: {Language} Version: {Version}";

Bemerkungen
Der Typ einer Konstantendeklaration gibt den Typ der Member an, die durch die Deklaration eingeführt werden.
Der Initialisierer einer konstanten lokalen Variable oder eines konstanten Felds muss ein konstanter Ausdruck
sein, der implizit in den Zieltyp konvertiert werden kann.
Ein konstanter Ausdruck ist ein Ausdruck, der während der Kompilierung vollständig ausgewertet werden kann.
Daher sind string und ein NULL-Verweis die einzig möglichen Werte für Verweistypkonstanten.
In der Konstantendeklaration können mehrere Konstanten deklariert werden, z. B.:

public const double X = 1.0, Y = 2.0, Z = 3.0;

Der static -Modifizierer ist in einer Konstantendeklaration nicht zulässig.


Eine Konstante kann wie folgt einen Teil eines konstanten Ausdrucks darstellen:

public const int C1 = 5;


public const int C2 = C1 + 100;
NOTE
Das readonly-Schlüsselwort unterscheidet sich vom const -Schlüsselwort. Ein const -Feld kann nur bei der Deklaration
des Felds initialisiert werden. Ein readonly -Feld kann entweder bei der Deklaration oder in einem Konstruktor initialisiert
werden. Daher können readonly -Felder abhängig vom verwendeten Konstruktor über unterschiedliche Werte verfügen.
Außerdem ist ein const -Feld eine Kompilierzeitkonstante, während ein readonly -Feld für Laufzeitkonstanten
verwendet werden kann, wie in der folgenden Codezeile:
public static readonly uint l1 = (uint)DateTime.Now.Ticks; .

Beispiele
public class ConstTest
{
class SampleClass
{
public int x;
public int y;
public const int C1 = 5;
public const int C2 = C1 + 5;

public SampleClass(int p1, int p2)


{
x = p1;
y = p2;
}
}

static void Main()


{
var mC = new SampleClass(11, 22);
Console.WriteLine($"x = {mC.x}, y = {mC.y}");
Console.WriteLine($"C1 = {SampleClass.C1}, C2 = {SampleClass.C2}");
}
}
/* Output
x = 11, y = 22
C1 = 5, C2 = 10
*/

In diesem Beispiel wird das Verwenden von Konstanten als lokale Variablen demonstriert.

public class SealedTest


{
static void Main()
{
const int C = 707;
Console.WriteLine($"My local constant = {C}");
}
}
// Output: My local constant = 707

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Modifizierer
readonly
event (C#-Referenz)
04.11.2021 • 2 minutes to read

Das event -Schlüsselwort wird verwendet, um ein Ereignis in einer Publisher-Klasse zu deklarieren.

Beispiel
Das folgende Beispiel zeigt das Deklarieren und Auslösen eines Ereignisses, das EventHandler als zugrunde
liegenden Delegattyp verwendet. Das vollständige Codebeispiel, das auch veranschaulicht, wie der generische
Delegattyp EventHandler<TEventArgs> verwendet, ein Ereignis abonniert und eine Ereignishandlermethode
erstellt wird, finden Sie unter Veröffentlichen von Ereignissen, die den .NET-Richtlinien entsprechen.

public class SampleEventArgs


{
public SampleEventArgs(string text) { Text = text; }
public string Text { get; } // readonly
}

public class Publisher


{
// Declare the delegate (if using non-generic pattern).
public delegate void SampleEventHandler(object sender, SampleEventArgs e);

// Declare the event.


public event SampleEventHandler SampleEvent;

// Wrap the event in a protected virtual method


// to enable derived classes to raise the event.
protected virtual void RaiseSampleEvent()
{
// Raise the event in a thread-safe manner using the ?. operator.
SampleEvent?.Invoke(this, new SampleEventArgs("Hello"));
}
}

Ereignisse sind eine besondere Art von Multicastdelegaten, die nur aus der Klasse oder Struktur, in der sie
deklariert sind (Publisher-Klasse), aufgerufen werden können. Wenn andere Klassen oder Strukturen das
Ereignis abonnieren, werden ihre Ereignishandlermethoden aufgerufen werden, wenn die Publisher-Klasse das
Ereignis auslöst. Weitere Informationen und Codebeispiele finden Sie unter Ereignisse und Delegaten.
Ereignisse können als public, private, protected, internal, protected internal oder private protected markiert
werden. Diese Zugriffsmodifizierer definieren, wie Benutzer der Klasse auf das Ereignis zugreifen können.
Weitere Informationen finden Sie unter Zugriffsmodifizierer.

Schlüsselwörter und Ereignisse


Die folgenden Schlüsselwörter gelten für Ereignisse.

W EIT ERE IN F O RM AT IO N EN F IN DEN SIE


SC H L ÜSSEL W O RT B ESC H REIB UN G UN T ER

static Stellt das Ereignis Aufrufern jederzeit Statische Klassen und statische
zur Verfügung, auch wenn keine Klassenmember
Instanz der Klasse vorhanden ist.
W EIT ERE IN F O RM AT IO N EN F IN DEN SIE
SC H L ÜSSEL W O RT B ESC H REIB UN G UN T ER

virtual Ermöglicht abgeleiteten Klassen, das Vererbung


Ereignisverhalten mithilfe des override-
Schlüsselworts zu überschreiben.

sealed Gibt an, dass für abgeleitete Klassen


„virtual“ nicht mehr gilt.

abstract Der Compiler wird keine add - und


remove -Ereignisaccessorblöcke
generieren und daher müssen
abgeleitete Klassen ihre eigene
Implementierung bereitstellen.

Ein Ereignis kann mithilfe des static-Schlüsselworts als statisches Ereignis deklariert werden. Dadurch steht das
Ereignis Aufrufern jederzeit zur Verfügung, auch wenn keine Instanz der Klasse vorhanden ist. Weitere
Informationen finden Sie unter Statische Klassen und statische Klassenmember.
Ein Ereignis kann mithilfe des virtual-Schlüsselworts als virtuelles Ereignis gekennzeichnet werden. Dies
ermöglicht abgeleiteten Klassen, das Ereignisverhalten mithilfe des override-Schlüsselworts zu überschreiben.
Weitere Informationen finden Sie unter Vererbung. Ein Ereignis, das ein virtuelles Ereignis überschreibt, kann
auch sealed sein, was angibt, dass für abgeleitete Klassen „virtual“ nicht mehr gilt. Schließlich kann ein Ereignis
als abstract deklariert werden, d.h., dass der Compiler die add - und remove -Ereignisaccessorblöcke nicht
generieren wird. Daher müssen abgeleitete Klassen ihre eigene Implementierung bereitstellen.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
add
remove
Modifizierer
Kombinieren von Delegaten (Multicastdelegaten)
extern (C#-Referenz)
04.11.2021 • 2 minutes to read

Der extern -Modifizierer wird verwendet, um eine extern implementierte Methode zu deklarieren. Der extern -
Modifizierer wird häufig mit dem DllImport -Attribut verwendet, wenn Sie nicht verwalteten Code mit Interop-
Diensten aufrufen. In diesem Fall muss die Methode auch als static deklariert werden, wie im folgenden
Beispiel gezeigt:

[DllImport("avifil32.dll")]
private static extern void AVIFileInit();

Das extern -Schlüsselwort kann ebenso einen externen Assemblyalias definieren. Dadurch wird es möglich, aus
einer einzigen Assembly heraus auf unterschiedliche Versionen derselben Komponente zu verweisen. Weitere
Informationen finden Sie unter extern-Alias.
Es ist ein Fehler, den abstract-Modifizierer und den extern -Modifizierer gleichzeitig auf demselben Member
anzuwenden. So bedeutet die Verwendung des extern -Modifizierers, dass die Methode außerhalb des C#-
Codes implementiert wird, während bei Verwendung des abstract -Modifizierers die
Methodenimplementierung nicht in der Klasse bereitgestellt wird.
Die Verwendung des extern-Schlüsselworts ist in C# eingeschränkter als in C++. Informationen zum
Vergleichen des C#-Schlüsselworts mit dem C++-Schlüsselwort finden Sie unter "Using extern to Specify
Linkage" in der C++-Sprachreferenz.

Beispiel 1
In diesem Beispiel empfängt das Programm eine Zeichenfolge vom Benutzer und zeigt sie in einem
Meldungsfeld an. Das Programm verwendet die MessageBox -Methode, die von der User32.dll-Bibliothek
importiert wurde.

//using System.Runtime.InteropServices;
class ExternTest
{
[DllImport("User32.dll", CharSet=CharSet.Unicode)]
public static extern int MessageBox(IntPtr h, string m, string c, int type);

static int Main()


{
string myString;
Console.Write("Enter your message: ");
myString = Console.ReadLine();
return MessageBox((IntPtr)0, myString, "My Message Box", 0);
}
}

Beispiel 2
Dieses Beispiel veranschaulicht ein C#-Programm, das eine C-Bibliothek aufruft (eine native DLL).
1. Erstellen Sie die folgende C-Datei mit dem Namen cmdll.c :
// cmdll.c
// Compile with: -LD
int __declspec(dllexport) SampleMethod(int i)
{
return i*10;
}

2. Öffnen Sie ein Visual Studio x64 (oder x32) Native Tools-Eingabeaufforderungsfenster im Visual Studio-
Installationsverzeichnis, und kompilieren Sie die cmdll.c -Datei, indem Sie in der Eingabeaufforderung cl
-LD cmdll.c eingeben.
3. Erstellen Sie im gleichen Verzeichnis die folgende C#-Datei mit dem Namen cm.cs :

// cm.cs
using System;
using System.Runtime.InteropServices;
public class MainClass
{
[DllImport("Cmdll.dll")]
public static extern int SampleMethod(int x);

static void Main()


{
Console.WriteLine("SampleMethod() returns {0}.", SampleMethod(5));
}
}

4. Öffnen Sie ein Visual Studio x64 (oder x32) Native Tools-Eingabeaufforderungsfenster im Visual Studio-
Installationsverzeichnis, und kompilieren Sie die cm.cs -Datei, indem Sie Folgendes eingeben:

csc cm.cs (für die x64-Eingabeaufforderung) oder csc -platform:x86 cm.cs (für die x32-
Eingabeaufforderung)

Dadurch wird die ausführbare Datei cm.exe erstellt.


5. cm.exe ausführen. Die SampleMethod -Methode übergibt den Wert 5 an die DLL-Datei, die den mit 10
multiplizierten Wert zurückgibt. Das Programm erzeugt die folgende Ausgabe:

SampleMethod() returns 50.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
System.Runtime.InteropServices.DllImportAttribute
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Modifizierer
in (generischer Modifizierer) (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort in gibt für generische Typparameter an, dass der Typparameter kontravariant ist. Sie
können das in -Schlüsselwort in generischen Schnittstellen und Delegaten verwenden.
Kontravarianz ermöglicht Ihnen die Verwendung eines weniger stark abgeleiteten Typs als der durch den
generischen Parameter angegebene. Dadurch wird eine implizite Konvertierung von Klassen berücksichtigt, die
kontravariante Schnittstellen und implizite Konvertierung von Delegattypen implementieren. Kovarianz und
Kontravarianz in generischen Typparametern werden für Verweistypen unterstützt, aber nicht für Werttypen.
Ein Typ kann nur als kontravariant in einer generischen Schnittstelle oder einem generischen Delegaten
deklariert werden, wenn er den Typ eines Methodenparameters und nicht eines Methodenrückgabetyps
definiert. Die Parameter In , ref und out müssen invariant sein. Sie dürfen also weder kovariant noch
kontravariant sein.
Mit einer Schnittstelle, die einen kontravarianten Typparameter hat, kann ihre Methode mehr abgeleitete Typen,
als durch den Typparameter der Schnittstelle angegeben, akzeptieren. Z.B. ist Typ T in der Schnittstelle
IComparer<T> kontravariant, und Sie können ein Objekt des IComparer<Person> -Typs an ein Objekt des
IComparer<Employee> -Typs zuweisen, ohne besondere Konvertierungsmethoden zu verwenden, wenn Employee
von Person erbt.
Ein kontravarianter Delegat kann einem anderen Delegaten desselben Typs zugewiesen werden, jedoch mit
einem weniger stark abgeleiteten generischen Typparameter.
Weitere Informationen finden Sie unter Kovarianz und Kontravarianz.

Kontravariante generische Schnittstelle


Im folgenden Beispiel wird gezeigt, wie Sie eine kontravariante generische Schnittstelle deklarieren, erweitern
und implementieren können. Es wird auch gezeigt, wie Sie die implizite Konvertierung für Klassen verwenden
können, die eine diese Schnittstelle implementieren können.

// Contravariant interface.
interface IContravariant<in A> { }

// Extending contravariant interface.


interface IExtContravariant<in A> : IContravariant<A> { }

// Implementing contravariant interface.


class Sample<A> : IContravariant<A> { }

class Program
{
static void Test()
{
IContravariant<Object> iobj = new Sample<Object>();
IContravariant<String> istr = new Sample<String>();

// You can assign iobj to istr because


// the IContravariant interface is contravariant.
istr = iobj;
}
}
Kontravarianter generischer Delegat
Das folgende Beispiel zeigt, wie Sie einen kontravarianten generischen Delegaten deklarieren, instanziieren und
aufrufen. Es zeigt außerdem, wie Sie einen Delegattyp implizit konvertieren können.

// Contravariant delegate.
public delegate void DContravariant<in A>(A argument);

// Methods that match the delegate signature.


public static void SampleControl(Control control)
{ }
public static void SampleButton(Button button)
{ }

public void Test()


{

// Instantiating the delegates with the methods.


DContravariant<Control> dControl = SampleControl;
DContravariant<Button> dButton = SampleButton;

// You can assign dControl to dButton


// because the DContravariant delegate is contravariant.
dButton = dControl;

// Invoke the delegate.


dButton(new Button());
}

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
out
Kovarianz und Kontravarianz
Modifizierer
new-Modifizierer (C#-Referenz)
04.11.2021 • 3 minutes to read

Wenn das Schlüsselwort new als Deklarationsmodifizierer verwendet wird, blendet es explizit einen von einer
Basisklasse geerbten Member aus. Wenn Sie einen geerbten Member ausblenden, ersetzt die abgeleitete
Version des Members die Basisklassenversion. Dabei wird davon ausgegangen, dass die Basisklassenversion des
Members sichtbar ist, da sie bereits ausgeblendet wäre, wenn sie als private oder in einigen Fällen als
internal markiert wäre. Obwohl Sie public - oder protected -Member ausblenden können, ohne dabei den
new -Modifizierer verwenden zu müssen, wird eine Compilerwarnung angezeigt. Wenn Sie new verwenden,
um einen Member explizit auszublenden, wird diese Warnung unterdrückt.
Sie können das Schlüsselwort new auch verwenden, um eine Instanz eines Typs zu erstellen oder als
Einschränkung eines generischen Typs.
Um einen geerbten Member auszublenden, deklarieren Sie ihn mit demselben Membernamen in der
abgeleiteten Klasse und ändern ihn mit dem new -Schlüsselwort. Zum Beispiel:

public class BaseC


{
public int x;
public void Invoke() { }
}
public class DerivedC : BaseC
{
new public void Invoke() { }
}

In diesem Beispiel wird BaseC.Invoke durch DerivedC.Invoke ausgeblendet. Der Vorgang wirkt sich nicht auf
das Feld x aus, da es nicht mit einem ähnlichen Namen ausgeblendet wird.
Das Ausblenden des Namens durch Vererbung nimmt eine der folgenden Formen an:
Im Allgemeinen blendet eine Konstante, ein Feld, eine Eigenschaft oder einen Typ, der in einer Klasse oder
Struktur eingegeben wird, alle Basisklassenmember aus, die den gleichen Namen haben. Es werden
bestimmte Fälle unterschieden. Wenn Sie beispielsweise ein neues Feld mit dem Namen N mit einem
nicht aufrufbaren Typ deklarieren, und ein Basistyp deklariert N als Methode, blendet das neue Feld die
Basisdeklaration nicht in der Aufrufsyntax aus. Weitere Informationen finden Sie im Abschnitt
Membersuchvorgänge der C#-Sprachspezifikation.
Durch eine Methode, die in eine Klasse oder Struktur eingeführt ist, werden Eigenschaften, Felder und
Typen mit dem gleichen Namen in der Basisklasse ausgeblendet. Alle Basisklassenmethoden mit der
gleichen Signatur werden ebenfalls ausgeblendet.
Durch einen Indexer, der in einer Klasse oder Struktur eingeführt ist, werden alle Basisklassenindexer mit
der gleichen Signatur ausgeblendet.
new und override dürfen nicht gleichzeitig auf denselben Member angewendet werden, da sich die
Bedeutungen der beiden Modifizierer gegenseitig ausschließen. Mit dem new -Modifizierer wird ein neuer
Member mit demselben Namen erstellt, und der ursprüngliche Member wird ausgeblendet. Der override -
Modifizierer verlängert die Implementierung für einen geerbten Member.
Wenn der new -Modifizierer in einer Deklaration verwendet wird, in der kein geerbter Member ausgeblendet
wird, wird eine Warnung generiert.
Beispiele
In diesem Beispiel wird von einer Basisklasse, BaseC , und einer abgeleiteten Klasse, DerivedC , der gleiche
Feldname x verwendet. Auf diese Weise wird der Wert des geerbten Felds ausgeblendet. Im Beispiel wird die
Verwendung des new -Modifizierers veranschaulicht. Außerdem wird gezeigt, wie mit den voll qualifizierten
Namen auf die ausgeblendeten Member der Basisklasse zugegriffen wird.

public class BaseC


{
public static int x = 55;
public static int y = 22;
}

public class DerivedC : BaseC


{
// Hide field 'x'.
new public static int x = 100;

static void Main()


{
// Display the new value of x:
Console.WriteLine(x);

// Display the hidden value of x:


Console.WriteLine(BaseC.x);

// Display the unhidden member y:


Console.WriteLine(y);
}
}
/*
Output:
100
55
22
*/

In diesem Beispiel blendet eine geschachtelte Klasse eine Klasse mit dem gleichen Namen in der Basisklasse aus.
Das Beispiel veranschaulicht, wie mit dem new -Modifizierer die Warnmeldung vermieden wird und wie mit den
voll qualifizierten Namen auf die ausgeblendeten Klassenmember zugegriffen wird.
public class BaseC
{
public class NestedC
{
public int x = 200;
public int y;
}
}

public class DerivedC : BaseC


{
// Nested type hiding the base type members.
new public class NestedC
{
public int x = 100;
public int y;
public int z;
}

static void Main()


{
// Creating an object from the overlapping class:
NestedC c1 = new NestedC();

// Creating an object from the hidden class:


BaseC.NestedC c2 = new BaseC.NestedC();

Console.WriteLine(c1.x);
Console.WriteLine(c2.x);
}
}
/*
Output:
100
200
*/

Wenn der new -Modifizierer entfernt wird, kann das Programm dennoch kompiliert und ausgeführt werden. Es
wird jedoch folgende Warnung angezeigt:

The keyword new is required on 'MyDerivedC.x' because it hides inherited member 'MyBaseC.x'.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Der new-Modifizierer der C#-Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Modifizierer
Versionsverwaltung mit den Schlüsselwörtern "override" und "new"
Wann müssen die Schlüsselwörter "override" und "new" verwendet werden?
out (generischer Modifizierer) (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort out gibt für generische Typparameter an, dass der Typparameter kovariant ist. Sie können
das out -Schlüsselwort in generischen Schnittstellen und Delegaten verwenden.
Kovarianz ermöglicht Ihnen die Verwendung eines stärker abgeleiteten Typs als durch den generischen
Parameter angegeben. Dadurch wird eine implizite Konvertierung von Klassen berücksichtigt, die kovariante
Schnittstellen und Konvertierung von Delegattypen implementiert. Kovarianz und Kontravarianz werden für
Verweistypen unterstützt, aber nicht für Werttypen.
Die Methoden einer Schnittstelle, die einen kovarianten Typparameter hat, können mehr abgeleitete Typen als
durch den Typparameter angegeben zurückgeben. Da z.B. in .NET Framework 4 Typ T in IEnumerable<T>
kovariant ist, können Sie ein Objekt des IEnumerable(Of String) -Typs an ein Objekt des IEnumerable(Of Object)
-Typs zuweisen, ohne besondere Konvertierungsmethoden zu verwenden.
Ein kovarianter Delegat kann einem anderen Delegaten desselben Typs zugewiesen werden, jedoch mit einem
stärker abgeleiteten generischen Typparameter.
Weitere Informationen finden Sie unter Kovarianz und Kontravarianz.

Beispiel: Kovariante generische Schnittstelle


Im folgenden Beispiel wird gezeigt, wie Sie eine kovariante generische Schnittstelle deklarieren, erweitern und
implementieren. Es wird auch gezeigt, wie eine implizite Konvertierung für Klassen verwendet wird, die eine
kovariante Schnittstelle implementieren.

// Covariant interface.
interface ICovariant<out R> { }

// Extending covariant interface.


interface IExtCovariant<out R> : ICovariant<R> { }

// Implementing covariant interface.


class Sample<R> : ICovariant<R> { }

class Program
{
static void Test()
{
ICovariant<Object> iobj = new Sample<Object>();
ICovariant<String> istr = new Sample<String>();

// You can assign istr to iobj because


// the ICovariant interface is covariant.
iobj = istr;
}
}

In einer generischen Schnittstelle kann ein Typparameter als kovariant deklariert werden, wenn er die folgenden
Bedingungen erfüllt:
Der Typparameter wird nur als Rückgabetyp von Schnittstellenmethoden, und nicht als Typ von
Methodenargumenten verwendet.
NOTE
Es gibt allerdings eine Ausnahme zu dieser Regel. Wenn Sie in einer kovarianten Schnittstelle einen
kontravarianten generischen Delegaten als Methodenparameter angegeben haben, können Sie den Typ als einen
generischen Typparameter für diesen Delegaten verwenden. Weitere Informationen über kovariante und
kontravariante generische Delegate finden Sie unter Varianz in Delegaten und Verwenden von Varianz für die
generischen Delegaten Func und Action.

Der Typparameter wird nicht als generische Einschränkung für die Schnittstellenmethoden verwendet.

Beispiel: Kovarianter generischer Delegat


Das folgende Beispiel zeigt, wie Sie einen kovarianten generischen Delegaten deklarieren, instanziieren und
aufrufen. Es wird gezeigt, wie Sie Delegattypen implizit konvertieren.

// Covariant delegate.
public delegate R DCovariant<out R>();

// Methods that match the delegate signature.


public static Control SampleControl()
{ return new Control(); }

public static Button SampleButton()


{ return new Button(); }

public void Test()


{
// Instantiate the delegates with the methods.
DCovariant<Control> dControl = SampleControl;
DCovariant<Button> dButton = SampleButton;

// You can assign dButton to dControl


// because the DCovariant delegate is covariant.
dControl = dButton;

// Invoke the delegate.


dControl();
}

In einem generischen Delegaten kann ein Typ als kovariant deklariert werden, wenn er nur als
Methodenrückgabetyp und nicht für Methodenargumente verwendet wird.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
Abweichungen bei generischen Schnittstellen
in
Modifizierer
override (C#-Referenz)
04.11.2021 • 2 minutes to read

Der override -Modifizierer wird benötigt, um die abstrakte oder virtuelle Implementierung einer geerbten
Methode, Eigenschaft, eines Indexers oder Ereignisses zu erweitern oder ändern.
Im folgenden Beispiel muss die Square -Klasse eine überschriebene Implementierung von GetArea
bereitstellen, weil GetArea von der abstrakten Klasse Shape geerbt wird:

abstract class Shape


{
public abstract int GetArea();
}

class Square : Shape


{
private int _side;

public Square(int n) => _side = n;

// GetArea method is required to avoid a compile-time error.


public override int GetArea() => _side * _side;

static void Main()


{
var sq = new Square(12);
Console.WriteLine($"Area of the square = {sq.GetArea()}");
}
}
// Output: Area of the square = 144

Eine override -Methode stellt eine neue Implementierung der Methode bereit, die von einer Basisklasse geerbt
wurde. Die Methode, die durch eine override -Deklaration überschrieben wird, wird als die überschriebene
Basismethode bezeichnet. Eine override -Methode muss dieselbe Signatur wie die überschriebene
Basismethode haben. Ab C# 9.0 unterstützen override -Methoden kovariante Rückgabetypen. Dies bedeutet,
dass der Rückgabetyp einer override -Methode vom Rückgabetyp der entsprechenden Basismethode abgeleitet
werden kann. In C# 8.0 und früher müssen die Rückgabetypen einer override -Methode und die der
überschriebenen Basismethode identisch sein.
Sie können keine nicht virtuelle oder statische Methode überschreiben. Die überschriebene Basismethode muss
virtual , abstract oder override sein.

Ein override -Deklaration kann nicht die Erreichbarkeit auf die virtual Methode ändern. Sowohl die Methode
override als auch virtual müssen den gleichen Zugriffsebenenmodifizierer besitzen.
Sie können die Modifizierer new , static oder virtual nicht verwenden, um eine override -Methode zu
ändern.
Eine überschreibende Eigenschaftsdeklaration muss genau denselben Zugriffsmodifizierer, Typ und Namen wie
die geerbte Eigenschaft angeben. Ab C# 9.0 unterstützen schreibgeschützte überschreibende Eigenschaften
kovariante Rückgabetypen. Die überschriebene Eigenschaft muss virtual , abstract oder override
entsprechen.
Weitere Informationen zur Verwendung des override -Schlüsselworts finden Sie unter Versionsverwaltung mit
den Schlüsselwörtern „override“ und „new“ und Wann müssen die Schlüsselwörter „override“ und „new“
verwendet werden?. Weitere Informationen zur Vererbung in C# finden Sie unter Vererbung.

Beispiel
In diesem Beispiel wird eine Basisklasse namens Employee und eine abgeleitete Klasse namens SalesEmployee
definiert. Die SalesEmployee -Klasse enthält ein zusätzliches Feld salesbonus , und überschreibt die
CalculatePay -Methode, um dies zu berücksichtigen.
class TestOverride
{
public class Employee
{
public string Name { get; }

// Basepay is defined as protected, so that it may be


// accessed only by this class and derived classes.
protected decimal _basepay;

// Constructor to set the name and basepay values.


public Employee(string name, decimal basepay)
{
Name = name;
_basepay = basepay;
}

// Declared virtual so it can be overridden.


public virtual decimal CalculatePay()
{
return _basepay;
}
}

// Derive a new class from Employee.


public class SalesEmployee : Employee
{
// New field that will affect the base pay.
private decimal _salesbonus;

// The constructor calls the base-class version, and


// initializes the salesbonus field.
public SalesEmployee(string name, decimal basepay, decimal salesbonus)
: base(name, basepay)
{
_salesbonus = salesbonus;
}

// Override the CalculatePay method


// to take bonus into account.
public override decimal CalculatePay()
{
return _basepay + _salesbonus;
}
}

static void Main()


{
// Create some new employees.
var employee1 = new SalesEmployee("Alice", 1000, 500);
var employee2 = new Employee("Bob", 1200);

Console.WriteLine($"Employee1 {employee1.Name} earned: {employee1.CalculatePay()}");


Console.WriteLine($"Employee2 {employee2.Name} earned: {employee2.CalculatePay()}");
}
}
/*
Output:
Employee1 Alice earned: 1500
Employee2 Bob earned: 1200
*/

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt override-Methoden der Sprachspezifikation für C#.
Weitere Informationen zu kovarianten Rückgabetypen finden Sie im Hinweis zum Featurevorschlag.

Siehe auch
C#-Referenz
Vererbung
C#-Schlüsselwörter
Modifizierer
abstract
virtual
new (Modifizierer)
Polymorphismus
readonly (C#-Referenz)
04.11.2021 • 3 minutes to read

Das Schlüsselwort readonly ist ein Modifizierer, der in vier Kontexten verwendet werden kann:
In einer Felddeklaration gibt readonly an, dass die Zuweisung zum Feld nur als Teil der Deklaration oder
in einem Konstruktor derselben Klasse erfolgen kann. Ein readonly-Feld kann innerhalb der
Felddeklaration und des Konstruktors mehrmals zugewiesen und neu zugewiesen werden.
Ein readonly -Feld kann nicht zugewiesen werden, sobald der Konstruktor vorhanden ist. Diese Regel hat
verschiedene Auswirkungen auf Wert- und Verweistypen:
Da Werttypen ihre Daten direkt enthalten, ist ein Feld des Werttyps readonly unveränderlich.
Da Verweistypen einen Verweis auf ihre Daten enthalten, muss ein Feld des Verweistyps readonly
immer auf das gleiche Objekt verweisen. Dieses Objekt ist nicht unveränderlich. Der readonly -
Modifizierer verhindert, dass das Feld durch eine andere Instanz des Verweistyps ersetzt wird. Der
Modifizierer verhindert jedoch nicht, dass die Instanzdaten des Felds durch das schreibgeschützte Feld
geändert werden.

WARNING
Ein extern sichtbarer Typ, der ein extern sichtbares schreibgeschütztes Feld enthält, bei dem es sich um einen
änderbaren Verweistyp handelt, kann ein Sicherheitsrisiko darstellen und die folgende Warnung auslösen: CA2104:
„Schreibgeschützte änderbare Verweistypen nicht deklarieren.“

In einer readonly struct -Typdefinition weist readonly darauf hin, dass der Strukturtyp unveränderlich
ist. Weitere Informationen finden Sie im Abschnitt zur readonly -Struktur des Artikels Strukturtypen.
In einer Instanzmemberdeklaration innerhalb eines Strukturtyps gibt readonly an, dass ein
Instanzmember den Zustand einer Struktur nicht ändert. Weitere Informationen finden Sie im Abschnitt
readonly -Instanzmember des Artikels Strukturtypen.

In einer ref readonly -Methodenrückgabe gibt der readonly -Modifizierer an, dass die Methode eine
Referenz zurückgibt, und Schreibvorgänge für diese Referenz nicht zulässig sind.
Die readonly struct - und ref readonly -Kontexte wurden in C# 7.2 hinzugefügt. readonly -Strukturmember
wurden in C# 8.0 hinzugefügt.

Beispiel für ein readonly-Feld


In diesem Beispiel kann der Wert des Felds year nicht zur Methode ChangeYear geändert werden, obwohl ihm
im Klassenkonstruktor ein Wert zugewiesen ist:
class Age
{
private readonly int _year;
Age(int year)
{
_year = year;
}
void ChangeYear()
{
//_year = 1967; // Compile error if uncommented.
}
}

Sie können einem readonly -Feld nur in den folgenden Kontexten einen Wert zuweisen:
Wenn die Variable in der Deklaration initialisiert ist, z.B.:

public readonly int y = 5;

In einem Instanzenkonstruktor der Klasse, die die Deklaration des Instanzfelds enthält.
Im statischen Konstruktor der Klasse, die die Deklaration des statischen Felds enthält.
Diese Konstruktorkontexte sind auch die einzigen Kontexte, in denen es zulässig ist, ein readonly -Feld als out-
oder ref-Parameter zu übergeben.

NOTE
Das Schlüsselwort readonly unterscheidet sich vom Schlüsselwort const. Ein const -Feld kann nur bei der Deklaration
des Felds initialisiert werden. Ein readonly -Feld kann mehrere Male in der Felddeklaration und in einem Konstruktor
zugewiesen werden. Daher können readonly -Felder abhängig vom verwendeten Konstruktor über unterschiedliche
Werte verfügen. Außerdem ist ein const -Feld eine Kompilierzeitkonstante, während ein readonly -Feld wie im
folgenden Beispiel für Laufzeitkonstanten verwendet werden kann:

public static readonly uint timeStamp = (uint)DateTime.Now.Ticks;


public class SamplePoint
{
public int x;
// Initialize a readonly field
public readonly int y = 25;
public readonly int z;

public SamplePoint()
{
// Initialize a readonly instance field
z = 24;
}

public SamplePoint(int p1, int p2, int p3)


{
x = p1;
y = p2;
z = p3;
}

public static void Main()


{
SamplePoint p1 = new SamplePoint(11, 21, 32); // OK
Console.WriteLine($"p1: x={p1.x}, y={p1.y}, z={p1.z}");
SamplePoint p2 = new SamplePoint();
p2.x = 55; // OK
Console.WriteLine($"p2: x={p2.x}, y={p2.y}, z={p2.z}");
}
/*
Output:
p1: x=11, y=21, z=32
p2: x=55, y=25, z=24
*/
}

Wenn Sie im vorherigen Beispiel eine Anweisung wie die folgende verwenden:

p2.y = 66; // Error

erhalten Sie die Compilerfehlermeldung:


Einem schreibgeschützten Feld kann nichts zugewiesen werden (außer in einem Konstruktor oder
Variableninitialisierer)

Rückgabebeispiel für ref readonly


Der readonly -Modifizierer für ref return gibt an, dass der zurückgegebene Verweis nicht geändert werden
kann. Das folgende Beispiel gibt einen Verweis auf den Ursprung zurück. Dabei wird über den readonly -
Modifizierer angegeben, dass die aufrufenden Funktionen den Ursprung nicht ändern können:

private static readonly SamplePoint s_origin = new SamplePoint(0, 0, 0);


public static ref readonly SamplePoint Origin => ref s_origin;

Der zurückgegebene Typ muss nicht readonly struct aufweisen. Jeder Typ, der von ref zurückgegeben
werden kann, kann auch von ref readonly zurückgegeben werden.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
Sehen Sie sich auch die Vorschläge zur Sprachspezifikation an:
readonly-Referenz und readonly-Struktur
readonly-Strukturmember

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Modifizierer
const
Felder
sealed (C#-Referenz)
04.11.2021 • 2 minutes to read

Der Modifizierer sealed verhindert, dass andere Klassen von einer Klasse erben, wenn er auf diese Klasse
angewendet wird. Im folgenden Beispiel erbt die Klasse B von der Klasse A , allerdings kann keine Klasse von
der Klasse B erben.

class A {}
sealed class B : A {}

Sie können den Modifizierer sealed auch auf eine Methode oder Eigenschaft anwenden, die eine virtuelle
Methode oder Eigenschaft in einer Basisklasse außer Kraft setzt. Dadurch können Sie zulassen, dass Klassen von
Ihrer Klasse abgeleitet werden, und verhindern, dass sie spezifische virtuelle Methoden oder Eigenschaften
außer Kraft setzen.

Beispiel
Im folgenden Beispiel erbt Z von Y , Z kann aber die virtuelle Funktion F nicht außer Kraft setzen, die in X
angegeben und in Y versiegelt ist.

class X
{
protected virtual void F() { Console.WriteLine("X.F"); }
protected virtual void F2() { Console.WriteLine("X.F2"); }
}

class Y : X
{
sealed protected override void F() { Console.WriteLine("Y.F"); }
protected override void F2() { Console.WriteLine("Y.F2"); }
}

class Z : Y
{
// Attempting to override F causes compiler error CS0239.
// protected override void F() { Console.WriteLine("Z.F"); }

// Overriding F2 is allowed.
protected override void F2() { Console.WriteLine("Z.F2"); }
}

Wenn Sie neue Methoden oder Eigenschaften in einer Klasse definieren, können Sie verhindern, dass ableitende
Klassen sie außer Kraft setzen, indem Sie sie nicht als virtual deklarieren.
Es wäre ein Fehler, den Modifizierer abstract mit einer versiegelten Klasse zu verwenden, da eine abstrakte
Klasse von einer Klasse geerbt werden muss, die eine Implementierung der abstrakten Methoden oder
Eigenschaften bereitstellt.
Der Modifizierer sealed muss immer mit override verwendet werden, wenn er auf eine Methode oder
Eigenschaft angewendet wird.
Da Strukturen implizit versiegelt sind, können sie nicht geerbt werden.
Weitere Informationen finden Sie unter Vererbung.
Weitere Beispiele finden Sie unter Abstrakte und versiegelte Klassen und Klassenmember.

sealed class SealedClass


{
public int x;
public int y;
}

class SealedTest2
{
static void Main()
{
var sc = new SealedClass();
sc.x = 110;
sc.y = 150;
Console.WriteLine($"x = {sc.x}, y = {sc.y}");
}
}
// Output: x = 110, y = 150

Im vorherigen Beispiel können Sie mithilfe der folgenden Anweisung versuchen, von der versiegelten Klasse zu
erben:
class MyDerivedC: SealedClass {} // Error

Daraus ergibt sich eine Fehlermeldung:


'MyDerivedC': cannot derive from sealed type 'SealedClass'

Hinweise
Sie sollten generell die folgenden zwei Punkte in Betracht ziehen, um festzustellen, ob Sie eine Klasse, Methode
oder Eigenschaft versiegeln sollten:
Die potentiellen Vorteile, die ableitende Klassen durch die Möglichkeit, Ihre Klasse anzupassen, erhalten
könnten
Die Möglichkeit, dass ableitende Klassen Ihre Klassen so ändern könnten, dass sie nicht mehr korrekt
oder wie erwartet funktionieren

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Statische Klassen und statische Klassenmember
Abstrakte und versiegelte Klassen und Klassenmember
Zugriffsmodifizierer
Modifizierer
override
virtual
static (C#-Referenz)
04.11.2021 • 3 minutes to read

Auf dieser Seite wird das Modifiziererschlüsselwort static behandelt. Das Schlüsselwort static ist auch Teil
der using static -Anweisung.
Verwenden Sie den Modifizierer static , um einen statischen Member zu deklarieren, der zum Typ selbst
gehört, anstatt zu einem bestimmten Objekt. Der static -Modifizierer kann zum Deklarieren von static -
Klassen verwendet werden. In Klassen, Schnittstellen und Strukturen können Sie den Modifizierer static zu
Feldern, Methoden, Eigenschaften, Operatoren, Ereignissen und Konstruktoren hinzufügen. Der static -
Modifizierer kann nicht mit Indexern oder Finalizern verwendet werden. Weitere Informationen finden Sie unter
Statische Klassen und statische Klassenmember.
Ab C# 8.0 können Sie den static -Modifizierer zu einer lokalen Funktion hinzufügen. Eine statische lokale
Funktion kann keine lokalen Variablen oder den Instanzzustand erfassen.
Ab C# 9.0 können Sie Lambdaausdrücken oder anonymen Methoden den static -Modifizierer hinzufügen. Eine
statische Lambda- oder anonyme Methode kann keine lokalen Variablen bzw. keinen Instanzzustand erfassen.

Beispiel: statische Klasse


Die folgende Klasse wird als static deklariert und enthält nur static -Methoden:

static class CompanyEmployee


{
public static void DoSomething() { /*...*/ }
public static void DoSomethingElse() { /*...*/ }
}

Eine Konstante oder Typdeklaration ist implizit ein static -Member. Ein static -Member kann nicht über eine
Instanz verwiesen werden. Stattdessen wird er über den Typnamen verwiesen. Betrachten Sie beispielsweise die
folgende Klasse:

public class MyBaseC


{
public struct MyStruct
{
public static int x = 100;
}
}

Verwenden Sie zum Verweisen auf einen static -Member x den vollqualifizierten Namen MyBaseC.MyStruct.x
, außer es kann auf den Member vom selben Geltungsbereich zugegriffen werden:

Console.WriteLine(MyBaseC.MyStruct.x);

Während die Instanz einer Klasse eine separate Kopie aller Instanzfelder der Klasse enthält, gibt es nur eine
Kopie von jedem static -Feld.
Es ist nicht möglich, this zum Verweis auf static -Methoden oder Eigenschaftenaccessoren zu verwenden.
Wenn das Schlüsselwort static auf eine Klasse angewandt wird, müssen alle Member der Klasse static sein.
Klassen, Schnittstellen und static -Klassen können static -Konstruktoren haben. Ein static -Konstruktor wird
irgendwann zwischen Programmstart und Klasseninstanziierung aufgerufen.

NOTE
Die Verwendung des Schlüsselworts static ist in C# eingeschränkter als in C++. Vergleiche mit dem C++-
Schlüsselwort finden Sie unter Speicherklassen (C++).

Stellen Sie sich zur Veranschaulichung von static -Membern eine Klasse vor, die einen Angestellten eines
Unternehmens darstellt. Es wird angenommen, dass die Klasse eine Methode zum Zählen von Angestellten
sowie ein Feld enthält, in dem die Anzahl von Angestellten gespeichert wird. Sowohl die Methode als auch das
Feld gehören nicht zu einer Instanz eines Angestellten. Stattdessen gehören diese zur Klasse der Angestellten als
Ganzes. Diese sollten als static -Member der Klasse deklariert werden.

Beispiel: statisches Feld und statische Methode


In diesem Beispiel wird der Name und die ID eines neuen Angestellten gelesen, der Angestelltenzähler wird um
1 erhöht, und die Informationen zum neuen Angestellten sowie die neue Anzahl von Angestellten wird
angezeigt. Das Programm liest die aktuelle Anzahl von Angestellten von der Tastatur.
public class Employee4
{
public string id;
public string name;

public Employee4()
{
}

public Employee4(string name, string id)


{
this.name = name;
this.id = id;
}

public static int employeeCounter;

public static int AddEmployee()


{
return ++employeeCounter;
}
}

class MainClass : Employee4


{
static void Main()
{
Console.Write("Enter the employee's name: ");
string name = Console.ReadLine();
Console.Write("Enter the employee's ID: ");
string id = Console.ReadLine();

// Create and configure the employee object.


Employee4 e = new Employee4(name, id);
Console.Write("Enter the current number of employees: ");
string n = Console.ReadLine();
Employee4.employeeCounter = Int32.Parse(n);
Employee4.AddEmployee();

// Display the new information.


Console.WriteLine($"Name: {e.name}");
Console.WriteLine($"ID: {e.id}");
Console.WriteLine($"New Number of Employees: {Employee4.employeeCounter}");
}
}
/*
Input:
Matthias Berndt
AF643G
15
*
Sample Output:
Enter the employee's name: Matthias Berndt
Enter the employee's ID: AF643G
Enter the current number of employees: 15
Name: Matthias Berndt
ID: AF643G
New Number of Employees: 16
*/

Beispiel: statische Initialisierung


Dieses Beispiel zeigt, dass Sie ein static -Feld durch Verwendung eines anderen static -Felds initialisieren
können, das noch nicht deklariert ist. Die Ergebnisse sind undefiniert, bis Sie dem Feld static explizit einen
Wert zuweisen.
class Test
{
static int x = y;
static int y = 5;

static void Main()


{
Console.WriteLine(Test.x);
Console.WriteLine(Test.y);

Test.x = 99;
Console.WriteLine(Test.x);
}
}
/*
Output:
0
5
99
*/

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Modifizierer
using static-Direktive
Statische Klassen und statische Klassenmember
unsafe (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort unsafe kennzeichnet einen unsicheren Kontext, der für alle Zeigeroperationen erforderlich
ist. Weitere Informationen finden Sie unter Unsicherer Code und Zeiger.
Sie können bei der Deklaration eines Typs oder Members den Modifizierer unsafe verwenden. Daraufhin wird
der gesamte Text des Typs oder Members als unsicherer Kontext angesehen. Hier sehen Sie eine Methode, die
mit dem Modifizierer unsafe deklariert wurde:

unsafe static void FastCopy(byte[] src, byte[] dst, int count)


{
// Unsafe context: can use pointers here.
}

Der unsichere Kontext erstreckt sich von der Parameterliste bis zum Ende der Methode, weshalb in der
Parameterliste auch Zeiger verwendet werden können:

unsafe static void FastCopy ( byte* ps, byte* pd, int count ) {...}

Sie können auch einen unsafe-Block verwenden, um die Verwendung von unsicherem Code in diesem Block zu
aktivieren. Beispiel:

unsafe
{
// Unsafe context: can use pointers here.
}

Um unsicheren Code kompilieren zu können, müssen Sie die Compileroption AllowUnsafeBlocks angeben.
Unsicherer Code kann nicht von der Common Language Runtime überprüft werden.

Beispiel
// compile with: -unsafe
class UnsafeTest
{
// Unsafe method: takes pointer to int.
unsafe static void SquarePtrParam(int* p)
{
*p *= *p;
}

unsafe static void Main()


{
int i = 5;
// Unsafe method: uses address-of operator (&).
SquarePtrParam(&i);
Console.WriteLine(i);
}
}
// Output: 25
C#-Sprachspezifikation
Weitere Informationen finden Sie unter Unsafe-Code in der C#-Sprachspezifikation. Die Sprachspezifikation ist
die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
fixed-Anweisung
Unsicherer Code und Zeiger
Puffer fester Größe
virtual (C#-Referenz)
04.11.2021 • 3 minutes to read

Das Schlüsselwort virtual wird zum Ändern einer Methoden-, Eigenschaften-, Indexer- oder
Ereignisdeklaration verwendet, und lässt zu, dass sie in einer abgeleiteten Klasse außer Kraft gesetzt werden.
Diese Methode kann z.B. von jeder Klasse, die sie erbt, überschrieben werden:

public virtual double Area()


{
return x * y;
}

Die Implementierung eines virtuellen Members kann durch einen overriding member (überschreibenden
Member) in einer abgeleiteten Klasse geändert werden. Weitere Informationen zur Verwendung des virtual -
Schlüsselworts finden Sie unter Versionsverwaltung mit den Schlüsselwörtern „override“ und „new“ und Wann
müssen die Schlüsselwörter „override“ und „new“ verwendet werden?.

Bemerkungen
Wenn eine virtuelle Methode aufgerufen wird, wird der Laufzeittyp des Objekts auf einen überschreibenden
Member überprüft. Der überschreibende Member in der abgeleitetsten Klasse (bei dem es sich um den
ursprünglichen Member handeln könnte) wird aufgerufen, wenn keine abgeleitete Klasse den Member außer
Kraft gesetzt hat.
Standardmäßig sind Methoden nicht virtuell. Sie können keine nicht virtuelle Methode überschreiben.
Sie können den Modifizierer virtual nicht mit den Modifizierern static , abstract , private oder override
verwenden. Im folgenden Beispiel wird eine virtuelle Methode gezeigt:
class MyBaseClass
{
// virtual auto-implemented property. Overrides can only
// provide specialized behavior if they implement get and set accessors.
public virtual string Name { get; set; }

// ordinary virtual property with backing field


private int _num;
public virtual int Number
{
get { return _num; }
set { _num = value; }
}
}

class MyDerivedClass : MyBaseClass


{
private string _name;

// Override auto-implemented property with ordinary property


// to provide specialized accessor behavior.
public override string Name
{
get
{
return _name;
}
set
{
if (!string.IsNullOrEmpty(value))
{
_name = value;
}
else
{
_name = "Unknown";
}
}
}
}

Virtuelle Eigenschaften verhalten sich wie virtuelle Methoden – sie unterscheiden sich lediglich in der
Deklarations- und Aufrufsyntax.
Es ist ein unzulässig, den virtual -Modifizierer für eine statische Eigenschaft zu verwenden.
Eine virtuelle vererbte Eigenschaft kann in einer abgeleiteten Klasse mithilfe der
Eigenschaftendeklaration, die den Modifizierer override verwendet, außer Kraft gesetzt werden.

Beispiel
In diesem Beispiel enthält die Klasse Shape die zwei Koordinaten x und y und die virtuelle Methode Area() .
Andere Formklassen, z.B. Circle , Cylinder und Sphere erben die Klasse Shape . Die Oberfläche wird für jede
Abbildung berechnet. Jede abgeleitete Klasse verfügt über ihre eigene Überschreibungsimplementierung von
Area() .

Beachten Sie, dass die geerbten Klassen Circle , Sphere und Cylinder alle Konstruktoren verwenden, die die
Basisklasse initialisieren, wie in der folgenden Deklaration gezeigt.

public Cylinder(double r, double h): base(r, h) {}


Das folgende Programm berechnet und zeigt den entsprechenden Bereich für jede Abbildung durch Aufruf der
entsprechenden Implementierung der Area() -Methode gemäß dem Objekt, das der Methode zugeordnet ist.

class TestClass
{
public class Shape
{
public const double PI = Math.PI;
protected double _x, _y;

public Shape()
{
}

public Shape(double x, double y)


{
_x = x;
_y = y;
}

public virtual double Area()


{
return _x * _y;
}
}

public class Circle : Shape


{
public Circle(double r) : base(r, 0)
{
}

public override double Area()


{
return PI * _x * _x;
}
}

public class Sphere : Shape


{
public Sphere(double r) : base(r, 0)
{
}

public override double Area()


{
return 4 * PI * _x * _x;
}
}

public class Cylinder : Shape


{
public Cylinder(double r, double h) : base(r, h)
{
}

public override double Area()


{
return 2 * PI * _x * _x + 2 * PI * _x * _y;
}
}

static void Main()


{
double r = 3.0, h = 5.0;
Shape c = new Circle(r);
Shape s = new Sphere(r);
Shape l = new Cylinder(r, h);
Shape l = new Cylinder(r, h);
// Display results.
Console.WriteLine("Area of Circle = {0:F2}", c.Area());
Console.WriteLine("Area of Sphere = {0:F2}", s.Area());
Console.WriteLine("Area of Cylinder = {0:F2}", l.Area());
}
}
/*
Output:
Area of Circle = 28.27
Area of Sphere = 113.10
Area of Cylinder = 150.80
*/

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
Polymorphismus
abstract
override
new (Modifizierer)
volatile (C#-Referenz)
04.11.2021 • 3 minutes to read

Das Schlüsselwort volatile gibt an, dass ein Feld von mehreren Threads geändert werden kann, die zur
gleichen Zeit ausgeführt werden. Der Compiler, das Runtimesystem und sogar die Hardware können aus
Leistungsgründen die Lese- und Schreibvorgänge in den Speicherorten neu anordnen. Felder, die als volatile
deklariert sind, sind von bestimmten Optimierungsarten ausgeschlossen. Es gibt keine Garantie für eine
einzelne Gesamtsortierung von volatile-Schreibvorgängen aus der Sicht aller ausgeführten Threads. Weitere
Informationen finden Sie in den Ausführungen zur Volatile-Klasse.

NOTE
In einem Multiprozessorsystem garantiert ein flüchtiger Lesevorgang nicht, dass der neueste von einem Prozessor in
diesen Speicherort geschriebene Wert erhalten wird. Ebenso garantiert ein flüchtiger Schreibvorgang nicht, dass der
geschriebene Wert für andere Prozessoren sofort sichtbar ist.

Das Schlüsselwort volatile kann auf Felder der folgenden Typen angewendet werden:
Verweistypen.
Zeigertypen (in unsicherem Kontext). Beachten Sie, dass der Zeiger selbst als „volatile“ deklariert sein kann,
das Objekt, auf das er zeigt aber nicht. Anders ausgedrückt können Sie keinen „Zeiger auf ‚volatile‘“
deklarieren.
Einfacher Typen wie sbyte , byte , short , ushort , int , uint , char , float und bool .
Ein enum -Typ mit einem der folgenden Basistypen: byte , sbyte , short , ushort , int oder uint .
Generische Typparameter, die als Verweistypen bekannt sind.
IntPtr und UIntPtr.
Andere Typen, einschließlich double und long , können nicht mit volatile markiert werden, da Lese- und
Schreibvorgänge in die Felder dieser Typen nicht unbedingt atomar sind. Um den Multithreadzugriff auf diese
Feldtypen zu schützen, verwenden Sie die Klassenmitglieder Interlocked oder schützen Sie den Zugriff mit der
Anweisung lock .
Das Schlüsselwort volatile kann nur auf Felder einer class oder struct angewendet werden. Lokale
Variablen können nicht als volatile deklariert werden.

Beispiel
Im folgenden Beispiel wird die Deklaration einer öffentlichen Feldvariable als volatile dargestellt.

class VolatileTest
{
public volatile int sharedStorage;

public void Test(int i)


{
sharedStorage = i;
}
}

Im folgenden Beispiel wird veranschaulicht, wie ein Hilfs- oder Arbeitsthread erstellt wird und für das Ausführen
der Verarbeitung parallel mit dem primären Thread verwendet werden kann. Weitere Informationen zum
Multithreading finden Sie unter Verwaltetes Threading.

public class Worker


{
// This method is called when the thread is started.
public void DoWork()
{
bool work = false;
while (!_shouldStop)
{
work = !work; // simulate some work
}
Console.WriteLine("Worker thread: terminating gracefully.");
}
public void RequestStop()
{
_shouldStop = true;
}
// Keyword volatile is used as a hint to the compiler that this data
// member is accessed by multiple threads.
private volatile bool _shouldStop;
}

public class WorkerThreadExample


{
public static void Main()
{
// Create the worker thread object. This does not start the thread.
Worker workerObject = new Worker();
Thread workerThread = new Thread(workerObject.DoWork);

// Start the worker thread.


workerThread.Start();
Console.WriteLine("Main thread: starting worker thread...");

// Loop until the worker thread activates.


while (!workerThread.IsAlive)
;

// Put the main thread to sleep for 500 milliseconds to


// allow the worker thread to do some work.
Thread.Sleep(500);

// Request that the worker thread stop itself.


workerObject.RequestStop();

// Use the Thread.Join method to block the current thread


// until the object's thread terminates.
workerThread.Join();
Console.WriteLine("Main thread: worker thread has terminated.");
}
// Sample output:
// Main thread: starting worker thread...
// Worker thread: terminating gracefully.
// Main thread: worker thread has terminated.
}

Wenn der volatile -Modifizierer der Deklaration von _shouldStop hinzugefügt wird, erhalten Sie immer die
gleichen Ergebnisse (ähnlich dem Auszug aus dem vorhergehenden Code). Ohne diesen Modifizierer für das
_shouldStop -Mitglied ist das Verhalten unvorhersehbar. Die DoWork -Methode kann den Mitgliederzugriff
optimieren, was zum Lesen veralteter Daten führt. Aufgrund der Natur der Multithreadprogrammierung ist die
Anzahl der veraltete Lesevorgänge unvorhersehbar. Verschiedene Programmläufe führen zu etwas
unterschiedlichen Ergebnissen.
C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Sprachspezifikation: Schlüsselwort „volatile“
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Modifizierer
lock-Anweisung
Interlocked
Anweisungsschlüsselwörter (C#-Referenz)
04.11.2021 • 2 minutes to read

Anweisungen sind Instruktionen des Programms. Sie werden nacheinander ausgeführt, mit Ausnahme der
Anweisungen, auf die in der folgenden Tabelle verwiesen wird. In der folgenden Tabelle sind die C#-
Anweisungsschlüsselwörter aufgeführt. Weitere Informationen zu Anweisungen, die durch kein Schlüsselwort
ausgedrückt werden, finden Sie unter Anweisungen.

C AT EGO RY C #- SC H L ÜSSEL W Ö RT ER

Auswahlanweisungen if , switch

Iterationsanweisungen do , for , foreach , while

Sprunganweisungen break, continue, goto, return, yield

Ausnahmebehandlungsanweisungen throw, try-catch, try-finally, try-catch-finally

Checked und unchecked checked, unchecked

fixed-Anweisung fixed

lock-Anweisung lock

Siehe auch
C#-Referenz
Anweisungen
C#-Schlüsselwörter
break (C#-Referenz)
04.11.2021 • 3 minutes to read

Die break -Anweisung beendet die Ausführung der nächsten einschließenden Schleife oder switch -Anweisung,
in der sie angezeigt wird. Das Steuerelement wird an die Anweisung übergeben, die auf die beendete Anweisung
folgt, falls vorhanden.

Beispiel 1
In diesem Beispiel enthält die Bedingungsanweisung einen Indikator, der normalerweise zum Zählen von 1 bis
100 verwendet wird. Allerdings beendet die break -Anweisung die Schleife nach 4 Durchgängen.

class BreakTest
{
static void Main()
{
for (int i = 1; i <= 100; i++)
{
if (i == 5)
{
break;
}
Console.WriteLine(i);
}

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/*
Output:
1
2
3
4
*/

Beispiel 2
In diesem Beispiel wird die Verwendung von break in einer switch -Anweisung veranschaulicht.
class Switch
{
static void Main()
{
Console.Write("Enter your selection (1, 2, or 3): ");
string s = Console.ReadLine();
int n = Int32.Parse(s);

switch (n)
{
case 1:
Console.WriteLine("Current value is 1");
break;
case 2:
Console.WriteLine("Current value is 2");
break;
case 3:
Console.WriteLine("Current value is 3");
break;
default:
Console.WriteLine("Sorry, invalid selection.");
break;
}

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/*
Sample Input: 1

Sample Output:
Enter your selection (1, 2, or 3): 1
Current value is 1
*/

Bei Eingabe von 4 sähe die Ausgabe wie folgt aus:

Enter your selection (1, 2, or 3): 4


Sorry, invalid selection.

Beispiel 3
In diesem Beispiel wird die break -Anweisung verwendet, um aus einer inneren geschachtelten Schleife
auszubrechen und die Steuerung an die äußere Schleife zurückzugeben. Die Steuerung wird nur auf der
nächsthöheren Ebene in den geschachtelten Schleifen zurückgegeben.
class BreakInNestedLoops
{
static void Main(string[] args)
{

int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
char[] letters = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j' };

// Outer loop.
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine($"num = {numbers[i]}");

// Inner loop.
for (int j = 0; j < letters.Length; j++)
{
if (j == i)
{
// Return control to outer loop.
break;
}
Console.Write($" {letters[j]} ");
}
Console.WriteLine();
}

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

/*
* Output:
num = 0

num = 1
a
num = 2
a b
num = 3
a b c
num = 4
a b c d
num = 5
a b c d e
num = 6
a b c d e f
num = 7
a b c d e f g
num = 8
a b c d e f g h
num = 9
a b c d e f g h i
*/

Beispiel 4
In diesem Beispiel wird die break -Anweisung nur verwendet, um während jeder Iteration der Schleife aus dem
aktuellen Branch auszubrechen. Die Schleife selbst ist nicht von den Instanzen von break betroffen, die zur
geschachtelten switch -Anweisung gehören.
class BreakFromSwitchInsideLoop
{
static void Main(string[] args)
{
// loop 1 to 3
for (int i = 1; i <= 3; i++)
{
switch(i)
{
case 1:
Console.WriteLine("Current value is 1");
break;
case 2:
Console.WriteLine("Current value is 2");
break;
case 3:
Console.WriteLine("Current value is 3");
break;
default:
Console.WriteLine("This shouldn't happen.");
break;
}
}

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}

/*
* Output:
Current value is 1
Current value is 2
Current value is 3
*/

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
switch -Anweisung
continue (C#-Referenz)
04.11.2021 • 2 minutes to read

Die continue -Anweisung übergibt die Steuerung an die nächste Iteration der einschließenden
Iterationsanweisung, in der sie auftritt.

Beispiel
In diesem Beispiel wird ein Zähler initialisiert, um von 1 bis 10 zu zählen. Indem die continue -Anweisung in
Verbindung mit dem (i < 9) -Ausdruck verwendet wird, werden die Anweisungen zwischen continue und
dem Ende des for -Teils in Iterationen übersprungen, in denen i weniger als 9 beträgt. In den letzten beiden
Iterationen der for -Schleife, für die i == 9 und i == 10 gilt, wird die continue -Anweisung nicht ausgeführt,
und der Wert von i wird in der Konsole zurückgegeben.

class ContinueTest
{
static void Main()
{
for (int i = 1; i <= 10; i++)
{
if (i < 9)
{
continue;
}
Console.WriteLine(i);
}

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/*
Output:
9
10
*/

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
break-Anweisung
goto (C#-Referenz)
04.11.2021 • 2 minutes to read

Die goto -Anweisung überträgt die Programmsteuerung direkt an eine Anweisung mit Bezeichnung.
goto wird häufig dazu verwendet, eine bestimmte Parameterbezeichnung oder die Standardbezeichnung in
einer switch -Anweisung zu steuern.
Mit der goto -Anweisung können Sie auch tief geschachtelte Schleifen verlassen.

Beispiel 1
Im folgenden Beispiel wird die Verwendung von goto in einer switch -Anweisung veranschaulicht.

class SwitchTest
{
static void Main()
{
Console.WriteLine("Coffee sizes: 1=Small 2=Medium 3=Large");
Console.Write("Please enter your selection: ");
string s = Console.ReadLine();
int n = int.Parse(s);
int cost = 0;
switch (n)
{
case 1:
cost += 25;
break;
case 2:
cost += 25;
goto case 1;
case 3:
cost += 50;
goto case 1;
default:
Console.WriteLine("Invalid selection.");
break;
}
if (cost != 0)
{
Console.WriteLine($"Please insert {cost} cents.");
}
Console.WriteLine("Thank you for your business.");

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/*
Sample Input: 2

Sample Output:
Coffee sizes: 1=Small 2=Medium 3=Large
Please enter your selection: 2
Please insert 50 cents.
Thank you for your business.
*/
Beispiel 2
Im folgenden Beispiel wird die Verwendung von goto zum Verlassen geschachtelter Schleifen veranschaulicht.

public class GotoTest1


{
static void Main()
{
int x = 200, y = 4;
int count = 0;
string[,] array = new string[x, y];

// Initialize the array.


for (int i = 0; i < x; i++)

for (int j = 0; j < y; j++)


array[i, j] = (++count).ToString();

// Read input.
Console.Write("Enter the number to search for: ");

// Input a string.
string myNumber = Console.ReadLine();

// Search.
for (int i = 0; i < x; i++)
{
for (int j = 0; j < y; j++)
{
if (array[i, j].Equals(myNumber))
{
goto Found;
}
}
}

Console.WriteLine($"The number {myNumber} was not found.");


goto Finish;

Found:
Console.WriteLine($"The number {myNumber} is found.");

Finish:
Console.WriteLine("End of search.");

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/*
Sample Input: 44

Sample Output
Enter the number to search for: 44
The number 44 is found.
End of search.
*/

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
goto-Anweisung (C++)
return (C#-Referenz)
04.11.2021 • 2 minutes to read

Die Anweisung return beendet die Ausführung der Methode, in der sie angezeigt wird, und gibt das
Steuerelement an die aufrufende Methode zurück. Zudem kann ein optionaler Wert zurückgegeben werden.
Wenn es sich bei der Methode um einen void -Typ handelt, kann die Anweisung return ausgelassen werden.
Wenn sich die „return“-Anweisung in einem try -Block befindet, wird der finally -Block, falls vorhanden,
ausgeführt bevor das Steuerelement an die aufrufende Methode zurückgegeben wird.

Beispiel
Im folgenden Beispiel gibt die Methode CalculateArea() die lokale Variable area als double -Wert zurück.

class ReturnTest
{
static double CalculateArea(int r)
{
double area = r * r * Math.PI;
return area;
}

static void Main()


{
int radius = 5;
double result = CalculateArea(radius);
Console.WriteLine("The area is {0:0.00}", result);

// Keep the console open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
// Output: The area is 78.54

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
return-Anweisung
throw (C#-Referenz)
04.11.2021 • 3 minutes to read

Signalisiert das Auftreten einer Ausnahme während der Programmausführung.

Bemerkungen
Die Syntax von throw lautet:

throw [e];

Wo eeine Instanz einer Klasse ist, die von System.Exception abgeleitet wird. Im folgenden Beispiel wird die
throw -Anweisung verwendet, um eine IndexOutOfRangeException auszulösen, wenn das Argument, das an
eine Methode mit dem Namen GetNumber übergeben wurde, nicht auf einen gültigen Index eines internen
Arrays reagiert.

using System;

namespace Throw2
{
public class NumberGenerator
{
int[] numbers = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };

public int GetNumber(int index)


{
if (index < 0 || index >= numbers.Length)
{
throw new IndexOutOfRangeException();
}
return numbers[index];
}
}

Methodenaufrufer verwenden anschließend einen try-catch - oder try-catch-finally -Block, um die


ausgelöste Ausnahme zu behandeln. Im folgenden Beispiel wird die Ausnahme behandelt, die von der Methode
GetNumber ausgelöst wurde.
using System;

public class Example


{
public static void Main()
{
var gen = new NumberGenerator();
int index = 10;
try
{
int value = gen.GetNumber(index);
Console.WriteLine($"Retrieved {value}");
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine($"{e.GetType().Name}: {index} is outside the bounds of the array");
}
}
}
// The example displays the following output:
// IndexOutOfRangeException: 10 is outside the bounds of the array

Erneutes Auslösen einer Ausnahme


throw kann auch in einem catch -Block verwendet werden, um eine Ausnahme erneut auszulösen, die in
einem catch -Block behandelt wurde. In diesem Fall verwendet throw keinen Operanden für die Ausnahme. Es
ist besonders hilfreich, wenn eine Methode ein Argument in einer aufrufenden Funktion an eine andere
Bibliotheksmethode übergibt, und die Bibliotheksmethode eine Ausnahme auslöst, die an den Aufrufer
übergeben werden muss. Im folgenden Beispiel wird z.B. eine NullReferenceException erneut ausgelöst, die beim
Versuch, das erste Zeichen einer deinitialisierten Zeichenfolge abzurufen, ausgelöst wird.
using System;

namespace Throw
{
public class Sentence
{
public Sentence(string s)
{
Value = s;
}

public string Value { get; set; }

public char GetFirstCharacter()


{
try
{
return Value[0];
}
catch (NullReferenceException e)
{
throw;
}
}
}

public class Example


{
public static void Main()
{
var s = new Sentence(null);
Console.WriteLine($"The first character is {s.GetFirstCharacter()}");
}
}
// The example displays the following output:
// Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an
object.
// at Sentence.GetFirstCharacter()
// at Example.Main()

IMPORTANT
Sie können auch die throw e -Syntax in einem catch -Block verwenden, um eine neue Ausnahme zu instanziieren, die
Sie an den Aufrufer übergeben. In diesem Fall wird die Stapelüberwachung der ursprünglichen Ausnahme, die von der
Eigenschaft StackTrace abrufbar ist, nicht beibehalten.

Der throw -Ausdruck


Ab C# 7.0 kann throw sowohl als Ausdruck als auch als Anweisung verwendet werden. Dadurch wird eine
Ausnahme in Kontexten ausgelöst, die zuvor nicht unterstützt wurden. Dazu zählen unter anderem folgende
Einstellungen:
Der bedingte Operator. Im folgenden Beispiel wird ein throw -Ausdruck verwendet, um eine
ArgumentException auszulösen, wenn eine Methode an ein leeres Zeichenfolgenarray übergeben wird.
Diese Logik müsste vor C# 7.0 in einer if / else -Anweisung erscheinen.
private static void DisplayFirstNumber(string[] args)
{
string arg = args.Length >= 1 ? args[0] :
throw new ArgumentException("You must supply an argument");
if (Int64.TryParse(arg, out var number))
Console.WriteLine($"You entered {number:F0}");
else
Console.WriteLine($"{arg} is not a number.");
}

Der NULL-Sammeloperator. Im folgenden Beispiel wird eine throw -Anweisung mit einem NULL-
Sammeloperator verwendet, um eine Ausnahme auszulösen, wenn die Zeichenfolge, die einer
Eigenschaft Name zugewiesen wurde, null ist.

public string Name


{
get => name;
set => name = value ??
throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
}

Ein Ausdruckskörperlambda oder eine Ausdruckskörpermethode. Das folgende Beispiel veranschaulicht


eine Ausdruckskörpermethode, die eine InvalidCastException auslöst, da eine Konvertierung in einen
DateTime-Wert nicht unterstützt wird.

DateTime ToDateTime(IFormatProvider provider) =>


throw new InvalidCastException("Conversion to a DateTime is not supported.");

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
try-catch
C#-Schlüsselwörter
How to: Explizites Auslösen von Ausnahmen
try-catch (C#-Referenz)
04.11.2021 • 9 minutes to read

Die try-catch-Anweisung besteht aus einem try -Block gefolgt von einer oder mehreren catch -Klauseln, die
Handler für verschiedene Ausnahmen angeben.
Wenn eine Ausnahme ausgelöst wird, sucht die Common Language Runtime (CLR) nach der catch -Anweisung,
die diese Ausnahme behandelt. Wenn die derzeit ausgeführte Methode keinen solchen catch -Block enthält,
betrachtet die CLR die Methode, die die aktuelle Methode aufgerufen hat, dann die vorhergehende in der
Aufrufliste usw. Wenn kein catch -Block gefunden wird, zeigt die CLR dem Benutzer eine Meldung über eine
nicht behandelte Ausnahme an und beendet die Ausführung des Programms.
Der try -Block enthält den überwachten Code, der möglicherweise die Ausnahme verursacht. Der Block wird
ausgeführt, bis eine Ausnahme ausgelöst wird, oder bis er erfolgreich abgeschlossen wird. Beispielsweise löst
der folgende Versuch, ein null -Objekt umzuwandeln, die NullReferenceException-Ausnahme aus:

object o2 = null;
try
{
int i2 = (int)o2; // Error
}

Zwar kann die catch -Klausel ohne Argumente verwendet werden, um jeden beliebigen Ausnahmetyp
abfangen, dies wird jedoch nicht empfohlen. Im Allgemeinen sollten Sie nur solche Ausnahmen abfangen, bei
denen Sie wissen, wie die Wiederherstellung durchgeführt wird. Daher sollten Sie immer ein von
System.Exception abgeleitetes Objektargument angeben. Der Ausnahmetyp sollte so spezifisch wie möglich
sein, um zu vermeiden, dass fälschlicherweise Ausnahmen akzeptiert werden, die ihr Ausnahmehandler
tatsächlich nicht auflösen kann. Bevorzugen Sie daher konkrete Ausnahmen gegenüber dem Exception -
Basistyp. Beispiel:

catch (InvalidCastException e)
{
// recover from exception
}

Es ist möglich, mehrere spezifische catch -Klauseln in derselben try-catch-Anweisung zu verwenden. In diesem
Fall ist die Reihenfolge der catch -Klauseln wichtig, da die catch -Klauseln nacheinander überprüft werden.
Fangen Sie spezifischere Ausnahmen vor den weniger spezifischen ab. Der Compiler erzeugt einen Fehler, wenn
Sie Ihre catch-Blöcke so anordnen, dass ein neuerer Block nie erreicht werden kann.
Die Verwendung von catch -Argumenten ist eine Möglichkeit zum Filtern der Ausnahmen, die Sie behandeln
möchten. Sie können auch einen Ausnahmefilter verwenden, der die Ausnahme weiter untersucht, um zu
entscheiden, ob Sie sie behandeln möchten. Wenn der Ausnahmefilter „FALSE“ zurückgibt, wird die Suche nach
einem Ausnahmehandler fortgesetzt.

catch (ArgumentException e) when (e.ParamName == "…")


{
// recover from exception
}
Ausnahmefilter sind dem Abfangen und erneuten Auslösen vorzuziehen (siehe nachfolgende Erläuterung), da
der Filter den Stapel nicht beschädigt. Wenn ein späterer Handler den Stapel löscht, können Sie feststellen, wo
die Ausnahme ursprünglich herkam, und nicht nur die letzte Stelle, an der sie erneut ausgelöst wurde.
Filterausdrücke für Ausnahmen werden häufig zu Protokollierungszwecken eingesetzt. Sie können einen Filter
erstellen, der immer FALSE zurückgibt und außerdem Ausgaben in ein Protokoll schreibt, und Sie können
Ausnahmen protokollieren, wenn sie auftreten, ohne sie zu behandeln und erneut auszulösen.
Eine throw catch -Anweisung kann in einem -Block verwendet werden, um die von der catch -Anweisung
abgefangene Ausnahme erneut auszulösen. Im folgenden Beispiel werden Quellinformationen aus einer
IOException-Ausnahme extrahiert, anschließend wird die Ausnahme in der übergeordneten Methode ausgelöst.

catch (FileNotFoundException e)
{
// FileNotFoundExceptions are handled here.
}
catch (IOException e)
{
// Extract some information from this exception, and then
// throw it to the parent method.
if (e.Source != null)
Console.WriteLine("IOException source: {0}", e.Source);
throw;
}

Sie können eine Ausnahme abfangen und eine andere Ausnahme auslösen. Wenn Sie dies tun, geben Sie die
abgefangene Ausnahme als innere Ausnahme an, wie im folgenden Beispiel gezeigt.

catch (InvalidCastException e)
{
// Perform some action here, and then throw a new exception.
throw new YourCustomException("Put your error message here.", e);
}

Sie können eine Ausnahme auch erneut auslösen, wenn eine angegebene Bedingung erfüllt ist, wie im
folgenden Beispiel gezeigt.

catch (InvalidCastException e)
{
if (e.Data == null)
{
throw;
}
else
{
// Take some action.
}
}
NOTE
Es ist auch möglich, einen Ausnahmefilter zu verwenden, um ein ähnliches Ergebnis auf eine meist übersichtlichere Weise
zu erhalten (sowie ohne den Stapel zu bearbeiten, wie weiter oben in diesem Artikel erläutert wurde). Das folgende
Beispiel verfügt über das gleiche Verhalten für Aufrufer wie das vorherige Beispiel. Die Funktion gibt
InvalidCastException an den Aufrufer zurück, wenn e.Data``null ist.

catch (InvalidCastException e) when (e.Data != null)


{
// Take some action.
}

Initialisieren Sie innerhalb eines try -Blocks nur Variablen, die auch in diesem deklariert sind. Andernfalls kann
eine Ausnahme auftreten, bevor die Ausführung des Blocks abgeschlossen ist. Beispiel: Im folgenden
Codebeispiel wird die n -Variable innerhalb des try -Blocks initialisiert. Beim Versuch, diese Variable außerhalb
des try -Blocks in der Write(n) -Anweisung zu verwenden, wird ein Compilerfehler generiert.

static void Main()


{
int n;
try
{
// Do not initialize this variable here.
n = 123;
}
catch
{
}
// Error: Use of unassigned local variable 'n'.
Console.Write(n);
}

Weitere Informationen zu „catch“ finden Sie unter try-catch-finally.

Ausnahmen in Async-Methoden
Eine asynchrone Methode wird mit einem async-Modifizierer gekennzeichnet und enthält in der Regel eine oder
mehrere await-Ausdrücke oder -Anweisungen. Ein „await“-Ausdruck wendet den await-Operator auf ein Task
oder Task<TResult> an.
Wenn ein await -Ausdruck in der asynchchronen Methode erreicht wird, wird die Ausführung der Methode
angehalten, bis die erwartete Aufgabe abgeschlossen ist. Wenn die Aufgabe abgeschlossen ist, kann die
Ausführung in der Methode fortgesetzt werden. Weitere Informationen finden Sie unter Asynchrone
Programmierung mit async und await.
Die abgeschlossene Aufgabe, auf die await angewendet wird, kann sich aufgrund einer unbehandelten
Ausnahme in der Methode, die die Aufgabe zurückgibt, in einem fehlerhaften Zustand befinden. Das Warten auf
die Aufgabe löst eine Ausnahme aus. Eine Aufgabe kann auch in einem abgebrochenen Zustand enden, wenn
der asynchrone Prozess, der sie zurückgibt, abgebrochen wird. Das Warten auf eine abgebrochene Aufgabe löst
eine OperationCanceledException -Ausnahme aus.
Um die Ausnahme abzufangen, warten Sie in einem try -Block auf die Aufgabe, und fangen Sie die Ausnahme
im zugehörigen catch -Block ab. Ein Beispiel hierfür finden Sie im Abschnitt Beispiel zu einer Async-Methode.
Eine Aufgabe kann sich in einem fehlerhaften Zustand befinden, da mehrere Ausnahmen in der erwarteten
asynchronen Methode aufgetreten sind. Beispielsweise kann die Aufgabe das Ergebnis eines Aufrufs an
Task.WhenAll sein. Wenn Sie auf eine solche Aufgabe warten, wird nur eine der Ausnahmen abgefangen, und
Sie können nicht vorhersagen, welche Ausnahme abgefangen wird. Ein Beispiel hierfür finden Sie im Abschnitt
Task.WhenAll-Beispiel.

Beispiel
Im folgenden Beispiel enthält der try -Block einen Aufruf der ProcessString -Methode, die eine Ausnahme
verursachen kann. Die catch -Klausel enthält den Ausnahmehandler, der lediglich eine Meldung auf dem
Bildschirm anzeigt. Wenn die throw -Anweisung aus ProcessString heraus aufgerufen wird, sucht das System
nach der catch -Anweisung und zeigt die Meldung Exception caught an.

class TryFinallyTest
{
static void ProcessString(string s)
{
if (s == null)
{
throw new ArgumentNullException(paramName: nameof(s), message: "parameter can't be null.");
}
}

public static void Main()


{
string s = null; // For demonstration purposes.

try
{
ProcessString(s);
}
catch (Exception e)
{
Console.WriteLine("{0} Exception caught.", e);
}
}
}
/*
Output:
System.ArgumentNullException: Value cannot be null.
at TryFinallyTest.Main() Exception caught.
* */

Beispiel für zwei Catch-Blöcke


Im folgenden Beispiel werden zwei catch-Blöcke verwendet, und die spezifischste Ausnahme, die an erster Stelle
steht, wird abgefangen.
Um die allgemeinste Ausnahme abzufangen, können Sie die throw-Anweisung in ProcessString durch die
folgende Anweisung ersetzen: throw new Exception() .
Wenn Sie den allgemeinsten catch-Block im Beispiel an erster Stelle platzieren, wird die folgende Fehlermeldung
angezeigt:
A previous catch clause already catches all exceptions of this or a super type ('System.Exception') .
class ThrowTest3
{
static void ProcessString(string s)
{
if (s == null)
{
throw new ArgumentNullException(paramName: nameof(s), message: "Parameter can't be null");
}
}

public static void Main()


{
try
{
string s = null;
ProcessString(s);
}
// Most specific:
catch (ArgumentNullException e)
{
Console.WriteLine("{0} First exception caught.", e);
}
// Least specific:
catch (Exception e)
{
Console.WriteLine("{0} Second exception caught.", e);
}
}
}
/*
Output:
System.ArgumentNullException: Value cannot be null.
at Test.ThrowTest3.ProcessString(String s) ... First exception caught.
*/

Beispiel für eine Async-Methode


Im folgenden Beispiel wird die Ausnahmebehandlung für asynchrone Methoden veranschaulicht. Um eine von
einer asynchronen Aufgabe ausgelöste Ausnahme abzufangen, platzieren Sie den await -Ausdruck in einem
try -Block, und fangen Sie die Ausnahme in einem catch -Block ab.

Heben Sie die Auskommentierung der Zeile throw new Exception im Beispiel auf, um die Ausnahmebehandlung
zu veranschaulichen. Die IsFaulted -Eigenschaft der Aufgabe wird auf True festgelegt, die
Exception.InnerException -Eigenschaft der Aufgabe auf die Ausnahme, und die Ausnahme wird im catch -Block
abgefangen.
Heben Sie die Auskommentierung der Zeile throw new OperationCanceledException auf, um zu veranschaulichen,
was beim Abbrechen eines asynchronen Prozesses passiert. Die IsCanceled -Eigenschaft der Aufgabe wird auf
true festgelegt, und die Ausnahme wird im catch -Block abgefangen. Unter bestimmten Bedingungen, die für
dieses Beispiel nicht gelten, wird die IsFaulted -Eigenschaft der Aufgabe auf true und IsCanceled auf false
festgelegt.
public async Task DoSomethingAsync()
{
Task<string> theTask = DelayAsync();

try
{
string result = await theTask;
Debug.WriteLine("Result: " + result);
}
catch (Exception ex)
{
Debug.WriteLine("Exception Message: " + ex.Message);
}
Debug.WriteLine("Task IsCanceled: " + theTask.IsCanceled);
Debug.WriteLine("Task IsFaulted: " + theTask.IsFaulted);
if (theTask.Exception != null)
{
Debug.WriteLine("Task Exception Message: "
+ theTask.Exception.Message);
Debug.WriteLine("Task Inner Exception Message: "
+ theTask.Exception.InnerException.Message);
}
}

private async Task<string> DelayAsync()


{
await Task.Delay(100);

// Uncomment each of the following lines to


// demonstrate exception handling.

//throw new OperationCanceledException("canceled");


//throw new Exception("Something happened.");
return "Done";
}

// Output when no exception is thrown in the awaited method:


// Result: Done
// Task IsCanceled: False
// Task IsFaulted: False

// Output when an Exception is thrown in the awaited method:


// Exception Message: Something happened.
// Task IsCanceled: False
// Task IsFaulted: True
// Task Exception Message: One or more errors occurred.
// Task Inner Exception Message: Something happened.

// Output when a OperationCanceledException or TaskCanceledException


// is thrown in the awaited method:
// Exception Message: canceled
// Task IsCanceled: True
// Task IsFaulted: False

Task.WhenAll-Beispiel
Das folgende Beispiel veranschaulicht die Behandlung von Ausnahmen in Fällen, in denen mehrere Aufgaben zu
mehreren Ausnahmen führen können. Der try -Block wartet auf die Aufgabe, die von einem Aufruf von
Task.WhenAll zurückgegeben wird. Die Aufgabe ist abgeschlossen, wenn die drei Aufgaben abgeschlossen sind,
auf die WhenAll angewendet wird.
Jede der drei Aufgaben löst eine Ausnahme aus. Der catch -Block iteriert durch die Ausnahmen, die in der
Exception.InnerExceptions -Eigenschaft der Aufgabe stehen, die von Task.WhenAll zurückgegeben wurde.
public async Task DoMultipleAsync()
{
Task theTask1 = ExcAsync(info: "First Task");
Task theTask2 = ExcAsync(info: "Second Task");
Task theTask3 = ExcAsync(info: "Third Task");

Task allTasks = Task.WhenAll(theTask1, theTask2, theTask3);

try
{
await allTasks;
}
catch (Exception ex)
{
Debug.WriteLine("Exception: " + ex.Message);
Debug.WriteLine("Task IsFaulted: " + allTasks.IsFaulted);
foreach (var inEx in allTasks.Exception.InnerExceptions)
{
Debug.WriteLine("Task Inner Exception: " + inEx.Message);
}
}
}

private async Task ExcAsync(string info)


{
await Task.Delay(100);

throw new Exception("Error-" + info);


}

// Output:
// Exception: Error-First Task
// Task IsFaulted: True
// Task Inner Exception: Error-First Task
// Task Inner Exception: Error-Second Task
// Task Inner Exception: Error-Third Task

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Die Try-Anweisung der C#-Sprachspezifikation.

Weitere Informationen
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
try, throw, and catch Statements (C++) (try-, throw- und catch-Anweisungen (C++))
throw
try-finally
Vorgehensweise: Explizites Auslösen von Ausnahmen
try-finally (C#-Referenz)
04.11.2021 • 3 minutes to read

Mit einem finally -Block können Sie alle Ressourcen bereinigen, die in einem try-Block belegt sind, und Sie
können Code selbst dann ausführen, wenn eine Ausnahme im try -Block auftritt. In der Regel werden die
Anweisungen eines finally -Blocks ausgeführt, wenn die Steuerung eine try -Anweisung verlässt. Die
Übertragung eines Steuerelements kann infolge einer normalen Ausführung, der Ausführung einer break -,
continue -, goto - oder return -Anweisung oder der Weitergabe einer Ausnahme aus der try -Anweisung
auftreten.
Innerhalb einer behandelten Ausnahme ist sichergestellt, dass der zugeordnete finally -Block ausgeführt wird.
Wenn die Ausnahme jedoch nicht behandelt wird, erfolgt die Ausführung des finally -Blocks je nachdem, wie
der Entladevorgang für die Ausnahme ausgelöst wird. Das hängt wiederum davon ab, wie der Computer
eingerichtet ist. Die einzigen Fälle, in denen finally -Klauseln nicht ausgeführt werden, betreffen die sofortige
Beendigung eines Programms. Ein Beispiel hierfür ist das Auslösen von InvalidProgramException aufgrund von
beschädigten IL-Anweisungen. Bei den meisten Betriebssystemen findet eine angemessene
Ressourcenbereinigung statt, während der Prozess beendet und entladen wird.
Wenn eine Anwendung durch einen Ausnahmefehler beendet wird, ist es in der Regel nicht wichtig, ob der
finally -Block ausgeführt wird. Wenn der finally -Block jedoch Anweisungen aufweist, die auch in dieser
Situation ausgeführt werden müssen, kann als Lösung ein catch -Block der try - finally -Anweisung
hinzugefügt werden. Sie können auch die Ausnahme abfangen, die möglicherweise im try -Block einer try -
finally -Anweisung weiter oben in der Aufrufliste ausgelöst wird. Die Ausnahme kann also in der Methode
abgefangen werden, mit der die Methode mit der try - finally -Anweisung aufgerufen wird. Die Ausnahme
kann aber auch in der Methode abgefangen werden, mit der diese Methode aufgerufen wird, oder in einer
beliebigen Methode in der Aufrufliste. Wenn die Ausnahme nicht abgefangen wird, wird der finally -Block je
nachdem, ob vom Betriebssystem ein Entladevorgang für die Ausnahme ausgelöst wird, ausgeführt.

Beispiel
Im folgenden Beispiel wird eine System.InvalidCastException Ausnahme durch eine ungültige
Konvertierungsanweisung verursacht. Die Ausnahme wird nicht behandelt.
public class ThrowTestA
{
public static void Main()
{
int i = 123;
string s = "Some string";
object obj = s;

try
{
// Invalid conversion; obj contains a string, not a numeric type.
i = (int)obj;

// The following statement is not run.


Console.WriteLine("WriteLine at the end of the try block.");
}
finally
{
// To run the program in Visual Studio, type CTRL+F5. Then
// click Cancel in the error dialog.
Console.WriteLine("\nExecution of the finally block after an unhandled\n" +
"error depends on how the exception unwind operation is triggered.");
Console.WriteLine("i = {0}", i);
}
}
// Output:
// Unhandled Exception: System.InvalidCastException: Specified cast is not valid.
//
// Execution of the finally block after an unhandled
// error depends on how the exception unwind operation is triggered.
// i = 123
}

Im folgenden Beispiel wird eine Ausnahme von der TryCast -Methode in einer Methode weiter oben in der
Aufrufliste abgefangen.
public class ThrowTestB
{
public static void Main()
{
try
{
// TryCast produces an unhandled exception.
TryCast();
}
catch (Exception ex)
{
// Catch the exception that is unhandled in TryCast.
Console.WriteLine
("Catching the {0} exception triggers the finally block.",
ex.GetType());

// Restore the original unhandled exception. You might not


// know what exception to expect, or how to handle it, so pass
// it on.
throw;
}
}

static void TryCast()


{
int i = 123;
string s = "Some string";
object obj = s;

try
{
// Invalid conversion; obj contains a string, not a numeric type.
i = (int)obj;

// The following statement is not run.


Console.WriteLine("WriteLine at the end of the try block.");
}
finally
{
// Report that the finally block is run, and show that the value of
// i has not been changed.
Console.WriteLine("\nIn the finally block in TryCast, i = {0}.\n", i);
}
}
// Output:
// In the finally block in TryCast, i = 123.

// Catching the System.InvalidCastException exception triggers the finally block.

// Unhandled Exception: System.InvalidCastException: Specified cast is not valid.


}

Weitere Informationen zu finally finden Sie unter try-catch-finally.


C# enthält auch die Using-Anweisung, die eine ähnliche Funktionalität für IDisposable-Objekte in einer
zweckmäßigen Syntax bereitstellt.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Die Try-Anweisung der C#-Sprachspezifikation.

Weitere Informationen
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
try, throw, and catch Statements (C++) (try-, throw- und catch-Anweisungen (C++))
throw
try-catch
Vorgehensweise: Explizites Auslösen von Ausnahmen
try-catch-finally (C#-Referenz)
04.11.2021 • 2 minutes to read

Häufige Verwendungen von catch und finally sind das Abrufen und Verwenden von Ressourcen in einem
try -Block, das Vorgehen bei außergewöhnlichen Umständen in einem catch -Block und das Freisetzen von
Ressourcen im finally -Block.
Weitere Informationen und Beispiele zum erneuten Auslösen von Ausnahmen finden Sie unter try-catch und
Auslösen von Ausnahmen. Weitere Information über den finally -Block finden unter try-finally.

Beispiel
public class EHClass
{
void ReadFile(int index)
{
// To run this code, substitute a valid path from your local machine
string path = @"c:\users\public\test.txt";
System.IO.StreamReader file = new System.IO.StreamReader(path);
char[] buffer = new char[10];
try
{
file.ReadBlock(buffer, index, buffer.Length);
}
catch (System.IO.IOException e)
{
Console.WriteLine("Error reading from {0}. Message = {1}", path, e.Message);
}
finally
{
if (file != null)
{
file.Close();
}
}
// Do something with buffer...
}
}

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Die Try-Anweisung der C#-Sprachspezifikation.

Weitere Informationen
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
try, throw, and catch Statements (C++) (try-, throw- und catch-Anweisungen (C++))
throw
Vorgehensweise: Explizites Auslösen von Ausnahmen
Using-Anweisung
Checked und Unchecked (C#-Referenz)
04.11.2021 • 2 minutes to read

C#-Anweisungen könnten entweder in einem geprüften oder nicht geprüften Kontext (checked oder unchecked)
ausgeführt werden. In einem überprüften Kontext löst der arithmetische Überlauf eine Ausnahme aus. In einem
nicht aktivierten Kontext wird der arithmetische Überlauf ignoriert und das Ergebnis gekürzt, indem alle
höherwertigen Bits verworfen werden, die nicht in den Zieltyp passen.
checked Gibt einen geprüften Kontext an.
unchecked Gibt einen ungeprüften Kontext an.
Die folgenden Vorgänge sind von der Überlaufüberprüfung betroffen:
Ausdrücke, die die folgenden vordefinierten Operatoren für ganzzahlige Typen verwenden:
++ , -- , unäres - , + , - , * , /

Explizite numerische Konvertierungen zwischen ganzzahligen Typen oder von float oder double in
einen integralen Typ.
Wenn weder checked noch unchecked festgelegt ist, wird der Standardkontext für nicht konstante Ausdrücke
(Ausdrücke, die zur Laufzeit ausgewertet werden) vom Wert der Compileroption
CheckForOverflowUnderflow definiert. Standardmäßig ist der Wert dieser Option nicht festgelegt, und
arithmetische Operationen werden in einem nicht geprüften Kontext ausgeführt.
Für konstante Ausdrücke (Ausdrücke, die zur Kompilierzeit vollständig ausgewertet werden können) ist der
Standardkontext immer geprüft. Überläufe, die während der Auswertung des Ausdrucks zur Kompilierzeit
auftreten, führen zu Kompilierzeitfehlern, wenn der konstante Ausdruck nicht explizit in einen ungeprüften
Kontext gebracht wird.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Anweisungsschlüsselwörter
checked (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort checked wird verwendet, um eine Überlaufüberprüfung bei arithmetischen Operationen für
ganzzahlige Typen und Konvertierungen explizit zu aktivieren.
Standardmäßig bewirkt ein Ausdruck, der nur konstante Werte enthält, einen Compilerfehler, wenn der
Ausdruck einen Wert erzeugt, der außerhalb des Bereichs des Zieltyps liegt. Wenn der Ausdruck einen oder
mehrere nicht konstante Werte enthält, erkennt der Compiler den Überlauf nicht. Die Auswertung des
Ausdrucks, der im folgenden Beispiel i2 zugewiesen ist, verursacht keinen Compilerfehler.

// The following example causes compiler error CS0220 because 2147483647


// is the maximum value for integers.
//int i1 = 2147483647 + 10;

// The following example, which includes variable ten, does not cause
// a compiler error.
int ten = 10;
int i2 = 2147483647 + ten;

// By default, the overflow in the previous statement also does


// not cause a run-time exception. The following line displays
// -2,147,483,639 as the sum of 2,147,483,647 and 10.
Console.WriteLine(i2);

Standardmäßig werden diese nicht konstanten Ausdrücke zur Laufzeit auch nicht auf Überläufe überprüft und
lösen keine Überlaufausnahmen aus. Das vorherige Beispiel zeigt -2,147,483,639 als Summe von zwei ganzen
Zahlen.
Die Überlaufüberprüfung kann durch Compileroptionen, Umgebungskonfiguration oder Verwendung des
checked -Schlüsselworts aktiviert werden. In den folgenden Beispielen wird veranschaulicht, wie Sie einen
checked -Ausdruck oder einen checked -Block verwenden, um den Überlauf zu erkennen, der von der
vorherigen Summe zur Laufzeit erzeugt wird. In beiden Beispielen wird eine Überlaufausnahme ausgelöst.

// If the previous sum is attempted in a checked environment, an


// OverflowException error is raised.

// Checked expression.
Console.WriteLine(checked(2147483647 + ten));

// Checked block.
checked
{
int i3 = 2147483647 + ten;
Console.WriteLine(i3);
}

Das unchecked-Schlüsselwort kann verwendet werden, um die Überlaufüberprüfung zu verhindern.

Beispiel
Dieses Beispiel zeigt, wie mit checked die Überlaufüberprüfung zur Laufzeit aktiviert wird.
class OverFlowTest
{
// Set maxIntValue to the maximum value for integers.
static int maxIntValue = 2147483647;

// Using a checked expression.


static int CheckedMethod()
{
int z = 0;
try
{
// The following line raises an exception because it is checked.
z = checked(maxIntValue + 10);
}
catch (System.OverflowException e)
{
// The following line displays information about the error.
Console.WriteLine("CHECKED and CAUGHT: " + e.ToString());
}
// The value of z is still 0.
return z;
}

// Using an unchecked expression.


static int UncheckedMethod()
{
int z = 0;
try
{
// The following calculation is unchecked and will not
// raise an exception.
z = maxIntValue + 10;
}
catch (System.OverflowException e)
{
// The following line will not be executed.
Console.WriteLine("UNCHECKED and CAUGHT: " + e.ToString());
}
// Because of the undetected overflow, the sum of 2147483647 + 10 is
// returned as -2147483639.
return z;
}

static void Main()


{
Console.WriteLine("\nCHECKED output value is: {0}",
CheckedMethod());
Console.WriteLine("UNCHECKED output value is: {0}",
UncheckedMethod());
}
/*
Output:
CHECKED and CAUGHT: System.OverflowException: Arithmetic operation resulted
in an overflow.
at ConsoleApplication1.OverFlowTest.CheckedMethod()

CHECKED output value is: 0


UNCHECKED output value is: -2147483639
*/
}

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Checked und Unchecked
unchecked
unchecked (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort unchecked wird verwendet, um eine Überlaufüberprüfung bei arithmetischen Operationen
für ganzzahlige Typen und Konvertierungen zu unterdrücken.
Wenn ein Ausdruck in einem ungeprüften Kontext einen Wert erzeugt, der außerhalb des Bereichs des Zieltyps
ist, wird der Überlauf nicht gekennzeichnet. Da z.B. die Berechnung im folgenden Beispiel in einem unchecked -
Block oder -Ausdruck ausgeführt wird, wird ignoriert, dass das Ergebnis zu groß für eine Ganzzahl ist, und int1
bekommt den Wert -2.147.483.639 zugewiesen.

unchecked
{
int1 = 2147483647 + 10;
}
int1 = unchecked(ConstantMax + 10);

Wenn die unchecked -Umgebung entfernt wird, tritt ein Kompilierungsfehler auf. Der Überlauf kann zur
Kompilierzeit erkannt werden, da alle Begriffe des Ausdrucks Konstanten sind.
Ausdrücke, die nicht konstante Begriffe enthalten, sind standardmäßig zur Kompilier- und Laufzeit deaktiviert.
Informationen zum Aktivieren einer überprüften Umgebung finden Sie unter checked.
Da die Überprüfung auf Überlauf Zeit in Anspruch nimmt, kann die Verwendung von einem nicht überprüften
Code in Situationen, in denen keine Überlaufgefahr besteht, die Leistung verbessern. Wenn jedoch ein Überlauf
möglich ist, sollte eine überprüfte Umgebung verwendet werden.

Beispiel
Im folgenden Beispiel wird die Verwendung des Schlüsselworts unchecked veranschaulicht.
class UncheckedDemo
{
static void Main(string[] args)
{
// int.MaxValue is 2,147,483,647.
const int ConstantMax = int.MaxValue;
int int1;
int int2;
int variableMax = 2147483647;

// The following statements are checked by default at compile time. They do not
// compile.
//int1 = 2147483647 + 10;
//int1 = ConstantMax + 10;

// To enable the assignments to int1 to compile and run, place them inside
// an unchecked block or expression. The following statements compile and
// run.
unchecked
{
int1 = 2147483647 + 10;
}
int1 = unchecked(ConstantMax + 10);

// The sum of 2,147,483,647 and 10 is displayed as -2,147,483,639.


Console.WriteLine(int1);

// The following statement is unchecked by default at compile time and run


// time because the expression contains the variable variableMax. It causes
// overflow but the overflow is not detected. The statement compiles and runs.
int2 = variableMax + 10;

// Again, the sum of 2,147,483,647 and 10 is displayed as -2,147,483,639.


Console.WriteLine(int2);

// To catch the overflow in the assignment to int2 at run time, put the
// declaration in a checked block or expression. The following
// statements compile but raise an overflow exception at run time.
checked
{
//int2 = variableMax + 10;
}
//int2 = checked(variableMax + 10);

// Unchecked sections frequently are used to break out of a checked


// environment in order to improve performance in a portion of code
// that is not expected to raise overflow exceptions.
checked
{
// Code that might cause overflow should be executed in a checked
// environment.
unchecked
{
// This section is appropriate for code that you are confident
// will not result in overflow, and for which performance is
// a priority.
}
// Additional checked code here.
}
}
}

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Checked und Unchecked
checked
fixed-Anweisung (C#-Referenz)
04.11.2021 • 3 minutes to read

Die fixed -Anweisung verhindert, dass der Garbage Collector eine bewegliche Variable verschiebt. Die fixed -
Anweisung ist nur in einem unsicheren (unsafe) Kontext zulässig. Sie können auch das Schlüsselwort fixed
verwenden, um Puffer mit fester Größe zu erstellen.
Die fixed -Anweisung setzt einen Zeiger auf eine verwaltete Variable und „fixiert“ diese Variable während der
Ausführung der Anweisung. Zeiger auf verschiebbare verwaltete Variablen sind nur in einem fixed -Kontext
nützlich. Ohne den fixed -Kontext könnte die automatische Speicherbereinigung die Variablen auf
unvorhersehbare Weise verschieben. Mit dem C#-Compiler können Sie einer verwalteten Variablen nur in einer
fixed -Anweisung einen Zeiger zuweisen.

class Point
{
public int x;
public int y;
}

unsafe private static void ModifyFixedStorage()


{
// Variable pt is a managed variable, subject to garbage collection.
Point pt = new Point();

// Using fixed allows the address of pt members to be taken,


// and "pins" pt so that it is not relocated.

fixed (int* p = &pt.x)


{
*p = 1;
}
}

Sie können einen Zeiger mit einem Array, einer Zeichenfolge, einem Puffer fester Größe oder der Adresse einer
Variablen initialisieren. Im folgenden Beispiel wird die Verwendung von Adressen, Arrays und Zeichenfolgen von
Variablen veranschaulicht:

Point point = new Point();


double[] arr = { 0, 1.5, 2.3, 3.4, 4.0, 5.9 };
string str = "Hello World";

// The following two assignments are equivalent. Each assigns the address
// of the first element in array arr to pointer p.

// You can initialize a pointer by using an array.


fixed (double* p = arr) { /*...*/ }

// You can initialize a pointer by using the address of a variable.


fixed (double* p = &arr[0]) { /*...*/ }

// The following assignment initializes p by using a string.


fixed (char* p = str) { /*...*/ }

// The following assignment is not valid, because str[0] is a char,


// which is a value, not a variable.
//fixed (char* p = &str[0]) { /*...*/ }
Ab C# 7.3 funktioniert die fixed -Anweisung mit weiteren Typen, nicht nur mit Arrays, Zeichenfolgen, Puffern
mit festgelegter Größe und nicht verwalteten Variablen. Jeder Typ, der eine Methode mit dem Namen
GetPinnableReference implementiert, kann angeheftet werden. GetPinnableReference muss eine ref -Variable
eines nicht verwalteten Typs zurückgeben. Die .NET-Typen System.Span<T> und System.ReadOnlySpan<T>, die
in .NET Core 2.0 eingeführt wurden, verwenden dieses Muster und können angeheftet werden. Dies wird im
folgenden Beispiel gezeigt:

unsafe private static void FixedSpanExample()


{
int[] PascalsTriangle = {
1,
1, 1,
1, 2, 1,
1, 3, 3, 1,
1, 4, 6, 4, 1,
1, 5, 10, 10, 5, 1
};

Span<int> RowFive = new Span<int>(PascalsTriangle, 10, 5);

fixed (int* ptrToRow = RowFive)


{
// Sum the numbers 1,4,6,4,1
var sum = 0;
for (int i = 0; i < RowFive.Length; i++)
{
sum += *(ptrToRow + i);
}
Console.WriteLine(sum);
}
}

Wenn Sie Typen erstellen, die auch dieses Muster verwenden sollen, finden Sie unter
Span<T>.GetPinnableReference() ein Beispiel für die Implementierung des Musters.
Wenn mehrere Zeiger vom selben Typ sind, können sie in einer gemeinsamen Anweisung initialisiert werden:

fixed (byte* ps = srcarray, pd = dstarray) {...}

Um Zeiger verschiedener Typen zu initialisieren, schachteln Sie einfach fixed -Anweisungen, wie im folgenden
Beispiel gezeigt.

fixed (int* p1 = &point.x)


{
fixed (double* p2 = &arr[5])
{
// Do something with p1 and p2.
}
}

Nachdem der Code in der Anweisung ausgeführt wird, können beliebige fixierte Variablen gelöst und an die
automatische Speicherbereinigung übergeben werden. Aus diesem Grund sollten Sie nicht außerhalb der
fixed -Anweisung auf diese Variablen verweisen. Die in der fixed -Anweisung deklarierten Variablen beziehen
sich auf diese Anweisung. Dadurch wird Folgendes vereinfacht:
fixed (byte* ps = srcarray, pd = dstarray)
{
...
}
// ps and pd are no longer in scope here.

Bei Zeigern, die in fixed -Anweisungen initialisiert werden, handelt es sich um readonly-Variablen. Wenn Sie
den Zeigerwert ändern möchten, müssen Sie eine zweite Zeigervariable deklarieren und diese ändern. Die in
der fixed -Anweisung deklarierte Variable kann nicht verändert werden:

fixed (byte* ps = srcarray, pd = dstarray)


{
byte* pSourceCopy = ps;
pSourceCopy++; // point to the next element.
ps++; // invalid: cannot modify ps, as it is declared in the fixed statement.
}

Sie können dem Stapel Arbeitsspeicher zuordnen, der nicht automatisch bereinigt wird und daher nicht fixiert
werden muss. Hierzu verwenden Sie einen stackalloc -Ausdruck.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Die fixed-Anweisung der C#-Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
unsafe
Zeigertypen
Puffer fester Größe
lock-Anweisung (C#-Referenz)
04.11.2021 • 2 minutes to read

Die lock -Anweisung ruft die Sperre für gegenseitigen Ausschluss für ein bestimmtes Objekt ab, führt einen
Anweisungsblock aus und hebt die Sperre anschließend auf. Während eine Sperre aufrechterhalten wird, kann
der Thread, der die Sperre aufrechterhält, die Sperre abrufen und aufheben. Für jeden anderen Thread wird das
Abrufen der Sperre blockiert, und die Sperre wartet auf die Aufhebung.
Die lock -Anweisung weist folgendes Format auf:

lock (x)
{
// Your code...
}

x entspricht einem Ausdruck eines Verweistyps. Dieser entspricht exakt Folgendem:

object __lockObj = x;
bool __lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
// Your code...
}
finally
{
if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}

Da ein try...finally-Block in diesem Code verwendet wird, wird die Sperre aufgehoben, wenn eine Ausnahme
innerhalb des Texts einer lock -Anweisung ausgelöst wird.
Sie können den Operator await nicht im Text einer lock -Anweisung verwenden.

Richtlinien
Wenn Sie den Threadzugriff auf eine freigegebene Ressource synchronisieren, sperren Sie eine dedizierte
Objektinstanz (z.B. private readonly object balanceLock = new object(); ) oder eine andere Instanz, die
wahrscheinlich nicht von anderen Teilen des Codes als lock-Objekt verwendet wird. Vermeiden Sie, die gleiche
lock-Objektinstanz für verschiedene freigegebene Ressourcen zu verwenden, da dies zu einem Deadlock oder
Sperrkonflikt führen kann. Vermeiden Sie insbesondere die Verwendung der folgenden Objekte als Sperre:
this– kann von den Aufrufern als Sperre verwendet werden.
Type-Instanzen – können vom typeof-Operator oder der Reflektion abgerufen werden.
Zeichenfolgeninstanzen, einschließlich Zeichenfolgenliteralen – können internalisiert sein.
Die Dauer von Sperren sollte so kurz wie möglich sein, um Sperrungskonflikte zu vermindern.

Beispiel
Im folgenden Beispiel wird eine Account -Klasse definiert, die den Zugriff auf das private balance -Feld
synchronisiert, indem eine dedizierte balanceLock -Instanz gesperrt wird. Durch Verwendung der gleichen
Instanz für die Sperre wird sichergestellt, dass das balance -Feld nicht von zwei Threads gleichzeitig aktualisiert
werden kann, die zur gleichen Zeit versuchen, die Methoden Debit oder Credit aufzurufen.

using System;
using System.Threading.Tasks;

public class Account


{
private readonly object balanceLock = new object();
private decimal balance;

public Account(decimal initialBalance) => balance = initialBalance;

public decimal Debit(decimal amount)


{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "The debit amount cannot be negative.");
}

decimal appliedAmount = 0;
lock (balanceLock)
{
if (balance >= amount)
{
balance -= amount;
appliedAmount = amount;
}
}
return appliedAmount;
}

public void Credit(decimal amount)


{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "The credit amount cannot be negative.");
}

lock (balanceLock)
{
balance += amount;
}
}

public decimal GetBalance()


{
lock (balanceLock)
{
return balance;
}
}
}

class AccountTest
{
static async Task Main()
{
var account = new Account(1000);
var tasks = new Task[100];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = Task.Run(() => Update(account));
}
await Task.WhenAll(tasks);
Console.WriteLine($"Account's balance is {account.GetBalance()}");
// Output:
// Account's balance is 2000
// Account's balance is 2000
}

static void Update(Account account)


{
decimal[] amounts = { 0, 2, -3, 6, -2, -1, 8, -5, 11, -6 };
foreach (var amount in amounts)
{
if (amount >= 0)
{
account.Credit(amount);
}
else
{
account.Debit(Math.Abs(amount));
}
}
}
}

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Die lock-Anweisung der C#-Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Schlüsselwörter
System.Threading.Monitor
System.Threading.SpinLock
System.Threading.Interlocked
Übersicht über Synchronisierungsprimitiven
Methodenparameter (C#-Referenz)
04.11.2021 • 2 minutes to read

Parameter, die ohne in, ref oder out für eine Methode deklariert werden, werden nach Werten an die
aufgerufene Methode übergeben. Dieser Wert kann in der Methode geändert werden, aber der geänderte Wert
wird nicht gespeichert, wenn die aufrufende Prozedur wieder die Steuerung übernimmt. Wenn Sie ein
Schlüsselwort für einen Methodenparameter verwenden, können Sie dieses Verhalten ändern.
In diesem Abschnitt wird das Schlüsselwort beschrieben, dass Sie verwenden können, wenn Sie
Methodenparameter deklarieren.
Mit params wird festgelegt, dass für diesen Parameter eine veränderliche Anzahl von Argumenten
akzeptiert werden.
Mit in wird festgelegt, dass dieser Parameter als Verweis übergeben wird, jedoch nur von der
aufgerufenen Methode gelesen wird.
Mit ref wird festgelegt, dass dieser Parameter als Verweis übergeben wird und von der aufgerufenen
Methode gelesen oder geschrieben werden kann.
Mit out wird festgelegt, dass dieser Parameter als Verweis übergeben wird und von der aufgerufenen
Methode geschrieben wird.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
params (C#-Referenz)
04.11.2021 • 2 minutes to read

Mithilfe des Schlüsselworts params kann ein Methodenparameter angegeben werden, der eine variable Anzahl
von Argumenten akzeptiert. Der Parametertyp muss ein eindimensionales Array sein.
Nach dem params -Schlüsselwort sind keine zusätzlichen Parameter in einer Methodendeklaration zugelassen.
Gleichzeitig ist nur ein params -Schlüsselwort in einer Methodendeklaration zulässig.
Wenn der deklarierte Typ des params -Parameters kein eindimensionales Array ist, tritt der Compilerfehler
CS0225 auf.
Wenn Sie eine Methode mit einem params -Parameter aufrufen, können Sie Folgendes übergeben:
Eine durch Trennzeichen getrennte Liste von Argumenten des Typs der Arrayelemente
Ein Array aus Argumenten des angegebenen Typs
Keine Argumente. Wenn Sie keine Argumente senden, ist die Länge der params -Liste 0 (null).

Beispiel
Im folgenden Beispiel werden verschiedene Methoden veranschaulicht, in denen Argumente an einen params -
Parameter gesendet werden können.
public class MyClass
{
public static void UseParams(params int[] list)
{
for (int i = 0; i < list.Length; i++)
{
Console.Write(list[i] + " ");
}
Console.WriteLine();
}

public static void UseParams2(params object[] list)


{
for (int i = 0; i < list.Length; i++)
{
Console.Write(list[i] + " ");
}
Console.WriteLine();
}

static void Main()


{
// You can send a comma-separated list of arguments of the
// specified type.
UseParams(1, 2, 3, 4);
UseParams2(1, 'a', "test");

// A params parameter accepts zero or more arguments.


// The following calling statement displays only a blank line.
UseParams2();

// An array argument can be passed, as long as the array


// type matches the parameter type of the method being called.
int[] myIntArray = { 5, 6, 7, 8, 9 };
UseParams(myIntArray);

object[] myObjArray = { 2, 'b', "test", "again" };


UseParams2(myObjArray);

// The following call causes a compiler error because the object


// array cannot be converted into an integer array.
//UseParams(myObjArray);

// The following call does not cause an error, but the entire
// integer array becomes the first element of the params array.
UseParams2(myIntArray);
}
}
/*
Output:
1 2 3 4
1 a test

5 6 7 8 9
2 b test again
System.Int32[]
*/

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Methodenparameter
Modifizierer für in-Parameter (C#-Verweis)
04.11.2021 • 5 minutes to read

Das Schlüsselwort in bewirkt, dass Argumente nach Verweis übermittelt werden, stellt aber auch sicher, dass
das Argument nicht geändert wird. Es stellt den formalen Parameter als Alias für das Argument dar, das eine
Variable sein muss. Anders ausgedrückt, jede Operation mit dem Parameter wird mit dem Argument
durchgeführt. Es verhält sich ähnlich wie die Schlüsselwörter ref und out. Der einzige Unterschied besteht darin,
dass in -Argumente nicht von der aufgerufenen Methode modifiziert werden können. Obwohl ref -
Argumente modifiziert werden können, müssen out -Argumente von der aufgerufenen Methode geändert
werden. Die Änderungen werden im Aufrufkontext angezeigt.

int readonlyArgument = 44;


InArgExample(readonlyArgument);
Console.WriteLine(readonlyArgument); // value is still 44

void InArgExample(in int number)


{
// Uncomment the following line to see error CS8331
//number = 19;
}

Das vorausgehende Beispiel veranschaulicht, dass der in -Modifizierer an der Aufrufstelle normalerweise nicht
benötigt wird. Er ist nur in der Methodendeklaration erforderlich.

NOTE
Das in -Schlüsselwort kann ebenfalls mit einem generischen Typparameter verwendet werden, um als der Bestandteil
einer foreach -Anweisung oder einer join -Klausel in einer LINQ-Abfrage anzugeben, dass der Typparameter
kontravariant ist. Weitere Informationen zur Verwendung des in -Schlüsselworts in diesen Kontexten und
entsprechende Links finden Sie unter in.

Variablen, die als in -Argumente übergeben wurden, müssen initialisiert werden, bevor sie in einen
Methodenaufruf übergeben werden. Es kann jedoch sein, dass die aufgerufene Methode keinen Wert zuweist
oder das Argument ändert.
Die in -Parametermodifizierer steht in C# 7.2 und höher zur Verfügung. Frühere Versionen generieren den
Compilerfehler CS8107 (Das Feature „schreibgeschützte Verweise“ ist in C# 7.0 nicht verfügbar. Verwenden Sie
Sprachversion 7.2 oder höher.). Hinweise zum Konfigurieren der Compilersprachversion finden Sie unter
Auswählen der C#-Sprachversion.
Obwohl die in -, out, - und ref -Parametermodifizierer als Teil einer Signatur betrachtet werden, dürfen sich
die in einem einzelnen Typ deklarierten Member in der Signatur nicht nur durch in , ref und out
unterscheiden. Aus diesem Grund können die Methoden nicht überladen werden, wenn der einzige Unterschied
darin besteht, dass eine Methode ein ref - oder in -Argument übernimmt und die andere ein out -Argument.
Der folgende Code wird z. B. nicht kompiliert:
class CS0663_Example
{
// Compiler error CS0663: "Cannot define overloaded
// methods that differ only on in, ref and out".
public void SampleMethod(in int i) { }
public void SampleMethod(ref int i) { }
}

Das Überladen bei vorhandenem in ist zulässig:

class InOverloads
{
public void SampleMethod(in int i) { }
public void SampleMethod(int i) { }
}

Regeln zur Überladungsauflösung


Die Regeln zur Auflösung von Methodenüberladungen bei der Übergabe von Argumenten als Wert und durch
in lassen sich nachvollziehen, wenn die Motivation für in -Argumente bekannt ist. Wenn Methoden mit in -
Parametern definiert werden, kann unter Umständen eine Leistungsoptimierung erzielt werden. Einige struct -
Typargumente beanspruchen viel Speicherplatz. Wenn dann in Schleifen mit vielen Durchläufen oder kritischen
Codepfaden Methoden aufgerufen werden, ist der Aufwand zum Kopieren dieser Strukturen enorm. Durch die
Deklaration von in -Parametern in Methoden wird festgelegt, dass Argumente sicher als Verweis übergeben
werden, da die aufgerufene Methode nicht den Zustand des Arguments ändert. Durch die Übergabe dieser
Argumente als Verweis wird ein möglicherweise aufwendiges Kopieren vermieden.
Die Angabe von in für Argumente an der Aufrufstelle ist üblicherweise optional. Zwischen der Übergabe eines
Arguments als Wert und der Übergabe als Verweis mithilfe des in -Modifizierers besteht kein semantischer
Unterschied. Der in -Modifizierer an der Aufrufstelle ist optional, da nicht kenntlich gemacht werden muss,
dass sich der Wert des Arguments ändern kann. Explizit geben Sie den in -Modifizierer an der Aufrufstelle an,
wenn Sie sicherstellen möchten, dass das Argument als Verweis und nicht als Wert übergeben wird. Die explizite
Verwendung von in hat die folgenden beiden Auswirkungen:
Erstens wird der Compiler durch die Angabe von in an der Aufrufstelle dazu gezwungen, die Methode mit dem
übereinstimmenden in -Parameter auszuwählen. Wenn dies nicht der Fall ist und sich zwei Methoden nur
durch die Angabe von in unterscheiden, wird die Methode überladen, für die Argumente als Wert übergeben
werden.
Zweitens wird durch in festgelegt, dass ein Argument als Verweis übergeben wird. Das mit in verwendete
Argument muss einen Speicherort darstellen, auf den direkt verwiesen werden kann. Es gelten die gleichen
allgemeinen Regeln für out - und ref -Argumente: Sie können keine Konstanten, normale Eigenschaften oder
andere Ausdrücke, die Werte erzeugen, verwenden. Wird in an der Aufrufstelle weggelassen, wird der
Compiler darüber informiert, dass die Erstellung einer temporären Variable und deren Übergabe als
schreibgeschützter Verweis an die Methode zulässig ist. Der Compiler erstellt in diesem Fall eine temporäre
Variable, um mehrere Einschränkungen im Zusammenhang mit in -Argumenten zu umgehen:
Eine temporäre Variable ermöglicht als Konstanten zur Kompilierzeit in -Parameter.
Eine temporäre Variable ermöglicht Eigenschaften oder andere Ausdrücke für in -Parameter.
Eine temporäre Variable ermöglicht Argumente, bei denen eine implizite Konvertierung des Argumenttyps in
den Parametertyp vorgenommen wird.
In den oben beschriebenen Fällen erstellt der Compiler eine temporäre Variable, die den Wert einer Konstante,
einer Eigenschaft oder eines anderen Ausdrucks speichert.
Das folgende Codebeispiel veranschaulicht diese Regeln:

static void Method(in int argument)


{
// implementation removed
}

Method(5); // OK, temporary variable created.


Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // OK, temporary int created with the value 0
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // passed by readonly reference
Method(in i); // passed by readonly reference, explicitly using `in`

Gehen wir nun davon aus, dass eine weitere Methode verfügbar ist, für die Argumente als Wert übergeben
werden: Wie im folgenden Codebeispiel zu sehen ist, ändern sich die Ergebnisse:

static void Method(int argument)


{
// implementation removed
}

static void Method(in int argument)


{
// implementation removed
}

Method(5); // Calls overload passed by value


Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // Calls overload passed by value.
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // Calls overload passed by value
Method(in i); // passed by readonly reference, explicitly using `in`

Nur beim letzten Methodenaufruf wird das Argument als Verweis übergeben.

NOTE
Im obigen Code wird aus Gründen der Einfachheit int als Argumenttyp verwendet. Da auf den meisten modernen
Computern int nicht größer ist als ein Verweis, ergibt sich kein Vorteil daraus, einen einzelnen int -Wert als
schreibgeschützten Verweis zu übergeben.

Einschränkungen für in -Parameter


Sie können keines der Schlüsselwörter in , ref und out für die folgenden Methodentypen verwenden:
Asynchrone Methoden, die Sie mit dem async-Modifizierer definieren.
Iterator-Methoden, die eine yield return- oder yield break -Anweisung enthalten.
Das erste Argument einer Erweiterungsmethode kann nur dann einen in -Modifizierer verwenden, wenn
dieses Argument eine Struktur ist.
Das erste Argument einer Erweiterungsmethode, wenn es sich bei diesem Argument um einen generischen
Typ handelt (selbst dann, wenn dieser Typ auf eine Struktur beschränkt ist).
Weitere Informationen über den in -Modifizierer und wie er sich von ref und out unterscheidet, finden Sie
im Artikel zu Schreiben von sicherem effizienten Code.

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
ref (C#-Referenz)
04.11.2021 • 8 minutes to read

Das Schlüsselwort ref zeigt an, dass ein Wert durch Verweis übergeben wird. Es wird in vier unterschiedlichen
Kontexten verwendet:
In einer Methodensignatur und in einem Methodenaufruf, um ein Argument an eine Methode als Verweis zu
übergeben. Weitere Informationen finden Sie unter Übergeben eines Arguments als Verweis.
In einer Methodensignatur, um einen Wert an den Aufrufer als Verweis zurückzugeben. Weitere
Informationen finden Sie unter Verweisrückgabewerte.
In einem Memberkörper, um anzugeben, dass ein Verweisrückgabewert lokal als Verweis, den der Aufrufer
ändern möchte, gespeichert wird. Oder um anzuzeigen, dass eine lokale Variable auf einen anderen Wert
durch Verweis zugreift. Weitere Informationen finden Sie unter Lokale ref-Variablen.
In einer struct -Deklaration, um ref struct oder readonly ref struct zu deklarieren. Weitere
Informationen finden Sie im Abschnitt zur ref -Struktur des Artikels Strukturtypen.

Übergeben eines Arguments als Verweis


In der Parameterliste einer Methode gibt das ref -Schlüsselwort an, dass ein Argument als Verweis und nicht
als Wert übergeben wird. Das ref -Schlüsselwort stellt den formalen Parameter als Alias für das Argument dar,
das eine Variable sein muss. Anders ausgedrückt, jede Operation mit dem Parameter wird mit dem Argument
durchgeführt.
Nehmen wir beispielsweise an, der Aufrufer übergibt einen lokalen Variablenausdruck oder einen Ausdruck für
den Zugriff auf ein Arrayelement. Die aufgerufene Methode kann dann das Objekt, auf das sich der ref-
Parameter bezieht, ersetzen. In diesem Fall verweist die lokale Variable des Aufrufers oder das Arrayelement auf
das neue Objekt, wenn die Methode zurückkehrt.

NOTE
Verwechseln Sie nicht das Konzept der Übergabe durch Verweis mit dem Konzept der Verweistypen. Die beiden Konzepte
sind nicht identisch. Ein Methodenparameter kann durch ref geändert werden, unabhängig davon, ob es sich um einen
Werttyp oder ein Verweistyp handelt. Es gibt keine Boxing-Konvertierung eines Werttyps, wenn er durch einen Verweis
übergeben wird.

Um einen ref -Parameter zu verwenden, müssen sowohl die Methodendefinition als auch die aufrufende
Methode explizit das Schlüsselwort ref verwenden, wie im folgenden Beispiel gezeigt. (Mit der Ausnahme,
dass die aufrufende Methode ref auslassen kann, wenn ein COM-Aufruf getätigt wird.)

void Method(ref int refArgument)


{
refArgument = refArgument + 44;
}

int number = 1;
Method(ref number);
Console.WriteLine(number);
// Output: 45

Ein Argument, das an einen ref - oder in -Parameter übergeben wird, muss vor der Übergabe initialisiert
werden. Diese Anforderung unterscheidet sich von den out-Parametern, deren Argumente nicht explizit
initialisiert werden müssen, bevor sie übergeben werden.
Member einer Klasse können keine Signaturen haben, die sich nur durch ref , in oder out voneinander
unterscheiden. Es tritt ein Compilerfehler auf, wenn der einzige Unterschied zwischen beiden Member eines Typs
der ist, dass einer von ihnen über einen ref -Parameter und der andere über einen out - oder in -Parameter
verfügt. Der folgende Code wird z. B. nicht kompiliert.

class CS0663_Example
{
// Compiler error CS0663: "Cannot define overloaded
// methods that differ only on ref and out".
public void SampleMethod(out int i) { }
public void SampleMethod(ref int i) { }
}

Allerdings können Methoden überladen werden, wenn eine Methode einen ref -, in - oder out -Parameter hat
und die andere wie im folgenden Beispiel dargestellt über einen Parameter verfügt, der als Wert übergeben
wird.

class RefOverloadExample
{
public void SampleMethod(int i) { }
public void SampleMethod(ref int i) { }
}

In anderen Situationen, die eine Signaturabstimmung benötigen, z.B. beim Ausblenden oder Überschreiben, sind
in , ref und out Bestandteil der Signatur und passen nicht zueinander.

Eigenschaften sind keine Variablen. Sie sind Methoden und können nicht an ref -Parameter übergeben werden.
Sie können keines der Schlüsselwörter ref , in und out für die folgenden Methodentypen verwenden:
Asynchrone Methoden, die Sie mit dem async-Modifizierer definieren.
Iterator-Methoden, die eine yield return- oder yield break -Anweisung enthalten.
Erweiterungsmethoden unterliegen auch Einschränkungen hinsichtlich der Verwendung dieser Schlüsselwörter:
Das out -Schlüsselwort kann nicht für das erste Argument einer Erweiterungsmethode verwendet werden.
Das ref -Schlüsselwort kann nicht für das erste Argument einer Erweiterungsmethode verwendet werden,
wenn es sich bei dem Argument nicht um eine Struktur handelt oder ein generischer Typ nicht auf eine
Struktur beschränkt ist.
Das in -Schlüsselwort kann nur verwendet werden, wenn das erste Argument eine Struktur ist. Das in -
Schlüsselwort kann nicht für einen generischen Typ verwendet werden – selbst bei einer Beschränkung auf
eine Struktur.

Übergeben eines Arguments per Verweis: Beispiel


In den vorherigen Beispielen wurden Werttypen als Verweis übergeben. Sie können das ref -Schlüsselwort
auch verwenden, um Verweistypen als Verweis zu übergeben. Die Übergabe eines Verweistyps als Verweis
ermöglicht es der aufgerufenen Methode, das Objekt, auf die der Verweisparameter im Aufrufer verweist, zu
ersetzen. Der Speicherort des Objekts wird als Wert des Verweisparameters an die Methode übergeben. Wenn
Sie den Wert am Speicherort des Parameters ändern (um auf ein neues Objekt zu verweisen), ändern Sie auch
den Speicherort, auf den der Aufrufer verweist. Im folgenden Beispiel wird eine Instanz eines Verweistyps als ein
ref -Parameter übergeben.
class Product
{
public Product(string name, int newID)
{
ItemName = name;
ItemID = newID;
}

public string ItemName { get; set; }


public int ItemID { get; set; }
}

private static void ChangeByReference(ref Product itemRef)


{
// Change the address that is stored in the itemRef parameter.
itemRef = new Product("Stapler", 99999);

// You can change the value of one of the properties of


// itemRef. The change happens to item in Main as well.
itemRef.ItemID = 12345;
}

private static void ModifyProductsByReference()


{
// Declare an instance of Product and display its initial values.
Product item = new Product("Fasteners", 54321);
System.Console.WriteLine("Original values in Main. Name: {0}, ID: {1}\n",
item.ItemName, item.ItemID);

// Pass the product instance to ChangeByReference.


ChangeByReference(ref item);
System.Console.WriteLine("Back in Main. Name: {0}, ID: {1}\n",
item.ItemName, item.ItemID);
}

// This method displays the following output:


// Original values in Main. Name: Fasteners, ID: 54321
// Back in Main. Name: Stapler, ID: 12345

Weitere Informationen zum Übergeben von Verweistypen durch einen Wert und durch einen Verweis finden Sie
unter Übergeben von Verweistypparametern.

Verweisrückgabewerte
Verweisrückgabewerte (auch ref-Rückgaben genannt) sind Werte, die von einer Methode an den Aufrufer als
Verweis zurückgegeben werden. Das bedeutet, dass der Aufrufer den von einer Methode zurückgegebenen
Wert ändern kann. Diese Änderung wird im Zustand des Objekts in der aufrufenden Methode wiedergegeben.
Ein Verweisrückgabewert wird definiert durch Verwenden des ref -Schlüsselworts:
In der Methodensignatur. Die folgende Methodensignatur gibt z.B. an, dass die Methode GetCurrentPrice
den Wert Decimal nach Verweis zurückgibt.

public ref decimal GetCurrentPrice()

Zwischen dem return -Token und der Variable, die in einer return -Anweisung in der Methode
zurückgegeben wird. Zum Beispiel:

return ref DecimalArray[0];


Zum Ändern des Zustands des Objekts durch den Aufrufer muss der Verweisrückgabewert in einer Variable
gespeichert werden, die explizit als lokale ref-Variable definiert ist.
Hier ist ein vollständigeres Beispiel für die Verweisrückgabe, das sowohl die Methodensignatur als auch den
Methodentext zeigt.

public static ref int Find(int[,] matrix, Func<int, bool> predicate)


{
for (int i = 0; i < matrix.GetLength(0); i++)
for (int j = 0; j < matrix.GetLength(1); j++)
if (predicate(matrix[i, j]))
return ref matrix[i, j];
throw new InvalidOperationException("Not found");
}

Die aufgerufene Methode kann den Rückgabewert auch als ref readonly deklarieren, um den Wert als Verweis
zurückzugeben, und durchsetzen, dass der aufrufende Code den zurückgegebenen Wert nicht ändern kann. Die
aufrufende Methode kann das Kopieren des Rückgabewerts verhindern, indem sie den Wert in einer lokalen
schreibgeschützten ref-Variablen speichert.
Ein Beispiel finden Sie unter Beispiel für ref-Rückgaben und lokale ref-Variablen.

Lokale ref-Variablen
Eine lokale ref-Variable wird verwendet, um auf Werte zu verweisen, die mit return ref zurückgegeben
werden. Eine lokale ref-Variable kann nicht für einen nicht ref-Rückgabewert initialisiert werden. Das heißt, die
rechte Seite der Initialisierung muss ein Verweis sein. Jede Änderung am Wert der lokalen ref-Variable wird im
Zustand des Objekts wiedergegeben, dessen Methode den Wert als Verweis zurückgegeben hat.
Sie definieren eine lokale ref-Variable, indem Sie das ref -Schlüsselwort an zwei Stellen verwenden:
Vor der Variablendeklaration.
Unmittelbar vor dem Aufruf der Methode, die den Wert durch Verweis zurückgibt.
Die folgende Anweisung definiert z.B. eine lokale ref-Variable, die von der Methode GetEstimatedValue
zurückgegeben wird:

ref decimal estValue = ref Building.GetEstimatedValue();

Auch auf Werte können Sie per Verweis zugreifen. In einigen Fällen erhöht dies die Leistung, da ein
möglicherweise aufwendiger Kopiervorgang vermieden wird. In der folgenden Anweisung wird z. B. gezeigt, wie
eine lokale ref-Variable definiert wird, mit der auf einen Wert verwiesen wird.

ref VeryLargeStruct reflocal = ref veryLargeStruct;

Das Schlüsselwort ref muss in beiden Beispielen an beiden Stellen verwendet werden. Andernfalls generiert
der Compiler den Fehler CS8172 „Eine by-reference-Variable kann nicht mit einem Wert initialisiert werden“.
Beginnend mit C# 7.3 kann die Iterationsvariable der foreach -Anweisung eine lokale ref-Variable oder eine
schreibgeschützte lokale ref-Variable sein. Weitere Informationen finden Sie im Artikel zur foreach-Anweisung.
Ebenfalls beginnend mit C# 7.3 können Sie eine lokale oder schreibgeschützte lokale ref-Variable mit dem ref-
Zuweisungsoperator neu zuweisen.

Lokale schreibgeschützte ref-Variable


Mit einer lokalen schreibgeschützten ref-Variablen können Sie auf Werte verweisen, die von eine Methode oder
Eigenschaft zurückgegeben werden, die ref readonly in der Signatur enthält und return ref verwendet. Eine
ref readonly -Variable kombiniert die Eigenschaften einer lokalen ref -Variablen mit einer readonly -
Variablen: Sie ist ein Alias für den Speicher, dem sie zugewiesen ist, und kann nicht geändert werden.

Beispiel für ref-Rückgaben und lokale ref-Variablen


Im folgenden Beispiel wird eine Book -Klasse mit zwei String-Feldern definiert: Title und Author . Außerdem
wird eine BookCollection -Klasse definiert, die ein privates Array von Book -Objekten enthält. Einzelne
Buchobjekte werden durch Aufrufen der GetBookByTitle -Methode als Verweis zurückgegeben.

public class Book


{
public string Author;
public string Title;
}

public class BookCollection


{
private Book[] books = { new Book { Title = "Call of the Wild, The", Author = "Jack London" },
new Book { Title = "Tale of Two Cities, A", Author = "Charles Dickens" }
};
private Book nobook = null;

public ref Book GetBookByTitle(string title)


{
for (int ctr = 0; ctr < books.Length; ctr++)
{
if (title == books[ctr].Title)
return ref books[ctr];
}
return ref nobook;
}

public void ListBooks()


{
foreach (var book in books)
{
Console.WriteLine($"{book.Title}, by {book.Author}");
}
Console.WriteLine();
}
}

Speichert der Aufrufer den von der GetBookByTitle -Methode zurückgegeben Wert als lokale ref-Variable,
werden Änderungen, die der Aufrufer am Rückgabewert vornimmt, im BookCollection -Objekt wiedergegeben.
Dies wird im folgenden Beispiel gezeigt:
var bc = new BookCollection();
bc.ListBooks();

ref var book = ref bc.GetBookByTitle("Call of the Wild, The");


if (book != null)
book = new Book { Title = "Republic, The", Author = "Plato" };
bc.ListBooks();
// The example displays the following output:
// Call of the Wild, The, by Jack London
// Tale of Two Cities, A, by Charles Dickens
//
// Republic, The, by Plato
// Tale of Two Cities, A, by Charles Dickens

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
Schreiben von sicherem und effizientem Code
Ref-Rückgabetypen und lokale ref-Variablen
Bedingter ref-Ausdruck
Übergeben von Parametern
Methodenparameter
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Modifizierer für out-Parameter (C#-Verweis)
04.11.2021 • 4 minutes to read

Das Schlüsselwort out bewirkt, dass Argumente per Verweis übergeben werden. Es stellt den formalen
Parameter als Alias für das Argument dar, das eine Variable sein muss. Anders ausgedrückt, jede Operation mit
dem Parameter wird mit dem Argument durchgeführt. Dies entspricht dem Schlüsselwort ref, mit Ausnahme
davon, dass bei ref die Variable initialisiert sein muss, bevor sie übergeben wird. Es ähnelt auch dem
Schlüsselwort in. Allerdings lässt in nicht zu, dass die aufgerufene Methode den Argumentwert verändern
kann. Um einen Parameter out zu verwenden, müssen sowohl die Methodendefinition als auch die aufrufende
Methode das Schlüsselwort out explizit verwenden. Zum Beispiel:

int initializeInMethod;
OutArgExample(out initializeInMethod);
Console.WriteLine(initializeInMethod); // value is now 44

void OutArgExample(out int number)


{
number = 44;
}

NOTE
Das Schlüsselwort out kann auch mit einem generischen Typparameter verwendet werden, um anzugeben, dass der
Typparameter kovariant ist. Weitere Informationen zur Verwendung des Schlüsselworts out in diesem Kontext finden Sie
unter out (generischer Modifizierer).

Variablen, die als out -Argumente übergeben wurden, müssen nicht initialisiert werden, bevor sie in einen
Methodenaufruf übergeben werden. Die aufgerufene Methode ist jedoch erforderlich, um einen Wert
zuzuweisen, bevor die Methode zurückgegeben wird.
Die Schlüsselwörter in , ref und out werden nicht als Teil der Methodensignatur zum Zwecke der
Überladungsauflösung betrachtet. Aus diesem Grund können die Methoden nicht überladen werden, wenn der
einzige Unterschied darin besteht, dass eine Methode ein ref - oder in -Argument übernimmt und die andere
ein out -Argument. Der folgende Code wird z. B. nicht kompiliert:

class CS0663_Example
{
// Compiler error CS0663: "Cannot define overloaded
// methods that differ only on ref and out".
public void SampleMethod(out int i) { }
public void SampleMethod(ref int i) { }
}

Überladen ist zwar legal, wenn jedoch eine Methode ein ref -, in - oder out -Argument übernimmt und die
andere über keinen dieser Modifizierer verfügt, gilt Folgendes:
class OutOverloadExample
{
public void SampleMethod(int i) { }
public void SampleMethod(out int i) => i = 5;
}

Der Compiler wählt die beste Überladung aus, indem er die Parametermodifizierer auf der Aufrufsite den im
Methodenaufruf verwendeten Parametermodifizierern zuordnet.
Eigenschaften sind keine Variablen und können daher nicht als out -Parameter übergeben werden.
Sie können keines der Schlüsselwörter in , ref und out für die folgenden Methodentypen verwenden:
Asynchrone Methoden, die Sie mit dem async-Modifizierer definieren.
Iterator-Methoden, die eine yield return- oder yield break -Anweisung enthalten.
Für Erweiterungsmethoden gelten die folgenden Einschränkungen:
Das out -Schlüsselwort kann nicht für das erste Argument einer Erweiterungsmethode verwendet werden.
Das ref -Schlüsselwort kann nicht für das erste Argument einer Erweiterungsmethode verwendet werden,
wenn es sich bei dem Argument nicht um eine Struktur handelt oder ein generischer Typ nicht auf eine
Struktur beschränkt ist.
Das in -Schlüsselwort kann nur verwendet werden, wenn das erste Argument eine Struktur ist. Das in -
Schlüsselwort kann nicht für einen generischen Typ verwendet werden – selbst bei einer Beschränkung auf
eine Struktur.

Deklarieren eines out -Parameters


Das Deklarieren einer Methode mit out -Argumenten ist eine gängige Methode, um mehrere Werte
zurückzugeben. Ab C# 7.0 sollten Sie Werttupel für ähnliche Szenarien berücksichtigen. Im folgenden Beispiel
wird out verwendet, um mit einem Methodenaufruf drei Variablen zurückzugeben. Das dritte Argument ist
NULL zugewiesen. Dadurch können Methoden Werte optional zurückgeben.

void Method(out int answer, out string message, out string stillNull)
{
answer = 44;
message = "I've been returned";
stillNull = null;
}

int argNumber;
string argMessage, argDefault;
Method(out argNumber, out argMessage, out argDefault);
Console.WriteLine(argNumber);
Console.WriteLine(argMessage);
Console.WriteLine(argDefault == null);

// The example displays the following output:


// 44
// I've been returned
// True

Aufrufen einer Methode mit einem out -Argument


In C# 6 und früheren Versionen müssen Sie eine Variable in einer separaten Anweisung deklarieren, bevor Sie es
als ein out -Argument übergeben. Das folgende Beispiel deklariert eine Variable namens number , bevor sie an
die Methode Int32.TryParse übergeben wird, die versucht, eine Zeichenfolge in eine Zahl umzuwandeln.

string numberAsString = "1640";

int number;
if (Int32.TryParse(numberAsString, out number))
Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
// Converted '1640' to 1640

Ab C# 7.0 können Sie in der Argumentliste des Methodenaufrufs anstatt in einer separaten Variablendeklaration
die out -Variable deklarieren. Dies erzeugt kompakteren, lesbaren Code und verhindert auch, dass Sie
versehentlich der Variable vor dem Aufruf der Methode einen Wert zuweisen. Das folgende Beispiel ähnelt dem
vorherigen Beispiel, außer dass es die number -Variable im Aufruf der Methode Int32.TryParse) definiert.

string numberAsString = "1640";

if (Int32.TryParse(numberAsString, out int number))


Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
// Converted '1640' to 1640

Im vorherigen Beispiel ist die number -Variable stark als int typisiert. Sie können auch eine implizit typisierte
lokale Variable deklarieren, wie es im folgenden Beispiel getan wird.

string numberAsString = "1640";

if (Int32.TryParse(numberAsString, out var number))


Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
// Converted '1640' to 1640

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Methodenparameter
Namespace
04.11.2021 • 2 minutes to read

Das namespace -Schlüsselwort wird verwendet, um einen Gültigkeitsbereich zu deklarieren, der eine Gruppe von
verwandten Objekten enthält. Sie können einen Namespace verwenden, um Codeelemente zu organisieren und
global eindeutige Typen zu erstellen.

namespace SampleNamespace
{
class SampleClass { }

interface ISampleInterface { }

struct SampleStruct { }

enum SampleEnum { a, b }

delegate void SampleDelegate(int i);

namespace Nested
{
class SampleClass2 { }
}
}

Mit dateibezogenen Namespacedeklarationen können Sie deklarieren, dass alle Typen in einer Datei sich in
einem einzigen Namespace befinden. Dateibezogene Namespacedeklarationen sind ab C# 10.0 verfügbar. Das
folgende Beispiel ähnelt dem vorherigen Beispiel, verwendet jedoch eine dateibezogene Namespacedeklaration:

using System;

namespace SampleFileScopedNamespace;

class SampleClass { }

interface ISampleInterface { }

struct SampleStruct { }

enum SampleEnum { a, b }

delegate void SampleDelegate(int i);

Das vorherige Beispiel enthält keinen geschachtelten Namespace. Dateibezogene Namespaces können keine
zusätzlichen Namespacedeklarationen enthalten. Es ist nicht möglich, einen geschachtelten Namespace oder
einen zweiten dateibezogenen Namespace zu deklarieren:
namespace SampleNamespace;

class AnotherSampleClass
{
public void AnotherSampleMethod()
{
System.Console.WriteLine(
"SampleMethod inside SampleNamespace");
}
}

namespace AnotherNamespace; // Not allowed!

namespace ANestedNamespace // Not allowed!


{
// declarations...
}

Innerhalb eines Namespace können Sie 0 (null) oder mehr der folgenden Typen deklarieren:
class
interface
struct
enum
delegate
Geschachtelte Namespaces können deklariert werden, jedoch nicht in dateibezogenen
Namespacedeklarationen.
Der Compiler fügt einen Standardnamespace hinzu. Dieser unbenannte Namespace, der manchmal auch als der
globale Namespace bezeichnet wird, ist in jeder Datei vorhanden. Er enthält Deklarationen, die nicht in einem
deklarierten Namespace enthalten sind. Jeder Bezeichner im globalen Namespace ist für die Verwendung in
einem benannten Namespace verfügbar.
Namespaces verfügen implizit über öffentlichen Zugriff. Eine Erläuterung der Zugriffsmodifizierer, die Sie einem
Element in einem Namespace zuweisen können, finden Sie unter Zugriffsmodifizierer.
Es ist möglich, einen Namespace in zwei oder mehr Deklarationen zu definieren. Im folgenden Beispiel werden
beispielsweise zwei Klassen als Teil des MyCompany -Namespace definiert:

namespace MyCompany.Proj1
{
class MyClass
{
}
}

namespace MyCompany.Proj1
{
class MyClass1
{
}
}

Im folgenden Beispiel wird veranschaulicht, wie eine statische Methode in einem geschachtelten Namespace
aufgerufen wird.
namespace SomeNameSpace
{
public class MyClass
{
static void Main()
{
Nested.NestedNameSpaceClass.SayHello();
}
}

// a nested namespace
namespace Nested
{
public class NestedNameSpaceClass
{
public static void SayHello()
{
Console.WriteLine("Hello");
}
}
}
}
// Output: Hello

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Namespaces der C#-Sprachspezifikation. Weitere Informationen
zu dateibezogenen Namespacedeklarationen finden Sie unter Featurespezifikation.

Weitere Informationen
C#-Referenz
C#-Schlüsselwörter
using
using static
Namespacealias-Qualifizierer ::
Namespaces
using (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort using hat drei Hauptverwendungen:


Die Using-Anweisung definiert einen Bereich, an dessen Ende ein Objekt verworfen wird.
Die Using-Anweisung erstellt einen Alias für einen Namespace oder importiert Typen, die in anderen
Namespaces definiert sind.

Weitere Informationen
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Namespaces
extern
using-Anweisung
04.11.2021 • 9 minutes to read

Mit der using -Anweisungen können Sie in einem Namespace definierte Typen verwenden, ohne den
vollqualifizierten Namespace für diese anzugeben. In der unveränderten Form importiert die using -Anweisung
alle Typen aus einem Namespace. Dies wird im folgenden Beispiel veranschaulicht:

using System.Text;

Sie können zwei Modifizierer auf eine using -Anweisung anwenden:


Der Modifizierer global hat die gleiche Wirkung wie das Hinzufügen derselben using -Anweisung zu jeder
Quelldatei im Projekt. Dieser Modifizierer wurde in C# 10.0 eingeführt.
Der Modifizierer static importiert die Member von static und geschachtelte Typen aus einem einzelnen
Typ, anstatt alle Typen in einem Namespace zu importieren. Dieser Modifizierer wurde in C# 6.0 eingeführt.
Sie können beide Modifizierer kombinieren, um die statischen Member aus einem Typ in alle Quelldateien im
Projekt zu importieren.
Sie können auch einen Alias für einen Namespace oder einen Typ mit einer using-Aliasanweisung erstellen.

using Project = PC.MyCompany.Project;

Sie können den Modifizierer global in einer using-Aliasanweisung verwenden.

NOTE
Das using -Schlüsselwort wird auch zum Erstellen von using-Anweisungen verwendet, mit denen sichergestellt wird,
dass IDisposable-Objekte wie Dateien und Schriftarten richtig verarbeitet werden. Weitere Informationen zur using-
Anweisung finden Sie unter using-Anweisung.

Ohne den Modifizierer global gilt eine using -Anweisung nur in der Datei, in der sie verwendet wird.
Die using -Direktive kann an folgenden Stellen erscheinen:
Am Anfang einer Quellcodedatei, vor Namespace- oder Typdeklarationen.
In einem beliebigen Namespace, aber vor allen Namespaces oder Typen, die in diesem Namespace deklariert
sind, es sei denn, der global -Modifizierer wird verwendet. In diesem Fall muss die Richtlinie vor allen
Namespace- und Typdeklarationen stehen.
Andernfalls wird der Compilerfehler CS1529 generiert.
Erstellen Sie eine using -Direktive, um die Typen in einem Namespace zu verwenden, ohne den Namespace
angeben zu müssen. Eine using -Anweisung ermöglicht Ihnen nicht den Zugriff auf Namespaces, die im
angegebenen Namespace geschachtelt sind. Gibt zwei Kategorien von Namespaces: benutzerdefinierte und
systemdefinierte Namespaces. Benutzerdefinierte Namespaces sind Namespaces, die im Code definiert sind.
Eine Liste der systemdefinierten Namespaces finden Sie unter .NET API-Browser.

Globaler Modifizierer
Wenn Sie den Modifizierer global zu einer using -Anweisung hinzufügen, wird using auf alle kompilierten
Dateien angewendet (üblicherweise auf ein ganzes Projekt). Die global using -Anweisung wurde in C# 10.0
hinzugefügt. Die Syntax sieht wie folgt aus:

global using <fully-qualified-namespace>;

Hierbei entspricht fully-qualified-namespace dem vollqualifizierten Namen des Namespace, auf dessen Typen
ohne Angabe des Namespace verwiesen werden kann.
Eine global using-Anweisung kann am Anfang jeder Quellcodedatei eingefügt werden. Alle global using -
Anweisungen in einer Datei müssen vor den folgenden Elementen eingefügt werden:
Alle using -Anweisungen ohne global -Modifizierer.
Alle Namespace- und Typdeklarationen in der Datei.
Sie können jeder Quelldatei global using -Anweisungen hinzufügen. Diese sollten sich jedoch alle an einem Ort
befinden. Die Reihenfolge der global using -Anweisungen spielt weder in einzelnen noch in mehreren Dateien
eine Rolle.
Der Modifizierer global kann mit dem Modifizierer static kombiniert werden. Der Modifizierer global kann
auf eine using-Aliasanweisung angewendet werden. In beiden Fällen gilt die Anweisung für alle Dateien, die
aktuell für die Kompilierung vorgesehen sind. Im folgenden Beispiel ermöglicht using die Verwendung aller
Methoden, die in System.Math deklariert wurden, in allen Dateien des Projekts:

global using static System.Math;

Sie können einen Namespace auch global einschließen, indem Sie Ihrer Projektdatei ein <Using> -Element
hinzufügen. Beispiel: <Using Include="My.Awesome.Namespace" /> . Weitere Informationen finden Sie unter
<Using> -Element.

IMPORTANT
Die C#-Vorlagen für .NET 6 verwenden Anweisungen der obersten Ebene. Ihre Anwendung passt möglicherweise nicht
zum Code in diesem Artikel, wenn Sie bereits ein Upgrade auf die .NET 6-Vorschauversionen durchgeführt haben. Weitere
Informationen finden Sie im Artikel Neue C#-Vorlagen generieren Anweisungen auf oberster Ebene.
Das .NET 6 SDK fügt auch eine Reihe impliziter global using -Anweisungen für Projekte hinzu, die die folgenden SDKs
verwenden:
Microsoft.NET.Sdk
Microsoft.NET.Sdk.Web
Microsoft.NET.Sdk.Worker
Diese impliziten global using -Anweisungen enthalten die gängigsten Namespaces für den Projekttyp.

static-Modifizierer
Die using static -Anweisung nennt einen Typ, auf dessen statische Member und geschachtelte Typen Sie ohne
Angabe eines Typnamens zugreifen können. Die using static -Direktive wurde in C# 6 eingeführt. Die Syntax
sieht wie folgt aus:

using static <fully-qualified-type-name>;


Hierbei steht <fully-qualified-type-name> für den Namen des Typs, auf dessen statische Member und
geschachtelte Typen verwiesen werden kann, ohne einen Typnamen anzugeben. Wenn Sie keinen
vollqualifizierten Typnamen angeben (der vollständige Namespacename mit dem Typnamen), generiert C# den
Compilerfehler CS0246: „The type or namespace name 'type/namespace' could not be found (are you missing a
using directive or an assembly reference?)“ (Der Typ- oder Namespacename "type/namespace" wurde nicht
gefunden (möglicherweise fehlt eine using-Anweisung oder ein Assemblyverweis)).
Die using static -Anweisung gilt für jeden Typ, der über statische Member (oder geschachtelte Typen) verfügt,
auch wenn er ebenfalls über Instanzmember verfügt. Instanzmember können jedoch nur über die Typinstanz
aufgerufen werden.
Sie können auf statische Member eines Typs zugreifen, ohne den Zugriff mit dem Typnamen zu qualifizieren:

using static System.Console;


using static System.Math;
class Program
{
static void Main()
{
WriteLine(Sqrt(3*3 + 4*4));
}
}

Wenn Sie einen statischen Member aufrufen, geben Sie normalerweise den Typnamen zusammen mit dem
Membernamen an. Das wiederholte Eingeben desselben Typnamens zum Aufrufen von Membern dieses Typs
kann zu ausführlichem, verwirrendem Code führen. Die folgende Definition einer Circle -Klasse verweist z. B.
auf mehrere Member der Klasse Math.

using System;

public class Circle


{
public Circle(double radius)
{
Radius = radius;
}

public double Radius { get; set; }

public double Diameter


{
get { return 2 * Radius; }
}

public double Circumference


{
get { return 2 * Radius * Math.PI; }
}

public double Area


{
get { return Math.PI * Math.Pow(Radius, 2); }
}
}

Da nicht mehr jedes Mal explizit auf die Klasse Math verwiesen werden muss, wenn auf einen Member
verwiesen wird, erzeugt die using static -Anweisung deutlich übersichtlicheren Code:
using System;
using static System.Math;

public class Circle


{
public Circle(double radius)
{
Radius = radius;
}

public double Radius { get; set; }

public double Diameter


{
get { return 2 * Radius; }
}

public double Circumference


{
get { return 2 * Radius * PI; }
}

public double Area


{
get { return PI * Pow(Radius, 2); }
}
}

using static importiert nur zugängliche statische Member und geschachtelte Typen, die im angegebenen Typ
deklariert sind. Geerbte Member werden nicht importiert. Sie können aus jedem benannten Typ mit einer
using static -Anweisung importieren, einschließlich Visual Basic-Module. Wenn F#-Funktionen der obersten
Ebene in den Metadaten als statische Member eines benannten Typs angezeigt werden, dessen Name ein
gültiger C#-Bezeichner ist, können die F#-Funktionen importiert werden.
using static macht Erweiterungsmethoden, die im angegebenen Typ deklariert sind, für die
Erweiterungsmethodensuche verfügbar. Die Namen der Erweiterungsmethoden werden jedoch bei nicht
qualifizierten Verweisen im Code nicht in den Gültigkeitsbereich importiert.
Methoden mit dem gleichen Namen, die aus verschiedenen Typen von verschiedenen statischen using static -
Direktiven in der gleichen Kompilierungseinheit oder dem gleichen Namespace importiert wurden, bilden eine
Methodengruppe. Die Überladungsauflösung innerhalb dieser Methodengruppen folgt den normalen C#-
Regeln.
Im folgenden Beispiele wird die using static -Direktive verwendet, um die statischen Member der Klassen
Console, Math und String zugänglich zu machen, ohne deren Typnamen angeben zu müssen.
using System;
using static System.Console;
using static System.Math;
using static System.String;

class Program
{
static void Main()
{
Write("Enter a circle's radius: ");
var input = ReadLine();
if (!IsNullOrEmpty(input) && double.TryParse(input, out var radius)) {
var c = new Circle(radius);

string s = "\nInformation about the circle:\n";


s = s + Format(" Radius: {0:N2}\n", c.Radius);
s = s + Format(" Diameter: {0:N2}\n", c.Diameter);
s = s + Format(" Circumference: {0:N2}\n", c.Circumference);
s = s + Format(" Area: {0:N2}\n", c.Area);
WriteLine(s);
}
else {
WriteLine("Invalid input...");
}
}
}

public class Circle


{
public Circle(double radius)
{
Radius = radius;
}

public double Radius { get; set; }

public double Diameter


{
get { return 2 * Radius; }
}

public double Circumference


{
get { return 2 * Radius * PI; }
}

public double Area


{
get { return PI * Pow(Radius, 2); }
}
}
// The example displays the following output:
// Enter a circle's radius: 12.45
//
// Information about the circle:
// Radius: 12.45
// Diameter: 24.90
// Circumference: 78.23
// Area: 486.95

In diesem Beispiel hätte die using static -Direktive auch auf den Typ Double angewendet werden können.
Durch das Hinzufügen dieser Anweisung kann die Methode TryParse(String, Double) ohne Angabe eines
Typnamens aufgerufen werden. Wenn Sie TryParse ohne Typnamen verwenden, wird allerdings weniger
übersichtlicher Code generiert, da die using static -Anweisungen überprüft werden müssen, um zu
bestimmen, welche TryParse -Methode eines numerischen Typs aufgerufen wird.
using-Alias
Erstellen Sie eine using -Alias-Direktive, um das Qualifizieren eines Bezeichners in einen Namespace oder Typ
zu vereinfachen. In jeder using -Anweisung muss der vollqualifizierte Namespace oder Typ unabhängig von
den davor aufgeführten using -Anweisungen verwendet werden. In der Deklaration einer using -Direktive
kann kein using -Alias verwendet werden. Beispielsweise verursacht das folgende Beispiel einen
Compilerfehler:

using s = System.Text;
using s.RegularExpressions; // Generates a compiler error.

Das folgende Beispiel zeigt, wie Sie einen using -Alias für einen Namespace definieren und verwenden:

namespace PC
{
// Define an alias for the nested namespace.
using Project = PC.MyCompany.Project;
class A
{
void M()
{
// Use the alias
var mc = new Project.MyClass();
}
}
namespace MyCompany
{
namespace Project
{
public class MyClass { }
}
}
}

Eine using-Aliasanweisung kann auf der rechten Seite nicht über einen offenen generischen Typ verfügen. Sie
können zum Beispiel keinen using-Alias für List<T> erstellen, jedoch für List<int> .
Das folgende Beispiel zeigt, wie Sie eine using -Direktive und einen using -Alias für eine Klasse definieren:
using System;

// Using alias directive for a class.


using AliasToMyClass = NameSpace1.MyClass;

// Using alias directive for a generic class.


using UsingAlias = NameSpace2.MyClass<int>;

namespace NameSpace1
{
public class MyClass
{
public override string ToString()
{
return "You are in NameSpace1.MyClass.";
}
}
}

namespace NameSpace2
{
class MyClass<T>
{
public override string ToString()
{
return "You are in NameSpace2.MyClass.";
}
}
}

namespace NameSpace3
{
class MainClass
{
static void Main()
{
var instance1 = new AliasToMyClass();
Console.WriteLine(instance1);

var instance2 = new UsingAlias();


Console.WriteLine(instance2);
}
}
}
// Output:
// You are in NameSpace1.MyClass.
// You are in NameSpace2.MyClass.

Verwenden des Visual Basic-Namespace My


Der Namespace Microsoft.VisualBasic.MyServices ( My in Visual Basic) bietet einen einfachen und intuitiven
Zugriff auf zahlreiche .NET-Klassen. Dies ermöglicht das Erstellen von Code, der mit dem Computer, der
Anwendung, den Einstellungen, den Ressourcen usw. interagiert. Auch wenn er ursprünglich für Visual Basic
entwickelt wurde, kann der MyServices -Namespace auch in C#-Anwendungen verwendet werden.
Weitere Informationen zum Verwenden des MyServices -Namespace in Visual Basic finden Sie unter
Development with My (Entwicklung mit „My“).
Sie müssen einen Verweis auf die Microsoft.VisualBasic.dll-Assembly in Ihrem Projekt hinzufügen. Nicht alle
Klassen des MyServices -Namespace können aus einer C#-Anwendung aufgerufen werden: die
FileSystemProxy-Klasse ist z.B. nicht kompatibel. In diesem Fall können stattdessen die statischen Methoden
verwendet werden, die Teil von FileSystem sind und die außerdem in „VisualBasic.dll“ enthalten sind. So können
Sie z.B. eine derartige Methode verwenden, um ein Verzeichnis zu duplizieren:

// Duplicate a directory
Microsoft.VisualBasic.FileIO.FileSystem.CopyDirectory(
@"C:\original_directory",
@"C:\copy_of_original_directory");

C#-Sprachspezifikation
Weitere Informationen finden Sie unter using-Direktiven in der C#-Sprachspezifikation. Die Sprachspezifikation
ist die verbindliche Quelle für die Syntax und Verwendung von C#.
Weitere Informationen zum global using-Modifizierer finden Sie in der Featurespezifikation für global using-
Anweisungen (C# 10.0).

Weitere Informationen
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Namespaces
Using-Anweisung
using-Anweisung (C#-Referenz)
04.11.2021 • 4 minutes to read

Bietet eine praktische Syntax, die den ordnungsgemäßen Einsatz von IDisposable-Objekten sicherstellt Ab C# 8.0
stellt die using -Anweisung die korrekte Verwendung von IAsyncDisposable-Objekten sicher.

Beispiel
Im folgenden Beispiel wird veranschaulicht, wie Sie die Anweisung using verwenden.

string manyLines=@"This is line one


This is line two
Here is line three
The penultimate line is line four
This is the final, fifth line.";

using (var reader = new StringReader(manyLines))


{
string? item;
do {
item = reader.ReadLine();
Console.WriteLine(item);
} while(item != null);
}

Ab C# 8,0 können Sie die folgende alternative Syntax für die using -Anweisung verwenden, die keine
geschweiften Klammern erfordert:

string manyLines=@"This is line one


This is line two
Here is line three
The penultimate line is line four
This is the final, fifth line.";

using var reader = new StringReader(manyLines);


string? item;
do {
item = reader.ReadLine();
Console.WriteLine(item);
} while(item != null);

Hinweise
File und Font sind Beispiele für verwaltete Typen, die auf nicht verwaltete Ressourcen zugreifen (in diesem Fall
Dateihandles und Gerätekontexte). Es gibt viele andere Arten von nicht verwalteten Ressourcen und
Klassenbibliothekstypen, die sie einschließen. Alle Typen dieser Art müssen die IDisposable-Schnittstelle oder
die IAsyncDisposable-Schnittstelle implementieren.
Wenn die Lebensdauer eines IDisposable -Objekts auf eine einzige Methode beschränkt ist, sollten Sie es in der
using -Anweisung deklarieren und instanziieren. Die using -Anweisung ruft die Methode Dispose
ordnungsgemäß für das Objekt auf. Wenn Sie sie, wie vorher gezeigt, verwenden, führt dies auch dazu, dass das
Objekt den gültigen Bereich verlässt, sobald Dispose aufgerufen wird. Innerhalb des using -Blocks ist das
Objekt schreibgeschützt und kann nicht geändert oder neu zugewiesen werden. Wenn das Objekt
IAsyncDisposable anstelle von IDisposable implementiert, ruft die using -Anweisung die DisposeAsync-
Methode und den awaits -Operator für die zurückgegebene ValueTask-Klasse auf. Weitere Informationen zu
IAsyncDisposable finden Sie unter Implementieren einer DisposeAsync-Methode.
Mit der Anweisung using wird sichergestellt, dass Dispose (oder DisposeAsync) aufgerufen wird, selbst wenn
eine Ausnahme im using -Block auftritt. Sie können das gleiche Ergebnis erzielen, indem Sie das Objekt in einen
try -Block einfügen und dann Dispose (oder DisposeAsync) in einem finally -Block aufrufen. So übersetzt der
Compiler die Anweisung using . Das vorherige Codebeispiel wird zur Kompilierzeit auf den folgenden Code
erweitert (beachten Sie die zusätzlichen geschweiften Klammern zum Erstellen des eingeschränkten
Gültigkeitsbereichs für das Objekt):

string manyLines=@"This is line one


This is line two
Here is line three
The penultimate line is line four
This is the final, fifth line.";

{
var reader = new StringReader(manyLines);
try {
string? item;
do {
item = reader.ReadLine();
Console.WriteLine(item);
} while(item != null);
} finally
{
reader?.Dispose();
}
}

Die neuere Syntax der using -Anweisung wird in ähnlichen Code übersetzt. Der try -Block wird geöffnet, in
dem die Variable deklariert wird. Der finally -Block wird am Ende des einschließenden Blocks hinzugefügt, in
der Regel am Ende einer Methode.
Weitere Informationen über die Anweisung try - finally finden Sie im Artikel zu try-finally.
Mehrere Instanzen eines Typs können wie im folgenden Beispiel gezeigt in einer einzelnen using -Anweisung
deklariert werden. Beachten Sie, dass Sie Variablen mit impliziten Typen ( var ) nicht verwenden können, wenn
Sie mehrere Variablen in einer einzelnen Anweisung deklarieren:
string numbers=@"One
Two
Three
Four.";
string letters=@"A
B
C
D.";

using (StringReader left = new StringReader(numbers),


right = new StringReader(letters))
{
string? item;
do {
item = left.ReadLine();
Console.Write(item);
Console.Write(" ");
item = right.ReadLine();
Console.WriteLine(item);
} while(item != null);
}

Sie können wie im folgenden Beispiel gezeigt auch mehrere Deklarationen desselben Typs mithilfe der neuen
Syntax kombinieren, die mit C# 8 eingeführt wurde:

string numbers=@"One
Two
Three
Four.";
string letters=@"A
B
C
D.";

using StringReader left = new StringReader(numbers),


right = new StringReader(letters);
string? item;
do {
item = left.ReadLine();
Console.Write(item);
Console.Write(" ");
item = right.ReadLine();
Console.WriteLine(item);
} while(item != null);

Sie können das Ressourcenobjekt instanziieren und die Variable an die using -Anweisung übergeben. Diese
Vorgehensweise wird jedoch nicht empfohlen. In diesem Fall verbleibt das Objekt im Gültigkeitsbereich,
nachdem das Steuerelement den using -Block verlassen hat, obwohl es wahrscheinlich keinen Zugriff auf
dessen nicht verwaltete Ressourcen hat. Das heißt, dass es nicht mehr vollständig initialisiert wird. Wenn Sie
versuchen, das Objekt außerhalb des using -Blocks zu verwenden, riskieren Sie, dass eine Ausnahme ausgelöst
wird. Aus diesem Grund ist es besser, das Objekt in der using -Anweisung zu instanziieren und dessen Bereich
auf den using -Block zu begrenzen.
string manyLines=@"This is line one
This is line two
Here is line three
The penultimate line is line four
This is the final, fifth line.";

var reader = new StringReader(manyLines);


using (reader)
{
string? item;
do {
item = reader.ReadLine();
Console.WriteLine(item);
} while(item != null);
}
// reader is in scope here, but has been disposed

Weitere Informationen zum Verwerfen von IDisposable -Objekten finden Sie unter Verwenden von Objekten,
die IDisposable implementieren.

C#-Sprachspezifikation
Weitere Informationen finden Sie unter Die using-Anweisung in der C#-Sprachspezifikation. Die
Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
using-Direktive
Garbage Collection
Verwenden von Objekten, die IDisposable implementieren
IDisposable-Schnittstelle
using-Anweisung in C# 8.0
extern-Alias (C#-Referenz)
04.11.2021 • 2 minutes to read

Sie müssen möglicherweise auf zwei Versionen von Assemblys verweisen, die denselben vollqualifizierten
Namen besitzen. Beispielsweise müssen Sie möglicherweise zwei oder mehr Versionen einer Assembly in
derselben Anwendung verwenden. Indem Sie einen externen Assemblyalias verwenden, können die
Namespaces jeder Assembly in Namespaces auf Stammebene, benannt durch den Alias, umschlossen werden,
was es ihnen ermöglicht, von derselben Datei verwendet zu werden.

NOTE
Das extern-Schlüsselwort dient außerdem als Methodenmodifizierer, der eine Methode deklariert, die in nicht verwaltetem
Code geschrieben wurde.

Um auf zwei Assemblys mit demselben vollqualifizierten Typnamen zu verweisen, muss ein Alias in einer
Befehlszeile wie folgt angegeben werden:
/r:GridV1=grid.dll

/r:GridV2=grid20.dll

Dies erstellt die externen Aliase GridV1 und GridV2 . Um diese Aliase aus einem Programm heraus zu
verwenden, verweisen Sie mithilfe des extern -Schlüsselworts auf sie. Beispiel:
extern alias GridV1;

extern alias GridV2;

Jede externe Aliasdeklaration führt einen zusätzlichen Namespace auf Stammebene ein, parallel zum (aber nicht
innerhalb des) globalen Namespace. Daher kann mithilfe des vollqualifizierten Namens, der als Stamm des
entsprechenden Namespacealias dient, auf Typen jeder Assembly eindeutig verwiesen werden.
Im vorherigen Beispiel wäre GridV1::Grid das Steuerelement von grid.dll , und GridV2::Grid wäre das
Steuerelement von grid20.dll .

Verwenden von Visual Studio


Wenn Sie Visual Studio verwenden, können Aliase auf ähnliche Weise bereitgestellt werden.
Fügen Sie Ihrem Projekt in Visual Studio Verweise auf grid.dll und grid20.dll hinzu. Öffnen Sie eine Registerkarte
mit Eigenschaften, und ändern Sie die Aliase von „global“ in „GridV1“ bzw. „GridV2“.
Verwenden Sie diese Aliase auf die gleiche Weise wie oben:

extern alias GridV1;

extern alias GridV2;

Jetzt können Sie einen Alias für einen Namespace oder Typ erstellen, indem Sie eine using-alias-Anweisung
verwenden. Weitere Information finden Sie unter using-Anweisung.
using Class1V1 = GridV1::Namespace.Class1;

using Class1V2 = GridV2::Namespace.Class1;

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
:: Operator
References (C#-Compileroptionen)
new-Einschränkung (C#-Referenz)
04.11.2021 • 2 minutes to read

Die new -Einschränkung gibt an, dass ein Typargument in einer generischen Klassendeklaration über einen
öffentlichen parameterlosen Konstruktor verfügen muss. Der Typ kann nicht abstrakt sein, um die new -
Einschränkung zu verwenden.
Wenden Sie die new -Einschränkung auf einen Typparameter an, wenn Ihre generische Klasse neue Instanzen
desselben Typs erstellt, wie im folgenden Beispiel gezeigt wird:

class ItemFactory<T> where T : new()


{
public T GetNewItem()
{
return new T();
}
}

Beim Verwenden der new() -Einschränkung mit anderen Einschränkungen muss sie zuletzt angegeben werden:

public class ItemFactory2<T>


where T : IComparable, new()
{ }

Weitere Informationen finden Sie unter Einschränkungen für Typparameter.


Sie können das Schlüsselwort new auch verwenden, um eine Instanz eines Typs zu erstellen oder als
Memberdeklarationsmodifizierer.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Einschränkungen für Typparameter der C#-Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Generics
where (generischer Typconstraint) (C#-Referenz)
04.11.2021 • 4 minutes to read

In einer generischen Typdefinition wird die where -Klausel verwendet, um Constraints für Typen anzugeben, die
als Argumente für einen Typenparameter in generischen Typen, Methoden, Delegaten oder lokalen Funktionen
verwendet werden können. Constraints können Schnittstellen und Basisklassen angeben oder einen
generischen Typ als Verweis-, Wert- oder nicht verwalteten Typ anfordern. Sie deklarieren die Funktionen, die
das Typargument aufweisen muss.
So können Sie beispielsweise eine generische Klasse erstellen, AGenericClass , deren Typparameter T die
Schnittstelle IComparable<T> implementiert:

public class AGenericClass<T> where T : IComparable<T> { }

NOTE
Weitere Informationen über die where-Klausel in einem Abfrageausdruck finden Sie unter where-Klausel.

Die where -Klausel kann auch einen Basisklassenconstraint enthalten. Der Basisklassenconstraint gibt an, dass
ein Typ, der als Typargument für den generischen Typ verwendet wird, über die angegebene Klasse als
Basisklasse verfügen oder diese Basisklasse sein muss. Wenn ein Basisklassenconstraint verwendet wird, muss
er vor jedem anderen Constraint für den Typparameter angezeigt werden. Einige Typen sind nicht als
Basisklassenconstraints zulässig: Object, Array und ValueType. Vor C# 7.3 waren Enum, Delegate und
MulticastDelegate ebenfalls nicht als Basisklassenconstraints zulässig. Das folgende Beispiel zeigt die Typen, die
jetzt als Basisklasse angegeben werden können:

public class UsingEnum<T> where T : System.Enum { }

public class UsingDelegate<T> where T : System.Delegate { }

public class Multicaster<T> where T : System.MulticastDelegate { }

In einem Nullable-Kontext in C# 8.0 und höher wird die NULL-Zulässigkeit des Basisklassentyps erzwungen.
Wenn die Basisklasse ein Non-Nullable-Typ ist (z. B. Base ), muss das Typargument ein Non-Nullable-Typ sein.
Ist die Basisklasse ein Nullable-Typ (z. B. Base? ), muss das Typargument ein Nullable- oder Non-Nullable-
Verweistyp sein. Der Compiler gibt eine Warnung aus, wenn das Typargument ein Nullable-Verweistyp ist und
die Basisklasse ein Non-Nullable-Typ.
Die where -Klausel kann angeben, ob der Typ class oder struct ist. Aufgrund des struct -Constraints ist die
Angabe eines Basisklassenconstraints von System.ValueType nicht notwendig. Der System.ValueType -Typ darf
nicht als Basisklassenconstraint verwendet werden. Im folgenden Beispiel werden die class - und struct -
Constraints dargestellt:

class MyClass<T, U>


where T : class
where U : struct
{ }

In einem Nullable-Kontext in C# 8.0 und höher erfordert der class -Constraint einen Non-Nullable-Verweistyp.
Um Nullable-Verweistypen zuzulassen, verwenden Sie den class? -Constraint, der sowohl Nullable- als auch
Non-Nullable-Verweistypen zulässt.
Die where -Klausel kann den notnull -Constraint enthalten. Der notnull -Constraint begrenzt den
Typparameter auf Nicht-Nullable-Typen. Bei diesem Typ kann es sich um einen Werttyp oder einen Non-
Nullable-Verweistyp handeln. Der notnull -Constraint ist ab C 8.0 für Code verfügbar, der in einem
nullable enable -Kontext kompiliert wird. Im Gegensatz zu anderen Constraints generiert der Compiler eine
Warnung statt eines Fehlers, wenn ein Typargument den notnull -Constraint verletzt. Warnungen werden nur in
einem nullable enable -Kontext generiert.
Das Addition von Nullable-Verweistypen führt zu einer potenziellen Mehrdeutigkeit in der Bedeutung von T?
in generischen Methoden. Wenn T ein struct ist, ist T? identisch mit System.Nullable<T>. Wenn jedoch T
ein Verweistyp ist, bedeutet T? , dass null ein gültiger Wert ist. Die Mehrdeutigkeit entsteht, da
überschreibende Methoden keine Einschränkungen enthalten können. Die neue default Einschränkung löst
diese Mehrdeutigkeit auf. Sie fügen sie hinzu, wenn eine Basisklasse oder Schnittstelle zwei Überladungen einer
Methode deklariert, eine, die die struct Einschränkung angibt, und eine, für die weder die struct - oder
class -Einschränkung angewendet wurde:

public abstract class B


{
public void M<T>(T? item) where T : struct { }
public abstract void M<T>(T? item);

Sie verwenden die default -Einschränkung, um anzugeben, dass die abgeleitete Klasse die Methode ohne die
Einschränkung in der abgeleiteten Klasse oder explizite Schnittstellenimplementierung überschreibt. Sie ist nur
für Methoden gültig, die Basismethoden oder explizite Schnittstellenimplementierungen überschreiben:

public class D : B
{
// Without the "default" constraint, the compiler tries to override the first method in B
public override void M<T>(T? item) where T : default { }
}

IMPORTANT
Generische Deklarationen, die den notnull -Constraint enthalten, können in einem Kontext verwendet werden, in dem
nicht bekannt ist, ob NULL-Werte zugelassen sind, aber der Compiler erzwingt den Constraint nicht.

#nullable enable
class NotNullContainer<T>
where T : notnull
{
}
#nullable restore

Die where -Klausel kann auch einen unmanaged -Constraint einschließen. Der unmanaged -Constraint schränkt den
Typparameter auf Typen ein, die als nicht verwaltete Typen bekannt sind. Der unmanaged -Constraint erleichtert
das Schreiben von Interop-Code in C# auf niedriger Ebene. Dieser Constraint ermöglicht wiederverwendbare
Routinen für alle nicht verwalteten Typen. Der unmanaged -Constraint kann nicht mit dem class - oder struct -
Constraint kombiniert werden. Der unmanaged -Constraint erzwingt, dass der Typ struct sein muss:
class UnManagedWrapper<T>
where T : unmanaged
{ }

Die where -Klausel kann auch einen new() -Konstruktorconstraint einschließen. Dieser Constraint ermöglicht
das Erstellen einer Instanz eines Typparameters unter Verwendung des new -Operators. Der new()-Constraint
informiert den Compiler, dass jedes angegebene Typargument über einen zugänglichen parameterlosen
Konstruktor verfügen muss. Zum Beispiel:

public class MyGenericClass<T> where T : IComparable<T>, new()


{
// The following line is not possible without new() constraint:
T item = new T();
}

Der new() -Constraint wird in der where -Klausel als Letztes angezeigt. Der new() -Constraint kann nicht mit
dem struct - oder unmanaged -Constraint kombiniert werden. Alle Typen, die diese Constraints erfüllen, müssen
einen zugänglichen parameterlosen Konstruktor aufweisen, wodurch der new() -Constraint redundant wird.
Bei mehreren Typparametern müssen Sie für jeden davon eine eigene where -Klausel verwenden, z.B.:

public interface IMyInterface { }

namespace CodeExample
{
class Dictionary<TKey, TVal>
where TKey : IComparable<TKey>
where TVal : IMyInterface
{
public void Add(TKey key, TVal val) { }
}
}

Sie können auch Constraints wie folgt an Typparameter generischer Methoden anfügen:

public void MyMethod<T>(T t) where T : IMyInterface { }

Beachten Sie, dass die Syntax zum Beschreiben der Parameterconstraints für Delegaten mit der Syntax von
Methoden identisch ist:

delegate T MyDelegate<T>() where T : new();

Informationen zu generischen Delegaten finden Sie unter Generic Delegates (Generische Delegaten).
Weitere Informationen zur Syntax und der Verwendung von Constraints finden Sie unter Constraints für
Typparameter.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
Einführung in Generics
new-Constraint
Constraints für Typparameter
base (C#-Referenz)
04.11.2021 • 2 minutes to read

Das base -Schlüsselwort wird verwendet, um aus einer abgeleiteten Klasse auf die Member der Basisklasse
zuzugreifen:
Rufen Sie eine Methode der Basisklasse auf, die durch eine andere Methode überschrieben wurde.
Geben Sie an, welcher Konstruktor der Basisklasse beim Erstellen von Instanzen der abgeleiteten Klasse
aufgerufen werden soll.
Zugriff auf eine Basisklasse ist nur in einem Konstruktor, einer Instanzenmethode oder einem
Instanzeneigenschaften-Accessor zulässig.
Die Nutzung eines base -Schlüsselworts innerhalb einer statischen Methode ist ein Fehler.
Die Basisklasse, auf die zugegriffen wird, ist die Basisklasse, die in der Klassendeklaration angegeben ist. Wenn
Sie z.B. class ClassB : ClassA angeben, wird von ClassB auf die Member von ClassA unabhängig von der
Basisklasse von ClassA zugegriffen.

Beispiel 1
In diesem Beispiel verfügen sowohl die Basisklasse Person als auch die abgeleitete Klasse Employee über eine
Methode mit dem Namen Getinfo . Mithilfe des base -Schlüsselworts ist es möglich, die Getinfo -Methode der
Basisklasse aus der abgeleiteten Klasse abzurufen.
public class Person
{
protected string ssn = "444-55-6666";
protected string name = "John L. Malgraine";

public virtual void GetInfo()


{
Console.WriteLine("Name: {0}", name);
Console.WriteLine("SSN: {0}", ssn);
}
}
class Employee : Person
{
public string id = "ABC567EFG";
public override void GetInfo()
{
// Calling the base class GetInfo method:
base.GetInfo();
Console.WriteLine("Employee ID: {0}", id);
}
}

class TestClass
{
static void Main()
{
Employee E = new Employee();
E.GetInfo();
}
}
/*
Output
Name: John L. Malgraine
SSN: 444-55-6666
Employee ID: ABC567EFG
*/

Weitere Beispiele finden Sie unter new, virtual und override.

Beispiel 2
In diesem Beispiel wird veranschaulicht, wie der aufgerufene Konstruktor der Basisklasse beim Erstellen von
Instanzen einer abgeleiteten Klasse angegeben wird.
public class BaseClass
{
int num;

public BaseClass()
{
Console.WriteLine("in BaseClass()");
}

public BaseClass(int i)
{
num = i;
Console.WriteLine("in BaseClass(int i)");
}

public int GetNum()


{
return num;
}
}

public class DerivedClass : BaseClass


{
// This constructor will call BaseClass.BaseClass()
public DerivedClass() : base()
{
}

// This constructor will call BaseClass.BaseClass(int i)


public DerivedClass(int i) : base(i)
{
}

static void Main()


{
DerivedClass md = new DerivedClass();
DerivedClass md1 = new DerivedClass(1);
}
}
/*
Output:
in BaseClass()
in BaseClass(int i)
*/

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
this
this (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort this verweist auf die aktuelle Instanz der Klasse und wird auch als Modifizierer des ersten
Parameters einer Erweiterungsmethode verwendet.

NOTE
Dieser Artikel behandelt die Verwendung von this mit Klasseninstanzen. Weitere Informationen zu seiner Verwendung
in Erweiterungsmethoden finden Sie unter Erweiterungsmethoden.

Häufige Verwendungen von this sind wie folgt:


Zum Qualifizieren von Membern, die durch ähnliche Namen ausgeblendet werden, wie z.B.:

public class Employee


{
private string alias;
private string name;

public Employee(string name, string alias)


{
// Use this to qualify the members of the class
// instead of the constructor parameters.
this.name = name;
this.alias = alias;
}
}

Zum Übergeben eines Objekts als ein Parameter an eine andere Methode, wie z.B.:

CalcTax(this);

Zum Deklarieren von Indexern, wie z.B.:

public int this[int param]


{
get { return array[param]; }
set { array[param] = value; }
}

Statische Memberfunktionen haben keinen this -Zeiger, da sie auf Klassenebene und nicht als Teil eines Objekts
existieren. Es ist ein Fehler, in einer statischen Methode auf this zu verweisen.

Beispiel
In diesem Beispiel wird this verwendet, um die Employee -Klassenmember name und alias zu qualifizieren,
die von ähnlichen Namen ausgeblendet werden. Er wird auch verwendet, um ein Objekt an die Methode
CalcTax zu übergeben, die zu einer anderen Klasse gehört.
class Employee
{
private string name;
private string alias;
private decimal salary = 3000.00m;

// Constructor:
public Employee(string name, string alias)
{
// Use this to qualify the fields, name and alias:
this.name = name;
this.alias = alias;
}

// Printing method:
public void printEmployee()
{
Console.WriteLine("Name: {0}\nAlias: {1}", name, alias);
// Passing the object to the CalcTax method by using this:
Console.WriteLine("Taxes: {0:C}", Tax.CalcTax(this));
}

public decimal Salary


{
get { return salary; }
}
}

class Tax
{
public static decimal CalcTax(Employee E)
{
return 0.08m * E.Salary;
}
}

class MainClass
{
static void Main()
{
// Create objects:
Employee E1 = new Employee("Mingda Pan", "mpan");

// Display results:
E1.printEmployee();
}
}
/*
Output:
Name: Mingda Pan
Alias: mpan
Taxes: $240.00
*/

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
base
Methoden
null (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort null ist ein Literal, das einen NULL-Verweis darstellt, der auf kein Objekt verweist. null ist
der Standardwert einer Verweistypvariablen. Normale Werttypen dürfen nicht NULL sein, mit Ausnahme von
Nullable-Werttypen.
Im Folgenden Beispiel werden einige Verhalten des null -Schlüsselworts gezeigt:
class Program
{
class MyClass
{
public void MyMethod() { }
}

static void Main(string[] args)


{
// Set a breakpoint here to see that mc = null.
// However, the compiler considers it "unassigned."
// and generates a compiler error if you try to
// use the variable.
MyClass mc;

// Now the variable can be used, but...


mc = null;

// ... a method call on a null object raises


// a run-time NullReferenceException.
// Uncomment the following line to see for yourself.
// mc.MyMethod();

// Now mc has a value.


mc = new MyClass();

// You can call its method.


mc.MyMethod();

// Set mc to null again. The object it referenced


// is no longer accessible and can now be garbage-collected.
mc = null;

// A null string is not the same as an empty string.


string s = null;
string t = String.Empty; // Logically the same as ""

// Equals applied to any null object returns false.


bool b = (t.Equals(s));
Console.WriteLine(b);

// Equality operator also returns false when one


// operand is null.
Console.WriteLine("Empty string {0} null string", s == t ? "equals": "does not equal");

// Returns true.
Console.WriteLine("null == null is {0}", null == null);

// A value type cannot be null


// int i = null; // Compiler error!

// Use a nullable value type instead:


int? i = null;

// Keep the console window open in debug mode.


System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
Siehe auch
C#-Referenz
C#-Schlüsselwörter
Standardwerte der C#-Typen
Nothing (Visual Basic)
bool (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort vom Typ bool ist ein Alias für den .NET-Strukturtyp System.Boolean, der einen booleschen
Wert ( true oder false ) darstellt.
Um logische Operationen mit Werten vom Typ bool durchzuführen, verwenden Sie die booleschen
Logikoperatoren. Der Typ bool ist der Ergebnistyp von Vergleichs- und Gleichheitsoperatoren. Ein bool -
Ausdruck kann ein steuernder bedingter Ausdruck in if-, do-, while- und for-Anweisungen und im bedingten
Operator ?: sein.
Der Standardwert des Typs bool ist false .

Literale
Sie können die Literale true und false verwenden, um eine bool -Variable zu initialisieren oder einen bool -
Wert zu übergeben:

bool check = true;


Console.WriteLine(check ? "Checked" : "Not checked"); // output: Checked

Console.WriteLine(false ? "Checked" : "Not checked"); // output: Not checked

Dreiwertige boolesche Logik


Verwenden Sie den Nullable-Typ bool? , wenn Sie dreiwertige Logik unterstützen müssen (wenn Sie
beispielsweise mit Datenbanken arbeiten, die einen dreiwertigen booleschen Typ unterstützen). Für die bool? -
Operanden unterstützen die vordefinierten & - und | -Operatoren die dreiwertige Logik. Weitere
Informationen finden Sie im Abschnitt Boolesche logische Operatoren, die NULL-Werte zulassen im Artikel
Boolesche logische Operatoren.
Weitere Informationen zu Nullable-Werttypen finden Sie unter Nullable-Werttypen.

Konvertierungen
C# bietet nur zwei Konvertierungen, die den Typ bool beinhalten. Dabei handelt es sich um eine implizite
Konvertierung in den entsprechenden Nullable-Typ bool? und eine explizite Konvertierung aus dem bool? -
Typ. .NET bietet jedoch zusätzliche Methoden, die Sie verwenden können, um in den oder aus dem Typ bool zu
konvertieren. Weitere Informationen finden Sie im Abschnitt Konvertieren in boolesche Werte und aus
booleschen Werten auf der System.Boolean-API-Referenzseite.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Der Typ „bool“ in der C#-Sprachspezifikation.

Weitere Informationen
C#-Referenz
Werttypen
true- und false-Operatoren
default (C#-Referenz)
04.11.2021 • 2 minutes to read

Das default -Schlüsselwort kann in den folgenden Kontexten verwendet werden:


Zum Angeben des Standardfalls in der switch -Anweisung.
Als Standardoperator oder -literal zum Erzeugen des Standardwerts eines Typs.
Als default Typeinschränkung für eine generische Methodenüberschreibung oder explizite
Schnittstellenimplementierungen.

Weitere Informationen
C#-Referenz
C#-Schlüsselwörter
Kontextabhängige Schlüsselwörter (C#-Referenz)
04.11.2021 • 2 minutes to read

Ein Kontextschlüsselwort wird verwendet, um eine spezifische Bedeutung im Code bereitzustellen, es ist jedoch
kein reserviertes Wort in C#. In diesem Abschnitt werden die folgenden Kontextschlüsselwörter eingeführt:

SC H L ÜSSEL W O RT B ESC H REIB UN G

add Definiert einen benutzerdefinierten Ereignisaccessor, der


aufgerufen wird, wenn der Clientcode das Ereignis abonniert

and Erstellt ein Muster, das übereinstimmt, wenn beide


geschachtelte Muster übereinstimmen.

async Zeigt an, dass die geänderte Methode, der Lambdaausdruck


oder die anonyme Methode asynchron ist.

await Hält eine async-Methode an, bis ein erwarteter Task


abgeschlossen ist

dynamic Definiert einen Verweistyp, der Vorgänge ermöglicht, in


denen er auftritt, um die Typüberprüfung zur Kompilierzeit
zu umgehen

get Definiert eine Accessormethode für eine Eigenschaft oder


einen Indexer

global Alias des globalen Namespace, der andernfalls unbenannt ist.

init Definiert eine Accessormethode für eine Eigenschaft oder


einen Indexer

nint Definiert einen Integerdatentyp mit nativer Größe.

not Erstellt ein Muster, das übereinstimmt, wenn das negierte


Muster nicht übereinstimmt.

nuint Definiert einen Integerdatentyp mit nativer Größe ohne


Vorzeichen.

or Erstellt ein Muster, das übereinstimmt, wenn eines der


geschachtelten Muster übereinstimmt.

partial Definiert partielle Klassen, Strukturen und Schnittstellen


innerhalb derselben Kompilierungseinheit

record Wird verwendet, um einen Datensatztyp zu definieren.

remove Definiert einen benutzerdefinierten Ereignisaccessor, der


aufgerufen wird, wenn der Clientcode das Abonnement vom
Ereignis aufhebt
SC H L ÜSSEL W O RT B ESC H REIB UN G

set Definiert eine Accessormethode für eine Eigenschaft oder


einen Indexer

value Wird verwendet, um Accessoren festzulegen und um


Ereignishandler hinzuzufügen oder zu entfernen

var Aktiviert den Typ einer Variable, die im Methodenbereich


deklariert wird, um vom Compiler bestimmt zu werden

when Gibt eine Filterbedingung für einen catch -Block oder


case -Bezeichnung einer switch -Anweisung an

where Fügt einer generischen Deklaration eine Einschränkung hinzu


(Siehe auch where).

yield Wird in einem Iteratorblock verwendet, um einen Wert auf


das Enumeratorobjekt zurückzugeben oder um das Ende der
Iteration zu signalisieren

Alle in C# 3.0 eingeführten Abfrageschlüsselwörter sind auch kontextabhängig. Weitere Informationen finden
Sie unter Abfrageschlüsselwörter (C#-Referenz).

Siehe auch
C#-Referenz
C#-Schlüsselwörter
C#-Operatoren und -Ausdrücke
add (C#-Referenz)
04.11.2021 • 2 minutes to read

Das kontextabhängige Schlüsselwort add definiert einen benutzerdefinierten Ereignisaccessor, der aufgerufen
wird, wenn der Clientcode ihr Ereignis abonniert. Wenn Sie einen benutzerdefinierten add -Accessor
bereitstellen, müssen Sie auch einen remove-Accessor angeben.

Beispiel
Im folgenden Beispiel wird ein Ereignis gezeigt, dass über benutzerdefinierte add - und remove-Accessoren
verfügt. Das vollständige Beispiel finden Sie unter Vorgehensweise: Implementieren von
Schnittstellenereignissen.

class Events : IDrawingObject


{
event EventHandler PreDrawEvent;

event EventHandler IDrawingObject.OnDraw


{
add => PreDrawEvent += value;
remove => PreDrawEvent -= value;
}
}

Sie müssen normalerweise keine eigenen benutzerdefinierten Ereignisaccessoren bereitstellen. Die Accessoren,
die automatisch vom Compiler generiert werden, wenn Sie ein Ereignis deklarieren, sind in den meisten
Szenarios ausreichend.

Siehe auch
Ereignisse
get (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort get definiert eine Accessor methode in einer Eigenschaft oder einem Indexer, die den
Eigenschaftswert oder das Indexer-Element zurückgibt. Weitere Informationen finden Sie unter Properties
(Eigenschaften), Auto-Implemented Properties (Automatisch implementierte Eigenschaften) und Indexers
(Indexer).
Im folgenden Beispiel werden ein get - und ein set -Accessor für eine Eigenschaft namens Seconds definiert.
Im Beispiel wird ein privates Feld mit dem Namen _seconds verwendet, um den Eigenschaftswert zu
unterstützen.

class TimePeriod
{
private double _seconds;

public double Seconds


{
get { return _seconds; }
set { _seconds = value; }
}
}

Der get -Accessor besteht häufig aus einer einzelnen Anweisung, die einen Wert zurückgibt (wie im vorherigen
Beispiel gezeigt). Ab C# 7.0 können Sie die get -Zugriffsmethode als Ausdruckskörpermember implementieren.
Im folgenden Beispiel wird sowohl der get - als auch der set -Accessor als Ausdruckskörpermember
implementiert.

class TimePeriod
{
private double _seconds;

public double Seconds


{
get => _seconds;
set => _seconds = value;
}
}

In einfachen Fällen, in denen der get - und der set -Accessor einer Eigenschaft nichts anderes durchführen als
das Festlegen oder Abrufen eines Wertes in einem privaten Unterstützungsfeld, können Sie die Vorteile der
Unterstützung von automatisch implementierten Eigenschaften durch einen C#-Compiler nutzen. Im folgenden
Beispiel wird Hours als automatisch implementierte Eigenschaft implementiert.

class TimePeriod2
{
public double Hours { get; set; }
}

C#-Programmiersprachenspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Eigenschaften
init (C#-Referenz)
04.11.2021 • 2 minutes to read

In C# 9 und höher wird mit dem init -Schlüsselwort eine Zugriffsmethode in einer Eigenschaft oder einem
Indexer definiert. Ein reiner init-Setter weist der Eigenschaft oder dem Indexerelement nur während der
Objekterstellung einen Wert zu. Weitere Informationen und Beispiele finden Sie unter Eigenschaften,
Automatisch implementierte Eigenschaften und Indexers.
Im folgenden Beispiel werden eine get - und eine init -Zugriffsmethode für eine Eigenschaft namens Seconds
definiert. Im Beispiel wird ein privates Feld mit dem Namen _seconds verwendet, um den Eigenschaftswert zu
unterstützen.

class InitExample
{
private double _seconds;

public double Seconds


{
get { return _seconds; }
init { _seconds = value; }
}
}

Der init -Accessor besteht häufig aus einer einzelnen Anweisung, die einen Wert zurückgibt (wie im
vorherigen Beispiel gezeigt). Sie können die init -Zugriffsmethode als Ausdruckskörpermember
implementieren. Im folgenden Beispiel wird sowohl der get - als auch der init -Accessor als
Ausdruckskörpermember implementiert.

class InitExampleExpressionBodied
{
private double _seconds;

public double Seconds


{
get => _seconds;
init => _seconds = value;
}
}

In einfachen Fällen, in denen der get - und der init -Accessor einer Eigenschaft nichts anderes durchführen als
das Festlegen oder Abrufen eines Wertes in einem privaten Unterstützungsfeld, können Sie die Vorteile der
Unterstützung von automatisch implementierten Eigenschaften durch einen C#-Compiler nutzen. Im folgenden
Beispiel wird Hours als automatisch implementierte Eigenschaft implementiert.

class InitExampleAutoProperty
{
public double Hours { get; init; }
}

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Eigenschaften
Partieller Typ (C#-Referenz)
04.11.2021 • 2 minutes to read

Partielle Typdefinitionen ermöglichen, dass die Definition einer Klasse, Struktur, Schnittstelle oder eines
Datensatzes in mehrere Dateien aufgeteilt wird.
In File1.cs:

namespace PC
{
partial class A
{
int num = 0;
void MethodA() { }
partial void MethodC();
}
}

Die Deklaration in File2.cs:

namespace PC
{
partial class A
{
void MethodB() { }
partial void MethodC() { }
}
}

Bemerkungen
Das Aufteilen eines Klassen-, Struktur- oder Schnittstellentyps auf mehrere Dateien kann nützlich sein, wenn Sie
mit großen Projekten oder mit automatisch erzeugten Codes arbeiten, wie z.B. denjenigen, die vom Windows
Forms-Designer bereitgestellt werden. Ein partieller Typ kann eine partielle Methode enthalten. Weitere
Informationen finden Sie unter Partielle Klassen und Methoden.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
Modifizierer
Einführung in Generics
partial-Methode (C#-Referenz)
04.11.2021 • 2 minutes to read

Bei partiellen Methoden wird die Signatur in einem Teil eines partiellen Typs definiert, und die Implementierung
wird in einem anderen Teil des Typs definiert. Mit partiellen Methoden können Klassen-Designer Methoden-
Hooks bereitstellen, die Ereignis-Handlern ähneln und die Entwickler ggf. implementieren können. Wenn der
Entwickler keine Implementierung angibt, entfernt der Compiler die Signatur bei der Kompilierung. Die
folgenden Bedingungen gelten für partielle Methoden:
Deklarationen müssen mit dem Kontextschlüsselwort partial beginnen.
Die Signaturen in beiden Teilen des partiellen Typs müssen übereinstimmen.
Eine partielle Methode erfordert in den folgenden Fällen keine Implementierung:
Sie verfügt über keine Zugriffsmodifizierer (einschließlich der Standardeinstellung private).
Der Rückgabewert ist void.
Sie verfügt über keine out-Parameter.
Sie verfügt über keinen der folgenden Modifizierer: virtual, override, sealed, new oder extern.
Jede Methode, die nicht allen Einschränkungen entspricht (z. B. die public virtual partial void -Methode),
muss eine Implementierung bereitstellen.
Im folgenden Beispiel wird eine partielle Methode veranschaulicht, die in zwei Teilen einer partiellen Klasse
definiert ist:

namespace PM
{
partial class A
{
partial void OnSomethingHappened(string s);
}

// This part can be in a separate file.


partial class A
{
// Comment out this method and the program
// will still compile.
partial void OnSomethingHappened(String s)
{
Console.WriteLine("Something happened: {0}", s);
}
}
}

Partielle Methoden können auch in Kombination mit Quell-Generatoren nützlich sein. Beispielsweise könnte ein
regulärer Ausdruck mithilfe des folgenden Musters definiert werden:

[RegexGenerated("(dog|cat|fish)")]
partial bool IsPetMatch(string input);

Weitere Informationen finden Sie unter Partielle Klassen und Methoden.


Siehe auch
C#-Referenz
partial-Typ
remove (C#-Referenz)
04.11.2021 • 2 minutes to read

Das kontextabhängige Schlüsselwort remove definiert einen benutzerdefinierten Ereignisaccessor, der


aufgerufen wird, wenn der Clientcode das Abonnement Ihres Ereignisses (event) aufhebt. Wenn Sie einen
benutzerdefinierten remove -Accessor bereitstellen, müssen Sie auch einen add-Accessor angeben.

Beispiel
Im folgende Beispiel wird ein Ereignis mit den benutzerdefinierten Accessoren add und remove gezeigt. Das
vollständige Beispiel finden Sie unter Vorgehensweise: Implementieren von Schnittstellenereignissen.

class Events : IDrawingObject


{
event EventHandler PreDrawEvent;

event EventHandler IDrawingObject.OnDraw


{
add => PreDrawEvent += value;
remove => PreDrawEvent -= value;
}
}

Sie müssen normalerweise keine eigenen benutzerdefinierten Ereignisaccessoren bereitstellen. Die Accessoren,
die automatisch vom Compiler generiert werden, wenn Sie ein Ereignis deklarieren, sind in den meisten
Szenarios ausreichend.

Siehe auch
Ereignisse
set (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Schlüsselwort set definiert eine Accessor-Methode in einer Eigenschaft oder einem Indexer, die der
Eigenschaft oder dem Indexer-Element einen Wert zuweist. Weitere Informationen und Beispiele finden Sie
unter Eigenschaften, Automatisch implementierte Eigenschaften und Indexers.
Im folgenden Beispiel werden ein get - und ein set -Accessor für eine Eigenschaft namens Seconds definiert.
Im Beispiel wird ein privates Feld mit dem Namen _seconds verwendet, um den Eigenschaftswert zu
unterstützen.

class TimePeriod
{
private double _seconds;

public double Seconds


{
get { return _seconds; }
set { _seconds = value; }
}
}

Der set -Accessor besteht häufig aus einer einzelnen Anweisung, die einen Wert zurückgibt (wie im vorherigen
Beispiel gezeigt). Ab C# 7.0 können Sie die set -Zugriffsmethode als Ausdruckskörpermember implementieren.
Im folgenden Beispiel wird sowohl der get - als auch der set -Accessor als Ausdruckskörpermember
implementiert.

class TimePeriod
{
private double _seconds;

public double Seconds


{
get => _seconds;
set => _seconds = value;
}
}

In einfachen Fällen, in denen der get - und der set -Accessor einer Eigenschaft nichts anderes durchführen als
das Festlegen oder Abrufen eines Wertes in einem privaten Unterstützungsfeld, können Sie die Vorteile der
Unterstützung von automatisch implementierten Eigenschaften durch einen C#-Compiler nutzen. Im folgenden
Beispiel wird Hours als automatisch implementierte Eigenschaft implementiert.

class TimePeriod2
{
public double Hours { get; set; }
}

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
Eigenschaften
when (C#-Referenz)
04.11.2021 • 2 minutes to read

Sie verwenden das kontextabhängige Schlüsselwort when , um in den folgenden Kontexten eine Filterbedingung
anzugeben:
In der catch -Anweisung eines try/catch- oder try/catch/finally-Blocks
Als Case Guard in der switch -Anweisung.
Als Case Guard im switch -Ausdruck.

when in einer catch -Anweisung


Ab mit C# 6 kann when in einer catch -Anweisung verwendet werden, um eine Bedingung mit dem Wert
„TRUE“ für den Handler anzugeben, damit eine spezifische Ausnahme ausgeführt werden kann. Die Syntax
lautet:

catch (ExceptionType [e]) when (expr)

where expr ist ein Ausdruck, der einen booleschen Wert ergibt. Wenn true zurückgegeben wird, wird der
Ausnahmehandler ausgeführt; wenn false zurückgegeben wird, nicht.
Im folgenden Beispiel wird das Schlüsselwort when verwendet, um Handler abhängig vom Text der
Ausnahmemeldung für HttpRequestException bedingt auszuführen.
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
static void Main()
{
Console.WriteLine(MakeRequest().Result);
}

public static async Task<string> MakeRequest()


{
var client = new HttpClient();
var streamTask = client.GetStringAsync("https://localHost:10000");
try
{
var responseText = await streamTask;
return responseText;
}
catch (HttpRequestException e) when (e.Message.Contains("301"))
{
return "Site Moved";
}
catch (HttpRequestException e) when (e.Message.Contains("404"))
{
return "Page Not Found";
}
catch (HttpRequestException e)
{
return e.Message;
}
}
}

Siehe auch
Try-Catch-Anweisung
try/catch/finally-Anweisung
value (C#-Referenz)
04.11.2021 • 2 minutes to read

Das kontextabhängige Schlüsselwort value wird im set -Accessor in den Deklarationen property und indexer
verwendet. Es ähnelt einem Eingabeparameter einer Methode. Das Wort value verweist auf den Wert, den
Clientcode der Eigenschaft oder dem Indexer zuweisen möchte. Im folgenden Beispiel verfügt MyDerivedClass
über eine Eigenschaft mit dem Namen Name , die den Parameter value verwendet, um dem Unterstützungsfeld
name eine neue Zeichenfolge zuzuweisen. Aus Sicht des Clientcodes ist der Vorgang als einfache Zuweisung
geschrieben.

class MyBaseClass
{
// virtual auto-implemented property. Overrides can only
// provide specialized behavior if they implement get and set accessors.
public virtual string Name { get; set; }

// ordinary virtual property with backing field


private int _num;
public virtual int Number
{
get { return _num; }
set { _num = value; }
}
}

class MyDerivedClass : MyBaseClass


{
private string _name;

// Override auto-implemented property with ordinary property


// to provide specialized accessor behavior.
public override string Name
{
get
{
return _name;
}
set
{
if (!string.IsNullOrEmpty(value))
{
_name = value;
}
else
{
_name = "Unknown";
}
}
}
}

Weitere Informationen finden Sie in den Artikeln zu Eigenschaften und Indexern.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.
Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Schlüsselwörter
yield (C#-Referenz)
04.11.2021 • 4 minutes to read

Wenn Sie das kontextabhängige Schlüsselwort yield in einer Anweisung verwenden, geben Sie damit an, dass
die Methode, der Operator oder der get -Accessor, in der bzw. dem es vorkommt, ein Iterator ist. Wird ein
Iterator mithilfe von yield definiert, ist eine explizite zusätzliche Klasse (die Klasse, die den Zustand für eine
Enumeration enthält, siehe beispielsweise IEnumerator<T>) nicht erforderlich, wenn Sie das IEnumerable-
Muster und das IEnumerator-Muster für einen benutzerdefinierten Auflistungstyp implementieren.
Im folgenden Beispiel werden zwei Formen der yield -Anweisung gezeigt.

yield return <expression>;


yield break;

Hinweise
Sie verwenden eine yield return -Anweisung, um jedes Element einzeln zurückzugeben.
Die von einer Iteratormethode zurückgegebene Sequenz kann durch eine foreach-Anweisung oder eine LINQ-
Abfrage verwendet werden. Jede Iteration der foreach -Schleife ruft die Iteratormethode auf. Wenn eine
yield return -Anweisung im Iterator erreicht wird, wird ein expression -Ausdruck zurückgegeben, und die
aktuelle Position im Code wird beibehalten. Wenn die Iteratorfunktion das nächste Mal aufgerufen wird, wird die
Ausführung von dieser Position neu gestartet.
Wenn der Iterator ein System.Collections.Generic.IAsyncEnumerable<T> zurückgibt, kann diese Sequenz
mithilfe einer await foreach-Anweisung asynchron verarbeitet werden. Die Iteration der Schleife entspricht der
foreach -Anweisung. Der Unterschied besteht darin, dass jede Iteration für einen asynchronen Vorgang
angehalten werden kann, bevor der Ausdruck für das nächste Element zurückgegeben wird.
Sie verwenden eine yield break -Anweisung, um die Iteration zu beenden.
Weitere Informationen zu Iteratoren finden Sie unter Iterators (Iteratoren).

Iteratormethoden und Get-Zugriffsmethoden


Die Deklaration eines Iterators muss die folgenden Anforderungen erfüllen:
Der Rückgabetyp muss einer der folgenden Typen sein:
IAsyncEnumerable<T>
IEnumerable<T>
IEnumerable
IEnumerator<T>
IEnumerator
Die Deklaration darf keine in-, ref- oder out-Parameter aufweisen.
Der yield -Typ eines Iterators, der IEnumerable oder IEnumerator zurückgibt, ist object . Wenn der Iterator
IEnumerable<T> oder IEnumerator<T> zurückgibt, ist eine implizite Konvertierung vom Typ des Ausdrucks in
der yield return -Anweisung in den generischen Typparameter erforderlich.
Folgendes darf keine yield return - oder yield break -Anweisung enthalten:
Lambdaausdrücke und anonyme Methoden.
Methoden, die unsichere Blöcke enthalten. Weitere Informationen finden Sie unter unsafe.

Ausnahmebehandlung
Eine yield return -Anweisung kann sich nicht in einem try-catch-Block befinden. Eine yield return -
Anweisung kann sich im try-Block einer try-finally-Anweisung befinden.
Eine yield break -Anweisung kann sich in einem try- oder catch-Block befinden, nicht jedoch in einem finally-
Block.
Wenn der foreach - oder await foreach -Text (außerhalb der Iteratormethode) eine Ausnahme auslöst, wird ein
finally -Block in der Iteratormethode ausgeführt.

Technische Implementierung
Der folgende Code gibt einen IEnumerable<string> aus einer Iteratormethode zurück und durchläuft dann die
Elemente.

IEnumerable<string> elements = MyIteratorMethod();


foreach (string element in elements)
{
...
}

Der Aufruf von MyIteratorMethod führt nicht den Text der Methode aus. Stattdessen gibt der Aufruf einen
IEnumerable<string> in die Variable elements zurück.
Bei einer Iteration der foreach -Schleife wird die Methode MoveNext für elements aufgerufen. Dieser Aufruf
führt MyIteratorMethod aus, bis die nächste yield return -Anweisung erreicht ist. Der Ausdruck, der durch die
yield return -Anweisung zurückgegeben wird, ermittelt nicht nur den Wert der element -Variable für die
Verwendung im Schleifentext, sondern auch die Current-Eigenschaft von elements (ein IEnumerable<string> ).
Bei jeder nachfolgenden Iteration der foreach -Schleife wird die Ausführung des Iteratortexts da fortgesetzt, wo
sie beendet wurde, und endet dann wieder an einer yield return -Anweisung. Die foreach -Schleife wird
beendet, wenn das Ende der Iteratormethode oder eine yield break -Anweisung erreicht wird.
Der folgende Code gibt einen IAsyncEnumerable<string> aus einer Iteratormethode zurück und durchläuft dann
die Elemente.

IAsyncEnumerable<string> elements = MyAsyncIteratorMethod();


await foreach (string element in elements)
{
// ...
}

Bei einer Iteration der await foreach -Schleife wird die Methode IAsyncEnumerator<T>.MoveNextAsync für
elements aufgerufen. Die System.Threading.Tasks.ValueTask<TResult>-Rückgabe von MoveNext wird
abgeschlossen, wenn der nächste erreicht yield return wird.
Bei jeder nachfolgenden Iteration der await foreach -Schleife wird die Ausführung des Iteratortexts da
fortgesetzt, wo sie beendet wurde, und endet dann wieder an einer yield return -Anweisung. Die
await foreach -Schleife wird beendet, wenn das Ende der Iteratormethode oder eine yield break -Anweisung
erreicht wird.
Beispiele
Das folgende Beispiel weist eine yield return -Anweisung auf, die sich innerhalb einer for -Schleife befindet.
Jede Iteration des foreach -Anweisungstexts in der Methode Main erzeugt einen Aufruf an die Iteratorfunktion
Power . Jeder Aufruf der Iteratorfunktion führt bei der nächsten Iteration der yield return -Schleife zur
nächsten Ausführung der for -Anweisung.
Der Rückgabetyp der Iteratormethode ist IEnumerable, was ein Iteratorschnittstellentyp ist. Wird die
Iteratormethode aufgerufen wird, gibt sie ein aufzählbares Objekt zurück, das die Potenzen einer Zahl enthält.

public class PowersOf2


{
static void Main()
{
// Display powers of 2 up to the exponent of 8:
foreach (int i in Power(2, 8))
{
Console.Write("{0} ", i);
}
}

public static System.Collections.Generic.IEnumerable<int> Power(int number, int exponent)


{
int result = 1;

for (int i = 0; i < exponent; i++)


{
result = result * number;
yield return result;
}
}

// Output: 2 4 8 16 32 64 128 256


}

Das folgende Beispiel zeigt einen get -Accessor, der ein Iterator ist. Im Beispiel gibt jede yield return -
Anweisung eine Instanz einer benutzerdefinierten Klasse zurück.
public static class GalaxyClass
{
public static void ShowGalaxies()
{
var theGalaxies = new Galaxies();
foreach (Galaxy theGalaxy in theGalaxies.NextGalaxy)
{
Debug.WriteLine(theGalaxy.Name + " " + theGalaxy.MegaLightYears.ToString());
}
}

public class Galaxies


{

public System.Collections.Generic.IEnumerable<Galaxy> NextGalaxy


{
get
{
yield return new Galaxy { Name = "Tadpole", MegaLightYears = 400 };
yield return new Galaxy { Name = "Pinwheel", MegaLightYears = 25 };
yield return new Galaxy { Name = "Milky Way", MegaLightYears = 0 };
yield return new Galaxy { Name = "Andromeda", MegaLightYears = 3 };
}
}
}

public class Galaxy


{
public String Name { get; set; }
public int MegaLightYears { get; set; }
}
}

C#-Sprachspezifikation
Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche
Quelle für die Syntax und Verwendung von C#.

Siehe auch
C#-Referenz
C#-Programmierhandbuch
foreach, in
Iteratoren
Abfrageschlüsselwörter (C#-Referenz)
04.11.2021 • 2 minutes to read

Dieser Abschnitt enthält die kontextabhängigen Schlüsselwörter, die in Abfrageausdrücken verwendet werden.

In diesem Abschnitt
K L A USEL B ESC H REIB UN G

from Gibt eine Datenquelle und eine Bereichsvariable (ähnlich


einer Iterationsvariable) an

where Filtert Quellelemente basierend auf einem oder mehreren


boolschen Ausdrücken, die durch logische AND- und OR-
Operatoren ( && oder || ) getrennt sind

select Gibt den Typ und die Form an, über die die Elemente in der
zurückgegebenen Sequenz verfügen werden, wenn die
Abfrage ausgeführt wird

group Gruppiert Abfrageergebnisse entsprechend eines


angegebenen Schlüsselwerts

into Stellt einen Bezeichner bereit, der als Verweis auf die
Ergebnisse einer join-, group- oder select-Klausel dienen
kann

orderby Sortiert Abfrageergebnisse in aufsteigender oder


absteigender Reihenfolge, basierend auf der
Standardvergleichsfunktion für den Elementtyp

join Verknüpft zwei Datenquellen basierend auf einem


Gleichheitsvergleich zwischen zwei angegebenen
übereinstimmenden Kriterien

let Führt eine Bereichsvariable zum Speichern von


Unterausdruckergebnissen in einem Abfrageausdruck ein

in Kontextabhängiges Schlüsselwort in einer join-Klausel

on Kontextabhängiges Schlüsselwort in einer join-Klausel

equals Kontextabhängiges Schlüsselwort in einer join-Klausel

by Kontextabhängiges Schlüsselwort in einer group-Klausel

ascending Kontextabhängiges Schlüsselwort in einer orderby-Klausel

descending Kontextabhängiges Schlüsselwort in einer orderby-Klausel


Siehe auch
C#-Schlüsselwörter
LINQ (Language Integrated Query)
LINQ in C#
from-Klausel (C#-Referenz)
04.11.2021 • 5 minutes to read

Ein Abfrageausdruck muss mit einer from -Klausel beginnen. Darüber hinaus kann ein Abfrageausdruck
Unterabfragen enthalten, die auch mit einer from -Klausel beginnen. Die from -Klausel gibt Folgendes an:
Die Datenquelle, für die die Abfrage oder Unterabfrage ausgeführt wird.
Eine lokale Bereichsvariable, die jedes Element in der Quellsequenz darstellt.
Sowohl die Bereichsvariable als auch die Datenquelle sind stark typisiert. Die Datenquelle, auf die in der from -
Klausel verwiesen wird, muss vom Typ IEnumerable, IEnumerable<T> oder von einem abgeleiteten Typ wie
IQueryable<T> sein.
Im folgenden Beispiel numbers ist die Datenquelle und num ist die Bereichsvariable. Beachten Sie, dass beide
Variablen stark typisiert sind, obwohl das var-Schlüsselwort verwendet wird.

class LowNums
{
static void Main()
{
// A simple data source.
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

// Create the query.


// lowNums is an IEnumerable<int>
var lowNums = from num in numbers
where num < 5
select num;

// Execute the query.


foreach (int i in lowNums)
{
Console.Write(i + " ");
}
}
}
// Output: 4 1 3 2 0

Die Bereichsvariable
Der Compiler leitet den Typ der Bereichsvariablen ab, wenn die Datenquelle IEnumerable<T> implementiert.
Wenn die Quelle beispielsweise vom Typ IEnumerable<Customer> ist, wird die Bereichsvariable als Customer
abgeleitet. Sie müssen den Typ nur explizit angeben, wenn die Quelle ein nicht-generischer IEnumerable -Typ wie
z.B. ArrayList ist. Weitere Informationen finden Sie unter Vorgehensweise: Abfragen von ArrayList mit LINQ.
Im vorherigen Beispiel wird num als Typ int abgeleitet. Da die Bereichsvariable stark typisiert ist, können Sie
für sie Methoden aufrufen oder sie in anderen Vorgängen verwenden. Anstatt z.B. select num zu schreiben,
könnten Sie select num.ToString() schreiben, sodass der Abfrageausdruck eine Sequenz von Zeichenfolgen
anstelle von Ganzzahlen zurückgibt. Sie könnten auch select num + 10 schreiben, damit der Ausdruck die
Sequenz „14, 11, 13, 12, 10“ zurückgibt. Weitere Informationen finden Sie unter select clause (select-Klausel).
Die Bereichsvariable entspricht einer Iterationsvariablen in einer foreach-Anweisung mit einer wichtigen
Ausnahme: Eine Bereichsvariable speichert niemals Daten aus der Quelle. Sie ist nur ein syntaktisches
Hilfsmittel, mit dem die Abfrage beschreiben kann, was eintritt, wenn die Abfrage ausgeführt wird. Weitere
Informationen finden Sie unter Introduction to LINQ queries (C#) (Einführung in LINQ-Abfragen (C#)).

Zusammengesetzte from-Klauseln
In einigen Fällen kann jedes Element in der Quellsequenz selbst eine Sequenz sein oder eine Sequenz enthalten.
Ihre Datenquelle kann beispielsweise ein IEnumerable<Student> sein, wobei jedes Student-Objekt in der Sequenz
eine Liste der Testergebnisse enthält. Sie können zusammengesetzte from -Klauseln verwenden, um auf die
innere Liste jedes Student -Objekts zuzugreifen. Die Technik entspricht dem Verwenden von geschachtelten
foreach-Anweisungen. Sie können die where-Klausel oder die orderby-Klausel zu einer der from -Klauseln
hinzufügen, um die Ergebnisse zu filtern. Das folgende Beispiel enthält eine Sequenz von Student -Objekten,
von denen jedes eine innere List mit Ganzzahlen enthält, die die Testergebnisse darstellen. Um auf die innere
Liste zuzugreifen, verwenden Sie eine zusammengesetzte from -Klausel. Bei Bedarf können Sie Klauseln
zwischen den beiden from -Klauseln einfügen.
class CompoundFrom
{
// The element type of the data source.
public class Student
{
public string LastName { get; set; }
public List<int> Scores {get; set;}
}

static void Main()


{

// Use a collection initializer to create the data source. Note that


// each element in the list contains an inner sequence of scores.
List<Student> students = new List<Student>
{
new Student {LastName="Omelchenko", Scores= new List<int> {97, 72, 81, 60}},
new Student {LastName="O'Donnell", Scores= new List<int> {75, 84, 91, 39}},
new Student {LastName="Mortensen", Scores= new List<int> {88, 94, 65, 85}},
new Student {LastName="Garcia", Scores= new List<int> {97, 89, 85, 82}},
new Student {LastName="Beebe", Scores= new List<int> {35, 72, 91, 70}}
};

// Use a compound from to access the inner sequence within each element.
// Note the similarity to a nested foreach statement.
var scoreQuery = from student in students
from score in student.Scores
where score > 90
select new { Last = student.LastName, score };

// Execute the queries.


Console.WriteLine("scoreQuery:");
// Rest the mouse pointer on scoreQuery in the following line to
// see its type. The type is IEnumerable<'a>, where 'a is an
// anonymous type defined as new {string Last, int score}. That is,
// each instance of this anonymous type has two members, a string
// (Last) and an int (score).
foreach (var student in scoreQuery)
{
Console.WriteLine("{0} Score: {1}", student.Last, student.score);
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/*
scoreQuery:
Omelchenko Score: 97
O'Donnell Score: 91
Mortensen Score: 94
Garcia Score: 97
Beebe Score: 91
*/

Verwenden von mehreren from-Klauseln zum Ausführen von Joins


Eine zusammengesetzte from -Klausel wird zum Zugriff auf innere Auflistungen in einer einzelnen Datenquelle
verwendet. Eine Abfrage kann jedoch auch mehrere from -Klauseln enthalten, die ergänzende Abfragen aus
unabhängigen Datenquellen generieren. Mit dieser Technik können Sie bestimmte Typen von
Verknüpfungsvorgängen durchführen, die beim Einsatz der join-Klausel nicht möglich sind.
Das folgende Beispiel veranschaulicht, wie zwei from -Klauseln verwendet werden können, um einen
vollständigen Cross Join zweier Datenquellen zu bilden.

class CompoundFrom2
{
static void Main()
{
char[] upperCase = { 'A', 'B', 'C' };
char[] lowerCase = { 'x', 'y', 'z' };

// The type of joinQuery1 is IEnumerable<'a>, where 'a


// indicates an anonymous type. This anonymous type has two
// members, upper and lower, both of type char.
var joinQuery1 =
from upper in upperCase
from lower in lowerCase
select new { upper, lower };

// The type of joinQuery2 is IEnumerable<'a>, where 'a


// indicates an anonymous type. This anonymous type has two
// members, upper and lower, both of type char.
var joinQuery2 =
from lower in lowerCase
where lower != 'x'
from upper in upperCase
select new { lower, upper };

// Execute the queries.


Console.WriteLine("Cross join:");
// Rest the mouse pointer on joinQuery1 to verify its type.
foreach (var pair in joinQuery1)
{
Console.WriteLine("{0} is matched to {1}", pair.upper, pair.lower);
}

Console.WriteLine("Filtered non-equijoin:");
// Rest the mouse pointer over joinQuery2 to verify its type.
foreach (var pair in joinQuery2)
{
Console.WriteLine("{0} is matched to {1}", pair.lower, pair.upper);
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Cross join:
A is matched to x
A is matched to y
A is matched to z
B is matched to x
B is matched to y
B is matched to z
C is matched to x
C is matched to y
C is matched to z
Filtered non-equijoin:
y is matched to A
y is matched to B
y is matched to C
z is matched to A
z is matched to B
z is matched to C
*/
Weitere Informationen zu Verknüpfungsvorgängen mit mehreren from -Klauseln finden Sie unter Ausführen
von Left Outer Joins.

Weitere Informationen
Abfrageschlüsselwörter (LINQ)
Language-Integrated Query (LINQ)
where-Klausel (C#-Referenz)
04.11.2021 • 3 minutes to read

Die where -Klausel wird in einem Abfrageausdruck verwendet, um anzugeben, welche Elemente aus der
Datenquelle im Abfrageausdruck zurückgegeben werden. Sie wendet eine boolesche Bedingung (Prädikat) auf
jedes Quellelement an, auf das durch die Bereichsvariable verwiesen wird, und gibt die Elemente zurück, bei
denen die angegebene Bedingung wahr ist. Ein einzelner Abfrageausdruck enthält möglicherweise mehrere
where -Klauseln, und eine einzelne Klausel kann mehrere Teilausdrücke des Prädikats enthalten.

Beispiel 1
Im folgenden Beispiel filtert die where -Klausel alle Zahlen mit Ausnahme derjenigen heraus, die niedriger als
fünf sind. Wenn Sie die where -Klausel entfernen, werden alle Zahlen aus der Datenquelle zurückgegeben. Der
Ausdruck num < 5 ist das Prädikat, das auf jedes Element angewendet wird.

class WhereSample
{
static void Main()
{
// Simple data source. Arrays support IEnumerable<T>.
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

// Simple query with one predicate in where clause.


var queryLowNums =
from num in numbers
where num < 5
select num;

// Execute the query.


foreach (var s in queryLowNums)
{
Console.Write(s.ToString() + " ");
}
}
}
//Output: 4 1 3 2 0

Beispiel 2
Innerhalb einer einzelnen where -Klausel können Sie so viele Prädikate wie nötig angeben, indem Sie die
Operatoren && und || verwenden. Im folgenden Beispiel gibt die Abfrage zwei Prädikate an, um nur die geraden
Zahlen auszuwählen, die niedriger als fünf sind.
class WhereSample2
{
static void Main()
{
// Data source.
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

// Create the query with two predicates in where clause.


var queryLowNums2 =
from num in numbers
where num < 5 && num % 2 == 0
select num;

// Execute the query


foreach (var s in queryLowNums2)
{
Console.Write(s.ToString() + " ");
}
Console.WriteLine();

// Create the query with two where clause.


var queryLowNums3 =
from num in numbers
where num < 5
where num % 2 == 0
select num;

// Execute the query


foreach (var s in queryLowNums3)
{
Console.Write(s.ToString() + " ");
}
}
}
// Output:
// 4 2 0
// 4 2 0

Beispiel 3
Eine where -Klausel kann eine oder mehrere Methoden enthalten, die boolesche Werte zurückgeben. Im
folgenden Beispiel verwendet die where -Klausel eine Methode, um zu bestimmen, ob der aktuelle Wert der
Bereichsvariable gerade oder ungerade ist.
class WhereSample3
{
static void Main()
{
// Data source
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

// Create the query with a method call in the where clause.


// Note: This won't work in LINQ to SQL unless you have a
// stored procedure that is mapped to a method by this name.
var queryEvenNums =
from num in numbers
where IsEven(num)
select num;

// Execute the query.


foreach (var s in queryEvenNums)
{
Console.Write(s.ToString() + " ");
}
}

// Method may be instance method or static method.


static bool IsEven(int i)
{
return i % 2 == 0;
}
}
//Output: 4 8 6 2 0

Bemerkungen
Die where -Klausel ist ein Filtermechanismus. Sie kann praktisch überall in einem Abfrageausdruck positioniert
werden; sie kann allerdings nicht die erste oder letzte Klausel sein. Eine where -Klausel kann entweder vor oder
nach der group-Klausel angezeigt werden, abhängig davon, ob Sie die Quellelemente vor oder nach deren
Gruppierung filtern müssen.
Wenn ein angegebenes Prädikat nicht für die Elemente in der Datenquelle gültig ist, tritt ein Kompilierzeitfehler
auf. Dies ist ein Vorteil der von LINQ bereitgestellten starken Typprüfung.
Zur Kompilierzeit wird das Schlüsselwort where in einen Aufruf der Standardabfrageoperator-Methode Where
konvertiert.

Weitere Informationen
Abfrageschlüsselwörter (LINQ)
from-Klausel
select-Klausel
Filtern von Daten
LINQ in C#
Language-Integrated Query (LINQ)
select-Klausel (C#-Referenz)
04.11.2021 • 5 minutes to read

In einem Abfrageausdruck gibt die select -Klausel den Typ der Werte an, die beim Ausführen der Abfrage
erstellt werden. Das Ergebnis basiert auf der Auswertung aller vorherigen Klauseln und auf allen Ausdrücke in
der select -Klausel selbst. Ein Abfrageausdruck muss entweder mit einer select -Klausel oder einer group-
Klausel enden.
Im folgenden Beispiel wird eine einfache select -Klausel in einem Abfrageausdruck dargestellt.

class SelectSample1
{
static void Main()
{
//Create the data source
List<int> Scores = new List<int>() { 97, 92, 81, 60 };

// Create the query.


IEnumerable<int> queryHighScores =
from score in Scores
where score > 80
select score;

// Execute the query.


foreach (int i in queryHighScores)
{
Console.Write(i + " ");
}
}
}
//Output: 97 92 81

Der Typ der mit der select -Klausel erstellten Sequenz bestimmt den Typ der Abfragevariable queryHighScores .
Im einfachsten Fall gibt die select -Klausel nur die Bereichsvariable an. Dadurch enthält die zurückgegebene
Sequenz Elemente desselben Typs wie die Datenquelle. Weitere Informationen finden Sie unter Typbeziehungen
in LINQ-Abfragevorgängen. Allerdings bietet die select -Klausel zudem ein leistungsstarkes Werkzeug, um
Quelldaten in neue Typen zu transformieren (oder zu projizieren). Weitere Informationen finden Sie unter
Datentransformationen mit LINQ (C#).

Beispiel
Das folgende Beispiel zeigt die verschiedenen Formen, die eine select -Klausel annehmen kann. Beachten Sie in
jeder Abfrage die Beziehung zwischen der select -Klausel und dem Typ der Abfragevariable ( studentQuery1 ,
studentQuery2 usw.).

class SelectSample2
{
// Define some classes
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public List<int> Scores;
public ContactInfo GetContactInfo(SelectSample2 app, int id)
{
{
ContactInfo cInfo =
(from ci in app.contactList
where ci.ID == id
select ci)
.FirstOrDefault();

return cInfo;
}

public override string ToString()


{
return First + " " + Last + ":" + ID;
}
}

public class ContactInfo


{
public int ID { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public override string ToString() { return Email + "," + Phone; }
}

public class ScoreInfo


{
public double Average { get; set; }
public int ID { get; set; }
}

// The primary data source


List<Student> students = new List<Student>()
{
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int>() {97, 92, 81,
60}},
new Student {First="Claire", Last="O'Donnell", ID=112, Scores= new List<int>() {75, 84, 91,
39}},
new Student {First="Sven", Last="Mortensen", ID=113, Scores= new List<int>() {88, 94, 65, 91}},
new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int>() {97, 89, 85, 82}},
};

// Separate data source for contact info.


List<ContactInfo> contactList = new List<ContactInfo>()
{
new ContactInfo {ID=111, Email="SvetlanO@Contoso.com", Phone="206-555-0108"},
new ContactInfo {ID=112, Email="ClaireO@Contoso.com", Phone="206-555-0298"},
new ContactInfo {ID=113, Email="SvenMort@Contoso.com", Phone="206-555-1130"},
new ContactInfo {ID=114, Email="CesarGar@Contoso.com", Phone="206-555-0521"}
};

static void Main(string[] args)


{
SelectSample2 app = new SelectSample2();

// Produce a filtered sequence of unmodified Students.


IEnumerable<Student> studentQuery1 =
from student in app.students
where student.ID > 111
select student;

Console.WriteLine("Query1: select range_variable");


foreach (Student s in studentQuery1)
{
Console.WriteLine(s.ToString());
}

// Produce a filtered sequence of elements that contain


// only one property of each Student.
IEnumerable<String> studentQuery2 =
from student in app.students
from student in app.students
where student.ID > 111
select student.Last;

Console.WriteLine("\r\n studentQuery2: select range_variable.Property");


foreach (string s in studentQuery2)
{
Console.WriteLine(s);
}

// Produce a filtered sequence of objects created by


// a method call on each Student.
IEnumerable<ContactInfo> studentQuery3 =
from student in app.students
where student.ID > 111
select student.GetContactInfo(app, student.ID);

Console.WriteLine("\r\n studentQuery3: select range_variable.Method");


foreach (ContactInfo ci in studentQuery3)
{
Console.WriteLine(ci.ToString());
}

// Produce a filtered sequence of ints from


// the internal array inside each Student.
IEnumerable<int> studentQuery4 =
from student in app.students
where student.ID > 111
select student.Scores[0];

Console.WriteLine("\r\n studentQuery4: select range_variable[index]");


foreach (int i in studentQuery4)
{
Console.WriteLine("First score = {0}", i);
}

// Produce a filtered sequence of doubles


// that are the result of an expression.
IEnumerable<double> studentQuery5 =
from student in app.students
where student.ID > 111
select student.Scores[0] * 1.1;

Console.WriteLine("\r\n studentQuery5: select expression");


foreach (double d in studentQuery5)
{
Console.WriteLine("Adjusted first score = {0}", d);
}

// Produce a filtered sequence of doubles that are


// the result of a method call.
IEnumerable<double> studentQuery6 =
from student in app.students
where student.ID > 111
select student.Scores.Average();

Console.WriteLine("\r\n studentQuery6: select expression2");


foreach (double d in studentQuery6)
{
Console.WriteLine("Average = {0}", d);
}

// Produce a filtered sequence of anonymous types


// that contain only two properties from each Student.
var studentQuery7 =
from student in app.students
where student.ID > 111
select new { student.First, student.Last };

Console.WriteLine("\r\n studentQuery7: select new anonymous type");


Console.WriteLine("\r\n studentQuery7: select new anonymous type");
foreach (var item in studentQuery7)
{
Console.WriteLine("{0}, {1}", item.Last, item.First);
}

// Produce a filtered sequence of named objects that contain


// a method return value and a property from each Student.
// Use named types if you need to pass the query variable
// across a method boundary.
IEnumerable<ScoreInfo> studentQuery8 =
from student in app.students
where student.ID > 111
select new ScoreInfo
{
Average = student.Scores.Average(),
ID = student.ID
};

Console.WriteLine("\r\n studentQuery8: select new named type");


foreach (ScoreInfo si in studentQuery8)
{
Console.WriteLine("ID = {0}, Average = {1}", si.ID, si.Average);
}

// Produce a filtered sequence of students who appear on a contact list


// and whose average is greater than 85.
IEnumerable<ContactInfo> studentQuery9 =
from student in app.students
where student.Scores.Average() > 85
join ci in app.contactList on student.ID equals ci.ID
select ci;

Console.WriteLine("\r\n studentQuery9: select result of join clause");


foreach (ContactInfo ci in studentQuery9)
{
Console.WriteLine("ID = {0}, Email = {1}", ci.ID, ci.Email);
}

// Keep the console window open in debug mode


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output
Query1: select range_variable
Claire O'Donnell:112
Sven Mortensen:113
Cesar Garcia:114

studentQuery2: select range_variable.Property


O'Donnell
Mortensen
Garcia

studentQuery3: select range_variable.Method


ClaireO@Contoso.com,206-555-0298
SvenMort@Contoso.com,206-555-1130
CesarGar@Contoso.com,206-555-0521

studentQuery4: select range_variable[index]


First score = 75
First score = 88
First score = 97

studentQuery5: select expression


Adjusted first score = 82.5
Adjusted first score = 96.8
Adjusted first score = 106.7
studentQuery6: select expression2
Average = 72.25
Average = 84.5
Average = 88.25

studentQuery7: select new anonymous type


O'Donnell, Claire
Mortensen, Sven
Garcia, Cesar

studentQuery8: select new named type


ID = 112, Average = 72.25
ID = 113, Average = 84.5
ID = 114, Average = 88.25

studentQuery9: select result of join clause


ID = 114, Email = CesarGar@Contoso.com
*/

Wie in studentQuery8 im vorherigen Beispiel kann es möglicherweise sinnvoll sein, dass die Elemente der
zurückgegebenen Sequenz nur eine Teilmenge der Eigenschaften der Quellelemente enthalten. Indem die
zurückgegebene Sequenz so klein wie möglich gehalten wird, können die Speicheranforderungen reduziert und
die Geschwindigkeit der Abfrageausführung erhöht werden. Erstellen Sie hierzu einen anonymen Typ in der
select -Klausel, und verwenden Sie einen Objektinitialisierer, um sie mit den entsprechenden Eigenschaften aus
dem Quellelement zu initialisieren. Ein Beispiel zur Vorgehensweise finden Sie unter Objekt- und
Auflistungsinitialisierer.

Bemerkungen
Beim Kompilieren wird die select -Klausel in einen Methodenaufruf des Select-Standardabfrageoperators
übersetzt.

Siehe auch
C#-Referenz
Abfrageschlüsselwörter (LINQ)
from-Klausel
partial (Methode) (C#-Referenz)
Anonyme Typen
LINQ in C#
Language-Integrated Query (LINQ)
group-Klausel (C#-Referenz)
04.11.2021 • 8 minutes to read

Die group -Klausel gibt eine Sequenz von IGrouping<TKey,TElement>-Objekten zurück, die null oder mehr
Elemente enthalten, die mit dem Schlüsselwert für die Gruppe übereinstimmen. Sie können z.B. eine Sequenz
von Zeichenfolgen entsprechend des ersten Buchstaben in jeder Zeichenfolge gruppieren. In diesem Fall ist der
erste Buchstabe der Schlüssel, verfügt über einen Typ char und wird in der Key -Eigenschaft jedes
IGrouping<TKey,TElement>-Objekts gespeichert. Der Compiler leiten den Typ des Schlüssels her.
Sie können einen Abfrageausdruck mit einer group -Klausel beenden, so wie in folgendem Beispiel gezeigt:

// Query variable is an IEnumerable<IGrouping<char, Student>>


var studentQuery1 =
from student in students
group student by student.Last[0];

Wenn Sie zusätzliche Abfragevorgänge für jede Gruppe ausführen möchten, können Sie einen temporären
Bezeichner mithilfe des kontextuellen Schlüsselworts into angeben. Wenn Sie into verwenden, müssen Sie mit
der Abfrage fortfahren und sie entweder mit einer select -Anweisung oder einer anderen group -Klausel
beenden, so wie im folgenden Auszug dargestellt:

// Group students by the first letter of their last name


// Query variable is an IEnumerable<IGrouping<char, Student>>
var studentQuery2 =
from student in students
group student by student.Last[0] into g
orderby g.Key
select g;

Weitere vollständige Gebrauchsbeispiele für group mit und ohne into sind im Abschnitt über Beispiele in
diesem Artikel enthalten.

Auflisten der Ergebnisse einer Gruppenabfrage


Da die IGrouping<TKey,TElement>-Objekte, die von einer group -Abfrage erstellt wurden, praktisch eine Liste
von Listen sind, müssen Sie eine geschachtelte foreach-Schleife verwenden, um auf die Elemente in jeder
Gruppe zuzugreifen. Die äußere Schleife durchläuft die Gruppenschlüssel, und die innere Schleife durchläuft
jedes Element in der Gruppe selbst. Eine Gruppe kann womöglich über einen Schlüssel verfügen, jedoch nicht
über Elemente. Nachstehend finden Sie die foreach -Schleife, die die Abfrage in den vorherigen Codebeispielen
ausführen:
// Iterate group items with a nested foreach. This IGrouping encapsulates
// a sequence of Student objects, and a Key of type char.
// For convenience, var can also be used in the foreach statement.
foreach (IGrouping<char, Student> studentGroup in studentQuery2)
{
Console.WriteLine(studentGroup.Key);
// Explicit type for student could also be used here.
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}", student.Last, student.First);
}
}

Schlüsseltypen
Gruppenschlüssel können von jedem Typ sein, z.B. eine Zeichenfolge, ein integrierter numerischer Typ oder ein
benutzerdefinierter benannter oder anonymer Typ.
Gruppieren nach Zeichenfolge
Das vorherige Codebeispiel verwendete einen char . Es hätte stattdessen einfach ein Zeichenfolgenschlüssel
angegeben werden können, z.B. der vollständige letzte Name.

// Same as previous example except we use the entire last name as a key.
// Query variable is an IEnumerable<IGrouping<string, Student>>
var studentQuery3 =
from student in students
group student by student.Last;

Gruppieren nach Bool


Das folgende Beispiel zeigt die Verwendung eines booleschen Werts für einen Schlüssel, um die Ergebnisse in
zwei Gruppen zu unterteilen. Beachten Sie, dass der Wert durch einen Unterausdruck in der group -Klausel
erstellt wird.
class GroupSample1
{
// The element type of the data source.
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public List<int> Scores;
}

public static List<Student> GetStudents()


{
// Use a collection initializer to create the data source. Note that each element
// in the list contains an inner sequence of scores.
List<Student> students = new List<Student>
{
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int> {97, 72, 81,
60}},
new Student {First="Claire", Last="O'Donnell", ID=112, Scores= new List<int> {75, 84, 91, 39}},
new Student {First="Sven", Last="Mortensen", ID=113, Scores= new List<int> {99, 89, 91, 95}},
new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int> {72, 81, 65, 84}},
new Student {First="Debra", Last="Garcia", ID=115, Scores= new List<int> {97, 89, 85, 82}}
};

return students;
}

static void Main()


{
// Obtain the data source.
List<Student> students = GetStudents();

// Group by true or false.


// Query variable is an IEnumerable<IGrouping<bool, Student>>
var booleanGroupQuery =
from student in students
group student by student.Scores.Average() >= 80; //pass or fail!

// Execute the query and access items in each group


foreach (var studentGroup in booleanGroupQuery)
{
Console.WriteLine(studentGroup.Key == true ? "High averages" : "Low averages");
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}:{2}", student.Last, student.First, student.Scores.Average());
}
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Low averages
Omelchenko, Svetlana:77.5
O'Donnell, Claire:72.25
Garcia, Cesar:75.5
High averages
Mortensen, Sven:93.5
Garcia, Debra:88.25
*/

Gruppieren nach numerischen Bereich


Das nächste Beispiel verwendet einen Ausdruck, um einen nummerischen Gruppenschlüssel zu erstellen, der
einen Prozentbereich darstellt. Beachten Sie, dass let an einer geeigneten Position eingesetzt wird, um
Ergebnisse eines Methodenaufrufs zu speichern, damit Sie die Methode nicht zweimal in der group -Klausel
aufrufen müssen. Weitere Informationen zur sicheren Verwendung von Methoden in Abfrageausdrücken finden
Sie unter Behandeln von Ausnahmen in Abfrageausdrücken.

class GroupSample2
{
// The element type of the data source.
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public List<int> Scores;
}

public static List<Student> GetStudents()


{
// Use a collection initializer to create the data source. Note that each element
// in the list contains an inner sequence of scores.
List<Student> students = new List<Student>
{
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int> {97, 72, 81,
60}},
new Student {First="Claire", Last="O'Donnell", ID=112, Scores= new List<int> {75, 84, 91, 39}},
new Student {First="Sven", Last="Mortensen", ID=113, Scores= new List<int> {99, 89, 91, 95}},
new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int> {72, 81, 65, 84}},
new Student {First="Debra", Last="Garcia", ID=115, Scores= new List<int> {97, 89, 85, 82}}
};

return students;
}

// This method groups students into percentile ranges based on their


// grade average. The Average method returns a double, so to produce a whole
// number it is necessary to cast to int before dividing by 10.
static void Main()
{
// Obtain the data source.
List<Student> students = GetStudents();

// Write the query.


var studentQuery =
from student in students
let avg = (int)student.Scores.Average()
group student by (avg / 10) into g
orderby g.Key
select g;

// Execute the query.


foreach (var studentGroup in studentQuery)
{
int temp = studentGroup.Key * 10;
Console.WriteLine("Students with an average between {0} and {1}", temp, temp + 10);
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}:{2}", student.Last, student.First, student.Scores.Average());
}
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Students with an average between 70 and 80
Students with an average between 70 and 80
Omelchenko, Svetlana:77.5
O'Donnell, Claire:72.25
Garcia, Cesar:75.5
Students with an average between 80 and 90
Garcia, Debra:88.25
Students with an average between 90 and 100
Mortensen, Sven:93.5
*/

Gruppieren nach zusammengesetzten Schlüsseln


Verwenden Sie einen zusammengesetzten Schlüssel, wenn Sie Elemente gemäß mehrerer Schlüssel gruppieren
möchten. Sie erstellen einen zusammengesetzten Schlüssel, indem Sie einen anonymen Typ oder einen
benannten Typ verwenden, um das Schlüsselelement aufzunehmen. Im folgenden Beispiel wird davon
ausgegangen, dass eine Person -Klasse mit Elementen namens surname und city deklariert wurde. Die group
-Klausel bewirkt, dass eine separate Gruppe für jede Gruppe von Personen mit dem gleichen Nachnamen und
der gleichen Stadt erstellt wird.

group person by new {name = person.surname, city = person.city};

Verwenden Sie einen benannten Typ, wenn Sie die Abfragevariable an eine andere Methode übergeben müssen.
Erstellen Sie eine spezielle Klasse mit automatisch implementierten Eigenschaften für die Schlüssel, und setzen
Sie anschließend die Methoden Equals und GetHashCode außer Kraft. Sie können auch eine Struktur verwenden.
In diesem Fall müssen Sie diese Methoden nicht unbedingt außer Kraft setzen. Weitere Informationen finden Sie
unter Vorgehensweise: Implementieren einer einfachen Klasse mit automatisch implementierten Eigenschaften
und Vorgehensweise: Abfragen von Dateiduplikaten in einer Verzeichnisstruktur. Der zweite Artikel enthält ein
Codebeispiel, in dem dargestellt wird, wie ein zusammengesetzter Schlüssel mit einem benannten Typ
verwendet wird.

Beispiel 1
Das folgende Beispiel zeigt das Standardmuster für das Sortieren von Quelldaten in Gruppen, wenn keine
zusätzliche Abfragelogik auf die Gruppen angewendet wird. Dies wird Gruppierung ohne Fortsetzung genannt.
Die Elemente in einem Zeichenfolgenarray werden gemäß des ersten Buchstabens gruppiert. Das Ergebnis der
Abfrage ist ein IGrouping<TKey,TElement>-Typ, der eine öffentliche Key -Eigenschaft des Typs char und eine
IEnumerable<T>-Sammlung enthält, die jedes Element in der Gruppierung enthält.
Das Ergebnis einer group -Klausel ist eine Sequenz von Sequenzen. Verwenden Sie deshalb eine geschachtelte
foreach -Schleife innerhalb der Schleife, die die Gruppenschlüssel durchläuft – so wie in folgendem Beispiel
gezeigt – um auf die einzelnen Elemente innerhalb jeder zurückgegebenen Gruppe zuzugreifen.
class GroupExample1
{
static void Main()
{
// Create a data source.
string[] words = { "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese" };

// Create the query.


var wordGroups =
from w in words
group w by w[0];

// Execute the query.


foreach (var wordGroup in wordGroups)
{
Console.WriteLine("Words that start with the letter '{0}':", wordGroup.Key);
foreach (var word in wordGroup)
{
Console.WriteLine(word);
}
}

// Keep the console window open in debug mode


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Words that start with the letter 'b':
blueberry
banana
Words that start with the letter 'c':
chimpanzee
cheese
Words that start with the letter 'a':
abacus
apple
*/

Beispiel 2
Dieses Beispiel zeigt, wie zusätzliche Logik auf die Gruppen ausgeführt wird, nachdem Sie diese erstellt haben,
indem eine Fortsetzung mit into verwendet wird. Weitere Informationen finden Sie unter into. Das folgende
Beispiel fragt jede Gruppe ab, wobei nur die Gruppe ausgewählt werden soll, dessen Schlüsselwert ein Vokal ist.
class GroupClauseExample2
{
static void Main()
{
// Create the data source.
string[] words2 = { "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese", "elephant",
"umbrella", "anteater" };

// Create the query.


var wordGroups2 =
from w in words2
group w by w[0] into grps
where (grps.Key == 'a' || grps.Key == 'e' || grps.Key == 'i'
|| grps.Key == 'o' || grps.Key == 'u')
select grps;

// Execute the query.


foreach (var wordGroup in wordGroups2)
{
Console.WriteLine("Groups that start with a vowel: {0}", wordGroup.Key);
foreach (var word in wordGroup)
{
Console.WriteLine(" {0}", word);
}
}

// Keep the console window open in debug mode


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Groups that start with a vowel: a
abacus
apple
anteater
Groups that start with a vowel: e
elephant
Groups that start with a vowel: u
umbrella
*/

Hinweise
group -Klauseln werden zur Kompilierzeit in Aufrufe der GroupBy-Methode übersetzt.

Weitere Informationen
IGrouping<TKey,TElement>
GroupBy
ThenBy
ThenByDescending
Abfrageschlüsselwörter
Language-Integrated Query (LINQ)
Erstellen einer geschachtelten Gruppe
Gruppieren von Abfrageergebnissen
Ausführen einer Unterabfrage für eine Gruppierungsoperation
into (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Kontextschlüsselwort into kann zum Erstellen eines temporären Bezeichners verwendet werden, der die
Ergebnisse einer group-, join- oder select-Klausel in einem neuen Bezeichner speichert. Dieser Bezeichner kann
wiederum ein Generator für zusätzliche Abfragebefehle sein. In einer group - oder select -Klausel wird die
Verwendung des neuen Bezeichners auch als Fortsetzung bezeichnet.

Beispiel
Das folgende Beispiel zeigt die Verwendung des Schlüsselworts into zur Aktivierung eines temporären
Bezeichners fruitGroup , der über einen abgeleiteten Typ IGrouping verfügt. Mit diesem Bezeichner können Sie
die Count-Methode für jede Gruppe aufrufen und nur die Gruppen auswählen, die mindestens zwei Wörter
enthalten.

class IntoSample1
{
static void Main()
{

// Create a data source.


string[] words = { "apples", "blueberries", "oranges", "bananas", "apricots"};

// Create the query.


var wordGroups1 =
from w in words
group w by w[0] into fruitGroup
where fruitGroup.Count() >= 2
select new { FirstLetter = fruitGroup.Key, Words = fruitGroup.Count() };

// Execute the query. Note that we only iterate over the groups,
// not the items in each group
foreach (var item in wordGroups1)
{
Console.WriteLine(" {0} has {1} elements.", item.FirstLetter, item.Words);
}

// Keep the console window open in debug mode


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
a has 2 elements.
b has 2 elements.
*/

Die Verwendung von into in einer group -Klausel ist nur erforderlich, wenn Sie zusätzliche Abfragevorgänge
für jede Gruppe ausführen möchten. Weitere Informationen finden Sie unter group-Klausel.
Ein Beispiel für die Verwendung von into in einer join -Klausel finden Sie unter join-Klausel.

Weitere Informationen
Abfrageschlüsselwörter (LINQ)
LINQ in C#
group-Klausel
orderby-Klausel (C#-Referenz)
04.11.2021 • 2 minutes to read

In einem Abfrageausdruck bewirkt die orderby -Klausel, dass die zurückgegebene Sequenz oder Untersequenz
(Gruppe) entweder in aufsteigender oder absteigender Reihenfolge sortiert wird. Es können mehrere Schlüssel
angegeben werden, um eine oder mehrere sekundäre Sortiervorgänge auszuführen. Die Sortierung erfolgt mit
dem Standardvergleich für den Typ des Elements. Standardmäßig wird eine aufsteigende Sortierreihenfolge
verwendet. Sie können auch einen benutzerdefinierten Vergleich angeben. Der ist jedoch nur mit der
methodenbasierten Syntax verfügbar. Weitere Informationen finden Sie unter Sortieren von Daten.

Beispiel 1
Im folgenden Beispiel sortiert die erste Abfrage die Wörter in alphabetischer Reihenfolge, beginnend mit A, und
die zweite Abfrage die gleichen Wörter in absteigender Reihenfolge. (Das Schlüsselwort ascending ist der
Standardwert für das Sortieren und kann ausgelassen werden.)
class OrderbySample1
{
static void Main()
{
// Create a delicious data source.
string[] fruits = { "cherry", "apple", "blueberry" };

// Query for ascending sort.


IEnumerable<string> sortAscendingQuery =
from fruit in fruits
orderby fruit //"ascending" is default
select fruit;

// Query for descending sort.


IEnumerable<string> sortDescendingQuery =
from w in fruits
orderby w descending
select w;

// Execute the query.


Console.WriteLine("Ascending:");
foreach (string s in sortAscendingQuery)
{
Console.WriteLine(s);
}

// Execute the query.


Console.WriteLine(Environment.NewLine + "Descending:");
foreach (string s in sortDescendingQuery)
{
Console.WriteLine(s);
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Ascending:
apple
blueberry
cherry

Descending:
cherry
blueberry
apple
*/

Beispiel 2
Im folgenden Beispiel wird eine primäre Sortierung der Nachnamen von Studenten und dann eine sekundäre
Sortierung auf ihre Vornamen ausgeführt.

class OrderbySample2
{
// The element type of the data source.
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
}
public static List<Student> GetStudents()
{
// Use a collection initializer to create the data source. Note that each element
// in the list contains an inner sequence of scores.
List<Student> students = new List<Student>
{
new Student {First="Svetlana", Last="Omelchenko", ID=111},
new Student {First="Claire", Last="O'Donnell", ID=112},
new Student {First="Sven", Last="Mortensen", ID=113},
new Student {First="Cesar", Last="Garcia", ID=114},
new Student {First="Debra", Last="Garcia", ID=115}
};

return students;
}
static void Main(string[] args)
{
// Create the data source.
List<Student> students = GetStudents();

// Create the query.


IEnumerable<Student> sortedStudents =
from student in students
orderby student.Last ascending, student.First ascending
select student;

// Execute the query.


Console.WriteLine("sortedStudents:");
foreach (Student student in sortedStudents)
Console.WriteLine(student.Last + " " + student.First);

// Now create groups and sort the groups. The query first sorts the names
// of all students so that they will be in alphabetical order after they are
// grouped. The second orderby sorts the group keys in alpha order.
var sortedGroups =
from student in students
orderby student.Last, student.First
group student by student.Last[0] into newGroup
orderby newGroup.Key
select newGroup;

// Execute the query.


Console.WriteLine(Environment.NewLine + "sortedGroups:");
foreach (var studentGroup in sortedGroups)
{
Console.WriteLine(studentGroup.Key);
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}", student.Last, student.First);
}
}

// Keep the console window open in debug mode


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
sortedStudents:
Garcia Cesar
Garcia Debra
Mortensen Sven
O'Donnell Claire
Omelchenko Svetlana

sortedGroups:
G
Garcia, Cesar
Garcia, Debra
Garcia, Debra
M
Mortensen, Sven
O
O'Donnell, Claire
Omelchenko, Svetlana
*/

Hinweise
Zur Kompilierzeit wird die orderby -Klausel in einen Aufruf der OrderBy-Methode übersetzt. Mehrere Schlüssel
in der orderby -Klausel werden in ThenBy-Methodenaufrufe übersetzt.

Siehe auch
C#-Referenz
Abfrageschlüsselwörter (LINQ)
LINQ in C#
group-Klausel
Language-Integrated Query (LINQ)
join-Klausel (C#-Referenz)
04.11.2021 • 9 minutes to read

Die join -Klausel ist sehr nützlich beim verknüpfen von Elementen aus unterschiedlichen Quellsequenzen, die
keine direkte Beziehung im Objektmodell haben. Die Elemente müssen lediglich in jeder Quelle einige Werte
freigeben, die auf Gleichheit verglichen werden können. Ein Lebensmittelgroßhändler hat z.B. eine Liste seiner
Lieferanten und seiner Käufer für ein bestimmtes Produkt. Eine join -Klausel kann beispielsweise verwendet
werden, um eine Liste von Lieferanten und Käufern dieses Produkts zu erstellen, die sich alle in einer
angegebenen Region befinden.
Eine join -Klausel akzeptiert zwei Quellsequenzen als Eingabe. Die Elemente jeder Sequenz müssen entweder
eine Eigenschaft sein oder eine Eigenschaft enthalten, die mit einer entsprechenden Eigenschaft einer anderen
Sequenz verglichen werden kann. Die join -Klausel vergleicht die angegebenen Schlüssel auf Gleichheit, indem
sie das besonderen Schlüsselwort equals verwendet. Alle Verknüpfungen, die von der join -Klausel
vorgenommen werden, sind Gleichheitsverknüpfungen. Die Form der Ausgabe einer join -Klausel hängt vom
Typ der durchgeführten Verknüpfung ab. Hier sind die am häufigsten vorkommenden Verknüpfungstypen:
Innere Verknüpfung
Gruppenverknüpfung
Left Outer Join

Innere Verknüpfung
Im folgenden Beispiel wird eine einfache Gleichheitsverknüpfung dargestellt. Diese Abfrage produziert eine
flache Sequenz aus Paaren der Form „Produktname/Kategorie“. Die gleiche Kategoriezeichenfolge taucht in
mehreren Elementen auf. Wenn ein Element aus categories keine entsprechenden products hat, wird diese
Kategorie nicht in den Ergebnissen angezeigt.

var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { ProductName = prod.Name, Category = category.Name }; //produces flat sequence

Weitere Informationen finden Sie unter Ausführen von inneren Verknüpfungen.

Gruppenverknüpfung
Eine join -Klausel mit einem into -Ausdruck wird Gruppenverknüpfung genannt.

var innerGroupJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select new { CategoryName = category.Name, Products = prodGroup };

Eine Gruppenverknüpfung erstellt eine hierarchische Ergebnissequenz, die Elemente in der linken Quellsequenz
mit mindestens einem entsprechenden Element der rechten Quellsequenz verknüpft. Eine Gruppenverknüpfung
hat keine entsprechenden Beziehungsbedingungen; eigentlich ist sie eine Sequenz von Objektarrays.
Wenn keine Elemente der rechten Quellsequenz gefunden werden, die mit Elementen der linken Sequenz
übereinstimmen, erstellt die join -Klausel ein leeres Array für dieses Element. Deshalb ist eine
Gruppenverknüpfung immer noch grundsätzlich eine innere Gleichheitsverknüpfung, nur dass die
Ergebnissequenz in Gruppen aufgeteilt ist.
Wenn Sie nur das Ergebnis einer Gruppenverknüpfung auswählen, können Sie auf Elemente zugreifen, aber Sie
können nicht den Schlüssel identifizieren, der ihnen gleich ist. Deshalb ist es im Allgemeinen sinnvoller, das
Ergebnis der Gruppenverknüpfung in einem neuen Typen auszuwählen, der auch einen Schlüsselnamen
aufweist – so wie im vorherigen Ergebnis erläutert.
Selbstverständlich können Sie auch das Ergebnis einer Gruppenverknüpfung als Generator einer anderen
Unterabfrage verwenden:

var innerGroupJoinQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from prod2 in prodGroup
where prod2.UnitPrice > 2.50M
select prod2;

Weitere Informationen finden Sie unter Ausführen von Gruppenverknüpfungen.

Left Outer Join


In einem Left Outer Join werden alle Elemente der linken Quellsequenz zurückgegeben, auch wenn sich keine
entsprechenden Elemente in der rechten Sequenz befinden. Um einen Left Outer Join in LINQ durchzuführen,
verwenden Sie die Methode DefaultIfEmpty zusammen mit einer Gruppenverknüpfung, um ein
Standardelement für den rechten Bereich festzulegen, das erstellt wird, wenn es kein entsprechendes Element im
linken Bereich gibt. Sie können null als Standardwert für jeden beliebigen Verweistyp verwenden, oder Sie
können einen benutzerdefinierten Standardtyp festlegen. Im folgendem Beispiel wird ein benutzerdefinierter
Standardtyp dargestellt:

var leftOuterJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from item in prodGroup.DefaultIfEmpty(new Product { Name = String.Empty, CategoryID = 0 })
select new { CatName = category.Name, ProdName = item.Name };

Weitere Informationen finden Sie unter Ausführen von Left Outer Joins.

Der equals-Operator
Eine join -Klausel führt eine Gleichheitsverknüpfung durch. D.h., dass Übereinstimmungen nur auf der
Gleichheit zweier Schlüssel basieren können. Andere Vergleichstypen wie etwa „größer als“ oder „ungleich“
werden nicht unterstützt. Um sicherzustellen, dass die Verknüpfungen Gleichheitsverknüpfungen sind,
verwendet die join -Klausel das Schlüsselwort equals statt des Operators == . Das Schlüsselwort equals
kann nur in einer join -Klausel verwendet werden, und es unterscheidet sich vom Operator == in einem
wesentlichen Punkt. Der linke Schlüssel verarbeitet mit equals die äußere Quellsequenz, und der rechte
Schlüssel verarbeitet die innere Quelle. Die äußere Quelle befindet sich nur links von equals im
Geltungsbereich, und die innere Quellsequenz befindet sich nur auf der rechten Seite im Geltungsbereich.

Nicht-Gleichheitsverknüpfungen
Sie können Nicht-Gleichheitsverknüpfungen, Kreuzverknüpfungen und andere benutzerdefinierte
Verknüpfungen durchführen, indem Sie mehrere from -Klauseln verwenden, um unabhängig neue Sequenzen
in eine Abfrage einzuführen. Weitere Informationen finden Sie unter Ausführen von benutzerdefinierten
Verknüpfungsoperationen.

Verknüpfungen für Objektauflistungen vs. relationale Tabellen


Verknüpfungsvorgänge in einem LINQ-Abfrageausdruck werden in Objektsammlungen durchgeführt.
Objektauflistungen können nicht wie relationale Tabellen „verknüpft“ werden. In LINQ sind explizite join -
Klauseln nur erforderlich, wenn zwei Quellsequenzen nicht durch eine Beziehung verbunden sind. Wenn Sie mit
LINQ to SQL arbeiten, werden Tabellen mit Fremdschlüsseln in einem Objektmodell als Eigenschaften der
primären Tabelle repräsentiert. In der Northwind-Datenbank weist die Tabelle „Customers“ (Kunden)
beispielsweise eine Fremdschlüsselbeziehung zu der Tabelle „Orders“ (Aufträge) auf. Wenn Sie die Tabellen dem
Objektmodell zuordnen, hat die Klasse „Customers“ eine Eigenschaft „Orders“, die die Auflistung der Aufträge
enthält, die zu diesem Kunden gehören. Tatsächlich wurde die Verknüpfung bereits für Sie vorgenommen.
Weitere Informationen zu Abfragen über verknüpfte Quellen hinweg in Bezug auf LINQ to SQL finden Sie unter
Vorgehensweise: Zuordnen von Datenbankbeziehungen.

Zusammengesetzte Schlüssel
Mithilfe eines zusammengesetzten Schlüssels können Sie auf die Gleichheit mehrerer Werte prüfen. Weitere
Informationen finden Sie unter Verknüpfen mithilfe eines zusammengesetzten Schlüssels. Zusammengesetzte
Schlüssel können auch in einer group -Klausel verwendet werden.

Beispiel
In folgendem Beispiel werden die Ergebnisse einer inneren Verknüpfung, einer Gruppenverknüpfung und einer
linken äußeren Verknüpfung in der gleichen Datenquelle anhand derselben übereinstimmenden Schlüssel
miteinander verglichen. Diesen Beispielen wurde zusätzlicher Code hinzugefügt, um die Ergebnisse in der
Konsolenanzeige zu verdeutlichen.

class JoinDemonstration
{
#region Data

class Product
{
public string Name { get; set; }
public int CategoryID { get; set; }
}

class Category
{
public string Name { get; set; }
public int ID { get; set; }
}

// Specify the first data source.


List<Category> categories = new List<Category>()
{
new Category {Name="Beverages", ID=001},
new Category {Name="Condiments", ID=002},
new Category {Name="Vegetables", ID=003},
new Category {Name="Grains", ID=004},
new Category {Name="Fruit", ID=005}
};

// Specify the second data source.


List<Product> products = new List<Product>()
{
new Product {Name="Cola", CategoryID=001},
new Product {Name="Tea", CategoryID=001},
new Product {Name="Tea", CategoryID=001},
new Product {Name="Mustard", CategoryID=002},
new Product {Name="Pickles", CategoryID=002},
new Product {Name="Carrots", CategoryID=003},
new Product {Name="Bok Choy", CategoryID=003},
new Product {Name="Peaches", CategoryID=005},
new Product {Name="Melons", CategoryID=005},
};
#endregion

static void Main(string[] args)


{
JoinDemonstration app = new JoinDemonstration();

app.InnerJoin();
app.GroupJoin();
app.GroupInnerJoin();
app.GroupJoin3();
app.LeftOuterJoin();
app.LeftOuterJoin2();

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}

void InnerJoin()
{
// Create the query that selects
// a property from each element.
var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { Category = category.ID, Product = prod.Name };

Console.WriteLine("InnerJoin:");
// Execute the query. Access results
// with a simple foreach statement.
foreach (var item in innerJoinQuery)
{
Console.WriteLine("{0,-10}{1}", item.Product, item.Category);
}
Console.WriteLine("InnerJoin: {0} items in 1 group.", innerJoinQuery.Count());
Console.WriteLine(System.Environment.NewLine);
}

void GroupJoin()
{
// This is a demonstration query to show the output
// of a "raw" group join. A more typical group join
// is shown in the GroupInnerJoin method.
var groupJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select prodGroup;

// Store the count of total items (for demonstration only).


int totalItems = 0;

Console.WriteLine("Simple GroupJoin:");

// A nested foreach statement is required to access group items.


foreach (var prodGrouping in groupJoinQuery)
{
Console.WriteLine("Group:");
foreach (var item in prodGrouping)
{
totalItems++;
Console.WriteLine(" {0,-10}{1}", item.Name, item.CategoryID);
}
}
Console.WriteLine("Unshaped GroupJoin: {0} items in {1} unnamed groups", totalItems,
groupJoinQuery.Count());
Console.WriteLine(System.Environment.NewLine);
}

void GroupInnerJoin()
{
var groupJoinQuery2 =
from category in categories
orderby category.ID
join prod in products on category.ID equals prod.CategoryID into prodGroup
select new
{
Category = category.Name,
Products = from prod2 in prodGroup
orderby prod2.Name
select prod2
};

//Console.WriteLine("GroupInnerJoin:");
int totalItems = 0;

Console.WriteLine("GroupInnerJoin:");
foreach (var productGroup in groupJoinQuery2)
{
Console.WriteLine(productGroup.Category);
foreach (var prodItem in productGroup.Products)
{
totalItems++;
Console.WriteLine(" {0,-10} {1}", prodItem.Name, prodItem.CategoryID);
}
}
Console.WriteLine("GroupInnerJoin: {0} items in {1} named groups", totalItems,
groupJoinQuery2.Count());
Console.WriteLine(System.Environment.NewLine);
}

void GroupJoin3()
{

var groupJoinQuery3 =
from category in categories
join product in products on category.ID equals product.CategoryID into prodGroup
from prod in prodGroup
orderby prod.CategoryID
select new { Category = prod.CategoryID, ProductName = prod.Name };

//Console.WriteLine("GroupInnerJoin:");
int totalItems = 0;

Console.WriteLine("GroupJoin3:");
foreach (var item in groupJoinQuery3)
{
totalItems++;
Console.WriteLine(" {0}:{1}", item.ProductName, item.Category);
}

Console.WriteLine("GroupJoin3: {0} items in 1 group", totalItems);


Console.WriteLine(System.Environment.NewLine);
}

void LeftOuterJoin()
{
// Create the query.
var leftOuterQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select prodGroup.DefaultIfEmpty(new Product() { Name = "Nothing!", CategoryID = category.ID });

// Store the count of total items (for demonstration only).


int totalItems = 0;

Console.WriteLine("Left Outer Join:");

// A nested foreach statement is required to access group items


foreach (var prodGrouping in leftOuterQuery)
{
Console.WriteLine("Group:");
foreach (var item in prodGrouping)
{
totalItems++;
Console.WriteLine(" {0,-10}{1}", item.Name, item.CategoryID);
}
}
Console.WriteLine("LeftOuterJoin: {0} items in {1} groups", totalItems, leftOuterQuery.Count());
Console.WriteLine(System.Environment.NewLine);
}

void LeftOuterJoin2()
{
// Create the query.
var leftOuterQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from item in prodGroup.DefaultIfEmpty()
select new { Name = item == null ? "Nothing!" : item.Name, CategoryID = category.ID };

Console.WriteLine("LeftOuterJoin2: {0} items in 1 group", leftOuterQuery2.Count());


// Store the count of total items
int totalItems = 0;

Console.WriteLine("Left Outer Join 2:");

// Groups have been flattened.


foreach (var item in leftOuterQuery2)
{
totalItems++;
Console.WriteLine("{0,-10}{1}", item.Name, item.CategoryID);
}
Console.WriteLine("LeftOuterJoin2: {0} items in 1 group", totalItems);
}
}
/*Output:

InnerJoin:
Cola 1
Tea 1
Mustard 2
Pickles 2
Carrots 3
Bok Choy 3
Peaches 5
Melons 5
InnerJoin: 8 items in 1 group.

Unshaped GroupJoin:
Group:
Cola 1
Tea 1
Group:
Mustard 2
Pickles 2
Group:
Carrots 3
Bok Choy 3
Bok Choy 3
Group:
Group:
Peaches 5
Melons 5
Unshaped GroupJoin: 8 items in 5 unnamed groups

GroupInnerJoin:
Beverages
Cola 1
Tea 1
Condiments
Mustard 2
Pickles 2
Vegetables
Bok Choy 3
Carrots 3
Grains
Fruit
Melons 5
Peaches 5
GroupInnerJoin: 8 items in 5 named groups

GroupJoin3:
Cola:1
Tea:1
Mustard:2
Pickles:2
Carrots:3
Bok Choy:3
Peaches:5
Melons:5
GroupJoin3: 8 items in 1 group

Left Outer Join:


Group:
Cola 1
Tea 1
Group:
Mustard 2
Pickles 2
Group:
Carrots 3
Bok Choy 3
Group:
Nothing! 4
Group:
Peaches 5
Melons 5
LeftOuterJoin: 9 items in 5 groups

LeftOuterJoin2: 9 items in 1 group


Left Outer Join 2:
Cola 1
Tea 1
Mustard 2
Pickles 2
Carrots 3
Bok Choy 3
Nothing! 4
Peaches 5
Melons 5
LeftOuterJoin2: 9 items in 1 group
Press any key to exit.
*/
Hinweise
Eine join -Klausel, auf die kein into folgt, wird in einen Join-Methodenaufruf übersetzt. Eine join -Klausel, auf
die into folgt, wird in einen GroupJoin-Methodenaufruf übersetzt.

Weitere Informationen
Abfrageschlüsselwörter (LINQ)
Language-Integrated Query (LINQ)
Verknüpfungsvorgänge
group-Klausel
Ausführen linker äußerer Verknüpfungen
Ausführen innerer Verknüpfungen
Ausführen von Gruppenverknüpfungen
Sortieren der Ergebnisse einer Join-Klausel
Verknüpfen mithilfe eines zusammengesetzten Schlüssels
Kompatible Datenbanksysteme für Visual Studio
let-Klausel (C#-Referenz)
04.11.2021 • 2 minutes to read

Bei einem Abfrageausdruck kann es manchmal nützlich sein, das Ergebnis eines Unterausdrucks zur
Verwendung in nachfolgenden Klauseln zu speichern. Sie können hierzu das Schlüsselwort let verwenden,
das eine neue Bereichsvariable erstellt und sie mit dem von Ihnen bereitgestellten Ergebnis des Ausdrucks
initialisiert. Sobald die Bereichsvariable mit einem Wert initialisiert wurde, kann sie nicht mehr zum Speichern
eines anderen Werts verwendet werden. Enthält die Bereichsvariable jedoch einen abfragbaren Typ, kann sie
abgefragt werden.

Beispiel
Im folgenden Beispiel wird let auf zweierlei Weise verwendet:
1. Um einen aufzählbaren Typ zu erstellen, der selbst abgefragt werden kann.
2. Um es der Abfrage zu ermöglichen, ToLower nur ein Mal für die Bereichsvariable word aufzurufen. Ohne
let müssten Sie ToLower in jedem Prädikat der where -Klausel aufrufen.
class LetSample1
{
static void Main()
{
string[] strings =
{
"A penny saved is a penny earned.",
"The early bird catches the worm.",
"The pen is mightier than the sword."
};

// Split the sentence into an array of words


// and select those whose first letter is a vowel.
var earlyBirdQuery =
from sentence in strings
let words = sentence.Split(' ')
from word in words
let w = word.ToLower()
where w[0] == 'a' || w[0] == 'e'
|| w[0] == 'i' || w[0] == 'o'
|| w[0] == 'u'
select word;

// Execute the query.


foreach (var v in earlyBirdQuery)
{
Console.WriteLine("\"{0}\" starts with a vowel", v);
}

// Keep the console window open in debug mode.


Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
"A" starts with a vowel
"is" starts with a vowel
"a" starts with a vowel
"earned." starts with a vowel
"early" starts with a vowel
"is" starts with a vowel
*/

Siehe auch
C#-Referenz
Abfrageschlüsselwörter (LINQ)
LINQ in C#
Language-Integrated Query (LINQ)
Behandeln von Ausnahmen in Abfrageausdrücken
ascending (C#-Referenz)
04.11.2021 • 2 minutes to read

Das kontextbezogene Schlüsselwort ascending wird in der orderby-Klausel in Abfrageausdrücken verwendet,


um die Sortierreihenfolge vom kleinsten zum größten Element festzulegen. ascending ist die standardmäßige
Sortierreihenfolge und muss nicht extra festgelegt werden.

Beispiel
Im folgenden Beispiel wird die Verwendung von ascending in einer orderby-Klausel gezeigt.

IEnumerable<string> sortAscendingQuery =
from vegetable in vegetables
orderby vegetable ascending
select vegetable;

Siehe auch
C#-Referenz
LINQ in C#
descending
descending (C#-Referenz)
04.11.2021 • 2 minutes to read

Das kontextbezogene Schlüsselwort descending wird in der orderby-Klausel in Abfrageausdrücken verwendet,


um die Sortierreihenfolge vom größten zum kleinsten Element festzulegen.

Beispiel
Im folgenden Beispiel wird die Verwendung von descending in einer orderby-Klausel gezeigt.

IEnumerable<string> sortDescendingQuery =
from vegetable in vegetables
orderby vegetable descending
select vegetable;

Siehe auch
C#-Referenz
LINQ in C#
ascending
on (C#-Referenz)
04.11.2021 • 2 minutes to read

Das kontextabhängiges Schlüsselwort on wird in der join-Klausel eines Abfrageausdrucks, um die


Verknüpfungsbedingung anzugeben.

Beispiel
Im folgenden Beispiel wird die Verwendung von on in einer join -Klausel gezeigt.

var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { ProductName = prod.Name, Category = category.Name };

Siehe auch
C#-Referenz
Language-Integrated Query (LINQ)
equals (C#-Referenz)
04.11.2021 • 2 minutes to read

Das kontextabhängige Schlüsselwort equals wird in einer join -Klausel in einem Abfrageausdruck verwendet,
um die Elemente zweier Sequenzen zu vergleichen. Weitere Informationen finden Sie unter join-Klausel.

Beispiel
Im folgenden Beispiel wird die Verwendung des Schlüsselworts equals in einer join -Klausel gezeigt.

var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { ProductName = prod.Name, Category = category.Name };

Siehe auch
Language-Integrated Query (LINQ)
by (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Kontextschlüsselwort by wird in der group -Klausel in einem Abfrageausdruck verwendet, um anzugeben,
wie die zurückgegebenen Elemente gruppiert werden sollen. Weitere Informationen finden Sie unter group-
Klausel.

Beispiel
Im folgenden Beispiel wird gezeigt, wie das Kontextschlüsselwort by in einer group -Klausel verwendet wird,
um anzugeben, dass die Kursteilnehmer anhand des ersten Buchstaben des Nachnamens gruppiert werden
sollen.

var query = from student in students


group student by student.LastName[0];

Weitere Informationen
LINQ in C#
in (C#-Referenz)
04.11.2021 • 2 minutes to read

Das in -Schlüsselwort kann in folgendem Kontext verwendet werden:


generische Typparameter in generischen Schnittstellen und Delegaten
Als Parametermodifizierer, mit dem Sie ein Argument an eine Methode nach Verweis anstatt nach Wert
übergeben können.
foreach-Anweisungen
from-Klauseln in LINQ-Abfrageausdrücken
join-Klauseln in LINQ-Abfrageausdrücken

Siehe auch
C#-Schlüsselwörter
C#-Referenz
C#-Operatoren und -Ausdrücke (C#-Referenz)
04.11.2021 • 5 minutes to read

C# bietet viele verschiedene Operatoren. Viele dieser Operatoren werden von den integrierten Typen unterstützt
und ermöglichen es Ihnen, grundlegende Vorgänge mit den Werten dieser Typen auszuführen. Diese
Operatoren sind in folgende Gruppen unterteilt:
Arithmetische Operatoren, die arithmetische Operationen mit numerischen Operanden ausführen
Vergleichsoperatoren, die numerische Operanden vergleichen
Boolesche Logikoperatoren, die logische Vorgänge mit bool -Operanden ausführen
Bitweise Operatoren und Schiebeoperatoren, die bitweise Vorgänge oder Schiebevorgänge mit Operanden
des integralen Typs ausführen
Gleichheitsoperatoren, die überprüfen, ob Operanden gleich sind oder nicht
In der Regel können Sie diese Operatoren überladen, also das Operatorverhalten für die Operanden eines
benutzerdefinierten Typs angeben.
Die einfachsten C#-Ausdrücke sind Literale (zum Beispiel Integer und reelle Zahlen) sowie Namen von Variablen.
Sie können diese mithilfe von Operatoren in komplexen Ausdrücken kombinieren. Die Operatorrangfolge und -
assoziativität legen die Reihenfolge fest, in der die Vorgänge in einem Ausdruck durchgeführt werden. Sie
können Klammern verwenden, um die Reihenfolge der Auswertung zu ändern, die durch die Operatorrangfolge
und -assoziativität festgelegt wird.
Im folgenden Code befinden sich die Beispiele der Ausdrücke auf der rechten Seite der Zuweisungen:

int a, b, c;
a = 7;
b = a;
c = b++;
b = a + b * c;
c = a >= 100 ? b : c / 10;
a = (int)Math.Sqrt(b * b + c * c);

string s = "String literal";


char l = s[s.Length - 1];

var numbers = new List<int>(new[] { 1, 2, 3 });


b = numbers.FindLast(n => n > 1);

In der Regel erzeugt ein Ausdruck ein Ergebnis und kann in einen anderen Ausdruck eingeschlossen werden. Ein
void -Methodenaufruf ist ein Beispiel eines Ausdrucks, der kein Ergebnis erzeugt. Er kann nur als Anweisung
wie im folgenden Beispiel verwendet werden.

Console.WriteLine("Hello, world!");

Im Folgenden finden Sie einige weitere Ausdrücke, die von C# bereitgestellt werden:
Interpolierte Zeichenfolgenausdrücke, die eine bequeme Syntax zum Erstellen formatierter Zeichenfolgen
bereitstellen:
var r = 2.3;
var message = $"The area of a circle with radius {r} is {Math.PI * r * r:F3}.";
Console.WriteLine(message);
// Output:
// The area of a circle with radius 2.3 is 16.619.

Lambdaausdrücke, mit denen Sie anonyme Funktionen erstellen können:

int[] numbers = { 2, 3, 4, 5 };
var maximumSquare = numbers.Max(x => x * x);
Console.WriteLine(maximumSquare);
// Output:
// 25

Abfrageausdrücke, mit denen Sie Abfragefunktionen direkt in C# verwenden können:

var scores = new[] { 90, 97, 78, 68, 85 };


IEnumerable<int> highScoresQuery =
from score in scores
where score > 80
orderby score descending
select score;
Console.WriteLine(string.Join(" ", highScoresQuery));
// Output:
// 97 90 85

Sie können eine Ausdruckskörperfunktion verwenden, um eine präzise Definition für eine Methode, einen
Konstruktor, eine Eigenschaft, einen Indexer oder einen Finalizer bereitzustellen.

Operatorrangfolge
In einem Ausdruck mit mehreren Operatoren werden die Operatoren mit höherer Rangfolge vor den
Operatoren mit niedrigerer Rangfolge ausgewertet. Im folgenden Beispiel wird die Multiplikation zuerst
durchgeführt, da Sie eine höhere Rangfolge aufweist als die Addition:

var a = 2 + 2 * 2;
Console.WriteLine(a); // output: 6

Verwenden Sie Klammern, um die Reihenfolge der Auswertung zu ändern, die durch die Operatorrangfolge
festgelegt ist:

var a = (2 + 2) * 2;
Console.WriteLine(a); // output: 8

Die folgende Tabelle listen die C#-Operatoren auf, und zwar von der höchsten zur niedrigsten Rangfolge. Die
Operatoren in jeder Zeile weisen die gleiche Rangfolge auf.

O P ERATO REN K AT EGO RIE O DER N A M E

x.y, f(x), a[i], x?.y , x?[y] , x++, x--, x!, new, typeof, Primär
checked, unchecked, default, nameof, delegate, sizeof,
stackalloc, x->y

+x, -x, !x, ~x, ++x, --x, ^x, (T)x, await, &x, *x, true und false Unär
O P ERATO REN K AT EGO RIE O DER N A M E

x..y Bereich

switch switch -Ausdruck

mit with -Ausdruck

x * y, x / y, x % y Multiplikativ

x + y, x – y Additiv

x << y, x >> y Shift

x < y, x > y, x <= y, x >= y, is, as Relational und Typtest

x == y, x != y Gleichheit

x & y Boolescher logischer AND-Operator oder bitweiser logischer


AND-Operator

x ^ y Boolescher logischer XOR-Operator oder bitweiser logischer


XOR-Operator

x | y Boolescher logischer OR-Operator oder bitweiser logischer


OR-Operator

x && y Bedingtes AND

x || y Bedingtes OR

x ?? y Nullzusammensetzungsoperator

c?t:f Bedingter Operator

x = y, x += y, x -= y, x *= y, x /= y, x %= y, x &= y, x |= y, x Zuweisungs- und Lambdadeklaration


^= y, x <<= y, x >>= y, x ??= y, =>

Operatorassoziativität
Wenn Operatoren die gleiche Rangfolge aufweisen, legt die Assoziativität der Operatoren die Reihenfolge fest,
in der Vorgänge durchgeführt werden:
Linksassoziative Operatoren werden von links nach rechts ausgewertet. Mit Ausnahme der
Zuweisungsoperatoren und des NULL-Sammeloperators sind alle binären Operatoren linksassoziativ.
a + b - c wird beispielsweise als (a + b) - c ausgewertet.
Rechtsassoziative Operatoren werden von rechts nach links ausgewertet. Die Zuweisungsoperatoren, die
NULL-Sammeloperatoren und der bedingte Operator ?: sind rechtsassoziativ. x = y = z wird
beispielsweise als x = (y = z) ausgewertet.

Verwenden Sie Klammern, um die Reihenfolge der Auswertung zu ändern, die durch die Operatorassoziativität
festgelegt ist:
int a = 13 / 5 / 2;
int b = 13 / (5 / 2);
Console.WriteLine($"a = {a}, b = {b}"); // output: a = 1, b = 6

Operandenauswertung
Unabhängig von der Operatorrangfolge und -assoziativität werden die Operanden in einem Ausdruck von links
nach rechts ausgewertet. Die folgenden Beispiele veranschaulichen die Reihenfolge, in der Operatoren und
Operanden ausgewertet werden:

EXP RESSIO N REIH EN F O L GE DER A USW ERT UN G

a + b a, b, +

a + b * c a, b, c, *, +

a / b + c * d a, b, /, c, d, *, +

a / (b + c) * d a, b, c, +, /, d, *

In der Regel werden alle Operanden eines Operators ausgewertet. Einige Operatoren werten Operanden jedoch
bedingt aus. Das heißt, der Wert des Operanden ganz links in einem solchen Operator definiert, ob (oder
welche) andere(n) Operanden ausgewertet werden sollen. Diese Operatoren sind die bedingten logischen
Operatoren AND ( && ) und OR ( || ), die NULL-Sammeloperatoren ?? und ??= , die NULL-bedingten
Operatoren ?. und ?[] sowie der bedingte Operator ?: . Weitere Informationen finden Sie in der
Beschreibung jedes Operators.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Ausdrücke
Operatoren

Siehe auch
C#-Referenz
Operatorüberladung
Ausdrucksbaumstrukturen
Arithmetische Operatoren (C#-Referenz)
04.11.2021 • 9 minutes to read

Die folgenden Operatoren führen arithmetische Operationen mit Operanden des numerischen Typs aus:
Unäre Operatoren: ++ (inkrementell), -- (dekrementell), + (plus) und - (minus)
Binäre Operatoren: * (Multiplikation), / (Division), % (Rest), + (Addition) und - (Subtraktion)
Diese Operatoren werden alle von numerischen Ganzzahl- und Gleitkommatypen unterstützt.
Bei integralen Typen werden diese Operatoren (außer den Operatoren ++ und -- ) für die Typen int , uint ,
long und ulong definiert. Wenn Operanden andere integrale Typen aufweisen ( sbyte , byte , short , ushort
oder char ), werden ihre Werte in den Typ int konvertiert. Hierbei handelt es sich auch um den Ergebnistyp
einer Operation. Wenn Operanden abweichende integrale Typen oder Gleitkommatypen aufweisen, werden ihre
Werte in den am besten geeigneten enthaltenden Typ konvertiert, falls solch ein Typ vorhanden ist. Weitere
Informationen finden Sie im Abschnitt Numerische Heraufstufungen der Spezifikation für die Sprache C#. Die
++ - und -- -Operatoren werden für alle numerischen integralen und Gleitkommatypen sowie den char-Typ
definiert.

Inkrementoperator ++
Der unäre Inkrementoperator ( ++ ) erhöht seinen Operanden um 1. Der Operand muss eine Variable, ein
Eigenschaftenzugriff oder ein Indexerzugriff sein.
Der Inkrementoperator wird in zwei Formen unterstützt: als Postfix-Inkrementoperator x++ und als Präfix-
Inkrementoperator ++x .
Postfix-Operator für Inkrement
Das Ergebnis von x++ ist der Wert von x vor dem Vorgang, wie das folgende Beispiel zeigt:

int i = 3;
Console.WriteLine(i); // output: 3
Console.WriteLine(i++); // output: 3
Console.WriteLine(i); // output: 4

Präfixinkrement-Operator
Das Ergebnis von ++x ist der Wert von x nach dem Vorgang, wie das folgende Beispiel zeigt:

double a = 1.5;
Console.WriteLine(a); // output: 1.5
Console.WriteLine(++a); // output: 2.5
Console.WriteLine(a); // output: 2.5

Dekrementoperator --
Der unäre Dekrementoperator -- verringert seinen Operanden um 1. Der Operand muss eine Variable, ein
Eigenschaftenzugriff oder ein Indexerzugriff sein.
Der Dekrementoperator wird in zwei Formen unterstützt: als Postfix-Dekrementoperator x-- und als Präfix-
Dekrementoperator --x .
Postfix-Operator für Dekrement
Das Ergebnis von x-- ist der Wert von x vor dem Vorgang, wie das folgende Beispiel zeigt:

int i = 3;
Console.WriteLine(i); // output: 3
Console.WriteLine(i--); // output: 3
Console.WriteLine(i); // output: 2

Präfix-Dekrementoperator
Das Ergebnis von --x ist der Wert von x nach dem Vorgang, wie das folgende Beispiel zeigt:

double a = 1.5;
Console.WriteLine(a); // output: 1.5
Console.WriteLine(--a); // output: 0.5
Console.WriteLine(a); // output: 0.5

Unäre Plus- und Minusoperatoren


Der unäre + -Operator gibt den Wert seines Operanden zurück. Der unäre - -Operator berechnet die
numerische Negation des Operanden.

Console.WriteLine(+4); // output: 4

Console.WriteLine(-4); // output: -4
Console.WriteLine(-(-4)); // output: 4

uint a = 5;
var b = -a;
Console.WriteLine(b); // output: -5
Console.WriteLine(b.GetType()); // output: System.Int64

Console.WriteLine(-double.NaN); // output: NaN

Der ulong-Typ unterstützt den unären - -Operator nicht.

Multiplikationsoperator *
Der Multiplikationsoperator * berechnet das Produkt seiner Operanden:

Console.WriteLine(5 * 2); // output: 10


Console.WriteLine(0.5 * 2.5); // output: 1.25
Console.WriteLine(0.1m * 23.4m); // output: 2.34

Der unäre * -Operator ist der Zeigerdereferenzierungsoperator.

Divisionsoperator /
Der Divisionsoperator / dividiert den linken Operanden durch den rechten Operanden.
Ganzzahldivision
Für die Operanden von Ganzzahltypen weist das Ergebnis des / -Operators einen Ganzzahltyp auf und ist
gleich dem Quotienten der beiden Operanden, gerundet auf Null:
Console.WriteLine(13 / 5); // output: 2
Console.WriteLine(-13 / 5); // output: -2
Console.WriteLine(13 / -5); // output: -2
Console.WriteLine(-13 / -5); // output: 2

Um den Quotienten der beiden Operanden als Gleitkommazahl abzurufen, verwenden Sie den Typ float ,
double oder decimal :

Console.WriteLine(13 / 5.0); // output: 2.6

int a = 13;
int b = 5;
Console.WriteLine((double)a / b); // output: 2.6

Gleitkommadivision
Für die Typen float , double oder decimal ist das Ergebnis des / -Operators der Quotient der beiden
Operanden:

Console.WriteLine(16.8f / 4.1f); // output: 4.097561


Console.WriteLine(16.8d / 4.1d); // output: 4.09756097560976
Console.WriteLine(16.8m / 4.1m); // output: 4.0975609756097560975609756098

Wenn einer der Operanden decimal lautet, kann ein anderer Operand weder float noch double sein, weil
weder float noch double implizit zu decimal konvertiert werden können. Sie müssen den Operanden float
oder double explizit zum Typ decimal konvertieren. Weitere Informationen zu Konvertierungen zwischen
numerischen Typen finden Sie unter Built-in numeric conversions (Integrierte numerischer Konvertierungen).

Restoperator %
Der Restoperator % berechnet den Rest nach der Division seines linken Operanden durch den rechten
Operanden.
Ganzzahliger Rest
Für Operanden von Ganzzahltypen entspricht das Ergebnis von a % b dem von a - (a / b) * b erzeugten
Wert. Das Vorzeichen des Rests, der ungleich 0 (null) ist, ist wie im folgenden Beispiel gezeigt identisch mit dem
des linken Operanden:

Console.WriteLine(5 % 4); // output: 1


Console.WriteLine(5 % -4); // output: 1
Console.WriteLine(-5 % 4); // output: -1
Console.WriteLine(-5 % -4); // output: -1

Verwenden Sie die Math.DivRem-Methode, wenn Sie sowohl Ganzzahldivision als auch Restergebnisse
berechnen möchten.
Gleitkommarest
Für die Operanden float und double entspricht das Ergebnis von x % y für die endlichen Werte x und y
dem Wert z , sodass:
das Vorzeichen von z dem Vorzeichen von x entspricht, sofern der Wert nicht 0 (null) ist.
der absolute Wert von z dem von |x| - n * |y| erzeugten Wert entspricht, wobei n der größtmöglichen
Ganzzahl entspricht, die kleiner oder gleich |x| / |y| ist. Hierbei sind |x| und |y| jeweils die absoluten
Werte von x und y .
NOTE
Diese Methode zum Berechnen des Rests ist analog zu der Methode, die für ganzzahlige Operanden verwendet wird,
unterscheidet sich jedoch von der IEEE 754-Spezifikation. Wenn Sie den Restvorgang benötigen, der der IEEE 754-
Spezifikation entspricht, verwenden Sie die Methode Math.IEEERemainder.

Weitere Informationen zum Verhalten des % -Operators bei nicht begrenzten Operanden finden Sie im
Abschnitt Restoperator der C#-Sprachspezifikation.
Der Restoperator % entspricht für die decimal -Operanden dem Restoperator vom Typ System.Decimal.
Im folgenden Beispiel wird das Verhalten des Restoperators mit Gleitkommaoperanden veranschaulicht:

Console.WriteLine(-5.2f % 2.0f); // output: -1.2


Console.WriteLine(5.9 % 3.1); // output: 2.8
Console.WriteLine(5.9m % 3.1m); // output: 2.8

Additionsoperator +
Der Additionsoperator + berechnet die Summe der Operanden:

Console.WriteLine(5 + 4); // output: 9


Console.WriteLine(5 + 4.3); // output: 9.3
Console.WriteLine(5.1m + 4.2m); // output: 9.3

Der + -Operator kann auch für die Zeichenfolgenverkettung und Delegatkombination verwendet werden.
Weitere Informationen finden Sie im Artikel zu den Operatoren + und += .

Subtraktionsoperator -
Der Subtraktionsoperator - subtrahiert den rechten Operanden vom linken:

Console.WriteLine(47 - 3); // output: 44


Console.WriteLine(5 - 4.3); // output: 0.7
Console.WriteLine(7.5m - 2.3m); // output: 5.2

Der - -Operator kann auch für die Delegatentfernung verwendet werden. Weitere Informationen finden Sie im
Artikel zu den Operatoren - und -= .

Verbundzuweisung
Bei einem binären Operator op entspricht ein Verbundzuweisungsausdruck der Form

x op= y

für die folgende Syntax:

x = x op y

außer dass x nur einmal überprüft wird.


Im folgenden Beispiel wird die Verwendung von Verbundzuweisungen mit arithmetischen Operatoren
veranschaulicht:

int a = 5;
a += 9;
Console.WriteLine(a); // output: 14

a -= 4;
Console.WriteLine(a); // output: 10

a *= 2;
Console.WriteLine(a); // output: 20

a /= 4;
Console.WriteLine(a); // output: 5

a %= 3;
Console.WriteLine(a); // output: 2

Aufgrund von numerischen Höherstufungen kann das Ergebnis der Operation op ggf. nicht implizit in den Typ
T von x konvertiert werden. In diesem Fall gilt Folgendes: Wenn op ein vordefinierter Operator ist und das
Ergebnis der Operation explizit in den Typ T von x konvertiert werden kann, entspricht ein
Verbundzuweisungsausdruck der Form x op= y dem Ausdruck x = (T)(x op y) . Der einzige Unterschied ist,
dass x nur einmal ausgewertet wird. Das folgende Beispiel veranschaulicht dieses Verhalten:

byte a = 200;
byte b = 100;

var c = a + b;
Console.WriteLine(c.GetType()); // output: System.Int32
Console.WriteLine(c); // output: 300

a += b;
Console.WriteLine(a); // output: 44

Die Operatoren += und -= können auch zum Abonnieren von Ereignissen und zum Kündigen eines
Ereignisabonnements verwendet werden. Weitere Informationen finden Sie unter Abonnieren von Ereignissen
und Kündigen von Ereignisabonnements.

Operatorrangfolge und Assoziativität


In der folgenden Liste sind die arithmetischen Operatoren beginnend mit dem höchsten Rangfolgenoperator
absteigend sortiert:
Postfixinkrementoperator x++ und Postfixdekrementoperator x--
Präfixinkrementoperator ++x und Präfixdekrementoperator --x sowie unäre + - und - -Operatoren
Multiplikative Operatoren * , / und %
Additive Operatoren + und -
Binäre arithmetische Operatoren sind linksassoziativ. Das bedeutet, dass Operatoren mit der gleichen
Rangfolgenebene von links nach rechts ausgewertet werden.
Verwenden Sie Klammern () , wenn Sie die Reihenfolge der Auswertung ändern möchten, die durch
Operatorrangfolge und Assoziativität festgelegt wird.
Console.WriteLine(2 + 2 * 2); // output: 6
Console.WriteLine((2 + 2) * 2); // output: 8

Console.WriteLine(9 / 5 / 2); // output: 0


Console.WriteLine(9 / (5 / 2)); // output: 4

Die vollständige Liste der nach Rangfolgenebene sortierten C#-Operatoren finden Sie im Abschnitt
Operatorrangfolge im Artikel C#-Operatoren.

Arithmetischer Überlauf und Division durch 0 (null)


Liegt das Ergebnis einer arithmetischen Operation außerhalb des Bereichs möglicher endlicher Werte des
betreffenden numerischen Typs, hängt das Verhalten eines arithmetischen Operators vom Typ der Operanden
ab.
Arithmetischer Überlauf bei ganzen Zahlen
Division ganzer Zahlen durch Null löst immer eine DivideByZeroException aus.
Im Fall eines arithmetischen Überlaufs bei ganzen Zahlen steuert ein Kontext für Überlaufprüfungen, der
aktiviert oder deaktiviert (Checked oder Unchecked) sein kann, das resultierende Verhalten:
In einem aktivierten Kontext tritt bei einem Überlauf in einem konstanten Ausdruck ein Kompilierzeitfehler
auf. Andernfalls wird, wenn die Operation zur Laufzeit ausgeführt wird, eine OverflowException-Ausnahme
ausgelöst.
In einem deaktivierten Kontext wird das Ergebnis gekürzt, indem alle höherwertigen Bits verworfen werden,
die nicht in den Zieltyp passen.
Neben den Anweisungen Checked und Unchecked können Sie mithilfe der checked - und unchecked -
Operatoren den Kontext für Überlaufprüfungen steuern, in dem ein Ausdruck ausgewertet wird:

int a = int.MaxValue;
int b = 3;

Console.WriteLine(unchecked(a + b)); // output: -2147483646


try
{
int d = checked(a + b);
}
catch(OverflowException)
{
Console.WriteLine($"Overflow occurred when adding {a} to {b}.");
}

Standardmäßig erscheinen arithmetische Operationen in einem unchecked-Kontext.


Arithmetischer Überlauf bei Gleitkommatypen
Bei arithmetischen Operationen mit den Typen float und double wird nie eine Ausnahme ausgelöst. Das
Ergebnis von arithmetischen Operationen mit diesen Typen können spezielle Werte sein, die unendliche und
nicht numerische Zahlen darstellen:
double a = 1.0 / 0.0;
Console.WriteLine(a); // output: Infinity
Console.WriteLine(double.IsInfinity(a)); // output: True

Console.WriteLine(double.MaxValue + double.MaxValue); // output: Infinity

double b = 0.0 / 0.0;


Console.WriteLine(b); // output: NaN
Console.WriteLine(double.IsNaN(b)); // output: True

Für Operanden vom Typ decimal löst ein arithmetischer Überlauf immer eine OverflowException-Ausnahme
und die Division durch 0 (null) immer eine DivideByZeroException-Ausnahme aus.

Rundungsfehler
Aufgrund allgemeiner Einschränkungen der Gleitkommadarstellung von reellen Zahlen und arithmetischer
Gleitkommaoperatoren können in Berechnungen mit Gleitkommatypen Rundungsfehler auftreten. Das
bedeutet, dass das generierte Ergebnis eines Ausdrucks vom erwarteten mathematischen Ergebnis abweichen
kann. Im folgenden Beispiel werden einige solcher Fälle dargestellt:

Console.WriteLine(.41f % .2f); // output: 0.00999999

double a = 0.1;
double b = 3 * a;
Console.WriteLine(b == 0.3); // output: False
Console.WriteLine(b - 0.3); // output: 5.55111512312578E-17

decimal c = 1 / 3.0m;
decimal d = 3 * c;
Console.WriteLine(d == 1.0m); // output: False
Console.WriteLine(d); // output: 0.9999999999999999999999999999

Weitere Informationen finden Sie in den Hinweisen auf den Referenzseiten zu System.Double, System.Single
oder System.Decimal.

Operatorüberladbarkeit
Ein benutzerdefinierter Typ kann die unären ( ++ , -- , + und - ) und binären ( * , / , % , + und - )
arithmetischen Operatoren überladen. Wenn ein binärer Operator überladen ist, wird der zugehörige
Verbundzuweisungsoperator implizit auch überladen. Ein benutzerdefinierter Typ kann einen
Verbundzuweisungsoperator nicht explizit überladen.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Postfix-Inkrementoperator und Postfix-Dekrementoperator
Präfix-Inkrementoperator und Präfix-Dekrementoperator
Unärer Plusoperator
Unärer Minusoperator
Multiplikationsoperator
Divisionsoperator
Restoperator
Additionsoperator
Subtraktionsoperator
Verbundzuweisung
The checked and unchecked operators (Checked- und Unchecked-Operatoren)
Numerische Heraufstufungen

Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
System.Math
System.MathF
Numerische Ausdrücke in .NET
Logische boolesche Operatoren (C#-Referenz)
04.11.2021 • 7 minutes to read

Die folgenden Operatoren führen logische Vorgänge mit bool-Operanden durch:


Unärer ! (logische Negation) Operator.
Binäre & (logisch AND), | (logisch OR) und ^ (logisch exklusiv OR) Operatoren. Diese Operatoren werten
immer beide Operanden aus.
Binäre && (bedingt logisch AND) und || (bedingt logisch OR) Operatoren. Diese Operatoren werten den
rechten Operanden nur dann aus, wenn es notwendig ist.
Für Operanden der integralen numerischen Typen führen die Operatoren & , | und ^ bitweise logische
Operationen durch. Weitere Informationen finden Sie unter Bitweise und Schiebeoperatoren.

Logischer Negationsoperator: !
Der unäre Präfix-Operator ! berechnet die logische Negation seines Operanden. Das heißt., er erzeugt true ,
wenn der Operand als false ausgewertet wird, und false , wenn der Operand als true ausgewertet wird:

bool passed = false;


Console.WriteLine(!passed); // output: True
Console.WriteLine(!true); // output: False

Ab C# 8.0 ist der unäre Postfix-Operator ! der NULL-tolerante Operator.

Logischer AND-Operator &


Der & -Operator berechnet die logische AND-Operation des Operanden. Das Ergebnis von x & y ist true ,
wenn sowohl x als auch y zu true ausgewertet werden. Andernfalls ist das Ergebnis false .
Der & -Operator wertet beide Operanden aus, selbst wenn der linke Operand als false ausgewertet wird,
sodass das Ergebnis des Vorgangs unabhängig vom Wert des rechten Operanden false ist.
Im folgenden Beispiel ist der rechte Operand des & -Operators ein Methodenaufruf, der unabhängig vom Wert
des linken Operanden ausgeführt wird:

bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}

bool a = false & SecondOperand();


Console.WriteLine(a);
// Output:
// Second operand is evaluated.
// False

bool b = true & SecondOperand();


Console.WriteLine(b);
// Output:
// Second operand is evaluated.
// True
Der bedingte logische AND-Operator && berechnet auch die logische AND-Operation der Operanden, wertet
den rechten Operanden aber nicht aus, wenn der linke Operand false ergibt.
Für Operanden der integralen numerischen Typen berechnet der Operator & bitweises logisches UND für seine
Operanden. Der unäre & -Operator ist der address-of-Operator.

Logischer exklusiver OR-Operator: ^


Die ^ -Operator berechnet das logische exklusive OR, auch als logischer XOR bezeichnet, seiner Operanden.
Das Ergebnis von x ^ y ist true , wenn x``true ergibt und y``false ergibt, oder x``false ergibt und
y``true ergibt. Andernfalls ist das Ergebnis false . Das heißt, für die bool -Operanden berechnet der ^ -
Operator das gleiche Ergebnis wie der Ungleichheitsoperator != .

Console.WriteLine(true ^ true); // output: False


Console.WriteLine(true ^ false); // output: True
Console.WriteLine(false ^ true); // output: True
Console.WriteLine(false ^ false); // output: False

Für Operanden der integralen numerischen Typen berechnet der Operator ^ bitweises logisches exklusives
ODER für seine Operanden.

Logischer OR-Operator: |
Der | -Operator berechnet die logische OR-Operation des Operanden. Das Ergebnis von x | y ist true , wenn
entweder x oder y``true ergibt. Andernfalls ist das Ergebnis false .
Der | -Operator wertet beide Operanden aus, selbst wenn der linke Operand als true ausgewertet wird,
sodass das Ergebnis des Vorgangs unabhängig vom Wert des rechten Operanden true ist.
Im folgenden Beispiel ist der rechte Operand des | -Operators ein Methodenaufruf, der unabhängig vom Wert
des linken Operanden ausgeführt wird:

bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}

bool a = true | SecondOperand();


Console.WriteLine(a);
// Output:
// Second operand is evaluated.
// True

bool b = false | SecondOperand();


Console.WriteLine(b);
// Output:
// Second operand is evaluated.
// True

Der bedingte logische OR-Operator || berechnet auch die logische OR-Operation der Operanden, wertet den
rechten Operanden aber nicht aus, wenn der linke Operand true ergibt.
Für Operanden der integralen numerischen Typen berechnet der Operator | bitweises logisches ODER für
seine Operanden.

Bedingter logischer AND-Operator &&


Der bedingte logische AND-Operator && , auch bekannt als der "kurzschließender" logischer AND-Operator
bezeichnet, berechnet die logische AND-Operation der Operanden. Das Ergebnis von x && y ist true , wenn
sowohl x als auch y zu true ausgewertet werden. Andernfalls ist das Ergebnis false . Wenn x als false
ausgewertet wird, wird y nicht ausgewertet.
Im folgenden Beispiel ist der rechte Operand des && -Operators ein Methodenaufruf, der nicht ausgeführt wird,
wenn der linke Operand false ergibt:

bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}

bool a = false && SecondOperand();


Console.WriteLine(a);
// Output:
// False

bool b = true && SecondOperand();


Console.WriteLine(b);
// Output:
// Second operand is evaluated.
// True

Der logische AND-Operator & berechnet auch die logische AND-Operation der Operanden, es werden jedoch
immer beide Operanden ausgewertet.

Bedingter logischer OR-Operator ||


Der bedingte logische OR-Operator || , auch bekannt als der "kurzschließender" logischer OR-Operator
bezeichnet, berechnet die logische OR-Operation der Operanden. Das Ergebnis von x || y ist true , wenn
entweder x oder y``true ergibt. Andernfalls ist das Ergebnis false . Wenn x als true ausgewertet wird,
wird y nicht ausgewertet.
Im folgenden Beispiel ist der rechte Operand des || -Operators ein Methodenaufruf, der nicht ausgeführt wird,
wenn der linke Operand true ergibt:

bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}

bool a = true || SecondOperand();


Console.WriteLine(a);
// Output:
// True

bool b = false || SecondOperand();


Console.WriteLine(b);
// Output:
// Second operand is evaluated.
// True

Der logische OR-Operator | berechnet auch die logische OR-Operation der Operanden, es werden jedoch
immer beide Operanden ausgewertet.
Logische boolesche Operatoren, die NULL-Werte zulassen
Für bool? -Operanden unterstützen die Operatoren & (logisches UND) und | (logisches ODER) die
dreiwertige Logik wie folgt:
Für den Operator & wird nur true zurückgegeben, wenn auch beide seiner Operanden true ergeben.
Wenn für x und y false zurückgegeben wird, ergibt x & y false (auch wenn ein anderer Operand
null ergibt). Andernfalls ist das Ergebnis von x & y null .

Für den Operator | wird nur false zurückgegeben, wenn auch beide seiner Operanden false
ergeben. Wenn für x und y true zurückgegeben wird, ergibt x | y true (auch wenn ein anderer
Operand null ergibt). Andernfalls ist das Ergebnis von x | y null .

Der folgenden Tabelle können Sie die Semantik entnehmen:

W Y X& Y X|Y

true true true true

true false false true

true NULL NULL true

false true false true

false false false false

False NULL False NULL

NULL true NULL true

NULL false False NULL

NULL NULL NULL NULL

Das Verhalten dieser Operatoren unterscheidet sich vom typischen Operatorverhalten bei Typen, die NULL-
Werte zulassen. In der Regel kann ein Operator, der für Operanden eines Werttyps definiert ist, auch mit
Operanden des entsprechenden Nullable-Typs verwendet werden. Ein solcher Operator generiert null , wenn
einer seiner Operanden in null ausgewertet wird. Die & - und | -Operatoren können jedoch Nicht-NULL-
Werte erzeugen, auch wenn einer der Operanden in null ausgewertet wird. Weitere Informationen zum
Operatorverhalten bei Nullable-Werttypen finden Sie im Abschnitt „Lifted“ Operatoren des Artikels Nullable-
Werttypen.
Sie können auch die ! - und ^ - Operatoren mit bool? -Operanden verwenden, wie das folgende Beispiel
zeigt:

bool? test = null;


Display(!test); // output: null
Display(test ^ false); // output: null
Display(test ^ null); // output: null
Display(true ^ null); // output: null

void Display(bool? b) => Console.WriteLine(b is null ? "null" : b.Value.ToString());

Die bedingten logischen Operatoren && und || unterstützen keine bool? -Operanden.
Verbundzuweisung
Bei einem binären Operator op entspricht ein Verbundzuweisungsausdruck der Form

x op= y

für die folgende Syntax:

x = x op y

außer dass x nur einmal überprüft wird.


Die & -, | - und ^ -Operatoren unterstützen die Verbundzuweisung, wie das folgende Beispiel zeigt:

bool test = true;


test &= false;
Console.WriteLine(test); // output: False

test |= true;
Console.WriteLine(test); // output: True

test ^= false;
Console.WriteLine(test); // output: True

NOTE
Die bedingten logischen Operatoren && und || unterstützen nicht die Verbundzuweisung.

Operatorrangfolge
In der folgenden Liste sind die logischen Operatoren beginnend mit dem höchsten Rangfolgenoperator
absteigend sortiert:
Logischer Negationsoperator !
Logischer AND-Operator &
Logischer exklusiver OR-Operator ^
Logischer OR-Operator |
Bedingter logischer AND-Operator &&
Bedingter logischer OR-Operator ||

Verwenden Sie Klammern () , wenn Sie die Reihenfolge der Auswertung ändern möchten, die durch
Operatorrangfolge festgelegt wird:
Console.WriteLine(true | true & false); // output: True
Console.WriteLine((true | true) & false); // output: False

bool Operand(string name, bool value)


{
Console.WriteLine($"Operand {name} is evaluated.");
return value;
}

var byDefaultPrecedence = Operand("A", true) || Operand("B", true) && Operand("C", false);


Console.WriteLine(byDefaultPrecedence);
// Output:
// Operand A is evaluated.
// True

var changedOrder = (Operand("A", true) || Operand("B", true)) && Operand("C", false);


Console.WriteLine(changedOrder);
// Output:
// Operand A is evaluated.
// Operand C is evaluated.
// False

Die vollständige Liste der nach Rangfolgenebene sortierten C#-Operatoren finden Sie im Abschnitt
Operatorrangfolge im Artikel C#-Operatoren.

Operatorüberladbarkeit
Ein benutzerdefinierter Typ kann die Operatoren ! , & , | und ^ überladen. Wenn ein binärer Operator
überladen ist, wird der zugehörige Verbundzuweisungsoperator implizit auch überladen. Ein benutzerdefinierter
Typ kann einen Verbundzuweisungsoperator nicht explizit überladen.
Ein benutzerdefinierter Typ kann die bedingten logischen Operatoren && und || nicht überladen. Wenn
jedoch ein benutzerdefinierter Typ die „true“ und „false“-Operatoren und den & - oder | -Operator in einer
bestimmten Weise überlädt, kann die && - bzw. || -Operation für die Operanden dieses Typs ausgewertet
werden. Weitere Informationen finden Sie im Abschnitt Benutzerdefinierte bedingte logische Operatoren der
C#-Sprachspezifikation.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Logischer Negationsoperator
Logical operators (Logische Operatoren)
Conditional logical operators (Bedingte logische Operatoren)
Verbundzuweisung

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Bitweise und Schiebeoperatoren
Bitweise und Schiebeoperatoren: C#-Referenz
04.11.2021 • 7 minutes to read

Mit den folgenden Operatoren werden bitweise oder Verschiebevorgänge mit Operanden durchgeführt, die
integrale numerische Typen oder den char-Typ aufweisen:
Unärer Operator ~ (bitweises Komplement)
Binäre Schiebeoperatoren << (Linksverschiebung) und >> (Rechtsverschiebung)
Binäre Operatoren & (logisches UND), | (logisches ODER) und ^ (logisches exklusives ODER)
Diese Operatoren werden für die Typen int , uint , long und ulong definiert. Wenn beide Operanden andere
integrale Typen aufweisen ( sbyte , byte , short , ushort oder char ), werden ihre Werte in den Typ int
konvertiert. Hierbei handelt es sich auch um den Ergebnistyp einer Operation. Wenn die Operanden
abweichende integrale Typen aufweisen, werden ihre Werte in den enthaltenden integralen Typ konvertiert, der
am besten geeignet ist. Weitere Informationen finden Sie im Abschnitt Numerische Heraufstufungen der
Spezifikation für die Sprache C#.
Die Operatoren & , | und ^ werden auch für Operanden des bool -Typs definiert. Weitere Informationen
finden Sie unter Logische boolesche Operatoren.
Bitweise und Schiebeoperationen verursachen niemals Überläufe und führen sowohl in geprüften als auch in
ungeprüften Kontexten zu identischen Ergebnissen.

Bitweiser Komplementoperator ~
Mit dem Operator ~ wird ein bitweises Komplement seines Operanden erzeugt, indem jedes Bit umgekehrt
wird:

uint a = 0b_0000_1111_0000_1111_0000_1111_0000_1100;
uint b = ~a;
Console.WriteLine(Convert.ToString(b, toBase: 2));
// Output:
// 11110000111100001111000011110011

Sie können das Symbol ~ auch verwenden, um Finalizers zu deklarieren. Weitere Informationen finden Sie
unter Finalizer.

Operator für Linksverschiebung <<


Mit dem Operator << wird der linke Operand um die Anzahl von Bits nach links verschoben, die durch den
rechten Operanden angegeben wird.
Bei der Operation zum Verschieben nach links werden die hohen Bits, die außerhalb des Bereichs des
Ergebnistyps liegen, verworfen, und die niedrigen leeren Bitpositionen werden auf null festgelegt. Dies ist im
folgenden Beispiel dargestellt:
uint x = 0b_1100_1001_0000_0000_0000_0000_0001_0001;
Console.WriteLine($"Before: {Convert.ToString(x, toBase: 2)}");

uint y = x << 4;
Console.WriteLine($"After: {Convert.ToString(y, toBase: 2)}");
// Output:
// Before: 11001001000000000000000000010001
// After: 10010000000000000000000100010000

Da die Verschiebeoperatoren nur für die Typen int , uint , long und ulong definiert werden, enthält das
Ergebnis einer Operation immer mindestens 32 Bit. Wenn der linke Operand einen abweichenden integralen
Typ aufweist ( sbyte , byte , short , ushort oder char ), wird sein Wert in den Typ int konvertiert. Dies ist im
folgenden Beispiel dargestellt:

byte a = 0b_1111_0001;

var b = a << 8;
Console.WriteLine(b.GetType());
Console.WriteLine($"Shifted byte: {Convert.ToString(b, toBase: 2)}");
// Output:
// System.Int32
// Shifted byte: 1111000100000000

Informationen dazu, wie der rechte Operand des Operators << die Anzahl für die Verschiebung definiert, finden
Sie im Abschnitt Anzahl für die Verschiebung durch Schiebeoperatoren.

Operator für Rechtsverschiebung >>


Mit dem Operator >> wird der linke Operand um die Anzahl von Bits nach rechts verschoben, die durch den
rechten Operanden angegeben wird.
Bei der Operation zum Verschieben nach rechts werden die niedrigen Bits verworfen. Dies ist im folgenden
Beispiel dargestellt:

uint x = 0b_1001;
Console.WriteLine($"Before: {Convert.ToString(x, toBase: 2), 4}");

uint y = x >> 2;
Console.WriteLine($"After: {Convert.ToString(y, toBase: 2), 4}");
// Output:
// Before: 1001
// After: 10

Die höheren leeren Bitpositionen werden basierend auf dem Typ des linken Operanden wie folgt festgelegt:
Wenn der linke Operand vom Typ int oder long ist, führt der Operator zur Rechtsverschiebung eine
arithmetische Verschiebung durch: Der Wert des Bits mit dem höchsten Stellenwert (MSB, „most
significant bit“) des linken Operanden wird auf die hohen leeren Bitpositionen übertragen. Die hohen
leeren Bitpositionen werden daher auf 0 festgelegt, wenn der linke Operand nicht negativ ist, bzw. auf 1,
wenn der linke Operand negativ ist.
int a = int.MinValue;
Console.WriteLine($"Before: {Convert.ToString(a, toBase: 2)}");

int b = a >> 3;
Console.WriteLine($"After: {Convert.ToString(b, toBase: 2)}");
// Output:
// Before: 10000000000000000000000000000000
// After: 11110000000000000000000000000000

Wenn der linke Operand vom Typ uint oder ulong ist, führt der Operator zur Rechtsverschiebung eine
logische Verschiebung durch: Die hohen leeren Bitpositionen werden immer auf 0 (null) festgelegt.

uint c = 0b_1000_0000_0000_0000_0000_0000_0000_0000;
Console.WriteLine($"Before: {Convert.ToString(c, toBase: 2), 32}");

uint d = c >> 3;
Console.WriteLine($"After: {Convert.ToString(d, toBase: 2), 32}");
// Output:
// Before: 10000000000000000000000000000000
// After: 10000000000000000000000000000

Informationen dazu, wie der rechte Operand des Operators >> die Anzahl für die Verschiebung definiert, finden
Sie im Abschnitt Anzahl für die Verschiebung durch Schiebeoperatoren.

Logischer AND-Operator &


Mit dem Operator & wird „bitweises logisches UND“ für die ganzzahligen Operanden berechnet:

uint a = 0b_1111_1000;
uint b = 0b_1001_1101;
uint c = a & b;
Console.WriteLine(Convert.ToString(c, toBase: 2));
// Output:
// 10011000

Für bool -Operanden berechnet der & -Operator das logische UND für die Operanden. Der unäre & -Operator
ist der address-of-Operator.

Logischer exklusiver OR-Operator: ^


Mit dem Operator ^ wird „bitweises logisches exklusives ODER“, auch als „bitweises logisches XOR“
bezeichnet, seiner ganzzahligen Operanden berechnet:

uint a = 0b_1111_1000;
uint b = 0b_0001_1100;
uint c = a ^ b;
Console.WriteLine(Convert.ToString(c, toBase: 2));
// Output:
// 11100100

Für bool -Operanden berechnet der ^ -Operator das logische exklusive ODER für die Operanden.

Logischer OR-Operator: |
Mit dem Operator | wird „bitweises logisches ODER“ der ganzzahligen Operanden berechnet:
uint a = 0b_1010_0000;
uint b = 0b_1001_0001;
uint c = a | b;
Console.WriteLine(Convert.ToString(c, toBase: 2));
// Output:
// 10110001

Für bool -Operanden berechnet der | -Operator das logische ODER für die Operanden.

Verbundzuweisung
Bei einem binären Operator op entspricht ein Verbundzuweisungsausdruck der Form

x op= y

für die folgende Syntax:

x = x op y

außer dass x nur einmal überprüft wird.


Im folgenden Beispiel wird die Verwendung von Verbundzuweisungen mit bitweisen und Schiebeoperatoren
veranschaulicht:

uint INITIAL_VALUE = 0b_1111_1000;

uint a = INITIAL_VALUE;
a &= 0b_1001_1101;
Display(a); // output: 10011000

a = INITIAL_VALUE;
a |= 0b_0011_0001;
Display(a); // output: 11111001

a = INITIAL_VALUE;
a ^= 0b_1000_0000;
Display(a); // output: 1111000

a = INITIAL_VALUE;
a <<= 2;
Display(a); // output: 1111100000

a = INITIAL_VALUE;
a >>= 4;
Display(a); // output: 1111

void Display(uint x) => Console.WriteLine($"{Convert.ToString(x, toBase: 2), 8}");

Aufgrund von numerischen Höherstufungen kann das Ergebnis der Operation op ggf. nicht implizit in den Typ
T von x konvertiert werden. In diesem Fall gilt Folgendes: Wenn op ein vordefinierter Operator ist und das
Ergebnis der Operation explizit in den Typ T von x konvertiert werden kann, entspricht ein
Verbundzuweisungsausdruck der Form x op= y dem Ausdruck x = (T)(x op y) . Der einzige Unterschied ist,
dass x nur einmal ausgewertet wird. Das folgende Beispiel veranschaulicht dieses Verhalten:
byte x = 0b_1111_0001;

int b = x << 8;
Console.WriteLine($"{Convert.ToString(b, toBase: 2)}"); // output: 1111000100000000

x <<= 8;
Console.WriteLine(x); // output: 0

Operatorrangfolge
In der folgenden Liste sind die bitweisen und Schiebeoperatoren absteigend nach Rangfolge sortiert:
Bitweiser Komplementoperator ~
Schiebeoperatoren << und >>
Logischer AND-Operator &
Logischer exklusiver OR-Operator ^
Logischer OR-Operator |
Verwenden Sie Klammern () , wenn Sie die Reihenfolge der Auswertung ändern möchten, die durch
Operatorrangfolge festgelegt wird:

uint a = 0b_1101;
uint b = 0b_1001;
uint c = 0b_1010;

uint d1 = a | b & c;
Display(d1); // output: 1101

uint d2 = (a | b) & c;
Display(d2); // output: 1000

void Display(uint x) => Console.WriteLine($"{Convert.ToString(x, toBase: 2), 4}");

Die vollständige Liste der nach Rangfolgenebene sortierten C#-Operatoren finden Sie im Abschnitt
Operatorrangfolge im Artikel C#-Operatoren.

Anzahl für die Verschiebung durch Schiebeoperatoren


Für die Schiebeoperatoren << und >> muss der Typ des rechten Operanden int lauten oder ein Typ sein, der
eine vordefinierte, implizite numerische Konvertierung in int aufweist.
Für die Ausdrücke x << count und x >> count hängt die tatsächliche Verschiebungsanzahl wie folgt vom Typ
von x ab:
Lautet der Typ von x``int oder uint , wird die Verschiebungsanzahl durch die niedrigen fünf Bits des
rechten Operanden definiert. Die Verschiebungsanzahl errechnet sich daher aus count & 0x1F (oder
count & 0b_1_1111 ).

Lautet der Typ von x``long oder ulong , wird die Verschiebungsanzahl durch die niedrigen sechs Bits
des rechten Operanden definiert. Die Verschiebungsanzahl errechnet sich daher aus count & 0x3F (oder
count & 0b_11_1111 ).

Das folgende Beispiel veranschaulicht dieses Verhalten:


int count1 = 0b_0000_0001;
int count2 = 0b_1110_0001;

int a = 0b_0001;
Console.WriteLine($"{a} << {count1} is {a << count1}; {a} << {count2} is {a << count2}");
// Output:
// 1 << 1 is 2; 1 << 225 is 2

int b = 0b_0100;
Console.WriteLine($"{b} >> {count1} is {b >> count1}; {b} >> {count2} is {b >> count2}");
// Output:
// 4 >> 1 is 2; 4 >> 225 is 2

NOTE
Wie im vorherigen Beispiel gezeigt wird, kann das Ergebnis eines Verschiebungsvorgangs nicht 0 (Null) sein, auch wenn
der Wert des rechten Operanden größer ist als die Anzahl der Bits im linken Operanden.

Logische Enumerationsoperatoren
Die Operatoren ~ , & , | und ^ werden auch von jedem Enumerationstyp unterstützt. Für Operanden mit
dem gleichen Enumerationstyp wird ein logischer Vorgang für die entsprechenden Werte des zugrunde
liegenden integralen Typs durchgeführt. Für alle x - und y -Elemente des Enumerationstyps T mit dem
zugrunde liegenden Typ U führt der Ausdruck x & y zum gleichen Ergebnis wie der Ausdruck
(T)((U)x & (U)y) .

Normalerweise verwenden Sie bitweise logische Operatoren mit einem Enumerationstyp, der mit dem Flags-
Attribut definiert wird. Weitere Informationen finden Sie im Abschnitt Enumerationstypen als Bitflags des
Artikels Enumerationstypen.

Operatorüberladbarkeit
Ein benutzerdefinierter Typ kann die Operatoren ~ , << , >> , & , | und ^ überladen. Wenn ein binärer
Operator überladen ist, wird der zugehörige Verbundzuweisungsoperator implizit auch überladen. Ein
benutzerdefinierter Typ kann einen Verbundzuweisungsoperator nicht explizit überladen.
Wenn ein benutzerdefinierter Typ T den Operator << oder >> überlädt, muss der Typ des linken Operanden
T und der Typ des rechten Operanden int lauten.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Bitweiser Komplementoperator
Shift operators (Schiebeoperatoren)
Logical operators (Logische Operatoren)
Verbundzuweisung
Numerische Heraufstufungen

Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
Logische boolesche Operatoren
Gleichheitsoperatoren (C#-Referenz)
04.11.2021 • 4 minutes to read

Die Operatoren == (Gleichheit) und != (Ungleichheit) überprüfen, ob die Operanden gleich sind oder nicht.

Gleichheitsoperator ==
Der Gleichheitsoperator == gibt true zurück, wenn die Operanden gleich sind; andernfalls false .
Gleichheit von Werttypen
Operanden der integrierten Werttypen sind gleich, wenn ihre Werte gleich sind:

int a = 1 + 2 + 3;
int b = 6;
Console.WriteLine(a == b); // output: True

char c1 = 'a';
char c2 = 'A';
Console.WriteLine(c1 == c2); // output: False
Console.WriteLine(c1 == char.ToLower(c2)); // output: True

NOTE
Bei den Operatoren == , < , > , <= und >= ist das Ergebnis eines Vorgangs false , wenn einer der Operanden
keine Zahl ist (Double.NaN oder Single.NaN). Das bedeutet, dass der NaN -Wert weder größer als noch kleiner als noch
gleich einem anderen double -Wert (oder float -Wert) ist, einschließlich NaN . Weitere Informationen und Beispiele
finden Sie im Double.NaN- oder Single.NaN-Referenzartikel.

Zwei Operanden desselben enum-Typs sind gleich, wenn die entsprechenden Werte des zugrunde liegenden
integralen Typs gleich sind.
Benutzerdefinierte Strukturtypen unterstützen den == -Operator nicht standardmäßig. Eine benutzerdefinierte
Struktur muss den == -Operator überladen, damit er unterstützt wird.
Ab C# 7.3 werden die Operatoren == und != für C#-Tupel unterstützt. Weitere Informationen finden Sie im
Abschnitt Tupelgleichheit im Artikel Tupeltypen.
Gleichheit von Verweistypen
Standardmäßig sind zwei Verweistypoperanden, die keine Datensätze darstellen, gleich, wenn sie auf dasselbe
Objekt verweisen:
public class ReferenceTypesEquality
{
public class MyClass
{
private int id;

public MyClass(int id) => this.id = id;


}

public static void Main()


{
var a = new MyClass(1);
var b = new MyClass(1);
var c = a;
Console.WriteLine(a == b); // output: False
Console.WriteLine(a == c); // output: True
}
}

Das Beispiel zeigt, dass benutzerdefinierte Verweistypen den == -Operator standardmäßig unterstützen. Ein
Verweistyp kann den Operator == aber überladen. Wenn ein Verweistyp den == -Operator überlädt,
verwenden Sie die Object.ReferenceEquals-Methode, um zu überprüfen, ob zwei Verweise dieses Typs auf
dasselbe Objekt verweisen.
Gleichheit von Datensatztypen
Datensatztypen sind in C# 9.0 und höher verfügbar und unterstützen die Operatoren == und != , die
standardmäßig Semantik für Wertgleichheit ausdrücken. Das bedeutet, dass zwei Datensatzoperanden gleich
sind, wenn beide gleich null oder die entsprechenden Werte aller Felder sowie die automatisch
implementierten Eigenschaften gleich sind.

public class RecordTypesEquality


{
public record Point(int X, int Y, string Name);
public record TaggedNumber(int Number, List<string> Tags);

public static void Main()


{
var p1 = new Point(2, 3, "A");
var p2 = new Point(1, 3, "B");
var p3 = new Point(2, 3, "A");

Console.WriteLine(p1 == p2); // output: False


Console.WriteLine(p1 == p3); // output: True

var n1 = new TaggedNumber(2, new List<string>() { "A" });


var n2 = new TaggedNumber(2, new List<string>() { "A" });
Console.WriteLine(n1 == n2); // output: False
}
}

Wie das vorherige Beispiel zeigt, werden bei Verweistypmembern, die keine Datensätze sind, die Verweiswerte
verglichen, nicht die referenzierten Instanzen.
Zeichenfolgengleichheit
Zwei Zeichenfolge-Operanden gleich sind, wenn beide gleich null sind oder wenn beide
Zeichenfolgeninstanzen dieselbe Länge und identische Zeichen in jeder Zeichenposition haben:
string s1 = "hello!";
string s2 = "HeLLo!";
Console.WriteLine(s1 == s2.ToLower()); // output: True

string s3 = "Hello!";
Console.WriteLine(s1 == s3); // output: False

Dies ist ein Ordinalvergleich unter Berücksichtigung der Groß-/Kleinschreibung. Weitere Informationen zum
Zeichenfolgenvergleich finden Sie unter Vergleichen von Zeichenfolgen in C#.
Delegatengleichheit
Zwei delegate-Operanden mit demselben Laufzeittyp sind gleich, wenn beide null sind oder wenn ihre
Aufruflisten die gleiche Länge aufweisen und an jeder Position gleiche Einträge enthalten:

Action a = () => Console.WriteLine("a");

Action b = a + a;
Action c = a + a;
Console.WriteLine(object.ReferenceEquals(b, c)); // output: False
Console.WriteLine(b == c); // output: True

Weitere Informationen finden Sie im Abschnitt Operatoren für Delegatengleichheit der C#-Sprachspezifikation.
Delegaten, die durch die Auswertung semantisch identischer Lambdaausdrücke erzeugt werden, sind
beispielsweise nicht gleich. Dies wird im folgenden Beispiel gezeigt:

Action a = () => Console.WriteLine("a");


Action b = () => Console.WriteLine("a");

Console.WriteLine(a == b); // output: False


Console.WriteLine(a + b == a + b); // output: True
Console.WriteLine(b + a == a + b); // output: False

Ungleichheitsoperator !=
Der Ungleichheitsoperator != gibt true zurück, wenn die Operanden nicht gleich sind; andernfalls false . Für
die Operanden der integrierten Typen führt der Ausdruck x != y zum selben Ergebnis wie der Ausdruck
!(x == y) . Weitere Informationen zur Typengleichheit finden Sie im Abschnitt Gleichheitsoperator.

Im folgenden Beispiel wird die Verwendung des != -Operators veranschaulicht:

int a = 1 + 1 + 2 + 3;
int b = 6;
Console.WriteLine(a != b); // output: True

string s1 = "Hello";
string s2 = "Hello";
Console.WriteLine(s1 != s2); // output: False

object o1 = 1;
object o2 = 1;
Console.WriteLine(o1 != o2); // output: True

Operatorüberladbarkeit
Ein benutzerdefinierter Typ kann die Operatoren == und != überladen. Wenn ein Typ einen der zwei
Operatoren überlädt, muss er auch den anderen Operator überladen.
Ein Datensatztyp kann die Operatoren == und != nicht explizit überladen. Wenn Sie das Verhalten der
Operatoren == und != für den Datensatztyp T ändern müssen, implementieren Sie die Methode
IEquatable<T>.Equals mit der folgenden Signatur:

public virtual bool Equals(T? other);

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Relationale und Typtestoperatoren in der C#-Sprachspezifikation.
Weitere Informationen zur Gleichheit von Datensatztypen finden Sie im Abschnitt Gleichheitsmember des
Artikels Datensätze.

Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
System.IEquatable<T>
Object.Equals
Object.ReferenceEquals
Übereinstimmungsvergleiche
Vergleichsoperatoren
Vergleichsoperatoren (C#-Referenz)
04.11.2021 • 2 minutes to read

Die Vergleichsoperatoren < (kleiner als), > (größer als), <= (kleiner als oder gleich) und >= (größer als oder
gleich) – auch bekannt als relationale Operatoren – vergleichen ihre Operanden. Diese Operatoren werden alle
von numerischen Ganzzahl- und Gleitkommatypen unterstützt.

NOTE
Bei den Operatoren == , < , > , <= und >= ist das Ergebnis eines Vorgangs gleich false , wenn einer der
Operanden keine Zahl ist (Double.NaN oder Single.NaN). Das bedeutet, dass der NaN -Wert weder größer als noch
kleiner als noch gleich einem anderen double -Wert (oder float -Wert) ist, einschließlich NaN . Weitere Informationen
und Beispiele finden Sie im Double.NaN- oder Single.NaN-Referenzartikel.

Der char-Typ unterstützt auch Vergleichsoperatoren. Im Fall von char -Operanden werden die entsprechenden
Zeichencodes verglichen.
Enumerationstypen unterstützen auch Vergleichsoperatoren. Für Operanden desselben enum-Typs werden die
entsprechenden Werte des zugrunde liegenden integralen Typs verglichen.
Die Operatoren == und != überprüfen, ob die Operanden gleich sind oder nicht.

„Kleiner als“-Operator <


Der -Operator gibt
< true zurück, wenn sein linker Operand kleiner ist als sein rechter Operand, andernfalls
false :

Console.WriteLine(7.0 < 5.1); // output: False


Console.WriteLine(5.1 < 5.1); // output: False
Console.WriteLine(0.0 < 5.1); // output: True

Console.WriteLine(double.NaN < 5.1); // output: False


Console.WriteLine(double.NaN >= 5.1); // output: False

„Größer als“-Operator >


Der -Operator gibt
> true zurück, wenn sein linker Operand größer ist als sein rechter Operand, andernfalls
false :

Console.WriteLine(7.0 > 5.1); // output: True


Console.WriteLine(5.1 > 5.1); // output: False
Console.WriteLine(0.0 > 5.1); // output: False

Console.WriteLine(double.NaN > 5.1); // output: False


Console.WriteLine(double.NaN <= 5.1); // output: False

„Kleiner oder gleich“-Operator <=


Der <= -Operator gibt true zurück, wenn sein linker Operand kleiner ist als der rechte Operand oder diesem
entspricht, andernfalls false :
Console.WriteLine(7.0 <= 5.1); // output: False
Console.WriteLine(5.1 <= 5.1); // output: True
Console.WriteLine(0.0 <= 5.1); // output: True

Console.WriteLine(double.NaN > 5.1); // output: False


Console.WriteLine(double.NaN <= 5.1); // output: False

„Größer oder gleich“-Operator >=


Der >= -Operator gibt true zurück, wenn sein linker Operand größer ist als der rechte Operand oder diesem
entspricht, andernfalls false :

Console.WriteLine(7.0 >= 5.1); // output: True


Console.WriteLine(5.1 >= 5.1); // output: True
Console.WriteLine(0.0 >= 5.1); // output: False

Console.WriteLine(double.NaN < 5.1); // output: False


Console.WriteLine(double.NaN >= 5.1); // output: False

Operatorüberladbarkeit
Ein benutzerdefinierter Typ kann die Operatoren < , > , <= und >= überladen.
Wenn ein Typ einen der < - oder > -Operatoren überlädt, muss er sowohl < als auch > überladen. Wenn ein
Typ einen der <= - oder >= -Operatoren überlädt, muss er sowohl <= als auch >= überladen.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Relationale und Typtestoperatoren in der C#-Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
System.IComparable<T>
Gleichheitsoperatoren
Operatoren und Ausdrücke für den Memberzugriff
(C#-Referenz)
04.11.2021 • 8 minutes to read

Sie können die folgenden Operatoren und Ausdrücke zum Zugriff auf einen Typmember verwenden:
. (Memberzugriff): für den Zugriff auf einen Member eines Namespaces oder Typs
[] (Zugriff auf Arrayelement oder Indexer) : Zugriff auf ein Arrayelement oder einen Typindexer
?. und ?[] (NULL-bedingte Operatoren): Ausführen eines Member- oder Elementzugriffs nur dann, wenn
ein Operand ungleich NULL ist.
() (Aufruf): Aufrufen einer Methode, auf die zugegriffen wurde, oder Aufrufen eines Delegaten
^ (Index vom Ende): Angeben, dass die Elementposition vom Ende einer Sequenz erfolgt.
.. Bereich: Angeben eines Bereichs von Indizes, mit dem ein Bereich von Sequenzelementen abgerufen
werden kann.

Memberzugriffsausdruck „.“
Sie verwenden das . -Token für den Zugriff auf einen Member eines Namespace oder eines Typs, wie die
folgenden Beispiele veranschaulichen:
Verwenden Sie . für den Zugriff auf einen geschachtelten Namespace innerhalb eines Namespace, wie im
folgenden Beispiel einer using -Anweisung gezeigt:

using System.Collections.Generic;

Verwenden Sie . , um einen qualifizierten Namen zu bilden, um auf einen Typ innerhalb eines Namespace
zuzugreifen, wie im folgenden Code gezeigt:

System.Collections.Generic.IEnumerable<int> numbers = new int[] { 1, 2, 3 };

Verwenden Sie eine using -Anweisung, um die Verwendung qualifizierter Namen optional zu machen.
Verwenden Sie . für den Zugriff auf Typmember, statische und nicht statische, wie im folgenden Code
gezeigt:

var constants = new List<double>();


constants.Add(Math.PI);
constants.Add(Math.E);
Console.WriteLine($"{constants.Count} values to show:");
Console.WriteLine(string.Join(", ", constants));
// Output:
// 2 values to show:
// 3.14159265358979, 2.71828182845905

Sie können auch . verwenden, um auf eine Erweiterungsmethode zuzugreifen.

Indexeroperator []
Eckige Klammern ( [] ) werden in der Regel für den Zugriff auf Arrays, Indexer oder Zeigerelemente verwendet.
Arrayzugriff
Im folgenden Beispiel wird der Zugriff auf Elemente des Arrays veranschaulicht:

int[] fib = new int[10];


fib[0] = fib[1] = 1;
for (int i = 2; i < fib.Length; i++)
{
fib[i] = fib[i - 1] + fib[i - 2];
}
Console.WriteLine(fib[fib.Length - 1]); // output: 55

double[,] matrix = new double[2,2];


matrix[0,0] = 1.0;
matrix[0,1] = 2.0;
matrix[1,0] = matrix[1,1] = 3.0;
var determinant = matrix[0,0] * matrix[1,1] - matrix[1,0] * matrix[0,1];
Console.WriteLine(determinant); // output: -3

Wenn ein Arrayindex sich außerhalb der Grenzen der entsprechenden Dimension eines Arrays befindet, wird
eine IndexOutOfRangeException ausgelöst.
Wie im vorherigen Beispiel gezeigt, verwenden Sie eckige Klammern auch zur Deklaration eines Arraytyps oder
Instanziierung von Arrayinstanzen.
Weitere Informationen zu Arrays finden Sie unter Arrays.
Indexerzugriff
Im folgenden Beispiel wird der Indexerzugriff anhand des .NET Dictionary<TKey,TValue>-Typs veranschaulicht:

var dict = new Dictionary<string, double>();


dict["one"] = 1;
dict["pi"] = Math.PI;
Console.WriteLine(dict["one"] + dict["pi"]); // output: 4.14159265358979

Mit Indexern können Sie Instanzen eines benutzerdefinierten Typs auf ähnliche Weise wie ein Array indizieren.
Im Gegensatz zu Arrayindizes, die ganze Zahlen sein müssen, können die Indexerparameter mit einem
beliebigen Typ deklariert werden.
Weitere Informationen über Indexer finden Sie unter Indexer.
Andere Verwendungen von „[]“
Weitere Informationen zum Zeigerelementzugriff finden Sie im Abschnitt Zeigerelementzugriff-Operator [] im
Artikel Operatoren im Zusammenhang mit Zeigern.
Sie verwenden eckige Klammern auch, um Attribute anzugeben:

[System.Diagnostics.Conditional("DEBUG")]
void TraceMethod() {}

NULL-bedingte Operatoren „?.“ und „?[]“


Ein in C# 6 und höher verfügbarer NULL-bedingter Operator wendet nur dann einen Memberzugriffsvorgang (
?. ) oder Elementzugriffsvorgang ( ?[] ) auf seinen Operanden an, wenn dieser Operand als ungleich NULL
ausgewertet wird. Andernfalls gibt er null zurück. Dies bedeutet:
Wenn a als null ausgewertet wird, ist das Ergebnis von a?.x oder a?[x] null .
Wenn a in einen Wert ungleich NULL ausgewertet wird, ist das Ergebnis von a?.x oder a?[x] mit
dem Ergebnis von a.x bzw. a[x] identisch.

NOTE
Wenn a.x oder a[x] eine Ausnahme auslöst, würden a?.x oder a?[x] für a ungleich NULL dieselbe
Ausnahme auslösen. Wenn a z. B. eine Arrayinstanz ungleich NULL ist und x außerhalb der Grenzen von a
liegt, löst a?[x] eine IndexOutOfRangeException aus.

Die NULL-bedingten Operatoren sind Kurzschlussoperatoren. D.h., wenn ein Vorgang in einer Kette von
bedingten Member- oder Elementzugriffsvorgängen null zurückgibt, wird der Rest der Kette nicht ausgeführt.
Im folgenden Beispiel wird B nicht ausgewertet, wenn A als null ausgewertet wird, und C wird nicht
ausgewertet, wenn A oder B als null ausgewertet wird:

A?.B?.Do(C);
A?.B?[C];

Wenn A NULL sein könnte, aber B und C nicht NULL wären, wenn A nicht NULL ist, müssen Sie nur den
NULL-bedingten Operator auf A anwenden:

A?.B.C();

Im vorherigen Beispiel wird B nicht ausgewertet und C() nicht aufgerufen, wenn A NULL ist. Wenn der
verkettete Memberzugriff jedoch unterbrochen wird (z. B. durch Klammern wie in (A?.B).C() ), erfolgt kein
Kurzschluss.
In den folgenden Beispielen wird die Verwendung der ?. - und ?[] -Operatoren veranschaulicht:

double SumNumbers(List<double[]> setsOfNumbers, int indexOfSetToSum)


{
return setsOfNumbers?[indexOfSetToSum]?.Sum() ?? double.NaN;
}

var sum1 = SumNumbers(null, 0);


Console.WriteLine(sum1); // output: NaN

var numberSets = new List<double[]>


{
new[] { 1.0, 2.0, 3.0 },
null
};

var sum2 = SumNumbers(numberSets, 0);


Console.WriteLine(sum2); // output: 6

var sum3 = SumNumbers(numberSets, 1);


Console.WriteLine(sum3); // output: NaN
using System;
using System.Collections.Generic;
using System.Linq;

namespace MemberAccessOperators2
{
public static class NullConditionalShortCircuiting
{
public static void Main()
{
Person person = null;
person?.Name.Write(); // no output: Write() is not called due to short-circuit.
try
{
(person?.Name).Write();
}
catch (NullReferenceException)
{
Console.WriteLine("NullReferenceException");
}; // output: NullReferenceException
}
}

public class Person


{
public FullName Name { get; set; }
}

public class FullName


{
public string FirstName { get; set; }
public string LastName { get; set; }
public void Write()
{
Console.WriteLine($"{FirstName} {LastName}");
}
}
}

Im ersten der vorangehenden zwei Beispiele wird auch der NULL-Sammeloperator ?? zum Angeben eines
alternativen Ausdrucks zum Auswerten verwendet, falls das Ergebnis eines NULL-bedingten Vorgangs null ist.
Wenn a.x oder a[x] vom Werttyp T ist, der keine NULL-Werte zulässt, ist a?.x oder a?[x] vom
entsprechenden Werttyp T? , der keine NULL-Werte zulässt. Wenn Sie einen Ausdruck vom Typ T benötigen,
wenden Sie den NULL-Sammeloperator ?? auf einen NULL-bedingten Ausdruck an, wie im folgenden Beispiel
gezeigt:

int GetSumOfFirstTwoOrDefault(int[] numbers)


{
if ((numbers?.Length ?? 0) < 2)
{
return 0;
}
return numbers[0] + numbers[1];
}

Console.WriteLine(GetSumOfFirstTwoOrDefault(null)); // output: 0
Console.WriteLine(GetSumOfFirstTwoOrDefault(new int[0])); // output: 0
Console.WriteLine(GetSumOfFirstTwoOrDefault(new[] { 3, 4, 5 })); // output: 7

Wenn Sie im vorherigen Beispiel nicht den ?? -Operator verwenden und numbers den Wert null hat, wird
numbers?.Length < 2 als false ausgewertet.
Der NULL-bedingte Memberzugriffsoperator ?. wird auch als Elvis-Operator bezeichnet.
Threadsicherer Delegataufruf
Verwenden Sie den ?. -Operator, um zu überprüfen, ob ein Delegat ungleich NULL ist, und ihn auf
threadsichere Weise aufzurufen (z.B. wenn Sie ein Ereignis auslösen), wie der folgende Code zeigt:

PropertyChanged?.Invoke(…)

Dass Code dem folgenden Code entspricht, den Sie in C# 5 oder früher verwenden würden:

var handler = this.PropertyChanged;


if (handler != null)
{
handler(…);
}

Dies ist eine threadsichere Möglichkeit, um sicherzustellen, dass nur ein handler ungleich NULL aufgerufen
wird. Da Delegatinstanzen unveränderlich sind, kann kein Thread das Objekt ändern, auf das von der lokalen
handler -Variable verwiesen wird. Insbesondere wenn der von einem anderen Thread ausgeführte Code das
Abonnement des PropertyChanged -Ereignisses aufhebt und PropertyChanged zu null wird, bevor handler
aufgerufen wird, bleibt das Objekt unverändert, auf das von handler verwiesen wird. Der ?. -Operator wertet
seinen linken Operanden nicht mehr als einmal aus, um sicherzustellen, dass er nicht in null geändert werden
kann, nachdem bestätigt wurde, dass er ungleich NULL ist.

Aufrufausdruck „()“
Verwenden Sie Klammern () zum Aufrufen einer Methode, oder rufen Sie einen Delegaten auf.
Im folgenden Beispiel wird der Aufruf einer Methode mit oder ohne Argumente sowie eines Delegaten
veranschaulicht:

Action<int> display = s => Console.WriteLine(s);

var numbers = new List<int>();


numbers.Add(10);
numbers.Add(17);
display(numbers.Count); // output: 2

numbers.Clear();
display(numbers.Count); // output: 0

Klammern verwenden Sie auch beim Aufrufen eines Konstruktors mit dem new -Operator.
Andere Verwendungen von „()“
Mit Klammern passen Sie auch die Reihenfolge an, in der Vorgänge in einem Ausdruck ausgewertet werden
sollen. Weitere Informationen finden Sie unter C#-Operatoren.
Cast-Ausdrücke, die explizite Typkonvertierungen ausführen, verwenden ebenfalls Klammern.

Index vom Endeoperator ^


Der ^ -Operator ist in C# 8.0 und höher verfügbar und gibt die Elementposition vom Ende einer Sequenz an.
Für eine Sequenz der Länge length verweist ^n auf das Element mit dem Offset length - n vom Beginn
einer Sequenz. ^1 zeigt beispielsweise auf das letzte Element einer Sequenz, und ^length zeigt auf das erste
Element einer Sequenz.
int[] xs = new[] { 0, 10, 20, 30, 40 };
int last = xs[^1];
Console.WriteLine(last); // output: 40

var lines = new List<string> { "one", "two", "three", "four" };


string prelast = lines[^2];
Console.WriteLine(prelast); // output: three

string word = "Twenty";


Index toFirst = ^word.Length;
char first = word[toFirst];
Console.WriteLine(first); // output: T

Wie das vorherige Beispiel zeigt, weist Ausdruck ^e den Typ System.Index auf. In Ausdruck ^e muss das
Ergebnis von e implizit in int konvertierbar sein.
Sie können auch den ^ -Operator mit dem Bereichsoperator verwenden, um einen Bereich von Indizes zu
erstellen. Weitere Informationen finden Sie unter Indizes und Bereiche.

Bereichsoperator .
Der Operator .. , der in C# 8.0 und höher verfügbar ist, gibt den Anfang und das Ende eines Bereichs von
Indizes als seine Operanden an. Der linke Operand ist der inklusive Anfang eines Bereichs. Der rechte Operand
ist das exklusive Ende eines Bereichs. Beide Operanden können ein Index vom Anfang oder vom Ende einer
Sequenz sein, wie das folgende Beispiel zeigt:

int[] numbers = new[] { 0, 10, 20, 30, 40, 50 };


int start = 1;
int amountToTake = 3;
int[] subset = numbers[start..(start + amountToTake)];
Display(subset); // output: 10 20 30

int margin = 1;
int[] inner = numbers[margin..^margin];
Display(inner); // output: 10 20 30 40

string line = "one two three";


int amountToTakeFromEnd = 5;
Range endIndices = ^amountToTakeFromEnd..^0;
string end = line[endIndices];
Console.WriteLine(end); // output: three

void Display<T>(IEnumerable<T> xs) => Console.WriteLine(string.Join(" ", xs));

Wie das vorherige Beispiel zeigt, weist der Ausdruck a..b den Typ System.Range auf. In Ausdruck a..b muss
das Ergebnis von a und b implizit in int oder Index konvertierbar sein.
Sie können Operanden des Operators .. auslassen, um einen Bereich ohne Ende abzurufen:
a.. entspricht a..^0
..b entspricht 0..b
.. entspricht 0..^0
int[] numbers = new[] { 0, 10, 20, 30, 40, 50 };
int amountToDrop = numbers.Length / 2;

int[] rightHalf = numbers[amountToDrop..];


Display(rightHalf); // output: 30 40 50

int[] leftHalf = numbers[..^amountToDrop];


Display(leftHalf); // output: 0 10 20

int[] all = numbers[..];


Display(all); // output: 0 10 20 30 40 50

void Display<T>(IEnumerable<T> xs) => Console.WriteLine(string.Join(" ", xs));

Weitere Informationen finden Sie unter Indizes und Bereiche.

Operatorüberladbarkeit
Die Operatoren . , () , ^ und .. können nicht überladen werden. Der [] -Operator wird auch als nicht
überladbarer Operator betrachtet. Verwenden Sie Indexer zur Unterstützung der Indizierung mit
benutzerdefinierten Typen.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Member access (Memberzugriff)
Elementzugriff
NULL-bedingter Operator
Aufrufausdrücke
Weitere Informationen zu Indizes und Bereichen finden Sie unter Hinweis zum Featurevorschlag.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
?? (NULL-Sammeloperator)
::-Operator
Typtestoperatoren und Cast-Ausdrücke (C#-
Referenz)
04.11.2021 • 5 minutes to read

Sie können die folgenden Operatoren und Ausdrücke zur Überprüfung oder Konvertierung von Typen
verwenden:
is-Operator: Prüft, ob der Laufzeittyp eines Ausdrucks mit einem angegebenen Typ kompatibel ist.
as-Operator: Konvertiert einen Ausdruck explizit in einen angegebenen Typ, wenn der Laufzeittyp mit diesem
Typ kompatibel ist.
Cast-Ausdruck: Führt eine explizite Konvertierung durch
typeof-Operator: Ruft die System.Type-Instanz für einen Typ ab.

is-Operator
Der is -Operator prüft, ob der Laufzeittyp eines Ausdrucksergebnisses mit einem angegebenen Typ kompatibel
ist. Ab C# 7.0 überprüft der is -Operator ein Ausdrucksergebnis auch anhand eines Musters.
Der Ausdruck mit dem is -Operator für die Typüberprüfung weist folgende Form auf:

E is T

Hierbei ist E ein Ausdruck, der einen Wert zurückgibt, und T ist der Name eines Typs oder Typparameters. E
kann weder eine anonyme Methode noch ein Lambdaausdruck sein.
Der is -Operator gibt true zurück, wenn ein Ausdrucksergebnis nicht NULL ist und eine der folgenden
Bedingungen erfüllt ist:
Der Laufzeittyp eines Ausdrucksergebnisses ist T .
Der Laufzeittyp eines Ausdrucksergebnisses wird aus Typ T abgeleitet, implementiert die Schnittstelle
T , oder es ist eine andere implizite Verweiskonvertierung von ihm zu T vorhanden.

Der Laufzeittyp eines Ausdrucksergebnisses ist ein Nullwerte zulassender Wert mit dem zugrunde
liegenden Typ T und Nullable<T>.HasValue ist true .
Eine Boxing- oder Unboxing-Konvertierung ist vom Laufzeittyp eines Ausdrucksergebnisses bis zum Typ
T vorhanden.

Der is -Operator berücksichtigt keine benutzerdefinierten Konvertierungen.


Das folgende Beispiel zeigt, dass der is -Operator true zurückgibt, wenn der Laufzeittyp eines
Ausdrucksergebnisses von einem angegebenen Typ abgeleitet ist, wenn also eine Verweiskonvertierung
zwischen Typen besteht:
public class Base { }

public class Derived : Base { }

public static class IsOperatorExample


{
public static void Main()
{
object b = new Base();
Console.WriteLine(b is Base); // output: True
Console.WriteLine(b is Derived); // output: False

object d = new Derived();


Console.WriteLine(d is Base); // output: True
Console.WriteLine(d is Derived); // output: True
}
}

Das nächste Beispiel zeigt, dass der is -Operator Boxing- und Unboxingkonvertierungen berücksichtigt,
numerische Konvertierungen aber nicht:

int i = 27;
Console.WriteLine(i is System.IFormattable); // output: True

object iBoxed = i;
Console.WriteLine(iBoxed is int); // output: True
Console.WriteLine(iBoxed is long); // output: False

Informationen zu C#-Konvertierungen finden Sie im Kapitel Konvertierungen der C#-Sprachspezifikation.


Typüberprüfung mit Musterabgleich
Ab C# 7.0 überprüft der is -Operator ein Ausdrucksergebnis auch anhand eines Musters. Im folgenden Beispiel
wird gezeigt, wie ein Deklarationsmuster verwendet wird, um den Laufzeittyp eines Ausdrucks zu überprüfen:

int i = 23;
object iBoxed = i;
int? jNullable = 7;
if (iBoxed is int a && jNullable is int b)
{
Console.WriteLine(a + b); // output 30
}

Informationen zu den unterstützten Mustern finden Sie unter Muster.

as-Operator
Der as -Operator konvertiert das Ergebnis eines Ausdrucks explizit in einen angegebenen Verweis- oder
Nullable-Typ. Wenn die Konvertierung nicht möglich ist, gibt der as -Operator null zurück. Im Gegensatz zum
Cast-Ausdruck löst der as -Operator nie eine Ausnahme aus.
Sehen Sie sich diesen Ausdruck an:

E as T

Hierbei ist E ein Ausdruck, der einen Wert zurückgibt, und T ist der Name eines Typs oder Typparameters.
Das führt zum gleichen Ergebnis wie dies:
E is T ? (T)(E) : (T)null

außer dass E nur einmal überprüft wird.


Der as -Operator berücksichtigt nur Verweis-, Nullable-, Boxing- und Unboxingkonvertierungen. Sie können
den as -Operator nicht verwenden, um eine benutzerdefinierte Konvertierung auszuführen. Verwenden Sie
hierzu einen Cast-Ausdruck.
Im folgenden Beispiel wird die Verwendung des as -Operators veranschaulicht:

IEnumerable<int> numbers = new[] { 10, 20, 30 };


IList<int> indexable = numbers as IList<int>;
if (indexable != null)
{
Console.WriteLine(indexable[0] + indexable[indexable.Count - 1]); // output: 40
}

NOTE
Wie das vorherige Beispiel zeigt, müssen Sie das Ergebnis des as -Ausdrucks mit null vergleichen, um zu überprüfen,
ob die Konvertierung erfolgreich war. Ab C# 7.0 können Sie den is-Operator verwenden, um sowohl die erfolgreiche
Durchführung der Konvertierung zu überprüfen als auch bei Erfolg das Ergebnis einer neuen Variable zuzuweisen.

Cast-Ausdruck
Ein cast-Ausdruck der Form (T)E führt eine explizite Konvertierung des Ergebnisses des Ausdrucks E in den
Typ T durch. Wenn keine explizite Konvertierung von Typ E in Typ T möglich ist, tritt ein Fehler während der
Kompilierung auf. Möglicherweise ist eine explizite Konvertierung zur Laufzeit nicht erfolgreich, und ein cast-
Ausdruck löst eine Ausnahme aus.
Das folgende Beispiel zeigt explizite numerische und Verweiskonvertierungen:

double x = 1234.7;
int a = (int)x;
Console.WriteLine(a); // output: 1234

IEnumerable<int> numbers = new int[] { 10, 20, 30 };


IList<int> list = (IList<int>)numbers;
Console.WriteLine(list.Count); // output: 3
Console.WriteLine(list[1]); // output: 20

Informationen zu expliziten Konvertierungen finden Sie im Abschnitt Explizite Konvertierungen der C#-
Sprachspezifikation. Informationen zum Definieren einer benutzerdefinierten expliziten oder impliziten
Typkonvertierung finden Sie unter Benutzerdefinierte Konvertierungsoperatoren.
Andere Verwendungen von „()“
Sie verwenden Klammern auch zum Aufrufen einer Methode oder eines Delegaten.
Mit Klammern können Sie auch die Reihenfolge anpassen, in der Vorgänge in einem Ausdruck ausgewertet
werden sollen. Weitere Informationen finden Sie unter C#-Operatoren.

typeof-Operator
Der typeof -Operator ruft die System.Type-Instanz für einen Typ ab. Das Argument für den typeof -Operator
muss der Name eines Typs oder Typparameters sein, wie das folgende Beispiel zeigt:

void PrintType<T>() => Console.WriteLine(typeof(T));

Console.WriteLine(typeof(List<string>));
PrintType<int>();
PrintType<System.Int32>();
PrintType<Dictionary<int, char>>();
// Output:
// System.Collections.Generic.List`1[System.String]
// System.Int32
// System.Int32
// System.Collections.Generic.Dictionary`2[System.Int32,System.Char]

Sie können den typeof -Operator auch mit ungebundenen generischen Typen verwenden. Der Name eines
ungebundenen generischen Typs muss die entsprechende Anzahl von Kommas enthalten: eines weniger als die
Anzahl von Typparametern. Das folgende Beispiel zeigt die Verwendung des typeof -Operators mit einem
ungebundenen generischen Typ:

Console.WriteLine(typeof(Dictionary<,>));
// Output:
// System.Collections.Generic.Dictionary`2[TKey,TValue]

Ein Ausdruck kann kein Argument des typeof -Operators sein. Verwenden Sie die Object.GetType-Methode, um
die System.Type-Instanz für den Laufzeittyp eines Ausdrucksergebnisses abzurufen.
Typüberprüfung mit dem typeof -Operator
Verwenden Sie den typeof -Operator, um zu überprüfen, ob der Laufzeittyp des Ausdrucksergebnisses exakt
mit einem angegebenen Typ übereinstimmt. Das folgende Beispiel zeigt den Unterschied zwischen der
Typüberprüfung mit dem typeof -Operator und mit dem is-Operator:

public class Animal { }

public class Giraffe : Animal { }

public static class TypeOfExample


{
public static void Main()
{
object b = new Giraffe();
Console.WriteLine(b is Animal); // output: True
Console.WriteLine(b.GetType() == typeof(Animal)); // output: False

Console.WriteLine(b is Giraffe); // output: True


Console.WriteLine(b.GetType() == typeof(Giraffe)); // output: True
}
}

Operatorüberladbarkeit
Die Operatoren is , as und typeof können nicht überladen werden.
Ein benutzerdefinierter Typ kann den () -Operator nicht überladen, kann aber benutzerdefinierte
Typkonvertierungen definieren, die durch einen cast-Ausdruck ausgeführt werden können. Weitere
Informationen finden Sie unter Benutzerdefinierte Konvertierungsoperatoren.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Der is-Operator
Der as-Operator
cast-Ausdrücke
Der typeof-Operator

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Vorgehensweise: Sicheres Umwandeln mit Musterabgleich und den Operatoren „is“ und „as“
Generics in .NET
Benutzerdefinierte Konvertierungsoperatoren (C#-
Referenz)
04.11.2021 • 2 minutes to read

Ein benutzerdefinierter Typ kann eine benutzerdefinierte implizite oder explizite Konvertierung von einem oder
in einen anderen Typ definieren.
Zum Aufrufen von impliziten Konvertierungen ist keine spezielle Syntax erforderlich. Implizite Konvertierungen
können in verschiedenen Situationen auftreten, beispielswiese in Arbeitsaufträgen und Methodenaufrufen.
Vordefinierte implizite C#-Konvertierungen sind immer erfolgreich und lösen keine Ausnahmen aus.
Benutzerdefinierte Konvertierungen sollten sich ebenso verhalten. Wenn bei einer benutzerdefinierten
Konvertierung eine Ausnahme ausgelöst werden kann oder Informationen verloren gehen können, definieren
Sie sie als explizite Konvertierung.
Benutzerdefinierte Konvertierungen werden vom is- und as-Operator nicht berücksichtigt. Verwenden Sie einen
Cast-Ausdruck, um eine benutzerdefinierte explizite Konvertierung aufzurufen.
Verwenden Sie zum Definieren einer impliziten bzw. expliziten Konvertierung die Schlüsselwörter operator und
implicit bzw. explicit . Bei dem Typ, der eine Konvertierung definiert, muss es sich um einen Quelltyp oder
um einen Zieltyp dieser Konvertierung handeln. Eine Konvertierung zwischen zwei benutzerdefinierten Typen
kann in einem der beiden Typen definiert werden.
Im folgenden Beispiel wird gezeigt, wie eine implizite und eine explizite Konvertierung definiert wird:
using System;

public readonly struct Digit


{
private readonly byte digit;

public Digit(byte digit)


{
if (digit > 9)
{
throw new ArgumentOutOfRangeException(nameof(digit), "Digit cannot be greater than nine.");
}
this.digit = digit;
}

public static implicit operator byte(Digit d) => d.digit;


public static explicit operator Digit(byte b) => new Digit(b);

public override string ToString() => $"{digit}";


}

public static class UserDefinedConversions


{
public static void Main()
{
var d = new Digit(7);

byte number = d;
Console.WriteLine(number); // output: 7

Digit digit = (Digit)number;


Console.WriteLine(digit); // output: 7
}
}

Sie können auch das Schlüsselwort operator verwenden, um einen vordefinierten C#-Operator zu überladen.
Weitere Informationen finden Sie unter Operatorüberladung.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Konvertierungsoperatoren
User-defined conversions (Benutzerdefinierte Konvertierungen)
Implicit conversions (Implizite Konvertierungen)
Explicit conversions (Explizite Konvertierungen)

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Operatorüberladung
Typtest- und Umwandlungsoperatoren
Umwandlung und Typkonvertierung
Entwurfsrichtlinien: Konvertierungsoperatoren
Chained user-defined explicit conversions in C# (Verkettete benutzerdefinierte, explizite Konvertierungen in
C#)
Operatoren im Zusammenhang mit Zeigern (C#-
Referenz)
04.11.2021 • 7 minutes to read

Sie können die folgenden Operatoren für Zeiger verwenden:


Unärer Operator & (Adresse von): Abrufen der Adresse einer Variablen
Unärer Operator * (Zeigerdereferenzierung): Abrufen der Variablen, auf die per Zeiger verwiesen wird
Operatoren -> (Memberzugriff) und [] (Elementzugriff)
Arithmetische Operatoren + , - , ++ und --
Vergleichsoperatoren == , != , < , > , <= und >=
Informationen zu Zeigertypen finden Sie unter Zeigertypen.

NOTE
Für alle Operationen mit Zeigern ist ein Kontext des Typs unsafe erforderlich. Code, in dem unsichere Blöcke enthalten
sind, muss mit der Compileroption AllowUnsafeBlocks kompiliert werden.

Adressoperator &
Der unäre Operator & gibt die Adresse seines Operanden zurück:

unsafe
{
int number = 27;
int* pointerToNumber = &number;

Console.WriteLine($"Value of the variable: {number}");


Console.WriteLine($"Address of the variable: {(long)pointerToNumber:X}");
}
// Output is similar to:
// Value of the variable: 27
// Address of the variable: 6C1457DBD4

Der Operand des Operators & muss eine feste Variable sein. Feste Variablen befinden sich an Speicherorten,
auf die sich Garbage Collector-Operationen nicht auswirken. Im vorherigen Beispiel ist die lokale Variable
number eine feste Variable, da sie im Stapel angeordnet ist. Variablen an Speicherorten, auf die sich der Garbage
Collector auswirken kann (z. B. durch eine Verschiebung), werden als bewegliche Variablen bezeichnet.
Objektfelder und Arrayelemente sind Beispiele für bewegliche Variablen. Sie können die Adresse einer
beweglichen Variablen erhalten, wenn Sie sie mit einer fixed -Anweisung „fixieren“ bzw. „anheften“. Die
erhaltene Adresse ist nur innerhalb des Blocks einer fixed -Anweisung gültig. Im folgenden Beispiel wird
veranschaulicht, wie Sie eine fixed -Anweisung und den & -Operator verwenden:
unsafe
{
byte[] bytes = { 1, 2, 3 };
fixed (byte* pointerToFirst = &bytes[0])
{
// The address stored in pointerToFirst
// is valid only inside this fixed statement block.
}
}

Sie können nicht die Adresse einer Konstanten oder eines Werts abrufen.
Weitere Informationen zu festen und beweglichen Variablen finden Sie im Abschnitt Feste und verschiebbare
Variablen der Spezifikation für die Sprache C#.
Mit dem binären Operator & wird der Wert für logisches UND der booleschen Operanden oder der Wert für
bitweise logisches UND für die integralen Operanden berechnet.

Zeigerdereferenzierungsoperator *
Mit dem unären Zeigerdereferenzierungsoperator * wird die Variable abgerufen, auf die der Operand verweist.
Er wird auch kurz als Dereferenzierungsoperator bezeichnet. Der Operand des Operators * muss einen
Zeigertyp aufweisen.

unsafe
{
char letter = 'A';
char* pointerToLetter = &letter;
Console.WriteLine($"Value of the `letter` variable: {letter}");
Console.WriteLine($"Address of the `letter` variable: {(long)pointerToLetter:X}");

*pointerToLetter = 'Z';
Console.WriteLine($"Value of the `letter` variable after update: {letter}");
}
// Output is similar to:
// Value of the `letter` variable: A
// Address of the `letter` variable: DCB977DDF4
// Value of the `letter` variable after update: Z

Sie können den Operator * nicht auf einen Ausdruck vom Typ void* anwenden.
Mit dem binären Operator * wird das Produkt seiner numerischen Operanden berechnet.

Zeigermember-Zugriffsoperator ->
Mit dem Operator -> werden die Zeigerdereferenzierung und der Memberzugriff kombiniert. Wenn x ein
Zeiger des Typs T* und y ein Member des Typs T ist, auf den zugegriffen werden kann, ist ein Ausdruck der
Form

x->y

für die folgende Syntax:

(*x).y

Im folgenden Beispiel wird die Verwendung des -> -Operators veranschaulicht:


public struct Coords
{
public int X;
public int Y;
public override string ToString() => $"({X}, {Y})";
}

public class PointerMemberAccessExample


{
public static unsafe void Main()
{
Coords coords;
Coords* p = &coords;
p->X = 3;
p->Y = 4;
Console.WriteLine(p->ToString()); // output: (3, 4)
}
}

Sie können den Operator -> nicht auf einen Ausdruck vom Typ void* anwenden.

Zeigerelementzugriff-Operator []
Für einen Ausdruck p mit einem Zeigertyp wird ein Zeigerelementzugriff der Form p[n] als *(p + n)
ausgewertet. Hierbei muss n einen Typ aufweisen, der implizit in int , uint , long oder ulong konvertiert
werden kann. Informationen zum Verhalten des Operators + mit Zeigern finden Sie im Abschnitt Addieren
oder Subtrahieren eines Integralwerts zu bzw. von einem Zeiger.
Im folgenden Beispiel wird veranschaulicht, wie Sie mit einem Zeiger und dem Operator [] auf Arrayelemente
zugreifen:

unsafe
{
char* pointerToChars = stackalloc char[123];

for (int i = 65; i < 123; i++)


{
pointerToChars[i] = (char)i;
}

Console.Write("Uppercase letters: ");


for (int i = 65; i < 91; i++)
{
Console.Write(pointerToChars[i]);
}
}
// Output:
// Uppercase letters: ABCDEFGHIJKLMNOPQRSTUVWXYZ

Im vorangegangenen Beispiel ordnet ein stackalloc -Ausdruck einen Speicherblock im Stapel zu.

NOTE
Der Zeigerelementzugriff-Operator führt keine Überprüfung auf Fehler vom Typ „Außerhalb des gültigen Bereichs“ durch.

Sie können [] nicht für den Zeigerelementzugriff mit einem Ausdruck vom Typ void* verwenden.
Sie können auch den Operator [] für den Arrayelement- oder Indexerzugriff nutzen.
Arithmetische Zeigeroperatoren
Sie können mit Zeigern die folgenden arithmetischen Operationen durchführen:
Addieren oder Subtrahieren eines Integralwerts zu bzw. von einem Zeiger
Subtrahieren von zwei Zeigern
Inkrementieren oder Dekrementieren eines Zeigers
Sie können diese Operationen nicht mit Zeigern vom Typ void* durchführen.
Weitere Informationen zu unterstützten arithmetischen Operationen mit numerischen Typen finden Sie unter
Arithmetische Operatoren.
Addieren oder Subtrahieren eines Integralwerts zu bzw. von einem Zeiger
Für einen Zeiger p vom Typ T* und einen Ausdruck n eines Typs, der implizit in int , uint , long oder
ulong konvertiert werden kann, sind die Addition und die Subtraktion wie folgt definiert:

Für die Ausdrücke p + n und n + p wird jeweils ein Zeiger vom Typ T* erzeugt, der sich aus dem
Addieren von n * sizeof(T) zur Adresse von p ergibt.
Für den Ausdruck p - n wird ein Zeiger vom Typ T* erzeugt, der sich ergibt, indem n * sizeof(T) von der
Adresse von p subtrahiert wird.

Mit dem Operator sizeof wird die Größe eines Typs in Byte abgerufen.
Im folgenden Beispiel wird die Verwendung des Operators + mit einem Zeiger veranschaulicht:

unsafe
{
const int Count = 3;
int[] numbers = new int[Count] { 10, 20, 30 };
fixed (int* pointerToFirst = &numbers[0])
{
int* pointerToLast = pointerToFirst + (Count - 1);

Console.WriteLine($"Value {*pointerToFirst} at address {(long)pointerToFirst}");


Console.WriteLine($"Value {*pointerToLast} at address {(long)pointerToLast}");
}
}
// Output is similar to:
// Value 10 at address 1818345918136
// Value 30 at address 1818345918144

Zeigersubtraktion
Für die beiden Zeiger p1 und p2 vom Typ T* ergibt der Ausdruck p1 - p2 den Unterschied zwischen den
Adressen von p1 und p2 dividiert durch sizeof(T) . Das Ergebnis hat den Typ long . p1 - p2 wird also als
((long)(p1) - (long)(p2)) / sizeof(T) berechnet.

Im folgenden Beispiel ist die Zeigersubtraktion dargestellt:

unsafe
{
int* numbers = stackalloc int[] { 0, 1, 2, 3, 4, 5 };
int* p1 = &numbers[1];
int* p2 = &numbers[5];
Console.WriteLine(p2 - p1); // output: 4
}

Inkrementieren und Dekrementieren von Zeigern


Mit dem Inkrementoperator ++ wird 1 zum Zeigeroperanden addiert. Mit dem Dekrementoperator -- wird 1
vom Zeigeroperanden subtrahiert.
Für beide Operatoren werden zwei Formen unterstützt: Postfix ( p++ und p-- ) und Präfix ( ++p und --p ). Das
Ergebnis von p++ und p-- ist der Wert von p vor der Operation. Das Ergebnis von ++p und --p ist der
Wert von p nach der Operation.
Im folgenden Beispiel wird das Verhalten von Postfix- und Präfix-Inkrementoperatoren veranschaulicht:

unsafe
{
int* numbers = stackalloc int[] { 0, 1, 2 };
int* p1 = &numbers[0];
int* p2 = p1;
Console.WriteLine($"Before operation: p1 - {(long)p1}, p2 - {(long)p2}");
Console.WriteLine($"Postfix increment of p1: {(long)(p1++)}");
Console.WriteLine($"Prefix increment of p2: {(long)(++p2)}");
Console.WriteLine($"After operation: p1 - {(long)p1}, p2 - {(long)p2}");
}
// Output is similar to
// Before operation: p1 - 816489946512, p2 - 816489946512
// Postfix increment of p1: 816489946512
// Prefix increment of p2: 816489946516
// After operation: p1 - 816489946516, p2 - 816489946516

Vergleichsoperatoren für Zeiger


Sie können die Operatoren == , != , < , > , <= und >= verwenden, um Operanden jedes Zeigertyps zu
vergleichen, z. B. void* . Mit diesen Operatoren werden die Adressen der zwei Operanden so verglichen, als ob
es sich um ganze Zahlen ohne Vorzeichen handeln würde.
Informationen zum Verhalten dieser Operatoren für Operanden anderer Typen finden Sie in den Artikeln zu
Gleichheitsoperatoren und Vergleichsoperatoren.

Operatorrangfolge
In der folgenden Liste sind die Zeigeroperatoren absteigend nach der Rangfolge sortiert:
Inkrementierungs- und Dekrementierungsoperatoren in Postfixnotation ( x++ und x-- ) und die Operatoren
-> und []
Inkrementierungs- und Dekrementierungsoperatoren in Präfixnotation ( ++x und --x ) und die Operatoren
& und *
Additive Operatoren + und -
Vergleichsoperatoren < , > , <= und >=
Gleichheitsoperatoren == und !=

Verwenden Sie Klammern () , wenn Sie die Reihenfolge der Auswertung ändern möchten, die durch die
Operatorrangfolge festgelegt ist.
Die vollständige Liste der nach Rangfolgenebene sortierten C#-Operatoren finden Sie im Abschnitt
Operatorrangfolge im Artikel C#-Operatoren.

Operatorüberladbarkeit
Mit einem benutzerdefinierten Typ können die Zeigeroperatoren & , * , -> und [] nicht überladen werden.
C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Fixed and moveable variables (Feste und verschiebbare Variablen)
Adresse-von-Operator
Zeigerdereferenzierung
Zeigermemberzugriff
Zeigerelementzugriff
Zeigerarithmetik
Inkrementieren und Dekrementieren von Zeigern
Zeigervergleich

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Zeigertypen
unsafe (Schlüsselwort)
fixed (Schlüsselwort)
stackalloc
sizeof (Operator)
Zuweisungsoperatoren (C#-Referenz)
04.11.2021 • 2 minutes to read

Der Zuweisungsoperator = weist den Wert seines rechtsseitigen Operanden einer Variablen, einer Eigenschaft
oder einem, durch seinen linksseitigen Operanden angegebenen Indexer-Element zu. Das Ergebnis eines
Zuweisungsausdrucks ist der Wert, der dem linksseitigen Operanden zugewiesen wird. Der Typ des
rechtsseitigen Operanden muss mit dem Typ des linksseitigen Operanden übereinstimmen oder implizit in ihn
konvertierbar sein.
Der Zuweisungsoperator = ist rechtsassoziativ, d. h. ein Ausdruck der Form

a = b = c

wird als ausgewertet,

a = (b = c)

Das folgende Beispiel zeigt die Verwendung des Zuweisungsoperators mit einer lokalen Variablen, einer
Eigenschaft und einem Indexerelement als linksseitigem Operanden:

var numbers = new List<double>() { 1.0, 2.0, 3.0 };

Console.WriteLine(numbers.Capacity);
numbers.Capacity = 100;
Console.WriteLine(numbers.Capacity);
// Output:
// 4
// 100

int newFirstElement;
double originalFirstElement = numbers[0];
newFirstElement = 5;
numbers[0] = newFirstElement;
Console.WriteLine(originalFirstElement);
Console.WriteLine(numbers[0]);
// Output:
// 1
// 5

ref-Zuweisungsoperator
Ab C# 7.3 können Sie mit dem ref-Zuweisungsoperator = ref eine ref local- oder ref readonly local-Variable
neu zuweisen. Im folgenden Beispiel wird die Verwendung des ref-Zuweisungsoperators veranschaulicht:
void Display(double[] s) => Console.WriteLine(string.Join(" ", s));

double[] arr = { 0.0, 0.0, 0.0 };


Display(arr);

ref double arrayElement = ref arr[0];


arrayElement = 3.0;
Display(arr);

arrayElement = ref arr[arr.Length - 1];


arrayElement = 5.0;
Display(arr);
// Output:
// 0 0 0
// 3 0 0
// 3 0 5

Im Fall des ref-Zuweisungsoperators müssen seine beiden Operanden denselben Typ haben.

Verbundzuweisung
Bei einem binären Operator op entspricht ein Verbundzuweisungsausdruck der Form

x op= y

für die folgende Syntax:

x = x op y

außer dass x nur einmal überprüft wird.


Verbundzuweisungen werden von arithmetischen, logischen booleschen und bitweisen logischen und
Verschiebungs--Operatoren unterstützt.

NULL-Coalescing-Zuweisung
Sie können ab C# 8.0 den NULL-Coalescing-Zuweisungsoperator ??= verwenden, um den Wert des rechten
Operanden dem linken Operanden nur dann zuzuweisen, wenn die Auswertung des linken Operanden null
ergibt. Weitere Informationen finden Sie im Artikel zu den Operatoren ?? und ??=.

Operatorüberladbarkeit
Ein benutzerdefinierter Typ kann den Zuweisungsoperator nicht überladen. Ein benutzerdefinierter Typ kann
jedoch eine implizite Konvertierung in einen anderen Typ definieren. Auf diese Weise kann der Wert eines
benutzerdefinierten Typs einer Variablen, einer Eigenschaft oder einem Indexerelement eines anderen Typs
zugewiesen werden. Weitere Informationen finden Sie unter Benutzerdefinierte Konvertierungsoperatoren.
Ein benutzerdefinierter Typ kann einen Verbundzuweisungsoperator nicht explizit überladen. Wenn jedoch ein
benutzerdefinierter Typ einen binären op -Operator überlädt, wird der op= -Operator, sofern vorhanden,
ebenfalls implizit überladen.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Zuweisungsoperatoren der C#-Sprachspezifikation.
Weitere Informationen zum REF-Zuweiseungsoperator = ref finden Sie unter Hinweis zum Featurevorschlag.
Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
ref (C#-Referenz)
Lambdaausdrücke (C#-Referenz)
04.11.2021 • 11 minutes to read

Sie verwenden einen Lambdaausdruck, um eine anonyme Funktion zu erstellen. Verwenden Sie den
Lambdadeklarationsoperator => , um die Parameterliste des Lambdas von dessen Text zu trennen. Ein
Lambdaausdruck kann eine der folgenden beiden Formen aufweisen:
Ein Ausdruckslambda mit einem Ausdruck als Text:

(input-parameters) => expression

Ein Anweisungslambda mit einem Anweisungsblock als Text:

(input-parameters) => { <sequence-of-statements> }

Geben Sie zum Erstellen eines Lambdaausdrucks Eingabeparameter (falls vorhanden) auf der linken Seite des
Lambdaoperators und einen Ausdruck oder Anweisungsblock auf der anderen Seite an.
Jeder Lambdaausdruck kann in einen Delegat-Typ konvertiert werden. Der Delegattyp, in den ein
Lambdaausdruck konvertiert werden kann, wird durch die Typen seiner Parameter und Rückgabewerte
definiert. Wenn ein Lambdaausdruck keinen Wert zurückgibt, kann er in einen der Action -Delegattypen
konvertiert werden. Andernfalls kann er in einen der Func -Delegattypen konvertiert werden. Ein
Lambdaausdruck, der beispielsweise zwei Parameter hat und keinen Wert zurückgibt, kann in einen
Action<T1,T2>-Delegat konvertiert werden. Außerdem kann ein Lambdaausdruck, der einen Parameter hat und
einen Wert zurückgibt, in einen Func<T,TResult>-Delegat konvertiert werden. Im folgenden Beispiel wird der
Lambdaausdruck x => x * x , der einen Parameter x angibt und den Wert von x im Quadrat zurückgibt,
einer Variablen eines Delegattyps zugewiesen:

Func<int, int> square = x => x * x;


Console.WriteLine(square(5));
// Output:
// 25

Zudem lassen sich Ausdruckslambdas in Ausdrucksbaumstruktur-Typen konvertieren, wie im folgenden Beispiel


gezeigt:

System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;


Console.WriteLine(e);
// Output:
// x => (x * x)

Sie können Lambdaausdrücke in jedem Code verwenden, der Instanzen von Delegattypen oder
Ausdrucksbaumstrukturen erfordert, z.B. als Argument für die Task.Run(Action)-Methode, um den im
Hintergrund auszuführenden Code zu übergeben. Wie im folgenden Beispiel gezeigt wird, können Sie beim
Schreiben von LINQ in C# auch Lambdaausdrücke verwenden:
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25

Wenn Sie zum Aufrufen der Enumerable.Select-Methode in der System.Linq.Enumerable-Klasse


methodenbasierte Syntax verwenden, wie in LINQ to Objects und LINQ to XML, ist der Parameter ein Delegattyp
System.Func<T,TResult>. Wenn Sie die Queryable.Select-Methode in der System.Linq.Queryable-Klasse
aufrufen, wie in LINQ to SQL, ist der Parametertyp ein Ausdrucksbaumstruktur-Typ
Expression<Func<TSource,TResult>> . In beiden Fällen können Sie denselben Lambdaausdruck verwenden, um die
Parameterwerte anzugeben. Dadurch sehen die Select -Aufrufe ähnlich aus, obwohl sich der mithilfe der
Lambdas erstellte Objekttyp unterscheidet.

Ausdruckslambdas
Ein Lambdaausdruck mit einem Ausdruck auf der rechten Seite des => -Operators wird als Ausdruckslambda
bezeichnet. Ein Ausdruckslambda gibt das Ergebnis des Ausdrucks zurück und hat folgende grundlegende Form:

(input-parameters) => expression

Der Text eines Ausdruckslambdas kann aus einem Methodenaufruf bestehen. Wenn Sie jedoch
Ausdrucksbaumstrukturen erstellen, die außerhalb des Kontexts der .NET Common Language Runtime (CLR)
ausgewertet werden, z. B. in SQL Server, sollten Sie in Lambdaausdrücken keine Methodenaufrufe verwenden.
Die Methoden haben außerhalb des Kontexts der .NET Common Language Runtime (CLR) keine Bedeutung.

Anweisungslambdas
Ein Anweisungslambda ähnelt einem Ausdruckslambda, allerdings sind die Anweisungen in Klammern
eingeschlossen:

(input-parameters) => { <sequence-of-statements> }

Der Text eines Anweisungslambdas kann aus einer beliebigen Anzahl von Anweisungen bestehen, wobei es sich
meistens um höchstens zwei oder drei Anweisungen handelt.

Action<string> greet = name =>


{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!

Anweisungslambdas können nicht zum Erstellen von Ausdrucksbaumstrukturen verwendet werden.

Eingabeparameter eines Lambdaausdrucks


Sie schließen die Eingabeparameter eines Lambdaausdrucks in Klammern ein. Geben Sie Eingabeparameter von
0 (null) mit leeren Klammern an:
Action line = () => Console.WriteLine();

Wenn ein Lambdaausdruck nur über einen Eingabeparameter verfügt, sind Klammern optional:

Func<double, double> cube = x => x * x * x;

Zwei oder mehr Eingabeparameter werden durch Kommas getrennt:

Func<int, int, bool> testForEquality = (x, y) => x == y;

Manchmal kann der Compiler die Typen von Eingabeparametern nicht ableiten. Sie können die Typen wie im
folgenden Beispiel dargestellt explizit angeben:

Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;

Eingabeparametertypen müssen alle entweder explizit oder implizit sein. Andernfalls tritt der Compilerfehler
CS0748 auf.
Ab C# 9.0 können Sie discards verwenden, um mindestens zwei Eingabeparameter eines Lambdaausdrucks
anzugeben, der nicht im Ausdruck verwendet wird:

Func<int, int, int> constant = (_, _) => 42;

Verwerfungsparameter von Lambdas können nützlich sein, wenn Sie einen Lambdaausdruck zur Bereitstellung
eines Ereignishandlers verwenden.

NOTE
Aus Gründen der Abwärtskompatibilität wird innerhalb eines Lambdaausdrucks _ als Name des Parameters behandelt,
wenn nur ein einzelner Eingabeparameter den Namen _ aufweist.

Asynchrone Lambdas
Sie können mit den Schlüsselwörtern async und await Lambda-Ausdrücke und Anweisungen, die asynchrone
Verarbeitung enthalten, leicht erstellen. Das folgende Windows Forms enthält z. B. einen Ereignishandler, der
eine Async-Methode, ExampleMethodAsync , aufruft und erwartet.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += button1_Click;
}

private async void button1_Click(object sender, EventArgs e)


{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
}

private async Task ExampleMethodAsync()


{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}

Sie können denselben Ereignishandler hinzufügen, indem Sie ein asynchrones Lambda verwenden. Um diesen
Handler hinzuzufügen, fügen Sie einen async -Modifizierer vor der Lambdaparameterliste hinzu, wie im
folgenden Beispiel dargestellt:

public partial class Form1 : Form


{
public Form1()
{
InitializeComponent();
button1.Click += async (sender, e) =>
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
};
}

private async Task ExampleMethodAsync()


{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}

Weitere Informationen zum Erstellen und Verwenden von asynchronen Methoden finden Sie unter
Asynchronous programming with async and await (Asynchrones Programmieren mit „async“ and „await“).

Lambdaausdrücke und Tupel


Ab C# 7.0 bietet die C#-Programmiersprache integrierte Unterstützung für Tupel. Sie können ein Tupel einem
Lambdaausdruck als Argument bereitstellen, und Ihr Lambdaausdruck kann ebenfalls einen Tupel zurückgeben.
In einigen Fällen verwendet der C#-Compiler den Typrückschluss, um den Typ der Tupelkomponenten zu
ermitteln.
Sie können ein Tupel definieren, indem Sie eine durch Trennzeichen getrennte Liste seiner Komponenten in
Klammern einschließen. In folgendem Beispiel wird ein Tupel mit drei Komponenten verwenden, um eine
Zahlensequenz an einen Lambdaausdruck zu übergeben; dadurch wird jeder Wert verdoppelt, und es wird ein
Tupel mit drei Komponenten zurückgegeben, das das Ergebnis der Multiplikation enthält.
Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)

Für gewöhnlich heißen die Felder eines Tupels Item1 , Item2 , usw. Sie können allerdings ein Tupel mit
benannten Komponenten definieren, wie in folgendem Beispiel veranschaulicht.

Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");

Weitere Informationen zu C#-Tupeln finden Sie unter Tupeltypen.

Lambdas mit Standardabfrageoperatoren


LINQ to Objects haben, neben anderen Implementierungen, einen Eingabeparameter, dessen Typ Teil der
Func<TResult>-Familie generischer Delegate ist. Diese Delegaten verwenden Typparameter zur Definition der
Anzahl und des Typs der Eingabeparameter sowie des Rückgabetyps des Delegaten. Func -Delegaten sind für
das Kapseln von benutzerdefinierten Ausdrücken, die für jedes Element in einem Satz von Quelldaten
übernommen werden, sehr nützlich. Betrachten Sie z.B. den Func<T,TResult>-Delegattyp:

public delegate TResult Func<in T, out TResult>(T arg)

Der Delegat kann als Func<int, bool> -Instanz instanziiert werden, wobei int ein Eingabeparameter und bool
der Rückgabewert ist. Der Rückgabewert wird immer im letzten Typparameter angegeben.
Func<int, string, bool> definiert z.B. einen Delegaten mit zwei Eingabeparametern, int und string , und
einen Rückgabetyp von bool . Der folgende Func -Delegat gibt bei einem Aufruf einen booleschen Wert zurück,
um anzugeben, ob der Eingabeparameter gleich fünf ist:

Func<int, bool> equalsFive = x => x == 5;


bool result = equalsFive(4);
Console.WriteLine(result); // False

Sie können einen Lambdaausdruck auch dann angeben, wenn der Argumenttyp Expression<TDelegate> ist,
beispielsweise in den Standardabfrageoperatoren, die in Typ Queryable definiert sind. Wenn Sie ein
Expression<TDelegate>-Argument angeben, wird der Lambdaausdruck in eine Ausdrucksbaumstruktur
kompiliert.
Im folgenden Beispiel wird der Count-Standardabfrageoperator verwendet:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");

Der Compiler kann den Typ des Eingabeparameters ableiten, Sie können ihn aber auch explizit angeben. Dieser
bestimmte Lambda-Ausdruck zählt die ganzen Zahlen ( n ), bei denen nach dem Dividieren durch zwei als Rest 1
bleibt.
In folgendem Beispiel wird eine Sequenz erzeugt, die alle Elemente im Array numbers enthält, die vor der 9
auftreten, da dies die erste Zahl in der Sequenz ist, die die Bedingung nicht erfüllt:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3

In folgendem Beispiel wird gezeigt, wie Sie mehrere Eingabeparameter angeben, indem Sie sie in Klammern
einschließen. Mit der Methode werden alle Elemente im numbers -Array zurückgegeben, bis eine Zahl erreicht
wird, deren Wert kleiner ist als ihre Ordnungspostion im Array:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4

Sie verwenden Lambdaausdrücke nicht direkt in Abfrageausdrücken, aber Sie können sie in Methodenaufrufen
innerhalb von Abfrageausdrücken verwenden, wie das folgende Beispiel zeigt:

var numberSets = new List<int[]>


{
new[] { 1, 2, 3, 4, 5 },
new[] { 0, 0, 0 },
new[] { 9, 8 },
new[] { 1, 0, 1, 0, 1, 0, 1, 0 }
};

var setsWithManyPositives =
from numberSet in numberSets
where numberSet.Count(n => n > 0) > 3
select numberSet;

foreach (var numberSet in setsWithManyPositives)


{
Console.WriteLine(string.Join(" ", numberSet));
}
// Output:
// 1 2 3 4 5
// 1 0 1 0 1 0 1 0

Typrückschluss in Lambdaausdrücken
Beim Schreiben von Lambdas müssen Sie oftmals keinen Typ für die Eingabeparameter angeben, da der
Compiler den Typ auf der Grundlage des Lambdatexts, der Parametertypen und anderer Faktoren ableiten kann,
wie in der C#-Programmiersprachenspezifikation beschrieben. Bei den meisten Standardabfrageoperatoren
entspricht die erste Eingabe dem Typ der Elemente in der Quellsequenz. Beim Abfragen von
IEnumerable<Customer> wird die Eingabevariable als Customer -Objekt abgeleitet, sodass Sie auf die zugehörigen
Methoden und Eigenschaften zugreifen können:

customers.Where(c => c.City == "London");

Die allgemeinen Regeln für Typrückschlüsse bei Lambdas lauten wie folgt:
Der Lambda-Ausdruck muss dieselbe Anzahl von Parametern enthalten wie der Delegattyp.
Jeder Eingabeparameter im Lambda muss implizit in den entsprechenden Delegatparameter konvertiert
werden können.
Der Rückgabewert des Lambdas (falls vorhanden) muss implizit in den Rückgabetyp des Delegaten
konvertiert werden können.
Beachten Sie, dass Lambdaausdrücke keinen eigenen Typ haben, da das allgemeine Typsystem kein internes
Konzept von „Lambdaausdrücken“ aufweist. Es kann manchmal praktisch sein, informell vom „Typ“ eines
Lambdaausdrucks zu sprechen. In einem solchen Fall bezeichnet Typ den Delegattyp bzw. den Expression -Typ, in
den der Lambda-Ausdruck konvertiert wird.

Erfassen äußerer Variablen sowie des Variablenbereichs in


Lambdaausdrücken
Lambdas können auf äußere Variablen verweisen. Hierbei handelt es sich um die Variablen, die im Bereich der
Methode, mit der der Lambdaausdruck definiert wird, oder im Bereich des Typs liegen, der den Lambdaausdruck
enthält. Variablen, die auf diese Weise erfasst werden, werden zur Verwendung in Lambda-Ausdrücken
gespeichert, auch wenn die Variablen andernfalls außerhalb des Gültigkeitsbereichs liegen und an die Garbage
Collection übergeben würden. Eine äußere Variable muss definitiv zugewiesen sein, bevor sie in einem Lambda-
Ausdruck verwendet werden kann. Das folgende Beispiel veranschaulicht diese Regeln:
public static class VariableScopeWithLambdas
{
public class VariableCaptureGame
{
internal Action<int> updateCapturedLocalVariable;
internal Func<int, bool> isEqualToCapturedLocalVariable;

public void Run(int input)


{
int j = 0;

updateCapturedLocalVariable = x =>
{
j = x;
bool result = j > input;
Console.WriteLine($"{j} is greater than {input}: {result}");
};

isEqualToCapturedLocalVariable = x => x == j;

Console.WriteLine($"Local variable before lambda invocation: {j}");


updateCapturedLocalVariable(10);
Console.WriteLine($"Local variable after lambda invocation: {j}");
}
}

public static void Main()


{
var game = new VariableCaptureGame();

int gameInput = 5;
game.Run(gameInput);

int jTry = 10;


bool result = game.isEqualToCapturedLocalVariable(jTry);
Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");

int anotherJ = 3;
game.updateCapturedLocalVariable(anotherJ);

bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);


Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
}
// Output:
// Local variable before lambda invocation: 0
// 10 is greater than 5: True
// Local variable after lambda invocation: 10
// Captured local variable is equal to 10: True
// 3 is greater than 5: False
// Another lambda observes a new value of captured variable: True
}

Die folgenden Regeln gelten für den Variablenbereich in Lambda-Ausdrücken:


Eine erfasste Variable wird erst dann an die Garbage Collection übergeben, wenn der darauf verweisende
Delegat für die Garbage Collection geeignet ist.
Variablen, die in einem Lambdaausdruck eingeführt wurden, sind in der einschließenden Methode nicht
sichtbar.
Ein Lambdaausdruck kann einen in-, ref- oder out-Parameter nicht direkt von der einschließenden
Methode erfassen.
Eine return-Anweisung in einem Lambdaausdruck bewirkt keine Rückgabe durch die einschließende
Methode.
Ein Lambdaausdruck darf keine goto-, break- oder continue-Anweisung enthalten, wenn das Ziel dieser
Sprunganweisung außerhalb des Lambdaausdrucksblocks liegt. Eine Sprunganweisung darf auch nicht
außerhalb des Lambdaausdrucksblocks sein, wenn das Ziel im Block ist.
Ab C# 9.0 können Sie den static -Modifizierer auf einen Lambdaausdruck anwenden, um zu verhindern, dass
lokale Variablen oder der Instanzzustand versehentlich durch die Lambdafunktion erfasst werden:

Func<double, double> square = static x => x * x;

Ein statischer Lambdaausdruck kann keine lokalen Variablen oder den Instanzzustand aus einschließenden
Bereichen erfassen, kann jedoch auf statische Member und Konstantendefinitionen verweisen.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Anonyme Funktionsausdrücke der C#-Sprachspezifikation.
Weitere Informationen zu in C# 9.0 eingeführten Features finden Sie in den folgenden Featurevorschlägen:
Parameter zum Verwerfen von Lambdafunktion
Statische anonyme Funktionen

Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
LINQ (Language Integrated Query)
Ausdrucksbaumstrukturen
Lokale Funktionen im Vergleich zu Lambdaausdrücken
Visual Studio 2008 C#-Beispiele (siehe LINQ-Beispielabfragedateien und XQuery-Programm)
Muster (C#-Referenz)
04.11.2021 • 14 minutes to read

In C# wurde Musterabgleich in C# 7.0 eingeführt. Seitdem wurden in jeder wichtigen C#-Version die
Musterabgleichsfunktionen erweitert. Die folgenden C#-Ausdrücke und -Anweisungen unterstützen
Musterabgleiche:
is -Ausdruck
switch -Anweisung
switch Ausdruck (eingeführt in C# 8.0)

In diesen Konstrukten können Sie einen Eingabeausdruck gegen jedes der folgenden Muster abgleichen:
Deklarationsmuster: um den Laufzeittyp eines Ausdrucks zu überprüfen und bei einem erfolgreichen
Abgleich einer deklarierten Variable ein Ausdrucksergebnis zuzuweisen. Eingeführt in C# 7.0.
Typmuster: um den Laufzeittyp eines Ausdrucks zu überprüfen. Eingeführt in C# 9.0.
Konstantenmuster: um zu testen, ob ein Ausdrucksergebnis einer angegebenen Konstante entspricht.
Eingeführt in C# 7.0.
Relationale Muster: um ein Ausdrucksergebnis mit einer angegebenen Konstante zu vergleichen. Eingeführt
in C# 9.0.
Logische Muster: um zu testen, ob ein Ausdruck mit einer logischen Kombination von Mustern
übereinstimmt. Eingeführt in C# 9.0.
Eigenschaftsmuster: um zu testen, ob die Eigenschaften oder Felder eines Ausdrucks mit geschachtelten
Mustern übereinstimmen. Eingeführt in C# 8.0.
Positionsmuster: um ein Ausdrucksergebnis zu dekonstruieren und zu testen, ob die resultierenden Werte
mit geschachtelten Mustern übereinstimmen. Eingeführt in C# 8.0.
var -Muster: um einen beliebigen Ausdruck abzugleichen und dessen Ergebnis einer deklarierten Variablen
zuzuweisen. Eingeführt in C# 7.0.
Ausschussmuster: um einen beliebigen Ausdruck abzugleichen. Eingeführt in C# 8.0.
Logische, Eigenschafts- und Positionsmuster sind rekursive Muster. Das heißt, Sie können geschachtelte Muster
enthalten.
Ein Beispiel dazu, wie diese Muster verwendet werden, um einen datengesteuerten Algorithmus zu erstellen,
finden Sie unter Tutorial: Verwenden von Musterabgleich, um typgesteuerte und datengesteuerte Algorithmen
zu erstellen.

Deklarations- und Typmuster


Sie können Deklarations- und Typmuster verwenden, um zu prüfen, ob der Laufzeittyp eines Ausdrucks mit
einem angegebenen Typ kompatibel ist. Mit einem Deklarationsmuster können Sie auch eine neue lokale
Variable deklarieren. Wenn ein Deklarationsmuster mit einem Ausdruck übereinstimmt, wird dieser Variablen
ein konvertiertes Ausdrucksergebnis zugewiesen, wie im folgenden Beispiel gezeigt:

object greeting = "Hello, World!";


if (greeting is string message)
{
Console.WriteLine(message.ToLower()); // output: hello, world!
}
Ab C# 7.0 entspricht ein Deklarationsmuster mit Typ T einem Ausdruck, wenn ein Ausdrucksergebnis nicht
NULL ist und eine der folgenden Bedingungen zutrifft:
Der Laufzeittyp eines Ausdrucksergebnisses ist T .
Der Laufzeittyp eines Ausdrucksergebnisses wird vom Typ T abgeleitet, oder er implementiert die T -
Schnittstelle, oder es gibt eine andere implizite Verweiskonvertierung von diesem Typ zu T . Das
folgende Beispiel zeigt zwei Fälle, in denen diese Bedingung whr ist:

var numbers = new int[] { 10, 20, 30 };


Console.WriteLine(GetSourceLabel(numbers)); // output: 1

var letters = new List<char> { 'a', 'b', 'c', 'd' };


Console.WriteLine(GetSourceLabel(letters)); // output: 2

static int GetSourceLabel<T>(IEnumerable<T> source) => source switch


{
Array array => 1,
ICollection<T> collection => 2,
_ => 3,
};

Im vorangehenden Beispiel entspricht das erste Muster im ersten Aufruf der GetSourceLabel -Methode
einem Argumentwert, da der Laufzeittyp int[] des Arguments vom Typ Array abgeleitet wird. Im
zweiten Aufruf der GetSourceLabel -Methode wird der Laufzeittyp List<T> des Arguments nicht vom Typ
Array abgeleitet, er implementiert aber die ICollection<T>-Schnittstelle.
Der Laufzeittyp eines Ausdrucksergebnisses ist ein Nullwerte zulassender Typ mit dem zugrunde
liegenden Typ T .
Eine Boxing- oder Unboxing-Konvertierung ist vom Laufzeittyp eines Ausdrucksergebnisses bis zum Typ
T vorhanden.

Im folgenden Beispiel werden die beiden letzten Bedingungen veranschaulicht:

int? xNullable = 7;
int y = 23;
object yBoxed = y;
if (xNullable is int a && yBoxed is int b)
{
Console.WriteLine(a + b); // output: 30
}

Wenn Sie nur den Typ eines Ausdrucks überprüfen möchten, können Sie eine Ausschussvariable _ anstelle des
Namens einer Variablen verwenden, wie im folgenden Beispiel gezeigt:
public abstract class Vehicle {}
public class Car : Vehicle {}
public class Truck : Vehicle {}

public static class TollCalculator


{
public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
{
Car _ => 2.00m,
Truck _ => 7.50m,
null => throw new ArgumentNullException(nameof(vehicle)),
_ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
};
}

Ab C# 9.0 können Sie zu diesem Zweck ein Typmuster verwenden, wie im folgenden Beispiel gezeigt:

public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch


{
Car => 2.00m,
Truck => 7.50m,
null => throw new ArgumentNullException(nameof(vehicle)),
_ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
};

Wie bei einem Deklarationsmuster stimmt ein Typmuster mit einem Ausdruck überein, wenn ein
Ausdrucksergebnis nicht NULL ist und sein Laufzeittyp eine der oben aufgeführten Bedingungen erfüllt.
Weitere Informationen finden Sie in den Abschnitten Deklarationsmuster und Typmuster der Hinweise zum
Featurevorschlag.

Konstantenmuster
Ab C# 7.0 verwenden Sie ein Konstantenmuster, um zu testen, ob ein Ausdrucksergebnis einer angegebenen
Konstante entspricht, wie im folgenden Beispiel gezeigt:

public static decimal GetGroupTicketPrice(int visitorCount) => visitorCount switch


{
1 => 12.0m,
2 => 20.0m,
3 => 27.0m,
4 => 32.0m,
0 => 0.0m,
_ => throw new ArgumentException($"Not supported number of visitors: {visitorCount}",
nameof(visitorCount)),
};

In einem Konstantenmuster können Sie einen beliebigen konstanten Ausdruck verwenden, z. B.:
ein numerisches Literal mit integralem oder Gleitkomma-Typ
ein char- oder string-Literal
ein boolescher Wert true oder false
ein enum-Wert
der Name eines deklarierten const-Felds oder lokalen const-Ausdrucks
null

Verwenden Sie ein Konstantenmuster, um auf null zu prüfen, wie im folgenden Beispiel gezeigt:
if (input is null)
{
return;
}

Der Compiler stellt sicher, dass kein vom Benutzer überladener Gleichheitsoperator == aufgerufen wird, wenn
der Ausdruck x is null ausgewertet wird.
Ab C# 9.0 können Sie ein negiertes null -Konstantenmuster verwenden, um auf nicht NULL zu prüfen, wie im
folgenden Beispiel gezeigt:

if (input is not null)


{
// ...
}

Weitere Informationen finden Sie im Abschnitt Konstantenmuster des Hinweises zum Featurevorschlag.

Relationale Muster
Ab C# 9.0 verwenden Sie ein relationales Muster, um ein Ausdrucksergebnis mit einer Konstante zu vergleichen,
wie im folgenden Beispiel gezeigt:

Console.WriteLine(Classify(13)); // output: Too high


Console.WriteLine(Classify(double.NaN)); // output: Unknown
Console.WriteLine(Classify(2.4)); // output: Acceptable

static string Classify(double measurement) => measurement switch


{
< -4.0 => "Too low",
> 10.0 => "Too high",
double.NaN => "Unknown",
_ => "Acceptable",
};

In einem relationalen Muster können Sie jeden der relationalen Operatoren < , > , <= oder >= verwenden.
Der rechte Teil eines relationalen Musters muss ein konstanter Ausdruck sein. Der Konstantenausdruck kann
einen der integralen, Gleitkomma-, char- oder enum-Typen haben.
Um zu überprüfen, ob sich ein Ausdrucksergebnis in einem bestimmten Bereich befindet, gleichen Sie es mit
einem konjunktiven and -Muster ab, wie im folgenden Beispiel gezeigt:

Console.WriteLine(GetCalendarSeason(new DateTime(2021, 3, 14))); // output: spring


Console.WriteLine(GetCalendarSeason(new DateTime(2021, 7, 19))); // output: summer
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 2, 17))); // output: winter

static string GetCalendarSeason(DateTime date) => date.Month switch


{
>= 3 and < 6 => "spring",
>= 6 and < 9 => "summer",
>= 9 and < 12 => "autumn",
12 or (>= 1 and < 3) => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};

Wenn ein Ausdrucksergebnis gleich null ist oder in einer Nullwerte zulassenden oder Unboxing-
Konvertierung nicht in den Typ einer Konstante konvertiert werden kann, entspricht ein relationales Muster
keinem Ausdruck.
Weitere Informationen finden Sie im Abschnitt Relationale Muster des Hinweises zum Featurevorschlag.

Logische Muster
Ab C# 9.0 verwenden Sie die not -, and - und or -Musterkombinatoren, um die folgenden logischen Muster zu
erstellen:
Negations- not -Muster, das mit einem Ausdruck übereinstimmt, wenn das negierte Muster nicht mit
dem Ausdruck übereinstimmt. Das folgende Beispiel zeigt, wie Sie ein konstantes null -Muster negieren
können, um zu überprüfen, ob ein Ausdruck ungleich NULL ist:

if (input is not null)


{
// ...
}

Konjunktives and -Muster, das mit einem Ausdruck übereinstimmt, wenn beide Muster mit dem
Ausdruck übereinstimmen. Im folgenden Beispiel wird gezeigt, wie Sie relationale Muster kombinieren
können, um zu überprüfen, ob ein Wert in einem bestimmten Bereich liegt:

Console.WriteLine(Classify(13)); // output: High


Console.WriteLine(Classify(-100)); // output: Too low
Console.WriteLine(Classify(5.7)); // output: Acceptable

static string Classify(double measurement) => measurement switch


{
< -40.0 => "Too low",
>= -40.0 and < 0 => "Low",
>= 0 and < 10.0 => "Acceptable",
>= 10.0 and < 20.0 => "High",
>= 20.0 => "Too high",
double.NaN => "Unknown",
};

Disjunktives or -Muster, das mit einem Ausdruck übereinstimmt, wenn eines der Muster mit dem
Ausdruck übereinstimmt, wie im folgenden Beispiel gezeigt:

Console.WriteLine(GetCalendarSeason(new DateTime(2021, 1, 19))); // output: winter


Console.WriteLine(GetCalendarSeason(new DateTime(2021, 10, 9))); // output: autumn
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 5, 11))); // output: spring

static string GetCalendarSeason(DateTime date) => date.Month switch


{
3 or 4 or 5 => "spring",
6 or 7 or 8 => "summer",
9 or 10 or 11 => "autumn",
12 or 1 or 2 => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month:
{date.Month}."),
};

Wie das vorherige Beispiel zeigt, können Sie die Musterkombinatoren wiederholt in einem Muster verwenden.
Der and -Musterkombinator hat Vorrang vor or . Verwenden Sie Klammern, um die Rangfolge explizit
anzugeben, wie im folgenden Beispiel gezeigt:
static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

NOTE
Die Reihenfolge, in der Muster überprüft werden, ist nicht definiert. Zur Laufzeit können die rechten geschachtelten
Muster von or - und and -Mustern zuerst geprüft werden.

Weitere Informationen finden Sie im Abschnitt Musterkombinatoren des Hinweises zum Featurevorschlag.

Eigenschaftsmuster
Ab C# 8.0 verwenden Sie ein Eigenschaftsmuster, um die Eigenschaften oder Felder eines Ausdrucks gegen
geschachtelte Muster abzugleichen, wie im folgenden Beispiel gezeigt:

static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };

Ein Eigenschaftsmuster stimmt mit einem Ausdruck überein, wenn ein Ausdrucksergebnis nicht NULL ist und
jedes geschachtelte Muster mit der entsprechenden Eigenschaft oder dem entsprechenden Feld des
Ausdrucksergebnisses übereinstimmt.
Sie können einem Eigenschaftsmuster auch eine Laufzeittypüberprüfung und eine Variablendeklaration
hinzufügen, wie im folgenden Beispiel gezeigt:

Console.WriteLine(TakeFive("Hello, world!")); // output: Hello


Console.WriteLine(TakeFive("Hi!")); // output: Hi!
Console.WriteLine(TakeFive(new[] { '1', '2', '3', '4', '5', '6', '7' })); // output: 12345
Console.WriteLine(TakeFive(new[] { 'a', 'b', 'c' })); // output: abc

static string TakeFive(object input) => input switch


{
string { Length: >= 5 } s => s.Substring(0, 5),
string s => s,

ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),


ICollection<char> symbols => new string(symbols.ToArray()),

null => throw new ArgumentNullException(nameof(input)),


_ => throw new ArgumentException("Not supported input type."),
};

Ein Eigenschaftsmuster ist ein rekursives Muster. Das heißt, Sie können ein beliebiges Muster als ein
geschachteltes Muster verwenden. Verwenden Sie ein Eigenschaftsmuster, um Teile von Daten gegen
geschachtelte Muster abzugleichen, wie im folgenden Beispiel gezeigt:

public record Point(int X, int Y);


public record Segment(Point Start, Point End);

static bool IsAnyEndOnXAxis(Segment segment) =>


segment is { Start: { Y: 0 } } or { End: { Y: 0 } };

Im vorherigen Beispiel werden zwei in C# 9.0 und höher verfügbare Features verwendet: or
-Musterkombinator und record-Typen.
Ab C# 10.0 können Sie auf geschachtelte Eigenschaften oder Felder innerhalb eines Eigenschaftsmusters
verweisen. Beispielsweise können Sie die Methode aus dem vorherigen Beispiel in den folgenden äquivalenten
Code umgestalten:

static bool IsAnyEndOnXAxis(Segment segment) =>


segment is { Start.Y: 0 } or { End.Y: 0 };

Weitere Informationen finden Sie im Abschnitt Eigenschaftenmuster des Hinweises zum Featurevorschlag sowie
im Hinweis zum Featurevorschlag Muster für erweiterte Eigenschaften.

Positionsmuster
Ab C# 8.0 verwenden Sie ein Positionsmuster, um ein Ausdrucksergebnis zu dekonstruieren und die
resultierenden Werte gegen die entsprechenden geschachtelten Muster abzugleichen, wie im folgenden Beispiel
gezeigt:

public readonly struct Point


{
public int X { get; }
public int Y { get; }

public Point(int x, int y) => (X, Y) = (x, y);

public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

static string Classify(Point point) => point switch


{
(0, 0) => "Origin",
(1, 0) => "positive X basis end",
(0, 1) => "positive Y basis end",
_ => "Just a point",
};

Im vorherigen Beispiel enthält der Typ eines Ausdrucks die Deconstruct-Methode, die zum Dekonstruieren eines
Ausdrucksergebnisses verwendet wird. Sie können auch Ausdrücke von Tupeltypen gegen Positionsmuster
abgleichen. Auf diese Weise können Sie mehrere Eingaben gegen verschiedene Muster abgleichen, wie im
folgenden Beispiel gezeigt:

static decimal GetGroupTicketPriceDiscount(int groupSize, DateTime visitDate)


=> (groupSize, visitDate.DayOfWeek) switch
{
(<= 0, _) => throw new ArgumentException("Group size must be positive."),
(_, DayOfWeek.Saturday or DayOfWeek.Sunday) => 0.0m,
(>= 5 and < 10, DayOfWeek.Monday) => 20.0m,
(>= 10, DayOfWeek.Monday) => 30.0m,
(>= 5 and < 10, _) => 12.0m,
(>= 10, _) => 15.0m,
_ => 0.0m,
};

Im vorherigen Beispiel werden relationale und logische Muster verwendet, die in C# 9.0 und höher verfügbar
sind.
Sie können die Namen von Tupelelementen und Deconstruct -Parametern in einem Positionsmuster verwenden,
wie im folgenden Beispiel gezeigt:
var numbers = new List<int> { 1, 2, 3 };
if (SumAndCount(numbers) is (Sum: var sum, Count: > 0))
{
Console.WriteLine($"Sum of [{string.Join(" ", numbers)}] is {sum}"); // output: Sum of [1 2 3] is 6
}

static (double Sum, int Count) SumAndCount(IEnumerable<int> numbers)


{
int sum = 0;
int count = 0;
foreach (int number in numbers)
{
sum += number;
count++;
}
return (sum, count);
}

Sie können ein Positionsmuster auch auf eine der folgenden Arten erweitern:
Fügen Sie eine Laufzeittypüberprüfung und eine Variablendeklaration hinzu, wie im folgenden Beispiel
gezeigt:

public record Point2D(int X, int Y);


public record Point3D(int X, int Y, int Z);

static string PrintIfAllCoordinatesArePositive(object point) => point switch


{
Point2D (> 0, > 0) p => p.ToString(),
Point3D (> 0, > 0, > 0) p => p.ToString(),
_ => string.Empty,
};

Im vorherigen Beispiel werden Datensätze mit Feldern fester Breite verwendet, die implizit die
Deconstruct -Methode bereitstellen.

Verwenden Sie ein Eigenschaftsmuster in einem Positionsmuster, wie im folgenden Beispiel gezeigt:

public record WeightedPoint(int X, int Y)


{
public double Weight { get; set; }
}

static bool IsInDomain(WeightedPoint point) => point is (>= 0, >= 0) { Weight: >= 0.0 };

Kombinieren Sie zwei vorangehende Verwendungen, wie im folgenden Beispiel gezeigt:

if (input is WeightedPoint (> 0, > 0) { Weight: > 0.0 } p)


{
// ..
}

Ein Positionsmuster ist ein rekursives Muster. Das heißt, Sie können ein beliebiges Muster als ein geschachteltes
Muster verwenden.
Weitere Informationen finden Sie im Abschnitt Positionsmuster des Hinweises zum Featurevorschlag.

var -Muster
Ab C# 7.0 verwenden Sie ein var -Muster, um einen beliebigen Ausdruck, einschließlich null , abzugleichen
und dessen Ergebnis einer neuen lokalen Variablen zuzuweisen, wie im folgenden Beispiel gezeigt:

static bool IsAcceptable(int id, int absLimit) =>


SimulateDataFetch(id) is var results
&& results.Min() >= -absLimit
&& results.Max() <= absLimit;

static int[] SimulateDataFetch(int id)


{
var rand = new Random();
return Enumerable
.Range(start: 0, count: 5)
.Select(s => rand.Next(minValue: -10, maxValue: 11))
.ToArray();
}

Ein var -Muster ist nützlich, wenn Sie eine temporäre Variable in einem booleschen Ausdruck benötigen, um
das Ergebnis von Zwischenberechnungen zu speichern. Sie können ein var -Muster auch verwenden, wenn Sie
zusätzliche Überprüfungen in den when -Ausdrücken eines switch -Ausdrucks oder einer switch-Anweisung
ausführen müssen, wie im folgenden Beispiel gezeigt:

public record Point(int X, int Y);

static Point Transform(Point point) => point switch


{
var (x, y) when x < y => new Point(-x, y),
var (x, y) when x > y => new Point(x, -y),
var (x, y) => new Point(x, y),
};

static void TestTransform()


{
Console.WriteLine(Transform(new Point(1, 2))); // output: Point { X = -1, Y = 2 }
Console.WriteLine(Transform(new Point(5, 2))); // output: Point { X = 5, Y = -2 }
}

Im vorherigen Beispiel ist das Muster var (x, y) gleichwertig mit einem Positionsmuster (var x, var y) .
In einem var -Muster ist der Typ einer deklarierten Variablen der Kompilierzeittyp des Ausdrucks, der gegen
das Muster abgeglichen wird.
Weitere Informationen finden Sie im Abschnitt Var-Muster des Hinweises zum Featurevorschlag.

Ausschussmuster
Ab C# 8.0 verwenden Sie ein Ausschussmuster _ , um einen beliebigen Ausdruck, einschließlich null ,
abzugleichen, wie im folgenden Beispiel gezeigt:
Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday)); // output: 5.0
Console.WriteLine(GetDiscountInPercent(null)); // output: 0.0
Console.WriteLine(GetDiscountInPercent((DayOfWeek)10)); // output: 0.0

static decimal GetDiscountInPercent(DayOfWeek? dayOfWeek) => dayOfWeek switch


{
DayOfWeek.Monday => 0.5m,
DayOfWeek.Tuesday => 12.5m,
DayOfWeek.Wednesday => 7.5m,
DayOfWeek.Thursday => 12.5m,
DayOfWeek.Friday => 5.0m,
DayOfWeek.Saturday => 2.5m,
DayOfWeek.Sunday => 2.0m,
_ => 0.0m,
};

Im vorherigen Beispiel wird ein Ausschussmuster verwendet, um null und jeden ganzzahligen Wert zu
verarbeiten, der nicht den entsprechenden Member der DayOfWeek-Enumeration hat. Dadurch wird
sichergestellt, dass ein switch -Ausdruck im Beispiel alle möglichen Eingabewerte verarbeitet. Wenn Sie kein
Ausschussmuster in einem switch -Ausdruck verwenden und keines der Muster des Ausdrucks mit einer
Eingabe übereinstimmt, löst die Runtime eine Ausnahme aus. Der Compiler generiert eine Warnung, wenn ein
switch -Ausdruck nicht alle möglichen Eingabewerte verarbeiten kann.

Ein Ausschussmuster kann kein Muster in einem is -Ausdruck oder in einer switch -Anweisung sein.
Verwenden Sie in diesen Fällen, um einen Ausdruck abzugleichen, ein var -Muster mit einem Ausschussmuster:
var _ .

Weitere Informationen finden Sie im Abschnitt Ausschussmuster des Hinweises zum Featurevorschlag.

Muster in Klammern
Ab C# 9.0 können Sie jedes Muster in Klammern setzen. In der Regel tun Sie dies, um die Rangfolge in logischen
Mustern hervorzuheben oder zu ändern, wie im folgenden Beispiel gezeigt:

if (input is not (float or double))


{
return;
}

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Hinweisen zu Featurevorschlägen:
Musterabgleich für C# 7.0
Rekursiver Musterabgleich (eingeführt in C# 8.0)
Musterabgleichänderungen für C# 9.0
Muster für erweiterte Eigenschaften (C# 10.0)

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Tutorial: Verwenden des Musterabgleichs für buildtypgesteuerte und datengesteuerte Algorithmen
Operatoren „+“ und „+=“ (C#-Referenz)
04.11.2021 • 2 minutes to read

Die Operatoren + und += werden von den integrierten numerischen integral- und floating-point-Typen sowie
den string- und delegate-Typen unterstützt.
Informationen zum arithmetischen Operator + finden Sie in den Abschnitten Unäre Plus- und
Minusoperatoren und Additionsoperator + des Artikels Arithmetische Operatoren (C#-Referenz).

Zeichenfolgenverkettung
Wenn ein Operand oder beide Operanden vom Typ String sind, verkettet der + -Operator die
Zeichenfolgendarstellungen der Operanden (die Zeichenfolgendarstellung von null is eine leere Zeichenfolge):

Console.WriteLine("Forgot" + "white space");


Console.WriteLine("Probably the oldest constant: " + Math.PI);
Console.WriteLine(null + "Nothing to add.");
// Output:
// Forgotwhite space
// Probably the oldest constant: 3.14159265358979
// Nothing to add.

Ab C# 6 bietet die Zeichenfolgeninterpolation eine benutzerfreundliche Option zum Formatieren von


Zeichenfolgen:

Console.WriteLine($"Probably the oldest constant: {Math.PI:F2}");


// Output:
// Probably the oldest constant: 3.14

Ab C# 10 können Sie die Zeichenfolgeninterpolation verwenden, um eine konstante Zeichenfolge zu


initialisieren, wenn alle für Platzhalter verwendeten Ausdrücke auch konstante Zeichenfolgen sind.

Kombinieren von Delegaten


Für Operanden des gleichen Delegattyps gibt der Operator + eine neue Delegatinstanz zurück, die bei Aufruf
den linken Operanden und dann den rechten Operanden aufruft. Wenn einer der Operanden null lautet, gibt
der + -Operator den Wert eines anderen Operanden zurück (der ggf. ebenfalls null ist). Das folgende Beispiel
zeigt, wie Delegaten mit dem + -Operator kombiniert werden können:

Action a = () => Console.Write("a");


Action b = () => Console.Write("b");
Action ab = a + b;
ab(); // output: ab

Um eine Delegatentfernung auszuführen, verwenden Sie den - -Operator.


Weitere Informationen zu Delegattypen finden Sie unter Delegaten.

Additionszuweisungsoperator (+=)
Ein Ausdruck mit dem Operator += , z.B.
x += y

für die folgende Syntax:

x = x + y

außer dass x nur einmal überprüft wird.


Im folgenden Beispiel wird die Verwendung des += -Operators veranschaulicht:

int i = 5;
i += 9;
Console.WriteLine(i);
// Output: 14

string story = "Start. ";


story += "End.";
Console.WriteLine(story);
// Output: Start. End.

Action printer = () => Console.Write("a");


printer(); // output: a

Console.WriteLine();
printer += () => Console.Write("b");
printer(); // output: ab

Sie verwenden den += -Operator auch, um eine Ereignishandlermethode anzugeben, wenn Sie ein Ereignis
abonnieren. Weitere Informationen finden Sie unter Vorgehensweise: Abonnieren von Ereignissen und
Kündigen von Ereignisabonnements.

Operatorüberladbarkeit
Ein benutzerdefinierter Typ kann den Operator + überladen. Wenn ein binärer Operator vom Typ + überladen
wird, wird der Operator += implizit ebenfalls überladen. Ein benutzerdefinierter Typ kann den Operator +=
nicht explizit überladen.

C#-Sprachspezifikation
Weitere Informationen finden Sie unter C#-Sprachspezifikation in den Abschnitten Unärer Plusoperator und
Additionsoperator.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Verketten mehrerer Zeichenfolgen
Ereignisse
Arithmetic operators (Arithmetische Operatoren)
Operatoren „-“ und „-=“ (C#-Referenz)
Operatoren „-“ und -=“ (C#-Referenz)
04.11.2021 • 3 minutes to read

Die Operatoren - und -= werden von den integrierten numerischen integral- und floating-point-Typen sowie
delegate-Typen unterstützt.
Informationen zum arithmetischen Operator - finden Sie in den Abschnitten Unäre Plus- und
Minusoperatoren und Subtraktionsoperator - des Artikels Arithmetische Operatoren (C#-Referenz).

Delegatentfernung
Für Operanden des gleichen Delegattyps gibt der Operator - eine wie folgt berechnete Delegatinstanz zurück:
Wenn beide Operanden nicht NULL sind und es sich bei der Aufrufliste des rechten Operanden um eine
ordnungsgemäße zusammenhängende Unterliste der Aufrufliste des linken Operanden handelt, entsteht
durch den Vorgang eine neue Aufrufliste, bei der die Einträge des rechten Operanden aus der Aufrufliste
des linken Operanden entfernt wurden. Wenn die Liste des rechten Operanden mehreren
zusammenhängenden Unterlisten aus der Liste des linken Operanden entspricht, wird nur die äußerst
rechte übereinstimmende Unterliste entfernt. Sollte durch die Entfernung eine leere Liste entstehen, ist
das Ergebnis null .

Action a = () => Console.Write("a");


Action b = () => Console.Write("b");

var abbaab = a + b + b + a + a + b;
abbaab(); // output: abbaab
Console.WriteLine();

var ab = a + b;
var abba = abbaab - ab;
abba(); // output: abba
Console.WriteLine();

var nihil = abbaab - abbaab;


Console.WriteLine(nihil is null); // output: True

Wenn es sich bei der Aufrufliste des rechten Operanden nicht um eine ordnungsgemäße
zusammenhängende Unterliste der Aufrufliste des linken Operanden handelt, ist das Ergebnis des
Vorgangs der linke Operand. Beim Entfernen eines Delegaten, der nicht Teil des Multicastdelegaten ist,
passiert nichts, und der Multicastdelegat bleibt unverändert.
Action a = () => Console.Write("a");
Action b = () => Console.Write("b");

var abbaab = a + b + b + a + a + b;
var aba = a + b + a;

var first = abbaab - aba;


first(); // output: abbaab
Console.WriteLine();
Console.WriteLine(object.ReferenceEquals(abbaab, first)); // output: True

Action a2 = () => Console.Write("a");


var changed = aba - a;
changed(); // output: ab
Console.WriteLine();
var unchanged = aba - a2;
unchanged(); // output: aba
Console.WriteLine();
Console.WriteLine(object.ReferenceEquals(aba, unchanged)); // output: True

Das vorherige Beispiel veranschaulicht auch, dass Delegatinstanzen beim Entfernen von Delegaten
verglichen werden. Delegaten, die durch die Auswertung identischer Lambdaausdrücke erzeugt werden,
sind beispielsweise nicht gleich. Weitere Informationen über die Delegatgleichheit finden Sie in der C#-
Sprachspezifikation unter Delegieren von Gleichheitsoperatoren.
Ist der linke Operand null , ist das Ergebnis des Vorgangs null . Ist der rechte Operand null , ist das
Ergebnis des Vorgangs der linke Operand.

Action a = () => Console.Write("a");

var nothing = null - a;


Console.WriteLine(nothing is null); // output: True

var first = a - null;


a(); // output: a
Console.WriteLine();
Console.WriteLine(object.ReferenceEquals(first, a)); // output: True

Verwenden Sie zum Kombinieren von Delegaten den + -Operator.


Weitere Informationen zu Delegattypen finden Sie unter Delegaten.

Subtraktionszuweisungsoperator „-=“
Ein Ausdruck mit dem Operator -= , z.B.

x -= y

für die folgende Syntax:

x = x - y

außer dass x nur einmal überprüft wird.


Im folgenden Beispiel wird die Verwendung des -= -Operators veranschaulicht:
int i = 5;
i -= 9;
Console.WriteLine(i);
// Output: -4

Action a = () => Console.Write("a");


Action b = () => Console.Write("b");
var printer = a + b + a;
printer(); // output: aba

Console.WriteLine();
printer -= a;
printer(); // output: ab

Mit dem Operator -= können Sie auch eine Ereignishandlermethode zum Entfernen angeben, wenn Sie das
Abonnement eines Ereignisses kündigen. Weitere Informationen finden Sie unter Abonnieren von Ereignissen
und Kündigen von Ereignisabonnements.

Operatorüberladbarkeit
Ein benutzerdefinierter Typ kann den Operator - überladen. Wenn ein binärer Operator vom Typ - überladen
wird, wird der Operator -= implizit ebenfalls überladen. Ein benutzerdefinierter Typ kann den Operator -=
nicht explizit überladen.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den Abschnitten Unärer Minusoperator und Subtraktionsoperator der C#-
Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Ereignisse
Arithmetic operators (Arithmetische Operatoren)
Operatoren „+“ und „+=“ (C#-Referenz)
Operator „?“ (C#-Referenz)
04.11.2021 • 3 minutes to read

Der bedingte Operator ?: , der auch als ternärer bedingter Operator bekannt ist, wertet einen booleschen
Ausdruck aus und gibt das Ergebnis für einen der zwei Ausdrücke zurück, abhängig davon, ob der boolesche
Ausdruck true oder false ergibt. Das folgende Beispiel stellt dies dar:

string GetWeatherDisplay(double tempInCelsius) => tempInCelsius < 20.0 ? "Cold." : "Perfect!";

Console.WriteLine(GetWeatherDisplay(15)); // output: Cold.


Console.WriteLine(GetWeatherDisplay(27)); // output: Perfect!

Wie das vorherige Beispiel zeigt, lautet die Syntax für den bedingten Operator wie folgt:

condition ? consequent : alternative

Der condition-Ausdruck muss als true oder false ausgewertet werden. Wenn condition``true ergibt, wird
der consequent -Ausdruck ausgewertet, und das Ergebnis ist das Ergebnis des Vorgangs. Wenn
condition``false ergibt, wird der alternative -Ausdruck ausgewertet, und das Ergebnis ist das Ergebnis des
Vorgangs. Nur consequent oder alternative wird ausgewertet.
Ab C# 9.0 weisen bedingte Ausdrücke das Typ des Ziels auf. Wenn der Zieltyp eines bedingten Ausdrucks also
bekannt ist, müssen die Typen von consequent und alternative implizit in den Zieltyp konvertierbar sein, wie
im folgenden Beispiel gezeigt wird:

var rand = new Random();


var condition = rand.NextDouble() > 0.5;

int? x = condition ? 12 : null;

IEnumerable<int> xs = x is null ? new List<int>() { 0, 1 } : new int[] { 2, 3 };

Wenn der Zieltyp eines bedingten Ausdrucks nicht bekannt ist, z. B. wenn Sie das var -Schlüsselwort nutzen
oder in C# 8.0 oder älteren Versionen, muss der Typ von consequent und alternative identisch sein, oder es
muss eine implizite Konvertierung von einem Typ in den anderen geben:

var rand = new Random();


var condition = rand.NextDouble() > 0.5;

var x = condition ? 12 : (int?)null;

Der bedingte Operator ist rechtsassoziativ, d.h. ein Ausdruck der Form

a ? b : c ? d : e

wird als ausgewertet,

a ? b : (c ? d : e)
TIP
Sie können sich anhand der folgenden Gedächtnisstütze merken, wie der bedingte Operator ausgewertet wird:

is this condition true ? yes : no

Bedingter ref-Ausdruck
Ab C# 7.2 kann eine lokale ref-Variable oder eine schreibgeschützte lokale ref-Variable mit dem bedingten ref-
Ausdruck bedingt zugewiesen werden. Sie können einen bedingten ref-Ausdruck auch als Verweisrückgabewert
oder als ref -Methodenargument verwenden.
Die Syntax für den bedingten ref-Ausdruck lautet folgendermaßen:

condition ? ref consequent : ref alternative

Wie der ursprüngliche bedingte Operator wertet der bedingte ref-Ausdruck nur einen von zwei Ausdrücken aus:
entweder consequent oder alternative .
Im Fall des bedingten ref-Ausdrucks muss der Typ von consequent und alternative identisch sein. Bedingte
ref-Ausdrücke weisen nicht den Typ des Ziels auf.
Im folgenden Beispiel wird die Verwendung des bedingten ref-Ausdrucks veranschaulicht:

var smallArray = new int[] { 1, 2, 3, 4, 5 };


var largeArray = new int[] { 10, 20, 30, 40, 50 };

int index = 7;
ref int refValue = ref ((index < 5) ? ref smallArray[index] : ref largeArray[index - 5]);
refValue = 0;

index = 2;
((index < 5) ? ref smallArray[index] : ref largeArray[index - 5]) = 100;

Console.WriteLine(string.Join(" ", smallArray));


Console.WriteLine(string.Join(" ", largeArray));
// Output:
// 1 2 100 4 5
// 10 20 0 40 50

Bedingter Operator und eine if -Anweisung


Die Verwendung des bedingten Operators anstelle einer if -Anweisung führt in Fällen, in denen Sie einen Wert
bedingt berechnen müssen, möglicherweise zu präziserem Code. Das folgende Beispiel zeigt zwei
Möglichkeiten, eine ganze Zahl als negativ oder nicht negativ zu klassifizieren:
int input = new Random().Next(-5, 5);

string classify;
if (input >= 0)
{
classify = "nonnegative";
}
else
{
classify = "negative";
}

classify = (input >= 0) ? "nonnegative" : "negative";

Operatorüberladbarkeit
Ein benutzerdefinierter Typ kann den bedingten Operator nicht überladen.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Bedingter Operator der C#-Sprachspezifikation.
Weitere Informationen zu in C# 7.2 und höher eingeführten Features finden Sie in den folgenden
Featurevorschlägen:
Bedingter ref-Ausdruck (C# 7.2)
Bedingter Ausdruck mit Zieltyp (C# 9.0)

Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
if-Anweisung
?.- und ?[]-Operatoren
??- und ??=-Operatoren
ref (C#-Referenz)
! NULL-toleranter Operator (C#-Referenz)
04.11.2021 • 2 minutes to read

Der unäre Postfix-Operator ! (der NULL-tolerante Operator) ist in C# 8.0 und höher verfügbar. In einem
aktivierten Nullable-Anmerkungskontext verwenden Sie den NULL-toleranten Operator, um zu deklarieren, dass
der Ausdruck x eines Verweistyps nicht null ist: x! . Der unäre Präfixoperator ! ist der Operator für
logische Negation.
Der NULL-tolerante Operator besitzt zur Laufzeit keine Auswirkungen. Er wirkt sich nur auf die statische
Flussanalyse des Compilers aus, indem der NULL-Status des Ausdrucks geändert wird. Zur Laufzeit wird
Ausdruck x! in das Ergebnis des zugrunde liegenden Ausdrucks x ausgewertet.
Weitere Informationen zum Feature „Nullable-Verweistypen“ finden Sie unter Nullable-Verweistypen.

Beispiele
Einer der Anwendungsfälle des NULL-toleranten Operators besteht darin, die Argumentvalidierungslogik zu
testen. Betrachten Sie beispielsweise die folgende Klasse:

#nullable enable
public class Person
{
public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));

public string Name { get; }


}

Mit dem MSTest-Testframework können Sie den folgenden Test für die Validierungslogik im Konstruktor
erstellen:

[TestMethod, ExpectedException(typeof(ArgumentNullException))]
public void NullNameShouldThrowTest()
{
var person = new Person(null!);
}

Ohne den NULL-toleranten Operator generiert der Compiler die folgende Warnung für den oben gezeigten
Code: Warning CS8625: Cannot convert null literal to non-nullable reference type . Durch die Verwendung des
NULL-toleranten Operators informieren Sie den Compiler darüber, dass die Übergabe von null erwartet wird
und keine Warnung erfolgen sollte.
Sie können den NULL-toleranten Operator auch verwenden, wenn Sie definitiv wissen, dass ein Ausdruck nicht
null sein kann, der Compiler aber nicht in der Lage ist, dies zu erkennen. Wenn die IsValid -Methode im
folgenden Beispiel true zurückgibt, ist das Argument nicht null , und Sie können es sicher dereferenzieren:
public static void Main()
{
Person? p = Find("John");
if (IsValid(p))
{
Console.WriteLine($"Found {p!.Name}");
}
}

public static bool IsValid(Person? person)


=> person is not null && person.Name is not null;

Ohne den NULL-toleranten Operator generiert der Compiler die folgende Warnung für den p.Name -Code:
Warning CS8602: Dereference of a possibly null reference .

Wenn Sie die IsValid -Methode ändern können, können Sie das NotNullWhen-Attribut verwenden, um den
Compiler darüber zu informieren, dass ein Argument der IsValid -Methode nicht null sein kann, wenn die
Methode true zurückgibt:

public static void Main()


{
Person? p = Find("John");
if (IsValid(p))
{
Console.WriteLine($"Found {p.Name}");
}
}

public static bool IsValid([NotNullWhen(true)] Person? person)


=> person is not null && person.Name is not null;

Im vorherigen Beispiel müssen Sie den NULL-toleranten Operator nicht verwenden, da der Compiler über
ausreichende Informationen verfügt, um zu ermitteln, dass p innerhalb der if -Anweisung nicht null sein
kann. Weitere Informationen zu den Attributen, mit denen Sie zusätzliche Informationen zum NULL-Status einer
Variablen bereitstellen können, finden Sie unter Aktualisieren von APIs mit Attributen zum Definieren von NULL-
Erwartungen.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Der NULL-tolerante Operator des Entwurfs der Spezifikation von
Nullable-Verweistypen.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Tutorial: Entwerfen mit Nullable-Verweistypen
?? und ??= (Operatoren) – C#-Referenz
04.11.2021 • 2 minutes to read

Der NULL-Zusammenfügungsoperator ?? gibt den Wert des linken Operanden zurück, wenn dieser nicht
null ist. Andernfalls wertet der Operator den rechten Operanden aus und gibt dessen Ergebnis zurück. Der
?? -Operator wertet seinen rechten Operanden nicht aus, wenn der linke Operand auf einen Wert ungleich
NULL ausgewertet wird.
Ab C# 8.0 können Sie den NULL-Sammelzuweisungsoperator ??= verwenden, um den Wert des rechten
Operanden dem linken Operanden nur dann zuzuweisen, wenn die Auswertung des linken Operanden null
ergibt. Der ??= -Operator wertet seinen rechten Operanden nicht aus, wenn der linke Operand auf einen Wert
ungleich NULL ausgewertet wird.

List<int> numbers = null;


int? a = null;

(numbers ??= new List<int>()).Add(5);


Console.WriteLine(string.Join(" ", numbers)); // output: 5

numbers.Add(a ??= 0);


Console.WriteLine(string.Join(" ", numbers)); // output: 5 0
Console.WriteLine(a); // output: 0

Der linke Operand des ??= -Operators muss eine Variable, eine Eigenschaft oder ein Indexer-Element sein.
In C# 7.3 und früheren Versionen muss der Typ des linken Operanden des ?? -Operators entweder ein
Verweistyp oder ein Nullable-Werttyp sein. Ab C# 8.0 wird diese Anforderung durch Folgendes ersetzt: der Typ
des linken Operanden der Operatoren ?? und ??= kann kein Werttyp sein, der nicht auf NULL festgelegt
werden kann. Das heißt, Sie können ab C# 8.0 die NULL-Sammeloperatoren mit uneingeschränkten
Typparametern verwenden:

private static void Display<T>(T a, T backup)


{
Console.WriteLine(a ?? backup);
}

Die NULL-Sammeloperatoren sind rechtsassoziativ. Das heißt, Ausdrücke wie

a ?? b ?? c
d ??= e ??= f

werden wie folgt ausgewertet

a ?? (b ?? c)
d ??= (e ??= f)

Beispiele
Die Operatoren ?? und ??= können in folgenden Szenarios nützlich sein:
In Ausdrücken mit den NULL-bedingten Operatoren „?.“ und „?[]“ können Sie den ?? -Operator
verwenden, um einen alternativen Ausdruck zum Auswerten für den Fall bereitzustellen, dass das
Ergebnis des NULL-bedingten Vorgangs null ist:

double SumNumbers(List<double[]> setsOfNumbers, int indexOfSetToSum)


{
return setsOfNumbers?[indexOfSetToSum]?.Sum() ?? double.NaN;
}

var sum = SumNumbers(null, 0);


Console.WriteLine(sum); // output: NaN

Wenn Sie mit Nullable-Werttypen arbeiten und den Wert eines zugrunde liegenden Werttyps
bereitstellen müssen, verwenden Sie den ?? -Operator, um den Wert für den Fall anzugeben, dass der
Wert eines Nullable-Typs null ist:

int? a = null;
int b = a ?? -1;
Console.WriteLine(b); // output: -1

Verwenden Sie die Nullable<T>.GetValueOrDefault()-Methode, wenn der Wert, der verwenden werden
soll, falls der Wert des Nullable-Typs null lautet, der Standardwert des zugrunde liegenden Werttyps
sein soll.
Ab C# 7.0 können Sie einen throw Ausdruck als rechten Operanden des ?? -Operators verwenden, um
den Code für die Überprüfung der Argumente präziser zu fassen:

public string Name


{
get => name;
set => name = value ?? throw new ArgumentNullException(nameof(value), "Name cannot be null");
}

Das oben stehende Beispiel veranschaulicht auch, wie Sie Ausdruckskörpermember verwenden, um eine
Eigenschaft zu definieren.
Ab C# 8.0 können Sie mit dem ??= -Operator den Code

if (variable is null)
{
variable = expression;
}

durch den folgenden Code:

variable ??= expression;

Operatorüberladbarkeit
Die Operatoren ?? und ??= können nicht überladen werden.

C#-Sprachspezifikation
Weitere Informationen über den ?? -Operator finden Sie im Abschnitt NULL-Sammeloperatoren der C#-
Sprachspezifikation.
Weitere Informationen zum ??= -Operator finden Sie unter Hinweis zum Featurevorschlag.

Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
?.- und ?[]-Operatoren
?:-Operator
Operator „=>-“ (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Token => wird auf zwei Weisen unterstützt: als Lambdaoperator und als Trennzeichen eines
Membernamens und der Memberimplementierung in der Definition eines Ausdruckskörpers.

Lambdaoperator
In Lambdaausdrücken trennt der Lambdaoperator => die Eingabeparameter auf der linken Seite vom
Lambdakörper auf der rechten Seite.
Im folgenden Beispiel wird das LINQ-Feature mit der Methodensyntax verwendet, um die Verwendung von
Lambdaausdrücken zu veranschaulichen:

string[] words = { "bot", "apple", "apricot" };


int minimalLength = words
.Where(w => w.StartsWith("a"))
.Min(w => w.Length);
Console.WriteLine(minimalLength); // output: 5

int[] numbers = { 4, 7, 10 };
int product = numbers.Aggregate(1, (interim, next) => interim * next);
Console.WriteLine(product); // output: 280

Eingabeparameter eines Lambdaausdrucks sind zur Kompilierzeit stark typisiert. Wenn der Compiler die Typen
von Eingabeparametern wie im obigen Beispiel ableiten kann, können Sie die Typdeklarationen weglassen.
Wenn Sie den Typ von Eingabeparametern festlegen müssen, müssen Sie ihn wie im folgenden Beispiel gezeigt
für jeden Parameter festlegen:

int[] numbers = { 4, 7, 10 };
int product = numbers.Aggregate(1, (int interim, int next) => interim * next);
Console.WriteLine(product); // output: 280

Im folgenden Beispiel wird gezeigt, wie ein Lambdaausdruck ohne Eingabeparameter definiert wird:

Func<string> greet = () => "Hello, World!";


Console.WriteLine(greet());

Weitere Informationen finden Sie unter Lambdaausdrücke.

Ausdruckskörperdefinition
Eine Ausdruckstextdefinition hat die folgende allgemeine Syntax:

member => expression;

Dabei ist expression ein gültiger Ausdruck. Der Rückgabetyp von expression muss implizit in den
Rückgabetyp des Members konvertiert werden können. Wenn der Rückgabetyp des Members void ist oder
der Member ein Konstruktor, Finalizer oder ein Eigenschaften- oder Indexer- set -Accessor ist, muss expression
ein Anweisungsausdruck sein. Da das Ergebnis des Ausdrucks verworfen wird, kann der Rückgabetyp dieses
Ausdrucks ein beliebiger Typ sein.
Im folgenden Beispiel wird eine Ausdruckskörperdefinition für eine Person.ToString -Methode angegeben:

public override string ToString() => $"{fname} {lname}".Trim();

Diese ist eine kompakte Version der folgenden Methodendefinition:

public override string ToString()


{
return $"{fname} {lname}".Trim();
}

Ausdruckskörperdefinitionen für Methoden, Operatoren und schreibgeschützte Eigenschaften werden ab C# 6


unterstützt. Ausdruckskörperdefinitionen für Konstruktoren, Finalizer sowie Eigenschaften- und
Indexeraccessors werden ab C# 7.0 unterstützt.
Weitere Informationen finden Sie unter Ausdruckskörpermember.

Operatorüberladbarkeit
Operator => kann nicht überladen werden.

C#-Sprachspezifikation
Weitere Informationen zum Lambdaoperator finden Sie im Abschnitt Anonyme Funktionsausdrücke der C#-
Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Operator „::“ (C#-Referenz)
04.11.2021 • 2 minutes to read

Verwenden Sie den Namespacealias-Qualifizierer :: , um auf einen Member eines Namespace mit Alias
zuzugreifen. Sie können den :: -Qualifizierer nur zwischen zwei Bezeichnern verwenden. Der linke Bezeichner
kann einer der folgenden Aliase sein:
Ein mit einer using alias-Anweisung erstellter Namespacealias:

using forwinforms = System.Drawing;


using forwpf = System.Windows;

public class Converters


{
public static forwpf::Point Convert(forwinforms::Point point) => new forwpf::Point(point.X,
point.Y);
}

Ein externer Alias.


Der global -Alias, ein globaler Namespacealias. Der globale Namespace ist der Namespace, der
Namespaces und Typen enthält, die nicht in einem benannten Namespace deklariert werden. Bei
Verwendung mit dem :: -Qualifizierer verweist der global -Alias immer auf den globalen Namespace,
auch wenn ein benutzerdefinierter global Namespacealias vorhanden ist.
Im folgenden Beispiel wird der global -Alias verwendet, um auf den .NET-Namespace System
zuzugreifen, der ein Member des globalen Namespace ist. Ohne den global -Alias würde auf den
benutzerdefinierten System -Namespace, der ein Member des MyCompany.MyProduct -Namespace ist,
zugegriffen:

namespace MyCompany.MyProduct.System
{
class Program
{
static void Main() => global::System.Console.WriteLine("Using global alias");
}

class Console
{
string Suggestion => "Consider renaming this class";
}
}

NOTE
Das Schlüsselwort global ist nur dann der globale Namespacealias, wenn es sich um den linken Bezeichner des
:: -Qualifizierers handelt.

Sie können auch das . -Token verwenden, um auf einen Member eines Namespace mit Alias zuzugreifen.
Allerdings wird das . -Token auch verwendet, um auf einen Typmember zuzugreifen. Der Qualifizierer :: stellt
sicher, dass sein linker Bezeichner immer auf einen Namespacealias verweist, selbst wenn ein Typ oder ein
Namespace mit demselben Namen vorhanden ist.
C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Namespacealias-Qualifizierer der C#-Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Der Operator „await“ (C#-Referenz)
04.11.2021 • 3 minutes to read

Der Operator await hält die Auswertung der einschließenden async-Methode an, bis der asynchrone Vorgang
abgeschlossen ist, der durch seinen Operanden dargestellt wird. Sobald der asynchrone Vorgang abgeschlossen
ist, gibt der Operator await ggf. das Ergebnis des Vorgangs zurück. Wenn der Operator await auf den
Operanden angewendet wird, der einen bereits abgeschlossenen Vorgang darstellt, wird das Ergebnis des
Vorgangs sofort zurückgegeben, ohne dass die einschließende Methode angehalten wird. Der Operator await
blockiert nicht den Thread, der die Async-Methode auswertet. Wenn der Operator await die einschließende
asynchrone Methode anhält, wird das Steuerelement an den Aufrufer der Methode zurückgegeben.
Im folgenden Beispiel gibt die Methode HttpClient.GetByteArrayAsync die Instanz Task<byte[]> zurück, die
einen asynchronen Vorgang darstellt, der ein Bytearray erzeugt, wenn er abgeschlossen wird. Der Operator
await hält so lange die Methode DownloadDocsMainPageAsync an, bis der Vorgang abgeschlossen ist. Wenn
DownloadDocsMainPageAsync angehalten wird, wird die Steuerung an die Methode Main zurückgegeben. Bei
dieser handelt es sich um den Aufrufer von DownloadDocsMainPageAsync . Die Methode Main wird so lange
ausgeführt, bis sie das Ergebnis des asynchronen Vorgangs benötigt, der von der Methode
DownloadDocsMainPageAsync ausgeführt wird. Wenn GetByteArrayAsync alle Bytes abruft, wird der Rest der
Methode DownloadDocsMainPageAsync ausgewertet. Danach wird der Rest der Methode Main ausgewertet.

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class AwaitOperator


{
public static async Task Main()
{
Task<int> downloading = DownloadDocsMainPageAsync();
Console.WriteLine($"{nameof(Main)}: Launched downloading.");

int bytesLoaded = await downloading;


Console.WriteLine($"{nameof(Main)}: Downloaded {bytesLoaded} bytes.");
}

private static async Task<int> DownloadDocsMainPageAsync()


{
Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: About to start downloading.");

var client = new HttpClient();


byte[] content = await client.GetByteArrayAsync("https://docs.microsoft.com/en-us/");

Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: Finished downloading.");


return content.Length;
}
}
// Output similar to:
// DownloadDocsMainPageAsync: About to start downloading.
// Main: Launched downloading.
// DownloadDocsMainPageAsync: Finished downloading.
// Main: Downloaded 27700 bytes.

Im vorangehenden Beispiel wird die asynchrone Main -Methode verwendet. Dies ist ab C# 7.1 möglich. Weitere
Informationen finden Sie im Abschnitt Der Operator „await“ in der Methode „Main“.
NOTE
Eine Einführung in die asynchrone Programmierung finden Sie unter Asynchrone Programmierung mit „async“ und
„await“. Die asynchrone Programmierung mit async und await folgt dem aufgabenbasierten asynchronen Muster.

Der Operator await kann nur in einer Methode, einem Lambdaausdruck oder einer anonymen Methode
verwendet werden, die von dem Schlüsselwort async geändert wird. Innerhalb einer Async-Methode können Sie
den Operator await nicht im Text einer synchronen Funktion, innerhalb des Blocks einer Lock-Anweisung oder
in einem unsicheren Kontext verwenden.
Der Operand des Operators await gehört normalerweise einem der folgenden .NET-Typen an: Task,
Task<TResult>, ValueTask oder ValueTask<TResult>. Allerdings kann es sich bei jedem Awaitable-Ausdruck um
den Operanden des Operators await handeln. Weitere Informationen finden Sie im Abschnitt Awaitable-
Ausdrücke der C#-Sprachspezifikation.
Der Ausdruck await t ist vom Typ TResult , wenn der Ausdruck t vom Typ Task<TResult> oder
ValueTask<TResult> ist. Wenn der Ausdruck t vom Typ Task oder ValueTask ist, ist await t vom Typ void . In
beiden Fällen löst await t die Ausnahme erneut aus, wenn t eine Ausnahme auslöst. Weitere Informationen
zur Bearbeitung von Ausnahmen finden Sie im Abschnitt Ausnahmen in Async-Methoden des Artikels Try-catch-
Anweisung.
Die Schlüsselwörter async und await sind in C# 5 und höher verfügbar.

Asynchrone Datenströme und verwerfbare Objekte


Ab C# 8.0 können Sie asynchrone Datenströme und verwerfbare Elemente verwenden.
Sie können die await foreach -Anweisung verwenden, um einen asynchronen Datenstrom zu verarbeiten.
Weitere Informationen finden Sie im Abschnitt foreach -Anweisung des Artikels Iterationsanweisungen und im
Abschnitt Asynchrone Streams des Artikels Neues in C# 8.0.
Sie können die await using -Anweisung nutzen, um ein asynchron verwerfbares Objekt zu nutzen, d. h. ein
Objekt eines Typs, der eine IAsyncDisposable-Schnittstelle implementiert. Weitere Informationen erhalten Sie im
Abschnitt Verwenden von asynchron verwerfbar des Artikels Implementieren einer DisposeAsync-Methode.

Der Operator „await“ in der Methode „Main“


Ab C# 7.1 kann die Methode Main , die den Einstiegspunkt der Anwendung darstellt, Task oder Task<int>
zurückgeben. Deshalb muss es sich um eine asynchrone Methode handeln, damit Sie den Operator await im
Text verwenden können. In früheren C#-Versionen können Sie den Wert der Eigenschaft Task<TResult>.Result
der Instanz Task<TResult> abrufen, die von der entsprechenden Async-Methode zurückgegeben wird, um
sicherzustellen, dass die Methode Main darauf wartet, dass ein asynchroner Vorgang abgeschlossen wird. Für
asynchrone Vorgänge, für die kein Wert zurückgegeben wird, können Sie die Methode Task.Wait aufrufen.
Informationen zum Auswählen der Sprachversion finden Sie unter Auswählen der C#-Sprachversion.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Await-Ausdrücke der C#-Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
async
Aufgabenbasiertes asynchrones Programmiermodell
Asynchrone Programmierung
Async ausführlich
Exemplarische Vorgehensweise: Zugreifen auf das Web mit „async“ und „await“
Tutorial: Generieren und Nutzen asynchroner Datenströme mit C# 8.0 und .NET Core 3.0
Standardwertausdrücke (C#-Referenz)
04.11.2021 • 2 minutes to read

Ein Standardwertausdruck erzeugt den Standardwert für einen Typ. Es gibt zwei Arten von
Standardwertausdrücken: den Aufruf des default-Operators und ein default-Literal.
Außerdem verwenden Sie das Schlüsselwort default in einer switch -Anweisung als case-
Standardbezeichnung.

default-Operator
Das Argument für den default -Operator muss der Name eines Typs oder Typparameters sein, wie das
folgende Beispiel zeigt:

Console.WriteLine(default(int)); // output: 0
Console.WriteLine(default(object) is null); // output: True

void DisplayDefaultOf<T>()
{
var val = default(T);
Console.WriteLine($"Default value of {typeof(T)} is {(val == null ? "null" : val.ToString())}.");
}

DisplayDefaultOf<int?>();
DisplayDefaultOf<System.Numerics.Complex>();
DisplayDefaultOf<System.Collections.Generic.List<int>>();
// Output:
// Default value of System.Nullable`1[System.Int32] is null.
// Default value of System.Numerics.Complex is (0, 0).
// Default value of System.Collections.Generic.List`1[System.Int32] is null.

default-Literal
Ab C# 7.1 können Sie das default -Literal verwenden, um den Standardwert eines Typs zu erzeugen, wenn der
Compiler den Ausdruckstyp ableiten kann. Der default -Literalausdruck erzeugt den gleichen Wert wie der
default(T) -Ausdruck, wobei T der abgeleitete Typ ist. Sie können das default -Literal in den folgenden Fälle
verwenden:
Bei der Zuweisung oder Initialisierung einer Variablen.
Bei der Deklaration des Standardwerts eines optionalen Methodenparameters
Bei einem Methodenaufruf zum Bereitstellen eines Argumentwerts.
In einer return -Anweisung oder als Ausdruck in einem Ausdruckskörpermember
Im folgenden Beispiel wird die Verwendung des default -Literals veranschaulicht:
T[] InitializeArray<T>(int length, T initialValue = default)
{
if (length < 0)
{
throw new ArgumentOutOfRangeException(nameof(length), "Array length must be nonnegative.");
}

var array = new T[length];


for (var i = 0; i < length; i++)
{
array[i] = initialValue;
}
return array;
}

void Display<T>(T[] values) => Console.WriteLine($"[ {string.Join(", ", values)} ]");

Display(InitializeArray<int>(3)); // output: [ 0, 0, 0 ]
Display(InitializeArray<bool>(4, default)); // output: [ False, False, False, False ]

System.Numerics.Complex fillValue = default;


Display(InitializeArray(3, fillValue)); // output: [ (0, 0), (0, 0), (0, 0) ]

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Ausdrücke mit Standardwert der C#-Sprachspezifikation.
Weitere Informationen zum default -Literal finden Sie unter Hinweis zum Featurevorschlag.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Standardwerte der C#-Typen
Generics in .NET
Operator „delegate“ (C#-Referenz)
04.11.2021 • 2 minutes to read

Der Operator delegate erstellt eine anonyme Methode, die in einen Delegattyp konvertiert werden kann:

Func<int, int, int> sum = delegate (int a, int b) { return a + b; };


Console.WriteLine(sum(3, 4)); // output: 7

NOTE
Ab C# 3 bieten Lambdaausdrücke eine präzisere und aussagekräftigere Möglichkeit zum Erstellen anonymer Funktionen.
Verwenden Sie den Operator „=>“, um einen Lambdaausdruck zu erstellen:

Func<int, int, int> sum = (a, b) => a + b;


Console.WriteLine(sum(3, 4)); // output: 7

Weitere Informationen zu Features von Lambdaausdrücken (etwa zum Erfassen äußerer Variablen) finden Sie unter
Lambdaausdrücke.

Bei Verwendung des Operators delegate können Sie die Parameterliste weglassen. Die erstellte anonyme
Methode kann dann mit einer beliebigen Parameterliste in einen Delegattyp konvertiert werden, wie im
folgenden Beispiel zu sehen:

Action greet = delegate { Console.WriteLine("Hello!"); };


greet();

Action<int, double> introduce = delegate { Console.WriteLine("This is world!"); };


introduce(42, 2.7);

// Output:
// Hello!
// This is world!

Dies ist die einzige Funktion anonymer Methoden, die von Lambdaausdrücken nicht unterstützt wird. In allen
anderen Fällen ist ein Lambdaausdruck eine bevorzugte Methode zum Schreiben von Inlinecode.
Ab C# 9.0 können Sie discards verwenden, um mindestens zwei Eingabeparameter einer anonymen Methode
anzugeben, die nicht von der Methode verwendet werden:

Func<int, int, int> constant = delegate (int _, int _) { return 42; };


Console.WriteLine(constant(3, 4)); // output: 42

Aus Gründen der Abwärtskompatibilität wird _ als Name dieses Parameters innerhalb einer anonymen
Methode behandelt, wenn nur ein einzelner Parameter den Namen _ aufweist.
Ab C# 9.0 können Sie außerdem den static -Modifizierer bei der Deklaration einer anonymen Methode
verwenden:
Func<int, int, int> sum = static delegate (int a, int b) { return a + b; };
Console.WriteLine(sum(10, 4)); // output: 14

Eine statische anonyme Methode kann keine lokalen Variablen oder den Instanzstatus aus einschließenden
Bereichen erfassen.
Zum Deklarieren eines Delegattyps wird auch das Schlüsselwort delegate verwendet.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Anonyme Funktionsausdrücke der C#-Sprachspezifikation.

Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
=>-Operator
is-Operator (C#-Referenz)
04.11.2021 • 2 minutes to read

Der is -Operator überprüft, ob das Ergebnis eines Ausdrucks mit einem bestimmten Typ kompatibel ist.
Informationen zu Typtests des is -Operators finden Sie im Abschnitt is-Operator des Artikels
Typtestoperatoren und Cast-Ausdrücke.
Ab C# 7.0 können Sie auch den is -Operator verwenden, um einen Ausdruck mit einem Muster abzugleichen,
wie im folgenden Beispiel gezeigt:

static bool IsFirstFridayOfOctober(DateTime date) =>


date is { Month: 10, Day: <=7, DayOfWeek: DayOfWeek.Friday };

Im vorherigen Beispiel vergleicht der is -Operator einen Ausdruck mit einem Eigenschaftsmuster mit
geschachtelten konstanten und relationalen Mustern.
Der is -Operator kann in den folgenden Szenarien nützlich sein:
So prüfen Sie den Laufzeittyp eines Ausdrucks, wie im folgenden Beispiel gezeigt:

int i = 34;
object iBoxed = i;
int? jNullable = 42;
if (iBoxed is int a && jNullable is int b)
{
Console.WriteLine(a + b); // output 76
}

Das vorherige Beispiel zeigt die Verwendung eines Deklarationsmusters.


So prüfen Sie auf null , wie im folgenden Beispiel gezeigt:

if (input is null)
{
return;
}

Wenn Sie einen Ausdruck mit null abgleichen, garantiert der Compiler, dass kein vom Benutzer
überladener == - oder != -Operator aufgerufen wird.
Ab C# 9.0 können Sie ein negiertes Muster verwenden, um auf nicht NULL zu prüfen, wie im folgenden
Beispiel gezeigt:

if (result is not null)


{
Console.WriteLine(result.ToString());
}

NOTE
Eine vollständige Liste der vom is -Operator unterstützten Muster finden Sie unter Muster.
C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt is-Operator der C#-Sprachspezifikation sowie in den folgenden
Vorschlägen für C#:
Mustervergleich
Musterabgleich mit Generics

Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
Muster
Tutorial: Verwenden des Musterabgleichs für buildtypgesteuerte und datengesteuerte Algorithmen
Typtest- und Umwandlungsoperatoren
nameof-Ausdruck (C#-Referenz)
04.11.2021 • 2 minutes to read

Ein nameof -Ausdruck erzeugt den Namen einer Variablen, eines Typs oder eines Members als
Zeichenfolgenkonstante:

Console.WriteLine(nameof(System.Collections.Generic)); // output: Generic


Console.WriteLine(nameof(List<int>)); // output: List
Console.WriteLine(nameof(List<int>.Count)); // output: Count
Console.WriteLine(nameof(List<int>.Add)); // output: Add

var numbers = new List<int> { 1, 2, 3 };


Console.WriteLine(nameof(numbers)); // output: numbers
Console.WriteLine(nameof(numbers.Count)); // output: Count
Console.WriteLine(nameof(numbers.Add)); // output: Add

Im Falle eines Typs und eines Namespace ist der erzeugte Name nicht vollqualifiziert, wie im obigen Beispiel zu
sehen.
Im Fall von ausführlichen Bezeichnern ist das @ -Zeichen nicht der Teil eines Namens, wie im folgenden Beispiel
gezeigt:

var @new = 5;
Console.WriteLine(nameof(@new)); // output: new

Ein nameof -Ausdruck wird zur Kompilierzeit ausgewertet und hat zur Laufzeit keine Auswirkung.
Sie können einen nameof -Ausdruck nutzen, um den Code zur Argumentüberprüfung besser verwalten zu
können:

public string Name


{
get => name;
set => name = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null");
}

Der nameof -Ausdruck ist ab C# 6 verfügbar.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Nameof-Ausdrücke der C#-Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
new-Operator (C#-Referenz)
04.11.2021 • 2 minutes to read

Der new -Operator erstellt eine neue Instanz eines Typs.


Sie können das Schlüsselwort new auch als einen Memberdeklarationsmodifizierer oder als Einschränkung
eines generischen Typs verwenden.

Konstruktoraufruf
Um eine neue Instanz eines Typs zu erstellen, rufen Sie in der Regel einen der Konstruktoren dieses Typs mit
dem new -Operator auf:

var dict = new Dictionary<string, int>();


dict["first"] = 10;
dict["second"] = 20;
dict["third"] = 30;

Console.WriteLine(string.Join("; ", dict.Select(entry => $"{entry.Key}: {entry.Value}")));


// Output:
// first: 10; second: 20; third: 30

Sie können einen Objekt- oder Auflistungsinitialisierer mit dem new -Operator verwenden, um ein Objekt in
einer Anweisung zu instanziieren und zu initialisieren, wie das folgende Beispiel zeigt:

var dict = new Dictionary<string, int>


{
["first"] = 10,
["second"] = 20,
["third"] = 30
};

Console.WriteLine(string.Join("; ", dict.Select(entry => $"{entry.Key}: {entry.Value}")));


// Output:
// first: 10; second: 20; third: 30

Ab C# 9.0 sind Konstruktoraufforderungsausdrücke als Ziel typisiert. Das heißt, wenn der Zieltyp eines
Ausdrucks bekannt ist, können Sie einen Typnamen wie im folgenden Beispiel weglassen:

List<int> xs = new();
List<int> ys = new(capacity: 10_000);
List<int> zs = new() { Capacity = 20_000 };

Dictionary<int, List<int>> lookup = new()


{
[1] = new() { 1, 2, 3 },
[2] = new() { 5, 8, 3 },
[5] = new() { 1, 0, 4 }
};

Wie das vorherige Beispiel zeigt, verwenden Sie immer Klammern in einem zieltypisierten new -Ausdruck.
Wenn der Zieltyp eines new -Ausdrucks unbekannt ist (beispielsweise, wenn Sie das Schlüsselwort var
verwenden), müssen Sie einen Typnamen angeben.
Arrayerstellung
Sie können den new -Operator auch verwenden, um eine Arrayinstanz zu erstellen, wie das folgende Beispiel
zeigt:

var numbers = new int[3];


numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;

Console.WriteLine(string.Join(", ", numbers));


// Output:
// 10, 20, 30

Verwenden Sie die Arrayinitialisierungssyntax, um eine Arrayinstanz zu erstellen und sie mit Elementen in einer
Anweisung zu auszufüllen. Im folgenden Beispiel werden verschiedene Möglichkeiten dafür veranschaulicht:

var a = new int[3] { 10, 20, 30 };


var b = new int[] { 10, 20, 30 };
var c = new[] { 10, 20, 30 };
Console.WriteLine(c.GetType()); // output: System.Int32[]

Weitere Informationen zu Arrays finden Sie unter Arrays.

Instanziierung von anonymen Typen


Um eine Instanz eines anonymen Typs zu erstellen, verwenden Sie die Syntax für die Initialisierung von new -
Operatoren und -Objekten:

var example = new { Greeting = "Hello", Name = "World" };


Console.WriteLine($"{example.Greeting}, {example.Name}!");
// Output:
// Hello, World!

Zerstörung von Typinstanzen


Sie müssen keine zuvor angelegten Typinstanzen zerstören. Instanzen sowohl von Verweis- als auch von
Werttypen werden automatisch zerstört. Instanzen von Werttypen werden zerstört, sobald der sie enthaltende
Kontext zerstört wird. Instanzen von Verweistypen werden vom Garbage Collector zu einem unbestimmten
Zeitpunkt zerstört, nachdem der letzte Verweis auf sie entfernt wurde.
Bei Typinstanzen, die nicht verwaltete Ressourcen wie beispielsweise ein Dateihandle enthalten, ist eine
deterministische Bereinigung empfehlenswert, um sicherzustellen, dass die darin enthaltenen Ressourcen so
bald wie möglich freigegeben werden. Weitere Informationen finden Sie im Artikel zum System.IDisposableAPI-
Verweis und der using-Anweisung.

Operatorüberladbarkeit
Ein benutzerdefinierter Typ kann den new -Operator nicht überladen.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Der new-Operator der C#-Sprachspezifikation.
Weitere Informationen zum zieltypisierten new -Ausdruck finden Sie unter Hinweis zum Featurevorschlag.
Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
Objekt- und Elementinitialisierer
sizeof-Operator (C#-Verweis)
04.11.2021 • 2 minutes to read

Der sizeof -Operator gibt die Anzahl der Bytes zurück, die von einer Variablen eines bestimmten Typs belegt
werden. Das Argument für den sizeof -Operator muss der Name eines nicht verwalteten Typs oder eines
Typparameters sein, der darauf beschränkt ist, ein nicht verwalteter Typ zu sein.
Der sizeof -Operator erfordert einen unsicheren Kontext. Die in der folgenden Tabelle dargestellten Ausdrücke
werden jedoch in der Kompilierzeit auf die entsprechenden konstanten Werte ausgewertet und erfordern
keinen unsicheren Kontext:

EXP RESSIO N KO N STA N T ER W ERT

sizeof(sbyte) 1

sizeof(byte) 1

sizeof(short) 2

sizeof(ushort) 2

sizeof(int) 4

sizeof(uint) 4

sizeof(long) 8

sizeof(ulong) 8

sizeof(char) 2

sizeof(float) 4

sizeof(double) 8

sizeof(decimal) 16

sizeof(bool) 1

Sie müssen auch keinen unsicheren Kontext verwenden, wenn der Operand des sizeof -Operators der Name
eines Enumerationstyps ist.
Im folgenden Beispiel wird die Verwendung des sizeof -Operators veranschaulicht:
using System;

public struct Point


{
public Point(byte tag, double x, double y) => (Tag, X, Y) = (tag, x, y);

public byte Tag { get; }


public double X { get; }
public double Y { get; }
}

public class SizeOfOperator


{
public static void Main()
{
Console.WriteLine(sizeof(byte)); // output: 1
Console.WriteLine(sizeof(double)); // output: 8

DisplaySizeOf<Point>(); // output: Size of Point is 24


DisplaySizeOf<decimal>(); // output: Size of System.Decimal is 16

unsafe
{
Console.WriteLine(sizeof(Point*)); // output: 8
}
}

static unsafe void DisplaySizeOf<T>() where T : unmanaged


{
Console.WriteLine($"Size of {typeof(T)} is {sizeof(T)}");
}
}

Der sizeof -Operator gibt die Anzahl der Bytes zurück, die von der Common Language Runtime im verwalteten
Speicher belegt werden. Wie im vorherigen Beispiel veranschaulicht, enthält dieser Wert für Strukturtypen alle
Auffüllzeichen. Das Ergebnis des sizeof -Operators unterscheidet sich möglicherweise von dem Ergebnis der
Marshal.SizeOf-Methode, die die Größe eines Typs in nicht verwaltetem Speicher zurückgibt.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Der sizeof-Operator in der C#-Sprachspezifikation.

Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
Operatoren im Zusammenhang mit Zeigern
Zeigertypen
Memory- und Span-bezogene Typen
Generics in .NET
stackalloc-Ausdruck (C#-Referenz)
04.11.2021 • 3 minutes to read

Ein stackalloc -Ausdruck ordnet einen Speicherblock im Stapel zu. Ein während der Ausführung der Methode
im Stapel zugeordneter Speicherblock wird automatisch verworfen, wenn diese Methode zurückgegeben wird.
Sie können den mit stackalloc zugeordneten Speicher nicht explizit freigeben. Ein im Stapel zugeordneter
Speicherblock unterliegt nicht der automatischen Speicherbereinigung und muss nicht mit einer fixed -
Anweisungen angeheftet werden.
Sie können das Ergebnis eines stackalloc -Ausdrucks einer Variablen mit einem der folgenden Typen zuweisen:
Ab C# 7.2, System.Span<T> oder System.ReadOnlySpan<T>, wie im folgenden Beispiel gezeigt:

int length = 3;
Span<int> numbers = stackalloc int[length];
for (var i = 0; i < length; i++)
{
numbers[i] = i;
}

Sie müssen keinen unsicheren Kontext verwenden, wenn Sie der Variablen Span<T> oder
ReadOnlySpan<T> einen im Stapel zugeordneten Speicherblock zuweisen.
Wenn Sie mit diesen Typen arbeiten, können Sie einen stackalloc -Ausdruck in bedingten oder
Zuweisungsausdrücken verwenden, wie das folgende Beispiel zeigt:

int length = 1000;


Span<byte> buffer = length <= 1024 ? stackalloc byte[length] : new byte[length];

Ab C# 8.0 können Sie einen stackalloc -Ausdruck innerhalb anderer Ausdrücke immer dann verwenden,
wenn eine Span<T>- oder ReadOnlySpan<T>-Variable zulässig ist, wie im folgenden Beispiel zu sehen:

Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };


var ind = numbers.IndexOfAny(stackalloc[] { 2, 4, 6 ,8 });
Console.WriteLine(ind); // output: 1

NOTE
Wir empfehlen, die Typen Span<T> oder ReadOnlySpan<T> zu verwenden, um nach Möglichkeit mit dem im
Stapel zugeordneten Speicher zu arbeiten.

Ein Zeigertyp, wie im folgenden Beispiel gezeigt:


unsafe
{
int length = 3;
int* numbers = stackalloc int[length];
for (var i = 0; i < length; i++)
{
numbers[i] = i;
}
}

Wie das vorhergehende Beispiel zeigt, müssen Sie einen unsafe -Kontext verwenden, wenn Sie mit
Zeigertypen arbeiten.
Im Fall von Zeigertypen können Sie einen stackalloc -Ausdruck nur in einer lokalen Variablendeklaration
zum Initialisieren der Variable verwenden.
Die Menge des verfügbaren Speichers im Stapel ist begrenzt. Wenn Sie zu viel Speicher im Stapel zuordnen,
wird eine StackOverflowException ausgelöst. Beachten Sie die folgenden Regeln, um dies zu vermeiden:
Begrenzen Sie die Speichermenge, die Sie mit stackalloc zuordnen. Wenn die vorgesehene Puffergröße
beispielsweise einen bestimmten Grenzwert unterschreitet, weisen Sie den Speicher auf dem Stapel zu.
Verwenden Sie andernfalls ein Array der erforderlichen Länge, wie im folgenden Code gezeigt:

const int MaxStackLimit = 1024;


Span<byte> buffer = inputLength <= MaxStackLimit ? stackalloc byte[inputLength] : new
byte[inputLength];

NOTE
Da die Menge des auf im Stapel verfügbaren Speichers von der Umgebung abhängt, in der der Code ausgeführt
wird, sollten Sie bei der Festlegung des tatsächlichen Grenzwerts konservativ vorgehen.

Vermeiden Sie die Verwendung von stackalloc in Schleifen. Ordnen Sie den Speicherblock außerhalb
einer Schleife zu, und verwenden Sie ihn innerhalb der Schleife wieder.
Der Inhalt des neu zugeordneten Speichers ist undefiniert. Er sollte vor Verwendung initialisiert werden.
Beispielsweise können Sie die Span<T>.Clear-Methode verwenden, die alle Elemente auf den Standardwert des
Typs T festlegt.
Ab C# 7.3 können Sie mit der Arrayinitialisierungssyntax den Inhalt des neu zugeordneten Speichers definieren.
Das folgende Beispiel zeigt verschiedene Möglichkeiten, dies zu erreichen:

Span<int> first = stackalloc int[3] { 1, 2, 3 };


Span<int> second = stackalloc int[] { 1, 2, 3 };
ReadOnlySpan<int> third = stackalloc[] { 1, 2, 3 };

Im Ausdruck stackalloc T[E] muss T ein nicht verwalteter Typ sein und E in einen nicht negativen int-Wert
ausgewertet werden.

Sicherheit
Mit der Verwendung von stackalloc werden automatisch Funktionen zum Erkennen eines Pufferüberlaufs in
der Common Language Runtime (CLR) aktiviert. Wenn ein Pufferüberlauf erkannt wird, wird der Vorgang so
schnell wie möglich beendet, damit das Risiko der Ausführung von schädlichem Code verringert wird.
C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Stapelzuordnung der C#-Sprachspezifikation sowie im Vorschlag
zum Feature Zulassen von stackalloc in geschachtelten Kontexten.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Operatoren im Zusammenhang mit Zeigern
Zeigertypen
Memory- und Span-bezogene Typen
Empfehlungen und Warnungen für „stackalloc“
switch-Ausdruck (C#-Referenz)
04.11.2021 • 3 minutes to read

Ab C# 8.0 verwenden Sie den Ausdruck switch , um einen einzelnen Ausdruck aus einer Liste mit möglichen
Ausdrücken auf Grundlage eines Musterabgleichs mit einem Eingabeausdruck auszuwerten. Informationen zur
switch -Anweisung, die switch -ähnliche Semantik in einem Anweisungskontext unterstützt, finden Sie im
Artikel Auswahlanweisungen (C#-Referenz) im Abschnitt zur switch -Anweisung.
Das folgende Beispiel veranschaulicht einen switch -Ausdruck, der Werte eines enum -Typs, die für visuelle
Richtungen auf einer Onlinekarte stehen, in die entsprechenden Kardinalrichtungen konvertiert:

public static class SwitchExample


{
public enum Direction
{
Up,
Down,
Right,
Left
}

public enum Orientation


{
North,
South,
East,
West
}

public static Orientation ToOrientation(Direction direction) => direction switch


{
Direction.Up => Orientation.North,
Direction.Right => Orientation.East,
Direction.Down => Orientation.South,
Direction.Left => Orientation.West,
_ => throw new ArgumentOutOfRangeException(nameof(direction), $"Not expected direction value:
{direction}"),
};

public static void Main()


{
var direction = Direction.Right;
Console.WriteLine($"Map view direction is {direction}");
Console.WriteLine($"Cardinal orientation is {ToOrientation(direction)}");
// Output:
// Map view direction is Right
// Cardinal orientation is East
}
}

Das vorstehende Beispiel zeigt die grundlegenden Elemente eines switch -Ausdrucks:
Es handelt sich um einen vom Schlüsselwort switch gefolgten Ausdruck. Im vorherigen Beispiel ist das der
Methodenparameter direction .
Hierbei handelt es sich um die durch Kommas getrennten Verzweigungsarme des switch -Ausdrucks. Jeder
Verzweigungsarm des switch -Ausdrucks enthält ein Muster, einen optionalen Case Guard, das => -Token
und einen Ausdruck.
Im vorherigen Beispiel nutzt ein switch -Ausdruck die folgenden Muster:
Ein Konstantenmuster: Hiermit werden die definierten Werte der Direction -Enumeration verarbeitet.
Ein Ausschussmuster: Hiermit wird jeder ganzzahlige Wert verarbeitet, der nicht über den entsprechenden
Member der Direction -Enumeration verfügt (z. B. (Direction)10 ). So liegt ein vollständiger switch -
Ausdruck vor.

IMPORTANT
Informationen zu den von einem switch -Ausdruck unterstützten Mustern und weitere Beispiele finden Sie unter
Muster.

Das Ergebnis eines switch -Ausdrucks ist der Wert des Ausdrucks des ersten switch -
Ausdruckverzweigungsarms, dessen Muster mit dem des Eingabeausdrucks übereinstimmt und bei dem der
Case Guard (sofern vorhanden) als true ausgewertet wird. Die switch -Ausdruckverzweigungsarme werden in
der Textreihenfolge ausgewertet.
Der Compiler gibt einen Fehler aus, wenn ein niedrigerer switch -Ausdruckverzweigungsarm nicht ausgewählt
werden kann, weil ein höherer switch -Ausdruckverzweigungsarm allen Werten entspricht.

Case Guards
Ein Muster ist möglicherweise nicht ausdrucksstark genug, um die Bedingung für die Auswertung des
Ausdrucks einer Verzweigung anzugeben. In so einem Fall können Sie ein Case Guard verwenden. Dabei handelt
es sich um eine zusätzliche Bedingung, die zusammen mit einem übereinstimmenden Muster erfüllt werden
muss. Ein Case Guard muss ein boolescher Ausdruck sein. Sie geben einen Case Guard nach dem when -
Schlüsselwort an, das einem Muster folgt. Sehen Sie sich dazu das folgende Beispiel an:

public readonly struct Point


{
public Point(int x, int y) => (X, Y) = (x, y);

public int X { get; }


public int Y { get; }
}

static Point Transform(Point point) => point switch


{
{ X: 0, Y: 0 } => new Point(0, 0),
{ X: var x, Y: var y } when x < y => new Point(x + y, y),
{ X: var x, Y: var y } when x > y => new Point(x - y, y),
{ X: var x, Y: var y } => new Point(2 * x, 2 * y),
};

Im vorherigen Beispiel werden Eigenschaftenmuster mit geschachtelten var-Mustern verwendet.

Unvollständige switch-Ausdrücke
Wenn keines der Muster eines switch -Ausdrucks mit einem Eingabewert übereinstimmt, wird von der Runtime
eine Ausnahme zurückgegeben. In .NET Core 3.0 und höher ist die Ausnahme eine
System.Runtime.CompilerServices.SwitchExpressionException-Klasse. Im .NET Framework ist die Ausnahme eine
InvalidOperationException-Klasse. Der Compiler generiert eine Warnung, wenn ein switch -Ausdruck nicht alle
möglichen Eingabewerte verarbeiten kann.
TIP
Wenn sichergestellt werden soll, dass ein switch -Ausdruck alle möglichen Eingabewerte verarbeitet, stellen Sie für einen
Verzweigungsarm des switch -Ausdrucks ein Ausschussmuster bereit.

C#-Sprachspezifikation
Weitere Informationen erhalten Sie im Abschnitt zum switch -Ausdruck des Artikels Rekursiver Musterabgleich.

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Muster
Tutorial: Verwenden des Musterabgleichs für buildtypgesteuerte und datengesteuerte Algorithmen
switch -Anweisung
Operatoren „true“ und „false“ (C#-Referenz)
04.11.2021 • 2 minutes to read

Der true -Operator gibt den bool-Wert true zurück, um anzugeben, dass der Operand definitiv den Wert
„true“ hat. Der false -Operator gibt den bool -Wert true zurück, um anzugeben, dass der Operand definitiv
den Wert „false“ hat. Es kann nicht garantiert werden, dass sich die true - und false -Operatoren gegenseitig
ergänzen. Dies bedeutet, dass sowohl der Operator true als auch der Operator false möglicherweise den
bool -Wert false für den gleichen Operanden zurückgeben. Wenn ein Typ einen der beiden Operatoren
definiert, muss er auch den anderen Operator definieren.

TIP
Verwenden Sie den Typ bool? , wenn Sie die dreiwertige Logik unterstützen müssen (z. B. wenn Sie mit Datenbanken
arbeiten, die einen dreiwertigen booleschen Typ unterstützen). C# stellt die & - und | -Operatoren zur Verfügung, die
die dreiwertige Logik mit den bool? -Operanden unterstützen. Weitere Informationen finden Sie im Abschnitt Boolesche
logische Operatoren, die NULL-Werte zulassen im Artikel Boolesche logische Operatoren.

Boolesche Ausdrücke
Ein Typ mit dem definierten true -Operator kann der Typ des Ergebnisses eines steuernden bedingten
Ausdrucks in if-, do-, while- und for-Anweisungen und im bedingten Operator ?: sein. Weitere Informationen
finden Sie im Abschnitt Boolescher Ausdruck der C#-Sprachspezifikation.

Benutzerdefinierte bedingte logische Operatoren


Wenn ein Typ mit den definierten Operatoren true und false den logischen OR-Operator oder den logischen
AND-Operator | in einer bestimmten Weise überlädt & , kann der bedingte logische OR-Operator || bzw. der
bedingte logische AND-Operator && für Operanden dieses Typs ausgewertet werden. Weitere Informationen
finden Sie im Abschnitt Benutzerdefinierte bedingte logische Operatoren der C#-Sprachspezifikation.

Beispiel
Das folgende Beispiel zeigt den Typen, der sowohl true - als auch false -Operatoren definiert. Außerdem
überlädt der Typ den logischen AND-Operator & so, dass der Operator && auch für die Operanden dieses Typs
ausgewertet werden kann.
using System;

public struct LaunchStatus


{
public static readonly LaunchStatus Green = new LaunchStatus(0);
public static readonly LaunchStatus Yellow = new LaunchStatus(1);
public static readonly LaunchStatus Red = new LaunchStatus(2);

private int status;

private LaunchStatus(int status)


{
this.status = status;
}

public static bool operator true(LaunchStatus x) => x == Green || x == Yellow;


public static bool operator false(LaunchStatus x) => x == Red;

public static LaunchStatus operator &(LaunchStatus x, LaunchStatus y)


{
if (x == Red || y == Red || (x == Yellow && y == Yellow))
{
return Red;
}

if (x == Yellow || y == Yellow)
{
return Yellow;
}

return Green;
}

public static bool operator ==(LaunchStatus x, LaunchStatus y) => x.status == y.status;


public static bool operator !=(LaunchStatus x, LaunchStatus y) => !(x == y);

public override bool Equals(object obj) => obj is LaunchStatus other && this == other;
public override int GetHashCode() => status;
}

public class LaunchStatusTest


{
public static void Main()
{
LaunchStatus okToLaunch = GetFuelLaunchStatus() && GetNavigationLaunchStatus();
Console.WriteLine(okToLaunch ? "Ready to go!" : "Wait!");
}

static LaunchStatus GetFuelLaunchStatus()


{
Console.WriteLine("Getting fuel launch status...");
return LaunchStatus.Red;
}

static LaunchStatus GetNavigationLaunchStatus()


{
Console.WriteLine("Getting navigation launch status...");
return LaunchStatus.Yellow;
}
}

Beachten Sie das kurzschließende Verhalten des && -Operators. Wenn die GetFuelLaunchStatus -Methode
LaunchStatus.Red zurückgibt, wird der rechte Operand des && -Operators nicht ausgewertet. Der Grund dafür
ist, dass LaunchStatus.Red definitiv „false“ ist. Dann hängt das Ergebnis des logischen AND-Operators nicht vom
Wert des rechten Operanden ab. Im Beispiel wird Folgendes ausgegeben:
Getting fuel launch status...
Wait!

Weitere Informationen
C#-Referenz
C#-Operatoren und -Ausdrücke
with-Ausdruck (C#-Referenz)
04.11.2021 • 3 minutes to read

Ab C# 9.0 ist ein with -Ausdruck verfügbar, der eine Kopie des dazugehörigen Operanden mit den
angegebenen Eigenschaften und bearbeiteten Feldern erzeugt:

using System;

public class WithExpressionBasicExample


{
public record NamedPoint(string Name, int X, int Y);

public static void Main()


{
var p1 = new NamedPoint("A", 0, 0);
Console.WriteLine($"{nameof(p1)}: {p1}"); // output: p1: NamedPoint { Name = A, X = 0, Y = 0 }

var p2 = p1 with { Name = "B", X = 5 };


Console.WriteLine($"{nameof(p2)}: {p2}"); // output: p2: NamedPoint { Name = B, X = 5, Y = 0 }

var p3 = p1 with
{
Name = "C",
Y = 4
};
Console.WriteLine($"{nameof(p3)}: {p3}"); // output: p3: NamedPoint { Name = C, X = 0, Y = 4 }

Console.WriteLine($"{nameof(p1)}: {p1}"); // output: p1: NamedPoint { Name = A, X = 0, Y = 0 }


}
}

Wie das vorherige Beispiel zeigt, verwenden Sie die Objektinitialisierersyntax, um anzugeben, welche Member
bearbeitet und welche neuen Werte dazu verwendet werden sollen.
In C# 9.0 muss der linke Operand eines with -Ausdrucks einen Datensatztyp aufweisen. Ab C# 10.0 kann der
linke Operand eines with -Ausdrucks auch einen Strukturtyp aufweisen.
Das Ergebnis eines with -Ausdrucks weist denselben Laufzeittyp auf wie der Operand des Ausdrucks. Sehen Sie
sich dazu das folgende Beispiel an:

using System;

public class InheritanceExample


{
public record Point(int X, int Y);
public record NamedPoint(string Name, int X, int Y) : Point(X, Y);

public static void Main()


{
Point p1 = new NamedPoint("A", 0, 0);
Point p2 = p1 with { X = 5, Y = 3 };
Console.WriteLine(p2 is NamedPoint); // output: True
Console.WriteLine(p2); // output: NamedPoint { X = 5, Y = 3, Name = A }

}
}
Wenn der Member einen Verweistyp aufweist, wird beim Kopieren eines Operanden nur der Verweis auf eine
Memberinstanz kopiert. Sowohl der kopierte als auch der ursprüngliche Operand können auf dieselbe Instanz
vom Typ Verweis zugreifen. Das folgende Beispiel veranschaulicht dieses Verhalten:

using System;
using System.Collections.Generic;

public class ExampleWithReferenceType


{
public record TaggedNumber(int Number, List<string> Tags)
{
public string PrintTags() => string.Join(", ", Tags);
}

public static void Main()


{
var original = new TaggedNumber(1, new List<string> { "A", "B" });

var copy = original with { Number = 2 };


Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
// output: Tags of copy: A, B

original.Tags.Add("C");
Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
// output: Tags of copy: A, B, C
}
}

Benutzerdefinierte Kopiersemantik
Jeder Datensatzklassentyp weist den Kopierkonstruktor auf. Dabei handelt es sich um einen Konstruktor mit
einzelnem Parameter, der den Typ des Datensatzes aufweist, in dem er enthalten ist. Der Zustand des
dazugehörigen Arguments wird in eine neue Datensatzinstanz kopiert. Wenn ein with -Ausdruck ausgewertet
wird, wird der Kopierkonstruktor aufgerufen, um eine neue Datensatzinstanz basierend auf dem ursprünglichen
Datensatz zu instanziieren. Danach wird die neue Instanz entsprechend der angegebenen Änderungen
aktualisiert. Standardmäßig ist der Kopierkonstruktor implizit, das heißt, vom Compiler generiert. Wenn Sie die
Kopiersemantik des Datensatzes anpassen möchten, deklarieren Sie explizit einen Kopierkonstruktor mit
gewünschtem Verhalten. Das folgende Beispiel aktualisiert das vorherige Beispiel mit einem expliziten
Kopierkonstruktor. Das neue Kopierverhalten besteht darin, Listenelemente anstatt eines Listenverweises zu
kopieren, wenn ein Datensatz kopiert wird:
using System;
using System.Collections.Generic;

public class UserDefinedCopyConstructorExample


{
public record TaggedNumber(int Number, List<string> Tags)
{
protected TaggedNumber(TaggedNumber original)
{
Number = original.Number;
Tags = new List<string>(original.Tags);
}

public string PrintTags() => string.Join(", ", Tags);


}

public static void Main()


{
var original = new TaggedNumber(1, new List<string> { "A", "B" });

var copy = original with { Number = 2 };


Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
// output: Tags of copy: A, B

original.Tags.Add("C");
Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
// output: Tags of copy: A, B
}
}

Die Kopiersemantik für Strukturtypen kann nicht angepasst werden.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten des Artikels Datensätze:
with-Ausdruck
Methoden zum Kopieren und Klonen von Membern

Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Datensätze
Strukturtypen
Operatorüberladung (C#-Referenz)
04.11.2021 • 3 minutes to read

Ein benutzerdefinierter Typ kann einen vordefinierten C#-Operator überladen. Das bedeutet, dass ein Typ die
benutzerdefinierte Implementierung eines Vorgangs bereitstellen kann, wenn mindestens einer der beiden
Operanden vom selben Typ ist. Im Abschnitt Überladbare Operatoren werden die C#-Operatoren angegeben,
die überladen werden können.
Verwenden Sie das Schlüsselwort operator , um einen Operator zu deklarieren. Jede Operatordeklaration muss
mit den folgenden Regeln konform sein:
Sie enthält sowohl einen public - als auch einen static -Modifizierer.
Ein unärer Operator verfügt über einen Eingabeparameter. Ein binärer Operator verfügt über zwei
Eingabeparameter. Auf jeden Fall muss mindestens ein Parameter vom Typ T oder T? sein, wobei T der
Typ ist, der die Operatordeklaration enthält.
Das folgende Beispiel definiert eine vereinfachte Struktur für die Darstellung einer rationalen Zahl. Die Struktur
überlädt einige der arithmetischen Operatoren:
using System;

public readonly struct Fraction


{
private readonly int num;
private readonly int den;

public Fraction(int numerator, int denominator)


{
if (denominator == 0)
{
throw new ArgumentException("Denominator cannot be zero.", nameof(denominator));
}
num = numerator;
den = denominator;
}

public static Fraction operator +(Fraction a) => a;


public static Fraction operator -(Fraction a) => new Fraction(-a.num, a.den);

public static Fraction operator +(Fraction a, Fraction b)


=> new Fraction(a.num * b.den + b.num * a.den, a.den * b.den);

public static Fraction operator -(Fraction a, Fraction b)


=> a + (-b);

public static Fraction operator *(Fraction a, Fraction b)


=> new Fraction(a.num * b.num, a.den * b.den);

public static Fraction operator /(Fraction a, Fraction b)


{
if (b.num == 0)
{
throw new DivideByZeroException();
}
return new Fraction(a.num * b.den, a.den * b.num);
}

public override string ToString() => $"{num} / {den}";


}

public static class OperatorOverloading


{
public static void Main()
{
var a = new Fraction(5, 4);
var b = new Fraction(1, 2);
Console.WriteLine(-a); // output: -5 / 4
Console.WriteLine(a + b); // output: 14 / 8
Console.WriteLine(a - b); // output: 6 / 8
Console.WriteLine(a * b); // output: 5 / 8
Console.WriteLine(a / b); // output: 10 / 4
}
}

Sie könnten das vorherige Beispiel erweitern, indem Sie eine implizite Konvertierung von int nach Fraction
definieren. Dann würden überladene Operatoren Argumente dieser beiden Typen unterstützen. Das bedeutet,
dass es dann möglich wäre, eine ganze Zahl und einen Bruch zu addieren und als Ergebnis einen Bruch zu
erhalten.
Verwenden Sie zudem das Kennwort operator , um eine benutzerdefinierte Konvertierung zu definieren.
Weitere Informationen finden Sie unter Benutzerdefinierte Konvertierungsoperatoren.

Überladbare Operatoren
Die folgende Tabelle enthält Informationen zur Überladbarkeit von C#-Operatoren:

O P ERATO REN ÜB ERL A DB A RK EIT

+x, -x, !x, ~x, ++, --, true, false Diese unären Operatoren können überladen werden.

x + y, x - y, x * y, x / y, x % y, x & y, x | y, x ^ y, x << y, x >> Diese binären Operatoren können überladen werden.


y, x == y, x != y, x < y, x > y, x <= y, x >= y Manche Operatoren müssen paarweise überladen werden.
Weitere Informationen dazu finden Sie im Hinweisfeld unter
dieser Tabelle.

x && y, x || y Bedingte logische Operatoren können nicht überladen


werden. Wenn jedoch ein Typ mit den überladenen
Operatoren true und false ebenfalls den Operator &
oder | auf eine bestimmte Weise überlädt, kann jeweils
entweder der Operator && oder der Operator || für die
Operanden dieses Typs ausgewertet werden. Weitere
Informationen finden Sie im Abschnitt Benutzerdefinierte
bedingte logische Operatoren der C#-Sprachspezifikation.

a[i], a?[i] Der Elementzugriff wird nicht als überladbarer Operator


betrachtet. Sie können aber einen Indexer definieren.

(T)x Der Cast-Operator kann nicht überladen werden, jedoch


können Sie benutzerdefinierte Typkonvertierungen
definieren, die von einem Cast-Ausdruck durchgeführt
werden können. Weitere Informationen finden Sie unter
Benutzerdefinierte Konvertierungsoperatoren.

+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= Zusammengesetzte Zuweisungsoperatoren können nicht
explizit überladen werden. Wenn Sie einen binären Operator
überladen, wird der zugehörige zusammengesetzte
Zuweisungsoperator jedoch, sofern er vorhanden ist, auch
implizit überladen. Wenn += beispielsweise mit +
ausgewertet wird. Selbiger kann überladen werden.

^x, x = y, x.y, x?.y , c ? t : f, x ?? y, x ??= y, x..y, x->y, =>, Diese Operatoren können nicht überladen werden.
f(x), as, await, checked, unchecked, default, delegate, is,
nameof, new, sizeof, stackalloc, switch, typeof, with

NOTE
Die Vergleichsoperatoren müssen paarweise überladen werden. Das bedeutet: Wenn ein Operator überladen wird, der
einem Paar angehört, muss der andere Operator ebenfalls überladen werden. Dies kann für die folgenden Paare zutreffen:
Die Operatoren == und !=
Die Operatoren < und >
Die Operatoren <= und >=

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Operatorüberladung
Operatoren
Siehe auch
C#-Referenz
C#-Operatoren und -Ausdrücke
Benutzerdefinierte Konvertierungsoperatoren
Entwurfsrichtlinien: Operatorüberladungen
Entwurfsrichtlinien: Gleichheitsoperatoren
Why are overloaded operators always static in C#? (Warum sind überladene Operatoren in C# immer
statisch?)
Iterationsanweisungen (C#-Referenz)
04.11.2021 • 6 minutes to read

Die folgenden Anweisungen führen wiederholt eine Anweisung bzw. einen Anweisungsblock aus:
Die for -Anweisung führt ihren Rumpf aus, während ein angegebener boolescher Ausdruck als true
ausgewertet wird.
Die foreach -Anweisung zählt die Elemente einer Sammlung auf und führt ihren Rumpf für jedes Element
der Sammlung aus.
Die do -Anweisung führt ihren Rumpf unter bestimmten Bedingungen einmal oder mehrmals aus.
Die while -Anweisung führt ihren Rumpf unter bestimmten Bedingungen keinmal oder mehrmals aus.

An jedem beliebigen Punkt im Rumpf einer Iterationsanweisung können Sie mit der Anweisung break die
Schleife verlassen oder mit der Anweisung continue zur nächsten Iteration in der Schleife übergehen.

Die Anweisung for


Die Anweisung for führt eine Anweisung oder einen Anweisungsblock aus, während ein angegebener
boolescher Ausdruck true ergibt. Das folgende Beispiel zeigt die Anweisung for , die ihren Rumpf ausführt,
solange ein Zähler für eine ganze Zahl kleiner als drei ist:

for (int i = 0; i < 3; i++)


{
Console.Write(i);
}
// Output:
// 012

Das vorherige Beispiel zeigt die Elemente der for -Anweisung:


Den Abschnitt initializer, der nur einmal ausgeführt wird, bevor die Schleife beginnt. In der Regel
deklarieren und initialisieren Sie in diesem Abschnitt eine lokale Schleifenvariable. Auf die deklarierte
Variable kann von außerhalb der for -Anweisung nicht zugegriffen werden.
Der Abschnitt initializer im vorherigen Beispiel deklariert und initialisiert eine Variable für einen Zähler
mit ganzer Zahl:

int i = 0

Den Abschnitt Bedingung, der bestimmt, ob die nächste Iteration in der Schleife ausgeführt werden soll.
Wenn er mit true ausgewertet wird oder nicht vorhanden ist, wird die nächste Iteration ausgeführt.
Ansonsten wird die Schleife verlassen. Der Abschnitt condition muss ein boolescher Ausdruck sein.
Der Abschnitt Bedingung im vorherigen Beispiel prüft, ob ein Zählerwert kleiner als drei ist:

i < 3

Den Abschnitt iterator, der definiert, was nach jeder Iteration des Schleifenrumpfs geschieht.
Der Abschnitt iterator im vorhergehenden Beispiel erhöht den Zähler:
i++

Den Schleifenrumpf, der entweder eine Anweisung oder ein Anweisungsblock sein muss.
Der Abschnitt „iterator “ enthält keine oder mehrere der folgenden durch Komma getrennten
Anweisungsausdrücke:
Präfix- oder Postfix-Inkrementausdruck, z.B. ++i oder i++
Präfix- oder Postfix-Dekrementausdruck, z.B. --i oder i--
Zuweisung
Aufruf einer Methode
await-Ausdruck
Erstellung eines Objekts mithilfe des new-Operators
Wenn Sie im Abschnitt „initializer “ keine Schleifenvariable deklarieren, können Sie keinen oder mehrere der
Ausdrücke in der vorhergehenden Liste auch im Abschnitt „initializer “ verwenden. Das folgende Beispiel
veranschaulicht mehrere weniger übliche Verwendungen der Abschnitte „initializer “ und „iterator “: das
Zuweisen eines Werts für eine externe Schleifenvariable im Abschnitt „initializer “, das Aufrufen einer Methode in
den Abschnitten „initializer “ und „iterator “ und das Ändern der Werte zweier Variablen im Abschnitt „iterator “.

int i;
int j = 3;
for (i = 0, Console.WriteLine($"Start: i={i}, j={j}"); i < j; i++, j--, Console.WriteLine($"Step: i={i}, j=
{j}"))
{
//...
}
// Output:
// Start: i=0, j=3
// Step: i=1, j=2
// Step: i=2, j=1

Alle Abschnitte der for -Anweisung sind optional. Im folgenden Beispiel wird die Endlosschleife for definiert:

for ( ; ; )
{
//...
}

Die Anweisung foreach


Die Anweisung foreach führt eine Anweisung oder einen Block von Anweisungen für jedes Element in einer
Instanz des Typs aus, der die Schnittstellen System.Collections.IEnumerable oder
System.Collections.Generic.IEnumerable<T> implementiert. Dies wird im folgenden Beispiel gezeigt:

var fibNumbers = new List<int> { 0, 1, 1, 2, 3, 5, 8, 13 };


foreach (int element in fibNumbers)
{
Console.Write($"{element} ");
}
// Output:
// 0 1 1 2 3 5 8 13

Die Anweisung foreach ist nicht auf diese Typen beschränkt. Sie können sie mit einer Instanz eines beliebigen
Typs verwenden, der die folgenden Bedingungen erfüllt:
Ein Typ hat die öffentliche parameterlose GetEnumerator -Methode. Ab C# 9.0 kann die GetEnumerator -
Methode die Erweiterungsmethode eines Typs sein.
Der Rückgabetyp der Methode GetEnumerator weist die öffentliche Eigenschaft Current und die öffentliche
parameterlose Methode MoveNext auf, deren Rückgabetyp bool ist.

Im folgenden Beispiel wird die Anweisung foreach mit einer Instanz des Typs System.Span<T> verwendet, der
keine Schnittstellen implementiert:

Span<int> numbers = new int[] { 3, 14, 15, 92, 6 };


foreach (int number in numbers)
{
Console.Write($"{number} ");
}
// Output:
// 3 14 15 92 6

Ab C# 7.3 können Sie die Iterationsvariable mit den Modifizierern ref oder ref readonly deklarieren, wenn
die Eigenschaft Current des Enumerators einen Verweisrückgabewert ( ref T , wobei T dem Typ des
Sammlungselements entspricht) zurückgibt. Dies wird im folgenden Beispiel gezeigt:

Span<int> storage = stackalloc int[10];


int num = 0;
foreach (ref int item in storage)
{
item = num++;
}
foreach (ref readonly var item in storage)
{
Console.Write($"{item} ");
}
// Output:
// 0 1 2 3 4 5 6 7 8 9

Wenn die foreach -Anweisung auf null angewendet wird, wird NullReferenceException ausgelöst. Falls die
Quellsammlung der foreach -Anweisung leer ist, wird der Rumpf der foreach -Anweisung nicht ausgeführt
und übersprungen.
await foreach
Ab C# 8.0 können Sie die Anweisung await foreach verwenden, um einen asynchronen Datenstrom zu
verarbeiten, also den Sammlungstyp, der die Schnittstelle IAsyncEnumerable<T> implementiert. Jede Iteration
der Schleife kann unterbrochen werden, während das nächste Element asynchron abgerufen wird. Im folgenden
Beispiel wird veranschaulicht, wie Sie die Anweisung await foreach verwenden:

await foreach (var item in GenerateSequenceAsync())


{
Console.WriteLine(item);
}

Sie können die await foreach -Anweisung auch mit einer Instanz eines beliebigen Typs verwenden, der die
folgenden Bedingungen erfüllt:
Ein Typ hat die öffentliche parameterlose GetAsyncEnumerator -Methode. Diese Methode kann die
Erweiterungsmethode eines Typs sein.
Der Rückgabetyp der GetAsyncEnumerator -Methode hat die öffentliche Current -Eigenschaft und die
öffentliche parameterlose MoveNextAsync -Methode, deren Rückgabetyp Task<bool> , ValueTask<bool> oder
ein beliebiger anderer awaitable-Typ ist, dessen GetResult -Methode des „awaiter “-Elements einen bool -
Wert zurückgibt.
Standardmäßig werden Streamelemente im erfassten Kontext verarbeitet. Wenn Sie die Erfassung des Kontexts
deaktivieren möchten, verwenden Sie die Erweiterungsmethode
TaskAsyncEnumerableExtensions.ConfigureAwait. Weitere Informationen über Synchronisierungskontexte und
die Erfassung des aktuellen Kontexts finden Sie im Artikel Verwenden des aufgabenbasierten asynchronen
Musters. Weitere Informationen finden Sie im Abschnitt Asynchrone Datenströme des Artikels Neues in C# 8.0.
Typ einer Iterationsvariablen
Sie können das Schlüsselwort var verwenden, damit der Compiler den Typ einer Iterationsvariablen in der
foreach -Anweisung ableiten kann. Dies wird im folgenden Code gezeigt:

foreach (var item in collection) { }

Sie können auch wie im folgenden Code explizit den Typ einer Iterationsvariablen angeben:

IEnumerable<T> collection = new T[5];


foreach (V item in collection) { }

Im obigen Formular muss der Typ T eines Sammlungselements implizit oder explizit in Typ V einer
Iterationsvariablen konvertierbar sein. Wenn eine explizite Konvertierung von T in V zur Laufzeit fehlschlägt,
löst die Anweisung foreach eine InvalidCastException aus. Wenn T z. B. ein nicht versiegelter Klassentyp ist,
kann V ein beliebiger Schnittstellentyp sein – sogar der Typ, den T nicht implementiert. Zur Laufzeit kann der
Typ eines Sammlungselements der Typ sein, der von T abgeleitet wird und V implementiert. Wenn dies nicht
der Fall ist, wird eine InvalidCastException ausgelöst.

Die Anweisung do
Die Anweisung do führt eine Anweisung oder einen Anweisungsblock aus, während ein angegebener
boolescher Ausdruck true ergibt. Da der Ausdruck nach jeder Ausführung der Schleife ausgewertet wird, wird
eine do -Schleife mindestens einmal ausgeführt. Dies unterscheidet sich von einer while-Schleife, die entweder
nie oder mehrmals ausgeführt wird.
Im folgenden Beispiel wird die Verwendung der do -Anweisung veranschaulicht:

int n = 0;
do
{
Console.Write(n);
n++;
} while (n < 5);
// Output:
// 01234

Die Anweisung while


Die Anweisung while führt eine Anweisung oder einen Anweisungsblock aus, während ein angegebener
boolescher Ausdruck true ergibt. Da der Ausdruck vor jeder Ausführung der Schleife ausgewertet wird, wird
eine while -Schleife entweder nie oder mehrmals ausgeführt. Dies unterscheidet sich von der do-Schleife, die
einmal oder mehrmals ausgeführt wird.
Im folgenden Beispiel wird die Verwendung der while -Anweisung veranschaulicht:

int n = 0;
while (n < 5)
{
Console.Write(n);
n++;
}
// Output:
// 01234

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Die for -Anweisung
Die foreach -Anweisung
Die do -Anweisung
Die while -Anweisung

Weitere Informationen zu in C# 8.0 und höher eingeführten Features finden Sie in den folgenden
Featurevorschlägen:
Asynchrone Datenströme (C# 8.0)
Unterstützung für die Erweiterung GetEnumerator in foreach -Schleifen (C# 9.0)

Weitere Informationen
C#-Referenz
Verwenden von foreach mit Arrays
Iteratoren
Auswahlanweisungen (C#-Referenz)
04.11.2021 • 5 minutes to read

Mit den folgenden Anweisungen werden Anweisungen aus einer Reihe möglicher Anweisungen basierend auf
dem Wert eines Ausdrucks zur Ausführung ausgewählt:
Die if -Anweisung wählt eine Anweisung basierend auf dem Wert eines booleschen Ausdrucks zur
Ausführung aus.
Die switch -Anweisung wählt eine Anweisungsliste basierend auf einer Musterübereinstimmung mit einem
Ausdruck zur Ausführung aus.

Die Anweisung if
Eine if -Anweisung kann eine der folgenden beiden Formen haben:
Eine if -Anweisung mit einem else -Teil wählt eine der beiden auszuführenden Anweisungen basierend
auf dem Wert eines booleschen Ausdrucks aus, wie im folgenden Beispiel gezeigt:

DisplayWeatherReport(15.0); // Output: Cold.


DisplayWeatherReport(24.0); // Output: Perfect!

void DisplayWeatherReport(double tempInCelsius)


{
if (tempInCelsius < 20.0)
{
Console.WriteLine("Cold.");
}
else
{
Console.WriteLine("Perfect!");
}
}

Eine if -Anweisung ohne else -Teil führt ihren Text nur aus, wenn ein boolescher Ausdruck als true
ausgewertet wird, wie im folgenden Beispiel gezeigt:

DisplayMeasurement(45); // Output: The measurement value is 45


DisplayMeasurement(-3); // Output: Warning: not acceptable value! The measurement value is -3

void DisplayMeasurement(double value)


{
if (value < 0 || value > 100)
{
Console.Write("Warning: not acceptable value! ");
}

Console.WriteLine($"The measurement value is {value}");


}

Sie können if -Anweisungen schachteln, um mehrere Bedingungen zu überprüfen, wie im folgenden Beispiel
gezeigt:
DisplayCharacter('f'); // Output: A lowercase letter: f
DisplayCharacter('R'); // Output: An uppercase letter: R
DisplayCharacter('8'); // Output: A digit: 8
DisplayCharacter(','); // Output: Not alphanumeric character: ,

void DisplayCharacter(char ch)


{
if (char.IsUpper(ch))
{
Console.WriteLine($"An uppercase letter: {ch}");
}
else if (char.IsLower(ch))
{
Console.WriteLine($"A lowercase letter: {ch}");
}
else if (char.IsDigit(ch))
{
Console.WriteLine($"A digit: {ch}");
}
else
{
Console.WriteLine($"Not alphanumeric character: {ch}");
}
}

In einem Ausdruckskontext können Sie mit dem bedingten Operator ?: einen der beiden Ausdrücke auf dem
Wert eines booleschen Ausdrucks basierend auswerten.

Die Anweisung switch


Die switch -Anweisung wählt eine Anweisungsliste basierend auf einer Musterübereinstimmung mit einem
Vergleichsausdruck zur Ausführung aus, wie das folgende Beispiel zeigt:

DisplayMeasurement(-4); // Output: Measured value is -4; too low.


DisplayMeasurement(5); // Output: Measured value is 5.
DisplayMeasurement(30); // Output: Measured value is 30; too high.
DisplayMeasurement(double.NaN); // Output: Failed measurement.

void DisplayMeasurement(double measurement)


{
switch (measurement)
{
case < 0.0:
Console.WriteLine($"Measured value is {measurement}; too low.");
break;

case > 15.0:


Console.WriteLine($"Measured value is {measurement}; too high.");
break;

case double.NaN:
Console.WriteLine("Failed measurement.");
break;

default:
Console.WriteLine($"Measured value is {measurement}.");
break;
}
}

Im vorherigen Beispiel nutzt eine switch -Anweisung die folgenden Muster:


Ein relationales Muster: um ein Ausdrucksergebnis mit einer Konstante zu vergleichen.
Ein Konstantenmuster: um zu testen, ob ein Ausdrucksergebnis einer Konstanten entspricht.

IMPORTANT
Informationen zu den von der switch -Anweisung unterstützten Mustern finden Sie unter Muster.

Im vorherigen Beispiel wird auch der default -Fall veranschaulicht. Der default -Fall gibt Anweisungen an, die
ausgeführt werden, wenn ein Vergleichsausdruck nicht mit einem anderen Fallmuster übereinstimmt. Wenn ein
Vergleichsausdruck keinem Fallmuster entspricht und kein default -Fall vorliegt, verlässt die Steuerung eine
switch -Anweisung.

Eine switch -Anweisung führt die Anweisungsliste im ersten Switch-Abschnitt aus, dessen Fallmuster mit einem
Vergleichsausdruck übereinstimmt und dessen Case Guard (falls vorhanden) als ausgewertet true wird. Eine
switch -Anweisung wertet Fallmuster in Textreihenfolge von oben nach unten aus. Der Compiler generiert
einen Fehler, wenn eine switch -Anweisung einen nicht erreichbaren Fall enthält. Dies ist ein Fall, der bereits von
einem übergeordneten Fall behandelt wird, oder dessen Muster nicht übereinstimmen kann.

NOTE
Der default -Fall kann an beliebiger Stelle in der switch -Anweisung auftreten. Unabhängig von seiner Position wird
der default -Fall immer zuletzt ausgewertet und nur, wenn kein anderes Fallmuster übereinstimmt.

Sie können mehrere Fallmuster für einen Abschnitt einer switch -Anweisung angeben, wie im folgenden
Beispiel gezeigt:

DisplayMeasurement(-4); // Output: Measured value is -4; out of an acceptable range.


DisplayMeasurement(50); // Output: Measured value is 50.
DisplayMeasurement(132); // Output: Measured value is 132; out of an acceptable range.

void DisplayMeasurement(int measurement)


{
switch (measurement)
{
case < 0:
case > 100:
Console.WriteLine($"Measured value is {measurement}; out of an acceptable range.");
break;

default:
Console.WriteLine($"Measured value is {measurement}.");
break;
}
}

Innerhalb einer switch -Anweisung kann die Steuerung nicht von einem Switch-Abschnitt zum nächsten
wechseln. Wie die Beispiele in diesem Abschnitt zeigen, verwenden Sie die break -Anweisung in der Regel am
Ende jedes Switch-Abschnitts, um die Steuerung aus einer switch -Anweisung heraus zu übergeben. Sie können
auch die Anweisungen return und throw verwenden, um die Steuerung aus einer switch -Anweisung heraus zu
übergeben. Um das Verhalten beim Verlassen zu imitieren und die Steuerung an einen anderen Switch-
Abschnitt zu übergeben, können Sie die goto -Anweisung verwenden.
In einem Ausdruckskontext können Sie den switch -Ausdruck verwenden, um einen einzelnen Ausdruck aus
einer Liste mit möglichen Ausdrücken auf Grundlage eines Musterabgleichs mit einem Ausdruck auszuwerten.
Case Guards
Ein Fallmuster ist möglicherweise nicht ausdrucksstark genug, um die Bedingung für die Ausführung des
Switch-Abschnitts anzugeben. In so einem Fall können Sie einen Case Guard verwenden. Dabei handelt es sich
um eine zusätzliche Bedingung, die zusammen mit einem übereinstimmenden Muster erfüllt werden muss. Ein
Case Guard muss ein boolescher Ausdruck sein. Sie geben einen Case Guard nach dem when -Schlüsselwort an,
das einem Muster folgt. Sehen Sie sich dazu das folgende Beispiel an:

DisplayMeasurements(3, 4); // Output: First measurement is 3, second measurement is 4.


DisplayMeasurements(5, 5); // Output: Both measurements are valid and equal to 5.

void DisplayMeasurements(int a, int b)


{
switch ((a, b))
{
case (> 0, > 0) when a == b:
Console.WriteLine($"Both measurements are valid and equal to {a}.");
break;

case (> 0, > 0):


Console.WriteLine($"First measurement is {a}, second measurement is {b}.");
break;

default:
Console.WriteLine("One or both measurements are not valid.");
break;
}
}

Im vorherigen Beispiel werden Positionsmuster mit geschachtelten relationalen Mustern verwendet.


Sprachversionsunterstützung
Die switch -Anweisung unterstützt den Musterabgleich ab C# 7.0.
In C# 6 und früher verwenden Sie die switch -Anweisung mit den folgenden Einschränkungen:
Ein Vergleichsausdruck muss einer der folgenden Typen sein: char, string, bool, ein integraler numerischer
Typ oder ein enum-Typ.
In case -Bezeichnungen sind nur konstante Ausdrücke zulässig.

C#-Sprachspezifikation
Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:
Die if -Anweisung
Die switch -Anweisung

Weitere Informationen zu in C# 7.0 und höher eingeführten Features finden Sie in den folgenden
Featurevorschlägen:
Switch-Anweisung (Musterabgleich für C# 7.0)

Siehe auch
C#-Referenz
Bedingter Operator ?:
Logische Operatoren
Muster
switch -Ausdruck
C#-Sonderzeichen
04.11.2021 • 2 minutes to read

Sonderzeichen sind vordefinierte, kontextbezogene Zeichen, die das Programmelement ändern, dem sie
vorangestellt sind. Bei dem Programmelement kann es sich um eine Literalzeichenfolge, einen Bezeichner oder
einen Attributnamen handeln. C# unterstützt die folgenden Sonderzeichen:
@, das Zeichen für ausführliche Bezeichner.
$, das Zeichen für interpolierte Zeichenfolgen.
Dieser Abschnitt enthält nur die Token, die keine Operatoren sind. Informationen zu allen Operatoren finden Sie
im Abschnitt zu Operatoren.

Weitere Informationen
C#-Referenz
C#-Programmierhandbuch
$ – Zeichenfolgeninterpolation (C#-Referenz)
04.11.2021 • 5 minutes to read

Das Sonderzeichen $ kennzeichnet ein Zeichenfolgenliteral als interpolierte Zeichenfolge. Eine interpolierte
Zeichenfolge ist ein Zeichenfolgenliteral, das möglicherweise Interpolationsausdrücke enthält. Wenn eine
interpolierte Zeichenfolge in eine Ergebniszeichenfolge aufgelöst wird, werden Elemente mit
Interpolationsausdrücken durch die Zeichenfolgendarstellungen der Ausdrucksergebnisse ersetzt. Dieses
Feature ist ab C# 6 verfügbar.
Die Zeichenfolgeninterpolation bietet eine lesbarere, benutzerfreundliche Syntax zum Formatieren von
Zeichenfolgen. Sie ist einfacher zu lesen als die zusammengesetzte Formatierung von Zeichenfolgen. Im
folgenden Beispiel wird mit beiden Features die gleiche Ausgabe erzeugt:

string name = "Mark";


var date = DateTime.Now;

// Composite formatting:
Console.WriteLine("Hello, {0}! Today is {1}, it's {2:HH:mm} now.", name, date.DayOfWeek, date);
// String interpolation:
Console.WriteLine($"Hello, {name}! Today is {date.DayOfWeek}, it's {date:HH:mm} now.");
// Both calls produce the same output that is similar to:
// Hello, Mark! Today is Wednesday, it's 19:40 now.

Struktur einer interpolierten Zeichenfolge


Wenn Sie ein Zeichenfolgenliteral als interpolierte Zeichenfolge ermitteln möchten, stellen Sie ihm ein $ -
Symbol voran. Zwischen $ und " am Anfang des Zeichenfolgenliterals dürfen sich keine Leerzeichen
befinden.
Die Struktur eines Elements mit einem Interpolationsausdruck sieht wie folgt aus:

{<interpolationExpression>[,<alignment>][:<formatString>]}

Elemente in eckigen Klammern sind optional. In der folgenden Tabelle wird jedes Element beschrieben:

EL EM EN T B ESC H REIB UN G

interpolationExpression Der Ausdruck, der zu einem Ergebnis führt, das formatiert


werden soll. Die Zeichenfolgendarstellung von null lautet
String.Empty.

alignment Der konstante Ausdruck, dessen Wert die Mindestanzahl


von Zeichen in der Zeichenfolgendarstellung des
Ausdrucksergebnisses definiert. Bei einem positiven Wert
wird die Zeichenfolge rechtsbündig ausgerichtet. Ist der
Wert negativ, wird sie linksbündig ausgerichtet. Weitere
Informationen finden Sie unter Ausrichtungskomponente.
EL EM EN T B ESC H REIB UN G

formatString Eine Formatierungszeichenfolge oder benutzerdefinierte


Formatierungszeichenfolge, die durch den Typ des
Ausdrucksergebnisses unterstützt wird. Weitere
Informationen finden Sie unter Formatzeichenfolgen-
Komponente.

Im folgenden Beispiel werden die oben beschriebenen Formatierungskomponenten verwendet:

Console.WriteLine($"|{"Left",-7}|{"Right",7}|");

const int FieldWidthRightAligned = 20;


Console.WriteLine($"{Math.PI,FieldWidthRightAligned} - default formatting of the pi number");
Console.WriteLine($"{Math.PI,FieldWidthRightAligned:F3} - display only three decimal digits of the pi
number");
// Expected output is:
// |Left | Right|
// 3.14159265358979 - default formatting of the pi number
// 3.142 - display only three decimal digits of the pi number

Ab C# 10 können Sie mithilfe der Zeichenfolgeninterpolation eine konstante Zeichenfolge initialisieren. Alle für
Platzhalter verwendeten Ausdrücke müssen konstante Zeichenfolgen sein. Mit anderen Worten: Jeder
Interpolationsausdruck muss eine Zeichenfolge und zudem eine Kompilierzeitkonstante sein.

Sonderzeichen
Wenn eine geschweifte Klammer („{“ oder „}“) im Text angezeigt werden soll, der durch die interpolierte
Zeichenfolge erstellt wird, müssen Sie zwei geschweifte Klammern verwenden, also „{{“ oder „}}“. Weitere
Informationen finden Sie unter Versehen von geschweiften Klammern mit Escapezeichen.
Da der Doppelpunkt („:“) in einem Element eines Interpolationsausdrucks eine besondere Funktion einnimmt,
müssen Sie zur Verwendung eines Bedingungsoperators in einem Interpolationsausdruck diesen Ausdruck in
runde Klammern einschließen.
Im folgenden Beispiel wird gezeigt, wie Sie eine runde Klammer in eine Ergebniszeichenfolge einschließen.
Außerdem wird die Verwendung eines bedingten Operators gezeigt:

string name = "Horace";


int age = 34;
Console.WriteLine($"He asked, \"Is your name {name}?\", but didn't wait for a reply :-{{");
Console.WriteLine($"{name} is {age} year{(age == 1 ? "" : "s")} old.");
// Expected output is:
// He asked, "Is your name Horace?", but didn't wait for a reply :-{
// Horace is 34 years old.

Ausführliche interpolierte Zeichenfolgen beginnen mit dem Zeichen $ , gefolgt vom Zeichen @ . Weitere
Informationen zu ausführlichen Zeichenfolgen finden Sie in den Artikeln zu Zeichenfolgen und ausführlichen
Bezeichnern.

NOTE
Ab C# 8.0 können Sie die Token $ und @ in beliebiger Reihenfolge verwenden: Sowohl $@"..." als auch @$"..."
sind gültige interpolierte ausführliche Zeichenfolgen. In früheren C#-Versionen musste das Token $ vor dem Token @
vorhanden sein.
Implizite Konvertierungen und Angeben der IFormatProvider -
Implementierung
Es gibt drei implizite Konvertierungen aus einer interpolierten Zeichenfolge:
1. Die Konvertierung einer interpolierten Zeichenfolge in eine String-Instanz. Die Zeichenfolge ist das
Ergebnis der Auflösung der interpolierten Zeichenfolge. Alle Elemente eines Interpolationsausdrucks
werden durch die ordnungsgemäß formatierten Zeichenfolgendarstellungen ihrer Ergebnisse ersetzt.
Diese Konvertierung verwendet CurrentCulture zum Formatieren von Ausdrucksergebnissen.
2. Konvertierung einer interpolierten Zeichenfolge in eine FormattableString-Instanz, die eine
zusammengesetzte Formatzeichenfolge mit den zu formatierenden Ausdrucksergebnissen darstellt.
Dadurch können Sie aus einer einzigen FormattableString-Instanz mehrere Ergebniszeichenfolgen mit
kulturspezifischem Inhalt erstellen. Rufen Sie hierzu eine der folgenden Methoden auf:
Eine Überladung von ToString(), die eine Ergebniszeichenfolge für das CurrentCulture-Element
erzeugt.
Eine Invariant-Methode, die eine Ergebniszeichenfolge für das InvariantCulture-Element erzeugt.
Eine ToString(IFormatProvider)-Methode, die eine Ergebniszeichenfolge für eine bestimmte Kultur
erzeugt.
ToString(IFormatProvider) bietet eine benutzerdefinierte Implementierung der IFormatProvider-
Schnittstelle, die das benutzerdefinierte Formatieren unterstützt. Weitere Informationen finden Sie im
Abschnitt Benutzerdefinierte Formatierung mit ICustomFormatter des Artikels Formatieren von Typen in
.NET.
3. Konvertierung einer interpolierten Zeichenfolge in eine IFormattable-Instanz, die Ihnen das Erstellen
mehrerer Ergebniszeichenfolgen mit kulturspezifischem Inhalt aus einer einzigen IFormattable-Instanz
ermöglicht.
Im folgenden Beispiel wird die implizite Konvertierung in FormattableString zum Erstellen kulturspezifischer
Ergebniszeichenfolgen verwendet:

double speedOfLight = 299792.458;


FormattableString message = $"The speed of light is {speedOfLight:N3} km/s.";

System.Globalization.CultureInfo.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("nl-NL");
string messageInCurrentCulture = message.ToString();

var specificCulture = System.Globalization.CultureInfo.GetCultureInfo("en-IN");


string messageInSpecificCulture = message.ToString(specificCulture);

string messageInInvariantCulture = FormattableString.Invariant(message);

Console.WriteLine($"{System.Globalization.CultureInfo.CurrentCulture,-10} {messageInCurrentCulture}");
Console.WriteLine($"{specificCulture,-10} {messageInSpecificCulture}");
Console.WriteLine($"{"Invariant",-10} {messageInInvariantCulture}");
// Expected output is:
// nl-NL The speed of light is 299.792,458 km/s.
// en-IN The speed of light is 2,99,792.458 km/s.
// Invariant The speed of light is 299,792.458 km/s.

Zusätzliche Ressourcen
Wenn Sie noch nicht mit der Zeichenfolgeninterpolation vertraut sind, finden Sie weitere Informationen im
interaktiven Tutorial Zeichenfolgeninterpolation in C#. Sie können sich auch ein weiteres Tutorial zur
Zeichenfolgeninterpolation in C# ansehen. Dieses Tutorial demonstriert die Verwendung interpolierter
Zeichenfolgen zum Erzeugen formatierter Zeichenfolgen.
Kompilierung interpolierter Zeichenfolgen
Wenn eine interpolierte Zeichenfolge vom Typ string ist, wird sie in der Regel in einen String.Format-
Methodenaufruf transformiert. Der Compiler ersetzt String.Concat möglicherweise mit einem String.Format,
wenn das analysierte Verhalten mit der Verkettung übereinstimmt.
Wenn eine interpolierte Zeichenfolge vom Typ IFormattable oder FormattableString ist, generiert der Compiler
einen Aufruf der FormattableStringFactory.Create-Methode.
Ab C# 10.0 überprüft der Compiler bei Verwendung einer interpolierten Zeichenfolge, ob die interpolierte
Zeichenfolge einem Typ zugewiesen ist, der das Muster von Handlern für interpolierte Zeichenfolgen erfüllt. Ein
Handler für interpolierte Zeichenfolgen ist ein benutzerdefinierter Typ, der die interpolierte Zeichenfolge in eine
Zeichenfolge konvertiert. Ein Handler für interpolierte Zeichenfolgen stellt ein erweitertes Szenario dar, das in
der Regel aus Leistungsgründen verwendet wird. Informationen zu den Anforderungen zum Erstellen eines
Handlers für interpolierte Zeichenfolgen finden Sie in der Sprachspezifikation für Verbesserungen interpolierter
Zeichenfolgen. Im Tutorial zu Handlern für interpolierte Zeichenfolgen können Sie einen Handler im Abschnitt
„Neuerungen in C#“ erstellen. Wenn Sie in .NET 6.0 eine interpolierte Zeichenfolge für ein Argument vom Typ
string verwenden, wird die interpolierte Zeichenfolge vom
System.Runtime.CompilerServices.DefaultInterpolatedStringHandler verarbeitet.

NOTE
Ein Nebeneffekt von Handlern für interpolierte Zeichenfolgen besteht darin, dass ein benutzerdefinierter Handler,
einschließlich System.Runtime.CompilerServices.DefaultInterpolatedStringHandler, nicht immer alle Ausdrücke auswerten
kann, die als Platzhalter in der interpolierten Zeichenfolge verwendet werden. Das bedeutet, dass in diesen Ausdrücken
keine Nebeneffekte auftreten können.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Abschnitt Interpolierte Zeichenfolgen der C#-Sprachspezifikation.

Siehe auch
C#-Referenz
C#-Sonderzeichen
Zeichenfolgen
Standardmäßige Zahlenformatzeichenfolgen
Kombinierte Formatierung
String.Format
@ (C#-Referenz)
04.11.2021 • 2 minutes to read

Das Sonderzeichen @ dient als ausführlicher Bezeichner. Er wird wie folgt verwendet:
1. Zum Aktivieren von C#-Schlüsselwörtern, die als Bezeichner verwendet werden sollen. Das Zeichen @
steht vor einem Codeelement, das der Compiler als Bezeichner und nicht als C#-Schlüsselwort
interpretieren soll. Im folgenden Beispiel wird das Zeichen @ zum Definieren eines Bezeichners mit dem
Namen for verwendet, der in einer for -Schleife verwendet wird.

string[] @for = { "John", "James", "Joan", "Jamie" };


for (int ctr = 0; ctr < @for.Length; ctr++)
{
Console.WriteLine($"Here is your gift, {@for[ctr]}!");
}
// The example displays the following output:
// Here is your gift, John!
// Here is your gift, James!
// Here is your gift, Joan!
// Here is your gift, Jamie!

2. Zum Angeben, dass ein Zeichenfolgenliteral wörtlich interpretiert werden soll. Das Zeichen @ in dieser
Instanz definiert ein ausführliches Zeichenfolgenliteral. Einfache Escapesequenzen (z.B. "\\" für einen
umgekehrten Schrägstrich), Escapesequenzen für Hexadezimalzahlen (z.B. "\x0041" für ein groß
geschriebenes A) und Escapesequenzen für Unicodezeichen (wie z.B. "\u0041" für ein groß
geschriebenes A) werden wörtlich interpretiert. Nur eine Escapesequenz für Anführungszeichen ( "" )
wird nicht wörtlich interpretiert, sie erzeugt ein doppeltes Anführungszeichen. Bei einer ausführlichen
interpolierten Zeichenfolge werden darüber hinaus Klammern als Escapesequenzen ( {{ und }} ) nicht
wörtlich interpretiert. In diesem Fall werden einfache geschweifte Klammerzeichen erzeugt. Im folgenden
Beispiel werden zwei identische Dateipfade definiert – einer durch Verwendung eines regulären
Zeichenfolgenliterals und der andere durch ein ausführliches Zeichenfolgenliteral. Dies ist einer der
häufigeren Verwendungsarten von ausführlichen Zeichenfolgenliteralen.

string filename1 = @"c:\documents\files\u0066.txt";


string filename2 = "c:\\documents\\files\\u0066.txt";

Console.WriteLine(filename1);
Console.WriteLine(filename2);
// The example displays the following output:
// c:\documents\files\u0066.txt
// c:\documents\files\u0066.txt

Im folgenden Beispiel werden die Auswirkungen des Definierens eines regulären Zeichenfolgenliterals
und eines ausführlichen Zeichenfolgenliterals mit identischen Zeichensequenzen dargestellt.
string s1 = "He said, \"This is the last \u0063hance\x0021\"";
string s2 = @"He said, ""This is the last \u0063hance\x0021""";

Console.WriteLine(s1);
Console.WriteLine(s2);
// The example displays the following output:
// He said, "This is the last chance!"
// He said, "This is the last \u0063hance\x0021"

3. Zum Zulassen, dass der Compiler im Fall eines Namenskonflikts zwischen Attributen unterscheiden kann.
Ein Attribut ist eine von Attribute abgeleitete Klasse. Der Name des Typs enthält normalerweise das Suffix
Attribute , obwohl der Compiler diese Konvention nicht erzwingt. Auf das Attribut kann dann im Code
entweder über den vollständigen Typnamen (z.B. [InfoAttribute] ) oder über den gekürzten Namen (z.B.
[Info] ) verwiesen werden. Allerdings tritt ein Namenskonflikt auf, wenn zwei gekürzte Typnamen des
Attributs identisch sind und ein Typname das Suffix Attribute enthält, der andere jedoch nicht. Der
folgende Code kann z.B. nicht kompiliert werden, da der Compiler nicht bestimmen kann, ob das Attribut
Info oder InfoAttribute auf die Klasse Example angewendet wird. Weitere Informationen finden Sie
unter CS1614.

using System;

[AttributeUsage(AttributeTargets.Class)]
public class Info : Attribute
{
private string information;

public Info(string info)


{
information = info;
}
}

[AttributeUsage(AttributeTargets.Method)]
public class InfoAttribute : Attribute
{
private string information;

public InfoAttribute(string info)


{
information = info;
}
}

[Info("A simple executable.")] // Generates compiler error CS1614. Ambiguous Info and InfoAttribute.
// Prepend '@' to select 'Info' ([@Info("A simple executable.")]). Specify the full name
'InfoAttribute' to select it.
public class Example
{
[InfoAttribute("The entry point.")]
public static void Main()
{
}
}

Siehe auch
C#-Referenz
C#-Programmierhandbuch
C#-Sonderzeichen
Reservierte Attribute: Attribute auf Assemblyebene
04.11.2021 • 2 minutes to read

Die meisten Attribute werden auf spezifische Sprachelemente wie Klassen oder Methoden angewendet. Einige
Attribute sind jedoch global – sie gelten für eine gesamte Assembly oder ein Modul. Zum Beispiel kann das
AssemblyVersionAttribute-Attribut zum Einbetten von Versionsinformationen in eine Assembly verwendet
werden. Diese sieht wie folgt aus:

[assembly: AssemblyVersion("1.0.0.0")]

Globale Attribute werden im Quellcode nach allen using -Direktiven der obersten Ebene und vor Typ-, Modul-
oder Namespacedeklarationen angezeigt. Globale Attribute können in mehreren Quelldateien auftreten, jedoch
müssen die Dateien in einem einzigen Kompilierungsdurchlauf kompiliert werden. Visual Studio fügt der Datei
„AssemblyInfo.cs“ in .NET Framework-Projekten globale Attribute hinzu. Diese Attribute werden nicht zu .NET
Core-Projekten hinzugefügt.
Assemblyattribute sind Werte, die Informationen zu einer Assembly bereitstellen. Sie werden in die folgenden
Kategorien eingeteilt:
Attribute für Assemblyidentitäten
Informationsattribute
Attribute für Assemblymanifeste.

Attribute für Assemblyidentitäten


Drei Attribute bestimmen mit einem starken Namen (falls zutreffend) die Identität einer Assembly: „name“,
„version“ und „culture“. Diese Attribute bilden den vollständigen Namen der Assembly und sind erforderlich,
wenn im Code auf sie verwiesen wird. Mit Attributen können die Version und Kultur einer Assembly festgelegt
werden. Allerdings wird der name-Wert bei Erstellung der Assembly vom Compiler, der Visual Studio-IDE im
Dialogfeld „Assemblyinformationen“ oder dem Assemblylinker (Al.exe) festgelegt. Der Assemblyname basiert
auf dem Assemblymanifest. Das Attribut AssemblyFlagsAttribute gibt an, ob mehrere Kopien der Assembly
parallel bestehen können.
In der folgenden Tabelle werden die Identitätsattribute aufgeführt.

AT T RIB UT Z W EC K

AssemblyVersionAttribute Gibt die Version einer Assembly an

AssemblyCultureAttribute Gibt an, welche Kultur die Assembly unterstützt.

AssemblyFlagsAttribute Gibt an, ob eine Assembly die parallele Ausführung auf


demselben Computer, im selben Prozess oder in derselben
Anwendungsdomäne unterstützt

Informationsattribute
Sie verwenden Informationsattribute, um zusätzliche Unternehmens- oder Produktinformationen für eine
Assembly bereitzustellen. Die folgende Tabelle zeigt die Informationsattribute, die im Namespace
System.Reflection definiert werden.
AT T RIB UT Z W EC K

AssemblyProductAttribute Gibt einen Produktnamen für ein Assemblymanifest an.

AssemblyTrademarkAttribute Gibt eine Marke für ein Assemblymanifest an.

AssemblyInformationalVersionAttribute Gibt Versionsinformationen für ein Assemblymanifest an.

AssemblyCompanyAttribute Gibt einen Unternehmensnamen für ein Assemblymanifest


an.

AssemblyCopyrightAttribute Definiert ein benutzerdefiniertes Attribut, das ein Copyright


für ein Assemblymanifest angibt

AssemblyFileVersionAttribute Legt eine bestimmte Versionsnummer für die Win32-


Dateiversionsressource fest.

CLSCompliantAttribute Gibt an, ob die Assembly mit der Common Language


Specification (CLS) kompatibel ist

Attribute für Assemblymanifeste.


Sie können Attribute für Assemblymanifeste verwenden, um Informationen im Assemblymanifest
bereitzustellen. Dies schließt Attribute für Titel, Beschreibung, Standardalias und Konfiguration ein. Die folgende
Tabelle zeigt die Attribute für Assemblymanifeste, die im Namespace System.Reflection definiert werden.

AT T RIB UT Z W EC K

AssemblyTitleAttribute Gibt einen Assemblytitel für ein Assemblymanifest an.

AssemblyDescriptionAttribute Gibt eine Assemblybeschreibung für ein Assemblymanifest


an.

AssemblyConfigurationAttribute Gibt eine Assemblykonfiguration (z. B. „Einzelhandel“ oder


„Debug“) für ein Assemblymanifest an.

AssemblyDefaultAliasAttribute Definiert einen benutzerfreundlichen Standardalias für ein


Assemblymanifest
Reservierte Attribute: Bestimmen von
Aufruferinformationen
04.11.2021 • 2 minutes to read

Mithilfe der Informationsattribute können Sie Informationen zum Aufrufer einer Methode abrufen. Sie können
den Dateipfad des Quellcodes, die Zeilennummer im Quellcode und den Membernamen des Aufrufers abrufen.
Um diese Memberaufruferinformationen zu erhalten, verwenden Sie die Attribute, die auf optionale Parameter
angewendet werden. Jeder optionale Parameter gibt einen Standardwert an. In der folgenden Tabelle sind die
Aufrufer-Informationsattribute angegeben, die im System.Runtime.CompilerServices-Namespace definiert sind:

AT T RIB UT B ESC H REIB UN G TYP

CallerFilePathAttribute Vollständiger Pfad der Quelldatei, die String


den Aufrufer enthält. Der vollständige
Pfad zum Zeitpunkt der Kompilierung.

CallerLineNumberAttribute Zeilennummer in der Quelldatei, in der Integer


die Methode aufgerufen wird

CallerMemberNameAttribute Der Methoden- oder String


Eigenschaftenname des Aufrufers

Diese Informationen sind für Ablaufverfolgung, Debuggen und zum Erstellen von Diagnosetools sehr nützlich.
Im folgenden Beispiel wird die Verwendung der Aufruferinformationsattribute für Aufrufer veranschaulicht. Bei
jedem Aufruf der TraceMessage -Methode werden die Aufruferinformationen als Argumente für optionale
Parameter ersetzt.

public void DoProcessing()


{
TraceMessage("Something happened.");
}

public void TraceMessage(string message,


[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Trace.WriteLine("message: " + message);
Trace.WriteLine("member name: " + memberName);
Trace.WriteLine("source file path: " + sourceFilePath);
Trace.WriteLine("source line number: " + sourceLineNumber);
}

// Sample Output:
// message: Something happened.
// member name: DoProcessing
// source file path: c:\Visual Studio Projects\CallerInfoCS\CallerInfoCS\Form1.cs
// source line number: 31

Sie geben einen expliziten Standardwert für jeden optionalen Parameter an. Sie können
Aufruferinformationsattribute nicht auf Parameter anwenden, die nicht als optional festgelegt wurden. Durch die
Aufruferinformationsattribute wird ein Parameter nicht optional. Stattdessen beeinflussen sie den Standardwert,
der beim Auslassen des Arguments übergeben wird. Aufruferinformationswerte werden zur Kompilierzeit als
Literale in Intermediate Language (IL) ausgegeben. Im Gegensatz zu den Ergebnissen der StackTrace-Eigenschaft
für Ausnahmen werden die Ergebnisse nicht durch Verbergen beeinflusst. Sie können die optionalen Argumente
explizit angeben, um die Aufruferinformationen zu steuern oder auszublenden.
Membernamen
Sie können das CallerMemberName -Attribut verwenden, um den Membernamen nicht als String -Argument für
die aufgerufene Methode angeben zu müssen. Auf diese Weise umgehen Sie das Problem, dass durch die
Umgestaltung mit Umbenennung die String -Werte nicht geändert werden. Dieser Vorteil ist für die
folgenden Aufgaben besonders hilfreich:
Verwenden der Ablaufverfolgung und der Diagnoseprogramme
Implementieren der INotifyPropertyChanged-Schnittstelle beim Binden von Daten Diese Schnittstelle
ermöglicht es der Eigenschaft eines Objekts, ein gebundenes Steuerelement über die Änderung der
Eigenschaft zu benachrichtigen, damit das Steuerelement die aktualisierten Informationen anzeigen kann.
Ohne das CallerMemberName -Attribut müssen Sie den Eigenschaftennamen als Literal angeben.
Im folgenden Diagramm sind die Membernamen aufgeführt, die beim Verwenden des CallerMemberName -
Attributs zurückgegeben werden.

A UF RUF E ERF O L GEN IN : M EM B ERN A M EN ERGEB N IS

Methode, Eigenschaft oder Ereignis Der Name der Methode, der Eigenschaft oder des
Ereignisses, aus dem bzw. aus der der Aufruf stammt.

Konstruktor Die Zeichenfolge „.ctor “

Statischer Konstruktor Die Zeichenfolge „.cctor “

Finalizer Die Zeichenfolge „Finalize“

Benutzerdefinierte Operatoren oder Konvertierungen Der generierte Name für den Member, beispielsweise
„op_Addition“.

Attributkonstruktor Der Name der Methode oder Eigenschaft, auf die das
Attribut angewendet wird. Wenn das Attribut ein beliebiges
Element in einem Member ist (z. B. ein Parameter, ein
Rückgabewert oder ein generischer Typparameter), wird als
Ergebnis der Name des Members ausgegeben, der diesem
Element zugeordnet ist.

Kein enthaltender Member (z. B. auf Assemblyebene oder Der Standardwert des optionalen Parameters.
Attribute, die auf Typen angewendet werden)

Siehe auch
Benannte und optionale Argumente
System.Reflection
Attribute
Attribute
Attribute für die statische Analyse des NULL-
Zustands
04.11.2021 • 14 minutes to read

In einem Nullable-Kontext führt der Compiler eine statische Analyse des Codes durch, um den NULL-Zustand
aller Verweistypvariablen zu bestimmen:
not-null: Die statische Analyse ermittelt, dass einer Variablen ein Wert ungleich NULL zugewiesen ist.
maybe-null: Die statische Analyse kann nicht ermittelt, ob einer Variablen ein Wert ungleich NULL
zugewiesen ist.
Diese Zustände ermöglichen es dem Compiler, Warnungen bereitzustellen, wenn Sie einen Verweis auf einen
NULL-Wert aufheben und dadurch eine System.NullReferenceException auslösen könnten. Diese Attribute
stellen dem Compiler semantische Informationen zum NULL-Zustand von Argumenten, Rückgabewerten und
Objektmembern basierend auf dem Zustand von Argumenten und Rückgabewerten bereit. Der Compiler gibt
genauere Warnungen an, wenn Ihre APIs ordnungsgemäß mit diesen semantischen Informationen versehen
wurden.
In diesem Artikel werden diese Attribute der Nullable-Verweistypen und ihre Verwendung kurz beschrieben.
Beginnen wir mit einem Beispiel. Angenommen, Ihre Bibliothek verfügt über die folgende API zum Abrufen einer
Ressourcenzeichenfolge. Diese Methode wurde ursprünglich vor C# 8.0 und Nullable-Anmerkungen
geschrieben:

bool TryGetMessage(string key, out string message)


{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}

Das vorstehende Beispiel folgt dem bekannten Try* -Muster in .NET. Für diese API sind zwei Verweisparameter
vorhanden: key und message . In Bezug auf den NULL-Zustand dieser Parameter umfasst diese API die
folgenden Regeln:
Aufrufer dürfen nicht null als Argument für key übergeben.
Aufrufer können eine Variable übergeben, deren Wert null als Argument für message lautet.
Wenn die Methode TryGetMessage den Wert true zurückgibt, ist der Wert von message ungleich NULL.
Wenn der Rückgabewert false, lautet, ist der Wert von message NULL.
Die Regel für key kann in C# 8.0 kurz und bündig ausgedrückt werden: key muss ein Non-Nullable-
Verweistyp sein. Der Parameter message ist komplexer. Er lässt eine Variable mit dem Wert null als Argument
zu, garantiert aber bei einem erfolgreichen Vorgang, dass das out -Argument nicht null ist. Für diese
Szenarien ist ein umfangreicheres Vokabular zum Beschreiben der Erwartungen erforderlich. Das unten
beschriebene Attribut NotNullWhen beschreibt den NULL-Zustand für das Argument, das für den message -
Parameter verwendet wird.
NOTE
Durch das Hinzufügen dieser Attribute erhält der Compiler mehr Informationen über die Regeln für Ihre API. Wenn
Aufrufcode in einem Nullable-aktivierten Kontext kompiliert wird, warnt der Compiler Aufrufer, wenn sie diese Regeln
verletzen. Diese Attribute aktivieren keine zusätzlichen Überprüfungen für Ihre Implementierung.

AT T RIB UT E K AT EGO RIE B EDEUT UN G

AllowNull Precondition Non-Nullable-Parameter, -Felder oder


-Eigenschaften dürfen NULL sein.

DisallowNull Precondition Nullable-Parameter, -Felder oder -


Eigenschaften dürfen niemals NULL
sein.

MaybeNull Nachbedingung Non-Nullable-Parameter, -Felder, -


Eigenschaften oder -Rückgabewerte
dürfen NULL sein.

NotNull Nachbedingung Nullable-Parameter, -Felder, -


Eigenschaften oder -Rückgabewerte
sind niemals NULL.

MaybeNullWhen Bedingte Nachbedingung Ein Non-Nullable-Argument darf NULL


sein, wenn die Methode den
angegebenen bool -Wert zurückgibt.

NotNullWhen Bedingte Nachbedingung Ein Nullable-Argument ist nicht NULL,


wenn die Methode den angegebenen
bool -Wert zurückgibt.

NotNullIfNotNull Bedingte Nachbedingung Rückgabewerte, Eigenschaften oder


Argumente sind nicht NULL, wenn das
Argument für den angegebenen
Parameter nicht NULL ist.

MemberNotNull Hilfsmethoden für Methoden und Der aufgeführte Member ist nicht
Eigenschaften NULL, wenn die Methode die
Rückgabe durchführt.

MemberNotNullWhen Hilfsmethoden für Methoden und Der aufgeführte Member ist nicht
Eigenschaften NULL, wenn die Methode den
angegebenen bool -Wert zurückgibt.

DoesNotReturn Unerreichbarer Code Eine Methode oder Eigenschaft gibt


niemals ein Ergebnis zurück. Anders
ausgedrückt: Die Methode löst immer
eine Ausnahme aus.

DoesNotReturnIf Unerreichbarer Code Diese Methode oder Eigenschaft gibt


niemals ein Ergebnis zurück, wenn der
zugeordnete bool -Parameter den
angegebenen Wert aufweist.

Die vorherigen Beschreibungen sind eine kurze Referenz zur Funktionsweise jedes Attributs. In den folgenden
Abschnitten werden Verhalten und Bedeutung dieser Attribute ausführlicher beschrieben.
Vorbedingungen: AllowNull und DisallowNull
Betrachten Sie eine Eigenschaft mit Lese-/Schreibzugriff, die niemals null zurückgibt, weil sie einen
angemessen Standardwert aufweist. Aufrufer übergeben null an die set-Zugriffsmethode, wenn diese auf den
Standardwert festgelegt wird. Denken Sie zum Beispiel an ein Messagingsystem, das in einem Livechat nach
einem Benutzernamen fragt. Wenn kein Name angegeben wird, erzeugt das System einen zufälligen Namen:

public string ScreenName


{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;

Wenn Sie den vorstehenden Code in einem Kontext ohne Nullable-Beachtung kompilieren, ist alles in Ordnung.
Sobald Sie Nullable-Verweistypen aktivieren, wird die ScreenName -Eigenschaft zu einem Non-Nullable-Verweis.
Dies ist für die get -Zugriffsmethode korrekt: sie gibt nie null zurück. Aufrufer müssen die zurückgegebene
Eigenschaft für null nicht überprüfen. Aber bei Festlegung der Eigenschaft auf null wird jetzt eine Warnung
generiert. Um diese Art von Code zu unterstützen, fügen Sie der Eigenschaft das Attribut
System.Diagnostics.CodeAnalysis.AllowNullAttribute hinzu, wie im folgenden Code gezeigt:

[AllowNull]
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();

Sie müssen möglicherweise eine using -Direktive für System.Diagnostics.CodeAnalysis hinzufügen, um diese
und weitere Attribute zu verwenden, die in diesem Artikel besprochen werden. Das Attribut wird auf die
Eigenschaft angewendet, nicht auf die set -Zugriffsmethode. Das AllowNull -Attribut gibt Vorbedingungen an
und wird nur auf Argumente angewendet. Die get -Zugriffsmethode umfasst einen Rückgabewert, aber keine
Parameter. Deshalb gilt das AllowNull -Attribut nur für die set -Zugriffsmethode.
Das vorangehende Beispiel veranschaulicht, wonach beim Hinzufügen des AllowNull -Attributs für ein
Argument gesucht werden muss:
1. Der allgemeine Vertrag für diese Variable sieht vor, dass sie nicht null sein darf, deshalb wird ein Non-
Nullable-Verweistyp benötigt.
2. Es gibt Szenarien, in denen ein Aufrufer null als Argument übergibt. Diese sind jedoch nicht die Regel.
In den meisten Fällen verwenden Sie dieses Attribut für Eigenschaften oder in -, out - und ref -Argumente.
Das AllowNull -Attribut stellt die beste Wahl dar, wenn eine Variable typischerweise ungleich NULL ist, Sie aber
null als Vorbedingung zulassen müssen.

Vergleichen Sie dies mit Szenarien für die Verwendung von DisallowNull : Mit diesem Attribut legen Sie fest,
dass ein Argument eines Nullwerte zulassenden Verweistyps nicht null sein darf. Nehmen wir als Beispiel eine
Eigenschaft, bei der null der Standardwert ist und Clients die Eigenschaft nur auf einen Wert ungleich NULL
festlegen können. Betrachten Sie folgenden Code:
public string ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;

Der vorstehende Code ist der beste Weg, um in Ihrem Entwurf auszudrücken, dass ReviewComment den Wert
null annehmen könnte, aber nicht auf null festgelegt werden kann. Sobald dieser Code Nullable-fähig ist,
können Sie dieses Konzept für Aufrufer mit System.Diagnostics.CodeAnalysis.DisallowNullAttribute klarer
ausdrücken:

[DisallowNull]
public string? ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;

In einem Nullable-Kontext kann die get -Zugriffsmethode von ReviewComment den Standardwert null
zurückgeben. Der Compiler gibt eine Warnung aus, dass vor dem Zugriff eine Überprüfung erforderlich ist.
Darüber hinaus werden Aufrufer gewarnt, dass der Wert null zwar möglich ist, Aufrufer aber den Wert null
nicht explizit festlegen sollten. Das DisallowNull -Attribut gibt ebenfalls eine Vorbedingung an, es hat keine
Auswirkung auf die get -Zugriffsmethode. Sie verwenden das DisallowNull -Attribut, wenn folgende Punkte
zutreffen:
1. Die Variable könnte in wichtigen Szenarien den Wert null annehmen, häufig bei der ersten Instanziierung.
2. Die Variable darf nicht explizit auf null festgelegt werden.

Diese Situationen sind häufig in Code anzutreffen, in dem NULL-Werte ursprünglich nicht beachtet wurden. Es
kann vorkommen, dass Objekteigenschaften in zwei unterschiedlichen Initialisierungsvorgängen festgelegt
werden. Einige Eigenschaften werden nur festgelegt, nachdem einige asynchrone Vorgänge ausgeführt wurden.
Mit den Attributen AllowNull und DisallowNull können Sie festlegen, dass Vorbedingungen für Variablen nicht
den Nullable-Anmerkungen für diese Variablen entsprechen dürfen. Sie liefern zusätzliche Informationen zu den
Merkmalen Ihrer API. Diese zusätzlichen Informationen helfen Aufrufern, Ihre API korrekt zu verwenden. Wie
bereits erwähnt, geben Sie Vorbedingungen mit den folgenden Attributen an:
AllowNull: Ein Non-Nullable-Argument darf NULL sein.
DisallowNull: Ein Nullwerte zulassendes Argument darf nie NULL sein.

Nachbedingungen: MaybeNull und NotNull


Angenommen, Sie verfügen über eine Methode mit der folgenden Signatur:

public Customer FindCustomer(string lastName, string firstName)

Sie haben wahrscheinlich schon eine Methode wie diese geschrieben, um null zurückzugeben, wenn ein
gesuchter Name nicht gefunden wurde. Durch den Wert null wird klar ausgedrückt, dass der Datensatz nicht
gefunden wurde. In diesem Beispiel ändern Sie den Rückgabetyp wahrscheinlich von Customer in Customer? .
Durch das Deklarieren des Rückgabetyps als Nullable-Verweistyp wird die Absicht dieser API klar ausgedrückt:
public Customer? FindCustomer(string lastName, string firstName)

Aus Gründen, die unter NULL-Zulässigkeit von Generics behandelt werden, führt diese Technik möglicherweise
nicht zu der statischen Analyse, die Ihrer API entspricht. Angenommen, Sie verfügen über eine generische
Methode, die einem ähnlichen Muster folgt:

public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

Die Methode gibt null zurück, wenn das gesuchte Element nicht gefunden wird. Sie können festlegen, dass die
Methode null zurückgibt, wenn ein Element nicht gefunden wird, indem Sie der Methodenrückgabe die
Anmerkung MaybeNull hinzufügen:

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

Der obige Code informiert Aufrufer darüber, dass der Rückgabewert tatsächlich NULL sein darf. Außerdem wird
der Compiler darüber informiert, dass die Methode einen null -Ausdruck zurückgeben kann, obwohl der Typ
keine NULL-Werte zulässt. Wenn Sie über eine generische Methode verfügen, die eine Instanz ihres
Typparameters ( T ) zurückgibt, können Sie mithilfe des NotNull -Attributs ausdrücken, dass sie niemals null
zurückgibt.
Sie können auch angeben, dass ein Rückgabewert oder ein Argument nicht NULL sein darf, obwohl der Typ ein
Nullable-Verweistyp ist. Die folgende Methode ist eine Hilfsmethode, die ausgelöst wird, wenn das erste
Argument null ist:

public static void ThrowWhenNull(object value, string valueExpression = "")


{
if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);
}

Sie können diese Routine folgendermaßen aufrufen:

public static void LogMessage(string? message)


{
ThrowWhenNull(message, $"{nameof(message)} must not be null");

Console.WriteLine(message.Length);
}

Nach dem Aktivieren von NULL-Verweistypen möchten Sie sicherstellen, dass der vorstehende Code ohne
Warnungen kompiliert wird. Wenn die Methode ein Ergebnis zurückgibt, wird garantiert, dass der value -
Parameter ungleich NULL ist. Es ist jedoch weiterhin zulässig, ThrowWhenNull mit einem NULL-Verweis
aufzurufen. Sie können value als Nullable-Verweistyp festlegen und die Nachbedingung NotNull zur
Parameterdeklaration hinzufügen:

public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")


{
_ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
// other logic elided

Der vorangehende Code drückt den vorhandenen Vertrag klar aus: Aufrufer können eine Variable mit dem Wert
null übergeben, aber es wird garantiert, dass das Argument nie NULL ist, wenn die Methode ohne Auslösen
einer Ausnahme zurückgegeben wird.
Sie geben nicht bedingte Nachbedingungen mit den folgenden Attributen an:
MaybeNull: Ein Non-Nullable-Rückgabewert darf NULL sein.
NotNull: Ein Nullable-Rückgabetyp ist niemals NULL.

Bedingte Nachbedingungen: NotNullWhen , MaybeNullWhen und


NotNullIfNotNull
Ihnen ist die string -Methode String.IsNullOrEmpty(String) wahrscheinlich vertraut. Diese Methode gibt true
zurück, wenn das Argument NULL oder eine leere Zeichenfolge ist. Die Methode ist eine Form der NULL-
Überprüfung: Aufrufer müssen eine NULL-Überprüfung des Arguments durchführen, wenn die Methode false
zurückgibt. Um eine Methode wie diese NULL-fähig zu machen, legen Sie das Argument auf einen Nullable-
Verweistyp fest und fügen das NotNullWhen -Attribut hinzu:

bool IsNullOrEmpty([NotNullWhen(false)] string? value)

Auf diese Weise wird der Compiler darüber informiert, dass jeglicher Code mit Rückgabewert false nicht auf
NULL überprüft werden muss. Durch das Hinzufügen des Attributs wird die statische Analyse des Compilers
darüber informiert, dass IsNullOrEmpty die erforderliche NULL-Überprüfung durchführt: bei Rückgabe von
false ist das Argument nicht null .

string? userInput = GetUserInput();


if (!string.IsNullOrEmpty(userInput))
{
int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.

Die String.IsNullOrEmpty(String)-Methode wird für .NET Core 3.0 wie oben gezeigt mit Anmerkungen versehen.
Sie verwenden möglicherweise ähnliche Methoden in Ihrer Codebasis, die den Zustand von Objekten auf NULL-
Werte überprüfen. Der Compiler erkennt keine benutzerdefinierten Methoden für die NULL-Überprüfung, und
Sie müssen die Anmerkungen selbst hinzufügen. Wenn Sie das Attribut hinzufügen, hat die statische Analyse
des Compilers Kenntnis darüber, wann die getestete Variable auf NULL-Werte überprüft wurde.
Eine weitere Verwendung für diese Attribute ist das Try* -Muster. Die Nachbedingungen für ref - und out -
Argumente werden über den Rückgabewert kommuniziert. Betrachten Sie diese oben gezeigte Methode (in
einem Non-Nullable-Kontext):

bool TryGetMessage(string key, out string message)


{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}

Die vorstehende Methode folgt einem typischen .NET-Idiom: Der Rückgabewert gibt an, ob message auf den
gefunden Wert oder – falls „message“ nicht gefunden wird – auf den Standardwert festgelegt wurde. Wenn die
Methode true zurückgibt, ist der Wert von message ungleich NULL. Andernfalls legt die Methode message auf
NULL fest.
In einem Nullable-Kontext können Sie dieses Idiom mithilfe des Attributs NotNullWhen kommunizieren. Wenn
Sie Parameter für Nullable-Verweistypen mit Anmerkungen versehen, ändern Sie message in string? , und
fügen Sie ein Attribut hinzu:

bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)


{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message is not null;
}

Im vorstehenden Beispiel ist der Wert von message bekanntermaßen ungleich NULL, wenn TryGetMessage den
Wert true zurückgibt. Sie sollten ähnliche Methoden in Ihrer Codebasis in gleicher Weise mit Anmerkungen
versehen: Die Argumente könnten null entsprechen und sind bekanntermaßen ungleich NULL, wenn die
Methode true zurückgibt.
Es gibt ein letztes Attribut, das Sie möglicherweise ebenfalls benötigen. Gelegentlich hängt der NULL-Zustand
eines Rückgabewerts vom NULL-Zustand von mindestens einem Argument ab. Diese Methoden geben einen
Nicht-NULL-Wert zurück, wenn bestimmt Argumente nicht null sind. Um diese Methoden korrekt mit
Anmerkungen zu versehen, verwenden Sie das NotNullIfNotNull -Attribut. Sehen Sie sich die folgende Methode
an:

string GetTopLevelDomainFromFullUrl(string url)

Wenn das url -Argument ungleich NULL ist, ist die Ausgabe nicht null . Sobald Nullable-Verweise aktiviert
sind, müssen Sie weitere Anmerkungen hinzufügen, falls Ihre API ein NULL-Argument akzeptieren kann. Sie
können den Rückgabetyp wie im folgenden Code gezeigt mit Anmerkungen versehen:

string? GetTopLevelDomainFromFullUrl(string? url)

Dies funktioniert ebenfalls, zwingt die Aufrufer aber häufig dazu, zusätzliche null -Überprüfungen zu
implementieren. Der Vertrag sieht vor, dass der Rückgabewert nur dann null lautet, wenn das Argument url
den Wert null aufweist. Um diesen Vertrag auszudrücken, versehen Sie die Methode wie im folgenden Code
gezeigt mit Anmerkungen:

[return: NotNullIfNotNull("url")]
string? GetTopLevelDomainFromFullUrl(string? url)

Der Rückgabewert und das Argument wurden beide um ? ergänzt, um darauf hinzuweisen, dass beide den
Wert null annehmen können. Das Attribut verdeutlicht außerdem, dass der Rückgabewert ungleich NULL ist,
wenn das url -Argument ungleich null ist.
Sie geben bedingte Nachbedingungen mit diesen Attributen an:
MaybeNullWhen: Ein Non-Nullable-Argument darf NULL sein, wenn die Methode den angegebenen bool -
Wert zurückgibt.
NotNullWhen: Ein Nullwerte zulassendes Argument ist nicht NULL, wenn die Methode den angegebenen
bool -Wert zurückgibt.
NotNullIfNotNull: Ein Rückgabewert ist nicht NULL, wenn das Argument für den angegebenen Parameter
nicht NULL ist.
Hilfsmethoden: MemberNotNull und MemberNotNullWhen
Diese Attribute geben ihre Absicht an, wenn Sie allgemeinen Code von Konstruktoren in Hilfsmethoden
umgestaltet haben. Der C#-Compiler analysiert Konstruktoren und Feldinitialisierer, um sicherzustellen, dass alle
Verweisfelder, die keine NULL-Werte zulassen, initialisiert sind, bevor jeder Konstruktor zurückgegeben wird.
Der C#-Compiler verfolgt jedoch nicht in allen Hilfsmethoden Feldzuweisungen. Der Compiler gibt die Warnung
CS8618 aus, wenn Felder nicht direkt im Konstruktor initialisiert werden, sondern in einer Hilfsmethode. Sie
fügen die MemberNotNullAttribute-Klasse einer Methodendeklaration hinzu und geben die Felder an, die mit
einem Wert ungleich NULL in der Methode initialisiert werden. Betrachten Sie etwa das folgende Beispiel:

public class Container


{
private string _uniqueIdentifier; // must be initialized.
private string? _optionalMessage;

public Container()
{
Helper();
}

public Container(string message)


{
Helper();
_optionalMessage = message;
}

[MemberNotNull(nameof(_uniqueIdentifier))]
private void Helper()
{
_uniqueIdentifier = DateTime.Now.Ticks.ToString();
}
}

Sie können mehrere Feldnamen als Argumente für den MemberNotNull -Attributkonstruktor angeben.
Das MemberNotNullWhenAttribute verfügt über ein bool -Argument. Sie verwenden MemberNotNullWhen , wenn
die Hilfsmethode ein bool zurückgibt, das angibt, ob Ihre Hilfsmethode Felder initialisiert.

Beenden der Nullable-Analyse bei Auslösen der aufgerufenen


Methode
Einige Methoden, typischerweise Ausnahmehilfsmethoden oder andere Hilfsmethoden, werden immer mit
Ausgabe einer Ausnahme beendet. Eine Hilfsmethode kann eine Ausnahme basierend auf dem Wert eines
booleschen Arguments auslösen.
Im ersten Fall können Sie das DoesNotReturnAttribute-Attribut zur Methodendeklaration hinzufügen. Bei der
vom Compiler durchgeführten Analyse des NULL-Zustands wird kein Code in einer Methode überprüft, die auf
den Aufruf einer Methode mit der Anmerkung DoesNotReturn folgt. Betrachten Sie diese Methode:
[DoesNotReturn]
private void FailFast()
{
throw new InvalidOperationException();
}

public void SetState(object containedField)


{
if (containedField is null)
{
FailFast();
}

// containedField can't be null:


_field = containedField;
}

Der Compiler gibt nach dem Aufruf von FailFast keine Warnungen aus.
Im zweiten Fall fügen Sie das System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute-Attribut einem
booleschen Parameter der Methode hinzu. Sie können das vorherige Beispiel folgendermaßen abändern:

private void FailFastIf([DoesNotReturnIf(true)] bool isNull)


{
if (isNull)
{
throw new InvalidOperationException();
}
}

public void SetFieldState(object? containedField)


{
FailFastIf(containedField == null);
// No warning: containedField can't be null here:
_field = containedField;
}

Wenn der Wert des Arguments dem Wert des DoesNotReturnIf -Konstruktors entspricht, führt der Compiler
nach dieser Methode keine Analyse des NULL-Zustands durch.

Zusammenfassung
IMPORTANT
In der offiziellen Dokumentation wird die aktuelle C#-Version genannt. Wir schreiben derzeit C# 9.0. In Abhängigkeit von
der verwendeten C#-Version sind verschiedene Features möglicherweise nicht verfügbar. Die C#-Standardversion für Ihr
Projekt basiert auf dem Zielframework. Weitere Informationen finden Sie unter Verwaltung der C#-Sprachversion.

Das Hinzufügen von Nullable-Verweistypen ermöglicht eine Beschreibung der Erwartungen für Ihre APIs für
Variablen, die den Wert null annehmen könnten. Die Attribute erweitern die Möglichkeiten zur Beschreibung
des NULL-Zustands von Variablen als Vor- und Nachbedingungen. Durch diese Attribute werden Ihre
Erwartungen klarer beschrieben und verbessern das Benutzererlebnis für Entwickler, die Ihre APIs nutzen.
Wenn Sie Bibliotheken für einen Nullable-Kontext aktualisieren, fügen Sie diese Attribute hinzu, um Benutzer bei
der richtigen Verwendung Ihrer APIs zu unterstützen. Diese Attribute unterstützen Sie bei der vollständigen
Beschreibung des NULL-Zustands von Argumenten und Rückgabewerten.
AllowNull: Non-Nullable-Felder, -Parameter oder -Eigenschaften dürfen NULL sein.
DisallowNull: Nullable-Felder, -Parameter oder -Eigenschaften dürfen niemals NULL sein.
MaybeNull: Non-Nullable-Felder, -Parameter, -Eigenschaften oder -Rückgabewerte dürfen NULL sein.
NotNull: Nullable-Felder, -Parameter, -Eigenschaften oder -Rückgabewerte sind niemals NULL.
MaybeNullWhen: Ein Non-Nullable-Argument darf NULL sein, wenn die Methode den angegebenen bool -
Wert zurückgibt.
NotNullWhen: Ein Nullwerte zulassendes Argument ist nicht NULL, wenn die Methode den angegebenen
bool -Wert zurückgibt.
NotNullIfNotNull: Parameter, Eigenschaften oder Rückgabewerte sind nicht NULL, wenn das Argument für
den angegebenen Parameter nicht NULL ist.
DoesNotReturn: Eine Methode oder Eigenschaft gibt niemals ein Ergebnis zurück. Anders ausgedrückt: Die
Methode löst immer eine Ausnahme aus.
DoesNotReturnIf: Diese Methode oder Eigenschaft gibt niemals ein Ergebnis zurück, wenn der zugeordnete
bool -Parameter den angegebenen Wert aufweist.
Reservierte Attribute: Verschiedenes
04.11.2021 • 10 minutes to read

Diese Attribute können auf Elemente in Ihrem Code angewendet werden. Sie fügen diesen Elementen eine
semantische Bedeutung hinzu. Der Compiler verwendet diese semantischen Bedeutungen, um seine Ausgabe zu
ändern und mögliche Entwicklerfehler im Code zu melden.

Conditional -Attribut
Das Conditional -Attribut macht die Ausführung einer Methode abhängig von einem
Vorverarbeitungsbezeichner. Das Conditional -Attribut ist ein Alias für ConditionalAttribute und kann auf eine
Methode oder Attributklasse angewendet werden.
Im folgenden Beispiel wird Conditional auf eine Methode angewendet, um die Anzeige programmspezifischer
Diagnoseinformationen zu aktivieren oder zu deaktivieren:

#define TRACE_ON
using System;
using System.Diagnostics;

namespace AttributeExamples
{
public class Trace
{
[Conditional("TRACE_ON")]
public static void Msg(string msg)
{
Console.WriteLine(msg);
}
}

public class TraceExample


{
public static void Main()
{
Trace.Msg("Now in Main...");
Console.WriteLine("Done.");
}
}
}

Wenn der TRACE_ON -Bezeichner nicht definiert ist, wird die Ausgabe der Ablaufverfolgung nicht angezeigt.
Untersuchen Sie den Code im interaktiven Fenster.
Das Conditional -Attribut wird oft zusammen mit dem DEBUG -Bezeichner verwendet, um die Ablaufverfolgung
und Protokollierung für Debugbuilds – nicht jedoch in Releasebuilds – wie im folgenden Beispiel gezeigt zu
aktivieren:

[Conditional("DEBUG")]
static void DebugMethod()
{
}

Wird eine als bedingt gekennzeichnete Methode aufgerufen, bestimmt das Vorhandensein oder Fehlen des
angegebenen Vorverarbeitungssymbols, ob Aufrufe der Methode vom Compiler eingeschlossen oder
ausgelassen werden. Wenn das Symbol definiert ist, wird der Aufruf einbezogen; andernfalls wird der Aufruf
ausgelassen. Eine bedingte Methode muss eine Methode in einer Klassen- oder Strukturdeklaration sein und
einen void -Rückgabetyp aufweisen. Die Verwendung von Conditional ist eine sauberere, elegantere und auch
weniger fehleranfällige Alternative zum Einschließen von Methoden innerhalb von #if…#endif -Blöcken.
Besitzt eine Methode mehrere Conditional -Attribute, schließt der Compiler Aufrufe der Methode ein, wenn
mindestens eines der bedingten Symbole definiert ist (und die Symbole durch den OR-Operator logisch
miteinander verknüpft sind). Im folgenden Beispiel führt das Vorhandensein von A oder B zu einem
Methodenaufruf:

[Conditional("A"), Conditional("B")]
static void DoIfAorB()
{
// ...
}

Verwenden von Conditional mit Attributklassen


Das Conditional -Attribut kann auch auf die Definition einer Attributklasse angewendet werden. Im folgenden
Beispiel fügt das benutzerdefinierte Attribut Documentation den Metadaten nur dann Informationen hinzu, wenn
DEBUG definiert ist.

[Conditional("DEBUG")]
public class DocumentationAttribute : System.Attribute
{
string text;

public DocumentationAttribute(string text)


{
this.text = text;
}
}

class SampleClass
{
// This attribute will only be included if DEBUG is defined.
[Documentation("This method displays an integer.")]
static void DoWork(int i)
{
System.Console.WriteLine(i.ToString());
}
}

Obsolete -Attribut
Das Obsolete -Attribut markiert ein Codeelement als nicht länger zur Verwendung empfohlen. Die Verwendung
einer als veraltet markierten Entität führt zu einer Warnung oder einem Fehler. Das Obsolete -Attribut ist ein
Attribut zur einmaligen Nutzung und kann auf jede Entität angewendet werden, die Attribute zulässt. Obsolete
ist ein Alias für ObsoleteAttribute.
Das folgende Beispiel zeigt, wie das Obsolete -Attribut auf die Klasse A und die Methode B.OldMethod
angewendet wird. Da das zweite Argument des Attributkonstruktors, das auf B.OldMethod angewendet wurde,
auf true festgelegt wird, verursacht diese Methode einen Compilerfehler. Die Verwendung der A -Klasse
erzeugt hingegen nur eine Warnung. Wenn Sie B.NewMethod aufrufen, werden weder Warnungen noch Fehler
erzeugt. Wenn Sie es mit den vorherigen Definitionen verwenden, generiert der folgende Code zwei Warnungen
und einen Fehler:
using System;

namespace AttributeExamples
{
[Obsolete("use class B")]
public class A
{
public void Method() { }
}

public class B
{
[Obsolete("use NewMethod", true)]
public void OldMethod() { }

public void NewMethod() { }


}

public static class ObsoleteProgram


{
public static void Main()
{
// Generates 2 warnings:
A a = new A();

// Generate no errors or warnings:


B b = new B();
b.NewMethod();

// Generates an error, compilation fails.


// b.OldMethod();
}
}
}

Die Zeichenfolge, die als erstes Argument für den Attributkonstruktor bereitgestellt wurde, wird als Teil der
Warnung oder des Fehlers angezeigt. Es werden zwei Warnungen für die Klasse A generiert: eine für die
Deklaration des Klassenverweises und eine für den Klassenkonstruktor. Das Obsolete -Attribut kann ohne
Argumente verwendet werden. Es wird jedoch empfohlen, in einer Erläuterung anzugeben, was stattdessen
verwendet werden soll.
In C# 10 können Sie konstante Zeichenfolgeninterpolation und den nameof -Operator verwenden, um
sicherzustellen, dass die Namen übereinstimmen:

public class B
{
[Obsolete($"use {nameof(NewMethod)} instead", true)]
public void OldMethod() { }

public void NewMethod() { }


}

AttributeUsage -Attribut
Das AttributeUsage -Attribut bestimmt, wie eine benutzerdefinierte Attributklasse verwendet werden kann. Bei
AttributeUsageAttribute handelt es sich um ein Attribut, das Sie auf benutzerdefinierte Attributdefinitionen
anwenden. Mithilfe des AttributeUsage -Attribut können Sie Folgendes steuern:
Auf welche Programmelemente das Attribut angewendet werden kann. Wenn Sie dessen Verwendung nicht
einschränken, kann ein Attribut auf jedes der folgenden Programmelemente angewendet werden:
Assembly
Modul
Feld
Ereignis
Methode
Parameter
Eigenschaft
Rückgabewert
type
Ob ein Attribut mehrfach auf ein einzelnes Programmelement angewendet werden kann.
Ob Attribute von abgeleiteten Klassen geerbt werden.
Die Standardeinstellungen ähneln folgendem Beispiel, wenn Sie explizit angewendet werden:

[AttributeUsage(AttributeTargets.All,
AllowMultiple = false,
Inherited = true)]
class NewAttribute : Attribute { }

In diesem Beispiel kann die NewAttribute -Klasse auf jedes unterstützte Programmelement angewendet werden.
Es kann jedoch nur einmal auf jede Entität angewendet werden. Wenn das Attribut auf eine Basisklasse
angewendet wird, wird es an eine abgeleitete Klasse vererbt.
Die Argumente AllowMultiple und Inherited sind optional, sodass folgender Code die gleiche Wirkung hat:

[AttributeUsage(AttributeTargets.All)]
class NewAttribute : Attribute { }

Das erste AttributeUsageAttribute-Argument muss mindestens ein Element der AttributeTargets-Enumeration


sein. Mehrere Zieltypen können wie im folgenden Beispiel dargestellt mithilfe des OR-Operators verknüpft
werden:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
class NewPropertyOrFieldAttribute : Attribute { }

Ab C# 7.3 können Attribut auf das Eigenschaften- oder das Unterstützungsfeld einer automatisch
implementierten Eigenschaft angewendet werden. Das Attribut wird auf die Eigenschaft angewendet, sofern Sie
den field -Bezeichner des Attributs nicht angeben. Beides wird im folgenden Beispiel veranschaulicht:

class MyClass
{
// Attribute attached to property:
[NewPropertyOrField]
public string Name { get; set; } = string.Empty;

// Attribute attached to backing field:


[field: NewPropertyOrField]
public string Description { get; set; } = string.Empty;
}

Wenn das AllowMultiple-Argument auf true festgelegt ist, kann das daraus entstehende Attribut wie im
folgenden Beispiel dargestellt mehr als einmal auf eine einzelne Entität angewendet werden:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
class MultiUse : Attribute { }

[MultiUse]
[MultiUse]
class Class1 { }

[MultiUse, MultiUse]
class Class2 { }

In diesem Fall kann MultiUseAttribute wiederholt angewendet werden, da AllowMultiple auf true festgelegt
wurde. Beide gezeigten Formate für das Anwenden von mehreren Attributen sind gültig.
Wenn Inherited auf false festgelegt wurde, wird das Attribut nicht von Klassen geerbt, die von einer
Attributklasse abgeleitet werden. Zum Beispiel:

[AttributeUsage(AttributeTargets.Class, Inherited = false)]


class NonInheritedAttribute : Attribute { }

[NonInherited]
class BClass { }

class DClass : BClass { }

In diesem Fall wird NonInheritedAttribute nicht durch Vererbung auf DClass angewendet.
Sie können diese Schlüsselwörter auch verwenden, um anzugeben, wo ein Attribut angewendet werden soll.
Beispielsweise können Sie den field: -Bezeichner verwenden, um dem Unterstützungsfeld einer automatisch
implementierten Eigenschaft ein Attribut hinzuzufügen. Sie können auch die Spezifizier field: , property:
oder param: verwenden, um ein Attribut auf eines der Elemente anzuwenden, die aus einem Positionsdatensatz
generiert wurden. Ein Beispiel finden Sie unter Positionssyntax für die Eigenschaftendefinition.

AsyncMethodBuilder -Attribut
Ab C# 7 fügen Sie das Attribut System.Runtime.CompilerServices.AsyncMethodBuilderAttribute einem Typ
hinzu, der möglicherweise ein asynchroner Rückgabetyp ist. Das Attribut gibt den Typ an, mit dem die
Implementierung der asynchronen Methode erstellt wird, wenn der angegebene Typ von einer asynchronen
Methode zurückgegeben wird. Das Attribut AsyncMethodBuilder kann auf einen Typ angewendet werden, auf
den Folgendes zutrifft:
Er verfügt über eine zugängliche GetAwaiter -Methode.
Das von der GetAwaiter -Methode zurückgegebene Objekt implementiert die Schnittstelle
System.Runtime.CompilerServices.ICriticalNotifyCompletion.
Der Konstruktor des Attributs AsyncMethodBuilder gibt den Typ des zugeordneten Generators an. Der Generator
muss die folgenden zugänglichen Member implementieren:
Eine statische Create() -Methode, die den Generatortyp zurückgibt.
Eine lesbare Task -Eigenschaft, die den asynchronen Rückgabetyp zurückgibt.
Eine void SetException(Exception) -Methode, die die Ausnahme für einen Fehler bei der Aufgabe festlegt.
Eine void SetResult() - oder void SetResult(T result) -Methode, die die Aufgabe als abgeschlossen
kennzeichnet und optional deren Ergebnis festlegt.
Eine Start -Methode mit der folgenden API-Signatur:
void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine

Eine AwaitOnCompleted -Methode mit der folgenden Signatur:

public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine


stateMachine)
where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion
where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine

Eine AwaitUnsafeOnCompleted -Methode mit der folgenden Signatur:

public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref


TStateMachine stateMachine)
where TAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine

Weitere Informationen zu Generatoren für asynchrone Methoden finden Sie in den Artikeln zu den folgenden
von .NET bereitgestellten Generatoren:
System.Runtime.CompilerServices.AsyncTaskMethodBuilder
System.Runtime.CompilerServices.AsyncTaskMethodBuilder<TResult>
System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder
System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder<TResult>
Ab C# 10.0 kann das AsyncMethodBuilder -Attribut auf eine asynchrone Methode angewendet werden, um den
Generator für diesen Typ zu überschreiben.

InterpolatedStringHandler - und InterpolatedStringHandlerArgumemts -


Attribute
Ab C# 10.0 verwenden Sie diese Attribute, um anzugeben, dass es sich bei einem Typ um einen Handler für
interpolierte Zeichenfolgen handelt. Die .NET 6.0-Bibliothek enthält
System.Runtime.CompilerServices.DefaultInterpolatedStringHandler bereits für Szenarien, in denen Sie eine
interpolierte Zeichenfolge als Argument für einen string -Parameter verwenden. Möglicherweise verfügen Sie
über weitere Instanzen, in denen Sie die Verarbeitung interpolierter Zeichenfolgen steuern möchten. Sie wenden
System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute auf den Typ an, der den Handler
implementiert. Sie wenden System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute auf
Parameter des Konstruktors dieses Typs an.
Weitere Informationen zum Erstellen eines Handlers für interpolierte Zeichenfolgen finden Sie in der C# 10-
Featurespezifikation für Verbesserungen interpolierter Zeichenfolgen.

ModuleInitializer -Attribut
Ab C# 9 kennzeichnet das Attribut ModuleInitializer eine Methode, die von der Laufzeit beim Laden der
Assembly aufgerufen wird. ModuleInitializer ist ein Alias für ModuleInitializerAttribute.
Das Attribut ModuleInitializer kann nur auf eine Methode angewendet werden, die
statisch ist.
parameterlos ist.
Gibt void zurück.
für das enthaltende Modul ( internal oder public ) zugänglich ist.
keine generische Methode ist.
in keiner generischen Klasse enthalten ist.
keine lokale Funktion ist.
Das Attribut ModuleInitializer kann auf mehrere Methoden angewendet werden. In diesem Fall ruft die
Laufzeit die Methoden in einer deterministischen, jedoch nicht angegebenen Reihenfolge auf.
Das folgende Beispiel veranschaulicht, wie mehrere Modulinitialisierermethoden verwendet werden. Die
Methoden Init1 und Init2 werden vor Main ausgeführt und fügen der Text -Eigenschaft jeweils eine
Zeichenfolge hinzu. Bei der Ausführung von Main verfügt die Text -Eigenschaft also bereits über Zeichenfolgen
von beiden Initialisierermethoden.

using System;

internal class ModuleInitializerExampleMain


{
public static void Main()
{
Console.WriteLine(ModuleInitializerExampleModule.Text);
//output: Hello from Init1! Hello from Init2!
}
}

using System.Runtime.CompilerServices;

internal class ModuleInitializerExampleModule


{
public static string? Text { get; set; }

[ModuleInitializer]
public static void Init1()
{
Text += "Hello from Init1! ";
}

[ModuleInitializer]
public static void Init2()
{
Text += "Hello from Init2! ";
}
}

Quellcode-Generatoren müssen gelegentlich Initialisierungscode generieren. Modulinitialisierer stellen für


diesen Code einen Standardspeicherort bereit.

SkipLocalsInit -Attribut
Ab C# 9 verhindert das SkipLocalsInit -Attribut, dass der Compiler bei der Ausgabe an Metadaten das Flag
.locals init festlegt. SkipLocalsInit ist ein einwertiges Attribut, das auf eine Methode, eine Eigenschaft, eine
Klasse, eine Struktur, eine Schnittstelle oder ein Modul angewendet werden kann, jedoch nicht auf eine
Assembly. SkipLocalsInit ist ein Alias für SkipLocalsInitAttribute.
Das Flag .locals init bewirkt, dass die CLR alle in einer Methode deklarierten lokalen Variablen mit ihrem
Standardwert initialisiert. Da auch der Compiler dafür sorgt, dass eine Variable erst nach dem Zuweisen eines
Werts verwendet werden kann, ist das .locals init -Attribut in der Regel nicht erforderlich. Allerdings kann die
zusätzliche Initialisierung mit 0 in einigen Szenarios zu nachweisbaren Leistungseinbußen führen, etwa bei der
Verwendung von stackalloc zur Zuordnung eines Arrays im Stapel. In diesen Fällen können Sie das Attribut
SkipLocalsInit hinzufügen. Wird das Attribut direkt auf eine Methode angewendet, wirkt es sich auf die
Methode und alle darin geschachtelten Funktionen, wie etwa Lambda- und lokale Funktionen, aus. Wird es auf
einen Typ oder ein Modul angewendet, werden alle darin geschachtelten Methoden beeinflusst. Das Attribut hat
keine Auswirkungen auf abstrakte Methoden, jedoch auf Code, der für die Implementierung generiert wurde.
Dieses Attribut erfordert die Compileroption AllowUnsafeBlocks. Diese Anforderung signalisiert, dass in einigen
Fällen nicht zugewiesener Arbeitsspeicher vom Code angezeigt werden kann (z. B. durch das Lesen aus dem
nicht initialisierten, im Stapel zugeordneten Arbeitsspeicher).
Das folgende Beispiel veranschaulicht, wie sich das SkipLocalsInit -Attribut auf eine Methode auswirkt, die
stackalloc verwendet. Mit der Methode wird der gesamte Inhalt des Arbeitsspeichers zum Zeitpunkt angezeigt,
als ein Array aus ganzen Zahlen zugeordnet wurde.

[SkipLocalsInit]
static void ReadUninitializedMemory()
{
Span<int> numbers = stackalloc int[120];
for (int i = 0; i < 120; i++)
{
Console.WriteLine(numbers[i]);
}
}
// output depends on initial contents of memory, for example:
//0
//0
//0
//168
//0
//-1271631451
//32767
//38
//0
//0
//0
//38
// Remaining rows omitted for brevity.

Wenn Sie diesen Code ausprobieren möchten, legen Sie in Ihrer CSPROJ-Datei die Compileroption
AllowUnsafeBlocks fest:

<PropertyGroup>
...
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Siehe auch
Attribute
System.Reflection
Attribute
Reflexion
Unsicherer Code, Zeigertypen und Funktionszeiger
04.11.2021 • 13 minutes to read

Der größte Teil des C#-Codes, den Sie schreiben, ist „überprüfbar sicherer Code“. Überprüfbar sicherer Code
bedeutet, dass .NET-Tools sicherstellen können, dass der Code sicher ist. Üblicherweise greift sicherer Code nicht
direkt über Zeiger auf den Arbeitsspeicher zu. Außerdem ordnet er keinen unformatierten Arbeitsspeicher zu.
Stattdessen erstellt er verwaltete Objekte.
C# unterstützt einen unsafe -Kontext, in dem Sie nicht überprüfbaren Code schreiben können. In einem unsafe
-Kontext kann Code Zeiger verwenden, Speicherblöcke zuordnen und freigeben sowie Methoden mit
Funktionszeigern aufrufen. Unsicherer Code in C# ist nicht notwendigerweise gefährlich. Es handelt sich dabei
lediglich um Code, dessen Sicherheit nicht überprüft werden kann.
Unsicherer Code verfügt über die folgenden Eigenschaften:
Methoden, Typen und Codeblöcke können als unsicher definiert werden.
In manchen Fällen kann unsicherer Code die Leistung einer Anwendung erhöhen, indem die Überprüfung
von Arraygrenzen entfernt wird.
Unsicherer Code ist erforderlich, wenn Sie native Funktionen aufrufen, die Zeiger erfordern.
Die Verwendung von unsicherem Code führt zu Sicherheits- und Stabilitätsrisiken.
Code, in dem unsichere Blöcke enthalten sind, muss mit der Compileroption AllowUnsafeBlocks kompiliert
werden.

Zeigertypen
In einem unsicheren Kontext kann ein Typ ein Zeigertyp, zusätzlich zu einem Werttyp, oder ein Verweistyp sein.
Eine Zeigertypdeklaration erfolgt in einer der folgenden Formen:

type* identifier;
void* identifier; //allowed but not recommended

Der Typ, der vor * in einem Zeigertyp angegeben wird, wird als Ver weistyp bezeichnet. Nur ein nicht
verwalteter Typ kann ein Verweistyp sein.
Zeigertypen erben nicht von object, und es ist keine Konvertierung zwischen Zeigertypen und object möglich.
Weiterhin unterstützen Boxing und Unboxing keine Zeiger. Es ist jedoch möglich, Konvertierungen zwischen
verschiedenen Zeigertypen sowie zwischen Zeigertypen und ganzzahligen Typen durchzuführen.
Wenn Sie mehrere Zeiger in derselben Deklaration deklarieren, schreiben Sie das Sternchen ( * ) nur zusammen
mit dem zugrunde liegenden Typ. Er wird nicht als Präfix für jeden Zeigernamen verwendet. Beispiel:

int* p1, p2, p3; // Ok


int *p1, *p2, *p3; // Invalid in C#

Ein Zeiger kann nicht auf einen Verweis oder eine Struktur verweisen, der oder die Verweise enthält, da ein
Objektverweis auch dann in die Garbage Collection aufgenommen werden kann, wenn ein Zeiger darauf
verweist. In der Garbage Collection wird nicht nachgehalten, ob von einem der Zeigertypen auf ein Objekt
verwiesen wird.
Der Wert der Zeigervariablen vom Typ MyType* ist die Adresse einer Variablen vom Typ MyType . Im Folgenden
finden Sie Beispiele für Zeigertypdeklarationen:
int* p : ist ein Zeiger auf einen ganzzahligen Wert.
p
int** p : p ist ein Zeiger auf einen Zeiger auf einen ganzzahligen Wert.
int*[] p : p ist ein eindimensionales Array von Zeigern auf ganzzahlige Werte.
char* p : p ist ein Zeiger auf eine char-Variable.
void* p : p ist ein Zeiger auf einen unbekannten Typ.

Sie können den Zeigerdereferenzierungsoperator * verwenden, um auf den Inhalt an der Speicherstelle
zuzugreifen, auf die die Zeigervariable zeigt. Betrachten Sie beispielsweise die folgende Deklaration:

int* myVariable;

Der Ausdruck *myVariable kennzeichnet die int -Variable, die sich an der in myVariable enthaltenen Adresse
befindet.
Es gibt mehrere Beispiele für Zeiger in den Artikeln zur fixed -Anweisung. Im folgenden Beispiel wird die
Verwendung des unsafe -Schlüsselworts und der fixed -Anweisung sowie die Vorgehensweise zum Erhöhen
eines inneren Zeigers veranschaulicht. Sie können diesen Code in die Hauptmethode einer Konsolenanwendung
einfügen, um ihn auszuführen. Diese Beispiele müssen mithilfe der Compileroption AllowUnsafeBlocks
kompiliert werden.
// Normal pointer to an object.
int[] a = new int[5] { 10, 20, 30, 40, 50 };
// Must be in unsafe code to use interior pointers.
unsafe
{
// Must pin object on heap so that it doesn't move while using interior pointers.
fixed (int* p = &a[0])
{
// p is pinned as well as object, so create another pointer to show incrementing it.
int* p2 = p;
Console.WriteLine(*p2);
// Incrementing p2 bumps the pointer by four bytes due to its type ...
p2 += 1;
Console.WriteLine(*p2);
p2 += 1;
Console.WriteLine(*p2);
Console.WriteLine("--------");
Console.WriteLine(*p);
// Dereferencing p and incrementing changes the value of a[0] ...
*p += 1;
Console.WriteLine(*p);
*p += 1;
Console.WriteLine(*p);
}
}

Console.WriteLine("--------");
Console.WriteLine(a[0]);

/*
Output:
10
20
30
--------
10
11
12
--------
12
*/

Der Dereferenzierungsoperator kann nicht auf Zeiger vom Typ void* angewendet werden. Sie können jedoch
eine Umwandlung verwenden, um einen void-Zeiger in einen anderen Zeigertyp und umgekehrt zu
konvertieren.
Ein Zeiger kann den Wert null annehmen. Die Anwendung des Dereferenzierungsoperators auf einen NULL-
Zeiger führt zu einem in der Implementierung definierten Verhalten.
Die Übergabe von Zeigern zwischen Methoden kann zu nicht definiertem Verhalten führen. Ziehen Sie eine
Methode in Betracht, die einen Zeiger auf eine lokale Variable als einen in -, out - oder ref -Parameter oder
als Funktionsergebnis zurückgibt. Wenn der Zeiger in einem fixed-Block festgelegt wurde, ist die Variable, auf
die der Zeiger verweist, möglicherweise nicht fixiert.
In der folgenden Tabelle werden die Operatoren und Anweisungen aufgelistet, die in einem unsicheren Kontext
auf Zeiger angewendet werden können.

O P ERATO R/ A N W EISUN G VERW EN DUN G

* Führt eine Zeigerdereferenzierung aus.


O P ERATO R/ A N W EISUN G VERW EN DUN G

-> Greift über einen Zeiger auf einen Member einer Struktur zu.

[] Indiziert einen Zeiger.

& Ruft die Adresse einer Variablen ab.

++ und -- Inkrementiert und dekrementiert Zeiger.

+ und - Führt Zeigerarithmetik aus.

== , != , < , > , <= und >= Vergleicht Zeiger.

stackalloc Belegt Speicher für den Stapel.

fixed -Anweisung Fixiert eine Variable vorübergehend, damit ihre Adresse


gefunden werden kann.

Weitere Informationen zu zeigerbezogenen Operatoren finden Sie unter Operatoren im Zusammenhang mit
Zeigern.
Jeder Zeigertyp kann implizit in einen void* -Typ konvertiert werden. Jedem Zeigertyp kann der Wert null
zugewiesen werden. Jeder Zeigertyp kann mithilfe eines Umwandlungsausdrucks explizit in einen anderen
Zeigertyp konvertiert werden. Sie können auch jeden integralen Typ in einen Zeigertyp oder jeden Zeigertyp in
einen integralen Typ konvertieren. Diese Konvertierungen erfordern eine explizite Umwandlung.
Im folgenden Beispiel wird ein int* -Typ in einen byte* -Typ konvertiert. Beachten Sie, dass der Zeiger auf das
Byte der Variable mit der niedrigsten Adresse zeigt. Wenn Sie das Ergebnis nach und nach bis auf die Größe von
int (4 Bytes) erhöhen, können Sie die verbleibenden Bytes der Variable anzeigen.

int number = 1024;

unsafe
{
// Convert to byte:
byte* p = (byte*)&number;

System.Console.Write("The 4 bytes of the integer:");

// Display the 4 bytes of the int variable:


for (int i = 0 ; i < sizeof(int) ; ++i)
{
System.Console.Write(" {0:X2}", *p);
// Increment the pointer:
p++;
}
System.Console.WriteLine();
System.Console.WriteLine("The value of the integer: {0}", number);

/* Output:
The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
*/
}

Puffer fester Größe


In C# können Sie die fixed-Anweisung verwenden, um einen Puffer mit einem Array fester Größe in einer
Datenstruktur zu erstellen. Puffer mit fester Größe sind nützlich, wenn Sie Methoden schreiben, die mit
Datenquellen aus anderen Sprachen oder Plattformen zusammenarbeiten. Das Array fester Größe kann
sämtliche Attribute und Modifizierer, die für reguläre Strukturmember zulässig sind, in Anspruch nehmen. Die
einzige Einschränkung besteht darin, dass der Arraytyp bool , byte , char , short , int , long , sbyte , ushort
, uint , ulong , float oder double sein muss.

private fixed char name[30];

Eine C#-Struktur in sicherem Code, die ein Array enthält, enthält nicht die Elemente des Arrays. Stattdessen
enthält die Struktur einen Verweis auf die Elemente. Sie können ein Array mit einer festen Größe in eine Struktur
einbetten, wenn es in einem unsicheren Codeblock verwendet wird.
Die Größe der folgenden struct hängt nicht von der Anzahl der Elemente im Array ab, da pathName ein
Verweis ist:

public struct PathArray


{
public char[] pathName;
private int reserved;
}

Ein kann ein eingebettetes Array in unsicheren Code enthalten. Im folgenden Beispiel verfügt das
struct
fixedBuffer -Array über eine feste Größe. Sie können eine fixed -Anweisung verwenden, um einen Zeiger auf
das erste Element festzulegen. Über diesen Zeiger können Sie auf die Elemente des Arrays zugreifen. Die fixed
-Anweisung fixiert das Instanzenfeld fixedBuffer an einem bestimmten Speicherort im Arbeitsspeicher.

internal unsafe struct Buffer


{
public fixed char fixedBuffer[128];
}

internal unsafe class Example


{
public Buffer buffer = default;
}

private static void AccessEmbeddedArray()


{
var example = new Example();

unsafe
{
// Pin the buffer to a fixed location in memory.
fixed (char* charPtr = example.buffer.fixedBuffer)
{
*charPtr = 'A';
}
// Access safely through the index:
char c = example.buffer.fixedBuffer[0];
Console.WriteLine(c);

// Modify through the index:


example.buffer.fixedBuffer[0] = 'B';
Console.WriteLine(example.buffer.fixedBuffer[0]);
}
}

Die Größe des 128-Element- char -Arrays beträgt 256 Bytes. char-Puffer mit fester Größe verwenden immer 2
Bytes pro Zeichen, unabhängig von der Codierung. Diese Arraygröße ist selbst dann identisch, wenn char-Puffer
mit CharSet = CharSet.Auto oder CharSet = CharSet.Ansi zu API-Methoden oder Strukturen gemarshallt
werden. Weitere Informationen finden Sie unter CharSet.
Im obigen Beispiel wird der Zugriff auf fixed -Felder ohne Anheften dargestellt – diese Funktionalität ist ab C#
7.3 verfügbar.
Ein anderes häufiges Array mit fester Größe ist das bool-Array. Die Elemente in einem bool -Array sind immer 1
Byte groß. bool -Arrays eignen sich nicht zum Erstellen von Bitarrays oder Puffern.
Puffer fester Größe werden mit der System.Runtime.CompilerServices.UnsafeValueTypeAttribute-Klasse
kompiliert, die die Common Language Runtime (CLR) anweist, dass ein Typ ein nicht verwaltetes Array enthält,
das potenziell überlaufen kann. Arbeitsspeicher, der mit stackalloc zugeordnet wurde, ermöglicht auch
automatisch Funktionen zur Erkennung von Pufferüberlauf in der CLR. Im vorherigen Beispiel wird gezeigt, wie
ein Puffer fester Größe in einer unsafe struct vorhanden sein kann.

internal unsafe struct Buffer


{
public fixed char fixedBuffer[128];
}

Der vom Compiler für Buffer generierte C#-Code wird wie folgt attributiert:

internal struct Buffer


{
[StructLayout(LayoutKind.Sequential, Size = 256)]
[CompilerGenerated]
[UnsafeValueType]
public struct <fixedBuffer>e__FixedBuffer
{
public char FixedElementField;
}

[FixedBuffer(typeof(char), 128)]
public <fixedBuffer>e__FixedBuffer fixedBuffer;
}

Puffer fester Größe unterscheiden sich folgendermaßen von normalen Arrays:


Können nur in einem unsafe Kontext verwendet werden
Können nur Instanzfelder von Strukturen sein
Sie sind immer Vektoren oder eindimensionale Arrays.
Die Deklaration sollte die Länge enthalten, z. B. fixed char id[8] . Es ist nicht möglich, fixed char id[] zu
verwenden.

Verwenden von Zeigern zum Kopieren eines Bytearrays


In folgendem Beispiel werden Zeiger verwendet, um Bytes aus einem Array in ein anderes zu kopieren.
In diesem Beispiel wird das Schlüsselwort unsafe verwendet, mit dem Sie Zeiger in der Copy -Methode
verwenden können. Die Anweisung fixed wird verwendet, um Zeiger auf das Quell- und Zielarray zu deklarieren.
Diese fixed -Anweisung heftet den Speicherort des Quell- und Zielarrays im Speicher an, damit diese während
der automatischen Speicherbereinigung nicht verschoben werden. Die Speicherblöcke der Arrays werden gelöst,
wenn der fixed -Block abgeschlossen wird. Da die Copy -Methode in diesem Beispiel das Schlüsselwort
unsafe verwendet, muss sie mit der Compileroption AllowUnsafeBlocks kompiliert werden.

In diesem Beispiel werden Indizes anstelle eines zweiten nicht verwalteten Zeigers verwendet, um auf die
Elemente beider Arrays zuzugreifen. Die Deklaration der Zeiger pSource und pTarget heftet die Arrays an.
Dieses Feature ist ab C# 7.3 verfügbar.

static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,


int targetOffset, int count)
{
// If either array is not instantiated, you cannot complete the copy.
if ((source == null) || (target == null))
{
throw new System.ArgumentException();
}

// If either offset, or the number of bytes to copy, is negative, you


// cannot complete the copy.
if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
{
throw new System.ArgumentException();
}

// If the number of bytes from the offset to the end of the array is
// less than the number of bytes you want to copy, you cannot complete
// the copy.
if ((source.Length - sourceOffset < count) ||
(target.Length - targetOffset < count))
{
throw new System.ArgumentException();
}

// The following fixed statement pins the location of the source and
// target objects in memory so that they will not be moved by garbage
// collection.
fixed (byte* pSource = source, pTarget = target)
{
// Copy the specified number of bytes from source to target.
for (int i = 0; i < count; i++)
{
pTarget[targetOffset + i] = pSource[sourceOffset + i];
}
}
}

static void UnsafeCopyArrays()


{
// Create two arrays of the same length.
int length = 100;
byte[] byteArray1 = new byte[length];
byte[] byteArray2 = new byte[length];

// Fill byteArray1 with 0 - 99.


for (int i = 0; i < length; ++i)
{
byteArray1[i] = (byte)i;
}

// Display the first 10 elements in byteArray1.


System.Console.WriteLine("The first 10 elements of the original are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray1[i] + " ");
}
System.Console.WriteLine("\n");

// Copy the contents of byteArray1 to byteArray2.


Copy(byteArray1, 0, byteArray2, 0, length);

// Display the first 10 elements in the copy, byteArray2.


System.Console.WriteLine("The first 10 elements of the copy are:");
for (int i = 0; i < 10; ++i)
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray2[i] + " ");
}
System.Console.WriteLine("\n");

// Copy the contents of the last 10 elements of byteArray1 to the


// beginning of byteArray2.
// The offset specifies where the copying begins in the source array.
int offset = length - 10;
Copy(byteArray1, offset, byteArray2, 0, length - offset);

// Display the first 10 elements in the copy, byteArray2.


System.Console.WriteLine("The first 10 elements of the copy are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray2[i] + " ");
}
System.Console.WriteLine("\n");
/* Output:
The first 10 elements of the original are:
0 1 2 3 4 5 6 7 8 9

The first 10 elements of the copy are:


0 1 2 3 4 5 6 7 8 9

The first 10 elements of the copy are:


90 91 92 93 94 95 96 97 98 99
*/
}

Funktionszeiger
C# stellt delegate -Typen bereit, um sichere Funktionszeigerobjekte zu definieren. Das Aufrufen eines Delegaten
umfasst das Instanziieren eines von System.Delegate abgeleiteten Typs und das Ausführen eines virtuellen
Methodenaufrufs für dessen Invoke -Methode. Dieser virtuelle Aufruf verwendet die callvirt -IL-Anweisung.
In leistungskritischen Codepfaden ist die Verwendung der calli -IL-Anweisung effizienter.
Sie können einen Funktionszeiger mit der delegate* -Syntax definieren. Der Compiler ruft die Funktion mit der
calli -Anweisung auf, anstatt ein delegate -Objekt zu instanziieren und Invoke aufzurufen. Der folgende Code
deklariert zwei Methoden, in denen ein delegate - oder ein delegate* -Zeiger verwendet wird, um zwei Objekte
desselben Typs zu kombinieren. In der ersten Methode wird ein System.Func<T1,T2,TResult>-Delegattyp
verwendet. In der zweiten Methode wird eine delegate* -Deklaration mit denselben Parametern und demselben
Rückgabetyp verwendet:

public static T Combine<T>(Func<T, T, T> combinator, T left, T right) =>


combinator(left, right);

public static T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) =>


combinator(left, right);

Der folgende Code zeigt, wie Sie eine statische lokale Funktion deklarieren und die UnsafeCombine -Methode
mithilfe eines Zeigers auf diese lokale Funktion aufrufen:

static int localMultiply(int x, int y) => x * y;


int product = UnsafeCombine(&localMultiply, 3, 4);

Der vorangehende Code veranschaulicht einige der Regeln für die Funktion, auf die als Funktionszeiger
zugegriffen wird:
Funktionszeiger können nur in einem unsafe -Kontext deklariert werden.
Methoden, die einen delegate* -Zeiger verwenden (oder einen delegate* -Zeiger zurückgeben), können nur
in einem unsafe -Kontext aufgerufen werden.
Der & -Operator zum Abrufen der Adresse einer Funktion ist nur für static -Funktionen zulässig. (Diese
Regel gilt sowohl für Memberfunktionen als auch für lokale Funktionen.)
Die Syntax weist Parallelen mit dem Deklarieren von delegate -Typen und dem Verwenden von Zeigern auf. Das
* -Suffix bei delegate kennzeichnet, dass die Deklaration ein Funktionszeiger ist. Das & kennzeichnet, wenn
eine Methodengruppe einem Funktionszeiger zugewiesen wird, dass der Vorgang die Adresse der Methode
verwendet.
Sie können die Aufrufkonvention für einen delegate* -Zeiger mit den Schlüsselwörtern managed und
unmanaged angeben. Außerdem können Sie für unmanaged -Funktionszeiger die Aufrufkonvention angeben. Die
folgenden Deklarationen zeigen Beispiele für jedes Schlüsselwort. In der ersten Deklaration wird die managed -
Aufrufkonvention verwendet, die die Standardkonvention ist. In den nächsten drei Deklarationen wird eine
unmanaged -Aufrufkonvention verwendet. In jeder Deklaration ist eine der Ecma International 335-
Aufrufkonventionen angegeben: Cdecl , Stdcall , Fastcall oder Thiscall . In den letzten Deklarationen wird
die unmanaged -Aufrufkonvention verwendet, wodurch die CLR angewiesen wird, die Standardaufrufkonvention
für die Plattform auszuwählen. Die CLR wählt die Aufrufkonvention zur Laufzeit aus.

public static T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>


combinator(left, right);
public static T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
combinator(left, right);

Weitere Informationen zu Funktionszeigern finden Sie im Funktionszeiger-Vorschlag für C# 9.0.

C#-Sprachspezifikation
Weitere Informationen finden Sie im Kapitel Unsicherer Code in der C#-Sprachspezifikation.
C#-Präprozessoranweisungen
04.11.2021 • 12 minutes to read

Obwohl der Compiler keinen separaten Präprozessor hat, werden die in diesem Abschnitt beschriebenen
Anweisungen verarbeitet, als gäbe es einen. Sie werden zur Unterstützung der bedingten Kompilierung
verwendet. Sie können diese Anweisungen im Gegensatz zu C- und C++-Anweisungen nicht verwenden, um
Makros zu erstellen. Eine Präprozessordirektive muss die einzige Anweisung in einer Zeile sein.

Nullable-Kontext
Die Präprozessoranweisung #nullable legt den Nullable-Anmerkungskontext und den Nullable-
Warnungskontext fest. Die Anweisung steuert, ob Nullable-Anmerkungen wirksam sind und ob Warnungen zur
NULL-Zulässigkeit angegeben werden. Jeder Kontext ist entweder deaktiviert oder aktiviert.
Beide Kontexte können auf Projektebene (außerhalb des C#-Quellcodes) festgelegt werden. Die #nullable -
Anweisung steuert die Anmerkungs- und Warnungskontexte und hat Vorrang vor anderen Einstellungen auf
Projektebene. Eine Anweisung legt die von ihr gesteuerten Kontexte fest, bis sie von einer anderen Anweisung
überschrieben wird oder bis zum Ende der Quelldatei.
Die Auswirkungen der Anweisungen lauten wie folgt:
#nullable disable : Diese Anweisung legt die Nullable-Anmerkungskontexte und -Warnungskontexte auf
deaktiviert fest.
#nullable enable : Diese Anweisung legt die Nullable-Anmerkungskontexte und -Warnungskontexte auf
aktiviert fest.
#nullable restore : Diese Anweisung stellt die Nullable-Anmerkungskontexte und -Warnungskontexte der
Projekteinstellungen wieder her.
#nullable disable annotations : Diese Anweisung legt den Nullable-Anmerkungskontext auf deaktiviert fest.
#nullable enable annotations : Diese Anweisung legt den Nullable-Anmerkungskontext auf aktiviert fest.
#nullable restore annotations : Diese Anweisung stellt den Nullable-Anmerkungskontext der
Projekteinstellungen wieder her.
#nullable disable warnings : Diese Anweisung legt den Nullable-Warnungskontext auf deaktiviert fest.
#nullable enable warnings : Diese Anweisung legt den Nullable-Warnungskontext auf aktiviert fest.
#nullable restore warnings : Diese Anweisung stellt den Nullable-Warnungskontext der Projekteinstellungen
wieder her.

Bedingte Kompilierung
Die bedingte Kompilierung wird über die folgenden vier Präprozessoranweisungen gesteuert:
#if : Öffnet eine bedingte Kompilierung, bei der Code nur dann kompiliert wird, wenn das angegebene
Symbol definiert ist.
#elif : Schließt die vorangehende bedingte Kompilierung und öffnet eine neue bedingte Kompilierung,
wenn das angegebene Symbol definiert ist.
#else : Schließt die vorangehende bedingte Kompilierung und öffnet eine neue bedingte Kompilierung,
wenn das angegebene Symbol nicht definiert ist.
#endif : Schließt die vorangehende bedingte Kompilierung.

Findet der C#-Compiler eine #if -Anweisung, die mit einer #endif -Anweisung beendet wird, wird der Code
zwischen den Anweisungen nur dann kompiliert, wenn das angegebene Symbol definiert ist. Im Gegensatz zu C
und C++ können Sie einem Symbol keinen numerischen Wert zuweisen. Die #if -Anweisung in C# ist ein
boolescher Wert und überprüft nur, ob das Symbol definiert wurde. Beispiel:

#if DEBUG
Console.WriteLine("Debug version");
#endif

Sie können die Operatoren == (Gleichheit) und != (Ungleichheit) zum Testen auf die bool -Werte true oder
false verwenden. true bedeutet, dass das Symbol definiert wurde. Die #if DEBUG -Anweisung hat die gleiche
Bedeutung wie #if (DEBUG == true) . Sie können die Operatoren && (und), || (oder) und ! (nicht)
verwenden, um auszuwerten, ob mehrere Symbole definiert wurden. Symbole und Operatoren können auch
mit Klammern gruppiert werden.
Wenn Sie #if mit den Direktiven #else , #elif , #endif , #define und #undef verwenden, können Sie Code
je nach dem Vorhandensein eines oder mehrerer Symbole ein- oder ausschließen. Die bedingte Kompilierung
kann hilfreich sein, wenn Code für einen Debugbuild oder für eine bestimmte Konfiguration kompiliert wird.
Eine bedingte Anweisung, die mit einer #if -Anweisung beginnt, muss explizit mit einer #endif -Anweisung
beendet werden. Über #define können Sie ein Symbol definieren. Wird dieses Symbol als Ausdruck an die #if
-Anweisung übergeben, wird der Ausdruck als true ausgewertet. Sie können ein Symbol auch mit der
Compileroption DefineConstants definieren. Die Definition eines Symbols kann mit #undef aufgehoben
werden. Der Gültigkeitsbereich eines mit #define erstellten Symbols ist die Datei, in der es definiert wurde.
Zwischen einem Symbol, das mit DefineConstants oder mit #define definiert wird, und einer Variablen mit
dem gleichen Namen kommt es zu keinem Konflikt. Das bedeutet, dass ein Variablenname nicht an eine
Präprozessoranweisung übergeben werden sollte und ein Symbol nur von einer Präprozessoranweisung
ausgewertet werden kann.
Mit #elif können zusammengesetzte bedingte Direktiven erstellt werden. Der #elif -Ausdruck wird
ausgewertet, wenn weder der Ausdruck der vorangehenden #if -Anweisung noch der Ausdruck einer
vorangehenden (optionalen) #elif -Anweisung als true ausgewertet wird. Wird ein #elif -Ausdruck als
true ausgewertet, wird der gesamte Code zwischen der #elif -Anweisung und der nächsten bedingten
Anweisung vom Compiler ausgewertet. Beispiel:

#define VC7
//...
#if debug
Console.WriteLine("Debug build");
#elif VC7
Console.WriteLine("Visual Studio 7");
#endif

Mit #else können Sie eine zusammengesetzte bedingte Anweisung erstellen, sodass der Compiler, wenn keiner
der Ausdrücke in den vorangehenden #if -Anweisungen oder (optional) #elif -Anweisungen als true
ausgewertet wird, den gesamten Code zwischen #else und der nächsten #endif -Anweisung auswertet.
#endif (#endif) muss die nächste Präprozessoranweisung nach #else sein.

#endif gibt das Ende einer bedingten Anweisung an, die mit der #if -Anweisung beginnt.
Das Buildsystem kennt zudem vordefinierte Präprozessorsymbole, die verschiedene Zielframeworks in
Projekten im SDK-Format darstellen. Diese sind hilfreich, wenn Sie Anwendungen erstellen, die für mehr als eine
.NET-Version bestimmt sind.
Z IEL F RA M EW O RK S SY M B O L E

.NET Framework NETFRAMEWORK , NET48 , NET472 , NET471 , NET47 ,


NET462 , NET461 , NET46 , NET452 , NET451 , NET45 ,
NET40 , NET35 , NET20 , NET48_OR_GREATER ,
NET472_OR_GREATER , NET471_OR_GREATER ,
NET47_OR_GREATER , NET462_OR_GREATER ,
NET461_OR_GREATER , NET46_OR_GREATER ,
NET452_OR_GREATER , NET451_OR_GREATER ,
NET45_OR_GREATER , NET40_OR_GREATER ,
NET35_OR_GREATER , NET20_OR_GREATER

.NET Standard NETSTANDARD , NETSTANDARD2_1 , NETSTANDARD2_0 ,


NETSTANDARD1_6 , NETSTANDARD1_5 , NETSTANDARD1_4 ,
NETSTANDARD1_3 , NETSTANDARD1_2 , NETSTANDARD1_1 ,
NETSTANDARD1_0 , NETSTANDARD2_1_OR_GREATER ,
NETSTANDARD2_0_OR_GREATER ,
NETSTANDARD1_6_OR_GREATER ,
NETSTANDARD1_5_OR_GREATER ,
NETSTANDARD1_4_OR_GREATER ,
NETSTANDARD1_3_OR_GREATER ,
NETSTANDARD1_2_OR_GREATER ,
NETSTANDARD1_1_OR_GREATER ,
NETSTANDARD1_0_OR_GREATER

.NET 5 oder höher (und .NET Core) NET , NET6_0 , NET6_0_ANDROID , NET6_0_IOS ,
NET6_0_MACOS , NET6_0_MACCATALYST , NET6_0_TVOS ,
NET6_0_WINDOWS , NET5_0 , NETCOREAPP , NETCOREAPP3_1
, NETCOREAPP3_0 , ,
NETCOREAPP2_2 , NETCOREAPP2_1
NETCOREAPP2_0 , ,
NETCOREAPP1_1 , NETCOREAPP1_0
NET6_0_OR_GREATER , NET6_0_ANDROID_OR_GREATER ,
NET6_0_IOS_OR_GREATER , NET6_0_MACOS_OR_GREATER ,
NET6_0_MACCATALYST_OR_GREATER ,
NET6_0_TVOS_OR_GREATER , NET6_0_WINDOWS_OR_GREATER ,
NET5_0_OR_GREATER , NETCOREAPP_OR_GREATER ,
NETCOREAPP3_1_OR_GREATER , NETCOREAPP3_0_OR_GREATER ,
NETCOREAPP2_2_OR_GREATER , NETCOREAPP2_1_OR_GREATER ,
NETCOREAPP2_0_OR_GREATER , NETCOREAPP1_1_OR_GREATER ,
NETCOREAPP1_0_OR_GREATER

Hinweise :
Versionslose Symbole werden unabhängig von der Version definiert, die Sie als Ziel verwenden.
Versionsspezifische Symbole werden nur für die Version definiert, die Sie als Ziel verwenden.
Die XXX_OR_GREATER -Symbole werden für die Zielversion und alle früheren Versionen definiert.

NOTE
Für herkömmliche Projekte, die kein SDK-Format aufweisen, müssen Sie die Symbole für die bedingte Kompilierung für die
verschiedenen Zielframeworks in Visual Studio über die Eigenschaftenseite des Projekts manuell konfigurieren.

Andere vordefinierte Symbole beinhalten die Konstanten DEBUG und TRACE . Sie können die für das Projekt
festgelegten Werte mit #define überschreiben. Das DEBUG-Symbol beispielsweise wird abhängig von den
Buildkonfigurationseigenschaften (Modus „Debug“ oder „Release“) automatisch festgelegt.
Im folgenden Beispiel wird gezeigt, wie Sie ein MYTEST -Symbol für eine Datei definieren und dann die Werte
der Symbole MYTEST und DEBUG testen. Die Ausgabe dieses Beispiels hängt davon ab, ob Sie das Projekt im
Konfigurationsmodus Debug oder Release erstellen.

#define MYTEST
using System;
public class MyClass
{
static void Main()
{
#if (DEBUG && !MYTEST)
Console.WriteLine("DEBUG is defined");
#elif (!DEBUG && MYTEST)
Console.WriteLine("MYTEST is defined");
#elif (DEBUG && MYTEST)
Console.WriteLine("DEBUG and MYTEST are defined");
#else
Console.WriteLine("DEBUG and MYTEST are not defined");
#endif
}
}

Im folgenden Beispiel wird gezeigt, wie für andere Zielframeworks zu testen, damit Sie neuere APIs möglichst
verwenden können:

public class MyClass


{
static void Main()
{
#if NET40
WebClient _client = new WebClient();
#else
HttpClient _client = new HttpClient();
#endif
}
//...
}

Definieren von Symbolen


Mit den folgenden beiden Präprozessoranweisungen können Sie Symbole für die bedingte Kompilierung
definieren oder eine Definition aufheben:
#define : Definiert ein Symbol.
#undef : Hebt die Definition eines Symbols auf.

Mit #define wird ein Symbol definiert. Wenn Sie das Symbol als Ausdruck verwenden, der an die #if -
Anweisung übergeben wird, wird der Ausdruck als true ausgewertet, wie in folgendem Beispiel dargestellt:

#define VERBOSE

#if VERBOSE
Console.WriteLine("Verbose output version");
#endif
NOTE
Die #define -Direktive kann nicht zur Deklaration konstanter Werte wie in C und C++ verwendet werden. Definieren Sie
Konstanten in C# als statische Member einer Klasse oder einer Struktur. Wenn Sie über mehrere solcher Konstanten
verfügen, erwägen Sie, eine separate "Constants"-Klasse zu erstellen.

Symbole können verwendet werden, um Bedingungen für die Kompilierung anzugeben. Ein Symbol kann
entweder mit #if oder mit #elif überprüft werden. Für die bedingte Kompilierung kann auch
ConditionalAttribute verwendet werden. Ein Symbol kann zwar definiert werden, aber es kann ihm kein Wert
zugewiesen werden. Die #define -Direktive muss in einer Datei vor allen Anweisungen, bei denen es sich nicht
um Präprozessordirektiven handelt, verwendet werden. Sie können ein Symbol auch mit der Compileroption
DefineConstants definieren. Die Definition eines Symbols kann mit #undef aufgehoben werden.

Definieren von Bereichen


Sie können Codebereiche definieren, die mithilfe der beiden folgenden Präprozessoranweisungen in einer
Gliederung reduziert werden können:
#region : Beginnt einen Bereich.
#endregion : Beendet einen Bereich.

Mit #region können Sie einen Codeblock festlegen, der bei Verwendung der Gliederungsfunktion des Code-
Editors erweitert oder reduziert werden kann. Es ist bei längeren Codedateien praktischer, einen oder mehrere
Bereiche zu reduzieren oder auszublenden, sodass Sie sich auf den Teil der Datei konzentrieren können, an dem
Sie gerade arbeiten. Das folgende Beispiel veranschaulicht, wie Sie einen Bereich definieren:

#region MyClass definition


public class MyClass
{
static void Main()
{
}
}
#endregion

Ein #region -Block muss mit einer #endregion -Anweisung beendet werden. Ein #region -Block kann sich nicht
mit einem #if -Block überschneiden. Allerdings kann ein #region -Block in einen #if -Block und ein #if -
Block in einen #region -Block geschachtelt werden.

Fehler- und Warnungsinformationen


Mit den folgenden Anweisungen weisen Sie den Compiler an, benutzerdefinierte Compilerfehler und -
warnungen zu generieren und Zeileninformationen zu steuern:
#error : Generiert einen Compilerfehler mit einer angegebenen Meldung.
#warning : Generiert eine Compilerwarnung mit einer angegebenen Meldung.
#line : Ändert die Zeilennummer, die mit Compilermeldungen gedruckt wird.

Mit #error können Sie von einem bestimmten Ort in Ihrem Code aus eine benutzerdefinierte Fehlermeldung
CS1029 generieren. Beispiel:

#error Deprecated code in this method.


NOTE
Der Compiler behandelt #error version auf besondere Weise und meldet den Compilerfehler CS8304 mit einer
Nachricht, die die verwendeten Compiler- und Sprachversionen enthält.

Mit #warning können Sie von einem bestimmten Ort in Ihrem Code aus eine Compilerwarnung CS1030 der
Stufe 1 generieren. Beispiel:

#warning Deprecated code in this method.

Mit #line können Sie die Zeilennummer des Compilers und (optional) die Dateinamensausgabe für Fehler und
Warnungen bearbeiten.
Das folgende Beispiel zeigt, wie Sie zwei Warnungen melden können, die Zeilennummern zugeordnet sind. Die
#line 200 -Anweisung erzwingt die Nummer 200 der nächsten Zeile (obwohl der Standardwert #6 ist), und bis
zur nächsten #line -Anweisung wird der Dateiname als „Special“ gemeldet. Die #line default -
Standardanweisung legt die Zeilennummerierung auf deren Standardnummerierung fest, bei der die Zeilen
gezählt werden, die von der vorherigen Anweisung neu nummeriert wurden.

class MainClass
{
static void Main()
{
#line 200 "Special"
int i;
int j;
#line default
char c;
float f;
#line hidden // numbering not affected
string s;
double d;
}
}

Bei der Kompilierung wird die folgende Ausgabe erzeugt:

Special(200,13): warning CS0168: The variable 'i' is declared but never used
Special(201,13): warning CS0168: The variable 'j' is declared but never used
MainClass.cs(9,14): warning CS0168: The variable 'c' is declared but never used
MainClass.cs(10,15): warning CS0168: The variable 'f' is declared but never used
MainClass.cs(12,16): warning CS0168: The variable 's' is declared but never used
MainClass.cs(13,16): warning CS0168: The variable 'd' is declared but never used

Die #line -Anweisung könnte in einem automatischen Zwischenschritt im Buildprozess verwendet werden.
Wenn beispielsweise Zeilen aus der ursprünglichen Quellcodedatei entfernt würden, Sie jedoch trotzdem
möchten, dass der Compiler eine Ausgabe basierend auf der ursprünglichen Zeilennummerierung in der Datei
generiert, könnten Sie Zeilen entfernen und anschließend die ursprüngliche Zeilennummerierung mit #line
simulieren.
Die #line hidden -Anweisung blendet die aufeinanderfolgenden Zeilen im Debugger aus, sodass alle Zeilen
zwischen einer #line hidden -Anweisung und der nächsten #line -Anweisung (vorausgesetzt, es handelt sich
nicht um eine weitere #line hidden -Anweisung) übersprungen werden, wenn der Entwickler den Code
durchläuft. Diese Option kann auch dazu verwendet werden, ASP.NET die Möglichkeit zu geben, zwischen
benutzerdefiniertem und computergeneriertem Code zu unterscheiden. Obwohl ASP.NET der primäre Anwender
dieser Funktion ist, werden sich wahrscheinlich mehr Quellgeneratoren diese zunutze machen.
Eine #line hidden -Anweisung hat keine Auswirkung auf Dateinamen oder Zeilennummern bei der
Fehlerberichterstattung. Das bedeutet, wenn der Compiler in einem ausgeblendeten Block einen Fehler findet,
meldet er den aktuellen Dateinamen und die Zeilennummer des Fehlers.
Die #line filename -Anweisung gibt den Dateinamen an, von dem Sie möchten, dass er in der Compilerausgabe
erscheint. Standardmäßig wird der tatsächliche Name der Quellcodedatei verwendet. Der Dateiname muss in
doppelten Anführungszeichen ("") und hinter einer Zeilennummer stehen.
Das folgende Beispiel zeigt, wie der Debugger die ausgeblendeten Zeilen im Code ignoriert. Wenn Sie das
Beispiel ausführen, werden drei Textzeilen angezeigt. Wenn Sie jedoch wie im Beispiel gezeigt einen Haltepunkt
setzen und F10 drücken, um den Code zu durchlaufen, wird die ausgeblendete Zeile vom Debugger ignoriert.
Die ausgeblendete Zeile wird selbst dann vom Debugger ignoriert, wenn Sie einen Haltepunkt an dieser Zeile
setzen.

// preprocessor_linehidden.cs
using System;
class MainClass
{
static void Main()
{
Console.WriteLine("Normal line #1."); // Set break point here.
#line hidden
Console.WriteLine("Hidden line.");
#line default
Console.WriteLine("Normal line #2.");
}
}

Pragmas
#pragma gibt dem Compiler spezielle Anweisungen für die Kompilierung der Datei, in der es auftritt. Die
Anweisungen müssen vom Compiler unterstützt werden. Das heißt, Sie können mit #pragma keine
benutzerdefinierten Präprozessoranweisungen erstellen.
#pragma warning : Aktiviert oder deaktiviert Warnungen.
#pragma checksum : Generiert eine Prüfsumme.

#pragma pragma-name pragma-arguments

Dabei stellt pragma-name den Namen eines erkannten Pragmas und pragma-arguments die pragmaspezifischen
Argumente dar.
#pragma warning
#pragma warning kann bestimmte Warnungen aktivieren oder deaktivieren.

#pragma warning disable warning-list


#pragma warning restore warning-list

Dabei ist warning-list eine durch Trennzeichen getrennte Liste mit Warnungsnummern. Das Präfix „CS“ ist
optional. Wenn keine Warnzahlen angegeben werden, deaktiviert disable alle Warnungen und restore
aktiviert sie.
NOTE
Um Warnzahlen in Visual Studio zu suchen, erstellen Sie Ihr Projekt und suchen Sie nach den Warnzahlen im Fenster
Ausgabe .

disable wirkt sich ab der nächsten Zeile der Quelldatei aus. Die Warnung wird in der Zeile nach restore
wiederhergestellt. Enthält die Datei kein restore , werden die Warnungen in der ersten Zeile aller späteren
Dateien derselben Kompilierung im Standardzustand wiederhergestellt.

// pragma_warning.cs
using System;

#pragma warning disable 414, CS3021


[CLSCompliant(false)]
public class C
{
int i = 1;
static void Main()
{
}
}
#pragma warning restore CS3021
[CLSCompliant(false)] // CS3021
public class D
{
int i = 1;
public static void F()
{
}
}

#pragma-Prüfsumme
Erstellt für Quelldateien Prüfsummen, um beim Debuggen von ASP.NET-Seiten zu helfen.

#pragma checksum "filename" "{guid}" "checksum bytes"

Dabei ist "filename" der Name der Datei, die auf Änderungen oder Updates überwacht werden soll, "{guid}"
die GUID (Global Unique Identifier) für den Hashalgorithmus und "checksum_bytes" die Zeichenfolge von
Hexadezimalziffern, die die Bytes der Prüfsumme darstellen. Dabei muss es sich um eine gerade Anzahl
hexadezimaler Ziffern handeln. Eine ungerade Anzahl von Ziffern führt zu einer Warnung zur Kompilierzeit, und
die Anweisung wird ignoriert.
Der Visual Studio-Debugger verwendet eine Prüfsumme, um sicherzustellen, dass immer die richtige Quelle
gefunden wird. Der Compiler berechnet die Prüfsumme für eine Quelldatei, und speichert das Ergebnis in der
Program Database-Datei (PDB). Der Debugger verwendet anschließend die PDB-Datei, um sie mit der
Prüfsumme zu vergleichen, die für die Quelldatei berechnet wird.
Diese Lösung funktioniert nicht bei ASP.NET-Projekten, weil die berechnete Prüfsumme für die generierte
Quelldatei und nicht für die ASPX-Datei gilt. #pragma checksum stellt für ASP.NET-Seiten Unterstützung von
Prüfsummen bereit, um dieses Problem zu beheben.
Wenn Sie ein ASP.NET-Projekt in Visual C# erstellen, enthält die generierte Quelldatei eine Prüfsumme für die
ASPX-Datei, von der die Quelle generiert wird. Der Compiler schreibt anschließend diese Informationen in die
PDB-Datei.
Findet der Compiler keine #pragma checksum -Anweisung in der Datei, berechnet er die Prüfsumme und schreibt
den Wert in die PDB-Datei.
class TestClass
{
static int Main()
{
#pragma checksum "file.cs" "{406EA660-64CF-4C82-B6F0-42D48172A799}" "ab007f1d23d9" // New checksum
}
}
C#-Compileroptionen
04.11.2021 • 2 minutes to read

In diesem Abschnitt werden die vom C#-Compiler interpretierten Optionen beschrieben. Optionen werden in
separaten Artikeln gruppiert, basierend darauf, was sie steuern, wie z. B. Sprachfeatures, Codegenerierung und
Ausgabe. Verwenden Sie das Inhaltsverzeichnis, um zwischen diesen zu navigieren.

Möglichkeiten zum Festlegen von Optionen


Es gibt zwei verschiedene Möglichkeiten zum Festlegen von Compileroptionen in .NET-Projekten:
In Ihrer *CSPROJ-Datei
Sie können MSBuild-Eigenschaften für jede Compileroption in Ihrer *CSPROJ-Datei im XML-Format
hinzufügen. Der Eigenschaftsname ist derselbe wie der Name der Compileroption. Der Wert der
Eigenschaft legt den Wert der Compileroption fest. Der folgende Projektdateiausschnitt legt
beispielsweise die LangVersion -Eigenschaft fest.

<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>

Weitere Informationen zum Festlegen von Optionen in Projektdateien finden Sie im Artikel MSBuild-
Eigenschaften für .NET SDK-Projekte.
Ver wenden der Visual Studio Eigenschaftenseiten
Visual Studio bietet Eigenschaftenseiten zum Bearbeiten von Buildeigenschaften. Weitere Informationen
dazu finden Sie unter Verwalten von Projekt- und Projektmappeneigenschaften: Windows oder Verwalten
von Projekt- und Projektmappeneigenschaften: Mac.
.NET Framework-Projekte

IMPORTANT
Dieser Abschnitt gilt nur für .NET Framework-Projekte.

Zusätzlich zu den oben beschriebenen Mechanismen können Sie Compileroptionen mithilfe von zwei
zusätzlichen Methoden für .NET Framework-Projekte festlegen:
Befehlszeilenargumente für .NET Framework-Projekte : In .NET Framework-Projekten wird csc.exe
anstelle von dotnet build verwendet, um Projekte zu erstellen. Sie können Befehlszeilenargumente für
csc.exe für .NET Framework-Projekte angeben.
Kompilier te ASP.NET-Seiten : .NET Framework-Projekte verwenden einen Abschnitt der Datei web.config
zum Kompilieren von Seiten. Für das neue Buildsystem und ASP.NET Core-Projekte werden Optionen aus der
Projektdatei entnommen.
Die Bezeichnung für einige Compileroptionen hat sich aus csc.exe und .NET Framework-Projekten in das neue
MSBuild-System geändert. Die neue Syntax wird in diesem Abschnitt verwendet. Beide Versionen werden oben
auf jeder Seite aufgelistet. Für csc.exe werden alle Argumente nach der Option und einem Doppelpunkt
aufgelistet. Die -doc -Option wäre beispielsweise:
-doc:DocFile.xml

Sie können den C#-Compiler aufrufen, indem Sie den Namen seiner ausführbaren Datei (csc.exe) in der
Befehlszeile eingeben.
Für .NET Framework-Projekte können Sie csc.exe auch über die Befehlszeile ausführen. Jede Compileroption ist
in zwei Varianten verfügbar: -option und /option . In .NET Framework-Webprojekten geben Sie Optionen für
die Kompilierung von Code Behind in der Datei web.config an. Weitere Informationen finden Sie unter
<compiler>Element.
Wenn Sie das Fenster Developer-Eingabeaufforderung für Visual Studio verwenden, werden alle
erforderlichen Umgebungsvariablen für Sie festgelegt. Weitere Informationen zum Zugreifen auf dieses Tool
finden Sie unter Developer-Eingabeaufforderung für Visual Studio.
Die ausführbare Datei csc.exe befindet sich in der Regel im Windows-Verzeichnis im Ordner
„Microsoft.NET\Framework\ <Version> “. Der Speicherort unterscheidet sich je nach Konfiguration auf den
einzelnen Computern. Wenn mehrere Versionen von .NET Framework auf dem Computer installiert sind, sind
auch mehrere Versionen dieser Datei vorhanden. Weitere Informationen zu dieser Art von Installation finden Sie
unter How to: determine which versions of the .NET Framework are installed (Vorgehensweise: Bestimmen der
installierten .NET Framework-Version).
C#-Compileroptionen für Sprachfunktionsregeln
04.11.2021 • 6 minutes to read

Mit den folgenden Optionen wird gesteuert, wie der Compiler Sprachfunktionen interpretiert. Die neue
MSBuild-Syntax wird fett formatier t dargestellt. Die ältere csc.exe-Syntax wird in code style dargestellt.
CheckForOverflowUnderflow / -checked : Generiert Überlaufüberprüfungen.
AllowUnsafeBlocks / -unsafe : Lässt „unsicheren“ Code zu.
DefineConstants / -define : Definiert Symbole für bedingte Kompilierung.
LangVersion / -langversion : Gibt die Sprachversion an, z. B. default (letzte Hauptversion) oder latest
(neueste Version, einschließlich Nebenversionen).
Nullable / -nullable : Lässt Kontext mit Nullwerten oder Warnungen mit Nullwerten zu.

CheckForOverflowUnderflow
Die Option CheckForOverflowUnderflow gibt an, ob eine Anweisung der Ganzzahlarithmetik, die einen Wert
außerhalb des Bereichs des Datentyps nach sich zieht, eine Laufzeitausnahme verursacht.

<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>

Eine Anweisung der Ganzzahlarithmetik, die im Rahmen eines checked - oder unchecked -Schlüsselworts liegt,
ist nicht der Auswirkung der Option CheckForOverflowUnderflow unterworfen. Wenn eine Anweisung der
Ganzzahlarithmetik, die sich nicht im Rahmen eines checked - oder unchecked -Schlüsselworts befindet, einen
Wert außerhalb des Bereichs des Datentyps ergibt, und CheckForOverflowUnderflow den Wert true
aufweist, löst die Anweisung zur Laufzeit eine Ausnahme aus. Wenn CheckForOverflowUnderflow den Wert
false aufweist, löst die Anweisung zur Laufzeit keine Ausnahme aus. Der Standardwert für diese Option lautet
false ; die Überlaufüberprüfung ist deaktiviert.

AllowUnsafeBlocks
Die Compileroption AllowUnsafeBlocks ermöglicht das Kompilieren von Code, der das Schlüsselwort unsafe
verwendet. Der Standardwert für diese Option ist false , was bedeutet, dass unsicherer Code unzulässig ist.

<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

Weitere Informationen zu unsicherem Code finden Sie unter Unsicherer Code und Zeiger.

DefineConstants
Die Option DefineConstants definiert Symbole in allen Quellcodedateien Ihres Programms.

<DefineConstants>name;name2</DefineConstants>

Diese Option gibt die Namen eines oder mehrerer Symbole an, die Sie definieren möchten. Die Option
DefineConstants hat dieselbe Auswirkung wie die Verwendung einer #define-Präprozessoranweisung, außer
dass die Compileroption für alle Dateien im Projekt gültig ist. Ein Symbol bleibt in einer Quelldatei definiert, bis
eine #undef-Anweisung in der Quelldatei die Definition entfernt. Wenn Sie die Option -define verwenden, hat
eine #undef -Anweisung in einer Datei keinerlei Auswirkung auf andere Quellcodedateien im Projekt. Sie
können die durch diese Option erstellten Symbole in Verbindung mit #if, #else, #elif, und #endif verwenden, um
Quelldateien bedingt zu kompilieren. Der C#-Compiler selbst definiert keine Symbole oder Makros, die Sie in
Ihrem Quellcode verwenden können. Alle Symboldefinitionen müssen benutzerdefiniert sein.

NOTE
Die C#-Direktive #define erlaubt es nicht, einem Symbol wie in Sprachen wie C++ einen Wert zuzuweisen.
Beispielsweise kann #define nicht zum Erstellen eines Makros oder zum Definieren einer Konstante verwendet werden.
Falls Sie eine Konstante definieren müssen, verwenden Sie eine enum -Variable. Wenn Sie ein C++-übliches Makro
erstellen möchten, erwägen Sie Alternativen wie Generics. Da Makros sehr fehleranfällig sind, ist ihre Verwendung in C#
nicht zugelassen. Es stehen jedoch sicherere Alternativen zur Verfügung.

LangVersion
Führt dazu, dass der Compiler nur Syntax akzeptiert, die in der ausgewählten C#-Sprachspezifikation enthalten
ist.

<LangVersion>9.0</LangVersion>

Folgende Werte sind gültig:

W ERT B EDEUT UN G

preview Der Compiler akzeptiert jede gültige Sprachsyntax der


letzten Vorschauversion.

latest Der Compiler akzeptiert die Syntax der neuesten


veröffentlichte Version des Compilers (einschließlich
Nebenversionen).

latestMajor ( default ) Der Compiler akzeptiert die Syntax der neuesten


veröffentlichte Hauptversion des Compilers.

10.0 Der Compiler akzeptiert nur Syntax, die in C# 10.0 oder


niedriger enthalten ist.

9.0 Der Compiler akzeptiert nur Syntax, die in C# 9.0 oder


niedriger enthalten ist.

8.0 Der Compiler akzeptiert nur Syntax, die in C# 8.0 oder


niedriger enthalten ist.

7.3 Der Compiler akzeptiert nur Syntax, die in C# 7.3 oder früher
enthalten ist.

7.2 Der Compiler akzeptiert nur Syntax, die in C# 7.2 oder früher
enthalten ist.

7.1 Der Compiler akzeptiert nur Syntax, die in C# 7.1 oder früher
enthalten ist.

7 Der Compiler akzeptiert nur Syntax, die in C# 7.0 oder früher


enthalten ist.
W ERT B EDEUT UN G

6 Der Compiler akzeptiert nur Syntax, die in C# 6.0 oder früher


enthalten ist.

5 Der Compiler akzeptiert nur Syntax, die in C# 5.0 oder früher


enthalten ist.

4 Der Compiler akzeptiert nur Syntax, die in C# 4.0 oder früher


enthalten ist.

3 Der Compiler akzeptiert nur Syntax, die in C# 3.0 oder früher


enthalten ist.

ISO-2 (oder 2 ) Der Compiler akzeptiert nur Syntax, die in ISO/IEC


23270:2006 C# (2.0) enthalten ist.

ISO-1 (oder 1 ) Der Compiler akzeptiert nur Syntax, die in ISO/IEC


23270:2003 C# (1.0/1.2) enthalten ist.

Die Standardsprachversion ist vom Zielframework für Ihre Anwendung und der installierten Version des SDK
oder von Visual Studio abhängig. Diese Regeln werden in der C#-Sprachversionsverwaltung definiert.
Metadaten, auf die von Ihrer C#-Anwendung verwiesen wird, unterliegen nicht der Compileroption
LangVersion .
Da jede Version des C#-Compilers Erweiterungen der Sprachspezifikation enthält, bietet LangVersion Ihnen
nicht die gleiche Funktionalität wie die einer früheren Compilerversion.
Darüber hinaus sind die neue Syntax und die neuen Features nicht unbedingt an die spezifische Version des
Frameworks gebunden, während C#-Versionsupdates für gewöhnlich mit den Releases von .NET Framework
einhergehen. Während die neuen Features ein Compilerupdate erfordern, das mit der C#-Revision veröffentlicht
wird, hat jedes Feature seine eigene mindestens erforderliche .NET-API- oder CLR-Anforderungen, durch die es
auf abwärtskompatiblen Frameworks ausgeführt werden kann, indem NuGet-Pakete oder andere Bibliotheken
einbezogen werden.
Unabhängig von der verwendeten LangVersion -Einstellung verwenden Sie die aktuelle Version der CLR, um
Ihre EXE- oder DLL-Dateien zu erstellen. Davon ausgenommen sind Friend-Assemblys und
ModuleAssemblyName , die unter -langversion:ISO-1 ausgeführt werden.
Weitere Möglichkeiten zum Angeben der C#-Sprachversion finden Sie unter C#-Sprachversionsverwaltung.
Informationen zum programmgesteuerten Festlegen dieser Compileroption finden Sie unter LanguageVersion.
C#-Sprachspezifikation
VERSIO N L IN K B ESC H REIB UN G

C# 7.0 und höher Derzeit nicht verfügbar

C# 6.0 Link C#-Spezifikation Version 6, inoffizieller


Entwurf: .NET Foundation

C# 5.0 PDF herunterladen Standard ECMA-334, 5. Edition

C# 3.0 DOC herunterladen C#-Programmiersprachenspezifikation


Version 3.0: Microsoft Corporation
VERSIO N L IN K B ESC H REIB UN G

C# 2.0 PDF herunterladen Standard ECMA-334, 4. Edition

C# 1.2 DOC herunterladen C#-Programmiersprachenspezifikation


Version 1.2: Microsoft Corporation

C# 1.0 DOC herunterladen C#-Programmiersprachenspezifikation


Version 1.0: Microsoft Corporation

Mindestens erforderliche SDK -Version, die erforderlich ist, um alle Sprachfeatures zu unterstützen
In der folgenden Tabelle sind die Mindestversionen des SDK mit dem C#-Compiler aufgeführt, der die
entsprechende Sprachversion unterstützt:

C #- VERSIO N M IN DEST VERSIO N DES SDK

C# 10.0 Microsoft Visual Studio/Build Tools 2022 oder .NET 6.0 SDK

C# 9.0 Microsoft Visual Studio/Build Tools 2019, Version 16.8 oder


.NET 5.0 SDK

C# 8.0 Microsoft Visual Studio/Build Tools 2019, Version 16.3, oder


.NET Core 3.0 SDK

C# 7.3 Microsoft Visual Studio/Build Tools 2017, Version 15.7

C# 7.2 Microsoft Visual Studio/Build Tools 2017, Version 15.5

C# 7.1 Microsoft Visual Studio/Build Tools 2017, Version 15.3

C# 7.0 Microsoft Visual Studio/Build Tools 2017

C# 6 Microsoft Visual Studio/Build Tools 2015

C# 5 Microsoft Visual Studio/Build Tools 2012 oder gebündelter


.NET Framework 4.5-Compiler

C# 4 Microsoft Visual Studio/Build Tools 2010 oder gebündelter


.NET Framework 4.0-Compiler

C# 3 Microsoft Visual Studio/Build Tools 2008 oder gebündelter


.NET Framework 3.5-Compiler

C# 2 Microsoft Visual Studio/Build Tools 2005 oder gebündelter


.NET Framework 2.0-Compiler

C# 1.0/1.2 Microsoft Visual Studio/Build Tools .NET 2002 oder


gebündelter .NET Framework 1.0-Compiler

Nullwerte zulässig
Mit der Option Nullable können Sie den Kontext festlegen, der Nullwerte zulässt. Der Standardwert für diese
Option ist disable .
<Nullable>enable</Nullable>

Eines der folgenden Argumente muss verwendet werden: enable , disable , warnings oder annotations . Das
Argument enable aktiviert den Nullwerte zulassenden Kontext. Durch Angeben von disable wird der
Nullwerte zulassende Kontext deaktiviert. Wenn das warnings -Argument angegeben wird,wird der Nullwerte
zulassende Warnungskontext aktiviert. Wenn das annotations -Argument angegeben wird,wird der Nullwerte
zulassende Anmerkungskontext aktiviert.
Die Ablaufsteuerungsanalyse wird dazu verwendet, die NULL-Zulässigkeit von Variablen in ausführbarem Code
abzuleiten. Die abgeleitete NULL-Zulässigkeit einer Variable ist unabhängig von der deklarierten NULL-
Zulässigkeit der Variable. Methodenaufrufe werden auch analysiert, wenn sie bedingt ausgelassen werden. Dies
gilt beispielsweise für die Methode Debug.Assert im Releasemodus.
Das Aufrufen von Methoden, die mit den folgenden Attributen versehen sind, wirken sich auf die
Ablaufsteuerungsanalyse aus:
Einfache Vorbedingungen: AllowNullAttribute und DisallowNullAttribute
Einfache Nachbedingungen: MaybeNullAttribute und NotNullAttribute
Bedingte Nachbedingungen: MaybeNullWhenAttribute und NotNullWhenAttribute
DoesNotReturnIfAttribute (z. B. DoesNotReturnIf(false) für Debug.Assert) und DoesNotReturnAttribute
NotNullIfNotNullAttribute
Nachbedingungen für Member: MemberNotNullAttribute(String) und MemberNotNullAttribute(String[])

IMPORTANT
Der globale Nullable-Kontext gilt nicht für generierte Codedateien. Der Nullable-Kontext ist unabhängig von dieser
Einstellung für alle als generiert gekennzeichneten Quelldateien deaktiviert. Es gibt viel Möglichkeiten, eine Datei als
generiert zu markieren:
1. Geben Sie in der EDITORCONFIG-Datei generated_code = true in einem Abschnitt an, der für diese Datei gilt.
2. Fügen Sie <auto-generated> oder <auto-generated/> ganz oben in der Datei in einem Kommentar ein. Dabei
kann es sich um eine beliebige Zeile des Kommentars handeln, jedoch muss es sich beim Kommentarblock um das
erste Element in der Datei handeln.
3. Beginnen Sie den Dateinamen mit TemporaryGeneratedFile_ .
4. Enden Sie den Dateinamen mit .designer.cs, .generated.cs, .g.cs oder .g.i.cs.
Generatoren können die Präprozessoranweisung #nullable verwenden.
C#-Compileroptionen, die die Compilerausgabe
steuern
04.11.2021 • 7 minutes to read

Die folgenden Optionen steuern die Generierung von Compilerausgaben.

M SB UIL D C SC . EXE B ESC H REIB UN G

DocumentationFile -doc: Generiert eine XML-DOC-Datei aus


/// -Kommentaren.

OutputAssembly -out: Gibt die Ausgabeassemblydatei an.

PlatformTarget -platform: Gibt die Zielplattform-CPU an.

ProduceReferenceAssembly -refout: Generiert eine Referenzassembly.

TargetType -target: Gibt den Typ der Ausgabeassembly an.

DocumentationFile
Mit der Option DocumentationFile können Sie Dokumentationskommentare in eine XML-Datei einfügen.
Weitere Informationen zum Dokumentieren Ihres Codes finden Sie unter Empfohlene Tags für
Dokumentationskommentare. Der Wert gibt den Pfad zur XML-Ausgabedatei an. Die XML-Datei enthält die
Kommentare in den Quellcodedateien der Kompilierung.

<DocumentationFile>path/to/file.xml</DocumentationFile>

Die Quellcodedatei, die die Main-Anweisungen oder die Anweisungen auf oberster Ebene enthält, wird zuerst in
die XML-Datei ausgegeben. Sie werden die generierte XML-Datei häufig mit IntelliSense verwenden wollen. Der
Name der XML-Datei muss mit dem Assemblynamen identisch sein. Die XML-Datei muss sich im selben
Verzeichnis wie die Assembly befinden. Wenn in einem Visual Studio-Projekt auf die Assembly verwiesen wird,
wird auch die XML-Datei gefunden. Weitere Informationen zum Generieren von Codekommentaren finden Sie
unter Bereitstellen von Codekommentaren. Wenn Sie nicht mit <TargetType:Module> kompilieren, enthält file
die Tags <assembly> und </assembly> , die den Namen der Datei mit dem Assemblymanifest für die
Ausgabedatei angeben. Beispiele dazu finden Sie unter Verwenden der XML-Dokumentationsfeatures.

NOTE
Die Option DocumentationFile gilt für alle Dateien im Projekt. Verwenden Sie zum Deaktivieren von Warnungen im
Zusammenhang mit Dokumentationskommentare für eine bestimmte Datei oder einen Codeabschnitt #pragma warning.

OutputAssembly
Die Option OutputAssembly gibt den Namen der Ausgabedatei an. Der Ausgabepfad gibt den Ordner an, in
dem die Compilerausgabe platziert wird.
<OutputAssembly>folder</OutputAssembly>

Geben Sie den vollständigen Namen und die Erweiterung der Datei an, die Sie erstellen möchten. Wenn Sie den
Namen der Ausgabedatei nicht angeben, verwendet MSBuild den Namen des Projekts, um den Namen der
Ausgabeassembly anzugeben. Projekte im alten Stil verwenden die folgenden Regeln:
Eine EXE-Datei übernimmt den Namen aus der Quellcodedatei, die die Main -Methode oder Anweisungen
auf oberster Ebene enthält.
Eine DLL- oder NETMODULE-Datei übernimmt den Namen aus der ersten Quellcodedatei.
Alle Module, die als Teil einer Kompilierung erstellt werden, werden Dateien, die jeder Assembly zugeordnet
sind, die auch bei der Kompilierung erstellt werden. Verwenden Sie ildasm.exe, um das Assemblymanifest mit
den zugehörigen Dateien anzuzeigen.
Die Compileroption OutputAssembly ist erforderlich, damit eine EXE-Datei das Ziel einer Friend-Assembly sein
kann.

PlatformTarget
Gibt an, welche Version der CLR die Assembly ausführen kann.

<PlatformTarget>anycpu</PlatformTarget>

anycpu (Standard) kompiliert die Assembly für die Ausführung auf einer beliebigen Plattform. Ihre
Anwendung wird nach Möglichkeit als 64-Bit-Prozess ausgeführt und wechselt zurück zu 32-Bit, wenn nur
dieser Modus verfügbar ist.
anycpu32bitpreferred kompiliert die Assembly für die Ausführung auf einer beliebigen Plattform. Die
Anwendung wird auf Systemen, die sowohl 64-Bit- als auch 32-Bit-Anwendungen unterstützen, im 32-Bit-
Modus ausgeführt. Sie können diese Option nur für Projekte angeben, die auf .NET Framework 4.5 oder
höher ausgerichtet sind.
ARM kompiliert Ihre Assembly für die Ausführung auf einem Computer mit einem ARM-Prozessor
(Advanced RISC-Computer).
ARM64 kompiliert Ihre Assembly für die Ausführung durch die 64-Bit-CLR auf einem Computer mit einem
Advanced RISC Machine-Prozessor (ARM), der den A64-Anweisungssatz unterstützt.
x64 kompiliert Ihre Assembly für die 64-Bit-CLR auf einem Computer, der den AMD64- oder EM64T-
Befehlssatz unterstützt.
x86 kompiliert die Assembly für die 32-Bit-CLR (Common Language Runtime), die mit x86 kompatibel ist.
Itanium kompiliert Ihre Assembly für die Ausführung durch die 64-Bit-CLR auf einem Computer mit einem
Itanium-Prozessor.
Auf einem 64-Bit-Windows-Betriebssystem:
Mit x86 kompilierte Assemblys werden in der 32-Bit-CLR unter WOW64 ausgeführt.
Eine mit anycpu kompilierte DLL wird in derselben CLR wie der Prozess ausgeführt, in den sie geladen
wurde.
Ausführbare Dateien, die mit anycpu kompiliert werden, werden in der 64-Bit-CLR ausgeführt.
Ausführbare Dateien, die mit anycpu32bitpreferred kompiliert werden, werden in der 32-Bit-CLR
ausgeführt.
Die Einstellung anycpu32bitpreferred gilt nur für ausführbare Dateien (.exe) und erfordert .NET
Framework 4.5 oder höher. Weitere Informationen zum Entwickeln einer Anwendung, die auf einem 64-Bit-
Windows-Betriebssystem ausgeführt werden soll, finden Sie unter 64-Bit-Anwendungen.
Sie legen die Option PlatformTarget auf der Seite Buildeigenschaften für Ihr Projekt in Visual Studio fest.
Das Verhalten von anycpu weist einige zusätzliche Aspekte für .NET Core und .NET 5 sowie neuere Releases auf.
Wenn Sie anycpu festlegen, veröffentlichen Sie Ihre App und führen sie entweder mit x86- dotnet.exe oder
x64- dotnet.exe aus. Bei eigenständigen Apps verpackt der Schritt dotnet publish die ausführbare Datei für die
RID-Konfiguration.

ProduceReferenceAssembly
Die Option ProduceReferenceAssembly gibt einen Dateipfad an, in den die Verweisassembly ausgegeben
werden soll. Dies entspricht metadataPeStream in der Emit-API. filepath gibt den Pfad für die Verweisassembly
an. Dieser sollte im Allgemeinen mit der primären Assembly übereinstimmen. Die empfohlene Konvention (von
MSBuild verwendet) besteht darin, die Verweisassembly in einem Unterordner „ref/“ zu platzieren, der relativ
zur primären Assembly ist.

<ProduceReferenceAssembly>filepath</ProduceReferenceAssembly>

Verweisassemblys sind eine besondere Art von Assembly, die nur die Mindestmenge an Metadaten enthalten,
die zum Darstellen der öffentlichen API-Oberfläche der Bibliothek erforderlich sind. Sie beinhalten Deklarationen
für alle Member, die beim Verweis auf eine Assembly in Buildtools von Bedeutung sind. Verweisassemblys
schließen alle Memberimplementierungen und Deklarationen privater Member aus. Diese Member besitzen
keine beobachtbaren Auswirkungen auf ihren API-Vertrag. Weitere Informationen finden Sie unter
Verweisassemblys im .NET-Leitfaden.
Die Optionen ProduceReferenceAssembly und ProduceOnlyReferenceAssembly schließen sich
gegenseitig aus.

TargetType
Die Compileroption TargetType kann in einem der folgenden Formate angegeben werden:
librar y zum Erstellen einer Codebibliothek. librar y ist der Standardwert.
exe zum Erstellen einer EXE-Datei.
module zum Erstellen eines Moduls.
winex zum Erstellen eines Windows-Programms.
winmdobj zum Erstellen einer WINMDOBJ-Zwischendatei.
appcontainerexe zum Erstellen einer EXE-Datei für Windows 8.x Store-Apps.

NOTE
Bei .NET Framework-Zielen bewirkt diese Option (sofern Sie nicht module angeben), dass ein .NET Framework-
Assemblymanifest in einer Ausgabedatei platziert wird. Weitere Informationen finden Sie unter Assemblys in .NET und
Häufig verwendete Attribute.

<TargetType>library</TargetType>

Der Compiler erstellt nur ein Assemblymanifest pro Kompilierung. Informationen über alle Dateien in einer
Kompilierung werden im Assemblymanifest platziert. Wenn mehrere Ausgabedateien in der Befehlszeile erstellt
werden, kann nur ein Assemblymanifest erstellt werden, und es muss in die erste Ausgabedatei gehen, die in der
Befehlszeile angegeben wurde.
Wenn Sie eine Assembly erstellen, können Sie angeben, dass der ganze oder ein Teil des Codes mit dem
CLSCompliantAttribute-Attribut CLS-kompatibel ist.
Bibliothek
Die Option librar y veranlasst den Compiler, eine Dynamic Link Library (DLL) statt einer ausführbaren Datei
(EXE) zu erstellen. Die DLL wird mit der Erweiterung .dll erstellt. Wenn nichts anderes mit der Option
OutputAssembly angegeben, übernimmt der Ausgabedateiname den Namen der ersten Eingabedatei. Beim
Erstellen einer DLL-Datei ist keine Main -Methode erforderlich.
exe
Die Option exe bewirkt, dass der Compiler eine ausführbare Konsolenanwendung (EXE-Datei) erstellt. Die
ausführbare Datei wird mit der Dateiendung „.exe“ erstellt. Verwenden Sie winexe , um ein ausführbares
Windows-Programm zu erstellen. Wenn nichts anderes mit der Option OutputAssembly angegeben,
übernimmt der Ausgabedateiname den Namen der Eingabedatei, die den Einstiegspunkt (Main-Methode oder
Anweisungen der obersten Ebene) enthält. In den Quellcodedateien, die in eine EXE-Datei kompiliert werden, ist
ein und nur ein Einstiegspunkt erforderlich. Mit der Compileroption Star tupObject können Sie angeben,
welche Klasse die Main -Methode enthält, falls Ihr Code mehr als eine Klasse mit einer Main -Methode enthält.
module
Diese Option bewirkt, dass der Compiler kein Assemblymanifest generiert. Standardmäßig weist die
Ausgabedatei, die durch Kompilieren mit dieser Option erstellt wird, eine Dateierweiterung NETMODULE auf.
Eine Datei ohne Assemblymanifest kann nicht von der .NET-Runtime geladen werden. Allerdings kann eine
solche Datei mithilfe von AddModules in das Assemblymanifest integriert werden. Wird mehr als ein Modul in
einer einzigen Kompilierung erstellt, werden interne Typen in einem Modul für andere Module in der
Kompilierung verfügbar. Wenn der Code in einem Modul auf internal -Typen in einem anderen Modul
verweist, dann müssen beide Module mithilfe von AddModules in ein Assemblymanifest integriert werden.
Das Erstellen eines Moduls wird in der Visual Studio-Entwicklungsumgebung nicht unterstützt.
winexe
Die Option winexe bewirkt, dass der Compiler ein ausführbares Windows-Programm (EXE-Datei) erstellt. Die
ausführbare Datei wird mit der Dateiendung „.exe“ erstellt. Windows-Programme stellen eine
Benutzeroberfläche immer aus der .NET-Bibliothek oder mithilfe von Windows-APIs bereit. Verwenden Sie exe ,
um eine Konsolenanwendung zu erstellen. Wenn nichts anderes mit der Option OutputAssembly angegeben
wurde, übernimmt der Name der Ausgabedatei den Namen der Eingabedatei, die die Main -Methode enthält.
Nur eine Main -Methode wird in den Quellcodedateien benötigt, die in eine EXE-Datei kompiliert werden. Mit
der Option Star tupObject können Sie angeben, welche Klasse die Main -Methode enthält, falls Ihr Code mehr
als eine Klasse mit einer Main -Methode enthält.
winmdobj
Wenn Sie die Option winmdobj verwenden, erstellt der Compiler eine WINMDOBJ-Zwischendatei, die Sie in
eine Windows-Runtime-Binärdatei (WINMD-Datei) konvertieren können. Die WINMD-Datei kann dann von
verwalteten Sprachprogrammen sowie von JavaScript- und C++-Programmen verwendet werden.
Die Einstellung winmdobj signalisiert dem Compiler, dass ein Zwischenmodul erforderlich ist. Die WINMDOBJ-
Datei kann dann durch das WinMDExp-Exporttool Eingaben erhalten, um eine Windows-Metadatendatei
(WINMD-Datei) zu generieren. Die WINMD-Datei enthält sowohl den Code aus der ursprünglichen Bibliothek
sowie die WinMD-Metadaten, die von JavaScript oder C++ und von der Windows-Runtime verwendet werden.
Die Ausgabe einer Datei, die mit der Compileroption winmdobj kompiliert wurde, wird nur als Eingabe für das
WimMDExp-Exporttool verwendet. Auf die WINMDOBJ-Datei selbst wird nicht direkt verwiesen. Sofern Sie nicht
die Option OutputAssembly verwenden, übernimmt die Ausgabedatei den Namen der ersten Eingabedatei.
Eine Main -Methode ist nicht erforderlich.
appcontainerexe
Wenn Sie die Compileroption appcontainerexe verwenden, erstellt der Compiler eine ausführbare Windows-
Datei (EXE-Datei), die in einem App-Container ausgeführt werden muss. Diese Option entspricht -target:winexe,
ist jedoch für Windows 8.x Store-Apps vorgesehen.
Diese Option legt ein Bit in der portierbaren ausführbaren Datei (Portable Executable, PE) fest, damit die App in
einem App-Container ausgeführt werden muss. Wenn dieses Bit festgelegt ist, tritt ein Fehler auf, wenn die
CreateProcess-Methode versucht, die ausführbare Datei außerhalb eines App-Containers zu starten. Sofern Sie
nicht die Option OutputAssembly verwenden, erhält der Name der Ausgabedatei den Namen der
Eingabedatei, die die Main -Methode enthält.
C#-Compileroptionen, die Eingaben angeben
04.11.2021 • 5 minutes to read

Mit den folgenden Optionen werden Compilereingaben gesteuert. Die neue MSBuild-Syntax wird fett
formatier t dargestellt. Die ältere csc.exe-Syntax wird in code style dargestellt.
References / -reference oder -references : Verweist auf Metadaten aus den angegebenen
Assemblydateien.
AddModules / -addmodule : Fügt dieser Assembly ein Modul hinzu (mit target:module erstellt).
EmbedInteropTypes / -link : Bettet Metadaten aus den angegebenen Interopassemblydateien ein.

References
Die Option References veranlasst den Compiler dazu, öffentliche Typinformationen in der angegebenen Datei
in das aktuelle Projekt zu importieren, sodass die Verweismetadaten aus den angegebenen Assemblydateien
aktiviert werden.

<Reference Include="filename" />

filename ist er Name einer Datei, die ein Assemblymanifest enthält. Fügen Sie ein separates Reference -
Element für jede Datei ein, um mehrere Dateien zu importieren. Sie können einen Alias als untergeordnetes
Element des Reference -Elements definieren:

<Reference Include="filename.dll">
<Aliases>LS</Aliases>
</Reference>

Im vorherigen Beispiel ist LS der gültige C#-Bezeichner, der einen Stammnamespace darstellt, der alle
Namespaces in der Assembly filename.dll enthält. Die von Ihnen importierten Dateien müssen ein Manifest
enthalten. Verwenden Sie AdditionalLibPaths , um das Verzeichnis anzugeben, in dem sich mindestens einer
Ihrer Assemblyverweise befindet. Im Thema AdditionalLibPaths werden auch die Verzeichnisse erläutert, in
denen der Compiler nach Assemblys sucht. Damit der Compiler einen Typ in einer Assembly, und nicht in einem
Modul, erkennen kann, muss die Auflösung des Typs durch die Definition einer Instanz des Typs erzwungen
werden. Der Compiler verfügt über andere Möglichkeiten, Typnamen in einer Assembly aufzulösen.
Beispielsweise wird beim Erben von einem Typ in einer Assembly der Typname vom Compiler erkannt werden.
Manchmal ist es erforderlich, auf zwei verschiedene Versionen derselben Komponente aus einer Assembly
heraus zu verweisen. Verwenden Sie dazu das Element Aliases für das Element References für jede Datei, um
zwischen den beiden Dateien zu unterscheiden. Dieser Alias wird als Qualifizierer für den Namen der
Komponente verwendet und wird für die Komponente in einer der Dateien aufgelöst.

NOTE
Verwenden Sie in Visual Studio den Befehl Ver weis hinzufügen . Weitere Informationen finden Sie unter
Vorgehensweise: Hinzufügen und Entfernen von Verweisen mit dem Verweis-Manager.

AddModules
Mit dieser Option wird ein Modul hinzugefügt, das mit dem Schalter <TargetType>module</TargetType> in der
aktuellen Kompilierung erstellt wurde:

<AddModule Include=file1 />


<AddModule Include=file2 />

Dabei sind file , file2 Ausgabedateien, die Metadaten enthalten. Die Datei darf kein Assemblymanifest
enthalten. Trennen Sie die Dateinamen entweder mit einem Komma oder einem Semikolon, um mehr als eine
Datei zu importieren. Alle Module, die mit AddModules hinzugefügt werden, müssen sich zur Laufzeit im
gleichen Verzeichnis wie die Ausgabedatei befinden. Das bedeutet, dass Sie zur Kompilierzeit ein beliebiges
Modul in einem Verzeichnis angeben können, sich das Modul aber zur Laufzeit im Anwendungsverzeichnis
befinden muss. Wenn sich das Modul zur Laufzeit nicht im Anwendungsverzeichnis befindet, wird eine
TypeLoadException ausgelöst. file darf keine Assembly enthalten. Wenn die Ausgabedatei z. B. mit der Option
TargetType von module erstellt wurde, können ihre Metadaten mit AddModules importiert werden.
Wenn die Ausgabedatei mit einer anderen Option TargetType als module erstellt wurde, können ihre
Metadaten nicht mit AddModules , aber mit der Option References importiert werden.

EmbedInteropTypes
Bewirkt, dass der Compiler dem Projekt, das Sie aktuell kompilieren, COM-Typinformationen in den
angegebenen Assemblys bereitstellt.

<References>
<EmbedInteropTypes>file1;file2;file3</EmbedInteropTypes>
</References>

Dabei ist file1;file2;file3 eine durch Semikolons getrennte Liste von Assemblydateinamen. Wenn der
Dateiname ein Leerzeichen enthält, müssen Sie den Namen in Anführungszeichen einschließen. Die Option
EmbedInteropTypes ermöglicht es Ihnen, eine Anwendung mit eingebetteten Typinformationen
bereitzustellen. Die Anwendung kann dann Typen in einer Runtime-Assembly verwenden, die die eingebetteten
Typinformationen implementieren, ohne dass ein Verweis auf die Runtime-Assembly erforderlich ist. Wenn
verschiedene Versionen der Runtime-Assembly veröffentlicht werden, kann die Anwendung, die die
eingebetteten Typinformationen enthält, mit den verschiedenen Versionen arbeiten, ohne neu kompiliert
werden zu müssen. Ein Beispiel finden Sie unter Exemplarische Vorgehensweise: Einbetten von Typen aus
verwalteten Assemblys.
Die Option EmbedInteropTypes ist besonders nützlich, wenn Sie COM-Interop verwenden. Sie können COM-
Typen einbetten, sodass für Ihre Anwendung keine primäre Interopassembly (PIA) auf dem Zielcomputer mehr
erforderlich ist. Die Option EmbedInteropTypes weist den Compiler an, die COM-Typinformationen aus der
Interopassembly, auf die verwiesen wird, in den sich ergebenden kompilierten Code einzubetten. Der COM-Typ
wird durch den CLSID (GUID)-Wert identifiziert. Dadurch kann Ihre Anwendung auf einem Zielcomputer
ausgeführt werden, auf dem die gleichen COM-Typen mit den gleichen CLSID-Werten installiert sind.
Anwendungen, die Microsoft Office automatisieren, sind ein gutes Beispiel. Da Anwendungen wie Office in der
Regel den gleichen CLSID-Wert in den verschiedenen Versionen behalten, kann die Anwendung die COM-Typen,
auf die verwiesen wird, verwenden, wenn .NET Framework 4 oder höher auf dem Zielcomputer installiert ist
und die Anwendung Methoden, Eigenschaften oder Ereignisse verwendet, die in den COM-Typen, auf die
verwiesen wird, enthalten sind. Die Option EmbedInteropTypes bettet nur Schnittstellen, Strukturen und
Delegaten ein. Das Einbetten von COM-Klassen wird nicht unterstützt.
NOTE
Wenn Sie eine Instanz eines eingebetteten COM-Typs in Ihrem Code erstellen, müssen Sie die Instanz mithilfe der
entsprechenden Schnittstelle erstellen. Der Versuch, eine Instanz eines eingebetteten COM-Typs mit der Co-Klasse zu
erstellen, verursacht einen Fehler.

Wie die Compileroption References verwendet auch die Compileroption EmbedInteropTypes die
Antwortdatei „Csc.rsp“, die auf häufig verwendete .NET-Assemblys verweist. Verwenden Sie die Compileroption
NoConfig , wenn Sie nicht möchten, dass der Compiler die Datei „Csc.rsp“ verwendet.

// The following code causes an error if ISampleInterface is an embedded interop type.


ISampleInterface<SampleType> sample;

Typen, die über einen generischen Parameter verfügen, dessen Typ aus einer Interop-Assembly eingebettet wird,
können nicht verwendet werden, wenn dieser Typ aus einer externen Assembly stammt. Diese Einschränkung
gilt nicht für Schnittstellen. Nehmen Sie z.B. die Range Schnittstellen, die in der Microsoft.Office.Interop.Excel-
Assembly definiert wird. Wenn eine Bibliothek Interop-Typen aus der Microsoft.Office.Interop.Excel-Assembly
einbettet und eine Methode verfügbar macht, die einen generischen Typ zurückgibt, der einen Parameter mit
dem Typ Range-Schnittstelle hat, muss diese Methode eine generische Schnittstelle zurückgeben, wie im
folgenden Codebeispiel gezeigt.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Office.Interop.Excel;

public class Utility


{
// The following code causes an error when called by a client assembly.
public List<Range> GetRange1()
{
return null;
}

// The following code is valid for calls from a client assembly.


public IList<Range> GetRange2()
{
return null;
}
}

Im folgenden Beispiel kann der Clientcode die Methode aufrufen, die die generische Schnittstelle IList ohne
Fehler zurückgibt.
public class Client
{
public void Main()
{
Utility util = new Utility();

// The following code causes an error.


List<Range> rangeList1 = util.GetRange1();

// The following code is valid.


List<Range> rangeList2 = (List<Range>)util.GetRange2();
}
}
C#-Compileroptionen zum Melden von Fehlern und
Warnungen
04.11.2021 • 3 minutes to read

Mit den folgenden Optionen wird gesteuert, wie der Compiler Fehler und Warnungen meldet. Die neue
MSBuild-Syntax wird fett formatier t dargestellt. Die ältere csc.exe-Syntax wird in code style dargestellt.
WarningLevel / -warn : Legt die Warnstufe fest.
TreatWarningsAsErrors / -warnaserror : Behandelt alle Warnungen als Fehler.
WarningsAsErrors / -warnaserror : Behandelt mindestens eine Warnung als Fehler.
WarningsNotAsErrors / -warnnotaserror : Behandelt mindestens eine Warnung nicht als Fehler.
DisabledWarnings / -nowarn : Legt eine Liste deaktivierter Warnungen fest.
CodeAnalysisRuleSet / -ruleset : Gibt eine Regelsatzdatei an, die bestimmte Diagnosefunktionen
deaktiviert.
ErrorLog / -errorlog : Gibt eine Datei für das Protokollieren der gesamten Compiler- und
Analysetooldiagnose an.
Repor tAnalyzer / -reportanalyzer : Berichten zusätzlicher Diagnoseanalysetool-Informationen, z. B.
Zeitpunkt der Ausführung.

WarningLevel
Die Option WarningLevel gibt die vom Compiler anzuzeigende Warnstufe an.

<WarningLevel>3</WarningLevel>

Der Elementwert ist die Warnstufe, die Sie für die Kompilierung anzeigen möchten: Niedrigere Zahlen zeigen
nur Warnungen mit hohem Schweregrad an. Höhere Werte zeigen mehr Warnungen an. Der Wert muss 0 (null)
oder eine positive ganze Zahl sein:

WA RN ST UF E B EDEUT UN G

0 Deaktiviert die Ausgabe aller Warnungmeldungen

1 Zeigt schwerwiegende Warnmeldungen an

2 Zeigt Warnungen der Stufe 1 sowie bestimmte, weniger


schwerwiegende Warnungen an, z.B. Warnungen zum
Ausblenden von Klassenmembern

3 Zeigt Warnungen der Stufe 2 sowie bestimmte, weniger


schwerwiegende Warnungen an, z.B. Warnungen zu
Ausdrücken, immer nach true oder false ausgewertet
werden

4 (Standard) Zeigt die Warnungen aller drei Stufen sowie informative


Warnungen an
WA RN ST UF E B EDEUT UN G

5 Hier werden Warnungen der Stufe 4 sowie zusätzliche


Warnungen vom Compiler angezeigt, der in C# 9.0 enthalten
ist.

Höher als Stufe 5 Jeder Wert über Stufe 5 wird wie bei Stufe 5 behandelt. Um
sicherzustellen, dass Sie immer alle Warnungen erhalten,
wenn der Compiler mit neuen Warnstufen aktualisiert wird,
legen Sie einen beliebig großen Wert fest (z. B. 9999 ).

Um Informationen zu einem Fehler oder einer Warnung zu erhalten, schlagen Sie den Fehlercode im Hilfeindex
nach. Andere Möglichkeiten zum Abrufen von Informationen zu einem Fehler oder einer Warnung finden Sie
unter C#-Compilerfehler. Verwenden Sie TreatWarningsAsErrors , um alle Warnungen als Fehler zu
behandeln. Verwenden Sie DisabledWarnings , um bestimmte Warnungen zu deaktivieren.

TreatWarningsAsErrors
Mit der Option TreatWarningsAsErrors werden alle Warnungen als Fehler behandelt. Sie können
TreatWarningsAsErrors auch verwenden, um nur einige Warnungen als Fehler festzulegen. Wenn Sie
TreatWarningsAsErrors aktivieren, können Sie TreatWarningsAsErrors verwenden, um Warnungen
aufzulisten, die nicht als Fehler behandelt werden sollen.

<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

Alle Warnmeldungen werden stattdessen als Fehler gemeldet. Der Buildprozess wird angehalten (es werden
keine Ausgabedateien erstellt). Standardmäßig ist TreatWarningsAsErrors nicht aktiviert, was bedeutet, dass
Warnungen die Generierung einer Ausgabedatei nicht verhindern. Wenn nur bestimmte Warnungen als Fehler
behandelt werden sollen, können Sie optional eine durch Trennzeichen getrennte Liste mit Warnungsnummern
angeben, die als Fehler behandelt werden sollen. Die Gruppe aller Warnungen zur Zulassung von Nullwerten
können mithilfe der Kurzform Nullable angegeben werden. Verwenden Sie WarningLevel , um die Warnstufe
anzugeben, die vom Compiler angezeigt werden soll. Verwenden Sie DisabledWarnings , um bestimmte
Warnungen zu deaktivieren.

WarningsAsErrors und WarningsNotAsErrors


Die Optionen WarningsAsErrors und WarningsNotAsErrors setzen die Option TreatWarningsAsErrors für
eine Liste von Warnungen außer Kraft.
Aktivieren der Warnungen 0219 und 0168 als Fehler:

<WarningsAsErrors>0219,0168</WarningsAsErrors>

Deaktivieren derselben Warnungen als Fehler:

<WarningsNotAsErrors>0219,0168</WarningsNotAsErrors>

Sie verwenden WarningsAsErrors , um einen Satz von Warnungen als Fehler zu konfigurieren. Verwenden Sie
WarningsNotAsErrors , um einen Satz von Warnungen zu konfigurieren, die keine Fehler sein sollen, wenn Sie
alle Warnungen als Fehler festgelegt haben.

DisabledWarnings
Mit der Option DisabledWarnings können Sie unterdrücken, dass der Compiler mindestens eine Warnung
anzeigt. Trennen Sie mehrere Warnnummern durch ein Komma.

<DisabledWarnings>number1, number2</DisabledWarnings>

number1 -, number2 -Warnnummer(n), die der Compiler unterdrücken soll. Sie geben den numerischen Teil des
Warnungsbezeichners an. Wenn Sie z. B. CS0028 unterdrücken möchten, könnten Sie
<DisabledWarnings>28</DisabledWarnings> angeben. Der Compiler ignoriert automatisch die Warnnummern, die
an DisabledWarnings übergeben werden und in einer früheren Version gültig waren, jedoch entfernt worden
sind. CS0679 war z. B. im Compiler in Visual Studio .NET 2002 gültig, wurde aber später entfernt.
Die folgenden Warnungen können nicht durch die Option DisabledWarnings unterdrückt werden:
Compilerwarnung (Stufe 1) CS2002
Compilerwarnung (Stufe 1) CS2023
Compilerwarnung (Stufe 1) CS2029

CodeAnalysisRuleSet
Gibt eine Regelsatzdatei an, die bestimmte Diagnosefunktionen deaktiviert.

<CodeAnalysisRuleSet>MyConfiguration.ruleset</CodeAnalysisRuleSet>

Dabei stellt MyConfiguration.ruleset den Pfad zur Regelsatzdatei dar. Weitere Informationen zur Verwendung
von Regelsätzen finden Sie im Artikel in der Visual Studio-Dokumentation zu Regelsätzen.

ErrorLog
Angeben einer Datei für das Protokollieren der gesamten Compiler- und Analysetooldiagnose.

<ErrorLog>compiler-diagnostics.sarif</ErrorLog>

Die Option ErrorLog bewirkt, dass der Compiler ein SARIF-Protokoll (Static Analysis Results Interchange
Format) ausgibt. SARIF-Protokolle werden in der Regel von Tools gelesen, die die Ergebnisse des Compilers und
der Analysetooldiagnose analysieren.

ReportAnalyzer
Berichten zusätzlicher Analysetoolinformationen, z.B. der Zeitpunkt der Ausführung.

<ReportAnalyzer>true</ReportAnalyzer>

Die Option Repor tAnalyzer veranlasst den Compiler, zusätzliche MSBuild-Protokollinformationen auszugeben,
die die Leistungsmerkmale der Analysetools im Build detailliert darstellen. Sie wird in der Regel von Autoren
von Analysetools im Rahmen der Überprüfung der Analysetools verwendet.
C#-Compileroptionen, die die Codegenerierung
steuern
04.11.2021 • 4 minutes to read

Die folgenden Optionen steuern die Codegenerierung durch den Compiler. Die neue MSBuild-Syntax wird fett
formatier t dargestellt. Die ältere csc.exe-Syntax wird in code style dargestellt.
DebugType / -debug : Gibt Debuginformationen aus (oder nicht aus).
Optimize / -optimize : Aktiviert Optimierungen.
Deterministic / -deterministic : Generiert eine Byte-für-Byte äquivalente Ausgabe aus derselben
Eingabequelle.
ProduceOnlyReferenceAssembly / -refonly : Generiert eine Verweisassembly anstelle einer
vollständigen Assembly als primäre Ausgabe.

DebugType
Die Option DebugType führt dazu, dass der Compiler Debuginformationen generiert und in mindestens einer
Ausgabedatei platziert. Debuginformationen werden standardmäßig für die Debug-Buildkonfiguration
hinzugefügt. Sie ist für die Release-Buildkonfiguration standardmäßig deaktiviert.

<DebugType>pdbonly</DebugType>

Für alle Compilerversionen ab C# 6.0 gibt es keinen Unterschied zwischen pdbonly und full. Wählen Sie pdbonly
aus. Informationen zum Ändern des Speicherorts der PDB-Datei finden Sie unter PdbFile .
Folgende Werte sind gültig:

W ERT B EDEUT UN G

full Ausgeben von Debuginformationen in die PDB-Datei im


Standardformat für die aktuelle Plattform:
Windows : eine Windows-PDB-Datei.
Linux/macOS: eine portierbare PDB-Datei.

pdbonly Wie in full . Weitere Informationen finden Sie im


folgenden Hinweis.

portable Ausgeben von Debuginformationen in die PDB-Datei im


plattformübergreifenden portierbaren PDB-Format.

embedded Ausgeben von Debuginformationen in die DLL-/EXE selbst


(PDB-Datei wird nicht erzeugt) im portierbaren PDB-Format.
IMPORTANT
Die folgenden Informationen gelten nur für Compiler, die älter als C# 6.0 sind. Der Wert dieses Elements kann entweder
full oder pdbonly sein. Das Argument full, das wirksam ist, wenn Sie pdbonly nicht angeben, ermöglicht das Anfügen
eines Debuggers an das ausgeführte Programm. Durch die Angabe von pdbonly wird das Debuggen von Quellcode
möglich, wenn das Programm im Debugger gestartet wird. Der Assembler wird jedoch nur angezeigt, wenn das aktive
Programm an den Debugger angefügt ist. Verwenden Sie diese Option, um Debugbuilds zu erstellen. Wenn Sie Full
verwenden, beachten Sie, dass dies Einfluss auf die Geschwindigkeit und Größe von optimiertem JIT-Code hat und die
Codequalität mit full beeinträchtigt sein kann. Zum Generieren von Releasecode wird empfohlen, pdbonly oder keine PDB-
Datei zu verwenden. Ein Unterschied zwischen pdbonly und full besteht darin, dass der Compiler mit full ein
DebuggableAttribute-Objekt ausgibt, das den JIT-Compiler informiert, dass Debuginformationen verfügbar sind. Deshalb
wird ein Fehler ausgegeben, wenn bei Verwendung von full in Ihrem Code DebuggableAttribute auf FALSE festgelegt ist.
Weitere Informationen zur Konfiguration der Leistung einer Anwendung beim Debuggen finden Sie unter Erleichtern des
Debuggens für ein Image.

Optimieren
Die Option Optimize aktiviert oder deaktiviert die vom Compiler durchgeführten Optimierungen, damit Ihre
Ausgabedatei kleiner, schneller und effizienter wird. Die Option Optimize ist für eine Release-Buildkonfiguration
standardmäßig aktiviert. Sie ist standardmäßig für eine Debug-Buildkonfiguration deaktiviert.

<Optimize>true</Optimize>

Sie legen die Option Optimize auf der Seite Buildeigenschaften für Ihr Projekt in Visual Studio fest.
Außerdem weist Optimize die Common Language Runtime an, den Code zur Laufzeit zu optimieren.
Optimierungen sind standardmäßig deaktiviert. Geben Sie Optimize an, um Optimierungen zu aktivieren. Beim
Erstellen eines Moduls, das von einer Assembly verwendet werden soll, verwenden Sie dieselben Optimize -
Einstellungen wie die der Assembly. Es ist möglich, die Optionen Optimize und Debug zu kombinieren.

Deterministisch
Bewirkt, dass der Compiler eine Assembly erstellt, deren Byte-für-Byte-Ausgabe über Kompilierungen identisch
ist, wenn die Eingaben identisch sind.

<Deterministic>true</Deterministic>

Standardmäßig ist die Compilerausgabe eindeutig, die aus den Eingaben entsteht, da der Compiler einen
Zeitstempel und eine MVID hinzufügt, die aus Zufallszahlen generiert wird. Verwenden Sie die <Deterministic> -
Option zum Erzeugen einer deterministischen Assembly. Deren Inhalt im Binärformat muss über
Kompilierungen identisch sein, solange die Eingabe identisch ist. In einem solchen Build werden die Felder für
den Zeitstempel und die MVID durch Werte ersetzt, die aus einem Hash aller Kompilierungseingaben abgeleitet
wurden. Der Compiler berücksichtigt die folgenden Eingaben, die sich auf den Determinismus auswirken:
Die Sequenz der Befehlszeilenparameter.
Der Inhalt der rsp-Antwortdatei des Compilers.
Die genaue Version des verwendeten Compilers und seiner verwiesenen Assemblys.
Der aktuelle Verzeichnispfad.
Die binären Inhalte aller Dateien, die explizit und entweder direkt oder indirekt an den Compiler übergeben
werden, einschließlich:
Quelldateien
Assemblys, auf die verwiesen wird
Module, auf die verwiesen wird
Ressourcen
Die Schlüsseldatei mit starkem Namen
@ Antwortdateien
Analyzer
RuleSets
Andere Dateien, die möglicherweise von Analysetools verwendet werden
Die aktuelle Kultur (für die Sprache, in der die Diagnose und die Ausnahmenachrichten erstellt werden).
Die Standardcodierung (oder die aktuelle Codepage), wenn die Codierung nicht angegeben wird.
Das Vorhandensein, Nichtvorhandensein und die Inhalte der Dateien auf den Suchpfaden des Compilers (z.B.
von -lib oder -recurse angegeben).
Die CLR-Plattform (Common Language Runtime), auf der der Compiler ausgeführt wird.
Der Wert von %LIBPATH% , der das Abhängigkeitsladen des Analyzers beeinträchtigen kann.

Deterministische Kompilierung kann verwendet werden, um festzustellen, ob eine Binärdatei aus einer
vertrauenswürdigen Quelle kompiliert wird. Die deterministische Ausgabe kann nützlich sein, wenn die Quelle
öffentlich verfügbar ist. Außerdem kann festgestellt werden, ob Buildschritte, die von Änderungen in
Binärdateien abhängen, im Buildprozess verwendet werden.

ProduceOnlyReferenceAssembly
Die Option ProduceOnlyReferenceAssembly gibt an, dass eine Verweisassembly statt einer
Implementierungsassembly als primäre Ausgabe ausgegeben werden soll. Der Parameter
ProduceOnlyReferenceAssembly deaktiviert im Hintergrund die Ausgabe von PDB-Dateien, da
Verweisassemblys nicht ausgeführt werden können.

<ProduceOnlyReferenceAssembly>true</ProduceOnlyReferenceAssembly>

Verweisassemblys sind eine besondere Art von Assembly. Verweisassemblys enthalten nur die Mindestmenge
an Metadaten, die zum Darstellen der öffentlichen API-Oberfläche der Bibliothek erforderlich sind. Sie
beinhalten Deklarationen für alle Member, die beim Verweis auf eine Assembly in Buildtools von Bedeutung
sind, schließen aber alle Memberimplementierungen und Deklarationen privater Member aus, die keine
beobachtbaren Auswirkungen auf ihren API-Vertrag haben. Weitere Informationen finden Sie unter
Verweisassemblys.
Die Optionen ProduceOnlyReferenceAssembly und ProduceReferenceAssembly schließen sich
gegenseitig aus.
C#-Compileroptionen für Sicherheit
04.11.2021 • 5 minutes to read

Mit den folgenden Optionen werden Compilersicherheitsoptionen gesteuert. Die neue MSBuild-Syntax wird fett
formatier t dargestellt. Die ältere csc.exe-Syntax wird in code style dargestellt.
PublicSign / -publicsign : Signiert die Assembly öffentlich.
DelaySign / -delaysign : Assembly nur mit dem öffentlichen Teil des Schlüssels für einen starken Namen
verzögert signieren.
KeyFile / -keyfile : Gibt eine Schlüsseldatei mit einem starken Namen an.
KeyContainer / -keycontainer : Gibt einen Schlüsselcontainer mit einem starken Namen an.
HighEntropyVA / -highentropyva : Aktiviert ASLR (Address Space Layout Randomization) mit hoher
Entropie.

PublicSign
Diese Option bewirkt, dass der Compiler einen öffentlichen Schlüssel anwendet, die Assembly aber nicht
tatsächlich signiert. Mit der Option PublicSign wird zudem ein Bit in der Assembly festgelegt, das die Runtime
informiert, dass die Datei signiert ist.

<PublicSign>true</PublicSign>

Für die Option PublicSign müssen die Optionen KeyFile oder KeyContainer verwendet werden. Die
Optionen Keyfile und KeyContainer geben den öffentlichen Schlüssel an. Die Optionen Delaysign und
Publicsign schließen sich gegenseitig aus. Bei der öffentlichen Signierung, die manchmal auch als „Fake-
Signierung“ oder „OSS-Signierung“ bezeichnet wird, wird der öffentliche Schlüssel in eine Ausgabeassembly
eingefügt und das Flag „signed“ (signiert) festgelegt. Bei öffentlicher Signierung wird die Assembly jedoch nicht
wirklich mit einem privaten Schlüssel signiert. Entwickler verwenden eine öffentliche Signatur für Open-Source-
Projekte. Entwickler erstellen Assemblys, die mit veröffentlichten „vollständig signierten“ Assemblys kompatibel
sind, wenn sie keinen Zugriff auf den privaten Schlüssel haben, der zum Signieren der Assemblys verwendet
wird. Da nur wenige Benutzer tatsächlich überprüfen müssen, ob die Assembly vollständig signiert ist, sind diese
öffentlich erstellten Assemblys in fast jedem Szenario verwendbar, in dem eine vollständig signierte Assembly
verwendet werden würde.

DelaySign
Durch diese Option reserviert der Compiler Speicherplatz in der Ausgabedatei, damit digitale Signaturen später
hinzugefügt werden können.

<DelaySign>true</DelaySign>

Verwenden Sie DelaySign- , wenn die Assembly vollständig signiert werden soll. Verwenden Sie DelaySign ,
wenn Sie nur den öffentlichen Schlüssel in der Assembly platzieren möchten. Die Option DelaySign hat keine
Auswirkung, wenn Sie nicht mit KeyFile oder KeyContainer verwendet wird. Die Optionen KeyContainer und
PublicSign schließen sich gegenseitig aus. Wenn Sie eine vollständig signierte Assembly anfordern, wird vom
Compiler der Hash der Datei mit dem Manifest (Assemblymetadaten) erstellt und mit dem privaten Schlüssel
signiert. Dadurch wird eine digitale Signatur erstellt, die in der Datei gespeichert wird, die das Manifest enthält.
Wenn eine Assembly mit Verzögerung signiert wird, wird die Signatur vom Compiler nicht berechnet und
gespeichert, sondern lediglich ein Bereich in der Datei reserviert, damit die Signatur zu einem späteren
Zeitpunkt hinzugefügt werden kann. Der Compiler reserviert lediglich einen Bereich in der Datei, damit die
Signatur zu einem späteren Zeitpunkt hinzugefügt werden kann.
Mit DelaySign können Tester die Assembly im globalen Cache ablegen. Nach dem Testen können Sie die
Assembly vollständig signieren, indem Sie den privaten Schlüssel der Assembly mithilfe des Hilfsprogramms
Assembly Linker platzieren. Weitere Informationen finden Sie unter Erstellen und Verwenden von Assemblys
mit starkem Namen und Verzögertes Signieren einer Assembly.

KeyFile
Gibt den Dateinamen mit dem kryptografischen Schlüssel an.

<KeyFile>filename</KeyFile>

file ist der Name der Datei mit dem Schlüssel mit starkem Namen. Wenn diese Option verwendet wird, fügt
der Compiler den öffentlichen Schlüssel von der angegebenen Datei in das Assemblymanifest ein und signiert
anschließend die endgültige Assembly mit dem privaten Schlüssel. Geben Sie sn -k file in die Befehlszeile ein,
um eine Schlüsseldatei zu generieren. Wenn Sie mit -target:module kompilieren, wird der Name der
Schlüsseldatei im Modul gespeichert und in die Assembly integriert, die erstellt wird, wenn Sie eine Assembly
mit AddModules kompilieren. Außerdem können Sie Ihre Verschlüsselungsinformationen mit Keycontainer
an den Compiler übergeben. Verwenden Sie DelaySign , wenn die Assembly teilweise signiert werden soll.
Wenn sowohl KeyFile als auch KeyContainer in der gleichen Kompilierung angegeben werden, versucht es
der Compiler zuerst mit dem Schlüsselcontainer. Wenn dies erfolgreich ist, wird die Assembly mit den
Informationen im Schlüsselcontainer signiert. Wenn den Compiler den Schlüsselcontainer nicht findet, wird
versucht, die mit KeyFile angegebene Datei zu verwenden. Wenn dies erfolgreich ist, wird die Assembly mit den
Informationen in der Schlüsseldatei signiert, und die Schlüsselinformationen werden im Schlüsselcontainer
installiert. Bei der nächsten Kompilierung ist der Schlüsselcontainer gültig. Eine Schlüsseldatei enthält
möglicherweise nur den öffentlichen Schlüssel. Weitere Informationen finden Sie unter Erstellen und
Verwenden von Assemblys mit starkem Namen und Verzögertes Signieren einer Assembly.

KeyContainer
Gibt den Namen des kryptografischen Schlüsselcontainers an.

<KeyContainer>container</KeyContainer>

container ist der Name des Schlüsselcontainers mit dem starken Namen. Wenn die Option KeyContainer
verwendet wird, erstellt der Compiler eine Komponente, die freigegeben werden kann. Der Compiler fügt einen
öffentlichen Schlüssel vom angegebenen Container in das Assemblymanifest ein und signiert die endgültige
Assembly mit dem privaten Schlüssel. Geben Sie sn -k file in die Befehlszeile ein, um eine Schlüsseldatei zu
generieren. sn -i installiert das Schlüsselpaar im Container. Diese Option wird nicht unterstützt, wenn der
Compiler unter CoreCLR ausgeführt wird. Verwenden Sie zum Signieren einer Assembly beim Erstellen unter
CoreCLR die Option KeyFile . Wenn Sie mit TargetType kompilieren, wird der Name der Schlüsseldatei im
Modul gespeichert und in die Assembly integriert, wenn Sie dieses Modul mit AddModules in eine Assembly
kompilieren. Sie können diese Option auch als benutzerdefiniertes Attribut
(System.Reflection.AssemblyKeyNameAttribute) im Quellcode für ein beliebiges MSIL-Modul (Microsoft
Intermediate Language) angeben. Außerdem können Sie Ihre Verschlüsselungsinformationen mit KeyFile an
den Compiler übergeben. Verwenden Sie DelaySign , um den öffentlichen Schlüssel zum Assemblymanifest
hinzuzufügen, die Assembly aber erst zu signieren, nachdem sie getestet wurde. Weitere Informationen finden
Sie unter Erstellen und Verwenden von Assemblys mit starkem Namen und Verzögertes Signieren einer
Assembly.
HighEntropyVA
Die Compileroption HighEntropyVA informiert den Windows-Kernel, ob eine bestimmte ausführbare Datei
ASLR (Address Space Layout Randomization) mit hoher Entropie unterstützt.

<HighEntropyVA>true</HighEntropyVA>

Diese Option gibt an, dass eine ausführbare 64-Bit-Datei oder eine Datei, die durch die Compileroption
PlatformTarget gekennzeichnet ist, einen virtuellen Adressraum mit hoher Entropie unterstützt. Diese Option
ist standardmäßig deaktiviert. Verwenden Sie HighEntropyVA , um sie zu aktivieren.
Die Option HighEntropyVA ermöglicht den kompatiblen Versionen des Windows-Kernels, ein höheres Maß an
Entropie zu verwenden, wenn das Adressbereichlayout eines Prozesses als Teil von ASLR zufällig festgelegt wird.
Die Verwendung höherer Entropiegrade bedeutet, dass eine größere Anzahl von Adressen Speicherregionen wie
Stacks und Heaps zugewiesen werden kann. Daher ist es schwieriger, den Ort eines bestimmten
Speicherbereichs zu schätzen. Die Compileroption HighEntropyVA setzt voraus, dass die ausführbare Zieldatei
und alle von ihr abhängigen Module Zeigerwerte größer als 4 GB verarbeiten können, wenn sie als 64-Bit-
Prozess ausgeführt werden.
C#-Compileroptionen, die Ressourcen angeben
04.11.2021 • 4 minutes to read

Mit den folgenden Optionen wird gesteuert, wie der C#-Compiler Win32-Ressourcen erstellt oder importiert.
Die neue MSBuild-Syntax wird fett formatier t dargestellt. Die ältere csc.exe-Syntax wird in code style
dargestellt.
Win32Resource / -win32res : Gibt eine Win32-Ressourcendatei (RES-Datei) an.
Win32Icon / -win32icon : Verweist auf Metadaten aus den angegebenen Assemblydateien.
Win32Manifest / -win32manifest : Gibt eine Win32-Manifestdatei (XML-Datei) an.
NoWin32Manifest / -nowin32manifest : Bindet das Win32-Standardmanifest nicht ein.
Resources / -resource : Bettet die angegebene Ressource ein (Kurzform: /res).
LinkResources / -linkresources : Verknüpft die angegebene Ressource mit dieser Assembly.

Win32Resource
Die Option Win32Resource fügt eine Win32-Ressource in die Ausgabedatei ein.

<Win32Resource>filename</Win32Resource>

filename ist die Ressourcendatei, die Sie Ihrer Ausgabedatei hinzufügen möchten. Eine Win32-Ressource kann
Versions- oder Bitmapinformationen (Symbolinformationen) enthalten, anhand derer die Anwendung im Datei-
Explorer identifiziert werden kann. Wenn sie diese Option nicht angeben, generiert der Compiler
Versionsinformationen auf Grundlage der Assemblyversion.

Win32Icon
Die Option Win32Icon fügt der Ausgabedatei eine ICO-Datei hinzu, die der Ausgabedatei im Datei-Explorer das
gewünschte Aussehen verleiht.

<Win32Icon>filename</Win32Icon>

filename ist die ICO-Datei, die Sie Ihrer Ausgabedatei hinzufügen möchten. Eine ICO-Datei kann mit dem
Ressourcencompiler erstellt werden. Der Ressourcencompiler wird aufgerufen, wenn Sie ein Visual C++-
Programm kompilieren. Aus der RC-Datei wird eine ICO-Datei erstellt.

Win32Manifest
Verwenden Sie die Option Win32Manifest , um eine benutzerdefinierte Win32-Anwendungsmanifestdatei
anzugeben, die in die portierbare ausführbare Datei (Portable Executable, PE-Datei) eines Projekts eingebettet
werden soll.

<Win32Manifest>filename</Win32Manifest>

filename ist der Name und Speicherort der benutzerdefinierten Manifestdatei. Standardmäßig bettet der C#-
Compiler ein Anwendungsmanifest ein, das eine angeforderte Ausführungsebene als „asInvoker “ angibt. Er
erstellt das Manifest in demselben Ordner, in dem die ausführbare Datei erstellt wird. Wenn Sie ein
benutzerdefiniertes Manifest bereitstellen möchten, z.B. um eine angeforderte Ausführungsebene von
„highestAvailable“ oder „requireAdministrator “ anzugeben, verwenden Sie diese Option zum Angeben des
Dateinamens.

NOTE
Diese Option und die Option Win32Resources schließen sich gegenseitig aus. Wenn Sie versuchen, beide Optionen in
derselben Befehlszeile zu verwenden, erhalten Sie einen Buildfehler.

Eine Anwendung ohne Anwendungsmanifest, das eine angeforderte Ausführungsebene angibt, unterliegt der
Datei- und Registrierungsvirtualisierung unter der Funktion „Benutzerkontensteuerung“ in Windows. Weitere
Informationen finden Sie unter Benutzerkontensteuerung.
Die Anwendung unterliegt der Virtualisierung, wenn eine dieser Bedingungen erfüllt ist:
Sie verwenden die Option NoWin32Manifest und stellen in einem späteren Buildschritt oder als Teil einer
Windows-Ressourcendatei (RES -Datei) kein Manifest bereit, indem Sie die Option Win32Resource
verwenden.
Sie stellen ein benutzerdefiniertes Manifest bereit, das keine angeforderte Ausführungsebene angibt.
Visual Studio erstellt eine MANIFEST-Standarddatei und speichert sie zusammen mit der ausführbaren Datei in
den Debug- oder Releaseverzeichnissen. Sie können ein benutzerdefiniertes Manifest hinzufügen, indem Sie es
in einem Text-Editor erstellen und die Datei anschließend zum Projekt hinzufügen. Sie können auch mit der
rechten Maustaste auf das Projektsymbol im Projektmappen-Explorer klicken und anschließend Neues
Element hinzufügen und dann Anwendungsmanifestdatei auswählen. Nachdem Sie Ihre neue oder
vorhandene Manifestdatei hinzugefügt haben, wird sie in der Dropdownliste Manifest angezeigt. Weitere
Informationen finden Sie unter Seite „Anwendung“, Projekt-Designer (C#).
Sie können das Anwendungsmanifest als benutzerdefinierten Schritt nach dem Buildvorgang oder als Teil einer
Win32-Ressourcendatei bereitstellen, indem Sie die Option NoWin32Manifest verwenden. Verwenden Sie
dieselbe Option, wenn Ihre Anwendung der Datei- oder Registrierungsvirtualisierung unter Windows Vista
unterliegen soll.

NoWin32Manifest
Verwenden Sie die Option NoWin32Manifest , um den Compiler anzuweisen, kein Anwendungsmanifest in die
ausführbare Datei einzubetten.

<NoWin32Manifest />

Wenn diese Option verwendet wird, unterliegt die Anwendung der Virtualisierung unter Windows Vista, es sei
denn, Sie stellen ein Anwendungsmanifest in einer Win32-Ressourcendatei oder einem späteren Buildschritt
bereit.
Legen Sie diese Option in Visual Studio auf der Seite Application Proper ty (Anwendungseigenschaft) fest,
indem Sie in der Manifest -Dropdownliste auf die Option Create Application Without a Manifest
(Anwendung ohne ein Manifest erstellen) klicken. Weitere Informationen finden Sie unter Seite „Anwendung“,
Projekt-Designer (C#).

Ressourcen
Bettet die angegebene Ressource in die Ausgabedatei ein.
<Resources Include=filename>
<LogicalName>identifier</LogicalName>
<Access>accessibility-modifier</Access>
</Resources>

filename ist die .NET-Ressourcendatei, die in die Ausgabedatei eingebettet werden soll. identifier (optional)
ist der logische Name der Ressource: der Name, mit dem die Ressource geladen wird. Der Standardwert ist der
Name der Datei. accessibility-modifier (optional) ist die Barrierefreiheit der Ressource: öffentlich oder privat.
Der Standardwert ist public. Ressourcen sind standardmäßig in der Assembly öffentlich, wenn sie mithilfe des
C#-Compilers erstellt werden. Geben Sie private als Modifizierer der Barrierefreiheit an. Außer public und
private sind keine anderen Zugriffsmethoden zulässig. Wenn es sich bei filename um eine .NET-
Ressourcendatei handelt, die beispielsweise von Resgen.exe oder in der Entwicklungsumgebung erstellt wurde,
ist der Zugriff mit Membern im System.Resources-Namespace möglich. Weitere Informationen finden Sie unter
System.Resources.ResourceManager. Verwenden Sie für alle anderen Ressourcen die GetManifestResource -
Methoden in der Assembly-Klasse, um zur Laufzeit auf die Ressource zuzugreifen. Die Reihenfolge der
Ressourcen in der Ausgabedatei wird durch die in der Projektdatei angegebene Reihenfolge bestimmt.

LinkResources
Erstellt einen Link zur .NET-Ressource in der Ausgabedatei Die Ressourcendateien wird der Ausgabedatei nicht
hinzugefügt. LinkResources unterscheidet sich von der Option Resource , die eine Ressourcendatei in die
Ausgabedatei einbettet.

<LinkResources Include=filename>
<LogicalName>identifier</LogicalName>
<Access>accessibility-modifier</Access>
</LinkResources>

filename ist die .NET-Ressourcendatei, die Sie aus der Assembly verknüpfen möchten. identifier (optional) ist
der logische Name der Ressource: der Name, mit dem die Ressource geladen wird. Der Standardwert ist der
Name der Datei. accessibility-modifier (optional) ist die Barrierefreiheit der Ressource: öffentlich oder privat.
Der Standardwert ist public. Verknüpfte Ressourcen sind standardmäßig in der Assembly öffentlich, wenn sie
mit den C#-Compiler erstellt werden. Geben Sie private als Modifizierer der Barrierefreiheit an. Außer public
und private sind keine anderen Modifizierer zulässig. Wenn es sich bei filename um eine .NET-
Ressourcendatei handelt, die beispielsweise von Resgen.exe oder in der Entwicklungsumgebung erstellt wurde,
ist der Zugriff mit Membern im System.Resources-Namespace möglich. Weitere Informationen finden Sie unter
System.Resources.ResourceManager. Verwenden Sie für alle anderen Ressourcen die GetManifestResource -
Methoden in der Assembly-Klasse, um zur Laufzeit auf die Ressource zuzugreifen. Die in filename angegebene
Datei kann jedes Format haben. Sie können z.B. eine native DLL zu einem Teil der Assembly machen, sodass sie
im globalen Assemblycache installiert und aus verwaltetem Code in der Assembly darauf zugegriffen werden
kann. Sie können dasselbe aber auch im Assemblylinker vornehmen. Weitere Informationen finden Sie unter
Al.exe (Assembly Linker-Tool) und Arbeiten mit Assemblys und dem globalen Assemblycache.
Verschiedene C#-Compileroptionen
04.11.2021 • 2 minutes to read

Mit den folgenden Optionen wird sonstiges Compilerverhalten gesteuert. Die neue MSBuild-Syntax wird fett
formatier t dargestellt. Die ältere csc.exe-Syntax wird in code style dargestellt.
ResponseFiles / -@ : Liest die Antwortdatei für weitere Optionen.
NoLogo / -nologo : Unterdrückt die Copyrightmeldung des Compilers.
NoConfig / -noconfig : Schließt die Datei CSC.RSP nicht automatisch ein.

ResponseFiles
Mit der Option ResponseFiles können Sie eine Datei angeben, die Compileroptionen und zu kompilierende
Quellcodedateien enthält.

<ResponseFiles>response_file</ResponseFiles>

response_file gibt die Datei an, die Compileroptionen oder zu kompilierende Quellcodedateien auflistet. Die
Compileroptionen und Quellcodedateien werden vom Compiler so verarbeitet, als ob sie in der Befehlszeile
angegeben worden wären. Wenn Sie mehr als eine Antwortdatei in einer Kompilierung angeben möchten,
geben Sie mehrere Antwortdateioptionen an. In einer Antwortdatei können mehrere Compileroptionen und
Quellcodedateien in einer Zeile angezeigt werden. Eine einzelne Compileroption muss in einer Zeile angegeben
werden (und darf nicht mehrere Zeilen umfassen). Antwortdateien können Kommentare aufweisen, die mit
einem #-Symbol beginnen. Die Angabe von Compileroptionen innerhalb einer Antwortdatei entspricht dem
Ausgeben von Befehlen in der Befehlszeile. Der Compiler verarbeitet die Befehlsoptionen in der Reihenfolge, in
der sie gelesen werden. Befehlszeilenargumente können zuvor aufgeführte Optionen in Antwortdateien außer
Kraft setzen. Im Gegensatz dazu überschreiben Optionen in einer Antwortdatei zuvor in der Befehlszeile oder in
anderen Antwortdateien aufgeführte Optionen. C# stellt die Datei „csc.rsp“ bereit, die sich im selben Verzeichnis
wie die Datei „csc.exe“ befindet. Weitere Informationen zum Format der Antwortdatei finden Sie unter
NoConfig . Diese Compileroption kann weder in der Visual Studio-Entwicklungsumgebung festgelegt werden,
noch kann sie programmgesteuert geändert werden. Nachfolgend sind einige Zeilen aus einer
Beispielantwortdatei aufgeführt:

# build the first output file


-target:exe -out:MyExe.exe source1.cs source2.cs

NoLogo
Die Option NoLogo unterdrückt die Anzeige der Startinformationen beim Start des Compilers sowie die
Anzeige von Informationsmeldungen während der Kompilierung.

<NoLogo>true</NoLogo>

NoConfig
Die Option NoConfig weist den Compiler an, nicht mit der Datei csc.rsp zu kompilieren.
<NoConfig>true</NoConfig>

Die Datei csc.rsp verweist auf alle Assemblys, die in .NET Framework standardmäßig enthalten sind. Die
tatsächlichen Verweise, die die .NET-Entwicklungsumgebung von Visual Studio enthält, hängen vom Projekttyp
ab. Sie können die Datei csc.rsp ändern und zusätzliche Compileroptionen angeben, die in jeder Kompilierung
enthalten sein sollen. Wenn der Compiler nicht nach den Einstellungen in der Datei csc.rsp suchen und diese
nicht verwenden soll, geben Sie NoConfig an. Diese Compileroption steht in Visual Studio nicht zur Verfügung
und kann auch nicht programmgesteuert angepasst werden.
Erweiterte C#-Compileroptionen
04.11.2021 • 10 minutes to read

Die folgenden Optionen unterstützen erweiterte Szenarien. Die neue MSBuild-Syntax wird fett formatier t
dargestellt. Die ältere csc.exe -Syntax wird in code style dargestellt.
MainEntr yPoint , Star tupObject / -main : Gibt den Typ an, der den Einstiegspunkt enthält.
PdbFile / -pdb : Gibt den Namen der Datei mit den Debuginformationen an.
PathMap / -pathmap : Gibt eine Zuordnung für die Quellpfadnamen an, die vom Compiler ausgegeben
werden.
ApplicationConfiguration / -appconfig : Gibt eine Anwendungskonfigurationsdatei mit
Assemblybindungseinstellungen an.
AdditionalLibPaths / -lib : Gibt zusätzliche Verzeichnisse an, in denen nach Verweisen gesucht werden
soll.
GenerateFullPaths / -fullpath : Der Compiler generiert vollqualifizierte Pfade.
PreferredUILang / -preferreduilang : Gibt den Namen der bevorzugten Ausgabesprache an.
BaseAddress / -baseaddress : Gibt die Basisadresse für die zu erstellende Bibliothek an.
ChecksumAlgorithm / -checksumalgorithm : Gibt den Algorithmus zur Berechnung der
Quelldateiprüfsumme an, der in der PDB-Datei gespeichert ist.
CodePage / -codepage : Gibt die beim Öffnen von Quelldateien zu verwendende Codepage an.
Utf8Output / -utf8output : Gibt Compilernachrichten in UTF-8-Codierung aus.
FileAlignment / -filealign : Gibt die für die Ausgabedateiabschnitte verwendete Ausrichtung an.
ErrorEndLocation / -errorendlocation : Gibt die Zeile und Spalte der Endposition jedes Fehlers aus.
NoStandardLib / -nostdlib : Nicht auf die Standardbibliothek mscorlib.dll verweisen.
SubSystemVersion / -subsystemversion : Gibt die Subsystemversion dieser Assembly an.
ModuleAssemblyName / -moduleassemblyname : Der Name der Assembly, zu der dieses Modul gehört.

MainEntryPoint oder StartupObject


Diese Option gibt die Klasse an, die den Einstiegspunkt des Programms enthält, wenn mehr als eine Klasse eine
Main -Methode enthält.

<StartupObject>MyNamespace.Program</StartupObject>

oder

<MainEntryPoint>MyNamespace.Program</MainEntryPoint>

Dabei ist Program der Typ, der die Main -Methode enthält. Der angegebene Klassenname muss vollqualifiziert
sein. Er muss den vollständigen Namespace mit der Klasse, gefolgt von dem Klassennamen enthalten. Wenn
sich die Main -Methode beispielsweise in der Program -Klasse im Namespace MyApplication.Core befindet,
muss die Compileroption -main:MyApplication.Core.Program lauten. Wenn Ihre Kompilierung mehr als einen Typ
mit einer Main -Methode enthält, können Sie angeben, welcher Typ die Main -Methode enthält.
NOTE
Diese Option kann nicht für ein Projekt verwendet werden, das Anweisungen auf oberster Ebene enthält, auch wenn
dieses Projekt mindestens eine Main -Methode enthält.

PdbFile
Die Compileroption PdbFile gibt den Namen und Speicherort der Debugsymboldatei an. Der Wert filename
gibt den Namen und Speicherort der Debugsymboldatei an.

<PdbFile>filename</PdbFile>

Wenn Sie DebugType angeben, erstellt der Compiler eine PDB-Datei in demselben Verzeichnis, in dem der
Compiler die Ausgabedatei (EXE- oder DLL-Datei) erstellt. Die PDB-Datei weist denselben Basisdateinamen wie
der Name der Ausgabedatei auf. Mit PdbFile können Sie einen Dateinamen und -speicherort für die PDB-Datei
angeben, die nicht dem Standard entsprechen. Diese Compileroption kann weder in der Visual Studio-
Entwicklungsumgebung festgelegt werden, noch kann sie programmgesteuert geändert werden.

PathMap
Mit der Compileroption PathMap wird angegeben, wie physische Pfade den Quellpfadnamen zugeordnet
werden, die vom Compiler ausgegeben werden. Bei dieser Option wird jeder physische Pfad auf dem Computer,
auf dem der Compiler ausgeführt wird, einem entsprechenden Pfad zugeordnet, der in die Ausgabedateien
geschrieben werden sollte. Im folgenden Beispiel ist path1 der vollständige Pfad zu den Quelldateien in der
aktuellen Umgebung, und sourcePath1 ist der Quellpfad, der für path1 in Ausgabedateien ersetzt wird. Wenn
Sie mehrere zugeordnete Quellpfade angeben möchten, trennen Sie diese jeweils durch Semikolons.

<PathMap>path1=sourcePath1;path2=sourcePath2</PathMap>

Aus folgenden Gründen schreibt der Compiler den Quellpfad in die Ausgabe:
1. Der Quellpfad ersetzt ein Argument, wenn das CallerFilePathAttribute auf einen optionalen Parameter
angewendet wird.
2. Der Quellpfad ist in eine PDB-Datei eingebettet.
3. Der Pfad der PDB-Datei ist in einer PE-Datei (Portable Executable, portierbare ausführbare Datei) eingebettet.

ApplicationConfiguration
Mit der Compileroption ApplicationConfiguration kann eine C#-Anwendung den Speicherort der
Anwendungskonfigurationsdatei („app.config“) der Assembly für die Common Language Runtime (CLR) zur
Assemblybindungszeit angeben.

<ApplicationConfiguration>file</ApplicationConfiguration>

Dabei ist file die Anwendungskonfigurationsdatei mit den Assemblybindungseinstellungen. Eine Verwendung
von ApplicationConfiguration sind erweiterte Szenarien, in denen eine Assembly sowohl auf die .NET
Framework-Version als auch auf die .NET Framework für Silverlight-Version einer bestimmten Verweisassembly
gleichzeitig verweisen muss. Ein in Windows Presentation Foundation (WPF) geschriebener XAML-Designer
muss möglicherweise für die Benutzeroberfläche des Designers sowohl auf den WPF-Desktop als auch auf die
Teilmenge von WPF, die in Silverlight enthalten ist, verweisen. Dieselbe Designerassembly muss auf beide
Assembly zugreifen. Standardmäßig verursachen die separaten Verweise einen Compilerfehler, da die
Assemblybindung die zwei Assemblys als Entsprechung ansieht. Mit der Compileroption
ApplicationConfiguration können Sie den Speicherort einer Datei „app.config“ festlegen, die das
Standardverhalten wie im folgenden Beispiel dargestellt mit dem <supportPortability> -Tag deaktiviert.

<supportPortability PKT="7cec85d7bea7798e" enable="false"/>

Der Compiler übergibt den Speicherort der Datei an die Assemblybindungslogik der CLR.

NOTE
Fügen Sie das Eigenschaftentag <UseAppConfigForCompiler> in die CSPROJ-Datei ein, und legen Sie seinen Wert auf
true fest, um die Datei „app.config“ zu verwenden, die bereits im Projekt festgelegt ist. Fügen Sie den Eigenschaftentag
<AppConfigForCompiler> in die CSPROJ-Datei ein, und legen Sie dessen Wert auf den Speicherort der Datei fest, um
eine andere app.config-Datei anzugeben.

In folgendem Beispiel wird eine app.config-Datei gezeigt, die es einer Anwendung ermöglicht, über Verweise
sowohl auf die .NET Framework-Implementierung als auch die .NET Framework for Silverlight-Implementierung
jeder .NET Framework-Assembly zu verfügen, die in beiden Implementierungen vorhanden ist. Die
Compileroption ApplicationConfiguration gibt den Speicherort dieser Datei „app.config“ an.

<configuration>
<runtime>
<assemblyBinding>
<supportPortability PKT="7cec85d7bea7798e" enable="false"/>
<supportPortability PKT="31bf3856ad364e35" enable="false"/>
</assemblyBinding>
</runtime>
</configuration>

AdditionalLibPaths
Die Option AdditionalLibPaths gibt den Speicherort von Assemblys an, auf die mit der Option References
verwiesen wird.

<AdditionalLibPaths>dir1[,dir2]</AdditionalLibPaths>

Dabei ist dir1 ein Verzeichnis, in dem der Compiler suchen soll, wenn eine referenzierte Assembly nicht im
aktuellen Arbeitsverzeichnis (dem Verzeichnis, aus dem Sie den Compiler aufrufen) oder im Systemverzeichnis
der Common Language Runtime gefunden wird. dir2 ist mindestens ein zusätzliches Verzeichnis, in dem nach
Assemblyverweisen gesucht werden soll. Trennen Sie Verzeichnisnamen durch ein Komma, ohne Leerzeichen zu
verwenden. Der Compiler sucht in folgender Reihenfolge nach Assemblyverweisen, die nicht vollqualifiziert
sind:
1. Aktuelles Arbeitsverzeichnis
2. Das Verzeichnis des CLR-Systems (Common Language Runtime)
3. Durch AdditionalLibPaths angegebene Verzeichnisse.
4. Von den LIB-Umgebungsvariablen angegebene Verzeichnisse
Verwenden Sie Reference , um einen Assemblyverweis anzugeben. AdditionalLibPaths ist additiv. Wenn diese
Option mehrmals angegeben wird, wird an vorherige Werte angefügt. Da der Pfad zur abhängigen Assembly
nicht im Assemblymanifest angegeben ist, wird die Anwendung die Assembly im globalen Assemblycache
finden und verwenden. Der Compiler, der auf die Assembly verweist, impliziert nicht, dass die Common
Language Runtime die Assembly zur Laufzeit finden und laden kann. Weitere Informationen dazu, wie die
Laufzeit nach verwiesenen Assemblys sucht, finden Sie unter So sucht Common Language Runtime nach
Assemblys.

GenerateFullPaths
Die Option GenerateFullPaths bewirkt, dass der Compiler beim Auflisten von Kompilierungsfehlern und -
warnungen den vollständigen Pfad der Datei angibt.

<GenerateFullPaths>true</GenerateFullPaths>

Standardmäßig ist in den aus der Kompilierung resultierenden Fehler- und Warnmeldungen der Name der Datei
enthalten, in der der entsprechende Fehler auftrat. Die Option GenerateFullPaths bewirkt, dass der Compiler
den vollständigen Pfad der Datei angibt. Diese Compileroption steht in Visual Studio nicht zur Verfügung und
kann auch nicht programmgesteuert angepasst werden.

PreferredUILang
Mithilfe der Compileroption PreferredUILang können Sie die Sprache festlegen, in der der C#-Compiler
Ausgaben anzeigt, z. B. Fehlermeldungen.

<PreferredUILang>language</PreferredUILang>

Dabei ist language der Sprachenname der Sprache, die für die Compilerausgabe verwendet wird. Sie können
die Compileroption PreferredUILang verwenden, um die Sprache anzugeben, die der C#-Compiler für
Fehlermeldungen und andere Befehlszeilenausgaben verwenden soll. Wenn das Language Pack für die Sprache
nicht installiert ist, wird stattdessen die Spracheinstellung des Betriebssystems verwendet.

BaseAddress
Mit der Option BaseAddress können Sie die bevorzugte Basisadresse angeben, an der eine DLL geladen
werden soll. Weitere Informationen zu Situationen und Gründen für die Verwendung dieser Option finden Sie in
Larry Ostermans WebLog.

<BaseAddress>address</BaseAddress>

Dabei ist address die Basisadresse für die DLL. Diese Adresse kann als dezimale, hexadezimale oder oktale Zahl
angegeben werden. Die Standardbasisadresse für eine DLL-Datei wird durch die Common Language Runtime
von .NET festgelegt. Das niederwertige Wort in dieser Adresse wird gerundet. Wenn Sie zum Beispiel
0x11110001 angeben, wird dieser Wert auf 0x11110000 gerundet. Um das Signieren für eine DLL abzuschließen,
verwenden Sie „SN.EXE“ mit der Option „-R“.

ChecksumAlgorithm
Mit dieser Option wird der Prüfsummenalgorithmus gesteuert, der zum Codieren von Quelldateien in der PDB-
Datei verwendet wird.

<ChecksumAlgorithm>algorithm</ChecksumAlgorithm>

algorithm muss SHA1 (Standardwert) oder SHA256 sein.


CodePage
Diese Option gibt an, welche Codepage beim Kompilieren verwendet werden soll, wenn die erforderliche Seite
nicht die aktuelle Standardcodepage für das System ist.

<CodePage>id</CodePage>

Dabei ist id die ID der Codepage, die für alle Quellcodedateien in der Kompilierung verwendet werden soll.
Der Compiler versucht zunächst, alle Quelldateien als UTF-8 zu interpretieren. Wenn Ihre Quellcodedateien eine
andere Codierung als UTF-8 aufweisen und andere Zeichen als 7-Bit-ASCII-Zeichen verwendet werden, sollten
Sie mit der Option CodePage angeben, welche Codepage verwendet werden soll. CodePage wirkt sich auf alle
in der Kompilierung verwendeten Quellcodedateien aus. Weitere Informationen darüber, wie ermittelt wird,
welche Codeseiten auf dem System unterstützt werden, finden Sie unter GetCPInfo.

Utf8Output
Die Option Utf8Output zeigt die Compilerausgabe mit UTF-8-Codierung an.

<Utf8Output>true</Utf8Output>

Bei einigen internationalen Konfigurationen kann die Compilerausgabe nicht ordnungsgemäß in der Konsole
angezeigt werden. Verwenden Sie Utf8Output , und leiten Sie die Compilerausgabe in eine Datei um.

FileAlignment
Mit der Option FileAlignment können Sie die Größe der Abschnitte in Ihrer Ausgabedatei angeben. Gültige
Werte sind 512, 1024, 2048, 4096 und 8192. Diese Werte sind in Bytes angegeben.

<FileAlignment>number</FileAlignment>

Sie legen die Option FileAlignment in den Buildeigenschaften für Ihr Projekt in Visual Studio auf der Seite
Er weiter t fest. Jeder Abschnitt wird auf einer Grenze ausgerichtet, die ein Vielfaches des Werts FileAlignment
darstellt. Es gibt keinen festen Standardwert. Wenn FileAlignment nicht angegeben wird, wählt die Common
Language Runtime zur Kompilierzeit einen Standardwert aus. Das Angeben der Abschnittsgröße wirkt sich auf
die Größe der Ausgabedatei aus. Das Ändern der Größe kann möglicherweise für Programme hilfreich sein, die
auf kleineren Geräten ausgeführt werden. Verwenden Sie DUMPBIN, um Informationen über Abschnitte in Ihrer
Ausgabedatei anzuzeigen.

ErrorEndLocation
Weist den Compiler an, die Zeile und Spalte der Endposition jedes Fehlers auszugeben.

<ErrorEndLocation>filename</ErrorEndLocation>

Standardmäßig schreibt der Compiler die Startposition für alle Fehler und Warnungen in die Quelle. Wenn diese
Option auf TRUE festgelegt ist, schreibt der Compiler sowohl die Start- als auch die Endposition für jeden Fehler
und jede Warnung.

NoStandardLib
NoStandardLib verhindert den Import der Datei „mscorlib.dll“, die den gesamten Systemnamespace definiert.
<NoStandardLib>true</NoStandardLib>

Verwenden Sie diese Option, wenn Sie Ihren eigenen Systemnamespace und die entsprechenden Objekte
definieren oder erstellen möchten. Wenn Sie NoStandardLib nicht angeben, wird „mscorlib.dll“ in Ihr
Programm importiert (entspricht der Angabe von <NoStandardLib>false</NoStandardLib> ).

SubsystemVersion
Gibt die mindestens erforderliche Version des Subsystems an, mit der die ausführbare Datei verwendet werden
kann. Meistens stellt diese Option sicher, dass die ausführbare Datei Sicherheitsfunktionen verwenden kann, die
in älteren Versionen von Windows nicht verfügbar sind.

NOTE
Verwenden Sie zum Angeben des Subsystems selbst die Compileroption TargetType .

<SubsystemVersion>major.minor</SubsystemVersion>

major.minor gibt die mindestens erforderliche Version des Subsystems an und wird in einer Punktschreibweise
für Haupt- und Nebenversionen ausgedrückt. Beispielsweise können Sie angeben, dass eine Anwendung nicht
unter einem Betriebssystem ausgeführt werden kann, das älter als Windows 7 ist. Legen Sie den Wert dieser
Option auf 6.01 fest, wie in der Tabelle weiter unten in diesem Artikel beschrieben. Sie geben die Werte für
major und minor als ganze Zahlen an. Führende Nullen in der Version minor ändern die Version nicht, jedoch
nachfolgende Nullen. 6.1 und 6.01 verweisen z.B. auf die gleiche Version, aber 6.10 verweist auf eine andere
Version. Es wird empfohlen, die Nebenversion in Form von zwei Ziffern auszudrücken, um Verwechslungen zu
vermeiden.
Die folgende Tabelle enthält allgemeine Subsystemversionen von Windows.

W IN DO W S- VERSIO N SUB SY ST EM VERSIO N

Windows Server 2003 5.02

Windows Vista 6.00

Windows 7 6.01

Windows Server 2008 6.01

Windows 8 6.02

Der Standardwert der Compileroption SubsystemVersion hängt von den Bedingungen in der folgenden Liste
ab:
Der Standardwert ist 6,02, wenn jede Compileroption in der folgenden Liste festgelegt ist:
/target:appcontainerexe
/target:winmdobj
-platform:arm
Wenn Sie MSBuild verwenden, .NET Framework 4.5 als Ziel festlegen und keine der Compileroptionen
verwenden, die weiter oben in der Liste angegeben sind, ist der Standardwert 6.00.
Der Standardwert ist 4,00, wenn keine der vorherigen Bedingungen TRUE ist.
ModuleAssemblyName
Gibt den Namen einer Assembly an, auf deren nicht öffentliche Typen eine NETMODULE-Datei zugreifen kann.

<ModuleAssemblyName>assembly_name</ModuleAssemblyName>

ModuleAssemblyName sollte beim Erstellen einer NETMODULE-Datei verwendet werden und wenn die
folgenden Bedingungen erfüllt sind:
Die NETMODULE-Datei benötigt Zugriff auf nicht öffentliche Typen in einer vorhandenen Assembly.
Sie kennen den Namen der Assembly, in die die NETMODULE-Datei integriert wird.
Die vorhandene Assembly hat der Assembly, in die NETMODULE-Datei integriert wird, Friend-Assembly-
Zugriff erteilt.
Weitere Informationen zum Erstellen einer NETMODULE-Datei finden Sie unter der Option TargetType von
module . Weitere Informationen finden Sie unter Friend-Assemblys.
XML-Dokumentationskommentare
04.11.2021 • 10 minutes to read

C#-Quelldateien können strukturierte Kommentare enthalten, die API-Dokumentation für die in diesen Dateien
definierten Typen erzeugen. Der C#-Compiler erzeugt eine XML-Datei, die strukturierte Daten enthält, die die
Kommentare und API-Signaturen darstellen. Andere Tools können diese XML-Ausgabe verarbeiten, um
beispielsweise eine lesbare Dokumentation in Form von Webseiten oder PDF-Dateien zu erstellen.
Dieser Prozess bietet viele Vorteile für das Hinzufügen von API-Dokumentation in Ihrem Code:
Der C#-Compiler kombiniert die Struktur des C#-Codes mit dem Text der Kommentare in einem einzelnen
XML-Dokument.
Der C#-Compiler überprüft, ob die Kommentare mit den API-Signaturen für relevante Tags übereinstimmen.
Tools, die die XML-Dokumentationsdateien verarbeiten, können XML-Elemente und -Attribute definieren, die
für diese Tools spezifisch sind.
Tools wie Visual Studio bieten IntelliSense für viele allgemeine XML-Elemente, die in
Dokumentationskommentaren verwendet werden.
In diesem Artikel werden folgende Themen erörtert:
Dokumentationskommentare und Generierung von XML-Dateien
Vom C#-Compiler und Visual Studio überprüfte Tags
Format der generierten XML-Datei

Erstellen der XML-Dokumentationsausgabe


Sie erstellen die Dokumentation für Ihren Code, indem Sie spezielle Kommentarfelder schreiben, die durch
dreifache Schrägstriche gekennzeichnet sind (///). Die Kommentarfelder enthalten XML-Elemente, die den
Codeblock beschreiben, der den Kommentaren folgt. Beispiel:

/// <summary>
/// This class performs an important function.
/// </summary>
public class MyClass {}

Sie legen die DocumentationFile -Option fest, und der Compiler findet alle Kommentarfelder mit XML-Tags im
Quellcode und erstellt aus diesen Kommentaren eine XML-Dokumentationsdatei. Wenn diese Option aktiviert
ist, generiert der Compiler die Warnung CS1591 für alle öffentlich sichtbaren Member, die in Ihrem Projekt ohne
XML-Dokumentationskommentare deklariert sind.

XML-Kommentarformate
Die Verwendung von XML-Dokumentationskommentaren erfordert Trennzeichen, die angeben, wo ein
Dokumentationskommentar beginnt und endet. Sie können die folgenden Arten von Trennzeichen mit den XML-
Dokumentationstags verwenden:
///Einzeiliges Trennzeichen: Dieses wird in den Dokumentationsbeispielen und C#-Projektvorlagen
verwendet. Wenn dem Trennzeichen Leerzeichen folgen, wird es nicht in die XML-Ausgabe aufgenommen.
NOTE
Visual Studio fügt die Tags <summary> und </summary> automatisch ein und positioniert den Cursor innerhalb
dieser Tags, nachdem Sie das Trennzeichen /// in den Code-Editor eingefügt haben. Sie können diese Funktion
im Dialogfeld „Optionen“ aktivieren oder deaktivieren.

/** */ Mehrzeilige Trennzeichen: Für die /** */ -Trennzeichen gelten die folgenden Formatierungsregeln:
In der Zeile mit dem /** -Trennzeichen, wenn der Rest der Zeile Leerraum ist, wird die Zeile für
Kommentare nicht verarbeitet. Wenn das erste Zeichen nach dem /** -Trennzeichen ein
Leerzeichen ist, wird dieses Leerzeichen ignoriert und der Rest der Zeile verarbeitet. Andernfalls
wird de gesamte Text der Zeile nach dem /** -Trennzeichen als Teil des Kommentars verarbeitet.
In der Zeile mit dem */ -Trennzeichen, wenn sie nur aus Leerzeichen bis zum */ -Trennzeichen
besteht, wird diese Zeile ignoriert. Andernfalls wird der Text der Zeile bis zum */ -Trennzeichen als
Teil des Kommentars verarbeitet.
Für die Zeilen nach der, die mit dem /** -Trennzeichen beginnt, sucht der Compiler ein
gemeinsames Muster am Anfang jeder Zeile. Das Muster kann aus optionalen Leerzeichen und
einem Sternchen bestehen ( * ), gefolgt von weiteren optionalen Leerzeichen. Wenn der Compiler
am Anfang jeder Zeile ein gemeinsames Muster findet, die nicht mit dem /** - oder */ -
Trennzeichen beginnt oder endet, ignoriert er dieses Muster für jede Zeile.
Der einzige Teil des folgenden Kommentars, der verarbeitet wird, ist die Zeile, die mit <summary>
beginnt. Die drei Tag-Formate ergeben identische Kommentare.

/** <summary>text</summary> */

/**
<summary>text</summary>
*/

/**
* <summary>text</summary>
*/

Der Compiler identifiziert ein gemeinsames Muster von „*“ am Anfang der zweiten und dritten
Zeile. Das Muster ist nicht in der Ausgabe enthalten.

/**
* <summary>
* text </summary>*/

Der Compiler findet kein gemeinsames Muster im folgenden Kommentar, da das zweite Zeichen in
der dritten Zeile kein Sternchen ist. Der gesamte Text wird in der zweiten und dritten Zeile als Teil
des Kommentars verarbeitet.

/**
* <summary>
text </summary>
*/

Der Compiler findet aus zwei Gründen kein Muster im folgenden Kommentar. Erstens ist die
Anzahl von Leerzeichen vor dem Sternchen nicht konsistent. Zweitens beginnt die fünfte Zeile mit
einem Tab, der mit Leerzeichen nicht übereinstimmt. Der gesamte Text wird von Zeile zwei bis fünf
als Teil des Kommentars verarbeitet.

/**
* <summary>
* text
* text2
* </summary>
*/

Zum Verweisen auf XML-Elemente (beispielsweise verarbeitet eine Funktion bestimmte XML-Elemente, die Sie
in einem XML-Dokumentationskommentar beschreiben möchten) können Sie die Standardnotierungsart
verwenden ( &lt; und &gt; ). Zum Verweisen auf generische Bezeichner in Codeverweiselementen ( cref -
Elementen) können Sie entweder die Escapezeichen (z.B. cref="List&lt;T&gt;" ) oder geschweifte Klammern (
cref="List{T}" ) verwenden. Als Sonderfall analysiert der Compiler die geschweiften Klammern als spitze
Klammern, um das Erstellen des Dokumentationskommentars beim Verweisen auf generische Bezeichner
weniger schwerfällig zu gestalten.

NOTE
XML-Dokumentationskommentare sind keine Metadaten und sind nicht in der kompilierten Assembly enthalten. Folglich
ist der Zugriff auf sie über Reflektion nicht möglich.

Tools, die XML-Dokumentationseingaben akzeptieren


Die folgenden Tools erstellen die Ausgabe aus XML-Kommentaren:
DocFX: DocFX ist ein API-Dokumentations-Generator für .NET, der derzeit C#, Visual Basic und F# unterstützt.
Außerdem können Sie damit die generierte Referenzdokumentation anpassen. DocFX erstellt eine statische
HTML-Website aus Ihrem Quellcode und Ihren Markdowndateien. Darüber hinaus bietet DocFX Ihnen die
Flexibilität, das Layout und das Format Ihrer Website über Vorlagen anzupassen. Sie können auch
benutzerdefinierte Vorlagen erstellen.
Sandcastle: Die Sandcastle-Tools erstellen Hilfedateien für verwaltete Klassenbibliotheken, die sowohl
konzeptionelle Seiten als auch API-Referenzseiten enthalten. Die Sandcastle-Tools sind befehlszeilenbasierte
Tools und verfügen nicht über GUI-Front-End, Projektverwaltungsfeatures oder einen automatisierten
Buildprozess. Der Sandcastle Help File Builder stellt eine eigenständige GUI und Befehlszeilentools zum
automatisierten Erstellen einer Hilfedatei zur Verfügung. Ein Visual Studio-Integrationspaket ist auch
verfügbar, sodass Hilfeprojekte vollständig innerhalb von Visual Studio erstellt und verwaltet werden können.
Doxygen: Doxygen generiert einen Onlinedokumentationsbrowser (in HTML) oder ein
Offlinereferenzhandbuch (in LaTeX) aus einer Reihe dokumentierter Quelldateien. Zusätzlich ist
Unterstützung für das Generieren von Ausgaben in RTF (MS Word), PostScript, PDF als Hyperlink,
komprimierte HTML, DocBook und Unix-Manpages verfügbar. Sie können Doxygen so konfigurieren, dass
die Codestruktur aus nicht dokumentierten Quelldateien extrahiert wird.
ID-Zeichenfolgen
Jeder Typ oder Member wird in einem Element in der XML-Ausgabedatei gespeichert. Jedes dieser Elemente
verfügt über eine eindeutige ID-Zeichenfolge, die den Typ oder Member identifiziert. Die ID-Zeichenfolge muss
Operatoren, Parameter, Rückgabewerte, generische Typparameter, ref , in und out -Parameter nachweisen.
Der Compiler folgt klar definierten Regeln zum Generieren der ID-Zeichenfolgen, um alle potenziellen Elemente
zu codieren. Programme, die die XML-Datei verarbeiten, können mithilfe der ID-Zeichenfolge das entsprechende
.NET-Metadatenelement oder -Reflektionselement identifizieren, für das die Dokumentation gilt.
Der Compiler beachtet beim Generieren der ID-Zeichenfolgen die folgenden Regeln:
Es befindet sich kein Leerraum in der Zeichenfolge.
Der erste Teil der Zeichenfolge kennzeichnet die Art des Members durch ein einzelnes Zeichen, dem ein
Doppelpunkt folgt. Die folgenden Membertypen werden verwendet:

Z EIC H EN M EM B ERA RT H IN W EISE

N namespace Einem Namespace können keine


Dokumentationskommentare
hinzugefügt werden. Falls
unterstützt, können jedoch cref-
Verweise hinzugefügt werden.

T Typ Ein Typ ist eine Klasse, eine


Schnittstelle, eine Struktur, eine
Enumeration oder ein Delegat.

F Feld

P property Schließt Indexer und andere


indizierte Eigenschaften ein

M Methode Schließt spezielle Methoden wie


Konstruktoren und Operatoren ein

E event

! Fehlerzeichenfolge Der verbleibende Teil der


Zeichenfolge enthält
Fehlerinformationen. Vom C#-
Compiler werden
Fehlerinformationen für Links
erstellt, die nicht aufgelöst werden
können.

Beim zweiten Teil der Zeichenfolge handelt es sich um den vollqualifizierten Namen eines Elements,
beginnend mit dem Namespace-Stammverzeichnis. Der Name des Elements, der oder die
einschließenden Typen und der Namespace sind durch Punkte getrennt. Wenn der Name des Elements
selbst Punkte enthält, werden sie durch ein Rautezeichen (#) ersetzt. Es wird vorausgesetzt, dass kein
Element direkt im Namen ein Rautezeichen enthält. Der vollqualifizierte Name des String-Konstruktors
lautet beispielsweise „System.String.#ctor “.
Bei Eigenschaften und Methoden folgt die in Klammern eingeschlossene Parameterliste. Wenn keine
Parameter vorhanden sind, werden keine Klammern verwendet. Die Parameter werden durch Kommas
voneinander getrennt. Die Codierung jedes Parameters erfolgt direkt wie bei einer .NET-Signatur
(Definitionen des Elements in Großbuchstaben finden Sie unter
Microsoft.VisualStudio.CorDebugInterop.CorElementType in der folgenden Liste):
Basistypen. Reguläre Typen ( ELEMENT_TYPE_CLASS oder ELEMENT_TYPE_VALUETYPE ) werden als
vollqualifizierter Name des Typs dargestellt.
Systeminterne Typen (zum Beispiel ELEMENT_TYPE_I4 , ELEMENT_TYPE_OBJECT , ELEMENT_TYPE_STRING ,
ELEMENT_TYPE_TYPEDBYREF und ELEMENT_TYPE_VOID ) werden als vollqualifizierter Name des
entsprechenden vollständigen Typs dargestellt. Zum Beispiel: System.Int32 oder
System.TypedReference .
ELEMENT_TYPE_PTR wird als * dargestellt, das auf den geänderten Typ folgt.
ELEMENT_TYPE_BYREF wird als @ dargestellt, das auf den geänderten Typ folgt.
ELEMENT_TYPE_CMOD_OPT wird als „!“ mit nachstehendem vollqualifizierten Namen der
Modifiziererklasse dargestellt, das auf den geänderten Typ folgt.
ELEMENT_TYPE_SZARRAY wird als „[]“ dargestellt, das auf den Elementtyp des Arrays folgt.
ELEMENT_TYPE_ARRAY wird als [lowerbound: size ,lowerbound: size ] dargestellt, wobei die Anzahl von
Kommas als Rang minus 1 berechnet wird und die untere Grenze sowie die Größe jeder Dimension –
sofern bekannt – dezimal dargestellt werden. Wenn die untere Grenze oder die Größe nicht
angegeben ist, wird sie ausgelassen. Wenn die untere Grenze und die Größe für eine bestimmte
Dimension ausgelassen werden, kann der Doppelpunkt (:) ebenfalls ausgelassen werden. [1:,1:] ist
beispielsweise ein zweidimensionales Array mit 1 als unterer Grenze und nicht angegebenen Größen.
Nur für Konvertierungsoperatoren ( op_Implicit und op_Explicit ) wird der Rückgabewert der Methode
als „ ~ “ gefolgt vom Rückgabewert codiert. Beispiel:
<member name="M:System.Decimal.op_Explicit(System.Decimal arg)~System.Int32"> ist das Tag für den in der
System.Decimal -Klasse deklarierten Umwandlungsoperator
public static explicit operator int (decimal value); .

Bei generischen Typen folgt auf den Namen des Typs ein Graviszeichen und dann eine Zahl, mit der die
Anzahl generischer Typparameter angegeben wird. Beispiel: <member name="T:SampleClass``2"> ist das Tag
für einen Typ, der als public class SampleClass<T, U> definiert ist. Bei Methoden, die generische Typen
als Parameter verwenden, werden die generischen Parameter des Typs als Zahlen mit vorangestelltem
Backticks angegeben (z. B. `0,`1). Jede Zahl stellt eine bei 0 beginnende Arraynotation für die generischen
Parameter des Typs dar.
ELEMENT_TYPE_PINNED wird als „^“ dargestellt, das auf den geänderten Typ folgt. Diese Codierung wird
nie vom C#-Compiler generiert.
ELEMENT_TYPE_CMOD_REQ wird als „|“ und vollqualifizierter Name der Modifiziererklasse dargestellt, das
bzw. der auf den geänderten Typ folgt. Diese Codierung wird nie vom C#-Compiler generiert.
ELEMENT_TYPE_GENERICARRAY wird als „[?]“ dargestellt, das auf den Elementtyp des Arrays folgt. Diese
Codierung wird nie vom C#-Compiler generiert.
ELEMENT_TYPE_FNPTR wird als „=FUNC: type (signature)“ dargestellt, wobei type den Rückgabetyp
angibt und es sich bei signature um die Argumente der Methode handelt. Sind keine Argumente
vorhanden, werden keine Klammern verwendet. Diese Codierung wird nie vom C#-Compiler
generiert.
Die folgenden Signaturkomponenten werden nicht dargestellt, weil sie nicht zur Unterscheidung
überladener Methoden verwendet werden:
Aufrufkonvention
Rückgabetyp
ELEMENT_TYPE_SENTINEL

In den folgenden Beispielen wird veranschaulicht, wie die ID-Zeichenfolgen für eine Klasse und ihre Member
generiert werden:

namespace MyNamespace
{
/// <summary>
/// Enter description here for class X.
/// ID string generated is "T:MyNamespace.X".
/// </summary>
public unsafe class MyClass
{
/// <summary>
/// Enter description here for the first constructor.
/// ID string generated is "M:MyNamespace.MyClass.#ctor".
/// </summary>
public MyClass() { }

/// <summary>
/// Enter description here for the second constructor.
/// ID string generated is "M:MyNamespace.MyClass.#ctor(System.Int32)".
/// </summary>
/// <param name="i">Describe parameter.</param>
public MyClass(int i) { }

/// <summary>
/// Enter description here for field message.
/// ID string generated is "F:MyNamespace.MyClass.message".
/// </summary>
public string message;

/// <summary>
/// Enter description for constant PI.
/// ID string generated is "F:MyNamespace.MyClass.PI".
/// </summary>
public const double PI = 3.14;

/// <summary>
/// Enter description for method func.
/// ID string generated is "M:MyNamespace.MyClass.func".
/// </summary>
/// <returns>Describe return value.</returns>
public int func() { return 1; }

/// <summary>
/// Enter description for method someMethod.
/// ID string generated is
"M:MyNamespace.MyClass.someMethod(System.String,System.Int32@,System.Void*)".
/// </summary>
/// <param name="str">Describe parameter.</param>
/// <param name="num">Describe parameter.</param>
/// <param name="ptr">Describe parameter.</param>
/// <returns>Describe return value.</returns>
public int someMethod(string str, ref int nm, void* ptr) { return 1; }

/// <summary>
/// Enter description for method anotherMethod.
/// ID string generated is
"M:MyNamespace.MyClass.anotherMethod(System.Int16[],System.Int32[0:,0:])".
/// </summary>
/// <param name="array1">Describe parameter.</param>
/// <param name="array">Describe parameter.</param>
/// <returns>Describe return value.</returns>
public int anotherMethod(short[] array1, int[,] array) { return 0; }

/// <summary>
/// Enter description for operator.
/// ID string generated is
"M:MyNamespace.MyClass.op_Addition(MyNamespace.MyClass,MyNamespace.MyClass)".
/// </summary>
/// <param name="first">Describe parameter.</param>
/// <param name="second">Describe parameter.</param>
/// <returns>Describe return value.</returns>
public static MyClass operator +(MyClass first, MyClass second) { return first; }

/// <summary>
/// Enter description for property.
/// ID string generated is "P:MyNamespace.MyClass.prop".
/// </summary>
public int prop { get { return 1; } set { } }

/// <summary>
/// Enter description for event.
/// ID string generated is "E:MyNamespace.MyClass.OnHappened".
/// </summary>
public event Del OnHappened;

/// <summary>
/// Enter description for index.
/// ID string generated is "P:MyNamespace.MyClass.Item(System.String)".
/// </summary>
/// <param name="str">Describe parameter.</param>
/// <returns></returns>
public int this[string s] { get { return 1; } }

/// <summary>
/// Enter description for class Nested.
/// ID string generated is "T:MyNamespace.MyClass.Nested".
/// </summary>
public class Nested { }

/// <summary>
/// Enter description for delegate.
/// ID string generated is "T:MyNamespace.MyClass.Del".
/// </summary>
/// <param name="i">Describe parameter.</param>
public delegate void Del(int i);

/// <summary>
/// Enter description for operator.
/// ID string generated is "M:MyNamespace.MyClass.op_Explicit(MyNamespace.X)~System.Int32".
/// </summary>
/// <param name="myParameter">Describe parameter.</param>
/// <returns>Describe return value.</returns>
public static explicit operator int(MyClass myParameter) { return 1; }
}
}

C#-Sprachspezifikation
Weitere Informationen finden Sie im Anhang der C#-Sprachspezifikation zu Dokumentationskommentaren.
Empfohlene XML-Tags für C#-
Dokumentationskommentare
04.11.2021 • 11 minutes to read

C#-Dokumentationskommentare verwenden XML-Elemente, um die Struktur der Ausgabedokumentation zu


definieren. Eine Folge dieses Features ist, dass Sie in Ihren Dokumentationskommentaren beliebige gültige XML-
Elemente hinzufügen können. Der C#-Compiler kopiert diese Elemente in die XML-Ausgabedatei. Sie können
zwar beliebige gültige XML-Elemente in Ihren Kommentaren verwenden (einschließlich aller gültigen HTML-
Elemente), Codedokumentierung wird jedoch aus vielen Gründen empfohlen.
Nachstehend finden Sie einige Empfehlungen, allgemeine Anwendungsfallszenarios und wissenswerte Hinweise
für den Fall, dass Sie XML-Dokumentationstags in Ihrem C#-Code verwenden. Sie können zwar beliebige Tags in
Ihren Dokumentationskommentaren verwenden, in diesem Artikel werden jedoch die empfohlenen Tags für die
gängigsten Sprachkonstrukte beschrieben. Sie sollten immer die folgenden Empfehlungen befolgen:
Aus Einheitlichkeitsgründen sollten alle öffentlich sichtbaren Typen und deren öffentliche Member
dokumentiert werden.
Private Member können auch mithilfe von XML-Kommentaren dokumentiert werden. Dies legt jedoch die
interne (potenziell vertrauliche) Funktionsweise Ihrer Bibliothek offen.
Als absolutes Minimum sollten Typen und deren Member über ein <summary> -Tag verfügen, da dessen Inhalt
für IntelliSense erforderlich ist.
Dokumentationstext sollte in vollständigen Sätze geschrieben werden, die mit Punkten enden.
Partielle Klassen werden vollständig unterstützt, und Dokumentationsinformationen werden für jeden Typ zu
einem einzelnen Eintrag verkettet.
Die XML-Dokumentation beginnt mit /// . Wenn Sie ein neues Projekt erstellen, fügen die Vorlagen einige ///
-Startzeilen für Sie ein. Die Verarbeitung dieser Kommentare weist einige Einschränkungen auf:
Die Dokumentation muss wohlgeformtes XML sein. Wenn der XML-Code nicht wohlgeformt ist, generiert
der Compiler eine Warnung. Die Dokumentationsdatei enthält einen Kommentar, der besagt, dass ein Fehler
aufgetreten ist.
Einige der empfohlenen Tags haben eine besondere Bedeutung:
Das <param> -Tag wird verwendet, um Parameter zu beschreiben. Wenn es verwendet wird, überprüft
der Compiler, ob der Parameter vorhanden ist, und ob alle Parameter in der Dokumentation
beschrieben werden. Wenn die Überprüfung fehlschlägt, gibt der Compiler eine Warnung aus.
Das cref -Attribut kann an jedes Tag angefügt werden, um auf ein Codeelement zu verweisen. Der
Compiler überprüft, ob dieses Codeelement vorhanden ist. Wenn die Überprüfung fehlschlägt, gibt
der Compiler eine Warnung aus. Der Compiler berücksichtigt alle using -Anweisungen, wenn er nach
einem Typ sucht, der im cref -Attribut beschrieben wird.
Das <summary> -Tag wird von IntelliSense in Visual Studio verwendet, um zusätzliche Informationen
über einen Typ oder Member anzuzeigen.

NOTE
Die XML-Datei enthält keine vollständigen Informationen über den Typ und die Member (z. B. fehlen
Typinformationen). Verwenden Sie die Dokumentationsdatei zusammen mit Reflektion über den
tatsächlichen Typ oder Member, um vollständige Informationen zu einem Typ oder Member zu erhalten.
Entwickler können ihren eigenen Satz von Tags erstellen. Der Compiler kopiert diese in die Ausgabedatei.
Einige der empfohlenen Tags können für jedes Sprachelement verwendet werden. Andere haben speziellere
Anwendungsfälle. Wieder andere Tags werden eingesetzt, um Text in Ihrer Dokumentation zu formatieren. In
diesem Artikel werden die empfohlenen Tags nach ihrer Verwendung sortiert beschrieben.
Der Compiler überprüft die Syntax der Elemente, die in der folgenden Liste mit einem einzelnen * markiert sind.
Visual Studio stellt IntelliSense für die vom Compiler überprüften Tags sowie alle Tags bereit, die in der
folgenden Liste durch ** gekennzeichnet sind. Zusätzlich zu den hier aufgeführten Tags überprüfen der
Compiler und Visual Studio die Tags <b> , <i> , <u> , <br/> und <a> . Der Compiler überprüft auch <tt> , ein
veraltetes HTML-Element.
Allgemeine Tags, die für verschiedene Elemente verwendet werden: Hierbei handelt es sich um die
Mindestmenge von Tags einer jeden API.
<summary> : Der Wert dieses Elements wird in IntelliSense in Visual Studio angezeigt.
<remarks> **
Für Member verwendete Tags: Diese Tags werden für das Dokumentieren von Methoden und Eigenschaften
verwendet.
<returns> : Der Wert dieses Elements wird in IntelliSense in Visual Studio angezeigt.
<param> *: Der Wert dieses Elements wird in IntelliSense in Visual Studio angezeigt.
<paramref>
<exception> *
<value> : Der Wert dieses Elements wird in IntelliSense in Visual Studio angezeigt.
Formatieren der Dokumentationsausgabe: Diese Tags stellen Formatierungsanweisungen für Tools dar, die
Dokumentation generieren.
<para>
<list>
<c>
<code>
<example>**
Wiederverwenden von Dokumentationstext: Diese Tags vereinfachen die Wiederverwendung von XML-
Kommentaren.
<inheritdoc> **
<include> *
Generieren von Links und Verweisen: Diese Tags generieren Links zu anderen Dokumentationen.
<see> *
<seealso> *
<cref>
<href>
Tags für generische Typen und Methoden: Diese Tags werden nur für generische Typen und Methoden
verwendet.
<typeparam> *: Der Wert dieses Elements wird in IntelliSense in Visual Studio angezeigt.
<typeparamref>

NOTE
Dokumentationskommentare können nicht auf einen Namespace angewendet werden.

Wenn Sie möchten, dass ein Dokumentationskommentar spitze Klammern enthält, verwenden Sie die HTML-
Codierung für < und > : &lt; und &gt; . Diese Codierung wird im folgenden Beispiel gezeigt.

/// <summary>
/// This property always returns a value &lt; 1.
/// </summary>

Allgemeine Tags
<summary>

<summary>description</summary>

Das <summary> -Tag sollte für die Beschreibung eines Typs oder Typmembers verwendet werden. Verwenden Sie
<remarks>, um zusätzliche Informationen zu einer Typbeschreibung hinzuzufügen. Verwenden Sie das cref-
Attribut, um Dokumentationswerkzeuge wie DocFX und Sandcastle zum Erstellen von internen Links zu
Dokumentationsseiten für Codeelemente zu aktivieren. Der Text für das Tag <summary> stellt die einzige
Informationsquelle in Bezug auf den Typ in IntelliSense dar und wird auch im Fenster „Objektkatalog“ angezeigt.
<remarks>

<remarks>
description
</remarks>

Mit dem Tag <remarks> werden Informationen zu einem Typ oder Typmember hinzugefügt. Dadurch werden die
mit <summary> angegebenen Informationen ergänzt. Diese Informationen werden im Fenster des
Objektkatalogs angezeigt. Dieses Tag kann ausführlichere Erklärungen enthalten. Unter Umständen stellen Sie
fest, dass das Schreiben von Markdown mit CDATA -Abschnitten erheblich einfacher wird. Tools wie docfx
verarbeiten den Markdowntext in CDATA -Abschnitten.

Dokumentmember
<returns>

<returns>description</returns>

Das <returns> -Tag sollte im Kommentar für eine Methodendeklaration verwendet werden, um den
Rückgabewert zu beschreiben.
<param>

<param name="name">description</param>

name : Der Name eines Methodenparameters. Setzen Sie den Namen in einfache oder doppelte
Anführungszeichen (" "). Parameternamen müssen mit der API-Signatur übereinstimmen. Wenn mindestens
ein Parameter nicht enthalten ist, gibt der Compiler eine Warnung aus. Der Compiler gibt ebenfalls eine
Warnung aus, wenn der Wert von name nicht mit einem formalen Parameter in der Methodendeklaration
übereinstimmt.
Das <param> -Tag sollte im Kommentar für eine Methodendeklaration verwendet werden, um einen der
Methodenparameter zu beschreiben. Verwenden Sie mehrere <param> -Tags, um mehrere Parameter zu
dokumentieren. Der Text für das <param> -Tag wird in IntelliSense, dem Objektkatalog und im Webbericht über
Codekommentare angezeigt.
<paramref>

<paramref name="name"/>

name : Der Name des Parameters, auf den verwiesen wird. Setzen Sie den Namen in einfache oder doppelte
Anführungszeichen (" ").
Das Tag <paramref> bietet Ihnen eine Möglichkeit anzugeben, dass sich ein Wort in den Codekommentaren, z. B.
in einem <summary> - oder <remarks> -Block, auf einen Parameter bezieht. Die XML-Datei kann so verarbeitet
werden, dass dieses Wort anders formatiert wird, z.B. fett oder kursiv.
<exception>

<exception cref="member">description</exception>

cref = " member ": Ein Verweis auf eine Ausnahme, die in der aktuellen Kompilierungsumgebung verfügbar ist.
Der Compiler prüft, ob die angegebene Ausnahme vorhanden ist, und übersetzt in der Ausgabe-XML member
in den kanonischen Elementnamen. member muss in doppelte Anführungszeichen (" ") gesetzt werden.

Mit dem Tag <exception> können Sie angeben, welche Ausnahmen ausgelöst werden können. Dieses Tag kann
für Definitionen für Methoden, Eigenschaften, Ereignisse und Indexer angewendet werden.
<value>

<value>property-description</value>

Mit dem <value> -Tag können Sie den Wert beschreiben, den eine Eigenschaft darstellt. Beim Hinzufügen einer
Eigenschaft über den Code-Assistenten in der Entwicklungsumgebung Visual Studio® .NET wird für die neue
Eigenschaft ein <summary>-Tag hinzugefügt. Sie fügen manuell ein <value> -Tag für die Beschreibung des
Werts hinzu, den die Eigenschaft darstellt.

Formatieren der Dokumentationsausgabe


<para>

<remarks>
<para>
This is an introductory paragraph.
</para>
<para>
This paragraph contains more details.
</para>
</remarks>

Das Tag <para> ist für die Verwendung innerhalb eines Tags wie <summary>, <remarks> oder <returns>
gedacht und ermöglicht es Ihnen, den Text zu strukturieren. Das Tag <para> erstellt einen Absatz mit doppeltem
Abstand. Verwenden Sie das Tag <br/> , wenn Sie einen Absatz mit einfachem Abstand möchten.
<list>
<list type="bullet|number|table">
<listheader>
<term>term</term>
<description>description</description>
</listheader>
<item>
<term>Assembly</term>
<description>The library or executable built from a compilation.</description>
</item>
</list>

Der <listheader> -Block wird verwendet, um die Überschriftenzeile einer Tabelle oder einer Definitionsliste zu
definieren. Beim Definieren einer Tabelle müssen Sie nur einen Eintrag für term in der Überschrift angeben.
Jedes Element der Liste wird mit einem <item> -Block angegeben. Beim Erstellen einer Definitionsliste müssen
Sie sowohl term als auch description angeben. Für eine Tabelle, eine Auflistung oder eine nummerierte Liste
muss jedoch nur ein Eintrag für description angegeben werden. Eine Liste oder Tabelle kann so viele <item> -
Blöcke besitzen wie nötig.
<c>

<c>text</c>

Mit dem <c> -Tag kann angegeben werden, dass Text in einer Beschreibung als Code gekennzeichnet werden
soll. Zum Angeben mehrerer Zeilen als Code wird <code> verwendet.
<code>

<code>
var index = 5;
index++;
</code>

Mit dem <code> -Tag werden mehrere Codezeilen angegeben. Verwenden Sie <c>, um anzugeben, dass
einzeiliger Text in einer Beschreibung als Code gekennzeichnet werden soll.
<example>

<example>
This shows how to increment an integer.
<code>
var index = 5;
index++;
</code>
</example>

Mit dem <example> -Tag kann ein Beispiel für die Verwendung einer Methode oder eines anderen
Bibliothekmembers angegeben werden. Ein Beispiel schließt im Allgemeinen die Verwendung des Tags <code>
ein.

Wiederverwenden des Dokumentationstexts


<inheritdoc>

<inheritdoc [cref=""] [path=""]/>

XML-Kommentare werden aus Basisklassen, Schnittstellen und ähnlichen Methoden geerbt. Durch die
Verwendung von inheritdoc wird das unerwünschte Kopieren und Einfügen doppelter XML-Kommentare
vermieden, und XML-Kommentare werden automatisch synchronisiert. Beachten Sie, dass beim Hinzufügen des
Tags <inheritdoc> zu einem Typ auch alle Member die Kommentare erben.
cref : Geben Sie den Member an, von dem die Dokumentation geerbt werden soll. Bereits definierte Tags für
den aktuellen Member werden nicht von den geerbten überschrieben.
path : Die XPath-Ausdrucksabfrage, die dazu führt, dass eine Gruppe von Knoten angezeigt wird. Sie können
mit diesem Attribut die Tags filtern, die in der geerbten Dokumentation enthalten oder nicht enthalten sein
sollen.
Fügen Sie Ihre XML-Kommentare in Basisklassen oder Schnittstellen hinzu, und lassen Sie die Kommentare von
inheritdoc in die implementierenden Klassen kopieren. Fügen Sie Ihre XML-Kommentare zu synchronen
Methoden hinzu, und lassen Sie die Kommentare von inheritdoc in die asynchronen Versionen derselben
Methoden kopieren. Wenn Sie die Kommentare aus einem bestimmten Member kopieren möchten, geben Sie
den Member mithilfe des Attributs cref an.
<include>

<include file='filename' path='tagpath[@name="id"]' />

filename : Der Name der XML-Datei, die die Dokumentation enthält. Der Dateiname kann mit einem Pfad
relativ zur Quellcodedatei qualifiziert werden. filename muss in einfache Anführungszeichen (‚‘)
eingeschlossen werden.
tagpath : Der Pfad der Tags in filename , der zum Tag name führt. Der Pfad muss in einfache
Anführungszeichen (‚‘) eingeschlossen werden.
name : Der Namensbezeichner in dem Tag, das vor den Kommentaren steht. name besitzt ein id -Element.
id : Die ID des Tags, das vor den Kommentaren steht. Die ID muss in doppelte Anführungszeichen („“)
eingeschlossen werden.
Mit dem <include> -Tag können Sie auf Kommentare in einer anderen Datei verweisen, in denen die Typen und
Member in Ihrem Quellcode beschrieben werden. Das Aufnehmen einer externen Datei ist eine Alternative zum
direkten Platzieren von Dokumentationskommentaren in der Quellcodedatei. Durch das Ablegen der
Dokumentation in einer separaten Datei können Sie die Quellcodeverwaltung unabhängig vom Quellcode auf
die Dokumentation anwenden. Eine Person kann die Quellcodedatei auschecken, eine andere die
Dokumentationsdatei. Das Tag <include> verwendet die XML-XPath-Syntax. Weitere Anpassungsmöglichkeiten
bei der Verwendung von <include> finden Sie in der XPath-Dokumentation.

Generieren von Links und Verweisen


<see>

/// <see cref="member"/>


// or
/// <see cref="member">Link text</see>
// or
/// <see href="link">Link Text</see>
// or
/// <see langword="keyword"/>

cref="member" : Ein Verweis auf einen Member oder ein Feld, das in der aktuellen Kompilierungsumgebung
aufgerufen werden kann. Der Compiler überprüft, ob das angegebene Codeelement vorhanden ist, und
übergibt member an den Elementnamen in der XML-Ausgabe. Setzen Sie member in doppelte
Anführungszeichen (" "). Sie können einen anderen Linktext für ein "cref"-Attribut angeben, indem Sie ein
separates schließendes Tag verwenden.
href="link" : Ein klickbarer Link zu einer bestimmten URL. <see href="https://github.com">GitHub</see>
erzeugt beispielsweise einen Link, auf den geklickt werden kann und der den Text GitHub aufweist. Er führt
zu https://github.com .
langword="keyword" : Ein Sprachschlüsselwort wie true .

Mit dem <see> -Tag kann ein Link im Text angegeben werden. Verwenden Sie <seealso>, um anzugeben, dass
Text in einen Abschnitt „Siehe auch“ eingefügt werden soll. Verwenden Sie das cref-Attribut, um interne Links zu
Dokumentationsseiten für Codeelemente zu erstellen. Sie fügen die Typparameter ein, um einen Verweis auf
einen generischen Typen oder eine generische Methode anzugeben, z. B. cref="cref="IDictionary{T, U}" .
Außerdem ist href ein gültiges Attribut, das als Hyperlink fungiert.
<seealso>

/// <seealso cref="member"/>


// or
/// <seealso href="link">Link Text</seealso>

cref="member" : Ein Verweis auf einen Member oder ein Feld, das in der aktuellen Kompilierungsumgebung
aufgerufen werden kann. Der Compiler überprüft, ob das angegebene Codeelement vorhanden ist, und
übergibt member an den Elementnamen in der XML-Ausgabe. member muss in doppelte Anführungszeichen
(" ") gesetzt werden.
href="link" : Ein klickbarer Link zu einer bestimmten URL.
<seealso href="https://github.com">GitHub</seealso> erzeugt beispielsweise einen Link, auf den geklickt
werden kann und der den Text GitHub aufweist. Er führt zu https://github.com .

Mit dem Tag <seealso> können Sie den Text angeben, der im Abschnitt Siehe auch angezeigt werden soll. Mit
<see> kann ein Link im Text angegeben werden. Das Tag seealso kann nicht in das Tag summary geschachtelt
werden.
cref-Attribut
Das Attribut cref in einem XML-Dokumentationstag bedeutet „Codeverweis“. Es gibt an, dass der innere Text
des Tags ein Codeelement ist, wie z.B. ein Typ, eine Methode oder Eigenschaft. Dokumentationstools wie DocFX
und Sandcastle verwenden die cref -Attribute zum automatischen Generieren von Hyperlinks auf der Seite, auf
der der Typ oder Member dokumentiert wird.
href-Attribut
Das href -Attribut steht für einen Verweis auf eine Webseite. Sie können damit direkt auf die
Onlinedokumentation zu Ihrer API oder Bibliothek verweisen.

Generische Typen und Methoden


<typeparam>

<typeparam name="TResult">The type returned from this method</typeparam>

: Der Name des Typparameters. Setzen Sie den Namen in einfache oder doppelte
TResult
Anführungszeichen (" ").
Die <typeparam> -Tag sollte im Kommentar für einen generischen Typ oder eine Methodendeklaration
verwendet werden, um einen Typparameter zu beschreiben. Fügen Sie ein Tag für jeden Typparameter des
generischen Typs oder der Methode hinzu. Der Text für das Tag <typeparam> wird in IntelliSense angezeigt.
<typeparamref>
<typeparamref name="TKey"/>

TKey : Der Name des Typparameters. Setzen Sie den Namen in einfache oder doppelte Anführungszeichen ("
").
Verwenden Sie dieses Tag, um Consumern der Dokumentationsdatei zu ermöglichen, das Wort auf
unterschiedliche Weise zu formatieren, z.B. in Kursivschrift.
Benutzerdefinierter Tags
Alle oben genannten Tags werden vom C#-Compiler erkannt. Ein Benutzer kann jedoch auch eigene Tags
definieren. Tools wie Sandcastle bieten Unterstützung für zusätzliche Tags wie <event> oder <note> und
unterstützen sogar das Dokumentieren von Namespaces. Benutzerdefinierte oder interne
Dokumentationsgenerierungstools können auch mit den Standardtags verwendet werden, und mehrere
Ausgabeformate von HTML in PDF können unterstützt werden.
Erstellen von XML-Dokumentationskommentaren
04.11.2021 • 18 minutes to read

Dieser Artikel enthält drei Beispiele dafür, wie den meisten Elementen der Sprache C# XML-
Dokumentationskommentare hinzugefügt werden. Das erste Beispiel zeigt, wie Sie eine Klasse mit
unterschiedlichen Membern dokumentieren. Das zweite Beispiel zeigt, wie Sie Erklärungen für eine Hierarchie
von Klassen oder Schnittstellen wiederverwenden. Das dritte Beispiel zeigt Tags für generische Klassen und
Member. Im zweiten und dritten Beispiel werden Konzepte verwendet, die im ersten Beispiel eingeführt werden.

Dokumentieren einer Klasse, Struktur oder Schnittstelle


Das folgende Beispiel zeigt allgemeine Sprachelemente und die Tags, mit denen Sie diese Elemente
wahrscheinlich beschreiben werden. In den Dokumentationskommentaren wird die Verwendung der Tags und
nicht der Klasse selbst beschrieben.

/// <summary>
/// Every class and member should have a one sentence
/// summary describing its purpose.
/// </summary>
/// <remarks>
/// You can expand on that one sentence summary to
/// provide more information for readers. In this case,
/// the <c>ExampleClass</c> provides different C#
/// elements to show how you would add documentation
///comments for most elements in a typical class.
/// <para>
/// The remarks can add multiple paragraphs, so you can
/// write detailed information for developers that use
/// your work. You should add everything needed for
/// readers to be successful. This class contains
/// examples for the following:
/// </para>
/// <list type="table">
/// <item>
/// <term>Summary</term>
/// <description>
/// This should provide a one sentence summary of the class or member.
/// </description>
/// </item>
/// <item>
/// <term>Remarks</term>
/// <description>
/// This is typically a more detailed description of the class or member
/// </description>
/// </item>
/// <item>
/// <term>para</term>
/// <description>
/// The para tag separates a section into multiple paragraphs
/// </description>
/// </item>
/// <item>
/// <term>list</term>
/// <description>
/// Provides a list of terms or elements
/// </description>
/// </item>
/// <item>
/// <term>returns, param</term>
/// <description>
/// <description>
/// Used to describe parameters and return values
/// </description>
/// </item>
/// <item>
/// <term>value</term>
/// <description>Used to describe properties</description>
/// </item>
/// <item>
/// <term>exception</term>
/// <description>
/// Used to describe exceptions that may be thrown
/// </description>
/// </item>
/// <item>
/// <term>c, cref, see, seealso</term>
/// <description>
/// These provide code style and links to other
/// documentation elements
/// </description>
/// </item>
/// <item>
/// <term>example, code</term>
/// <description>
/// These are used for code examples
/// </description>
/// </item>
/// </list>
/// <para>
/// The list above uses the "table" style. You could
/// also use the "bullet" or "number" style. Neither
/// would typically use the "term" element.
/// <br/>
/// Note: paragraphs are double spaced. Use the *br*
/// tag for single spaced lines.
/// </para>
/// </remarks>
public class ExampleClass
{
/// <value>
/// The <c>Label</c> property represents a label
/// for this instance.
/// </value>
/// <remarks>
/// The <see cref="Label"/> is a <see langword="string"/>
/// that you use for a label.
/// <para>
/// Note that there isn't a way to provide a "cref" to
/// each accessor, only to the property itself.
/// </para>
/// </remarks>
public string Label
{
get;
set;
}

/// <summary>
/// Adds two integers and returns the result.
/// </summary>
/// <returns>
/// The sum of two integers.
/// </returns>
/// <param name="left">
/// The left operand of the addition.
/// </param>
/// <param name="right">
/// The right operand of the addition.
/// </param>
/// <example>
/// <code>
/// int c = Math.Add(4, 5);
/// if (c > 10)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// <exception cref="System.OverflowException">
/// Thrown when one parameter is
/// <see cref="Int32.MaxValue">MaxValue</see> and the other is
/// greater than 0.
/// Note that here you can also use
/// <see href="https://docs.microsoft.com/dotnet/api/system.int32.maxvalue"/>
/// to point a web page instead.
/// </exception>
/// <see cref="ExampleClass"/> for a list of all
/// the tags in these examples.
/// <seealso cref="ExampleClass.Label"/>
public static int Add(int left, int right)
{
if ((left == int.MaxValue && right > 0) || (right == int.MaxValue && left > 0))
throw new System.OverflowException();

return left + right;


}
}

/// <summary>
/// This is an example of a positional record.
/// </summary>
/// <remarks>
/// There isn't a way to add XML comments for properties
/// created for positional records, yet. The language
/// design team is still considering what tags should
/// be supported, and where. Currently, you can use
/// the "param" tag to describe the parameters to the
/// primary constructor.
/// </remarks>
/// <param name="FirstName">
/// This tag will apply to the primary constructor parameter.
/// </param>
/// <param name="LastName">
/// This tag will apply to the primary constructor parameter.
/// </param>
public record Person(string FirstName, string LastName);
}

Das Einfügen von Dokumentation kann Ihren Quellcode mit sehr vielen Kommentaren überladen, die für
Benutzer Ihrer Bibliothek vorgesehen sind. Mit dem Tag <Include> trennen Sie Ihre XML-Kommentare von
Ihrem Quellcode. Ihr Quellcode verweist mit dem Tag <Include> auf eine XML-Datei:
/// <include file='xml_include_tag.xml' path='MyDocs/MyMembers[@name="test"]/*' />
class Test
{
static void Main()
{
}
}

/// <include file='xml_include_tag.xml' path='MyDocs/MyMembers[@name="test2"]/*' />


class Test2
{
public void Test()
{
}
}

Die zweite Datei, xml_include_tag.xml, enthält die folgenden Dokumentationskommentare.

<MyDocs>
<MyMembers name="test">
<summary>
The summary for this type.
</summary>
</MyMembers>
<MyMembers name="test2">
<summary>
The summary for this other type.
</summary>
</MyMembers>
</MyDocs>

Dokumentieren einer Hierarchie von Klassen und Schnittstellen


Das Element <inheritdoc> bedeutet, dass ein Typ oder Member Dokumentationskommentare von einer
Basisklasse oder Schnittstelle erbt. Sie können das Element <inheritdoc> auch mit dem cref -Attribut
verwenden, damit Kommentare von einem Member desselben Typs geerbt werden. Im folgenden Beispiel
werden verschiedene Verwendungsmöglichkeiten dieses Tags gezeigt. Beachten Sie, dass beim Hinzufügen des
inheritdoc -Attributs zu einem Typ Memberkommentare geerbt werden. Sie können die Verwendung von
geerbten Kommentaren verhindern, indem Sie Kommentare zu den Membern im abgeleiteten Typ schreiben.
Diese werden den geerbten Kommentaren vorgezogen.

/// <summary>
/// A summary about this class.
/// </summary>
/// <remarks>
/// These remarks would explain more about this class.
/// In this example, these comments also explain the
/// general information about the derived class.
/// </remarks>
public class MainClass
{
}

///<inheritdoc/>
public class DerivedClass : MainClass
{
}

/// <summary>
/// This interface would describe all the methods in
/// its contract.
/// </summary>
/// <remarks>
/// While elided for brevity, each method or property
/// in this interface would contain docs that you want
/// to duplicate in each implementing class.
/// </remarks>
public interface ITestInterface
{
/// <summary>
/// This method is part of the test interface.
/// </summary>
/// <remarks>
/// This content would be inherited by classes
/// that implement this interface when the
/// implementing class uses "inheritdoc"
/// </remarks>
/// <returns>The value of <paramref name="arg" /> </returns>
/// <param name="arg">The argument to the method</param>
int Method(int arg);
}

///<inheritdoc cref="ITestInterface"/>
public class ImplementingClass : ITestInterface
{
// doc comments are inherited here.
public int Method(int arg) => arg;
}

/// <summary>
/// This class shows hows you can "inherit" the doc
/// comments from one method in another method.
/// </summary>
/// <remarks>
/// You can inherit all comments, or only a specific tag,
/// represented by an xpath expression.
/// </remarks>
public class InheritOnlyReturns
{
/// <summary>
/// In this example, this summary is only visible for this method.
/// </summary>
/// <returns>A boolean</returns>
public static bool MyParentMethod(bool x) { return x; }

/// <inheritdoc cref="MyParentMethod" path="/returns"/>


public static bool MyChildMethod() { return false; }
}

/// <Summary>
/// This class shows an example ofsharing comments across methods.
/// </Summary>
public class InheritAllButRemarks
{
/// <summary>
/// In this example, this summary is visible on all the methods.
/// </summary>
/// <remarks>
/// The remarks can be inherited by other methods
/// using the xpath expression.
/// </remarks>
/// <returns>A boolean</returns>
public static bool MyParentMethod(bool x) { return x; }

/// <inheritdoc cref="MyParentMethod" path="//*[not(self::remarks)]"/>


public static bool MyChildMethod() { return false; }
}
Generische Typen
Verwenden Sie das Tag <typeparam> , um Typparameter für generische Typen und Methoden zu beschreiben.
Der Wert für das cref -Attribut erfordert eine neue Syntax, um auf eine generische Methode oder Klasse zu
verweisen:

/// <summary>
/// This is a generic class.
/// </summary>
/// <remarks>
/// This example shows how to specify the <see cref="GenericClass{T}"/>
/// type as a cref attribute.
/// In generic classes and methods, you'll often want to reference the
/// generic type, or the type parameter.
/// </remarks>
class GenericClass<T>
{
// Fields and members.
}

/// <Summary>
/// This shows examples of typeparamref and typeparam tags
/// </Summary>
public class ParamsAndParamRefs
{
/// <summary>
/// The GetGenericValue method.
/// </summary>
/// <remarks>
/// This sample shows how to specify the <see cref="GetGenericValue"/>
/// method as a cref attribute.
/// The parameter and return value are both of an arbitrary type,
/// <typeparamref name="T"/>
/// </remarks>
public static T GetGenericValue<T>(T para)
{
return para;
}
}

Beispiel für eine mathematische Klasse


Der folgende Code zeigt ein realistisches Beispiel für das Hinzufügen von Dokumentationskommentaren zu
einer mathematischen Bibliothek.

namespace TaggedLibrary
{
/*
The main Math class
Contains all methods for performing basic math functions
*/
/// <summary>
/// The main <c>Math</c> class.
/// Contains all methods for performing basic math functions.
/// <list type="bullet">
/// <item>
/// <term>Add</term>
/// <description>Addition Operation</description>
/// </item>
/// <item>
/// <term>Subtract</term>
/// <description>Subtraction Operation</description>
/// </item>
/// <item>
/// <item>
/// <term>Multiply</term>
/// <description>Multiplication Operation</description>
/// </item>
/// <item>
/// <term>Divide</term>
/// <description>Division Operation</description>
/// </item>
/// </list>
/// </summary>
/// <remarks>
/// <para>
/// This class can add, subtract, multiply and divide.
/// </para>
/// <para>
/// These operations can be performed on both
/// integers and doubles.
/// </para>
/// </remarks>
public class Math
{
// Adds two integers and returns the result
/// <summary>
/// Adds two integers <paramref name="a"/> and <paramref name="b"/>
/// and returns the result.
/// </summary>
/// <returns>
/// The sum of two integers.
/// </returns>
/// <example>
/// <code>
/// int c = Math.Add(4, 5);
/// if (c > 10)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// <exception cref="System.OverflowException">
/// Thrown when one parameter is <see cref="Int32.MaxValue"/> and the other
/// is greater than 0.
/// </exception>
/// See <see cref="Math.Add(double, double)"/> to add doubles.
/// <seealso cref="Math.Subtract(int, int)"/>
/// <seealso cref="Math.Multiply(int, int)"/>
/// <seealso cref="Math.Divide(int, int)"/>
/// <param name="a">An integer.</param>
/// <param name="b">An integer.</param>
public static int Add(int a, int b)
{
// If any parameter is equal to the max value of an integer
// and the other is greater than zero
if ((a == int.MaxValue && b > 0) ||
(b == int.MaxValue && a > 0))
{
throw new System.OverflowException();
}
return a + b;
}

// Adds two doubles and returns the result


/// <summary>
/// Adds two doubles <paramref name="a"/> and <paramref name="b"/>
/// and returns the result.
/// </summary>
/// <returns>
/// The sum of two doubles.
/// </returns>
/// <example>
/// <code>
/// <code>
/// double c = Math.Add(4.5, 5.4);
/// if (c > 10)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// <exception cref="System.OverflowException">
/// Thrown when one parameter is max and the other
/// is greater than 0.</exception>
/// See <see cref="Math.Add(int, int)"/> to add integers.
/// <seealso cref="Math.Subtract(double, double)"/>
/// <seealso cref="Math.Multiply(double, double)"/>
/// <seealso cref="Math.Divide(double, double)"/>
/// <param name="a">A double precision number.</param>
/// <param name="b">A double precision number.</param>
public static double Add(double a, double b)
{
// If any parameter is equal to the max value of an integer
// and the other is greater than zero
if ((a == double.MaxValue && b > 0)
|| (b == double.MaxValue && a > 0))
{
throw new System.OverflowException();
}

return a + b;
}

// Subtracts an integer from another and returns the result


/// <summary>
/// Subtracts <paramref name="b"/> from <paramref name="a"/>
/// and returns the result.
/// </summary>
/// <returns>
/// The difference between two integers.
/// </returns>
/// <example>
/// <code>
/// int c = Math.Subtract(4, 5);
/// if (c > 1)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// See <see cref="Math.Subtract(double, double)"/> to subtract doubles.
/// <seealso cref="Math.Add(int, int)"/>
/// <seealso cref="Math.Multiply(int, int)"/>
/// <seealso cref="Math.Divide(int, int)"/>
/// <param name="a">An integer.</param>
/// <param name="b">An integer.</param>
public static int Subtract(int a, int b)
{
return a - b;
}

// Subtracts a double from another and returns the result


/// <summary>
/// Subtracts a double <paramref name="b"/> from another
/// double <paramref name="a"/> and returns the result.
/// </summary>
/// <returns>
/// The difference between two doubles.
/// </returns>
/// <example>
/// <code>
/// double c = Math.Subtract(4.5, 5.4);
/// if (c > 1)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// See <see cref="Math.Subtract(int, int)"/> to subtract integers.
/// <seealso cref="Math.Add(double, double)"/>
/// <seealso cref="Math.Multiply(double, double)"/>
/// <seealso cref="Math.Divide(double, double)"/>
/// <param name="a">A double precision number.</param>
/// <param name="b">A double precision number.</param>
public static double Subtract(double a, double b)
{
return a - b;
}

// Multiplies two integers and returns the result


/// <summary>
/// Multiplies two integers <paramref name="a"/>
/// and <paramref name="b"/> and returns the result.
/// </summary>
/// <returns>
/// The product of two integers.
/// </returns>
/// <example>
/// <code>
/// int c = Math.Multiply(4, 5);
/// if (c > 100)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// See <see cref="Math.Multiply(double, double)"/> to multiply doubles.
/// <seealso cref="Math.Add(int, int)"/>
/// <seealso cref="Math.Subtract(int, int)"/>
/// <seealso cref="Math.Divide(int, int)"/>
/// <param name="a">An integer.</param>
/// <param name="b">An integer.</param>
public static int Multiply(int a, int b)
{
return a * b;
}

// Multiplies two doubles and returns the result


/// <summary>
/// Multiplies two doubles <paramref name="a"/> and
/// <paramref name="b"/> and returns the result.
/// </summary>
/// <returns>
/// The product of two doubles.
/// </returns>
/// <example>
/// <code>
/// double c = Math.Multiply(4.5, 5.4);
/// if (c > 100.0)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// See <see cref="Math.Multiply(int, int)"/> to multiply integers.
/// <seealso cref="Math.Add(double, double)"/>
/// <seealso cref="Math.Subtract(double, double)"/>
/// <seealso cref="Math.Divide(double, double)"/>
/// <param name="a">A double precision number.</param>
/// <param name="b">A double precision number.</param>
public static double Multiply(double a, double b)
{
return a * b;
}

// Divides an integer by another and returns the result


/// <summary>
/// Divides an integer <paramref name="a"/> by another
/// integer <paramref name="b"/> and returns the result.
/// </summary>
/// <returns>
/// The quotient of two integers.
/// </returns>
/// <example>
/// <code>
/// int c = Math.Divide(4, 5);
/// if (c > 1)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// <exception cref="System.DivideByZeroException">
/// Thrown when <paramref name="b"/> is equal to 0.
/// </exception>
/// See <see cref="Math.Divide(double, double)"/> to divide doubles.
/// <seealso cref="Math.Add(int, int)"/>
/// <seealso cref="Math.Subtract(int, int)"/>
/// <seealso cref="Math.Multiply(int, int)"/>
/// <param name="a">An integer dividend.</param>
/// <param name="b">An integer divisor.</param>
public static int Divide(int a, int b)
{
return a / b;
}

// Divides a double by another and returns the result


/// <summary>
/// Divides a double <paramref name="a"/> by another double
/// <paramref name="b"/> and returns the result.
/// </summary>
/// <returns>
/// The quotient of two doubles.
/// </returns>
/// <example>
/// <code>
/// double c = Math.Divide(4.5, 5.4);
/// if (c > 1.0)
/// {
/// Console.WriteLine(c);
/// }
/// </code>
/// </example>
/// <exception cref="System.DivideByZeroException">
/// Thrown when <paramref name="b"/> is equal to 0.
/// </exception>
/// See <see cref="Math.Divide(int, int)"/> to divide integers.
/// <seealso cref="Math.Add(double, double)"/>
/// <seealso cref="Math.Subtract(double, double)"/>
/// <seealso cref="Math.Multiply(double, double)"/>
/// <param name="a">A double precision dividend.</param>
/// <param name="b">A double precision divisor.</param>
public static double Divide(double a, double b)
{
return a / b;
}
}
}
Unter Umständen werden Sie feststellen, dass der Code durch alle diese Kommentare unleserlich wird. Das
letzte Beispiel zeigt, wie diese Bibliothek so angepasst wird, dass sie das Tag include verwendet. Dazu wird die
gesamte Dokumentation in eine XML-Datei verschoben:

<docs>
<members name="math">
<Math>
<summary>
The main <c>Math</c> class.
Contains all methods for performing basic math functions.
</summary>
<remarks>
<para>This class can add, subtract, multiply and divide.</para>
<para>These operations can be performed on both integers and doubles.</para>
</remarks>
</Math>
<AddInt>
<summary>
Adds two integers <paramref name="a"/> and <paramref name="b"/>
and returns the result.
</summary>
<returns>
The sum of two integers.
</returns>
<example>
<code>
int c = Math.Add(4, 5);
if (c > 10)
{
Console.WriteLine(c);
}
</code>
</example>
<exception cref="System.OverflowException">Thrown when one
parameter is max
and the other is greater than 0.</exception>
See <see cref="Math.Add(double, double)"/> to add doubles.
<seealso cref="Math.Subtract(int, int)"/>
<seealso cref="Math.Multiply(int, int)"/>
<seealso cref="Math.Divide(int, int)"/>
<param name="a">An integer.</param>
<param name="b">An integer.</param>
</AddInt>
<AddDouble>
<summary>
Adds two doubles <paramref name="a"/> and <paramref name="b"/>
and returns the result.
</summary>
<returns>
The sum of two doubles.
</returns>
<example>
<code>
double c = Math.Add(4.5, 5.4);
if (c > 10)
{
Console.WriteLine(c);
}
</code>
</example>
<exception cref="System.OverflowException">Thrown when one parameter is max
and the other is greater than 0.</exception>
See <see cref="Math.Add(int, int)"/> to add integers.
<seealso cref="Math.Subtract(double, double)"/>
<seealso cref="Math.Multiply(double, double)"/>
<seealso cref="Math.Divide(double, double)"/>
<param name="a">A double precision number.</param>
<param name="b">A double precision number.</param>
<param name="b">A double precision number.</param>
</AddDouble>
<SubtractInt>
<summary>
Subtracts <paramref name="b"/> from <paramref name="a"/> and
returns the result.
</summary>
<returns>
The difference between two integers.
</returns>
<example>
<code>
int c = Math.Subtract(4, 5);
if (c > 1)
{
Console.WriteLine(c);
}
</code>
</example>
See <see cref="Math.Subtract(double, double)"/> to subtract doubles.
<seealso cref="Math.Add(int, int)"/>
<seealso cref="Math.Multiply(int, int)"/>
<seealso cref="Math.Divide(int, int)"/>
<param name="a">An integer.</param>
<param name="b">An integer.</param>
</SubtractInt>
<SubtractDouble>
<summary>
Subtracts a double <paramref name="b"/> from another
double <paramref name="a"/> and returns the result.
</summary>
<returns>
The difference between two doubles.
</returns>
<example>
<code>
double c = Math.Subtract(4.5, 5.4);
if (c > 1)
{
Console.WriteLine(c);
}
</code>
</example>
See <see cref="Math.Subtract(int, int)"/> to subtract integers.
<seealso cref="Math.Add(double, double)"/>
<seealso cref="Math.Multiply(double, double)"/>
<seealso cref="Math.Divide(double, double)"/>
<param name="a">A double precision number.</param>
<param name="b">A double precision number.</param>
</SubtractDouble>
<MultiplyInt>
<summary>
Multiplies two integers <paramref name="a"/> and
<paramref name="b"/> and returns the result.
</summary>
<returns>
The product of two integers.
</returns>
<example>
<code>
int c = Math.Multiply(4, 5);
if (c > 100)
{
Console.WriteLine(c);
}
</code>
</example>
See <see cref="Math.Multiply(double, double)"/> to multiply doubles.
<seealso cref="Math.Add(int, int)"/>
<seealso cref="Math.Subtract(int, int)"/>
<seealso cref="Math.Divide(int, int)"/>
<param name="a">An integer.</param>
<param name="b">An integer.</param>
</MultiplyInt>
<MultiplyDouble>
<summary>
Multiplies two doubles <paramref name="a"/> and
<paramref name="b"/> and returns the result.
</summary>
<returns>
The product of two doubles.
</returns>
<example>
<code>
double c = Math.Multiply(4.5, 5.4);
if (c > 100.0)
{
Console.WriteLine(c);
}
</code>
</example>
See <see cref="Math.Multiply(int, int)"/> to multiply integers.
<seealso cref="Math.Add(double, double)"/>
<seealso cref="Math.Subtract(double, double)"/>
<seealso cref="Math.Divide(double, double)"/>
<param name="a">A double precision number.</param>
<param name="b">A double precision number.</param>
</MultiplyDouble>
<DivideInt>
<summary>
Divides an integer <paramref name="a"/> by another integer
<paramref name="b"/> and returns the result.
</summary>
<returns>
The quotient of two integers.
</returns>
<example>
<code>
int c = Math.Divide(4, 5);
if (c > 1)
{
Console.WriteLine(c);
}
</code>
</example>
<exception cref="System.DivideByZeroException">
Thrown when <paramref name="b"/> is equal to 0.
</exception>
See <see cref="Math.Divide(double, double)"/> to divide doubles.
<seealso cref="Math.Add(int, int)"/>
<seealso cref="Math.Subtract(int, int)"/>
<seealso cref="Math.Multiply(int, int)"/>
<param name="a">An integer dividend.</param>
<param name="b">An integer divisor.</param>
</DivideInt>
<DivideDouble>
<summary>
Divides a double <paramref name="a"/> by another
double <paramref name="b"/> and returns the result.
</summary>
<returns>
The quotient of two doubles.
</returns>
<example>
<code>
double c = Math.Divide(4.5, 5.4);
if (c > 1.0)
{
{
Console.WriteLine(c);
}
</code>
</example>
<exception cref="System.DivideByZeroException">Thrown when <paramref name="b"/> is equal to 0.
</exception>
See <see cref="Math.Divide(int, int)"/> to divide integers.
<seealso cref="Math.Add(double, double)"/>
<seealso cref="Math.Subtract(double, double)"/>
<seealso cref="Math.Multiply(double, double)"/>
<param name="a">A double precision dividend.</param>
<param name="b">A double precision divisor.</param>
</DivideDouble>
</members>
</docs>

Im obigen XML werden die Dokumentationskommentare jedes Members direkt innerhalb eines Tags angezeigt,
das nach der Funktion benannt ist. Sie können Ihre eigene Strategie auswählen. Der Code verwendet das Tag
<include> , um auf das entsprechende Element in der XML-Datei zu verweisen:

namespace IncludeTag
{

/*
The main Math class
Contains all methods for performing basic math functions
*/
/// <include file='include.xml' path='docs/members[@name="math"]/Math/*'/>
public class Math
{
// Adds two integers and returns the result
/// <include file='include.xml' path='docs/members[@name="math"]/AddInt/*'/>
public static int Add(int a, int b)
{
// If any parameter is equal to the max value of an integer
// and the other is greater than zero
if ((a == int.MaxValue && b > 0) || (b == int.MaxValue && a > 0))
throw new System.OverflowException();

return a + b;
}

// Adds two doubles and returns the result


/// <include file='include.xml' path='docs/members[@name="math"]/AddDouble/*'/>
public static double Add(double a, double b)
{
// If any parameter is equal to the max value of an integer
// and the other is greater than zero
if ((a == double.MaxValue && b > 0) || (b == double.MaxValue && a > 0))
throw new System.OverflowException();

return a + b;
}

// Subtracts an integer from another and returns the result


/// <include file='include.xml' path='docs/members[@name="math"]/SubtractInt/*'/>
public static int Subtract(int a, int b)
{
return a - b;
}

// Subtracts a double from another and returns the result


/// <include file='include.xml' path='docs/members[@name="math"]/SubtractDouble/*'/>
public static double Subtract(double a, double b)
{
return a - b;
}
}

// Multiplies two integers and returns the result


/// <include file='include.xml' path='docs/members[@name="math"]/MultiplyInt/*'/>
public static int Multiply(int a, int b)
{
return a * b;
}

// Multiplies two doubles and returns the result


/// <include file='include.xml' path='docs/members[@name="math"]/MultiplyDouble/*'/>
public static double Multiply(double a, double b)
{
return a * b;
}

// Divides an integer by another and returns the result


/// <include file='include.xml' path='docs/members[@name="math"]/DivideInt/*'/>
public static int Divide(int a, int b)
{
return a / b;
}

// Divides a double by another and returns the result


/// <include file='include.xml' path='docs/members[@name="math"]/DivideDouble/*'/>
public static double Divide(double a, double b)
{
return a / b;
}
}
}

Das file -Attribut stellt den Namen der XML-Datei dar, die die Dokumentation enthält.
Das path -Attribut stellt eine XPath-Abfrage an den vorhandenen tag name im angegebenen file dar.
Das name -Attribut stellt den Namensbezeichner in dem Tag dar, das sich vor den Kommentaren befindet.
Das id -Attribut, das anstelle von name verwendet werden kann, stellt die ID für das Tag dar, das den
Kommentaren vorangestellt ist.
C#-Compilerfehler
04.11.2021 • 2 minutes to read

Zu einigen C#-Compilerfehlern gibt es entsprechende Themen, die erläutern, warum ein Fehler generiert wurde
und – in einigen Fällen – wie sich der Fehler beheben lässt. Finden Sie anhand der folgenden Schritte heraus, ob
für eine bestimmte Fehlermeldung Hilfe verfügbar ist.
Wenn Sie Visual Studio verwenden, wählen Sie die Fehlernummer (z. B. CS0029) im Ausgabefenster aus, und
drücken Sie dann die Taste F1.
Geben Sie die Fehlernummer im Inhaltsverzeichnis in das Feld Nach Titel filtern ein.
Wenn Sie mit keinem dieser Schritte Informationen über den Fehler erhalten, scrollen Sie zum Ende dieser Seite,
und senden Sie uns Feedback (einschließlich Nummer oder Text des Fehlers).
Informationen zum Konfigurieren von Fehler- und Warnungsoptionen in C# finden Sie unter C#-
Compileroptionen oder zu Visual Studio unter Seite „Erstellen“, Projekt-Designer (C#).

NOTE
Auf Ihrem Computer werden möglicherweise andere Namen oder Speicherorte für die Benutzeroberflächenelemente von
Visual Studio angezeigt als die in den folgenden Anweisungen aufgeführten. Diese Elemente sind von der jeweiligen Visual
Studio-Version und den verwendeten Einstellungen abhängig. Weitere Informationen finden Sie unter Personalisieren der
IDE.

Weitere Informationen
C#-Compileroptionen
Seite „Erstellen“, Projekt-Designer (C#)
WarningLevel (C#-Compileroptionen)
DisabledWarnings (C#-Compileroptionen)
Einführung
04.11.2021 • 97 minutes to read

C# (Aussprache „C Sharp“) ist eine einfache, moderne, objektorientierte und typsichere Programmiersprache. C#
hat seine Wurzeln in der c-Sprachen Familie und ist sofort mit c-, C++-und Java-Programmierern vertraut. C#
wird von ECMA International als *ECMA-334 _ Standard und ISO/IEC als _ *ISO/IEC 23270**-Standard
standardisiert. Der c#-Compiler von Microsoft für die .NET Framework ist eine konforme Implementierung
beider Standards.
C# ist eine objektorientierte Sprache, umfasst allerdings auch Unterstützung für eine
komponentenorientier te Programmierung. Die Softwareentwicklung von heute beruht zunehmend auf
Softwarekomponenten in Form von eigenständigen und selbstbeschreibenden Funktionspaketen. Wichtig bei
solchen Komponenten ist, dass sie für ein Programmiermodell mit Eigenschaften, Methoden und Ereignissen
stehen. Sie verfügen über Attribute, die deklarative Informationen zur Komponente bereitstellen, und lassen sich
in ihre eigene Dokumentation integrieren. C# stellt Sprachkonstrukte bereit, um diese Konzepte direkt zu
unterstützen, sodass c# eine natürliche Sprache ist, in der Softwarekomponenten erstellt und verwendet werden
können.
Mehrere c#-Features helfen bei der Erstellung stabiler und dauerhafter Anwendungen: Garbage Collection _
gibt automatisch Speicher frei, der von nicht verwendeten Objekten belegt wird. die _Ausnahmebehandlung_
bietet eine strukturierte und erweiterbare Methode zur Fehlererkennung und-Wiederherstellung. und das _
typsichere Design der Sprache macht es nicht möglich, aus nicht initialisierten Variablen zu lesen, Arrays
außerhalb ihrer Grenzen zu indizieren oder nicht überprüfte Typumwandlungen auszuführen.
C# verfügt über ein einheitliches Typsystem . Alle C#-Typen, einschließlich primitiver Typen wie int und
double , erben von einem einzelnen object -Stammtyp. Daher verwenden alle Typen einen Satz allgemeiner
Vorgänge, und Werte eines beliebigen Typs können gespeichert, übertragen und konsistent ausgeführt werden.
Darüber hinaus unterstützt C# benutzerdefinierte Verweistypen und Werttypen und ermöglicht so die
dynamische Zuordnung von Objekten sowie die Inlinespeicherung einfacher Strukturen.
Um sicherzustellen, dass C#-Programme und -Bibliotheken im Lauf der Zeit kompatibel weiterentwickelt
werden können, wurde bei der Entwicklung von C# viel Wert auf Versionsver waltung gelegt. Viele
Programmiersprachen schenken diesem Problem wenig Beachtung, und in dieser Sprache geschriebene
Programme stürzen daher häufiger als notwendig ab, wenn neuere Versionen abhängiger Bibliotheken
eingeführt werden. Zu den Aspekten der Entwicklung von C#, die direkt von Überlegungen bei der
Versionskontrolle beeinflusst wurden, gehören die separaten virtual - und override -Modifizierer, die Regeln
für die Überladungsauflösung und die Unterstützung für explizite Schnittstellenmember-Deklarationen.
Im weiteren Verlauf dieses Kapitels werden die wesentlichen Features der Programmiersprache c# beschrieben.
Obwohl in späteren Kapiteln Regeln und Ausnahmen ausführlich und manchmal auf mathematische Weise
beschrieben werden, werden in diesem Kapitel Klarheit und Übersichtlichkeit auf Kosten der Vollständigkeit
angestrebt. Die Absicht besteht darin, dem Reader eine Einführung in die Sprache bereitzustellen, die das
Schreiben von frühen Programmen und das Lesen von späteren Kapiteln erleichtert.

Hello World
Das Programm „Hello, World“ wird für gewöhnlich zur Einführung einer Programmiersprache verwendet. Hier
ist es in C#:
using System;

class Hello
{
static void Main() {
Console.WriteLine("Hello, World");
}
}

C#-Quelldateien weisen in der Regel die Dateierweiterung .cs auf. Wenn das Programm "Hello, World" in der
Datei gespeichert ist hello.cs , kann das Programm über die Befehlszeile mit dem Microsoft c#-Compiler
kompiliert werden.

csc hello.cs

die eine ausführbare Assembly mit dem Namen erzeugt hello.exe . Die von dieser Anwendung beim
Ausführen von erstellte Ausgabe ist

Hello, World

Das Programm „Hello, World“ wird mit einer using -Richtlinie gestartet, die auf den System -Namespace
verweist. Namespaces bieten eine hierarchische Möglichkeit zum Organisieren von C#-Programmen und -
Bibliotheken. Namespaces enthalten Typen und andere Namespaces. Beispiel: Der System -Namespace enthält
eine Reihe von Typen, wie etwa die Console -Klasse, auf die im Programm verwiesen wird, und eine Reihe
anderer Namespaces, wie etwa IO und Collections . Eine using -Richtlinie, die auf einen bestimmten
Namespace verweist, ermöglicht die nicht qualifizierte Nutzung der Typen, die Member dieses Namespace sind.
Aufgrund der using -Direktive kann das Programm Console.WriteLine als Abkürzung für
System.Console.WriteLine verwenden.

Die Hello-Klasse, die vom Programm „Hello, World“ deklariert wird, verfügt über einen einzelnen Member: die
Main -Methode. Die Main -Methode wird mit dem Modifizierer static deklariert. Auch wenn Instanzmethoden
mit dem Schlüsselwort this auf eine bestimmte einschließende Objektinstanz verweisen können, agieren
statische Methoden ohne Verweis auf ein bestimmtes Objekt. Gemäß der Konvention fungiert eine statische
Methode mit der Bezeichnung Main als Einstiegspunkt eines Programms.
Die Ausgabe des Programms wird anhand der WriteLine -Methode der Console -Klasse im System -Namespace
generiert. Diese Klasse wird von den .NET Framework-Klassenbibliotheken bereitgestellt, die standardmäßig
automatisch vom Microsoft c#-Compiler referenziert werden. Beachten Sie, dass c# selbst über keine separate
Lauf Zeit Bibliothek verfügt. Stattdessen ist die .NET Framework die Lauf Zeit Bibliothek von c#.

Programmstruktur
Die wichtigsten Organisationskonzepte in c# sind Programme _, _Namespaces_, _Typen_, _Member und_
Assemblys _.* C#-Programme bestehen aus mindestens einer Quelldatei. Programme deklarieren Typen, die
Member enthalten, und können in Namespaces organisiert werden. Klassen und Schnittstellen sind Beispiele für
Typen. Felder, Methoden, Eigenschaften und Ereignisse sind Beispiele für Member. Wenn C#-Programme
kompiliert werden, werden sie physisch in Assemblys verpackt. Assemblys verfügen in der Regel über die
Dateierweiterung .exe oder .dll , je nachdem, ob Sie Anwendungen oder _ -Bibliotheken * implementieren.
Das Beispiel
using System;

namespace Acme.Collections
{
public class Stack
{
Entry top;

public void Push(object data) {


top = new Entry(top, data);
}

public object Pop() {


if (top == null) throw new InvalidOperationException();
object result = top.data;
top = top.next;
return result;
}

class Entry
{
public Entry next;
public object data;

public Entry(Entry next, object data) {


this.next = next;
this.data = data;
}
}
}
}

deklariert eine Klasse mit dem Namen Stack in einem Namespace mit dem Namen Acme.Collections . Der
vollqualifizierte Name dieser Klasse ist Acme.Collections.Stack . Die Klasse enthält mehrere Member: ein Feld
mit dem Namen top , zwei Methoden mit dem Namen Push und Pop sowie eine geschachtelte Klasse mit
dem Namen Entry . Die Entry -Klasse enthält weitere drei Member: ein Feld mit dem Namen next , ein Feld
mit dem Namen data und einen Konstruktor. Vorausgesetzt, dass der Quellcode des Beispiels in der Datei
acme.cs gespeichert wird, kompiliert die Befehlszeile

csc /t:library acme.cs

das Beispiel als Bibliothek (Code ohne Main -Einstiegspunkt) und erstellt eine Assembly mit dem Namen
acme.dll .

Assemblys enthalten ausführbaren Code in Form von *Intermediate Language _ (IL)-Anweisungen und
symbolische Informationen in Form von _ Metadata *. Vor der Ausführung wird der IL-Code in einer Assembly
automatisch durch den Just-in-Time-Compiler (JIT) der .NET Common Language Runtime in
prozessorspezifischen Code konvertiert.
Da eine Assembly eine selbstbeschreibende Funktionseinheit mit Code und Metadaten ist, besteht in C# keine
Notwendigkeit für #include -Direktiven und Headerdateien. Die öffentlichen Typen und Member, die in einer
bestimmten Assembly enthalten sind, werden einfach durch Verweisen auf die Assembly beim Kompilieren des
Programms in einem C#-Programm verfügbar gemacht. Dieses Programm verwendet z.B. die
Acme.Collections.Stack -Klasse aus der acme.dll -Assembly:
using System;
using Acme.Collections;

class Test
{
static void Main() {
Stack s = new Stack();
s.Push(1);
s.Push(10);
s.Push(100);
Console.WriteLine(s.Pop());
Console.WriteLine(s.Pop());
Console.WriteLine(s.Pop());
}
}

Wenn das Programm in der Datei gespeichert wird test.cs , test.cs kann bei der Kompilierung acme.dll
von auf die Assembly mit der-Option des Compilers verwiesen werden /r :

csc /r:acme.dll test.cs

So wird eine ausführbare Assembly mit dem Namen test.exe erstellt, die bei Ausführung folgende Ausgabe
erzeugt:

100
10
1

In C# kann der Quelltext eines Programms in verschiedenen Quelldateien gespeichert werden. Bei der
Kompilierung eines C#-Programms mit mehreren Dateien werden alle Quelldateien zusammen verarbeitet, und
die Quelldateien können frei aufeinander verweisen – vom Konzept her ist es so, als seien alle Quelldateien vor
der Verarbeitung in einer einzigen großen Datei verkettet worden. Vorwärtsdeklarationen sind in C# nie
erforderlich, da die Reihenfolge der Deklaration mit wenigen Ausnahmen unbedeutend ist. C# beschränkt eine
Quelldatei weder auf die Deklaration eines einzigen öffentlichen Typs, noch muss der Name der Quelldatei mit
einem in der Quelldatei deklarierten Typ übereinstimmen.

Typen und Variablen


Es gibt zwei Arten von Typen in c#: *Wer ttypen _ und _ Verweis Typen *. Variablen von Werttypen enthalten
ihre Daten direkt, Variablen von Verweistypen speichern hingegen Verweise auf ihre Daten – letztere werden als
Objekte bezeichnet. Mit Verweistypen können zwei Variablen auf das gleiche Objekt verweisen, und so können
an einer Variablen durchgeführte Vorgänge das Objekt beeinflussen, auf das die andere Variable verweist. Bei
Werttypen besitzt jede Variable eine eigene Kopie der Daten, und auf eine Variable angewendete Vorgänge
können die andere Variable nicht beeinflussen (außer im Fall von ref - und out -Parametervariablen).
Die Werttypen von c# sind weiter unterteilt in einfache Typen _, Enumerationstypen, _Strukturtypen_ und
Typen, die _NULL-Werte_ zulassen, und die Verweis Typen von c# sind weiter unterteilt in _Klassentypen_,
_Schnittstellentypen_, _Array Typen*_ und _ *-Delegattypen * *. **
In der folgenden Tabelle finden Sie eine Übersicht über das c#-Typsystem.

K AT EGO RIE B ESC H REIB UN G

Werttypen Einfache Typen Ganzzahlig mit Vorzeichen: sbyte ,


short , int , long
K AT EGO RIE B ESC H REIB UN G

Ganzzahlig ohne Vorzeichen: byte ,


ushort , uint , ulong

Unicode-Zeichen: char

IEEE-Gleitkomma: float , double

Dezimalwert mit hoher Genauigkeit:


decimal

Boolesch: bool

Enumerationstypen Benutzerdefinierte Typen der Form


enum E {...}

Strukturtypen Benutzerdefinierte Typen der Form


struct S {...}

Nullable-Typen Erweiterungen aller anderen


Werttypen mit einem null -Wert

Verweistypen Klassentypen Ultimative Basisklasse aller anderen


Typen: object

Unicode-Zeichenfolgen: string

Benutzerdefinierte Typen der Form


class C {...}

Schnittstellentypen Benutzerdefinierte Typen der Form


interface I {...}

Arraytypen Ein- und mehrdimensional, z.B. int[]


und int[,]

Delegattypen Benutzerdefinierte Typen der Form, z.


b. delegate int D(...)

Die acht Ganzzahltypen unterstützen 8-Bit-, 16-Bit, 32-Bit- und 64-Bit-Werte mit oder ohne Vorzeichen.
Die zwei Gleit Komma Typen, float und double , werden mit den 32-Bit-Formaten für die einfache
Genauigkeit und 64 Bit mit doppelter Genauigkeit mit doppelter 754 Genauigkeit dargestellt.
Der decimal -Typ ist ein für Finanz-und Währungsberechnungen geeigneter 128-Bit-Datentyp.
Der c#- bool Typ wird verwendet, um boolesche Werte darzustellen – Werte, die entweder true oder sind
false .

Zur Zeichen- und Zeichenfolgenverarbeitung in C# wird die Unicode-Codierung verwendet. Der char -Typ stellt
eine UTF-16-Codeeinheit dar und der string -Typ eine Folge von UTF-16-Codeeinheiten.
In der folgenden Tabelle sind die numerischen c#-Typen zusammengefasst.
K AT EGO RIE B IT S TYP B EREIC H / GEN A UIGK EIT

Ganzzahliges Vorzeichen 8 sbyte -128... 127

16 short -32768... 32, 767

32 int -2147483648... 2, 147, 483,


647

64 long -
9.223.372.036.854.775.808
... 9, 223, 372, 036, 854,
775, 807

Ganzzahlig ohne Vorzeichen 8 byte 0... 255

16 ushort 0... 65, 535

32 uint 0... 4, 294, 967, 295

64 ulong 0... 18, 446, 744, 073, 709,


551, 615

Gleitkomma 32 float 1,5 × 10 ^ − 45 bis 3,4 ×


10 ^ 38, 7-stellige
Genauigkeit

64 double 5,0 × 10 ^ − 324 bis 1,7 ×


10 ^ 308, 15-stellige
Genauigkeit

Decimal 128 decimal 1,0 × 10 ^ − 28 bis 7,9 ×


10 ^ 28, 28-stellige
Genauigkeit

C#-Programme verwenden Typdeklarationen , um neue Typen zu erstellen. Eine Typdeklaration gibt den
Namen und die Member des neuen Typs an. Fünf Typkategorien von C# sind benutzerdefinierbar: Klassentypen,
Strukturtypen, Schnittstellentypen, Enumerationstypen und Delegattypen.
Ein Klassentyp definiert eine Datenstruktur, die Datenmember (Felder) und Funktionsmember (Methoden,
Eigenschaften usw.) enthält. Klassentypen unterstützen einzelne Vererbung und Polymorphie. Dies sind
Mechanismen, durch die abgeleitete Klassen erweitert und Basisklassen spezialisiert werden können.
Ein Strukturtyp ähnelt einem Klassentyp darin, dass er eine Struktur mit Datenmembern und
Funktionsmembern darstellt. Im Unterschied zu Klassen sind Strukturen jedoch Werttypen und erfordern keine
Heap Zuordnung. Strukturtypen unterstützen keine benutzerdefinierte Vererbung, und alle Strukturtypen erben
implizit vom Typ object .
Ein Schnittstellentyp definiert einen Vertrag als einen benannten Satz von öffentlichen Funktionsmembern. Eine
Klasse oder Struktur, die eine Schnittstelle implementiert, muss Implementierungen der Funktionsmember der
Schnittstelle bereitstellen. Eine Schnittstelle kann von mehreren Basis Schnittstellen erben, und eine Klasse oder
Struktur kann mehrere Schnittstellen implementieren.
Ein Delegattyp stellt Verweise auf Methoden mit einer bestimmten Parameterliste und dem Rückgabetyp dar.
Delegate ermöglichen die Behandlung von Methoden als Entitäten, die Variablen zugewiesen und als Parameter
übergeben werden können. Delegate ähneln dem Konzept von Funktionszeigern, die Sie in einigen anderen
Sprachen finden. Im Gegensatz zu Funktionszeigern sind Delegate allerdings objektorientiert und typsicher.
Klassen-, Struktur-, Schnittstellen-und Delegattypen unterstützen alle Generika, wobei Sie mit anderen Typen
parametrisiert werden können.
Ein Enumerationstyp ist ein eindeutiger Typ mit benannten Konstanten. Jeder Enumerationstyp verfügt über
einen zugrunde liegenden Typ, bei dem es sich um einen der acht ganzzahligen Typen handeln muss. Der Satz
von Werten eines Enumerationstyps ist mit dem Satz von Werten des zugrunde liegenden Typs identisch.
C# unterstützt ein- und mehrdimensionale Arrays beliebigen Typs. Im Gegensatz zu den oben aufgeführten
Typen müssen Arraytypen nicht deklariert werden, bevor sie verwendet werden können. Stattdessen werden
Arraytypen erstellt, indem hinter einen Typnamen eckige Klammern gesetzt werden. Beispielsweise int[] ist
ein eindimensionales Array von int , int[,] ist ein zweidimensionales Array von int und int[][] ist ein
eindimensionales Array von eindimensionalen Arrays von int .
Typen, die NULL-Werte zulassen, müssen auch nicht deklariert werden, bevor Sie verwendet werden können.
Für jeden Werttyp, der keine NULL-Werte zulässt T , gibt es einen entsprechenden Werte zulässt-Typ T? , der
einen zusätzlichen Wert enthalten kann null . Beispielsweise int? ist ein Typ, der eine beliebige 32-Bit-
Ganzzahl oder den Wert enthalten kann null .
Das Typsystem von c# ist einheitlich, sodass ein Wert eines beliebigen Typs als Objekt behandelt werden kann.
Jeder Typ in C# ist direkt oder indirekt vom object -Klassentyp abgeleitet, und object ist die ultimative
Basisklasse aller Typen. Werte von Verweistypen werden als Objekte behandelt, indem die Werte einfach als Typ
object angezeigt werden. Werte von Werttypen werden als Objekte behandelt, indem *Boxing _-und _
*Unboxing**-Vorgänge durchgeführt werden. Im folgenden Beispiel wird ein int -Wert in ein object und
wieder in einen int -Wert konvertiert.

using System;

class Test
{
static void Main() {
int i = 123;
object o = i; // Boxing
int j = (int)o; // Unboxing
}
}

Wenn ein Wert eines Werttyps in den Typ konvertiert wird object , wird eine Objektinstanz, die auch als "Box"
bezeichnet wird, zum Speichern des Werts zugeordnet, und der Wert wird in dieses Feld kopiert. Wenn
umgekehrt ein object Verweis in einen Werttyp umgewandelt wird, wird überprüft, ob das Objekt, auf das
verwiesen wird, ein Feld des korrekten Werttyps ist, und wenn die Überprüfung erfolgreich ist, wird der Wert im
Feld kopiert.
Das vereinheitlichte c#-Typsystem bedeutet, dass Werttypen "Bedarfs gesteuert" Objekte werden können.
Aufgrund der Vereinheitlichung können Bibliotheken für allgemeine Zwecke, die den Typ object verwenden,
sowohl mit Verweis- als auch Werttypen verwendet werden.
Es gibt mehrere Arten von Variablen in C#, einschließlich Feldern, Arrayelementen, lokalen Variablen und
Parametern. Variablen stellen Speicherorte dar, und jede Variable verfügt über einen Typ, der bestimmt, welche
Werte in der Variablen gespeichert werden können, wie in der folgenden Tabelle dargestellt.

VA RIA B L EN T Y P M Ö GL IC H ER IN H A LT

Nicht auf NULL festlegbarer Werttyp Ein Wert genau dieses Typs
VA RIA B L EN T Y P M Ö GL IC H ER IN H A LT

Auf NULL festlegbarer Werttyp Ein NULL-Wert oder ein Wert dieses exakten Typs.

object Ein NULL-Verweis, ein Verweis auf ein Objekt eines


beliebigen Verweis Typs oder ein Verweis auf einen geboxten
Wert eines beliebigen Werttyps.

Klassentyp Ein NULL-Verweis, ein Verweis auf eine Instanz dieses


Klassen Typs oder ein Verweis auf eine Instanz einer Klasse,
die von diesem Klassentyp abgeleitet ist.

Schnittstellentyp Ein NULL-Verweis, ein Verweis auf eine Instanz eines Klassen
Typs, der diesen Schnittstellentyp implementiert, oder ein
Verweis auf einen geboxten Wert eines Werttyps, der diesen
Schnittstellentyp implementiert.

Arraytyp Ein NULL-Verweis, ein Verweis auf eine Instanz dieses Array
Typs oder ein Verweis auf eine Instanz eines kompatiblen
Arraytyps.

Delegattyp Ein NULL-Verweis oder ein Verweis auf eine Instanz dieses
Delegattyps.

Ausdrücke
Ausdrücke _ werden aus _Operanden_ und _Operatoren*_ erstellt. Die Operatoren eines Ausdrucks geben an,
welche Operationen auf die Operanden angewendet werden. Beispiele für Operatoren sind + , - , _ , / und
new . Beispiele für Operanden sind Literale, Felder, lokale Variablen und Ausdrücke.

Wenn ein Ausdruck mehrere Operatoren enthält, steuert die *Rangfolge _ der Operatoren die Reihenfolge, in
der die einzelnen Operatoren ausgewertet werden. Der Ausdruck x + y _ z wird z.B. als x + (y * z)
ausgewertet, da der * -Operator Vorrang vor dem + -Operator hat.
Die meisten Operatoren können überladen werden. Das Überladen von Operatoren ermöglicht die Angabe
benutzerdefinierter Operatorimplementierungen für Vorgänge, in denen einer der Operanden oder beide einer
benutzerdefinierten Klasse oder einem benutzerdefinierten Strukturtyp angehören.
In der folgenden Tabelle sind die c#-Operatoren zusammengefasst, wobei die Operator Kategorien in der
Rangfolge von der höchsten zur niedrigsten aufgeführt sind. Operatoren in derselben Kategorie haben die
gleiche Rangfolge.

K AT EGO RIE A USDRUC K B ESC H REIB UN G

Primär x.m Memberzugriff

x(...) Methoden- und Delegataufruf

x[...] Array- und Indexerzugriff

x++ Postinkrement

x-- Postdekrement
K AT EGO RIE A USDRUC K B ESC H REIB UN G

new T(...) Objekt- und Delegaterstellung

new T(...){...} Objekterstellung mit Initialisierer

new {...} Anonymer Objektinitialisierer

new T[...] Arrayerstellung

typeof(T) Abrufen von System.Type -Objekt für


T

checked(x) Auswerten von Ausdrücken in


geprüftem Kontext

unchecked(x) Auswerten von Ausdrücken in nicht


geprüftem Kontext

default(T) Abrufen des Standardwerts vom Typ


T

delegate {...} Anonyme Funktion (anonyme


Methode)

Unär +x Identity

-x Negation

!x Logische Negation

~x Bitweise Negation

++x Präinkrement

--x Prädekrement

(T)x Explizites Konvertieren von x in den


Typ T

await x Asynchrones Warten auf den


Abschluss von x

Multiplikativ x * y Multiplikation

x / y Division

x % y Rest

Additiv x + y Addition, Zeichenfolgenverkettung,


Delegatkombination
K AT EGO RIE A USDRUC K B ESC H REIB UN G

x - y Subtraktion, Delegatentfernung

Shift x << y Linksverschiebung

x >> y Rechtsverschiebung

Relational und Typtest x < y Kleiner als

x > y Größer als

x <= y Kleiner oder gleich

x >= y Größer als oder gleich

x is T true zurückgeben, wenn x ein T


ist, andernfalls false

x as T x als T typisiert zurückgeben, oder


null , wenn x kein T ist

Gleichheit x == y Gleich

x != y Ungleich

Logisches AND x & y Ganzzahliges bitweises AND,


boolesches logisches AND

Logisches XOR x ^ y Ganzzahliges bitweises XOR,


boolesches logisches XOR

Logisches OR x | y Ganzzahliges bitweises OR, boolesches


logisches OR

Bedingtes AND x && y Wertet y nur aus, wenn x``true

Bedingtes OR x || y Wertet y nur aus, wenn x``false

NULL-Sammeloperator x ?? y Ergibt y , wenn x ist null , x


andernfalls.

Bedingt x ? y : z Wertet y aus, wenn x``true ist,


z , wenn x``false ist

Zuweisung oder anonyme Funktion x = y Zuweisung

x op= y Verbund Zuweisung; Unterstützte


Operatoren *= /= %= += -=
<<= sind >>= &= ^= |=
K AT EGO RIE A USDRUC K B ESC H REIB UN G

(T x) => y Anonyme Funktion (Lambda-


Ausdruck)

Anweisungen
Die Aktionen eines Programms werden mit Anweisungen ausgedrückt. C# unterstützt verschiedene Arten von
Anweisungen, von denen ein Teil als eingebettete Anweisungen definiert ist.
Ein Block ermöglicht, mehrere Anweisungen in Kontexten zu schreiben, in denen eine einzelne Anweisung
zulässig ist. Ein Block besteht aus einer Liste von Anweisungen, die zwischen den Trennzeichen { und }
geschrieben sind.
Deklarationsanweisungen werden verwendet, um lokale Variablen und Konstanten deklarieren.
Ausdrucksanweisungen werden zum Auswerten von Ausdrücken verwendet. Ausdrücke, die als
Anweisungen verwendet werden können, umfassen Methodenaufrufe, Objekt Zuordnungen mit dem- new
Operator, Zuweisungen mithilfe von = und die Verbund Zuweisungs Operatoren, Inkrement-und
dekrementvorgänge mit den ++ -- Operatoren und und erwarten Ausdrücke.
Auswahlanweisungen werden verwendet, um eine Anzahl von möglichen Anweisungen für die Ausführung
anhand des Werts eines Ausdrucks auszuwählen. Zu dieser Gruppe gehören die if - und switch -
Anweisungen.
Iterations Anweisungen werden verwendet, um eine eingebettete Anweisung wiederholt auszuführen. Zu
dieser Gruppe gehören die while -, do -, for - und foreach -Anweisungen.
Sprunganweisungen werden verwendet, um die Steuerung zu übertragen. Zu dieser Gruppe gehören die
break -, continue -, goto -, throw -, return - und yield -Anweisungen.

Mit der try ... catch -Anweisung werden Ausnahmen abgefangen, die während der Ausführung eines Blocks
auftreten, und mit der try ... finally -Anweisung wird Finalisierungscode angegeben, der immer ausgeführt
wird, unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht.
Die checked -und unchecked -Anweisungen werden verwendet, um den Überlauf Überprüfungs Kontext für
arithmetische Operationen im ganzzahligen Typ und Konvertierungen zu steuern.
Die lock -Anweisung wird verwendet, um die Sperre für gegenseitigen Ausschluss für ein bestimmtes Objekt
abzurufen, eine Anweisung auszuführen und die Sperre aufzuheben.
Die using -Anweisung wird verwendet, um eine Ressource abzurufen, eine Anweisung auszuführen und dann
diese Ressource zu verwerfen.
Im folgenden finden Sie Beispiele für jede Art von Anweisung.
Deklarationen von lokalen Variablen

static void Main() {


int a;
int b = 2, c = 3;
a = 1;
Console.WriteLine(a + b + c);
}

Deklaration der lokalen Konstante


static void Main() {
const float pi = 3.1415927f;
const int r = 25;
Console.WriteLine(pi * r * r);
}

Ausdrucksanweisung

static void Main() {


int i;
i = 123; // Expression statement
Console.WriteLine(i); // Expression statement
i++; // Expression statement
Console.WriteLine(i); // Expression statement
}

if -Anweisung

static void Main(string[] args) {


if (args.Length == 0) {
Console.WriteLine("No arguments");
}
else {
Console.WriteLine("One or more arguments");
}
}

switch -Anweisung

static void Main(string[] args) {


int n = args.Length;
switch (n) {
case 0:
Console.WriteLine("No arguments");
break;
case 1:
Console.WriteLine("One argument");
break;
default:
Console.WriteLine("{0} arguments", n);
break;
}
}

while -Anweisung

static void Main(string[] args) {


int i = 0;
while (i < args.Length) {
Console.WriteLine(args[i]);
i++;
}
}

do -Anweisung
static void Main() {
string s;
do {
s = Console.ReadLine();
if (s != null) Console.WriteLine(s);
} while (s != null);
}

for -Anweisung

static void Main(string[] args) {


for (int i = 0; i < args.Length; i++) {
Console.WriteLine(args[i]);
}
}

foreach -Anweisung

static void Main(string[] args) {


foreach (string s in args) {
Console.WriteLine(s);
}
}

break -Anweisung

static void Main() {


while (true) {
string s = Console.ReadLine();
if (s == null) break;
Console.WriteLine(s);
}
}

continue -Anweisung

static void Main(string[] args) {


for (int i = 0; i < args.Length; i++) {
if (args[i].StartsWith("/")) continue;
Console.WriteLine(args[i]);
}
}

goto -Anweisung

static void Main(string[] args) {


int i = 0;
goto check;
loop:
Console.WriteLine(args[i++]);
check:
if (i < args.Length) goto loop;
}

return -Anweisung
static int Add(int a, int b) {
return a + b;
}

static void Main() {


Console.WriteLine(Add(1, 2));
return;
}

yield -Anweisung

static IEnumerable<int> Range(int from, int to) {


for (int i = from; i < to; i++) {
yield return i;
}
yield break;
}

static void Main() {


foreach (int x in Range(-10,10)) {
Console.WriteLine(x);
}
}

throw und- try Anweisungen

static double Divide(double x, double y) {


if (y == 0) throw new DivideByZeroException();
return x / y;
}

static void Main(string[] args) {


try {
if (args.Length != 2) {
throw new Exception("Two numbers required");
}
double x = double.Parse(args[0]);
double y = double.Parse(args[1]);
Console.WriteLine(Divide(x, y));
}
catch (Exception e) {
Console.WriteLine(e.Message);
}
finally {
Console.WriteLine("Good bye!");
}
}

checked und- unchecked Anweisungen

static void Main() {


int i = int.MaxValue;
checked {
Console.WriteLine(i + 1); // Exception
}
unchecked {
Console.WriteLine(i + 1); // Overflow
}
}

lock -Anweisung
class Account
{
decimal balance;
public void Withdraw(decimal amount) {
lock (this) {
if (amount > balance) {
throw new Exception("Insufficient funds");
}
balance -= amount;
}
}
}

using -Anweisung

static void Main() {


using (TextWriter w = File.CreateText("test.txt")) {
w.WriteLine("Line one");
w.WriteLine("Line two");
w.WriteLine("Line three");
}
}

Klassen und Objekte


*Klassen _ sind die grundlegendsten der c#-Typen. Eine Klasse ist eine Datenstruktur, die einen Zustand (Felder)
und Aktionen (Methoden und andere Funktionsmember) in einer einzigen Einheit kombiniert. Eine-Klasse stellt
eine Definition für dynamisch erstellte Instanzen der-Klasse bereit, die auch als- Objekte bezeichnet werden.
Klassen unterstützen Vererbung und Polymorphie, Mechanismen, bei denen abgeleitete Klassen erweitert
werden können, und spezialisiert _ Basisklassen *.
Neue Klassen werden mithilfe von Klassendeklarationen erstellt. Eine Klassendeklaration beginnt mit einem
Header, der die Attribute und Modifizierer der Klasse, den Namen der Klasse, die Basisklasse (sofern vorhanden)
und die von der Klasse implementierten Schnittstellen angibt. Auf den Header folgt der Klassenkörper. Dieser
besteht aus einer Liste der Memberdeklarationen, die zwischen den Trennzeichen { und } eingefügt werden.
Nachfolgend sehen Sie eine Deklaration einer einfachen Klasse namens Point :

public class Point


{
public int x, y;

public Point(int x, int y) {


this.x = x;
this.y = y;
}
}

Instanzen von Klassen werden mit dem new -Operator erstellt. Dieser reserviert Speicher für eine neue Instanz,
ruft einen Konstruktor zum Initialisieren der Instanz auf und gibt einen Verweis auf die Instanz zurück. Mit den
folgenden Anweisungen werden zwei Point -Objekte erstellt und Verweise auf diese Objekte in zwei Variablen
gespeichert:

Point p1 = new Point(0, 0);


Point p2 = new Point(10, 20);
Der von einem Objekt belegte Arbeitsspeicher wird automatisch freigegeben, wenn das Objekt nicht mehr
verwendet wird. Es ist weder erforderlich noch möglich, die Zuweisung von Objekten in C# explizit aufzuheben.
Member
Die Member einer Klasse sind entweder *statische Member _ oder _ Instanzmember *. Statische Member
gehören zu Klassen, Instanzmember gehören zu Objekten (Instanzen von Klassen).
Die folgende Tabelle enthält eine Übersicht über die Arten von Membern, die eine Klasse enthalten kann.

M EM B ER B ESC H REIB UN G

Konstanten Konstante Werte, die der Klasse zugeordnet sind

Felder Variablen der Klasse

Methoden Berechnungen und Aktionen, die von der Klasse ausgeführt


werden

Eigenschaften Aktionen im Zusammenhang mit dem Lesen und Schreiben


von benannten Eigenschaften der Klasse

Indexer Aktionen im Zusammenhang mit dem Indizieren von


Instanzen der Klasse, z.B. einem Array

Ereignisse Benachrichtigungen, die von der Klasse generiert werden


können

Operatoren Operatoren für Konvertierungen und Ausdrücke, die von der


Klasse unterstützt werden

Konstruktoren Aktionen, die zum Initialisieren von Instanzen der Klasse


oder der Klasse selbst benötigt werden

Destruktoren Aktionen, die ausgeführt werden, bevor Instanzen der Klasse


dauerhaft verworfen werden

Typen Geschachtelte Typen, die von der Klasse deklariert werden

Zugriff
Jeder Member einer Klasse ist mit einem Zugriff verknüpft, der die Regionen des Programmtexts steuert, die auf
den Member zugreifen können. Es gibt fünf mögliche Formen des Zugriffs. Eine Zusammenfassung finden Sie in
der folgenden Tabelle:

B EDIEN UN GSH IL F EN B EDEUT UN G

public Der Zugriff ist nicht eingeschränkt.

protected Der Zugriff ist auf diese Klasse oder auf von dieser Klasse
abgeleitete Klassen beschränkt.

internal Der Zugriff ist auf dieses Programm beschränkt.

protected internal Der Zugriff ist auf dieses Programm oder auf von dieser
Klasse abgeleitete Klassen beschränkt.
B EDIEN UN GSH IL F EN B EDEUT UN G

private Der Zugriff ist auf diese Klasse beschränkt.

Typparameter
Eine Klassendefinition kann einen Satz an Typparametern angeben, indem eine Liste der Typparameternamen in
spitzen Klammern an den Klassennamen angehängt wird. Die Typparameter können dann im Körper der
Klassendeklarationen zum Definieren der Klassenmember verwendet werden. Im folgenden Beispiel lauten die
Typparameter von Pair``TFirst und TSecond :

public class Pair<TFirst,TSecond>


{
public TFirst First;
public TSecond Second;
}

Ein Klassentyp, der als Typparameter deklariert wird, wird als generischer Klassentyp bezeichnet. Struktur-,
Schnittstellen- und Delegattypen können auch generisch sein.
Wenn die generische Klasse verwendet wird, müssen für jeden der Typparameter Typargumente angegeben
werden:

Pair<int,string> pair = new Pair<int,string> { First = 1, Second = "two" };


int i = pair.First; // TFirst is int
string s = pair.Second; // TSecond is string

Ein generischer Typ mit Typargumenten, wie Pair<int,string> oben angegeben, wird als konstruierter Typ
bezeichnet.
Basisklassen
Eine Klassendeklaration kann eine Basisklasse angeben, indem ein Doppelpunkt und der Name der Basisklasse
an den Klassennamen und die Typparameter angehängt wird. Das Auslassen einer Basisklassenspezifikation ist
dasselbe wie eine Ableitung vom Typ object . Im folgenden Beispiel ist Point die Basisklasse von Point3D , und
die Basisklasse von Point ist object :

public class Point


{
public int x, y;

public Point(int x, int y) {


this.x = x;
this.y = y;
}
}

public class Point3D: Point


{
public int z;

public Point3D(int x, int y, int z): base(x, y) {


this.z = z;
}
}

Eine Klasse erbt die Member der zugehörigen Basisklasse. Vererbung bedeutet, dass eine Klasse implizit alle
Member der Basisklasse enthält, mit Ausnahme der Instanzkonstruktoren und statischen Konstruktoren sowie
der Dekonstruktoren der Basisklasse. Eine abgeleitete Klasse kann den geerbten Membern neue Member
hinzufügen, aber die Definition eines geerbten Members kann nicht entfernt werden. Im vorherigen Beispiel erbt
Point3D die Felder x und y von Point , und jede Point3D -Instanz enthält drei Felder: x , y und z .

Ein Klassentyp kann implizit in einen beliebigen zugehörigen Basisklassentyp konvertiert werden. Deshalb kann
eine Variable eines Klassentyps auf eine Instanz dieser Klasse oder auf eine Instanz einer beliebigen abgeleiteten
Klasse verweisen. Beispielsweise kann in den vorherigen Klassendeklarationen eine Variable vom Typ Point
entweder auf Point oder auf Point3D verweisen:

Point a = new Point(10, 20);


Point b = new Point3D(10, 20, 30);

Felder
Ein Feld ist eine Variable, die einer Klasse oder einer Instanz einer Klasse zugeordnet ist.
Ein mit dem- static Modifizierer deklarierter Feld definiert ein statisches Feld . Ein statisches Feld identifiziert
genau einen Speicherort. Unabhängig davon, wie viele Instanzen einer Klasse erstellt werden, gibt es nur eine
Kopie eines statischen Felds.
Ein Feld, das ohne den static Modifizierer deklariert wurde, definiert ein Instanzfeld . Jede Instanz einer
Klasse enthält eine separate Kopie aller Instanzfelder dieser Klasse.
Im folgenden Beispiel weist jede Instanz der Color -Klasse eine separate Kopie der Instanzfelder r , g und b
auf, aber es gibt nur eine Kopie der statischen Felder Black , White , Red , Green und Blue :

public class Color


{
public static readonly Color Black = new Color(0, 0, 0);
public static readonly Color White = new Color(255, 255, 255);
public static readonly Color Red = new Color(255, 0, 0);
public static readonly Color Green = new Color(0, 255, 0);
public static readonly Color Blue = new Color(0, 0, 255);
private byte r, g, b;

public Color(byte r, byte g, byte b) {


this.r = r;
this.g = g;
this.b = b;
}
}

Wie im vorherigen Beispiel gezeigt, können schreibgeschützte Felder mit einem readonly -Modifizierer
deklariert werden. Die Zuweisung zu einem readonly Feld kann nur als Teil der Deklaration des Felds oder in
einem Konstruktor in derselben Klasse erfolgen.
Methoden
Ein *method _ ist ein Member, der eine Berechnung oder eine Aktion implementiert, die von einem Objekt oder
einer Klasse ausgeführt werden kann. Auf statische Methoden wird über die-Klasse zugegriffen. _
Instanzmethoden* werden über Instanzen der-Klasse aufgerufen.
Methoden verfügen über eine (möglicherweise leere) Liste von *Parametern _, die Werte oder Variablen
Verweise darstellen, die an die-Methode übermittelt werden, und einen _ -Rückgabetyp *, der den Typ des von
der-Methode berechneten und zurückgegebenen Werts angibt. Der Rückgabetyp einer Methode ist, void Wenn
Sie keinen Wert zurückgibt.
Ebenso wie Typen können Methoden einen Satz an Typparametern aufweisen, für den beim Aufruf der Methode
Typargumente angegeben werden müssen. Im Gegensatz zu Typen können die Typargumente häufig aus den
Argumenten eines Methodenaufrufs abgeleitet werden und müssen nicht explizit angegeben werden.
Die Signatur einer Methode muss innerhalb der Klasse eindeutig sein, in der die Methode deklariert ist. Die
Signatur einer Methode besteht aus dem Namen der Methode, der Anzahl von Typparametern und der Anzahl,
den Modifizierern und den Typen der zugehörigen Parameter. Die Signatur einer Methode umfasst nicht den
Rückgabetyp.
Parameter
Parameter werden dazu verwendet, Werte oder Variablenverweise an Methoden zu übergeben. Die Parameter
einer Methode erhalten ihre tatsächlichen Werte über Argumente , die angegeben werden, wenn die Methode
aufgerufen wird. Es gibt vier Arten von Parametern: Wertparameter, Verweisparameter, Ausgabeparameter und
Parameterarrays.
Ein Wer tparameter wird zum Übergeben von Eingabeparametern verwendet. Ein Wertparameter entspricht
einer lokalen Variablen, die ihren Anfangswert von dem Argument erhält, das für den Parameter übergeben
wurde. Änderungen an einem Wertparameter wirken sich nicht auf das Argument aus, das für den Parameter
übergeben wurde.
Wertparameter können optional sein, indem ein Standardwert festgelegt wird, damit die zugehörigen
Argumente weggelassen werden können.
Ein Ver weisparameter wird sowohl für die Übergabe von Eingabe- als auch Ausgabeparametern verwendet.
Das für einen Verweisparameter übergebene Argument muss eine Variable sein, und während der Ausführung
der Methode repräsentiert der Verweisparameter denselben Speicherort wie die Argumentvariable. Ein
Verweisparameter wird mit dem ref -Modifizierer deklariert. Das folgende Beispiel veranschaulicht die
Verwendung des ref -Parameters.

using System;

class Test
{
static void Swap(ref int x, ref int y) {
int temp = x;
x = y;
y = temp;
}

static void Main() {


int i = 1, j = 2;
Swap(ref i, ref j);
Console.WriteLine("{0} {1}", i, j); // Outputs "2 1"
}
}

Ein Ausgabeparameter wird zum Übergeben von Ausgabeparametern verwendet. Ein Ausgabeparameter
ähnelt einem Verweisparameter, mit dem Unterschied, dass der Anfangswert des vom Aufrufer bereitgestellten
Arguments nicht von Bedeutung ist. Ein Ausgabeparameter wird mit dem out -Modifizierer deklariert. Das
folgende Beispiel veranschaulicht die Verwendung des out -Parameters.
using System;

class Test
{
static void Divide(int x, int y, out int result, out int remainder) {
result = x / y;
remainder = x % y;
}

static void Main() {


int res, rem;
Divide(10, 3, out res, out rem);
Console.WriteLine("{0} {1}", res, rem); // Outputs "3 1"
}
}

Ein Parameterarray ermöglicht es, eine variable Anzahl von Argumenten an eine Methode zu übergeben. Ein
Parameterarray wird mit dem params -Modifizierer deklariert. Nur der letzte Parameter einer Methode kann ein
Parameterarray sein, und es muss sich um ein eindimensionales Parameterarray handeln. Die Methoden Write
und WriteLine der Klasse System.Console sind gute Beispiele für die Nutzung eines Parameterarrays. Sie
werden folgendermaßen deklariert.

public class Console


{
public static void Write(string fmt, params object[] args) {...}
public static void WriteLine(string fmt, params object[] args) {...}
...
}

Innerhalb einer Methode mit einem Parameterarray verhält sich das Parameterarray wie ein regulärer Parameter
des Arraytyps. Beim Aufruf einer Methode mit einem Parameterarray ist es jedoch möglich, entweder ein
einzelnes Argument des Parameterarraytyps oder eine beliebige Anzahl von Argumenten des Elementtyps des
Parameterarrays zu übergeben. Im letzteren Fall wird automatisch eine Arrayinstanz erstellt und mit den
vorgegebenen Argumenten initialisiert. Dieses Beispiel:

Console.WriteLine("x={0} y={1} z={2}", x, y, z);

...entspricht dem folgenden Code:

string s = "x={0} y={1} z={2}";


object[] args = new object[3];
args[0] = x;
args[1] = y;
args[2] = z;
Console.WriteLine(s, args);

Methodenkörper und lokale Variablen


Der Text einer Methode gibt die-Anweisungen an, die beim Aufrufen der-Methode ausgeführt werden sollen.
Ein Methodenkörper kann Variablen deklarieren, die für den Aufruf der Methode spezifisch sind. Diese Variable
werden lokale Variablen genannt. Die Deklaration einer lokalen Variable gibt einen Typnamen, einen
Variablennamen und eventuell einen Anfangswert an. Im folgenden Beispiel wird eine lokale Variable i mit
einem Anfangswert von 0 und einer lokalen Variablen j ohne Anfangswert deklariert.
using System;

class Squares
{
static void Main() {
int i = 0;
int j;
while (i < 10) {
j = i * i;
Console.WriteLine("{0} x {0} = {1}", i, j);
i = i + 1;
}
}
}

In C# muss eine lokale Variable definitiv zugewiesen sein, bevor ihr Wert abgerufen werden kann. Wenn
beispielsweise die vorherige Deklaration von i keinen Anfangswert enthielte, würde der Compiler bei der
nachfolgenden Verwendung von i einen Fehler melden, weil i zu diesen Zeitpunkten im Programm nicht
definitiv zugewiesen wäre.
Eine Methode kann return -Anweisungen verwenden, um die Steuerung an den zugehörigen Aufrufer
zurückzugeben. In einer Methode, die void zurückgibt, können return -Anweisungen keinen Ausdruck
angeben. In einer Methode, die nicht-- void return Anweisungen zurückgibt, müssen-Anweisungen einen
Ausdruck enthalten, der den Rückgabewert berechnet.
Statische Methoden und Instanzmethoden
Eine Methode, die mit einem static -Modifizierer deklariert wird, ist eine statische Methode . Eine statische
Methode führt keine Vorgänge für eine spezifische Instanz aus und kann nur direkt auf statische Member
zugreifen.
Eine Methode, die ohne einen static -Modifizierer deklariert wird, ist eine Instanzmethode . Eine
Instanzmethode führt Vorgänge für eine spezifische Instanz aus und kann sowohl auf statische Member als auch
auf Instanzmember zugreifen. Auf die Instanz, für die eine Instanzmethode aufgerufen wurde, kann explizit als
this zugegriffen werden. Es ist ein Fehler, in einer statischen Methode auf this zu verweisen.

Die folgende Entity -Klasse umfasst sowohl statische Member als auch Instanzmember.

class Entity
{
static int nextSerialNo;
int serialNo;

public Entity() {
serialNo = nextSerialNo++;
}

public int GetSerialNo() {


return serialNo;
}

public static int GetNextSerialNo() {


return nextSerialNo;
}

public static void SetNextSerialNo(int value) {


nextSerialNo = value;
}
}

Jede Entity -Instanz enthält eine Seriennummer (und vermutlich weitere Informationen, die hier nicht
angezeigt werden). Der Entity -Konstruktor (der einer Instanzmethode ähnelt) initialisiert die neue Instanz mit
der nächsten verfügbaren Seriennummer. Da der Konstruktor ein Instanzmember ist, kann er sowohl auf das
serialNo -Instanzfeld als auch auf das statische nextSerialNo -Feld zugreifen.

Die statischen Methoden GetNextSerialNo und SetNextSerialNo können auf das statische Feld nextSerialNo
zugreifen, aber es wäre ein Fehler, über diese Methoden direkt auf das Instanzfeld serialNo zuzugreifen.
Im folgenden Beispiel wird die Verwendung der Entity -Klasse veranschaulicht.

using System;

class Test
{
static void Main() {
Entity.SetNextSerialNo(1000);
Entity e1 = new Entity();
Entity e2 = new Entity();
Console.WriteLine(e1.GetSerialNo()); // Outputs "1000"
Console.WriteLine(e2.GetSerialNo()); // Outputs "1001"
Console.WriteLine(Entity.GetNextSerialNo()); // Outputs "1002"
}
}

Beachten Sie, dass die statischen Methoden SetNextSerialNo und GetNextSerialNo für die Klasse aufgerufen
werden, während die GetSerialNo -Instanzmethode für Instanzen der Klasse aufgerufen wird.
Virtuelle, überschriebene und abstrakte Methoden
Wenn eine Instanzmethodendeklaration einen virtual Modifizierer enthält, wird die Methode als *vir tuelle
Methode _ bezeichnet. Wenn kein virtual Modifizierer vorhanden ist, wird die Methode als _ nicht virtuelle
Methode * bezeichnet.
Wenn eine virtuelle Methode aufgerufen wird, bestimmt der *Lauf Zeittyp _ der-Instanz, für die dieser Aufruf
erfolgt, die tatsächliche Methoden Implementierung, die aufgerufen werden soll. Bei einem nicht virtuellen
Methodenaufruf ist der _-Kompilier Zeittyp* der-Instanz der bestimmende Faktor.
Eine virtuelle Methode kann in einer abgeleiteten Klasse überschrieben werden. Wenn eine
Instanzmethodendeklaration einen override Modifizierer enthält, überschreibt die Methode eine geerbte
virtuelle Methode mit derselben Signatur. Während eine Deklaration einer virtuellen Methode eine neue
Methode einführt, spezialisiert eine Deklaration einer überschriebenen Methode eine vorhandene geerbte
virtuelle Methode, indem eine neue Implementierung dieser Methode bereitgestellt wird.
Eine abstrakte Methode ist eine virtuelle Methode ohne Implementierung. Eine abstrakte Methode wird mit
dem abstract -Modifizierer deklariert und ist nur in einer Klasse zulässig, die ebenfalls deklariert wird
abstract . Eine abstrakte Methode muss in jeder nicht abstrakten abgeleiteten Klasse überschrieben werden.

Im folgenden Beispiel wird die abstrakte Klasse Expression deklariert, die einen Ausdrucksbaumstrukturknoten
sowie drei abgeleitete Klassen repräsentiert: Constant , VariableReference und Operation . Diese
implementieren Ausdrucksbaumstrukturknoten für Konstanten, variable Verweise und arithmetische
Operationen. (Dies ist vergleichbar mit, sollte jedoch nicht mit den Ausdrucks Baum Typen verwechselt werden,
die in Ausdrucks Baumstruktur Typeneingeführt wurden).
using System;
using System.Collections;

public abstract class Expression


{
public abstract double Evaluate(Hashtable vars);
}

public class Constant: Expression


{
double value;

public Constant(double value) {


this.value = value;
}

public override double Evaluate(Hashtable vars) {


return value;
}
}

public class VariableReference: Expression


{
string name;

public VariableReference(string name) {


this.name = name;
}

public override double Evaluate(Hashtable vars) {


object value = vars[name];
if (value == null) {
throw new Exception("Unknown variable: " + name);
}
return Convert.ToDouble(value);
}
}

public class Operation: Expression


{
Expression left;
char op;
Expression right;

public Operation(Expression left, char op, Expression right) {


this.left = left;
this.op = op;
this.right = right;
}

public override double Evaluate(Hashtable vars) {


double x = left.Evaluate(vars);
double y = right.Evaluate(vars);
switch (op) {
case '+': return x + y;
case '-': return x - y;
case '*': return x * y;
case '/': return x / y;
}
throw new Exception("Unknown operator");
}
}

Die vorherigen vier Klassen können zum Modellieren arithmetischer Ausdrücke verwendet werden.
Beispielsweise kann mithilfe von Instanzen dieser Klassen der Ausdruck x + 3 folgendermaßen dargestellt
werden.
Expression e = new Operation(
new VariableReference("x"),
'+',
new Constant(3));

Die Evaluate -Methode einer Expression -Instanz wird aufgerufen, um den vorgegebenen Ausdruck
auszuwerten und einen double -Wert zu generieren. Die-Methode übernimmt als Argument Hashtable , das
Variablennamen (als Schlüssel der Einträge) und Werte (als Werte der Einträge) enthält. Die- Evaluate Methode
ist eine virtuelle abstrakte Methode. Dies bedeutet, dass nicht abstrakte abgeleitete Klassen Sie überschreiben
müssen, um eine tatsächliche Implementierung bereitzustellen.
Eine Implementierung von Constant für Evaluate gibt lediglich die gespeicherte Konstante zurück. Die
VariableReference -Implementierung von A sucht den Variablennamen in der Hash Tabelle und gibt den
resultierenden Wert zurück. Eine Implementierung von Operation wertet zunächst (durch einen rekursiven
Aufruf der zugehörigen Evaluate -Methoden) den linken und rechten Operanden aus und führt dann die
vorgegebene arithmetische Operation aus.
Das folgende Programm verwendet die Expression -Klassen zum Auswerten des Ausdrucks x * (y + 2) für
verschiedene Werte von x und y .

using System;
using System.Collections;

class Test
{
static void Main() {
Expression e = new Operation(
new VariableReference("x"),
'*',
new Operation(
new VariableReference("y"),
'+',
new Constant(2)
)
);
Hashtable vars = new Hashtable();
vars["x"] = 3;
vars["y"] = 5;
Console.WriteLine(e.Evaluate(vars)); // Outputs "21"
vars["x"] = 1.5;
vars["y"] = 9;
Console.WriteLine(e.Evaluate(vars)); // Outputs "16.5"
}
}

Methodenüberladung
Die Methode *Overload _ ermöglicht, dass mehrere Methoden in derselben Klasse denselben Namen haben,
solange Sie eindeutige Signaturen aufweisen. Beim Kompilieren eines Aufrufs einer überladenen Methode
verwendet der Compiler die _-*Überladungs Auflösung**, um die aufzurufende Methode zu bestimmen. Mithilfe
der Überladungsauflösung wird die Methode ermittelt, die den Argumenten am besten entspricht, bzw. es wird
ein Fehler ausgegeben, wenn keine passende Methode gefunden wird. Das folgende Beispiel zeigt die
Verwendung der Überladungsauflösung. Der Kommentar für jeden Aufruf in der Main -Methode zeigt, welche
Methode tatsächlich aufgerufen wird.
class Test
{
static void F() {
Console.WriteLine("F()");
}

static void F(object x) {


Console.WriteLine("F(object)");
}

static void F(int x) {


Console.WriteLine("F(int)");
}

static void F(double x) {


Console.WriteLine("F(double)");
}

static void F<T>(T x) {


Console.WriteLine("F<T>(T)");
}

static void F(double x, double y) {


Console.WriteLine("F(double, double)");
}

static void Main() {


F(); // Invokes F()
F(1); // Invokes F(int)
F(1.0); // Invokes F(double)
F("abc"); // Invokes F(object)
F((double)1); // Invokes F(double)
F((object)1); // Invokes F(object)
F<int>(1); // Invokes F<T>(T)
F(1, 1); // Invokes F(double, double)
}
}

Wie im Beispiel gezeigt, kann eine bestimmte Methode immer ausgewählt werden, indem die Argumente
explizit in die passenden Parametertypen konvertiert und/oder explizit Typargumente angegeben werden.
Andere Funktionsmember
Member, die ausführbaren Code enthalten, werden als Funktionsmember einer Klasse bezeichnet. In den
vorangegangenen Abschnitten wurden die Methoden beschrieben, die wichtigste Form der Funktionsmember.
In diesem Abschnitt werden die anderen Arten von Funktionsmembern beschrieben, die von c# unterstützt
werden: Konstruktoren, Eigenschaften, Indexer, Ereignisse, Operatoren und Dekonstruktoren.
Der folgende Code zeigt eine generische Klasse mit dem Namen List<T> , die eine wachsende-Objektliste
implementiert. Die Klasse enthält verschiedene Beispiele der gängigsten Arten von Funktionsmembern.

public class List<T> {


// Constant...
const int defaultCapacity = 4;

// Fields...
T[] items;
int count;

// Constructors...
public List(int capacity = defaultCapacity) {
items = new T[capacity];
}

// Properties...
// Properties...
public int Count {
get { return count; }
}
public int Capacity {
get {
return items.Length;
}
set {
if (value < count) value = count;
if (value != items.Length) {
T[] newItems = new T[value];
Array.Copy(items, 0, newItems, 0, count);
items = newItems;
}
}
}

// Indexer...
public T this[int index] {
get {
return items[index];
}
set {
items[index] = value;
OnChanged();
}
}

// Methods...
public void Add(T item) {
if (count == Capacity) Capacity = count * 2;
items[count] = item;
count++;
OnChanged();
}
protected virtual void OnChanged() {
if (Changed != null) Changed(this, EventArgs.Empty);
}
public override bool Equals(object other) {
return Equals(this, other as List<T>);
}
static bool Equals(List<T> a, List<T> b) {
if (a == null) return b == null;
if (b == null || a.count != b.count) return false;
for (int i = 0; i < a.count; i++) {
if (!object.Equals(a.items[i], b.items[i])) {
return false;
}
}
return true;
}

// Event...
public event EventHandler Changed;

// Operators...
public static bool operator ==(List<T> a, List<T> b) {
return Equals(a, b);
}
public static bool operator !=(List<T> a, List<T> b) {
return !Equals(a, b);
}
}

Konstruktoren
C# unterstützt sowohl Instanzkonstruktoren als auch statische Konstruktoren. Ein *Instanzkonstruktor _ ist ein
Member, der die erforderlichen Aktionen zum Initialisieren einer Instanz einer Klasse implementiert. Ein
statischer Konstruktor* ist ein Member, der die erforderlichen Aktionen zum Initialisieren einer Klasse selbst
beim ersten Laden implementiert.
Ein Konstruktor wird wie eine Methode ohne Rückgabetyp und mit demselben Namen wie die enthaltende
Klasse deklariert. Wenn eine Konstruktordeklaration einen static -Modifizierer enthält, deklariert diese einen
statischen Konstruktor. Andernfalls wird ein Instanzkonstruktor deklariert.
Instanzkonstruktoren können überladen werden. Beispielsweise deklariert die List<T> -Klasse zwei
Instanzkonstruktoren, einen ohne Parameter und einen weiteren mit einem int -Parameter.
Instanzkonstruktoren werden über den new -Operator aufgerufen. Die folgenden-Anweisungen weisen zwei-
Instanzen zu, die List<string> jeden der Konstruktoren der- List Klasse verwenden.

List<string> list1 = new List<string>();


List<string> list2 = new List<string>(10);

Im Gegensatz zu anderen Members werden Instanzkonstruktoren nicht geerbt, und eine Klasse weist keine
anderen Instanzkonstruktoren auf als diejenigen, die tatsächlich in der Klasse deklariert wurden. Wenn kein
Instanzkonstruktor für eine Klasse angegeben ist, wird automatisch ein leerer Instanzkonstruktor ohne
Parameter bereitgestellt.
Eigenschaften
*Proper ties _ sind eine natürliche Erweiterung von Feldern. Beide sind benannte Member mit zugeordneten
Typen, und für den Zugriff auf Felder und Eigenschaften wird dieselbe Syntax verwendet. Im Gegensatz zu
Feldern bezeichnen Eigenschaften jedoch keine Speicherorte. Stattdessen verfügen Eigenschaften über _
*Accessoren**, die die auszuführenden Anweisungen angeben, wenn ihre Werte gelesen oder geschrieben
werden.
Eine Eigenschaft wird wie ein Feld deklariert, mit der Ausnahme, dass die Deklaration mit einem get Accessor
und/oder einem Accessor endet, der set zwischen den Trennzeichen und nicht mit { } einem Semikolon
endet. Eine Eigenschaft, die sowohl einen get -Accessor als auch einen- set Accessor aufweist, ist eine Lese-
/Schreibeigenschaft _, eine Eigenschaft, die nur einen- get Accessor aufweist, ist eine schreibgeschützte
_Eigenschaft*, und eine Eigenschaft, die nur einen-Accessor aufweist, set ist eine _ *-schreibgeschützte
Eigenschaft_* *.
Ein- get Accessor entspricht einer Parameter losen Methode mit einem Rückgabewert des Eigenschaftentyps.
Wenn in einem Ausdruck auf eine Eigenschaft verwiesen wird, wird der- get Accessor der Eigenschaft
aufgerufen, um den Wert der-Eigenschaft zu berechnen, außer als Ziel einer Zuweisung.
Ein set -Accessor entspricht einer Methode mit einem einzelnen Parameter mit dem Namen value und
keinem Rückgabetyp. Wenn als Ziel einer Zuweisung oder als Operand von oder auf eine Eigenschaft verwiesen
wird ++ -- , set wird der Accessor mit einem Argument aufgerufen, das den neuen Wert bereitstellt.
Die List<T> -Klasse deklariert die beiden Eigenschaften „ Count “ und „ Capacity “, von denen die eine
schreibgeschützt ist und die andere Lese- und Schreibzugriff besitzt. Es folgt ein Beispiel zur Verwendung dieser
Eigenschaften.

List<string> names = new List<string>();


names.Capacity = 100; // Invokes set accessor
int i = names.Count; // Invokes get accessor
int j = names.Capacity; // Invokes get accessor

Ähnlich wie bei Feldern und Methoden unterstützt C# sowohl Instanzeigenschaften als auch statische
Eigenschaften. Statische Eigenschaften werden mit dem static -Modifizierer deklariert, und
Instanzeigenschaften werden ohne Sie deklariert.
Die Accessors einer Eigenschaft können virtuell sein. Wenn eine Eigenschaftendeklaration einen virtual -,
abstract - oder override -Modifizierer enthält, wird dieser auf den Accessor der Eigenschaft angewendet.

Indexer
Ein Indexer ist ein Member, mit dem Objekte wie ein Array indiziert werden können. Ein Indexer wird wie eine
Eigenschaft deklariert, abgesehen davon, dass der Name des Members this ist, gefolgt von einer
Parameterliste, die zwischen die Trennzeichen [ und ] geschrieben wird. Die Parameter stehen im Accessor
des Indexers zur Verfügung. Ähnlich wie Eigenschaften können Indexer Lese-/Schreibzugriff besitzen,
schreibgeschützt und lesegeschützt sein und virtuelle Accessors verwenden.
Die List -Klasse deklariert einen einzigen Indexer mit Lese-/Schreibzugriff, der einen int -Parameter
akzeptiert. Der Indexer ermöglicht es, Instanzen von List mit int -Werten zu indizieren. Beispiel:

List<string> names = new List<string>();


names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
for (int i = 0; i < names.Count; i++) {
string s = names[i];
names[i] = s.ToUpper();
}

Indexer können überladen werden, d.h. eine Klasse kann mehrere Indexer deklarieren, solange sich die Anzahl
oder Typen ihrer Parameter unterscheiden.
Events
Ein Ereignis ist ein Member, der es einer Klasse oder einem Objekt ermöglicht, Benachrichtigungen
bereitzustellen. Ein Ereignis wird wie ein Feld deklariert, abgesehen davon, dass es ein event -Schlüsselwort
enthält und einen Delegattyp aufweisen muss.
Innerhalb einer Klasse, die einen Ereignismember deklariert, verhält sich das Ereignis wie ein Feld des
Delegattyps (vorausgesetzt, das Ereignis ist nicht abstrakt und deklariert keine Accessors). Das Feld speichert
einen Verweis auf einen Delegaten, der die Ereignishandler repräsentiert, die dem Ereignis hinzugefügt wurden.
Wenn keine Ereignis Handles vorhanden sind, ist das Feld null .
Die List<T> -Klasse deklariert einen einzigen Ereignismember namens Changed , der angibt, dass der Liste ein
neues Element hinzugefügt wurde. Das- Changed Ereignis wird von der OnChanged virtuellen-Methode
ausgelöst, die zuerst überprüft, ob das Ereignis ist null (d. h., dass keine Handler vorhanden sind). Das
Auslösen eines Ereignisses entspricht exakt dem Aufrufen des Delegaten, der durch das Ereignis repräsentiert
wird, es gibt deshalb keine besonderen Sprachkonstrukte zum Auslösen von Ereignissen.
Clients reagieren über Ereignishandler auf Ereignisse. Ereignishandler werden unter Verwendung des += -
Operators angefügt und mit dem -= -Operator entfernt. Im folgenden Beispiel wird dem Changed -Ereignis von
List<string> ein Ereignishandler hinzugefügt.
using System;

class Test
{
static int changeCount;

static void ListChanged(object sender, EventArgs e) {


changeCount++;
}

static void Main() {


List<string> names = new List<string>();
names.Changed += new EventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(changeCount); // Outputs "3"
}
}

In komplexeren Szenarien, in denen die zugrunde liegende Speicherung eines Ereignisses gesteuert werden soll,
können in einer Ereignisdeklaration explizit die add - und remove -Accessors bereitgestellt werden. Diese ähneln
in gewisser Weise dem set -Accessor einer Eigenschaft.
Operatoren
Ein Operator ist ein Member, der die Bedeutung der Anwendung eines bestimmten Ausdrucksoperators auf
Instanzen einer Klasse definiert. Es können drei Arten von Operatoren definiert werden: unäre Operatoren,
binäre Operatoren und Konvertierungsoperatoren. Alle Operatoren müssen als public und static deklariert
werden.
Die List<T> -Klasse deklariert zwei Operatoren, operator== und operator!= , und verleiht so Ausdrücken, die
diese Operatoren auf List -Instanzen anwenden, eine neue Bedeutung. Insbesondere die Operatoren definieren
die Gleichheit für zwei Instanzen von List<T> , indem alle enthaltenen Objekte mithilfe ihrer Equals -Methoden
verglichen werden. Im folgenden Beispiel wird der == -Operator verwendet, um zwei Instanzen von List<int>
zu vergleichen.

using System;

class Test
{
static void Main() {
List<int> a = new List<int>();
a.Add(1);
a.Add(2);
List<int> b = new List<int>();
b.Add(1);
b.Add(2);
Console.WriteLine(a == b); // Outputs "True"
b.Add(3);
Console.WriteLine(a == b); // Outputs "False"
}
}

Die erste Methode Console.WriteLine gibt True aus, weil die zwei Listen dieselbe Anzahl von Objekten mit
denselben Werten in derselben Reihenfolge enthalten. Wenn List<T> nicht operator== definieren würde,
würde die Ausgabe der ersten Console.WriteLine -Methode False lauten, weil a und b auf unterschiedliche
List<int> -Instanzen verweisen.

Destruktoren
Ein destruktur tor ist ein Member, der die erforderlichen Aktionen zum Zerstörung einer Instanz einer Klasse
implementiert. Dekonstruktoren können keine Parameter haben, Sie können keine Zugriffsmodifizierer
aufweisen und können nicht explizit aufgerufen werden. Der Dekonstruktor für eine-Instanz wird automatisch
während Garbage Collection aufgerufen.
Der Garbage Collector ist bei der Entscheidung unterstützt, wann Objekte gesammelt und dedeerdektoren
ausgeführt werden sollen. Insbesondere ist die zeitliche Steuerung der dekonstruktoraufrufe nicht
deterministisch, und Dekonstruktoren können auf jedem beliebigen Thread ausgeführt werden. Aus diesen und
anderen Gründen sollten Klassen deserialisierungsgeräte nur implementieren, wenn keine anderen Lösungen
möglich sind.
Die using -Anweisung bietet einen besseren Ansatz für die Objektzerstörung.

Strukturen
Wie Klassen sind Strukturen Datenstrukturen, die Datenmember und Funktionsmember enthalten können,
aber im Gegensatz zu Klassen sind Strukturen Werttypen und erfordern keine Heapzuordnung. Eine Variable
eines Strukturtyps speichert die Daten der Struktur direkt, während eine Variable eines Klassentyps einen
Verweis auf ein dynamisch zugeordnetes Objekt speichert. Strukturtypen unterstützen keine benutzerdefinierte
Vererbung, und alle Strukturtypen erben implizit vom Typ object .
Strukturen sind besonders nützlich für kleine Datenstrukturen, die über Wertsemantik verfügen. Komplexe
Zahlen, Punkte in einem Koordinatensystem oder Schlüssel-Wert-Paare im Wörterbuch sind gute Beispiele für
Strukturen. Die Verwendung von Strukturen statt Klassen für kleine Datenstrukturen kann bei der Anzahl der
Speicherbelegungen, die eine Anwendung durchführt, einen großen Unterschied ausmachen. Das folgende
Programm erstellt und initialisiert z.B. ein Array aus 100 Punkten. Mit Point als implementierter Klasse werden
101 separate Objekte instanziiert – eines für das Array und jeweils eines für jedes der 100 Elemente.

class Point
{
public int x, y;

public Point(int x, int y) {


this.x = x;
this.y = y;
}
}

class Test
{
static void Main() {
Point[] points = new Point[100];
for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
}
}

Eine Alternative besteht darin, Point eine Struktur zu erstellen.

struct Point
{
public int x, y;

public Point(int x, int y) {


this.x = x;
this.y = y;
}
}

Jetzt wird nur ein Objekt instanziiert – für das Array – und die Point -Instanzen werden inline im Array
gespeichert.
Strukturkonstruktoren werden mit dem neuen Operator new aufgerufen, doch das bedeutet nicht, dass der
Arbeitsspeicher belegt wird. Statt ein Objekt dynamisch zuzuordnen und einen Verweis darauf zurückzugeben,
gibt ein Strukturkonstruktor einfach den Strukturwert selbst zurück (in der Regel in einen temporären
Speicherort auf dem Stapel), und dieser Wert wird dann nach Bedarf kopiert.
Mit Klassen können zwei Variablen auf das gleiche Objekt verweisen, und so können an einer Variablen
durchgeführte Vorgänge das Objekt beeinflussen, auf das die andere Variable verweist. Mit Strukturen besitzt
jede Variable eine eigene Kopie der Daten, und es ist nicht möglich, dass an einer Variablen durchgeführte
Vorgänge die andere beeinflussen. Beispielsweise ist die Ausgabe, die vom folgenden Code Fragment erzeugt
wird, davon abhängig, ob Point eine Klasse oder eine Struktur ist.

Point a = new Point(10, 10);


Point b = a;
a.x = 20;
Console.WriteLine(b.x);

Wenn Point eine Klasse ist, ist die Ausgabe, 20 weil a und auf b das gleiche Objekt verweisen. Wenn
Point eine Struktur ist, ist die Ausgabe, 10 da durch die Zuweisung von a b eine Kopie des Werts erstellt
wird und diese Kopie von der nachfolgenden Zuweisung zu nicht beeinträchtigt wird a.x .
Im vorherigen Beispiel werden zwei der Einschränkungen von Strukturen hervorgehoben. Erstens ist das
Kopieren einer gesamten Struktur in der Regel weniger effizient als das Kopieren eines Objektverweises, sodass
Zuweisung und Wertparameterübergabe mit Strukturen aufwändiger sein kann als mit Verweistypen. Zweitens
ist es mit Ausnahme der ref - und out -Parameter nicht möglich, Verweise auf Strukturen zu erstellen, was ihre
Verwendung in einer Reihe von Situationen ausschließt.

Arrays
Ein *Array _ ist eine Datenstruktur, die eine Reihe von Variablen enthält, auf die über berechnete Indizes
zugegriffen wird. Die Variablen, die in einem Array enthalten sind, auch als Elemente des Arrays bezeichnet, sind
vom selben Typ, und dieser Typ wird als _-Elementtyp* des Arrays bezeichnet.
Arraytypen sind Verweistypen, und die Deklaration einer Arrayvariablen reserviert Speicher für einen Verweis
auf eine Arrayinstanz. Tatsächliche Array Instanzen werden zur Laufzeit dynamisch mithilfe des-Operators
erstellt new . Der new -Vorgang legt die Länge der neuen Arrayinstanz fest, die dann für die Lebensdauer der
Instanz beibehalten wird. Die Indizes der Arrayelemente reichen von 0 bis Length - 1 . Der new -Operator
initialisiert die Elemente eines Arrays automatisch mit ihren Standardwerten. Dieser lautet z.B. für alle
numerischen Typen 0 und für alle Verweistypen null .
Im folgenden Beispiel wird ein Array aus int -Elementen erstellt. Anschließend wird das Array initialisiert und
die Inhalte des Arrays werden gedruckt.
using System;

class Test
{
static void Main() {
int[] a = new int[10];
for (int i = 0; i < a.Length; i++) {
a[i] = i * i;
}
for (int i = 0; i < a.Length; i++) {
Console.WriteLine("a[{0}] = {1}", i, a[i]);
}
}
}

In diesem Beispiel wird ein *eindimensionales Array _ erstellt und bearbeitet. C# unterstützt auch
mehrdimensionale Arrays. Die Anzahl der Dimensionen eines Arraytyps, auch bekannt als _ Rank* des
Arraytyps, ist 1 plus die Anzahl von Kommas, die zwischen den eckigen Klammern des Arraytyps geschrieben
wurde. Im folgenden Beispiel wird ein eindimensionales, ein zweidimensionales und ein dreidimensionales
Array zugeordnet.

int[] a1 = new int[10];


int[,] a2 = new int[10, 5];
int[,,] a3 = new int[10, 5, 2];

Das a1 -Array enthält 10 Elemente, das a2 -Array umfasst 50 (10 × 5) Elemente, und das a3 -Array enthält 100
(10 × 5 × 2) Elemente.
Ein Array kann einen beliebigen Elementtyp verwenden, einschließlich eines Arraytyps. Ein Array mit Elementen
eines Arraytyps wird auch als verzweigtes Array bezeichnet, weil die Länge der Elementarrays nicht identisch
sein muss. Im folgenden Beispiel wird ein Array aus int -Arrays zugewiesen:

int[][] a = new int[3][];


a[0] = new int[10];
a[1] = new int[5];
a[2] = new int[20];

In der ersten Zeile wird ein Array mit drei Elementen erstellt, das jeweils den Typ int[] und einen Anfangswert
von null aufweist. In den folgenden Zeilen werden die drei Elemente mit Verweisen auf einzelne
Arrayinstanzen unterschiedlicher Länge initialisiert.
Der new -Operator erlaubt es, die Anfangswerte der Arrayelemente unter Verwendung eines
Arrayinitialisierers anzugeben, bei dem es sich um eine Liste von Ausdrücken zwischen den Trennzeichen {
und } handelt. Mit dem folgenden Beispiel wird ein int[] mit drei Elementen zugewiesen und initialisiert.

int[] a = new int[] {1, 2, 3};

Beachten Sie, dass die Länge des Arrays von der Anzahl von Ausdrücken zwischen und abgeleitet { wird } .
Deklarationen lokaler Variablen und Felder können weiter verkürzt werden, sodass der Arraytyp nicht erneut
aufgeführt werden muss.

int[] a = {1, 2, 3};

Die zwei vorherigen Beispiele entsprechen dem folgenden:


int[] t = new int[3];
t[0] = 1;
t[1] = 2;
t[2] = 3;
int[] a = t;

Schnittstellen
Eine Schnittstelle definiert einen Vertrag, der von Klassen und Strukturen implementiert werden kann. Eine
Schnittstelle kann Methoden, Eigenschaften, Ereignisse und Indexer enthalten. Eine Schnittstelle stellt keine
Implementierungen der von ihr definierten Member bereit. Sie gibt lediglich die Member an, die von Klassen
oder Strukturen bereitgestellt werden müssen, die die Schnittstelle implementieren.
Schnittstellen können Mehrfachvererbung einsetzen. Im folgenden Beispiel erbt die Schnittstelle IComboBox
sowohl von ITextBox als auch IListBox .

interface IControl
{
void Paint();
}

interface ITextBox: IControl


{
void SetText(string text);
}

interface IListBox: IControl


{
void SetItems(string[] items);
}

interface IComboBox: ITextBox, IListBox {}

Klassen und Strukturen können mehrere Schnittstellen implementieren. Im folgenden Beispiel implementiert die
Klasse EditBox sowohl IControl als auch IDataBound .

interface IDataBound
{
void Bind(Binder b);
}

public class EditBox: IControl, IDataBound


{
public void Paint() {...}
public void Bind(Binder b) {...}
}

Wenn eine Klasse oder Struktur eine bestimmte Schnittstelle implementiert, können Instanzen dieser Klasse
oder Struktur implizit in diesen Schnittstellentyp konvertiert werden. Beispiel:

EditBox editBox = new EditBox();


IControl control = editBox;
IDataBound dataBound = editBox;

In Fällen, in denen nicht bekannt ist, dass eine Instanz eine bestimmte Schnittstelle implementiert, können
dynamische Typumwandlungen verwendet werden. Die folgenden-Anweisungen verwenden z. b. dynamische
Typumwandlungen, um die- IControl und-Schnittstellen Implementierungen eines Objekts abzurufen
IDataBound . Da der tatsächliche Objekttyp ist EditBox , sind die Umwandlungen erfolgreich.

object obj = new EditBox();


IControl control = (IControl)obj;
IDataBound dataBound = (IDataBound)obj;

In der vorherigen EditBox Klasse werden die Paint -Methode aus der IControl -Schnittstelle und die- Bind
Methode aus der- IDataBound Schnittstelle mithilfe public von Membern implementiert. C# unterstützt auch
explizite Implementierungen von Schnittstellenmembern, wobei die Klasse oder Struktur das Erstellen der
Member vermeiden kann public . Eine explizite Implementierung eines Schnittstellenmembers wird mit dem
vollqualifizierten Namen des Schnittstellenmembers geschrieben. Die EditBox -Klasse könnte z.B. die
IControl.Paint - und IDataBound.Bind -Methode wie folgt über explizite Implementierungen eines
Schnittstellenmembers implementieren.

public class EditBox: IControl, IDataBound


{
void IControl.Paint() {...}
void IDataBound.Bind(Binder b) {...}
}

Der Zugriff auf explizite Schnittstellenmember kann nur über den Schnittstellentyp erfolgen. Beispielsweise kann
die Implementierung von IControl.Paint , die von der vorherigen-Klasse bereitgestellt EditBox wird, nur
aufgerufen werden, indem zuerst der EditBox Verweis in den IControl Schnittstellentyp umgerechnet wird.

EditBox editBox = new EditBox();


editBox.Paint(); // Error, no such method
IControl control = editBox;
control.Paint(); // Ok

Enumerationen
Ein Enumerationstyp ist ein eindeutiger Werttyp mit einem Satz benannter Konstanten. Im folgenden Beispiel
wird ein Enumerationstyp mit dem Namen Color mit den drei Konstanten Werten,, und deklariert und
verwendet Red Green Blue .
using System;

enum Color
{
Red,
Green,
Blue
}

class Test
{
static void PrintColor(Color color) {
switch (color) {
case Color.Red:
Console.WriteLine("Red");
break;
case Color.Green:
Console.WriteLine("Green");
break;
case Color.Blue:
Console.WriteLine("Blue");
break;
default:
Console.WriteLine("Unknown color");
break;
}
}

static void Main() {


Color c = Color.Red;
PrintColor(c);
PrintColor(Color.Blue);
}
}

Jeder Aufzählungstyp verfügt über einen entsprechenden ganzzahligen Typ, der als zugrunde liegenden Typ
des Aufzählungs Typs bezeichnet wird. Ein Aufzählungs Typ, der den zugrunde liegenden Typ nicht explizit
deklariert, verfügt über einen zugrunde liegenden Typ von int . Das Speicherformat eines Enumerationstyps
und der Bereich möglicher Werte werden durch den zugrunde liegenden Typ bestimmt. Der Satz von Werten,
der von einem Enumerationstyp übernommen werden kann, wird nicht durch seine Enumerationsmember
eingeschränkt. Insbesondere kann jeder Wert des zugrunde liegenden Typs einer Enumeration in den
Enumerationstyp umgewandelt werden und ist ein eindeutiger gültiger Wert dieses Enumerationstyps.
Im folgenden Beispiel wird ein Aufzählungstyp Alignment mit dem Namen mit dem zugrunde liegenden Typ
deklariert sbyte .

enum Alignment: sbyte


{
Left = -1,
Center = 0,
Right = 1
}

Wie im vorherigen Beispiel gezeigt, kann eine Enumerationsmember-Deklaration einen konstanten Ausdruck
enthalten, der den Wert des Members angibt. Der Konstante Wert für jedes Enumerationsmember muss im
Bereich des zugrunde liegenden Typs der Enumeration liegen. Wenn eine Enumerationsmember-Deklaration
nicht explizit einen Wert angibt, erhält der Member den Wert 0 (null), wenn es sich um den ersten Member im
Enumerationstyp handelt, oder den Wert des texthalen vorangehenden Enumerationsmembers plus eins.
Enumerationswerte können mithilfe von Typumwandlungen in ganzzahlige Werte und umgekehrt konvertiert
werden. Beispiel:
int i = (int)Color.Blue; // int i = 2;
Color c = (Color)2; // Color c = Color.Blue;

Der Standardwert eines beliebigen Enumerationstyps ist der ganzzahlige Wert NULL, der in den
Enumerationstyp konvertiert wird. In Fällen, in denen Variablen automatisch mit einem Standardwert initialisiert
werden, ist dies der Wert, der Variablen von Enumerationstypen zugewiesen wird. Damit der Standardwert eines
Enumerationstyps leicht verfügbar ist, wird das Literale 0 implizit in einen beliebigen Enumerationstyp
konvertiert. Daher ist Folgendes zugelassen.

Color c = 0;

Delegaten
Ein Delegattyp stellt Verweise auf Methoden mit einer bestimmten Parameterliste und dem Rückgabetyp dar.
Delegate ermöglichen die Behandlung von Methoden als Entitäten, die Variablen zugewiesen und als Parameter
übergeben werden können. Delegate ähneln dem Konzept von Funktionszeigern, die Sie in einigen anderen
Sprachen finden. Im Gegensatz zu Funktionszeigern sind Delegate allerdings objektorientiert und typsicher.
Im folgenden Beispiel wird ein Delegattyp namens Function deklariert und verwendet.

using System;

delegate double Function(double x);

class Multiplier
{
double factor;

public Multiplier(double factor) {


this.factor = factor;
}

public double Multiply(double x) {


return x * factor;
}
}

class Test
{
static double Square(double x) {
return x * x;
}

static double[] Apply(double[] a, Function f) {


double[] result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}

static void Main() {


double[] a = {0.0, 0.5, 1.0};
double[] squares = Apply(a, Square);
double[] sines = Apply(a, Math.Sin);
Multiplier m = new Multiplier(2.0);
double[] doubles = Apply(a, m.Multiply);
}
}
Eine Instanz des Delegattyps Function kann auf jede Methode verweisen, die ein double -Argument und einen
double -Wert akzeptiert. Die Apply -Methode wendet eine jeweilige Function (Funktion) auf die Elemente einer
double[] -Methode an. Mit den Ergebnissen wird eine double[] -Methode zurückgegeben. In der Main -
Methode wird Apply verwendet, um drei verschiedene Funktionen auf ein double[] anzuwenden.
Ein Delegat kann entweder auf eine statische Methode verweisen (z.B. Square oder Math.Sin im vorherigen
Beispiel) oder eine Instanzmethode (z.B. m.Multiply im vorherigen Beispiel). Ein Delegat, der auf eine
Instanzmethode verweist, verweist auch auf ein bestimmtes Objekt, und wenn die Instanzmethode durch den
Delegaten aufgerufen wird, wird das Objekt this im Aufruf.
Delegaten können auch mit anonymen Funktionen erstellt werden, die dynamisch erstellte „Inlinemethoden“
sind. Anonyme Funktionen können die lokalen Variablen der umgebenden Methoden sehen. Daher kann das
obige Multiplikator-Beispiel einfacher geschrieben werden, ohne eine Klasse zu verwenden Multiplier :

double[] doubles = Apply(a, (double x) => x * 2.0);

Eine interessante und nützliche Eigenschaft eines Delegaten ist, dass er die Klasse der Methode, auf die er
verweist, nicht kennt oder sie ignoriert; wichtig ist nur, dass die referenzierte Methode die gleichen Parameter
und den gleichen Rückgabetyp hat wie der Delegat.

Attributes
Typen, Member und andere Entitäten in einem C#-Programm unterstützen Modifizierer, die bestimmte Aspekte
ihres Verhaltens steuern. Der Zugriff auf eine Methode wird beispielsweise mithilfe der Modifizierer public ,
protected , internal und private kontrolliert. C# generalisiert diese Funktionalität, indem benutzerdefinierte
Typen deklarativer Informationen an eine Programmentität angefügt und zur Laufzeit abgerufen werden
können. Programme geben diese zusätzlichen deklarativen Informationen an, indem Sie Attribute definieren
und verwenden.
Im folgenden Beispiel wird ein HelpAttribute -Attribut deklariert, dass in Programmentitäten platziert werden
kann, um Links zur zugehörigen Dokumentation bereitzustellen.

using System;

public class HelpAttribute: Attribute


{
string url;
string topic;

public HelpAttribute(string url) {


this.url = url;
}

public string Url {


get { return url; }
}

public string Topic {


get { return topic; }
set { topic = value; }
}
}

Alle Attribut Klassen werden von der System.Attribute Basisklasse abgeleitet, die von der .NET Framework
bereitgestellt wird. Attribute können durch Angabe ihres Namens angewendet werden, zusammen mit
beliebigen Argumenten. Diese müssen in eckigen Klammern genau vor der zugehörigen Deklaration eingefügt
werden. Wenn der Name eines Attributs auf endet Attribute , kann dieser Teil des Namens ausgelassen
werden, wenn auf das Attribut verwiesen wird. Beispielsweise kann das HelpAttribute -Attribut wie folgt
verwendet werden.

[Help("http://msdn.microsoft.com/.../MyClass.htm")]
public class Widget
{
[Help("http://msdn.microsoft.com/.../MyClass.htm", Topic = "Display")]
public void Display(string text) {}
}

In diesem Beispiel wird ein HelpAttribute an die Widget -Klasse und ein anderes HelpAttribute an die-
Display Methode in der-Klasse angefügt. Die öffentlichen Konstruktoren einer Attributklasse steuern die
Informationen, die beim Anfügen des Attributs an eine Programmentität angegeben werden müssen.
Zusätzliche Informationen können angegeben werden, indem auf öffentliche Eigenschaften mit Lese-
/Schreibzugriff der Attributklasse verwiesen wird (z.B. wie der obige Verweis auf die Topic -Eigenschaft).
Im folgenden Beispiel wird gezeigt, wie Attributinformationen für eine bestimmte Programm Entität zur Laufzeit
mithilfe von Reflektion abgerufen werden können.

using System;
using System.Reflection;

class Test
{
static void ShowHelp(MemberInfo member) {
HelpAttribute a = Attribute.GetCustomAttribute(member,
typeof(HelpAttribute)) as HelpAttribute;
if (a == null) {
Console.WriteLine("No help for {0}", member);
}
else {
Console.WriteLine("Help for {0}:", member);
Console.WriteLine(" Url={0}, Topic={1}", a.Url, a.Topic);
}
}

static void Main() {


ShowHelp(typeof(Widget));
ShowHelp(typeof(Widget).GetMethod("Display"));
}
}

Wenn per Reflektion ein bestimmtes Attribut angefordert wird, wird der Konstruktor für die Attributklasse mit
den in der Programmquelle angegebenen Informationen aufgerufen, und die resultierende Attributinstanz wird
zurückgegeben. Wenn zusätzliche Informationen über Eigenschaften bereitgestellt wurden, werden diese
Eigenschaften auf die vorgegebenen Werte festgelegt, bevor die Attributinstanz zurückgegeben wird.
Lexikalische Struktur
04.11.2021 • 58 minutes to read

Programme
Ein c# Programm _ besteht aus mindestens einer _Quelldatei_, die formal als _ Kompilierungs Einheiten
(Kompilierungs Einheiten) bekannt ist. Eine Quelldatei ist eine geordnete Sequenz von Unicode-Zeichen.
Quelldateien verfügen in der Regel über eine eins-zu-eins-Entsprechung zu Dateien in einem Dateisystem, diese
Entsprechung ist jedoch nicht erforderlich. Zur maximalen Portabilität wird empfohlen, dass Dateien in einem
Dateisystem mit der UTF-8-Codierung codiert werden.
Konzeptionell wird ein Programm mithilfe von drei Schritten kompiliert:
1. Transformation, die eine Datei von einem bestimmten Zeichen-und Codierungsschema in eine Unicode-
Zeichenfolge konvertiert.
2. Lexikalische Analyse, die einen Stream von Unicode-Eingabezeichen in einen Datenstrom von Token
übersetzt.
3. Syntaktische Analyse, bei der der Datenstrom von Token in ausführbaren Code übersetzt wird.

Grammars (Grammatik)
Diese Spezifikation zeigt die Syntax der c#-Programmiersprache mit zwei Grammatiken. Die lexikalische
Grammatik _ (lexikalischeGrammatik) definiert, wie Unicode-Zeichen kombiniert werden, um Zeilen Abschluss
Zeichen, Leerzeichen, Kommentare, Token und Vorverarbeitungs Direktiven zu bilden. Die syntaktische
Grammatik * (syntaktische Grammatik) definiert, wie die aus der lexikalischen Grammatik resultierenden Token
kombiniert werden, um c#-Programme zu bilden.
Grammatik Notation
Die lexikalischen und syntaktischen Grammatiken werden in Backus-Naur Formular mithilfe der Notation des
antlr-Grammatik Tools dargestellt.
Lexikalische Grammatik
Die lexikalische Grammatik von c# wird in lexikalischen Analysen, Tokenund Vorverarbeitungs
Direktivendargestellt. Die Terminal Symbole der lexikalischen Grammatik sind die Zeichen des Unicode-
Zeichensatzes, und die lexikalische Grammatik gibt an, wie Zeichen kombiniert werden, um Token (Token),
Leerzeichen (Leerraum), Kommentare (Kommentare) und Vorverarbeitungs Direktiven (Vorverarbeitungs
Direktiven) zu bilden.
Jede Quelldatei in einem c#-Programm muss der Eingabe Produktion der lexikalischen Grammatik (lexikalische
Analyse) entsprechen.
Syntaktische Grammatik
Die syntaktische Grammatik von c# wird in den Kapiteln und Anhänge dargestellt, die diesem Kapitel folgen. Die
Terminal Symbole der syntaktischen Grammatik sind die von der lexikalischen Grammatik definierten Token,
und die syntaktische Grammatik gibt an, wie Token kombiniert werden, um c#-Programme zu bilden.
Jede Quelldatei in einem c#-Programm muss der compilation_unit Produktion der syntaktischen Grammatik
(Kompilierungs Einheiten) entsprechen.

Lexikalische Analyse
Die Eingabe Produktion definiert die lexikalische Struktur einer c#-Quelldatei. Jede Quelldatei in einem c#-
Programm muss mit dieser lexikalischen Grammatik Produktion übereinstimmen.

input
: input_section?
;

input_section
: input_section_part+
;

input_section_part
: input_element* new_line
| pp_directive
;

input_element
: whitespace
| comment
| token
;

Fünf grundlegende Elemente bilden die lexikalische Struktur einer c#-Quelldatei: Zeilen Abschluss Zeichen
(ZeilenAbschluss Zeichen), Leerraum (Leerraum), Kommentare (Kommentare), Token (Token) und
Vorverarbeitungs Direktiven (Vorverarbeitungs Direktiven). Von diesen grundlegenden Elementen sind nur
Token in der syntaktischen Grammatik eines c#-Programms (syntaktische Grammatik) von Bedeutung.
Die lexikalische Verarbeitung einer c#-Quelldatei besteht darin, dass die Datei in eine Sequenz von Token
reduziert wird, die zur Eingabe für die syntaktische Analyse wird. Zeilen Abschluss Zeichen, Leerzeichen und
Kommentare können für separate Token dienen, und Vorverarbeitungs Direktiven können dazu führen, dass
Abschnitte der Quelldatei übersprungen werden. andernfalls haben diese lexikalischen Elemente keinerlei
Auswirkung auf die syntaktische Struktur eines c#-Programms.
Bei interpoliert-Zeichen folgen Literalen (interpoliert Zeichen folgen Literale) wird zunächst ein einzelnes Token
von der lexikalischen Analyse erstellt, aber in mehrere Eingabeelemente unterteilt, die wiederholt der
lexikalischen Analyse unterzogen werden, bis alle interpoliert-Zeichen folgen Literale aufgelöst wurden. Die
resultierenden Token dienen dann als Eingabe für die syntaktische Analyse.
Wenn mehrere lexikalische Grammatik Produktionen einer Sequenz von Zeichen in einer Quelldatei
entsprechen, bildet die lexikalische Verarbeitung immer das längstmögliche lexikalische Element. Beispielsweise
wird die Zeichenfolge // als Anfang eines einzeiligen Kommentars verarbeitet, da dieses lexikalische Element
länger als ein einzelnes Token ist / .
Zeilen Abschluss Zeichen
Zeilen Abschluss Zeichen teilen die Zeichen einer c#-Quelldatei in Zeilen auf.

new_line
: '<Carriage return character (U+000D)>'
| '<Line feed character (U+000A)>'
| '<Carriage return character (U+000D) followed by line feed character (U+000A)>'
| '<Next line character (U+0085)>'
| '<Line separator character (U+2028)>'
| '<Paragraph separator character (U+2029)>'
;

Aus Gründen der Kompatibilität mit Tools zur Quell Code Bearbeitung, die Dateiendemarker hinzufügen und
eine Quelldatei als Sequenz von ordnungsgemäß beendeten Zeilen angezeigt werden können, werden die
folgenden Transformationen in der entsprechenden Reihenfolge auf jede Quelldatei in einem c#-Programm
angewendet:
Wenn das letzte Zeichen der Quelldatei ein Control-Z-Zeichen ( U+001A ) ist, wird dieses Zeichen gelöscht.
Ein Wagen Rücklauf Zeichen ( U+000D ) wird am Ende der Quelldatei hinzugefügt, wenn diese Quelldatei
nicht leer ist, und wenn das letzte Zeichen der Quelldatei kein Wagen Rücklauf Zeichen ( U+000D ), ein
Zeilenvorschub ( U+000A ), ein Zeilen Trennzeichen ( U+2028 ) oder ein Absatz Trennzeichen ( U+2029 ) ist.
Kommentare
Es werden zwei Arten von Kommentaren unterstützt: einzeilige Kommentare und durch Trennzeichen getrennte
Kommentare.\ Einzeilige Kommentare _ beginnen mit den Zeichen // und erweitern bis zum Ende der
Quellzeile. Durch Trennzeichen getrennte _Kommentare_ beginnen mit den Zeichen /_ und enden mit den
Zeichen */ . Durch Trennzeichen getrennte Kommentare können mehrere Zeilen umfassen.

comment
: single_line_comment
| delimited_comment
;

single_line_comment
: '//' input_character*
;

input_character
: '<Any Unicode character except a new_line_character>'
;

new_line_character
: '<Carriage return character (U+000D)>'
| '<Line feed character (U+000A)>'
| '<Next line character (U+0085)>'
| '<Line separator character (U+2028)>'
| '<Paragraph separator character (U+2029)>'
;

delimited_comment
: '/*' delimited_comment_section* asterisk+ '/'
;

delimited_comment_section
: '/'
| asterisk* not_slash_or_asterisk
;

asterisk
: '*'
;

not_slash_or_asterisk
: '<Any Unicode character except / or *>'
;

Kommentare werden nicht geschachtelt. Die Zeichen folgen /* und */ haben keine besondere Bedeutung
innerhalb eines // Kommentars, und die Zeichen folgen // und /* haben keine besondere Bedeutung in
einem durch Trennzeichen getrennten Kommentar.
Kommentare werden nicht innerhalb von Zeichen-und Zeichen folgen literalen verarbeitet.
Das Beispiel
/* Hello, world program
This program writes "hello, world" to the console
*/
class Hello
{
static void Main() {
System.Console.WriteLine("hello, world");
}
}

enthält einen durch Trennzeichen getrennten Kommentar.


Das Beispiel

// Hello, world program


// This program writes "hello, world" to the console
//
class Hello // any name will do for this class
{
static void Main() { // this method must be named "Main"
System.Console.WriteLine("hello, world");
}
}

enthält mehrere einzeilige Kommentare.


Leerzeichen
Leerraum ist als beliebiges Zeichen mit Unicode-Klassen-ZS (einschließlich Leerzeichen) sowie dem horizontalen
Tabstopp Zeichen, dem vertikalen Tabstopp Zeichen und dem Formular Vorschub Zeichen definiert.

whitespace
: '<Any character with Unicode class Zs>'
| '<Horizontal tab character (U+0009)>'
| '<Vertical tab character (U+000B)>'
| '<Form feed character (U+000C)>'
;

Token
Es gibt mehrere Arten von Token: Bezeichner, Schlüsselwörter, Literale, Operatoren und Satzzeichen. Leerzeichen
und Kommentare sind keine Token, obwohl Sie als Trennzeichen für Token fungieren.

token
: identifier
| keyword
| integer_literal
| real_literal
| character_literal
| string_literal
| interpolated_string_literal
| operator_or_punctuator
;

Unicode -Escapesequenzen
Eine Unicode-Escapesequenz stellt ein Unicode-Zeichen dar. Escapesequenzen von Unicode-Zeichen werden in
bezeichern (bezeichern), Zeichen Literalen (Zeichen literalen) und regulären Zeichen folgen Literalen (Zeichen
folgen Literale) verarbeitet. Eine Unicode-Escapesequenz wird an keinem anderen Speicherort verarbeitet (z. b.
zum bilden eines Operators, eines interpunterators oder eines Schlüssel Worts).

unicode_escape_sequence
: '\\u' hex_digit hex_digit hex_digit hex_digit
| '\\U' hex_digit hex_digit hex_digit hex_digit hex_digit hex_digit hex_digit hex_digit
;

Eine Unicode-Escapesequenz stellt das einzelne Unicode-Zeichen dar, das durch die hexadezimal Zahl nach den
\u Zeichen "" oder "" gebildet wird \U . Da in c# eine 16-Bit-Codierung von Unicode-Code Punkten in Zeichen
und Zeichen folgen Werten verwendet wird, ist ein Unicode-Zeichen im Bereich u + 10000 bis U + 10FFFF in
einem Zeichenliterals nicht zulässig und wird mit einem Unicode-Ersatz Zeichenpaar in einem
Zeichenfolgenliteralzeichen dargestellt. Unicode-Zeichen mit Code Punkten oberhalb von 0x10FFFF werden
nicht unterstützt.
Es werden nicht mehrere Übersetzungen ausgeführt. Beispielsweise entspricht das Zeichen folgen Literalzeichen
"" \u005Cu005C \u005C anstelle von "" \ . Der Unicode-Wert \u005C ist das Zeichen " \ ".
Das Beispiel

class Class1
{
static void Test(bool \u0066) {
char c = '\u0066';
if (\u0066)
System.Console.WriteLine(c.ToString());
}
}

zeigt verschiedene Verwendungen von \u0066 , d. h. die Escapesequenz für den Buchstaben " f ". Das
Programm entspricht

class Class1
{
static void Test(bool f) {
char c = 'f';
if (f)
System.Console.WriteLine(c.ToString());
}
}

Bezeichner
Die Regeln für Bezeichner, die in diesem Abschnitt angegeben sind, entsprechen genau den Regeln, die vom
Unicode-Standard Anhang 31 empfohlen werden, mit dem Unterschied, dass Unterstriche als Ausgangs Zeichen
zulässig sind (wie es in der C-Programmiersprache üblich ist), Unicode-Escapesequenzen sind in bezeichlen
zulässig, und das @ Zeichen "" ist als Präfix zulässig
identifier
: available_identifier
| '@' identifier_or_keyword
;

available_identifier
: '<An identifier_or_keyword that is not a keyword>'
;

identifier_or_keyword
: identifier_start_character identifier_part_character*
;

identifier_start_character
: letter_character
| '_'
;

identifier_part_character
: letter_character
| decimal_digit_character
| connecting_character
| combining_character
| formatting_character
;

letter_character
: '<A Unicode character of classes Lu, Ll, Lt, Lm, Lo, or Nl>'
| '<A unicode_escape_sequence representing a character of classes Lu, Ll, Lt, Lm, Lo, or Nl>'
;

combining_character
: '<A Unicode character of classes Mn or Mc>'
| '<A unicode_escape_sequence representing a character of classes Mn or Mc>'
;

decimal_digit_character
: '<A Unicode character of the class Nd>'
| '<A unicode_escape_sequence representing a character of the class Nd>'
;

connecting_character
: '<A Unicode character of the class Pc>'
| '<A unicode_escape_sequence representing a character of the class Pc>'
;

formatting_character
: '<A Unicode character of the class Cf>'
| '<A unicode_escape_sequence representing a character of the class Cf>'
;

Informationen zu den oben erwähnten Unicode-Zeichenklassen finden Sie im Unicode-Standard, Version 3,0,
Abschnitt 4,5.
Beispiele für gültige Bezeichner sind " identifier1 ", " _identifier2 " und " @if ".
Ein Bezeichner in einem übereinstimmenden Programm muss das kanonische Format aufweisen, das durch die
Unicode-normalisierungs Form C definiert ist, wie im Unicode-Standard Anhang 15 definiert Das Verhalten
beim Auffinden eines Bezeichners, der nicht in der normalisierungs Form C vorliegt, ist Implementierungs
definiert. eine Diagnose ist jedoch nicht erforderlich.
Das-Präfix " @ " ermöglicht die Verwendung von Schlüsselwörtern als Bezeichner, was bei der Schnittstellen mit
anderen Programmiersprachen nützlich ist. Das Zeichen @ ist nicht Teil des Bezeichners, daher kann der
Bezeichner in anderen Sprachen als normaler Bezeichner ohne das Präfix angezeigt werden. Ein Bezeichner mit
einem @ Präfix wird als ausführlicher Bezeichner bezeichnet. Die Verwendung des @ Präfix für Bezeichner,
bei denen es sich nicht um Schlüsselwörter handelt, ist zulässig, aber es wird dringend davon abgeraten.
Beispiel:

class @class
{
public static void @static(bool @bool) {
if (@bool)
System.Console.WriteLine("true");
else
System.Console.WriteLine("false");
}
}

class Class1
{
static void M() {
cl\u0061ss.st\u0061tic(true);
}
}

definiert eine Klasse mit dem Namen " class " mit einer statischen Methode mit dem Namen "" static , die
einen Parameter mit dem Namen " bool " annimmt. Beachten Sie, dass das Token "" ein Bezeichner ist, da
Unicode-Escapezeichen in Schlüsselwörtern nicht zulässig sind cl\u0061ss und denselben Bezeichner wie "
@class " haben.

Zwei Bezeichner werden als identisch angesehen, wenn Sie nach dem Anwenden der folgenden
Transformationen identisch sind:
Wenn Sie verwendet wird, wird das Präfix " @ " entfernt.
Jede unicode_escape_sequence wird in das entsprechende Unicode-Zeichen transformiert.
Alle formatting_character s werden entfernt.
Bezeichner, die zwei aufeinander folgende Unterstriche () enthalten, U+005F sind für die Verwendung durch die-
Implementierung reserviert. Beispielsweise kann eine-Implementierung erweiterte Schlüsselwörter
bereitstellen, die mit zwei unterstrichen beginnen.
Keywords
Ein Schlüsselwor t ist eine bezeichnerartige Zeichenfolge, die reserviert ist und nicht als Bezeichner verwendet
werden kann, es sei denn, das Zeichen wird vorangestellt @ .

keyword
: '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' | 'ref' | 'return' | 'sbyte' | 'sealed'
| 'short' | 'sizeof' | 'stackalloc' | 'static' | 'string'
| 'struct' | 'switch' | 'this' | 'throw' | 'true'
| 'try' | 'typeof' | 'uint' | 'ulong' | 'unchecked'
| 'unsafe' | 'ushort' | 'using' | 'virtual' | 'void'
| 'volatile' | 'while'
;
An manchen Stellen in der Grammatik haben bestimmte Bezeichner eine besondere Bedeutung, aber keine
Schlüsselwörter. Solche Bezeichner werden manchmal als "kontextabhängige Schlüsselwörter" bezeichnet. In
einer Eigenschafts Deklaration get haben die Bezeichner "" und " set " z. b. eine besondere Bedeutung
(Accessoren). Ein anderer Bezeichner als get oder set ist in diesen Speicherorten nie zulässig, sodass diese
Verwendung nicht mit der Verwendung dieser Wörter als Bezeichner in Konflikt steht. In anderen Fällen, z. b. mit
dem Bezeichner " var " in implizit typisierten lokalen Variablen Deklarationen (lokale Variablen Deklarationen),
kann ein Kontext Schlüsselwort mit deklarierten Namen in Konflikt stehen. In solchen Fällen hat der deklarierte
Name Vorrang vor der Verwendung des Bezeichners als kontextbezogenes Schlüsselwort.
Literale
Ein Literal ist die Quellcodedarstellung eines Werts.

literal
: boolean_literal
| integer_literal
| real_literal
| character_literal
| string_literal
| null_literal
;

Boolesche Literale
Es gibt zwei boolesche Literalwerte: true und false .

boolean_literal
: 'true'
| 'false'
;

Der Typ eines boolean_literal ist bool .


Ganzzahlenliteral
Ganzzahlige Literale werden verwendet, um Werte der Typen int , uint , und zu schreiben long ulong .
Ganzzahlige Literale haben zwei mögliche Formen: decimal und hexadezimal.
integer_literal
: decimal_integer_literal
| hexadecimal_integer_literal
;

decimal_integer_literal
: decimal_digit+ integer_type_suffix?
;

decimal_digit
: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
;

integer_type_suffix
: 'U' | 'u' | 'L' | 'l' | 'UL' | 'Ul' | 'uL' | 'ul' | 'LU' | 'Lu' | 'lU' | 'lu'
;

hexadecimal_integer_literal
: '0x' hex_digit+ integer_type_suffix?
| '0X' hex_digit+ integer_type_suffix?
;

hex_digit
: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
| 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f';

Der Typ eines ganzzahligen Literals wird wie folgt bestimmt:


Wenn das Literale kein Suffix aufweist, verfügt es über den ersten dieser Typen, in dem sein Wert dargestellt
werden kann: int , uint , long , ulong .
Wenn das Literale von oder als Suffix versehen wird U u , verfügt es über den ersten dieser Typen, in dem
sein Wert dargestellt werden kann: uint , ulong .
Wenn das Literale von oder als Suffix versehen wird L l , verfügt es über den ersten dieser Typen, in dem
sein Wert dargestellt werden kann: long , ulong .
Wenn das Literale von UL , Ul , uL , ul , LU ,, oder suffixt wird Lu lU lu , ist es vom Typ ulong .

Wenn der durch ein Ganzzahlliteral dargestellte Wert außerhalb des Bereichs des ulong Typs liegt, tritt ein
Kompilierzeitfehler auf.
Es wird empfohlen, L l beim Schreiben von literalen des Typs "" anstelle von "" long zu verwenden, da es
einfach ist, den Buchstaben " l " mit der Ziffer "" zu verwechseln 1 .
Um zuzulassen, dass die kleinsten möglichen int long Werte und als Dezimale ganzzahlige Literale
geschrieben werden, sind die folgenden zwei Regeln vorhanden:
Wenn eine decimal_integer_literal mit dem Wert 2147483648 (2 ^ 31) und kein integer_type_suffix als das
Token unmittelbar nach einem unären Minus Operator Token (unärer Minus Operator) angezeigt wird, ist das
Ergebnis eine Konstante vom Typ int mit dem Wert-2147483648 (-2 ^ 31). In allen anderen Fällen ist eine
solche decimal_integer_literal vom Typ uint .
Wenn ein decimal_integer_literal mit dem Wert 9.223.372.036.854.775.808 (2 ^ 63) und kein
integer_type_suffix oder die integer_type_suffix L oder l als Token unmittelbar nach einem unären Minus
Operator Token (unärer Minus Operator) angezeigt wird, ist das Ergebnis eine Konstante vom Typ long mit
dem Wert-9.223.372.036.854.775.808 (-2 ^ 63). In allen anderen Fällen ist eine solche
decimal_integer_literal vom Typ ulong .
Real-Literale
Echte Literale werden verwendet, um Werte der Typen float , double und zu schreiben decimal .
real_literal
: decimal_digit+ '.' decimal_digit+ exponent_part? real_type_suffix?
| '.' decimal_digit+ exponent_part? real_type_suffix?
| decimal_digit+ exponent_part real_type_suffix?
| decimal_digit+ real_type_suffix
;

exponent_part
: 'e' sign? decimal_digit+
| 'E' sign? decimal_digit+
;

sign
: '+'
| '-'
;

real_type_suffix
: 'F' | 'f' | 'D' | 'd' | 'M' | 'm'
;

Wenn keine real_type_suffix angegeben wird, ist der Typ des echten Literals double . Andernfalls bestimmt das
echte Typsuffix den Typ des echten Literals wie folgt:
Ein echtes LiteralSuffix, das von F oder f ist, ist vom Typ float . Die Literale 1f ,, 1.5f 1e10f und sind
z. b 123.456F . vom Typ float .
Ein echtes LiteralSuffix, das von D oder d ist, ist vom Typ double . Die Literale 1d ,, 1.5d 1e10d und sind
z. b 123.456D . vom Typ double .
Ein echtes LiteralSuffix, das von M oder m ist, ist vom Typ decimal . Die Literale 1m ,, 1.5m 1e10m und
sind z. b 123.456M . vom Typ decimal . Diese Literale werden in einen- decimal Wert konvertiert, indem der
genaue Wert verwendet wird, und, falls erforderlich, auf den nächstgelegenen darstellbaren Wert mit der-
Rundung (dem Decimal-Typ) gerundet. Alle in der Literale sichtbaren Skalierungen bleiben erhalten, es sei
denn, der Wert ist gerundet, oder der Wert ist NULL (in letzterem Fall ist das Vorzeichen und die
Dezimalstellen 0). Daher wird das Literale 2.900m analysiert, um das Dezimaltrennzeichen, den 0
Koeffizienten und die Skala zu bilden 2900 3 .

Wenn das angegebene Literale nicht im angegebenen Typ dargestellt werden kann, tritt ein Kompilierzeitfehler
auf.
Der Wert eines echten Literals vom Typ float oder double wird mit dem IEEE-Modus "Round to Next"
bestimmt.
Beachten Sie, dass in einem echten Literalzeichen nach dem Dezimaltrennzeichen immer Dezimalstellen
erforderlich sind. Beispielsweise 1.3F ist ein echtes Literalzeichen, aber 1.F nicht.
Zeichenliterale
Ein Zeichenliteral stellt ein einzelnes Zeichen dar und besteht normalerweise aus einem Zeichen in
Anführungszeichen, wie in 'a' .
Hinweis: die Grammatik-Notation von antlr macht folgendes verwirrend! Wenn Sie in antlr schreiben, \' steht
es für ein einzelnes Anführungszeichen ' . Und wenn Sie schreiben, \\ steht ein einzelner umgekehrter
Schrägstrich \ . Daher bedeutet die erste Regel für ein Zeichenliteral, dass Sie mit einem einfachen
Anführungszeichen, einem Zeichen und einem einfachen Anführungszeichen beginnt. Und die elf möglichen
einfachen Escapesequenzen sind \' , \" , \\ , \0 , \a , \b , \f , \n , \r , \t , \v .
character_literal
: '\'' character '\''
;

character
: single_character
| simple_escape_sequence
| hexadecimal_escape_sequence
| unicode_escape_sequence
;

single_character
: '<Any character except \' (U+0027), \\ (U+005C), and new_line_character>'
;

simple_escape_sequence
: '\\\'' | '\\"' | '\\\\' | '\\0' | '\\a' | '\\b' | '\\f' | '\\n' | '\\r' | '\\t' | '\\v'
;

hexadecimal_escape_sequence
: '\\x' hex_digit hex_digit? hex_digit? hex_digit?;

Ein Zeichen, das einem umgekehrten Schrägstrich ( \ ) in einem Zeichen folgt, muss eines der folgenden
Zeichen sein: ' , " , \ , 0 , a , b , f , n , r , t , u , U , x , v . Andernfalls tritt ein
Kompilierungsfehler auf.
Eine hexadezimale Escapesequenz stellt ein einzelnes Unicode-Zeichen dar, wobei der Wert durch die
hexadezimale Zahl nach "" gebildet wird \x .
Wenn der Wert, der von einem Zeichenliterals dargestellt wird, größer als ist U+FFFF , tritt ein
Kompilierzeitfehler auf.
Eine Unicode-Escapesequenz (Escapesequenzen fürUnicode-Zeichen) in einem Zeichenliterals muss im Bereich
U+0000 von liegen U+FFFF .

Eine einfache Escapesequenz stellt eine Unicode-Zeichencodierung dar, wie in der folgenden Tabelle
beschrieben.

ESC A P ESEQ UEN Z Z EIC H EN N A M E UN IC O DE- C O DIERUN G

\' Einfaches Anführungszeichen 0x0027

\" Doppeltes Anführungszeichen 0x0022

\\ Umgekehrter Schrägstrich 0x005C

\0 Null 0x0000

\a Warnung 0x0007

\b Rücktaste 0x0008

\f Seitenvorschub 0x000C

\n Zeilenwechsel 0x000A

\r Wagenrücklauf 0x000D
ESC A P ESEQ UEN Z Z EIC H EN N A M E UN IC O DE- C O DIERUN G

\t Horizontaler Tabulator 0x0009

\v Vertikaler Tabulator 0x000B

Der Typ eines character_literal ist char .


Zeichenfolgenliterale
C# unterstützt zwei Formen von Zeichenfolgenliteralen: *reguläre Zeichenfolge Literale _ und _ ausführliche
Zeichen folgen Literale *.
Ein reguläres Zeichenfolgenliterale besteht aus null oder mehr Zeichen, die in doppelten Anführungszeichen
eingeschlossen sind, wie z. b., und kann sowohl einfache Escapesequenzen (z. b. "hello" \t für das Tabstopp
Zeichen) als auch hexadezimale und
Ein ausführlichen Zeichenfolgenliterals besteht aus einem @ Zeichen, gefolgt von einem doppelten
Anführungszeichen, NULL oder mehr Zeichen und einem schließenden doppelten Anführungszeichen. Ein
einfaches Beispiel ist @"hello" . In einem ausführlichen zeichenfolgenliteralliteralen werden die Zeichen
zwischen den Trennzeichen wörtlich interpretiert, die einzige Ausnahme ist eine quote_escape_sequence.
Insbesondere einfache Escapesequenzen und hexadezimale und Unicode-Escapesequenzen werden in
ausführlichen Zeichenfolgenliteralen nicht verarbeitet. Ein ausführlicher zeichenfolgenliteralvorgang kann
mehrere Zeilen umfassen.

string_literal
: regular_string_literal
| verbatim_string_literal
;

regular_string_literal
: '"' regular_string_literal_character* '"'
;

regular_string_literal_character
: single_regular_string_literal_character
| simple_escape_sequence
| hexadecimal_escape_sequence
| unicode_escape_sequence
;

single_regular_string_literal_character
: '<Any character except " (U+0022), \\ (U+005C), and new_line_character>'
;

verbatim_string_literal
: '@"' verbatim_string_literal_character* '"'
;

verbatim_string_literal_character
: single_verbatim_string_literal_character
| quote_escape_sequence
;

single_verbatim_string_literal_character
: '<any character except ">'
;

quote_escape_sequence
: '""'
;
Ein Zeichen, das einem umgekehrten Schrägstrich ( \ ) in einem regular_string_literal_character folgt, muss
eines der folgenden Zeichen sein: ' , " , \ , 0 , a , b , f , n , r , t ,,, u U x , v . Andernfalls tritt
ein Kompilierungsfehler auf.
Das Beispiel

string a = "hello, world"; // hello, world


string b = @"hello, world"; // hello, world

string c = "hello \t world"; // hello world


string d = @"hello \t world"; // hello \t world

string e = "Joe said \"Hello\" to me"; // Joe said "Hello" to me


string f = @"Joe said ""Hello"" to me"; // Joe said "Hello" to me

string g = "\\\\server\\share\\file.txt"; // \\server\share\file.txt


string h = @"\\server\share\file.txt"; // \\server\share\file.txt

string i = "one\r\ntwo\r\nthree";
string j = @"one
two
three";

zeigt eine Vielzahl von Zeichenfolgenliteralen an. Das letzte Zeichenfolgenliterale, j , ist ein ausführlicher
Zeichenfolgenliteral, das mehrere Zeilen Die Zeichen zwischen den Anführungszeichen, einschließlich
Leerzeichen, z. b. neue Zeilenzeichen, werden wörtlich beibehalten.
Da eine hexadezimale Escapesequenz eine Variable Anzahl von hexadezimalen Ziffern aufweisen kann, enthält
das Zeichenfolgenliterale "\x123" ein einzelnes Zeichen 123 mit dem hexadezi Um eine Zeichenfolge zu
erstellen, die das Zeichen mit dem Hexadezimalwert 12 gefolgt vom Zeichen 3 enthält, könnte ein "\x00123"
oder stattdessen geschrieben werden "\x12" + "3" .
Der Typ eines string_literal ist string .
Jedes Zeichenfolgenliterale führt nicht unbedingt zu einer neuen Zeichen folgen Instanz. Wenn zwei oder mehr
Zeichen folgen Literale, die gemäß dem Zeichen folgen Gleichheits Operator (Zeichen folgen Gleichheits
Operatoren) gleichwertig sind, im selben Programm vorkommen, verweisen diese Zeichen folgen Literale auf
dieselbe Zeichen folgen Instanz. Beispielsweise die Ausgabe, die von

class Test
{
static void Main() {
object a = "hello";
object b = "hello";
System.Console.WriteLine(a == b);
}
}

liegt True daran, dass die beiden Literale auf dieselbe Zeichen folgen Instanz verweisen.
Interpoliert Zeichen folgen Literale
Interinterpolierte Zeichen folgen Literale ähneln Zeichen folgen Literalen, enthalten jedoch Lücken, die durch
und getrennt sind { } , wobei Ausdrücke auftreten können. Zur Laufzeit werden die Ausdrücke so
ausgewertet, dass Ihre Text Formulare in der Zeichenfolge an der Stelle, an der die Lücke auftritt, ersetzt werden.
Die Syntax und die Semantik der Zeichen folgen Interpolationen werden im Abschnitt (interpoliertZeichen
folgen) beschrieben.
Wie bei Zeichenfolgenliteralen können interpoliert Zeichen folgen Literale entweder regulär oder wörtlich sein.
Interpoliert reguläre Zeichenfolgenliterale werden durch $" und getrennt " , und interpoliert, ausführliche
Zeichen folgen Literale werden durch $@" und getrennt " .
Wie bei anderen literalen ergibt die lexikalische Analyse eines interpoliert für interpoliert zunächst ein einzelnes
Token, wie gemäß der Grammatik unten. Vor der syntaktischen Analyse wird jedoch das einzelne Token eines
interpoliert-Zeichenfolgenliterals in mehrere Token für die Teile der Zeichenfolge aufgeteilt, die die Löcher
einschließen, und die in den Löchern auftretenden Eingabeelemente werden lexikalisch erneut analysiert. Dies
kann wiederum dazu führen, dass mehr interinterpolierte Zeichenfolgenliterale verarbeitet werden, die
verarbeitet werden können, aber wenn lexikalisch korrekt ist, führt letztendlich zu einer Sequenz von Tokens,
damit die syntaktische Analyse verarbeitet wird.

interpolated_string_literal
: '$' interpolated_regular_string_literal
| '$' interpolated_verbatim_string_literal
;

interpolated_regular_string_literal
: interpolated_regular_string_whole
| interpolated_regular_string_start interpolated_regular_string_literal_body
interpolated_regular_string_end
;

interpolated_regular_string_literal_body
: regular_balanced_text
| interpolated_regular_string_literal_body interpolated_regular_string_mid regular_balanced_text
;

interpolated_regular_string_whole
: '"' interpolated_regular_string_character* '"'
;

interpolated_regular_string_start
: '"' interpolated_regular_string_character* '{'
;

interpolated_regular_string_mid
: interpolation_format? '}' interpolated_regular_string_characters_after_brace? '{'
;

interpolated_regular_string_end
: interpolation_format? '}' interpolated_regular_string_characters_after_brace? '"'
;

interpolated_regular_string_characters_after_brace
: interpolated_regular_string_character_no_brace
| interpolated_regular_string_characters_after_brace interpolated_regular_string_character
;

interpolated_regular_string_character
: single_interpolated_regular_string_character
| simple_escape_sequence
| hexadecimal_escape_sequence
| unicode_escape_sequence
| open_brace_escape_sequence
| close_brace_escape_sequence
;

interpolated_regular_string_character_no_brace
: '<Any interpolated_regular_string_character except close_brace_escape_sequence and any
hexadecimal_escape_sequence or unicode_escape_sequence designating } (U+007D)>'
;

single_interpolated_regular_string_character
: '<Any character except \" (U+0022), \\ (U+005C), { (U+007B), } (U+007D), and new_line_character>'
;

open_brace_escape_sequence
open_brace_escape_sequence
: '{{'
;

close_brace_escape_sequence
: '}}'
;

regular_balanced_text
: regular_balanced_text_part+
;

regular_balanced_text_part
: single_regular_balanced_text_character
| delimited_comment
| '@' identifier_or_keyword
| string_literal
| interpolated_string_literal
| '(' regular_balanced_text ')'
| '[' regular_balanced_text ']'
| '{' regular_balanced_text '}'
;

single_regular_balanced_text_character
: '<Any character except / (U+002F), @ (U+0040), \" (U+0022), $ (U+0024), ( (U+0028), ) (U+0029), [
(U+005B), ] (U+005D), { (U+007B), } (U+007D) and new_line_character>'
| '</ (U+002F), if not directly followed by / (U+002F) or * (U+002A)>'
;

interpolation_format
: ':' interpolation_format_character+
;

interpolation_format_character
: '<Any character except \" (U+0022), : (U+003A), { (U+007B) and } (U+007D)>'
;

interpolated_verbatim_string_literal
: interpolated_verbatim_string_whole
| interpolated_verbatim_string_start interpolated_verbatim_string_literal_body
interpolated_verbatim_string_end
;

interpolated_verbatim_string_literal_body
: verbatim_balanced_text
| interpolated_verbatim_string_literal_body interpolated_verbatim_string_mid verbatim_balanced_text
;

interpolated_verbatim_string_whole
: '@"' interpolated_verbatim_string_character* '"'
;

interpolated_verbatim_string_start
: '@"' interpolated_verbatim_string_character* '{'
;

interpolated_verbatim_string_mid
: interpolation_format? '}' interpolated_verbatim_string_characters_after_brace? '{'
;

interpolated_verbatim_string_end
: interpolation_format? '}' interpolated_verbatim_string_characters_after_brace? '"'
;

interpolated_verbatim_string_characters_after_brace
: interpolated_verbatim_string_character_no_brace
| interpolated_verbatim_string_characters_after_brace interpolated_verbatim_string_character
;

interpolated_verbatim_string_character
interpolated_verbatim_string_character
: single_interpolated_verbatim_string_character
| quote_escape_sequence
| open_brace_escape_sequence
| close_brace_escape_sequence
;

interpolated_verbatim_string_character_no_brace
: '<Any interpolated_verbatim_string_character except close_brace_escape_sequence>'
;

single_interpolated_verbatim_string_character
: '<Any character except \" (U+0022), { (U+007B) and } (U+007D)>'
;

verbatim_balanced_text
: verbatim_balanced_text_part+
;

verbatim_balanced_text_part
: single_verbatim_balanced_text_character
| comment
| '@' identifier_or_keyword
| string_literal
| interpolated_string_literal
| '(' verbatim_balanced_text ')'
| '[' verbatim_balanced_text ']'
| '{' verbatim_balanced_text '}'
;

single_verbatim_balanced_text_character
: '<Any character except / (U+002F), @ (U+0040), \" (U+0022), $ (U+0024), ( (U+0028), ) (U+0029), [
(U+005B), ] (U+005D), { (U+007B) and } (U+007D)>'
| '</ (U+002F), if not directly followed by / (U+002F) or * (U+002A)>'
;

Ein interpolated_string_literal Token wird wie folgt als mehrere Token und andere Eingabeelemente in der
Reihenfolge des interpolated_string_literal neu interpretiert:
Vorkommen der folgenden werden als separate einzelne Token neu interpretiert: das führende $
Vorzeichen, interpolated_regular_string_whole, interpolated_regular_string_start, die
interpolated_regular_string_mid, interpolated_regular_string_end, interpolated_verbatim_string_whole,
interpolated_verbatim_string_start, interpolated_verbatim_string_mid und interpolated_verbatim_string_end.
Vorkommen von regular_balanced_text und verbatim_balanced_text dazwischen werden als input_section
(lexikalische Analyse) neu verarbeitet und als resultierende Sequenz von Eingabe Elementen interpretiert.
Diese können wiederum interinterpolierte zeichenfolgenliteraltoken enthalten, die neu interpretiert werden.
Die syntaktische Analyse kombiniert die Token erneut in eine interpolated_string_expression (interpoliert).
Beispiele TODO
Das NULL-Literale

null_literal
: 'null'
;

Der null_literal kann implizit in einen Verweistyp oder einen Typ konvertiert werden, der NULL-Werte zulässt.
Operatoren und Trennzeichen
Es gibt verschiedene Arten von Operatoren und Trennzeichen. Operatoren werden in Ausdrücken verwendet,
um Vorgänge mit einem oder mehreren Operanden zu beschreiben. Der Ausdruck a + b verwendet
beispielsweise den Operator + , um die zwei Operanden a und b hinzuzufügen. Trennzeichen werden zum
Gruppieren und Trennen verwendet.

operator_or_punctuator
: '{' | '}' | '[' | ']' | '(' | ')' | '.' | ',' | ':' | ';'
| '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '!' | '~'
| '=' | '<' | '>' | '?' | '??' | '::' | '++' | '--' | '&&' | '||'
| '->' | '==' | '!=' | '<=' | '>=' | '+=' | '-=' | '*=' | '/=' | '%='
| '&=' | '|=' | '^=' | '<<' | '<<=' | '=>'
;

right_shift
: '>>'
;

right_shift_assignment
: '>>='
;

Der senkrechte Strich in den right_shift -und right_shift_assignment Produktionen wird verwendet, um
anzugeben, dass im Gegensatz zu anderen Produktionen in der syntaktischen Grammatik keine Zeichen
jeglicher Art (nicht sogar Leerzeichen) zwischen den Token zulässig sind. Diese Produktionen werden speziell
behandelt, um die korrekte Handhabung von type_parameter_list s (Typparameter) zu ermöglichen.

Vorverarbeitungs Direktiven
Die Vorverarbeitungs Direktiven bieten die Möglichkeit, Abschnitte von Quelldateien bedingt zu überspringen,
Fehler-und Warnungs Bedingungen zu melden und verschiedene Bereiche des Quellcodes zu gliedern. Der
Begriff "Vorverarbeitungs Direktiven" wird nur aus Gründen der Konsistenz mit den Programmiersprachen C
und C++ verwendet. In c# gibt es keinen separaten Schritt vor der Verarbeitung. Vorverarbeitungs Direktiven
werden im Rahmen der lexikalischen Analysephase verarbeitet.

pp_directive
: pp_declaration
| pp_conditional
| pp_line
| pp_diagnostic
| pp_region
| pp_pragma
;

Die folgenden Vorverarbeitungs Direktiven sind verfügbar:


#define und #undef , die zum definieren bzw. Aufheben der Definition von Symbolen für bedingte
Kompilierung (Deklarations Anweisungen) verwendet werden.
#if , #elif , #else und #endif , die verwendet werden, um Abschnitte des Quellcodes bedingt zu
überspringen (bedingte Kompilierungs Direktiven).
#line , der verwendet wird, um Zeilennummern zu steuern, die für Fehler und Warnungen ausgegeben
werden (Zeilen Anweisungen).
#error und #warning , die verwendet werden, um Fehler und Warnungen bzw. (Diagnose Direktiven)
auszugeben.
#region und #endregion , die verwendet werden, um Abschnitte des Quellcodes (Regions Direktiven)
explizit zu markieren.
#pragma , der verwendet wird, um optionale Kontextinformationen für den Compiler anzugeben (pragma-
Direktiven).
Eine Vorverarbeitungs Direktive belegt immer eine separate Zeile des Quellcodes und beginnt immer mit einem
# Zeichen und einem Vorverarbeitungs-Direktivennamen. Leerräume können vor dem # Zeichen und
zwischen dem # Zeichen und dem Direktivennamen auftreten.
Eine Quellzeile, die eine-,-,-,-,- #define #undef ,-oder- #if #elif Direktive enthält, #else #endif #line
#endregion kann mit einem einzeiligen Kommentar enden. Durch Trennzeichen getrennte Kommentare (der
/* */ Stil von Kommentaren) sind in Quellzeilen mit Vorverarbeitungs Direktiven nicht zulässig.

Vorverarbeitungs Direktiven sind keine Token und sind nicht Teil der syntaktischen Grammatik von c#.
Allerdings können Vorverarbeitungs Direktiven verwendet werden, um Sequenzen von Token einzuschließen
oder auszuschließen, und sich auf diese Weise auf die Bedeutung eines c#-Programms auswirken. Bei der
Kompilierung wird das Programm z. b.:

#define A
#undef B

class C
{
#if A
void F() {}
#else
void G() {}
#endif

#if B
void H() {}
#else
void I() {}
#endif
}

ergibt genau dieselbe Sequenz von Token wie das Programm:

class C
{
void F() {}
void I() {}
}

Im Gegensatz dazu sind die beiden Programme ganz anders, syntaktisch, Sie sind jedoch identisch.
Symbole für bedingte Kompilierung
Die Funktionen für die bedingte Kompilierung, die von den #if Direktiven,, und bereitgestellt werden, werden
#elif #else #endif durch Vorverarbeitungs Ausdrücke (Vorverarbeitungs Ausdrücke) und bedingte
Kompilierungs Symbole gesteuert.

conditional_symbol
: '<Any identifier_or_keyword except true or false>'
;

Ein Symbol für die bedingte Kompilierung hat zwei mögliche Zustände: defined _ oder _ * nicht definiert *. Am
Anfang der lexikalischen Verarbeitung einer Quelldatei ist ein Symbol für die bedingte Kompilierung nicht
definiert, es sei denn, es wurde explizit durch einen externen Mechanismus definiert (z. b. eine Befehlszeilen-
Compileroption). Wenn eine #define Direktive verarbeitet wird, wird das in dieser Direktive benannte bedingte
Kompilierungs Symbol in dieser Quelldatei definiert. Das Symbol bleibt so lange definiert, bis eine- #undef
Direktive für das gleiche Symbol verarbeitet wird oder bis das Ende der Quelldatei erreicht wird. Dies bedeutet,
dass die #define -und- #undef Direktiven in einer Quelldatei keine Auswirkung auf andere Quelldateien im
gleichen Programm haben.
Wenn in einem Vorverarbeitungs Ausdruck darauf verwiesen wird, hat ein definiertes bedingtes Kompilierungs
Symbol den booleschen Wert true , und ein nicht definiertes bedingtes Kompilierungs Symbol weist den
booleschen Wert auf false . Es ist nicht erforderlich, dass bedingte Kompilierungs Symbole explizit deklariert
werden, bevor in Vorverarbeitungs Ausdrücken auf Sie verwiesen wird. Stattdessen sind nicht deklarierte
Symbole einfach undefiniert und verfügen daher über den Wert false .
Der Namespace für Symbole für die bedingte Kompilierung ist eindeutig und von allen anderen benannten
Entitäten in einem c#-Programm getrennt. Auf Symbole für die bedingte Kompilierung kann nur in #define -
und #undef -Direktiven und in Vorverarbeitungs Ausdrücken verwiesen werden.
Vorverarbeiten von Ausdrücken
Vorverarbeitungs Ausdrücke können in #if -und- #elif Direktiven auftreten. Die Operatoren, ! == , !=
&& und || sind in Vorverarbeitungs Ausdrücken zulässig, und für die Gruppierung können Klammern
verwendet werden.

pp_expression
: whitespace? pp_or_expression whitespace?
;

pp_or_expression
: pp_and_expression
| pp_or_expression whitespace? '||' whitespace? pp_and_expression
;

pp_and_expression
: pp_equality_expression
| pp_and_expression whitespace? '&&' whitespace? pp_equality_expression
;

pp_equality_expression
: pp_unary_expression
| pp_equality_expression whitespace? '==' whitespace? pp_unary_expression
| pp_equality_expression whitespace? '!=' whitespace? pp_unary_expression
;

pp_unary_expression
: pp_primary_expression
| '!' whitespace? pp_unary_expression
;

pp_primary_expression
: 'true'
| 'false'
| conditional_symbol
| '(' whitespace? pp_expression whitespace? ')'
;

Wenn in einem Vorverarbeitungs Ausdruck darauf verwiesen wird, hat ein definiertes bedingtes Kompilierungs
Symbol den booleschen Wert true , und ein nicht definiertes bedingtes Kompilierungs Symbol weist den
booleschen Wert auf false .
Bei der Auswertung eines Vorverarbeitungs Ausdrucks ergibt sich immer ein boolescher Wert. Die Regeln für
die Auswertung eines Vorverarbeitungs Ausdrucks sind identisch mit denen für einen konstanten Ausdruck
(Konstante Ausdrücke), mit dem Unterschied, dass nur benutzerdefinierte Entitäten, auf die verwiesen werden
kann, Symbole für die bedingte Kompilierung sind.
Deklarations Direktiven
Die Deklarations Anweisungen werden verwendet, um Symbole für die bedingte Kompilierung zu definieren
oder zu deaktivieren.
pp_declaration
: whitespace? '#' whitespace? 'define' whitespace conditional_symbol pp_new_line
| whitespace? '#' whitespace? 'undef' whitespace conditional_symbol pp_new_line
;

pp_new_line
: whitespace? single_line_comment? new_line
;

Die Verarbeitung einer- #define Direktive bewirkt, dass das angegebene bedingte Kompilierungs Symbol
definiert wird, beginnend mit der Quellzeile, die auf die-Direktive folgt. Entsprechend bewirkt die Verarbeitung
einer- #undef Direktive, dass das angegebene bedingte Kompilierungs Symbol undefiniert wird, beginnend mit
der Quellzeile, die auf die-Direktive folgt.
Alle #define -und- #undef Direktiven in einer Quelldatei müssen vor dem ersten Token (Tokens) in der
Quelldatei auftreten; andernfalls tritt ein Kompilierzeitfehler auf. In intuitiver Hinsicht #define müssen-und-
#undef Direktiven allen "echten Code" in der Quelldatei vorangestellt sein.

Beispiel:

#define Enterprise

#if Professional || Enterprise


#define Advanced
#endif

namespace Megacorp.Data
{
#if Advanced
class PivotTable {...}
#endif
}

ist gültig, da die- #define Anweisungen dem ersten Token (dem- namespace Schlüsselwort) in der Quelldatei
vorangestellt sind.
Das folgende Beispiel führt zu einem Kompilierzeitfehler, da ein #define echter Code befolgt:

#define A
namespace N
{
#define B
#if B
class Class1 {}
#endif
}

Ein #define kann ein Symbol für die bedingte Kompilierung definieren, das bereits definiert ist, ohne #undef
dass ein Eingreifen für dieses Symbol vorliegt. Im folgenden Beispiel wird ein Symbol für die bedingte
Kompilierung definiert A und dann wieder definiert.

#define A
#define A

Ein #undef kann ein bedingtes Kompilierungs Symbol, das nicht definiert ist, nicht definieren. Im folgenden
Beispiel wird ein Symbol für die bedingte Kompilierung definiert A und dann zweimal wieder definiert. das
zweite #undef hat jedoch keine Auswirkung, aber es ist noch gültig.
#define A
#undef A
#undef A

Bedingte Kompilierungs Direktiven


Die Direktiven für die bedingte Kompilierung werden verwendet, um Teile einer Quelldatei bedingt
einzuschließen oder auszuschließen.

pp_conditional
: pp_if_section pp_elif_section* pp_else_section? pp_endif
;

pp_if_section
: whitespace? '#' whitespace? 'if' whitespace pp_expression pp_new_line conditional_section?
;

pp_elif_section
: whitespace? '#' whitespace? 'elif' whitespace pp_expression pp_new_line conditional_section?
;

pp_else_section:
| whitespace? '#' whitespace? 'else' pp_new_line conditional_section?
;

pp_endif
: whitespace? '#' whitespace? 'endif' pp_new_line
;

conditional_section
: input_section
| skipped_section
;

skipped_section
: skipped_section_part+
;

skipped_section_part
: skipped_characters? new_line
| pp_directive
;

skipped_characters
: whitespace? not_number_sign input_character*
;

not_number_sign
: '<Any input_character except #>'
;

Wie durch die-Syntax angegeben, müssen bedingte Kompilierungs Direktiven als Sätze geschrieben werden, die
aus, in Reihenfolge, einer- #if Direktive, NULL oder mehr #elif -Direktiven, keiner oder einer-Direktive
#else und einer- #endif Direktive bestehen Zwischen den Direktiven bestehen bedingte Abschnitte des
Quellcodes. Jeder Abschnitt wird von der unmittelbar vorangehenden-Direktive gesteuert. Ein bedingter
Abschnitt kann selbst ggf. ggf. ggf
Ein pp_conditional wählt höchstens eine der enthaltenen conditional_section s für die normale lexikalische
Verarbeitung aus:
Die pp_expression s der #if -und- #elif Direktiven werden in der Reihenfolge ausgewertet, bis ein
Ergebnis ergibt true . Wenn ein Ausdruck ergibt true , wird der conditional_section der entsprechenden
Direktive ausgewählt.
Wenn alle pp_expression s ergeben false und eine- #else Direktive vorhanden ist, wird der
conditional_section der #else Direktive ausgewählt.
Andernfalls ist keine conditional_section ausgewählt.
Der ausgewählte conditional_section wird, sofern vorhanden, als normaler input_section verarbeitet: der im
Abschnitt enthaltene Quellcode muss der lexikalischen Grammatik entsprechen. Token werden aus dem
Quellcode im-Abschnitt generiert. und Vorverarbeitungs Direktiven im-Abschnitt haben die vorgeschriebenen
Auswirkungen.
Die verbleibenden conditional_section s, sofern vorhanden, werden als skipped_section s verarbeitet: mit
Ausnahme von Vorverarbeitungs Direktiven muss der Quellcode im-Abschnitt nicht der lexikalischen
Grammatik entsprechen. aus dem Quellcode im-Abschnitt werden keine Token generiert. und Vorverarbeitungs
Direktiven im Abschnitt müssen lexikalisch korrigiert werden, werden jedoch nicht anderweitig verarbeitet.
Innerhalb einer conditional_section , die als skipped_section verarbeitet wird, werden alle geschachtelten
conditional_section s (die in geschachtelten #if ... #endif -und #region ...- #endregion Konstrukten enthalten
sind) ebenfalls als skipped_section s verarbeitet.
Im folgenden Beispiel wird veranschaulicht, wie bedingte Kompilierungs Direktiven schachteln können:

#define Debug // Debugging on


#undef Trace // Tracing off

class PurchaseTransaction
{
void Commit() {
#if Debug
CheckConsistency();
#if Trace
WriteToLog(this.ToString());
#endif
#endif
CommitHelper();
}
}

Mit Ausnahme von Vorverarbeitungs Direktiven unterliegt der übersprungene Quellcode nicht der lexikalischen
Analyse. Beispielsweise ist Folgendes gültig, obwohl der nicht abgeschlossener Kommentar im Abschnitt lautet
#else :

#define Debug // Debugging on

class PurchaseTransaction
{
void Commit() {
#if Debug
CheckConsistency();
#else
/* Do something else
#endif
}
}

Beachten Sie jedoch, dass Vorverarbeitungs Direktiven auch in übersprungenen Abschnitten des Quellcodes
lexikalisch korrigiert werden müssen.
Vorverarbeitungs Direktiven werden nicht verarbeitet, wenn Sie in mehrzeiligen Eingabe Elementen angezeigt
werden. Das Programm lautet z. b.:
class Hello
{
static void Main() {
System.Console.WriteLine(@"hello,
#if Debug
world
#else
Nebraska
#endif
");
}
}

Ergebnisse in der Ausgabe:

hello,
#if Debug
world
#else
Nebraska
#endif

In besonderen Fällen hängt der Satz der verarbeiteten Vorverarbeitungs Direktiven möglicherweise von der
Auswertung der pp_expression ab. Beispiel:

#if X
/*
#else
/* */ class Q { }
#endif

erzeugt immer denselben Tokenstream ( class Q { } ), unabhängig davon, ob X definiert ist oder nicht.
Wenn X definiert ist, sind die einzigen verarbeiteten Direktiven #if und #endif , aufgrund des mehrzeiligen
Kommentars. Wenn nicht X definiert ist, sind drei-Direktiven ( #if , #else , #endif ) Teil der
direktivenmenge.
Diagnose Direktiven
Die Diagnose Direktiven werden verwendet, um Fehler-und Warnmeldungen explizit zu generieren, die auf die
gleiche Weise wie andere Kompilierzeitfehler und-Warnungen gemeldet werden.

pp_diagnostic
: whitespace? '#' whitespace? 'error' pp_message
| whitespace? '#' whitespace? 'warning' pp_message
;

pp_message
: new_line
| whitespace input_character* new_line
;

Beispiel:
#warning Code review needed before check-in

#if Debug && Retail


#error A build can't be both debug and retail
#endif

class Test {...}

erzeugt immer eine Warnung ("Code Review ist vor dem Einchecken erforderlich") und erzeugt einen
Kompilierzeitfehler ("ein Build kann nicht gleichzeitig Debuggen und Einzelhandel sein"), wenn die bedingten
Symbole Debug und Retail definiert sind. Beachten Sie, dass ein pp_message beliebigen Text enthalten kann.
insbesondere muss Sie keine wohlgeformten Token enthalten, wie durch das einfache Anführungszeichen im
Wort gezeigt can't .
Regions Direktiven
Die Regions Direktiven werden verwendet, um Bereiche des Quellcodes explizit zu markieren.

pp_region
: pp_start_region conditional_section? pp_end_region
;

pp_start_region
: whitespace? '#' whitespace? 'region' pp_message
;

pp_end_region
: whitespace? '#' whitespace? 'endregion' pp_message
;

An einen Bereich ist keine semantische Bedeutung angefügt. Regionen sind für die Verwendung durch den
Programmierer oder automatisierte Tools vorgesehen, um einen Abschnitt des Quellcodes zu markieren. Die in
einer- #region oder- #endregion Direktive angegebene Meldung hat ebenfalls keine semantische Bedeutung;
Sie dient lediglich zur Identifizierung der Region. Übereinstimmende #region -und- #endregion Direktiven
haben möglicherweise andere pp_message s
Die lexikalische Verarbeitung einer Region:

#region
...
#endregion

entspricht genau der lexikalischen Verarbeitung einer Direktive für die bedingte Kompilierung in der Form:

#if true
...
#endif

Line -Direktiven
Zeilen Anweisungen können verwendet werden, um die Zeilennummern und Quell Dateinamen zu ändern, die
vom Compiler in Ausgabe wie Warnungen und Fehlern gemeldet werden, und die von Aufrufer-Informations
Attributen (aufruferinformationsattribute) verwendet werden.
Zeilen Direktiven werden am häufigsten in metaprogrammierungs Tools verwendet, die c#-Quellcode aus einer
anderen Texteingabe generieren.
pp_line
: whitespace? '#' whitespace? 'line' whitespace line_indicator pp_new_line
;

line_indicator
: decimal_digit+ whitespace file_name
| decimal_digit+
| 'default'
| 'hidden'
;

file_name
: '"' file_name_character+ '"'
;

file_name_character
: '<Any input_character except ">'
;

Wenn keine #line -Direktiven vorhanden sind, meldet der Compiler echte Zeilennummern und Quell
Dateinamen in der Ausgabe. Bei der Verarbeitung einer- #line Direktive, die eine line_indicator enthält, die
nicht ist default , behandelt der Compiler die Zeile nach der-Direktive als mit der angegebenen Zeilennummer
(und dem Dateinamen, falls angegeben).
Eine- #line default Direktive kehrt die Auswirkungen aller vorangehenden #line Direktiven um. Der Compiler
meldet echte Zeilen Informationen für nachfolgende Zeilen, genau so, als ob keine #line Direktiven verarbeitet
wurden.
Eine #line hidden -Direktive hat keine Auswirkung auf die Datei-und Zeilennummern, die in Fehlermeldungen
gemeldet werden, wirkt sich aber auf das Debugging auf Quell Ebene aus Beim Debuggen verfügen alle Zeilen
zwischen einer #line hidden -Direktive und der nachfolgenden #line Direktive (das nicht #line hidden ) über
keine Zeilennummern Informationen. Wenn Sie Code im Debugger schrittweise durchlaufen, werden diese
Zeilen vollständig übersprungen.
Beachten Sie, dass sich ein file_name von einem regulären Zeichenfolgenliterals unterscheidet, da Escapezeichen
nicht verarbeitet Das \ Zeichen "" bezeichnet einfach einen normalen umgekehrten Schrägstrich innerhalb
eines file_name.
Pragma-Anweisungen
Die #pragma Vorverarbeitungs Direktive wird verwendet, um dem Compiler optionale Kontextinformationen
anzugeben. Die in einer- #pragma Direktive angegebenen Informationen ändern die Programm Semantik nie.

pp_pragma
: whitespace? '#' whitespace? 'pragma' whitespace pragma_body pp_new_line
;

pragma_body
: pragma_warning_body
;

C# stellt #pragma Anweisungen zum Steuern von Compilerwarnungen bereit. Zukünftige Versionen der Sprache
können zusätzliche Anweisungen enthalten #pragma . Um die Interoperabilität mit anderen c#-Compilern
sicherzustellen, gibt der Microsoft c#-Compiler keine Kompilierungsfehler für unbekannte #pragma Direktiven
aus. solche Direktiven generieren jedoch Warnungen.
Pragma-Warnung
Die- #pragma warning Anweisung wird verwendet, um alle oder einen bestimmten Satz von Warnmeldungen
während der Kompilierung des nachfolgenden Programm Texts zu deaktivieren oder wiederherzustellen.
pragma_warning_body
: 'warning' whitespace warning_action
| 'warning' whitespace warning_action whitespace warning_list
;

warning_action
: 'disable'
| 'restore'
;

warning_list
: decimal_digit+ (whitespace? ',' whitespace? decimal_digit+)*
;

Eine Anweisung, die die Warnungs Liste auslässt, wirkt sich auf alle Warnungen aus. Eine
#pragma warning
#pragma warning Anweisung, die eine Warnungs Liste enthält, wirkt sich nur auf die Warnungen aus, die in der
Liste angegeben sind.
Eine- #pragma warning disable Direktive deaktiviert alle oder den angegebenen Satz von Warnungen.
Eine- #pragma warning restore Direktive stellt alle oder den angegebenen Satz von Warnungen in dem Zustand
wieder her, der am Anfang der Kompilierungseinheit wirksam war. Beachten Sie Folgendes: Wenn eine
bestimmte Warnung extern deaktiviert wurde, #pragma warning restore wird diese Warnung von a (ob für alle
oder eine bestimmte Warnung) nicht erneut aktiviert.
Das folgende Beispiel zeigt die Verwendung von #pragma warning , um die Warnung, die bei referenzierten
Membern verwiesen wird, vorübergehend zu deaktivieren. dabei wird die Warnungs Nummer des Microsoft c#-
Compilers verwendet.

using System;

class Program
{
[Obsolete]
static void Foo() {}

static void Main() {


#pragma warning disable 612
Foo();
#pragma warning restore 612
}
}
Grundlegende Konzepte
04.11.2021 • 91 minutes to read

Anwendungsstart
Eine Assembly mit dem -**Einstiegspunkt* _ wird als Anwendung bezeichnet. Wenn eine Anwendung
ausgeführt wird, wird eine neue _-Anwendungsdomäne* erstellt. Mehrere verschiedene Instanziierungen einer
Anwendung können gleichzeitig auf demselben Computer vorhanden sein, und jede verfügt über eine eigene
Anwendungsdomäne.
Eine Anwendungsdomäne ermöglicht die Anwendungs Isolation, indem Sie als Container für den Anwendungs
Zustand fungiert. Eine Anwendungsdomäne fungiert als Container und Grenze für die Typen, die in der
Anwendung definiert sind, und die Klassenbibliotheken, die Sie verwendet. Typen, die in eine
Anwendungsdomäne geladen werden, unterscheiden sich vom gleichen Typ, der in eine andere
Anwendungsdomäne geladen wurde, und Instanzen von Objekten werden nicht direkt zwischen Anwendungs
Domänen freigegeben. Zum Beispiel verfügt jede Anwendungsdomäne über eine eigene Kopie statischer
Variablen für diese Typen, und ein statischer Konstruktor für einen Typ wird höchstens einmal pro
Anwendungsdomäne ausgeführt. Implementierungen können mit Implementierungs spezifischen Richtlinien
oder Mechanismen für die Erstellung und Zerstörung von Anwendungs Domänen bereitgestellt werden.
Der Anwendungsstar t erfolgt, wenn die Ausführungsumgebung eine bestimmte Methode aufruft, die als
Einstiegspunkt der Anwendung bezeichnet wird. Diese Einstiegspunkt Methode Main hat immer den Namen
und kann eine der folgenden Signaturen aufweisen:

static void Main() {...}

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

static int Main() {...}

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

Wie gezeigt, kann der Einstiegspunkt optional einen- int Wert zurückgeben. Dieser Rückgabewert wird beim
Beenden der Anwendung (Beendigung der Anwendung) verwendet.
Der Einstiegspunkt kann optional einen formalen Parameter aufweisen. Der-Parameter kann einen beliebigen
Namen haben, aber der Typ des-Parameters muss lauten string[] . Wenn der formale Parameter vorhanden
ist, erstellt und übergibt die Ausführungsumgebung ein-Argument, das string[] die Befehlszeilenargumente
enthält, die beim Starten der Anwendung angegeben wurden. Das string[] Argument ist nie NULL, kann
jedoch eine Länge von 0 (null) aufweisen, wenn keine Befehlszeilenargumente angegeben wurden.
Da c# das Überladen von Methoden unterstützt, kann eine Klasse oder Struktur mehrere Definitionen einer
Methode enthalten, vorausgesetzt, jede hat eine andere Signatur. Allerdings kann in einem einzelnen Programm
keine Klasse oder Struktur mehr als eine Methode mit dem Namen enthalten, Main deren Definition die
Verwendung als Anwendungs Einstiegspunkt qualifiziert. Andere überladene Versionen von Main sind jedoch
zulässig, vorausgesetzt, Sie verfügen über mehr als einen Parameter, oder der einzige Parameter ist ein anderer
Parameter als Type string[] .
Eine Anwendung kann aus mehreren Klassen oder Strukturen bestehen. Es ist möglich, dass mehr als eine dieser
Klassen oder Strukturen eine Methode mit dem Namen enthält, Main deren Definition die Verwendung als
Anwendungs Einstiegspunkt qualifiziert. In solchen Fällen muss ein externer Mechanismus (z. b. eine
Befehlszeilen-Compileroption) verwendet werden, um eine dieser Main Methoden als Einstiegspunkt
auszuwählen.
In c# muss jede Methode als Member einer Klasse oder Struktur definiert werden. Normalerweise wird die
deklarierte Barrierefreiheit (derdeklarierteZugriff) einer Methode durch die Zugriffsmodifizierer
(Zugriffsmodifizierer) bestimmt, die in der Deklaration angegeben sind Damit eine bestimmte Methode eines
bestimmten Typs aufgerufen werden kann, muss der Zugriff auf den Typ und den Member möglich sein. Der
Einstiegspunkt der Anwendung ist jedoch ein Sonderfall. Insbesondere kann die Ausführungsumgebung auf den
Einstiegspunkt der Anwendung zugreifen, unabhängig von der deklarierten Barrierefreiheit und unabhängig
von der deklarierten Barrierefreiheit ihrer einschließenden Typdeklarationen.
Die Einstiegspunkt Methode der Anwendung darf sich nicht in einer generischen Klassen Deklaration befinden.
In allen anderen Punkten verhalten sich Einstiegspunkt Methoden wie solche, die keine Einstiegspunkte sind.

Application termination (Beenden von Anwendungen)


Beim Beenden der Anwendung wird die Steuerung an die Ausführungsumgebung zurückgegeben.
Wenn der Rückgabetyp der *Entr y Point _-Methode der Anwendung ist int , wird der zurückgegebene Wert
als _ -Beendigungs Statuscode * der Anwendung dienen. Der Zweck dieses Codes besteht darin, die
Kommunikation über Erfolg oder Misserfolg der Ausführungsumgebung zuzulassen.
Wenn der Rückgabetyp der Einstiegspunkt Methode ist void und die Rechte geschweifter Klammer () erreicht
wird, } die diese Methode beendet oder eine Anweisung ausführt, return die keinen Ausdruck hat, führt dies
zu einem Beendigungs Statuscode von 0 .
Vor dem Beenden einer Anwendung werden Dekonstruktoren für alle Objekte, die noch keine Garbage
Collection durchgeführt haben, aufgerufen, es sei denn, eine solche Bereinigung wurde unterdrückt (z. b. durch
einen Aufruf der Library-Methode GC.SuppressFinalize ).

Deklarationen
Deklarationen in einem c#-Programm definieren die Bestandteile des Programms. C#-Programme werden
mithilfe von Namespaces (Namespaces) organisiert, die Typdeklarationen und schsted Namespace
Deklarationen enthalten können. Typdeklarationen (Typdeklarationen) werden verwendet, um Klassen (Klassen),
Strukturen (Strukturen), Schnittstellen (Schnittstellen), Enumerationen (Enumerationen) und Delegaten
(Delegaten) zu definieren. Die Arten von Membern, die in einer Typdeklaration zulässig sind, hängen von der
Form der Typdeklaration ab. Klassen Deklarationen können z. a. Deklarationen für Konstanten (Konstanten)
enthalten, Felder (Felder), Methoden (Methoden), Eigenschaften (Eigenschaften), Ereignisse (Ereignisse), Indexer
(Indexer), Operatoren (Operatoren), Instanzkonstruktoren (Instanzkonstruktoren), statische Konstruktoren
(statische Konstruktoren), Dekonstruktoren (debugtoren) und geclusterte Typen (geclusterte Typen).
Eine Deklaration definiert einen Namen im Deklarations Bereich , zu dem die Deklaration gehört. Mit
Ausnahme überladener Elemente (Signaturen und überladen) handelt es sich um einen Kompilierzeitfehler, der
zwei oder mehr Deklarationen mit demselben Namen in einem Deklarations Raum einführt. Es ist nie möglich,
dass ein Deklarations Bereich unterschiedliche Arten von Membern mit dem gleichen Namen enthält.
Beispielsweise kann ein Deklarations Raum nie ein Feld und eine Methode mit demselben Namen enthalten.
Es gibt mehrere verschiedene Typen von Deklarations Bereichen, die im folgenden beschrieben werden.
Innerhalb aller Quelldateien eines Programms sind namespace_member_declaration s ohne einschließende
namespace_declaration Member eines einzelnen kombinierten Deklarations Raums, der als globaler
Deklarations Bereich bezeichnet wird.
Innerhalb aller Quelldateien eines Programms sind namespace_member_declaration s innerhalb
namespace_declaration en, die denselben voll qualifizierten Namespace Namen aufweisen, Mitglieder eines
einzelnen kombinierten Deklarations Raums.
Jede Klassen-, Struktur-oder Schnittstellen Deklaration erstellt einen neuen Deklarations Bereich. Namen
werden in diesem Deklarations Bereich durch class_member_declaration s, struct_member_declaration s,
interface_member_declaration s oder type_parameter s eingeführt. Mit Ausnahme von überladenen
Instanzkonstruktordeklarationen und statischen Konstruktordeklarationen kann eine Klasse oder Struktur
keine Element Deklaration mit dem gleichen Namen wie die Klasse oder Struktur enthalten. Eine Klasse,
Struktur oder Schnittstelle ermöglicht die Deklaration überladener Methoden und Indexer. Außerdem
ermöglicht eine Klasse oder Struktur die Deklaration überladener Instanzkonstruktoren und Operatoren. Eine
Klasse, Struktur oder Schnittstelle kann z. b. mehrere Methoden Deklarationen mit demselben Namen
enthalten, sofern sich diese Methoden Deklarationen in Ihrer Signatur (Signaturen und überladen)
unterscheiden. Beachten Sie, dass Basisklassen nicht zum Deklarations Bereich einer Klasse beitragen und
Basis Schnittstellen nicht zum Deklarations Bereich einer Schnittstelle beitragen. Daher kann eine abgeleitete
Klasse oder Schnittstelle einen Member mit demselben Namen wie ein geerbten Member deklarieren. Ein
solcher Member gibt an, dass der geerbte Member ausgeblendet wird.
Jede Delegatdeklaration erstellt einen neuen Deklarations Bereich. Namen werden in diesem Deklarations
Raum durch formale Parameter (fixed_parameter s und parameter_array s) und type_parameter s eingeführt.
Jede Enumerationsdeklaration erstellt einen neuen Deklarations Bereich. Namen werden in diesem
Deklarations Raum durch enum_member_declarations eingeführt.
Jede Methoden Deklaration, Indexer-Deklaration, Operator Deklaration, Instanzkonstruktordeklaration und
anonyme Funktion erstellt einen neuen Deklarations Bereich, der als *lokaler Variablen Deklarations
Raum _ bezeichnet wird. Namen werden in diesem Deklarations Raum durch formale Parameter
(_fixed_parameter * s und parameter_array s) und type_parameter s eingeführt. Der Text des
Funktionsmembers oder der anonymen Funktion wird, falls vorhanden, als geschachtelt im lokalen Variablen
Deklarations Bereich betrachtet. Es ist ein Fehler bei einem Deklarations Raum für lokale Variablen und
einem in der Tabelle enthaltenen Deklarations Bereich der lokalen Variablen, der Elemente mit dem gleichen
Namen enthalten soll. Daher ist es in einem geschachtelten Deklarations Bereich nicht möglich, eine lokale
Variable oder Konstante mit dem gleichen Namen wie eine lokale Variable oder Konstante in einem
einschließenden Deklarations Bereich zu deklarieren. Es ist möglich, dass zwei Deklarations Bereiche
Elemente mit dem gleichen Namen enthalten, solange kein Deklarations Raum den anderen enthält.
Jeder Block oder switch_block sowie eine for-, foreach -und using -Anweisung erstellt einen lokalen
Variablen Deklarations Raum für lokale Variablen und lokale Konstanten. Namen werden in diesem
Deklarations Raum durch local_variable_declaration s und local_constant_declaration s eingeführt. Beachten
Sie, dass Blöcke, die als oder innerhalb des Texts eines Funktionsmembers oder einer anonymen Funktion
auftreten, innerhalb des Deklarations Raums für lokale Variablen geschachtelt sind, der von diesen
Funktionen für Ihre Parameter deklariert wird. Daher ist es ein Fehler, z. b. eine Methode mit einer lokalen
Variablen und einem Parameter mit dem gleichen Namen zu haben.
Jeder Block oder switch_block erstellt einen separaten Deklarations Raum für Bezeichnungen. Namen
werden in diesem Deklarations Bereich durch labeled_statement s eingeführt, und auf die Namen wird über
goto_statement s verwiesen. Der Zeichenbereich der Bezeichnungs Deklaration eines-Blocks enthält alle in
der Liste enthaltenen Blöcke. Daher ist es in einem geschachtelten Block nicht möglich, eine Bezeichnung mit
demselben Namen wie eine Bezeichnung in einem einschließenden Block zu deklarieren.
Die Text Reihenfolge, in der die Namen deklariert werden, ist im Allgemeinen nicht von Bedeutung.
Insbesondere ist die Text Reihenfolge für die Deklaration und Verwendung von Namespaces, Konstanten,
Methoden, Eigenschaften, Ereignissen, Indexern, Operatoren, Instanzkonstruktoren, Dekonstruktoren, statischen
Konstruktoren und Typen nicht signifikant. Die Deklarations Reihenfolge ist wie folgt signifikant:
Die Deklarations Reihenfolge für Feld Deklarationen und lokale Variablen Deklarationen bestimmt die
Reihenfolge, in der Ihre Initialisierer (sofern vorhanden) ausgeführt werden.
Lokale Variablen müssen definiert werden, bevor Sie verwendet werden (Bereiche).
Die Deklarations Reihenfolge für Enumerationmember-Deklarationen (Enumeration-Member) ist wichtig,
wenn constant_expression Werte ausgelassen werden
Der Deklarations Bereich eines Namespaces ist "Open End", und zwei Namespace Deklarationen mit demselben
voll qualifizierten Namen tragen zum selben Deklarations Bereich bei. Beispiel:

namespace Megacorp.Data
{
class Customer
{
...
}
}

namespace Megacorp.Data
{
class Order
{
...
}
}

Die beiden oben genannten Namespace Deklarationen tragen zum selben Deklarations Bereich bei, in diesem
Fall werden zwei Klassen mit den voll qualifizierten Namen Megacorp.Data.Customer und deklariert
Megacorp.Data.Order . Da die beiden Deklarationen zum gleichen Deklarations Bereich beitragen, hätte Sie einen
Kompilierzeitfehler verursacht, wenn jeder eine Deklaration einer Klasse mit dem gleichen Namen enthielt.
Wie oben angegeben, enthält der Deklarations Bereich eines-Blocks alle in der Liste enthaltenen Blöcke. Folglich
führen im folgenden Beispiel die F -Methode und die- G Methode zu einem Kompilierzeitfehler, da der Name
i im äußeren Block deklariert ist und nicht im Inneren Block erneut deklariert werden kann. Die- H Methode
und die I -Methode sind jedoch gültig, da die beiden i in separaten nicht--Blöcken deklariert werden.

class A
{
void F() {
int i = 0;
if (true) {
int i = 1;
}
}

void G() {
if (true) {
int i = 0;
}
int i = 1;
}

void H() {
if (true) {
int i = 0;
}
if (true) {
int i = 1;
}
}

void I() {
for (int i = 0; i < 10; i++)
H();
for (int i = 0; i < 10; i++)
H();
}
}
Member
Namespaces und Typen verfügen über Mitglieder . Die Member einer Entität sind in der Regel über einen
qualifizierten Namen verfügbar, der mit einem Verweis auf die Entität beginnt, gefolgt von einem " . "-Token,
gefolgt vom Namen des Members.
Member eines Typs werden entweder in der Typdeklaration deklariert oder von der Basisklasse des Typs geerbt
. Wenn ein Typ von einer Basisklasse erbt, werden alle Member der Basisklasse, ausgenommen
Instanzkonstruktoren, destrukturtoren und statische Konstruktoren, zu Membern des abgeleiteten Typs. Der
deklarierte Zugriff eines Basisklassenmembers steuert nicht, ob der Member geerbt wird – die Vererbung wird
auf einen Member ausgedehnt, der kein Instanzkonstruktor, statischer Konstruktor oder Dekonstruktor ist. Es ist
jedoch möglich, dass auf einen geerbten Member nicht in einem abgeleiteten Typ zugegriffen werden kann,
entweder aufgrund seiner deklarierten Barrierefreiheit (deklariert Barrierefreiheit) oder weil er durch eine
Deklaration im Typ selbst ausgeblendet wird (durch Vererbung verbergen).
Namespace members (Namespacemember)
Namespaces und Typen, die keinen einschließenden Namespace aufweisen, sind Member des globalen
Namespace . Dies entspricht direkt den im globalen Deklarations Bereich deklarierten Namen.
Namespaces und Typen, die in einem Namespace deklariert werden, sind Member dieses Namespace. Dies
entspricht direkt den Namen, die im Deklarations Raum des-Namespace deklariert werden.
Namespaces haben uneingeschränkten Zugriff. Es ist nicht möglich, private, geschützte oder interne
Namespaces zu deklarieren, und Namespace Namen sind immer öffentlich zugänglich.
Struct members (Strukturmember)
Die Member einer Struktur sind die Member, die in der Struktur deklariert sind, sowie die Member, die von der
direkten Basisklasse der Struktur System.ValueType und der indirekten Basisklasse geerbt wurden object .
Die Member eines einfachen Typs entsprechen direkt den Membern des Struktur Typs, der durch den einfachen
Typ Alias:
Die Member von sbyte sind die Member der System.SByte Struktur.
Die Member von byte sind die Member der System.Byte Struktur.
Die Member von short sind die Member der System.Int16 Struktur.
Die Member von ushort sind die Member der System.UInt16 Struktur.
Die Member von int sind die Member der System.Int32 Struktur.
Die Member von uint sind die Member der System.UInt32 Struktur.
Die Member von long sind die Member der System.Int64 Struktur.
Die Member von ulong sind die Member der System.UInt64 Struktur.
Die Member von char sind die Member der System.Char Struktur.
Die Member von float sind die Member der System.Single Struktur.
Die Member von double sind die Member der System.Double Struktur.
Die Member von decimal sind die Member der System.Decimal Struktur.
Die Member von bool sind die Member der System.Boolean Struktur.

Enumerationsmember
Die Member einer Enumeration sind die Konstanten, die in der-Enumeration deklariert sind, sowie die Member,
die von der direkten-Basisklasse der-Enumeration System.Enum und den indirekten Basisklassen und geerbt
werden System.ValueType object .
Klassenmember
Die Member einer Klasse sind die Member, die in der Klasse deklariert werden, und die Member, die von der
Basisklasse geerbt wurden (mit Ausnahme der Klasse object , die keine Basisklasse aufweist). Die von der
Basisklasse geerbten Member enthalten die Konstanten, Felder, Methoden, Eigenschaften, Ereignisse, Indexer,
Operatoren und Typen der Basisklasse, nicht jedoch die Instanzkonstruktoren, destrukturtoren und statischen
Konstruktoren der Basisklasse. Basisklassenmember werden ohne Rücksicht auf ihre Barrierefreiheit geerbt.
Eine Klassen Deklaration kann Deklarationen von Konstanten, Feldern, Methoden, Eigenschaften, Ereignissen,
Indexern, Operatoren, Instanzkonstruktoren, Debuggern, statischen Konstruktoren und Typen enthalten.
Die Member von object und string entsprechen direkt den Membern der Klassentypen, die Sie Alias:
Die Member von object sind die Member der- System.Object Klasse.
Die Member von string sind die Member der- System.String Klasse.
Interface members (Schnittstellenmember)
Die Member einer Schnittstelle sind die Member, die in der-Schnittstelle und in allen Basis Schnittstellen der-
Schnittstelle deklariert werden. Die Member in der Klasse object sind nicht, streng genommen Member einer
beliebigen Schnittstelle (Schnittstellenmember). Allerdings sind die Member in der Klasse object über die
Element Suche in einem beliebigen Schnittstellentyp (Member-Suche) verfügbar.
Array members (Arraymember)
Die Member eines Arrays sind die Member, die von der-Klasse geerbt werden System.Array .
Delegatmember
Die Member eines Delegaten sind die Member, die von der-Klasse geerbt werden System.Delegate .

Memberzugriff
Deklarationen von Membern ermöglichen die Steuerung des Member-Zugriffs. Der Zugriff auf einen Member
wird durch die deklarierte Barrierefreiheit (alsBarrierefreiheit deklariert) des Members in Kombination mit dem
Zugriff auf den direkt enthaltenden Typ hergestellt, sofern vorhanden.
Wenn der Zugriff auf ein bestimmtes Element zulässig ist, wird der Member als *zugänglich _ bezeichnet.
Wenn hingegen der Zugriff auf ein bestimmtes Element nicht zulässig ist, wird der Member als _ * nicht
zugänglich* * bezeichnet. Der Zugriff auf ein Mitglied ist zulässig, wenn der Text Speicherort, in dem der Zugriff
erfolgt, in der Zugriffs Domäne (Barrierefreiheits Domänen) des Mitglieds enthalten ist.
Deklarierter Zugriff
Die deklarier te Barrierefreiheit eines Members kann eine der folgenden sein:
Public, das durch Einschließen eines- public Modifizierers in die Element Deklaration ausgewählt wird. Die
intuitive Bedeutung von public ist "Access not Limited".
Geschützt, das durch Einschließen eines- protected Modifizierers in die Element Deklaration ausgewählt
wird. Die intuitive Bedeutung von protected ist "der Zugriff ist auf die enthaltende Klasse oder auf Typen
beschränkt, die von der enthaltenden Klasse abgeleitet sind".
Intern, das durch Einschließen eines- internal Modifizierers in die Element Deklaration ausgewählt wird. Die
intuitive Bedeutung von internal ist "Zugriff beschränkt auf dieses Programm".
Geschützter interner (d.h. geschützt oder intern), der durch Einschließen eines protected -und- internal
Modifizierers in die Element Deklaration ausgewählt wird. Die intuitive Bedeutung von protected internal
ist "der Zugriff ist auf dieses Programm oder auf Typen beschränkt, die von der enthaltenden Klasse
abgeleitet sind".
Privat, das durch Einschließen eines- private Modifizierers in die Element Deklaration ausgewählt wird. Die
intuitive Bedeutung von private besteht darin, dass der Zugriff auf den enthaltenden Typ beschränkt ist.

Abhängig vom Kontext, in dem eine Element Deklaration stattfindet, sind nur bestimmte Typen von deklarierter
Barrierefreiheit zulässig. Wenn eine Member-Deklaration keine Zugriffsmodifizierer enthält, bestimmt der
Kontext, in dem die Deklaration stattfindet, die standardmäßige deklarierte Barrierefreiheit.
Namespaces verfügen implizit über die public Barrierefreiheit. Für Namespace Deklarationen sind keine
Zugriffsmodifizierer zulässig.
Typen, die in Kompilierungs Einheiten oder Namespaces deklariert sind, können Zugriff haben public oder
internal deklarieren und erhalten standardmäßig internal deklarierten Zugriff.
Klassenmember können eine der fünf Arten von deklarierten Zugriffsmöglichkeiten aufweisen und erhalten
standardmäßig private deklarierte Zugriffsmöglichkeiten. (Beachten Sie, dass ein Typ, der als Member einer
Klasse deklariert wird, eine der fünf Arten der deklarierten Barrierefreiheit aufweisen kann, wohingegen ein
als Member eines Namespaces deklarierter Typ nur public oder deklariert werden kann internal .)
Strukturmember können public , internal oder private als deklariert werden und erhalten
standardmäßig private deklarierten Zugriff, da Strukturen implizit versiegelt sind. Strukturmember, die in
einer Struktur eingeführt werden (d. h. nicht von dieser Struktur geerbt), dürfen keine protected
Barrierefreiheit haben oder protected internal deklarieren. (Beachten Sie, dass ein Typ, der als Member
einer Struktur deklariert ist,, public internal oder als private deklariert werden kann, während ein Typ,
der als Member eines Namespaces deklariert ist, nur über public oder internal deklarierte
Zugriffsmöglichkeiten verfügen kann.)
Schnittstellenmember haben implizit die public Barrierefreiheit deklariert. Zugriffsmodifizierer sind für
Schnittstellenmember-Deklarationen unzulässig
Enumerationsmember haben implizit " public Barrierefreiheit" deklariert. Es sind keine Zugriffsmodifizierer
für Enumerationsmember zulässig.
Barrierefreiheits Domänen
Die *Barrierefreiheits Domäne _ eines Members besteht aus den (möglicherweise zusammenhängenden)
Abschnitten von Programmtext, in dem der Zugriff auf den Member zulässig ist. Zum Definieren der Zugriffs
Domäne eines Members wird ein Member als oberste Ebene bezeichnet, wenn er nicht innerhalb eines Typs
deklariert ist, und ein Member wird als geschachtelt bezeichnet, wenn er ** in einem anderen Typ deklariert
wird. Außerdem wird der Programm Text eines Programms als sämtlicher Programmtext definiert, der in allen
Quelldateien des Programms enthalten ist, und der Programmtext eines Typs wird als sämtlicher Programmtext
definiert, der in den _type_declaration * s dieses Typs enthalten ist (einschließlich, eventuell Typen, die innerhalb
des Typs geschachtelt sind).
Die Zugriffs Domäne eines vordefinierten Typs (z. b object int ., oder double ) ist unbegrenzt.
Die Zugriffs Domäne eines ungebundenen Typs der obersten Ebene T (gebundene und ungebundene Typen),
die in einem Programm deklariert ist, P wird wie folgt definiert:
Wenn die deklarierte Zugriffsart von den Wert T public hat, entspricht die Zugriffs Domäne von T dem
Programmtext von P und jedem Programm, das auf verweist P .
Wenn die deklarierte Zugriffsart von T den Wert internal hat, entspricht die Zugriffsdomäne von T dem
Programmtext von P .

Aus diesen Definitionen folgt, dass die Zugriffs Domäne eines ungebundenen Typs der obersten Ebene immer
mindestens dem Programmtext des Programms entspricht, in dem der Typ deklariert ist.
Die Zugriffs Domäne für einen konstruierten Typ T<A1, ..., An> ist die Schnittmenge der Barrierefreiheits
Domäne des ungebundenen generischen Typs T und der Barrierefreiheits Domänen der Typargumente
A1, ..., An .

Die Zugriffs Domäne eines geschachtelten Members, der M in einem Typ T innerhalb eines Programms
deklariert P ist, wird wie folgt definiert (es M kann sein, dass selbst ein Typ sein kann):
Wenn die deklarierte Zugriffsart von M den Wert public hat, entspricht die Zugriffsdomäne von M der
von T .
Wenn die deklarierte Zugriffsart von den Wert M protected internal hat, kann der D Programmtext von
P und der Programmtext eines beliebigen Typs sein, der von abgeleitet wird T , der außerhalb von
deklariert wurde P . Die Zugriffs Domäne von M ist die Schnittmenge der Zugriffs Domäne von T mit D .
Wenn die deklarierte Zugriffsart von den Wert M protected hat, können Sie den D Programmtext von T
und den Programmtext eines beliebigen Typs, der von abgeleitet ist, darstellen T . Die Zugriffs Domäne von
M ist die Schnittmenge der Zugriffs Domäne von T mit D .
Wenn die deklarierte Zugriffsart von M den Wert internal hat, entspricht die Zugriffsdomäne von M der
Schnittmenge zwischen der Zugriffsdomäne von T und dem Programmtext von P .
Wenn die deklarierte Zugriffsart von M den Wert private hat, entspricht die Zugriffsdomäne von M dem
Programmtext von T .

Aus diesen Definitionen folgt, dass die Barrierefreiheits Domäne eines in einem Bereich eingefügten Members
immer mindestens dem Programmtext des Typs entspricht, in dem der Member deklariert ist. Ferner folgt, dass
die Zugriffs Domäne eines Members nie inklusiver ist als die Zugriffs Domäne des Typs, in dem der Member
deklariert ist.
Wenn auf einen Typ oder Member M zugegriffen wird, werden die folgenden Schritte in intuitiver Hinsicht
ausgewertet, um sicherzustellen, dass der Zugriff zulässig ist:
Wenn als erstes M innerhalb eines Typs deklariert wird (im Gegensatz zu einer Kompilierungseinheit oder
einem Namespace), tritt ein Kompilierzeitfehler auf, wenn auf diesen Typ nicht zugegriffen werden kann.
Wenn den Wert M public hat, ist der Zugriff zulässig.
Andernfalls, wenn M protected internal den Wert hat, ist der Zugriff zulässig, wenn er innerhalb des
Programms auftritt, in dem M deklariert ist, oder wenn es in einer von der-Klasse abgeleiteten Klasse auftritt,
in der M deklariert wird und durch den abgeleiteten Klassentyp (geschützter Zugriff für Instanzmember)
erfolgt.
Andernfalls, wenn M protected den Wert hat, ist der Zugriff zulässig, wenn er innerhalb der Klasse auftritt,
in der M deklariert ist, oder wenn er in einer von der-Klasse abgeleiteten Klasse auftritt, in der M deklariert
ist und durch den abgeleiteten Klassentyp (geschützter Zugriff für Instanzmember) erfolgt.
Andernfalls, wenn M internal den Wert hat, ist der Zugriff zulässig, wenn er innerhalb des Programms, in
dem M deklariert ist, auftritt.
Andernfalls, wenn M private den Wert hat, ist der Zugriff zulässig, wenn er innerhalb des Typs auftritt, in
dem M deklariert ist.
Andernfalls ist der Typ oder Member nicht zugänglich, und es tritt ein Kompilierzeitfehler auf.
Im Beispiel
public class A
{
public static int X;
internal static int Y;
private static int Z;
}

internal class B
{
public static int X;
internal static int Y;
private static int Z;

public class C
{
public static int X;
internal static int Y;
private static int Z;
}

private class D
{
public static int X;
internal static int Y;
private static int Z;
}
}

die Klassen und Member haben die folgenden Barrierefreiheits Domänen:


Die Zugriffs Domäne von A und A.X ist unbegrenzt.
Die Zugriffs Domäne von A.Y , B , B.X , B.Y , B.C , B.C.X und B.C.Y ist der Programmtext des
enthaltenden Programms.
Die Zugriffs Domäne von A.Z ist der Programmtext von A .
Die Zugriffs Domäne von B.Z und B.D ist der Programmtext von B , einschließlich des Programm Texts
von B.C und B.D .
Die Zugriffs Domäne von B.C.Z ist der Programmtext von B.C .
Die Zugriffs Domäne von B.D.X und B.D.Y ist der Programmtext von B , einschließlich des Programm
Texts von B.C und B.D .
Die Zugriffs Domäne von B.D.Z ist der Programmtext von B.D .

Wie das Beispiel zeigt, ist die Zugriffs Domäne eines Members nie größer als die eines enthaltenden Typs. Wenn
z. b. alle Member X öffentlich deklarierte Barrierefreiheit haben, verfügen alle außer A.X über Barrierefreiheits
Domänen, die durch einen enthaltenden Typ eingeschränkt werden.
Wie in Membersbeschrieben, werden alle Member einer Basisklasse, mit Ausnahme von Instanzkonstruktoren,
destrukturiertoren und statischen Konstruktoren, von abgeleiteten Typen geerbt. Dies schließt sogar private
Member einer Basisklasse ein. Die Zugriffs Domäne eines privaten Members enthält jedoch nur den
Programmtext des Typs, in dem der Member deklariert ist. Im Beispiel
class A
{
int x;

static void F(B b) {


b.x = 1; // Ok
}
}

class B: A
{
static void F(B b) {
b.x = 1; // Error, x not accessible
}
}

die- B Klasse erbt den privaten Member x von der- A Klasse. Da der Member privat ist, kann nur innerhalb
der class_body von darauf zugegriffen werden A . Folglich ist der Zugriff auf b.x in der- A.F Methode
erfolgreich, schlägt in der-Methode jedoch fehl B.F .
Geschützter Zugriff für Instanzmember
Wenn auf einen protected Instanzmember außerhalb des Programm Texts der Klasse zugegriffen wird, in der er
deklariert ist, und wenn protected internal auf einen Instanzmember außerhalb des Programm Texts des
Programms zugegriffen wird, in dem es deklariert ist, muss der Zugriff innerhalb einer Klassen Deklaration
erfolgen, die von der Klasse abgeleitet ist, in der Sie deklariert ist. Darüber hinaus muss der Zugriff durch eine
Instanz dieses abgeleiteten Klassen Typs oder eines aus der Klasse erstellten Klassen Typs erfolgen. Diese
Einschränkung verhindert, dass eine abgeleitete Klasse auf geschützte Member anderer abgeleiteter Klassen
zugreift, auch wenn die Elemente von derselben Basisklasse geerbt werden.
Dabei handelt es sich um B eine Basisklasse, die einen geschützten Instanzmember deklariert M , und D eine
Klasse, die von abgeleitet wird B . In der class_body von D kann der Zugriff auf M eine der folgenden Formen
annehmen:
Eine nicht qualifizierte TYPE_NAME oder primary_expression des Formulars M .
Eine primary_expression des Formulars E.M , bereitgestellt, wenn der Typ von E T oder eine von
abgeleitete Klasse ist, T wobei T der Klassentyp D oder ein Klassentyp ist, der aus erstellt wurde. D
Eine primary_expression des Formulars base.M .

Zusätzlich zu diesen Zugriffs Formen kann eine abgeleitete Klasse auf einen geschützten Instanzkonstruktor
einer Basisklasse in einer constructor_initializer (Konstruktorinitialisierer) zugreifen.
Im Beispiel
public class A
{
protected int x;

static void F(A a, B b) {


a.x = 1; // Ok
b.x = 1; // Ok
}
}

public class B: A
{
static void F(A a, B b) {
a.x = 1; // Error, must access through instance of B
b.x = 1; // Ok
}
}

in A ist es möglich, x über Instanzen von A und B auf zuzugreifen, da der Zugriff in beiden Fällen durch
eine Instanz von A oder eine von abgeleitete Klasse erfolgt A . In B ist es jedoch nicht möglich, auf x über
eine Instanz von zuzugreifen A , da A nicht von abgeleitet ist B .
Im Beispiel

class C<T>
{
protected T x;
}

class D<T>: C<T>


{
static void F() {
D<T> dt = new D<T>();
D<int> di = new D<int>();
D<string> ds = new D<string>();
dt.x = default(T);
di.x = 123;
ds.x = "test";
}
}

die drei Zuweisungen zu x sind zulässig, da Sie alle durch Instanzen von Klassentypen erfolgen, die aus dem
generischen Typ erstellt werden.
Barrierefreiheits Einschränkungen
Mehrere Konstrukte in der Programmiersprache c# erfordern, dass ein Typ mindestens so zugänglich ist wie
ein Member oder ein anderer Typ. Ein Typ T ist mindestens so zugänglich wie ein Member oder Typ, M Wenn
die Zugriffs Domäne von T eine supermenge der Zugriffs Domäne von ist M . Mit anderen Worten: T ist
zumindest so zugänglich wie, M Wenn T in allen Kontexten, auf die zugegriffen werden kann, zugänglich ist
M .

Die folgenden Barrierefreiheits Einschränkungen sind vorhanden:


Die direkte Basisklasse eines Klassentyps muss mindesten dieselben Zugriffsmöglichkeiten wie der
Klassentyp selbst bieten.
Die explizite Basisschnittstelle eines Schnittstellentyps muss mindesten dieselben Zugriffsmöglichkeiten
bieten wie der Schnittstellentyp selbst.
Die Rückgabe- und Parametertypen eines Delegattyps müssen mindestens dieselben Zugriffsmöglichkeiten
wie der Delegattyp selbst bieten.
Der Typ einer Konstante muss mindestens dieselben Zugriffsmöglichkeiten wie die Konstante selbst bieten.
Der Typ eines Felds muss mindestens dieselben Zugriffsmöglichkeiten bieten wie das Feld selbst.
Die Rückgabe- und Parametertypen einer Methode müssen mindestens dieselben Zugriffsmöglichkeiten
bieten wie die Methode selbst.
Der Typ einer Eigenschaft muss mindestens dieselben Zugriffsmöglichkeiten bieten wie die Eigenschaft
selbst.
Der Typ eines Ereignisses muss mindestens dieselben Zugriffsmöglichkeiten bieten wie das Ereignis selbst.
Der Typ und die Parametertypen eines Indexers müssen mindestens dieselben Zugriffsmöglichkeiten bieten
wie der Indexer selbst.
Die Rückgabe- und Parametertypen eines Operators müssen mindestens dieselben Zugriffsmöglichkeiten
bieten wie der Operator selbst.
Die Parametertypen eines Instanzkonstruktors müssen mindestens so zugänglich sein wie der
Instanzkonstruktor selbst.
Im Beispiel

class A {...}

public class B: A {...}

die B -Klasse führt zu einem Kompilierzeitfehler, da A nicht mindestens so zugänglich ist wie B .
Gleiches gilt für das Beispiel

class A {...}

public class B
{
A F() {...}

internal A G() {...}

public A H() {...}


}

die H -Methode in B führt zu einem Kompilierzeitfehler, da der Rückgabetyp A nicht mindestens so


zugänglich ist wie die-Methode.

Signatures and overloading (Signaturen und Überladen)


Methoden, Instanzkonstruktoren, Indexer und Operatoren sind durch ihre Signaturen gekennzeichnet:
Die Signatur einer Methode besteht aus dem Namen der Methode, der Anzahl der Typparameter und dem
Typ und der Art (Wert, Verweis oder Ausgabe) der einzelnen formalen Parameter, die in der Reihenfolge von
links nach rechts berücksichtigt werden. Zu diesem Zweck wird jeder Typparameter der Methode, die im Typ
eines formalen Parameters vorkommt, nicht anhand seines Namens identifiziert, sondern anhand seiner
Ordinalposition in der Typargument Liste der Methode. Die Signatur einer Methode enthält nicht den
Rückgabetyp, den params Modifizierer, der für den ganz rechts Esten Parameter angegeben werden kann,
oder die optionalen Typparameter Einschränkungen.
Die Signatur eines Instanzkonstruktors besteht aus Typ und Art (Wert, Verweis oder Ausgabe) der einzelnen
formalen Parameter, die in der Reihenfolge von links nach rechts berücksichtigt werden. Die Signatur eines
Instanzkonstruktors schließt speziell den params Modifizierer nicht ein, der für den ganz rechts Esten
Parameter angegeben werden kann.
Die Signatur eines Indexers besteht aus dem Typ der einzelnen formalen Parameter, die in der Reihenfolge
von links nach rechts berücksichtigt werden. Die Signatur eines Indexers enthält weder den Elementtyp noch
den params Modifizierer, der für den ganz rechts Esten Parameter angegeben werden kann.
Die Signatur eines Operators besteht aus dem Namen des Operators und dem Typ der einzelnen formalen
Parameter, die in der Reihenfolge von links nach rechts betrachtet werden. Die Signatur eines Operators
schließt den Ergebnistyp nicht ein.
Signaturen sind der aktivierende Mechanismus für das überladen von Membern in Klassen, Strukturen und
Schnittstellen:
Das Überladen von Methoden ermöglicht einer Klasse, Struktur oder Schnittstelle das Deklarieren mehrerer
Methoden mit demselben Namen, vorausgesetzt, ihre Signaturen sind innerhalb dieser Klasse, Struktur oder
Schnittstelle eindeutig.
Das Überladen von Instanzkonstruktoren ermöglicht einer Klasse oder Struktur das Deklarieren mehrerer
Instanzkonstruktoren, vorausgesetzt, ihre Signaturen sind innerhalb dieser Klasse oder Struktur eindeutig.
Das Überladen von indexatoren ermöglicht es einer Klasse, Struktur oder Schnittstelle, mehrere Indexer zu
deklarieren, vorausgesetzt, ihre Signaturen sind innerhalb dieser Klasse, Struktur oder Schnittstelle eindeutig.
Das Überladen von Operatoren ermöglicht einer Klasse oder Struktur das Deklarieren mehrerer Operatoren
mit demselben Namen, vorausgesetzt, ihre Signaturen sind innerhalb dieser Klasse oder Struktur eindeutig.
Obwohl out und ref Parametermodifizierer als Teil einer Signatur angesehen werden, können in einem
einzelnen Typ deklarierte Member nicht in der Signatur ausschließlich von ref und abweichen out . Ein
Kompilierzeitfehler tritt auf, wenn zwei Member im gleichen Typ mit Signaturen deklariert werden, die identisch
wären, wenn alle Parameter in beiden Methoden mit out modifiziererelementen in ref Modifizierer geändert
wurden. Für andere Zwecke der Signatur Übereinstimmung (z. b. ausblenden oder überschreiben) ref out
werden und als Teil der Signatur angesehen und stimmen nicht mit einander überein. (Diese Einschränkung
besteht darin, dass c#-Programme auf einfache Weise für die Common Language Infrastructure (CLI) übersetzt
werden können, die keine Möglichkeit bietet, Methoden zu definieren, die sich ausschließlich in und
unterscheiden ref out .)
Für Signaturen werden die Typen object und dynamic als identisch betrachtet. Member, die in einem einzelnen
Typ deklariert werden, können sich daher nicht in der Signatur ausschließlich durch und unterscheiden object
dynamic .

Das folgende Beispiel zeigt eine Reihe von überladenen Methoden Deklarationen zusammen mit ihren
Signaturen.

interface ITest
{
void F(); // F()

void F(int x); // F(int)

void F(ref int x); // F(ref int)

void F(out int x); // F(out int) error

void F(int x, int y); // F(int, int)

int F(string s); // F(string)

int F(int x); // F(int) error

void F(string[] a); // F(string[])

void F(params string[] a); // F(string[]) error


}
Beachten Sie, dass alle ref -und- out Parametermodifizierer (Methoden Parameter) Teil einer Signatur sind.
Daher F(int) sind und F(ref int) eindeutige Signaturen. F(ref int) Und können jedoch F(out int) nicht
innerhalb derselben Schnittstelle deklariert werden, da sich Ihre Signaturen ausschließlich durch und
unterscheiden ref out . Beachten Sie außerdem, dass der Rückgabetyp und der- params Modifizierer nicht
Teil einer Signatur sind. Daher ist es nicht möglich, nur auf der Grundlage des Rückgabe Typs oder auf das
einschließen oder ausschließen des params Modifizierers zu überlasten. Daher führen die Deklarationen der
F(int) F(params string[]) oben genannten Methoden zu einem Kompilierzeitfehler.

Bereiche
Der *Bereich _ eines Namens ist der Bereich des Programm Texts, in dem auf die durch den Namen deklarierte
Entität verwiesen werden kann, ohne dass der Name qualifiziert ist. Bereiche können geschachtelt werden, und
ein innerer Bereich kann die Bedeutung eines Namens aus einem äußeren Gültigkeitsbereich neu deklarieren
(Dies bedeutet jedoch nicht, dass die Einschränkung, die von Deklarationen aufgrund von Deklarationen in
einem geschachtelten Block festgelegt wird, nicht möglich ist, eine lokale Variable mit dem gleichen Namen wie
eine lokale Variable in einem einschließenden Block zu deklarieren). Der Name aus dem äußeren
Gültigkeitsbereich wird dann als _ Hidden* in der vom inneren Bereich behandelten Programmtext angezeigt,
und der Zugriff auf den äußeren Namen ist nur durch Qualifizierung des Namens möglich.
Der Gültigkeitsbereich eines von einem namespace_member_declaration (Namespace Members)
deklarierten Namespace Members ohne einschließende namespace_declaration ist der gesamte
Programmtext.
Der Gültigkeitsbereich eines Namespace Members, der von einem namespace_member_declaration in einem
namespace_declaration deklariert wird, dessen voll qualifizierter Name N der namespace_body jedes
namespace_declaration ist, dessen voll qualifizierter Name ist N N , gefolgt von einem Zeitraum.
Der durch eine extern_alias_directive definierte Bereich des Namens erstreckt sich über die using_directive s,
global_attributes und namespace_member_declaration n der unmittelbar enthaltenden Kompilierungseinheit
oder des Namespace Texts. Ein extern_alias_directive führt keine neuen Member zum zugrunde liegenden
Deklarations Bereich ein. Anders ausgedrückt: eine extern_alias_directive ist nicht transitiv, sondern wirkt sich
nur auf die Kompilierungseinheit oder den Namespace Körper aus, in der Sie auftritt.
Der Gültigkeitsbereich eines Namens, der von einem using_directive definiert oder importiert wird (using-
Direktiven), erstreckt sich über die namespace_member_declaration s der compilation_unit oder
namespace_body , in der der using_directive auftritt. Eine using_directive kann NULL oder mehr Namespace-,
Typ-oder Elementnamen innerhalb eines bestimmten compilation_unit oder namespace_body bereitstellen,
führt jedoch keine neuen Member zum zugrunde liegenden Deklarations Bereich. Anders ausgedrückt: eine
using_directive ist nicht transitiv, sondern wirkt sich nur auf die compilation_unit oder namespace_body aus,
in der Sie auftritt.
Der Gültigkeitsbereich eines Typparameters, der von einer type_parameter_list in einer class_declaration
deklariert wird (Klassen Deklarationen), ist die class_base, type_parameter_constraints_clause s und
class_body dieser class_declaration.
Der Gültigkeitsbereich eines Typparameters, der von einer type_parameter_list in einer struct_declaration
(Struktur Deklarationen) deklariert wird, ist die struct_interfaces, type_parameter_constraints_clause s und
struct_body dieser struct_declaration.
Der Gültigkeitsbereich eines Typparameters, der durch eine type_parameter_list in einem
interface_declaration (Schnittstellen Deklarationen) deklariert wird, ist die interface_base,
type_parameter_constraints_clause s und interface_body dieser interface_declaration.
Der Gültigkeitsbereich eines Typparameters, der von einer type_parameter_list in einer delegate_declaration
deklariert wird (Delegatdeklarationen), ist die return_type, formal_parameter_list und
type_parameter_constraints_clause e dieses delegate_declaration.
Der Gültigkeitsbereich eines Members, der von einem class_member_declaration (Klassen Text) deklariert
wird, ist die class_body , in der die Deklaration erfolgt. Außerdem erstreckt sich der Gültigkeitsbereich eines
Klassenmembers auf die class_body der abgeleiteten Klassen, die in der Zugriffs Domäne (Barrierefreiheits
Domänen) des Members enthalten sind.
Der Gültigkeitsbereich eines Members, der von einem struct_member_declaration (Strukturmember)
deklariert wird, ist die struct_body , in der die Deklaration erfolgt.
Der Gültigkeitsbereich eines Members, der von einem enum_member_declaration (Enumerationsmember)
deklariert wird, ist die enum_body , in der die Deklaration erfolgt.
Der Gültigkeitsbereich eines in einem method_declaration (Methoden) deklarierten Parameters ist die
method_body dieser method_declaration.
Der Gültigkeitsbereich eines in einem indexer_declaration (Indexer) deklarierten Parameters ist die
accessor_declarations dieser indexer_declaration.
Der Gültigkeitsbereich eines in einem operator_declaration (Operatoren) deklarierten Parameters ist der
Block dieser operator_declaration.
Der Gültigkeitsbereich eines in einem constructor_declaration (Instanzkonstruktors) deklarierten Parameters
ist der constructor_initializer und Block von constructor_declaration.
Der Gültigkeitsbereich eines in einem lambda_expression (anonymen Funktions Ausdruck) deklarierten
Parameters ist die anonymous_function_body dieser lambda_expression
Der Gültigkeitsbereich eines in einem anonymous_method_expression (anonymen Funktions Ausdruck)
deklarierten Parameters ist der Block dieser anonymous_method_expression.
Der Gültigkeitsbereich einer Bezeichnung, die in einer labeled_statement deklariert ist (Anweisungen mit
Bezeichnung), ist der Block , in dem die Deklaration auftritt.
Der Gültigkeitsbereich einer lokalen Variablen, die in einem local_variable_declaration deklariert ist
(Deklarationen von lokalen Variablen), ist der Block, in dem die Deklaration auftritt.
Der Gültigkeitsbereich einer lokalen Variablen, die in einem switch_block einer- switch Anweisung (der
Switch-Anweisung) deklariert ist, ist die switch_block.
Der Gültigkeitsbereich einer lokalen Variablen, die in einem for_initializer einer- for Anweisung (der for-
Anweisung) deklariert wurde, ist die for_initializer, die for_condition, die for_iterator und die enthaltene
Anweisung der for Anweisung.
Der Gültigkeitsbereich einer lokalen Konstante, die in einer local_constant_declaration (lokale Konstante
Deklarationen) deklariert ist, ist der Block, in dem die Deklaration auftritt. Es handelt sich um einen
Kompilierzeitfehler, der auf eine lokale Konstante in einer Textposition verweist, die dem constant_declarator
vorangestellt ist.
Der Gültigkeitsbereich einer Variablen, die als Teil einer foreach_statement, using_statement lock_statement
oder query_expression deklariert wird, wird durch die Erweiterung des angegebenen Konstrukts bestimmt.
Innerhalb des Gültigkeits Bereichs eines Namespace, einer Klasse, einer Struktur oder eines
Enumerationsmembers kann auf das Element in einer Textposition verwiesen werden, die der Deklaration des
Members vorangestellt ist. Beispiel:

class A
{
void F() {
i = 1;
}

int i = 0;
}

Hier ist es gültig für, auf F den verwiesen i wird, bevor er deklariert wird.
Innerhalb des Gültigkeits Bereichs einer lokalen Variablen ist dies ein Kompilierzeitfehler, der auf die lokale
Variable in einer Textposition verweist, die dem local_variable_declarator der lokalen Variablen vorangestellt ist.
Beispiel:
class A
{
int i = 0;

void F() {
i = 1; // Error, use precedes declaration
int i;
i = 2;
}

void G() {
int j = (j = 1); // Valid
}

void H() {
int a = 1, b = ++a; // Valid
}
}

In der F obigen Methode verweist die erste Zuweisung zu i speziell nicht auf das Feld, das im äußeren
Gültigkeitsbereich deklariert wurde. Stattdessen verweist Sie auf die lokale Variable und führt zu einem
Kompilierzeitfehler, da Sie sich textumal der Deklaration der Variablen vorangestellt ist. In der- G Methode ist
die Verwendung von j im Initialisierer für die Deklaration von j gültig, da die Verwendung dem
local_variable_declarator nicht vorangestellt ist. In der- H Methode verweist eine nachfolgende
local_variable_declarator ordnungsgemäß auf eine lokale Variable, die in einem früheren
local_variable_declarator innerhalb desselben local_variable_declaration deklariert wurde.
Die Bereichs Regeln für lokale Variablen dienen dazu, sicherzustellen, dass die Bedeutung eines Namens, der in
einem Ausdrucks Kontext verwendet wird, innerhalb eines-Blocks immer identisch ist. Wenn der
Gültigkeitsbereich einer lokalen Variablen nur von der Deklaration bis zum Ende des Blocks erweitert werden
soll, wird im obigen Beispiel die erste Zuweisung der Instanzvariablen zugewiesen, und die zweite Zuweisung
würde der lokalen Variablen zugewiesen, was möglicherweise zu Kompilier Zeitfehlern führt, wenn die
Anweisungen des Blocks später neu angeordnet werden.
Die Bedeutung eines Namens innerhalb eines-Blocks kann sich je nach Kontext unterscheiden, in dem der Name
verwendet wird. Im Beispiel

using System;

class A {}

class Test
{
static void Main() {
string A = "hello, world";
string s = A; // expression context

Type t = typeof(A); // type context

Console.WriteLine(s); // writes "hello, world"


Console.WriteLine(t); // writes "A"
}
}

der Name A wird in einem Ausdrucks Kontext verwendet, um auf die lokale Variable A und in einem
typkontext zu verweisen, um auf die Klasse zu verweisen A .
Namens ausblenden
Der Gültigkeitsbereich einer Entität umfasst in der Regel mehr Programmtext als der Deklarations Bereich der
Entität. Der Gültigkeitsbereich einer Entität kann insbesondere Deklarationen enthalten, die neue Deklarations
Bereiche mit Entitäten mit demselben Namen enthalten. Diese Deklarationen bewirken, dass die ursprüngliche
Entität *Hidden _ wird. Umgekehrt wird eine Entität als _ sichtbar* bezeichnet, wenn Sie nicht ausgeblendet ist.
Das Ausblenden von Namen tritt auf, wenn sich Bereiche überlappen Die Merkmale der beiden Arten von
ausblenden werden in den folgenden Abschnitten beschrieben.
Ausblenden durch Schachtelung
Der Name, der durch Schachtelung ausgeblendet wird, kann als Ergebnis der Schachtelung von Namespaces
oder Typen innerhalb von Namespaces auftreten, als Ergebnis der Schachtelung von Typen in Klassen oder
Strukturen, und als Ergebnis von Parameter-und lokalen Variablen Deklarationen.
Im Beispiel

class A
{
int i = 0;

void F() {
int i = 1;
}

void G() {
i = 1;
}
}

innerhalb der- F Methode wird die Instanzvariable i von der lokalen Variablen ausgeblendet i , aber
innerhalb der- G Methode i verweist immer noch auf die-Instanzvariable.
Wenn ein Name in einem inneren Gültigkeitsbereich einen Namen in einem äußeren Gültigkeitsbereich verbirgt,
werden alle überladenen Vorkommen dieses Namens ausgeblendet. Im Beispiel

class Outer
{
static void F(int i) {}

static void F(string s) {}

class Inner
{
void G() {
F(1); // Invokes Outer.Inner.F
F("Hello"); // Error
}

static void F(long l) {}


}
}

der Aufruf F(1) Ruft den F in deklarierten auf, Inner da alle äußeren Vorkommen von F durch die innere
Deklaration ausgeblendet werden. Aus demselben Grund führt der Aufruf zu F("Hello") einem
Kompilierzeitfehler.
Ausblenden durch Vererbung
Das Ausblenden des Namens durch Vererbung tritt auf, wenn Klassen oder Strukturen Namen, die von
Basisklassen geerbt wurden, neu deklarieren. Diese Art von namens ausblenden hat eine der folgenden Formen:
Eine Konstante, ein Feld, eine Eigenschaft, ein Ereignis oder ein Typ, die in einer Klasse oder Struktur
eingeführt wurde, verbergen alle Basisklassenmember mit demselben Namen.
Eine Methode, die in einer Klasse oder Struktur eingeführt wurde, verbirgt alle nicht-Method-
Basisklassenmember mit demselben Namen und alle Basisklassen Methoden mit der gleichen Signatur
(Methodenname und Parameter Anzahl, Modifizierer und Typen).
Ein Indexer, der in einer Klasse oder Struktur eingeführt wurde, verbirgt alle Basisklassenindexer mit der
gleichen Signatur (Parameter Anzahl und-Typen).
Die Regeln für Operator Deklarationen (Operatoren) machen es für eine abgeleitete Klasse unmöglich, einen
Operator mit derselben Signatur wie ein Operator in einer Basisklasse zu deklarieren. Folglich verbergen
Operatoren nie eine andere.
Im Gegensatz zum Ausblenden eines Namens aus einem äußeren Gültigkeitsbereich bewirkt das Ausblenden
eines zugänglichen namens aus einem geerbten Bereich, dass eine Warnung ausgegeben wird. Im Beispiel

class Base
{
public void F() {}
}

class Derived: Base


{
public void F() {} // Warning, hiding an inherited name
}

die Deklaration von F in Derived bewirkt, dass eine Warnung gemeldet wird. Das Ausblenden eines geerbten
Namens ist kein Fehler, da dies die getrennte Weiterentwicklung von Basisklassen ausschließen würde. Die
obige Situation könnte z. b. eintreten, weil eine neuere Version von Base eine Methode eingeführt hat F , die
in einer früheren Version der Klasse nicht vorhanden war. Hätte die obige Situation einen Fehler verursacht,
kann jede Änderung, die an einer Basisklasse in einer separat versionierten Klassenbibliothek vorgenommen
wurde, möglicherweise dazu führen, dass abgeleitete Klassen ungültig werden.
Die Warnung, die durch das Ausblenden eines geerbten Namens verursacht wurde, kann mithilfe des- new
Modifizierers entfernt werden:

class Base
{
public void F() {}
}

class Derived: Base


{
new public void F() {}
}

Der new -Modifizierer gibt an, dass der F in Derived "New" ist, und dass er tatsächlich dazu gedacht ist, den
geerbten Member auszublenden.
Eine Deklaration eines neuen Members verbirgt einen geerbten Member nur innerhalb des Gültigkeits Bereichs
des neuen Members.
class Base
{
public static void F() {}
}

class Derived: Base


{
new private static void F() {} // Hides Base.F in Derived only
}

class MoreDerived: Derived


{
static void G() { F(); } // Invokes Base.F
}

Im obigen Beispiel blendet die Deklaration von F in Derived den F aus, der von geerbt wurde Base , aber da
der neue F in Derived privaten Zugriff hat, wird sein Bereich nicht auf erweitert MoreDerived . Daher ist der
Aufruf F() in MoreDerived.G gültig und wird aufgerufen Base.F .

Namespace and type names (Namespace- und Typnamen)


Mehrere Kontexte in einem c#-Programm erfordern, dass ein namespace_name oder eine TYPE_NAME
angegeben wird.

namespace_name
: namespace_or_type_name
;

type_name
: namespace_or_type_name
;

namespace_or_type_name
: identifier type_argument_list?
| namespace_or_type_name '.' identifier type_argument_list?
| qualified_alias_member
;

Eine namespace_name ist eine namespace_or_type_name , die auf einen Namespace verweist. Nach der
Auflösung, wie unten beschrieben, muss die namespace_or_type_name eines namespace_name auf einen
Namespace verweisen, andernfalls tritt ein Kompilierzeitfehler auf. In einem namespace_name können keine
Typargumente (Typargumente) vorhanden sein (nur Typen können Typargumente aufweisen).
Ein TYPE_NAME ist eine namespace_or_type_name , die auf einen Typ verweist. Nach der Auflösung, wie unten
beschrieben, muss die namespace_or_type_name eines TYPE_NAME auf einen Typ verweisen, andernfalls tritt
ein Kompilierzeitfehler auf.
Wenn die namespace_or_type_name ein Qualified-Alias-Member ist, wird ihre Bedeutung wie in
Namespacealias-Qualifizierernbeschrieben beschrieben. Andernfalls hat eine namespace_or_type_name eine
von vier Formen:
I
I<A1, ..., Ak>
N.I
N.I<A1, ..., Ak>

Wenn I ein einzelner Bezeichner ist, N ist ein namespace_or_type_name und <A1, ..., Ak> ein optionaler
type_argument_list. Wenn keine type_argument_list angegeben ist, sollten k Sie den Wert 0 (null) angeben.
Die Bedeutung eines namespace_or_type_name wird wie folgt bestimmt:
Wenn die namespace_or_type_name das Formular oder das folgende Format hat I I<A1, ..., Ak> :
Wenn K 0 (null) ist und die namespace_or_type_name in einer generischen Methoden Deklaration
(Methoden) enthalten ist und diese Deklaration einen Typparameter (Typparameter) mit dem Namen
enthält I , verweist der namespace_or_type_name auf diesen Typparameter.
Andernfalls, wenn das namespace_or_type_name in einer Typdeklaration angezeigt wird, dann für
jeden Instanztyp T (der Instanztyp), beginnend mit dem Instanztyp dieser Typdeklaration und mit
dem Instanztyp jeder einschließenden Klasse oder Struktur Deklaration (sofern vorhanden):
Wenn K 0 (null) ist und die Deklaration von T einen Typparameter mit dem Namen enthält
I , verweist der namespace_or_type_name auf diesen Typparameter.
Andernfalls T I K verweist der namespace_or_type_name auf den Typ, der mit den
angegebenen Typargumenten erstellt wurde, wenn die namespace_or_type_name im Text der
Typdeklaration angezeigt wird und oder einer der zugehörigen Basis Typen einen
geschachtelten zugänglichen Typ mit den Parametern "Name" und "Type" enthält. Wenn mehr
als ein solcher Typ vorhanden ist, wird der in einem stärker abgeleiteten Typ deklarierte Typ
ausgewählt. Beachten Sie, dass nicht-Typmember (Konstanten, Felder, Methoden, Eigenschaften,
Indexer, Operatoren, Instanzkonstruktoren, Dekonstruktoren und statische Konstruktoren) und
Typmember mit einer anderen Anzahl von Typparametern ignoriert werden, wenn die
Bedeutung der namespace_or_type_name bestimmt wird.
Wenn die vorherigen Schritte nicht erfolgreich waren, wird für jeden Namespace N , beginnend mit
dem Namespace, in dem der namespace_or_type_name auftritt, der Fortschritt mit jedem
einschließenden Namespace (sofern vorhanden) und mit dem globalen Namespace eine Auswertung
der folgenden Schritte ausgeführt, bis eine Entität gefunden wird:
Wenn K 0 (null) ist und I der Name eines Namespace in ist N , dann gilt Folgendes:
Wenn der Speicherort, an dem der namespace_or_type_name auftritt, von einer
Namespace Deklaration für eingeschlossen ist N und die Namespace Deklaration eine
extern_alias_directive oder using_alias_directive enthält, die den Namen I einem
Namespace oder Typ zuordnet, ist die namespace_or_type_name mehrdeutig, und es
tritt ein Kompilierungsfehler auf.
Andernfalls verweist der namespace_or_type_name auf den Namespace mit dem
Namen I in N .
Wenn andernfalls einen zugreif baren N Typ mit den Parametern "Name" und "Type" enthält
I K , dann:
Wenn K 0 (null) ist und die Position, an der die namespace_or_type_name auftritt, von
einer Namespace Deklaration für eingeschlossen wird N und die Namespace
Deklaration eine extern_alias_directive oder using_alias_directive enthält, die den Namen
I einem Namespace oder Typ zuordnet, ist die namespace_or_type_name mehrdeutig
und ein Kompilierzeitfehler aufgetreten.
Andernfalls verweist der namespace_or_type_name auf den Typ, der mit den
angegebenen Typargumenten erstellt wurde.
Andernfalls ist der Speicherort, an dem der namespace_or_type_name auftritt, von einer
Namespace Deklaration für Folgendes eingeschlossen N :
Wenn K 0 (null) ist und die-Namespace Deklaration eine extern_alias_directive oder
using_alias_directive enthält, die den Namen I einem importierten Namespace oder
Typ zuordnet, verweist der namespace_or_type_name auf diesen Namespace oder Typ.
Wenn die von den using_namespace_directive s und using_alias_directive s der
Namespace Deklaration importierten Namespaces und Typdeklarationen genau einen
zugreif baren Typ aufweisen, der über die Parameter "Name" und "Type" verfügt I K
, verweist der namespace_or_type_name auf diesen Typ, der mit den angegebenen
Typargumenten erstellt wurde.
Wenn die Namespaces und Typdeklarationen, die von den using_namespace_directive s
und using_alias_directive s der Namespace Deklaration importiert werden, mehr als
einen zugreif baren Typ aufweisen I , der über namens-und K Typparameter
verfügt, ist die namespace_or_type_name mehrdeutig, und es tritt ein Fehler auf.
Andernfalls ist der namespace_or_type_name nicht definiert, und es tritt ein Kompilierzeitfehler auf.
Andernfalls hat der namespace_or_type_name das Format N.I oder das Formular N.I<A1, ..., Ak> . N
wird zuerst als namespace_or_type_name aufgelöst. Wenn die Auflösung von N nicht erfolgreich ist, tritt ein
Kompilierzeitfehler auf. Andernfalls N.I wird oder N.I<A1, ..., Ak> wie folgt aufgelöst:
Wenn K 0 (null) ist und N auf einen Namespace verweist und N einen schsted Namespace mit dem
Namen enthält I , verweist der namespace_or_type_name auf diesen schsted Namespace.
Wenn N auf einen Namespace verweist und einen zugreif baren N Typ mit den Parametern "Name"
und "Type" enthält I K , verweist der namespace_or_type_name auf diesen Typ, der mit den
angegebenen Typargumenten erstellt wurde.
Wenn sich andernfalls N auf eine (möglicherweise konstruiertes) Klasse oder einen Strukturtyp
bezieht und N oder eine der zugehörigen Basisklassen einen für den nsted Typ zugänglichen Typ I
K mit den Parametern Name und Type enthält, verweist der namespace_or_type_name auf diesen
Typ, der mit den angegebenen Typargumenten erstellt wurde. Wenn mehr als ein solcher Typ
vorhanden ist, wird der in einem stärker abgeleiteten Typ deklarierte Typ ausgewählt. Beachten Sie
Folgendes: Wenn die Bedeutung von N.I als Teil der Auflösung der Basisklassen Spezifikation von
bestimmt wird, N gilt die direkte Basisklasse von als N Object (Basisklassen).
Andernfalls N.I ist ein ungültiger namespace_or_type_name, und es tritt ein Kompilierzeitfehler auf.
Ein namespace_or_type_name darf nur dann auf eine statische Klasse (statische Klassen) verweisen, wenn
Der namespace_or_type_name ist T ein namespace_or_type_name des Formulars. T.I
Der namespace_or_type_name ist das T in einem typeof_expression (Argument Listen1) im Formular
typeof(T) .

Vollqualifizierte Namen
Jeder Namespace und Typ verfügt über einen voll qualifizier ten Namen , der den Namespace oder den Typ
unter allen anderen eindeutig identifiziert. Der voll qualifizierte Name eines Namespaces oder Typs N wird wie
folgt bestimmt:
Wenn N ein Member des globalen Namespace ist, lautet der voll qualifizierte Name N .
Andernfalls lautet der voll qualifizierte Name S.N , wobei S der voll qualifizierte Name des Namespace
oder Typs ist, in dem N deklariert wird.

Der voll qualifizierte Name von N ist also der vollständige hierarchische Pfad der Bezeichner, die zu N
beginnen, beginnend mit dem globalen Namespace. Da jeder Member eines Namespaces oder Typs einen
eindeutigen Namen haben muss, folgt der voll qualifizierte Name eines Namespace oder Typs immer eindeutig.
Das folgende Beispiel zeigt mehrere Namespace-und Typdeklarationen zusammen mit den zugehörigen voll
qualifizierten Namen.
class A {} // A

namespace X // X
{
class B // X.B
{
class C {} // X.B.C
}

namespace Y // X.Y
{
class D {} // X.Y.D
}
}

namespace X.Y // X.Y


{
class E {} // X.Y.E
}

Automatische Speicherverwaltung
C# verwendet die automatische Speicherverwaltung, sodass Entwickler den von Objekten belegten
Arbeitsspeicher manuell zuordnen und freigeben können. Automatische Speicher Verwaltungsrichtlinien
werden von einem Garbage Collector implementiert. Der Lebenszyklus der Speicherverwaltung eines Objekts
lautet wie folgt:
1. Wenn das Objekt erstellt wird, wird Arbeitsspeicher zugeordnet, der Konstruktor wird ausgeführt, und das
Objekt wird als Live-Objekt betrachtet.
2. Wenn auf das Objekt oder einen Teil davon nicht durch eine mögliche Fortsetzung der Ausführung
zugegriffen werden kann, abgesehen von der Ausführung von dedededededektoren, wird das Objekt als
nicht mehr verwendet und ist für die Zerstörung infrage. Der c#-Compiler und der Garbage Collector können
Code analysieren, um zu bestimmen, welche Verweise auf ein Objekt in Zukunft verwendet werden können.
Wenn beispielsweise eine lokale Variable, die sich im Gültigkeitsbereich befindet, der einzige vorhandene
Verweis auf ein Objekt ist, aber auf diese lokale Variable in keiner möglichen Fortsetzung der Ausführung
vom aktuellen Ausführungs Punkt in der Prozedur verwiesen wird, kann das Garbage Collector das Objekt
nicht mehr in Gebrauch behandeln.
3. Sobald das Objekt für die Zerstörung geeignet ist, wird zu einem späteren Zeitpunkt der Dekonstruktor
(Dekonstruktoren) für das Objekt ausgeführt. Unter normalen Umständen wird der debugtor für das Objekt
nur einmal ausgeführt, obwohl Implementierungs spezifische APIs das Überschreiben dieses Verhaltens
zulassen können.
4. Sobald der Dekonstruktor für ein Objekt ausgeführt wird, und der Zugriff auf das Objekt oder einen Teil
davon durch eine mögliche Fortsetzung der Ausführung (einschließlich der Ausführung von
Dekonstruktoren) nicht möglich ist, wird das Objekt als nicht zugänglich angesehen, und das Objekt wird für
die Auflistung qualifiziert.
5. Schließlich gibt der Garbage Collector zu einem späteren Zeitpunkt, nachdem das Objekt für die Auflistung
infrage kommt, den diesem Objekt zugeordneten Arbeitsspeicher frei.
Der Garbage Collector verwaltet Informationen zur Objekt Verwendung und verwendet diese Informationen,
um Entscheidungen hinsichtlich der Speicherverwaltung zu treffen, z. b. wo im Arbeitsspeicher ein neu erstelltes
Objekt zu finden ist, wann ein Objekt verschoben werden soll und wann ein Objekt nicht mehr verwendet wird
oder nicht.
Wie andere Sprachen, die voraussetzen, dass ein Garbage Collector vorhanden ist, wurde c# so entworfen, dass
die Garbage Collector eine Vielzahl von Richtlinien für die Speicherverwaltung implementieren kann.
Beispielsweise ist es für c# nicht erforderlich, dass Dekonstruktoren ausgeführt werden oder dass Objekte
gesammelt werden, sobald Sie qualifiziert sind oder dass Dekonstruktoren in einer bestimmten Reihenfolge
oder in einem bestimmten Thread ausgeführt werden.
Das Verhalten des Garbage Collector kann in gewissem Maße über statische Methoden in der-Klasse gesteuert
werden System.GC . Diese Klasse kann verwendet werden, um eine Auflistung anzufordern, Dekonstruktoren
auszuführen (oder nicht ausgeführt) usw.
Da die Garbage Collector den Breitengrad der Entscheidung, wann Objekte gesammelt werden sollen, und die
Ausführung von Debuggern unterstützt, kann eine konforme Implementierung eine Ausgabe ergeben, die sich
von der im folgenden Code gezeigten unterscheidet. Das Programm

using System;

class A
{
~A() {
Console.WriteLine("Destruct instance of A");
}
}

class B
{
object Ref;

public B(object o) {
Ref = o;
}

~B() {
Console.WriteLine("Destruct instance of B");
}
}

class Test
{
static void Main() {
B b = new B(new A());
b = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}

erstellt eine Instanz der A -Klasse und eine Instanz der-Klasse B . Diese Objekte sind für Garbage Collection
qualifiziert, wenn der b Wert der Variablen der Wert zugewiesen wird null . ab diesem Zeitpunkt ist es für
keinen Benutzer geschriebenen Code unmöglich, darauf zuzugreifen. Die Ausgabe kann entweder

Destruct instance of A
Destruct instance of B

oder

Destruct instance of B
Destruct instance of A

Da in der Sprache keine Einschränkungen für die Reihenfolge auferlegt werden, in der Objekte in die Garbage
Collection aufgenommen werden.
In einigen Fällen kann es wichtig sein, den Unterschied zwischen "berechtigte für Zerstörung" und "berechtigte
Sammlung" zu unterscheiden. Beispiel:
using System;

class A
{
~A() {
Console.WriteLine("Destruct instance of A");
}

public void F() {


Console.WriteLine("A.F");
Test.RefA = this;
}
}

class B
{
public A Ref;

~B() {
Console.WriteLine("Destruct instance of B");
Ref.F();
}
}

class Test
{
public static A RefA;
public static B RefB;

static void Main() {


RefB = new B();
RefA = new A();
RefB.Ref = RefA;
RefB = null;
RefA = null;

// A and B now eligible for destruction


GC.Collect();
GC.WaitForPendingFinalizers();

// B now eligible for collection, but A is not


if (RefA != null)
Console.WriteLine("RefA is not null");
}
}

Wenn im obigen Programm der Garbage Collector den debugtor von A vor dem debugtor von ausführen
möchte B , kann die Ausgabe dieses Programms wie folgt lauten:

Destruct instance of A
Destruct instance of B
A.F
RefA is not null

Beachten Sie, dass die-Instanz zwar A nicht in Gebrauch war und A der Dekonstruktor ausgeführt wurde, aber
es ist weiterhin möglich, dass Methoden von A (in diesem Fall F ) von einem anderen Dekonstruktor
aufgerufen werden. Beachten Sie außerdem, dass das Ausführen eines Dekonstruktors dazu führen kann, dass
ein Objekt erneut aus dem Haupt-Programm verwendet werden kann. In diesem Fall hat der B Dekonstruktor
von ausgeführt, dass eine Instanz von A , die zuvor nicht verwendet wurde, über den Live Verweis zugänglich
ist Test.RefA . Nach dem-Aufrufwert WaitForPendingFinalizers ist die Instanz von für die-Auflistung B
qualifiziert, aber die-Instanz A ist aufgrund des-Verweises nicht Test.RefA .
Um Verwirrung und unerwartetes Verhalten zu vermeiden, ist es im Allgemeinen eine gute Idee, dass
debugtoren nur die Bereinigung für Daten ausführen, die in den eigenen Feldern Ihres Objekts gespeichert sind,
und keine Aktionen für referenzierte Objekte oder statische Felder durchführen.
Eine Alternative zur Verwendung von Dekonstruktoren besteht darin, dass eine Klasse die- System.IDisposable
Schnittstelle implementiert. Dadurch kann der Client des Objekts bestimmen, wann die Ressourcen des Objekts
freigegeben werden sollen, in der Regel durch Zugriff auf das Objekt als Ressource in einer- using Anweisung
(using-Anweisung).

Ausführungsreihenfolge
Die Ausführung eines c#-Programms wird so fortgesetzt, dass die Nebeneffekte der einzelnen ausführenden
Threads an kritischen Ausführungs Punkten beibehalten werden. Ein Nebeneffekt wird als Lese-oder
Schreibvorgang eines flüchtigen Felds, eines Schreibzugriffs auf eine nicht flüchtige Variable, eines
Schreibzugriffs auf eine externe Ressource und das Auslösen einer Ausnahme definiert. Die kritischen
Ausführungs Punkte, bei denen die Reihenfolge dieser Nebeneffekte beibehalten werden muss, sind Verweise
auf flüchtige Felder (flüchtige Felder), lock Anweisungen (lock-Anweisung) und Thread Erstellung und-
Beendigung. In der Ausführungsumgebung kann die Reihenfolge der Ausführung eines c#-Programms
geändert werden, wobei die folgenden Einschränkungen gelten:
Die Daten Abhängigkeit wird innerhalb eines Ausführungs Threads beibehalten. Das heißt, der Wert jeder
Variablen wird berechnet, als ob alle Anweisungen im Thread in der ursprünglichen Programm Reihenfolge
ausgeführt wurden.
Die Regeln für die Initialisierungs Reihenfolge werden beibehalten (Feld Initialisierung und
Variableninitialisierer).
Die Reihenfolge von Nebeneffekten wird in Bezug auf flüchtige Lese-und Schreibvorgänge (flüchtige Felder)
beibehalten. Darüber hinaus muss die Ausführungsumgebung einen Teil eines Ausdrucks nicht auswerten,
wenn er ableiten kann, dass der Wert des Ausdrucks nicht verwendet wird und keine erforderlichen
Nebeneffekte erzeugt werden (einschließlich der durch Aufrufen einer Methode oder zugreifen auf ein
flüchtiges Feld verursachten). Wenn die Programmausführung durch ein asynchrones Ereignis (z. b. eine von
einem anderen Thread ausgelöste Ausnahme) unterbrochen wird, ist es nicht sichergestellt, dass die
wahrnehmbaren Nebeneffekte in der ursprünglichen Programm Reihenfolge sichtbar sind.
Typen
04.11.2021 • 65 minutes to read

Die Typen der Programmiersprache c# sind in zwei Hauptkategorien unterteilt: Wer ttypen _ und _Verweis
Typen*. Sowohl Werttypen als auch Verweis Typen können generische Typen sein, die einen oder mehrere _
*-Typparameter annehmen * *. Typparameter können sowohl Werttypen als auch Verweis Typen bestimmen.

type
: value_type
| reference_type
| type_parameter
| type_unsafe
;

Die letzte Kategorie von Typen, Zeiger, ist nur in unsicherem Code verfügbar. Dies wird in Zeiger
Typenausführlicher erläutert.
Werttypen unterscheiden sich von Verweis Typen in den Variablen der Werttypen, die Ihre Daten direkt
enthalten, wohingegen Variablen der Verweis Typen *Ver weise _ auf die Daten speichern, letztere werden als _
Objects * bezeichnet. Bei Verweis Typen können zwei Variablen auf das gleiche Objekt verweisen, und so können
Vorgänge in einer Variablen das Objekt beeinflussen, auf das von der anderen Variablen verwiesen wird. Bei
Werttypen verfügen die Variablen jeweils über eine eigene Kopie der Daten, und es ist nicht möglich, dass sich
der Vorgang auf einen anderen auswirkt.
Das Typsystem von c# ist einheitlich, sodass ein Wert eines beliebigen Typs als Objekt behandelt werden kann.
Jeder Typ in C# ist direkt oder indirekt vom object -Klassentyp abgeleitet, und object ist die ultimative
Basisklasse aller Typen. Werte von Verweistypen werden als Objekte behandelt, indem die Werte einfach als Typ
object angezeigt werden. Werte von Werttypen werden als Objekte behandelt, indem Boxing-und Unboxing-
Vorgänge (Boxing und Unboxing) durchgeführt werden.

Werttypen
Ein Werttyp ist entweder ein Strukturtyp oder ein Enumerationstyp. C# stellt einen Satz vordefinierter
Strukturtypen bereit, die als einfache Typen bezeichnet werden. Die einfachen Typen werden mithilfe von
reservierten Wörtern identifiziert.
value_type
: struct_type
| enum_type
;

struct_type
: type_name
| simple_type
| nullable_type
;

simple_type
: numeric_type
| 'bool'
;

numeric_type
: integral_type
| floating_point_type
| 'decimal'
;

integral_type
: 'sbyte'
| 'byte'
| 'short'
| 'ushort'
| 'int'
| 'uint'
| 'long'
| 'ulong'
| 'char'
;

floating_point_type
: 'float'
| 'double'
;

nullable_type
: non_nullable_value_type '?'
;

non_nullable_value_type
: type
;

enum_type
: type_name
;

Anders als bei einer Variablen eines Verweis Typs kann eine Variable eines Werttyps den Wert null nur
enthalten, wenn der Werttyp ein Typ ist, der NULL-Werte zulässt. Für jeden Werttyp, der nicht auf NULL
festgelegt werden kann, gibt es einen entsprechenden Werte zulässt-Werttyp, der denselben Satz von Werten
und den Wert bezeichnet null .
Durch die Zuweisung zu einer Variablen eines Werttyps wird eine Kopie des zugewiesenen Werts erstellt. Dies
unterscheidet sich von der Zuweisung zu einer Variablen eines Verweis Typs, die den Verweis, aber nicht das
durch den Verweis identifizierte Objekt kopiert.
Der Typ "System. ValueType"
Alle Werttypen erben implizit von der-Klasse System.ValueType , die wiederum von der-Klasse erbt object . Es
ist nicht möglich, dass ein Typ von einem Werttyp abgeleitet wird. Werttypen sind daher implizit versiegelt
(versiegelte Klassen).
Beachten Sie, dass System.ValueType nicht selbst ein value_type ist. Vielmehr handelt es sich um eine class_type
, von der alle value_type s automatisch abgeleitet werden.
Standardkonstruktoren
Alle Werttypen deklarieren implizit einen öffentlichen Parameter losen Instanzenkonstruktor, der als
*Standardkonstruktor _ bezeichnet wird. Der Standardkonstruktor gibt eine NULL initialisierte-Instanz zurück,
die als _-Standardwert* für den Werttyp bekannt ist:
Der Standardwert für alle Simple_Type s ist der Wert, der von einem Bitmuster aller Nullen erzeugt wird:
Für sbyte , byte , short , ushort , int , uint , long und ulong ist der Standardwert 0 .
char Der Standardwert für ist '\x0000' .
float Der Standardwert für ist 0.0f .
double Der Standardwert für ist 0.0d .
decimal Der Standardwert für ist 0.0m .
bool Der Standardwert für ist false .
Bei einem enum_type E ist der Standardwert 0 , der in den-Typ konvertiert wird E .
Bei einem struct_type ist der Standardwert der Wert, der erzeugt wird, indem alle Werttyp Felder auf ihren
Standardwert und alle Verweistyp Felder auf festgelegt werden null .
Bei einem- nullable_type ist der Standardwert eine-Instanz, für die die HasValue -Eigenschaft false ist und
die- Value Eigenschaft nicht definiert ist. Der Standardwert wird auch als NULL- Wer t des Typs bezeichnet,
der NULL-Werte zulässt.
Wie jeder andere Instanzkonstruktor wird der Standardkonstruktor eines Werttyps mit dem- new Operator
aufgerufen. Aus Effizienzgründen ist diese Anforderung nicht dafür vorgesehen, dass die Implementierung einen
konstruktorbefehl generiert. Im folgenden Beispiel i werden Variablen und j beide mit 0 (null) initialisiert.

class A
{
void F() {
int i = 0;
int j = new int();
}
}

Da jeder Werttyp implizit über einen öffentlichen Parameter losen Instanzenkonstruktor verfügt, ist es nicht
möglich, dass ein Strukturtyp eine explizite Deklaration eines Parameter losen Konstruktors enthält. Ein
Strukturtyp ist jedoch zulässig, um parametrisierte Instanzkonstruktoren (Konstruktoren) zu deklarieren.
Strukturtypen
Ein Strukturtyp ist ein Werttyp, der Konstanten, Felder, Methoden, Eigenschaften, Indexer, Operatoren,
Instanzkonstruktoren, statische Konstruktoren und Strukturtypen deklarieren kann. Die Deklaration von
Strukturtypen wird in Struktur Deklarationenbeschrieben.
Einfache Typen
C# stellt einen Satz vordefinierter Strukturtypen bereit, die als einfache Typen bezeichnet werden. Die
einfachen Typen werden mithilfe von reservierten Wörtern identifiziert. diese reservierten Wörter sind jedoch
einfach Aliase für vordefinierte Strukturtypen im- System Namespace, wie in der folgenden Tabelle beschrieben.

RESERVIERT ES W O RT A L IA S- T Y P

sbyte System.SByte
RESERVIERT ES W O RT A L IA S- T Y P

byte System.Byte

short System.Int16

ushort System.UInt16

int System.Int32

uint System.UInt32

long System.Int64

ulong System.UInt64

char System.Char

float System.Single

double System.Double

bool System.Boolean

decimal System.Decimal

Da ein einfacher Typ einen Strukturtyp Aliase, verfügt jeder einfache Typ über Member. Beispielsweise int
verfügt über die in deklarierten Member System.Int32 und die von geerbten Member System.Object , und die
folgenden Anweisungen sind zulässig:

int i = int.MaxValue; // System.Int32.MaxValue constant


string s = i.ToString(); // System.Int32.ToString() instance method
string t = 123.ToString(); // System.Int32.ToString() instance method

Die einfachen Typen unterscheiden sich von anderen Strukturtypen dadurch, dass sie bestimmte zusätzliche
Vorgänge ermöglichen:
Bei den meisten einfachen Typen können Werte erstellt werden, indem Literale (Literale) geschrieben
werden. Beispielsweise 123 ist ein Literaltyp, int und 'a' ist ein Literaltyp char . C# stellt im
Allgemeinen keine Bereitstellung von literalen von Strukturtypen bereit, und nicht standardmäßige Werte
anderer Strukturtypen werden letztendlich immer durch Instanzkonstruktoren dieser Strukturtypen erstellt.
Wenn es sich bei den Operanden eines Ausdrucks um einfache Typkonstanten handelt, kann der Compiler
den Ausdruck zur Kompilierzeit auswerten. Ein solcher Ausdruck wird als constant_expression (Konstante
Ausdrücke) bezeichnet. Ausdrücke mit Operatoren, die von anderen Strukturtypen definiert werden, werden
nicht als Konstante Ausdrücke betrachtet.
Mithilfe const von Deklarationen ist es möglich, Konstanten der einfachen Typen (Konstanten) zu
deklarieren. Es ist nicht möglich, Konstanten anderer Strukturtypen zu haben, aber es wird ein ähnlicher
Effekt durch static readonly Felder bereitgestellt.
Konvertierungen, die einfache Typen umfassen, können an der Auswertung von Konvertierungs Operatoren
teilnehmen, die von anderen Strukturtypen definiert werden, aber ein benutzerdefinierter Konvertierungs
Operator kann nie an der Auswertung eines anderen benutzerdefinierten Operators teilnehmen
(Auswertung benutzerdefinierter Konvertierungen).
Ganzzahlige Typen
C# unterstützt neun ganzzahlige Typen: sbyte , byte , short , ushort , int , uint , long , ulong und char
. Die ganzzahligen Typen weisen die folgenden Größen und Wertebereiche auf:
Der sbyte -Typ stellt signierte 8-Bit-Ganzzahlen mit Werten zwischen-128 und 127 dar.
Der- byte Typ stellt nicht signierte 8-Bit-Ganzzahlen mit Werten zwischen 0 und 255 dar.
Der short -Typ stellt signierte 16-Bit-Ganzzahlen mit Werten zwischen-32768 und 32767 dar.
Der- ushort Typ stellt ganze 16-Bit-Ganzzahlen ohne Vorzeichen mit Werten zwischen 0 und 65535 dar.
Der- int Typ stellt signierte 32-Bit-Ganzzahlen mit Werten zwischen-2147483648 und 2147483647 dar.
Der uint -Typ stellt nicht signierte 32-Bit-Ganzzahlen mit Werten zwischen 0 und 4294967295 dar.
Der- long Typ stellt signierte 64-Bit-Ganzzahlen mit Werten zwischen-9.223.372.036.854.775.808 und
9223372036854775807 dar.
Der ulong -Typ stellt nicht signierte 64-Bit-Ganzzahlen mit Werten zwischen 0 und
18446744073709551615 dar.
Der- char Typ stellt ganze 16-Bit-Ganzzahlen ohne Vorzeichen mit Werten zwischen 0 und 65535 dar. Die
Menge der darstellbaren Werte für den char -Typ stimmt mit dem Unicode-Zeichensatz überein. Obwohl
char die gleiche Darstellung wie hat ushort , sind nicht alle Vorgänge zulässig, die für einen Typ zulässig
sind.
Der unäre und binäre Operator des ganzzahligen Typs funktionieren immer mit der signierten 32-Bit-
Genauigkeit, der 32-Bit-Genauigkeit ohne Vorzeichen, der 64-Bit-Genauigkeit mit Vorzeichen oder der 64-Bit-
Genauigkeit ohne Vorzeichen:
Für die unären + ~ Operatoren und wird der Operand in den Typ konvertiert T , wobei T der erste von
int ,, und ist, der uint long ulong alle möglichen Werte des Operanden vollständig darstellen kann. Der
Vorgang wird dann mit der Genauigkeit des Typs ausgeführt T , und der Ergebnistyp ist T .
Für den unären - Operator wird der Operand in den Typ konvertiert T . dabei T ist der erste von int
und long , der alle möglichen Werte des Operanden vollständig darstellen kann. Der Vorgang wird dann mit
der Genauigkeit des Typs ausgeführt T , und der Ergebnistyp ist T . Der unäre - Operator kann nicht auf
Operanden vom Typ angewendet werden ulong .
Für die binären Operatoren,,,,, + - ,, * / % & ^ | , == , != ,,, > < >= und <= werden die
Operanden in den-Typ konvertiert T . dabei T ist der erste von int , uint , und, long ulong der alle
möglichen Werte beider Operanden vollständig darstellen kann. Der Vorgang wird dann mit der Genauigkeit
des Typs durchgeführt T , und der Ergebnistyp ist T (oder bool für die relationalen Operatoren). Es ist
nicht zulässig, dass ein Operand vom Typ long und der andere vom Typ ulong mit den binären Operatoren
ist.
Für die binären << -und- >> Operatoren wird der linke Operand in den-Typ konvertiert T , wobei T der
erste von int ,, und ist, der uint long ulong alle möglichen Werte des Operanden vollständig darstellen
kann. Der Vorgang wird dann mit der Genauigkeit des Typs ausgeführt T , und der Ergebnistyp ist T .

Der char Typ wird als ganzzahliger Typ klassifiziert, aber er unterscheidet sich von den anderen ganzzahligen
Typen auf zwei Arten:
Es gibt keine impliziten Konvertierungen anderen Typen in Typ char . Insbesondere wenn die sbyte byte
Typen, und ushort Wertebereiche aufweisen, die mithilfe des-Typs vollständig Darstell Bar sind char , sind
implizite Konvertierungen von sbyte , byte oder ushort char nicht vorhanden.
Konstanten des char Typs müssen in Kombination mit einer Umwandlung in den Typ als character_literal s
oder als integer_literal s geschrieben werden char . (char)10 entspricht beispielsweise '\x000A' .

Die checked unchecked Operatoren und und Anweisungen werden verwendet, um die Überlauf Überprüfung
bei arithmetischen Operationen und Konvertierungenvon ganzzahligen Typen zu steuern In einem checked
Kontext erzeugt ein Überlauf einen Kompilierzeitfehler oder bewirkt, dass eine ausgelöst
System.OverflowException wird. In einem unchecked Kontext werden Überläufe ignoriert, und alle
höherwertigen Bits, die nicht in den Zieltyp passen, werden verworfen.
Gleit Komma Typen
C# unterstützt zwei Gleit Komma Typen: float und double . Der float - double Typ und der-Typ werden
mithilfe der 32-Bit-Formate für die einfache Genauigkeit und 64 Bit mit doppelter 754 Genauigkeit dargestellt,
die die folgenden Werte Sätze bereitstellen:
Positive null (+0) und negative null (-0): In den meisten Fällen verhalten sich positiv NULL und negatives
NULL identisch mit dem einfachen Wert 0 (null), aber bestimmte Vorgänge unterscheiden zwischen den
beiden (Divisions Operator).
Positiv unendlich und minus unendlich. Unendlich ist das Ergebnis von Vorgängen wie das Teilen einer Zahl
ungleich null (0) durch null (0). 1.0 / 0.0 ergibt beispielsweise positiv unendlich und -1.0 / 0.0 negativ
unendlich.
Der not-a-Number- Wert, häufig als NaN abgekürzt. NaN-Werte werden durch ungültige
Gleitkommavorgänge erzeugt, z. B. beim Teilen von null durch null.
Der endliche Satz von Werten ungleich 0 (null) im Formular s * m * 2^e , wobei s 1 oder-1 ist, und m und
e durch den jeweiligen Gleit kommatyp bestimmt werden: für float , 0 < m < 2^24 und -149 <= e <= 104
, und für double 0 < m < 2^53 und -1075 <= e <= 970 . Denormalisierte Gleit Komma Zahlen gelten als
gültige Werte ungleich 0 (null).
Der- float Typ kann Werte zwischen ungefähr 1.5 * 10^-45 und 3.4 * 10^38 und einer Genauigkeit von 7
Ziffern darstellen.
Der- double Typ kann Werte zwischen ungefähr 5.0 * 10^-324 und 1.7 × 10^308 und einer Genauigkeit von
15-16 Ziffern darstellen.
Wenn einer der Operanden eines binären Operators ein Gleit kommatyp ist, muss der andere Operand ein
ganzzahliger Typ oder ein Gleit kommatyp sein, und der Vorgang wird wie folgt ausgewertet:
Wenn einer der Operanden ein ganzzahliger Typ ist, wird dieser Operand in den Gleit kommatyp des
anderen Operanden konvertiert.
Wenn einer der Operanden vom Typ ist double , wird der andere Operand in konvertiert double , der
Vorgang wird mit mindestens double Bereich und Genauigkeit durchgeführt, und der Ergebnistyp ist
double (oder bool für die relationalen Operatoren).
Andernfalls wird der Vorgang mit mindestens float Bereich und Genauigkeit durchgeführt, und der
Ergebnistyp ist float (oder bool für die relationalen Operatoren).

Die Gleit Komma Operatoren, einschließlich der Zuweisungs Operatoren, führen niemals zu Ausnahmen. In
Ausnahmefällen wird von Gleit Komma Vorgängen, wie unten beschrieben, NULL, unendlich oder NaN erzeugt:
Wenn das Ergebnis einer Gleit Komma Operation für das Zielformat zu klein ist, wird das Ergebnis des
Vorgangs positiv 0 (null) oder negativ 0 (null).
Wenn das Ergebnis einer Gleit Komma Operation für das Zielformat zu groß ist, wird das Ergebnis des
Vorgangs positiv unendlich oder negativ unendlich.
Wenn ein Gleit Komma Vorgang ungültig ist, wird das Ergebnis des Vorgangs "NaN".
Wenn einer der Operanden oder beide Operanden eines Gleitkommavorgangs NaN ergibt, ist das Ergebnis
des Vorgangs NaN.
Gleit Komma Operationen können mit höherer Genauigkeit ausgeführt werden als der Ergebnistyp des
Vorgangs. Beispielsweise unterstützen einige Hardwarearchitekturen einen "Extended"-oder "long Double"-Gleit
kommatyp mit größerem Bereich und präziser als den double -Typ und führen implizit alle Gleit Komma
Vorgänge mit diesem Typ höherer Genauigkeit aus. Nur zu hohen Leistungseinbußen können solche
Hardwarearchitekturen zum Ausführen von Gleit Komma Vorgängen mit geringerer Genauigkeit gemacht
werden, und anstatt eine Implementierung zu erfordern, um sowohl die Leistung als auch die Genauigkeit zu
verlieren, kann c# für alle Gleit Komma Vorgänge einen höheren Genauigkeits-Typ verwenden. Abgesehen von
der Bereitstellung präziseren Ergebnisse hat dies nur selten messbare Auswirkungen. In Ausdrücken der Form
x * y / z , in denen die Multiplikation ein Ergebnis erzeugt, das außerhalb des double Bereichs liegt, aber die
nachfolgende Division das temporäre Ergebnis wieder in den double Bereich bringt, kann die Tatsache, dass der
Ausdruck in einem höheren Bereichs Format ausgewertet wird, dazu führen, dass ein endliches Ergebnis anstelle
von unendlich erzeugt wird.
Der Dezimaltyp
Der decimal -Typ ist ein für Finanz-und Währungsberechnungen geeigneter 128-Bit-Datentyp. Der- decimal
Typ kann Werte von 1.0 * 10^-28 bis zu ungefähr 7.9 * 10^28 mit 28-29 signifikanten Ziffern darstellen.
Der endliche Satz von Werten vom Typ decimal hat die Form (-1)^s * c * 10^-e , wobei das Vorzeichen s 0
oder 1 ist, der Koeffizienten c von angegeben wird 0 <= *c* < 2^96 und die Skala e so ist 0 <= e <= 28 . Der
decimal Typ unterstützt keine signierten Nullen, Infinities oder NaN. Eine decimal wird als 96-Bit-Ganzzahl
dargestellt, die durch eine Potenz von zehn skaliert wird. Für decimal s mit einem absoluten Wert 1.0m , der
kleiner als ist, entspricht der Wert exakt dem 28. Dezimaltrennzeichen, aber nicht weiter. Für decimal s mit
einem absoluten Wert, der größer oder gleich ist 1.0m , entspricht der Wert exakt 28 oder 29 Ziffern. Im
Gegensatz zu float den double Datentypen und können dezimale Bruchzahlen wie 0,1 genau in der-
Darstellung dargestellt werden decimal . In der float -und double -Darstellung sind solche Zahlen häufig
unendliche Bruchzahlen, sodass diese Darstellungen anfälliger für Probleme bei der Durchführung von Fehlern
werden.
Wenn einer der Operanden eines binären Operators vom Typ ist decimal , muss der andere Operand ein
ganzzahliger Typ oder vom Typ sein decimal . Wenn ein ganzzahliger Typoperand vorhanden ist, wird er in
konvertiert, decimal bevor der Vorgang durchgeführt wird.
Das Ergebnis eines Vorgangs für Werte des Typs decimal ist, dass sich das Ergebnis aus der Berechnung eines
exakten Ergebnisses ergibt (wie für jeden Operator definiert, wie für jeden Operator definiert) und dann an die
Darstellung angepasst wird. Die Ergebnisse werden auf den nächstgelegenen darstellbaren Wert gerundet und,
wenn ein Ergebnis gleich nah bei zwei darstellbaren Werten ist, bis zu dem Wert, der eine gerade Zahl in der am
wenigsten wichtigen Ziffern Position aufweist (Dies wird als "Banker srundung" bezeichnet). Ein NULL-Ergebnis
hat immer ein Vorzeichen von 0 und eine Skala von 0.
Wenn eine arithmetische decimal-Operation einen Wert erzeugt, der kleiner als oder gleich dem 5 * 10^-29
absoluten Wert ist, wird das Ergebnis des Vorgangs 0 (null). Wenn eine decimal arithmetische Operation ein
Ergebnis erzeugt, das zu groß für das decimal Format ist, wird eine ausgelöst System.OverflowException .
Der- decimal Typ hat eine höhere Genauigkeit, aber einen kleineren Bereich als die Gleit Komma Typen. Folglich
können Konvertierungen von Gleit Komma Typen in decimal Überlauf Ausnahmen erzeugen, und
Konvertierungen von decimal in den Gleit Komma Typen können zu Genauigkeits Verlusten führen. Aus diesen
Gründen gibt es keine impliziten Konvertierungen zwischen den Gleit Komma Typen und und decimal ohne
explizite Umwandlungen ist es nicht möglich, Gleit Komma Zahlen und decimal Operanden im gleichen
Ausdruck zu mischen.
Der boolesche Typ
Der bool Typ stellt boolesche logische Mengen dar. Mögliche Werte vom Typ bool sind true und false .
Zwischen und anderen Typen sind keine Standard Konvertierungen vorhanden bool . Der bool -Typ ist
insbesondere eindeutig und von den ganzzahligen Typen getrennt, und ein bool Wert kann nicht anstelle eines
ganzzahligen Werts und umgekehrt verwendet werden.
In den Programmiersprachen C und C++ kann ein ganzzahliger Wert oder ein Gleit Komma Wert von 0 (null)
oder ein NULL-Zeiger in den booleschen Wert konvertiert werden false , und ein ganzzahliger oder Gleit
Komma Wert ungleich NULL bzw. ein nicht-NULL-Zeiger kann in den booleschen Wert konvertiert werden
true . In c# werden solche Konvertierungen durch explizites Vergleichen eines ganzzahligen oder Gleit Komma
Werts mit 0 (null) oder durch explizites Vergleichen eines Objekt Verweises mit erreicht null .
Enumerationstypen
Ein Enumerationstyp ist ein eindeutiger Typ mit benannten Konstanten. Jeder Enumerationstyp verfügt über
einen zugrunde liegenden Typ, der byte ,, sbyte short , ushort , int , uint long oder ulong sein muss.
Der Satz von Werten des Enumerationstyps ist mit dem Satz von Werten des zugrunde liegenden Typs
identisch. Werte des Enumerationstyps sind nicht auf die Werte der benannten Konstanten beschränkt.
Enumerationstypen werden durch Enumerationsdeklarationen (Enumerationsdeklarationen)definiert.
Nullable -Typen
Ein Typ, der NULL-Werte zulässt, kann alle Werte seines zugrunde liegenden Typs und einen zusätzlichen
NULL-Wert darstellen. Ein Typ, der NULL-Werte zulässt, wird geschrieben T? , wobei T der zugrunde liegende
Typ ist. Diese Syntax ist eine System.Nullable<T> Kurzform für, und die beiden Formen können austauschbar
verwendet werden.
Ein Wer ttyp , der nicht auf NULL festgelegt werden kann, ist umgekehrt ein beliebiger Werttyp als
System.Nullable<T> und seine Kurzform T? (für Any T ) sowie alle Typparameter, die auf einen Werttyp
beschränkt sind, der keine NULL-Werte zulässt (d. h. alle Typparameter mit einer struct Einschränkung). Der
System.Nullable<T> Typ gibt die Werttyp Einschränkung für T (Typparameter Einschränkungen) an. Dies
bedeutet, dass der zugrunde liegende Typ eines Typs, der NULL-Werte zulässt, ein beliebiger Werttyp sein kann
Der zugrunde liegende Typ eines Typs, der NULL-Werte zulässt, kann kein Typ oder Verweistyp sein, der NULL-
Werte zulässt. Beispielsweise int?? sind und string? ungültige Typen.
Eine Instanz eines Typs, der NULL-Werte zulässt, T? verfügt über zwei öffentliche schreibgeschützte
Eigenschaften:
Eine HasValue Eigenschaft vom Typ. bool
Eine Value Eigenschaft vom Typ. T

Eine-Instanz, für die HasValue true ist, wird als ungleich NULL bezeichnet. Eine nicht-NULL-Instanz enthält einen
bekannten Wert und Value gibt diesen Wert zurück.
Eine-Instanz, für die HasValue false ist, wird als NULL bezeichnet. Eine NULL-Instanz hat einen nicht definierten
Wert. Der Versuch, den einer NULL-Instanz zu lesen, Value bewirkt System.InvalidOperationException , dass
eine ausgelöst wird. Der Prozess des Zugriffs auf die- Value Eigenschaft einer Instanz, die NULL-Werte zulässt,
wird als zum Entpacken bezeichnet.
Zusätzlich zum Standardkonstruktor verfügt jeder Typ, der NULL-Werte zulässt, T? über einen öffentlichen
Konstruktor, der ein einzelnes Argument vom Typ annimmt T . Bei einem Wert x vom Typ T , einem
Konstruktoraufruf des Formulars

new T?(x)

erstellt eine Instanz von T? , die nicht NULL ist und für die die- Value Eigenschaft ist x . Das Erstellen einer
nicht-NULL-Instanz eines Typs, der NULL-Werte zulässt, wird als Wrapping bezeichnet.
Implizite Konvertierungen sind aus dem null -Literalwert für T? (NULL-Literale Konvertierungen) und von T
in verfügbar T? (implizite Konvertierungen, die NULL zulassen)
Verweistypen
Bei einem Verweistyp handelt es sich um einen Klassentyp, einen Schnittstellentyp, einen Arraytyp oder einen
Delegattyp.

reference_type
: class_type
| interface_type
| array_type
| delegate_type
;

class_type
: type_name
| 'object'
| 'dynamic'
| 'string'
;

interface_type
: type_name
;

array_type
: non_array_type rank_specifier+
;

non_array_type
: type
;

rank_specifier
: '[' dim_separator* ']'
;

dim_separator
: ','
;

delegate_type
: type_name
;

Ein Verweistyp Wert ist ein Verweis auf eine *instance _ des Typs, der letztere als _ -Objekt * bezeichnet. Der
spezielle Wert null ist mit allen Verweis Typen kompatibel und gibt an, dass keine Instanz vorhanden ist.
Klassentypen
Ein Klassentyp definiert eine Datenstruktur, die Datenmember (Konstanten und Felder), Funktionsmember
(Methoden, Eigenschaften, Ereignisse, Indexer, Operatoren, Instanzkonstruktoren, destrukturatoren und statische
Konstruktoren) und die in der Struktur enthaltenen Typen enthält. Klassentypen unterstützen Vererbung, einen
Mechanismus, bei dem abgeleitete Klassen die Basisklassen erweitern und spezialisieren können. Instanzen von
Klassentypen werden mithilfe von object_creation_expression s erstellt (Objekt Erstellungs Ausdrücke).
Klassentypen werden in Klassenbeschrieben.
Bestimmte vordefinierte Klassentypen haben in der c#-Sprache eine besondere Bedeutung, wie in der folgenden
Tabelle beschrieben.

K L A SSEN T Y P B ESC H REIB UN G


K L A SSEN T Y P B ESC H REIB UN G

System.Object Die ultimative Basisklasse aller anderen Typen. Siehe


Objekttyp.

System.String Der Zeichen Folgentyp der Programmiersprache c#. Siehe


den String-Typ.

System.ValueType Die Basisklasse aller Werttypen. Siehe den Typ System.


ValueType.

System.Enum Die Basisklasse aller Enumerationstypen. Siehe -Auffinden.

System.Array Die Basisklasse aller Array Typen. Siehe Arrays.

System.Delegate Die Basisklasse aller Delegattypen. Siehe Delegaten.

System.Exception Die Basisklasse aller Ausnahme Typen. Siehe Ausnahmen.

den Objekttyp
Der object Klassentyp ist die ultimative Basisklasse aller anderen Typen. Jeder Typ in c# ist direkt oder indirekt
vom object Klassentyp abgeleitet.
Das Schlüsselwort object ist einfach ein Alias für die vordefinierte Klasse System.Object .
Der dynamische Typ
Der dynamic Typ, wie object , kann auf ein beliebiges Objekt verweisen. Wenn Operatoren auf Ausdrücke vom
Typ angewendet werden dynamic , wird die Auflösung so lange verzögert, bis das Programm ausgeführt wird.
Wenn der Operator daher nicht auf das Objekt angewendet werden kann, auf das verwiesen wird, wird während
der Kompilierung kein Fehler angegeben. Stattdessen wird eine Ausnahme ausgelöst, wenn die Auflösung des
Operators zur Laufzeit fehlschlägt.
Der Zweck besteht darin, dynamische Bindungen zuzulassen, die im Detail unter dynamische
Bindungbeschrieben werden.
dynamic gilt als identisch mit, object außer in den folgenden Punkten:
Vorgänge für Ausdrücke vom Typ dynamic können dynamisch gebunden werden (dynamische Bindung).
Der Typrückschluss (Typrückschluss) wird bevorzugt, dynamic object Wenn beide Kandidaten sind.

Aufgrund dieser Äquivalenz enthält Folgendes:


Es gibt eine implizite Identitäts Konvertierung zwischen object und dynamic sowie zwischen konstruierten
Typen, die beim Ersetzen von mit identisch sind. dynamic``object
Implizite und explizite Konvertierungen von und in object gelten auch für und von dynamic .
Methoden Signaturen, die bei der Ersetzung durch identisch sind, dynamic object werden als dieselbe
Signatur angesehen
Der Typ kann dynamic von zur Laufzeit nicht unterschieden werden object .
Ein Ausdruck des Typs dynamic wird als dynamischer Ausdruck bezeichnet.
Der Zeichenfolgentyp
Der string Typ ist ein versiegelter Klassentyp, der direkt von erbt object . Instanzen der- string Klasse
stellen Unicode-Zeichen folgen dar.
Werte des string Typs können als Zeichen folgen Literale (Zeichenfolgenliterale) geschrieben werden.
Das Schlüsselwort string ist einfach ein Alias für die vordefinierte Klasse System.String .
Schnittstellentypen
Eine Schnittstelle definiert einen Vertrag. Eine Klasse oder Struktur, die eine Schnittstelle implementiert, muss
ihren Vertrag einhalten. Eine Schnittstelle kann von mehreren Basis Schnittstellen erben, und eine Klasse oder
Struktur kann mehrere Schnittstellen implementieren.
Schnittstellentypen werden unter Schnittstellenbeschrieben.
Arraytypen
Ein Array ist eine Datenstruktur, die NULL oder mehr Variablen enthält, auf die über berechnete Indizes
zugegriffen wird. Die im Array enthaltenen Variablen, auch Elemente des Arrays genannt, weisen alle denselben
Typ auf. Dieser Typ wird als Elementtyp des Arrays bezeichnet.
Array Typen werden in Arraysbeschrieben.
Delegattypen
Bei einem Delegaten handelt es sich um eine Datenstruktur, die auf eine oder mehrere Methoden verweist. Bei
Instanzmethoden bezieht sie sich auch auf ihre entsprechenden Objektinstanzen.
Die nächstliegende Entsprechung eines Delegaten in C oder C++ ist ein Funktionszeiger, während ein
Funktionszeiger nur auf statische Funktionen verweisen kann, kann ein Delegat sowohl auf statische Methoden
als auch auf Instanzmethoden verweisen. Im letzteren Fall speichert der Delegat nicht nur einen Verweis auf den
Einstiegspunkt der Methode, sondern auch einen Verweis auf die Objektinstanz, für die die Methode aufgerufen
werden soll.
Delegattypen werden inDelegaten beschrieben.

Boxing und Unboxing


Das Konzept von Boxing und Unboxing ist für das Typsystem von c# von zentraler Bedeutung. Sie bietet eine
Brücke zwischen value_type s und reference_type s, indem es ermöglicht wird, dass jeder Wert eines value_type
in einen und aus dem Typ konvertiert werden kann object . Boxing und Unboxing ermöglichen eine
einheitliche Ansicht des Typsystems, wobei ein Wert eines beliebigen Typs letztendlich als Objekt behandelt
werden kann.
Boxing-Konvertierungen
Eine Boxing-Konvertierung ermöglicht eine implizite Konvertierung einer value_type in eine reference_type. Die
folgenden boxkonvertierungen sind vorhanden:
Von einem beliebigen value_type bis zum-Typ object .
Von einem beliebigen value_type bis zum-Typ System.ValueType .
Von allen non_nullable_value_type bis INTERFACE_TYPE , die vom value_type implementiert werden.
Von allen nullable_type bis INTERFACE_TYPE , die vom zugrunde liegenden Typ des nullable_type
implementiert werden.
Von einem beliebigen enum_type bis zum-Typ System.Enum .
Von allen nullable_type mit einer zugrunde liegenden enum_type bis zum-Typ System.Enum .
Beachten Sie, dass eine implizite Konvertierung von einem Typparameter als Boxing-Konvertierung
ausgeführt wird, wenn Sie zur Laufzeit von einem Werttyp in einen Verweistyp konvertiert wird (implizite
Konvertierungen mit Typparametern).
Das Boxing eines Werts einer non_nullable_value_type besteht aus der Zuordnung einer Objektinstanz und dem
Kopieren des non_nullable_value_type Werts in diese Instanz.
Das Boxing eines Werts einer nullable_type erzeugt einen NULL-Verweis, wenn es sich um den null Wert (
HasValue ist false ) handelt, oder das Ergebnis der entpacken und Boxing des zugrunde liegenden Werts
andernfalls.
Der eigentliche Prozess des Boxens eines Werts eines non_nullable_value_type wird am besten erläutert, indem
das vorhanden sein einer generischen Boxing-Klasse dargestellt wird, die sich so verhält, als wäre sie wie folgt
deklariert:

sealed class Box<T>: System.ValueType


{
T value;

public Box(T t) {
value = t;
}
}

Das Boxing eines Werts v vom Typ T besteht jetzt aus der Ausführung des Ausdrucks new Box<T>(v) und
dem Zurückgeben der resultierenden Instanz als Wert des Typs object . Folglich werden die Anweisungen

int i = 123;
object box = i;

konzeptionell entsprechen

int i = 123;
object box = new Box<int>(i);

Eine Boxing-Klasse wie Box<T> oben ist nicht vorhanden, und der dynamische Typ eines geschachtelten Werts
ist eigentlich kein Klassentyp. Stattdessen hat ein geachtelter Wert des Typs T den dynamischen Typ T , und
eine dynamische Typüberprüfung mit dem is Operator kann einfach auf den Typ verweisen T . Beispiel:

int i = 123;
object box = i;
if (box is int) {
Console.Write("Box contains an int");
}

die Zeichenfolge " Box contains an int " wird in der Konsole ausgegeben.
Eine Boxing-Konvertierung impliziert das Erstellen einer Kopie des Werts, der gekapselt wird. Dies unterscheidet
sich von der Konvertierung eines reference_type in den Typ object , in dem der Wert weiterhin auf dieselbe
Instanz verweist und einfach als weniger abgeleiteter Typ angesehen wird object . Beispielsweise mit der
Deklaration

struct Point
{
public int x, y;

public Point(int x, int y) {


this.x = x;
this.y = y;
}
}
die folgenden Anweisungen

Point p = new Point(10, 10);


object box = p;
p.x = 20;
Console.Write(((Point)box).x);

Gibt den Wert 10 in der Konsole aus, da der implizite Boxing-Vorgang, der bei der Zuweisung von auf auftritt,
bewirkt, dass p box der Wert von p kopiert wird. Point Wurde stattdessen als deklariert class . der Wert
20 würde ausgegeben, weil p und auf box dieselbe Instanz verweisen würden.
Unboxing-Konvertierungen
Eine Unboxing-Konvertierung ermöglicht das explizite Konvertieren eines reference_type in eine value_type. Die
folgenden Unboxing-Konvertierungen sind vorhanden:
Vom Typ object zu beliebigen value_type.
Vom Typ System.ValueType zu beliebigen value_type.
Von allen INTERFACE_TYPE bis zu non_nullable_value_type , die die INTERFACE_TYPE implementiert.
Von allen INTERFACE_TYPE zu beliebigen nullable_type , deren zugrunde liegender Typ den INTERFACE_TYPE
implementiert.
Vom Typ System.Enum zu beliebigen enum_type.
Vom Typ System.Enum zu einer beliebigen nullable_type mit einer zugrunde liegenden enum_type.
Beachten Sie, dass eine explizite Konvertierung in einen Typparameter als Unboxing-Konvertierung
ausgeführt wird, wenn Sie zur Laufzeit von einem Verweistyp in einen Werttyp (explizite dynamische
Konvertierungen) konvertiert wird.
Ein Unboxing-Vorgang für eine non_nullable_value_type besteht darin, zuerst zu überprüfen, ob die
Objektinstanz ein geachtelter Wert der angegebenen non_nullable_value_type ist, und dann den Wert aus der-
Instanz zu kopieren.
Beim Unboxing in eine nullable_type wird der NULL-Wert des nullable_type erzeugt, wenn der Quell Operand
ist null , oder das umschließende Ergebnis des Unboxing der Objektinstanz in den zugrunde liegenden Typ des
nullable_type andernfalls.
Bei der im vorherigen Abschnitt beschriebenen imaginären Boxingklasse besteht eine Unboxing-Konvertierung
eines Objekts box in eine value_type T aus der Ausführung des Ausdrucks ((Box<T>)box).value . Folglich
werden die Anweisungen

object box = 123;


int i = (int)box;

konzeptionell entsprechen

object box = new Box<int>(123);


int i = ((Box<int>)box).value;

Damit eine Unboxing-Konvertierung in eine angegebene non_nullable_value_type zur Laufzeit erfolgreich


ausgeführt werden kann, muss der Wert des Quell Operanden ein Verweis auf einen geachtelten Wert dieses
non_nullable_value_type sein. Wenn der Quell Operand ist null , wird eine ausgelöst
System.NullReferenceException . Wenn der Quell Operand ein Verweis auf ein inkompatibles Objekt ist, wird
eine ausgelöst System.InvalidCastException .
Damit eine Unboxing-Konvertierung in eine angegebene nullable_type zur Laufzeit erfolgreich ausgeführt
werden kann, muss der Wert des Quell Operanden entweder null oder ein Verweis auf einen geachtelten Wert
der zugrunde liegenden non_nullable_value_type der nullable_type sein. Wenn der Quell Operand ein Verweis
auf ein inkompatibles Objekt ist, wird eine ausgelöst System.InvalidCastException .

Constructed types (Konstruierte Typen)


Eine generische Typdeklaration gibt allein einen ungebundenen generischen Typ _ an, der als "Blueprint"
verwendet wird, um viele verschiedene Typen durch Anwenden von _Typargumenten*_ zu bilden. Die
Typargumente werden in spitzen Klammern ( < und > ) direkt nach dem Namen des generischen Typs
geschrieben. Ein Typ, der mindestens ein Typargument enthält, wird als konstruierter Typ bezeichnet. Ein
konstruierter Typ kann an den meisten Stellen in der Sprache verwendet werden, in der ein Typname angezeigt
werden kann. Ein ungebundener generischer Typ kann nur innerhalb einer _typeof_expression * verwendet
werden (der typeof-Operator).
Konstruierte Typen können auch in Ausdrücken als einfache Namen (einfache Namen) oder beim Zugriff auf
einen Member (Member Access) verwendet werden.
Wenn ein namespace_or_type_name ausgewertet wird, werden nur generische Typen mit der richtigen Anzahl
von Typparametern berücksichtigt. Daher ist es möglich, denselben Bezeichner zu verwenden, um
unterschiedliche Typen zu identifizieren, sofern die Typen eine unterschiedliche Anzahl von Typparametern
aufweisen. Dies ist nützlich, wenn generische und nicht generische Klassen in demselben Programm gemischt
werden:

namespace Widgets
{
class Queue {...}
class Queue<TElement> {...}
}

namespace MyApplication
{
using Widgets;

class X
{
Queue q1; // Non-generic Widgets.Queue
Queue<int> q2; // Generic Widgets.Queue
}
}

Ein TYPE_NAME kann einen konstruierten Typ identifizieren, obwohl er keine Typparameter direkt angibt. Dies
kann vorkommen, wenn ein Typ in einer generischen Klassen Deklaration geschachtelt ist und der Instanztyp der
enthaltenden Deklaration implizit für die Namenssuche (geschachtelteTypen in generischen Klassen) verwendet
wird:

class Outer<T>
{
public class Inner {...}

public Inner i; // Type of i is Outer<T>.Inner


}

In unsicherem Code kann ein konstruierter Typ nicht als unmanaged_type (Zeiger Typen) verwendet werden.
Typargumente
Jedes Argument in einer Typargument Liste ist einfach ein Typ.
type_argument_list
: '<' type_arguments '>'
;

type_arguments
: type_argument (',' type_argument)*
;

type_argument
: type
;

In unsicherem Code (unsicherer Code) ist ein type_argument möglicherweise kein Zeigertyp. Jedes
Typargument muss alle Einschränkungen für den entsprechenden Typparameter (Typparameter
Einschränkungen) erfüllen.
Open-und Closed-Typen
Alle Typen können entweder als *Open Types _-oder _ Closed-Typen * klassifiziert werden. Ein offener Typ ist
ein Typ, der Typparameter umfasst. Dies gilt insbesondere in folgenden Fällen:
Ein Typparameter definiert einen geöffneten Typ.
Ein Arraytyp ist nur dann ein offener Typ, wenn sein Elementtyp ein offener Typ ist.
Ein konstruierter Typ ist ein offener Typ, wenn es sich bei mindestens einem Typargument um einen
geöffneten Typ handelt. Ein konstruierter, von einem Typ erstellter Typ ist ein offener Typ, wenn es sich bei
mindestens einem Typargument oder den Typargumenten der enthaltenden Typen um einen geöffneten Typ
handelt.
Ein geschlossener Typ ist ein Typ, bei dem es sich nicht um einen geöffneten Typ handelt.
Zur Laufzeit wird der gesamte Code in einer generischen Typdeklaration im Kontext eines geschlossenen
konstruierten Typs ausgeführt, der durch Anwenden von Typargumenten auf die generische Deklaration erstellt
wurde. Jeder Typparameter innerhalb des generischen Typs ist an einen bestimmten Lauf Zeittyp gebunden. Die
Lauf Zeit Verarbeitung aller Anweisungen und Ausdrücke tritt immer bei geschlossenen Typen auf, und offene
Typen werden nur während der Kompilierungszeit verarbeitet.
Jeder geschlossene konstruierte Typ verfügt über einen eigenen Satz statischer Variablen, die nicht gemeinsam
mit anderen geschlossenen konstruierten Typen verwendet werden. Da ein offener Typ zur Laufzeit nicht
vorhanden ist, sind keine statischen Variablen mit einem geöffneten Typ verknüpft. Zwei geschlossene
konstruierte Typen weisen denselben Typ auf, wenn Sie aus demselben ungebundenen generischen Typ erstellt
werden und die entsprechenden Typargumente denselben Typ haben.
Gebundene und ungebundene Typen
Der Begriff *ungebundener Typ _ verweist auf einen nicht generischen Typ oder einen ungebundenen
generischen Typ. Der Begriff _ gebundener Typ* verweist auf einen nicht generischen Typ oder einen
konstruierten Typ.
Ein ungebundener Typ verweist auf die durch eine Typdeklaration deklarierte Entität. Ein ungebundener
generischer Typ ist nicht selbst ein Typ und kann nicht als Typ einer Variablen, eines Arguments oder eines
Rückgabewerts oder als Basistyp verwendet werden. Das einzige Konstrukt, in dem auf einen ungebundenen
generischen Typ verwiesen werden kann, ist der typeof Ausdruck (der typeof-Operator).
Erfüllen von Einschränkungen
Wenn auf einen konstruierten Typ oder eine generische Methode verwiesen wird, werden die angegebenen
Typargumente mit den Typparameter Einschränkungen überprüft, die für den generischen Typ oder die
generische Methode deklariert sind (Typparameter Einschränkungen). Für jede where Klausel wird das
Typargument, A das dem benannten Typparameter entspricht, wie folgt für jede Einschränkung überprüft:
Wenn es sich bei der Einschränkung um einen Klassentyp, einen Schnittstellentyp oder einen Typparameter
handelt, C Stellen Sie diese Einschränkung mit den bereitgestellten Typargumenten dar, die für
Typparameter ersetzt werden, die in der Einschränkung vorkommen. Um die Einschränkung zu erfüllen, muss
der Typ wie A folgt in den Typ konvertiert werden C :
Eine Identitäts Konvertierung (Identitäts Konvertierung)
Implizite Verweis Konvertierung (implizite Verweis Konvertierungen)
Eine Boxing-Konvertierung (boxkonvertierungen), vorausgesetzt, dass TYPE a ein Werttyp ist, der
keine NULL-Werte zulässt.
Eine implizite Verweis-, Boxing-oder Typparameter Konvertierung von einem Typparameter A in C .
Wenn es sich bei der Einschränkung um die Verweistyp Einschränkung ( class ) handelt, muss der Typ A
eine der folgenden Bedingungen erfüllen:
A ist ein Schnittstellentyp, Klassentyp, Delegattyp oder Arraytyp. Beachten Sie, dass
System.ValueType und System.Enum Verweis Typen sind, die diese Einschränkung erfüllen.
A ein Typparameter, bei dem es sich um einen Verweistyp (Typparameter Einschränkungen) handelt.
Wenn es sich bei der Einschränkung um die Werttyp Einschränkung ( struct ) handelt, muss der Typ A
eine der folgenden Bedingungen erfüllen:
A ist ein Strukturtyp oder ein Aufzählungs Typ, aber kein Typ, der NULL-Werte zulässt. Beachten Sie,
dass System.ValueType und System.Enum Verweis Typen sind, die diese Einschränkung nicht erfüllen.
A ein Typparameter mit der Werttyp Einschränkung (Typparameter Einschränkungen).
Wenn es sich bei der Einschränkung um die Konstruktoreinschränkung handelt new() , darf der Typ A nicht
sein, abstract und er muss über einen öffentlichen Parameter losen Konstruktor verfügen. Dies ist erfüllt,
wenn eine der folgenden Bedingungen zutrifft:
A ist ein Werttyp, da alle Werttypen über einen öffentlichen Standardkonstruktor verfügen
(Standardkonstruktoren).
A ein Typparameter mit der Konstruktoreinschränkung (Typparameter Einschränkungen).
A ein Typparameter mit der Werttyp Einschränkung (Typparameter Einschränkungen).
A ist eine Klasse, die nicht ist abstract und einen explizit deklarierten public Konstruktor ohne
Parameter enthält.
A ist nicht abstract und verfügt über einen Standardkonstruktor (Standardkonstruktoren).

Ein Kompilierzeitfehler tritt auf, wenn eine oder mehrere der Einschränkungen eines Typparameters nicht durch
die angegebenen Typargumente erfüllt werden.
Da Typparameter nicht vererbt werden, werden Einschränkungen nie geerbt. Im folgenden Beispiel D muss die-
Einschränkung für den Typparameter angeben, T sodass T die von der Basisklasse erzwungene
Einschränkung erfüllt B<T> . Im Gegensatz dazu muss die Klasse E keine Einschränkung angeben, da List<T>
IEnumerable für beliebige implementiert T .

class B<T> where T: IEnumerable {...}

class D<T>: B<T> where T: IEnumerable {...}

class E<T>: B<List<T>> {...}

Typparameter
Ein Typparameter ist ein Bezeichner, der einen Werttyp oder Verweistyp festlegt, an den der Parameter zur
Laufzeit gebunden ist.
type_parameter
: identifier
;

Da ein Typparameter mit vielen verschiedenen tatsächlichen Typargumenten instanziiert werden kann, haben
Typparameter etwas andere Vorgänge und Einschränkungen als andere Typen. Dazu gehören:
Ein Typparameter kann nicht direkt verwendet werden, um eine Basisklasse (Basisklasse) oder eine
Schnittstelle (Variant-Typparameter Listen) zu deklarieren.
Die Regeln für die Element Suche für Typparameter hängen von den Einschränkungen ab, die ggf. auf den
Typparameter angewendet werden. Sie werden in der Mitglieder Sucheausführlich erläutert.
Die verfügbaren Konvertierungen für einen Typparameter hängen von den Einschränkungen ab, die ggf. auf
den Typparameter angewendet werden. Sie werden in impliziten Konvertierungen mit Typparametern und
expliziten dynamischen Konvertierungenausführlich erläutert.
Das Literale null kann nicht in einen Typ konvertiert werden, der durch einen Typparameter angegeben
wird, es sei denn, der Typparameter ist ein Verweistyp (implizite Konvertierungen mit Typparametern).
Stattdessen kann jedoch ein- default Ausdruck (Standardwert Ausdrücke) verwendet werden. Außerdem
kann ein Wert mit einem Typ, der durch einen Typparameter angegeben wird, mit null == und !=
(Verweis Typen-Gleichheits Operatoren) verglichen werden, es sei denn, der Typparameter weist die Werttyp
Einschränkung auf.
Ein- new Ausdruck (Objekt Erstellungs Ausdrücke) kann nur mit einem Typparameter verwendet werden,
wenn der Typparameter durch eine constructor_constraint oder die Werttyp Einschränkung (Typparameter
Einschränkungen) eingeschränkt wird.
Ein Typparameter kann nicht an einer beliebigen Stelle innerhalb eines Attributs verwendet werden.
Ein Typparameter kann nicht in einem Element Zugriff (Member Access) oder Typname (Namespace-und
Typnamen) verwendet werden, um einen statischen Member oder einen schsted Typ zu identifizieren.
In unsicherem Code kann ein Typparameter nicht als unmanaged_type (Zeiger Typen) verwendet werden.
Typparameter sind ein reines Kompilierzeit Konstrukt. Zur Laufzeit wird jeder Typparameter an einen Lauf
Zeittyp gebunden, der durch Bereitstellen eines Typarguments an die generische Typdeklaration angegeben
wurde. Daher ist der Typ einer Variablen, die mit einem Typparameter deklariert wird, zur Laufzeit ein
geschlossener konstruierter Typ (Open-und Closed-Typen). Die Lauf Zeit Ausführung aller Anweisungen und
Ausdrücke, die Typparameter betreffen, verwendet den eigentlichen Typ, der als Typargument für diesen
Parameter angegeben wurde.

Expression tree types (Ausdrucksbaumstrukturtypen)


*Expression Trees _ zulassen, dass Lambda-Ausdrücke als Datenstrukturen anstelle von ausführbarem Code
dargestellt werden. Ausdrucks Baumstrukturen sind Werte von _ Expression tree types* der Form
System.Linq.Expressions.Expression<D> , wobei D ein beliebiger Delegattyp ist. Für den Rest dieser
Spezifikation werden diese Typen mit der kurzzeile bezeichnet Expression<D> .
Wenn eine Konvertierung von einem Lambda-Ausdruck in einen Delegattyp vorhanden D ist, ist auch eine
Konvertierung für den Ausdrucks bauentyp vorhanden Expression<D> . Während die Konvertierung eines
Lambda-Ausdrucks in einen Delegattyp einen Delegaten generiert, der auf den ausführbaren Code für den
Lambda-Ausdruck verweist, erstellt die Konvertierung in einen Ausdrucks Strukturtyp eine Ausdrucks
Baumstruktur-Darstellung des Lambda Ausdrucks.
Ausdrucks Baumstrukturen sind effiziente in-Memory-Daten Darstellungen von Lambda-Ausdrücken und
machen die Struktur des Lambda Ausdrucks transparent und explizit.
Genau wie ein Delegattyp weist D Expression<D> auch Parameter-und Rückgabe Typen auf, die mit denen von
identisch sind D .
Im folgenden Beispiel wird ein Lambda-Ausdruck sowohl als ausführbarer Code als auch als Ausdrucks
Baumstruktur dargestellt. Da eine Konvertierung in vorhanden Func<int,int> ist, gibt es auch eine
Konvertierung in Expression<Func<int,int>> :

Func<int,int> del = x => x + 1; // Code

Expression<Func<int,int>> exp = x => x + 1; // Data

Nach diesen Zuweisungen verweist der Delegat auf del eine Methode, die zurückgibt x + 1 , und die
Ausdrucks Baum exp Struktur verweist auf eine Datenstruktur, die den Ausdruck beschreibt x => x + 1 .
Die genaue Definition des generischen Typs Expression<D> sowie die präzisen Regeln zum Erstellen einer
Ausdrucks Baumstruktur, wenn ein Lambda Ausdruck in einen Ausdrucks Strukturtyp konvertiert wird, liegen
sowohl außerhalb des Gültigkeits Bereichs dieser Spezifikation.
Zwei Dinge sind wichtig, um explizit zu machen:
Nicht alle Lambda-Ausdrücke können in Ausdrucks Baumstrukturen konvertiert werden. Beispielsweise
können Lambda-Ausdrücke mit Anweisungs Text und Lambda-Ausdrücke, die Zuweisungs Ausdrücke
enthalten, nicht dargestellt werden. In diesen Fällen ist noch eine Konvertierung vorhanden, schlägt
jedoch zur Kompilierzeit fehl. Diese Ausnahmen werden in anonymen Funktions
Konvertierungenausführlich erläutert.
Expression<D> bietet eine Instanzmethode Compile , die einen Delegaten vom Typ erzeugt D :

Func<int,int> del2 = exp.Compile();

Das Aufrufen dieses Delegaten bewirkt, dass der durch die Ausdrucks Baumstruktur dargestellte Code
ausgeführt wird. Folglich sind die oben aufgeführten Definitionen gleichwertig, und die folgenden zwei
Anweisungen haben die gleiche Wirkung:

int i1 = del(1);

int i2 = del2(1);

Nach dem Ausführen dieses Codes i1 i2 haben und beide den Wert 2 .
Variablen
04.11.2021 • 60 minutes to read

Variablen stellen Speicherorte dar. Jede Variable verfügt über einen Typ, der bestimmt, welche Werte in der
Variablen gespeichert werden können. C# ist eine typsichere Sprache, und der C#-Compiler garantiert, dass in
Variablen gespeicherte Werte immer den entsprechenden Typ aufweisen. Der Wert einer Variablen kann durch
Zuweisung oder durch Verwendung der Operatoren und geändert ++ -- werden.
Eine Variable muss definitiv zugewiesen werden (Definitive Zuweisung), bevor ihr Wert abgerufen werden
kann.
Wie in den folgenden Abschnitten beschrieben, sind Variablen entweder anfänglich zugewiesen _ oder _
anfänglich nicht zugewiesen**. Eine anfänglich zugewiesene Variable verfügt über einen klar definierten
Anfangswert und gilt immer als definitiv zugewiesen. Eine anfänglich nicht zugewiesene Variable hat keinen
Anfangswert. Damit eine anfänglich nicht zugewiesene Variable als definitiv an einem bestimmten Speicherort
zugewiesen betrachtet wird, muss eine Zuweisung zur Variablen in jedem möglichen Ausführungspfad erfolgen,
der zu diesem Speicherort führt.

Variablenkategorien
C# definiert sieben Kategorien von Variablen: statische Variablen, Instanzvariablen, Arrayelemente,
Wertparameter, Verweisparameter, Ausgabeparameter und lokale Variablen. In den folgenden Abschnitten
werden die einzelnen Kategorien beschrieben.
Im Beispiel

class A
{
public static int x;
int y;

void F(int[] v, int a, ref int b, out int c) {


int i = 1;
c = a + b++;
}
}

x ist eine statische Variable, y eine Instanzvariable, v[0] ein Arrayelement, a ein Wertparameter, b ein
Verweisparameter, c ein Ausgabeparameter und eine lokale i Variable.
Statische Variablen
Ein mit dem static -Modifizierer deklariertes Feld wird als statische Variable bezeichnet. Eine statische
Variable tritt vor der Ausführung des statischen Konstruktors (statische Konstruktoren) für ihren enthaltenden
Typ auf und endet, wenn die zugeordnete Anwendungsdomäne nicht mehr vorhanden ist.
Der Anfangswert einer statischen Variablen ist der Standardwert (Standardwerte) des Variablentyps.
Für eine bestimmte Zuweisungsprüfung wird eine statische Variable als anfänglich zugewiesen betrachtet.
Instanzvariablen
Ein Feld, das ohne den static Modifizierer deklariert wird, wird als Instanzvariable bezeichnet.
Instanzvariablen in Klassen
Eine Instanzvariable einer Klasse tritt auf, wenn eine neue Instanz dieser Klasse erstellt wird, und wird nicht mehr
vorhanden, wenn keine Verweise auf diese Instanz vorhanden sind und der Destruktor der Instanz (sofern
vorhanden) ausgeführt wurde.
Der Anfangswert einer Instanzvariablen einer Klasse ist der Standardwert (Standardwerte) des Variablentyps.
Für die Überprüfung der eindeutigen Zuweisung wird eine Instanzvariable einer Klasse als anfänglich
zugewiesen betrachtet.
Instanzvariablen in Strukturen
Eine Instanzvariable einer Struktur hat genau die gleiche Lebensdauer wie die Strukturvariable, zu der sie
gehört. Anders ausgedrückt: Wenn eine Variable eines Strukturtyps existiert oder nicht mehr vorhanden ist,
auch die Instanzvariablen der Struktur.
Der anfängliche Zuweisungsstatus einer Instanzvariablen einer Struktur ist mit dem der enthaltenden
Strukturvariablen identisch. Anders ausgedrückt: Wenn eine Strukturvariable als anfänglich zugewiesen
betrachtet wird, sind auch ihre Instanzvariablen und wenn eine Strukturvariable als anfänglich nicht zugewiesen
betrachtet wird, werden ihre Instanzvariablen ebenfalls nicht zugewiesen.
Arrayelemente
Die Elemente eines Arrays werden beim Erstellen einer Arrayinstanz vorhanden und werden nicht mehr
vorhanden sein, wenn keine Verweise auf diese Arrayinstanz vorhanden sind.
Der Anfangswert jedes Elements eines Arrays ist der Standardwert (Standardwerte) des Typs der Arrayelemente.
Für die Überprüfung der eindeutigen Zuweisung gilt ein Arrayelement als anfänglich zugewiesen.
Wertparameter
Ein Parameter, der ohne einen ref - oder out -Modifizierer deklariert wird, ist ein Wer tparameter.
Ein Value-Parameter tritt beim Aufruf des Funktionsmitglieds (Methode, Instanzkonstruktor, Accessor oder
Operator) oder der anonymen Funktion, zu der der Parameter gehört, auf und wird mit dem Wert des
Arguments initialisiert, das im Aufruf angegeben wird. Ein Value-Parameter ist normalerweise nicht mehr
vorhanden, wenn der Funktions member oder die anonyme Funktion zurückgibt. Wenn der Wertparameter
jedoch von einer anonymen Funktion(anonyme Funktionsausdrücke)erfasst wird, verlängert sich die
Lebensdauer mindestens so lange, bis der aus dieser anonymen Funktion erstellte Delegat oder die
Ausdrucksbaumstruktur für die Garbage Collection geeignet ist.
Für die überprüfung bestimmter Zuweisungen wird ein Wertparameter als anfänglich zugewiesen betrachtet.
Verweisparameter
Ein mit einem ref -Modifizierer deklarierter Parameter ist ein Ver weisparameter.
Ein Verweisparameter erstellt keinen neuen Speicherort. Stattdessen stellt ein Verweisparameter den gleichen
Speicherort wie die Variable dar, die als Argument im Funktionsmember oder anonymer Funktionsaufruf
angegeben wird. Daher ist der Wert eines Verweisparameters immer der gleiche wie die zugrunde liegende
Variable.
Die folgenden eindeutigen Zuweisungsregeln gelten für Verweisparameter. Beachten Sie die verschiedenen
Regeln für Ausgabeparameter, die unter Ausgabeparameterbeschrieben sind.
Eine Variable muss definitiv zugewiesen werden (Definitive Zuweisung), bevor sie als Verweisparameter in
einem Funktionsmember- oder Delegataufruf übergeben werden kann.
Innerhalb eines Funktionsmembers oder einer anonymen Funktion gilt ein Verweisparameter als anfänglich
zugewiesen.
Innerhalb einer Instanzmethode oder eines Instanzaccessors eines Strukturtyps verhält sich das this
Schlüsselwort genau wie ein Verweisparameter des Strukturtyps ( DieserZugriff).
Ausgabeparameter
Ein mit einem out Modifizierer deklarierter Parameter ist ein Ausgabeparameter.
Ein Ausgabeparameter erstellt keinen neuen Speicherort. Stattdessen stellt ein Ausgabeparameter den gleichen
Speicherort wie die Variable dar, die als Argument im Funktionsmember- oder Delegataufruf angegeben wird.
Daher ist der Wert eines Ausgabeparameters immer der gleiche wie die zugrunde liegende Variable.
Die folgenden eindeutigen Zuweisungsregeln gelten für Ausgabeparameter. Beachten Sie die verschiedenen
Regeln für Verweisparameter, die unter Verweisparameterbeschrieben sind.
Eine Variable muss nicht definitiv zugewiesen werden, bevor sie als Ausgabeparameter in einem
Funktionsmember- oder Delegataufruf übergeben werden kann.
Nach dem normalen Abschluss eines Funktions- oder Delegataufrufs gilt jede Variable, die als
Ausgabeparameter übergeben wurde, als in diesem Ausführungspfad zugewiesen.
Innerhalb eines Funktionsmitglieds oder einer anonymen Funktion gilt ein Ausgabeparameter als anfänglich
nicht zugewiesen.
Jeder Ausgabeparameter eines Funktionsmitglieds oder einer anonymen Funktion muss
definitivzugewiesenwerden ( Bestimmte Zuweisung ), bevor der Funktions member oder die anonyme
Funktion normal zurückgegeben wird.
Innerhalb eines Instanzkonstruktors eines Strukturtyps verhält sich das Schlüsselwort genau wie ein
Ausgabeparameter des this Strukturtyps ( DieserZugriff).
Lokale Variablen
Eine * lokale Variable _ wird von einem _local_variable_declaration* deklariert, der in einem -Block, einem
for_statement, einem switch_statement oder einem using_statement; oder durch einen foreach_statement oder
einen specific_catch_clause für eine try_statement.
Die Lebensdauer einer lokalen Variablen ist der Teil der Programmausführung, in dem der Speicher garantiert
für sie reserviert ist. Diese Lebensdauer reicht mindestens vom Eintrag in den Block , for_statement,
switch_statement, using_statement, foreach_statement oder specific_catch_clause, dem sie zugeordnet ist, bis
die Ausführung dieses Blocks , for_statement, switch_statement, using_statement, foreach_statement oder
specific_catch_clause auf irgendeine Weise endet. (Wenn Sie einen eingeschlossenen Block eingeben oder eine
Methode aufrufen, wird die Ausführung des aktuellen Blocks, for_statement, switch_statement, using_statement,
foreach_statement oder specific_catch_clause angehalten, aber nicht beendet.) Wenn die lokale Variable von
einer anonymen Funktion (Erfasste äußere Variablen)erfasst wird, verlängert sich ihre Lebensdauer mindestens
so lange, bis der aus der anonymen Funktion erstellte Delegat oder die Ausdrucksbaumstruktur zusammen mit
allen anderen Objekten, die auf die erfasste Variable verweisen, für die Garbage Collection geeignet sind.
Wenn der übergeordnete Block , for_statement, switch_statement, using_statement, foreach_statement oder
specific_catch_clause rekursiv eingegeben wird, wird jedes Mal eine neue Instanz der lokalen Variablen erstellt,
und die local_variable_initializer, falls vorhanden, wird jedes Mal ausgewertet.
Eine lokale Variable, die von einem local_variable_declaration eingeführt wird, wird nicht automatisch initialisiert
und hat daher keinen Standardwert. Zum Zweck der eindeutigen Zuweisungsüberprüfung wird eine lokale
Variable, die von einem local_variable_declaration eingeführt wird, anfänglich als nicht zugewiesen betrachtet.
Ein local_variable_declaration kann eine local_variable_initializer enthalten. In diesem Fall gilt die Variable nur
nach dem Initialisierungsausdruck (Deklarationsanweisungen) als definitiv zugewiesen.
Innerhalb des Bereichs einer lokalen Variablen, die von einem local_variable_declaration eingeführt wurde, ist es
ein Kompilierzeitfehler, auf diese lokale Variable an einer Textposition zu verweisen, die ihrer
local_variable_declarator vorangeht. Wenn die lokale Variablendeklaration implizit ist (lokale
Variablendeklarationen),ist es auch ein Fehler, auf die Variable innerhalb ihrer local_variable_declarator zu
verweisen.
Eine lokale Variable, die von einem foreach_statement oder einem specific_catch_clause eingeführt wurde, gilt
als definitiv im gesamten Bereich zugewiesen.
Die tatsächliche Lebensdauer einer lokalen Variablen ist implementierungsabhängig. Beispielsweise kann ein
Compiler statisch bestimmen, dass eine lokale Variable in einem -Block nur für einen kleinen Teil dieses Blocks
verwendet wird. Mithilfe dieser Analyse könnte der Compiler Code generieren, der dazu führt, dass der Speicher
der Variablen eine kürzere Lebensdauer als der enthaltende Block hat.
Der Speicher, auf den durch eine lokale Verweisvariable verwiesen wird, wird unabhängig von der Lebensdauer
dieser lokalen Verweisvariablen(Automatische Speicherverwaltung) wieder verfügbar.

Standardwerte
Die folgenden Kategorien von Variablen werden automatisch mit ihren Standardwerten initialisiert:
Statische Variablen.
Instanzvariablen von Klasseninstanzen.
Array-Elemente.
Der Standardwert einer Variablen hängt vom Typ der Variablen ab und wird wie folgt bestimmt:
Für eine Variable eines value_type ist der Standardwert mit dem Wert identisch, der vom
Standardkonstruktor des value_type berechnet wird (Standardkonstruktoren).
Für eine Variable eines reference_type ist der Standardwert null .

Die Initialisierung von Standardwerten erfolgt in der Regel, indem der Arbeitsspeicher vom Speicher-Manager
oder Garbage Collector auf "all-bits-zero" initialisiert wird, bevor er zur Verwendung zugeordnet wird. Aus
diesem Grund ist es praktisch, all-bits-zero zu verwenden, um den NULL-Verweis zu darstellen.

Definite assignment (Festgelegte Zuweisung)


An einem bestimmten Speicherort im ausführbaren Code eines Funktionsmitglieds wird eine Variable als
definitiv zugewiesen bezeichnet, wenn der Compiler durch eine bestimmte statische Flussanalyse(PräziseRegeln
zum Bestimmen der eindeutigen Zuweisung) nachweisen kann, dass die Variable automatisch initialisiert wurde
oder das Ziel mindestens einer Zuweisung war. Informell erwähnt sind die Regeln einer bestimmten Zuweisung:
Eine anfänglich zugewiesene Variable (Anfänglich zugewiesene Variablen) wird immer als definitiv
zugewiesen betrachtet.
Eine anfänglich nicht zugewiesene Variable ( Anfänglich nicht zugewiesene Variablen ) gilt als definitiv an
einem bestimmten Speicherort zugewiesen, wenn alle möglichenAusführungspfade,die zu diesem
Speicherort führen, mindestens eine der folgenden Elemente enthalten:
Eine einfache Zuweisung(einfache Zuweisung),in der die Variable der linke Operand ist.
Ein Aufrufausdruck (Aufrufausdrücke) oder ein Objekterstellungsausdruck
(Objekterstellungsausdrücke), der die Variable als Ausgabeparameter übergibt.
Bei einer lokalen Variablen eine lokale Variablendeklaration (Lokale Variablendeklarationen), die einen
Variableninitialisierer enthält.
Die formale Spezifikation, die den oben genannten informellen Regeln zugrunde liegt, wird unter Anfänglich
zugewiesene Variablen, Anfänglich nicht zugewiesene Variablenund Präzise Regeln zum Bestimmen der
bestimmten Zuweisungbeschrieben.
Die eindeutigen Zuweisungszustände von Instanzvariablen einer struct_type Variable werden einzeln und
zusammen nachverfolgt. Zusätzlich zu den oben genannten Regeln gelten die folgenden Regeln für struct_type
Variablen und deren Instanzvariablen:
Eine Instanzvariable gilt als definitiv zugewiesen, wenn ihre enthaltende struct_type Variable als definitiv
zugewiesen betrachtet wird.
Eine struct_type Variable gilt als definitiv zugewiesen, wenn jede ihrer Instanzvariablen als definitiv
zugewiesen gilt.
Eine bestimmte Zuweisung ist in den folgenden Kontexten eine Anforderung:
Eine Variable muss definitiv an jedem Speicherort zugewiesen werden, an dem ihr Wert abgerufen wird.
Dadurch wird sichergestellt, dass nicht definierte Werte nie auftreten. Das Vorkommen einer Variablen in
einem Ausdruck wird als Abrufen des Werts der Variablen betrachtet, es sei denn,
Die Variable ist der linke Operand einer einfachen Zuweisung.
die Variable als Ausgabeparameter übergeben wird, oder
Die Variable ist eine struct_type Variable und tritt als linker Operand eines Memberzugriffs auf.
Eine Variable muss definitiv an jedem Speicherort zugewiesen werden, an dem sie als Verweisparameter
übergeben wird. Dadurch wird sichergestellt, dass der aufgerufene Funktionsmember den anfänglich
zugewiesenen Verweisparameter berücksichtigen kann.
Alle Ausgabeparameter eines Funktionsmitglieds müssen definitiv an jeder Stelle zugewiesen werden, an der
der Funktions member zurückgegeben wird (durch eine -Anweisung oder durch Ausführung, die das Ende
des return Funktionsteilkörpers erreicht). Dadurch wird sichergestellt, dass Funktionsmitglieder keine nicht
definierten Werte in Ausgabeparametern zurückgeben, sodass der Compiler einen Funktions memberaufruf
in Betracht ziehen kann, der eine Variable als Ausgabeparameter akzeptiert, der einer Zuweisung zur
Variablen entspricht.
Die Variable eines struct_type Instanzkonstruktors muss definitiv an jedem Speicherort zugewiesen werden,
an dem this dieser Instanzkonstruktor zurückgegeben wird.
Anfänglich zugewiesene Variablen
Die folgenden Kategorien von Variablen werden als anfänglich zugewiesen klassifiziert:
Statische Variablen.
Instanzvariablen von Klasseninstanzen.
Instanzvariablen der anfänglich zugewiesenen Strukturvariablen.
Array-Elemente.
Wertparameter.
Verweisparameter.
Variablen, die in einer catch -Klausel oder einer -Anweisung deklariert foreach werden.
Anfänglich nicht zugewiesene Variablen
Die folgenden Kategorien von Variablen werden als anfänglich nicht zugewiesen klassifiziert:
Instanzvariablen von anfänglich nicht zugewiesenen Strukturvariablen.
Ausgabeparameter, einschließlich der this Variablen von Strukturinstanzkonstruktoren.
Lokale Variablen, mit Ausnahme der in einer -Klausel oder einer catch -Anweisung foreach deklarierten
Variablen.
Genaue Regeln zum Bestimmen einer bestimmten Zuweisung
Um zu bestimmen, ob jede verwendete Variable definitiv zugewiesen ist, muss der Compiler einen Prozess
verwenden, der dem in diesem Abschnitt beschriebenen entspricht.
Der Compiler verarbeitet den Text jedes Funktionsmitglieds, das über eine oder mehrere anfänglich nicht
zugewiesene Variablen verfügt. Für jede anfänglich nicht zugewiesene Variable v bestimmt der Compiler an
jedem der folgenden Punkte im Funktions member einen ***** bestimmten Zuweisungszustand _ für _v*:
Am Anfang jeder Anweisung
Am Endpunkt(Endpunkte und Erreichbarkeit)jeder Anweisung
In jedem Bogen, der die Steuerung an eine andere Anweisung oder an den Endpunkt einer Anweisung
überträgt
Am Anfang jedes Ausdrucks
Am Ende jedes Ausdrucks
Der eindeutige Zuweisungsstatus von v kann folgende sein:
Definitiv zugewiesen. Dies gibt an, dass v bei allen möglichen Steuerungsflüssen bis zu diesem Punkt ein
Wert zugewiesen wurde.
Nicht definitiv zugewiesen. Für den Zustand einer Variablen am Ende eines Ausdrucks vom Typ bool kann
der Zustand einer Variablen, die nicht definitiv zugewiesen ist, in einen der folgenden Unterzustände fallen
(aber nicht unbedingt):
Definitiv nach true-Ausdruck zugewiesen. Dieser Status gibt an, dass v definitiv zugewiesen wird,
wenn der boolesche Ausdruck als TRUE ausgewertet wird, aber nicht notwendigerweise zugewiesen
wird, wenn der boolesche Ausdruck als FALSE ausgewertet wird.
Definitiv nach false-Ausdruck zugewiesen. Dieser Zustand gibt an, dass v definitiv zugewiesen wird,
wenn der boolesche Ausdruck als FALSE ausgewertet wird, aber nicht notwendigerweise zugewiesen
wird, wenn der boolesche Ausdruck als TRUE ausgewertet wird.
Die folgenden Regeln bestimmen, wie der Zustand einer Variablen v an jedem Standort bestimmt wird.
Allgemeine Regeln für -Anweisungen
v wird am Anfang eines Funktionsmembertexts nicht definitiv zugewiesen.
v wird definitiv am Anfang einer nicht erreichbaren Anweisung zugewiesen.
Der eindeutige Zuweisungszustand von v am Anfang einer anderen Anweisung wird bestimmt, indem der
eindeutige Zuweisungsstatus von v für alle Ablaufsteuerungsübertragungen überprüft wird, die auf den
Anfang dieser Anweisung ausgerichtet sind. Wenn (und nur, wenn) v bei allen derartigen
Ablaufsteuerungsübertragungen definitiv zugewiesen ist, wird v definitiv am Anfang der Anweisung
zugewiesen. Der Satz möglicher Ablaufsteuerungsübertragungen wird auf die gleiche Weise bestimmt wie
bei der Überprüfung der Erreichbarkeit der Anweisung (Endpunkte und Erreichbarkeit).
Der eindeutige Zuweisungszustand von v am Ende eines Blocks, , , , , , , , , , , oder wird bestimmt, indem der
eindeutige Zuweisungsstatus von v bei allen Ablaufsteuerungsübertragungen überprüft wird, die auf den
Endpunkt dieser Anweisung checked unchecked if while do for foreach lock using switch zielen.
Wenn v für alle derartigen Ablaufsteuerungsübertragungen definitiv zugewiesen ist, wird v definitiv am Ende
der Anweisung zugewiesen. Andernfalls; v wird nicht definitiv am Ende der -Anweisung zugewiesen. Der Satz
möglicher Ablaufsteuerungsübertragungen wird auf die gleiche Weise bestimmt wie für die Überprüfung
der Erreichbarkeit der Anweisung (Endpunkte und Erreichbarkeit).
Block-Anweisungen, checked- und unchecked-Anweisungen
Der eindeutige Zuweisungsstatus von v bei der Steuerungsübertragung an die erste Anweisung der
Anweisungsliste im -Block (oder zum Endpunkt des Blocks, wenn die Anweisungsliste leer ist) ist mit der
anweisungsweisen Zuweisung von v vor dem -Block, der -Anweisung oder der -Anweisung checked unchecked
identisch.
Ausdrucksanweisungen
Für eine ausdrucksausdrucks-Anweisung stmt, die aus dem Ausdruck expr besteht:
v weist denselben eindeutigen Zuweisungszustand am Anfang von expr auf wie am Anfang von stmt.
Wenn v auf jeden Fall am Ende von expr zugewiesen wird, wird es definitiv am Endpunkt von stmt
zugewiesen. andernfalls; es ist nicht definitiv am Endpunkt von stmt zugewiesen.
Deklarationsanweisungen
Wenn stmt eine Deklarations-Anweisung ohne Initialisierer ist, hat v am Ende von stmt denselben
eindeutigen Zuweisungszustand wie am Anfang von stmt.
Wenn stmt eine Deklarations-Anweisung mit Initialisierern ist, wird der eindeutige Zuweisungszustand für v
so bestimmt, als wäre stmt eine Anweisungsliste, mit einer Zuweisungs-Anweisung für jede Deklaration mit
einem Initialisierer (in der Reihenfolge der Deklaration).
If-Anweisungen
Für eine if -Anweisung stmt des Folgenden:

if ( expr ) then_stmt else else_stmt

v weist am Anfang von expr den gleichen eindeutigen Zuweisungsstatus wie am Anfang von stmt auf.
Wenn v definitiv am Ende von expr zugewiesen wird, wird es bei der Ablaufsteuerungsübertragung definitiv
an then_stmt und entweder an else_stmt oder an den Endpunkt von stmt zugewiesen, wenn keine else-
Klausel vorhanden ist.
Wenn v den Status "definitiv nach true expression zugewiesen" am Ende von expr aufwies, wird er auf jeden
Fall bei der Ablaufsteuerungsübertragung then_stmt zugewiesen und bei der Ablaufsteuerungsübertragung
nicht definitiv entweder else_stmt oder dem Endpunkt von stmt zugewiesen, wenn keine else-Klausel
vorhanden ist.
Wenn v den Status "definitiv nach false expression zugewiesen" am Ende von expr aufwies, wird er definitiv
bei der Ablaufsteuerungsübertragung an else_stmt zugewiesen und nicht definitiv bei der
Ablaufsteuerungsübertragung an then_stmt. Es wird definitiv am Endpunkt von stmt zugewiesen, wenn und
nur, wenn es definitiv am Endpunkt von then_stmt zugewiesen ist.
Andernfalls wird v bei der Ablaufsteuerungsübertragung nicht als definitiv dem then_stmt oder else_stmt
oder dem Endpunkt von stmt zugewiesen, wenn keine else-Klausel vorhanden ist.
switch-Anweisungen
In einer switch -Anweisung stmt mit einem steuernden Ausdruck expr:
Der eindeutige Zuweisungszustand von v am Anfang von expr entspricht dem Status von v am Anfang von
stmt.
Der eindeutige Zuweisungsstatus von v bei der Ablaufsteuerungsübertragung an eine erreichbare
Switchblock-Anweisungsliste entspricht dem eindeutigen Zuweisungszustand von v am Ende von expr.
While-Anweisungen
Für eine while Stmt-Anweisung im Format:

while ( expr ) while_body

v weist am Anfang von expr den gleichen eindeutigen Zuweisungsstatus wie am Anfang von stmt auf.
Wenn v definitiv am Ende von expr zugewiesen wird, wird es definitiv für die Ablaufsteuerungsübertragung
an while_body und an den Endpunkt von stmt zugewiesen.
Wenn v am Ende von expr den Status "definitiv nach dem true-Ausdruck zugewiesen" hat, wird er definitiv
bei der Ablaufsteuerungsübertragung an while_body zugewiesen, aber nicht definitiv am Ende von stmt
zugewiesen.
Wenn v den Status "definitiv nach false expression" am Ende von expr zugewiesen hat, wird er definitiv bei
der Ablaufsteuerungsübertragung an den Endpunkt von stmt zugewiesen, aber nicht definitiv bei der
Ablaufsteuerungsübertragung an while_body.
Do-Anweisungen
Für eine do -Anweisung stmt im -Formular:

do do_body while ( expr ) ;


v verfügt über denselben bestimmten Zuweisungszustand für die Ablaufsteuerungsübertragung vom Anfang
von stmt zu do_body wie am Anfang von stmt.
v weist denselben bestimmten Zuweisungszustand am Anfang von expr auf wie am Do_body.
Wenn v definitiv am Ende von expr zugewiesen wird, wird es definitiv bei der Ablaufsteuerungsübertragung
an den Endpunkt von stmt zugewiesen.
Wenn v den Status "definitiv nach false expression" am Ende von expr zugewiesen hat, wird er definitiv bei
der Ablaufsteuerungsübertragung an den Endpunkt von stmt zugewiesen.
For-Anweisungen
Überprüfung der definitiven Zuweisung for für eine Anweisung im Folgenden:

for ( for_initializer ; for_condition ; for_iterator ) embedded_statement

wird so durchgeführt, als wäre die Anweisung geschrieben:

{
for_initializer ;
while ( for_condition ) {
embedded_statement ;
for_iterator ;
}
}

Wenn die for_condition anweisung weggelassen wird, wird die Auswertung der definitiven Zuweisung so
fortgesetzt, als ob for_condition in der obigen Erweiterung durch for ersetzt true wurden.
Break-, Continue- und Goto-Anweisungen
Der durch eine -, - oder -Anweisung verursachte eindeutige Zuweisungsstatus von v für die
Ablaufsteuerungsübertragung ist identisch mit dem definitiven Zuweisungszustand von v am Anfang break
der continue goto Anweisung.
Throw-Anweisungen
Für eine Anweisung stmt des Formulars

throw expr ;

Der eindeutige Zuweisungszustand von v am Anfang von expr entspricht dem eindeutigen Zuweisungszustand
von v am Anfang von stmt.
Rückgabeanweisungen
Für eine Anweisung stmt des Formulars

return expr ;

Der eindeutige Zuweisungszustand von v am Anfang von expr entspricht dem eindeutigen
Zuweisungszustand von v am Anfang von stmt.
Wenn v ein Ausgabeparameter ist, muss er definitiv zugewiesen werden:
nach expr
oder am Ende des -Blocks eines oder , das finally try - finally die try - catch - finally
return -Anweisung einschließt.

Für eine Stmt-Anweisung im Format:


return ;

Wenn v ein Ausgabeparameter ist, muss er definitiv zugewiesen werden:


before stmt
oder am Ende des -Blocks eines oder , das finally try - finally die try - catch - finally
return -Anweisung einschließt.

Try-catch-Anweisungen
Für eine Stmt-Anweisung im Format:

try try_block
catch(...) catch_block_1
...
catch(...) catch_block_n

Der eindeutige Zuweisungszustand von v am Anfang von try_block entspricht dem eindeutigen
Zuweisungszustand von v am Anfang von stmt.
Der eindeutige Zuweisungszustand von v am Anfang von catch_block_i (für i) entspricht dem eindeutigen
Zuweisungszustand von v am Anfang von stmt.
Der eindeutige Zuweisungszustand von v am Endpunkt von stmt wird definitiv zugewiesen, wenn (und nur
wenn) v definitiv am Endpunkt von try_block und jedem catch_block_i zugewiesen wird (für jedes i von 1 bis
n).
Try-finally-Anweisungen
Für eine try Stmt-Anweisung im Format:

try try_block finally finally_block

Der eindeutige Zuweisungsstatus von v am Anfang try_block ist identisch mit dem eindeutigen
Zuweisungszustand von v am Anfang von stmt.
Der eindeutige Zuweisungsstatus von v am Anfang finally_block ist identisch mit dem eindeutigen
Zuweisungszustand von v am Anfang von stmt.
Der eindeutige Zuweisungszustand von v am Ende von stmt wird definitiv zugewiesen, wenn mindestens
eine der folgenden Bedingungen zutrifft:
v wird definitiv am Ende des try_block
v wird definitiv am Ende des finally_block
Wenn eine Ablaufsteuerungsübertragung (z. B. eine -Anweisung) erfolgt, die innerhalb von try_block beginnt
und außerhalb von try_block endet, wird v auch als definitiv für diese Ablaufsteuerungsübertragung zugewiesen
angesehen, wenn v definitiv am Ende von goto finally_block zugewiesen ist. (Dies ist nicht nur dann der Fall,
wenn v definitiv aus einem anderen Grund für diese Ablaufsteuerungsübertragung zugewiesen ist, wird es
dennoch als definitiv zugewiesen betrachtet.)
Try-catch-finally-Anweisungen
Definitive Zuweisungsanalyse try - catch - finally für eine Anweisung im Folgenden:

try try_block
catch(...) catch_block_1
...
catch(...) catch_block_n
finally *finally_block*
wird so durchgeführt, als wäre die Anweisung eine -Anweisung, die try - finally eine -Anweisung try -
catch umschließen würde:

try {
try try_block
catch(...) catch_block_1
...
catch(...) catch_block_n
}
finally finally_block

Im folgenden Beispiel wird veranschaulicht, wie sich die verschiedenen Blöcke einer -Anweisung try (Die try-
Anweisung) auf die eindeutige Zuweisung auswirken.

class A
{
static void F() {
int i, j;
try {
goto LABEL;
// neither i nor j definitely assigned
i = 1;
// i definitely assigned
}

catch {
// neither i nor j definitely assigned
i = 3;
// i definitely assigned
}

finally {
// neither i nor j definitely assigned
j = 5;
// j definitely assigned
}
// i and j definitely assigned
LABEL:;
// j definitely assigned

}
}

Foreach-Anweisungen
Für eine foreach -Anweisung stmt des Folgenden:

foreach ( type identifier in expr ) embedded_statement

Der eindeutige Zuweisungsstatus von v am Anfang von expr ist identisch mit dem Zustand von v am Anfang
von stmt.
Der eindeutige Zuweisungsstatus von v für die Ablaufsteuerungsübertragung an embedded_statement oder
an den Endpunkt von stmt ist mit dem Status von v am Ende von expr identisch.
Verwenden von Anweisungen
Für eine using -Anweisung stmt im -Formular:

using ( resource_acquisition ) embedded_statement

Der eindeutige Zuweisungszustand von v am Anfang von resource_acquisition entspricht dem Zustand von v
am Anfang von stmt.
Der eindeutige Zuweisungsstatus von v bei der Ablaufsteuerungsübertragung an embedded_statement
entspricht dem Status von v am Ende resource_acquisition.
Lock-Anweisungen
Für eine lock Stmt-Anweisung im Format:

lock ( expr ) embedded_statement

Der eindeutige Zuweisungszustand von v am Anfang von expr entspricht dem Status von v am Anfang von
stmt.
Der eindeutige Zuweisungszustand von v bei der Ablaufsteuerungsübertragung an embedded_statement
entspricht dem Status von v am Ende von expr.
Yield-Anweisungen
Für eine yield return Stmt-Anweisung im Format:

yield return expr ;

Der eindeutige Zuweisungszustand von v am Anfang von expr entspricht dem Zustand von v am Anfang von
stmt.
Der eindeutige Zuweisungszustand von v am Ende von stmt entspricht dem Status von v am Ende von expr.
Eine yield break -Anweisung hat keine Auswirkungen auf den endgültigen Zuweisungszustand.
Allgemeine Regeln für einfache Ausdrücke
Die folgende Regel gilt für diese Arten von Ausdrücken: Literale (Literale),einfache Namen (Einfache Namen),
Memberzugriffsausdrücke (Memberzugriff), nicht indizierte Basiszugriffsausdrücke (Basiszugriff), typeof
Ausdrücke ( Dertypeof-Operator), Standardwertausdrücke (Standardwertausdrücke) und nameof Ausdrücke
(Nameof-Ausdrücke).
Der eindeutige Zuweisungszustand von v am Ende eines solchen Ausdrucks entspricht dem eindeutigen
Zuweisungszustand von v am Anfang des Ausdrucks.
Allgemeine Regeln für Ausdrücke mit eingebetteten Ausdrücken
Die folgenden Regeln gelten für diese Arten von Ausdrücken: ausdrücke in Klammern ( in Klammern aufgeteilte
Ausdrücke), Elementzugriffsausdrücke (Elementzugriff),Basiszugriffsausdrückemit Indizierung (Basiszugriff),
Inkrement- und Dekrementausdrücke ( Postfix-Inkrement- undDekrementoperatoren,Präfixinkrement- und
Dekrementoperatoren), Cast-Ausdrücke(Cast-Ausdrücke ), unäre , , , Ausdrücke, binäre Ausdrücke , , , , , , , , ,
Ausdrücke ( arithmetische Operatoren + - ~ * + - * / % << >> < <= , > >= == != is as
& | ^ UMSCHALT-Operatoren, relationale checked unchecked operatoren und Typtestoperatoren , Logische
Operatoren ), Zusammengesetzte Zuweisungsausdrücke ( Verbundzuweisung ) und Ausdrücke ( Die aktivierten
und nicht aktivierten Operatoren ) sowie Array- und Delegaterstellungsausdrücke ( Der neue Operator ).
Jeder dieser Ausdrücke verfügt über einen oder mehrere Unterausdrücke, die bedingungslos in einer festen
Reihenfolge ausgewertet werden. Beispielsweise wertet der binäre Operator die linke Seite des Operators %
und dann die rechte Seite aus. Ein Indizierungsvorgang wertet den indizierten Ausdruck aus und wertet dann
jeden Indexausdruck in der Reihenfolge von links nach rechts aus. Für einen Ausdruck expr mit den
Unterausdrücken e1, e2, ..., eN, wird in dieser Reihenfolge ausgewertet:
Der definitive Zuweisungszustand von v am Anfang von e1 entspricht dem eindeutigen Zuweisungszustand
am Anfang von expr.
Der definitive Zuweisungszustand von v am Anfang von ei (i größer als eins) ist derselbe wie der
zustandsbeendete Zuweisung am Ende des vorherigen Unterausdrucks.
Der eindeutige Zuweisungsstatus von v am Ende von expr ist identisch mit dem eindeutigen
Zuweisungszustand am Ende von eN.
Aufrufausdrücke und Objekterstellungsausdrücke
Für einen Aufrufausdruck expr des Formulars:

primary_expression ( arg1 , arg2 , ... , argN )

oder ein Objekterstellungsausdruck des Formulars:

new type ( arg1 , arg2 , ... , argN )

Bei einem Aufrufausdruck entspricht der eindeutige Zuweisungsstatus von v vor primary_expression dem
Zustand von v vor expr.
Bei einem Aufrufausdruck entspricht der eindeutige Zuweisungsstatus von v vor arg1 dem Status von v nach
primary_expression.
Bei einem Objekterstellungsausdruck entspricht der eindeutige Zuweisungsstatus von v vor arg1 dem
Zustand von v vor expr.
Für jedes Argumentargumenti wird der eindeutige Zuweisungszustand von v nach argi durch die normalen
Ausdrucksregeln bestimmt, wobei alle ref - oder out -Modifizierer ignoriert werden.
Für jedes Argumentargument für i größer als eins entspricht der eindeutige Zuweisungsstatus von v vor argi
dem Status von v nach dem vorherigen Arg.
Wenn die Variable v als out Argument (d. h. ein Argument im Format ) in einem der Argumente übergeben
out v wird, wird der Status von v nach expr definitiv zugewiesen. Andernfalls Der Status von v nach expr
entspricht dem Status von v nach argN.
Für Arrayinitialisierer (Arrayerstellungsausdrücke), Objektinitialisierer (Objektinitialisierer),
Auflistungsinitialisierer (Auflistungsinitialisierer) und anonyme Objektinitialisierer (Anonyme
Objekterstellungsausdrücke) wird der eindeutige Zuweisungszustand durch die Erweiterung bestimmt, in der
diese Konstrukte definiert sind.
Einfache Zuweisungsausdrücke
Für einen Ausdruck expr im Format w = expr_rhs :
Der eindeutige Zuweisungsstatus von v vor expr_rhs entspricht dem eindeutigen Zuweisungszustand von v
vor expr.
Der eindeutige Zuweisungsstatus von v nach expr wird durch Folgenden bestimmt:
Wenn w die gleiche Variable wie v ist, wird der eindeutige Zuweisungszustand von v nach expr
definitiv zugewiesen.
Wenn die Zuweisung andernfalls innerhalb des Instanzkonstruktors eines Strukturtyps auftritt, wenn
w eine Eigenschaft ist, die eine automatisch implementierte Eigenschaft P für die zu erstellende
Instanz bestimmt, und v das ausgeblendete Hintergrundfeld von P ist, wird der eindeutige
Zuweisungszustand von v nach expr definitiv zugewiesen.
Andernfalls ist der eindeutige Zuweisungsstatus von v nach expr mit dem eindeutigen
Zuweisungsstatus von v nach expr_rhs.
&& (conditional AND)-Ausdrücke
Für einen Ausdruck expr der Form expr_first && expr_second :
Der eindeutige Zuweisungsstatus von v vor expr_first ist mit dem eindeutigen Zuweisungsstatus von v vor
expr identisch.
Der eindeutige Zuweisungszustand von v vor expr_second wird definitiv zugewiesen, wenn der Status von v
nach expr_first entweder definitiv zugewiesen oder "definitiv nach true expression" zugewiesen ist.
Andernfalls ist sie nicht definitiv zugewiesen.
Der eindeutige Zuweisungsstatus von v nach expr wird durch Folgenden bestimmt:
Wenn expr_first Konstantenausdruck mit dem Wert ist, ist der eindeutige Zuweisungszustand von v
nach expr mit dem eindeutigen Zuweisungsstatus von false v nach expr_first.
Wenn andernfalls der Status von v nach expr_first definitiv zugewiesen ist, wird der Status von v nach
expr definitiv zugewiesen.
Wenn andernfalls der Status von v nach expr_second definitiv zugewiesen ist und der Status von v
nach expr_first "definitiv nach false expression" zugewiesen ist, wird der Status von v nach expr
definitiv zugewiesen.
Wenn der Status von v nach expr_second definitiv zugewiesen oder "definitiv nach true expression
zugewiesen" ist, wird der Status von v nach expr "definitiv nach true expression zugewiesen".
Wenn der Status von v nach expr_first "definitiv nach false expression zugewiesen" und der Status von
v nach expr_second "definitiv nach false expression zugewiesen" ist, wird der Status von v nach expr
"definitiv nach false expression zugewiesen".
Andernfalls wird der Status von v nach expr nicht definitiv zugewiesen.
Im Beispiel

class A
{
static void F(int x, int y) {
int i;
if (x >= 0 && (i = y) >= 0) {
// i definitely assigned
}
else {
// i not definitely assigned
}
// i not definitely assigned
}
}

Die Variable i gilt als definitiv in einer der eingebetteten Anweisungen einer Anweisung if zugewiesen,
jedoch nicht in der anderen. In der -Anweisung in der if -Methode F wird die Variable definitiv in der ersten
i eingebetteten Anweisung zugewiesen, da die Ausführung des (i = y) Ausdrucks immer der Ausführung
dieser eingebetteten Anweisung vorausgeht. Im Gegensatz dazu wird die Variable i in der zweiten
eingebetteten Anweisung nicht definitiv zugewiesen, da x >= 0 möglicherweise false getestet hat, was dazu
führt, dass die Variable i nicht zugewiesen wird.
|| (Bedingte OR)-Ausdrücke
Für einen Ausdruck expr im Format expr_first || expr_second :
Der eindeutige Zuweisungsstatus von v vor expr_first entspricht dem eindeutigen Zuweisungszustand von v
vor expr.
Der eindeutige Zuweisungszustand von v vor expr_second wird definitiv zugewiesen, wenn der Status von v
nach expr_first entweder definitiv zugewiesen oder "definitiv nach false expression zugewiesen" ist.
Andernfalls wird sie nicht definitiv zugewiesen.
Die eindeutige Zuweisungs-Anweisung von v nach expr wird durch Folgendes bestimmt:
Wenn expr_first ein konstanter Ausdruck mit dem Wert true ist, entspricht der eindeutige
Zuweisungsstatus von v nach expr dem eindeutigen Zuweisungszustand von v nach expr_first.
Wenn andernfalls der Status von v nach expr_first definitiv zugewiesen ist, wird der Status von v nach
expr definitiv zugewiesen.
Wenn andernfalls der Status von v nach expr_second definitiv zugewiesen ist und der Status von v
nach expr_first "definitiv nach true expression" zugewiesen ist, wird der Status von v nach expr
definitiv zugewiesen.
Wenn andernfalls der Status von v nach expr_second definitiv oder "definitiv nach false expression
zugewiesen" ist, wird der Status von v nach expr "definitiv nach false expression" zugewiesen.
Wenn andernfalls der Status von v nach expr_first "definitiv nach true expression" zugewiesen wird
und der Status von v nach expr_second "definitiv nach true expression" zugewiesen ist, wird der Status
von v nach expr "definitiv nach true expression" zugewiesen.
Andernfalls wird der Status von v nach expr nicht definitiv zugewiesen.
Im Beispiel

class A
{
static void G(int x, int y) {
int i;
if (x >= 0 || (i = y) >= 0) {
// i not definitely assigned
}
else {
// i definitely assigned
}
// i not definitely assigned
}
}

Die Variable gilt als definitiv in einer der eingebetteten Anweisungen einer -Anweisung i if zugewiesen, aber
nicht in der anderen. In der -Anweisung in der -Methode wird die Variable definitiv in der zweiten eingebetteten
Anweisung zugewiesen, da die Ausführung des Ausdrucks immer der Ausführung dieser eingebetteten if G
Anweisung voraus i (i = y) geht. Im Gegensatz dazu wird die Variable in der ersten eingebetteten
Anweisung nicht definitiv zugewiesen, da möglicherweise TRUE getestet hat, was dazu führt, dass die Variable
i x >= 0 nicht zugewiesen i wird.

! Ausdrücke (logische Negation )


Für einen Ausdruck expr im ! expr_operand Folgenden:
Der eindeutige Zuweisungsstatus von v vor expr_operand ist mit dem eindeutigen Zuweisungsstatus von v
vor expr identisch.
Der eindeutige Zuweisungsstatus von v nach expr wird durch Folgenden bestimmt:
Wenn der Status von v nach *expr_operand *definitiv zugewiesen ist, wird der Status von v nach expr
definitiv zugewiesen.
Wenn der Status von v nach *expr_operand *nicht definitiv zugewiesen ist, wird der Status von v nach
expr nicht definitiv zugewiesen.
Wenn der Status von v nach *expr_operand *definitiv nach false expression zugewiesen ist, wird der
Status von v nach expr "definitiv nach true expression zugewiesen".
Wenn der Status von v nach *expr_operand * "definitiv nach true expression zugewiesen" ist, wird der
Status von v nach expr "definitiv nach false expression zugewiesen".
?? (null coalescing)-Ausdrücke
Für einen Ausdruck expr im Format expr_first ?? expr_second :
Der eindeutige Zuweisungsstatus von v vor expr_first entspricht dem eindeutigen Zuweisungszustand von v
vor expr.
Der eindeutige Zuweisungsstatus von v vor expr_second entspricht dem eindeutigen Zuweisungszustand
von v nach expr_first.
Die eindeutige Zuweisungs-Anweisung von v nach expr wird durch Folgendes bestimmt:
Wenn expr_first ein konstanter Ausdruck (Konstante Ausdrücke) mit dem Wert NULL ist, entspricht
der Status von v nach expr dem Status von v nach expr_second.
Andernfalls entspricht der Status von v nach expr dem eindeutigen Zuweisungszustand von v nach expr_first.
?: (bedingte) Ausdrücke
Für einen Ausdruck expr im Format expr_cond ? expr_true : expr_false :
Der eindeutige Zuweisungszustand von v vor expr_cond entspricht dem Status von v vor expr.
Der eindeutige Zuweisungsstatus von v vor expr_true wird definitiv zugewiesen, wenn und nur dann, wenn
einer der folgenden Punkte zutrifft:
expr_cond ist ein konstanter Ausdruck mit dem Wert false
Der Status von v nach expr_cond ist definitiv zugewiesen oder "definitiv nach dem true-Ausdruck
zugewiesen".
Der eindeutige Zuweisungszustand von v vor expr_false wird definitiv zugewiesen, wenn und nur dann einer
der folgenden Punkte zu folgendem Ergebnis gehört:
expr_cond ist ein konstanter Ausdruck mit dem -Wert. true
der Status von v nach expr_cond ist definitiv zugewiesen oder "definitiv nach false expression
zugewiesen".
Der eindeutige Zuweisungsstatus von v nach expr wird durch Folgenden bestimmt:
Wenn expr_cond Konstantenausdruck (Konstante Ausdrücke ) mit dem Wert ist, ist der Status von v
nach true expr mit dem Status von v nach expr_true.
Andernfalls expr_cond ein konstanter Ausdruck (KonstanteAusdrücke ) mit dem Wert ist, ist der Status
von v nach expr mit dem Status von false v nach expr_false.
Wenn andernfalls der Status von v nach expr_true definitiv zugewiesen ist und der Status von v nach
expr_false definitiv zugewiesen ist, wird der Status von v nach expr definitiv zugewiesen.
Andernfalls wird der Status von v nach expr nicht definitiv zugewiesen.
Anonyme Funktionen
Für einen lambda_expression oder anonymous_method_expression expr mit einem Textkörper (block oder
expression) aus:
Der eindeutige Zuweisungszustand einer äußeren Variablen v vor dem Text ist mit dem Zustand von v vor
expr identisch. Das heißt, der zuweisungszustand äußerer Variablen wird vom Kontext der anonymen
Funktion geerbt.
Der eindeutige Zuweisungszustand einer äußeren Variablen v nach expr ist mit dem Status von v vor expr
identisch.
Das Beispiel

delegate bool Filter(int i);

void F() {
int max;

// Error, max is not definitely assigned


Filter f = (int n) => n < max;

max = 5;
DoWork(f);
}

generiert einen Kompilierzeitfehler, da nicht definitiv zugewiesen max wird, wo die anonyme Funktion
deklariert ist. Das Beispiel
delegate void D();

void F() {
int n;
D d = () => { n = 1; };

d();

// Error, n is not definitely assigned


Console.WriteLine(n);
}

generiert auch einen Kompilierzeitfehler, da die Zuweisung zu n in der anonymen Funktion keinen Einfluss auf
den eindeutigen Zuweisungsstatus n von außerhalb der anonymen Funktion hat.

Variablenverweise
Ein variable_reference ist ein Ausdruck, der als Variable klassifiziert ist. Ein variable_reference gibt einen
Speicherort an, auf den sowohl zum Abrufen des aktuellen Werts als auch zum Speichern eines neuen Werts
zugegriffen werden kann.

variable_reference
: expression
;

In C und C++ wird ein variable_reference als lvalue bezeichnet.

Atomicity of variable references (Unteilbarkeit variabler Verweise)


Lese- und Schreibvorgänge der folgenden Datentypen sind atomar: bool , , , , , , , , char und byte sbyte
short ushort uint int float Verweistypen. Darüber hinaus sind Lese- und Schreibvorgänge von
Enumerationstypen mit einem zugrunde liegenden Typ in der vorherigen Liste ebenfalls atomar. Lese- und
Schreibvorgänge anderer Typen, einschließlich long , , und sowie ulong double decimal benutzerdefinierter
Typen, sind nicht garantiert atomar. Abgesehen von den für diesen Zweck entworfenen Bibliotheksfunktionen
gibt es keine Garantie für atomare Lese-/Änderungs-/Schreibzugriffe, z. B. bei Inkrement oder Dekrementierung.
Konvertierungen
04.11.2021 • 90 minutes to read

Eine -**Konvertierung* _ ermöglicht die Behandlung eines Ausdrucks als einen bestimmten Typ. Eine
Konvertierung kann bewirken, dass ein Ausdruck eines bestimmten Typs als einen anderen Typ behandelt wird,
oder es kann dazu führen, dass ein Ausdruck ohne einen Typ einen Typ erhält. Konvertierungen können implizit
oder _ explizit * sein. Dadurch wird bestimmt, ob eine explizite Umwandlung erforderlich ist. Die Konvertierung
von Typ int in Typ ist beispielsweise long implizit, sodass Ausdrücke vom Typ int implizit als Typ behandelt
werden können long . Die umgekehrte Konvertierung von Typ long in Typ int ist explizit, sodass eine
explizite Umwandlung erforderlich ist.

int a = 123;
long b = a; // implicit conversion from int to long
int c = (int) b; // explicit conversion from long to int

Einige Konvertierungen werden von der Sprache definiert. Programme können auch Ihre eigenen
Konvertierungen (benutzerdefinierte Konvertierungen) definieren.

Implizite Konvertierungen
Die folgenden Konvertierungen werden als implizite Konvertierungen klassifiziert:
Identitäts Konvertierungen
Implizite numerische Konvertierungen
Implizite Enumerationskonvertierungen
Implizite interinterpolierte Zeichen folgen Konvertierungen
Implizite Nullable-Konvertierungen
NULL Literale Konvertierungen
Implizite Verweis Konvertierungen
Boxing-Konvertierungen
Implizite dynamische Konvertierungen
Implizite Konstante Ausdrucks Konvertierungen
Benutzerdefinierte implizite Konvertierungen
Anonymous function conversions (Konvertierung anonymer Funktionen)
Method group conversions (Konvertierung von Methodengruppen)
Implizite Konvertierungen können in einer Vielzahl von Situationen auftreten, einschließlich Funktionsmember-
Aufrufe (Überprüfung der dynamischen Überladungs Auflösung), Umwandlungs Ausdrücke
(UmwandlungsAusdrücke) und Zuweisungen (Zuweisungs Operatoren).
Die vordefinierten impliziten Konvertierungen sind immer erfolgreich und bewirken nie, dass Ausnahmen
ausgelöst werden. Ordnungsgemäß entworfene benutzerdefinierte implizite Konvertierungen sollten auch diese
Merkmale aufweisen.
Bei der Konvertierung werden die Typen object und dynamic als gleichwertig betrachtet.
Dynamische Konvertierungen (implizite dynamische Konvertierungen und explizite dynamische
Konvertierungen) gelten jedoch nur für Ausdrücke vom Typ dynamic (dynamischer Typ).
Identitäts Konvertierung
Eine Identitäts Konvertierung konvertiert von einem beliebigen Typ in denselben Typ. Diese Konvertierung
besteht darin, dass eine Entität, die bereits über einen erforderlichen Typ verfügt, in diesen Typ konvertiert
werden kann.
Da objectund als dynamic Äquivalent angesehen werden, gibt es eine Identitäts Konvertierung zwischen
object und dynamic sowie zwischen konstruierten Typen, die beim Ersetzen aller Vorkommen von dynamic
mit identisch sind object .
Implizite numerische Konvertierungen
Die impliziten numerischen Konvertierungen lauten:
Von sbyte bis short , int , long , float , double oder decimal .
Von byte bis short , ushort , int , uint , long , ulong , float , double oder decimal .
Von short bis int , long , float , double oder decimal .
Von ushort bis int , uint , long , ulong , float , double oder decimal .
Von int bis long , float , double oder decimal .
Von uint bis long , ulong , float , double oder decimal .
Von long nach float , double oder decimal .
Von ulong nach float , double oder decimal .
Von char bis ushort , int , uint , long , ulong , float , double oder decimal .
Von float in double .

Konvertierungen von int , uint , long oder ulong in float und von long oder ulong in double können
zu einem Genauigkeits Verlust führen, verursachen jedoch nie einen Größen Verlust. Die anderen impliziten
numerischen Konvertierungen verlieren nie Informationen.
Es gibt keine impliziten Konvertierungen in den- char Typ, sodass Werte der anderen ganzzahligen Typen nicht
automatisch in den-Typ konvertiert werden char .
Implizite Enumerationskonvertierungen
Eine implizite Enumerationskonvertierung ermöglicht das Konvertieren des decimal_integer_literal 0 in
beliebige enum_type und in beliebige nullable_type , deren zugrunde liegender Typ ein enum_type ist. Im
letzteren Fall wird die Konvertierung ausgewertet, indem Sie in die zugrunde liegende enum_type konvertiert
und das Ergebnis umwickelt (Nullable-Typen).
Implizite interinterpolierte Zeichen folgen Konvertierungen
Eine implizite interpolierte Zeichen folgen Konvertierung ermöglicht das Konvertieren eines
interpolated_string_expression (interpoliertZeichen folgen) in System.IFormattable oder
System.FormattableString (das implementiert System.IFormattable ).

Wenn diese Konvertierung angewendet wird, wird ein Zeichen folgen Wert nicht aus der interinterpolierten
Zeichenfolge zusammengesetzt. Stattdessen wird eine Instanz von System.FormattableString erstellt, die in
interpoliertZeichen folgen weiter unten beschrieben wird.
Implizite Nullable -Konvertierungen
Vordefinierte implizite Konvertierungen, die nicht auf NULL festleg Bare Werttypen angewendet werden,
können auch mit null-fähigen Formularen dieser Typen verwendet werden. Für jede der vordefinierten
impliziten Identitäts-und numerischen Konvertierungen, die von einem Werttyp, der keine NULL-Werte zulässt,
S in einen Werttyp konvertieren, der keine NULL-Werte zulässt T , sind die folgenden impliziten
Konvertierungen vorhanden:
Eine implizite Konvertierung von S? in T? .
Eine implizite Konvertierung von S in T? .
Die Auswertung einer impliziten Konvertierung auf NULL-Werte auf Grundlage einer zugrunde liegenden
Konvertierung von S in T verläuft wie folgt:
Wenn die Konvertierung auf NULL-Werte S? zulässt T? :
Wenn der Quellwert NULL ( HasValue Property ist false) ist, ist das Ergebnis der NULL-Wert des Typs
T? .
Andernfalls wird die Konvertierung als zum Entpacken von S? auf ausgewertet S , gefolgt von der
zugrunde liegenden Konvertierung von S in T , gefolgt von einem Wrapping (Nullable-Typen) von
T in T? .
Wenn die Konvertierung, die NULL-Werte zulässt S , von in ist T? , wird die Konvertierung als die
zugrunde liegende Konvertierung von in ausgewertet, S T gefolgt von einem Wrapping von T in T?
.
NULL Literale Konvertierungen
Eine implizite Konvertierung ist vom null Literaltyp zu einem beliebigen Typ, der NULL-Werte zulässt. Diese
Konvertierung erzeugt den NULL-Wert (Werte zulässt-Typen) des angegebenen Typs, der NULL-Werte zulässt.
Implizite Verweis Konvertierungen
Die impliziten Verweis Konvertierungen sind:
Von allen reference_type zu object und dynamic .
Von allen class_type S zu beliebigen class_type T wird bereitgestellt S von abgeleitet T .
Von allen class_type S zu beliebigen INTERFACE_TYPE T implementiert, S implementiert T .
Von allen INTERFACE_TYPE S zu beliebigen INTERFACE_TYPE T wird bereitgestellt S von abgeleitet T .
Von einer array_type S mit einem Elementtyp SE zu einer array_type T mit einem Elementtyp TE ist
Folgendes angegeben:
S und T unterscheiden sich nur im Elementtyp. Mit anderen Worten, S und T haben die gleiche
Anzahl von Dimensionen.
SE Und TE sind reference_type s.
Eine implizite Verweis Konvertierung ist SE in vorhanden TE .
Von allen array_type zu System.Array und den Schnittstellen, die es implementiert.
Von einem eindimensionalen Arraytyp S[] zu System.Collections.Generic.IList<T> und dessen Basis
Schnittstellen, vorausgesetzt, dass es eine implizite Identität oder Verweis Konvertierung von S in gibt T .
Von allen delegate_type zu System.Delegate und den Schnittstellen, die es implementiert.
Von der NULL-Literale zu beliebigen reference_type.
Von allen reference_type zu einer reference_type , T Wenn Sie über eine implizite Identität oder Verweis
Konvertierung in eine reference_type verfügt T0 und T0 über eine Identitäts Konvertierung zu verfügt T .
Von allen reference_type zu einer Schnittstelle oder einem Delegattyp, T Wenn eine implizite Identität oder
Verweis Konvertierung in eine Schnittstelle oder ein Delegattyp vorhanden T0 T0 ist und die Varianz
konvertierbar ist (Varianz Konvertierung) T .
Implizite Konvertierungen mit Typparametern, die als Verweis Typen bekannt sind. Weitere Informationen zu
impliziten Konvertierungen, die Typparameter einschließen, finden Sie unter implizite Konvertierungen mit
Typparametern .
Die impliziten Verweis Konvertierungen sind Konvertierungen zwischen reference_type en, die sich als immer
erfolgreich erweisen können und daher keine Überprüfungen zur Laufzeit erfordern.
Verweis Konvertierungen, implizit oder explizit, ändern niemals die referenzielle Identität des Objekts, das
konvertiert wird. Anders ausgedrückt: während eine Verweis Konvertierung den Typ des Verweises ändern
kann, ändert Sie niemals den Typ oder Wert des Objekts, auf das verwiesen wird.
Boxing-Konvertierungen
Eine Boxing-Konvertierung ermöglicht das implizite Konvertieren eines value_type in einen Verweistyp. Eine
Boxing-Konvertierung ist von allen non_nullable_value_type zu object und dynamic , zu System.ValueType und
zu allen INTERFACE_TYPE vorhanden, die vom non_nullable_value_type implementiert werden. Außerdem kann
eine enum_type in den-Typ konvertiert werden System.Enum .
Eine Boxing-Konvertierung ist aus einer nullable_type in einen Verweistyp vorhanden, und zwar nur dann, wenn
eine Boxing-Konvertierung von der zugrunde liegenden non_nullable_value_type in den Verweistyp vorhanden
ist.
Ein Werttyp hat eine Boxing-Konvertierung in einen Schnittstellentyp, I Wenn er über eine Boxing-
Konvertierung in einen Schnittstellentyp verfügt I0 und I0 über eine Identitäts Konvertierung zu verfügt I .
Ein Werttyp verfügt über eine Boxing-Konvertierung in einen Schnittstellentyp, I Wenn er eine Boxing-
Konvertierung in eine Schnittstelle oder einen Delegattyp aufweist I0 und I0 Varianz konvertierbar ist
(Varianz Konvertierung) I .
Das Boxing eines Werts einer non_nullable_value_type besteht aus der Zuordnung einer Objektinstanz und dem
Kopieren des value_type Werts in diese Instanz. Eine Struktur kann in den Typ gekapselt werden
System.ValueType , da dies eine Basisklasse für alle Strukturen ist (Vererbung).

Das Boxing eines Werts eines nullable_type verläuft wie folgt:


Wenn der Quellwert NULL ( HasValue Property ist false) ist, ist das Ergebnis ein NULL-Verweis des Zieltyps.
Andernfalls ist das Ergebnis ein Verweis auf einen geschachtelt, der T durch entpacken und Boxing des
Quell Werts erzeugt wird.
Boxing-Konvertierungen werden weiter unten in Boxing-Konvertierungenbeschrieben.
Implizite dynamische Konvertierungen
Eine implizite dynamische Konvertierung ist von einem Ausdruck vom Typ dynamic in einen beliebigen Typ
vorhanden T . Die Konvertierung ist dynamisch gebunden (dynamische Bindung), d. h., es wird zur Laufzeit
eine implizite Konvertierung von dem Lauf Zeittyp des Ausdrucks zu durchgeführt T . Wenn keine
Konvertierung gefunden wird, wird eine Lauf Zeit Ausnahme ausgelöst.
Beachten Sie, dass diese implizite Konvertierung scheinbar gegen den Rat am Anfang von impliziten
Konvertierungen verstößt, dass eine implizite Konvertierung nie eine Ausnahme auslösen sollte. Dabei handelt
es sich jedoch nicht um die Konvertierung, sondern um die Suche nach der Konvertierung, die die Ausnahme
verursacht. Das Risiko von Lauf Zeit Ausnahmen ist die Verwendung der dynamischen Bindung. Wenn die
dynamische Bindung der Konvertierung nicht erwünscht ist, kann der Ausdruck zuerst in object und dann in
den gewünschten Typ konvertiert werden.
Das folgende Beispiel veranschaulicht implizite dynamische Konvertierungen:

object o = "object"
dynamic d = "dynamic";

string s1 = o; // Fails at compile-time -- no conversion exists


string s2 = d; // Compiles and succeeds at run-time
int i = d; // Compiles but fails at run-time -- no conversion exists

Die Zuweisungen für s2 und i beide verwenden implizite dynamische Konvertierungen, bei denen die
Bindung der Vorgänge bis zur Laufzeit angehalten wird. Zur Laufzeit werden implizite Konvertierungen vom
Lauf Zeittyp d -- string -bis zum Zieltyp gesucht. Eine Konvertierung ist in, string aber nicht in enthalten
int .
Implizite Konstante Ausdrucks Konvertierungen
Eine implizite Konstante Ausdrucks Konvertierung ermöglicht die folgenden Konvertierungen:
Eine constant_expression (Konstante Ausdrücke) vom Typ int kann in den Typ sbyte , byte ,, short , oder
konvertiert werden ushort uint ulong , wenn der Wert des constant_expression innerhalb des Bereichs
des Zieltyps liegt.
Eine constant_expression vom Typ long kann in den Typ konvertiert werden ulong , wenn der Wert des
constant_expression nicht negativ ist.
Implizite Konvertierungen mit Typparametern
Die folgenden impliziten Konvertierungen sind für einen bestimmten Typparameter vorhanden T :
Von T bis zu seiner effektiven Basisklasse C , von T auf eine beliebige Basisklasse von C und von auf T
eine beliebige Schnittstelle, die von implementiert wird C . Wenn zur Laufzeit T ein Werttyp ist, wird die
Konvertierung als Boxing-Konvertierung ausgeführt. Andernfalls wird die Konvertierung als implizite
Verweis Konvertierung oder Identitäts Konvertierung ausgeführt.
Von T zu einem Schnittstellentyp I in T den effektiven Schnittstellen Satz und von T auf eine beliebige
Basisschnittstelle von I . Wenn zur Laufzeit T ein Werttyp ist, wird die Konvertierung als Boxing-
Konvertierung ausgeführt. Andernfalls wird die Konvertierung als implizite Verweis Konvertierung oder
Identitäts Konvertierung ausgeführt.
Von T bis zu einem Typparameter hängt von ab U T U (Typparameter Einschränkungen). Wenn zur
Laufzeit U ein Werttyp ist, T U sind und notwendigerweise derselbe Typ, und es wird keine Konvertierung
durchgeführt. Andernfalls, wenn T ein Werttyp ist, wird die Konvertierung als Boxing-Konvertierung
ausgeführt. Andernfalls wird die Konvertierung als implizite Verweis Konvertierung oder Identitäts
Konvertierung ausgeführt.
Von der NULL-Literale bis zu T , T ist bekannt, dass es sich um einen Verweistyp handelt.
Von T zu einem Verweistyp, I Wenn er eine implizite Konvertierung in einen Verweistyp hat S0 und S0
über eine Identitäts Konvertierung in verfügt S . Zur Laufzeit wird die Konvertierung auf die gleiche Weise
ausgeführt wie die Konvertierung in S0 .
Von T zu einem Schnittstellentyp, I Wenn eine implizite Konvertierung in eine Schnittstelle oder einen
Delegattyp I0 I0 erfolgt und die Varianz konvertierbar ist I (Varianz Konvertierung). Wenn zur Laufzeit
T ein Werttyp ist, wird die Konvertierung als Boxing-Konvertierung ausgeführt. Andernfalls wird die
Konvertierung als implizite Verweis Konvertierung oder Identitäts Konvertierung ausgeführt.
Wenn T bekannt ist, dass es sich um einen Verweistyp handelt (Typparameter Einschränkungen), werden die
obigen Konvertierungen als implizite Verweis Konvertierungen (implizite Verweis Konvertierungen) klassifiziert.
Wenn T nicht bekannt ist, dass es sich um einen Verweistyp handelt, werden die obigen Konvertierungen als
boxkonvertierungen (Boxing-Konvertierungen) klassifiziert.
Benutzerdefinierte implizite Konvertierungen
Eine benutzerdefinierte implizite Konvertierung besteht aus einer optionalen standardmäßigen impliziten
Konvertierung, gefolgt von der Ausführung eines benutzerdefinierten impliziten Konvertierungs Operators,
gefolgt von einer anderen optionalen standardmäßigen impliziten Konvertierung. Die genauen Regeln für das
Auswerten benutzerdefinierter impliziter Konvertierungen werden bei der Verarbeitung von
benutzerdefinierten impliziten Konvertierungenbeschrieben.
Anonyme Funktions Konvertierungen und Methoden Gruppen Konvertierungen
Anonyme Funktionen und Methoden Gruppen weisen keine Typen in und von sich selbst auf, können aber
implizit in Delegattypen oder Ausdrucks Baumstruktur konvertiert werden. Anonyme Funktions
Konvertierungen werden ausführlicher in Anonyme Funktions Konvertierungen und Methoden Gruppen
Konvertierungen in Methoden Gruppen Konvertierungenbeschrieben.
Explizite Konvertierungen
Die folgenden Konvertierungen werden als explizite Konvertierungen klassifiziert:
Alle impliziten Konvertierungen.
Explizite numerische Konvertierungen.
Explizite Enumerationskonvertierungen.
Explizite Konvertierungen, die NULL zulassen.
Explizite Verweis Konvertierungen.
Explizite Schnittstellen Konvertierungen.
Unboxing-Konvertierungen.
Explizite dynamische Konvertierungen
Benutzerdefinierte explizite Konvertierungen.
Explizite Konvertierungen können in Umwandlungs Ausdrücken (UmwandlungsAusdrücke) auftreten.
Der Satz expliziter Konvertierungen umfasst alle impliziten Konvertierungen. Dies bedeutet, dass redundante
Umwandlungs Ausdrücke zulässig sind.
Bei den expliziten Konvertierungen, bei denen es sich nicht um implizite Konvertierungen handelt, handelt es
sich um Konvertierungen, die sich nicht immer als erfolgreich erweisen können, Konvertierungen, die
bekanntermaßen Informationen verlieren, und Konvertierungen zwischen Domänen von Typen ausreichend
verschieden
Explizite numerische Konvertierungen
Die expliziten numerischen Konvertierungen sind Konvertierungen von einer numeric_type in eine andere
numeric_type , für die eine implizite numerische Konvertierung (implizite numerische Konvertierungen) nicht
bereits vorhanden ist:
Von sbyte bis byte , ushort , uint , ulong oder char .
Von byte zu sbyte und char .
Von short bis sbyte , byte , ushort , uint , ulong oder char .
Von ushort bis sbyte , byte , short oder char .
Von int bis sbyte , byte , short , ushort , uint , ulong oder char .
Von uint bis sbyte , byte , short , ushort , int oder char .
Von long bis sbyte , byte , short , ushort , int , uint , ulong oder char .
Von ulong bis sbyte , byte , short , ushort , int , uint , long oder char .
Von char nach sbyte , byte oder short .
Von float bis sbyte , byte , short , ushort , int , uint , long , ulong , char oder decimal .
Von double bis sbyte , byte , short , ushort , int , uint , long , ulong , char , float oder decimal
.
Von decimal bis sbyte , byte , short , ushort , int , uint , long , ulong , char , float oder double
.
Da die expliziten Konvertierungen alle impliziten und expliziten numerischen Konvertierungen einschließen, ist
es immer möglich, mithilfe eines Umwandlungs Ausdrucks (Umwandlungs Ausdrücke) von beliebigen
numeric_type in andere numeric_type zu konvertieren.
Die expliziten numerischen Konvertierungen verlieren möglicherweise Informationen oder bewirken
möglicherweise, dass Ausnahmen ausgelöst werden. Eine explizite numerische Konvertierung wird wie folgt
verarbeitet:
Für eine Konvertierung eines ganzzahligen Typs in einen anderen ganzzahligen Typ hängt die Verarbeitung
vom Kontext der Überlauf Überprüfung (die aktivierten und deaktiviertenOperatoren) ab, in der die
Konvertierung stattfindet:
In einem checked Kontext wird die Konvertierung erfolgreich ausgeführt, wenn der Wert des Quell
Operanden innerhalb des Bereichs des Zieltyps liegt, aber eine auslöst, System.OverflowException
Wenn der Wert des Quell Operanden außerhalb des Bereichs des Zieltyps liegt.
In einem unchecked Kontext ist die Konvertierung immer erfolgreich und wird wie folgt fortgesetzt.
Wenn der Quelltyp größer als der Zieltyp ist, wird der Quellwert abgeschnitten, indem die
wichtigsten „zusätzlichen“ Teile verworfen werden. Das Ergebnis wird dann als Wert des
Zieltyps behandelt.
Wenn der Quelltyp kleiner als der Zieltyp ist, wird der Quellwert entweder mit Vorzeichen oder
Nullen (0) erweitert, sodass er die gleiche Größe wie der Zieltyp aufweist. Die
Vorzeichenerweiterung wird verwendet, wenn der Quelltyp mit einem Vorzeichen versehen ist.
Die Erweiterung mit Nullen (0) wird verwendet, wenn der Quelltyp mit keinem Vorzeichen
versehen ist. Das Ergebnis wird dann als Wert des Zieltyps behandelt.
Wenn der Quelltyp die gleiche Größe wie der Zieltyp aufweist, wird der Quellwert als Wert vom
Zieltyp behandelt.
Bei einer Konvertierung von decimal in einen ganzzahligen Typ wird der Quellwert auf 0 (null) bis zum
nächstgelegenen ganzzahligen Wert gerundet, und dieser ganzzahlige Wert wird zum Ergebnis der
Konvertierung. Wenn sich der resultierende ganzzahlige Wert außerhalb des Bereichs des Zieltyps befindet,
wird eine ausgelöst System.OverflowException .
Für eine Konvertierung von float oder double in einen ganzzahligen Typ hängt die Verarbeitung vom
Kontext der Überlauf Überprüfung (die aktivierten und deaktiviertenOperatoren) ab, in der die Konvertierung
stattfindet:
In einem checked Kontext erfolgt die Konvertierung wie folgt:
Wenn der Wert des Operanden NaN oder Infinite ist, wird eine ausgelöst
System.OverflowException .
Andernfalls wird der Quell Operand auf den nächsten ganzzahligen Wert in Richtung 0 (null)
gerundet. Wenn sich dieser ganzzahlige Wert innerhalb des Bereichs des Zieltyps befindet, ist
dieser Wert das Ergebnis der Konvertierung.
Andernfalls wird eine System.OverflowException ausgelöst.
In einem unchecked Kontext ist die Konvertierung immer erfolgreich und wird wie folgt fortgesetzt.
Wenn der Wert des Operanden NaN oder Infinite ist, ist das Ergebnis der Konvertierung ein
nicht spezifizierter Wert des Zieltyps.
Andernfalls wird der Quell Operand auf den nächsten ganzzahligen Wert in Richtung 0 (null)
gerundet. Wenn sich dieser ganzzahlige Wert innerhalb des Bereichs des Zieltyps befindet, ist
dieser Wert das Ergebnis der Konvertierung.
Andernfalls ist das Ergebnis der Konvertierung ein nicht spezifizierter Wert des Zieltyps.
Bei einer Konvertierung von double in float wird der double Wert auf den nächstgelegenen Wert
gerundet float . Wenn der double Wert zu klein ist, um als darzustellen float , wird das Ergebnis positiv
0 (null) oder negativ 0 (null). Wenn der double Wert zu groß ist, um als darzustellen float , wird das
Ergebnis positiv unendlich oder minus unendlich. Wenn der double Wert "NaN" ist, ist das Ergebnis
ebenfalls "NaN".
Bei einer Konvertierung von float oder double in decimal wird der Quellwert in eine decimal -
Darstellung konvertiert und bei Bedarf auf die nächste Zahl nach dem 28. Dezimaltrennzeichen gerundet
(der Decimal-Typ). Wenn der Quellwert zu klein ist, um als darzustellen decimal , wird das Ergebnis 0 (null).
Wenn der Quellwert Nan, Infinity oder zu groß ist, um als darzustellen decimal , wird eine ausgelöst
System.OverflowException .
Bei einer Konvertierung von decimal in float oder double wird der decimal Wert auf den
nächstgelegenen- double Wert oder-Wert gerundet float . Während diese Konvertierung die Genauigkeit
verlieren kann, bewirkt dies nie, dass eine Ausnahme ausgelöst wird.
Explizite Enumerationskonvertierungen
Die expliziten Enumerationskonvertierungen sind:
From sbyte , byte , short , ushort , int , uint , long , ulong , char , float , double oder decimal
zu beliebigen enum_type.
Von allen enum_type bis sbyte , byte , short , ushort , int , uint , long , ulong , char , float ,
double oder decimal .
Von allen enum_type zu anderen enum_type.
Eine explizite Enumerationskonvertierung zwischen zwei Typen wird verarbeitet, indem alle Beteiligten
enum_type als zugrunde liegender Typ dieser enum_type behandelt und dann eine implizite oder explizite
numerische Konvertierung zwischen den resultierenden Typen durchgeführt wird. Wenn beispielsweise ein
enum_type E mit und dem zugrunde liegenden Typ von angegeben wird int , wird eine Konvertierung von
E in byte als explizite numerische Konvertierung (explizite numerische Konvertierungen) von int in
verarbeitet byte , und eine Konvertierung von byte in E wird als implizite numerische Konvertierung
(implizite numerische Konvertierungen) von byte in verarbeitet int .
Explizite Nullable -Konvertierungen
Explizite Konver tierungen , die NULL-Werte zulassen, ermöglichen vordefinierte explizite Konvertierungen,
die für nicht auf NULL festleg Bare Werttypen verwendet werden, auch mit null-fähigen Formen dieser Typen
Für jede der vordefinierten expliziten Konvertierungen, die von einem Werttyp, der keine NULL-Werte zulässt,
S in einen nicht auf NULL festleg baren Werttyp konvertieren T (Identitäts Konvertierung, implizite
numerische Konvertierungen, Implizite Enumerationskonvertierungen, explizite numerische
Konvertierungenund Explizite Enumerationskonvertierungen), sind folgende Konvertierungen vorhanden:
Eine explizite Konvertierung von S? in T? .
Eine explizite Konvertierung von S in T? .
Eine explizite Konvertierung von S? in T .

Die Auswertung einer Konvertierung, die NULL-Werte zulässt, basierend auf einer zugrunde liegenden
Konvertierung von S in T verläuft wie folgt:
Wenn die Konvertierung auf NULL-Werte S? zulässt T? :
Wenn der Quellwert NULL ( HasValue Property ist false) ist, ist das Ergebnis der NULL-Wert des Typs
T? .
Andernfalls wird die Konvertierung als ein zum Entpacken von in ausgewertet S? S , gefolgt von
der zugrunde liegenden Konvertierung von S in T , gefolgt von einem Wrapping von T in T? .
Wenn die Konvertierung, die NULL-Werte zulässt S , von in ist T? , wird die Konvertierung als die
zugrunde liegende Konvertierung von in ausgewertet, S T gefolgt von einem Wrapping von T in T? .
Wenn die Konvertierung, die NULL-Werte zulässt S? , von in ist T , wird die Konvertierung als ein zum
Entpacken von auf ausgewertet, S? S gefolgt von der zugrunde liegenden Konvertierung von S in T .

Beachten Sie, dass beim Versuch, einen Werte zulässt-Wert zu entpacken, eine Ausnahme ausgelöst wird, wenn
der Wert ist null .
Explizite Verweis Konvertierungen
Die expliziten Verweis Konvertierungen lauten:
Von object und dynamic zu allen anderen reference_type.
Von allen class_type S bis class_type T S ist eine Basisklasse von T .
Von allen class_type S zu beliebigen INTERFACE_TYPE T ist die bereitgestellte S nicht versiegelt und wird
S nicht implementiert T .
Von allen INTERFACE_TYPE S zu beliebigen class_type T ist die bereitgestellte T nicht versiegelt oder T
implementiert implementiert S .
Aus beliebigen INTERFACE_TYPE S in beliebige INTERFACE_TYPE T wird der bereitgestellte S nicht von
abgeleitet T .
Von einer array_type S mit einem Elementtyp SE zu einer array_type T mit einem Elementtyp TE ist
Folgendes angegeben:
S und T unterscheiden sich nur im Elementtyp. Mit anderen Worten, S und T haben die gleiche
Anzahl von Dimensionen.
SE Und TE sind reference_type s.
Eine explizite Verweis Konvertierung ist SE in vorhanden TE .
Von System.Array und den Schnittstellen, die es implementiert, in beliebige array_type.
Von einem eindimensionalen Arraytyp S[] zu System.Collections.Generic.IList<T> und dessen Basis
Schnittstellen, vorausgesetzt, dass es eine explizite Verweis Konvertierung von S in gibt T .
Von System.Collections.Generic.IList<S> und seinen Basis Schnittstellen zu einem eindimensionalen
Arraytyp T[] , vorausgesetzt, dass es eine explizite Identitäts-oder Verweis Konvertierung von S in gibt T
.
Von System.Delegate und den Schnittstellen, die es implementiert, in beliebige delegate_type.
Von einem Referenztyp zu einem Verweistyp, T Wenn er eine explizite Verweis Konvertierung in einen
Verweistyp hat T0 und T0 über eine Identitäts Konvertierung verfügt T .
Von einem Referenztyp zu einer Schnittstelle oder einem Delegattyp, T Wenn er eine explizite Verweis
Konvertierung in eine Schnittstelle oder einen Delegattyp aufweist T0 und entweder T0 Varianz
konvertierbar in T oder T ist Varianz konvertierbar in T0 (Varianz Konvertierung).
Von D<S1...Sn> bis zu, der D<T1...Tn> D<X1...Xn> ein generischer Delegattyp ist, D<S1...Sn> nicht mit
oder identisch mit ist D<T1...Tn> und für jeden Typparameter Xi der D folgenden enthält:
Wenn Xi invariant ist, dann Si ist identisch mit Ti .
Wenn Xi kovariant ist, gibt es eine implizite oder explizite Identitäts-oder Verweis Konvertierung von
Si in Ti .
Wenn Xi kontra Variant ist, Si sind und Ti entweder identische oder beide Verweis Typen.
Explizite Konvertierungen mit Typparametern, die als Verweis Typen bekannt sind. Weitere Informationen zu
expliziten Konvertierungen, die Typparameter einschließen, finden Sie unter explizite Konvertierungen mit
Typparametern.
Die expliziten Verweis Konvertierungen sind Konvertierungen zwischen Verweis Typen, für die
Laufzeitüberprüfungen erforderlich sind, um sicherzustellen, dass Sie korrekt sind.
Damit eine explizite Verweis Konvertierung zur Laufzeit erfolgreich ist, muss der Wert des Quell Operanden
lauten null , oder der tatsächliche Typ des Objekts, auf das vom Quell Operanden verwiesen wird, muss ein
Typ sein, der durch eine implizite Verweis Konvertierung (implizite Verweis Konvertierungen) oder
boxkonvertierungen in den Zieltyp konvertiertwerden kann. Wenn eine explizite Verweis Konvertierung
fehlschlägt, System.InvalidCastException wird eine ausgelöst.
Verweis Konvertierungen, implizit oder explizit, ändern niemals die referenzielle Identität des Objekts, das
konvertiert wird. Anders ausgedrückt: während eine Verweis Konvertierung den Typ des Verweises ändern
kann, ändert Sie niemals den Typ oder Wert des Objekts, auf das verwiesen wird.
Unboxing-Konvertierungen
Eine Unboxing-Konvertierung ermöglicht das explizite Konvertieren eines Verweis Typs in einen value_type. Eine
Unboxing-Konvertierung ist von den Typen object , von dynamic System.ValueType allen
non_nullable_value_type und von allen INTERFACE_TYPE zu allen non_nullable_value_type , die die
INTERFACE_TYPE implementiert, vorhanden. Außerdem kann der Typ System.Enum in beliebige enum_type
entpackt werden.
Eine Unboxing-Konvertierung ist von einem Referenztyp zu einem nullable_type vorhanden, wenn eine
Unboxing-Konvertierung vom Verweistyp in den zugrunde liegenden non_nullable_value_type der nullable_type
vorhanden ist.
Ein Werttyp S hat eine Unboxing-Konvertierung von einem Schnittstellentyp I , wenn er eine Unboxing-
Konvertierung von einem Schnittstellentyp aufweist I0 und I0 über eine Identitäts Konvertierung zu verfügt
I .

Ein Werttyp S verfügt über eine Unboxing-Konvertierung von einem Schnittstellentyp, I Wenn eine
Unboxing-Konvertierung von einer Schnittstelle oder einem Delegattyp I0 erfolgt und entweder I0 Varianz
konvertierbar in I oder I Varianz konvertierbar ist I0 (Varianz Konvertierung).
Ein Unboxing-Vorgang besteht darin, zuerst zu überprüfen, ob die Objektinstanz ein geachtelter Wert der
angegebenen value_type ist, und dann den Wert aus der-Instanz zu kopieren. Beim Unboxing eines NULL-
Verweises auf einen nullable_type wird der NULL-Wert des nullable_type erzeugt. Eine Struktur kann vom Typ
entpackt werden System.ValueType , da dies eine Basisklasse für alle Strukturen ist (Vererbung).
Unboxing-Konvertierungen werden weiter unten in Unboxing-Konvertierungenbeschrieben.
Explizite dynamische Konvertierungen
Eine explizite dynamische Konvertierung ist von einem Ausdruck vom Typ dynamic in einen beliebigen Typ
vorhanden T . Die Konvertierung ist dynamisch gebunden (dynamische Bindung), was bedeutet, dass zur
Laufzeit eine explizite Konvertierung vom Lauf Zeittyp des Ausdrucks zu durchgeführt wird T . Wenn keine
Konvertierung gefunden wird, wird eine Lauf Zeit Ausnahme ausgelöst.
Wenn die dynamische Bindung der Konvertierung nicht erwünscht ist, kann der Ausdruck zuerst in object und
dann in den gewünschten Typ konvertiert werden.
Nehmen Sie an, dass die folgende Klasse definiert ist:

class C
{
int i;

public C(int i) { this.i = i; }

public static explicit operator C(string s)


{
return new C(int.Parse(s));
}
}

Das folgende Beispiel veranschaulicht explizite dynamische Konvertierungen:

object o = "1";
dynamic d = "2";

var c1 = (C)o; // Compiles, but explicit reference conversion fails


var c2 = (C)d; // Compiles and user defined conversion succeeds

Die beste Konvertierung von o in C wird zur Kompilierzeit als explizite Verweis Konvertierung gefunden. Dies
schlägt zur Laufzeit fehl, weil "1" nicht tatsächlich a ist C . Die Konvertierung von d in C wird jedoch als
explizite dynamische Konvertierung zur Laufzeit angehalten, bei der eine benutzerdefinierte Konvertierung vom
Lauf Zeittyp von d -- string -in C gefunden wird und erfolgreich ist.
Explizite Konvertierungen mit Typparametern
Die folgenden expliziten Konvertierungen sind für einen bestimmten Typparameter vorhanden T :
Von der effektiven Basisklasse C von T zu T und von einer beliebigen Basisklasse von C zu T . Wenn
zur Laufzeit T ein Werttyp ist, wird die Konvertierung als Unboxing-Konvertierung ausgeführt. Andernfalls
wird die Konvertierung als explizite Verweis Konvertierung oder Identitäts Konvertierung ausgeführt.
Von einem beliebigen Schnittstellentyp zu T . Wenn zur Laufzeit T ein Werttyp ist, wird die Konvertierung
als Unboxing-Konvertierung ausgeführt. Andernfalls wird die Konvertierung als explizite Verweis
Konvertierung oder Identitäts Konvertierung ausgeführt.
Von T zu allen I bereitgestellten INTERFACE_TYPE gibt es keine implizite Konvertierung von T in I .
Wenn zur Laufzeit T ein Werttyp ist, wird die Konvertierung als Boxing-Konvertierung gefolgt von einer
expliziten Verweis Konvertierung ausgeführt. Andernfalls wird die Konvertierung als explizite Verweis
Konvertierung oder Identitäts Konvertierung ausgeführt.
Von einem Typparameter U zu T abhängig ist, T hängt von ab U (Typparameter Einschränkungen).
Wenn zur Laufzeit U ein Werttyp ist, T U sind und notwendigerweise derselbe Typ, und es wird keine
Konvertierung durchgeführt. Andernfalls, wenn T ein Werttyp ist, wird die Konvertierung als Unboxing-
Konvertierung ausgeführt. Andernfalls wird die Konvertierung als explizite Verweis Konvertierung oder
Identitäts Konvertierung ausgeführt.
Wenn T bekannt ist, dass es sich um einen Verweistyp handelt, werden die obigen Konvertierungen als
explizite Verweis Konvertierungen klassifiziert (explizite Verweis Konvertierungen). Wenn T nicht bekannt ist,
dass es sich um einen Verweistyp handelt, werden die obigen Konvertierungen als Unboxing-Konvertierungen
(Unboxing-Konvertierungen) klassifiziert.
Die obigen Regeln erlauben keine direkte explizite Konvertierung von einem uneingeschränkten Typparameter
in einen nicht-Schnittstellentyp, was möglicherweise überraschend ist. Der Grund für diese Regel besteht darin,
Verwirrung zu vermeiden und die Semantik solcher Konvertierungen zu löschen. Betrachten Sie beispielsweise
die folgende Deklaration:

class X<T>
{
public static long F(T t) {
return (long)t; // Error
}
}

Wenn die direkte explizite Konvertierung von t in int zulässig wäre, könnte man leicht erwarten, dass
X<int>.F(7) zurückgibt 7L . Dies würde jedoch nicht der Fall sein, da die standardmäßigen numerischen
Konvertierungen nur berücksichtigt werden, wenn die Typen bekannt sind, dass Sie zur Bindungs Zeit numerisch
sind. Damit die Semantik eindeutig ist, muss das obige Beispiel stattdessen geschrieben werden:

class X<T>
{
public static long F(T t) {
return (long)(object)t; // Ok, but will only work when T is long
}
}

Dieser Code wird jetzt kompiliert, aber durch X<int>.F(7) die Ausführung von wird zur Laufzeit eine Ausnahme
ausgelöst, da ein geschachtelt int nicht direkt in eine konvertiert werden kann long .
Benutzerdefinierte explizite Konvertierungen
Eine benutzerdefinierte explizite Konvertierung besteht aus einer optionalen expliziten Standard Konvertierung,
gefolgt von der Ausführung eines benutzerdefinierten impliziten oder expliziten Konvertierungs Operators,
gefolgt von einer anderen optionalen expliziten Standard Konvertierung. Die genauen Regeln für das Auswerten
benutzerdefinierter expliziter Konvertierungen werden bei der Verarbeitung benutzerdefinierter expliziter
Konvertierungenbeschrieben.

Standardkonvertierungen
Bei den Standard Konvertierungen handelt es sich um vordefinierte Konvertierungen, die als Teil einer
benutzerdefinierten Konvertierung auftreten können.
Implizite Standard Konvertierungen
Die folgenden impliziten Konvertierungen werden als implizite Standard Konvertierungen klassifiziert:
Identitäts Konvertierungen (Identitäts Konvertierung)
Implizite numerische Konvertierungen (implizite numerische Konvertierungen)
Implizite Konvertierungen, die NULL-Werte zulassen (implizite Konvertierungen auf NULL
Implizite Verweis Konvertierungen (implizite Verweis Konvertierungen)
Boxing-Konvertierungen (Boxing-Konvertierungen)
Implizite Konstante Ausdrucks Konvertierungen (implizite Konstante Ausdrucks Konvertierungen)
Implizite Konvertierungen mit Typparametern (implizite Konvertierungen mit Typparametern)
Die standardmäßigen impliziten Konvertierungen schließen speziell benutzerdefinierte implizite
Konvertierungen aus.
Explizite Standard Konvertierungen
Die expliziten Standard Konvertierungen sind alle standardmäßigen impliziten Konvertierungen und die
Teilmenge der expliziten Konvertierungen, für die eine gegenteilige standardmäßige implizite Konvertierung
vorhanden ist. Anders ausgedrückt: Wenn eine implizite Standard Konvertierung von einem Typ A in einen Typ
vorhanden B ist, ist eine explizite Standard Konvertierung von Typ A in Typ B und vom Typ B in Typ
vorhanden A .

Benutzerdefinierte Konvertierungen
In c# können die vordefinierten und expliziten Konvertierungen durch benutzerdefinier te Konver tierungen
erweitert werden. Benutzerdefinierte Konvertierungen werden durch das Deklarieren von Konvertierungs
Operatoren (Konvertierungs Operatoren) in Klassen-und Strukturtypen eingeführt.
Zulässige benutzerdefinierte Konvertierungen
In c# können nur bestimmte benutzerdefinierte Konvertierungen deklariert werden. Insbesondere ist es nicht
möglich, eine bereits vorhandene implizite oder explizite Konvertierung neu zu definieren.
Geben Sie für einen angegebenen Quelltyp S und Zieltyp T , wenn S oder T NULL-Werte zulassen, die S0
T0 zugrunde liegenden Typen an, und verweisen Sie andernfalls S0 T0 gleich S T bzw. Eine Klasse oder
Struktur darf nur dann eine Konvertierung von einem Quelltyp S in einen Zieltyp deklarieren, T wenn
Folgendes zutrifft:
S0 und T0 sind unterschiedliche Typen.
Entweder S0 oder T0 ist der Klassen-oder Strukturtyp, in dem die Operator Deklaration stattfindet.
Weder S0 noch T0 ist eine INTERFACE_TYPE.
Ohne benutzerdefinierte Konvertierungen ist eine Konvertierung von S zu T oder von zu nicht vorhanden
T S .

Die Einschränkungen, die für benutzerdefinierte Konvertierungen gelten, werden in den Konvertierungs
Operatorenweiter erläutert.
„Lifted“ Konvertierungsoperatoren
Bei einem benutzerdefinierten Konvertierungs Operator, der von einem Werttyp, der keine NULL-Werte zulässt,
S in einen nicht auf NULL festleg baren Werttyp konvertiert T , ist ein Operator mit erhöhten
Konver tierungen vorhanden, der von S? in T? Dieser Operator für die gesteigerte Konvertierung führt ein
zum Entpacken von auf aus, S? S gefolgt von der benutzerdefinierten Konvertierung von S in T , gefolgt
von einem Wrapping von T in, mit dem Unterschied T? , dass ein NULL-Wert S? direkt in einen NULL-Wert
konvertiert T? .
Ein Operator mit erhöhten Konvertierungen hat dieselbe implizite oder explizite Klassifizierung wie der
zugrunde liegende benutzerdefinierte Konvertierungs Operator. Der Begriff "benutzerdefinierte Konvertierung"
gilt für die Verwendung von benutzerdefinierten und erhöhten Konvertierungs Operatoren.
Auswertung von benutzerdefinierten Konvertierungen
Eine benutzerdefinierte Konvertierung konvertiert einen Wert aus seinem Typ, der als Quelltyp _ bezeichnet
wird, in einen anderen Typ, der als _Zieltyp*_ bezeichnet wird. Die Auswertung einer benutzerdefinierten
Konvertierung konzentriert sich auf das Auffinden des _ spezifischsten* benutzerdefinierten Konvertierungs
Operators für bestimmte Quell-und Zieltypen. Diese Bestimmung ist in mehrere Schritte unterteilt:
Suchen des Satzes von Klassen und Strukturen, von denen benutzerdefinierte Konvertierungs Operatoren
berücksichtigt werden. Diese Gruppe besteht aus dem Quelltyp und den zugehörigen Basisklassen sowie
dem Zieltyp und den zugehörigen Basisklassen (mit den impliziten Annahmen, dass nur Klassen und
Strukturen benutzerdefinierte Operatoren deklarieren können, und dass nicht Klassentypen keine
Basisklassen aufweisen). Wenn entweder der Quell-oder Zieltyp ein nullable_type ist, wird stattdessen der
zugrunde liegende Typ verwendet, wenn der Quell-oder Zieltyp ein ist.
Aus diesem Satz von Typen, um zu bestimmen, welche benutzerdefinierten und erhöhten Konvertierungs
Operatoren anwendbar sind. Damit ein Konvertierungs Operator anwendbar ist, muss es möglich sein, eine
Standard Konvertierung (Standard Konvertierungen) vom Quelltyp in den Operanden des Operators
auszuführen. Außerdem muss es möglich sein, eine Standard Konvertierung vom Ergebnistyp des Operators
in den Zieltyp auszuführen.
Aus dem Satz der anwendbaren benutzerdefinierten Operatoren, wobei festgelegt wird, welcher Operator
eindeutig der spezifischsten ist. In der Regel ist der spezifischere Operator der Operator, dessen Operanden
dem Quelltyp "am nächsten" und dessen Ergebnistyp "am nächsten" dem Zieltyp entspricht.
Benutzerdefinierte Konvertierungs Operatoren werden für gesteigerte Konvertierungs Operatoren
bevorzugt. Die genauen Regeln zum Einrichten des spezifischsten benutzerdefinierten Konvertierungs
Operators werden in den folgenden Abschnitten definiert.
Nachdem ein spezifiziererer benutzerdefinierter Konvertierungs Operator identifiziert wurde, umfasst die
tatsächliche Ausführung der benutzerdefinierten Konvertierung bis zu drei Schritte:
Wenn dies erforderlich ist, wird eine Standard Konvertierung vom Quelltyp in den Operanden des
benutzerdefinierten oder des aufzurufenden Konvertierungs Operators durchgeführt.
Im nächsten Schritt wird der benutzerdefinierte oder der Operator für die Aufhebung der Konvertierung
aufgerufen, um die Konvertierung auszuführen.
Wenn dies erforderlich ist, wird eine Standard Konvertierung vom Ergebnistyp des benutzerdefinierten oder
des Operators für die gesteigerte Konvertierung in den Zieltyp durchgeführt.
Die Auswertung einer benutzerdefinierten Konvertierung umfasst nie mehr als einen benutzerdefinierten oder
einen Operator mit erhöhten Konvertierungen. Anders ausgedrückt: eine Konvertierung von Typ S in Typ T
führt nie zuerst eine benutzerdefinierte Konvertierung von S in aus und führt X dann eine benutzerdefinierte
Konvertierung von X in aus T .
Die genauen Definitionen der Auswertung benutzerdefinierter impliziter oder expliziter Konvertierungen
werden in den folgenden Abschnitten angegeben. In den Definitionen werden die folgenden Begriffe verwendet:
Wenn eine implizite Standard Konvertierung (standardmäßige implizite Konvertierungen) von einem Typ A
in einen Typ vorhanden B ist, und wenn weder A noch B INTERFACE_TYPE s vorhanden sind, A wird als
*durch _ eingeschlossen B , und B wird als _ umfasst bezeichnet * A .
Der umfassendste Typ in einer Reihe von Typen ist der einzige Typ, der alle anderen Typen im Satz umfasst.
Wenn kein einzelner Typ alle anderen Typen umfasst, hat der Satz keinen ganz umfassenden Typ. In intuitiver
Hinsicht ist der umfassendste Typ der "größte" Typ im Satz – der einzige Typ, in den jeder der anderen Typen
implizit konvertiert werden kann.
Der am häufigsten in einem Satz von Typen eingeschlossenen Typ ist ein Typ, der von allen anderen Typen
im Satz eingeschlossen wird. Wenn kein einzelner Typ von allen anderen Typen eingeschlossen wird, hat der
Satz nicht den meisten Typ. In intuitiver Hinsicht ist der Typ, der am häufigsten in der Menge enthalten ist, der
"kleinste" Typ im Satz – ein Typ, der implizit in jeden der anderen Typen konvertiert werden kann.
Verarbeiten von benutzerdefinierten impliziten Konvertierungen
Eine benutzerdefinierte implizite Konvertierung von Typ S in Typ T wird wie folgt verarbeitet:
Bestimmen Sie die Typen S0 und T0 . Wenn S oder T NULL-Werte zulassen, S0 und T0 sind die
zugrunde liegenden Typen, andernfalls S0 und T0 sind gleich S T bzw..
Suchen Sie den Satz von Typen, D , aus dem benutzerdefinierte Konvertierungs Operatoren berücksichtigt
werden. Dieser Satz besteht aus S0 (wenn S0 eine Klasse oder Struktur ist), den Basisklassen von S0
(wenn S0 eine Klasse ist) und T0 (wenn eine Klasse T0 oder Struktur ist).
Suchen Sie nach dem Satz der anwendbaren benutzerdefinierten und erhöhten Konvertierungs Operatoren
U . Dieser Satz besteht aus den benutzerdefinierten und angehobenen impliziten Konvertierungs
Operatoren, die von den Klassen oder Strukturen in deklariert werden D , die von einem Typ in S einen Typ
konvertieren, der von eingeschlossen wird T . Wenn U leer ist, ist die Konvertierung nicht definiert, und es
tritt ein Kompilierzeitfehler auf.
Suchen Sie den spezifischsten Quelltyp SX der Operatoren in U :
Wenn einer der Operatoren in U konvertiert S , dann SX ist S .
Andernfalls SX ist der am häufigsten in den kombinierten Satz von Quell Typen der Operatoren in
eingeschlossenen Typ U . Wenn nicht genau ein Typ gefunden werden kann, der in der zwischen
Version enthalten ist, ist die Konvertierung mehrdeutig, und es tritt ein Kompilierzeitfehler auf.
Suchen Sie den spezifischsten Zieltyp TX der Operatoren in U :
Wenn einer der Operatoren in U konvertiert T , dann TX ist T .
Andernfalls TX ist der am weitesten umfassendste Typ in der kombinierten Gruppe von Zieltypen der
Operatoren in U . Wenn genau ein Typ mit der höchsten Verfügbarkeit nicht gefunden werden kann,
ist die Konvertierung mehrdeutig, und es tritt ein Kompilierzeitfehler auf.
Suchen Sie den spezifischsten Konvertierungs Operator:
Wenn U genau einen benutzerdefinierten Konvertierungs Operator enthält, der von SX in
konvertiert TX , dann ist dies der spezifischere Konvertierungs Operator.
Wenn andernfalls U genau einen Operator mit erhöhten Konvertierungen enthält, SX der von in
konvertiert TX , dann ist dies der spezifischere Konvertierungs Operator.
Andernfalls ist die Konvertierung mehrdeutig, und es tritt ein Kompilierzeitfehler auf.
Übernehmen Sie schließlich die Konvertierung:
Wenn S nicht ist SX , wird eine implizite Standard Konvertierung von S in SX ausgeführt.
Der spezifischere Konvertierungs Operator wird aufgerufen, um von in zu konvertieren SX TX .
Wenn TX nicht ist T , wird eine implizite Standard Konvertierung von TX in T ausgeführt.
Verarbeiten benutzerdefinierter expliziter Konvertierungen
Eine benutzerdefinierte explizite Konvertierung von Typ S in Typ T wird wie folgt verarbeitet:
Bestimmen Sie die Typen S0 und T0 . Wenn S oder T NULL-Werte zulassen, S0 und T0 sind die
zugrunde liegenden Typen, andernfalls S0 und T0 sind gleich S T bzw..
Suchen Sie den Satz von Typen, D , aus dem benutzerdefinierte Konvertierungs Operatoren berücksichtigt
werden. Dieser Satz besteht aus S0 (wenn eine Klasse S0 oder Struktur ist), den Basisklassen von S0
(wenn eine S0 Klasse ist), T0 (wenn T0 eine Klasse oder Struktur ist) und den Basisklassen von T0 ( T0
Wenn eine Klasse ist).
Suchen Sie nach dem Satz der anwendbaren benutzerdefinierten und erhöhten Konvertierungs Operatoren
U . Dieser Satz besteht aus den benutzerdefinierten und angehobenen impliziten oder expliziten
Konvertierungs Operatoren, die von den Klassen oder Strukturen in deklariert werden, die von einem Typ
konvertieren, der von einem Typ, der von oder umgeben ist D S , in einen Typ konvertiert, der T von
Wenn U leer ist, ist die Konvertierung nicht definiert, und es tritt ein Kompilierzeitfehler auf.
Suchen Sie den spezifischsten Quelltyp SX der Operatoren in U :
Wenn einer der Operatoren in U konvertiert S , dann SX ist S .
Andernfalls ist, wenn einer der Operatoren in U der Konvertierung von Typen S , die umfasst, SX
der Typ, der in der kombinierten Gruppe von Quell Typen dieser Operatoren am meisten
eingeschlossen ist. Wenn kein Typ gefunden werden kann, ist die Konvertierung mehrdeutig, und es
tritt ein Kompilierzeitfehler auf.
Andernfalls SX ist der am weitesten umfassendste Typ in der kombinierten Gruppe von Quell Typen
der Operatoren in U . Wenn genau ein Typ mit der höchsten Verfügbarkeit nicht gefunden werden
kann, ist die Konvertierung mehrdeutig, und es tritt ein Kompilierzeitfehler auf.
Suchen Sie den spezifischsten Zieltyp TX der Operatoren in U :
Wenn einer der Operatoren in U konvertiert T , dann TX ist T .
Andernfalls ist, wenn einer der Operatoren in in U Typen konvertiert, die von eingeschlossen werden
T , TX der umfassendste Typ in der kombinierten Gruppe von Zieltypen dieser Operatoren. Wenn
genau ein Typ mit der höchsten Verfügbarkeit nicht gefunden werden kann, ist die Konvertierung
mehrdeutig, und es tritt ein Kompilierzeitfehler auf.
Andernfalls TX ist der am häufigsten in den kombinierten Satz von Zieltypen der Operatoren in
eingeschlossen U . Wenn kein Typ gefunden werden kann, ist die Konvertierung mehrdeutig, und es
tritt ein Kompilierzeitfehler auf.
Suchen Sie den spezifischsten Konvertierungs Operator:
Wenn U genau einen benutzerdefinierten Konvertierungs Operator enthält, der von SX in
konvertiert TX , dann ist dies der spezifischere Konvertierungs Operator.
Wenn andernfalls U genau einen Operator mit erhöhten Konvertierungen enthält, SX der von in
konvertiert TX , dann ist dies der spezifischere Konvertierungs Operator.
Andernfalls ist die Konvertierung mehrdeutig, und es tritt ein Kompilierzeitfehler auf.
Übernehmen Sie schließlich die Konvertierung:
Wenn S nicht ist SX , wird eine explizite Standard Konvertierung von S in SX ausgeführt.
Der spezifischere benutzerdefinierte Konvertierungs Operator wird aufgerufen, um von SX in zu
konvertieren TX .
Wenn TX nicht ist T , wird eine explizite Standard Konvertierung von TX in T ausgeführt.

Anonymous function conversions (Konvertierung anonymer


Funktionen)
Eine anonymous_method_expression oder lambda_expression wird als anonyme Funktion (Anonyme Funktions
Ausdrücke) klassifiziert. Der Ausdruck weist keinen Typ auf, kann aber implizit in einen kompatiblen Delegattyp
oder Ausdrucks Strukturtyp konvertiert werden. Eine anonyme Funktion ist insbesondere F mit einem
bereitgestellten Delegattyp kompatibel D :
Wenn F eine anonymous_function_signature enthält, D haben und F dieselbe Anzahl von Parametern.
Wenn keine F anonymous_function_signature enthält, D kann NULL oder mehr Parameter eines
beliebigen Typs aufweisen, solange kein Parameter von D den out Parametermodifizierer aufweist.
Wenn F eine explizit typisierte Parameterliste aufweist, hat jeder Parameter in D denselben Typ und
dieselben Modifizierer wie der entsprechende Parameter in F .
Wenn F eine implizit typisierte Parameterliste aufweist, D hat keinen- ref Parameter oder- out
Parameter.
Wenn der Text von F ein Ausdruck ist und entweder D über einen void Rückgabetyp verfügt oder
asynchron F ist und D den Rückgabetyp aufweist Task , F D ist der Text von F ein gültiger Ausdruck
(WRT Expressions), der als statement_expression (Ausdrucks Anweisungen) zulässig ist, wenn jedem
Parameter von der Typ des entsprechenden Parameters in zugewiesen wird.
Wenn der Text von F ein Anweisungsblock ist und entweder D über einen void Rückgabetyp verfügt oder
asynchron F ist und D den Rückgabetyp aufweist Task , dann F D ist der Text von F ein gültiger
Anweisungsblock (WRT- Blöcke), in dem keine return Anweisung einen Ausdruck angibt, wenn jeder
Parameter von den Typ des entsprechenden Parameters in erhält.
Wenn der Text von F ein Ausdruck ist und entweder F nicht Async ist und D einen nicht leeren
Rückgabetyp aufweist, oder asynchron ist T F und D über einen Rückgabetyp verfügt Task<T> , ist F D
der Text von F ein gültiger Ausdruck (WRT Expressions), der implizit in konvertierbar ist T .
Wenn der Text von F ein Anweisungsblock ist, und ist entweder F nicht Async und D weist einen nicht
leeren Rückgabetyp T auf, oder ist asynchron F und D hat einen Rückgabetyp Task<T> . Wenn jedem
Parameter von F der Typ des entsprechenden Parameters in zugewiesen wird D , ist der Text von F ein
gültiger Anweisungsblock (WRT- Blöcke) mit einem nicht erreichbaren Endpunkt, bei dem jede return
Anweisung einen Ausdruck angibt, der implizit in konvertiert werden T kann
Aus Gründen der Kürze verwendet dieser Abschnitt die Kurzform für die Aufgaben Typen Task und Task<T>
(Async Functions).
Ein Lambda-Ausdruck F ist mit einem Ausdrucks bauentyp kompatibel, Expression<D> Wenn F mit dem
Delegattyp kompatibel ist D . Beachten Sie, dass dies nicht für anonyme Methoden gilt, sondern nur für
Lambda-Ausdrücke.
Bestimmte Lambda-Ausdrücke können nicht in Ausdrucks Baumstruktur Typen konvertiert werden: Obwohl die
Konvertierung vorhanden ist, schlägt Sie zur Kompilierzeit fehl. Dies ist der Fall, wenn der Lambda-Ausdruck:
Weist einen Block Text auf.
Enthält einfache oder Verbund Zuweisungs Operatoren.
Enthält einen dynamisch gebundenen Ausdruck.
Ist Async
In den folgenden Beispielen wird ein generischer Delegattyp verwendet, Func<A,R> der eine Funktion darstellt,
die ein Argument vom Typ annimmt A und einen Wert des Typs zurückgibt R :

delegate R Func<A,R>(A arg);

In den Zuweisungen

Func<int,int> f1 = x => x + 1; // Ok

Func<int,double> f2 = x => x + 1; // Ok

Func<double,int> f3 = x => x + 1; // Error

Func<int, Task<int>> f4 = async x => x + 1; // Ok


der Parameter und die Rückgabe Typen der einzelnen anonymen Funktionen werden vom Typ der Variablen
bestimmt, der die anonyme Funktion zugewiesen wird.
Bei der ersten Zuweisung wird die anonyme Funktion erfolgreich in den Delegattyp konvertiert Func<int,int> ,
da, wenn der x Typ ist int , x+1 ein gültiger Ausdruck ist, der implizit in den Typ int konvertiert werden
kann.
Entsprechend konvertiert die zweite Zuweisung die anonyme Funktion erfolgreich in den Delegattyp,
Func<int,double> da das Ergebnis von x+1 (vom Typ int ) implizit in den Typ double konvertiert werden
kann.
Die dritte Zuweisung ist jedoch ein Kompilierzeitfehler, da x double das Ergebnis von x+1 (vom Typ double )
nicht implizit in den Typ int konvertiert werden kann, wenn den Typ hat.
Die vierte Zuweisung konvertiert die anonyme Async-Funktion erfolgreich in den Delegattyp,
Func<int, Task<int>> da das Ergebnis von x+1 (vom Typ int ) implizit in den Ergebnistyp int des Aufgaben
Typs Task<int> konvertiert werden kann.
Anonyme Funktionen können die Überladungs Auflösung beeinflussen und an einem Typrückschluss
teilnehmen. Weitere Informationen finden Sie unter Funktionsmember .
Auswertung anonymer Funktions Konvertierungen in Delegattypen
Die Konvertierung einer anonymen Funktion in einen Delegattyp erzeugt eine Delegatinstanz, die auf die
anonyme Funktion und den (möglicherweise leeren) Satz erfasster äußerer Variablen verweist, die zum
Zeitpunkt der Auswertung aktiv sind. Wenn der Delegat aufgerufen wird, wird der Text der anonymen Funktion
ausgeführt. Der Code im Text wird mit dem Satz erfasster äußerer Variablen ausgeführt, auf die der Delegat
verweist.
Die Aufruf Liste eines Delegaten, der aus einer anonymen Funktion erstellt wurde, enthält einen einzelnen
Eintrag. Das genaue Zielobjekt und die Ziel Methode des Delegaten sind nicht angegeben. Insbesondere ist nicht
angegeben, ob das Zielobjekt des Delegaten null , der this Wert des einschließenden Funktionsmembers
oder ein anderes Objekt ist.
Konvertierungen von semantisch identischen anonymen Funktionen mit dem gleichen (möglicherweise leeren)
Satz erfasster externer Variablen Instanzen in dieselben Delegattypen sind zulässig ( jedoch nicht erforderlich),
um dieselbe Delegatinstanz zurückzugeben. Der Begriff semantisch identisch wird hier verwendet, um zu
bedeuten, dass die Ausführung der anonymen Funktionen in allen Fällen dieselben Effekte mit denselben
Argumenten erzeugt. Diese Regel ermöglicht es, Code wie den folgenden zu optimieren.

delegate double Function(double x);

class Test
{
static double[] Apply(double[] a, Function f) {
double[] result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}

static void F(double[] a, double[] b) {


a = Apply(a, (double x) => Math.Sin(x));
b = Apply(b, (double y) => Math.Sin(y));
...
}
}

Da die beiden anonymen Funktions Delegaten denselben (leeren) Satz erfasster äußerer Variablen aufweisen
und die anonymen Funktionen semantisch identisch sind, ist es dem Compiler gestattet, dass die Delegaten auf
dieselbe Ziel Methode verweisen. Tatsächlich ist es dem Compiler gestattet, dieselbe Delegatinstanz aus beiden
anonymen Funktions Ausdrücken zurückzugeben.
Auswertung anonymer Funktions Konvertierungen in Ausdrucks Baumstruktur Typen
Die Konvertierung einer anonymen Funktion in einen Ausdrucks bauentyp erzeugt eine Ausdrucks
Baumstruktur (Ausdrucks Baumstruktur Typen). Genauer bedeutet, dass die Auswertung der anonymen
Funktions Konvertierung zur Erstellung einer Objektstruktur führt, die die Struktur der anonymen Funktion
darstellt. Die genaue Struktur der Ausdrucks Baumstruktur sowie der genaue Prozess für die Erstellung werden
implementiert.
Beispiel für die Implementierung
In diesem Abschnitt wird eine mögliche Implementierung anonymer Funktions Konvertierungen in Bezug auf
andere c#-Konstrukte beschrieben. Die hier beschriebene Implementierung basiert auf denselben Prinzipien, die
vom Microsoft c#-Compiler verwendet werden. Dies bedeutet jedoch, dass es sich nicht um eine
vorgeschriebene Implementierung handelt, und auch nicht die einzige Möglichkeit. Die Konvertierung in
Ausdrucks Baumstrukturen wird nur kurz erwähnt, da die genaue Semantik außerhalb des Gültigkeits Bereichs
dieser Spezifikation liegt.
Der Rest dieses Abschnitts enthält mehrere Beispiele für Code, der anonyme Funktionen mit unterschiedlichen
Merkmalen enthält. Für jedes Beispiel wird eine entsprechende Übersetzung in Code bereitgestellt, der nur
andere c#-Konstrukte verwendet. In den Beispielen wird davon ausgegangen, dass der Bezeichner D den
folgenden Delegattyp darstellt:

public delegate void D();

Die einfachste Form einer anonymen Funktion ist eine, die keine äußeren Variablen aufzeichnet:

class Test
{
static void F() {
D d = () => { Console.WriteLine("test"); };
}
}

Dies kann in eine Delegatinstanziierung übersetzt werden, die auf eine vom Compiler generierte statische
Methode verweist, in der der Code der anonymen Funktion platziert wird:

class Test
{
static void F() {
D d = new D(__Method1);
}

static void __Method1() {


Console.WriteLine("test");
}
}

Im folgenden Beispiel verweist die anonyme Funktion auf Instanzmember von this :
class Test
{
int x;

void F() {
D d = () => { Console.WriteLine(x); };
}
}

Dies kann in eine vom Compiler generierte Instanzmethode übersetzt werden, die den Code der anonymen
Funktion enthält:

class Test
{
int x;

void F() {
D d = new D(__Method1);
}

void __Method1() {
Console.WriteLine(x);
}
}

In diesem Beispiel erfasst die anonyme Funktion eine lokale Variable:

class Test
{
void F() {
int y = 123;
D d = () => { Console.WriteLine(y); };
}
}

Die Lebensdauer der lokalen Variablen muss nun mindestens zur Lebensdauer des anonymen Funktions
Delegaten verlängert werden. Dies kann erreicht werden, indem Sie die lokale Variable in ein Feld einer vom
Compiler generierten Klasse einbinden. Die Instanziierung der lokalen Variablen (Instanziierung lokaler
Variablen) entspricht dann dem Erstellen einer Instanz der vom Compiler generierten Klasse, und der Zugriff auf
die lokale Variable entspricht dem Zugriff auf ein Feld in der Instanz der vom Compiler generierten Klasse.
Außerdem wird die anonyme Funktion zu einer Instanzmethode der vom Compiler generierten Klasse:

class Test
{
void F() {
__Locals1 __locals1 = new __Locals1();
__locals1.y = 123;
D d = new D(__locals1.__Method1);
}

class __Locals1
{
public int y;

public void __Method1() {


Console.WriteLine(y);
}
}
}
Zum Schluss erfasst die folgende anonyme Funktion this und zwei lokale Variablen mit unterschiedlichen
Lebens dauern:

class Test
{
int x;

void F() {
int y = 123;
for (int i = 0; i < 10; i++) {
int z = i * 2;
D d = () => { Console.WriteLine(x + y + z); };
}
}
}

Hier wird eine vom Compiler generierte Klasse für jeden Anweisungsblock erstellt, in dem lokale Variablen
aufgezeichnet werden, sodass die lokalen Variablen in den verschiedenen Blöcken eine unabhängige
Lebensdauer aufweisen können. Eine Instanz von __Locals2 , die vom Compiler generierte-Klasse für den
inneren Anweisungsblock, enthält die lokale z -Variable und ein-Feld, das auf eine Instanz von verweist
__Locals1 . Eine Instanz von __Locals1 , die vom Compiler generierte-Klasse für den äußeren
Anweisungsblock, enthält die lokale Variable y und ein Feld, das this auf den einschließenden
Funktionsmember verweist. Mit diesen Datenstrukturen ist es möglich, alle aufgezeichneten äußeren Variablen
über eine Instanz von zu erreichen __Local2 , und der Code der anonymen Funktion kann daher als
Instanzmethode dieser Klasse implementiert werden.

class Test
{
void F() {
__Locals1 __locals1 = new __Locals1();
__locals1.__this = this;
__locals1.y = 123;
for (int i = 0; i < 10; i++) {
__Locals2 __locals2 = new __Locals2();
__locals2.__locals1 = __locals1;
__locals2.z = i * 2;
D d = new D(__locals2.__Method1);
}
}

class __Locals1
{
public Test __this;
public int y;
}

class __Locals2
{
public __Locals1 __locals1;
public int z;

public void __Method1() {


Console.WriteLine(__locals1.__this.x + __locals1.y + z);
}
}
}

Dieselbe Technik, die hier für die Erfassung von lokalen Variablen angewendet wird, kann auch beim Umrechnen
anonymer Funktionen in Ausdrucks Baumstrukturen verwendet werden: Verweise auf die vom Compiler
generierten Objekte können in der Ausdrucks Baumstruktur gespeichert werden, und der Zugriff auf die lokalen
Variablen kann als Feld Zugriffstyp für diese Objekte dargestellt werden. Der Vorteil dieses Ansatzes besteht
darin, dass die "angehobenen" lokalen Variablen für Delegaten und Ausdrucks Baumstrukturen freigegeben
werden können.

Method group conversions (Konvertierung von Methodengruppen)


Eine implizite Konvertierung (implizite Konvertierungen) ist in einer Methoden Gruppe (Ausdrucks
Klassifizierungen) zu einem kompatiblen Delegattyp vorhanden. Bei einem Delegattyp D und einem Ausdruck
E , der als Methoden Gruppe klassifiziert wird, ist eine implizite Konvertierung von in vorhanden, E Wenn
mindestens D E eine Methode enthält, die in ihrer normalen Form (anwendbares Funktionsmember) auf eine
Argumentliste anwendbar ist, die durch die Verwendung der Parametertypen und Modifizierer von erstellt wird
D , wie im folgenden beschrieben.

Die Kompilierzeit Anwendung einer Konvertierung von einer Methoden Gruppe E in einen Delegattyp D wird
im folgenden beschrieben. Beachten Sie, dass das vorhanden sein einer impliziten Konvertierung von E in D
nicht sicherstellt, dass die Kompilierzeit Anwendung der Konvertierung ohne Fehler erfolgreich ausgeführt wird.
Es wird eine einzelne Methode M ausgewählt, die einem Methodenaufruf (Methodenaufrufe) des Formulars
entspricht E(A) , mit den folgenden Änderungen:
Die Argumentliste A ist eine Liste von Ausdrücken, die jeweils als Variable und mit dem Typ und
Modifizierer ( ref oder out ) des entsprechenden Parameters in der formal_parameter_list von
klassifiziert sind D .
Bei den Kandidaten Methoden handelt es sich nur um die Methoden, die in ihrer normalen Form
(anwendbares Funktionsmember) anwendbar sind, nicht die, die nur in der erweiterten Form
anwendbar sind.
Wenn der Algorithmus von Methoden aufrufen einen Fehler erzeugt, tritt ein Kompilierzeitfehler auf.
Andernfalls erzeugt der Algorithmus eine einzige beste Methode M , die dieselbe Anzahl von Parametern
wie hat D , und die Konvertierung gilt als vorhanden.
Die ausgewählte Methode M muss kompatibel (delegatkompatibilität) mit dem Delegattyp sein D .
andernfalls tritt ein Kompilierzeitfehler auf.
Wenn die ausgewählte Methode M eine Instanzmethode ist, bestimmt der Instanzausdruck, der zugeordnet
ist, E das Zielobjekt des Delegaten.
Wenn die ausgewählte Methode M eine Erweiterungsmethode ist, die mithilfe eines Element Zugriffs auf
einen Instanzausdruck bezeichnet wird, bestimmt dieser Instanzausdruck das Zielobjekt des Delegaten.
Das Ergebnis der Konvertierung ist ein Wert vom Typ D , d. h. ein neu erstellter Delegat, der auf die
ausgewählte Methode und das ausgewählte Zielobjekt verweist.
Beachten Sie, dass dieser Prozess zur Erstellung eines Delegaten für eine Erweiterungsmethode führen kann,
wenn der Algorithmus von Methoden aufrufen keine Instanzmethode findet, sondern den Aufruf von E(A)
als Erweiterungs Methodenaufruf (Erweiterungs Methodenaufrufe) verarbeitet. Ein Delegat, der auf diese
Weise erstellt wurde, erfasst sowohl die Erweiterungsmethode als auch das erste Argument.
Im folgenden Beispiel werden Methoden Gruppen Konvertierungen veranschaulicht:
delegate string D1(object o);

delegate object D2(string s);

delegate object D3();

delegate string D4(object o, params object[] a);

delegate string D5(int i);

class Test
{
static string F(object o) {...}

static void G() {


D1 d1 = F; // Ok
D2 d2 = F; // Ok
D3 d3 = F; // Error -- not applicable
D4 d4 = F; // Error -- not applicable in normal form
D5 d5 = F; // Error -- applicable but not compatible

}
}

Die Zuweisung zum d1 impliziten Konvertieren der Methoden Gruppe F in einen Wert des Typs D1 .
Die Zuweisung von d2 zeigt, wie es möglich ist, einen Delegaten für eine Methode zu erstellen, die weniger
abgeleitete Parametertypen (kontra Variant) und einen stärker abgeleiteten (kovarianten) Rückgabetyp aufweist.
Die Zuweisung von d3 zeigt, wie keine Konvertierung vorhanden ist, wenn die Methode nicht anwendbar ist.
Die Zuweisung von d4 zeigt, wie die Methode in ihrer normalen Form anwendbar sein muss.
Die Zuweisung d5 von zeigt, wie Parameter und Rückgabe Typen des Delegaten und der Methode nur für
Verweis Typen unterschiedlich sein dürfen.
Wie bei allen anderen impliziten und expliziten Konvertierungen kann der Cast-Operator verwendet werden, um
eine Konvertierung der Methoden Gruppe explizit auszuführen. Das Beispiel

object obj = new EventHandler(myDialog.OkClick);

Stattdessen können Sie schreiben.

object obj = (EventHandler)myDialog.OkClick;

Methoden Gruppen können die Überladungs Auflösung beeinflussen und am Typrückschluss teilnehmen.
Weitere Informationen finden Sie unter Funktionsmember .
Die Lauf Zeit Auswertung einer Methoden Gruppen Konvertierung verläuft wie folgt:
Wenn die zur Kompilierzeit ausgewählte Methode eine Instanzmethode ist oder es sich um eine
Erweiterungsmethode handelt, auf die als Instanzmethode zugegriffen wird, wird das Zielobjekt des
Delegaten aus dem Instanzausdruck bestimmt, dem zugeordnet ist E :
Der Instanzausdruck wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden
keine weiteren Schritte ausgeführt.
Wenn der Instanzausdruck eine reference_type ist, wird der durch den Instanzausdruck berechnete
Wert zum Zielobjekt. Wenn die ausgewählte Methode eine Instanzmethode ist und das Zielobjekt ist
null , wird eine ausgelöst, System.NullReferenceException und es werden keine weiteren Schritte
ausgeführt.
Wenn der Instanzausdruck eine value_type ist, wird ein Boxing-Vorgang (Boxing-Konvertierungen)
ausgeführt, um den Wert in ein Objekt zu konvertieren, und dieses Objekt wird zum Zielobjekt.
Andernfalls ist die ausgewählte Methode Teil eines statischen Methoden Aufrufes, und das Zielobjekt des
Delegaten ist null .
Eine neue Instanz des Delegattyps D wird zugewiesen. Wenn nicht genügend Arbeitsspeicher zur Verfügung
steht, um die neue Instanz zuzuordnen, wird eine ausgelöst, System.OutOfMemoryException und es werden
keine weiteren Schritte ausgeführt.
Die neue Delegatinstanz wird mit einem Verweis auf die Methode initialisiert, die zur Kompilierzeit bestimmt
wurde, und ein Verweis auf das oben berechnete Zielobjekt.
Ausdrücke
04.11.2021 • 419 minutes to read

Ein Ausdruck ist eine Folge von Operatoren und Operanden. In diesem Kapitel werden die Syntax, die
Reihenfolge der Auswertung von Operanden und Operatoren sowie die Bedeutung von Ausdrücken definiert.

Expression classifications (Ausdrucksklassifizierungen)


Ein Ausdruck ist eines der folgenden Elemente:
Ein Wert. Jeder Wert verfügt über einen zugeordneten Typ.
Eine Variable. Jede Variable verfügt über einen zugeordneten Typ, nämlich den deklarierten Typ der
Variablen.
Ein Namespace. Ein Ausdruck mit dieser Klassifizierung kann nur auf der linken Seite eines member_access
(Member Access) angezeigt werden. In jedem anderen Kontext verursacht ein Ausdruck, der als Namespace
klassifiziert ist, einen Kompilierzeitfehler.
Ein Typ. Ein Ausdruck mit dieser Klassifizierung kann nur als linke Seite eines member_access (Member
Access) oder als Operand für den as Operator (as-Operator), is Operator (is-Operator) oder typeof
Operator (der typeof-Operator) angezeigt werden. In jedem anderen Kontext verursacht ein Ausdruck, der als
Typ klassifiziert ist, einen Kompilierzeitfehler.
Eine Methoden Gruppe, bei der es sich um eine Reihe von überladenen Methoden handelt, die sich aus einer
Element Suche (Member Suche) ergeben. Eine Methoden Gruppe kann über einen zugeordneten
Instanzausdruck und eine zugeordnete Typargument Liste verfügen. Wenn eine Instanzmethode aufgerufen
wird, wird das Ergebnis der Auswertung des Instanzausdrucks zu der durch dargestellten Instanz this
(dieser Zugriff). Eine Methoden Gruppe ist in einem invocation_expression (Aufruf Ausdrücke), einem
delegate_creation_expression (delegaterstellungs Ausdruck) und als linke Seite eines is-Operators zulässig
und kann implizit in einen kompatiblen Delegattyp (Methoden Gruppen Konvertierungen) konvertiert
werden. In jedem anderen Kontext verursacht ein Ausdruck, der als Methoden Gruppe klassifiziert ist, einen
Kompilierzeitfehler.
Ein NULL-Literale. Ein Ausdruck mit dieser Klassifizierung kann implizit in einen Verweistyp oder einen Werte
zulässt-Typ konvertiert werden.
Eine anonyme Funktion. Ein Ausdruck mit dieser Klassifizierung kann implizit in einen kompatiblen
Delegattyp oder Ausdrucks Strukturtyp konvertiert werden.
Ein Eigenschaften Zugriff. Jeder Eigenschaften Zugriff verfügt über einen zugeordneten Typ, nämlich den Typ
der Eigenschaft. Außerdem kann ein Eigenschaften Zugriff über einen zugeordneten Instanzausdruck
verfügen. Wenn ein Accessor (der- get oder- set Block) eines Instanzeigenschaft Zugriffs aufgerufen wird,
wird das Ergebnis der Auswertung des Instanzausdrucks zu der durch dargestellten Instanz this (dieser
Zugriff).
Ein Ereignis Zugriff. Jedem Ereignis Zugriff ist ein Typ zugeordnet, nämlich der Typ des Ereignisses.
Außerdem kann ein Ereignis Zugriff über einen zugeordneten Instanzausdruck verfügen. Ein Ereignis Zugriff
kann als Linker Operand des += -= Operators und (Ereignis Zuweisung) angezeigt werden. In jedem
anderen Kontext verursacht ein Ausdruck, der als Ereignis Zugriff klassifiziert ist, einen Kompilierzeitfehler.
Ein Indexer-Zugriff. Jedem Indexer-Zugriff ist ein Typ zugeordnet, nämlich der Elementtyp des Indexers.
Außerdem verfügt ein Indexer-Zugriff über einen zugeordneten Instanzausdruck und eine zugeordnete
Argumentliste. Wenn ein Accessor (der- get oder- set Block) eines Indexerzugriffs aufgerufen wird, wird
das Ergebnis der Auswertung des Instanzausdrucks zu der durch dargestellten Instanz this (dieser Zugriff),
und das Ergebnis der Auswertung der Argumentliste wird zur Parameterliste des aufzurufenden
aufzurufenden.
Nichts. Dies tritt auf, wenn der Ausdruck ein Aufruf einer Methode mit dem Rückgabetyp ist void . Ein
Ausdruck, der als Nothing klassifiziert ist, ist nur im Kontext einer statement_expression (Ausdrucks
Anweisungen) gültig.
Das Endergebnis eines Ausdrucks ist nie ein Namespace, ein Typ, eine Methoden Gruppe oder ein Ereignis
Zugriff. Wie oben bereits erwähnt, sind diese Kategorien von Ausdrücken zwischenkonstrukte, die nur in
bestimmten Kontexten zulässig sind.
Ein Eigenschaften Zugriff oder Indexer-Zugriff wird immer als Wert neu klassifiziert, indem ein Aufruf des Get-
Accessor oder des set-Accessors ausgeführt wird. Der jeweilige Accessor wird durch den Kontext der Eigenschaft
oder des Indexerzugriffs bestimmt: Wenn der Zugriff das Ziel einer Zuweisung ist, wird der Set-Accessor
aufgerufen, um einen neuen Wert zuzuweisen (einfache Zuweisung). Andernfalls wird der Get-Accessor
aufgerufen, um den aktuellen Wert zu erhalten (Werte von Ausdrücken).
Werte von Ausdrücken
Die meisten Konstrukte, die einen Ausdruck einschließen, erfordern letztendlich, dass der Ausdruck einen Wer t
angibt. In solchen Fällen tritt ein Kompilierzeitfehler auf, wenn der tatsächliche Ausdruck einen Namespace,
einen Typ, eine Methoden Gruppe oder nichts angibt. Wenn der Ausdruck jedoch einen Eigenschaften Zugriff,
einen Indexer-Zugriff oder eine Variable angibt, wird der Wert der Eigenschaft, des Indexers oder der Variablen
implizit ersetzt:
Der Wert einer Variablen ist einfach der Wert, der zurzeit an dem von der Variablen identifizierten
Speicherort gespeichert ist. Eine Variable muss als definitiv zugewiesen (definitive Zuweisung) betrachtet
werden, bevor ihr Wert abgerufen werden kann. andernfalls tritt ein Kompilierzeitfehler auf.
Der Wert eines Eigenschafts Zugriffs Ausdrucks wird abgerufen, indem der Get-Accessor der Eigenschaft
aufgerufen wird. Wenn die Eigenschaft keinen Get-Accessor aufweist, tritt ein Kompilierzeitfehler auf.
Andernfalls wird ein Funktionsmember-Aufruf (Kompilierzeit Überprüfung der dynamischen Überladungs
Auflösung) ausgeführt, und das Ergebnis des aufzurufenden Ausdrucks wird zum Wert des Eigenschafts
Zugriffs Ausdrucks.
Der Wert eines indexerzugriffsausdrucks wird abgerufen, indem der Get-Accessor des Indexers aufgerufen
wird. Wenn der Indexer keinen Get-Accessor aufweist, tritt ein Kompilierzeitfehler auf. Andernfalls wird ein
Funktionsmember-Aufruf (Kompilierzeit Überprüfung der dynamischen Überladungs Auflösung) mit der
Argumentliste ausgeführt, die dem indexerzugriffsausdruck zugeordnet ist, und das Ergebnis des
aufzurufenden Ausdrucks wird zum Wert des Zugriffs Ausdrucks des Indexers.

Static and Dynamic Binding (Statische und dynamische Bindung)


Der Prozess der Bestimmung der Bedeutung eines Vorgangs basierend auf dem Typ oder Wert von
konstituierenden Ausdrücken (Argumente, Operanden, Empfänger) wird häufig als Bindung bezeichnet. Zum
Beispiel wird die Bedeutung eines Methoden Aufrufes basierend auf dem Typ des Empfängers und der
Argumente bestimmt. Die Bedeutung eines Operators wird basierend auf dem Typ seiner Operanden bestimmt.
In c# wird die Bedeutung eines Vorgangs in der Regel zur Kompilierzeit bestimmt, basierend auf dem Kompilier
Zeittyp der enthaltenen Ausdrücke. Wenn ein Ausdruck einen Fehler enthält, wird der Fehler auch vom Compiler
erkannt und gemeldet. Diese Vorgehensweise wird als statische Bindung bezeichnet.
Wenn ein Ausdruck jedoch ein dynamischer Ausdruck ist (d. h. den-Typ aufweist dynamic ), gibt dies an, dass
jede Bindung, an der Sie teilnimmt, auf dem Lauf Zeittyp (d. h. dem tatsächlichen Typ des Objekts, das Sie zur
Laufzeit bezeichnet) und nicht auf dem Typ, der zur Kompilierzeit vorhanden ist, basieren soll. Die Bindung eines
solchen Vorgangs wird daher bis zu der Zeit verzögert, in der der Vorgang während der Ausführung des
Programms ausgeführt werden soll. Dies wird als dynamische Bindung bezeichnet.
Wenn ein Vorgang dynamisch gebunden ist, wird vom Compiler nur eine kleine oder keine Überprüfung
durchgeführt. Wenn die Lauf Zeitbindung fehlschlägt, werden Fehler zur Laufzeit als Ausnahmen gemeldet.
Die folgenden Vorgänge in c# unterliegen der Bindung:
Mitgliederzugriff: e.M
Methodenaufruf: e.M(e1, ..., eN)
Delegataufruf: e(e1, ..., eN)
Element Zugriff: e[e1, ..., eN]
Objekt Erstellung: new C(e1, ..., eN)
Überladene unäre Operatoren: + , - , ! , ~ , ++ , -- , true , false
Überladene binäre Operatoren: + , - , * , / , % , & , && , | , || , ?? , ^ , << , >> , == , != , > ,
< , >= , <=
Zuweisungs Operatoren: = , += , -= , *= , /= , %= , &= , |= , ^= , <<= , >>=
Implizite und explizite Konvertierungen
Wenn keine dynamischen Ausdrücke beteiligt sind, wird in c# standardmäßig die statische Bindung verwendet.
Dies bedeutet, dass die Kompilierzeit Typen von konstituierenden Ausdrücken im Auswahl Vorgang verwendet
werden. Wenn jedoch einer der in den oben aufgeführten Vorgängen aufgeführten Ausdrücke ein dynamischer
Ausdruck ist, wird der Vorgang stattdessen dynamisch gebunden.
Bindungs Zeit
Die statische Bindung erfolgt zur Kompilierzeit, während die dynamische Bindung zur Laufzeit stattfindet. In den
folgenden Abschnitten bezieht sich der Begriff " Bindungs Zeit " entweder auf die Kompilierzeit oder die
Laufzeit, je nachdem, wann die Bindung stattfindet.
Das folgende Beispiel veranschaulicht die Begriffe der statischen und dynamischen Bindung und der Bindungs
Zeit:

object o = 5;
dynamic d = 5;

Console.WriteLine(5); // static binding to Console.WriteLine(int)


Console.WriteLine(o); // static binding to Console.WriteLine(object)
Console.WriteLine(d); // dynamic binding to Console.WriteLine(int)

Die ersten beiden Aufrufe sind statisch gebunden: die Überladung von Console.WriteLine wird basierend auf
dem Kompilier Zeittyp ihres Arguments ausgewählt. Folglich ist die Bindungs Zeit die Kompilierzeit.
Der dritte Aufruf ist dynamisch gebunden: die Überladung von Console.WriteLine wird basierend auf dem Lauf
Zeittyp des Arguments ausgewählt. Dies liegt daran, dass das-Argument ein dynamischer Ausdruck ist, der
Kompilier Zeittyp ist dynamic . Folglich ist die Bindungs Zeit für den dritten Aufruf Lauf Zeit.
Dynamische Bindung
Der Zweck der dynamischen Bindung besteht darin, dass c#-Programme mit dynamischen Objekten
interagieren können, d. h. Objekte, die nicht den normalen Regeln des c#-Typsystems entsprechen. Dynamische
Objekte können Objekte aus anderen Programmiersprachen mit unterschiedlichen Typen Systemen sein, oder
es handelt sich um Objekte, die Programm gesteuert eingerichtet werden, um Ihre eigene Bindungs Semantik
für verschiedene Vorgänge zu implementieren.
Der Mechanismus, mit dem ein dynamisches Objekt seine eigene Semantik implementiert, ist die
Implementierung definiert. Eine bestimmte Schnittstelle, die wieder implementiert wird, wird von dynamischen
Objekten implementiert, um der c#-Laufzeit zu signalisieren, dass Sie über eine besondere Semantik verfügen.
Wenn also Vorgänge für ein dynamisches Objekt dynamisch gebunden werden, übernehmen Sie die eigene
Bindungs Semantik anstelle von c#-Daten, die in diesem Dokument angegeben sind.
Der Zweck der dynamischen Bindung besteht darin, die Interoperabilität mit dynamischen Objekten zuzulassen,
aber c# ermöglicht die dynamische Bindung für alle Objekte, unabhängig davon, ob Sie dynamisch sind oder
nicht. Dies ermöglicht eine reibungslosere Integration dynamischer Objekte, da die Ergebnisse von Vorgängen
für diese nicht selbst dynamische Objekte sein können, aber immer noch ein Typ ist, der dem Programmierer
zur Kompilierzeit unbekannt ist. Außerdem kann die dynamische Bindung helfen, fehleranfälligen
reflektionsbasierten Code zu eliminieren, auch wenn keine Objekte an dynamischen Objekten beteiligt sind.
In den folgenden Abschnitten wird für jedes Konstrukt in der Sprache exakt beschrieben, wann die dynamische
Bindung angewendet wird, welche Kompilierzeit Überprüfung (sofern vorhanden) angewendet wird und wie das
Ergebnis und die Ausdrucks Klassifizierung der Kompilierung ist.
Typen von konstituierenden Ausdrücken
Wenn ein Vorgang statisch gebunden ist, wird der Typ eines einzelnen Ausdrucks (z. b. ein Empfänger, ein
Argument, ein Index oder ein Operand) immer als der Kompilier Zeittyp dieses Ausdrucks betrachtet.
Wenn ein Vorgang dynamisch gebunden wird, wird der Typ eines einzelnen Ausdrucks basierend auf dem
Kompilier Zeittyp des konstituierenden Ausdrucks auf unterschiedliche Weise bestimmt:
Ein konstituierender Ausdruck des Kompilierzeit Typs dynamic wird als der Typ des tatsächlichen Werts
betrachtet, zu dem der Ausdruck zur Laufzeit ausgewertet wird.
Ein konstituierender Ausdruck, dessen Kompilier Zeittyp ein Typparameter ist, wird als Typ betrachtet, an den
der Typparameter zur Laufzeit gebunden ist.
Andernfalls wird der entsprechende Kompilier Zeittyp für den enthaltenen Ausdruck berücksichtigt.

Operatoren
Ausdrücke werden aus Operanden _-und- _Operatoren*_ erstellt. Die Operatoren eines Ausdrucks geben an,
welche Operationen auf die Operanden angewendet werden. Beispiele für Operatoren sind + , - , _ , / und
new . Beispiele für Operanden sind Literale, Felder, lokale Variablen und Ausdrücke.

Es gibt drei Arten von Operatoren:


Unäre Operatoren. Die unären Operatoren nehmen einen Operanden an und verwenden entweder eine
Präfix Notation (z. b. --x ) oder eine Postfix Notation (z x++ . b.).
Binäre Operatoren. Die binären Operatoren nehmen zwei Operanden an und verwenden die Infix-Notation (z
x + y . b.).
Ternärer Operator. Nur ein ternärer Operator, ?: , ist vorhanden; er nimmt drei Operanden an und
verwendet die Infix-Notation ( c ? x : y ).

Die Reihenfolge der Auswertung von Operatoren in einem Ausdruck wird durch die * Rang Folge _ und _
Assoziativität* der Operatoren (Operator Rangfolge und Assoziativität) bestimmt.
Operanden in einem Ausdruck werden von links nach rechts ausgewertet. In wird die-Methode z. b. mit
F(i) + G(i++) * H(i) F dem alten Wert von aufgerufen. i anschließend G wird die-Methode mit dem alten
Wert von aufgerufen, i und schließlich H wird die-Methode mit dem neuen Wert von aufgerufen i . Dies ist
getrennt von und nicht mit der Operator Rangfolge.
Bestimmte Operatoren können überladen werden. Die Operator Überladung ermöglicht das Angeben von
benutzerdefinierten Operator Implementierungen für Vorgänge, bei denen einer oder beide der Operanden eine
benutzerdefinierte Klasse oder ein Strukturtyp sind (Operator Überladung).
Operatorrangfolge und Assoziativität
Wenn ein Ausdruck mehrere Operatoren enthält, steuert die *Rangfolge _ der Operatoren die Reihenfolge, in
der die einzelnen Operatoren ausgewertet werden. Beispielsweise wird der Ausdruck x + y _ z als
x + (y * z) ausgewertet, da der * -Operator eine höhere Rangfolge aufweist als der binäre + -Operator. Die
Rangfolge eines Operators wird durch die Definition der zugehörigen Grammatikproduktion festgelegt. Ein
additive_expression besteht z. b. aus einer Sequenz von multiplicative_expression s, die durch + or- -
Operatoren getrennt sind, sodass der + -Operator und der- - Operator eine niedrigere Rangfolge als die *
/ % Operatoren, und

In der folgenden Tabelle werden alle Operatoren in der Rangfolge von der höchsten zur niedrigsten aufgeführt:

B EREIC H K AT EGO RIE O P ERATO REN

Primäre Ausdrücke Primär x.y f(x) a[x] x++ x-- new


typeof default checked
unchecked delegate

Unäre Operatoren Unär + - ! ~ ++x --x (T)x

Arithmetic operators (Arithmetische Multiplikativ * / %


Operatoren)

Arithmetic operators (Arithmetische Additiv + -


Operatoren)

Shift operators (Schiebeoperatoren) Shift << >>

Relationale und Typtest Operatoren Relational und Typtest < > <= >= is as

Relationale und Typtest Operatoren Gleichheit == !=

Logische Operatoren Logisches AND &

Logische Operatoren Logisches XOR ^

Logische Operatoren Logisches OR |

Conditional logical operators (Bedingte Bedingtes AND &&


logische Operatoren)

Conditional logical operators (Bedingte Bedingtes OR ||


logische Operatoren)

The null coalescing operator (Der NULL-Sammeloperator ??


NULL-Sammeloperator)

Bedingter Operator Bedingt ?:

Zuweisungs Operatoren, Anonyme Zuweisungs- und Lambda-Ausdrücke = *= /= %= += -= <<= >>=


Funktions Ausdrücke &= ^= |= =>

Tritt ein Operand zwischen zwei Operatoren mit gleicher Rangfolge auf, steuert die Assoziativität der Operatoren
die Reihenfolge, in der die Vorgänge ausgeführt werden:
Mit Ausnahme der Zuweisungs Operatoren und des NULL-Sammel Operators sind alle binären Operatoren
Links bündig. Dies bedeutet, dass Vorgänge von links nach rechts ausgeführt werden. x + y + z wird
beispielsweise als (x + y) + z ausgewertet.
Die Zuweisungs Operatoren, der NULL-Sammel Operator und der bedingte Operator ( ?: ) sind Rechts
assoziativ , was bedeutet, dass Vorgänge von rechts nach links ausgeführt werden. x = y = z wird
beispielsweise als x = (y = z) ausgewertet.

Rangfolge und Assoziativität können mit Klammern gesteuert werden. In x + y * z wird beispielsweise zuerst
y mit z multipliziert und dann das Ergebnis zu x addiert, aber in (x + y) * z werden zunächst x und y
addiert, und dann wird das Ergebnis mit z multipliziert.
Überladen von Operatoren
Alle unären und binären Operatoren verfügen über vordefinierte Implementierungen, die automatisch in jedem
Ausdruck verfügbar sind. Zusätzlich zu den vordefinierten Implementierungen können benutzerdefinierte
Implementierungen durch Einschließen von operator Deklarationen in Klassen und Strukturen (Operatoren)
eingeführt werden. Implementierungen von benutzerdefinierten Operatoren haben immer Vorrang vor
vordefinierten Operator Implementierungen: nur wenn keine anwendbaren benutzerdefinierten Operator
Implementierungen vorhanden sind, werden die vordefinierten Operator Implementierungen in Erwägung
gezogen, wie unter unäre Operator Überladungs Auflösung und binäre Operator Überladungs
Auflösungbeschrieben.
Die über ladbaren unären Operatoren sind:

+ - ! ~ ++ -- true false

Obwohl true und false nicht explizit in Ausdrücken verwendet werden (und daher nicht in der Rang folgen
Tabelle in der Operator Rangfolge und Assoziativitätenthalten sind), werden Sie als Operatoren angesehen, da
Sie in mehreren Ausdrucks Kontexten aufgerufen werden: boolesche Ausdrücke (boolesche Ausdrücke) und
Ausdrücke mit bedingtem (bedingtem Operator) und bedingten logischen Operatoren (bedingte logische
Operatoren).
Die über ladbaren binären Operatoren sind:

+ - * / % & | ^ << >> == != > < >= <=

Nur die oben aufgeführten Operatoren können überladen werden. Insbesondere ist es nicht möglich, Element
Zugriffe, Methodenaufrufe oder die = && || Operatoren,,,, ?? ?: , => , checked , unchecked ,,, new
typeof default , as und is zu überladen.

Wenn ein binärer Operator überladen ist, wird der zugehörige Zuweisungsoperator, sofern er vorhanden ist,
auch implizit überladen. Beispielsweise ist eine Überladung des-Operators * auch eine Überladung des-
Operators *= . Dies wird in der Verbund Zuweisungweiter unten beschrieben. Beachten Sie, dass der
Zuweisungs Operator selbst ( = ) nicht überladen werden kann. Eine Zuweisung führt immer eine einfache
bitweise Kopie eines Werts in eine Variable aus.
Umwandlungs Vorgänge, wie z (T)x . b., werden durch die Bereitstellung von benutzerdefinierten
Konvertierungen (benutzerdefinierte Konvertierungen) überladen.
Der Element Zugriff, z a[x] . b., wird nicht als über ladbarer Operator angesehen. Stattdessen wird die
benutzerdefinierte Indizierung durch Indexer (Indexer) unterstützt.
In Ausdrücken wird mithilfe der Operator Notation auf Operatoren verwiesen, und in Deklarationen wird auf
Operatoren mithilfe der funktionalen Notation verwiesen. In der folgenden Tabelle wird die Beziehung zwischen
Operator-und Funktions Notizen für unäre und binäre Operatoren veranschaulicht. Im ersten Eintrag bezeichnet
op alle über ladbaren unären Präfix Operatoren. Im zweiten Eintrag bezeichnet op die unären postfix ++ -und-
-- Operatoren. Im dritten Eintrag bezeichnet op jeden über ladbaren binären Operator.
O P ERATO R N OTAT IO N F UN K T IO N A L E N OTAT IO N

op x operator op(x)

x op operator op(x)

x op y operator op(x,y)

Benutzerdefinierte Operator Deklarationen erfordern immer, dass mindestens einer der Parameter vom Klassen-
oder Strukturtyp ist, der die Operator Deklaration enthält. Daher ist es nicht möglich, dass ein
benutzerdefinierter Operator dieselbe Signatur wie ein vordefinierter Operator hat.
Benutzerdefinierte Operator Deklarationen können die Syntax, Rangfolge oder Assoziativität eines Operators
nicht ändern. Beispielsweise ist der- / Operator immer ein binärer Operator, verfügt immer über die in
Operator Rangfolge und Assoziativitätangegebene Rang folgen Ebene und ist immer links assoziativ.
Obwohl es möglich ist, dass ein benutzerdefinierter Operator jede beliebige Berechnung durchführt, wird
dringend davon abgeraten, Implementierungen, die andere Ergebnisse als diejenigen ergeben, die intuitiv
erwartet werden. Beispielsweise sollte eine Implementierung von operator == die beiden Operanden auf
Gleichheit vergleichen und ein entsprechendes Ergebnis zurückgeben bool .
Die Beschreibungen der einzelnen Operatoren in Primary Expressions durch bedingte logische Operatoren
geben die vordefinierten Implementierungen der Operatoren sowie alle zusätzlichen Regeln an, die für die
einzelnen Operatoren gelten. Die Beschreibungen verwenden die Begriffe unäre Operator Überladungs
Auflösung _, _binäre Operator Überladungs Auflösung*_ und _ *numerische herauf Stufung * *, Definitionen
von, die in den folgenden Abschnitten zu finden sind.
Überladungs Auflösung für unären Operator
Ein Vorgang des Formulars op x oder x op , wobei op ein über ladbarer unärer Operator ist und x ein
Ausdruck vom Typ ist X , wird wie folgt verarbeitet:
Der Satz von benutzerdefinierten Operatoren, die von X für den Vorgang bereitgestellt werden,
operator op(x) wird mithilfe der Regeln von benutzerdefinierten Operatoren des Kandidatenbestimmt.
Wenn der Satz von benutzerdefinierten Operatoren des Kandidaten nicht leer ist, wird dies zur Gruppe der
Kandidaten Operatoren für den Vorgang. Andernfalls werden die vordefinierten unären operator op
Implementierungen, einschließlich ihrer angehobenen Formulare, zur Gruppe der Kandidaten Operatoren für
den Vorgang. Die vordefinierten Implementierungen eines angegebenen Operators werden in der
Beschreibung des Operators (primär Ausdrücke und unäre Operatoren) angegeben.
Die Regeln für die Überladungs Auflösung der Überladungs Auflösung werden auf den Satz von Kandidaten
Operatoren angewendet, um den besten Operator in Bezug auf die Argumentliste auszuwählen (x) , und
dieser Operator wird zum Ergebnis der Überladungs Auflösung. Wenn bei der Überladungs Auflösung nicht
der einzige beste Operator ausgewählt werden kann, tritt ein Bindungs Zeitfehler auf.
Binäre Operator Überladungs Auflösung
Ein Vorgang in der Form x op y , wobei op ein über ladbarer binärer Operator ist, x ein Ausdruck vom Typ
X und y ein Ausdruck vom Typ ist Y , wird wie folgt verarbeitet:

Der Satz von benutzerdefinierten Operatoren, die von X und für den-Vorgang bereitgestellt werden, Y
operator op(x,y) wird bestimmt. Der Satz besteht aus der Vereinigung der Kandidaten Operatoren, die von
bereitgestellt werden X , und der von bereitgestellten Kandidaten Operatoren Y , die jeweils mithilfe der
Regeln von benutzerdefinierten Operatoren des Kandidatenbestimmt werden. Wenn X und Y denselben
Typ haben oder wenn X und Y von einem gemeinsamen Basistyp abgeleitet sind, treten freigegebene
Kandidaten Operatoren nur einmal in der kombinierten Menge auf.
Wenn der Satz von benutzerdefinierten Operatoren des Kandidaten nicht leer ist, wird dies zur Gruppe der
Kandidaten Operatoren für den Vorgang. Andernfalls werden die vordefinierten binären operator op
Implementierungen, einschließlich ihrer angehobenen Formulare, zur Gruppe der Kandidaten Operatoren für
den Vorgang. Die vordefinierten Implementierungen eines angegebenen Operators werden in der
Beschreibung des Operators (arithmetische Operatoren durch bedingte logische Operatoren) angegeben. Bei
vordefinierten Aufzählungs-und delegatoperatoren sind die einzigen Operatoren, die von einem
Aufzählungs-oder Delegattyp definiert werden, der der Bindungstyp von einem der Operanden ist.
Die Regeln für die Überladungs Auflösung der Überladungs Auflösung werden auf den Satz von Kandidaten
Operatoren angewendet, um den besten Operator in Bezug auf die Argumentliste auszuwählen (x,y) , und
dieser Operator wird zum Ergebnis der Überladungs Auflösung. Wenn bei der Überladungs Auflösung nicht
der einzige beste Operator ausgewählt werden kann, tritt ein Bindungs Zeitfehler auf.
Benutzerdefinierte Operatoren für Kandidaten
Bei einem Typ T und einem Vorgang operator op(A) , bei dem op es sich um einen über ladbaren Operator
handelt und A es sich um eine Argumentliste handelt, wird der von for bereitgestellte Satz von
benutzerdefinierten Operatoren T operator op(A) wie folgt bestimmt:
Bestimmen Sie den Typ T0 . Wenn T ein Typ ist, der NULL-Werte zulässt, T0 ist der zugrunde liegende
Typ, andernfalls T0 ist gleich T .
Bei allen operator op Deklarationen in T0 und allen aufgelegten Formen solcher Operatoren, wenn
mindestens ein Operator anwendbar ist (anwendbarer Funktionsmember), der sich auf die Argumentliste
betrifft A , besteht der Satz von Kandidaten Operatoren aus allen anwendbaren Operatoren in T0 .
Andernfalls T0 object ist der Satz von Kandidaten Operatoren leer, wenn ist.
Andernfalls ist der Satz von Kandidaten Operatoren, der von bereitgestellt T0 wird, der Satz von Kandidaten
Operatoren, der von der direkten Basisklasse von bereitgestellt wird T0 , oder die effektive Basisklasse von,
T0 Wenn T0 ein Typparameter ist.

Numerische Heraufstufungen
Die numerische herauf Stufung besteht aus dem automatischen Ausführen bestimmter impliziter
Konvertierungen der Operanden der vordefinierten unären und binären numerischen Operatoren. Die
numerische herauf Stufung ist kein eindeutiger Mechanismus, sondern wirkt sich eher auf die Anwendung der
Überladungs Auflösung auf die vordefinierten Operatoren aus. Die numerische herauf Stufung wirkt sich nicht
auf die Auswertung von benutzerdefinierten Operatoren aus, obwohl benutzerdefinierte Operatoren
implementiert werden können, um ähnliche Effekte zu erzeugen.
Sehen Sie sich als Beispiel für die numerische herauf Stufung die vordefinierten Implementierungen des binären
* Operators an:

int operator *(int x, int y);


uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);

Wenn Regeln zur Überladungs Auflösung (ÜberladungsAuflösung) auf diese Gruppe von Operatoren
angewendet werden, besteht der Effekt darin, den ersten Operator auszuwählen, für den implizite
Konvertierungen aus den Operanden Typen vorhanden sind. Für den b * s -Vorgang, bei dem b ein byte
und s ein ist short , wählt die Überladungs Auflösung beispielsweise operator *(int,int) als besten
Operator aus. Folglich ist der Effekt, dass b und s in konvertiert werden int , und der Ergebnistyp ist int .
Entsprechend i * d wählt die Überladungs Auflösung für den-Vorgang, bei dem i ein int und d ein ist
double , operator *(double,double) als besten Operator aus.
Unäre numerische Aktionen
Unäre numerische herauf Stufung tritt für die Operanden der vordefinierten + - Operatoren, und auf ~ . Die
Unäre numerische herauf Stufung besteht einfach aus der Umstellung von Operanden vom Typ sbyte , byte ,
short , ushort oder char in den-Typ int . Außerdem konvertiert die Unäre numerische herauf Stufung für
den unären - Operator Operanden vom Typ uint in den Typ long .
Binäre numerische Aktionen
Die binäre numerische herauf Stufung erfolgt bei den Operanden der vordefinierten + Operatoren,,,, - * /
% , & , | , ^ ,,,,, == != > < >= und <= . Die binäre numerische herauf Stufung konvertiert beide
Operanden implizit in einen gemeinsamen Typ, der bei nicht relationalen Operatoren auch zum Ergebnistyp des
Vorgangs wird. Die binäre numerische herauf Stufung besteht aus der Anwendung der folgenden Regeln in der
Reihenfolge, in der Sie angezeigt werden:
Wenn einer der beiden Operanden vom Typ ist decimal , wird der andere Operand in den Typ konvertiert
decimal , oder es tritt ein Bindungs Zeitfehler auf, wenn der andere Operand vom Typ float oder ist
double .
Wenn einer der beiden Operanden vom Typ ist double , wird der andere Operand in den Typ konvertiert
double .
Wenn einer der beiden Operanden vom Typ ist float , wird der andere Operand in den Typ konvertiert
float .
Andernfalls, wenn einer der beiden Operanden vom Typ ist ulong , wird der andere Operand in den Typ
konvertiert ulong , oder es tritt ein Bindungs Zeitfehler auf, wenn der andere Operand vom Typ sbyte ,
short , int oder ist long .
Wenn einer der beiden Operanden vom Typ ist long , wird der andere Operand in den Typ konvertiert
long .
Wenn einer der beiden uint Operanden vom Typ und der andere Operand vom Typ sbyte , oder ist short
int , werden beide Operanden in den Typ konvertiert long .
Wenn einer der beiden Operanden vom Typ ist uint , wird der andere Operand in den Typ konvertiert
uint .
Andernfalls werden beide Operanden in den Typ konvertiert int .

Beachten Sie, dass die erste Regel Vorgänge, die den decimal Typ mit den double Typen und mischen, nicht
zulässt float . Die Regel folgt der Tatsache, dass keine impliziten Konvertierungen zwischen dem- decimal Typ
und dem-Typ und dem-Typ vorhanden sind double float .
Beachten Sie außerdem, dass es nicht möglich ist, dass ein Operand vom Typ ulong ist, wenn der andere
Operand einen ganzzahligen Typ mit Vorzeichen hat. Der Grund dafür ist, dass kein ganzzahliger Typ vorhanden
ist, der den vollständigen Bereich von und die ganzzahligen Typen mit Vorzeichen darstellen kann ulong .
In beiden oben genannten Fällen kann ein Umwandlungs Ausdruck verwendet werden, um einen Operanden
explizit in einen Typ zu konvertieren, der mit dem anderen Operanden kompatibel ist.
Im Beispiel

decimal AddPercent(decimal x, double percent) {


return x * (1.0 + percent / 100.0);
}

ein Fehler bei der Bindungs Zeit tritt auf, weil ein decimal nicht mit einem multipliziert werden kann double .
Der Fehler wird behoben, indem der zweite Operand wie folgt explizit in umgerechnet wird decimal :
decimal AddPercent(decimal x, double percent) {
return x * (decimal)(1.0 + percent / 100.0);
}

„Lifted“ Operatoren
Mithilfe von aufzurufenden Operatoren können vordefinierte und benutzerdefinierte Operatoren, die nicht auf
NULL festleg Bare Werttypen angewendet werden, auch mit null-fähigen Formularen dieser Typen verwendet
werden Gesteigerte Operatoren werden aus vordefinierten und benutzerdefinierten Operatoren erstellt, die
bestimmte Anforderungen erfüllen, wie im folgenden beschrieben:
Für die unären Operatoren

+ ++ - -- ! ~

eine angehobene Form eines Operators ist vorhanden, wenn der Operand und die Ergebnistypen beide
Werttypen sind, die keine NULL-Werte zulassen. Das angefügte Formular wird erstellt, indem ein
einzelner ? Modifizierer zu den Operanden-und Ergebnistypen hinzugefügt wird. Der Operator
"angehoben" erzeugt einen NULL-Wert, wenn der Operand NULL ist. Andernfalls entpackt der
angehobene Operator den Operanden, wendet den zugrunde liegenden Operator an und umschließt das
Ergebnis.
Für die binären Operatoren

+ - * / % & | ^ << >>

ein angezeigter Typ eines Operators ist vorhanden, wenn der Operand und die Ergebnistypen alle
Werttypen darstellen, die keine NULL-Werte zulassen. Das angefügte Formular wird erstellt ? , indem
jedem Operanden und Ergebnistyp ein einzelner Modifizierer hinzugefügt wird. Der gesteigerte Operator
erzeugt einen NULL-Wert, wenn ein oder beide Operanden NULL sind (eine Ausnahme ist der & -
Operator und der-Operator | des- bool? Typs, wie in booleschen logischen Operatorenbeschrieben).
Andernfalls entpackt der angehobene Operator die Operanden, wendet den zugrunde liegenden
Operator an und umschließt das Ergebnis.
Für die Gleichheits Operatoren

== !=

eine angehobene Form eines Operators ist vorhanden, wenn die Operanden Typen sowohl nicht auf
NULL festleg Bare Werttypen als auch, wenn der Ergebnistyp ist bool . Das angefügte Formular wird
erstellt, indem einem ? Operanden ein einzelner Modifizierer hinzugefügt wird. Der Operator
"angehoben" berücksichtigt zwei NULL-Werte gleich, und ein NULL-Wert entspricht keinem Wert, der
ungleich NULL ist. Wenn beide Operanden nicht NULL sind, entpackt der angehobene Operator die
Operanden und wendet den zugrunde liegenden Operator an, um das Ergebnis zu erhalten bool .
Für die relationalen Operatoren

< > <= >=

eine angehobene Form eines Operators ist vorhanden, wenn die Operanden Typen sowohl nicht auf
NULL festleg Bare Werttypen als auch, wenn der Ergebnistyp ist bool . Das angefügte Formular wird
erstellt, indem einem ? Operanden ein einzelner Modifizierer hinzugefügt wird. Der Operator
"angehoben" erzeugt den Wert, false Wenn ein oder beide Operanden NULL sind. Andernfalls entpackt
der angehobene Operator die Operanden und wendet den zugrunde liegenden Operator an, um das
Ergebnis zu erhalten bool .

Member lookup (Membersuche)


Bei der Suche nach Membern handelt es sich um den Prozess, bei dem die Bedeutung eines Namens im Kontext
eines Typs bestimmt wird. Eine Element Suche kann als Teil der Auswertung eines Simple_name (einfache
Namen) oder eines member_access (Member Access) in einem Ausdruck auftreten. Wenn die Simple_name
oder member_access als primary_expression einer invocation_expression (Methodenaufrufe) auftritt, wird der
Member als aufgerufen bezeichnet.
Wenn ein Member eine Methode oder ein Ereignis ist, oder wenn es sich um eine Konstante, ein Feld oder eine
Eigenschaft eines Delegattyps (Delegaten ) oderdes Typs dynamic (des dynamischen Typs) handelt, wird der
Member als Aufruf barer Ausdruck bezeichnet.
Die Element Suche berücksichtigt nicht nur den Namen eines Members, sondern auch die Anzahl der
Typparameter, die der Member hat und ob auf den Member zugegriffen werden kann. Für die Suche nach
Membern verfügen generische Methoden und generische generische Typen über die Anzahl der Typparameter,
die in den jeweiligen Deklarationen angegeben sind, und alle anderen Member haben keine Typparameter.
Eine Member-Suche mit einem Namen N mit K Typparametern in einem Typ T wird wie folgt verarbeitet:
Zuerst wird ein Satz barrierefreier Member mit dem Namen N bestimmt:
Wenn T ein Typparameter ist, dann entspricht der Satz der Menge der zugreif baren Member, die N
in jedem der Typen benannt sind, die als primäre Einschränkung oder sekundäre Einschränkung
(Typparameter Einschränkungen) für angegeben T sind, sowie der Menge der zugänglichen
Member, die in benannt sind N object .
Andernfalls besteht der Satz aus allen zugänglichen (Member Access) Membern N , die in benannt
T sind, einschließlich der geerbten Member und der zugänglichen Member N in object . Wenn T
ein konstruierter Typ ist, wird der Satz von Membern durch Ersetzen von Typargumenten abgerufen,
wie in Members von konstruierten Typenbeschrieben. Member, die einen- override Modifizierer
einschließen, werden aus dem Satz ausgeschlossen.
Wenn dann auf K 0 (null) gesetzt ist, werden alle nsted-Typen, deren Deklarationen Typparameter enthalten,
entfernt Wenn K nicht 0 (null) ist, werden alle Elemente mit einer anderen Anzahl von Typparametern
entfernt. Beachten Sie, dass K , wenn 0 (null) ist, keine Methoden mit Typparametern entfernt werden, da
der Typrückschluss-Prozess (Typrückschluss) möglicherweise die Typargumente ableiten kann.
Wenn der Member aufgerufen wird, werden alle nicht Aufruf baren Member aus dem Satz entfernt.
Als nächstes werden Elemente, die von anderen Membern ausgeblendet werden, aus dem Satz entfernt. Für
jedes Element S.M im Satz, wobei S der Typ ist, in dem der Member M deklariert wird, werden die
folgenden Regeln angewendet:
Wenn M eine Konstante, ein Feld, eine Eigenschaft, ein Ereignis oder ein Enumerationsmember ist,
werden alle in einem Basistyp von deklarierten Member S aus dem Satz entfernt.
Wenn M eine Typdeklaration ist, werden alle nicht-Typen, die in einem Basistyp von deklariert S
sind, aus dem Satz entfernt, und alle Typdeklarationen mit der gleichen Anzahl von Typparametern, die
M in einem Basistyp von deklariert S werden, werden aus dem Satz entfernt.
Wenn M eine Methode ist, werden alle nicht-Methoden Elemente, die in einem Basistyp von deklariert
S sind, aus dem Satz entfernt.
Als nächstes werden Schnittstellenmember, die von Klassenmembern ausgeblendet werden, aus dem Satz
entfernt. Dieser Schritt wirkt sich nur dann aus, wenn T ein Typparameter ist und T sowohl eine effektive
Basisklasse als als object auch eine nicht leere effektive Schnittstellen Menge aufweist (Typparameter
Einschränkungen). Für jeden Member S.M in der Menge, wobei S der Typ ist, in dem der Member M
deklariert wird, werden die folgenden Regeln angewendet, wenn S eine andere Klassen Deklaration als ist
object :
Wenn M eine Konstante, ein Feld, eine Eigenschaft, ein Ereignis, ein Enumerationsmember oder eine
Typdeklaration ist, werden alle in einer Schnittstellen Deklaration deklarierten Member aus dem Satz
entfernt.
Wenn M eine Methode ist, werden alle nicht-Methoden Member, die in einer Schnittstellen
Deklaration deklariert sind, aus dem Satz entfernt, und alle Methoden mit derselben Signatur, wie Sie
M in einer Schnittstellen Deklaration deklariert sind, werden aus dem Satz entfernt.
Schließlich wird das Ergebnis der Suche bestimmt, wenn verborgene Member entfernt wurden:
Wenn der Satz aus einem einzelnen Member besteht, der keine Methode ist, dann ist dieser Member
das Ergebnis der Suche.
Andernfalls ist diese Gruppe von Methoden das Ergebnis der Suche, wenn der Satz nur Methoden
enthält.
Andernfalls ist die Suche mehrdeutig, und es tritt ein Bindungs Zeitfehler auf.
Für Member-suchen in anderen Typen als Typparameter und Schnittstellen und für Member-suchen in
Schnittstellen, die nur eine einzelne Vererbung haben ( jede Schnittstelle in der Vererbungs Kette weist genau
null oder eine direkte Basisschnittstelle auf), besteht die Auswirkung der Such Regeln lediglich darin, dass
abgeleitete Member Basiselemente mit dem gleichen Namen oder der gleichen Signatur ausblenden. Solche
Suchvorgänge mit einer einzelnen Vererbung sind nie mehrdeutig. Die Mehrdeutigkeiten, die möglicherweise
von Member-suchen in Schnittstellen mit mehreren Vererbungen auftreten können, werden unter Zugreifen auf
die Benutzeroberflächebeschrieben.
Basis Typen
Für Zwecke der Element Suche wird ein Typ T als die folgenden Basis Typen betrachtet:
Wenn T ist object , dann T hat keinen Basistyp.
Wenn T ein enum_type ist, sind die Basis Typen von T die Klassentypen System.Enum , System.ValueType
und object .
Wenn T ein struct_type ist, sind die Basis Typen von T die Klassentypen System.ValueType und object .
Wenn T ein class_type ist, sind die Basis Typen von die T Basisklassen von T , einschließlich des Klassen
Typs object .
Wenn T ein INTERFACE_TYPE ist, sind die Basis Typen von T die Basis Schnittstellen von T und der-
Klassentyp object .
Wenn T ein array_type ist, sind die Basis Typen von T die Klassentypen System.Array und object .
Wenn T ein delegate_type ist, sind die Basis Typen von T die Klassentypen System.Delegate und object .

Function members (Funktionselemente)


Funktionsmember sind Elemente, die ausführbare Anweisungen enthalten. Funktionsmember sind immer
Member von Typen und können keine Member von Namespaces sein. C# definiert die folgenden Kategorien
von Funktionsmembern:
Methoden
Eigenschaften
Ereignisse
Indexer
Benutzerdefinierte Operatoren
Instanzkonstruktoren
Statische Konstruktoren
Destruktoren
Mit Ausnahme der destrukturtoren und statischer Konstruktoren (die nicht explizit aufgerufen werden können)
werden die in Funktionsmembern enthaltenen Anweisungen durch Funktionselement Aufrufe ausgeführt. Die
tatsächliche Syntax zum Schreiben eines Funktionsmember-aufzurufenden ist von der jeweiligen
Funktionsmember-Kategorie abhängig.
Die Argumentliste (Argumentlisten) eines Funktionsmember-aufzurufenden enthält tatsächliche Werte oder
Variablen Verweise für die Parameter des Funktionsmembers.
Aufrufe generischer Methoden können den Typrückschluss verwenden, um den Satz von Typargumenten zu
ermitteln, die an die Methode übergeben werden sollen. Dieser Prozess wird unter Typrückschlussbeschrieben.
Aufrufe von Methoden, Indexern, Operatoren und Instanzkonstruktoren verwenden die Überladungs Auflösung,
um zu bestimmen, welcher Satz von Funktions Membern aufgerufen werden soll. Dieser Prozess wird in der
Überladungs Auflösungbeschrieben.
Sobald ein bestimmter Funktionsmember zur Bindungs Zeit (möglicherweise durch Überladungs Auflösung)
identifiziert wurde, wird der tatsächliche Lauf Zeit Prozess des Aufrufs des Funktionsmembers in der
Kompilierzeit Überprüfung der dynamischen Überladungs Auflösungbeschrieben.
In der folgenden Tabelle wird die Verarbeitung zusammengefasst, die in-Konstrukten mit den sechs Kategorien
von Funktionsmembern stattfindet, die explizit aufgerufen werden können. In der-Tabelle e geben,, x y und
value Ausdrücke an, die als Variablen oder Werte klassifiziert sind, T einen Ausdruck, der als Typ klassifiziert
ist, F den einfachen Namen einer Methode und P den einfachen Namen einer Eigenschaft angibt.

ERST EL L EN B EISP IEL B ESC H REIB UN G

Methodenaufruf F(x,y) Die Überladungs Auflösung wird


angewendet, um die beste Methode
F in der enthaltenden Klasse oder
Struktur auszuwählen. Die-Methode
wird mit der Argumentliste aufgerufen
(x,y) . Wenn die-Methode nicht ist
static , ist der Instanzausdruck
this .

T.F(x,y) Die Überladungs Auflösung wird


angewendet, um die beste Methode
F in der Klasse oder Struktur
auszuwählen T . Ein Bindungs Zeit
Fehler tritt auf, wenn die Methode
nicht ist static . Die-Methode wird
mit der Argumentliste aufgerufen
(x,y) .

e.F(x,y) Die Überladungs Auflösung wird


angewendet, um die beste Methode F
in der Klasse, Struktur oder
Schnittstelle auszuwählen, die durch
den Typ von angegeben wird e . Ein
Bindungs Zeit Fehler tritt auf, wenn
die-Methode ist static . Die-
Methode wird mit dem
Instanzausdruck e und der
Argumentliste aufgerufen (x,y) .
ERST EL L EN B EISP IEL B ESC H REIB UN G

Eigenschaftenzugriff P Der- get Accessor der Eigenschaft


P in der enthaltenden Klasse oder
Struktur wird aufgerufen. Ein
Kompilierzeitfehler tritt auf, wenn P
schreibgeschützt ist. Wenn P nicht ist
static , ist der Instanzausdruck
this .

P = value Der- set Accessor der Eigenschaft


P in der enthaltenden Klasse oder
Struktur wird mit der Argumentliste
(value) aufgerufen. Ein
Kompilierzeitfehler tritt auf, wenn
schreibgeschützt P ist. Wenn P
nicht ist static , ist der
Instanzausdruck this .

T.P Der- get Accessor der Eigenschaft


P in der Klasse oder Struktur T wird
aufgerufen. Ein Kompilierzeitfehler tritt
auf, wenn P nicht ist static oder
wenn P schreibgeschützt ist.

T.P = value Der- set Accessor der Eigenschaft


P in der Klasse oder Struktur T wird
mit der Argumentliste aufgerufen
(value) . Ein Kompilierzeitfehler tritt
auf, wenn P nicht ist static oder
wenn schreibgeschützt P ist.

e.P Der- get Accessor der Eigenschaft


P in der Klasse, Struktur oder
Schnittstelle, die durch den-Typ
angegeben e wird, wird mit dem
Instanzausdruck aufgerufen e . Ein
Fehler bei der Bindungs Zeit tritt auf,
wenn P ist static oder wenn P
schreibgeschützt ist.

e.P = value Der set -Accessor der Eigenschaft


P in der Klasse, Struktur oder
Schnittstelle, die durch den Typ von
angegeben e wird, wird mit dem
Instanzausdruck e und der
Argumentliste aufgerufen (value) .
Ein Fehler bei der Bindungs Zeit tritt
auf, wenn P ist static oder wenn
schreibgeschützt P ist.

Ereignis Zugriff E += value Der- add Accessor des Ereignisses E


in der enthaltenden Klasse oder
Struktur wird aufgerufen. Wenn E
nicht statisch ist, ist der
Instanzausdruck this .
ERST EL L EN B EISP IEL B ESC H REIB UN G

E -= value Der- remove Accessor des Ereignisses


E in der enthaltenden Klasse oder
Struktur wird aufgerufen. Wenn E
nicht statisch ist, ist der
Instanzausdruck this .

T.E += value Der- add Accessor des Ereignisses E


in der Klasse oder Struktur T wird
aufgerufen. Ein Fehler bei der Bindungs
Zeit tritt auf, wenn E nicht statisch
ist.

T.E -= value Der- remove Accessor des Ereignisses


E in der Klasse oder Struktur T wird
aufgerufen. Ein Fehler bei der Bindungs
Zeit tritt auf, wenn E nicht statisch
ist.

e.E += value Der- add Accessor des Ereignisses E


in der Klasse, Struktur oder
Schnittstelle, die durch den-Typ
angegeben e wird, wird mit dem
Instanzausdruck aufgerufen e . Ein
Fehler bei der Bindungs Zeit tritt auf,
wenn E statisch ist.

e.E -= value Der- remove Accessor des Ereignisses


E in der Klasse, Struktur oder
Schnittstelle, die durch den-Typ
angegeben e wird, wird mit dem
Instanzausdruck aufgerufen e . Ein
Fehler bei der Bindungs Zeit tritt auf,
wenn E statisch ist.

Indexerzugriff e[x,y] Die Überladungs Auflösung wird


angewendet, um den besten Indexer in
der Klasse, Struktur oder Schnittstelle
auszuwählen, die durch den Typ von e
angegeben wird. Der get -Accessor
des Indexers wird mit dem
Instanzausdruck e und der
Argumentliste aufgerufen (x,y) . Ein
Bindungs Zeit Fehler tritt auf, wenn der
Indexer schreibgeschützt ist.

e[x,y] = value Die Überladungs Auflösung wird


angewendet, um den besten Indexer in
der Klasse, Struktur oder Schnittstelle
auszuwählen, die durch den Typ von
angegeben wird e . Der set -
Accessor des Indexers wird mit dem
Instanzausdruck e und der
Argumentliste aufgerufen
(x,y,value) . Ein Bindungs Zeit
Fehler tritt auf, wenn der Indexer
schreibgeschützt ist.
ERST EL L EN B EISP IEL B ESC H REIB UN G

Operator Aufruf -x Die Überladungs Auflösung wird


angewendet, um den besten unären
Operator in der Klasse oder Struktur
auszuwählen, die durch den Typ von
angegeben wird x . Der ausgewählte
Operator wird mit der Argumentliste
aufgerufen (x) .

x + y Die Überladungs Auflösung wird


angewendet, um den besten binären
Operator in den Klassen oder
Strukturen auszuwählen, die von den
Typen von und angegeben werden x
y . Der ausgewählte Operator wird
mit der Argumentliste aufgerufen
(x,y) .

Instanzkonstruktoraufruf new T(x,y) Die Überladungs Auflösung wird


angewendet, um den besten
Instanzkonstruktor in der Klasse oder
Struktur auszuwählen T . Der
Instanzkonstruktor wird mit der
Argumentliste aufgerufen (x,y) .

Argument Listen
Jeder Funktionsmember und Delegataufruf enthält eine Argumentliste, die tatsächliche Werte oder Variablen
Verweise für die Parameter des Funktionsmembers bereitstellt. Die Syntax zum Angeben der Argumentliste
eines Funktionsmember-aufzurufenden ist von der Funktionsmember-Kategorie abhängig:
Bei Instanzkonstruktoren, Methoden, Indexern und Delegaten werden die Argumente als argument_list
angegeben, wie unten beschrieben. Bei Indexers schließt beim Aufrufen der-Zugriffsmethode die set
Argumentliste zusätzlich den Ausdruck ein, der als rechter Operand des Zuweisungs Operators angegeben
ist.
Bei-Eigenschaften ist die Argumentliste leer, wenn der- get Accessor aufgerufen wird, und besteht aus dem
Ausdruck, der beim Aufrufen der Zugriffsmethode als rechter Operand des Zuweisungs Operators
angegeben wurde set .
Bei Ereignissen besteht die Argumentliste aus dem Ausdruck, der als rechter Operand des or- += Operators
angegeben ist -= .
Bei benutzerdefinierten Operatoren besteht die Argumentliste aus dem einzelnen Operanden des unären
Operators oder den beiden Operanden des binären Operators.
Die Argumente von Eigenschaften (Eigenschaften), Ereignissen (Ereignissen) und benutzerdefinierten
Operatoren (Operatoren) werden immer als Wert Parameter (value-Parameter) übermittelt. Die Argumente von
Indexer (Indexer) werden immer als Wert Parameter (Wert Parameter) oder Parameter Arrays (Parameter
Arrays) übergeben. Verweis-und Ausgabeparameter werden für diese Kategorien von Funktionsmembern nicht
unterstützt.
Die Argumente eines Instanzkonstruktors, einer Methode, eines Indexers oder eines delegataufrufers werden als
argument_list angegeben:
argument_list
: argument (',' argument)*
;

argument
: argument_name? argument_value
;

argument_name
: identifier ':'
;

argument_value
: expression
| 'ref' variable_reference
| 'out' variable_reference
;

Ein argument_list besteht aus einem oder mehreren Argumenten, getrennt durch Kommas. Jedes Argument
besteht aus einem optionalen argument_name gefolgt von einem argument_value. Ein Argument mit einem
argument_name wird als *benanntes Argument _ bezeichnet, wohingegen ein Argument * ohne
argument_name ein *Positions Argument ist. Es ist ein Fehler für ein Positions Argument, das nach einem
benannten Argument in einem _argument_list * angezeigt wird.
Der argument_value kann eine der folgenden Formen annehmen:
Ein Ausdruck, der angibt, dass das Argument als Wert Parameter (Wert Parameter) übergeben wird.
Das Schlüsselwort ref , gefolgt von einem variable_reference (Variablen Verweise), das angibt, dass das
Argument als Verweis Parameter (Verweis Parameter) übergeben wird. Eine Variable muss definitiv
zugewiesen werden (definitive Zuweisung), bevor Sie als Verweis Parameter übergeben werden kann. Das
Schlüsselwort out , gefolgt von einem variable_reference (Variablen Verweise), das angibt, dass das
Argument als Ausgabeparameter (Ausgabeparameter) übergeben wird. Eine Variable wird als definitiv
zugewiesen (definitive Zuweisung) nach einem Funktionselement Aufruf, bei dem die Variable als Output-
Parameter übergeben wird.
Entsprechende Parameter
Für jedes Argument in einer Argumentliste muss ein entsprechender Parameter im Funktions Member oder
Delegaten vorhanden sein, der aufgerufen wird.
Die im folgenden verwendete Parameterliste wird wie folgt bestimmt:
Bei virtuellen Methoden und indexatoren, die in Klassen definiert sind, wird die Parameterliste aus der
spezifischsten Deklaration oder Überschreibung des Funktionsmembers ausgewählt, beginnend mit dem
statischen Typ des Empfängers und Durchsuchen der zugehörigen Basisklassen.
Für Schnittstellen Methoden und Indexer wird die Parameterliste aus der spezifischsten Definition des
Members ausgewählt, beginnend mit dem Schnittstellentyp und Durchsuchen der Basis Schnittstellen. Wenn
keine eindeutige Parameterliste gefunden wird, wird eine Parameterliste mit nicht zugänglichen Namen und
optionalen Parametern erstellt, sodass Aufrufe keine benannten Parameter verwenden können oder keine
optionalen Argumente weglassen.
Bei partiellen Methoden wird die Parameterliste der definierenden partiellen Methoden Deklaration
verwendet.
Für alle anderen Funktionsmember und Delegaten gibt es nur eine einzige Parameterliste, die verwendet
wird.
Die Position eines Arguments oder Parameters wird als die Anzahl der Argumente oder Parameter definiert, die
in der Argumentliste oder Parameterliste vorangestellt sind.
Die entsprechenden Parameter für Funktionsmember-Argumente werden wie folgt festgelegt:
Argumente im argument_list von Instanzkonstruktoren, Methoden, Indexern und Delegaten:
Ein Positions Argument, bei dem ein fester Parameter an derselben Position in der Parameterliste
auftritt, entspricht diesem Parameter.
Ein Positions Argument eines Funktionsmembers mit einem Parameter Array, das in der normalen
Form aufgerufen wird, entspricht dem Parameter Array, das an derselben Position in der
Parameterliste vorkommen muss.
Ein Positions Argument eines Funktionsmembers mit einem Parameter Array, das in der erweiterten
Form aufgerufen wird, wobei kein fester Parameter an derselben Position in der Parameterliste auftritt,
entspricht einem Element im Parameter Array.
Ein benanntes Argument entspricht dem-Parameter mit dem gleichen Namen in der Parameterliste.
Bei Indexers set entspricht der als rechter Operand des Zuweisungs Operators angegebene
Ausdruck dem impliziten value Parameter der Accessor-Deklaration, wenn der Accessor aufgerufen
wird set .
Bei-Eigenschaften get gibt es keine Argumente, wenn der-Accessor aufgerufen wird. Wenn der- set
Accessor aufgerufen wird, entspricht der als rechter Operand des Zuweisungs Operators angegebene
Ausdruck dem impliziten value Parameter der set Accessordeklaration.
Bei benutzerdefinierten unären Operatoren (einschließlich Konvertierungen) entspricht der einzelne Operand
dem einzelnen Parameter der Operator Deklaration.
Für benutzerdefinierte binäre Operatoren entspricht der linke Operand dem ersten Parameter, und der rechte
Operand entspricht dem zweiten Parameter der Operator Deklaration.
Lauf Zeit Auswertung von Argumentlisten
Während der Lauf Zeit Verarbeitung eines Funktionsmember-aufzurufenden (Kompilierzeit Überprüfung der
dynamischen Überladungs Auflösung) werden die Ausdrücke oder Variablen Verweise einer Argumentliste in
der Reihenfolge von links nach rechts ausgewertet, wie folgt:
Bei einem value-Parameter wird der Argument Ausdruck ausgewertet und eine implizite Konvertierung
(implizite Konvertierungen) in den entsprechenden Parametertyp durchgeführt. Der resultierende Wert wird
zum Anfangswert des value-Parameters im Funktionselement Aufruf.
Bei einem Verweis-oder Ausgabeparameter wird der Variablen Verweis ausgewertet, und der resultierende
Speicherort wird zu dem Speicherort, der durch den-Parameter im Aufruf der Funktionselemente dargestellt
wird. Wenn der als Verweis oder Ausgabeparameter angegebene Variablen Verweis ein Array Element einer
reference_type ist, wird eine Lauf Zeit Überprüfung durchgeführt, um sicherzustellen, dass der Elementtyp
des Arrays mit dem Parametertyp identisch ist. Wenn bei dieser Überprüfung ein Fehler auftritt, wird eine
ausgelöst System.ArrayTypeMismatchException .
Methoden, Indexer und Instanzkonstruktoren können Ihren äußersten ganz rechts Parameter als Parameter
Array deklarieren (Parameter Arrays). Solche Funktionsmember werden entweder in ihrer normalen Form oder
in der erweiterten Form aufgerufen, je nachdem, welche anwendbar ist (anwendbares Funktionsmember):
Wenn ein Funktionsmember mit einem Parameter Array in der normalen Form aufgerufen wird, muss das
für das Parameter Array angegebene Argument ein einzelner Ausdruck sein, der implizit konvertierbar
(implizite Konvertierungen) in den Typ des Parameter Arrays ist. In diesem Fall verhält sich das Parameter
Array genau wie ein value-Parameter.
Wenn ein Funktionsmember mit einem Parameter Array in der erweiterten Form aufgerufen wird, muss der
Aufruf NULL oder mehr Positions Argumente für das Parameter Array angeben, wobei jedes Argument ein
Ausdruck ist, der implizit konvertierbar (implizite Konvertierungen) in den Elementtyp des Parameter Arrays
ist. In diesem Fall erstellt der Aufruf eine Instanz des Parameter Array Typs mit einer Länge, die der Anzahl
der Argumente entspricht, initialisiert die Elemente der Array Instanz mit den angegebenen Argument
Werten und verwendet die neu erstellte Array Instanz als tatsächliches Argument.
Die Ausdrücke einer Argumentliste werden immer in der Reihenfolge ausgewertet, in der Sie geschrieben
werden. Das Beispiel

class Test
{
static void F(int x, int y = -1, int z = -2) {
System.Console.WriteLine("x = {0}, y = {1}, z = {2}", x, y, z);
}

static void Main() {


int i = 0;
F(i++, i++, i++);
F(z: i++, x: i++);
}
}

erzeugt die Ausgabe

x = 0, y = 1, z = 2
x = 4, y = -1, z = 3

Die Array-Co-Varianz Regeln (Array-Kovarianz) erlauben, dass ein Wert eines Arraytyps A[] ein Verweis auf
eine Instanz eines Arraytyps B[] ist, vorausgesetzt, dass eine implizite Verweis Konvertierung von in
vorhanden ist B A . Aufgrund dieser Regeln ist eine Lauf Zeit Überprüfung erforderlich, wenn ein Array
Element einer reference_type als Verweis-oder Ausgabeparameter übergeben wird, um sicherzustellen, dass der
tatsächliche Elementtyp des Arrays mit dem des Parameters identisch ist. Im Beispiel

class Test
{
static void F(ref object x) {...}

static void Main() {


object[] a = new object[10];
object[] b = new string[10];
F(ref a[0]); // Ok
F(ref b[1]); // ArrayTypeMismatchException
}
}

der zweite Aufruf von F bewirkt System.ArrayTypeMismatchException , dass eine ausgelöst wird, da der
tatsächliche Elementtyp von b ist string und nicht object .
Wenn ein Funktionsmember mit einem Parameter Array in der erweiterten Form aufgerufen wird, wird der
Aufruf genau so verarbeitet, als ob ein Array Erstellungs Ausdruck mit einem Arrayinitialisierer (Array
Erstellungs Ausdrücke) um die erweiterten Parameter eingefügt wurde. Beispielsweise mit der Deklaration

void F(int x, int y, params object[] args);

die folgenden Aufrufe der erweiterten Form der Methode

F(10, 20);
F(10, 20, 30, 40);
F(10, 20, 1, "hello", 3.0);

genau entsprechen
F(10, 20, new object[] {});
F(10, 20, new object[] {30, 40});
F(10, 20, new object[] {1, "hello", 3.0});

Beachten Sie insbesondere, dass ein leeres Array erstellt wird, wenn keine Argumente für das Parameter Array
angegeben werden.
Wenn Argumente von einem Funktionsmember mit entsprechenden optionalen Parametern ausgelassen
werden, werden die Standardargumente der Deklaration des Funktions Members implizit übermittelt. Da diese
immer konstant sind, wirkt sich Ihre Auswertung nicht auf die Auswertungs Reihenfolge der restlichen
Argumente aus.
Typrückschluss
Wenn eine generische Methode ohne Angabe von Typargumenten aufgerufen wird, versucht ein
Typrückschluss -Prozess, Typargumente für den Aufruf abzuleiten. Das vorhanden sein des Typrückschlusses
ermöglicht die Verwendung einer bequemeren Syntax zum Aufrufen einer generischen Methode und
ermöglicht dem Programmierer, das Angeben von redundanten Typinformationen zu vermeiden. Beispielsweise
mit der Methoden Deklaration:

class Chooser
{
static Random rand = new Random();

public static T Choose<T>(T first, T second) {


return (rand.Next(2) == 0)? first: second;
}
}

Es ist möglich, die Methode aufzurufen, Choose ohne explizit ein Typargument anzugeben:

int i = Chooser.Choose(5, 213); // Calls Choose<int>

string s = Chooser.Choose("foo", "bar"); // Calls Choose<string>

Durch den Typrückschluss werden die Typargumente int und string aus den Argumenten der Methode
ermittelt.
Der Typrückschluss tritt als Teil der Bindungs Zeit Verarbeitung eines Methoden Aufrufs (Methodenaufrufe) auf
und findet vor dem Schritt zur Überladungs Auflösung des Aufrufs statt. Wenn eine bestimmte Methoden
Gruppe in einem Methodenaufruf angegeben wird und im Rahmen des Methoden aufruschlusses keine
Typargumente angegeben werden, wird der Typrückschluss auf jede generische Methode in der Methoden
Gruppe angewendet. Wenn der Typrückschluss erfolgreich ist, werden die Typargumente abgeleitet, um die
Argument Typen für die nachfolgende Überladungs Auflösung zu bestimmen. Wenn die Überladungs Auflösung
eine generische Methode als die aufzurufende Methode auswählt, werden die abgerufenen Typargumente als
tatsächliche Typargumente für den Aufruf verwendet. Wenn der Typrückschluss für eine bestimmte Methode
fehlschlägt, wird diese Methode nicht an der Überladungs Auflösung beteiligt. Der Fehler beim Typrückschluss
in und von sich führt nicht zu einem Bindungs Fehler. Allerdings führt dies häufig zu einem Bindungs Fehler,
wenn die Überladungs Auflösung dann keine anwendbaren Methoden findet.
Wenn sich die angegebene Anzahl von Argumenten von der Anzahl der Parameter in der Methode
unterscheidet, schlägt die Ableitung sofort fehl. Angenommen, die generische Methode hat die folgende
Signatur:
Tr M<X1,...,Xn>(T1 x1, ..., Tm xm)

Mit einem Methoden Aufrufder Form M(E1...Em) ist die Aufgabe des Typrückschlusses das Auffinden
eindeutiger Typargumente S1...Sn für jeden der Typparameter, X1...Xn sodass der-Befehl
M<S1...Sn>(E1...Em) gültig wird.

Während des Xi Abschlusses wird jeder Typparameter entweder mit einem bestimmten Typ korrigiert Si
oder mit einem zugeordneten Satz von Begrenzungen nicht korrigiert . Jede der Begrenzungen ist ein Typ T .
Anfänglich wird jede Typvariable Xi mit einem leeren Satz von Begrenzungen nicht korrigiert.
Der Typrückschluss findet in Phasen statt. In jeder Phase wird versucht, Typargumente für weitere Typvariablen
basierend auf den Ergebnissen der vorherigen Phase abzuleiten. In der ersten Phase werden einige anfängliche
Rückschlüsse von Begrenzungen erstellt, während in der zweiten Phase Typvariablen für bestimmte Typen
korrigiert und weitere Begrenzungen abgeleitet werden. Die zweite Phase muss möglicherweise mehrmals
wiederholt werden.
Hinweis: Der Typrückschluss findet nicht nur statt, wenn eine generische Methode aufgerufen wird. Der
Typrückschluss für die Konvertierung von Methoden Gruppen wird unter Typrückschluss für die Konvertierung
von Methoden Gruppen beschrieben und der beste allgemeine Typ eines Satzes von Ausdrücken finden Sie
unter Ermitteln des besten allgemeinen Typs eines Satzes von Ausdrücken.
Die erste Phase
Für jedes der Methodenargumente Ei :
Wenn Ei eine anonyme Funktion ist, wird ein expliziter parametertyprückschluss (explizite
parametertyprückschluss) von Ei zu Ti
Andernfalls, wenn Ei einen-Typ aufweist U und xi ein value-Parameter ist, wird ein untergeordneter
Daten Rückschluss von U bis erstellt Ti .
Andernfalls, wenn Ei einen-Typ aufweist U und xi ein-oder-Parameter ist, ref out wird ein genauer
Rückschluss von U auf vorgenommen Ti .
Andernfalls wird kein Rückschluss für dieses Argument gemacht.
Die zweite Phase
Die zweite Phase verläuft wie folgt:
Alle unfixed -Typvariablen Xi , die nicht von abhängen (Abhängigkeit), Xj werden korrigiert (Korrektur).
Wenn keine derartigen Typvariablen vorhanden sind, werden alle nicht fixierten Typvariablen Xi korrigiert ,
für die Folgendes gilt:
Es ist mindestens eine Typvariable vorhanden Xj , von der abhängig ist. Xi
Xi weist einen nicht leeren Satz von Begrenzungen auf.
Wenn keine Typvariablen vorhanden sind und noch immer unfixe Typvariablen vorhanden sind, schlägt der
Typrückschluss fehl.
Andernfalls ist der Typrückschluss erfolgreich, wenn keine weiteren unfixed -Typvariablen vorhanden sind.
Andernfalls wird für alle Argumente Ei mit dem entsprechenden Parametertyp, Ti bei dem die
Ausgabetypen (Ausgabetypen) nicht fixierte Typvariablen enthalten Xj , die Eingabetypen (Eingabetypen)
jedoch nicht, ein Ausgabetyp Rückschluss (Ausgabetyp Rückschlüsse) von Ei auf erstellt Ti . Die zweite
Phase wird wiederholt.
Eingabetypen
Wenn E eine Methoden Gruppe oder implizit typisierte anonyme Funktion ist und T ein Delegattyp oder ein
Ausdrucks Strukturtyp ist, sind alle Parametertypen von T Eingabetypen vom E Typ T .
Ausgabetypen
Wenn E eine Methoden Gruppe oder eine anonyme Funktion ist und T ein Delegattyp oder ein Ausdrucks
Strukturtyp ist, ist der T Rückgabetyp von ein Ausgabetyp von E mit dem Typ T .
Hän
Eine Variable vom Typ " unfixed " Xi ist direkt von einer Variablen vom Typ "unfixed" abhängig , Xj Wenn für
ein Argument vom Ek Typ Tk Xj in einem Eingabetyp von Ek mit Type auftritt Tk und Xi in einem
Ausgabetyp von Ek mit Type auftritt Tk .
Xjhängt von ab Xi , wenn direkt von oder abhängig ist, von der abhängig ist Xj Xi und abhängig ist Xi
Xk Xk Xj . Daher ist "hängt von" der transitiv, aber nicht reflexive Abschluss von "ist direkt abhängig".

Ausschlüsse auf Ausgabetyp


Ein ausgabetyprückschluss erfolgt auf folgende Weise von einem Ausdruck E zu einem Typ T :
Wenn E eine anonyme Funktion mit einem abzurufenden Rückgabetyp U (Rückgabetyp abgeleitet) ist und
T ein Delegattyp oder ein Ausdrucks Strukturtyp mit dem Rückgabetyp ist Tb , wird ein niedrigerer
gebundener Typrückschluss (niedrigerer gebundener Rückschluss) von U bis vorgenommen Tb .
Andernfalls, wenn E eine Methoden Gruppe ist und T ein Delegattyp oder ein Ausdrucks Strukturtyp mit
Parametertypen T1...Tk und dem Rückgabetyp ist Tb und die Überladungs Auflösung von E mit den-
Typen T1...Tk eine einzelne Methode mit dem Rückgabetyp ergibt U , wird ein untergeordneter Daten
Rückschluss von U in vorgenommen Tb .
Andernfalls, wenn E ein Ausdruck mit dem Typ ist U , wird ein untergeordneter Daten Rückschluss von U
bis erstellt T .
Andernfalls werden keine Rückschlüsse gemacht.
Explizite Parametertyp Rückschlüsse
Ein expliziter Parameter-Typrückschluss erfolgt auf folgende Weise von einem Ausdruck E zu einem Typ T :
Wenn E eine explizit typisierte anonyme Funktion mit Parametertypen U1...Uk ist und T ein Delegattyp
oder ein Ausdrucks Strukturtyp mit Parametertypen ist V1...Vk Ui , wird für jeden ein genauer Rück
Schluss (exakte Rückschlüsse) von Ui auf das entsprechende-Objekt erstellt Vi .
Exakte Rückschlüsse
Eine genaue Ableitung von einem Typ U zu einem Typ V erfolgt wie folgt:
Wenn V eine der unfixen ist Xi U , wird dem Satz der exakten Begrenzungen für hinzugefügt Xi .
Andernfalls legt V1...Vk und U1...Uk fest, indem überprüft wird, ob eines der folgenden Fälle zutrifft:
Vist ein Arraytyp V1[...] und U ist ein Arraytyp U1[...] desselben Rang.
V der Typ V1? und U ist der Typ. U1?
V ist ein konstruierter Typ C<V1...Vk> und U ist ein konstruierter Typ. C<U1...Uk>
Wenn einer dieser Fälle zutrifft, erfolgt ein genauer Rückschluss von jedem Ui zum entsprechenden Vi
.
Andernfalls werden keine Rückschlüsse gemacht.
Unten gebundene Rückschlüsse
Ein untergeordneter Daten Rückschluss von einem Typ U zu einem Typ V wird wie folgt erstellt:
Wenn V eine der nicht fixierten ist Xi U , wird der Gruppe der unteren Grenzen für hinzugefügt Xi .
Andernfalls, wenn V der-Typ V1? und U der-Typ ist, U1? wird ein niedrigerer gebundener Rückschluss
von U1 auf vorgenommen V1 .
Andernfalls legt U1...Uk und V1...Vk fest, indem überprüft wird, ob eines der folgenden Fälle zutrifft:
V ist ein Arraytyp V1[...] und U ist ein Arraytyp U1[...] (oder ein Typparameter, dessen
effektiver Basistyp ist U1[...] ) desselben Rang.
V ist eine von IEnumerable<V1> , ICollection<V1> oder, IList<V1> und U ist ein
eindimensionaler Arraytyp U1[] (oder ein Typparameter, dessen effektiver Basistyp ist U1[] ).
V ist eine konstruierte Klasse, eine Struktur, eine Schnittstelle oder ein Delegattyp, C<V1...Vk>
und es gibt einen eindeutigen Typ, C<U1...Uk> sodass U (oder, wenn U ein Typparameter ist, die
effektive Basisklasse oder ein beliebiger Member des effektiven Schnittstellen Satzes) mit identisch
ist, von (direkt oder indirekt) erbt oder (direkt oder indirekt) implementiert C<U1...Uk> .
(Die "Eindeutigkeits Einschränkung" bedeutet, dass in der Fall Schnittstelle
C<T> {} class U: C<X>, C<Y> {} kein Rückschluss erfolgt, wenn von zu abgeleitet wird, U C<T>
da U1 möglicherweise X oder ist Y .)
Wenn einer dieser Fälle zutrifft, erfolgt ein Rückschluss von jedem Ui zum entsprechenden, Vi wie
folgt:
Wenn Ui nicht bekannt ist, dass es sich um einen Verweistyp handelt, wird ein genauer Rückschluss
gemacht.
Andernfalls, wenn U ein Arraytyp ist, wird ein Rück Schluss mit niedrigerer Bindung erstellt.
Andernfalls V C<V1...Vk> hängt der Rückschluss vom i-th Type-Parameter von ab C :
Wenn Sie kovariant ist, wird ein Rück Schluss ausgelöst.
Wenn Sie kontra Variant ist, wird eine obere gebundene Ableitung vorgenommen.
Wenn Sie invariante ist, wird ein genauer Rückschluss gemacht.
Andernfalls werden keine Rückschlüsse gemacht.
Obere gebundene Rückschlüsse
Eine obere gebundene Ableitung von einem Typ U zu einem Typ V wird wie folgt durchgeführt:
Wenn V eine der nicht fixierten ist Xi U , wird dem Satz von oberen Begrenzungen für hinzugefügt
Xi .

Andernfalls legt V1...Vk und U1...Uk fest, indem überprüft wird, ob eines der folgenden Fälle zutrifft:
U ist ein Arraytyp U1[...] und V ist ein Arraytyp V1[...] desselben Rang.
U ist eine von IEnumerable<Ue> , ICollection<Ue> oder, IList<Ue> und V ist ein
eindimensionaler Arraytyp. Ve[]
U der Typ U1? und V ist der Typ. V1?

U ist eine konstruierte Klasse, Struktur, Schnittstelle oder Delegattyp C<U1...Uk> und V ist eine
Klasse, Struktur, Schnittstelle oder ein Delegattyp, der mit identisch ist, von (direkt oder indirekt)
erbt oder (direkt oder indirekt) einen eindeutigen Typ implementiert. C<V1...Vk>
(Die "Eindeutigkeits Einschränkung" bedeutet, dass, wenn dies der Fall ist
interface C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{} , kein Rückschluss erfolgt, wenn von zu
abgeleitet wird C<U1> V<Q> . Rückschlüsse werden nicht von U1 entweder in oder aus
vorgenommen X<Q> Y<Q> .)
Wenn einer dieser Fälle zutrifft, erfolgt ein Rückschluss von jedem Ui zum entsprechenden, Vi wie
folgt:
Wenn Ui nicht bekannt ist, dass es sich um einen Verweistyp handelt, wird ein genauer Rückschluss
gemacht.
Andernfalls, wenn V ein Arraytyp ist, wird eine obere gebundene Ableitung vorgenommen.
Andernfalls U C<U1...Uk> hängt der Rückschluss vom i-th Type-Parameter von ab C :
Wenn Sie kovariant ist, wird eine obere gebundene Ableitung vorgenommen.
Wenn Sie kontra Variant ist, wird ein Rück Schluss festgestellt.
Wenn Sie invariante ist, wird ein genauer Rückschluss gemacht.
Andernfalls werden keine Rückschlüsse gemacht.
Korrektur von
Eine Variable vom Typ " unfixed " Xi mit einem Satz von Begrenzungen ist wie folgt festgelegt :
Der Satz von Kandidaten Typen Uj beginnt als der Satz aller Typen im Satz von Begrenzungen für Xi .
Anschließend untersuchen wir jede gebundene Grenze Xi : für jede exakte Grenze U Xi aller Typen Uj ,
die nicht identisch sind, U werden aus dem Kandidaten Satz entfernt. Für jede untere Grenze U Xi aller
Typen Uj , auf die keine implizite Konvertierung von erfolgt, U werden aus dem Kandidaten Satz entfernt.
Für jede obere Grenze U Xi aller Typen Uj , von denen keine implizite Konvertierung in vorhanden ist, U
werden aus dem Kandidaten Satz entfernt.
Wenn es sich bei den verbleibenden Kandidaten Typen um Uj einen eindeutigen Typ handelt V , von dem
eine implizite Konvertierung in alle anderen Kandidaten Typen erfolgt, dann Xi ist auf fest V .
Andernfalls schlägt der Typrückschluss fehl.
Rückgabetyp abgeleitet
Der abzurufende Rückgabetyp einer anonymen Funktion F wird bei der Typrückschluss-und Überladungs
Auflösung verwendet. Der zurückgegebene Rückgabetyp kann nur für eine anonyme Funktion bestimmt
werden, bei der alle Parametertypen bekannt sind, da Sie entweder explizit angegeben werden, indem Sie durch
eine anonyme Funktions Konvertierung bereitgestellt oder während des Typrückschlusses bei einem
einschließenden generischen Methodenaufruf abgeleitet werden.
Der abhergelegte Ergebnistyp wird wie folgt bestimmt:
Wenn der Text von F ein Ausdruck ist, der über einen-Typ verfügt, ist der herausgestellte Ergebnistyp von
F der Typ dieses Ausdrucks.
Wenn es sich bei dem Text von F um einen- Block handelt und der Satz von Ausdrücken in den-
Anweisungen des Blocks return einen am besten allgemeinen Typ aufweist (suchen Sie T den am besten
allgemeinen Typ eines Satzes von Ausdrücken), dann ist der abhergelegte Ergebnistyp von F T .
Andernfalls kann ein Ergebnistyp nicht für abgeleitet werden F .
Der zurückgegebene Rückgabetyp wird wie folgt bestimmt:
Wenn F Async ist und der Text von F entweder ein Ausdruck ist, der als Nothing (Ausdrucks
Klassifizierungen) klassifiziert ist, oder ein Anweisungsblock, in dem keine Return-Anweisungen über
Ausdrücke verfügen, ist der Rückschluss Rückgabetyp. System.Threading.Tasks.Task
Wenn F Async ist und einen abhergelegten Ergebnistyp aufweist T , ist der Rückschluss Rückgabetyp
System.Threading.Tasks.Task<T> .
Wenn F nicht Async ist und einen abhergelegten Ergebnistyp aufweist T , ist der Rückschluss Rückgabetyp
T .
Andernfalls kann kein Rückgabetyp für abgeleitet werden F .
Sehen Sie sich als Beispiel für den Typrückschluss mit anonymen Funktionen die Select in der-Klasse
deklarierte Erweiterungsmethode an System.Linq.Enumerable :
namespace System.Linq
{
public static class Enumerable
{
public static IEnumerable<TResult> Select<TSource,TResult>(
this IEnumerable<TSource> source,
Func<TSource,TResult> selector)
{
foreach (TSource element in source) yield return selector(element);
}
}
}

Vorausgesetzt, System.Linq dass der Namespace mit einer using -Klausel importiert wurde und eine-Klasse
Customer mit einer- Name Eigenschaft des Typs angegeben wurde string , kann die- Select Methode
verwendet werden, um die Namen einer Kundenliste auszuwählen:

List<Customer> customers = GetCustomerList();


IEnumerable<string> names = customers.Select(c => c.Name);

Der Aufruf der Erweiterungsmethode (Erweiterungs Methodenaufrufe) von Select wird verarbeitet, indem der
Aufruf in einen statischen Methodenaufruf umgeschrieben wird:

IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);

Da Typargumente nicht explizit angegeben wurden, wird der Typrückschluss verwendet, um die Typargumente
abzuleiten. Zuerst ist das- customers Argument mit dem-Parameter verknüpft und wird als source abgeleitet
T Customer . Anschließend wird mit dem oben beschriebenen Prozess der anonymen Funktionstyp Ableitung
der c Typ angegeben Customer , und der Ausdruck c.Name ist mit dem Rückgabetyp des selector
Parameters verknüpft, S string der als abgeleitet wird. Daher entspricht der Aufruf dem

Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)

und das Ergebnis ist vom Typ IEnumerable<string> .


Im folgenden Beispiel wird veranschaulicht, wie der anonyme Funktionstyp Rückschluss das Eingeben von
Typinformationen zwischen Argumenten in einem generischen Methodenaufruf zulässt. Bei Angabe der-
Methode:

static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2) {


return f2(f1(value));
}

Typrückschluss für den Aufruf:

double seconds = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalSeconds);

geht wie folgt vor: zuerst wird das-Argument mit "1:15:30" dem-Parameter verknüpft, der als value
abgeleitet wird X string . Anschließend erhält der-Parameter der ersten anonymen Funktion, s , den
abzurufenden Typ string , und der Ausdruck TimeSpan.Parse(s) ist mit dem Rückgabetyp von verknüpft f1 ,
der als abgeleitet wird Y System.TimeSpan . Schließlich erhält der-Parameter der zweiten anonymen Funktion,
t , den abzurufenden Typ System.TimeSpan , und der Ausdruck t.TotalSeconds ist mit dem Rückgabetyp von
verknüpft, der als f2 abgeleitet wird Z double . Daher ist das Ergebnis des auf-aufgabens vom Typ double .
Typrückschluss für die Konvertierung von Methoden Gruppen
Ähnlich wie bei Aufrufen von generischen Methoden muss der Typrückschluss auch angewendet werden, wenn
eine Methoden Gruppe M , die eine generische Methode enthält, in einen angegebenen Delegattyp konvertiert
wird D (Methoden Gruppen Konvertierungen). Bei Angabe einer Methode

Tr M<X1...Xn>(T1 x1 ... Tm xm)

und die Methoden Gruppe, die M dem Delegattyp zugewiesen wird, D ist die Aufgabe des Typrückschlusses,
die Typargumente zu suchen S1...Sn , sodass der Ausdruck:

M<S1...Sn>

wird mit kompatibel (Delegatdeklarationen) D .


Im Gegensatz zum Typrückschluss-Algorithmus für generische Methodenaufrufe gibt es in diesem Fall nur
Argument Typen, keine Argument Ausdrücke. Insbesondere gibt es keine anonymen Funktionen und daher
keinen Bedarf an mehreren Phasen des Inferenz.
Stattdessen werden alle Xi als nicht korrigiert betrachtet, und es wird ein Rück Schluss aus jedem
Argumenttyp Uj von D auf den entsprechenden Parametertyp von erstellt Tj M . Wenn für eine beliebige
Grenze Xi keine Begrenzungen gefunden werden, schlägt der Typrückschluss fehl. Andernfalls werden alle Xi
entsprechend korrigiert Si , was das Ergebnis des Typrückschlusses ist.
Ermitteln des am häufigsten verbreiteten Typs eines Satzes von Ausdrücken
In einigen Fällen muss ein gemeinsamer Typ für einen Satz von Ausdrücken abgeleitet werden. Insbesondere
werden die Elementtypen von implizit typisierten Arrays und die Rückgabe Typen anonymer Funktionen mit
Block Text auf diese Weise gefunden.
Intuitiv, wenn ein Satz von Ausdrücken ist, E1...Em sollte dieser Rückschluss dem Aufrufen einer Methode
entsprechen.

Tr M<X>(X x1 ... X xm)

mit den Ei As-Argumenten.


Genauer ist, dass der Rückschluss mit einer Variablen vom Typ " unfixed " beginnt X . Die Ausschlüsse auf
Ausgabetyp werden dann von jedem Ei in vorgenommen X . Schließlich X ist korrigiert , und wenn der
Vorgang erfolgreich ist, ist der resultierende Typ S der resultierende am besten geeignete Typ für die
Ausdrücke. Wenn kein solcher S vorhanden ist, haben die Ausdrücke keinen besten allgemeinen Typ.
Überladungsauflösung
Bei der Überladungs Auflösung handelt es sich um einen Bindungs zeitmechanismus zum Auswählen des
besten Funktionsmembers, der beim Aufrufen einer Argumentliste und eines Satzes von Kandidaten Funktions
Membern aufgerufen wird. Die Überladungs Auflösung wählt den Funktions Member aus, der in den folgenden
unterschiedlichen Kontexten in c# aufgerufen werden soll:
Aufruf einer Methode mit dem Namen in einem invocation_expression (Methodenaufrufe).
Aufruf eines Instanzkonstruktors, der in einem object_creation_expression namens (Objekt Erstellungs
Ausdrücke) genannt wird.
Aufruf eines Indexeraccessors über einen element_access (Element Zugriff).
Aufruf eines vordefinierten oder benutzerdefinierten Operators, auf den in einem Ausdruck verwiesen wird
(unäre Operator Überladungs Auflösung und binäre Operator Überladungs Auflösung).
Jeder dieser Kontexte definiert den Satz von Kandidaten Funktionsmembern und die Liste der Argumente in der
eigenen, eindeutigen Weise, wie in den oben aufgeführten Abschnitten ausführlich beschrieben. Beispielsweise
enthält der Satz von Kandidaten für einen Methodenaufruf keine Methoden, die als override (Member Suche)
markiert sind, und Methoden in einer Basisklasse sind keine Kandidaten, wenn eine Methode in einer
abgeleiteten Klasse anwendbar ist (MethodenAufrufe).
Nachdem die Kandidaten Funktions Member und die Argumentliste identifiziert wurden, ist die Auswahl des
besten Funktionsmembers in allen Fällen identisch:
In Anbetracht der Menge der anwendbaren Kandidaten Funktionsmember befindet sich das beste
Funktionsmember in dieser Gruppe. Wenn die Menge nur ein Funktionsmember enthält, ist dieses
Funktionsmember das beste Funktionsmember. Andernfalls ist das beste Funktionsmember der ein
Funktionsmember, der besser als alle anderen Funktionsmember in Bezug auf die angegebene Argumentliste
ist, vorausgesetzt, dass jeder Funktionsmember mit allen anderen Funktionsmembern verglichen wird, wobei
die Regeln in einem besseren Funktionsmemberverwendet werden. Wenn nicht genau ein Funktionsmember
vorhanden ist, der besser als alle anderen Funktionsmember ist, dann ist der Funktionselement Aufruf
mehrdeutig, und ein Bindungs Fehler tritt auf.
In den folgenden Abschnitten wird die genaue Bedeutung der Begriffe *anwendbarer Funktionsmember _
und _ besseres Funktionsmember * definiert.
Anwendbares Funktionsmember
Ein Funktionsmember wird als anwendbares Funktionsmember in Bezug auf eine Argumentliste bezeichnet,
A Wenn alle folgenden Punkte zutreffen:

Jedes Argument in A entspricht einem Parameter in der Deklaration des Funktions Members, wie in den
entsprechenden Parameternbeschrieben, und jeder Parameter, dem kein Argument entspricht, ist ein
optionaler Parameter.
Für jedes Argument in A ist der Parameter Übergabe Modus des Arguments (d. h. Value, ref oder out )
identisch mit dem Parameter Übergabe Modus des entsprechenden Parameters.
für einen value-Parameter oder ein Parameter Array ist eine implizite Konvertierung (implizite
Konvertierungen) vom-Argument in den Typ des entsprechenden Parameters vorhanden, oder
bei einem- ref oder- out Parameter ist der Typ des Arguments mit dem Typ des entsprechenden
Parameters identisch. Schließlich ref out ist der-Parameter oder der-Parameter ein Alias für das
übergebenen Argument.
Bei einem Funktionsmember, der ein Parameter Array enthält, gilt Folgendes: Wenn das Funktionsmember
durch die oben genannten Regeln anwendbar ist, wird es als anwendbar in der *normalen Form _ bezeichnet.
Wenn ein Funktionsmember, der ein Parameter Array enthält, nicht in der normalen Form anwendbar ist, kann
der Funktionsmember stattdessen in seiner Form " erweitert" ( *) angewendet werden:
Das erweiterte Formular wird erstellt, indem das Parameter Array in der Deklaration des Funktions Members
durch Null oder mehr Wert Parameter des Elementtyps des Parameter Arrays ersetzt wird, sodass die Anzahl
der Argumente in der Argumentliste mit A der Gesamtanzahl der Parameter übereinstimmt. Wenn A
weniger Argumente aufweist als die Anzahl fester Parameter in der Deklaration des Funktions Members,
kann die erweiterte Form des Funktionsmembers nicht erstellt werden und ist daher nicht anwendbar.
Andernfalls ist das erweiterte Formular anwendbar, wenn für jedes Argument im A Parameter Übergabe
Modus des Arguments mit dem Parameter Übergabe Modus des entsprechenden Parameters identisch ist,
und
bei einem Parameter mit festem Wert oder einem Wert Parameter, der durch die Erweiterung erstellt
wurde, ist eine implizite Konvertierung (implizite Konvertierungen) vom Typ des Arguments in den
Typ des entsprechenden Parameters vorhanden.
bei einem- ref oder- out Parameter ist der Typ des Arguments mit dem Typ des entsprechenden
Parameters identisch.
Besseres Funktionsmember
Zum Ermitteln des besseren Funktionsmembers wird eine Liste der aus dem ausgebauten Argument
enthaltenen Elemente erstellt, die nur die Argument Ausdrücke selbst in der Reihenfolge enthält, in der Sie in der
ursprünglichen Argumentliste angezeigt werden.
Parameter Listen für jeden der Kandidaten Funktionsmember werden wie folgt erstellt:
Das erweiterte Formular wird verwendet, wenn das Funktionsmember nur in der erweiterten Form
anwendbar ist.
Optionale Parameter ohne entsprechende Argumente werden aus der Parameterliste entfernt.
Die Parameter werden neu angeordnet, sodass Sie an derselben Position wie das entsprechende Argument in
der Argumentliste auftreten.
Wenn eine Argumentliste A mit einem Satz von Argument Ausdrücken {E1, E2, ..., En} und zwei
anwendbaren Funktionsmembern Mp und Mq mit Parametertypen {P1, P2, ..., Pn} und {Q1, Q2, ..., Qn}
Mp definiert ist, wird ein besseres Funktionsmember definiert als Mq if

die implizite Konvertierung von in ist für jedes Ex Argument Qx nicht besser geeignet als bei der impliziten
Konvertierung von Ex in Px .
für mindestens ein Argument ist die Konvertierung von Ex in Px besser als die Konvertierung von Ex in
Qx .

Wenn Sie diese Auswertung durchführen, wenn Mp oder Mq in der erweiterten Form anwendbar ist Px , Qx
verweist oder auf einen Parameter in der erweiterten Form der Parameterliste.
Für den Fall, dass die Parametertyp Sequenzen {P1, P2, ..., Pn} und {Q1, Q2, ..., Qn} äquivalent sind (d. h.
Pi , jede verfügt über eine Identitäts Konvertierung in die entsprechende Qi ), werden die folgenden Regeln
zum Durchbrechen von Regeln angewendet, um den besseren Funktionsmember zu ermitteln.
Wenn Mp eine nicht generische Methode ist und Mq eine generische Methode ist, dann Mp ist besser als
Mq .
Andernfalls ist, wenn Mp in der normalen Form anwendbar und Mq ein params -Array hat und nur in der
erweiterten Form anwendbar ist, Mp besser als Mq .
Andernfalls ist, wenn Mp mehr deklarierte Parameter als aufweist Mq , Mp besser als Mq . Dies kann
vorkommen, wenn beide Methoden über params Arrays verfügen und nur in ihren erweiterten Formularen
anwendbar sind.
Andernfalls, wenn alle Parameter von Mp ein entsprechendes Argument aufweisen, während
Standardargumente mindestens einen optionalen Parameter in ersetzen müssen, Mq Mp ist besser als Mq .
Wenn andernfalls Mp spezifischere Parametertypen als aufweist Mq , dann Mp ist besser als Mq .
{R1, R2, ..., Rn} Stellen Sie {S1, S2, ..., Sn} die nicht instanziierten und nicht erweiterten
Parametertypen von Mp und dar Mq . Mp die Parametertypen sind spezifischer als, Mq Wenn für jeden
Parameter Rx nicht weniger spezifisch als ist, Sx und für mindestens einen Parameter Rx eher spezifischer
als Sx :
Ein Typparameter ist weniger spezifisch als ein nicht-Typparameter.
Ein konstruierter Typ ist rekursiv spezifischer als ein anderer konstruierter Typ (mit der gleichen
Anzahl von Typargumenten), wenn mindestens ein Typargument spezifischer ist und kein
Typargument weniger spezifisch als das entsprechende Typargument in der anderen ist.
Ein Arraytyp ist spezifischer als ein anderer Arraytyp (mit der gleichen Anzahl von Dimensionen),
wenn der Elementtyp des ersten spezifischeren ist als der Elementtyp der zweiten.
Wenn ein Member ein nicht gesperrter Operator und der andere ein gesperrter Operator ist, ist der nicht--
gesteigerte-Operator besser.
Andernfalls ist keines der Funktionsmember besser.
Bessere Konvertierung von Ausdrücken
Wenn eine implizite Konvertierung, C1 die von einem Ausdruck E in einen-Typ konvertiert T1 wird, und eine
implizite Konvertierung, C2 die von einem Ausdruck E in einen-Typ konvertiert wird T2 , C1 eine bessere
Konver tierung ist, als C2 Wenn E nicht genau übereinstimmt T2 und mindestens einer der folgenden
Punkte enthält:
E genau Übereinstimmungen T1 (exakt übereinstimmenden Ausdruck)
T1 ist ein besseres Konvertierungs Ziel als T2 (besseres Konvertierungs Ziel)

Exakt übereinstimmender Ausdruck


Bei einem Ausdruck E und einem Typ T E stimmt genau überein, T Wenn eine der folgenden Punkte
Folgendes enthält:
E weist einen Typ S auf, und eine Identitäts Konvertierung ist von S zu T
E ist eine anonyme Funktion, T ist entweder ein Delegattyp D oder ein Ausdrucks Strukturtyp,
Expression<D> und einer der folgenden enthält Folgendes:
Für wird ein abherüberder Rückgabetyp X E im Kontext der Parameterliste D (abgeleitet
Rückgabetyp) und eine Identitäts Konvertierung von in X den Rückgabetyp D
Entweder E ist nicht Async und D hat einen Rückgabetyp oder ist asynchron Y E und D verfügt
über einen Rückgabetyp Task<Y> , und eine der folgenden Punkte enthält:
Der Text von E ist ein Ausdruck, der genau übereinstimmt. Y
Der Text von E ist ein Anweisungsblock, in dem jede Return-Anweisung einen Ausdruck
zurückgibt, der genau übereinstimmt Y
Besseres Konvertierungs Ziel
Bei zwei verschiedenen Typen T1 und T2 T1 ist ein besseres Konvertierungs Ziel als, T2 Wenn keine
implizite Konvertierung von T2 in vorhanden ist T1 , und mindestens eine der folgenden Werte enthält:
Eine implizite Konvertierung von T1 in ist T2 vorhanden.
T1 ist entweder ein Delegattyp D1 oder ein Ausdrucks bauentyp Expression<D1> , T2 ist entweder ein
Delegattyp D2 oder ein Ausdrucks bauentyp Expression<D2> , D1 hat einen Rückgabetyp S1 und eine der
folgenden Punkte:
D2 ist void-Rückgabe
D2 hat einen Rückgabetyp S2 und S1 ist ein besseres Konvertierungs Ziel als S2
T1 ist Task<S1> , T2 ist Task<S2> , und S1 ist ein besseres Konvertierungs Ziel als S2
T1 ist S1 oder, wobei ein ganzzahliger Typ mit Vorzeichen ist S1? S1 , und T2 ist oder, wenn ein
ganzzahliger S2 S2? S2 Typ ohne Vorzeichen ist. Dies betrifft insbesondere:
S1 ist sbyte S2 , und ist byte , ushort , uint oder ulong
S1 ist short S2 , und ist ushort , uint oder. ulong
S1 ist int S2 , und ist uint , oder ulong
S1 ist, long und S2 ist ulong

Überladen in generischen Klassen


Obwohl Signaturen wie deklariert eindeutig sein müssen, ist es möglich, dass die Ersetzung von Typargumenten
zu identischen Signaturen führt. In solchen Fällen werden die oben genannten Regeln der Überladungs
Auflösung über den spezifischsten Member ausgewählt.
Die folgenden Beispiele zeigen über Ladungen, die gemäß dieser Regel gültig und ungültig sind:
interface I1<T> {...}

interface I2<T> {...}

class G1<U>
{
int F1(U u); // Overload resolution for G<int>.F1
int F1(int i); // will pick non-generic

void F2(I1<U> a); // Valid overload


void F2(I2<U> a);
}

class G2<U,V>
{
void F3(U u, V v); // Valid, but overload resolution for
void F3(V v, U u); // G2<int,int>.F3 will fail

void F4(U u, I1<V> v); // Valid, but overload resolution for


void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail

void F5(U u1, I1<V> v2); // Valid overload


void F5(V v1, U u2);

void F6(ref U u); // valid overload


void F6(out V v);
}

Kompilierzeit Überprüfung der Auflösung dynamischer Überladungen


Bei den meisten dynamisch gebundenen Vorgängen ist der Satz möglicher Kandidaten für die Auflösung zur
Kompilierzeit nicht bekannt. In bestimmten Fällen ist der Kandidaten Satz jedoch zum Zeitpunkt der
Kompilierung bekannt:
Statische Methodenaufrufe mit dynamischen Argumenten
Instanzmethodenaufrufe, bei denen der Empfänger kein dynamischer Ausdruck ist
Indexer-Aufrufe, bei denen der Empfänger kein dynamischer Ausdruck ist
Konstruktoraufrufe mit dynamischen Argumenten
In diesen Fällen wird für jeden Kandidaten eine beschränkte Kompilierzeit Überprüfung durchgeführt, um
festzustellen, ob eine dieser Vorgänge möglicherweise zur Laufzeit angewendet werden könnte. Diese
Überprüfung umfasst die folgenden Schritte:
Partieller Typrückschluss: alle Typargumente, die nicht direkt oder indirekt von einem Argument des Typs
abhängen, werden dynamic mithilfe der Regeln des Typrückschlussesabgeleitet. Die übrigen Typargumente
sind unbekannt.
Partielle Anwendbarkeit der Anwendbarkeit: die Anwendbarkeit wird gemäß dem anwendbaren
Funktionsmemberüberprüft, wobei Parameter, deren Typen unbekannt sind, ignoriert werden.
Wenn kein Kandidat diesen Test durchläuft, tritt ein Kompilierzeitfehler auf.
Funktionselement Aufruf
In diesem Abschnitt wird der Prozess beschrieben, der zur Laufzeit ausgeführt wird, um einen bestimmten
Funktionsmember aufzurufen. Es wird davon ausgegangen, dass ein Bindungs Zeit Prozess bereits den
aufzurufenden Member ermittelt hat, möglicherweise durch Anwenden der Überladungs Auflösung auf einen
Satz von Kandidaten Funktionsmembern.
Um den Aufrufprozess zu beschreiben, werden Funktionsmember in zwei Kategorien unterteilt:
Statische Funktionsmember. Dabei handelt es sich um Instanzkonstruktoren, statische Methoden, statische
Eigenschaftenaccessoren und benutzerdefinierte Operatoren. Statische Funktionsmember sind immer nicht
virtuell.
Instanzfunktionsmember. Dabei handelt es sich um Instanzmethoden, Accessoren für Instanzeigenschaften
und Indexer-Accessoren. Instanzfunktionsmember sind entweder nicht virtuell oder virtuell und werden
immer für eine bestimmte Instanz aufgerufen. Die Instanz wird durch einen Instanzausdruck berechnet und
kann innerhalb des Funktionsmembers als this (dieser Zugriff) aufgerufen werden.
Die Lauf Zeit Verarbeitung eines Funktionsmember-aufzurufenden besteht aus den folgenden Schritten, wobei
M der Funktionsmember ist und, wenn M ein Instanzmember ist, E der Instanzausdruck ist:

Wenn M ein statischer Funktionsmember ist:


Die Argumentliste wird entsprechend der Beschreibung in den Argumentlistenausgewertet.
M wird aufgerufen.
Wenn M ein Instanzfunktionsmember ist, der in einem value_type deklariert ist:
Ewird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren
Schritte ausgeführt.
Wenn E nicht als Variable klassifiziert ist, wird eine temporäre lokale Variable vom E Typ erstellt
und der Wert von der E Variablen zugewiesen. E wird dann als Verweis auf diese temporäre lokale
Variable neu klassifiziert. Der Zugriff auf die temporäre Variable ist als this innerhalb M möglich,
aber nicht auf andere Weise. Folglich kann der Aufrufer nur dann, wenn E eine echte Variable ist, die
Änderungen beobachten, die M an vornimmt this .
Die Argumentliste wird entsprechend der Beschreibung in den Argumentlistenausgewertet.
M wird aufgerufen. Die Variable, auf die verweist, E wird zur Variablen, auf die verweist this .
Wenn M ein Instanzfunktionsmember ist, der in einem reference_type deklariert ist:
E wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren
Schritte ausgeführt.
Die Argumentliste wird entsprechend der Beschreibung in den Argumentlistenausgewertet.
Wenn der Typ von E ein value_type ist, wird eine Boxing-Konvertierung (Boxing-Konvertierungen)
ausgeführt, um E Sie in den-Typ zu konvertieren object , und E wird object in den folgenden
Schritten als Typ betrachtet. In diesem Fall M kann nur ein Member von sein System.Object .
Der Wert von E wird als gültig geprüft. Wenn der Wert von E ist null , wird eine ausgelöst,
System.NullReferenceException und es werden keine weiteren Schritte ausgeführt.
Die aufzurufende Funktionsmember-Implementierung wird bestimmt:
Wenn der Bindungstyp von E eine Schnittstelle ist, ist das aufzurufende Funktionsmember die
Implementierung von, die M von dem Lauf Zeittyp der Instanz bereitgestellt wird, auf die von
verwiesen wird E . Dieses Funktionsmember wird bestimmt, indem die Schnittstellen
Zuordnungs Regeln (Schnittstellen Zuordnung) angewendet werden, um die Implementierung
von zu bestimmen, die M durch den Lauf Zeittyp der Instanz bereitgestellt wird, auf die
verweist E .
Andernfalls, wenn M ein virtuelles Funktionsmember ist, ist das aufzurufende
Funktionsmember die Implementierung von, die M vom Lauf Zeittyp der Instanz bereitgestellt
wird, auf die von verwiesen wird E . Dieses Funktionsmember wird festgelegt, indem die
Regeln zum Bestimmen der am weitesten abgeleiteten Implementierung (virtuelle Methoden)
von M in Bezug auf den Lauf Zeittyp der Instanz, auf die von verwiesen wird, angewendet
werden E .
Andernfalls M ist ein nicht virtuelles Funktionsmember, und der aufzurufende
Funktionsmember ist M selbst.
Die im obigen Schritt festgelegte Funktionsmember-Implementierung wird aufgerufen. Das Objekt,
auf das verweist, E wird das Objekt, auf das verweist this .
Aufrufe für geboxte Instanzen
Ein Funktionsmember, der in einem value_type implementiert ist, kann über eine geachtelte Instanz von
aufgerufen werden, die in den folgenden Situationen value_type :
Wenn das Funktionsmember eine einer Methode ist, die override vom-Typ geerbt wurde, object und wird
durch einen Instanzausdruck vom Typ aufgerufen object .
Wenn das Funktionsmember eine Implementierung eines Schnittstellenfunktionsmembers ist und durch
einen Instanzausdruck eines INTERFACE_TYPE aufgerufen wird.
Wenn der Funktionsmember durch einen Delegaten aufgerufen wird.
In diesen Fällen wird die geschachtelte Instanz als eine Variable des value_type enthalten, und diese Variable
wird zur Variablen, auf die this innerhalb des Funktionselement aufzurufenden verwiesen wird. Dies bedeutet
insbesondere, dass beim Aufrufen eines Funktionsmembers für eine geachtelte Instanz der Funktionsmember
den in der geboxten Instanz enthaltenen Wert ändern kann.

Primäre Ausdrücke
Primäre Ausdrücke enthalten die einfachsten Formen von Ausdrücken.

primary_expression
: primary_no_array_creation_expression
| array_creation_expression
;

primary_no_array_creation_expression
: literal
| interpolated_string_expression
| simple_name
| parenthesized_expression
| member_access
| invocation_expression
| element_access
| this_access
| base_access
| post_increment_expression
| post_decrement_expression
| object_creation_expression
| delegate_creation_expression
| anonymous_object_creation_expression
| typeof_expression
| checked_expression
| unchecked_expression
| default_value_expression
| nameof_expression
| anonymous_method_expression
| primary_no_array_creation_expression_unsafe
;

Primäre Ausdrücke werden zwischen array_creation_expression s und primary_no_array_creation_expression s


aufgeteilt. Wenn Sie Array-Creation-Expression auf diese Weise behandeln, anstatt sie zusammen mit den
anderen einfachen Ausdrucks Formularen aufzulisten, ermöglicht die Grammatik, potenziell verwirrenden Code,
wie z. b.

object o = new int[3][1];

der andernfalls interpretiert werden würde.


object o = (new int[3])[1];

Literale
Eine primary_expression , die aus einem Literalzeichen (Literale) besteht, wird als Wert klassifiziert.
Interpolierte Zeichenfolgen
Ein interpolated_string_expression besteht aus einem $ Vorzeichen, gefolgt von einem regulären oder
ausführlichen Zeichenfolgenliteral, bei dem die durch und getrennten Lücken, die { } Ausdrücke und
Formatierungs Spezifikationen einschließen. Ein interintererter Zeichen folgen Ausdruck ist das Ergebnis einer
interpolated_string_literal , die in einzelne Token unterteilt wurde, wie in interpoliert-
Zeichenfolgenliteralenbeschrieben.

interpolated_string_expression
: '$' interpolated_regular_string
| '$' interpolated_verbatim_string
;

interpolated_regular_string
: interpolated_regular_string_whole
| interpolated_regular_string_start interpolated_regular_string_body interpolated_regular_string_end
;

interpolated_regular_string_body
: interpolation (interpolated_regular_string_mid interpolation)*
;

interpolation
: expression
| expression ',' constant_expression
;

interpolated_verbatim_string
: interpolated_verbatim_string_whole
| interpolated_verbatim_string_start interpolated_verbatim_string_body interpolated_verbatim_string_end
;

interpolated_verbatim_string_body
: interpolation (interpolated_verbatim_string_mid interpolation)+
;

Der constant_expression in einer Interpolations muss über eine implizite Konvertierung in verfügen int .
Ein interpolated_string_expression wird als Wert klassifiziert. Wenn Sie sofort in System.IFormattable oder
System.FormattableString mit einer impliziten interkrelierten Zeichen folgen Konvertierung (implizite
interpoliert-Zeichen folgen Konvertierungen) konvertiert wird, hat der interpoliert Zeichen folgen Ausdruck
diesen Typ. Andernfalls weist Sie den-Typ auf string .
Wenn der Typ einer interinterpolierten Zeichenfolge System.IFormattable oder ist System.FormattableString , ist
die Bedeutung ein-Rückruf System.Runtime.CompilerServices.FormattableStringFactory.Create . Wenn der Typ ist
string , ist die Bedeutung des Ausdrucks ein-Rückruf string.Format . In beiden Fällen besteht die
Argumentliste des Aufrufes aus einem Format Zeichenfolgenliteralzeichen mit Platzhaltern für jede interpolung
und einem Argument für jeden Ausdruck, der den Platzhaltern entspricht.
Der Format Zeichenfolgenliterale wird wie folgt erstellt, wobei N die Anzahl der Interpolationen in der
interpolated_string_expression ist:
Wenn ein interpolated_regular_string_whole oder eine interpolated_verbatim_string_whole auf das $
Vorzeichen folgt, ist das Format Zeichenfolgenliteralzeichen das Token.
Andernfalls besteht das Format Zeichenfolgenliterale aus folgendem:
Zuerst die interpolated_regular_string_start oder interpolated_verbatim_string_start
Dann für jede Zahl I von 0 zu N-1 :
Die Dezimal Darstellung von. I
Wenn die entsprechende Interpolations constant_expression ist, dann ein , (Komma), gefolgt
von der Dezimal Darstellung des Werts des constant_expression
Anschließend werden die interpolated_regular_string_mid, die interpolated_regular_string_end,
interpolated_verbatim_string_mid oder interpolated_verbatim_string_end unmittelbar auf die
entsprechende interpolung folgt.
Die nachfolgenden Argumente sind einfach die Ausdrücke aus den Interpolationen (sofern vorhanden), in der
richtigen Reihenfolge.
TODO: Beispiele.
Einfache Namen
Eine Simple_name besteht aus einem Bezeichner, optional gefolgt von einer Typargument Liste:

simple_name
: identifier type_argument_list?
;

Ein Simple_name weist entweder das Formular I oder das Format auf I<A1,...,Ak> , wobei I ein einzelner
Bezeichner und <A1,...,Ak> ein optionaler type_argument_list ist. Wenn keine type_argument_list angegeben
ist, sollten K Sie den Wert 0 (null) angeben. Die Simple_name wird wie folgt ausgewertet und klassifiziert:
Wenn K 0 (null) ist und die Simple_name in einem- Block angezeigt wird, und wenn der Block(oder ein
einschließender Block) der Deklaration der lokalen Variablen Deklaration (Deklarationen) eine lokale
Variable, einen Parameter oder eine Konstante mit Namen enthält I , verweist der Simple_name auf
diese lokale Variable, den Parameter oder die Konstante und wird als Variable oder Wert klassifiziert.
Wenn K 0 (null) ist und die Simple_name im Text einer generischen Methoden Deklaration angezeigt
wird und diese Deklaration einen Typparameter mit dem Namen enthält I , verweist der Simple_name
auf diesen Typparameter.
Andernfalls für jeden Instanztyp T (Instanztyp), beginnend mit dem Instanztyp der unmittelbar
einschließenden Typdeklaration und fortsetzen mit dem Instanztyp jeder einschließenden Klasse oder
Struktur Deklaration (sofern vorhanden):
Wenn K 0 (null) ist und die Deklaration von T einen Typparameter mit dem Namen enthält I ,
verweist der Simple_name auf diesen Typparameter.
Andernfalls, wenn eine Member-Suche (Member-Suche) von I in T mit K Typargumenten eine
Entsprechung erzeugt:
Wenn T der Instanztyp der unmittelbar einschließenden Klasse oder des Struktur Typs ist und
die Suche eine oder mehrere Methoden identifiziert, ist das Ergebnis eine Methoden Gruppe
mit einem zugeordneten Instanzausdruck von this . Wenn eine Typargument Liste angegeben
wurde, wird Sie beim Aufrufen einer generischen Methode (Methodenaufrufe) verwendet.
Andernfalls, wenn der T Instanztyp der unmittelbar einschließenden Klasse oder des Struktur
Typs ist, wenn die Suche einen Instanzmember identifiziert und der Verweis innerhalb des Texts
eines Instanzkonstruktors, einer Instanzmethode oder eines Instanzaccessors auftritt, ist das
Ergebnis das gleiche wie ein Element Zugriff (Member Access) des Formulars this.I . Dies
kann nur vorkommen, wenn K 0 (null) ist.
Andernfalls ist das Ergebnis das gleiche wie ein Element Zugriff (Member Access) des
Formulars T.I oder T.I<A1,...,Ak> . In diesem Fall handelt es sich um einen Bindungs Zeit
Fehler, damit der Simple_name auf einen Instanzmember verweist.
Andernfalls N werden die folgenden Schritte für jeden Namespace, beginnend mit dem Namespace, in
dem der Simple_name auftritt, mit jedem einschließenden Namespace (sofern vorhanden) und mit dem
globalen Namespace fortgesetzt, bis eine Entität gefunden wird:
Wenn K 0 (null) ist und I der Name eines Namespace in ist N , dann gilt Folgendes:
Wenn der Speicherort, an dem der Simple_name auftritt, von einer Namespace Deklaration für
eingeschlossen ist N und die Namespace Deklaration eine extern_alias_directive oder
using_alias_directive enthält, die den Namen I einem Namespace oder Typ zuordnet, ist die
Simple_name mehrdeutig, und es tritt ein Kompilierungsfehler auf.
Andernfalls verweist der Simple_name auf den Namespace mit dem Namen I in N .
Wenn andernfalls einen zugreif baren N Typ mit den Parametern "Name" und "Type" enthält I K ,
dann:
Wenn K 0 (null) ist und die Position, an der die Simple_name auftritt, von einer Namespace
Deklaration für eingeschlossen wird N und die Namespace Deklaration eine
extern_alias_directive oder using_alias_directive enthält, die den Namen I einem Namespace
oder Typ zuordnet, ist die Simple_name mehrdeutig und ein Kompilierzeitfehler aufgetreten.
Andernfalls verweist der namespace_or_type_name auf den Typ, der mit den angegebenen
Typargumenten erstellt wurde.
Andernfalls ist der Speicherort, an dem der Simple_name auftritt, von einer Namespace Deklaration
für Folgendes eingeschlossen N :
Wenn K 0 (null) ist und die-Namespace Deklaration eine extern_alias_directive oder
using_alias_directive enthält, die den Namen I einem importierten Namespace oder Typ
zuordnet, verweist der Simple_name auf diesen Namespace oder Typ.
Wenn die Namespaces und Typdeklarationen, die von den using_namespace_directive s und
using_static_directive s der Namespace Deklaration importiert werden, genau einen zugreif
baren Typ oder einen statischen Member ohne Erweiterung haben, der über die Parameter
"Name" und "Type" verfügt I K , verweist der Simple_name auf diesen Typ oder Member,
der mit den angegebenen Typargumenten
Wenn die Namespaces und Typen, die durch die using_namespace_directive s der Namespace
Deklaration importiert werden, mehr als einen zugreif baren Typ oder einen statischen Member
ohne Erweiterungsmethode haben, der über die Parameter "Name" und "Type" verfügt I K
, ist die Simple_name mehrdeutig und ein Fehler.
Beachten Sie, dass der gesamte Schritt genau mit dem entsprechenden Schritt bei der Verarbeitung eines
namespace_or_type_name (Namespace-und Typnamen) identisch ist.
Andernfalls ist der Simple_name nicht definiert, und es tritt ein Kompilierzeitfehler auf.
Ausdrücke in Klammern
Ein parenthesized_expression besteht aus einem Ausdruck , der in Klammern eingeschlossen ist.

parenthesized_expression
: '(' expression ')'
;

Eine parenthesized_expression wird ausgewertet, indem der Ausdruck innerhalb der Klammern ausgewertet
wird. Wenn der Ausdruck innerhalb der Klammern einen Namespace oder Typ angibt, tritt ein
Kompilierzeitfehler auf. Andernfalls ist das Ergebnis der parenthesized_expression das Ergebnis der Auswertung
des enthaltenen Ausdrucks.
Memberzugriff
Eine member_access besteht aus einem primary_expression, einem predefined_type oder einem
qualified_alias_member, gefolgt von einem " . "-Token, gefolgt von einem Bezeichner, optional gefolgt von
einem type_argument_list.

member_access
: primary_expression '.' identifier type_argument_list?
| predefined_type '.' identifier type_argument_list?
| qualified_alias_member '.' identifier
;

predefined_type
: 'bool' | 'byte' | 'char' | 'decimal' | 'double' | 'float' | 'int' | 'long'
| 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong' | 'ushort'
;

Die qualified_alias_member Produktion wird in Namespacealias-Qualifiziererndefiniert.


Ein member_access weist entweder das Formular E.I oder das Format auf E.I<A1, ..., Ak> , wobei E ein
primärer Ausdruck ist, I ein einzelner Bezeichner ist und <A1, ..., Ak> ein optionaler type_argument_list ist.
Wenn keine type_argument_list angegeben ist, sollten K Sie den Wert 0 (null) angeben.
Eine member_access mit einer primary_expression vom Typ dynamic ist dynamisch gebunden (dynamische
Bindung). In diesem Fall klassifiziert der Compiler den Member Access als Eigenschaften Zugriff vom Typ
dynamic . Die nachstehenden Regeln zum Bestimmen der Bedeutung der member_access werden dann zur
Laufzeit angewendet und verwenden den Lauf Zeittyp anstelle des Kompilierzeit Typs der primary_expression.
Wenn diese Lauf Zeit Klassifizierung zu einer Methoden Gruppe führt, muss der Element Zugriff die
primary_expression eines invocation_expression sein.
Die member_access wird wie folgt ausgewertet und klassifiziert:
Wenn K 0 (null) ist und E ein Namespace ist und E einen schsted Namespace mit dem Namen enthält
I , ist das Ergebnis dieser Namespace.
Wenn E ein Namespace ist und E einen zugänglichen Typ I mit den Parametern "Name" und "Type"
enthält K , ist das Ergebnis der Typ, der mit den angegebenen Typargumenten erstellt wurde.
Wenn E ein predefined_type oder ein primary_expression als Typ klassifiziert ist, wenn E kein
Typparameter ist, und wenn eine Member-Suche (Member-Suche) von I in E mit K Typparametern eine
Entsprechung erzeugt, E.I wird wie folgt ausgewertet und klassifiziert:
Wenn I einen Typ identifiziert, ist das Ergebnis der Typ, der mit den angegebenen Typargumenten
erstellt wurde.
Wenn I eine oder mehrere Methoden identifiziert, ist das Ergebnis eine Methoden Gruppe ohne
zugeordneten Instanzausdruck. Wenn eine Typargument Liste angegeben wurde, wird Sie beim
Aufrufen einer generischen Methode (Methodenaufrufe) verwendet.
Wenn I eine static Eigenschaft identifiziert, ist das Ergebnis ein Eigenschaften Zugriff ohne
zugeordneten Instanzausdruck.
Wenn I ein static Feld angibt:
Wenn das Feld den readonly Wert hat und der Verweis außerhalb des statischen Konstruktors
der Klasse oder Struktur auftritt, in der das Feld deklariert ist, dann ist das Ergebnis ein Wert,
nämlich der Wert des statischen Felds I in E .
Andernfalls ist das Ergebnis eine Variable, nämlich das statische Feld I in E .
Wenn I ein- static Ereignis identifiziert:
Wenn der Verweis innerhalb der Klasse oder Struktur auftritt, in der das Ereignis deklariert ist,
und das Ereignis ohne event_accessor_declarations (Ereignisse) deklariert wurde, E.I wird
genau so verarbeitet, als wäre es I ein statisches Feld.
Andernfalls ist das Ergebnis ein Ereignis Zugriff ohne zugeordneten Instanzausdruck.
Wenn I eine Konstante identifiziert, ist das Ergebnis ein Wert, nämlich der Wert dieser Konstante.
Wenn I einen Enumerationsmember identifiziert, ist das Ergebnis ein Wert, nämlich der Wert dieses
Enumerationsmembers.
Andernfalls E.I ist ein ungültiger Member-Verweis, und ein Kompilierzeitfehler tritt auf.
Wenn E ein Eigenschaften Zugriff, Indexer-Zugriff, Variable oder Wert ist, der Typ ist T , und eine Member-
Suche (Member-Suche) von I in T mit K Typargumenten eine Entsprechung erzeugt, E.I wird wie folgt
ausgewertet und klassifiziert:
Zuerst wird E der Wert der Eigenschaft oder des Indexerzugriffs abgerufen (Werte von Ausdrücken)
und E als Wert neu klassifiziert, wenn ein Eigenschafts-oder Indexer-Zugriff ist.
Wenn I eine oder mehrere Methoden identifiziert, ist das Ergebnis eine Methoden Gruppe mit
einem zugeordneten Instanzausdruck von E . Wenn eine Typargument Liste angegeben wurde, wird
Sie beim Aufrufen einer generischen Methode (Methodenaufrufe) verwendet.
Wenn I eine Instanzeigenschaft identifiziert,
Wenn E ist this , I eine automatisch implementierte Eigenschaft (automatisch
implementierte Eigenschaften) ohne Setter identifiziert, und der Verweis innerhalb eines
Instanzkonstruktors für einen Klassen-oder Strukturtyp ist T , dann ist das Ergebnis eine
Variable, d. h. das verborgene dahinter liegende Feld für die Auto-Eigenschaft, die I in der
Instanz von angegeben ist, die T von angegeben wird this .
Andernfalls ist das Ergebnis ein Eigenschaften Zugriff mit einem zugeordneten Instanzausdruck
von E .
Wenn T ein class_type ist und I ein Instanzfeld dieses class_type angibt:
Wenn der Wert von E ist null , wird eine ausgelöst System.NullReferenceException .
Andernfalls readonly ist das Ergebnis ein Wert, d. h. der Wert des Felds in dem Objekt, auf das
verwiesen wird, wenn das Feld den Wert hat und der Verweis außerhalb eines
Instanzkonstruktors der Klasse auftritt, in der das Feld deklariert ist I E .
Andernfalls ist das Ergebnis eine Variable, nämlich das Feld I in dem Objekt, auf das von
verwiesen wird E .
Wenn T ein struct_type ist und I ein Instanzfeld dieses struct_type angibt:
Wenn E ein Wert ist oder wenn das Feld ist readonly und der Verweis außerhalb eines
Instanzkonstruktors der Struktur auftritt, in der das Feld deklariert ist, dann ist das Ergebnis ein
Wert, d. h. der Wert des Felds I in der Struktur Instanz, die von angegeben wird E .
Andernfalls ist das Ergebnis eine Variable, nämlich das Feld I in der Struktur Instanz, das von
angegeben wird E .
Wenn I ein Instanzereignis angibt:
Wenn der Verweis innerhalb der Klasse oder Struktur auftritt, in der das Ereignis deklariert ist,
und das Ereignis ohne event_accessor_declarations (Ereignisse) deklariert wurde, und der
Verweis nicht als linke Seite eines or- += Operators auftritt -= , E.I wird genau so
verarbeitet, als wäre I ein Instanzfeld.
Andernfalls ist das Ergebnis ein Ereignis Zugriff mit einem zugeordneten Instanzausdruck von
E .
Andernfalls wird versucht, E.I als Erweiterungs Methodenaufruf (Erweiterungs Methodenaufrufe) zu
verarbeiten. Wenn dies nicht möglich E.I ist, ist ein ungültiger Element Verweis, und ein Bindungs Fehler
tritt auf.
Identische einfache Namen und Typnamen
Wenn ein Element Zugriff auf das Formular E.I ist, wenn E ein einzelner Bezeichner ist und die Bedeutung
von E als Simple_name (einfache Namen) eine Konstante, ein Feld, eine Eigenschaft, eine lokale Variable oder
ein Parameter mit demselben Typ wie die Bedeutung von E als TYPE_NAME (Namespace-und Typnamen) ist,
sind beide möglichen Bedeutungen von E zulässig. Die beiden möglichen Bedeutungen von E.I sind nie
mehrdeutig, da I E in beiden Fällen notwendigerweise ein Member des-Typs sein muss. Anders ausgedrückt:
die Regel gestattet einfach den Zugriff auf die statischen Member und die schsted Typen, E bei denen
andernfalls ein Kompilierzeitfehler aufgetreten ist. Beispiel:

struct Color
{
public static readonly Color White = new Color(...);
public static readonly Color Black = new Color(...);

public Color Complement() {...}


}

class A
{
public Color Color; // Field Color of type Color

void F() {
Color = Color.Black; // References Color.Black static member
Color = Color.Complement(); // Invokes Complement() on Color field
}

static void G() {


Color c = Color.White; // References Color.White static member
}
}

Grammatik Mehrdeutigkeiten
Die Produktion für Simple_name (einfache Namen) und member_access (Member Access) kann zu
Mehrdeutigkeiten in der Grammatik für Ausdrücke führen. Beispielsweise ist die-Anweisung:

F(G<A,B>(7));

kann als ein F -Befehl mit zwei Argumenten (und) interpretiert werden G < A B > (7) . Alternativ könnte Sie
auch als ein-Rückruf F mit einem Argument interpretiert werden, bei dem es sich um einen aufgerufenen
Vorgang einer generischen Methode G mit zwei Typargumenten und einem regulären Argument handelt.
Wenn eine Sequenz von Token (im Kontext) als Simple_name (einfache Namen), member_access (Member
Access) oder pointer_member_access (Zeiger Element Zugriff) mit einer type_argument_list (Typargumente)
analysiert werden kann, wird das Token, das unmittelbar auf das schließende Token folgt, > untersucht. Wenn
eine von

( ) ] } : ; , . ? == != | ^

Anschließend wird der type_argument_list als Teil der Simple_name, member_access oder
pointer_member_access aufbewahrt, und jede andere mögliche Analyse der Sequenz von Token wird verworfen.
Andernfalls wird der type_argument_list nicht als Teil der Simple_name, member_access oder
pointer_member_access betrachtet, auch wenn es keine andere Möglichkeit gibt, die Sequenz von Token zu
analysieren. Beachten Sie, dass diese Regeln beim Durchlaufen einer type_argument_list in einer
namespace_or_type_name (Namespace-und Typnamen) nicht angewendet werden. Die Anweisung

F(G<A,B>(7));

wird gemäß dieser Regel als ein-Rückruf F mit einem Argument interpretiert, bei dem es sich um einen
Aufrufe einer generischen Methode G mit zwei Typargumenten und einem regulären Argument handelt. Die
Anweisungen

F(G < A, B > 7);


F(G < A, B >> 7);

wird jeweils als ein-Rückruf F mit zwei Argumenten interpretiert. Die Anweisung

x = F < A > +y;

wird als ein kleiner-als-Operator, größer als-Operator und Unärer Plus-Operator interpretiert, als ob die-
Anweisung geschrieben worden wäre x = (F < A) > (+y) , anstatt als Simple_name mit einem
type_argument_list gefolgt von einem binären Plus-Operator. In der-Anweisung

x = y is C<T> + z;

die Token C<T> werden als namespace_or_type_name mit einem type_argument_list interpretiert.
Aufrufausdrücke
Ein invocation_expression wird verwendet, um eine Methode aufzurufen.

invocation_expression
: primary_expression '(' argument_list? ')'
;

Ein- invocation_expression ist dynamisch gebunden (dynamische Bindung), wenn mindestens einer der
folgenden Punkte enthält:
Der primary_expression weist den Kompilier Zeittyp auf dynamic .
Mindestens ein Argument der optionalen argument_list hat einen Kompilier Zeittyp, dynamic und die
primary_expression weist keinen Delegattyp auf.
In diesem Fall klassifiziert der Compiler die invocation_expression als Wert des Typs dynamic . Die
nachstehenden Regeln zum Bestimmen der Bedeutung der invocation_expression werden dann zur Laufzeit
angewendet. dabei wird der Lauf Zeittyp anstelle des Kompilier Zeittyps der primary_expression und Argumente
verwendet, die den Kompilier Zeittyp aufweisen dynamic . Wenn das primary_expression keinen Kompilier
Zeittyp hat dynamic , wird der Methodenaufruf einer begrenzten Kompilierungszeit Überprüfung unterzogen,
wie in der Kompilierzeit Überprüfung der dynamischen Überladungs Auflösungbeschrieben.
Der primary_expression eines invocation_expression muss eine Methoden Gruppe oder ein Wert eines
delegate_type sein. Wenn die primary_expression eine Methoden Gruppe ist, ist die invocation_expression ein
Methodenaufruf (Methodenaufrufe). Wenn die primary_expression ein Wert eines delegate_type ist, ist der
invocation_expression ein Delegataufruf (Delegataufrufe). Wenn die primary_expression weder eine Methoden
Gruppe noch der Wert eines delegate_type ist, tritt ein Bindungs Fehler auf.
Die optionalen argument_list (Argumentlisten) stellen Werte oder Variablen Verweise für die Parameter der
Methode bereit.
Das Ergebnis der Auswertung eines invocation_expression wird wie folgt klassifiziert:
Wenn die invocation_expression eine Methode oder einen Delegaten aufruft, die zurückgibt void , ist das
Ergebnis "Nothing". Ein Ausdruck, der als "Nothing" klassifiziert wird, ist nur im Kontext einer
statement_expression (Ausdrucks Anweisungen) oder als Text eines lambda_expression (Anonyme Funktions
Ausdrücke) zulässig. Andernfalls tritt ein Bindungs Zeitfehler auf.
Andernfalls ist das Ergebnis ein Wert des Typs, der von der-Methode oder dem-Delegaten zurückgegeben
wird.
Methodenaufrufe
Bei einem Methodenaufruf muss die primary_expression der invocation_expression eine Methoden Gruppe sein.
Die Methoden Gruppe identifiziert die eine Methode, die aufgerufen werden soll, oder den Satz überladener
Methoden, von denen eine bestimmte aufzurufende Methode ausgewählt werden soll. Im letzteren Fall basiert
die Bestimmung der aufzurufenden Methode auf dem Kontext, der von den Typen der Argumente im
argument_list bereitgestellt wird.
Die Bindungs Zeit Verarbeitung eines Methoden M(A) aufruens im Formular, wobei M eine Methoden Gruppe
(möglicherweise ein type_argument_list) und A ein optionaler argument_list ist, besteht aus den folgenden
Schritten:
Der Satz von Kandidaten Methoden für den Methodenaufruf wird erstellt. Für jede Methode, F die der
Methoden Gruppe zugeordnet ist M :
Wenn F nicht generisch ist, F ist ein Kandidat, wenn:
M hat keine Typargument Liste, und
F ist in Bezug auf A (anwendbares Funktionsmember) anwendbar.
Wenn F generisch ist und M keine Typargument Liste aufweist, F ist ein Kandidat für Folgendes:
Der Typrückschluss (Typrückschluss) ist erfolgreich, wobei eine Liste der Typargumente für den-
Befehl abgeleitet wird.
Nachdem die abzurufenden Typargumente die entsprechenden Methodentypparameter ersetzt
haben, erfüllen alle konstruierten Typen in der Parameterliste von F ihre Einschränkungen (die
dieEinschränkungenerfüllen), und die Parameterliste von F ist in Bezug auf A (anwendbares
Funktionsmember) anwendbar.
Wenn F generisch ist und M eine Typargument Liste enthält, F ist ein Kandidat, wenn Folgendes
gilt:
F verfügt über die gleiche Anzahl von Methodentypparametern, die in der Typargument Liste
angegeben wurden, und
Nachdem die Typargumente die entsprechenden Methodentypparameter ersetzt haben,
erfüllen alle konstruierten Typen in der Parameterliste von F ihre Einschränkungen (die
dieEinschränkungenerfüllen), und die Parameterliste von F ist in Bezug auf A (anwendbares
Funktionsmember) anwendbar.
Der Satz von Kandidaten Methoden wird so reduziert, dass er nur Methoden der am weitesten abgeleiteten
Typen enthält: für jede Methode C.F in der Menge, wobei C der Typ ist, in dem die Methode F deklariert
ist, werden alle Methoden, die in einem Basistyp von deklariert C sind, aus dem Satz entfernt. Wenn
außerdem C ein anderer Klassentyp als ist object , werden alle in einem Schnittstellentyp deklarierten
Methoden aus dem Satz entfernt. (Diese letztere Regel hat nur Auswirkungen, wenn die Methoden Gruppe
das Ergebnis einer Member-Suche nach einem Typparameter ist, der eine effektive Basisklasse als Object und
eine nicht leere effektive Schnittstellen Menge aufweist.)
Wenn der resultierende Satz von Kandidaten Methoden leer ist, wird die weitere Verarbeitung der folgenden
Schritte abgebrochen, und stattdessen wird versucht, den Aufruf als Erweiterungs Methodenaufruf
(Erweiterungs Methodenaufrufe) zu verarbeiten. Wenn dies fehlschlägt, gibt es keine anwendbaren
Methoden, und ein Bindungs Fehler tritt auf.
Die beste Methode des Satzes von Kandidaten Methoden wird mithilfe der Regeln zur Überladungs
Auflösung der Überladungs Auflösungidentifiziert. Wenn eine einzige optimale Methode nicht identifiziert
werden kann, ist der Methodenaufruf mehrdeutig, und es tritt ein Fehler bei der Bindung auf. Beim
Durchführen der Überladungs Auflösung werden die Parameter einer generischen Methode berücksichtigt,
nachdem die Typargumente (bereitgestellt oder abgeleitet) für die entsprechenden Methodentyp Parameter
ersetzt wurden.
Die endgültige Überprüfung der ausgewählten optimalen Methode wird ausgeführt:
Die-Methode wird im Kontext der-Methoden Gruppe überprüft: Wenn es sich bei der besten Methode
um eine statische Methode handelt, muss die Methoden Gruppe aus einer Simple_name oder einer
member_access durch einen Typ resultieren. Wenn die beste Methode eine Instanzmethode ist, muss
die Methoden Gruppe durch eine Simple_name, eine member_access durch eine Variable oder einen
Wert oder eine base_access resultieren. Wenn keine dieser Anforderungen erfüllt ist, tritt ein Fehler
bei der Bindung auf.
Wenn es sich bei der besten Methode um eine generische Methode handelt, werden die
Typargumente (bereitgestellt oder abgeleitet) mit den Einschränkungen (die sich durch dieErfüllung
der Einschränkungenin der generischen Methode) überprüfen Wenn ein Typargument die
entsprechenden Einschränkungen für den Typparameter nicht erfüllt, tritt ein Bindungs Zeitfehler auf.
Nachdem eine Methode mithilfe der obigen Schritte zur Bindungs Zeit ausgewählt und überprüft wurde, wird
der tatsächliche Lauf Zeit Aufruf gemäß den Regeln für den Funktionselement Aufruf verarbeitet, der unter
Kompilierzeit Überprüfung der dynamischen Überladungs Auflösungbeschrieben wird.
Die intuitiver Auswirkung der oben beschriebenen Auflösungs Regeln lautet wie folgt: um die von einem
Methodenaufruf aufgerufene Methode zu suchen, beginnen Sie mit dem Typ, der durch den Methodenaufruf
angegeben wird, und setzen Sie die Vererbungs Kette fort, bis mindestens eine anwendbare, barrierefreie, nicht
über schreibende Methoden Deklaration gefunden wurde. Führen Sie dann den Typrückschluss und die
Überladungs Auflösung für den Satz der anwendbaren, zugänglichen, nicht überschreibenden Methoden aus,
die in diesem Typ deklariert sind, und rufen Sie die Methode auf Wenn keine Methode gefunden wurde,
versuchen Sie stattdessen, den Aufruf als Erweiterungs Methodenaufruf zu verarbeiten.
Erweiterungs Methodenaufrufe
In einem Methodenaufruf (Aufrufe für geboxte Instanzen) eines der Formulare

expr . identifier ( )

expr . identifier ( args )

expr . identifier < typeargs > ( )

expr . identifier < typeargs > ( args )

Wenn bei der normalen Verarbeitung des aufzurufenden-aufzurufenden keine anwendbaren Methoden
gefunden werden, wird versucht, das Konstrukt als Erweiterungs Methodenaufruf zu verarbeiten. Wenn expr
oder eines der args den Kompilier Zeittyp aufweist dynamic , werden keine Erweiterungs Methoden
angewendet.
Ziel ist es, die beste TYPE_NAME zu finden C , damit der entsprechende Aufruf der statischen Methode
stattfinden kann:

C . identifier ( expr )

C . identifier ( expr , args )

C . identifier < typeargs > ( expr )

C . identifier < typeargs > ( expr , args )

Eine Erweiterungsmethode Ci.Mj ist berechtigt , wenn Folgendes gilt:


Ci ist eine nicht generische, nicht-Klassen-Klasse.
Der Name von Mj ist ein Bezeichner .
Mj ist verfügbar und anwendbar, wenn Sie auf die Argumente als statische Methode angewendet wird, wie
oben gezeigt.
Eine implizite Identitäts-, Verweis-oder Boxingkonvertierung ist von expr zum Typ des ersten Parameters von
vorhanden Mj .
Die Suche nach C verläuft wie folgt:
Beginnend mit der nächstgelegenen, einschließenden Namespace Deklaration, der Fortsetzung der
einschließenden Namespace Deklaration und dem Ende der enthaltenden Kompilierungseinheit werden
nachfolgende Versuche unternommen, um einen Kandidaten Satz von Erweiterungs Methoden zu finden:
Wenn der angegebene Namespace oder die Kompilierungseinheit direkt nicht generische
Typdeklarationen Ci mit berechtigten Erweiterungs Methoden enthält Mj , ist der Satz dieser
Erweiterungs Methoden der Kandidaten Satz.
Wenn Typen, die Ci von using_static_declarations importiert und direkt in Namespaces deklariert
werden, die von using_namespace_directive s in den angegebenen Namespace oder in der
Kompilierungseinheit importiert wurden, die geeigneten Erweiterungs Methoden direkt enthalten Mj
, ist der Satz dieser Erweiterungs Methoden der Kandidaten Satz.
Wenn kein Kandidaten Satz in einer einschließenden Namespace Deklaration oder Kompilierungseinheit
gefunden wird, tritt ein Kompilierzeitfehler auf.
Andernfalls wird die Überladungs Auflösung auf den Kandidaten Satz angewendet, wie in (Überladungs
Auflösung) beschrieben. Wenn keine einzige optimale Methode gefunden wird, tritt ein Kompilierzeitfehler
auf.
C der Typ, in dem die beste Methode als Erweiterungsmethode deklariert wird.

C Wenn als Ziel verwendet wird, wird der Methodenaufruf als statischer Methodenaufruf verarbeitet
(Kompilierzeit Überprüfung der dynamischen Überladungs Auflösung).
Die vorstehenden Regeln bedeuten, dass Instanzmethoden Vorrang vor Erweiterungs Methoden haben, dass die
in inneren Namespace Deklarationen verfügbaren Erweiterungs Methoden Vorrang vor Erweiterungs Methoden
haben, die in äußeren Namespace Deklarationen verfügbar sind, und dass Erweiterungs Methoden, die direkt in
einem Namespace deklariert werden, Vorrang vor Erweiterungs Methoden haben, die mit einer using-
namespace Direktive in denselben Namespace Beispiel:
public static class E
{
public static void F(this object obj, int i) { }

public static void F(this object obj, string s) { }


}

class A { }

class B
{
public void F(int i) { }
}

class C
{
public void F(object obj) { }
}

class X
{
static void Test(A a, B b, C c) {
a.F(1); // E.F(object, int)
a.F("hello"); // E.F(object, string)

b.F(1); // B.F(int)
b.F("hello"); // E.F(object, string)

c.F(1); // C.F(object)
c.F("hello"); // C.F(object)
}
}

In diesem Beispiel hat die B -Methode Vorrang vor der ersten Erweiterungsmethode, und C die-Methode hat
Vorrang vor beiden Erweiterungs Methoden.
public static class C
{
public static void F(this int i) { Console.WriteLine("C.F({0})", i); }
public static void G(this int i) { Console.WriteLine("C.G({0})", i); }
public static void H(this int i) { Console.WriteLine("C.H({0})", i); }
}

namespace N1
{
public static class D
{
public static void F(this int i) { Console.WriteLine("D.F({0})", i); }
public static void G(this int i) { Console.WriteLine("D.G({0})", i); }
}
}

namespace N2
{
using N1;

public static class E


{
public static void F(this int i) { Console.WriteLine("E.F({0})", i); }
}

class Test
{
static void Main(string[] args)
{
1.F();
2.G();
3.H();
}
}
}

Die Ausgabe dieses Beispiels lautet wie folgt:

E.F(1)
D.G(2)
C.H(3)

D.G``C.G hat Vorrang vor und hat Vorrang vor E.F D.F und C.F .
Delegataufrufe
Bei einem Delegataufruf muss die primary_expression der invocation_expression ein Wert eines delegate_type
sein. Außerdem muss der delegate_type in Bezug auf den argument_list der invocation_expression anwendbar
sein, wenn der delegate_type ein Funktionsmember mit derselben Parameterliste wie der delegate_type ist.
Die Lauf Zeit Verarbeitung eines delegataufruens im Formular D(A) , wobei D eine primary_expression eines
delegate_type und A ein optionaler argument_list ist, besteht aus den folgenden Schritten:
D wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren Schritte
ausgeführt.
Der Wert von D wird als gültig geprüft. Wenn der Wert von D ist null , wird eine ausgelöst,
System.NullReferenceException und es werden keine weiteren Schritte ausgeführt.
Andernfalls D ist ein Verweis auf eine Delegatinstanz. Funktionsmember-Aufrufe (Kompilierungszeit
Überprüfung der dynamischen Überladungs Auflösung) werden für jede der Aufruf baren Entitäten in der
Aufruf Liste des Delegaten ausgeführt. Bei Aufruf baren Entitäten, die aus einer Instanz-und Instanzmethode
bestehen, ist die Instanz für den Aufruf die Instanz, die in der Aufruf baren Entität enthalten ist.
Elementzugriff
Eine element_access besteht aus einem primary_no_array_creation_expression, gefolgt von einem " [ "-Token,
gefolgt von einem argument_list, gefolgt von einem " ] "-Token. Der argument_list besteht aus mindestens
einem Argument s, getrennt durch Kommas.

element_access
: primary_no_array_creation_expression '[' expression_list ']'
;

Der argument_list eines element_access darf keine- ref oder-Argumente enthalten out .
Ein- element_access ist dynamisch gebunden (dynamische Bindung), wenn mindestens einer der folgenden
Punkte enthält:
Der primary_no_array_creation_expression weist den Kompilier Zeittyp auf dynamic .
Mindestens ein Ausdruck des argument_list hat einen Kompilier Zeittyp, dynamic und die
primary_no_array_creation_expression weist keinen Arraytyp auf.
In diesem Fall klassifiziert der Compiler die element_access als Wert des Typs dynamic . Die nachstehenden
Regeln zum Bestimmen der Bedeutung der element_access werden dann zur Laufzeit angewendet. dabei wird
der Lauf Zeittyp anstelle des Kompilier Zeittyps der primary_no_array_creation_expression und argument_list
Ausdrücke verwendet, die den Kompilier Zeittyp aufweisen dynamic . Wenn das
primary_no_array_creation_expression keinen Kompilier Zeittyp hat dynamic , wird der Zugriff auf die
Kompilierzeit durch den Element Zugriff eingeschränkt, wie in der Kompilierzeit Überprüfung der dynamischen
Überladungs Auflösungbeschrieben.
Wenn die primary_no_array_creation_expression eines element_access ein Wert eines array_type ist, ist der
element_access ein Array Zugriff (Array Zugriff). Andernfalls muss es sich bei der
primary_no_array_creation_expression um eine Variable oder einen Wert einer Klasse, Struktur oder eines
Schnittstellen Typs handeln, die mindestens ein Indexer-Element aufweist. in diesem Fall ist der element_access
ein Indexer-Zugriff (Indexer-Zugriff).
Arrayzugriff
Bei einem Array Zugriff muss die primary_no_array_creation_expression der element_access ein Wert eines
array_type sein. Außerdem darf die argument_list eines Array Zugriffs keine benannten Argumente enthalten.
Die Anzahl der Ausdrücke in der argument_list muss mit dem Rang des array_type identisch sein, und jeder
Ausdruck muss den Typ int , uint ,, oder aufweisen, der long ulong implizit in einen oder mehrere dieser
Typen konvertiert werden muss.
Das Ergebnis der Auswertung eines Array Zugriffs ist eine Variable des Elementtyps des Arrays, d. h. das Array
Element, das von den Werten der Ausdrücke im argument_list ausgewählt wird.
Die Lauf Zeit Verarbeitung eines Array Zugriffs auf das Formular P[A] , wobei P eine
primary_no_array_creation_expression eines array_type und A eine argument_list ist, besteht aus den
folgenden Schritten:
P wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren Schritte
ausgeführt.
Die Index Ausdrücke der argument_list werden in der Reihenfolge von links nach rechts ausgewertet. Nach
der Auswertung der einzelnen Index Ausdrücke wird eine implizite Konvertierung (implizite
Konvertierungen) in einen der folgenden Typen ausgeführt: int , uint , long , ulong . Der erste Typ in
dieser Liste, für den eine implizite Konvertierung vorhanden ist, wird ausgewählt. Wenn der Index Ausdruck
beispielsweise vom Typ ist, short wird eine implizite Konvertierung in int durchgeführt, da implizite
Konvertierungen von short in int und von short in long möglich sind. Wenn die Auswertung eines
Index Ausdrucks oder der nachfolgenden impliziten Konvertierung eine Ausnahme verursacht, werden keine
weiteren Index Ausdrücke ausgewertet, und es werden keine weiteren Schritte ausgeführt.
Der Wert von P wird als gültig geprüft. Wenn der Wert von P ist null , wird eine ausgelöst,
System.NullReferenceException und es werden keine weiteren Schritte ausgeführt.
Der Wert jedes Ausdrucks im argument_list wird anhand der tatsächlichen Begrenzungen der einzelnen
Dimensionen der Array Instanz überprüft, auf die von verwiesen wird P . Wenn mindestens ein Wert
außerhalb des gültigen Bereichs liegt, wird eine ausgelöst, System.IndexOutOfRangeException und es werden
keine weiteren Schritte ausgeführt.
Der Speicherort des Array Elements, das durch den Index Ausdruck (n) angegeben wird, wird berechnet, und
dieser Speicherort wird zum Ergebnis des Array Zugriffs.
Indexerzugriff
Für einen Indexer-Zugriff muss die primary_no_array_creation_expression der element_access eine Variable
oder ein Wert einer Klasse, Struktur oder eines Schnittstellen Typs sein, und dieser Typ muss einen oder mehrere
Indexer implementieren, die in Bezug auf die argument_list der element_access anwendbar sind.
Die Bindungs Zeit Verarbeitung eines Indexer-Zugriffs auf das Formular P[A] , wobei P eine
primary_no_array_creation_expression einer Klasse, einer Struktur oder eines Schnittstellen Typs ist T und A
eine argument_list ist, besteht aus den folgenden Schritten:
Der von bereitgestellte Indexer-Satz T wird erstellt. Der Satz besteht aus allen indexermembern, die in
deklariert T werden, oder einem Basistyp von T , die keine override Deklarationen sind und auf die im
aktuellen Kontext zugegriffen werden kann (Member Access).
Der Satz wird auf die Indexer reduziert, die anwendbar sind und von anderen Indexer nicht ausgeblendet
werden. Die folgenden Regeln werden auf jeden Indexer S.I im Satz angewendet, wobei S der Typ ist, in
dem der Indexer I deklariert ist:
Wenn I nicht in Bezug auf A (anwendbares Funktionsmember) anwendbar ist, I wird aus dem
Satz entfernt.
Wenn in I Bezug auf A (anwendbares Funktionsmember) anwendbar ist, werden alle in einem
Basistyp von deklarierten Indexer S aus dem Satz entfernt.
Wenn in I Bezug auf A (anwendbares Funktionsmember) anwendbar ist und S ein anderer
Klassentyp als ist object , werden alle in einer Schnittstelle deklarierten Indexer aus dem Satz
entfernt.
Wenn der resultierende Satz von Kandidaten Indexer leer ist, sind keine anwendbaren Indexer vorhanden,
und es tritt ein Bindungs Zeitfehler auf.
Der beste Indexer der Gruppe von Kandidaten Indexern wird mithilfe der Regeln zur Überladungs Auflösung
der Überladungs Auflösungidentifiziert. Wenn ein einzelner optimaler Indexer nicht identifiziert werden
kann, ist der Indexerzugriff mehrdeutig, und es tritt ein Fehler bei der Bindung auf.
Die Index Ausdrücke der argument_list werden in der Reihenfolge von links nach rechts ausgewertet. Das
Ergebnis der Verarbeitung des Indexer-Zugriffs ist ein Ausdruck, der als Indexer-Zugriff klassifiziert ist. Der
indexerzugriffsausdruck verweist auf den Indexer, der im obigen Schritt festgelegt wurde, und verfügt über
einen zugeordneten Instanzausdruck P und eine zugeordnete Argumentliste von A .
Abhängig vom Kontext, in dem Sie verwendet werden, bewirkt ein Indexer-Zugriff, dass entweder der Get-
Accessor oder der Set-Accessor des Indexers aufgerufen wird. Wenn der Indexer-Zugriff das Ziel einer
Zuweisung ist, wird der Set-Accessor aufgerufen, um einen neuen Wert zuzuweisen (einfache Zuweisung). In
allen anderen Fällen wird der Get-Accessor aufgerufen, um den aktuellen Wert zu erhalten (Werte von
Ausdrücken).
Dieser Zugriff
Ein this_access besteht aus dem reservierten Wort this .
this_access
: 'this'
;

Eine this_access ist nur im- Block eines Instanzkonstruktors, einer Instanzmethode oder eines Instanzaccessors
zulässig. Dies hat eine der folgenden Bedeutungen:
Wenn this in einer primary_expression in einem Instanzkonstruktor einer Klasse verwendet wird, wird Sie
als Wert klassifiziert. Der Typ des Werts ist der Instanztyp (der Instanztyp) der Klasse, in der die Verwendung
erfolgt, und der Wert ist ein Verweis auf das Objekt, das erstellt wird.
Wenn this in einer primary_expression in einer Instanzmethode oder einem Instanzaccessor einer Klasse
verwendet wird, wird Sie als Wert klassifiziert. Der Typ des Werts ist der Instanztyp (der Instanztyp) der
Klasse, in der die Verwendung erfolgt, und der Wert ist ein Verweis auf das Objekt, für das die Methode oder
der Accessor aufgerufen wurde.
Wenn this in einem primary_expression innerhalb eines Instanzkonstruktors einer Struktur verwendet
wird, wird es als Variable klassifiziert. Der Typ der Variablen ist der Instanztyp (der Instanztyp) der Struktur, in
der die Verwendung erfolgt, und die Variable stellt die Struktur dar, die erstellt wird. Die this Variable eines
Instanzkonstruktors einer Struktur verhält sich genau wie ein out Parameter des Struktur Typs – Dies
bedeutet insbesondere, dass die Variable definitiv in jedem Ausführungs Pfad des Instanzkonstruktors
zugewiesen werden muss.
Wenn this in einer primary_expression in einer Instanzmethode oder einem Instanzaccessor einer Struktur
verwendet wird, wird Sie als Variable klassifiziert. Der Typ der Variablen ist der Instanztyp (der Instanztyp)
der Struktur, in der die Verwendung erfolgt.
Wenn die Methode oder der Accessor kein Iterator (Iteratoren) ist, this stellt die Variable die Struktur
dar, für die die Methode oder der Accessor aufgerufen wurde, und verhält sich genau wie ein ref
Parameter des Struktur Typs.
Wenn es sich bei der Methode oder dem Accessor um einen Iterator handelt, this stellt die Variable
eine Kopie der Struktur dar, für die die Methode oder der Accessor aufgerufen wurde, und verhält sich
genau wie ein Wert Parameter des Struktur Typs.
Die Verwendung von this in einem primary_expression in einem anderen als dem oben aufgeführten Kontext
ist ein Kompilierzeitfehler. Insbesondere ist es nicht möglich, auf this eine statische Methode, einen statischen
Eigenschafts Accessor oder eine variable_initializer einer Feld Deklaration zu verweisen.
Basis Zugriff
Ein base_access besteht aus dem reservierten Wort, base gefolgt von einem " . "-Token und einem-
Bezeichner oder einem in eckige Klammern eingeschlossenen argument_list :

base_access
: 'base' '.' identifier
| 'base' '[' expression_list ']'
;

Ein base_access wird für den Zugriff auf Basisklassenmember verwendet, die durch ähnlich benannte Member
in der aktuellen Klasse oder Struktur ausgeblendet werden. Eine base_access ist nur im- Block eines
Instanzkonstruktors, einer Instanzmethode oder eines Instanzaccessors zulässig. Wenn base.I in einer Klasse
oder Struktur vorkommt, I muss einen Member der Basisklasse dieser Klasse oder Struktur bezeichnen.
Ebenso muss bei base[E] Auftreten von in einer Klasse ein anwendbarer Indexer in der Basisklasse vorhanden
sein.
Bei der Bindungs Zeit werden base_access Ausdrücke des Formulars base.I und genauso base[E]
ausgewertet, als wären Sie geschrieben worden ((B)this).I ((B)this)[E] , wobei B die Basisklasse der
Klasse oder Struktur ist, in der das Konstrukt auftritt. Folglich base.I entsprechen und und base[E] this.I
this[E] , außer this als Instanz der Basisklasse.

Wenn eine base_access auf einen virtuellen Funktionsmember (eine Methode, eine Eigenschaft oder einen
Indexer) verweist, wird bestimmt, welcher Funktionsmember zur Laufzeit aufgerufen werden soll (über Prüfung
der Auflösung der dynamischen Überlastung). Der aufgerufene Funktionsmember wird ermittelt, indem die am
meisten abgeleitete Implementierung (virtuelle Methoden) des Funktionsmembers in Bezug auf B (anstelle des
Lauf Zeit Typs this , wie bei einem nicht-Basis-Zugriff üblich) gefunden wird. Daher override virtual kann
ein base_access in einem eines Funktionsmembers verwendet werden, um die geerbte Implementierung des
Funktionsmembers aufzurufen. Wenn das von einem base_access referenzierte Funktionsmember abstrakt ist,
tritt ein Fehler bei der Bindung auf.
Postfix-Inkrementoperator und Postfix-Dekrementoperator

post_increment_expression
: primary_expression '++'
;

post_decrement_expression
: primary_expression '--'
;

Der Operand eines Postfix-Inkrement-oder dekrementvorgangs muss ein Ausdruck sein, der als Variable,
Eigenschafts Zugriff oder Indexerzugriff klassifiziert ist. Das Ergebnis des Vorgangs ist ein Wert desselben Typs
wie der Operand.
Wenn das primary_expression den Kompilier Zeittyp aufweist dynamic , wird der Operator dynamisch
gebunden (dynamische Bindung), die post_increment_expression oder post_decrement_expression den
Kompilier Zeittyp, dynamic und die folgenden Regeln werden zur Laufzeit mithilfe des Lauf Zeit Typs der
primary_expression angewendet.
Wenn der Operand einer Postfix-Inkrement-oder Dekrementoperation eine Eigenschaft oder ein Indexer-Zugriff
ist, muss die Eigenschaft oder der Indexer sowohl einen get -als auch einen- set Accessor aufweisen. Wenn
dies nicht der Fall ist, tritt ein Bindungs Zeitfehler auf.
Unäre Operator Überladungs Auflösung (unäre Operator Überladungs Auflösung) wird angewendet, um eine
bestimmte Operator Implementierung auszuwählen. ++ -- Für die folgenden Typen sind vordefinierte-und-
Operatoren vorhanden: sbyte , byte , short , ushort , int , uint , long , ulong , char , float , double ,
decimal und ein beliebiger Enumerationstyp. Die vordefinierten ++ Operatoren geben den Wert zurück, der
beim Hinzufügen von 1 zum Operanden erzeugt wurde, und die vordefinierten -- Operatoren geben den Wert
zurück, der durch die Subtraktion von 1 vom Operanden erzeugt wird. Wenn in einem checked Kontext das
Ergebnis dieser Addition oder Subtraktion außerhalb des Bereichs des Ergebnis Typs liegt und der Ergebnistyp
ein ganzzahliger Typ oder Enumeration-Typ ist, System.OverflowException wird eine ausgelöst.
Die Lauf Zeit Verarbeitung einer Postfix-Inkrement-oder Dekrementoperation des Formulars x++ oder x--
besteht aus den folgenden Schritten:
Wenn als Variable klassifiziert ist:
x
x wird ausgewertet, um die Variable zu entwickeln.
Der Wert von x wird gespeichert.
Der ausgewählte Operator wird mit dem gespeicherten Wert von x als sein Argument aufgerufen.
Der vom-Operator zurückgegebene Wert wird an dem Speicherort gespeichert, der durch die
Auswertung von angegeben wird x .
Der gespeicherte Wert von x wird das Ergebnis des Vorgangs.
Wenn x als Eigenschaft oder Indexer-Zugriff klassifiziert ist:
Der Instanzausdruck (wenn x nicht ist static ), und die Argumentliste (wenn x ein Indexer-Zugriff
ist), die zugeordnet ist x , wird ausgewertet, und die Ergebnisse werden in den nachfolgenden get -
und-Accessoraufrufen verwendet set .
Der get -Accessor von x wird aufgerufen, und der zurückgegebene Wert wird gespeichert.
Der ausgewählte Operator wird mit dem gespeicherten Wert von x als sein Argument aufgerufen.
Der- set Accessor von x wird mit dem Wert aufgerufen, der vom Operator als sein Argument
zurückgegeben wurde value .
Der gespeicherte Wert von x wird das Ergebnis des Vorgangs.

Die ++ -- Operatoren und unterstützen auch Präfix Notation (Präfix Inkrement-und Dekrementoperatoren). In
der Regel ist das Ergebnis von x++ oder x-- der Wert von x vor dem Vorgang, wohingegen das Ergebnis
von ++x oder --x der Wert von x nach dem Vorgang ist. In jedem Fall x hat nach dem Vorgang denselben
Wert.
Eine- operator ++ oder- operator -- Implementierung kann entweder mithilfe der Postfix-oder Präfix Notation
aufgerufen werden. Es ist nicht möglich, separate Operator Implementierungen für die beiden Notationen zu
haben.
Der new-Operator
Der- new Operator wird verwendet, um neue Instanzen von-Typen zu erstellen.
Es gibt drei Formen von new Ausdrücken:
Objekt Erstellungs Ausdrücke werden verwendet, um neue Instanzen von Klassentypen und Werttypen zu
erstellen.
Array Erstellungs Ausdrücke werden verwendet, um neue Instanzen von Array Typen zu erstellen.
Delegaterstellungs-Ausdrücke werden verwendet, um neue Instanzen von Delegattypen zu erstellen.
Der new Operator impliziert die Erstellung einer Instanz eines Typs, impliziert jedoch nicht notwendigerweise
die dynamische Speicher Belegung. Vor allem sind für Instanzen von Werttypen außerhalb der Variablen, in
denen Sie sich befinden, kein zusätzlicher Arbeitsspeicher erforderlich. es treten keine dynamischen
Zuordnungen auf, wenn new zum Erstellen von Instanzen von Werttypen verwendet wird.
Objekt Erstellungs Ausdrücke
Eine object_creation_expression wird zum Erstellen einer neuen Instanz einer class_type oder einer value_type
verwendet.

object_creation_expression
: 'new' type '(' argument_list? ')' object_or_collection_initializer?
| 'new' type object_or_collection_initializer
;

object_or_collection_initializer
: object_initializer
| collection_initializer
;

Der Typ eines object_creation_expression muss ein class_type, ein value_type oder ein type_parameter sein. Der
Typ darf keine abstract class_type sein.
Die optionalen argument_list (Argumentlisten) sind nur zulässig, wenn es sich bei dem Typ um einen class_type
oder eine struct_type handelt.
Ein Objekt Erstellungs Ausdruck kann die Liste der Konstruktorargumente weglassen und einschließende
Klammern einschließen, wenn er einen Objektinitialisierer oder einen Auflistungsinitialisierer enthält. Das
Weglassen der Konstruktorargumentliste und das Einschließen von Klammern entspricht der Angabe einer
leeren Argumentliste.
Die Verarbeitung eines Ausdrucks zur Objekt Erstellung, der einen Objektinitialisierer oder
Auflistungsinitialisierer enthält, besteht aus der ersten Verarbeitung des Instanzkonstruktors und der
anschließenden Verarbeitung der Element-oder Element Initialisierungen, die vom Objektinitialisierer
(Objektinitialisierer) oder sammlungsinitialisierer (sammlungsinitialisierer
Wenn eines der Argumente im optionalen argument_list den Kompilier Zeittyp aufweist dynamic , wird der
object_creation_expression dynamisch gebunden (dynamische Bindung), und die folgenden Regeln werden zur
Laufzeit verwendet, wobei der Lauf Zeittyp der Argumente der argument_list , die den Kompilier Zeittyp
aufweisen, verwendet wird dynamic . Allerdings wird bei der Objekt Erstellung eine beschränkte
Kompilierungszeit Überprüfung durchgeführt, wie in der Kompilierzeit Überprüfung der dynamischen
Überladungs Auflösungbeschrieben.
Die Bindungs Zeit Verarbeitung einer object_creation_expression der Form new T(A) , wobei T ein class_type
oder ein value_type ist und A ein optionaler argument_list ist, besteht aus den folgenden Schritten:
Wenn T ein value_type ist und A nicht vorhanden ist:
Der object_creation_expression ist ein standardkonstruktoraufruf. Das Ergebnis der
object_creation_expression ist ein Wert vom Typ T , d. h. der Standardwert für, T wie im System.
ValueType-Typdefiniert.
Andernfalls, wenn T ein type_parameter ist und A nicht vorhanden ist:
Wenn keine Werttyp Einschränkung oder Konstruktoreinschränkung (Typparameter Einschränkungen)
für angegeben wurde T , tritt ein Bindungs Zeitfehler auf.
Das Ergebnis der object_creation_expression ist ein Wert des Lauf Zeittyps, an den der Typparameter
gebunden wurde, nämlich das Ergebnis des Aufrufs des Standardkonstruktors dieses Typs. Der Lauf
Zeittyp kann ein Referenztyp oder ein Werttyp sein.
Andernfalls, wenn T ein class_type oder ein struct_type ist:
Wenn T ein abstract class_type ist, tritt ein Kompilierzeitfehler auf.
Der aufzurufende Instanzkonstruktor wird mithilfe der Regeln zur Überladungs Auflösung der
Überladungs Auflösungbestimmt. Der Satz von Kandidaten Instanzkonstruktoren besteht aus allen
zugänglichen Instanzkonstruktoren T , die in deklariert sind und in Bezug auf A (anwendbares
Funktionsmember) anwendbar sind. Wenn der Satz von standardinstanzkonstruktoren leer ist, oder
wenn ein einzelner Konstruktor mit der besten Instanz nicht identifiziert werden kann, tritt ein
Bindungs Zeitfehler auf.
Das Ergebnis der object_creation_expression ist ein Wert vom Typ T , d. h. der Wert, der durch
Aufrufen des Instanzkonstruktors erzeugt wird, der im obigen Schritt festgelegt wurde.
Andernfalls ist der object_creation_expression ungültig, und ein Bindungs Fehler tritt auf.
Auch wenn die object_creation_expression dynamisch gebunden ist, ist der Kompilier Zeittyp immer noch T .
Die Lauf Zeit Verarbeitung einer object_creation_expression der Form new T(A) , wobei T class_type oder ein
struct_type ist und A eine optionale argument_list ist, besteht aus den folgenden Schritten:
Wenn T ein class_type ist:
Es wird eine neue Instanz der-Klasse T zugeordnet. Wenn nicht genügend Arbeitsspeicher zur
Verfügung steht, um die neue Instanz zuzuordnen, wird eine ausgelöst, System.OutOfMemoryException
und es werden keine weiteren Schritte ausgeführt.
Alle Felder der neuen Instanz werden mit ihren Standardwerten initialisiert (Standardwerte).
Der Instanzkonstruktor wird entsprechend den Regeln für den Funktionselement Aufruf (Kompilierzeit
Überprüfung der dynamischen Überladungs Auflösung) aufgerufen. Ein Verweis auf die neu
zugeordnete-Instanz wird automatisch an den Instanzkonstruktor übergeben, und der Zugriff auf die-
Instanz kann innerhalb dieses Konstruktors als erfolgen this .
Wenn T ein struct_type ist:
Eine Instanz vom Typ T wird erstellt, indem eine temporäre lokale Variable zugewiesen wird. Da ein
Instanzkonstruktor einer struct_type erforderlich ist, um jedem Feld der erstellten Instanz einen Wert
definitiv zuzuweisen, ist keine Initialisierung der temporären Variablen erforderlich.
Der Instanzkonstruktor wird entsprechend den Regeln für den Funktionselement Aufruf (Kompilierzeit
Überprüfung der dynamischen Überladungs Auflösung) aufgerufen. Ein Verweis auf die neu
zugeordnete-Instanz wird automatisch an den Instanzkonstruktor übergeben, und der Zugriff auf die-
Instanz kann innerhalb dieses Konstruktors als erfolgen this .
Objektinitialisierer
Ein Objektinitialisierer gibt Werte für NULL oder mehr Felder, Eigenschaften oder indizierte Elemente eines
Objekts an.

object_initializer
: '{' member_initializer_list? '}'
| '{' member_initializer_list ',' '}'
;

member_initializer_list
: member_initializer (',' member_initializer)*
;

member_initializer
: initializer_target '=' initializer_value
;

initializer_target
: identifier
| '[' argument_list ']'
;

initializer_value
: expression
| object_or_collection_initializer
;

Ein Objektinitialisierer besteht aus einer Sequenz von Elementinitialisierern, die von { -und- } Token
eingeschlossen und durch Kommas getrennt sind. Jede member_initializer bestimmt ein Ziel für die
Initialisierung. Ein Bezeichner muss ein barrierefreies Feld oder eine Eigenschaft des Objekts benennen, das
initialisiert wird, wohingegen eine in eckige Klammern eingeschlossene argument_list Argumente für einen
zugreif baren Indexer für das Objekt angeben muss, das initialisiert wird. Es ist ein Fehler, wenn ein
Objektinitialisierer mehr als einen Elementinitialisierer für das gleiche Feld oder die gleiche Eigenschaft
einschließt.
Auf jede initializer_target folgt ein Gleichheitszeichen und entweder ein Ausdruck, ein Objektinitialisierer oder
ein Auflistungsinitialisierer. Es ist nicht möglich, dass Ausdrücke im Objektinitialisierer auf das neu erstellte
Objekt verweisen, das initialisiert wird.
Ein Member-Initialisierer, der einen Ausdruck angibt, nachdem das Gleichheitszeichen auf die gleiche Weise wie
eine Zuweisung (einfache Zuweisung) zum Ziel verarbeitet wird.
Ein Member-Initialisierer, der einen Objektinitialisierer angibt, nachdem das Gleichheitszeichen ein
instanzinitialisierer ist, d. h. eine Initialisierung eines eingebetteten Objekts. Anstatt dem Feld oder der
Eigenschaft einen neuen Wert zuzuweisen, werden die Zuweisungen im Initialisierer für den initialisierten
Objektinitialisierer als Zuweisungen zu Membern des Felds oder der Eigenschaft behandelt. Initialisierer für ein
Objekt können nicht auf Eigenschaften mit einem Werttyp oder auf schreibgeschützte Felder mit einem Werttyp
angewendet werden.
Ein Member-Initialisierer, der einen Auflistungsinitialisierer angibt, nachdem das Gleichheitszeichen eine
Initialisierung einer eingebetteten Auflistung ist. Anstatt dem Zielfeld, der Eigenschaft oder dem Indexer eine
neue Auflistung zuzuweisen, werden die im Initialisierer angegebenen Elemente der Auflistung hinzugefügt, auf
die das Ziel verweist. Das Ziel muss einen Sammlungstyp aufweisen, der die in Auflistungsinitialisierer
angegebenen Anforderungen erfüllt.
Die Argumente für einen indexinitialisierer werden immer genau einmal ausgewertet. Folglich werden Sie,
selbst wenn die Argumente am Ende nie verwendet werden (z. b. aufgrund eines leeren Initialisierers), auf Ihre
Nebeneffekte ausgewertet.
Die folgende Klasse stellt einen Punkt mit zwei Koordinaten dar:

public class Point


{
int x, y;

public int X { get { return x; } set { x = value; } }


public int Y { get { return y; } set { y = value; } }
}

Eine Instanz von Point kann wie folgt erstellt und initialisiert werden:

Point a = new Point { X = 0, Y = 1 };

Dies hat die gleiche Wirkung wie

Point __a = new Point();


__a.X = 0;
__a.Y = 1;
Point a = __a;

dabei __a ist eine anderweitig unsichtbare und nicht zugängliche temporäre Variable. Die folgende Klasse stellt
ein Rechteck dar, das aus zwei Punkten erstellt wurde:

public class Rectangle


{
Point p1, p2;

public Point P1 { get { return p1; } set { p1 = value; } }


public Point P2 { get { return p2; } set { p2 = value; } }
}

Eine Instanz von Rectangle kann wie folgt erstellt und initialisiert werden:

Rectangle r = new Rectangle {


P1 = new Point { X = 0, Y = 1 },
P2 = new Point { X = 2, Y = 3 }
};

Dies hat die gleiche Wirkung wie


Rectangle __r = new Rectangle();
Point __p1 = new Point();
__p1.X = 0;
__p1.Y = 1;
__r.P1 = __p1;
Point __p2 = new Point();
__p2.X = 2;
__p2.Y = 3;
__r.P2 = __p2;
Rectangle r = __r;

dabei __r __p1 handelt es sich bei und __p2 um temporäre Variablen, die andernfalls unsichtbar und nicht
zugänglich sind.
, Wenn Rectangle der Konstruktor die beiden eingebetteten Instanzen ordnet. Point

public class Rectangle


{
Point p1 = new Point();
Point p2 = new Point();

public Point P1 { get { return p1; } }


public Point P2 { get { return p2; } }
}

Das folgende Konstrukt kann verwendet werden, um die eingebetteten Instanzen zu initialisieren, Point anstatt
neue Instanzen zuzuweisen:

Rectangle r = new Rectangle {


P1 = { X = 0, Y = 1 },
P2 = { X = 2, Y = 3 }
};

Dies hat die gleiche Wirkung wie

Rectangle __r = new Rectangle();


__r.P1.X = 0;
__r.P1.Y = 1;
__r.P2.X = 2;
__r.P2.Y = 3;
Rectangle r = __r;

Im folgenden Beispiel wird eine entsprechende Definition von C angegeben:

var c = new C {
x = true,
y = { a = "Hello" },
z = { 1, 2, 3 },
["x"] = 5,
[0,0] = { "a", "b" },
[1,2] = {}
};

entspricht dieser Reihe von Zuweisungen:


C __c = new C();
__c.x = true;
__c.y.a = "Hello";
__c.z.Add(1);
__c.z.Add(2);
__c.z.Add(3);
string __i1 = "x";
__c[__i1] = 5;
int __i2 = 0, __i3 = 0;
__c[__i2,__i3].Add("a");
__c[__i2,__i3].Add("b");
int __i4 = 1, __i5 = 2;
var c = __c;

Where __c , usw., sind generierte Variablen, die unsichtbar sind und für den Quellcode nicht zugänglich sind.
Beachten Sie, dass die Argumente für [0,0] nur einmal ausgewertet werden und die Argumente für [1,2]
einmal ausgewertet werden, auch wenn Sie nie verwendet werden.
Auflistungsinitialisierer
Ein Auflistungsinitialisierer gibt die Elemente einer Auflistung an.

collection_initializer
: '{' element_initializer_list '}'
| '{' element_initializer_list ',' '}'
;

element_initializer_list
: element_initializer (',' element_initializer)*
;

element_initializer
: non_assignment_expression
| '{' expression_list '}'
;

expression_list
: expression (',' expression)*
;

Ein Auflistungsinitialisierer besteht aus einer Sequenz von Elementinitialisierern, die von { -und- } Token
eingeschlossen und durch Kommas getrennt sind. Jeder Elementinitialisierer gibt ein Element an, das dem
aufzurufenden Auflistungs Objekt hinzugefügt werden soll, und besteht aus einer Liste von Ausdrücken, die von
{ -und- } Token eingeschlossen und durch Kommas getrennt sind. Ein Elementinitialisierer mit einem
Ausdruck kann ohne geschweifte Klammern geschrieben werden, kann jedoch kein Zuweisungs Ausdruck sein,
um Mehrdeutigkeit mit Member-Initialisierern zu vermeiden. Die non_assignment_expression Produktion ist in
Expressiondefiniert.
Im folgenden finden Sie ein Beispiel für einen Objekt Erstellungs Ausdruck, der einen Auflistungsinitialisierer
umfasst:

List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

Das Auflistungs Objekt, auf das ein Auflistungsinitialisierer angewendet wird, muss einen Typ aufweisen, der
implementiert, System.Collections.IEnumerable oder ein Kompilierzeitfehler tritt auf. Für jedes angegebene
Element in der Reihenfolge ruft der sammlungsinitialisierer eine Add Methode für das Zielobjekt mit der
Ausdrucks Liste des elementinitialisierers als Argumentliste auf und wendet die normale Member-Suche-und
Überladungs Auflösung für jeden Aufruf an. Folglich muss das Auflistungs Objekt über eine gültige Instanz-oder
Erweiterungsmethode mit dem Namen Add für jeden Elementinitialisierer verfügen.
Die folgende Klasse stellt einen Kontakt mit einem Namen und einer Liste von Telefonnummern dar:

public class Contact


{
string name;
List<string> phoneNumbers = new List<string>();

public string Name { get { return name; } set { name = value; } }

public List<string> PhoneNumbers { get { return phoneNumbers; } }


}

Eine List<Contact> kann wie folgt erstellt und initialisiert werden:

var contacts = new List<Contact> {


new Contact {
Name = "Chris Smith",
PhoneNumbers = { "206-555-0101", "425-882-8080" }
},
new Contact {
Name = "Bob Harris",
PhoneNumbers = { "650-555-0199" }
}
};

Dies hat die gleiche Wirkung wie

var __clist = new List<Contact>();


Contact __c1 = new Contact();
__c1.Name = "Chris Smith";
__c1.PhoneNumbers.Add("206-555-0101");
__c1.PhoneNumbers.Add("425-882-8080");
__clist.Add(__c1);
Contact __c2 = new Contact();
__c2.Name = "Bob Harris";
__c2.PhoneNumbers.Add("650-555-0199");
__clist.Add(__c2);
var contacts = __clist;

dabei __clist __c1 handelt es sich bei und __c2 um temporäre Variablen, die andernfalls unsichtbar und
nicht zugänglich sind.
Ausdrücke zum Erstellen von Arrays
Eine array_creation_expression wird zum Erstellen einer neuen Instanz eines- array_type verwendet.

array_creation_expression
: 'new' non_array_type '[' expression_list ']' rank_specifier* array_initializer?
| 'new' array_type array_initializer
| 'new' rank_specifier array_initializer
;

Ein Array Erstellungs Ausdruck des ersten Formulars ordnet eine Array Instanz des Typs zu, der sich aus dem
Löschen der einzelnen Ausdrücke aus der Ausdrucks Liste ergibt. Beispielsweise erzeugt der Ausdruck für die
Array Erstellung new int[10,20] eine Array Instanz vom Typ int[,] , und der Ausdruck für die Array Erstellung
new int[10][,] erzeugt ein Array vom Typ int[][,] . Jeder Ausdruck in der Ausdrucks Liste muss vom Typ
int , uint , oder sein long ulong oder implizit in einen oder mehrere dieser Typen konvertiert werden
können. Der Wert jedes Ausdrucks bestimmt die Länge der entsprechenden Dimension in der neu zugeordneten
Array Instanz. Da die Länge einer Array Dimension nicht negativ sein muss, ist dies ein Kompilierzeitfehler, bei
dem ein constant_expression mit einem negativen Wert in der Ausdrucks Liste vorliegt.
Das Layout von Arrays ist nicht festgelegt, es sei denn, es ist ein unsicherer Kontext (unsichere Kontexte).
Wenn ein Array Erstellungs Ausdruck des ersten Formulars einen Arrayinitialisierer enthält, muss jeder
Ausdruck in der Ausdrucks Liste eine Konstante sein, und die von der Ausdrucks Liste angegebenen Rang-und
Dimensions Längen müssen mit denen des Arrayinitialisierers identisch sein.
In einem Array Erstellungs Ausdruck des zweiten oder dritten Formulars muss der Rang des angegebenen
Arraytyps oder rangspezifizierers mit dem des Arrayinitialisierers identisch sein. Die einzelnen Dimensions
Längen werden von der Anzahl von Elementen in jeder der entsprechenden Schachtelungs Ebenen des
Arrayinitialisierers abgeleitet. Folglich ist der Ausdruck

new int[,] {{0, 1}, {2, 3}, {4, 5}}

entspricht genau

new int[3, 2] {{0, 1}, {2, 3}, {4, 5}}

Ein Array Erstellungs Ausdruck des dritten Formulars wird als *implizit typisier ter Array Erstellungs
Ausdruck _ bezeichnet. Sie ähnelt dem zweiten Formular, mit dem Unterschied, dass der Elementtyp des Arrays
nicht explizit angegeben wird, sondern als der am besten geeignete Typ (suchendes besten allgemeinen Typs
eines Satzes von Ausdrücken) des Satzes von Ausdrücken im Arrayinitialisierer festgelegt ist. Bei einem
mehrdimensionalen Array, d. h. einer, bei dem der _rank_specifier * mindestens ein Komma enthält, besteht
dieser Satz aus allen Ausdrücken, die in den array_initializer s gefunden wurden.
Arrayinitialisierer werden in arrayinitialisierernausführlicher beschrieben.
Das Ergebnis der Auswertung eines Array Erstellungs Ausdrucks wird als Wert klassifiziert, nämlich als Verweis
auf die neu zugeordnete Array Instanz. Die Lauf Zeit Verarbeitung eines Ausdrucks zur Array Erstellung besteht
aus den folgenden Schritten:
Die Dimensions Längen Ausdrücke der expression_list werden in der Reihenfolge von links nach rechts
ausgewertet. Nach der Auswertung der einzelnen Ausdrücke wird eine implizite Konvertierung (implizite
Konvertierungen) in einen der folgenden Typen ausgeführt: int , uint , long , ulong . Der erste Typ in
dieser Liste, für den eine implizite Konvertierung vorhanden ist, wird ausgewählt. Wenn die Auswertung
eines Ausdrucks oder der nachfolgenden impliziten Konvertierung eine Ausnahme verursacht, werden keine
weiteren Ausdrücke ausgewertet, und es werden keine weiteren Schritte ausgeführt.
Die berechneten Werte für die Dimensions Längen werden wie folgt überprüft. Wenn mindestens einer der-
Werte kleiner als 0 (null) ist, wird eine ausgelöst, System.OverflowException und es werden keine weiteren
Schritte ausgeführt.
Eine Array Instanz mit den angegebenen Dimensions Längen ist zugeordnet. Wenn nicht genügend
Arbeitsspeicher zur Verfügung steht, um die neue Instanz zuzuordnen, wird eine ausgelöst,
System.OutOfMemoryException und es werden keine weiteren Schritte ausgeführt.
Alle Elemente der neuen Array Instanz werden mit ihren Standardwerten initialisiert (Standardwerte).
Wenn der Ausdruck für die Array Erstellung einen Arrayinitialisierer enthält, wird jeder Ausdruck im
Arrayinitialisierer ausgewertet und dem entsprechenden Array Element zugewiesen. Die Auswertungen und
Zuweisungen werden in der Reihenfolge ausgeführt, in der die Ausdrücke in den Arrayinitialisierer
geschrieben werden – mit anderen Worten, Elemente werden in steigender Index Reihenfolge initialisiert,
wobei die äußerste Rechte Dimension zuerst zunimmt. Wenn die Auswertung eines angegebenen Ausdrucks
oder der nachfolgenden Zuweisung zum entsprechenden Array Element eine Ausnahme auslöst, werden
keine weiteren Elemente initialisiert (und die restlichen Elemente haben daher ihre Standardwerte).
Ein Array Erstellungs Ausdruck ermöglicht die Instanziierung eines Arrays mit Elementen eines Arraytyps, aber
die Elemente eines solchen Arrays müssen manuell initialisiert werden. Beispielsweise ist die-Anweisung

int[][] a = new int[100][];

erstellt ein eindimensionales Array mit 100 Elementen vom Typ int[] . Der Anfangswert jedes Elements ist
null . Es ist nicht möglich, dass derselbe Array Erstellungs Ausdruck auch die Unterarrays und die-Anweisung
instanziiert.

int[][] a = new int[100][5]; // Error

führt zu einem Kompilierzeitfehler. Die Instanziierung der Unterarrays muss stattdessen manuell ausgeführt
werden, wie in

int[][] a = new int[100][];


for (int i = 0; i < 100; i++) a[i] = new int[5];

Wenn ein Array von Arrays eine "rechteckige" Form hat, d. h., wenn die Teil Arrays die gleiche Länge haben, ist es
effizienter, ein mehrdimensionales Array zu verwenden. Im obigen Beispiel erstellt die Instanziierung des Arrays
von Arrays 101 Objekte – ein äußeres Array und 100 Teil Arrays. Im Gegensatz dazu

int[,] = new int[100, 5];

erstellt nur ein einzelnes-Objekt, ein zweidimensionales Array und erreicht die Zuordnung in einer einzelnen
Anweisung.
Im folgenden finden Sie Beispiele für implizit typisierte Array Erstellungs Ausdrücke:

var a = new[] { 1, 10, 100, 1000 }; // int[]

var b = new[] { 1, 1.5, 2, 2.5 }; // double[]

var c = new[,] { { "hello", null }, { "world", "!" } }; // string[,]

var d = new[] { 1, "one", 2, "two" }; // Error

Der letzte Ausdruck verursacht einen Kompilierzeitfehler, weil weder int noch string implizit in den anderen
konvertiert werden kann, sodass es keinen optimalen allgemeinen Typ gibt. In diesem Fall muss ein explizit
typisierter Array Erstellungs Ausdruck verwendet werden, z. b. die Angabe des Typs object[] . Alternativ kann
eines der Elemente in einen allgemeinen Basistyp umgewandelt werden, der dann zum abzurufenden
Elementtyp wird.
Implizit typisierte Array Erstellungs Ausdrücke können mit anonymen Objektinitialisierern (anonymen Objekt
Erstellungs Ausdrücken) kombiniert werden, um anonym typisierte Datenstrukturen zu erstellen. Beispiel:
var contacts = new[] {
new {
Name = "Chris Smith",
PhoneNumbers = new[] { "206-555-0101", "425-882-8080" }
},
new {
Name = "Bob Harris",
PhoneNumbers = new[] { "650-555-0199" }
}
};

Delegatenerstellungs Ausdrücke
Eine delegate_creation_expression wird zum Erstellen einer neuen Instanz einer delegate_type verwendet.

delegate_creation_expression
: 'new' delegate_type '(' expression ')'
;

Das Argument eines delegaterstellungs-Ausdrucks muss eine Methoden Gruppe, eine anonyme Funktion oder
ein Wert entweder der Kompilier Zeittyp dynamic oder ein delegate_type sein. Wenn das Argument eine
Methoden Gruppe ist, identifiziert es die-Methode und für eine Instanzmethode das-Objekt, für das ein Delegat
erstellt werden soll. Wenn das Argument eine anonyme Funktion ist, werden die Parameter und der Methoden
Text des delegatziels direkt definiert. Wenn das-Argument ein Wert ist, wird eine Delegatinstanz identifiziert, von
der eine Kopie erstellt werden soll.
Wenn der Ausdruck den Kompilier Zeittyp aufweist dynamic , wird der delegate_creation_expression dynamisch
gebunden (dynamische Bindung), und die unten aufgeführten Regeln werden zur Laufzeit mit dem Lauf Zeittyp
des Ausdrucks angewendet. Andernfalls werden die Regeln zur Kompilierzeit angewendet.
Die Bindungs Zeit Verarbeitung einer delegate_creation_expression der Form new D(E) , wobei D ein
delegate_type und E ein Ausdruck ist, besteht aus den folgenden Schritten:
Wenn E eine Methoden Gruppe ist, wird der delegaterstellungs-Ausdruck auf die gleiche Weise wie eine
Methoden Gruppen Konvertierung (Methoden Gruppen Konvertierungen) von E in verarbeitet D .
Wenn E eine anonyme Funktion ist, wird der delegaterstellungs-Ausdruck auf die gleiche Weise wie eine
anonyme Funktions Konvertierung (Anonyme Funktions Konvertierungen) von E in verarbeitet D .
Wenn E ein-Wert ist, E muss kompatibel (Delegatdeklarationen) mit sein D , und das Ergebnis ist ein
Verweis auf einen neu erstellten Delegaten vom Typ, der D auf dieselbe Aufruf Liste wie verweist E . Wenn
E nicht mit kompatibel ist D , tritt ein Kompilierzeitfehler auf.

Die Lauf Zeit Verarbeitung einer delegate_creation_expression der Form new D(E) , wobei D ein delegate_type
und E ein Ausdruck ist, besteht aus den folgenden Schritten:
Wenn E eine Methoden Gruppe ist, wird der delegaterstellungs-Ausdruck als Methoden Gruppen
Konvertierung (Methoden Gruppen Konvertierungen) von E in ausgewertet D .
Wenn E eine anonyme Funktion ist, wird die Delegaterstellung als anonyme Funktions Konvertierung von
E in ausgewertet D (Anonyme Funktions Konvertierungen).
Wenn E ein Wert eines delegate_type ist:
E wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren
Schritte ausgeführt.
Wenn der Wert von E ist null , wird eine ausgelöst, System.NullReferenceException und es werden
keine weiteren Schritte ausgeführt.
Eine neue Instanz des Delegattyps D wird zugewiesen. Wenn nicht genügend Arbeitsspeicher zur
Verfügung steht, um die neue Instanz zuzuordnen, wird eine ausgelöst, System.OutOfMemoryException
und es werden keine weiteren Schritte ausgeführt.
Die neue Delegatinstanz wird mit der gleichen Aufruf Liste initialisiert wie die Delegatinstanz, die von
angegeben wird E .
Die Aufruf Liste eines Delegaten wird bestimmt, wenn der Delegat instanziiert wird, und wird dann für die
gesamte Lebensdauer des Delegaten konstant bleiben. Anders ausgedrückt: Es ist nicht möglich, die Ziel Aufruf
baren Entitäten eines Delegaten zu ändern, nachdem er erstellt wurde. Wenn zwei Delegaten kombiniert werden
oder eine aus einer anderen entfernt wird (Delegatdeklarationen), ergibt sich ein neuer Delegat. der Inhalt eines
vorhandenen Delegaten wurde nicht geändert.
Es ist nicht möglich, einen Delegaten zu erstellen, der auf eine Eigenschaft, einen Indexer, einen
benutzerdefinierten Operator, einen Instanzkonstruktor, einen Dekonstruktor oder einen statischen Konstruktor
verweist.
Wie oben beschrieben, wird beim Erstellen eines Delegaten aus einer Methoden Gruppe die Liste der formalen
Parameter und der Rückgabetyp des Delegaten bestimmt, welche der überladenen Methoden ausgewählt
werden sollen. Im Beispiel

delegate double DoubleFunc(double x);

class A
{
DoubleFunc f = new DoubleFunc(Square);

static float Square(float x) {


return x * x;
}

static double Square(double x) {


return x * x;
}
}

Das- A.f Feld wird mit einem Delegaten initialisiert, der sich auf die zweite Methode bezieht, Square da diese
Methode exakt mit der formalen Parameterliste und dem Rückgabetyp von übereinstimmt DoubleFunc . Wenn
die zweite Square Methode nicht vorhanden wäre, wäre ein Kompilierzeitfehler aufgetreten.
Ausdrücke zum Erstellen anonymer Objekte
Ein anonymous_object_creation_expression wird zum Erstellen eines Objekts eines anonymen Typs verwendet.

anonymous_object_creation_expression
: 'new' anonymous_object_initializer
;

anonymous_object_initializer
: '{' member_declarator_list? '}'
| '{' member_declarator_list ',' '}'
;

member_declarator_list
: member_declarator (',' member_declarator)*
;

member_declarator
: simple_name
| member_access
| base_access
| null_conditional_member_access
| identifier '=' expression
;
Ein Anonymer Objektinitialisierer deklariert einen anonymen Typ und gibt eine Instanz dieses Typs zurück. Ein
anonymer Typ ist ein namenloser Klassentyp, der direkt von erbt object . Die Member eines anonymen Typs
sind eine Sequenz von schreibgeschützten Eigenschaften, die vom anonymen Objektinitialisierer abgeleitet
werden, um eine Instanz des Typs zu erstellen. Genauer gesagt: ein Anonymer Objektinitialisierer des Formulars

new { p1 = e1, p2 = e2, ..., pn = en }

deklariert einen anonymen Typ des Formulars.

class __Anonymous1
{
private readonly T1 f1;
private readonly T2 f2;
...
private readonly Tn fn;

public __Anonymous1(T1 a1, T2 a2, ..., Tn an) {


f1 = a1;
f2 = a2;
...
fn = an;
}

public T1 p1 { get { return f1; } }


public T2 p2 { get { return f2; } }
...
public Tn pn { get { return fn; } }

public override bool Equals(object __o) { ... }


public override int GetHashCode() { ... }
}

wobei jede Tx der Typen des entsprechenden Ausdrucks ist ex . Der Ausdruck, der in einem
member_declarator verwendet wird, muss einen Typ aufweisen. Daher ist es ein Kompilierzeitfehler für einen
Ausdruck in einer member_declarator NULL oder eine anonyme Funktion zu sein. Es ist auch ein
Kompilierzeitfehler, wenn der Ausdruck einen unsicheren Typ hat.
Die Namen eines anonymen Typs und des-Parameters für die zugehörige- Equals Methode werden vom
Compiler automatisch generiert, und im Programmtext kann nicht auf Sie verwiesen werden.
Innerhalb desselben Programms werden zwei anonyme Objektinitialisierer, die eine Sequenz von Eigenschaften
mit denselben Namen und Kompilierzeit Typen in derselben Reihenfolge angeben, Instanzen desselben
anonymen Typs erstellen.
Im Beispiel

var p1 = new { Name = "Lawnmower", Price = 495.00 };


var p2 = new { Name = "Shovel", Price = 26.95 };
p1 = p2;

die Zuweisung in der letzten Zeile ist zulässig, da p1 und p2 denselben anonymen Typ haben.
Die Equals -Methode und die- GetHashcode Methode für anonyme Typen überschreiben die von geerbten
Methoden object und werden im Hinblick auf die Equals und GetHashcode der Eigenschaften definiert, sodass
zwei Instanzen desselben anonymen Typs nur dann gleich sind, wenn alle Eigenschaften gleich sind.
Ein Element Deklarator kann mit einem einfachen Namen (Typrückschluss), einem Element Zugriff (Kompilierzeit
Überprüfung der dynamischen Überladungs Auflösung), einem Basis Zugriff (Basis Zugriff) oder einem NULL
bedingten Member-Zugriff (null bedingte Ausdrücke als Projektions Initialisierer) abgekürzt werden. Dies wird
als Projektions Initialisierer bezeichnet und ist eine Abkürzung für eine Deklaration von und die Zuweisung
zu einer Eigenschaft mit demselben Namen. Insbesondere Member-Deklaratoren der Formulare

identifier
expr.identifier

entsprechen genau den folgenden:

identifier = identifier
identifier = expr.identifier

Folglich wählt der Bezeichner in einem Projektions Initialisierer sowohl den Wert als auch das Feld oder die
Eigenschaft aus, dem der Wert zugewiesen wird. Intuitiv projiziert ein Projektions Initialisierer nicht nur einen
Wert, sondern auch den Namen des Werts.
Der typeof-Operator
Der- typeof Operator wird zum Abrufen des- System.Type Objekts für einen Typ verwendet.

typeof_expression
: 'typeof' '(' type ')'
| 'typeof' '(' unbound_type_name ')'
| 'typeof' '(' 'void' ')'
;

unbound_type_name
: identifier generic_dimension_specifier?
| identifier '::' identifier generic_dimension_specifier?
| unbound_type_name '.' identifier generic_dimension_specifier?
;

generic_dimension_specifier
: '<' comma* '>'
;

comma
: ','
;

Die erste Form typeof_expression besteht aus einem typeof Schlüsselwort, gefolgt von einem Typ in
Klammern. Das Ergebnis eines Ausdrucks dieses Formulars ist das- System.Type Objekt für den jeweiligen Typ.
Es ist nur ein- System.Type Objekt für einen bestimmten Typ vorhanden. Dies bedeutet, dass für einen T Typ
typeof(T) == typeof(T) immer true ist. Der Typ darf nicht sein dynamic .

Die zweite Form von typeof_expression besteht aus einem typeof Schlüsselwort, gefolgt von einer
unbound_type_name in Klammern. Ein- unbound_type_name ähnelt einem TYPE_NAME (Namespace und
Typnamen), mit dem Unterschied, dass ein unbound_type_name generic_dimension_specifier s enthält, in denen
eine TYPE_NAME type_argument_list s enthält. Wenn der Operand einer typeof_expression eine Sequenz von
Token ist, die die Grammatiken von unbound_type_name und TYPE_NAME erfüllt, d. h., wenn weder ein
generic_dimension_specifier noch ein type_argument_list enthalten ist, wird die Sequenz der Token als
TYPE_NAME betrachtet. Die Bedeutung eines unbound_type_name wird wie folgt bestimmt:
Konvertieren Sie die Sequenz von Token in eine TYPE_NAME , indem Sie alle generic_dimension_specifier
durch eine type_argument_list ersetzen, die dieselbe Anzahl von Kommas und das Schlüsselwort object wie
jedes type_argument hat.
Werten Sie die resultierende TYPE_NAME aus, während alle Typparameter Einschränkungen ignoriert
werden.
Der unbound_type_name wird in den ungebundenen generischen Typ aufgelöst, der dem resultierenden
konstruierten Typ zugeordnet ist (gebundene und ungebundene Typen).
Das Ergebnis des typeof_expression ist das- System.Type Objekt für den resultierenden ungebundenen
generischen Typ.
Die dritte Form von typeof_expression besteht aus einem typeof Schlüsselwort, gefolgt von einem void
Schlüsselwort in Klammern. Das Ergebnis eines Ausdrucks dieses Formulars ist das System.Type Objekt, das das
Fehlen eines Typs darstellt. Das von zurückgegebene Typobjekt unter typeof(void) scheidet sich vom Typobjekt,
das für einen beliebigen Typ zurückgegeben wurde. Dieses besondere Typobjekt ist nützlich in
Klassenbibliotheken, die die Reflektion auf Methoden in der Sprache zulassen, wobei diese Methoden eine
Möglichkeit haben möchten, den Rückgabetyp jeder Methode, einschließlich void-Methoden, mit einer Instanz
von darzustellen System.Type .
Der- typeof Operator kann für einen Typparameter verwendet werden. Das Ergebnis ist das- System.Type
Objekt für den Lauf Zeittyp, der an den Typparameter gebunden wurde. Der typeof Operator kann auch für
einen konstruierten Typ oder einen ungebundenen generischen Typ (gebundene und ungebundene Typen)
verwendet werden. Das- System.Type Objekt für einen ungebundenen generischen Typ ist nicht mit dem-
System.Type Objekt des Instanztyps identisch. Der Instanztyp ist zur Laufzeit immer ein geschlossener
konstruierter Typ, sodass System.Type das Objekt von den verwendeten Lauf Zeittyp Argumenten abhängt,
während der ungebundene generische Typ keine Typargumente aufweist.
Das Beispiel

using System;

class X<T>
{
public static void PrintTypes() {
Type[] t = {
typeof(int),
typeof(System.Int32),
typeof(string),
typeof(double[]),
typeof(void),
typeof(T),
typeof(X<T>),
typeof(X<X<T>>),
typeof(X<>)
};
for (int i = 0; i < t.Length; i++) {
Console.WriteLine(t[i]);
}
}
}

class Test
{
static void Main() {
X<int>.PrintTypes();
}
}

erzeugt die folgende Ausgabe:


System.Int32
System.Int32
System.String
System.Double[]
System.Void
System.Int32
X`1[System.Int32]
X`1[X`1[System.Int32]]
X`1[T]

Beachten Sie, dass int und System.Int32 denselben Typ haben.


Beachten Sie auch, dass das Ergebnis von typeof(X<>) nicht vom Typargument abhängt, sondern vom Ergebnis
von typeof(X<T>) .
Checked- und Unchecked-Operatoren
Die checked unchecked Operatoren und werden verwendet, um den Überlauf Überprüfungs Kontext für
arithmetische Operationen und Konvertierungen im ganzzahligen Typ zu steuern.

checked_expression
: 'checked' '(' expression ')'
;

unchecked_expression
: 'unchecked' '(' expression ')'
;

Der checked Operator wertet den enthaltenen Ausdruck in einem überprüften Kontext aus, und der unchecked
Operator wertet den enthaltenen Ausdruck in einem nicht überprüften Kontext aus. Ein checked_expression oder
unchecked_expression entspricht genau einem parenthesized_expression (in Klammern Klammern), mit dem
Unterschied, dass der enthaltene Ausdruck im angegebenen Überlauf Prüfungs Kontext ausgewertet wird.
Der Überlauf Überprüfungs Kontext kann auch durch die checked -und- unchecked Anweisungen
(dieaktivierten und deaktivierten Anweisungen) gesteuert werden.
Die folgenden Vorgänge wirken sich auf den Überlauf Prüfungs Kontext aus, der von den checked unchecked
Operatoren und und-Anweisungen festgelegt wird:
Der vordefinierte ++ und -- unäre Operator (postfix-Inkrement-und Dekrementoperatoren und Präfix
Inkrement-und Dekrementoperatoren), wenn der Operand ein ganzzahliger Typ ist.
Der vordefinierte - unäre Operator (unärer Minus Operator), wenn der Operand ein ganzzahliger Typ ist.
Die vordefinierten + - * Operatoren,, und / (arithmetische Operatoren), wenn beide Operanden
ganzzahlige Typen sind.
Explizite numerische Konvertierungen (explizite numerische Konvertierungen) von einem ganzzahligen Typ
in einen anderen ganzzahligen Typ oder von float oder in einen ganzzahligen double Typ.

Wenn eine der obigen Vorgänge ein Ergebnis erzeugt, das zu groß für die Darstellung im Zieltyp ist, steuert der
Kontext, in dem der Vorgang ausgeführt wird, das resultierende Verhalten:
In einem checked Kontext tritt ein Kompilierzeitfehler auf, wenn es sich bei dem Vorgang um einen
konstanten Ausdruck (Konstante Ausdrücke) handelt. Andernfalls wird eine ausgelöst, wenn der Vorgang zur
Laufzeit ausgeführt wird System.OverflowException .
In einem unchecked Kontext wird das Ergebnis abgeschnitten, indem alle höherwertigen Bits verworfen
werden, die nicht in den Zieltyp passen.
Für nicht konstante Ausdrücke (Ausdrücke, die zur Laufzeit ausgewertet werden), die nicht von einem checked
or- unchecked Operator oder-Anweisungen eingeschlossen sind, ist der Standardkontext der Überlauf
Überprüfung, unchecked es sei denn, externe Faktoren (z. b. compilerswitches und Konfiguration der
Ausführungsumgebung) werden zur checked Auswertung aufgerufen.
Für konstante Ausdrücke (Ausdrücke, die zur Kompilierzeit vollständig ausgewertet werden können) ist der
Standardkontext der Überlauf Überprüfung immer checked . Wenn ein konstanter Ausdruck nicht explizit in
einem Kontext abgelegt wird unchecked , verursachen über Flüsse, die während der Kompilierzeit Auswertung
des Ausdrucks auftreten, immer Kompilierzeitfehler.
Der Text einer anonymen Funktion ist nicht von- checked oder- unchecked Kontexten betroffen, in denen die
anonyme Funktion auftritt.
Im Beispiel

class Test
{
static readonly int x = 1000000;
static readonly int y = 1000000;

static int F() {


return checked(x * y); // Throws OverflowException
}

static int G() {


return unchecked(x * y); // Returns -727379968
}

static int H() {


return x * y; // Depends on default
}
}

Es werden keine Kompilierzeitfehler gemeldet, da keiner der Ausdrücke zur Kompilierzeit ausgewertet werden
kann. Zur Laufzeit löst die F Methode eine aus System.OverflowException , und die G Methode gibt-
727379968 (die unteren 32 Bits des Ergebnisses außerhalb des gültigen Bereichs) zurück. Das Verhalten der- H
Methode hängt vom Standardkontext der Überlauf Überprüfung für die Kompilierung ab, ist jedoch entweder
identisch mit F oder gleich G .
Im Beispiel

class Test
{
const int x = 1000000;
const int y = 1000000;

static int F() {


return checked(x * y); // Compile error, overflow
}

static int G() {


return unchecked(x * y); // Returns -727379968
}

static int H() {


return x * y; // Compile error, overflow
}
}

die Überläufe, die beim Auswerten der Konstanten Ausdrücke in auftreten F und H bewirken, dass
Kompilierzeitfehler gemeldet werden, weil die Ausdrücke in einem Kontext ausgewertet werden checked . Ein
Überlauf tritt auch auf, wenn der Konstante Ausdruck in ausgewertet wird G , aber da die Auswertung in einem
Kontext stattfindet unchecked , wird der Überlauf nicht gemeldet.
Die checked unchecked Operatoren und wirken sich nur auf den Überlauf Überprüfungs Kontext für die
Vorgänge aus, die in den ( Token "" und "" textuell enthalten sind ) . Die Operatoren haben keine Auswirkung
auf Funktionsmember, die als Ergebnis der Auswertung des enthaltenen Ausdrucks aufgerufen werden. Im
Beispiel

class Test
{
static int Multiply(int x, int y) {
return x * y;
}

static int F() {


return checked(Multiply(1000000, 1000000));
}
}

die Verwendung von checked in F hat keine Auswirkung auf die Auswertung von x * y in Multiply , daher
x * y wird im Standardkontext der Überlauf Überprüfung ausgewertet.

Der- unchecked Operator ist praktisch, wenn Konstanten der ganzzahligen Typen mit Vorzeichen in hexadezimal
Notation geschrieben werden. Beispiel:

class Test
{
public const int AllBits = unchecked((int)0xFFFFFFFF);

public const int HighBit = unchecked((int)0x80000000);


}

Beide hexadezimal Konstanten oben sind vom Typ uint . Da die Konstanten int ohne den-Operator außerhalb
des Bereichs liegen unchecked , würden die Umwandlungen in int Kompilierzeitfehler verursachen.
checked Mit den unchecked Operatoren und und Anweisungen können Programmierer bestimmte Aspekte
einiger numerischer Berechnungen steuern. Das Verhalten einiger numerischer Operatoren hängt jedoch von
den Datentypen ihrer Operanden ab. Beispielsweise führt die Multiplikation von zwei Dezimalzahlen immer zu
einer Ausnahme bei einem Überlauf, auch innerhalb eines expliziten unchecked Konstrukts. Analog dazu führt
das Multiplizieren von zwei Gleit Komma zahlen niemals zu einer Ausnahme bei einem Überlauf, auch innerhalb
eines expliziten checked Aufbaus Außerdem sind andere Operatoren nie von dem Überprüfungs Modus
betroffen, ob Standard oder explizit.
Standardwert Ausdrücke
Ein Standardwert Ausdruck wird zum Abrufen des Standardwerts (Standardwerte) eines Typs verwendet. In der
Regel wird ein Standardwert Ausdruck für Typparameter verwendet, da möglicherweise nicht bekannt ist, ob der
Typparameter ein Werttyp oder ein Verweistyp ist. (Es ist keine Konvertierung vom null Literaltyp zu einem
Typparameter vorhanden, es sei denn, der Typparameter ist ein Referenztyp.)

default_value_expression
: 'default' '(' type ')'
;

Wenn der Typ in einem default_value_expression zur Laufzeit als Verweistyp ausgewertet wird, wird das
Ergebnis null in diesen Typ konvertiert. Wenn der Typ in einer default_value_expression zur Laufzeit auf einen
Werttyp ausgewertet wird, ist das Ergebnis der Standardwert der value_type(Standardkonstruktoren).
Ein default_value_expression ist ein konstanter Ausdruck (Konstante Ausdrücke), wenn der Typ ein Verweistyp
oder ein Typparameter ist, der als Verweistyp bekannt ist (Typparameter Einschränkungen). Außerdem ist ein
default_value_expression ein konstanter Ausdruck, wenn der Typ einem der folgenden Werttypen entspricht:
sbyte , byte , short , ushort , int , uint , long , ulong , char , float , double , decimal , bool oder
ein beliebiger Enumerationstyp.
Nameof-Ausdrücke
Ein nameof_expression wird zum Abrufen des Namens einer Programm Entität als Konstante Zeichenfolge
verwendet.

nameof_expression
: 'nameof' '(' named_entity ')'
;

named_entity
: simple_name
| named_entity_target '.' identifier type_argument_list?
;

named_entity_target
: 'this'
| 'base'
| named_entity
| predefined_type
| qualified_alias_member
;

Der named_entity Operand ist immer ein Ausdruck. Da nameof kein reserviertes Schlüsselwort ist, ist ein
nameof-Ausdruck immer syntaktisch mehrdeutig, wenn der einfache Name aufgerufen wird nameof . Aus
Kompatibilitätsgründen, wenn eine Namenssuche (simple names) des namens nameof erfolgreich ist, wird der
Ausdruck als invocation_expression behandelt, unabhängig davon, ob der Aufruf zulässig ist. Andernfalls
handelt es sich um einen nameof_expression.
Die Bedeutung des named_entity eines nameof_expression ist die Bedeutung des Ausdrucks als Ausdruck. Das
heißt, entweder als Simple_name, als base_access oder als member_access. Wenn die in einfachen Namen und
dem Element Zugriff beschriebene Suche jedoch zu einem Fehler führt, weil ein Instanzmember in einem
statischen Kontext gefunden wurde, erzeugt eine nameof_expression keinen derartigen Fehler.
Es handelt sich um einen Kompilierzeitfehler für eine named_entity die eine Methoden Gruppe für eine
type_argument_list festlegt. Es handelt sich um einen Kompilierzeitfehler für eine named_entity_target , die über
den-Typ verfügen dynamic .
Bei einem nameof_expression handelt es sich um einen konstanten Ausdruck vom Typ string , der zur Laufzeit
keine Auswirkung hat. Insbesondere wird der named_entity nicht ausgewertet, und wird für die konkrete
Zuweisungs Analyse ignoriert (Allgemeine Regeln für einfache Ausdrücke). Der Wert ist der letzte Bezeichner
des named_entity vor der optionalen abschließenden type_argument_list, wie folgt transformiert:
Wenn Sie verwendet wird, wird das Präfix " @ " entfernt.
Jede unicode_escape_sequence wird in das entsprechende Unicode-Zeichen transformiert.
Alle formatting_characters werden entfernt.
Dabei handelt es sich um dieselben Transformationen, die beim Testen der Gleichheit zwischen bezeichlern in
Bezeichner angewendet werden.
TODO: Beispiele
Anonyme Methoden Ausdrücke
Eine anonymous_method_expression ist eine von zwei Möglichkeiten, eine anonyme Funktion zu definieren.
Diese werden in Anonyme Funktions Ausdrückeweiter unten beschrieben.

Unäre Operatoren
Die ? + - Operatoren,,, ! , ~ , ++ ,, -- Cast und await werden als unäre Operatoren bezeichnet.

unary_expression
: primary_expression
| null_conditional_expression
| '+' unary_expression
| '-' unary_expression
| '!' unary_expression
| '~' unary_expression
| pre_increment_expression
| pre_decrement_expression
| cast_expression
| await_expression
| unary_expression_unsafe
;

Wenn der Operand einer unary_expression den Kompilier Zeittyp aufweist dynamic , wird er dynamisch
gebunden (dynamische Bindung). In diesem Fall ist der Kompilier Zeittyp der unary_expression dynamic , und
die unten beschriebene Auflösung erfolgt zur Laufzeit mit dem Lauf Zeittyp des Operanden.
NULL -bedingter Operator
Der NULL bedingte Operator wendet eine Liste von Vorgängen auf seinen Operanden nur an, wenn dieser
Operand nicht NULL ist. Andernfalls ist das Ergebnis der Anwendung des-Operators null .

null_conditional_expression
: primary_expression null_conditional_operations
;

null_conditional_operations
: null_conditional_operations? '?' '.' identifier type_argument_list?
| null_conditional_operations? '?' '[' argument_list ']'
| null_conditional_operations '.' identifier type_argument_list?
| null_conditional_operations '[' argument_list ']'
| null_conditional_operations '(' argument_list? ')'
;

Die Liste der Vorgänge kann Member-und Element Zugriffs Vorgänge (die selbst bedingt NULL sein können)
sowie Aufrufe einschließen.
Der Ausdruck ist z. b. a.b?[0]?.c() ein null_conditional_expression mit einem primary_expression a.b und
null_conditional_operations ?[0] (null bedingter Element Zugriff), ?.c (null bedingter Member-Zugriff) und
() (Aufruf).

Bei einer null_conditional_expression E mit einem primary_expression P ist E0 der Ausdruck, der durch
textuelles Entfernen der führenden ? aus den einzelnen null_conditional_operations von abgerufen wird E .
Konzeptionell E0 ist der Ausdruck, der ausgewertet wird, wenn keine der Null-Überprüfungen, die von den s
dargestellt werden, ? eine findet null .
Außerdem soll E1 der Ausdruck abgerufen werden, der durch textuelles Entfernen der führenden ? aus nur
der ersten des null_conditional_operations in abgerufen wird E . Dies kann zu einem primären Ausdruck
(sofern es nur einen gibt ? ) oder zu einem anderen null_conditional_expression führen.
Wenn z. b. E der Ausdruck ist a.b?[0]?.c() , dann E0 ist der Ausdruck, a.b[0].c() und E1 ist der Ausdruck
a.b[0]?.c() .
Wenn E0 als "Nothing" klassifiziert wird, E wird als "Nothing" klassifiziert. Andernfalls wird E als Wert
klassifiziert.
E0 und E1 werden verwendet, um die Bedeutung von zu bestimmen E :
Wenn E als statement_expression wird, ist die Bedeutung von mit der- E Anweisung identisch.

if ((object)P != null) E1;

mit der Ausnahme, dass P nur einmal ausgewertet wird.


Andernfalls, wenn E0 als "Nothing" klassifiziert wird, tritt ein Kompilierzeitfehler auf.
Andernfalls ist T0 der Typ von E0 .
Wenn ein T0 Typparameter ist, der nicht bekannt ist, dass es sich um einen Verweistyp oder einen
nicht auf NULL festleg baren Werttyp handelt, tritt ein Kompilierzeitfehler auf.
Wenn T0 ein Werttyp ist, der keine NULL-Werte zulässt, ist der Typ von E T0? , und die
Bedeutung von E entspricht.

((object)P == null) ? (T0?)null : E1

mit der Ausnahme, dass P nur einmal ausgewertet wird.


Andernfalls ist der Typ von e t0, und die Bedeutung von e ist identisch mit.

((object)P == null) ? null : E1

mit der Ausnahme, dass P nur einmal ausgewertet wird.

Wenn E1 selbst ein null_conditional_expression ist, werden diese Regeln erneut angewendet, und die Tests
werden null geschachtelt, bis keine weiteren vorhanden sind ? , und der Ausdruck wurde bis zum primären
Ausdruck reduziert E0 .
Wenn der Ausdruck beispielsweise a.b?[0]?.c() als Anweisungs Ausdruck auftritt, wie in der-Anweisung:

a.b?[0]?.c();

seine Bedeutung ist äquivalent zu:

if (a.b != null) a.b[0]?.c();

Das wiederum entspricht:

if (a.b != null) if (a.b[0] != null) a.b[0].c();

Mit Ausnahme von a.b und a.b[0] werden nur einmal ausgewertet.
Wenn Sie in einem Kontext auftritt, in dem ihr Wert verwendet wird, wie in:

var x = a.b?[0]?.c();
und vorausgesetzt, dass der Typ des letzten aufzurufenden aufzurufenden nicht auf NULL festleg baren
Werttypen ist, entspricht seine Bedeutung folgenden Werten:

var x = (a.b == null) ? null : (a.b[0] == null) ? null : a.b[0].c();

mit Ausnahme von a.b und a.b[0] werden nur einmal ausgewertet.
NULL bedingte Ausdrücke als Projektions Initialisierer
Ein NULL bedingter Ausdruck ist nur als member_declarator in einer anonymous_object_creation_expression
zulässig (Ausdrücke zum Erstellen anonymer Objekte), wenn er mit einem (optional NULL bedingten) Member-
Zugriff endet. Diese Anforderung kann wie folgt ausgedrückt werden:

null_conditional_member_access
: primary_expression null_conditional_operations? '?' '.' identifier type_argument_list?
| primary_expression null_conditional_operations '.' identifier type_argument_list?
;

Dies ist ein Sonderfall der Grammatik für null_conditional_expression oben. Die Produktion für
member_declarator in Ausdrücken der anonymen Objekt Erstellung enthält dann nur
null_conditional_member_access.
NULL bedingte Ausdrücke als Anweisungs Ausdrücke
Ein NULL bedingter Ausdruck ist nur als statement_expression (Ausdrucks Anweisungen) zulässig, wenn er mit
einem Aufruf endet. Diese Anforderung kann wie folgt ausgedrückt werden:

null_conditional_invocation_expression
: primary_expression null_conditional_operations '(' argument_list? ')'
;

Dies ist ein Sonderfall der Grammatik für null_conditional_expression oben. Die Produktion für
statement_expression in Ausdrucks Anweisungen enthält dann nur null_conditional_invocation_expression.
Unärer Plus-Operator
Bei einem Vorgang des Formulars +x wird die Überladungs Auflösung des unären Operators (unäre Operator
Überladungs Auflösung) angewendet, um eine bestimmte Operator Implementierung auszuwählen. Der
Operand wird in den Parametertyp des ausgewählten Operators konvertiert, und der Ergebnistyp ist der
Rückgabetyp des Operators. Die vordefinierten unären plus Operatoren sind:

int operator +(int x);


uint operator +(uint x);
long operator +(long x);
ulong operator +(ulong x);
float operator +(float x);
double operator +(double x);
decimal operator +(decimal x);

Für jeden dieser Operatoren ist das Ergebnis einfach der Wert des Operanden.
Unärer Minus-Operator
Bei einem Vorgang des Formulars -x wird die Überladungs Auflösung des unären Operators (unäre Operator
Überladungs Auflösung) angewendet, um eine bestimmte Operator Implementierung auszuwählen. Der
Operand wird in den Parametertyp des ausgewählten Operators konvertiert, und der Ergebnistyp ist der
Rückgabetyp des Operators. Die vordefinierten Negations Operatoren lauten wie folgt:
Ganzzahlige Negation:
int operator -(int x);
long operator -(long x);

Das Ergebnis wird durch Subtrahieren x von 0 (null) berechnet. Wenn der Wert von x der kleinste
darstellbare Wert des Operanden Typs (-2 ^ 31 für int oder-2 ^ 63 für) ist long , kann die
mathematische Negation von x nicht innerhalb des Operanden Typs darstellbar sein. Wenn dies
innerhalb eines checked Kontexts auftritt, wird eine ausgelöst System.OverflowException . Wenn Sie in
einem unchecked Kontext auftritt, ist das Ergebnis der Wert des Operanden, und der Überlauf wird nicht
gemeldet.
Wenn der Operand des Negations Operators vom Typ ist uint , wird er in den Typ konvertiert long ,
und der Ergebnistyp ist long . Eine Ausnahme ist die Regel, die zulässt, dass der int Wert-2147483648
(-2 ^ 31) als dezimales Ganzzahlliteral (ganzzahlige Literale) geschrieben wird.
Wenn der Operand des Negations Operators vom Typ ist ulong , tritt ein Kompilierzeitfehler auf. Eine
Ausnahme ist die Regel, die zulässt, dass der long Wert-9.223.372.036.854.775.808 (-2 ^ 63) als
dezimales Ganzzahlliteral (ganzzahlige Literale) geschrieben wird.
Gleit Komma Negation:

float operator -(float x);


double operator -(double x);

Das Ergebnis ist der Wert von x mit dem zugehörigen Vorzeichen. Wenn x NaN ist, ist das Ergebnis
ebenfalls NaN.
Dezimale Negation:

decimal operator -(decimal x);

Das Ergebnis wird durch Subtrahieren x von 0 (null) berechnet. Die Dezimale Negation entspricht der
Verwendung des unären Minus Operators vom Typ System.Decimal .
Logischer Negationsoperator:
Bei einem Vorgang des Formulars !x wird die Überladungs Auflösung des unären Operators (unäre Operator
Überladungs Auflösung) angewendet, um eine bestimmte Operator Implementierung auszuwählen. Der
Operand wird in den Parametertyp des ausgewählten Operators konvertiert, und der Ergebnistyp ist der
Rückgabetyp des Operators. Es ist nur ein vordefinierter logischer Negations Operator vorhanden:

bool operator !(bool x);

Dieser Operator berechnet die logische Negation des Operanden: Wenn der Operand ist true , ist das Ergebnis
false . Wenn der Operand false ist, ist das Ergebnis true .

Bitweiser Komplementoperator
Bei einem Vorgang des Formulars ~x wird die Überladungs Auflösung des unären Operators (unäre Operator
Überladungs Auflösung) angewendet, um eine bestimmte Operator Implementierung auszuwählen. Der
Operand wird in den Parametertyp des ausgewählten Operators konvertiert, und der Ergebnistyp ist der
Rückgabetyp des Operators. Die vordefinierten bitweisen Komplement Operatoren sind:
int operator ~(int x);
uint operator ~(uint x);
long operator ~(long x);
ulong operator ~(ulong x);

Für jeden dieser Operatoren ist das Ergebnis des Vorgangs das bitweise Komplement von x .
Jeder Enumerationstyp E stellt implizit den folgenden bitweisen Komplement Operator bereit:

E operator ~(E x);

Das Ergebnis der Auswertung von ~x , wobei x ein Ausdruck eines Enumerationstyps E mit einem zugrunde
liegenden Typ ist U , ist identisch mit dem Auswerten von (E)(~(U)x) , mit der Ausnahme, dass die
Konvertierung in E immer als if in einem Kontext ausgeführt wird (die aktivierten und deaktivierten unchecked
Operatoren).
Präfix-Inkrementoperator und Präfix-Dekrementoperator

pre_increment_expression
: '++' unary_expression
;

pre_decrement_expression
: '--' unary_expression
;

Der Operand eines Präfix Inkrement-oder dekrementvorgangs muss ein Ausdruck sein, der als Variable,
Eigenschafts Zugriff oder Indexerzugriff klassifiziert ist. Das Ergebnis des Vorgangs ist ein Wert desselben Typs
wie der Operand.
Wenn der Operand einer Präfix Inkrement-oder Dekrementoperation eine Eigenschaft oder ein Indexer-Zugriff
ist, muss die Eigenschaft oder der Indexer sowohl einen get -als auch einen- set Accessor aufweisen. Wenn
dies nicht der Fall ist, tritt ein Bindungs Zeitfehler auf.
Unäre Operator Überladungs Auflösung (unäre Operator Überladungs Auflösung) wird angewendet, um eine
bestimmte Operator Implementierung auszuwählen. ++ -- Für die folgenden Typen sind vordefinierte-und-
Operatoren vorhanden: sbyte , byte , short , ushort , int , uint , long , ulong , char , float , double ,
decimal und ein beliebiger Enumerationstyp. Die vordefinierten ++ Operatoren geben den Wert zurück, der
beim Hinzufügen von 1 zum Operanden erzeugt wurde, und die vordefinierten -- Operatoren geben den Wert
zurück, der durch die Subtraktion von 1 vom Operanden erzeugt wird. Wenn in einem checked Kontext das
Ergebnis dieser Addition oder Subtraktion außerhalb des Bereichs des Ergebnis Typs liegt und der Ergebnistyp
ein ganzzahliger Typ oder Enumeration-Typ ist, System.OverflowException wird eine ausgelöst.
Die Lauf Zeit Verarbeitung einer Präfix Inkrement-oder Dekrementoperation des Formulars ++x oder --x
besteht aus den folgenden Schritten:
Wenn x als Variable klassifiziert ist:
x wird ausgewertet, um die Variable zu entwickeln.
Der ausgewählte Operator wird mit dem Wert von x als sein Argument aufgerufen.
Der vom-Operator zurückgegebene Wert wird an dem Speicherort gespeichert, der durch die
Auswertung von angegeben wird x .
Der vom Operator zurückgegebene Wert wird zum Ergebnis des Vorgangs.
Wenn x als Eigenschaft oder Indexer-Zugriff klassifiziert ist:
Der Instanzausdruck (wenn x nicht ist static ), und die Argumentliste (wenn x ein Indexer-Zugriff
ist), die zugeordnet ist x , wird ausgewertet, und die Ergebnisse werden in den nachfolgenden get -
und-Accessoraufrufen verwendet set .
Der- get Accessor von x wird aufgerufen.
Der ausgewählte Operator wird mit dem Wert aufgerufen, der vom get Accessor als Argument
zurückgegeben wurde.
Der- set Accessor von x wird mit dem Wert aufgerufen, der vom Operator als sein Argument
zurückgegeben wurde value .
Der vom Operator zurückgegebene Wert wird zum Ergebnis des Vorgangs.
Die ++ -- Operatoren und unterstützen auch postfix-Notation (postfix-Inkrement-und Dekrementoperatoren).
In der Regel ist das Ergebnis von x++ oder x-- der Wert von x vor dem Vorgang, wohingegen das Ergebnis
von ++x oder --x der Wert von x nach dem Vorgang ist. In jedem Fall x hat nach dem Vorgang denselben
Wert.
Eine- operator++ oder- operator-- Implementierung kann entweder mithilfe der Postfix-oder Präfix Notation
aufgerufen werden. Es ist nicht möglich, separate Operator Implementierungen für die beiden Notationen zu
haben.
cast-Ausdrücke
Ein cast_expression wird verwendet, um einen Ausdruck explizit in einen bestimmten Typ zu konvertieren.

cast_expression
: '(' type ')' unary_expression
;

Eine cast_expression des Formulars (T)E , wobei T ein- Typ und E ein- unary_expression ist, führt eine
explizite Konvertierung (explizite Konvertierungen) des Werts von E in den Typ aus T . Wenn keine explizite
Konvertierung von E in vorhanden T ist, tritt ein Bindungs Zeitfehler auf. Andernfalls ist das Ergebnis der
Wert, der von der expliziten Konvertierung erzeugt wird. Das Ergebnis wird immer als Wert klassifiziert, auch
wenn E eine Variable bezeichnet.
Die Grammatik für eine cast_expression führt zu bestimmten syntaktischen Mehrdeutigkeiten. Der Ausdruck
könnte z. b. (x)-y entweder als cast_expression (eine -y Umwandlung von in den-Typ x ) oder als
additive_expression in Kombination mit einem parenthesized_expression (der den Wert berechnet) interpretiert
werden x - y) .
Um cast_expression Mehrdeutigkeiten aufzulösen, ist die folgende Regel vorhanden: eine Sequenz von einem
oder mehreren Token(Leerraum), die in Klammern eingeschlossen ist, wird nur als Anfang eines cast_expression
betrachtet, wenn mindestens einer der folgenden Punkte zutrifft:
Die Reihenfolge der Token ist die korrekte Grammatik für einen Typ, jedoch nicht für einen Ausdruck.
Die Reihenfolge der Token ist für einen Typ eine korrekte Grammatik, und das Token, das unmittelbar auf die
schließenden Klammern folgt, ist das Token " ~ ", das Token " ! ", das Token " ( ", ein Bezeichner
(Escapesequenzen für Unicode-Zeichen), ein Literalzeichen (Literale) oder ein beliebiges Schlüsselwort
(Schlüsselwörter) außer as und is .

Der Begriff "korrekte Grammatik" oben bedeutet nur, dass die Abfolge der Token der jeweiligen
grammatikerischen Produktion entsprechen muss. Dabei wird insbesondere nicht die tatsächliche Bedeutung
von einzelnen Bezeichner berücksichtigt. Wenn z. b. x -und-Bezeichner y sind, dann x.y ist die korrekte
Grammatik für einen Typ, auch wenn x.y nicht tatsächlich einen Typ bezeichnet.
Aus der disambiguationrule folgt, dass, wenn und Bezeichner sind,, x y (x)y (x)(y) und (x)(-y)
cast_expression s sind, aber (x)-y nicht, auch wenn x einen Typ identifiziert. Wenn jedoch x ein
Schlüsselwort ist, das einen vordefinierten Typ identifiziert (z. b. int ), werden alle vier Formulare
cast_expression s (da ein solches Schlüsselwort nicht möglicherweise ein Ausdruck allein sein kann).
Erwartungs Ausdrücke
Der Erwartungs Operator wird verwendet, um die Auswertung der einschließenden Async-Funktion anzuhalten,
bis der asynchrone Vorgang, der durch den Operanden dargestellt wird, abgeschlossen ist.

await_expression
: 'await' unary_expression
;

Ein- await_expression ist nur im Text einer Async-Funktion (Async-Funktionen) zulässig. In der nächsten
einschließenden asynchronen Funktion tritt möglicherweise keine await_expression an folgenden Stellen auf:
Innerhalb einer geschachtelten (nicht Async) anonymen Funktion
Innerhalb des Blocks einer lock_statement
In einem unsicheren Kontext
Beachten Sie, dass ein await_expression an den meisten Stellen innerhalb eines query_expression nicht
vorkommen kann, da diese syntaktisch transformiert werden, um nicht asynchrone Lambda-Ausdrücke zu
verwenden.
Innerhalb einer Async-Funktion await kann nicht als Bezeichner verwendet werden. Daher gibt es keine
syntaktische Mehrdeutigkeit zwischen "Erwartung-Ausdrücke" und verschiedenen Ausdrücken mit bezeichmern.
Außerhalb der Async-Funktionen await fungiert als normaler Bezeichner.
Der Operand eines await_expression wird als *Task _ bezeichnet. Sie stellt einen asynchronen Vorgang dar, der
zum Zeitpunkt der Auswertung der _await_expression * möglicherweise nicht ausgeführt wird. Der erwartete
Operator besteht darin, die Ausführung der einschließenden Async-Funktion anzuhalten, bis die erwartete
Aufgabe abgeschlossen ist, und dann das Ergebnis zu erhalten.
Awanutzbare Ausdrücke
Die Aufgabe eines Erwartungs Ausdrucks muss er war tet werden. t Wenn eine der folgenden Punkte
enthält, kann ein Ausdruck ausgenutzt werden:
t weist den Kompilier Zeittyp auf dynamic
t verfügt über eine barrierefreie Instanz-oder Erweiterungsmethode GetAwaiter , die ohne Parameter und
ohne Typparameter aufgerufen wird, sowie über einen Rückgabetyp, A für den die folgenden Werte gelten:
A implementiert die-Schnittstelle System.Runtime.CompilerServices.INotifyCompletion (im folgenden
als INotifyCompletion aus Gründen der Kürze bekannt).
A verfügt über eine barrierefreie, lesbare IsCompleted Instanzeigenschaft vom Typ. bool
A verfügt über eine barrierefreie Instanzmethode GetResult ohne Parameter und ohne
Typparameter.
Der Zweck der- GetAwaiter Methode ist das Abrufen eines *akellner _ für die Aufgabe. Der Typ A wird als _
akellnertyp* für den Erwartungs Ausdruck bezeichnet.
Mit der- IsCompleted Eigenschaft wird bestimmt, ob die Aufgabe bereits beendet ist. Wenn dies der Fall ist,
muss die Evaluierung nicht angehalten werden.
Der Zweck der- INotifyCompletion.OnCompleted Methode besteht darin, eine "Fortsetzung" für die Aufgabe zu
registrieren, d. h. einen Delegaten (vom Typ System.Action ), der nach Abschluss der Aufgabe aufgerufen wird.
Der Zweck der- GetResult Methode besteht darin, das Ergebnis der Aufgabe zu erhalten, sobald Sie fertig ist.
Dieses Ergebnis kann erfolgreich abgeschlossen werden, möglicherweise mit einem Ergebniswert, oder es
handelt sich um eine Ausnahme, die von der-Methode ausgelöst wird GetResult .
Klassifizierung von Erwartungs Ausdrücken
Der Ausdruck await t wird auf die gleiche Weise klassifiziert wie der Ausdruck (t).GetAwaiter().GetResult() .
Wenn der Rückgabetyp von den Wert GetResult void hat, wird der await_expression daher als "Nothing"
klassifiziert. Wenn Sie einen nicht leeren Rückgabetyp aufweist T , wird die await_expression als Wert des Typs
klassifiziert T .
Lauf Zeit Auswertung von Erwartungs Ausdrücken
Zur Laufzeit wird der Ausdruck await t wie folgt ausgewertet:
Ein akellner a wird durch Auswerten des Ausdrucks abgerufen (t).GetAwaiter() .
Eine bool b wird durch Auswerten des Ausdrucks abgerufen (a).IsCompleted .
Wenn b false die Auswertung von ist, hängt davon ab, ob a die-Schnittstelle implementiert
System.Runtime.CompilerServices.ICriticalNotifyCompletion (im folgenden als ICriticalNotifyCompletion
aus Gründen der Kürze bekannt). Diese Überprüfung erfolgt zur Bindungs Zeit. a , wenn beispielsweise zur
Laufzeit, wenn den Kompilier Zeittyp dynamic und zur Kompilierzeit aufweist. r Bezeichnen Sie den
Wiederaufnahme Delegaten (Async-Funktionen):
Wenn a nicht implementiert ICriticalNotifyCompletion , wird der Ausdruck
(a as (INotifyCompletion)).OnCompleted(r) ausgewertet.
Wenn a implementiert ICriticalNotifyCompletion , wird der Ausdruck
(a as (ICriticalNotifyCompletion)).UnsafeOnCompleted(r) ausgewertet.
Die Auswertung wird dann angehalten, und die Steuerung wird an den aktuellen Aufrufer der Async-
Funktion zurückgegeben.
Der Ausdruck wird entweder unmittelbar nach (wenn b was true ) oder nach einem späteren Aufruf des
Wiederaufnahme Delegaten (wenn b Was ist false ) (a).GetResult() ausgewertet. Wenn ein Wert
zurückgegeben wird, ist dieser Wert das Ergebnis der await_expression. Andernfalls ist das Ergebnis
"Nothing".
Die Implementierung der Schnittstellen Methoden eines akellers INotifyCompletion.OnCompleted und
ICriticalNotifyCompletion.UnsafeOnCompleted sollte bewirken r , dass der Delegat höchstens einmal
aufgerufen wird. Andernfalls ist das Verhalten der einschließenden Async-Funktion nicht definiert.

Arithmetische operatoren
Die * / % Operatoren,,, + und - werden als arithmetische Operatoren bezeichnet.

multiplicative_expression
: unary_expression
| multiplicative_expression '*' unary_expression
| multiplicative_expression '/' unary_expression
| multiplicative_expression '%' unary_expression
;

additive_expression
: multiplicative_expression
| additive_expression '+' multiplicative_expression
| additive_expression '-' multiplicative_expression
;

Wenn ein Operand eines arithmetischen Operators den Kompilier Zeittyp aufweist dynamic , wird der Ausdruck
dynamisch gebunden (dynamische Bindung). In diesem Fall ist der Kompilier Zeittyp des Ausdrucks dynamic ,
und die unten beschriebene Auflösung erfolgt zur Laufzeit mit dem Lauf Zeittyp der Operanden, die den
Kompilier Zeittyp aufweisen dynamic .
Multiplikationsoperator
Bei einem Vorgang des Formulars x * y wird die binäre Operator Überladungs Auflösung (binäre Operator
Überladungs Auflösung) angewendet, um eine bestimmte Operator Implementierung auszuwählen. Die
Operanden werden in die Parametertypen des ausgewählten Operators konvertiert, und der Ergebnistyp ist der
Rückgabetyp des Operators.
Die vordefinierten Multiplikations Operatoren sind unten aufgeführt. Alle Operatoren berechnen das Produkt
von x und y .
Integer-Multiplikation:

int operator *(int x, int y);


uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);

Wenn sich checked das Produkt in einem Kontext außerhalb des Bereichs des Ergebnis Typs befindet,
wird eine ausgelöst System.OverflowException . In einem unchecked Kontext werden Überläufe nicht
gemeldet, und alle signifikanten höherwertigen Bits außerhalb des Bereichs des Ergebnis Typs werden
verworfen.
Gleit Komma Multiplikation:

float operator *(float x, float y);


double operator *(double x, double y);

Das Produkt wird gemäß den Regeln der IEEE 754-Arithmetik berechnet. In der folgenden Tabelle sind die
Ergebnisse für alle möglichen Kombinationen aus endlichen Werten ungleich Null, Nullen, unendlichen
Werten und NaN-Werten aufgeführt. In der Tabelle sind x und y positive endliche Werte. z ist das
Ergebnis von x * y . Wenn das Ergebnis für den Zieltyp zu groß ist, ist z ein unendlicher Wert. Wenn
das Ergebnis für den Zieltyp zu klein ist, ist z Null.

+y -y +0 -0 +inf -inf NaN

+x +z -Z +0 -0 +inf -inf NaN

-X -Z +z -0 +0 -inf +inf NaN

+0 +0 -0 +0 -0 NaN NaN NaN

-0 -0 +0 -0 +0 NaN NaN NaN

+inf +inf -inf NaN NaN +inf -inf NaN

-inf -inf +inf NaN NaN -inf +inf NaN

NaN NaN NaN NaN NaN NaN NaN NaN

Dezimale Multiplikation:

decimal operator *(decimal x, decimal y);

Wenn der resultierende Wert zu groß für die Darstellung im- decimal Format ist,
System.OverflowException wird eine ausgelöst. Wenn der Ergebniswert zu klein ist, um im decimal
Format darzustellen, ist das Ergebnis 0 (null). Die Dezimalstellen des Ergebnisses, vor der Rundung, sind
die Summe der Skalen der beiden Operanden.
Die Dezimal Multiplikation entspricht der Verwendung des Multiplikations Operators vom Typ
System.Decimal .

Divisionsoperator
Bei einem Vorgang des Formulars x / y wird die binäre Operator Überladungs Auflösung (binäre Operator
Überladungs Auflösung) angewendet, um eine bestimmte Operator Implementierung auszuwählen. Die
Operanden werden in die Parametertypen des ausgewählten Operators konvertiert, und der Ergebnistyp ist der
Rückgabetyp des Operators.
Die vordefinierten Divisions Operatoren sind unten aufgeführt. Die Operatoren berechnen alle den Quotienten
von x und y .
Ganzzahlige Division:

int operator /(int x, int y);


uint operator /(uint x, uint y);
long operator /(long x, long y);
ulong operator /(ulong x, ulong y);

Wenn der Wert des rechten Operanden 0 (null) ist, wird eine ausgelöst System.DivideByZeroException .
Die Division rundet das Ergebnis auf 0 (null). Daher ist der absolute Wert des Ergebnisses die
größtmögliche Ganzzahl, die kleiner oder gleich dem absoluten Wert des Quotienten der beiden
Operanden ist. Das Ergebnis ist 0 (null) oder positiv, wenn die beiden Operanden das gleiche Vorzeichen
aufweisen und 0 (null) oder negativ ist, wenn die beiden Operanden gegenteilige Vorzeichen verfügen.
Wenn der linke Operand der kleinste darstellbare int -oder long -Wert und der rechte Operand ist -1
, tritt ein Überlauf auf. In einem checked Kontext bewirkt dies, System.ArithmeticException dass eine
(oder eine Unterklasse davon) ausgelöst wird. In einem unchecked Kontext ist die Implementierung
definiert, ob ein System.ArithmeticException (oder eine Unterklasse) ausgelöst wird, oder der Überlauf
wird nicht gemeldet, und der resultierende Wert ist der des linken Operanden.
Gleit Komma Division:

float operator /(float x, float y);


double operator /(double x, double y);

Der Quotienten wird gemäß den Regeln der IEEE 754-Arithmetik berechnet. In der folgenden Tabelle sind
die Ergebnisse für alle möglichen Kombinationen aus endlichen Werten ungleich Null, Nullen,
unendlichen Werten und NaN-Werten aufgeführt. In der Tabelle sind x und y positive endliche Werte.
z ist das Ergebnis von x / y . Wenn das Ergebnis für den Zieltyp zu groß ist, ist z ein unendlicher
Wert. Wenn das Ergebnis für den Zieltyp zu klein ist, ist z Null.

+y -y +0 -0 +inf -inf NaN

+x +z -Z +inf -inf +0 -0 NaN

-X -Z +z -inf +inf -0 +0 NaN


+0 +0 -0 NaN NaN +0 -0 NaN

-0 -0 +0 NaN NaN -0 +0 NaN

+inf +inf -inf +inf -inf NaN NaN NaN

-inf -inf +inf -inf +inf NaN NaN NaN

NaN NaN NaN NaN NaN NaN NaN NaN

Dezimale Division:

decimal operator /(decimal x, decimal y);

Wenn der Wert des rechten Operanden 0 (null) ist, wird eine ausgelöst System.DivideByZeroException .
Wenn der resultierende Wert zu groß für die Darstellung im- decimal Format ist,
System.OverflowException wird eine ausgelöst. Wenn der Ergebniswert zu klein ist, um im decimal
Format darzustellen, ist das Ergebnis 0 (null). Die Skala des Ergebnisses ist die kleinste Skala, die ein
Ergebnis mit dem nächstgelegenen darstellbaren Dezimalwert dem tatsächlichen mathematischen
Ergebnis beibehält.
Die Dezimal Division entspricht der Verwendung des Divisions Operators vom Typ System.Decimal .
Restoperator
Bei einem Vorgang des Formulars x % y wird die binäre Operator Überladungs Auflösung (binäre Operator
Überladungs Auflösung) angewendet, um eine bestimmte Operator Implementierung auszuwählen. Die
Operanden werden in die Parametertypen des ausgewählten Operators konvertiert, und der Ergebnistyp ist der
Rückgabetyp des Operators.
Die vordefinierten Rest-Operatoren sind unten aufgeführt. Die Operatoren berechnen alle den Rest der Division
zwischen x und y .
Ganzzahliger Rest:

int operator %(int x, int y);


uint operator %(uint x, uint y);
long operator %(long x, long y);
ulong operator %(ulong x, ulong y);

Das Ergebnis von x % y ist der Wert, der von erzeugt wird x - (x / y) * y . Wenn y 0 (null) ist, wird
eine ausgelöst System.DivideByZeroException .
Wenn der linke Operand der kleinste int -oder long -Wert und der rechte Operand ist -1 ,
System.OverflowException wird eine ausgelöst. In keinem Fall löst x % y eine Ausnahme aus, bei der
x / y keine Ausnahme ausgelöst wird.

Gleit Komma Restwert:

float operator %(float x, float y);


double operator %(double x, double y);

In der folgenden Tabelle sind die Ergebnisse für alle möglichen Kombinationen aus endlichen Werten
ungleich Null, Nullen, unendlichen Werten und NaN-Werten aufgeführt. In der Tabelle sind x und y
positive endliche Werte. z Das Ergebnis von x % y und wird als berechnet x - n * y , wobei n die
größtmögliche Ganzzahl ist, die kleiner oder gleich ist x / y . Diese Methode zum Berechnen des
Restwerts entspricht der für ganzzahligen Operanden, unterscheidet sich jedoch von der IEEE 754-
Definition (bei der n es sich um die ganze Zahl handelt, die am nächsten ist x / y ).

+y -y +0 -0 +inf -inf NaN

+x +z +z NaN NaN x x NaN

-X -Z -Z NaN NaN -X -X NaN

+0 +0 +0 NaN NaN +0 +0 NaN

-0 -0 -0 NaN NaN -0 -0 NaN

+inf NaN NaN NaN NaN NaN NaN NaN

-inf NaN NaN NaN NaN NaN NaN NaN

NaN NaN NaN NaN NaN NaN NaN NaN

Dezimal Restwert:

decimal operator %(decimal x, decimal y);

Wenn der Wert des rechten Operanden 0 (null) ist, wird eine ausgelöst System.DivideByZeroException .
Die Dezimalstellen des Ergebnisses, vor der Rundung, sind die größere der Skalen der beiden Operanden,
und das Vorzeichen des Ergebnisses ist, wenn ungleich NULL, mit dem von identisch x .
Der Dezimal Rest entspricht der Verwendung des Rest-Operators vom Typ System.Decimal .
Additionsoperator
Bei einem Vorgang des Formulars x + y wird die binäre Operator Überladungs Auflösung (binäre Operator
Überladungs Auflösung) angewendet, um eine bestimmte Operator Implementierung auszuwählen. Die
Operanden werden in die Parametertypen des ausgewählten Operators konvertiert, und der Ergebnistyp ist der
Rückgabetyp des Operators.
Die vordefinierten Additions Operatoren sind unten aufgeführt. Für numerische und Enumerationstypen
berechnen die vordefinierten Additions Operatoren die Summe der beiden Operanden. Wenn ein oder beide
Operanden vom Typ Zeichenfolge sind, verketten die vordefinierten Additions Operatoren die Zeichen folgen
Darstellung der Operanden.
Ganzzahlige Addition:

int operator +(int x, int y);


uint operator +(uint x, uint y);
long operator +(long x, long y);
ulong operator +(ulong x, ulong y);

In einem checked Kontext wird eine ausgelöst, wenn die Summe außerhalb des Bereichs des Ergebnis
Typs liegt System.OverflowException . In einem unchecked Kontext werden Überläufe nicht gemeldet, und
alle signifikanten höherwertigen Bits außerhalb des Bereichs des Ergebnis Typs werden verworfen.
Gleit Komma Addition:

float operator +(float x, float y);


double operator +(double x, double y);

Die Summe wird gemäß den Regeln der IEEE 754-Arithmetik berechnet. In der folgenden Tabelle sind die
Ergebnisse für alle möglichen Kombinationen aus endlichen Werten ungleich Null, Nullen, unendlichen
Werten und NaN-Werten aufgeführt. In der Tabelle sind x und y endliche Werte ungleich Null, und z
ist das Ergebnis aus x + y . Wenn x und y die gleiche Größe jedoch andere Vorzeichen haben, ist z
eine positive Null. Wenn zu x + y groß ist, um im Zieltyp darzustellen, z ist ein Unendlichkeits Wert
mit demselben Vorzeichen wie x + y .

Y +0 -0 +inf -inf NaN

w z x x +inf -inf NaN

+0 Y +0 +0 +inf -inf NaN

-0 Y +0 -0 +inf -inf NaN

+inf +inf +inf +inf +inf NaN NaN

-inf -inf -inf -inf NaN -inf NaN

NaN NaN NaN NaN NaN NaN NaN

Dezimal Addition:

decimal operator +(decimal x, decimal y);

Wenn der resultierende Wert zu groß für die Darstellung im- decimal Format ist,
System.OverflowException wird eine ausgelöst. Die Dezimalstellen des Ergebnisses, vor der Rundung, sind
die größere der Skalen der beiden Operanden.
Die Dezimal Addition entspricht der Verwendung des Additions Operators vom Typ System.Decimal .
Enumerationsaddition. Jeder Enumerationstyp stellt implizit die folgenden vordefinierten Operatoren
bereit, wobei E der Enumerationstyp und U der zugrunde liegende Typ von ist E :

E operator +(E x, U y);


E operator +(U x, E y);

Zur Laufzeit werden diese Operatoren genau wie ausgewertet (E)((U)x + (U)y) .
Verkettung von Zeichen folgen:

string operator +(string x, string y);


string operator +(string x, object y);
string operator +(object x, string y);
Diese über Ladungen des binären + Operators führen die Verkettung von Zeichen folgen aus. Wenn ein
Operand der Zeichen folgen Verkettung ist null , wird eine leere Zeichenfolge ersetzt. Andernfalls wird
jedes nicht-Zeichen folgen Argument in seine Zeichen folgen Darstellung konvertiert, indem die
ToString vom Typ geerbte virtuelle Methode aufgerufen wird object . Wenn ToString zurückgibt
null , wird eine leere Zeichenfolge ersetzt.

using System;

class Test
{
static void Main() {
string s = null;
Console.WriteLine("s = >" + s + "<"); // displays s = ><
int i = 1;
Console.WriteLine("i = " + i); // displays i = 1
float f = 1.2300E+15F;
Console.WriteLine("f = " + f); // displays f = 1.23E+15
decimal d = 2.900m;
Console.WriteLine("d = " + d); // displays d = 2.900
}
}

Das Ergebnis des Zeichenfolgenverkettungs-Operators ist eine Zeichenfolge, die aus den Zeichen des
linken Operanden besteht, gefolgt von den Zeichen des rechten Operanden. Der Operator für die Zeichen
folgen Verkettung gibt niemals einen null Wert zurück. Eine System.OutOfMemoryException kann
ausgelöst werden, wenn nicht genügend Arbeitsspeicher zur Verfügung steht, um die resultierende
Zeichenfolge zuzuordnen.
Delegatkombination. Jeder Delegattyp stellt implizit den folgenden vordefinierten Operator bereit, wobei
D der Delegattyp ist:

D operator +(D x, D y);

Der binäre + Operator führt eine Delegatkombination aus, wenn beide Operanden einen Delegattyp
haben D . (Wenn die Operanden unterschiedliche Delegattypen aufweisen, tritt ein Bindungs Zeitfehler
auf.) Wenn der erste Operand ist null , ist das Ergebnis der Operation der Wert des zweiten Operanden
(auch wenn dies ebenfalls der Wert ist null ). Andernfalls ist der zweite Operand, wenn der zweite
Operand ist null , das Ergebnis des Vorgangs der Wert des ersten Operanden. Andernfalls ist das
Ergebnis des Vorgangs eine neue Delegatinstanz, die beim Aufrufen den ersten Operanden aufruft und
dann den zweiten Operanden aufruft. Beispiele für die Kombination von Delegaten finden Sie unter
Subtraktions Operator und Delegataufruf. Da System.Delegate kein Delegattyp ist, operator + ist für
ihn nicht definiert.
Subtraktionsoperator
Bei einem Vorgang des Formulars x - y wird die binäre Operator Überladungs Auflösung (binäre Operator
Überladungs Auflösung) angewendet, um eine bestimmte Operator Implementierung auszuwählen. Die
Operanden werden in die Parametertypen des ausgewählten Operators konvertiert, und der Ergebnistyp ist der
Rückgabetyp des Operators.
Die vordefinierten Subtraktions Operatoren sind unten aufgeführt. Die Operatoren, die alle von subtrahieren y
x .

Ganzzahlige Subtraktion:
int operator -(int x, int y);
uint operator -(uint x, uint y);
long operator -(long x, long y);
ulong operator -(ulong x, ulong y);

Wenn sich checked der Unterschied in einem Kontext außerhalb des Bereichs des Ergebnis Typs befindet,
wird eine ausgelöst System.OverflowException . In einem unchecked Kontext werden Überläufe nicht
gemeldet, und alle signifikanten höherwertigen Bits außerhalb des Bereichs des Ergebnis Typs werden
verworfen.
Gleit Komma Subtraktion:

float operator -(float x, float y);


double operator -(double x, double y);

Der Unterschied wird gemäß den Regeln der IEEE 754-Arithmetik berechnet. In der folgenden Tabelle
werden die Ergebnisse aller möglichen Kombinationen von nicht NULL Endwerten, Nullen, Infinities und
Nane aufgelistet. In der Tabelle sind x und y endliche Werte ungleich Null, und z ist das Ergebnis aus
x - y . Wenn x und y gleich sind, ist z eine positive Null. Wenn zu x - y groß ist, um im Zieltyp
darzustellen, z ist ein Unendlichkeits Wert mit demselben Vorzeichen wie x - y .

Y +0 -0 +inf -inf NaN

w z x x -inf +inf NaN

+0 -y +0 +0 -inf +inf NaN

-0 -y -0 +0 -inf +inf NaN

+inf +inf +inf +inf NaN +inf NaN

-inf -inf -inf -inf -inf NaN NaN

NaN NaN NaN NaN NaN NaN NaN

Dezimale Subtraktion:

decimal operator -(decimal x, decimal y);

Wenn der resultierende Wert zu groß für die Darstellung im- decimal Format ist,
System.OverflowException wird eine ausgelöst. Die Dezimalstellen des Ergebnisses, vor der Rundung, sind
die größere der Skalen der beiden Operanden.
Die dezimale Subtraktion entspricht der Verwendung des Subtraktions Operators vom Typ
System.Decimal .

Enumerationssubtraktion. Jeder Enumerationstyp stellt implizit den folgenden vordefinierten Operator


bereit, wobei E der Enumerationstyp und U der zugrunde liegende Typ von ist E :

U operator -(E x, E y);


Dieser Operator wird genau wie ausgewertet (U)((U)x - (U)y) . Mit anderen Worten, der-Operator
berechnet den Unterschied zwischen den Ordinalwerten von x und y , und der Ergebnistyp ist der
zugrunde liegende Typ der Enumeration.

E operator -(E x, U y);

Dieser Operator wird genau wie ausgewertet (E)((U)x - y) . Mit anderen Worten, der Operator
subtrahiert einen Wert vom zugrunde liegenden Typ der Enumeration und gibt einen Wert der-
Enumeration aus.
Entfernen von Delegaten. Jeder Delegattyp stellt implizit den folgenden vordefinierten Operator bereit,
wobei D der Delegattyp ist:

D operator -(D x, D y);

Der binäre - Operator führt eine Delegatentfernung aus, wenn beide Operanden einen Delegattyp
haben D . Wenn die Operanden unterschiedliche Delegattypen aufweisen, tritt ein Bindungs Zeitfehler
auf. Ist der erste Operand null , ist das Ergebnis des Vorgangs null . Andernfalls ist der zweite Operand,
wenn der zweite Operand ist null , das Ergebnis des Vorgangs der Wert des ersten Operanden.
Andernfalls stellen beide Operanden Aufruf Listen (Delegatdeklarationen) dar, die über mindestens einen
Eintrag verfügen, und das Ergebnis ist eine neue Aufruf Liste, die aus der ersten Operanden Liste besteht,
aus der die Einträge des zweiten Operanden entfernt wurden, vorausgesetzt, die Liste des zweiten
Operanden ist eine ordnungsgemäße zusammenhängende unter Liste der ersten. (Um die
Übereinstimmung zu ermitteln, werden die entsprechenden Einträge als für den Delegat-Gleichheits
Operator (Delegat-Gleichheits Operatoren) verglichen.) Andernfalls ist das Ergebnis der Wert des linken
Operanden. Die Listen der Operanden werden im Prozess nicht geändert. Wenn die Liste des zweiten
Operanden mit mehreren Unterlisten zusammenhängender Einträge in der Liste der ersten Operanden
übereinstimmt, wird die am weitesten rechts gerichtete unter Liste von zusammenhängenden Einträgen
entfernt. Sollte durch die Entfernung eine leere Liste entstehen, ist das Ergebnis null . Beispiel:
delegate void D(int x);

class C
{
public static void M1(int i) { /* ... */ }
public static void M2(int i) { /* ... */ }
}

class Test
{
static void Main() {
D cd1 = new D(C.M1);
D cd2 = new D(C.M2);
D cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd1; // => M1 + M2 + M2

cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1


cd3 -= cd1 + cd2; // => M2 + M1

cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1


cd3 -= cd2 + cd2; // => M1 + M1

cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1


cd3 -= cd2 + cd1; // => M1 + M2

cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1


cd3 -= cd1 + cd1; // => M1 + M2 + M2 + M1
}
}

Schiebeoperatoren
Die << >> Operatoren und werden verwendet, um Bitverschiebungs Vorgänge auszuführen.

shift_expression
: additive_expression
| shift_expression '<<' additive_expression
| shift_expression right_shift additive_expression
;

Wenn ein Operand eines shift_expression den Kompilier Zeittyp aufweist dynamic , wird der Ausdruck
dynamisch gebunden (dynamische Bindung). In diesem Fall ist der Kompilier Zeittyp des Ausdrucks dynamic ,
und die unten beschriebene Auflösung erfolgt zur Laufzeit mit dem Lauf Zeittyp der Operanden, die den
Kompilier Zeittyp aufweisen dynamic .
Bei einem Vorgang des Formulars x << count oder x >> count wird die binäre Operator Überladungs
Auflösung (binäre Operator Überladungs Auflösung) angewendet, um eine bestimmte Operator
Implementierung auszuwählen. Die Operanden werden in die Parametertypen des ausgewählten Operators
konvertiert, und der Ergebnistyp ist der Rückgabetyp des Operators.
Beim Deklarieren eines überladenen Verschiebungs Operators muss der Typ des ersten Operanden immer die
Klasse oder Struktur sein, die die Operator Deklaration enthält, und der Typ des zweiten Operanden muss immer
sein int .
Die vordefinierten Shift-Operatoren sind unten aufgeführt.
Nach links verschieben:
int operator <<(int x, int count);
uint operator <<(uint x, int count);
long operator <<(long x, int count);
ulong operator <<(ulong x, int count);

Der << Operator verschiebt x nach links um eine Anzahl von Bits, die wie unten beschrieben berechnet
werden.
Die höherwertigen Bits außerhalb des Bereichs des Ergebnis Typs von x werden verworfen, die
restlichen Bits werden nach links verschoben, und die unteren leeren Bitpositionen werden auf 0 (null)
festgelegt.
Nach rechts verschieben:

int operator >>(int x, int count);


uint operator >>(uint x, int count);
long operator >>(long x, int count);
ulong operator >>(ulong x, int count);

Der >> Operator verschiebt sich x nach rechts um eine Anzahl von Bits, die wie unten beschrieben
berechnet werden.
Wenn x int den Typ oder aufweist long , werden die nieder wertigen Bits von x verworfen, die
restlichen Bits werden nach rechts verschoben, und die übergeordnete leere Bitpositionen werden auf 0
(null) festgelegt, wenn x nicht negativ ist, und auf einen Wert festgelegt, wenn x negativ ist.
Wenn x uint den Typ oder aufweist ulong , werden die nieder wertigen Bits von x verworfen, die
restlichen Bits werden nach rechts verschoben, und die obersten leeren Bitpositionen werden auf 0 (null)
festgelegt.
Für die vordefinierten Operatoren wird die Anzahl der zu Verschiebungs Bits wie folgt berechnet:
Wenn der Typ von x int oder ist uint , wird die UMSCHALT Anzahl durch die nieder wertigen fünf Bits
von angegeben count . Mit anderen Worten: die UMSCHALT Anzahl wird aus berechnet count & 0x1F .
Wenn der Typ von x long oder ist ulong , wird die UMSCHALT Anzahl durch die nieder wertigen sechs
Bits von angegeben count . Mit anderen Worten: die UMSCHALT Anzahl wird aus berechnet count & 0x3F .

Wenn die resultierende Verschiebungs Anzahl 0 (null) ist, geben die Schiebe Operatoren einfach den Wert von
zurück x .
Verschiebungs Vorgänge verursachen niemals Überläufe und erzeugen dieselben Ergebnisse in checked den
unchecked Kontexten und.

Wenn der linke Operand des Operators einen ganzzahligen >> Typ mit Vorzeichen hat, führt der Operator eine
arithmetische Verschiebung nach rechts aus, in der der Wert des signifikantesten Bits (das Signier Bit) des
Operanden an die übergeordnete leere Bitpositionen weitergegeben wird. Wenn der linke Operand des
Operators einen ganzzahligen >> Typ ohne Vorzeichen aufweist, führt der Operator eine logische Schiebe nach
rechts aus, bei der die Positionen für die hohe Reihenfolge von leeren Bitpositionen immer auf NULL festgelegt
sind. Explizite Umwandlungen können verwendet werden, um den umgekehrten Vorgang von auszuführen, der
vom Operanden-Typ abgeleitet wird. Wenn z. b. x eine Variable vom Typ ist int , führt der Vorgang
unchecked((int)((uint)x >> y)) eine logische Verschiebung rechts von aus x .

Relationale und Typtestoperatoren


Die == != < Operatoren,,, > , <= , >= is und as werden als relationale und Typtest Operatoren
bezeichnet.

relational_expression
: shift_expression
| relational_expression '<' shift_expression
| relational_expression '>' shift_expression
| relational_expression '<=' shift_expression
| relational_expression '>=' shift_expression
| relational_expression 'is' type
| relational_expression 'as' type
;

equality_expression
: relational_expression
| equality_expression '==' relational_expression
| equality_expression '!=' relational_expression
;

Der is -Operator wird im is-Operator beschrieben, und der- as Operator wird im as-Operatorbeschrieben.
Die == != < Operatoren,,, > <= und >= sind Vergleichs Operatoren .
Wenn ein Operand eines Vergleichs Operators den Kompilier Zeittyp aufweist dynamic , wird der Ausdruck
dynamisch gebunden (dynamische Bindung). In diesem Fall ist der Kompilier Zeittyp des Ausdrucks dynamic ,
und die unten beschriebene Auflösung erfolgt zur Laufzeit mit dem Lauf Zeittyp der Operanden, die den
Kompilier Zeittyp aufweisen dynamic .
Bei einem Vorgang der Form x op y , bei dem op ein Vergleichs Operator ist, wird die Überladungs Auflösung
(binäre Operator Überladungs Auflösung) angewendet, um eine bestimmte Operator Implementierung
auszuwählen. Die Operanden werden in die Parametertypen des ausgewählten Operators konvertiert, und der
Ergebnistyp ist der Rückgabetyp des Operators.
Die vordefinierten Vergleichs Operatoren werden in den folgenden Abschnitten beschrieben. Alle vordefinierten
Vergleichs Operatoren geben ein Ergebnis vom Typ zurück bool , wie in der folgenden Tabelle beschrieben.

VO RGA N G ERGEB N IS

x == y true``x , wenn gleich ist y , false andernfalls.

x != y true , wenn ungleich x ist y , false andernfalls.

x < y true , wenn x kleiner ist als y , sonst false

x > y true , wenn x größer ist als y , sonst false

x <= y true , wenn x kleiner als oder gleich y , sonst false

x >= y true , wenn x größer als oder gleich y , sonst false

Integer-Vergleichs Operatoren
Die vordefinierten ganzzahligen Vergleichs Operatoren lauten wie folgt:
bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);

bool operator !=(int x, int y);


bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);

bool operator <(int x, int y);


bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);

bool operator >(int x, int y);


bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);

bool operator <=(int x, int y);


bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);

bool operator >=(int x, int y);


bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);

Jeder dieser Operatoren vergleicht die numerischen Werte der beiden ganzzahligen Operanden und gibt einen
bool Wert zurück, der angibt, ob die jeweilige Beziehung true oder ist false .

Vergleichs Operatoren für Gleit Komma Zahlen


Die vordefinierten Gleit Komma Vergleichs Operatoren lauten wie folgt:

bool operator ==(float x, float y);


bool operator ==(double x, double y);

bool operator !=(float x, float y);


bool operator !=(double x, double y);

bool operator <(float x, float y);


bool operator <(double x, double y);

bool operator >(float x, float y);


bool operator >(double x, double y);

bool operator <=(float x, float y);


bool operator <=(double x, double y);

bool operator >=(float x, float y);


bool operator >=(double x, double y);

Die Operanden werden von den Operatoren gemäß den Regeln des IEEE 754-Standards verglichen:
Wenn einer der beiden Operanden NaN ist, gilt das Ergebnis false für alle Operatoren mit Ausnahme
!= von, für die das Ergebnis ist true . Bei zwei-Operanden x != y erzeugt immer dasselbe Ergebnis
wie !(x == y) . Wenn jedoch ein oder beide Operanden NaN sind, führen die < > <= Operatoren,,
und >= nicht zu den gleichen Ergebnissen wie die logische Negation des umgekehrten Operators. Wenn
z. b. einer der beiden Optionen x und y NaN ist, dann x < y ist false , aber !(x >= y) ist true .
Wenn keiner der Operanden NaN ist, vergleichen die Operatoren die Werte der beiden Gleit Komma
Operanden in Bezug auf die Reihenfolge.

-inf < -max < ... < -min < -0.0 == +0.0 < +min < ... < +max < +inf

min dabei sind und die kleinsten und größten positiven Endwerte, die im angegebenen Gleit
max
Komma Format dargestellt werden können. Wichtige Auswirkungen dieser Reihenfolge:
Negative und positive Nullen gelten als gleich.
Minus unendlich gilt als kleiner als alle anderen Werte, aber gleichbedeutend mit einem anderen
negativen unendlich.
Eine positive Unendlichkeit gilt als größer als alle anderen Werte, aber gleichbedeutend mit einer
anderen positiven unendlich.
Dezimale Vergleichs Operatoren
Die vordefinierten dezimalen Vergleichs Operatoren lauten wie folgt:

bool operator ==(decimal x, decimal y);


bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);

Jeder dieser Operatoren vergleicht die numerischen Werte der beiden Decimal-Operanden und gibt einen bool
Wert zurück, der angibt, ob die jeweilige Beziehung true oder ist false . Jeder Dezimal Vergleich entspricht
der Verwendung des entsprechenden relationalen or-Gleichheits Operators vom Typ System.Decimal .
Boolesche Gleichheits Operatoren
Die vordefinierten booleschen Gleichheits Operatoren lauten wie folgt:

bool operator ==(bool x, bool y);


bool operator !=(bool x, bool y);

Das Ergebnis von == ist, true Wenn sowohl x als auch y sind true oder wenn sowohl x als y false
auch gleich sind. Andernfalls ist das Ergebnis false .
Das Ergebnis von != ist, false Wenn sowohl x als auch y sind true oder wenn sowohl x als y false
auch gleich sind. Andernfalls ist das Ergebnis true . Wenn die Operanden den Typ bool haben, != erzeugt der
Operator dasselbe Ergebnis wie der ^ Operator.
Enumerationsvergleichsoperatoren
Jeder Enumerationstyp stellt implizit die folgenden vordefinierten Vergleichs Operatoren bereit:

bool operator ==(E x, E y);


bool operator !=(E x, E y);
bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);

Das Ergebnis der Auswertung von x op y , wobei x und y Ausdrücke eines Enumerationstyps E mit einem
zugrunde liegenden Typ sind U , und op ist einer der Vergleichs Operatoren, ist identisch mit dem Auswerten
von ((U)x) op ((U)y) . Mit anderen Worten, die Vergleichs Operatoren des Enumerationstyps vergleichen
einfach die zugrunde liegenden ganzzahligen Werte der beiden Operanden.
Verweistyp-Gleichheits Operatoren
Die vordefinierten Verweistyp-Gleichheits Operatoren sind:

bool operator ==(object x, object y);


bool operator !=(object x, object y);

Die Operatoren geben das Ergebnis des Vergleichs der beiden Verweise auf Gleichheit oder nicht Gleichheit
zurück.
Da die vordefinierten Verweistyp-Gleichheits Operatoren Operanden vom Typ akzeptieren object , gelten Sie
für alle Typen, die keine anwendbaren Elemente und Member deklarieren operator == operator != . Im
Gegensatz dazu Blenden alle anwendbaren benutzerdefinierten Gleichheits Operatoren die vordefinierten
Verweistyp-Gleichheits Operatoren aus.
Die vordefinierten Verweistyp-Gleichheits Operatoren erfordern eine der folgenden:
Beide Operanden sind ein Wert eines Typs, der bekanntermaßen eine reference_type oder das Literale ist
null . Darüber hinaus ist eine explizite Verweis Konvertierung (explizite Verweis Konvertierungen) vom Typ
eines der beiden Operanden bis zum Typ des anderen Operanden vorhanden.
Ein Operand ist ein Wert vom Typ, T wobei T eine type_parameter und der andere Operand das Literale ist
null . Außerdem T weist nicht die Werttyp Einschränkung auf.

Wenn eine dieser Bedingungen nicht zutrifft, tritt ein Fehler bei der Bindung auf. Wichtige Implikationen dieser
Regeln sind:
Es handelt sich um einen Bindungs Fehler, bei dem die vordefinierten Verweistyp-Gleichheits Operatoren
verwendet werden, um zwei Verweise zu vergleichen, die bekanntermaßen bei der Bindungs Zeit
unterschiedlich sind. Wenn die Bindungs Zeit Typen der Operanden z. b. zwei Klassentypen und sind A B
und weder A noch B vom anderen abgeleitet sind, kann es unmöglich sein, dass die beiden Operanden auf
das gleiche Objekt verweisen. Daher wird der Vorgang als Bindungs Zeit Fehler betrachtet.
Die vordefinierten Verweistyp-Gleichheits Operatoren lassen nicht zu, dass Werttyp Operanden verglichen
werden. Daher ist es nicht möglich, Werte dieses Struktur Typs zu vergleichen, es sei denn, ein Strukturtyp
deklariert seine eigenen Gleichheits Operatoren.
Die vordefinierten Verweistyp-Gleichheits Operatoren bewirken nie, dass Boxing-Vorgänge für ihre
Operanden ausgeführt werden. Es wäre bedeutungslos, solche Boxing-Vorgänge auszuführen, da Verweise
auf die neu zugeordneten geachtelten Instanzen notwendigerweise von allen anderen verweisen abweichen.
Wenn ein Operand eines Typparameter Typs T mit verglichen wird null und der Lauf Zeittyp von T ein
Werttyp ist, ist das Ergebnis des Vergleichs false .

Im folgenden Beispiel wird überprüft, ob ein Argument eines nicht eingeschränkten Typparameter Typs ist null
.

class C<T>
{
void F(T x) {
if (x == null) throw new ArgumentNullException();
...
}
}

Das x == null -Konstrukt ist zulässig T , obwohl einen Werttyp darstellen könnte, und das Ergebnis wird
einfach als definiert, false Wenn T ein Werttyp ist.
Bei einem Vorgang im Formular x == y oder x != y , sofern zutreffend operator == oder operator !=
vorhanden, wählt die Operator Überladungs Auflösung (binäre Operator Überladungs Auflösung) diesen
Operator anstelle des vordefinierten Verweistyp Gleichheits Operators aus. Es ist jedoch immer möglich, den
vordefinierten Verweistyp Gleichheits Operator auszuwählen, indem eine oder beide der Operanden explizit in
den Typ umgewandelt werden object . Das Beispiel

using System;

class Test
{
static void Main() {
string s = "Test";
string t = string.Copy(s);
Console.WriteLine(s == t);
Console.WriteLine((object)s == t);
Console.WriteLine(s == (object)t);
Console.WriteLine((object)s == (object)t);
}
}

erzeugt die Ausgabe

True
False
False
False

Die s t Variablen und verweisen auf zwei unterschiedliche string Instanzen, die die gleichen Zeichen
enthalten. Der erste Vergleichswert True gibt an, dass der vordefinierte Zeichen folgen Gleichheits Operator
(Zeichen folgen GleichheitsOperator) ausgewählt ist, wenn beide Operanden vom Typ sind string . Die
verbleibenden Vergleiche werden alle ausgegeben, False da der vordefinierte Verweistyp Gleichheits Operator
ausgewählt wird, wenn einer oder beide Operanden den Typ haben object .
Beachten Sie, dass die oben beschriebene Technik für Werttypen nicht sinnvoll ist. Das Beispiel

class Test
{
static void Main() {
int i = 123;
int j = 123;
System.Console.WriteLine((object)i == (object)j);
}
}

gibt Ausgaben False aus, da die Umwandlungen Verweise auf zwei separate Instanzen von geboxten int
Werten erstellen.
Operatoren für Zeichen folgen
Die vordefinierten Zeichen folgen-Gleichheits Operatoren sind:

bool operator ==(string x, string y);


bool operator !=(string x, string y);

Zwei string Werte werden als gleich betrachtet, wenn eine der folgenden Punkte zutrifft:
Beide Werte sind null .
Beide Werte sind Verweise ungleich NULL auf Zeichen folgen Instanzen, die identische Längen und
identische Zeichen an jeder Zeichenposition aufweisen.
Die Gleichheits Operatoren für Zeichen folgen vergleichen Zeichen folgen Werte anstelle von Zeichen folgen
verweisen. Wenn zwei separate Zeichen folgen Instanzen genau dieselbe Zeichenfolge enthalten, sind die Werte
der Zeichen folgen gleich, aber die Verweise unterscheiden sich. Wie in Verweistyp Gleichheits
Operatorenbeschrieben, können die Verweistyp-Gleichheits Operatoren verwendet werden, um Zeichen folgen
Verweise anstelle von Zeichen folgen Werten zu vergleichen.
Delegatenoperatoren
Jeder Delegattyp stellt implizit die folgenden vordefinierten Vergleichs Operatoren bereit:

bool operator ==(System.Delegate x, System.Delegate y);


bool operator !=(System.Delegate x, System.Delegate y);

Zwei Delegatinstanzen werden wie folgt als gleich betrachtet:


Wenn eine der Delegatinstanzen ist null , sind Sie nur dann gleich, wenn beide gleich sind null .
Wenn die Delegaten einen anderen Lauf Zeittyp aufweisen, sind Sie nie gleich.
Wenn beide Delegatinstanzen über eine Aufruf Liste (Delegatdeklarationen) verfügen, sind diese Instanzen
nur dann gleich, wenn Ihre Aufruf Listen dieselbe Länge aufweisen und jeder Eintrag in einer Aufruf Liste
(wie unten definiert) dem entsprechenden Eintrag in der Reihenfolge in der Aufruf Liste eines anderen
entspricht.
Die folgenden Regeln bestimmen die Gleichheit von Aufruf Listeneinträgen:
Wenn zwei Aufruf Listeneinträge auf dieselbe statische Methode verweisen, sind die Einträge gleich.
Wenn zwei Aufruf Listeneinträge auf dieselbe nicht statische Methode im gleichen Zielobjekt verweisen (wie
durch die Verweis Gleichheits Operatoren definiert), sind die Einträge gleich.
Aufruf Listeneinträge, die aus der Auswertung semantisch identischer anonymous_method_expression s
oder lambda_expression s mit demselben (möglicherweise leeren) Satz erfasster externer Variablen
Instanzen erstellt werden, sind zulässig (aber nicht erforderlich).
Gleichheits Operatoren und NULL
Der == -Operator und der- != Operator gestatten einem Operanden einen Wert eines Typs, der NULL-Werte
zulässt, und der andere als null Literalwert, auch wenn kein vordefinierter oder benutzerdefinierter Operator
für den Vorgang vorhanden ist.
Für einen Vorgang eines der Formulare

x == null
null == x
x != null
null != x

dabei x ist ein Ausdruck eines Typs, der NULL-Werte zulässt, wenn die Operator Überladungs Auflösung
(binäre Operator Überladungs Auflösung) keinen anwendbaren Operator findet, wird das Ergebnis stattdessen
aus der- HasValue Eigenschaft von berechnet x . Insbesondere werden die ersten beiden Formulare in
übersetzt !x.HasValue , und die letzten beiden Formulare werden in übersetzt x.HasValue .
Der is-Operator
Der- is Operator wird verwendet, um dynamisch zu überprüfen, ob der Lauf Zeittyp eines Objekts mit einem
angegebenen Typ kompatibel ist. Das Ergebnis des Vorgangs E is T , wobei E ein Ausdruck und T ein Typ
ist, ist ein boolescher Wert, der angibt, ob E erfolgreich T durch eine Verweis Konvertierung, eine Boxing-
Konvertierung oder eine Unboxing-Konvertierung in den Typ konvertiert werden kann. Der Vorgang wird wie
folgt ausgewertet, nachdem Typargumente für alle Typparameter ersetzt wurden:
Wenn E eine anonyme Funktion ist, tritt ein Kompilierzeitfehler auf.
Wenn E eine Methoden Gruppe oder das null Literale ist, wenn der Typ von E ein Verweistyp oder ein
Werte zulässt-Typ ist und der Wert von E NULL ist, ist das Ergebnis false.
Stellen Sie andernfalls D den dynamischen Typ von E wie folgt dar:
Wenn der Typ von E ein Verweistyp ist, D ist der Lauf Zeittyp des instanzverweises von E .
Wenn der Typ von E ein Typ ist, der NULL-Werte zulässt, D ist der zugrunde liegende Typ dieses
Typs, der NULL-Werte zulässt.
Wenn der Typ von E ein Werttyp ist, der keine NULL-Werte zulässt, D ist der Typ von E .
Das Ergebnis des Vorgangs hängt von D und T wie folgt ab:
Wenn T ein Verweistyp ist, ist das Ergebnis true, wenn D und T denselben Typ haben, wenn D ein
Verweistyp und eine implizite Verweis Konvertierung von D in vorhanden ist T , oder wenn D ein
Werttyp und eine Boxingkonvertierung von D in vorhanden ist T .
Wenn T ein Typ ist, der NULL-Werte zulässt, ist das Ergebnis true, wenn D der zugrunde liegende
Typ von ist T .
Wenn T ein Werttyp ist, der keine NULL-Werte zulässt, ist das Ergebnis true, wenn D und T
denselben Typ haben.
Andernfalls ist das Ergebnis false.
Beachten Sie, dass benutzerdefinierte Konvertierungen vom-Operator nicht berücksichtigt werden is .
Der as-Operator
Der- as Operator wird verwendet, um einen Wert explizit in einen angegebenen Verweistyp oder Werte
zulässt-Typ zu konvertieren. Anders als bei einem Umwandlungs Ausdruck (Cast-Ausdrücke) löst der as
Operator nie eine Ausnahme aus. Wenn die angegebene Konvertierung nicht möglich ist, ist der resultierende
Wert null .
Bei einem Vorgang des Formulars E as T E muss ein Ausdruck sein, und er T muss ein Verweistyp sein, ein
Typparameter, der als Verweistyp bekannt ist, oder ein Typ, der NULL-Werte zulässt. Außerdem muss
mindestens einer der folgenden Punkte zutreffen. andernfalls tritt ein Kompilierzeitfehler auf:
Eine Identität (Identitäts Konvertierung), implizite NULL-Werte zulassen (implizit NULL-Wertezulassen),
impliziter Verweis (implizite Verweis Konvertierungen), Boxing (Boxing-Konvertierungen), explizite NULL-
Werte zulassen (explizite Konvertierungen, die NULL zulassen), expliziter Verweis (explizite Verweis
Konvertierungen) oder Unboxing-Konvertierung (Unboxing-Konvertierung) von E in T
Der Typ von E oder T ist ein offener Typ.
E ist das null Literale.

Wenn der Kompilier Zeittyp von E nicht ist dynamic , erzeugt der Vorgang E as T dasselbe Ergebnis wie

E is T ? (T)(E) : (T)null

außer dass E nur einmal überprüft wird. Es ist zu erwarten, dass der Compiler E as T eine Optimierung
durchführt, um höchstens eine dynamische Typüberprüfung auszuführen, im Gegensatz zu den zwei
dynamischen Typüberprüfungen, die von der obigen Erweiterung impliziert werden.
Wenn der Kompilier Zeittyp von den Wert E dynamic hat, ist der Operator im Gegensatz zum Cast Operator
as nicht dynamisch gebunden (dynamische Bindung). Daher ist die Erweiterung in diesem Fall:
E is T ? (T)(object)(E) : (T)null

Beachten Sie, dass einige Konvertierungen, wie z. b. benutzerdefinierte Konvertierungen, mit dem-Operator
nicht möglich sind as und stattdessen mithilfe von Umwandlungs Ausdrücken ausgeführt werden.
Im Beispiel

class X
{

public string F(object o) {


return o as string; // OK, string is a reference type
}

public T G<T>(object o) where T: Attribute {


return o as T; // Ok, T has a class constraint
}

public U H<U>(object o) {
return o as U; // Error, U is unconstrained
}
}

der Typparameter T von G ist bekannt, dass es sich um einen Verweistyp handelt, da er die-Klassen
Einschränkung aufweist. Der Typparameter U von H ist jedoch nicht zulässig. Daher ist die Verwendung des-
as Operators in unzulässig H .

Logische Operatoren
Die & ^ | Operatoren, und werden als logische Operatoren bezeichnet.

and_expression
: equality_expression
| and_expression '&' equality_expression
;

exclusive_or_expression
: and_expression
| exclusive_or_expression '^' and_expression
;

inclusive_or_expression
: exclusive_or_expression
| inclusive_or_expression '|' exclusive_or_expression
;

Wenn ein Operand eines logischen Operators den Kompilier Zeittyp aufweist dynamic , wird der Ausdruck
dynamisch gebunden (dynamische Bindung). In diesem Fall ist der Kompilier Zeittyp des Ausdrucks dynamic ,
und die unten beschriebene Auflösung erfolgt zur Laufzeit mit dem Lauf Zeittyp der Operanden, die den
Kompilier Zeittyp aufweisen dynamic .
Bei einem Vorgang im Formular x op y , bei dem op es sich um einen der logischen Operatoren handelt, wird
die Überladungs Auflösung (binäre Operator Überladungs Auflösung) angewendet, um eine bestimmte
Operator Implementierung auszuwählen. Die Operanden werden in die Parametertypen des ausgewählten
Operators konvertiert, und der Ergebnistyp ist der Rückgabetyp des Operators.
Die vordefinierten logischen Operatoren werden in den folgenden Abschnitten beschrieben.
Ganz Zahl logische Operatoren
Die vordefinierten ganzzahligen logischen Operatoren lauten wie folgt:

int operator &(int x, int y);


uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);

int operator |(int x, int y);


uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);

int operator ^(int x, int y);


uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);

Der- & Operator berechnet die bitweise logische AND der beiden Operanden, der | -Operator berechnet die
bitweise logische OR der beiden Operanden, und der- ^ Operator berechnet das bitweise logische exklusive-
Element der beiden- OR Operanden. Von diesen Vorgängen können keine über Flüsse durchlaufen werden.
Logische Enumerationsoperatoren
Jeder Enumerationstyp E stellt implizit die folgenden vordefinierten logischen Operatoren bereit:

E operator &(E x, E y);


E operator |(E x, E y);
E operator ^(E x, E y);

Das Ergebnis der Auswertung von x op y , wobei x und y Ausdrücke eines Enumerationstyps E mit einem
zugrunde liegenden Typ sind U , und op ist einer der logischen Operatoren, ist identisch mit dem Auswerten
von (E)((U)x op (U)y) . Mit anderen Worten: die logischen Operatoren des Enumerationstyps führen einfach
die logische Operation für den zugrunde liegenden Typ der beiden Operanden aus.
Logische boolesche Operatoren
Die vordefinierten booleschen logischen Operatoren lauten wie folgt:

bool operator &(bool x, bool y);


bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);

Das Ergebnis von x & y ist true , wenn sowohl x als auch y zu true ausgewertet werden. Andernfalls ist
das Ergebnis false .
Das Ergebnis von x | y ist, true Wenn entweder x oder y ist true . Andernfalls ist das Ergebnis false .
Das Ergebnis von x ^ y ist true , wenn den Wert x true y hat und ist false , oder x ist false und ist
y true . Andernfalls ist das Ergebnis false . Wenn die Operanden vom Typ sind bool , ^ berechnet der
Operator dasselbe Ergebnis wie der != Operator.
Boolesche logische Operatoren, die NULL -Werte zulassen
Der booleschen-Typ, der NULL bool? -Werte zulässt, kann drei Werte, true , false und darstellen null und
ist konzeptionell vergleichbar mit dem dreiwertigen Typ, der für boolesche Ausdrücke in SQL verwendet wird.
Um sicherzustellen, dass die Ergebnisse, die von den & | bool? Operatoren und für Operanden erzeugt
werden, mit der dreiwertigen Logik von SQL übereinstimmen, werden die folgenden vordefinierten Operatoren
bereitgestellt:
bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);

In der folgenden Tabelle werden die Ergebnisse aufgelistet, die von diesen Operatoren für alle Kombinationen
der Werte true , und erzeugt werden false null .

X Y X & Y X | Y

true true true true

true false false true

true null null true

false true false true

false false false false

false null false null

null true null true

null false false null

null null null null

Bedingte logische Operatoren


Die Operatoren && und || werden als bedingte logische Operatoren bezeichnet. Sie werden auch als
"Kurzschluss" logische Operatoren bezeichnet.

conditional_and_expression
: inclusive_or_expression
| conditional_and_expression '&&' inclusive_or_expression
;

conditional_or_expression
: conditional_and_expression
| conditional_or_expression '||' conditional_and_expression
;

Die && || Operatoren und sind bedingte Versionen der & -und- | Operatoren:
Der-Vorgang x && y entspricht dem-Vorgang, mit dem Unterschied, x & y dass y nur ausgewertet wird,
wenn x nicht ist false .
Der-Vorgang x || y entspricht dem-Vorgang, mit dem Unterschied, x | y dass y nur ausgewertet wird,
wenn x nicht ist true .
Wenn ein Operand eines bedingten logischen Operators den Kompilier Zeittyp aufweist dynamic , wird der
Ausdruck dynamisch gebunden (dynamische Bindung). In diesem Fall ist der Kompilier Zeittyp des Ausdrucks
dynamic , und die unten beschriebene Auflösung erfolgt zur Laufzeit mit dem Lauf Zeittyp der Operanden, die
den Kompilier Zeittyp aufweisen dynamic .
Ein Vorgang des Formulars x && y oder x || y wird durch Anwenden der Überladungs Auflösung (binäre
Operator Überladungs Auflösung) so verarbeitet, als ob der Vorgang geschrieben wurde x & y oder x | y .
Dies ergibt folgende Szenarien:
Wenn die Überladungs Auflösung keinen einzelnen optimalen Operator findet oder wenn die Überladungs
Auflösung einen der vordefinierten logischen ganzzahligen Operatoren auswählt, tritt ein Bindungs
Zeitfehler auf.
Andernfalls wird der Vorgang wie in booleschen bedingten logischenOperatoren beschrieben verarbeitet,
wenn der ausgewählte Operator einer der vordefinierten booleschen logischen Operatoren (boolesche
logische Operatoren) oder NULL-Werte zulässt.
Andernfalls ist der ausgewählte Operator ein benutzerdefinierter Operator, und der Vorgang wird wie unter
Benutzerdefinierte bedingte logische Operatorenbeschrieben verarbeitet.
Es ist nicht möglich, die bedingten logischen Operatoren direkt zu überladen. Da die bedingten logischen
Operatoren jedoch in Bezug auf die regulären logischen Operatoren ausgewertet werden, sind über Ladungen
der regulären logischen Operatoren mit bestimmten Einschränkungen auch als über Ladungen der bedingten
logischen Operatoren zu berücksichtigen. Dies wird weiter unten unter Benutzerdefinierte bedingte logische
Operatorenbeschrieben.
Boolesche bedingte logische Operatoren
Wenn die Operanden von && oder || vom Typ sind bool oder wenn es sich bei den Operanden um Typen
handelt, die keinen anwendbaren operator & oder definieren operator | , aber implizite Konvertierungen in
definieren, bool wird der Vorgang wie folgt verarbeitet:
Der Vorgang x && y wird als ausgewertet x ? y : false . Anders ausgedrückt, x wird zuerst ausgewertet
und in den-Typ konvertiert bool . Wenn den Wert hat, x true y wird ausgewertet und in den-Typ
konvertiert bool , und dies wird das Ergebnis des Vorgangs. Andernfalls ist das Ergebnis des Vorgangs
false .
Der Vorgang x || y wird als ausgewertet x ? true : y . Anders ausgedrückt, x wird zuerst ausgewertet
und in den-Typ konvertiert bool . Wenn den Wert x true hat, ist das Ergebnis des Vorgangs true .
Andernfalls y wird ausgewertet und in den-Typ konvertiert bool , und dies wird das Ergebnis des
Vorgangs.
Benutzerdefinierte bedingte logische Operatoren
Wenn es sich bei den Operanden von && oder || um Typen handelt, die ein anwendbares benutzerdefiniertes
oder deklarieren operator & operator | , müssen die beiden folgenden Optionen true sein, wobei T der Typ
ist, in dem der ausgewählte Operator deklariert ist:
Der Rückgabetyp und der Typ jedes Parameters des ausgewählten Operators müssen sein T . Mit anderen
Worten, der-Operator muss das logische AND oder das logische OR von zwei Operanden vom Typ
berechnen T , und muss ein Ergebnis vom Typ zurückgeben T .
T muss Deklarationen von operator true und enthalten operator false .

Ein Fehler bei der Bindungs Zeit tritt auf, wenn eine dieser Anforderungen nicht erfüllt wird. Andernfalls wird
der- && Vorgang oder der- || Vorgang ausgewertet, indem der benutzerdefinierte operator true oder
operator false der ausgewählte benutzerdefinierte Operator kombiniert wird:

Der Vorgang x && y wird als ausgewertet T.false(x) ? x : T.&(x, y) , wobei T.false(x) ein Aufruf von
ist, der operator false in deklariert ist T , und T.&(x, y) ein Aufruf des ausgewählten operator & . Mit
anderen Worten, x wird zuerst ausgewertet und für operator false das Ergebnis aufgerufen, um zu
bestimmen, ob x definitiv false ist. Wenn dann x definitiv false ist, ist das Ergebnis der Operation der Wert,
der zuvor für berechnet wurde x . Andernfalls y wird ausgewertet, und der ausgewählte operator & wird
für den Wert aufgerufen, der zuvor für berechnet wurde, x und der Wert, der für berechnet wird, y um das
Ergebnis des Vorgangs zu erhalten.
Der Vorgang x || y wird als ausgewertet T.true(x) ? x : T.|(x, y) , wobei T.true(x) ein Aufruf von ist,
der operator true in deklariert ist T , und T.|(x,y) ein Aufruf des ausgewählten operator| . Mit anderen
Worten, x wird zuerst ausgewertet und für operator true das Ergebnis aufgerufen, um zu bestimmen, ob
x definitiv true ist. Wenn dann x definitiv true ist, ist das Ergebnis der Operation der Wert, der zuvor für
berechnet wurde x . Andernfalls y wird ausgewertet, und der ausgewählte operator | wird für den Wert
aufgerufen, der zuvor für berechnet wurde, x und der Wert, der für berechnet wird, y um das Ergebnis des
Vorgangs zu erhalten.
Bei beiden Vorgängen wird der von angegebene Ausdruck x nur einmal ausgewertet, und der von angegebene
Ausdruck y wird entweder nicht genau einmal ausgewertet oder ausgewertet.
Ein Beispiel für einen Typ, der operator true und implementiert operator false , finden Sie unter Daten Bank
boolescher Typ.

The null coalescing operator (Der NULL-Sammeloperator)


Der- ?? Operator wird als NULL-Sammel Operator bezeichnet.

null_coalescing_expression
: conditional_or_expression
| conditional_or_expression '??' null_coalescing_expression
;

Ein NULL-Sammel Ausdruck des Formulars a ?? b erfordert a , dass ein Typ oder Verweistyp ist, der NULL-
Werte zulässt. Wenn a nicht NULL ist, ist das Ergebnis von a ?? b a . andernfalls ist das Ergebnis b . Der
Vorgang wertet b nur aus, wenn a NULL ist.
Der NULL-Sammel Operator ist rechts assoziativ, was bedeutet, dass Vorgänge von rechts nach Links gruppiert
werden. Beispielsweise wird ein Ausdruck des Formulars a ?? b ?? c als ausgewertet a ?? (b ?? c) . In der
Regel gibt ein Ausdruck der Form E1 ?? E2 ?? ... ?? En den ersten der Operanden zurück, der nicht NULL ist,
oder NULL, wenn alle Operanden NULL sind.
Der Typ des Ausdrucks a ?? b hängt davon ab, welche impliziten Konvertierungen für die Operanden
verfügbar sind. In der angegebenen Reihenfolge ist der Typ a ?? b von A0 , A oder B , wobei A der Typ von
a (bereitgestellt ist, der einen- a Typ aufweist), B der Typ von b (bereitgestellt mit b einem-Typ) und A0
der zugrunde liegende Typ von ist, A Wenn A ein Typ ist, der NULL-Werte zulässt, A andernfalls.
Insbesondere a ?? b wird wie folgt verarbeitet:
Wenn A vorhanden und kein Werte zulässt-Typ oder Verweistyp ist, tritt ein Kompilierzeitfehler auf.
Wenn b ein dynamischer Ausdruck ist, ist der Ergebnistyp dynamic . Zur Laufzeit a wird zuerst
ausgewertet. Wenn a nicht NULL ist, a wird in Dynamic konvertiert, und dies wird zum Ergebnis.
Andernfalls b wird ausgewertet, und dies wird zum Ergebnis.
Andernfalls A b A0 ist der Ergebnistyp, wenn vorhanden und ein Typ ist, der NULL-Werte zulässt, und
eine implizite Konvertierung von in vorhanden ist A0 . Zur Laufzeit a wird zuerst ausgewertet. Wenn a
nicht NULL ist, a wird in den-Typ entpackt A0 , und dies wird zum Ergebnis. Andernfalls b wird
ausgewertet und in A0 den-Typ konvertiert, sodass dies das Ergebnis ist.
Andernfalls A b A ist der Ergebnistyp, wenn vorhanden und eine implizite Konvertierung von in
vorhanden ist A . Zur Laufzeit a wird zuerst ausgewertet. Wenn a nicht NULL ist, a wird das Ergebnis.
Andernfalls b wird ausgewertet und in A den-Typ konvertiert, sodass dies das Ergebnis ist.
Andernfalls b B a B ist der Ergebnistyp, wenn einen-Typ aufweist und eine implizite Konvertierung von
in vorhanden ist B . Zur Laufzeit a wird zuerst ausgewertet. Wenn a nicht NULL ist, a wird in den-Typ
entpackt A0 (wenn A vorhanden und NULL-Werte zulässt) und in B den-Typ konvertiert werden. Dies
wird zum Ergebnis. Andernfalls b wird ausgewertet und zum Ergebnis.
Andernfalls a sind und nicht b kompatibel, und es tritt ein Kompilierzeitfehler auf.

Bedingter Operator
Der- ?: Operator wird als Bedingter Operator bezeichnet. Es wird manchmal auch als ternärer Operator
bezeichnet.

conditional_expression
: null_coalescing_expression
| null_coalescing_expression '?' expression ':' expression
;

Ein bedingter Ausdruck des Formulars b ? x : y wertet die Bedingung zuerst aus b . Wenn den Wert b
true hat, x wird ausgewertet und zum Ergebnis des Vorgangs. Andernfalls y wird ausgewertet und wird
zum Ergebnis des Vorgangs. Ein bedingter Ausdruck wertet nie sowohl x als auch aus y .
Der bedingte Operator ist rechts assoziativ, was bedeutet, dass Vorgänge von rechts nach Links gruppiert
werden. Beispielsweise wird ein Ausdruck des Formulars a ? b : c ? d : e als ausgewertet
a ? b : (c ? d : e) .

Der erste Operand des ?: Operators muss ein Ausdruck sein, der implizit in konvertiert werden kann bool ,
oder ein Ausdruck eines Typs, der implementiert operator true . Wenn keine dieser Anforderungen erfüllt ist,
tritt ein Kompilierzeitfehler auf.
Mit dem zweiten und dritten Operanden x y des ?: Operators wird der Typ des bedingten Ausdrucks
gesteuert.
Wenn den x Typ aufweist X und den y Typ hat, Y dann
Wenn eine implizite Konvertierung (implizite Konvertierungen) von X in Y , aber nicht von Y in
vorhanden ist X , dann Y ist der Typ des bedingten Ausdrucks.
Wenn eine implizite Konvertierung (implizite Konvertierungen) von Y in X , aber nicht von X in
vorhanden ist Y , dann X ist der Typ des bedingten Ausdrucks.
Andernfalls kann kein Ausdruckstyp ermittelt werden, und es tritt ein Kompilierzeitfehler auf.
Wenn nur einer von x und y einen-Typ aufweist und sowohl x als auch y , von implizit in diesen Typ
konvertiert werden können, ist dies der Typ des bedingten Ausdrucks.
Andernfalls kann kein Ausdruckstyp ermittelt werden, und es tritt ein Kompilierzeitfehler auf.
Die Lauf Zeit Verarbeitung eines bedingten Ausdrucks der Form b ? x : y besteht aus den folgenden Schritten:
Zuerst b wird ausgewertet, und der bool Wert von b wird bestimmt:
Wenn eine implizite Konvertierung vom Typ von b in bool vorhanden ist, wird diese implizite
Konvertierung durchgeführt, um einen bool Wert zu erhalten.
Andernfalls wird die operator true , die durch den Typ von definiert b wird, aufgerufen, um einen
Wert zu erhalten bool .
Wenn der bool durch den obigen Schritt erstellte Wert ist true , x wird ausgewertet und in den Typ des
bedingten Ausdrucks konvertiert, und dies wird das Ergebnis des bedingten Ausdrucks.
Andernfalls y wird ausgewertet und in den Typ des bedingten Ausdrucks konvertiert, und dies wird das
Ergebnis des bedingten Ausdrucks.

Anonymous function expressions (Anonyme Funktionsausdrücke)


Eine anonyme Funktion ist ein Ausdruck, der eine "Inline"-Methoden Definition darstellt. Eine anonyme
Funktion verfügt nicht über einen Wert oder einen Typ in und von sich selbst, kann jedoch in einen kompatiblen
Delegaten oder Ausdrucks Strukturtyp konvertiert werden. Die Auswertung einer anonymen Funktions
Konvertierung hängt vom Zieltyp der Konvertierung ab: Wenn es sich um einen Delegattyp handelt, wird die
Konvertierung zu einem Delegatwert ausgewertet, der auf die Methode verweist, die von der anonymen
Funktion definiert wird. Wenn es sich um einen Ausdrucks bauentyp handelt, wird die Konvertierung zu einer
Ausdrucks Baumstruktur ausgewertet, die die Struktur der Methode als Objektstruktur darstellt.
Aus historischen Gründen gibt es zwei syntaktische Varianten von anonymen Funktionen, nämlich
lambda_expression s und anonymous_method_expression s. Für fast alle Zwecke sind lambda_expression s
präziser und ausdrucksstarker als anonymous_method_expression s, die für die Abwärtskompatibilität in der
Sprache verbleiben.

lambda_expression
: anonymous_function_signature '=>' anonymous_function_body
;

anonymous_method_expression
: 'delegate' explicit_anonymous_function_signature? block
;

anonymous_function_signature
: explicit_anonymous_function_signature
| implicit_anonymous_function_signature
;

explicit_anonymous_function_signature
: '(' explicit_anonymous_function_parameter_list? ')'
;

explicit_anonymous_function_parameter_list
: explicit_anonymous_function_parameter (',' explicit_anonymous_function_parameter)*
;

explicit_anonymous_function_parameter
: anonymous_function_parameter_modifier? type identifier
;

anonymous_function_parameter_modifier
: 'ref'
| 'out'
;

implicit_anonymous_function_signature
: '(' implicit_anonymous_function_parameter_list? ')'
| implicit_anonymous_function_parameter
;

implicit_anonymous_function_parameter_list
: implicit_anonymous_function_parameter (',' implicit_anonymous_function_parameter)*
;

implicit_anonymous_function_parameter
: identifier
;

anonymous_function_body
: expression
| block
;

Der Operator => verfügt über die gleiche Rangfolge wie die Zuweisung ( = ) und ist rechtsassoziativ.
Eine anonyme Funktion mit dem async -Modifizierer ist eine Async-Funktion und folgt den in Async-
Funktionenbeschriebenen Regeln.
Die Parameter einer anonymen Funktion in Form eines lambda_expression können explizit oder implizit
eingegeben werden. In einer explizit typisierten Parameterliste wird der Typ jedes Parameters explizit
angegeben. In einer implizit typisierten Parameterliste werden die Parametertypen aus dem Kontext abgeleitet,
in dem die anonyme Funktion auftritt – insbesondere wenn die anonyme Funktion in einen kompatiblen
Delegattyp oder Ausdrucks Strukturtyp konvertiert wird, stellt dieser Typ die Parametertypen (Anonyme
Funktions Konvertierungen) bereit.
In einer anonymen Funktion mit einem einzelnen, implizit typisierten Parameter können die Klammern in der
Parameterliste weggelassen werden. Anders ausgedrückt: eine anonyme Funktion der Form

( param ) => expr

kann abgekürzt werden zu

param => expr

Die Parameterliste einer anonymen Funktion in Form einer anonymous_method_expression ist optional. Wenn
angegeben, müssen die Parameter explizit typisiert werden. Andernfalls kann die anonyme Funktion in einen
Delegaten mit einer beliebigen Parameterliste konvertiert werden, die keine out Parameter enthält.
Ein Block Körper einer anonymen Funktion ist erreichbar (Endpunkte und Erreichbarkeit), es sei denn, die
anonyme Funktion tritt innerhalb einer nicht erreichbaren Anweisung auf.
Im folgenden finden Sie einige Beispiele für anonyme Funktionen:

x => x + 1 // Implicitly typed, expression body


x => { return x + 1; } // Implicitly typed, statement body
(int x) => x + 1 // Explicitly typed, expression body
(int x) => { return x + 1; } // Explicitly typed, statement body
(x, y) => x * y // Multiple parameters
() => Console.WriteLine() // No parameters
async (t1,t2) => await t1 + await t2 // Async
delegate (int x) { return x + 1; } // Anonymous method expression
delegate { return 1 + 1; } // Parameter list omitted

Das Verhalten von lambda_expression s und anonymous_method_expression s ist mit Ausnahme der folgenden
Punkte identisch:
anonymous_method_expression s erlauben, dass die Parameterliste vollständig ausgelassen wird, sodass die
Konvertierungs Möglichkeit zum Delegieren von Typen von Wert Parametern ermöglicht wird.
lambda_expression s zulassen, dass Parametertypen ausgelassen und abgeleitet werden, während
anonymous_method_expression s Parametertypen explizit angeben müssen.
Der Text eines lambda_expression kann ein Ausdruck oder ein Anweisungsblock sein, während der Text eines
anonymous_method_expression ein Anweisungsblock sein muss.
Nur lambda_expression s verfügen über Konvertierungen in kompatible Ausdrucks Baumstruktur Typen
(Ausdrucks Baumstruktur Typen).
Anonyme Funktions Signaturen
Der optionale anonymous_function_signature einer anonymen Funktion definiert die Namen und optional die
Typen der formalen Parameter für die anonyme Funktion. Der Gültigkeitsbereich der Parameter der anonymen
Funktion ist der anonymous_function_body. (Bereiche) In Verbindung mit der Parameterliste (falls angegeben)
bildet der anonyme Methoden Text einen Deklarations Raum (Deklarationen). Daher ist es ein
Kompilierzeitfehler, wenn der Name eines Parameters der anonymen Funktion mit dem Namen einer lokalen
Variablen, lokalen Konstante oder eines Parameters identisch ist, deren Bereich die
anonymous_method_expression oder lambda_expression enthält.
Wenn eine anonyme Funktion über eine explicit_anonymous_function_signature verfügt, ist der Satz
kompatibler Delegattypen und Ausdrucks Baum Typen auf diejenigen beschränkt, die dieselben Parametertypen
und Modifizierer in der gleichen Reihenfolge aufweisen. Im Gegensatz zu Methoden Gruppen Konvertierungen
(Methoden Gruppen Konvertierungen) wird die kontra Varianz anonymer Funktionsparameter Typen nicht
unterstützt. Wenn eine anonyme Funktion keine anonymous_function_signature hat, ist der Satz kompatibler
Delegattypen und Ausdrucks Baum Typen auf diejenigen beschränkt, die keine Parameter haben out .
Beachten Sie, dass ein anonymous_function_signature keine Attribute oder ein Parameter Array enthalten kann.
Dennoch kann ein anonymous_function_signature mit einem Delegattyp kompatibel sein, dessen Parameterliste
ein Parameter Array enthält.
Beachten Sie auch, dass bei der Konvertierung in einen Ausdrucks bauentyp, auch wenn diese kompatibel ist,
während der Kompilierzeit (Ausdrucks Baumstruktur Typen) weiterhin Fehler auftreten können.
Anonyme Funktions Texte
Der Text (Ausdruck oder Block) einer anonymen Funktion unterliegt den folgenden Regeln:
Wenn die anonyme Funktion eine Signatur enthält, sind die in der Signatur angegebenen Parameter im
Textkörper verfügbar. Wenn die anonyme Funktion keine Signatur aufweist, kann Sie in einen Delegattyp
oder einen Ausdruckstyp mit Parametern (Anonyme Funktions Konvertierungen) konvertiert werden, aber
auf die Parameter kann im Text nicht zugegriffen werden.
Mit Ausnahme ref von-oder- out Parametern, die in der Signatur (sofern vorhanden) der nächstgelegenen
einschließenden anonymen Funktion angegeben sind, ist dies ein Kompilierzeitfehler für den Text, um auf
einen- ref oder- out Parameter zuzugreifen
Wenn der Typ von this ein Strukturtyp ist, ist dies ein Kompilierzeitfehler für den Text, auf den zugegriffen
werden kann this . Dies gilt unabhängig davon, ob der Zugriff explizit (wie in this.x ) oder implizit ist (wie
in, x wobei x ein Instanzmember der Struktur ist). Diese Regel verhindert einen solchen Zugriff und wirkt
sich nicht darauf aus, ob die Member-Suche zu einem Member der Struktur führt.
Der Text hat Zugriff auf die äußeren Variablen (äußere Variablen) der anonymen Funktion. Der Zugriff auf
eine äußere Variable verweist auf die Instanz der Variablen, die zum Zeitpunkt der Auswertung der
lambda_expression oder anonymous_method_expression (Auswertung anonymer Funktions Ausdrücke)
aktiv ist.
Es ist ein Kompilierzeitfehler, wenn der Text eine goto Anweisung, eine break Anweisung oder eine
Anweisung enthält, continue deren Ziel außerhalb des Texts oder innerhalb des Texts einer enthaltenen
anonymen Funktion liegt.
Eine- return Anweisung im Text gibt die Steuerung von einem Aufruf der nächsten einschließenden
anonymen Funktion zurück, nicht vom einschließenden Funktionsmember. Ein Ausdruck, der in einer-
return Anweisung angegeben ist, muss implizit in den Rückgabetyp des Delegattyps oder Ausdrucks
Struktur Typs konvertiert werden, in den die nächstgelegene einschließende lambda_expression oder
anonymous_method_expression konvertiert wird (Anonyme Funktions Konvertierungen).
Es ist explizit nicht angegeben, ob es eine Möglichkeit gibt, den Block einer anonymen Funktion auszuführen,
außer durch Auswertung und Aufruf der lambda_expression oder anonymous_method_expression.
Insbesondere kann der Compiler eine anonyme Funktion durch das Zusammenführen einer oder mehrerer
benannter Methoden oder Typen implementieren. Die Namen dieser Elemente mit synthetischer
kompilarverwendung müssen ein für die Compilerverwendung reserviertes Formular sein.
Überladungs Auflösung und Anonyme Funktionen
Anonyme Funktionen in einer Argumentliste sind an der Typrückschluss-und Überladungs Auflösung beteiligt.
Die genauen Regeln finden Sie unter Typrückschluss und Überladungs Auflösung .
Das folgende Beispiel veranschaulicht die Auswirkung anonymer Funktionen auf die Überladungs Auflösung.

class ItemList<T>: List<T>


{
public int Sum(Func<T,int> selector) {
int sum = 0;
foreach (T item in this) sum += selector(item);
return sum;
}

public double Sum(Func<T,double> selector) {


double sum = 0;
foreach (T item in this) sum += selector(item);
return sum;
}
}

Die- ItemList<T> Klasse verfügt über zwei Sum Methoden. Jede übernimmt ein- selector Argument, das den
Wert aus einem Listenelement in Summen extrahiert. Der extrahierte Wert kann entweder ein int oder ein
sein, double und die resultierende Summe ist ebenfalls entweder ein int oder ein double .
Die Sum Methoden können z. b. verwendet werden, um Summen aus einer Liste von Detail Zeilen in einer
Reihenfolge zu berechnen.

class Detail
{
public int UnitCount;
public double UnitPrice;
...
}

void ComputeSums() {
ItemList<Detail> orderDetails = GetOrderDetails(...);
int totalUnits = orderDetails.Sum(d => d.UnitCount);
double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount);
...
}

Beim ersten Aufruf von orderDetails.Sum Sum sind beide Methoden anwendbar, da die anonyme Funktion
d => d. UnitCount sowohl mit als auch mit kompatibel Func<Detail,int> ist Func<Detail,double> . Die
Überladungs Auflösung wählt jedoch die erste Sum Methode aus, da die Konvertierung in Func<Detail,int>
besser ist als die Konvertierung in Func<Detail,double> .
Beim zweiten Aufruf von orderDetails.Sum ist nur die zweite Sum Methode anwendbar, da die anonyme
Funktion d => d.UnitPrice * d.UnitCount einen Wert vom Typ erzeugt double . Daher wählt die Überladungs
Auflösung die zweite Sum Methode für diesen Aufruf aus.
Anonyme Funktionen und dynamische Bindung
Eine anonyme Funktion kann kein Empfänger, Argument oder Operand eines dynamisch gebundenen Vorgangs
sein.
Äußere Variablen
Alle lokalen Variablen, Wert Parameter oder Parameter Arrays, deren Bereich die lambda_expression oder
anonymous_method_expression enthält, werden als äußere Variable der anonymen Funktion bezeichnet. In
einem Instanzfunktionsmember einer Klasse this wird der Wert als Wert Parameter betrachtet und ist eine
äußere Variable einer beliebigen anonymen Funktion, die im Funktionsmember enthalten ist.
Erfasste äußere Variablen
Wenn auf eine äußere Variable durch eine anonyme Funktion verwiesen wird, wird die äußere Variable als von
der anonymen Funktion aufgezeichnet . Normalerweise ist die Lebensdauer einer lokalen Variable auf die
Ausführung des Blocks oder der Anweisung beschränkt, mit der Sie verknüpft ist (lokale Variablen). Die
Lebensdauer einer erfassten äußeren Variable wird jedoch mindestens so lange verlängert, bis der Delegat oder
die Ausdrucks Struktur, der aus der anonymen Funktion erstellt wurde, für Garbage Collection qualifiziert wird.
Im Beispiel

using System;

delegate int D();

class Test
{
static D F() {
int x = 0;
D result = () => ++x;
return result;
}

static void Main() {


D d = F();
Console.WriteLine(d());
Console.WriteLine(d());
Console.WriteLine(d());
}
}

die lokale Variable x wird von der anonymen Funktion aufgezeichnet, und die Lebensdauer von x wird
mindestens so lange verlängert, bis der Delegat, der von zurückgegeben wird, F für Garbage Collection
qualifiziert ist (was bis zum Ende des Programms nicht erfolgt). Da jeder Aufruf der anonymen Funktion auf
derselben Instanz von ausgeführt wird x , lautet die Ausgabe des Beispiels wie folgt:

1
2
3

Wenn eine lokale Variable oder ein value-Parameter von einer anonymen Funktion aufgezeichnet wird, wird die
lokale Variable oder der Parameter nicht mehr als eine festgelegte Variable (fest-und verschiebbare Variablen)
betrachtet, sondern als eine verschiebbare Variable angesehen. Folglich unsafe muss jeglicher Code, der die
Adresse einer erfassten äußeren Variablen annimmt, zuerst die- fixed Anweisung verwenden, um die Variable
zu korrigieren.
Beachten Sie, dass im Gegensatz zu einer nicht erfassten Variablen eine aufgezeichnete lokale Variable
gleichzeitig für mehrere Ausführungs Threads verfügbar gemacht werden kann.
Instanziierung von lokalen Variablen
Eine lokale Variable wird als instanziier t betrachtet, wenn die Ausführung in den Gültigkeitsbereich der
Variablen eintritt. Wenn z. b. die folgende Methode aufgerufen wird, wird die lokale Variable x dreimal
instanziiert und initialisiert – einmal für jede Iterationen der Schleife.

static void F() {


for (int i = 0; i < 3; i++) {
int x = i * 2 + 1;
...
}
}

Das Verschieben der Deklaration von x außerhalb der Schleife führt jedoch zu einer einzelnen Instanziierung
von x :

static void F() {


int x;
for (int i = 0; i < 3; i++) {
x = i * 2 + 1;
...
}
}

Bei nicht Erfassung kann nicht genau beachtet werden, wie oft eine lokale Variable instanziiert wird – da die
Lebensdauer der Instanziierungen disjunkt ist, kann jede Instanziierung einfach denselben Speicherort
verwenden. Wenn eine anonyme Funktion jedoch eine lokale Variable erfasst, werden die Auswirkungen der
Instanziierung offensichtlich.
Das Beispiel

using System;

delegate void D();

class Test
{
static D[] F() {
D[] result = new D[3];
for (int i = 0; i < 3; i++) {
int x = i * 2 + 1;
result[i] = () => { Console.WriteLine(x); };
}
return result;
}

static void Main() {


foreach (D d in F()) d();
}
}

erzeugt die Ausgabe:

1
3
5

Wenn jedoch die Deklaration von x außerhalb der Schleife verschoben wird:

static D[] F() {


D[] result = new D[3];
int x;
for (int i = 0; i < 3; i++) {
x = i * 2 + 1;
result[i] = () => { Console.WriteLine(x); };
}
return result;
}

die Ausgabe lautet:


5
5
5

Wenn eine for-Schleife eine Iterations Variable deklariert, wird die Variable selbst als außerhalb der Schleife
deklariert. Wenn das Beispiel so geändert wird, dass die Iterations Variable selbst aufgezeichnet wird:

static D[] F() {


D[] result = new D[3];
for (int i = 0; i < 3; i++) {
result[i] = () => { Console.WriteLine(i); };
}
return result;
}

Es wird nur eine Instanz der Iterations Variablen aufgezeichnet, die die Ausgabe erzeugt:

3
3
3

Es ist möglich, dass anonyme Funktions Delegaten einige erfasste Variablen freigeben, aber über separate
Instanzen anderer Instanzen verfügen. Wenn beispielsweise F in geändert wird.

static D[] F() {


D[] result = new D[3];
int x = 0;
for (int i = 0; i < 3; i++) {
int y = 0;
result[i] = () => { Console.WriteLine("{0} {1}", ++x, ++y); };
}
return result;
}

die drei Delegaten erfassen dieselbe Instanz von x , aber separate Instanzen von y , und die Ausgabe lautet:

1 1
2 1
3 1

Separate anonyme Funktionen können dieselbe Instanz einer äußeren Variablen erfassen. Im Beispiel:
using System;

delegate void Setter(int value);

delegate int Getter();

class Test
{
static void Main() {
int x = 0;
Setter s = (int value) => { x = value; };
Getter g = () => { return x; };
s(5);
Console.WriteLine(g());
s(10);
Console.WriteLine(g());
}
}

die beiden anonymen Funktionen erfassen dieselbe Instanz der lokalen Variablen x und können daher über
diese Variable kommunizieren. Die Ausgabe des Beispiels lautet wie folgt:

5
10

Auswertung anonymer Funktions Ausdrücke


Eine anonyme Funktion F muss immer in einen Delegattyp D oder einen Ausdrucks bauentyp konvertiert
werden E , entweder direkt oder durch die Ausführung eines Ausdrucks zum Erstellen eines new D(F)
Delegaten. Diese Konvertierung bestimmt das Ergebnis der anonymen Funktion, wie in Anonyme Funktions
Konvertierungenbeschrieben.

Abfrageausdrücke
Abfrage Ausdrücke bieten eine sprach integrierte Syntax für Abfragen, die relationalen und hierarchischen
Abfrage Sprachen wie SQL und XQuery ähneln.

query_expression
: from_clause query_body
;

from_clause
: 'from' type? identifier 'in' expression
;

query_body
: query_body_clauses? select_or_group_clause query_continuation?
;

query_body_clauses
: query_body_clause
| query_body_clauses query_body_clause
;

query_body_clause
: from_clause
| let_clause
| where_clause
| join_clause
| join_into_clause
| orderby_clause
;
;

let_clause
: 'let' identifier '=' expression
;

where_clause
: 'where' boolean_expression
;

join_clause
: 'join' type? identifier 'in' expression 'on' expression 'equals' expression
;

join_into_clause
: 'join' type? identifier 'in' expression 'on' expression 'equals' expression 'into' identifier
;

orderby_clause
: 'orderby' orderings
;

orderings
: ordering (',' ordering)*
;

ordering
: expression ordering_direction?
;

ordering_direction
: 'ascending'
| 'descending'
;

select_or_group_clause
: select_clause
| group_clause
;

select_clause
: 'select' expression
;

group_clause
: 'group' expression 'by' expression
;

query_continuation
: 'into' identifier query_body
;

Ein Abfrage Ausdruck beginnt mit einer from -Klausel und endet mit einer- select oder- group Klausel. Auf
die Initial- from Klausel können NULL oder mehr from Klauseln, let , where oder join folgen orderby .
Jede from Klausel ist ein Generator, der eine *Range-Variable _ einführt, in der die Elemente einer _ -Sequenz
* liegen. Jede let Klausel führt eine Bereichs Variable ein, die einen Wert darstellt, der mithilfe vorheriger
Bereichs Variablen berechnet wurde. Jede- where Klausel ist ein Filter, der Elemente aus dem Ergebnis
ausschließt. Jede join Klausel vergleicht die angegebenen Schlüssel der Quell Sequenz mit Schlüsseln einer
anderen Sequenz und gibt passende Paare aus. Jede- orderby Klausel ordnet Elemente gemäß den
angegebenen Kriterien neu an. Die abschließende- select oder- group Klausel gibt die Form des Ergebnisses
in Bezug auf die Bereichs Variablen an. Zum Schluss kann eine- into Klausel verwendet werden, um Abfragen
zu "Splice" zu verwenden, indem die Ergebnisse einer Abfrage als Generator in einer nachfolgenden Abfrage
behandelt werden.
Mehrdeutigkeiten in Abfrage Ausdrücken
Abfrage Ausdrücke enthalten eine Reihe von "Kontext Schlüsselwörtern", d. h. Bezeichner, die in einem
bestimmten Kontext eine besondere Bedeutung haben. Dabei handelt es sich insbesondere um from , where ,
join , on , equals , into , let , orderby , ascending , descending , select group und by . Um
Mehrdeutigkeiten in Abfrage Ausdrücken zu vermeiden, die durch die gemischte Verwendung dieser Bezeichner
als Schlüsselwörter oder einfache Namen verursacht werden, werden diese Bezeichner als Schlüsselwörter
betrachtet, wenn Sie an einer beliebigen Stelle innerhalb eines Abfrage Ausdrucks auftreten.
Zu diesem Zweck ist ein Abfrage Ausdruck ein beliebiger Ausdruck, der mit " from identifier " gefolgt von
einem beliebigen Token mit Ausnahme von "" ; , " = " oder " , " beginnt.
Um diese Wörter als Bezeichner innerhalb eines Abfrage Ausdrucks zu verwenden, kann Ihnen "" (Bezeichner)
vorangestellt werden @ .
Abfrage Ausdrucks Übersetzung
In der Programmiersprache c# ist die Ausführungs Semantik von Abfrage Ausdrücken nicht angegeben.
Stattdessen werden Abfrage Ausdrücke in Aufrufe von Methoden übersetzt, die dem Abfrage Ausdrucksmuster
(dem Abfrage Ausdrucksmuster) entsprechen. Abfrage Ausdrücke werden insbesondere in Aufrufe von
Methoden mit den Namen Where , Select , SelectMany , Join , GroupJoin , OrderBy , OrderByDescending ,
ThenBy , ThenByDescending , und übersetzt GroupBy Cast . Diese Methoden verfügen über bestimmte
Signaturen und Ergebnistypen, wie im Abfrage Ausdrucksmusterbeschrieben. Diese Methoden können
Instanzmethoden des Objekts sein, das abgefragt wird, oder Erweiterungs Methoden, die sich außerhalb des
Objekts befinden, und Sie implementieren die tatsächliche Ausführung der Abfrage.
Die Übersetzung von Abfrage Ausdrücken in Methodenaufrufe ist eine syntaktische Zuordnung, die vor der
Durchführung einer Typbindung oder Überladungs Auflösung auftritt. Es ist garantiert, dass die Übersetzung
syntaktisch korrekt ist, aber es wird nicht garantiert, dass semantisch korrekter c#-Code erzeugt wird. Nach der
Übersetzung von Abfrage Ausdrücken werden die resultierenden Methodenaufrufe als reguläre
Methodenaufrufe verarbeitet. Dies kann wiederum zu Fehlern führen, z. b. wenn die Methoden nicht vorhanden
sind, wenn Argumente falsche Typen aufweisen oder wenn die Methoden generisch sind und der Typrückschluss
fehlschlägt.
Ein Abfrage Ausdruck wird durch wiederholtes Anwenden der folgenden Übersetzungen verarbeitet, bis keine
weiteren Reduzierungen möglich sind. Die Übersetzungen werden in der Reihenfolge der Anwendung
aufgelistet: in jedem Abschnitt wird davon ausgegangen, dass die Übersetzungen in den vorangehenden
Abschnitten umfassend ausgeführt wurden, und sobald Sie aufgebraucht sind, wird ein Abschnitt bei der
Verarbeitung desselben Abfrage Ausdrucks nicht mehr untersucht.
Die Zuweisung zu Bereichs Variablen ist in Abfrage Ausdrücken nicht zulässig. Eine c#-Implementierung darf
diese Einschränkung jedoch nicht immer erzwingen, da dies möglicherweise manchmal nicht mit dem hier
dargestellten syntaktische Translation-Schema möglich ist.
Bestimmte Übersetzungen fügen Bereichs Variablen mit transparenten Bezeichner ein, die von angegeben
werden * . Die besonderen Eigenschaften von transparenten bezeichmern werden in transparenten
bezeichmernweiter erläutert.
SELECT-und GroupBy-Klauseln mit Fortsetzungen
Ein Abfrage Ausdruck mit einer Fortsetzung

from ... into x ...

wird übersetzt in

from x in ( from ... ) ...


Bei den Übersetzungen in den folgenden Abschnitten wird davon ausgegangen, dass Abfragen keine into
Fortsetzungen aufweisen.
Das Beispiel

from c in customers
group c by c.Country into g
select new { Country = g.Key, CustCount = g.Count() }

wird übersetzt in

from g in
from c in customers
group c by c.Country
select new { Country = g.Key, CustCount = g.Count() }

die letzte Übersetzung, von der

customers.
GroupBy(c => c.Country).
Select(g => new { Country = g.Key, CustCount = g.Count() })

Explizite Bereichs Variablen Typen


Eine from Klausel, die explizit einen Bereichs Variablentyp angibt.

from T x in e

wird übersetzt in

from x in ( e ) . Cast < T > ( )

Eine join Klausel, die explizit einen Bereichs Variablentyp angibt.

join T x in e on k1 equals k2

wird übersetzt in

join x in ( e ) . Cast < T > ( ) on k1 equals k2

Bei den Übersetzungen in den folgenden Abschnitten wird davon ausgegangen, dass Abfragen keine expliziten
Bereichs Variablen Typen aufweisen.
Das Beispiel

from Customer c in customers


where c.City == "London"
select c

wird übersetzt in
from c in customers.Cast<Customer>()
where c.City == "London"
select c

die letzte Übersetzung, von der

customers.
Cast<Customer>().
Where(c => c.City == "London")

Explizite Bereichs Variablen Typen eignen sich zum Abfragen von Auflistungen, die die nicht generische-
IEnumerable Schnittstelle implementieren, nicht jedoch die generische- IEnumerable<T> Schnittstelle. Im obigen
Beispiel wäre dies der Fall, wenn customers vom Typ wäre ArrayList .
Degenerierte Abfrage Ausdrücke
Ein Abfrage Ausdruck der Form

from x in e select x

wird übersetzt in

( e ) . Select ( x => x )

Das Beispiel

from c in customers
select c

wird übersetzt in

customers.Select(c => c)

Ein degenerierter Abfrage Ausdruck ist ein Ausdruck, der die Elemente der Quelle trivial auswählt. In einer
späteren Phase der Übersetzung werden degenerierte Abfragen entfernt, die durch andere Übersetzungsschritte
eingeführt wurden, indem Sie durch ihre Quelle ersetzt werden. Es ist jedoch wichtig zu gewährleisten, dass das
Ergebnis eines Abfrage Ausdrucks nie das Quell Objekt selbst ist, da dadurch der Typ und die Identität der
Quelle für den Client der Abfrage offengelegt werden. Daher schützt dieser Schritt degenerierte Abfragen, die
direkt im Quellcode geschrieben wurden, indem explizit Select für die Quelle aufgerufen wird. Dann werden
die Implementierer von Select und anderen Abfrage Operatoren übernommen, um sicherzustellen, dass diese
Methoden nie das Quell Objekt selbst zurückgeben.
From-, Let-, WHERE-, Join-und OrderBy-Klauseln
Ein Abfrage Ausdruck mit einer zweiten from Klausel gefolgt von einer- select Klausel.

from x1 in e1
from x2 in e2
select v

wird übersetzt in
( e1 ) . SelectMany( x1 => e2 , ( x1 , x2 ) => v )

Ein Abfrage Ausdruck mit einer zweiten from Klausel, gefolgt von einem anderen Element als einer- select
Klausel:

from x1 in e1
from x2 in e2
...

wird übersetzt in

from * in ( e1 ) . SelectMany( x1 => e2 , ( x1 , x2 ) => new { x1 , x2 } )


...

Ein Abfrage Ausdruck mit einer- let Klausel.

from x in e
let y = f
...

wird übersetzt in

from * in ( e ) . Select ( x => new { x , y = f } )


...

Ein Abfrage Ausdruck mit einer- where Klausel.

from x in e
where f
...

wird übersetzt in

from x in ( e ) . Where ( x => f )


...

Ein Abfrage Ausdruck mit einer- join Klausel ohne, into gefolgt von einer- select Klausel.

from x1 in e1
join x2 in e2 on k1 equals k2
select v

wird übersetzt in

( e1 ) . Join( e2 , x1 => k1 , x2 => k2 , ( x1 , x2 ) => v )

Ein Abfrage Ausdruck mit einer- join Klausel ohne, into gefolgt von einem anderen als einer- select Klausel.
from x1 in e1
join x2 in e2 on k1 equals k2
...

wird übersetzt in

from * in ( e1 ) . Join( e2 , x1 => k1 , x2 => k2 , ( x1 , x2 ) => new { x1 , x2 })


...

Ein Abfrage Ausdruck mit einer- join Klausel mit einer, into gefolgt von einer- select Klausel.

from x1 in e1
join x2 in e2 on k1 equals k2 into g
select v

wird übersetzt in

( e1 ) . GroupJoin( e2 , x1 => k1 , x2 => k2 , ( x1 , g ) => v )

Ein Abfrage Ausdruck mit einer- join Klausel mit einem into gefolgt von einer- select Klausel.

from x1 in e1
join x2 in e2 on k1 equals k2 into g
...

wird übersetzt in

from * in ( e1 ) . GroupJoin( e2 , x1 => k1 , x2 => k2 , ( x1 , g ) => new { x1 , g })


...

Ein Abfrage Ausdruck mit einer- orderby Klausel.

from x in e
orderby k1 , k2 , ..., kn
...

wird übersetzt in

from x in ( e ) .
OrderBy ( x => k1 ) .
ThenBy ( x => k2 ) .
... .
ThenBy ( x => kn )
...

Wenn eine ORDER-Klausel einen descending Richtungsindikator angibt, wird stattdessen ein Aufruf von
OrderByDescending oder ThenByDescending erzeugt.

Bei den folgenden Übersetzungen wird davon ausgegangen, dass es keine let where join -, orderby -oder-
Klauseln und nicht mehr als eine anfängliche- from Klausel in jedem Abfrage Ausdruck gibt.
Das Beispiel
from c in customers
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }

wird übersetzt in

customers.
SelectMany(c => c.Orders,
(c,o) => new { c.Name, o.OrderID, o.Total }
)

Das Beispiel

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

wird übersetzt in

from * in customers.
SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

die letzte Übersetzung, von der

customers.
SelectMany(c => c.Orders, (c,o) => new { c, o }).
OrderByDescending(x => x.o.Total).
Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })

dabei x ist ein vom Compiler generierter Bezeichner, der andernfalls unsichtbar ist und nicht zugänglich ist.
Das Beispiel

from o in orders
let t = o.Details.Sum(d => d.UnitPrice * d.Quantity)
where t >= 1000
select new { o.OrderID, Total = t }

wird übersetzt in

from * in orders.
Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
where t >= 1000
select new { o.OrderID, Total = t }

die letzte Übersetzung, von der

orders.
Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }).
Where(x => x.t >= 1000).
Select(x => new { x.o.OrderID, Total = x.t })
dabei x ist ein vom Compiler generierter Bezeichner, der andernfalls unsichtbar ist und nicht zugänglich ist.
Das Beispiel

from c in customers
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }

wird übersetzt in

customers.Join(orders, c => c.CustomerID, o => o.CustomerID,


(c, o) => new { c.Name, o.OrderDate, o.Total })

Das Beispiel

from c in customers
join o in orders on c.CustomerID equals o.CustomerID into co
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }

wird übersetzt in

from * in customers.
GroupJoin(orders, c => c.CustomerID, o => o.CustomerID,
(c, co) => new { c, co })
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }

die letzte Übersetzung, von der

customers.
GroupJoin(orders, c => c.CustomerID, o => o.CustomerID,
(c, co) => new { c, co }).
Select(x => new { x, n = x.co.Count() }).
Where(y => y.n >= 10).
Select(y => new { y.x.c.Name, OrderCount = y.n)

dabei x sind und vom y Compiler generierte Bezeichner, die ansonsten unsichtbar sind und nicht zugänglich
sind.
Das Beispiel

from o in orders
orderby o.Customer.Name, o.Total descending
select o

hat die endgültige Übersetzung

orders.
OrderBy(o => o.Customer.Name).
ThenByDescending(o => o.Total)

Select-Klauseln
Ein Abfrage Ausdruck der Form

from x in e select v

wird übersetzt in

( e ) . Select ( x => v )

mit der Ausnahme, dass v der Bezeichner x ist, ist die Übersetzung einfach

( e )

Beispiel:

from c in customers.Where(c => c.City == "London")


select c

wird einfach in übersetzt

customers.Where(c => c.City == "London")

GroupBy-Klauseln
Ein Abfrage Ausdruck der Form

from x in e group v by k

wird übersetzt in

( e ) . GroupBy ( x => k , x => v )

außer wenn v der Bezeichner x ist, ist die Übersetzung

( e ) . GroupBy ( x => k )

Das Beispiel

from c in customers
group c.Name by c.Country

wird übersetzt in

customers.
GroupBy(c => c.Country, c => c.Name)

Transparente Bezeichner
Bestimmte Übersetzungen fügen Bereichs Variablen mit *transparenten Bezeichner _ ein, die von angegeben
werden _ . Transparente Bezeichner sind keine ordnungsgemäße Sprachfunktion. Sie sind nur als
Zwischenschritt im Übersetzungsprozess des Abfrage Ausdrucks vorhanden.
Wenn eine Abfrage Übersetzung einen transparenten Bezeichner einfügt, verbreiten weitere
Übersetzungsschritte den transparenten Bezeichner an anonyme Funktionen und anonyme Objektinitialisierer.
In diesen Kontexten weisen transparente Bezeichner folgendes Verhalten auf:
Wenn ein transparenter Bezeichner als Parameter in einer anonymen Funktion auftritt, werden die Member
des zugeordneten anonymen Typs automatisch im Gültigkeitsbereich des Texts der anonymen Funktion
angezeigt.
Wenn sich ein Element mit einem transparenten Bezeichner im Gültigkeitsbereich befindet, sind die Member
dieses Members ebenfalls im Gültigkeitsbereich.
Wenn ein transparenter Bezeichner als Member-Deklarator in einem anonymen Objektinitialisierer auftritt,
führt er einen Member mit einem transparenten Bezeichner ein.
In den oben beschriebenen Übersetzungs Schritten werden transparente Bezeichner immer mit anonymen
Typen eingeführt, mit dem Ziel, mehrere Bereichs Variablen als Member eines einzelnen Objekts zu erfassen.
Eine Implementierung von c# darf einen anderen Mechanismus als anonyme Typen verwenden, um mehrere
Bereichs Variablen zu gruppieren. In den folgenden Übersetzungs Beispielen wird davon ausgegangen, dass
anonyme Typen verwendet werden, und es wird gezeigt, wie transparente Bezeichner übersetzt werden
können.
Das Beispiel

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.Total }

wird übersetzt in

from * in customers.
SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.Total }

weiter übersetzt in

customers.
SelectMany(c => c.Orders, (c,o) => new { c, o }).
OrderByDescending(* => o.Total).
Select(* => new { c.Name, o.Total })

, die beim Löschen transparenter Bezeichner entspricht.

customers.
SelectMany(c => c.Orders, (c,o) => new { c, o }).
OrderByDescending(x => x.o.Total).
Select(x => new { x.c.Name, x.o.Total })

dabei x ist ein vom Compiler generierter Bezeichner, der andernfalls unsichtbar ist und nicht zugänglich ist.
Das Beispiel
from c in customers
join o in orders on c.CustomerID equals o.CustomerID
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

wird übersetzt in

from * in customers.
Join(orders, c => c.CustomerID, o => o.CustomerID,
(c, o) => new { c, o })
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

Dies wird weiter reduziert auf

customers.
Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }).
Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d }).
Join(products, * => d.ProductID, p => p.ProductID, (*, p) => new { *, p }).
Select(* => new { c.Name, o.OrderDate, p.ProductName })

die letzte Übersetzung, von der

customers.
Join(orders, c => c.CustomerID, o => o.CustomerID,
(c, o) => new { c, o }).
Join(details, x => x.o.OrderID, d => d.OrderID,
(x, d) => new { x, d }).
Join(products, y => y.d.ProductID, p => p.ProductID,
(y, p) => new { y, p }).
Select(z => new { z.y.x.c.Name, z.y.x.o.OrderDate, z.p.ProductName })

dabei x y sind, und vom Compiler generierte Bezeichner z , die andernfalls unsichtbar und nicht zugänglich
sind.
Das Abfrage Ausdrucksmuster
Das Abfrage Ausdrucksmuster stellt ein Muster von Methoden dar, die von Typen implementiert werden
können, um Abfrage Ausdrücke zu unterstützen. Da Abfrage Ausdrücke über eine syntaktische Zuordnung in
Methodenaufrufe übersetzt werden, haben Typen bei der Implementierung des Abfrage Ausdrucks Musters eine
beträchtliche Flexibilität. Beispielsweise können die Methoden des Musters als Instanzmethoden oder als
Erweiterungs Methoden implementiert werden, da beide die gleiche Aufruf Syntax aufweisen und die Methoden
Delegaten oder Ausdrucks Baumstrukturen anfordern können, da anonyme Funktionen in beides konvertierbar
sind.
Die empfohlene Form eines generischen Typs C<T> , der das Abfrage Ausdrucksmuster unterstützt, ist unten
dargestellt. Ein generischer Typ wird verwendet, um die richtigen Beziehungen zwischen Parameter-und
Ergebnistypen zu veranschaulichen, aber es ist auch möglich, das Muster für nicht generische Typen zu
implementieren.
delegate R Func<T1,R>(T1 arg1);

delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);

class C
{
public C<T> Cast<T>();
}

class C<T> : C
{
public C<T> Where(Func<T,bool> predicate);

public C<U> Select<U>(Func<T,U> selector);

public C<V> SelectMany<U,V>(Func<T,C<U>> selector,


Func<T,U,V> resultSelector);

public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,


Func<U,K> innerKeySelector, Func<T,U,V> resultSelector);

public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,


Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector);

public O<T> OrderBy<K>(Func<T,K> keySelector);

public O<T> OrderByDescending<K>(Func<T,K> keySelector);

public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector);

public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector,


Func<T,E> elementSelector);
}

class O<T> : C<T>


{
public O<T> ThenBy<K>(Func<T,K> keySelector);

public O<T> ThenByDescending<K>(Func<T,K> keySelector);


}

class G<K,T> : C<T>


{
public K Key { get; }
}

Die oben genannten Methoden verwenden die generischen Delegattypen Func<T1,R> und Func<T1,T2,R> , aber
Sie könnten auch andere Delegattypen von Delegaten oder Ausdrücken mit denselben Beziehungen in
Parameter-und Ergebnistypen verwenden.
Beachten Sie die empfohlene Beziehung zwischen C<T> und O<T> , wodurch sichergestellt wird, dass die
ThenBy -Methode und die- ThenByDescending Methode nur für das Ergebnis eines oder verfügbar sind OrderBy
OrderByDescending . Beachten Sie auch die empfohlene Form des Ergebnisses von GroupBy --eine Sequenz von
Sequenzen, wobei jede innere Sequenz über eine zusätzliche Key Eigenschaft verfügt.
Der- System.Linq Namespace stellt eine Implementierung des Abfrage Operator Musters für jeden Typ bereit,
der die- System.Collections.Generic.IEnumerable<T> Schnittstelle implementiert.

Zuweisungsoperatoren
Die Zuweisungs Operatoren weisen einer Variablen, einer Eigenschaft, einem Ereignis oder einem Indexer-
Element einen neuen Wert zu.
assignment
: unary_expression assignment_operator expression
;

assignment_operator
: '='
| '+='
| '-='
| '*='
| '/='
| '%='
| '&='
| '|='
| '^='
| '<<='
| right_shift_assignment
;

Der linke Operand einer Zuweisung muss ein Ausdruck sein, der als Variable, Eigenschaften Zugriff,
Indexerzugriff oder Ereignis Zugriff klassifiziert ist.
Der- = Operator wird als einfacher Zuweisungs Operator bezeichnet. Er weist den Wert des rechten
Operanden der Variablen, der Eigenschaft oder dem Indexer-Element zu, die durch den linken Operanden
angegeben werden. Der linke Operand des einfachen Zuweisungs Operators ist möglicherweise kein Ereignis
Zugriff (außer wie in Feld ähnlichen Ereignissenbeschrieben). Der einfache Zuweisungs Operator wird unter
einfache Zuweisungbeschrieben.
Die anderen Zuweisungs Operatoren als der- = Operator werden als Verbund Zuweisungs Operatoren
bezeichnet. Diese Operatoren führen den angegebenen Vorgang für die beiden Operanden aus und weisen
dann den resultierenden Wert der Variablen, der Eigenschaft oder dem Indexer-Element zu, das durch den linken
Operanden angegeben wird. Die Verbund Zuweisungs Operatoren werden unter Verbund
Zuweisungbeschrieben.
Die += -= Operatoren und mit einem Ereignis Zugriffs Ausdruck als Linker Operand werden als Ereignis
Zuweisungs Operatoren bezeichnet. Kein anderer Zuweisungs Operator ist mit einem Ereignis Zugriff als Linker
Operand gültig. Die Ereignis Zuweisungs Operatoren werden unter Ereignis Zuweisungbeschrieben.
Die Zuweisungs Operatoren sind rechts assoziativ, was bedeutet, dass Vorgänge von rechts nach Links gruppiert
werden. Beispielsweise wird ein Ausdruck des Formulars a = b = c als ausgewertet a = (b = c) .
Einfache Zuweisung
Der- = Operator wird als einfacher Zuweisungs Operator bezeichnet.
Wenn der linke Operand einer einfachen Zuweisung das Format E.P aufweist oder wenn E[Ei] E den
Kompilier Zeittyp aufweist dynamic , wird die Zuweisung dynamisch gebunden (dynamische Bindung). In
diesem Fall ist der Kompilier Zeittyp des Zuweisungs Ausdrucks dynamic , und die unten beschriebene
Auflösung erfolgt zur Laufzeit basierend auf dem Lauf Zeittyp von E .
In einer einfachen Zuweisung muss der rechte Operand ein Ausdruck sein, der implizit in den Typ des linken
Operanden konvertiert werden kann. Der-Vorgang weist den Wert des rechten Operanden der Variablen, der
Eigenschaft oder dem Indexer-Element zu, die durch den linken Operanden angegeben werden.
Das Ergebnis eines einfachen Zuweisungs Ausdrucks ist der Wert, der dem linken Operanden zugewiesen wird.
Das Ergebnis hat denselben Typ wie der linke Operand und wird immer als Wert klassifiziert.
Wenn der linke Operand eine Eigenschaft oder ein Indexer-Zugriff ist, muss die Eigenschaft oder der Indexer
über einen set Accessor verfügen. Wenn dies nicht der Fall ist, tritt ein Bindungs Zeitfehler auf.
Die Lauf Zeit Verarbeitung einer einfachen Zuweisung des Formulars x = y besteht aus den folgenden
Schritten:
Wenn xals Variable klassifiziert ist:
x wird ausgewertet, um die Variable zu entwickeln.
y wird ausgewertet und, falls erforderlich, durch eine implizite Konvertierung in den Typ von
konvertiert x (implizite Konvertierungen).
Wenn die von angegebene Variable x ein Array Element einer reference_type ist, wird eine Lauf Zeit
Überprüfung durchgeführt, um sicherzustellen, dass der für berechnete Wert y mit der Array Instanz
von kompatibel ist, bei der x es sich um ein Element handelt. Die Überprüfung y ist erfolgreich,
wenn ist null , oder wenn eine implizite Verweis Konvertierung (implizite Verweis Konvertierungen)
aus dem tatsächlichen Typ der Instanz vorhanden ist, auf die von verwiesen wird, y auf den
tatsächlichen Elementtyp der Array Instanz, die enthält x . Andernfalls wird eine
System.ArrayTypeMismatchException ausgelöst.
Der Wert, der sich aus der Auswertung und Konvertierung von ergibt, y wird an dem Speicherort
gespeichert, der durch die Auswertung von angegeben wird x .
Wenn x als Eigenschaft oder Indexer-Zugriff klassifiziert ist:
Der Instanzausdruck (wenn x nicht ist static ), und die Argumentliste (wenn x ein Indexer-Zugriff
ist), die zugeordnet ist x , wird ausgewertet, und die Ergebnisse werden im nachfolgenden
accessoraufruf verwendet set .
y wird ausgewertet und, falls erforderlich, durch eine implizite Konvertierung in den Typ von
konvertiert x (implizite Konvertierungen).
Der- set Accessor von x wird mit dem Wert aufgerufen, der für y als sein-Argument berechnet
wird value .

Die Array-Co-Varianz Regeln (Array-Kovarianz) erlauben, dass ein Wert eines Arraytyps A[] ein Verweis auf
eine Instanz eines Arraytyps B[] ist, vorausgesetzt, dass eine implizite Verweis Konvertierung von in
vorhanden ist B A . Aufgrund dieser Regeln erfordert die Zuweisung zu einem Array Element einer
reference_type eine Lauf Zeit Überprüfung, um sicherzustellen, dass der zugewiesene Wert mit der Array Instanz
kompatibel ist. Im Beispiel

string[] sa = new string[10];


object[] oa = sa;

oa[0] = null; // Ok
oa[1] = "Hello"; // Ok
oa[2] = new ArrayList(); // ArrayTypeMismatchException

die letzte Zuweisung bewirkt System.ArrayTypeMismatchException , dass eine ausgelöst wird, da eine Instanz von
ArrayList nicht in einem Element von gespeichert werden kann string[] .

Wenn eine in einem struct_type deklarierte Eigenschaft oder ein Indexer das Ziel einer Zuweisung ist, muss der
Instanzausdruck, der der Eigenschaft oder dem Indexer-Zugriff zugeordnet ist, als Variable klassifiziert werden.
Wenn der Instanzausdruck als Wert klassifiziert wird, tritt ein Fehler bei der Bindung auf. Aufgrund des Mitglieds
Zugriffsgilt die gleiche Regel auch für Felder.
Bei den Deklarationen:
struct Point
{
int x, y;

public Point(int x, int y) {


this.x = x;
this.y = y;
}

public int X {
get { return x; }
set { x = value; }
}

public int Y {
get { return y; }
set { y = value; }
}
}

struct Rectangle
{
Point a, b;

public Rectangle(Point a, Point b) {


this.a = a;
this.b = b;
}

public Point A {
get { return a; }
set { a = value; }
}

public Point B {
get { return b; }
set { b = value; }
}
}

im Beispiel

Point p = new Point();


p.X = 100;
p.Y = 100;
Rectangle r = new Rectangle();
r.A = new Point(10, 10);
r.B = p;

die Zuweisungen für p.X , p.Y , r.A und r.B sind zulässig, da p und r Variablen sind. Im Beispiel

Rectangle r = new Rectangle();


r.A.X = 10;
r.A.Y = 10;
r.B.X = 100;
r.B.Y = 100;

die Zuweisungen sind ungültig, da r.A und r.B keine Variablen sind.
Verbundzuweisung
Wenn der linke Operand einer Verbund Zuweisung das Format E.P aufweist oder wenn E[Ei] E den
Kompilier Zeittyp aufweist dynamic , wird die Zuweisung dynamisch gebunden (dynamische Bindung). In
diesem Fall ist der Kompilier Zeittyp des Zuweisungs Ausdrucks dynamic , und die unten beschriebene
Auflösung erfolgt zur Laufzeit basierend auf dem Lauf Zeittyp von E .
Ein Vorgang des Formulars x op= y wird verarbeitet, indem die binäre Operator Überladungs Auflösung
(binäre Operator Überladungs Auflösung) angewendet wird, als ob der Vorgang geschrieben wurde x op y .
Dies ergibt folgende Szenarien:
Wenn der Rückgabetyp des ausgewählten Operators implizit in den Typ von konvertiert x werden kann,
wird der Vorgang als ausgewertet x = x op y , mit der Ausnahme, dass x nur einmal ausgewertet wird.
Andernfalls wird, wenn der ausgewählte Operator ein vordefinierter Operator ist, der Rückgabetyp des
ausgewählten Operators explizit in den Typ von konvertiert x werden kann, und wenn y implizit in den
Typ von konvertiert werden kann, x oder wenn der Operator ein Shift-Operator ist, wird der Vorgang als
ausgewertet x = (T)(x op y) , wobei T der Typ von ist x , außer dass x nur einmal ausgewertet wird.
Andernfalls ist die Verbund Zuweisung ungültig, und es tritt ein Bindungs Zeitfehler auf.
Der Begriff "nur einmal ausgewertet" bedeutet, dass bei der Auswertung von x op y die Ergebnisse aller
eingebenden Ausdrücke von x temporär gespeichert und dann wieder verwendet werden, wenn die
Zuweisung zu erfolgt x . Beispielsweise werden in der A()[B()] += C() -Zuweisung, bei der A eine Methode
zurückgibt int[] , und B und C sind Methoden zurückgegeben int , die-Methoden nur einmal in der
Reihenfolge, A , aufgerufen B C .
Wenn der linke Operand einer Verbund Zuweisung ein Eigenschaften Zugriff oder ein Indexer-Zugriff ist, muss
die Eigenschaft oder der Indexer sowohl einen get -Accessor als auch einen- set Accessor aufweisen. Wenn
dies nicht der Fall ist, tritt ein Bindungs Zeitfehler auf.
Mit der zweiten obigen Regel x op= y können Sie x = (T)(x op y) in bestimmten Kontexten als ausgewertet
werden. Die Regel besteht darin, dass die vordefinierten Operatoren als Verbund Operatoren verwendet werden
können, wenn der linke Operand vom Typ sbyte , byte , short , ushort oder ist char . Auch wenn beide
Argumente von einem dieser Typen sind, führen die vordefinierten Operatoren zu einem Ergebnis vom Typ int
, wie in Binäre numerischeErweiterungen beschrieben. Daher wäre es ohne Umwandlung nicht möglich, das
Ergebnis dem linken Operanden zuzuweisen.
Die intuitive Auswirkung der Regel auf vordefinierte Operatoren ist nur x op= y zulässig, wenn sowohl x op y
als auch zulässig x = y sind. Im Beispiel

byte b = 0;
char ch = '\0';
int i = 0;

b += 1; // Ok
b += 1000; // Error, b = 1000 not permitted
b += i; // Error, b = i not permitted
b += (byte)i; // Ok

ch += 1; // Error, ch = 1 not permitted


ch += (char)1; // Ok

der intuitive Grund für jeden Fehler ist, dass eine entsprechende einfache Zuweisung ebenfalls ein Fehler wäre.
Dies bedeutet auch, dass aufgesetzte Zuweisungs Vorgänge aufgesetzte Vorgänge unterstützen. Im Beispiel

int? i = 0;
i += 1; // Ok

der Operator "angehoben" +(int?,int?) wird verwendet.


Ereignis Zuweisung
Wenn der linke Operand eines += or- -= Operators als Ereignis Zugriff klassifiziert ist, wird der Ausdruck wie
folgt ausgewertet:
Der Instanzausdruck des Ereignis Zugriffs wird ausgewertet, falls vorhanden.
Der rechte Operand des += Operators or -= wird ausgewertet und, falls erforderlich, durch eine implizite
Konvertierung in den Typ des linken Operanden konvertiert (implizite Konvertierungen).
Ein Ereignis Accessor des Ereignisses wird mit einer Argumentliste aufgerufen, die den rechten Operanden
nach der Auswertung und ggf. Konvertierung umfasst. Wenn der-Operator war += , add wird der-Accessor
aufgerufen. wenn der-Operator war -= , wird der- remove Accessor aufgerufen.
Ein Ereignis Zuweisungs Ausdruck ergibt keinen Wert. Daher ist ein Ereignis Zuweisungs Ausdruck nur im
Kontext einer statement_expression (Ausdrucks Anweisungen) gültig.

Ausdruck
Ein Ausdruck ist entweder eine non_assignment_expression oder eine Zuweisung.

expression
: non_assignment_expression
| assignment
;

non_assignment_expression
: conditional_expression
| lambda_expression
| query_expression
;

Konstante Ausdrücke
Ein constant_expression ist ein Ausdruck, der zur Kompilierzeit vollständig ausgewertet werden kann.

constant_expression
: expression
;

Ein konstanter Ausdruck muss das null Literale oder ein-Wert mit einem der folgenden Typen sein: sbyte ,
byte , short , ushort , int , uint , long , ulong , char , float , double , decimal , bool , object , oder
ein string beliebiger Enumerationstyp. Nur die folgenden Konstrukte sind in konstanten Ausdrücken zulässig:
Literale (einschließlich Literale null ).
Verweise auf Member const der Klassen-und Strukturtypen.
Verweise auf Member von Enumerationstypen.
Verweise auf const Parameter oder lokale Variablen
Unter Ausdrücke in Klammern, die selbst Konstante Ausdrücke sind.
Umwandlungs Ausdrücke, wenn der Zieltyp einem der oben aufgeführten Typen entspricht.
checked -und- unchecked Ausdrücke
Standardwert Ausdrücke
Nameof-Ausdrücke
Die vordefinierten + - ! Operatoren,, und ~ .
Der vordefinierte,,,,,,,,,,,,,,,, + - * / % << >> & | ^ && || == != < > <= , und >= binäre
Operatoren, sofern jeder Operand einen oben aufgeführten Typ hat.
Der ?: bedingte Operator.

Die folgenden Konvertierungen sind in konstanten Ausdrücken zulässig:


Identitäts Konvertierungen
Numerische Konvertierungen
Enumerationskonvertierungen
Konstante Ausdrucks Konvertierungen
Implizite und explizite Verweis Konvertierungen, vorausgesetzt, dass die Quelle der Konvertierungen ein
konstanter Ausdruck ist, der zu einem NULL-Wert ausgewertet wird.
Andere Konvertierungen, einschließlich Boxing, Unboxing und impliziter Verweis Konvertierungen von nicht-
NULL-Werten, sind in konstanten Ausdrücken nicht zulässig. Beispiel:

class C {
const object i = 5; // error: boxing conversion not permitted
const object str = "hello"; // error: implicit reference conversion
}

die Initialisierung von i ist ein Fehler, da eine Boxing-Konvertierung erforderlich ist. Die Initialisierung von Str ist
ein Fehler, da eine implizite Verweis Konvertierung von einem nicht-NULL-Wert erforderlich ist.
Wenn ein Ausdruck die oben aufgeführten Anforderungen erfüllt, wird der Ausdruck zur Kompilierzeit
ausgewertet. Dies gilt auch, wenn der Ausdruck ein unter Ausdruck eines größeren Ausdrucks ist, der nicht
konstante Konstrukte enthält.
Die Kompilierzeit Auswertung konstanter Ausdrücke verwendet dieselben Regeln wie die Lauf Zeit Auswertung
von nicht konstanten Ausdrücken, mit dem Unterschied, dass die Kompilierzeit Auswertung dazu führt, dass ein
Kompilierzeitfehler auftritt, wenn die Lauf Zeit Auswertung eine Ausnahme ausgelöst hätte.
Wenn ein konstanter Ausdruck nicht explizit in einem Kontext abgelegt wird unchecked , verursachen über
Flüsse, die in arithmetischen Operationen und Konvertierungen von ganzzahligen Typen auftreten, während der
Kompilierzeit Auswertung des Ausdrucks immer Kompilierzeitfehler (Konstante Ausdrücke).
In den unten aufgeführten Kontexten treten Konstante Ausdrücke auf. In diesen Kontexten tritt ein
Kompilierzeitfehler auf, wenn ein Ausdruck zur Kompilierzeit nicht vollständig ausgewertet werden kann.
Konstante Deklarationen (Konstanten).
Deklarationen von Enumerationsmembern (enum-Member).
Standardargumente von formalen Parameterlisten (Methoden Parameter)
case Bezeichnungen einer- switch Anweisung (Switch-Anweisung).
goto case -Anweisungen (die GoTo-Anweisung).
Dimensions Längen in einem Array Erstellungs Ausdruck (Array Erstellungs Ausdrücke), der einen
Initialisierer einschließt.
Attribute (Attribute).
Eine implizite Konstante Ausdrucks Konvertierung (implizite Konstante Ausdrucks Konvertierungen) ermöglicht
das Konvertieren eines konstanten Ausdrucks vom Typ int in sbyte , byte , short , ushort , uint oder
ulong , wenn der Wert des konstanten Ausdrucks innerhalb des Bereichs des Zieltyps liegt.

Boolesche Ausdrücke
Ein Boolean_expression ist ein Ausdruck, der ein Ergebnis des Typs ergibt bool , entweder direkt oder durch
Anwendung von operator true in bestimmten Kontexten, wie im folgenden angegeben.
boolean_expression
: expression
;

Der steuernde bedingte Ausdruck eines if_statement (der if-Anweisung), while_statement (die while-Anweisung),
do_statement (die Do-Anweisung) oder for_Statement (die for-Anweisung) ist eine Boolean_expression. Der
steuernde bedingte Ausdruck des ?: Operators (Bedingter Operator) folgt denselben Regeln wie eine
Boolean_expression, aber aus Gründen der Operator Rangfolge wird dies als conditional_or_expression
klassifiziert.
Ein Boolean_expression E ist erforderlich, um in der Lage zu sein, einen Wert vom Typ zu erhalten bool , wie
im folgenden dargestellt:
Wenn E implizit in konvertierbar ist, wird bool zur Laufzeit die implizite Konvertierung angewendet.
Andernfalls wird die Überladungs Auflösung des unären Operators (unäre Operator Überladungs Auflösung)
verwendet, um eine eindeutige beste Implementierung des Operators in zu finden true E , und diese
Implementierung wird zur Laufzeit angewendet.
Wenn kein solcher Operator gefunden wird, tritt ein Bindungs Zeitfehler auf.
Der DBBool Strukturtyp in einem booleschen Datenbanktyp bietet ein Beispiel für einen Typ, der operator true
und implementiert operator false .
Anweisungen
04.11.2021 • 100 minutes to read

C# stellt eine Reihe von-Anweisungen bereit. Die meisten dieser Anweisungen werden Entwicklern vertraut sein,
die in C und C++ programmiert haben.

statement
: labeled_statement
| declaration_statement
| embedded_statement
;

embedded_statement
: block
| empty_statement
| expression_statement
| selection_statement
| iteration_statement
| jump_statement
| try_statement
| checked_statement
| unchecked_statement
| lock_statement
| using_statement
| yield_statement
| embedded_statement_unsafe
;

Das embedded_statement nicht Terminal wird für-Anweisungen verwendet, die in anderen-Anweisungen


angezeigt werden. Die Verwendung von embedded_statement anstelle von- Anweisungen schließt die
Verwendung von Deklarations Anweisungen und bezeichneten Anweisungen in diesen Kontexten aus. Das
Beispiel

void F(bool b) {
if (b)
int i = 44;
}

führt zu einem Kompilierzeitfehler, da eine- if Anweisung anstelle einer- Anweisung für Ihre if-Verzweigung
eine embedded_statement erfordert. Wenn dieser Code zulässig ist, wird die Variable i deklariert, Sie konnte
aber nie verwendet werden. Beachten Sie jedoch, dass i das Beispiel durch Platzieren der Deklaration in
einem-Block gültig ist.

End points and reachability (Endpunkte und Erreichbarkeit)


Jede-Anweisung verfügt über einen Endpunkt . Der Endpunkt einer-Anweisung ist in intuitiver Hinsicht der
Speicherort, der direkt auf die-Anweisung folgt. Die Ausführungs Regeln für zusammengesetzte Anweisungen
(Anweisungen, die eingebettete Anweisungen enthalten) geben die Aktion an, die durchgeführt wird, wenn das
Steuerelement den Endpunkt einer eingebetteten Anweisung erreicht. Wenn das Steuerelement z. b. den
Endpunkt einer-Anweisung in einem-Block erreicht, wird die Steuerung an die nächste Anweisung im-Block
übertragen.
Wenn eine-Anweisung möglicherweise durch die Ausführung erreicht werden kann, wird die-Anweisung als
*erreichbar _ bezeichnet. Wenn es nicht möglich ist, dass eine-Anweisung ausgeführt wird, wird die-
Anweisung als _ nicht erreichbar * bezeichnet.
Im Beispiel

void F() {
Console.WriteLine("reachable");
goto Label;
Console.WriteLine("unreachable");
Label:
Console.WriteLine("reachable");
}

der zweite Aufruf von Console.WriteLine ist nicht erreichbar, da es nicht möglich ist, dass die Anweisung
ausgeführt wird.
Eine Warnung wird gemeldet, wenn der Compiler feststellt, dass eine-Anweisung nicht erreichbar ist. Es ist kein
Fehler, wenn eine-Anweisung nicht erreichbar ist.
Um zu ermitteln, ob eine bestimmte Anweisung oder ein Endpunkt erreichbar ist, führt der Compiler die Fluss
Analyse gemäß den für jede Anweisung definierten Erreichbarkeits Regeln aus. Die Fluss Analyse berücksichtigt
die Werte konstanter Ausdrücke (Konstantenausdrücke), die das Verhalten von-Anweisungen steuern, aber die
möglichen Werte von nicht konstanten Ausdrücken werden nicht berücksichtigt. Dies bedeutet, dass ein nicht
konstanter Ausdruck eines bestimmten Typs für die Ablauf Steuerungs Analyse einen möglichen Wert dieses
Typs hat.
Im Beispiel

void F() {
const int i = 1;
if (i == 2) Console.WriteLine("unreachable");
}

der boolesche Ausdruck der- if Anweisung ist ein konstanter Ausdruck, da beide Operanden des ==
Operators Konstanten sind. Da der Konstante Ausdruck zum Zeitpunkt der Kompilierung ausgewertet wird und
den Wert erzeugt false , Console.WriteLine wird der Aufruf als nicht erreichbar betrachtet. Wenn jedoch i in
eine lokale Variable geändert wird

void F() {
int i = 1;
if (i == 2) Console.WriteLine("reachable");
}

der Console.WriteLine Aufruf wird als erreichbar betrachtet, obwohl er in Wirklichkeit nie ausgeführt wird.
Der Block eines Funktionsmembers gilt immer als erreichbar. Wenn Sie die Erreichbarkeits Regeln der einzelnen
Anweisungen in einem Block nacheinander auswerten, kann die Erreichbarkeit einer beliebigen Anweisung
bestimmt werden.
Im Beispiel

void F(int x) {
Console.WriteLine("start");
if (x < 0) Console.WriteLine("negative");
}

die Erreichbarkeit der zweiten Console.WriteLine wird wie folgt bestimmt:


Die erste Console.WriteLine Ausdrucks Anweisung ist erreichbar, da der-Block der- F Methode erreichbar
ist.
Der Endpunkt der ersten Console.WriteLine Ausdrucks Anweisung ist erreichbar, da diese Anweisung
erreichbar ist.
Die- if Anweisung ist erreichbar, da der Endpunkt der ersten Console.WriteLine Ausdrucks Anweisung
erreichbar ist.
Die zweite Console.WriteLine Ausdrucks Anweisung ist erreichbar, da der boolesche Ausdruck der if
Anweisung nicht über den konstanten Wert verfügt false .

Es gibt zwei Situationen, in denen es sich um einen Kompilierzeitfehler für den Endpunkt einer-Anweisung
handelt, der erreichbar ist:
Da die switch Anweisung nicht zulässt, dass ein Switch-Abschnitt zum nächsten switch-Abschnitt
"durchläuft", ist dies ein Kompilierzeitfehler für den Endpunkt der Anweisungs Liste eines Switch-Abschnitts,
der erreichbar ist. Wenn dieser Fehler auftritt, ist dies in der Regel ein Hinweis darauf, dass eine- break
Anweisung fehlt.
Es handelt sich um einen Kompilierzeitfehler für den Endpunkt des Blocks eines Funktionsmembers, der
einen Wert berechnet, der erreichbar ist. Wenn dieser Fehler auftritt, ist dies in der Regel ein Hinweis darauf,
dass eine- return Anweisung fehlt.

Blöcke
Ein Block ermöglicht, mehrere Anweisungen in Kontexten zu schreiben, in denen eine einzelne Anweisung
zulässig ist.

block
: '{' statement_list? '}'
;

Ein- Block besteht aus einem optionalen statement_list (Anweisungs Listen), der in geschweiften Klammern
eingeschlossen ist. Wenn die Anweisungs Liste weggelassen wird, wird der Block als leer bezeichnet.
Ein-Block kann Deklarations Anweisungen (Deklarations Anweisungen) enthalten. Der Gültigkeitsbereich einer
lokalen Variablen oder Konstanten, die in einem-Block deklariert ist, ist der-Block.
Ein-Block wird wie folgt ausgeführt:
Wenn der-Block leer ist, wird die Steuerung an den Endpunkt des-Blocks übertragen.
Wenn der Block nicht leer ist, wird die Steuerung an die Anweisungs Liste übertragen. Wenn und wenn das
Steuerelement den Endpunkt der Anweisungs Liste erreicht, wird die Steuerung an den Endpunkt des Blocks
übertragen.
Die Anweisungs Liste eines-Blocks ist erreichbar, wenn der Block selbst erreichbar ist.
Der Endpunkt eines-Blocks ist erreichbar, wenn der-Block leer ist oder der Endpunkt der Anweisungs Liste
erreichbar ist.
Ein- Block , der eine oder mehrere- yield Anweisungen (die yield-Anweisung) enthält, wird als Iteratorblock
bezeichnet. Iteratorblöcke werden verwendet, um Funktionsmember als Iteratoren (Iteratoren) zu
implementieren. Für iteratorblöcke gelten einige zusätzliche Einschränkungen:
Es ist ein Kompilierzeitfehler, wenn eine return Anweisung in einem Iteratorblock angezeigt wird (
yield return Anweisungen sind jedoch zulässig).
Es ist ein Kompilierzeitfehler für einen Iteratorblock, der einen unsicheren Kontext (unsichere Kontexte)
enthalten soll. Ein Iteratorblock definiert immer einen sicheren Kontext, auch wenn seine Deklaration in
einem unsicheren Kontext eingebettet ist.
Anweisungs Listen
Eine -**Anweisungs Liste* _ besteht aus einer oder mehreren Anweisungen, die nacheinander geschrieben
werden. Anweisungs Listen treten in _block * s (Blöcken) und switch_block s (der Switch-Anweisung) auf.

statement_list
: statement+
;

Eine Anweisungs Liste wird ausgeführt, indem die Steuerung an die erste Anweisung übertragen wird. Wenn
und wenn das Steuerelement den Endpunkt einer-Anweisung erreicht, wird die Steuerung an die nächste
Anweisung übertragen. Wenn und wenn das Steuerelement den Endpunkt der letzten Anweisung erreicht, wird
die Steuerung an den Endpunkt der Anweisungs Liste übertragen.
Eine-Anweisung in einer Anweisungs Liste ist erreichbar, wenn mindestens einer der folgenden Punkte zutrifft:
Die-Anweisung ist die erste Anweisung, und die Anweisungs Liste selbst ist erreichbar.
Der Endpunkt der vorhergehenden Anweisung ist erreichbar.
Die-Anweisung ist eine Anweisung mit Bezeichnung, und auf die Bezeichnung wird durch eine erreichbare
goto Anweisung verwiesen.

Der Endpunkt einer Anweisungs Liste ist erreichbar, wenn der Endpunkt der letzten Anweisung in der Liste
erreichbar ist.

Die leere Anweisung


Ein empty_statement führt keine Aktion aus.

empty_statement
: ';'
;

Eine leere-Anweisung wird verwendet, wenn keine Vorgänge in einem Kontext durchgeführt werden müssen, in
dem eine-Anweisung erforderlich ist.
Durch die Ausführung einer leeren-Anweisung wird die Steuerung einfach an den Endpunkt der Anweisung
übertragen. Daher ist der Endpunkt einer leeren Anweisung erreichbar, wenn die leere Anweisung erreichbar ist.
Eine leere Anweisung kann beim Schreiben einer while Anweisung mit einem NULL-Text verwendet werden:

bool ProcessMessage() {...}

void ProcessMessages() {
while (ProcessMessage())
;
}

Außerdem kann eine leere Anweisung verwendet werden, um eine Bezeichnung direkt vor dem schließenden "
} " eines Blocks zu deklarieren:
void F() {
...
if (done) goto exit;
...
exit: ;
}

Anweisungen mit Bezeichnung


Ein labeled_statement ermöglicht einer-Anweisung, eine Bezeichnung als Präfix festzustellen. Anweisung mit
Bezeichnung ist in-Blöcken zulässig, aber nicht als eingebettete Anweisungen zulässig.

labeled_statement
: identifier ':' statement
;

Eine Anweisung mit Bezeichnung deklariert eine Bezeichnung mit dem Namen, der vom Bezeichner angegeben
wird. Der Gültigkeitsbereich einer Bezeichnung ist der gesamte Block, in dem die Bezeichnung deklariert wird,
einschließlich aller Blöcke, die in der Liste enthalten sind. Es handelt sich um einen Kompilierzeitfehler für zwei
Bezeichnungen mit dem gleichen Namen, die sich überlappende Bereiche befinden.
Auf eine Bezeichnung kann in- goto Anweisungen (der GOTO-Anweisung) innerhalb des Bereichs der
Bezeichnung verwiesen werden. Dies bedeutet, dass goto -Anweisungen die Steuerung innerhalb von Blöcken
und aus Blöcken, aber nie in Blöcke übertragen können.
Bezeichnungen verfügen über einen eigenen Deklarations Bereich und stören andere Bezeichner nicht. Das
Beispiel

int F(int x) {
if (x >= 0) goto x;
x = -x;
x: return x;
}

ist gültig und verwendet den Namen x sowohl als Parameter als auch als Bezeichnung.
Die Ausführung einer Anweisung mit Bezeichnung entspricht genau der Ausführung der Anweisung nach der
Bezeichnung.
Zusätzlich zur Erreichbarkeit der normalen Ablauf Steuerung ist eine Anweisung mit der Bezeichnung erreichbar,
wenn von einer erreichbaren Anweisung auf die Bezeichnung verwiesen wird goto . (Ausnahme: Wenn sich
eine- goto Anweisung innerhalb eines try finally -Blocks befindet, der einen-Block enthält, und die
Anweisung mit der Bezeichnung außerhalb von liegt try und der Endpunkt des finally Blocks nicht
erreichbar ist, kann die Anweisung mit der Bezeichnung nicht von dieser Anweisung erreicht werden goto .)

Deklarationsanweisungen
Ein- declaration_statement deklariert eine lokale Variable oder eine Konstante. Deklarations Anweisungen sind
in-Blöcken zulässig, aber nicht als eingebettete Anweisungen zulässig.

declaration_statement
: local_variable_declaration ';'
| local_constant_declaration ';'
;
Deklarationen von lokalen Variablen
Eine local_variable_declaration die eine oder mehrere lokale Variablen deklariert.

local_variable_declaration
: local_variable_type local_variable_declarators
;

local_variable_type
: type
| 'var'
;

local_variable_declarators
: local_variable_declarator
| local_variable_declarators ',' local_variable_declarator
;

local_variable_declarator
: identifier
| identifier '=' local_variable_initializer
;

local_variable_initializer
: expression
| array_initializer
| local_variable_initializer_unsafe
;

Der local_variable_type eines local_variable_declaration entweder direkt den Typ der Variablen angibt, die von
der Deklaration eingeführt wurden, oder gibt mit dem Bezeichner an var , dass der Typ basierend auf einem
Initialisierer abgeleitet werden soll. Auf den Typ folgt eine Liste von local_variable_declarator en, von denen jede
eine neue Variable einführt. Ein local_variable_declarator besteht aus einem Bezeichner , der die Variable
benennt, optional gefolgt von einem " = "-Token und einem local_variable_initializer , der den Anfangswert der
Variablen liefert.
Im Kontext einer lokalen Variablen Deklaration fungiert der Bezeichner var als kontextbezogenes Schlüsselwort
(SchlüsselWort). Wenn die local_variable_type als angegeben wird var und sich kein Typ mit dem Namen im
Gültigkeits var Bereich befindet, ist die Deklaration eine implizit typisier te lokale Variablen Deklaration ,
deren Typ vom Typ des zugeordneten initialisiererausdrucks abgeleitet wird. Implizit typisierte lokale Variablen
Deklarationen unterliegen den folgenden Einschränkungen:
Der local_variable_declaration kann nicht mehrere local_variable_declarator s enthalten.
Der local_variable_declarator muss ein local_variable_initializer enthalten.
Der local_variable_initializer muss ein Ausdruck sein.
Der initialisiererausdruck muss einen Kompilier Zeittyp aufweisen.
Der initialisiererausdruck kann nicht auf die deklarierte Variable selbst verweisen.
Im folgenden finden Sie Beispiele für falsche implizit typisierte lokale Variablen Deklarationen:

var x; // Error, no initializer to infer type from


var y = {1, 2, 3}; // Error, array initializer not permitted
var z = null; // Error, null does not have a type
var u = x => x + 1; // Error, anonymous functions do not have a type
var v = v++; // Error, initializer cannot refer to variable itself

Der Wert einer lokalen Variablen wird in einem Ausdruck mit einem Simple_name (einfache Namen) abgerufen,
und der Wert einer lokalen Variablen wird mithilfe einer Zuweisung (Zuweisungs Operatoren) geändert. Eine
lokale Variable muss an jedem Speicherort, an dem ihr Wert abgerufen wird, definitiv zugewiesen werden
(definitive Zuweisung).
Der Gültigkeitsbereich einer lokalen Variablen, die in einem local_variable_declaration deklariert ist, ist der
Block, in dem die Deklaration auftritt. Es ist ein Fehler, auf eine lokale Variable in einer Textposition zu verweisen,
die dem local_variable_declarator der lokalen Variablen vorangestellt ist. Innerhalb des Gültigkeits Bereichs einer
lokalen Variablen ist es ein Kompilierzeitfehler, eine andere lokale Variable oder Konstante mit dem gleichen
Namen zu deklarieren.
Eine lokale Variablen Deklaration, die mehrere Variablen deklariert, entspricht mehreren Deklarationen von
einzelnen Variablen mit demselben Typ. Außerdem entspricht ein Variableninitialisierer in einer lokalen
Variablen Deklaration genau einer Zuweisungsanweisung, die unmittelbar nach der Deklaration eingefügt wird.
Das Beispiel

void F() {
int x = 1, y, z = x * 2;
}

entspricht genau

void F() {
int x; x = 1;
int y;
int z; z = x * 2;
}

In einer implizit typisierten lokalen Variablen Deklaration wird der Typ der zu deklarierenden lokalen Variablen
mit dem Typ des Ausdrucks, der zum Initialisieren der Variablen verwendet wird, identisch sein. Beispiel:

var i = 5;
var s = "Hello";
var d = 1.0;
var numbers = new int[] {1, 2, 3};
var orders = new Dictionary<int,Order>();

Die oben genannten implizit typisierten lokalen Variablen Deklarationen entsprechen genau den folgenden
explizit typisierten Deklarationen:

int i = 5;
string s = "Hello";
double d = 1.0;
int[] numbers = new int[] {1, 2, 3};
Dictionary<int,Order> orders = new Dictionary<int,Order>();

Lokale Konstante Deklarationen


Eine local_constant_declaration die eine oder mehrere lokale Konstanten deklariert.
local_constant_declaration
: 'const' type constant_declarators
;

constant_declarators
: constant_declarator (',' constant_declarator)*
;

constant_declarator
: identifier '=' constant_expression
;

Der Typ einer local_constant_declaration der den Typ der Konstanten angibt, die von der Deklaration eingeführt
wurden. Auf den Typ folgt eine Liste von constant_declarator en, von denen jede eine neue Konstante einführt.
Ein constant_declarator besteht aus einem Bezeichner , der die Konstante benennt, gefolgt von einem " = "-
Token, gefolgt von einem constant_expression (Konstantenausdrücken), der den Wert der Konstante übergibt.
Der Typ und constant_expression einer lokalen Konstanten Deklaration müssen dieselben Regeln wie die einer
Konstanten Element Deklaration (Konstanten) einhalten.
Der Wert einer lokalen Konstante wird in einem Ausdruck mithilfe eines Simple_name (einfache Namen)
abgerufen.
Der Gültigkeitsbereich einer lokalen Konstante ist der Block, in dem die Deklaration auftritt. Es ist ein Fehler, in
einer Textposition, die dem constant_declarator vorangestellt ist, auf eine lokale Konstante zu verweisen.
Innerhalb des Gültigkeits Bereichs einer lokalen Konstante handelt es sich um einen Kompilierzeitfehler, um eine
andere lokale Variable oder Konstante mit dem gleichen Namen zu deklarieren.
Eine lokale Konstantendeklaration, die mehrere Konstanten deklariert, entspricht mehreren Deklarationen von
einzelnen Konstanten desselben Typs.

Ausdrucksanweisungen
Ein- expression_statement wertet einen angegebenen Ausdruck aus. Der von dem Ausdruck berechnete Wert,
falls vorhanden, wird verworfen.

expression_statement
: statement_expression ';'
;

statement_expression
: invocation_expression
| null_conditional_invocation_expression
| object_creation_expression
| assignment
| post_increment_expression
| post_decrement_expression
| pre_increment_expression
| pre_decrement_expression
| await_expression
;

Nicht alle Ausdrücke sind als Anweisungen zulässig. Insbesondere Ausdrücke wie x + y und x == 1 , die nur
einen Wert berechnen (der verworfen wird), sind nicht als-Anweisungen zulässig.
Durch die Ausführung eines expression_statement wird der enthaltene Ausdruck ausgewertet, und die
Steuerung wird an den Endpunkt der expression_statement übertragen. Der Endpunkt eines
expression_statement ist erreichbar, wenn dieser expression_statement erreichbar ist.
Auswahlanweisungen
Auswahl Anweisungen wählen Sie eine der möglichen Anweisungen für die Ausführung basierend auf dem
Wert eines Ausdrucks aus.

selection_statement
: if_statement
| switch_statement
;

Die if-Anweisung
Die- if Anweisung wählt eine Anweisung für die Ausführung basierend auf dem Wert eines booleschen
Ausdrucks aus.

if_statement
: 'if' '(' boolean_expression ')' embedded_statement
| 'if' '(' boolean_expression ')' embedded_statement 'else' embedded_statement
;

Ein else Teil ist mit dem lexikalisch nächstgelegenen vorangehenden-Element verknüpft if , das von der
Syntax zugelassen wird. Folglich eine- if Anweisung der Form

if (x) if (y) F(); else G();

für die folgende Syntax:

if (x) {
if (y) {
F();
}
else {
G();
}
}

Eine- if Anweisung wird wie folgt ausgeführt:


Die Boolean_expression (boolesche Ausdrücke) werden ausgewertet.
Wenn der boolesche Ausdruck ergibt true , wird die Steuerung an die erste eingebettete Anweisung
übertragen. Wenn und wenn das Steuerelement den Endpunkt dieser Anweisung erreicht, wird die Steuerung
an den Endpunkt der Anweisung übertragen if .
Wenn der boolesche Ausdruck ergibt false und ein else Teil vorhanden ist, wird die Steuerung an die
zweite eingebettete Anweisung übertragen. Wenn und wenn das Steuerelement den Endpunkt dieser
Anweisung erreicht, wird die Steuerung an den Endpunkt der Anweisung übertragen if .
Wenn der boolesche Ausdruck ergibt false und ein else Teil nicht vorhanden ist, wird die Steuerung an
den Endpunkt der Anweisung übertragen if .

Die erste eingebettete Anweisung einer- if Anweisung ist erreichbar, wenn die if -Anweisung erreichbar ist
und der boolesche Ausdruck nicht über den konstanten Wert verfügt false .
Die zweite eingebettete Anweisung einer- if Anweisung (falls vorhanden) ist erreichbar, wenn die if -
Anweisung erreichbar ist und der boolesche Ausdruck nicht über den konstanten Wert verfügt true .
Der Endpunkt einer- if Anweisung ist erreichbar, wenn der Endpunkt von mindestens einer der eingebetteten
Anweisungen erreichbar ist. Außerdem ist der Endpunkt einer- if Anweisung ohne else Teil erreichbar, wenn
die if -Anweisung erreichbar ist und der boolesche Ausdruck nicht über den konstanten Wert verfügt true .
Die Switch-Anweisung
Die Switch-Anweisung wählt für die Ausführung eine Anweisungs Liste mit einer zugeordneten Switch-
Bezeichnung aus, die dem Wert des switch-Ausdrucks entspricht.

switch_statement
: 'switch' '(' expression ')' switch_block
;

switch_block
: '{' switch_section* '}'
;

switch_section
: switch_label+ statement_list
;

switch_label
: 'case' constant_expression ':'
| 'default' ':'
;

Ein switch_statement besteht aus dem-Schlüsselwort switch , gefolgt von einem Ausdruck in Klammern (als
Switch-Ausdruck bezeichnet), gefolgt von einem switch_block. Der switch_block besteht aus null oder mehr
switch_section s, der in geschweiften Klammern eingeschlossen ist. Jede switch_section besteht aus mindestens
einer switch_label s, gefolgt von einer statement_list (Anweisungs Liste).
Der governancetyp einer switch Anweisung wird durch den Switch-Ausdruck festgelegt.
Wenn der Typ des switch-Ausdrucks sbyte ,, byte short , ushort , int , uint , long , ulong , bool ,
char , string oder ein enum_type ist, oder wenn es sich um den Typ handelt, der NULL-Werte zulässt, der
einem dieser Typen entspricht, dann ist dies der regierende Typ der switch Anweisung.
Andernfalls muss genau eine benutzerdefinierte implizite Konvertierung (benutzerdefinierte
Konvertierungen) vom Typ des switch-Ausdrucks bis zu einem der folgenden möglichen Typen vorhanden
sein: sbyte , byte , short , ushort , int , uint , long , ulong , char , string oder, ein Typ, der NULL-
Werte zulässt, der einem dieser Typen entspricht.
Andernfalls tritt ein Kompilierzeitfehler auf, wenn keine solche implizite Konvertierung vorhanden ist oder
wenn mehr als eine implizite Konvertierung vorhanden ist.
Der Konstante Ausdruck jeder case Bezeichnung muss einen Wert angeben, der implizit konvertierbar
(implizite Konvertierungen) in den richtentyp der switch Anweisung ist. Ein Kompilierzeitfehler tritt auf, wenn
zwei oder mehr case Bezeichnungen in derselben switch Anweisung denselben Konstanten Wert angeben.
In einer Switch-Anweisung darf höchstens eine Bezeichnung vorhanden sein default .
Eine- switch Anweisung wird wie folgt ausgeführt:
Der Switch-Ausdruck wird ausgewertet und in den regierenden Typ konvertiert.
Wenn eine der Konstanten, die in einer case Bezeichnung in derselben switch Anweisung angegeben ist,
gleich dem Wert des switch-Ausdrucks ist, wird die Steuerung an die Anweisungs Liste nach der passenden
Bezeichnung übertragen case .
Wenn keine der in case Bezeichnungen in derselben Anweisung angegebenen Konstanten switch dem
Wert des switch-Ausdrucks entspricht und eine default Bezeichnung vorhanden ist, wird die Steuerung an
die Anweisungs Liste nach der Bezeichnung übertragen default .
Wenn keine der Konstanten, die in case Bezeichnungen in derselben switch Anweisung angegeben sind,
gleich dem Wert des switch-Ausdrucks ist, und wenn keine default Bezeichnung vorhanden ist, wird die
Steuerung an den Endpunkt der Anweisung übertragen switch .

Wenn der Endpunkt der Anweisungs Liste eines Switch-Abschnitts erreichbar ist, tritt ein Kompilierzeitfehler auf.
Dies wird als "No FallThrough"-Regel bezeichnet. Das Beispiel

switch (i) {
case 0:
CaseZero();
break;
case 1:
CaseOne();
break;
default:
CaseOthers();
break;
}

ist gültig, da kein Switch-Abschnitt über einen erreichbaren Endpunkt verfügt. Im Gegensatz zu C und C++ ist
die Ausführung eines Switch-Abschnitts nicht zulässig, um zum nächsten switchabschnitt zu wechseln, und das
Beispiel

switch (i) {
case 0:
CaseZero();
case 1:
CaseZeroOrOne();
default:
CaseAny();
}

führt zu einem Kompilierzeitfehler. Wenn die Ausführung eines Switch-Abschnitts durch die Ausführung eines
anderen Switch-Abschnitts befolgt werden soll, muss eine explizite- goto case oder- goto default Anweisung
verwendet werden:

switch (i) {
case 0:
CaseZero();
goto case 1;
case 1:
CaseZeroOrOne();
goto default;
default:
CaseAny();
break;
}

Mehrere Bezeichnungen sind in einer switch_section zulässig. Das Beispiel


switch (i) {
case 0:
CaseZero();
break;
case 1:
CaseOne();
break;
case 2:
default:
CaseTwo();
break;
}

ist gültig. Das Beispiel verstößt nicht gegen die Regel "No FallThrough", da die Bezeichnungen case 2: und
default: Teil desselben switch_section sind.

Die "No FallThrough"-Regel verhindert eine gängige Klasse von Fehlern, die in C und C++ auftreten, wenn-
break Anweisungen versehentlich ausgelassen werden. Außerdem können die Switch-Abschnitte einer
Anweisung aufgrund dieser Regel switch beliebig neu angeordnet werden, ohne dass sich dies auf das
Verhalten der Anweisung auswirkt. Beispielsweise können die Abschnitte der switch obigen Anweisung
umgekehrt werden, ohne dass sich dies auf das Verhalten der-Anweisung auswirkt:

switch (i) {
default:
CaseAny();
break;
case 1:
CaseZeroOrOne();
goto default;
case 0:
CaseZero();
goto case 1;
}

Die Anweisungs Liste eines Switch-Abschnitts endet in der Regel mit einer- break ,- goto case oder-
goto default Anweisung, aber alle Konstrukte, die den Endpunkt der Anweisungs Liste nicht erreichbar
rendern, sind zulässig. Beispielsweise while ist es bekannt, dass eine vom booleschen Ausdruck gesteuerte
Anweisung den true Endpunkt niemals erreicht. Ebenso überträgt eine throw return -oder-Anweisung
immer an eine andere Stelle und erreicht ihren Endpunkt nicht. Daher ist das folgende Beispiel gültig:

switch (i) {
case 0:
while (true) F();
case 1:
throw new ArgumentException();
case 2:
return;
}

Der governancetyp einer switch Anweisung kann der Typ sein string . Beispiel:
void DoCommand(string command) {
switch (command.ToLower()) {
case "run":
DoRun();
break;
case "save":
DoSave();
break;
case "quit":
DoQuit();
break;
default:
InvalidCommand(command);
break;
}
}

Wie bei Zeichen folgen Gleichheits Operatoren (Zeichen folgen Gleichheits Operatoren) switch wird bei der
Anweisung die Groß-/Kleinschreibung beachtet, und es wird nur ein bestimmter Schalter Abschnitt ausgeführt,
wenn die Zeichenfolge des switch-Ausdrucks case
Wenn der governancetyp einer- switch Anweisung ist string , ist der Wert null als Konstante für die Case-
Bezeichnung zulässig.
Die statement_list s einer switch_block können Deklarations Anweisungen (Deklarations Anweisungen)
enthalten. Der Gültigkeitsbereich einer lokalen Variablen oder Konstanten, die in einem Switch-Block deklariert
ist, ist der Switch-Block.
Die Anweisungs Liste eines bestimmten switchabschnitts ist erreichbar, wenn die switch -Anweisung erreichbar
ist und mindestens einer der folgenden Punkte zutrifft:
Der Switch-Ausdruck ist ein nicht konstanter Wert.
Der Switch-Ausdruck ist ein konstanter Wert, der case mit einer Bezeichnung im Switch-Abschnitt
übereinstimmt.
Der Switch-Ausdruck ist ein konstanter Wert, der keiner case Bezeichnung entspricht, und der Switch-
Abschnitt enthält die default Bezeichnung.
Auf eine Switch-Bezeichnung des Switch-Abschnitts wird durch eine erreichbare- goto case oder-
goto default Anweisung verwiesen.

Der Endpunkt einer- switch Anweisung ist erreichbar, wenn mindestens einer der folgenden Punkte zutrifft:
Die- switch Anweisung enthält eine erreichbare break Anweisung, die die- switch Anweisung beendet.
Die switch -Anweisung ist erreichbar, der Switch-Ausdruck ist ein nicht konstanter Wert, und default es ist
keine Bezeichnung vorhanden.
Die- switch Anweisung ist erreichbar, der Switch-Ausdruck ist ein konstanter Wert, der keiner case
Bezeichnung entspricht, und default es ist keine Bezeichnung vorhanden.

Iterationsanweisungen
Iterations Anweisungen führen eine eingebettete Anweisung wiederholt aus.
iteration_statement
: while_statement
| do_statement
| for_statement
| foreach_statement
;

Die while -Anweisung


Die- while Anweisung führt eine eingebettete Anweisung bedingt NULL oder mehrmals aus.

while_statement
: 'while' '(' boolean_expression ')' embedded_statement
;

Eine- while Anweisung wird wie folgt ausgeführt:


Die Boolean_expression (boolesche Ausdrücke) werden ausgewertet.
Wenn der boolesche Ausdruck ergibt true , wird die Steuerung an die eingebettete Anweisung übertragen.
Wenn und wenn das Steuerelement den Endpunkt der eingebetteten Anweisung erreicht (möglicherweise
aus der Ausführung einer- continue Anweisung), wird die Steuerung an den Anfang der Anweisung
übertragen while .
Wenn der boolesche Ausdruck ergibt false , wird die Steuerung an den Endpunkt der Anweisung
übertragen while .
Innerhalb der eingebetteten Anweisung einer- while Anweisung kann eine- break Anweisung (die Break-
Anweisung) verwendet werden, um die Steuerung an den Endpunkt der Anweisung zu übertragen (sodass die
while Iterationen der eingebetteten Anweisung beendet werden), und eine- continue Anweisung (die
Continue-Anweisung) kann verwendet werden, um die Steuerung an den Endpunkt der eingebetteten
Anweisung zu übertragen (wodurch eine andere Iterations Anweisung ausgeführt wird while )
Die eingebettete Anweisung einer- while Anweisung ist erreichbar, wenn die while -Anweisung erreichbar ist
und der boolesche Ausdruck nicht über den konstanten Wert verfügt false .
Der Endpunkt einer- while Anweisung ist erreichbar, wenn mindestens einer der folgenden Punkte zutrifft:
Die- while Anweisung enthält eine erreichbare break Anweisung, die die- while Anweisung beendet.
Die while -Anweisung ist erreichbar, und der boolesche Ausdruck weist nicht den konstanten Wert auf
true .

Die Do -Anweisung
Die do Anweisung führt mindestens einmal eine eingebettete Anweisung aus.

do_statement
: 'do' embedded_statement 'while' '(' boolean_expression ')' ';'
;

Eine- do Anweisung wird wie folgt ausgeführt:


Das Steuerelement wird an die eingebettete Anweisung übertragen.
Wenn und wenn das Steuerelement den Endpunkt der eingebetteten Anweisung erreicht (möglicherweise
aus der Ausführung einer- continue Anweisung), wird der Boolean_expression (boolesche Ausdrücke)
ausgewertet. Wenn der boolesche Ausdruck ergibt true , wird die Steuerung an den Anfang der Anweisung
übertragen do . Andernfalls wird die Steuerung an den Endpunkt der Anweisung übertragen do .
Innerhalb der eingebetteten Anweisung einer- do Anweisung kann eine- break Anweisung (die Break-
Anweisung) verwendet werden, um die Steuerung an den Endpunkt der Anweisung zu übertragen (sodass do
die Iterations Ende der eingebetteten Anweisung beendet wird), und eine- continue Anweisung (die Continue-
Anweisung) kann verwendet werden, um die Steuerung an den Endpunkt der eingebetteten Anweisung zu
übertragen.
Die eingebettete Anweisung einer- do Anweisung ist erreichbar, wenn die- do Anweisung erreichbar ist.
Der Endpunkt einer- do Anweisung ist erreichbar, wenn mindestens einer der folgenden Punkte zutrifft:
Die- do Anweisung enthält eine erreichbare break Anweisung, die die- do Anweisung beendet.
Der Endpunkt der eingebetteten Anweisung ist erreichbar, und der boolesche Ausdruck weist nicht den
konstanten Wert auf true .
Die for-Anweisung
Die for -Anweisung wertet eine Sequenz von Initialisierungs Ausdrücken aus und führt dann, während eine
Bedingung true ist, wiederholt eine eingebettete Anweisung aus und wertet eine Sequenz von Iterations
Ausdrücken aus.

for_statement
: 'for' '(' for_initializer? ';' for_condition? ';' for_iterator? ')' embedded_statement
;

for_initializer
: local_variable_declaration
| statement_expression_list
;

for_condition
: boolean_expression
;

for_iterator
: statement_expression_list
;

statement_expression_list
: statement_expression (',' statement_expression)*
;

Der for_initializer, falls vorhanden, besteht entweder aus einer local_variable_declaration (lokale Variablen
Deklarationen) oder einer Liste von statement_expression s (Ausdrucks Anweisungen), die durch Kommas
getrennt sind. Der Gültigkeitsbereich einer lokalen Variablen, die von einem for_initializer deklariert wird,
beginnt bei der local_variable_declarator für die Variable und reicht bis zum Ende der eingebetteten Anweisung
aus. Der Bereich umfasst die for_condition und die for_iterator.
Der for_condition muss, falls vorhanden, eine Boolean_expression (boolesche Ausdrücke) sein.
Der for_iterator, falls vorhanden, besteht aus einer Liste von statement_expression s (Ausdrucks Anweisungen),
die durch Kommas getrennt sind.
Eine for-Anweisung wird wie folgt ausgeführt:
Wenn eine for_initializer vorhanden ist, werden die Variableninitialisierer oder Anweisungs Ausdrücke in der
Reihenfolge ausgeführt, in der Sie geschrieben werden. Dieser Schritt wird nur einmal ausgeführt.
Wenn ein for_condition vorhanden ist, wird es ausgewertet.
Wenn die for_condition nicht vorhanden ist oder die Auswertung ergibt true , wird die Steuerung an die
eingebettete Anweisung übertragen. Wenn und wenn das Steuerelement den Endpunkt der eingebetteten
Anweisung erreicht (möglicherweise aus der Ausführung einer- continue Anweisung), werden die
Ausdrücke des for_iterator, sofern vorhanden, nacheinander ausgewertet. Anschließend wird eine weitere
Iterationen ausgeführt, beginnend mit der Auswertung des for_condition im obigen Schritt.
Wenn die for_condition vorhanden ist und die Auswertung ergibt false , wird die Steuerung an den
Endpunkt der Anweisung übertragen for .
Innerhalb der eingebetteten Anweisung einer- for Anweisung eine- break Anweisung (die Break-Anweisung)
kann verwendet werden, um die Steuerung an den Endpunkt der Anweisung zu übertragen (d. h. das for Ende
der Iterationen der eingebetteten Anweisung), und eine- continue Anweisung (die Continue-Anweisung) kann
verwendet werden, um die Steuerung an den Endpunkt der eingebetteten Anweisung zu übertragen (
beginnend for mit der for_condition ). for_iterator
Die eingebettete Anweisung einer- for Anweisung ist erreichbar, wenn eine der folgenden Aussagen zutrifft:
Die for -Anweisung ist erreichbar, und es ist keine for_condition vorhanden.
Die for -Anweisung ist erreichbar, und ein for_condition ist vorhanden und weist nicht den konstanten Wert
auf false .

Der Endpunkt einer- for Anweisung ist erreichbar, wenn mindestens einer der folgenden Punkte zutrifft:
Die- for Anweisung enthält eine erreichbare break Anweisung, die die- for Anweisung beendet.
Die for -Anweisung ist erreichbar, und ein for_condition ist vorhanden und weist nicht den konstanten Wert
auf true .
Die foreach-Anweisung
Die- foreach Anweisung zählt die Elemente einer Auflistung auf und führt eine eingebettete Anweisung für
jedes Element der Auflistung aus.

foreach_statement
: 'foreach' '(' local_variable_type identifier 'in' expression ')' embedded_statement
;

Der Typ und der Bezeichner einer foreach Anweisung deklarieren die *iterations Variable _ der Anweisung.
Wenn der var Bezeichner als _local_variable_type * angegeben wird und sich kein Typ mit dem Namen im
Gültigkeits var Bereich befindet, wird die Iterations Variable als implizit typisier te Iterations Variable
bezeichnet, und ihr Typ wird als Elementtyp der foreach Anweisung verwendet, wie unten angegeben. Die
Iterations Variable entspricht einer schreibgeschützten lokalen Variablen mit einem Bereich, der die eingebettete
Anweisung erweitert. Während der Ausführung einer- foreach Anweisung stellt die Iterations Variable das
Auflistungs Element dar, für das zurzeit eine Iterations Ausführung ausgeführt wird. Ein Kompilierzeitfehler tritt
auf, wenn die eingebettete Anweisung versucht, die Iterations Variable (über die ++ -und- -- Operatoren) zu
ändern oder die Iterations Variable als-oder-Parameter zu übergeben ref out .
Im folgenden wird aus Gründen der Übersichtlichkeit,, IEnumerable IEnumerator IEnumerable<T> und
IEnumerator<T> auf die entsprechenden Typen in den-Namespaces System.Collections und verwiesen
System.Collections.Generic .

Die Kompilierzeit Verarbeitung einer foreach-Anweisung bestimmt zuerst den Sammlungstyp _, den
_Enumeratortyp_ und den _-Elementtyp** des Ausdrucks. Diese Bestimmung verläuft wie folgt:
Wenn der Typ X des Ausdrucks ein Arraytyp ist, gibt es eine implizite Verweis Konvertierung von X in
die- IEnumerable Schnittstelle (da System.Array diese Schnittstelle implementiert). Der -
**Sammlungstyp* _ ist die- IEnumerable Schnittstelle, der Enumeratortyp ist die IEnumerator -
Schnittstelle, und der -Elementtyp* ist der Elementtyp des Arraytyps X .
Wenn der Typ X des Ausdrucks ist dynamic , gibt es eine implizite Konvertierung von einem Ausdruck in
die- IEnumerableSchnittstelle (implizite dynamische Konvertierungen). Der -**Sammlungstyp* _ ist die
IEnumerable -Schnittstelle, und der Enumeratortyp ist die- IEnumerator Schnittstelle. Wenn der var
Bezeichner als _local_variable_type * angegeben wird, ist der Elementtyp ; dynamic andernfalls ist er
object .

Stellen Sie andernfalls fest, ob der Typ X über eine geeignete GetEnumerator Methode verfügt:
Führt eine Element Suche für den Typ X mit Bezeichnern GetEnumerator und ohne Typargumente
durch. Wenn die Element Suche keine Entsprechung erzeugt oder eine Mehrdeutigkeit erzeugt oder
eine Entsprechung erzeugt, die keine Methoden Gruppe ist, überprüfen Sie wie unten beschrieben, ob
eine Aufzähl Bare-Schnittstelle vorliegt. Es wird empfohlen, dass eine Warnung ausgegeben wird,
wenn die Element Suche nur eine Methoden Gruppe oder keine Entsprechung erzeugt.
Führt die Überladungs Auflösung mithilfe der resultierenden Methoden Gruppe und einer leeren
Argumentliste durch. Wenn die Überladungs Auflösung keine anwendbaren Methoden zur Folge hat,
zu einer Mehrdeutigkeit führt oder zu einer einzigen optimalen Methode führt, diese Methode jedoch
entweder statisch oder nicht öffentlich ist, überprüfen Sie, wie unten beschrieben, eine Aufzähl Bare
Schnittstelle. Es wird empfohlen, dass eine Warnung ausgegeben wird, wenn die Überladungs
Auflösung nur eine eindeutige öffentliche Instanzmethode oder keine anwendbaren Methoden
erzeugt.
Wenn der Rückgabetyp der E GetEnumerator Methode keine Klasse, Struktur oder Schnittstellentyp
ist, wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.
Die Element Suche erfolgt E mit dem-Bezeichner Current und ohne Typargumente. Wenn die
Member-Suche keine Entsprechung erzeugt, ist das Ergebnis ein Fehler, oder es handelt sich um ein
anderes Ergebnis als eine öffentliche Instanzeigenschaft, die Lesevorgänge zulässt, es wird ein Fehler
erzeugt, und es werden keine weiteren Schritte ausgeführt.
Die Element Suche erfolgt E mit dem-Bezeichner MoveNext und ohne Typargumente. Wenn die
Member-Suche keine Entsprechung erzeugt, ist das Ergebnis ein Fehler, oder es handelt sich um ein
anderes Ergebnis als eine Methoden Gruppe. es wird ein Fehler erzeugt, und es werden keine weiteren
Schritte ausgeführt.
Die Überladungs Auflösung wird für die Methoden Gruppe mit einer leeren Argumentliste ausgeführt.
Wenn die Überladungs Auflösung keine anwendbaren Methoden zur Folge hat, führt dies zu einer
Mehrdeutigkeit, oder führt dies zu einer einzigen optimalen Methode, aber diese Methode ist
entweder statisch oder nicht öffentlich, oder der Rückgabetyp ist nicht bool . es wird ein Fehler
erzeugt, und es werden keine weiteren Schritte ausgeführt.
Der Sammlungstyp _ ist X , der _Enumeratortyp_ ist E , und der _-Elementtyp** ist der Typ der
Current Eigenschaft.
Überprüfen Sie andernfalls eine Aufzähl Bare-Schnittstelle:
Wenn zwischen allen Typen Ti , für die eine implizite Konvertierung von in vorhanden ist, ein
eindeutiger Typ vorhanden ist, der X nicht ist IEnumerable<Ti> T T dynamic und für alle anderen
Ti eine implizite Konvertierung von in vorhanden ist, IEnumerable<T> dann ist der - IEnumerable<Ti>
Sammlungstyp _ die-Schnittstelle IEnumerable<T> , der _Enumeratortyp_ ist die-Schnittstelle
IEnumerator<T> , und der _-Elementtyp** ist T .
Andernfalls wird ein Fehler erzeugt, wenn mehr als ein solcher Typ vorhanden ist T , und es werden
keine weiteren Schritte ausgeführt.
Andernfalls ist bei einer impliziten Konvertierung von X in die- System.Collections.IEnumerable
Schnittstelle der -Sammlungstyp _ diese Schnittstelle, der _Enumeratortyp_ ist die-Schnittstelle
System.Collections.IEnumerator , und der _-Elementtyp** ist object .
Andernfalls wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.
Die oben genannten Schritte, wenn erfolgreich, führen zu einer eindeutigen Generierung eines C Auflistungs
Typs, enumeratortyps E und Elementtyps T . Eine foreach-Anweisung des Formulars

foreach (V v in x) embedded_statement

wird dann auf erweitert:

{
E e = ((C)(x)).GetEnumerator();
try {
while (e.MoveNext()) {
V v = (V)(T)e.Current;
embedded_statement
}
}
finally {
... // Dispose e
}
}

Die Variable e ist für den Ausdruck x oder die eingebettete Anweisung oder einen anderen Quellcode des
Programms nicht sichtbar oder kann nicht darauf zugegriffen werden. Die Variable v ist in der eingebetteten
Anweisung schreibgeschützt. Wenn keine explizite Konvertierung (explizite Konvertierungen) von T (dem
Elementtyp) in V (die local_variable_type in der foreach-Anweisung) vorhanden ist, wird ein Fehler erzeugt, und
es werden keine weiteren Schritte ausgeführt. Wenn x den Wert hat null , System.NullReferenceException
wird zur Laufzeit eine ausgelöst.
Eine Implementierung darf eine bestimmte foreach-Anweisung anders implementieren, z. b. aus
Leistungsgründen, solange das Verhalten mit der obigen Erweiterung konsistent ist.
Die Platzierung von v innerhalb der while-Schleife ist wichtig für die Erfassung durch eine anonyme Funktion,
die in der embedded_statement auftritt.
Beispiel:

int[] values = { 7, 9, 13 };
Action f = null;

foreach (var value in values)


{
if (f == null) f = () => Console.WriteLine("First value: " + value);
}

f();

Wenn v außerhalb der while-Schleife deklariert wurde, wird Sie von allen Iterationen gemeinsam genutzt, und
ihr Wert nach der for-Schleife wäre der endgültige Wert, 13 , der den Aufruf von f druckt. Da jede Iterations
Variable über eine eigene Variable verfügt v , hält die, die von f in der ersten Iterationen aufgezeichnet wird,
den Wert 7 , der gedruckt wird, weiterhin. (Hinweis: frühere Versionen von c#, v die außerhalb der while-
Schleife deklariert wurden.)
Der Hauptteil des Blocks zum Schluss wird gemäß den folgenden Schritten erstellt:
Wenn eine implizite Konvertierung von E in die- System.IDisposable Schnittstelle erfolgt, dann
Wenn E ein Werttyp ist, der keine NULL-Werte zulässt, wird die schließlich-Klausel auf die
semantische Entsprechung von erweitert:
finally {
((System.IDisposable)e).Dispose();
}

Andernfalls wird die schließlich-Klausel auf die semantische Entsprechung von erweitert:

finally {
if (e != null) ((System.IDisposable)e).Dispose();
}

mit der Ausnahme, dass bei E einem Werttyp oder einem Typparameter, der auf einen Werttyp
instanziiert wird, die Umwandlung von e in System.IDisposable nicht dazu führt, dass Boxing auftritt.
Andernfalls, wenn E ein versiegelter Typ ist, wird die letzte-Klausel auf einen leeren Block erweitert:

finally {
}

Andernfalls wird die schließlich-Klausel auf erweitert:

finally {
System.IDisposable d = e as System.IDisposable;
if (d != null) d.Dispose();
}

Die lokale Variable d ist für keinen Benutzercode sichtbar oder nicht verfügbar. Dies hat insbesondere
keinen Konflikt mit einer anderen Variablen, deren Bereich den letzten Block enthält.
Die Reihenfolge, in der foreach die Elemente eines Arrays durchlaufen werden, lautet wie folgt: für
eindimensionale Arrays werden Elemente in steigender Index Reihenfolge durchlaufen, beginnend mit Index 0
und endende mit Index Length - 1 . Bei mehrdimensionalen Arrays werden Elemente so durchlaufen, dass die
Indizes der äußersten rechten Dimension zuerst angehoben werden, dann die nächste linke Dimension usw.
Im folgenden Beispiel werden die einzelnen Werte in der Reihenfolge der Elemente in einem zweidimensionalen
Array ausgegeben:

using System;

class Test
{
static void Main() {
double[,] values = {
{1.2, 2.3, 3.4, 4.5},
{5.6, 6.7, 7.8, 8.9}
};

foreach (double elementValue in values)


Console.Write("{0} ", elementValue);

Console.WriteLine();
}
}

Die erstellte Ausgabe lautet wie folgt:


1.2 2.3 3.4 4.5 5.6 6.7 7.8 8.9

Im Beispiel

int[] numbers = { 1, 3, 5, 7, 9 };
foreach (var n in numbers) Console.WriteLine(n);

der Typ von n wird als int , der Elementtyp von, abgeleitet numbers .

Sprunganweisungen
Jump-Anweisungen: Bedingungs Übertragung der Steuerung.

jump_statement
: break_statement
| continue_statement
| goto_statement
| return_statement
| throw_statement
;

Der Speicherort, an den eine Jump-Anweisung die Steuerung überträgt, wird als Ziel der Jump-Anweisung
bezeichnet.
Wenn eine Jump-Anweisung innerhalb eines-Blocks auftritt und sich das Ziel dieser Jump-Anweisung außerhalb
dieses Blocks befindet, wird die Jump-Anweisung zum Beenden des Blocks bezeichnet. Während eine Jump-
Anweisung die Steuerung von einem-Block übertragen kann, kann Sie die Steuerung niemals in einen-Block
übertragen.
Die Ausführung von Jump-Anweisungen wird durch das vorhanden sein von dazwischenliegenden try
Anweisungen erschwert. Wenn solche try Anweisungen fehlen, überträgt eine Jump-Anweisung
bedingungslos die Steuerung von der Jump-Anweisung an das Ziel. Wenn diese dazwischenliegenden try
Anweisungen vorhanden sind, ist die Ausführung komplexer. Wenn die Jump-Anweisung einen oder mehrere
try Blöcke mit zugeordneten finally Blöcken beendet, wird die Steuerung anfänglich an den finally Block
der innersten Anweisung übertragen try . Wenn und wenn das Steuerelement den Endpunkt eines- finally
Blocks erreicht, wird die Steuerung an den- finally Block der nächsten einschließenden Anweisung übertragen
try . Dieser Vorgang wird wiederholt, bis die finally Blöcke aller dazwischen liegenden try Anweisungen
ausgeführt wurden.
Im Beispiel
using System;

class Test
{
static void Main() {
while (true) {
try {
try {
Console.WriteLine("Before break");
break;
}
finally {
Console.WriteLine("Innermost finally block");
}
}
finally {
Console.WriteLine("Outermost finally block");
}
}
Console.WriteLine("After break");
}
}

die finally Blöcke, die zwei try Anweisungen zugeordnet sind, werden ausgeführt, bevor die Steuerung an
das Ziel der Jump-Anweisung übertragen wird.
Die erstellte Ausgabe lautet wie folgt:

Before break
Innermost finally block
Outermost finally block
After break

Die Break-Anweisung
Die- break Anweisung beendet die nächste einschließende-,-,-,- switch oder- while do for foreach
Anweisung.

break_statement
: 'break' ';'
;

Das Ziel einer- Anweisung ist der Endpunkt der nächsten einschließenden switch , while ,, do oder-
break
for foreach Anweisung. Wenn eine-Anweisung nicht durch eine-,-,-,- break oder-Anweisung eingeschlossen
ist switch while do for foreach , tritt ein Kompilierzeitfehler auf.
Wenn mehrere-,-,-,- switch oder-Anweisungen ineinander while do for foreach geschachtelt sind, gilt
eine- break Anweisung nur für die innerste-Anweisung. Um die Steuerung auf mehrere Schachtelungs Ebenen
zu übertragen, muss eine- goto Anweisung (die GoTo-Anweisung) verwendet werden.
Eine- break Anweisung kann einen finally Block nicht beenden (Try-Anweisung). Wenn eine- break
Anweisung innerhalb eines- finally Blocks auftritt, muss sich das Ziel der- break Anweisung innerhalb
desselben- finally Blocks befinden; andernfalls tritt ein Kompilierzeitfehler auf.
Eine- break Anweisung wird wie folgt ausgeführt:
Wenn die break Anweisung einen oder mehrere try Blöcke mit zugeordneten finally Blöcken beendet,
wird die Steuerung anfänglich an den finally Block der innersten Anweisung übertragen try . Wenn und
wenn das Steuerelement den Endpunkt eines- finally Blocks erreicht, wird die Steuerung an den- finally
Block der nächsten einschließenden Anweisung übertragen try . Dieser Vorgang wird wiederholt, bis die
finally Blöcke aller dazwischen liegenden try Anweisungen ausgeführt wurden.
Das Steuerelement wird an das Ziel der-Anweisung übertragen break .
Da eine break Anweisung die Steuerung an andere Stellen überträgt, ist der Endpunkt einer break Anweisung
nie erreichbar.
Die Continue -Anweisung
Mit der- continue Anweisung wird eine neue Iterations Anweisung der nächsten einschließenden-,-,- while
oder-Anweisung gestartet do for foreach .

continue_statement
: 'continue' ';'
;

Das Ziel einer- continue Anweisung ist der Endpunkt der eingebetteten Anweisung der nächsten
einschließenden-,-,- while do oder- for foreach Anweisung. Wenn eine- continue Anweisung nicht durch
eine-,-,-oder-Anweisung eingeschlossen ist while do for foreach , tritt ein Kompilierzeitfehler auf.
Wenn mehrere-,-,- while oder-Anweisungen ineinander do for foreach geschachtelt sind, gilt eine-
continue Anweisung nur für die innerste-Anweisung. Um die Steuerung auf mehrere Schachtelungs Ebenen zu
übertragen, muss eine- goto Anweisung (die GoTo-Anweisung) verwendet werden.
Eine- continue Anweisung kann einen finally Block nicht beenden (Try-Anweisung). Wenn eine- continue
Anweisung innerhalb eines- finally Blocks auftritt, muss sich das Ziel der- continue Anweisung innerhalb
desselben- finally Blocks befinden; andernfalls tritt ein Kompilierzeitfehler auf.
Eine- continue Anweisung wird wie folgt ausgeführt:
Wenn die continue Anweisung einen oder mehrere try Blöcke mit zugeordneten finally Blöcken
beendet, wird die Steuerung anfänglich an den finally Block der innersten Anweisung übertragen try .
Wenn und wenn das Steuerelement den Endpunkt eines- finally Blocks erreicht, wird die Steuerung an
den- finally Block der nächsten einschließenden Anweisung übertragen try . Dieser Vorgang wird
wiederholt, bis die finally Blöcke aller dazwischen liegenden try Anweisungen ausgeführt wurden.
Das Steuerelement wird an das Ziel der-Anweisung übertragen continue .
Da eine continue Anweisung die Steuerung an andere Stellen überträgt, ist der Endpunkt einer continue
Anweisung nie erreichbar.
Die goto -Anweisung
Mit der- goto Anweisung wird die Steuerung an eine Anweisung übertragen, die durch eine Bezeichnung
gekennzeichnet ist.

goto_statement
: 'goto' identifier ';'
| 'goto' 'case' constant_expression ';'
| 'goto' 'default' ';'
;

Das Ziel einer goto bezeichneranweisung ist die bezeichnete Anweisung mit der angegebenen Bezeichnung.
Wenn eine Bezeichnung mit dem angegebenen Namen nicht im aktuellen Funktionsmember vorhanden ist oder
die goto Anweisung nicht innerhalb des Bereichs der Bezeichnung liegt, tritt ein Kompilierzeitfehler auf. Diese
Regel ermöglicht die Verwendung einer- goto Anweisung, um die Steuerung aus einem nicht in einen Bereich
eingefügten Bereich zu übertragen, jedoch nicht in einen eingefügten Bereich. Im Beispiel
using System;

class Test
{
static void Main(string[] args) {
string[,] table = {
{"Red", "Blue", "Green"},
{"Monday", "Wednesday", "Friday"}
};

foreach (string str in args) {


int row, colm;
for (row = 0; row <= 1; ++row)
for (colm = 0; colm <= 2; ++colm)
if (str == table[row,colm])
goto done;

Console.WriteLine("{0} not found", str);


continue;
done:
Console.WriteLine("Found {0} at [{1}][{2}]", str, row, colm);
}
}
}

eine- goto Anweisung wird verwendet, um die Steuerung aus einem in einem Bereich befindlichen Bereich zu
übertragen.
Das Ziel einer- goto case Anweisung ist die Anweisungs Liste in der unmittelbar einschließenden switch
Anweisung (Switch-Anweisung), die eine case Bezeichnung mit dem angegebenen konstanten Wert enthält.
Wenn die goto case Anweisung nicht durch eine-Anweisung eingeschlossen ist switch , wenn die
constant_expression nicht implizit konvertierbar ist (implizite Konvertierungen), in den richtentyp der
nächstgelegenen einschließenden switch Anweisung oder wenn die nächste einschließende switch
Anweisung keine case Bezeichnung mit dem angegebenen konstanten Wert enthält, tritt ein
Kompilierzeitfehler auf.
Das Ziel einer- goto default Anweisung ist die Anweisungs Liste in der unmittelbar einschließenden switch
Anweisung (Switch-Anweisung), die eine default Bezeichnung enthält. Wenn die goto default Anweisung
nicht durch eine-Anweisung eingeschlossen ist switch , oder wenn die nächste einschließende switch
Anweisung keine default Bezeichnung enthält, tritt ein Kompilierzeitfehler auf.
Eine- goto Anweisung kann einen finally Block nicht beenden (Try-Anweisung). Wenn eine- goto
Anweisung innerhalb eines- finally Blocks auftritt, muss sich das Ziel der- goto Anweisung innerhalb
desselben Blocks befinden finally , andernfalls tritt ein Kompilierzeitfehler auf.
Eine- goto Anweisung wird wie folgt ausgeführt:
Wenn die goto Anweisung einen oder mehrere try Blöcke mit zugeordneten finally Blöcken beendet,
wird die Steuerung anfänglich an den finally Block der innersten Anweisung übertragen try . Wenn und
wenn das Steuerelement den Endpunkt eines- finally Blocks erreicht, wird die Steuerung an den- finally
Block der nächsten einschließenden Anweisung übertragen try . Dieser Vorgang wird wiederholt, bis die
finally Blöcke aller dazwischen liegenden try Anweisungen ausgeführt wurden.
Das Steuerelement wird an das Ziel der-Anweisung übertragen goto .
Da eine goto Anweisung die Steuerung an andere Stellen überträgt, ist der Endpunkt einer goto Anweisung
nie erreichbar.
Return-Anweisung
Die- return Anweisung gibt die Steuerung an den aktuellen Aufrufer der Funktion zurück, in der die- return
Anweisung angezeigt wird.

return_statement
: 'return' expression? ';'
;

Eine return Anweisung ohne Ausdruck kann nur in einem Funktionsmember verwendet werden, der keinen
Wert berechnet, d. h. eine Methode mit dem Ergebnistyp (Methoden Text) void , den set Accessor einer
Eigenschaft oder einen Indexer, die add -und- remove Accessoren eines Ereignisses, einen Instanzkonstruktor,
einen statischen Konstruktor oder einen destrukturtor.
Eine- return Anweisung mit einem-Ausdruck kann nur in einem Funktionsmember verwendet werden, der
einen-Wert berechnet, d. h. eine Methode mit einem nicht leeren Ergebnistyp, den- get Accessor einer
Eigenschaft oder einen Indexer oder einen benutzerdefinierten Operator. Eine implizite Konvertierung (implizite
Konvertierungen) muss vom Typ des Ausdrucks bis zum Rückgabetyp des enthaltenden Funktionsmembers
vorhanden sein.
Return-Anweisungen können auch im Text der anonymen Funktions Ausdrücke (Anonyme FunktionsAusdrücke)
verwendet werden und daran beteiligt sein, zu bestimmen, welche Konvertierungen für diese Funktionen
vorhanden sind.
Es handelt sich um einen Kompilierzeitfehler, damit eine- return Anweisung in einem- finally Block (der try-
Anweisung) angezeigt wird.
Eine- return Anweisung wird wie folgt ausgeführt:
Wenn die return Anweisung einen Ausdruck angibt, wird der Ausdruck ausgewertet, und der resultierende
Wert wird durch eine implizite Konvertierung in den Rückgabetyp der enthaltenden Funktion konvertiert.
Das Ergebnis der Konvertierung wird der Ergebniswert, der von der-Funktion erzeugt wird.
Wenn die- return Anweisung von einem oder mehreren- try oder- catch Blöcken mit zugeordneten-
Blöcken eingeschlossen wird finally , wird die Steuerung anfänglich an den- finally Block der innersten-
Anweisung übertragen try . Wenn und wenn das Steuerelement den Endpunkt eines- finally Blocks
erreicht, wird die Steuerung an den- finally Block der nächsten einschließenden Anweisung übertragen
try . Dieser Vorgang wird wiederholt, bis die finally Blöcke aller einschließenden try Anweisungen
ausgeführt wurden.
Wenn die enthaltende Funktion keine Async-Funktion ist, wird die Steuerung an den Aufrufer der
enthaltenden Funktion zusammen mit dem Ergebniswert zurückgegeben, falls vorhanden.
Wenn die enthaltende Funktion eine Async-Funktion ist, wird die Steuerung an den aktuellen Aufrufer
zurückgegeben, und der Ergebniswert wird ggf. in der Rückgabe Aufgabe aufgezeichnet, wie in
(Enumeratorschnittstellen) beschrieben.
Da eine return Anweisung die Steuerung an andere Stellen überträgt, ist der Endpunkt einer return
Anweisung nie erreichbar.
Die throw-Anweisung
Die throw -Anweisung löst eine Ausnahme aus.

throw_statement
: 'throw' expression? ';'
;

Eine- throw Anweisung mit einem Ausdruck löst den Wert aus, der durch Auswerten des Ausdrucks erzeugt
wird. Der Ausdruck muss einen Wert des Klassen Typs System.Exception , eines Klassen Typs, der von abgeleitet
System.Exception ist, oder von einem typparametertyp mit System.Exception (oder einer Unterklasse) als
effektive Basisklasse bezeichnen. Wenn die Auswertung des Ausdrucks erzeugt null ,
System.NullReferenceException wird stattdessen eine ausgelöst.

Eine- throw Anweisung ohne Ausdruck kann nur in einem-Block verwendet werden catch . in diesem Fall löst
die-Anweisung die Ausnahme erneut aus, die zurzeit von diesem Block behandelt wird catch .
Da eine throw Anweisung die Steuerung an andere Stellen überträgt, ist der Endpunkt einer throw Anweisung
nie erreichbar.
Wenn eine Ausnahme ausgelöst wird, wird die Steuerung an die erste catch Klausel in einer einschließenden
Anweisung übertragen, die try die Ausnahme behandeln kann. Der Prozess, der ab dem Zeitpunkt der
Ausnahme ausgelöst wird, der zum Zeitpunkt der Übertragung der Steuerung an einen geeigneten
Ausnahmehandler ausgelöst wird, wird als *Exception Propagierung _ bezeichnet. Die Weitergabe einer
Ausnahme besteht aus dem wiederholten Auswerten der folgenden Schritte, bis eine catch Klausel gefunden
wird, die der Ausnahme entspricht. In dieser Beschreibung ist _ throw Point* anfänglich der Speicherort, an dem
die Ausnahme ausgelöst wird.
Im aktuellen Funktionsmember wird jede try Anweisung, die den Throw-Punkt einschließt, untersucht.
Für jede Anweisung S , beginnend mit der innersten try -Anweisung und mit der äußersten- try
Anweisung, werden die folgenden Schritte ausgewertet:
Wenn der try Block von S den Throw-Punkt einschließt und S über eine oder mehrere catch
Klauseln verfügt, catch werden die Klauseln in der Reihenfolge der Darstellung untersucht, um
nach einem geeigneten Handler für die Ausnahme zu suchen, der den im Abschnitt der try-
Anweisungangegebenen Regeln entspricht. Wenn eine übereinstimmende catch Klausel
gefunden wird, wird die Ausnahme Weitergabe durch übertragen der Steuerung an den Block
dieser catch Klausel abgeschlossen.
Andernfalls try wird die Steuerung an den-Block übertragen, wenn der-Block oder ein catch S
-Block den Throw-Punkt einschließt und wenn S über einen- finally Block verfügt finally .
Wenn der- finally Block eine andere Ausnahme auslöst, wird die Verarbeitung der aktuellen
Ausnahme beendet. Andernfalls finally wird die Verarbeitung der aktuellen Ausnahme
fortgesetzt, wenn die Steuerung den Endpunkt des Blocks erreicht.
Wenn ein Ausnahmehandler im aktuellen Funktionsaufruf nicht gefunden wurde, wird der
Funktionsaufruf beendet, und eine der folgenden Aktionen wird ausgeführt:
Wenn die aktuelle Funktion nicht Async ist, werden die obigen Schritte für den Aufrufer der
Funktion mit einem Throw-Punkt wiederholt, der der Anweisung entspricht, aus der der
Funktionsmember aufgerufen wurde.
Wenn die aktuelle Funktion Async ist und die Aufgabe zurückgibt, wird die Ausnahme in der
Rückgabe Aufgabe aufgezeichnet, die in einen fehlerhaften oder abgebrochenen Zustand versetzt
wird, wie in Enumeratorschnittstellenbeschrieben.
Wenn die aktuelle Funktion Async ist und void zurückgibt, wird der Synchronisierungs Kontext des
aktuellen Threads wie in Aufzähl baren Schnittstellenbeschrieben benachrichtigt.
Wenn die Ausnahme Verarbeitung alle Funktionselement Aufrufe im aktuellen Thread beendet und
angibt, dass der Thread keinen Handler für die Ausnahme aufweist, wird der Thread selbst beendet. Die
Auswirkungen dieser Beendigung sind Implementierungs definiert.

Die try-Anweisung
Die- try Anweisung stellt einen Mechanismus zum Abfangen von Ausnahmen bereit, die während der
Ausführung eines-Blocks auftreten. Außerdem bietet die- try Anweisung die Möglichkeit, einen Codeblock
anzugeben, der immer ausgeführt wird, wenn das Steuerelement die- try Anweisung verlässt.

try_statement
: 'try' block catch_clause+
| 'try' block finally_clause
| 'try' block catch_clause+ finally_clause
;

catch_clause
: 'catch' exception_specifier? exception_filter? block
;

exception_specifier
: '(' type identifier? ')'
;

exception_filter
: 'when' '(' expression ')'
;

finally_clause
: 'finally' block
;

Es gibt drei mögliche Formen von- try Anweisungen:


Ein- try Block, gefolgt von einem oder mehreren- catch Blöcken.
Ein- try Block, gefolgt von einem- finally Block.
Ein- try Block, gefolgt von einem oder mehreren- catch Blöcken, gefolgt von einem- finally Block.
Wenn eine- catch Klausel eine exception_specifier angibt, muss der Typ System.Exception , ein Typ, der von
abgeleitet ist, System.Exception oder ein typparametertyp sein, der System.Exception (oder eine Unterklasse
davon) als effektive Basisklasse aufweist.
Wenn eine- catch Klausel sowohl einen exception_specifier mit einem Bezeichner angibt, wird eine
*Ausnahme Variable _ mit dem angegebenen Namen und Typ deklariert. Die Exception-Variable entspricht
einer lokalen Variablen mit einem Bereich, der die- catch Klausel erweitert. Während der Ausführung des
_exception_filter * und des Blocks stellt die Ausnahme Variable die derzeit behandelte Ausnahme dar. Zum
Zweck der eindeutigen Zuweisungs Überprüfung wird die Ausnahme Variable als definitiv in Ihrem gesamten
Bereich zugewiesen.
catch Es ist nicht möglich, auf das Ausnahme Objekt im Filter und Block zuzugreifen, es sei denn, eine Klausel
enthält einen Ausnahme Variablennamen catch .
Eine- catch Klausel, die keine exception_specifier angibt, wird als allgemeine catch Klausel bezeichnet.
Einige Programmiersprachen unterstützen möglicherweise Ausnahmen, die nicht als von abgeleitete Objekt
darstellbar sind System.Exception , obwohl solche Ausnahmen nie durch c#-Code generiert werden könnten.
Eine allgemeine catch Klausel kann verwendet werden, um solche Ausnahmen abzufangen. Daher
unterscheidet sich eine allgemeine catch Klausel semantisch von einer, die den Typ angibt System.Exception ,
da der erste auch Ausnahmen von anderen Sprachen abfängt.
Um einen Handler für eine Ausnahme zu finden, catch werden Klauseln in lexikalischer Reihenfolge untersucht.
Wenn eine catch Klausel einen Typ, aber keinen Ausnahme Filter angibt, ist dies ein Kompilierzeitfehler für eine
spätere catch Klausel in derselben try Anweisung, um einen Typ anzugeben, der dem Typ entspricht oder von
diesem abgeleitet ist. Wenn eine catch Klausel keinen Typ und keinen Filter angibt, muss es sich um die letzte
catch Klausel für diese try Anweisung handeln.
Innerhalb eines- catch Blocks kann eine- throw Anweisung (die throw-Anweisung) ohne Ausdruck verwendet
werden, um die vom-Block aufgefangene Ausnahme erneut auszulösen catch . Bei Zuweisungen zu einer
Ausnahme Variablen wird die Ausnahme, die erneut ausgelöst wird, nicht geändert.
Im Beispiel

using System;

class Test
{
static void F() {
try {
G();
}
catch (Exception e) {
Console.WriteLine("Exception in F: " + e.Message);
e = new Exception("F");
throw; // re-throw
}
}

static void G() {


throw new Exception("G");
}

static void Main() {


try {
F();
}
catch (Exception e) {
Console.WriteLine("Exception in Main: " + e.Message);
}
}
}

die Methode F fängt eine Ausnahme ab, schreibt einige Diagnoseinformationen in die Konsole, ändert die
Ausnahme Variable und löst die Ausnahme erneut aus. Die Ausnahme, die erneut ausgelöst wird, ist die
ursprüngliche Ausnahme, sodass die Ausgabe erzeugt wird:

Exception in F: G
Exception in Main: G

Wenn der erste catch-Block e ausgelöst wurde und die aktuelle Ausnahme nicht erneut ausgelöst wurde, sieht
die Ausgabe wie folgt aus:

Exception in F: G
Exception in Main: F

Es handelt sich um einen Kompilierzeitfehler für eine- break ,- continue oder- goto Anweisung zum
Übertragen der Steuerung aus einem- finally Block. Wenn eine- break ,- continue oder- goto Anweisung
in einem- finally Block auftritt, muss sich das Ziel der-Anweisung innerhalb desselben Blocks befinden
finally . andernfalls tritt ein Kompilierzeitfehler auf.

Es handelt sich um einen Kompilierzeitfehler, damit eine- return Anweisung in einem- finally Block auftritt.
Eine- try Anweisung wird wie folgt ausgeführt:
Das Steuerelement wird an den-Block übertragen try .
Wenn und wenn das Steuerelement den Endpunkt des try Blocks erreicht:
Wenn die try Anweisung über einen- finally Block verfügt, wird der- finally Block ausgeführt.
Das Steuerelement wird an den Endpunkt der Anweisung übertragen try .
Wenn eine Ausnahme try während der Ausführung des Blocks an die Anweisung weitergegeben wird
try :

Die catch Klauseln werden ggf. in der Reihenfolge ihrer Darstellung untersucht, um einen geeigneten
Handler für die Ausnahme zu suchen. Wenn eine- catch Klausel keinen Typ angibt oder den
Ausnahmetyp oder einen Basistyp des Ausnahme Typs angibt:
Wenn die- catch Klausel eine Exception-Variable deklariert, wird das Ausnahme Objekt der
Ausnahme Variablen zugewiesen.
Wenn die- catch Klausel einen Ausnahme Filter deklariert, wird der Filter ausgewertet. Wenn
ausgewertet wird false , ist die catch-Klausel keine Entsprechung, und die Suche wird durch
alle nachfolgenden catch Klauseln für einen geeigneten Handler fortgesetzt.
Andernfalls wird die catch -Klausel als Übereinstimmung angesehen, und die Steuerung wird
an den entsprechenden-Block übertragen catch .
Wenn und wenn das Steuerelement den Endpunkt des catch Blocks erreicht:
Wenn die try Anweisung über einen- finally Block verfügt, wird der- finally Block
ausgeführt.
Das Steuerelement wird an den Endpunkt der Anweisung übertragen try .
Wenn eine Ausnahme try während der Ausführung des Blocks an die Anweisung
weitergegeben wird catch :
Wenn die try Anweisung über einen- finally Block verfügt, wird der- finally Block
ausgeführt.
Die Ausnahme wird an die nächste einschließende try Anweisung weitergegeben.
Wenn die try Anweisung keine catch Klauseln aufweist oder wenn keine catch Klausel mit der
Ausnahme übereinstimmt:
Wenn die try Anweisung über einen- finally Block verfügt, wird der- finally Block
ausgeführt.
Die Ausnahme wird an die nächste einschließende try Anweisung weitergegeben.
Die-Anweisungen eines- finally Blocks werden immer ausgeführt, wenn die Steuerung eine- try Anweisung
verlässt. Dies gilt unabhängig davon, ob die Steuerung aufgrund der normalen Ausführung stattfindet, weil eine-
,-, break - continue oder-Anweisung ausgeführt goto return wird oder wenn eine Ausnahme aus der- try
Anweisung weitergegeben wird.
Wenn während der Ausführung eines-Blocks eine Ausnahme ausgelöst wird finally und nicht innerhalb
desselben letzten Blocks abgefangen wird, wird die Ausnahme an die nächste einschließende Anweisung
weitergegeben try . Wenn eine andere Ausnahme gerade weitergegeben wurde, geht diese Ausnahme
verloren. Der Prozess der Weitergabe einer Ausnahme wird weiter unten in der Beschreibung der throw
Anweisung (der throw-Anweisung) erläutert.
Der- try Block einer- try Anweisung ist erreichbar, wenn die- try Anweisung erreichbar ist.
Ein catch Block einer- try Anweisung ist erreichbar, wenn die- try Anweisung erreichbar ist.
Der- finally Block einer- try Anweisung ist erreichbar, wenn die- try Anweisung erreichbar ist.
Der Endpunkt einer- try Anweisung ist erreichbar, wenn beide der folgenden Punkte zutreffen:
Der Endpunkt des try Blocks ist erreichbar, oder der Endpunkt von mindestens einem catch Block ist
erreichbar.
Wenn ein- finally Block vorhanden ist, ist der Endpunkt des- finally Blocks erreichbar.

The checked and unchecked statements (Geprüfte und nicht geprüfte


Anweisungen)
Die checked -und unchecked -Anweisungen werden verwendet, um den Überlauf Überprüfungs Kontext
für arithmetische Operationen im ganzzahligen Typ und Konvertierungen zu steuern.

checked_statement
: 'checked' block
;

unchecked_statement
: 'unchecked' block
;

Die checked -Anweisung bewirkt, dass alle Ausdrücke im- Block in einem überprüften Kontext ausgewertet
werden, und die- unchecked Anweisung bewirkt, dass alle Ausdrücke im Block in einem nicht überprüften
Kontext ausgewertet werden.
Die checked -und- unchecked Anweisungen entsprechen genau den checked Operatoren und (die aktivierten
und deaktivierten unchecked Operatoren), mit dem Unterschied, dass Sie anstelle von Ausdrücken an-Blöcken
arbeiten.

The lock statement (Die lock-Anweisung)


Die lock -Anweisung ruft die Sperre für den gegenseitigen Ausschluss für ein bestimmtes Objekt ab, führt
eine-Anweisung aus und gibt dann die Sperre frei.

lock_statement
: 'lock' '(' expression ')' embedded_statement
;

Der Ausdruck einer- lock Anweisung muss einen Wert eines Typs bezeichnen, der bekanntermaßen ein
reference_type ist. Für den Ausdruck einer-Anweisung wird nie eine implizite Boxing-Konvertierung (Boxing-
Konvertierungen) ausgeführt lock , und daher ist es ein Kompilierzeitfehler, wenn der Ausdruck einen Wert
eines value_type angibt.
Eine- lock Anweisung der Form

lock (x) ...

Where x ist ein Ausdruck einer reference_type, ist genau Äquivalent zu

bool __lockWasTaken = false;


try {
System.Threading.Monitor.Enter(x, ref __lockWasTaken);
...
}
finally {
if (__lockWasTaken) System.Threading.Monitor.Exit(x);
}

außer dass x nur einmal überprüft wird.


Während eine gegenseitige Ausschluss Sperre aufrechterhalten wird, kann der Code, der im selben Ausführungs
Thread ausgeführt wird, auch die Sperre abrufen und freigeben. Der Code, der in anderen Threads ausgeführt
wird, wird jedoch blockiert, bis die Sperre aufgehoben wird.
Das Sperren von System.Type Objekten, um den Zugriff auf statische Daten zu synchronisieren, wird nicht
empfohlen. Anderer Code kann denselben Typ sperren, was zu einem Deadlock führen kann. Ein besserer Ansatz
besteht darin, den Zugriff auf statische Daten zu synchronisieren, indem ein privates statisches Objekt gesperrt
wird. Beispiel:

class Cache
{
private static readonly object synchronizationObject = new object();

public static void Add(object x) {


lock (Cache.synchronizationObject) {
...
}
}

public static void Remove(object x) {


lock (Cache.synchronizationObject) {
...
}
}
}

Die using-Anweisung
Die- using Anweisung ruft eine oder mehrere Ressourcen ab, führt eine-Anweisung aus und verwirft dann die
Ressource.

using_statement
: 'using' '(' resource_acquisition ')' embedded_statement
;

resource_acquisition
: local_variable_declaration
| expression
;

Eine Ressource ist eine Klasse oder Struktur, die implementiert System.IDisposable , die eine einzelne
Parameter lose Methode namens enthält Dispose . Code, der eine Ressource verwendet, kann aufzurufen,
Dispose um anzugeben, dass die Ressource nicht mehr benötigt wird. Wenn Dispose nicht aufgerufen wird,
wird die automatische Entfernung schließlich aufgrund Garbage Collection ausgelöst.
Wenn die Form von resource_acquisition local_variable_declaration ist, muss der Typ des
local_variable_declaration entweder dynamic oder ein Typ sein, der implizit in konvertiert werden kann
System.IDisposable . Wenn die Form von resource_acquisition Ausdruck ist, muss dieser Ausdruck implizit in
konvertierbar sein System.IDisposable .
Lokale Variablen, die in einem resource_acquisition deklariert sind, sind schreibgeschützt und müssen einen
Initialisierer enthalten. Ein Kompilierzeitfehler tritt auf, wenn die eingebettete Anweisung versucht, diese lokalen
Variablen (über die ++ -und- -- Operatoren) zu ändern, die Adresse dieser Variablen zu übernehmen oder Sie
als-oder-Parameter zu übergeben ref out .
Eine- using Anweisung wird in drei Teile übersetzt: Beschaffung, Verwendung und Entsorgung. Die
Verwendung der Ressource ist implizit in eine- try Anweisung eingeschlossen, die eine- finally Klausel
einschließt. Diese finally Klausel verwirft die Ressource. Wenn eine null Ressource abgerufen wird, wird
kein-Rückruf durch Dispose geführt, und es wird keine Ausnahme ausgelöst. Wenn die Ressource vom Typ ist,
dynamic wird Sie dynamisch durch eine implizite dynamische Konvertierung (implizite dynamische
Konvertierungen) in in den Erwerb konvertiert, um IDisposable sicherzustellen, dass die Konvertierung vor der
Verwendung und der Entsorgung erfolgreich war.
Eine- using Anweisung der Form

using (ResourceType resource = expression) statement

entspricht einer von drei möglichen Erweiterungen. Wenn ResourceType ein Werttyp ist, der keine NULL-Werte
zulässt, ist die Erweiterung

{
ResourceType resource = expression;
try {
statement;
}
finally {
((IDisposable)resource).Dispose();
}
}

Andernfalls: Wenn ResourceType ein Werte zulässt-Werttyp oder ein anderer Verweistyp als ist dynamic , wird
die Erweiterung

{
ResourceType resource = expression;
try {
statement;
}
finally {
if (resource != null) ((IDisposable)resource).Dispose();
}
}

Andernfalls ResourceType dynamic ist die Erweiterung, wenn den Wert hat.

{
ResourceType resource = expression;
IDisposable d = (IDisposable)resource;
try {
statement;
}
finally {
if (d != null) d.Dispose();
}
}

Bei beiden Erweiterungen ist die resource -Variable in der eingebetteten-Anweisung schreibgeschützt, und die-
Variable d ist in der eingebetteten-Anweisung nicht verfügbar, und Sie ist nicht sichtbar.
Eine Implementierung darf eine angegebene using-Anweisung anders implementieren, z. b. aus
Leistungsgründen, solange das Verhalten mit der obigen Erweiterung konsistent ist.
Eine- using Anweisung der Form
using (expression) statement

hat dieselben drei möglichen Erweiterungen. In diesem Fall ResourceType ist implizit der Kompilier Zeittyp von
expression , wenn er über einen verfügt. Andernfalls wird die-Schnittstelle IDisposable selbst als verwendet
ResourceType . resource Auf die-Variable kann nicht zugegriffen werden, und die eingebettete-Anweisung ist
unsichtbar.
Wenn ein resource_acquisition die Form eines local_variable_declaration hat, ist es möglich, mehrere
Ressourcen eines bestimmten Typs zu erhalten. Eine- using Anweisung der Form

using (ResourceType r1 = e1, r2 = e2, ..., rN = eN) statement

entspricht genau einer Sequenz von- using Anweisungen:

using (ResourceType r1 = e1)


using (ResourceType r2 = e2)
...
using (ResourceType rN = eN)
statement

Im folgenden Beispiel wird eine Datei mit dem Namen erstellt log.txt , und es werden zwei Textzeilen in die
Datei geschrieben. Im Beispiel wird dann dieselbe Datei zum Lesen geöffnet, und die enthaltenen Textzeilen
werden in die Konsole kopiert.

using System;
using System.IO;

class Test
{
static void Main() {
using (TextWriter w = File.CreateText("log.txt")) {
w.WriteLine("This is line one");
w.WriteLine("This is line two");
}

using (TextReader r = File.OpenText("log.txt")) {


string s;
while ((s = r.ReadLine()) != null) {
Console.WriteLine(s);
}

}
}
}

Da die TextWriter TextReader Klassen und die- IDisposable Schnittstelle implementieren, kann im Beispiel-
Anweisungen verwendet werden, using um sicherzustellen, dass die zugrunde liegende Datei nach den
Schreib-oder Lesevorgängen ordnungsgemäß geschlossen wird

The yield statement (Die yield-Anweisung)


Die- yield Anweisung wird in einem Iteratorblock (Blocks) verwendet, um einen Wert für das
Enumeratorobjekt (Enumeratorobjekte) oder das Aufzähl Bare Objekt (Aufzähl Bare Objekte) eines Iterators oder
das Ende der Iterationen anzugeben.
yield_statement
: 'yield' 'return' expression ';'
| 'yield' 'break' ';'
;

yield ist kein reserviertes Wort. Sie hat nur dann eine besondere Bedeutung, wenn Sie direkt vor einem
return Schlüsselwort oder verwendet wird break . In anderen Kontexten yield kann als Bezeichner
verwendet werden.
Es gibt mehrere Einschränkungen hinsichtlich des Orts, an dem eine- yield Anweisung angezeigt werden kann,
wie im folgenden beschrieben.
Es handelt sich um einen Kompilierzeitfehler für eine- yield Anweisung (von beiden Formularen), die
außerhalb einer method_body, operator_body oder accessor_body
Es handelt sich um einen Kompilierzeitfehler für eine- yield Anweisung (von beiden Formularen), die in
einer anonymen Funktion angezeigt wird.
Es handelt sich um einen Kompilierzeitfehler für eine- yield Anweisung (von beiden Formularen), die in
der- finally Klausel einer-Anweisung angezeigt wird try .
Es ist ein Kompilierzeitfehler, wenn eine- yield return Anweisung an einer beliebigen Stelle in einer-
Anweisung angezeigt wird try , die catch Klauseln enthält.
Das folgende Beispiel zeigt einige gültige und ungültige Verwendungen von- yield Anweisungen.

delegate IEnumerable<int> D();

IEnumerator<int> GetEnumerator() {
try {
yield return 1; // Ok
yield break; // Ok
}
finally {
yield return 2; // Error, yield in finally
yield break; // Error, yield in finally
}

try {
yield return 3; // Error, yield return in try...catch
yield break; // Ok
}
catch {
yield return 4; // Error, yield return in try...catch
yield break; // Ok
}

D d = delegate {
yield return 5; // Error, yield in an anonymous function
};
}

int MyMethod() {
yield return 1; // Error, wrong return type for an iterator block
}

Eine implizite Konvertierung (implizite Konvertierungen) muss vom Typ des Ausdrucks in der- yield return
Anweisung bis zum Yield-Typ (Yield-Typ) des Iterators vorhanden sein.
Eine- yield return Anweisung wird wie folgt ausgeführt:
Der in der-Anweisung angegebene Ausdruck wird ausgewertet, implizit in den Yield-Typ konvertiert und der-
Current Eigenschaft des Enumeratorobjekts zugewiesen.
Die Ausführung des Iteratorblocks wurde angehalten. Wenn sich die- yield return Anweisung innerhalb
eines oder mehrerer try Blöcke befindet, werden die zugeordneten finally Blöcke zurzeit nicht
ausgeführt.
Die- MoveNext Methode des Enumeratorobjekts wird an den Aufrufer zurückgegeben true und gibt an,
dass das Enumeratorobjekt erfolgreich auf das nächste Element erweitert wurde.
Der nächste aufrufungs Vorgang der-Methode des Enumeratorobjekts setzt die MoveNext Ausführung des
Iteratorblocks fort, von wo er zuletzt angehalten wurde.
Eine- yield break Anweisung wird wie folgt ausgeführt:
Wenn die- yield break Anweisung von einem oder mehreren- try Blöcken mit zugeordneten-Blöcken
eingeschlossen wird finally , wird die Steuerung anfänglich an den- finally Block der innersten-
Anweisung übertragen try . Wenn und wenn das Steuerelement den Endpunkt eines- finally Blocks
erreicht, wird die Steuerung an den- finally Block der nächsten einschließenden Anweisung übertragen
try . Dieser Vorgang wird wiederholt, bis die finally Blöcke aller einschließenden try Anweisungen
ausgeführt wurden.
Das Steuerelement wird an den Aufrufer des Iteratorblocks zurückgegeben. Dies ist entweder die- MoveNext
Methode oder die- Dispose Methode des Enumeratorobjekts.
Da eine yield break Anweisung die Steuerung an andere Stellen überträgt, ist der Endpunkt einer yield break
Anweisung nie erreichbar.
Namespaces
04.11.2021 • 33 minutes to read

C#-Programme werden mithilfe von Namespaces organisiert. Namespaces werden sowohl als internes
Organisationssystem für ein Programm als auch als "externes" Organisationssystem verwendet – eine Methode
zur Darstellung von Programmelementen, die für andere Programme verfügbar gemacht werden.
Using-Direktiven (using-Direktiven) werden bereitgestellt, um die Verwendung von Namespaces zu
vereinfachen.

Compilation units (Kompilationseinheiten)


Eine compilation_unit die die Gesamtstruktur einer Quelldatei definiert. Eine Kompilierungseinheit besteht aus
null oder mehr using_directive s, gefolgt von 0 (null) oder mehr global_attributes gefolgt von 0 (null) oder mehr
namespace_member_declaration s.

compilation_unit
: extern_alias_directive* using_directive* global_attributes? namespace_member_declaration*
;

Ein c#-Programm besteht aus einer oder mehreren Kompilierungs Einheiten, die jeweils in einer separaten
Quelldatei enthalten sind. Wenn ein c#-Programm kompiliert wird, werden alle Kompilierungs Einheiten
zusammen verarbeitet. Daher können Kompilierungs Einheiten voneinander abhängig sein, möglicherweise
zirkulär.
Die using_directive s einer Kompilierungseinheit wirken sich auf die global_attributes und
namespace_member_declaration en dieser Kompilierungseinheit aus, haben aber keine Auswirkungen auf
andere Kompilierungs Einheiten.
Die global_attributes (Attribute) einer Kompilierungseinheit erlauben die Spezifikation von Attributen für die
Zielassembly und das Zielmodul. Assemblys und Module fungieren als physische Container für-Typen. Eine
Assembly besteht möglicherweise aus mehreren physisch getrennten Modulen.
Die namespace_member_declaration en der einzelnen Kompilierungs Einheiten eines Programms tragen
Member zu einem einzelnen Deklarations Bereich bei, der als globaler Namespace bezeichnet wird. Beispiel:
Datei A.cs :

class A {}

Datei B.cs :

class B {}

Die beiden Kompilierungs Einheiten tragen zum einzelnen globalen Namespace bei, in diesem Fall werden zwei
Klassen mit den voll qualifizierten Namen A und deklariert B . Da die beiden Kompilierungs Einheiten zum
selben Deklarations Bereich beitragen, wäre ein Fehler aufgetreten, wenn jede eine Deklaration eines Members
mit dem gleichen Namen enthielt.
Namespacedeklarationen
Ein namespace_declaration besteht aus dem Schlüsselwort namespace , gefolgt von einem Namespace Namen
und einem Text, gefolgt von einem Semikolon.

namespace_declaration
: 'namespace' qualified_identifier namespace_body ';'?
;

qualified_identifier
: identifier ('.' identifier)*
;

namespace_body
: '{' extern_alias_directive* using_directive* namespace_member_declaration* '}'
;

Eine namespace_declaration kann als Deklaration der obersten Ebene in einer compilation_unit oder als Element
Deklaration innerhalb eines anderen namespace_declaration auftreten. Wenn eine namespace_declaration als
Deklaration der obersten Ebene in einer compilation_unit auftritt, wird der Namespace zu einem Member des
globalen Namespace. Wenn ein namespace_declaration in einem anderen namespace_declaration auftritt, wird
der innere Namespace zu einem Member des äußeren Namespace. In beiden Fällen muss der Name eines
Namespace innerhalb des enthaltenden Namespace eindeutig sein.
Namespaces sind implizit, public und die Deklaration eines Namespace darf keine Zugriffsmodifizierer
enthalten.
Innerhalb einer namespace_body importieren die optionalen using_directive s die Namen von anderen
Namespaces, Typen und Membern, sodass direkt anstelle von qualifizierten Namen auf Sie verwiesen werden
kann. Die optionalen namespace_member_declaration s tragen Member zum Deklarations Raum des
Namespace bei. Beachten Sie, dass alle using_directive s vor allen Member-Deklarationen angezeigt werden
müssen.
Der qualified_identifier eines namespace_declaration kann ein einzelner Bezeichner oder eine Sequenz von
Bezeichnern sein, die durch ""-Token getrennt sind . . Das zweite Formular ermöglicht einem Programm, einen
geschachtelten Namespace zu definieren, ohne dass mehrere Namespace Deklarationen lexikalisch
verschachtelt werden. Beispiel:

namespace N1.N2
{
class A {}

class B {}
}

ist semantisch äquivalent zu

namespace N1
{
namespace N2
{
class A {}

class B {}
}
}

Namespaces sind offen, und zwei Namespace Deklarationen mit demselben voll qualifizierten Namen tragen
zum selben Deklarations Bereich bei (Deklarationen). Im Beispiel

namespace N1.N2
{
class A {}
}

namespace N1.N2
{
class B {}
}

die beiden oben genannten Namespace Deklarationen tragen zum selben Deklarations Bereich bei, in diesem
Fall werden zwei Klassen mit den voll qualifizierten Namen N1.N2.A und deklariert N1.N2.B . Da die beiden
Deklarationen zum gleichen Deklarations Bereich beitragen, wäre es ein Fehler, wenn jede eine Deklaration eines
Members mit dem gleichen Namen enthielt.

Extern aliases (Externe Aliase)


Eine extern_alias_directive führt einen Bezeichner ein, der als Alias für einen Namespace fungiert. Die
Spezifikation des Alias Namespace ist außerhalb des Quellcodes des Programms und gilt auch für die schsted
Namespaces des Alias Namespace.

extern_alias_directive
: 'extern' 'alias' identifier ';'
;

Der Gültigkeitsbereich einer extern_alias_directive erstreckt sich über die using_directive s, global_attributes und
namespace_member_declaration n der unmittelbar enthaltenden Kompilierungseinheit oder des Namespace
Texts.
Innerhalb einer Kompilierungseinheit oder eines Namespace Texts, der einen extern_alias_directive enthält, kann
der durch die extern_alias_directive eingeführte Bezeichner verwendet werden, um auf den Alias Namespace zu
verweisen. Es handelt sich um einen Kompilierzeitfehler für den Bezeichner , der das Wort ist global .
Ein extern_alias_directive der einen Alias innerhalb einer bestimmten Kompilierungseinheit oder eines
Namespace Texts verfügbar macht, führt jedoch nicht zu neuen Membern, die dem zugrunde liegenden
Deklarations Bereich angehören. Anders ausgedrückt: eine extern_alias_directive ist nicht transitiv, sondern wirkt
sich nur auf die Kompilierungseinheit oder den Namespace Körper aus, in der Sie auftritt.
Das folgende Programm deklariert und verwendet zwei externe Aliase X : und Y , die jeweils den Stamm einer
unterschiedlichen Namespace Hierarchie darstellen:

extern alias X;
extern alias Y;

class Test
{
X::N.A a;
X::N.B b1;
Y::N.B b2;
Y::N.C c;
}

Das Programm deklariert das vorhanden sein der externen Aliase X und Y , aber die tatsächlichen
Definitionen der Aliase sind für das Programm extern. Auf die identisch benannten N.B Klassen kann nun als
X.N.B und oder unter Y.N.B Verwendung des Namespace-alias Qualifizierers und verwiesen werden X::N.B
Y::N.B . Ein Fehler tritt auf, wenn ein Programm einen externen Alias deklariert, für den keine externe Definition
bereitgestellt wird.

using-Direktiven
*Using-Direktiven _ vereinfachen die Verwendung von Namespaces und Typen, die in anderen Namespaces
definiert sind. Using-Direktiven wirken sich auf den namens Auflösungsprozess von _namespace_or_type_name
* s (Namespace-und Typnamen) und Simple_name s (simple names) aus, aber im Gegensatz zu Deklarationen
tragen using-Direktiven keine neuen Member zu den zugrunde liegenden Deklarations Bereichen der
Kompilierungs Einheiten oder Namespaces bei, in denen Sie verwendet werden.

using_directive
: using_alias_directive
| using_namespace_directive
| using_static_directive
;

Ein using_alias_directive (unter Verwendung von Alias Direktiven) führt einen Alias für einen Namespace oder
Typ ein.
Ein using_namespace_directive (unter Verwendung von Namespace Direktiven) importiert die Typmember
eines Namespaces.
Ein using_static_directive (unter Verwendung statischer Direktiven) importiert die Untertypen und statischen
Member eines Typs.
Der Gültigkeitsbereich einer using_directive erstreckt sich über die namespace_member_declaration s der
unmittelbar enthaltenden Kompilierungseinheit oder des Namespace Texts. Der Gültigkeitsbereich einer
using_directive enthält nicht den Peer using_directive s. Daher wirken sich die Peer- using_directive s nicht
gegenseitig aus, und die Reihenfolge, in der Sie geschrieben werden, ist unerheblich.
Using-Alias Direktiven
Eine using_alias_directive führt einen Bezeichner ein, der als Alias für einen Namespace oder Typ innerhalb der
unmittelbar einschließenden Kompilierungseinheit oder des Namespace Texts fungiert.

using_alias_directive
: 'using' identifier '=' namespace_or_type_name ';'
;

Innerhalb von Element Deklarationen in einer Kompilierungseinheit oder einem Namespace Text, der eine
using_alias_directive enthält, kann der durch die using_alias_directive eingeführte Bezeichner verwendet
werden, um auf den angegebenen Namespace oder Typ zu verweisen. Beispiel:

namespace N1.N2
{
class A {}
}

namespace N3
{
using A = N1.N2.A;

class B: A {}
}
Darüber hinaus ist innerhalb von Element Deklarationen im- N3 Namespace A ein Alias für N1.N2.A , und die-
Klasse wird daher N3.B von der-Klasse abgeleitet N1.N2.A . Sie können denselben Effekt erzielen, indem Sie
einen Alias R für erstellen N1.N2 und dann auf verweisen R.A :

namespace N3
{
using R = N1.N2;

class B: R.A {}
}

Der Bezeichner eines using_alias_directive muss innerhalb des Deklarations Bereichs der Kompilierungseinheit
oder des Namespaces eindeutig sein, der die using_alias_directive sofort enthält. Beispiel:

namespace N3
{
class A {}
}

namespace N3
{
using A = N1.N2.A; // Error, A already exists
}

Oben N3 enthält bereits einen Member A , sodass es sich um einen Kompilierzeitfehler für ein
using_alias_directive zur Verwendung dieses Bezeichners handelt. Ebenso ist es ein Kompilierzeitfehler für
mindestens zwei using_alias_directive s im gleichen Kompilierungs-oder Namespace Text, um Aliase mit dem
gleichen Namen zu deklarieren.
Ein using_alias_directive der einen Alias innerhalb einer bestimmten Kompilierungseinheit oder eines
Namespace Texts verfügbar macht, bringt jedoch keine neuen Member in den zugrunde liegenden Deklarations
Bereich. Anders ausgedrückt: eine using_alias_directive ist nicht transitiv, sondern wirkt sich nur auf die
Kompilierungseinheit oder den Namespace Körper aus, in der Sie auftritt. Im Beispiel

namespace N3
{
using R = N1.N2;
}

namespace N3
{
class B: R.A {} // Error, R unknown
}

der Bereich der using_alias_directive , der nur in den Element R Deklarationen im Namespace Text erweitert, in
dem er enthalten ist, R ist in der zweiten Namespace Deklaration unbekannt. Das Platzieren des
using_alias_directive in der enthaltenden Kompilierungseinheit bewirkt jedoch, dass der Alias in beiden
Namespace Deklarationen verfügbar wird:
using R = N1.N2;

namespace N3
{
class B: R.A {}
}

namespace N3
{
class C: R.A {}
}

Ebenso wie reguläre Elemente werden die Namen, die von using_alias_directive s eingeführt werden, durch
ähnlich benannte Member in den in einem Bereich genannten Bereichen ausgeblendet. Im Beispiel

using R = N1.N2;

namespace N3
{
class R {}

class B: R.A {} // Error, R has no member A


}

der Verweis auf R.A in der Deklaration von B verursacht einen Kompilierzeitfehler R , da auf N3.R und nicht
auf verwiesen wird N1.N2 .
Die Reihenfolge, in der using_alias_directive s geschrieben werden, hat keine Bedeutung, und die Auflösung des
namespace_or_type_name , auf das von einer using_alias_directive verwiesen wird, wird nicht von der
using_alias_directive selbst oder von anderen using_directive s in der direkt enthaltenden Kompilierungseinheit
oder im Namespace Text beeinflusst. Anders ausgedrückt: die namespace_or_type_name eines
using_alias_directive wird so aufgelöst, als ob die unmittelbar enthaltende Kompilierungseinheit oder der
Namespace Körper keine using_directive s hat. Eine using_alias_directive kann jedoch von extern_alias_directive
s in der direkt enthaltenden Kompilierungseinheit oder dem Namespace Text betroffen sein. Im Beispiel

namespace N1.N2 {}

namespace N3
{
extern alias E;

using R1 = E.N; // OK

using R2 = N1; // OK

using R3 = N1.N2; // OK

using R4 = R2.N2; // Error, R2 unknown


}

der letzte using_alias_directive führt zu einem Kompilierzeitfehler, da der erste using_alias_directive nicht
beeinträchtigt wird. Der erste using_alias_directive führt nicht zu einem Fehler, da der Bereich des extern-Alias
E den using_alias_directive enthält.

Ein using_alias_directive kann einen Alias für einen beliebigen Namespace oder Typ erstellen, einschließlich des
Namespaces, in dem er angezeigt wird, sowie eines beliebigen Namespaces oder Typs, der in diesem
Namespace geschachtelt ist.
Der Zugriff auf einen Namespace oder Typ über einen Alias ergibt genau dasselbe Ergebnis wie der Zugriff auf
diesen Namespace oder Typ über den deklarierten Namen. Beispiel:

namespace N1.N2
{
class A {}
}

namespace N3
{
using R1 = N1;
using R2 = N1.N2;

class B
{
N1.N2.A a; // refers to N1.N2.A
R1.N2.A b; // refers to N1.N2.A
R2.A c; // refers to N1.N2.A
}
}

die Namen N1.N2.A , R1.N2.A und R2.A sind äquivalent, und alle verweisen auf die-Klasse, deren voll
qualifizierter Name ist N1.N2.A .
Die Verwendung von Aliasen kann einen geschlossenen konstruierten Typ benennen, aber keine ungebundene
generische Typdeklaration benennen, ohne Typargumente anzugeben. Beispiel:

namespace N1
{
class A<T>
{
class B {}
}
}

namespace N2
{
using W = N1.A; // Error, cannot name unbound generic type

using X = N1.A.B; // Error, cannot name unbound generic type

using Y = N1.A<int>; // Ok, can name closed constructed type

using Z<T> = N1.A<T>; // Error, using alias cannot have type parameters
}

Using-namespace Direktiven
Ein using_namespace_directive importiert die in einem Namespace enthaltenen Typen in den unmittelbar
einschließenden Kompilierungs Einheits-oder Namespace Text, sodass der Bezeichner jedes Typs ohne
Qualifizierung verwendet werden kann.

using_namespace_directive
: 'using' namespace_name ';'
;

Innerhalb von Element Deklarationen in einer Kompilierungseinheit oder einem Namespace Text, der eine
using_namespace_directive enthält, kann auf die im angegebenen Namespace enthaltenen Typen direkt
verwiesen werden. Beispiel:
namespace N1.N2
{
class A {}
}

namespace N3
{
using N1.N2;

class B: A {}
}

Darüber hinaus sind innerhalb von Element Deklarationen im- N3 Namespace die Typmember von N1.N2
direkt verfügbar, und daher wird die-Klasse von der- N3.B Klasse abgeleitet N1.N2.A .
Ein- using_namespace_directive importiert die Typen, die im angegebenen Namespace enthalten sind,
importiert jedoch nicht die schsted Namespaces. Im Beispiel

namespace N1.N2
{
class A {}
}

namespace N3
{
using N1;

class B: N2.A {} // Error, N2 unknown


}

der using_namespace_directive importiert die Typen, die in enthalten N1 sind, jedoch nicht die in geschbten
Namespaces N1 . Folglich führt der Verweis auf N2.A in der Deklaration von zu B einem Kompilierzeitfehler,
da sich keine Member mit dem Namen im Gültigkeits N2 Bereich befinden.
Im Gegensatz zu einem using_alias_directive kann ein using_namespace_directive Typen importieren, deren
Bezeichner bereits in der einschließenden Kompilierungseinheit oder im Namespace Körper definiert sind.
Tatsächlich werden Namen, die von einem using_namespace_directive importiert werden, durch ähnlich
benannte Member in der einschließenden Kompilierungseinheit oder im Namespace Körper ausgeblendet.
Beispiel:

namespace N1.N2
{
class A {}

class B {}
}

namespace N3
{
using N1.N2;

class A {}
}

Hier bezieht sich innerhalb von Element Deklarationen im- N3 Namespace A auf N3.A anstelle von N1.N2.A .
Wenn mehr als ein Namespace oder Typ, der von using_namespace_directive s oder using_static_directive s in
derselben Kompilierungseinheit oder im gleichen Namespace Körper importiert wurde, Typen mit demselben
Namen enthalten, werden Verweise auf diesen Namen als TYPE_NAME als mehrdeutig eingestuft. Im Beispiel
namespace N1
{
class A {}
}

namespace N2
{
class A {}
}

namespace N3
{
using N1;

using N2;

class B: A {} // Error, A is ambiguous


}

N1 und N2 enthalten einen-Member A , und da N3 beide importiert, A ist der Verweis auf in N3 ein
Kompilierzeitfehler. In dieser Situation kann der Konflikt entweder durch Qualifizierung von Verweisen auf A
oder durch Einführen eines using_alias_directive gelöst werden, der einen bestimmten Wert auswählt A .
Beispiel:

namespace N3
{
using N1;

using N2;

using A = N1.A;

class B: A {} // A means N1.A


}

Wenn mehr als ein Namespace oder ein Typ, der von using_namespace_directive s oder using_static_directive s
in derselben Kompilierungseinheit oder im gleichen Namespace Körper importiert wurde, Typen oder Member
mit demselben Namen enthalten, werden Verweise auf diesen Namen als Simple_name als mehrdeutig
eingestuft. Im Beispiel
namespace N1
{
class A {}
}

class C
{
public static int A;
}

namespace N2
{
using N1;
using static C;

class B
{
void M()
{
A a = new A(); // Ok, A is unambiguous as a type-name
A.Equals(2); // Error, A is ambiguous as a simple-name
}
}
}

N1 enthält einen Typmember A und C enthält ein statisches Feld A , und da N2 sowohl als auch Verweise
A als Simple_name nicht eindeutig und ein Kompilierzeitfehler ist.
Wie bei einer using_alias_directive trägt ein using_namespace_directive keine neuen Member zum zugrunde
liegenden Deklarations Bereich der Kompilierungseinheit oder des Namespace bei, sondern wirkt sich nur auf
die Kompilierungseinheit oder den Namespace Text aus, in dem er angezeigt wird.
Die namespace_name , auf die von einem using_namespace_directive verwiesen wird, wird auf die gleiche
Weise aufgelöst wie die namespace_or_type_name , auf die von einem using_alias_directive verwiesen wird
Daher wirken sich using_namespace_directive s in derselben Kompilierungseinheit oder im gleichen Namespace
Text nicht gegenseitig aus und können in beliebiger Reihenfolge geschrieben werden.
Verwenden statischer Direktiven
Ein using_static_directive importiert die in einer Typdeklaration enthaltenen, in eine Typdeklaration enthaltenen,
in eine Typdeklaration enthaltenen, in die unmittelbar einschließenden Kompilierungseinheit oder im
Namespace Text enthaltenen statischen Member

using_static_directive
: 'using' 'static' type_name ';'
;

Innerhalb von Element Deklarationen in einer Kompilierungseinheit oder einem Namespace Text, der eine
using_static_directive enthält, kann direkt auf die zugänglichen geschachtelten Typen und statischen Member
(mit Ausnahme von Erweiterungs Methoden) verwiesen werden, die direkt in der Deklaration des angegebenen
Typs enthalten sind. Beispiel:
namespace N1
{
class A
{
public class B{}
public static B M(){ return new B(); }
}
}

namespace N2
{
using static N1.A;
class C
{
void N() { B b = M(); }
}
}

Darüber hinaus sind innerhalb von Element Deklarationen im N2 -Namespace die statischen Member und
geschachtelten Typen von N1.A direkt verfügbar. daher N kann die-Methode auf die B -und-Member von
verweisen M N1.A .
Ein- using_static_directive importiert Erweiterungs Methoden speziell nicht direkt als statische Methoden, stellt
Sie jedoch für den Aufruf der Erweiterungsmethode (Erweiterungs Methodenaufrufe) zur Verfügung. Im Beispiel

namespace N1
{
static class A
{
public static void M(this string s){}
}
}

namespace N2
{
using static N1.A;

class B
{
void N()
{
M("A"); // Error, M unknown
"B".M(); // Ok, M known as extension method
N1.A.M("C"); // Ok, fully qualified
}
}
}

der using_static_directive importiert die M in enthaltene Erweiterungsmethode N1.A , jedoch nur als
Erweiterungsmethode. Folglich führt der erste Verweis auf M im Text von zu B.N einem Kompilierzeitfehler, da
sich keine Member mit dem Namen im Gültigkeits M Bereich befinden.
Ein using_static_directive nur Member und Typen importiert, die direkt im angegebenen Typ deklariert sind,
nicht Elemente und Typen, die in Basisklassen deklariert werden.
TODO: Beispiel
Mehrdeutigkeiten zwischen mehreren using_namespace_directives und using_static_directives werden unter
using namespace-Direktivenerläutert.

Namespace members (Namespacemember)


Ein namespace_member_declaration ist entweder namespace_declaration (Namespace Deklarationen) oder eine
type_declaration (Typdeklarationen).

namespace_member_declaration
: namespace_declaration
| type_declaration
;

Eine Kompilierungseinheit oder ein Namespace Text kann namespace_member_declaration s enthalten, und
solche Deklarationen tragen neue Member zum zugrunde liegenden Deklarations Bereich der enthaltenden
Kompilierungseinheit oder des Namespace Texts bei.

Type declarations (Typdeklarationen)


Eine type_declaration ist eine class_declaration (Klassen Deklarationen), eine struct_declaration (Struktur
Deklarationen), eine interface_declaration (Schnittstellen Deklarationen), eine enum_declaration
(Enumerationsdeklarationen ) odereine delegate_declaration (Delegatdeklarationen).

type_declaration
: class_declaration
| struct_declaration
| interface_declaration
| enum_declaration
| delegate_declaration
;

Eine type_declaration kann als Deklaration der obersten Ebene in einer Kompilierungseinheit oder als Element
Deklaration innerhalb eines Namespace, einer Klasse oder einer Struktur auftreten.
Wenn eine Typdeklaration für einen Typ T als Deklaration der obersten Ebene in einer Kompilierungseinheit
auftritt, ist der voll qualifizierte Name des neu deklarierten Typs einfach T . Wenn eine Typdeklaration für einen
Typ T innerhalb eines Namespace, einer Klasse oder einer Struktur auftritt, ist der voll qualifizierte Name des
neu deklarierten Typs N.T , wobei N der voll qualifizierte Name des enthaltenden Namespace, der Klasse oder
der Struktur ist.
Ein innerhalb einer Klasse oder Struktur deklarierter Typ wird als geschachtelter Typ (geschachtelteTypen)
bezeichnet.
Die zulässigen Zugriffsmodifizierer und der Standard Zugriff für eine Typdeklaration sind abhängig von dem
Kontext, in dem die Deklaration stattfindet (alsBarrierefreiheit deklariert):
In Kompilierungs Einheiten oder Namespaces deklarierte Typen können- public oder- internal Zugriff
besitzen. Der Standardwert ist internal Access.
In Klassen deklarierte Typen können über public , protected internal , protected , internal oder
private Zugriff verfügen. Der Standardwert ist private Access.
In Strukturen deklarierte Typen können über public , internal oder private Zugriff verfügen. Der
Standardwert ist private Access.

Namespace alias qualifiers (Qualifizierer für Namespacealiase)


Der Namespacealias-Qualifizierer :: ermöglicht es, sicherzustellen, dass die Suche nach Typnamen durch
die Einführung neuer Typen und Member nicht beeinträchtigt wird. Der Namespacealias-Qualifizierer wird
immer zwischen zwei bezeichern angezeigt, die als linke und Rechte Bezeichner bezeichnet werden. Im
Gegensatz zum regulären . Qualifizierer wird der linke Bezeichner des :: Qualifizierers nur als extern oder
using-Alias gesucht.
Ein qualified_alias_member wird wie folgt definiert:

qualified_alias_member
: identifier '::' identifier type_argument_list?
;

Eine qualified_alias_member kann als namespace_or_type_name (Namespace-und Typnamen) oder als Linker
Operand in einem member_access (Member Access) verwendet werden.
Ein qualified_alias_member hat eine von zwei Formen:
N::I<A1, ..., Ak>, wobei N und I Identifizierer darstellen und <A1, ..., Ak> eine Typargument Liste ist.
( K ist immer mindestens ein.)
N::I , wobei N und I Bezeichner darstellen. (In diesem Fall gilt K als 0 (null).)
Mit dieser Notation wird die Bedeutung eines qualified_alias_member wie folgt bestimmt:
Wenn N der Bezeichner ist global , wird der globale Namespace durchsucht I :
Wenn der globale-Namespace einen Namespace mit dem Namen I und 0 (null) enthält K ,
verweist der qualified_alias_member auf diesen Namespace.
Wenn der globale Namespace andernfalls einen nicht generischen Typ mit dem Namen I und K 0
(null) enthält, verweist der qualified_alias_member auf diesen Typ.
Wenn der globale Namespace einen Typ mit dem Namen enthält, I K der über Typparameter
verfügt, verweist der qualified_alias_member auf diesen Typ, der mit den angegebenen
Typargumenten erstellt wurde.
Andernfalls ist der qualified_alias_member nicht definiert, und es tritt ein Kompilierzeitfehler auf.
Andernfalls werden ab der Namespace Deklaration (Namespace Deklarationen), die sofort die
qualified_alias_member enthält (sofern vorhanden), mit jeder einschließenden Namespace Deklaration
(sofern vorhanden) und mit der Kompilierungseinheit, die die qualified_alias_member enthält, die
folgenden Schritte ausgewertet, bis eine Entität gefunden wird:
Wenn die Namespace Deklaration oder Kompilierungseinheit eine using_alias_directive enthält, die N
einem Typ zugeordnet ist, ist die qualified_alias_member nicht definiert, und ein Kompilierzeitfehler
tritt auf.
Andernfalls, wenn die Namespace Deklaration oder Kompilierungseinheit eine extern_alias_directive
oder using_alias_directive enthält, die N einem Namespace zugeordnet ist, dann:
Wenn der zugeordnete Namespace N einen Namespace mit dem Namen I und 0 (null)
enthält K , verweist der qualified_alias_member auf diesen Namespace.
Wenn der Namespace, der zugeordnet N ist, einen nicht generischen Typ mit I dem Namen
und 0 (null) enthält K , verweist der qualified_alias_member auf diesen Typ.
Wenn der Namespace, der zugeordnet N ist, einen Typ mit I dem Namen enthält K , der
über Typparameter verfügt, verweist der qualified_alias_member auf diesen Typ, der mit den
angegebenen Typargumenten erstellt wurde.
Andernfalls ist der qualified_alias_member nicht definiert, und es tritt ein Kompilierzeitfehler
auf.
Andernfalls ist der qualified_alias_member nicht definiert, und es tritt ein Kompilierzeitfehler auf.
Beachten Sie, dass die Verwendung des Namespace-alias Qualifizierers mit einem Alias, der auf einen Typ
verweist, zu einem Kompilierzeitfehler Beachten Sie auch, dass die N Suche im globalen Namespace
durchgeführt wird, wenn der Bezeichner ist, und zwar global auch dann, wenn ein Using-Alias global mit
einem Typ oder Namespace verknüpft ist.
Eindeutigkeit von Aliasen
Jede Kompilierungseinheit und jeder Namespace Körper verfügen über einen separaten Deklarations Raum für
externe Aliase und mithilfe von Aliasen. Folglich muss der Name eines externen Alias oder der using-alias
innerhalb des Satzes externer Aliase eindeutig sein und Aliase verwenden, die in der direkt enthaltenden
Kompilierungseinheit oder im Namespace Körper deklariert sind. ein Alias kann daher denselben Namen wie
ein Typ oder Namespace aufweisen, solange er nur mit dem Qualifizierer verwendet wird :: .
Im Beispiel

namespace N
{
public class A {}

public class B {}
}

namespace N
{
using A = System.IO;

class X
{
A.Stream s1; // Error, A is ambiguous

A::Stream s2; // Ok
}
}

der Name A weist zwei mögliche Bedeutungen im zweiten Namespace Text auf, da sich sowohl die A -Klasse
als auch der using-Alias im Gültigkeits A Bereich befinden. Aus diesem Grund A ist die Verwendung von im
qualifizierten Namen A.Stream mehrdeutig und bewirkt, dass ein Kompilierzeitfehler auftritt. Die Verwendung
von A mit dem :: Qualifizierer ist jedoch kein Fehler, da A nur als Namespacealias gesucht wird.
Klassen
04.11.2021 • 302 minutes to read

Eine-Klasse ist eine Datenstruktur, die Datenmember (Konstanten und Felder), Funktionsmember (Methoden,
Eigenschaften, Ereignisse, Indexer, Operatoren, Instanzkonstruktoren, Dekonstruktoren und statische
Konstruktoren) und die in der Struktur enthaltenen Typen enthalten kann. Klassentypen unterstützen Vererbung,
einen Mechanismus, mit dem eine abgeleitete Klasse eine Basisklasse erweitern und spezialisieren kann.

Class declarations (Klassendeklarationen)


Eine class_declaration ist eine type_declaration (Typdeklarationen), die eine neue Klasse deklariert.

class_declaration
: attributes? class_modifier* 'partial'? 'class' identifier type_parameter_list?
class_base? type_parameter_constraints_clause* class_body ';'?
;

Eine class_declaration besteht aus einem optionalen Satz von Attributen (Attributen), gefolgt von einer
optionalen Gruppe von class_modifier s (Klassenmodifizierer). gefolgt von einem optionalen partial -
Modifizierer, gefolgt vom-Schlüsselwort class und einem Bezeichner , der die-Klasse benennt, gefolgt von
einer optionalen type_parameter_list (Typparameter), gefolgt von einer optionalen class_base -Spezifikation
(Klassenbasis Spezifikation), gefolgt von einem optionalen Satz von type_parameter_constraints_clause s
(Typparameter Einschränkungen), gefolgt von einer class_body (Klassen Text), optional gefolgt von einem
Semikolon.
Eine Klassen Deklaration kann nur type_parameter_constraints_clause s bereitstellen, wenn Sie auch eine
type_parameter_list bereitstellt.
Eine Klassen Deklaration, die einen type_parameter_list bereitstellt, ist eine generische Klassen Deklaration .
Außerdem ist jede Klasse, die in einer generischen Klassen Deklaration oder generischen Struktur Deklaration
geschachtelt ist, selbst eine generische Klassen Deklaration, da Typparameter für den enthaltenden Typ
angegeben werden müssen, um einen konstruierten Typ zu erstellen.
Klassenmodifizierer
Eine class_declaration kann optional eine Sequenz von Klassenmodifizierer einschließen:

class_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'abstract'
| 'sealed'
| 'static'
| class_modifier_unsafe
;

Es ist ein Kompilierzeitfehler, damit derselbe Modifizierer mehrmals in einer Klassen Deklaration angezeigt wird.
Der- new Modifizierer ist für-Klassen zulässig. Er gibt an, dass die Klasse einen geerbten Member mit
demselben Namen verbirgt, wie im neuen Modifiziererbeschrieben. Es ist ein Kompilierzeitfehler, damit der-
new Modifizierer in einer Klassen Deklaration angezeigt wird, die keine Klassen Deklaration ist.
Die public protected internal private Modifizierer,, und Steuern den Zugriff auf die-Klasse. Abhängig vom
Kontext, in dem die Klassen Deklaration auftritt, sind einige dieser Modifizierer möglicherweise nicht zulässig
(alsBarrierefreiheit deklariert).
Die abstract sealed static modifiziererer, und werden in den folgenden Abschnitten erläutert.
Abstrakte Klassen
Der abstract -Modifizierer wird verwendet, um anzugeben, dass eine Klasse unvollständig ist und nur als
Basisklasse verwendet werden soll. Eine abstrakte Klasse unterscheidet sich wie folgt von einer nicht abstrakten
Klasse:
Eine abstrakte Klasse kann nicht direkt instanziiert werden, und es handelt sich um einen Kompilierzeitfehler,
wenn der new Operator für eine abstrakte Klasse verwendet werden soll. Obwohl es möglich ist, Variablen
und Werte zu haben, deren Kompilier Zeittypen abstrakt sind, sind diese Variablen und Werte
notwendigerweise entweder ein null oder enthalten Verweise auf Instanzen von nicht abstrakten Klassen,
die von den abstrakten Typen abgeleitet sind.
Eine abstrakte Klasse ist zulässig ( jedoch nicht erforderlich), um abstrakte Member zu enthalten.
Eine abstrakte Klasse kann nicht versiegelt werden.
Wenn eine nicht abstrakte Klasse von einer abstrakten Klasse abgeleitet wird, muss die nicht abstrakte Klasse
tatsächliche Implementierungen aller geerbten abstrakten Member enthalten, wodurch diese abstrakten
Member überschrieben werden. Im Beispiel

abstract class A
{
public abstract void F();
}

abstract class B: A
{
public void G() {}
}

class C: B
{
public override void F() {
// actual implementation of F
}
}

die abstrakte-Klasse A führt eine abstrakte Methode ein F . Die-Klasse B führt eine zusätzliche-Methode ein
G , aber da Sie keine Implementierung von bereitstellt F , B muss auch als abstrakt deklariert werden. C Die
Klasse überschreibt F und stellt eine tatsächliche Implementierung bereit. Da keine abstrakten Member in
vorhanden sind C , C ist zulässig ( jedoch nicht erforderlich), um nicht abstrakt zu sein.
Versiegelte Klassen
Der- sealed Modifizierer wird verwendet, um die Ableitung von einer Klasse zu verhindern. Ein
Kompilierzeitfehler tritt auf, wenn eine versiegelte Klasse als Basisklasse einer anderen Klasse angegeben wird.
Eine versiegelte Klasse kann nicht auch eine abstrakte Klasse sein.
Der- sealed Modifizierer wird hauptsächlich verwendet, um eine unbeabsichtigte Ableitung zu verhindern, er
ermöglicht aber auch bestimmte Lauf Zeit Optimierungen. Insbesondere weil eine versiegelte Klasse
bekanntermaßen keine abgeleiteten Klassen hat, ist es möglich, die Aufrufe virtueller Funktionsmember für
versiegelte Klassen Instanzen in nicht virtuelle Aufrufe umzuwandeln.
Statische Klassen
Der- static Modifizierer wird verwendet, um die Klasse zu markieren, die als statische Klasse deklariert wird.
Eine statische Klasse kann nicht instanziiert werden, kann nicht als Typ verwendet werden und darf nur statische
Member enthalten. Nur eine statische Klasse kann Deklarationen von Erweiterungs Methoden (Erweiterungs
Methoden) enthalten.
Eine statische Klassen Deklaration unterliegt den folgenden Einschränkungen:
Eine statische Klasse darf keinen- sealed abstract Modifizierer oder-Modifizierer enthalten. Beachten Sie
jedoch, dass eine statische Klasse, die nicht von instanziiert oder abgeleitet werden kann, so verhält, als ob
Sie sowohl versiegelt als auch abstrakt wäre.
Eine statische Klasse darf keine class_base Spezifikation (Klassenbasis Spezifikation) enthalten und kann
weder eine Basisklasse noch eine Liste implementierter Schnittstellen explizit angeben. Eine statische Klasse
erbt implizit vom Typ object .
Eine statische Klasse kann nur statische Member (statische Member und Instanzmember) enthalten. Beachten
Sie, dass Konstanten und Untertypen als statische Member klassifiziert werden.
Eine statische Klasse kann keine Member mit protected oder protected internal deklarierter
Barrierefreiheit haben.
Es handelt sich um einen Kompilierzeitfehler, der gegen diese Einschränkungen verstößt.
Eine statische Klasse hat keine Instanzkonstruktoren. Es ist nicht möglich, einen Instanzkonstruktor in einer
statischen Klasse zu deklarieren, und für eine statische Klasse wird kein Standardinstanzkonstruktor
(Standardkonstruktoren) bereitgestellt.
Die Member einer statischen Klasse sind nicht automatisch statisch, und die Element Deklarationen müssen
explizit einen static Modifizierer einschließen (mit Ausnahme von Konstanten und Typen). Wenn eine Klasse in
einer statischen äußeren Klasse geschachtelt ist, ist die geschachtelte Klasse keine statische Klasse, es sei denn,
Sie enthält explizit einen static Modifizierer.
Ver weisen auf statische Klassentypen
Eine namespace_or_type_name (Namespace-und Typnamen) darf auf eine statische Klasse verweisen, wenn
Der namespace_or_type_name ist T ein namespace_or_type_name des Formulars. T.I
Der namespace_or_type_name ist das T in einem typeof_expression (Argument Listen1) im Formular
typeof(T) .

Eine primary_expression (Funktionsmember) darf auf eine statische Klasse verweisen, wenn
Der primary_expression ist das E in einer member_access (Überprüfung der Auflösung der dynamischen
Überlastung) des Formulars E.I .
In jedem anderen Kontext ist es ein Kompilierzeitfehler, um auf eine statische Klasse zu verweisen. Es ist z. b. ein
Fehler für eine statische Klasse, die als Basisklasse, als konstituierender Typ (in Form vonTypen) eines Members,
als generisches Typargument oder als Typparameter Einschränkung verwendet werden soll. Ebenso kann eine
statische Klasse nicht in einem Arraytyp, einem Zeigertyp, einem new Ausdruck, einem Umwandlungs
Ausdruck, einem is Ausdruck, einem Ausdruck, as einem Ausdruck sizeof oder einem Standardwert
Ausdruck verwendet werden.
Partieller Modifizierer
Der- partial Modifizierer wird verwendet, um anzugeben, dass diese class_declaration eine partielle
Typdeklaration ist. Mehrere partielle Typdeklarationen mit demselben Namen innerhalb eines einschließenden
Namespace oder einer Typdeklaration kombinieren eine Typdeklaration, die den in partiellen
Typenangegebenen Regeln folgt.
Die Deklaration einer Klasse, die über separate Segmente von Programmtext verteilt ist, kann nützlich sein,
wenn diese Segmente in verschiedenen Kontexten erstellt oder verwaltet werden. Beispielsweise kann ein Teil
einer Klassen Deklaration maschinell generiert werden, während der andere manuell erstellt wird. Die Text
Trennung der beiden verhindert, dass Updates durch eine in Konflikt mit Updates durch die andere verursacht
werden.
Typparameter
Ein Typparameter ist ein einfacher Bezeichner, der einen Platzhalter für ein Typargument angibt, das zum
Erstellen eines konstruierten Typs bereitgestellt wird. Ein Typparameter ist ein formaler Platzhalter für einen Typ,
der später bereitgestellt wird. Im Gegensatz dazu ist einTypargument (Typargumente) der tatsächliche Typ, der
beim Erstellen eines konstruierten Typs den Typparameter ersetzt.

type_parameter_list
: '<' type_parameters '>'
;

type_parameters
: attributes? type_parameter
| type_parameters ',' attributes? type_parameter
;

type_parameter
: identifier
;

Jeder Typparameter in einer Klassen Deklaration definiert einen Namen im Deklarations Raum (Deklarationen)
dieser Klasse. Daher kann er nicht denselben Namen wie ein anderer Typparameter oder ein Member haben, der
in dieser Klasse deklariert ist. Ein Typparameter kann nicht den gleichen Namen haben wie der Typ selbst.
Klassenbasis Spezifikation
Eine Klassen Deklaration kann eine class_base Spezifikation enthalten, die die direkte Basisklasse der Klasse und
die Schnittstellen (Schnittstellen) definiert, die von der-Klasse direkt implementiert werden.

class_base
: ':' class_type
| ':' interface_type_list
| ':' class_type ',' interface_type_list
;

interface_type_list
: interface_type (',' interface_type)*
;

Die in einer Klassen Deklaration angegebene Basisklasse kann ein konstruierter Klassentyp (konstruierte Typen)
sein. Eine Basisklasse kann nicht eigenständig ein Typparameter sein, Sie kann jedoch die Typparameter
enthalten, die sich im Gültigkeitsbereich befinden.

class Extend<V>: V {} // Error, type parameter used as base class

Basisklassen
Wenn ein class_type im class_base enthalten ist, gibt es die direkte Basisklasse der Klasse an, die deklariert wird.
Wenn eine Klassen Deklaration keine class_base hat oder wenn die class_base nur Schnittstellentypen auflistet,
wird angenommen, dass die direkte Basisklasse ist object . Eine Klasse erbt Member von ihrer direkten
Basisklasse, wie in Vererbungbeschrieben.
Im Beispiel
class A {}

class B: A {}

A die Klasse ist die direkte Basisklasse von B , und wird als B abgeleitet bezeichnet A . Da A nicht explizit
eine direkte Basisklasse angibt, ist die direkte Basisklasse implizit object .
Wenn eine Basisklasse für einen konstruierten Klassentyp in der generischen Klassen Deklaration angegeben
wird, wird die Basisklasse des konstruierten Typs abgerufen, indem für jede type_parameter in der Basisklassen
Deklaration der entsprechende type_argument des konstruierten Typs ersetzt wird. Bei Angabe der generischen
Klassen Deklarationen

class B<U,V> {...}

class G<T>: B<string,T[]> {...}

die Basisklasse des konstruierten Typs G<int> wäre B<string,int[]> .


Die direkte Basisklasse eines Klassen Typs muss mindestens so zugänglich sein wie der Klassentyp selbst
(Barrierefreiheits Domänen). Beispielsweise ist es ein Kompilierzeitfehler für eine public Klasse, die von einer-
private Klasse oder-Klasse abgeleitet werden soll internal .

Die direkte Basisklasse eines Klassen Typs darf keinem der folgenden Typen sein: System.Array ,
System.Delegate , System.MulticastDelegate , System.Enum oder System.ValueType . Darüber hinaus kann eine
generische Klassen Deklaration nicht System.Attribute als direkte oder indirekte Basisklasse verwenden.
Bei der Bestimmung der Bedeutung der direkten Basisklassen Spezifikation A einer Klasse B wird die direkte
Basisklasse von vorübergehend als festgelegt B object . Intuitiv wird dadurch sichergestellt, dass die
Bedeutung einer Basisklassen Spezifikation nicht rekursiv von sich selbst abhängig ist. Beispiel:

class A<T> {
public class B {}
}

class C : A<C.B> {}

ist fehlerhaft, da in der Basisklassen Spezifikation A<C.B> die direkte Basisklasse von C als betrachtet wird
object und daher (gemäß den Regeln von Namespace-und Typnamen) C nicht als Member angesehen wird
B .

Die Basisklassen eines Klassen Typs sind die direkte Basisklasse und deren Basisklassen. Mit anderen Worten:
der Satz von Basisklassen ist der transitiv Abschluss der direkten Basisklassen Beziehung. Im obigen Beispiel
sind die Basisklassen von B A und object . Im Beispiel

class A {...}

class B<T>: A {...}

class C<T>: B<IComparable<T>> {...}

class D<T>: C<T[]> {...}

die Basisklassen von D<int> sind C<int[]> , B<IComparable<int[]>> , A und object .


Mit Ausnahme von Class object hat jeder Klassentyp genau eine direkte Basisklasse. Die object Klasse verfügt
über keine direkte Basisklasse und ist die ultimative Basisklasse aller anderen Klassen.
Wenn eine Klasse B von einer Klasse abgeleitet ist A , ist dies ein Kompilierzeitfehler, von A dem abhängig ist
B . Eine Klasse hängt direkt von _ ihrer direkten Basisklasse (sofern vorhanden) ab und _hängt direkt*_ von
der Klasse ab, in der Sie sofort geschachtelt ist (sofern vorhanden). Bei dieser Definition ist der gesamte Satz von
Klassen, von dem eine Klasse abhängt, das reflexive und transitiv Schließen der _ direkt von * Beziehung
abhängig.
Das Beispiel

class A: A {}

ist fehlerhaft, da die-Klasse von sich selbst abhängt. Ebenso ist das Beispiel

class A: B {}
class B: C {}
class C: A {}

ist fehlerhaft, da die Klassen zirkulär von sich selbst abhängen. Abschließend wird das Beispiel

class A: B.C {}

class B: A
{
public class C {}
}

führt zu einem Kompilierzeitfehler, da von A B.C (seiner direkten Basisklasse) abhängt, das von B (seiner
unmittelbar einschließenden Klasse) abhängt, von dem zirkulär abhängig ist A .
Beachten Sie, dass eine Klasse nicht von den Klassen abhängig ist, die darin geschachtelt sind. Im Beispiel

class A
{
class B: A {}
}

B hängt von ab A (da A sowohl die direkte Basisklasse als auch die unmittelbar einschließende Klasse ist),
aber nicht von abhängt A B (da B weder eine Basisklasse noch eine einschließende Klasse von ist A ). Daher
ist das Beispiel gültig.
Es ist nicht möglich, von einer sealed Klasse abzuleiten. Im Beispiel

sealed class A {}

class B: A {} // Error, cannot derive from a sealed class

B die Klasse ist fehlerhaft, da Sie versucht, von der- sealed Klasse abzuleiten A .
Schnittstellenimplementierungen
Eine class_base Spezifikation kann eine Liste von Schnittstellentypen enthalten. in diesem Fall wird die Klasse so
genannte, dass die angegebenen Schnittstellentypen direkt implementiert werden. Schnittstellen
Implementierungen werden in Schnittstellen Implementierungenausführlicher erläutert.
Typparameter Einschränkungen
Generische Typen-und Methoden Deklarationen können optional Typparameter Einschränkungen angeben,
indem Sie type_parameter_constraints_clause s einschließen.

type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;

type_parameter_constraints
: primary_constraint
| secondary_constraints
| constructor_constraint
| primary_constraint ',' secondary_constraints
| primary_constraint ',' constructor_constraint
| secondary_constraints ',' constructor_constraint
| primary_constraint ',' secondary_constraints ',' constructor_constraint
;

primary_constraint
: class_type
| 'class'
| 'struct'
;

secondary_constraints
: interface_type
| type_parameter
| secondary_constraints ',' interface_type
| secondary_constraints ',' type_parameter
;

constructor_constraint
: 'new' '(' ')'
;

Jede type_parameter_constraints_clause besteht aus dem Token where , gefolgt vom Namen eines
Typparameters, gefolgt von einem Doppelpunkt und der Liste der Einschränkungen für diesen Typparameter.
where Für jeden Typparameter kann höchstens eine Klausel vorhanden sein, und die where Klauseln können in
beliebiger Reihenfolge aufgelistet werden. Wie das get -Token und das- set Token in einem Eigenschaften
Accessor where ist das Token kein Schlüsselwort.
Die Liste der Einschränkungen, die in einer- where Klausel angegeben werden, kann eine der folgenden
Komponenten in dieser Reihenfolge enthalten: eine einzelne primäre Einschränkung, eine oder mehrere
sekundäre Einschränkungen und die Konstruktoreinschränkung new() .
Eine primäre Einschränkung kann ein Klassentyp oder der Ver weistyp Einschränkung _ class oder die
_Werttyp Einschränkung*_ sein struct . Eine sekundäre Einschränkung kann ein _type_parameter * oder
INTERFACE_TYPE sein.
Die Verweistyp Einschränkung gibt an, dass ein für den Typparameter verwendetes Typargument ein Verweistyp
sein muss. Alle Klassentypen, Schnittstellentypen, Delegattypen, Array Typen und Typparameter, die als
Verweistyp bekannt sind (wie unten definiert), erfüllen diese Einschränkung.
Die Werttyp Einschränkung gibt an, dass das für den Typparameter verwendete Typargument ein Werttyp sein
muss, der keine NULL-Werte zulässt. Alle Strukturtypen, Enumerationstypen und Typparameter, die keine NULL-
Werte zulassen und die Werttyp Einschränkung aufweisen, erfüllen diese Einschränkung. Beachten Sie, dass ein
Typ, der NULL-Werte zulässt (Typen, dienull-Werte zulassen), nicht der Werttyp Einschränkung entspricht. Ein
Typparameter mit der Werttyp Einschränkung kann nicht auch über die constructor_constraint verfügen.
Zeiger Typen dürfen nicht als Typargumente eingestuft werden und werden nicht berücksichtigt, um entweder
den Verweistyp oder die Werttyp Einschränkungen zu erfüllen.
Wenn es sich bei einer Einschränkung um einen Klassentyp, einen Schnittstellentyp oder einen Typparameter
handelt, gibt dieser Typ einen minimalen "Basistyp" an, den jedes Typargument für diesen Typparameter
unterstützen muss. Wenn ein konstruierter Typ oder eine generische Methode verwendet wird, wird das
Typargument zur Kompilierzeit mit den Einschränkungen für den Typparameter verglichen. Das angegebene
Typargument muss die unter " erfüllen von Einschränkungen" beschriebenen Bedingungen erfüllen.
Eine class_type Einschränkung muss die folgenden Regeln erfüllen:
Der Typ muss ein Klassentyp sein.
Der Typ darf nicht sein sealed .
Der Typ darf keinem der folgenden Typen sein: System.Array , System.Delegate , System.Enum oder
System.ValueType .
Der Typ darf nicht sein object . Da alle Typen von abgeleitet object sind, hätte eine solche Einschränkung
keine Auswirkung, wenn Sie zulässig wäre.
Höchstens eine Einschränkung für einen angegebenen Typparameter kann ein Klassentyp sein.
Ein als INTERFACE_TYPE Einschränkung angegebener Typ muss die folgenden Regeln erfüllen:
Der Typ muss ein Schnittstellentyp sein.
Ein Typ darf in einer gegebenen Klausel nicht mehrmals angegeben werden where .

In beiden Fällen kann die Einschränkung einen der Typparameter der zugeordneten Typ-oder Methoden
Deklaration als Teil eines konstruierten Typs einschließen und den Typ einschließen, der deklariert wird.
Jeder Klassen-oder Schnittstellentyp, der als Typparameter Einschränkung angegeben ist, muss mindestens so
zugänglich sein (BarrierefreiheitsEinschränkungen) wie der generische Typ oder die Methode, der deklariert
wird.
Ein als type_parameter Einschränkung angegebener Typ muss die folgenden Regeln erfüllen:
Der Typ muss ein Typparameter sein.
Ein Typ darf in einer gegebenen Klausel nicht mehrmals angegeben werden where .

Außerdem dürfen im Abhängigkeits Diagramm der Typparameter keine Zyklen vorhanden sein, bei denen die
Abhängigkeit eine transitiv Beziehung ist, die durch definiert wird:
Wenn ein Typparameter T als Einschränkung für den Typparameter verwendet wird S , S hängt von ab
T .
Wenn ein Typparameter von S einem Typparameter abhängt T und von T einem Typparameter abhängt,
U hängt von S ab U .

Bei dieser Beziehung handelt es sich um einen Kompilierzeitfehler für einen Typparameter, der direkt oder
indirekt von sich selbst abhängig ist.
Alle Einschränkungen müssen zwischen abhängigen Typparametern einheitlich sein. Wenn der Typparameter
vom S Typparameter abhängt, T dann:
T darf nicht über die Werttyp Einschränkung verfügen. Andernfalls T ist tatsächlich versiegelt, sodass S
gezwungen wird, denselben Typ wie zu T haben, sodass zwei Typparameter nicht mehr benötigt werden.
Wenn S die Werttyp Einschränkung hat, T darf keine class_type -Einschränkung aufweisen.
Wenn S über eine class_type -Einschränkung verfügt A und T eine class_type -Einschränkung aufweist,
B muss eine Identitäts Konvertierung oder eine implizite Verweis Konvertierung von A in B oder eine
implizite Verweis Konvertierung von in vorhanden sein B A .
Wenn S auch vom Typparameter abhängt U und über U eine class_type -Einschränkung verfügt A und
T eine class_type -Einschränkung aufweist, B muss eine Identitäts Konvertierung oder eine implizite
Verweis Konvertierung von A in B oder eine implizite Verweis Konvertierung von in vorhanden sein B A
.
Es ist zulässig S , dass die Werttyp Einschränkung und T die Verweistyp Einschränkung aufweisen. Dies
schränkt praktisch T die Typen System.Object , System.ValueType , System.Enum und alle Schnittstellentypen
ein.
Wenn die- where Klausel für einen Typparameter eine Konstruktoreinschränkung (die das-Format aufweist
new() ) enthält, kann der-Operator verwendet werden, new um Instanzen des-Typs (Objekt Erstellungs
Ausdrücke) zu erstellen. Alle Typargumente, die für einen Typparameter mit einer Konstruktoreinschränkung
verwendet werden, müssen über einen öffentlichen Parameter losen Konstruktor verfügen (dieser Konstruktor
ist für jeden Werttyp implizit vorhanden) oder ein Typparameter mit der Werttyp Einschränkung oder der
Konstruktoreinschränkung (Weitere Informationen finden Sie unter Typparameter Einschränkungen ).
Im folgenden finden Sie Beispiele für Einschränkungen:

interface IPrintable
{
void Print();
}

interface IComparable<T>
{
int CompareTo(T value);
}

interface IKeyProvider<T>
{
T GetKey();
}

class Printer<T> where T: IPrintable {...}

class SortedList<T> where T: IComparable<T> {...}

class Dictionary<K,V>
where K: IComparable<K>
where V: IPrintable, IKeyProvider<K>, new()
{
...
}

Das folgende Beispiel ist fehlerhaft, da es eine Zirkularität im Abhängigkeits Diagramm der Typparameter
verursacht:

class Circular<S,T>
where S: T
where T: S // Error, circularity in dependency graph
{
...
}

In den folgenden Beispielen werden zusätzliche ungültige Situationen veranschaulicht:


class Sealed<S,T>
where S: T
where T: struct // Error, T is sealed
{
...
}

class A {...}

class B {...}

class Incompat<S,T>
where S: A, T
where T: B // Error, incompatible class-type constraints
{
...
}

class StructWithClass<S,T,U>
where S: struct, T
where T: U
where U: A // Error, A incompatible with struct
{
...
}

Die effektive Basisklasse eines Typparameters T wird wie folgt definiert:


Wenn T keine Primary-Einschränkungen oder Typparameter Einschränkungen aufweist, ist die effektive
Basisklasse object .
Wenn T die Werttyp Einschränkung aufweist, ist die effektive Basisklasse System.ValueType .
Wenn T eine class_type Einschränkung C , aber keine type_parameter Einschränkungen hat, ist die
effektive Basisklasse C .
Wenn T keine class_type Einschränkung hat, aber über eine oder mehrere type_parameter -
Einschränkungen verfügt, ist die effektive Basisklasse der am häufigsten in der Reihe der in der Reihe von
gültigen Basisklassen der type_parameter Einschränkungen enthaltenen Typen. Durch die Konsistenzregeln
wird sichergestellt, dass ein solcher Typ vorhanden ist.
Wenn T sowohl eine class_type -Einschränkung als auch eine oder mehrere type_parameter -
Einschränkungen aufweist, ist die effektive Basisklasse der am häufigsten durch zufügungs Operator
(gesteigerte Konvertierungs Operator) in der Menge, die aus der class_type -Einschränkung von T und den
effektiven Basisklassen der type_parameter Einschränkungen besteht. Durch die Konsistenzregeln wird
sichergestellt, dass ein solcher Typ vorhanden ist.
Wenn T die Verweistyp Einschränkung, aber keine class_type Einschränkungen aufweist, ist die effektive
Basisklasse object .

Verwenden Sie für diese Regeln V stattdessen den spezifischsten Basistyp von, bei dem es sich um eine
value_type-Einschränkung handelt, bei der es sich um eine V class_type handelt. Dies kann in einer explizit
angegebenen Einschränkung nie vorkommen, kann jedoch auftreten, wenn die Einschränkungen einer
generischen Methode implizit von einer über schreibenden Methoden Deklaration oder einer expliziten
Implementierung einer Schnittstellen Methode geerbt werden.
Diese Regeln stellen sicher, dass die effektive Basisklasse immer ein class_type ist.
Der effektive Schnittstellen Satz eines Typparameters T wird wie folgt definiert:
Wenn T keine secondary_constraints hat, ist der effektive Schnittstellen Satz leer.
Wenn T INTERFACE_TYPE Einschränkungen, aber keine type_parameter Einschränkungen aufweist, ist der
effektive Schnittstellen Satz der Satz von INTERFACE_TYPE Einschränkungen.
Wenn T keine INTERFACE_TYPE Einschränkungen aufweist, aber über type_parameter Einschränkungen
verfügt, ist der effektive Schnittstellen Satz die Vereinigung der effektiven Schnittstellen Sätze seiner
type_parameter Einschränkungen.
Wenn T sowohl INTERFACE_TYPE Einschränkungen als auch type_parameter Einschränkungen aufweist, ist
der effektive Schnittstellen Satz die Vereinigung seines Satzes von INTERFACE_TYPE Einschränkungen und
die effektiven Schnittstellen Sätze der type_parameter Einschränkungen.
Ein Typparameter ist bekanntermaßen ein Referenztyp, wenn er über die Verweistyp Einschränkung oder
seine effektive Basisklasse nicht object oder ist System.ValueType .
Werte eines eingeschränkten Typparameter Typs können für den Zugriff auf die Instanzmember verwendet
werden, die durch die Einschränkungen impliziert sind. Im Beispiel

interface IPrintable
{
void Print();
}

class Printer<T> where T: IPrintable


{
void PrintOne(T x) {
x.Print();
}
}

die-Methoden von IPrintable können direkt auf aufgerufen x werden T , da eingeschränkt ist, um immer zu
implementieren IPrintable .
Klassen Text
Der class_body einer Klasse definiert die Member dieser Klasse.

class_body
: '{' class_member_declaration* '}'
;

Partial types (Partielle Typen)


Eine Typdeklaration kann über mehrere par tielle Typdeklarationen hinweg aufgeteilt werden. Die
Typdeklaration wird anhand der in diesem Abschnitt aufgeführten Regeln erstellt, woraufhin Sie im Rest der
Kompilierzeit-und Lauf Zeit Verarbeitung des Programms als eine einzige Deklaration behandelt wird.
Eine class_declaration, struct_declaration oder interface_declaration stellt eine partielle Typdeklaration dar, wenn
Sie einen- partial Modifizierer enthält. partial ist kein Schlüsselwort und fungiert nur als Modifizierer, wenn
er unmittelbar vor einem der Schlüsselwörter class struct oder interface in einer Typdeklaration oder vor
dem Typ void in einer Methoden Deklaration angezeigt wird. In anderen Kontexten kann es als normaler
Bezeichner verwendet werden.
Jeder Teil einer partiellen Typdeklaration muss einen partial Modifizierer enthalten. Er muss denselben Namen
haben und in derselben Namespace-oder Typdeklaration wie die anderen Teile deklariert werden. Der- partial
Modifizierer gibt an, dass zusätzliche Teile der Typdeklaration an anderer Stelle vorhanden sein können, aber das
vorhanden sein solcher zusätzlicher Teile ist nicht erforderlich. es ist für einen Typ mit einer einzelnen
Deklaration gültig, den partial Modifizierer einzuschließen.
Alle Teile eines partiellen Typs müssen zusammen kompiliert werden, sodass die Teile zur Kompilierzeit in eine
einzelne Typdeklaration zusammengeführt werden können. Bei partiellen Typen können nicht bereits
kompilierte Typen erweitert werden.
Mit dem-Modifizierer können mit dem-Modifizierer in mehreren Teilen deklarierte Typen deklariert werden
partial . In der Regel wird der enthaltende Typ partial auch mit deklariert, und jeder Teil des
untergeordneten Typs wird in einem anderen Teil des enthaltenden Typs deklariert.
Der- partial Modifizierer ist in Delegaten-oder Enumerationsdeklarationen unzulässig.
Attribute
Die Attribute eines partiellen Typs werden festgelegt, indem die Attribute der einzelnen Teile in einer nicht
angegebenen Reihenfolge kombiniert werden. Wenn ein Attribut in mehreren Teilen platziert wird, entspricht es
dem mehrfachen angeben des Attributs für den Typ. Die beiden Teile sind z. b.:

[Attr1, Attr2("hello")]
partial class A {}

[Attr3, Attr2("goodbye")]
partial class A {}

Äquivalent zu einer Deklaration, z. b.:

[Attr1, Attr2("hello"), Attr3, Attr2("goodbye")]


class A {}

Attribute für Typparameter werden auf ähnliche Weise kombiniert.


Modifizierer
Wenn eine partielle Typdeklaration eine Barrierefreiheits Spezifikation (die public protected internal
Modifizierer,, und) enthält, private muss Sie mit allen anderen Teilen übereinstimmen, die eine Barrierefreiheits
Spezifikation enthalten. Wenn kein Teil eines partiellen Typs eine Barrierefreiheits Spezifikation enthält, erhält
der Typ die entsprechende Standard Barrierefreiheit (alsBarrierefreiheit deklariert).
Wenn eine oder mehrere partielle Deklarationen eines in einem Typ geänderten Typs einen new Modifizierer
enthalten, wird keine Warnung ausgegeben, wenn der Typ eines geerbten Members (durch
Vererbungausblenden) ausgeblendet wird.
Wenn eine oder mehrere partielle Deklarationen einer Klasse einen abstract Modifizierer enthalten, gilt die
Klasse als abstrakt (abstrakte Klassen). Andernfalls gilt die Klasse als nicht abstrakt.
Wenn mindestens eine partielle Deklaration einer Klasse einen sealed Modifizierer enthält, gilt die Klasse als
versiegelt (versiegelte Klassen). Andernfalls wird die Klasse als nicht versiegelt angesehen.
Beachten Sie, dass eine Klasse nicht gleichzeitig abstrakt und versiegelt sein kann.
Wenn der- unsafe Modifizierer für eine partielle Typdeklaration verwendet wird, wird nur dieser bestimmte Teil
als unsicherer Kontext (unsichere Kontexte) betrachtet.
Typparameter und Einschränkungen
Wenn ein generischer Typ in mehreren Teilen deklariert ist, muss jeder Teil die Typparameter angeben. Jeder Teil
muss die gleiche Anzahl von Typparametern und den gleichen Namen für jeden Typparameter in der richtigen
Reihenfolge aufweisen.
Wenn eine partielle generische Typdeklaration Einschränkungen ( where Klauseln) enthält, müssen die
Einschränkungen allen anderen Teilen, die Einschränkungen einschließen, zustimmen. Insbesondere müssen alle
Teile, die Einschränkungen enthalten, Einschränkungen für denselben Satz von Typparametern aufweisen, und
für jeden Typparameter müssen die Sätze der primären, sekundären und Konstruktoreinschränkungen
gleichwertig sein. Zwei Sätze von Einschränkungen sind äquivalent, wenn Sie dieselben Member enthalten.
Wenn kein Teil eines partiellen generischen Typs Typparameter Einschränkungen angibt, gelten die
Typparameter als nicht eingeschränkt.
Das Beispiel

partial class Dictionary<K,V>


where K: IComparable<K>
where V: IKeyProvider<K>, IPersistable
{
...
}

partial class Dictionary<K,V>


where V: IPersistable, IKeyProvider<K>
where K: IComparable<K>
{
...
}

partial class Dictionary<K,V>


{
...
}

ist richtig, da diese Teile, die Einschränkungen enthalten (die ersten beiden), effektiv denselben Satz von
primären, sekundären und Konstruktoreinschränkungen für denselben Satz von Typparametern angeben.
Basisklasse
Wenn eine partielle Klassen Deklaration eine Basisklassen Spezifikation enthält, muss Sie mit allen anderen
Teilen übereinstimmen, die eine Basisklassen Spezifikation enthalten. Wenn kein Teil einer partiellen Klasse eine
Basisklassen Spezifikation enthält, wird die Basisklasse zu System.Object (Basisklassen).
Basis Schnittstellen
Der Satz von Basis Schnittstellen für einen in mehreren Teilen deklarierten Typ ist die Vereinigung der Basis
Schnittstellen, die für jeden Teil angegeben werden. Eine bestimmte Basisschnittstelle kann nur einmal pro Teil
benannt werden, es ist jedoch zulässig, dass mehrere Teile die gleichen Basis Schnittstellen benennen. Es darf
nur eine Implementierung der Member einer bestimmten Basisschnittstelle vorhanden sein.
Im Beispiel

partial class C: IA, IB {...}

partial class C: IC {...}

partial class C: IA, IB {...}

der Satz von Basis Schnittstellen für die C -Klasse ist IA , IB und IC .
In der Regel stellt jeder Teil eine Implementierung der Schnittstellen bereit, die für diesen Teil deklariert werden.
Dies ist jedoch keine Voraussetzung. Ein Teil kann die Implementierung für eine Schnittstelle bereitstellen, die für
einen anderen Teil deklariert wurde:
partial class X
{
int IComparable.CompareTo(object o) {...}
}

partial class X: IComparable


{
...
}

Member
Mit Ausnahme von partiellen Methoden (partielle Methoden) ist der Satz von Membern eines Typs, der in
mehreren Teilen deklariert wurde, einfach die Vereinigung der Menge von Membern, die in jedem Teil deklariert
werden. Die Texte aller Teile der Typdeklaration verwenden denselben Deklarations Bereich (Deklarationen), und
derGültigkeitsBereich der einzelnen Member (Bereiche) erstreckt sich auf den Text aller Teile. Die Zugriffs
Domäne eines beliebigen Members enthält immer alle Teile des einschließenden Typs. ein private Member, der
in einem Teil deklariert ist, kann aus einem anderen Teil frei zugänglich sein. Es handelt sich um einen
Kompilierzeitfehler, um denselben Member in mehr als einem Teil des Typs zu deklarieren, es sei denn, dieser
Member ist ein Typ mit dem- partial Modifizierer.

partial class A
{
int x; // Error, cannot declare x more than once

partial class Inner // Ok, Inner is a partial type


{
int y;
}
}

partial class A
{
int x; // Error, cannot declare x more than once

partial class Inner // Ok, Inner is a partial type


{
int z;
}
}

Die Reihenfolge von Membern innerhalb eines Typs ist nur selten für c#-Code wichtig, kann jedoch bei der
Schnittstelle mit anderen Sprachen und Umgebungen von Bedeutung sein. In diesen Fällen ist die Reihenfolge
von Membern innerhalb eines in mehreren Teilen deklarierten Typs nicht definiert.
Partielle Methoden
Partielle Methoden können in einem Teil einer Typdeklaration definiert und in einem anderen implementiert
werden. Die Implementierung ist optional. Wenn kein Teil die partielle Methode implementiert, werden die
Deklaration der partiellen Methode und alle Aufrufe an die Deklaration aus der Typdeklaration entfernt, die sich
aus der Kombination der Teile ergibt.
Partielle Methoden können keine Zugriffsmodifizierer definieren, sondern implizit private . Der Rückgabetyp
muss sein void , und ihre Parameter dürfen nicht den- out Modifizierer aufweisen. Der Bezeichner partial
wird in einer Methoden Deklaration nur dann als sonderschlüsselwort erkannt, wenn er direkt vor dem Typ
angezeigt wird void . andernfalls kann er als normaler Bezeichner verwendet werden. Eine partielle Methode
kann Schnittstellen Methoden nicht explizit implementieren.
Es gibt zwei Arten von partiellen Methoden Deklarationen: Wenn der Text der Methoden Deklaration ein
Semikolon ist, wird die Deklaration als *definierende par tielle Methoden Deklaration _ bezeichnet. Wenn
der Text als _block * angegeben wird, wird die Deklaration als eine implementierende par tielle Methoden
Deklaration bezeichnet. In den Teilen einer Typdeklaration darf nur eine partielle Methoden Deklaration mit
einer bestimmten Signatur definiert werden, und es kann nur eine partielle Methoden Deklaration mit einer
bestimmten Signatur implementiert werden. Wenn eine implementierende partielle Methoden Deklaration
angegeben wird, muss eine entsprechende definierende partielle Methoden Deklaration vorhanden sein, und
die Deklarationen müssen übereinstimmen, wie im folgenden angegeben:
Die Deklarationen müssen die gleichen modifiziererer (auch nicht unbedingt in derselben Reihenfolge), den
Methodennamen, die Anzahl der Typparameter und die Anzahl von Parametern aufweisen.
Die entsprechenden Parameter in den Deklarationen müssen dieselben Modifizierer aufweisen (obwohl Sie
nicht notwendigerweise in derselben Reihenfolge sind) und dieselben Typen (Modulo-Unterschiede in
Typparameter Namen).
Die entsprechenden Typparameter in den Deklarationen müssen dieselben Einschränkungen aufweisen
(Modulo-Unterschiede in Typparameter Namen).
Eine implementierende partielle Methoden Deklaration kann im gleichen Teil wie die entsprechende
definierende partielle Methoden Deklaration vorkommen.
Nur eine definierende partielle Methode ist an der Überladungs Auflösung beteiligt. Unabhängig davon, ob eine
implementierende Deklaration angegeben wird, können Aufruf Ausdrücke in Aufrufe der partiellen Methode
aufgelöst werden. Da eine partielle Methode immer zurückgibt void , sind solche Aufruf Ausdrücke immer
Ausdrucks Anweisungen. Da eine partielle Methode implizit ist private , treten diese Anweisungen immer
innerhalb eines der Teile der Typdeklaration auf, in der die partielle Methode deklariert ist.
Wenn kein Teil einer partiellen Typdeklaration eine implementierende Deklaration für eine bestimmte partielle
Methode enthält, wird jede Ausdrucks Anweisung, die Sie aufruft, einfach aus der kombinierten Typdeklaration
entfernt. Folglich hat der Aufruf Ausdruck, einschließlich aller konstituierender Ausdrücke, keine Auswirkung zur
Laufzeit. Die partielle Methode selbst wird ebenfalls entfernt und ist kein Member der kombinierten
Typdeklaration.
Wenn eine implementierende Deklaration für eine bestimmte partielle Methode vorhanden ist, werden die
Aufrufe der partiellen Methoden beibehalten. Die partielle Methode führt zu einer Methoden Deklaration, die
der Implementierung der partiellen Methoden Deklaration ähnelt, mit Ausnahme folgender:
Der- partial Modifizierer ist nicht eingeschlossen.
Die Attribute in der resultierenden Methoden Deklaration sind die kombinierten Attribute der definierenden
und der implementierenden partiellen Methoden Deklaration in einer nicht angegebenen Reihenfolge.
Duplikate werden nicht entfernt.
Die Attribute für die Parameter der resultierenden Methoden Deklaration sind die kombinierten Attribute der
entsprechenden Parameter der definierenden und der implementierenden partiellen Methoden Deklaration
in einer nicht angegebenen Reihenfolge. Duplikate werden nicht entfernt.
Wenn eine definierende Deklaration, aber keine implementierende Deklaration für eine partielle Methode M
angegeben wird, gelten die folgenden Einschränkungen:
Es handelt sich um einen Kompilierzeitfehler zum Erstellen eines Delegaten für die Methode
(delegaterstellungs-Ausdrücke).
Es handelt sich um einen Kompilierzeitfehler, auf den in M einer anonymen Funktion verwiesen wird, die in
einen Ausdrucks Strukturtyp konvertiert wird (Auswertung anonymer Funktions Konvertierungen in
Ausdrucks Baumstruktur Typen).
Ausdrücke, die als Teil des aufzurufenden von auftreten M , wirken sich nicht auf den eindeutigen
Zuweisungs Zustand (definitive Zuweisung) aus, der potenziell zu Kompilier Zeitfehlern führen kann.
M kann nicht der Einstiegspunkt für eine Anwendung (Anwendungsstart) sein.

Partielle Methoden sind hilfreich, um einem Teil einer Typdeklaration das Anpassen des Verhaltens eines
anderen Teils zu ermöglichen, z. b. eines, das von einem Tool generiert wird. Beachten Sie die folgende
Deklaration der partiellen Klasse:

partial class Customer


{
string name;

public string Name {


get { return name; }
set {
OnNameChanging(value);
name = value;
OnNameChanged();
}

partial void OnNameChanging(string newName);

partial void OnNameChanged();


}

Wenn diese Klasse ohne andere Teile kompiliert wird, werden die definierenden partiellen Methoden
Deklarationen und deren Aufrufe entfernt, und die resultierende kombinierte Klassen Deklaration entspricht
Folgendem:

class Customer
{
string name;

public string Name {


get { return name; }
set { name = value; }
}
}

Angenommen, es wird jedoch ein anderer Teil angegeben, der die Implementierung von Deklarationen der
partiellen Methoden bereitstellt:

partial class Customer


{
partial void OnNameChanging(string newName)
{
Console.WriteLine("Changing " + name + " to " + newName);
}

partial void OnNameChanged()


{
Console.WriteLine("Changed to " + name);
}
}

Dann entspricht die resultierende kombinierte Klassen Deklaration folgendem:


class Customer
{
string name;

public string Name {


get { return name; }
set {
OnNameChanging(value);
name = value;
OnNameChanged();
}

void OnNameChanging(string newName)


{
Console.WriteLine("Changing " + name + " to " + newName);
}

void OnNameChanged()
{
Console.WriteLine("Changed to " + name);
}
}

Namens Bindung
Obwohl jeder Teil eines erweiterbaren Typs innerhalb desselben Namespace deklariert werden muss, werden
die Teile in der Regel in verschiedenen Namespace Deklarationen geschrieben. Daher using können für jeden
Teil verschiedene Direktiven (using-Direktiven) vorhanden sein. Bei der Interpretation von einfachen Namen
(Typrückschluss) innerhalb eines Teils using werden nur die Direktiven der Namespace Deklaration (en)
berücksichtigt, die diesen Teil einschließen. Dies kann dazu führen, dass derselbe Bezeichner in unterschiedlichen
Teilen mit unterschiedlichen Bedeutungen übereinstimmen:

namespace N
{
using List = System.Collections.ArrayList;

partial class A
{
List x; // x has type System.Collections.ArrayList
}
}

namespace N
{
using List = Widgets.LinkedList;

partial class A
{
List y; // y has type Widgets.LinkedList
}
}

Klassenmember
Die Member einer Klasse bestehen aus den Membern, die von den class_member_declaration en und den von
der direkten Basisklasse geerbten Membern eingeführt wurden.
class_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| destructor_declaration
| static_constructor_declaration
| type_declaration
;

Die Member eines Klassen Typs sind in die folgenden Kategorien unterteilt:
Konstanten, die Konstante Werte darstellen, die der-Klasse (Konstanten) zugeordnet sind.
Felder, bei denen es sich um die Variablen der-Klasse (Felder) handelt.
-Methoden, die die Berechnungen und Aktionen implementieren, die von der-Klasse ausgeführt werden
können (-Methoden).
Eigenschaften, die benannte Merkmale und die Aktionen definieren, die mit dem Lesen und Schreiben dieser
Eigenschaften verknüpft sind (Eigenschaften).
Ereignisse, die Benachrichtigungen definieren, die von der-Klasse (Ereignissen) generiert werden können.
Indexer, die es ermöglichen, Instanzen der Klasse auf die gleiche Weise (syntaktisch) als Arrays (Indexer) zu
indizieren.
Operatoren, die die Ausdrucks Operatoren definieren, die auf Instanzen der-Klasse (Operatoren) angewendet
werden können.
Instanzkonstruktoren, die die zum Initialisieren von Instanzen der-Klasse erforderlichen Aktionen
implementieren (Instanzkonstruktoren).
Dekonstruktoren, die die auszuführenden Aktionen implementieren, bevor Instanzen der Klasse dauerhaft
verworfen werden (Dekonstruktoren).
Statische Konstruktoren, die die zum Initialisieren der Klasse selbst erforderlichen Aktionen implementieren
(statische Konstruktoren).
Typen, die die Typen darstellen, die für die-Klasse lokal sind (unterTypen).
Member, die ausführbaren Code enthalten können, werden zusammen mit den Funktionsmembern des Klassen
Typs bezeichnet. Die Funktionsmember eines Klassen Typs sind die Methoden, Eigenschaften, Ereignisse, Indexer,
Operatoren, Instanzkonstruktoren, destrukturtoren und statischen Konstruktoren dieses Klassen Typs.
Ein class_declaration erstellt einen neuen Deklarations Raum (Deklarationen), und die class_member_declaration
s, die sofort im class_declaration enthalten sind, stellen neue Member in diesen Deklarations Bereich ein. Die
folgenden Regeln gelten für class_member_declaration s:
Instanzkonstruktoren, Dekonstruktoren und statische Konstruktoren müssen den gleichen Namen wie die
unmittelbar einschließende Klasse haben. Alle anderen Member müssen Namen haben, die sich von dem
Namen der unmittelbar einschließenden Klasse unterscheiden.
Der Name einer Konstante, eines Felds, einer Eigenschaft, eines Ereignisses oder eines Typs muss sich von
den Namen aller anderen Member unterscheiden, die in derselben Klasse deklariert sind.
Der Name einer Methode muss sich von den Namen aller anderen nicht-Methoden unterscheiden, die in
derselben Klasse deklariert sind. Außerdem müssen sich die Signatur (Signaturen und überladen) einer
Methode von den Signaturen aller anderen Methoden unterscheiden, die in derselben Klasse deklariert sind,
und zwei Methoden, die in derselben Klasse deklariert sind, dürfen keine Signaturen aufweisen, die sich
ausschließlich durch und unterscheiden ref out .
Die Signatur eines Instanzkonstruktors muss sich von den Signaturen aller anderen Instanzkonstruktoren
unterscheiden, die in derselben Klasse deklariert sind, und zwei in derselben Klasse deklarierte Konstruktoren
verfügen möglicherweise nicht über Signaturen, die sich ausschließlich durch und unterscheiden ref out .
Die Signatur eines Indexer muss sich von den Signaturen aller anderen Indexer unterscheiden, die in
derselben Klasse deklariert sind.
Die Signatur eines Operators muss sich von den Signaturen aller anderen Operatoren unterscheiden, die in
derselben Klasse deklariert sind.
Die geerbten Member eines Klassen Typs (Vererbung) sind nicht Teil des Deklarations Raums einer Klasse.
Folglich kann eine abgeleitete Klasse einen Member mit demselben Namen oder derselben Signatur wie ein
geerbten Member deklarieren (wodurch der geerbte Member in der Tat ausgeblendet wird).
Der Instanztyp.
Jede Klassen Deklaration verfügt über einen zugeordneten gebundenen Typ (gebundene und ungebundene
Typen), den Instanztyp . Bei einer generischen Klassen Deklaration wird der Instanztyp durch Erstellen eines
konstruierten Typs (konstruierte Typen) aus der Typdeklaration gebildet, wobei jedes der angegebenen
Typargumente der entsprechende Typparameter ist. Da der Instanztyp die Typparameter verwendet, kann er nur
dort verwendet werden, wo sich die Typparameter im Gültigkeitsbereich befinden. Das heißt, innerhalb der
Klassen Deklaration. Der Instanztyp ist der Typ von this für Code, der innerhalb der Klassen Deklaration
geschrieben wurde. Bei nicht generischen Klassen ist der Instanztyp einfach die deklarierte Klasse. Das folgende
Beispiel zeigt mehrere Klassen Deklarationen zusammen mit ihren Instanztypen:

class A<T> // instance type: A<T>


{
class B {} // instance type: A<T>.B
class C<U> {} // instance type: A<T>.C<U>
}

class D {} // instance type: D

Member konstruierter Typen


Die nicht geerbten Member eines konstruierten Typs werden abgerufen, indem für jede type_parameter in der
Element Deklaration der entsprechende type_argument des konstruierten Typs ersetzt wird. Der Ersetzungs
Vorgang basiert auf der semantischen Bedeutung von Typdeklarationen und ist nicht einfach die Text Ersetzung.
Beispielsweise bei der Deklaration der generischen Klasse

class Gen<T,U>
{
public T[,] a;
public void G(int i, T t, Gen<U,T> gt) {...}
public U Prop { get {...} set {...} }
public int H(double d) {...}
}

der konstruierte Typ Gen<int[],IComparable<string>> verfügt über die folgenden Member:

public int[,][] a;
public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...}
public IComparable<string> Prop { get {...} set {...} }
public int H(double d) {...}

Der Typ des Members a in der generischen Klassen Deklaration Gen ist das zweidimensionale Array von T ,
sodass der Typ des Members a im konstruierten Typ oben "zweidimensionales Array eines eindimensionalen
Arrays von int ", oder ist int[,][] .
Innerhalb von instanzfunktionsmembern ist der Typ von this der Instanztyp (der Instanztyp) der enthaltenden
Deklaration.
Alle Member einer generischen Klasse können Typparameter aus einer beliebigen einschließenden Klasse
verwenden, entweder direkt oder als Teil eines konstruierten Typs. Wenn ein bestimmter, von einem Typ
geschlossenes konstruierter Typ (Open-und Closed-Typen) zur Laufzeit verwendet wird, wird jede Verwendung
eines Typparameters durch das tatsächliche Typargument ersetzt, das für den konstruierten Typ angegeben
wird. Beispiel:

class C<V>
{
public V f1;
public C<V> f2 = null;

public C(V x) {
this.f1 = x;
this.f2 = this;
}
}

class Application
{
static void Main() {
C<int> x1 = new C<int>(1);
Console.WriteLine(x1.f1); // Prints 1

C<double> x2 = new C<double>(3.1415);


Console.WriteLine(x2.f1); // Prints 3.1415
}
}

Vererbung
Eine Klasse erbt die Member ihres direkten Basisklassen Typs. Vererbung bedeutet, dass eine Klasse implizit alle
Member ihres direkten Basisklassen Typs enthält, mit Ausnahme der Instanzkonstruktoren, Dekonstruktoren
und statischer Konstruktoren der Basisklasse. Einige wichtige Aspekte der Vererbung sind:
Vererbung ist transitiv. Wenn C von abgeleitet ist B und B von abgeleitet ist A , C erbt die in
deklarierten Member B sowie die in deklarierten Member A .
Eine abgeleitete Klasse erweitert die direkte Basisklasse. Eine abgeleitete Klasse kann den geerbten Membern
neue Member hinzufügen, aber die Definition eines geerbten Members kann nicht entfernt werden.
Instanzkonstruktoren, destrukturtoren und statische Konstruktoren werden nicht geerbt, aber alle anderen
Member sind, unabhängig von ihrer deklarierten Barrierefreiheit (Member Access). Abhängig von der
deklarierten Barrierefreiheit sind vererbte Member jedoch möglicherweise in einer abgeleiteten Klasse nicht
zugänglich.
Eine abgeleitete Klasse kann vererbte Member ausblenden (durch Vererbung Ausblenden ), indem neue
Member mit demselben Namen oder derselben Signatur deklariert werden. Beachten Sie jedoch, dass beim
Ausblenden eines geerbten Members dieser Member nicht entfernt wird – er macht diesen Member lediglich
direkt über die abgeleitete Klasse zugänglich.
Eine Instanz einer Klasse enthält einen Satz aller Instanzfelder, die in der-Klasse und den zugehörigen
Basisklassen deklariert sind, und eine implizite Konvertierung (implizite Verweis Konvertierungen) ist von
einem abgeleiteten Klassentyp zu einem der zugehörigen Basisklassen Typen vorhanden. Folglich kann ein
Verweis auf eine Instanz einer abgeleiteten Klasse als Verweis auf eine Instanz der zugehörigen Basisklassen
behandelt werden.
Eine Klasse kann virtuelle Methoden, Eigenschaften und Indexer deklarieren, und abgeleitete Klassen können
die Implementierung dieser Funktionsmember überschreiben. Dadurch können Klassen polymorphes
Verhalten darstellen, wobei die von einem Funktionselement Aufruf ausgeführten Aktionen je nach Lauf
Zeittyp der Instanz variieren, durch die der Funktions Member aufgerufen wird.
Der geerbte Member eines konstruierten Klassen Typs sind die Member des unmittelbaren Basisklassen Typs
(Basisklassen), die gefunden werden, indem die Typargumente des konstruierten Typs für jedes Vorkommen der
entsprechenden Typparameter in der class_base Spezifikation ersetzt werden. Diese Member werden wiederum
transformiert, indem für jede type_parameter in der Element Deklaration der entsprechende type_argument der
class_base Spezifikation ersetzt wird.

class B<U>
{
public U F(long index) {...}
}

class D<T>: B<T[]>


{
public T G(string s) {...}
}

Im obigen Beispiel hat der konstruierte Typ D<int> einen nicht geerbten Member, der public int G(string s)
durch das Typargument int für den Typparameter abgerufen wurde T . D<int> verfügt auch über einen
geerbten Member aus der Klassen Deklaration B . Dieser geerbte Member wird bestimmt, indem zuerst der
Basis Klassentyp bestimmt wird, B<int[]> D<int> indem int T in der Basisklassen Spezifikation ersetzt wird
B<T[]> . Anschließend wird als Typargument B int[] für in durch ersetzt U public U F(long index) ,
wodurch der geerbte Member bereitstellt wird public int[] F(long index) .
Der New-Modifizierer.
Ein class_member_declaration darf einen Member mit demselben Namen oder derselben Signatur wie ein
geerbte Member deklarieren. In diesem Fall wird der Member der abgeleiteten Klasse zum Ausblenden des
Basisklassenmembers bezeichnet. Das Ausblenden eines geerbten Members wird nicht als Fehler betrachtet,
bewirkt jedoch, dass der Compiler eine Warnung ausgibt. Um die Warnung zu unterdrücken, kann die
Deklaration des Members der abgeleiteten Klasse einen- new Modifizierer einschließen, um anzugeben, dass
der abgeleitete Member den Basismember ausblenden soll. Dieses Thema wird unter Ausblenden durch
Vererbungausführlicher erläutert.
Wenn ein new Modifizierer in einer Deklaration enthalten ist, die einen geerbten Member nicht verbirgt, wird
eine Warnung für diesen Effekt ausgegeben. Diese Warnung wird durch Entfernen des- new Modifizierers
unterdrückt.
Zugriffsmodifizierer
Eine class_member_declaration kann eine der fünf möglichen Arten von deklarierten zugreif barkeit (deklariert
als Barrierefreiheit) aufweisen: public , protected internal , protected , internal oder private . Mit
Ausnahme der protected internal Kombination ist es ein Kompilierzeitfehler, mehr als einen
Zugriffsmodifizierer anzugeben. Wenn eine class_member_declaration keine Zugriffsmodifizierer einschließt,
private wird angenommen.

Konstituierende Typen
Typen, die in der Deklaration eines Members verwendet werden, werden als konstituierende Typen dieses
Members bezeichnet. Mögliche Typen sind der Typ einer Konstante, eines Felds, einer Eigenschaft, eines
Ereignisses oder eines Indexers, der Rückgabetyp einer Methode oder eines Operators und die Parametertypen
einer Methode, eines Indexers, eines Operators oder eines Instanzkonstruktors. Die einzelnen Typen eines
Members müssen mindestens so zugänglich sein, wie der Member selbst (BarrierefreiheitsEinschränkungen).
Statische Member und Instanzmember
Member einer Klasse sind entweder *statische Member _ oder _ Instanzmember *. Im Allgemeinen ist es
sinnvoll, statische Member als zu den Klassentypen und Instanzmembern gehörend zu betrachten (Instanzen
von Klassentypen).
Wenn ein Feld, eine Methode, eine Eigenschaft, eine Ereignis-, Operator-oder Konstruktordeklaration einen-
static Modifizierer enthält, wird ein statischer Member deklariert. Außerdem deklariert eine Konstante oder
Typdeklaration implizit einen statischen Member. Statische Member haben die folgenden Merkmale:
Wenn auf einen statischen Member M in einer member_access (Element Zugriff) des Formulars verwiesen
wird E.M , E muss einen Typ angeben, der enthält M . Es handelt sich um einen Kompilierzeitfehler E , mit
dem eine Instanz bezeichnet wird.
Ein statisches Feld identifiziert genau einen Speicherort, der von allen Instanzen eines gegebenen
geschlossenen Klassen Typs freigegeben werden soll. Unabhängig davon, wie viele Instanzen eines
bestimmten geschlossenen Klassen Typs erstellt werden, gibt es nur eine Kopie eines statischen Felds.
Ein statisches Funktionsmember (Methode, Eigenschaft, Ereignis, Operator oder Konstruktor) funktioniert
nicht für eine bestimmte Instanz, und es handelt sich um einen Kompilierzeitfehler, auf den this in einem
derartigen Funktionsmember verwiesen wird.
Wenn ein Feld, eine Methode, eine Eigenschaft, ein Ereignis, ein Indexer, ein Konstruktor oder eine
destrukturerdeklaration keinen static Modifizierer enthält, wird ein Instanzmember deklariert. (Ein
Instanzmember wird manchmal als nicht statischer Member bezeichnet.) Instanzmember haben die folgenden
Merkmale:
Wenn auf einen Instanzmember M in einem member_access (Element Zugriff) des Formulars verwiesen
wird E.M , E muss eine Instanz eines Typs angeben, der enthält M . Es handelt sich um einen Bindungs Zeit
Fehler E , mit dem ein Typ bezeichnet wird.
Jede Instanz einer Klasse enthält einen separaten Satz aller Instanzfelder der Klasse.
Ein Instanzfunktionsmember (Methode, Eigenschaft, Indexer, Instanzkonstruktor oder Dekonstruktor) arbeitet
auf einer bestimmten Instanz der Klasse, und auf diese Instanz kann als this (dieser Zugriff) zugegriffen
werden.
Das folgende Beispiel veranschaulicht die Regeln für den Zugriff auf statische Member und Instanzmember:

class Test
{
int x;
static int y;

void F() {
x = 1; // Ok, same as this.x = 1
y = 1; // Ok, same as Test.y = 1
}

static void G() {


x = 1; // Error, cannot access this.x
y = 1; // Ok, same as Test.y = 1
}

static void Main() {


Test t = new Test();
t.x = 1; // Ok
t.y = 1; // Error, cannot access static member through instance
Test.x = 1; // Error, cannot access instance member through type
Test.y = 1; // Ok
}
}

Die- F Methode zeigt, dass in einem Instanzfunktionsmember eine Simple_name (einfache Namen) verwendet
werden kann, um auf Instanzmember und statische Member zuzugreifen. Die- G Methode zeigt, dass es sich
bei einem statischen Funktionsmember um einen Kompilierzeitfehler handelt, der über eine Simple_name auf
einen Instanzmember zugreifen kann. Die- Main Methode zeigt, dass in einem member_access (Element
Zugriff) auf Instanzmemberüber-Instanzen zugegriffen werden muss, und auf statische Member muss über-
Typen zugegriffen werden.
Geschachtelte Typen
Ein in einer Klassen-oder Struktur Deklaration deklarierter Typ wird als * geschachtelter Typ _ bezeichnet. Ein
Typ, der in einer Kompilierungseinheit oder einem Namespace deklariert wird, wird als _ nicht geschachtelter
Typ * bezeichnet.
Im Beispiel

using System;

class A
{
class B
{
static void F() {
Console.WriteLine("A.B.F");
}
}
}

die Klasse ist ein geschachtelter B Typ, da Sie innerhalb der Klasse deklariert wird A und die Klasse A ein
nicht geschachtelter Typ ist, da Sie innerhalb einer Kompilierungseinheit deklariert wird.
Vollqualifizierter Name
Der voll qualifizierte Name (vollgekennzeichnete Namen) für einen Typ ist S.N , wobei S der voll qualifizierte
Name des Typs ist, in dem der Typ N deklariert ist.
Deklarierter Zugriff
Nicht--nicht-------typtypen können public Barrierefreiheit haben oder internal deklarieren und sind
internal standardmäßig als In Form von untergeordneten Typen können auch diese Formen der deklarierten
Barrierefreiheit sowie eine oder mehrere zusätzliche Formen der deklarierten Barrierefreiheit enthalten sein, je
nachdem, ob der enthaltende Typ eine Klasse oder Struktur ist:
Ein in einer Klasse deklarierter, in einer Klasse deklarierter Typ kann über eine beliebige von fünf Formen der
deklarierten Barrierefreiheit ( public ,, protected internal protected , internal oder) verfügen, private
und wie bei anderen Klassenmembern wird standardmäßig die private deklarierte Barrierefreiheit
verwendet.
Ein in einer Struktur deklarierter, in einer Struktur deklarierter Typ kann über eine von drei Formen der
deklarierten Barrierefreiheit ( public , oder) verfügen, und wie bei anderen Strukturmembern internal
private werden standardmäßig private deklarierte Zugriffsmöglichkeiten verwendet.

Das Beispiel
public class List
{
// Private data structure
private class Node
{
public object Data;
public Node Next;

public Node(object data, Node next) {


this.Data = data;
this.Next = next;
}
}

private Node first = null;


private Node last = null;

// Public interface
public void AddToFront(object o) {...}
public void AddToBack(object o) {...}
public object RemoveFromFront() {...}
public object RemoveFromBack() {...}
public int Count { get {...} }
}

deklariert eine private, in einer Klasse Node .


Zieher
Ein-Typ kann einen Basismember ausblenden (Nameausblenden). Der- new Modifizierer ist für Klassentyp
Deklarationen zulässig, damit das Ausblenden explizit ausgedrückt werden kann. Das Beispiel

using System;

class Base
{
public static void M() {
Console.WriteLine("Base.M");
}
}

class Derived: Base


{
new public class M
{
public static void F() {
Console.WriteLine("Derived.M.F");
}
}
}

class Test
{
static void Main() {
Derived.M.F();
}
}

zeigt eine M in der Liste definierte Klasse, die die M in definierte Methode verbirgt Base .
Dieser Zugriff
Ein-Typ und der enthaltende Typ haben keine besondere Beziehung hinsichtlich this_access (dieser Zugriff).
Insbesondere innerhalb eines geschachtelten this Typs kann nicht verwendet werden, um auf Instanzmember
des enthaltenden Typs zu verweisen. In Fällen, in denen ein geclusterter Typ Zugriff auf die Instanzmember
seines enthaltenden Typs benötigt, kann der Zugriff bereitgestellt werden, indem der this für die Instanz des
enthaltenden Typs als Konstruktorargument für den schsted Type bereitgestellt wird. Beispiel:

using System;

class C
{
int i = 123;

public void F() {


Nested n = new Nested(this);
n.G();
}

public class Nested


{
C this_c;

public Nested(C c) {
this_c = c;
}

public void G() {


Console.WriteLine(this_c.i);
}
}
}

class Test
{
static void Main() {
C c = new C();
c.F();
}
}

zeigt diese Methode. Eine Instanz von C erstellt eine Instanz von Nested und übergibt ihren eigenen this an
Nested den-Konstruktor, um den nachfolgenden Zugriff auf C die Instanzmember bereitzustellen.

Zugriff auf private und geschützte Member des enthaltenden Typs


Ein Typ, der einen Typ aufweist, kann auf alle Member zugreifen, auf die der enthaltende Typ zugreifen kann,
einschließlich der Member des enthaltenden Typs, die über die Berechtigung "Barrierefreiheit" verfügen
private protected . Das Beispiel
using System;

class C
{
private static void F() {
Console.WriteLine("C.F");
}

public class Nested


{
public static void G() {
F();
}
}
}

class Test
{
static void Main() {
C.Nested.G();
}
}

zeigt eine Klasse an C , die eine-Klasse enthält Nested . In Nested G Ruft die-Methode die statische Methode
F auf, die in definiert C ist, und F verfügt über eine private deklarierte Barrierefreiheit.

Ein-Typ kann auch auf geschützte Member zugreifen, die in einem Basistyp seines enthaltenden Typs definiert
sind. Im Beispiel

using System;

class Base
{
protected void F() {
Console.WriteLine("Base.F");
}
}

class Derived: Base


{
public class Nested
{
public void G() {
Derived d = new Derived();
d.F(); // ok
}
}
}

class Test
{
static void Main() {
Derived.Nested n = new Derived.Nested();
n.G();
}
}

die Derived.Nested schsted Class greift auf die geschützte Methode zu, die F in Derived der Basisklasse von
definiert ist, Base indem Sie durch eine Instanz von aufruft Derived .
Typen in generischen Klassen in generischen Klassen
Eine generische Klassen Deklaration kann eine Typdeklaration für einen Typ enthalten. Die Typparameter der
einschließenden Klasse können innerhalb der geschachtelten Typen verwendet werden. Eine Typdeklaration in
einem Typ kann zusätzliche Typparameter enthalten, die nur für den Typ "nsted" gelten.
Jede in einer generischen Klassen Deklaration enthaltene Typdeklaration ist implizit eine generische
Typdeklaration. Beim Schreiben eines Verweises auf einen Typ, der in einem generischen Typ geschachtelt ist,
muss der enthaltende konstruierte Typ, einschließlich der Typargumente, benannt werden. Allerdings kann der
geschachtelte Typ innerhalb der äußeren Klasse ohne Qualifizierung verwendet werden. der Instanztyp der
äußeren Klasse kann beim Konstruieren des-Typs implizit verwendet werden. Im folgenden Beispiel werden drei
verschiedene korrekte Möglichkeiten zum Verweisen auf einen konstruierten Typ gezeigt, der aus erstellt wurde
Inner . die ersten beiden sind äquivalent:

class Outer<T>
{
class Inner<U>
{
public static void F(T t, U u) {...}
}

static void F(T t) {


Outer<T>.Inner<string>.F(t, "abc"); // These two statements have
Inner<string>.F(t, "abc"); // the same effect

Outer<int>.Inner<string>.F(3, "abc"); // This type is different

Outer.Inner<string>.F(t, "abc"); // Error, Outer needs type arg


}
}

Obwohl es sich um einen ungültigen Programmierstil handelt, kann ein Typparameter in einem Schraffurtyp
einen Member oder Typparameter ausblenden, der im äußeren Typ deklariert ist:

class Outer<T>
{
class Inner<T> // Valid, hides Outer's T
{
public T t; // Refers to Inner's T
}
}

Reservierte Elementnamen
Um die zugrunde liegende c#-Lauf Zeit Implementierung zu vereinfachen, muss die Implementierung für jede
Deklaration des Quell Members, die eine Eigenschaft, ein Ereignis oder einen Indexer ist, zwei Methoden
Signaturen reservieren, basierend auf der Art der Element Deklaration, dem Namen und dem Typ. Es ist ein
Kompilierzeitfehler für ein Programm, einen Member zu deklarieren, dessen Signatur mit einer der reservierten
Signaturen übereinstimmt, auch wenn die zugrunde liegende Lauf Zeit Implementierung diese Reservierungen
nicht verwendet.
Die reservierten Namen führen keine Deklarationen ein, sodass Sie nicht an der Member-Suche beteiligt sind.
Die zugeordneten reservierten Methoden Signaturen einer Deklaration nehmen jedoch an der Vererbung
(Vererbung) Teil und können mit dem new Modifizierer (dem neuen Modifizierer) ausgeblendet werden.
Die Reservierung dieser Namen dient drei Zwecken:
Damit die zugrunde liegende Implementierung einen normalen Bezeichner als Methodennamen für den Get-
oder Set-Zugriff auf die c#-Sprachfunktion verwendet.
Damit andere Sprachen mithilfe eines normalen Bezeichners als Methodenname interagieren können, um
den Zugriff auf die c#-Sprachfunktion zu erhalten oder festzulegen.
Um sicherzustellen, dass die von einem konformen Compiler akzeptierte Quelle von einer anderen akzeptiert
wird, indem die Besonderheiten von reservierten Elementnamen in allen c#-Implementierungen einheitlich
sind.
Die Deklaration eines destrukturtors(destrukturatoren) bewirkt auch, dass eine Signatur reserviert wird (für
destrukturtoren reservierte Elementnamen).
Für Eigenschaften reservierte Elementnamen
Für eine Eigenschaft P (Eigenschaften) vom Typ T sind die folgenden Signaturen reserviert:

T get_P();
void set_P(T value);

Beide Signaturen sind reserviert, auch wenn die Eigenschaft schreibgeschützt oder schreibgeschützt ist.
Im Beispiel

using System;

class A
{
public int P {
get { return 123; }
}
}

class B: A
{
new public int get_P() {
return 456;
}

new public void set_P(int value) {


}
}

class Test
{
static void Main() {
B b = new B();
A a = b;
Console.WriteLine(a.P);
Console.WriteLine(b.P);
Console.WriteLine(b.get_P());
}
}

eine Klasse A definiert eine schreibgeschützte Eigenschaft P und reserviert somit Signaturen für get_P -und-
set_P Methoden. Eine-Klasse wird B von abgeleitet A und verbirgt beide reservierten Signaturen. Das
Beispiel erzeugt die Ausgabe:

123
123
456

Für Ereignisse reservierte Elementnamen


Für ein Ereignis E (Ereignisse) des Delegattyps T sind die folgenden Signaturen reserviert:

void add_E(T handler);


void remove_E(T handler);
Für Indexer reservierte Elementnamen
Für einen Indexer (Indexer) vom Typ T mit Parameter-List L sind die folgenden Signaturen reserviert:

T get_Item(L);
void set_Item(L, T value);

Beide Signaturen sind reserviert, auch wenn der Indexer schreibgeschützt oder schreibgeschützt ist.
Außerdem ist der Elementname Item reserviert.
Für destrukturtoren reservierte Elementnamen
Für eine Klasse, die einen Dekonstruktor (Dekonstruktoren) enthält, ist die folgende Signatur reserviert:

void Finalize();

Konstanten
Ein *Constant _ ist ein Klassenmember, der einen konstanten Wert darstellt: einen Wert, der zur Kompilierzeit
berechnet werden kann. Eine _constant_declaration * führt eine oder mehrere Konstanten eines bestimmten
Typs ein.

constant_declaration
: attributes? constant_modifier* 'const' type constant_declarators ';'
;

constant_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;

constant_declarators
: constant_declarator (',' constant_declarator)*
;

constant_declarator
: identifier '=' constant_expression
;

Eine constant_declaration kann einen Satz von Attributen (Attribute), einen new Modifizierer (den neuen
Modifizierer) und eine gültige Kombination der vier Zugriffsmodifizierer (Zugriffsmodifizierer) enthalten. Die
Attribute und Modifizierer gelten für alle vom constant_declaration deklarierten Member. Obwohl Konstanten
als statische Member angesehen werden, ist ein constant_declaration weder noch einen static Modifizierer
erforderlich. Es ist ein Fehler, dass derselbe Modifizierer mehrmals in einer Konstanten Deklaration angezeigt
wird.
Der Typ einer constant_declaration der den Typ der Member angibt, die von der Deklaration eingeführt wurden.
Auf den Typ folgt eine Liste von constant_declarator en, von denen jeder einen neuen Member einführt. Ein
constant_declarator besteht aus einem Bezeichner , der den Member benennt, gefolgt von einem " = "-Token,
gefolgt von einem constant_expression (Konstantenausdrücken), der den Wert des Members übergibt.
Der in einer Konstanten Deklaration angegebene Typ muss sbyte , byte , short , ushort , int , uint ,
long , ulong , char , float , double , decimal , bool , string , ein enum_type oder eine reference_type
sein. Jede constant_expression muss einen Wert des Zieltyps oder eines Typs liefern, der durch eine implizite
Konvertierung (implizite Konvertierungen) in den Zieltyp konvertiert werden kann.
Der Typ einer Konstanten muss mindestens so zugänglich sein, wie die Konstante selbst (Barrierefreiheits
Einschränkungen).
Der Wert einer Konstanten wird in einem Ausdruck mithilfe eines Simple_name (einfache Namen) oder eines
member_access (Member Access) abgerufen.
Eine Konstante kann an einer constant_expression teilnehmen. Daher kann eine Konstante in jedem Konstrukt
verwendet werden, das eine constant_expression erfordert. Beispiele für derartige Konstrukte sind case
Bezeichnungen, goto case Anweisungen, Element enum Deklarationen, Attribute und andere Konstante
Deklarationen.
Wie in konstanten Ausdrückenbeschrieben, ist ein constant_expression ein Ausdruck, der zur Kompilierzeit
vollständig ausgewertet werden kann. Da die einzige Möglichkeit zum Erstellen eines nicht-NULL-Werts eines
reference_type außer string ist, den new -Operator anzuwenden, und da der new Operator in einem
constant_expression nicht zulässig ist, ist der einzige mögliche Wert für Konstanten von reference_type s, der
nicht string ist null .
Wenn ein symbolischer Name für einen konstanten Wert gewünscht ist, aber wenn der Typ dieses Werts in
einer Konstanten Deklaration nicht zulässig ist oder wenn der Wert nicht zur Kompilierzeit von einem
constant_expression berechnet werden kann, readonly wird stattdessen ein Feld (schreibgeschützte Felder)
verwendet.
Eine Konstante Deklaration, die mehrere Konstanten deklariert, entspricht mehreren Deklarationen von
einzelnen Konstanten mit denselben Attributen, modifizierertypen und Typen. Beispiel:

class A
{
public const double X = 1.0, Y = 2.0, Z = 3.0;
}

für die folgende Syntax:

class A
{
public const double X = 1.0;
public const double Y = 2.0;
public const double Z = 3.0;
}

Konstanten dürfen von anderen Konstanten innerhalb desselben Programms abhängen, solange die
Abhängigkeiten nicht aus Zirkel Natur bestehen. Der Compiler ordnet automatisch an, die Konstanten
Deklarationen in der entsprechenden Reihenfolge auszuwerten. Im Beispiel

class A
{
public const int X = B.Z + 1;
public const int Y = 10;
}

class B
{
public const int Z = A.Y + 1;
}

der Compiler wertet zunächst A.Y B.Z A.X die Werte 10 , 11 und 12 aus und wertet Sie anschließend aus.
Konstante Deklarationen können von Konstanten anderer Programme abhängen, aber solche Abhängigkeiten
sind nur in einer Richtung möglich. Wenn A und in separaten Programmen in Bezug auf das obige Beispiel B
deklariert wurden, kann es möglich sein, von abhängig zu sein A.X B.Z , aber es B.Z kann nicht gleichzeitig
von abhängen A.Y .

Felder
Ein *Field _ ist ein Member, der eine Variable darstellt, die einem Objekt oder einer Klasse zugeordnet ist. Ein
_field_declaration * führt ein oder mehrere Felder eines bestimmten Typs ein.

field_declaration
: attributes? field_modifier* type variable_declarators ';'
;

field_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'readonly'
| 'volatile'
| field_modifier_unsafe
;

variable_declarators
: variable_declarator (',' variable_declarator)*
;

variable_declarator
: identifier ('=' variable_initializer)?
;

variable_initializer
: expression
| array_initializer
;

Eine field_declaration kann einen Satz von Attributen (Attribute), einen new Modifizierer (den neuen
Modifizierer), eine gültige Kombination der vier Zugriffsmodifizierer (Zugriffsmodifizierer) und einen static
Modifizierer (statische Felder und Instanzfelder) enthalten. Außerdem kann eine field_declaration einen
readonly Modifizierer (schreibgeschützte Felder) oder einen volatile Modifizierer (flüchtige Felder) enthalten,
aber nicht beides. Die Attribute und Modifizierer gelten für alle vom field_declaration deklarierten Member. Es ist
ein Fehler, dass derselbe Modifizierer mehrmals in einer Feld Deklaration angezeigt wird.
Der Typ einer field_declaration der den Typ der Member angibt, die von der Deklaration eingeführt wurden. Auf
den Typ folgt eine Liste von variable_declarator en, von denen jeder einen neuen Member einführt. Ein
variable_declarator besteht aus einem Bezeichner , der diesen Member benennt, optional gefolgt von einem " =
"-Token und einem variable_initializer (Variableninitialisierer), der den Anfangswert dieses Members enthält.
Der Typ eines Felds muss mindestens so zugänglich sein wie das Feld selbst (Barrierefreiheits Einschränkungen).
Der Wert eines Felds wird in einem Ausdruck mithilfe eines Simple_name (einfache Namen) oder eines
member_access (Member Access) abgerufen. Der Wert eines nicht schreibgeschützten Felds wird mithilfe einer
Zuweisung (Zuweisungs Operatoren) geändert. Der Wert eines nicht schreibgeschützten Felds kann mit Postfix-
Inkrement-und Dekrementoperatoren (postfix-Inkrement-und Dekrementoperatoren) und Präfix Inkrement-und
Dekrementoperatoren (Präfix Inkrement-und Dekrementoperatoren) abgerufen und geändert werden.
Eine Feld Deklaration, die mehrere Felder deklariert, entspricht mehreren Deklarationen von einzelnen Feldern
mit denselben Attributen, modifizierertypen und Typen. Beispiel:
class A
{
public static int X = 1, Y, Z = 100;
}

für die folgende Syntax:

class A
{
public static int X = 1;
public static int Y;
public static int Z = 100;
}

Statische Felder und Instanzfelder


Wenn eine Feld Deklaration einen static Modifizierer enthält, sind die von der Deklaration eingeführten Felder
*statische Felder _. Wenn kein static Modifizierer vorhanden ist, sind die von der Deklaration eingeführten
Felder Instanzfelder. Statische Felder und Instanzfelder sind zwei der verschiedenen Arten von Variablen
(Variablen), die von c# unterstützt werden. manchmal werden Sie als statische Variablen bzw. _ Instanzvariablen
* bezeichnet.
Ein statisches Feld ist nicht Teil einer bestimmten Instanz. Stattdessen wird Sie von allen Instanzen eines
geschlossenen Typs (offene und geschlossene Typen) gemeinsam verwendet. Unabhängig davon, wie viele
Instanzen eines geschlossenen Klassen Typs erstellt werden, gibt es nur eine Kopie eines statischen Felds für die
zugehörige Anwendungsdomäne.
Beispiel:

class C<V>
{
static int count = 0;

public C() {
count++;
}

public static int Count {


get { return count; }
}
}

class Application
{
static void Main() {
C<int> x1 = new C<int>();
Console.WriteLine(C<int>.Count); // Prints 1

C<double> x2 = new C<double>();


Console.WriteLine(C<int>.Count); // Prints 1

C<int> x3 = new C<int>();


Console.WriteLine(C<int>.Count); // Prints 2
}
}

Ein Instanzfeld gehört zu einer Instanz. Insbesondere enthält jede Instanz einer Klasse einen separaten Satz aller
Instanzfelder dieser Klasse.
Wenn auf ein Feld in einem member_access verwiesen wird (Member-Zugriff) E.M , wenn M ein statisches
Feld ist, E muss einen Typ angeben M , der enthält, und wenn M ein Instanzfeld ist, muss E eine Instanz eines
Typs angeben, der enthält M .
Die Unterschiede zwischen statischen und Instanzmembern werden in statischen und
Instanzmembernausführlicher erläutert.
Schreibgeschützte Felder
Wenn eine field_declaration einen readonly Modifizierer enthält, sind die von der Deklaration eingeführten
Felder schreibgeschützte Felder . Direkte Zuweisungen zu schreibgeschützten Feldern können nur als Teil der
Deklaration oder in einem Instanzkonstruktor oder statischen Konstruktor in derselben Klasse erfolgen. (Ein
Schreib geschütztes Feld kann in diesen Kontexten mehrmals zugewiesen werden.) Direkte Zuweisungen zu
einem readonly Feld sind nur in den folgenden Kontexten zulässig:
In der variable_declarator , die das-Feld einführt (durch Einschließen einer variable_initializer in die-
Deklaration).
Für ein Instanzfeld in den Instanzkonstruktoren der-Klasse, die die Feld Deklaration enthält; für ein statisches
Feld im statischen Konstruktor der Klasse, die die Feld Deklaration enthält. Dies sind auch die einzigen
Kontexte, in denen es zulässig ist, ein readonly Feld als- out oder-Parameter zu übergeben ref .
Der Versuch, einem readonly Feld zuzuweisen oder es als- out oder- ref Parameter in einem anderen
Kontext zu übergeben, ist ein Kompilierzeitfehler.
Verwenden statischer Schreib geschützter Felder für Konstanten
Ein static readonly Feld ist nützlich, wenn ein symbolischer Name für einen konstanten Wert erwünscht ist,
aber wenn der Typ des Werts nicht in einer const Deklaration zulässig ist oder wenn der Wert nicht zur
Kompilierzeit berechnet werden kann. Im Beispiel

public class Color


{
public static readonly Color Black = new Color(0, 0, 0);
public static readonly Color White = new Color(255, 255, 255);
public static readonly Color Red = new Color(255, 0, 0);
public static readonly Color Green = new Color(0, 255, 0);
public static readonly Color Blue = new Color(0, 0, 255);

private byte red, green, blue;

public Color(byte r, byte g, byte b) {


red = r;
green = g;
blue = b;
}
}

die Member Black , White , Red , Green und Blue können nicht als Member deklariert werden, const da
ihre Werte zur Kompilierzeit nicht berechnet werden können. Das deklarieren static readonly der Dateien hat
jedoch die gleiche Wirkung.
Versionierung von Konstanten und statischen schreibgeschützten Feldern
Konstanten und schreibgeschützte Felder haben unterschiedliche Semantik für die binäre Versionsverwaltung.
Wenn ein Ausdruck auf eine Konstante verweist, wird der Wert der Konstante zur Kompilierzeit abgerufen.
Wenn jedoch ein Ausdruck auf ein Schreib geschütztes Feld verweist, wird der Wert des Felds bis zur Laufzeit
nicht abgerufen. Stellen Sie sich eine Anwendung vor, die aus zwei separaten Programmen besteht:
using System;

namespace Program1
{
public class Utils
{
public static readonly int X = 1;
}
}

namespace Program2
{
class Test
{
static void Main() {
Console.WriteLine(Program1.Utils.X);
}
}
}

Die Program1 Program2Namespaces und bezeichnen zwei Programme, die separat kompiliert werden. Da
Program1.Utils.X als statisches Schreib geschütztes Feld deklariert ist, ist der Wert, der von der-Anweisung
ausgegeben wird Console.WriteLine , zur Kompilierzeit nicht bekannt, sondern wird zur Laufzeit abgerufen.
Wenn also der Wert von X geändert und Program1 neu kompiliert wird, gibt die Console.WriteLine
Anweisung den neuen Wert auch dann aus, wenn Program2 nicht neu kompiliert wird. War jedoch X eine
Konstante. der Wert von wurde zum X Zeitpunkt der Program2 Kompilierung abgerufen und bleibt von
Änderungen in Program1 bis zur Program2 erneuten Kompilierung nicht beeinträchtigt.
Flüchtige Felder
Wenn eine field_declaration einen volatile Modifizierer enthält, sind die von dieser Deklaration eingeführten
Felder flüchtige Felder .
Bei nicht flüchtigen Feldern können Optimierungstechniken, die Anweisungen neu anordnen, zu unerwarteten
und unvorhersehbaren Ergebnissen in Multithreadprogrammen führen, die auf Felder ohne Synchronisierung
zugreifen, wie z. b., die vom lock_statement (lock-Anweisung) bereitgestellt werden. Diese Optimierungen
können vom Compiler, vom Laufzeitsystem oder von der Hardware ausgeführt werden. Für flüchtige Felder sind
solche neubestellungs Optimierungen eingeschränkt:
Ein Lesevorgang eines flüchtigen Felds wird als flüchtiger Lese Vorgang bezeichnet. Ein flüchtiger
Lesevorgang hat "Semantik abrufen". Das heißt, es wird sichergestellt, dass es vor allen in der Anweisungs
Sequenz auftretenden verweisen auf den Speicher auftritt.
Ein Schreibvorgang eines flüchtigen Felds wird als flüchtiges schreiben bezeichnet. Ein flüchtiger
Schreibvorgang weist die Freigabe Semantik auf. Das heißt, es wird sichergestellt, dass es nach allen Speicher
verweisen vor der Schreib Anweisung in der Anweisungs Sequenz erfolgt.
Diese Einschränkungen stellen sicher, dass alle Threads volatile-Schreibvorgänge, die von einem anderen Thread
ausgeführt werden, in der Reihenfolge ihrer Ausführung berücksichtigen. Eine konforme Implementierung ist
nicht erforderlich, um eine einzelne Gesamt Reihenfolge von flüchtigen Schreibvorgängen zu gewährleisten, die
von allen Ausführungs Threads erkannt werden. Der Typ eines flüchtigen Felds muss einer der folgenden sein:
Eine reference_type.
Der Typ byte , sbyte , short ,,, ushort int uint , char , float , bool , System.IntPtr oder
System.UIntPtr .
Ein enum_type mit dem Aufzählungs Basistyp byte , sbyte , short , ushort , int oder uint .
Das Beispiel
using System;
using System.Threading;

class Test
{
public static int result;
public static volatile bool finished;

static void Thread2() {


result = 143;
finished = true;
}

static void Main() {


finished = false;

// Run Thread2() in a new thread


new Thread(new ThreadStart(Thread2)).Start();

// Wait for Thread2 to signal that it has a result by setting


// finished to true.
for (;;) {
if (finished) {
Console.WriteLine("result = {0}", result);
return;
}
}
}
}

erzeugt die Ausgabe:

result = 143

In diesem Beispiel startet die-Methode Main einen neuen Thread, der die-Methode ausführt Thread2 . Diese
Methode speichert einen Wert in ein nicht flüchtiges Feld result mit dem Namen und speichert dann true im
flüchtigen Feld finished . Der Haupt Thread wartet darauf, dass das Feld finished auf festgelegt wird true ,
und liest dann das Feld result . Da finished deklariert wurde volatile , muss der Haupt Thread den Wert
143 aus dem Feld lesen result . Wenn das Feld finished nicht deklariert wurde volatile , ist es zulässig,
dass der Speicher für result den Haupt Thread nach dem Speichern in sichtbar finished ist, sodass der Haupt
Thread den Wert 0 aus dem Feld liest result . Das deklarieren finished als volatile Feld verhindert
jegliche Inkonsistenz.
Feld Initialisierung
Der Anfangswert eines Felds, unabhängig davon, ob es sich um ein statisches Feld oder ein Instanzfeld handelt,
ist der Standardwert (Standardwerte) des Feldtyps. Es ist nicht möglich, den Wert eines Felds zu beobachten,
bevor diese Standard Initialisierung erfolgt ist, und ein Feld wird daher nie "nicht initialisiert". Das Beispiel
using System;

class Test
{
static bool b;
int i;

static void Main() {


Test t = new Test();
Console.WriteLine("b = {0}, i = {1}", b, t.i);
}
}

erzeugt die Ausgabe

b = False, i = 0

b , da und i automatisch mit den Standardwerten initialisiert werden.


Variableninitialisierer
Feld Deklarationen können variable_initializer s enthalten. Bei statischen Feldern entsprechen
Variableninitialisierern Zuweisungs Anweisungen, die während der Initialisierung der Klasse ausgeführt werden.
Bei Instanzfeldern entsprechen Variableninitialisierern Zuweisungs Anweisungen, die ausgeführt werden, wenn
eine Instanz der Klasse erstellt wird.
Das Beispiel

using System;

class Test
{
static double x = Math.Sqrt(2.0);
int i = 100;
string s = "Hello";

static void Main() {


Test a = new Test();
Console.WriteLine("x = {0}, i = {1}, s = {2}", x, a.i, a.s);
}
}

erzeugt die Ausgabe

x = 1.4142135623731, i = 100, s = Hello

eine Zuweisung zu x erfolgt, wenn statische Feldinitialisierer ausgeführt werden und Zuweisungen zu i und
auftreten, s Wenn die instanzfeldinitialisierer ausgeführt werden.
Die in der Feld Initialisierung beschriebene Standardwert Initialisierung tritt für alle Felder auf, einschließlich der
Felder, die über Variableninitialisierer verfügen. Wenn eine Klasse initialisiert wird, werden daher alle statischen
Felder in dieser Klasse zuerst mit ihren Standardwerten initialisiert, und anschließend werden die statischen
Feldinitialisierer in der Textfolge ausgeführt. Wenn eine Instanz einer Klasse erstellt wird, werden alle
Instanzfelder in dieser Instanz zuerst mit ihren Standardwerten initialisiert, und anschließend werden die
instanzfeldinitialisierer in der Textfolge ausgeführt.
Es ist möglich, dass statische Felder mit Variableninitialisierern in ihrem Standardwert Zustand beobachtet
werden. Dies wird jedoch dringend von einem Stil abgeraten. Das Beispiel
using System;

class Test
{
static int a = b + 1;
static int b = a + 1;

static void Main() {


Console.WriteLine("a = {0}, b = {1}", a, b);
}
}

weist dieses Verhalten auf. Trotz der zirkulären Definitionen von a und b ist das Programm gültig. Dadurch wird
die Ausgabe ausgegeben.

a = 1, b = 2

Da die statischen Felder a und b auf 0 (den Standardwert für) initialisiert werden, int bevor Ihre
Initialisierer ausgeführt werden. Wenn der Initialisierer für ausgeführt wird a , ist der Wert von b 0 (null) und
a wird daher mit initialisiert 1 . Wenn der Initialisierer für ausgeführt wird b , ist der Wert von a bereits 1
, und b wird daher mit initialisiert 2 .
Initialisierung statischer Felder
Die statischen feldvariableninitialisierer einer Klasse entsprechen einer Sequenz von Zuweisungen, die in der
Text Reihenfolge ausgeführt werden, in der Sie in der Klassen Deklaration angezeigt werden. Wenn ein statischer
Konstruktor (statischer Konstruktor) in der Klasse vorhanden ist, erfolgt die Ausführung der statischen
Feldinitialisierer unmittelbar vor der Ausführung dieses statischen Konstruktors. Andernfalls werden die
statischen Feldinitialisierer zu einem Implementierungs abhängigen Zeitpunkt vor der ersten Verwendung eines
statischen Felds dieser Klasse ausgeführt. Das Beispiel

using System;

class Test
{
static void Main() {
Console.WriteLine("{0} {1}", B.Y, A.X);
}

public static int F(string s) {


Console.WriteLine(s);
return 1;
}
}

class A
{
public static int X = Test.F("Init A");
}

class B
{
public static int Y = Test.F("Init B");
}

erzeugt möglicherweise die folgende Ausgabe:


Init A
Init B
1 1

oder die Ausgabe:

Init B
Init A
1 1

Da die Ausführung des X Initialisierers und Y des Initialisierers in einer der beiden Reihenfolge auftreten kann,
sind Sie nur so eingeschränkt, dass Sie vor den verweisen auf diese Felder auftreten. Im Beispiel:

using System;

class Test
{
static void Main() {
Console.WriteLine("{0} {1}", B.Y, A.X);
}

public static int F(string s) {


Console.WriteLine(s);
return 1;
}
}

class A
{
static A() {}

public static int X = Test.F("Init A");


}

class B
{
static B() {}

public static int Y = Test.F("Init B");


}

die Ausgabe muss wie folgt lauten:

Init B
Init A
1 1

Da die Regeln für den Fall, dass statische Konstruktoren ausgeführt werden (wie in statischen
Konstruktorendefiniert), den B statischen Konstruktor (und somit B die statischen Feldinitialisierer) ausführen
müssen, müssen vor A dem statischen Konstruktor und den feldinitialisierern ausgeführt werden.
Instanzfeldinitialisierung
Die instanzfeldvariableninitialisierer einer Klasse entsprechen einer Sequenz von Zuweisungen, die unmittelbar
nach dem Eintrag für einen der Instanzkonstruktoren (Konstruktorinitialisierer) dieser Klasse ausgeführt werden.
Die Variableninitialisierer werden in der Text Reihenfolge ausgeführt, in der Sie in der Klassen Deklaration
angezeigt werden. Die Erstellung und Initialisierung der Klasseninstanz wird in
Instanzkonstruktorenausführlicher beschrieben.
Ein Variableninitialisierer für ein Instanzfeld kann nicht auf die erstellte Instanz verweisen. Daher ist es ein
Kompilierzeitfehler, this der in einem Variableninitialisierer referenziert werden kann, da es sich um einen
Kompilierzeitfehler für einen Variableninitialisierer handelt, der über eine Simple_name auf ein Instanzmember
verweist. Im Beispiel

class A
{
int x = 1;
int y = x + 1; // Error, reference to instance member of this
}

der Variableninitialisierer für y führt zu einem Kompilierzeitfehler, da er auf einen Member der Instanz
verweist, die erstellt wird.

Methoden
Ein *method _ ist ein Member, der eine Berechnung oder eine Aktion implementiert, die von einem Objekt oder
einer Klasse ausgeführt werden kann. Methoden werden mit _method_declaration * s deklariert:

method_declaration
: method_header method_body
;

method_header
: attributes? method_modifier* 'partial'? return_type member_name type_parameter_list?
'(' formal_parameter_list? ')' type_parameter_constraints_clause*
;

method_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'async'
| method_modifier_unsafe
;

return_type
: type
| 'void'
;

member_name
: identifier
| interface_type '.' identifier
;

method_body
: block
| '=>' expression ';'
| ';'
;

Eine method_declaration kann eine Reihe von Attributen (Attribute) und eine gültige Kombination der vier
Zugriffsmodifizierer (Zugriffsmodifizierer), ( new der neuen Modifizierer), static (statische und
Instanzmethoden), virtual (virtuelle Methoden), override (Überschreibungs Methoden), sealed (versiegelte
Methoden), abstract (abstrakte Methoden) und extern (Externe Methoden)
Eine-Deklaration hat eine gültige Kombination von Modifizierer, wenn Folgendes zutrifft:
Die-Deklaration enthält eine gültige Kombination von zugriffsmodifizierermodifizierer
Die-Deklaration enthält nicht denselben Modifizierer mehrmals.
Die-Deklaration enthält höchstens einen der folgenden Modifizierer: static , virtual und override .
Die-Deklaration enthält höchstens einen der folgenden Modifizierer: new und override .
Wenn die Deklaration den- abstract Modifizierer enthält, enthält die Deklaration keinen der folgenden
Modifizierer static : virtual , sealed oder extern .
Wenn die Deklaration den- private Modifizierer enthält, enthält die Deklaration keinen der folgenden
Modifizierer: virtual , override oder abstract .
Wenn die Deklaration den- sealed Modifizierer enthält, enthält die Deklaration auch den- override
Modifizierer.
Wenn die Deklaration den- partial Modifizierer enthält, enthält Sie keinen der folgenden Modifizierer: new
, public , protected , internal , private , virtual , sealed , override , abstract oder extern .

Eine Methode, die über den async -Modifizierer verfügt, ist eine Async-Funktion und befolgt die in Async-
Funktionenbeschriebenen Regeln.
Der return_type einer Methoden Deklaration gibt den Typ des Werts an, der von der-Methode berechnet und
zurückgegeben wird. Der return_type ist, void Wenn die Methode keinen Wert zurückgibt. Wenn die
Deklaration den- partial Modifizierer enthält, muss der Rückgabetyp sein void .
Der MEMBER_NAME der den Namen der Methode angibt. Wenn die Methode keine explizite
Schnittstellenmember-Implementierung (explizite Schnittstellenmember-Implementierungen) ist, ist die
MEMBER_NAME einfach ein Bezeichner. Bei einer expliziten Schnittstellenmember-Implementierung besteht die
MEMBER_NAME aus einer INTERFACE_TYPE gefolgt von " . " und einem Bezeichner.
Der optionale type_parameter_list gibt die Typparameter der Methode (Typparameter) an. Wenn ein
type_parameter_list angegeben wird, ist die Methode eine *generische Methode _. Wenn die Methode über
einen extern Modifizierer verfügt, kann kein _type_parameter_list * angegeben werden.
Der optionale formal_parameter_list gibt die Parameter der Methode (Methoden Parameter) an.
Die optionalen type_parameter_constraints_clause s geben Einschränkungen für einzelne Typparameter an
(Typparameter Einschränkungen) und können nur angegeben werden, wenn eine type_parameter_list ebenfalls
bereitgestellt wird, und die-Methode verfügt über keinen- override Modifizierer.
Die return_type und alle Typen, auf die im formal_parameter_list einer Methode verwiesen wird, müssen
mindestens so zugänglich sein wie die Methode selbst (Barrierefreiheits Einschränkungen).
Der method_body ist entweder ein Semikolon, ein -**Anweisungs Text* _ oder ein Ausdrucks Körper. Ein
Anweisungs Text besteht aus einer _block *, die die Anweisungen angibt, die beim Aufrufen der Methode
ausgeführt werden sollen. Ein Ausdrucks Körper besteht aus => , gefolgt von einem Ausdruck und einem
Semikolon und deutet auf einen einzelnen Ausdruck hin, der beim Aufrufen der Methode ausgeführt werden
soll.
Für abstract extern die-Methode und die-Methode besteht die method_body einfach aus einem Semikolon.
Für partial Methoden kann die method_body entweder aus einem Semikolon, einem Block Text oder einem
Ausdrucks Text bestehen. Bei allen anderen Methoden ist die method_body entweder ein Block Körper oder ein
Ausdrucks Körper.
Wenn das method_body aus einem Semikolon besteht, kann die Deklaration den- async Modifizierer nicht
enthalten.
Der Name, die Typparameter Liste und die Liste der formalen Parameter einer Methode definieren die Signatur
(Signaturen und überladen) der Methode. Insbesondere besteht die Signatur einer Methode aus dem Namen,
der Anzahl der Typparameter und der Anzahl, den modifizierertypen und den Typen der formalen Parameter. Zu
diesem Zweck wird jeder Typparameter der Methode, die im Typ eines formalen Parameters vorkommt, nicht
anhand seines Namens identifiziert, sondern anhand seiner Ordinalposition in der Typargument Liste der
Methode. Der Rückgabetyp ist weder Teil der Signatur einer Methode noch die Namen der Typparameter oder
der formalen Parameter.
Der Name einer Methode muss sich von den Namen aller anderen nicht-Methoden unterscheiden, die in
derselben Klasse deklariert sind. Außerdem muss sich die Signatur einer Methode von den Signaturen aller
anderen Methoden unterscheiden, die in derselben Klasse deklariert sind, und zwei Methoden, die in derselben
Klasse deklariert werden, dürfen keine Signaturen aufweisen, die sich ausschließlich durch und unterscheiden
ref out .

Die type_parameter s der Methode befinden sich im Gültigkeitsbereich des gesamten method_declaration und
können verwendet werden, um Typen in diesem Bereich in return_type, method_body und
type_parameter_constraints_clause s, jedoch nicht in Attributen zu bilden.
Alle formalen Parameter und Typparameter müssen unterschiedliche Namen aufweisen.
Methoden Parameter
Die Parameter einer Methode, sofern vorhanden, werden durch den formal_parameter_list der Methode
deklariert.

formal_parameter_list
: fixed_parameters
| fixed_parameters ',' parameter_array
| parameter_array
;

fixed_parameters
: fixed_parameter (',' fixed_parameter)*
;

fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;

default_argument
: '=' expression
;

parameter_modifier
: 'ref'
| 'out'
| 'this'
;

parameter_array
: attributes? 'params' array_type identifier
;

Die Liste formaler Parameter besteht aus mindestens einem durch Trennzeichen getrennten Parameter, bei dem
nur der letzte eine parameter_array sein kann.
Ein fixed_parameter besteht aus einem optionalen Satz von Attributen (Attributen), einem optionalen ref out
this -Modifizierer oder-Modifizierer, einem Typ, einem Bezeichner und einem optionalen default_argument.
Jede fixed_parameter deklariert einen Parameter des angegebenen Typs mit dem angegebenen Namen. Der
this -Modifizierer legt die-Methode als Erweiterungsmethode fest und ist nur für den ersten Parameter einer
statischen Methode zulässig. Erweiterungs Methoden werden weiter unten in den Erweiterungs
Methodenbeschrieben.
Ein fixed_parameter mit einem default_argument wird als *optionaler Parameter _ bezeichnet, wohingegen
ein fixed_parameter * ohne default_argument ein *erforderlicher Parameter ist. Ein erforderlicher Parameter
darf nicht nach einem optionalen Parameter in einer _formal_parameter_list * angezeigt werden.
Ein- ref oder- out Parameter kann keine default_argument haben. Der Ausdruck in einem default_argument
muss einer der folgenden sein:
eine constant_expression
ein Ausdruck der Form, new S() in der S ein Werttyp ist.
ein Ausdruck der Form, default(S) in der S ein Werttyp ist.
Der Ausdruck muss implizit durch eine Identität oder eine Konvertierung in den Typ des Parameters konvertiert
werden können.
Wenn optionale Parameter in einer implementierenden partiellen Methoden Deklaration (partielle Methoden),
einer expliziten Schnittstellenmember-Implementierung (explizite Schnittstellenmember-Implementierungen)
oder in einer Indexer-Deklaration mit einem einzelnen Parameter (Indexer) auftreten, sollte der Compiler eine
Warnung angeben, da diese Member niemals so aufgerufen werden können, dass Argumente ausgelassen
werden.
Ein parameter_array besteht aus einem optionalen Satz von Attributen (Attributen), einem params Modifizierer,
einem array_type und einem Bezeichner. Ein Parameter Array deklariert einen einzelnen Parameter des
angegebenen Array Typs mit dem angegebenen Namen. Der array_type eines Parameter Arrays muss ein
eindimensionaler Arraytyp (Array Typen) sein. Bei einem Methodenaufruf ermöglicht ein Parameter Array, dass
entweder ein einzelnes Argument des angegebenen Array Typs angegeben wird, oder es lässt NULL oder mehr
Argumente des Array Elementtyps angegeben werden. Parameter Arrays werden in Parameter
Arraysausführlicher beschrieben.
Eine parameter_array kann nach einem optionalen Parameter auftreten, kann aber keinen Standardwert
aufweisen. das Weglassen von Argumenten für eine parameter_array würde stattdessen zur Erstellung eines
leeren Arrays führen.
Das folgende Beispiel veranschaulicht verschiedene Arten von Parametern:

public void M(
ref int i,
decimal d,
bool b = false,
bool? n = false,
string s = "Hello",
object o = null,
T t = default(T),
params int[] a
) { }

Im formal_parameter_list für M i ist ein erforderlicher ref-Parameter, d ist ein erforderlicher Wert Parameter,
b , s , o und t sind optionale Wert Parameter und a ein Parameter Array.

Eine Methoden Deklaration erstellt einen separaten Deklarations Bereich für Parameter, Typparameter und lokale
Variablen. Namen werden in diesem Deklarations Bereich durch die Typparameter Liste und die Liste formaler
Parameter der-Methode und durch lokale Variablen Deklarationen im- Block der-Methode eingeführt. Es ist ein
Fehler, wenn zwei Member eines Methoden Deklarations Raums denselben Namen haben. Es ist ein Fehler für
den Deklarations Bereich der Methode und der Deklarations Bereich der lokalen Variablen eines in der Tabelle
enthaltenen Deklarations Raums, der Elemente mit dem gleichen Namen enthalten soll.
Ein Methodenaufruf (Methodenaufrufe) erstellt eine Kopie, die für diesen Aufruf spezifisch ist, der formalen
Parameter und lokalen Variablen der Methode, und die Argumentliste des Aufrufs weist den neu erstellten
formalen Parametern Werte oder Variablen Verweise zu. Innerhalb des- Blocks einer Methode kann von ihren
bezeichtern in Simple_name Ausdrücken (einfache Namen) auf formale Parameter verwiesen werden.
Es gibt vier Arten von formalen Parametern:
Wert Parameter, die ohne modifiziererer deklariert werden.
Verweis Parameter, die mit dem- ref Modifizierer deklariert werden.
Ausgabeparameter, die mit dem- out Modifizierer deklariert werden.
Parameter Arrays, die mit dem- params Modifizierer deklariert werden.

Wie in Signaturen und überladenbeschrieben, ref sind die out Modifizierer und Teil der Signatur einer
Methode, der- params Modifizierer ist jedoch nicht.
Wert Parameter
Ein Parameter, der ohne Modifizierer deklariert wird, ist ein value-Parameter. Ein value-Parameter entspricht
einer lokalen Variablen, die den Anfangswert aus dem entsprechenden Argument abruft, das im
Methodenaufruf angegeben wird.
Wenn ein formaler Parameter ein value-Parameter ist, muss das entsprechende Argument in einem
Methodenaufruf ein Ausdruck sein, der implizit konvertierbar ist (implizite Konvertierungen), in den formalen
Parametertyp.
Eine Methode darf einem Wert Parameter neue Werte zuweisen. Solche Zuweisungen wirken sich nur auf den
lokalen Speicherort aus, der durch den value-Parameter dargestellt wird – Sie haben keine Auswirkung auf das
tatsächliche Argument, das im Methodenaufruf angegeben wurde.
Verweisparameter
Ein Parameter, der mit einem- ref Modifizierer deklariert wird, ist ein Verweis Parameter. Anders als bei einem
value-Parameter erstellt ein Verweis Parameter keinen neuen Speicherort. Stattdessen stellt ein Verweis
Parameter denselben Speicherort wie die Variable dar, die im Methodenaufruf als Argument angegeben wurde.
Wenn ein formaler Parameter ein Verweis Parameter ist, muss das entsprechende Argument in einem
Methodenaufruf aus dem Schlüsselwort bestehen, ref gefolgt von einem variable_reference (genaue Regeln
zum Bestimmen der eindeutigen Zuweisung) desselben Typs wie der formale Parameter. Eine Variable muss
definitiv zugewiesen werden, bevor Sie als Verweis Parameter übergeben werden kann.
Innerhalb einer Methode wird ein Verweis Parameter immer als definitiv zugewiesen betrachtet.
Eine Methode, die als Iterator (Iteratoren) deklariert ist, darf keine Verweis Parameter aufweisen.
Das Beispiel
using System;

class Test
{
static void Swap(ref int x, ref int y) {
int temp = x;
x = y;
y = temp;
}

static void Main() {


int i = 1, j = 2;
Swap(ref i, ref j);
Console.WriteLine("i = {0}, j = {1}", i, j);
}
}

erzeugt die Ausgabe

i = 2, j = 1

Für den Aufruf von Swap in Main x stellt i und y dar j . Folglich hat der Aufruf die Auswirkungen, die
Werte von i und auszutauschen j .
In einer Methode, die Verweis Parameter annimmt, ist es möglich, dass mehrere Namen denselben Speicherort
darstellen. Im Beispiel

class A
{
string s;

void F(ref string a, ref string b) {


s = "One";
a = "Two";
b = "Three";
}

void G() {
F(ref s, ref s);
}
}

der Aufruf von F in G übergibt einen Verweis auf s sowohl für a als auch für b . Daher beziehen sich bei
diesem Aufruf die Namen, s a und b alle auf denselben Speicherort, und die drei Zuweisungen ändern das
Instanzfeld s .
Ausgabeparameter
Ein mit einem- out Modifizierer deklarierter Parameter ist ein Output-Parameter. Ähnlich wie ein Verweis
Parameter wird von einem Output-Parameter kein neuer Speicherort erstellt. Stattdessen stellt ein
Ausgabeparameter denselben Speicherort wie die Variable dar, die im Methodenaufruf als Argument angegeben
wurde.
Wenn ein formaler Parameter ein Ausgabeparameter ist, muss das entsprechende Argument in einem
Methodenaufruf aus dem Schlüsselwort bestehen, out gefolgt von einem variable_reference (präzise Regeln
zum Bestimmen der eindeutigen Zuweisung) desselben Typs wie der formale Parameter. Eine Variable muss
nicht definitiv zugewiesen werden, bevor Sie als Output-Parameter übergeben werden kann. nach einem Aufruf,
bei dem eine Variable als Output-Parameter übergeben wurde, wird die Variable als definitiv zugewiesen
betrachtet.
Innerhalb einer Methode wird ein Ausgabeparameter, wie eine lokale Variable, anfänglich als nicht zugewiesen
betrachtet und muss definitiv zugewiesen werden, bevor der Wert verwendet wird.
Jeder Ausgabeparameter einer Methode muss definitiv zugewiesen werden, bevor die Methode zurückgegeben
wird.
Eine Methode, die als partielle Methode (partielle Methoden) oder Iterator (Iteratoren) deklariert ist, darf keine
Ausgabeparameter aufweisen.
Ausgabeparameter werden in der Regel in Methoden verwendet, die mehrere Rückgabewerte liefern. Beispiel:

using System;

class Test
{
static void SplitPath(string path, out string dir, out string name) {
int i = path.Length;
while (i > 0) {
char ch = path[i - 1];
if (ch == '\\' || ch == '/' || ch == ':') break;
i--;
}
dir = path.Substring(0, i);
name = path.Substring(i);
}

static void Main() {


string dir, name;
SplitPath("c:\\Windows\\System\\hello.txt", out dir, out name);
Console.WriteLine(dir);
Console.WriteLine(name);
}
}

Das Beispiel erzeugt die Ausgabe:

c:\Windows\System\
hello.txt

Beachten Sie, dass die dir Zuweisung der-Variable und der-Variablen aufgehoben name werden kann, bevor
Sie an übermittelt werden SplitPath , und dass Sie nach dem-Befehl definitiv zugewiesen werden
Parameterarrays
Ein Parameter, der mit einem- params Modifizierer deklariert wird, ist ein Parameter Array. Wenn eine Liste
formaler Parameter ein Parameter Array enthält, muss es sich um den letzten Parameter in der Liste handeln,
und es muss sich um einen eindimensionalen Arraytyp handeln. Beispielsweise string[] können die-Typen und
string[][] als Typ eines Parameter Arrays verwendet werden, aber der-Typ ist string[,] nicht möglich. Es ist
nicht möglich, den params -Modifizierer mit den Modifizierern und zu kombinieren ref out .
Ein Parameter Array ermöglicht das Angeben von Argumenten in einem Methodenaufruf auf zwei Arten:
Das für ein Parameter Array angegebene Argument kann ein einzelner Ausdruck sein, der implizit
konvertierbar ist (implizite Konvertierungen), in den Typ des Parameter Arrays. In diesem Fall verhält sich das
Parameter Array genau wie ein value-Parameter.
Alternativ kann der Aufruf NULL oder mehr Argumente für das Parameter Array angeben, wobei jedes
Argument ein Ausdruck ist, der implizit konvertierbar ist (implizite Konvertierungen), in den Elementtyp des
Parameter Arrays. In diesem Fall erstellt der Aufruf eine Instanz des Parameter Array Typs mit einer Länge, die
der Anzahl der Argumente entspricht, initialisiert die Elemente der Array Instanz mit den angegebenen
Argument Werten und verwendet die neu erstellte Array Instanz als tatsächliches Argument.
Mit der Ausnahme, dass eine Variable Anzahl von Argumenten in einem Aufruf zugelassen wird, entspricht ein
Parameter Array exakt einem Wert Parameter (value-Parameter) desselben Typs.
Das Beispiel

using System;

class Test
{
static void F(params int[] args) {
Console.Write("Array contains {0} elements:", args.Length);
foreach (int i in args)
Console.Write(" {0}", i);
Console.WriteLine();
}

static void Main() {


int[] arr = {1, 2, 3};
F(arr);
F(10, 20, 30, 40);
F();
}
}

erzeugt die Ausgabe

Array contains 3 elements: 1 2 3


Array contains 4 elements: 10 20 30 40
Array contains 0 elements:

Beim ersten Aufruf von wird F das Array einfach a als value-Parameter übergeben. Der zweite Aufruf von F
erstellt automatisch ein vier-Element int[] mit den angegebenen Element Werten und übergibt diese Array
Instanz als Wert Parameter. Ebenso erstellt der dritte Aufruf von F ein NULL-Element int[] und übergibt
diese Instanz als value-Parameter. Der zweite und der dritte Aufruf entsprechen genau dem Schreiben:

F(new int[] {10, 20, 30, 40});


F(new int[] {});

Beim Durchführen der Überladungs Auflösung kann eine Methode mit einem Parameter Array entweder in der
normalen Form oder in der erweiterten Form (anwendbares Funktionsmember) anwendbar sein. Die erweiterte
Form einer Methode ist nur verfügbar, wenn die normale Form der Methode nicht anwendbar ist und nur, wenn
eine anwendbare Methode mit derselben Signatur wie das erweiterte Formular nicht bereits im selben Typ
deklariert ist.
Das Beispiel
using System;

class Test
{
static void F(params object[] a) {
Console.WriteLine("F(object[])");
}

static void F() {


Console.WriteLine("F()");
}

static void F(object a0, object a1) {


Console.WriteLine("F(object,object)");
}

static void Main() {


F();
F(1);
F(1, 2);
F(1, 2, 3);
F(1, 2, 3, 4);
}
}

erzeugt die Ausgabe

F();
F(object[]);
F(object,object);
F(object[]);
F(object[]);

Im Beispiel sind zwei der möglichen erweiterten Formen der-Methode mit einem Parameter Array bereits als
reguläre Methoden in der-Klasse enthalten. Diese erweiterten Formulare werden daher bei der Überladungs
Auflösung nicht berücksichtigt, und der erste und dritte Methodenaufruf wählen daher die regulären Methoden
aus. Wenn eine Klasse eine Methode mit einem Parameter Array deklariert, ist es nicht ungewöhnlich, dass auch
einige der erweiterten Formulare als reguläre Methoden enthalten sind. Auf diese Weise ist es möglich, die
Zuordnung einer Array Instanz zu vermeiden, die auftritt, wenn eine erweiterte Form einer Methode mit einem
Parameter Array aufgerufen wird.
Wenn der Typ eines Parameter Arrays ist object[] , entsteht eine potenzielle Mehrdeutigkeit zwischen der
normalen Form der-Methode und dem aufgewendet-Formular für einen einzelnen object Parameter. Der
Grund für die Mehrdeutigkeit besteht darin, dass eine object[] selbst implizit in den Typ konvertiert werden
kann object . Die Mehrdeutigkeit stellt jedoch kein Problem dar, da Sie durch Einfügen einer Umwandlung bei
Bedarf aufgelöst werden kann.
Das Beispiel
using System;

class Test
{
static void F(params object[] args) {
foreach (object o in args) {
Console.Write(o.GetType().FullName);
Console.Write(" ");
}
Console.WriteLine();
}

static void Main() {


object[] a = {1, "Hello", 123.456};
object o = a;
F(a);
F((object)a);
F(o);
F((object[])o);
}
}

erzeugt die Ausgabe

System.Int32 System.String System.Double


System.Object[]
System.Object[]
System.Int32 System.String System.Double

Im ersten und letzten Aufruf von F ist die normale Form von anwendbar, F da eine implizite Konvertierung
vom Argumenttyp in den Parametertyp besteht (beide sind vom Typ object[] ). Folglich wählt die Überladungs
Auflösung die normale Form von aus F , und das-Argument wird als regulärer Wert Parameter übergeben. Im
zweiten und dritten Aufruf ist die normale Form von F nicht anwendbar, weil keine implizite Konvertierung
vom Argumenttyp zum Parametertyp vorhanden ist (der Typ object kann nicht implizit in den Typ konvertiert
werden object[] ). Allerdings ist die erweiterte Form von F anwendbar, sodass Sie durch Überladungs
Auflösung ausgewählt wird. Folglich wird ein One-Element object[] durch den Aufruf erstellt, und das einzelne
Element des Arrays wird mit dem angegebenen Argument Wert initialisiert (der selbst ein Verweis auf ein ist
object[] ).

Statische Methoden und Instanzmethoden


Wenn eine Methoden Deklaration einen- static Modifizierer enthält, wird diese Methode als statische
Methode bezeichnet. Wenn kein static Modifizierer vorhanden ist, wird die Methode als Instanzmethode
bezeichnet.
Eine statische Methode funktioniert nicht für eine bestimmte Instanz, und Sie ist ein Kompilierzeitfehler, auf den
this in einer statischen Methode verwiesen wird.

Eine Instanzmethode arbeitet auf einer bestimmten Instanz einer Klasse, und auf diese Instanz kann als this
(dieser Zugriff) zugegriffen werden.
Wenn auf eine Methode in einer member_access verwiesen wird (Member-Zugriff) E.M , wenn M eine
statische Methode ist, E muss einen Typ angeben, der enthält M , und wenn M eine Instanzmethode ist, muss
eine E Instanz eines Typs angeben, der enthält M .
Die Unterschiede zwischen statischen und Instanzmembern werden in statischen und
Instanzmembernausführlicher erläutert.
Virtuelle Methoden
Wenn eine Instanzmethodendeklaration einen- virtual Modifizierer enthält, wird diese Methode als virtuelle
Methode bezeichnet. Wenn kein virtual Modifizierer vorhanden ist, wird die Methode als nicht virtuelle
Methode bezeichnet.
Die Implementierung einer nicht virtuellen Methode ist unveränderlich: die Implementierung ist identisch,
unabhängig davon, ob die-Methode für eine Instanz der-Klasse, in der Sie deklariert ist, oder für eine Instanz
einer abgeleiteten Klasse aufgerufen wird. Im Gegensatz dazu kann die Implementierung einer virtuellen
Methode durch abgeleitete Klassen abgelöst werden. Der Prozess der Ersetzung der Implementierung einer
geerbten virtuellen Methode wird als über Schreiben dieser Methode bezeichnet (Überschreibungs
Methoden).
Beim Aufruf einer virtuellen Methode bestimmt der *Lauf Zeittyp _ der-Instanz, für die dieser Aufruf erfolgt,
die tatsächliche Methoden Implementierung, die aufgerufen werden soll. Bei einem Aufruf der nicht virtuellen
Methode ist der _-Kompilier Zeittyp* der-Instanz der bestimmende Faktor. Wenn eine Methode mit dem Namen
mit einer N Argumentliste A für eine Instanz mit einem Kompilier Zeittyp C und einem Lauf Zeittyp R
(wobei R entweder C oder eine von abgeleitete Klasse ist) aufgerufen wird C , wird der Aufruf wie folgt
verarbeitet:
Zuerst wird die Überladungs Auflösung auf C , N und angewendet A , um eine bestimmte Methode M
aus dem Satz von Methoden auszuwählen, der in deklariert und von geerbt wurde C . Dies wird unter
Methodenaufrufebeschrieben.
Wenn dann M eine nicht virtuelle Methode ist, M wird aufgerufen.
Andernfalls M ist eine virtuelle Methode, und die am meisten abgeleitete Implementierung von M in Bezug
auf R wird aufgerufen.
Für jede virtuelle Methode, die in der Klasse deklariert oder von einer Klasse geerbt wurde, gibt es in Bezug auf
diese Klasse eine am meisten abgeleitete Implementierung der-Methode. Die am weitesten abgeleitete
Implementierung einer virtuellen Methode in M Bezug auf eine Klasse R wird wie folgt bestimmt:
Wenn R die Introducing- virtual Deklaration von enthält M , dann handelt es sich hierbei um die am
meisten abgeleitete Implementierung von M .
Wenn andernfalls R einen von enthält override M , ist dies die am meisten abgeleitete Implementierung
von M .
Andernfalls ist die am meisten abgeleitete Implementierung von M in Bezug auf R die gleiche wie die am
meisten abgeleitete Implementierung von in M Bezug auf die direkte Basisklasse von R .
Im folgenden Beispiel werden die Unterschiede zwischen virtuellen und nicht virtuellen Methoden
veranschaulicht:
using System;

class A
{
public void F() { Console.WriteLine("A.F"); }

public virtual void G() { Console.WriteLine("A.G"); }


}

class B: A
{
new public void F() { Console.WriteLine("B.F"); }

public override void G() { Console.WriteLine("B.G"); }


}

class Test
{
static void Main() {
B b = new B();
A a = b;
a.F();
b.F();
a.G();
b.G();
}
}

Im Beispiel wird A eine nicht virtuelle Methode F und eine virtuelle Methode eingeführt G . Die-Klasse B
führt eine neue nicht virtuelle Methode ein F , wodurch die geerbte-Methode ausgeblendet F wird, und
überschreibt auch die geerbte-Methode G . Das Beispiel erzeugt die Ausgabe:

A.F
B.F
B.G
B.G

Beachten Sie, dass die-Anweisung aufruft a.G() B.G , nicht A.G . Dies liegt daran, dass der Lauf Zeittyp der-
Instanz (d. h B .), nicht der Kompilier Zeittyp der-Instanz (d A . h.), die tatsächliche Methoden Implementierung
bestimmt, die aufgerufen werden soll.
Da Methoden vererbte Methoden ausblenden dürfen, kann eine Klasse mehrere virtuelle Methoden mit der
gleichen Signatur enthalten. Dies stellt kein mehrdeutigkeitsproblem dar, da alle außer der am weitesten
abgeleiteten Methode ausgeblendet sind. Im Beispiel
using System;

class A
{
public virtual void F() { Console.WriteLine("A.F"); }
}

class B: A
{
public override void F() { Console.WriteLine("B.F"); }
}

class C: B
{
new public virtual void F() { Console.WriteLine("C.F"); }
}

class D: C
{
public override void F() { Console.WriteLine("D.F"); }
}

class Test
{
static void Main() {
D d = new D();
A a = d;
B b = d;
C c = d;
a.F();
b.F();
c.F();
d.F();
}
}

die C D Klassen und enthalten zwei virtuelle Methoden mit der gleichen Signatur: die, die von eingeführt
wurde, A und die, die von eingeführt wird C . Die durch eingeführte Methode C blendet die von geerbte
Methode aus A . Folglich überschreibt die Überschreibungs Deklaration in D die von eingeführte Methode C
, und es ist nicht möglich, D die durch eingeführte Methode zu überschreiben A . Das Beispiel erzeugt die
Ausgabe:

B.F
B.F
D.F
D.F

Beachten Sie, dass es möglich ist, die verborgene virtuelle Methode aufzurufen, indem Sie auf eine Instanz von
D über einen weniger abgeleiteten Typ zugreifen, in dem die Methode nicht ausgeblendet ist.

Überschreibungs Methoden
Wenn eine Instanzmethodendeklaration einen- override Modifizierer enthält, wird die-Methode als
Überschreibungs Methode bezeichnet. Eine Überschreibungs Methode überschreibt eine geerbte virtuelle
Methode mit derselben Signatur. Während eine Deklaration einer virtuellen Methode eine neue Methode
einführt, spezialisiert eine Deklaration einer überschriebenen Methode eine vorhandene geerbte virtuelle
Methode, indem eine neue Implementierung dieser Methode bereitgestellt wird.
Die Methode, die durch eine- override Deklaration überschrieben wird, wird als die überschriebene Basis
Methode bezeichnet. Bei einer M in einer Klasse deklarierten Überschreibungs Methode C wird die
überschriebene Basis Methode bestimmt, indem jeder Basis Klassentyp von untersucht wird C , beginnend mit
dem direkten Basis Klassentyp von C und mit jedem aufeinander folgenden direkten Basis Klassentyp, bis in
einem gegebenen Basis Klassentyp mindestens eine barrierefreie Methode gefunden wird, die dieselbe Signatur
wie M nach der Ersetzung von Typargumenten aufweist. Zum Ermitteln der überschriebenen Basis Methode
wird eine Methode als verfügbar betrachtet, wenn dies der Fall ist, wenn dies der Fall ist, public protected
protected internal oder wenn Sie internal in demselben Programm wie deklariert und deklariert wird C .

Ein Kompilierzeitfehler tritt auf, es sei denn, für eine Überschreibungs Deklaration gilt Folgendes:
Eine überschriebene Basis Methode kann wie oben beschrieben gefunden werden.
Es gibt genau eine solche überschriebene Basis Methode. Diese Einschränkung hat nur Auswirkungen, wenn
der Basis Klassentyp ein konstruierter Typ ist, bei dem durch die Ersetzung von Typargumenten die Signatur
zweier Methoden identisch ist.
Die überschriebene Basis Methode ist eine virtuelle Methode, eine abstrakte Methode oder eine
Überschreibungs Methode. Anders ausgedrückt: die überschriebene Basis Methode kann nicht statisch oder
nicht virtuell sein.
Die überschriebene Basis Methode ist keine versiegelte Methode.
Die Überschreibungs Methode und die überschriebene Basis Methode haben denselben Rückgabetyp.
Die Überschreibungs Deklaration und die überschriebene Basis Methode haben dieselbe deklarierte
Zugriffsmethode. Anders ausgedrückt: eine Überschreibungs Deklaration kann nicht den Zugriff auf die
virtuelle Methode ändern. Wenn die überschriebene Basis Methode jedoch intern geschützt ist und in einer
anderen Assembly als der Assembly deklariert ist, die die Überschreibungs Methode enthält, muss die
deklarierte Barrierefreiheit der Überschreibungs Methode geschützt werden.
Die Überschreibungs Deklaration gibt keine Type-Parameter-Einschränkungs Klauseln an. Stattdessen werden
die Einschränkungen von der überschriebenen Basis Methode geerbt. Beachten Sie, dass Einschränkungen,
die Typparameter in der überschriebenen Methode sind, durch Typargumente in der geerbten Einschränkung
ersetzt werden können. Dies kann zu Einschränkungen führen, die nicht zulässig sind, wenn Sie explizit
angegeben werden, z. b. Werttypen oder versiegelte Typen.
Im folgenden Beispiel wird veranschaulicht, wie die über schreibenden Regeln für generische Klassen
funktionieren:

abstract class C<T>


{
public virtual T F() {...}
public virtual C<T> G() {...}
public virtual void H(C<T> x) {...}
}

class D: C<string>
{
public override string F() {...} // Ok
public override C<string> G() {...} // Ok
public override void H(C<T> x) {...} // Error, should be C<string>
}

class E<T,U>: C<U>


{
public override U F() {...} // Ok
public override C<U> G() {...} // Ok
public override void H(C<T> x) {...} // Error, should be C<U>
}

Eine Überschreibungs Deklaration kann mithilfe eines base_access (Basis Zugriff) auf die überschriebene Basis
Methode zugreifen. Im Beispiel
class A
{
int x;

public virtual void PrintFields() {


Console.WriteLine("x = {0}", x);
}
}

class B: A
{
int y;

public override void PrintFields() {


base.PrintFields();
Console.WriteLine("y = {0}", y);
}
}

der base.PrintFields() Aufruf in B Ruft die PrintFields in deklarierte Methode auf A . Ein base_access
deaktiviert den virtuellen Aufruf Mechanismus und behandelt die Basis Methode einfach als nicht virtuelle
Methode. Wenn der Aufruf von B geschrieben wurde ((A)this).PrintFields() , würde er die in deklarierte
Methode rekursiv aufrufen, PrintFields B nicht die in deklarierte Methode A , da PrintFields virtuell ist
und der Lauf Zeittyp von ((A)this) ist B .
Nur durch das Einschließen eines- override Modifizierers kann eine Methode eine andere Methode
überschreiben. In allen anderen Fällen verbirgt eine Methode, die dieselbe Signatur wie eine geerbte Methode
hat, einfach die geerbte Methode. Im Beispiel

class A
{
public virtual void F() {}
}

class B: A
{
public virtual void F() {} // Warning, hiding inherited F()
}

die -Methode in B enthält keinen override -Modifizierer und überschreibt daher nicht die- F Methode in
F
A . Stattdessen verbirgt die F -Methode in B die-Methode in A , und es wird eine Warnung ausgegeben, da
die Deklaration keinen new Modifizierer enthält.
Im Beispiel

class A
{
public virtual void F() {}
}

class B: A
{
new private void F() {} // Hides A.F within body of B
}

class C: B
{
public override void F() {} // Ok, overrides A.F
}
die- F Methode in B F blendet die von geerbte virtuelle Methode aus A . Da das neue F in B privaten
Zugriff hat, umfasst sein Bereich nur den Klassen Text von B und wird nicht auf erweitert C . Daher ist es
zulässig, die Deklaration von F in C zu überschreiben, das F von geerbt wurde A .
Versiegelte Methoden
Wenn eine Instanzmethodendeklaration einen- sealed Modifizierer enthält, wird diese Methode als
versiegelte Methode bezeichnet. Wenn eine Instanzmethodendeklaration den- sealed Modifizierer enthält,
muss Sie auch den- override Modifizierer einschließen. Die Verwendung des- sealed Modifizierers verhindert,
dass eine abgeleitete Klasse die-Methode weiter überschreibt.
Im Beispiel

using System;

class A
{
public virtual void F() {
Console.WriteLine("A.F");
}

public virtual void G() {


Console.WriteLine("A.G");
}
}

class B: A
{
sealed override public void F() {
Console.WriteLine("B.F");
}

override public void G() {


Console.WriteLine("B.G");
}
}

class C: B
{
override public void G() {
Console.WriteLine("C.G");
}
}

die-Klasse B stellt zwei Überschreibungs Methoden bereit: eine F Methode, die über den sealed -
Modifizierer und eine- G Methode verfügt, die nicht. B die Verwendung des versiegelten verhindert, dass
weiter überschrieben wird modifier C F .
Abstrakte Methoden
Wenn eine Instanzmethodendeklaration einen- abstract Modifizierer enthält, wird diese Methode als
abstrakte Methode bezeichnet. Obwohl eine abstrakte Methode implizit auch eine virtuelle Methode ist, kann
Sie nicht den-Modifizierer aufweisen virtual .
Eine abstrakte Methoden Deklaration führt eine neue virtuelle Methode ein, stellt jedoch keine Implementierung
dieser Methode bereit. Stattdessen sind nicht abstrakte abgeleitete Klassen erforderlich, um eine eigene
Implementierung bereitzustellen, indem diese Methode überschrieben wird. Da eine abstrakte Methode keine
tatsächliche Implementierung bereitstellt, besteht die method_body einer abstrakten Methode einfach aus
einem Semikolon.
Abstrakte Methoden Deklarationen sind nur in abstrakten Klassen zulässig (abstrakte Klassen).
Im Beispiel

public abstract class Shape


{
public abstract void Paint(Graphics g, Rectangle r);
}

public class Ellipse: Shape


{
public override void Paint(Graphics g, Rectangle r) {
g.DrawEllipse(r);
}
}

public class Box: Shape


{
public override void Paint(Graphics g, Rectangle r) {
g.DrawRect(r);
}
}

die- Shape Klasse definiert das abstrakte Konzept eines geometrischen Shape-Objekts, das sich selbst zeichnen
kann. Die Paint Methode ist abstrakt, da keine sinnvolle Standard Implementierung vorhanden ist. Die
Ellipse Box Klassen und sind konkrete Shape Implementierungen. Da diese Klassen nicht abstrakt sind,
müssen Sie die-Methode überschreiben Paint und eine tatsächliche Implementierung bereitstellen.
Es handelt sich um einen Kompilierzeitfehler für einen base_access (Basis Zugriff), der auf eine abstrakte
Methode verweist. Im Beispiel

abstract class A
{
public abstract void F();
}

class B: A
{
public override void F() {
base.F(); // Error, base.F is abstract
}
}

für den Aufruf wird ein Kompilierzeitfehler gemeldet, base.F() weil er auf eine abstrakte Methode verweist.
Eine abstrakte Methoden Deklaration darf eine virtuelle Methode überschreiben. Dadurch kann eine abstrakte
Klasse die erneute Implementierung der Methode in abgeleiteten Klassen erzwingen und die ursprüngliche
Implementierung der Methode nicht verfügbar machen. Im Beispiel
using System;

class A
{
public virtual void F() {
Console.WriteLine("A.F");
}
}

abstract class B: A
{
public abstract override void F();
}

class C: B
{
public override void F() {
Console.WriteLine("C.F");
}
}

die-Klasse A deklariert eine virtuelle Methode, die-Klasse B überschreibt diese Methode mit einer abstrakten-
Methode, und die-Klasse C überschreibt die abstrakte-Methode, um eine eigene Implementierung
bereitzustellen.
Externe Methoden
Wenn eine Methoden Deklaration einen extern Modifizierer enthält, wird diese Methode als *externe
Methode _ bezeichnet. Externe Methoden werden extern implementiert, in der Regel mit einer anderen Sprache
als c#. Da eine externe Methoden Deklaration keine tatsächliche Implementierung bereitstellt, besteht die
_method_body * einer externen Methode einfach aus einem Semikolon. Eine externe Methode darf nicht
generisch sein.
Der- extern Modifizierer wird in der Regel in Verbindung mit einem- DllImport Attribut (Interoperation mit
com-und Win32-Komponenten) verwendet, sodass externe Methoden durch DLLs (Dynamic Link Libraries)
implementiert werden können. Die Ausführungsumgebung unterstützt möglicherweise andere Mechanismen,
bei denen Implementierungen externer Methoden bereitgestellt werden können.
Wenn eine externe Methode ein- DllImport Attribut enthält, muss die Methoden Deklaration auch einen-
static Modifizierer enthalten. Dieses Beispiel veranschaulicht die Verwendung des extern -Modifizierers und
des- DllImport Attributs:

using System.Text;
using System.Security.Permissions;
using System.Runtime.InteropServices;

class Path
{
[DllImport("kernel32", SetLastError=true)]
static extern bool CreateDirectory(string name, SecurityAttribute sa);

[DllImport("kernel32", SetLastError=true)]
static extern bool RemoveDirectory(string name);

[DllImport("kernel32", SetLastError=true)]
static extern int GetCurrentDirectory(int bufSize, StringBuilder buf);

[DllImport("kernel32", SetLastError=true)]
static extern bool SetCurrentDirectory(string name);
}
Partielle Methoden (Recap)
Wenn eine Methoden Deklaration einen- partial Modifizierer enthält, wird diese Methode als par tielle
Methode bezeichnet. Partielle Methoden können nur als Member von partiellen Typen (partielle Typen)
deklariert werden und unterliegen einer Reihe von Einschränkungen. Partielle Methoden werden weiter unten in
partiellen Methodenbeschrieben.
Erweiterungsmethoden
Wenn der erste Parameter einer Methode den- this Modifizierer enthält, wird diese Methode als
Er weiterungsmethode bezeichnet. Erweiterungs Methoden können nur in nicht generischen, nicht in der
Tabelle statischen Klassen deklariert werden. Der erste Parameter einer Erweiterungsmethode kann keine
anderen Modifizierer als aufweisen this , und der Parametertyp darf kein Zeigertyp sein.
Im folgenden finden Sie ein Beispiel für eine statische Klasse, die zwei Erweiterungs Methoden deklariert:

public static class Extensions


{
public static int ToInt32(this string s) {
return Int32.Parse(s);
}

public static T[] Slice<T>(this T[] source, int index, int count) {
if (index < 0 || count < 0 || source.Length - index < count)
throw new ArgumentException();
T[] result = new T[count];
Array.Copy(source, index, result, 0, count);
return result;
}
}

Eine Erweiterungsmethode ist eine reguläre statische Methode. Außerdem kann eine Erweiterungsmethode mit
instanzmethodenaufruf-Syntax (Erweiterungs Methodenaufrufe) mithilfe des Empfänger Ausdrucks als erstes
Argument aufgerufen werden, wenn sich Ihre einschließende statische Klasse im Gültigkeitsbereich befindet.
Im folgenden Programm werden die oben deklarierten Erweiterungs Methoden verwendet:

static class Program


{
static void Main() {
string[] strings = { "1", "22", "333", "4444" };
foreach (string s in strings.Slice(1, 2)) {
Console.WriteLine(s.ToInt32());
}
}
}

Die Slice -Methode ist in verfügbar string[] , und die- ToInt32 Methode ist in verfügbar string , da Sie als
Erweiterungs Methoden deklariert wurden. Die Bedeutung des Programms ist mit den folgenden statischen
Methoden aufrufen identisch:

static class Program


{
static void Main() {
string[] strings = { "1", "22", "333", "4444" };
foreach (string s in Extensions.Slice(strings, 1, 2)) {
Console.WriteLine(Extensions.ToInt32(s));
}
}
}
Methoden Text
Der method_body einer Methoden Deklaration besteht aus einem Block Körper, einem Ausdrucks Körper oder
einem Semikolon.
Der Ergebnistyp einer Methode ist void , wenn der Rückgabetyp ist void , oder wenn die Methode Async ist
und der Rückgabetyp ist System.Threading.Tasks.Task . Andernfalls ist der Ergebnistyp einer nicht Async-
Methode der Rückgabetyp, und der Ergebnistyp einer Async-Methode mit dem Rückgabetyp
System.Threading.Tasks.Task<T> ist T .

Wenn eine Methode über einen void Ergebnistyp und einen Block Text verfügt, ist return es nicht zulässig,
Anweisungen (die Return-Anweisung) im-Block anzugeben. Wenn die Ausführung des Blocks einer void-
Methode normal abgeschlossen ist (d. h., der Ablauf der Steuerelemente wird vom Ende des Methoden Texts
entfernt), kehrt diese Methode einfach an den aktuellen Aufrufer zurück.
Wenn eine Methode über ein void Ergebnis und einen Ausdrucks Text verfügt, muss der Ausdruck E eine
statement_expression sein, und der Text entspricht genau einem Block Text des Formulars { E; } .
Wenn eine Methode einen nicht leeren Ergebnistyp und einen Block Text hat, return muss jede Anweisung im
Block einen Ausdruck angeben, der implizit in den Ergebnistyp konvertiert werden kann. Der Endpunkt eines
Block Texts einer Wert Rückgabe Methode darf nicht erreichbar sein. Anders ausgedrückt: in einer Rückgabe
Methode mit einem Block Text ist das Steuerelement nicht berechtigt, das Ende des Methoden Texts zu
übertragen.
Wenn eine Methode einen nicht leeren Ergebnistyp und einen Ausdrucks Körper aufweist, muss der Ausdruck
implizit in den Ergebnistyp konvertiert werden, und der Text entspricht genau dem Block Text des Formulars
{ return E; } .

Im Beispiel

class A
{
public int F() {} // Error, return value required

public int G() {


return 1;
}

public int H(bool b) {


if (b) {
return 1;
}
else {
return 0;
}
}

public int I(bool b) => b ? 1 : 0;


}

die Wert Rückgabe F Methode führt zu einem Kompilierzeitfehler, da das Steuerelement vom Ende des
Methoden Texts entfernt werden kann. Die G -Methode und die- H Methode sind korrekt, da alle möglichen
Ausführungs Pfade in einer Return-Anweisung enden, die einen Rückgabewert angibt. Die I Methode ist
korrekt, da Ihr Text einem Anweisungsblock mit nur einer einzigen return-Anweisung entspricht.
Methodenüberladung
Die Regeln zur Auflösung von Methoden Überladungen werden unter Typrückschlussbeschrieben.

Eigenschaften
Eine *Proper ty _ ist ein Member, der den Zugriff auf ein Merkmal eines Objekts oder einer Klasse ermöglicht.
Beispiele für Eigenschaften sind die Länge einer Zeichenfolge, die Größe einer Schriftart, die Beschriftung eines
Fensters, der Name eines Kunden usw. Eigenschaften sind eine natürliche Erweiterung von Feldern – beide sind
benannte Member mit zugeordneten Typen, und die Syntax für den Zugriff auf Felder und Eigenschaften ist
identisch. Im Gegensatz zu Feldern bezeichnen Eigenschaften jedoch keine Speicherorte. Stattdessen verfügen
Eigenschaften über _ *Accessoren**, die die auszuführenden Anweisungen angeben, wenn ihre Werte gelesen
oder geschrieben werden. Eigenschaften bieten somit einen Mechanismus zum Zuordnen von Aktionen zum
Lesen und Schreiben der Attribute eines Objekts. Außerdem können solche Attribute berechnet werden.
Eigenschaften werden mit property_declaration s deklariert:

property_declaration
: attributes? property_modifier* type member_name property_body
;

property_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| property_modifier_unsafe
;

property_body
: '{' accessor_declarations '}' property_initializer?
| '=>' expression ';'
;

property_initializer
: '=' variable_initializer ';'
;

Eine property_declaration kann eine Reihe von Attributen (Attribute) und eine gültige Kombination der vier
Zugriffsmodifizierer (Zugriffsmodifizierer), ( new der neuen Modifizierer), static (statische und
Instanzmethoden), virtual (virtuelle Methoden), override (Überschreibungs Methoden), sealed (versiegelte
Methoden), abstract (abstrakte Methoden) und extern (Externe Methoden)
Eigenschafts Deklarationen unterliegen den gleichen Regeln wie Methoden Deklarationen (Methoden) in Bezug
auf gültige Kombinationen von modifiziererobjekten.
Der Typ einer Eigenschafts Deklaration gibt den Typ der Eigenschaft an, die von der Deklaration eingeführt
wurde, und der MEMBER_NAME gibt den Namen der Eigenschaft an. Wenn die Eigenschaft keine explizite
Implementierung des Schnittstellenmembers ist, ist die MEMBER_NAME einfach ein Bezeichner. Bei einer
expliziten Schnittstellenmember-Implementierung (explizite Schnittstellenmember-Implementierungen) besteht
die MEMBER_NAME aus einer INTERFACE_TYPE gefolgt von " . " und einem Bezeichner.
Der Typ einer Eigenschaft muss mindestens so zugänglich sein wie die Eigenschaft selbst (Barrierefreiheits
Einschränkungen).
Eine property_body kann entweder aus einem -**Accessor Body* _ oder einem Ausdrucks Körper bestehen. In
einem Accessor-Text _accessor_declarations *, der in die { Token "" und "" eingeschlossen werden muss } , die
Accessoren (Accessoren) der Eigenschaft. Die Accessoren geben die ausführbaren Anweisungen an, die dem
Lesen und Schreiben der Eigenschaft zugeordnet sind.
Ein Ausdrucks Text, der aus => gefolgt von einem Ausdruck E und einem Semikolon besteht, entspricht exakt
dem Anweisungs Text { get { return E; } } und kann daher nur zum Angeben von nur-Getter-Eigenschaften
verwendet werden, bei denen das Ergebnis des Getters von einem einzelnen Ausdruck angegeben wird.
Eine property_initializer kann nur für eine automatisch implementierte Eigenschaft (automatisch implementierte
Eigenschaften) angegeben werden und bewirkt die Initialisierung des zugrunde liegenden Felds dieser
Eigenschaften mit dem Wert, der vom Ausdruck angegeben wird.
Obwohl die Syntax für den Zugriff auf eine Eigenschaft mit der Syntax für ein Feld identisch ist, wird eine
Eigenschaft nicht als Variable klassifiziert. Daher ist es nicht möglich, eine Eigenschaft als-oder- ref Argument
zu übergeben out .
Wenn eine Eigenschafts Deklaration einen extern Modifizierer enthält, wird die Eigenschaft als *externe
Eigenschaft _ bezeichnet. Da eine externe Eigenschaften Deklaration keine tatsächliche Implementierung
bereitstellt, besteht jede Ihrer _accessor_declarations * aus einem Semikolon.
Statische Eigenschaften und Instanzeigenschaften
Wenn eine Eigenschafts Deklaration einen static Modifizierer enthält, wird die Eigenschaft als *statische
Eigenschaft _ bezeichnet. Wenn kein static Modifizierer vorhanden ist, wird die Eigenschaft als _
-Instanzeigenschaft * bezeichnet.
Eine statische Eigenschaft ist keiner bestimmten Instanz zugeordnet, und es handelt sich um einen
Kompilierzeitfehler, auf this den in den Accessoren einer statischen Eigenschaft verwiesen wird.
Eine Instanzeigenschaft ist einer bestimmten Instanz einer Klasse zugeordnet, und auf diese Instanz kann als
this (dieser Zugriff) in den Accessoren dieser Eigenschaft zugegriffen werden.

Wenn auf eine Eigenschaft in einem member_access verwiesen wird (Member-Zugriff) E.M , wenn M eine
statische Eigenschaft ist, E muss einen Typ angeben M , der enthält, und wenn M eine Instanzeigenschaft ist,
muss E eine Instanz eines Typs angeben, der enthält M .
Die Unterschiede zwischen statischen und Instanzmembern werden in statischen und
Instanzmembernausführlicher erläutert.
Accessoren
Der accessor_declarations einer Eigenschaft gibt die ausführbaren Anweisungen an, die dem Lesen und
Schreiben dieser Eigenschaft zugeordnet sind.
accessor_declarations
: get_accessor_declaration set_accessor_declaration?
| set_accessor_declaration get_accessor_declaration?
;

get_accessor_declaration
: attributes? accessor_modifier? 'get' accessor_body
;

set_accessor_declaration
: attributes? accessor_modifier? 'set' accessor_body
;

accessor_modifier
: 'protected'
| 'internal'
| 'private'
| 'protected' 'internal'
| 'internal' 'protected'
;

accessor_body
: block
| ';'
;

Die Accessordeklarationen bestehen aus einer get_accessor_declaration, einem set_accessor_declaration oder


beidem. Jede Accessordeklaration besteht aus dem Token get oder set gefolgt von einem optionalen
accessor_modifier und einem accessor_body.
Die Verwendung von accessor_modifier s unterliegt den folgenden Einschränkungen:
Eine accessor_modifier kann nicht in einer Schnittstelle oder in einer expliziten Schnittstellenmember-
Implementierung verwendet werden.
Für eine Eigenschaft oder einen Indexer, der über keinen override Modifizierer verfügt, ist ein
accessor_modifier nur zulässig, wenn die Eigenschaft oder der Indexer sowohl einen get -Accessor als auch
einen- set Accessor aufweist und dann nur für einen dieser Accessoren zulässig ist.
Für eine Eigenschaft oder einen Indexer, der einen- override Modifizierer enthält, muss ein-Accessor mit
dem accessor_modifier(sofern vorhanden) des Accessors, der überschrieben wird, abgleichen.
Der accessor_modifier muss eine Barrierefreiheit deklarieren, die strikt restriktiver ist als der deklarierte
Zugriff auf die Eigenschaft oder den Indexer selbst. Genauer gesagt:
Wenn die Eigenschaft oder der Indexer über eine deklarierte Zugriffsmöglichkeit von verfügt public ,
kann die accessor_modifier entweder protected internal , internal , protected oder sein private .
Wenn die Eigenschaft oder der Indexer über eine deklarierte Zugriffsmöglichkeit von verfügt
protected internal , kann die accessor_modifier entweder internal , protected oder sein private .
Wenn die Eigenschaft oder der Indexer eine deklarierte Zugriffsmöglichkeit von internal oder
aufweist protected , muss die accessor_modifier sein private .
Wenn die Eigenschaft oder der Indexer über eine deklarierte Zugriffsmöglichkeit von verfügt private
, kann keine accessor_modifier verwendet werden.
Für abstract -und- extern Eigenschaften ist die accessor_body für jeden angegebenen Accessor einfach ein
Semikolon. Eine nicht abstrakte, nicht externe Eigenschaft kann jede accessor_body ein Semikolon sein. in
diesem Fall handelt es sich um eine *automatisch implementier te Eigenschaft _ (automatisch
implementierte Eigenschaften). Eine automatisch implementierte Eigenschaft muss mindestens über einen get-
Accessor verfügen. Für die Accessoren anderer nicht abstrakter, nicht externer Eigenschaften ist der
_accessor_body * ein- Block , der die auszuführenden Anweisungen angibt, wenn der entsprechende-Accessor
aufgerufen wird.
Ein- get Accessor entspricht einer Parameter losen Methode mit einem Rückgabewert des Eigenschaftentyps.
Wenn in einem Ausdruck auf eine Eigenschaft verwiesen wird, wird der- get Accessor der Eigenschaft
aufgerufen, um den Wert der-Eigenschaft (Werte von Ausdrücken) zu berechnen, außer als Ziel einer
Zuweisung. Der Text einer get -Zugriffsmethode muss den Regeln für die Rückgabe von Werten entsprechen,
die im Methoden Textbeschrieben werden. Insbesondere return müssen alle Anweisungen im Textkörper einer-
get Zugriffsmethode einen Ausdruck angeben, der implizit in den Eigenschaftentyp konvertiert werden kann.
Außerdem darf der Endpunkt einer- get Zugriffsmethode nicht erreichbar sein.
Ein set -Accessor entspricht einer Methode mit einem einzelnen Wert Parameter des Eigenschaftentyps und
einem void Rückgabetyp. Der implizite Parameter einer- set Zugriffsmethode wird immer benannt value .
Wenn auf eine Eigenschaft als Ziel einer Zuweisung (Zuweisungs Operatoren) verwiesen wird, oder als Operand
von ++ oder -- (postfix Inkrement-und Dekrementoperatoren, Präfix Inkrement-und Dekrementoperatoren),
wird der- set Accessor mit einem Argument aufgerufen (dessen Wert der rechten Seite der Zuweisung oder
dem Operanden des OR-Operators entspricht), der ++ -- den neuen Wert (einfache Zuweisung) bereitstellt.
Der Text einer- set Zugriffsmethode muss den Regeln für Methoden entsprechen, die void im Methoden
Textbeschrieben werden. Insbesondere können return Anweisungen im set Accessor-Text keinen Ausdruck
angeben. Da ein- set Accessor implizit einen Parameter mit dem Namen aufweist value , handelt es sich um
einen Kompilierzeitfehler für eine lokale Variable oder eine Konstante Deklaration in einem- set Accessor, der
über diesen Namen verfügt.
Basierend auf dem vorhanden sein oder Fehlen der get -und- set Accessoren wird eine Eigenschaft wie folgt
klassifiziert:
Eine Eigenschaft, die sowohl einen get -Accessor als auch einen- set Accessor enthält, wird als Lese-
/Schreib-Eigenschaft bezeichnet.
Eine Eigenschaft, die nur einen- get Accessor aufweist, wird als schreibgeschützte Eigenschaft bezeichnet. Es
handelt sich um einen Kompilierzeitfehler für eine schreibgeschützte Eigenschaft, die das Ziel einer
Zuweisung ist.
Eine Eigenschaft, die nur einen- set Accessor aufweist, wird als Schreib geschützte Eigenschaft bezeichnet.
Außer als Ziel einer Zuweisung ist es ein Kompilierzeitfehler, der auf eine schreibgeschützte Eigenschaft in
einem Ausdruck verweist.
Im Beispiel

public class Button: Control


{
private string caption;

public string Caption {


get {
return caption;
}
set {
if (caption != value) {
caption = value;
Repaint();
}
}
}

public override void Paint(Graphics g, Rectangle r) {


// Painting code goes here
}
}

Das- Button Steuerelement deklariert eine öffentliche Caption Eigenschaft. Der- get Accessor der- Caption
Eigenschaft gibt die Zeichenfolge zurück, die im privaten Feld gespeichert ist caption . Der set -Accessor
überprüft, ob sich der neue Wert vom aktuellen Wert unterscheidet. wenn dies der Fall ist, wird der neue Wert
gespeichert und das Steuerelement neu gezeichnet. Eigenschaften folgen häufig dem oben gezeigten Muster:
der get Accessor gibt einfach einen in einem privaten Feld gespeicherten Wert zurück, und der set Accessor
ändert dieses private Feld und führt dann alle zusätzlichen Aktionen aus, die erforderlich sind, um den Status
des Objekts vollständig zu aktualisieren.
Bei der Button obigen Klasse ist Folgendes ein Beispiel für die Verwendung der- Caption Eigenschaft:

Button okButton = new Button();


okButton.Caption = "OK"; // Invokes set accessor
string s = okButton.Caption; // Invokes get accessor

Hier wird der- set Accessor aufgerufen, indem der-Eigenschaft ein Wert zugewiesen wird, und der- get
Accessor wird aufgerufen, indem auf die-Eigenschaft in einem Ausdruck verwiesen wird.
Die get -und- set Accessoren einer Eigenschaft sind keine unterschiedlichen Member, und es ist nicht
möglich, die Accessoren einer Eigenschaft separat zu deklarieren. Daher ist es nicht möglich, dass die beiden
Accessoren einer Eigenschaft mit Lese-/Schreibzugriff über unterschiedliche Zugriffsberechtigungen verfügen.
Das Beispiel

class A
{
private string name;

public string Name { // Error, duplicate member name


get { return name; }
}

public string Name { // Error, duplicate member name


set { name = value; }
}
}

deklariert keine einzige Lese-/Schreibeigenschaft. Stattdessen werden zwei Eigenschaften mit demselben
Namen deklariert, ein Schreib geschützter und ein Schreib geschützter. Da zwei Member, die in derselben Klasse
deklariert werden, nicht denselben Namen haben können, bewirkt das Beispiel, dass ein Kompilierzeitfehler
auftritt.
Wenn eine abgeleitete Klasse eine Eigenschaft mit demselben Namen wie eine geerbte Eigenschaft deklariert,
verbirgt die abgeleitete Eigenschaft die geerbte Eigenschaft in Bezug auf Lese-und Schreibvorgänge. Im Beispiel

class A
{
public int P {
set {...}
}
}

class B: A
{
new public int P {
get {...}
}
}

die- P Eigenschaft in blendet B die- P Eigenschaft in in A Bezug auf das Lesen und schreiben aus. Folglich in
den Anweisungen
B b = new B();
b.P = 1; // Error, B.P is read-only
((A)b).P = 1; // Ok, reference to A.P

die Zuweisung von b.P bewirkt, dass ein Kompilierzeitfehler gemeldet wird, da die schreibgeschützte P
Eigenschaft in B die schreibgeschützte P Eigenschaft in A ausblendet. Beachten Sie jedoch, dass eine
Umwandlung verwendet werden kann, um auf die Hidden- P Eigenschaft zuzugreifen.
Im Gegensatz zu öffentlichen Feldern stellen Eigenschaften eine Trennung zwischen dem internen Zustand eines
Objekts und seiner öffentlichen Schnittstelle dar. Beachten Sie folgendes Beispiel:

class Label
{
private int x, y;
private string caption;

public Label(int x, int y, string caption) {


this.x = x;
this.y = y;
this.caption = caption;
}

public int X {
get { return x; }
}

public int Y {
get { return y; }
}

public Point Location {


get { return new Point(x, y); }
}

public string Caption {


get { return caption; }
}
}

Hier verwendet die Label -Klasse zwei int Felder, x und y , um ihren Speicherort zu speichern. Der
Speicherort wird sowohl als als X auch als Eigenschaft Y Location des Typs öffentlich verfügbar gemacht
Point . Wenn es in einer zukünftigen Version von Label bequemer wird, den Speicherort als intern zu
speichern Point , kann die Änderung vorgenommen werden, ohne dass sich dies auf die öffentliche
Schnittstelle der Klasse auswirkt:
class Label
{
private Point location;
private string caption;

public Label(int x, int y, string caption) {


this.location = new Point(x, y);
this.caption = caption;
}

public int X {
get { return location.x; }
}

public int Y {
get { return location.y; }
}

public Point Location {


get { return location; }
}

public string Caption {


get { return caption; }
}
}

Waren x und y stattdessen public readonly Felder, wäre es unmöglich, eine solche Änderung an der- Label
Klasse vorzunehmen.
Das verfügbar machen des Zustands durch Eigenschaften ist nicht notwendigerweise weniger effizient, als
Felder direkt verfügbar zu machen. Insbesondere wenn eine Eigenschaft nicht virtuell ist und nur eine kleine
Menge an Code enthält, kann die Ausführungsumgebung Aufrufe von Accessoren durch den tatsächlichen Code
der Accessoren ersetzen. Dieser Prozess wird als Inlining bezeichnet und macht den Eigenschafts Zugriff so
effizient wie der Feld Zugriff, behält jedoch die höhere Flexibilität von Eigenschaften bei.
Da das Aufrufen eines get Accessoren konzeptionell Äquivalent zum Lesen des Werts eines Felds ist, gilt es als
ungültiges Programmier Format für get Accessoren, um Observable-Nebeneffekte zu haben. Im Beispiel

class Counter
{
private int next;

public int Next {


get { return next++; }
}
}

der Wert der- Next Eigenschaft hängt von der Häufigkeit ab, mit der zuvor auf die Eigenschaft zugegriffen
wurde. Folglich erzeugt der Zugriff auf die-Eigenschaft einen beobachtbaren Nebeneffekt, und die-Eigenschaft
sollte stattdessen als Methode implementiert werden.
Die "No Side-Effects"-Konvention für get Accessoren bedeutet nicht, dass get Accessoren immer so
geschrieben werden müssen, dass Sie einfach in Feldern gespeicherte Werte zurückgeben. get Accessoren
berechnen häufig den Wert einer Eigenschaft, indem Sie auf mehrere Felder zugreifen oder Methoden aufrufen.
Ein ordnungsgemäß entworfener get Accessor führt jedoch keine Aktionen aus, die Observable-Änderungen
im Status des Objekts bewirken.
Eigenschaften können verwendet werden, um die Initialisierung einer Ressource zu verzögern, bis zu dem
Zeitpunkt, zu dem Sie erstmals referenziert wird. Beispiel:
using System.IO;

public class Console


{
private static TextReader reader;
private static TextWriter writer;
private static TextWriter error;

public static TextReader In {


get {
if (reader == null) {
reader = new StreamReader(Console.OpenStandardInput());
}
return reader;
}
}

public static TextWriter Out {


get {
if (writer == null) {
writer = new StreamWriter(Console.OpenStandardOutput());
}
return writer;
}
}

public static TextWriter Error {


get {
if (error == null) {
error = new StreamWriter(Console.OpenStandardError());
}
return error;
}
}
}

Die Console -Klasse enthält die drei Eigenschaften,, In Out und Error , die jeweils die standardmäßigen
Eingabe-, Ausgabe-und Fehler Geräte darstellen. Wenn diese Member als Eigenschaften verfügbar gemacht
werden, kann die- Console Klasse Ihre Initialisierung verzögern, bis Sie tatsächlich verwendet werden.
Beispielsweise nach dem ersten Verweis auf die- Out Eigenschaft, wie in

Console.Out.WriteLine("hello, world");

der zugrunde liegende TextWriter für das Ausgabegerät wird erstellt. Wenn die Anwendung jedoch keinen
Verweis auf die-Eigenschaft und die-Eigenschaft durchführt In Error , werden für diese Geräte keine Objekte
erstellt.
Automatisch implementierte Eigenschaften
Eine automatisch implementierte Eigenschaft (oder Auto-Eigenschaft für Short) ist eine nicht abstrakte nicht-
externe Eigenschaft mit nur Semikolon-Zugriffsmethoden. Auto-Eigenschaften müssen über einen get-Accessor
verfügen und optional über einen Set-Accessor verfügen.
Wenn eine Eigenschaft als automatisch implementierte Eigenschaft angegeben wird, ist ein verborgenes
dahinter liegendes Feld für die Eigenschaft automatisch verfügbar, und die Accessoren werden implementiert,
um aus dem dahinter liegenden Feld zu lesen und in dieses zu schreiben. Wenn die Auto-Eigenschaft keinen Set-
Accessor aufweist, wird das Unterstützungs Feld als (schreibgeschützte readonly Felder) betrachtet. Genau wie
ein readonly Feld kann auch eine nur-Getter-Auto-Eigenschaft im Text eines Konstruktors der einschließenden
Klasse zugewiesen werden. Eine solche Zuweisung wird direkt dem schreibgeschützten Unterstützungs Feld der-
Eigenschaft zugewiesen.
Eine Auto-Eigenschaft kann optional über eine property_initializer verfügen, die direkt auf das dahinter liegende
Feld als variable_initializer (Variableninitialisierer) angewendet wird.
Im Beispiel unten geschieht Folgendes:

public class Point {


public int X { get; set; } = 0;
public int Y { get; set; } = 0;
}

entspricht der folgenden Deklaration:

public class Point {


private int __x = 0;
private int __y = 0;
public int X { get { return __x; } set { __x = value; } }
public int Y { get { return __y; } set { __y = value; } }
}

Im Beispiel unten geschieht Folgendes:

public class ReadOnlyPoint


{
public int X { get; }
public int Y { get; }
public ReadOnlyPoint(int x, int y) { X = x; Y = y; }
}

entspricht der folgenden Deklaration:

public class ReadOnlyPoint


{
private readonly int __x;
private readonly int __y;
public int X { get { return __x; } }
public int Y { get { return __y; } }
public ReadOnlyPoint(int x, int y) { __x = x; __y = y; }
}

Beachten Sie, dass die Zuweisungen zum schreibgeschützten Feld zulässig sind, da Sie im Konstruktor auftreten.
Zugriff
Wenn ein Accessor über eine accessor_modifier verfügt, wird die Zugriffs Domäne (Barrierefreiheits Domänen)
der Zugriffsmethode mithilfe der deklarierten Barrierefreiheit des accessor_modifier bestimmt. Wenn ein
Accessor keine accessor_modifier hat, wird die Zugriffs Domäne des Accessors anhand der deklarierten
Zugriffsmethode der Eigenschaft oder des Indexers bestimmt.
Das vorhanden sein eines accessor_modifier hat niemals Auswirkungen auf die Suche nach Membern
(Operatoren) oder Überladungs Auflösung (Überladungs Auflösung). Die Modifizierer für die Eigenschaft oder
den Indexer bestimmen unabhängig vom Kontext des Zugriffs immer, an welche Eigenschaft oder welcher
Indexer gebunden ist.
Nachdem eine bestimmte Eigenschaft oder ein Indexer ausgewählt wurde, werden die Barrierefreiheits
Domänen der beteiligten spezifischen Accessoren verwendet, um zu bestimmen, ob diese Verwendung gültig
ist:
Wenn die Verwendung als Wert (Werte von Ausdrücken) verwendet wird, get muss der Accessor
vorhanden und zugänglich sein.
Wenn die Verwendung als Ziel einer einfachen Zuweisung (einfache Zuweisung) verwendet wird, muss der
set -Accessor vorhanden und zugänglich sein.
Wenn die Verwendung als Ziel der Verbund Zuweisung (Verbund Zuweisung) oder als Ziel der ++ --
Operatoren oder (Funktionsmember0,9, Aufruf Ausdrücke) verwendet wird, get müssen die Accessoren
und der set Accessor vorhanden und zugänglich sein.

Im folgenden Beispiel wird die-Eigenschaft A.Text durch die-Eigenschaft ausgeblendet B.Text , auch in
Kontexten, in denen nur der- set Accessor aufgerufen wird. Im Gegensatz dazu kann die-Eigenschaft B.Count
nicht von der-Klasse aufgerufen M werden, sodass stattdessen die barrierefreie Eigenschaft A.Count verwendet
wird.

class A
{
public string Text {
get { return "hello"; }
set { }
}

public int Count {


get { return 5; }
set { }
}
}

class B: A
{
private string text = "goodbye";
private int count = 0;

new public string Text {


get { return text; }
protected set { text = value; }
}

new protected int Count {


get { return count; }
set { count = value; }
}
}

class M
{
static void Main() {
B b = new B();
b.Count = 12; // Calls A.Count set accessor
int i = b.Count; // Calls A.Count get accessor
b.Text = "howdy"; // Error, B.Text set accessor not accessible
string s = b.Text; // Calls B.Text get accessor
}
}

Ein Accessor, der zur Implementierung einer Schnittstelle verwendet wird, verfügt möglicherweise nicht über
eine accessor_modifier. Wenn nur ein Accessor verwendet wird, um eine Schnittstelle zu implementieren, kann
der andere Accessor mit einem accessor_modifier deklariert werden:
public interface I
{
string Prop { get; }
}

public class C: I
{
public string Prop {
get { return "April"; } // Must not have a modifier here
internal set {...} // Ok, because I.Prop has no set accessor
}
}

Virtual-, sealed-, override -und Abstract-Eigenschaftenaccessoren


Eine virtual Eigenschafts Deklaration gibt an, dass die Accessoren der Eigenschaft virtuell sind. Der- virtual
Modifizierer gilt für beide Accessoren einer Lese-/Schreibeigenschaft – es ist nicht möglich, dass nur ein
Accessor einer Eigenschaft mit Lese-/Schreibzugriff virtuell ist.
Eine abstract Eigenschafts Deklaration gibt an, dass die Accessoren der Eigenschaft virtuell sind, aber keine
tatsächliche Implementierung der Accessoren bereitstellt. Stattdessen sind nicht abstrakte abgeleitete Klassen
erforderlich, um eine eigene Implementierung für die Accessoren bereitzustellen, indem die-Eigenschaft
überschrieben wird. Da ein Accessor für eine abstrakte Eigenschaften Deklaration keine tatsächliche
Implementierung bereitstellt, besteht die accessor_body einfach aus einem Semikolon.
Eine Eigenschafts Deklaration, die den abstract - override Modifizierer und den-Modifizierer enthält, gibt an,
dass die Eigenschaft abstrakt ist, und überschreibt Die Accessoren einer solchen Eigenschaft sind ebenfalls
abstrakt.
Abstrakte Eigenschafts Deklarationen sind nur in abstrakten Klassen zulässig (abstrakte Klassen). Die Accessoren
einer geerbten virtuellen Eigenschaft können in einer abgeleiteten Klasse überschrieben werden, indem Sie eine
Eigenschaften Deklaration einschließen, die eine- override Direktive angibt. Dies wird als über schreibende
Eigenschaften Deklaration bezeichnet. Eine über schreibende Eigenschaften Deklaration deklariert keine
neue Eigenschaft. Stattdessen werden lediglich die Implementierungen der Accessoren einer vorhandenen
virtuellen Eigenschaft spezialisiert.
Eine über schreibende Eigenschaften Deklaration muss genau dieselben Zugriffsmodifizierer, denselben Typ und
denselben Namen wie die geerbte Eigenschaft angeben. Wenn die geerbte Eigenschaft nur über einen einzigen
Accessor verfügt (d. h., wenn die geerbte Eigenschaft schreibgeschützt oder schreibgeschützt ist), muss die über
schreibende Eigenschaft nur den Accessor enthalten. Wenn die geerbte Eigenschaft beide Accessoren enthält (d.
h., wenn die geerbte Eigenschaft Lese-/Schreibzugriff hat), kann die über schreibende Eigenschaft entweder
einen einzelnen Accessor oder beide Accessoren einschließen.
Eine über schreibende Eigenschaften Deklaration kann den- sealed Modifizierer enthalten. Durch die
Verwendung dieses Modifizierers wird verhindert, dass eine abgeleitete Klasse die Eigenschaft weiter
überschreibt. Die Accessoren einer versiegelten Eigenschaft sind ebenfalls versiegelt.
Mit Ausnahme der Unterschiede in der Deklaration und der Aufruf Syntax Verhalten sich virtuelle, versiegelte,
Überschreibungs-und abstrakte Accessoren genauso wie virtuelle, versiegelte, Überschreibungs-und abstrakte
Methoden. Insbesondere gelten die in virtuellen Methoden, Methodenzum überschreiben, versiegelten
Methodenund abstrakten Methoden beschriebenen Regeln so, als wären Accessoren Methoden einer
entsprechenden Form:
Ein get -Accessor entspricht einer Parameter losen Methode mit einem Rückgabewert des
Eigenschaftentyps und denselben Modifiziererwerten wie die enthaltende Eigenschaft.
Ein set -Accessor entspricht einer Methode mit einem einzelnen Wert Parameter des Eigenschaftentyps,
einem void Rückgabetyp und denselben Modifiziererwerten wie die enthaltende Eigenschaft.
Im Beispiel

abstract class A
{
int y;

public virtual int X {


get { return 0; }
}

public virtual int Y {


get { return y; }
set { y = value; }
}

public abstract int Z { get; set; }


}

X ist eine virtuelle schreibgeschützte Eigenschaft, Y ist eine virtuelle Lese-/Schreibeigenschaft und Z eine
abstrakte Lese-/Schreibeigenschaft. Da Z abstrakt ist, muss die enthaltende Klasse A auch als abstrakt
deklariert werden.
Eine Klasse, die von abgeleitet A wird, wird unten angezeigt:

class B: A
{
int z;

public override int X {


get { return base.X + 1; }
}

public override int Y {


set { base.Y = value < 0? 0: value; }
}

public override int Z {


get { return z; }
set { z = value; }
}
}

Hier überschreiben die Deklarationen von X , Y und Z Eigenschafts Deklarationen. Jede Eigenschaften
Deklaration stimmt genau mit den zugriffsmodifizierertypen und dem Namen der entsprechenden geerbten
Eigenschaft überein. Der get -Accessor von X und der- set Accessor von Y verwenden das- base
Schlüsselwort, um auf die geerbten Accessoren zuzugreifen. Die Deklaration von Z überschreibt beide
abstrakten Accessoren – folglich gibt es keine ausstehenden abstrakten Funktionsmember in B , und es darf
sich B um eine nicht abstrakte Klasse handeln.
Wenn eine Eigenschaft als deklariert wird override , müssen alle überschriebenen Accessoren für den über
schreibenden Code zugänglich sein. Außerdem muss der deklarierte Zugriff sowohl für die Eigenschaft oder den
Indexer selbst als auch für die Accessoren mit der der überschriebenen Member und Accessoren identisch sein.
Beispiel:
public class B
{
public virtual int P {
protected set {...}
get {...}
}
}

public class D: B
{
public override int P {
protected set {...} // Must specify protected here
get {...} // Must not have a modifier here
}
}

Ereignisse
Ein *Ereignis _ ist ein Member, der es einem Objekt oder einer Klasse ermöglicht, Benachrichtigungen
bereitzustellen. Clients können ausführbaren Code für Ereignisse anfügen, indem Sie die _ -Ereignishandler *
bereitstellen.
Ereignisse werden mithilfe von event_declaration s deklariert:

event_declaration
: attributes? event_modifier* 'event' type variable_declarators ';'
| attributes? event_modifier* 'event' type member_name '{' event_accessor_declarations '}'
;

event_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| event_modifier_unsafe
;

event_accessor_declarations
: add_accessor_declaration remove_accessor_declaration
| remove_accessor_declaration add_accessor_declaration
;

add_accessor_declaration
: attributes? 'add' block
;

remove_accessor_declaration
: attributes? 'remove' block
;

Eine event_declaration kann eine Reihe von Attributen (Attribute) und eine gültige Kombination der vier
Zugriffsmodifizierer (Zugriffsmodifizierer), ( new der neuen Modifizierer), static (statische und
Instanzmethoden), virtual (virtuelle Methoden), override (Überschreibungs Methoden), sealed (versiegelte
Methoden), abstract (abstrakte Methoden) und extern (Externe Methoden)
Ereignis Deklarationen unterliegen den gleichen Regeln wie Methoden Deklarationen (Methoden) in Bezug auf
gültige Kombinationen von modifiziererereignissen.
Der Typ einer Ereignis Deklaration muss ein delegate_type (Verweis Typen) sein, und der delegate_type muss
mindestens so zugänglich sein wie das Ereignis selbst (Barrierefreiheits Einschränkungen).
Eine Ereignis Deklaration kann event_accessor_declarations enthalten. Wenn dies jedoch nicht der Fall ist, stellt
der Compiler Sie für nicht-externe, nicht abstrakte Ereignisse automatisch bereit (Feld ähnliche Ereignisse). bei
externen Ereignissen werden die Accessoren extern bereitgestellt.
In einer Ereignis Deklaration, die event_accessor_declarations auslässt, wird mindestens ein Ereignis definiert –
eines für jede variable_declarator s. Die Attribute und Modifizierer gelten für alle Member, die von einem
solchen event_declaration deklariert werden.
Es handelt sich hierbei um einen Kompilierzeitfehler für eine event_declaration , die sowohl den abstract
Modifizierer als auch das mit geschweiften Klammern getrennte event_accessor_declarations enthalten soll.
Wenn eine Ereignis Deklaration einen extern Modifizierer enthält, wird das Ereignis als *externes Ereignis _
bezeichnet. Da eine externe Ereignis Deklaration keine tatsächliche Implementierung bereitstellt, ist es ein Fehler,
dass Sie sowohl den extern -Modifizierer als auch das _event_accessor_declarations * einschließen.
Es handelt sich um einen Kompilierzeitfehler für eine variable_declarator einer Ereignis Deklaration mit einem-
abstract oder- external Modifizierer, der eine variable_initializer einschließt.

Ein Ereignis kann als Linker Operand des += -= Operators und (Ereignis Zuweisung) verwendet werden. Diese
Operatoren werden zum Anfügen von Ereignis Handlern an oder zum Entfernen von Ereignis Handlern aus
einem Ereignis verwendet, und die Zugriffsmodifizierer des Ereignisses steuern die Kontexte, in denen solche
Vorgänge zulässig sind.
Da += und -= die einzigen Vorgänge sind, die für ein Ereignis außerhalb des Typs zulässig sind, der das
Ereignis deklariert, kann externer Code Handler für ein Ereignis hinzufügen und entfernen, aber nicht auf andere
Weise die zugrunde liegende Liste von Ereignis Handlern abrufen oder ändern.
Bei einem Vorgang des Formulars x += y oder x -= y , wenn x ein Ereignis ist und der Verweis außerhalb
des Typs stattfindet, der die Deklaration von enthält x , hat das Ergebnis des Vorgangs den Typ void (im
Gegensatz zum Typ von x , mit dem Wert x nach der Zuweisung). Diese Regel verhindert, dass externer Code
indirekt den zugrunde liegenden Delegaten eines Ereignisses untersucht.
Das folgende Beispiel zeigt, wie Ereignishandler an Instanzen der-Klasse angefügt werden Button :
public delegate void EventHandler(object sender, EventArgs e);

public class Button: Control


{
public event EventHandler Click;
}

public class LoginDialog: Form


{
Button OkButton;
Button CancelButton;

public LoginDialog() {
OkButton = new Button(...);
OkButton.Click += new EventHandler(OkButtonClick);
CancelButton = new Button(...);
CancelButton.Click += new EventHandler(CancelButtonClick);
}

void OkButtonClick(object sender, EventArgs e) {


// Handle OkButton.Click event
}

void CancelButtonClick(object sender, EventArgs e) {


// Handle CancelButton.Click event
}
}

Hier erstellt der LoginDialog Instanzkonstruktor zwei Button -Instanzen und fügt Ereignishandler an die-
Click Ereignisse an.

Feld ähnliche Ereignisse


Im Programmtext der Klasse oder Struktur, die die Deklaration eines Ereignisses enthält, können bestimmte
Ereignisse wie Felder verwendet werden. Um auf diese Weise verwendet zu werden, darf ein Ereignis nicht
abstract oder sein extern und darf nicht explizit event_accessor_declarations enthalten. Ein derartiges
Ereignis kann in jedem Kontext verwendet werden, der ein Feld zulässt. Das-Feld enthält einen
Delegaten(Delegaten), der auf die Liste der Ereignishandler verweist, die dem-Ereignis hinzugefügt wurden.
Wenn keine Ereignishandler hinzugefügt wurden, enthält das Feld null .
Im Beispiel

public delegate void EventHandler(object sender, EventArgs e);

public class Button: Control


{
public event EventHandler Click;

protected void OnClick(EventArgs e) {


if (Click != null) Click(this, e);
}

public void Reset() {


Click = null;
}
}

Click wird als Feld in der- Button Klasse verwendet. Wie das Beispiel zeigt, kann das-Feld überprüft, geändert
und in Delegataufrufausdrücken verwendet werden. Die- OnClick Methode in der- Button Klasse löst das-
Ereignis aus Click . Das Auslösen eines Ereignisses entspricht exakt dem Aufrufen des Delegaten, der durch
das Ereignis repräsentiert wird, es gibt deshalb keine besonderen Sprachkonstrukte zum Auslösen von
Ereignissen. Beachten Sie, dass dem Delegataufruf eine Prüfung vorangestellt ist, die sicherstellt, dass der
Delegat nicht NULL ist.
Außerhalb der Deklaration der Button Click -Klasse kann der Member nur auf der linken Seite der +=
Operatoren und verwendet werden -= , wie in

b.Click += new EventHandler(...);

, der einen Delegaten an die Aufruf Liste des Ereignisses anfügt Click , und

b.Click -= new EventHandler(...);

entfernt einen Delegaten aus der Aufruf Liste des Click Ereignisses.
Beim Kompilieren eines Feld ähnlichen Ereignisses erstellt der Compiler automatisch Speicher, um den
Delegaten aufzunehmen, und erstellt Accessoren für das Ereignis, mit dem Ereignishandler zum Delegatfeld
hinzugefügt oder daraus entfernt werden. Die hinzu Füge-und Entfernungs Vorgänge sind Thread sicher und
können ( jedoch nicht erforderlich) ausgeführt werden, während die Sperre (lock-Anweisung) für das
enthaltende Objekt für ein Instanzereignis oder das Typobjekt (Ausdrücke zum Erstellen anonymer Objekte) für
ein statisches Ereignis aufrechterhalten werden.
Daher ist eine Instanzereignisdeklaration der folgenden Form:

class X
{
public event D Ev;
}

wird in eine entsprechende kompiliert:

class X
{
private D __Ev; // field to hold the delegate

public event D Ev {
add {
/* add the delegate in a thread safe way */
}

remove {
/* remove the delegate in a thread safe way */
}
}
}

In der X -Klasse bewirken Verweise auf Ev auf der linken Seite der += -= Operatoren und, dass die Add-und
remove-Accessoren aufgerufen werden. Alle anderen Verweise auf Ev werden kompiliert, um stattdessen auf
das ausgeblendete Feld zu verweisen __Ev (Member Access). Der Name " __Ev " ist willkürlich; das
ausgeblendete Feld kann einen beliebigen Namen oder keinen Namen haben.
Ereignisaccessoren
Ereignis Deklarationen lassen event_accessor_declarations in der Regel aus, wie im Button obigen Beispiel
gezeigt. Eine Situation hierfür ist der Fall, in dem die Speicherkosten eines Felds pro Ereignis nicht zulässig sind.
In solchen Fällen kann eine Klasse event_accessor_declarations enthalten und einen privaten Mechanismus zum
Speichern der Liste von Ereignis Handlern verwenden.
Die event_accessor_declarations eines Ereignisses geben die ausführbaren Anweisungen an, die dem
Hinzufügen und Entfernen von Ereignis Handlern zugeordnet sind.
Die Accessordeklarationen bestehen aus einer add_accessor_declaration und einer
remove_accessor_declaration. Jede Accessordeklaration besteht aus dem Token add oder remove gefolgt von
einem- Block. Der einem add_accessor_declaration zugeordnete- Block gibt die-Anweisungen an, die beim
Hinzufügen eines Ereignis Handlers ausgeführt werden sollen, und der einem remove_accessor_declaration
zugeordnete Block gibt die Anweisungen an, die beim Entfernen eines Ereignis Handlers ausgeführt werden
sollen.
Jede add_accessor_declaration und remove_accessor_declaration entspricht einer Methode mit einem einzelnen
Wert Parameter des Ereignis Typs und einem void Rückgabetyp. Der implizite Parameter eines Ereignis
Accessors heißt value . Wenn ein Ereignis in einer Ereignis Zuweisung verwendet wird, wird der entsprechende
Ereignis Accessor verwendet. Insbesondere, wenn der Zuweisungs Operator ist += , wird der Add-Accessor
verwendet, und wenn der Zuweisungs Operator ist, -= wird der remove-Accessor verwendet. In beiden Fällen
wird der rechte Operand des Zuweisungs Operators als Argument für den Ereignis Accessor verwendet. Der-
Block einer add_accessor_declaration oder einer remove_accessor_declaration muss den Regeln für Methoden
entsprechen, die void im Methoden Textbeschrieben werden. Insbesondere- return Anweisungen in einem
solchen Block dürfen keinen Ausdruck angeben.
Da ein Ereignis Accessor implizit einen Parameter mit dem Namen aufweist value , handelt es sich um einen
Kompilierzeitfehler für eine lokale Variable oder Konstante, die in einem Ereignis Accessor deklariert wurde, um
diesen Namen zu haben.
Im Beispiel

class Control: Component


{
// Unique keys for events
static readonly object mouseDownEventKey = new object();
static readonly object mouseUpEventKey = new object();

// Return event handler associated with key


protected Delegate GetEventHandler(object key) {...}

// Add event handler associated with key


protected void AddEventHandler(object key, Delegate handler) {...}

// Remove event handler associated with key


protected void RemoveEventHandler(object key, Delegate handler) {...}

// MouseDown event
public event MouseEventHandler MouseDown {
add { AddEventHandler(mouseDownEventKey, value); }
remove { RemoveEventHandler(mouseDownEventKey, value); }
}

// MouseUp event
public event MouseEventHandler MouseUp {
add { AddEventHandler(mouseUpEventKey, value); }
remove { RemoveEventHandler(mouseUpEventKey, value); }
}

// Invoke the MouseUp event


protected void OnMouseUp(MouseEventArgs args) {
MouseEventHandler handler;
handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey);
if (handler != null)
handler(this, args);
}
}
die- Control Klasse implementiert einen internen Speichermechanismus für-Ereignisse. Die-Methode ordnet
einem AddEventHandler Schlüssel einen Delegatwert zu, die GetEventHandler Methode gibt den derzeit einem
Schlüssel zugeordneten Delegaten zurück, und die RemoveEventHandler Methode entfernt einen Delegaten als
Ereignishandler für das angegebene Ereignis. Vermutlich ist der zugrunde liegende Speichermechanismus so
konzipiert, dass es keine Kosten für das Zuordnen eines null Delegatwerts zu einem Schlüssel gibt. daher
verbrauchen nicht behandelte Ereignisse keinen Speicherplatz.
Statische Ereignisse und Instanzereignisse
Wenn eine Ereignis Deklaration einen static Modifizierer enthält, wird das Ereignis als *statisches Ereignis _
bezeichnet. Wenn kein static Modifizierer vorhanden ist, wird das Ereignis als _ -Instanzereignis * bezeichnet.
Ein statisches Ereignis ist nicht mit einer bestimmten Instanz verknüpft, und es ist ein Kompilierzeitfehler, auf
this den in den Accessoren eines statischen Ereignisses verwiesen wird.

Ein Instanzereignis ist einer bestimmten Instanz einer Klasse zugeordnet, und auf diese Instanz kann this in
den Accessoren dieses Ereignisses als (dieser Zugriff) zugegriffen werden.
Wenn auf ein Ereignis in einem member_access verwiesen wird (Member-Zugriff) E.M , wenn M ein statisches
Ereignis ist, E muss einen Typ angeben M , der enthält, und wenn M ein Instanzereignis ist, muss E eine
Instanz eines Typs angeben, der enthält M .
Die Unterschiede zwischen statischen und Instanzmembern werden in statischen und
Instanzmembernausführlicher erläutert.
Virtual-, sealed-, override -und Abstract-Ereignisaccessoren
Eine- virtual Ereignis Deklaration gibt an, dass die Accessoren dieses Ereignisses virtuell sind. Der- virtual
Modifizierer gilt für beide Accessoren eines Ereignisses.
Eine abstract Ereignis Deklaration gibt an, dass die Accessoren des Ereignisses virtuell sind, aber keine
tatsächliche Implementierung der Accessoren bereitstellt. Stattdessen sind nicht abstrakte abgeleitete Klassen
erforderlich, um eine eigene Implementierung für die Accessoren bereitzustellen, indem das-Ereignis
überschrieben wird. Da eine abstrakte Ereignis Deklaration keine tatsächliche Implementierung bereitstellt, kann
Sie keine durch Klammern getrennte event_accessor_declarations bereitstellen.
Eine Ereignis Deklaration, die den abstract - override Modifizierer und den-Modifizierer enthält, gibt an, dass
das Ereignis abstrakt ist, und überschreibt ein Die Accessoren eines solchen Ereignisses sind ebenfalls abstrakt.
Abstrakte Ereignis Deklarationen sind nur in abstrakten Klassen zulässig (abstrakte Klassen).
Die Accessoren eines geerbten virtuellen Ereignisses können durch Einschließen einer Ereignis Deklaration, die
einen Modifizierer angibt, in einer abgeleiteten Klasse überschrieben werden override . Dies wird als über
schreibende Ereignis Deklaration bezeichnet. Eine über schreibende Ereignis Deklaration deklariert kein
neues Ereignis. Stattdessen werden lediglich die Implementierungen der Accessoren eines vorhandenen
virtuellen Ereignisses spezialisiert.
Eine über schreibende Ereignis Deklaration muss genau dieselben Zugriffsmodifizierer, denselben Typ und
denselben Namen wie das überschriebene Ereignis angeben.
Eine über schreibende Ereignis Deklaration kann den- sealed Modifizierer enthalten. Durch die Verwendung
dieses Modifizierers wird verhindert, dass eine abgeleitete Klasse das Ereignis weiter überschreibt. Die
Accessoren eines versiegelten Ereignisses werden ebenfalls versiegelt.
Es ist ein Kompilierzeitfehler, wenn eine über schreibende Ereignis Deklaration einen- new Modifizierer
einschließt.
Mit Ausnahme der Unterschiede in der Deklaration und der Aufruf Syntax Verhalten sich virtuelle, versiegelte,
Überschreibungs-und abstrakte Accessoren genauso wie virtuelle, versiegelte, Überschreibungs-und abstrakte
Methoden. Insbesondere gelten die in Virtual Methods, override Methods, sealed Methodsund abstract Methods
beschriebenen Regeln so, als wären Accessoren Methoden einer entsprechenden Form. Jeder-Accessor
entspricht einer Methode mit einem einzelnen value-Parameter des Ereignis Typs, einem void Rückgabetyp
und denselben Modifiziererwerten wie das enthaltende Ereignis.

Indexer
Ein *Indexer _ ist ein Member, mit dem ein Objekt auf die gleiche Weise wie ein Array indiziert werden kann.
Indexer werden mithilfe von _indexer_declaration * s deklariert:

indexer_declaration
: attributes? indexer_modifier* indexer_declarator indexer_body
;

indexer_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| indexer_modifier_unsafe
;

indexer_declarator
: type 'this' '[' formal_parameter_list ']'
| type interface_type '.' 'this' '[' formal_parameter_list ']'
;

indexer_body
: '{' accessor_declarations '}'
| '=>' expression ';'
;

Eine indexer_declaration kann einen Satz von Attributen (Attribute) und eine gültige Kombination der vier
Zugriffsmodifizierer (Zugriffsmodifizierer), ( new der neuen Modifizierer), virtual (virtuelle Methoden),
override (Überschreibungs Methoden), sealed (versiegelteMethoden), abstract (abstrakte Methoden) und
extern (Externe Methoden)-Modifizierer enthalten.

Indexer-Deklarationen unterliegen den gleichen Regeln wie Methoden Deklarationen (Methoden) in Bezug auf
gültige Kombinationen von Modifizierern, wobei die einzige Ausnahme darin besteht, dass der statische
Modifizierer in einer Indexer-Deklaration nicht zulässig ist.
Die modifiziererer virtual, override und abstract schließen sich gegenseitig aus, außer in einem Fall. Die
abstract override Modifizierer und können zusammen verwendet werden, damit ein abstrakter Indexer einen
virtuellen überschreiben kann.
Der Typ einer Indexer-Deklaration gibt den Elementtyp des Indexers an, der von der Deklaration eingeführt wird.
Es sei denn , der Indexer ist eine explizite Schnittstellenmember-Implementierung, gefolgt vom-Schlüsselwort
this . Bei einer expliziten Schnittstellenmember-Implementierung folgt dem- Typ ein INTERFACE_TYPE, ein " .
" und das-Schlüsselwort this . Im Gegensatz zu anderen Membern haben Indexer keine benutzerdefinierten
Namen.
Der formal_parameter_list der die Parameter des Indexers angibt. Die Liste formaler Parameter eines Indexers
entspricht der einer Methode (Methoden Parameter), mit dem Unterschied, dass mindestens ein Parameter
angegeben werden muss und dass die ref -und- out Parametermodifizierer nicht zulässig sind.
Der Typ eines Indexers und alle Typen, auf die im formal_parameter_list verwiesen wird, müssen mindestens so
zugänglich sein wie der Indexer selbst (Barrierefreiheits Einschränkungen).
Eine indexer_body kann entweder aus einem -**Accessor Body* _ oder einem Ausdrucks Körper bestehen. In
einem Accessor-Text _accessor_declarations *, der in die { Token "" und "" eingeschlossen werden muss } , die
Accessoren (Accessoren) der Eigenschaft. Die Accessoren geben die ausführbaren Anweisungen an, die dem
Lesen und Schreiben der Eigenschaft zugeordnet sind.
Ein Ausdrucks Text, der aus " => " gefolgt von einem Ausdruck E und einem Semikolon besteht, entspricht
exakt dem Anweisungs Text { get { return E; } } und kann daher nur zum Angeben von nur-Getter-
indexatoren verwendet werden, bei denen das Ergebnis des Getters durch einen einzelnen Ausdruck angegeben
wird.
Obwohl die Syntax für den Zugriff auf ein Indexer-Element mit der Syntax für ein Array Element identisch ist,
wird ein Indexer-Element nicht als Variable klassifiziert. Folglich ist es nicht möglich, ein Indexer-Element als-
oder-Argument zu übergeben ref out .
Die Liste der formalen Parameter eines Indexers definiert die Signatur (Signaturen und überladen) des Indexers.
Insbesondere besteht die Signatur eines Indexers aus der Anzahl und den Typen der formalen Parameter. Der
Elementtyp und die Namen der formalen Parameter sind nicht Teil der Signatur eines Indexers.
Die Signatur eines Indexer muss sich von den Signaturen aller anderen Indexer unterscheiden, die in derselben
Klasse deklariert sind.
Indexer und Eigenschaften sind in der Konzeption sehr ähnlich, unterscheiden sich jedoch wie folgt:
Eine Eigenschaft wird anhand ihres Namens identifiziert, während ein Indexer durch die Signatur identifiziert
wird.
Der Zugriff auf eine Eigenschaft erfolgt über einen Simple_name (einfache Namen) oder über einen
member_access (Member Access), während auf ein Indexer-Element über einen element_access (Indexer-
Zugriff) zugegriffen wird.
Eine Eigenschaft kann ein static Member sein, während ein Indexer immer ein Instanzmember ist.
Ein- get Accessor einer Eigenschaft entspricht einer Methode ohne Parameter, während ein- get Accessor
eines Indexers einer Methode mit derselben formalen Parameterliste wie der Indexer entspricht.
Ein- set Accessor einer Eigenschaft entspricht einer Methode mit einem einzelnen Parameter mit value
dem Namen, während ein- set Accessor eines Indexers einer Methode mit derselben formalen
Parameterliste wie der Indexer und einem zusätzlichen Parameter mit dem Namen entspricht value .
Es ist ein Kompilierzeitfehler für einen Indexer-Accessor, eine lokale Variable mit dem gleichen Namen wie ein
Indexer-Parameter zu deklarieren.
In einer über schreibenden Eigenschaften Deklaration wird auf die geerbte Eigenschaft mithilfe der-Syntax
zugegriffen base.P , wobei P der Eigenschaftsname ist. In einer über schreibenden Indexer-Deklaration
wird auf den geerbten Indexer mithilfe der-Syntax zugegriffen base[E] , wobei E eine durch Kommas
getrennte Liste von Ausdrücken ist.
Es gibt kein Konzept für einen "automatisch implementierten Indexer". Es ist ein Fehler, wenn ein nicht
abstrakter, nicht externer Indexer mit Semikolon-Accessoren vorliegt.
Abgesehen von diesen Unterschieden gelten alle in Accessoren und automatisch implementierten Eigenschaften
definierten Regeln sowohl für Indexer-Accessoren als auch für Eigenschaftenaccessoren.
Wenn eine Indexerdeklaration einen extern Modifizierer enthält, wird der Indexer als *externer Indexer _
bezeichnet. Da eine externe Indexer-Deklaration keine tatsächliche Implementierung bereitstellt, besteht jede
Ihrer _accessor_declarations * aus einem Semikolon.
Im folgenden Beispiel wird eine BitArray Klasse deklariert, die einen Indexer für den Zugriff auf die einzelnen
Bits im BitArray implementiert.

using System;

class BitArray
{
int[] bits;
int length;

public BitArray(int length) {


if (length < 0) throw new ArgumentException();
bits = new int[((length - 1) >> 5) + 1];
this.length = length;
}

public int Length {


get { return length; }
}

public bool this[int index] {


get {
if (index < 0 || index >= length) {
throw new IndexOutOfRangeException();
}
return (bits[index >> 5] & 1 << index) != 0;
}
set {
if (index < 0 || index >= length) {
throw new IndexOutOfRangeException();
}
if (value) {
bits[index >> 5] |= 1 << index;
}
else {
bits[index >> 5] &= ~(1 << index);
}
}
}
}

Eine Instanz der BitArray -Klasse beansprucht wesentlich weniger Arbeitsspeicher als eine entsprechende
bool[] (da jeder Wert des ersten-Werts nur ein Bit und nicht das zweite Byte) beansprucht, aber die gleichen
Vorgänge wie eine zulässt bool[] .
Die folgende CountPrimes Klasse verwendet einen BitArray und den klassischen "Sieve"-Algorithmus, um die
Anzahl von PRIMES zwischen 1 und einem angegebenen Maximum zu berechnen:
class CountPrimes
{
static int Count(int max) {
BitArray flags = new BitArray(max + 1);
int count = 1;
for (int i = 2; i <= max; i++) {
if (!flags[i]) {
for (int j = i * 2; j <= max; j += i) flags[j] = true;
count++;
}
}
return count;
}

static void Main(string[] args) {


int max = int.Parse(args[0]);
int count = Count(max);
Console.WriteLine("Found {0} primes between 1 and {1}", count, max);
}
}

Beachten Sie, dass die Syntax für den Zugriff auf Elemente von BitArray exakt der gleiche ist wie für ein
bool[] .

Im folgenden Beispiel wird eine 26 * 10-Raster Klasse gezeigt, die über einen Indexer mit zwei Parametern
verfügt. Der erste Parameter muss ein groß-oder Kleinbuchstabe im Bereich A-Z sein, und der zweite Parameter
muss eine ganze Zahl im Bereich 0-9 sein.

using System;

class Grid
{
const int NumRows = 26;
const int NumCols = 10;

int[,] cells = new int[NumRows, NumCols];

public int this[char c, int col] {


get {
c = Char.ToUpper(c);
if (c < 'A' || c > 'Z') {
throw new ArgumentException();
}
if (col < 0 || col >= NumCols) {
throw new IndexOutOfRangeException();
}
return cells[c - 'A', col];
}

set {
c = Char.ToUpper(c);
if (c < 'A' || c > 'Z') {
throw new ArgumentException();
}
if (col < 0 || col >= NumCols) {
throw new IndexOutOfRangeException();
}
cells[c - 'A', col] = value;
}
}
}

Überladen von Indexern


Die Regeln der Indexer-Überladungs Auflösung werden unter Typrückschlussbeschrieben.

Operatoren
Ein *Operator _ ist ein Member, der die Bedeutung eines Ausdrucks Operators definiert, der auf Instanzen der
Klasse angewendet werden kann. Operatoren werden mit _operator_declaration * s deklariert:

operator_declaration
: attributes? operator_modifier+ operator_declarator operator_body
;

operator_modifier
: 'public'
| 'static'
| 'extern'
| operator_modifier_unsafe
;

operator_declarator
: unary_operator_declarator
| binary_operator_declarator
| conversion_operator_declarator
;

unary_operator_declarator
: type 'operator' overloadable_unary_operator '(' type identifier ')'
;

overloadable_unary_operator
: '+' | '-' | '!' | '~' | '++' | '--' | 'true' | 'false'
;

binary_operator_declarator
: type 'operator' overloadable_binary_operator '(' type identifier ',' type identifier ')'
;

overloadable_binary_operator
: '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '<<'
| right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
;

conversion_operator_declarator
: 'implicit' 'operator' type '(' type identifier ')'
| 'explicit' 'operator' type '(' type identifier ')'
;

operator_body
: block
| '=>' expression ';'
| ';'
;

Es gibt drei Kategorien von über ladbaren Operatoren: unäre Operatoren (unäre Operatoren), binäre Operatoren
(binäre Operatoren) und Konvertierungs Operatoren (Konvertierungs Operatoren).
Der operator_body ist entweder ein Semikolon, ein -**Anweisungs Text* _ oder ein Ausdrucks Körper. Ein
Anweisungs Text besteht aus einer _block *, die die auszuführenden Anweisungen angibt, wenn der Operator
aufgerufen wird. Der- Block muss den Regeln für Rückgabe Methoden entsprechen, die im Methoden
Textbeschrieben werden. Ein Ausdrucks Körper besteht aus => , gefolgt von einem Ausdruck und einem
Semikolon und deutet auf einen einzelnen Ausdruck hin, der beim Aufrufen des Operators ausgeführt werden
soll.
Für extern Operatoren besteht die operator_body einfach aus einem Semikolon. Für alle anderen Operatoren
ist die operator_body entweder ein Block Körper oder ein Ausdrucks Körper.
Die folgenden Regeln gelten für alle Operator Deklarationen:
Eine Operator Deklaration muss sowohl einen public -als auch einen- static Modifizierer enthalten.
Die Parameter eines Operators müssen Wert Parameter (value-Parameter) sein. Es handelt sich um einen
Kompilierzeitfehler für eine Operator Deklaration zum Angeben von- ref oder- out Parametern.
Die Signatur eines Operators (unäre Operatoren, binäre Operatorenund Konvertierungs Operatoren) muss
sich von den Signaturen aller anderen Operatoren unterscheiden, die in derselben Klasse deklariert sind.
Alle Typen, auf die in einer Operator Deklaration verwiesen wird, müssen mindestens so zugänglich sein wie
der Operator selbst (Barrierefreiheits Einschränkungen).
Es ist ein Fehler, dass derselbe Modifizierer mehrmals in einer Operator Deklaration angezeigt wird.
Jede Operator Kategorie erzwingt zusätzliche Einschränkungen, wie in den folgenden Abschnitten beschrieben.
Wie bei anderen Membern werden Operatoren, die in einer Basisklasse deklariert werden, von abgeleiteten
Klassen geerbt. Da Operator Deklarationen immer die Klasse oder Struktur erfordern, in der der Operator
deklariert ist, um an der Signatur des Operators teilzunehmen, ist es nicht möglich, dass ein Operator, der in
einer abgeleiteten Klasse deklariert ist, einen in einer Basisklasse deklarierten Operator ausblenden kann. Daher
ist der new -Modifizierer in einer Operator Deklaration nie erforderlich und daher nie zulässig.
Weitere Informationen zu unären und binären Operatoren finden Sie unter Operatoren.
Weitere Informationen zu Konvertierungs Operatoren finden Sie unter benutzerdefinierte Konvertierungen.
Unäre Operatoren
Die folgenden Regeln gelten für unäre Operator Deklarationen, wobei T den Instanztyp der Klasse oder
Struktur bezeichnet, die die Operator Deklaration enthält:
Ein unärer + - Operator,, ! oder ~ muss einen einzelnen Parameter vom Typ oder annehmen T T?
und kann jeden beliebigen Typ zurückgeben.
Ein unärer ++ or -- -Operator muss einen einzelnen Parameter vom Typ T oder annehmen T? , und er
muss denselben Typ oder einen von ihm abgeleiteten Typ zurückgeben.
Ein unärer true or false -Operator muss einen einzelnen Parameter vom Typ T oder annehmen T? und
muss den Typ zurückgeben bool .

Die Signatur eines unären Operators besteht aus dem Operator Token ( + , - , ! , ~ , ++ , -- , true oder
false ) und dem Typ des einzelnen formalen Parameters. Der Rückgabetyp ist weder Teil der Signatur eines
unären Operators noch der Name des formalen Parameters.
Die true false unären Operatoren und erfordern eine paarweise Deklaration. Ein Kompilierzeitfehler tritt auf,
wenn eine Klasse einen dieser Operatoren deklariert, ohne auch den anderen zu deklarieren. Die true false
Operatoren und werden weiter unten in benutzerdefinierten bedingten logischen Operatoren und booleschen
Ausdrückenbeschrieben.
Das folgende Beispiel zeigt eine-Implementierung und die nachfolgende Verwendung von operator ++ für eine
ganzzahlige Vektor Klasse:
public class IntVector
{
public IntVector(int length) {...}

public int Length {...} // read-only property

public int this[int index] {...} // read-write indexer

public static IntVector operator ++(IntVector iv) {


IntVector temp = new IntVector(iv.Length);
for (int i = 0; i < iv.Length; i++)
temp[i] = iv[i] + 1;
return temp;
}
}

class Test
{
static void Main() {
IntVector iv1 = new IntVector(4); // vector of 4 x 0
IntVector iv2;

iv2 = iv1++; // iv2 contains 4 x 0, iv1 contains 4 x 1


iv2 = ++iv1; // iv2 contains 4 x 2, iv1 contains 4 x 2
}
}

Beachten Sie, dass die Operator-Methode den Wert zurückgibt, der durch das Hinzufügen von 1 zum
Operanden erzeugt wird, genau wie die Postfix-Inkrement-und Dekrementoperatoren (postfix-Inkrement-und
Dekrementoperatoren) und die Präfix Inkrement-undDekrementoperatoren Anders als in C++ muss diese
Methode den Wert des Operanden nicht direkt ändern. Tatsächlich würde das Ändern des Operanden-Werts
gegen die Standard Semantik des Postfix-Inkrementoperators verstoßen.
Binäre Operatoren
Die folgenden Regeln gelten für binäre Operator Deklarationen, wobei T den Instanztyp der Klasse oder
Struktur bezeichnet, die die Operator Deklaration enthält:
Ein binärer nicht Verschiebungs Operator muss zwei Parameter annehmen, von denen mindestens eine den
Typ oder aufweisen muss T T? und jeden beliebigen Typ zurückgeben kann.
Ein binärer << or >> -Operator muss zwei Parameter annehmen. der erste muss den-Typ aufweisen, T
T? und der zweite muss den-Typ int oder aufweisen int? und jeden beliebigen Typ zurückgeben.

Die Signatur eines binären Operators besteht aus dem Operator Token ( + , - , * , / , % , & , | , ^ , << ,
>> , == , != , > , < , >= oder <= ) und den Typen der beiden formalen Parameter. Der Rückgabetyp und
die Namen der formalen Parameter sind nicht Teil der Signatur eines binären Operators.
Bestimmte binäre Operatoren erfordern eine paarweise Deklaration. Für jede Deklaration eines der beiden
Operatoren eines Paares muss eine entsprechende Deklaration des anderen Operators des Paars vorhanden
sein. Zwei Operator Deklarationen stimmen überein, wenn Sie den gleichen Rückgabetyp und denselben Typ für
jeden Parameter aufweisen. Die folgenden Operatoren erfordern eine paarweise Deklaration:
operator == und operator !=
operator > und operator <
operator >= und operator <=

Konvertierungsoperatoren
Eine Konvertierungs Operator Deklaration führt eine benutzerdefinier te Konver tierung (benutzerdefinierte
Konvertierungen) ein, mit der die vordefinierten und expliziten Konvertierungen erweitert werden.
Eine Konvertierungs Operator Deklaration, die das- implicit Schlüsselwort enthält, führt eine
benutzerdefinierte implizite Konvertierung ein. Implizite Konvertierungen können in einer Vielzahl von
Situationen auftreten, einschließlich Funktionsmember-Aufrufe, Umwandlungs Ausdrücke und Zuweisungen.
Dies wird in implizite Konvertierungenbeschrieben.
Eine Konvertierungs Operator Deklaration, die das- explicit Schlüsselwort enthält, führt eine
benutzerdefinierte explizite Konvertierung ein. Explizite Konvertierungen können in Umwandlungs Ausdrücken
auftreten und werden weiter unten in expliziten Konvertierungenbeschrieben.
Ein Konvertierungs Operator konvertiert von einem Quelltyp, der durch den Parametertyp des Konvertierungs
Operators angegeben ist, in einen Zieltyp, der durch den Rückgabetyp des Konvertierungs Operators
angegeben wird.
Geben Sie für einen angegebenen Quelltyp S und Zieltyp T , wenn S oder T NULL-Werte zulassen, die S0
T0 zugrunde liegenden Typen an, und verweisen Sie andernfalls S0 T0 gleich S T bzw. Eine Klasse oder
Struktur darf nur dann eine Konvertierung von einem Quelltyp S in einen Zieltyp deklarieren, T wenn
Folgendes zutrifft:
S0 und T0 sind unterschiedliche Typen.
Entweder S0 oder T0 ist der Klassen-oder Strukturtyp, in dem die Operator Deklaration stattfindet.
Weder S0 noch T0 ist eine INTERFACE_TYPE.
Ohne benutzerdefinierte Konvertierungen ist eine Konvertierung von S zu T oder von zu nicht vorhanden
T S .

Bei diesen Regeln werden alle Typparameter, die mit oder verknüpft sind, S T als eindeutige Typen betrachtet,
die keine Vererbungs Beziehung mit anderen Typen aufweisen, und Einschränkungen für diese Typparameter
werden ignoriert.
Im Beispiel

class C<T> {...}

class D<T>: C<T>


{
public static implicit operator C<int>(D<T> value) {...} // Ok
public static implicit operator C<string>(D<T> value) {...} // Ok
public static implicit operator C<T>(D<T> value) {...} // Error
}

die ersten beiden Operator Deklarationen sind zulässig, da für Indexer-Indexer, T und int string bzw. als
eindeutige Typen ohne Beziehung angesehen werden. Der dritte Operator ist jedoch ein Fehler, da C<T> die
Basisklasse von ist D<T> .
Aus der zweiten Regel folgt, dass ein Konvertierungs Operator entweder in oder aus dem Klassen-oder
Strukturtyp konvertieren muss, in dem der Operator deklariert ist. Beispielsweise ist es möglich, dass ein
Klassen-oder Strukturtyp C eine Konvertierung von C in int und von int in C , aber nicht von in definiert
int bool .

Es ist nicht möglich, eine vordefinierte Konvertierung direkt neu zu definieren. Folglich ist es nicht zulässig,
Konvertierungs Operatoren von oder in zu konvertieren, object da zwischen object und allen anderen Typen
bereits implizite und explizite Konvertierungen vorhanden sind. Ebenso kann weder die Quelle noch die
Zieltypen einer Konvertierung ein Basistyp der anderen sein, da eine Konvertierung dann bereits vorhanden
wäre.
Es ist jedoch möglich, Operatoren für generische Typen zu deklarieren, die für bestimmte Typargumente
Konvertierungen angeben, die bereits als vordefinierte Konvertierungen vorhanden sind. Im Beispiel
struct Convertible<T>
{
public static implicit operator Convertible<T>(T value) {...}
public static explicit operator T(Convertible<T> value) {...}
}

Wenn Type object als Typargument für angegeben wird T , deklariert der zweite Operator eine
Konvertierung, die bereits vorhanden ist (ein implizites und somit auch eine explizite Konvertierung von einem
beliebigen Typ in den Typ object ).
In Fällen, in denen eine vordefinierte Konvertierung zwischen zwei Typen vorhanden ist, werden alle
benutzerdefinierten Konvertierungen zwischen diesen Typen ignoriert. Dies betrifft insbesondere:
Wenn eine vordefinierte implizite Konvertierung (implizite Konvertierungen) vom Typ S in den Typ
vorhanden T ist, werden alle benutzerdefinierten Konvertierungen (implizit oder explizit) von S zu T
ignoriert.
Wenn eine vordefinierte explizite Konvertierung (explizite Konvertierungen) vom Typ S in den Typ
vorhanden T ist, werden alle benutzerdefinierten expliziten Konvertierungen von S in T ignoriert.
Außerdem gilt Folgendes:
Wenn T ein Schnittstellentyp ist, werden benutzerdefinierte implizite Konvertierungen von S in T ignoriert.
Andernfalls werden benutzerdefinierte implizite Konvertierungen von S in T immer noch berücksichtigt.
Für alle Typen, jedoch verursachen object die vom oben genannten Typ deklarierten Operatoren
Convertible<T> keinen Konflikt mit vordefinierten Konvertierungen. Beispiel:

void F(int i, Convertible<int> n) {


i = n; // Error
i = (int)n; // User-defined explicit conversion
n = i; // User-defined implicit conversion
n = (Convertible<int>)i; // User-defined implicit conversion
}

Für object den Typ verbergen vordefinierte Konvertierungen jedoch die benutzerdefinierten Konvertierungen
in allen Fällen, aber eine:

void F(object o, Convertible<object> n) {


o = n; // Pre-defined boxing conversion
o = (object)n; // Pre-defined boxing conversion
n = o; // User-defined implicit conversion
n = (Convertible<object>)o; // Pre-defined unboxing conversion
}

Benutzerdefinierte Konvertierungen dürfen nicht von oder in INTERFACE_TYPE s konvertiert werden. Diese
Einschränkung stellt insbesondere sicher, dass keine benutzerdefinierten Transformationen beim Konvertieren in
eine INTERFACE_TYPE auftreten und dass eine Konvertierung in eine INTERFACE_TYPE nur erfolgreich ist, wenn
das Objekt, das konvertiert wird, tatsächlich die angegebene INTERFACE_TYPE implementiert.
Die Signatur eines Konvertierungs Operators besteht aus dem Quelltyp und dem Zieltyp. (Beachten Sie, dass
dies die einzige Form der Member ist, für die der Rückgabetyp an der Signatur teilnimmt.) Die implicit
explicit -oder-Klassifizierung eines Konvertierungs Operators ist nicht Teil der Signatur des Operators. Daher
kann eine Klasse oder Struktur nicht sowohl einen implicit -als auch einen- explicit Konvertierungs
Operator mit denselben Quell-und Zieltypen deklarieren.
Im Allgemeinen sollten benutzerdefinierte implizite Konvertierungen so entworfen werden, dass Sie niemals
Ausnahmen auslösen und nie Informationen verlieren. Wenn eine benutzerdefinierte Konvertierung
Ausnahmen auslösen kann (z. b. weil das Quell Argument außerhalb des gültigen Bereichs liegt) oder
Informationen verloren geht (z. b. das Verwerfen von großen Bits), sollte diese Konvertierung als explizite
Konvertierung definiert werden.
Im Beispiel

using System;

public struct Digit


{
byte value;

public Digit(byte value) {


if (value < 0 || value > 9) throw new ArgumentException();
this.value = value;
}

public static implicit operator byte(Digit d) {


return d.value;
}

public static explicit operator Digit(byte b) {


return new Digit(b);
}
}

die Konvertierung von Digit in byte ist implizit, weil Sie niemals Ausnahmen auslöst oder Informationen
verliert, aber die Konvertierung von byte in Digit ist explizit, da Digit nur eine Teilmenge der möglichen
Werte eines darstellen kann byte .

Instanzkonstruktoren
Ein *Instanzkonstruktor _ ist ein Member, der die erforderlichen Aktionen zum Initialisieren einer Instanz einer
Klasse implementiert. Instanzkonstruktoren werden mit _constructor_declaration * s deklariert:
constructor_declaration
: attributes? constructor_modifier* constructor_declarator constructor_body
;

constructor_modifier
: 'public'
| 'protected'
| 'internal'
| 'private'
| 'extern'
| constructor_modifier_unsafe
;

constructor_declarator
: identifier '(' formal_parameter_list? ')' constructor_initializer?
;

constructor_initializer
: ':' 'base' '(' argument_list? ')'
| ':' 'this' '(' argument_list? ')'
;

constructor_body
: block
| ';'
;

Eine constructor_declaration kann einen Satz von Attributen (Attribute), eine gültige Kombination der vier
Zugriffsmodifizierer (Zugriffsmodifizierer) und einen- extern Modifizierer (externe Methode) enthalten. Eine
Konstruktordeklaration darf nicht denselben Modifizierer mehrmals einschließen.
Der Bezeichner eines constructor_declarator muss der Klasse, in der der Instanzkonstruktor deklariert ist, einen
Namen benennen. Wenn ein anderer Name angegeben wird, tritt ein Kompilierzeitfehler auf.
Der optionale formal_parameter_list eines Instanzkonstruktors unterliegt den gleichen Regeln wie der
formal_parameter_list einer Methode (Methoden). Die Liste formaler Parameter definiert die Signatur
(Signaturen und überladen) eines Instanzkonstruktors und steuert den Prozess, bei dem die Überladungs
Auflösung (Typrückschluss) einen bestimmten Instanzkonstruktor in einem Aufruf auswählt.
Auf jeden der Typen, auf die im formal_parameter_list eines Instanzkonstruktors verwiesen wird, muss
mindestens der Zugriff auf den Konstruktor selbst (Barrierefreiheits Einschränkungen) möglich sein.
Der optionale constructor_initializer gibt einen anderen Instanzkonstruktor an, der vor dem Ausführen der
Anweisungen in der constructor_body dieses Instanzkonstruktors aufgerufen werden soll. Dies wird in
konstruktorinitialisierernausführlicher beschrieben.
Wenn eine Konstruktordeklaration einen extern Modifizierer enthält, wird der Konstruktor als *externer
Konstruktor _ bezeichnet. Da eine externe Konstruktordeklaration keine tatsächliche Implementierung
bereitstellt, besteht die _constructor_body * aus einem Semikolon. Für alle anderen Konstruktoren besteht die
constructor_body aus einem- Block , der die Anweisungen angibt, um eine neue Instanz der-Klasse zu
initialisieren. Dies entspricht exakt dem Block einer Instanzmethode mit einem void Rückgabetyp (Methoden
Text).
Instanzkonstruktoren werden nicht geerbt. Folglich hat eine Klasse keine Instanzkonstruktoren, die nicht
tatsächlich in der Klasse deklariert sind. Wenn eine Klasse keine Instanzkonstruktordeklarationen enthält, wird
automatisch ein Standardinstanzkonstruktor bereitgestellt (Standardkonstruktoren).
Instanzkonstruktoren werden von object_creation_expression s (Objekt Erstellungs Ausdrücke) und durch
constructor_initializer s aufgerufen.
Konstruktorinitialisierer
Alle Instanzkonstruktoren (außer den Klassen object ) enthalten implizit einen Aufruf eines anderen
Instanzkonstruktors direkt vor dem constructor_body. Der implizit aufzurufende Konstruktor wird vom
constructor_initializer bestimmt:
Ein instanzkonstruktorinitialisierer des Formulars base(argument_list) oder base() bewirkt, dass ein
Instanzkonstruktor von der direkten Basisklasse aufgerufen wird. Dieser Konstruktor wird mit argument_list ,
sofern vorhanden, und den Regeln zur Überladungs Auflösung der Überladungs Auflösungausgewählt. Der
Satz von Kandidaten Instanzkonstruktoren besteht aus allen zugänglichen Instanzkonstruktoren, die in der
direkten Basisklasse enthalten sind, oder dem Standardkonstruktor (Standardkonstruktoren), wenn in der
direkten Basisklasse keine Instanzkonstruktoren deklariert werden. Wenn dieser Satz leer ist oder ein
einzelner Konstruktor mit der besten Instanz nicht identifiziert werden kann, tritt ein Kompilierzeitfehler auf.
Ein instanzkonstruktorinitialisierer des Formulars this(argument-list) oder this() bewirkt, dass ein
Instanzkonstruktor von der Klasse selbst aufgerufen wird. Der Konstruktor wird mit argument_list , sofern
vorhanden, und den Regeln zur Überladungs Auflösung der Überladungs Auflösungausgewählt. Der Satz von
Kandidaten Instanzkonstruktoren besteht aus allen zugänglichen Instanzkonstruktoren, die in der Klasse
selbst deklariert sind. Wenn dieser Satz leer ist oder ein einzelner Konstruktor mit der besten Instanz nicht
identifiziert werden kann, tritt ein Kompilierzeitfehler auf. Wenn eine Instanzkonstruktordeklaration einen
Konstruktorinitialisierer enthält, der den Konstruktor selbst aufruft, tritt ein Kompilierzeitfehler auf.
Wenn ein Instanzkonstruktor über keinen Konstruktorinitialisierer verfügt, wird ein Konstruktorinitialisierer des
Formulars base() implizit bereitgestellt. Folglich ist eine Instanzkonstruktordeklaration der Form

C(...) {...}

ist genau Äquivalent zu

C(...): base() {...}

Der Gültigkeitsbereich der Parameter, die vom formal_parameter_list einer Instanzkonstruktordeklaration


angegeben werden, umfasst den Konstruktorinitialisierer dieser Deklaration. Daher ist es einem
Konstruktorinitialisierer gestattet, auf die Parameter des Konstruktors zuzugreifen. Beispiel:

class A
{
public A(int x, int y) {}
}

class B: A
{
public B(int x, int y): base(x + y, x - y) {}
}

Ein instanzkonstruktorinitialisierer kann nicht auf die Instanz zugreifen, die erstellt wird. Daher ist es ein
Kompilierzeitfehler, this der in einem Argument Ausdruck des konstruktorinitialisierers referenziert werden
kann, ebenso wie ein Kompilierzeitfehler für einen Argument Ausdruck, der auf ein Instanzmember durch eine
Simple_name verweist.
Instanzvariableninitialisierer
Wenn ein Instanzkonstruktor über keinen Konstruktorinitialisierer oder einen Konstruktorinitialisierer des
Formulars verfügt base(...) , führt dieser Konstruktor implizit die Initialisierungen aus, die von den
variable_initializer s der Instanzfelder angegeben werden, die in der Klasse deklariert sind. Dies entspricht einer
Sequenz von Zuweisungen, die unmittelbar nach dem Einstieg in den Konstruktor und vor dem impliziten Aufruf
des direkten Basisklassenkonstruktors ausgeführt werden. Die Variableninitialisierer werden in der Text
Reihenfolge ausgeführt, in der Sie in der Klassen Deklaration angezeigt werden.
Konstruktorausführung
Variableninitialisierer werden in Zuweisungs Anweisungen transformiert, und diese Zuweisungs Anweisungen
werden vor dem Aufruf des Basisklasseninstanzkonstruktors ausgeführt. Diese Reihenfolge stellt sicher, dass alle
Instanzfelder durch ihre Variableninitialisierer initialisiert werden, bevor Anweisungen ausgeführt werden, die
auf diese Instanz zugreifen können.
Im Beispiel

using System;

class A
{
public A() {
PrintFields();
}

public virtual void PrintFields() {}


}

class B: A
{
int x = 1;
int y;

public B() {
y = -1;
}

public override void PrintFields() {


Console.WriteLine("x = {0}, y = {1}", x, y);
}
}

Wenn new B() verwendet wird, um eine Instanz von zu erstellen B , wird die folgende Ausgabe erzeugt:

x = 1, y = 0

Der Wert von x ist 1, da der Variableninitialisierer ausgeführt wird, bevor der basisklasseninstanzkonstruktor
aufgerufen wird. Der Wert von ist jedoch y 0 (der Standardwert von), int da die Zuweisung zu y erst
ausgeführt wird, nachdem der Basisklassenkonstruktor zurückgegeben wurde.
Es ist hilfreich, instanzvariableninitialisierer und Konstruktorinitialisierer als Anweisungen zu betrachten, die vor
der constructor_body automatisch eingefügt werden. Das Beispiel
using System;
using System.Collections;

class A
{
int x = 1, y = -1, count;

public A() {
count = 0;
}

public A(int n) {
count = n;
}
}

class B: A
{
double sqrt2 = Math.Sqrt(2.0);
ArrayList items = new ArrayList(100);
int max;

public B(): this(100) {


items.Add("default");
}

public B(int n): base(n - 1) {


max = n;
}
}

enthält mehrere Variableninitialisierer. Sie enthält auch Konstruktorinitialisierer von beiden Formularen ( base
und this ). Das Beispiel entspricht dem unten gezeigten Code, wobei jeder Kommentar eine automatisch
eingefügte Anweisung angibt (die Syntax, die für die automatisch eingefügten Konstruktoraufrufe verwendet
wird, ist nicht gültig, dient lediglich zur Veranschaulichung des Mechanismus).
using System.Collections;

class A
{
int x, y, count;

public A() {
x = 1; // Variable initializer
y = -1; // Variable initializer
object(); // Invoke object() constructor
count = 0;
}

public A(int n) {
x = 1; // Variable initializer
y = -1; // Variable initializer
object(); // Invoke object() constructor
count = n;
}
}

class B: A
{
double sqrt2;
ArrayList items;
int max;

public B(): this(100) {


B(100); // Invoke B(int) constructor
items.Add("default");
}

public B(int n): base(n - 1) {


sqrt2 = Math.Sqrt(2.0); // Variable initializer
items = new ArrayList(100); // Variable initializer
A(n - 1); // Invoke A(int) constructor
max = n;
}
}

Standardkonstruktoren
Wenn eine Klasse keine Instanzkonstruktordeklarationen enthält, wird automatisch ein
Standardinstanzkonstruktor bereitgestellt. Dieser Standardkonstruktor ruft einfach den Parameter losen
Konstruktor der direkten Basisklasse auf. Wenn die Klasse abstrakt ist, wird die deklarierte Barrierefreiheit für
den Standardkonstruktor geschützt. Andernfalls ist die deklarierte Barrierefreiheit für den Standardkonstruktor
öffentlich. Daher ist der Standardkonstruktor immer das Formular.

protected C(): base() {}

oder

public C(): base() {}

dabei C ist der Name der Klasse. Wenn die Überladungs Auflösung keinen eindeutigen besten Kandidaten für
den basisklassenkonstruktorinitialisierer ermitteln kann, tritt ein Kompilierzeitfehler auf.
Im Beispiel
class Message
{
object sender;
string text;
}

ein Standardkonstruktor wird bereitgestellt, da die-Klasse keine Instanzkonstruktordeklarationen enthält.


Folglich entspricht das Beispiel genau dem

class Message
{
object sender;
string text;

public Message(): base() {}


}

Private Konstruktoren
Wenn eine Klasse T nur private Instanzkonstruktoren deklariert, ist es nicht möglich, dass Klassen außerhalb
des Programm Texts von T T oder direkt Instanzen von erstellen T . Wenn eine Klasse nur statische Member
enthält und nicht instanziiert werden soll, wird durch das Hinzufügen eines leeren privaten Instanzkonstruktors
eine Instanziierung verhindert. Beispiel:

public class Trig


{
private Trig() {} // Prevent instantiation

public const double PI = 3.14159265358979323846;

public static double Sin(double x) {...}


public static double Cos(double x) {...}
public static double Tan(double x) {...}
}

Die Trig -Klasse gruppiert verwandte Methoden und Konstanten, sollte jedoch nicht instanziiert werden. Daher
wird ein einzelner leerer privater Instanzkonstruktor deklariert. Mindestens ein Instanzkonstruktor muss
deklariert werden, um die automatische Generierung eines Standardkonstruktors zu unterdrücken.
Optionale instanzkonstruktorparameter
Die this(...) Form des konstruktorinitialisierers wird häufig in Verbindung mit überladen verwendet, um
optionale instanzkonstruktorparameter zu implementieren. Im Beispiel

class Text
{
public Text(): this(0, 0, null) {}

public Text(int x, int y): this(x, y, null) {}

public Text(int x, int y, string s) {


// Actual constructor implementation
}
}

die ersten beiden Instanzkonstruktoren stellen lediglich die Standardwerte für die fehlenden Argumente bereit.
Beide verwenden einen this(...) Konstruktorinitialisierer, um den dritten Instanzkonstruktor aufzurufen, der
tatsächlich die Initialisierung der neuen Instanz bewirkt. Der Effekt besteht aus den optionalen
Konstruktorparametern:

Text t1 = new Text(); // Same as Text(0, 0, null)


Text t2 = new Text(5, 10); // Same as Text(5, 10, null)
Text t3 = new Text(5, 20, "Hello");

Statische Konstruktoren
Ein *statischer Konstruktor _ ist ein Member, der die zum Initialisieren eines geschlossenen Klassen Typs
erforderlichen Aktionen implementiert. Statische Konstruktoren werden mit _static_constructor_declaration * s
deklariert:

static_constructor_declaration
: attributes? static_constructor_modifiers identifier '(' ')' static_constructor_body
;

static_constructor_modifiers
: 'extern'? 'static'
| 'static' 'extern'?
| static_constructor_modifiers_unsafe
;

static_constructor_body
: block
| ';'
;

Eine static_constructor_declaration kann einen Satz von Attributen (Attribute) und einen extern Modifizierer
(Externe Methoden) enthalten.
Der Bezeichner eines static_constructor_declaration muss der Klasse, in der der statische Konstruktor deklariert
ist, einen Namen benennen. Wenn ein anderer Name angegeben wird, tritt ein Kompilierzeitfehler auf.
Wenn eine statische Konstruktordeklaration einen extern Modifizierer enthält, wird der statische Konstruktor
als *externer statischer Konstruktor _ bezeichnet. Da eine externe statische Konstruktordeklaration keine
tatsächliche Implementierung bereitstellt, besteht die _static_constructor_body * aus einem Semikolon. Für alle
anderen statischen Konstruktordeklarationen besteht die static_constructor_body aus einem- Block , der die
auszuführenden Anweisungen angibt, um die-Klasse zu initialisieren. Dies entspricht exakt dem method_body
einer statischen Methode mit einem void Rückgabetyp (Methoden Text).
Statische Konstruktoren werden nicht geerbt und können nicht direkt aufgerufen werden.
Der statische Konstruktor für einen geschlossenen Klassentyp wird höchstens einmal in einer bestimmten
Anwendungsdomäne ausgeführt. Die Ausführung eines statischen Konstruktors wird ausgelöst, wenn das erste
der folgenden Ereignisse in einer Anwendungsdomäne auftritt:
Eine Instanz des-Klassen Typs wird erstellt.
Auf alle statischen Member des Klassen Typs wird verwiesen.
Wenn eine Klasse die Main Methode (Anwendungsstart) enthält, in der die Ausführung beginnt, wird der
statische Konstruktor für diese Klasse ausgeführt, bevor die- Main Methode aufgerufen wird.
Um einen neuen geschlossenen Klassentyp zu initialisieren, wird zuerst ein neuer Satz statischer Felder
(statische Felder und Instanzfelder) für diesen bestimmten geschlossenen Typ erstellt. Jedes der statischen
Felder wird mit dem Standardwert initialisiert (Standardwerte). Als nächstes werden die statischen
Feldinitialisierer (statische Feld Initialisierung) für diese statischen Felder ausgeführt. Schließlich wird der
statische Konstruktor ausgeführt.
Das Beispiel

using System;

class Test
{
static void Main() {
A.F();
B.F();
}
}

class A
{
static A() {
Console.WriteLine("Init A");
}
public static void F() {
Console.WriteLine("A.F");
}
}

class B
{
static B() {
Console.WriteLine("Init B");
}
public static void F() {
Console.WriteLine("B.F");
}
}

die Ausgabe muss erzeugt werden:

Init A
A.F
Init B
B.F

, da der A statische Konstruktor der Ausführung durch den-aufzurufenden aufgerufen wird A.F und die
Ausführung des B statischen Konstruktors durch den-Befehl ausgelöst wird B.F .
Es ist möglich, zirkuläre Abhängigkeiten zu erstellen, die es ermöglichen, dass statische Felder mit
Variableninitialisierern im Standardwert Zustand beobachtet werden.
Das Beispiel
using System;

class A
{
public static int X;

static A() {
X = B.Y + 1;
}
}

class B
{
public static int Y = A.X + 1;

static B() {}

static void Main() {


Console.WriteLine("X = {0}, Y = {1}", A.X, B.Y);
}
}

erzeugt die Ausgabe

X = 1, Y = 2

Zum Ausführen der Main -Methode führt das System zuerst den Initialisierer für aus B.Y , bevor der B
statische Konstruktor der Klasse ausgeführt wird. Y der Initialisierer bewirkt A , dass der statische Konstruktor
ausgeführt wird, da auf den Wert von A.X verwiesen wird. Der statische Konstruktor von A führt wiederum
den Wert von aus und ruft dabei X den Standardwert von ab, der 0 (null) Y ist. A.X wird daher mit 1
initialisiert. Der Prozess der Ausführung der A statischen Feldinitialisierer und des statischen Konstruktors wird
dann abgeschlossen, wobei die Berechnung des Anfangs Werts von zurückgegeben Y wird. Dadurch wird das
Ergebnis 2.
Da der statische Konstruktor für jeden geschlossenen konstruierten Klassentyp genau einmal ausgeführt wird,
können Sie Laufzeitüberprüfungen für den Typparameter erzwingen, der zur Kompilierzeit nicht über
Einschränkungen (Typparameter Einschränkungen) überprüft werden kann. Der folgende Typ verwendet
beispielsweise einen statischen Konstruktor, um zu erzwingen, dass das Typargument eine-Enum ist:

class Gen<T> where T: struct


{
static Gen() {
if (!typeof(T).IsEnum) {
throw new ArgumentException("T must be an enum");
}
}
}

Destruktoren
Ein -**destrukturtor* _ ist ein Member, der die erforderlichen Aktionen zum Zerstörung einer Instanz einer
Klasse implementiert. Ein Dekonstruktor wird mit einem _destructor_declaration * deklariert:
destructor_declaration
: attributes? 'extern'? '~' identifier '(' ')' destructor_body
| destructor_declaration_unsafe
;

destructor_body
: block
| ';'
;

Eine destructor_declaration kann einen Satz von Attributen (Attribute) enthalten.


Der Bezeichner eines destructor_declaration muss der Klasse, in der der Dekonstruktor deklariert ist, einen
Namen benennen. Wenn ein anderer Name angegeben wird, tritt ein Kompilierzeitfehler auf.
Wenn eine dekonstruktordeklaration einen extern Modifizierer enthält, wird der Dekonstruktor als *externer
Dekonstruktor _ bezeichnet. Da eine externe dekonstruktordeklaration keine tatsächliche Implementierung
bereitstellt, besteht die _destructor_body * aus einem Semikolon. Für alle anderen destrukturtoren besteht die
destructor_body aus einem- Block , der die auszuführenden Anweisungen angibt, um eine Instanz der-Klasse zu
Zerstörung. Ein- destructor_body entspricht exakt dem method_body einer Instanzmethode mit einem void
Rückgabetyp (Methoden Text).
Deerdektoren werden nicht geerbt. Folglich hat eine Klasse keine Dekonstruktoren, die nicht die
Dekonstruktoren aufweisen, die in dieser Klasse deklariert werden können.
Da für einen Dekonstruktor keine Parameter erforderlich sind, kann er nicht überladen werden, sodass eine
Klasse höchstens einen Dekonstruktor aufweisen kann.
Dededektoren werden automatisch aufgerufen und können nicht explizit aufgerufen werden. Eine Instanz ist für
die Zerstörung infrage, wenn es für keinen Code mehr möglich ist, diese Instanz zu verwenden. Die Ausführung
des Dekonstruktors für die Instanz kann zu einem beliebigen Zeitpunkt erfolgen, nachdem die Instanz für eine
Zerstörung berechtigt ist. Wenn eine Instanz von zerstört wird, werden die Dekonstruktoren in der Vererbungs
Kette dieser Instanz in der richtigen Reihenfolge von der am wenigsten abgeleiteten zum geringsten
abgeleiteten aufgerufen. Ein Dekonstruktor kann in jedem Thread ausgeführt werden. Weitere Informationen zu
den Regeln, die bestimmen, wann und wie ein Dekonstruktor ausgeführt wird, finden Sie unter Automatische
Speicherverwaltung.
Die Ausgabe des Beispiels
using System;

class A
{
~A() {
Console.WriteLine("A's destructor");
}
}

class B: A
{
~B() {
Console.WriteLine("B's destructor");
}
}

class Test
{
static void Main() {
B b = new B();
b = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}

is

B's destructor
A's destructor

Da deerdektoren in einer Vererbungs Kette in der Reihenfolge von den meisten abgeleiteten zu den am
wenigsten abgeleiteten
Dededektoren werden implementiert, indem die virtuelle Methode Finalize für überschrieben wird
System.Object . C#-Programme dürfen diese Methode nicht überschreiben oder Sie direkt (oder überschreiben)
(oder überschreiben). Beispielsweise wird das Programm

class A
{
override protected void Finalize() {} // error

public void F() {


this.Finalize(); // error
}
}

enthält zwei Fehler.


Der Compiler verhält sich so, als ob diese Methode und über schreibungen davon überhaupt nicht vorhanden
sind. Dieses Programm ist also:

class A
{
void Finalize() {} // permitted
}

ist gültig, und die-Methode hat die System.Object - Finalize Methode ausgeblendet.
Eine Erläuterung des Verhaltens, wenn eine Ausnahme von einem debugtor ausgelöst wird, finden Sie unter so
werden Ausnahmen behandelt.

Iterators
Ein Funktionsmember (Funktionsmember), der mit einem Iteratorblock (Blocks) implementiert wird, wird als
Iterator bezeichnet.
Ein Iteratorblock kann als Text eines Funktionsmembers verwendet werden, solange der Rückgabetyp des
entsprechenden Funktionsmembers einer der Enumeratorschnittstellen (Enumeratorschnittstellen) oder einer
der Aufzähl Bare-Schnittstellen (Aufzähl Bare Schnittstellen) ist. Sie kann als method_body, operator_body oder
accessor_body auftreten, wohingegen Ereignisse, Instanzkonstruktoren, statische Konstruktoren und
Dekonstruktoren nicht als Iteratoren implementiert werden können.
Wenn ein Funktionsmember mit einem Iteratorblock implementiert wird, ist dies ein Kompilierzeitfehler für die
Liste formaler Parameter des Funktionsmembers, um beliebige- ref oder- out Parameter anzugeben.
Enumeratorschnittstellen
Die Enumeratorschnittstellen sind die nicht generische Schnittstelle System.Collections.IEnumerator und alle
Instanziierungen der generischen-Schnittstelle System.Collections.Generic.IEnumerator<T> . Aus Gründen der
Kürze werden diese Schnittstellen in diesem Kapitel als IEnumerator bzw. referenziert IEnumerator<T> .
Enumerable -Schnittstellen
Die Aufzähl Bare-Schnittstellen sind die nicht generische Schnittstelle System.Collections.IEnumerable und
alle Instanziierungen der generischen-Schnittstelle System.Collections.Generic.IEnumerable<T> . Aus Gründen
der Kürze werden diese Schnittstellen in diesem Kapitel als IEnumerable bzw. referenziert IEnumerable<T> .
Yield-Typ
Ein Iterator erzeugt eine Sequenz von Werten, die alle denselben Typ haben. Dieser Typ wird als Yield-Typ des
Iterators bezeichnet.
Der Yield-Typ eines Iterators, der oder zurückgibt, IEnumerator IEnumerable ist object .
Der Yield-Typ eines Iterators, der oder zurückgibt, IEnumerator<T> IEnumerable<T> ist T .
Enumeratorobjekte
Wenn ein Funktionsmember, der einen enumeratorschnittstellentyp zurückgibt, mit einem Iteratorblock
implementiert wird, führt der Aufruf des Funktionsmembers den Code nicht sofort im Iteratorblock aus.
Stattdessen wird ein Enumeratorobjekt erstellt und zurückgegeben. Dieses Objekt kapselt den im Iteratorblock
angegebenen Code, und die Ausführung des Codes im Iteratorblock tritt auf, wenn die-Methode des
Enumeratorobjekts MoveNext aufgerufen wird. Ein Enumeratorobjekt weist die folgenden Eigenschaften auf:
Es implementiert IEnumerator und IEnumerator<T> , wobei T der Yield-Typ des Iterators ist.
Sie implementiert System.IDisposable .
Sie wird mit einer Kopie der Argument Werte (sofern vorhanden) und dem Instanzwert initialisiert, der an
das Funktionsmember übermittelt wurde.
Sie hat vier mögliche Zustände: vor _, ** wird ausgeführt _, angeh_ alten und _nach*, und befindet sich
anfänglich im _ *before**-Zustand.
Bei einem Enumeratorobjekt handelt es sich in der Regel um eine Instanz einer vom Compiler generierten
Enumeratorklasse, die den Code im Iteratorblock kapselt und die Enumeratorschnittstellen implementiert, aber
andere Implementierungs Methoden sind möglich. Wenn eine Enumeratorklasse vom Compiler generiert wird,
wird diese Klasse direkt oder indirekt in der Klasse, die den Funktionsmember enthält, in die-Klasse eingefügt,
Sie verfügt über private zugreif barkeit und hat einen Namen, der fürdie Verwendung durch den Compiler
reserviert ist.
Ein Enumeratorobjekt kann mehr Schnittstellen implementieren, als die oben genannten.
In den folgenden Abschnitten wird das genaue Verhalten der MoveNext Current -,-und-Member Dispose der
IEnumerable -und-Schnittstellen Implementierungen beschrieben, die IEnumerable<T> von einem
Enumeratorobjekt bereitgestellt werden.
Beachten Sie, dass Enumeratorobjekte die-Methode nicht unterstützen IEnumerator.Reset . Das Aufrufen dieser
Methode bewirkt System.NotSupportedException , dass eine ausgelöst wird.
Die Methode "Design ext"
Die- Methode eines Enumeratorobjekts kapselt den Code eines Iteratorblocks. Durch Aufrufen der-
MoveNext
MoveNext Methode wird Code im Iteratorblock ausgeführt, und die- Current Eigenschaft des
Enumeratorobjekts wird entsprechend festgelegt. Die genaue Aktion, die von ausgeführt MoveNext wird, hängt
vom Status des Enumeratorobjekts ab, wenn MoveNext aufgerufen wird:
Wenn der Status des Enumeratorobjekts vor ist, wird aufgerufen MoveNext :
Ändert den Status in wird ausgeführ t .
Initialisiert die Parameter (einschließlich this ) des Iteratorblocks mit den Argument Werten und
dem Instanzwert, die beim Initialisieren des Enumeratorobjekts gespeichert wurden.
Führt den Iteratorblock von dem Anfang aus, bis die Ausführung unterbrochen wird (wie unten
beschrieben).
Wenn der Status des Enumeratorobjekts ausgeführ t wird, ist das Ergebnis des Aufrufs MoveNext nicht
angegeben.
Wenn der Status des Enumeratorobjekts angehalten wird, wird aufgerufen MoveNext :
Ändert den Status in wird ausgeführ t .
Stellt die Werte aller lokalen Variablen und Parameter (einschließlich dieser) für die Werte wieder her,
die bei der letzten Ausführung des Iteratorblocks gespeichert wurden. Beachten Sie, dass sich der
Inhalt aller Objekte, auf die von diesen Variablen verwiesen wird, seit dem vorherigen-Befehl von
"muvenext" geändert hat.
Nimmt die Ausführung des Iteratorblocks unmittelbar nach der yield return Anweisung an, die die
Unterbrechung der Ausführung verursacht hat, und wird fortgesetzt, bis die Ausführung unterbrochen
wurde (wie unten beschrieben).
Wenn der Zustand des Enumeratorobjekts nach ist, wird der Aufruf von MoveNext zurückgegeben false .

Wenn MoveNext den Iteratorblock ausführt, kann die Ausführung auf vier Arten unterbrochen werden: durch
eine yield return Anweisung, durch eine- yield break Anweisung, durch das Ende des Iteratorblocks und
durch eine Ausnahme, die ausgelöst und aus dem Iteratorblock weitergegeben wird.
Wenn eine- yield return Anweisung gefunden wird (die yield-Anweisung):
Der in der-Anweisung angegebene Ausdruck wird ausgewertet, implizit in den Yield-Typ konvertiert
und der- Current Eigenschaft des Enumeratorobjekts zugewiesen.
Die Ausführung des iteratortexts wurde angehalten. Die Werte aller lokalen Variablen und Parameter
(einschließlich this ) werden gespeichert, ebenso wie der Speicherort dieser yield return
Anweisung. Wenn sich die- yield return Anweisung innerhalb eines oder mehrerer try Blöcke
befindet, werden die zugeordneten finally Blöcke zurzeit nicht ausgeführt.
Der Status des Enumeratorobjekts wird in " angehalten" geändert.
Die- MoveNext Methode kehrt an ihren Aufrufer zurück true und gibt an, dass die Iterationen
erfolgreich auf den nächsten Wert erweitert wurden.
Wenn eine- yield break Anweisung gefunden wird (die yield-Anweisung):
Wenn die yield break Anweisung innerhalb eines oder mehrerer try Blöcke liegt, werden die
zugeordneten finally Blöcke ausgeführt.
Der Status des Enumeratorobjekts wird in nach geändert.
Die MoveNext Methode kehrt an ihren Aufrufer zurück false und gibt an, dass die Iterations Funktion
vollständig ist.
Wenn das Ende des iteratortexts erreicht ist:
Der Status des Enumeratorobjekts wird in nach geändert.
Die MoveNext Methode kehrt an ihren Aufrufer zurück false und gibt an, dass die Iterations Funktion
vollständig ist.
Wenn eine Ausnahme ausgelöst und aus dem Iteratorblock weitergegeben wird:
finally Die entsprechenden Blöcke im iteratortext werden von der Ausnahme Weitergabe
ausgeführt.
Der Status des Enumeratorobjekts wird in nach geändert.
Die Ausnahme Weitergabe wird an den Aufrufer der-Methode weitergegeben MoveNext .
Die aktuelle Eigenschaft
Die-Eigenschaft eines Enumeratorobjekts wirkt Current sich yield return auf-Anweisungen im Iteratorblock
aus.
Wenn sich ein Enumeratorobjekt **im *** angehaltenen -Zustand befindet, ist der Wert von Current der Wert,
der durch den vorherigen-Befehl festgelegt wurde MoveNext . Wenn sich ein Enumeratorobjekt in den
Zuständen " before", " Running" oder " *after**" befindet, ist das Ergebnis des Zugriffs Current nicht
angegeben.
Bei einem Iterator mit einem anderen Yield-Typ als object entspricht das Ergebnis des Zugriffs auf Current
die-Implementierung des Enumeratorobjekts dem IEnumerable Zugriff Current über die-Implementierung des
Enumeratorobjekts IEnumerator<T> und das Umwandeln des Ergebnisses in object .
Die verwerfen-Methode
Die- Dispose Methode wird verwendet, um die Iterationen zu bereinigen, indem das Enumeratorobjekt in den
after -Zustand versetzt wird.
Wenn der Status des Enumeratorobjekts *vor _ ist, wird beim Aufrufen von Dispose der Zustand in _ after *
geändert.
Wenn der Status des Enumeratorobjekts ausgeführ t wird, ist das Ergebnis des Aufrufs Dispose nicht
angegeben.
Wenn der Status des Enumeratorobjekts angehalten wird, wird aufgerufen Dispose :
Ändert den Status in wird ausgeführ t .
Führt beliebige letzte Blöcke aus, als wäre die letzte ausgeführte yield return Anweisung eine-
yield break Anweisung. Wenn dies bewirkt, dass eine Ausnahme ausgelöst und aus dem iteratortext
weitergegeben wird, wird der Status des Enumeratorobjekts auf after festgelegt, und die Ausnahme
wird an den Aufrufer der Methode weitergegeben Dispose .
Ändert den Zustand in nach .
Wenn der Zustand des Enumeratorobjekts nach ist, hat der Aufruf von Dispose keine Auswirkungen.
Enumerable -Objekte
Wenn ein Funktionsmember, der einen Aufzähl baren Schnittstellentyp zurückgibt, mit einem Iteratorblock
implementiert wird, führt der Aufruf des Funktionsmembers den Code nicht sofort im Iteratorblock aus.
Stattdessen wird ein Aufzähl bares Objekt erstellt und zurückgegeben. Die-Methode des Aufzähl Bare-Objekts
GetEnumerator gibt ein Enumeratorobjekt zurück, das den im Iteratorblock angegebenen Code kapselt, und die
Ausführung des Codes im Iteratorblock tritt auf, wenn die-Methode des Enumeratorobjekts MoveNext
aufgerufen wird. Ein Aufzähl Bare-Objekt hat die folgenden Eigenschaften:
Es implementiert IEnumerable und IEnumerable<T> , wobei T der Yield-Typ des Iterators ist.
Sie wird mit einer Kopie der Argument Werte (sofern vorhanden) und dem Instanzwert initialisiert, der an
das Funktionsmember übermittelt wurde.
Ein Aufzähl Bare-Objekt ist in der Regel eine Instanz einer vom Compiler generierten Aufzähl Bare-Klasse, die
den Code im Iteratorblock kapselt und die Aufzähl Bare-Schnittstellen implementiert, aber andere
Implementierungs Methoden sind möglich. Wenn eine Aufzähl Bare-Klasse vom Compiler generiert wird, wird
diese Klasse direkt oder indirekt in der-Klasse, die den Funktionsmember enthält, in eine private Barrierefreiheit
eingefügt, und Sie erhält einen Namen, der für die Verwendung durch denCompiler (Bezeichner) reserviert ist.
Ein Aufzähl Bare-Objekt kann mehr Schnittstellen implementieren, als die oben genannten. Insbesondere kann
ein Aufzähl Bare-Objekt auch und implementieren IEnumerator IEnumerator<T> , sodass es sowohl als Aufzähl
Bare-als auch als Enumerator fungieren kann. Bei diesem Implementierungstyp GetEnumerator wird das Aufzähl
Bare Objekt selbst zurückgegeben, wenn die-Methode eines Aufzähl Bare-Objekts zum ersten Mal aufgerufen
wird. Nachfolgende Aufrufe des Aufzähl baren Objekts GetEnumerator geben, falls vorhanden, eine Kopie des
Aufzähl Bare-Objekts zurück. Folglich hat jeder zurückgegebene Enumerator seinen eigenen Zustand, und
Änderungen in einem Enumerator haben keine Auswirkung auf einen anderen Enumerator.
Die getenreerator-Methode
Ein Aufzähl Bare-Objekt stellt eine Implementierung der GetEnumerator Methoden der IEnumerable -
Schnittstelle und der-Schnittstelle bereit IEnumerable<T> . Die beiden GetEnumerator Methoden verwenden eine
gemeinsame-Implementierung, die ein verfügbares Enumeratorobjekt abruft und zurückgibt. Das
Enumeratorobjekt wird mit den Argument Werten und dem Instanzwert initialisiert, die gespeichert wurden, als
das Aufzähl Bare Objekt initialisiert wurde. andernfalls ist das Enumeratorobjekt wie in
enumeratorobjektenbeschrieben funktionsfähig.
Beispiel für die Implementierung
In diesem Abschnitt wird eine mögliche Implementierung von Iteratoren in Bezug auf Standard-c#-Konstrukte
beschrieben. Die hier beschriebene Implementierung basiert auf denselben Prinzipien, die vom Microsoft c#-
Compiler verwendet werden. Dies bedeutet jedoch nicht, dass es sich um eine vorgeschriebene
Implementierung oder die einzige Möglichkeit handelt.
Die folgende Stack<T> Klasse implementiert die- GetEnumerator Methode mit einem Iterator. Der Iterator listet
die Elemente des Stapels in der Reihenfolge von oben nach unten auf.
using System;
using System.Collections;
using System.Collections.Generic;

class Stack<T>: IEnumerable<T>


{
T[] items;
int count;

public void Push(T item) {


if (items == null) {
items = new T[4];
}
else if (items.Length == count) {
T[] newItems = new T[count * 2];
Array.Copy(items, 0, newItems, 0, count);
items = newItems;
}
items[count++] = item;
}

public T Pop() {
T result = items[--count];
items[count] = default(T);
return result;
}

public IEnumerator<T> GetEnumerator() {


for (int i = count - 1; i >= 0; --i) yield return items[i];
}
}

Die- GetEnumerator Methode kann in eine Instanziierung einer vom Compiler generierten Enumeratorklasse
übersetzt werden, die den Code im Iteratorblock kapselt, wie im folgenden gezeigt.
class Stack<T>: IEnumerable<T>
{
...

public IEnumerator<T> GetEnumerator() {


return new __Enumerator1(this);
}

class __Enumerator1: IEnumerator<T>, IEnumerator


{
int __state;
T __current;
Stack<T> __this;
int i;

public __Enumerator1(Stack<T> __this) {


this.__this = __this;
}

public T Current {
get { return __current; }
}

object IEnumerator.Current {
get { return __current; }
}

public bool MoveNext() {


switch (__state) {
case 1: goto __state1;
case 2: goto __state2;
}
i = __this.count - 1;
__loop:
if (i < 0) goto __state2;
__current = __this.items[i];
__state = 1;
return true;
__state1:
--i;
goto __loop;
__state2:
__state = 2;
return false;
}

public void Dispose() {


__state = 2;
}

void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}

In der obigen Übersetzung wird der Code im Iteratorblock in einen Zustands Automat umgewandelt und in die-
MoveNext Methode der Enumeratorklasse eingefügt. Außerdem wird die lokale Variable i in ein Feld im
Enumeratorobjekt umgewandelt, sodass Sie weiterhin über Aufrufe von vorhanden sein kann MoveNext .
Im folgenden Beispiel wird eine einfache Multiplikationstabelle der ganzen Zahlen 1 bis 10 gedruckt. Die
FromTo -Methode im Beispiel gibt ein Aufzähl bares Objekt zurück und wird mit einem Iterator implementiert.
using System;
using System.Collections.Generic;

class Test
{
static IEnumerable<int> FromTo(int from, int to) {
while (from <= to) yield return from++;
}

static void Main() {


IEnumerable<int> e = FromTo(1, 10);
foreach (int x in e) {
foreach (int y in e) {
Console.Write("{0,3} ", x * y);
}
Console.WriteLine();
}
}
}

Die- FromTo Methode kann in eine Instanziierung einer vom Compiler generierten Aufzähl Bare-Klasse
übersetzt werden, die den Code im Iteratorblock kapselt, wie im folgenden gezeigt.

using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;

class Test
{
...

static IEnumerable<int> FromTo(int from, int to) {


return new __Enumerable1(from, to);
}

class __Enumerable1:
IEnumerable<int>, IEnumerable,
IEnumerator<int>, IEnumerator
{
int __state;
int __current;
int __from;
int from;
int to;
int i;

public __Enumerable1(int __from, int to) {


this.__from = __from;
this.to = to;
}

public IEnumerator<int> GetEnumerator() {


__Enumerable1 result = this;
if (Interlocked.CompareExchange(ref __state, 1, 0) != 0) {
result = new __Enumerable1(__from, to);
result.__state = 1;
}
result.from = result.__from;
return result;
}

IEnumerator IEnumerable.GetEnumerator() {
return (IEnumerator)GetEnumerator();
}
public int Current {
get { return __current; }
}

object IEnumerator.Current {
get { return __current; }
}

public bool MoveNext() {


switch (__state) {
case 1:
if (from > to) goto case 2;
__current = from++;
__state = 1;
return true;
case 2:
__state = 2;
return false;
default:
throw new InvalidOperationException();
}
}

public void Dispose() {


__state = 2;
}

void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}

Die Aufzähl Bare-Klasse implementiert sowohl die Aufzähl Bare-Schnittstelle als auch die
Enumeratorschnittstellen, sodass Sie sowohl als Aufzähl Bare-als auch als Enumerator fungieren kann. Wenn
die-Methode zum ersten Mal GetEnumerator aufgerufen wird, wird das Aufzähl Bare-Objekt selbst
zurückgegeben. Nachfolgende Aufrufe des Aufzähl baren Objekts GetEnumerator geben, falls vorhanden, eine
Kopie des Aufzähl Bare-Objekts zurück. Folglich hat jeder zurückgegebene Enumerator seinen eigenen Zustand,
und Änderungen in einem Enumerator haben keine Auswirkung auf einen anderen Enumerator. Die
Interlocked.CompareExchange -Methode wird verwendet, um einen Thread sicheren Vorgang sicherzustellen.

Der from-Parameter und der- to Parameter werden in Felder in der Aufzähl Bare-Klasse umgewandelt. Da
from im Iteratorblock geändert wird, wird ein zusätzliches __from Feld eingeführt, das den Anfangswert
enthält, der für from jeden Enumerator angegeben wird.
Die Methode löst eine aus, InvalidOperationException Wenn Sie aufgerufen wird, wenn den Wert hat
MoveNext
__state 0 . Dadurch wird verhindert, dass das Aufzähl Bare Objekt als Enumeratorobjekt verwendet wird,
ohne dass zuerst aufgerufen wird GetEnumerator .
Das folgende Beispiel zeigt eine einfache Strukturklasse. Die- Tree<T> Klasse implementiert die- GetEnumerator
Methode mit einem Iterator. Der Iterator listet die Elemente der Struktur in der Infix-Reihenfolge auf.
using System;
using System.Collections.Generic;

class Tree<T>: IEnumerable<T>


{
T value;
Tree<T> left;
Tree<T> right;

public Tree(T value, Tree<T> left, Tree<T> right) {


this.value = value;
this.left = left;
this.right = right;
}

public IEnumerator<T> GetEnumerator() {


if (left != null) foreach (T x in left) yield x;
yield value;
if (right != null) foreach (T x in right) yield x;
}
}

class Program
{
static Tree<T> MakeTree<T>(T[] items, int left, int right) {
if (left > right) return null;
int i = (left + right) / 2;
return new Tree<T>(items[i],
MakeTree(items, left, i - 1),
MakeTree(items, i + 1, right));
}

static Tree<T> MakeTree<T>(params T[] items) {


return MakeTree(items, 0, items.Length - 1);
}

// The output of the program is:


// 1 2 3 4 5 6 7 8 9
// Mon Tue Wed Thu Fri Sat Sun

static void Main() {


Tree<int> ints = MakeTree(1, 2, 3, 4, 5, 6, 7, 8, 9);
foreach (int i in ints) Console.Write("{0} ", i);
Console.WriteLine();

Tree<string> strings = MakeTree(


"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun");
foreach (string s in strings) Console.Write("{0} ", s);
Console.WriteLine();
}
}

Die- GetEnumerator Methode kann in eine Instanziierung einer vom Compiler generierten Enumeratorklasse
übersetzt werden, die den Code im Iteratorblock kapselt, wie im folgenden gezeigt.

class Tree<T>: IEnumerable<T>


{
...

public IEnumerator<T> GetEnumerator() {


return new __Enumerator1(this);
}

class __Enumerator1 : IEnumerator<T>, IEnumerator


{
Node<T> __this;
Node<T> __this;
IEnumerator<T> __left, __right;
int __state;
T __current;

public __Enumerator1(Node<T> __this) {


this.__this = __this;
}

public T Current {
get { return __current; }
}

object IEnumerator.Current {
get { return __current; }
}

public bool MoveNext() {


try {
switch (__state) {

case 0:
__state = -1;
if (__this.left == null) goto __yield_value;
__left = __this.left.GetEnumerator();
goto case 1;

case 1:
__state = -2;
if (!__left.MoveNext()) goto __left_dispose;
__current = __left.Current;
__state = 1;
return true;

__left_dispose:
__state = -1;
__left.Dispose();

__yield_value:
__current = __this.value;
__state = 2;
return true;

case 2:
__state = -1;
if (__this.right == null) goto __end;
__right = __this.right.GetEnumerator();
goto case 3;

case 3:
__state = -3;
if (!__right.MoveNext()) goto __right_dispose;
__current = __right.Current;
__state = 3;
return true;

__right_dispose:
__state = -1;
__right.Dispose();

__end:
__state = 4;
break;

}
}
finally {
if (__state < 0) Dispose();
}
return false;
return false;
}

public void Dispose() {


try {
switch (__state) {

case 1:
case -2:
__left.Dispose();
break;

case 3:
case -3:
__right.Dispose();
break;

}
}
finally {
__state = 4;
}
}

void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}

Die vom Compiler generierten temporare, die in den foreach -Anweisungen verwendet werden, werden in die
__left __right Felder und des Enumeratorobjekts gehoben. Das- __state Feld des Enumeratorobjekts wird
sorgfältig aktualisiert, sodass die richtige Dispose() Methode ordnungsgemäß aufgerufen wird, wenn eine
Ausnahme ausgelöst wird. Beachten Sie, dass es nicht möglich ist, den übersetzten Code mit einfachen
Anweisungen zu schreiben foreach .

Asynchrone Funktionen
Eine Methode (Methoden) oder eine anonyme Funktion (Anonyme Funktions Ausdrücke) mit dem- async
Modifizierer wird als *Async-Funktion _ bezeichnet. Im Allgemeinen wird der Begriff _ Async* verwendet, um
alle Arten von Funktionen zu beschreiben, die den- async Modifizierer haben.
Es handelt sich um einen Kompilierzeitfehler für die Liste formaler Parameter einer Async-Funktion, um
beliebige- ref oder- out Parameter anzugeben.
Der return_type einer Async-Methode muss entweder void oder ein Tasktyp sein. Die Aufgaben Typen sind
System.Threading.Tasks.Task -und-Typen, die aus erstellt werden System.Threading.Tasks.Task<T> . Der Kürze
halber wird in diesem Kapitel auf diese Typen als Task Task<T> bzw. verwiesen. Eine Async-Methode, die einen
Tasktyp zurückgibt, wird als Aufgaben Rückgabe bezeichnet.
Die genaue Definition der Aufgaben Typen ist implementiert, aber aus der Sicht der Sprache befindet sich ein
Aufgabentyp in einem der Zustände unvollständig, erfolgreich oder fehlerhaft. Eine fehlerhafte Aufgabe zeichnet
eine relevante Ausnahme auf. Ein erfolgreicher Datensatz Task<T> zeichnet ein Ergebnis des Typs auf T .
Aufgaben Typen sind möglich und können daher die Operanden von Erwartungs Ausdrücken
(ErwartungsAusdrücke) sein.
Ein Async-Funktionsaufruf bietet die Möglichkeit zum Aussetzen der Auswertung mithilfe von Erwartungs
Ausdrücken (ErwartungsAusdrücke) im Textkörper. Die Auswertung kann später an dem Punkt fortgesetzt
werden, an dem der aufrufende Erwartungs Ausdruck über einen -**Wiederaufnahme* Delegaten _ durchführt.
Der Wiederaufnahme Delegat ist vom Typ System.Action , und wenn er aufgerufen wird, wird die Auswertung
des asynchronen Funktions aufrutens aus dem Erwartungs Ausdruck fortgesetzt, an dem er unterbrochen
wurde. Der _ Current-Aufrufer * eines Async-Funktions Aufrufers ist der ursprüngliche Aufrufer, wenn der
Funktionsaufruf nie angehalten wurde, andernfalls der letzte Aufrufer des Wiederaufnahme Delegaten.
Auswertung einer Aufgaben Rückgabe Async-Funktion
Durch den Aufruf einer Aufgaben Rückgabe Async-Funktion wird eine Instanz des zurückgegebenen Aufgaben
Typs generiert. Dies wird als Rückgabe Task der Async-Funktion bezeichnet. Der Task befindet sich anfänglich
in einem unvollständigen Zustand.
Der asynchrone Funktions Text wird dann ausgewertet, bis er entweder angehalten wird (indem ein Erwartungs
Ausdruck erreicht wird) oder beendet wird, an dem die Punkt Steuerung zusammen mit der Rückgabe Aufgabe
an den Aufrufer zurückgegeben wird.
Wenn der Text der Async-Funktion beendet wird, wird die Rückgabe Aufgabe aus dem unvollständigen Zustand
verschoben:
Wenn der Funktions Rumpf durch das Erreichen einer Return-Anweisung oder des Endes des Texts beendet
wird, werden alle Ergebnis Werte in der Rückgabe Aufgabe aufgezeichnet, die in den Status "erfolgreich"
versetzt wird.
Wenn der Funktions Text als Ergebnis einer nicht abgefangenen Ausnahme (der throw-Anweisung) beendet
wird, wird die Ausnahme in der Rückgabe Aufgabe aufgezeichnet, die in einen fehlerhaften Zustand versetzt
wird.
Auswertung einer "void"-Rückgabe Async-Funktion
Wenn der Rückgabetyp der Async-Funktion ist void , unterscheidet sich die Auswertung auf folgende Weise
von der obigen: da keine Aufgabe zurückgegeben wird, kommuniziert die Funktion stattdessen mit Abschluss
und Ausnahmen mit dem Synchronisierungs Kontext des aktuellen Threads. Die genaue Definition des
Synchronisierungs Kontexts ist implementierungsabhängig, ist jedoch eine Darstellung von "Where", in der der
aktuelle Thread ausgeführt wird. Der Synchronisierungs Kontext wird benachrichtigt, wenn die Auswertung
einer Async-Funktion mit void-Rückgabe beginnt, erfolgreich abgeschlossen wird oder eine nicht abgefangene
Ausnahme ausgelöst wird.
Dies ermöglicht es dem Kontext nachzuverfolgen, wie viele void-zurückgegebene asynchrone Funktionen
darunter ausgeführt werden, und um zu entscheiden, wie Ausnahmen weitergegeben werden sollen, die aus
ihnen stammen.
Strukturen
04.11.2021 • 30 minutes to read

Strukturen ähneln Klassen darin, dass Sie Datenstrukturen darstellen, die Datenmember und Funktionsmember
enthalten können. Im Unterschied zu Klassen sind Strukturen jedoch Werttypen und erfordern keine Heap
Zuordnung. Eine Variable eines Struktur Typs enthält direkt die Daten der Struktur, während eine Variable eines
Klassen Typs einen Verweis auf die Daten enthält, wobei es sich um ein Objekt handelt, das als Objekt bezeichnet
wird.
Strukturen sind besonders nützlich für kleine Datenstrukturen, die über Wertsemantik verfügen. Komplexe
Zahlen, Punkte in einem Koordinatensystem oder Schlüssel-Wert-Paare im Wörterbuch sind gute Beispiele für
Strukturen. Der Schlüssel für diese Datenstrukturen besteht darin, dass Sie nur wenige Datenmember haben,
dass Sie keine Vererbung oder referenzielle Identität benötigen, und dass Sie mithilfe der Wert Semantik, bei
der die Zuweisung den Wert anstelle des Verweises kopiert, bequem implementiert werden können.
Wie in einfache Typenbeschrieben, sind die einfachen Typen, die von c# bereitgestellt werden, z int . b double
., und bool , tatsächlich alle Strukturtypen. Ebenso wie diese vordefinierten Typen Strukturen sind, ist es auch
möglich, Strukturen und Operator Überladung zu verwenden, um neue "primitive" Typen in der
Programmiersprache c# zu implementieren. Am Ende dieses Kapitels (Struktur Beispiele) werden zwei Beispiele
für solche Typen angegeben.

Struct declarations (Strukturdeklarationen)


Eine struct_declaration ist eine type_declaration (Typdeklarationen), die eine neue Struktur deklariert:

struct_declaration
: attributes? struct_modifier* 'partial'? 'struct' identifier type_parameter_list?
struct_interfaces? type_parameter_constraints_clause* struct_body ';'?
;

Ein struct_declaration besteht aus einem optionalen Satz von Attributen (Attributen), gefolgt von einer
optionalen Gruppe von struct_modifier s (Strukturmodifizierern), gefolgt von einem partial optionalen-
Modifizierer, gefolgt vom-Schlüsselwort struct und einem Bezeichner , der die Struktur benennt, gefolgt von
einer optionalen type_parameter_list Specification (Typparameter), gefolgt von einer optionalen struct_interfaces
Spezifikation (partieller Modifizierer), gefolgt von einer optionalen type_parameter_constraints_clause s-
Spezifikation (Typparameter Einschränkungen), gefolgt von einer struct_body (Struktur Text), optional gefolgt
von einem Semikolon
Strukturmodifizierer
Eine struct_declaration kann optional eine Sequenz von strukturmodifizierermodifizierer einschließen:

struct_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| struct_modifier_unsafe
;

Es ist ein Kompilierzeitfehler, damit derselbe Modifizierer mehrmals in einer Struktur Deklaration angezeigt wird.
Die Modifizierer einer Struktur Deklaration haben dieselbe Bedeutung wie die einer Klassen Deklaration (Klassen
Deklarationen).
Partieller Modifizierer
Der- partial Modifizierer gibt an, dass dieser struct_declaration eine partielle Typdeklaration ist. Mehrere
partielle Struktur Deklarationen mit demselben Namen innerhalb eines einschließenden Namespace oder einer
Typdeklaration kombinieren eine Struktur Deklaration, die den in partiellen Typenangegebenen Regeln folgt.
Struktur Schnittstellen
Eine Struktur Deklaration kann eine struct_interfaces Spezifikation enthalten. in diesem Fall wird die Struktur so
bezeichnet, dass die angegebenen Schnittstellentypen direkt implementiert werden.

struct_interfaces
: ':' interface_type_list
;

Schnittstellen Implementierungen werden in Schnittstellen Implementierungenausführlicher erläutert.


Struktur Text
Der struct_body einer Struktur definiert die Member der Struktur.

struct_body
: '{' struct_member_declaration* '}'
;

Struct members (Strukturmember)


Die Member einer Struktur bestehen aus den Membern, die von den struct_member_declaration en eingeführt
wurden, und den Membern, die vom-Typ geerbt wurden System.ValueType .

struct_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| static_constructor_declaration
| type_declaration
| struct_member_declaration_unsafe
;

Mit Ausnahme der Unterschiede in den Klassen-und Struktur unterschiedengelten die Beschreibungen von
Klassenmembern, die in Klassenmembern durch Async-Funktionen bereitgestellt werden, auch für
Strukturmember.

Class and struct differences (Klassen- und Strukturdifferenzen)


Strukturen unterscheiden sich in mehreren wichtigen Punkten von Klassen:
Strukturen sind Werttypen (Wert Semantik).
Alle Strukturtypen erben implizit von der-Klasse System.ValueType (Vererbung).
Durch die Zuweisung zu einer Variablen eines Struktur Typs wird eine Kopie des zugewiesenen Werts
(Zuweisung) erstellt.
Der Standardwert einer Struktur ist der Wert, der erzeugt wird, indem alle Werttyp Felder auf ihren
Standardwert und alle Verweistyp Felder auf null (Standardwerte) festgelegt werden.
Boxing-und Unboxing-Vorgänge werden verwendet, um zwischen einem Strukturtyp und object (Boxing
und Unboxing) zu konvertieren.
Die Bedeutung von this unterscheidet sich für Strukturen (dieser Zugriff).
Instanzfelddeklarationen für eine Struktur dürfen keine Variableninitialisierer (Feldinitialisierer) enthalten.
Eine Struktur darf keinen Parameter losen Instanzenkonstruktor (Konstruktoren) deklarieren.
Es ist nicht zulässig, einen destrukturtor (destrukturtoren) zu deklarieren.
Wert Semantik
Strukturen sind Werttypen (Werttypen) und werden als Wert Semantik bezeichnet. Klassen hingegen sind
Verweis Typen (Verweis Typen) und werden als Verweis Semantik bezeichnet.
Eine Variable eines Struktur Typs enthält direkt die Daten der Struktur, während eine Variable eines Klassen Typs
einen Verweis auf die Daten enthält, wobei es sich um ein Objekt handelt, das als Objekt bezeichnet wird. Wenn
eine Struktur B ein Instanzfeld vom Typ enthält A und A ein Strukturtyp ist, ist dies ein Kompilierzeitfehler für
A , von dem abhängig ist, B oder ein von konstruierter Typ B . Eine Struktur X * hängt direkt von _ einer
Struktur ab, Y Wenn X ein Instanzfeld vom Typ enthält Y . Bei dieser Definition ist der gesamte Satz von
Strukturen, von denen eine Struktur abhängt, das transitiv Schließen der _ direkt abhängig von * Relationship.
Beispiel:

struct Node
{
int data;
Node next; // error, Node directly depends on itself
}

ist ein Fehler, da Node ein Instanzfeld seines eigenen Typs enthält. Ein weiteres Beispiel

struct A { B b; }

struct B { C c; }

struct C { A a; }

ist ein Fehler, da jeder der Typen A , B und voneinander C abhängig ist.
Mit-Klassen können zwei Variablen auf das gleiche Objekt verweisen, und so können Vorgänge in einer
Variablen das Objekt beeinflussen, auf das von der anderen Variablen verwiesen wird. Bei Strukturen verfügen
die Variablen jeweils über eine eigene Kopie der Daten (außer im Fall von ref -und- out Parameter Variablen),
und es ist nicht möglich, dass sich Vorgänge auf einem anderen befinden. Da Strukturen keine Verweis Typen
sind, ist es außerdem nicht möglich, dass die Werte eines Struktur Typs sind null .
Bei Angabe der Deklaration
struct Point
{
public int x, y;

public Point(int x, int y) {


this.x = x;
this.y = y;
}
}

das Code Fragment

Point a = new Point(10, 10);


Point b = a;
a.x = 100;
System.Console.WriteLine(b.x);

Gibt den Wert aus 10 . Die Zuweisung von a zum Erstellen b einer Kopie des Werts und ist folglich b von
der Zuweisung zu nicht betroffen a.x . Hätte Point stattdessen als Klasse deklariert werden, würde die
Ausgabe lauten, 100 weil a und auf b dasselbe Objekt verweisen würden.
Vererbung
Alle Strukturtypen erben implizit von der-Klasse System.ValueType , die wiederum von der-Klasse erbt object .
Eine Struktur Deklaration kann eine Liste der implementierten Schnittstellen angeben, aber es ist nicht möglich,
dass eine Struktur Deklaration eine Basisklasse angibt.
Strukturtypen sind niemals abstrakt und sind immer implizit versiegelt. Die abstract sealed Modifizierer und
sind daher in einer Struktur Deklaration nicht zulässig.
Da die Vererbung für Strukturen nicht unterstützt wird, kann der deklarierte Zugriff auf einen Strukturmember
nicht protected oder sein protected internal .
Funktionsmember in einer Struktur können nicht abstract oder sein virtual , und der- override Modifizierer
darf nur Methoden überschreiben, die von geerbt wurden System.ValueType .
Zuweisung
Durch die Zuweisung zu einer Variablen eines Struktur Typs wird eine Kopie des zugewiesenen Werts erstellt.
Dies unterscheidet sich von der Zuweisung zu einer Variablen eines Klassen Typs, die den Verweis, aber nicht
das durch den Verweis identifizierte Objekt kopiert.
Ähnlich wie bei einer Zuweisung wird eine Kopie der Struktur erstellt, wenn eine Struktur als Wert Parameter
übergeben oder als Ergebnis eines Funktionsmembers zurückgegeben wird. Eine Struktur kann mithilfe eines-
oder-Parameters als Verweis an einen Funktionsmember übergeben werden ref out .
Wenn eine Eigenschaft oder ein Indexer einer Struktur das Ziel einer Zuweisung ist, muss der Instanzausdruck,
der der Eigenschaft oder dem Indexer-Zugriff zugeordnet ist, als Variable klassifiziert werden. Wenn der
Instanzausdruck als Wert klassifiziert wird, tritt ein Kompilierzeitfehler auf. Dies wird in " einfache Zuweisung"
ausführlicher beschrieben.
Standardwerte
Wie in Standardwertebeschrieben, werden bei der Erstellung automatisch verschiedene Arten von Variablen auf
ihren Standardwert initialisiert. Für Variablen von Klassentypen und anderen Verweis Typen ist dieser
Standardwert null . Da Strukturen jedoch Werttypen sind, die nicht sein dürfen null , ist der Standardwert
einer Struktur der Wert, der erzeugt wird, indem alle Werttyp Felder auf ihren Standardwert und alle Verweistyp
Felder auf festgelegt werden null .
Point Im Beispiel wird auf die oben deklarierte Struktur verwiesen.

Point[] a = new Point[100];

Initialisiert jede Point im-Array mit dem Wert, der erstellt wird, indem die x Felder und auf NULL festgelegt
werden y .
Der Standardwert einer Struktur entspricht dem Wert, der vom Standardkonstruktor der Struktur
zurückgegeben wird (Standardkonstruktoren). Anders als bei einer Klasse ist es nicht zulässig, einen Parameter
losen Instanzkonstruktor zu deklarieren. Stattdessen verfügt jede Struktur implizit über einen Parameter losen
Instanzenkonstruktor, der immer den Wert zurückgibt, der sich aus dem Festlegen aller Werttyp Felder auf ihren
Standardwert und alle Verweistyp Felder auf ergibt null .
Strukturen sollten so entworfen werden, dass der standardmäßige Initialisierungs Zustand einen gültigen Status
aufweist. Im Beispiel

using System;

struct KeyValuePair
{
string key;
string value;

public KeyValuePair(string key, string value) {


if (key == null || value == null) throw new ArgumentException();
this.key = key;
this.value = value;
}
}

der benutzerdefinierte Instanzkonstruktor schützt nur bei NULL-Werten, wenn er explizit aufgerufen wird. In
Fällen, in denen eine KeyValuePair Variable der Standardwert Initialisierung unterliegt, sind die key value
Felder und NULL, und die Struktur muss darauf vorbereitet sein, diesen Zustand zu verarbeiten.
Boxing und Unboxing
Ein Wert eines Klassen Typs kann in den Typ object oder in einen Schnittstellentyp konvertiert werden, der von
der-Klasse implementiert wird, indem der Verweis zum Zeitpunkt der Kompilierung als anderer Typ behandelt
wird. Ebenso kann ein Wert vom Typ object oder ein Wert eines Schnittstellen Typs zurück in einen Klassentyp
konvertiert werden, ohne den Verweis zu ändern (in diesem Fall ist jedoch eine Lauf Zeittyp Überprüfung
erforderlich).
Da Strukturen keine Verweis Typen sind, werden diese Vorgänge für Strukturtypen unterschiedlich
implementiert. Wenn ein Wert eines Struktur Typs in object den Typ oder in einen Schnittstellentyp konvertiert
wird, der von der Struktur implementiert wird, wird ein Boxing-Vorgang durchgeführt. Wenn ein Wert des Typs
object oder ein Wert eines Schnittstellen Typs zurück in einen Strukturtyp konvertiert wird, findet auch ein
Unboxing-Vorgang statt. Ein wichtiger Unterschied von denselben Vorgängen für Klassentypen besteht darin,
dass Boxing und Unboxing den Struktur Wert entweder in die oder aus der geachtelten Instanz kopieren.
Folglich werden Änderungen an der Unboxing-Struktur, die an der Unboxing-Struktur vorgenommen werden,
nicht in der Struktur der Struktur spiegele widergespiegelt.
Wenn ein Strukturtyp eine von geerbte virtuelle Methode überschreibt (z. b., System.Object Equals
GetHashCode oder ToString ), bewirkt der Aufruf der virtuellen Methode über eine Instanz des Struktur Typs
nicht, dass Boxing erfolgt. Dies gilt auch, wenn die Struktur als Typparameter verwendet wird und der Aufruf
durch eine Instanz des Typparameter Typs erfolgt. Beispiel:
using System;

struct Counter
{
int value;

public override string ToString() {


value++;
return value.ToString();
}
}

class Program
{
static void Test<T>() where T: new() {
T x = new T();
Console.WriteLine(x.ToString());
Console.WriteLine(x.ToString());
Console.WriteLine(x.ToString());
}

static void Main() {


Test<Counter>();
}
}

Die Ausgabe des Programms lautet wie folgt:

1
2
3

Obwohl es für ToString den Fall, dass es nicht möglich ist, Nebeneffekte hat, zeigt das Beispiel, dass kein Boxing
für die drei Aufrufe von erfolgt ist x.ToString() .
Auf ähnliche Weise tritt beim Zugriff auf ein Element in einem eingeschränkten Typparameter nie implizit ein
Boxing auf. Angenommen, eine Schnittstelle ICounter enthält eine Methode, Increment die zum Ändern eines
Werts verwendet werden kann. Wenn ICounter als Einschränkung verwendet wird, wird die Implementierung
der- Increment Methode mit einem Verweis auf die Variable aufgerufen, die Increment für aufgerufen wurde,
nie eine geachtelte Kopie.
using System;

interface ICounter
{
void Increment();
}

struct Counter: ICounter


{
int value;

public override string ToString() {


return value.ToString();
}

void ICounter.Increment() {
value++;
}
}

class Program
{
static void Test<T>() where T: ICounter, new() {
T x = new T();
Console.WriteLine(x);
x.Increment(); // Modify x
Console.WriteLine(x);
((ICounter)x).Increment(); // Modify boxed copy of x
Console.WriteLine(x);
}

static void Main() {


Test<Counter>();
}
}

Der erste-Befehl, Increment der den Wert in der Variablen ändert x . Dies entspricht nicht dem zweiten-Befehl,
der Increment den Wert in einer geachtelten Kopie von ändert x . Folglich lautet die Ausgabe des Programms
wie folgt:

0
1
1

Weitere Informationen zu Boxing und Unboxing finden Sie unter Boxing und Unboxing.
Bedeutung dieses
Innerhalb eines Instanzkonstruktors oder Instanzfunktionsmembers einer Klasse this wird als Wert
klassifiziert. Folglich this kann verwendet werden, um auf die-Instanz zu verweisen, für die der
Funktionsmember aufgerufen wurde. es ist nicht möglich, this in einem Funktionsmember einer Klasse
zuzuweisen.
Innerhalb eines Instanzkonstruktors einer Struktur this entspricht einem out Parameter vom Strukturtyp, und
innerhalb eines Instanzfunktionsmembers einer Struktur this entspricht einem ref Parameter des Struktur
Typs. In beiden Fällen this wird als Variable klassifiziert, und es ist möglich, die gesamte Struktur, für die der
Funktionsmember aufgerufen wurde, zu ändern, indem Sie this oder als- ref oder- out Parameter
übergeben.
Feldinitialisierer
Wie in Standardwertebeschrieben, besteht der Standardwert einer Struktur aus dem Wert, der sich aus dem
Festlegen aller Werttyp Felder auf ihren Standardwert und alle Verweistyp Felder auf ergibt null . Aus diesem
Grund lässt eine Struktur nicht zu, dass Instanzfelddeklarationen Variableninitialisierer einschließen. Diese
Einschränkung gilt nur für Instanzfelder. Statische Felder einer Struktur dürfen Variableninitialisierer
einschließen.
Das Beispiel

struct Point
{
public int x = 1; // Error, initializer not permitted
public int y = 1; // Error, initializer not permitted
}

ist fehlerhaft, da die Instanzen Feld Deklarationen Variableninitialisierer enthalten.


Konstruktoren
Anders als bei einer Klasse ist es nicht zulässig, einen Parameter losen Instanzkonstruktor zu deklarieren.
Stattdessen verfügt jede Struktur implizit über einen Parameter losen Instanzenkonstruktor, der immer den Wert
zurückgibt, der sich aus dem Festlegen aller Werttyp Felder auf ihren Standardwert und alle Verweistyp Felder
auf NULL (Standardkonstruktoren) ergibt. Eine Struktur kann Instanzkonstruktoren mit Parametern deklarieren.
Beispiel:

struct Point
{
int x, y;

public Point(int x, int y) {


this.x = x;
this.y = y;
}
}

Bei der obigen Deklaration werden die Anweisungen

Point p1 = new Point();


Point p2 = new Point(0, 0);

Erstellen Sie einen Point mit x und y Initialisiert mit 0 (null).


Ein Strukturinstanzkonstruktor darf keinen Konstruktorinitialisierer des Formulars einschließen base(...) .
Wenn der Strukturinstanzkonstruktor keinen Konstruktorinitialisierer angibt, this entspricht die Variable einem
out Parameter des Strukturtyps. ähnlich wie bei einem out Parameter this muss an jedem Speicherort, an
dem der Konstruktor zurückgegeben wird, definitiv zugewiesen werden (definitive Zuweisung). Wenn der
Strukturinstanzkonstruktor einen Konstruktorinitialisierer angibt, this entspricht die Variable einem ref
Parameter des Strukturtyps und ähnelt einem ref Parameter this als definitiv beim Eintrag dem
Konstruktortext zugewiesen. Beachten Sie die folgende instanzkonstruktorimplementierung:
struct Point
{
int x, y;

public int X {
set { x = value; }
}

public int Y {
set { y = value; }
}

public Point(int x, int y) {


X = x; // error, this is not yet definitely assigned
Y = y; // error, this is not yet definitely assigned
}
}

Es kann keine Instanzmember-Funktion (einschließlich der Set-Accessoren für die Eigenschaften X und Y )
aufgerufen werden, bis alle Felder der Struktur, die erstellt wird, definitiv zugewiesen wurden. Die einzige
Ausnahme betrifft automatisch implementierte Eigenschaften (automatisch implementierte Eigenschaften). Die
konkreten Zuweisungs Regeln (einfache Zuweisungs Ausdrücke) geben explizit die Zuweisung zu einer Auto-
Eigenschaft eines Struktur Typs innerhalb eines Instanzkonstruktors dieses Struktur Typs aus: eine solche
Zuweisung wird als eindeutige Zuweisung des verborgenen dahinter liegenden Felds der Auto-Eigenschaft
betrachtet. Daher ist Folgendes zulässig:

struct Point
{
public int X { get; set; }
public int Y { get; set; }

public Point(int x, int y) {


X = x; // allowed, definitely assigns backing field
Y = y; // allowed, definitely assigns backing field
}

Destruktoren
Es ist nicht zulässig, einen Dekonstruktor zu deklarieren.
Statische Konstruktoren
Statische Konstruktoren für Strukturen folgen den meisten der gleichen Regeln wie für-Klassen. Die Ausführung
eines statischen Konstruktors für einen Strukturtyp wird durch das erste der folgenden Ereignisse ausgelöst, die
innerhalb einer Anwendungsdomäne auftreten:
Es wird auf einen statischen Member des Struktur Typs verwiesen.
Ein explizit deklarierter Konstruktor des Struktur Typs wird aufgerufen.
Das Erstellen von Standardwerten (Standardwerte) von Strukturtypen löst nicht den statischen Konstruktor aus.
(Ein Beispiel hierfür ist der Anfangswert von Elementen in einem Array.)

Struct examples (Strukturbeispiele)


Im folgenden werden zwei wichtige Beispiele für die Verwendung von struct Typen zum Erstellen von Typen
veranschaulicht, die ähnlich wie die vordefinierten Typen der Sprache verwendet werden können, jedoch mit
einer geänderten Semantik.
Daten Bank Integertyp
Die DBIntfolgende Struktur implementiert einen ganzzahligen Typ, der den gesamten Satz von Werten des
int Typs darstellen kann, zuzüglich eines zusätzlichen Zustands, der einen unbekannten Wert angibt. Ein Typ
mit diesen Merkmalen wird häufig in Datenbanken verwendet.

using System;

public struct DBInt


{
// The Null member represents an unknown DBInt value.

public static readonly DBInt Null = new DBInt();

// When the defined field is true, this DBInt represents a known value
// which is stored in the value field. When the defined field is false,
// this DBInt represents an unknown value, and the value field is 0.

int value;
bool defined;

// Private instance constructor. Creates a DBInt with a known value.

DBInt(int value) {
this.value = value;
this.defined = true;
}

// The IsNull property is true if this DBInt represents an unknown value.

public bool IsNull { get { return !defined; } }

// The Value property is the known value of this DBInt, or 0 if this


// DBInt represents an unknown value.

public int Value { get { return value; } }

// Implicit conversion from int to DBInt.

public static implicit operator DBInt(int x) {


return new DBInt(x);
}

// Explicit conversion from DBInt to int. Throws an exception if the


// given DBInt represents an unknown value.

public static explicit operator int(DBInt x) {


if (!x.defined) throw new InvalidOperationException();
return x.value;
}

public static DBInt operator +(DBInt x) {


return x;
}

public static DBInt operator -(DBInt x) {


return x.defined ? -x.value : Null;
}

public static DBInt operator +(DBInt x, DBInt y) {


return x.defined && y.defined? x.value + y.value: Null;
}

public static DBInt operator -(DBInt x, DBInt y) {


return x.defined && y.defined? x.value - y.value: Null;
}

public static DBInt operator *(DBInt x, DBInt y) {


return x.defined && y.defined? x.value * y.value: Null;
}

public static DBInt operator /(DBInt x, DBInt y) {


return x.defined && y.defined? x.value / y.value: Null;
}

public static DBInt operator %(DBInt x, DBInt y) {


return x.defined && y.defined? x.value % y.value: Null;
}

public static DBBool operator ==(DBInt x, DBInt y) {


return x.defined && y.defined? x.value == y.value: DBBool.Null;
}

public static DBBool operator !=(DBInt x, DBInt y) {


return x.defined && y.defined? x.value != y.value: DBBool.Null;
}

public static DBBool operator >(DBInt x, DBInt y) {


return x.defined && y.defined? x.value > y.value: DBBool.Null;
}

public static DBBool operator <(DBInt x, DBInt y) {


return x.defined && y.defined? x.value < y.value: DBBool.Null;
}

public static DBBool operator >=(DBInt x, DBInt y) {


return x.defined && y.defined? x.value >= y.value: DBBool.Null;
}

public static DBBool operator <=(DBInt x, DBInt y) {


return x.defined && y.defined? x.value <= y.value: DBBool.Null;
}

public override bool Equals(object obj) {


if (!(obj is DBInt)) return false;
DBInt x = (DBInt)obj;
return value == x.value && defined == x.defined;
}

public override int GetHashCode() {


return value;
}

public override string ToString() {


return defined? value.ToString(): "DBInt.Null";
}
}

Boolescher Dateityp
Die DBBool folgende Struktur implementiert einen logischen Typ mit drei Werten. Mögliche Werte dieses Typs
sind DBBool.True , DBBool.False und DBBool.Null , bei denen der Null Member einen unbekannten Wert
angibt. Solche logischen Typen mit drei Werten werden häufig in Datenbanken verwendet.

using System;

public struct DBBool


{
// The three possible DBBool values.

public static readonly DBBool Null = new DBBool(0);


public static readonly DBBool False = new DBBool(-1);
public static readonly DBBool True = new DBBool(1);

// Private field that stores -1, 0, 1 for False, Null, True.


sbyte value;

// Private instance constructor. The value parameter must be -1, 0, or 1.

DBBool(int value) {
this.value = (sbyte)value;
}

// Properties to examine the value of a DBBool. Return true if this


// DBBool has the given value, false otherwise.

public bool IsNull { get { return value == 0; } }

public bool IsFalse { get { return value < 0; } }

public bool IsTrue { get { return value > 0; } }

// Implicit conversion from bool to DBBool. Maps true to DBBool.True and


// false to DBBool.False.

public static implicit operator DBBool(bool x) {


return x? True: False;
}

// Explicit conversion from DBBool to bool. Throws an exception if the


// given DBBool is Null, otherwise returns true or false.

public static explicit operator bool(DBBool x) {


if (x.value == 0) throw new InvalidOperationException();
return x.value > 0;
}

// Equality operator. Returns Null if either operand is Null, otherwise


// returns True or False.

public static DBBool operator ==(DBBool x, DBBool y) {


if (x.value == 0 || y.value == 0) return Null;
return x.value == y.value? True: False;
}

// Inequality operator. Returns Null if either operand is Null, otherwise


// returns True or False.

public static DBBool operator !=(DBBool x, DBBool y) {


if (x.value == 0 || y.value == 0) return Null;
return x.value != y.value? True: False;
}

// Logical negation operator. Returns True if the operand is False, Null


// if the operand is Null, or False if the operand is True.

public static DBBool operator !(DBBool x) {


return new DBBool(-x.value);
}

// Logical AND operator. Returns False if either operand is False,


// otherwise Null if either operand is Null, otherwise True.

public static DBBool operator &(DBBool x, DBBool y) {


return new DBBool(x.value < y.value? x.value: y.value);
}

// Logical OR operator. Returns True if either operand is True, otherwise


// Null if either operand is Null, otherwise False.

public static DBBool operator |(DBBool x, DBBool y) {


return new DBBool(x.value > y.value? x.value: y.value);
}
// Definitely true operator. Returns true if the operand is True, false
// otherwise.

public static bool operator true(DBBool x) {


return x.value > 0;
}

// Definitely false operator. Returns true if the operand is False, false


// otherwise.

public static bool operator false(DBBool x) {


return x.value < 0;
}

public override bool Equals(object obj) {


if (!(obj is DBBool)) return false;
return value == ((DBBool)obj).value;
}

public override int GetHashCode() {


return value;
}

public override string ToString() {


if (value > 0) return "DBBool.True";
if (value < 0) return "DBBool.False";
return "DBBool.Null";
}
}
Arrays
04.11.2021 • 17 minutes to read

Ein Array ist eine Datenstruktur, die eine Reihe von Variablen enthält, auf die über berechnete Indizes zugegriffen
wird. Die im Array enthaltenen Variablen, auch Elemente des Arrays genannt, weisen alle denselben Typ auf.
Dieser Typ wird als Elementtyp des Arrays bezeichnet.
Ein Array verfügt über einen Rang, der die Anzahl der Indizes bestimmt, die mit jedem Array Element verknüpft
sind. Der Rang eines Arrays wird auch als Dimensionen des Arrays bezeichnet. Ein Array mit dem Rang eins wird
als *eindimensionales Array _ bezeichnet. Ein Array mit einem Rang größer als eins wird als _
mehrdimensionales Array * bezeichnet. Spezifische mehrdimensionale Arrays werden oft als zweidimensionale
Arrays, dreidimensionale Arrays usw. bezeichnet.
Jede Dimension eines Arrays hat eine zugeordnete Länge, die eine ganzzahlige Zahl größer oder gleich 0 (null)
ist. Die Dimensions Längen sind nicht Teil des Array Typs, sondern werden stattdessen festgelegt, wenn eine
Instanz des Arraytyps zur Laufzeit erstellt wird. Die Länge einer Dimension bestimmt den gültigen Bereich von
Indizes für diese Dimension: für eine Dimension der Länge N können Indizes von 0 bis N - 1 einschließlich
liegen. Die Gesamtanzahl der Elemente in einem Array ist das Produkt der Längen der einzelnen Dimensionen
im Array. Wenn mindestens eine Dimension eines Arrays eine Länge von 0 (null) aufweist, wird das Array als
leer bezeichnet.
Ein Array kann einen beliebigen Elementtyp verwenden, einschließlich eines Arraytyps.

Arraytypen
Ein Arraytyp wird als non_array_type geschrieben, auf den mindestens ein rank_specifier s folgt:

array_type
: non_array_type rank_specifier+
;

non_array_type
: type
;

rank_specifier
: '[' dim_separator* ']'
;

dim_separator
: ','
;

Ein non_array_type ist ein beliebiger Typ , der nicht selbst ein array_type ist.
Der Rang eines Arraytyps wird von der am weitesten links stehenden rank_specifier in der array_type
angegeben: ein rank_specifier gibt an, dass das Array ein Array mit einem Rang von 1 plus der Anzahl der " ,
"-Token in der rank_specifier ist.
Der Elementtyp eines Array Typs ist der Typ, der sich aus dem Löschen der am weitesten links stehenden
rank_specifier ergibt:
Ein Arraytyp der Form T[R] ist ein Array mit Rang R und einem nicht-Array-Elementtyp T .
Ein Arraytyp der Form T[R][R1]...[Rn] ist ein Array mit Rang R und einem Elementtyp T[R1]...[Rn] .
Tatsächlich werden die rank_specifier s von links nach rechts vor dem endgültigen nicht-Array Elementtyp
gelesen. Der-Typ int[][,,][,] ist ein eindimensionales Array von dreidimensionalen Arrays von
zweidimensionalen Arrays von int .
Zur Laufzeit kann ein Wert eines Arraytyps null oder ein Verweis auf eine Instanz dieses Array Typs sein.
Der Typ "System. Array"
Der Typ System.Array ist der abstrakte Basistyp aller Array Typen. Eine implizite Verweis Konvertierung
(implizite Verweis Konvertierungen) ist von einem beliebigen Arraytyp in vorhanden System.Array , und eine
explizite Verweis Konvertierung (explizite Verweis Konvertierungen) ist von System.Array in beliebige
Arraytypen vorhanden. Beachten Sie, dass System.Array nicht selbst ein array_type ist. Vielmehr handelt es sich
um eine class_type , von der alle array_type s abgeleitet werden.
Zur Laufzeit kann ein Wert vom Typ System.Array null oder ein Verweis auf eine Instanz eines beliebigen
Array Typs sein.
Arrays und die generische IList-Schnittstelle
Ein eindimensionales Array T[] implementiert die-Schnittstelle System.Collections.Generic.IList<T> (
IList<T> kurz) und die zugehörigen Basis Schnittstellen. Dementsprechend gibt es eine implizite Konvertierung
von T[] in IList<T> und deren Basis Schnittstellen. Wenn eine implizite Verweis Konvertierung von in
vorhanden ist, wird S außerdem T S[] implementiert, IList<T> und es gibt eine implizite Verweis
Konvertierung von S[] in IList<T> und deren Basis Schnittstellen (implizite Verweis Konvertierungen). Wenn
eine explizite Verweis Konvertierung von in vorhanden ist S , T gibt es eine explizite Verweis Konvertierung
von S[] in IList<T> und deren Basis Schnittstellen (explizite Verweis Konvertierungen). Beispiel:

using System.Collections.Generic;

class Test
{
static void Main() {
string[] sa = new string[5];
object[] oa1 = new object[5];
object[] oa2 = sa;

IList<string> lst1 = sa; // Ok


IList<string> lst2 = oa1; // Error, cast needed
IList<object> lst3 = sa; // Ok
IList<object> lst4 = oa1; // Ok

IList<string> lst5 = (IList<string>)oa1; // Exception


IList<string> lst6 = (IList<string>)oa2; // Ok
}
}

Die Zuweisung lst2 = oa1 generiert einen Kompilierzeitfehler, da die Konvertierung von object[] in
IList<string> eine explizite Konvertierung ist, nicht implizit. Die Umwandlung (IList<string>)oa1 bewirkt,
dass zur Laufzeit eine Ausnahme ausgelöst wird, da auf oa1 einen object[] und nicht auf ein verweist
string[] . Die Umwandlung (IList<string>)oa2 bewirkt jedoch nicht, dass eine Ausnahme ausgelöst wird, da
auf oa2 eine verwiesen wird string[] .
Wenn eine implizite oder explizite Verweis Konvertierung von in vorhanden S[] ist IList<T> , gibt es auch
eine explizite Verweis Konvertierung von IList<T> und deren Basis Schnittstellen in S[] (explizite Verweis
Konvertierungen).
Wenn ein Arraytyp S[] implementiert IList<T> , können einige der Member der implementierten
Schnittstelle Ausnahmen auslösen. Das genaue Verhalten der Implementierung der-Schnittstelle geht über den
Rahmen dieser Spezifikation hinaus.
Arrayerstellung
Array Instanzen werden durch array_creation_expression s (Array Erstellungs Ausdrücke) oder durch Feld-oder
lokale Variablen Deklarationen erstellt, die eine array_initializer (Arrayinitialisierer) enthalten.
Wenn eine Array Instanz erstellt wird, werden der Rang und die Länge jeder Dimension festgelegt und dann für
die gesamte Lebensdauer der Instanz konstant bleiben. Anders ausgedrückt: Es ist nicht möglich, den Rang einer
vorhandenen Array Instanz zu ändern, und es ist nicht möglich, die Größe der Dimensionen zu ändern.
Eine Array Instanz ist immer ein Arraytyp. Der System.Array Typ ist ein abstrakter Typ, der nicht instanziiert
werden kann.
Elemente von Arrays, die von array_creation_expression s erstellt werden, werden immer auf ihren
Standardwert (Standardwerte) initialisiert.

Array element access (Zugriff auf Arrayelemente)


Der Zugriff auf Array Elemente erfolgt mithilfe von element_access Ausdrücken (Array Zugriff) im Formular
A[I1, I2, ..., In] , wobei A ein Ausdruck eines Arraytyps ist und jeder Ix Ausdruck vom Typ int , uint ,
long , ulong oder implizit in einen oder mehrere dieser Typen konvertiert werden kann. Das Ergebnis eines
Arrays Element Zugriffs ist eine Variable, nämlich das Array Element, das von den Indizes ausgewählt wird.
Die Elemente eines Arrays können mithilfe einer- foreach Anweisung (der foreach-Anweisung) aufgelistet
werden.

Array members (Arraymember)


Jeder Arraytyp erbt die vom Typ deklarierten Member System.Array .

Array covariance (Array-Kovarianz)


Bei zwei reference_type s A und B , wenn eine implizite Verweis Konvertierung (implizite Verweis
Konvertierungen) oder eine explizite Verweis Konvertierung (explizite Verweis Konvertierungen) von A in
vorhanden ist, B wird dieselbe Verweis Konvertierung auch vom Arraytyp A[R] in den Arraytyp durch B[R]
gegeben, wobei R eine beliebige rank_specifier ist ( jedoch für beide Array Typen gleich). Diese Beziehung wird
als Array Kovarianz bezeichnet. Array-Kovarianz bedeutet insbesondere, dass ein Wert eines Arraytyps A[R]
tatsächlich ein Verweis auf eine Instanz eines Arraytyps sein kann B[R] , wenn eine implizite Verweis
Konvertierung von in vorhanden ist B A .
Aufgrund von Array Kovarianz enthalten Zuweisungen zu Elementen von Verweistyp Arrays eine Lauf Zeit
Überprüfung, mit der sichergestellt wird, dass der Wert, der dem Array Element zugewiesen wird, tatsächlich ein
zulässiger Typ ist (einfache Zuweisung). Beispiel:

class Test
{
static void Fill(object[] array, int index, int count, object value) {
for (int i = index; i < index + count; i++) array[i] = value;
}

static void Main() {


string[] strings = new string[100];
Fill(strings, 0, 100, "Undefined");
Fill(strings, 0, 10, null);
Fill(strings, 90, 10, 0);
}
}
Die Zuweisung zu array[i] in der- Fill Methode beinhaltet implizit eine Lauf Zeit Überprüfung, mit der
sichergestellt wird, dass das Objekt, auf das verweist, value entweder null oder eine-Instanz ist, die mit dem
tatsächlichen Elementtyp von kompatibel ist array . In Main wurden die ersten beiden Aufrufe von Fill
erfolgreich ausgeführt, aber der dritte Aufruf bewirkt, dass eine ausgelöst System.ArrayTypeMismatchException
wird, wenn die erste Zuweisung zu ausgeführt wird array[i] . Die Ausnahme tritt auf, weil ein geschachtelt
int nicht in einem-Array gespeichert werden kann string .

Array-Kovarianz wird nicht auf Arrays von value_type s erweitert. Beispielsweise ist keine Konvertierung
vorhanden, die zulässt, dass eine int[] als behandelt werden kann object[] .

Array initializers (Arrayinitialisierer)


Arrayinitialisierer können in Feld Deklarationen (Feldern), lokalen Variablen Deklarationen (lokale Variablen
Deklarationen) und Array Erstellungs Ausdrücken (Array Erstellungs Ausdrücke) angegeben werden:

array_initializer
: '{' variable_initializer_list? '}'
| '{' variable_initializer_list ',' '}'
;

variable_initializer_list
: variable_initializer (',' variable_initializer)*
;

variable_initializer
: expression
| array_initializer
;

Ein Arrayinitialisierer besteht aus einer Sequenz von Variableninitialisierern, die durch die Token " { " und " }
" eingeschlossen und durch ""-Token getrennt sind , . Jeder Variableninitialisierer ist ein Ausdruck oder, wenn
es sich um ein mehrdimensionales Array handelt, ein Initialisierer für einen arsted Array.
Der Kontext, in dem ein Arrayinitialisierer verwendet wird, bestimmt den Typ des zu initialisierenden Arrays. In
einem Array Erstellungs Ausdruck wird der Arraytyp unmittelbar vor dem Initialisierer, oder er wird von den
Ausdrücken im Arrayinitialisierer abgeleitet. In einer Feld-oder Variablen Deklaration ist der Arraytyp der Typ
des Felds oder der Variablen, der deklariert wird. Wenn ein Arrayinitialisierer in einer Feld-oder Variablen
Deklaration verwendet wird, z. b.:

int[] a = {0, 2, 4, 6, 8};

Es ist einfach eine Kurzform für einen äquivalenten Array Erstellungs Ausdruck:

int[] a = new int[] {0, 2, 4, 6, 8};

Bei einem eindimensionalen Array muss der Arrayinitialisierer aus einer Sequenz von Ausdrücken bestehen,
deren Zuweisung mit dem Elementtyp des Arrays kompatibel ist. Die Ausdrücke initialisieren Array Elemente in
aufsteigender Reihenfolge, beginnend mit dem Element bei Index 0 (null). Die Anzahl der Ausdrücke im
Arrayinitialisierer bestimmt die Länge der erstellten Array Instanz. Beispielsweise erstellt der oben genannte
Arrayinitialisierer eine int[] Instanz der Länge 5 und initialisiert dann die Instanz mit den folgenden Werten:

a[0] = 0; a[1] = 2; a[2] = 4; a[3] = 6; a[4] = 8;

Bei einem mehrdimensionalen Array muss der Arrayinitialisierer so viele Schachtelungs Ebenen aufweisen, wie
Dimensionen im Array vorhanden sind. Die äußerste Schachtelungs Ebene entspricht der äußersten linken
Dimension, und die innerste Schachtelungs Ebene entspricht der äußersten rechten Dimension. Die Länge der
einzelnen Dimensionen des Arrays hängt von der Anzahl der Elemente auf der entsprechenden Schachtelungs
Ebene im Arrayinitialisierer ab. Die Anzahl der Elemente muss für jeden Initialisierer für ein
Initialisierungsprogramm identisch mit dem anderen Arrayinitialisierer auf derselben Ebene sein. Beispiel:

int[,] b = {{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}};

erstellt ein zweidimensionales Array mit einer Länge von fünf für die ganz links-Dimension und eine Länge von
zwei für die ganz rechts-Dimension:

int[,] b = new int[5, 2];

und initialisiert dann die Array Instanz mit den folgenden Werten:

b[0, 0] = 0; b[0, 1] = 1;
b[1, 0] = 2; b[1, 1] = 3;
b[2, 0] = 4; b[2, 1] = 5;
b[3, 0] = 6; b[3, 1] = 7;
b[4, 0] = 8; b[4, 1] = 9;

Wenn eine andere Dimension als die äußteste Länge mit der Länge 0 (null) angegeben wird, wird davon
ausgegangen, dass die nachfolgenden Dimensionen ebenfalls die Länge 0 (null Beispiel:

int[,] c = {};

erstellt ein zweidimensionales Array mit einer Länge von 0 (null) für die linke und die Rechte äußteste
Dimension:

int[,] c = new int[0, 0];

Wenn ein Array Erstellungs Ausdruck explizite Dimensions Längen und einen Arrayinitialisierer enthält, müssen
die Längen Konstante Ausdrücke sein, und die Anzahl der Elemente auf jeder Schachtelungs Ebene muss mit der
entsprechenden Dimensions Länge übereinstimmen. Im Folgenden finden Sie einige Beispiele:

int i = 3;
int[] x = new int[3] {0, 1, 2}; // OK
int[] y = new int[i] {0, 1, 2}; // Error, i not a constant
int[] z = new int[3] {0, 1, 2, 3}; // Error, length/initializer mismatch

Hier führt der Initialisierer für zu y einem Kompilierzeitfehler, da der Dimensions Längen Ausdruck keine
Konstante ist, und der Initialisierer für z führt zu einem Kompilierzeitfehler, da die Länge und die Anzahl der
Elemente im Initialisierer nicht übereinstimmen.
Schnittstellen
04.11.2021 • 52 minutes to read

Eine Schnittstelle definiert einen Vertrag. Eine Klasse oder Struktur, die eine Schnittstelle implementiert, muss
ihren Vertrag einhalten. Eine Schnittstelle kann von mehreren Basis Schnittstellen erben, und eine Klasse oder
Struktur kann mehrere Schnittstellen implementieren.
Schnittstellen können Methoden, Eigenschaften, Ereignisse und Indexer enthalten. Die Schnittstelle selbst stellt
keine Implementierungen für die Member bereit, die Sie definiert. Die-Schnittstelle gibt lediglich die Member an,
die von Klassen oder Strukturen bereitgestellt werden müssen, die die-Schnittstelle implementieren.

Interface declarations (Schnittstellendeklarationen)


Ein interface_declaration ist ein type_declaration (Typdeklarationen), der einen neuen Schnittstellentyp
deklariert.

interface_declaration
: attributes? interface_modifier* 'partial'? 'interface'
identifier variant_type_parameter_list? interface_base?
type_parameter_constraints_clause* interface_body ';'?
;

Ein interface_declaration besteht aus einem optionalen Satz von Attributen (Attributen), gefolgt von einer
optionalen Gruppe von interface_modifier s (Schnittstellen Modifizierern), gefolgt von einem partial
optionalen-Modifizierer, gefolgt vom-Schlüsselwort interface und einem Bezeichner , der die-Schnittstelle
benennt, gefolgt von einer optionalen variant_type_parameter_list -Spezifikation (Variant-Typparameter Listen),
gefolgt von einer optionalen interface_base Spezifikation (Basis Schnittstellen), gefolgt von einer optionalen
type_parameter_constraints_clause s-Spezifikation (Typparameter Einschränkungen), gefolgt von einer
interface_body (Schnittstellen Text), optional gefolgt von einem semi
Schnittstellenmodifizierer
Eine interface_declaration kann optional eine Sequenz von Schnittstellen Modifizierer einschließen:

interface_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| interface_modifier_unsafe
;

Es ist ein Kompilierzeitfehler, damit derselbe Modifizierer mehrmals in einer Schnittstellen Deklaration angezeigt
wird.
Der- new Modifizierer ist nur für Schnittstellen zulässig, die innerhalb einer Klasse definiert sind. Er gibt an, dass
die Schnittstelle einen geerbten Member mit demselben Namen verbirgt, wie im neuen
Modifiziererbeschrieben.
Die public protected internal private Modifizierer,, und Steuern den Zugriff auf die-Schnittstelle. Abhängig
vom Kontext, in dem die Schnittstellen Deklaration auftritt, dürfen nur einige dieser Modifizierer zulässig sein
(alsBarrierefreiheit deklariert).
Partieller Modifizierer
Der- partial Modifizierer gibt an, dass dieser interface_declaration eine partielle Typdeklaration ist. Mehrere
partielle Schnittstellen Deklarationen mit demselben Namen innerhalb eines einschließenden Namespace oder
einer Typdeklaration kombinieren eine Schnittstellen Deklaration, die den in partiellen Typenangegebenen
Regeln folgt.
Parameterlisten für Variant-Typen
Variant-Typparameter Listen können nur für Schnittstellen-und Delegattypen auftreten. Der Unterschied
zwischen normaler type_parameter_list s ist der optionale variance_annotation für jeden Typparameter.

variant_type_parameter_list
: '<' variant_type_parameters '>'
;

variant_type_parameters
: attributes? variance_annotation? type_parameter
| variant_type_parameters ',' attributes? variance_annotation? type_parameter
;

variance_annotation
: 'in'
| 'out'
;

Wenn die Varianz Anmerkung ist out , wird der Typparameter als *kovariant _ bezeichnet. Wenn die Varianz
Anmerkung ist in , wird der Typparameter als kontra Variant bezeichnet. Wenn keine Varianz Anmerkung
vorhanden ist, wird der Typparameter als _ invariante * bezeichnet.
Im Beispiel

interface C<out X, in Y, Z>


{
X M(Y y);
Z P { get; set; }
}

X ist kovariant, Y ist kontra Variant und Z ist unveränderlich.


Varianz Sicherheit
Das Vorkommen von Varianz Anmerkungen in der Typparameter Liste eines Typs schränkt die stellen ein, an
denen Typen innerhalb der Typdeklaration vorkommen können.
Ein Typ T ist " Output-unsicher ", wenn einer der folgenden Punkte enthält:
T ist ein kontra varianter Typparameter.
T ist ein Arraytyp mit einem Ausgabe unsicheren Elementtyp.
T ist eine Schnittstelle oder ein Delegattyp, der S<A1,...,Ak> aus einem generischen Typ erstellt wird,
S<X1,...,Xk> wobei mindestens einer der Ai folgenden Punkte enthält:
Xi ist kovariant oder invariante und Ai ist Ausgabe unsicher.
Xi ist kontra Variant oder invariante und Ai ist Eingabe sicher.

Ein Typ T ist Eingabe unsicher , wenn eine der folgenden Punkte Folgendes enthält:
T ist ein kovariant-Typparameter.
T ist ein Arraytyp mit einem Eingabe unsicheren Elementtyp.
T ist eine Schnittstelle oder ein Delegattyp, der S<A1,...,Ak> aus einem generischen Typ erstellt wird,
S<X1,...,Xk> wobei mindestens einer der Ai folgenden Punkte enthält:
Xi ist kovariant oder invariante und Ai ist Eingabe unsicher.
Xi ist kontra Variant oder invariante und Ai ist Ausgabe unsicher.

Intuitiv ist ein durch die Ausgabe unsicherer Typ in einer Ausgabe Position unzulässig, und ein Eingabe
unsicherer Typ ist an einer Eingabe Position unzulässig.
Ein Typ ist *Output-Safe _, wenn er nicht für die Ausgabe unsicher ist, und _ *Input-Safe**, wenn er nicht
Eingabe unsicher ist.
Varianz Konvertierung
Der Zweck von Varianz Anmerkungen besteht darin, für die Schnittstellen-und Delegattypen eine genauere
(aber dennoch typsichere) Konvertierung bereitzustellen. Zu diesem Zweck verwenden die Definitionen
impliziter (impliziter Konvertierungen) und expliziter Konvertierungen (explizite Konvertierungen) das Konzept
der Varianz Konvertierungen, das wie folgt definiert wird:
Ein Typ T<A1,...,An> ist Varianz konvertierbar in einen T<B1,...,Bn> Typ T , wenn entweder eine Schnittstelle
oder ein Delegattyp ist, der mit den Varianten Typparametern deklariert T<X1,...,Xn> wurde, und für jeden
Variant-Typparameter, Xi den eine der folgenden Werte enthält:
Xi ist kovariant, und ein impliziter Verweis oder eine Identitäts Konvertierung ist von in vorhanden. Ai``Bi
Xi ist kontra Variant, und ein impliziter Verweis oder eine Identitäts Konvertierung ist von Bi zu Ai
Xi ist invariante, und eine Identitäts Konvertierung ist von Ai zu Bi

Basis Schnittstellen
Eine Schnittstelle kann von NULL oder mehr Schnittstellentypen erben, die als explizite Basis Schnittstellen
der Schnittstelle bezeichnet werden. Wenn eine Schnittstelle über eine oder mehrere explizite Basis
Schnittstellen verfügt, folgt in der Deklaration dieser Schnittstelle der Schnittstellen Bezeichner einen
Doppelpunkt und eine durch Kommas getrennte Liste von Basis Schnittstellentypen.

interface_base
: ':' interface_type_list
;

Für einen konstruierten Schnittstellentyp werden die expliziten Basis Schnittstellen gebildet, indem die expliziten
Basis Schnittstellen Deklarationen in der generischen Typdeklaration übernommen und für jede type_parameter
in der Basis Schnittstellen Deklaration ersetzt werden, die entsprechende type_argument des konstruierten Typs.
Die expliziten Basis Schnittstellen einer Schnittstelle müssen mindestens so zugänglich sein wie die Schnittstelle
selbst (Barrierefreiheits Einschränkungen). Beispielsweise ist es ein Kompilierzeitfehler, eine- private oder-
internal Schnittstelle im interface_base einer- public Schnittstelle anzugeben.

Es ist ein Kompilierzeitfehler für eine Schnittstelle, die direkt oder indirekt von sich selbst erbt.
Die Basis Schnittstellen einer Schnittstelle sind die expliziten Basis Schnittstellen und deren Basis
Schnittstellen. Mit anderen Worten: der Satz von Basis Schnittstellen ist die komplette transitiv Schließung der
expliziten Basis Schnittstellen, ihrer expliziten Basis Schnittstellen usw. Eine Schnittstelle erbt alle Member ihrer
Basis Schnittstellen. Im Beispiel
interface IControl
{
void Paint();
}

interface ITextBox: IControl


{
void SetText(string text);
}

interface IListBox: IControl


{
void SetItems(string[] items);
}

interface IComboBox: ITextBox, IListBox {}

die Basis Schnittstellen von IComboBox sind IControl , ITextBox und IListBox .
Mit anderen Worten, die IComboBox obige Schnittstelle erbt Member SetText und SetItems sowie Paint .
Jede Basisschnittstelle einer Schnittstelle muss Ausgabe sicher sein (Varianz Sicherheit). Eine Klasse oder
Struktur, die eine Schnittstelle implementiert, implementiert auch implizit alle Basis Schnittstellen der
Schnittstelle.
Schnittstellen Text
Der interface_body einer Schnittstelle definiert die Member der Schnittstelle.

interface_body
: '{' interface_member_declaration* '}'
;

Interface members (Schnittstellenmember)


Die Member einer Schnittstelle sind die Member, die von den Basis Schnittstellen und den Membern geerbt
werden, die von der Schnittstelle selbst deklariert werden.

interface_member_declaration
: interface_method_declaration
| interface_property_declaration
| interface_event_declaration
| interface_indexer_declaration
;

Eine Schnittstellen Deklaration kann NULL oder mehr Member deklarieren. Die Member einer Schnittstelle
müssen Methoden, Eigenschaften, Ereignisse oder Indexer sein. Eine Schnittstelle kann keine Konstanten, Felder,
Operatoren, Instanzkonstruktoren, Dekonstruktoren oder Typen enthalten, und eine Schnittstelle kann keine
statischen Member beliebiger Art enthalten.
Alle Schnittstellenmember haben implizit öffentlichen Zugriff. Es handelt sich um einen Kompilierzeitfehler für
Schnittstellenmember-Deklarationen, um beliebige Modifizierer einzuschließen Insbesondere können
Schnittstellenmember nicht mit den modifizierermembern abstract ,,,,,, oder deklariert werden public
protected internal private virtual override static .

Das Beispiel
public delegate void StringListEvent(IStringList sender);

public interface IStringList


{
void Add(string s);
int Count { get; }
event StringListEvent Changed;
string this[int index] { get; set; }
}

deklariert eine Schnittstelle, die eine der möglichen Arten von Membern enthält: eine Methode, eine Eigenschaft,
ein Ereignis und einen Indexer.
Ein interface_declaration erstellt einen neuen Deklarations Raum (Deklarationen), und die
interface_member_declaration s, die sofort im interface_declaration enthalten sind, stellen neue Member in
diesen Deklarations Bereich ein. Die folgenden Regeln gelten für interface_member_declaration s:
Der Name einer Methode muss sich von den Namen aller Eigenschaften und Ereignisse unterscheiden, die in
derselben Schnittstelle deklariert werden. Außerdem müssen sich die Signatur (Signaturen und überladen)
einer Methode von den Signaturen aller anderen Methoden unterscheiden, die in derselben Schnittstelle
deklariert sind, und zwei Methoden, die in der gleichen Schnittstelle deklariert werden, verfügen
möglicherweise nicht über Signaturen, die sich ausschließlich durch und unterscheiden ref out .
Der Name einer Eigenschaft oder eines Ereignisses muss sich von den Namen aller anderen Member
unterscheiden, die in derselben Schnittstelle deklariert werden.
Die Signatur eines Indexers muss sich von den Signaturen aller anderen in derselben Schnittstelle
deklarierten Indexer unterscheiden.
Die geerbten Member einer Schnittstelle sind nicht Teil des Deklarations Raums der Schnittstelle. Daher kann
eine Schnittstelle einen Member mit demselben Namen oder derselben Signatur wie ein geerbte Member
deklarieren. Wenn dies auftritt, wird der Member der abgeleiteten Schnittstelle zum Ausblenden des
basisschnittstellenmembers bezeichnet. Das Ausblenden eines geerbten Members wird nicht als Fehler
betrachtet, bewirkt jedoch, dass der Compiler eine Warnung ausgibt. Um die Warnung zu unterdrücken, muss
die Deklaration des abgeleiteten Schnittstellenmembers einen new Modifizierer enthalten, um anzugeben, dass
der abgeleitete Member den Basismember ausblenden soll. Dieses Thema wird unter Ausblenden durch
Vererbungausführlicher erläutert.
Wenn ein new Modifizierer in einer Deklaration enthalten ist, die einen geerbten Member nicht ausgeblendet
hat, wird eine Warnung für diesen Effekt ausgegeben. Diese Warnung wird durch Entfernen des- new
Modifizierers unterdrückt.
Beachten Sie, dass die Member in der Klasse object nicht, streng genommen Member einer beliebigen
Schnittstelle (Schnittstellenmember) sind. Allerdings sind die Member in der Klasse object über die Element
Suche in einem beliebigen Schnittstellentyp (Member-Suche) verfügbar.
Schnittstellen Methoden
Schnittstellen Methoden werden mithilfe von interface_method_declaration s deklariert:

interface_method_declaration
: attributes? 'new'? return_type identifier type_parameter_list
'(' formal_parameter_list? ')' type_parameter_constraints_clause* ';'
;

Die Attribute, return_type, Bezeichner und formal_parameter_list einer Schnittstellen Methoden Deklaration
haben dieselbe Bedeutung wie die einer Methoden Deklaration in einer Klasse (Methoden). Eine Schnittstellen
Methoden Deklaration darf keinen Methoden Text angeben, und die Deklaration endet daher immer mit einem
Semikolon.
Jeder formale Parametertyp einer Schnittstellen Methode muss Eingabe sicher (Varianz Sicherheit) sein, und der
void Rückgabetyp muss entweder oder Output-sicher sein. Außerdem müssen jede Klassentyp Einschränkung,
schnittstellentypeinschränkung und Typparameter Einschränkung für jeden Typparameter der Methode Eingabe
sicher sein.
Diese Regeln stellen sicher, dass jede kovariante oder Kontra Variante Verwendung der Schnittstelle typsicher
bleibt. Beispiel:

interface I<out T> { void M<U>() where U : T; }

ist unzulässig, da die Verwendung von T als Typparameter Einschränkung für U nicht Eingabe sicher ist.
Wenn diese Einschränkung nicht vorhanden ist, kann die Typsicherheit folgendermaßen verletzt werden:

class B {}
class D : B{}
class E : B {}
class C : I<D> { public void M<U>() {...} }
...
I<B> b = new C();
b.M<E>();

Dabei handelt es sich um einen-Rückruf C.M<E> . Dieser-Rückruf erfordert jedoch, dass E von abgeleitet D ist,
sodass die Typsicherheit hier verletzt wird.
Schnittstelleneigenschaften
Schnittstelleneigenschaften werden mit interface_property_declaration s deklariert:

interface_property_declaration
: attributes? 'new'? type identifier '{' interface_accessors '}'
;

interface_accessors
: attributes? 'get' ';'
| attributes? 'set' ';'
| attributes? 'get' ';' attributes? 'set' ';'
| attributes? 'set' ';' attributes? 'get' ';'
;

Die Attribute, der Typ und der Bezeichner einer Schnittstellen Eigenschafts Deklaration haben dieselbe
Bedeutung wie die einer Eigenschafts Deklaration in einer Klasse (Eigenschaften).
Die Accessoren einer Schnittstelleneigenschaften Deklaration entsprechen den Accessoren einer Klassen
Eigenschafts Deklaration (Accessoren), mit dem Unterschied, dass der Accessortext immer ein Semikolon sein
muss. Daher geben die Accessoren einfach an, ob die Eigenschaft Lese-/Schreibzugriff, schreibgeschützt oder
schreibgeschützt ist.
Der Typ einer Schnittstellen Eigenschaft muss bei einem get-Accessor Ausgabe sicher sein und muss eingegeben
werden können, wenn ein Set-Accessor vorhanden ist.
Schnittstellen Ereignisse
Schnittstellen Ereignisse werden mithilfe von interface_event_declaration s deklariert:
interface_event_declaration
: attributes? 'new'? 'event' type identifier ';'
;

Die Attribute, der Typ und der Bezeichner einer Schnittstellen-Ereignis Deklaration haben dieselbe Bedeutung
wie die einer Ereignis Deklaration in einer Klasse (Ereignisse).
Der Typ eines Schnittstellen Ereignisses muss Eingabe sicher sein.
Schnittstellen Indexer
Schnittstellenindexer werden mithilfe von interface_indexer_declaration s deklariert:

interface_indexer_declaration
: attributes? 'new'? type 'this' '[' formal_parameter_list ']' '{' interface_accessors '}'
;

Die Attribute, der Typ und formal_parameter_list einer Schnittstellenindexer-Deklaration haben dieselbe
Bedeutung wie die einer Indexer-Deklaration in einer Klasse (Indexer).
Die Accessoren einer Schnittstellenindexer-Deklaration entsprechen den Accessoren einer Klassenindexer-
Deklaration (Indexer), mit dem Unterschied, dass der Accessor-Text immer ein Semikolon sein muss. Daher
geben die Accessoren einfach an, ob der Indexer Lese-/Schreibzugriff, schreibgeschützt oder schreibgeschützt
ist.
Alle formalen Parametertypen eines schnittstellenindexers müssen Eingabe sicher sein. Außerdem out müssen
alle oder ref formalen Parametertypen ebenfalls Ausgabe sicher sein. Beachten Sie, dass auch out Parameter
aufgrund einer Einschränkung der zugrunde liegenden Ausführungsplattform Eingabe sicher sein müssen.
Der Typ eines schnittstellenindexers muss bei einem get-Accessor Ausgabe sicher sein und muss eingegeben
werden können, wenn ein Set-Accessor vorhanden ist.
Zugriff auf Schnittstellenmember
Der Zugriff auf Schnittstellenmember erfolgt über Member Access (Member Access) und Indexer Access
(Indexer Access)-Ausdrücke der Form I.M und I[A] , wobei I ein Schnittstellentyp ist, M eine Methode, eine
Eigenschaft oder ein Ereignis dieses Schnittstellen Typs ist und A eine Indexer-Argumentliste ist.
Bei Schnittstellen, die nur eine einzelne Vererbung haben ( jede Schnittstelle in der Vererbungs Kette weist genau
null oder eine direkte Basisschnittstelle auf), die Auswirkungen der Regeln für die Member-Suche
(ElementSuche), Methodenaufrufe (Methodenaufrufe) und Indexer-Zugriff (Indexer-Zugriff) sind identisch mit
den Regeln für Klassen und Strukturen: mehr abgeleitete Member Blenden weniger abgeleitete Member mit
demselben Namen oder derselben Signatur aus. Allerdings können Mehrdeutigkeiten bei Schnittstellen mit
mehreren Vererbungen auftreten, wenn zwei oder mehr nicht verknüpfte Basis Schnittstellen Member mit
demselben Namen oder derselben Signatur deklarieren. In diesem Abschnitt werden einige Beispiele für
derartige Situationen gezeigt. In allen Fällen können explizite Umwandlungen verwendet werden, um die
Mehrdeutigkeiten aufzulösen.
Im Beispiel
interface IList
{
int Count { get; set; }
}

interface ICounter
{
void Count(int i);
}

interface IListCounter: IList, ICounter {}

class C
{
void Test(IListCounter x) {
x.Count(1); // Error
x.Count = 1; // Error
((IList)x).Count = 1; // Ok, invokes IList.Count.set
((ICounter)x).Count(1); // Ok, invokes ICounter.Count
}
}

die ersten beiden-Anweisungen verursachen Kompilierzeitfehler, da die Element Suche (Member Suche) von
Count in IListCounter mehrdeutig ist. Wie im Beispiel gezeigt, wird die Mehrdeutigkeit durch umwandeln x
in den entsprechenden Basis Schnittstellentyp aufgelöst. Solche Umwandlungen haben keine Lauf Zeit Kosten –
Sie bestehen lediglich aus der Anzeige der Instanz als weniger abgeleiteter Typ zur Kompilierzeit.
Im Beispiel

interface IInteger
{
void Add(int i);
}

interface IDouble
{
void Add(double d);
}

interface INumber: IInteger, IDouble {}

class C
{
void Test(INumber n) {
n.Add(1); // Invokes IInteger.Add
n.Add(1.0); // Only IDouble.Add is applicable
((IInteger)n).Add(1); // Only IInteger.Add is a candidate
((IDouble)n).Add(1); // Only IDouble.Add is a candidate
}
}

der Aufruf n.Add(1) IInteger.Add wird durch Anwenden der Regeln zur Überladungs Auflösung der
Überladungs Auflösungausgewählt. Entsprechend wählt der Aufruf n.Add(1.0) aus IDouble.Add . Wenn
explizite Umwandlungen eingefügt werden, gibt es nur eine Kandidaten Methode und somit keine
Mehrdeutigkeit.
Im Beispiel
interface IBase
{
void F(int i);
}

interface ILeft: IBase


{
new void F(int i);
}

interface IRight: IBase


{
void G();
}

interface IDerived: ILeft, IRight {}

class A
{
void Test(IDerived d) {
d.F(1); // Invokes ILeft.F
((IBase)d).F(1); // Invokes IBase.F
((ILeft)d).F(1); // Invokes ILeft.F
((IRight)d).F(1); // Invokes IBase.F
}
}

der IBase.F Member wird vom Member ausgeblendet ILeft.F . Der Aufruf d.F(1) wählt daher ILeft.F aus,
obwohl IBase.F anscheinend nicht im Zugriffs Pfad verborgen ist, der durchführt IRight .
Die intuitive Regel für das Ausblenden von Schnittstellen mit mehreren Vererbungen ist einfach: Wenn ein
Member in einem beliebigen Zugriffs Pfad ausgeblendet ist, wird er in allen Zugriffs Pfaden ausgeblendet. Da
der Zugriffs Pfad von IDerived zu ILeft IBase IBase.F ausgeblendet wird, wird der-Member auch im
Zugriffs Pfad von IDerived bis IRight zu ausgeblendet IBase .

Fully qualified interface member names (Vollqualifizierte


Schnittstellenmembernamen)
Auf einen Schnittstellenmember wird manchmal durch seinen voll qualifizier ten Namen verwiesen. Der voll
qualifizierte Name eines Schnittstellenmembers besteht aus dem Namen der Schnittstelle, in der der Member
deklariert ist, gefolgt von einem Punkt, gefolgt vom Namen des Members. Der voll qualifizierte Name eines
Members verweist auf die Schnittstelle, in der der Member deklariert wird. Beispielsweise mit den Deklarationen

interface IControl
{
void Paint();
}

interface ITextBox: IControl


{
void SetText(string text);
}

der voll qualifizierte Name von Paint ist, IControl.Paint und der voll qualifizierte Name von SetText ist
ITextBox.SetText .

Im obigen Beispiel kann nicht auf als verwiesen werden Paint ITextBox.Paint .
Wenn eine Schnittstelle Teil eines Namespace ist, enthält der voll qualifizierte Name eines
Schnittstellenmembers den Namespace Namen. Beispiel:
namespace System
{
public interface ICloneable
{
object Clone();
}
}

Hier lautet der voll qualifizierte Name der Clone Methode System.ICloneable.Clone .

Schnittstellenimplementierungen
Schnittstellen können von Klassen und Strukturen implementiert werden. Um anzugeben, dass eine Klasse oder
Struktur direkt eine Schnittstelle implementiert, ist der Schnittstellen Bezeichner in der Basisklassen Liste der
Klasse oder Struktur enthalten. Beispiel:

interface ICloneable
{
object Clone();
}

interface IComparable
{
int CompareTo(object other);
}

class ListEntry: ICloneable, IComparable


{
public object Clone() {...}
public int CompareTo(object other) {...}
}

Eine Klasse oder Struktur, die eine Schnittstelle direkt implementiert, implementiert auch direkt alle Basis
Schnittstellen der Schnittstelle. Dies gilt auch, wenn die Klasse oder Struktur nicht explizit alle Basis Schnittstellen
in der Basisklassen Liste aufführt. Beispiel:

interface IControl
{
void Paint();
}

interface ITextBox: IControl


{
void SetText(string text);
}

class TextBox: ITextBox


{
public void Paint() {...}
public void SetText(string text) {...}
}

Hier implementiert Class TextBox sowohl IControl als auch ITextBox .


Wenn eine Klasse C direkt eine Schnittstelle implementiert, implementieren alle Klassen, die von C abgeleitet
sind, auch die-Schnittstelle implizit. Die in einer Klassen Deklaration angegebenen Basis Schnittstellen können
als Schnittstellentypen (konstruierte Typen) erstellt werden. Eine Basisschnittstelle kann nicht eigenständig ein
Typparameter sein. Sie kann jedoch die Typparameter enthalten, die sich im Gültigkeitsbereich befinden. Der
folgende Code veranschaulicht, wie eine Klasse konstruierte Typen implementieren und erweitern kann:

class C<U,V> {}

interface I1<V> {}

class D: C<string,int>, I1<string> {}

class E<T>: C<int,T>, I1<T> {}

Die Basis Schnittstellen einer generischen Klassen Deklaration müssen der in der Eindeutigkeit implementierter
SchnittStelle beschriebenen Eindeutigkeits Regel entsprechen.
Explizite Schnittstellenmember-Implementierungen
Zum Implementieren von Schnittstellen kann eine Klasse oder Struktur explizite Implementierungen von
Schnittstellenmembern deklarieren. Eine explizite Implementierung des Schnittstellenmembers ist eine
Methode, eine Eigenschaft, ein Ereignis oder eine Indexerdeklaration, die auf einen voll qualifizierten
schnittstellenelementnamen verweist. Beispiel:

interface IList<T>
{
T[] GetElements();
}

interface IDictionary<K,V>
{
V this[K key];
void Add(K key, V value);
}

class List<T>: IList<T>, IDictionary<int,T>


{
T[] IList<T>.GetElements() {...}
T IDictionary<int,T>.this[int index] {...}
void IDictionary<int,T>.Add(int index, T value) {...}
}

Hier IDictionary<int,T>.this und IDictionary<int,T>.Add sind explizite Schnittstellenmember-


Implementierungen.
In einigen Fällen ist der Name eines Schnittstellenmembers für die implementierende Klasse möglicherweise
nicht geeignet. in diesem Fall kann der Schnittstellenmember mithilfe der expliziten Implementierung des
Schnittstellenmembers implementiert werden. Eine Klasse, die eine Datei Abstraktion implementiert, würde z. b.
wahrscheinlich eine Element Funktion implementieren, die Close die Datei Ressource freigibt, und die-
Dispose Methode der- IDisposable Schnittstelle mithilfe der expliziten Implementierung von Schnittstellen
Membern implementieren:
interface IDisposable
{
void Dispose();
}

class MyFile: IDisposable


{
void IDisposable.Dispose() {
Close();
}

public void Close() {


// Do what's necessary to close the file
System.GC.SuppressFinalize(this);
}
}

Es ist nicht möglich, auf eine explizite Schnittstellenmember-Implementierung über den voll qualifizierten
Namen in einem Methodenaufruf, dem Eigenschafts Zugriff oder dem Indexerzugriff zuzugreifen. Auf eine
explizite Schnittstellenmember-Implementierung kann nur über eine Schnittstellen Instanz zugegriffen werden,
und in diesem Fall wird einfach anhand ihres Element namens darauf verwiesen.
Es handelt sich um einen Kompilierzeitfehler für eine explizite Schnittstellenmember-Implementierung, um
Zugriffsmodifizierer einzuschließen, und es handelt sich um einen Kompilierungsfehler, der die Modifizierer
abstract , virtual , override oder einschließt static .

Explizite Schnittstellenmember-Implementierungen haben unterschiedliche Barrierefreiheits Merkmale als


andere Member. Da explizite Schnittstellenmember-Implementierungen nie über Ihren voll qualifizierten Namen
in einem Methodenaufruf oder einem Eigenschaften Zugriff zugänglich sind, sind Sie in einem Sinn privat. Da
jedoch über eine Schnittstellen Instanz auf Sie zugegriffen werden kann, sind Sie auch öffentlich.
Explizite Schnittstellenmember-Implementierungen dienen zwei Haupt Zwecken:
Da die Implementierungen expliziter Schnittstellen Member nicht über Klassen-oder Struktur Instanzen
zugänglich sind, können Schnittstellen Implementierungen von der öffentlichen Schnittstelle einer Klasse
oder Struktur ausgeschlossen werden. Dies ist besonders nützlich, wenn eine Klasse oder Struktur eine
interne Schnittstelle implementiert, die für einen Consumer dieser Klasse oder Struktur nicht von Interesse
ist.
Explizite Schnittstellenmember-Implementierungen ermöglichen die Eindeutigkeit von
Schnittstellenmembern mit derselben Signatur. Ohne explizite Implementierungen von
Schnittstellenmembern wäre es für eine Klasse oder Struktur unmöglich, unterschiedliche
Implementierungen von Schnittstellenmembern mit derselben Signatur und demselben Rückgabetyp zu
haben. es ist nicht möglich, dass eine Klasse oder Struktur eine Implementierung für alle
Schnittstellenmember mit derselben Signatur, aber mit unterschiedlichen Rückgabe Typen hat.
Damit eine explizite Schnittstellenmember-Implementierung gültig ist, muss die Klasse oder Struktur eine
Schnittstelle in der Basisklassen Liste benennen, die ein Element enthält, dessen voll qualifizierter Name, Typ
und Parametertypen exakt mit denen der expliziten Schnittstellenmember-Implementierung übereinstimmen.
Daher wird in der folgenden Klasse

class Shape: ICloneable


{
object ICloneable.Clone() {...}
int IComparable.CompareTo(object other) {...} // invalid
}

die Deklaration von IComparable.CompareTo führt zu einem Kompilierzeitfehler, da IComparable nicht in der
Basisklassen Liste von aufgeführt ist Shape und keine Basisschnittstelle von ist ICloneable . Ebenso in den
Deklarationen

class Shape: ICloneable


{
object ICloneable.Clone() {...}
}

class Ellipse: Shape


{
object ICloneable.Clone() {...} // invalid
}

die Deklaration von ICloneable.Clone in Ellipse führt zu einem Kompilierzeitfehler, da ICloneable nicht
explizit in der Basisklassen Liste von aufgeführt wird Ellipse .
Der voll qualifizierte Name eines Schnittstellenmembers muss auf die Schnittstelle verweisen, in der der
Member deklariert wurde. Folglich in den Deklarationen

interface IControl
{
void Paint();
}

interface ITextBox: IControl


{
void SetText(string text);
}

class TextBox: ITextBox


{
void IControl.Paint() {...}
void ITextBox.SetText(string text) {...}
}

die explizite Implementierung des Schnittstellenmembers von Paint muss als geschrieben werden
IControl.Paint .

Eindeutigkeit der implementierten Schnittstellen


Die von einer generischen Typdeklaration implementierten Schnittstellen müssen für alle möglichen
konstruierten Typen eindeutig bleiben. Ohne diese Regel wäre es nicht möglich, die richtige Methode zu
ermitteln, die für bestimmte konstruierte Typen aufgerufen werden soll. Nehmen Sie beispielsweise an, dass
eine generische Klassen Deklaration wie folgt geschrieben werden darf:

interface I<T>
{
void F();
}

class X<U,V>: I<U>, I<V> // Error: I<U> and I<V> conflict


{
void I<U>.F() {...}
void I<V>.F() {...}
}

War dies zulässig, konnte nicht ermittelt werden, welcher Code im folgenden Fall ausgeführt werden soll:
I<int> x = new X<int,int>();
x.F();

Um zu ermitteln, ob die Schnittstellen Liste einer generischen Typdeklaration gültig ist, werden die folgenden
Schritte ausgeführt:
Dies ist L die Liste der Schnittstellen, die direkt in einer generischen Klassen-, Struktur-oder Schnittstellen
Deklaration angegeben werden C .
Fügen Sie L allen Basis Schnittstellen der Schnittstellen hinzu, die bereits in vorhanden sind L .
Entfernen Sie alle Duplikate aus L .
Wenn ein möglicher konstruierter Typ, der von erstellt C wird, nach dem Ersetzen von Typargumenten
ersetzt L werden muss, werden zwei Schnittstellen in L identisch sein, und die Deklaration von C ist
ungültig. Einschränkungs Deklarationen werden nicht berücksichtigt, wenn alle möglichen konstruierten
Typen ermittelt werden.
In der obigen Klassen Deklaration X besteht die Schnittstellen Liste L aus I<U> und I<V> . Die Deklaration
ist ungültig, da ein konstruierter Typ mit U und V dem gleichen Typ dazu führen würde, dass diese beiden
Schnittstellen identische Typen darstellen.
Es ist möglich, dass die Schnittstellen, die auf unterschiedlichen Vererbungs Ebenen angegeben werden,

interface I<T>
{
void F();
}

class Base<U>: I<U>


{
void I<U>.F() {...}
}

class Derived<U,V>: Base<U>, I<V> // Ok


{
void I<V>.F() {...}
}

Dieser Code ist gültig, obwohl Derived<U,V> sowohl als auch implementiert I<U> I<V> . Der Code

I<int> x = new Derived<int,int>();


x.F();

Ruft die-Methode in auf Derived , da Derived<int,int> tatsächlich neu implementiert I<int> (Schnittstellen
erneute Implementierung).
Implementierung generischer Methoden
Wenn eine generische Methode implizit eine Schnittstellen Methode implementiert, müssen die
Einschränkungen, die für jeden Methodentypparameter angegeben werden, in beiden Deklarationen äquivalent
sein (nachdem alle Schnittstellen Typparameter durch die entsprechenden Typargumente ersetzt wurden), wobei
Methodentypparameter durch Ordinalpositionen von links nach rechts identifiziert werden.
Wenn eine generische Methode explizit eine Schnittstellen Methode implementiert, sind jedoch keine
Einschränkungen für die implementierende Methode zulässig. Stattdessen werden die Einschränkungen von der
Schnittstellen Methode geerbt.
interface I<A,B,C>
{
void F<T>(T t) where T: A;
void G<T>(T t) where T: B;
void H<T>(T t) where T: C;
}

class C: I<object,C,string>
{
public void F<T>(T t) {...} // Ok
public void G<T>(T t) where T: C {...} // Ok
public void H<T>(T t) where T: string {...} // Error
}

Die-Methode C.F<T> implementiert implizit I<object,C,string>.F<T> . In diesem Fall C.F<T> ist nicht
erforderlich (und nicht zulässig), um die Einschränkung anzugeben, T:object da object eine implizite
Einschränkung für alle Typparameter ist. Die-Methode C.G<T> implementiert implizit I<object,C,string>.G<T> ,
da die Einschränkungen mit denen in der-Schnittstelle übereinstimmen, nachdem die Schnittstellen
Typparameter durch die entsprechenden Typargumente ersetzt wurden. Die Einschränkung für die-Methode
C.H<T> ist ein Fehler, da versiegelte Typen ( string in diesem Fall) nicht als Einschränkungen verwendet
werden können. Das Weglassen der Einschränkung wäre auch ein Fehler, da Einschränkungen von impliziten
Schnittstellen Methoden Implementierungen erforderlich sind, um eine Entsprechung zu finden. Daher ist es
unmöglich, implizit zu implementieren I<object,C,string>.H<T> . Diese Schnittstellen Methode kann nur mit
einer expliziten Schnittstellenmember-Implementierung implementiert werden:

class C: I<object,C,string>
{
...

public void H<U>(U u) where U: class {...}

void I<object,C,string>.H<T>(T t) {
string s = t; // Ok
H<T>(t);
}
}

In diesem Beispiel ruft die explizite Implementierung des Schnittstellenmembers eine öffentliche Methode auf,
die streng schwächere Einschränkungen hat. Beachten Sie, dass die Zuweisung von t zu s gültig ist T , da
eine Einschränkung von erbt T:string , auch wenn diese Einschränkung im Quellcode nicht ausdrucksfähig ist.
Schnittstellen Zuordnung
Eine Klasse oder Struktur muss Implementierungen aller Member der Schnittstellen bereitstellen, die in der
Basisklassen Liste der Klasse oder Struktur aufgeführt sind. Der Prozess der Suche nach Implementierungen von
Schnittstellenmembern in einer implementierenden Klasse oder Struktur wird als Schnittstellen Zuordnung
bezeichnet.
Bei der Schnittstellen Zuordnung für eine Klasse oder Struktur wird C eine Implementierung für jedes Element
jeder Schnittstelle gesucht, die in der Basisklassen Liste von angegeben ist C . Die Implementierung eines
bestimmten Schnittstellenmembers I.M , wobei I die Schnittstelle ist, in der der Member M deklariert wird,
wird durch Untersuchen der einzelnen Klassen oder Strukturen S , beginnend mit C und wiederholen für jede
aufeinander folgende Basisklasse von C , bis eine Entsprechung gefunden wird:
Wenn S eine Deklaration einer expliziten Schnittstellenmember-Implementierung enthält, die mit und
übereinstimmt I M , dann ist dieser Member die Implementierung von I.M .
Wenn andernfalls S eine Deklaration eines nicht statischen öffentlichen Members enthält, der entspricht M
, ist dieser Member die Implementierung von I.M . Wenn mehr als ein Member übereinstimmt, wird nicht
angegeben, welcher Member die Implementierung von ist I.M . Diese Situation kann nur auftreten S ,
wenn ein konstruierter Typ ist, bei dem die beiden Member, die im generischen Typ deklariert sind, über
unterschiedliche Signaturen verfügen, aber die Typargumente die Signaturen identisch machen.
Ein Kompilierzeitfehler tritt auf, wenn-Implementierungen nicht für alle Elemente aller Schnittstellen gefunden
werden können, die in der Basisklassen Liste von angegeben sind C . Beachten Sie, dass die Member einer
Schnittstelle jene Member enthalten, die von Basis Schnittstellen geerbt werden.
Zum Zwecke der Schnittstellen Zuordnung entspricht ein Klassenmember einem A Schnittstellenmember in
folgenden Fällen B :
A und B sind-Methoden, und der Name, der Typ und die formalen Parameterlisten von A und B sind
identisch.
A und B sind Eigenschaften, der Name und der Typ von A und B sind identisch, und A verfügt über
dieselben Accessoren wie B ( A ist berechtigt, zusätzliche Accessoren zu haben, wenn es sich nicht um eine
explizite Schnittstellenmember-Implementierung handelt).
A und B sind-Ereignisse, und der Name und der Typ von A und B sind identisch.
A und B sind Indexer, der Typ und die formalen Parameterlisten von A und B sind identisch, und weisen
A dieselben Accessoren wie B auf ( A ist für zusätzliche Accessoren zulässig, wenn es sich nicht um eine
explizite Schnittstellenmember-Implementierung handelt).
Wichtige Implikationen des Schnittstellen Mapping-Algorithmus:
Explizite Schnittstellenmember-Implementierungen haben Vorrang vor anderen Membern in derselben
Klasse oder Struktur, wenn Sie die Klasse oder den Strukturmember ermitteln, der einen
Schnittstellenmember implementiert.
Keines der nicht öffentlichen und statischen Member ist an der Schnittstellen Zuordnung beteiligt.
Im Beispiel

interface ICloneable
{
object Clone();
}

class C: ICloneable
{
object ICloneable.Clone() {...}
public object Clone() {...}
}

der ICloneable.Clone Member von C wird zur Implementierung von Clone in, ICloneable da explizite
Schnittstellenmember-Implementierungen Vorrang vor anderen Membern haben.
Wenn eine Klasse oder Struktur zwei oder mehr Schnittstellen implementiert, die einen Member mit demselben
Namen, Typ und denselben Parametertypen enthalten, ist es möglich, jedes dieser Schnittstellenmember einer
einzelnen Klasse oder einem Strukturelement zuzuordnen. Beispiel:
interface IControl
{
void Paint();
}

interface IForm
{
void Paint();
}

class Page: IControl, IForm


{
public void Paint() {...}
}

Hier werden die Paint Methoden von IControl und IForm auf der- Paint Methode in zugeordnet Page .
Natürlich ist es auch möglich, dass Sie über separate explizite Schnittstellenmember-Implementierungen für die
beiden Methoden verfügen.
Wenn eine Klasse oder Struktur eine Schnittstelle implementiert, die ausgeblendete Member enthält, müssen
einige Member notwendigerweise durch explizite Implementierungen von Schnittstellenmembern
implementiert werden. Beispiel:

interface IBase
{
int P { get; }
}

interface IDerived: IBase


{
new int P();
}

Eine Implementierung dieser Schnittstelle erfordert mindestens eine explizite Implementierung des
Schnittstellenmembers und würde eine der folgenden Formen annehmen:

class C: IDerived
{
int IBase.P { get {...} }
int IDerived.P() {...}
}

class C: IDerived
{
public int P { get {...} }
int IDerived.P() {...}
}

class C: IDerived
{
int IBase.P { get {...} }
public int P() {...}
}

Wenn eine Klasse mehrere Schnittstellen implementiert, die über dieselbe Basisschnittstelle verfügen, kann nur
eine Implementierung der Basisschnittstelle vorhanden sein. Im Beispiel
interface IControl
{
void Paint();
}

interface ITextBox: IControl


{
void SetText(string text);
}

interface IListBox: IControl


{
void SetItems(string[] items);
}

class ComboBox: IControl, ITextBox, IListBox


{
void IControl.Paint() {...}
void ITextBox.SetText(string text) {...}
void IListBox.SetItems(string[] items) {...}
}

Es ist nicht möglich, separate Implementierungen für den IControl benannten in der Basisklassen Liste, den
IControl von geerbten ITextBox und den IControl von geerbten IListBox zu haben. Tatsächlich gibt es
keine andere Identität für diese Schnittstellen. Stattdessen verwenden die Implementierungen von ITextBox
und IListBox die gleiche Implementierung von und IControl werden lediglich für die Implementierung von
ComboBox drei Schnittstellen, IControl , ITextBox und berücksichtigt IListBox .

Die Member einer Basisklasse werden an der Schnittstellen Zuordnung beteiligt. Im Beispiel

interface Interface1
{
void F();
}

class Class1
{
public void F() {}
public void G() {}
}

class Class2: Class1, Interface1


{
new public void G() {}
}

die F -Methode in Class1 wird in der Class2 -Implementierung von verwendet Interface1 .
Vererbung der Schnittstellen Implementierung
Eine Klasse erbt alle Schnittstellen Implementierungen, die von ihren Basisklassen bereitgestellt werden.
Ohne eine explizite Neuimplementierung einer Schnittstelle kann eine abgeleitete Klasse die Schnittstellen
Zuordnungen, die Sie von ihren Basisklassen erbt, nicht ändern. Beispielsweise in den Deklarationen
interface IControl
{
void Paint();
}

class Control: IControl


{
public void Paint() {...}
}

class TextBox: Control


{
new public void Paint() {...}
}

die Paint -Methode in TextBox Paint blendet die-Methode in aus Control , ändert aber nicht die Zuordnung
von Control.Paint IControl.Paint zu, und Aufrufe von Paint über Klassen Instanzen und Schnittstellen
Instanzen haben die folgenden Auswirkungen:

Control c = new Control();


TextBox t = new TextBox();
IControl ic = c;
IControl it = t;
c.Paint(); // invokes Control.Paint();
t.Paint(); // invokes TextBox.Paint();
ic.Paint(); // invokes Control.Paint();
it.Paint(); // invokes Control.Paint();

Wenn eine Schnittstellen Methode jedoch einer virtuellen Methode in einer Klasse zugeordnet ist, kann es
vorkommen, dass abgeleitete Klassen die virtuelle Methode überschreiben und die Implementierung der
Schnittstelle ändern. Beispielsweise das Umschreiben der obigen Deklarationen in

interface IControl
{
void Paint();
}

class Control: IControl


{
public virtual void Paint() {...}
}

class TextBox: Control


{
public override void Paint() {...}
}

die folgenden Effekte werden nun beobachtet.

Control c = new Control();


TextBox t = new TextBox();
IControl ic = c;
IControl it = t;
c.Paint(); // invokes Control.Paint();
t.Paint(); // invokes TextBox.Paint();
ic.Paint(); // invokes Control.Paint();
it.Paint(); // invokes TextBox.Paint();

Da explizite Implementierungen von Schnittstellen Membern nicht als virtuell deklariert werden können, ist es
nicht möglich, eine explizite Schnittstellenmember-Implementierung zu überschreiben. Allerdings ist es für eine
explizite Schnittstellenmember-Implementierung durchaus gültig, eine andere Methode aufzurufen, und diese
andere Methode kann als virtuell deklariert werden, damit abgeleitete Klassen Sie überschreiben können.
Beispiel:

interface IControl
{
void Paint();
}

class Control: IControl


{
void IControl.Paint() { PaintControl(); }
protected virtual void PaintControl() {...}
}

class TextBox: Control


{
protected override void PaintControl() {...}
}

Hier können von abgeleitete Klassen Control die-Implementierung von spezialisieren, IControl.Paint indem
die-Methode überschrieben wird PaintControl .
Neuimplementierung der Schnittstelle
Eine Klasse, die eine Schnittstellen Implementierung erbt, kann die-Schnittstelle erneut implementieren ,
indem Sie in die Basisklassen Liste eingeschlossen wird.
Eine erneute Implementierung einer Schnittstelle folgt genau denselben Schnittstellen Mapping-Regeln wie eine
anfängliche Implementierung einer Schnittstelle. Folglich hat die geerbte Schnittstellen Zuordnung keinerlei
Auswirkungen auf die Schnittstellen Zuordnung, die für die erneute Implementierung der Schnittstelle
eingerichtet wurde. Beispielsweise in den Deklarationen

interface IControl
{
void Paint();
}

class Control: IControl


{
void IControl.Paint() {...}
}

class MyControl: Control, IControl


{
public void Paint() {}
}

die Tatsache, dass sich Zuordnungen Control IControl.Paint auf Control.IControl.Paint nicht auf die erneute
Implementierung in auswirkt MyControl , die zugeordnet wird IControl.Paint MyControl.Paint .
Geerbte öffentliche Element Deklarationen und geerbte explizite Schnittstellenmember-Deklarationen nehmen
am Schnittstellen Zuordnungsprozess für neu implementierte Schnittstellen Teil Beispiel:
interface IMethods
{
void F();
void G();
void H();
void I();
}

class Base: IMethods


{
void IMethods.F() {}
void IMethods.G() {}
public void H() {}
public void I() {}
}

class Derived: Base, IMethods


{
public void F() {}
void IMethods.H() {}
}

Hier ordnet die-Implementierung von IMethods in Derived die Schnittstellen Methoden auf Derived.F ,
Base.IMethods.G , Derived.IMethods.H und zu Base.I .

Wenn eine Klasse eine Schnittstelle implementiert, implementiert Sie implizit auch alle Basis Schnittstellen
dieser Schnittstelle. Ebenso ist eine erneute Implementierung einer Schnittstelle implizit eine erneute
Implementierung aller Basis Schnittstellen der Schnittstelle. Beispiel:

interface IBase
{
void F();
}

interface IDerived: IBase


{
void G();
}

class C: IDerived
{
void IBase.F() {...}
void IDerived.G() {...}
}

class D: C, IDerived
{
public void F() {...}
public void G() {...}
}

Hier wird die erneute Implementierung von IDerived ebenfalls neu implementiert, wobei eine Zuordnung zu
durchführt IBase IBase.F D.F .
Abstrakte Klassen und Schnittstellen
Wie eine nicht abstrakte Klasse muss eine abstrakte Klasse Implementierungen aller Member der Schnittstellen
bereitstellen, die in der Basisklassen Liste der Klasse aufgelistet sind. Eine abstrakte Klasse darf jedoch
Schnittstellen Methoden zu abstrakten Methoden zuordnen. Beispiel:
interface IMethods
{
void F();
void G();
}

abstract class C: IMethods


{
public abstract void F();
public abstract void G();
}

Hier ist die Implementierung von IMethods Maps F und G abstrakten Methoden, die in nicht abstrakten
Klassen, die von abgeleitet werden, überschrieben werden müssen C .
Beachten Sie, dass explizite Implementierungen von Schnittstellen Membern nicht abstrakt sein können, aber
explizite Implementierungen von Schnittstellen Membern sind natürlich berechtigt, abstrakte Methoden
aufzurufen. Beispiel:

interface IMethods
{
void F();
void G();
}

abstract class C: IMethods


{
void IMethods.F() { FF(); }
void IMethods.G() { GG(); }
protected abstract void FF();
protected abstract void GG();
}

Hier müssten nicht abstrakte Klassen, die von abgeleitet C werden, FF und GG die tatsächliche
Implementierung von überschreiben IMethods .
Enumerationen
04.11.2021 • 11 minutes to read

Ein Enumerationstyp ist ein eindeutiger Werttyp (Werttypen), der einen Satz benannter Konstanten deklariert.
Das Beispiel

enum Color
{
Red,
Green,
Blue
}

deklariert einen Enumerationstyp mit dem Namen mit Membern Color Red , Green und Blue .

Enum declarations (Enum-Deklarationen)


Eine Aufzählungs Deklaration deklariert einen neuen Aufzählungstyp. Eine Enumerationsdeklaration beginnt mit
dem-Schlüsselwort enum und definiert den Namen, die Barrierefreiheit, den zugrunde liegenden Typ und
Member der Enumeration.

enum_declaration
: attributes? enum_modifier* 'enum' identifier enum_base? enum_body ';'?
;

enum_base
: ':' integral_type
;

enum_body
: '{' enum_member_declarations? '}'
| '{' enum_member_declarations ',' '}'
;

Jeder Aufzählungstyp verfügt über einen entsprechenden ganzzahligen Typ, der als zugrunde liegenden Typ
des Aufzählungs Typs bezeichnet wird. Dieser zugrunde liegende Typ muss in der Lage sein, alle
Enumeratorwerte darzustellen, die in der-Enumeration definiert sind. Eine Aufzählungs Deklaration kann explizit
einen zugrunde liegenden Typ von byte , sbyte ,, short ushort , int , uint long oder ulong deklarieren.
Beachten Sie, dass char nicht als zugrunde liegender Typ verwendet werden kann. Eine Aufzählungs
Deklaration, die den zugrunde liegenden Typ nicht explizit deklariert, hat einen zugrunde liegenden Typ von
int .

Das Beispiel

enum Color: long


{
Red,
Green,
Blue
}

deklariert eine-Aufzählung mit einem zugrunde liegenden Typ von long . Ein Entwickler kann einen zugrunde
liegenden Typ von verwenden long , wie z. b., um die Verwendung von Werten zu ermöglichen, die im Bereich
von long , jedoch nicht im Bereich von liegen int , oder um diese Option für die Zukunft beizubehalten.

Enum modifiers (Enumerationsmodifizierer)


Eine enum_declaration kann optional eine Sequenz von enumerationsmodifiziererwerten enthalten:

enum_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;

Es ist ein Kompilierzeitfehler, damit derselbe Modifizierer mehrmals in einer Enumeration-Deklaration angezeigt
wird.
Die Modifizierer einer Enumerationsdeklaration haben dieselbe Bedeutung wie die einer Klassen Deklaration
(Klassenmodifizierer). Beachten Sie jedoch, dass die abstract sealed Modifizierer und in einer Aufzählungs
Deklaration nicht zulässig sind. Auffüge Zeichen können nicht abstrakt sein und dürfen keine Ableitung zulassen.

Enum members (Enumerationsmember)


Der Text einer Enumerationstypdeklaration definiert NULL oder mehr Enumerationsmember, bei denen es sich
um die benannten Konstanten des Enumerationstyps handelt. Es können nicht zwei Enumerationsmember
denselben Namen haben.

enum_member_declarations
: enum_member_declaration (',' enum_member_declaration)*
;

enum_member_declaration
: attributes? identifier ('=' constant_expression)?
;

Jedem Enumerationsmember ist ein konstanter Wert zugeordnet. Der Typ dieses Werts ist der zugrunde
liegende Typ für die enthaltende Enumeration. Der Konstante Wert für jedes Enumerationsmember muss im
Bereich des zugrunde liegenden Typs für die Enumeration liegen. Das Beispiel

enum Color: uint


{
Red = -1,
Green = -2,
Blue = -3
}

führt zu einem Kompilierzeitfehler, da sich die Konstanten Werte -1 , -2 und -3 nicht im Bereich des
zugrunde liegenden ganzzahligen Typs befinden uint .
Mehrere Enumerationsmember können denselben zugeordneten Wert gemeinsam verwenden. Das Beispiel
enum Color
{
Red,
Green,
Blue,

Max = Blue
}

zeigt eine Enumeration an, in der zwei Enumerationsmember-- Blue und Max --den gleichen zugeordneten
Wert aufweisen.
Der zugeordnete Wert eines Enumerationsmembers wird entweder implizit oder explizit zugewiesen. Wenn die
Deklaration des Enumerationsmembers über einen constant_expression Initialisierer verfügt, ist der Wert dieses
konstanten Ausdrucks, der implizit in den zugrunde liegenden Typ der Enumeration konvertiert wird, der
zugeordnete Wert des Enumerationsmembers. Wenn die Deklaration des Enumerationsmembers keinen
Initialisierer aufweist, wird der zugehörige Wert implizit wie folgt festgelegt:
Wenn das Enumerationsmember das erste Enumerationsmember ist, das im Enumerationstyp deklariert
wurde, ist der zugeordnete Wert 0 (null).
Andernfalls wird der zugeordnete Wert des Enumerationsmembers abgerufen, indem der zugeordnete Wert
des texthin vorangehenden Enumerationsmembers um eins erhöht wird. Dieser erweiterte Wert muss
innerhalb des Bereichs von Werten liegen, der durch den zugrunde liegenden Typ dargestellt werden kann.
andernfalls tritt ein Kompilierzeitfehler auf.
Das Beispiel

using System;

enum Color
{
Red,
Green = 10,
Blue
}

class Test
{
static void Main() {
Console.WriteLine(StringFromColor(Color.Red));
Console.WriteLine(StringFromColor(Color.Green));
Console.WriteLine(StringFromColor(Color.Blue));
}

static string StringFromColor(Color c) {


switch (c) {
case Color.Red:
return String.Format("Red = {0}", (int) c);

case Color.Green:
return String.Format("Green = {0}", (int) c);

case Color.Blue:
return String.Format("Blue = {0}", (int) c);

default:
return "Invalid color";
}
}
}
druckt die enumerationselementnamen und die zugehörigen Werte aus. Die Ausgabe ist:

Red = 0
Green = 10
Blue = 11

aus den folgenden Gründen:


dem Enumerationsmember Red wird automatisch der Wert 0 (null) zugewiesen (da er über keinen
Initialisierer verfügt und der erste Enumerationsmember ist);
der Enumeration-Member Green erhält explizit den Wert 10 .
dem Enumerationsmember Blue wird automatisch der Wert zugewiesen, der größer ist als der Member, der
ihm texthal vorangestellt ist.
Der zugeordnete Wert eines Enumerationsmembers darf nicht direkt oder indirekt den Wert seines eigenen
zugeordneten Enumerationsmembers verwenden. Abgesehen von dieser zirkulareinschränkungs Einschränkung
verweisen Enumerationsmember-Initialisierer möglicherweise unabhängig von ihrer Textposition auf andere
Initialisierer von Enumerationsmembern. In einem enumerationselementinitialisierer werden Werte anderer
Enumerationsmember immer so behandelt, als hätten Sie den Typ des zugrunde liegenden Typs, sodass
Umwandlungen beim Verweis auf andere Enumerationsmember nicht erforderlich sind.
Das Beispiel

enum Circular
{
A = B,
B
}

führt zu einem Kompilierzeitfehler, da die Deklarationen von A und B zirkulär sind. A hängt explizit von ab
B und ist implizit von abhängig B A .

Enumerationsmember werden genau analog zu den Feldern innerhalb von Klassen benannt und auf eine Weise
festgelegt. Der Gültigkeitsbereich eines Enumerationsmembers ist der Text seines enthaltenden
Enumerationstyps. Innerhalb dieses Bereichs kann mit dem einfachen Namen auf Enumerationsmember
verwiesen werden. Aus dem gesamten anderen Code muss der Name eines Enumerationsmembers mit dem
Namen seines Enumerationstyps qualifiziert werden. Enumerationsmember haben keine deklarierten
Barrierefreiheits Elemente. ein Enumerationsmember ist verfügbar, wenn auf den enthaltenden Enumerationstyp
zugegriffen werden kann.

The System.Enum type (Der System.Enum-Typ)


Der Typ System.Enum ist die abstrakte Basisklasse aller Enumerationstypen (Dies unterscheidet sich von dem
zugrunde liegenden Typ des Enumerationstyps), und die von geerbten Member System.Enum sind in jedem
Enumerationstyp verfügbar. Eine Boxing-Konvertierung (Boxing-Konvertierungen) ist von einem beliebigen
Aufgabentyp in vorhanden System.Enum , und eine Unboxing-Konvertierung (Unboxing-Konvertierungen) ist
von System.Enum in einen beliebigen Aufgabentyp vorhanden.
Beachten Sie, dass System.Enum nicht selbst ein enum_type ist. Vielmehr handelt es sich um eine class_type , von
der alle enum_type s abgeleitet werden. Der-Typ System.Enum erbt vom- System.ValueType Typ (System.
ValueType-Typ), der wiederum vom-Typ erbt object . Zur Laufzeit kann ein Wert vom Typ System.Enum null
oder ein Verweis auf einen geboxten Wert eines beliebigen Enumerationstyps sein.

Enum values and operations (Enumerationswerte und -vorgänge)


Jeder Aufzählungstyp definiert einen eindeutigen Typ. eine explizite Enumerationskonvertierung (Explizite
Enumerationskonvertierungen) ist erforderlich, um zwischen einem Enumerationstyp und einem ganzzahligen
Typ oder zwischen zwei Enumerationstypen zu konvertieren. Der Satz von Werten, der von einem
Enumerationstyp übernommen werden kann, wird nicht durch seine Enumerationsmember eingeschränkt.
Insbesondere kann jeder Wert des zugrunde liegenden Typs einer Enumeration in den Enumerationstyp
umgewandelt werden und ist ein eindeutiger gültiger Wert dieses Enumerationstyps.
Enumerationsmember haben den Typ Ihres enthaltenden Enumerationstyps (außer innerhalb anderer
Enumerationmember-Initialisierer: siehe Enumeration-Member). Der Wert eines Enumerationsmembers, der im
Enumerationstyp mit zugeordneter E Wert deklariert v ist, ist (E)v .
Die folgenden Operatoren können für Werte von Enumerationstypen verwendet werden: == , != , < , > ,
<= , >= (enumerationsvergleichs-Operatoren), Binary + (AdditionsOperator), Binary - (Subtraktions
Operator), ^ , & , | (logische enumerationsoperatoren), ~ (bitweiser Komplement Operator) ++ und --
(postfix-Inkrement-und Dekrementoperatoren und Präfix-
Jeder Enumerationstyp wird automatisch von der System.Enum -Klasse abgeleitet (die wiederum von und
abgeleitet ist System.ValueType object ). Daher können geerbte Methoden und Eigenschaften dieser Klasse für
Werte eines Enumerationstyps verwendet werden.
Delegaten
04.11.2021 • 17 minutes to read

Delegaten ermöglichen Szenarien, in denen andere Sprachen – wie C++, Pascal und Modula) mit Funktions
Zeigern adressiert wurden. Im Gegensatz zu C++-Funktions Zeigern sind Delegaten jedoch vollständig
objektorientiert, und im Gegensatz zu C++-Zeigern werden Delegaten sowohl eine Objektinstanz als auch eine
Methode kapseln.
Eine Delegatdeklaration definiert eine Klasse, die von der-Klasse abgeleitet wird System.Delegate . Eine
Delegatinstanz kapselt eine Aufruf Liste, bei der es sich um eine Liste von mindestens einer Methode handelt,
von denen jede als Aufruf Bare Entität bezeichnet wird. Bei Instanzmethoden besteht eine Aufruf Bare Entität aus
einer-Instanz und einer-Methode für diese Instanz. Bei statischen Methoden besteht eine Aufruf Bare Entität nur
aus einer-Methode. Das Aufrufen einer Delegatinstanz mit einem geeigneten Satz von Argumenten bewirkt,
dass alle Aufruf baren Entitäten des Delegaten mit dem angegebenen Satz von Argumenten aufgerufen werden.
Eine interessante und nützliche Eigenschaft einer Delegatinstanz besteht darin, dass die Klassen der gekapselten
Methoden nicht bekannt sind. alles, was wichtig ist, ist, dass diese Methoden mit dem Typ des Delegaten
kompatibel sind (Delegatdeklarationen). Dadurch eignen sich Delegaten hervorragend für den "anonymen"
Aufruf.

Delegate declarations (Delegatdeklarationen)


Ein delegate_declaration ist ein type_declaration (Typdeklarationen), der einen neuen Delegattyp deklariert.

delegate_declaration
: attributes? delegate_modifier* 'delegate' return_type
identifier variant_type_parameter_list?
'(' formal_parameter_list? ')' type_parameter_constraints_clause* ';'
;

delegate_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| delegate_modifier_unsafe
;

Es ist ein Kompilierzeitfehler, damit derselbe Modifizierer mehrmals in einer Delegatdeklaration angezeigt wird.
Der- new Modifizierer ist nur für Delegaten zulässig, die in einem anderen Typ deklariert sind. in diesem Fall
gibt er an, dass ein solcher Delegat einen geerbten Member mit demselben Namen verbirgt, wie im neuen
Modifiziererbeschrieben.
Die public protected internal private Modifizierer,, und steuern die Barrierefreiheit des Delegattyps.
Abhängig vom Kontext, in dem die Delegatdeklaration auftritt, sind einige dieser Modifizierer möglicherweise
nicht zulässig (alsBarrierefreiheit deklariert).
Der Typname des Delegaten ist ein Bezeichner.
Der optionale formal_parameter_list gibt die Parameter des Delegaten an, und return_type gibt den
Rückgabetyp des Delegaten an.
Der optionale variant_type_parameter_list (Variant-Typparameter Listen) gibt die Typparameter für den
Delegaten an.
Der Rückgabetyp eines Delegattyps muss entweder void oder Output-Safe (Varianz Sicherheit) sein.
Alle formalen Parametertypen eines Delegattyps müssen Eingabe sicher sein. Außerdem müssen alle- out oder
ref -Parametertypen ebenfalls Ausgabe sicher sein. Beachten Sie, dass auch out Parameter aufgrund einer
Einschränkung der zugrunde liegenden Ausführungsplattform Eingabe sicher sein müssen.
Delegattypen in c# sind namens äquivalent, nicht strukturell äquivalent. Insbesondere werden zwei
unterschiedliche Delegattypen, die über die gleichen Parameterlisten und Rückgabe Typen verfügen, als andere
Delegattypen betrachtet. Instanzen von zwei eindeutigen, aber strukturell äquivalenten Delegattypen können als
gleich (Delegat-Gleichheits Operatoren) verglichen werden.
Beispiel:

delegate int D1(int i, double d);

class A
{
public static int M1(int a, double b) {...}
}

class B
{
delegate int D2(int c, double d);
public static int M1(int f, double g) {...}
public static void M2(int k, double l) {...}
public static int M3(int g) {...}
public static void M4(int g) {...}
}

Die A.M1 -Methoden und B.M1 sind mit den Delegattypen D1 und kompatibel D2 , da Sie den gleichen
Rückgabetyp und die gleiche Parameterliste aufweisen. diese Delegattypen sind jedoch zwei verschiedene
Typen, sodass Sie nicht austauschbar sind. Die Methoden B.M2 , B.M3 und B.M4 sind mit den Delegattypen
und nicht kompatibel D1 D2 , da Sie unterschiedliche Rückgabe Typen oder Parameterlisten aufweisen.
Wie andere generische Typdeklarationen müssen Typargumente angegeben werden, um einen konstruierten
Delegattyp zu erstellen. Die Parametertypen und der Rückgabetyp eines konstruierten Delegattyps werden
erstellt, indem für jeden Typparameter in der Delegatdeklaration das entsprechende Typargument des
konstruierten Delegattyps ersetzt wird. Der resultierende Rückgabetyp und die Parametertypen werden
verwendet, um zu bestimmen, welche Methoden mit einem konstruierten Delegattyp kompatibel sind. Beispiel:

delegate bool Predicate<T>(T value);

class X
{
static bool F(int i) {...}
static bool G(string s) {...}
}

Die X.F -Methode ist mit dem Delegattyp kompatibel, Predicate<int> und die-Methode X.G ist mit dem
Delegattyp kompatibel Predicate<string> .
Die einzige Möglichkeit zum Deklarieren eines Delegattyps ist eine delegate_declaration. Ein Delegattyp ist ein
Klassentyp, der von abgeleitet wird System.Delegate . Delegattypen sind implizit sealed , daher ist es nicht
zulässig, einen beliebigen Typ von einem Delegattyp abzuleiten. Es ist auch nicht zulässig, einen nicht-
delegatklassentyp von abzuleiten System.Delegate . Beachten Sie, dass System.Delegate nicht selbst ein
Delegattyp ist, sondern ein Klassentyp, von dem alle Delegattypen abgeleitet werden.
C# bietet eine spezielle Syntax für die Instanziierung und den Aufruf von Delegaten. Mit Ausnahme der
Instanziierung kann jeder Vorgang, der auf eine Klasse oder eine Klasseninstanz angewendet werden kann, auch
auf eine Delegatklasse bzw.-Instanz angewendet werden. Insbesondere ist es möglich, System.Delegate über die
übliche Syntax des Element Zugriffs auf Member des Typs zuzugreifen.
Der Satz von Methoden, die von einer Delegatinstanz gekapselt werden, wird als Aufruf Liste bezeichnet. Wenn
eine Delegatinstanz (delegatkompatibilität) aus einer einzelnen Methode erstellt wird, kapselt Sie diese Methode,
und die Aufruf Liste enthält nur einen Eintrag. Wenn jedoch zwei nicht-NULL-Delegatinstanzen kombiniert
werden, werden die Aufruf Listen verkettet, und zwar im Order Left-Operand, Right Operand--, um eine neue
Aufruf Liste zu erstellen, die zwei oder mehr Einträge enthält.
Delegaten werden mit dem binären + (AdditionsOperator) und += Operatoren (Verbund Zuweisung)
kombiniert. Ein Delegat kann aus einer Kombination von Delegaten entfernt werden, wobei der binäre -
Operator (Subtraktions Operator) und die -= Operatoren (Verbund Zuweisung) verwendet werden. Delegaten
können auf Gleichheit verglichen werden (delegatgleichheits-Operatoren).
Im folgenden Beispiel wird die Instanziierung einer Reihe von Delegaten und ihre entsprechenden Aufruf Listen
veranschaulicht:

delegate void D(int x);

class C
{
public static void M1(int i) {...}
public static void M2(int i) {...}

class Test
{
static void Main() {
D cd1 = new D(C.M1); // M1
D cd2 = new D(C.M2); // M2
D cd3 = cd1 + cd2; // M1 + M2
D cd4 = cd3 + cd1; // M1 + M2 + M1
D cd5 = cd4 + cd3; // M1 + M2 + M1 + M1 + M2
}

Wenn cd1 und cd2 instanziiert werden, Kapseln Sie jeweils eine Methode. Wenn cd3 instanziiert wird,
verfügt sie über eine Aufruf Liste mit zwei Methoden, M1 und M2 in dieser Reihenfolge. cd4 die Aufruf Liste
enthält M1 , M2 und M1 in dieser Reihenfolge. Zum Schluss cd5 enthält die Aufruf Liste M1 , M2 , M1 , M1
und M2 in dieser Reihenfolge. Weitere Beispiele für das kombinieren (und entfernen) von Delegaten finden Sie
unter Delegataufruf.

Delegate compatibility (Delegatkompatibilität)


Eine Methode oder ein Delegat M ist mit einem Delegattyp kompatibel , D wenn Folgendes zutrifft:
D und M haben die gleiche Anzahl von Parametern, und jeder Parameter in D hat denselben- ref oder-
out Modifizierer wie der entsprechende Parameter in M .
Für jeden value-Parameter (ein Parameter ohne- ref oder- out Modifizierer) ist eine Identitäts
Konvertierung (Identitäts Konvertierung) oder eine implizite Verweis Konvertierung (implizite Verweis
Konvertierungen) vom Parametertyp in D zum entsprechenden Parametertyp in vorhanden M .
Für jeden ref - out Parameter oder-Parameter ist der Parametertyp in mit dem D Parametertyp in
identisch M .
Eine Identität oder eine implizite Verweis Konvertierung ist vom Rückgabetyp M zum Rückgabetyp von
vorhanden D .

Delegate instantiation (Delegatinstanziierung)


Eine Instanz eines Delegaten wird durch einen delegate_creation_expression (delegaterstellungs-Ausdrücke)
oder eine Konvertierung in einen Delegattyp erstellt. Die neu erstellte Delegatinstanz verweist dann auf eine der
folgenden Optionen:
Die statische Methode, auf die in der delegate_creation_expression verwiesen wird, oder
Das Zielobjekt (das nicht sein kann null ) und die Instanzmethode, auf die in der
delegate_creation_expression verwiesen wird, oder
Ein weiterer Delegat.
Beispiel:

delegate void D(int x);

class C
{
public static void M1(int i) {...}
public void M2(int i) {...}
}

class Test
{
static void Main() {
D cd1 = new D(C.M1); // static method
C t = new C();
D cd2 = new D(t.M2); // instance method
D cd3 = new D(cd2); // another delegate
}
}

Nach der instanziierten werden Delegatinstanzen immer auf das gleiche Zielobjekt und die gleiche Methode
verwiesen. Beachten Sie Folgendes: Wenn zwei Delegaten kombiniert werden oder eine aus einer anderen
entfernt wird, ergibt sich ein neuer Delegat mit einer eigenen Aufruf Liste. die Aufruf Listen der
zusammengesetzten oder entfernten Delegaten bleiben unverändert.

Delegate invocation (Delegataufruf)


C# bietet eine spezielle Syntax zum Aufrufen eines Delegaten. Wenn eine Delegatinstanz ungleich NULL, deren
Aufruf Liste einen Eintrag enthält, aufgerufen wird, ruft Sie die eine Methode mit denselben Argumenten auf, die
Sie angegeben hat, und gibt denselben Wert zurück wie die Methode, auf die verwiesen wird. (Weitere
Informationen zum Delegataufruf finden Sie unter delegieren von aufrufen.) Wenn während des Aufrufs eines
solchen Delegaten eine Ausnahme auftritt und diese Ausnahme nicht innerhalb der aufgerufenen Methode
abgefangen wird, wird die Suche nach einer Ausnahme catch-Klausel in der Methode fortgesetzt, die den
Delegaten aufgerufen hat, als ob diese Methode direkt die Methode aufgerufen hätte, auf die der Delegat
verwiesen hat.
Der Aufruf einer Delegatinstanz, deren Aufruf Liste mehrere Einträge enthält, verläuft durch das synchrone
Aufrufen der Methoden in der Aufruf Liste. Jede Methode, die so aufgerufen wird, wird der gleiche Satz von
Argumenten übergeben, die an die Delegatinstanz übergeben wurden. Wenn ein solcher Delegataufruf Verweis
Parameter (Verweis Parameter) enthält, erfolgt jeder Methodenaufruf mit einem Verweis auf die gleiche
Variable. Änderungen an dieser Variablen durch eine Methode in der Aufruf Liste werden für Methoden weiter
unten in der Aufruf Liste angezeigt. Wenn der Delegataufruf Ausgabeparameter oder einen Rückgabewert
enthält, erfolgt der endgültige Wert aus dem Aufruf des letzten Delegaten in der Liste.
Wenn bei der Verarbeitung des Aufrufs eines solchen Delegaten eine Ausnahme auftritt und diese Ausnahme
nicht innerhalb der aufgerufenen Methode abgefangen wird, wird die Suche nach einer Ausnahme catch-Klausel
in der Methode fortgesetzt, die den Delegaten aufgerufen hat, und alle Methoden weiter unten in der Aufruf
Liste werden nicht aufgerufen.
Der Versuch, eine Delegatinstanz aufzurufen, deren Wert NULL ist, führt zu einer Ausnahme vom Typ
System.NullReferenceException .

Im folgenden Beispiel wird gezeigt, wie Delegaten instanziiert, kombiniert, entfernt und aufgerufen werden:
using System;

delegate void D(int x);

class C
{
public static void M1(int i) {
Console.WriteLine("C.M1: " + i);
}

public static void M2(int i) {


Console.WriteLine("C.M2: " + i);
}

public void M3(int i) {


Console.WriteLine("C.M3: " + i);
}
}

class Test
{
static void Main() {
D cd1 = new D(C.M1);
cd1(-1); // call M1

D cd2 = new D(C.M2);


cd2(-2); // call M2

D cd3 = cd1 + cd2;


cd3(10); // call M1 then M2

cd3 += cd1;
cd3(20); // call M1, M2, then M1

C c = new C();
D cd4 = new D(c.M3);
cd3 += cd4;
cd3(30); // call M1, M2, M1, then M3

cd3 -= cd1; // remove last M1


cd3(40); // call M1, M2, then M3

cd3 -= cd4;
cd3(50); // call M1 then M2

cd3 -= cd2;
cd3(60); // call M1

cd3 -= cd2; // impossible removal is benign


cd3(60); // call M1

cd3 -= cd1; // invocation list is empty so cd3 is null

cd3(70); // System.NullReferenceException thrown

cd3 -= cd1; // impossible removal is benign


}
}

Wie in der-Anweisung gezeigt cd3 += cd1; , kann ein Delegat mehrmals in einer Aufruf Liste vorhanden sein. In
diesem Fall wird Sie nur einmal pro vorkommen aufgerufen. In einer Aufruf Liste wie diesem wird beim
Entfernen dieses Delegaten das letzte Vorkommen in der Aufruf Liste tatsächlich entfernt.
Unmittelbar vor der Ausführung der abschließenden Anweisung cd3 -= cd1; verweist der Delegat cd3 auf
eine leere Aufruf Liste. Der Versuch, einen Delegaten aus einer leeren Liste zu entfernen (oder um einen nicht
vorhandenen Delegaten aus einer nicht leeren Liste zu entfernen), ist kein Fehler.
Die erstellte Ausgabe lautet wie folgt:

C.M1: -1
C.M2: -2
C.M1: 10
C.M2: 10
C.M1: 20
C.M2: 20
C.M1: 20
C.M1: 30
C.M2: 30
C.M1: 30
C.M3: 30
C.M1: 40
C.M2: 40
C.M3: 40
C.M1: 50
C.M2: 50
C.M1: 60
C.M1: 60
Ausnahmen
04.11.2021 • 8 minutes to read

Ausnahmen in c# bieten eine strukturierte, einheitliche und typsichere Methode zum Behandeln von
Fehlerbedingungen auf Systemebene und Anwendungsebene. Der Ausnahme Mechanismus in c# ähnelt dem
von C++. es gibt jedoch einige wichtige Unterschiede:
In c# müssen alle Ausnahmen durch eine Instanz eines von abgeleiteten Klassen Typs dargestellt werden
System.Exception . In C++ kann jeder beliebige Wert eines beliebigen Typs verwendet werden, um eine
Ausnahme darzustellen.
In c# kann ein schließlich-Block (die try-Anweisung) verwendet werden, um Beendigungs Code zu schreiben,
der sowohl bei normaler Ausführung als auch bei Ausnahmebedingungen ausgeführt wird. Ein solcher Code
ist in C++ schwer zu schreiben, ohne Code zu duplizieren.
In c# haben Ausnahmen auf Systemebene, z. b. Überlauf, Division durch Null und NULL-Dereferenzierungen,
klar definierte Ausnahme Klassen, die mit Fehlerbedingungen auf Anwendungsebene übereinstimmt.

Ursache von Ausnahmen


Eine Ausnahme kann auf zwei verschiedene Arten ausgelöst werden.
Eine throw -Anweisung (die throw-Anweisung) löst sofort und bedingungslos eine Ausnahme aus. Das-
Steuerelement erreicht die-Anweisung nie unmittelbar nach dem throw .
Bestimmte Ausnahmebedingungen, die während der Verarbeitung von c#-Anweisungen und-Ausdrücken
auftreten, verursachen in bestimmten Situationen eine Ausnahme, wenn der Vorgang nicht normal
abgeschlossen werden kann. Beispielsweise löst eine Ganzzahl-Divisions Operation (Divisions Operator) eine
aus, System.DivideByZeroException Wenn der Nenner 0 (null) ist. Eine Liste der verschiedenen Ausnahmen,
die auf diese Weise auftreten können, finden Sie unter allgemeine Ausnahme Klassen .

The System.Exception class (Die System.Exception-Klasse)


Die- System.Exception Klasse ist der Basistyp aller Ausnahmen. Diese Klasse verfügt über einige wichtige
Eigenschaften, die von allen Ausnahmen gemeinsam genutzt werden:
Message ist eine schreibgeschützte Eigenschaft vom Typ string , die eine lesbare Beschreibung des Grunds
für die Ausnahme enthält.
InnerException ist eine schreibgeschützte Eigenschaft vom Typ Exception . Wenn der Wert ungleich NULL
ist, verweist er auf die Ausnahme, die die aktuelle Ausnahme verursacht hat – d. h., die aktuelle Ausnahme
wurde in einem catch-Block ausgelöst, der den verarbeitet hat InnerException . Andernfalls ist der Wert
NULL, was darauf hinweist, dass diese Ausnahme nicht durch eine andere Ausnahme verursacht wurde. Die
Anzahl von Ausnahme Objekten, die auf diese Weise verkettet sind, kann beliebig sein.
Der Wert dieser Eigenschaften kann in Aufrufen des Instanzkonstruktors für angegeben werden
System.Exception .

Behandlung von Ausnahmen


Ausnahmen werden von einer- try Anweisung (der try-Anweisung) behandelt.
Wenn eine Ausnahme auftritt, sucht das System nach der nächstgelegenen Klausel, die catch die Ausnahme
verarbeiten kann, wie durch den Lauf Zeittyp der Ausnahme festgelegt. Zuerst wird die aktuelle Methode nach
einer lexikalisch einschließenden try Anweisung durchsucht, und die zugeordneten catch-Klauseln der try-
Anweisung werden in der richtigen Reihenfolge berücksichtigt. Wenn dies fehlschlägt, wird die Methode, die die
aktuelle Methode aufgerufen hat, nach einer lexikalisch einschließenden Anweisung durchsucht try , die den
Punkt des Aufrufs der aktuellen Methode einschließt. Diese Suche wird fortgesetzt catch , bis eine Klausel
gefunden wird, die die aktuelle Ausnahme behandeln kann, indem Sie eine Ausnahme Klasse benennt, die der
gleichen Klasse oder einer Basisklasse entspricht, und zwar vom Lauf Zeittyp der ausgelösten Ausnahme. Eine-
catch Klausel, die keine Ausnahme Klasse nennt, kann jede Ausnahme behandeln.

Sobald eine passende catch-Klausel gefunden wird, bereitet das System die Übertragung der Steuerung an die
erste Anweisung der catch-Klausel vor. Bevor die Ausführung der catch-Klausel beginnt, führt das System zuerst
nacheinander alle Klauseln, die finally try-Anweisungen zugeordnet waren, als die, die die Ausnahme
abgefangen haben, aus.
Wenn keine übereinstimmende catch-Klausel gefunden wird, tritt eines von zwei Dingen auf:
Wenn die Suche nach einer übereinstimmenden catch-Klausel einen statischen Konstruktor (statische
Konstruktoren) oder einen statischen Feldinitialisierer erreicht, System.TypeInitializationException wird eine
an dem Punkt ausgelöst, der den Aufruf des statischen Konstruktors ausgelöst hat. Die innere Ausnahme von
System.TypeInitializationException enthält die Ausnahme, die ursprünglich ausgelöst wurde.
Wenn die Suche nach übereinstimmenden catch-Klauseln den Code erreicht, der den Thread anfänglich
gestartet hat, wird die Ausführung des Threads beendet. Die Auswirkungen dieser Beendigung sind
Implementierungs definiert.
Ausnahmen, die während der dekonstruktorausführung auftreten, sind besonders erwähnenswert. Wenn
während der dekonstruktorausführung eine Ausnahme auftritt und diese Ausnahme nicht abgefangen wird,
wird die Ausführung dieses Dekonstruktors beendet und der Dekonstruktor der Basisklasse (sofern vorhanden)
aufgerufen. Wenn keine Basisklasse vorhanden ist (wie im Fall des object Typs) oder wenn kein
basisklassendekonstruktor vorhanden ist, wird die Ausnahme verworfen.

Common Exception Classes (Allgemeine Ausnahmeklassen)


Die folgenden Ausnahmen werden von bestimmten c#-Vorgängen ausgelöst.

System.ArithmeticException Eine Basisklasse für Ausnahmen (z.B.


System.DivideByZeroException und
System.OverflowException ), die während arithmetischer
Operationen auftreten.

System.ArrayTypeMismatchException Wird ausgelöst, wenn ein Speicher in ein Array fehlschlägt,


weil der tatsächliche Typ des gespeicherten Elements nicht
mit dem tatsächlichen Typ des Arrays kompatibel ist.

System.DivideByZeroException Wird ausgelöst, wenn versucht wird, einen ganzzahligen


Wert durch Null zu teilen.

System.IndexOutOfRangeException Wird ausgelöst, wenn versucht wird, ein Array über einen
Index zu indizieren, der kleiner als 0 (null) oder außerhalb der
Grenzen des Arrays ist.

System.InvalidCastException Wird ausgelöst, wenn eine explizite Konvertierung von einem


Basistyp oder einer Schnittstelle in einen abgeleiteten Typ zur
Laufzeit fehlschlägt.
System.NullReferenceException Wird ausgelöst, wenn ein null Verweis so verwendet wird,
dass das Objekt, auf das verwiesen wird, benötigt wird.

System.OutOfMemoryException Wird ausgelöst, wenn beim Zuweisen von Speicher (via) ein
Fehler auftritt new .

System.OverflowException Wird ausgelöst, wenn eine arithmetische Operation im


Kontext checked überläuft.

System.StackOverflowException Wird ausgelöst, wenn der Ausführungs Stapel erschöpft ist,


indem zu viele ausstehende Methodenaufrufe vorhanden
sind. in der Regel ein Hinweis auf eine sehr tiefe oder
unbegrenzte Rekursion.

System.TypeInitializationException Wird ausgelöst, wenn ein statischer Konstruktor eine


Ausnahme auslöst und keine catch Klauseln zum
Abfangen vorhanden sind.
Attribute
04.11.2021 • 46 minutes to read

Ein Großteil der Programmiersprache c# ermöglicht es dem Programmierer, deklarative Informationen über die
im Programm definierten Entitäten anzugeben. Der Zugriff auf eine Methode in einer Klasse wird z. b.
angegeben, indem Sie mit den method_modifier s public , protected , und versehen wird internal private .
C# ermöglicht Programmierern das erfinden neuer Arten von deklarativen Informationen, die als Attribute
bezeichnet werden. Programmierer können dann Attribute an verschiedene Programm Entitäten anfügen und
Attributinformationen in einer Laufzeitumgebung abrufen. Ein Framework kann z. b. ein- HelpAttribute Attribut
definieren, das auf bestimmte Programmelemente (z. b. Klassen und Methoden) platziert werden kann, um eine
Zuordnung von diesen Programmelementen zu Ihrer Dokumentation bereitzustellen.
Attribute werden durch die Deklaration von Attribut Klassen (Attribut Klassen) definiert, die über positionelle
und benannte Parameter verfügen können (positionelle und benannte Parameter). Attribute werden mithilfe von
Attribut Spezifikationen (Attribut Spezifikation) an Entitäten in einem c#-Programm angefügt und können zur
Laufzeit als Attribut Instanzen (Attribut Instanzen) abgerufen werden.

Attribute classes (Attributklassen)


Eine Klasse, die von der abstrakten-Klasse abgeleitet wird, unabhängig davon, System.Attribute ob Sie direkt
oder indirekt ist, ist eine *Attribut Klasse _. Die Deklaration einer Attribut Klasse definiert eine neue Art von _
*Attribute**, die in einer-Deklaration platziert werden kann. Gemäß der Konvention werden Attribut Klassen mit
dem Suffix benannt Attribute . Die Verwendung eines Attributs kann dieses Suffix entweder einschließen oder
weglassen.
Attribut Verwendung
Das-Attribut AttributeUsage (AttributeUsage-Attribut) wird verwendet, um zu beschreiben, wie eine Attribut
Klasse verwendet werden kann.
AttributeUsage verfügt über einen Positions Parameter (Positions Parameter und benannte Parameter), der
einer Attribut Klasse das Angeben der Arten von Deklarationen ermöglicht, für die Sie verwendet werden kann.
Das Beispiel

using System;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
public class SimpleAttribute: Attribute
{
...
}

definiert eine Attribut Klasse SimpleAttribute mit dem Namen, die nur auf class_declaration s und
interface_declaration s platziert werden kann. Das Beispiel

[Simple] class Class1 {...}

[Simple] interface Interface1 {...}

zeigt verschiedene Verwendungen des- Simple Attributs an. Obwohl dieses Attribut mit dem Namen definiert
ist SimpleAttribute , wird das Suffix möglicherweise weggelassen, wenn dieses Attribut verwendet wird
Attribute . Dies führt zu einem Kurznamen Simple . Daher ist das obige Beispiel semantisch äquivalent zu
folgendem:

[SimpleAttribute] class Class1 {...}

[SimpleAttribute] interface Interface1 {...}

AttributeUsage verfügt über einen benannten Parameter (positionell und benannte Parameter) AllowMultiple
mit dem Namen, der angibt, ob das Attribut für eine bestimmte Entität mehrmals angegeben werden kann.
Wenn AllowMultiple für eine Attribut Klasse true festgelegt ist, ist diese Attribut Klasse eine *Multiuse-
Attribut Klasse _ und kann mehrmals für eine Entität angegeben werden. Wenn AllowMultiple für eine
Attribut Klasse false oder nicht angegeben ist, ist diese Attribut Klasse eine _ -Attribut Klasse mit einmaliger
Verwendung * und kann höchstens einmal für eine Entität angegeben werden.
Das Beispiel

using System;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]


public class AuthorAttribute: Attribute
{
private string name;

public AuthorAttribute(string name) {


this.name = name;
}

public string Name {


get { return name; }
}
}

definiert eine Multiuse-Attribut Klasse mit dem Namen AuthorAttribute . Das Beispiel

[Author("Brian Kernighan"), Author("Dennis Ritchie")]


class Class1
{
...
}

zeigt eine Klassen Deklaration mit zwei Verwendungen des- Author Attributs an.
AttributeUsage verfügt über einen anderen benannten Parameter Inherited mit dem Namen, der angibt, ob
das Attribut, wenn es in einer Basisklasse angegeben wird, auch von Klassen geerbt wird, die von dieser
Basisklasse abgeleitet werden. Wenn Inherited für eine Attribut Klasse true ist, wird dieses Attribut geerbt.
Wenn Inherited für eine Attribut Klasse false ist, wird dieses Attribut nicht geerbt. Wenn Sie nicht angegeben
ist, ist der Standardwert true.
Einer Attribut Klasse X AttributeUsage , der kein Attribut angefügt ist, wie in

using System;

class X: Attribute {...}

entspricht Folgendem:
using System;

[AttributeUsage(
AttributeTargets.All,
AllowMultiple = false,
Inherited = true)
]
class X: Attribute {...}

Positions Parameter und benannte Parameter


Attribut Klassen können die benannten Parameter"*Positions Parameter " und " *" aufweisen * *. Jeder
öffentliche Instanzkonstruktor für eine Attribut Klasse definiert eine gültige Sequenz von positionellen
Parametern für diese Attribut Klasse. Alle nicht statischen öffentlichen Lese-/Schreibfelder und-Eigenschaften für
eine Attribut Klasse definieren einen benannten Parameter für die Attribut Klasse.
Das Beispiel

using System;

[AttributeUsage(AttributeTargets.Class)]
public class HelpAttribute: Attribute
{
public HelpAttribute(string url) { // Positional parameter
...
}

public string Topic { // Named parameter


get {...}
set {...}
}

public string Url {


get {...}
}
}

definiert eine Attribut Klasse mit HelpAttribute dem Namen, die über einen positionellen Parameter, url und
einen benannten Parameter verfügt Topic . Obwohl Sie nicht statisch und öffentlich ist, definiert die-
Eigenschaft Url keinen benannten Parameter, da Sie nicht über Lese-/Schreibzugriff verfügt.
Diese Attribut Klasse kann wie folgt verwendet werden:

[Help("http://www.mycompany.com/.../Class1.htm")]
class Class1
{
...
}

[Help("http://www.mycompany.com/.../Misc.htm", Topic = "Class2")]


class Class2
{
...
}

Attributparametertypen
Die Typen von positionellen und benannten Parametern für eine Attribut Klasse sind auf die Attribut
Parameter typen beschränkt, die wie folgt lauten:
Einer der folgenden Typen: bool , byte , char , double , float , int , long , sbyte , short , string ,
uint , ulong , ushort .
Typ object .
Typ System.Type .
Ein Enumerationstyp, sofern er über öffentliche Zugriffsmöglichkeiten verfügt und die Typen, in denen er
gespeichert ist (sofern vorhanden), auch über die öffentliche Barrierefreiheit verfügen (Attribut Spezifikation).
Eindimensionale Arrays der oben genannten Typen.
Ein Konstruktorargument oder ein öffentliches Feld, das nicht über einen dieser Typen verfügt, kann nicht als
Positions Parameter oder als benannter Parameter in einer Attribut Spezifikation verwendet werden.

Attribute specification (Attributspezifikation)


*Attribut Spezifikation _ ist die Anwendung eines zuvor definierten Attributs auf eine Deklaration. Ein Attribut
ist eine zusätzliche deklarative Information, die für eine-Deklaration angegeben wird. Attribute können im
globalen Gültigkeitsbereich (zum Angeben von Attributen für die enthaltende Assembly oder das enthaltende
Modul) und für _type_declaration * s (Typdeklarationen) angegeben werden. class_member_declaration s
(Typparameter Einschränkungen), interface_member_declaration s (Schnittstellenmember),
struct_member_declarations (Strukturmember), enum_member_declaration s (enum Members),
accessor_declarations (Accessoren), event_accessor_declarations (Feld ähnliche Ereignisse) und
formal_parameter_list s (Methoden Parameter).
Attribute werden in Attribut Abschnitten angegeben. Ein Attribut Abschnitt besteht aus einem Paar von
eckigen Klammern, die eine durch Trennzeichen getrennte Liste mit einem oder mehreren Attributen
umschließen. Die Reihenfolge, in der Attribute in einer solchen Liste angegeben werden, und die Reihenfolge, in
der Abschnitte, die an dieselbe Programm Entität angefügt sind, nicht signifikant ist. Beispielsweise sind die
Attribut Spezifikationen,, [A][B] [B][A] [A,B] und [B,A] Äquivalent.

global_attributes
: global_attribute_section+
;

global_attribute_section
: '[' global_attribute_target_specifier attribute_list ']'
| '[' global_attribute_target_specifier attribute_list ',' ']'
;

global_attribute_target_specifier
: global_attribute_target ':'
;

global_attribute_target
: 'assembly'
| 'module'
;

attributes
: attribute_section+
;

attribute_section
: '[' attribute_target_specifier? attribute_list ']'
| '[' attribute_target_specifier? attribute_list ',' ']'
;

attribute_target_specifier
: attribute_target ':'
;

attribute_target
: 'field'
| 'event'
| 'method'
| 'param'
| 'property'
| 'return'
| 'type'
;

attribute_list
: attribute (',' attribute)*
;

attribute
: attribute_name attribute_arguments?
;

attribute_name
: type_name
;

attribute_arguments
: '(' positional_argument_list? ')'
| '(' positional_argument_list ',' named_argument_list ')'
| '(' named_argument_list ')'
;

positional_argument_list
: positional_argument (',' positional_argument)*
;

positional_argument
: attribute_argument_expression
;

named_argument_list
: named_argument (',' named_argument)*
;

named_argument
: identifier '=' attribute_argument_expression
;

attribute_argument_expression
: expression
;

Ein Attribut besteht aus einem ATTRIBUTE_NAME und einer optionalen Liste von positionellen und benannten
Argumenten. Die Positions Argumente (sofern vorhanden) sind den benannten Argumenten vorangestellt. Ein
Positions Argument besteht aus einem attribute_argument_expression. ein benanntes Argument besteht aus
einem Namen, gefolgt von einem Gleichheitszeichen, gefolgt von einer attribute_argument_expression, die in
der Regel durch die gleichen Regeln wie die einfache Zuweisung eingeschränkt werden. Die Reihenfolge der
benannten Argumente ist nicht signifikant.
Der ATTRIBUTE_NAME der eine Attribut Klasse identifiziert. Wenn die Form von ATTRIBUTE_NAME TYPE_NAME
ist, muss dieser Name auf eine Attribut Klasse verweisen. Andernfalls tritt ein Kompilierungsfehler auf. Das
Beispiel

class Class1 {}

[Class1] class Class2 {} // Error

führt zu einem Kompilierzeitfehler, da er versucht, Class1 als Attribut Klasse zu verwenden, wenn Class1 keine
Attribut Klasse ist.
Bestimmte Kontexte ermöglichen die Angabe eines Attributs für mehr als ein Ziel. Ein Programm kann das Ziel
explizit angeben, indem er eine attribute_target_specifier einschließt. Wenn ein Attribut auf der globalen Ebene
platziert wird, ist ein global_attribute_target_specifier erforderlich. An allen anderen Standorten wird ein
angemessener Standardwert angewendet, aber ein attribute_target_specifier kann verwendet werden, um den
Standardwert in bestimmten mehrdeutigen Fällen zu bestätigen oder zu überschreiben (oder um die
Standardeinstellung in nicht mehrdeutigen Fällen zu bestätigen). Folglich können attribute_target_specifier s in
der Regel ausgelassen werden, außer auf der globalen Ebene. Die potenziell mehrdeutigen Kontexte werden wie
folgt aufgelöst:
Ein Attribut, das im globalen Gültigkeitsbereich angegeben ist, kann entweder auf die Zielassembly oder das
Zielmodul angewendet werden. Für diesen Kontext ist kein Standardwert vorhanden. Daher ist in diesem
Kontext immer ein attribute_target_specifier erforderlich. Das vorhanden sein des assembly
attribute_target_specifier gibt an, dass das Attribut auf die Zielassembly angewendet wird. das vorhanden
sein des module attribute_target_specifier gibt an, dass das Attribut auf das Zielmodul angewendet wird.
Ein Attribut, das für eine Delegatdeklaration angegeben wird, kann entweder auf den deklarierten Delegaten
oder seinen Rückgabewert angewendet werden. Wenn keine attribute_target_specifier vorhanden ist, wird
das Attribut auf den Delegaten angewendet. Das vorhanden sein des type attribute_target_specifier gibt an,
dass das Attribut für den Delegaten gilt; das vorhanden sein des return attribute_target_specifier gibt an,
dass das Attribut auf den Rückgabewert angewendet wird.
Ein Attribut, das in einer Methoden Deklaration angegeben wird, kann entweder auf die deklarierte Methode
oder auf ihren Rückgabewert angewendet werden. Wenn keine attribute_target_specifier vorhanden ist, wird
das-Attribut auf die-Methode angewendet. Das vorhanden sein des method attribute_target_specifier gibt an,
dass das Attribut auf die Methode angewendet wird. das vorhanden sein des return
attribute_target_specifier gibt an, dass das Attribut auf den Rückgabewert angewendet wird.
Ein Attribut, das in einer Operator Deklaration angegeben wird, kann entweder auf den deklarierten Operator
oder seinen Rückgabewert angewendet werden. Wenn keine attribute_target_specifier vorhanden ist, wird
das-Attribut auf den-Operator angewendet. Das vorhanden sein des method attribute_target_specifier gibt
an, dass das Attribut für den Operator gilt; das vorhanden sein des return attribute_target_specifier gibt an,
dass das Attribut auf den Rückgabewert angewendet wird.
Ein in einer Ereignis Deklaration festgelegtes Attribut, das Ereignisaccessoren auslässt, kann auf das zu
deklarier Ende Ereignis, auf das zugeordnete Feld (wenn das Ereignis nicht abstrakt ist) oder auf die
zugeordneten Add-und Remove-Methoden angewendet werden. Wenn keine attribute_target_specifier
vorhanden ist, gilt das-Attribut für das-Ereignis. Das vorhanden sein des event attribute_target_specifier
gibt an, dass das Attribut auf das Ereignis angewendet wird. das vorhanden sein des field
attribute_target_specifier gibt an, dass das Attribut auf das Feld angewendet wird, und das vorhanden sein
der method attribute_target_specifier gibt an, dass das Attribut auf die Methoden angewendet wird.
Ein Attribut, das in einer Get-Accessor-Deklaration für eine Eigenschaft oder Indexer-Deklaration angegeben
ist, kann entweder auf die zugeordnete Methode oder auf ihren Rückgabewert angewendet werden Wenn
keine attribute_target_specifier vorhanden ist, wird das-Attribut auf die-Methode angewendet. Das
vorhanden sein des method attribute_target_specifier gibt an, dass das Attribut auf die Methode angewendet
wird. das vorhanden sein des return attribute_target_specifier gibt an, dass das Attribut auf den
Rückgabewert angewendet wird.
Ein Attribut, das für einen Set-Accessor für eine Eigenschaft oder eine Indexer-Deklaration angegeben ist,
kann entweder auf die zugeordnete Methode oder auf den entsprechenden impliziten Parameter
angewendet werden. Wenn keine attribute_target_specifier vorhanden ist, wird das-Attribut auf die-Methode
angewendet. Das vorhanden sein des method attribute_target_specifier gibt an, dass das Attribut auf die
Methode angewendet wird. das vorhanden sein des param attribute_target_specifier gibt an, dass das
Attribut auf den Parameter angewendet wird. das vorhanden sein der return attribute_target_specifier gibt
an, dass das Attribut auf den Rückgabewert angewendet wird.
Ein Attribut, das in einer Add-oder remove-Accessor-Deklaration für eine Ereignis Deklaration angegeben ist,
kann entweder auf die zugeordnete Methode oder auf den zugehörigen Parameter anwenden. Wenn keine
attribute_target_specifier vorhanden ist, wird das-Attribut auf die-Methode angewendet. Das vorhanden sein
des method attribute_target_specifier gibt an, dass das Attribut auf die Methode angewendet wird. das
vorhanden sein des param attribute_target_specifier gibt an, dass das Attribut auf den Parameter
angewendet wird. das vorhanden sein der return attribute_target_specifier gibt an, dass das Attribut auf
den Rückgabewert angewendet wird.
In anderen Kontexten ist das Einschließen einer attribute_target_specifier zulässig, aber nicht erforderlich.
Beispielsweise kann eine Klassen Deklaration den Spezifizierer einschließen oder weglassen type :

[type: Author("Brian Kernighan")]


class Class1 {}

[Author("Dennis Ritchie")]
class Class2 {}

Es ist ein Fehler, einen ungültigen attribute_target_specifier anzugeben. Beispielsweise kann der Spezifizierer
param nicht in einer Klassen Deklaration verwendet werden:

[param: Author("Brian Kernighan")] // Error


class Class1 {}

Gemäß der Konvention werden Attribut Klassen mit dem Suffix benannt Attribute . Ein ATTRIBUTE_NAME der
Form TYPE_NAME kann dieses Suffix entweder einschließen oder weglassen. Wenn eine Attribut Klasse sowohl
mit als auch ohne dieses Suffix gefunden wird, ist eine Mehrdeutigkeit vorhanden und ein Fehler bei der
Kompilierzeit. Wenn die ATTRIBUTE_NAME so geschrieben ist, dass der ganz rechts Bezeichner ein ausführlicher
Bezeichner (Bezeichner) ist, dann wird nur ein Attribut ohne Suffix abgeglichen, sodass diese Mehrdeutigkeit
aufgelöst werden kann. Das Beispiel

using System;

[AttributeUsage(AttributeTargets.All)]
public class X: Attribute
{}

[AttributeUsage(AttributeTargets.All)]
public class XAttribute: Attribute
{}

[X] // Error: ambiguity


class Class1 {}

[XAttribute] // Refers to XAttribute


class Class2 {}

[@X] // Refers to X
class Class3 {}

[@XAttribute] // Refers to XAttribute


class Class4 {}

zeigt zwei Attribut Klassen mit dem Namen X und XAttribute . Das-Attribut [X] ist mehrdeutig, da es
entweder auf X oder verweisen kann XAttribute . Die Verwendung eines ausführlichen Bezeichners
ermöglicht das Angeben der exakten Absicht in seltenen Fällen. Das Attribut [XAttribute] ist nicht mehrdeutig
(obwohl es sich um eine Attribut Klasse mit dem Namen XAttributeAttribute ! handelt). Wenn die Deklaration
für die Klasse X entfernt wird, verweisen beide Attribute wie folgt auf die Attribut Klasse mit dem Namen
XAttribute :
using System;

[AttributeUsage(AttributeTargets.All)]
public class XAttribute: Attribute
{}

[X] // Refers to XAttribute


class Class1 {}

[XAttribute] // Refers to XAttribute


class Class2 {}

[@X] // Error: no attribute named "X"


class Class3 {}

Es ist ein Kompilierzeitfehler, eine Attribut Klasse mit nur einer Verwendung mehrmals für dieselbe Entität zu
verwenden. Das Beispiel

using System;

[AttributeUsage(AttributeTargets.Class)]
public class HelpStringAttribute: Attribute
{
string value;

public HelpStringAttribute(string value) {


this.value = value;
}

public string Value {


get {...}
}
}

[HelpString("Description of Class1")]
[HelpString("Another description of Class1")]
public class Class1 {}

führt zu einem Kompilierzeitfehler, weil versucht wird,, bei dem es sich um HelpString eine Attribut Klasse mit
einmaliger Verwendung handelt, mehrmals in der Deklaration von zu verwenden Class1 .
Ein Ausdruck E ist eine attribute_argument_expression , wenn alle der folgenden Aussagen zutreffen:
Der Typ von E ist ein Attribut Parametertyp (Attribut Parametertypen).
Zum Zeitpunkt der Kompilierung kann der Wert von E in einen der folgenden Werte aufgelöst werden:
Ein konstanter Wert.
Ein System.Type -Objekt.
Ein eindimensionales Array von attribute_argument_expression s.
Beispiel:
using System;

[AttributeUsage(AttributeTargets.Class)]
public class TestAttribute: Attribute
{
public int P1 {
get {...}
set {...}
}

public Type P2 {
get {...}
set {...}
}

public object P3 {
get {...}
set {...}
}
}

[Test(P1 = 1234, P3 = new int[] {1, 3, 5}, P2 = typeof(float))]


class MyClass {}

Ein typeof_expression (der typeof-Operator), der als Attribut Argument Ausdruck verwendet wird, kann auf
einen nicht generischen Typ, einen geschlossenen konstruierten Typ oder einen ungebundenen generischen Typ
verweisen, er kann jedoch nicht auf einen offenen Typ verweisen. Dadurch wird sichergestellt, dass der
Ausdruck zur Kompilierzeit aufgelöst werden kann.

class A: Attribute
{
public A(Type t) {...}
}

class G<T>
{
[A(typeof(T))] T t; // Error, open type in attribute
}

class X
{
[A(typeof(List<int>))] int x; // Ok, closed constructed type
[A(typeof(List<>))] int y; // Ok, unbound generic type
}

Attribute instances (Attributinstanzen)


Eine Attribut Instanz ist eine-Instanz, die ein Attribut zur Laufzeit darstellt. Ein Attribut wird mit einer Attribut
Klasse, positionellen Argumenten und benannten Argumenten definiert. Eine Attribut Instanz ist eine Instanz der
Attribut Klasse, die mit den positionellen und benannten Argumenten initialisiert wird.
Das Abrufen einer Attribut Instanz umfasst sowohl die Kompilierzeit-als auch die Lauf Zeit Verarbeitung, wie in
den folgenden Abschnitten beschrieben.
Kompilierung eines Attributs
Die Kompilierung eines Attributs mit der Attribut Klasse T , positional_argument_list P und
named_argument_list N besteht aus den folgenden Schritten:
Befolgen Sie die Schritte zur Kompilierzeit Verarbeitung zum Kompilieren einer object_creation_expression
des Formulars new T(P) . Diese Schritte führen entweder zu einem Kompilierzeitfehler, oder Sie bestimmen
einen Instanzkonstruktor C für T , der zur Laufzeit aufgerufen werden kann.
Wenn C keine öffentliche Barrierefreiheit hat, tritt ein Kompilierzeitfehler auf.
Für jede named_argument Arg in N :
Dies ist Name der Bezeichner der named_argument Arg .
Name für muss ein nicht statisches öffentliches Feld oder eine nicht statische Lese-/Schreib-
Eigenschaft identifiziert werden T . Wenn T kein solches Feld oder keine solche Eigenschaft
aufweist, tritt ein Kompilierzeitfehler auf.
Behalten Sie die folgenden Informationen zur Lauf Zeit Instanziierung des-Attributs bei: der Attribut Klasse
T , dem Instanzkonstruktor C für T , dem positional_argument_list P und dem named_argument_list N
.
Lauf Zeit Abruf einer Attribut Instanz
Die Kompilierung eines Attributs ergibt eine Attribut Klasse T , einen Instanzkonstruktor C für T , eine
positional_argument_list P und eine named_argument_list N . Diese Informationen können mithilfe der
folgenden Schritte zur Laufzeit abgerufen werden:
Befolgen Sie die Schritte zur Lauf Zeit Verarbeitung zum Ausführen einer object_creation_expression des
Formulars new T(P) mit dem Instanzkonstruktor, der C zur Kompilierzeit bestimmt wird. Diese Schritte
führen entweder zu einer Ausnahme oder eine Instanz O von T .
Für jede named_argument Arg in N in der angegebenen Reihenfolge:
Dies ist Name der Bezeichner der named_argument Arg . Wenn kein Name nicht statisches
öffentliches Lese-/Schreibfeld oder Eigenschaft für identifiziert O , wird eine Ausnahme ausgelöst.
Value Das Ergebnis der Auswertung der attribute_argument_expression von Arg .
Wenn Name ein Feld in angibt O , legen Sie dieses Feld auf fest Value .
Andernfalls Name identifiziert eine Eigenschaft für O . Legen Sie diese Eigenschaft auf Value fest.
Das Ergebnis ist O , eine Instanz der Attribut Klasse T , die mit dem positional_argument_list P und
dem named_argument_list initialisiert wurde N .

Reserved attributes (Reservierte Attribute)


Eine kleine Anzahl von Attributen wirkt sich auf irgendeine Weise auf die Sprache aus. Zu diesen Attributen
gehören folgende:
System.AttributeUsageAttribute (Das AttributeUsage-Attribut), das verwendet wird, um die Methoden zu
beschreiben, mit denen eine Attribut Klasse verwendet werden kann.
System.Diagnostics.ConditionalAttribute (Das Conditional-Attribut), das verwendet wird, um bedingte
Methoden zu definieren.
System.ObsoleteAttribute (Das Obsolete-Attribut), das verwendet wird, um einen Member als veraltet zu
markieren.
System.Runtime.CompilerServices.CallerLineNumberAttribute ,
System.Runtime.CompilerServices.CallerFilePathAttribute und
System.Runtime.CompilerServices.CallerMemberNameAttribute (aufruferinformationsattribute), die zum
Bereitstellen von Informationen über den aufrufenden Kontext von optionalen Parametern verwendet
werden.
AttributeUsage -Attribut
Das-Attribut AttributeUsage wird verwendet, um die Art und Weise zu beschreiben, in der die Attribut Klasse
verwendet werden kann.
Eine Klasse, die mit dem- AttributeUsage Attribut ergänzt wird System.Attribute , muss direkt oder indirekt
von abgeleitet werden. Andernfalls tritt ein Kompilierungsfehler auf.
namespace System
{
[AttributeUsage(AttributeTargets.Class)]
public class AttributeUsageAttribute: Attribute
{
public AttributeUsageAttribute(AttributeTargets validOn) {...}
public virtual bool AllowMultiple { get {...} set {...} }
public virtual bool Inherited { get {...} set {...} }
public virtual AttributeTargets ValidOn { get {...} }
}

public enum AttributeTargets


{
Assembly = 0x0001,
Module = 0x0002,
Class = 0x0004,
Struct = 0x0008,
Enum = 0x0010,
Constructor = 0x0020,
Method = 0x0040,
Property = 0x0080,
Field = 0x0100,
Event = 0x0200,
Interface = 0x0400,
Parameter = 0x0800,
Delegate = 0x1000,
ReturnValue = 0x2000,

All = Assembly | Module | Class | Struct | Enum | Constructor |


Method | Property | Field | Event | Interface | Parameter |
Delegate | ReturnValue
}
}

Das Conditional-Attribut
Das-Attribut Conditional ermöglicht die Definition von * Conditional Methods _ und _ Conditional Attribute
Classes *.

namespace System.Diagnostics
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public class ConditionalAttribute: Attribute
{
public ConditionalAttribute(string conditionString) {...}
public string ConditionString { get {...} }
}
}

Bedingte Methoden
Eine Methode, die mit dem- Conditional Attribut ergänzt wird, ist eine bedingte Methode. Das Conditional
Attribut gibt eine Bedingung an, indem ein Symbol für die bedingte Kompilierung getestet wird. Aufrufe einer
bedingten Methode werden entweder eingeschlossen oder ausgelassen, je nachdem, ob dieses Symbol an der
Stelle des Aufrufs definiert ist. Wenn das Symbol definiert ist, ist der-Befehl enthalten. Andernfalls wird der-
Befehl (einschließlich der Auswertung des Empfängers und der Parameter des Aufrufes) ausgelassen.
Eine bedingte Methode unterliegt den folgenden Einschränkungen:
Die bedingte Methode muss eine Methode in einer class_declaration oder struct_declaration sein. Ein
Kompilierzeitfehler tritt auf, wenn das- Conditional Attribut für eine Methode in einer Schnittstellen
Deklaration angegeben wird.
Die bedingte Methode muss den Rückgabetyp aufweisen void .
Die bedingte Methode darf nicht mit dem- override Modifizierer markiert werden. Eine bedingte Methode
kann jedoch mit dem- virtual Modifizierer markiert werden. Über schreibungen einer solchen Methode
sind implizit bedingt und dürfen nicht explizit mit einem-Attribut gekennzeichnet werden Conditional .
Die bedingte Methode darf keine Implementierung einer Schnittstellen Methode sein. Andernfalls tritt ein
Kompilierungsfehler auf.
Außerdem tritt ein Kompilierzeitfehler auf, wenn eine bedingte Methode in einer delegate_creation_expression
verwendet wird. Das Beispiel

#define DEBUG

using System;
using System.Diagnostics;

class Class1
{
[Conditional("DEBUG")]
public static void M() {
Console.WriteLine("Executed Class1.M");
}
}

class Class2
{
public static void Test() {
Class1.M();
}
}

deklariert Class1.M als bedingte Methode. Class2 die- Test Methode ruft diese Methode auf. Da das bedingte
Kompilierungs Symbol DEBUG definiert ist, Class2.Test wird beim Aufruf von aufgerufen M . Wenn das
Symbol DEBUG nicht definiert wurde, wird Class2.Test nicht aufgerufen Class1.M .
Beachten Sie unbedingt, dass der Einschluss oder Ausschluss eines Aufrufes einer Bedingungs Methode durch
die Symbole für die bedingte Kompilierung zum Zeitpunkt des Aufrufes gesteuert wird. Im Beispiel
Datei class1.cs :

using System.Diagnostics;

class Class1
{
[Conditional("DEBUG")]
public static void F() {
Console.WriteLine("Executed Class1.F");
}
}

Datei class2.cs :

#define DEBUG

class Class2
{
public static void G() {
Class1.F(); // F is called
}
}
Datei class3.cs :

#undef DEBUG

class Class3
{
public static void H() {
Class1.F(); // F is not called
}
}

die Klassen Class2 und Class3 jeweils enthalten Aufrufe an die bedingte Methode Class1.F , die abhängig
davon bedingt, ob Sie DEBUG definiert ist oder nicht. Da dieses Symbol im Kontext von Class2 , jedoch nicht
definiert ist Class3 , wird der-Befehl F in Class2 eingeschlossen, während der-Befehl F in Class3
weggelassen wird.
Die Verwendung bedingter Methoden in einer Vererbungs Kette kann verwirrend sein. Aufrufe, die an eine
bedingte Methode über base , der Form base.M , vorgenommen werden, unterliegen den normalen Aufruf
Regeln für die bedingte Methode. Im Beispiel
Datei class1.cs :

using System;
using System.Diagnostics;

class Class1
{
[Conditional("DEBUG")]
public virtual void M() {
Console.WriteLine("Class1.M executed");
}
}

Datei class2.cs :

using System;

class Class2: Class1


{
public override void M() {
Console.WriteLine("Class2.M executed");
base.M(); // base.M is not called!
}
}

Datei class3.cs :

#define DEBUG

using System;

class Class3
{
public static void Test() {
Class2 c = new Class2();
c.M(); // M is called
}
}
Class2 schließt einen aufzurufenden an, der M in seiner Basisklasse definiert ist. Dieser Befehl wird
ausgelassen, da die Basis Methode abhängig vom vorhanden sein des Symbols DEBUG , das nicht definiert ist,
bedingt ist. Folglich schreibt die-Methode nur in die Konsole Class2.M executed . Durch die Verwendung
pp_declaration s können solche Probleme beseitigt werden.
Bedingte Attribut Klassen
Eine Attribut Klasse (Attribut Klassen), die mit einem oder mehreren Conditional Attributen versehen ist, ist
eine Klasse des bedingten Attributs . Eine Conditional-Attribut Klasse ist daher den in den Attributen
deklarierten bedingten Kompilierungs Symbolen zugeordnet Conditional . In diesem Beispiel werden folgende
Schritte ausgeführt:

using System;
using System.Diagnostics;
[Conditional("ALPHA")]
[Conditional("BETA")]
public class TestAttribute : Attribute {}

deklariert TestAttribute als bedingte Attribut Klasse, die den Symbolen für bedingte Kompilierungen ALPHA
und zugeordnet ist BETA .
Attribut Spezifikationen (Attribut Spezifikation) eines bedingten Attributs sind enthalten, wenn mindestens eines
der zugeordneten bedingten Kompilierungs Symbole zum Zeitpunkt der Spezifikation definiert ist. andernfalls
wird die Attribut Spezifikation ausgelassen.
Beachten Sie, dass die Einbindung oder der Ausschluss einer Attribut Spezifikation einer bedingten Attribut
Klasse durch die Symbole für die bedingte Kompilierung an der Stelle der Spezifikation gesteuert wird. Im
Beispiel
Datei test.cs :

using System;
using System.Diagnostics;

[Conditional("DEBUG")]

public class TestAttribute : Attribute {}

Datei class1.cs :

#define DEBUG

[Test] // TestAttribute is specified

class Class1 {}

Datei class2.cs :

#undef DEBUG

[Test] // TestAttribute is not specified

class Class2 {}

die Klassen Class1 und Class2 werden jeweils mit dem-Attribut ergänzt Test , das bedingt darauf basiert, ob
DEBUG definiert ist oder nicht. Da dieses Symbol im Kontext von Class1 , jedoch nicht definiert ist Class2 ,
wird die Spezifikation des- Test Attributs für Class1 eingeschlossen, während die Spezifikation des- Test
Attributs für Class2 ausgelassen wird.
Das Obsolete -Attribut
Das Obsolete -Attribut wird verwendet, um Typen und Member von Typen zu markieren, die nicht mehr
verwendet werden sollen.

namespace System
{
[AttributeUsage(
AttributeTargets.Class |
AttributeTargets.Struct |
AttributeTargets.Enum |
AttributeTargets.Interface |
AttributeTargets.Delegate |
AttributeTargets.Method |
AttributeTargets.Constructor |
AttributeTargets.Property |
AttributeTargets.Field |
AttributeTargets.Event,
Inherited = false)
]
public class ObsoleteAttribute: Attribute
{
public ObsoleteAttribute() {...}
public ObsoleteAttribute(string message) {...}
public ObsoleteAttribute(string message, bool error) {...}
public string Message { get {...} }
public bool IsError { get {...} }
}
}

Wenn ein Programm einen Typ oder Member verwendet, der mit dem- Obsolete Attribut ergänzt wird, gibt der
Compiler eine Warnung oder einen Fehler aus. Insbesondere gibt der Compiler eine Warnung aus, wenn kein
Fehler Parameter bereitgestellt wird, oder wenn der Error-Parameter bereitgestellt wird und den Wert aufweist
false . Der Compiler gibt einen Fehler aus, wenn der Error-Parameter angegeben wird und den Wert aufweist
true .

Im Beispiel

[Obsolete("This class is obsolete; use class B instead")]


class A
{
public void F() {}
}

class B
{
public void F() {}
}

class Test
{
static void Main() {
A a = new A(); // Warning
a.F();
}
}

die-Klasse A wird mit dem- Obsolete Attribut ergänzt. Jede Verwendung von A in Main führt zu einer
Warnung, die die angegebene Meldung "Diese Klasse ist veraltet;" enthält. Verwenden Sie stattdessen Klasse B. "
Aufruferinformationsattribute
Für Zwecke wie z. b. Protokollierung und Berichterstellung ist es manchmal hilfreich, wenn ein
Funktionsmember bestimmte Kompilierzeit Informationen über den aufrufenden Code erhält. Die Attribute für
Aufruferinformationen bieten eine Möglichkeit, solche Informationen transparent zu übergeben.
Wenn ein optionaler Parameter mit einem der aufruferinformationsattribute kommentiert wird, führt das
Weglassen des entsprechenden Arguments in einem Aufruf nicht zwangsläufig dazu, dass der
Standardparameter Wert ersetzt wird. Wenn stattdessen die angegebenen Informationen über den aufrufenden
Kontext verfügbar sind, werden diese Informationen als Argument Wert übermittelt.
Beispiel:

using System.Runtime.CompilerServices

...

public void Log(


[CallerLineNumber] int line = -1,
[CallerFilePath] string path = null,
[CallerMemberName] string name = null
)
{
Console.WriteLine((line < 0) ? "No line" : "Line "+ line);
Console.WriteLine((path == null) ? "No file path" : path);
Console.WriteLine((name == null) ? "No member name" : name);
}

Ein- Log() Ausdruck ohne Argumente gibt die Zeilennummer und den Dateipfad des Aufrufes sowie den
Namen des Members aus, in dem der-Befehl aufgetreten ist.
Aufrufer-Informations Attribute können bei optionalen Parametern überall auftreten, einschließlich in
Delegatdeklarationen. Allerdings gelten für die speziellen aufruferinformationsattribute Einschränkungen
hinsichtlich der Typen der Parameter, die Sie Attributen können, damit immer eine implizite Konvertierung von
einem Ersatzwert in den Parametertyp erfolgt.
Es ist ein Fehler, das gleiche aufruferinformationsinformationen für einen Parameter des definierenden und
implementierenden Teils einer partiellen Methoden Deklaration zu haben. Nur Aufrufer-Informations Attribute
im definierenden Teil werden angewendet, wohingegen Aufrufer-Informations Attribute, die nur im
implementierenden Teil auftreten, ignoriert werden.
Aufruferinformationen beeinflussen nicht die Überladungs Auflösung. Da die attributierten optionalen
Parameter immer noch im Quellcode des Aufrufers weggelassen werden, ignoriert die Überladungs Auflösung
diese Parameter auf dieselbe Weise, wie andere ausgelassene optionale Parameter (Überladungs Auflösung)
ignoriert werden.
Aufruferinformationen werden nur ersetzt, wenn eine Funktion explizit im Quellcode aufgerufen wird. Implizite
Aufrufe, wie z. b. implizite Aufrufe von übergeordneten Konstruktoren, verfügen über keinen Quell Speicherort
und ersetzen keine Aufruferinformationen. Außerdem werden Aufrufe, die dynamisch gebunden sind, keine
Aufruferinformationen ersetzen. Wenn in solchen Fällen ein attributierter aufruferinfo-Parameter weggelassen
wird, wird stattdessen der angegebene Standardwert des Parameters verwendet.
Eine Ausnahme sind Abfrage Ausdrücke. Diese werden als syntaktische Erweiterungen betrachtet, und wenn die
Aufrufe zum weglassen optionaler Parameter mit Aufrufer-Informations Attributen erweitert werden, werden
Aufruferinformationen ersetzt. Der verwendete Speicherort ist der Speicherort der Abfrage Klausel, aus der der-
Befehl generiert wurde.
Wenn für einen bestimmten Parameter mehr als ein aufruferinformationsattribut angegeben wird, werden Sie in
der folgenden Reihenfolge bevorzugt: CallerLineNumber , CallerFilePath , CallerMemberName .
Das callerlinenumber-Attribut
Der System.Runtime.CompilerServices.CallerLineNumberAttribute ist für optionale Parameter zulässig, wenn eine
standardmäßige implizite Konvertierung (standardmäßige implizite Konvertierungen) vom Konstanten Wert
int.MaxValue zum Typ des Parameters vorhanden ist. Dadurch wird sichergestellt, dass jede nicht negative
Zeilennummer bis zu diesem Wert fehlerfrei gegeben werden kann.
Wenn ein Funktionsaufruf von einem Speicherort im Quellcode einen optionalen Parameter mit dem auslässt
CallerLineNumberAttribute , wird ein numerisches wahrsten, das die Zeilennummer dieses Speicher Orts
darstellt, anstelle des Standardparameter Werts als Argument für den Aufruf verwendet.
Wenn der Aufruf mehrere Zeilen umfasst, ist die ausgewählte Zeile implementierungsabhängig.
Beachten Sie, dass die Zeilennummer von #line Direktiven (Zeilen Anweisungen) betroffen sein kann.
Das callerfilepath-Attribut
Der System.Runtime.CompilerServices.CallerFilePathAttribute ist für optionale Parameter zulässig, wenn eine
standardmäßige implizite Konvertierung (standardmäßige implizite Konvertierungen) von string in den Typ
des Parameters vorhanden ist.
Wenn ein Funktionsaufruf von einem Speicherort im Quellcode einen optionalen Parameter mit dem
CallerFilePathAttribute -Wert auslässt, wird ein Zeichenfolgenliteral, das den Dateipfad des Speicher Orts
darstellt, anstelle des Standardparameter Werts als Argument für den Aufruf verwendet.
Das Format des Dateipfads ist implementierungsabhängig.
Beachten Sie, dass der Dateipfad von #line Direktiven (Zeilen Anweisungen) betroffen sein kann.
Das callermembership Name-Attribut
Der System.Runtime.CompilerServices.CallerMemberNameAttribute ist für optionale Parameter zulässig, wenn eine
standardmäßige implizite Konvertierung (standardmäßige implizite Konvertierungen) von string in den Typ
des Parameters vorhanden ist.
Wenn ein Funktionsaufruf von einer Position innerhalb des Texts eines Funktionsmembers oder innerhalb eines
Attributs, das auf den Funktions Member selbst oder seinen Rückgabetyp angewendet wird, Parameter oder
Typparameter im Quellcode einen optionalen Parameter mit dem-Wert auslässt CallerMemberNameAttribute ,
wird ein Zeichenfolgenliteral, das den Namen dieses Members darstellt, anstelle des Standardparameter Werts
als Argument für den Aufruf verwendet.
Bei aufrufen, die innerhalb von generischen Methoden auftreten, wird nur der Methodenname selbst ohne die
Typparameter Liste verwendet.
Bei aufrufen, die in expliziten Schnittstellenmember-Implementierungen auftreten, wird nur der Methodenname
selbst verwendet, ohne die vorherige Schnittstellen Qualifikation.
Bei aufrufen, die innerhalb von Eigenschafts-oder Ereignisaccessoren auftreten, entspricht der verwendete
Elementname der Eigenschaft oder dem Ereignis selbst.
Bei aufrufen, die innerhalb von Indexeraccessoren auftreten, wird der verwendete Elementname von einem
IndexerNameAttribute (das IndexerName-Attribut) für das Indexer-Element, sofern vorhanden, oder durch den
Standardnamen angegeben Item .
Bei aufrufen, die in Deklarationen von Instanzkonstruktoren, statischen Konstruktoren, Dekonstruktoren und
Operatoren auftreten, ist der verwendete Elementname implementierungsabhängig.

Attributes for Interoperation (Attribute zur Interoperation)


Hinweis: Dieser Abschnitt gilt nur für die Microsoft .NET Implementierung von c#.
Interoperabilität mit com-und Win32-Komponenten
Die .NET-Laufzeit bietet eine große Anzahl von Attributen, mit denen c#-Programme mit Komponenten
interagieren können, die mithilfe von com-und Win32-DLLs geschrieben wurden. Beispielsweise kann das-
DllImport Attribut für eine-Methode verwendet werden, static extern um anzugeben, dass die
Implementierung der-Methode in einer Win32-DLL gefunden werden soll. Diese Attribute finden Sie im
System.Runtime.InteropServices -Namespace. eine ausführliche Dokumentation für diese Attribute finden Sie in
der .net-Lauf zeitdokumentation.
Interoperabilität mit anderen .NET -Sprachen
Das IndexerName-Attribut
Indexer werden in .NET mithilfe von indizierten Eigenschaften implementiert und haben einen Namen in den
.NET-Metadaten. Wenn IndexerName für einen Indexer kein Attribut vorhanden ist, Item wird standardmäßig
der Name verwendet. Mithilfe des IndexerName -Attributs kann ein Entwickler diesen Standardwert
überschreiben und einen anderen Namen angeben.

namespace System.Runtime.CompilerServices.CSharp
{
[AttributeUsage(AttributeTargets.Property)]
public class IndexerNameAttribute: Attribute
{
public IndexerNameAttribute(string indexerName) {...}
public string Value { get {...} }
}
}
Unsicherer Code
04.11.2021 • 69 minutes to read

Die in den vorstehenden Kapiteln definierte c#-Kernsprache unterscheidet sich insbesondere von C und C++,
wenn Zeiger als Datentyp ausgelassen werden. Stattdessen bietet c# Verweise und die Möglichkeit, Objekte zu
erstellen, die von einem Garbage Collector verwaltet werden. Dieser Entwurf ist mit anderen Features gekoppelt
und macht c# zu einer viel sichereren Sprache als C oder C++. In der c#-Kernsprache ist es einfach nicht
möglich, eine nicht initialisierte Variable, einen "Verb leibend"-Zeiger oder einen Ausdruck zu verwenden, der
ein Array über seine Begrenzungen hinaus indiziert. Die gesamten Kategorien von Fehlern, die routinemäßig C-
und C++-Programme Plagen, werden dadurch eliminiert.
Obwohl praktisch jedes Zeigertyp Konstrukt in C oder C++ einen Verweistyp Gegenstück in c# hat, gibt es
jedoch Situationen, in denen der Zugriff auf Zeiger Typen zu einer Notwendigkeit wird. Beispielsweise ist die
Schnittstellen mit dem zugrunde liegenden Betriebssystem, der Zugriff auf ein Speicher Abbild oder die
Implementierung eines zeitkritischen Algorithmus möglicherweise ohne Zugriff auf Zeiger nicht möglich oder
praktikabel. Um diesem Bedarf gerecht zu werden, bietet c# die Möglichkeit, unsicheren Code zu schreiben.
Im unsicheren Code ist es möglich, Zeiger zu deklarieren und zu verarbeiten, Konvertierungen zwischen Zeigern
und ganzzahligen Typen auszuführen, die Adresse von Variablen zu übernehmen usw. In gewisser Weise ist das
Schreiben von unsicherem Code ähnlich wie das Schreiben von C-Code in einem c#-Programm.
Unsicherer Code ist tatsächlich eine "sichere" Funktion aus der Perspektive von Entwicklern und Benutzern.
Unsicherer Code muss eindeutig mit dem-Modifizierer markiert werden unsafe , sodass Entwickler nicht
versehentlich unsichere Features verwenden können, und die Ausführungs-Engine kann sicherstellen, dass
unsicherer Code nicht in einer nicht vertrauenswürdigen Umgebung ausgeführt werden kann.

Unsafe contexts (Unsichere Kontexte)


Die unsicheren Features von c# sind nur in unsicheren Kontexten verfügbar. Ein unsicherer Kontext wird durch
das Einschließen eines unsafe Modifizierers in die Deklaration eines Typs oder Members oder durch die
Verwendung eines unsafe_statement eingeführt:
Eine Deklaration einer Klasse, Struktur, Schnittstelle oder eines Delegaten kann einen unsafe Modifizierer
enthalten. in diesem Fall wird der gesamte Text Block dieser Typdeklaration (einschließlich des Texts der
Klasse, Struktur oder Schnittstelle) als unsicherer Kontext betrachtet.
Eine Deklaration eines Felds, einer Methode, einer Eigenschaft, eines Ereignisses, eines Indexers, eines
Operators, eines Instanzkonstruktors, eines Dekonstruktors oder eines statischen Konstruktors kann einen
unsafe Modifizierer enthalten. in diesem Fall wird der gesamte Text Block der Element Deklaration als
unsicherer Kontext betrachtet.
Ein- unsafe_statement ermöglicht die Verwendung eines unsicheren Kontexts innerhalb eines- Blocks. Der
gesamte Text Block des zugeordneten Blocks wird als unsicherer Kontext betrachtet.
Die zugehörigen Grammatik Produktionen sind unten dargestellt.
class_modifier_unsafe
: 'unsafe'
;

struct_modifier_unsafe
: 'unsafe'
;

interface_modifier_unsafe
: 'unsafe'
;

delegate_modifier_unsafe
: 'unsafe'
;

field_modifier_unsafe
: 'unsafe'
;

method_modifier_unsafe
: 'unsafe'
;

property_modifier_unsafe
: 'unsafe'
;

event_modifier_unsafe
: 'unsafe'
;

indexer_modifier_unsafe
: 'unsafe'
;

operator_modifier_unsafe
: 'unsafe'
;

constructor_modifier_unsafe
: 'unsafe'
;

destructor_declaration_unsafe
: attributes? 'extern'? 'unsafe'? '~' identifier '(' ')' destructor_body
| attributes? 'unsafe'? 'extern'? '~' identifier '(' ')' destructor_body
;

static_constructor_modifiers_unsafe
: 'extern'? 'unsafe'? 'static'
| 'unsafe'? 'extern'? 'static'
| 'extern'? 'static' 'unsafe'?
| 'unsafe'? 'static' 'extern'?
| 'static' 'extern'? 'unsafe'?
| 'static' 'unsafe'? 'extern'?
;

embedded_statement_unsafe
: unsafe_statement
| fixed_statement
;

unsafe_statement
: 'unsafe' block
;
Im Beispiel

public unsafe struct Node


{
public int Value;
public Node* Left;
public Node* Right;
}

der unsafe in der Struktur Deklaration angegebene Modifizierer bewirkt, dass der gesamte Text Block der
Struktur Deklaration zu einem unsicheren Kontext wird. Daher ist es möglich, die Left Felder und als Right
Zeigertyp zu deklarieren. Das obige Beispiel könnte auch geschrieben werden.

public struct Node


{
public int Value;
public unsafe Node* Left;
public unsafe Node* Right;
}

Hier bewirken die unsafe Modifizierer in den Feld Deklarationen, dass diese Deklarationen als unsichere
Kontexte angesehen werden.
Abgesehen von der Einrichtung eines unsicheren Kontexts, der die Verwendung von Zeiger Typen ermöglicht,
hat der- unsafe Modifizierer keine Auswirkung auf einen Typ oder einen Member. Im Beispiel

public class A
{
public unsafe virtual void F() {
char* p;
...
}
}

public class B: A
{
public override void F() {
base.F();
...
}
}

der- unsafe Modifizierer für die- F Methode in A bewirkt lediglich, dass der Text Wert von F zu einem
unsicheren Kontext wird, in dem die unsicheren Funktionen der-Sprache verwendet werden können. Bei der
außer Kraft Setzung von F in B ist es nicht notwendig, den unsafe Modifizierer erneut anzugeben, es sei
denn, die F Methode in B selbst benötigt Zugriff auf unsichere Features.
Die Situation ist etwas anders, wenn ein Zeigertyp Teil der Methoden Signatur ist.

public unsafe class A


{
public virtual void F(char* p) {...}
}

public class B: A
{
public unsafe override void F(char* p) {...}
}
Da F die Signatur einen Zeigertyp enthält, kann Sie nur in einem unsicheren Kontext geschrieben werden. Der
unsichere Kontext kann jedoch eingeführt werden, indem entweder die gesamte Klasse unsicher gemacht wird,
wie es in der Fall ist A , oder indem ein unsafe Modifizierer in die Methoden Deklaration eingeschlossen wird,
wie es in der Fall ist B .

Zeigertypen
In einem unsicheren Kontext kann ein Typ (types) ein pointer_type sowie ein value_type oder ein reference_type
sein. Eine pointer_type kann jedoch auch in einem typeof Ausdruck (anonymen Objekt Erstellungs Ausdrücken)
außerhalb eines unsicheren Kontexts verwendet werden, da diese Verwendung nicht unsicher ist.

type_unsafe
: pointer_type
;

Ein pointer_type wird als unmanaged_type oder als Schlüsselwort void , gefolgt von einem Token, geschrieben
* :

pointer_type
: unmanaged_type '*'
| 'void' '*'
;

unmanaged_type
: type
;

Der Typ, der vor * in einem Zeigertyp angegeben wird, wird als Ver weistyp des Zeiger Typs bezeichnet. Sie
stellt den Typ der Variablen dar, auf die ein Wert des Zeiger Typs zeigt.
Anders als Verweise (Werte von Verweis Typen) werden Zeiger nicht vom Garbage Collector nachverfolgt, und
der Garbage Collector hat keine Informationen über Zeiger und die Daten, auf die Sie zeigen. Aus diesem Grund
kann ein Zeiger nicht auf einen Verweis oder eine Struktur verweisen, die Verweise enthält, und der Verweistyp
eines Zeigers muss ein unmanaged_type sein.
Ein unmanaged_type ist ein beliebiger Typ, der kein reference_type oder konstruierter Typ ist und keine
reference_type oder konstruierten typanfelder auf einer beliebigen Schachtelungs Ebene enthält. Mit anderen
Worten: ein unmanaged_type ist einer der folgenden:
sbyte , byte , short , ushort , int , uint , long , ulong , char , float , double , decimal oder bool
.
Alle enum_type.
Alle pointer_type.
Alle benutzerdefinierten struct_type , die kein konstruierter Typ sind und nur Felder unmanaged_type s
enthalten.
Die intuitive Regel zum Mischen von Zeigern und verweisen ist, dass Verweise von verweisen (Objekten) Zeiger
enthalten dürfen, aber Verweise von Zeigern dürfen keine Verweise enthalten.
In der folgenden Tabelle sind einige Beispiele für Zeiger Typen angegeben:

B EISP IEL B ESC H REIB UN G

byte* Zeiger auf byte


B EISP IEL B ESC H REIB UN G

char* Zeiger auf char

int** Zeiger auf Zeiger auf int

int*[] Eindimensionales Array von Zeigern auf int

void* Zeiger auf unbekannten Typ

Für eine bestimmte Implementierung müssen alle Zeiger Typen die gleiche Größe und Darstellung aufweisen.
Im Gegensatz zu C und C++, wenn mehrere Zeiger in der gleichen Deklaration deklariert werden, wird in c# *
zusammen mit dem zugrunde liegenden Typ und nicht als Präfix Satzzeichen für jeden Zeiger Namen
geschrieben. Beispiel:

int* pi, pj; // NOT as int *pi, *pj;

Der Wert eines Zeigers, der den Typ aufweist, T* stellt die Adresse einer Variablen vom Typ dar T . Der Zeiger
Dereferenzierungsoperator * (Zeigerdereferenzierung) kann verwendet werden, um auf diese Variable
zuzugreifen. Wenn z. b. eine Variable P vom Typ int* ist, *P gibt der Ausdruck die Variable an, die int bei
der in enthaltenen Adresse gefunden wurde P .
Ein Zeiger kann wie ein Objekt Verweis sein null . Das Anwenden des Dereferenzierungsoperators auf einen
Zeiger führt zu einem von null der Implementierung definierten Verhalten. Ein Zeiger mit einem Wert null
wird durch "All-Bits-Zero" dargestellt.
Der- void* Typ stellt einen Zeiger auf einen unbekannten Typ dar. Da der Verweistyp unbekannt ist, kann der
Dereferenzierungsoperator nicht auf einen Zeiger vom Typ angewendet werden void* , und es können keine
Arithmetik für einen solchen Zeiger ausgeführt werden. Ein Zeiger vom Typ kann jedoch void* in einen
anderen Zeigertyp (und umgekehrt) umgewandelt werden.
Zeiger Typen sind eine separate Kategorie von Typen. Anders als bei Verweis Typen und Werttypen erben Zeiger
Typen nicht von, object und zwischen Zeiger Typen und sind keine Konvertierungen vorhanden object . Vor
allem werden Boxing und Unboxing (Boxing und Unboxing) für Zeiger nicht unterstützt. Allerdings sind
Konvertierungen zwischen verschiedenen Zeiger Typen sowie zwischen Zeiger Typen und ganzzahligen Typen
zulässig. Dies wird in Zeiger Konvertierungenbeschrieben.
Eine pointer_type kann nicht als Typargument (konstruierte Typen) verwendet werden, und der Typrückschluss
(Typrückschluss) schlägt bei generischen Methoden aufrufen fehl, die ein Typargument als Zeigertyp abgeleitet
hätten.
Eine pointer_type kann als Typ eines flüchtigen Felds (flüchtige Felder) verwendet werden.
Obwohl Zeiger als-Parameter oder-Parameter weitergegeben werden können ref out , kann dies zu
undefiniertem Verhalten führen, da der Zeiger möglicherweise so festgelegt ist, dass er auf eine lokale Variable
verweist, die nicht mehr vorhanden ist, wenn die aufgerufene Methode zurückgibt, oder das Fixed-Objekt, auf
das Sie verweist, nicht mehr korrigiert ist Beispiel:
using System;

class Test
{
static int value = 20;

unsafe static void F(out int* pi1, ref int* pi2) {


int i = 10;
pi1 = &i;

fixed (int* pj = &value) {


// ...
pi2 = pj;
}
}

static void Main() {


int i = 10;
unsafe {
int* px1;
int* px2 = &i;

F(out px1, ref px2);

Console.WriteLine("*px1 = {0}, *px2 = {1}",


*px1, *px2); // undefined behavior
}
}
}

Eine Methode kann einen Wert eines Typs zurückgeben, und dieser Typ kann ein Zeiger sein. Wenn
beispielsweise ein Zeiger auf eine zusammenhängende Sequenz von int s, die Element Anzahl dieser Sequenz
und ein anderer int Wert angegeben wird, gibt die folgende Methode die Adresse dieses Werts in dieser
Sequenz zurück, wenn eine Entsprechung auftritt; andernfalls wird Folgendes zurückgegeben null :

unsafe static int* Find(int* pi, int size, int value) {


for (int i = 0; i < size; ++i) {
if (*pi == value)
return pi;
++pi;
}
return null;
}

In einem unsicheren Kontext sind mehrere-Konstrukte zum Ausführen von Zeigern verfügbar:
Der * Operator kann verwendet werden, um Zeiger Dereferenzierung (Zeiger Dereferenzierung)
auszuführen.
Der -> Operator kann verwendet werden, um auf einen Member einer Struktur über einen Zeiger (Zeiger
Element Zugriff) zuzugreifen.
Der [] Operator kann zum Indizieren eines Zeigers (Zeiger Element Zugriff) verwendet werden.
Der & Operator kann verwendet werden, um die Adresse einer Variablen (dem address-of-Operator)
abzurufen.
Die ++ -- Operatoren und können verwendet werden, um Zeiger zu erhöhen und zu dekrementiert
(Zeiger Inkrement und Dekrement).
Die + - Operatoren und können verwendet werden, um Zeigerarithmetik (Zeigerarithmetik) auszuführen.
Die == != < Operatoren,,,, > <= und >= können verwendet werden, um Zeiger zu vergleichen (Zeiger
Vergleiche).
Der- stackalloc Operator kann verwendet werden, um Speicher aus der-aufrufsstapel zuzuordnen (Puffer
fester Größe).
Die- fixed Anweisung kann verwendet werden, um eine Variable temporär zu korrigieren, sodass Ihre
Adresse abgerufen werden kann (die fixed-Anweisung).

Fixed and moveable variables (Feste und verschiebbare Variablen)


Der Address-of-Operator (der Address-of-Operator) und die- fixed Anweisung (die fixed-Anweisung)
dividieren Variablen in zwei Kategorien: *Fixed variables _ und _ verschiebbare Variablen *.
Fixierte Variablen befinden sich in Speicherorten, die von der Garbage Collector nicht betroffen sind. (Beispiele
für fixierte Variablen sind lokale Variablen, Wert Parameter und Variablen, die durch dereferenzierende Zeiger
erstellt werden.) Auf der anderen Seite befinden sich verschiebbare Variablen in Speicherorten, die von der
Garbage Collector verlagert oder verfügbar gemacht werden. (Beispiele für verschiebbare Variablen sind Felder
in Objekten und Elementen von Arrays.)
Der- & Operator (der Address-of-Operator) gestattet, dass die Adresse einer Variablen mit fester Größe ohne
Einschränkungen abgerufen wird. Da eine verschiebbare Variable jedoch vom Garbage Collector verschoben
oder aufgehoben werden kann, kann die Adresse einer verschiebbaren Variablen nur mit einer- fixed
Anweisung (fixed-Anweisung) abgerufen werden, und diese Adresse bleibt nur für die Dauer dieser fixed
Anweisung gültig.
Eine Variable mit fester Genauigkeit ist eine der folgenden:
Eine Variable, die sich aus einer Simple_name (einfache Namen) ergibt, die auf eine lokale Variable oder
einen value-Parameter verweist, es sei denn, die Variable wird von einer anonymen Funktion aufgezeichnet.
Eine Variable, die sich aus einer member_access (Member Access) des Formulars ergibt V.I , wobei V eine
festgelegte Variable eines struct_type ist.
Eine Variable, die sich aus einer pointer_indirection_expression (Zeigerdereferenzierung) des Formulars *P ,
einer pointer_member_access (Zeiger Element Zugriff) des Formulars P->I oder einer
pointer_element_access (Zeiger Element Zugriff) des Formulars ergibt P[E] .
Alle anderen Variablen werden als verschiebbare Variablen klassifiziert.
Beachten Sie, dass ein statisches Feld als eine verschiebbare Variable klassifiziert ist. Beachten Sie außerdem,
ref dass out der-Parameter oder der-Parameter als eine verschiebbare Variable klassifiziert ist, auch wenn
das für den-Parameter angegebene Argument eine Fixed-Variable ist. Beachten Sie schließlich, dass eine
Variable, die durch dereferenzieren eines Zeigers erzeugt wird, immer als eine festgelegte Variable klassifiziert
wird.

Zeigerkonvertierungen
In einem unsicheren Kontext wird der Satz der verfügbaren impliziten Konvertierungen (implizite
Konvertierungen) um die folgenden impliziten Zeiger Konvertierungen erweitert:
Von einem beliebigen pointer_type bis zum-Typ void* .
Von der null literalen zu beliebigen pointer_type.

Außerdem wird in einem unsicheren Kontext der Satz der verfügbaren expliziten Konvertierungen (explizite
Konvertierungen) erweitert, um die folgenden expliziten Zeiger Konvertierungen einzubeziehen:
Von allen pointer_type zu anderen pointer_type.
From sbyte , byte , short , ushort , int , uint , long oder ulong zu beliebigen pointer_type.
Von allen pointer_type bis sbyte , byte , short , ushort , int , uint , long oder ulong .
Schließlich umfasst der Satz von impliziten Standard Konvertierungen (standardmäßige implizite
Konvertierungen) in einem unsicheren Kontext die folgende Zeiger Konvertierung:
Von einem beliebigen pointer_type bis zum-Typ void* .

Konvertierungen zwischen zwei Zeiger Typen ändern niemals den tatsächlichen Zeiger Wert. Anders
ausgedrückt: eine Konvertierung von einem Zeigertyp in einen anderen hat keine Auswirkungen auf die
zugrunde liegende Adresse, die durch den Zeiger angegeben wird.
Wenn ein Zeigertyp in einen anderen konvertiert wird, wenn der resultierende Zeiger nicht ordnungsgemäß für
den Verweis auf den Typ ausgerichtet ist, ist das Verhalten nicht definiert, wenn das Ergebnis dereferenziert
wird. Im Allgemeinen ist das Konzept "ordnungsgemäß ausgerichtet" transitiv: Wenn ein Zeiger auf den Typ A
ordnungsgemäß für einen Zeiger auf den Typ ausgerichtet ist B , der wiederum ordnungsgemäß für einen
Zeiger auf den Typ ausgerichtet ist C , wird ein Zeiger auf den Typ A ordnungsgemäß für einen Zeiger auf den
Typ ausgerichtet C .
Beachten Sie den folgenden Fall, in dem auf eine Variable mit einem Typ über einen Zeiger auf einen anderen
Typ zugegriffen wird:

char c = 'A';
char* pc = &c;
void* pv = pc;
int* pi = (int*)pv;
int i = *pi; // undefined
*pi = 123456; // undefined

Wenn ein Zeigertyp in einen Zeiger auf ein Byte konvertiert wird, zeigt das Ergebnis auf das niedrigste
adressierte Byte der Variablen. Aufeinanderfolgende Inkremente des Ergebnisses bis zur Größe der Variablen
ergeben Zeiger auf die restlichen Bytes dieser Variablen. Die folgende Methode zeigt beispielsweise alle acht
Bytes in einem Double-Wert als Hexadezimalwert an:

using System;

class Test
{
unsafe static void Main() {
double d = 123.456e23;
unsafe {
byte* pb = (byte*)&d;
for (int i = 0; i < sizeof(double); ++i)
Console.Write("{0:X2} ", *pb++);
Console.WriteLine();
}
}
}

Die erstellte Ausgabe hängt natürlich von der-Unterscheidung ab.


Zuordnungen zwischen Zeigern und ganzen Zahlen sind Implementierungs definiert. Allerdings Verhalten sich
Konvertierungen von Zeigern auf oder von ganzzahligen Typen auf 32 *-und 64-Bit-CPU-Architekturen mit
einem linearen Adressraum normalerweise genau so, wie Konvertierungen von- uint oder- ulong Werten in
bzw. aus diesen ganzzahligen Typen.
Zeiger Arrays
In einem unsicheren Kontext können Arrays von Zeigern erstellt werden. Nur einige der Konvertierungen, die für
andere Array Typen gelten, sind in Zeiger Arrays zulässig:
Die implizite Verweis Konvertierung (implizite Verweis Konvertierungen) von allen array_type in
System.Array und die implementierten Schnittstellen gelten auch für Zeiger Arrays. Allerdings wird bei
jedem Versuch, auf die Array Elemente über System.Array oder die von ihm implementierten Schnittstellen
zuzugreifen, zur Laufzeit eine Ausnahme ausgelöst, da Zeiger Typen nicht in konvertierbar sind object .
Die impliziten und expliziten Verweis Konvertierungen (implizite VerweisKonvertierungen, explizite Verweis
Konvertierungen) von einem eindimensionalen Arraytyp S[] in System.Collections.Generic.IList<T> und
die zugehörigen generischen Basis Schnittstellen werden nie auf Zeiger Arrays angewendet, da Zeiger Typen
nicht als Typargumente verwendet werden können und keine Konvertierungen von Zeiger Typen zu nicht-
Zeiger Typen vorhanden sind.
Die explizite Verweis Konvertierung (explizite Verweis Konvertierungen) von System.Array und die für
beliebige array_type implementierten Schnittstellen gelten für Zeiger Arrays.
Die expliziten Verweis Konvertierungen (explizite Verweis Konvertierungen) von
System.Collections.Generic.IList<S> und deren Basis Schnittstellen auf einen eindimensionalen Arraytyp
T[] gelten nie für Zeiger Arrays, da Zeiger Typen nicht als Typargumente verwendet werden können und
keine Konvertierungen von Zeiger Typen zu nicht-Zeiger Typen vorhanden sind.
Diese Einschränkungen bedeuten, dass die Erweiterung für die- foreach Anweisung über Arrays, die in der
foreach-Anweisung beschrieben werden, nicht auf Zeiger Arrays angewendet werden kann. Stattdessen wird
eine foreach-Anweisung der Form

foreach (V v in x) embedded_statement

Wenn der Typ von x ein Arraytyp des Formulars ist T[,,...,] , N die Anzahl der Dimensionen minus 1 und
T oder V ein Zeigertyp ist, wird wie folgt mithilfe von schzitierten for-Schleifen erweitert:

{
T[,,...,] a = x;
for (int i0 = a.GetLowerBound(0); i0 <= a.GetUpperBound(0); i0++)
for (int i1 = a.GetLowerBound(1); i1 <= a.GetUpperBound(1); i1++)
...
for (int iN = a.GetLowerBound(N); iN <= a.GetUpperBound(N); iN++) {
V v = (V)a.GetValue(i0,i1,...,iN);
embedded_statement
}
}

Die-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-oder-Variablen a i0 i1 iN sind nicht x embedded_statement sichtbar Die Variable


v ist in der eingebetteten Anweisung schreibgeschützt. Wenn keine explizite Konvertierung (Zeiger
Konvertierungen) von T (dem Elementtyp) in vorhanden ist V , wird ein Fehler erzeugt, und es werden keine
weiteren Schritte ausgeführt. Wenn x den Wert hat null , System.NullReferenceException wird zur Laufzeit
eine ausgelöst.

Pointers in expressions (Zeiger in Ausdrücken)


In einem unsicheren Kontext kann ein Ausdruck das Ergebnis eines Zeiger Typs ergeben, aber außerhalb eines
unsicheren Kontexts ist es ein Kompilierzeitfehler für einen Ausdruck, der einen Zeigertyp aufweisen soll. Genau
gesagt: außerhalb eines unsicheren Kontexts tritt ein Kompilierzeitfehler auf, wenn Simple_name (einfache
Namen), member_access (Member Access), invocation_expression (Aufruf Ausdrücke) oder element_access
(Element Zugriff) einen Zeigertyp haben.
In einem unsicheren Kontext ermöglichen die primary_no_array_creation_expression (Primary Expressions) und
unary_expression (unäre Operatoren) die folgenden zusätzlichen Konstrukte:
primary_no_array_creation_expression_unsafe
: pointer_member_access
| pointer_element_access
| sizeof_expression
;

unary_expression_unsafe
: pointer_indirection_expression
| addressof_expression
;

Diese Konstrukte werden in den folgenden Abschnitten beschrieben. Die-Grammatik ist die Rangfolge und
Assoziativität der unsicheren Operatoren.
Zeigerdereferenzierung
Ein pointer_indirection_expression besteht aus einem Sternchen ( * ), gefolgt von einem unary_expression.

pointer_indirection_expression
: '*' unary_expression
;

Der unäre * Operator bezeichnet die Zeiger Dereferenzierung und wird zum Abrufen der Variablen verwendet,
auf die ein Zeiger zeigt. Das Ergebnis der Auswertung von *P , wobei P ein Ausdruck eines Zeiger Typs ist T*
, ist eine Variable vom Typ T . Es ist ein Kompilierzeitfehler, wenn der unäre * Operator auf einen Ausdruck
vom Typ void* oder auf einen Ausdruck angewendet wird, der kein Zeigertyp ist.
Die Auswirkung der Anwendung des unären * Operators auf einen null Zeiger ist Implementierungs
definiert. Insbesondere gibt es keine Garantie, dass dieser Vorgang eine auslöst System.NullReferenceException .
Wenn dem Zeiger ein ungültiger Wert zugewiesen wurde, ist das Verhalten des unären * Operators nicht
definiert. Unter den ungültigen Werten für die Dereferenzierung eines Zeigers durch den unären * Operator
handelt es sich um eine Adresse, die für den Typ, auf den verwiesen wird (siehe Beispiel in Zeiger
Konvertierungen), und die Adresse einer Variablen nach dem Ende der Lebensdauer nicht ordnungsgemäß
ausgerichtet ist.
Zum Zweck der eindeutigen Zuweisungs Analyse wird eine Variable, die durch die Auswertung eines Ausdrucks
des Formulars erzeugt *P wird, als anfänglich zugewiesen (anfänglich zugewiesene Variablen).
Zeigermemberzugriff
Eine pointer_member_access besteht aus einem primary_expression, gefolgt von einem " -> "-Token, gefolgt
von einem Bezeichner und einem optionalen type_argument_list.

pointer_member_access
: primary_expression '->' identifier
;

In einem Zeiger Element Zugriff auf das Formular P->I P muss ein Ausdruck eines anderen Zeiger Typs als
sein void* , und I muss einen zugänglichen Member des Typs angeben, auf den P verweist.
Ein Zeiger Element Zugriff auf das Formular P->I wird genau wie ausgewertet (*P).I . Eine Beschreibung des
Zeigerdereferenzierungsoperators ( * ) finden Sie unter Zeiger Dereferenzierung. Eine Beschreibung des
Member Access Operators ( . ) finden Sie unter Member Access.
Im Beispiel
using System;

struct Point
{
public int x;
public int y;

public override string ToString() {


return "(" + x + "," + y + ")";
}
}

class Test
{
static void Main() {
Point point;
unsafe {
Point* p = &point;
p->x = 10;
p->y = 20;
Console.WriteLine(p->ToString());
}
}
}

der -> -Operator wird verwendet, um auf Felder zuzugreifen und eine Methode einer Struktur mithilfe eines
Zeigers aufzurufen. Da der Vorgang P->I exakt entspricht (*P).I , Main könnte die Methode gleichermaßen
gut geschrieben worden sein:

class Test
{
static void Main() {
Point point;
unsafe {
Point* p = &point;
(*p).x = 10;
(*p).y = 20;
Console.WriteLine((*p).ToString());
}
}
}

Zeigerelementzugriff
Ein pointer_element_access besteht aus einem primary_no_array_creation_expression gefolgt von einem
Ausdruck, der in " [ " und "" eingeschlossen ist ] .

pointer_element_access
: primary_no_array_creation_expression '[' expression ']'
;

In einem Zeiger Element Zugriff auf das Formular P[E] P muss ein Ausdruck eines anderen Zeiger Typs als
sein void* , und E muss ein Ausdruck sein, der implizit in int ,, oder konvertiert werden uint kann long
ulong .

Ein Zeiger Element Zugriff auf das Formular P[E] wird genau wie ausgewertet *(P + E) . Eine Beschreibung
des Zeigerdereferenzierungsoperators ( * ) finden Sie unter Zeiger Dereferenzierung. Eine Beschreibung des
Zeiger Additions Operators ( + ) finden Sie unter Zeigerarithmetik.
Im Beispiel
class Test
{
static void Main() {
unsafe {
char* p = stackalloc char[256];
for (int i = 0; i < 256; i++) p[i] = (char)i;
}
}
}

Ein Zeiger Element Zugriff wird verwendet, um den Zeichen Puffer in einer Schleife zu initialisieren for . Da
der-Vorgang P[E] exakt entspricht *(P + E) , könnte das Beispiel ebenfalls geschrieben worden sein:

class Test
{
static void Main() {
unsafe {
char* p = stackalloc char[256];
for (int i = 0; i < 256; i++) *(p + i) = (char)i;
}
}
}

Der Zeiger Element-Zugriffs Operator prüft nicht auf Fehler aufgrund von Fehlern, und das Verhalten beim
Zugriff auf ein Out-of-Bounds-Element ist nicht definiert. Dies entspricht C und C++.
Adresse -von-Operator
Ein addressof_expression besteht aus einem kaufmännischen und-( & ), gefolgt von einem unary_expression.

addressof_expression
: '&' unary_expression
;

Bei einem Ausdruck E , der ein-Typ ist T und als Fixed-Variable (Fixed-und verschiebbare Variablen)
klassifiziert ist, berechnet das-Konstrukt &E die Adresse der Variablen, die von angegeben wird E . Der
Ergebnistyp ist, T* und wird als Wert klassifiziert. Ein Kompilierzeitfehler tritt auf E , wenn nicht als Variable
klassifiziert ist, wenn als schreibgeschützte E lokale Variable klassifiziert ist, oder wenn E eine verschiebbare
Variable bezeichnet. Im letzten Fall kann eine fixed-Anweisung (die fixed-Anweisung) verwendet werden, um die
Variable temporär zu korrigieren, bevor Sie Ihre Adresse erhält. Wie in Member Accessangegeben, wird das Feld
außerhalb eines Instanzkonstruktors oder statischen Konstruktors für eine Struktur oder Klasse, die ein
readonly Feld definiert, als Wert, nicht als Variable angesehen. Daher kann die Adresse nicht übernommen
werden. Ebenso kann die Adresse einer Konstante nicht übernommen werden.
Der & Operator verlangt nicht, dass sein Argument definitiv zugewiesen wird. nach einem & Vorgang wird
jedoch die Variable, auf die der Operator angewendet wird, als definitiv im Ausführungs Pfad zugewiesen, in
dem der Vorgang ausgeführt wird. Es liegt in der Verantwortung des Programmierers, sicherzustellen, dass die
richtige Initialisierung der Variablen in dieser Situation tatsächlich stattfindet.
Im Beispiel
using System;

class Test
{
static void Main() {
int i;
unsafe {
int* p = &i;
*p = 123;
}
Console.WriteLine(i);
}
}

i wird nach dem &i Vorgang, der zum Initialisieren von verwendet wird, als definitiv zugewiesen p . Die
Zuweisung zu *p wird tatsächlich initialisiert i , aber die Einbindung dieser Initialisierung liegt in der
Verantwortung des Programmierers, und es tritt kein Kompilierzeitfehler auf, wenn die Zuweisung entfernt
wurde.
Die Regeln der eindeutigen Zuweisung für den & Operator sind so vorhanden, dass die redundante
Initialisierung lokaler Variablen vermieden werden kann. Viele externe APIs nehmen z. b. einen Zeiger auf eine
Struktur, die von der API ausgefüllt wird. Aufrufe dieser APIs übergeben in der Regel die Adresse einer lokalen
Struktur Variablen, und ohne die Regel wäre eine redundante Initialisierung der Struktur Variablen erforderlich.
Inkrementieren und Dekrementieren von Zeigern
In einem unsicheren Kontext können die ++ -- Operatoren und (postfix-Inkrement-und Dekrementoperatoren
und Präfix Inkrement-und Dekrementoperatoren) auf Zeiger Variablen aller Typen außer angewendet werden
void* . Daher sind für jeden Zeigertyp T* die folgenden Operatoren implizit definiert:

T* operator ++(T* x);


T* operator --(T* x);

Die Operatoren führen zu den gleichen Ergebnissen wie x + 1 und x - 1 bzw. (Zeigerarithmetik). Anders
ausgedrückt: für eine Zeiger Variable vom Typ T* ++ Fügt der Operator der sizeof(T) in der Variablen
enthaltenen Adresse hinzu, und der -- Operator subtrahiert sizeof(T) von der Adresse, die in der Variablen
enthalten ist.
Wenn ein Zeiger Inkrement-oder Dekrement-Vorgang die Domäne des Zeiger Typs überschreitet, wird das
Ergebnis durch die Implementierung definiert, es werden jedoch keine Ausnahmen erzeugt.
Zeigerarithmetik
In einem unsicheren Kontext können die + - Operatoren und (AdditionsOperator und Subtraktions Operator)
auf Werte aller Zeiger Typen außer angewendet werden void* . Daher sind für jeden Zeigertyp T* die
folgenden Operatoren implizit definiert:
T* operator +(T* x, int y);
T* operator +(T* x, uint y);
T* operator +(T* x, long y);
T* operator +(T* x, ulong y);

T* operator +(int x, T* y);


T* operator +(uint x, T* y);
T* operator +(long x, T* y);
T* operator +(ulong x, T* y);

T* operator -(T* x, int y);


T* operator -(T* x, uint y);
T* operator -(T* x, long y);
T* operator -(T* x, ulong y);

long operator -(T* x, T* y);

Wenn ein Ausdruck P eines Zeiger Typs T* und ein Ausdruck N des Typs int ,, uint oder angegeben long
ulong ist, berechnen die Ausdrücke P + N und N + P berechnen den Zeiger Wert des Typs, der T* sich aus
dem Hinzufügen N * sizeof(T) zur von angegebenen Adresse ergibt P . Entsprechend berechnet der
Ausdruck P - N den Zeiger Wert des Typs, der T* sich aus der Subtraktion N * sizeof(T) von der Adresse
ergibt, die von angegeben wird P .
Bei zwei Ausdrücken, P und Q , von einem Zeigertyp T* berechnet der Ausdruck P - Q den Unterschied
zwischen den Adressen, die von und angegeben werden, P Q und dividiert diese Differenz durch sizeof(T) .
Der Ergebnistyp ist immer long . In der Tat P - Q wird als berechnet ((long)(P) - (long)(Q)) / sizeof(T) .
Beispiel:

using System;

class Test
{
static void Main() {
unsafe {
int* values = stackalloc int[20];
int* p = &values[1];
int* q = &values[15];
Console.WriteLine("p - q = {0}", p - q);
Console.WriteLine("q - p = {0}", q - p);
}
}
}

der die Ausgabe erzeugt:

p - q = -14
q - p = 14

Wenn eine Zeiger arithmetische Operation die Domäne des Zeiger Typs überschreitet, wird das Ergebnis in einer
durch die Implementierung definierten Weise abgeschnitten, es werden jedoch keine Ausnahmen erzeugt.
Zeigervergleich
In einem unsicheren Kontext können die == != < Operatoren,,, > , <= und => (relationale und Typtest
Operatoren) auf Werte aller Zeiger Typen angewendet werden. Die Zeiger Vergleichs Operatoren lauten wie
folgt:
bool operator ==(void* x, void* y);
bool operator !=(void* x, void* y);
bool operator <(void* x, void* y);
bool operator >(void* x, void* y);
bool operator <=(void* x, void* y);
bool operator >=(void* x, void* y);

Da eine implizite Konvertierung von einem Zeigertyp in den void* Typ vorhanden ist, können die Operanden
eines beliebigen Zeigertyps mithilfe dieser Operatoren verglichen werden. Die Vergleichs Operatoren
vergleichen die Adressen, die von den beiden Operanden angegeben werden, so, als wären Sie ganze Zahlen
ohne Vorzeichen.
Der sizeof-Operator
Der sizeof -Operator gibt die Anzahl der Bytes zurück, die von einer Variablen eines bestimmten Typs belegt
werden. Der als Operand angegebene Typ sizeof muss ein unmanaged_type (Zeiger Typen) sein.

sizeof_expression
: 'sizeof' '(' unmanaged_type ')'
;

Das Ergebnis des- sizeof Operators ist ein Wert vom Typ int . Für bestimmte vordefinierte Typen sizeof
ergibt der Operator einen konstanten Wert, wie in der folgenden Tabelle dargestellt.

A USDRUC K ERGEB N IS

sizeof(sbyte) 1

sizeof(byte) 1

sizeof(short) 2

sizeof(ushort) 2

sizeof(int) 4

sizeof(uint) 4

sizeof(long) 8

sizeof(ulong) 8

sizeof(char) 2

sizeof(float) 4

sizeof(double) 8

sizeof(bool) 1

Für alle anderen Typen ist das Ergebnis des sizeof Operators Implementierungs definiert und wird als Wert,
nicht als Konstante klassifiziert.
Die Reihenfolge, in der Elemente in einer Struktur verpackt werden, ist nicht angegeben.
Zu Ausrichtungs Zwecken gibt es möglicherweise unbenannte Auffüll Zeichen am Anfang einer Struktur,
innerhalb einer Struktur und am Ende der Struktur. Der Inhalt der als Auffüllung verwendeten Bits ist
unbestimmt.
Wenn ein Operand mit einem Strukturtyp angewendet wird, ist das Ergebnis die Gesamtzahl der Bytes in einer
Variablen dieses Typs, einschließlich aller Auffüll Zeichen.

The fixed statement (Die fixed-Anweisung)


In einem unsicheren Kontext lässt die embedded_statement (-Anweisungen) in der Produktionsumgebung ein
zusätzliches Konstrukt (die-Anweisung) zu, das fixed verwendet wird, um eine verschiebbare Variable zu
"korrigieren", sodass die Adresse für die Dauer der Anweisung konstant bleibt.

fixed_statement
: 'fixed' '(' pointer_type fixed_pointer_declarators ')' embedded_statement
;

fixed_pointer_declarators
: fixed_pointer_declarator (',' fixed_pointer_declarator)*
;

fixed_pointer_declarator
: identifier '=' fixed_pointer_initializer
;

fixed_pointer_initializer
: '&' variable_reference
| expression
;

Jede fixed_pointer_declarator deklariert eine lokale Variable der angegebenen pointer_type und initialisiert diese
lokale Variable mit der Adresse, die vom entsprechenden fixed_pointer_initializer berechnet wird. Auf eine lokale
Variable, die in einer fixed -Anweisung deklariert ist, kann in allen fixed_pointer_initializer en zugegriffen
werden, die auf der rechten Seite der Deklaration der Variablen und im embedded_statement der fixed
Anweisung auftreten. Eine lokale Variable, die durch eine- fixed Anweisung deklariert wird, wird als
schreibgeschützt betrachtet. Ein Kompilierzeitfehler tritt auf, wenn die eingebettete Anweisung versucht, diese
lokale Variable (über die ++ -und- -- Operatoren) zu ändern oder Sie als-oder-Parameter zu übergeben ref
out .

Eine fixed_pointer_initializer kann eine der folgenden sein:


Das Token " & " gefolgt von einem variable_reference (genaue Regeln zum Bestimmen der eindeutigen
Zuweisung) für eine verschiebbare Variable (Feste und verschiebbare Variablen) eines nicht verwalteten Typs
T , sofern der Typ T* implizit in den Zeigertyp konvertiert werden kann, der in der-Anweisung angegeben
ist fixed . In diesem Fall berechnet der Initialisierer die Adresse der angegebenen Variablen, und die
Variable bleibt für die Dauer der Anweisung garantiert an einer bestimmten Adresse fixed .
Ein Ausdruck einer array_type mit Elementen eines nicht verwalteten Typs T , sofern der Typ implizit in T*
den Zeigertyp konvertiert werden kann, der in der-Anweisung angegeben ist fixed . In diesem Fall
berechnet der Initialisierer die Adresse des ersten Elements im Array, und das gesamte Array bleibt für die
Dauer der Anweisung garantiert an einer Fixed-Adresse fixed . Wenn der Array Ausdruck NULL ist oder das
Array über keine Elemente verfügt, berechnet der Initialisierer eine Adresse, die gleich 0 (null) ist.
Ein Ausdruck vom Typ string , sofern der Typ char* implizit in den Zeigertyp konvertiert werden kann, der
in der-Anweisung angegeben ist fixed . In diesem Fall berechnet der Initialisierer die Adresse des ersten
Zeichens in der Zeichenfolge, und die gesamte Zeichenfolge bleibt für die Dauer der Anweisung garantiert
bei einer Fixed-Adresse fixed . Das Verhalten der fixed -Anweisung ist Implementierungs definiert, wenn
der Zeichen folgen Ausdruck NULL ist.
Eine Simple_name oder member_access , die auf einen Puffer mit fester Größe einer verschiebbaren
Variablen verweist, vorausgesetzt, dass der Typ des Puffer Elements mit fester Größe implizit in den in der
Anweisung angegebenen Zeigertyp konvertiert werden kann fixed . In diesem Fall berechnet der
Initialisierer einen Zeiger auf das erste Element des Puffers mit fester Größe (Puffer fester Größe in
Ausdrücken), und der Puffer fester Größe bleibt für die Dauer der Anweisung garantiert an einer bestimmten
Adresse fixed .
Für jede Adresse, die von einem fixed_pointer_initializer berechnet fixed wird, stellt die-Anweisung sicher, dass
die Variable, auf die von der Adresse verwiesen wird, nicht für die Dauer der Anweisung von der Garbage
Collector verlagert oder zur Verfügung gestellt wird fixed . Wenn die von einer fixed_pointer_initializer
berechnete Adresse beispielsweise auf ein Feld eines Objekts oder auf ein Element einer Array Instanz verweist,
stellt die- fixed Anweisung sicher, dass die enthaltende Objektinstanz während der Lebensdauer der
Anweisung nicht verschoben oder verworfen wird.
Es ist Aufgabe des Programmierers, sicherzustellen, dass Zeiger, die von-Anweisungen erstellt werden, fixed
nicht über die Ausführung dieser Anweisungen hinaus überleben. Wenn beispielsweise Zeiger, die von- fixed
Anweisungen erstellt werden, an externe APIs übermittelt werden, liegt es in der Verantwortung des
Programmierers sicherzustellen, dass die APIs keinen Arbeitsspeicher für diese Zeiger erhalten.
Fixed-Objekte können die Fragmentierung des Heaps verursachen (da Sie nicht verschoben werden können).
Aus diesem Grund sollten Objekte nur dann korrigiert werden, wenn Sie unbedingt erforderlich sind, und dann
nur für die kürzeste benötigte Zeit.
Das Beispiel

class Test
{
static int x;
int y;

unsafe static void F(int* p) {


*p = 1;
}

static void Main() {


Test t = new Test();
int[] a = new int[10];
unsafe {
fixed (int* p = &x) F(p);
fixed (int* p = &t.y) F(p);
fixed (int* p = &a[0]) F(p);
fixed (int* p = a) F(p);
}
}
}

veranschaulicht verschiedene Verwendungen der- fixed Anweisung. Mit der ersten Anweisung wird die
Adresse eines statischen Felds korrigiert und abgerufen, mit der zweiten Anweisung wird die Adresse eines
Instanzfelds korrigiert und abgerufen, und die dritte Anweisung korrigiert und ruft die Adresse eines Array
Elements ab. In jedem Fall wäre es ein Fehler, den regulären Operator zu verwenden, & da die Variablen alle als
verschiebbare Variablen klassifiziert werden.
Die vierte fixed Anweisung im obigen Beispiel führt zu einem ähnlichen Ergebnis wie das dritte.
In diesem Beispiel für die-Anweisung wird Folgendes fixed verwendet string :
class Test
{
static string name = "xx";

unsafe static void F(char* p) {


for (int i = 0; p[i] != '\0'; ++i)
Console.WriteLine(p[i]);
}

static void Main() {


unsafe {
fixed (char* p = name) F(p);
fixed (char* p = "xx") F(p);
}
}
}

In einem unsicheren Kontext Array werden Elemente von eindimensionalen Arrays in einer zunehmenden Index
Reihenfolge gespeichert, beginnend mit Index 0 und endende mit Index Length - 1 . Bei mehrdimensionalen
Arrays werden Array Elemente so gespeichert, dass die Indizes der äußersten rechten Dimension zuerst, dann
die nächste linke Dimension usw. nach links angehoben werden. Innerhalb einer-Anweisung, die fixed einen
Zeiger p auf eine Array Instanz a abruft, sind die Zeiger Werte von, die die p p + a.Length - 1 Adressen der
Elemente im Array darstellen. Ebenso sind die Variablen, die von reichen, p[0] mit p[a.Length - 1] den
eigentlichen Array Elementen. Angesichts der Art und Weise, in der Arrays gespeichert werden, können wir ein
Array einer beliebigen Dimension so behandeln, als wäre es linear.
Beispiel:

using System;

class Test
{
static void Main() {
int[,,] a = new int[2,3,4];
unsafe {
fixed (int* p = a) {
for (int i = 0; i < a.Length; ++i) // treat as linear
p[i] = i;
}
}

for (int i = 0; i < 2; ++i)


for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 4; ++k)
Console.Write("[{0},{1},{2}] = {3,2} ", i, j, k, a[i,j,k]);
Console.WriteLine();
}
}
}

der die Ausgabe erzeugt:

[0,0,0] = 0 [0,0,1] = 1 [0,0,2] = 2 [0,0,3] = 3


[0,1,0] = 4 [0,1,1] = 5 [0,1,2] = 6 [0,1,3] = 7
[0,2,0] = 8 [0,2,1] = 9 [0,2,2] = 10 [0,2,3] = 11
[1,0,0] = 12 [1,0,1] = 13 [1,0,2] = 14 [1,0,3] = 15
[1,1,0] = 16 [1,1,1] = 17 [1,1,2] = 18 [1,1,3] = 19
[1,2,0] = 20 [1,2,1] = 21 [1,2,2] = 22 [1,2,3] = 23

Im Beispiel
class Test
{
unsafe static void Fill(int* p, int count, int value) {
for (; count != 0; count--) *p++ = value;
}

static void Main() {


int[] a = new int[100];
unsafe {
fixed (int* p = a) Fill(p, 100, -1);
}
}
}

eine- fixed Anweisung wird verwendet, um ein Array zu korrigieren, sodass die Adresse an eine Methode, die
einen Zeiger annimmt, übermittelt werden kann.
Im Beispiel:

unsafe struct Font


{
public int size;
public fixed char name[32];
}

class Test
{
unsafe static void PutString(string s, char* buffer, int bufSize) {
int len = s.Length;
if (len > bufSize) len = bufSize;
for (int i = 0; i < len; i++) buffer[i] = s[i];
for (int i = len; i < bufSize; i++) buffer[i] = (char)0;
}

Font f;

unsafe static void Main()


{
Test test = new Test();
test.f.size = 10;
fixed (char* p = test.f.name) {
PutString("Times New Roman", p, 32);
}
}
}

eine fixed-Anweisung wird verwendet, um einen Puffer fester Größe einer Struktur zu korrigieren, sodass die
Adresse als Zeiger verwendet werden kann.
Ein char* Wert, der durch das Reparieren einer Zeichen folgen Instanz erzeugt wird, verweist immer auf eine
mit NULL endenden Zeichenfolge Innerhalb einer fixed-Anweisung, die einen Zeiger p auf eine Zeichen folgen
Instanz s abruft, sind die Zeiger Werte von, die die p p + s.Length - 1 Adressen der Zeichen in der
Zeichenfolge darstellen, und der Zeiger Wert p + s.Length zeigt immer auf ein NULL-Zeichen (das Zeichen mit
Wert '\0' ).
Das Ändern von Objekten des verwalteten Typs durch Fixed-Zeiger kann zu undefiniertem Verhalten führen. Da
z. b. Zeichen folgen unveränderlich sind, ist es die Aufgabe des Programmierers sicherzustellen, dass die
Zeichen, auf die von einem Zeiger auf eine festgelegte Zeichenfolge verwiesen wird, nicht geändert werden.
Die automatische NULL-Beendigung von Zeichen folgen ist besonders praktisch, wenn externe APIs aufgerufen
werden, die "C-Style"-Zeichen folgen erwarten. Beachten Sie jedoch, dass eine Zeichen folgen Instanz NULL-
Zeichen enthalten darf. Wenn solche NULL-Zeichen vorhanden sind, wird die Zeichenfolge abgeschnitten, wenn
Sie als NULL-terminierte behandelt wird char* .

Puffer fester Größe


Puffer fester Größe werden verwendet, um in-Line-Arrays im C-Format als Member von Strukturen zu
deklarieren und sind hauptsächlich für die Schnittstellen mit nicht verwalteten APIs nützlich.
Puffer Deklarationen fester Größe
Ein Puffer fester Größe ist ein Member, der den Speicher für einen Puffer fester Länge von Variablen eines
bestimmten Typs darstellt. Eine Puffer Deklaration mit fester Größe führt einen oder mehrere Puffer fester
Größe eines bestimmten Elementtyps ein. Puffer fester Größe sind nur in Struktur Deklarationen zulässig und
können nur in unsicheren Kontexten (unsichere Kontexte) vorkommen.

struct_member_declaration_unsafe
: fixed_size_buffer_declaration
;

fixed_size_buffer_declaration
: attributes? fixed_size_buffer_modifier* 'fixed' buffer_element_type fixed_size_buffer_declarator+ ';'
;

fixed_size_buffer_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'unsafe'
;

buffer_element_type
: type
;

fixed_size_buffer_declarator
: identifier '[' constant_expression ']'
;

Eine Puffer Deklaration fester Größe kann einen Satz von Attributen (Attribute), einen new Modifizierer
(Modifizierer), eine gültige Kombination der vier Zugriffsmodifizierer (Typparameter und Einschränkungen) und
einen unsafe Modifizierer (unsichere Kontexte) enthalten. Die Attribute und Modifizierer gelten für alle Member,
die von der Puffer Deklaration mit fester Größe deklariert werden. Es ist ein Fehler, dass derselbe Modifizierer
mehrmals in einer Puffer Deklaration fester Größe angezeigt wird.
Eine Puffer Deklaration mit fester Größe darf den- static Modifizierer nicht enthalten.
Der Puffer Elementtyp einer Puffer Deklaration mit fester Größe gibt den Elementtyp der Puffer an, die von der
Deklaration eingeführt wurden. Der Puffer Elementtyp muss einer der vordefinierten Typen,,,, sbyte byte
short ushort int , uint , long , ulong , char , float , oder sein double bool .

Auf den Puffer Elementtyp folgt eine Liste von Puffer Deklaratoren fester Größe, von denen jeder einen neuen
Member einführt. Ein Puffer mit fester Größe besteht aus einem Bezeichner, der den Member benennt, gefolgt
von einem konstanten Ausdruck, der in [ -und-Token eingeschlossen ist ] . Der Konstante Ausdruck gibt die
Anzahl der Elemente in dem Element an, das von diesem Puffer Deklarator mit fester Größe eingeführt wurde.
Der Typ des konstanten Ausdrucks muss implizit in den Typ konvertierbar sein int , und der Wert muss eine
positive ganze Zahl ungleich 0 (null) sein.
Es ist sichergestellt, dass die Elemente eines Puffers mit fester Größe sequenziell im Arbeitsspeicher angeordnet
werden.
Eine Puffer Deklaration fester Größe, die mehrere Puffer fester Größe deklariert, entspricht mehreren
Deklarationen einer einzelnen Puffer Deklaration mit fester Größe mit denselben Attributen und Elementtypen.
Beispiel:

unsafe struct A
{
public fixed int x[5], y[10], z[100];
}

für die folgende Syntax:

unsafe struct A
{
public fixed int x[5];
public fixed int y[10];
public fixed int z[100];
}

Puffer fester Größe in Ausdrücken


Die Member-Suche (Operatoren) eines Puffer Elements fester Größe verläuft genau wie die Element Suche eines
Felds.
Auf einen Puffer fester Größe kann in einem Ausdruck mithilfe eines Simple_name (Typrückschluss) oder eines
member_access verwiesen werden (über Prüfung der dynamischen Überladungs Auflösung zur Kompilierzeit).
Wenn ein Puffer Element mit fester Größe als einfacher Name referenziert wird, entspricht der Effekt dem
Element Zugriff auf das Formular this.I , wobei I das Puffer Element mit fester Größe ist.
Wenn ein Element Zugriff auf das Formular E.I ist, wenn ein E Strukturtyp ist und eine Member-Suche von
I in diesem Strukturtyp einen Member mit fester Größe identifiziert, E.I wird wie folgt ausgewertet:

Wenn der Ausdruck E.I nicht in einem unsicheren Kontext auftritt, tritt ein Kompilierzeitfehler auf.
Wenn E als Wert klassifiziert wird, tritt ein Kompilierzeitfehler auf.
Andernfalls E tritt ein Kompilierzeitfehler auf, wenn eine verschiebbare Variable (Fixed und verschiebbare
Variablen) ist und der Ausdruck E.I keine fixed_pointer_initializer ist (die fixed-Anweisung).
Andernfalls E verweist auf eine Variable mit fester Größe, und das Ergebnis des Ausdrucks ist ein Zeiger auf
das erste Element des Puffer Elements mit fester Größe I in E . Das Ergebnis ist vom Typ S* , wobei S
der Elementtyp von ist I und als Wert klassifiziert wird.

Auf die nachfolgenden Elemente des Puffers mit fester Größe kann mithilfe von Zeiger Vorgängen aus dem
ersten Element zugegriffen werden. Im Gegensatz zum Zugriff auf Arrays ist der Zugriff auf die Elemente eines
Puffers fester Größe ein unsicherer Vorgang und ist nicht Bereichs geprüft.
Im folgenden Beispiel wird eine Struktur mit einem Puffer Element fester Größe deklariert und verwendet.
unsafe struct Font
{
public int size;
public fixed char name[32];
}

class Test
{
unsafe static void PutString(string s, char* buffer, int bufSize) {
int len = s.Length;
if (len > bufSize) len = bufSize;
for (int i = 0; i < len; i++) buffer[i] = s[i];
for (int i = len; i < bufSize; i++) buffer[i] = (char)0;
}

unsafe static void Main()


{
Font f;
f.size = 10;
PutString("Times New Roman", f.name, 32);
}
}

Definitive Zuweisungs Überprüfung


Puffer fester Größe unterliegen nicht der eindeutigen Zuweisungs Überprüfung (definitive Zuweisung), und
Puffer Elemente fester Größe werden ignoriert, um eine definitive Zuweisungs Überprüfung von Strukturtyp
Variablen zu bestimmen.
Wenn die äußerste enthaltende Struktur Variable eines Puffer Elements mit fester Größe eine statische Variable,
eine Instanzvariable einer Klasseninstanz oder ein Array Element ist, werden die Elemente des Puffers fester
Größe automatisch mit ihren Standardwerten initialisiert (Standardwerte). In allen anderen Fällen ist der
anfängliche Inhalt eines Puffers mit fester Größe nicht definiert.

Stapelbelegung
In einem unsicheren Kontext kann eine lokale Variablen Deklaration (Deklarationen von lokalen Variablen) einen
Stapel Zuordnungs Initialisierer enthalten, der Arbeitsspeicher aus der-aufrufsliste zuweist.

local_variable_initializer_unsafe
: stackalloc_initializer
;

stackalloc_initializer
: 'stackalloc' unmanaged_type '[' expression ']'
;

Der unmanaged_type der den Typ der Elemente angibt, die am neu zugewiesenen Speicherort gespeichert
werden, und der Ausdruck gibt die Anzahl dieser Elemente an. Diese geben die erforderliche Zuordnungs Größe
an. Da die Größe einer Stapel Zuordnung nicht negativ sein kann, handelt es sich um einen Kompilierzeitfehler,
um die Anzahl der Elemente als constant_expression anzugeben, der einen negativen Wert ergibt.
Ein Stapel Zuordnungs Initialisierer des Formulars stackalloc T[E] erfordert T , dass es sich um einen nicht
verwalteten Typ (Zeiger Typen) und E um einen Ausdruck vom Typ handelt int . Das-Konstrukt ordnet
E * sizeof(T) Bytes aus der-aufrufsstapel zu und gibt einen Zeiger vom Typ T* auf den neu belegten Block
zurück. Wenn E ein negativer Wert ist, ist das Verhalten nicht definiert. Wenn E 0 (null) ist, wird keine
Zuweisung durchgeführt, und der zurückgegebene Zeiger ist Implementierungs definiert. Wenn nicht genügend
Arbeitsspeicher verfügbar ist, um einen Block mit der angegebenen Größe zuzuordnen,
System.StackOverflowException wird eine ausgelöst.
Der Inhalt des neu zugeordneten Speichers ist undefiniert.
Stapel Zuordnungs Initialisierer sind in- catch oder- finally Blöcken (try-Anweisung) nicht zulässig.
Es gibt keine Möglichkeit, Arbeitsspeicher, der mit zugewiesen wurde, explizit freizugeben stackalloc Alle
Stapel zugeordneten Speicherblöcke, die während der Ausführung eines Funktionsmembers erstellt werden,
werden automatisch verworfen, wenn der Funktions Member zurückgegeben wird. Dies entspricht der- alloca
Funktion, eine Erweiterung, die häufig in C-und C++-Implementierungen gefunden wird.
Im Beispiel

using System;

class Test
{
static string IntToString(int value) {
int n = value >= 0? value: -value;
unsafe {
char* buffer = stackalloc char[16];
char* p = buffer + 16;
do {
*--p = (char)(n % 10 + '0');
n /= 10;
} while (n != 0);
if (value < 0) *--p = '-';
return new string(p, 0, (int)(buffer + 16 - p));
}
}

static void Main() {


Console.WriteLine(IntToString(12345));
Console.WriteLine(IntToString(-999));
}
}

stackalloc in der-Methode wird ein Initialisierer verwendet IntToString , um einen Puffer mit 16 Zeichen im
Stapel zuzuordnen. Der Puffer wird automatisch verworfen, wenn die Methode zurückgibt.

Dynamic memory allocation (Dynamische Speicherbelegung)


Mit Ausnahme des- stackalloc Operators bietet c# keine vordefinierten Konstrukte zum Verwalten von nicht
Garbage Collection-Speicher. Solche Dienste werden in der Regel durch Unterstützung von Klassenbibliotheken
oder direkt aus dem zugrunde liegenden Betriebssystem bereitgestellt. Die Memory folgende Klasse zeigt
beispielsweise, wie auf die Heap Funktionen eines zugrunde liegenden Betriebssystems aus c# zugegriffen
werden kann:

using System;
using System.Runtime.InteropServices;

public static unsafe class Memory


{
// Handle for the process heap. This handle is used in all calls to the
// HeapXXX APIs in the methods below.
private static readonly IntPtr s_heap = GetProcessHeap();

// Allocates a memory block of the given size. The allocated memory is


// automatically initialized to zero.
public static void* Alloc(int size)
{
void* result = HeapAlloc(s_heap, HEAP_ZERO_MEMORY, (UIntPtr)size);
if (result == null) throw new OutOfMemoryException();
return result;
}

// Copies count bytes from src to dst. The source and destination
// blocks are permitted to overlap.
public static void Copy(void* src, void* dst, int count)
{
byte* ps = (byte*)src;
byte* pd = (byte*)dst;
if (ps > pd)
{
for (; count != 0; count--) *pd++ = *ps++;
}
else if (ps < pd)
{
for (ps += count, pd += count; count != 0; count--) *--pd = *--ps;
}
}

// Frees a memory block.


public static void Free(void* block)
{
if (!HeapFree(s_heap, 0, block)) throw new InvalidOperationException();
}

// Re-allocates a memory block. If the reallocation request is for a


// larger size, the additional region of memory is automatically
// initialized to zero.
public static void* ReAlloc(void* block, int size)
{
void* result = HeapReAlloc(s_heap, HEAP_ZERO_MEMORY, block, (UIntPtr)size);
if (result == null) throw new OutOfMemoryException();
return result;
}

// Returns the size of a memory block.


public static int SizeOf(void* block)
{
int result = (int)HeapSize(s_heap, 0, block);
if (result == -1) throw new InvalidOperationException();
return result;
}

// Heap API flags


private const int HEAP_ZERO_MEMORY = 0x00000008;

// Heap API functions


[DllImport("kernel32")]
private static extern IntPtr GetProcessHeap();

[DllImport("kernel32")]
private static extern void* HeapAlloc(IntPtr hHeap, int flags, UIntPtr size);

[DllImport("kernel32")]
private static extern bool HeapFree(IntPtr hHeap, int flags, void* block);

[DllImport("kernel32")]
private static extern void* HeapReAlloc(IntPtr hHeap, int flags, void* block, UIntPtr size);

[DllImport("kernel32")]
private static extern UIntPtr HeapSize(IntPtr hHeap, int flags, void* block);
}

Unten ist ein Beispiel angegeben, das die- Memory Klasse verwendet:
class Test
{
static unsafe void Main()
{
byte* buffer = null;
try
{
const int Size = 256;
buffer = (byte*)Memory.Alloc(Size);
for (int i = 0; i < Size; i++) buffer[i] = (byte)i;
byte[] array = new byte[Size];
fixed (byte* p = array) Memory.Copy(buffer, p, Size);
for (int i = 0; i < Size; i++) Console.WriteLine(array[i]);
}
finally
{
if (buffer != null) Memory.Free(buffer);
}
}
}

Im Beispiel wird der Arbeitsspeicher von 256 Bytes zugeordnet Memory.Alloc , und der Speicherblock mit
Werten, die von 0 bis 255 steigen, wird initialisiert. Anschließend wird ein 256-Element-Bytearray zugeordnet
und verwendet Memory.Copy , um den Inhalt des Speicherblocks in das Bytearray zu kopieren. Schließlich wird
der Speicherblock mithilfe von freigegeben, Memory.Free und der Inhalt des Bytearrays wird in der Konsole
ausgegeben.
Dokumentationskommentare
04.11.2021 • 34 minutes to read

C# bietet einen Mechanismus, mit dem Programmierer Ihren Code mithilfe einer speziellen Kommentar Syntax
dokumentieren können, die XML-Text enthält. In Quell Code Dateien können Kommentare mit einem
bestimmten Formular verwendet werden, um ein Tool zum Erstellen von XML aus diesen Kommentaren und den
Quell Code Elementen zu leiten, denen Sie vorangestellt sind. Kommentare, die diese Syntax verwenden, werden
als *Dokumentations Kommentare _ bezeichnet. Sie müssen direkt einem benutzerdefinierten Typ (z. b. einer
Klasse, einem Delegaten oder einer Schnittstelle) oder einem Member (z. b. einem Feld, einem Ereignis, einer
Eigenschaft oder einer Methode) vorangestellt sein. Das XML-Generierungs Tool wird als Dokumentations
Generator bezeichnet. (Dieser Generator kann, aber nicht, der c#-Compiler selbst sein.) Die vom
Dokumentations Generator erstellte Ausgabe wird als Dokumentations Datei bezeichnet. Eine Dokumentations
Datei wird als Eingabe für einen _ *-Dokumentations-Viewer verwendet * *; ein Tool, mit dem eine visuelle
Darstellung der Typinformationen und der zugehörigen Dokumentation erzeugt werden soll.
Diese Spezifikation schlägt vor, dass ein Satz von Tags in Dokumentations Kommentaren verwendet wird, aber
die Verwendung dieser Tags ist nicht erforderlich, und andere Tags können bei Bedarf verwendet werden,
solange die Regeln von wohl geformtem XML befolgt werden.

Einführung
Kommentare mit einem speziellen Formular können verwendet werden, um ein Tool zum Erstellen von XML aus
diesen Kommentaren und den Quell Code Elementen zu leiten, denen Sie vorangestellt sind. Solche
Kommentare sind einzeilige Kommentare, die mit drei Schrägstrichen ( /// ) oder durch getrennte
Kommentare beginnen, die mit einem Schrägstrich und zwei Sternen beginnen ( /** ). Sie müssen unmittelbar
vor einem benutzerdefinierten Typ (z. b. einer Klasse, einem Delegaten oder einer Schnittstelle) oder einem
Member (z. b. einem Feld, einem Ereignis, einer Eigenschaft oder einer Methode), dem Sie kommentiert werden,
voranstehen. Attribut Abschnitte (Attribut Spezifikation) werden als Teil von Deklarationen betrachtet. Daher
müssen Dokumentations Kommentare den Attributen vorangestellt werden, die auf einen Typ oder Member
angewendet werden.
Syntax:

single_line_doc_comment
: '///' input_character*
;

delimited_doc_comment
: '/**' delimited_comment_section* asterisk+ '/'
;

Wenn in einer single_line_doc_comment ein leer Zeichen nach den Zeichen auf den einzelnen
single_line_doc_comment s vorhanden ist, die /// sich neben dem aktuellen single_line_doc_comment
befinden, ist dieses leer Zeichen nicht in der XML-Ausgabe enthalten.
Wenn in einem durch Trennzeichen getrennten doc-Kommentar das erste Zeichen in der zweiten Zeile, das kein
Leerzeichen ist, ein Sternchen und dasselbe Muster optionaler leer Raum Zeichen und ein Sternchen am Anfang
der einzelnen Zeilen innerhalb des durch Trennzeichen getrennten-doc-Kommentars wiederholt werden, werden
die Zeichen des wiederholten Musters nicht in der XML-Ausgabe eingeschlossen. Das Muster kann Leerzeichen
nach und vor das Sternchen Zeichen enthalten.
Beispiel:

/// <summary>Class <c>Point</c> models a point in a two-dimensional


/// plane.</summary>
///
public class Point
{
/// <summary>method <c>draw</c> renders the point.</summary>
void draw() {...}
}

Der Text in den Dokumentations Kommentaren muss gemäß den Regeln von XML () wohl geformt sein
https://www.w3.org/TR/REC-xml) . Wenn das XML nicht ordnungsgemäß formatiert ist, wird eine Warnung
generiert, und die Dokumentations Datei enthält einen Kommentar, der besagt, dass ein Fehler aufgetreten ist.
Obwohl Entwickler kostenlos einen eigenen Satz von Tags erstellen können, wird ein empfohlener Satz in
empfohlenen Tagsdefiniert. Einige der empfohlenen Tags haben eine besondere Bedeutung:
Das <param> -Tag wird verwendet, um Parameter zu beschreiben. Wenn ein solches Tag verwendet wird,
muss der Dokumentations Generator überprüfen, ob der angegebene Parameter vorhanden ist und dass alle
Parameter in den Dokumentations Kommentaren beschrieben werden. Wenn diese Überprüfung fehlschlägt,
gibt der Dokumentations Generator eine Warnung aus.
Das cref -Attribut kann an jedes Tag angefügt werden, um einen Verweis auf ein Codeelement
bereitzustellen. Der Dokumentations Generator muss überprüfen, ob dieses Code Element vorhanden ist.
Wenn die Überprüfung fehlschlägt, gibt der Dokumentations Generator eine Warnung aus. Bei der Suche
nach einem Namen, der in einem- cref Attribut beschrieben wird, muss der Dokumentations-Generator die
Sichtbarkeit von Namespaces gemäß den using Anweisungen im Quellcode berücksichtigen. Für Code
Elemente, die generisch sind, kann die normale generische Syntax (d List<T> . h. "") nicht verwendet
werden, da Sie ungültiges XML erzeugt. Geschweifte Klammern können anstelle von Klammern (d. h. "
List{T} ") verwendet werden, oder die XML-Escapesyntax kann verwendet werden (d List&lt;T&gt; . h. "").
Das- <summary> Tag soll von einem Dokumentations-Viewer verwendet werden, um zusätzliche
Informationen zu einem Typ oder Member anzuzeigen.
Das- <include> Tag enthält Informationen aus einer externen XML-Datei.
Beachten Sie, dass die Dokumentations Datei keine vollständigen Informationen über den Typ und die Member
bereitstellt (z. b. enthält Sie keine Typinformationen). Um solche Informationen zu einem Typ oder Member zu
erhalten, muss die Dokumentations Datei in Verbindung mit der Reflektion für den tatsächlichen Typ oder
Member verwendet werden.

Recommended tags (Empfohlene Tags)


Der Dokumentations-Generator muss alle Tags akzeptieren und verarbeiten, die gemäß den XML-Regeln gültig
sind. Die folgenden Tags stellen häufig verwendete Funktionen in der Benutzerdokumentation bereit. (Natürlich
sind andere Tags möglich.)

TA G B EREIC H Z W EC K

<c> <c> Festlegen von Text in einer Code


ähnlichen Schriftart

<code> <code> Mindestens eine Zeile des Quellcodes


oder der Programmausgabe festlegen

<example> <example> Anzeigen eines Beispiels


TA G B EREIC H Z W EC K

<exception> <exception> Identifiziert die Ausnahmen, die von


einer Methode ausgelöst werden
können.

<include> <include> Enthält XML aus einer externen Datei.

<list> <list> Erstellen einer Liste oder Tabelle

<para> <para> Struktur zum Hinzufügen von Text


zulassen

<param> <param> Beschreibt einen Parameter für eine


Methode oder einen Konstruktor.

<paramref> <paramref> Erkennen, dass ein Wort ein Parameter


Name ist

<permission> <permission> Dokumentieren der Sicherheits


Barrierefreiheit eines Mitglieds

<remarks> <remarks> Beschreiben zusätzlicher Informationen


zu einem Typ

<returns> <returns> Beschreiben des Rückgabewerts einer


Methode

<see> <see> Link angeben

<seealso> <seealso> Einen Eintrag "Siehe auch" generieren

<summary> <summary> Beschreibt einen Typ oder einen


Member eines Typs.

<value> <value> Beschreiben einer Eigenschaft

<typeparam> Beschreiben eines generischen


Typparameters

<typeparamref> Erkennen, dass ein Wort ein


Typparameter Name ist

<c>

Dieses Tag stellt einen Mechanismus bereit, mit dem angegeben wird, dass ein Textfragment innerhalb einer
Beschreibung in einer speziellen Schriftart, z. b. der für einen Codeblock verwendeten, festgelegt werden soll.
Verwenden Sie für Zeilen des tatsächlichen Codes <code> ( <code> ).
Syntax:

<c>text</c>

Beispiel:
/// <summary>Class <c>Point</c> models a point in a two-dimensional
/// plane.</summary>

public class Point


{
// ...
}

<code>

Dieses Tag wird verwendet, um eine oder mehrere Zeilen des Quellcodes oder der Programmausgabe in einer
speziellen Schriftart festzulegen. Verwenden Sie bei kleinen Code Fragmenten in der Erzählung <c> ( <c> ).
Syntax:

<code>source code or program output</code>

Beispiel:

/// <summary>This method changes the point's location by


/// the given x- and y-offsets.
/// <example>For example:
/// <code>
/// Point p = new Point(3,5);
/// p.Translate(-1,3);
/// </code>
/// results in <c>p</c>'s having the value (2,8).
/// </example>
/// </summary>

public void Translate(int xor, int yor) {


X += xor;
Y += yor;
}

<example>

Dieses Tag ermöglicht Beispielcode in einem Kommentar, um anzugeben, wie eine Methode oder ein anderes
Bibliothekselement verwendet werden kann. Normalerweise würde dies auch die Verwendung des-Tags <code>
( <code> ) beinhalten.
Syntax:

<example>description</example>

Beispiel:
Ein Beispiel finden Sie unter <code> ( <code> ).
<exception>

Dieses Tag bietet eine Möglichkeit, die Ausnahmen zu dokumentieren, die eine Methode auslösen kann.
Syntax:

<exception cref="member">description</exception>

where
member der Name eines Members. Der Dokumentations Generator überprüft, ob der angegebene Member
vorhanden ist, und übersetzt ihn member in der Dokumentations Datei in den kanonischen Elementnamen.
description eine Beschreibung der Umstände, in denen die Ausnahme ausgelöst wird.

Beispiel:

public class DataBaseOperations


{
/// <exception cref="MasterFileFormatCorruptException"></exception>
/// <exception cref="MasterFileLockedOpenException"></exception>
public static void ReadRecord(int flag) {
if (flag == 1)
throw new MasterFileFormatCorruptException();
else if (flag == 2)
throw new MasterFileLockedOpenException();
// ...
}
}

<include>

Dieses Tag ermöglicht das Einschließen von Informationen aus einem XML-Dokument, das für die Quell Code
Datei extern ist. Die externe Datei muss ein wohl geformtes XML-Dokument sein, und es wird ein XPath-
Ausdruck auf das Dokument angewendet, um anzugeben, welches XML-Dokument in diesem Dokument
enthalten sein soll. Das <include> Tag wird dann durch den ausgewählten XML-Code aus dem externen
Dokument ersetzt.
Syntax:

<include file="filename" path="xpath" />

where
filename der Dateiname einer externen XML-Datei. Der Dateiname wird relativ zur Datei interpretiert, die
das include-Tag enthält.
xpath ein XPath-Ausdruck, der einen Teil des XML-Codes in der externen XML-Datei auswählt.

Beispiel:
Wenn der Quellcode eine Deklaration wie z. b. enthält:

/// <include file="docs.xml" path='extradoc/class[@name="IntList"]/*' />


public class IntList { ... }

die externe Datei "docs.xml" wies den folgenden Inhalt auf:


<?xml version="1.0"?>
<extradoc>
<class name="IntList">
<summary>
Contains a list of integers.
</summary>
</class>
<class name="StringList">
<summary>
Contains a list of integers.
</summary>
</class>
</extradoc>

dann wird dieselbe Dokumentation ausgegeben, als wäre der Quellcode wie folgt:

/// <summary>
/// Contains a list of integers.
/// </summary>
public class IntList { ... }

<list>

Dieses Tag wird verwendet, um eine Liste oder eine Tabelle mit Elementen zu erstellen. Sie kann einen-
<listheader> Block enthalten, um die Überschriften Zeile einer Tabelle oder einer Definitionsliste zu definieren.
(Beim Definieren einer Tabelle muss nur ein Eintrag für term in der Überschrift angegeben werden.)
Jedes Element der Liste wird mit einem <item> -Block angegeben. Beim Erstellen einer Definitionsliste müssen
sowohl term als auch description angegeben werden. Allerdings muss für eine Tabelle, eine aufzählige Liste
oder eine nummerierte Liste nur description angegeben werden.
Syntax:

<list type="bullet" | "number" | "table">


<listheader>
<term>term</term>
<description>*description*</description>
</listheader>
<item>
<term>term</term>
<description>*description*</description>
</item>
...
<item>
<term>term</term>
<description>description</description>
</item>
</list>

where
term der Begriff, der definiert werden soll, dessen Definition in ist description .
description ist entweder ein Element in einer Aufzählungs Liste oder nummerierte Liste oder die Definition
einer term .
Beispiel:
public class MyClass
{
/// <summary>Here is an example of a bulleted list:
/// <list type="bullet">
/// <item>
/// <description>Item 1.</description>
/// </item>
/// <item>
/// <description>Item 2.</description>
/// </item>
/// </list>
/// </summary>
public static void Main () {
// ...
}
}

<para>

Dieses Tag dient zur Verwendung innerhalb anderer Tags, z <summary> . b <remarks> . () oder <returns> (
<returns> ), und lässt zu, dass die Struktur dem Text hinzugefügt wird.

Syntax:

<para>content</para>

dabei content ist der Text des Absatzes.


Beispiel:

/// <summary>This is the entry point of the Point class testing program.
/// <para>This program tests each method and operator, and
/// is intended to be run after any non-trivial maintenance has
/// been performed on the Point class.</para></summary>
public static void Main() {
// ...
}

<param>

Dieses Tag wird verwendet, um einen Parameter für eine Methode, einen Konstruktor oder einen Indexer zu
beschreiben.
Syntax:

<param name="name">description</param>

where
name ist der Name des Parameters.
description eine Beschreibung des Parameters.

Beispiel:
/// <summary>This method changes the point's location to
/// the given coordinates.</summary>
/// <param name="xor">the new x-coordinate.</param>
/// <param name="yor">the new y-coordinate.</param>
public void Move(int xor, int yor) {
X = xor;
Y = yor;
}

<paramref>

Dieses Tag wird verwendet, um anzugeben, dass ein Wort ein Parameter ist. Die Dokumentations Datei kann
verarbeitet werden, um diesen Parameter auf unterschiedliche Weise zu formatieren.
Syntax:

<paramref name="name"/>

dabei name ist der Name des Parameters.


Beispiel:

/// <summary>This constructor initializes the new Point to


/// (<paramref name="xor"/>,<paramref name="yor"/>).</summary>
/// <param name="xor">the new Point's x-coordinate.</param>
/// <param name="yor">the new Point's y-coordinate.</param>

public Point(int xor, int yor) {


X = xor;
Y = yor;
}

<permission>

Mit diesem Tag kann die Sicherheits Barrierefreiheit eines Members dokumentiert werden.
Syntax:

<permission cref="member">description</permission>

where
member der Name eines Members. Der Dokumentations Generator überprüft, ob das angegebene Code
Element vorhanden ist, und übersetzt den Member in den kanonischen Elementnamen in der
Dokumentations Datei.
description eine Beschreibung des Zugriffs auf den Member.

Beispiel:

/// <permission cref="System.Security.PermissionSet">Everyone can


/// access this method.</permission>

public static void Test() {


// ...
}

<remarks>
Dieses Tag wird verwendet, um zusätzliche Informationen zu einem Typ anzugeben. (Verwenden <summary> Sie
( <summary> ) um den Typ selbst und die Member eines Typs zu beschreiben.)
Syntax:

<remarks>description</remarks>

dabei description steht für den Text der Anmerkung.


Beispiel:

/// <summary>Class <c>Point</c> models a point in a


/// two-dimensional plane.</summary>
/// <remarks>Uses polar coordinates</remarks>
public class Point
{
// ...
}

<returns>

Dieses Tag wird verwendet, um den Rückgabewert einer Methode zu beschreiben.


Syntax:

<returns>description</returns>

dabei description ist eine Beschreibung des Rückgabewerts.


Beispiel:

/// <summary>Report a point's location as a string.</summary>


/// <returns>A string representing a point's location, in the form (x,y),
/// without any leading, trailing, or embedded whitespace.</returns>
public override string ToString() {
return "(" + X + "," + Y + ")";
}

<see>

Dieses Tag ermöglicht das Angeben eines Links in Text. Verwenden <seealso> <seealso> Sie (), um Text
anzugeben, der in einem Abschnitt Siehe auch angezeigt werden soll.
Syntax:

<see cref="member"/>

dabei member ist der Name eines Members. Der Dokumentations Generator überprüft, ob das angegebene
Code Element vorhanden ist, und ändert den Member in der generierten Dokumentations Datei in den
Elementnamen.
Beispiel:
/// <summary>This method changes the point's location to
/// the given coordinates.</summary>
/// <see cref="Translate"/>
public void Move(int xor, int yor) {
X = xor;
Y = yor;
}

/// <summary>This method changes the point's location by


/// the given x- and y-offsets.
/// </summary>
/// <see cref="Move"/>
public void Translate(int xor, int yor) {
X += xor;
Y += yor;
}

<seealso>

Dieses Tag ermöglicht das Generieren eines Eintrags für den Abschnitt "Siehe auch". Verwenden <see> <see>
Sie (), um einen Link aus Text anzugeben.
Syntax:

<seealso cref="member"/>

dabei member ist der Name eines Members. Der Dokumentations Generator überprüft, ob das angegebene
Code Element vorhanden ist, und ändert den Member in der generierten Dokumentations Datei in den
Elementnamen.
Beispiel:

/// <summary>This method determines whether two Points have the same
/// location.</summary>
/// <seealso cref="operator=="/>
/// <seealso cref="operator!="/>
public override bool Equals(object o) {
// ...
}

<summary>

Dieses Tag kann verwendet werden, um einen Typ oder einen Member eines Typs zu beschreiben. Verwenden
<remarks> <remarks> Sie (), um den Typ selbst zu beschreiben.

Syntax:

<summary>description</summary>

dabei description ist eine Zusammenfassung des Typs oder Members.


Beispiel:

/// <summary>This constructor initializes the new Point to (0,0).</summary>


public Point() : this(0,0) {
}

<value>
Dieses Tag ermöglicht die Beschreibung einer Eigenschaft.
Syntax:

<value>property description</value>

dabei property description ist eine Beschreibung der Eigenschaft.


Beispiel:

/// <value>Property <c>X</c> represents the point's x-coordinate.</value>


public int X
{
get { return x; }
set { x = value; }
}

<typeparam>

Dieses Tag wird verwendet, um einen generischen Typparameter für eine Klasse, Struktur, Schnittstelle, einen
Delegaten oder eine Methode zu beschreiben.
Syntax:

<typeparam name="name">description</typeparam>

dabei name ist der Name des Typparameters, und description ist seine Beschreibung.
Beispiel:

/// <summary>A generic list class.</summary>


/// <typeparam name="T">The type stored by the list.</typeparam>
public class MyList<T> {
...
}

<typeparamref>

Dieses Tag wird verwendet, um anzugeben, dass ein Wort ein Typparameter ist. Die Dokumentations Datei kann
verarbeitet werden, um diesen Typparameter auf unterschiedliche Weise zu formatieren.
Syntax:

<typeparamref name="name"/>

dabei name ist der Name des Typparameters.


Beispiel:

/// <summary>This method fetches data and returns a list of <typeparamref name="T"/>.</summary>
/// <param name="query">query to execute</param>
public List<T> FetchData<T>(string query) {
...
}
Processing the documentation file (Verarbeiten der
Dokumentationsdatei)
Der Dokumentations Generator generiert eine ID-Zeichenfolge für jedes Element im Quellcode, das mit einem
Dokumentations Kommentar gekennzeichnet ist. Diese ID-Zeichenfolge identifiziert eindeutig ein Quell Element.
In einem Dokumentations-Viewer kann eine ID-Zeichenfolge verwendet werden, um die entsprechenden
Metadaten/reflektionselementelemente zu identifizieren
Die Dokumentations Datei ist keine hierarchische Darstellung des Quellcodes. Vielmehr handelt es sich um eine
flache Liste mit einer generierten ID-Zeichenfolge für jedes Element.
ID-Zeichen folgen Format
Beim Generieren der ID-Zeichen folgen werden die folgenden Regeln vom Dokumentations-Generator beachtet:
In der Zeichenfolge wird kein Leerraum platziert.
Der erste Teil der Zeichenfolge identifiziert die Art des dokumentierten Members, über ein einzelnes
Zeichen gefolgt von einem Doppelpunkt. Die folgenden Arten von Membern sind definiert:

Z EIC H EN B ESC H REIB UN G

E Ereignis

F Feld

M Methode (einschließlich Konstruktoren, Dekonstruktoren


und Operatoren)

N Namespace

P Eigenschaft (einschließlich Indexer)

T Type (z. b. Klasse, Delegat, Enumeration, Schnittstelle und


Struktur)

! Fehler Zeichenfolge; der Rest der Zeichenfolge enthält


Informationen zum Fehler. Der Dokumentations
Generator generiert beispielsweise Fehlerinformationen
für Links, die nicht aufgelöst werden können.

Der zweite Teil der Zeichenfolge ist der voll qualifizierte Name des Elements, beginnend mit dem Stamm
des Namespace. Der Name des Elements, der einschließende Typ (en) und der Namespace werden durch
Punkte getrennt. Wenn der Name des Elements selbst über Zeiträume verfügt, werden Sie durch
#(U+0023) Zeichen ersetzt. (Es wird angenommen, dass kein Element über dieses Zeichen im Namen
verfügt.)
Bei Methoden und Eigenschaften mit Argumenten folgt die Argumentliste in Klammern. Für diejenigen
ohne Argumente werden die Klammern ausgelassen. Die Argumente werden durch Kommas
voneinander getrennt. Die Codierung jedes Arguments ist wie folgt mit einer CLI-Signatur identisch:
Argumente werden durch ihren Dokumentations Namen dargestellt, der auf dem voll qualifizierten
Namen basiert, wie folgt geändert:
Argumente, die generische Typen darstellen, haben ein angefügtes ` Zeichen (Backtick),
gefolgt von der Anzahl der Typparameter.
Argumente, die den- out ref Modifizierer oder den-Modifizierer haben, haben einen @
folgenden Argumente, die nach Wert oder via übermittelt werden, params haben keine
besondere Notation.
Argumente, bei denen es sich um Arrays handelt, werden als dargestellt
[lowerbound:size, ... , lowerbound:size] , wobei die Anzahl der Kommas der Rang kleiner ist,
und die unteren Begrenzungen und die Größe der einzelnen Dimensionen, sofern bekannt, in
Dezimalform dargestellt werden. Wenn eine Untergrenze oder Größe nicht angegeben ist, wird
Sie ausgelassen. Wenn die Untergrenze und die Größe für eine bestimmte Dimension
ausgelassen werden, : wird auch der weggelassen. Verzweigte Arrays werden durch eine []
pro Ebene dargestellt.
Argumente, die andere Zeiger Typen als void aufweisen, werden mithilfe eines * nach dem
Typnamen dargestellt. Ein void-Zeiger wird mithilfe eines Typnamens dargestellt System.Void .
Argumente, die auf generische Typparameter verweisen, die für-Typen definiert sind, werden
mit dem ` Zeichen (Backtick) codiert, gefolgt vom Null basierten Index des Typparameters.
Argumente, die generische Typparameter verwenden, die in-Methoden definiert sind,
verwenden einen doppelten Backtick `` anstelle der ` für-Typen verwendeten.
Argumente, die auf konstruierte generische Typen verweisen, werden mit dem generischen Typ
gefolgt von codiert { , gefolgt von einer durch Trennzeichen getrennten Liste von
Typargumenten, gefolgt von } .
ID-Zeichen folgen Beispiele
In den folgenden Beispielen wird jeweils ein Fragment des c#-Codes sowie die ID-Zeichenfolge dargestellt, die
aus jedem Quell Element erstellt wurde, das einen Dokumentations Kommentar aufweisen kann:
Typen werden mit dem voll qualifizierten Namen dargestellt, der auf generische Informationen erweitert
ist:

enum Color { Red, Blue, Green }

namespace Acme
{
interface IProcess {...}

struct ValueType {...}

class Widget: IProcess


{
public class NestedClass {...}
public interface IMenuItem {...}
public delegate void Del(int i);
public enum Direction { North, South, East, West }
}

class MyList<T>
{
class Helper<U,V> {...}
}
}

"T:Color"
"T:Acme.IProcess"
"T:Acme.ValueType"
"T:Acme.Widget"
"T:Acme.Widget.NestedClass"
"T:Acme.Widget.IMenuItem"
"T:Acme.Widget.Del"
"T:Acme.Widget.Direction"
"T:Acme.MyList`1"
"T:Acme.MyList`1.Helper`2"
Felder werden durch ihren voll qualifizierten Namen dargestellt:

namespace Acme
{
struct ValueType
{
private int total;
}

class Widget: IProcess


{
public class NestedClass
{
private int value;
}

private string message;


private static Color defaultColor;
private const double PI = 3.14159;
protected readonly double monthlyAverage;
private long[] array1;
private Widget[,] array2;
private unsafe int *pCount;
private unsafe float **ppValues;
}
}

"F:Acme.ValueType.total"
"F:Acme.Widget.NestedClass.value"
"F:Acme.Widget.message"
"F:Acme.Widget.defaultColor"
"F:Acme.Widget.PI"
"F:Acme.Widget.monthlyAverage"
"F:Acme.Widget.array1"
"F:Acme.Widget.array2"
"F:Acme.Widget.pCount"
"F:Acme.Widget.ppValues"

Konstruktoren.

namespace Acme
{
class Widget: IProcess
{
static Widget() {...}
public Widget() {...}
public Widget(string s) {...}
}
}

"M:Acme.Widget.#cctor"
"M:Acme.Widget.#ctor"
"M:Acme.Widget.#ctor(System.String)"

Destruktoren.
namespace Acme
{
class Widget: IProcess
{
~Widget() {...}
}
}

"M:Acme.Widget.Finalize"

Anzuwenden.

namespace Acme
{
struct ValueType
{
public void M(int i) {...}
}

class Widget: IProcess


{
public class NestedClass
{
public void M(int i) {...}
}

public static void M0() {...}


public void M1(char c, out float f, ref ValueType v) {...}
public void M2(short[] x1, int[,] x2, long[][] x3) {...}
public void M3(long[][] x3, Widget[][,,] x4) {...}
public unsafe void M4(char *pc, Color **pf) {...}
public unsafe void M5(void *pv, double *[][,] pd) {...}
public void M6(int i, params object[] args) {...}
}

class MyList<T>
{
public void Test(T t) { }
}

class UseList
{
public void Process(MyList<int> list) { }
public MyList<T> GetValues<T>(T inputValue) { return null; }
}
}

"M:Acme.ValueType.M(System.Int32)"
"M:Acme.Widget.NestedClass.M(System.Int32)"
"M:Acme.Widget.M0"
"M:Acme.Widget.M1(System.Char,System.Single@,Acme.ValueType@)"
"M:Acme.Widget.M2(System.Int16[],System.Int32[0:,0:],System.Int64[][])"
"M:Acme.Widget.M3(System.Int64[][],Acme.Widget[0:,0:,0:][])"
"M:Acme.Widget.M4(System.Char*,Color**)"
"M:Acme.Widget.M5(System.Void*,System.Double*[0:,0:][])"
"M:Acme.Widget.M6(System.Int32,System.Object[])"
"M:Acme.MyList`1.Test(`0)"
"M:Acme.UseList.Process(Acme.MyList{System.Int32})"
"M:Acme.UseList.GetValues``(``0)"

Eigenschaften und Indexer.


namespace Acme
{
class Widget: IProcess
{
public int Width { get {...} set {...} }
public int this[int i] { get {...} set {...} }
public int this[string s, int i] { get {...} set {...} }
}
}

"P:Acme.Widget.Width"
"P:Acme.Widget.Item(System.Int32)"
"P:Acme.Widget.Item(System.String,System.Int32)"

Ereignisse.

namespace Acme
{
class Widget: IProcess
{
public event Del AnEvent;
}
}

"E:Acme.Widget.AnEvent"

Unäre Operatoren.

namespace Acme
{
class Widget: IProcess
{
public static Widget operator+(Widget x) {...}
}
}

"M:Acme.Widget.op_UnaryPlus(Acme.Widget)"

Der gesamte Satz unärer Operator Funktionsnamen wird wie folgt verwendet: op_UnaryPlus ,
op_UnaryNegation , op_LogicalNot , op_OnesComplement , op_Increment , op_Decrement , op_True und
op_False .

Binäre Operatoren.

namespace Acme
{
class Widget: IProcess
{
public static Widget operator+(Widget x1, Widget x2) {...}
}
}

"M:Acme.Widget.op_Addition(Acme.Widget,Acme.Widget)"

Der gesamte verwendete Satz von Funktionsnamen für binäre Operatoren lautet wie folgt: op_Addition ,
op_Subtraction , op_Multiply , op_Division , op_Modulus , op_BitwiseAnd , op_BitwiseOr ,
op_ExclusiveOr , op_LeftShift , op_RightShift , op_Equality , op_Inequality , op_LessThan ,
op_LessThanOrEqual , op_GreaterThan und op_GreaterThanOrEqual .
Konvertierungs Operatoren verfügen über eine nachfolgende " ~ ", gefolgt vom Rückgabetyp.

namespace Acme
{
class Widget: IProcess
{
public static explicit operator int(Widget x) {...}
public static implicit operator long(Widget x) {...}
}
}

"M:Acme.Widget.op_Explicit(Acme.Widget)~System.Int32"
"M:Acme.Widget.op_Implicit(Acme.Widget)~System.Int64"

Beispiel
C#-Quellcode
Das folgende Beispiel zeigt den Quellcode einer Point Klasse:

namespace Graphics
{

/// <summary>Class <c>Point</c> models a point in a two-dimensional plane.


/// </summary>
public class Point
{

/// <summary>Instance variable <c>x</c> represents the point's


/// x-coordinate.</summary>
private int x;

/// <summary>Instance variable <c>y</c> represents the point's


/// y-coordinate.</summary>
private int y;

/// <value>Property <c>X</c> represents the point's x-coordinate.</value>


public int X
{
get { return x; }
set { x = value; }
}

/// <value>Property <c>Y</c> represents the point's y-coordinate.</value>


public int Y
{
get { return y; }
set { y = value; }
}

/// <summary>This constructor initializes the new Point to


/// (0,0).</summary>
public Point() : this(0,0) {}

/// <summary>This constructor initializes the new Point to


/// (<paramref name="xor"/>,<paramref name="yor"/>).</summary>
/// <param><c>xor</c> is the new Point's x-coordinate.</param>
/// <param><c>yor</c> is the new Point's y-coordinate.</param>
public Point(int xor, int yor) {
X = xor;
Y = yor;
}

/// <summary>This method changes the point's location to


/// the given coordinates.</summary>
/// <param><c>xor</c> is the new x-coordinate.</param>
/// <param><c>yor</c> is the new y-coordinate.</param>
/// <see cref="Translate"/>
public void Move(int xor, int yor) {
X = xor;
Y = yor;
}

/// <summary>This method changes the point's location by


/// the given x- and y-offsets.
/// <example>For example:
/// <code>
/// Point p = new Point(3,5);
/// p.Translate(-1,3);
/// </code>
/// results in <c>p</c>'s having the value (2,8).
/// </example>
/// </summary>
/// <param><c>xor</c> is the relative x-offset.</param>
/// <param><c>yor</c> is the relative y-offset.</param>
/// <see cref="Move"/>
public void Translate(int xor, int yor) {
X += xor;
Y += yor;
}

/// <summary>This method determines whether two Points have the same
/// location.</summary>
/// <param><c>o</c> is the object to be compared to the current object.
/// </param>
/// <returns>True if the Points have the same location and they have
/// the exact same type; otherwise, false.</returns>
/// <seealso cref="operator=="/>
/// <seealso cref="operator!="/>
public override bool Equals(object o) {
if (o == null) {
return false;
}

if (this == o) {
return true;
}

if (GetType() == o.GetType()) {
Point p = (Point)o;
return (X == p.X) && (Y == p.Y);
}
return false;
}

/// <summary>Report a point's location as a string.</summary>


/// <returns>A string representing a point's location, in the form (x,y),
/// without any leading, training, or embedded whitespace.</returns>
public override string ToString() {
return "(" + X + "," + Y + ")";
}

/// <summary>This operator determines whether two Points have the same
/// location.</summary>
/// <param><c>p1</c> is the first Point to be compared.</param>
/// <param><c>p2</c> is the second Point to be compared.</param>
/// <returns>True if the Points have the same location and they have
/// the exact same type; otherwise, false.</returns>
/// <seealso cref="Equals"/>
/// <seealso cref="operator!="/>
public static bool operator==(Point p1, Point p2) {
if ((object)p1 == null || (object)p2 == null) {
return false;
}
if (p1.GetType() == p2.GetType()) {
return (p1.X == p2.X) && (p1.Y == p2.Y);
}

return false;
}

/// <summary>This operator determines whether two Points have the same
/// location.</summary>
/// <param><c>p1</c> is the first Point to be compared.</param>
/// <param><c>p2</c> is the second Point to be compared.</param>
/// <returns>True if the Points do not have the same location and the
/// exact same type; otherwise, false.</returns>
/// <seealso cref="Equals"/>
/// <seealso cref="operator=="/>
public static bool operator!=(Point p1, Point p2) {
return !(p1 == p2);
}

/// <summary>This is the entry point of the Point class testing


/// program.
/// <para>This program tests each method and operator, and
/// is intended to be run after any non-trivial maintenance has
/// been performed on the Point class.</para></summary>
public static void Main() {
// class test code goes here
}
}
}

Resultierende XML
Hier sehen Sie die Ausgabe, die von einem Dokumentations Generator erzeugt wird, wenn der Quellcode für
die-Klasse angegeben Point wird, wie oben gezeigt:

<?xml version="1.0"?>
<doc>
<assembly>
<name>Point</name>
</assembly>
<members>
<member name="T:Graphics.Point">
<summary>Class <c>Point</c> models a point in a two-dimensional
plane.
</summary>
</member>

<member name="F:Graphics.Point.x">
<summary>Instance variable <c>x</c> represents the point's
x-coordinate.</summary>
</member>

<member name="F:Graphics.Point.y">
<summary>Instance variable <c>y</c> represents the point's
y-coordinate.</summary>
</member>

<member name="M:Graphics.Point.#ctor">
<summary>This constructor initializes the new Point to
(0,0).</summary>
</member>

<member name="M:Graphics.Point.#ctor(System.Int32,System.Int32)">
<summary>This constructor initializes the new Point to
(<paramref name="xor"/>,<paramref name="yor"/>).</summary>
<param><c>xor</c> is the new Point's x-coordinate.</param>
<param><c>yor</c> is the new Point's y-coordinate.</param>
</member>

<member name="M:Graphics.Point.Move(System.Int32,System.Int32)">
<summary>This method changes the point's location to
the given coordinates.</summary>
<param><c>xor</c> is the new x-coordinate.</param>
<param><c>yor</c> is the new y-coordinate.</param>
<see cref="M:Graphics.Point.Translate(System.Int32,System.Int32)"/>
</member>

<member
name="M:Graphics.Point.Translate(System.Int32,System.Int32)">
<summary>This method changes the point's location by
the given x- and y-offsets.
<example>For example:
<code>
Point p = new Point(3,5);
p.Translate(-1,3);
</code>
results in <c>p</c>'s having the value (2,8).
</example>
</summary>
<param><c>xor</c> is the relative x-offset.</param>
<param><c>yor</c> is the relative y-offset.</param>
<see cref="M:Graphics.Point.Move(System.Int32,System.Int32)"/>
</member>

<member name="M:Graphics.Point.Equals(System.Object)">
<summary>This method determines whether two Points have the same
location.</summary>
<param><c>o</c> is the object to be compared to the current
object.
</param>
<returns>True if the Points have the same location and they have
the exact same type; otherwise, false.</returns>
<seealso
cref="M:Graphics.Point.op_Equality(Graphics.Point,Graphics.Point)"/>
<seealso
cref="M:Graphics.Point.op_Inequality(Graphics.Point,Graphics.Point)"/>
</member>

<member name="M:Graphics.Point.ToString">
<summary>Report a point's location as a string.</summary>
<returns>A string representing a point's location, in the form
(x,y),
without any leading, training, or embedded whitespace.</returns>
</member>

<member
name="M:Graphics.Point.op_Equality(Graphics.Point,Graphics.Point)">
<summary>This operator determines whether two Points have the
same
location.</summary>
<param><c>p1</c> is the first Point to be compared.</param>
<param><c>p2</c> is the second Point to be compared.</param>
<returns>True if the Points have the same location and they have
the exact same type; otherwise, false.</returns>
<seealso cref="M:Graphics.Point.Equals(System.Object)"/>
<seealso
cref="M:Graphics.Point.op_Inequality(Graphics.Point,Graphics.Point)"/>
</member>

<member
name="M:Graphics.Point.op_Inequality(Graphics.Point,Graphics.Point)">
<summary>This operator determines whether two Points have the
same
location.</summary>
<param><c>p1</c> is the first Point to be compared.</param>
<param><c>p1</c> is the first Point to be compared.</param>
<param><c>p2</c> is the second Point to be compared.</param>
<returns>True if the Points do not have the same location and
the
exact same type; otherwise, false.</returns>
<seealso cref="M:Graphics.Point.Equals(System.Object)"/>
<seealso
cref="M:Graphics.Point.op_Equality(Graphics.Point,Graphics.Point)"/>
</member>

<member name="M:Graphics.Point.Main">
<summary>This is the entry point of the Point class testing
program.
<para>This program tests each method and operator, and
is intended to be run after any non-trivial maintenance has
been performed on the Point class.</para></summary>
</member>

<member name="P:Graphics.Point.X">
<value>Property <c>X</c> represents the point's
x-coordinate.</value>
</member>

<member name="P:Graphics.Point.Y">
<value>Property <c>Y</c> represents the point's
y-coordinate.</value>
</member>
</members>
</doc>
Musterabgleich für c# 7
04.11.2021 • 28 minutes to read

Muster Vergleichs Erweiterungen für c# ermöglichen viele der Vorteile von algebraischen Datentypen und
Musterabgleich von funktionalen Sprachen, aber auf eine Weise, die nahtlos in das Verhalten der zugrunde
liegenden Sprache integriert ist. Die grundlegenden Features sind: Daten Satz Typen, bei denen es sich um
Typen handelt, deren semantische Bedeutung durch die Form der Daten beschrieben wird. und Musterabgleich,
bei dem es sich um ein neues Ausdrucks Formular handelt, das eine extrem präzise Zerlegung dieser
Datentypen ermöglicht. Elemente dieses Ansatzes sind von verwandten Features in den Programmiersprachen F
# und Scalainspiriert.

Is-Ausdruck
Der- is Operator wird erweitert, um einen Ausdruck mit einem Muster zu testen.

relational_expression
: relational_expression 'is' pattern
;

Diese Form von relational_expression ist zusätzlich zu den vorhandenen Formularen in der c#-Spezifikation. Es
handelt sich um einen Kompilierzeitfehler, wenn der relational_expression Links vom is Token keinen Wert
oder keinen Typ hat.
Jeder Bezeichner des Musters führt eine neue lokale Variable ein, die definitiv zugewiesen wird, nachdem der
is Operator ist true (d.h. definitiv zugewiesen, wenn true).

Hinweis: Es gibt technisch gesehen eine Mehrdeutigkeit zwischen dem Typ in einer is-expression -und-
constant_pattern, von denen jede eine gültige Analyse eines qualifizierten Bezeichners sein könnte. Wir
versuchen, Sie als Typ für die Kompatibilität mit früheren Versionen der Sprache zu binden. nur wenn dies
nicht möglich ist, lösen wir es wie in anderen Kontexten, bis zum ersten gefundenen (das entweder eine
Konstante oder ein Typ sein muss). Diese Mehrdeutigkeit ist nur auf der rechten Seite eines is Ausdrucks
vorhanden.

Muster
Muster werden im is -Operator und in einem switch_statement verwendet, um die Form der Daten
auszudrücken, mit denen eingehende Daten verglichen werden sollen. Muster können rekursiv sein, damit Teile
der Daten mit unter Mustern verglichen werden können.
pattern
: declaration_pattern
| constant_pattern
| var_pattern
;

declaration_pattern
: type simple_designation
;

constant_pattern
: shift_expression
;

var_pattern
: 'var' simple_designation
;

Hinweis: Es gibt technisch gesehen eine Mehrdeutigkeit zwischen dem Typ in einer is-expression -und-
constant_pattern, von denen jede eine gültige Analyse eines qualifizierten Bezeichners sein könnte. Wir
versuchen, Sie als Typ für die Kompatibilität mit früheren Versionen der Sprache zu binden. nur wenn dies
nicht möglich ist, lösen wir es wie in anderen Kontexten, bis zum ersten gefundenen (das entweder eine
Konstante oder ein Typ sein muss). Diese Mehrdeutigkeit ist nur auf der rechten Seite eines is Ausdrucks
vorhanden.

Deklarations Muster
Der declaration_pattern testet, ob ein Ausdruck einen bestimmten Typ hat, und wandelt ihn in diesen Typ um,
wenn der Test erfolgreich ist. Wenn die simple_designation ein Bezeichner ist, wird eine lokale Variable des
angegebenen Typs eingeführt, der durch den angegebenen Bezeichner benannt wird. Diese lokale Variable ist
definitiv zugewiesen , wenn das Ergebnis der Muster Vergleichsoperation "true" ist.

declaration_pattern
: type simple_designation
;

Die Lauf Zeit Semantik dieses Ausdrucks besteht darin, dass er den Lauf Zeittyp des linken relational_expression
Operanden mit dem Typ im Muster testet. Wenn es sich um einen Lauf Zeittyp (oder einen Untertyp) handelt, ist
das Ergebnis von is operator true . Er deklariert eine neue lokale Variable mit dem Namen durch den
Bezeichner , dem der Wert des linken Operanden zugewiesen wird, wenn das Ergebnis ist true .
Bestimmte Kombinationen von statischem Typ der linken Seite und des angegebenen Typs werden als nicht
kompatibel betrachtet und führen zu einem Kompilierzeitfehler. Ein Wert vom Typ statischer Typ E wird als
Muster kompatibel mit dem Typ bezeichnet, T Wenn eine Identitäts Konvertierung, eine implizite Verweis
Konvertierung, eine Boxing-Konvertierung, eine explizite Verweis Konvertierung oder eine Unboxing-
Konvertierung von in vorhanden ist E T . Es handelt sich um einen Kompilierzeitfehler, wenn ein Ausdruck
vom Typ E nicht mit dem Typ in einem Typmuster kompatibel ist, mit dem er übereinstimmt.

Hinweis: in c# 7,1 erweitern wir dies , um einen Muster Vergleichs Vorgang zuzulassen, wenn entweder der
Eingabetyp oder der Typ T ein offener Typ ist. Dieser Absatz wird durch Folgendes ersetzt:
Bestimmte Kombinationen von statischem Typ der linken Seite und des angegebenen Typs werden als nicht
kompatibel betrachtet und führen zu einem Kompilierzeitfehler. Ein Wert vom Typ statischer Typ E wird als
Muster kompatibel mit dem Typ bezeichnet, T Wenn eine Identitäts Konvertierung, eine implizite Verweis
Konvertierung, eine Boxing-Konvertierung, eine explizite Verweis Konvertierung oder eine Unboxing-
Konvertierung von in vorhanden ist, E T oder wenn entweder E oder T ein offener Typ ist . Es
handelt sich um einen Kompilierzeitfehler, wenn ein Ausdruck vom Typ E nicht mit dem Typ in einem
Typmuster kompatibel ist, mit dem er übereinstimmt.

Das Deklarations Muster ist nützlich zum Ausführen von Lauf Zeittyp Tests von Verweis Typen und ersetzt die
Ausdrucksweise.

var v = expr as Type;


if (v != null) { // code using v }

Mit etwas präziseren

if (expr is Type v) { // code using v }

Es ist ein Fehler, wenn der Typ ein Werte zulässt-Werttyp ist.
Das Deklarations Muster kann verwendet werden, um Werte von Typen zu testen, die NULL-Werte zulassen: ein
Wert vom Typ Nullable<T> (oder ein schachtelt T ) entspricht einem Typmuster T2 id , wenn der Wert nicht
NULL ist und der Typ von T2 oder ein T Basistyp oder eine Schnittstelle von ist T . Beispielsweise im Code
Fragment

int? x = 3;
if (x is int v) { // code using v }

Die Bedingung der if -Anweisung ist true zur Laufzeit, und die-Variable v enthält den Wert 3 des Typs
int innerhalb des-Blocks.

Konstantenmuster

constant_pattern
: shift_expression
;

Ein konstantes Muster testet den Wert eines Ausdrucks mit einem konstanten Wert. Die Konstante kann ein
beliebiger konstanter Ausdruck sein, z. b. ein Literalwert, der Name einer deklarierten const Variable oder eine
Enumerationskonstante oder ein- typeof Ausdruck.
Wenn sowohl e als auch c ganzzahlige Typen sind, wird das Muster als Übereinstimmung betrachtet, wenn das
Ergebnis des Ausdrucks e == c ist true .
Andernfalls wird das Muster als übereinstimmend betrachtet, wenn object.Equals(e, c) zurückgibt true . In
diesem Fall handelt es sich um einen Kompilierzeitfehler, wenn der statische Typ von e nicht mit dem Typ der
Konstante kompatibel ist.
Var-Muster

var_pattern
: 'var' simple_designation
;

Ein Ausdruck e stimmt mit einem var_pattern immer überein. Anders ausgedrückt, eine Entsprechung für ein
var-Muster ist immer erfolgreich. Wenn die simple_designation ein Bezeichner ist, wird zur Laufzeit der Wert
von e an eine neu eingeführte lokale Variable gebunden. Der Typ der lokalen Variablen ist der statische Typ von
e.
Wenn der Name var an einen Typ gebunden ist, ist ein Fehler aufgetreten.

switch-Anweisung
Die- switch Anweisung wird erweitert, um für die Ausführung auszuwählen. der erste Block verfügt über ein
zugeordnetes Muster, das dem Switch-Ausdruck entspricht

switch_label
: 'case' complex_pattern case_guard? ':'
| 'case' constant_expression case_guard? ':'
| 'default' ':'
;

case_guard
: 'when' expression
;

Die Reihenfolge, in der Muster abgeglichen werden, ist nicht definiert. Ein Compiler ist berechtigt, Muster in
falscher Reihenfolge abzugleichen und die Ergebnisse von bereits übereinstimmenden Mustern
wiederzuverwenden, um das Ergebnis der Übereinstimmung von anderen Mustern zu berechnen.
Wenn ein Case-Guard vorhanden ist, ist der Ausdruck vom Typ bool . Es wird als zusätzliche Bedingung
ausgewertet, die erfüllt werden muss, damit die Groß-/Kleinschreibung als erfüllt betrachtet wird.
Es tritt ein Fehler auf, wenn ein switch_label zur Laufzeit keine Auswirkung haben kann, da sein Muster in
vorherigen Fällen subsudiert wird. [TODO: Wir sollten präziser zu den Techniken sein, die der Compiler zum
Erreichen dieses Urteils benötigt.]
Eine in einem switch_label deklarierte Muster Variable wird in Ihrem Fall Block definitiv zugewiesen, wenn dieser
Fall Block genau einen switch_label enthält.
[TODO: Wir sollten angeben, wann ein switch-Block erreichbar ist.]
Bereich von Muster Variablen
Der Gültigkeitsbereich einer Variablen, die in einem Muster deklariert ist, lautet wie folgt:
Wenn das Muster eine Case-Bezeichnung ist, ist der Gültigkeitsbereich der Variablen der Case-Block.
Andernfalls wird die Variable in einem is_pattern Ausdruck deklariert, und ihr Bereich basiert auf dem-
Konstrukt, das den Ausdruck, der den is_pattern Ausdruck enthält, unmittelbar wie folgt umschließt:
Wenn sich der Ausdruck in einem Ausdrucks Körper-Lambda befindet, ist sein Bereich der Text des Lambda-
Ausdrucks.
Wenn sich der Ausdruck in einer Ausdrucks Körper-Methode oder-Eigenschaft befindet, ist sein Bereich der
Text der Methode oder Eigenschaft.
Wenn der Ausdruck in einer Klausel einer Klausel enthalten ist when catch , ist sein Bereich diese catch
Klausel.
Wenn sich der Ausdruck in einem iteration_statement befindet, ist sein Bereich nur diese Anweisung.
Andernfalls ist der Gültigkeitsbereich der Bereich, der die Anweisung enthält, wenn sich der Ausdruck in
einem anderen Anweisungs Formular befindet.
Zum Ermitteln des Bereichs wird ein embedded_statement als in seinem eigenen Bereich betrachtet.
Beispielsweise ist die Grammatik für eine if_statement
if_statement
: 'if' '(' boolean_expression ')' embedded_statement
| 'if' '(' boolean_expression ')' embedded_statement 'else' embedded_statement
;

Wenn die gesteuerte Anweisung einer if_statement eine Muster Variable deklariert, ist der Gültigkeitsbereich auf
diese embedded_statement beschränkt:

if (x) M(y is var z);

In diesem Fall ist der Gültigkeitsbereich von z die eingebettete Anweisung M(y is var z); .
Andere Fälle sind Fehler aus anderen Gründen (z. b. im Standardwert eines Parameters oder in einem Attribut,
bei denen es sich um einen Fehler handelt, da für diese Kontexte ein konstanter Ausdruck erforderlich ist).

In c# 7,3 haben wir die folgenden Kontexte hinzugefügt , in denen eine Muster Variable deklariert werden
kann:
Wenn sich der Ausdruck in einem Konstruktorinitialisierer befindet, ist sein Bereich der
Konstruktorinitialisierer und der Text des Konstruktors.
Wenn sich der Ausdruck in einem Feldinitialisierer befindet, ist sein Bereich der equals_value_clause , in
dem er angezeigt wird.
Wenn der Ausdruck in einer Abfrage Klausel enthalten ist, die angegeben wird, dass Sie in den Text eines
Lambda-Ausdrucks übersetzt werden soll, ist der Gültigkeitsbereich nur dieser Ausdruck.

Änderungen an syntaktischer Eindeutigkeit


Es gibt Situationen, in denen die c#-Grammatik mehrdeutig ist, und die Sprachspezifikation gibt an, wie diese
Mehrdeutigkeiten gelöst werden können:
7.6.5.2 Grammatik Mehrdeutigkeiten
Die Produktion für einfachen Namen ("7.6.3") und " Member-Access " (§ 7.6.5) kann zu Mehrdeutigkeiten in
der Grammatik für Ausdrücke führen. Beispielsweise ist die-Anweisung:

F(G<A,B>(7));

kann als ein F -Befehl mit zwei Argumenten (und) interpretiert werden G < A B > (7) . Alternativ könnte
Sie auch als ein-Rückruf F mit einem Argument interpretiert werden, bei dem es sich um einen
aufgerufenen Vorgang einer generischen Methode G mit zwei Typargumenten und einem regulären
Argument handelt.

Wenn eine Sequenz von Token (im Kontext) als Simple-Name (7.6.3), Element -Access (§ 7.6.5) oder Pointer-
Member-Access (§ 18.5.2) analysiert werden kann, die mit einer Type-Argument-List (§ 4.4.1) endet, wird das
Token, das unmittelbar auf das schließende Token folgt, > untersucht. Wenn eine von

( ) ] } : ; , . ? == != | ^

Anschließend wird die Type-Argument-List als Teil des Simple-namens, des Mitglieds Zugriffs oder des
Zeigers für den Zeiger Zugriff aufbewahrt, und jede andere mögliche Analyse der Sequenz von Token wird
verworfen. Andernfalls wird die Type-Argument-List nicht als Teil des Simple-namens, des Member-Access -
oder > Pointer-Member-Access betrachtet, auch wenn es keine andere Möglichkeit gibt, die Sequenz von
Token zu analysieren. Beachten Sie, dass diese Regeln beim Parsen einer Type-Argument-List in einem
Namespace-oder-Type-Name (§ 3,8) nicht angewendet werden. Die Anweisung

F(G<A,B>(7));

wird gemäß dieser Regel als ein-Rückruf F mit einem Argument interpretiert, bei dem es sich um einen
Aufrufe einer generischen Methode G mit zwei Typargumenten und einem regulären Argument handelt.
Die Anweisungen

F(G < A, B > 7);


F(G < A, B >> 7);

wird jeweils als ein-Rückruf F mit zwei Argumenten interpretiert. Die Anweisung

x = F < A > +y;

wird als ein kleiner-als-Operator, größer als-Operator und Unärer Plus-Operator interpretiert, als ob die
Anweisung geschrieben worden wäre x = (F < A) > (+y) , anstatt als einfacher Name mit einer Type-
Argument-List gefolgt von einem binären Plus-Operator. In der-Anweisung

x = y is C<T> + z;

die Token C<T> werden als Namespace-oder-Typname mit einer Type-Argument-List interpretiert.

In c# 7 wurden eine Reihe von Änderungen eingeführt, die diese mehrdeutigkeits Regeln nicht mehr ausreichen,
um die Komplexität der Sprache zu verarbeiten.
out-Variablendeklaration
Es ist nun möglich, eine Variable in einem out-Argument zu deklarieren:

M(out Type name);

Der Typ kann jedoch generisch sein:

M(out A<B> name);

Da die Sprachgrammatik für das-Argument Expression verwendet, unterliegt dieser Kontext der
disambiguations-Regel. In diesem Fall > folgt eine Kennung, bei der es sich nicht um eines der Token handelt,
mit dem Sie als Type-Argument-List behandelt werden kann. Ich schlage daher vor, einen Bezeichner zu dem
Satz von Token hinzuzufügen, der die Eindeutigkeit einer Type-Argument-List auslöst.
Tupel-und Debug-Deklarationen
Ein tupelliteral wird genau das gleiche Problem haben. Beachten Sie den Tupelausdruck.

(A < B, C > D, E < F, G > H)

Im Rahmen der alten c# 6-Regeln zum Analysieren einer Argumentliste würde dies als Tupel mit vier Elementen
analysiert werden, beginnend mit A < B dem ersten. Wenn dies jedoch auf der linken Seite einer Debug-Datei
angezeigt wird, soll die Unterscheidung wie oben beschrieben durch das Bezeichnertoken ausgelöst werden:
(A<B,C> D, E<F,G> H) = e;

Dabei handelt es sich um eine Dekonstruktions Deklaration, die zwei Variablen deklariert, wobei die erste den
Typ A<B,C> und den Namen hat D . Das heißt, das tupelliterale enthält zwei Ausdrücke, von denen jeder ein
Deklarations Ausdruck ist.
Aus Gründen der Einfachheit der Spezifikation und des Compilers schlage ich vor, dass dieses tupelliterals ein
Tupel mit zwei Elementen analysiert wird, wo es angezeigt wird (unabhängig davon, ob es auf der linken Seite
einer Zuweisung angezeigt wird). Dies wäre ein natürliches Ergebnis der Eindeutigkeit, die im vorherigen
Abschnitt beschrieben wurde.
Muster Vergleich
Der Muster Vergleich führt einen neuen Kontext ein, bei dem die Ausdrucks typmehrdeutigkeit auftritt. Zuvor
war die Rechte Seite eines is Operators ein Typ. Nun kann es sich um einen Typ oder einen Ausdruck handeln,
und wenn es sich um einen Typ handelt, kann ein Bezeichner folgen. Dies kann technisch gesehen die Bedeutung
von vorhandenem Code ändern:

var x = e is T < A > B;

Diese kann unter c# 6-Regeln analysiert werden als

var x = ((e is T) < A) > B;

unter "c# 7"-Regeln (mit der oben vorgeschlagenen Mehrdeutigkeit) werden jedoch als

var x = e is T<A> B;

, die eine Variable B vom Typ deklariert T<A> . Glücklicherweise haben die systemeigenen und Roslyn-
Compiler einen Fehler, wenn Sie einen Syntax Fehler im c# 6-Code verursachen. Daher ist diese spezielle
Breaking Change kein Problem.
Bei Pattern-Matching werden zusätzliche Token eingeführt, die die mehrdeutigkeitsauflösung bei der Auswahl
eines Typs steuern. Die folgenden Beispiele für vorhandenen gültigen c# 6-Code werden ohne zusätzliche
mehrdeutigkeits Regeln getrennt:

var x = e is A<B> && f; // &&


var x = e is A<B> || f; // ||
var x = e is A<B> & f; // &
var x = e is A<B>[]; // [

Vorgeschlagene Änderung der Eindeutigkeits Regel


Ich schlage vor, die Spezifikation zu überarbeiten, um die Liste der mehrdeutigkeits Token zu ändern.

( ) ] } : ; , . ? == != | ^

zu

( ) ] } : ; , . ? == != | ^ && || & [
In bestimmten Kontexten behandeln wir den Bezeichner als Eindeutigkeits Token. In diesen Kontexten wird der
Sequenz von Token, die unterschieden werden, unmittelbar eine der Schlüsselwörter is , case , oder out
vorangestellt, oder es tritt beim Parsen des ersten Elements eines tupelliterals auf (in diesem Fall werden die
Token oder vorangestellt, ( : und der Bezeichner folgt einem , ) oder einem nachfolgenden Element eines
tupelliterals.
Geänderte disambiguations-Regel
Die überarbeitete Regel für die Mehrdeutigkeit würde etwa wie folgt aussehen.

Wenn eine Sequenz von Token (im Kontext) als Simple-Name (7.6.3), Element -Access (§ 7.6.5) oder Pointer-
Member-Access (§ 18.5.2) analysiert werden kann, die mit einer Type-Argument-List (§ 4.4.1) endet, wird das
Token, das unmittelbar auf das schließende Token folgt, > untersucht, um festzustellen, ob es
Einer von ( ) ] } : ; , . ? == != | ^ && || & [ , oder.
Einer der relationalen Operatoren < > <= >= is as ; oder
Ein Kontext abhängiges Abfrage Schlüsselwort in einem Abfrage Ausdruck. noch
In bestimmten Kontexten behandeln wir den Bezeichner als Eindeutigkeits Token. In diesen Kontexten
wird der Sequenz von Token, die unterschieden werden, direkt eines der Schlüsselwörter vorangestellt
is , case oder, oder es wird out beim Parsen des ersten Elements eines tupelliterals vorangestellt (in
diesem Fall werden den Token oder vorangestellt, ( : und der Bezeichner folgt einem , ) oder einem
nachfolgenden Element eines tupelliterals.
Wenn das folgende Token in dieser Liste enthalten ist, oder ein Bezeichner in einem solchen Kontext, wird
die Type-Argument-List als Teil des Simple-namens, des Element Zugriffs oder des Zeigers des Zeiger
Members beibehalten, und jede andere mögliche Analyse der Sequenz von Token wird verworfen.
Andernfalls wird die Type-Argument-List nicht als Bestandteil des Simple-namens, des Element Zugriffs oder
des Zeigers für Zeiger Elemente angesehen, auch wenn es keine andere Möglichkeit gibt, die Sequenz von
Token zu analysieren. Beachten Sie, dass diese Regeln beim Parsen einer Type-Argument-List in einem
Namespace-oder-Type-Name (§ 3,8) nicht angewendet werden.

Wichtige Änderungen aufgrund dieses Angebots


Aufgrund dieser vorgeschlagenen mehrdeutigkeits Regel sind keine wichtigen Änderungen bekannt.
Interessante Beispiele
Im folgenden finden Sie einige interessante Ergebnisse dieser mehrdeutigkeits Regeln:
Der Ausdruck (A < B, C > D) ist ein Tupel mit zwei Elementen, bei denen es sich jeweils um einen Vergleich
handelt.
Der Ausdruck (A<B,C> D, E) ist ein Tupel mit zwei Elementen, wobei der erste ein Deklarations Ausdruck ist.
Der Aufruf M(A < B, C > D, E) hat drei Argumente.
Der Aufruf M(out A<B,C> D, E) hat zwei Argumente, wobei der erste eine out Deklaration ist.
Der Ausdruck e is A<B> C verwendet einen Deklarations Ausdruck.
Die Case-Bezeichnung case A<B> C: verwendet einen Deklarations Ausdruck.

Einige Beispiele für den Musterabgleich


Is-As
Wir können die Ausdrucksweise ersetzen.
var v = expr as Type;
if (v != null) {
// code using v
}

Mit etwas präziseren und direkteren

if (expr is Type v) {
// code using v
}

Testen von NULL -Werten


Wir können die Ausdrucksweise ersetzen.

Type? v = x?.y?.z;
if (v.HasValue) {
var value = v.GetValueOrDefault();
// code using value
}

Mit etwas präziseren und direkteren

if (x?.y?.z is Type value) {


// code using value
}

Arithmetische Vereinfachung
Angenommen, wir definieren eine Reihe von rekursiven Typen zur Darstellung von Ausdrücken (gemäß einem
separaten Vorschlag):

abstract class Expr;


class X() : Expr;
class Const(double Value) : Expr;
class Add(Expr Left, Expr Right) : Expr;
class Mult(Expr Left, Expr Right) : Expr;
class Neg(Expr Value) : Expr;

Nun können wir eine Funktion definieren, um die (nicht reduzierte) Ableitung eines Ausdrucks zu berechnen:

Expr Deriv(Expr e)
{
switch (e) {
case X(): return Const(1);
case Const(*): return Const(0);
case Add(var Left, var Right):
return Add(Deriv(Left), Deriv(Right));
case Mult(var Left, var Right):
return Add(Mult(Deriv(Left), Right), Mult(Left, Deriv(Right)));
case Neg(var Value):
return Neg(Deriv(Value));
}
}

Ein Ausdrucks vereinfachende zeigt Positions Muster:


Expr Simplify(Expr e)
{
switch (e) {
case Mult(Const(0), *): return Const(0);
case Mult(*, Const(0)): return Const(0);
case Mult(Const(1), var x): return Simplify(x);
case Mult(var x, Const(1)): return Simplify(x);
case Mult(Const(var l), Const(var r)): return Const(l*r);
case Add(Const(0), var x): return Simplify(x);
case Add(var x, Const(0)): return Simplify(x);
case Add(Const(var l), Const(var r)): return Const(l+r);
case Neg(Const(var k)): return Const(-k);
default: return e;
}
}
Lokale Funktionen
04.11.2021 • 3 minutes to read

C# wird erweitert, um die Deklaration von Funktionen im Block Bereich zu unterstützen. Lokale Funktionen
können (Capture)-Variablen aus dem einschließenden Gültigkeitsbereich verwenden.
Der Compiler erkennt anhand der Datenflussanalyse, welche Variablen eine lokale Funktion verwendet, bevor
Sie Ihr einen Wert zuweist. Jeder Aufrufe der Funktion erfordert, dass solche Variablen definitiv zugewiesen
werden. Auf ähnliche Weise bestimmt der Compiler, welche Variablen bei Rückgabe definitiv zugewiesen
werden. Diese Variablen werden als definitiv zugewiesen, nachdem die lokale Funktion aufgerufen wurde.
Lokale Funktionen können vor ihrer Definition von einem lexikalischen Punkt aus aufgerufen werden. Lokale
Funktions Deklarations Anweisungen verursachen keine Warnung, wenn Sie nicht erreichbar sind.
TODO: Spezifikation schreiben

Syntax Grammatik
Diese Grammatik wird als diff von der aktuellen Spezifikations Grammatik dargestellt.

declaration-statement
: local-variable-declaration ';'
| local-constant-declaration ';'
+ | local-function-declaration
;

+local-function-declaration
+ : local-function-header local-function-body
+ ;

+local-function-header
+ : local-function-modifiers? return-type identifier type-parameter-list?
+ ( formal-parameter-list? ) type-parameter-constraints-clauses
+ ;

+local-function-modifiers
+ : (async | unsafe)
+ ;

+local-function-body
+ : block
+ | arrow-expression-body
+ ;

Lokale Funktionen können im einschließenden Bereich definierte Variablen verwenden. Die aktuelle
Implementierung erfordert, dass jede in einer lokalen Funktion gelesene Variable definitiv zugewiesen wird, als
ob die lokale Funktion an ihrer Definitions Stelle ausgeführt wird. Außerdem muss die Definition der lokalen
Funktion an jedem beliebigen Verwendungs Punkt "ausgeführt" sein.
Nachdem Sie etwas ausprobiert haben (es ist z. b. nicht möglich, zwei gegenseitig rekursive lokale Funktionen
zu definieren), haben wir seitdem überarbeitet, wie die definitive Zuweisung funktionieren soll. Die Revision
(noch nicht implementiert) besteht darin, dass alle lokalen Variablen, die in einer lokalen Funktion gelesen
werden, bei jedem Aufruf der lokalen Funktion definitiv zugewiesen werden müssen. Das ist wirklich etwas
feiner als das, und es gibt eine Reihe von verbleibenden arbeiten, um die Arbeit zu vereinfachen. Nachdem der
Vorgang abgeschlossen ist, können Sie Ihre lokalen Funktionen an das Ende des einschließenden Blocks
verschieben.
Die neuen eindeutigen Zuweisungs Regeln sind nicht mit dem Ableiten des Rückgabe Typs einer lokalen
Funktion kompatibel. Daher wird die Unterstützung für das Ableiten des Rückgabe Typs wahrscheinlich entfernt.
Wenn Sie eine lokale Funktion nicht in einen Delegaten konvertieren, erfolgt die Erfassung in Frames, die
Werttypen sind. Dies bedeutet, dass Sie keinen GC-Druck durch die Verwendung lokaler Funktionen mit der
Erfassung erhalten.
Reachability
Wir fügen die Spezifikation hinzu.

Der Text eines Lambda-Ausdrucks oder einer lokalen Funktion mit Anweisungs Körper ist als erreichbar.
out-Variablendeklaration
04.11.2021 • 2 minutes to read

Mit der Deklaration der Out-Variablen Deklaration kann eine Variable an dem Speicherort deklariert werden, an
dem Sie als Argument übermittelt wird out .

argument_value
: 'out' type identifier
| ...
;

Eine auf diese Weise deklarierte Variable wird als out-Variable bezeichnet. Sie können das Kontext var
Schlüsselwort für den Variablentyp verwenden. Der Gültigkeitsbereich ist derselbe wie für eine Pattern-Variable ,
die durch Muster Übereinstimmung eingeführt wurde.
Gemäß der Sprachspezifikation (Abschnitt 7.6.7 Element Access) enthält die Argument-List eines Element
Zugriffs (Index Ausdruck) keine ref-oder out-Argumente. Allerdings werden Sie vom Compiler für verschiedene
Szenarien zugelassen, z. b. Indexer, die in Metadaten deklariert sind, die akzeptieren out .
Innerhalb des Gültigkeits Bereichs einer lokalen Variablen, die von einem argument_value eingeführt wurde,
handelt es sich um einen Kompilierzeitfehler, der auf diese lokale Variable in einer Textposition verweist, die der
Deklaration vorangestellt ist.
Es ist auch ein Fehler, auf eine implizit typisierte (§ 8.5.1) out-Variable in derselben Argumentliste zu verweisen,
die sofort Ihre Deklaration enthält.
Die Überladungs Auflösung wird wie folgt geändert:
Wir fügen eine neue Konvertierung hinzu:

Es gibt eine Konvertierung von einem Ausdruck aus einer implizit typisierten out-Variablen Deklaration in
jeden Typ.

Außerdem

Der Typ eines explizit typisierten out-Variablen Arguments ist der deklarierte Typ.

und

Ein implizit typisiertes out-Variablen Argument weist keinen Typ auf.

Die Konvertierung von einem Ausdruck aus einer implizit typisierten out-Variablen Deklaration ist nicht besser
als eine andere Konvertierung von Ausdruck.
Der Typ der implizit typisierten out-Variablen ist der Typ des entsprechenden Parameters in der Signatur der
Methode, die von der Überladungs Auflösung ausgewählt wird.
Der neue Syntax Knoten DeclarationExpressionSyntax wird hinzugefügt, um die Deklaration in einem out var-
Argument darzustellen.
Throw-Ausdruck
04.11.2021 • 2 minutes to read

Wir erweitern den Satz von Ausdrucks Formularen auf include.

throw_expression
: 'throw' null_coalescing_expression
;

null_coalescing_expression
: throw_expression
;

Die Typregeln lauten wie folgt:


Ein throw_expression weist keinen Typ auf.
Eine throw_expression kann durch eine implizite Konvertierung in jeden Typ konvertiert werden.
Ein throw-Ausdruck löst den Wert aus, der durch Auswerten des null_coalescing_expression erzeugt wird, der
einen Wert des Klassen Typs angeben muss System.Exception , eines Klassen Typs, der von abgeleitet wird,
System.Exception oder von einem typparametertyp, der System.Exception (oder einer Unterklasse) als effektive
Basisklasse aufweist. Wenn die Auswertung des Ausdrucks erzeugt null , System.NullReferenceException wird
stattdessen eine ausgelöst.
Das Verhalten zur Laufzeit der Auswertung eines Throw- Ausdrucks entspricht dem für eine throw-Anweisung
angegebenenVerhalten.
Die Regeln für die Fluss Analyse lauten wie folgt:
Für jede Variable v wird v definitiv vor dem null_coalescing_expression eines throw_expression IFF
zugewiesen, dass es definitiv vor dem throw_expression zugewiesen wird.
Für jede Variable v wird v nach throw_expression definitiv zugewiesen.
Ein throw-Ausdruck ist nur in den folgenden syntaktischen Kontexten zulässig:
Als zweiter oder dritter Operand eines ternären bedingten Operators ?:
Als zweiter Operand eines NULL-Sammel Operators ??
Als Text eines Ausdrucks Körper Ausdrucks oder einer Methode.
Binäre Literale
04.11.2021 • 2 minutes to read

Es gibt eine relativ häufige Anforderung zum Hinzufügen von binären literalen zu c# und VB. Für Bitmasken (z. b.
Flag-Aufstände) scheint dies wirklich nützlich zu sein, aber es wäre auch für Schulungszwecke gut geeignet.
Binäre Literale würden wie folgt aussehen:

int nineteen = 0b10011;

Syntaktisch und semantisch sind Sie mit hexadezimalen Literalen identisch, mit Ausnahme b / B der
Verwendung von anstelle von x / X , wobei nur Ziffern 0 und 1 und in Basis 2 anstelle von 16 interpretiert
werden.
Es gibt wenig Kosten für die Implementierung dieser und wenig konzeptionellen Aufwand für Benutzer der
Sprache.

Syntax
Die Grammatik sieht wie folgt aus:

integer-literal:
: ...
| binary-integer-literal
;
binary-integer-literal:
: `0b` binary-digits integer-type-suffix-opt
| `0B` binary-digits integer-type-suffix-opt
;
binary-digits:
: binary-digit
| binary-digits binary-digit
;
binary-digit:
: `0`
| `1`
;
Zahlentrennzeichen
04.11.2021 • 2 minutes to read

Die Möglichkeit, Ziffern in großen numerischen Literalen zu gruppieren, hätte eine gute Lesbarkeit und keinen
signifikanten Nachteil.
Durch das Hinzufügen von binären Literalen (#215) wird die Wahrscheinlichkeit erhöht, dass numerische
Literale lang sind, sodass sich die beiden Features gegenseitig verbessern.
Wir folgen Java und anderen und verwenden einen Unterstrich _ als Ziffern Trennzeichen. Sie wäre in der Lage,
überall in einem numerischen Literalformat (mit Ausnahme des ersten und des letzten Zeichens) zu
vorkommen, da unterschiedliche Gruppierungen in verschiedenen Szenarios und insbesondere in
verschiedenen numerischen Basen sinnvoll sein können:

int bin = 0b1001_1010_0001_0100;


int hex = 0x1b_a0_44_fe;
int dec = 33_554_432;
int weird = 1_2__3___4____5_____6______7_______8________9;
double real = 1_000.111_1e-1_000;

Jede Sequenz von Ziffern kann durch Unterstriche getrennt werden, möglicherweise mehr als ein Unterstrich
zwischen zwei aufeinander folgenden Ziffern. Sie sind in Dezimalzahlen und Exponenten zulässig, aber nach der
vorherigen Regel werden Sie möglicherweise nicht neben dem Dezimal 10_.0 Trennzeichen (), neben dem
Exponentenzeichen ( 1.1e_1 ) oder neben dem Typspezifizierer ( 10_f ) angezeigt. Wenn Sie in binären und
hexadezimalen Literalen verwendet werden, werden Sie möglicherweise nicht direkt nach 0x oder angezeigt
0b .

Die Syntax ist einfach, und die Trennzeichen haben keine semantischen Auswirkungen. Sie werden einfach
ignoriert.
Dies hat einen Breitenwert und lässt sich leicht implementieren.
Async-Aufgaben Typen in C #
04.11.2021 • 5 minutes to read

Erweitert, async um Aufgaben Typen zu unterstützen, die einem bestimmten Muster entsprechen, zusätzlich zu
den bekannten Typen System.Threading.Tasks.Task und System.Threading.Tasks.Task<T> .

Aufgabentyp
Ein Tasktyp ist ein oder ein zugeordneter Builder-Typ, der class mit gekennzeichnet ist
struct
System.Runtime.CompilerServices.AsyncMethodBuilderAttribute . Der Tasktyp kann nicht generisch sein, bei
asynchronen Methoden, die keinen Wert zurückgeben, oder generisch für Methoden, die einen Wert
zurückgeben.
Zur Unterstützung von await muss der Tasktyp über eine entsprechende, barrierefreie GetAwaiter() Methode
verfügen, die eine Instanz eines akellner-Typs zurückgibt (Weitere Informationen finden Sie unter c# 7.7.7.1
awanutzbare Ausdrücke).

[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
class MyTask<T>
{
public Awaiter<T> GetAwaiter();
}

class Awaiter<T> : INotifyCompletion


{
public bool IsCompleted { get; }
public T GetResult();
public void OnCompleted(Action completion);
}

Builder-Typ
Der Builder-Typ ist ein class oder struct , der dem jeweiligen Tasktyp entspricht. Der Builder-Typ kann
höchstens einen Typparameter aufweisen und darf nicht in einem generischen Typ eingefügt werden. Der
Builder-Typ verfügt über die folgenden public Methoden. Bei nicht generischen Generator Typen SetResult()
weist keine Parameter auf.
class MyTaskMethodBuilder<T>
{
public static MyTaskMethodBuilder<T> Create();

public void Start<TStateMachine>(ref TStateMachine stateMachine)


where TStateMachine : IAsyncStateMachine;

public void SetStateMachine(IAsyncStateMachine stateMachine);


public void SetException(Exception exception);
public void SetResult(T result);

public void AwaitOnCompleted<TAwaiter, TStateMachine>(


ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine;

public MyTask<T> Task { get; }


}

Ausführung
Die oben genannten Typen werden vom Compiler verwendet, um den Code für den Zustands Automaten einer
Methode zu generieren async . (Der generierte Code entspricht dem Code, der für Async-Methoden generiert
wird, die Task , oder zurückgeben Task<T> void . Der Unterschied besteht darin, dass für bekannte Typen die
Generator Typen auch dem Compiler bekannt sind.)
Builder.Create() wird aufgerufen, um eine Instanz des Builder-Typs zu erstellen.
Wenn der Zustands Automat als implementiert wird struct , builder.SetStateMachine(stateMachine) wird mit
einer geboxten Instanz des Zustands Automaten aufgerufen, die der Generator bei Bedarf zwischenspeichern
kann.
builder.Start(ref stateMachine) wird aufgerufen, um den Generator der vom Compiler generierten Zustands
Automaten Instanz zuzuordnen. Der Generator muss stateMachine.MoveNext() entweder in Start() oder nach
Start() zurückgegeben, um den Zustands Automat zu verschieben. Nachdem Start() zurückgegeben wurde,
ruft die-Methode auf, async builder.Task damit der Task von der Async-Methode zurückgegeben wird.
Bei jedem-Vorgang stateMachine.MoveNext() wird der Zustands Automat fortsetzen. Wenn der Zustands
Automat erfolgreich abgeschlossen wird, builder.SetResult() wird aufgerufen und mit dem Rückgabewert der
Methode (sofern vorhanden). Wenn eine Ausnahme im Zustands Automat ausgelöst wird,
builder.SetException(exception) wird aufgerufen.

Wenn der Zustands Automat einen await expr Ausdruck erreicht, expr.GetAwaiter() wird aufgerufen. Wenn
der akellner implementiert ICriticalNotifyCompletion und IsCompleted false ist, ruft der Zustands Automat auf
builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine) . AwaitUnsafeOnCompleted() sollte
awaiter.OnCompleted(action) mit einer Aktion aufrufen, die aufruft, stateMachine.MoveNext() Wenn der akellner
abgeschlossen ist. Gleiches gilt für INotifyCompletion und builder.AwaitOnCompleted() .

Overload Resolution
Die Überladungs Auflösung wird erweitert, sodass zusätzlich zu und Aufgaben Typen erkannt werden Task
Task<T> .

Ein async Lambda ohne Rückgabewert ist eine genaue Entsprechung für einen Überladungs Kandidaten
Parameter eines nicht generischen Tasktyps, und ein async Lambda mit dem Rückgabetyp T ist eine genaue
Entsprechung für einen Überladungs Kandidaten Parameter des generischen Aufgaben Typs.
Andernfalls, wenn ein async Lambda-Ausdruck keine genaue Entsprechung für einen der beiden Kandidaten
Parameter von Aufgaben Typen oder eine genaue Entsprechung für beides ist und es eine implizite
Konvertierung von einem Kandidaten Typen in einen anderen gibt, gewinnt der von Candidate. Überprüfen Sie
andernfalls die Typen A und B innerhalb von Task1<A> und, Task2<B> um eine bessere Entsprechung zu
erzielen.
Andernfalls, wenn ein async Lambda-Ausdruck keine genaue Entsprechung für einen der beiden Kandidaten
Parameter von Aufgaben Typen ist, aber ein Kandidat ein spezialisierteren Typ als der andere ist, gewinnt der
speziellere Kandidat.
Async-Haupt-
04.11.2021 • 4 minutes to read

[x] vorgeschlagen
[] Prototyp
[]-Implementierung
[]-Spezifikation

Zusammenfassung
Ermöglicht await die Verwendung in der Main/EntryPoint-Methode einer Anwendung, indem der EntryPoint
zurückgegeben Task / Task<int> und gekennzeichnet werden kann async .

Motivation
Beim Erlernen von c#, beim Schreiben von konsolenbasierten Dienstprogrammen und beim Schreiben von
kleinen Test-apps, um-und- await async Methoden von Main aufzurufen, kommt es sehr häufig vor.
Heutzutage fügen wir hier eine Komplexität hinzu, indem wir eine solche await "ung" in einer separaten Async-
Methode erzwingen. Dies bewirkt, dass Entwickler Code Bausteine wie die folgenden schreiben müssen, um die
ersten Schritte zu starten:

public static void Main()


{
MainAsync().GetAwaiter().GetResult();
}

private static async Task MainAsync()


{
... // Main body here
}

Wir können die Notwendigkeit dieses Bausteine entfernen und den Einstieg vereinfachen, indem wir einfach nur
den Hauptbenutzer async so einrichten, dass await er in ihm verwendet werden kann.

Detaillierter Entwurf
Die folgenden Signaturen sind zurzeit zulässige entryPoints:

static void Main()


static void Main(string[])
static int Main()
static int Main(string[])

Wir erweitern die Liste der zulässigen entryPoints um Folgendes:

static Task Main()


static Task<int> Main()
static Task Main(string[])
static Task<int> Main(string[])

Um Kompatibilitäts Risiken zu vermeiden, werden diese neuen Signaturen nur als gültige Einstiegspunkte
betrachtet, wenn keine über Ladungen der vorherigen Menge vorhanden sind. Die Sprache/der Compiler
erfordert nicht, dass der Einstiegspunkt als markiert wird async , obwohl wir erwarten, dass die meisten
Verwendungen als solche gekennzeichnet werden.
Wenn einer dieser Methoden als EntryPoint identifiziert wird, erstellt der Compiler eine tatsächliche EntryPoint-
Methode, die eine dieser codierten Methoden aufruft:
static Task Main() führt dazu, dass der Compiler das Äquivalent von ausgibt.
private static void $GeneratedMain() => Main().GetAwaiter().GetResult();
static Task Main(string[]) führt dazu, dass der Compiler das Äquivalent von ausgibt.
private static void $GeneratedMain(string[] args) => Main(args).GetAwaiter().GetResult();
static Task<int> Main() führt dazu, dass der Compiler das Äquivalent von ausgibt.
private static int $GeneratedMain() => Main().GetAwaiter().GetResult();
static Task<int> Main(string[]) führt dazu, dass der Compiler das Äquivalent von ausgibt.
private static int $GeneratedMain(string[] args) => Main(args).GetAwaiter().GetResult();

Beispielverwendung:

using System;
using System.Net.Http;

class Test
{
static async Task Main(string[] args) =>
Console.WriteLine(await new HttpClient().GetStringAsync(args[0]));
}

Nachteile
Der Hauptnachteil ist einfach die zusätzliche Komplexität der Unterstützung zusätzlicher EntryPoint-Signaturen.

Alternativen
Weitere Varianten, die berücksichtigt werden:
Zulassen von async void . Wir müssen die Semantik für Code, der Sie direkt aufruft, unverändert lassen,
wodurch es für einen generierten EntryPoint schwierig wird, ihn aufzurufen (keine Aufgabe zurückgegeben). Wir
könnten dies beheben, indem wir zwei andere Methoden erstellen, z. b.

public static async void Main()


{
... // await code
}

Vervollständigung

public static async void Main() => await $MainTask();

private static void $EntrypointMain() => Main().GetAwaiter().GetResult();

private static async Task $MainTask()


{
... // await code
}
Es gibt auch Bedenken hinsichtlich der Verwendung von async void .
Verwenden von "mainasync" anstelle von "Main" als Name. Obwohl das Async-Suffix für Methoden
zurückgegeben wird, die Methoden zurückgeben, liegt das in erster Linie in der Bibliotheks Funktionalität, bei
der es sich nicht um den Hauptschlüssel handelt, und die Unterstützung zusätzlicher entrypointnamen über
"Main" hinaus.

Nicht aufgelöste Fragen


Treffen von Besprechungen


n/v
"Standardliteralname" mit Zieltyp
04.11.2021 • 3 minutes to read

[x] vorgeschlagen
[x] Prototyp
[x]-Implementierung
[]-Spezifikation

Zusammenfassung
Das mit dem Ziel typisierte default Feature ist eine kürzere Formular Variation des default(T) Operators,
sodass der Typ ausgelassen werden kann. Der Typ wird stattdessen von der Ziel Typisierung abgeleitet.
Abgesehen davon verhält es sich wie default(T) .

Motivation
Die Hauptmotivation besteht darin, redundante Informationen zu vermeiden.
Wenn beispielsweise aufgerufen void Method(ImmutableArray<SomeType> array) wird, lässt das standardinteral
M(default) anstelle von zu M(default(ImmutableArray<SomeType>)) .

Dies gilt für eine Reihe von Szenarien, wie z. b.:


Deklarieren von lokalen Variablen ( ImmutableArray<SomeType> x = default; )
TERNÄRE Vorgänge ( var x = flag ? default : ImmutableArray<SomeType>.Empty; )
zurückgeben in Methoden und Lambdas ( return default; )
Deklarieren von Standardwerten für optionale Parameter (
void Method(ImmutableArray<SomeType> arrayOpt = default) )
einschließen von Standardwerten in Ausdrücke zum Erstellen von Arrays (
var x = new[] { default, ImmutableArray.Create(y) }; )

Detaillierter Entwurf
Ein neuer Ausdruck wird eingeführt, das standardliteralzeichen. Ein Ausdruck mit dieser Klassifizierung kann
durch eine standardliteralkonvertierung implizit in einen beliebigen Typ konvertiert werden.
Das Ableiten des Typs für das standardliteralformat funktioniert genauso wie für das null -Literale, mit dem
Unterschied, dass ein beliebiger Typ zulässig ist (nicht nur Verweis Typen).
Diese Konvertierung erzeugt den Standardwert des abhergenten Typs.
Das standardliterale kann abhängig vom abhergenten Typ einen konstanten Wert aufweisen. Dies
const int x = default; ist zwar zulässig, const int? y = default; ist jedoch nicht.

Beim standardliteralwert kann es sich um den Operanden von Gleichheits Operatoren handeln, solange der
andere Operand einen Typ aufweist. default == x Und x == default sind gültige Ausdrücke, sind aber
unzulässig default == default .

Nachteile
Ein geringfügiger Nachteil ist, dass in den meisten Kontexten anstelle von null -literalen standardliterale
verwendet werden kann. Zwei Ausnahmen sind throw null; und null == null , die für das null -Literale
zulässig sind, aber nicht das standardliteralzeichen .

Alternativen
Es gibt einige Alternativen zu berücksichtigende Aspekte:
Der Status quo: das Feature ist nicht für seine eigenen Vorzüge gerechtfertigt, und Entwickler verwenden
weiterhin den Standard Operator mit einem expliziten Typ.
Erweitern des NULL-Literals: Dies ist der VB-Ansatz mit Nothing . Wir könnten dies erlauben int x = null; .

Nicht aufgelöste Fragen


[x] sollte standardmäßig als Operand des is -oder As -Operators zulässig sein? Antwort: nicht zulassen
default is T , zulassen x is default , zulassen default as RefType (mit Always-NULL-Warnung)

Treffen von Besprechungen


LDM 3/7/2017
LDM 3/28/2017
LDM 5/31/2017
Ableiten von tupelnamen (auch als bezeichnet).
tupelprojektionsinitialisierer)
04.11.2021 • 4 minutes to read

Zusammenfassung
In einer Reihe gängiger Fälle ermöglicht dieses Feature, dass die tupelelementnamen ausgelassen und
stattdessen abgeleitet werden. Anstatt beispielsweise einzugeben (f1: x.f1, f2: x?.f2) , können die
Elementnamen "F1" und "F2" aus abgeleitet werden (x.f1, x?.f2) .
Dies ist vergleichbar mit dem Verhalten anonymer Typen, die das Ableiten von Elementnamen während der
Erstellung ermöglichen. Deklariert beispielsweise die Member new { x.f1, y?.f2 } "F1" und "F2".
Dies ist besonders bei der Verwendung von Tupeln in LINQ nützlich:

// "c" and "result" have element names "f1" and "f2"


var result = list.Select(c => (c.f1, c.f2)).Where(t => t.f2 == 1);

Detaillierter Entwurf
Die Änderung besteht aus zwei Teilen:
1. Versuchen Sie, einen Kandidaten Namen für jedes Tupelelement abzuleiten, das keinen expliziten Namen hat:
Die gleichen Regeln wie bei der namens Ableitung für anonyme Typen werden verwendet.
In c# lässt dies drei Fälle zu: y (Bezeichner), x.y (einfacher Member-Zugriff) und x?.y
(bedingter Zugriff).
In VB ermöglicht dies zusätzliche Fälle, z x.y() . b..
Ablehnen von reservierten tupelnamen (Unterscheidung nach Groß-/Kleinschreibung in c#, keine
Unterscheidung nach Groß-/Kleinschreibung in VB) Beispielsweise, ItemN Rest und ToString .
Wenn es sich bei allen Kandidaten Namen um Duplikate handelt (Unterscheidung nach Groß-
/Kleinschreibung in c#, keine Unterscheidung nach Groß-/Kleinschreibung in VB), werden wir diese
Kandidaten verwerfen.
2. Bei Konvertierungen (bei denen das Ablegen von Namen aus tupelliteralen überprüft und gewarnt wird)
werden keine Warnungen ausgegeben. Dadurch wird das Abbrechen von vorhandenem tupelcode
vermieden.
Beachten Sie, dass die Regel für die Verarbeitung von Duplikaten von der Regel für anonyme Typen abweicht.
Beispielsweise new { x.f1, x.f1 } erzeugt einen Fehler, ist jedoch (x.f1, x.f1) weiterhin zulässig (nur ohne
abherzufügende Namen). Dadurch wird das Abbrechen von vorhandenem tupelcode vermieden.
Aus Konsistenz Gründen gilt dasselbe für Tupel, die durch Dekonstruktions Zuweisungen (in c#) erzeugt werden:

// tuple has element names "f1" and "f2"


var tuple = ((x.f1, x?.f2) = (1, 2));

Dasselbe gilt auch für VB-Tupel, wobei die VB-spezifischen Regeln für das Ableiten von Namen aus Ausdrücken
und die Groß-/Kleinschreibung von namens vergleichen verwendet werden.
Wenn Sie den c# 7,1-Compiler (oder höher) mit der Sprachversion "7,0" verwenden, werden die Elementnamen
abgeleitet (obwohl das Feature nicht verfügbar ist), es gibt jedoch einen Verwendungs Standort Fehler, um
darauf zuzugreifen. Dadurch wird das Hinzufügen von neuem Code, der später dem Kompatibilitätsproblem
ausgesetzt wäre, eingeschränkt (siehe unten).

Nachteile
Der Hauptnachteil besteht darin, dass dadurch ein Kompatibilitäts Umbruch von c# 7,0 eingeführt wird:

Action y = () => M();


var t = (x: x, y);
t.y(); // this might have previously picked up an extension method called “y”, but would now call the
lambda.

Der Compatibility Council hat diese Unterbrechung als akzeptabel erkannt, weil er begrenzt ist und das
Zeitfenster seit dem Versand von Tupeln (in c# 7,0) kurz ist.

References
LDM, 4. April 2017
GitHub-Diskussion (vielen Dank @alrz , dass Sie dieses Problem beheben)
Design von Tupeln
Muster Vergleich mit Generika
04.11.2021 • 3 minutes to read

[x] vorgeschlagen
[] Prototyp:
[]-Implementierung:
[]-Spezifikation:

Zusammenfassung
Die Spezifikation für den vorhandenen c# as-Operator ermöglicht es, keine Konvertierung zwischen dem Typ
des Operanden und dem angegebenen Typ durchzusetzen, wenn ein offener Typ ist. In c# 7 Type identifier
erfordert das Muster jedoch eine Konvertierung zwischen dem Typ der Eingabe und dem angegebenen Typ.
Es wird vorgeschlagen, dies zu lockern und zu ändern expression is Type identifier , zusätzlich zu den
Bedingungen, die in c# 7 zulässig sind, auch zulässig, wenn zulässig expression as Type wäre. Insbesondere
handelt es sich bei den neuen Fällen um Fälle, in denen der Typ des Ausdrucks oder der angegebene Typ ein
offener Typ ist.

Motivation
Fälle, in denen Muster Vergleiche "offensichtlich" zugelassen werden, können derzeit nicht kompiliert werden.
Siehe z https://github.com/dotnet/roslyn/issues/16195 . b..

Detaillierter Entwurf
Wir ändern den Absatz in der Spezifikation für die Muster Übereinstimmung (die vorgeschlagene Addition ist
fett dargestellt):

Bestimmte Kombinationen von statischem Typ der linken Seite und des angegebenen Typs werden als nicht
kompatibel betrachtet und führen zu einem Kompilierzeitfehler. Ein Wert vom Typ statischer Typ E wird als
Muster kompatibel mit dem Typ bezeichnet, T Wenn eine Identitäts Konvertierung, eine implizite Verweis
Konvertierung, eine Boxing-Konvertierung, eine explizite Verweis Konvertierung oder eine Unboxing-
Konvertierung von in vorhanden ist, E T oder wenn entweder E oder T ein offener Typ ist . Es
handelt sich um einen Kompilierzeitfehler, wenn ein Ausdruck vom Typ E nicht mit dem Typ in einem
Typmuster kompatibel ist, mit dem er übereinstimmt.

Nachteile
Keine.

Alternativen
Keine.

Nicht aufgelöste Fragen


Keine.
Treffen von Besprechungen
LDM hat diese Frage in Erwägung gezogen und gespürt, dass es sich um eine Fehler Behebungs Ebene handelt.
Wir behandeln Sie als separate Sprachfunktion, da die Änderung nach der Veröffentlichung der Sprache eine
vorwärts Inkompatibilität mit sich bringen würde. Für die Verwendung der vorgeschlagenen Änderung muss
der Programmierer Sprachversion 7,1 angeben.
Schreibgeschützte Verweise
04.11.2021 • 45 minutes to read

[x] vorgeschlagen
[x] Prototyp
[x] Implementierung: gestartet
[] Spezifikation: nicht gestartet

Zusammenfassung
Die Funktion "schreibgeschützte Verweise" ist tatsächlich eine Gruppe von Features, die die Effizienz der
Weitergabe von Variablen als Verweis nutzen, ohne die Daten für Änderungen verfügbar zu machen:
in Metern
ref readonly -Rückgaben
readonly Strukturen
ref / in Erweiterungs Methoden
ref readonly Bewohner
ref bedingte Ausdrücke

Übergeben von Argumenten als schreibgeschützte Verweise.


Es gibt ein vorhandenes Angebot, das dieses Thema https://github.com/dotnet/roslyn/issues/115 als Sonderfall
von schreibgeschützten Parametern berührt, ohne in viele Details zu gehen. An dieser Stelle möchte ich nur
bestätigen, dass die Idee allein nicht ganz neu ist.
Motivation
Vor diesem Feature verfügte c# nicht über eine effiziente Möglichkeit, Struktur Variablen in Methodenaufrufe für
schreibgeschützte Zwecke zu übergeben, ohne zu ändern. Die reguläre durch-Wert-Argument Übergabe
impliziert das Kopieren, wodurch unnötige Kosten entstehen. Dadurch werden Benutzer zur Verwendung der by-
ref-Argument Übergabe und der Verwendung von Kommentaren/Dokumentationen aufgefordert, um
anzugeben, dass die Daten nicht vom aufgerufenen mutiert werden sollen. Es ist aus vielen Gründen keine gute
Lösung.
Bei den Beispielen handelt es sich um zahlreiche Vektor-/matrixoperatoren in Grafik Bibliotheken, wie XNA
bekanntermaßen nur aufgrund von Leistungs Überlegungen zu Referenz Operanden gehören. Der Roslyn-
Compiler selbst enthält Code, der Strukturen verwendet, um Zuordnungen zu vermeiden, und Sie dann als
Verweis übergibt, um das Kopieren von Kosten zu vermeiden.
Lösung ( in Parameter)
Ähnlich wie bei den out Parametern in werden Parameter als verwaltete Verweise mit zusätzlichen Garantien
vom aufgerufenen übergeben.
Im Unterschied zu out Parametern, die vom aufgerufenen vor einer anderen Verwendung zugewiesen werden
müssen , in können Parameter nicht vom aufgerufenen überhaupt zugewiesen werden.
Als Ergebnis in Parameter wird die Effektivität der indirekten Argument Übergabe ermöglicht, ohne
Argumente für Mutationen durch den aufgerufenen verfügbar zu machen.
Deklarieren eines in -Parameters
in Parameter werden mithilfe in des Schlüssel Worts als Modifizierer in der Parameter Signatur deklariert.
Für alle Zwecke wird der- in Parameter als readonly Variable behandelt. Die meisten Einschränkungen bei der
Verwendung von in Parametern in der-Methode sind identisch mit den- readonly Feldern.

Tatsächlich kann ein- in Parameter ein readonly Feld darstellen. Die Ähnlichkeit von Einschränkungen
stellt keinen Zufall dar.

Beispielsweise werden Felder eines in Parameters, der über einen Strukturtyp verfügt, alle rekursiv als
readonly Variablen klassifiziert.

static Vector3 Add (in Vector3 v1, in Vector3 v2)


{
// not OK!!
v1 = default(Vector3);

// not OK!!
v1.X = 0;

// not OK!!
foo(ref v1.X);

// OK
return new Vector3(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

in Parameter sind überall dort zulässig, wo gewöhnliche ByVal-Parameter zulässig sind. Dies schließt
Indexer, Operatoren (einschließlich Konvertierungen), Delegaten, Lambdas, lokale Funktionen ein.

(in int x) => x // lambda expression


TValue this[in TKey index]; // indexer
public static Vector3 operator +(in Vector3 x, in Vector3 y) => ... // operator

in ist nicht in Kombination mit out oder mit allem zulässig, das out nicht mit kombiniert wird.
Es ist nicht zulässig, dass ref / out / in Unterschiede überladen werden.
Es ist zulässig, bei gewöhnlichen ByVal und Unterschieden zu überlasten in .
Für den Zweck von ohi (überladen, ausblenden, implementieren) in verhält sich ähnlich wie ein- out
Parameter. Es gelten dieselben Regeln. Beispielsweise muss die über schreibende Methode in Parameter
mit in Parametern eines Identitäts konvertierbaren Typs vergleichen.
Im Sinne von Delegaten/Lambda-/methodengruppenkonvertierungen in verhält sich ähnlich wie ein-
out Parameter. Lambdas und entsprechende Methoden Gruppen Konvertierungs Kandidaten müssen
in die Parameter des Ziel Delegaten mit in Parametern eines Identitäts konvertierbaren Typs
vergleichen.
Zum Zweck der generischen Varianz in sind Parameter nicht Variant.

Hinweis: Es gibt keine Warnungen zu in Parametern, die Verweis-oder primitive Typen aufweisen. Im
Allgemeinen ist es möglicherweise sinnlos, aber in einigen Fällen muss der Benutzer primitive als
übergeben in . Beispiele: Überschreiben einer generischen Methode wie z. b. Method(in T param) , wenn
durch T ersetzt wurde int oder wenn Methoden wie Volatile.Read(in int location)
Es ist denkbar, dass Sie über einen Analyzer verfügen, der bei ineffizienter Verwendung von in Parametern
gewarnt wird, aber die Regeln für diese Analyse wären zu groß, um Teil einer Sprachspezifikation zu werden.
Verwendung von in an aufrufenden Websites. ( in Argumente )
Es gibt zwei Möglichkeiten, Argumente an Parameter zu übergeben in .
in Argumente können den in Parametern entsprechen:
Ein Argument mit einem in Modifizierer an der aufrufssite kann mit in Parametern verglichen werden.

int x = 1;

void M1<T>(in T x)
{
// . . .
}

var x = M1(in x); // in argument to a method

class D
{
public string this[in Guid index];
}

D dictionary = . . . ;
var y = dictionary[in Guid.Empty]; // in argument to an indexer

in Das Argument muss ein lesbarer lvalue (*) sein. Beispiel: M1(in 42) ist ungültig

(*) Das Konzept von Lvalue/Rvalue variiert je nach Sprache.


Hier ist nach lvalue ein Ausdruck gemeint, der einen Speicherort darstellt, auf den direkt verwiesen werden
kann. Und Rvalue bedeutet einen Ausdruck, der ein temporäres Ergebnis ergibt, das nicht eigenständig
persistent gespeichert wird.

Insbesondere readonly können Felder, in Parameter oder andere formal readonly Variablen als
Argumente übergeben werden in . Beispiel: dictionary[in Guid.Empty] ist zulässig. Guid.Empty ist ein
statisches Schreib geschütztes Feld.
in Das Argument muss eine Typidentität aufweisen, die in den Typ des Parameters konvertiert werden
muss. Beispiel: M1<object>(in Guid.Empty) ist ungültig. Guid.Empty ist nicht Identitäts konvertierbar in
object

Die Motivation für die oben genannten Regeln besteht darin, dass in Argumente das Aliasing der Argument
Variablen garantieren. Der aufgerufene erhält immer einen direkten Verweis auf denselben Speicherort, der
durch das-Argument dargestellt wird.
in seltenen Fällen, in denen in Argumente aufgrund await von Ausdrücken, die als Operanden desselben
Aufrufes verwendet werden, Stapel überläuft werden müssen, ist das Verhalten mit dem- out Argument
und dem ref -Argument identisch. wenn die Variable nicht referenziell transparent übertragen werden
kann, wird ein Fehler gemeldet.
Beispiele:
1. ist gültig. staticField ist ein statisches Feld, auf das mehr als
M1(in staticField, await SomethingAsync())
ein Mal zugegriffen werden kann, ohne dass sichtbare Nebeneffekte auftreten. Daher können die Reihenfolge
von Nebeneffekten und Aliasing Anforderungen bereitgestellt werden.
2. M1(in RefReturningMethod(), await SomethingAsync()) führt zu einem Fehler. RefReturningMethod() ist eine
ref Rückgabe Methode. Ein Methodenaufruf kann sichtbare Nebeneffekte aufweisen und muss daher vor
dem Operanden ausgewertet werden SomethingAsync() . Das Ergebnis des aufforderes ist jedoch ein
Verweis, der nicht über den Unterbrechungs Punkt hinweg beibehalten werden kann await , der die direkte
Verweis Anforderung unmöglich macht.

Hinweis: die Stapelüberlauf Fehler gelten als Implementierungs spezifische Einschränkungen. Daher haben
Sie keinen Einfluss auf die Überladungs Auflösung oder den Lambda-Inference.

Normale ByVal-Argumente können den in Parametern entsprechen:


Reguläre Argumente ohne modifiziererer können mit in Parametern verglichen werden. In diesem Fall
verfügen die Argumente über die gleichen gelockerten Einschränkungen wie normale ByVal-Argumente.
Die Motivation für dieses Szenario besteht darin, dass in Parameter in APIs zu Unannehmlichkeiten für den
Benutzer führen können, wenn Argumente nicht als direkter Verweis (ex: Literale, berechnete oder await -Ed-
Ergebnisse oder Argumente, für die spezifischere Typen vorhanden sind) nicht weitergegeben werden können.
Alle diese Fälle haben eine triviale Lösung, den Argument Wert in einem temporären lokalen des
entsprechenden Typs zu speichern und diese lokal als in Argument zu übergeben.
Um die Notwendigkeit eines solchen Code Compilers zu reduzieren, kann die gleiche Transformation
durchgeführt werden, wenn dies erforderlich in ist, wenn der Modifizierer nicht an der aufrufssite vorhanden
ist.
Außerdem gibt es in einigen Fällen, z. b. Aufrufen von Operatoren oder in Erweiterungs Methoden, überhaupt
keine syntaktische Methode anzugeben in . Das allein erfordert, dass das Verhalten gewöhnlicher ByVal-
Argumente beim Zuordnen von in Parametern angegeben wird.
Dies gilt insbesondere für:
Es ist gültig, Rvalues zu übergeben. Ein Verweis auf eine temporäre wird in diesem Fall übermittelt. Beispiel:

Print("hello"); // not an error.

void Print<T>(in T x)
{
//. . .
}

implizite Konvertierungen sind zulässig.

Dies ist ein Sonderfall, wenn ein rvalue-Wert übergeben wird.

In einem solchen Fall wird ein Verweis auf einen temporären, mit dem Wert konvertierten Wert übermittelt.
Beispiel:

Print<int>(Short.MaxValue) // not an error.

bei einem Empfänger einer in Erweiterungsmethode (im Gegensatz zu ref Erweiterungs Methoden) sind
Rvalues oder implizite this-Argument-Konvertierungen zulässig. In einem solchen Fall wird ein Verweis auf
einen temporären, mit dem Wert konvertierten Wert übermittelt. Beispiel:

public static IEnumerable<T> Concat<T>(in this (IEnumerable<T>, IEnumerable<T>) arg) => . . .;

("aa", "bb").Concat<char>() // not an error.

Weitere Informationen zu ref / in Erweiterungs Methoden finden Sie weiter unten in diesem Dokument.
ein Argument await Überlauf aufgrund von Operanden könnte ggf. "by-Value" überlaufen. In Szenarien, in
denen ein direkter Verweis auf das Argument nicht möglich ist, ist await eine Kopie des Argument Werts
stattdessen überlaufen.
Beispiel:

M1(RefReturningMethod(), await SomethingAsync()) // not an error.

Da das Ergebnis eines parallelnden auffalls ein Verweis ist, der nicht über die Unterbrechung hinweg
beibehalten werden kann await , wird stattdessen ein temporäres mit dem tatsächlichen Wert beibehalten (wie
es in einem normalen ByVal-Parameter Fall der Fall wäre).
Nicht optionale Argumente ausgelassen
Es ist zulässig, dass ein in Parameter einen Standardwert angibt. Dadurch wird das entsprechende Argument
optional.
Wenn Sie das optionale Argument an der aufrufssite weglassen, wird der Standardwert über einen temporären
übergeben.

Print("hello"); // not an error, same as


Print("hello", c: Color.Black);

void Print(string s, in Color c = Color.Black)


{
// . . .
}

Aliasing-Verhalten im allgemeinen
Ebenso wie ref -und- out Variablen in sind Variablen Verweise/Aliase auf vorhandene Speicherorte.
Obwohl der aufgerufene nicht in der aufgerufenen Schreib Berechtigung ist, kann das Lesen eines- in
Parameters verschiedene Werte als Nebeneffekte anderer Auswertungen beobachten.
Beispiel:

static Vector3 v = Vector3.UnitY;

static void Main()


{
Test(v);
}

static void Test(in Vector3 v1)


{
Debug.Assert(v1 == Vector3.UnitY);
// changes v1 deterministically (no races required)
ChangeV();
Debug.Assert(v1 == Vector3.UnitX);
}

static void ChangeV()


{
v = Vector3.UnitX;
}

in Parameter und die Erfassung von lokalen Variablen.


Für die Verwendung von Lambda-/Async-Erfassungs in Parametern Verhalten sich dieselben wie die out -
und- ref Parameter.
in Parameter können nicht in einem Abschluss aufgezeichnet werden.
in Parameter sind in Iteratormethoden nicht zulässig.
in Parameter sind in Async-Methoden nicht zulässig.
Temporäre Variablen.
Einige Verwendungen der in Parameter Übergabe erfordern möglicherweise die indirekte Verwendung einer
temporären lokalen Variablen:
in Argumente werden immer als direkte Aliase weitergegeben, wenn die "CallSite" verwendet in .
Temporär wird in diesem Fall nie verwendet.
in Argumente müssen keine direkten Aliase sein, wenn die Aufrufsite nicht verwendet in . Wenn das
Argument kein lvalue ist, kann ein temporäres verwendet werden.
in der Parameter kann einen Standardwert aufweisen. Wenn das entsprechende Argument an der
aufrufssite weggelassen wird, wird der Standardwert über einen temporären Wert übermittelt.
in Argumente können implizite Konvertierungen aufweisen, einschließlich derjenigen, die die Identität nicht
beibehalten. In diesen Fällen wird ein temporäres verwendet.
Empfänger von gewöhnlichen Struktur aufrufen sind möglicherweise keine beschreibbaren Lvalues
(vorhandener Fall! ). In diesen Fällen wird ein temporäres verwendet.
Die Lebensdauer der Argument temporare stimmt mit dem nächstgelegenen Bereich der Aufruf Site überein.
Die formale Lebensdauer temporärer Variablen ist in Szenarien mit escapeanalysen von Variablen, die als
Verweis zurückgegeben werden, semantisch signifikant.
Metadatendarstellung von in Parametern.
Wenn System.Runtime.CompilerServices.IsReadOnlyAttribute auf einen ByRef-Parameter angewendet wird,
bedeutet dies, dass der-Parameter ein- in Parameter ist.
Wenn die Methode ebenfalls abstrakt oder virtuell ist, muss die Signatur solcher Parameter (und nur solcher
Parameter) über verfügen modreq[System.Runtime.InteropServices.InAttribute] .
Motivation : Dies geschieht, um sicherzustellen, dass bei der Überschreibung/Implementierung der Parameter
eine Methode vorliegt in .
Die gleichen Anforderungen gelten für Invoke Methoden in Delegaten.
Motivation : Dadurch wird sichergestellt, dass vorhandene Compiler readonly beim Erstellen oder Zuweisen
von Delegaten nicht einfach ignorieren können.

Rückgabe durch einen schreibgeschützten Verweis.


Motivation
Die Motivation für diese Unterfunktion ist ungefähr symmetrisch zu den Gründen für die in Parameter und
vermeidet das Kopieren, aber auf der Rückgabe Seite. Vor dieser Funktion hatten eine Methode oder ein Indexer
zwei Optionen: 1) Rückgabe als Verweis und verfügbar für mögliche Mutationen oder 2) Rückgabe nach Wert,
der zum Kopieren führt.
Lösung ( ref readonly gibt zurück)
Die Funktion ermöglicht es einem Member, Variablen als Verweis zurückzugeben, ohne Sie für Mutationen
verfügbar zu machen.
Deklarieren zurückgegebener Member ref readonly

Eine Kombination von modifiziererelementen ref readonly in der Rückgabe Signatur wird verwendet, um
anzugeben, dass der Member einen schreibgeschützten Verweis zurückgibt.
Für alle Zwecke ref readonly wird ein Member als Variable behandelt readonly , ähnlich wie bei readonly
Feldern und in Parametern.
Beispielsweise werden Felder mit ref readonly einem-Strukturtyp, die einen Strukturtyp haben, rekursiv als
readonly Variablen klassifiziert. -Es ist zulässig, Sie als Argumente zu übergeben in , aber nicht als- ref oder-
out Argumente.

ref readonly Guid Method1()


{
}

Method2(in Method1()); // valid. Can pass as `in` argument.

Method3(ref Method1()); // not valid. Cannot pass as `ref` argument

Rückgabe sind an denselben Stellen zulässig, an ref denen zurückgegebene


ref readonly
zurückgegeben werden. Dies schließt Indexer, Delegaten, Lambdas, lokale Funktionen ein.
Es ist nicht zulässig, on ref / ref readonly /Unterschiede zu überlasten.
Es ist zulässig, bei normalem ByVal zu überladen und ref readonly Unterschiede zurückzugeben.
Der Zweck von ohi (überladen, ausblenden, implementieren) ref readonly ist ähnlich, unterscheidet sich
jedoch von ref . Beispielsweise muss eine Methode, die eine überschreibt ref readonly , selbst sein
ref readonly und über einen Identitäts konvertierbaren Typ verfügen.

Für den Zweck der Konvertierung von Delegaten/Lambda-/Methodengruppen ref readonly ist ähnlich,
unterscheidet sich jedoch von ref . Lambdas und entsprechende Methoden Gruppen Konvertierungs
Kandidaten müssen die ref readonly Rückgabe des Ziel Delegaten mit der ref readonly Rückgabe des
Typs, der Identitäts konvertierbar ist, abgleichen.
Zum Zweck der generischen Abweichung ref readonly sind die Rückgabe nicht Variant.

Hinweis: Es gibt keine Warnungen für ref readonly Rückgaben, die Verweis-oder primitive Typen
aufweisen. Im Allgemeinen ist es möglicherweise sinnlos, aber in einigen Fällen muss der Benutzer primitive
als übergeben in . Beispiele: Überschreiben einer generischen Methode wie ref readonly T Method() ,
wenn als T ersetzt wurde int .
Es ist denkbar, eine Analyse zu verwenden, die bei einer ineffizienten Verwendung von- ref readonly
Rückgaben gewarnt wird, aber die Regeln für diese Analyse sind zu groß, um Teil einer Sprachspezifikation
zu werden.

Zurückgeben ref readonly von Membern


Innerhalb des Methoden Texts ist die Syntax identisch mit der regulären Ref-Rückgabe. Der readonly wird von
der enthaltenden Methode abgeleitet.
Die Motivation ist, dass return ref readonly <expression> unnötig lange ist und nur fehl Übereinstimmungen
für den readonly Teil zulässt, der immer zu Fehlern führen würde. ref Ist jedoch aus Gründen der Konsistenz
mit anderen Szenarien erforderlich, in denen etwas durch Strict Aliasing und durch Wert übermittelt wird.

Anders als bei in Parametern ref readonly gibt Return nie über eine lokale Kopie zurück. Wenn Sie in
Erwägung ziehen, dass die Kopie sofort nach der Rückgabe einer solchen Übung nicht mehr vorhanden ist,
wäre es sinnlos und gefährlich. Folglich ref readonly werden immer direkte Verweise zurückgegeben.

Beispiel:
struct ImmutableArray<T>
{
private readonly T[] array;

public ref readonly T ItemRef(int i)


{
// returning a readonly reference to an array element
return ref this.array[i];
}
}

Ein Argument von return ref muss ein Lvalue (vorhandene Regel ) sein.
Ein Argument von return ref muss "sicher zur Rückgabe" (vorhandene Regel ) sein.
In einem- ref readonly Member muss ein Argument von return ref nicht beschreibbar sein .
Beispielsweise kann ein solcher Member Ref-ein Schreib geschütztes Feld oder einen seiner Parameter
zurückgeben in .
Sichere Rückgaberegeln.
Normale sicher zum Zurückgeben von Regeln für Verweise gelten auch für schreibgeschützte Verweise.
Beachten Sie, dass eine ref readonly aus einem regulären ref local/Parameter/Return abgerufen werden
kann, aber nicht umgekehrt. Andernfalls wird die Sicherheit der ref readonly Rückgabe auf die gleiche Weise
wie bei regulären ref Rückgaben abgeleitet.
Wenn Sie in Erwägung ziehen, dass Rvalues als in Parameter übergeben und zurückgegeben werden können,
wenn ref readonly eine weitere Regel erforderlich ist, können die Rvalues nicht als Ver weis zurück
gegeben werden.

Beachten Sie die Situation, in der ein rvalue in über eine Kopie an einen-Parameter übergeben und dann in
einer Form von zurückgegeben wird ref readonly . Im Zusammenhang mit dem Aufrufer ist das Ergebnis
eines solchen Aufrufers ein Verweis auf lokale Daten, sodass die Rückgabe unsicher ist. Wenn die Rückgabe
von Rvalues nicht sicher ist, wird dieser Fall bereits von der vorhandenen Regel #6 behandelt.

Beispiel:

ref readonly Vector3 Test1()


{
// can pass an RValue as "in" (via a temp copy)
// but the result is not safe to return
// because the RValue argument was not safe to return by reference
return ref Test2(default(Vector3));
}

ref readonly Vector3 Test2(in Vector3 r)


{
// this is ok, r is returnable
return ref r;
}

Aktualisierte safe to return Regeln:


1. die Rückgabe von Refs auf Variablen im Heap ist sicher.
2. ref/in-Parameter können sicher zurückgegeben in werden. Parameter können natürlich nur als
schreibgeschützt zurückgegeben werden.
3. out-Parameter können sicher zurückgegeben werden (Sie müssen jedoch definitiv zugewiesen werden,
wie dies bereits heute der Fall ist).
4. instanzstrukturfelder können sicher zurückgegeben werden, solange der Empfänger sicher
zurückgegeben werden kann.
5. "This" kann nicht sicher von Strukturmembern zurückgegeben werden.
6. ein Ver weis, der von einer anderen Methode zurückgegeben wurde, kann sicher zurückgegeben
werden, wenn alle an diese Methode weiter gegebenen Refs/out-Wer te, wie Sie sicher zurück
gegeben werden konnten Insbesondere ist es unerheblich, ob der Empfänger sicher zurückgegeben werden
kann, unabhängig davon, ob der Empfänger eine Struktur oder Klasse ist oder als generischer Typparameter
typisiert ist.
7. Rvalues können nicht als Ver weis zurückgegeben werden. Insbesondere Rvalues können als
Parameter sicher übergeben werden.

Hinweis: Es gibt weitere Regeln bezüglich der Sicherheit von zurückgegebenen Rückgaben, wenn Ref-like-
Typen und ref-reassignments beteiligt sind. Die Regeln gelten gleichermaßen für die Member ref und
ref readonly und werden daher hier nicht erwähnt.

Aliasing Verhalten.
ref readonly Member bieten das gleiche Aliasing Verhalten wie normale Member ref (mit Ausnahme von
"schreibgeschützt"). Aus diesem Grund dienen die Erfassung in Lambdas, async, Iteratoren, Stapelüberlauf usw...
Es gelten die gleichen Einschränkungen. d. aufgrund der Tatsache, dass die eigentlichen Verweise nicht erfasst
werden können, sind solche Szenarios aufgrund der Nebenwirkungen der Mitglieder Auswertung nicht zulässig.

Es ist zulässig und erforderlich, eine Kopie zu erstellen ref readonly , wenn die Rückgabe ein Empfänger
regulärer struct-Methoden ist, die this als normaler Beschreib barer Verweis übernommen werden. In der
Vergangenheit wird in allen Fällen, in denen solche Aufrufe auf die schreibgeschützte Variable angewendet
werden, eine lokale Kopie erstellt.

Metadatendarstellung.
Wenn System.Runtime.CompilerServices.IsReadOnlyAttribute auf die Rückgabe einer ByRef-Rückgabe Methode
angewendet wird, bedeutet dies, dass die Methode einen schreibgeschützten Verweis zurückgibt.
Außerdem muss die Ergebnis Signatur solcher Methoden (und nur dieser Methoden) über verfügen
modreq[System.Runtime.CompilerServices.IsReadOnlyAttribute] .

Motivation : Dadurch wird sichergestellt, dass vorhandene Compiler readonly beim Aufrufen von Methoden
mit Rückgabe nicht einfach ignorieren können. ref readonly

Schreibgeschützte Strukturen
Kurz gesagt: eine Funktion, die this Parameter aller Instanzmember einer Struktur, mit Ausnahme von
Konstruktoren, als Parameter definiert in .
Motivation
Der Compiler muss davon ausgehen, dass die Instanz durch einen beliebigen Methoden aufrufin einer Struktur
Instanz geändert werden kann. Tatsächlich wird ein Beschreib barer Verweis als Parameter an die Methode
übergeben this und ermöglicht dieses Verhalten vollständig. Um solche Aufrufe für Variablen zuzulassen
readonly , werden die Aufrufe auf temporäre Kopien angewendet. Dies könnte nicht intuitiv sein, und es
erzwingt manchmal, dass Personen readonly aus Leistungsgründen abgebrochen werden.
Beispiel: https://codeblog.jonskeet.uk/2014/07/16/micro-optimization-the-surprising-inefficiency-of-readonly-
fields/
Nach dem Hinzufügen der Unterstützung für in Parameter und ref readonly der Rückgabe des Problems des
defensiven Kopierens kommt es zu einer schlechteren, da schreibgeschützte Variablen häufiger verwendet
werden.
Lösung
Lässt readonly Modifizierer für Struktur Deklarationen zu, was dazu führen würde, dass this Sie als in
Parameter für alle strukturinstanzmethoden mit Ausnahme von Konstruktoren behandelt werden.

static void Test(in Vector3 v1)


{
// no need to make a copy of v1 since Vector3 is a readonly struct
System.Console.WriteLine(v1.ToString());
}

readonly struct Vector3


{
. . .

public override string ToString()


{
// not OK!! `this` is an `in` parameter
foo(ref this.X);

// OK
return $"X: {X}, Y: {Y}, Z: {Z}";
}
}

Einschränkungen für Mitglieder der schreibgeschützten Struktur


Instanzfelder einer schreibgeschützten Struktur müssen schreibgeschützt sein.
Motivation: kann nur in extern, aber nicht über Member geschrieben werden.
Instanzeigenschaften einer schreibgeschützten Struktur müssen nur abgerufen werden.
Motivation: die Folge der Einschränkung für Instanzfelder.
Die schreibgeschützte Struktur darf keine Feld ähnlichen Ereignisse deklarieren.
Motivation: die Folge der Einschränkung für Instanzfelder.
Metadatendarstellung.
Wenn System.Runtime.CompilerServices.IsReadOnlyAttribute auf einen Werttyp angewendet wird, bedeutet dies,
dass der Typ eine ist readonly struct .
Dies gilt insbesondere für:
Die Identität des IsReadOnlyAttribute Typs ist unwichtig. Sie kann bei Bedarf von dem Compiler in die
enthaltende Assembly eingebettet werden.

ref / in Erweiterungs Methoden


Tatsächlich gibt es einen vorhandenen Vorschlag ( https://github.com/dotnet/roslyn/issues/165) und den
entsprechenden Prototyp-PR () https://github.com/dotnet/roslyn/pull/15650) . Ich möchte nur bestätigen, dass
diese Idee nicht völlig neu ist. Dies ist jedoch wichtig, da ref readonly das größte Problem bei solchen
Methoden durch die Verwendung von Rvalue-Empfängern auf elegante Weise entfernt wird.
Die allgemeine Idee besteht darin, dass Erweiterungs Methoden den this Parameter als Verweis verwenden
können, solange der Typ bekannt ist, dass es sich um einen Strukturtyp handelt.
public static void Extension(ref this Guid self)
{
// do something
}

Die Gründe für das Schreiben solcher Erweiterungs Methoden sind hauptsächlich:
1. Kopiervorgang vermeiden, wenn der Empfänger eine große Struktur ist
2. Verändernde Erweiterungs Methoden für Strukturen zulassen
Die Gründe, warum wir dies nicht für Klassen zulassen möchten.
1. Dies wäre ein sehr eingeschränkter Zweck.
2. Es würde eine lange bestehende invariante unterbrechen, dass ein Methodenaufruf den nicht-Empfänger
nicht aktivieren kann null , um nach dem Aufruf zu werden null .

Tatsächlich kann eine nicht-Variable derzeit null nicht werden, null es sei denn, Sie wird explizit
zugewiesen oder übermittelt ref out . Dadurch wird die Lesbarkeit oder andere Formen von "kann dies
eine NULL hier-Analyse sein" erheblich unterstützt. 3. Es wäre schwierig, mit der Semantik "einmal
auswerten" der Semantik von NULL-bedingten Zugriffen zu stimmen. Beispiel:
obj.stringField?.RefExtension(...) -Sie müssen eine Kopie von erfassen, stringField um die NULL-
Überprüfung aussagekräftig zu machen, aber dann werden Zuweisungen zu this innerhalb von
refextension nicht wieder in das Feld übernommen.

Eine Möglichkeit zum Deklarieren von Erweiterungs Methoden für Strukturen , die das erste Argument als
Verweis akzeptieren, war eine langfristige Anforderung. Eine der blockierenden Aspekte lautete: "Was geschieht,
wenn der Empfänger kein lvalue ist?".
Es gibt einen Präzedenzfall, dass jede Erweiterungsmethode auch als statische Methode aufgerufen werden
kann (manchmal ist Sie die einzige Möglichkeit, die Mehrdeutigkeit aufzulösen). Es würde vorschreiben, dass
Rvalue-Empfänger nicht zulässig sein sollten.
Andererseits ist es ratsam, in ähnlichen Situationen, in denen strukturinstanzmethoden beteiligt sind, einen
Aufruf auf eine Kopie vorzunehmen.
Der Grund, warum das implizite kopieren vorhanden ist, besteht darin, dass die Mehrzahl der Struktur
Methoden nicht die Struktur tatsächlich ändert und nicht in der Lage ist, dies anzugeben. Die praktischste
Lösung bestand daher darin, den Aufruf auf eine Kopie zu übernehmen, aber diese Vorgehensweise ist bekannt,
um die Leistung zu beeinträchtigen und Fehler zu verursachen.
Mit der Verfügbarkeit von in Parametern kann eine Erweiterung nun der Absicht signalisieren. Daher kann das
Problem gelöst werden ref , indem Erweiterungen mit beschreibbaren Empfängern aufgerufen werden
müssen, während in Erweiterungen implizites kopieren bei Bedarf zulassen.
// this can be called on either RValue or an LValue
public static void Reader(in this Guid self)
{
// do something nonmutating.
WriteLine(self == default(Guid));
}

// this can be called only on an LValue


public static void Mutator(ref this Guid self)
{
// can mutate self
self = new Guid();
}

in Erweiterungen und Generika.


Der Zweck der ref Erweiterungs Methoden besteht darin, den Empfänger direkt oder durch Aufrufen von
mutierenden Membern zu mutieren. Daher ref this T sind Erweiterungen zulässig T , solange die
Einschränkung auf eine Struktur beschränkt ist.
Auf der anderen Seite in existieren Erweiterungs Methoden speziell, um implizites kopieren zu verringern. Die
Verwendung eines- in T Parameters muss jedoch über einen Schnittstellenmember erfolgen. Da alle
Schnittstellenmember als muating angesehen werden, ist für jede solche Verwendung eine Kopie erforderlich. -
Anstatt das Kopieren zu verringern, wäre der Effekt das Gegenteil. Daher in this T ist nicht zulässig, wenn T
ein generischer Typparameter ist, unabhängig von Einschränkungen.
Gültige Arten von Erweiterungs Methoden (Recap):
Die folgenden Formen der this Deklaration in einer Erweiterungsmethode sind jetzt zulässig:
1. this T arg -reguläre ByVal-Erweiterung. (vorhandener Fall )

"T" kann ein beliebiger Typ sein, einschließlich Verweis Typen oder Typparametern. Die Instanz ist nach
dem-Befehl dieselbe Variable. Lässt implizite Konvertierungen dieser Art von Argument Konvertierungen
zu. Kann für Rvalues aufgerufen werden.
in this T self - in weiterung. "T" muss ein tatsächlicher Strukturtyp sein. Die Instanz ist nach dem-
Befehl dieselbe Variable. Lässt implizite Konvertierungen dieser Art von Argument Konvertierungen zu.
Kann für Rvalues aufgerufen werden (kann bei Bedarf für eine Temp aufgerufen werden).
ref this T self - ref weiterung. T muss ein Strukturtyp oder ein generischer Typparameter sein, der
als Struktur eingeschränkt ist. Die-Instanz kann durch den Aufruf von geschrieben werden. Lässt nur
Identitäts Konvertierungen zu. Muss für beschreibbaren lvalue aufgerufen werden. (nie über eine Temp
aufgerufen).

Schreibgeschützte lokale Ref-Variablen.


Ationen.
Nachdem ref readonly Mitglieder eingeführt wurden, war es klar, dass Sie mit der passenden Art von lokalem
paar kombiniert werden müssen. Durch die Auswertung eines Members können Nebeneffekte erzeugt oder
beobachtet werden. wenn das Ergebnis mehrmals verwendet werden muss, muss es daher gespeichert werden.
Normale ref lokale Variablen helfen hier nicht, da Ihnen kein Verweis zugewiesen werden kann readonly .
Not.
Hiermit wird das Deklarieren von ref readonly lokalen Dabei handelt es sich um eine neue Art von lokal ref ,
die nicht beschreibbar ist. Daher ref readonly können lokale Verweise auf schreibgeschützte Variablen
akzeptieren, ohne diese Variablen für Schreibvorgänge verfügbar zu machen.
Deklarieren und Verwenden von ref readonly lokalen Variablen.
Die Syntax dieser lokalen Variablen verwendet ref readonly Modifizierer auf der Deklarations Site (in dieser
bestimmten Reihenfolge). Ähnlich wie normale ref lokale Variablen ref readonly müssen die lokalen
Variablen bei der Deklaration als ref-initialisiert werden. Anders als normale ref lokale ref readonly können
lokale Variablen auf readonly Lvalues wie in Parameter, readonly Felder und ref readonly Methoden
verweisen.
Für alle Zwecke ref readonly wird eine lokale-Variable als readonly Variable behandelt. Die meisten
Einschränkungen in Bezug auf die Verwendung von readonly Feldern oder in Parametern sind identisch.
Beispielsweise werden Felder eines in Parameters, der über einen Strukturtyp verfügt, alle rekursiv als
readonly Variablen klassifiziert.

static readonly ref Vector3 M1() => . . .

static readonly ref Vector3 M1_Trace()


{
// OK
ref readonly var r1 = ref M1();

// Not valid. Need an LValue


ref readonly Vector3 r2 = ref default(Vector3);

// Not valid. r1 is readonly.


Mutate(ref r1);

// OK.
Print(in r1);

// OK.
return ref r1;
}

Einschränkungen bei der Verwendung von ref readonly lokalen Variablen


Mit Ausnahme ihrer readonly Natur Verhalten sich die ref readonly lokalen Variablen wie normale ref lokale
und unterliegen exakt denselben Einschränkungen.
Beispielsweise gelten Einschränkungen im Zusammenhang mit der Erfassung von Abschlüssen, das Deklarieren
von in- async Methoden oder die safe-to-return Analyse gilt auch für ref readonly lokale.

TERNÄRE ref Ausdrücke. (Alias "Conditional Lvalues")


Motivation
Durch die Verwendung von ref -und ref readonly -Variablen wurde eine Anforderung für das ref-
initialisieren solcher lokal mit einer oder einer anderen Zielvariablen auf der Grundlage einer Bedingung
offengelegt.
Eine typische Problem Umgehung besteht darin, eine Methode wie die folgende einzuführen:
ref T Choice(bool condition, ref T consequence, ref T alternative)
{
if (condition)
{
return ref consequence;
}
else
{
return ref alternative;
}
}

Beachten Sie, dass Choice keine genaue Ersetzung eines ternären ist, da alle Argumente an der aufrufssite
ausgewertet werden müssen, was zu nicht intuitivem Verhalten und Fehlern geführt hat.
Folgendes funktioniert nicht wie erwartet:

// will crash with NRE because 'arr[0]' will be executed unconditionally


ref var r = ref Choice(arr != null, ref arr[0], ref otherArr[0]);

Lösung
Hiermit wird eine spezielle Art von bedingtem Ausdruck zugelassen, die basierend auf einer Bedingung zu
einem Verweis auf ein Lvalue-Argument ausgewertet wird.
Verwenden des ref ternären Ausdrucks.
Die Syntax für die Konfiguration ref eines bedingten Ausdrucks lautet:
<condition> ? ref <consequence> : ref <alternative>;

Genau wie bei dem normalen bedingten Ausdruck <consequence> oder <alternative> wird abhängig vom
Ergebnis des Ausdrucks der booleschen Bedingung ausgewertet.
Anders als bei normalem Bedingungs Ausdruck, ref bedingter Ausdruck:
erfordert, dass <consequence> und <alternative> Lvalues sind.
ref der bedingte Ausdruck selbst ist ein Lvalue und
ref der bedingte Ausdruck ist beschreibbar, wenn sowohl <consequence> als auch <alternative>
beschreibbare Lvalues sind.
Beispiele:
ref TERNÄRE ist ein Lvalue und kann daher als Verweis erfolgreich/zugewiesen/zurückgegeben werden.

// pass by reference
foo(ref (arr != null ? ref arr[0]: ref otherArr[0]));

// return by reference
return ref (arr != null ? ref arr[0]: ref otherArr[0]);

Dabei kann es sich um einen lvalue-Wert handeln, der ebenfalls zugewiesen werden kann.

// assign to
(arr != null ? ref arr[0]: ref otherArr[0]) = 1;

// error. readOnlyField is readonly and thus conditional expression is readonly


(arr != null ? ref arr[0]: ref obj.readOnlyField) = 1;

Kann als Empfänger eines Methoden Aufrufes verwendet werden und das Kopieren bei Bedarf überspringen.
// no copies
(arr != null ? ref arr[0]: ref otherArr[0]).StructMethod();

// invoked on a copy.
// The receiver is `readonly` because readOnlyField is readonly.
(arr != null ? ref arr[0]: ref obj.readOnlyField).StructMethod();

// no copies. `ReadonlyStructMethod` is a method on a `readonly` struct


// and can be invoked directly on a readonly receiver
(arr != null ? ref arr[0]: ref obj.readOnlyField).ReadonlyStructMethod();

ref TERNÄRE kann auch in einem regulären (not Ref) Kontext verwendet werden.

// only an example
// a regular ternary could work here just the same
int x = (arr != null ? ref arr[0]: ref otherArr[0]);

Nachteile
Ich kann zwei wichtige Argumente für erweiterte Unterstützung für Verweise und schreibgeschützte Verweise
sehen:
1. Die hier gelösten Probleme sind sehr alt. Warum lösen Sie diese jetzt plötzlich, besonders weil Sie
vorhandenen Code nicht unterstützen würde?
Da c# und .net in neuen Domänen verwendet werden, werden einige Probleme immer deutlicher.
Als Beispiele für Umgebungen, die kritischer als der Durchschnitt bei Berechnungs überschreitungs Werten sind,
kann ich
Cloud-/Datacenter-Szenarios, in denen die Berechnung abgerechnet wird und die Reaktionsfähigkeit einen
Wettbewerbsvorteil ist.
Spiele/VR/AR mit Soft-Echtzeitanforderungen bei Latenzen
Mit diesem Feature werden keine der vorhandenen stärken, wie z. b. Typsicherheit, geopfert, während einige
gängige Szenarien einen geringeren Aufwand ermöglichen.
2. Kann sichergestellt werden, dass der aufgerufene die Regeln durchführt, wenn er sich für readonly Verträge
entscheidet?
Wir haben bei der Verwendung von eine ähnliche Vertrauensstellung out . Eine falsche Implementierung von
out kann ein nicht bestimmtes Verhalten verursachen, aber in der Realität kommt es selten vor.

Die formalen Überprüfungs Regeln, die mit vertraut sind, ref readonly würden das Vertrauensstellungs
Problem weiter verringern.
Alternativen
Der wichtigste konkurrierende Entwurf ist wirklich "Do Nothing".
Nicht aufgelöste Fragen
Treffen von Besprechungen
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-02-22.md
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-03-01.md
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-08-28.md
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-09-25.md
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-09-27.md
Erzwingung der Sicherheit für Verweis ähnliche
Typen in der Kompilierzeit
04.11.2021 • 29 minutes to read

Einführung
Der Hauptgrund für die zusätzlichen Sicherheitsregeln beim Umgang mit Typen wie Span<T> und
ReadOnlySpan<T> ist, dass solche Typen auf den Ausführungs Stapel beschränkt werden müssen.

Es gibt zwei Gründe, warum Span<T> und ähnliche Typen nur Stapel-Typen sein müssen.
1. Span<T> ist semantisch eine Struktur, die einen Verweis und einen Bereich enthält (ref T data, int length)
. Unabhängig von der eigentlichen Implementierung wäre das Schreiben in eine solche Struktur nicht
atomarisch. Das gleichzeitige "zerreißen" dieser Struktur würde dazu führen length , dass nicht mit dem
übereinstimmt, was zu data Zugriffs enden Zugriffen und typsicherheitsverstößen führt, was letztendlich zu
einer GC-Heap Beschädigung im scheinbar "sicheren" Code führen könnte.
2. Einige Implementierungen von Span<T> enthalten buchstäblich einen verwalteten Zeiger in einem ihrer
Felder. Verwaltete Zeiger werden nicht als Felder von Heap Objekten unterstützt, und Code, der einen
verwalteten Zeiger auf den GC-Heap speichert, stürzt normalerweise bei der JIT-Zeit ab.
Alle oben genannten Probleme würden verringert werden, wenn die Instanzen von Span<T> nur auf dem
Ausführungs Stapel vorhanden sind.
Aufgrund der Komposition entsteht ein zusätzliches Problem. Im allgemeinen wäre es wünschenswert,
komplexere Datentypen zu erstellen, die Span<T> -und-Instanzen einbetten würden ReadOnlySpan<T> . Solche
zusammengesetzten Typen müssten Strukturen sein und alle Risiken und Anforderungen von Teilen Span<T> .
Daher sollten die hier beschriebenen Sicherheitsregeln für den gesamten Bereich von ref-ähnlichen Typen
angezeigt werden.
Die Spezifikation für die Entwurfs Sprache soll sicherstellen, dass die Werte eines Verweis ähnlichen Typs nur im
Stapel vorkommen.

Generalisierte ref-like Typen im Quellcode


ref-like Strukturen werden mithilfe des-Modifizierers explizit im Quellcode gekennzeichnet ref :

ref struct TwoSpans<T>


{
// can have ref-like instance fields
public Span<T> first;
public Span<T> second;
}

// error: arrays of ref-like types are not allowed.


TwoSpans<T>[] arr = null;

Wenn eine Struktur als ref-like festgelegt wird, können in der Struktur Verweis ähnliche Instanzfelder verwendet
werden, und es werden auch alle Anforderungen von Verweis ähnlichen Typen auf die Struktur angewendet.

Metadatendarstellung oder ref-like-Strukturen


Ref-like-Strukturen werden mit dem System. Runtime. CompilerSer vices. isreflikeattribute -Attribut
markiert.
Das-Attribut wird zu allgemeinen Basis Bibliotheken hinzugefügt, z mscorlib . b.. Wenn das Attribut nicht
verfügbar ist, generiert der Compiler eine interne, ähnlich wie andere eingebettete Attribute wie z
IsReadOnlyAttribute . b..

Es wird ein zusätzliches Measure verwendet, um die Verwendung von Verweis ähnlichen Strukturen in
Compilern zu verhindern, die mit den Sicherheitsregeln nicht vertraut sind (Dies schließt c#-Compiler ein, die
vor dem implementiert sind, in dem diese Funktion implementiert ist).
Wenn keine anderen guten Alternativen vorhanden sind, die in alten Compilern ohne Wartung funktionieren,
Obsolete wird ein Attribut mit einer bekannten Zeichenfolge allen Ref-like-Strukturen hinzugefügt. Compiler,
die wissen, wie ref-like-Typen verwendet werden, ignorieren diese spezielle Form von Obsolete .
Eine typische Metadatendarstellung:

[IsRefLike]
[Obsolete("Types with embedded references are not supported in this version of your compiler.")]
public struct TwoSpans<T>
{
// . . . .
}

Hinweis: Es ist nicht das Ziel, es so zu machen, dass die Verwendung von Verweis ähnlichen Typen für alte
Compiler 100% fehlschlägt. Das ist schwer zu erreichen und ist nicht unbedingt erforderlich. Beispielsweise wäre
es immer eine Möglichkeit, die Obsolete mithilfe von dynamischem Code zu umgehen oder beispielsweise ein
Array von Verweis ähnlichen Typen durch Reflektion zu erstellen.
Insbesondere, wenn ein Benutzer ein- Obsolete oder- Deprecated Attribut für einen ref-like-Typ platzieren
möchte, haben wir keine andere Wahl als den vordefinierten, da das Attribut nicht mehrmals Obsolete
angewendet werden kann.

Beispiele:
SpanLikeType M1(ref SpanLikeType x, Span<byte> y)
{
// this is all valid, unconcerned with stack-referring stuff
var local = new SpanLikeType(y);
x = local;
return x;
}

void Test1(ref SpanLikeType param1, Span<byte> param2)


{
Span<byte> stackReferring1 = stackalloc byte[10];
var stackReferring2 = new SpanLikeType(stackReferring1);

// this is allowed
stackReferring2 = M1(ref stackReferring2, stackReferring1);

// this is NOT allowed


stackReferring2 = M1(ref param1, stackReferring1);

// this is NOT allowed


param1 = M1(ref stackReferring2, stackReferring1);

// this is NOT allowed


param2 = stackReferring1.Slice(10);

// this is allowed
param1 = new SpanLikeType(param2);

// this is allowed
stackReferring2 = param1;
}

ref SpanLikeType M2(ref SpanLikeType x)


{
return ref x;
}

ref SpanLikeType Test2(ref SpanLikeType param1, Span<byte> param2)


{
Span<byte> stackReferring1 = stackalloc byte[10];
var stackReferring2 = new SpanLikeType(stackReferring1);

ref var stackReferring3 = M2(ref stackReferring2);

// this is allowed
stackReferring3 = M1(ref stackReferring2, stackReferring1);

// this is allowed
M2(ref stackReferring3) = stackReferring2;

// this is NOT allowed


M1(ref param1) = stackReferring2;

// this is NOT allowed


param1 = stackReferring3;

// this is NOT allowed


return ref stackReferring3;

// this is allowed
return ref param1;
}
Entwurfs Sprachen Spezifikation
Im folgenden wird ein Satz von Sicherheitsregeln für Verweis ähnliche Typen beschrieben ref struct , um
sicherzustellen, dass die Werte dieser Typen nur im Stapel vorkommen. Ein anderer, einfacherer Satz von
Sicherheitsregeln wäre möglich, wenn die lokalen Variablen nicht als Verweis übermittelt werden können. Diese
Spezifikation ermöglicht außerdem die sichere Neuzuweisung von lokalen Ref-Variablen.
Übersicht
Wir ordnen jedem Ausdruck zum Zeitpunkt der Kompilierung das Konzept des Bereichs zu, mit dem dieser
Ausdruck als Escapezeichen versehen werden darf. Ebenso behalten wir für jeden lvalue ein Konzept, mit dem
ein Verweis auf das Escapezeichen "Ref-Safe-to-Escape" versehen werden kann. Bei einem bestimmten lvalue-
Ausdruck können sich diese unterscheiden.
Diese entsprechen der "sicheren Rückgabe" des Features "Ref Locals", aber Sie ist präziser. Wenn der "Safe-to-
return"-Ausdruck eines Ausdrucks nur dann aufgezeichnet wird, ob er die einschließende Methode als Ganzes
mit einem Escapezeichen versehen soll (oder nicht), werden die Datensätze mit sicherer Escapezeichen
aufgezeichnet. Der grundlegende Sicherheitsmechanismus wird wie folgt erzwungen. Bei einer Zuweisung von
einem Ausdruck E1 mit einem abgesicherten Bereich S1 zu einem (lvalue)-Ausdruck E2 mit einem Sicherheits-
zu-escapebereich S2 ist ein Fehler, wenn S2 ein größerer Bereich als S1 ist. Bei der Erstellung befinden sich die
beiden Bereiche S1 und S2 in einer Schachtelungs Beziehung, da ein gültiger Ausdruck immer sicher von einem
Bereich, der den Ausdruck einschließt, zurückgegeben werden kann.
Die Zeit reicht für die Analyse aus, um nur zwei Bereiche zu unterstützen, die sich außerhalb der-Methode
befinden, sowie den Bereich der obersten Ebene der-Methode. Dies liegt daran, dass Ref-like-Werte mit inneren
Bereichen nicht erstellt werden können und lokale Ref-Variablen keine erneute Zuweisung unterstützen. Die
Regeln können jedoch mehr als zwei Bereichs Ebenen unterstützen.
Die genauen Regeln zum Berechnen des Status von " sicher an Rückgabe " eines Ausdrucks und der Regeln für
die Rechtmäßigkeit von Ausdrücken folgen.
Ref-Safe -to -Escape
Das ref-Safe-to-Escape -Zeichen ist ein Bereich, der einen lvalue-Ausdruck einschließt, in den es sicher ist, dass
ein Verweis auf den lvalue-Wert mit Escapezeichen versehen wird. Wenn dieser Bereich die gesamte Methode
ist, sagen wir, dass ein Verweis auf den lvalue-Wert sicher von der-Methode zurückgegeben werden kann.
sichere Escapezeichen
Der Safe-to-Escape -Vorgang ist ein Bereich, der einen Ausdruck einschließt, in den der Wert für den
Escapezeichen sicher ist. Wenn dieser Bereich die gesamte Methode ist, sagen wir, dass der Wert sicher von der-
Methode zurückgegeben werden kann.
Ein Ausdruck, dessen Typ kein ref struct Typ ist, kann von der gesamten einschließenden Methode
abgehandelt werden. Andernfalls verweisen wir auf die unten aufgeführten Regeln.
Parameter
Ein Lvalue, der einen formalen Parameter festlegt , ist Ref-Safe-to-Escape (als Verweis) wie folgt:
Wenn der-Parameter ein- ref ,-oder-Parameter ist out in , ist er von der gesamten Methode (z. b. durch
eine-Anweisung) ref-Safe- Escapezeichen, return ref andernfalls.
Wenn es sich bei dem Parameter um den this Parameter eines Struktur Typs handelt, ist dieser Verweis
sicher in den Bereich der obersten Ebene der Methode (aber nicht in der gesamten Methode selbst). Beispiel
Andernfalls handelt es sich bei dem Parameter um einen value-Parameter, der ref-Safe-to-Escape- Vorgang
mit dem Bereich der obersten Ebene der Methode (aber nicht von der Methode selbst) ist.
Ein Ausdruck, bei dem es sich um einen Rvalue-Wert handelt, der angibt, dass ein formaler Parameter
verwendet wird, kann von der gesamten Methode (z. b. durch eine-Anweisung) als sicherer Escapezeichen
verwendet werden return . Dies gilt auch für den- this Parameter.
Locals
Ein Lvalue, der eine lokale Variable festlegt, ist wie folgt ref-Safe-to-Escape (als Verweis):
Wenn die Variable eine ref Variable ist, wird der ref-Safe-to-Escape -Wert von der ref-Safe- to-Escape-
Aktion des Initialisierungs Ausdrucks übernommen; andernfalls
Die Variable ist ein ref-Safe-to-Escape -Zeichenbereich, in dem Sie deklariert wurde.
Ein Ausdruck, bei dem es sich um einen Rvalue-Wert handelt, der die Verwendung einer lokalen Variablen
angibt, kann wie folgt als sichere Escapezeichen (durch Wert) verwendet werden:
Die oben genannte allgemeine Regel, bei der es sich jedoch nicht um einen Typ handelt, ref struct ist die
sichere Rückgabe von der gesamten einschließenden Methode.
Wenn es sich bei der Variablen um eine Iterations Variable einer foreach Schleife handelt, ist der Safe-to-
Escape -Bereich der Variablen identisch mit dem sicheren foreach Escapezeichen des Ausdrucks der
Schleife.
Eine lokale vom ref struct Typ, die zum Zeitpunkt der Deklaration nicht initialisiert wurde, kann von der
gesamten einschließenden Methode sicher zurückgegeben werden.
Andernfalls ist der Typ der Variable ein- ref struct Typ, und die Deklaration der Variablen erfordert einen
Initialisierer. Der Safe-to-Escape -Bereich der Variablen ist mit dem " Safe-to-Escape" des Initialisierers
identisch.
Feldverweis
Ein Lvalue, der einen Verweis auf ein Feld, e.F , angibt, ist wie folgt ref-Safe-to-Escape (als Verweis):
Wenn e ein Verweistyp ist, ist es ref-Safe-to-Escape von der gesamten Methode; andernfalls
Wenn e ein Werttyp ist, wird das ref-Safe-to-Escape -Zeichen von der ref-Safe-to-Escape -Aktion von
übernommen e .
Ein rvalue-Wert, der einen Verweis auf ein Feld () festlegt, verfügt über einen Bereich mit sicherer
Escapezeichen, der mit dem sicheren Escapezeichen e.F von identisch ist e .
Operatoren einschließlich ?:

Die Anwendung eines benutzerdefinierten Operators wird als Methodenaufruf behandelt.


Bei einem Operator, der einen Rvalue-Wert (z. b. oder) ergibt e1 + e2 c ? e1 : e2 , ist das " Safe-to-Escape"-
Ergebnis der engste Bereich zwischen dem sicheren und Escapezeichen der Operanden des Operators. Folglich
ist für einen unären Operator, der einen Rvalue ergibt (z. b.), das +e Safe-to-Escape -Ergebnis des Operanden.
Bei einem Operator, der einen lvalue ergibt, z. b. c ? ref e1 : ref e2

der ref-Safe-to-Escape -Wert des Ergebnisses ist der engste Bereich zwischen der ref-Safe-to-Escape-
Operation der Operanden des Operators.
der Safe-to-Escape- Operator muss zustimmen, und das ist der sichere Escapezeichen des resultierenden
lvalue.
Methodenaufruf
Ein Lvalue, der sich aus einem Verweis Rückgabe Methodenaufruf e1.M(e2, ...) ergibt , ist Ref-Safe-to-Escape
der kleinsten der folgenden Bereiche:
Die gesamte einschließende Methode
der ref-Safe-to-Escape- Ausdruck aller- ref und out Argument Ausdrücke (mit Ausnahme des
Empfängers)
Für jeden in Parameter der Methode, wenn es einen entsprechenden Ausdruck, bei dem es sich um einen
lvalue handelt, seinen Verweis sicheren Escapezeichen (andernfalls den nächstgelegenen einschließenden
Bereich) gibt.
der Safe-to-Escape- Ausdruck aller Argument Ausdrücke (einschließlich des Empfängers)

Hinweis: das letzte Aufzählungs Zeichen ist erforderlich, um Code wie

var sp = new Span(...)


return ref sp[0];

oder

return ref M(sp, 0);

Ein rvalue, der sich aus einem Methodenaufruf ergibt e1.M(e2, ...) , kann aus den kleinsten der folgenden
Bereiche abgegrenzt werden:
Die gesamte einschließende Methode
der Safe-to-Escape- Ausdruck aller Argument Ausdrücke (einschließlich des Empfängers)
Ein rvalue
Ein rvalue-Wert ist vom nächsten einschließenden Bereich Ref-Safe-to-Escape . Dies tritt z. b. bei einem Aufruf
auf, z M(ref d.Length) . b., wenn d vom Typ ist dynamic . Sie ist auch mit der Behandlung von Argumenten
konsistent, die den Parametern entsprechen (und möglicherweise auch davon subsumdieren) in .
Eigenschafts Aufrufe
Ein Eigenschafts Aufruf ( get oder), der set durch die oben genannten Regeln als Methodenaufruf der
zugrunde liegenden Methode behandelt wird.
stackalloc

Bei einem stackzuweisung-Ausdruck handelt es sich um einen Rvalue-Wert, der sicher mit dem Bereich der
obersten Ebene der Methode (aber nicht aus der gesamten Methode selbst) entfernt werden kann.
Konstruktoraufrufe
Ein- new Ausdruck, der einen Konstruktor aufruft, befolgt dieselben Regeln wie ein Methodenaufruf, der für die
Rückgabe des konstruierten Typs gilt.
Außerdem ist " Safe-to-Escape " nicht breiter als das kleinste der " Safe "-Escapezeichen aller
Argumente/Operanden der objektinitialisiererausdrücke, rekursiv, wenn der Initialisierer vorhanden ist.
Span-Konstruktor
Die Sprache basiert darauf, Span<T> dass kein Konstruktor der folgenden Form vorhanden ist:

void Example(ref int x)


{
// Create a span of length one
var span = new Span<int>(ref x);
}

Ein solcher Konstruktor macht Span<T> , die als Felder verwendet werden, die nicht von einem Feld
unterschieden werden können ref . Die in diesem Dokument beschriebenen Sicherheitsregeln hängen von
ref Feldern ab, die kein gültiges Konstrukt in c# oder .net sind.

default -Ausdrücke
Ein- default Ausdruck kann von der gesamten einschließenden Methode abgehandelt werden.
Spracheinschränkungen
Wir möchten sicherstellen, dass keine ref lokale Variable und keine Variable vom ref struct Typ auf den
Stapel Speicher oder die nicht mehr aktiven Variablen verweist. Daher haben wir die folgenden
Spracheinschränkungen:
Weder ein ref-Parameter noch ein lokaler ref-Parameter oder ein Parameter oder ein lokaler ref struct
Typ eines Typs können in eine Lambda-oder lokale Funktion gehoben werden.
Weder ein ref-Parameter noch ein Parameter eines ref struct Typs kann ein Argument für eine
Iteratormethode oder eine- async Methode sein.
Weder eine lokale ref-Anweisung noch eine lokale eines Typs können sich im Gültigkeitsbereich des
Gültigkeits Bereichs der- ref struct yield return Anweisung oder eines await Ausdrucks befinden.
Ein ref struct Typ kann nicht als Typargument oder als Elementtyp in einem tupeltyp verwendet
werden.
Ein ref struct Typ ist möglicherweise nicht der deklarierte Typ eines Felds, mit dem Unterschied, dass
er der deklarierte Typ eines Instanzfelds eines anderen ist ref struct .
Ein ref struct Typ kann nicht der Elementtyp eines Arrays sein.
Ein Wert eines ref struct Typs darf nicht gekapselt werden:
Es gibt keine Konvertierung von einem ref struct Typ in den Typ object oder den Typ
System.ValueType .
Ein ref struct Typ kann nicht für die Implementierung einer Schnittstelle deklariert werden.
Es kann keine Instanzmethode object , die in oder in deklariert System.ValueType , aber in einem Typ
nicht überschrieben wurde ref struct , mit einem Empfänger dieses Typs aufgerufen werden
ref struct .
Es kann keine Instanzmethode eines ref struct Typs durch die Methoden Konvertierung in einen
Delegattyp aufgezeichnet werden.
Bei einer ref-Neuzuweisung ref e1 = ref e2 muss das ref-Safe-to-Escape -Zeichen mindestens e2 so
breit sein wie das ref-Safe-to-Escape -Zeichen von e1 .
Bei einer ref Return-Anweisung return ref e1 muss der ref-Safe-to-Escape -Wert e1 von der gesamten
Methode ref-Safe-to-Escape sein. (TODO: benötigen wir außerdem eine Regel, die e1 von der gesamten
Methode abgehandelt werden muss oder die redundant ist?)
Bei einer Return return e1 -Anweisung muss der Safe-to -Escape-Wert e1 von der gesamten Methode
Abgesicherter Escapezeichen sein.
Wenn bei einer Zuweisung e1 = e2 der Typ von e1 ein Typ ist ref struct , muss der Safe-to-Escape-
Wert mindestens e2 so breit sein wie der sichere e1 und Escapezeichen von.
Bei einem Methodenaufruf, bei dem ein- ref oder- out Argument eines ref struct Typs
(einschließlich des Empfängers) mit einem Safe-to-Escape -E1 vorhanden ist, kann kein Argument
(einschließlich des Empfängers) einen engeren Sicherheits-zu- Escapezeichen als E1 aufweisen. Beispiel
Eine lokale Funktion oder eine anonyme Funktion verweist möglicherweise nicht auf eine lokale oder
einen Parameter vom Typ, der ref struct in einem einschließenden Bereich deklariert wurde.

Problem öffnen: Wir benötigen eine Regel, mit der wir einen Fehler verursachen können, wenn ein Stapel
Wert eines ref struct Typs auf einen Erwartungs Ausdruck überschwemmt werden muss, z. b. im Code.
Foo(new Span<int>(...), await e2);

Erklärungen
In diesen Erläuterungen und Beispielen wird erläutert, warum viele der oben aufgeführten Sicherheitsregeln
vorhanden sind.
Methodenargumente müssen entsprechen
Wenn eine Methode aufgerufen wird, bei der ein- out ref Parameter, ref struct der den Empfänger
einschließt, den Empfänger enthält, muss alle ref struct die gleiche Lebensdauer haben. Dies ist erforderlich,
weil c# alle Entscheidungen hinsichtlich der Lebensdauer Sicherheit auf der Grundlage der Informationen in der
Signatur der Methode und der Lebensdauer der Werte an der aufrufssite treffen muss.
Wenn ref die Parameter vorhanden sind, ref struct können Sie die Inhalte umtauschen. Daher müssen wir
an der aufrufssite sicherstellen, dass alle diese möglichen Austausch Vorgängen kompatibel sind. Wenn die
Sprache diese nicht erzwingt, wird ein fehlerhafter Code wie der folgende zugelassen.

void M1(ref Span<int> s1)


{
Span<int> s2 = stackalloc int[1];
Swap(ref s1, ref s2);
}

void Swap(ref Span<int> x, ref Span<int> y)


{
// This will effectively assign the stackalloc to the s1 parameter and allow it
// to escape to the caller of M1
ref x = ref y;
}

Die Einschränkung für den Empfänger ist erforderlich, da kein Inhalt von Ref-Safe-to-Escape-Vorgang
bereitgestellte Werte speichern kann. Dies bedeutet, dass Sie mit einer nicht übereinstimmenden Lebensdauer
wie folgt eine typsicherheitslücke erstellen können:

ref struct S
{
public Span<int> Span;

public void Set(Span<int> span)


{
Span = span;
}
}

void Broken(ref S s)
{
Span<int> span = stackalloc int[1];

// The result of a stackalloc is now stored in s.Span and escaped to the caller
// of Broken
s.Set(span);
}

Struct this Escape


Wenn es um Span-Sicherheitsregeln geht, this wird der Wert in einem Instanzmember als Parameter für den
Member modelliert. Für einen struct ist der Typ, der this ref S in einem einfach ist, class einfach S (für
Member eines class / struct benannten s).
Hat jedoch andere this Escaperegeln als andere ref Parameter. Insbesondere ist es nicht Ref-Safe-to-Escape,
während andere Parameter lauten:

ref struct S
{
int Field;

// Illegal because `this` isn't safe to escape as ref


ref int Get() => ref Field;

// Legal
ref int GetParam(ref int p) => ref p;
}

Der Grund für diese Einschränkung hat nur wenig zu tun, was mit dem Element Aufruf zu tun ist struct . Es
gibt einige Regeln, die in Bezug auf den Element Aufruf für Member, struct bei denen der Empfänger ein
rvalue ist, ausgearbeitet werden müssen. Das ist jedoch sehr genehmigbar.
Der Grund für diese Einschränkung ist tatsächlich der Schnittstellen Aufruf. Insbesondere wird darauf
zurückgegriffen, ob das folgende Beispiel nicht kompiliert werden soll.

interface I1
{
ref int Get();
}

ref int Use<T>(T p)


where T : I1
{
return ref p.Get();
}

Beachten Sie den Fall, in dem T als instanziiert wird struct . Wenn der this Parameter ref-Safe-to-Escape ist,
kann die Rückgabe von p.Get auf den Stapel zeigen (insbesondere könnte es sich um ein Feld innerhalb des
instanziierten Typs handeln T ). Dies bedeutet, dass die Sprache diese Stichprobe nicht kompilieren kann, da
Sie ref an einen Stapel Speicherort zurückgeben könnte. Wenn hingegen this nicht Ref-Safe-to-Escape ist,
p.Get kann nicht auf den Stapel verwiesen werden, und daher ist die Rückgabe sicher.

Aus diesem Grund geht es bei der escapeshaftigkeit von this in einem struct wirklich um Schnittstellen. Die
Anwendung kann für Sie geeignet sein, aber Sie hat einen Kompromiss. Der Entwurf wurde schließlich
zugunsten von Schnittstellen flexibler.
Es besteht die Möglichkeit, dies in Zukunft zu lockern.

Überlegungen für die Zukunft


Länge einer Spanne <T> über Ref-Werten
Obwohl es heute nicht zulässig ist, gibt es Fälle, in denen das Erstellen einer Länge einer Span<T> Instanz über
einen Wert vorteilhaft wäre:
void RefExample()
{
int x = ...;

// Today creating a length one Span<int> requires a stackalloc and a new


// local
Span<int> span1 = stackalloc [] { x };
Use(span1);
x = span1[0];

// Simpler to just allow length one span


var span2 = new Span<int>(ref x);
Use(span2);
}

Diese Funktion wird immer wichtiger, wenn wir die Einschränkungen für Puffer mit fester Größe anheben, da
dies für Span<T> Instanzen mit einer noch größeren Länge möglich wäre.
Wenn es jemals erforderlich ist, diesen Pfad zu verlassen, kann die Sprache dies ermöglichen, indem
sichergestellt wird, dass diese Span<T> Instanzen nur nach unten gerichtet sind. Das heißt, dass Sie nur einmal
sicher in den Bereich, in dem Sie erstellt wurden, wieder hergestellt werden konnten. Dadurch wird
sichergestellt, dass die Sprache niemals einen Wert als Escapezeichen für eine ref Methode über eine
ref struct Rückgabe oder ein Feld von berücksichtigt ref struct . Dies würde wahrscheinlich auch weitere
Änderungen erfordern, um solche Konstruktoren so zu erkennen, dass Sie einen ref Parameter auf diese Weise
erfassen.
Nicht schließende benannte Argumente
04.11.2021 • 5 minutes to read

Zusammenfassung
Zulassen, dass benannte Argumente an einer nicht nachfolgenden Position verwendet werden, solange Sie an
der richtigen Position verwendet werden. Beispiel: DoSomething(isEmployed:true, name, age); .

Motivation
Die Hauptmotivation besteht darin, redundante Informationen zu vermeiden. Es ist üblich, ein Argument zu
benennen, das wahrsten ist (z. b. null true ), um den Code zu verdeutlichen, anstatt Argumente außerhalb der
Reihenfolge zu übergeben. Dies ist zurzeit nicht zulässig ( CS1738 ), es sei denn, die folgenden Argumente
werden ebenfalls benannt.

DoSomething(isEmployed:true, name, age); // currently disallowed, even though all arguments are in position
// CS1738 "Named argument specifications must appear after all fixed arguments have been specified"

Einige zusätzliche Beispiele:

public void DoSomething(bool isEmployed, string personName, int personAge) { ... }

DoSomething(isEmployed:true, name, age); // currently CS1738, but would become legal


DoSomething(true, personName:name, age); // currently CS1738, but would become legal
DoSomething(name, isEmployed:true, age); // remains illegal
DoSomething(name, age, isEmployed:true); // remains illegal
DoSomething(true, personAge:age, personName:name); // already legal

Dies würde auch mit Parametern funktionieren:

public class Task


{
public static Task When(TaskStatus all, TaskStatus any, params Task[] tasks);
}
Task.When(all: TaskStatus.RanToCompletion, any: TaskStatus.Faulted, task1, task2)

Detaillierter Entwurf
In "7.5.1" (Argument Listen) gibt die Spezifikation derzeit Folgendes an:

Ein Argument mit einem Argument Namen wird als benanntes Argument bezeichnet, wohingegen ein
Argument ohne Argument Name ein Positions Argument ist. Es ist ein Fehler für ein Positions Argument,
das nach einem benannten Argument in einer Argumentliste angezeigt wird.

Der Vorschlag besteht darin, diesen Fehler zu entfernen und die Regeln für die Suche nach dem entsprechenden
Parameter für ein Argument ("7.5.1.1") zu aktualisieren:
Argumente in der Argumentliste von Instanzkonstruktoren, Methoden, Indexern und Delegaten:
[vorhandene Regeln]
Ein unbenanntes Argument entspricht keinem Parameter, wenn es sich nach einem benannten Argument
außerhalb der Position oder einem benannten params-Argument befindet.
Dies verhindert insbesondere das Aufrufen void M(bool a = true, bool b = true, bool c = true, ); von mit
M(c: false, valueB); . Das erste Argument wird außerhalb der Position verwendet (das Argument wird an der
ersten Position verwendet, aber der Parameter mit dem Namen "c" befindet sich an der dritten Position). Daher
sollten die folgenden Argumente benannt werden.
Anders ausgedrückt: nicht nachfolgende benannte Argumente sind nur zulässig, wenn der Name und die
Position dazu führen, den gleichen entsprechenden Parameter zu suchen.

Nachteile
Dieser Vorschlag verschärft vorhandene Feinheiten mit benannten Argumenten in der Überladungs Auflösung.
Beispiel:

void M(int x, int y) { }


void M<T>(T y, int x) { }

void M2()
{
M(3, 4);
M(y: 3, x: 4); // Invokes M(int, int)
M(y: 3, 4); // Invokes M<T>(T, int)
}

Sie können diese Situation heute erzielen, indem Sie die Parameter austauschen:

void M(int y, int x) { }


void M<T>(int x, T y) { }

void M2()
{
M(3, 4);
M(x: 3, y: 4); // Invokes M(int, int)
M(3, y: 4); // Invokes M<T>(int, T)
}

Wenn Sie über zwei Methoden und verfügen void M(int a, int b) void M(int x, string y) , erzeugt der
falsche Aufruf M(x: 1, 2) eine Diagnose auf der Grundlage der zweiten Überladung ("kann nicht von" int "in"
String "konvertiert werden). Dieses Problem ist bereits vorhanden, wenn das benannte Argument an einer
nachfolgenden Position verwendet wird.

Alternativen
Es gibt einige Alternativen zu berücksichtigende Aspekte:
Der Status quo
Bereitstellen von IDE-Unterstützung zum Ausfüllen aller Namen von nachfolgenden Argumenten, wenn Sie
einen bestimmten Namen in der Mitte eingeben.
Beide sind von ausführlicheren Ausführlichkeit, da Sie mehrere benannte Argumente einführen, auch wenn Sie
nur einen Namen eines Literals am Anfang der Argumentliste benötigen.

Nicht aufgelöste Fragen


Treffen von Besprechungen
Die Funktion wurde kurz am 16. Mai 2017 in LDM erläutert, mit Genehmigung im Prinzip ("OK", um zu
"Vorschlag/Prototyp" zu wechseln). Es wurde auch kurz am 28. Juni 2017 erläutert.
Bezieht sich auf die erste Diskussion https://github.com/dotnet/csharplang/issues/518 bezieht sich auf das
Problem. https://github.com/dotnet/csharplang/issues/570
private protected
04.11.2021 • 18 minutes to read

[x] vorgeschlagen
[x] Prototyp: Fertig stellen
[x] Implementierung: Complete
[x] Spezifikation: Fertig stellen

Zusammenfassung
Machen Sie den CLR- protectedAndInternal Barrierefreiheits Grad in c# als verfügbar private protected .

Motivation
Es gibt viele Situationen, in denen eine API Member enthält, die nur für die Implementierung und Verwendung
durch Unterklassen in der Assembly, die den Typ bereitstellt, verwendet werden sollen. Obwohl die CLR für
diesen Zweck eine Barrierefreiheits Ebene bereitstellt, ist Sie in c# nicht verfügbar. Folglich werden API-Besitzer
gezwungen, entweder den internal Schutz und die Self-Disziplin oder eine benutzerdefinierte Analyse zu
verwenden oder protected mit zusätzlichen Dokumentationen zu verwenden, die erläutern, dass der Member,
der in der öffentlichen Dokumentation für den Typ erscheint, nicht als Teil der öffentlichen API gedacht ist.
Beispiele der letzteren finden Sie unter Member von Roslyn, CSharpCompilationOptions deren Namen mit
beginnen Common .
Durch die direkte Unterstützung dieser Zugriffsebene in c# können diese Umstände natürlich in der Sprache
ausgedrückt werden.

Detaillierter Entwurf
private protected Access-Modifizierer
Es wird vorgeschlagen, eine neue zugriffsmodifiziererkombination hinzuzufügen private protected (die in
jeder beliebigen Reihenfolge unter den Modifizierern vorkommen kann). Dies wird dem CLR-Konzept von
protectedandinternal zugeordnet und verwendet dieselbe Syntax, die derzeit in C++/CLIverwendet wird.
Auf einen deklarierten Member private protected kann innerhalb einer Unterklasse seines Containers
zugegriffen werden, wenn sich diese Unterklasse in derselben Assembly befindet wie der Member.
Die Sprachspezifikation wird wie folgt geändert (Additions Fett). Abschnitts Nummern werden unten nicht
angezeigt, da Sie je nach Version der Spezifikation, in die Sie integriert ist, variieren können.

Die deklarierte Barrierefreiheit eines Members kann eine der folgenden sein:

Public, das durch Einschließen eines öffentlichen Modifizierers in die Element Deklaration ausgewählt wird.
Die intuitive Bedeutung von Public ist "Access not Limited".
Geschützt, das durch Einschließen eines geschützten Modifizierers in die Element Deklaration ausgewählt
wird. Die intuitive Bedeutung von Protected ist "der Zugriff ist auf die enthaltende Klasse oder auf Typen
beschränkt, die von der enthaltenden Klasse abgeleitet sind".
Intern, das durch Einschließen eines internen Modifizierers in die Element Deklaration ausgewählt wird. Die
intuitive Bedeutung von Internal ist "Zugriff beschränkt auf diese Assembly".
Geschützter interner, der durch Einschließen eines geschützten und eines internen Modifizierers in die
Element Deklaration ausgewählt wird. Die intuitive Bedeutung geschützter interner ist "Zugriff innerhalb
dieser Assembly und von Typen, die von der enthaltenden Klasse abgeleitet sind".
Privat geschützt, das durch Einschließen eines privaten und eines geschützten Modifizierers in
die Element Deklaration ausgewählt wird. Die intuitive Bedeutung von private Protected ist
"Zugriff innerhalb dieser Assembly nach Typen, die von der enthaltenden Klasse abgeleitet
sind".

Abhängig vom Kontext, in dem eine Element Deklaration stattfindet, sind nur bestimmte Typen von
deklarierter Barrierefreiheit zulässig. Wenn eine Member-Deklaration keine Zugriffsmodifizierer enthält,
bestimmt der Kontext, in dem die Deklaration stattfindet, die standardmäßige deklarierte Barrierefreiheit.

Namespaces verfügen implizit über eine Public deklarierte Barrierefreiheit. Für Namespace Deklarationen
sind keine Zugriffsmodifizierer zulässig.
Typen, die direkt in Kompilierungs Einheiten oder Namespaces deklariert werden (im Gegensatz zu anderen
Typen), können über eine öffentliche oder intern deklarierte Barrierefreiheit verfügen, und Sie erhalten
standardmäßig intern deklarierten Zugriff.
Klassenmember können eine der fünf Arten von deklarierter Barrierefreiheit aufweisen und werden
standardmäßig als private deklarierte Barrierefreiheit angezeigt. [Hinweis: ein Typ, der als Member einer
Klasse deklariert ist, kann eine der fünf Arten der deklarierten Barrierefreiheit aufweisen, wohingegen ein als
Member eines Namespaces deklarierter Typ nur über eine öffentliche oder intern deklarierte Barrierefreiheit
verfügen kann. Hinweis Ende]
Strukturmember können als "Public", "Internal" oder "private" deklariert werden und sind standardmäßig für
die private deklarierte Barrierefreiheit, da Strukturen implizit versiegelt sind. Strukturmember, die in einer
Struktur eingeführt werden ( d. h. nicht von dieser Struktur geerbt), können keine geschützte , geschützte
interne oder private geschützte Barrierefreiheit haben. [Hinweis: ein Typ, der als Member einer Struktur
deklariert wurde, kann als "Public", "Internal" oder "private" deklariert werden, wohingegen ein Typ, der als
Member eines Namespaces deklariert ist, nur über eine öffentliche oder intern deklarierte Barrierefreiheit
verfügen kann. Hinweis Ende]
Schnittstellenmember haben implizit öffentlich deklarierte Zugriffsmöglichkeiten. Zugriffsmodifizierer sind
für Schnittstellenmember-Deklarationen unzulässig
Enumerationsmember haben implizit öffentlich deklarierte Zugriffsmöglichkeiten. Es sind keine
Zugriffsmodifizierer für Enumerationsmember zulässig.

Die Zugriffs Domäne eines geschachtelten Members M, der in einem Typ T innerhalb eines Programms P
deklariert ist, wird wie folgt definiert (Beachten Sie, dass M selbst möglicherweise ein Typ sein kann):

Wenn die deklarierte Barrierefreiheit von m öffentlich ist, ist die Zugriffs Domäne m die Zugriffs Domäne T.
Wenn die deklarierte Zugriffsart von M als intern geschützt ist, lassen Sie D den Programmtext von P und
den Programmtext jedes Typs, der von T abgeleitet ist, als außerhalb von P deklariert. Die Zugriffs Domäne M
ist die Schnittmenge der Zugriffs Domäne T mit D.
Wenn die deklarier te Zugriffsar t von M Privat geschützt ist, lassen Sie die Schnittmenge des
Programm Texts von P und des Programm Texts eines beliebigen Typs, der von T abgeleitet
wird, angegeben werden. Die Zugriffs Domäne M ist die Schnittmenge der Zugriffs Domäne T
mit D.
Wenn die deklarierte Zugriffsart von M geschützt ist, lassen Sie D den Programmtext von t und den
Programmtext eines beliebigen Typs, der von t abgeleitet ist, darstellen. Die Zugriffs Domäne M ist die
Schnittmenge der Zugriffs Domäne T mit D.
Wenn die deklarierte Barrierefreiheit von m intern ist, entspricht die Zugriffs Domäne m der Schnittmenge
der Zugriffs Domäne T mit dem Programmtext P.
Wenn die deklarierte Barrierefreiheit von m privat ist, ist die Zugriffs Domäne m der Programmtext von T.

Wenn außerhalb des Programm Texts der Klasse, in der Sie deklariert ist, auf einen geschützten oder
privaten Member der geschützten Instanz zugegriffen wird, und wenn außerhalb des Programm Texts des
Programms, in dem es deklariert ist, auf einen geschützten internen Instanzmember zugegriffen wird,
erfolgt der Zugriff innerhalb einer Klassen Deklaration, die von der Klasse abgeleitet ist, in der Sie deklariert
ist. Darüber hinaus muss der Zugriff durch eine Instanz dieses abgeleiteten Klassen Typs oder eines aus der
Klasse erstellten Klassen Typs erfolgen. Diese Einschränkung verhindert, dass eine abgeleitete Klasse auf
geschützte Member anderer abgeleiteter Klassen zugreift, auch wenn die Elemente von derselben
Basisklasse geerbt werden.

Die zulässigen Zugriffsmodifizierer und der Standard Zugriff für eine Typdeklaration hängen vom Kontext
ab, in dem die Deklaration stattfindet (9.5.2):

In Kompilierungs Einheiten oder Namespaces deklarierte Typen können öffentlichen oder internen Zugriff
haben. Der Standardwert ist "interner Zugriff".
In Klassen deklarierte Typen können über einen öffentlichen, geschützten internen, privaten , geschützten,
geschützten, internen oder privaten Zugriff verfügen. Der Standardwert ist privater Zugriff.
In Strukturen deklarierte Typen können über öffentlichen, internen oder privaten Zugriff verfügen. Der
Standardwert ist privater Zugriff.

Eine statische Klassen Deklaration unterliegt den folgenden Einschränkungen:

Eine statische Klasse darf keinen versiegelten oder abstrakten Modifizierer enthalten. (Da eine statische
Klasse jedoch nicht instanziiert oder von abgeleitet werden kann, verhält sie sich so, als ob Sie versiegelt und
abstrakt wäre.)
Eine statische Klasse darf keine Klassenbasis Spezifikation ("16.2.5") enthalten und kann weder eine
Basisklasse noch eine Liste implementierter Schnittstellen explizit angeben. Eine statische Klasse erbt implizit
vom Type-Objekt.
Eine statische Klasse darf nur statische Member enthalten ("16.4.8"). [Hinweis: alle Konstanten und Typen von
Typen werden als statische Member klassifiziert. Hinweis Ende]
Eine statische Klasse darf keine Member mit geschützter , privater oder geschützter interner Barrierefreiheit
haben.

Es handelt sich um einen Kompilierzeitfehler, der gegen diese Einschränkungen verstößt.

Eine Klassenmember-Deklaration kann eine der fünfmöglichen Ar ten von deklarierten Barrierefreiheit
aufweisen ("9.5.2"): Public, private Protected , protected internal, protected, internal oder private. Mit
Ausnahme der geschützten internen und privaten geschützten Kombi Nationen ist es ein
Kompilierzeitfehler, wenn mehr als ein Zugriffsmodifizierer angegeben wird. Wenn eine Klassenmember-
Deklaration keine Zugriffsmodifizierer enthält, wird als private angenommen.

Nicht-in-Typen können über öffentliche oder intern deklarierte Barrierefreiheit verfügen und standardmäßig
über intern deklarierte Zugriffsmöglichkeiten verfügen. In Form von untergeordneten Typen können auch
diese Formen der deklarierten Barrierefreiheit sowie eine oder mehrere zusätzliche Formen der deklarierten
Barrierefreiheit enthalten sein, je nachdem, ob der enthaltende Typ eine Klasse oder Struktur ist:
Ein in einer Klasse deklarierter, in einer Klasse deklarierter Typ kann eine beliebige von fünfFormen
deklarierter Barrierefreiheit aufweisen (Public, private Protected , protected internal, protected, internal
oder private), und, wie bei anderen Klassenmembern, standardmäßig auf private deklarierte Barrierefreiheit
festgelegt.
Ein in einer Struktur deklarierter, in einer Struktur deklarierter Typ kann eine beliebige von drei Formen der
deklarierten Barrierefreiheit (Public, internal oder private) aufweisen und, wie andere Strukturmember,
standardmäßig auf private deklarierte Barrierefreiheit festgelegt werden.

Die Methode, die durch eine Überschreibungs Deklaration überschrieben wird, wird als die überschriebene
Basis Methode für eine in einer Klasse C deklarierte Überschreibungs Methode M bezeichnet. die
überschriebene Basis Methode wird bestimmt, indem jeder Basis Klassentyp von c untersucht wird,
beginnend mit dem direkten Basis Klassentyp von c und mit jedem nachfolgenden direkten Basis Klassentyp,
bis in einem gegebenen Basis Klassentyp mindestens eine barrierefreie Methode gefunden wird, die die
gleiche Signatur wie M nach der Ersetzung von aufweist. Typargumente. Zum Ermitteln der überschriebenen
Basis Methode gilt eine Methode als verfügbar, wenn Sie öffentlich ist, wenn Sie geschützt ist, wenn Sie
geschützt ist, oder wenn Sie intern oder privat geschützt ist und im selben Programm wie C deklariert
ist.

Die Verwendung der Accessormodifizierer unterliegt den folgenden Einschränkungen:

Ein Accessor-Modifizierer darf nicht in einer Schnittstelle oder in einer expliziten Schnittstellenmember-
Implementierung verwendet werden.
Für eine Eigenschaft oder einen Indexer, der über keinen Überschreibungsmodifizierer verfügt, ist ein
Accessor-Modifier nur zulässig, wenn die Eigenschaft oder der Indexer sowohl einen get-als auch einen Set-
Accessor aufweist und dann nur für einen dieser Accessoren zulässig ist.
Für eine Eigenschaft oder einen Indexer, der einen Überschreibungsmodifizierer enthält, muss ein Accessor
mit dem Accessor-Modifier (sofern vorhanden) des Accessors identisch sein, der überschrieben wird.
Der Accessor-Modifizierer muss eine Barrierefreiheit deklarieren, die strikt restriktiver ist als der deklarierte
Zugriff auf die Eigenschaft oder den Indexer selbst. Genauer gesagt:
Wenn die Eigenschaft oder der Indexer einen deklarierten Zugriff auf Public hat, kann der Accessor-
Modifizierer entweder Privat geschützt , geschützt, geschützt, intern, geschützt oder privat sein.
Wenn die Eigenschaft oder der Indexer über eine deklarierte Zugriffsmethode für protected internal
verfügt, kann der Accessor-Modifizierer entweder Privat geschützt , intern, geschützt oder privat
sein.
Wenn die Eigenschaft oder der Indexer eine deklarierte Zugriffsmethode (intern oder geschützt)
aufweist, muss der Accessor-Modifier entweder privat geschützt oder privat sein.
Wenn die Eigenschaft oder der Indexer über eine deklarier te Zugriffsmethode für private
geschützt verfügt, muss der Accessor-Modifier privat sein.
Wenn die Eigenschaft oder der Indexer einen deklarierten Zugriff auf private hat, kann kein Accessor-
Modifizierer verwendet werden.

Da die Vererbung für Strukturen nicht unterstützt wird, kann die deklarierte Barrierefreiheit eines
Strukturmembers nicht geschützt, Privat geschützt oder geschützt werden.

Nachteile
Wie bei allen Sprach Features müssen wir Fragen, ob die zusätzliche Komplexität der Sprache in der zusätzlichen
Klarheit für den Text der c#-Programme, die von der Funktion profitieren würden, zurückgegeben wird.

Alternativen
Eine Alternative wäre die Bereitstellung einer API, die ein Attribut und einen Analyzer kombiniert. Das-Attribut
wird vom Programmierer für ein-Element platziert, internal um darauf hinzuweisen, dass der Member nur in
Unterklassen verwendet werden soll, und der Analyzer prüft, ob diese Einschränkungen befolgt werden.

Nicht aufgelöste Fragen


Die Implementierung ist größtenteils fertiggestellt. Das einzige geöffnete Arbeits Element ist das Entwerfen
einer entsprechenden Spezifikation für VB.

Treffen von Besprechungen


TBD
Bedingte Verweis Ausdrücke
04.11.2021 • 3 minutes to read

Das Muster, eine Verweis Variable an einen oder einen anderen Ausdruck bedingt zu binden, ist in c# derzeit
nicht ausdrucksfähig.
Die typische Problem Umgehung besteht darin, eine Methode wie die folgende einzuführen:

ref T Choice(bool condition, ref T consequence, ref T alternative)


{
if (condition)
{
return ref consequence;
}
else
{
return ref alternative;
}
}

Beachten Sie, dass dies keine genaue Ersetzung eines ternären ist, da alle Argumente an der aufrufssite
ausgewertet werden müssen.
Folgendes funktioniert nicht wie erwartet:

// will crash with NRE because 'arr[0]' will be executed unconditionally


ref var r = ref Choice(arr != null, ref arr[0], ref otherArr[0]);

Die vorgeschlagene Syntax sieht wie folgt aus:

<condition> ? ref <consequence> : ref <alternative>;

Der obige Versuch mit "Choice" kann mithilfe von "Ref ternäre" Ordnungs gemäß geschrieben werden:

ref var r = ref (arr != null ? ref arr[0]: ref otherArr[0]);

Der Unterschied besteht darin, dass die Folge und Alternative Ausdrücke auf wirklich bedingte Weise aufgerufen
werden. Daher wird kein Absturz angezeigt, wenn arr == null
Der ternäre Verweis ist nur ein ternärer Verweis, bei dem Alternative und Folge Refs sind. Dies erfordert
natürlich, dass die Folge/Alternative Operanden Lvalues sind. Außerdem ist es erforderlich, dass diese Folge und
Alternative über Typen verfügen, die in einander als Identitätswechsel konvertierbar sind.
Der Typ des Ausdrucks wird ähnlich wie der für die reguläre ternäre berechnet. Das heißt, in einem Fall, wenn
eine Folge und eine Alternative Identitäts konvertierbar, aber unterschiedliche Typen aufweisen, gelten die
vorhandenen Regeln zum Zusammenführen von Typen.
"Safe-to-return" wird von den bedingten Operanden konservativ angenommen. Wenn eine der beiden Optionen
unsicher ist, ist die Rückgabe unsicher.
Ref ternäre ist ein Lvalue und kann daher als Verweis erfolgreich/zugewiesen/zurückgegeben werden.
// pass by reference
foo(ref (arr != null ? ref arr[0]: ref otherArr[0]));

// return by reference
return ref (arr != null ? ref arr[0]: ref otherArr[0]);

Dabei kann es sich um einen lvalue-Wert handeln, der ebenfalls zugewiesen werden kann.

// assign to
(arr != null ? ref arr[0]: ref otherArr[0]) = 1;

Ref ternäre kann auch in einem regulären (nicht Verweis-) Kontext verwendet werden. Obwohl es nicht üblich
wäre, sollten Sie nur ein reguläres Ternäres verwenden.

int x = (arr != null ? ref arr[0]: ref otherArr[0]);

Implementierungs Hinweise:
Die Komplexität der Implementierung wäre anscheinend die Größe einer mittelgroßen zu großen
Fehlerbehebung. -I. E ist nicht sehr aufwendig. Ich denke nicht, dass wir Änderungen an der Syntax oder der
Verarbeitung benötigen. Es gibt keine Auswirkung auf Metadaten oder Interop. Die Funktion ist vollständig
Ausdrucks basiert. Keine Auswirkung auf Debuggen/PDB
Ziffern Trennzeichen nach 0B oder 0x zulassen
04.11.2021 • 2 minutes to read

In c# 7,2 erweitern wir den Satz von stellen, die Ziffern Trennzeichen (Unterstrich Zeichen) in ganzzahligen
literalen enthalten können. Ab c# 7,0 sind Trennzeichen zwischen den Ziffern eines Literals zulässig. In c# 7,2
sind nun auch Ziffern Trennzeichen vor der ersten signifikanten Ziffer eines binären oder hexadezimalen Literals
nach dem Präfix zulässig.

123 // permitted in C# 1.0 and later


1_2_3 // permitted in C# 7.0 and later
0x1_2_3 // permitted in C# 7.0 and later
0b101 // binary literals added in C# 7.0
0b1_0_1 // permitted in C# 7.0 and later

// in C# 7.2, _ is permitted after the `0x` or `0b`


0x_1_2 // permitted in C# 7.2 and later
0b_1_0_1 // permitted in C# 7.2 and later

Es ist nicht zulässig, dass ein dezimales Ganzzahlliteral einen führenden Unterstrich hat. Ein Token wie _123 ist
ein Bezeichner.
Nicht verwaltete Typeinschränkung
04.11.2021 • 8 minutes to read

Zusammenfassung
Die nicht verwaltete Einschränkungs Funktion bietet eine sprach Erzwingung für die Klasse von Typen, die in der
c#-Sprachspezifikation als "nicht verwaltete Typen" bezeichnet werden. Dies wird im Abschnitt 18,2 als Typ
definiert, der kein Referenztyp ist und keine Verweistyp Felder auf einer beliebigen Schachtelungs Ebene enthält.

Motivation
Die Hauptmotivation besteht darin, das Erstellen von Interop-Code auf niedriger Ebene in c# zu vereinfachen.
Nicht verwaltete Typen sind einer der Kern Bausteine für Interop-Code, aber aufgrund der fehlenden
Unterstützung in Generika ist es unmöglich, wiederverwendbare Routinen für alle nicht verwalteten Typen zu
erstellen. Stattdessen sind Entwickler gezwungen, den gleichen Code für die kesselplatte für jeden nicht
verwalteten Typ in der Bibliothek zu erstellen:

int Hash(Point point) { ... }


int Hash(TimeSpan timeSpan) { ... }

Um diese Art von Szenario zu ermöglichen, wird in der Sprache eine neue Einschränkung eingeführt: nicht
verwaltet:

void Hash<T>(T value) where T : unmanaged


{
...
}

Diese Einschränkung kann nur von Typen erfüllt werden, die in die nicht verwaltete Typdefinition in der c#-
Sprachspezifikation passen. Eine andere Möglichkeit der Betrachtung ist, dass ein Typ die nicht verwaltete
Einschränkung erfüllt, wenn er auch als Zeiger verwendet werden kann.

Hash(new Point()); // Okay


Hash(42); // Okay
Hash("hello") // Error: Type string does not satisfy the unmanaged constraint

Typparameter mit der nicht verwalteten Einschränkung können alle Features verwenden, die für nicht
verwaltete Typen verfügbar sind: Zeiger, korrigiert usw...

void Hash<T>(T value) where T : unmanaged


{
// Okay
fixed (T* p = &value)
{
...
}
}

Diese Einschränkung ermöglicht außerdem die effiziente Konvertierung zwischen strukturierten Daten und
Streams von Bytes. Dies ist ein Vorgang, der in Netzwerk Stapeln und serialisierungsschichten üblich ist:
Span<byte> Convert<T>(ref T value) where T : unmanaged
{
...
}

Solche Routinen sind von Vorteil, da Sie zur Kompilierzeit und Zuordnungs Kosten sicher sind. Interop-Autoren
können dies nicht tun (auch wenn es sich um eine Ebene handelt, bei der Leistungs wichtig ist). Stattdessen
müssen Sie Routinen verwenden, die über teure Laufzeitüberprüfungen verfügen, um sicherzustellen, dass die
Werte ordnungsgemäß nicht verwaltet werden.

Detaillierter Entwurf
In der Sprache wird eine neue Einschränkung mit dem Namen eingeführt unmanaged . Um diese Einschränkung
zu erfüllen, muss ein Typ eine Struktur sein, und alle Felder des Typs müssen in eine der folgenden Kategorien
fallen:
Weisen Sie den Typ sbyte , byte , short ,,, ushort int uint , long , ulong , char , float , double ,
decimal , oder auf bool IntPtr UIntPtr .
Ein beliebiger enum Typ.
Ist ein Zeigertyp.
Dabei handelt es sich um eine benutzerdefinierte Struktur, die der unmanaged Einschränkung entspricht.

Vom Compiler generierte Instanzfelder, wie z. b. solche, die automatisch implementierte Eigenschaften
unterstützen, müssen diese Einschränkungen ebenfalls erfüllen.
Beispiel:

// Unmanaged type
struct Point
{
int X;
int Y {get; set;}
}

// Not an unmanaged type


struct Student
{
string FirstName;
string LastName;
}

Die- unmanaged Einschränkung kann nicht mit struct , class oder kombiniert werden new() . Diese
Einschränkung ist von dem Fakt abgeleitet, der impliziert, dass unmanaged struct die anderen Einschränkungen
nicht sinnvoll sind.
Die unmanaged Einschränkung wird nicht von CLR erzwungen, sondern nur von der Sprache. Um die fehl
Verwendung durch andere Sprachen zu verhindern, werden Methoden mit dieser Einschränkung durch eine
mod-req geschützt. Dadurch wird verhindert, dass andere Sprachen Typargumente verwenden, bei denen es
sich nicht um verwaltete Typen handelt.
Das Token unmanaged in der Einschränkung ist weder ein Schlüsselwort noch ein Kontext Schlüsselwort.
Stattdessen ist es so var , als ob es an diesem Speicherort ausgewertet wird und eine der folgenden Aktionen
durchführt:
Bindung an benutzerdefinierten oder referenzierten Typ mit unmanaged dem Namen: Diese wird wie jede
andere benannte Typeinschränkung behandelt.
Binden an keinen Typ: Dies wird als unmanaged Einschränkung interpretiert.

Wenn ein Typ mit dem Namen vorhanden ist unmanaged und dieser ohne Qualifizierung im aktuellen Kontext
verfügbar ist, gibt es keine Möglichkeit, die Einschränkung zu verwenden unmanaged . Dies entspricht den
Regeln für das Feature var und benutzerdefinierte Typen desselben Namens.

Nachteile
Der Hauptnachteil dieses Features besteht darin, dass es eine kleine Anzahl von Entwicklern bietet: in der Regel
auf der Basis von Bibliotheks Autoren oder-Frameworks. Daher wird für eine kleine Anzahl von Entwicklern eine
kostbare sprach Zeit aufgewendet.
Diese Frameworks sind aber häufig die Grundlage für die Mehrzahl der .NET-Anwendungen. Daher kann die
Leistung/Richtigkeit auf dieser Ebene einen Ripple-Effekt auf das .NET-Ökosystem haben. Dadurch wird das
Feature auch mit eingeschränkter Zielgruppe in Erwägung gezogen.

Alternativen
Es gibt einige Alternativen zu berücksichtigende Aspekte:
Der Status quo: das Feature ist nicht für seine eigenen Vorzüge gerechtfertigt, und Entwickler verwenden
weiterhin das implizite Opt-in-Verhalten.

Fragen
Metadatendarstellung
Die F #-Sprache codiert die Einschränkung in der Signatur Datei, was bedeutet, dass c# Ihre Darstellung nicht
wieder verwenden kann. Für diese Einschränkung muss ein neues Attribut ausgewählt werden. Darüber hinaus
muss eine Methode, die diese Einschränkung hat, durch einen mod-req geschützt werden.
Blitfähige und nicht verwaltete
Die Sprache F # verfügt über ein sehr ähnliches Feature , das das Schlüsselwort nicht verwaltet verwendet. Der
blitfähige Name stammt aus der Verwendung in Midori. Sie sollten sich hier als Rangfolge ansehen und nicht
verwaltete verwenden.
Lösung Die Sprache entscheidet sich für die Verwendung nicht verwalteter
Verifier
Muss der Verifier/die Laufzeit aktualisiert werden, um die Verwendung von Zeigern auf generische
Typparameter zu verstehen? Oder kann es einfach unverändert funktionieren, ohne dass Änderungen
vorgenommen werden?
Lösung Es sind keine Änderungen erforderlich. Alle Zeiger Typen sind einfach nicht verifizierbar.

Treffen von Besprechungen


n/v
Indizierungs fixed Felder sollten nicht unabhängig
vom verschiebbaren/nicht verschiebbaren Kontext
fixiert werden.
04.11.2021 • 2 minutes to read

Die Änderung hat die Größe einer Fehlerbehebung. Sie kann sich in 7,3 befinden und steht nicht in Konflikt mit
der von uns weiterführenden Richtung. Diese Änderung besteht lediglich darin, dass das folgende Szenario
funktioniert, auch wenn es s möglich ist. Sie ist bereits gültig, wenn nicht verschoben werden s kann.
Hinweis: in beiden Fällen ist weiterhin ein unsafe Kontext erforderlich. Es ist möglich, nicht initialisierte Daten
zu lesen oder sogar außerhalb des gültigen Bereichs. Dies ändert sich nicht.

unsafe struct S
{
public fixed int myFixedField[10];
}

class Program
{
static S s;

unsafe static void Main()


{
int p = s.myFixedField[5]; // indexing fixed-size array fields would be ok
}
}

Die wichtigste "Herausforderung", die hier angezeigt wird, ist die Erläuterung der Entspannung in der
Spezifikation. Dies ist insbesondere erforderlich, da das anhepup weiterhin erforderlich ist. (da nicht s
verwendbar ist und das Feld explizit als Zeiger verwendet wird)

unsafe struct S
{
public fixed int myFixedField[10];
}

class Program
{
static S s;

unsafe static void Main()


{
int* ptr = s.myFixedField; // taking a pointer explicitly still requires pinning.
int p = ptr[5];
}
}

Ein Grund für das Anheften des Ziels, wenn es verschoben werden muss, ist das Artefakt unserer Code
Generierungs Strategie,-wir konvertieren stets in einen nicht verwalteten Zeiger und erzwingen somit, dass der
Benutzer die PIN via-Anweisung anheftet fixed . Die Konvertierung in nicht verwaltete ist jedoch bei der
Indizierung nicht erforderlich. Die gleiche unsichere Zeiger Mathematik ist gleichermaßen anwendbar, wenn der
Empfänger in Form eines verwalteten Zeigers vorhanden ist. Wenn dies der Fall ist, wird der zwischen Verweis
verwaltet (GC-nachverfolgt), und das anheunen ist nicht erforderlich.
Die Änderung https://github.com/dotnet/roslyn/pull/24966 ist ein Prototyp-PR, der diese Anforderung lockert.
Musterbasierte fixed -Anweisung
04.11.2021 • 5 minutes to read

Zusammenfassung
Stellen Sie ein Muster vor, das es Typen ermöglicht, an fixed Anweisungen teilzunehmen.

Motivation
Die Sprache bietet einen Mechanismus zum Fixieren von verwalteten Daten und zum Abrufen eines
systemeigenen Zeigers auf den zugrunde liegenden Puffer.

fixed(byte* ptr = byteArray)


{
// ptr is a native pointer to the first element of the array
// byteArray is protected from being moved/collected by the GC for the duration of this block
}

Der Satz von Typen, die an teilnehmen können, fixed ist hart codiert und auf Arrays und beschränkt
System.String . Das hart codieren von "speziellen" Typen wird nicht skaliert, wenn neue primitive, wie z
ImmutableArray<T> Span<T> . b.,, Utf8String eingeführt werden.

Darüber hinaus basiert die aktuelle Lösung für System.String auf einer Recht strengen API. Die Form der API
impliziert, dass System.String ein zusammenhängendes Objekt ist, das UTF16 codierte Daten an einem
fixierten Offset aus dem Objekt Header einbettet. Diese Vorgehensweise wurde in mehreren Vorschlägen als
problematisch eingestuft, die Änderungen am zugrunde liegenden Layout erfordern könnten. Es wäre
wünschenswert, zu einem flexibleren Element zu wechseln, das das System.String Objekt aus seiner internen
Darstellung für den Zweck der nicht verwalteten Interop entkoppelt.

Detaillierter Entwurf
Muster
Ein brauchbares, auf Mustern basierendes "Festes" muss Folgendes sein:
Stellen Sie die verwalteten Verweise bereit, um die Instanz anzuheften und den Zeiger zu initialisieren
(vorzugsweise ist dies derselbe Verweis).
Vermitteln Sie eindeutig den Typ des nicht verwalteten Elements (d. h. "char" für "String")
Weisen Sie das Verhalten im "leeren" Fall an, wenn keine Verweise vorhanden sind.
Sollten API-Autoren nicht in Bezug auf Entwurfsentscheidungen pushen, die die Verwendung des Typs
außerhalb von verletzen fixed .
Ich denke, das oben genannte Element könnte durch das Erkennen eines speziell benannten Ref-zurück
gebenden Members erfüllt werden: ref [readonly] T GetPinnableReference() .
fixedDie folgenden Bedingungen müssen erfüllt sein, damit Sie von der-Anweisung verwendet werden
können:
1. Es ist nur ein solcher Member für einen Typ angegeben.
2. Gibt den Wert von ref oder zurück ref readonly . ( readonly ist zulässig, damit Autoren von
unveränderlichen/schreibgeschützten Typen das Muster implementieren können, ohne eine beschreibbare
API hinzuzufügen, die in sicherem Code verwendet werden kann.)
3. T ist ein nicht verwalteter Typ. (da T* wird zum Zeigertyp. Die Einschränkung wird auf natürliche Weise
erweitert, wenn das Konzept "nicht verwaltet" erweitert wird.
4. Gibt Managed zurück, nullptr Wenn keine Daten zum Anheften vorhanden sind – wahrscheinlich die
günstigste Methode zum vermitteln von leergaben. (Beachten Sie, dass die Zeichenfolge "" einen Verweis auf
"\ 0" zurückgibt, da Zeichen folgen auf Null enden).
Alternativ dazu #3 können wir zulassen, dass das Ergebnis in leeren Fällen nicht definiert oder
Implementierungs spezifisch ist. Dies kann jedoch die API gefährlicher machen und anfällig für Missbrauch und
unbeabsichtigte Kompatibilitäts Belastungen werden.

Übersetzung
fixed(byte* ptr = thing)
{
// <BODY>
}

wird zum folgenden Pseudo Code (nicht alles ausdrucksfähig in c#)

byte* ptr;
// specially decorated "pinned" IL local slot, not visible to user code.
pinned ref byte _pinned;

try
{
// NOTE: null check is omitted for value types
// NOTE: `thing` is evaluated only once (temporary is introduced if necessary)
if (thing != null)
{
// obtain and "pin" the reference
_pinned = ref thing.GetPinnableReference();

// unsafe cast in IL
ptr = (byte*)_pinned;
}
else
{
ptr = default(byte*);
}

// <BODY>
}
finally // finally can be omitted when not observable
{
// "unpin" the object
_pinned = nullptr;
}

Nachteile
Getpinnablereferenzierungsweise ist nur in für die Verwendung in vorgesehen fixed , aber nichts hindert
die Verwendung in sicherem Code, daher muss dies von implemenor berücksichtigt werden.

Alternativen
Benutzer können getpinablereferenzierungsart oder ähnliches Mitglied einführen und als

fixed(byte* ptr = thing.GetPinnableReference())


{
// <BODY>
}

Es gibt keine Lösung für, System.String Wenn eine alternative Lösung gewünscht ist.

Nicht aufgelöste Fragen


[] Verhalten im "leeren" Zustand. - nullptr oder undefined ?
[] Sollten die Erweiterungs Methoden berücksichtigt werden?
[] Wenn ein Muster in erkannt wird System.String , sollte es gewinnen?

Treffen von Besprechungen


Noch keine.
Lokale Ref-Neuzuweisung
04.11.2021 • 2 minutes to read

In c# 7,3 wird Unterstützung für das erneute Binden des Verweises einer lokalen Ref-Variablen oder eines ref-
Parameters hinzugefügt.
Wir fügen dem Satz von die folgenden Informationen hinzu assignment_operator :

assignment_operator
: '=' 'ref'
;

Der =ref Operator wird als *ref-Zuweisungs Operator _ bezeichnet. Es handelt sich nicht um einen
_compound Zuweisungs Operator *. Der linke Operand muss ein Ausdruck sein, der an eine lokale Ref-Variable,
einen ref-Parameter (mit Ausnahme this von) oder einen out-Parameter gebunden ist. Der rechte Operand
muss ein Ausdruck sein, der einen lvalue ergibt, der einen Wert des gleichen Typs wie der linke Operand angibt.
Der rechte Operand muss definitiv am Punkt der Ref-Zuweisung zugewiesen werden.
Wenn der linke Operand an einen- out Parameter gebunden wird, ist dies ein Fehler, wenn dieser out
Parameter am Anfang des ref-Zuweisungs Operators nicht definitiv zugewiesen wurde.
Wenn der linke Operand ein Beschreib barer Verweis ist (d. h., er legt einen anderen als einen ref readonly
lokalen oder einen in Parameter fest), dann muss der rechte Operand ein Beschreib barer lvalue sein.
Der Ref-Zuweisungs Operator ergibt einen lvalue des zugewiesenen Typs. Es ist beschreibbar, wenn der linke
Operand beschreibbar ist (d. h. nicht ref readonly oder in ).
Die Sicherheitsregeln für diesen Operator lauten:
Bei einer ref-Neuzuweisung e1 = ref e2 muss das ref-Safe-to-Escape -Zeichen mindestens e2 so breit sein
wie das ref-Safe-to-Escape -Zeichen von e1 .
Where ref-Safe-to-Escape ist für Ref-like-Typen in Sicherheit definiert.
Stackalloc-Arrayinitialisierer
04.11.2021 • 2 minutes to read

Zusammenfassung
Ermöglicht die Verwendung von arrayinitialisierersyntax mit stackalloc .

Motivation
Bei normalen Arrays können ihre Elemente zum Zeitpunkt der Erstellung initialisiert werden. Es scheint sinnvoll,
dies zuzulassen stackalloc .
Die Frage, warum eine solche Syntax mit nicht zulässig ist, wird stackalloc Recht häufig angezeigt.
Siehe z. b. #1112

Detaillierter Entwurf
Normale Arrays können mithilfe der folgenden Syntax erstellt werden:

new int[3]
new int[3] { 1, 2, 3 }
new int[] { 1, 2, 3 }
new[] { 1, 2, 3 }

Die Erstellung von Stapel zugeordneten Arrays sollte durch folgende Aktionen zugelassen werden:

stackalloc int[3] // currently allowed


stackalloc int[3] { 1, 2, 3 }
stackalloc int[] { 1, 2, 3 }
stackalloc[] { 1, 2, 3 }

Die Semantik aller Fälle ist ungefähr mit Arrays identisch.


Beispiel: im letzten Fall wird der Elementtyp vom Initialisierer abgeleitet und muss ein nicht verwalteter Typ sein.
Hinweis: die Funktion ist nicht vom Ziel abhängig Span<T> . Dies gilt T* auch für den Fall, dass es für die Groß-
/Kleinschreibung nicht sinnvoll erscheint Span<T> .

Sprachübersetzung
Die naive Implementierung könnte das Array einfach direkt nach der Erstellung durch eine Reihe von Element
weisen Zuweisungen initialisieren.
Ähnlich wie bei Arrays kann es möglich und wünschenswert sein, Fälle zu erkennen, in denen alle oder die
meisten Elemente blitfähige Typen sind, und effizientere Techniken zu verwenden, indem Sie den vorab
erstellten Zustand aller Konstanten Elemente kopieren.

Nachteile
Alternativen
Dies ist ein praktisches Feature. Es ist möglich, nichts zu tun.
Nicht aufgelöste Fragen
Treffen von Besprechungen
Noch keine.
Automatisch implementierte Eigenschaften Field-
Targeted Attribute
04.11.2021 • 2 minutes to read

Zusammenfassung
Diese Funktion soll es Entwicklern ermöglichen, Attribute direkt auf die Unterstützungs Felder von automatisch
implementierten Eigenschaften anzuwenden.

Motivation
Derzeit ist es nicht möglich, Attribute auf die Unterstützungs Felder von automatisch implementierten
Eigenschaften anzuwenden. In Fällen, in denen der Entwickler ein Attribut für die Feld Ausrichtung verwenden
muss, wird er gezwungen, das Feld manuell zu deklarieren und die ausführlichere Eigenschaften Syntax zu
verwenden. Da c# immer feldorientierte Attribute für das generierte Unterstützungs Feld für Ereignisse
unterstützt, ist es sinnvoll, die gleiche Funktionalität auf die Eigenschaften-Kin auszuweiten.

Detaillierter Entwurf
Kurz gesagt, wäre Folgendes zulässig, und es wird keine Warnung ausgegeben:

[Serializable]
public class Foo
{
[field: NonSerialized]
public string MySecret { get; set; }
}

Dies würde dazu führen, dass die feldorientierten Attribute auf das vom Compiler generierte Unterstützungs
Feld angewendet werden:

[Serializable]
public class Foo
{
[NonSerialized]
private string _mySecretBackingField;

public string MySecret


{
get { return _mySecretBackingField; }
set { _mySecretBackingField = value; }
}
}

Wie bereits erwähnt, wird die Parität mit der Ereignis Syntax aus c# 1,0 bewirkt, da Folgendes bereits zulässig ist
und sich wie erwartet verhält:
[Serializable]
public class Foo
{
[field: NonSerialized]
public event EventHandler MyEvent;
}

Nachteile
Es gibt zwei mögliche Nachteile bei der Implementierung dieser Änderung:
1. Wenn Sie versuchen, ein Attribut auf das Feld einer automatisch implementierten Eigenschaft anzuwenden,
wird eine Compilerwarnung erzeugt, dass die Attribute in diesem Block ignoriert werden. Wenn der Compiler
dahingehend geändert wurde, dass diese Attribute unterstützt werden, werden Sie auf das dahinter liegende
Feld bei einer nachfolgenden Neukompilierung angewendet, wodurch das Verhalten des Programms zur
Laufzeit geändert werden kann.
2. Der Compiler überprüft derzeit nicht die AttributeUsage-Ziele der Attribute, wenn versucht wird, diese auf
das Feld der automatisch implementierten Eigenschaft anzuwenden. Wenn der Compiler geändert wurde, um
feldorientierte Attribute zu unterstützen, und das betreffende Attribut nicht auf ein Feld angewendet werden
kann, würde der Compiler einen Fehler ausgeben, anstatt eine Warnung zu erstellen, die den Build
unterbricht.

Alternativen
Nicht aufgelöste Fragen
Treffen von Besprechungen
Ausdrucksvariablen in Initialisierern
04.11.2021 • 2 minutes to read

Zusammenfassung
Wir erweitern die in c# 7 eingeführten Features, um Ausdrücke mit Ausdrucks Variablen (out-Variablen
Deklarationen und Deklarations Muster) in feldinitialisierern, eigenschafteninitialisierern, ctor-Initialisierern und
Abfrage Klauseln zuzulassen.

Motivation
Dadurch sind einige der groben Kanten in der c#-Sprache aufgrund fehlender Zeit abgeschlossen.

Detaillierter Entwurf
Wir entfernen die Einschränkung, die die Deklaration von Ausdrucks Variablen (out-Variablen Deklarationen und
Deklarations Muster) in einem ctor-Initialisierer verhindert. Eine solche deklarierte Variable befindet sich im
Bereich im Hauptteil des Konstruktors.
Wir entfernen die Einschränkung, die die Deklaration von Ausdrucks Variablen (out-Variablen Deklarationen und
Deklarations Muster) in einem Feld-oder Eigenschafteninitialisierer verhindert. Eine solche deklarierte Variable
befindet sich im Gültigkeitsbereich des Initialisierungs Ausdrucks.
Die Einschränkung, die die Deklaration von Ausdrucks Variablen (out-Variablen Deklarationen und Deklarations
Muster) verhindert, wird in einer Abfrage Ausdrucks Klausel entfernt, die in den Text eines Lambda-Ausdrucks
übersetzt wird. Eine solche deklarierte Variable befindet sich im Gültigkeitsbereich des gesamten Ausdrucks der
Abfrage Klausel.

Nachteile
Keine.

Alternativen
Der geeignete Bereich für in diesen Kontexten deklarierte Ausdrucks Variablen ist nicht offensichtlich und
verdient weitere LDM-Diskussionen.

Nicht aufgelöste Fragen


[] Was ist der geeignete Bereich für diese Variablen?

Treffen von Besprechungen


Keine.
Unterstützung für = = und! = für Tupeltypen
04.11.2021 • 9 minutes to read

Lässt Ausdrücke t1 == t2 zu t1 , bei denen und t2 Tupel-Tupeltypen derselben Kardinalität sind, und wertet
Sie ungefähr wie temp1.Item1 == temp2.Item1 && temp1.Item2 == temp2.Item2 (angenommen) aus
var temp1 = t1; var temp2 = t2; .

Umgekehrt würde Sie t1 != t2 es als zulassen und auswerten


temp1.Item1 != temp2.Item1 || temp1.Item2 != temp2.Item2 .

In Fällen, in denen Nullwerte zulässig sind, werden zusätzliche Überprüfungen für temp1.HasValue und
temp2.HasValue verwendet. nullableT1 == nullableT2 Wertet beispielsweise als aus
temp1.HasValue == temp2.HasValue ? (temp1.HasValue ? ... : true) : false .

Wenn ein Element weiser Vergleich ein nicht-boolescher Ergebnis zurückgibt (z. b. Wenn ein nicht boolescher
benutzerdefinierter operator == oder operator != in einem dynamischen Vergleich verwendet wird), wird
dieses Ergebnis entweder in konvertiert bool oder durchlaufen, operator true operator false um eine zu
erhalten bool . Beim tupelvergleich wird immer ein zurückgegeben bool .
Ab c# 7,2 erzeugt dieser Code einen Fehler (
error CS0019: Operator '==' cannot be applied to operands of type '(...)' and '(...)' ), es sei denn, es ist ein
benutzerdefiniertes vorhanden operator== .

Details
Beim Binden des == Operators (oder != ) sind die vorhandenen Regeln: (1) dynamischer Fall, (2) Überladungs
Auflösung und (3) fehlgeschlagen. Dieses Angebot fügt einen tupelfall zwischen (1) und (2) hinzu: Wenn beide
Operanden eines Vergleichs Operators Tupel sind (über Tupeltypen verfügen oder tupelliterale sind) und eine
passende Kardinalität aufweisen, wird der Vergleich Element Weise ausgeführt. Diese tupelgleichheit wird auch
auf auf NULL festleg Bare Tupel angehoben.
Beide Operanden (und im Fall von tupelliteralen) werden in der Reihenfolge von links nach rechts ausgewertet.
Jedes Element Paar wird dann als Operanden verwendet, um den Operator == (oder != ) rekursiv zu binden.
Alle Elemente mit dem Kompilier Zeittyp dynamic verursachen einen Fehler. Die Ergebnisse dieser Element
weisen Vergleiche werden als Operanden in einer Kette von bedingten Operatoren und-Operatoren (oder oder)
verwendet.
Beispielsweise würde im Kontext von (int, (int, int)) t1, t2; t1 == (1, (2, 3)) als ausgewertet werden
temp1.Item1 == temp2.Item1 && temp1.Item2.Item1 == temp2.Item2.Item1 && temp1.Item2.Item2 ==
temp2.Item2.Item2
.
Wenn ein tupelliteral als Operand (auf beiden Seiten) verwendet wird, empfängt es einen konvertierten
tupeltyp, der durch die Element bezogenen Konvertierungen gebildet wird, die beim Binden des Operators ==
(oder != ) Element Weise eingeführt werden.
Beispielsweise ist in (1L, 2, "hello") == (1, 2L, null) der konvertierte Typ für beide tupelliterale,
(long, long, string) und das zweite Literale hat keinen natürlichen Typ.
Debug und Konvertierungen in Tupel
In (a, b) == x spielt die Tatsache, dass die x Funktion in zwei Elemente decoeinigen kann, keine Rolle. Dies
könnte in einem zukünftigen Vorschlag der Fall sein, obwohl Sie Fragen zu haben würde x == y (ist dies ein
einfacher Vergleich oder ein Element weiser Vergleich, und wenn dies der Fall ist, welche Kardinalität verwendet
wird). Ebenso spielen Konvertierungen in Tupel keine Rolle.
Tupelelementnamen
Beim Umrechnen eines tupelliterals warnen wir, wenn ein expliziter tupelelementname im Literalformat
angegeben wurde, aber nicht mit dem Ziel-tupelelementnamen identisch ist. Wir verwenden die gleiche Regel
beim Tupel-Vergleich, damit wir davon ausgehen, dass (int a, int b) t in in eine Warnung ausgegeben wird
d t == (c, d: 0) .

Nicht boolesche Vergleichsergebnisse (Element Weise )


Wenn ein Element weiser Vergleich in einer tupelgleichheit dynamisch ist, wird ein dynamischer Aufruf des-
Operators verwendet false und negieren diese, um eine zu erhalten bool und mit weiteren Element
bezogenen vergleichen fortzufahren.
Wenn ein Element weiser Vergleich einen anderen nicht booleschen Typ in einer tupelgleichheit zurückgibt, gibt
es zwei Fälle:
Wenn der Typ, der kein boolescher Typ bool ist, in konvertiert, wird diese Konvertierung angewendet.
Wenn keine solche Konvertierung vorhanden ist, der Typ jedoch einen Operator aufweist false , verwenden
wir diesen und negieren das Ergebnis.
Bei einer tupelungleichheit gelten dieselben Regeln, mit der Ausnahme, dass der Operator true (ohne
Negation) anstelle des Operators verwendet wird false .
Diese Regeln ähneln den Regeln für die Verwendung eines nicht-booleschen Typs in einer if -Anweisung und
in anderen vorhandenen Kontexten.

Auswertungs Reihenfolge und Sonderfälle


Der Wert auf der linken Seite wird zuerst ausgewertet, dann der Wert auf der rechten Seite, dann die Element
weisen Vergleiche von links nach rechts (einschließlich Konvertierungen und mit Early Exit basierend auf
vorhandenen Regeln für bedingte and/or-Operatoren).
Wenn beispielsweise eine Konvertierung von Typ A in Typ B und eine Methode erfolgt (A, A) GetTuple() ,
bedeutet das Auswerten Folgendes (new A(1), (new B(2), new B(3))) == (new B(4), GetTuple()) :
new A(1)
new B(2)
new B(3)
new B(4)
GetTuple()
Anschließend werden die Element weisen Konvertierungen und Vergleiche und die bedingte Logik
ausgewertet ( new A(1) in den Typ konvertiert B , dann mit verglichen new B(4) usw.).
Vergleich null mit null

Dies ist ein Sonderfall von regulären vergleichen, der auf tupelvergleiche überträgt. Der null == null Vergleich
ist zulässig, und die null Literale erhalten keinen Typ. Bei tupelgleichheit bedeutet das, dass
(0, null) == (0, null) ebenfalls zulässig ist und die null -und tupelliterale keinen Typ erhalten.

Vergleichen einer Struktur, die NULL -Werte zulässt, mit null ohne operator==

Dies ist ein weiterer Sonderfall von regulären vergleichen, der auf tupelvergleiche überträgt. Wenn Sie über ein
struct S ohne verfügen operator== , (S?)x == null ist der Vergleich zulässig und wird als interpretiert
((S?).x).HasValue . Bei der tupelgleichheit wird dieselbe Regel angewendet, daher (0, (S?)x) == (0, null) ist
zulässig.
Kompatibilität
Wenn jemand seine eigenen ValueTuple Typen mit einer Implementierung des Vergleichs Operators verfasst
hat, wäre er zuvor durch Überladungs Auflösung übernommen worden. Da jedoch der neue tupelfall vor der
Überladungs Auflösung steht, wird dieser Fall mit tupelvergleichen behandelt, anstatt auf den
benutzerdefinierten Vergleich zu vertrauen.

Bezieht sich auf relationale und Typtest Operatoren in Beziehung zu #190


Verbesserte Überladungskandidaten
04.11.2021 • 2 minutes to read

Zusammenfassung
Die Regeln für die Überladungs Auflösung wurden in nahezu jedem c#-sprach Update aktualisiert, um die
Möglichkeiten für Programmierer zu verbessern, indem mehrdeutige Aufrufe ausgewählt werden. Dies muss
sorgfältig durchgeführt werden, um die Abwärtskompatibilität zu gewährleisten. da wir jedoch in der Regel die
Fehlerfälle auflösen, funktionieren diese Verbesserungen in der Regel gut.
1. Wenn eine Methoden Gruppe sowohl Instanzmember als auch statische Member enthält, werden die
Instanzmember verworfen, wenn Sie ohne instanzempfänger oder-Kontext aufgerufen werden, und die
statischen Member verwerfen, wenn Sie mit einem instanzempfänger aufgerufen werden. Wenn kein
Empfänger vorhanden ist, schließen wir nur statische Member in einen statischen Kontext ein, andernfalls
sowohl statische als auch Instanzmember. Wenn der Empfänger aufgrund einer Farb farbsituation
mehrdeutig eine Instanz oder einen Typ ist, enthalten wir beides. Ein statischer Kontext, in dem ein impliziter
instanzempfänger nicht verwendet werden kann, enthält den Hauptteil der Member, in denen keine definiert
ist, wie z. b. statische Member, und Orte, an denen dies nicht verwendet werden kann, wie z. b.
Feldinitialisierer und Konstruktorinitialisierer.
2. Wenn eine Methodengruppe einige generische Methoden enthält, deren Typargumente ihre
Einschränkungen nicht erfüllen, werden diese Member aus dem Satz von Kandidaten entfernt.
3. Für eine Methodengruppenkonvertierung werden Kandidatenmethoden, deren Rückgabetyp nicht mit dem
des Delegaten übereinstimmt, aus dem Satz entfernt.
Verweis Typen, die NULL-Werte zulassen in C #
04.11.2021 • 13 minutes to read

Ziel dieses Features ist Folgendes:


Ermöglicht es Entwicklern, auszudrücken, ob eine Variable, ein Parameter oder ein Ergebnis eines Verweis
Typs NULL sein soll oder nicht.
Geben Sie Warnungen an, wenn derartige Variablen, Parameter und Ergebnisse nicht gemäß dieser Absicht
verwendet werden.

Ausdruck der Absicht


Die Sprache enthält bereits die T? Syntax für Werttypen. Es ist einfach, diese Syntax auf Verweis Typen
auszuweiten.
Es wird davon ausgegangen, dass es sich bei der Absicht eines unzierten Verweis Typs um einen T nicht-NULL-
Wert handelt.

Überprüfen von verweisen, die NULL zulassen


Eine Fluss Analyse verfolgt Verweis Variablen, die NULL-Werte zulassen. Wenn die Analyse nicht NULL ist (z. b.
nach einer Überprüfung oder einer Zuweisung), wird Ihr Wert als nicht-NULL-Verweis betrachtet.
Ein Verweis, der NULL-Werte zulässt, kann auch explizit mit dem postfix- x! Operator (dem "Damnit"-
Operator) als ungleich NULL behandelt werden, wenn die Fluss Analyse keine nicht-NULL-Situation festlegen
kann, die der Entwickler kennt.
Andernfalls wird eine Warnung ausgegeben, wenn ein Verweis, der NULL-Werte zulässt, dereferenziert wird
oder in einen nicht-NULL-Typ konvertiert wird.
Beim Umstellen von S[] in T?[] und von in wird eine Warnung S?[] ausgegeben T[] .
Beim Umrechnen von in wird eine Warnung ausgegeben C<S> C<T?> , es sei denn, der Typparameter ist
kovariant ( out ), und bei der Umstellung von C<S?> auf ist C<T> der Typparameter kontra Variant ( in ).
C<T?> Wenn der Typparameter Einschränkungen ungleich NULL aufweist, wird eine Warnung ausgegeben.

Überprüfen von nicht-NULL-verweisen


Eine Warnung wird ausgegeben, wenn ein NULL-wahrsten einer Variablen ungleich NULL zugewiesen oder als
nicht-NULL-Parameter übergeben wird.
Eine Warnung wird auch ausgegeben, wenn ein Konstruktor nicht NULL-Verweis Felder explizit initialisiert.
Wir können nicht ausreichend nachverfolgen, ob alle Elemente eines Arrays von verweisen, die nicht NULL sind,
initialisiert werden. Wir könnten jedoch eine Warnung ausgeben, wenn kein Element eines neu erstellten Arrays
zugewiesen wird, bevor das Array gelesen oder weitergereicht wird. Dies kann den häufigen Fall verarbeiten,
ohne zu stark zu sein.
Wir müssen entscheiden, ob default(T) eine Warnung generiert oder einfach als Typ behandelt wird T? .

Metadatendarstellung
Zusatzelemente der NULL-Zulässigkeit sollten in Metadaten als Attribute dargestellt werden. Dies bedeutet, dass
downlevelcompiler diese ignorieren.
Wir müssen entscheiden, ob nur NULL-Werte zulässig sind, oder es gibt Aufschluss darüber, ob ein nicht-NULL-
Element in der Assembly "on" war.

Generics
Wenn ein Typparameter T Einschränkungen aufweist, die keine NULL-Werte zulassen, wird er innerhalb seines
Gültigkeits Bereichs als nicht auf NULL festlegbar behandelt.
Wenn ein Typparameter nicht eingeschränkt ist oder nur NULL-Werte zulässt, ist die Situation etwas komplexer:
Dies bedeutet, dass das entsprechende Typargument entweder NULL-Werte zulässt oder keine NULL-Werte
zulässt. Die in dieser Situation sichere Aufgabe besteht darin, den Typparameter sowohl als Werte zulässt als
auch als nicht-NULL-Werte zu behandeln, und es werden Warnungen ausgegeben, wenn eine Verletzung
auftritt.
Es lohnt sich zu erwägen, ob explizite Verweis Einschränkungen auf NULL-Werte zulässig sind. Beachten Sie
jedoch, dass es nicht möglich ist, Verweis Typen, die NULL-Werte zulassen, implizit in bestimmten Fällen
eingeschränkt zu werden (geerbte Einschränkungen).
Die class Einschränkung ist nicht NULL. Wir können berücksichtigen, ob class? eine gültige Einschränkung,
die NULL-Werte zulässt, eine gültige Einschränkung ist, die NULL-Werte zulässt.

Typrückschluss
Wenn beim Typrückschluss ein Mitwirkender Typ ein Verweistyp ist, der NULL-Werte zulässt, sollte der
resultierende Typ NULL-Werte zulassen. Das heißt, dass NULL-Werte weitergegeben werden.
Wir sollten berücksichtigen, ob das null Literale als teilnehmende Ausdruck NULL sein sollte. Dies ist heute
nicht der Fall: für Werttypen führt dies zu einem Fehler, während für Verweis Typen der NULL-Wert erfolgreich
in den Plain-Typ konvertiert wird.

string? n = "world";
var x = b ? "Hello" : n; // string?
var y = b ? "Hello" : null; // string? or error
var z = b ? 7 : null; // Error today, could be int?

Leitfaden zu NULL-Schutz
Als Funktion können die Verweis Typen, die NULL-Werte zulassen, ihre Absicht ausdrücken und Warnungen
über die Fluss Analyse bereitstellen, wenn diese Absicht widerspricht. Es gibt eine häufige Frage, ob NULL-
Wächter erforderlich sind oder nicht.
Beispiel für NULL Guard

public void DoWork(Worker worker)


{
// Guard against worker being null
if (worker is null)
{
throw new ArgumentNullException(nameof(worker));
}

// Otherwise use worker argument


}
Im vorherigen Beispiel DoWork akzeptiert die Funktion eine Worker und schützt Sie, die möglicherweise ist
null . Wenn das- worker Argument ist null , DoWork wird die Funktion verwendet throw . Bei Verweis
Typen, die NULL-Werte zulassen, hat der Code im vorherigen Beispiel die Absicht, dass der- Worker Parameter
nicht ist null . Wenn es sich bei der DoWork Funktion um eine öffentliche API handelt, wie z. b. ein nuget-Paket
oder eine freigegebene Bibliothek, sollten Sie als Leitfaden NULL-Wächter vorhanden lassen. Als öffentliche API
ist die einzige Garantie, dass ein Aufrufer nicht weitergereicht, null darauf zu schützen.
Express Intent
Eine überzeugendere Verwendung des vorherigen Beispiels besteht darin, auszudrücken, dass der Worker
Parameter möglicherweise ist null , sodass der NULL-Schutz besser geeignet ist. Wenn Sie den nullguard im
folgenden Beispiel entfernen, warnt der Compiler, dass Sie möglicherweise null dereferenzieren. Beide NULL-
Wächter sind immer noch gültig.

public void DoWork(Worker? worker)


{
// Guard against worker being null
if (worker is null)
{
throw new ArgumentNullException(nameof(worker));
}

// Otherwise use worker argument


}

Bei nicht öffentlichen APIs, wie z. b. Quellcode, der von einem Entwickler-oder Entwicklerteam vollständig
gesteuert wird, können die Verweis Typen, die NULL-Werte zulassen, das sichere Entfernen von NULL-
Schutzkräften ermöglichen, wenn die Entwickler garantieren können, dass Sie nicht erforderlich sind Die-
Funktion kann bei Warnungen helfen, aber es kann nicht garantiert werden, dass die Ausführung eines Lauf Zeit
Codes zu einer führen könnte NullReferenceException .

Aktuelle Änderungen
Warnungen ungleich NULL sind eine offensichtliche Breaking Change für vorhandenen Code und sollten mit
einem Opt-in-Mechanismus einhergehen.
Weniger offensichtlich sind Warnungen von Typen, die NULL-Werte zulassen (wie oben beschrieben), eine
Breaking Change auf vorhandenem Code in bestimmten Szenarien, in denen die NULL-Zulässigkeit implizit ist:
Nicht eingeschränkte Typparameter werden als implizit NULL-Werte behandelt, sodass die Zuweisung
object oder der Zugriff auf z. b. ToString Warnungen liefert.
Wenn Typrückschluss NULL-Werte von null Ausdrücken ableitet, gibt vorhandener Code in manchen Fällen
NULL-Werte als nicht auf NULL festleg Bare Typen zurück, was zu neuen Warnungen führen kann.
Daher müssen auf NULL festleg Bare Warnungen ebenfalls optional sein.
Zum Schluss ist das Hinzufügen von Anmerkungen zu einer vorhandenen API eine Breaking Change für
Benutzer, die sich für Warnungen entschieden haben, wenn Sie die Bibliothek aktualisieren. Dies hat auch die
Möglichkeit, sich zu entscheiden. "Ich möchte die Fehlerbehebungen, aber ich bin nicht bereit, mit ihren neuen
Anmerkungen umzugehen"
Zusammenfassend müssen Sie in der Lage sein, Folgendes zu abonnieren:
NULL-Werte zulassen
Nicht-NULL-Warnungen
Warnungen von Anmerkungen in anderen Dateien
Die Granularität der Zustimmung deutet auf ein Analyse ähnliches Modell hin, bei dem sich Teil Striche mit
Pragmas anmelden können, und Schweregrade können vom Benutzer ausgewählt werden. Außerdem kann es
sein, dass pro Bibliothek-Optionen ("die Anmerkungen von JSON.net ignorieren, bis ich bereit für den Umgang
mit dem Fallout sind") möglicherweise im Code als Attribute ausgedrückt werden kann.
Der Entwurf der Opt-in-/Übergangs--Funktion ist entscheidend für den Erfolg und die Nützlichkeit dieses
Features. Wir müssen Folgendes sicherstellen:
Benutzer können die Überprüfung der NULL-Zulässigkeit nach und nach nach Bedarf übernehmen
Bibliotheks Autoren können NULL-Zulässigkeit-Anmerkungen hinzufügen
Obwohl dies nicht der Fall ist, ist "Konfigurations Alptraum" nicht sinnvoll.

Anpassungen
Wir könnten die ? Anmerkungen nicht für lokale Variablen verwenden, sondern nur beobachten, ob Sie in
Übereinstimmung mit den Ihnen zugewiesenen Elementen verwendet werden. Ich bevorzuge dies nicht. Ich
denke, wir sollten den Benutzern die Absicht überlassen, ihre Absicht auszudrücken.
Wir könnten einen Kurzwert T! x für Parameter in Erwägung gezogen, der automatisch eine NULL-Lauf Zeit
Überprüfung generiert.
Bestimmte Muster bei generischen Typen, wie z. FirstOrDefault b. oder TryGet , weisen ein etwas seltsames
Verhalten mit nicht auf NULL festleg baren Typargumenten auf, da Sie in bestimmten Situationen explizit
Standardwerte ergeben. Wir könnten versuchen, das Typsystem zu unterstützen, um diese bessere Anpassung
zu ermöglichen. Beispielsweise können wir ? mit nicht eingeschränkten Typparametern zulassen, obwohl das
Typargument bereits NULL-Werte zulassen kann. Ich bin mir sicher, dass es Wert ist, und es führt zu einer
Bedeutung, die sich auf die Interaktion mit auf NULL festleg baren Werttypen bezieht.

Auf NULL festlegbare Werttypen


Wir könnten auch einige der obigen Semantik für Werttypen, die NULL-Werte zulassen, in Erwägung gezogen.
Wir haben bereits den Typrückschluss erwähnt, bei dem wir aus ableiten konnten int? (7, null) , anstatt nur
einen Fehler zu geben.
Eine weitere Möglichkeit besteht darin, die Fluss Analyse auf Werte zulässt-Werttypen anzuwenden. Wenn Sie
als ungleich Null angesehen werden, können wir die Verwendung von als nicht auf NULL festleg baren Typ auf
bestimmte Weise zulassen (z. b. Element Zugriff). Wir müssen nur darauf achten, dass die Dinge, die Sie bereits
für einen Werte zulässt-Werttyp ausführen können, für die Back-Kompatibilitäts-Gründe bevorzugt werden.
Rekursiver Musterabgleich
04.11.2021 • 22 minutes to read

Zusammenfassung
Muster Vergleichs Erweiterungen für c# ermöglichen viele der Vorteile von algebraischen Datentypen und
Musterabgleich von funktionalen Sprachen, aber auf eine Weise, die nahtlos in das Verhalten der zugrunde
liegenden Sprache integriert ist. Elemente dieses Ansatzes sind von verwandten Features in den
Programmiersprachen F # und Scalainspiriert.

Detaillierter Entwurf
Is-Ausdruck
Der- is Operator wird erweitert, um einen Ausdruck mit einem Muster zu testen.

relational_expression
: is_pattern_expression
;
is_pattern_expression
: relational_expression 'is' pattern
;

Diese Form von relational_expression ist zusätzlich zu den vorhandenen Formularen in der c#-Spezifikation. Es
handelt sich um einen Kompilierzeitfehler, wenn der relational_expression Links vom is Token keinen Wert
oder keinen Typ hat.
Jeder Bezeichner des Musters führt eine neue lokale Variable ein, die definitiv zugewiesen wird, nachdem der
is Operator ist true (d.h. definitiv zugewiesen, wenn true).

Hinweis: Es gibt technisch gesehen eine Mehrdeutigkeit zwischen dem Typ in einer is-expression -und-
constant_pattern, von denen jede eine gültige Analyse eines qualifizierten Bezeichners sein könnte. Wir
versuchen, Sie als Typ für die Kompatibilität mit früheren Versionen der Sprache zu binden. nur wenn dies
nicht möglich ist, lösen wir es wie einen Ausdruck in anderen Kontexten auf, um den ersten gefundenen
Vorgang (bei dem es sich um eine Konstante oder einen Typ handeln muss) durchzuführen. Diese
Mehrdeutigkeit ist nur auf der rechten Seite eines is Ausdrucks vorhanden.

Muster
Muster werden im is_pattern Operator, in einem switch_statement und in einem switch_expression verwendet,
um die Form der Daten auszudrücken, mit denen eingehende Daten (die als Eingabe Wert bezeichnet werden)
verglichen werden sollen. Muster können rekursiv sein, damit Teile der Daten mit unter Mustern verglichen
werden können.
pattern
: declaration_pattern
| constant_pattern
| var_pattern
| positional_pattern
| property_pattern
| discard_pattern
;
declaration_pattern
: type simple_designation
;
constant_pattern
: constant_expression
;
var_pattern
: 'var' designation
;
positional_pattern
: type? '(' subpatterns? ')' property_subpattern? simple_designation?
;
subpatterns
: subpattern
| subpattern ',' subpatterns
;
subpattern
: pattern
| identifier ':' pattern
;
property_subpattern
: '{' '}'
| '{' subpatterns ','? '}'
;
property_pattern
: type? property_subpattern simple_designation?
;
simple_designation
: single_variable_designation
| discard_designation
;
discard_pattern
: '_'
;

Deklarations Muster

declaration_pattern
: type simple_designation
;

Der declaration_pattern testet, ob ein Ausdruck einen bestimmten Typ hat, und wandelt ihn in diesen Typ um,
wenn der Test erfolgreich ist. Dadurch kann eine lokale Variable des angegebenen Typs, benannt durch den
angegebenen Bezeichner, eingeführt werden, wenn die Bezeichnung eine single_variable_designation ist. Diese
lokale Variable ist definitiv zugewiesen , wenn das Ergebnis der Muster Vergleichsoperation ist true .
Die Lauf Zeit Semantik dieses Ausdrucks besteht darin, dass er den Lauf Zeittyp des linken relational_expression
Operanden mit dem Typ im Muster testet. Wenn es sich um einen Lauf Zeittyp (oder einen Untertyp) handelt und
nicht null , ist das Ergebnis von is operator true .
Bestimmte Kombinationen von statischem Typ der linken Seite und des angegebenen Typs werden als nicht
kompatibel betrachtet und führen zu einem Kompilierzeitfehler. Ein Wert vom Typ static E ist Muster
kompatibel mit einem Typ, T Wenn eine Identitäts Konvertierung, eine implizite Verweis Konvertierung, eine
Boxing-Konvertierung, eine explizite Verweis Konvertierung oder eine Unboxing-Konvertierung von in
vorhanden ist, E T oder wenn einer dieser Typen ein offener Typ ist. Es handelt sich um einen
Kompilierzeitfehler, wenn eine Eingabe vom Typ E in einem Typmuster, mit dem Sie übereinstimmt, nicht mit
dem Typ übereinstimmt, mit dem der Typ übereinstimmt.
Das Typmuster ist nützlich zum Ausführen von Lauf Zeittyp Tests von Verweis Typen und ersetzt die
Ausdrucksweise.

var v = expr as Type;


if (v != null) { // code using v

Mit etwas präziseren

if (expr is Type v) { // code using v

Es ist ein Fehler, wenn der Typ ein Werte zulässt-Werttyp ist.
Das Typmuster kann verwendet werden, um Werte von Typen zu testen, die NULL-Werte zulassen: ein Wert
vom Typ Nullable<T> (oder ein schachtelt T ) entspricht einem Typmuster T2 id , wenn der Wert nicht NULL
ist und der Typ von T2 oder ein T Basistyp oder eine Schnittstelle von ist T . Beispielsweise im Code
Fragment

int? x = 3;
if (x is int v) { // code using v

Die Bedingung der if -Anweisung ist true zur Laufzeit, und die-Variable v enthält den Wert 3 des Typs
int innerhalb des-Blocks. Nach dem Block befindet sich die Variable im Gültigkeits v Bereich, ist aber nicht
definitiv zugewiesen.
Konstantes Muster

constant_pattern
: constant_expression
;

Ein konstantes Muster testet den Wert eines Ausdrucks mit einem konstanten Wert. Die Konstante kann ein
beliebiger konstanter Ausdruck sein, z. b. ein Literalwert, der Name einer deklarierten const Variable oder eine
Enumerationskonstante. Wenn der Eingabe Wert kein offener Typ ist, wird der Konstante Ausdruck implizit in
den Typ des übereinstimmenden Ausdrucks konvertiert. Wenn der Typ des Eingabe Werts nicht mit dem Typ
des konstanten Ausdrucks Muster kompatibel ist, ist der Muster Vergleichs Vorgang ein Fehler.
Das Muster c wird als Übereinstimmung mit dem konvertierten Eingabe Wert e betrachtet, wenn
object.Equals(c, e) zurückgibt true .

Wir gehen davon e is null aus null , dass Sie in neu geschriebenem Code auf die gängigste Weise testen, da
Sie keine benutzerdefinierten aufrufen kann operator== .
Var-Muster
var_pattern
: 'var' designation
;
designation
: simple_designation
| tuple_designation
;
simple_designation
: single_variable_designation
| discard_designation
;
single_variable_designation
: identifier
;
discard_designation
: _
;
tuple_designation
: '(' designations? ')'
;
designations
: designation
| designations ',' designation
;

Wenn die Bezeichnung eine simple_designation ist, entspricht der Ausdruck e dem Muster. Anders ausgedrückt:
eine Entsprechung zu einem var-Muster ist immer mit einem simple_designation erfolgreich. Wenn die
simple_designation eine single_variable_designation ist, liegt der Wert von e an einer neu eingeführten lokalen
Variablen. Der Typ der lokalen Variablen ist der statische Typ von e.
Wenn die Bezeichnung eine tuple_designation ist, entspricht das Muster einem positional_pattern der Formular
(var Bezeichnung..., ) wobei die Bezeichnung s innerhalb des tuple_designation gefunden werden.
Beispielsweise entspricht das Muster var (x, (y, z)) (var x, (var y, var z)) .
Wenn der Name var an einen Typ gebunden ist, ist ein Fehler aufgetreten.
Muster verwerfen

discard_pattern
: '_'
;

Ein Ausdruck e stimmt immer mit dem Muster überein _ . Dies bedeutet, dass jeder Ausdruck mit dem
Verwerfungs Muster übereinstimmt.
Ein Verwerfungs Muster kann nicht als Muster eines is_pattern_expression verwendet werden.
Positions Muster
Ein Positions Muster überprüft, ob der Eingabe Wert nicht ist null , ruft eine entsprechende Deconstruct
Methode auf und führt einen weiteren Musterabgleich für die resultierenden Werte aus. Sie unterstützt auch
eine tupelähnliche Muster Syntax (ohne den Typ), wenn der Typ des Eingabe Werts mit dem Typ übereinstimmt,
der enthält Deconstruct , oder wenn der Typ des Eingabe Werts ein tupeltyp ist, oder wenn der Typ des Eingabe
Werts object oder ist ITuple und der Lauf Zeittyp des Ausdrucks implementiert ITuple .
positional_pattern
: type? '(' subpatterns? ')' property_subpattern? simple_designation?
;
subpatterns
: subpattern
| subpattern ',' subpatterns
;
subpattern
: pattern
| identifier ':' pattern
;

Wenn der Typ weggelassen wird, wird er als statischer Typ des Eingabe Werts übernommen.
Wenn eine Übereinstimmung eines Eingabe Werts mit dem Mustertyp ( subpattern_list vorliegt ) , wird eine
Methode ausgewählt, indem der Typ nach zugänglichen Deklarationen von gesucht Deconstruct und eine
Untergruppe mit denselben Regeln wie für die Dekonstruktions Deklaration ausgewählt wird.
Es ist ein Fehler, wenn ein positional_pattern den Typ auslässt, über ein einzelnes unter Muster ohne einen
Bezeichner verfügt, keine property_subpattern hat und keine simple_designation hat. Dies unterscheidet sich
zwischen einer constant_pattern , die in Klammern gesetzt ist, und einem positional_pattern.
Um die Werte zu extrahieren, die mit den Mustern in der Liste abgeglichen werden sollen,
Wenn Type ausgelassen wurde und der Typ des Eingabe Werts ein tupeltyp ist, muss die Anzahl der Teil
Muster der Kardinalität des Tupels entsprechen. Jedes Tupelelement wird mit dem entsprechenden Teil
Muster verglichen, und die Übereinstimmung ist erfolgreich, wenn alle diese erfolgreich sind. Wenn ein Teil
Muster über einen Bezeichner verfügt, muss dieser ein Tupelelement an der entsprechenden Position im
tupeltyp benennen.
Wenn eine geeignete Deconstruct als Member des Typs vorhanden ist, handelt es sich um einen
Kompilierzeitfehler, wenn der Typ des Eingabe Werts nicht mit dem Typ" Muster kompatibel " ist. Zur Laufzeit
wird der Eingabe Wert anhand des Typs getestet. Wenn dies fehlschlägt, schlägt die Zuordnung des Positions
Musters fehl. Wenn dies erfolgreich ist, wird der Eingabe Wert in diesen Typ konvertiert und Deconstruct
mit den vom Compiler generierten Variablen aufgerufen, um die out Parameter zu empfangen. Jeder
empfangene Wert wird mit dem entsprechenden Teil Muster verglichen, und die Übereinstimmung ist
erfolgreich, wenn alle diese erfolgreich sind. Wenn ein Teil Muster über einen Bezeichner verfügt, muss dieser
einen Parameter an der entsprechenden Position von benennen Deconstruct .
Andernfalls, wenn der Typ ausgelassen wurde und der Eingabe Wert vom Typ object oder ITuple oder
einem Typ ist, der ITuple durch eine implizite Verweis Konvertierung in konvertiert werden kann, und in
den Teil Mustern kein Bezeichner angezeigt wird, stimmen wir mithilfe von ab ITuple .
Andernfalls ist das Muster ein Kompilierzeitfehler.
Die Reihenfolge, in der Teil Muster zur Laufzeit abgeglichen werden, ist nicht angegeben, und eine
fehlgeschlagene Übereinstimmung versucht möglicherweise nicht, alle Teil Muster abzugleichen.
B e i sp i e l

In diesem Beispiel werden viele Funktionen verwendet, die in dieser Spezifikation beschrieben werden.

var newState = (GetState(), action, hasKey) switch {


(DoorState.Closed, Action.Open, _) => DoorState.Opened,
(DoorState.Opened, Action.Close, _) => DoorState.Closed,
(DoorState.Closed, Action.Lock, true) => DoorState.Locked,
(DoorState.Locked, Action.Unlock, true) => DoorState.Closed,
(var state, _, _) => state };

Eigenschafts Muster
Ein Eigenschafts Muster prüft, ob der Eingabe Wert nicht null und rekursiv mit Werten übereinstimmt, die
durch die Verwendung von zugänglichen Eigenschaften oder Feldern extrahiert werden.

property_pattern
: type? property_subpattern simple_designation?
;
property_subpattern
: '{' '}'
| '{' subpatterns ','? '}'
;

Es tritt ein Fehler auf, wenn ein Teil Muster einer property_pattern keinen Bezeichner enthält (es muss sich um
das zweite Formular handeln, das einen Bezeichner aufweist). Ein nach gestelltes Komma nach dem letzten Teil
Muster ist optional.
Beachten Sie, dass ein Muster mit NULL-Überprüfung von einem trivialen Eigenschafts Muster abfällt. Wenn Sie
überprüfen möchten, ob die Zeichenfolge ungleich s NULL ist, können Sie jedes der folgenden Formulare
schreiben.

if (s is object o) ... // o is of type object


if (s is string x) ... // x is of type string
if (s is {} x) ... // x is of type string
if (s is {}) ...

Wenn eine Entsprechung eines Ausdrucks e mit dem Mustertyp { property_pattern_list wird } , ist dies ein
Kompilierzeitfehler, wenn der Ausdruck e nicht mit dem Typ T kompatibel ist, der vom Typ festgelegt wurde.
Wenn der Typ nicht vorhanden ist, wird er als statischer Typ von e übernommen. Wenn der Bezeichner
vorhanden ist, wird eine Muster Variable vom Typ Type deklariert. Jeder Bezeichner, der auf der linken Seite des
property_pattern_list angezeigt wird, muss eine barrierefreie lesbare Eigenschaft oder ein Feld von T angeben.
Wenn die simple_designation des property_pattern vorhanden ist, wird eine Muster Variable vom Typ T
definiert.
Zur Laufzeit wird der Ausdruck für T getestet. Wenn dies fehlschlägt, schlägt das Eigenschafts Muster
Übereinstimmung fehl, und das Ergebnis ist false . Wenn der Vorgang erfolgreich ist, werden die einzelnen
property_subpattern Felder oder-Eigenschaften gelesen und deren Wert mit dem entsprechenden Muster
übereinstimmen. Das Ergebnis der gesamten Entsprechung ist false nur, wenn das Ergebnis eines dieser
Ergebnisse ist false . Die Reihenfolge, in der Teil Muster abgeglichen werden, ist nicht angegeben, und eine
fehlgeschlagene Übereinstimmung entspricht möglicherweise nicht allen Teil Mustern zur Laufzeit. Wenn die
Übereinstimmung erfolgreich ist und die simple_designation der property_pattern ein
single_variable_designation ist, wird eine Variable vom Typ T definiert, der der übereinstimmende Wert
zugewiesen wird.

Hinweis: das Eigenschafts Muster kann verwendet werden, um eine Muster Übereinstimmung mit
anonymen Typen zu erhalten.

B e i sp i e l

if (o is string { Length: 5 } s)

Switch-Ausdruck
Ein- switch_expression wird zur Unterstützung switch ähnlicher Semantik für einen Ausdrucks Kontext
hinzugefügt.
Die Syntax der c#-Sprache wird durch die folgenden syntaktischen Produktionen erweitert:
multiplicative_expression
: switch_expression
| multiplicative_expression '*' switch_expression
| multiplicative_expression '/' switch_expression
| multiplicative_expression '%' switch_expression
;
switch_expression
: range_expression 'switch' '{' '}'
| range_expression 'switch' '{' switch_expression_arms ','? '}'
;
switch_expression_arms
: switch_expression_arm
| switch_expression_arms ',' switch_expression_arm
;
switch_expression_arm
: pattern case_guard? '=>' expression
;
case_guard
: 'when' null_coalescing_expression
;

Der switch_expression ist als expression_statement nicht zulässig.

Wir möchten dies in einer zukünftigen Revision lockern.

Der Typ des switch_expression ist der am häufigsten vorkommende Typ der Ausdrücke, die auf der rechten Seite
der => Token des switch_expression_arm s angezeigt werden, wenn ein solcher Typ vorhanden ist und der
Ausdruck in jedem Arm des switch-Ausdrucks implizit in diesen Typ konvertiert werden kann. Außerdem fügen
wir eine neue switchausdrucks Konvertierung hinzu, bei der es sich um eine vordefinierte implizite
Konvertierung von einem Switch-Ausdruck in jeden Typ handelt, T für den eine implizite Konvertierung von
jedem Arm-Ausdruck in vorhanden ist T .
Wenn sich das Muster einiger switch_expression_arm nicht auf das Ergebnis auswirken kann, tritt ein Fehler auf,
da einige vorherige Muster und Wächter immer eine Entsprechung haben.
Ein Switch-Ausdruck wird als voll ständig bezeichnet, wenn ein Arm des switch-Ausdrucks jeden Wert seiner
Eingabe behandelt. Der Compiler erzeugt eine Warnung, wenn ein Switch-Ausdruck nicht voll ständig ist.
Zur Laufzeit ist das Ergebnis des switch_expression der Wert des Ausdrucks des ersten switch_expression_arm ,
für den der Ausdruck auf der linken Seite des switch_expression dem Muster der switch_expression_arm
entspricht, und für den die case_guard des switch_expression_arm, falls vorhanden, zu ausgewertet wird true .
Wenn keine solche switch_expression_arm vorhanden ist, löst die switch_expression eine Instanz der Ausnahme
aus System.Runtime.CompilerServices.SwitchExpressionException .
Optionale Unterstriche beim Umschalten eines tupelliterals.
Um mit dem switch_statement ein tupelliteralelement zu wechseln, müssen Sie die scheinbar redundante
Parameter schreiben.

switch ((a, b))


{

Zum zulassen

switch (a, b)
{

die Klammern der Switch-Anweisung sind optional, wenn der Ausdruck, für den gewechselt wird, ein tupelliteral
ist.
Reihenfolge der Auswertung in Pattern-Matching
Die Flexibilität des Compilers beim Neuordnen der während des Musterabgleich ausgeführten Vorgänge kann
die Flexibilität ermöglichen, die zur Verbesserung der Effizienz der Muster Übereinstimmung verwendet werden
kann. Die (nicht erzwungene) Anforderung wäre, dass Eigenschaften, auf die in einem Muster zugegriffen wird,
und die dekonstruktionsmethoden "Pure" (Nebeneffekt frei, idempotent usw.) sein müssen. Dies bedeutet nicht,
dass wir die Reinheit als sprach Konzept hinzufügen würden, nur dass wir die compilerflexibilität beim
Neuanordnen von Vorgängen ermöglichen würden.
Lösung 2018-04-04 LDM : bestätigt: der Compiler ist berechtigt, Aufrufe an Deconstruct , Eigenschaften
Zugriffe und Aufrufe von Methoden in neu anzuordnen, ITuple und kann davon ausgehen, dass die
zurückgegebenen Werte bei mehreren aufrufen identisch sind. Der Compiler sollte keine Funktionen aufrufen,
die das Ergebnis nicht beeinflussen können, und wir werden sehr vorsichtig sein, bevor wir Änderungen an der
vom Compiler generierten Reihenfolge der Auswertung in Zukunft vornehmen.
Einige mögliche Optimierungen
Die Kompilierung des Musterabgleich kann allgemeine Teile von Mustern nutzen. Wenn z. b. der Typtest der
obersten Ebene von zwei aufeinander folgenden Mustern in einer switch_statement denselben Typ hat, kann der
generierte Code den Typtest für das zweite Muster überspringen.
Wenn einige der Muster ganze Zahlen oder Zeichen folgen sind, kann der Compiler dieselbe Art von Code
generieren, der für eine Switch-Anweisung in früheren Versionen der Sprache generiert wurde.
Weitere Informationen zu diesen Optimierungs Arten finden Sie unter [Scott und Ramsey (2000)].
Standardschnittstellen Methoden
04.11.2021 • 49 minutes to read

[x] vorgeschlagen
[] Prototyp: in Bearbeitung
[] Implementierung: keine
[] Spezifikation: in Bearbeitung

Zusammenfassung
Hinzufügen von Unterstützung für virtuelle Erweiterungs Methoden -Methoden in Schnittstellen mit konkreten
Implementierungen. Eine Klasse oder Struktur, die eine solche Schnittstelle implementiert, muss über eine
einzige spezifischere Implementierung für die Schnittstellen Methode verfügen, die entweder von der Klasse
oder Struktur implementiert wird oder von ihren Basisklassen oder Schnittstellen geerbt wird. Mithilfe von
virtuellen Erweiterungs Methoden kann ein API-Autor Methoden zu einer Schnittstelle in zukünftigen Versionen
hinzufügen, ohne dass die Quell-oder Binärkompatibilität mit vorhandenen Implementierungen dieser
Schnittstelle unterbrochen wird.
Diese ähneln den "Standardmethoden"von Java.
(Basierend auf dem wahrscheinlichen Implementierungs Verfahren) erfordert dieses Feature entsprechende
Unterstützung in der CLI/CLR. Programme, die dieses Feature nutzen, können in früheren Versionen der
Plattform nicht ausgeführt werden.

Motivation
Die Hauptgründe für dieses Feature sind
Mithilfe von Standardschnittstellen Methoden kann ein API-Autor in zukünftigen Versionen Methoden zu
einer Schnittstelle hinzufügen, ohne dass die Quell-oder Binärkompatibilität mit vorhandenen
Implementierungen dieser Schnittstelle unterbrochen wird.
Mit der-Funktion kann c# mit APIs für Android (Java) und IOS (SWIFT)interagieren, die ähnliche Funktionen
unterstützen.
Wie sich herausstellt, stellt das Hinzufügen von Standardschnittstellen Implementierungen die Elemente der
Sprachfunktion "Merkmale" ( https://en.wikipedia.org/wiki/Trait_(computer_programming) ) bereit.
Merkmale haben sich als leistungsstarke Programmiertechnik bewährt (
http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf ).

Detaillierter Entwurf
Die Syntax für eine Schnittstelle wird auf zulassen erweitert.
Member-Deklarationen, die Konstanten, Operatoren, statische Konstruktoren und die Typen von Typen
deklarieren.
ein Text für eine Methode oder einen Indexer, eine Eigenschaft oder einen Ereignis Accessor (d. h. eine
"Default"-Implementierung);
Member-Deklarationen, die statische Felder, Methoden, Eigenschaften, Indexer und Ereignisse deklarieren.
Member-Deklarationen mit der expliziten Schnittstellen Implementierungs Syntax; immer
Explizite Zugriffsmodifizierer (der Standardzugriff ist public )
Member mit Text ermöglichen der-Schnittstelle, eine "Default"-Implementierung für die Methode in Klassen und
Strukturen bereitzustellen, die keine über schreibende Implementierung bereitstellen.
Schnittstellen dürfen keinen Instanzstatus enthalten. Obwohl statische Felder jetzt zulässig sind, sind
Instanzfelder in Schnittstellen nicht zulässig. Automatische Eigenschaften von Instanzen werden in Schnittstellen
nicht unterstützt, da sie implizit ein ausgeblendetes Feld deklarieren würden.
Statische und private Methoden ermöglichen ein nützliches Refactoring und eine Organisation des Codes, der
zum Implementieren der öffentlichen API der Schnittstelle verwendet wird.
Eine Methoden Überschreibung in einer Schnittstelle muss die explizite Schnittstellen Implementierungs Syntax
verwenden.
Es ist ein Fehler, einen Klassentyp, Strukturtyp oder Enumerationstyp innerhalb des Gültigkeits Bereichs eines
Typparameters zu deklarieren, der mit einem variance_annotation deklariert wurde. Beispielsweise ist die
Deklaration von C unten ein Fehler.

interface IOuter<out T>


{
class C { } // error: class declaration within the scope of variant type parameter 'T'
}

Konkrete Methoden in Schnittstellen


Die einfachste Form dieses Features ist die Möglichkeit, eine konkrete Methode in einer Schnittstelle zu
deklarieren, bei der es sich um eine Methode mit einem Text handelt.

interface IA
{
void M() { WriteLine("IA.M"); }
}

Eine Klasse, die diese Schnittstelle implementiert, muss Ihre konkrete Methode nicht implementieren.

class C : IA { } // OK

IA i = new C();
i.M(); // prints "IA.M"

Die abschließende Überschreibung für IA.M in C der-Klasse ist die konkrete Methode, die M in deklariert wird
IA . Beachten Sie, dass eine Klasse keine Member von ihren Schnittstellen erbt. Dies wird von dieser Funktion
nicht geändert:

new C().M(); // error: class 'C' does not contain a member 'M'

In einem Instanzmember einer Schnittstelle this verfügt über den Typ der einschließenden Schnittstelle.
Modifiziererer in Schnittstellen
Die Syntax für eine Schnittstelle wird gelockert, um modifiziererer auf ihren Membern zuzulassen. Folgendes ist
zulässig: private , protected , internal , public , virtual , abstract , sealed , static , extern und
partial .

TODO : Überprüfen Sie, welche anderen modifiziererer vorhanden sind.

Ein Schnittstellenmember, dessen Deklaration einen Text enthält, ist ein virtual Member, es sei denn, der-
sealed oder- private Modifizierer Der- virtual Modifizierer kann auf einem Funktionsmember verwendet
werden, der andernfalls implizit wäre virtual . Auch wenn abstract die Standardeinstellung für
Schnittstellenmember ohne Text ist, kann dieser Modifizierer explizit angegeben werden. Ein nicht virtueller
Member kann mit dem- sealed Schlüsselwort deklariert werden.
Es ist ein Fehler, wenn ein- private oder- sealed Funktionsmember einer Schnittstelle keinen Text hat. Ein
private Funktionsmember darf nicht über den-Modifizierer verfügen sealed .

Zugriffsmodifizierer können für Schnittstellenmember aller zulässigen Member verwendet werden. Die
Zugriffsebene public ist die Standardeinstellung, Sie kann jedoch explizit angegeben werden.

Problem öffnen: Wir müssen die genaue Bedeutung der Zugriffsmodifizierer, wie z. b. protected und
internal , angeben und angeben, welche Deklarationen Sie (in einer abgeleiteten Schnittstelle)
überschreiben oder implementieren (in einer Klasse, die die-Schnittstelle implementiert).

Schnittstellen können Member deklarieren static , einschließlich der Typen, Methoden, Indexer, Eigenschaften,
Ereignisse und statischen Konstruktoren. Die Standard Zugriffsebene für alle Schnittstellenmember ist public .
Schnittstellen können keine Instanzkonstruktoren, Dekonstruktoren oder Felder deklarieren.

*Geschlossenes Problem: _ sollten Operator Deklarationen in einer Schnittstelle zulässig sein?


Wahrscheinlich keine Konvertierungs Operatoren, aber was ist mit anderen? Entscheidung: Operatoren sind
für Konvertierungs-, Gleichheits-und Ungleichheits Operatoren except * zulässig.

*Geschlossenes Problem: _ sollte new für Schnittstellenmember-Deklarationen zulässig sein, die


Member von Basis Schnittstellen ausblenden? _ Entscheidung *: Ja.

*Geschlossenes Problem:"" ist derzeit nicht partial für eine Schnittstelle oder deren Member zulässig.
Dies erfordert einen separaten Vorschlag. Entscheidung *: Ja.
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-
in-interface

Über Schreibungen in Schnittstellen


Überschreibungs Deklarationen (d. h. solche, die den override Modifizierer enthalten) ermöglichen es dem
Programmierer, eine spezifischere Implementierung eines virtuellen Members in einer Schnittstelle
bereitzustellen, auf der der Compiler oder die Laufzeit andernfalls keinen finden würde. Außerdem ermöglicht
es die Umwandlung eines abstrakten Members aus einer Super Schnittstelle in ein Standardelement in einer
abgeleiteten Schnittstelle. Eine Überschreibungs Deklaration ist berechtigt, eine bestimmte Basis Schnittstellen
Methode explizit zu überschreiben, indem Sie die Deklaration mit dem Schnittstellennamen qualifiziert (in
diesem Fall ist kein Zugriffsmodifizierer zulässig). Implizite über schreibungen sind nicht zulässig.

interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
override void IA.M() { WriteLine("IB.M"); } // explicitly named
}
interface IC : IA
{
override void M() { WriteLine("IC.M"); } // implicitly named
}
Überschreibungs Deklarationen in Schnittstellen können nicht deklariert werden sealed .
Öffentliche virtual Funktionsmember in einer Schnittstelle können in einer abgeleiteten Schnittstelle explizit
überschrieben werden (indem Sie den Namen in der Überschreibungs Deklaration mit dem Schnittstellentyp
qualifizieren, der die Methode ursprünglich deklariert hat, und einen Zugriffsmodifizierer weggelassen).
virtual Funktionsmember in einer Schnittstelle können nur explizit (nicht implizit) in abgeleiteten
Schnittstellen überschrieben werden, und Member, die nicht public explizit in einer Klasse oder Struktur
implementiert werden können (nicht implizit). In beiden Fällen muss auf den überschriebenen oder
implementierten Member zugegriffen werden können , wo er überschrieben wurde.
Neuabstraktion
Eine in einer Schnittstelle deklarierte virtuelle (konkrete) Methode kann in einer abgeleiteten Schnittstelle als
abstrakt überschrieben werden.

interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
abstract void IA.M();
}
class C : IB { } // error: class 'C' does not implement 'IA.M'.

Der- abstract Modifizierer ist in der Deklaration von nicht erforderlich IB.M (Dies ist die Standardeinstellung
in-Schnittstellen), aber es ist wahrscheinlich empfehlenswert, explizit in einer Überschreibungs Deklaration zu
sein.
Dies ist in abgeleiteten Schnittstellen nützlich, bei denen die Standard Implementierung einer Methode
ungeeignet ist und eine geeignetere Implementierung durch Implementieren von Klassen bereitgestellt werden
sollte.

Problem öffnen: Sollte eine erneute Abstraktion zulässig sein?

Die spezifischsten Überschreibungs Regel


Wir verlangen, dass jede Schnittstelle und Klasse über eine spezifischere außer Kraft setzung für jedes virtuelle
Element unter den außer Kraft setzungen verfügen, die im Typ oder seinen direkten und indirekten Schnittstellen
angezeigt werden. Die spezifischere außer Kraft Setzung ist eine eindeutige außer Kraft setzung, die spezifischer
ist als jede andere außer Kraft Setzung. Wenn keine außer Kraft Setzung vorhanden ist, wird der Member selbst
als die spezifischsten außer Kraft Setzung betrachtet.
Eine außer Kraft Setzung M1 gilt als spezifischere als eine andere außer Kraft Setzung M2 M1 , wenn für den
Typ deklariert wird T1 , M2 für den Typ deklariert wird T2 , und
1. T1 enthält T2 unter den direkten oder indirekten Schnittstellen oder
2. T2 ist ein Schnittstellentyp, T1 ist jedoch kein Schnittstellentyp.
Beispiel:
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
void IA.M() { WriteLine("IC.M"); }
}
interface ID : IB, IC { } // error: no most specific override for 'IA.M'
abstract class C : IB, IC { } // error: no most specific override for 'IA.M'
abstract class D : IA, IB, IC // ok
{
public abstract void M();
}

Die spezifischere Überschreibungs Regel stellt sicher, dass ein Konflikt (d. h. eine Mehrdeutigkeit durch die
rautenvererbung) explizit vom Programmierer an dem Punkt aufgelöst wird, an dem der Konflikt auftritt.
Da wir explizite abstrakte über Schreibungen in Schnittstellen unterstützen, können wir dies auch in Klassen tun.

abstract class E : IA, IB, IC // ok


{
abstract void IA.M();
}

Open Issue : sollten explizite Schnittstellen abstrakte über Schreibungen in Klassen unterstützt werden?

Außerdem ist es ein Fehler, wenn in einer Klassen Deklaration die spezifischere außer Kraft Setzung einer
Schnittstellen Methode eine abstrakte außer Kraft Setzung ist, die in einer Schnittstelle deklariert wurde. Dies ist
eine vorhandene Regel, die mithilfe der neuen Terminologie neu angegeben wurde.

interface IF
{
void M();
}
abstract class F : IF { } // error: 'F' does not implement 'IF.M'

Eine in einer Schnittstelle deklarierte virtuelle Eigenschaft kann eine spezifischere außer Kraft setzung für den
get Accessor in einer Schnittstelle und eine spezifischere außer Kraft setzung für den set Accessor in einer
anderen Schnittstelle aufweisen. Dies wird als Verstoß gegen die spezifischsten Überschreibungs Regel
angesehen.
Die Methoden static und private

Da Schnittstellen nun ausführbaren Code enthalten können, ist es hilfreich, allgemeinen Code in private und
statische Methoden zu abstrahieren. Diese werden jetzt in Schnittstellen zugelassen.

*Geschlossenes Problem : sollen private Methoden unterstützt werden? Sollten statische Methoden
unterstützt werden? Entscheidung: Ja*

Open Issue : sollen Schnittstellen Methoden protected oder andere Zugriffe zugelassen werden internal
? Wenn dies der Fall ist, wie lautet die Semantik? Sind Sie virtual standardmäßig? Wenn dies der Fall ist,
gibt es eine Möglichkeit, Sie als nicht virtuell zu gestalten?

Open Issue : Wenn statische Methoden unterstützt werden, sollten (statische) Operatoren unterstützt
werden?

Basis Schnittstellen Aufrufe


Code in einem Typ, der von einer Schnittstelle mit einer Standardmethode abgeleitet ist, kann die "Base"-
Implementierung dieser Schnittstelle explizit aufrufen.

interface I0
{
void M() { Console.WriteLine("I0"); }
}
interface I1 : I0
{
override void M() { Console.WriteLine("I1"); }
}
interface I2 : I0
{
override void M() { Console.WriteLine("I2"); }
}
interface I3 : I1, I2
{
// an explicit override that invoke's a base interface's default method
void I0.M() { I2.base.M(); }
}

Eine Instanzmethode (nicht statisch) kann die Implementierung einer zugänglichen Instanzmethode in einer
direkten Basisschnittstelle nicht virtuell aufrufen, indem Sie Sie mit der-Syntax benennt base(Type).M . Dies ist
hilfreich, wenn eine außer Kraft setzung, die aufgrund der rautenvererbung bereitgestellt werden muss, durch
Delegieren an eine bestimmte Basis Implementierung aufgelöst wird.

interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
override void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
override void IA.M() { WriteLine("IC.M"); }
}

class D : IA, IB, IC


{
void IA.M() { base(IB).M(); }
}

Wenn auf ein-Element oder ein-Element virtual abstract mithilfe der-Syntax zugegriffen wird base(Type).M ,
ist es erforderlich, das Type eine eindeutige außer Kraft setzung für enthält M .
Bindungs Basis Klauseln
Schnittstellen enthalten nun Typen. Diese Typen können in der Base-Klausel als Basis Schnittstellen verwendet
werden. Beim Binden einer Basis Klausel müssen wir möglicherweise den Satz von Basis Schnittstellen kennen,
um diese Typen zu binden (z. b. um Nachrichten zu suchen und geschützten Zugriff aufzulösen). Die Bedeutung
der Basis Klausel einer Schnittstelle wird daher zirkulär definiert. Um den Kreis zu unterbrechen, fügen wir eine
neue Sprachregel hinzu, die einer ähnlichen Regel entspricht, die für Klassen bereits vorhanden ist.
Beim Ermitteln der Bedeutung des interface_base einer Schnittstelle wird davon ausgegangen, dass die Basis
Schnittstellen temporär leer sind. Intuitiv wird dadurch sichergestellt, dass die Bedeutung einer Basis Klausel
nicht rekursiv von sich selbst abhängig ist.
Wir haben die folgenden Regeln ver wendet:
"Wenn eine Klasse B von einer Klasse a abgeleitet ist, ist dies ein Kompilierzeitfehler für eine, die von B abhängig
ist. Eine Klasse hängt direkt von ihrer direkten Basisklasse (sofern vorhanden) ab und hängt direkt von der
Klasse ab, in der Sie sofort geschachtelt ist (sofern vorhanden). Bei dieser Definition ist der gesamte Satz von
Klassen , von dem eine Klasse abhängt, die reflexive und transitiv Schließung der direkt von der Beziehung
abhängig. "
Es ist ein Kompilierzeitfehler für eine Schnittstelle, die direkt oder indirekt von sich selbst erbt. Die Basis
Schnittstellen einer Schnittstelle sind die expliziten Basis Schnittstellen und deren Basis Schnittstellen. Mit
anderen Worten: der Satz von Basis Schnittstellen ist die komplette transitiv Schließung der expliziten Basis
Schnittstellen, ihrer expliziten Basis Schnittstellen usw.
Wir passen Sie wie folgt an:
Wenn eine Klasse B von einer Klasse a abgeleitet ist, ist dies ein Kompilierzeitfehler für eine, die von B abhängig
ist. Eine Klasse hängt direkt von ihrer direkten Basisklasse (sofern vorhanden) ab und hängt direkt von dem
Typ ab, in dem Sie sofort geschachtelt ist (sofern vorhanden).
Wenn ein Interface-IB eine Interface IA erweitert, handelt es sich um einen Kompilierzeitfehler, damit IA von IB
abhängig ist. Eine Schnittstelle hängt direkt von ihren direkten Basis Schnittstellen (sofern vorhanden) ab und
hängt direkt von dem Typ ab, in dem Sie sofort geschachtelt ist (sofern vorhanden).
Bei diesen Definitionen ist der gesamte Satz von Typen , von dem ein Typ abhängt, der reflexive und transitiv
Abschluss der direkt von Beziehung abhängig.
Auswirkung auf vorhandene Programme
Die hier vorgestellten Regeln sollen keine Auswirkung auf die Bedeutung vorhandener Programme haben.
Beispiel 1:

interface IA
{
void M();
}
class C: IA // Error: IA.M has no concrete most specific override in C
{
public static void M() { } // method unrelated to 'IA.M' because static
}

Beispiel 2:
interface IA
{
void M();
}
class Base: IA
{
void IA.M() { }
}
class Derived: Base, IA // OK, all interface members have a concrete most specific override
{
private void M() { } // method unrelated to 'IA.M' because private
}

Die gleichen Regeln führen zu ähnlichen Ergebnissen wie bei den Standardschnittstellen Methoden:

interface IA
{
void M() { }
}
class Derived: IA // OK, all interface members have a concrete most specific override
{
private void M() { } // method unrelated to 'IA.M' because private
}

*Closed Issue : Vergewissern Sie sich, dass dies eine beabsichtigte Folge der Spezifikation ist.
Entscheidung: Ja*

Auflösung der Lauf Zeit Methode


Geschlossene Probleme: Die Spezifikation sollte den Algorithmus zur Auflösung der Lauf Zeit Methode
im Gesicht der Standardmethoden der Schnittstelle beschreiben. Wir müssen sicherstellen, dass die
Semantik mit der sprach Semantik konsistent ist, z. b. welche deklarierten Methoden dies tun und keine
Methode überschreiben oder implementieren internal .

CLR -Support-API
Damit Compiler erkennen können, dass Sie für eine Laufzeitumgebung kompiliert werden, die dieses Feature
unterstützt, werden Bibliotheken für diese Laufzeiten so geändert, dass diese Fakten über die API angekündigt
werden, die in erläutert wird https://github.com/dotnet/corefx/issues/17116 . Wir fügen

namespace System.Runtime.CompilerServices
{
public static class RuntimeFeature
{
// Presence of the field indicates runtime support
public const string DefaultInterfaceImplementation = nameof(DefaultInterfaceImplementation);
}
}

*Open Issue _: ist der beste Name für das _CLR *-Feature? Die CLR-Funktion ist viel mehr als nur das (z. b.
lockern von Schutz Einschränkungen, unterstützt über Schreibungen in Schnittstellen usw.). Vielleicht sollte
Sie z. b. "konkrete Methoden in Schnittstellen" oder "Merkmale" genannt werden?

Weitere Bereiche, die angegeben werden sollen


[] Es ist hilfreich, die Arten von Quell-und Binär Kompatibilitäts Effekten zu katalogisieren, die durch das
Hinzufügen von Standardschnittstellen Methoden und über schreibungen zu vorhandenen Schnittstellen
verursacht werden.

Nachteile
Dieser Vorschlag erfordert ein koordiniertes Update der CLR-Spezifikation (zur Unterstützung konkreter
Methoden in Schnittstellen und Methoden Auflösung). Es ist daher ziemlich "teuer", und es kann sinnvoll sein, in
Kombination mit anderen Features, die wir erwarten, CLR-Änderungen zu erfordern.

Alternativen
Keine.

Nicht aufgelöste Fragen


Offene Fragen werden im obigen Vorschlag genannt.
https://github.com/dotnet/csharplang/issues/406Eine Liste der offenen Fragen finden Sie unter.
Die ausführliche Spezifikation muss den Auflösungs Mechanismus beschreiben, der zur Laufzeit verwendet
wird, um die genaue Methode auszuwählen, die aufgerufen werden soll.
Die Interaktion von Metadaten, die von neuen Compilern erzeugt und von älteren Compilern genutzt werden,
muss ausführlich verarbeitet werden. Beispielsweise müssen wir sicherstellen, dass die von uns verwendete
Metadatendarstellung nicht bewirkt, dass eine Standard Implementierung in einer Schnittstelle hinzugefügt
wird, um eine vorhandene Klasse zu unterbrechen, die diese Schnittstelle implementiert, wenn Sie von einem
älteren Compiler kompiliert wird. Dies kann sich auf die Metadatendarstellung auswirken, die wir verwenden
können.
Der Entwurf muss die Interoperation mit anderen Sprachen und vorhandenen Compilern für andere
Sprachen in Erwägung gezogen werden.

Aufgelöste Fragen
Abstrakte außer Kraft Setzung
Die vorherige Entwurfs Spezifikation enthielt die Möglichkeit, eine geerbte Methode "reabstract" zu erstellen:

interface IA
{
void M();
}
interface IB : IA
{
override void M() { }
}
interface IC : IB
{
override void M(); // make it abstract again
}

Meine Notizen für 2017-03-20 haben ergeben, dass wir dies nicht zulassen. Hierfür gibt es jedoch mindestens
zwei Anwendungsfälle:
1. Die Java-APIs, mit denen einige Benutzer dieses Features zusammenarbeiten, sind von dieser Funktion
abhängig.
2. Die Programmierung mit Merkmalen profitiert davon. Reabstraktion ist eines der Elemente der
Sprachfunktion "Merkmale" () https://en.wikipedia.org/wiki/Trait_(computer_programming)) . Die folgenden
Klassen sind zulässig:
public abstract class Base
{
public abstract void M();
}
public abstract class A : Base
{
public override void M() { }
}
public abstract class B : A
{
public override abstract void M(); // reabstract Base.M
}

Leider kann dieser Code nicht als Satz von Schnittstellen (Merkmalen) umgestaltet werden, es sei denn, dies ist
zulässig. Das Jared-Prinzip der Gier sollte zugelassen werden.

Geschlossene Probleme: Sollte eine erneute Abstraktion zulässig sein? Zwar Meine Notizen waren falsch.
Die LDM bemerkt , dass eine erneute Abstraktion in einer Schnittstelle zulässig ist. Nicht in einer Klasse.

Virtueller Modifizierer vs sealed-Modifizierer


Aus Aleksey tsingauz:

Es wurde beschlossen, Modifizierer explizit für Schnittstellenmember anzugeben, es sei denn, es gibt einen
Grund dafür, einige davon zu unterbinden. Dies bringt eine interessante Frage in Bezug auf den virtuellen
Modifizierer. Sollte es für Member mit Standard Implementierung erforderlich sein?
Wir könnten Folgendes sagen:
Wenn keine Implementierung vorhanden ist und weder Virtual noch Sealed angegeben sind, wird davon
ausgegangen, dass der Member abstrakt ist.
Wenn eine Implementierung vorhanden ist und weder abstract noch Sealed angegeben sind, wird davon
ausgegangen, dass der Member virtuell ist.
der sealed-Modifizierer ist erforderlich, um eine Methode weder virtuell noch abstrakt zu machen.
Wir könnten auch sagen, dass der virtuelle Modifizierer für ein virtuelles Element erforderlich ist. Wenn also
ein Member vorhanden ist, dessen Implementierung nicht explizit mit dem virtuellen Modifizierer markiert
ist, ist es weder virtuell noch abstrakt. Diese Vorgehensweise bietet möglicherweise eine bessere Leistung,
wenn eine Methode von einer Klasse zu einer Schnittstelle verschoben wird:
eine abstrakte Methode bleibt abstrakt.
eine virtuelle Methode bleibt virtuell.
eine Methode ohne einen Modifizierer bleibt weder virtuell noch abstrakt.
der sealed-Modifizierer kann nicht auf eine Methode angewendet werden, die keine außer Kraft Setzung
ist.
Was halten Sie davon?

Geschlossene Probleme: Sollte eine konkrete Methode (mit Implementierung) implizit sein virtual ?
Zwar

Entscheidungen: Erstellt in LDM 2017-04-05:


1. nicht virtuell sollte explizit durch oder ausgedrückt werden sealed private .
2. sealed ist das Schlüsselwort, um schnittstelleninstanzmember mit nicht virtuellen Textkörper zu erstellen
3. Wir möchten alle modifiziererer in Schnittstellen zulassen.
4. Der Standard Zugriff für Schnittstellenmember ist öffentlich, einschließlich der Untertypen
5. Private Funktionsmember in Schnittstellen werden implizit versiegelt und sind sealed für Sie nicht zulässig.
6. Private Klassen (in Schnittstellen) sind zulässig und können versiegelt werden, und das bedeutet versiegelt in
der Klassen Sense Sealed.
7. Wenn kein gutes Angebot vorhanden ist, ist partiell bei Schnittstellen oder ihren Membern weiterhin nicht
zulässig.
Binäre Kompatibilität 1
Wenn eine Bibliothek eine Standard Implementierung bereitstellt

interface I1
{
void M() { Impl1 }
}
interface I2 : I1
{
}
class C : I2
{
}

Wir wissen, dass die Implementierung von I1.M in C ist I1.M . Was geschieht, wenn die Assembly, die enthält,
I2 wie folgt geändert und neu kompiliert wird.

interface I2 : I1
{
override void M() { Impl2 }
}

C wird jedoch nicht neu kompiliert. Was geschieht, wenn das Programm ausgeführt wird? Ein Aufruf von
(C as I1).M()

1. Parallel I1.M
2. Parallel I2.M
3. Löst einen Laufzeitfehler aus.
Entscheidung: 2017-04-11: wird ausgeführt I2.M . Dies ist die eindeutig spezifischere außer Kraft Setzung zur
Laufzeit.
Ereignisaccessoren (geschlossen)
Geschlossene Probleme: Kann ein Ereignis "schrittweise" überschrieben werden?

Beachten Sie diesen Fall:


public interface I1
{
event T e1;
}
public interface I2 : I1
{
override event T
{
add { }
// error: "remove" accessor missing
}
}

Diese "partielle" Implementierung des Ereignisses ist unzulässig, da die Syntax für eine Ereignis Deklaration, wie
in einer Klasse, nicht nur einen Accessor zulässt. Beide (oder keine) müssen angegeben werden. Sie können das
gleiche erreichen, indem Sie zulassen, dass der abstrakte remove-Accessor in der Syntax implizit abstrakt ist,
wenn kein Text vorhanden ist:

public interface I1
{
event T e1;
}
public interface I2 : I1
{
override event T
{
add { }
remove; // implicitly abstract
}
}

Beachten Sie, dass es sich hierbei um eine neue (vorgeschlagene) Syntax handelt. In der aktuellen Grammatik
haben Ereignisaccessoren einen obligatorischen Text.

Geschlossene Probleme: Kann ein Ereignis Accessor (implizit) durch das Weglassen eines Texts
abstrahiert werden, ähnlich der Art, wie Methoden in Schnittstellen und Eigenschaftenaccessoren (implizit)
durch das Weglassen eines Texts abstrakt sind?

Entscheidung: (2017-04-18) Nein, Ereignis Deklarationen erfordern sowohl konkrete Accessoren (oder keines
von beiden).
Neuabstraktion in einer Klasse (geschlossen)
Geschlossene Probleme: Wir sollten bestätigen, dass dies zulässig ist (Andernfalls wäre das Hinzufügen einer
Standard Implementierung eine Breaking Change):

interface I1
{
void M() { }
}
abstract class C : I1
{
public abstract void M(); // implement I1.M with an abstract method in C
}

Entscheidung: (2017-04-18) ja, das Hinzufügen eines Texts zu einer Schnittstellenmember-Deklaration sollte C
nicht unterbrechen.
Versiegelte außer Kraft Setzung (geschlossen)
Die vorherige Frage geht implizit davon aus, dass der- sealed Modifizierer auf eine override in einer
Schnittstelle angewendet werden kann. Dies widerspricht der Entwurfs Spezifikation. Möchten wir das
Versiegeln einer außer Kraft Setzung zulassen? Die Auswirkungen auf die Quelle und die binäre Kompatibilität
der Versiegelung sollten berücksichtigt werden.

Geschlossene Probleme: Sollten wir das Versiegeln einer außer Kraft Setzung zulassen?

Entscheidung: (2017-04-18) ist für außer Kraft setzungen sealed in Schnittstellen nicht zulässig. Die einzige
Verwendung von sealed für Schnittstellenmember besteht darin, dass Sie in ihrer ursprünglichen Deklaration
nicht virtuell sind.
Rautenvererbung und-Klassen (geschlossen)
Der Entwurf des Angebots bevorzugt die Außerkraftsetzungs Überschreibungen von Klassen in
rautenvererbungs Szenarien:

Wir verlangen, dass jede Schnittstelle und Klasse eine spezifischere außer Kraft setzung für jede
Schnittstellen Methode unter den außer Kraft setzungen aufweisen, die im Typ oder den direkten und
indirekten Schnittstellen angezeigt werden. Die spezifischere außer Kraft Setzung ist eine eindeutige außer
Kraft setzung, die spezifischer ist als jede andere außer Kraft Setzung. Wenn keine außer Kraft Setzung
vorhanden ist, wird die Methode selbst als die spezifischere außer Kraft Setzung betrachtet.
Eine außer Kraft Setzung M1 gilt als spezifischere als eine andere außer Kraft Setzung M2 M1 , wenn für
den Typ deklariert wird T1 , M2 für den Typ deklariert wird T2 , und
1. T1 enthält T2 unter den direkten oder indirekten Schnittstellen oder
2. T2 ist ein Schnittstellentyp, T1 ist jedoch kein Schnittstellentyp.

Das Szenario ist

interface IA
{
void M();
}
interface IB : IA
{
override void M() { WriteLine("IB"); }
}
class Base : IA
{
void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
static void Main()
{
Ia a = new Derived();
a.M(); // what does it do?
}
}

Wir sollten dieses Verhalten bestätigen (oder andernfalls festlegen).

*Geschlossenes Problem: _ bestätigen Sie die Entwurfs Spezifikation (oben) für _most spezifische außer
Kraft setzung *, da Sie auf gemischte Klassen und Schnittstellen angewendet wird (eine Klasse hat Vorrang
vor einer Schnittstelle). Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-
2017-04-19.md#diamonds-with-classes.
Schnittstellen Methoden im Vergleich zu Strukturen (geschlossen)
Es gibt einige unglückliche Interaktionen zwischen Standardschnittstellen Methoden und Strukturen.

interface IA
{
public void M() { }
}
struct S : IA
{
}

Beachten Sie, dass Schnittstellenmember nicht geerbt werden:

var s = default(S);
s.M(); // error: 'S' does not contain a member 'M'

Folglich muss der Client die Struktur zum Aufrufen von Schnittstellen Methoden in Box Box

IA s = default(S); // an S, boxed
s.M(); // ok

Wenn Sie auf diese Weise Boxing, werden die Hauptvorteile eines struct Typs zunichte. Außerdem haben alle
mutations Methoden keinen offensichtlichen Effekt, da Sie auf einer geachtelten Kopie der Struktur ausgeführt
werden:

interface IB
{
public void Increment() { P += 1; }
public int P { get; set; }
}
struct T : IB
{
public int P { get; set; } // auto-property
}

T t = default(T);
Console.WriteLine(t.P); // prints 0
(t as IB).Increment();
Console.WriteLine(t.P); // prints 0

Geschlossene Probleme: Dazu können Sie Folgendes tun:


1. Verbieten, dass ein struct eine Standard Implementierung erbt. Alle Schnittstellen Methoden werden in
einer als abstrakt behandelt struct . Dann können wir später einige Zeit in Anspruch nehmen, um die
Arbeit zu verbessern.
2. Entwickeln Sie eine Art Code Generierungs Strategie, die Boxing vermeidet. Innerhalb einer Methode wie
IB.Increment wäre der Typ von this vielleicht vergleichbar mit einem Typparameter, der auf
eingeschränkt ist IB . Um das Boxing im Aufrufer zu vermeiden, werden nicht abstrakte Methoden in
Verbindung mit den Schnittstellen geerbt. Dadurch kann der Compiler und die CLR-Implementierung
erheblich zunehmen.
3. Machen Sie sich keine Gedanken um die IT, und lassen Sie Sie einfach als wart.
4. Weitere Ideen?

Entscheidung: Machen Sie sich keine Gedanken um die IT, und lassen Sie Sie einfach als wart. Siehe
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#structs-and-default-
implementations.
Basis Schnittstellen Aufrufe (geschlossen)
Die Entwurfs Spezifikation schlägt eine Syntax für Basis Schnittstellen Aufrufe vor, die von Java inspiriert sind:
Interface.base.M() . Wir müssen eine Syntax auswählen, zumindest für den anfänglichen Prototyp. Mein
Favorit ist base<Interface>.M() .

Geschlossene Probleme: Was ist die Syntax für einen Basismember-Aufruf?

Entscheidung: Die Syntax ist base(Interface).M() . Siehe


https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation. Die
Schnittstelle, die so benannt ist, muss eine Basisschnittstelle sein, es muss sich jedoch nicht um eine direkte
Basisschnittstelle handeln.

Problem öffnen: Sollten Basis Schnittstellen Aufrufe in Klassenmembern zulässig sein?

Entscheidung : Ja. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-


19.md#base-invocation
Überschreiben von nicht öffentlichen Schnittstellenmembern (geschlossen)
In einer Schnittstelle werden nicht öffentliche Member von Basis Schnittstellen mit dem- override Modifizierer
überschrieben. Wenn es sich um eine explizite außer Kraft Setzung handelt, die die Schnittstelle mit dem
Member benennt, wird der Zugriffsmodifizierer ausgelassen.

Geschlossene Probleme: Muss der Zugriffsmodifizierer abgeglichen werden, wenn es sich um eine
"implizite" außer Kraft Setzung handelt, die der Schnittstelle nicht entspricht?

Entscheidung: Nur öffentliche Member können implizit überschrieben werden, und der Zugriff muss
entsprechend erfolgen. Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-
04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.

Problem öffnen: Ist der Zugriffsmodifizierer für eine explizite außer Kraft Setzung erforderlich, optional
oder ausgelassen, z.b. override void IB.M() {} ?

Problem öffnen: Ist override erforderlich, optional oder bei einer expliziten außer Kraft Setzung
ausgelassen, z. b. void IB.M() {} ?

Wie implementiert ein nicht öffentliches Schnittstellenmember in einer Klasse? Sie müssen möglicherweise
explizit ausgeführt werden?

interface IA
{
internal void MI();
protected void MP();
}
class C : IA
{
// are these implementations?
internal void MI() {}
protected void MP() {}
}

Geschlossene Probleme: Wie implementiert ein nicht öffentliches Schnittstellenmember in einer Klasse?
Entscheidung: Nicht öffentliche Schnittstellenmember können nur explizit implementiert werden. Siehe
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-
a-non-public-interface-member-not-in-list.
Entscheidung : override für Schnittstellenmember ist kein Schlüsselwort zulässig.
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-
an-interface-introduce-a-new-member
Binäre Kompatibilität 2 (geschlossen)
Beachten Sie den folgenden Code, in dem sich die einzelnen Typen in einer separaten Assembly befinden.

interface I1
{
void M() { Impl1 }
}
interface I2 : I1
{
override void M() { Impl2 }
}
interface I3 : I1
{
}
class C : I2, I3
{
}

Wir wissen, dass die Implementierung von I1.M in C ist I2.M . Was geschieht, wenn die Assembly, die enthält,
I3 wie folgt geändert und neu kompiliert wird.

interface I3 : I1
{
override void M() { Impl3 }
}

C wird jedoch nicht neu kompiliert. Was geschieht, wenn das Programm ausgeführt wird? Ein Aufruf von
(C as I1).M()

1. Parallel I1.M
2. Parallel I2.M
3. Parallel I3.M
4. Entweder 2 oder 3, deterministisch
5. Löst eine Art von Lauf Zeit Ausnahme aus.
Entscheidung : lösen Sie eine Ausnahme aus (5). Siehe
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#issues-in-default-
interface-methods.
partial In Schnittstelle zulassen? geschlossen
Da Schnittstellen in ähnlicher Weise wie abstrakte Klassen verwendet werden können, kann es hilfreich sein, Sie
zu deklarieren partial . Dies wäre besonders nützlich bei Generatoren.

Angebot: Entfernen Sie die sprach Einschränkung, dass Schnittstellen und Member von Schnittstellen nicht
deklariert werden dürfen partial .

Entscheidung : Ja. Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-


17.md#permit-partial-in-interface.
Main in einer Schnittstelle? geschlossen

Problem öffnen: Ist eine static Main Methode in einer Schnittstelle ein Kandidat für den Einstiegspunkt
des Programms?

Entscheidung : Ja. Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-


17.md#main-in-an-interface.
Bestätigen, dass öffentliche, nicht virtuelle Methoden unterstützt werden sollen (geschlossen)
Können wir unsere Entscheidung bestätigen (oder umkehren), um nicht virtuelle öffentliche Methoden in einer
Schnittstelle zuzulassen?

interface IA
{
public sealed void M() { }
}

Semiclosed-Problem: (2017-04-18) Wir sind davon überzeugt, dass es nützlich sein wird, es wird jedoch
zurückgegeben. Dabei handelt es sich um einen Block für das Muster des geistigen Modells.

Entscheidung : Ja. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-


17.md#confirm-that-we-support-public-non-virtual-methods.
Führt ein override in einer Schnittstelle ein neues Mitglied ein? geschlossen
Es gibt mehrere Möglichkeiten, um zu überprüfen, ob eine Überschreibungs Deklaration einen neuen Member
einführt.

interface IA
{
void M(int x) { }
}
interface IB : IA
{
override void M(int y) { }
}
interface IC : IB
{
static void M2()
{
M(y: 3); // permitted?
}
override void IB.M(int z) { } // permitted? What does it override?
}

Problem öffnen: Führt eine Überschreibungs Deklaration in einer Schnittstelle einen neuen Member ein?
geschlossen

In einer Klasse ist eine über schreibende Methode in gewisser Hinsicht "sichtbar". Die Namen der Parameter
haben beispielsweise Vorrang vor den Namen von Parametern in der überschriebenen Methode.
Möglicherweise ist es möglich, dieses Verhalten in Schnittstellen zu duplizieren, da immer eine spezifischere
außer Kraft Setzung vorliegt. Möchten Sie dieses Verhalten jedoch duplizieren?
Außerdem ist es möglich, eine Überschreibungs Methode außer Kraft zu setzen? Fraglich
Entscheidung : override für Schnittstellenmember ist kein Schlüsselwort zulässig.
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-
an-interface-introduce-a-new-member.
Eigenschaften mit einem privaten Accessor (geschlossen)
Wir sagen, dass private Member nicht virtuell sind und die Kombination aus Virtual und private nicht zulässig
ist. Aber wie sieht es mit einer Eigenschaft mit einem privaten Accessor aus?

interface IA
{
public virtual int P
{
get => 3;
private set => { }
}
}

Ist dies zulässig? Ist der set Accessor hier virtual oder nicht? Kann sie überschrieben werden, wo Sie
zugänglich ist? Implementiert Folgendes implizit nur den get Accessor?

class C : IA
{
public int P
{
get => 4;
set { }
}
}

Der folgende Fehler ist vermutlich ein Fehler, weil IA vorliegt. P. Set ist nicht virtuell und auch weil nicht darauf
zugegriffen werden kann?

class C : IA
{
int IA.P
{
get => 4;
set { }
}
}

Entscheidung : das erste Beispiel ist gültig, während das letzte nicht. Dies wird analog zur Funktionsweise in c#
aufgelöst. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-
17.md#properties-with-a-private-accessor
Basis Schnittstellen Aufrufe, Round 2 (geschlossen)
Unsere vorherige "Lösung" zur Behandlung von grundlegenden aufrufen bietet keine ausreichende
Ausdruckskraft. Es stellt sich heraus, dass Sie in c# und CLR im Gegensatz zu Java sowohl die-Schnittstelle
angeben müssen, die die Methoden Deklaration enthält, als auch den Speicherort der Implementierung, die Sie
aufrufen möchten.
Ich schlage die folgende Syntax für Basis Aufrufe in Schnittstellen vor. Ich bin nicht in der Liebe, aber es wird
veranschaulicht, welche Syntax in der Lage sein muss, folgendes auszudrücken:
interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I4 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3, I4
{
void I1.M()
{
base<I3>(I1).M(); // calls I3's implementation of I1.M
base<I4>(I1).M(); // calls I4's implementation of I1.M
}
void I2.M()
{
base<I3>(I2).M(); // calls I3's implementation of I2.M
base<I4>(I2).M(); // calls I4's implementation of I2.M
}
}

Wenn keine Mehrdeutigkeit vorliegt, können Sie Sie einfacher schreiben.

interface I1 { void M(); }


interface I3 : I1 { void I1.M() { } }
interface I4 : I1 { void I1.M() { } }
interface I5 : I3, I4
{
void I1.M()
{
base<I3>.M(); // calls I3's implementation of I1.M
base<I4>.M(); // calls I4's implementation of I1.M
}
}

oder

interface I1 { void M(); }


interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3
{
void I1.M()
{
base(I1).M(); // calls I3's implementation of I1.M
}
void I2.M()
{
base(I2).M(); // calls I3's implementation of I2.M
}
}

oder

interface I1 { void M(); }


interface I3 : I1 { void I1.M() { } }
interface I5 : I3
{
void I1.M()
{
base.M(); // calls I3's implementation of I1.M
}
}
Entscheidung : entscheiden Sie base(N.I1<T>).M(s) sich dafür, dass bei einer Aufruf Bindung möglicherweise
später ein Problem vorliegt. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-11-
14.md#default-interface-implementations
Warnung für die Struktur, die die Standardmethode nicht implementiert? geschlossen
@vancem bestätigt, dass wir ernsthaft eine Warnung erstellen sollten, wenn eine Werttyp Deklaration eine
Schnittstellen Methode nicht außer Kraft setzt, auch wenn Sie eine Implementierung dieser Methode von einer
Schnittstelle erbt. Da dies zum Boxing und zum untergraben von eingeschränkten aufrufen bewirkt.
Entscheidung : Dies scheint etwas, das für einen Analyzer besser geeignet ist. Es scheint auch, dass diese
Warnung nicht mehr angezeigt wird, da Sie auch dann ausgelöst würde, wenn die Standardschnittstellen
Methode niemals aufgerufen wird und nie Boxing auftritt.
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#warning-for-struct-
not-implementing-default-method
Statische Schnittstellen Konstruktoren (geschlossen)
Wann werden statische Konstruktoren für die Schnittstelle ausgeführt? Der aktuelle CLI-Entwurf schlägt vor, dass
er auftritt, wenn auf die erste statische Methode oder das erste Feld zugegriffen wird. Wenn keines davon
vorhanden ist, wird es möglicherweise nie ausgeführt?
[2018-10-09 das CLR-Team schlägt vor, was wir für Werttypen tun (cctor Check on Access to each instance
method)]
Entscheidung : statische Konstruktoren werden auch für den Eintrag in Instanzmethoden ausgeführt, wenn der
statische Konstruktor nicht war beforefieldinit . in diesem Fall werden statische Konstruktoren vor dem
Zugriff auf das erste statische Feld ausgeführt.
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#when-are-interface-
static-constructors-run

Treffen von Besprechungen


2017-03-08 LDM-Besprechungs Hinweise 2017-03-21 LDM-Besprechungs Hinweise 2017-03-23 Meeting
"CLR-Verhalten für Standardschnittstellen Methoden" 2017-04-05 LDM-Besprechungs Hinweise 2017-04-11
LDM-Besprechungs Hinweise 2017-04-18 LDM-Besprechungs Hinweise 2017-04-19 LDM-Besprechungs
Hinweise 2017-05-17 LDM-Besprechungs Hinweise 2017-05-31 LDM-Besprechungs Hinweise 2017-06-14
LDM-Besprechungs Hinweise 2018-10-17 LDM-Besprechungs Hinweise 2018-11-14 LDM-Besprechungs
Hinweise
Async-Streams
04.11.2021 • 39 minutes to read

[x] vorgeschlagen
[x] Prototyp
[]-Implementierung
[]-Spezifikation

Zusammenfassung
C# unterstützt Iteratormethoden und asynchrone Methoden, bietet aber keine Unterstützung für eine Methode,
bei der es sich um einen Iterator und eine Async-Methode handelt. Dies sollten Sie beheben, indem Sie zulassen,
dass await in einer neuen Form eines async Iterators verwendet wird, der einen IAsyncEnumerable<T> oder
IAsyncEnumerator<T> anstelle von oder zurückgibt IEnumerable<T> , der IEnumerator<T> IAsyncEnumerable<T> in
einem neuen verwendet werden kann await foreach . Eine IAsyncDisposable Schnittstelle wird auch verwendet,
um das asynchrone Cleanup zu aktivieren.

Verwandte Diskussion
https://github.com/dotnet/roslyn/issues/261
https://github.com/dotnet/roslyn/issues/114

Detaillierter Entwurf
Schnittstellen
Iasyncverwerfbare
Es gab viele Erörterung von IAsyncDisposable (z. b. https://github.com/dotnet/roslyn/issues/114) und ob es
eine gute Idee ist). Es ist jedoch ein erforderliches Konzept, das für die Unterstützung von asynchronen Iteratoren
hinzugefügt werden muss. Da finally Blöcke möglicherweise await s enthalten, und da finally Blöcke im
Rahmen der Freigabe von Iteratoren ausgeführt werden müssen, ist eine asynchrone Entfernung erforderlich. Es
ist auch in der Regel hilfreich, wenn Ressourcen bereinigt werden können, z. b. das Schließen von Dateien (d. h.
Leerungen), das Aufheben von Rückrufen und das Bereitstellen einer Möglichkeit, zu wissen, wann die
Registrierung abgeschlossen ist usw.
Die folgende Schnittstelle wird den .net-Kernbibliotheken (z. b. System. private. Corelib/System. Runtime)
hinzugefügt:

namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}

Wie bei Dispose ist das Aufrufen von mehrmals DisposeAsync zulässig, und nachfolgende Aufrufe nach dem
ersten müssen als nops behandelt werden. dabei wird eine synchron abgeschlossene Aufgabe zurückgegeben
(Sie DisposeAsync müssen jedoch nicht Thread sicher sein, und gleichzeitige Aufrufe müssen nicht unterstützt
werden). Darüber hinaus können Typen sowohl IDisposable als auch implementieren IAsyncDisposable , und
wenn dies der Fall ist, ist es genauso akzeptabel, aufzurufen Dispose und dann DisposeAsync oder umgekehrt,
aber nur das erste sollte sinnvoll sein, und nachfolgende Aufrufe von beiden sollten ein NOP sein. Wenn ein Typ
beide implementiert, wird empfohlen, nur einmal und nur einmal die relevantere Methode auf der Grundlage
des Kontexts, Dispose in synchronen Kontexten und DisposeAsync in asynchronen Kontexten aufzurufen.
(In diesem Artikel wird erläutert IAsyncDisposable , wie mit using in eine separate Diskussion interagiert wird.
Und die Abdeckung der Interaktion mit foreach wird später in diesem Vorschlag behandelt.)
Berücksichtigte Alternativen:
DisposeAsync akzeptieren a CancellationToken : Obwohl es theoretisch ist, dass alle asynchronen Elemente
abgebrochen werden können, ist die Bereinigung der Bereinigung, das Schließen von Dingen, das Einfrieren
von Ressourcen usw., was in der Regel nicht abgebrochen werden sollte. die Bereinigung ist nach wie vor
wichtig für die abgebrochene Arbeit. Das gleiche CancellationToken , das dazu geführt hat, dass die
tatsächliche Arbeit abgebrochen wurde, wäre in der Regel das gleiche Token, das an übergebene wird
DisposeAsync , was zu DisposeAsync einem wertlos führt, weil der Abbruch der Arbeit DisposeAsync zu
einem NOP Wenn Sie verhindern möchten, dass eine Sperre blockiert wird, können Sie vermeiden, auf das
resultierende zu warten ValueTask , oder darauf warten, dass Sie nur für einen bestimmten Zeitraum
warten.
DisposeAsync zurückgeben Task a: Nachdem ein nicht generisches ValueTask vorhanden ist und aus einem
erstellt werden kann IValueTaskSource , kann durch die Rückgabe von ein ValueTask DisposeAsync
vorhandenes-Objekt wieder verwendet werden, das als Zusage die spätere asynchrone Fertigstellung von
darstellt DisposeAsync , Task wobei eine Zuordnung in dem Fall gespeichert wird, in der DisposeAsync
asynchron abgeschlossen wird.
Konfigurieren von DisposeAsync mit a bool continueOnCapturedContext ( ConfigureAwait ): Es gibt zwar
Probleme im Zusammenhang mit der Offenlegung eines solchen Konzepts für using , foreach und andere
Sprachkonstrukte, die diese verwenden. aus Schnittstellen Sicht führt es jedoch keine "Do"-Aktion durch,
await und es ist nichts zu konfigurieren... Consumer der ValueTask können Sie jedoch verwenden.
Erben: da nur ein oder das andere verwendet werden sollte, ist es nicht sinnvoll, die Implementierung beider
Typen zu erzwingen. IAsyncDisposable IDisposable
IDisposableAsync anstelle von IAsyncDisposable : Wir haben der Namensgebung folgen, dass es sich bei den
Vorgängen/Typen um ein "Async etwas" handelt, während Vorgänge "Done Async" sind, sodass die Typen
"Async" als Präfix aufweisen und Methoden "Async" als Suffix aufweisen.
Iasyncengerable/iasyncenumschlag
Den .net-Kernbibliotheken werden zwei Schnittstellen hinzugefügt:

namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}

public interface IAsyncEnumerator<out T> : IAsyncDisposable


{
ValueTask<bool> MoveNextAsync();
T Current { get; }
}
}

Der typische Verbrauch (ohne zusätzliche sprach Features) sieht wie folgt aus:
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
Use(enumerator.Current);
}
}
finally { await enumerator.DisposeAsync(); }

Verworfene Optionen:
Task<bool> MoveNextAsync(); T current { get; } : Task<bool> Die Verwendung von unterstützt die
Verwendung eines zwischengespeicherten Task Objekts zur Darstellung synchroner, erfolgreicher
MoveNextAsync Aufrufe, aber eine Zuordnung wäre weiterhin für den asynchronen Abschluss erforderlich.
Durch die Rückgabe von ValueTask<bool> wird das Enumeratorobjekt auf sich selbst implementieren
IValueTaskSource<bool> und als Unterstützung für das ValueTask<bool> von zurückgegebene verwendet
MoveNextAsync , das wiederum deutlich reduzierten Overheads ermöglicht.
ValueTask<(bool, T)> MoveNextAsync(); : Es ist nicht nur schwerer zu verwenden, sondern es bedeutet, dass
T nicht mehr kovariant sein kann.
ValueTask<T?> TryMoveNextAsync(); : Nicht kovariant.
Task<T?> TryMoveNextAsync(); : Nicht kovariant, Zuordnungen bei jedem-Befehl usw.
ITask<T?> TryMoveNextAsync(); : Nicht kovariant, Zuordnungen bei jedem-Befehl usw.
ITask<(bool,T)> TryMoveNextAsync(); : Nicht kovariant, Zuordnungen bei jedem-Befehl usw.
Task<bool> TryMoveNextAsync(out T result); : Das out Ergebnis müsste festgelegt werden, wenn der
Vorgang synchron zurückgegeben wird, und nicht, wenn die Aufgabe asynchron abgeschlossen wird, die
möglicherweise später in der Zukunft liegt. zu diesem Zeitpunkt gibt es keine Möglichkeit, das Ergebnis zu
kommunizieren.
IAsyncEnumerator<T> nicht implementiert IAsyncDisposable : Wir könnten diese Optionen trennen. Dies
erschwert jedoch bestimmte andere Bereiche des Angebots, da Code dann in der Lage sein muss, mit der
Möglichkeit zu umgehen, dass ein Enumerator keine Entsorgung bereitstellt. dadurch ist es schwierig,
Musterbasierte Hilfsprogramme zu schreiben. Außerdem ist es für Enumeratoren üblich, dass Sie zur
Verfügung stehen (z. b. einen asynchronen c#-Iterator, der über einen letzten Block verfügt, in dem die
meisten Elemente von einer Netzwerkverbindung aufgelistet sind usw.), und wenn dies nicht der Fall ist, ist es
einfach, die Methode nur public ValueTask DisposeAsync() => default(ValueTask); mit minimalem
zusätzlichen Aufwand zu implementieren.
_ IAsyncEnumerator<T> GetAsyncEnumerator() : Kein Abbruch Token-Parameter.
Mögliche Alternative:

namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator();
}

public interface IAsyncEnumerator<out T> : IAsyncDisposable


{
ValueTask<bool> WaitForNextAsync();
T TryGetNext(out bool success);
}
}

TryGetNext wird in einer inneren Schleife verwendet, um Elemente mit einem einzelnen Schnittstellen
Aufrufwert zu verarbeiten, solange Sie synchron verfügbar sind. Wenn das nächste Element nicht synchron
abgerufen werden kann, wird false zurückgegeben, und jedes Mal, wenn es false zurückgibt, muss ein Aufrufer
später aufrufen, um zu warten, bis WaitForNextAsync das nächste Element verfügbar ist, oder um zu bestimmen,
dass es nie ein anderes Element gibt. Der typische Verbrauch (ohne zusätzliche sprach Features) sieht wie folgt
aus:

IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();


try
{
while (await enumerator.WaitForNextAsync())
{
while (true)
{
int item = enumerator.TryGetNext(out bool success);
if (!success) break;
Use(item);
}
}
}
finally { await enumerator.DisposeAsync(); }

Der Vorteil besteht darin, dass zwei fache, ein kleiner und ein hauptsächlich:
Neben Version: ermöglicht einem Enumerator, mehrere Consumer zu unterstützen. Möglicherweise gibt es
Szenarien, in denen es für einen Enumerator wichtig ist, mehrere gleichzeitige Consumer zu unterstützen.
Dies ist nicht möglich, wenn MoveNextAsync und Current voneinander getrennt sind, sodass eine
Implementierung ihre Verwendung nicht atomarisch machen kann. Im Gegensatz dazu bietet dieser Ansatz
eine einzelne Methode TryGetNext , die das Übertragen des Enumerators und das nächste Element
unterstützt, sodass der Enumerator die Atomizität aktivieren kann, wenn gewünscht. Es ist jedoch
wahrscheinlich, dass solche Szenarios auch aktiviert werden können, indem jeder Consumer seinen eigenen
Enumerator aus einem freigegebenen Aufzähl baren Element bereitstellt. Außerdem möchten wir nicht
erzwingen, dass jeder Enumerator die gleichzeitige Verwendung unterstützt, da dadurch nicht triviale
Aufwand zum Mehrheits Fall hinzugefügt werden, der nicht erforderlich ist. Dies bedeutet, dass sich ein
Consumer der Schnittstelle im Allgemeinen nicht auf diese Weise verlassen kann.
Hauptversion: Leistung. Der MoveNextAsync / Current Ansatz erfordert zwei Schnittstellen Aufrufe pro
Vorgang, wohingegen der beste Fall für WaitForNextAsync / TryGetNext ist, dass die meisten Iterationen
synchron ausgeführt werden, sodass eine enge innere Schleife mit ermöglicht wird TryGetNext , sodass nur
ein Schnittstellen Aufruf pro Vorgang erfolgt. Dies kann in Situationen, in denen die Schnittstellen Aufrufe die
Berechnung dominieren, messbare Auswirkungen haben.
Es gibt jedoch nicht triviale Nachteile, einschließlich einer erheblich größeren Komplexität, wenn diese manuell
genutzt werden, und eine größere Chance, bei deren Verwendung Fehler zu verursachen. Und während die
Leistungsvorteile in den Mikrobenchmarks angezeigt werden, glauben wir nicht, dass Sie sich auf den größten
Teil der realen Verwendung auswirken werden. Wenn sich dies herausstellt, können wir einen zweiten Satz von
Schnittstellen in einer hellen Weise einführen.
Verworfene Optionen:
ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result); : out Parameter können nicht kovariant
sein. Hier gibt es auch eine kleine Auswirkung (ein Problem mit dem try-Muster im allgemeinen), dass dies
wahrscheinlich eine Lauf Zeit Schreib Barriere für Verweistyp Ergebnisse verursacht.
Abbruch
Es gibt mehrere mögliche Ansätze zur Unterstützung von Abbruch:
1. IAsyncEnumerable<T> / IAsyncEnumerator<T> sind Abbruch agnostisch: wird CancellationToken nicht an einer
beliebigen Stelle angezeigt. Der Abbruch wird erreicht, indem das logisch in CancellationToken den Aufzähl
Bare-und/oder-Enumerator in beliebiger Weise durchsucht wird, z. b. beim Aufrufen eines Iterators, beim
übergeben CancellationToken von als Argument an die Iteratormethode und deren Verwendung im Text des
Iterators, wie es bei einem anderen Parameter der Fall ist.
2. IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken) : Übergeben Sie einen CancellationToken an
GetAsyncEnumerator , und nachfolgende MoveNextAsync Vorgänge berücksichtigen dies, dies kann jedoch der
Fall sein.
3. IAsyncEnumerator<T>.MoveNextAsync(CancellationToken) : Übergeben Sie ein CancellationToken an jeden
einzelnen-Befehl MoveNextAsync .
4. 1 && 2: Sie Betten CancellationToken s in ihren Enumerable/Enumerator ein und übergeben Sie
CancellationToken in GetAsyncEnumerator .
5. 1 && 3: Sie Betten CancellationToken s in ihren Enumerable/Enumerator ein und übergeben Sie
CancellationToken in MoveNextAsync .

Aus rein theoretischen Sicht ist (5) die stabilste, da (a) das akzeptieren von MoveNextAsync a CancellationToken
die präzisere Kontrolle über das abgebrochene ermöglicht, und (b) CancellationToken ist nur ein beliebiger
anderer Typ, der als Argument an Iteratoren übergeben werden kann, die in beliebige Typen eingebettet sind,
usw.
Bei diesem Ansatz gibt es jedoch mehrere Probleme:
Wie wird ein CancellationToken durchlaufen, um GetAsyncEnumerator ihn in den Text des Iterators zu legen?
Wir könnten ein neues iterator Schlüsselwort verfügbar machen, für das Sie den Zugriff auf die
CancellationToken haben, die an übermittelt GetEnumerator wird. a) das ist aber eine Menge zusätzlicher
Maschinen, b) wir machen es zu einem erstklassigen Bürger und c) der Fall von 99% wäre der gleiche Code,
der sowohl einen Iterator als auch einen Aufruf von einem Iterator und einen Aufruf für diesen Code
GetAsyncEnumerator hat. in diesem Fall kann er einfach das CancellationToken als Argument an die Methode
übergeben.
Wie wird ein CancellationToken an MoveNextAsync den Text der-Methode geleitet? Dies ist noch schlimmer,
als ob es von einem lokalen Objekt verfügbar gemacht wird. der iterator Wert kann sich über mehrere
Ebenen hinweg ändern. Dies bedeutet, dass jeder Code, der mit dem Token registriert ist, die Registrierung
bei ihm vor den Vorgängen aufheben und dann erneut registrieren muss. es ist auch möglich, dass die
Registrierung und die Aufhebung der Registrierung in jedem-Befehl MoveNextAsync durchgeführt werden
müssen, unabhängig davon, ob der Compiler in einem Iterator oder einem Entwickler manuell implementiert
ist.
Wie bricht ein Entwickler eine foreach Schleife ab? Wenn dies geschieht, indem einem CancellationToken
Aufzähl Bare/Enumerator eine zugewiesen wird, dann müssen wir eine foreach "over Enumeratoren"
unterstützen, die Sie als erstklassige Bürger auslöst. und jetzt müssen Sie sich über ein Ökosystem Gedanken
machen, das auf Enumeratoren aufbaut (z. b. LINQ-Methoden) oder b) Wir müssen das CancellationToken in
das Aufzähl Bare Element einbinden, indem wir eine WithCancellation Erweiterungsmethode von benötigen
IAsyncEnumerable<T> , die das bereitgestellte Token speichert und es dann an das umschließende Aufzähl
Bare Element übergibt, GetAsyncEnumerator Wenn der GetAsyncEnumerator auf der zurückgegebenen Struktur
aufgerufen wird. Oder Sie können das verwenden, das CancellationToken Sie im Text des Foreach-Texts
verwenden.
Wenn/wenn Abfrage Ausdrücke unterstützt werden, wie würde die CancellationToken bereitgestellte
GetEnumerator oder an MoveNextAsync jede Klausel übergeben werden? Am einfachsten wäre es, wenn die-
Klausel die-Klausel erfasst. an diesem Punkt wird das Token, an das das-Token übermittelt wird,
GetAsyncEnumerator / MoveNextAsync ignoriert.

Eine frühere Version dieses Dokuments wurde empfohlen (1), wir haben jedoch zu (4) gewechselt.
Die zwei Hauptprobleme bei (1):
Producer von abbrechbaren Enumerationen müssen einige Bausteine implementieren und können nur die
Unterstützung von asynchronen Iteratoren durch den Compiler nutzen, um eine Methode zu implementieren
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken) .
Es ist wahrscheinlich, dass viele Producer einfach einen CancellationToken Parameter zu ihrer asynchronen
Enumerable-Signatur hinzufügen würden, wodurch verhindert wird, dass Consumer das gewünschte
Abbruch Token übergeben, wenn Sie einen- IAsyncEnumerable Typ erhalten.

Es gibt zwei Haupt Verwendungs Szenarien:


1. await foreach (var i in GetData(token)) ... wo der Consumer die Async-Iterator-Methode aufruft,
2. await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ... der Consumer behandelt eine
bestimmte- IAsyncEnumerable Instanz.

Wir stellen fest, dass ein angemessener Kompromiss zur Unterstützung beider Szenarien auf eine Weise, die
sowohl für Producer als auch für Consumer von asynchronen Streams geeignet ist, die Verwendung eines
speziell mit Anmerkungen versehene Parameters in der Async-Iterator-Methode ist. [EnumeratorCancellation]
Zu diesem Zweck wird das-Attribut verwendet. Das Platzieren dieses Attributs für einen Parameter weist den
Compiler an, dass, wenn ein Token an die Methode übergeben wird GetAsyncEnumerator , dieses Token anstelle
des ursprünglich für den-Parameter übergebenen Werts verwendet werden soll.
Gehen Sie von IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default) aus.
Der Implementierer dieser Methode kann einfach den-Parameter im Methoden Text verwenden. Der Consumer
kann entweder die folgenden Verbrauchsmuster verwenden:
1. Wenn Sie verwenden GetData(token) , wird das Token in der Async-Enumerable gespeichert und in
Iterationen verwendet.
2. Wenn Sie verwenden givenIAsyncEnumerable.WithCancellation(token) , ersetzt das an übergebenen Token
GetAsyncEnumerator alle Token, die im Async-Enumerable-Element gespeichert werden.

foreach
foreach wird erweitert, um IAsyncEnumerable<T> zusätzlich zu der vorhandenen Unterstützung für zu
unterstützen IEnumerable<T> . Außerdem wird die Entsprechung von IAsyncEnumerable<T> als Muster
unterstützt, wenn die relevanten Member öffentlich verfügbar gemacht werden, wobei die Schnittstelle direkt
verwendet wird, wenn dies nicht der Fall ist, um auf struct basierende Erweiterungen zu aktivieren, die die
Zuordnung vermeiden und Alternative awagables als Rückgabetyp von MoveNextAsync und verwenden
DisposeAsync .

Syntax
Die Syntax lautet .

foreach (var i in enumerable)

C# wird weiterhin enumerable als synchrones Aufzähl bares Element behandelt, sodass selbst dann, wenn es die
relevanten APIs für asynchrone Enumerables verfügbar macht (das Muster verfügbar macht oder die
Schnittstelle implementiert), nur die synchronen APIs berücksichtigt werden.
Um zu erzwingen, dass foreach Sie stattdessen nur die asynchronen APIs berücksichtigt, await wird wie folgt
eingefügt:

await foreach (var i in enumerable)

Es wurde keine Syntax bereitgestellt, die die Verwendung der Async-oder Sync-APIs unterstützt. der Entwickler
muss basierend auf der verwendeten Syntax auswählen.
Verworfene Optionen:
foreach (var i in await enumerable) : Dies ist bereits eine gültige Syntax, und das Ändern der Bedeutung ist
eine Breaking Change. Dies bedeutet await enumerable , dass eine synchrone Iterable aus der Datei
zurückgibt und diese dann synchron durchläuft.
foreach (var i await in enumerable) , foreach (var await i in enumerable) ,
foreach (await var i in enumerable) : Dies deutet darauf hin, dass wir auf das nächste Element warten, aber
es gibt noch weitere Warteschlangen, die an foreach beteiligt sind, insbesondere, wenn das Aufzähl Bare
Element ein ist IAsyncDisposable await . Diese Erwartung ist als Bereich von foreach und nicht für jedes
einzelne Element, weshalb das await Schlüsselwort auf der Ebene liegen muss foreach . Wenn Sie mit dem
verknüpft ist, können foreach wir die foreach mit einem anderen Begriff beschreiben, z. b. "warten auf
foreach". Noch wichtiger ist jedoch, foreach dass Sie die Syntax gleichzeitig mit der Syntax in Erwägung
ziehen using , damit Sie konsistent zueinander bleiben und using (await ...) bereits eine gültige Syntax
ist.
foreach await (var i in enumerable)

Beachten Sie Folgendes:


foreach Derzeit unterstützt das Iterieren durch einen Enumerator nicht. Wir gehen davon aus, dass es
häufiger vorhanden sein wird IAsyncEnumerator<T> , und daher ist es verlockend, await foreach sowohl mit
als auch mit zu unterstützen IAsyncEnumerable<T> IAsyncEnumerator<T> . Nachdem wir diese Unterstützung
hinzugefügt haben, wird die Frage gestellt, ob es IAsyncEnumerator<T> sich um einen erstklassigen Bürger
handelt und ob Sie über über Ladungen von combinatoren verfügen müssen, die auf Enumeratoren neben
Enumerationen angewendet werden? Möchten wir Methoden zum Zurückgeben von Enumeratoren anstelle
von Enumerables ermutigen? Wir sollten dies weiterhin besprechen. Wenn wir entscheiden, dass wir Sie nicht
unterstützen möchten, können wir eine Erweiterungsmethode einführen,
public static IAsyncEnumerable<T> AsEnumerable<T>(this IAsyncEnumerator<T> enumerator); die es einem
Enumerator ermöglicht, weiterhin zu werden foreach . Wenn wir uns entscheiden, dies zu unterstützen,
müssen wir auch entscheiden, ob der await foreach für den Aufruf von für DisposeAsync den Enumerator
zuständig wäre, und die Antwort ist wahrscheinlich, dass die Kontrolle über die Entfernung von jedem
Benutzer behandelt werden soll, der aufgerufen hat GetEnumerator .
Musterbasierte Kompilierung
Der Compiler bindet eine Bindung an die musterbasierten APIs, wenn Sie vorhanden sind, und bevorzugt diese
für die Verwendung der-Schnittstelle (das Muster ist möglicherweise mit Instanzmethoden oder Erweiterungs
Methoden erfüllt). Folgende Anforderungen gelten für das Muster:
Das Aufzähl Bare Element muss eine GetAsyncEnumerator Methode verfügbar machen, die ohne Argumente
aufgerufen werden kann und einen Enumerator zurückgibt, der das relevante Muster erfüllt.
Der Enumerator muss eine Methode verfügbar machen MoveNextAsync , die ohne Argumente aufgerufen
werden kann, und die einen Wert zurückgibt, der möglicherweise await ein-und GetResult() zurückgibt
bool .
Der Enumerator muss auch eine Eigenschaft verfügbar machen, Current deren Getter einen zurückgibt T ,
der die Art der aufzuzählenden Daten darstellt.
Der Enumerator kann optional eine Methode verfügbar machen DisposeAsync , die ohne Argumente
aufgerufen werden kann, und die einen Wert zurückgibt, der ggf await . ein-und GetResult() zurückgibt
void .

Mit diesem Code wird Folgendes durchgeführt:


var enumerable = ...;
await foreach (T item in enumerable)
{
...
}

wird in die-Entsprechung von übersetzt:

var enumerable = ...;


var enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
T item = enumerator.Current;
...
}
}
finally
{
await enumerator.DisposeAsync(); // omitted, along with the try/finally, if the enumerator doesn't
expose DisposeAsync
}

Wenn der Iterierte Typ das richtige Muster nicht verfügbar macht, werden die Schnittstellen verwendet.
"Configureawait"
Diese Musterbasierte Kompilierung ermöglicht ConfigureAwait die Verwendung in allen Vorgängen über eine
ConfigureAwait Erweiterungsmethode:

await foreach (T item in enumerable.ConfigureAwait(false))


{
...
}

Dies basiert auf den Typen, die wir .net ebenfalls hinzufügen, wahrscheinlich
System.Threading.Tasks.Extensions.dll:
// Approximate implementation, omitting arg validation and the like
namespace System.Threading.Tasks
{
public static class AsyncEnumerableExtensions
{
public static ConfiguredAsyncEnumerable<T> ConfigureAwait<T>(this IAsyncEnumerable<T> enumerable,
bool continueOnCapturedContext) =>
new ConfiguredAsyncEnumerable<T>(enumerable, continueOnCapturedContext);

public struct ConfiguredAsyncEnumerable<T>


{
private readonly IAsyncEnumerable<T> _enumerable;
private readonly bool _continueOnCapturedContext;

internal ConfiguredAsyncEnumerable(IAsyncEnumerable<T> enumerable, bool


continueOnCapturedContext)
{
_enumerable = enumerable;
_continueOnCapturedContext = continueOnCapturedContext;
}

public ConfiguredAsyncEnumerator<T> GetAsyncEnumerator() =>


new ConfiguredAsyncEnumerator<T>(_enumerable.GetAsyncEnumerator(),
_continueOnCapturedContext);

public struct Enumerator


{
private readonly IAsyncEnumerator<T> _enumerator;
private readonly bool _continueOnCapturedContext;

internal Enumerator(IAsyncEnumerator<T> enumerator, bool continueOnCapturedContext)


{
_enumerator = enumerator;
_continueOnCapturedContext = continueOnCapturedContext;
}

public ConfiguredValueTaskAwaitable<bool> MoveNextAsync() =>


_enumerator.MoveNextAsync().ConfigureAwait(_continueOnCapturedContext);

public T Current => _enumerator.Current;

public ConfiguredValueTaskAwaitable DisposeAsync() =>


_enumerator.DisposeAsync().ConfigureAwait(_continueOnCapturedContext);
}
}
}
}

Beachten Sie, dass dieser Ansatz nicht ConfigureAwait für die Verwendung mit musterbasierten Enumerables
verwendet werden kann. aber auch hier ist es schon immer der Fall, dass das ConfigureAwait nur als
Erweiterung von verfügbar gemacht wird Task / Task<T> / ValueTask / ValueTask<T> und nicht auf
willkürliche, nicht aufnutzbare Dinge angewendet werden kann, da es nur bei der Anwendung auf Tasks sinnvoll
ist (es steuert ein Verhalten, das in der Fortsetzungs Unterstützung der Aufgabe implementiert ist) Jede Person,
die überwachte Dinge zurückgibt, kann Ihr eigenes benutzerdefiniertes Verhalten in solchen erweiterten
Szenarien bereitstellen
(Wenn es eine Möglichkeit gibt, eine Lösung auf Bereichs-oder Assemblyebene zu unterstützen ConfigureAwait
, ist dies nicht erforderlich.)

Async-Iteratoren
Die Sprache/der Compiler unterstützt die Erstellung von IAsyncEnumerable<T> s und IAsyncEnumerator<T> s
zusätzlich zur Nutzung. Heute unterstützt die Sprache das Schreiben eines Iterators wie:
static IEnumerable<int> MyIterator()
{
try
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1000);
yield return i;
}
}
finally
{
Thread.Sleep(200);
Console.WriteLine("finally");
}
}

await kann jedoch nicht im Text dieser Iteratoren verwendet werden. Wir werden diese Unterstützung
hinzufügen.
Syntax
Die vorhandene Sprachunterstützung für Iteratoren leitet die iteratorart der Methode ab, je nachdem, ob Sie
beliebige yield s enthält. Das gleiche gilt für Async-Iteratoren. Solche Async-Iteratoren werden durch das
Hinzufügen zur Signatur abgegrenzt und unterscheiden sich von synchronen Iteratoren async . Außerdem
muss entweder IAsyncEnumerable<T> oder IAsyncEnumerator<T> als Rückgabetyp aufweisen. Das obige Beispiel
könnte z. b. wie folgt als Async-Iterator geschrieben werden:

static async IAsyncEnumerable<int> MyIterator()


{
try
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(1000);
yield return i;
}
}
finally
{
await Task.Delay(200);
Console.WriteLine("finally");
}
}

Berücksichtigte Alternativen:
Nichtverwendung von async in der Signatur: async die Verwendung von ist für den Compiler
wahrscheinlich technisch erforderlich, da er verwendet wird, um zu bestimmen, ob await in diesem Kontext
gültig ist. Aber auch wenn dies nicht erforderlich ist, haben wir festgestellt, dass await nur in Methoden
verwendet werden kann async , die als gekennzeichnet sind, und dass es wichtig ist, die Konsistenz zu
wahren.
Aktivieren von benutzerdefinierten IAsyncEnumerable<T> Generatoren für: das ist etwas, das wir uns für die
Zukunft ansehen könnten, aber die Technik ist kompliziert, und wir unterstützen das nicht für die synchronen
Gegenstücke.
Das Verwenden eines iterator Schlüssel Worts in der Signatur: Async-Iteratoren verwenden
async iterator in der Signatur und yield könnten nur in Methoden verwendet werden, async die
enthalten iterator iterator . Anschließend werden Sie bei synchronen Iteratoren optional gemacht.
Abhängig von ihrer Perspektive hat dies den Vorteil, dass es von der Signatur der Methode ganz klar ist, ob
yield zulässig ist und ob die Methode tatsächlich IAsyncEnumerable<T> anstelle der compilerfertigung eine
Instanz des Typs zurückgeben soll yield . Sie unterscheidet sich jedoch von synchronen Iteratoren, die nicht
für eine Anforderung erforderlich sind. Außerdem sind einige Entwickler nicht der zusätzlichen Syntax
gefallen. Wenn wir Sie von Grund auf neu entwerfen, wäre dies wahrscheinlich erforderlich, aber an diesem
Punkt gibt es noch viel mehr Wert, wenn Sie asynchrone Iteratoren in der Nähe der Synchronisierungs
Iteratoren halten.

LINQ
Es gibt über ~ 200 über Ladungen von Methoden für die- System.Linq.Enumerable Klasse, die alle im Hinblick
auf funktionieren IEnumerable<T> . einige dieser Bedingungen, von denen einige diese akzeptieren,
IEnumerable<T> IEnumerable<T> und viele führen beides aus. Das Hinzufügen von LINQ-Unterstützung für
IAsyncEnumerable<T> würde wahrscheinlich dazu führen, dass alle diese über Ladungen für das Element
duplizieren, bei einem anderen ~ 200. Da IAsyncEnumerator<T> wahrscheinlich häufiger als eigenständige Entität
in der asynchronen Welt IEnumerator<T> ist, als Sie in der synchronen Welt ist, könnten wir möglicherweise
weitere ~ 200-über Ladungen benötigen, die mit funktionieren IAsyncEnumerator<T> . Eine große Anzahl von
über Ladungen behandelt außerdem Prädikate (z. b. Where , die einen Func<T, bool> -Wert annimmt), und es
kann wünschenswert sein, über IAsyncEnumerable<T> -basierte über Ladungen zu verfügen, die sowohl
synchrone als auch asynchrone Prädikate (z. b. zusätzlich Func<T, ValueTask<bool>> zu Func<T, bool> )
behandeln. Dies gilt zwar nicht für alle jetzt ~ 400 neuen über Ladungen, aber eine grobe Berechnung ist, dass
Sie auf die Hälfte anwendbar ist, was eine weitere ~ 200-Überladung bedeutet, um insgesamt ~ 600 neue
Methoden zu erhalten.
Das ist eine unglaubliche Anzahl von APIs, mit der Möglichkeit, noch mehr zu tun, wenn
Erweiterungsbibliotheken wie interaktive Erweiterungen (IX) berücksichtigt werden. IX verfügt aber bereits über
eine Implementierung vieler dieser Vorgänge, und es scheint keinen großen Grund dafür zu geben, die Arbeit zu
duplizieren. Wir sollten die Community stattdessen dabei unterstützen, IX zu verbessern und für den Fall zu
empfehlen, dass Entwickler LINQ mit verwenden möchten IAsyncEnumerable<T> .
Es gibt auch das Problem der Abfrage Verständnis Syntax. Aufgrund der musterbasierten Natur von Abfrage
Vorgängen können Sie mit einigen Operatoren "einfach arbeiten", z. b. wenn IX die folgenden Methoden
bereitstellt:

public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source,


Func<TSource, TResult> func);
public static IAsyncEnumerable<T> Where(this IAsyncEnumerable<T> source, Func<T, bool> func);

Anschließend wird dieser c#-Code "just work":

IAsyncEnumerable<int> enumerable = ...;


IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select item * 2;

Es gibt jedoch keine Abfrage Verständnis Syntax, die die Verwendung von in den-Klauseln unterstützt, d. h.,
await Wenn IX hinzugefügt wurde, z.b.:

public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source,


Func<TSource, ValueTask<TResult>> func);

dann würde dies "just work" lauten:


IAsyncEnumerable<string> result = from url in urls
where item % 2 == 0
select SomeAsyncMethod(item);

async ValueTask<int> SomeAsyncMethod(int item)


{
await Task.Yield();
return item * 2;
}

Es gibt jedoch keine Möglichkeit, Sie mit dem await Inline in der-Klausel zu schreiben select . Als separater
Aufwand könnten wir uns mit dem Hinzufügen von async { ... } Ausdrücken zu der Sprache befassen. zu
diesem Zeitpunkt könnten wir die Verwendung in den Abfrage Begriffen erlauben, und die oben genannten
könnte stattdessen wie folgt geschrieben werden:

IAsyncEnumerable<int> result = from item in enumerable


where item % 2 == 0
select async
{
await Task.Yield();
return item * 2;
};

oder, um await die direkte Verwendung in Ausdrücken zu ermöglichen, z. b. durch Unterstützung von
async from . Es ist jedoch unwahrscheinlich, dass sich hier der Rest der featuremenge auf eine oder andere
Weise auswirkt, und dies ist keine besonders hohe Bedeutung, um sofort zu investieren. der Vorschlag ist hier,
hier nichts weiter zu tun.

Integration in andere asynchrone Frameworks


Die Integration in IObservable<T> und andere asynchrone Frameworks (z. b. reaktive Streams) erfolgt auf
Bibliotheks Ebene und nicht auf Sprachebene. Beispielsweise können alle Daten aus IAsyncEnumerator<T> in
einem veröffentlicht werden, IObserver<T> indem Sie await foreach den Enumerator überspringen und
OnNext die Daten an den Beobachter übergeben, sodass eine AsObservable<T> Erweiterungsmethode möglich
ist. Wenn IObservable<T> Sie einen in einem verwenden await foreach , müssen die Daten gepuffert werden
(für den Fall, dass ein anderes Element per Push übertragen wird, während das vorherige Element noch
verarbeitet wird). ein solcher Push-Pull-Adapter kann jedoch problemlos implementiert werden, um zu
ermöglichen, dass ein IObservable<T> von mit einem IAsyncEnumerator<T> Etc. RX/IX stellt bereits Prototypen
solcher Implementierungen bereit, und Bibliotheken wie
https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels stellen verschiedene Arten von
Pufferung von Datenstrukturen bereit. Die Sprache muss an dieser Phase nicht beteiligt sein.
Bereiche
04.11.2021 • 22 minutes to read

Zusammenfassung
Bei diesem Feature geht es um die Bereitstellung von zwei neuen Operatoren, die das Erstellen von - und -
Objekten ermöglichen, und deren Verwendung zum System.Index System.Range Indizieren/Segmentieren von
Auflistungen zur Laufzeit.

Übersicht
Bekannte Typen und Member
Um die neuen syntaktischen Formulare für und zu verwenden, sind möglicherweise neue bekannte Typen und
Member erforderlich, je nachdem, welche System.Index System.Range syntaktischen Formulare verwendet
werden.
Um den Hat-Operator () zu ^ verwenden, ist Folgendes erforderlich:

namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}

Um den Typ System.Index als Argument in einem Arrayelementzugriff zu verwenden, ist der folgende Member
erforderlich:

int System.Index.GetOffset(int length);

Die Syntax für erfordert den -Typ sowie mindestens einen .. der folgenden System.Range System.Range
Member:

namespace System
{
public readonly struct Range
{
public Range(System.Index start, System.Index end);
public static Range StartAt(System.Index start);
public static Range EndAt(System.Index end);
public static Range All { get; }
}
}

Die Syntax lässt zu, dass entweder .. beide oder keines der Argumente fehlt. Unabhängig von der Anzahl der
Argumente ist der Konstruktor immer ausreichend Range für die Verwendung der Range Syntax. Wenn jedoch
eines der anderen Member vorhanden ist und mindestens eines der Argumente fehlt, kann das entsprechende
.. Member ersetzt werden.

Damit schließlich ein Wert vom Typ in einem Arrayelementzugriffsausdruck verwendet werden kann, muss
System.Range der folgende Member vorhanden sein:

namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}

System.Index
C# bietet keine Möglichkeit, eine Sammlung vom Ende aus zu indizieren, sondern die meisten Indexer
verwenden das Konzept "von Anfang" oder einen "length - i"-Ausdruck. Wir führen einen neuen Indexausdruck
ein, der "from the end" bedeutet. Das Feature führt einen neuen unären "hat"-Präfixoperator ein. Der einzelne
Operand muss in konvertierbar System.Int32 sein. Sie wird in den entsprechenden System.Index
Factorymethodeaufruf gesenkt.
Die Grammatik für die Unary_expression wird mit der folgenden zusätzlichen Syntaxform unterstützt:

unary_expression
: '^' unary_expression
;

Dies wird als Index vom Endoperator bezeichnet. Die vordefinierten Index von Endoperatoren lauten wie folgt:

System.Index operator ^(int fromEnd);

Das Verhalten dieses Operators wird nur für Eingabewerte definiert, die größer oder gleich 0 (null) sind.
Beispiele:

var array = new int[] { 1, 2, 3, 4, 5 };


var thirdItem = array[2]; // array[2]
var lastItem = array[^1]; // array[new Index(1, fromEnd: true)]

System.Range
C# bietet keine syntaktische Möglichkeit, auf "Bereiche" oder "Slices" von Auflistungen zu zugreifen. In der Regel
werden Benutzer gezwungen, komplexe Strukturen zum Filtern/Verarbeiten von Slices des Arbeitsspeichers zu
implementieren oder auf LINQ-Methoden wie zurück zu list.Skip(5).Take(2) setzen. Durch das Addition von
und anderen ähnlichen Typen wird es wichtiger, diese Art von Vorgang auf einer tieferen Ebene in der
Sprache/Runtime zu unterstützen und die Schnittstelle System.Span<T> zu vereinheitlichen.
Die Sprache führt einen neuen Bereichsoperator x..y ein. Es handelt sich um einen binären Infixoperator, der
zwei Ausdrücke akzeptiert. Beide Operanden können weggelassen werden (Beispiele unten), und sie müssen in
konvertierbar System.Index sein. Sie wird auf den entsprechenden Aufruf der System.Range Factorymethode
gesenkt.
Wir ersetzen die C#-Grammatikregeln für multiplicative_expression durch Folgendes (um eine neue
Rangfolgeebene einzuführen):
range_expression
: unary_expression
| range_expression? '..' range_expression?
;

multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;

Alle Formen des Bereichsoperator haben die gleiche Rangfolge. Diese neue Rangfolgegruppe ist niedriger als
die unären Operatoren und höher als die multiplikativen arithmetischen Operatoren.
Wir nennen den .. Operator den Bereichsoperator. Der integrierte Bereichsoperator kann ungefähr so
verstanden werden, dass er dem Aufruf eines integrierten Operators in dieser Form entspricht:

System.Range operator ..(Index start = 0, Index end = ^0);

Beispiele:

var array = new int[] { 1, 2, 3, 4, 5 };


var slice1 = array[2..^3]; // array[new Range(2, new Index(3, fromEnd: true))]
var slice2 = array[..^3]; // array[Range.EndAt(new Index(3, fromEnd: true))]
var slice3 = array[2..]; // array[Range.StartAt(2)]
var slice4 = array[..]; // array[Range.All]

Darüber hinaus sollte eine implizite Konvertierung von haben, um zu vermeiden, dass das Mischen von ganzen
Zahlen und Indizes über mehrdimensionale System.Index System.Int32 Signaturen überladen werden muss.

Hinzufügen von Index- und Bereichsunterstützung zu vorhandenen


Bibliothekstypen
Implizite Indexunterstützung
Die Sprache stellt einem Instanzindexer-Member einen einzelnen Parameter vom Typ für Typen zur Index
Verfügung, die die folgenden Kriterien erfüllen:
Der Typ ist Countable.
Der Typ verfügt über einen zugänglichen Instanzindexer, der ein einzelnes int als Argument verwendet.
Der Typ verfügt nicht über einen zugänglichen Instanzindexer, der als Index ersten Parameter akzeptiert.
Muss Index der einzige Parameter sein, oder die verbleibenden Parameter müssen optional sein.

Ein Typ ist Countable, wenn er über eine Eigenschaft namens oder mit einem Length zugänglichen Getter und
dem Count Rückgabetyp int verfügt. Die Sprache kann diese Eigenschaft verwenden, um einen Ausdruck
vom Typ in einen am Punkt des Ausdrucks zu konvertieren, ohne den Typ Index int überhaupt verwenden zu
Index müssen. Für den Length Fall, dass Count sowohl als auch vorhanden sind, wird Length bevorzugt. Der
Einfachheit halber wird der Vorschlag in Zukunft den Namen verwenden, Length um oder Count zu Length
repräsentieren.
Bei solchen Typen wird die Sprache so verwendet, als ob ein Indexerelement des Formulars vorkehrt, wobei der
Rückgabetyp des basierten Indexers einschließlich aller Stilanmerkungen T this[Index index] T int ref ist.
Das neue Member hat die gleichen Member get und set mit übereinstimmenden Barrierefreiheiten wie int
der Indexer.
Der neue Indexer wird implementiert, indem das Argument vom Typ in einen konvertiert und ein Aufruf an den
basierten Index int int Indexer ausgegeben wird. Zu Diskussionszwecken verwenden wir das Beispiel
receiver[expr] von . Die Konvertierung von expr in int erfolgt wie folgt:

Wenn das Argument das -Formular ^expr2 hat und der Typ von expr2 int ist, wird es in
receiver.Length - expr2 übersetzt.
Andernfalls wird sie als expr.GetOffset(receiver.Length) übersetzt.
Dadurch können Entwickler das Feature für vorhandene Index Typen verwenden, ohne dass Änderungen
vorgenommen werden müssen. Beispiel:

List<char> list = ...;


var value = list[^1];

// Gets translated to
var value = list[list.Count - 1];

Die receiver Ausdrücke und werden entsprechend überlauft, um sicherzustellen, dass alle Nebeneffekte
Length nur einmal ausgeführt werden. Beispiel:

class Collection {
private int[] _array = new[] { 1, 2, 3 };

public int Length {


get {
Console.Write("Length ");
return _array.Length;
}
}

public int this[int index] => _array[index];


}

class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}

void Use() {
int i = Get()[^1];
Console.WriteLine(i);
}
}

Dieser Code gibt "Get Length 3" (Länge 3 erhalten) aus.


Implizite Bereichsunterstützung
Die Sprache stellt einem Instanzindexer-Member einen einzelnen Parameter vom Typ für Typen zur Range
Verfügung, die die folgenden Kriterien erfüllen:
Der Typ ist Countable.
Der Typ verfügt über einen zugänglichen Member mit Slice dem Namen , der über zwei Parameter vom
Typ int verfügt.
Der Typ verfügt nicht über einen Instanzindexer, der einen einzelnen als Range ersten Parameter akzeptiert.
Muss Range der einzige Parameter sein, oder die verbleibenden Parameter müssen optional sein.
Bei solchen Typen wird die Sprache so gebunden, als ob ein Indexerelement des Formulars vorkehrt, wobei der
Rückgabetyp der Methode einschließlich aller T this[Range range] T Slice ref Stilanmerkungen ist. Der
neue Member hat auch einen übereinstimmenden Zugriff auf Slice .
Wenn der basierte Indexer an einen Ausdruck namens gebunden ist, wird er durch Konvertieren des Ausdrucks
in zwei Werte gesenkt, die dann an die Range receiver Range -Methode übergeben Slice werden. Zu
Diskussionszwecken verwenden wir das Beispiel receiver[expr] von .
Das erste Argument von Slice wird durch Konvertieren des bereichstypierten Ausdrucks wie folgt ermittelt:
Wenn expr das -Formular auf hat (wobei weggelassen werden kann) und den Typ auf hat, wird
expr1..expr2 es als expr2 expr1 int expr1 ausgegeben.
Wenn expr das -Formular ^expr1..expr2 auf hat (wobei expr2 weggelassen werden kann), wird es als
receiver.Length - expr1 ausgegeben.
Wenn expr das -Formular ..expr2 auf hat (wobei expr2 weggelassen werden kann), wird es als 0
ausgegeben.
Andernfalls wird sie als expr.Start.GetOffset(receiver.Length) ausgegeben.

Dieser Wert wird bei der Berechnung des zweiten Arguments erneut Slice verwendet. Dabei wird sie als
start bezeichnet. Das zweite Argument von wird durch Konvertieren des bereichstypierten Slice Ausdrucks
wie folgt ermittelt:
Wenn expr das -Formular auf hat (wobei weggelassen werden kann) und den Typ auf hat, wird
expr1..expr2 es als expr1 expr2 int expr2 - start ausgegeben.
Wenn expr das -Formular expr1..^expr2 auf hat (wobei expr1 weggelassen werden kann), wird es als
(receiver.Length - expr2) - start ausgegeben.
Wenn expr das -Formular expr1.. auf hat (wobei expr1 weggelassen werden kann), wird es als
receiver.Length - start ausgegeben.
Andernfalls wird sie als expr.End.GetOffset(receiver.Length) - start ausgegeben.

Die Ausdrücke , und werden entsprechend überlauft, um sicherzustellen, dass alle Nebeneffekte receiver
Length nur einmal ausgeführt expr werden. Beispiel:
class Collection {
private int[] _array = new[] { 1, 2, 3 };

public int Length {


get {
Console.Write("Length ");
return _array.Length;
}
}

public int[] Slice(int start, int length) {


var slice = new int[length];
Array.Copy(_array, start, slice, 0, length);
return slice;
}
}

class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}

void Use() {
var array = Get()[0..2];
Console.WriteLine(array.Length);
}
}

Dieser Code gibt "Get Length 2" (Länge 2 erhalten) aus.


In der Sprache werden die folgenden bekannten Typen als Sonderfall verwendet:
string : Die Substring -Methode wird anstelle von Slice verwendet.
array : Die System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray -Methode wird anstelle von Slice
verwendet.

Alternativen
Die neuen Operatoren ( ^ .. und ) sind syntaktischer Sugar. Die Funktionalität kann durch explizite Aufrufe
von - und -Factorymethoden implementiert werden, führt jedoch zu viel mehr Codebausteinen, und die
Erfahrung ist System.Index System.Range nicht ungeeignet.

IL-Darstellung
Diese beiden Operatoren werden auf reguläre Indexer-/Methodenaufrufe gesenkt, ohne dass sich die
nachfolgenden Compilerebenen ändern.

Laufzeitverhalten
Der Compiler kann Indexer für integrierte Typen wie Arrays und Zeichenfolgen optimieren und die
Indizierung auf die entsprechenden vorhandenen Methoden senken.
System.Index löst aus, wenn mit einem negativen Wert erstellt wird.
^0 löst keine Ausnahme aus, wird aber in die Länge der Auflistung bzw. des Aufzählbaren übersetzt, für die
sie bereitgestellt wird.
Range.All entspricht semantisch , und kann in diese 0..^0 Indizes dekonstruiert werden.

Überlegungen
Erkennen von indizierbaren Daten basierend auf ICollection
Die Idee für dieses Verhalten waren Auflistungsinitialisierer. Verwenden der Struktur eines Typs, um zu
vermitteln, dass er sich für ein Feature entschieden hat. Im Fall von Auflistungsinitialisierern können Typen das
Feature durch Implementieren der IEnumerable -Schnittstelle (nicht generisch) verwenden.
Dieser Vorschlag erforderte zunächst, dass Typen implementiert ICollection werden, um als indexierbar zu
qualifizieren. Dies erforderte jedoch eine Reihe von Sonderfällen:
ref struct : Diese können keine Schnittstellen implementieren, aber Typen wie Span<T> eignen sich ideal
für die Index-/Bereichsunterstützung.
string : implementiert und fügt keine ICollection hinzu, die interface hohe Kosten hat.

Dies bedeutet, dass die Unterstützung von Schlüsseltypen bereits eine spezielle Groß-/Kleinschreibung
erfordert. Die spezielle Groß-/Kleinschreibung von string ist weniger interessant, da die Sprache dies in
anderen Bereichen foreach (Senken, Konstanten usw.) tut. Die spezielle Groß-/Kleinschreibung von ist wichtiger,
da es sich ref struct um eine spezielle Groß-/Kleinschreibung für eine ganze Klasse von Typen handelt. Sie
werden als Indexable bezeichnet, wenn sie einfach über eine Eigenschaft Count namens mit dem Rückgabetyp
int verfügen.

Nach Überlegungen wurde der Entwurf normalisiert, um zu sagen, dass jeder Typ, der über eine Eigenschaft
Count / Length mit einem Rückgabetyp von int verfügt, indexierbar ist. Dadurch werden alle speziellen
Groß-/Kleinschreibungen entfernt, auch für string -Arrays und -Arrays.
Nur Anzahl erkennen
Das Erkennen der Eigenschaftennamen Count oder erschwert den Entwurf Length etwas. Es reicht jedoch nicht
aus, nur einen zu standardisieren, da eine große Anzahl von Typen ausgeschlossen wird:
Verwenden Sie Length : schließt so ziemlich jede Sammlung in System.Collections und unteren Namespaces
aus. Diese leiten sich tendenziell von ab ICollection und bevorzugen daher die Count Länge.
Verwenden Sie Count : schließt string , Arrays und die meisten Span<T> ref struct basierten Typen aus.

Die zusätzliche Komplizierung bei der anfänglichen Erkennung indexierbarer Typen wird durch die
Vereinfachung in anderen Aspekten aufwiegen.
Auswahl von Slice als Name
Der Name wurde ausgewählt, da es sich um Slice den De-facto-Standardnamen für Sliceformatvorgänge in
.NET handelt. Ab netcoreapp2.1 verwenden alle Span-Stiltypen den Namen Slice für Aufschnittvorgänge. Vor
netcoreapp2.1 gibt es tatsächlich keine Beispiele für Daslicing, nach einem Beispiel zu suchen. Typen wie , und
wären ideal für das Aufschneiden von Daten List<T> ArraySegment<T> SortedList<T> gewesen, aber das
Konzept existierte nicht, als Typen hinzugefügt wurden.
Daher Slice wurde es als einziges Beispiel als Name ausgewählt.
Konvertierung des Indexzieltyps
Eine weitere Möglichkeit zum Anzeigen der Index Transformation in einem Indexerausdruck ist eine
Zieltypkonvertierung. Anstatt zu binden, als ob ein Member des Formulars vorhanden return_type this[Index]
wäre, weist die Sprache stattdessen eine typisierte Zielkonvertierung zu int zu.
Dieses Konzept kann für den gesamten Memberzugriff auf zählbare Typen generalisiert werden. Wenn ein
Ausdruck vom Typ Index als Argument für den Aufruf eines Instanzmembers verwendet wird und der
Empfänger Countable ist, verfügt der Ausdruck über eine Zieltypkonvertierung in int . Zu den
Memberaufrufen, die für diese Konvertierung gelten, gehören Methoden, Indexer, Eigenschaften,
Erweiterungsmethoden usw. Nur Konstruktoren werden ausgeschlossen, da sie keinen Empfänger haben.
Die Zieltypkonvertierung wird wie folgt für jeden Ausdruck implementiert, der über den Typ Index verfügt. Zu
Diskussionszwecken verwenden wir das Beispiel von receiver[expr] :
Wenn expr das Format hat und der Typ von ^expr2 expr2 int ist, wird er in receiver.Length - expr2
übersetzt.
Andernfalls wird sie als expr.GetOffset(receiver.Length) übersetzt.

Die receiver Length Ausdrücke und werden nach Bedarf überschwappt, um sicherzustellen, dass alle
Nebeneffekte nur einmal ausgeführt werden. Beispiel:

class Collection {
private int[] _array = new[] { 1, 2, 3 };

public int Length {


get {
Console.Write("Length ");
return _array.Length;
}
}

public int GetAt(int index) => _array[index];


}

class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}

void Use() {
int i = Get().GetAt(^1);
Console.WriteLine(i);
}
}

Dieser Code gibt "Get Length 3" (Länge abrufen 3) aus.


Dieses Feature wäre für alle Member von Vorteil, die über einen Parameter verfügen, der einen Index darstellt.
Beispiel: List<T>.InsertAt . Dies kann auch verwirrend sein, da die Sprache keine Hinweise dazu geben kann, ob
ein Ausdruck für die Indizierung vorgesehen ist oder nicht. Sie können nur einen Index beliebigen Ausdruck
int in konvertieren, wenn sie einen Member für einen Zählbaren Typ aufrufen.

Einschränkungen:
Diese Konvertierung ist nur anwendbar, wenn der Ausdruck mit dem Typ Index direkt ein Argument für den
Member ist. Dies gilt nicht für geschachtelte Ausdrücke.

Während der Implementierung getroffene Entscheidungen


Alle Member im Muster müssen Instanzmember sein.
Wenn eine Length-Methode gefunden wird, sie aber den falschen Rückgabetyp auf hat, suchen Sie weiter
nach Anzahl.
Der für das Indexmuster verwendete Indexer muss genau einen int-Parameter aufweisen.
Die für das Range-Muster verwendete Slice-Methode muss genau zwei int-Parameter aufweisen.
Bei der Suche nach den Mustermembern suchen wir nach ursprünglichen Definitionen, nicht nach
konstruierten Membern.

Entwerfen von Besprechungen


10. Januar 2018
18. Januar 2018
22. Januar 2018
3. Dezember 2018
25. März 2019
1. April 2019
April 15, 2019
"musterbasierte Verwendung" und "Verwenden von
Deklarationen"
04.11.2021 • 7 minutes to read

Zusammenfassung
Die Sprache fügt zwei neue Funktionen für die -Anweisung hinzu, um die Ressourcenverwaltung zu
vereinfachen: sollte zusätzlich zu ein verwerfbares Muster erkennen und der Sprache eine using using
IDisposable using Deklaration hinzufügen.

Motivation
Die using -Anweisung ist heute ein effektives Tool für die Ressourcenverwaltung, erfordert jedoch einiges
Ansenden. Methoden, die über eine Reihe von Zu verwaltenden Ressourcen verfügen, können mit einer Reihe
von Anweisungen syntaktisch heruntergefahren using werden. Diese Syntaxlast reicht aus, damit die meisten
Richtlinien für Codierungsstile explizit eine Ausnahme um geschweifte Klammern für dieses Szenario haben.
Die -Deklaration entfernt hier einen Großen Teil der Deklaration und gleicht C# anderen Sprachen using an, die
Ressourcenverwaltungsblöcke enthalten. Darüber hinaus können Entwickler mit dem musterbasierten -Muster
using den Satz von Typen erweitern, die hier teilnehmen können. In vielen Fällen ist es nicht mehr notwendig,
Wrappertypen zu erstellen, die nur vorhanden sind, um die Verwendung von Werten in einer -Anweisung zu
using ermöglichen.

Zusammen ermöglichen diese Features Entwicklern, die Szenarien zu vereinfachen und zu erweitern, in using
denen sie angewendet werden können.

Detaillierter Entwurf
using-Deklaration
Die Sprache ermöglicht es, using einer lokalen Variablendeklaration hinzugefügt zu werden. Eine solche
Deklaration hat die gleiche Wirkung wie das Deklarieren der Variablen in using einer -Anweisung am gleichen
Speicherort.

if (...)
{
using FileStream f = new FileStream(@"C:\users\jaredpar\using.md");
// statements
}

// Equivalent to
if (...)
{
using (FileStream f = new FileStream(@"C:\users\jaredpar\using.md"))
{
// statements
}
}

Die Lebensdauer eines lokalen using -Bereichs erstreckt sich bis zum Ende des Bereichs, in dem er deklariert
wird. Die using lokalen Daten werden dann in umgekehrter Reihenfolge verworfen, in der sie deklariert
werden.
{
using var f1 = new FileStream("...");
using var f2 = new FileStream("..."), f3 = new FileStream("...");
...
// Dispose f3
// Dispose f2
// Dispose f1
}

Es gibt keine Einschränkungen für goto , oder ein anderes Ablaufsteuerungskonstrukt bei einer using
Deklaration. Stattdessen verhält sich der Code wie bei der entsprechenden using -Anweisung:

{
using var f1 = new FileStream("...");
target:
using var f2 = new FileStream("...");
if (someCondition)
{
// Causes f2 to be disposed but has no effect on f1
goto target;
}
}

Ein lokal deklarierter in using einer lokalen Deklaration ist implizit schreibgeschützt. Dies entspricht dem
Verhalten von lokalen , die in einer -Anweisung deklariert using sind.
Die Sprachgrammatik using für Deklarationen ist wie folgt:

local-using-declaration:
using type using-declarators

using-declarators:
using-declarator
using-declarators , using-declarator

using-declarator:
identifier = expression

Einschränkungen bei der using Deklaration:


Darf nicht direkt in einer Bezeichnung angezeigt case werden, sondern muss sich innerhalb eines Blocks
innerhalb der case Bezeichnung befinden.
Wird möglicherweise nicht als Teil einer out Variablendeklaration angezeigt.
Für jeden Deklarator muss ein Initialisierer verwendet werden.
Der lokale Typ muss implizit in das Muster konvertierbar sein IDisposable oder dieses using erfüllen.
musterbasiert mit
Die Sprache fügt das Konzept eines verwerfbaren Musters hinzu: Dies ist ein Typ, der über eine zugängliche
Dispose Instanzmethode verfügt. Typen, die zum verwerfbare Muster passen, können an einer using
Anweisung oder Deklaration teilnehmen, ohne dass implementiert werden IDisposable muss.
class Resource
{
public void Dispose() { ... }
}

using (var r = new Resource())


{
// statements
}

Dies ermöglicht Entwicklern die Nutzung using in einer Reihe neuer Szenarien:
ref struct : Diese Typen können derzeit keine Schnittstellen implementieren und können daher nicht an
using -Anweisungen teilnehmen.
Erweiterungsmethoden ermöglichen Es Entwicklern, Typen in anderen Assemblys zu erweitern, um an
using -Anweisungen teilzunehmen.

In situationen, in denen ein Typ implizit in konvertiert werden kann IDisposable und auch dem
Verwerfungsmuster entspricht, IDisposable wird bevorzugt. Obwohl dies den umgekehrten Ansatz von
foreach (Muster bevorzugt gegenüber Schnittstelle) verfolgt, ist dies aus Gründen der Abwärtskompatibilität
erforderlich.
Die gleichen Einschränkungen wie bei einer herkömmlichen using Anweisung gelten auch hier: lokale
Variablen, die in deklariert using sind, sind schreibgeschützt, ein null Wert führt nicht dazu, dass eine
Ausnahme ausgelöst wird usw. Die Codegenerierung unterscheidet sich nur darin, dass vor dem Aufrufen von
Dispose keine Umwandlung in IDisposable erfolgt:

{
Resource r = new Resource();
try {
// statements
}
finally {
if (r != null) r.Dispose();
}
}

Damit sie dem verwerfbaren Muster entspricht, muss auf die Dispose Methode zugegriffen werden können, sie
muss parameterlos sein und einen void Rückgabetyp aufweisen. Es gibt keine weiteren Einschränkungen. Dies
bedeutet explizit, dass Erweiterungsmethoden hier verwendet werden können.

Überlegungen
Case -Bezeichnungen ohne Blöcke
Ein using declaration ist direkt innerhalb einer Bezeichnung aufgrund von Schwierigkeiten um seine
tatsächliche Lebensdauer case unzulässig. Eine mögliche Lösung besteht darin, ihm einfach die gleiche
Lebensdauer wie ein an demselben Standort zu out var gewähren. Dies wurde als zusätzliche Komplexität der
Featureimplementierung angesehen, und die einfache Arbeit um (fügen Sie einfach der Bezeichnung einen Block
hinzu) hat es nicht rechtfertigen, diese case Route zu verwenden.

Zukünftige Erweiterungen
Feste lokale Daten
Eine fixed -Anweisung verfügt über alle Eigenschaften von -Anweisungen, die die Fähigkeit zum using
Verwenden lokaler using -Objekte motiviert haben. Es sollte auch die Erweiterung dieses Features auf fixed
lokale Lokale berücksichtigt werden. Die Lebensdauer- und Reihenfolgenregeln sollten für und hier using
gleichermaßen fixed gut gelten.
Statische lokale Funktionen
04.11.2021 • 2 minutes to read

Zusammenfassung
Unterstützung lokaler Funktionen, die das Erfassen des Zustands aus dem einschließenden Bereich nicht
zulassen.

Motivation
Vermeiden Sie unbeabsichtigt das Erfassen des Zustands aus dem einschließenden Kontext. Ermöglicht die
Verwendung lokaler Funktionen in Szenarien, in denen eine static Methode erforderlich ist.

Detaillierter Entwurf
Eine lokale Funktion, die deklariert wurde, static kann den Zustand nicht aus dem einschließenden Bereich
erfassen Demzufolge sind lokale, Parameter und this aus dem einschließenden Bereich in einer lokalen
Funktion nicht verfügbar static .
Eine staticlokale Funktion kann nicht auf Instanzmember von einem impliziten oder expliziten this oder
base Verweis verweisen.
Eine static lokale Funktion kann auf Member static aus dem einschließenden Bereich verweisen.
Eine static lokale Funktion kann auf constant Definitionen aus dem einschließenden Bereich verweisen.
nameof() in einer static lokalen Funktion können auf lokale, Parameter oder this oder base aus dem
einschließenden Bereich verwiesen werden.
Zugriffsregeln für Elemente private im einschließenden Bereich sind für static und nicht static lokale
Funktionen identisch.
Eine static lokale Funktionsdefinition wird als static Methode in den Metadaten ausgegeben, auch wenn Sie
nur in einem Delegaten verwendet wird.
Eine nicht static lokale Funktion oder ein Lambda-Ausdruck kann den Zustand von einer einschließenden
static lokalen Funktion erfassen, aber keinen Zustand außerhalb der einschließenden static lokalen
Funktion erfassen.
Eine static lokale Funktion kann nicht in einer Ausdrucks Baumstruktur aufgerufen werden.
Ein-Aufrufe an eine lokale Funktion wird call anstelle von ausgegeben callvirt , unabhängig davon, ob die
lokale Funktion ist static .
Die Überladungs Auflösung eines Aufrufes in einer lokalen Funktion ist nicht davon betroffen, ob die lokale
Funktion ist static .
Wenn Sie den- static Modifizierer aus einer lokalen Funktion in einem gültigen Programm entfernen, wird die
Bedeutung des Programms nicht geändert.

Treffen von Besprechungen


https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-09-10.md#static-local-functions
Koaleszierende NULL-Zuweisung
04.11.2021 • 4 minutes to read

[x] vorgeschlagen
[x] Prototyp: abgeschlossen
[x] Implementierung: abgeschlossen
[x] Spezifikation: unten

Zusammenfassung
Vereinfacht ein gängiges Codierungs Muster, bei dem einer Variablen ein Wert zugewiesen wird, wenn Sie NULL
ist.
Im Rahmen dieses Angebots lockern wir auch die typanforderungen auf ?? , damit ein Ausdruck, dessen Typ
ein uneingeschränkter Typparameter ist, auf der linken Seite verwendet werden kann.

Motivation
Es kommt häufig vor, dass Code des Formulars angezeigt wird.

if (variable == null)
{
variable = expression;
}

In diesem Vorschlag wird der Sprache, die diese Funktion ausführt, ein nicht über ladbarer binärer Operator
hinzugefügt.
Für dieses Feature gab es mindestens acht communityanforderungen.

Detaillierter Entwurf
Wir fügen eine neue Form von Zuweisungs Operatoren hinzu.

assignment_operator
: '??='
;

Der den vorhandenen Semantik Regeln für Verbund Zuweisungs Operatorenfolgt, mit dem Unterschied, dass
die Zuweisung entfernt wird, wenn die linke Seite nicht NULL ist. Die Regeln für dieses Feature lauten wie folgt.
Gibt an a ??= b , wobei A der Typ von a , B der Typ von b und A0 der zugrunde liegende Typ von ist, A
Wenn A ein Werte zulässt-Werttyp ist:
1. Wenn A nicht vorhanden ist oder ein nicht auf NULL festleg barer Werttyp ist, tritt ein Kompilierzeitfehler
auf.
2. Wenn B nicht implizit in A oder A0 (falls A0 vorhanden) konvertiert werden kann, tritt ein
Kompilierzeitfehler auf.
3. Wenn A0 vorhanden und B implizit in konvertierbar ist A0 und B nicht dynamisch ist, ist der Typ von
a ??= b A0 . a ??= b wird zur Laufzeit wie folgt ausgewertet:
var tmp = a.GetValueOrDefault();
if (!a.HasValue) { tmp = b; a = tmp; }
tmp

Mit der Ausnahme, dass a nur einmal ausgewertet wird.


4. Andernfalls ist der Typ von a ??= b A . a ??= b wird zur Laufzeit als ausgewertet a ?? (a = b) , mit dem
Unterschied, dass a nur einmal ausgewertet wird.
Um die typanforderungen von zu erfüllen ?? , aktualisieren wir die Spezifikation, in der Sie aktuell angibt,
a ?? b wo A der Typ von ist a :

1. Wenn ein vorhanden ist und kein Werte zulässt-Typ oder Verweistyp ist, tritt ein Kompilierzeitfehler auf.

Wir lockern diese Anforderung für Folgendes:


1. Wenn eine vorhanden ist und ein Werttyp ist, der keine NULL-Werte zulässt, tritt ein Kompilierzeitfehler auf.
Dadurch kann der NULL-Sammel Operator an nicht eingeschränkten Typparametern arbeiten, da der nicht
eingeschränkte Typparameter T vorhanden ist, kein Werte zulässt-Typ ist und kein Referenztyp ist.

Nachteile
Wie bei allen Sprach Features müssen wir Fragen, ob die zusätzliche Komplexität der Sprache in der zusätzlichen
Klarheit für den Text der c#-Programme, die von der Funktion profitieren würden, zurückgegeben wird.

Alternativen
Der Programmierer kann (x = x ?? y) , if (x == null) x = y; oder x ?? (x = y) per Hand schreiben.

Nicht aufgelöste Fragen


[] Erfordert eine LDM-Überprüfung
[] Sollten auch &&= -und- ||= Operatoren unterstützt werden?

Treffen von Besprechungen


Keine.
Schreibgeschützte Instanzmember
04.11.2021 • 8 minutes to read

Problem behoben: https://github.com/dotnet/csharplang/issues/1710

Zusammenfassung
Stellen Sie eine Möglichkeit bereit, einzelne Instanzmember für eine Struktur anzugeben, die den Zustand nicht
ändern, und zwar auf die gleiche Weise, die angibt, dass keine Instanzmember den readonly struct Zustand
ändern.
Es ist zu bedenken, dass readonly instance member != pure instance member ist. Ein pure Instanzmember
garantiert, dass kein Zustand geändert wird. Ein readonly Instanzmember garantiert nur, dass der
Instanzzustand nicht geändert wird.
Alle Instanzmember in einem readonly struct können implizit als betrachtet readonly instance members
werden. Explizit readonly instance members deklarierte nicht schreibgeschützte Strukturen verhalten sich auf die
gleiche Weise. Sie würden beispielsweise weiterhin ausgeblendete Kopien erstellen, wenn Sie einen
Instanzmember (in der aktuellen Instanz oder in einem Feld der Instanz) aufgerufen haben, der selbst nicht
schreibgeschützt war.

Motivation
Heutzutage haben Benutzer die Möglichkeit, Typen zu readonly struct erstellen, die der Compiler erzwingt,
dass alle Felder schreibgeschützter Art sind (und durch Erweiterung, dass keine Instanzmember den Zustand
ändern). Es gibt jedoch einige Szenarien, in denen Sie über eine vorhandene API verfügen, die zugängliche
Felder verfügbar macht oder eine Mischung aus mutierenden und nicht mutierenden Membern enthält. Unter
diesen Umständen können Sie den Typ nicht als markieren readonly (dies wäre eine Breaking Change).
Dies hat normalerweise keine großen Auswirkungen, außer bei in Parametern. Bei in Parametern für nicht
schreibgeschützte Strukturen erstellt der Compiler eine Kopie des Parameters für jeden Instanzmemberaufruf,
da er nicht garantieren kann, dass der interne Zustand durch den Aufruf nicht geändert wird. Dies kann zu einer
Vielzahl von Kopien und einer schlechteren Gesamtleistung führen, als wenn Sie die Struktur direkt nach Wert
übergeben hätten. Ein Beispiel finden Sie in diesem Code auf sharplab.
Einige andere Szenarien, in denen ausgeblendete Kopien auftreten können, sind static readonly fields und
literals . Wenn sie in Zukunft unterstützt werden, blittable constants würde im selben Konstrukt enden. Das
heißt, sie alle erfordern derzeit eine vollständige Kopie (beim Aufruf von Instanzmembern), wenn die Struktur
nicht als markiert readonly ist.

Entwurf
Erlauben Sie einem Benutzer anzugeben, dass ein Instanzmitglied selbst ist und den Zustand der Instanz nicht
ändert (natürlich mit der entsprechenden Überprüfung readonly durch den Compiler). Zum Beispiel:
public struct Vector2
{
public float x;
public float y;

public readonly float GetLengthReadonly()


{
return MathF.Sqrt(LengthSquared);
}

public float GetLength()


{
return MathF.Sqrt(LengthSquared);
}

public readonly float GetLengthIllegal()


{
var tmp = MathF.Sqrt(LengthSquared);

x = tmp; // Compiler error, cannot write x


y = tmp; // Compiler error, cannot write y

return tmp;
}

public readonly float LengthSquared


{
get
{
return (x * x) +
(y * y);
}
}
}

public static class MyClass


{
public static float ExistingBehavior(in Vector2 vector)
{
// This code causes a hidden copy, the compiler effectively emits:
// var tmpVector = vector;
// return tmpVector.GetLength();
//
// This is done because the compiler doesn't know that `GetLength()`
// won't mutate `vector`.

return vector.GetLength();
}

public static float ReadonlyBehavior(in Vector2 vector)


{
// This code is emitted exactly as listed. There are no hidden
// copies as the `readonly` modifier indicates that the method
// won't mutate `vector`.

return vector.GetLengthReadonly();
}
}

Readonly kann auf Eigenschaftenzugriffsoren angewendet werden, um anzugeben, dass this nicht im -
Accessor mutiert wird. Die folgenden Beispiele verfügen über schreibgeschützte Setter, da diese Accessoren den
Zustand des Memberfelds ändern, aber nicht den Wert dieses Memberfelds.
public readonly int Prop1
{
get
{
return this._store["Prop1"];
}
set
{
this._store["Prop1"] = value;
}
}

Wenn readonly auf die Eigenschaftensyntax angewendet wird, bedeutet dies, dass alle Accessoren readonly
sind.

public readonly int Prop2


{
get
{
return this._store["Prop2"];
}
set
{
this._store["Prop2"] = value;
}
}

Readonly kann nur auf Accessoren angewendet werden, die den enthaltenden Typ nicht ändern.

public int Prop3


{
readonly get
{
return this._prop3;
}
set
{
this._prop3 = value;
}
}

Readonly kann auf einige automatisch implementierte Eigenschaften angewendet werden, hat jedoch keine
sinnvollen Auswirkungen. Der Compiler behandelt alle automatisch implementierten Getter als
schreibgeschützt, unabhängig davon, ob das Schlüsselwort readonly vorhanden ist.

// Allowed
public readonly int Prop4 { get; }
public int Prop5 { readonly get; }
public int Prop6 { readonly get; set; }

// Not allowed
public readonly int Prop7 { get; set; }
public int Prop8 { get; readonly set; }

Readonly kann auf manuell implementierte Ereignisse angewendet werden, jedoch nicht auf feldspezifische
Ereignisse. Readonly kann nicht auf einzelne Ereigniszugriffsoren angewendet werden (Hinzufügen/Entfernen).
// Allowed
public readonly event Action<EventArgs> Event1
{
add { }
remove { }
}

// Not allowed
public readonly event Action<EventArgs> Event2;
public event Action<EventArgs> Event3
{
readonly add { }
readonly remove { }
}
public static readonly event Event4
{
add { }
remove { }
}

Einige weitere Syntaxbeispiele:


Ausdrucks-Bodied-Member: public readonly float ExpressionBodiedMember => (x * x) + (y * y);
Generische Einschränkungen: public readonly void GenericMethod<T>(T value) where T : struct { }

Der Compiler gibt den Instanz member wie gewohnt aus und gibt zusätzlich ein vom Compiler erkanntes
Attribut aus, das angibt, dass der Instanzmitglied den Zustand nicht ändert. Dies bewirkt, dass der this
ausgeblendete Parameter anstelle in T von zu ref T wird.
Dies würde es dem Benutzer ermöglichen, diese Instanzmethode sicher auf aufruft, ohne dass der Compiler eine
Kopie erstellen muss.
Die Einschränkungen umfassen Folgendes:
Der readonly Modifizierer kann nicht auf statische Methoden, Konstruktoren oder Destruktoren angewendet
werden.
Der readonly Modifizierer kann nicht auf Delegaten angewendet werden.
Der readonly Modifizierer kann nicht auf Member der Klasse oder Schnittstelle angewendet werden.

Nachteile
Die gleichen Nachteile wie heute bei readonly struct -Methoden.

Das könnte Ihnen auch gefallen