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 ';'?
    ;

Ein class_declaration besteht aus einem optionalen Satz von Attributen (§22), gefolgt von einem optionalen Satz von class_modifiers (§15.2.2), gefolgt von einem optionalen partial Modifizierer (§15.2.7), gefolgt von dem Schlüsselwort class und einem Bezeichner, der die Klasse benennt, gefolgt von einer optionalen type_parameter_list (§15.2.3) gefolgt von einer optionalen class_base Spezifikation (§15.2.4)), gefolgt von einem optionalen Satz von type_parameter_constraints_clause s (§15.2.5), gefolgt von einer class_body (§15.2.6), optional gefolgt von einemSemikolon.

Eine Klassendeklaration stellt keine type_parameter_constraints_clausezur Verfügung, es sei denn, sie stellt auch eine type_parameter_list zur Verfügung.

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 handelt sich um einen Kompilierungszeitfehler für den Modifizierer, der new in einer Klassendeklaration angezeigt wird, 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 Teiltypdeklaration (§15.2.7) eine Barrierefreiheitsspezifikation (über die public, , protected, internalund private Modifizierer) enthält, muss diese Spezifikation allen anderen Teilen zustimmen, die eine Barrierefreiheitsspezifikation enthalten. Wenn kein Teil eines Teiltyps eine Barrierefreiheitsspezifikation enthält, erhält der Typ die entsprechende Standardbarrierefreiheit (§7.5.2).

Die abstractModifizierer sealedund static Modifizierer werden in den folgenden Unterclauses 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 abstrahiert sind, diese Variablen und Werte enthalten jedoch notwendigerweise null Verweise auf Instanzen von nicht abstrakten Klassen, die von den abstrakten Typen abgeleitet sind.
  • Eine abstrakte Klasse ist zulässig (aber nicht erforderlich), abstrakte Member zu enthalten.
  • Eine abstrakte Klasse kann nicht versiegelt werden.

Wenn eine nicht abstrakte Klasse von einer abstrakten Klasse abgeleitet wird, muss die nicht abstrakte Klasse tatsächliche Implementierungen aller geerbten abstrakten Member enthalten, wodurch diese abstrakten Member überschrieben werden.

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 Gein, aber da sie keine Implementierung von F, B muss auch als abstrakt deklariert werden. Klassenüberschreibungen C F und stellen eine tatsächliche Implementierung bereit. Da es keine abstrakten Member gibt C, C ist es zulässig (aber nicht erforderlich), nicht abstrakt zu 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 versiegelte Klasse nicht über abgeleitete Klassen verfügt, ist es insbesondere möglich, Aufrufe virtueller Funktionsmememmationen für versiegelte Klasseninstanzen in nicht virtuelle Aufrufe zu transformieren. Endnote

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 keinen Modifizierer enthalten sealed abstract . (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. Endnote

  • Eine statische Klasse darf keine Member mit protected, private protectedoder protected internal deklarierter Barrierefreiheit haben.

Es handelt sich um einen Kompilierungsfehler, um gegen diese Einschränkungen zu verstoßen.

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

  • Die namespace_or_type_name ist die T in einer namespace_or_type_name des Formulars T.I oder
  • Der namespace_or_type-Name ist der T in einem typeof_expression (§12.8.18) des Formulars typeof(T).

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

  • Die primary_expression ist die E in einer member_access (§12.8.7) des FormularsE.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 Arraytyp, einem neuen Ausdruck, einem Umwandlungsausdruck, einem Ausdruck, einem Ausdruck, einem Ausdruck als Ausdruck, einem sizeof Ausdruck oder einem Standardwertausdruck verwendet werden. Endnote

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
    : '<' type_parameters '>'
    ;

type_parameters
    : attributes? type_parameter
    | type_parameters ',' 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 Member 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, und B soll von Aabgeleitet werden . Da A keine direkte Basisklasse explizit angegeben wird, wird die direkte Basisklasse implizit objectangegeben.

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 nicht als direkte oder indirekte Basisklasse (§22.2.1) verwendet System.Attribute werden.

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: Die folgenden

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

class Z : X<Z.Y> {}

ist fehlerhaft, da in der Basisklassenspezifikation X<Z.Y> die direkte Basisklasse Z als gilt objectund daher (durch die Regeln von §7.8) Z nicht als Mitglied Ybetrachtet 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 sind D<int> C<int[]>, , B<IComparable<int[]>>, Aund 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). In Anbetracht dieser Definition hängt der vollständige Satz von Klassen, von denen eine Klasse abhängt, vom transitiven Schließen der direkt abhängigen Beziehung ab .

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. Abschließend wird das Beispiel

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

führt zu einem Kompilierzeitfehler, da A von (seiner direkten Basisklasse) abhängt B.C , die von (der unmittelbar eingeschlossenen Klasse) abhängt B , von der zirkel abhängig ist A.

Endbeispiel

Eine Klasse hängt nicht von den klassen ab, die darin geschachtelt sind.

Beispiel: Im folgenden Code

class A
{
    class B : A {}
}

Bhängt davon ab (da es sich sowohl um A die direkte Basisklasse als auch um die unmittelbar eingeschlossene Klasse handelt), aber A nicht von B (da B es sich weder um eine Basisklasse noch um eine eingeschlossene Klasse handeltA).A 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 in diesem Teil deklarierten Schnittstellen bereit; 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 Schnittstellentypen (§8.4, §18.2) konstruiert werden. 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_clauses
    : type_parameter_constraints_clause
    | type_parameter_constraints_clauses type_parameter_constraints_clause
    ;

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 Token set in einem Eigenschaftsaccessor 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 Bezugstypeinschränkung, die Werttypeinschränkung, die nicht NULL-Einschränkungnotnull struct oder die nicht verwaltete Typeinschränkung unmanagedclass sein. Der Klassentyp und die Bezugstypeinschränkung können die nullable_type_annotation enthalten.

Eine sekundäre Einschränkung kann eine interface_type oder type_parameter sein, optional gefolgt von einem nullable_type_annotation. Das Vorhandensein der nullable_type_annotatione* gibt an, dass das Typargument der nullable Bezugstyp sein darf, der einem nicht nullablen Bezugstyp 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 Verweistypeinschränkung und sekundäre Einschränkungen können die Null-Typanmerkung enthalten. Das Vorhandensein oder Fehlen dieser Anmerkung für den Typparameter gibt die Nullbarkeitserwartungen für das Typargument an:

  • Wenn die Einschränkung nicht die Nullwerte-Typanmerkung enthält, wird erwartet, dass das Typargument ein nicht nullabler Bezugstyp ist. Ein Compiler gibt möglicherweise eine Warnung aus, wenn das Typargument ein Nullwertverweistyp ist.
  • Wenn die Einschränkung die Nullwerte-Typanmerkung enthält, wird die Einschränkung sowohl durch einen nicht nullablen Verweistyp als auch durch einen nullablen Bezugstyp erfüllt.

Die Nullierbarkeit des Typarguments muss nicht mit der Nullierbarkeit des Typparameters übereinstimmen. Der Compiler gibt möglicherweise eine Warnung aus, wenn die Nullierbarkeit des Typparameters nicht mit der Nullierbarkeit 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. Endnote

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

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

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 nullabler Werttyp ist oder ein nicht nullabler Verweistyp zulässig ist, aber der Compiler kann eine Diagnosewarnung erzeugen.

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 wertfähiger Werttyp (§8.3.12) zwar als Werttyp klassifiziert wurde, die Werttypeinschränkung jedoch 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 System.Nullable<T> Typ gibt die Nicht-NULL-Werttypeinschränkung für T. Rekursiv konstruierte Formen T?? und Nullable<Nullable<T>> sind daher verboten. Endnote

Da unmanaged es sich nicht um ein Schlüsselwort handelt, ist in primary_constraint die nicht verwaltete Einschränkung immer syntaktisch mehrdeutig mit class_type. Aus Kompatibilitätsgründen wird eine Namenssuche (§12.8.4) des Namens unmanaged erfolgreich behandelt.class_type Andernfalls wird sie als nicht verwaltete Einschränkung behandelt.

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.

Zeigertypen dürfen niemals Typargumente sein und erfüllen keine Typeinschränkungen, auch nicht verwaltete Typen, obwohl sie nicht verwaltet werden.

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, S hä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 S dann 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 ist effektiv versiegelt, T sodass S er gezwungen wäre, denselben Typ zu haben wie T, wodurch der Bedarf an zwei Typparametern wegfällt.
  • Wenn S die Werttypeinschränkung vorhanden ist, T darf keine class_type Einschränkung vorhanden sein.
  • Wenn eine class_type Einschränkung vorhanden ist und T eine class_type Einschränkung B vorhanden ist, muss es eine Konvertierung der Identität oder impliziten Referenzkonvertierung von A zu B oder eine implizite Verweiskonvertierung von B in A.A S
  • 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 in B oder eine implizite Verweiskonvertierung von A B in .A

Es ist gültig, S damit die Werttypeinschränkung und T die Bezugstypeinschränkung vorhanden ist. 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 handelt sich um einen Kompilierungszeitfehler für type_parameter_constraints mit einer primary_constraint oder struct unmanaged einer constructor_constraint.

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 Zirkelzahl im Abhängigkeitsdiagramm 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 Cₓ wird wie folgt erstellt:

  • Wenn C es sich um einen geschachtelten Typ Outer.Inner handelt, handelt es sich um Cₓ einen geschachtelten Typ Outerₓ.Innerₓ.
  • Wenn C Cₓes sich um einen konstruierten Typ G<A¹, ..., Aⁿ> mit Typargumenten A¹, ..., Aⁿ handelt, handelt es sich um Cₓ den konstruierten Typ G<A¹ₓ, ..., Aⁿₓ>.
  • Wenn C es sich um einen Arraytyp E[] handelt, handelt es sich um Cₓ den Arraytyp Eₓ[].
  • Wenn C es sich um eine dynamische Datei handelt, ist objectdies Cₓ der Wert .
  • Andernfalls lautet CₓC.

Die effektive Basisklasse eines Typparameters T wird wie folgt definiert:

Lassen Sie uns R eine Reihe von Typen so sein, dass:

  • Für jede Einschränkung, die T ein Typparameter ist, R enthält die effektive Basisklasse.
  • Für jede Einschränkung, die T ein Strukturtyp ist, R enthält System.ValueType.
  • Enthält für jede Einschränkung dieses T Typs einen EnumerationstypSystem.EnumR.
  • Für jede Einschränkung, die T ein Delegattyp ist, R enthält die dynamische Löschung.
  • Für jede Einschränkung, die T ein Arraytyp ist, R enthält System.Array.
  • Für jede Einschränkung, die T ein Klassentyp ist, R enthält die dynamische Löschung.

Then

  • Wenn T die Werttypeinschränkung vorhanden ist, ist System.ValueTypedie 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 objectdie effektive Basisklasse von T . 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 vorhanden ist, ist der effektive Schnittstellensatz leer.
  • Wenn T interface_type Einschränkungen, aber keine type_parameter Einschränkungen vorhanden sind, ist der effektive Schnittstellensatz die dynamische Löschung 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 vorhanden sind, ist der effektive Schnittstellensatz die Vereinigung der dynamischen Löschungen ihrer interface_type Einschränkungen und die effektiven Schnittstellensätze 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 Typparametertyps können verwendet werden, um auf die Instanzmember zuzugreifen, die von den Einschränkungen impliziert werden.

Beispiel: Im Folgenden:

interface IPrintable
{
    void Print();
}

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

Die Methoden IPrintable können direkt x aufgerufen werden, da T sie auf die Implementierung IPrintablebeschränkt ist.

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 gleichwertig, wenn sie dieselben Member 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 Teilerklärungen

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 nur eine besondere Bedeutung unmittelbar vor einem der Schlüsselwörter class, , structoder interface.

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 zusammen zusammengestellt werden, sodass die Teile zur Kompilierungszeit 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 auch verwendet partial , und jeder Teil des geschachtelten Typs wird in einem anderen Teil 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 Teildeklaration angegeben sind, wird in §22.3 erläutert.

15.3 Klassenmitglieder

15.3.1 Allgemein

