Freigeben über


15 Klassen

15.1 Allgemein

Eine Klasse ist eine Datenstruktur, die Datenmember (Konstanten und Felder), Funktionsmember (Methoden, Eigenschaften, Ereignisse, Indexer, Operatoren, Instanzkonstruktoren, Finalizer und statische Konstruktoren) und geschachtelte Typen enthalten kann. Klassentypen unterstützen die Vererbung, einen Mechanismus, mit dem eine abgeleitete Klasse eine Basisklasse erweitern und spezialisieren kann.

15.2 Klassendeklarationen

15.2.1 Allgemein

Eine class_declaration ist eine type_declaration (§14.7), 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 Klassendeklaration besteht aus einer optionalen Menge von Attributen (§22), gefolgt von einer optionalen Menge von Klassenmodifikatoren(§15.2.2), gefolgt von einem optionalen partial Modifikator (§15.2.7), gefolgt von dem Schlüsselwort class und einem Identifikator , der die Klasse benennt, gefolgt von einer optionalen Typ-Parameterliste (§15.2.3), gefolgt von einer optionalen class_base Spezifikation (§15.2.4), gefolgt von einem optionalen Satz von type_parameter_constraints_clauses (§15.2.5), gefolgt von einem class_body (§15.2.6), optional gefolgt von einem Semikolon.

Eine Klassendeklaration darf keine type_parameter_constraints_clauses liefern, wenn sie nicht auch eine type_parameter_listliefert.

Eine Klassendeklaration, die eine type_parameter_list bereitstellt, ist eine generische Klassendeklaration. Darüber hinaus ist jede Klasse, die in einer generischen Klassendeklaration oder einer generischen Strukturdeklaration geschachtelt ist, selbst eine generische Klassendeklaration, da Typargumente für den enthaltenden Typ bereitgestellt werden müssen, um einen konstruierten Typ (§8.4) zu erstellen.

15.2.2 Klassenmodifizierer

15.2.2.1 Allgemein

Ein class_declaration kann optional eine Sequenz von Klassenmodifizierern enthalten:

class_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'abstract'
    | 'sealed'
    | 'static'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.

Es handelt sich um einen Kompilierungszeitfehler für denselben Modifizierer, der mehrmals in einer Klassendeklaration angezeigt wird.

Der new Modifizierer ist für geschachtelte Klassen zulässig. Es gibt an, dass die Klasse ein geerbtes Element unter demselben Namen ausblendet, wie in §15.3.5 beschrieben. Es ist ein Kompilierzeitfehler, wenn der new Modifizierer in einer Klassendeklaration erscheint, die keine geschachtelte Klassendeklaration ist.

Die public, protected, internalund private Modifizierer steuern die Barrierefreiheit der Klasse. Abhängig vom Kontext, in dem die Klassendeklaration auftritt, sind einige dieser Modifizierer möglicherweise nicht zulässig (§7.5.2).

Wenn eine partielle Typdeklaration (§15.2.7) eine Spezifikation der Zugänglichkeit enthält (über die Modifikatoren public, protected, internalund private ), muss diese Spezifikation mit allen anderen Teilen übereinstimmen, die eine Spezifikation der Zugänglichkeit enthalten. Wenn kein Teil eines Teiltyps eine Barrierefreiheitsspezifikation enthält, erhält der Typ die entsprechende Standardbarrierefreiheit (§7.5.2).

Die abstract-Modifizierer, die sealed-Modifizierer und die static-Modifizierer werden in den folgenden Unterabsätzen erläutert.

15.2.2.2 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 von einer nicht abstrakten Klasse auf folgende Weise:

  • Eine abstrakte Klasse kann nicht direkt instanziiert werden, und es handelt sich um einen Kompilierungszeitfehler, um den new Operator für eine abstrakte Klasse zu verwenden. Es ist zwar möglich, Variablen und Werte zu haben, deren Kompilierungszeittypen abstrakt sind, aber diese Variablen und Werte werden notwendigerweise entweder null sein oder Verweise auf Instanzen von nicht-abstrakten Klassen enthalten, die von den abstrakten Typen abgeleitet sind.
  • Eine abstrakte Klasse darf abstrakte Mitglieder enthalten, muss dies jedoch nicht.
  • 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 Elemente enthalten, wodurch diese abstrakten Elemente außer Kraft gesetzt werden.

Beispiel: Im folgenden Code

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 Fein. Klasse B führt eine zusätzliche Methode G ein, aber da sie keine Implementierung von F bereitstellt, muss B auch als abstrakt deklariert werden. Die Klasse C überschreibt F und stellt eine konkrete Implementierung bereit. Da es in Ckeine abstrakten Mitglieder gibt, darf (muss aber nicht) C nicht-abstrakt sein.

Endbeispiel

Wenn ein oder mehrere Teile einer partiellen Typdeklaration (§15.2.7) einer Klasse den abstract Modifizierer enthalten, ist die Klasse abstrakt. Andernfalls ist die Klasse nicht abstrakt.

15.2.2.3 Versiegelte Klassen

Der sealed Modifizierer wird verwendet, um die Ableitung von einer Klasse zu verhindern. Wenn eine versiegelte Klasse als Basisklasse einer anderen Klasse angegeben wird, tritt ein Kompilierungszeitfehler auf.

Eine versiegelte Klasse kann nicht auch eine abstrakte Klasse sein.

Hinweis: Der sealed Modifizierer wird hauptsächlich verwendet, um unbeabsichtigte Ableitungen zu verhindern, ermöglicht aber auch bestimmte Laufzeitoptimierungen. Da eine abgeschlossene Klasse bekanntlich niemals über abgeleitete Klassen verfügt, ist es insbesondere möglich, virtuelle Funktionsaufrufe bei Instanzen abgeschlossener Klassen in nicht-virtuelle Aufrufe umzuwandeln. Hinweisende

Wenn ein oder mehrere Teile einer Partialtypdeklaration (§15.2.7) einer Klasse den sealed Modifizierer enthalten, wird die Klasse versiegelt. Andernfalls wird die Klasse nicht versiegelt.

15.2.2.4 Statische Klassen

15.2.2.4.1 Allgemein

Der static Modifizierer wird verwendet, um die Klasse zu kennzeichnen, die als statische Klasse deklariert wird. Eine statische Klasse darf nicht instanziiert werden, darf nicht als Typ verwendet werden und darf nur statische Member enthalten. Nur eine statische Klasse kann Deklarationen von Erweiterungsmethoden (§15.6.10) enthalten.

Eine statische Klassendeklaration unterliegt den folgenden Einschränkungen:

  • Eine statische Klasse darf weder einen sealed- noch einen abstract-Modifizierer enthalten. (Da eine statische Klasse jedoch nicht instanziiert oder abgeleitet werden kann, verhält sie sich so, als wäre sie sowohl versiegelt als auch abstrahiert.)
  • Eine statische Klasse darf keine class_base Spezifikation (§15.2.4) enthalten und kann keine Basisklasse oder eine Liste der implementierten Schnittstellen explizit angeben. Eine statische Klasse erbt implizit vom Typ object.
  • Eine statische Klasse darf nur statische Member (§15.3.8) enthalten.

    Hinweis: Alle Konstanten und geschachtelten Typen werden als statische Member klassifiziert. Hinweisende

  • Eine statische Klasse darf keine Mitglieder mit protected, private protected, oder protected internal deklarierter Zugänglichkeit haben.

Ein Kompilierungsfehler tritt auf, wenn eine dieser Einschränkungen verletzt wird.

Eine statische Klasse weist keine Instanzkonstruktoren auf. Es ist nicht möglich, einen Instanzkonstruktor in einer statischen Klasse zu deklarieren, und für eine statische Klasse wird kein Standardinstanzkonstruktor (§15.11.5) bereitgestellt.

Die Member einer statischen Klasse sind nicht automatisch statisch, und die Memberdeklarationen enthalten explizit einen static Modifizierer (mit Ausnahme von Konstanten und geschachtelten 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.

Wenn mindestens ein Teil einer Teiltypdeklaration (§15.2.7) einer Klasse den static Modifizierer enthält, ist die Klasse statisch. Andernfalls ist die Klasse nicht statisch.

15.2.2.4.2 Verweisen auf statische Klassentypen

Ein namespace_or_type_name (§7.8) darf auf eine statische Klasse verweisen, wenn

  • Der namespace_or_type_name ist der T in einem namespace_or_type_name der Form T.I, oder
  • Der namespace_or_type-name ist der T in einem Ausdruckstyp (§12.8.18) der Form typeof(T).

Ein primary_expression (§12.8) darf auf eine statische Klasse verweisen, wenn

  • Der primäre_Ausdruck ist der E in einem Mitglied_Zugang (§12.8.7) der Form E.I.

In jedem anderen Kontext ist es ein Kompilierungszeitfehler, um auf eine statische Klasse zu verweisen.

Hinweis: Es handelt sich z. B. um einen Fehler für eine statische Klasse, die als Basisklasse, einen Bestandteiltyp (§15.3.7) eines Elements, ein generisches Typargument oder eine Typparametereinschränkung verwendet werden soll. Ebenso kann eine statische Klasse nicht in einem Array-Typ, einem new-Ausdruck, einem cast-Ausdruck, einem is-Ausdruck, einem as-Ausdruck, einem sizeof -Ausdruck oder einem Standardwert-Ausdruck verwendet werden. Hinweisende

15.2.3 Typparameter

Ein Typparameter ist ein einfacher Bezeichner, der einen Platzhalter für ein Typargument angibt, das zum Erstellen eines konstruierten Typs bereitgestellt wird. Bei constrast ist ein Typargument (§8.4.2) der Typ, der beim Erstellen eines konstruierten Typs durch den Typparameter ersetzt wird.

type_parameter_list
    : '<' decorated_type_parameter (',' decorated_type_parameter)* '>'
    ;

decorated_type_parameter
    : attributes? type_parameter
    ;

type_parameter ist in §8.5 definiert.

Jeder Typparameter in einer Klassendeklaration definiert einen Namen im Deklarationsraum (§7.3) dieser Klasse. Daher kann er nicht denselben Namen wie ein anderer Typparameter dieser Klasse oder eines In dieser Klasse deklarierten Elements haben. Ein Typparameter darf nicht denselben Namen wie der Typ selbst haben.

Zwei partielle generische Typdeklarationen (im selben Programm) tragen zum gleichen ungebundenen generischen Typ bei, wenn sie denselben vollqualifizierten Namen haben (einschließlich eines generic_dimension_specifier (§12.8.18) für die Anzahl der Typparameter) (§7.8.3). Zwei solche Teiltypdeklarationen müssen denselben Namen für jeden Typparameter in der Reihenfolge angeben.

15.2.4 Klassenbasisspezifikation

15.2.4.1 Allgemein

Eine Klassendeklaration kann eine class_base Spezifikation enthalten, die die direkte Basisklasse der Klasse und die von der Klasse direkt implementierten Schnittstellen (§18) definiert.

class_base
    : ':' class_type
    | ':' interface_type_list
    | ':' class_type ',' interface_type_list
    ;

interface_type_list
    : interface_type (',' interface_type)*
    ;

15.2.4.2 Basisklassen

Wenn ein class_type in der class_base enthalten ist, gibt es die direkte Basisklasse der klasse an, die deklariert wird. Wenn eine nicht partielle Klassendeklaration keine class_base aufweist oder wenn die class_base nur Schnittstellentypen auflistet, wird die direkte Basisklasse als angenommen object. Wenn eine partielle Klassendeklaration eine Basisklassenspezifikation enthält, muss diese Basisklassenspezifikation auf denselben Typ verweisen wie alle anderen Teile dieses Teiltyps, die eine Basisklassenspezifikation enthalten. Wenn kein Teil einer partiellen Klasse eine Basisklassenspezifikation enthält, lautet objectdie Basisklasse . Eine Klasse erbt Mitglieder von ihrer direkten Basisklasse, wie in §15.3.4 beschrieben.

Beispiel: Im folgenden Code

class A {}
class B : A {}

Klasse A wird als direkte Basisklasse von B bezeichnet, und B wird als von A abgeleitet angesehen. Da A keine direkte Basisklasse explizit angibt, ist die direkte Basisklasse implizit object.

Endbeispiel

Bei einem konstruierten Klassentyp, einschließlich eines geschachtelten Typs, der in einer generischen Typdeklaration deklariert ist (§15.3.9.7), wird bei Angabe einer Basisklasse in der generischen Klassendeklaration die Basisklasse des konstruierten Typs durch Substituieren für jede type_parameter in der Basisklassendeklaration abgerufen, die entsprechende type_argument des konstruierten Typs.

Beispiel: Angesichts der generischen Klassendeklarationen

class B<U,V> {...}
class G<T> : B<string,T[]> {...}

die Basisklasse des konstruierten Typs G<int> wäre B<string,int[]>.

Endbeispiel

Die in einer Klassendeklaration angegebene Basisklasse kann ein konstruierter Klassentyp (§8.4) sein. Eine Basisklasse kann nicht selbst ein Typparameter sein (§8.5), obwohl sie die Typparameter einbeziehen kann, die sich im Bereich befinden.

Beispiel:

class Base<T> {}

// Valid, non-constructed class with constructed base class
class Extend1 : Base<int> {}

// Error, type parameter used as base class
class Extend2<V> : V {}

// Valid, type parameter used as type argument for base class
class Extend3<V> : Base<V> {}

Endbeispiel

Die direkte Basisklasse eines Klassentyps muss mindestens so zugänglich sein wie der Klassentyp selbst (§7.5.5). Beispielsweise handelt es sich um einen Kompilierungszeitfehler für eine öffentliche Klasse, die von einer privaten oder internen Klasse abgeleitet wird.

Die direkte Basisklasse eines Klassentyps darf keine der folgenden Typen sein: System.Array, , System.Delegate, , System.Enumoder System.ValueType der dynamic Typ. Darüber hinaus darf eine generische Klassendeklaration System.Attribute nicht als direkte oder indirekte Basisklasse verwendet werden (§22.2.1).

Bei der Bestimmung der Bedeutung der direkten Basisklassenspezifikation A einer Klasse Bwird die direkte Basisklasse B vorübergehend angenommen object, was sicherstellt, dass die Bedeutung einer Basisklassenspezifikation nicht rekursiv von sich selbst abhängen kann.

Beispiel: Folgende

class X<T>
{
    public class Y{}
}

class Z : X<Z.Y> {}

ist fehlerhaft, da in der Basisklassenspezifikation X<Z.Y> die direkte Basisklasse von Z als object gilt und daher (durch die Regeln von §7.8) Z nicht als Mitglied von Y betrachtet wird.

Endbeispiel

Die Basisklassen einer Klasse sind die direkte Basisklasse und ihre Basisklassen. Mit anderen Worten, der Satz von Basisklassen ist das transitive Schließen der direkten Basisklassenbeziehung.

Beispiel: Im Folgenden:

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.

Endbeispiel

Mit Ausnahme der Klasse objectverfügt jede Klasse über genau eine direkte Basisklasse. Die object Klasse hat keine direkte Basisklasse und ist die ultimative Basisklasse aller anderen Klassen.

Es handelt sich um einen Kompilierungszeitfehler für eine Klasse, die von sich selbst abhängt. Für diese Regel hängt eine Klasse direkt von ihrer direkten Basisklasse (falls vorhanden) ab und hängt direkt von der nächstgelegenen eingeschlossenen Klasse ab, in der sie geschachtelt ist (falls vorhanden). Angesichts dieser Definition ist die vollständige Menge der Klassen, von denen eine Klasse abhängt, der transitive Abschluss der Beziehung direkt abhängt von .

Beispiel: 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. Zum Schluss das Beispiel

class A : B.C {}
class B : A
{
    public class C {}
}

führt zu einem Kompilierzeitfehler, da A von B.C (seiner direkten Basisklasse) abhängt, die wiederum von B (der unmittelbar umschließenden Klasse) abhängt, welche ihrerseits von A zyklisch abhängig ist.

Endbeispiel

Eine Klasse hängt nicht von den Klassen ab, die darin verschachtelt sind.

Beispiel: Im folgenden Code

class A
{
    class B : A {}
}

B hängt von A ab (da A sowohl seine direkte Basisklasse als auch seine unmittelbar eingeschlossene Klasse ist), aber A hängt nicht von B ab (da B weder eine Basisklasse noch eine eingeschlossene Klasse von A ist). Daher ist das Beispiel gültig.

Endbeispiel

Es ist nicht möglich, von einer versiegelten Klasse abzuleiten.

Beispiel: Im folgenden Code

sealed class A {}
class B : A {} // Error, cannot derive from a sealed class

Die Klasse B ist fehlerhaft, da sie versucht, von der versiegelten Klasse Aabzuleiten.

Endbeispiel

15.2.4.3 Schnittstellenimplementierungen

Eine class_base Spezifikation kann eine Liste von Schnittstellentypen enthalten, in diesem Fall wird die Klasse gesagt, die angegebenen Schnittstellentypen zu implementieren. Für einen konstruierten Klassentyp, einschließlich eines geschachtelten Typs, der in einer generischen Typdeklaration (§15.3.9.7) deklariert ist, wird jeder implementierte Schnittstellentyp durch Ersetzen jedes type_parameter in der angegebenen Schnittstelle, der entsprechenden type_argument des konstruierten Typs abgerufen.

Der Satz von Schnittstellen für einen Typ, der in mehreren Teilen deklariert ist (§15.2.7) ist die Vereinigung der schnittstellen, die auf jedem Teil angegeben sind. Eine bestimmte Schnittstelle kann nur einmal auf jedem Teil benannt werden, aber mehrere Teile können die gleichen Basisschnittstellen benennen. Es darf nur eine Implementierung jedes Mitglieds einer bestimmten Schnittstelle geben.

Beispiel: Im Folgenden:

partial class C : IA, IB {...}
partial class C : IC {...}
partial class C : IA, IB {...}

der Satz von Basisschnittstellen für die Klasse C lautet IA, IBund IC.

Endbeispiel

In der Regel stellt jeder Teil eine Implementierung der schnittstellen bereit, die in diesem Teil deklariert sind; Dies ist jedoch keine Voraussetzung. Ein Teil kann die Implementierung für eine Schnittstelle bereitstellen, die auf einem anderen Teil deklariert ist.

Beispiel:

partial class X
{
    int IComparable.CompareTo(object o) {...}
}

partial class X : IComparable
{
    ...
}

Endbeispiel

Die in einer Klassendeklaration angegebenen Basisschnittstellen können zu Schnittstellentypen konstruiert werden (§8.4, §18.2). Eine Basisschnittstelle kann nicht eigenständig ein Typparameter sein, obwohl sie die Typparameter einbeziehen kann, die sich im Bereich befinden.

Beispiel: 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> {}

Endbeispiel

Schnittstellenimplementierungen werden in §18.6 weiter erörtert.

15.2.5 Einschränkungen des Typparameters

Generische Typ- und Methodendeklarationen können optional Typparametereinschränkungen angeben, indem type_parameter_constraints_clauses eingeschlossen werden.

type_parameter_constraints_clause
    : 'where' type_parameter ':' type_parameter_constraints
    ;

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type nullable_type_annotation?
    | 'class' nullable_type_annotation?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type nullable_type_annotation?
    | type_parameter nullable_type_annotation?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

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. Es kann höchstens eine where Klausel für jeden Typparameter geben, und die where Klauseln können in beliebiger Reihenfolge aufgeführt werden. Wie bei den get- und set-Tokens in einem Eigenschaftszugriff ist das where-Token kein Schlüsselwort.

Die Liste der in einer where Klausel angegebenen Einschränkungen kann eine der folgenden Komponenten enthalten, in dieser Reihenfolge: 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, die Referenztyp-Einschränkung, die Werttyp-Einschränkung, die Nicht-Null-Einschränkung oder die nicht verwaltete Typ-Einschränkung sein. Der Klassentyp und die Bezugstypeinschränkung können die nullable_type_annotation enthalten.

Eine sekundäre Einschränkung kann ein Schnittstellentyp oder Typparameter sein, optional gefolgt von einer nullbare_Typannotation. Das Vorhandensein der nullable_type_annotation zeigt an, dass das Typargument ein nullable Referenztyp sein darf, der einem nicht-nullable Referenztyp entspricht, der die Einschränkung erfüllt.

Die Einschränkung des Bezugstyps gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein Bezugstyp sein soll. Alle Klassentypen, Schnittstellentypen, Delegattypen, Arraytypen und Typparameter, die als Referenztyp (wie unten definiert) bezeichnet werden, erfüllen diese Einschränkung.

Der Klassentyp, die Referenztypeinschränkung und die sekundären Einschränkungen können die Nullable-Typanmerkung enthalten. Das Vorhandensein oder Fehlen dieser Anmerkung für den Typparameter gibt die Nullbarkeitserwartungen für das Typargument an:

  • Wenn die Einschränkung die Nullable-Typanmerkung nicht enthält, wird erwartet, dass das Typargument ein nicht Nullable-Referenztyp ist. Ein Compiler gibt möglicherweise eine Warnung aus, wenn das Typargument ein Nullwertverweistyp ist.
  • Wenn die Einschränkung die Annotation nullable type enthält, wird die Einschränkung sowohl von einem nicht-nullbaren Referenztyp als auch von einem nullbaren Referenztyp erfüllt.

Die Nullierbarkeit des Typarguments muss nicht mit der Nullierbarkeit des Typparameters übereinstimmen. Ein Compiler kann eine Warnung ausgeben, wenn die Nullbarkeit des Typparameters nicht mit der Nullbarkeit des Typarguments übereinstimmt.

Hinweis: Um anzugeben, dass ein Typargument ein nullabler Bezugstyp ist, fügen Sie die Nullable-Typanmerkung nicht als Einschränkung (Verwendung T : class oder T : BaseClass) hinzu, verwenden Sie T? jedoch die allgemeine Deklaration, um den entsprechenden nullablen Bezugstyp für das Typargument anzugeben. Hinweisende

Die nullable Typanmerkung ? kann nicht für ein nicht eingeschränktes Typargument verwendet werden.

Bei einem Typparameter T, wenn das Typargument ein nullabler Bezugstyp C? ist, werden Instanzen von T? als C? interpretiert, nicht als C??.

Beispiel: Die folgenden Beispiele zeigen, wie sich die Nullierbarkeit eines Typarguments auf die Nullierbarkeit einer Deklaration des Typparameters auswirkt:

public class C
{
}

public static class  Extensions
{
    public static void M<T>(this T? arg) where T : notnull
    {

    }
}

public class Test
{
    public void M()
    {
        C? mightBeNull = new C();
        C notNull = new C();

        int number = 5;
        int? missing = null;

        mightBeNull.M(); // arg is C?
        notNull.M(); //  arg is C?
        number.M(); // arg is int?
        missing.M(); // arg is int?
    }
}

Wenn das Typargument ein nicht nullabler Typ ist, gibt die ? Typanmerkung an, dass der Parameter der entsprechende nullable Typ ist. Wenn das Typargument bereits ein nullabler Bezugstyp ist, ist der Parameter derselbe nullable Typ.

Endbeispiel

Die Nicht-NULL-Einschränkung gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein nicht nullabler Werttyp oder ein nicht nullabler Bezugstyp sein soll. Ein Typargument, das kein nicht nullbarer Werttyp oder nicht nullbarer Referenztyp ist, ist zulässig, ein Compiler kann jedoch eine Diagnosewarnung ausgeben.

Da notnull kein Schlüsselwort ist, ist die Nicht-NULL-Einschränkung in primary_constraint immer syntaktisch mehrdeutig mit class_type. Aus Kompatibilitätsgründen soll eine Namenssuche (§12.8.4) des Namens notnull bei Erfolg wie ein class_type behandelt werden. Andernfalls wird sie als Nicht-Null-Einschränkung behandelt.

Beispiel: Die folgende Klasse veranschaulicht die Verwendung verschiedener Typargumente gegen unterschiedliche Einschränkungen und gibt Warnungen an, die von einem Compiler ausgegeben werden können.

#nullable enable
public class C { }
public class A<T> where T : notnull { }
public class B1<T> where T : C { }
public class B2<T> where T : C? { }
class Test
{
    static void M()
    {
        // nonnull constraint allows nonnullable struct type argument
        A<int> x1;
        // possible warning: nonnull constraint prohibits nullable struct type argument
        A<int?> x2;
        // nonnull constraint allows nonnullable class type argument
        A<C> x3;
        // possible warning: nonnull constraint prohibits nullable class type argument
        A<C?> x4;
        // nonnullable base class requirement allows nonnullable class type argument
        B1<C> x5;
        // possible warning: nonnullable base class requirement prohibits nullable class type argument
        B1<C?> x6;
        // nullable base class requirement allows nonnullable class type argument
        B2<C> x7;
        // nullable base class requirement allows nullable class type argument
        B2<C?> x8;
    }
}

Die Werttypeinschränkung gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein nicht nullwertebarer Werttyp sein soll. Alle nicht nullbaren Strukturtypen, Enumerationstypen und Typparameter mit der Werttypeinschränkung erfüllen diese Einschränkung. Beachten Sie, dass ein nullable-Werttyp (§8.3.12), obwohl als Werttyp klassifiziert, die Werttypeinschränkung nicht erfüllt. Ein Typparameter mit der Werttypeinschränkung darf nicht auch über die constructor_constraint verfügen, obwohl er als Typargument für einen anderen Typparameter mit einem constructor_constraint verwendet werden kann.

Hinweis: Der -Typ gibt die nicht-nullbare Werttypeinschränkung für System.Nullable<T> an. Rekursiv konstruierte Formen T?? und Nullable<Nullable<T>> sind daher verboten. Hinweisende

Die Nicht verwaltete Typeinschränkung gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein nicht nullabler nicht verwalteter Typ (§8.8) sein soll.

Da unmanaged kein Schlüsselwort ist, ist die Unmanaged-Einschränkung in primary_constraint immer syntaktisch mehrdeutig im Vergleich zu class_type. Aus Kompatibilitätsgründen wird, wenn eine Namenssuche (§12.8.4) des Namens unmanaged erfolgreich ist, sie als class_type behandelt. Andernfalls wird sie als nicht verwaltete Einschränkung behandelt.

Zeigertypen dürfen niemals Typargumente sein und erfüllen keine Typleinschränkungen, nicht einmal die für nicht verwaltete Typen, obwohl sie selbst nicht verwaltete Typen sind.

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, das für diesen Typparameter verwendet wird, unterstützen soll. Jedes Mal, wenn ein konstruierter Typ oder eine generische Methode verwendet wird, wird das Typargument auf die Einschränkungen für den Typparameter zur Kompilierungszeit überprüft. Das angegebene Typargument erfüllt die in §8.4.5 beschriebenen Bedingungen.

Eine class_type Einschränkung erfüllt die folgenden Regeln:

  • Der Typ muss ein Klassentyp sein.
  • Der Typ darf nicht sein sealed.
  • Der Typ darf keine der folgenden Typen sein: System.Array oder System.ValueType.
  • Der Typ darf nicht sein object.
  • Bei den meisten Einschränkungen für einen bestimmten Typparameter kann es sich um einen Klassentyp handeln.

Ein als interface_type Einschränkung festgelegter Typ muss die folgenden Regeln erfüllen:

  • Der Typ muss ein Schnittstellentyp sein.
  • Ein Typ darf in einer bestimmten where Klausel nicht mehr als einmal angegeben werden.

In beiden Fällen kann die Einschränkung einen der Typparameter des zugeordneten Typs oder der Methodendeklaration als Teil eines konstruierten Typs umfassen und den deklarierten Typ umfassen.

Alle klassen- oder Schnittstellentypen, die als Typparametereinschränkung angegeben sind, müssen mindestens so barrierefrei (§7.5.5) sein, wie der generische Typ oder die methode deklariert wird.

Ein als type_parameter Einschränkung festgelegter Typ muss die folgenden Regeln erfüllen:

  • Der Typ muss ein Typparameter sein.
  • Ein Typ darf in einer bestimmten where Klausel nicht mehr als einmal angegeben werden.

Darüber hinaus gibt es keine Zyklen in der Abhängigkeitsdiagramm von Typparametern, wobei abhängigkeit eine transitive Beziehung ist, die durch Folgendes definiert wird:

  • Wenn ein Typparameter T als Einschränkung für typparameter S verwendet wird, Shängt es davonT ab.
  • Wenn ein Typparameter S von einem Typparameter T abhängt und T von einem Typparameter U abhängt, hängt es Sdann davon abU.

Bei dieser Beziehung handelt es sich um einen Kompilierungszeitfehler für einen Typparameter, der von sich selbst (direkt oder indirekt) abhängig ist.

Alle Einschränkungen müssen zwischen abhängigen Typparametern konsistent sein. Wenn der Typparameter S vom Typparameter T abhängt, dann:

  • T darf die Werttypeinschränkung nicht aufweisen. Andernfalls wird T effektiv versiegelt, sodass S gezwungen wäre, denselben Typ wie T zu haben, wodurch der Bedarf an zwei Typparametern entfällt.
  • Wenn S die Wertetyp-Einschränkung hat, darf T keine class_type Einschränkung haben.
  • Wenn S eine class_type-Einschränkung A hat und T eine class_type-Einschränkung B hat, muss es eine Identitätskonversion oder eine implizite Referenzkonvertierung von A zu B oder eine implizite Referenzkonvertierung von B zu A geben.
  • Wenn S auch vom Typparameter U abhängt und U eine class_type Einschränkung A aufweist und T eine class_type Einschränkung B aufweist, muss es eine Identitätskonvertierung oder implizite Verweiskonvertierung von A in B oder eine implizite Verweiskonvertierung von B in A geben.

Es ist korrekt, dass S die Werttypenbeschränkung und T die Referenztypenbeschränkung hat. Dies beschränkt T sich effektiv auf die Typen System.Object, System.ValueType, , System.Enumund alle Schnittstellentypen.

Wenn die where Klausel für einen Typparameter eine Konstruktoreinschränkung enthält (die das Formular new()hat), ist es möglich, den new Operator zum Erstellen von Instanzen des Typs zu verwenden (§12.8.17.2). Jedes Typargument, das für einen Typparameter mit einer Konstruktoreinschränkung verwendet wird, ist ein Werttyp, eine nicht abstrakte Klasse mit einem öffentlichen parameterlosen Konstruktor oder ein Typparameter mit der Werttypeinschränkung oder Konstruktoreinschränkung.

Es ist ein Kompilierfehler, wenn type_parameter_constraints mit einem primary_constraint von struct oder unmanaged auch einen constructor_constrainthaben.

Beispiel: Im Folgenden sind Beispiele für Einschränkungen aufgeführt:

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ängigkeitsgraphen der Typparameter verursacht.

class Circular<S,T>
    where S: T
    where T: S // Error, circularity in dependency graph
{
    ...
}

Die folgenden Beispiele veranschaulichen zusätzliche ungültige Situationen:

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

Endbeispiel

Die dynamische Löschung eines Typs C ist vom Typ Cₓ und wie folgt aufgebaut:

  • Wenn C ein geschachtelter Typ Outer.Inner ist, dann ist Cₓ ein geschachtelter Typ Outerₓ.Innerₓ.
  • Wenn CCₓein konstruierter Typ G<A¹, ..., Aⁿ> mit Typargumenten A¹, ..., Aⁿ ist, dann ist Cₓ der konstruierte Typ G<A¹ₓ, ..., Aⁿₓ>.
  • Wenn C ein Arraytyp E[] ist, dann ist Cₓ der Arraytyp Eₓ[].
  • Wenn C dynamisch ist, dann ist Cₓobject.
  • Ansonsten ist CₓC.

Die effektive Basisklasse eines Typparameters T wird wie folgt definiert:

Sei R eine Menge von Typen, so dass:

  • Für jede Einschränkung von T, die ein Typparameter ist, enthält R die effektive Basisklasse.
  • Für jede Einschränkung von T, die einen Strukturtyp darstellt, enthält RSystem.ValueType.
  • Für jede Einschränkung von T, die ein Enumerationstyp ist, enthält RSystem.Enum.
  • Für jede Einschränkung von T , die ein Delegatentyp ist, enthält R seine dynamische Löschung.
  • Für jede Einschränkung von T, die ein Arraytyp ist, enthält RSystem.Array.
  • Für jede Einschränkung von T, die ein Klassentyp ist, enthält R ihre dynamische Löschung.

Dann

  • Wenn T den Werttyp-Beschränkung hat, ist System.ValueType die effektive Basisklasse.
  • R Andernfalls ist die effektive Basisklasse objectleer.
  • Andernfalls ist die effektive Basisklasse T der am meisten eingeschlossene Typ (§10.5.3) des Satzes R. Wenn der Satz keinen eingeschlossenen Typ aufweist, ist Tdie effektive Basisklasse von object . Die Konsistenzregeln stellen sicher, dass der umfassendste Typ vorhanden ist.

Wenn der Typparameter ein Methodentypparameter ist, dessen Einschränkungen von der Basismethode geerbt werden, wird die effektive Basisklasse nach der Typersetzung berechnet.

Diese Regeln stellen sicher, dass die effektive Basisklasse immer ein class_type ist.

Der effektive Schnittstellensatz eines Typparameters T wird wie folgt definiert:

  • Wenn T keine secondary_constraints hat, ist seine effektive Schnittstellenmenge leer.
  • Wenn TInterface_type Einschränkungen aufweist, aber keine type_parameter Einschränkungen vorhanden sind, ist der effektive Schnittstellensatz die Menge der dynamischen Tilgungen ihrer interface_type Einschränkungen.
  • Wenn T keine interface_type Einschränkungen vorhanden sind, aber type_parameter Einschränkungen aufweisen, ist der effektive Schnittstellensatz die Vereinigung der effektiven Schnittstellensätze ihrer type_parameter Einschränkungen.
  • Wenn T sowohl interface_type-Einschränkungen als auch type_parameter-Einschränkungen hat, besteht ihr effektiver Schnittstellensatz aus der Vereinigung der dynamischen Löschungen ihrer interface_type-Einschränkungen mit den effektiven Schnittstellensätzen ihrer type_parameter-Einschränkungen.

Ein Typparameter ist als Bezugstyp bekannt, wenn er die Bezugstypeinschränkung aufweist oder seine effektive Basisklasse nicht object oder System.ValueType. Ein Typparameter ist als nicht nullabler Bezugstyp bekannt, wenn er als Bezugstyp bekannt ist und die Nicht-NULL-Bezugstypeinschränkung aufweist.

Werte eines eingeschränkten Parametertyps können verwendet werden, um auf die durch die Einschränkungen implizierten Instanzelemente zuzugreifen.

Beispiel: Im Folgenden:

interface IPrintable
{
    void Print();
}

class Printer<T> where T : IPrintable
{
    void PrintOne(T x) => x.Print();
}

Die Methoden von IPrintable können direkt auf x aufgerufen werden, weil T so eingeschränkt ist, dass es immer IPrintable implementieren muss.

Endbeispiel

Wenn eine partielle generische Typdeklaration Einschränkungen enthält, stimmen die Einschränkungen allen anderen Teilen zu, die Einschränkungen enthalten. Insbesondere müssen alle Teile, die Einschränkungen enthalten, Einschränkungen für den gleichen Satz von Typparametern aufweisen, und für jeden Typparameter müssen die Gruppen der primären, sekundären und Konstruktoreinschränkungen gleichwertig sein. Zwei Gruppen von Einschränkungen sind gleich, wenn sie dieselben Mitglieder enthalten. Wenn kein Teil eines partiellen generischen Typs Typenparametereinschränkungen angibt, werden die Typparameter als nicht eingeschränkt betrachtet.

Beispiel:

partial class Map<K,V>
    where K : IComparable<K>
    where V : IKeyProvider<K>, new()
{
    ...
}

partial class Map<K,V>
    where V : IKeyProvider<K>, new()
    where K : IComparable<K>
{
    ...
}

partial class Map<K,V>
{
    ...
}

ist richtig, da diese Teile, die Einschränkungen (die ersten beiden) enthalten, effektiv denselben Satz von primären, sekundären und Konstruktoreinschränkungen für denselben Satz von Typparametern angeben.

Endbeispiel

15.2.6 Klassentext

Die class_body einer Klasse definiert die Member dieser Klasse.

class_body
    : '{' class_member_declaration* '}'
    ;

15.2.7 Partielle Typdeklarationen

Der Modifizierer partial wird beim Definieren einer Klasse, Struktur oder eines Schnittstellentyps in mehreren Teilen verwendet. Der partial Modifizierer ist ein kontextbezogenes Schlüsselwort (§6.4.4) und hat eine besondere Bedeutung unmittelbar vor den Schlüsselwörtern class, structund interface. (Ein Teiltyp kann Teilmethodendeklarationen (§15.6.9) enthalten.

Jeder Teil einer Teiltypdeklaration muss einen partial Modifizierer enthalten und muss im selben Namespace oder im selben Typ wie die anderen Teile deklariert werden. Der partial Modifizierer gibt an, dass an anderer Stelle zusätzliche Teile der Typdeklaration vorhanden sein können, aber das Vorhandensein solcher zusätzlichen Teile ist keine Anforderung; er ist gültig für die einzige Deklaration eines Typs, um den partial Modifizierer einzuschließen. Es ist nur für eine Deklaration eines Teiltyps gültig, um die Basisklasse oder implementierte Schnittstellen einzuschließen. Alle Deklarationen einer Basisklasse oder implementierter Schnittstellen müssen jedoch übereinstimmen, einschließlich der Nullierbarkeit aller angegebenen Typargumente.

Alle Teile eines Teiltyps müssen gemeinsam kompiliert werden, sodass die Teile während der Kompilierung zusammengeführt werden können. Partielle Typen lassen nicht zu, dass bereits kompilierte Typen erweitert werden.

Geschachtelte Typen können mithilfe des partial Modifizierers in mehreren Teilen deklariert werden. In der Regel wird der enthaltende Typ ebenfalls mit partial deklariert, und jeder Teil des geschachtelten Typs wird in einem anderen Abschnitt des enthaltenden Typs deklariert.

Beispiel: Die folgende partielle Klasse wird in zwei Teilen implementiert, die sich in unterschiedlichen Kompilierungseinheiten befinden. Der erste Teil wird von einem Datenbankzuordnungstool generiert, während der zweite Teil manuell erstellt wird:

public partial class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }
}