Die Member einer Klasse bestehen aus den Membern, die durch ihre class_member_declarationund die Von der direkten Basisklasse geerbten Member eingeführt wurden.

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_declarationunmittelbar in der class_declaration neue Member in diesen Deklarationsbereich einführen. Die folgenden Regeln gelten für class_member_declaration:

  • 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. Endnote

  • 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 Member einer Klasse (§15.3.4) sind nicht Teil des Deklarationsraums 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). Endnote

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 Barrierefreiheitsdomäne jedes Mitglieds umfasst immer alle Teile des eingeschlossenen Typs; Ein in einem Teil deklariertes privates Mitglied ist frei zugänglich von einem anderen Teil. Es handelt sich um einen Kompilierungsfehler, um dasselbe Element in mehr als einem Teil des Typs zu deklarieren, es sei denn, dieses Element ist ein Typ mit dem partial Modifizierer.

Beispiel:

partial class A
{
    int x;                   // Error, cannot declare x more than once

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

partial class A
{
    int x;                   // Error, cannot declare x more than once

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

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 für this Code, der in 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 Elemente konstruierter Typen

Die nicht geerbten Member eines konstruierten Typs werden durch Substituieren der einzelnen type_parameter in der Memberdeklaration durch die entsprechende type_argument des konstruierten Typs abgerufen. Der Ersetzungsprozess basiert auf der semantischen Bedeutung von Typdeklarationen und ist nicht einfach nur textbezogene Ersetzung.

Beispiel: Aufgrund der generischen 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 des this Instanztyps (§15.3.2) der enthaltenden 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 Member der 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 , und B wird von Aabgeleitet, dann erbt die C in deklarierten B Member sowie die in deklarierten Member.AB

  • Eine abgeleitete Klasse erweitert ihre direkte Basisklasse. Eine abgeleitete Klasse kann den geerbten Membern neue Member hinzufügen, aber die Definition eines geerbten Members kann nicht entfernt werden.

  • Instanzkonstruktoren, Finalizer und statische Konstruktoren werden nicht geerbt, aber alle anderen Member sind unabhängig von ihrer deklarierten Barrierefreiheit (§7.5). Je nach deklarierter Barrierefreiheit sind geerbte Member jedoch in einer abgeleiteten Klasse möglicherweise nicht zugänglich.

  • Eine abgeleitete Klasse kann geerbte Member (§7.7.2.3) ausblenden, indem neue Member mit demselben Namen oder derselben Signatur deklariert werden. Durch das Ausblenden eines geerbten Elements wird dieses Element jedoch nicht entfernt, sondern nur, dass dieses Element direkt über die abgeleitete Klasse nicht darauf zugreifen kann.

  • 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 Member eines konstruierten Klassentyps sind die Member des unmittelbaren Basisklassentyps (§15.2.4.2), die durch Substituieren der Typargumente des konstruierten Typs für jedes Vorkommen der entsprechenden Typparameter in der base_class_specification gefunden werden. Diese Member werden wiederum durch Substituieren für jede type_parameter in der Mitgliedsdeklaration durch die entsprechende type_argument der base_class_specification transformiert.

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 Element, G(string s) int das durch Das Ersetzen des Typarguments int für den Typparameter Tabgerufen wird. D<int> verfügt außerdem über ein geerbtes Element aus der Klassendeklaration B. Dieses geerbte Element wird bestimmt, indem zuerst der Basisklassentyp B<int[]> D<int> durch Substituieren T int in der Basisklassenspezifikation B<T[]>bestimmt wird. Anschließend wird als Typargument Bfür " int[] in public U F(long index)" das geerbte Element public int[] F(long index)zurückgegebenU.

Endbeispiel

15.3.5 Der neue Modifizierer

Ein class_member_declaration darf ein Mitglied mit demselben Namen oder derselben Signatur wie ein geerbtes Mitglied deklarieren. Wenn dies geschieht, wird das abgeleitete Klassenmememmelement zum Ausblenden des Basisklassenelements gesagt. 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 M auf sie zugegriffen werden kann, und es gibt kein anderes geerbtes, barrierefreies Element N, das bereits ausgeblendet wirdM. Das implizite Ausblenden eines geerbten Elements wird nicht als Fehler betrachtet, führt jedoch dazu, dass der Compiler eine Warnung ausgibt, es sei denn, die Deklaration des abgeleiteten Klassenmembers enthält einen new Modifizierer, um explizit anzugeben, dass das abgeleitete Element das Basiselement 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 Modifizierer in einer Deklaration enthalten ist, die kein verfügbares geerbtes Element ausblendet, wird eine Warnung zu diesem Effekt ausgegeben.

15.3.6 Zugriffsmodifizierer

Eine class_member_declaration kann eine der zulässigen Arten von deklarierter Barrierefreiheit (§7.5.2): public, , protectedprotected internal, , private protected, internaloder private. Mit Ausnahme der protected internal Kombinationen ist private protected es ein Kompilierungszeitfehler, um mehrere Zugriffsmodifizierer anzugeben. Wenn ein class_member_declaration keine Zugriffsmodifizierer enthält, private wird davon ausgegangen.

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 so zugänglich wie dieses Mitglied selbst (§7.5.5).

15.3.8 Statische und Instanzmitglieder

Member einer Klasse sind statische Member oder Instanzmember.

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. Endnote

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 Member weisen die folgenden Merkmale auf:

  • 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 handelt sich um einen Kompilierungszeitfehler, um E eine Instanz zu kennzeichnen.
  • 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 Instanzfunktionsmemm (Methode, Eigenschaft, Indexer, Instanzkonstruktor oder Finalizer) wird für eine bestimmte Instanz der Klasse ausgeführt, und auf diese Instanz kann zugegriffen this werden (§12.8.14).

Beispiel: Im folgenden Beispiel werden die Regeln für den Zugriff auf statische und Instanzmmber 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 sich bei einem statischen Funktionsmememm um einen Kompilierungszeitfehler handelt, um über eine simple_name auf ein Instanzelement zuzugreifen. Die Main Methode zeigt, dass in einem member_access (§12.8.7) Instanzmember über Instanzen zugegriffen werden soll, und statische Member müssen über Typen aufgerufen werden.

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 geschachtelte Typdeklaration S.N , wobei S der vollqualifizierte Name der Typdeklaration, in der der Typ N deklariert wird, N und der nicht qualifizierte Name (§7.8.2) der Deklaration des geschachtelten Typs (einschließlich aller generic_dimension_specifier (§12.8.18)) ist.

15.3.9.3 Barrierefreiheit deklariert

Nicht geschachtelte Typen können Barrierefreiheit aufweisen public oder internal deklariert haben und standardmäßig die Barrierefreiheit deklariert haben internal . 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 ist, kann über eine der zulässigen Arten deklarierter Barrierefreiheit verfügen und wie andere Klassenmember standardmäßig die private deklarierte Barrierefreiheit aufweisen.
  • Ein geschachtelter Typ, der in einer Struktur deklariert ist, kann drei Formen der deklarierten Barrierefreiheit (public, internaloder private) aufweisen und, wie andere Strukturmember, standardmäßig deklarierte private Barrierefreiheit aufweisen.

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 M in Base.

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 innerhalb eines geschachtelten Typs nicht verwendet werden, this um auf Instanzenmember des enthaltenden Typs zu verweisen. In Fällen, in denen ein geschachtelter Typ Zugriff auf die Instanzmember des zugehörigen Typs benötigt, kann der Zugriff bereitgestellt werden, indem er die this Instanz des enthaltenden Typs als Konstruktorargument für den geschachtelten Typ bereitstellt.

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 erstellt eine Instanz von NestedC , und übergibt dies Nestedan den Konstruktor, um nachfolgenden Zugriff auf die Instanzmember zu Cermöglichen.

Endbeispiel

15.3.9.6 Zugriff auf private und geschützte Member des enthaltenden Typs

Ein geschachtelter Typ hat Zugriff auf alle Member, auf die der enthaltende Typ zugreifen kann, einschließlich Elemente des enthaltenden Typs, die Barrierefreiheit aufweisen und protected deklariert habenprivate.

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 Nestedder Methode wird die in der Methode definierte statische Methode G F aufgerufen Cund F die private Barrierefreiheit deklariert.

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 auf die in Derivedder Basisklasse definierte geschützte Methode F zu, Baseindem sie eine Instanz von Derived.

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. Aus der äußeren Klasse kann der geschachtelte Typ jedoch ohne Qualifikation verwendet werden; Der Instanztyp der äußeren Klasse kann implizit beim Erstellen des geschachtelten Typs verwendet werden.

Beispiel: Im Folgenden werden drei verschiedene richtige Methoden zum Verweisen auf einen konstruierten Typ gezeigt, der erstellt Innerwurde; 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 Membernamen

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.

Endnote

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 Membernamen für Eigenschaften reserviert

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 schreibgeschü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 Methoden reserviert werden set_P . A Die Klasse B wird von beiden reservierten Signaturen abgeleitet A und ausgeblendet. Das Beispiel erzeugt die Ausgabe:

123
123
456

Endbeispiel

15.3.10.3 Membernamen, 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 Membernamen, 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 Mitgliedsnamen für Finalizer reserviert

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 explizite (schmalende) Koersion
op_False false
op_GreaterThan >
op_GreaterThanOrEqual >=
op_Implicit implizite (Verbreiterung) Koersion
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 Member betrachtet werden, erfordert ein constant_declaration weder einen Modifizierer noch lässt einen static Modifizierer zu. Es handelt sich um einen Fehler für denselben Modifizierer, der mehrmals in einer Konstantendeklaration angezeigt wird.

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 , , byte, short, ushort, int, uint, long, chardecimalstringulongbooldoublefloatein enum_type oder ein reference_type sein.sbyte 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 Konstante wird in einem Ausdruck mithilfe eines simple_name (§12.8.4) oder einer 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. Endnote

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 Endnote

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). Endnote

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

für die folgende Syntax:

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

Endbeispiel

Konstanten dürfen von anderen Konstanten innerhalb desselben Programms abhängig sein, solange die Abhängigkeiten nicht zirkulär sind. Der Compiler ordnet automatisch die Konstantendeklarationen in der entsprechenden Reihenfolge an.

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

Der Compiler wertet A.Yzuerst aus, wertet dann aus und wertet B.ZA.Xschließlich aus, erzeugt die Werte 10, 11und 12.

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 A Beispiel verwiesen und B in separaten Programmen deklariert wurde, wäre es möglich A.X , von diesen abhängig zu sein B.Z, aber B.Z dann nicht gleichzeitig davon abhängig zu sein A.Y. 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 für denselben Modifizierer, der mehrmals in einem field_declaration angezeigt wird.

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 gelesenen Felds wird mithilfe einer Zuordnung geändert (§12.21). Der Wert eines nicht readonly-Felds kann mit postfix-Inkrement- und Dekrementoperatoren (§12.8.16) und Präfixoperatoren (§12.9.6) abgerufen 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;
}

für die folgende Syntax:

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 Readonly-Felder

15.5.3.1 Allgemein

Wenn ein field_declaration einen Modifizierer enthält, sind die durch die Deklaration eingeführten Felder schreibgeschützt.readonly Direkte Zuordnungen 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 der variable_declarator , die das Feld einführt (indem ein variable_initializer in die Deklaration eingeschlossen 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 lesegeschützten Feld zuzuweisen oder als Ausgabe- oder Referenzparameter in einem anderen Kontext zu übergeben, ist ein Kompilierungsfehler.

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 viel die gleiche Wirkung.

Endbeispiel

15.5.3.3 Versionsverwaltung von Konstanten und statischen Readonly-Feldern

Konstanten und Readonly-Felder weisen unterschiedliche binäre Versionsverwaltungssemantik auf. 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);
        }
    }
}

Die Program1 Namespaces weisen Program2 zwei Programme auf, die separat kompiliert werden. Da Program1.Utils.X sie als static readonly Feld deklariert wird, wird die Wertausgabe der Console.WriteLine Anweisung zur Kompilierungszeit nicht bekannt, sondern zur Laufzeit abgerufen. Wenn der Wert geändert X und Program1 neu kompiliert wird, gibt die Console.WriteLine Anweisung den neuen Wert aus, auch wenn Program2 er nicht neu kompiliert wird. Wenn X es sich jedoch um eine Konstante handelte, wäre der Wert X zum Zeitpunkt Program2 der Kompilierung erhalten worden und würde von Änderungen Program1 Program2 bis zum erneuten Kompilieren unberührt bleiben.

Endbeispiel

15.5.4 Veränderliche Felder

Wenn ein field_declaration einen Modifizierer enthält, sind die durch diese Deklaration eingeführten Felder veränderliche Felder.volatile 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. Bei veränderliche Felder sind solche Neuanordnungsoptimierungen eingeschränkt:

  • Ein Lesen eines veränderliche Felds wird als veränderliche Lesefunktion bezeichnet. Ein veränderliches Lesen hat "Semantik erwerben"; d. h., es wird garantiert vor allen Verweisen auf den Speicher auftreten, die nach der Anweisungssequenz auftreten.
  • Ein Schreibvorgang eines veränderliche Felds wird als veränderliche Schreibzugriff bezeichnet. Ein veränderliche Schreibvorgang hat "Releasesemantik"; d. h., es ist garantiert, dass nach allen Speicherverweisen vor der Schreibanweisung in der Anweisungssequenz geschehen.

Diese Einschränkungen stellen sicher, dass alle Threads volatile-Schreibvorgänge, die von einem anderen Thread ausgeführt werden, in der Reihenfolge ihrer Ausführung berücksichtigen. Eine konforme Implementierung ist nicht erforderlich, um eine einzige Gesamtanzahl von veränderliche Schreibvorgängen bereitzustellen, wie aus allen Threads der Ausführung zu sehen. Die Art eines veränderliche Felds muss eine der folgenden Sein:

  • Eine reference_type.
  • Eine type_parameter , die als Bezugstyp bekannt ist (§15.2.5).
  • Der Typ byte, , sbyte, short, intushort, uint, char, , float, bool, , oder System.IntPtrSystem.UIntPtr. .
  • Ein enum_type einen enum_base Typ von byte, , sbyte, short, ushort, oder intuint.

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 veränderliche Feld gespeichert, das dann resultim veränderliche Feld finishedgespeichert true wird. Der Hauptthread wartet auf das Feld, auf das das Feld finished festgelegt trueist, und liest dann das Feld resultvor. Da finished deklariert volatilewurde, muss der Hauptthread den Wert 143 aus dem Feld resultlesen. Wenn das Feld finished nicht deklariert volatilewurde, wäre es zulässig, dass der Speicher result nach dem Speichern finishedfür den Hauptthread sichtbar ist, und damit der Hauptthread den Wert 0 aus dem Feld resultliest. 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 Variable Initialisierer

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. Beispielsweise Felder entsprechen variablen Initialisierern 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 x ausgeführt wird, wenn statische Feldinitialisierer ausgeführt und zugewiesen i s werden, 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 an. Trotz der Zirkeldefinitionen und a b-definitionen ist das Programm gültig. Es führt zu der Ausgabe

a = 1, b = 2

da die statischen Felder a und initialisiert 0 werden (der Standardwert für int) bevor ihre Initialisierer b ausgeführt werden. Wenn der Initialisierer für a die Ausführung ausgeführt wird, ist der Wert null b und wird daher a initialisiert in 1. Wenn der Initialisierer ausgeführt b wird, ist der Wert eines bereits 1vorhanden und wird daher b initialisiert in 2.

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

kann entweder die Ausgabe erzeugen:

Init A
Init B
1 1

oder die Ausgabe:

Init B
Init A
1 1

da die Ausführung von X's initializer' und Y's initializer in beiden Reihenfolge auftreten kann; sie sind nur eingeschränkt, bevor die Verweise auf diese Felder erfolgen. 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 ist zu folgendem Ergebnis zu führen:

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 den in einem Variableninitialisierer verwiesen this wird, da es sich um einen Kompilierungszeitfehler für einen Variableninitialisierer handelt, um über eine simple_name auf ein Instanzmemm zu verweisen.

Beispiel: Im folgenden Code

class A
{
    int x = 1;
    int y = x + 1;     // Error, reference to instance member of this
}

Der Variable initializer für y ergebnisse in einem Kompilierungszeitfehler, da er auf ein Element 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'
    | 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.
  • bei der Anerkennung einer method_body , wenn sowohl die alternativen null_conditional_invocation_expression als auch die Ausdrucksalternativen anwendbar sind, wird der erste Ausgewä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. Endnote

Eine method_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), der new (§15.3.5), static (§15.6.3), virtual (§15.3) enthalten.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7), extern (§15.6.8) und async (§15.15) Modifizierer.

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 Deklaration den abstract Modifizierer enthält, enthält die Deklaration keine der folgenden Modifizierer: static, , , virtualsealedoder 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, , , publicprivatesealedvirtualprotectedinternal, override, , oder . abstractextern

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

  • Wenn ref vorhanden, wird die Methode nach Ref zurückgegeben und gibt einen Variablenverweis zurück, der optional schreibgeschützt ist;
  • Andernfalls gibt die Methode, wenn return_type istvoid, den Wert "no-value" zurück und gibt keinen Wert zurück.
  • Andernfalls wird die Methode zurückgegeben nach Wert und gibt einen Wert zurück.

Die return_type einer Rückgabe-nach-Wert- oder Rückgabe-no-Wert-Methodendeklaration gibt den Typ des Ergebnisses an, falls vorhanden, von der Methode zurückgegeben. Nur eine Rückgabe-no-Wert-Methode kann den partial Modifizierer (§15.6.9) enthalten. 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.15.1).

Die ref_return_type einer Rückgabe-nach-Bezug-Methodendeklaration gibt den Typ der Variablen an, auf die von der methode zurückgegebenen variable_reference verwiesen wird.

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_clause angeben die Einschränkungen für die Typparameter.

Eine generische method_declaration für eine explizite Schnittstellenmemberimplementierung darf keine type_parameter_constraints_clauses aufweisen; die Deklaration erbt alle Einschränkungen von den Einschränkungen für die Schnittstellenmethode.

Ebenso darf eine Methodendeklaration mit dem override Modifizierer keine type_parameter_constraints_clausehaben, 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 Schnittstellenmememimplementierung besteht die member_name aus einem interface_type 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).

Die 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).

Die method_body einer Rückgabe-nach-Wert- oder Rückgabe-No-Value-Methode ist entweder ein Semikolon, ein Blocktext oder ein Ausdruckstext. Ein Blocktext besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn die Methode aufgerufen wird. Ein Ausdruckstext =>besteht aus , gefolgt von einem null_conditional_invocation_expression oder Ausdruck und einem Semikolon und zeigt einen einzelnen Ausdruck an, der ausgeführt werden soll, wenn die Methode aufgerufen wird.

Bei abstrakten und externen Methoden besteht die method_body einfach aus einem Semikolon. Bei Teilmethoden kann die method_body entweder aus einem Semikolon, einem Blocktext oder einem Ausdruckstext 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.

Die ref_method_body einer Returns-by-Ref-Methode ist entweder ein Semikolon, ein Blocktext oder ein Ausdruckstext. Ein Blocktext besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn die Methode aufgerufen wird. Ein Ausdruckstext =>besteht aus , gefolgt von ref, einem variable_reference und einem Semikolon und gibt einen einzelnen variable_reference an, der ausgewertet werden soll, wenn die Methode aufgerufen wird.

Bei abstrakten und externen Methoden besteht die ref_method_body einfach aus einem Semikolon; für alle anderen Methoden ist die ref_method_body entweder ein Blocktext oder ein Ausdruckstext.

Der Name, die Anzahl der Typparameter und die Parameterliste einer Methode definieren die Signatur (§7.6) der Methode. Insbesondere besteht die Signatur einer Methode aus ihrem Namen, der Anzahl der Typparameter und der Zahl, parameter_mode_modifier s (§15.6.2.1) undtypen 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_parameterder Methode sind im gesamten method_declaration im Gültigkeitsbereich enthalten und können für Formulartypen in diesem Bereich in return_type oder ref_return_type, method_body oder ref_method_body und type_parameter_constraints_clause, aber nicht in Attributen verwendet werden.

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 refOder out this modifizierer kann keine default_argument haben. Ein Eingabeparameter hat möglicherweise eine default_argument. Der Ausdruck in einem default_argument muss eine der folgenden Sein:

  • ein constant_expression
  • ein Ausdruck des Formulars new S() , in dem S es sich um einen Werttyp handelt
  • ein Ausdruck des Formulars default(S) , in dem S es sich um einen Werttyp handelt

Der Ausdruck muss implizit durch eine Identitäts- oder Null-Konvertierung in den Typ des Parameters konvertiert werden.

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

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 Arraytyp (§17.2) sein. Bei einem Methodenaufruf lässt ein Parameterarray entweder ein einzelnes Argument des angegebenen Arraytyps anzugeben, oder es kann null oder mehr Argumente des Arrayelementtyps angegeben werden. 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 , ist ein erforderlicher ref Parameter, d ein erforderlicher Wertparameter, b, s, o und sind optionale Wertparameter und a t ein Parameterarray. i M

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. Wenn vorhanden, wird der Textkörper der Methode als in diesem Deklarationsbereich geschachtelt betrachtet. Es ist ein Fehler für zwei Member eines Methodendeklarationsraums, um denselben Namen zu 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 inModifizierer outund ref Modifizierer Teil der Signatur einer Methode, der params Modifizierer ist jedoch nicht. Endnote

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-Reference-Parameter

15.6.2.3.1 Allgemein

Eingabe-, Ausgabe- und Referenzparameter sind Nachverweisparameter. 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.14) oder asynchrone Funktion (§15.15) 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 (hinsichtlich der Speicheranforderungen) ist, ist es nützlich, den gesamten Wert des Arguments beim Aufrufen der Methode zu vermeiden. 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. Endnote

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, x represents i and y represents j. Daher hat der Aufruf die Auswirkung, die Werte von i und j.

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 der F In-Aufrufe G übergibt einen Verweis s auf beide a und 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 this Schlüsselwort innerhalb einer Instanzmethode, des Instanzaccessors (§12.2.1) 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. Endnote

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

c:\Windows\System\
hello.txt

Beachten Sie, dass die dir und name die Variablen nicht zugewiesen werden können, bevor sie an sie übergeben SplitPathwerden, und dass sie nach dem Aufruf als definitiv zugewiesen gelten.

Endbeispiel

15.6.2.4 Parameterarrays

Ein mit einem Modifizierer deklarierter params Parameter ist ein Parameterarray. Wenn eine Parameterliste ein Parameterarray enthält, muss es sich um den letzten Parameter in der Liste und es muss sich um einen eindimensionalen Arraytyp handelt.

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, oder outref. Endnote

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 eines F Nullelements int[] ein Nullelement 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 implizit konvertierbarer Typ objectist. Die Mehrdeutigkeit stellt jedoch kein Problem dar, da es gelöst werden kann, indem bei Bedarf eine Umwandlung eingefügt wird.

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). Die erweiterte Form von F ist jedoch anwendbar, sodass sie durch Überladungsauflösung ausgewählt wird. Daher wird ein 1-Element object[] durch den Aufruf erstellt, und das einzelne Element des Arrays wird mit dem angegebenen Argumentwert initialisiert (was selbst ein Verweis auf einen object[]).

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 funktioniert nicht für eine bestimmte Instanz, und es handelt sich um einen Kompilierungszeitfehler, auf this den in einer statischen Methode verwiesen wird.

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 benannte N Methode mit einer Argumentliste A in einer Instanz mit einem Kompilierungszeittyp und einem Laufzeittyp C R aufgerufen wird (wobei R eine C oder eine klasse abgeleitet Cist), wird der Aufruf wie folgt verarbeitet:

  • Bei Bindungszeit wird die Überladungsauflösung auf , und , um Ceine bestimmte Methode M aus der Gruppe der methoden auszuwählen, die in und geerbt von C.AN Dies wird in §12.8.10.2 beschrieben.
  • Dann zur Laufzeit:
    • Wenn M es sich um eine nicht virtuelle Methode handelt, M wird diese aufgerufen.
    • M Andernfalls handelt es sich um eine virtuelle Methode, und die abgeleitete Implementierung M in Bezug auf diese R Methode 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 eingeführte virtuelle Erklärung Menthalten ist, ist dies die abgeleitetste Implementierung M in Bezug auf R.
  • Andernfalls, wenn R eine Außerkraftsetzung von Menthält, ist dies die abgeleitetste Implementierung M in Bezug auf R.
  • Andernfalls entspricht die am meisten abgeleitete Implementierung M in Bezug auf R die abgeleitete Implementierung M in Bezug auf die direkte 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 Fein, wodurch die geerbte FMethode ausgeblendet wird, und überschreibt auch die geerbte MethodeG. Das Beispiel erzeugt die Ausgabe:

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