// File: Customer2.cs
public partial class Customer
{
    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

Wenn die beiden obigen Teile zusammen kompiliert werden, verhält sich der resultierende Code wie folgt, als ob die Klasse als einzelne Einheit geschrieben wurde:

public class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }

    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

Endbeispiel

Die Behandlung von Attributen, die für den Typ oder die Typparameter verschiedener Teile einer Teiltypdeklaration angegeben sind, wird in §22.3 erläutert.

15.3 Klassenmitglieder

15.3.1 Allgemein

Die Mitglieder einer Klasse bestehen aus den durch ihre class_member_declarations eingeführten Mitgliedern und den von der direkten Basisklasse geerbten Mitgliedern.

class_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    ;

Die Mitglieder einer Klasse sind in die folgenden Kategorien unterteilt:

  • Konstanten, die konstanten Werte darstellen, die der Klasse zugeordnet sind (§15.4).
  • Felder, die die Variablen der Klasse sind (§15.5).
  • Methoden, die die Berechnungen und Aktionen implementieren, die von der Klasse ausgeführt werden können (§15.6).
  • Eigenschaften, die benannte Merkmale und die Aktionen definieren, die mit dem Lesen und Schreiben dieser Merkmale verbunden sind (§15.7).
  • Ereignisse, die Benachrichtigungen definieren, die von der Klasse generiert werden können (§15.8).
  • Indexer, die zulassen, dass Instanzen der Klasse auf die gleiche Weise (syntaktisch) wie Arrays (§15.9) indiziert werden können.
  • Operatoren, die die Ausdrucksoperatoren definieren, die auf Instanzen der Klasse angewendet werden können (§15.10).
  • Instanzkonstruktoren, die die zum Initialisieren von Instanzen der Klasse erforderlichen Aktionen implementieren (§15.11)
  • Finalizer, die die auszuführenden Aktionen implementieren, bevor Instanzen der Klasse endgültig verworfen werden (§15.13).
  • Statische Konstruktoren, die die zum Initialisieren der Klasse erforderlichen Aktionen implementieren (§15.12).
  • Typen, die die Typen darstellen, die für die Klasse lokal sind (§14.7).

Ein class_declaration erstellt einen neuen Deklarationsraum (§7.3), und die type_parameters und die class_member_declarations, die unmittelbar in der class_declaration enthalten sind, führen neue Mitglieder in diesen Deklarationsraum ein. Die folgenden Regeln gelten für Klassenmitgliedserklärung:

  • Instanzkonstruktoren, Finalizer und statische Konstruktoren haben denselben Namen wie die unmittelbar eingeschlossene Klasse. Alle anderen Mitglieder haben Namen, die sich vom Namen der unmittelbar eingeschlossenen Klasse unterscheiden.

  • Der Name eines Typparameters in der type_parameter_list einer Klassendeklaration unterscheidet sich von den Namen aller anderen Typparameter in demselben type_parameter_list und unterscheidet sich von dem Namen der Klasse und den Namen aller Member der Klasse.

  • Der Name eines Typs unterscheidet sich von den Namen aller Nichttypmitglieder, die in derselben Klasse deklariert sind. Wenn zwei oder mehr Typdeklarationen denselben vollqualifizierten Namen aufweisen, müssen die Deklarationen den partial Modifizierer (§15.2.7) haben und diese Deklarationen kombinieren, um einen einzelnen Typ zu definieren.

Hinweis: Da der vollqualifizierte Name einer Typdeklaration die Anzahl der Typparameter codiert, können zwei unterschiedliche Typen denselben Namen aufweisen, solange sie unterschiedliche Anzahl von Typparametern haben. Hinweisende

  • Der Name einer Konstante, eines Felds, einer Eigenschaft oder eines Ereignisses unterscheidet sich von den Namen aller anderen Elemente, die in derselben Klasse deklariert sind.

  • Der Name einer Methode unterscheidet sich von den Namen aller anderen Nichtmethoden, die in derselben Klasse deklariert sind. Darüber hinaus unterscheidet sich die Signatur (§7.6) einer Methode von den Signaturen aller anderen Methoden, die in derselben Klasse deklariert sind, und zwei Methoden, die in derselben Klasse deklariert sind, dürfen keine Signaturen aufweisen, die sich ausschließlich von in, out, und ref.

  • Die Signatur eines Instanzkonstruktors unterscheidet sich von den Signaturen aller anderen Instanzkonstruktoren, die in derselben Klasse deklariert sind, und zwei in derselben Klasse deklarierte Konstruktoren dürfen keine Signaturen aufweisen, die sich ausschließlich von ref und out.

  • Die Signatur eines Indexers unterscheidet sich von den Signaturen aller anderen in derselben Klasse deklarierten Indexer.

  • Die Signatur eines Betreibers unterscheidet sich von den Signaturen aller anderen Betreiber, die in derselben Klasse deklariert sind.

Die geerbten Mitglieder einer Klasse (§15.3.4) sind nicht Teil des Deklarationsbereichs einer Klasse.

Hinweis: Daher kann eine abgeleitete Klasse ein Element mit demselben Namen oder derselben Signatur wie ein geerbtes Element deklarieren (wodurch das geerbte Element tatsächlich ausgeblendet wird). Hinweisende

Der Satz von Mitgliedern eines Typs, der in mehreren Teilen deklariert ist (§15.2.7) ist die Vereinigung der in jedem Teil deklarierten Mitglieder. Die Körper aller Teile der Typdeklaration haben denselben Deklarationsbereich (§7.3), und der Umfang jedes Mitglieds (§7.7) erstreckt sich auf die Körper aller Teile. Die Accessibility-Domäne eines Mitglieds umfasst immer alle Teile des einschließenden Typs; ein in einem Teil deklariertes privates Mitglied ist von einem anderen Teil aus frei zugänglich. Es handelt sich um einen Kompilierungsfehler, wenn dasselbe Mitglied in mehr als einem Teil des Typs deklariert wird, es sei denn, dieses Mitglied verfügt über den partial Modifier.

Beispiel:

partial class A
{
    int x;                   // Error, cannot declare x more than once
    partial void M();        // Ok, defining partial method declaration

    partial class Inner      // Ok, Inner is a partial type
    {
        int y;
    }
}

partial class A
{
    int x;                   // Error, cannot declare x more than once
    partial void M() { }     // Ok, implementing partial method declaration

    partial class Inner      // Ok, Inner is a partial type
    {
        int z;
    }
}

Endbeispiel

Die Feldinitialisierungsreihenfolge kann innerhalb des C#-Codes erheblich sein, und einige Garantien werden gemäß §15.5.6.1 bereitgestellt. Andernfalls ist die Reihenfolge von Elementen innerhalb eines Typs selten signifikant, kann aber bei der Interfacierung mit anderen Sprachen und Umgebungen erheblich sein. In diesen Fällen ist die Reihenfolge von Elementen innerhalb eines Typs, der in mehreren Teilen deklariert ist, nicht definiert.

15.3.2 Der Instanztyp

Jede Klassendeklaration weist einen zugeordneten Instanztyp auf. Bei einer generischen Klassendeklaration wird der Instanztyp durch Erstellen eines konstruierten Typs (§8.4) aus der Typdeklaration gebildet, wobei jedes der angegebenen Typargumente der entsprechende Typparameter ist. Da der Instanztyp die Typparameter verwendet, kann er nur verwendet werden, wo sich die Typparameter im Bereich befinden. d. h. innerhalb der Klassendeklaration. Der Instanztyp ist der Typ von this für Code, der innerhalb der Klassendeklaration geschrieben wurde. Bei nicht generischen Klassen ist der Instanztyp einfach die deklarierte Klasse.

Beispiel: Im Folgenden werden mehrere Klassendeklarationen zusammen mit ihren Instanztypen gezeigt:

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

Endbeispiel

15.3.3 Mitglieder von konstruierten Typen

Die nicht geerbten Member eines konstruierten Typs erhalten Sie, indem Sie für jeden Typ-Parameter in der Member-Deklaration das entsprechende Typ-Argument des konstruierten Typs ersetzen. Der Ersetzungsprozess basiert auf der semantischen Bedeutung von Typdeklarationen und ist nicht einfach nur textbezogene Ersetzung.

Beispiel: Angenommen die generische Klassendeklaration

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>> hat die folgenden Elemente:

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 Elements a in der generischen Klassendeklaration Gen ist "zweidimensionales Array von T", sodass der Typ des Elements a im obigen konstruierten Typ "zweidimensionales Array eines eindimensionalen Arrays von int" oder int[,][].

Endbeispiel

Innerhalb von Instanzfunktionsmitgliedern ist der Typ von this der Instanztyp (§15.3.2) der enthaltenen Deklaration.

Alle Member einer generischen Klasse können Typparameter aus jeder eingeschlossenen Klasse verwenden, entweder direkt oder als Teil eines konstruierten Typs. Wenn zur Laufzeit ein bestimmter geschlossener konstruierter Typ (§8.4.3) verwendet wird, wird jede Verwendung eines Typparameters durch das Typargument ersetzt, das für den konstruierten Typ bereitgestellt wird.

Beispiel:

class C<V>
{
    public V f1;
    public C<V> f2;

    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
    }
}

Endbeispiel

15.3.4 Vererbung

Eine Klasse erbt die Mitglieder ihrer direkten Basisklasse. Vererbung bedeutet, dass eine Klasse implizit alle Member ihrer direkten Basisklasse enthält, mit Ausnahme der Instanzkonstruktoren, Finalizer und statischen Konstruktoren der Basisklasse. Einige wichtige Aspekte der Vererbung sind:

  • Vererbung ist transitiv. Wenn C von B abgeleitet ist und B von A abgeleitet ist, dann erbt C die in B deklarierten Mitglieder sowie die in A deklarierten Mitglieder.

  • Eine abgeleitete Klasse erweitert ihre direkte Basisklasse. Eine abgeleitete Klasse kann ihren geerbten Mitgliedern neue hinzufügen, jedoch die Definition eines geerbten Mitglieds nicht entfernen.

  • Instanzkonstruktoren, Finalisierer und statische Konstruktoren werden nicht vererbt, aber alle anderen Mitglieder, unabhängig von ihrer deklarierten Zugänglichkeit (§7.5). Abhängig von ihrer deklarierten Accessibility sind vererbte Mitglieder jedoch möglicherweise nicht in einer abgeleiteten Klasse zugänglich.

  • Eine abgeleitete Klasse kann geerbte Member (§7.7.2.3) ausblenden, indem neue Member mit demselben Namen oder derselben Signatur deklariert werden. Das Ausblenden eines geerbten Elements entfernt dieses Element jedoch nicht, sondern verhindert lediglich den direkten Zugriff darauf über die abgeleitete Klasse.

  • Eine Instanz einer Klasse enthält einen Satz aller Instanzfelder, die in der Klasse und deren Basisklassen deklariert sind, und eine implizite Konvertierung (§10.2.8) besteht aus einem abgeleiteten Klassentyp in einen seiner Basisklassentypen. Daher kann ein Verweis auf eine Instanz einiger abgeleiteter Klassen als Verweis auf eine Instanz einer seiner Basisklassen behandelt werden.

  • Eine Klasse kann virtuelle Methoden, Eigenschaften, Indexer und Ereignisse deklarieren und abgeleitete Klassen können die Implementierung dieser Funktionsmember überschreiben. Dadurch können Klassen polymorphes Verhalten aufweisen, wobei die aktionen, die von einem Funktionselementaufruf ausgeführt werden, je nach Laufzeittyp der Instanz variieren, über die dieses Funktionselement aufgerufen wird.

Die geerbten Mitglieder eines konstruierten Klassentyps sind die Mitglieder des unmittelbaren Basisklassentyps (§15.2.4.2), der durch Ersetzen der Typargumente des konstruierten Typs für jedes Vorkommen der entsprechenden Typparameter in der Basisklassenspezifikationgefunden wird. Diese Mitglieder wiederum werden umgewandelt, indem für jeden Typ-Parameter in der Mitgliederdeklaration das entsprechende Typ-Argument der Basisklassen-Spezifikationersetzt wird.

Beispiel:

class B<U>
{
    public U F(long index) {...}
}

class D<T> : B<T[]>
{
    public T G(string s) {...}
}

Im obigen Code verfügt der konstruierte Typ D<int> über ein nicht geerbtes öffentliches Element intG(string s), das durch das Ersetzen des Typarguments int für den Typparameter T abgerufen wird. D<int> verfügt außerdem über ein geerbtes Element aus der Klassendeklaration B. Dieses geerbte Mitglied wird ermittelt, indem zunächst der Basisklassentyp B<int[]> von D<int> bestimmt wird, indem int für T in der Basisklassenspezifikation B<T[]>ersetzt wird. Dann wird B als Typargument für int[]durch U in public U F(long index)ersetzt, was das geerbte Mitglied public int[] F(long index)ergibt.

Endbeispiel

15.3.5 Der neue Modifizierer

Eine class_member_declaration darf ein Mitglied mit demselben Namen oder derselben Signatur wie ein geerbtes Mitglied deklarieren. Wenn dies geschieht, wird gesagt, dass das Mitglied der abgeleiteten Klasse das Mitglied der Basisklasse versteckt. Siehe §7.7.2.3 für eine genaue Spezifikation, wann ein Mitglied ein geerbtes Mitglied ausblendet.

Ein geerbtes Element M wird als verfügbar betrachtet, wenn zugänglich ist und es kein anderes geerbtes zugängliches Element N gibt, das bereits ausblendet. Das implizite Ausblenden eines geerbten Mitglieds wird nicht als Fehler betrachtet, aber ein Compiler zeigt eine Warnung an, es sei denn, die Deklaration des Mitglieds der abgeleiteten Klasse enthält einen new-Modifizierer, um explizit anzugeben, dass das abgeleitete Mitglied das Basismitglied ausblenden soll. Wenn mindestens ein Teil einer Teildeklaration (§15.2.7) eines geschachtelten Typs den new Modifizierer enthält, wird keine Warnung ausgegeben, wenn der geschachtelte Typ ein verfügbares geerbtes Element ausblendet.

Wenn ein new Modifikator in einer Deklaration enthalten ist, die ein verfügbares geerbtes Mitglied nicht ausblendet, wird eine entsprechende Warnung ausgegeben.

15.3.6 Zugriffsmodifizierer

Eine class_member_declaration kann eine der erlaubten Arten der angegebenen Zugänglichkeit (§7.5.2) haben: public, protected internal, protected, private protected, internal oder private. Mit Ausnahme der protected internal und private protected Kombinationen ist es ein Fehler zur Kompilierzeit, mehr als einen Zugriffsmodifizierer anzugeben. Wenn eine class_member_declaration keine Zugriffsmodifikatoren enthält, wird private angenommen.

15.3.7 Bestandteiltypen

Typen, die in der Deklaration eines Elements verwendet werden, werden als Bestandteiltypen dieses Elements bezeichnet. Mögliche Komponententypen sind der Typ einer Konstante, eines Felds, einer Eigenschaft, eines Ereignisses oder eines Indexers, der Rückgabetyp einer Methode oder eines Operators sowie die Parametertypen einer Methode, eines Indexers, eines Operators oder eines Instanzkonstruktors. Die Bestandteiltypen eines Mitglieds sind mindestens ebenso zugänglich wie dieses Mitglied selbst (§7.5.5).

15.3.8 Statische und Instanzmitglieder

Mitglieder einer Klasse sind entweder statische Mitglieder oder Instanzmitglieder.

Hinweis: Im Allgemeinen ist es hilfreich, statische Member als Zugehörigkeit zu Klassen und Instanzmbern als Zugehörigkeit zu Objekten (Instanzen von Klassen) zu betrachten. Hinweisende

Wenn ein Feld, eine Methode, eine Eigenschaft, ein Ereignis, ein Operator oder eine Konstruktordeklaration einen static Modifizierer enthält, deklariert es ein statisches Element. Darüber hinaus deklariert eine Konstante oder Typdeklaration implizit ein statisches Element. Statische Mitglieder haben die folgenden Merkmale:

  • Wenn auf ein statisches Element M in einem member_access (§12.8.7) des Formulars E.Mverwiesen wird, E wird ein Typ mit einem Element Mbezeichnet. Es ist ein Kompilierzeitfehler, wenn E eine Instanz bezeichnet.
  • Ein statisches Feld in einer nicht generischen Klasse identifiziert genau einen Speicherort. Unabhängig davon, wie viele Instanzen einer nicht generischen Klasse erstellt werden, gibt es immer nur eine Kopie eines statischen Felds. Jeder unterschiedliche geschlossene konstruierte Typ (§8.4.3) verfügt über einen eigenen Satz statischer Felder, unabhängig von der Anzahl der Instanzen des geschlossenen konstruierten Typs.
  • Ein statisches Funktionselement (Methode, Eigenschaft, Ereignis, Operator oder Konstruktor) wird nicht für eine bestimmte Instanz ausgeführt, und es handelt sich um einen Kompilierungszeitfehler, der in einem solchen Funktionselement darauf verweist.

Wenn eine Feld-, Methoden-, Eigenschafts-, Ereignis-, Indexer-, Konstruktor- oder Finalizerdeklaration keinen statischen Modifizierer enthält, deklariert sie ein Instanzmemm. (Ein Instanzmitglied wird manchmal als nicht statisches Element bezeichnet.) Instanzmitglieder weisen die folgenden Merkmale auf:

  • Wenn auf ein Instanzmitglied M in einem member_access (§12.8.7) des Formulars E.Mverwiesen wird, E wird eine Instanz eines Typs bezeichnet, der über ein Mitglied Mverfügt. Es handelt sich um einen Bindungszeitfehler, bei dem E einen Typ angibt.
  • Jede Instanz einer Klasse enthält einen separaten Satz aller Instanzfelder der Klasse.
  • Ein Instanzfunktionsmitglied (Methode, Eigenschaft, Indexer, Instanzkonstruktor oder Finalisierer) operiert auf einer bestimmten Instanz der Klasse, und auf diese Instanz kann als this zugegriffen werden (§12.8.14).

Beispiel: Im folgenden Beispiel werden die Regeln für den Zugriff auf statische und Instanzmitglieder veranschaulicht.

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 ein simple_name (§12.8.4) für den Zugriff auf Instanzmember und statische Member verwendet werden kann. Die G Methode zeigt, dass es ein Kompilierungszeitfehler ist, in einem statischen Funktionsmitglied über einen simple_name auf ein Instanzelement zuzugreifen. Die Main-Methode verdeutlicht, dass in einem member_access (§12.8.7) Instanzmitglieder über Instanzen zugänglich gemacht werden sollen und statische Mitglieder über Typen zugänglich gemacht werden sollen.

Endbeispiel

15.3.9 Geschachtelte Typen

15.3.9.1 Allgemein

Ein in einer Klasse oder Struktur deklarierter Typ wird als geschachtelter Typ bezeichnet. Ein Typ, der in einer Kompilierungseinheit oder einem Namespace deklariert wird, wird als nicht geschachtelter Typ bezeichnet.

Beispiel: Im folgenden Beispiel:

class A
{
    class B
    {
        static void F()
        {
            Console.WriteLine("A.B.F");
        }
    }
}

Die Klasse B ist ein geschachtelter Typ, da sie innerhalb der Klasse Adeklariert ist und die Klasse A ein nicht geschachtelter Typ ist, da sie in einer Kompilierungseinheit deklariert wird.

Endbeispiel

15.3.9.2 Vollqualifizierter Name

Der vollqualifizierte Name (§7.8.3) für eine verschachtelte Typdeklaration ist S.N, wobei S der vollqualifizierte Name der Typdeklaration ist, in der der Typ N deklariert wird, und N der nicht qualifizierte Name (§7.8.2) der verschachtelten Typdeklaration ist (einschließlich aller generic_dimension_specifier (§12.8.18)).

15.3.9.3 Barrierefreiheit deklariert

Nicht verschachtelte Typen können public oder internal deklarierte Zugänglichkeit haben und haben standardmäßig internal deklarierte Zugänglichkeit. Geschachtelte Typen können auch diese Formen der deklarierten Barrierefreiheit aufweisen, sowie eine oder mehrere zusätzliche Formen der deklarierten Barrierefreiheit, je nachdem, ob der enthaltende Typ eine Klasse oder Struktur ist:

  • Ein geschachtelter Typ, der in einer Klasse deklariert wird, kann über eine der zulässigen Arten erklärter Zugriffsebenen verfügen und weist, wie andere Klassenmitglieder, standardmäßig die private erklärte Zugriffsebene auf.
  • Ein geschachtelter Typ, der in einer Struktur deklariert ist, kann eine von drei Formen der deklarierten Sichtbarkeit aufweisen (public, internal oder private) und hat, wie andere Strukturmitglieder, standardmäßig die deklarierte private Sichtbarkeit.

Beispiel: 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 geschachtelte Klasse Node.

Endbeispiel

15.3.9.4 Ausblenden

Ein geschachtelter Typ kann ein Basiselement (§7.7.2.2) ausblenden. Der new Modifizierer (§15.3.5) ist für geschachtelte Typdeklarationen zulässig, sodass das Ausblenden explizit ausgedrückt werden kann.

Beispiel: Das Beispiel

class Base
{
    public static void M()
    {
        Console.WriteLine("Base.M");
    }
}

class Derived: Base
{
    public new class M
    {
        public static void F()
        {
            Console.WriteLine("Derived.M.F");
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.M.F();
    }
}

zeigt eine geschachtelte Klasse M an, die die Methode M in Base versteckt.

Endbeispiel

15.3.9.5 dieser Zugriff

Ein geschachtelter Typ und sein enthaltender Typ haben keine besondere Beziehung zu this_access (§12.8.14). Insbesondere kann this innerhalb eines verschachtelten Typs nicht verwendet werden, um auf Instanzmitglieder des enthaltenen Typs zu verweisen. In Fällen, in denen ein verschachtelter Typ Zugriff auf die Instanzmember seines enthaltenden Typs benötigt, kann der Zugriff gewährt werden, indem die this für die Instanz des enthaltenden Typs als Konstruktorargument für den verschachtelten Typ angegeben wird.

Beispiel: Das folgende Beispiel

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 Technik. Eine Instanz von C erzeugt eine Instanz von Nestedund übergibt ihr eigenes this an den Konstruktor von Nested, um den späteren Zugriff auf die Instanzmitglieder von Czu ermöglichen.

Endbeispiel

15.3.9.6 Zugriff auf private und geschützte Elemente des enthaltenen Typs

Ein verschachtelter Typ hat Zugriff auf alle Mitglieder, auf die sein enthaltender Typ zugreifen kann, einschließlich der Mitglieder des enthaltenden Typs, für die private und protected als zugriffsfähig erklärt wurden.

Beispiel: Das Beispiel

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 C mit einer geschachtelten Klasse Nestedan. Innerhalb von Nested ruft die Methode G die in F definierte statische Methode C auf, und F hat private deklarierte Zugriffsebene.

Endbeispiel

Ein geschachtelter Typ kann auch auf geschützte Elemente zugreifen, die in einem Basistyp des zugehörigen Typs definiert sind.

Beispiel: Im folgenden Code

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 geschachtelte Klasse Derived.Nested greift durch eine Instanz von F auf die geschützte Methode Derived zu, die in der Basisklasse von Base, Derived, definiert ist.

Endbeispiel

15.3.9.7 Geschachtelte Typen in generischen Klassen

Eine generische Klassendeklaration kann geschachtelte Typdeklarationen enthalten. Die Typparameter der eingeschlossenen Klasse können innerhalb der geschachtelten Typen verwendet werden. Eine geschachtelte Typdeklaration kann zusätzliche Typparameter enthalten, die nur für den geschachtelten Typ gelten.

Jede Typdeklaration, die in einer generischen Klassendeklaration enthalten ist, 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 seiner Typargumente, benannt werden. Innerhalb der äußeren Klasse kann der verschachtelte Typ jedoch ohne Qualifikation verwendet werden; der Instanztyp der äußeren Klasse kann implizit beim Erstellen des verschachtelten Typs verwendet werden.

Beispiel: Im Folgenden werden drei verschiedene korrekte Wege zur Darstellung eines konstruierten Typs vorgestellt, der aus Inner erstellt wurde; die ersten beiden sind gleichwertig:

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
    }
}

Endbeispiel

Obwohl es sich um einen schlechten Programmierstil handelt, kann ein Typparameter in einem geschachtelten Typ einen Element- oder Typparameter ausblenden, der im äußeren Typ deklariert ist.

Beispiel:

class Outer<T>
{
    class Inner<T>                                  // Valid, hides Outer's T
    {
        public T t;                                 // Refers to Inner's T
    }
}

Endbeispiel

15.3.10 Reservierte Mitgliedsnamen

15.3.10.1 Allgemein

Um die zugrunde liegende C#-Laufzeitimplementierung zu erleichtern, reserviert die Implementierung für jede Quellmitgliedsdeklaration, die eine Eigenschaft, ein Ereignis oder einen Indexer ist, zwei Methodensignaturen basierend auf der Art der Memberdeklaration, ihrem Namen und ihrem Typ (§15.3.10.2, §15.3.10.3, §15.3.10.4). Es handelt sich um einen Kompilierzeitfehler für ein Programm, um ein Mitglied zu deklarieren, dessen Signatur mit einer Signatur übereinstimmt, die von einem Mitglied reserviert ist, das im selben Bereich deklariert ist, auch wenn die zugrunde liegende Laufzeitimplementierung diese Reservierungen nicht verwendet.

Die reservierten Namen führen keine Deklarationen ein, daher nehmen sie nicht an der Mitgliedersuche teil. Die zugeordneten reservierten Methodensignaturen einer Deklaration nehmen jedoch an der Vererbung (§15.3.4) teil und können mit dem new Modifizierer (§15.3.5) ausgeblendet werden.

Hinweis: Die Reservierung dieser Namen dient drei Zwecken:

  1. Damit die zugrunde liegende Implementierung einen normalen Bezeichner als Methodenname zum Abrufen oder Festlegen des Zugriffs auf das C#-Sprachfeature verwenden kann.
  2. Damit andere Sprachen mit einem gewöhnlichen Bezeichner als Methodenname zum Abrufen oder Festlegen des Zugriffs auf das C#-Sprachfeature interoperieren können.
  3. Um sicherzustellen, dass die von einem konformen Compiler akzeptierte Quelle von einem anderen akzeptiert wird, indem die Besonderheiten reservierter Membernamen in allen C#-Implementierungen konsistent sind.

Hinweisende

Die Deklaration eines Finalizers (§15.13) bewirkt auch, dass eine Signatur reserviert wird (§15.3.10.5).

Bestimmte Namen sind für die Verwendung als Operatormethodennamen reserviert (§15.3.10.6).

15.3.10.2 Für Eigenschaften reservierte Mitgliedsnamen

Für eine Eigenschaft P (§15.7) vom Typ Tsind die folgenden Signaturen reserviert:

T get_P();
void set_P(T value);

Beide Signaturen sind reserviert, auch wenn die Eigenschaft schreibgeschützt oder lesegeschützt ist.

Beispiel: Im folgenden Code

class A
{
    public int P
    {
        get => 123;
    }
}

class B : A
{
    public new int get_P() => 456;

    public new 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, wodurch Signaturen für get_P und set_P Methoden reserviert werden. A Die Klasse B leitet sich von A ab und verbirgt beide dieser reservierten Signaturen. Das Beispiel ergibt die Ausgabe:

123
123
456

Endbeispiel

15.3.10.3 Mitgliedsnamen, die für Ereignisse reserviert sind

Für ein Ereignis E (§15.8) des Delegatentyps Tsind die folgenden Signaturen reserviert:

void add_E(T handler);
void remove_E(T handler);

15.3.10.4 Mitgliedsnamen, die für Indexer reserviert sind

Für einen Indexer (§15.9) vom Typ T mit Parameterliste Lsind 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.

Darüber hinaus ist der Membername Item reserviert.

15.3.10.5 Für Finalizer reservierte Mitgliedsnamen

Für eine Klasse mit einem Finalizer (§15.13) ist die folgende Signatur reserviert:

void Finalize();

15.3.10.6 Methodennamen, die für Operatoren reserviert sind

Die folgenden Methodennamen sind reserviert. Während viele entsprechende Operatoren in dieser Spezifikation haben, sind einige für die Verwendung durch zukünftige Versionen reserviert, während einige für die Interoperabilität mit anderen Sprachen reserviert sind.

Methodenname C#-Operator
op_Addition + (binär)
op_AdditionAssignment (reserviert)
op_AddressOf (reserviert)
op_Assign (reserviert)
op_BitwiseAnd & (binär)
op_BitwiseAndAssignment (reserviert)
op_BitwiseOr \|
op_BitwiseOrAssignment (reserviert)
op_CheckedAddition (reserviert für die zukünftige Nutzung)
op_CheckedDecrement (reserviert für die zukünftige Nutzung)
op_CheckedDivision (reserviert für die zukünftige Nutzung)
op_CheckedExplicit (reserviert für die zukünftige Nutzung)
op_CheckedIncrement (reserviert für die zukünftige Nutzung)
op_CheckedMultiply (reserviert für die zukünftige Nutzung)
op_CheckedSubtraction (reserviert für die zukünftige Nutzung)
op_CheckedUnaryNegation (reserviert für die zukünftige Nutzung)
op_Comma (reserviert)
op_Decrement -- (Präfix und Postfix)
op_Division /
op_DivisionAssignment (reserviert)
op_Equality ==
op_ExclusiveOr ^
op_ExclusiveOrAssignment (reserviert)
op_Explicit expliziter (verengender) Zwang
op_False false
op_GreaterThan >
op_GreaterThanOrEqual >=
op_Implicit impliziter (sich erweiternder) Zwang
op_Increment ++ (Präfix und Postfix)
op_Inequality !=
op_LeftShift <<
op_LeftShiftAssignment (reserviert)
op_LessThan <
op_LessThanOrEqual <=
op_LogicalAnd (reserviert)
op_LogicalNot !
op_LogicalOr (reserviert)
op_MemberSelection (reserviert)
op_Modulus %
op_ModulusAssignment (reserviert)
op_MultiplicationAssignment (reserviert)
op_Multiply * (binär)
op_OnesComplement ~
op_PointerDereference (reserviert)
op_PointerToMemberSelection (reserviert)
op_RightShift >>
op_RightShiftAssignment (reserviert)
op_SignedRightShift (reserviert)
op_Subtraction - (binär)
op_SubtractionAssignment (reserviert)
op_True true
op_UnaryNegation - (unär)
op_UnaryPlus + (unär)
op_UnsignedRightShift (reserviert für die zukünftige Nutzung)
op_UnsignedRightShiftAssignment (reserviert)

15.4 Konstanten

Eine Konstante ist ein Klassenelement, das einen Konstantenwert darstellt: ein Wert, der zur Kompilierungszeit 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'
    ;

Ein constant_declaration kann einen Satz von Attributen (§22), einen new Modifizierer (§15.3.5) und eine der zulässigen Arten deklarierter Barrierefreiheit (§15.3.6) enthalten. Die Attribute und Modifizierer gelten für alle Elemente, die vom constant_declaration deklariert wurden. Auch wenn Konstanten als statische Mitglieder betrachtet werden, erfordert oder erlaubt eine Konstanten-Deklaration weder einen Modifizierer noch lässt sie einen zu. Es ist ein Fehler, wenn derselbe Modifizierer mehrfach in einer Konstantendeklaration auftaucht.

Der Typ eines constant_declaration gibt den Typ der elemente an, die durch die Deklaration eingeführt wurden. Auf den Typ folgt eine Liste der constant_declarator s (§13.6.3), von denen jedes ein neuesMitglied einführt. Ein constant_declarator besteht aus einem Bezeichner , der das Element benennt, gefolgt von einem "="-Token, gefolgt von einem constant_expression (§12.23), der den Wert des Elements angibt.

Der in einer Konstantendeklaration angegebene Typ muss sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, ein enum_type oder ein reference_type sein. Jede constant_expression erhält einen Wert des Zieltyps oder eines Typs, der durch eine implizite Konvertierung (§10.2) in den Zieltyp konvertiert werden kann.

Der Typ einer Konstante muss mindestens so zugänglich sein wie die Konstante selbst (§7.5.5).

Der Wert einer Konstanten wird in einem Ausdruck mithilfe eines simple_name (§12.8.4) oder eines member_access (§12.8.7) abgerufen.

Eine Konstante kann sich selbst an einer constant_expression beteiligen. Daher kann eine Konstante in jedem Konstrukt verwendet werden, das eine constant_expression erfordert.

Hinweis: Beispiele für solche Konstrukte sind case Bezeichnungen, goto case Anweisungen, enum Memberdeklarationen, Attribute und andere Konstantendeklarationen. Hinweisende

Hinweis: Wie in §12.23 beschrieben, ist ein constant_expression ein Ausdruck, der zur Kompilierungszeit vollständig ausgewertet werden kann. Da die einzige Möglichkeit zum Erstellen eines Nicht-Null-Werts einer anderen reference_type als string die Anwendung des new Operators ist und da der new Operator in einem constant_expression nicht zulässig ist, ist der einzige mögliche Wert für Konstanten von reference_typeanderen als string .null Hinweisende

Wenn ein symbolischer Name für einen Konstantenwert gewünscht wird, aber wenn der Typ dieses Werts in einer Konstantendeklaration nicht zulässig ist oder wenn der Wert nicht zur Kompilierungszeit von einem constant_expression berechnet werden kann, kann stattdessen ein Readonly-Feld (§15.5.3) verwendet werden.

Hinweis: Die Versionsverwaltungssemantik von const und readonly unterscheidet sich (§15.5.3.3). Hinweisende

Eine Konstantendeklaration, die mehrere Konstanten deklariert, entspricht mehreren Deklarationen einzelner Konstanten mit denselben Attributen, Modifizierern und Typ.

Beispiel:

class A
{
    public const double X = 1.0, Y = 2.0, Z = 3.0;
}

entspricht

class A
{
    public const double X = 1.0;
    public const double Y = 2.0;
    public const double Z = 3.0;
}

Endbeispiel

Konstanten dürfen von anderen Konstanten innerhalb desselben Programms abhängig sein, solange die Abhängigkeiten nicht zirkulär sind.

Beispiel: Im folgenden Code

class A
{
    public const int X = B.Z + 1;
    public const int Y = 10;
}

class B
{
    public const int Z = A.Y + 1;
}

ein Compiler muss zuerst A.Y auswerten und dann B.Z auswerten und schließlich A.X auswerten, wobei die Werte 10, 11 und 12 erzeugt werden.

Endbeispiel

Konstantendeklarationen können von Konstanten aus anderen Programmen abhängen, aber solche Abhängigkeiten sind nur in einer Richtung möglich.

Beispiel: Wenn auf das obige Beispiel verwiesen wird und A sowie B in separaten Programmen deklariert wurden, wäre es möglich, dass A.X von B.Z abhängig ist, aber B.Z könnte dann nicht gleichzeitig von A.Y abhängig sein. Endbeispiel

15.5 Felder

15.5.1 Allgemein

Ein Feld ist ein Element, das 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'
    | unsafe_modifier   // unsafe code support
    ;

variable_declarators
    : variable_declarator (',' variable_declarator)*
    ;

variable_declarator
    : identifier ('=' variable_initializer)?
    ;

unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.

Ein field_declaration kann einen Satz von Attributen (§22), einen new Modifizierer (§15.3.5), eine gültige Kombination der vier Zugriffsmodifizierer (§15.3.6) und einen static Modifizierer (§15.5.2) enthalten. Darüber hinaus kann ein field_declaration einen readonly Modifizierer (§15.5.3) oder einen volatile Modifizierer (§15.5.4) enthalten, jedoch nicht beide. Die Attribute und Modifizierer gelten für alle Elemente, die vom field_declaration deklariert wurden. Es ist ein Fehler, wenn derselbe Modifizierer mehrmals in einer field_declaration erscheint.

Der Typ eines field_declaration gibt den Typ der elemente an, die durch die Deklaration eingeführt wurden. Auf den Typ folgt eine Liste der variable_declarator, von denen jedes ein neues Mitglied einführt. Ein variable_declarator besteht aus einem Bezeichner , der das Element benennt, optional gefolgt von einem "="-Token und einem variable_initializer (§15.5.6), der den Anfangswert dieses Elements angibt.

Der Typ eines Felds muss mindestens so zugänglich sein wie das Feld selbst (§7.5.5).

Der Wert eines Felds wird in einem Ausdruck mithilfe eines simple_name (§12.8.4), eines member_access (§12.8.7) oder eines base_access (§12.8.15) abgerufen. Der Wert eines nicht-lesbaren Feldes wird mit einer Zuweisung (§12.21) geändert. Der Wert eines nicht-lesbaren Feldes kann sowohl mit Postfix-Inkrement- und Dekrement-Operatoren (§12.8.16) als auch mit Präfix-Inkrement- und Dekrement-Operatoren (§12.9.6) ermittelt und geändert werden.

Eine Felddeklaration, die mehrere Felder deklariert, entspricht mehreren Deklarationen einzelner Felder mit denselben Attributen, Modifizierern und Typ.

Beispiel:

class A
{
    public static int X = 1, Y, Z = 100;
}

entspricht

class A
{
    public static int X = 1;
    public static int Y;
    public static int Z = 100;
}

Endbeispiel

15.5.2 Statische Felder und Instanzfelder

Wenn eine Felddeklaration einen static Modifizierer enthält, sind die durch die Deklaration eingeführten Felder statische Felder. Wenn kein static Modifizierer vorhanden ist, sind die durch die Deklaration eingeführten Felder Instanzfelder. Statische Felder und Instanzfelder sind zwei der verschiedenen Arten von Variablen (§9), die von C# unterstützt werden, und manchmal werden sie als statische Variablen bzw. Instanzvariablen bezeichnet.

Wie in §15.3.8 erläutert, enthält jede Instanz einer Klasse einen vollständigen Satz der Instanzfelder der Klasse, während es nur einen Satz statischer Felder für jeden nicht generischen oder geschlossenen konstruierten Typ gibt, unabhängig von der Anzahl der Instanzen der Klasse oder des geschlossenen konstruierten Typs.

15.5.3 Schreibgeschützte Felder

15.5.3.1 Allgemein

Wenn eine Felddeklaration einen readonly Modifikator enthält, sind die durch die Deklaration eingeführten Felder nur lesbare Felder. Direkte Zuweisungen zu schreibgeschützten Feldern können nur als Teil dieser Deklaration oder in einem Instanzkonstruktor oder statischen Konstruktor in derselben Klasse auftreten. (In diesen Kontexten kann ein readonly-Feld mehrmals zugewiesen werden.) Insbesondere sind direkte Zuordnungen zu einem Readonly-Feld nur in den folgenden Kontexten zulässig:

  • In dem variablen_deklarator , der das Feld einführt (indem ein variabler_initializer in die Deklaration aufgenommen wird).
  • Für ein Beispielfeld, in den Instanzkonstruktoren der Klasse, die die Felddeklaration enthält; für ein statisches Feld im statischen Konstruktor der Klasse, die die Felddeklaration enthält. Dies sind auch die einzigen Kontexte, in denen es gültig ist, ein Readonly-Feld als Ausgabe- oder Referenzparameter zu übergeben.

Der Versuch, einem schreibgeschützten Feld zuzuweisen oder es in einem anderen Kontext als Ausgabe- oder Referenzparameter zu übergeben, ist ein Kompilierzeitfehler.

15.5.3.2 Verwenden statischer Readonly-Felder für Konstanten

Ein statisches Readonly-Feld ist nützlich, wenn ein symbolischer Name für einen Konstantenwert gewünscht wird, aber wenn der Typ des Werts in einer Konstdeklaration nicht zulässig ist oder wenn der Wert zur Kompilierung nicht berechnet werden kann.

Beispiel: Im folgenden Code

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 Black, White, Red, Greenund Blue Member können nicht als Const-Member deklariert werden, da ihre Werte nicht zur Kompilierungszeit berechnet werden können. Das Deklarieren static readonly dieser Elemente hat jedoch nahezu die gleiche Wirkung.

Endbeispiel

15.5.3.3 Versionsverwaltung von Konstanten und statischen Readonly-Feldern

Konstanten und schreibgeschützte Felder haben eine unterschiedliche Semantik der binären Versionierung. Wenn ein Ausdruck auf eine Konstante verweist, wird der Wert der Konstante zur Kompilierungszeit abgerufen, aber wenn ein Ausdruck auf ein readonly-Feld verweist, wird der Wert des Felds erst während der Laufzeit abgerufen.

Beispiel: Betrachten Sie eine Anwendung, die aus zwei separaten Programmen besteht:

namespace Program1
{
    public class Utils
    {
        public static readonly int x = 1;
    }
}

und

namespace Program2
{
    class Test
    {
        static void Main()
        {
            Console.WriteLine(Program1.Utils.X);
        }
    }
}

Program1 und Program2 Namespaces bezeichnen zwei Programme, die separat kompiliert werden. Da Program1.Utils.X als ein static readonly Feld deklariert ist, ist der durch die Console.WriteLine Anweisung ausgegebene Wert nicht zur Kompilierungszeit bekannt, sondern wird zur Laufzeit ermittelt. Wenn der Wert von X geändert wird und Program1 neu kompiliert wird, gibt die Console.WriteLine-Anweisung den neuen Wert aus, auch wenn Program2 nicht neu kompiliert wird. Sollte es sich bei X jedoch um eine Konstante handeln, wäre der Wert von X zum Zeitpunkt der Kompilierung von Program2 erhalten worden und würde von Änderungen in Program1 unbeeinflusst bleiben, bis Program2 erneut kompiliert wird.

Endbeispiel

15.5.4 Veränderliche Felder

Wenn eine field_declaration einen volatile Modifizierer enthält, sind die durch diese Deklaration eingeführten Felder volatile Felder. Für nicht veränderliche Felder können Optimierungstechniken, die Anweisungen neu anordnen, zu unerwarteten und unvorhersehbaren Ergebnissen in Multithread-Programmen führen, die ohne Synchronisierung auf Felder zugreifen, z. B. die von der lock_statement (§13.13). Diese Optimierungen können vom Compiler, vom Laufzeitsystem oder von Hardware ausgeführt werden. Für volatile Felder sind solche Neuanordnungsoptimierungen eingeschränkt.

  • Ein Lesen eines veränderlichen Feldes wird als veränderliches Lesen bezeichnet. Ein flüchtiges Lesen hat eine „Akquisitions-Semantik“; das heißt, es tritt garantiert vor allen Verweisen auf den Speicher auf, die danach in der Befehlssequenz auftreten.
  • Ein Schreibvorgang eines veränderlichen Felds wird als veränderlicher Schreibzugriff bezeichnet. Ein flüchtiger Schreibvorgang hat eine „Release-Semantik“, d. h. er findet garantiert nach allen Speicherverweisen vor dem Schreibbefehl in der Befehlssequenz statt.

Diese Einschränkungen stellen sicher, dass alle Threads volatile Schreiboperationen, die von einem anderen Thread ausgeführt werden, in der Reihenfolge, in der sie ausgeführt wurden, berücksichtigen. Eine konforme Implementierung ist nicht erforderlich, um eine einzige Gesamtreihenfolge von flüchtigen Schreibvorgängen bereitzustellen, wie sie von allen Ausführungsthreads aus zu sehen ist. Der Typ eines veränderlichen Felds soll einer der folgenden sein:

  • Eine reference_type.
  • Eine type_parameter , die als Bezugstyp bekannt ist (§15.2.5).
  • Der Typ byte, sbyte, short, ushort, int, uint, char, float, bool, System.IntPtr oder System.UIntPtr.
  • Ein enum_type mit einem enum_base-Typ von byte, sbyte, short, ushort, int, oder uint.

Beispiel: Das Beispiel

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 = {result}");
                return;
            }
        }
    }
}

erzeugt die Ausgabe:

result = 143

In diesem Beispiel startet die Methode Main einen neuen Thread, der die Methode Thread2ausführt. Mit dieser Methode wird ein Wert in einem nicht flüchtigen Feld namens result gespeichert und dann true in dem flüchtigen Feld finished gespeichert. Der Hauptthread wartet darauf, dass das Feld finished auf true gesetzt wird, und liest dann das Feld result. Da finished deklariert volatilewurde, muss der Hauptthread den Wert 143 aus dem Feld resultlesen. Wenn das Feld finished nicht deklariert volatile worden wäre, dann wäre es zulässig, dass der Speicher result für den Hauptthread nach der Speicherung in finished sichtbar ist, und somit könnte der Hauptthread den Wert 0 aus dem Feld result lesen. Das Deklarieren finished als volatile Feld verhindert eine solche Inkonsistenz.

Endbeispiel

15.5.5 Feldinitialisierung

Der Anfangswert eines Felds, unabhängig davon, ob es sich um ein statisches Feld oder ein Instanzfeld handelt, ist der Standardwert (§9.3) des Feldtyps. Es ist nicht möglich, den Wert eines Felds zu beobachten, bevor diese Standardinitialisierung erfolgt ist, und ein Feld ist daher nie "nicht initialisiert".

Beispiel: Das Beispiel

class Test
{
    static bool b;
    int i;

    static void Main()
    {
        Test t = new Test();
        Console.WriteLine($"b = {b}, i = {t.i}");
    }
}

erzeugt die Ausgabe

b = False, i = 0

da b und i beide automatisch in Standardwerte initialisiert werden.

Endbeispiel

15.5.6 Variableninitialisierer

15.5.6.1 Allgemein

Felddeklarationen können variable_initializers enthalten. Bei statischen Feldern entsprechen Variableninitialisierer Zuordnungsanweisungen, die während der Klasseninitialisierung ausgeführt werden. Bei Instanzfeldern entsprechen die Variableninitialisierer den Zuordnungsanweisungen, die ausgeführt werden, wenn eine Instanz der Klasse erstellt wird.

Beispiel: Das Beispiel

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 = {x}, i = {a.i}, s = {a.s}");
    }
}

erzeugt die Ausgabe

x = 1.4142135623730951, i = 100, s = Hello

da eine Zuordnung zu x erfolgt, wenn die statischen Feldinitialisierer ausgeführt werden, und die Zuordnungen zu i und s erfolgen, wenn die Instanzfeldinitialisierer ausgeführt werden.

Endbeispiel

Die in §15.5.5.5 beschriebene Standardwertinitialisierung erfolgt für alle Felder, einschließlich Feldern mit Variableninitialisierern. Wenn eine Klasse initialisiert wird, werden daher alle statischen Felder in dieser Klasse zuerst mit ihren Standardwerten initialisiert, und dann werden die statischen Feldinitialisierer in textualer Reihenfolge ausgeführt. Ebenso werden beim Erstellen einer Instanz einer Klasse zuerst alle Instanzfelder in dieser Instanz mit ihren Standardwerten initialisiert, und dann werden die Instanzfeldinitialisierer in textualer Reihenfolge ausgeführt. Wenn es Felddeklarationen in mehreren partiellen Typdeklarationen für denselben Typ gibt, ist die Reihenfolge der Teile nicht angegeben. Innerhalb jedes Teils werden die Feldinitialisierer jedoch in der Reihenfolge ausgeführt.

Es ist möglich, dass statische Felder mit variablen Initialisierern im Standardwertzustand beobachtet werden.

Beispiel: Dies wird jedoch dringend als Stilsache abgeraten. Das Beispiel

class Test
{
    static int a = b + 1;
    static int b = a + 1;

    static void Main()
    {
        Console.WriteLine($"a = {a}, b = {b}");
    }
}

zeigt dieses Verhalten. Trotz der Zirkeldefinitionen von a und b ist das Programm gültig. Es resultiert in der Ausgabe

a = 1, b = 2

da die statischen Felder a und b auf 0 (den Standardwert für int) initialisiert werden, bevor ihre Initialisierer ausgeführt werden. Wenn der Initialisierer für a ausgeführt wird, ist der Wert von b null, und deshalb wird a mit 1 initialisiert. Wenn der Initialisierer für b läuft, ist der Wert von a bereits 1und daher wird b auf 2initialisiert.

Endbeispiel

15.5.6.2 Initialisierung statischer Felder

Die Initialisierer einer statischen Feldvariablen einer Klasse entsprechen einer Abfolge von Zuordnungen, die in der Textreihenfolge ausgeführt werden, in der sie in der Klassendeklaration angezeigt werden (§15.5.6.1). Innerhalb einer Teilklasse wird die Bedeutung von "Textreihenfolge" durch §15.5.6.1 angegeben. Wenn ein statischer Konstruktor (§15.12) 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 einer implementierungsabhängigen Zeit vor der ersten Verwendung eines statischen Felds dieser Klasse ausgeführt.

Beispiel: Das Beispiel