Beachten Sie, dass die Anweisung a.G() aufgerufen wird B.G, nicht A.G. 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 abgeleiteten Methoden ausgeblendet 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 von A und die von .C Die von der Methode eingeführte C Methode blendet die von A. Daher überschreibt die Überschreibungsdeklaration in D der von C, und es ist nicht möglich D , die von Ader Methode eingeführte Methode außer Kraft zu setzen. Das Beispiel erzeugt 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 Override-Methoden

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 Überschreibungsdeklaration überschrieben wird, wird als überschriebene Basismethode für eine in einer Klasse Cdeklarierte Außerkraftsetzungsmethode M bezeichnet, die überschriebene Basismethode wird bestimmt, indem jede Basisklasse Cuntersucht wird, beginnend mit der direkten Basisklasse von C und fortgesetzt mit jeder aufeinander folgenden direkten Basisklasse, bis sich in einem bestimmten Basisklassentyp mindestens eine barrierefreie Methode befindet, die dieselbe Signatur wie M nach der Ersetzung von Typargumenten aufweist. Für die Suche nach der überschriebenen Basismethode wird eine Methode als barrierefrei betrachtet, wenn es publicsich um , wenn es protectedsich um , wenn es sich handelt protected internal, oder wenn sie entweder oder private protected oder und im selben Programm deklariert ist internal wie C.

Ein Kompilierungszeitfehler tritt auf, es sei denn, für eine Außerkraftsetzungsdeklaration gilt Folgendes:

  • Eine überschriebene Basismethode kann wie oben beschrieben gefunden 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 überschriebene Basismethode ist eine virtuelle, abstrakte oder Außerkraftsetzungsmethode. Mit anderen Worten, die überschriebene Basismethode kann nicht statisch oder nicht virtuell sein.
  • Die überschriebene Basismethode ist keine versiegelte Methode.
  • Es gibt eine Identitätskonvertierung zwischen dem Rückgabetyp der überschriebenen Basismethode und der Überschreibungsmethode.
  • Die Überschreibungsdeklaration und die überschriebene Basismethode weisen die gleiche deklarierte Barrierefreiheit auf. Anders ausgedrückt: Eine Außerkraftsetzungsdeklaration kann die Barrierefreiheit der virtuellen Methode nicht ändern. Wenn die überschriebene Basismethode jedoch intern geschützt ist und sie in einer anderen Assembly als die Assembly deklariert wird, die die Überschreibungsdeklaration enthält, muss die deklarierte Barrierefreiheit der Deklaration überschreiben.
  • Die Außerkraftsetzungsdeklaration gibt keine type_parameter_constraints_clauses an. Stattdessen werden die Einschränkungen von der überschriebenen 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 Außerkraftsetzungsdeklaration kann mithilfe einer base_access (§12.8.15) auf die überschriebene Basismethode zugreifen.

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 base.PrintFields() Aufruf aufruft B die printFields-Methode, die in Adeklariert ist. Ein base_access deaktiviert den virtuellen Aufrufmechanismus und behandelt einfach die Basismethode als nicht-Methodevirtual . Wenn der Aufruf B geschrieben wurde, würde sie die in deklarierte BMethode rekursiv aufrufenPrintFields, nicht die in , nicht die in A, da PrintFields ist virtuell und der Laufzeittyp ist ((A)this) B.((A)this).PrintFields()

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 override keinen Modifizierer und überschreibt daher die F Methode nicht in A. Stattdessen wird die Methode in B der F Methode Aausgeblendet, und eine Warnung wird gemeldet, da die Deklaration keinen neuen Modifizierer 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 F Methode in B blendet die virtuelle F Methode aus, von Ader geerbt wird. Da das neue F In B privaten Zugriff hat, umfasst sein Bereich nur den Klassentext von B und wird nicht auf C. Daher darf die In-Deklaration C F die F geerbte Von A.

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 override Modifizierer 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 stellt zwei Außerkraftsetzungsmethoden bereit: eine F Methode mit sealed dem Modifizierer und einer G Methode, die nicht verwendet wird. BDie Verwendung des sealed Modifizierers verhindert C die weitere Außerkraftsetzung 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 müssen nicht abstrakte abgeleitete Klassen ihre eigene Implementierung bereitstellen, indem diese Methode überschrieben wird. 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 Klassen sind Box 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, mit dem eine Verknüpfung mit einer externen Methode erreicht wird, wird durch implementierungsdefiniert.

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 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 den Teilen einer Typdeklaration kann es nur eine definierende partielle Methodendeklaration mit einer bestimmten Signatur geben, und es kann nur eine implementierung teilweise Methodendeklaration mit einer bestimmten Signatur geben. 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.

Nur eine definierende Partielle Methode nimmt an der Überladungsauflösung teil. 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 keine Übereinstimmung mit Parameternamen. Dies kann zu überraschenden, wenn auch gut definierten Verhaltensweisen führen, wenn benannte Argumente (§12.6.2.1) verwendet werden. Beispiel: Angesichts der definierenden partiellen Methodendeklaration für M eine Datei und der implementierenden 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.

Endnote

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 handelt sich um einen Kompilierungsfehler, um einen Delegat aus M (§12.8.17.6) zu erstellen.

  • Es handelt sich um einen Kompilierungszeitfehler, der M in eine anonyme Funktion verweist, die in einen Ausdrucksstrukturtyp konvertiert wird (§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 der Einstiegspunkt für eine Bewerbung (§7.1) nicht sein kann.

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 kann sich nur um einen Referenzparameter handelt, wenn er einen Werttyp aufweist oder einen generischen Typ auf die Struktur beschränkt hat.
  • 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. Wenn sich die eingeschlossene statische Klasse im Bereich befindet, kann eine Erweiterungsmethode mithilfe der Instanzmethodenaufrufssyntax (§12.8.10.3) mithilfe des Empfängerausdrucks als erstes Argument aufgerufen werden.

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 für das string[], und die ToInt32 Methode ist verfügbar für 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 istvoid, wenn der Rückgabetyp lautet voidoder die Methode asynchron ist und der Rückgabetyp (§15.15.1) lautet «TaskType» . 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.15.1) lautet T.

Wenn der effektive Rückgabetyp einer Methode ist void und die Methode über einen Blocktext verfügt, return dürfen Anweisungen (§13.10.5) im Block keinen Ausdruck angeben. Wenn die Ausführung des Blocks einer void-Methode normal abgeschlossen ist (d. h. die Steuerung fließt vom Ende des Methodentexts ab), wird diese Methode einfach an den Aufrufer zurückgegeben.

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 Rückgabe-by-Ref-Methode (§15.6.1) gibt jede Rückgabeanweisung im Textkörper dieser Methode einen Ausdruck an, dessen Typ der effektiven Rückgabetyp ist und über einen ref-safe-context des Caller-Context (§9.7.2) verfügt.

Für Rückgabe-nach-Wert- und Rückgabemethoden ist der Endpunkt des Methodentexts nicht erreichbar. Mit anderen Worten, die Steuerung darf nicht vom Ende des Methodentexts fließ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 Methode für die Wertrückgabe F führt zu einem Kompilierungszeitfehler, da die Steuerung das Ende des Methodentexts beenden kann. Die G Methoden sind H richtig, da alle möglichen Ausführungspfade in einer Rückgabe-Anweisung 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'
    | 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.

Es gibt zwei Arten von property_declaration:

  • Die erste deklariert eine Nicht-Verweis-Eigenschaft. Der Wert weist Typ auf. Diese Art von Eigenschaft kann lesbar und/oder schreibbar sein.
  • Die zweite deklariert eine ref-valued-Eigenschaft. Der Wert ist eine variable_reference (§9.5), die eine Variable vom Typtyp sein readonlykann. Diese Art von Eigenschaft ist nur lesbar.

Eine property_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§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) Modifizierer.

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 Schnittstellenmemembeimplementierung handelt, ist die member_name einfach ein Bezeichner. Für eine explizite Schnittstellenmememimplementierung (§18.6.2) besteht die member_name aus einer interface_type gefolgt von einem "." und einem Bezeichner.

Der Typ einer Immobilie 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 werden sollen, die Accessoren (§15.7.3) der Eigenschaft. Die Accessoren geben die ausführbaren Anweisungen an, die dem Lesen und Schreiben der Eigenschaft zugeordnet sind.