class Test
{
    static void Main()
    {
        Console.WriteLine($"{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");
}

könnte entweder die Ausgabe erzeugen:

Init A
Init B
1 1

oder die Ausgabe:

Init B
Init A
1 1

da die Ausführung des Initialisierers von X und des Initialisierers von Y in beliebiger Reihenfolge erfolgen kann; sie müssen nur vor den Verweisen auf diese Felder stattfinden. Im Beispiel ist jedoch Folgendes zu beachten:

class Test
{
    static void Main()
    {
        Console.WriteLine($"{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 sein:

Init B
Init A
1 1

da die Regeln für die Ausführung statischer Konstruktoren (wie in §15.12 definiert) angeben, dass Bder statische Konstruktor (und damit Bauch statische Feldinitialisierer) vor Aden statischen Konstruktoren und Feldinitialisierern ausgeführt werden muss.

Endbeispiel

15.5.6.3 Instanzfeldinitialisierung

Die Instanzfeldvariableninitialisierer einer Klasse entsprechen einer Abfolge von Zuordnungen, die unmittelbar nach dem Eintrag zu einem der Instanzkonstruktoren (§15.11.3) dieser Klasse ausgeführt werden. Innerhalb einer Teilklasse wird die Bedeutung von "Textreihenfolge" durch §15.5.6.1 angegeben. Die Variableninitialisierer werden in der Textreihenfolge ausgeführt, in der sie in der Klassendeklaration (§15.5.6.1) angezeigt werden. Der Erstellungs- und Initialisierungsprozess der Klasseninstanz wird in §15.11 weiter beschrieben.

Ein variabler Initialisierer für ein Instanzfeld kann nicht auf die erstellte Instanz verweisen. Daher ist es ein Kompilierungszeitfehler, auf this in einem Variableninitialisierer zu verweisen, da es ein Kompilierungszeitfehler ist, wenn ein Variableninitialisierer über einen simple_name auf ein Instanzmitglied verweist.

Beispiel: Im folgenden Code

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 Kompilierfehler, da er auf ein Mitglied der zu erstellenden Instanz verweist.

Endbeispiel

15.6 Methoden

15.6.1 Allgemein

Eine Methode ist ein Member, das eine Berechnung oder eine Aktion implementiert, die durch ein Objekt oder eine Klasse durchgeführt werden kann. Methoden werden mit method_declaration sdeklariert:

method_declaration
    : attributes? method_modifiers return_type method_header method_body
    | attributes? ref_method_modifiers ref_kind ref_return_type method_header
      ref_method_body
    ;

method_modifiers
    : method_modifier* 'partial'?
    ;

ref_kind
    : 'ref'
    | 'ref' 'readonly'
    ;

ref_method_modifiers
    : ref_method_modifier*
    ;

method_header
    : member_name '(' parameter_list? ')'
    | member_name type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause*
    ;

method_modifier
    : ref_method_modifier
    | 'async'
    ;

ref_method_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;

return_type
    : ref_return_type
    | 'void'
    ;

ref_return_type
    : type
    ;

member_name
    : identifier
    | interface_type '.' identifier
    ;

method_body
    : block
    | '=>' null_conditional_invocation_expression ';'
    | '=>' expression ';'
    | ';'
    ;

ref_method_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

Grammatiknotizen:

  • unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
  • wenn bei der Erkennung eines method_body sowohl die Alternative null_conditional_invocation_expression als auch die Alternative expression anwendbar sind, wird die erstere gewählt.

Hinweis: Die Überschneidung von Alternativen und Priorität zwischen diesen Alternativen dient ausschließlich der beschreibenden Einfachheit. Die Grammatikregeln könnten ausgearbeitet werden, um die Überschneidung zu entfernen. ANTLR und andere Grammatiksysteme übernehmen den gleichen Komfort und method_body weist die angegebene Semantik automatisch auf. Hinweisende

Eine Methodendeklaration kann ein Set von Attributen (§22) und eine der erlaubten Arten der deklarierten Zugänglichkeit (§15.3.6), die new (§15.3.5), static (§15.6.3), virtual (§15.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7), extern (§15.6.8) und async (§15.14). Zusätzlich kann eine method_declaration, die direkt in einem struct_declaration enthalten ist, den readonly-Modifizierer (§16.4.12) haben.

Eine Deklaration verfügt über eine gültige Kombination aus Modifizierern, wenn alle folgenden Bedingungen erfüllt sind:

  • Die Deklaration enthält eine gültige Kombination aus Zugriffsmodifizierern (§15.3.6).
  • Die Deklaration enthält nicht mehrmals denselben Modifizierer.
  • Die Deklaration enthält höchstens einen der folgenden Modifizierer: static, , virtualund override.
  • Die Deklaration enthält höchstens einen der folgenden Modifizierer: new und override.
  • Wenn die Erklärung den Modifikator abstract enthält, dann enthält die Erklärung keinen der folgenden Modifikatoren: static, virtual, sealed oder extern.
  • Wenn die Deklaration den private Modifizierer enthält, enthält die Deklaration keine der folgenden Modifizierer: virtual, , overrideoder 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 keine der folgenden Modifizierer: new, public, protected, internal, private, virtual, sealed, override, abstract oder extern.

Methoden werden nach dem klassifiziert, was, wenn etwas, sie zurückgeben:

  • Wenn ref vorhanden ist, ist die Methode Returns-by-ref und gibt eine Variablenreferenzzurück, die optional schreibgeschützt ist;
  • Andernfalls, wenn return_typevoid ist, ist die Methode wertlos und gibt keinen Wert zurück.
  • Andernfalls ist die Methode Returns-by-value und gibt einen Wert zurück.

Der Return-Typ einer Return-by-Value- oder Returns-no-Value-Methoden-Deklaration gibt den Typ des Ergebnisses an, das die Methode gegebenenfalls zurückgibt. Nur eine Methode, die keinen Wert zurückgibt, darf den Modifikator partial enthalten (§15.6.9). Wenn die Deklaration den async Modifizierer enthält, muss return_type sein void oder die Methode gibt nach Wert zurück, und der Rückgabetyp ist ein Vorgangstyp (§15.14.1).

Der ref_return_type einer returns-by-ref-Methodendeklaration gibt den Typ der Variablen an, auf die die variable_reference der Methode verweist.

Eine generische Methode ist eine Methode, deren Deklaration eine type_parameter_list enthält. Dadurch werden die Typparameter für die Methode angegeben. Die optionalen type_parameter_constraints_clauses geben die Einschränkungen für die Typparameter an.

Eine generische Methoden-Deklaration für eine explizite Schnittstellen-Member-Implementierung darf keine type_parameter_constraints_clausehaben; die Deklaration erbt alle Beschränkungen von den Beschränkungen der Schnittstellenmethode.

Ebenso darf eine Methodendeklaration mit dem override Modifikator keine type_parameter_constraints_clauses haben und die Einschränkungen der Typparameter der Methode werden von der virtuellen Methode geerbt, die überschrieben wird.

Die member_name gibt den Namen der Methode an. Es sei denn, die Methode ist eine explizite Schnittstellenmemmimplementierung (§18.6.2), ist die member_name einfach ein Bezeichner.

Bei einer expliziten Implementierung eines Schnittstellenmitglieds besteht der Mitgliedsname aus einem Schnittstellentyp gefolgt von einem "." und einem Bezeichner. In diesem Fall enthält die Erklärung keine anderen Modifizierer als (möglicherweise) extern oder async.

Die optionale parameter_list gibt die Parameter der Methode an (§15.6.2).

Der return_type oder ref_return_type und alle Typen, auf die in der parameter_list einer Methode verwiesen wird, müssen mindestens so zugänglich sein wie die Methode selbst (§7.5.5).

Der Methodenkörper einer Return-by-Value oder Returns-no-Value Methode ist entweder ein Semikolon, ein Blockkörper oder ein Ausdruckskörper. Ein Blocktext besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn die Methode aufgerufen wird. Ein Ausdruckskörper besteht aus =>, gefolgt von einem null_conditional_invocation_expression oder expressionund einem Semikolon und bezeichnet einen einzelnen Ausdruck, der ausgeführt werden soll, wenn die Methode aufgerufen wird.

Bei abstrakten und externen Methoden besteht die method_body einfach aus einem Semikolon. Bei partiellen Methoden kann der Methodenkörper entweder aus einem Semikolon, einem Blockkörper oder einem Ausdruckskörper bestehen. Bei allen anderen Methoden ist der method_body entweder ein Blocktext oder ein Ausdruckstext.

Besteht die method_body aus einem Semikolon, enthält die Deklaration nicht den async Modifizierer.

Der ref_method_body einer returns-by-ref Methode ist entweder ein Semikolon, ein Blockkörper oder ein Ausdruckskörper. Ein Blocktext besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn die Methode aufgerufen wird. Ein Ausdruckskörper besteht aus =>, gefolgt von ref, einer Variablenreferenzund einem Semikolon und bezeichnet eine einzelne Variablenreferenz , die ausgewertet wird, wenn die Methode aufgerufen wird.

Bei abstrakten und externen Methoden besteht der ref_method_body einfach aus einem Semikolon; bei allen anderen Methoden ist der ref_method_body entweder ein Blockkörper oder ein Ausdruckskörper.

Der Name, die Anzahl der Typparameter und die Parameterliste einer Methode definieren die Signatur (§7.6) der Methode. Genauer gesagt besteht die Signatur einer Methode aus ihrem Namen, der Anzahl ihrer Typ-Parameter und der Anzahl, parameter_mode_modifiers (§15.6.2.1) und Typen ihrer Parameter. Der Rückgabetyp ist weder Teil der Signatur einer Methode noch die Namen der Parameter, die Namen der Typparameter oder die Einschränkungen. Wenn ein Parametertyp auf einen Typparameter der Methode verweist, wird die Ordnungsposition des Typparameters (nicht der Name des Typparameters) für die Äquivalenz des Typs verwendet.

Der Name einer Methode unterscheidet sich von den Namen aller anderen Nichtmethoden, die in derselben Klasse deklariert sind. Darüber hinaus unterscheidet sich die Signatur einer Methode von den Signaturen aller anderen Methoden, die in derselben Klasse deklariert sind, und zwei Methoden, die in derselben Klasse deklariert sind, dürfen keine Signaturen haben, die sich ausschließlich von in, outund .ref

Die type_parameter der Methode sind im gesamten method_declaration im Gültigkeitsbereich und können verwendet werden, um Typen in diesem Gültigkeitsbereich in return_type oder ref_return_type, method_body oder ref_method_body und type_parameter_constraints_clause zu bilden, aber nicht in Attributen.

Alle Parameter und Typparameter müssen unterschiedliche Namen haben.

15.6.2 Methodenparameter

15.6.2.1 Allgemein

Die Parameter einer Methode werden ggf. durch die parameter_list der Methode deklariert.

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
    : parameter_mode_modifier
    | 'this'
    ;

parameter_mode_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

parameter_array
    : attributes? 'params' array_type identifier
    ;

Die Parameterliste besteht aus einem oder mehreren kommagetrennten Parametern, von denen nur der letzte eine parameter_array sein kann.

Ein fixed_parameter besteht aus einem optionalen Satz von Attributen (§22); einem optionalen in, , outref, oder this 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 bezeichnet die Methode als Erweiterungsmethode und ist nur für den ersten Parameter einer statischen Methode in einer nicht generischen, nicht geschachtelten statischen Klasse zulässig. Wenn es sich bei dem Parameter um einen struct Typ oder einen Typparameter handelt, der auf einen structParameter beschränkt ist, kann der this Modifizierer entweder mit dem ref Modifizierer oder in dem Modifizierer kombiniert werden, jedoch nicht mit dem out Modifizierer. Erweiterungsmethoden werden weiter in §15.6.10 beschrieben. Ein fixed_parameter mit einem default_argument wird als optionaler Parameter bezeichnet, während ein fixed_parameter ohne default_argument ein erforderlicher Parameter ist. Ein erforderlicher Parameter wird nach einem optionalen Parameter in einem parameter_list nicht angezeigt.

Ein Parameter mit einem ref, out oder this Modifizierer kann keine default_argument haben. Ein Eingabeparameter kann ein Standardargument haben. Der Ausdruck in einem Standardargument muss einer der folgenden sein:

  • ein Konstantausdruck
  • ein Ausdruck der Form new S(), bei dem S ein Werttyp ist
  • ein Ausdruck der Form default(S), bei dem S ein Werttyp ist

Der Ausdruck muss implizit durch eine Identitäts- oder nullbare Konvertierung in den Typ des Parameters konvertierbar sein.

Wenn optionale Parameter in einer implementierenden partiellen Methodendeklaration (§15.6.9), einer expliziten Schnittstellenmitgliedimplementierung (§18.6.2), einer Indexerdeklaration mit einem einzigen Parameter (§15.9) oder in einer Operatordeklaration (§15.10.1) auftreten, sollte ein Compiler eine Warnung ausgeben, da diese Member niemals so aufgerufen werden können, dass Argumente ausgelassen werden.

Ein parameter_array besteht aus einem optionalen Satz von Attributen (§22), einem params Modifizierer, einer array_type und einem Bezeichner. Ein Parameterarray deklariert einen einzelnen Parameter des angegebenen Arraytyps mit dem angegebenen Namen. Die array_type eines Parameterarrays muss ein eindimensionales Array vom Typ (§17.2) sein. Bei einem Methodenaufruf erlaubt ein Parameterarray entweder die Angabe eines einzelnen Arguments des angegebenen Arraytyps oder die Angabe von null oder mehr Argumenten des Typs der Arrayelemente. Parameterarrays werden weiter in §15.6.2.4 beschrieben.

Ein parameter_array kann nach einem optionalen Parameter auftreten, aber keinen Standardwert aufweisen – das Auslassen von Argumenten für eine parameter_array würde stattdessen zur Erstellung eines leeren Arrays führen.

Beispiel: Im Folgenden werden verschiedene Arten von Parametern veranschaulicht:

void M<T>(
    ref int i,
    decimal d,
    bool b = false,
    bool? n = false,
    string s = "Hello",
    object o = null,
    T t = default(T),
    params int[] a
) { }

In der parameter_list für M, i ist ein erforderlicher ref Parameter, d ist ein erforderlicher Wertparameter, b, s, o und t sind optionale Wertparameter und a ist ein Parameterarray.

Endbeispiel

Eine Methodendeklaration erstellt einen separaten Deklarationsraum (§7.3) für Parameter und Typparameter. Namen werden in diesen Deklarationsbereich durch die Typparameterliste und die Parameterliste der Methode eingeführt. Der Text der Methode, falls vorhanden, gilt als innerhalb dieses Deklarationsraums verschachtelt. Es ist ein Fehler, wenn zwei Member eines Methodendeklarationsraums den gleichen Namen haben.

Ein Methodenaufruf (§12.8.10.2) erstellt eine Kopie, spezifisch für diesen Aufruf, die Parameter und lokale Variablen der Methode, und die Argumentliste des Aufrufs weist den neu erstellten Parametern Werte oder Variablenverweise zu. Innerhalb des Blocks einer Methode können Parameter anhand ihrer Bezeichner in simple_name Ausdrücken (§12.8.4) referenziert werden.

Die folgenden Parametertypen sind vorhanden:

Hinweis: Wie in §7.6 beschrieben, sind die in, out und ref-Modifikatoren Teil der Signatur einer Methode, aber der params-Modifikator ist es nicht. Hinweisende

15.6.2.2 Wertparameter

Ein Parameter, der ohne Modifizierer deklariert ist, ist ein Wertparameter. Ein Wertparameter ist eine lokale Variable, die ihren Anfangswert aus dem entsprechenden Argument abruft, das im Methodenaufruf angegeben wird.

Bestimmte Zuordnungsregeln finden Sie unter §9.2.5.

Das entsprechende Argument in einem Methodenaufruf muss ein Ausdruck sein, der implizit in den Parametertyp (§10.2) konvertierbar ist.

Eine Methode darf einem Wertparameter neue Werte zuweisen. Solche Zuordnungen wirken sich nur auf den lokalen Speicherort aus, der durch den Wertparameter dargestellt wird. Sie haben keine Auswirkungen auf das tatsächliche Argument, das im Methodenaufruf angegeben wird.

15.6.2.3 By-Verweisparameter

15.6.2.3.1 Allgemein

Eingabe-, Ausgabe- und Referenzparameter sind durch-Referenz-Parameters. Ein Nachverweisparameter ist eine lokale Referenzvariable (§9.7). Der anfängliche Referent wird aus dem entsprechenden Argument abgerufen, das im Aufruf der Methode angegeben wird.

Hinweis: Der Referent eines By-Reference-Parameters kann mit dem Verweiszuweisungsoperator (= ref) geändert werden.

Wenn ein Parameter ein By-Reference-Parameter ist, besteht das entsprechende Argument in einem Methodenaufruf aus dem entsprechenden Schlüsselwort, in, , refoder out, gefolgt von einem variable_reference (§9.5) desselben Typs wie der Parameter. Wenn der Parameter jedoch ein in Parameter ist, kann es sich bei dem Argument um einen Ausdruck handeln, für den eine implizite Konvertierung (§10.2) von diesem Argumentausdruck in den Typ des entsprechenden Parameters vorhanden ist.

By-Reference-Parameter sind für Als Iterator (§15.15) oder asynchrone Funktion (§15.14) deklarierte Funktionen nicht zulässig.

In einer Methode, die mehrere Nachverweisparameter verwendet, ist es möglich, dass mehrere Namen denselben Speicherort darstellen.

15.6.2.3.2 Eingabeparameter

Ein mit einem Modifizierer deklarierter in Parameter ist ein Eingabeparameter. Das Argument, das einem Eingabeparameter entspricht, ist entweder eine Variable, die am Punkt des Methodenaufrufs vorhanden ist, oder eine variable, die durch die Implementierung (§12.6.2.3) im Aufruf der Methode erstellt wurde. Bestimmte Zuordnungsregeln finden Sie unter §9.2.8.

Es handelt sich um einen Kompilierungszeitfehler, um den Wert eines Eingabeparameters zu ändern.

Hinweis: Der Hauptzweck von Eingabeparametern ist für die Effizienz. Wenn der Typ eines Methodenparameters eine große Struktur ist (in Bezug auf den Speicherbedarf), ist es nützlich, beim Aufrufen der Methode das Kopieren des gesamten Wertes des Arguments vermeiden zu können. Eingabeparameter ermöglichen es Methoden, auf vorhandene Werte im Arbeitsspeicher zu verweisen, während gleichzeitig Schutz vor unerwünschten Änderungen an diesen Werten bereitgestellt wird. Hinweisende

15.6.2.3.3 Referenzparameter

Ein parameter, der mit einem ref Modifizierer deklariert ist, ist ein Verweisparameter. Bestimmte Zuordnungsregeln finden Sie unter §9.2.6.

Beispiel: Das Beispiel

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 = {i}, j = {j}");
    }
}

erzeugt die Ausgabe

i = 2, j = 1

Für den Aufruf von Swap in Main steht x für i und y steht für j. Daher hat der Aufruf die Auswirkung, die Werte von i und j zu vertauschen.

Endbeispiel

Beispiel: Im folgenden Code

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 eine Referenz an s für sowohl a als auch b. Daher beziehen sich für diesen Aufruf die Namen s, aund b alle auf denselben Speicherort, und die drei Zuordnungen ändern alle das Instanzfeld s.

Endbeispiel

Bei einem struct Typ verhält sich das Schlüsselwort innerhalb einer Instanzmethode, des Instanzaccessors (this) oder des Instanzkonstruktors mit einem Konstruktorinitialisierer genau als Referenzparameter des Strukturtyps (§12.8.14).

15.6.2.3.4 Ausgabeparameter

Ein parameter, der mit einem out Modifizierer deklariert ist, ist ein Ausgabeparameter. Bestimmte Zuordnungsregeln finden Sie unter §9.2.7.

Eine als Teilmethode deklarierte Methode (§15.6.9) darf keine Ausgabeparameter aufweisen.

Hinweis: Ausgabeparameter werden in der Regel in Methoden verwendet, die mehrere Rückgabewerte erzeugen. Hinweisende

Beispiel:

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 ergibt die Ausgabe:

c:\Windows\System\
hello.txt

Beachten Sie, dass die Variablen dir und name möglicherweise nicht zugewiesen sind, bevor sie an SplitPath übergeben werden, und dass sie nach dem Aufruf als definitiv zugewiesen betrachtet werden.

Endbeispiel

15.6.2.4 Parameter-Arrays

Ein Parameter, der mit einem params Modifizierer deklariert ist, ist ein Parameterarray. Wenn eine Parameterliste ein Parameterarray enthält, muss es der letzte Parameter in der Liste sein und es muss sich um einen eindimensionalen Arraytyp handeln.

Beispiel: Die Typen string[] und string[][] können als Typ eines Parameterarrays verwendet werden, der Typ string[,] kann jedoch nicht verwendet werden. Endbeispiel

Hinweis: Es ist nicht möglich, den params Modifizierer mit den Modifizierern in, out oder ref zu kombinieren. Hinweisende

Ein Parameterarray ermöglicht die Angabe von Argumenten auf eine von zwei Arten in einem Methodenaufruf:

  • Das argument für ein Parameterarray kann ein einzelner Ausdruck sein, der implizit in den Parameterarraytyp (§10.2) konvertierbar ist. In diesem Fall fungiert das Parameterarray genau wie ein Wertparameter.
  • Alternativ kann der Aufruf null oder mehr Argumente für das Parameterarray angeben, wobei jedes Argument ein Ausdruck ist, der implizit (§10.2) in den Elementtyp des Parameterarrays umgewandelt wird. In diesem Fall erstellt der Aufruf eine Instanz des Parameterarraytyps mit einer Länge, die der Anzahl der Argumente entspricht, initialisiert die Elemente der Arrayinstanz mit den angegebenen Argumentwerten und verwendet die neu erstellte Arrayinstanz als tatsächliches Argument.

Mit Ausnahme einer variablen Anzahl von Argumenten in einem Aufruf entspricht ein Parameterarray genau einem Wertparameter (§15.6.2.2.2) desselben Typs.

Beispiel: Das Beispiel

class Test
{
    static void F(params int[] args)
    {
        Console.Write($"Array contains {args.Length} elements:");
        foreach (int i in args)
        {
            Console.Write($" {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:

Der erste Aufruf des F Arrays übergibt das Array arr einfach als Wertparameter. Der zweite Aufruf von F erstellt automatisch ein Vier-Element int[] mit den angegebenen Elementwerten und übergibt diese Arrayinstanz als Wertparameter. Ebenso erstellt der dritte Aufruf von F ein Nullelement int[] und übergibt diese Instanz als Wertparameter. Die zweiten und dritten Aufrufe entsprechen genau dem Schreiben:

F(new int[] {10, 20, 30, 40});
F(new int[] {});

Endbeispiel

Bei der Überladungsauflösung kann eine Methode mit einem Parameterarray entweder in normaler Form oder in erweiterter Form (§12.6.4.2) 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.

Beispiel: Das Beispiel

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 Parameterarray bereits als reguläre Methoden in der Klasse enthalten. Diese erweiterten Formen werden daher beim Ausführen der Überladungsauflösung nicht berücksichtigt, und die ersten und dritten Methodenaufrufe wählen daher die regulären Methoden aus. Wenn eine Klasse eine Methode mit einem Parameterarray deklariert, ist es nicht ungewöhnlich, auch einige der erweiterten Formulare als normale Methoden einzuschließen. Dadurch ist es möglich, die Zuordnung einer Arrayinstanz zu vermeiden, die auftritt, wenn eine erweiterte Form einer Methode mit einem Parameterarray aufgerufen wird.

Endbeispiel

Ein Array ist ein Verweistyp, sodass der für ein Parameterarray übergebene Wert sein nullkann.

Beispiel: Das Beispiel:

class Test
{
    static void F(params string[] array) =>
        Console.WriteLine(array == null);

    static void Main()
    {
        F(null);
        F((string) null);
    }
}

erzeugt die Ausgabe:

True
False

Der zweite Aufruf erzeugt False , da er entspricht F(new string[] { null }) und übergibt ein Array, das einen einzelnen NULL-Verweis enthält.

Endbeispiel

Wenn der Typ eines Parameterarrays lautet object[], entsteht eine potenzielle Mehrdeutigkeit zwischen der Normalenform der Methode und der erweiterten Form für einen einzelnen object Parameter. Der Grund für die Mehrdeutigkeit ist, dass ein object[] selbst in den Typ object implizit konvertierbar ist. Die Mehrdeutigkeit stellt jedoch kein Problem dar, da sie bei Bedarf durch Einfügen eines Gipsverbandes behoben werden kann.

Beispiel: Das Beispiel

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

In den ersten und letzten Aufrufen von Fist die normale Form anwendbar F , da eine implizite Konvertierung vom Argumenttyp in den Parametertyp vorhanden ist (beide sind vom Typ object[]). Daher wählt die Überladungsauflösung die normale Form von F, und das Argument wird als normaler Wertparameter übergeben. In den zweiten und dritten Aufrufen ist die normale Form nicht F anwendbar, da keine implizite Konvertierung vom Argumenttyp in den Parametertyp vorhanden ist (Typ object kann nicht implizit in Typ object[]konvertiert werden). Allerdings ist die erweiterte Form von F anwendbar, so dass sie durch Überlastungsauflösung ausgewählt wird. Daher wird durch den Aufruf ein einzelnes Element object[] erstellt, und dieses eine Element des Arrays wird mit dem angegebenen Argumentwert initialisiert (der selbst ein Verweis auf einen object[] ist).

Endbeispiel

15.6.3 Statische Methoden und Instanzmethoden

Wenn eine Methodendeklaration 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 operiert nicht auf einer bestimmten Instanz, und es ist ein Kompilierfehler, sich in einer statischen Methode auf this zu beziehen.

Eine Instanzmethode wird für eine bestimmte Instanz einer Klasse ausgeführt, und auf diese Instanz kann zugegriffen werden (this§12.8.14).

Die Unterschiede zwischen statischen und Instanzmitgliedern werden in §15.3.8 weiter erörtert.

15.6.4 Virtuelle Methoden

Wenn eine Instanzmethodendeklaration einen virtuellen Modifizierer enthält, wird diese Methode als virtuelle Methode bezeichnet. Wenn kein virtueller Modifizierer vorhanden ist, wird die Methode als nicht virtuelle Methode bezeichnet.

Die Implementierung einer nicht virtuellen Methode ist invariant: Die Implementierung ist identisch, ob die Methode für eine Instanz der Klasse aufgerufen wird, in der sie deklariert wird, oder eine Instanz einer abgeleiteten Klasse. Im Gegensatz dazu kann die Implementierung einer virtuellen Methode durch abgeleitete Klassen ersetzt werden. Der Prozess der Außerkraftsetzung der Implementierung einer geerbten virtuellen Methode wird als Überschreibung dieser Methode (§15.6.5) bezeichnet.

Bei einem aufruf der virtuellen Methode bestimmt der Laufzeittyp der Instanz, für die dieser Aufruf stattfindet, die tatsächliche Methodenimplementierung, die aufgerufen werden soll. Bei einem Aufruf einer nicht virtuellen Methode ist der Kompilierungs-Zeittyp der Instanz der bestimmende Faktor. Wenn eine Methode namens N mit einer Argumentliste A auf einer Instanz mit einem Kompilierungszeittyp C und einem Laufzeittyp R aufgerufen wird (wobei R entweder C oder eine Klasse, die von C abgeleitet ist), wird der Aufruf wie folgt verarbeitet:

  • Zur Bindezeit wird die Überlastauflösung auf C, Nund Aangewendet, um eine bestimmte Methode M aus der Menge der in deklarierten und von C geerbten Methoden auszuwählen. Dies wird in §12.8.10.2 beschrieben.
  • Dann zur Laufzeit:
    • Wenn M eine nicht-virtuelle Methode ist, wird M aufgerufen.
    • M Andernfalls handelt es sich um eine virtuelle Methode, und die meistabgeleitete Implementierung von M im Hinblick auf R wird aufgerufen.

Für jede virtuelle Methode, die von einer Klasse deklariert oder geerbt wird, gibt es eine abgeleitete Implementierung der Methode in Bezug auf diese Klasse. Die am meisten abgeleitete Implementierung einer virtuellen Methode M in Bezug auf eine Klasse R wird wie folgt bestimmt:

  • Wenn R die einführende virtuelle Deklaration von M enthält, ist dies die am meisten abgeleitete Implementierung von M in Bezug auf R.
  • Andernfalls, wenn R eine Außerkraftsetzung von M enthält, ist dies die am weitesten abgeleitete Implementierung von M in Bezug auf R.
  • Andernfalls entspricht die am stärksten abgeleitete Implementierung von M hinsichtlich R der am stärksten abgeleiteten Implementierung von M bezüglich der direkten Basisklasse von R.

Beispiel: Im folgenden Beispiel werden die Unterschiede zwischen virtuellen und nicht virtuellen Methoden veranschaulicht:

class A
{
    public void F() => Console.WriteLine("A.F");
    public virtual void G() => Console.WriteLine("A.G");
}

class B : A
{
    public new 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 A wird eine nicht virtuelle Methode F und eine virtuelle Methode Geingeführt. Die Klasse B führt eine neue nicht virtuelle Methode F ein, wodurch die geerbte Methode F wird, und überschreibt außerdem die geerbte Methode . Das Beispiel ergibt die Ausgabe:

A.F
B.F
B.G
B.G

Beachten Sie, dass die Anweisung a.G()B.G und nicht A.G aufruft. Dies liegt daran, dass der Laufzeittyp der Instanz (dies ist B), nicht der Kompilierungszeittyp der Instanz (was heißt A), die tatsächliche Methodenimplementierung bestimmt, die aufgerufen werden soll.

Endbeispiel

Da Methoden geerbte Methoden ausblenden dürfen, ist es möglich, dass eine Klasse mehrere virtuelle Methoden mit derselben Signatur enthält. Dies stellt kein Mehrdeutigkeitsproblem dar, da alle Methoden bis auf die am weitesten entwickelte verborgen sind.

Beispiel: Im folgenden Code

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

class B : A
{
    public override void F() => Console.WriteLine("B.F");
}

class C : B
{
    public new 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- und D-Klassen enthalten zwei virtuelle Methoden mit der gleichen Signatur: Die eine eingeführt durch A und die andere eingeführt durch C. Die von C eingeführte Methode versteckt die von Ageerbte Methode. So überschreibt die Override-Deklaration in D die von Ceingeführte Methode, und es ist nicht möglich, dass D die von Aeingeführte Methode überschreibt. Das Beispiel ergibt die Ausgabe:

B.F
B.F
D.F
D.F

Beachten Sie, dass es möglich ist, die ausgeblendete virtuelle Methode aufzurufen, indem auf eine Instanz eines D weniger abgeleiteten Typs zugegriffen wird, in dem die Methode nicht ausgeblendet ist.

Endbeispiel

15.6.5 Außerkraftsetzungsmethoden

Wenn eine Instanzmethodendeklaration einen override Modifizierer enthält, wird die Methode als Außerkraftsetzungsmethode bezeichnet. Eine Außerkraftsetzungsmethode setzt eine geerbte virtuelle Methode mit derselben Signatur außer Kraft. Während eine virtuelle Methodendeklaration eine neue Methode einführt , ist eine Überschreibungsmethodedeklaration auf eine vorhandene geerbte virtuelle Methode spezialisiert , indem eine neue Implementierung dieser Methode bereitgestellt wird.

Die Methode, die durch eine Override-Deklaration überschrieben wird, wird als überschriebene Basismethode bezeichnet. Für eine Override-Methode M , die in einer Klasse Cdeklariert ist, wird die überschriebene Basismethode bestimmt, indem jede Basisklasse von Cuntersucht wird, beginnend mit der direkten Basisklasse von C und fortfahrend mit jeder folgenden direkten Basisklasse, bis in einem gegebenen Basisklassentyp mindestens eine zugängliche Methode gefunden wird, die die gleiche Signatur wie M nach Ersetzung der Typargumente hat. Zum Auffinden der überschriebenen Basismethode wird eine Methode als zugänglich betrachtet, wenn sie publicist, protectedist, protected internalist, oder sie entweder internal oder private protectedist und im selben Programm deklariert ist wie C.

Ein Kompilierzeitfehler tritt auf, es sei denn, alle der folgenden Punkte gelten für eine Außerkraftsetzungsdeklaration:

  • Eine überschriebene Basismethode kann wie oben beschrieben lokalisiert werden.
  • Es gibt genau eine solche überschriebene Basismethode. Diese Einschränkung ist nur wirksam, wenn der Basisklassentyp ein konstruierter Typ ist, bei dem die Ersetzung von Typargumenten die Signatur von zwei Methoden gleich macht.
  • Die Außerkraftsetzungsbasismethode ist eine virtuelle, abstrakte oder Außerkraftsetzungsmethode. Mit anderen Worten, die überschriebene Basismethode kann nicht statisch oder nicht virtuell sein.
  • Die Außerkraftsetzungsbasismethode ist keine versiegelte Methode.
  • Es gibt eine Identitätsumwandlung zwischen dem Rückgabetyp der außer Kraft gesetzten Basismethode und der Außerkraftsetzungsmethode.
  • Die Außerkraftsetzungsdeklaration und die Außerkraftsetzungsbasismethode haben die gleiche deklarierte Zugänglichkeit. Anders ausgedrückt: Eine Überschreibdeklaration kann die Zugänglichkeit der virtuellen Methode nicht ändern. Wenn die Außerkraftsetzungsbasismethode jedoch intern geschützt ist und in einer anderen Assembly als der Assembly, die die Außerkraftsetzungsdeklaration enthält, deklariert ist, ist die deklarierte Zugänglichkeit der Außerkraftsetzungsdeklaration geschützt.
  • Die Überschreibungsdeklaration gibt keine type_parameter_constraints_clauses an. Stattdessen werden die Einschränkungen von der außer Kraft gesetzten Basismethode geerbt. Einschränkungen, die Typparameter in der überschriebenen Methode sind, können durch Typargumente in der geerbten Einschränkung ersetzt werden. Dies kann zu Einschränkungen führen, die nicht gültig sind, wenn explizit angegeben, z. B. Werttypen oder versiegelte Typen.

Beispiel: Im Folgenden wird veranschaulicht, wie die überschreibenden 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>
}

Endbeispiel

Eine Überschreibungsdeklaration kann auf die überschriebene Basismethode mit einem Basiszugriff zugreifen (§12.8.15).

Beispiel: Im folgenden Code

class A
{
    int x;

    public virtual void PrintFields() => Console.WriteLine($"x = {x}");
}

class B : A
{
    int y;

    public override void PrintFields()
    {
        base.PrintFields();
        Console.WriteLine($"y = {y}");
    }
}

Der Aufruf von base.PrintFields() ruft in B die in A deklarierte PrintFields-Methode auf. Ein base_access deaktiviert den virtuellen Aufrufmechanismus und erachtet die Basismethode als nicht-virtuelle Methodevirtual. Wäre der Aufruf in B als ((A)this).PrintFields() geschrieben, würde er die in PrintFields deklarierte Methode rekursiv aufrufen und nicht die in B, da A virtuell ist und der Laufzeittyp von PrintFields((A)this) ist B.

Endbeispiel

Nur durch das Einschließen eines Modifizierers kann eine override Methode eine andere Methode außer Kraft setzen. In allen anderen Fällen blendet eine Methode mit derselben Signatur wie eine geerbte Methode einfach die geerbte Methode aus.

Beispiel: Im folgenden Code

class A
{
    public virtual void F() {}
}

class B : A
{
    public virtual void F() {} // Warning, hiding inherited F()
}

Die F-Methode in B enthält keinen override-Modifier und überschreibt die F-Methode in A daher nicht. Vielmehr verbirgt die Methode F in B die Methode in A, und es wird eine Warnung ausgegeben, weil die Deklaration keinen neuen Modifikator enthält.

Endbeispiel

Beispiel: Im folgenden Code

class A
{
    public virtual void F() {}
}

class B : A
{
    private new void F() {} // Hides A.F within body of B
}

class C : B
{
    public override void F() {} // Ok, overrides A.F
}

die Methode F in B versteckt die virtuelle Methode F , die von Ageerbt wurde. Da das neue F in B privaten Zugriff hat, umfasst sein Geltungsbereich nur den Klassenkörper von B und erstreckt sich nicht auf C. Daher darf die Deklaration von F in C das von Fgeerbte A außer Kraft setzen.

Endbeispiel

15.6.6 Versiegelte Methoden

Wenn eine Instanzmethodendeklaration einen sealed Modifizierer enthält, wird diese Methode als versiegelte Methode bezeichnet. Eine versiegelte Methode setzt eine geerbte virtuelle Methode mit derselben Signatur außer Kraft. Eine versiegelte Methode muss auch mit dem Modifikator override gekennzeichnet werden. Die Verwendung des sealed Modifizierers verhindert, dass eine abgeleitete Klasse die Methode weiter außer Kraft setzt.

Beispiel: Das Beispiel

class A
{
    public virtual void F() => Console.WriteLine("A.F");
    public virtual void G() => Console.WriteLine("A.G");
}

class B : A
{
    public sealed override void F() => Console.WriteLine("B.F");
    public override void G()        => Console.WriteLine("B.G");
}

class C : B
{
    public override void G() => Console.WriteLine("C.G");
}

Die Klasse B bietet zwei Überschreibungsmethoden: eine F -Methode, die den sealed -Modifikator hat, und eine G -Methode, die ihn nicht hat. B's Verwendung des Modifikators sealed verhindert, dass C weiterhin außer Kraft setzt F.

Endbeispiel

15.6.7 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 über den Modifizierer virtualverfügen.

Eine abstrakte Methodendeklaration führt eine neue virtuelle Methode ein, stellt jedoch keine Implementierung dieser Methode bereit. Stattdessen sind nicht abstrakte abgeleitete Klassen dazu verpflichtet, ihre eigene Implementierung bereitzustellen, indem sie diese Methode überschreiben. Da eine abstrakte Methode keine tatsächliche Implementierung bereitstellt, besteht der Methodentext einer abstrakten Methode einfach aus einem Semikolon.

Abstrakte Methodendeklarationen sind nur in abstrakten Klassen zulässig (§15.2.2.2.2).

Beispiel: Im folgenden Code

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 den abstrakten Begriff eines geometrischen Formobjekts, das sich selbst zeichnen kann. Die Paint Methode ist abstrakt, da keine sinnvolle Standardimplementierung vorhanden ist. Die Ellipse- und Box-Klassen sind konkrete Shape-Implementierungen. Da diese Klassen nicht abstrakt sind, müssen sie die Paint Methode außer Kraft setzen und eine tatsächliche Implementierung bereitstellen.

Endbeispiel

Es handelt sich um einen Kompilierungszeitfehler für einen base_access (§12.8.15), um auf eine abstrakte Methode zu verweisen.

Beispiel: Im folgenden Code

abstract class A
{
    public abstract void F();
}

class B : A
{
    // Error, base.F is abstract
    public override void F() => base.F();
}

Für den base.F() Aufruf wird ein Kompilierungsfehler gemeldet, da er auf eine abstrakte Methode verweist.

Endbeispiel

Eine abstrakte Methodendeklaration darf eine virtuelle Methode außer Kraft setzen. 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.

Beispiel: Im folgenden Code

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, eine Klasse B überschreibt diese Methode mit einer abstrakten Methode und überschreibt C die abstrakte Methode, um eine eigene Implementierung bereitzustellen.

Endbeispiel

15.6.8 Externe Methoden

Wenn eine Methodendeklaration einen extern Modifizierer enthält, wird die Methode als externe Methode bezeichnet. Externe Methoden werden extern implementiert, in der Regel wird eine andere Sprache als C# verwendet. Da eine externe Methodendeklaration keine tatsächliche Implementierung bereitstellt, besteht der Methodentext einer externen Methode einfach aus einem Semikolon. Eine externe Methode darf nicht generisch sein.

Der Mechanismus, durch den eine Verbindung mit einer externen Methode hergestellt wird, ist implementierungsspezifisch.

Beispiel: Im folgenden Beispiel wird die Verwendung des extern Modifizierers und des DllImport Attributs veranschaulicht:

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);
}

Endbeispiel

15.6.9 Teilmethoden

Wenn eine Methodendeklaration einen partial Modifizierer enthält, wird diese Methode als partielle Methode bezeichnet. Partielle Methoden dürfen nur als Mitglieder von Teiltypen (§15.2.7) deklariert werden und unterliegen einer Reihe von Einschränkungen.

Partielle Methoden können in einem Teil einer Typdeklaration definiert und in einer anderen implementiert werden. Die Umsetzung ist optional; wenn keine Komponente die partielle Methode implementiert, werden die partielle Methodendeklaration und alle Aufrufe davon aus der Typdeklaration entfernt, die sich aus der Kombination der Teile ergibt.

Teilmethoden dürfen keine Zugriffsmodifizierer definieren; sie sind implizit privat. Ihr Rückgabetyp muss sein void, und ihre Parameter dürfen keine Ausgabeparameter sein. Der Bezeichner partial wird nur dann als kontextbezogenes Schlüsselwort (§6.4.4) in einer Methodendeklaration erkannt, wenn er unmittelbar vor dem void Schlüsselwort angezeigt wird. Eine partielle Methode kann keine Schnittstellenmethoden explizit implementieren.

Es gibt zwei Arten von partiellen Methodendeklarationen: Wenn der Textkörper der Methodendeklaration ein Semikolon ist, wird die Deklaration als definierende partielle Methodendeklaration bezeichnet. Wenn der Textkörper kein Semikolon ist, wird die Deklaration als implementierende Partielle Methodendeklaration bezeichnet. In allen Teilen einer Typdeklaration darf nur eine Teilmethodendeklaration mit einer bestimmten Signatur definiert werden, und es darf höchstens nur eine Teilmethodendeklaration mit einer bestimmten Signatur implementiert werden. Wenn eine Teilmethodenerklärung zur Durchführung gegeben wird, muss eine entsprechende definitionsbezogene Teilmethodenerklärung vorhanden sein, und die Erklärungen stimmen wie in den folgenden Angaben angegeben überein:

  • Die Deklarationen müssen dieselben Modifizierer haben (auch wenn nicht notwendigerweise in derselben Reihenfolge), Methodenname, Anzahl der Typparameter und Anzahl von Parametern.
  • Die entsprechenden Parameter in den Deklarationen müssen die gleichen Modifizierer aufweisen (obwohl nicht notwendigerweise in derselben Reihenfolge) und die gleichen Typen oder Identitätsveränderertypen (Modulounterschiede bei Typparameternamen).
  • Die entsprechenden Typparameter in den Deklarationen müssen dieselben Einschränkungen aufweisen (Modulounterschiede bei Typparameternamen).

Eine implementierende partielle Methodendeklaration kann im selben Teil wie die entsprechende definierende partielle Methodendeklaration angezeigt werden.

An der Überladungsauflösung ist nur eine definierende Teilmethode beteiligt. Unabhängig davon, ob eine Implementierungsdeklaration angegeben wird, können Aufrufausdrücke in Aufrufe der partiellen Methode aufgelöst werden. Da eine partielle Methode immer zurückgibt void, sind solche Aufrufausdrücke immer Ausdrucksanweisungen. Da eine partielle Methode implizit privateist, treten solche Anweisungen immer innerhalb eines der Teile der Typdeklaration auf, innerhalb der die partielle Methode deklariert wird.

Hinweis: Die Definition des Abgleichs zum Definieren und Implementieren partieller Methodendeklarationen erfordert nicht, dass Parameternamen übereinstimmen. Dies kann zu überraschenden, wenn auch gut definierten Verhaltensweisen führen, wenn benannte Argumente (§12.6.2.1) verwendet werden. Beispiel: Angenommen, es gibt eine definierende partielle Methodendeklaration für M in einer Datei und eine implementierende partielle Methodendeklaration in einer anderen Datei:

// File P1.cs:
partial class P
{
    static partial void M(int x);
}

// File P2.cs:
partial class P
{
    static void Caller() => M(y: 0);
    static partial void M(int y) {}
}

ist ungültig , da der Aufruf den Argumentnamen aus der Implementierung und nicht die definierende Partielle Methodendeklaration verwendet.

Hinweisende

Wenn kein Teil einer Partdeklaration eine Implementierungsdeklaration für eine bestimmte partielle Methode enthält, wird jede Ausdrucksanweisung, die sie aufruft, einfach aus der kombinierten Typdeklaration entfernt. Daher hat der Aufrufausdruck, einschließlich aller Unterausdrücke, zur Laufzeit keine Auswirkung. Die partielle Methode selbst wird ebenfalls entfernt und ist kein Mitglied der kombinierten Typdeklaration.

Wenn eine Implementierungsdeklaration für eine bestimmte partielle Methode vorhanden ist, werden die Aufrufe der Partielle Methoden beibehalten. Die partielle Methode führt zu einer Methodendeklaration, die der implementierenden partiellen Methodendeklaration ähnelt, mit Ausnahme der folgenden:

  • Der partial Modifizierer ist nicht enthalten.

  • Die Attribute in der resultierenden Methodendeklaration sind die kombinierten Attribute der definierenden und implementierenden partiellen Methodendeklaration in nicht angegebener Reihenfolge. Duplikate werden nicht entfernt.

  • Die Attribute für die Parameter der resultierenden Methodendeklaration sind die kombinierten Attribute der entsprechenden Parameter der Definition und der implementierenden partiellen Methodendeklaration in nicht angegebener Reihenfolge. Duplikate werden nicht entfernt.

Wenn eine definierende Deklaration, aber keine Implementierungsdeklaration für eine partielle Methode Mangegeben wird, gelten die folgenden Einschränkungen:

  • Es ist ein Kompilierfehler, einen Delegaten von M zu erstellen (§12.8.17.5).

  • Es ist ein Kompilierfehler, innerhalb einer anonymen Funktion, die in einen Ausdrucksbaumtyp konvertiert ist, auf M zu verweisen (§8.6).

  • Ausdrücke, die im Rahmen eines Aufrufs M auftreten, wirken sich nicht auf den endgültigen Zuordnungszustand (§9.4) aus, was möglicherweise zu Kompilierungsfehlern führen kann.

  • M kann nicht der Einstiegspunkt für eine Anwendung (§7.1) sein.

Partielle Methoden sind nützlich, um einem Teil einer Typdeklaration das Verhalten eines anderen Teils anzupassen, z. B. eine, die von einem Tool generiert wird. Betrachten Sie die folgende partielle Klassendeklaration:

partial class Customer
{
    string name;

    public string Name
    {
        get => 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 Methodendeklarationen und deren Aufrufe entfernt, und die resultierende kombinierte Klassendeklaration entspricht folgendem:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set => name = value;
    }
}

Gehen Sie davon aus, dass ein weiterer Teil angegeben wird, der jedoch Implementierungsdeklarationen 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}");
}

Anschließend entspricht die resultierende kombinierte Klassendeklaration folgendem:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

15.6.10 Erweiterungsmethoden

Wenn der erste Parameter einer Methode den this Modifizierer enthält, wird diese Methode als Erweiterungsmethode bezeichnet. Erweiterungsmethoden dürfen nur in nicht generischen, nicht geschachtelten statischen Klassen deklariert werden. Der erste Parameter einer Erweiterungsmethode ist wie folgt eingeschränkt:

  • Es kann nur ein Eingabeparameter sein, wenn er einen Werttyp aufweist.
  • Es darf nur ein Referenzparameter sein, wenn er einen Werttyp hat oder einen generischen Typ hat, der auf Struktur beschränkt ist
  • Es darf kein Zeigertyp sein.

Beispiel: Im Folgenden sehen Sie ein Beispiel für eine statische Klasse, die zwei Erweiterungsmethoden deklariert:

public static class Extensions
{
    public static int ToInt32(this string s) => 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;
    }
}

Endbeispiel

Eine Erweiterungsmethode ist eine normale statische Methode. Außerdem kann eine Erweiterungsmethode, wenn ihre umschließende statische Klasse im Gültigkeitsbereich ist, mit der Syntax für den Aufruf von Instanzmethoden (§12.8.10.3) aufgerufen werden, wobei der Empfängerausdruck als erstes Argument verwendet wird.

Beispiel: Im folgenden Programm werden die oben deklarierten Erweiterungsmethoden 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 verfügbar auf string[], und die ToInt32 Methode ist verfügbar auf string, da sie als Erweiterungsmethoden deklariert wurden. Die Bedeutung des Programms ist identisch mit dem folgenden, wobei gewöhnliche statische Methodenaufrufe verwendet werden:

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));
        }
    }
}

Endbeispiel

15.6.11 Methodentext

Der Methodentext einer Methodendeklaration besteht entweder aus einem Blocktext, einem Ausdruckstext oder einem Semikolon.

Abstrakte und externe Methodendeklarationen stellen keine Methodenimplementierung bereit, sodass ihre Methodentexte einfach aus einem Semikolon bestehen. Bei jeder anderen Methode ist der Methodentext ein Block (§13.3), der die auszuführenden Anweisungen enthält, wenn diese Methode aufgerufen wird.

Der effektive Rückgabetyp einer Methode ist void , wenn der Rückgabetyp ist voidoder wenn die Methode asynchron ist und der Rückgabetyp ist «TaskType» (§15.14.1). Andernfalls ist der effektive Rückgabetyp einer nicht asynchronen Methode der Rückgabetyp, und der effektive Rückgabetyp einer asynchronen Methode mit Rückgabetyp «TaskType»<T>(§15.14.1) lautet T.

Wenn der effektive Rückgabetyp einer Methode void ist und die Methode einen Blockkörper hat, dürfen return Anweisungen (§13.10.5) im Block keinen Ausdruck enthalten. Wenn die Ausführung des Blocks einer void -Methode normal abgeschlossen wird (d. h. die Steuerung vom Ende des Methodenkörpers abläuft), kehrt diese Methode einfach zum Aufrufer zurück.

Wenn der effektive Rückgabetyp einer Methode ist void und die Methode über einen Ausdruckstext verfügt, muss der Ausdruck E ein statement_expression sein, und der Textkörper entspricht exakt einem Blockkörper des Formulars { E; }.

Bei einer Rückgabe-nach-Wert-Methode (§15.6.1) muss jede Rückgabe-Anweisung im Textkörper dieser Methode einen Ausdruck angeben, der implizit in den effektiven Rückgabetyp konvertierbar ist.

Bei einer Return-by-Ref-Methode (§15.6.1) muss jede Return-Anweisung im Körper dieser Methode einen Ausdruck angeben, dessen Typ dem effektiven Rückgabetyp entspricht und einen ref-safe-context von caller-context hat (§9.7.2).

Für Rückgabe-nach-Wert- und Rückgabe-nach-Referenz-Methoden sollte der Endpunkt des Methodenkörpers nicht erreichbar sein. Mit anderen Worten: Die Kontrolle darf nicht über das Ende des Methodentexts hinausfließen.

Beispiel: Im folgenden Code

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 wertgebende F -Methode führt zu einem Kompilierfehler, da die Kontrolle am Ende des Methodenkörpers abfließen kann. Die Methoden G und H sind korrekt, da alle möglichen Ausführungspfade in einer Rückgabeanweisung enden, die einen Rückgabewert angibt. Die I Methode ist richtig, da ihr Textkörper einem Block mit nur einer einzelnen Rückgabe-Anweisung entspricht.

Endbeispiel

15.7 Eigenschaften

15.7.1 Allgemein

Eine Eigenschaft ist ein Element, das Zugriff auf ein Merkmal eines Objekts oder einer Klasse bietet. Beispiele für Eigenschaften sind die Länge einer Zeichenfolge, der Schriftgrad, die Beschriftung eines Fensters und der Name eines Kunden. 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 Accessors zum Angeben der Anweisungen, die beim Lesen oder Schreiben ihrer Werte ausgeführt werden sollen. Eigenschaften bieten somit einen Mechanismus zum Zuordnen von Aktionen zum Lesen und Schreiben von Eigenschaften eines Objekts oder einer Klasse; ferner erlauben sie, diese Merkmale zu berechnen.

Eigenschaften werden mit property_declaration sdeklariert:

property_declaration
    : attributes? property_modifier* type member_name property_body
    | attributes? property_modifier* ref_kind type member_name ref_property_body
    ;    

property_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;
    
property_body
    : '{' accessor_declarations '}' property_initializer?
    | '=>' expression ';'
    ;

property_initializer
    : '=' variable_initializer ';'
    ;

ref_property_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.

Eine property_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten von deklarierter Sichtbarkeit (§15.3.6), der new (§15.3.5), static (§15.7.2), virtual (§15.6.4, §15.7.6), override (§15.6.5, §15.7.6), sealed (§15.6.6), abstract (§15.6.7, §15.7.6) und extern (§15.6.8). Darüber hinaus kann eine property_declaration, die direkt von einer struct_declaration enthalten wird, den readonly Modifizierer (§16.4.11) umfassen.

  • Die erste deklariert eine Eigenschaft ohne Referenzwert. Sein Wert hat den Typ Typ. Diese Art von Eigenschaft kann lesbar und/oder schreibbar sein.
  • Die zweite deklariert eine Eigenschaft mit Referenzwert. Sein Wert ist eine Variablenreferenz (§9.5), die readonlysein kann, auf eine Variable vom Typ Typ. Diese Art von Eigenschaft ist nur lesbar.

Eine property_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten von deklarierter Zugänglichkeit (§15.3.6), new (§15.3.5), static (§15.7.2), virtual (§15.6.4, §15.7.6), override (§15.6.5, §15.7.6), sealed (§15.6.6), abstract (§15.6.7, §15.7.6) und extern (§15.6.8) Modifier umfassen.

Eigenschaftsdeklarationen unterliegen den gleichen Regeln wie Methodendeklarationen (§15.6) in Bezug auf gültige Kombinationen von Modifizierern.

Der member_name (§15.6.1) gibt den Namen der Eigenschaft an. Sofern es sich bei der Eigenschaft nicht um eine explizite Schnittstellenmitgliedimplementierung handelt, ist member_name einfach ein Bezeichner. Für eine explizite Schnittstellenmemberimplementierung (§18.6.2) besteht die member_name aus einem interface_type, gefolgt von einem "." und einem Bezeichner.

Der Typ einer Eigenschaft muss mindestens so zugänglich sein wie die Eigenschaft selbst (§7.5.5).

Ein property_body kann entweder aus einem Anweisungstext oder einem Ausdruckstext bestehen. In einem Anweisungstext deklarieren accessor_declarations, die in „{“ und „}“ Token eingeschlossen sein müssen, die Accessoren (§15.7.3) der Eigenschaft. Die Accessoren geben die ausführbaren Anweisungen an, die mit dem Lesen und Schreiben der Eigenschaft verknüpft sind.

In einem property_body ist ein Ausdruckstext, der aus => gefolgt von einem AusdruckE und einem Semikolon besteht, genau äquivalent zum Anweisungstext { get { return E; } }und kann daher nur verwendet werden, um schreibgeschützte Eigenschaften zu spezifizieren, bei denen das Ergebnis des get-Accessors durch einen einzigen Ausdruck gegeben ist.

Eine property_initializer kann nur für eine automatisch implementierte Eigenschaft (§15.7.4) angegeben werden und bewirkt die Initialisierung des zugrunde liegenden Felds solcher Eigenschaften mit dem vom Ausdruck angegebenen Wert.

Ein ref_property_body kann entweder aus einem Anweisungskörper oder einem Ausdruckskörper bestehen. In einem Anweisungstext deklariert eine get_accessor_declaration den get-Accessors (§15.7.3) der Eigenschaft. Der Accessor gibt die ausführbaren Anweisungen an, die mit dem Lesen der Eigenschaft verknüpft sind.

In einem ref_property_body, in dem ein Ausdruckskörper besteht aus => gefolgt von ref, einem variable_referenceV und einem Semikolon, entspricht dies genau dem Anweisungskörper { get { return ref V; } }.

Hinweis: Obwohl die Syntax für den Zugriff auf eine Eigenschaft mit dem für ein Feld identisch ist, wird eine Eigenschaft nicht als Variable klassifiziert. Daher ist es nicht möglich, eine Eigenschaft als in, outoder ref Argument zu übergeben, es sei denn, die Eigenschaft ist ref-valued und gibt daher einen Variablenverweis (§9.7) zurück. Hinweisende

Wenn eine Eigenschaftsdeklaration einen extern Modifizierer enthält, wird die Eigenschaft als externe Eigenschaft bezeichnet. Da eine externe Eigenschaftsdeklaration keine tatsächliche Implementierung liefert, muss jede der Accessor_bodys in ihren accessor_declarations ein Semikolon sein.

15.7.2 Statische und Instanz-Eigenschaften

Wenn eine Eigenschaftsdeklaration einen static Modifizierer enthält, wird die Eigenschaft als statische Eigenschaft bezeichnet. Wenn kein static Modifizierer vorhanden ist, wird die Eigenschaft als Instanz-Eigenschaft bezeichnet.

Eine statische Eigenschaft ist keiner bestimmten Instanz zugeordnet, und es ist ein Kompilierungszeit-Fehler, wenn in den Zugriffsmethoden einer statischen Eigenschaft auf this verwiesen wird.

Eine Instanzeigenschaft ist einer bestimmten Instanz einer Klasse zugeordnet, und auf diese Instanz kann in den Accessoren dieser Eigenschaft als this (§12.8.14) zugegriffen werden.

Die Unterschiede zwischen statischen und Instanzmitgliedern werden in §15.3.8 weiter erörtert.

15.7.3 Accessoren

Hinweis: Diese Klausel gilt sowohl für Eigenschaften (§15.7) als auch für Indexer (§15.9). Die Klausel ist in Form von Eigenschaften geschrieben. Wenn Sie nach Indexern lesen, ersetzen Sie indexer/indexers durch property/properties und konsultieren Sie die Liste der Unterschiede zwischen Eigenschaften und Indexern in §15.9.2. Hinweisende

Die accessor_declarations einer Eigenschaft geben die ausführbaren Anweisungen an, die mit dem Schreiben und/oder Lesen dieser Eigenschaft verknüpft 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'
    | 'protected' 'private'
    | 'private' 'protected'
    | 'readonly'        // direct struct members only
    ;

accessor_body
    : block
    | '=>' expression ';'
    | ';' 
    ;

ref_get_accessor_declaration
    : attributes? accessor_modifier? 'get' ref_accessor_body
    ;
    
ref_accessor_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

Die Accessor-Deklarationen bestehen aus einer get-Accessor-Deklaration, einer set-Accessor-Deklaration oder beidem. Jede Accessordeklaration besteht aus optionalen Attributen, einem optionalen accessor_modifier, dem Token get oder set, gefolgt von einer accessor_body.

Für eine referenzwertige Eigenschaft besteht die ref_get_accessor_declaration aus optionalen Attributen, einem optionalen accessor_modifier, dem Token get, gefolgt von einem ref_accessor_body.

Die Verwendung von accessor_modifiers unterliegt den folgenden Einschränkungen:

  • Ein accessor_modifier darf nicht in einer Schnittstelle oder bei einer expliziten Implementierung eines Schnittstellenmembers verwendet werden.
  • Die accessor_modifierreadonly ist nur in einer property_declaration oder indexer_declaration zulässig, die direkt in einem struct_declaration enthalten ist (§16.4.11, §16.4.13).
  • Für eine Eigenschaft oder einen Indexer, der keinen override Modifikator hat, ist ein accessor_modifier nur dann erlaubt, wenn die Eigenschaft oder der Indexer sowohl einen get- als auch einen set-Accessor hat, und dann auch nur für einen dieser Accessoren erlaubt ist.
  • Für eine Eigenschaft oder einen Indexer, der einen override-Modifizierer enthält, muss ein Accessor, falls vorhanden, mit dem accessor_modifier des überschriebenen Accessors übereinstimmen.
  • Der accessor_modifier muss eine Zugänglichkeit deklarieren, die streng restriktiver ist als die deklarierte Zugänglichkeit der Eigenschaft oder des Indexers selbst. Um genau zu sein:
    • Wenn die Eigenschaft oder der Indexer eine deklarierte Zugänglichkeit von publichat, kann die durch accessor_modifier deklarierte Zugänglichkeit entweder private protected, protected internal, internal, protectedoder privatesein.
    • Wenn die Eigenschaft oder der Indexer eine deklarierte Zugänglichkeit von protected internalhat, kann die durch accessor_modifier deklarierte Zugänglichkeit entweder private protected, protected private, internal, protectedoder privatesein.
    • Wenn die Eigenschaft oder der Indexer die deklarierte Zugänglichkeit von internal oder protected hat, muss die von accessor_modifier deklarierte Zugänglichkeit entweder private protected oder private sein.
    • Wenn die Eigenschaft oder der Indexer eine deklarierte Zugänglichkeit von private protectedhat, muss die durch accessor_modifier deklarierte Zugänglichkeit privatesein.
    • Wenn die Eigenschaft oder der Indexer über eine deklarierte Zugänglichkeit von private verfügt, können keine accessor_modifier verwendet werden.

Für abstract und extern nicht-ref-bewertete Eigenschaften ist jeder accessor_body für jeden angegebenen Accessor einfach ein Semikolon. Bei einer nicht-abstrakten, nicht-externen Eigenschaft, aber nicht bei einem Indexer, kann der Accessor_body für alle angegebenen Accessoren auch ein Semikolon sein. In diesem Fall ist es eine automatisch implementierte Eigenschaft (§15.7.4). Eine automatisch implementierte Eigenschaft muss mindestens einen get-Accessor haben. Für die Accessoren anderer nicht abstrakter, nicht externer Eigenschaften ist die accessor_body entweder:

  • ein Block , der die auszuführenden Anweisungen angibt, wenn der entsprechende Accessor aufgerufen wird; oder
  • einen Ausdruckstext, der aus =>, gefolgt von einem Ausdruck und einem Semikolon, besteht und einen einzelnen Ausdruck bezeichnet, der ausgeführt wird, wenn der entsprechende Accessor aufgerufen wird.

Für abstract und extern ref-bewertete Eigenschaften ist das ref_accessor_body einfach ein Semikolon. Für den Accessor jeder anderen nicht-abstrakten, nicht-externen Eigenschaft ist der ref_accessor_body entweder:

  • ein Block , der die auszuführenden Anweisungen angibt, wenn der Get-Accessor aufgerufen wird; oder
  • ein Ausdruckskörper, der aus =>, gefolgt von ref, einem variable_reference und einem Semikolon besteht. Die Variablenreferenz wird ausgewertet, wenn der get-Accessor aufgerufen wird.

Ein Get-Accessor für eine Eigenschaft ohne Bezugswert entspricht einer parameterlosen Methode mit einem Rückgabewert des Eigenschaftstyps. Mit Ausnahme der Verwendung als Ziel einer Zuweisung wird, wenn auf eine solche Eigenschaft in einem Ausdruck verwiesen wird, der get-Accessor aufgerufen, um den Wert der Eigenschaft zu berechnen (§12.2.2).

Der Hauptteil eines get-Accessors für eine nicht referenzierte Eigenschaft muss den in § 15.6.11 beschriebenen Regeln für Methoden zur Wertrückgabe entsprechen. Insbesondere müssen alle return-Anweisungen im Texttext eines get-Accessors einen Ausdruck angeben, der implizit in den Eigenschaftstyp konvertierbar ist. Darüber hinaus darf der Endpunkt eines Get-Accessors nicht erreichbar sein.

Ein get-Accessors für eine ref-valued-Eigenschaft entspricht einer parameterlosen Methode mit einem Rückgabewert einer variable_reference zu einer Variablen des Eigenschaftstyps. Wenn auf eine solche Eigenschaft in einem Ausdruck verwiesen wird, wird der Get-Accessor aufgerufen, um den variable_reference Wert der Eigenschaft zu berechnen. Diese Variablenreferenzwird dann wie jede andere verwendet, um die referenzierte Variable zu lesen oder, bei nicht lesegeschützten Variablenreferenzen, zu schreiben, wie es der Kontext erfordert.

Beispiel: Das folgende Beispiel veranschaulicht eine neu bewertete Eigenschaft als Ziel einer Aufgabe:

class Program
{
    static int field;
    static ref int Property => ref field;

    static void Main()
    {
        field = 10;
        Console.WriteLine(Property); // Prints 10
        Property = 20;               // This invokes the get accessor, then assigns
                                     // via the resulting variable reference
        Console.WriteLine(field);    // Prints 20
    }
}

Endbeispiel

Der Hauptteil eines get-Accessors für eine neu bewertete Eigenschaft muss den Regeln für neu bewertete Methoden entsprechen, die in § 15.6.11 beschrieben sind.

Ein Set-Accessor entspricht einer Methode mit einem einzelnen Wertparameter des Eigenschaftstyps und einem void Rückgabetyp. Der implizite Parameter eines Set-Accessors wird immer benannt value. Wenn auf eine Eigenschaft als Ziel einer Zuordnung (§12.21) oder als Operand von ++ oder –- (§12.8.16, §12.9.6) verwiesen wird, wird der Set-Accessor mit einem Argument aufgerufen, das den neuen Wert (§12.21.2) bereitstellt. Der Text eines Set-Accessors muss den Regeln für void Methoden entsprechen, die in §15.6.11beschrieben sind. Insbesondere sind Rückgabeanweisungen im gesetzten Accessor-Text nicht zulässig, um einen Ausdruck anzugeben. Da ein Set-Accessor implizit einen Parameter mit dem Namen valuehat, handelt es sich um einen Kompilierungszeitfehler für eine lokale Variable oder konstante Deklaration in einem Set-Accessor, der über diesen Namen verfügt.

Basierend auf dem Vorhandensein oder Nichtvorhandensein der Get- und Set-Accessors wird eine Eigenschaft wie folgt klassifiziert:

  • Eine Eigenschaft, die sowohl einen Get-Accessor als auch einen Set-Accessor enthält, wird als Lese -/Schreibzugriffseigenschaft bezeichnet.
  • Eine Eigenschaft, die nur über einen get-Accessors verfügt, wird als schreibgeschützte Eigenschaft bezeichnet. Es ist ein Kompilierzeitfehler, dass eine schreibgeschützte Eigenschaft das Ziel einer Zuweisung ist.
  • Eine Eigenschaft, die nur einen Set-Accessor hat, wird als schreibgeschützte Eigenschaftbezeichnet. Außer als Ziel einer Zuweisung ist es ein Kompilierzeitfehler, auf eine schreibgeschützte Eigenschaft in einem Ausdruck zu verweisen.

Hinweis: Die Prä- und Postfix-Operatoren ++ und -- sowie zusammengesetzte Zuweisungsoperatoren können nicht auf schreibgeschützte Eigenschaften angewendet werden, da diese Operatoren den alten Wert ihres Operanden lesen, bevor sie den neuen schreiben. Hinweisende

Beispiel: Im folgenden Code

public class Button : Control
{
    private string caption;

    public string Caption
    {
        get => 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-Accessors der Eigenschaft Beschriftung gibt das string zurück, das im privaten caption-Feld gespeichert ist. Der set Accessor prüft, ob der neue Wert vom aktuellen Wert abweicht, und wenn ja, speichert er den neuen Wert und färbt das Steuerelement neu. Eigenschaften folgen häufig dem oben gezeigten Muster: Der Get-Accessor gibt einfach einen Wert zurück, der in einem private Feld gespeichert ist, und der Set-Accessor ändert dieses private Feld und führt dann alle zusätzlichen Aktionen aus, die erforderlich sind, um den Vollständigen Status des Objekts zu aktualisieren. In Anbetracht 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.

Endbeispiel

Die get- und set-Accessors einer Eigenschaft sind keine separaten Mitglieder, und es ist nicht möglich, die Zugriffsberechtigten einer Eigenschaft separat zu deklarieren.

Beispiel: Das Beispiel

class A
{
    private string name;

    // Error, duplicate member name
    public string Name
    { 
        get => name;
    }

    // Error, duplicate member name
    public string Name
    { 
        set => name = value;
    }
}

deklariert keine einzelne Schreib-Lese-Eigenschaft. Vielmehr werden zwei Eigenschaften mit demselben Namen deklariert, eine schreibgeschützt und eine schreibgeschützt. Da zwei Elemente, die in derselben Klasse deklariert sind, nicht denselben Namen haben können, tritt im Beispiel ein Kompilierungsfehler auf.

Endbeispiel

Wenn eine abgeleitete Klasse eine Eigenschaft mit demselben Namen wie eine geerbte Eigenschaft deklariert, blendet die abgeleitete Eigenschaft die geerbte Eigenschaft sowohl beim Lesen als auch beim Schreiben aus.

Beispiel: Im folgenden Code

class A
{
    public int P
    {
        set {...}
    }
}

class B : A
{
    public new int P
    {
        get {...}
    }
}

die Eigenschaft P in B verbirgt die Eigenschaft P in A sowohl beim Lesen als auch beim Schreiben. Demnach sind in den Statements

B b = new B();
b.P = 1;       // Error, B.P is read-only
((A)b).P = 1;  // Ok, reference to A.P

die Zuweisung an b.P führt zu einem Kompilierfehler, da die schreibgeschützte Eigenschaft P in B die schreibgeschützte Eigenschaft P in Aausblendet. Beachten Sie jedoch, dass ein Cast verwendet werden kann, um auf die versteckte Eigenschaft P zuzugreifen.

Endbeispiel

Im Gegensatz zu öffentlichen Feldern stellen Eigenschaften eine Trennung zwischen dem internen Zustand eines Objekts und seiner öffentlichen Schnittstelle bereit.

Beispiel: Betrachten Sie den folgenden Code, der eine Struktur verwendet, um einen Point Ort darzustellen.

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 => x;
    public int Y => y;
    public Point Location => new Point(x, y);
    public string Caption => caption;
}

Hier verwendet die Label Klasse zwei int Felder x und y, um den Speicherort zu speichern. Der Speicherort wird sowohl als Eigenschaft X und Y als auch als Eigenschaft Location vom Typ Point öffentlich verfügbar gemacht. Wenn es in einer zukünftigen Version von Label einfacher wird, den Speicherort intern als Point zu speichern, 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 => location.X;
    public int Y => location.Y;
    public Point Location => location;
    public string Caption => caption;
}

Hätten x und y stattdessen public readonly Felder sein müssen, wäre es unmöglich gewesen, eine solche Änderung an der Label Klasse vorzunehmen.

Endbeispiel

Hinweis: Das Verfügbarmachen des Zustands über Eigenschaften ist nicht notwendigerweise weniger effizient als das direkte Verfügbarmachen von Feldern. Wenn eine Eigenschaft nicht virtuell ist und nur eine kleine Menge Code enthält, kann die Ausführungsumgebung Aufrufe von Accessoren durch den tatsächlichen Code der Accessoren ersetzen. Dieser Prozess wird als Einlinderung bezeichnet und macht den Zugriff auf Eigenschaften so effizient wie Feldzugriff, behält jedoch die erhöhte Flexibilität von Eigenschaften bei. Hinweisende

Beispiel: Da das Aufrufen eines get-Accessors konzeptionell gleichbedeutend mit dem Lesen des Wertes eines Feldes ist, wird es als schlechter Programmierstil für get-Accessors angesehen, beobachtbare Nebenwirkungen zu haben. Im Beispiel

class Counter
{
    private int next;

    public int Next => next++;
}

der Wert der Next Eigenschaft hängt davon ab, wie oft zuvor auf die Eigenschaft zugegriffen wurde. Daher erzeugt der Zugriff auf die Eigenschaft einen feststellbaren Nebeneffekt, und die Eigenschaft sollte stattdessen als Methode implementiert werden.

Die Konvention „keine Nebenwirkungen“ für get-Accessors bedeutet nicht, dass get-Accessors immer einfach so geschrieben werden sollten, dass in Feldern gespeicherte Werte zurückgegeben werden. Tatsächlich berechnen Accessoren 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 zu feststellbaren Änderungen im Zustand des Objekts führen.

Endbeispiel

Eigenschaften können verwendet werden, um die Initialisierung einer Ressource bis zu dem Zeitpunkt zu verzögern, an dem sie zuerst referenziert wird.

Beispiel:

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 drei Eigenschaften, In, Outund Error, die die Standardeingabe, Ausgabe und Fehlergeräte darstellen. Indem diese Member als Eigenschaften zugänglich gemacht werden, kann die Console Klasse die Initialisierung verzögern, bis sie tatsächlich verwendet werden. Beispiel: Beim ersten Verweisen auf die Out Eigenschaft wie in