In einem property_body einem Ausdruckstext, der => aus einem Ausdruck E besteht und ein Semikolon exakt dem Textkörper der Anweisung { get { return E; } }entspricht, und kann daher nur verwendet werden, um schreibgeschützte Eigenschaften anzugeben, bei denen das Ergebnis des Get-Accessors durch einen einzelnen Ausdruck angegeben wird.

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 Anweisungstext oder einem Ausdruckstext bestehen. In einem Anweisungstext deklariert ein get_accessor_declaration den Get-Accessor (§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 einem Ausdruckstext, der aus folgt refbesteht=>, entspricht ein variable_reference V und ein Semikolon exakt dem Textkörper der Anweisung{ 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. Endnote

Wenn eine Eigenschaftsdeklaration einen extern Modifizierer enthält, wird die Eigenschaft als externe Eigenschaft bezeichnet. Da eine externe Eigenschaftsdeklaration keine tatsächliche Implementierung bereitstellt, muss jedes accessor_bodys in seinem accessor_declarations ein Semikolon sein.

15.7.2 Statische Und Instanzeigenschaften

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 Instanzeigenschaft bezeichnet.

Eine statische Eigenschaft ist keiner bestimmten Instanz zugeordnet, und es handelt sich um einen Kompilierungszeitfehler, der in den Accessoren einer statischen Eigenschaft referenziert this werden soll.

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 wird in Bezug auf Eigenschaften geschrieben, wenn das Lesen von Indexern indexer/indexer für Eigenschaften/Eigenschaften ersetzt und die Liste der Unterschiede zwischen Eigenschaften und Indexern in §15.9.2 konsultieren. Endnote

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

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

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

Die accessor_declarations bestehen aus einem get_accessor_declaration, einem set_accessor_declaration 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 in einer expliziten Schnittstellenmememerimplementierung verwendet werden.
  • Für eine Eigenschaft oder einen Indexer ohne override Modifizierer ist eine accessor_modifier nur zulässig, wenn die Eigenschaft oder der Indexer über einen Get- und Set-Accessor verfügt und dann nur für einen dieser Accessoren zulässig ist.
  • Für eine Eigenschaft oder einen Indexer, die einen override Modifizierer enthält, muss ein Accessor mit dem accessor_modifier übereinstimmen, sofern vorhanden, des überschriebenen Accessors.
  • Die accessor_modifier deklarieren eine Barrierefreiheit, die streng restriktiver ist als die deklarierte Barrierefreiheit der Eigenschaft oder des Indexers selbst. Um genau zu sein:
    • Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit verfügt public, kann die von accessor_modifier deklarierte Barrierefreiheit entweder private protected, , protected internal, , internal, protectedoder private.
    • Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit verfügt protected internal, kann die von accessor_modifier deklarierte Barrierefreiheit entweder private protected, , protected private, , internal, protectedoder private.
    • Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit von internal oder protectedverfügt, muss die von accessor_modifier deklarierte Barrierefreiheit entweder private protected oder private.
    • Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit verfügt, lautet privatedie von accessor_modifier deklarierte Barrierefreiheitprivate protected.
    • Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit von privateverfügt, können keine accessor_modifier verwendet werden.

Für abstract nicht extern bezugswerte Eigenschaften ist jede accessor_body für jeden angegebenen Accessor einfach ein Semikolon. Eine nicht abstrakte, nicht externe Eigenschaft, aber kein Indexer, kann auch die accessor_body für alle Accessoren als Semikolon angegeben sein. In diesem Fall handelt es sich um eine automatisch implementierte Eigenschaft (§15.7.4). Eine automatisch implementierte Eigenschaft muss mindestens über einen Get-Accessor verfügen. 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
  • Ein Ausdruckstext, der => aus einem Ausdruck und einem Semikolon besteht und einen einzelnen Ausdruck angibt, der ausgeführt werden soll, wenn der entsprechende Accessor aufgerufen wird.

Bei abstract eigenschaften und extern ref-valued ist die ref_accessor_body einfach ein Semikolon. Für den Accessor anderer nicht abstrakter, nicht externer Eigenschaft ist die ref_accessor_body eine der folgenden:

  • ein Block , der die auszuführenden Anweisungen angibt, wenn der Get-Accessor aufgerufen wird; oder
  • ein Ausdruckstext, gefolgt von => ref, einem variable_reference und einem Semikolon. Der Variableverweis 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 des Ziels einer Zuweisung wird, wenn auf eine solche Eigenschaft in einem Ausdruck verwiesen wird, der Accessor zum Abrufen aufgerufen wird, um den Wert der Eigenschaft zu berechnen (§12.2.2).

Der Textkörper eines Get-Accessors für eine nicht refwertierte Eigenschaft muss den Regeln für rückgabefähige Methoden entsprechen, die in §15.6.11 beschrieben sind. Insbesondere müssen alle return Anweisungen im Textkörper 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-Accessor für eine referenzwertige Eigenschaft entspricht einer parameterlosen Methode mit einem Rückgabewert eines variable_reference einer Variablen des Eigenschaftstyps. Wenn auf eine solche Eigenschaft in einem Ausdruck verwiesen wird, wird der Accessor zum Abrufen aufgerufen, um den variable_reference Wert der Eigenschaft zu berechnen. Dieser Variablenverweis wird dann wie jeder andere verwendet, um die variable Variable_reference zu lesen oder für nicht-readonly variable_references die referenzierte Variable nach Bedarf im Kontext zu schreiben.

Beispiel: Im folgenden Beispiel wird eine Referenzeigenschaft als Ziel einer Zuordnung veranschaulicht:

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 Textkörper eines Get-Accessors für eine referenzwertige Eigenschaft muss den Regeln für refwertige 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. Die Einrichtung eines Set-Accessors entspricht den in void §15.6.11 beschriebenen Methoden. Insbesondere dürfen Rückgabeanweisungen im Textkörper des Set-Accessors keinen Ausdruck angeben. 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 Fehlen der Get- und Set-Accessoren wird eine Eigenschaft wie folgt klassifiziert:

  • Eine Eigenschaft, die sowohl einen Get-Accessor als auch einen Set-Accessor enthält, wird als Lese -/Schreibzugriffseigenschaft bezeichnet.
  • Eine Eigenschaft, die nur über einen Get-Accessor verfügt, wird als schreibgeschützte Eigenschaft bezeichnet. Es handelt sich um einen Kompilierungszeitfehler für eine schreibgeschützte Eigenschaft, die das Ziel einer Zuordnung sein soll.
  • Eine Eigenschaft, die nur einen Set-Accessor besitzt, wird als schreibgeschützte Eigenschaft bezeichnet. Mit Ausnahme des Ziels einer Zuordnung handelt es sich um einen Kompilierungszeitfehler, um auf eine schreibgeschützte Eigenschaft in einem Ausdruck zu verweisen.

Hinweis: Die Vor- und Postfix ++ - und Operatoren und -- Verbundzuordnungsoperatoren können nicht auf schreibgeschützte Eigenschaften angewendet werden, da diese Operatoren den alten Wert ihres Operanden vor dem Schreiben des neuen Lesens lesen. Endnote

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-Accessor der Caption-Eigenschaft gibt das string gespeicherte im privaten caption Feld zurück. Der Festgelegte Accessor überprüft, ob sich der neue Wert vom aktuellen Wert unterscheidet, und wenn ja, speichert er den neuen Wert und speichert 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-Accessoren einer Eigenschaft sind keine unterschiedlichen Member, und es ist nicht möglich, die Accessoren 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 Lese-/Schreibzugriffseigenschaft. Stattdessen 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 P Eigenschaft A im B Hinblick auf Lese- und Schreibvorgänge ausgeblendetP. Daher in den Aussagen

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

die Zuordnung bewirkt b.P , dass ein Kompilierungszeitfehler gemeldet wird, da die schreibgeschützte P Eigenschaft in B der schreibgeschützten P Eigenschaft Aausgeblendet wird. Beachten Sie jedoch, dass eine Umwandlung für den Zugriff auf die ausgeblendete P Eigenschaft verwendet werden kann.

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 Speicherort 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 Y als auch als Location Eigenschaft vom Typ Pointöffentlich verfügbar gemacht. Wenn es in einer zukünftigen Version von Label" einfacher wird, den Standort intern zu Point 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ätte und x y stattdessen Felder gewesen public readonly , 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. Endnote

Beispiel: Da das Aufrufen eines Get-Accessors konzeptuell dem Lesen des Werts eines Felds entspricht, gilt es als ungültiger Programmierstil für Accessoren, um feststellbare 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 Nebeneffekte" für Accessoren bedeutet nicht, dass Accessoren immer einfach geschrieben werden sollten, um in Feldern gespeicherte Werte zurückzugeben. 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 zum Moment zu verzögern, auf die zuerst verwiesen 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. Durch die Bereitstellung dieser Member als Eigenschaften kann die Klasse ihre Console Initialisierung verzögern, bis sie tatsächlich verwendet werden. Beispiel: Beim ersten Verweisen auf die Out Eigenschaft wie in

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

das für das Ausgabegerät zugrunde liegende Gerät TextWriter erstellt wird. 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 eine automatische Eigenschaft für kurze Zeit) ist eine nicht abstrakte, nicht externe, nicht refwertierte Eigenschaft mit semikolonsgeschützten accessor_bodys. Auto-Eigenschaften müssen über einen Get-Accessor verfügen und optional über einen Set-Accessor verfügen.

Wenn eine Eigenschaft als automatisch implementierte Eigenschaft angegeben wird, 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. Auf das ausgeblendete Sicherungsfeld kann nicht zugegriffen werden, es kann nur über die automatisch implementierten Eigenschaftenaccessoren gelesen und geschrieben werden, auch innerhalb des enthaltenden Typs. Wenn die auto-Eigenschaft keinen Set-Accessor aufweist, wird das Sicherungsfeld als berücksichtigt readonly (§15.5.3). Genau wie ein readonly Feld kann auch eine schreibgeschützte auto-Eigenschaft im Textkörper eines Konstruktors der eingeschlossenen Klasse zugewiesen werden. Eine solche Zuordnung weist direkt dem schreibgeschützten Sicherungsfeld der Eigenschaft zu.

Eine auto-Eigenschaft kann optional eine property_initializer aufweisen, die direkt auf das Sicherungsfeld als variable_initializer (§17.7) angewendet wird.

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 Zuordnungen zum schreibgeschützten Feld sind gültig, da sie innerhalb des Konstruktors auftreten.

Endbeispiel

Obwohl das Sicherungsfeld ausgeblendet ist, kann dieses Feld feldorientierte Attribute über die property_declaration der automatisch implementierten Eigenschaft (§15.7.1) direkt darauf angewendet haben.

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 einer accessor_modifier betrifft niemals die Membersuche (§12.5) oder Überladungsauflösung (§12.6.4). Die Modifizierer für die Eigenschaft oder den Indexer bestimmen immer, an welche Eigenschaft oder indexer unabhängig vom Kontext des Zugriffs gebunden ist.

Nachdem eine bestimmte nicht bezugsbezogene Eigenschaft oder ein nicht bezugswertiger Indexer ausgewählt wurde, werden die Barrierefreiheitsdomänen der beteiligten Accessoren verwendet, um festzustellen, ob diese Verwendung gültig ist:

  • Wenn die Nutzung als Wert (§12.2.2) gilt, muss der Get-Accessor vorhanden und zugänglich sein.
  • Wenn die Verwendung als Ziel einer einfachen Zuordnung (§12.21.2) dient, muss der Set-Accessor vorhanden und zugänglich sein.
  • Ist die Verwendung als Ziel der Zusammengesetzten Zuordnung (§12.21.4) oder als Ziel der ++ Operatoren -- (§12.8.16, §12.9.6), sind sowohl die Accessoren als auch der festgelegte Accessor vorhanden und zugänglich.

Beispiel: Im folgenden Beispiel wird die Eigenschaft durch die Eigenschaft A.Text B.Textausgeblendet, auch in Kontexten, in denen nur der Set-Accessor aufgerufen wird. Im Gegensatz dazu kann auf die Eigenschaft B.Count nicht auf die Klasse Mzugegriffen werden, sodass stattdessen die barrierefreie 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; ob es sich bei der Verwendung um einen Wert, das Ziel einer einfachen Zuordnung oder um das Ziel einer zusammengesetzten Zuordnung handelt; Die Barrierefreiheitsdomäne des beteiligten Zugriffsberechtigungsmoduls wird 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, außer Kraft setzen und abstrakte Accessoren

Hinweis: Diese Klausel gilt sowohl für Eigenschaften (§15.7) als auch für Indexer (§15.9). Die Klausel wird in Bezug auf Eigenschaften geschrieben, wenn das Lesen von Indexern indexer/indexer für Eigenschaften/Eigenschaften ersetzt und die Liste der Unterschiede zwischen Eigenschaften und Indexern in §15.9.2 konsultieren. Endnote

Eine Deklaration einer virtuellen Eigenschaft gibt an, dass die Accessoren der Eigenschaft virtuell sind. Der virtual Modifizierer gilt für alle nicht privaten Accessoren einer Eigenschaft. Wenn ein Accessor einer virtuellen Eigenschaft über die private accessor_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 ihre eigene Implementierung für die Accessoren bereitstellen, indem die Eigenschaft überschrieben wird. 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 Accessor haben private .

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

Abstrakte Eigenschaftendeklarationen sind nur in abstrakten Klassen zulässig (§15.2.2.2.2). Die Accessoren einer geerbten virtuellen Eigenschaft können in einer abgeleiteten Klasse überschrieben werden, indem eine Eigenschaftsdeklaration eingeschlossen wird, die eine override Direktive angibt. Dies wird als überschriebene 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 sind erforderlich, damit die gleiche deklarierte Barrierefreiheit vorhanden ist. Mit anderen Worten, eine Außerkraftsetzungsdeklaration darf die Barrierefreiheit der Basiseigenschaft nicht ändern. Wenn die überschriebene Basiseigenschaft jedoch intern geschützt ist und sie in einer anderen Assembly deklariert wird als die Assembly, die die Überschreibungsdeklaration enthält, dann muss die deklarierte Barrierefreiheit der Deklaration überschrieben werden. Wenn die geerbte Eigenschaft nur über einen einzelnen Accessor verfügt (d. h., wenn die geerbte Eigenschaft schreibgeschützt oder schreibgeschützt ist), darf die überschriebene Eigenschaft nur diesen Accessor enthalten. Wenn die geerbte Eigenschaft beide Accessoren enthält (d. h., wenn die geerbte Eigenschaft schreibgeschützt ist), kann die überschriebene Eigenschaft entweder einen einzelnen Accessor oder beide Accessoren enthalten. Es muss eine Identitätskonvertierung zwischen dem Typ der Außerkraftsetzung und der geerbten Eigenschaft geben.

Eine überschreibende Eigenschaftsdeklaration kann den sealed Modifizierer enthalten. Die Verwendung dieses Modifizierers verhindert, dass eine abgeleitete Klasse die Eigenschaft weiter überschreibt. Die Accessoren 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, außer Außerkraftsetzungen 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-Accessor entspricht einer parameterlosen Methode mit einem Rückgabewert des Eigenschaftstyps und den gleichen Modifizierern wie die enthaltende Eigenschaft.
  • Ein Set-Accessor entspricht einer Methode mit einem einzelnen Wertparameter des Eigenschaftstyps, einem void-Rückgabetyp und den gleichen Modifizierern 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 schreibgeschützte Eigenschaft, Y ist eine virtuelle Lese-/Schreibeigenschaft und Z eine abstrakte Lese-/Schreibeigenschaft. Da Z sie abstrahiert ist, muss die enthaltende Klasse A auch 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 werden die Deklarationen von X, Yund Z es werden Eigenschaftsdeklarationen überschrieben. Jede Eigenschaftsdeklaration entspricht exakt den Modifizierern, Typ und Namen der entsprechenden geerbten Eigenschaft. Der Accessor X abrufen und der Set-Accessor der Verwendung des Y Basisschlüsselworts für den Zugriff auf die geerbten Accessoren. Die Deklaration von Z Außerkraftsetzungen beider abstrakter Accessoren – daher gibt es keine herausragenden abstract Funktionsmember in B, und B es ist zulässig, eine nicht abstrakte Klasse zu sein.

Endbeispiel

Wenn eine Eigenschaft als Außerkraftsetzung deklariert wird, können alle überschriebenen Accessoren auf den überschreibenden Code zugreifen. Darüber hinaus muss die deklarierte Barrierefreiheit sowohl der Eigenschaft als auch des Indexers selbst und der Accessoren mit der der überschriebenen Member und Accessoren ü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'
    | 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.

Ein event_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), der 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) Modifizierer.

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 Ereignisdeklaration kann event_accessor_declarations enthalten. Ist dies jedoch nicht der Fall, wird der Compiler bei nicht extern nicht abstrakten Ereignissen automatisch (§15.8.2) bereitgestellt. Für extern Ereignisse werden die Accessoren extern bereitgestellt.

Eine Ereignisdeklaration, die event_accessor_declaration ausgelassen wird, definiert ein oder mehrere Ereignisse – eines für jedes der variable_declarators. Die Attribute und Modifizierer gelten für alle Elemente, die von einem solchen event_declaration deklariert wurden.

Es handelt sich um einen Kompilierungszeitfehler für eine event_declaration , um sowohl den abstract Modifizierer als auch event_accessor_declarations einzuschließen.

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 handelt sich um einen Kompilierungszeitfehler für eine variable_declarator einer Ereignisdeklaration mit einem abstract oder external einem Modifizierer, um eine variable_initializer einzuschließen.

Ein Ereignis kann als linker Operand der += Operatoren -= 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 einem Vorgang des Formulars x += y oder x –= y, wenn es sich um ein Ereignis handelt, x hat das Ergebnis des Vorgangs Typ void (§12.21.5) (im Gegensatz zum Typ von x) mit dem Wert nach x der Zuordnung, wie für andere die 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 nulldas Feld .

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 Aufruf der Stellvertretung eine Überprüfung vorausgeht, die sicherstellt, dass der Delegat nicht null ist und dass die Überprüfung in einer lokalen Kopie vorgenommen wird, um die Threadsicherheit sicherzustellen.

Außerhalb der Deklaration der Button Klasse kann das Click Element nur auf der linken Seite der += Und-Operatoren –= verwendet werden, wie in

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

die eine Stellvertretung an die Aufrufliste des Click Ereignisses anfügen und

Click –= new EventHandler(...);

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

Endbeispiel

Beim Kompilieren eines feldähnlichen Ereignisses erstellt der Compiler automatisch Speicher zum Speichern des Delegaten und erstellt Accessoren für das Ereignis, das Ereignishandler zum Delegatfeld hinzu- oder entfernt. Die Hinzufügungs- und Entfernungsvorgänge sind threadsicher und können (aber nicht erforderlich) ausgeführt werden, während die Sperre (§13.13) für 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;
}

wird zu folgendem Äquivalent zusammengestellt:

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 Xführen Verweise Ev auf die linke Seite der += Operatoren –= dazu, dass die Accessoren zum Hinzufügen und Entfernen aufgerufen werden. Alle anderen Verweise, die Ev kompiliert werden, um stattdessen auf das ausgeblendete Feld __Ev zu verweisen (§12.8.7). Der Name "__Ev" ist beliebig; das ausgeblendete Feld könnte überhaupt einen Namen oder keinen Namen haben.

Endnote

15.8.3 Ereignisaccessoren

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. Endnote

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 Ereignisaccessor verwendet. Wenn der Zuordnungsoperator verwendet wird += , wird der Accessor zum Hinzufügen verwendet, und wenn der Zuordnungsoperator verwendet wird –= , wird der Remove-Accessor verwendet. In beiden Fällen wird der rechte Operand des Zuordnungsoperators als Argument für den Ereignisaccessor verwendet. Der Block einer add_accessor_declaration oder einer remove_accessor_declaration entspricht den void In §15.6.9 beschriebenen Methoden. Insbesondere return dürfen 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 Methode gibt die GetEventHandler Stellvertretung zurück, die 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 handelt sich um einen Kompilierungszeitfehler, der in den Accessoren eines statischen Ereignisses referenziert this wird.

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, außer Kraft setzen und abstrakte Accessoren

Eine virtuelle Ereignisdeklaration gibt an, dass die Accessoren dieses Ereignisses virtuell sind. Der virtual Modifizierer gilt für beide Accessoren 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 ihre eigene Implementierung für die Accessoren bereitstellen, indem das Ereignis überschrieben wird. 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 Modifizierer als auch die abstract override Modifizierer enthält, gibt an, dass das Ereignis abstrakt ist und ein Basisereignis überschreibt. Die Accessoren eines solchen Ereignisses sind auch 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 überschriebene Ereignisdeklaration 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 Barrierefreiheitsmodifizierer und -namen wie das außerkraftsetzungsereignis angeben, es muss eine Identitätskonvertierung zwischen dem Typ des überschriebenen und überschriebenen Ereignisses vorhanden sein, und sowohl die Add- als auch Remove-Accessoren müssen innerhalb 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 Accessoren eines versiegelten Ereignisses sind ebenfalls versiegelt.

Es handelt sich um einen Kompilierungszeitfehler für eine überschreibende Ereignisdeklaration, um einen new Modifizierer einzuschließen.

Mit Ausnahme von Unterschieden bei der Deklarations- und Aufrufssyntax verhalten sich virtuelle, versiegelte, überschreibende und abstrakte Accessoren genau wie virtuelle, versiegelte, außer Außerkraftsetzungen 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'
    | 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.

Es gibt zwei Arten von indexer_declaration:

  • Der erste deklariert einen Nicht-Verweis-Indexer. Der Wert weist Typ auf. Diese Art von Indexer kann lesbar und/oder schreibbar sein.
  • Die zweite deklariert einen Referenzwertindexer. Der Wert ist eine variable_reference (§9.5), die eine Variable vom Typtyp sein readonlykann. Diese Art von Indexer ist nur lesbar.

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

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 Ausdruckselementtyp, der für ein Array definiert ist, auch mit einem Indexer verwendet. Endnote