Console.Out.WriteLine("hello, world");

wird das zugrunde liegende TextWriter für das Ausgabegerät erstellt. Wenn die Anwendung jedoch keinen Verweis auf die In Und-Eigenschaften Error macht, werden keine Objekte für diese Geräte erstellt.

Endbeispiel

15.7.4 Automatisch implementierte Eigenschaften

Eine automatisch implementierte Eigenschaft (oder kurz Auto-Eigenschaft) ist eine nicht-abstrakte, nicht-externe, nicht-ref-bewertete Eigenschaft mit nur Semikolon accessor_bodys. Auto-Eigenschaften müssen einen get-Accessors haben und können optional einen set-Accessors haben.

Wenn eine Eigenschaft als automatisch implementierte Eigenschaft angegeben wird, steht automatisch ein ausgeblendetes Sicherungsfeld für die Eigenschaft zur Verfügung, und die Accessoren werden implementiert, um aus diesem Sicherungsfeld zu lesen und zu schreiben. Das ausgeblendete Unterstützungsfeld ist unzugänglich, es kann nur über die automatisch implementierten Eigenschaften-Accessors gelesen und geschrieben werden, auch innerhalb des Containertyps. Wenn die Auto-Eigenschaft keinen set-Accessor hat, wird das hinterlegte Feld als readonly betrachtet (§15.5.3). Genau wie ein readonly -Feld kann auch eine schreibgeschützte Auto-Eigenschaft im Body eines Konstruktors der umschließenden Klasse zugewiesen werden. Eine solche Abtretung geht direkt an den read-only Hintergrundfeld der Immobilie.

Eine Auto-Eigenschaft kann optional mit einer eigenschafts_initialisator, direkt auf das Hintergrundfeld als Variablen_Initialisierer (§17.7).

Beispiel:

public class Point
{
    public int X { get; set; } // Automatically implemented
    public int Y { get; set; } // Automatically implemented
}

entspricht der folgenden Deklaration:

public class Point
{
    private int x;
    private int y;

    public int X { get { return x; } set { x = value; } }
    public int Y { get { return y; } set { y = value; } }
}

Endbeispiel

Beispiel: Im folgenden Beispiel

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;
    }
}

Die Zuweisungen zum schreibgeschützten Feld sind gültig, da sie innerhalb des Konstruktors auftreten.

Endbeispiel

Obwohl das Hintergrundfeld ausgeblendet ist, können diesem Feld über die property_declaration der automatisch implementierten Eigenschaft feldspezifische Attribute direkt zugewiesen werden (§15.7.1).

Beispiel: Der folgende Code

[Serializable]
public class Foo
{
    [field: NonSerialized]
    public string MySecret { get; set; }
}

führt dazu, dass das feldorientierte Attribut NonSerialized auf das vom Compiler generierte Sicherungsfeld angewendet wird, als ob der Code wie folgt geschrieben wurde:

[Serializable]
public class Foo
{
    [NonSerialized]
    private string _mySecretBackingField;
    public string MySecret
    {
        get { return _mySecretBackingField; }
        set { _mySecretBackingField = value; }
    }
}

Endbeispiel

15.7.5 Barrierefreiheit

Wenn ein Accessor über eine accessor_modifier verfügt, wird die Barrierefreiheitsdomäne (§7.5.3) des Accessors mithilfe der deklarierten Barrierefreiheit der accessor_modifier bestimmt. Wenn ein Accessor über keine accessor_modifier verfügt, wird die Barrierefreiheitsdomäne des Accessors anhand der deklarierten Barrierefreiheit der Eigenschaft oder des Indexers bestimmt.

Das Vorhandensein eines accessor_modifier hat keinen Einfluss auf die Suche nach Mitgliedern (§12.5) oder die Auflösung von Überlasten (§12.6.4). Die Modifizierer für die Eigenschaft oder den Indexer bestimmen immer, an welche Eigenschaft oder welchen Indexer sie unabhängig vom Kontext des Zugriffs gebunden sind.

Nachdem eine bestimmte nicht referenzwertige Eigenschaft oder ein nicht referenzwertiger Indexer ausgewählt wurde, werden die Zugriffsbereiche der beteiligten Accessoren verwendet, um festzustellen, ob diese Verwendung gültig ist.

  • Wenn die Nutzung als Wert (§12.2.2) erfolgt, muss der Get-Accessor vorhanden und zugänglich sein.
  • Wenn die Verwendung als Ziel einer einfachen Zuweisung (§12.21.2) erfolgt, muss der set-Accessor existieren und zugänglich sein.
  • Ist die Verwendung als Ziel der zusammengesetzten Zuordnung (§12.21.4) oder als Ziel der ++- oder ---Operatoren (§12.8.16, §12.9.6), müssen sowohl die Get-Accessoren als auch der Set-Accessor vorhanden und zugänglich sein.

Beispiel: Im folgenden Beispiel wird die Eigenschaft A.Text durch die Eigenschaft B.Text ausgeblendet, auch in Kontexten, in denen nur der Set-Accessor aufgerufen wird. Hingegen ist die Eigenschaft B.Count für die Klasse M nicht zugänglich, sodass stattdessen die zugängliche Eigenschaft A.Count verwendet wird.

class A
{
    public string Text
    {
        get => "hello";
        set { }
    }

    public int Count
    {
        get => 5;
        set { }
    }
}

class B : A
{
    private string text = "goodbye";
    private int count = 0;

    public new string Text
    {
        get => text;
        protected set => text = value;
    }

    protected new int Count
    {
        get => 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
    }
}

Endbeispiel

Sobald eine bestimmte referenzwertige Eigenschaft oder ein verweiswertiger Indexer ausgewählt wurde – egal, ob die Verwendung als Wert, als Ziel einer einfachen Zuordnung oder als Ziel einer zusammengesetzten Zuordnung erfolgt – wird die Zugriffsdomain des beteiligten Zugriffsmodifikators verwendet, um festzustellen, ob diese Verwendung gültig ist.

Ein Accessor, der zum Implementieren einer Schnittstelle verwendet wird, darf keine accessor_modifier haben. Wenn nur ein Accessor zum Implementieren einer Schnittstelle verwendet wird, kann der andere Accessor mit einem accessor_modifier deklariert werden:

Beispiel:

public interface I
{
    string Prop { get; }
}

public class C : I
{
    public string Prop
    {
        get => "April";     // Must not have a modifier here
        internal set {...}  // Ok, because I.Prop has no set accessor
    }
}

Endbeispiel

15.7.6 Virtuelle, versiegelte, überschreibende und abstrakte Accessors

Hinweis: Diese Klausel gilt sowohl für Eigenschaften (§15.7) als auch für Indexer (§15.9). Die Klausel ist in Form von Eigenschaften geschrieben. Wenn Sie nach Indexern lesen, ersetzen Sie indexer/indexers durch property/properties und konsultieren Sie die Liste der Unterschiede zwischen Eigenschaften und Indexern in §15.9.2. Hinweisende

Eine Deklaration einer virtuellen Eigenschaft gibt an, dass die Accessoren der Eigenschaft virtuell sind. Der Modifikator virtual gilt für alle nicht-privaten Accessors einer Eigenschaft. Wenn ein Accessor einer virtuellen Eigenschaft über die privateaccessor_modifier verfügt, ist der private Accessor implizit nicht virtuell.

Eine abstrakte Eigenschaftsdeklaration gibt an, dass die Accessoren der Eigenschaft virtuell sind, aber keine tatsächliche Implementierung der Accessoren bereitstellt. Stattdessen müssen nicht abstrakte abgeleitete Klassen eine eigene Implementierung für die Accessors bereitstellen, indem sie die Eigenschaft überschreiben. Da ein Accessor für eine abstrakte Eigenschaftsdeklaration keine tatsächliche Implementierung bereitstellt, besteht die accessor_body einfach aus einem Semikolon. Eine abstrakte Eigenschaft darf keinen private-Accessor haben.

Eine Eigenschaftsdeklaration, die sowohl die Modifizierer als auch die abstractoverride Eigenschaft enthält, gibt an, dass die Eigenschaft abstrakt ist und eine Basiseigenschaft überschreibt. Die Accessors einer solchen Eigenschaft sind ebenfalls abstrakt.

Abstrakte Eigenschaftendeklarationen sind nur in abstrakten Klassen zulässig (§15.2.2.2.2). Die Accessors einer geerbten virtuellen Eigenschaft können in einer abgeleiteten Klasse überschrieben werden, indem eine Eigenschaftsdeklaration aufgenommen wird, die eine override-Anweisung angibt. Dies wird als überschreibende Eigenschaftsdeklaration bezeichnet. Eine überschreibende Eigenschaftsdeklaration deklariert keine neue Eigenschaft. Stattdessen ist sie einfach auf die Implementierungen der Accessoren einer vorhandenen virtuellen Eigenschaft spezialisiert.

Die Überschreibungsdeklaration und die überschriebene Basiseigenschaft müssen die gleiche deklarierte Zugänglichkeit aufweisen. Mit anderen Worten, eine Außerkraftsetzungsdeklaration ändert nichts an der Zugänglichkeit der Basiseigenschaft. Wenn die Außerkraftsetzungsbasiseigenschaft jedoch intern geschützt ist und in einer anderen Assembly als der Assembly, die die Außerkraftsetzungsdeklaration enthält, deklariert ist, ist die deklarierte Accessibility der Außerkraftsetzungsdeklaration geschützt. Wenn die geerbte Eigenschaft nur über einen einzelnen Accessor verfügt (d. h., wenn die geerbte Eigenschaft entweder schreibgeschützt oder nur schreibend ist), darf die überschriebene Eigenschaft nur diesen Accessor enthalten. Wenn die geerbte Eigenschaft beide Accessoren enthält (d. h., wenn die geerbte Eigenschaft Lese- und Schreibzugriff hat), kann die überschreibende Eigenschaft entweder einen einzelnen Accessor oder beide Accessoren enthalten. Es muss eine Identitätsumwandlung zwischen dem Typ der überschreibenden und der geerbten Eigenschaft erfolgen.

Eine überschreibende Eigenschaftsdeklaration kann den sealed Modifier enthalten. Die Verwendung dieses Modifizierers verhindert, dass eine abgeleitete Klasse die Eigenschaft weiter überschreibt. Die Accessors einer versiegelten Eigenschaft sind ebenfalls versiegelt.

Mit Ausnahme von Unterschieden bei der Deklarations- und Aufrufssyntax verhalten sich virtuelle, versiegelte, überschreibende und abstrakte Accessoren genau wie virtuelle, versiegelte, überschreibende und abstrakte Methoden. Insbesondere gelten die in §15.6.4, §15.6.5, §15.6.6 und §15.6.7 beschriebenen Regeln so, als wären Accessoren Methoden einer entsprechenden Form:

  • Ein get-Accessors entspricht einer parameterlosen Methode mit einem Rückgabewert des Eigenschaftstyps und den gleichen Modifikatoren wie die enthaltende Eigenschaft.
  • Ein set-Accessor entspricht einer Methode mit einem einzelnen Wertparameter des Eigenschaftstyps, einem Void-Rückgabetyp und den gleichen Modifikatoren wie die enthaltende Eigenschaft.

Beispiel: Im folgenden Code

abstract class A
{
    int y;

    public virtual int X
    {
        get => 0;
    }

    public virtual int Y
    {
        get => y;
        set => y = value;
    }

    public abstract int Z { get; set; }
}

X ist eine virtuelle Nur-Lese-Eigenschaft, Y ist eine virtuelle Lese-Schreib-Eigenschaft und Z ist eine abstrakte Lese-Schreib-Eigenschaft. Da Z abstrakt ist, muss die enthaltende Klasse A ebenfalls als abstrakt deklariert werden.

Eine Klasse, die von A der abgeleitet wird, wird unten gezeigt:

class B : A
{
    int z;

    public override int X
    {
        get => base.X + 1;
    }

    public override int Y
    {
        set => base.Y = value < 0 ? 0: value;
    }

    public override int Z
    {
        get => z;
        set => z = value;
    }
}

Hier sind die Deklarationen von X, Yund Z übergeordnete Eigenschaftsdeklarationen. Jede Eigenschaftsdeklaration stimmt exakt mit den Zugriffsmodifizierern, dem Typ und dem Namen der entsprechenden geerbten Eigenschaft überein. Der get-Accessors von X und der set-Accessors von Y verwenden das Basis-Schlüsselwort, um auf die geerbten Accessors zuzugreifen. Die Deklaration von Z setzt beide abstrakten Accessoren außer Kraft. Daher gibt es keine ausstehenden abstract-Funktionsmitglieder in B, und B darf eine nicht-abstrakte Klasse sein.

Endbeispiel

Wenn eine Eigenschaft als Außerkraftsetzung deklariert wird, müssen alle außer Kraft gesetzten Accessors auf den Außerkraftsetzungscode zugreifen können. Darüber hinaus muss die deklarierte Accessibility sowohl der Eigenschaft oder des Indexers selbst als auch der Accessors mit der des überschriebenen Mitglieds und der Accessors übereinstimmen.

Beispiel:

public class B
{
    public virtual int P
    {
        get {...}
        protected set {...}
    }
}

public class D: B
{
    public override int P
    {
        get {...}            // Must not have a modifier here
        protected set {...}  // Must specify protected here
    }
}

Endbeispiel

15.8 Veranstaltungen

15.8.1 Allgemein

Ein Ereignis ist ein Member, der es einem Objekt oder einer Klasse ermöglicht, Benachrichtigungen bereitzustellen. Clients können Ereignissen ausführbaren Code hinzufügen, indem Sie Ereignishandler bereitstellen.

Ereignisse werden mit event_declarations 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'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;

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
    ;

unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.

Eine event_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten der deklarierten Zugänglichkeit (§15.3.6), die new (§15.3.5), static (§15.6.3, §15.8.4), virtual (§15.6.4, §15.8.5), override (§15.6.5, §15.8.5), sealed (§15.6.6), abstract (§15.6.7, §15.8.5) und extern (§15.6.8) Modifikatoren umfassen. " Darüber hinaus kann ein event_declaration, das direkt in einer struct_declaration enthalten ist, den readonly Modifikator (§16.4.12) umfassen.

Ereignisdeklarationen unterliegen den gleichen Regeln wie Methodendeklarationen (§15.6) in Bezug auf gültige Kombinationen von Modifizierern.

Die Art einer Ereigniserklärung muss eine delegate_type (§8.2.8) sein und dass delegate_type mindestens so zugänglich sein muss wie das Ereignis selbst (§7.5.5).

Eine Ereigniserklärung kann event_accessor_declarations enthalten. Wenn dies jedoch nicht der Fall ist, muss der Compiler für nicht externe, nicht abstrakte Ereignisse diese automatisch bereitstellen (§15.8.2); für extern-Ereignisse werden die Accessoren extern bereitgestellt.

Eine Ereignisdeklaration, die event_accessor_declarations auslässt, definiert ein oder mehrere Ereignisse – eines für jeden variable_declarator. Die Attribute und Modifizierer gelten für alle Mitglieder, die durch eine event_declaration deklariert wurden.

Es handelt sich um einen Kompilierzeitfehler, wenn eine event_declaration sowohl den abstract Modifizierer als auch event_accessor_declarations einschließt.

Wenn eine Ereignisdeklaration einen extern Modifizierer enthält, wird das Ereignis als externes Ereignis bezeichnet. Da eine externe Ereignisdeklaration keine tatsächliche Implementierung bereitstellt, handelt es sich um einen Fehler, der sowohl den extern Modifizierer als auch event_accessor_declarationenthält.

Es ist ein Kompilierzeitfehler, wenn ein variabler_deklarator einer Ereignisdeklaration mit einem abstract oder external Modifikator einen variablen_initializerenthält.

Ein Ereignis kann als linker Operand der Operatoren += und -= verwendet werden. Diese Operatoren werden verwendet, um Ereignishandler anzufügen oder Ereignishandler aus einem Ereignis zu entfernen, und die Zugriffsmodifizierer des Ereignisses steuern die Kontexte, in denen solche Vorgänge zulässig sind.

Die einzigen Vorgänge, die für ein Ereignis durch Code zulässig sind, der sich außerhalb des Typs befindet, in dem dieses Ereignis deklariert wird, sind += und -=. Daher kann ein solcher Code Handler für ein Ereignis hinzufügen und entfernen, nicht direkt die zugrunde liegende Liste der Ereignishandler abrufen oder ändern.

Bei einer Operation der Form x += y oder x –= y, wenn x ein Ereignis ist, hat das Ergebnis der Operation den Typ void (§12.21.5), im Gegensatz zum Typ von x, mit dem Wert von x nach der Zuordnung, wie bei anderen +=- und -=-Operatoren, die für Nicht-Ereignistypen definiert sind. Dadurch wird verhindert, dass externer Code indirekt den zugrunde liegenden Delegat eines Ereignisses untersucht.

Beispiel: Das folgende Beispiel zeigt, wie Ereignishandler an Instanzen der Button Klasse angefügt werden:

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.

Endbeispiel

15.8.2 Feldähnliche Ereignisse

Innerhalb des Programmtexts 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 abstrakt oder extern sein und darf nicht ausdrücklich event_accessor_declarations enthalten. Ein solches Ereignis kann in jedem Kontext verwendet werden, der ein Feld zulässt. Das Feld enthält einen Delegaten (§20), der sich auf die Liste der Ereignishandler bezieht, die dem Ereignis hinzugefügt wurden. Wenn keine Ereignishandler hinzugefügt wurden, enthält das Feld null.

Beispiel: Im folgenden Code

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;

    protected void OnClick(EventArgs e)
    {
        EventHandler handler = Click;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    public void Reset() => Click = null;
}

Click wird als Feld innerhalb der Button Klasse verwendet. Wie das Beispiel zeigt, kann das Feld untersucht, geändert und in Stellvertretungsaufrufausdrücken verwendet werden. Die OnClick Methode in der Button Klasse "löst" das Click Ereignis aus. 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 Delegat-Aufruf eine Überprüfung vorangestellt wird, die sicherstellt, dass das Delegat nicht Null ist und dass die Überprüfung auf einer lokalen Kopie durchgeführt wird, um die Threadsicherheit zu gewährleisten.

Außerhalb der Deklaration der Button Klasse kann das Click Mitglied nur auf der linken Seite der += und –= Operatoren verwendet werden, wie in ...

b.Click += new EventHandler(...);

die einen Delegaten an die Aufrufliste des Ereignisses Click anhängt, und

Click –= new EventHandler(...);

wodurch ein Delegat aus der Aufrufliste des Click Ereignisses entfernt wird.

Endbeispiel

Beim Kompilieren eines feldähnlichen Ereignisses erstellt ein Compiler automatisch Speicherplatz für den Delegaten und erstellt Accessoren für das Ereignis, die Ereignishandler dem Delegatfeld hinzufügen oder entfernen. Die Hinzufügungs- und Entfernungsoperationen sind thread-sicher und können (müssen aber nicht) durchgeführt werden, während die Sperre (§13.13) auf das enthaltende Objekt für ein Instanzereignis oder das System.Type Objekt (§12.8.18) für ein statisches Ereignis gehalten wird.

Hinweis: Eine Instanzereignisdeklaration des Formulars:

class X
{
    public event D Ev;
}

muss so zusammengestellt werden, dass es Folgendes entspricht:

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 */
        }
    }
}

Innerhalb der Klasse X führen Verweise auf Ev auf der linken Seite der Operatoren += und –= dazu, dass die Accessoren zum Hinzufügen und Entfernen aufgerufen werden. Alle anderen Verweise auf Ev werden so kompiliert, dass sie stattdessen auf das ausgeblendete Feld __Ev verweisen (§12.8.7). Der Name "__Ev" ist beliebig; das ausgeblendete Feld könnte überhaupt einen Namen oder keinen Namen haben.

Hinweisende

15.8.3 Ereignis-Accessors

Hinweis: Ereignisdeklarationen lassen in der Regel event_accessor_declarations aus, wie im Button obigen Beispiel gezeigt. Sie können beispielsweise einbezogen werden, wenn die Speicherkosten eines Felds pro Ereignis nicht zulässig sind. In solchen Fällen kann eine Klasse event_accessor_declarationenthalten und einen privaten Mechanismus zum Speichern der Liste der Ereignishandler verwenden. Hinweisende

Die event_accessor_declarations eines Ereignisses geben die ausführbaren Anweisungen an, die dem Hinzufügen und Entfernen von Ereignishandlern zugeordnet sind.

Die Accessordeklarationen bestehen aus einem add_accessor_declaration und einem remove_accessor_declaration. Jede Accessordeklaration besteht aus dem Token-Hinzufügen oder Entfernen gefolgt von einem Block. Der einem add_accessor_declaration zugeordnete Block gibt die auszuführenden Anweisungen an, wenn ein Ereignishandler hinzugefügt wird, und der einem remove_accessor_declaration zugeordnete Block gibt die auszuführenden Anweisungen an, wenn ein Ereignishandler entfernt wird.

Jede add_accessor_declaration und remove_accessor_declaration entspricht einer Methode mit einem einzelnen Wertparameter des Ereignistyps und einem void Rückgabetyp. Der implizite Parameter eines Ereignisaccessors wird benannt value. Wenn ein Ereignis in einer Ereigniszuweisung verwendet wird, wird der entsprechende Ereignis-Accessor verwendet. Wenn der Zuordnungsoperator += ist, wird der Hinzufügen-Accessor verwendet. Wenn der Zuordnungsoperator –= ist, wird der Entfernen-Accessor verwendet. In beiden Fällen wird der rechte Operand des Zuweisungsoperators als Argument für den Ereignis-Accessor verwendet. Der Block einer add_accessor_declaration oder einer remove_accessor_declaration muss den Regeln für void Methoden entsprechen, wie in §15.6.9 beschrieben. Insbesondere dürfen die return -Anweisungen in einem solchen Block keinen Ausdruck angeben.

Da ein Ereignisaccessor implizit einen Parameter mit dem Namen valuehat, handelt es sich um einen Kompilierungszeitfehler für eine lokale Variable oder Konstante, die in einem Ereignisaccessor deklariert ist, um diesen Namen zu haben.

Beispiel: Im folgenden Code


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 AddEventHandler-Methode ordnet einen Delegatwert einem Schlüssel zu, die GetEventHandler-Methode gibt den Delegaten zurück, der derzeit einem Schlüssel zugeordnet ist, 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 Nulldelegatwerts zu einem Schlüssel gibt, und somit verbrauchen unbehandelte Ereignisse keinen Speicher.

Endbeispiel

15.8.4 Statische Ereignisse und Instanzereignisse

Wenn eine Ereignisdeklaration 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 keiner bestimmten Instanz zugeordnet, und es ist ein Fehler zur Kompilierungszeit, in den Accessoren eines statischen Ereignisses auf this zu verweisen.

Ein Instanzereignis ist einer bestimmten Instanz einer Klasse zugeordnet, und auf diese Instanz kann in den Accessoren dieses Ereignisses als this (§12.8.14) zugegriffen werden.

Die Unterschiede zwischen statischen und Instanzmitgliedern werden in §15.3.8 weiter erörtert.

15.8.5 Virtuelle, versiegelte, überschreibende und abstrakte Accessors

Eine virtuelle Ereignisdeklaration gibt an, dass die Accessoren dieses Ereignisses virtuell sind. Der Modifikator virtual gilt für beide Accessors eines Ereignisses.

Eine abstrakte Ereignisdeklaration gibt an, dass die Accessoren des Ereignisses virtuell sind, aber keine tatsächliche Implementierung der Accessoren bereitstellt. Stattdessen müssen nicht abstrakte abgeleitete Klassen eine eigene Implementierung für die Accessors bereitstellen, indem sie das Ereignis überschreiben. Da ein Accessor für eine abstrakte Ereignisdeklaration keine tatsächliche Implementierung bereitstellt, stellt er event_accessor_declarations nicht bereit.

Eine Ereignisdeklaration, die sowohl die abstract- als auch die override-Modifizierer enthält, gibt an, dass das Ereignis abstrakt ist und ein Basisereignis überschreibt. Die Teilnehmer eines solchen Ereignisses sind ebenfalls abstrakt.

Abstrakte Ereignisdeklarationen sind nur in abstrakten Klassen zulässig (§15.2.2.2.2).

Die Accessoren eines geerbten virtuellen Ereignisses können in einer abgeleiteten Klasse überschrieben werden, indem eine Ereignisdeklaration eingeschlossen wird, die einen override Modifizierer angibt. Dies wird als Deklaration eines überschreibenden Ereignisses bezeichnet. Eine überschreibende Ereignisdeklaration deklariert kein neues Ereignis. Stattdessen ist es einfach auf die Implementierungen der Accessoren eines vorhandenen virtuellen Ereignisses spezialisiert.

Eine überschreibende Ereignisdeklaration muss genau die gleichen Zugriffsmodifizierer und den Namen wie das überschriebenes Ereignis angeben, es muss eine Identitätskonvertierung zwischen dem Typ des überschreibenden und des überschriebenen Ereignisses vorhanden sein, und sowohl die add- als auch die remove-Zugriffsoperatoren müssen in der Deklaration angegeben werden.

Eine überschreibende Ereignisdeklaration kann den sealed Modifizierer enthalten. Die Verwendung des this Modifizierers verhindert, dass eine abgeleitete Klasse das Ereignis weiter überschreibt. Die Accessors eines versiegelten Ereignis sind ebenfalls versiegelt.

Es ist ein Kompilierungszeitfehler, wenn eine überschreibende Ereignisdeklaration einen new Modifizierer enthält.

Mit Ausnahme von Unterschieden bei der Deklarations- und Aufrufssyntax verhalten sich virtuelle, versiegelte, überschreibende und abstrakte Accessoren genau wie virtuelle, versiegelte, überschreibende und abstrakte Methoden. Insbesondere gelten die in §15.6.4, §15.6.5, §15.6.6 und §15.6.7 beschriebenen Regeln, als wären Accessoren Methoden eines entsprechenden Formulars. Jeder Accessor entspricht einer Methode mit einem einzelnen Wertparameter des Ereignistyps, einem void Rückgabetyp und denselben Modifizierern wie das enthaltende Ereignis.

15.9 Indexer

15.9.1 Allgemein

Ein Indexer ist ein Element, mit dem ein Objekt auf die gleiche Weise indiziert werden kann wie ein Array. Indexer werden mit indexer_declarations deklariert:

indexer_declaration
    : attributes? indexer_modifier* indexer_declarator indexer_body
    | attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
    ;

indexer_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;

indexer_declarator
    : type 'this' '[' parameter_list ']'
    | type interface_type '.' 'this' '[' parameter_list ']'
    ;

indexer_body
    : '{' accessor_declarations '}' 
    | '=>' expression ';'
    ;  

ref_indexer_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.

Ein indexer_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten der erklärten Zugänglichkeit (§15.3.6), new (§15.3.5), virtual (§15.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7) und extern (§15.6.8) Modifizierern enthalten. Zusätzlich kann ein indexer_declaration, der direkt in einem struct_declaration enthalten ist, den readonly Modifizierer (§16.4.12) enthalten.

  • Die erste deklariert einen Indexer ohne Referenzwert. Sein Wert hat den Typ Typ. Diese Art von Indexer kann lesbar und/oder schreibbar sein.
  • Die zweite deklariert einen Referenzwertindexer. Sein Wert ist eine Variablenreferenz (§9.5), die readonlysein kann, auf eine Variable vom Typ Typ. Diese Art von Indexer ist nur lesbar.

Eine indexer_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten von deklarierter Zugänglichkeit (§15.3.6), new (§15.3.5), virtual (§15.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7) und extern (§15.6.8) Modifizierer enthalten.

Indexerdeklarationen unterliegen den gleichen Regeln wie Methodendeklarationen (§15.6) in Bezug auf gültige Kombinationen von Modifizierern, wobei eine Ausnahme darin besteht, dass der static Modifizierer für eine Indexerdeklaration nicht zulässig ist.

Der Typ einer Indexerdeklaration gibt den Elementtyp des in der Deklaration eingeführten Indexers an.

Hinweis: Da Indexer für die Verwendung in Arrayelement-ähnlichen Kontexten konzipiert sind, wird der Begriff Elementtyp, wie er für ein Array definiert ist, auch mit einem Indexer verwendet. Hinweisende

Sofern der Indexer keine explizite Schnittstellenmemberimplementierung ist, folgt dem Typ das Schlüsselwort this. Für eine explizite Schnittstellenmemberimplementierung folgt auf den `type` ein `interface_type`, ein „.“, und das Schlüsselwort `this`. Im Gegensatz zu anderen Mitgliedern haben Indexer keine benutzerdefinierten Namen.

Die parameter_list gibt die Parameter des Indexers an. Die Parameterliste eines Indexers entspricht der einer Methode (§15.6.2), mit der Ausnahme, dass mindestens ein Parameter angegeben werden muss und dass die thisModifizierer refout und Parametermodifizierer nicht zulässig sind.

Der Typ eines Indexers und jeder der typen, auf die in der parameter_list verwiesen wird, muss mindestens so zugänglich sein wie der Indexer selbst (§7.5.5).

Ein indexer_body kann entweder aus einem Anweisungstext (§15.7.1) oder einem Ausdruckstext (§15.6.1) bestehen. In einem Anweisungstext deklarieren accessor_declarations, die in „{“ und „}“ Token eingeschlossen sein müssen, die Accessoren (§15.7.3) des Indexers. Die Accessoren geben die ausführbaren Anweisungen an, die dem Lesen und Schreiben von Indexerelementen zugeordnet sind.

In einem indexer_body ist ein Ausdruckstext, der aus „=>“, gefolgt von einem Ausdruck E und einem Semikolon besteht, genau gleichbedeutend mit dem Anweisungstext { get { return E; } } und kann daher nur verwendet werden, um schreibgeschützte Indexer anzugeben, bei denen das Ergebnis des get-Accessors durch einen einzelnen Ausdruck angegeben wird.

Ein ref_indexer_body kann entweder aus einem Anweisungskörper oder einem Ausdruckskörper bestehen. In einem Anweisungsblock deklariert eine get_accessor_declaration den Get-Accessor (§15.7.3) des Indexers. Der Accessor gibt die ausführbaren Anweisungen an, die mit dem Lesen des Indexers verknüpft sind.

In einem ref_indexer_body ist ein Ausdruckskörper, bestehend aus =>, gefolgt von ref, einem variablen_verweisV und einem Semikolon, genau gleichwertig mit dem Anweisungskörper { get { return ref V; } }.

Hinweis: Obwohl die Syntax für den Zugriff auf ein Indexerelement mit dem für ein Arrayelement identisch ist, wird ein Indexerelement nicht als Variable klassifiziert. Daher ist es nicht möglich, ein Indexerelement als in, outoder ref Argument zu übergeben, es sei denn, der Indexer ist ref-valued und gibt daher einen Verweis zurück (§9.7). Hinweisende

Die parameter_list eines Indexers definiert die Signatur (§7.6) des Indexers. Insbesondere besteht die Signatur eines Indexers aus der Anzahl und den Typen seiner Parameter. Der Elementtyp und die Namen der Parameter sind nicht Teil der Signatur eines Indexers.

Die Signatur eines Indexers unterscheidet sich von den Signaturen aller anderen in derselben Klasse deklarierten Indexer.

Wenn eine Indexerdeklaration einen extern Modifizierer enthält, wird der Indexer als externer Indexer bezeichnet. Da eine externe Indexerdeklaration keine tatsächliche Implementierung bereitstellt, muss jedes accessor_body in seinen accessor_declarations ein Semikolon sein.

Beispiel: Im folgenden Beispiel wird eine BitArray Klasse deklariert, die einen Indexer für den Zugriff auf die einzelnen Bits im Bitarray implementiert.

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 => 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 verbraucht wesentlich weniger Arbeitsspeicher als eine entsprechende bool[] (da jeder Wert des Ersteren nur ein Bit anstelle der Letzten byte belegt), aber sie erlaubt die gleichen Operationen wie ein bool[].

Die folgende CountPrimes Klasse verwendet einen BitArray und den klassischen "Sieve"-Algorithmus, um die Anzahl der Primes zwischen 2 und einem bestimmten Maximum zu berechnen:

class CountPrimes
{
    static int Count(int max)
    {
        BitArray flags = new BitArray(max + 1);
        int count = 0;
        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 {count} primes between 2 and {max}");
    }
}

Beachten Sie, dass die Syntax für den Zugriff auf Elemente des BitArray genau wie für ein bool[] ist.

Das folgende Beispiel zeigt eine 26×10-Rasterklasse mit einem Indexer mit zwei Parametern. Der erste Parameter muss ein Groß- oder Kleinbuchstabe im Bereich A–Z sein, und die zweite muss eine ganze Zahl im Bereich von 0 bis 9 sein.

class Grid
{
    const int NumRows = 26;
    const int NumCols = 10;
    int[,] cells = new int[NumRows, NumCols];

    public int this[char row, int col]
    {
        get
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            return cells[row - 'A', col];
        }
        set
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException ("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            cells[row - 'A', col] = value;
        }
    }
}

Endbeispiel

15.9.2 Indexer- und Eigenschaftsunterschiede

Indexer und Eigenschaften sind im Konzept sehr ähnlich, unterscheiden sich jedoch auf die folgenden Arten:

  • Eine Eigenschaft wird anhand ihres Namens identifiziert, während ein Indexer anhand seiner Signatur identifiziert wird.
  • Auf eine Eigenschaft wird über eine simple_name (§12.8.4) oder eine member_access (§12.8.7) zugegriffen, während auf ein Indexerelement über eine element_access (§12.8.12.3) zugegriffen wird.
  • Eine Eigenschaft kann ein statisches Element sein, während ein Indexer immer ein Instanzmemm ist.
  • Ein Get-Accessor einer Eigenschaft entspricht einer Methode ohne Parameter, während ein Get-Accessor eines Indexers einer Methode mit derselben Parameterliste wie der Indexer entspricht.
  • Ein Set-Accessor einer Eigenschaft entspricht einer Methode mit einem einzigen Parameter namens value, während ein Set-Accessor eines Indexers einer Methode mit derselben Parameterliste wie der Indexer entspricht, sowie einem zusätzlichen Parameter mit dem Namen value.
  • Es ist ein Kompilierungszeitfehler, wenn ein Indexer-Accessor eine lokale Variable oder lokale Konstante mit demselben Namen wie ein Indexerparameter deklariert.
  • Bei einer überschreibenden Eigenschaftsdeklaration wird mithilfe der Syntax base.Pauf die geerbte Eigenschaft zugegriffen, wobei P der Eigenschaftsname angegeben ist. In einer überschreibenden Indexerdeklaration wird mithilfe der Syntax base[E]auf den geerbten Indexer zugegriffen, wobei E es sich um eine durch Trennzeichen getrennte Liste von Ausdrücken handelt.
  • Es gibt kein Konzept für einen "automatisch implementierten Indexer". Es ist ein Fehler, wenn ein nicht abstrakter, nicht externer Indexer mit Semikolon accessor_bodys vorhanden ist.

Abgesehen von diesen Unterschieden gelten alle in §15.7.3, §15.7.5 und §15.7.6 definierten Regeln sowohl für Indexer-Accessoren als auch für Eigenschaftsaccessoren.

Diese Ersetzung von Eigenschaft/Eigenschaften durch Indexer/Indexer beim Lesen von §15.7.3, §15.7.5 und §15.7.6 gilt auch für definierte Begriffe. Insbesondere wird Lesen-Schreiben-Eigenschaft zu Lesen-Schreiben-Indexer, Nur-Lesen-Eigenschaft wird zu Nur-Lesen-Indexer, und Nur-Schreiben-Eigenschaft wird zu Nur-Schreiben-Indexer.

15.10 Operatoren

15.10.1 Allgemein

Ein Operator ist ein Element, das die Bedeutung eines Ausdrucksoperators definiert, der auf Instanzen der Klasse angewendet werden kann. Operatoren werden mit operator_declarations deklariert:

operator_declaration
    : attributes? operator_modifier+ operator_declarator operator_body
    ;

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

operator_declarator
    : unary_operator_declarator
    | binary_operator_declarator
    | conversion_operator_declarator
    ;

unary_operator_declarator
    : type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
    ;

logical_negation_operator
    : '!'
    ;

overloadable_unary_operator
    : '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
    ;

binary_operator_declarator
    : type 'operator' overloadable_binary_operator
        '(' fixed_parameter ',' fixed_parameter ')'
    ;

overloadable_binary_operator
    : '+'  | '-'  | '*'  | '/'  | '%'  | '&' | '|' | '^'  | '<<' 
    | right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
    ;

conversion_operator_declarator
    : 'implicit' 'operator' type '(' fixed_parameter ')'
    | 'explicit' 'operator' type '(' fixed_parameter ')'
    ;

operator_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.

Hinweis: Die präfixe logische Negation (§12.9.4) und die postfixen nullvergebenden Operatoren (§12.8.9) werden zwar durch dasselbe lexikalische Token (!) repräsentiert, sind aber unterschiedlich. Letzterer ist kein überlasteter Operator. Hinweisende

Es gibt drei Kategorien überladener Operatoren: Unäre Operatoren (§15.10.2), binäre Operatoren (§15.10.3) und Konvertierungsoperatoren (§15.10.4).

Die operator_body ist entweder ein Semikolon, ein Blocktext (§15.6.1) oder ein Ausdruckstext (§15.6.1). Ein Blockkörper besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn der Operator aufgerufen wird. Der Block muss den Regeln für wertzurückgebende Methoden entsprechen, die in §15.6.11 beschrieben sind. Ein Ausdruckskörper besteht aus =>, gefolgt von einem Ausdruck und einem Semikolon und gibt einen einzelnen Ausdruck an, der ausgeführt werden soll, wenn der Operator aufgerufen wird.

Bei extern Operatoren besteht die operator_body einfach aus einem Semikolon. Bei allen anderen Operatoren ist die operator_body entweder ein Blocktext oder ein Ausdruckstext.

Die folgenden Regeln gelten für alle Operatordeklarationen:

  • Eine Operator-Deklaration enthält sowohl einen public als auch einen static Modifizierer.
  • Die Parameter eines Operators dürfen keine anderen Modifizierer als in haben.
  • Die Signatur eines Betreibers (§15.10.2, §15.10.3, §15.10.4) unterscheidet sich von den Signaturen aller anderen Betreiber, die in derselben Klasse deklariert sind.
  • Alle typen, auf die in einer Betreibererklärung verwiesen wird, sind mindestens so zugänglich wie der Betreiber selbst (§7.5.5).
  • Es handelt sich um einen Fehler für denselben Modifizierer, der mehrmals in einer Operatordeklaration angezeigt wird.

Jede Operatorkategorie legt zusätzliche Einschränkungen fest, wie in den folgenden Unterlisten beschrieben.

Wie andere Member werden in einer Basisklasse deklarierte Operatoren von abgeleiteten Klassen geerbt. Da Operatordeklarationen immer die Klasse oder Anweisung erfordern, an der der Operator deklariert wird, um an der Signatur des Operators teilzunehmen, ist es nicht möglich, dass ein in einer abgeleiteten Klasse deklarierter Operator einen in einer Basisklasse deklarierten Operator ausblenden kann. Daher ist der new Modifizierer niemals erforderlich und daher in einer Operatordeklaration nie zulässig.

Weitere Informationen zu unären und binären Operatoren finden Sie unter §12.4.

Weitere Informationen zu Konvertierungsoperatoren finden Sie unter §10.5.

15.10.2 Unäre Operatoren

Die folgenden Regeln gelten für unäre Operatordeklarationen, wobei T der Instanztyp der Klasse oder Struktur, die die Operatordeklaration enthält, bezeichnet wird:

  • Ein unärer +, -, ! (nur logische Negation) oder ~-Operator muss einen einzelnen Parameter vom Typ T oder T? annehmen und kann einen beliebigen Typ zurückgeben.
  • Ein unärer ++- oder ---Operator muss einen einzelnen Parameter des Typs T oder T? annehmen und denselben Typ oder einen davon abgeleiteten Typ zurückgeben.
  • Ein unärer true- oder false-Operator soll einen einzigen Parameter vom Typ T oder T? annehmen und den Typ bool zurückgeben.

Die Signatur eines unären Operators besteht aus dem Operatortoken (+, , -!~++--trueoder false) und dem Typ des einzelnen Parameters. Der Rückgabetyp ist weder Teil der Signatur eines unären Operators noch der Name des Parameters.

Für die true und false unäre Operatoren ist eine paarweise Deklaration erforderlich. Wenn eine Klasse einen dieser Operatoren deklariert, ohne die andere zu deklarieren, tritt ein Kompilierungszeitfehler auf. Die true und false Operatoren werden in §12.24 weiter beschrieben.

Beispiel: Das folgende Beispiel zeigt eine Implementierung und nachfolgende Verwendung von Operator++ für eine ganzzahlige Vektorklasse:

public class IntVector
{
    public IntVector(int length) {...}
    public int Length { get { ... } }                      // Read-only property
    public int this[int index] { get { ... } set { ... } } // 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, wie die Operatormethode den Wert zurückgibt, der durch Hinzufügen von 1 zum Operanden entsteht, genau wie die Postfix-Inkrement- und -Drekrementoperatoren (§12.8.16) sowie die Präfix-Inkrement- und -Drekrementoperatoren (§12.9.6). Im Gegensatz zu C++ sollte diese Methode den Wert des Operanden nicht direkt ändern, da dies gegen die Standardsemantik des Postfix-Inkrementoperators (§12.8.16) verstößt.

Endbeispiel

15.10.3 Binäre Operatoren

Die folgenden Regeln gelten für binäre Operatordeklarationen, wobei T der Instanztyp der Klasse oder Struktur, die die Operatordeklaration enthält, bezeichnet wird:

  • Ein binärer Nicht-Schiebeoperator muss zwei Parameter annehmen, von denen mindestens einer den Typ T oder T? haben soll, und kann einen beliebigen Typ zurückgeben.
  • Ein binärer <<- oder >>-Operator (§12.11) soll zwei Parameter annehmen, von denen der erste den Typ T oder T? und der zweite den Typ int oder int?hat, und er kann einen beliebigen Rückgabewert zurückgeben.

Die Signatur eines binären Operators besteht aus dem Operatortoken (+, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, oder <=) und den Typen der beiden Parameter. Der Rückgabetyp und die Namen der Parameter sind nicht Teil der Signatur eines binären Operators.

Für bestimmte binäre Operatoren ist eine paarweise Deklaration erforderlich. Für jede Erklärung eines der Betreiber eines Paares muss eine übereinstimmende Erklärung des anderen Betreibers des Paares vorliegen. Zwei Operatordeklarationen stimmen überein, wenn Identitätskonvertierungen zwischen ihren Rückgabetypen und den entsprechenden Parametertypen vorhanden sind. Für die folgenden Operatoren ist eine paarweise Deklaration erforderlich:

  • Operator == und Operator !=
  • Operator > und Operator <
  • Operator >= und Operator <=

15.10.4 Konvertierungsoperatoren

Eine Konvertierungsoperatordeklaration führt eine benutzerdefinierte Konvertierung (§10.5) ein, die die vordefinierten impliziten und expliziten Konvertierungen erweitert.

Eine Konvertierungsoperatordeklaration, 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 Funktionsmitgliedsaufrufen, Cast-Ausdrücken und Zuweisungen. Dies wird weiter in §10.2 beschrieben.

Eine Konvertierungsoperatordeklaration, die das explicit Schlüsselwort enthält, führt eine benutzerdefinierte explizite Konvertierung ein. Explizite Konvertierungen können in Cast-Ausdrücken auftreten und werden in §10.3näher beschrieben.

Ein Konvertierungsoperator konvertiert von einem Quelltyp, der durch den Parametertyp des Konvertierungsoperators angegeben ist, in einen Zieltyp, der durch den Rückgabetyp des Konvertierungsoperators angegeben wird.

Für einen bestimmten Quelltyp S und Zieltyp T, wenn S oder T nullable Werttypen sind, lassen S₀ und T₀ auf ihre zugrunde liegenden Typen verweisen; andernfalls sind S₀ und T₀ gleich S und T. Eine Klasse oder Struktur darf eine Konvertierung von einem Quelltyp in einen Zieltyp ST nur deklarieren, wenn alle folgenden Werte zutreffen:

  • S₀ und T₀ sind unterschiedliche Typen.

  • Entweder S₀ oder T₀ ist der Instanztyp der Klasse oder Struktur, die die Operatordeklaration enthält.

  • Weder S₀ noch T₀ ist ein interface_type.

  • Ohne benutzerdefinierte Konvertierungen existiert keine Konvertierung von S nach T oder von T nach S.

Für die Zwecke dieser Regeln gelten alle Typparameter, die mit S oder T verknüpft sind, als eindeutige Typen, die keine Vererbungsbeziehung mit anderen Typen haben, und alle Einschränkungen für diese Typparameter werden ignoriert.

Beispiel: Im Folgenden:

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 Operatordeklarationen sind zulässig, da T und int sowie string als eindeutige Typen ohne Beziehung zueinander betrachtet werden. Der dritte Operator ist jedoch ein Fehler, da C<T> die Basisklasse von D<T> ist.

Endbeispiel

Aus der zweiten Regel folgt, dass ein Umrechnungsoperator entweder in oder aus der Klasse oder dem Strukturtyp konvertiert wird, in den der Operator deklariert wird.

Beispiel: Es ist möglich, dass ein Klassen- oder Strukturtyp C eine Konvertierung von C zu int und von int zu C definiert, aber nicht von int zu bool. Endbeispiel

Es ist nicht möglich, eine vordefinierte Konvertierung direkt neu zu definieren. Daher dürfen Konvertierungsoperatoren nicht von oder in object konvertieren, da implizite und explizite Konvertierungen bereits zwischen object und allen anderen Typen vorhanden sind. Ebenso kann weder die Quelle noch die Zieltypen einer Konvertierung ein Basistyp des anderen sein, da dann bereits eine Konvertierung 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.

Beispiel:

struct Convertible<T>
{
    public static implicit operator Convertible<T>(T value) {...}
    public static explicit operator T(Convertible<T> value) {...}
}

wenn der Typ object als Typargument Tangegeben wird, deklariert der zweite Operator eine bereits vorhandene Konvertierung (eine implizite und daher auch eine explizite Konvertierung von jedem Typ in ein Typobjekt).

Endbeispiel

In Fällen, in denen eine vordefinierte Konvertierung zwischen zwei Typen vorhanden ist, werden alle benutzerdefinierten Konvertierungen zwischen diesen Typen ignoriert. Speziell:

  • Wenn eine vordefinierte implizite Konvertierung (§10.2) vom Typ S zum Typ Tvorhanden ist, werden alle benutzerdefinierten Konvertierungen (implizit oder explizit) von S zu T ignoriert.
  • Wenn eine vordefinierte explizite Konvertierung (§10.3) vom Typ S zum Typ Tvorhanden ist, werden alle benutzerdefinierten expliziten Konvertierungen von S zu T " ignoriert". Außerdem:
    • Wenn entweder S oder T Schnittstellentypen sind, werden benutzerdefinierte implizite Konvertierungen von S zu T ignoriert.
    • Andernfalls werden benutzerdefinierte implizite Konvertierungen von S zu T weiterhin berücksichtigt.

Für alle Typen außer objectstehen die vom obigen Typ Convertible<T> deklarierten Operatoren nicht im Widerspruch zu 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 den Typ objectverbergen die vordefinierten Konvertierungen jedoch die benutzerdefinierten Konvertierungen in allen Fällen außer einem:

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
}

Endbeispiel

Benutzerdefinierte Konvertierungen sind nicht erlaubt, um von oder nach interface_types zu konvertieren. Insbesondere stellt diese Einschränkung sicher, dass beim Konvertieren in einen interface_type keine benutzerdefinierten Transformationen auftreten und dass eine Konvertierung in einen interface_type nur erfolgreich ist, wenn das konvertierte object tatsächlich den angegebenen interface_type implementiert.

Die Signatur eines Konvertierungsoperators besteht aus dem Quelltyp und dem Zieltyp. (Dies ist die einzige Form des Mitglieds, für das der Rückgabetyp an der Signatur teilnimmt.) Die implizite oder explizite Klassifizierung eines Konvertierungsoperators ist nicht Teil der Signatur des Operators. Daher kann eine Klasse oder Struktur nicht sowohl einen impliziten als auch einen expliziten Konvertierungsoperator mit denselben Quell- und Zieltypen deklarieren.

Hinweis: Im Allgemeinen sollten benutzerdefinierte implizite Konvertierungen so konzipiert werden, dass keine Ausnahmen ausgelöst werden und niemals Informationen verloren gehen. Wenn eine benutzerdefinierte Konvertierung Ausnahmen verursachen kann (z. B. weil das Quellargument außerhalb des Zulässigen liegt) oder Verlust von Informationen (z. B. Verwerfen von Bits mit hoher Reihenfolge), sollte diese Konvertierung als explizite Konvertierung definiert werden. Hinweisende

Beispiel: Im folgenden Code

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) => d.value;
    public static explicit operator Digit(byte b) => new Digit(b);
}

die Konvertierung von Digit in byte ist implizit, da sie niemals Ausnahmen auslöst oder Informationen verliert, während die Konvertierung von byte zu Digit explizit ist, weil Digit nur eine Teilmenge der möglichen Werte eines byte darstellen kann.

Endbeispiel

15.11 Instanzkonstruktoren

15.11.1 Allgemein

Ein Instanzkonstruktor ist ein Member, der die erforderlichen Aktionen zum Initialisieren einer Instanz einer Klasse implementiert. Instanzkonstruktoren werden mit constructor_declarations deklariert:

constructor_declaration
    : attributes? constructor_modifier* constructor_declarator constructor_body
    ;

constructor_modifier
    : 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

constructor_declarator
    : identifier '(' parameter_list? ')' constructor_initializer?
    ;

constructor_initializer
    : ':' 'base' '(' argument_list? ')'
    | ':' 'this' '(' argument_list? ')'
    ;

constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.

Eine constructor_declaration kann einen Satz von Attributen (§22), eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6) und einen extern (§15.6.8)-Modifizierer enthalten. Eine Konstruktordeklaration darf denselben Modifizierer nicht mehrmals einschließen.

Der Bezeichner einer constructor_declarator muss die Klasse benennen, in der der Instanzkonstruktor deklariert wird. Wenn ein anderer Name angegeben ist, tritt ein Kompilierungszeitfehler auf.

Die optionale parameter_list eines Instanzkonstruktors unterliegt den gleichen Regeln wie die parameter_list einer Methode (§15.6). Da der this Modifizierer für Parameter nur für Erweiterungsmethoden (§15.6.10) gilt, darf kein Parameter im parameter_list eines Konstruktors den this Modifizierer enthalten. Die Parameterliste definiert die Signatur (§7.6) eines Instanzkonstruktors und steuert den Prozess, bei dem die Überladungsauflösung (§12.6.4) einen bestimmten Instanzkonstruktor in einem Aufruf auswählt.

Jeder der typen, auf die im parameter_list eines Instanzkonstruktors verwiesen wird, muss mindestens so zugänglich sein wie der Konstruktor selbst (§7.5.5).

Die optionale constructor_initializer gibt einen anderen Instanzkonstruktor an, der aufgerufen werden soll, bevor die anweisungen im constructor_body dieses Instanzkonstruktors ausgeführt werden. Dies wird weiter in §15.11.2 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 einer der beiden

  • ein Block, der die Anweisungen angibt, um eine neue Instanz der Klasse zu initialisieren;
  • ein Ausdruckskörper, bestehend aus => gefolgt von einem Ausdruck und einem Semikolon, und einen einzelnen Ausdruck darstellt, um eine neue Instanz der Klasse zu initialisieren.

Ein Konstruktorkörper , der ein Block oder Ausdruckskörper ist, entspricht genau dem Block einer Instanzmethode mit einem void Rückgabetyp (§15.6.11).

Instanzkonstruktoren werden nicht vererbt. Daher verfügt eine Klasse über keine anderen Instanzkonstruktoren als die tatsächlich in der Klasse deklarierten Instanzen, mit der Ausnahme, dass, wenn eine Klasse keine Instanzkonstruktordeklarationen enthält, automatisch ein Standardinstanzkonstruktor bereitgestellt wird (§15.11.5).

Instanzkonstruktoren werden von object_creation_expressions (§12.8.17.2) und über constructor_initializers aufgerufen.

15.11.2 Konstruktor-Initialisierer

Alle Instanzkonstruktoren (mit Ausnahme der objectKlassenkonstruktoren) enthalten implizit einen Aufruf eines anderen Instanzkonstruktors unmittelbar vor dem constructor_body. Der implizit aufgerufene Konstruktor wird durch die constructor_initializer bestimmt:

  • Ein Instanzkonstruktorinitialisierer des Formulars base(argument_list) (wobei argument_list optional ist) bewirkt, dass ein Instanzkonstruktor aus der direkten Basisklasse aufgerufen wird. Dieser Konstruktor wird mit argument_list und den Überladungsauflösungsregeln von §12.6.4 ausgewählt. Der Satz von Kandidateninstanzkonstruktoren besteht aus allen barrierefreien Instanzkonstruktoren der direkten Basisklasse. Wenn dieser Satz leer ist oder ein einzelner Konstruktor der besten Instanz nicht identifiziert werden kann, tritt ein Kompilierungszeitfehler auf.
  • Ein Instanzkonstruktorinitialisierer des Formulars this(argument_list) (wobei argument_list optional ist) ruft einen anderen Instanzkonstruktor aus derselben Klasse auf. Der Konstruktor wird mit argument_list und den Überladungsauflösungsregeln von §12.6.4 ausgewählt. Der Satz von Kandidateninstanzkonstruktoren besteht aus allen Instanzkonstruktoren, die in der Klasse selbst deklariert sind. Wenn der resultierende Satz anwendbarer Instanzkonstruktoren leer ist oder ein einzelner optimaler Instanzkonstruktor nicht identifiziert werden kann, tritt ein Kompilierungszeitfehler auf. Wenn sich eine Instanzkonstruktordeklaration über eine Kette eines oder mehrerer Konstruktorinitialisierer aufruft, tritt ein Kompilierungsfehler auf.

Wenn ein Instanzkonstruktor keinen Konstruktorinitialisierer hat, wird implizit ein Konstruktorinitialisierer des Formulars base() bereitgestellt.

Hinweis: Eine Instanzkonstruktordeklaration des Formulars

C(...) {...}

ist genau gleichbedeutend mit

C(...) : base() {...}

Hinweisende

Der Umfang der parameter, die vom parameter_list einer Instanzkonstruktordeklaration angegeben werden, enthält 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) {}
}

Endbeispiel

Ein Instanzkonstruktorinitialisierer kann nicht auf die erstellte Instanz zugreifen. Daher ist es ein Kompilierungszeitfehler, dies in einem Argumentausdruck des Konstruktorinitialisierers zu referenzieren, da es ein Kompilierungszeitfehler für einen Argumentausdruck ist, ein Instanzmitglied über einen simple_name zu referenzieren.

15.11.3 Instanzvariablen-Initialisierer

Wenn ein nicht externer Instanzkonstruktor keinen Konstruktorinitialisierer hat oder über einen Konstruktorinitialisierer des Formulars base(...)verfügt, führt dieser Konstruktor implizit die initialisierungen aus, die von den variable_initializers der in der Klasse deklarierten Instanzfelder angegeben wurden. Dies entspricht einer Abfolge von Zuweisungen, die unmittelbar nach dem Eintrag zum Konstruktor und vor dem impliziten Aufruf des direkten Basisklassenkonstruktors ausgeführt werden. Die Variableninitialisierer werden in der Textreihenfolge ausgeführt, in der sie in der Klassendeklaration (§15.5.6) angezeigt werden.

Variable Initialisierer müssen nicht von externen Instanzkonstruktoren ausgeführt werden.

15.11.4 Konstruktorausführung

Variableninitialisierer werden in Zuordnungsanweisungen umgewandelt, und diese Zuordnungsanweisungen werden vor dem Aufruf des Basisklasseninstanzkonstruktors ausgeführt. Diese Reihenfolge stellt sicher, dass alle Instanzfelder durch ihre Variableninitialisierer initialisiert werden, bevor jede Anweisung ausgeführt wird, die Zugriff auf diese Instanz hat.

Beispiel: In Anbetracht der folgenden Punkte:

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 = {x}, y = {y}");
}

Wenn ein neues B() zum Erstellen einer Instanz von B verwendet wird, wird die folgende Ausgabe erzeugt:

x = 1, y = 0

Der Wert von x ist 1, da der Variableninitialisierer ausgeführt wird, bevor der Konstruktor der Basisklassen-Instanz aufgerufen wird. Der Wert von y ist jedoch 0 (dem Standardwert einer int), da die Zuordnung zu y erst ausgeführt wird, nachdem der Basisklassenkonstruktor zurückkehrt. Es ist sinnvoll, sich die Initialisierungen von Instanzvariablen und Konstruktoren als Anweisungen vorzustellen, die automatisch vor dem Konstruktor_bodyeingefügt werden. Das Beispiel

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 variable Initialisierer; sie enthält auch Konstruktorinitialisierer beider Formulare (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 ungültig, dient aber lediglich dazu, den Mechanismus zu veranschaulichen).

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;
    }
}

Endbeispiel

15.11.5 Standardkonstruktoren

Wenn eine Klasse keine Instanzkonstruktordeklarationen enthält, wird automatisch ein Standardinstanzkonstruktor bereitgestellt. Dieser Standardkonstruktor ruft einfach einen Konstruktor der direkten Basisklasse auf, als hätte er einen Konstruktorinitialisierer des Formulars base(). Wenn die Klasse abstrakt ist, ist die deklarierte Barrierefreiheit für den Standardkonstruktor geschützt. Andernfalls ist die deklarierte Barrierefreiheit für den Standardkonstruktor öffentlich.

Hinweis: Daher ist der Standardkonstruktor immer des Formulars.

protected C(): base() {}

oder

public C(): base() {}

dabei C handelt es sich um den Namen der Klasse.

Hinweisende

Wenn die Überladungsauflösung keinen eindeutigen besten Kandidaten für den Initialisierer des Basisklassenkonstruktors ermitteln kann, tritt ein Kompilierungszeitfehler auf.

Beispiel: Im folgenden Code

class Message
{
    object sender;
    string text;
}

Ein Standardkonstruktor wird bereitgestellt, da die Klasse keine Instanzkonstruktordeklarationen enthält. Das Beispiel ist also genau gleichbedeutend mit

class Message
{
    object sender;
    string text;

    public Message() : base() {}
}

Endbeispiel

15.12 Statische Konstruktoren

Ein statischer Konstruktor ist ein Element, das die zum Initialisieren einer geschlossenen Klasse erforderlichen Aktionen implementiert. Statische Konstruktoren werden mit static_constructor_declaration sdeklariert:

static_constructor_declaration
    : attributes? static_constructor_modifiers identifier '(' ')'
        static_constructor_body
    ;

static_constructor_modifiers
    : 'static'
    | 'static' 'extern' unsafe_modifier?
    | 'static' unsafe_modifier 'extern'?
    | 'extern' 'static' unsafe_modifier?
    | 'extern' unsafe_modifier 'static'
    | unsafe_modifier 'static' 'extern'?
    | unsafe_modifier 'extern' 'static'
    ;

static_constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.

Ein static_constructor_declaration kann einen Satz von Attributen (§22) und einen extern Modifizierer (§15.6.8) enthalten.

Der Bezeichner eines static_constructor_declaration muss die Klasse benennen, in der der statische Konstruktor deklariert wird. Wenn ein anderer Name angegeben ist, tritt ein Kompilierungszeitfehler 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 einer der beiden

  • ein Block, der die auszuführenden Anweisungen angibt, um die Klasse zu initialisieren;
  • ein Ausdruckskörper, der aus einem =>, einem Ausdruck und einem Semikolon besteht, und einen einzelnen Ausdruck angibt, der ausgeführt werden soll, um die Klasse zu initialisieren.

Ein static_constructor_body in Form eines Blocks oder Ausdruckskörpers entspricht genau dem method_body einer statischen Methode mit einem void Rückgabetyp (§15.6.11).

Statische Konstruktoren werden nicht geerbt und können nicht direkt aufgerufen werden.

Der statische Konstruktor für eine geschlossene Klasse wird in einer bestimmten Anwendungsdomäne höchstens einmal ausgeführt. Die Ausführung eines statischen Konstruktors wird durch die ersten der folgenden Ereignisse ausgelöst, die in einer Anwendungsdomäne auftreten:

  • Eine Instanz der Klasse wird erstellt.
  • Alle statischen Elemente der Klasse werden referenziert.

Wenn eine Klasse die Main Methode (§7.1) 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, muss zunächst ein neuer Satz statischer Felder (§15.5.2) für diesen bestimmten geschlossenen Typ erstellt werden. Jedes der statischen Felder muss auf seinen Standardwert (§15.5.5.5) initialisiert werden. Folgen Sie diesen Schritten:

  • Wenn entweder kein statischer Konstruktor oder ein nicht externer statischer Konstruktor vorhanden ist, dann:
    • die statischen Feldinitialisierer (§15.5.6.2) sollen für diese statischen Felder ausgeführt werden;
    • dann muss der nicht externe statische Konstruktor (sofern vorhanden) ausgeführt werden.
  • Falls andererseits ein externer statischer Konstruktor vorhanden ist, muss dieser ausgeführt werden. Statische Variableninitialisierer müssen nicht von externen statischen Konstruktoren ausgeführt werden.

Beispiel: Das Beispiel

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");
    }
}

muss die Ausgabe erzeugen:

Init A
A.F
Init B
B.F

da die Ausführung des Astatischen Konstruktors durch den Aufruf A.Fausgelöst wird und die Ausführung des Bstatischen Konstruktors durch den Aufruf B.Fausgelöst wird.

Endbeispiel

Es ist möglich, Zirkelabhängigkeiten zu konstruieren, die es statischen Feldern mit Variableninitialisierern ermöglichen, in ihrem Standardwertzustand beobachtet zu werden.

Beispiel: Das Beispiel

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 = {A.X}, Y = {B.Y}");
    }
}

erzeugt die Ausgabe

X = 1, Y = 2

Zum Ausführen der Main Methode führt das System zunächst den Initialisierer für B.Y aus, bevor es den statischen Konstruktor der Klasse B ausführt. Der Initialisierer von Y bewirkt, dass der A-Konstruktor von static ausgeführt wird, weil der Wert von A.X referenziert wird. Der statische Konstruktor von A fährt fort, den Wert von X zu berechnen und ruft dabei den voreingestellten Wert von Y ab, der null ist. A.X wird daher auf 1 initialisiert. Der Prozess der Ausführung der statischen Feldinitialisierer und des statischen Konstruktors wird dann abgeschlossen und kehrt zurück zur Berechnung des Anfangswerts von A, dessen Ergebnis 2 wird.

Endbeispiel

Da der statische Konstruktor genau einmal für jeden geschlossenen konstruierten Klassentyp ausgeführt wird, ist es praktisch, Laufzeitüberprüfungen für den Typparameter zu erzwingen, der nicht zur Kompilierungszeit über Einschränkungen (§15.2.5) überprüft werden kann.

Beispiel: Der folgende Typ verwendet einen statischen Konstruktor, um zu erzwingen, dass das Typargument eine Enumeration ist:

class Gen<T> where T : struct
{
    static Gen()
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("T must be an enum");
        }
    }
}

Endbeispiel

15.13 Finalizer