Sofern der Indexer keine explizite Schnittstellenmememingimplementierung ist, folgt der Typ dem Schlüsselwort this. Für eine explizite Schnittstellenmememimplementierung folgt der Typ einem interface_type, einem "." und dem Schlüsselwortthis. 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 werden sollen, 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 einen Ausdruckstext, der aus "=>" gefolgt von einem Ausdruck E und einem Semikolon besteht, entspricht genau dem Textkörper der Anweisung { 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 Anweisungstext oder einem Ausdruckstext bestehen. In einem Anweisungstext deklariert ein 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 einem Ausdruckstext, der aus gefolgt wird => ref, entspricht ein variable_reference V und ein Semikolon exakt dem Textkörper der Anweisung{ 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). Endnote

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 s in seinem 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 ein entsprechendes bool[] (da jeder Wert des Ehemaligen nur ein Bit anstelle des zweiten bytebelegt), aber es erlaubt die gleichen Vorgänge 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 Elements genau wie für ein bool[].

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 durch ihren Namen identifiziert, während ein Indexer durch seine 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 handelt sich um einen Kompilierungszeitfehler für einen Indexer-Accessor, um eine lokale Variable oder lokale Konstante mit demselben Namen wie ein Indexerparameter zu deklarieren.
  • 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 eines "automatisch implementierten Indexers". 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 Eigenschaften/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 die Eigenschaft mit Lese-/Schreibzugriff in den Indexer, schreibgeschützte Eigenschaft wird schreibgeschützter Indexer, und schreibgeschützte Eigenschaft wird schreibgeschützter 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äfix logische Negation (§12.9.4) und postfix null-forgiving operators (§12.8.9), während sie durch dasselbe lexikalische Token (!) dargestellt werden, unterscheiden sich. Letzteres ist kein überladener Operator. Endnote

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 Blocktext besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn der Operator aufgerufen wird. Der Block muss den Regeln für rückgabende Methoden entsprechen, die in §15.6.11 beschrieben sind. Ein Ausdruckstext => besteht aus einem Ausdruck, gefolgt von einem Ausdruck und einem Semikolon und zeigt 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 Betreibererklärung enthält sowohl einen public Als auch einen static Modifizierer.
  • Die Parameter eines Operators dürfen keine anderen Modifizierer als in.
  • 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 Betreiber

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 annehmen oder T? einen beliebigen Typ zurückgeben.
  • Ein unärer ++ oder -- betreiber muss einen einzigen Typparameter T annehmen oder T? diesen Typ oder einen daraus abgeleiteten Typ zurückgeben.
  • Ein unärer true oder false betreiber hat einen einzigen Parameter vom Typ oder T? T gibt den Typ boolzurück.

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 Betreiber und false Betreiber true 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 vom Operanden erzeugten Wert zurückgibt, genau wie die Postfix-Inkrement- und Dekrementoperatoren (§12.8.16) und die Präfixinkrement- und Dekrementoperatoren (§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-Schichtoperator muss zwei Parameter annehmen, von denen mindestens ein Typ oder T T?einen Typ aufweisen soll und einen beliebigen Typ zurückgeben kann.
  • Ein Binärer << oder Operator (§12.11) muss zwei Parameter annehmen, von denen der erste Typ T oder T sein soll, und das zweite, von dem typ int oder int?, und kann einen >> beliebigen Typ 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, z. B. Funktionsmemembeaufrufe, Umwandlungsausdrücke 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 Umwandlungsausdrücken auftreten und in §10.3 weiter beschrieben werden.

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 und Zieltyp S T, wenn S oder T nullable Werttypen sind, lassen S₀ und T₀ verweisen Sie auf ihre zugrunde liegenden Typen; andernfalls S₀ und T₀ sind gleich bzwT. gleichS. Eine Klasse oder Struktur darf eine Konvertierung von einem Quelltyp in einen Zieltyp S T 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₀ T₀ noch ist es ein interface_type.

  • Ohne benutzerdefinierte Konvertierungen ist eine Konvertierung nicht von S zu T oder von T zu .S

Für die Zwecke dieser Regeln werden alle Typparameter, die mit oder als eindeutige Typen verknüpft S sind oder T als eindeutige Typen gelten, die keine Vererbungsbeziehung mit anderen Typen aufweisen, 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 bzw int string. , bzw. als eindeutige Typen ohne Beziehung betrachtet werden. Der dritte Operator ist jedoch ein Fehler, da es sich um C<T> die Basisklasse von D<T>.

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, 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 diese object konvertiert werden, da implizite und explizite Konvertierungen bereits zwischen object 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 es sich um S einen Schnittstellentyp handelt T , werden benutzerdefinierte implizite Konvertierungen von S zu T "zu" ignoriert.
    • Andernfalls werden benutzerdefinierte implizite Konvertierungen von S zu T

Für alle Typen, aber objectdie operatoren, die Convertible<T> vom obigen Typ deklariert wurden, führen keinen Konflikt mit vordefinierten Konvertierungen.

Beispiel:

void F(int i, Convertible<int> n)
{
    i = n;                    // Error
    i = (int)n;               // User-defined explicit conversion
    n = i;                    // User-defined implicit conversion
    n = (Convertible<int>)i;  // User-defined implicit conversion
}

Bei typspezifischen objectKonvertierungen blenden jedoch die benutzerdefinierten Konvertierungen in allen Fällen, aber einer aus:

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 dürfen nicht von oder in interface_types konvertiert werden. Insbesondere stellt diese Einschränkung sicher, dass beim Konvertieren in eine interface_type keine benutzerdefinierten Transformationen auftreten und dass eine Konvertierung in eine interface_type nur erfolgreich ist, wenn die object konvertierte konvertierte interface_type tatsächlich 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. Endnote

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, die Konvertierung von byte zu Digit ist jedoch explizit, da Digit nur eine Teilmenge der möglichen Werte einer .byte

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 Ausdruckstext, der => aus einem Ausdruck und einem Semikolon besteht und einen einzelnen Ausdruck angibt, um eine neue Instanz der Klasse zu initialisieren.

Ein constructor_body , der ein Block- oder Ausdruckstext ist, entspricht exakt dem Block einer Instanzmethode mit einem void Rückgabetyp (§15.6.11).

Instanzkonstruktoren werden nicht geerbt. 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_expression s (§12.8.17.2) und über constructor_initializers aufgerufen.

15.11.2 Konstruktorinitialisierer

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

entspricht genau dem

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

Endnote

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 handelt es sich um einen Kompilierzeitfehler, um in einem Argumentausdruck des Konstruktorinitialisierers darauf zu verweisen, da es sich um einen Kompilierungszeitfehler für einen Argumentausdruck handelt, um über einen simple_name auf ein Instanzmemm zu verweisen.

15.11.3 Instanzvariable Initialisierungen

Wenn ein Instanzkonstruktor keinen Konstruktorinitialisierer hat oder über einen Konstruktorinitialisierer des Formulars base(...)verfügt, führt dieser Konstruktor implizit die initialisierungen aus, die durch die variable_initializers der in der Klasse deklarierten Instanzfelder angegeben werden. 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.

15.11.4 Konstruktorausführung

Variableninitialisierer werden in Zuordnungsanweisungen umgewandelt, und diese Zuordnungsanweisungen werden vor dem Aufruf des Basisklasseninstanzkonstruktors ausgeführt. Durch diese Sortierung wird sichergestellt, dass alle Instanzfelder von ihren Variableninitialisierern initialisiert werden, bevor Anweisungen ausgeführt werden, die Zugriff auf diese Instanz haben.

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 eine neue B() Instanz Berstellt wird, wird die folgende Ausgabe erzeugt:

x = 1, y = 0

Der Wert von x 1, da der Variable Initializer ausgeführt wird, bevor der Konstruktor der Basisklasseninstanz aufgerufen wird. Der Wert von y 0 (Standardwert einer int) ist jedoch, da die Zuordnung y erst ausgeführt wird, nachdem der Basisklassenkonstruktor zurückgegeben wurde. Es ist nützlich, sich instanzvariable Initialisierer und Konstruktorinitialisierer als Anweisungen vor dem constructor_body zu vorstellen. 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() {}

or

public C(): base() {}

dabei C handelt es sich um den Namen der Klasse.

Endnote

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 entspricht also genau dem

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 Ausdruckstext, der => aus einem Ausdruck und einem Semikolon besteht, und gibt einen einzelnen Ausdruck an, der ausgeführt werden soll, um die Klasse zu initialisieren.

Ein static_constructor_body , der ein Block - oder Ausdruckstext ist, entspricht exakt 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.
  • Auf alle statischen Member der Klasse wird verwiesen.

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.

Zum Initialisieren eines neuen geschlossenen Klassentyps wird zunächst ein neuer Satz statischer Felder (§15.5.2) für diesen bestimmten geschlossenen Typ erstellt. Jedes der statischen Felder wird auf den Standardwert (§15.5.5.5) initialisiert. Als Nächstes werden die statischen Feldinitialisierer (§15.5.6.2) für diese statischen Felder ausgeführt. Schließlich wird der statische Konstruktor ausgeführt.

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 erstellen, die es statischen Feldern mit Variableninitialisierern ermöglichen, in ihrem Standardwert zu beobachten.

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.Yden statischen Konstruktor der Klasse Baus. YDer Initialisierer bewirkt A, dass der static Konstruktor ausgeführt wird, da auf den Wert A.X verwiesen wird. Der statische Konstruktor wiederum A berechnet den Wert von X, und ruft dabei den Standardwert von Y, der Null ist. A.X wird daher auf 1 initialisiert. Der Prozess der Ausführung Ader statischen Feldinitialisierer und des statischen Konstruktors wird dann abgeschlossen, und es wird zur Berechnung des Anfangswerts zurückgegeben Y, 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 erhalten, sollte eine verwendet Disposewerden. Endnote

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 Ausdruckstext, der => aus einem Ausdruck und einem Semikolon besteht, und gibt einen einzelnen Ausdruck an, der ausgeführt werden soll, um eine Instanz der Klasse abzuschließen.

Ein finalizer_body , bei dem es sich um einen Block - oder Ausdruckstext handelt, entspricht exakt dem method_body einer Instanzmethode mit einem void Rückgabetyp (§15.6.11).

Finalizer werden nicht geerbt. Daher hat eine Klasse keine Finalisierer außer dem, 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. Endnote

Finalizer werden automatisch aufgerufen und können nicht explizit aufgerufen werden. Eine Instanz ist für die Finalisierung berechtigt, wenn es nicht mehr möglich ist, dass code diese Instanz verwenden 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 abgeschlossen ist, werden die Finalizer in der Vererbungskette dieser Instanz von den meisten abgeleiteten bis zu den am wenigsten abgeleiteten Instanzen 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();
    }
}

stimmt

B's finalizer
A's finalizer

da Finalizer in einer Vererbungskette in der Reihenfolge aufgerufen werden, von den meisten abgeleiteten bis zu den am wenigsten abgeleiteten.

Endbeispiel

Finalizer werden implementiert, indem die virtuelle Methode Finalize System.Objectüberschrieben wird. C#-Programme dürfen diese Methode nicht außer Kraft setzen oder sie direkt aufrufen (oder außer Kraft setzen).

Beispiel: Beispiel: Das Programm

class A
{
    override protected void Finalize() {}  // Error
    public void F()
    {
        this.Finalize();                   // Error
    }
}

enthält zwei Fehler.

Endbeispiel

Der Compiler verhält sich so, als ob diese Methode und Außerkraftsetzungen überhaupt nicht vorhanden sind.

Beispiel: Dieses Programm:

class A
{
    void Finalize() {}  // Permitted
}

ist gültig, und die angezeigte Methode blendet System.ObjectFinalize die Methode aus.

Endbeispiel

Eine Erläuterung des Verhaltens, wenn eine Ausnahme von einem Finalizer ausgelöst wird, finden Sie unter §21.4.

15.14 Iteratoren

15.14.1 Allgemein

Ein mithilfe eines Iteratorblocks (§13.3) implementiertes Funktionselement (§12.6) 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.14.2) oder eine der aufzählbaren Schnittstellen (§15.14.3) ist. Es kann als method_body, operator_body oder accessor_body auftreten, während Ereignisse, Instanzkonstruktoren, statische Konstruktoren und Finalizer nicht als Iteratoren implementiert werden dürfen.

Wenn ein Funktionselement mithilfe eines Iteratorblocks implementiert wird, handelt es sich um einen Kompilierungszeitfehler für die Parameterliste des Funktionsmememers, um beliebige in, outoder Parameter oder ref einen Parameter eines ref struct Typs anzugeben.

15.14.2 Enumeratorschnittstellen

Die Enumerationsschnittstellen sind die nicht generische Schnittstelle System.Collections.IEnumerator und alle Instanziierungen der generischen Schnittstelle System.Collections.Generic.IEnumerator<T>. Aus Platzgründen werden in diesem Unterclause und dessen gleichgeordneten Schnittstellen jeweils als IEnumerator bzw IEnumerator<T>. gleichgeordnet referenziert.

15.14.3 Aufzählbare Schnittstellen

Die aufzählbaren Schnittstellen sind die nicht generische Schnittstelle System.Collections.IEnumerable und alle Instanziierungen der generischen Schnittstelle System.Collections.Generic.IEnumerable<T>. Aus Platzgründen werden in diesem Unterclause und dessen gleichgeordneten Schnittstellen jeweils als IEnumerable bzw IEnumerable<T>. gleichgeordnet referenziert.

15.14.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 Ertragtyp eines Iterators, der zurückgibt IEnumerator<T> oder IEnumerable<T> ist T.

15.14.5 Enumeratorobjekte

15.14.5.1 Allgemein

Wenn ein Funktionselement, das einen Enumerator-Schnittstellentyp zurückgibt, mithilfe eines Iteratorblocks implementiert wird, führt das Aufrufen des Funktionsmememers 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 des Codes im Iteratorblock tritt auf, wenn die Methode des Enumeratorobjekts MoveNext aufgerufen wird. Ein Enumerationsobjekt weist die folgenden Merkmale auf:

  • Es implementiert IEnumerator und IEnumerator<T>, wo T ist der Ertragtyp des Iterators.
  • Sie implementiert System.IDisposable.
  • Sie wird mit einer Kopie der Argumentwerte (falls vorhanden) initialisiert und instanzwert, der an das Funktionselement übergeben wird.
  • Es verfügt über vier potenzielle Zustände, bevor, ausgeführt, angehalten und danach, und befindet sich zunächst im Vorzustand .

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 Enumerationsklasse vom Compiler generiert wird, wird diese Klasse direkt oder indirekt in der Klasse, die das Funktionselement enthält, geschachtelt und verfügt über eine private Barrierefreiheit 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 Unterclauses beschreiben das erforderliche Verhalten der MoveNextVon CurrentDispose einem Enumeratorobjekt bereitgestellten Implementierungen und Member der IEnumerator SchnittstellenimplementierungenIEnumerator<T>.

Enumeratorobjekte unterstützen die IEnumerator.Reset Methode nicht. Wenn Sie diese Methode aufrufen, wird ein System.NotSupportedException Fehler ausgelöst.

15.14.5.2 Die MoveNext-Methode

Die MoveNext Methode eines Enumeratorobjekts kapselt den Code eines Iteratorblocks. Durch Aufrufen der MoveNext Methode wird Code im Iteratorblock ausgeführt und die Current Eigenschaft des Enumeratorobjekts entsprechend festgelegt. Die genaue Aktion, die ausgeführt MoveNext wird, hängt vom Status des Enumeratorobjekts ab, wenn MoveNext aufgerufen wird:

  • Wenn sich der Zustand des Enumeratorobjekts vor der Enumerator-Objekt befindet, wird Folgendes angezeigtMoveNext:
    • Ändert den Zustand in "Ausgeführt".
    • 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 Zustand des Enumeratorobjekts ausgeführt wird, wird das Ergebnis des Aufrufens MoveNext nicht angegeben.
  • Wenn der Zustand des Enumeratorobjekts angehalten wird , wird MoveNext aufrufen:
    • Ändert den Zustand in "Ausgeführt".
    • Stellt die Werte aller lokalen Variablen und Parameter (einschließlich this) auf die Beim letzten Anhalten des Iteratorblocks gespeicherten Werte wieder her.

      Hinweis: Der Inhalt aller Objekte, auf die von diesen Variablen verwiesen wird, kann sich seit dem vorherigen Aufruf MoveNextvon . Endnote

    • Setzt die Ausführung des Iteratorblocks unmittelbar nach der Renditerückgabe-Anweisung fort, die das Anhalten der Ausführung verursacht hat und fortgesetzt wird, bis die Ausführung unterbrochen wird (wie unten beschrieben).
  • Wenn der Zustand des Enumeratorobjekts folgt, gibt das Aufrufen MoveNext "false" zurück.