Hinweis: In einer früheren Version dieser Spezifikation wurde das, was jetzt als "Finalizer" bezeichnet wird, als "Destruktor" bezeichnet. Die Erfahrung hat gezeigt, dass der Begriff "Destruktor" Verwirrung verursachte und häufig zu falschen Erwartungen geführt hat, insbesondere für Programmierer, die C++ kennen. In C++ wird ein Destruktor auf bestimmte Weise aufgerufen, während in C# kein Finalisierer ist. Um ein bestimmtes Verhalten von C# zu erzielen, sollte man Dispose verwenden. Hinweisende

Ein Finalizer ist ein Member, der die erforderlichen Aktionen zum Bereinigen einer Instanz einer Klasse implementiert. Ein Finalizer wird mithilfe eines finalizer_declaration deklariert:

finalizer_declaration
    : attributes? '~' identifier '(' ')' finalizer_body
    | attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
      finalizer_body
    | attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
      finalizer_body
    ;

finalizer_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.

Ein finalizer_declaration kann einen Satz von Attributen (§22) enthalten.

Der Bezeichner einer finalizer_declarator muss die Klasse benennen, in der der Finalizer deklariert wird. Wenn ein anderer Name angegeben ist, tritt ein Kompilierungszeitfehler auf.

Wenn eine Finalizerdeklaration einen extern Modifizierer enthält, wird der Finalizer als externer Finalizer bezeichnet. Da eine externe Finalizerdeklaration keine tatsächliche Implementierung bereitstellt, besteht die finalizer_body aus einem Semikolon. Für alle anderen Finalisierer besteht die finalizer_body aus einer der beiden

  • ein Block, der die auszuführenden Anweisungen angibt, um eine Instanz der Klasse abzuschließen.
  • oder ein Ausdruckskörper, der aus => gefolgt von einem Ausdruck und einem Semikolon besteht und einen einzelnen Ausdruck bezeichnet, der ausgeführt wird, um eine Instanz der Klasse zu finalisieren.

Ein finalizer_body , der ein Block oder ein Ausdruckskörper ist, entspricht genau dem method_body einer Instanzmethode mit einem void Rückgabetyp (§15.6.11).

Finalizer werden nicht vererbt. Somit hat eine Klasse keine anderen Finalizer als den, der in dieser Klasse deklariert werden kann.

Hinweis: Da ein Finalizer über keine Parameter verfügen muss, kann er nicht überladen werden, sodass eine Klasse höchstens einen Finalizer haben kann. Hinweisende

Finalizer werden automatisch aufgerufen und können nicht explizit aufgerufen werden. Eine Instanz kann abgeschlossen werden, wenn kein Code mehr für diese Instanz verwendet werden kann. Die Ausführung des Finalizers für die Instanz kann jederzeit erfolgen, nachdem die Instanz zur Fertigstellung berechtigt ist (§7.9). Wenn eine Instanz fertiggestellt ist, werden die Finalizer in der Vererbungskette dieser Instanz in der Reihenfolge vom am weitesten abgeleiteten bis zum am wenigsten abgeleiteten aufgerufen. Ein Finalizer kann für jeden Thread ausgeführt werden. Weitere Erläuterungen zu den Regeln, die bestimmen, wann und wie ein Finalizer ausgeführt wird, finden Sie unter §7.9.

Beispiel: Die Ausgabe des Beispiels

class A
{
    ~A()
    {
        Console.WriteLine("A's finalizer");
    }
}

class B : A
{
    ~B()
    {
        Console.WriteLine("B's finalizer");
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

ist

B's finalizer
A's finalizer

da Finalizer in einer Vererbungskette der Reihe nach aufgerufen werden, von den meisten abgeleiteten bis zu den am wenigsten abgeleiteten.

Endbeispiel

Finalizer werden implementiert, indem die virtuelle Methode Finalize auf System.Objectüberschrieben wird. C#-Programme dürfen diese Methode nicht überschreiben oder sie (oder Überschreibungen davon) direkt aufrufen.

Beispiel: Beispiel: Das Programm

class A
{
    override protected void Finalize() {}  // Error
    public void F()
    {
        this.Finalize();                   // Error
    }
}

enthält zwei Fehler.

Endbeispiel

Ein Compiler soll sich so verhalten, als ob diese Methode und ihre Überschreibungen überhaupt nicht existieren.

Beispiel: Demnach ist dieses Programm:

class A
{
    void Finalize() {}  // Permitted
}

ist gültig und die gezeigte Methode blendet die Methode System.Objectvon Finalize aus.

Endbeispiel

Eine Erläuterung des Verhaltens, wenn eine Ausnahme von einem Finalizer ausgelöst wird, finden Sie unter §21.4.

15.14 Asynchrone Funktionen

15.14.1 Allgemein

Eine Methode (§15.6) oder anonyme Funktion (§12.19) mit dem async Modifizierer wird als asynchrone Funktion bezeichnet. Im Allgemeinen wird der Begriff "async" verwendet, um jede Art von Funktion zu beschreiben, die den async Modifizierer enthält.

Es handelt sich um einen Kompilierungszeitfehler für die Parameterliste einer asynchronen Funktion, wenn Parameter vom Typ in, out oder ref oder ein beliebiger Parameter eines ref struct Typs angegeben werden.

Die return_type einer asynchronen Methode muss entweder voidein Aufgabentyp oder ein asynchroner Iteratortyp (§15.15) sein. Bei einer asynchronen Methode, die einen Ergebniswert erzeugt, muss ein Aufgabentyp oder ein asynchroner Iteratortyp (§15.15.3) generisch sein. Bei einer asynchronen Methode, die keinen Ergebniswert erzeugt, darf ein Vorgangstyp nicht generisch sein. Solche Typen werden in dieser Spezifikation als «TaskType»<T> und «TaskType» bezeichnet. Der Standardbibliothekstyp System.Threading.Tasks.Task und die aus System.Threading.Tasks.Task<TResult> und System.Threading.Tasks.ValueTask<T> konstruierten Typen sind Aufgabentypen, ebenso wie ein Klassen-, Struktur- oder Schnittstellentyp, der einem Aufgaben-Generator-Typ über das Attribut System.Runtime.CompilerServices.AsyncMethodBuilderAttribute zugeordnet ist. Solche Typen werden in dieser Spezifikation als «TaskBuilderType»<T> und «TaskBuilderType»bezeichnet. Ein Vorgangstyp kann höchstens einen Typparameter aufweisen und kann nicht in einem generischen Typ geschachtelt werden.

Eine asynchrone Methode, die einen Aufgabentyp zurückgibt, wird als task-returning bezeichnet.

Vorgangstypen können in ihrer genauen Definition variieren, aber aus der Sicht der Sprache befindet sich ein Vorgangstyp in einem der Zustände unvollständig, erfolgreich oder fehlerhaft. Eine fehlerhafte Aufgabe zeichnet eine relevante Ausnahme auf. Ein erfolgreich«TaskType»<T> zeichnet ein Ergebnis vom Typ Tauf. Aufgabentypen sind await-fähig, und Aufgaben können daher die Operanden von await-Ausdrücken sein (§12.9.8).

Beispiel: Der Aufgabentyp MyTask<T> ist dem Aufgaben-Generator-Typ MyTaskMethodBuilder<T> und dem Awaiter-Typ Awaiter<T>zugeordnet:

using System.Runtime.CompilerServices; 
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
class MyTask<T>
{
    public Awaiter<T> GetAwaiter() { ... }
}

class Awaiter<T> : INotifyCompletion
{
    public void OnCompleted(Action completion) { ... }
    public bool IsCompleted { get; }
    public T GetResult() { ... }
}

Endbeispiel

Ein Aufgaben-Generator-Typ ist eine Klasse oder ein Strukturtyp, der einem bestimmten Aufgabentyp entspricht (§15.14.2). Der Aufgaben-Generator-Typ muss genau mit der deklarierten Barrierefreiheit des entsprechenden Aufgabentyps übereinstimmen.

Hinweis: Wenn der Aufgabentyp deklariert internalwird, muss der entsprechende Generatortyp ebenfalls deklariert internal und in derselben Assembly definiert werden. Wenn der Aufgabentyp in einem anderen Typ geschachtelt ist, muss der Aufgabenerstellungstyp auch in demselben Typ geschachtelt sein. Hinweisende

Eine asynchrone Funktion hat die Möglichkeit, die Auswertung mit Hilfe von await-Ausdrücken (§12.9.8) in ihrem Körper auszusetzen. Die Auswertung kann später an der Stelle des aussetzenden await-Ausdrucks mit Hilfe eines Wiederaufnahme-Delegatenwieder aufgenommen werden. Der Resumption-Delegat ist vom Typ System.Action. Wenn er aufgerufen wird, wird die Auswertung des asynchronen Funktionsaufrufs ab dem await-Ausdruck an der Stelle fortgesetzt, an der er unterbrochen wurde. Der aktuelle Aufrufer eines asynchronen Funktionsaufrufs ist der ursprüngliche Aufrufer, wenn der Funktionsaufruf nie angehalten wurde oder der letzte Aufrufer des Reaktivierungsdelegats andernfalls.

15.14.2 Muster des Aufgabentyp-Generators

Ein Aufgaben-Generator-Typ kann höchstens einen Typparameter aufweisen und kann nicht in einem generischen Typ geschachtelt werden. Ein Task Builder-Typ muss die folgenden Mitglieder (für nicht-generische Task Builder-Typen hat SetResult keine Parameter) mit deklarierter public Zugänglichkeit haben:

class «TaskBuilderType»<T>
{
    public static «TaskBuilderType»<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 «TaskType»<T> Task { get; }
}

Ein Compiler generiert Code, der den «TaskBuilderType» verwendet, um die Semantik des Anhaltens und Fortsetzens der Auswertung der asynchronen Funktion zu implementieren. Ein Compiler verwendet den «TaskBuilderType» wie folgt:

  • «TaskBuilderType».Create() wird aufgerufen, um eine Instanz von «TaskBuilderType», die in dieser Liste benannt ist builder , zu erstellen.
  • builder.Start(ref stateMachine) wird aufgerufen, um den Builder mit einer vom Compiler erzeugten Zustandsmaschineninstanz zu verknüpfen stateMachine.
    • Der Ersteller ruft stateMachine.MoveNext() entweder in Start() oder nach der Rückkehr von Start() auf, um die Zustandsmaschine voranzutreiben.
  • Nachdem Start() zurückgekehrt ist, ruft die async-Methode builder.Task auf, damit die Aufgabe aus der asynchronen Methode zurückkehrt.
  • Jeder Aufruf von stateMachine.MoveNext() bringt die Zustandsmaschine voran.
  • Wenn der Zustandsautomat erfolgreich abgeschlossen wird, wird builder.SetResult() mit dem Rückgabewert der Methode aufgerufen, falls vorhanden.
  • Andernfalls, wenn eine Ausnahme e in der Zustandsmaschine ausgelöst wird, wird builder.SetException(e) aufgerufen.
  • Wenn der Zustandsautomat einen await expr Ausdruck erreicht, wird expr.GetAwaiter() aufgerufen.
  • Wenn der Waiter ICriticalNotifyCompletion implementiert und IsCompleted falsch ist, ruft der Zustandsautomat builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)auf.
    • AwaitUnsafeOnCompleted() sollte awaiter.UnsafeOnCompleted(action) mit einem Action aufrufen, der stateMachine.MoveNext() aufruft, wenn der Waiter fertig ist.
  • Andernfalls ruft der Zustandsautomat builder.AwaitOnCompleted(ref awaiter, ref stateMachine)auf.
    • AwaitOnCompleted() sollte awaiter.OnCompleted(action) mit einem Action aufrufen, der stateMachine.MoveNext() aufruft, wenn der Waiter fertig ist.
  • SetStateMachine(IAsyncStateMachine) kann von der vom Compiler erzeugten IAsyncStateMachine-Implementierung aufgerufen werden, um die Instanz des Builders zu identifizieren, die mit einer Zustandsmaschineninstanz verbunden ist, insbesondere in Fällen, in denen die Zustandsmaschine als Werttyp implementiert ist.
    • Wenn der Builder stateMachine.SetStateMachine(stateMachine)aufruft, ruft stateMachinebuilder.SetStateMachine(stateMachine) auf der Builder-Instanz auf, die mitstateMachineverbunden ist.

Hinweis: Sowohl der Parameter als auch das Argument müssen identitätskonvertierbar zu SetResult(T result) sein für «TaskType»<T> Task { get; } und T. Dies ermöglicht es einem Aufgabentyp-Generator, Typen wie Tupel zu unterstützen, bei denen zwei Typen, die nicht gleich sind, identitätskonvertierbar sind. Hinweisende

15.14.3 Auswertung einer Task-Returning-Async-Funktion

Der Aufruf einer asynchronen Funktion zur Rückgabe einer Aufgabe bewirkt, dass eine Instanz des zurückgegebenen Aufgabentyps generiert wird. Dies wird als Rückgabeaufgabe der asynchronen Funktion bezeichnet. Der Vorgang befindet sich zunächst in einem unvollständigen Zustand.

Der asynchrone Funktionstext wird dann ausgewertet, bis er entweder angehalten wird (durch Erreichen eines Warteausdrucks) oder beendet wird, woraufhin die Steuerung zusammen mit der Rückgabeaufgabe an den Anrufer zurückgegeben wird.

Wenn der Text der asynchronen Funktion beendet wird, wird die Rückgabeaufgabe aus dem unvollständigen Zustand verschoben:

  • Wenn der Funktionskörper durch das Erreichen einer Return-Anweisung oder des Endes des Körpers beendet wird, wird jeder Ergebniswert in der Return-Task aufgezeichnet, die in den Zustand Erfolgreich versetzt wird.
  • Wenn der Funktionskörper aufgrund einer nicht abgefangenen OperationCanceledExceptionbeendet wird, wird die Ausnahme in der Rückgabeaufgabe aufgezeichnet, die in den Zustand abgebrochen versetzt wird.
  • Wenn der Funktionskörper aufgrund einer anderen nicht abgefangenen Ausnahme (§13.10.6) beendet wird, wird die Ausnahme in der Rückgabeaufgabe aufgezeichnet, die in den Zustand faulted versetzt wird.

15.14.4 Auswertung einer asynchronen Funktion, die keinen Wert zurückgibt

Wenn der Rückgabetyp der asynchronen Funktion lautetvoid, unterscheidet sich die Auswertung von der obigen Vorgehensweise: Da keine Aufgabe zurückgegeben wird, kommuniziert die Funktion stattdessen den Abschluss und Ausnahmen mit dem Synchronisierungskontext des aktuellen Threads. Die genaue Definition des Synchronisierungskontexts ist implementierungsabhängig und repräsentiert, an welchem Ort der aktuelle Thread ausgeführt wird. Der Synchronisationskontext wird benachrichtigt, wenn die Auswertung einer void-returning async-Funktion beginnt, erfolgreich abgeschlossen wird oder eine nicht abgefangene Ausnahme ausgelöst wird.

Dadurch kann der Kontext verfolgen, wie viele void-returning async-Funktionen unter ihm laufen, und entscheiden, wie Ausnahmen, die von ihnen ausgehen, propagiert werden sollen.

15.15 Synchrone und asynchrone Iteratoren

15.15.1 Allgemein

Ein Funktionselement (§12.6) oder eine lokale Funktion (§13.6.4), die mit einem Iteratorblock (§13.3) implementiert wurde, wird als Iterator bezeichnet. Ein Iteratorblock kann als Textkörper eines Funktionselements verwendet werden, solange der Rückgabetyp des entsprechenden Funktionselements eine der Enumerationsschnittstellen (§15.15.2) oder eine der aufzählbaren Schnittstellen (§15.15.3) ist.

Eine mithilfe eines Iteratorblocks (§13.3) implementierte asynchrone Funktion (§15.14) wird als asynchroner Iterator bezeichnet. Ein asynchroner Iteratorblock kann als Körper eines Funktionsmitglieds verwendet werden, solange der Rückgabetyp des entsprechenden Funktionsmitglieds die asynchronen Enumerationsschnittstellen (§15.15.2) oder die asynchronen aufzählbaren Schnittstellen (§15.15.3) ist.

Ein Iteratorblock kann als method_body, operator_body oder accessor_body auftreten, während Ereignisse, Instanzkonstruktoren, statische Konstruktoren und Finalizer nicht als synchrone oder asynchrone Iteratoren implementiert werden dürfen.

Wenn ein Funktionselement oder eine lokale Funktion mithilfe eines Iteratorblocks implementiert wird, führt es zu einem Kompilierungszeitfehler, wenn die Parameterliste des Funktionsmitglieds beliebige in, out oder ref Parameter oder einen Parameter eines ref struct-Typs angibt.

15.15.2 Enumeratorschnittstellen

Die Enumerationsschnittstellen sind die nicht generische Schnittstelle System.Collections.IEnumerator und alle Instanziierungen der generischen Schnittstellen System.Collections.Generic.IEnumerator<T>.

Die asynchronen Enumerationsschnittstellen sind alle Instanziationen der generischen Schnittstelle System.Collections.Generic.IAsyncEnumerator<T>.

Aus Gründen der Kürze werden in diesem Unterabschnitt und seinen gleichgeordneten Abschnitten diese Schnittstellen als IEnumerator, IEnumerator<T> und IAsyncEnumerator<T> bezeichnet.

15.15.3 Aufzählbare Schnittstellen

Die aufzählbaren Schnittstellen sind die nicht generische Schnittstelle System.Collections.IEnumerable und alle Instanziierungen der generischen Schnittstellen System.Collections.Generic.IEnumerable<T>.

Die asynchronen enumerationsfähigen Schnittstellen sind alle Instanziationen der generischen Schnittstelle System.Collections.Generic.IAsyncEnumerable<T>.

Aus Gründen der Kürze werden in diesem Unterabschnitt und seinen gleichgeordneten Abschnitten diese Schnittstellen als IEnumerable, IEnumerable<T> und IAsyncEnumerable<T> bezeichnet.

15.15.4 Ertragtyp

Ein Iterator erzeugt eine Abfolge von Werten, die alle denselben Typ aufweisen. Dieser Typ wird als Ertragtyp des Iterators bezeichnet.

  • Der Ertragtyp eines Iterators, der zurückgibt IEnumerator oder IEnumerable ist object.
  • Der Rückgabetyp eines Iterators, der ein IEnumerator<T>, IAsyncEnumerator<T>, IEnumerable<T> oder IAsyncEnumerable<T> zurückgibt, ist T.

15.15.5 Enumeratorobjekte

15.15.5.1 Allgemein

Wenn ein Funktionselement oder eine lokale Funktion, die einen Enumerator-Schnittstellentyp zurückgibt, mithilfe eines Iteratorblocks implementiert wird, führt das Aufrufen der Funktion den Code nicht sofort im Iteratorblock aus. Stattdessen wird ein Enumerationsobjekt erstellt und zurückgegeben. Dieses Objekt kapselt den im Iteratorblock angegebenen Code, und die Ausführung dieses Codes erfolgt, wenn die Methode MoveNext oder MoveNextAsync des Enumerator-Objekts aufgerufen wird. Ein Enumerationsobjekt weist die folgenden Merkmale auf:

  • Er implementiert System.IDisposable, IEnumerator und IEnumerator<T>, oder System.IAsyncDisposable und IAsyncEnumerator<T>, wobei T der Rückgabetyp des Iterators ist.
  • Sie wird mit einer Kopie der Argumentwerte (falls vorhanden) und dem Instanzwert initialisiert, der an das Funktionsmitglied übergeben wird.
  • Es verfügt über vier potenzielle Zustände: vorher, laufend, angehalten und danach, und befindet sich anfangs im Zustand vorher.

Ein Enumeratorobjekt ist in der Regel eine Instanz einer compilergenerierten Enumerationsklasse, die den Code im Iteratorblock kapselt und die Enumerationsschnittstellen implementiert, aber andere Implementierungsmethoden sind möglich. Wenn eine Aufzählungsklasse vom Compiler generiert wird, wird diese Klasse direkt oder indirekt in der Klasse, die das Funktionsmitglied enthält, geschachtelt, sie wird private Zugriffsberechtigung haben und einen Namen, der für die Compilerverwendung reserviert ist (§6.4.3).

Ein Enumeratorobjekt kann mehr Schnittstellen implementieren als die oben angegebenen.

Die folgenden Unterklauseln beschreiben das erforderliche Verhalten des Mitglieds, um den Enumerator weiterzuschalten, den aktuellen Wert aus dem Enumerator abzurufen und die vom Enumerator verwendeten Ressourcen zu entsorgen. Diese sind in den folgenden Mitgliedern für synchrone bzw. asynchrone Enumeratoren definiert:

  • Um den Enumerator voranzuschreiten: MoveNext und MoveNextAsync.
  • So rufen Sie den aktuellen Wert ab: Current.
  • So löschen Sie Ressourcen: Dispose und DisposeAsync.

Enumeratorobjekte unterstützen die IEnumerator.Reset Methode nicht. Wenn Sie diese Methode aufrufen, wird ein System.NotSupportedException Fehler ausgelöst.

Synchrone und asynchrone Iteratorblöcke unterscheiden sich dadurch, dass asynchrone Iteratormitglieder Aufgabentypen zurückgeben und gewartet werden können.

Den Enumerator weiterentwickeln

Die MoveNext- und MoveNextAsync-Methoden eines Enumeratorobjekts kapseln den Code eines Iteratorblocks. Durch das Aufrufen der Methode MoveNext oder MoveNextAsync wird der Code im Iteratorblock ausgeführt und die Current-Eigenschaft im Enumeratorobjekt entsprechend gesetzt.

MoveNext gibt einen bool Wert zurück, dessen Bedeutung unten beschrieben wird. MoveNextAsync gibt ein ValueTask<bool> (§15.14.3) zurück. Der Ergebniswert der zurückgegebenen MoveNextAsync Aufgabe hat die gleiche Bedeutung wie der Ergebniswert aus MoveNext. In der folgenden Beschreibung gelten die Aktionen, die für MoveNext beschrieben sind, auch für MoveNextAsync mit dem folgenden Unterschied: Wo angegeben ist, dass MoveNexttrue oder false zurückgibt, setzt MoveNextAsync seine Aufgabe auf den Zustand abgeschlossen und legt den Ergebniswert dieser Aufgabe auf den entsprechenden true oder false Wert fest.

Die genaue Aktion, die von MoveNext oder MoveNextAsync ausgeführt wird, hängt vom Status des Enumeratorobjekts ab, wenn sie aufgerufen wird.

  • Wenn der Status des Enumerator-Objekts vorist, wird MoveNextaufgerufen:
    • Ändert den Zustand in laufend.
    • Initialisiert die Parameter (einschließlich this) des Iteratorblocks an die Argumentwerte und den Instanzwert, die beim Initialisieren des Enumerationsobjekts gespeichert wurden.
    • Führt den Iteratorblock vom Anfang aus aus, bis die Ausführung unterbrochen wird (wie unten beschrieben).
  • Wenn der Status des Enumerator-Objekts laufendist, ist das Ergebnis des Aufrufs von MoveNext nicht spezifiziert.
  • Wenn der Status des Enumerator-Objekts suspendedist, wird der Aufruf von MoveNext:
    • Ändert den Zustand in laufend.
    • Stellt die Werte aller lokalen Variablen und Parameter (einschließlich this) auf die Werte wieder her, die beim letzten Anhalten des Iteratorblocks gespeichert wurden.

      Hinweis: Der Inhalt aller Objekte, auf die von diesen Variablen verwiesen wird, kann sich seit dem vorherigen Aufruf von MoveNext geändert haben. Hinweisende

    • Setzt die Ausführung des Iterator-Blocks unmittelbar nach der Renditeanweisung fort, die die Aussetzung der Ausführung verursacht hat, und fährt fort, bis die Ausführung unterbrochen wird (wie unten beschrieben).
  • Wenn der Status des Enumerator-Objekts nachist, gibt der Aufruf von MoveNext false zurück.

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 Erreichen des Endes des Iterator-Blocks und durch eine ausgelöste und aus dem Iterator-Block heraus weitergeleitete Ausnahme.

  • Wenn eine yield return -Anweisung auftaucht (§9.4.4.20):
    • Der in der Anweisung angegebene Ausdruck wird ausgewertet, implizit in den Ertragtyp konvertiert und der Current Eigenschaft des Enumeratorobjekts zugewiesen.
    • Die Ausführung des Iterator-Texts wird ausgesetzt. Die Werte aller lokalen Variablen und Parameter (einschließlich this) werden gespeichert, wie die Position dieser yield return Anweisung. Wenn sich die yield return -Anweisung innerhalb eines oder mehrerer try -Blöcke befindet, werden die zugehörigen finally-Blöcke zu diesem Zeitpunkt nicht ausgeführt.
    • Der Zustand des Enumeratorobjekts wird in angehalten geändert.
    • Die Methode MoveNext gibt true an den Aufrufer zurück und zeigt an, dass die Iteration erfolgreich zum nächsten Wert fortgeschritten ist.
  • Wenn eine yield break -Anweisung auftaucht (§9.4.4.20):
    • Wenn sich die yield break Anweisung innerhalb eines oder mehrerer try Blöcke befindet, werden die zugehörigen finally Blöcke ausgeführt.
    • Der Zustand des Enumerator-Objekts wird auf nachgeändert.
    • Die MoveNext Methode kehrt false zum Aufrufer zurück, der angibt, dass die Iteration abgeschlossen ist.
  • Wenn das Ende des Iterator-Texts auftritt:
    • Der Zustand des Enumerator-Objekts wird auf nachgeändert.
    • Die MoveNext Methode kehrt false zum Aufrufer zurück, der angibt, dass die Iteration abgeschlossen ist.
  • Wenn eine Ausnahme ausgelöst und aus dem Iteratorblock propagiert wird:
    • Entsprechende finally -Blöcke im Iterator-Body werden von der Ausnahmefortpflanzung ausgeführt.
    • Der Zustand des Enumerator-Objekts wird auf nachgeändert.
    • Die Ausbreitung der Ausnahme setzt sich bis zum Aufrufer der MoveNext Methode fort.

15.15.5.3 Abrufen des aktuellen Werts

Die Eigenschaft eines Enumeratorobjekts Current wird von yield return Anweisungen im Iteratorblock beeinflusst.

Hinweis: Die Current Eigenschaft ist eine synchrone Eigenschaft für synchrone und asynchrone Iteratorobjekte. Hinweisende

Wenn sich ein Enumerationsobjekt im angehaltenen Zustand befindet, entspricht der Wert von Current dem Wert, der durch den vorherigen Aufruf von MoveNext festgelegt wurde. Wenn sich ein Enumeratorobjekt im Vorher-, Laufend-, oder Nachher-Zustand befindet, ist das Ergebnis beim Zugriff auf Current nicht definiert.

Für einen Iterator mit einem Ertragstyp, der nicht object ist, entspricht das Ergebnis des Zugriffs auf Current durch die Implementierung IEnumerable des Enumeratorobjekts dem Zugriff auf Current durch die Implementierung IEnumerator<T> des Enumeratorobjekts und der Umwandlung des Ergebnisses in object.

15.15.5.4 Ressourcen löschen

Die Dispose- oder DisposeAsync-Methode wird verwendet, um die Iteration zu bereinigen, indem das Enumerationsobjekt in den Zustand 'nach' versetzt wird.

  • Wenn der Zustand des Enumeratorobjekts vorher ist, ändert das Aufrufen von Dispose den Zustand zu nachher.
  • Wenn der Status des Enumerator-Objekts laufendist, ist das Ergebnis des Aufrufs von Dispose nicht spezifiziert.
  • Wenn der Status des Enumerator-Objekts ausgesetztist, wird Disposeaufgerufen:
    • Ändert den Zustand in laufend.
    • Führt alle finally-Blöcke aus, als ob die zuletzt ausgeführte yield return -Anweisung eine yield break -Anweisung wäre. Wenn dies dazu führt, dass eine Ausnahme ausgelöst und aus dem Iterator-Körper heraus propagiert wird, wird der Status des Enumerator-Objekts auf nach gesetzt und die Ausnahme wird an den Aufrufer der Dispose -Methode weitergegeben.
    • Ändert den Zustand in "Danach".
  • Wenn der Zustand des Enumeratorobjekts nachher ist, hat der Aufruf von Dispose keine Auswirkungen.

15.15.6 Aufzählbare Objekte

15.15.6.1 Allgemein

Wenn ein Funktionselement oder eine lokale Funktion, die einen enumerationsfähigen Schnittstellentyp zurückgibt, mithilfe eines Iteratorblocks implementiert wird, führt das Aufrufen des Funktionsmememers den Code im Iteratorblock nicht sofort aus. Stattdessen wird ein aufzählbares Objekt erstellt und zurückgegeben.

Die GetEnumerator- oder GetAsyncEnumerator-Methode des Aufzählungsobjekts gibt ein Enumeratorobjekt zurück, das den im Iteratorblock angegebenen Code kapselt, und der Code im Iteratorblock wird ausgeführt, wenn die Methode MoveNext oder MoveNextAsync des Enumeratorobjekts aufgerufen wird. Ein aufzählbares Objekt weist die folgenden Merkmale auf:

  • Er implementiert IEnumerable und IEnumerable<T> oder IAsyncEnumerable<T>, wo T ist der Ertragtyp des Iterators.
  • Sie wird mit einer Kopie der Argumentwerte (falls vorhanden) und dem Instanzwert initialisiert, der an das Funktionsmitglied übergeben wird.

Ein aufzählbares Objekt ist in der Regel eine Instanz einer vom Compiler generierten aufzählbaren Klasse, die den Code im Iteratorblock kapselt und die aufzählbaren Schnittstellen implementiert, aber andere Implementierungsmethoden sind möglich. Wenn eine aufzählbare Klasse vom Compiler generiert wird, wird diese Klasse direkt oder indirekt in der Klasse, die das Funktionsmitglied enthält, geschachtelt sein, sie wird private Zugriffsberechtigung haben und einen Namen, der für die Verwendung durch den Compiler reserviert ist, tragen (§6.4.3).

Ein aufzählbares Objekt kann mehr Schnittstellen als die oben angegebenen implementieren.

Hinweis: Ein aufzählbares Objekt kann zum Beispiel auch IEnumerator und IEnumerator<T> implementieren, so dass es sowohl als aufzählbares Objekt als auch als Aufzähler dienen kann. Normalerweise würde eine solche Implementierung ihre eigene Instanz (um Zuweisungen zu sparen) vom ersten Aufruf von GetEnumeratorzurückgeben. Nachfolgende Aufrufe von GetEnumerator, falls vorhanden, würden eine neue Klasseninstanz zurückgeben, in der Regel derselben Klasse, sodass Aufrufe verschiedener Enumerationsinstanzen sich nicht gegenseitig beeinflussen. Es kann nicht dieselbe Instanz zurückgeben, auch wenn der vorherige Enumerator bereits über das Ende der Sequenz hinausgegangen ist, da alle zukünftigen Aufrufe eines verbrauchten Enumerators Ausnahmen auslösen müssen. Hinweisende

15.15.6.2 Die GetEnumerator- oder GetAsyncEnumerator-Methode

Ein aufzählbares Objekt stellt eine Implementierung der GetEnumerator-Methoden der IEnumerable- und IEnumerable<T>-Schnittstellen bereit. Die beiden GetEnumerator Methoden verwenden eine gemeinsame Implementierung, die ein verfügbares Enumerationsobjekt erwirbt und zurückgibt. Das Enumerationsobjekt wird mit den Argumentwerten und Instanzwerten initialisiert, die beim Initialisieren des enumerationsfähigen Objekts gespeichert wurden, andernfalls funktioniert das Enumerationsobjekt wie in §15.15.5 beschrieben.

Ein asynchrones aufzählbares Objekt stellt eine Implementierung der GetAsyncEnumerator Methode der IAsyncEnumerable<T> Schnittstelle bereit. Diese Methode gibt ein verfügbares asynchrones Enumerationsobjekt zurück. Das Enumerationsobjekt wird mit den Argumentwerten und Instanzwerten initialisiert, die beim Initialisieren des enumerationsfähigen Objekts gespeichert wurden, andernfalls funktioniert das Enumerationsobjekt wie in §15.15.5 beschrieben.