Wenn MoveNext der Iteratorblock ausgeführt wird, kann die Ausführung auf vier Arten unterbrochen werden: Durch eine Anweisung durch eine yield return yield break Anweisung, indem das Ende des Iteratorblocks auftritt und eine Ausnahme ausgelöst und aus dem Iteratorblock verteilt wird.

  • Wenn eine yield return Anweisung gefunden wird (§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 Iteratortexts wird angehalten. 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 in einem oder try mehreren Blöcken befindet, werden die zugeordneten Blöcke zurzeit nicht ausgeführt.
    • Der Zustand des Enumeratorobjekts wird in angehalten geändert.
    • Die MoveNext Methode kehrt zum Aufrufer zurück true , der angibt, dass die Iteration erfolgreich zum nächsten Wert erweitert wurde.
  • Wenn eine yield break Anweisung gefunden wird (§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 Enumeratorobjekts wird in "Danach" geändert.
    • Die MoveNext Methode kehrt false zum Aufrufer zurück, der angibt, dass die Iteration abgeschlossen ist.
  • Wenn das Ende des Iteratortexts gefunden wird:
    • Der Zustand des Enumeratorobjekts wird in "Danach" geä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 verteilt wird:
    • Die finally entsprechenden Blöcke im Iteratortext werden von der Ausnahmeverteilung ausgeführt.
    • Der Zustand des Enumeratorobjekts wird in "Danach" geändert.
    • Die Ausnahmeverteilung wird weiterhin an den Aufrufer der MoveNext Methode weitergeleitet.

15.14.5.3 Die aktuelle Eigenschaft

Die Eigenschaft eines Enumeratorobjekts Current wird von yield return Anweisungen im Iteratorblock beeinflusst.

Wenn sich ein Enumerationsobjekt im angehaltenen Zustand befindet, ist der Wert des Current Werts, der durch den vorherigen Aufruf festgelegt MoveNextwird. Wenn sich ein Enumeratorobjekt im Vor-, Ausführungs- oder Nachstatus befindet, wird das Ergebnis des Zugriffs Currentnicht angegeben.

Für einen Iterator mit einem anderen Ertragtyp als object, entspricht das Ergebnis des Zugriffs durch Current die Implementierung des Enumeratorobjekts IEnumerable dem Zugriff über Current die Implementierung des Enumeratorobjekts und dem Umwandeln des Ergebnisses IEnumerator<T> in object.

15.14.5.4 Die Dispose-Methode

Die Dispose Methode wird verwendet, um die Iteration zu bereinigen, indem das Enumerationsobjekt in den After-Zustand versetzt wird.

  • Wenn der Zustand des Enumeratorobjekts vorher ist, ändert das Aufrufen Dispose des Zustands in "Danach".
  • Wenn der Zustand des Enumeratorobjekts ausgeführt wird, wird das Ergebnis des Aufrufens Dispose nicht angegeben.
  • Wenn der Zustand des Enumeratorobjekts angehalten wird, wird DisposeFolgendes angezeigt:
    • Ändert den Zustand in "Ausgeführt".
    • Führt schließlich Blöcke aus, als wäre die letzte ausgeführte yield return Anweisung eine yield break Anweisung. Wenn dadurch eine Ausnahme ausgelöst und aus dem Iteratortext verteilt wird, wird der Zustand des Enumerationsobjekts auf "After" festgelegt, und die Ausnahme wird an den Aufrufer der Dispose Methode weitergegeben.
    • Ändert den Zustand in "Danach".
  • Wenn der Zustand des Enumeratorobjekts folgt, hat das Aufrufen Dispose keine Auswirkungen.

15.14.6 Aufzählbare Objekte

15.14.6.1 Allgemein

Wenn ein Funktionsmitglied, das einen enumerationsfähigen Schnittstellentyp zurückgibt, mithilfe eines Iteratorblocks implementiert wird, führt das Aufrufen des Funktionsmememers den Code nicht sofort im Iteratorblock aus. Stattdessen wird ein aufzählbares Objekt erstellt und zurückgegeben. Die Aufzählungsobjektmethode gibt ein Enumeratorobjekt GetEnumerator zurück, das den im Iteratorblock angegebenen Code kapselt, und die Ausführung des Codes im Iteratorblock tritt auf, wenn die Methode des Enumeratorobjekts MoveNext aufgerufen wird. Ein aufzählbares Objekt weist die folgenden Merkmale auf:

  • Es implementiert IEnumerable und IEnumerable<T>, wo T ist der Ertragtyp des Iterators.
  • Sie wird mit einer Kopie der Argumentwerte (falls vorhanden) initialisiert und instanzwert, der an das Funktionselement ü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 Funktionselement enthält, geschachtelt und verfügt über eine private Barrierefreiheit und einen Namen, der für die Compilerverwendung reserviert ist (§6.4.3).

Ein aufzählbares Objekt kann mehr Schnittstellen als die oben angegebenen implementieren.

Hinweis: Ein aufzählbares Objekt kann beispielsweise auch implementieren IEnumerator und IEnumerator<T>ermöglichen, dass es sowohl als enumerationsfähig als auch als Enumerator dienen kann. In der Regel würde eine solche Implementierung eine eigene Instanz (zum Speichern von Zuordnungen) vom ersten Aufruf an zurückgeben GetEnumerator. 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 Enumerationsator bereits über das Ende der Sequenz aufgezählt wurde, da alle zukünftigen Aufrufe eines erschöpften Enumerators Ausnahmen auslösen müssen. Endnote

15.14.6.2 Die GetEnumerator-Methode

Ein aufzählbares Objekt stellt eine Implementierung der GetEnumerator Methoden und IEnumerable 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.14.5 beschrieben.

15.15 Asynchrone Funktionen

15.15.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, um einen beliebigen inParameter outoder ref Parameter eines ref struct Typs anzugeben.

Die return_type einer asynchronen Methode muss entweder void oder ein Vorgangstyp sein. Bei einer asynchronen Methode, die einen Ergebniswert erzeugt, muss ein Vorgangstyp generisch sein. Bei einer asynchronen Methode, die keinen Ergebniswert erzeugt, darf ein Vorgangstyp nicht generisch sein. Solche Typen werden in dieser Spezifikation bzw. in dieser Spezifikation «TaskType»<T> «TaskType»genannt. Der aus System.Threading.Tasks.Task<TResult> einem Aufgaben-Generator erstellte Standardbibliothekstyp System.Threading.Tasks.Task und -typen sind Aufgabentypen sowie ein Klassen-, Struktur- oder Schnittstellentyp, der einem Aufgaben-Generator-Typ über das Attribut System.Runtime.CompilerServices.AsyncMethodBuilderAttributezugeordnet ist. Solche Typen werden in dieser Spezifikation als «TaskBuilderType»<T> und «TaskBuilderType». 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 erfolgreicher «TaskType»<T> Datensatz zeichnet ein Ergebnis des Typs Tauf. Aufgabentypen sind wartend, und Aufgaben können daher die Operanden von Await-Ausdrücken (§12.9.8) sein.

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 Vorgangs-Generator-Typ ist eine Klasse oder ein Strukturtyp, der einem bestimmten Aufgabentyp entspricht (§15.15.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 Vorgangstyp in einem anderen Typ geschachtelt ist, muss der Vorgangstyp auch in diesem Typ geschachtelt sein. Endnote

Eine asynchrone Funktion hat die Möglichkeit, die Auswertung mithilfe von Await-Ausdrücken (§12.9.8) im Textkörper anzusetzen. Die Auswertung kann später an der Stelle des Anhalteausdrucks fortgesetzt werden, indem eine Fortsetzungsdelegat verwendet wird. Der Fortsetzungsdelegat ist vom Typ System.Action, und wenn er aufgerufen wird, wird die Auswertung des Aufrufs der asynchronen Funktion vom Await-Ausdruck fortgesetzt, wo 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.15.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 Aufgaben-Generator-Typ muss über die folgenden Member (für nicht generische Aufgaben-Generator-Typen, SetResult keine Parameter) mit deklarierter public Barrierefreiheit verfügen:

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

Der Compiler generiert Code, der den «TaskBuilderType» verwendet, um die Semantik des Anhaltens und Fortsetzens der Auswertung der asynchronen Funktion zu implementieren. Der 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 Generator einer compilergenerierten Zustandsautomatinstanz zuzuordnen. stateMachine
    • Der Baumeister ruft stateMachine.MoveNext() entweder ein Start() oder nach Start() der Rückkehr zur Weiterleitung der Zustandsmaschine auf.
  • Nach Start() dem Zurückgeben ruft die async Methode die Aufgabe auf builder.Task , die von der asynchronen Methode zurückgegeben werden soll.
  • Jeder Aufruf führt stateMachine.MoveNext() den Zustandsautomaten weiter.
  • Wenn der Zustandsautomat erfolgreich abgeschlossen wird, builder.SetResult() wird er mit dem Rückgabewert der Methode (falls vorhanden) aufgerufen.
  • Andernfalls wird, wenn eine Ausnahme e im Zustandscomputer ausgelöst wird, builder.SetException(e) aufgerufen.
  • Wenn der Zustandsautomat einen await expr Ausdruck erreicht, expr.GetAwaiter() wird er aufgerufen.
  • Wenn der Awaiter implementiert ICriticalNotifyCompletion und IsCompleted falsch ist, wird der Zustandsautomat builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)aufgerufen.
    • AwaitUnsafeOnCompleted()sollte mit einem Action Anruf aufgerufen awaiter.UnsafeOnCompleted(action) werden, der aufgerufen stateMachine.MoveNext() wird, wenn der Awaiter abgeschlossen ist.
  • Andernfalls ruft der Zustandsautomat builder.AwaitOnCompleted(ref awaiter, ref stateMachine)auf.
    • AwaitOnCompleted()sollte mit einem Action Anruf aufgerufen awaiter.OnCompleted(action) werden, der aufgerufen stateMachine.MoveNext() wird, wenn der Awaiter abgeschlossen ist.
  • SetStateMachine(IAsyncStateMachine) kann von der compilergenerierten IAsyncStateMachine Implementierung aufgerufen werden, um die Instanz des Generators zu identifizieren, der einer Zustandsautomatinstanz zugeordnet ist, insbesondere für Fälle, in denen der Zustandsautomat als Werttyp implementiert wird.
    • Wenn der Generator aufruftstateMachine.SetStateMachine(stateMachine), ruft builder.SetStateMachine(stateMachine) der stateMachine Generator die generator-Instanz auf, die zugeordnet stateMachineist.

Hinweis: Für beide SetResult(T result) und «TaskType»<T> Task { get; }, muss der Parameter bzw. das Argument identitätsvertierbar sein.T Auf diese Weise kann ein Aufgabentyp-Generator Typen wie Tupel unterstützen, wobei zwei Typen, die nicht identisch sind, identitätsverwandelt sind. Endnote

15.15.3 Auswertung einer asynchronen 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 angehalten wird (durch Erreichen eines Await-Ausdrucks) oder beendet wird, an welchem Punkt das Steuerelement an den Aufrufer zurückgegeben wird, zusammen mit der Rückgabeaufgabe.

Wenn der Textkörper der asynchronen Funktion beendet wird, wird die Rückgabeaufgabe aus dem unvollständigen Zustand verschoben:

  • Wenn der Funktionstext als Ergebnis des Erreichens einer Rückgabe-Anweisung oder des Endes des Textkörpers beendet wird, wird jeder Ergebniswert in der Rückgabeaufgabe aufgezeichnet, die in einen erfolgreichen Zustand versetzt wird.
  • Wenn der Funktionstext aufgrund eines nicht abgeschlossenen OperationCanceledExceptionVorgangs beendet wird, wird die Ausnahme in der Rückgabeaufgabe aufgezeichnet, die in den abgebrochenen Zustand versetzt wird.
  • Wenn der Funktionstext als Ergebnis einer anderen untaktischen Ausnahme (§13.10.6) beendet wird, wird die Ausnahme in der Rückgabeaufgabe aufgezeichnet, die in einen fehlerhaften Zustand versetzt wird.

15.15.4 Auswertung einer asynchronen Funktion für "void-returning"

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, aber eine Darstellung von "wo" der aktuelle Thread ausgeführt wird. Der Synchronisierungskontext wird benachrichtigt, wenn die Auswertung einer void"-returning"-asynchronen Funktion beginnt, erfolgreich abgeschlossen wird oder bewirkt, dass eine ungenaue Ausnahme ausgelöst wird.

Dadurch kann der Kontext nachverfolgen, wie viele voidasynchrone Funktionen ausgeführt werden und wie Ausnahmen verteilt werden, die aus ihnen stammen.