Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
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.
Structs (§16) und Schnittstellen (§18) haben Elemente ähnlich wie Klassen, aber mit bestimmten Einschränkungen. Diese Klausel definiert die Deklarationen für Klassen und Klassenmmber. Die Klauseln für Strukturen und Schnittstellen definieren die Einschränkungen für diese Typen in Bezug auf die entsprechenden Deklarationen in Klassentypen.
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 (§23), 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_clauses (§15.2.5), gefolgt von einem class_body (§15.2.6), optional gefolgt von einem Semikolon.
Eine Klassendeklaration darf keine type_parameter_constraints_clauses liefern, wenn sie nicht auch eine type_parameter_listliefert.
Eine Klassendeklaration, die eine type_parameter_list bereitstellt, ist eine generische Klassendeklaration. Darüber hinaus ist jede Klasse, die in einer generischen Klassendeklaration oder einer generischen Strukturdeklaration geschachtelt ist, selbst eine generische Klassendeklaration, da Typargumente für den enthaltenden Typ bereitgestellt werden müssen, um einen konstruierten Typ (§8.4) zu erstellen.
15.2.2 Klassenmodifizierer
15.2.2.1 Allgemein
Ein class_declaration kann optional eine Sequenz von Klassenmodifizierern enthalten:
class_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'abstract'
| 'sealed'
| 'static'
| unsafe_modifier // unsafe code support
;
unsafe_modifier (§24.2) ist nur im unsicheren Code (§24) verfügbar.
Es handelt sich um einen Kompilierungszeitfehler für denselben Modifizierer, der mehrmals in einer Klassendeklaration angezeigt wird.
Der new Modifizierer ist für geschachtelte Klassen zulässig. Es gibt an, dass die Klasse ein geerbtes Element unter demselben Namen ausblendet, wie in §15.3.5 beschrieben. Es ist ein Kompilierzeitfehler, wenn der new Modifizierer in einer Klassendeklaration erscheint, die keine geschachtelte Klassendeklaration ist.
Die public, protected, internalund private Modifizierer steuern die Barrierefreiheit der Klasse. Abhängig vom Kontext, in dem die Klassendeklaration auftritt, sind einige dieser Modifizierer möglicherweise nicht zulässig (§7.5.2).
Wenn eine partielle Typdeklaration (§15.2.7) eine Spezifikation der Zugänglichkeit enthält (über die Modifikatoren public, protected, internalund private ), muss diese Spezifikation mit allen anderen Teilen übereinstimmen, die eine Spezifikation der Zugänglichkeit enthalten. Wenn kein Teil eines Teiltyps eine Barrierefreiheitsspezifikation enthält, erhält der Typ die entsprechende Standardbarrierefreiheit (§7.5.2).
Die abstract-Modifizierer, die sealed-Modifizierer und die static-Modifizierer werden in den folgenden Unterabsätzen erläutert.
15.2.2.2 Abstrakte Klassen
Der abstract Modifizierer wird verwendet, um anzugeben, dass eine Klasse unvollständig ist und nur als Basisklasse verwendet werden soll. Eine abstrakte Klasse unterscheidet sich von einer nicht abstrakten Klasse auf folgende Weise:
- Eine abstrakte Klasse kann nicht direkt instanziiert werden, und es handelt sich um einen Kompilierungszeitfehler, um den
newOperator für eine abstrakte Klasse zu verwenden. Es ist zwar möglich, Variablen und Werte zu haben, deren Kompilierungszeittypen abstrakt sind, aber diese Variablen und Werte werden notwendigerweise entwedernullsein oder Verweise auf Instanzen von nicht-abstrakten Klassen enthalten, die von den abstrakten Typen abgeleitet sind. - Eine abstrakte Klasse darf abstrakte Mitglieder enthalten, muss dies jedoch nicht.
- Eine abstrakte Klasse kann nicht versiegelt werden.
Wenn eine nicht abstrakte Klasse von einer abstrakten Klasse abgeleitet wird, muss die nicht abstrakte Klasse tatsächliche Implementierungen aller geerbten abstrakten Elemente enthalten, wodurch diese abstrakten Elemente außer Kraft gesetzt werden.
Beispiel: Im folgenden Code
abstract class A { public abstract void F(); } abstract class B : A { public void G() {} } class C : B { public override void F() { // Actual implementation of F } }die abstrakte Klasse
Aführt eine abstrakte MethodeFein. KlasseBführt eine zusätzliche MethodeGein, aber da sie keine Implementierung vonFbereitstellt, mussBauch als abstrakt deklariert werden. Die KlasseCüberschreibtFund stellt eine konkrete Implementierung bereit. Da es inCkeine abstrakten Mitglieder gibt, darf (muss aber nicht)Cnicht-abstrakt sein.Endbeispiel
Wenn ein oder mehrere Teile einer partiellen Typdeklaration (§15.2.7) einer Klasse den abstract Modifizierer enthalten, ist die Klasse abstrakt. Andernfalls ist die Klasse nicht abstrakt.
15.2.2.3 Versiegelte Klassen
Der sealed Modifizierer wird verwendet, um die Ableitung von einer Klasse zu verhindern. Wenn eine versiegelte Klasse als Basisklasse einer anderen Klasse angegeben wird, tritt ein Kompilierungszeitfehler auf.
Eine versiegelte Klasse kann nicht auch eine abstrakte Klasse sein.
Hinweis: Der
sealedModifizierer wird hauptsächlich verwendet, um unbeabsichtigte Ableitungen zu verhindern, ermöglicht aber auch bestimmte Laufzeitoptimierungen. Da eine abgeschlossene Klasse bekanntlich niemals über abgeleitete Klassen verfügt, ist es insbesondere möglich, virtuelle Funktionsaufrufe bei Instanzen abgeschlossener Klassen in nicht-virtuelle Aufrufe umzuwandeln. Hinweisende
Wenn ein oder mehrere Teile einer Partialtypdeklaration (§15.2.7) einer Klasse den sealed Modifizierer enthalten, wird die Klasse versiegelt. Andernfalls wird die Klasse nicht versiegelt.
15.2.2.4 Statische Klassen
15.2.2.4.1 Allgemein
Der static Modifizierer wird verwendet, um die Klasse zu kennzeichnen, die als statische Klasse deklariert wird. Eine statische Klasse darf nicht instanziiert werden, darf nicht als Typ verwendet werden und darf nur statische Member enthalten. Nur eine statische Klasse kann Deklarationen von Erweiterungsmethoden (§15.6.10) enthalten.
Eine statische Klassendeklaration unterliegt den folgenden Einschränkungen:
- Eine statische Klasse darf weder einen
sealed- noch einenabstract-Modifizierer enthalten. (Da eine statische Klasse jedoch nicht instanziiert oder abgeleitet werden kann, verhält sie sich so, als wäre sie sowohl versiegelt als auch abstrahiert.) - Eine statische Klasse darf keine class_base Spezifikation (§15.2.4) enthalten und kann keine Basisklasse oder eine Liste der implementierten Schnittstellen explizit angeben. Eine statische Klasse erbt implizit vom Typ
object. - Eine statische Klasse darf nur statische Member (§15.3.8) enthalten.
Hinweis: Alle Konstanten und geschachtelten Typen werden als statische Member klassifiziert. Hinweisende
- Eine statische Klasse darf keine Mitglieder mit
protected,private protected, oderprotected internaldeklarierter Zugänglichkeit haben.
Ein Kompilierungsfehler tritt auf, wenn eine dieser Einschränkungen verletzt wird.
Eine statische Klasse weist keine Instanzkonstruktoren auf. Es ist nicht möglich, einen Instanzkonstruktor in einer statischen Klasse zu deklarieren, und für eine statische Klasse wird kein Standardinstanzkonstruktor (§15.11.5) bereitgestellt.
Die Member einer statischen Klasse sind nicht automatisch statisch, und die Memberdeklarationen enthalten explizit einen static Modifizierer (mit Ausnahme von Konstanten und geschachtelten Typen). Wenn eine Klasse in einer statischen äußeren Klasse geschachtelt ist, ist die geschachtelte Klasse keine statische Klasse, es sei denn, sie enthält explizit einen static Modifizierer.
Wenn mindestens ein Teil einer Teiltypdeklaration (§15.2.7) einer Klasse den static Modifizierer enthält, ist die Klasse statisch. Andernfalls ist die Klasse nicht statisch.
15.2.2.4.2 Verweisen auf statische Klassentypen
Ein namespace_or_type_name (§7.8) darf auf eine statische Klasse verweisen, wenn
- Der namespace_or_type_name ist der
Tin einem namespace_or_type_name der FormT.I, oder - Der namespace_or_type-name ist der
Tin einem Ausdruckstyp (§12.8.18) der Formtypeof(T).
Ein primary_expression (§12.8) darf auf eine statische Klasse verweisen, wenn
- Der primäre_Ausdruck ist der
Ein einem Mitglied_Zugang (§12.8.7) der FormE.I.
In jedem anderen Kontext ist es ein Kompilierungszeitfehler, um auf eine statische Klasse zu verweisen.
Hinweis: Es handelt sich z. B. um einen Fehler für eine statische Klasse, die als Basisklasse, einen Bestandteiltyp (§15.3.7) eines Elements, ein generisches Typargument oder eine Typparametereinschränkung verwendet werden soll. Ebenso kann eine statische Klasse nicht in einem Array-Typ, einem new-Ausdruck, einem cast-Ausdruck, einem is-Ausdruck, einem as-Ausdruck, einem
sizeof-Ausdruck oder einem Standardwert-Ausdruck verwendet werden. Hinweisende
15.2.3 Typparameter
Ein Typparameter ist ein einfacher Bezeichner, der einen Platzhalter für ein Typargument angibt, das zum Erstellen eines konstruierten Typs bereitgestellt wird. Ein Typargument (§8.4.2) ist dagegen der Typ, der beim Erstellen eines konstruierten Typs durch den Typparameter ersetzt wird.
type_parameter_list
: '<' decorated_type_parameter (',' decorated_type_parameter)* '>'
;
decorated_type_parameter
: attributes? type_parameter
;
type_parameter ist in §8.5 definiert.
Jeder Typparameter in einer Klassendeklaration definiert einen Namen im Deklarationsraum (§7.3) dieser Klasse. Daher kann er nicht denselben Namen wie ein anderer Typparameter dieser Klasse oder eines In dieser Klasse deklarierten Elements haben. Ein Typparameter darf nicht denselben Namen wie der Typ selbst haben.
Zwei partielle generische Typdeklarationen (im selben Programm) tragen zum gleichen ungebundenen generischen Typ bei, wenn sie denselben vollqualifizierten Namen haben (einschließlich eines generic_dimension_specifier (§12.8.18) für die Anzahl der Typparameter) (§7.8.3). Zwei solche Teiltypdeklarationen müssen denselben Namen für jeden Typparameter in der Reihenfolge angeben.
15.2.4 Klassenbasisspezifikation
15.2.4.1 Allgemein
Eine Klassendeklaration kann eine class_base Spezifikation enthalten, die die direkte Basisklasse der Klasse und die von der Klasse direkt implementierten Schnittstellen (§19) definiert.
class_base
: ':' class_type
| ':' interface_type_list
| ':' class_type ',' interface_type_list
;
interface_type_list
: interface_type (',' interface_type)*
;
15.2.4.2 Basisklassen
Wenn ein class_type in der class_base enthalten ist, gibt es die direkte Basisklasse der klasse an, die deklariert wird. Wenn eine nicht partielle Klassendeklaration keine class_base aufweist oder wenn die class_base nur Schnittstellentypen auflistet, wird die direkte Basisklasse als angenommen object. Wenn eine partielle Klassendeklaration eine Basisklassenspezifikation enthält, muss diese Basisklassenspezifikation auf denselben Typ verweisen wie alle anderen Teile dieses Teiltyps, die eine Basisklassenspezifikation enthalten. Wenn kein Teil einer partiellen Klasse eine Basisklassenspezifikation enthält, lautet objectdie Basisklasse . Eine Klasse erbt Mitglieder von ihrer direkten Basisklasse, wie in §15.3.4 beschrieben.
Beispiel: Im folgenden Code
class A {} class B : A {}Klasse
Awird als direkte Basisklasse vonBbezeichnet, undBwird als vonAabgeleitet angesehen. DaAkeine direkte Basisklasse explizit angibt, ist die direkte Basisklasse implizitobject.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äreB<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 (System.Attribute) verwendet 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: Folgende
class X<T> { public class Y{} } class Z : X<Z.Y> {}ist fehlerhaft, da in der Basisklassenspezifikation
X<Z.Y>die direkte Basisklasse vonZalsobjectgilt und daher (durch die Regeln von §7.8)Znicht als Mitglied vonYbetrachtet wird.Endbeispiel
Die Basisklassen einer Klasse sind die direkte Basisklasse und ihre Basisklassen. Mit anderen Worten, der Satz von Basisklassen ist das transitive Schließen der direkten Basisklassenbeziehung.
Beispiel: Im Folgenden:
class A {...} class B<T> : A {...} class C<T> : B<IComparable<T>> {...} class D<T> : C<T[]> {...}Die Basisklassen von
D<int>sindC<int[]>,B<IComparable<int[]>>,Aundobject.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 vom Typ ab , in dem sie sofort geschachtelt ist (falls vorhanden).
Beispiel: Das Beispiel
class A : A {}ist fehlerhaft, da die Klasse von sich selbst abhängt. Ebenso ist das Beispiel
class A : B {} class B : C {} class C : A {}ist fehlerhaft, da die Klassen zirkulär von sich selbst abhängen. Zum Schluss das Beispiel
class A : B.C {} class B : A { public class C {} }führt zu einem Kompilierzeitfehler, da A von
B.C(seiner direkten Basisklasse) abhängt, die wiederum vonB(der unmittelbar umschließenden Klasse) abhängt, welche ihrerseits vonAzyklisch abhängig ist.Endbeispiel
Eine Klasse hängt nicht von den Klassen ab, die darin verschachtelt sind.
Beispiel: Im folgenden Code
class A { class B : A {} }
Bhängt vonAab (daAsowohl seine direkte Basisklasse als auch seine unmittelbar eingeschlossene Klasse ist), aberAhängt nicht vonBab (daBweder eine Basisklasse noch eine eingeschlossene Klasse vonAist). 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 classDie Klasse
Bist fehlerhaft, da sie versucht, von der versiegelten KlasseAabzuleiten.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
ClautetIA,IBundIC.Endbeispiel
In der Regel stellt jeder Teil eine Implementierung der schnittstellen bereit, die in diesem Teil deklariert sind; Dies ist jedoch keine Voraussetzung. Ein Teil kann die Implementierung für eine Schnittstelle bereitstellen, die auf einem anderen Teil deklariert ist.
Beispiel:
partial class X { int IComparable.CompareTo(object o) {...} } partial class X : IComparable { ... }Endbeispiel
Die in einer Klassendeklaration angegebenen Basisschnittstellen können Schnittstellentypen (§8.4, §19.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 §19.6 weiter erörtert.
15.2.5 Einschränkungen des Typparameters
Generische Typ- und Methodendeklarationen können optional Typparametereinschränkungen angeben, indem type_parameter_constraints_clauses eingeschlossen werden.
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
| secondary_constraints (',' constructor_constraint)?
| constructor_constraint
;
primary_constraint
: class_type nullable_type_annotation?
| 'class' nullable_type_annotation?
| 'struct'
| 'notnull'
| 'unmanaged'
;
secondary_constraint
: interface_type nullable_type_annotation?
| type_parameter nullable_type_annotation?
;
secondary_constraints
: secondary_constraint (',' secondary_constraint)*
;
constructor_constraint
: 'new' '(' ')'
;
Jede type_parameter_constraints_clause besteht aus dem Token where, gefolgt vom Namen eines Typparameters, gefolgt von einem Doppelpunkt und der Liste der Einschränkungen für diesen Typparameter. Es kann höchstens eine where Klausel für jeden Typparameter geben, und die where Klauseln können in beliebiger Reihenfolge aufgeführt werden. Wie bei den get- und set-Tokens in einem Eigenschaftszugriff ist das where-Token kein Schlüsselwort.
Die Liste der in einer where Klausel angegebenen Einschränkungen kann eine der folgenden Komponenten enthalten, in dieser Reihenfolge: eine einzelne primäre Einschränkung, eine oder mehrere sekundäre Einschränkungen und die Konstruktoreinschränkung. new()
Eine primäre Einschränkung kann ein Klassentyp, die Referenztyp-Einschränkung, die Werttyp-Einschränkung, die Nicht-Null-Einschränkung oder die nicht verwaltete Typ-Einschränkung sein. Der Klassentyp und die Bezugstypeinschränkung können die nullable_type_annotation enthalten.
Eine sekundäre Einschränkung kann ein Schnittstellentyp oder Typparameter sein, optional gefolgt von einer nullbare_Typannotation. Das Vorhandensein der nullable_type_annotation zeigt an, dass das Typargument ein nullable Referenztyp sein darf, der einem nicht-nullable Referenztyp entspricht, der die Einschränkung erfüllt.
Die Einschränkung des Bezugstyps gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein Bezugstyp sein soll. Alle Klassentypen, Schnittstellentypen, Delegattypen, Arraytypen und Typparameter, die als Referenztyp (wie unten definiert) bezeichnet werden, erfüllen diese Einschränkung.
Der Klassentyp, die Referenztypeinschränkung und die sekundären Einschränkungen können die Nullable-Typanmerkung enthalten. Das Vorhandensein oder Fehlen dieser Anmerkung für den Typparameter gibt die Nullbarkeitserwartungen für das Typargument an:
- Wenn die Einschränkung die Nullable-Typanmerkung nicht enthält, wird erwartet, dass das Typargument ein nicht Nullable-Referenztyp ist. Ein Compiler gibt möglicherweise eine Warnung aus, wenn das Typargument ein Nullwertverweistyp ist.
- Wenn die Einschränkung die Annotation nullable type enthält, wird die Einschränkung sowohl von einem nicht-nullbaren Referenztyp als auch von einem nullbaren Referenztyp erfüllt.
Die Nullierbarkeit des Typarguments muss nicht mit der Nullierbarkeit des Typparameters übereinstimmen. Ein Compiler kann eine Warnung ausgeben, wenn die Nullbarkeit des Typparameters nicht mit der Nullbarkeit des Typarguments übereinstimmt.
Hinweis: Um anzugeben, dass ein Typargument ein nullabler Bezugstyp ist, fügen Sie die Nullable-Typanmerkung nicht als Einschränkung (Verwendung
T : classoderT : BaseClass) hinzu, verwenden SieT?jedoch die allgemeine Deklaration, um den entsprechenden nullablen Bezugstyp für das Typargument anzugeben. Hinweisende
Die Nullwerte-Typanmerkung kann nur für einen Typparameter verwendet werden, ?der die Werttypeinschränkung, die Verweistypeinschränkung ohne die nullable_type_annotation oder eine Klassentypeinschränkung ohne die nullable_type_annotation enthält.
Beispiel: Die folgenden Beispiele zeigen, wie sich die Nullierbarkeit eines Typarguments auf die Nullierbarkeit einer Deklaration des Typparameters auswirkt:
public class C { } public static class Extensions { public static void M<T>(this T? arg) where T : notnull { } } public class Test { public void M() { C? mightBeNull = new C(); C notNull = new C(); int number = 5; int? missing = null; mightBeNull.M(); // arg is C? notNull.M(); // arg is C? number.M(); // arg is int? missing.M(); // arg is int? } }Wenn das Typargument ein nicht nullabler Typ ist, gibt die
?Typanmerkung an, dass der Parameter der entsprechende nullable Typ ist. Wenn das Typargument bereits ein nullabler Bezugstyp ist, ist der Parameter derselbe nullable Typ.Endbeispiel
Die Nicht-NULL-Einschränkung gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein nicht nullabler Werttyp oder ein nicht nullabler Bezugstyp sein soll. Ein Typargument, das kein nicht nullbarer Werttyp oder nicht nullbarer Referenztyp ist, ist zulässig, ein Compiler kann jedoch eine Diagnosewarnung ausgeben.
Da notnull kein Schlüsselwort ist, ist die Nicht-NULL-Einschränkung in primary_constraint immer syntaktisch mehrdeutig mit class_type. Aus Kompatibilitätsgründen soll eine Namenssuche (§12.8.4) des Namens notnull bei Erfolg wie ein class_type behandelt werden. Andernfalls wird sie als Nicht-Null-Einschränkung behandelt.
Beispiel: Die folgende Klasse veranschaulicht die Verwendung verschiedener Typargumente gegen unterschiedliche Einschränkungen und gibt Warnungen an, die von einem Compiler ausgegeben werden können.
#nullable enable public class C { } public class A<T> where T : notnull { } public class B1<T> where T : C { } public class B2<T> where T : C? { } class Test { static void M() { // nonnull constraint allows nonnullable struct type argument A<int> x1; // possible warning: nonnull constraint prohibits nullable struct type argument A<int?> x2; // nonnull constraint allows nonnullable class type argument A<C> x3; // possible warning: nonnull constraint prohibits nullable class type argument A<C?> x4; // nonnullable base class requirement allows nonnullable class type argument B1<C> x5; // possible warning: nonnullable base class requirement prohibits nullable class type argument B1<C?> x6; // nullable base class requirement allows nonnullable class type argument B2<C> x7; // nullable base class requirement allows nullable class type argument B2<C?> x8; } }
Die Werttypeinschränkung gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein nicht nullwertebarer Werttyp sein soll. Alle nicht nullbaren Strukturtypen, Enumerationstypen und Typparameter mit der Werttypeinschränkung erfüllen diese Einschränkung. Beachten Sie, dass ein nullable-Werttyp (§8.3.12), obwohl als Werttyp klassifiziert, die Werttypeinschränkung nicht erfüllt. Ein Typparameter mit der Werttypeinschränkung darf nicht auch über die constructor_constraint verfügen, obwohl er als Typargument für einen anderen Typparameter mit einem constructor_constraint verwendet werden kann.
Hinweis: Der -Typ gibt die nicht-nullbare Werttypeinschränkung für
System.Nullable<T>an. Rekursiv konstruierte FormenT??undNullable<Nullable<T>>sind daher verboten. Hinweisende
Die Nicht verwaltete Typeinschränkung gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein nicht nullabler nicht verwalteter Typ (§8.8) sein soll.
Da unmanaged kein Schlüsselwort ist, ist die Unmanaged-Einschränkung in primary_constraint immer syntaktisch mehrdeutig im Vergleich zu class_type. Aus Kompatibilitätsgründen wird, wenn eine Namenssuche (§12.8.4) des Namens unmanaged erfolgreich ist, sie als class_type behandelt. Andernfalls wird sie als nicht verwaltete Einschränkung behandelt.
Zeigertypen dürfen niemals Typargumente sein und erfüllen keine Typleinschränkungen, nicht einmal die für nicht verwaltete Typen, obwohl sie selbst nicht verwaltete Typen sind.
Wenn es sich bei einer Einschränkung um einen Klassentyp, einen Schnittstellentyp oder einen Typparameter handelt, gibt dieser Typ einen minimalen "Basistyp" an, den jedes Typargument, das für diesen Typparameter verwendet wird, unterstützen soll. Jedes Mal, wenn ein konstruierter Typ oder eine generische Methode verwendet wird, wird das Typargument auf die Einschränkungen für den Typparameter zur Kompilierungszeit überprüft. Das angegebene Typargument erfüllt die in §8.4.5 beschriebenen Bedingungen.
Eine class_type Einschränkung erfüllt die folgenden Regeln:
- Der Typ muss ein Klassentyp sein.
- Der Typ darf nicht sein
sealed. - Der Typ darf keine der folgenden Typen sein:
System.ArrayoderSystem.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
whereKlausel 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
whereKlausel 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
Tals Einschränkung für typparameterSverwendet wird,Shängt es davonTab. - Wenn ein Typparameter
Svon einem TypparameterTabhängt undTvon einem TypparameterUabhängt, hängt esSdann 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:
-
Tdarf die Werttypeinschränkung nicht aufweisen. Andernfalls wirdTeffektiv versiegelt, sodassSgezwungen wäre, denselben Typ wieTzu haben, wodurch der Bedarf an zwei Typparametern entfällt. - Wenn
Sdie Wertetyp-Einschränkung hat, darfTkeine class_type Einschränkung haben. - Wenn
Seine class_type-EinschränkungAhat undTeine class_type-EinschränkungBhat, muss es eine Identitätskonversion oder eine implizite Referenzkonvertierung vonAzuBoder eine implizite Referenzkonvertierung vonBzuAgeben. - Wenn
Sauch vom TypparameterUabhängt undUeine class_type EinschränkungAaufweist undTeine class_type EinschränkungBaufweist, muss es eine Identitätskonvertierung oder implizite Verweiskonvertierung vonAinBoder eine implizite Verweiskonvertierung vonBinAgeben.
Es ist korrekt, dass S die Werttypenbeschränkung und T die Referenztypenbeschränkung hat. Dies beschränkt T sich effektiv auf die Typen System.Object, System.ValueType, , System.Enumund alle Schnittstellentypen.
Wenn die where Klausel für einen Typparameter eine Konstruktoreinschränkung enthält (die das Formular new()hat), ist es möglich, den new Operator zum Erstellen von Instanzen des Typs zu verwenden (§12.8.17.2). Jedes Typargument, das für einen Typparameter mit einer Konstruktoreinschränkung verwendet wird, ist ein Werttyp, eine nicht abstrakte Klasse mit einem öffentlichen parameterlosen Konstruktor oder ein Typparameter mit der Werttypeinschränkung oder Konstruktoreinschränkung.
Es ist ein Kompilierfehler, wenn type_parameter_constraints mit einem primary_constraint von struct oder unmanaged auch einen constructor_constrainthaben.
Beispiel: Im Folgenden sind Beispiele für Einschränkungen aufgeführt:
interface IPrintable { void Print(); } interface IComparable<T> { int CompareTo(T value); } interface IKeyProvider<T> { T GetKey(); } class Printer<T> where T : IPrintable {...} class SortedList<T> where T : IComparable<T> {...} class Dictionary<K,V> where K : IComparable<K> where V : IPrintable, IKeyProvider<K>, new() { ... }Das folgende Beispiel ist fehlerhaft, da es eine Zirkularität im Abhängigkeitsgraphen der Typparameter verursacht.
class Circular<S,T> where S: T where T: S // Error, circularity in dependency graph { ... }Die folgenden Beispiele veranschaulichen zusätzliche ungültige Situationen:
class Sealed<S,T> where S : T where T : struct // Error, `T` is sealed { ... } class A {...} class B {...} class Incompat<S,T> where S : A, T where T : B // Error, incompatible class-type constraints { ... } class StructWithClass<S,T,U> where S : struct, T where T : U where U : A // Error, A incompatible with struct { ... }Endbeispiel
Die dynamische Löschung eines Typs C ist vom Typ Cₓ und wie folgt aufgebaut:
- Wenn
Cein geschachtelter TypOuter.Innerist, dann istCₓein geschachtelter TypOuterₓ.Innerₓ. - Wenn
CCₓein konstruierter TypG<A¹, ..., Aⁿ>mit TypargumentenA¹, ..., Aⁿist, dann istCₓder konstruierte TypG<A¹ₓ, ..., Aⁿₓ>. - Wenn
Cein ArraytypE[]ist, dann istCₓder ArraytypEₓ[]. - Wenn
Cdynamisch ist, dann istCₓobject. - Ansonsten ist
CₓC.
Die effektive Basisklasse eines Typparameters T wird wie folgt definiert:
Sei R eine Menge von Typen, so dass:
- Für jede Einschränkung von
T, die ein Typparameter ist, enthältRdie effektive Basisklasse. - Für jede Einschränkung von
T, die einen Strukturtyp darstellt, enthältRSystem.ValueType. - Für jede Einschränkung von
T, die ein Enumerationstyp ist, enthältRSystem.Enum. - Für jede Einschränkung von
T, die ein Delegatentyp ist, enthältRseine dynamische Löschung. - Für jede Einschränkung von
T, die ein Arraytyp ist, enthältRSystem.Array. - Für jede Einschränkung von
T, die ein Klassentyp ist, enthältRihre dynamische Löschung.
Dann
- Wenn
Tden Werttyp-Beschränkung hat, istSystem.ValueTypedie effektive Basisklasse. -
RAndernfalls ist die effektive Basisklasseobjectleer. - Andernfalls ist die effektive Basisklasse
Tder am meisten eingeschlossene Typ (§10.5.3) des SatzesR. Wenn der Satz keinen eingeschlossenen Typ aufweist, istTdie effektive Basisklasse vonobject. 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
Tkeine secondary_constraints hat, ist seine effektive Schnittstellenmenge leer. - Wenn
TInterface_type Einschränkungen aufweist, aber keine type_parameter Einschränkungen vorhanden sind, ist der effektive Schnittstellensatz die Menge der dynamischen Tilgungen ihrer interface_type Einschränkungen. - Wenn
Tkeine 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
Tsowohl interface_type-Einschränkungen als auch type_parameter-Einschränkungen hat, besteht ihr effektiver Schnittstellensatz aus der Vereinigung der dynamischen Löschungen ihrer interface_type-Einschränkungen mit den effektiven Schnittstellensätzen ihrer type_parameter-Einschränkungen.
Ein Typparameter ist als Bezugstyp bekannt, wenn er die Bezugstypeinschränkung aufweist oder seine effektive Basisklasse nicht object oder System.ValueType. Ein Typparameter ist als nicht nullabler Bezugstyp bekannt, wenn er als Bezugstyp bekannt ist und die Nicht-NULL-Bezugstypeinschränkung aufweist.
Werte eines eingeschränkten Parametertyps können verwendet werden, um auf die durch die Einschränkungen implizierten Instanzelemente zuzugreifen.
Beispiel: Im Folgenden:
interface IPrintable { void Print(); } class Printer<T> where T : IPrintable { void PrintOne(T x) => x.Print(); }Die Methoden von
IPrintablekönnen direkt aufxaufgerufen werden, weilTso eingeschränkt ist, dass es immerIPrintableimplementieren muss.Endbeispiel
Wenn eine partielle generische Typdeklaration Einschränkungen enthält, stimmen die Einschränkungen allen anderen Teilen zu, die Einschränkungen enthalten. Insbesondere müssen alle Teile, die Einschränkungen enthalten, Einschränkungen für den gleichen Satz von Typparametern aufweisen, und für jeden Typparameter müssen die Gruppen der primären, sekundären und Konstruktoreinschränkungen gleichwertig sein. Zwei Gruppen von Einschränkungen sind gleich, wenn sie dieselben Mitglieder enthalten. Wenn kein Teil eines partiellen generischen Typs Typenparametereinschränkungen angibt, werden die Typparameter als nicht eingeschränkt betrachtet.
Beispiel:
partial class Map<K,V> where K : IComparable<K> where V : IKeyProvider<K>, new() { ... } partial class Map<K,V> where V : IKeyProvider<K>, new() where K : IComparable<K> { ... } partial class Map<K,V> { ... }ist richtig, da diese Teile, die Einschränkungen (die ersten beiden) enthalten, effektiv denselben Satz von primären, sekundären und Konstruktoreinschränkungen für denselben Satz von Typparametern angeben.
Endbeispiel
15.2.6 Klassentext
Die class_body einer Klasse definiert die Member dieser Klasse.
class_body
: '{' class_member_declaration* '}'
;
15.2.7 Partielle Typdeklarationen
Der Modifizierer partial wird beim Definieren einer Klasse, Struktur oder eines Schnittstellentyps in mehreren Teilen verwendet. Der partial Modifizierer ist ein kontextbezogenes Schlüsselwort (§6.4.4) und hat eine besondere Bedeutung unmittelbar vor den Schlüsselwörtern class, structund interface. (Ein Teiltyp kann Teilmethodendeklarationen (§15.6.9) enthalten.
Jeder Teil einer Teiltypdeklaration muss einen partial Modifizierer enthalten und muss im selben Namespace oder im selben Typ wie die anderen Teile deklariert werden. Der partial Modifizierer gibt an, dass an anderer Stelle zusätzliche Teile der Typdeklaration vorhanden sein können, aber das Vorhandensein solcher zusätzlichen Teile ist keine Anforderung; er ist gültig für die einzige Deklaration eines Typs, um den partial Modifizierer einzuschließen. Es ist nur für eine Deklaration eines Teiltyps gültig, um die Basisklasse oder implementierte Schnittstellen einzuschließen. Alle Deklarationen einer Basisklasse oder implementierter Schnittstellen müssen jedoch übereinstimmen, einschließlich der Nullierbarkeit aller angegebenen Typargumente.
Alle Teile eines Teiltyps müssen gemeinsam kompiliert werden, sodass die Teile während der Kompilierung zusammengeführt werden können. Partielle Typen lassen nicht zu, dass bereits kompilierte Typen erweitert werden.
Geschachtelte Typen können mithilfe des partial Modifizierers in mehreren Teilen deklariert werden. In der Regel wird der enthaltende Typ ebenfalls mit partial deklariert, und jeder Teil des geschachtelten Typs wird in einem anderen Abschnitt des enthaltenden Typs deklariert.
Beispiel: Die folgende partielle Klasse wird in zwei Teilen implementiert, die sich in unterschiedlichen Kompilierungseinheiten befinden. Der erste Teil wird von einem Datenbankzuordnungstool generiert, während der zweite Teil manuell erstellt wird:
public partial class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } } // File: Customer2.cs public partial class Customer { public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }Wenn die beiden obigen Teile zusammen kompiliert werden, verhält sich der resultierende Code wie folgt, als ob die Klasse als einzelne Einheit geschrieben wurde:
public class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }Endbeispiel
Die Behandlung von Attributen, die für den Typ oder die Typparameter verschiedener Teile einer Teiltypdeklaration angegeben sind, wird in §23.3 erläutert.
15.3 Klassenmitglieder
15.3.1 Allgemein
Die Mitglieder einer Klasse bestehen aus den durch ihre class_member_declarations eingeführten Mitgliedern und den von der direkten Basisklasse geerbten Mitgliedern.
class_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| finalizer_declaration
| static_constructor_declaration
| type_declaration
;
Die Mitglieder einer Klasse sind in die folgenden Kategorien unterteilt:
- Konstanten, die konstanten Werte darstellen, die der Klasse zugeordnet sind (§15.4).
- Felder, die die Variablen der Klasse sind (§15.5).
- Methoden, die die Berechnungen und Aktionen implementieren, die von der Klasse ausgeführt werden können (§15.6).
- Eigenschaften, die benannte Merkmale und die Aktionen definieren, die mit dem Lesen und Schreiben dieser Merkmale verbunden sind (§15.7).
- Ereignisse, die Benachrichtigungen definieren, die von der Klasse generiert werden können (§15.8).
- Indexer, die zulassen, dass Instanzen der Klasse auf die gleiche Weise (syntaktisch) wie Arrays (§15.9) indiziert werden können.
- Operatoren, die die Ausdrucksoperatoren definieren, die auf Instanzen der Klasse angewendet werden können (§15.10).
- Instanzkonstruktoren, die die zum Initialisieren von Instanzen der Klasse erforderlichen Aktionen implementieren (§15.11)
- Finalizer, die die auszuführenden Aktionen implementieren, bevor Instanzen der Klasse endgültig verworfen werden (§15.13).
- Statische Konstruktoren, die die zum Initialisieren der Klasse erforderlichen Aktionen implementieren (§15.12).
- Typen, die die Typen darstellen, die für die Klasse lokal sind (§14.7).
Ein class_declaration erstellt einen neuen Deklarationsraum (§7.3), und die type_parameters und die class_member_declarations, die unmittelbar in der class_declaration enthalten sind, führen neue Mitglieder in diesen Deklarationsraum ein. Die folgenden Regeln gelten für Klassenmitgliedserklärung:
Instanzkonstruktoren, Finalizer und statische Konstruktoren haben denselben Namen wie die unmittelbar eingeschlossene Klasse. Alle anderen Mitglieder haben Namen, die sich vom Namen der unmittelbar eingeschlossenen Klasse unterscheiden.
Der Name eines Typparameters in der type_parameter_list einer Klassendeklaration unterscheidet sich von den Namen aller anderen Typparameter in demselben type_parameter_list und unterscheidet sich von dem Namen der Klasse und den Namen aller Member der Klasse.
Der Name eines Typs unterscheidet sich von den Namen aller Nichttypmitglieder, die in derselben Klasse deklariert sind. Wenn zwei oder mehr Typdeklarationen denselben vollqualifizierten Namen aufweisen, müssen die Deklarationen den
partialModifizierer (§15.2.7) haben und diese Deklarationen kombinieren, um einen einzelnen Typ zu definieren.
Hinweis: Da der vollqualifizierte Name einer Typdeklaration die Anzahl der Typparameter codiert, können zwei unterschiedliche Typen denselben Namen aufweisen, solange sie unterschiedliche Anzahl von Typparametern haben. Hinweisende
Der Name einer Konstante, eines Felds, einer Eigenschaft oder eines Ereignisses unterscheidet sich von den Namen aller anderen Elemente, die in derselben Klasse deklariert sind.
Der Name einer Methode unterscheidet sich von den Namen aller anderen Nichtmethoden, die in derselben Klasse deklariert sind. Darüber hinaus unterscheidet sich die Signatur (§7.6) einer Methode von den Signaturen aller anderen Methoden, die in derselben Klasse deklariert sind, und zwei Methoden, die in derselben Klasse deklariert sind, dürfen keine Signaturen aufweisen, die sich ausschließlich von
in,out, undref.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
refundout.Die Signatur eines Indexers unterscheidet sich von den Signaturen aller anderen in derselben Klasse deklarierten Indexer.
Die Signatur eines Betreibers unterscheidet sich von den Signaturen aller anderen Betreiber, die in derselben Klasse deklariert sind.
Die geerbten Mitglieder einer Klasse (§15.3.4) sind nicht Teil des Deklarationsbereichs einer Klasse.
Hinweis: Daher kann eine abgeleitete Klasse ein Element mit demselben Namen oder derselben Signatur wie ein geerbtes Element deklarieren (wodurch das geerbte Element tatsächlich ausgeblendet wird). Hinweisende
Der Satz von Mitgliedern eines Typs, der in mehreren Teilen deklariert ist (§15.2.7) ist die Vereinigung der in jedem Teil deklarierten Mitglieder. Die Körper aller Teile der Typdeklaration haben denselben Deklarationsbereich (§7.3), und der Umfang jedes Mitglieds (§7.7) erstreckt sich auf die Körper aller Teile. Die Accessibility-Domäne eines Mitglieds umfasst immer alle Teile des einschließenden Typs; ein in einem Teil deklariertes privates Mitglied ist von einem anderen Teil aus frei zugänglich. Es handelt sich um einen Kompilierungsfehler, wenn dasselbe Mitglied in mehr als einem Teil des Typs deklariert wird, es sei denn, dieses Mitglied verfügt über den partial Modifier.
Beispiel:
partial class A { int x; // Error, cannot declare x more than once partial void M(); // Ok, defining partial method declaration partial class Inner // Ok, Inner is a partial type { int y; } } partial class A { int x; // Error, cannot declare x more than once partial void M() { } // Ok, implementing partial method declaration partial class Inner // Ok, Inner is a partial type { int z; } }Endbeispiel
Die Feldinitialisierungsreihenfolge kann innerhalb des C#-Codes erheblich sein, und einige Garantien werden gemäß §15.5.6.1 bereitgestellt. Andernfalls ist die Reihenfolge von Elementen innerhalb eines Typs selten signifikant, kann aber bei der Interfacierung mit anderen Sprachen und Umgebungen erheblich sein. In diesen Fällen ist die Reihenfolge von Elementen innerhalb eines Typs, der in mehreren Teilen deklariert ist, nicht definiert.
15.3.2 Der Instanztyp
Jede Klassendeklaration weist einen zugeordneten Instanztyp auf. Bei einer generischen Klassendeklaration wird der Instanztyp durch Erstellen eines konstruierten Typs (§8.4) aus der Typdeklaration gebildet, wobei jedes der angegebenen Typargumente der entsprechende Typparameter ist. Da der Instanztyp die Typparameter verwendet, kann er nur verwendet werden, wo sich die Typparameter im Bereich befinden. d. h. innerhalb der Klassendeklaration. Der Instanztyp ist der Typ von this für Code, der innerhalb der Klassendeklaration geschrieben wurde. Bei nicht generischen Klassen ist der Instanztyp einfach die deklarierte Klasse.
Beispiel: Im Folgenden werden mehrere Klassendeklarationen zusammen mit ihren Instanztypen gezeigt:
class A<T> // instance type: A<T> { class B {} // instance type: A<T>.B class C<U> {} // instance type: A<T>.C<U> } class D {} // instance type: DEndbeispiel
15.3.3 Mitglieder von konstruierten Typen
Die nicht geerbten Member eines konstruierten Typs erhalten Sie, indem Sie für jeden Typ-Parameter in der Member-Deklaration das entsprechende Typ-Argument des konstruierten Typs ersetzen. Der Ersetzungsprozess basiert auf der semantischen Bedeutung von Typdeklarationen und ist nicht einfach nur textbezogene Ersetzung.
Beispiel: Angenommen die generische Klassendeklaration
class Gen<T,U> { public T[,] a; public void G(int i, T t, Gen<U,T> gt) {...} public U Prop { get {...} set {...} } public int H(double d) {...} }der konstruierte Typ
Gen<int[],IComparable<string>>hat die folgenden Elemente:public int[,][] a; public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...} public IComparable<string> Prop { get {...} set {...} } public int H(double d) {...}Der Typ des Elements
ain der generischen KlassendeklarationGenist "zweidimensionales Array vonT", sodass der Typ des Elementsaim obigen konstruierten Typ "zweidimensionales Array eines eindimensionalen Arrays vonint" oderint[,][].Endbeispiel
Innerhalb von Instanzfunktionsmitgliedern ist der Typ von this der Instanztyp (§15.3.2) der enthaltenen Deklaration.
Alle Member einer generischen Klasse können Typparameter aus jeder eingeschlossenen Klasse verwenden, entweder direkt oder als Teil eines konstruierten Typs. Wenn zur Laufzeit ein bestimmter geschlossener konstruierter Typ (§8.4.3) verwendet wird, wird jede Verwendung eines Typparameters durch das Typargument ersetzt, das für den konstruierten Typ bereitgestellt wird.
Beispiel:
class C<V> { public V f1; public C<V> f2; public C(V x) { this.f1 = x; this.f2 = this; } } class Application { static void Main() { C<int> x1 = new C<int>(1); Console.WriteLine(x1.f1); // Prints 1 C<double> x2 = new C<double>(3.1415); Console.WriteLine(x2.f1); // Prints 3.1415 } }Endbeispiel
15.3.4 Vererbung
Eine Klasse erbt die Mitglieder ihrer direkten Basisklasse. Vererbung bedeutet, dass eine Klasse implizit alle Member ihrer direkten Basisklasse enthält, mit Ausnahme der Instanzkonstruktoren, Finalizer und statischen Konstruktoren der Basisklasse. Einige wichtige Aspekte der Vererbung sind:
Vererbung ist transitiv. Wenn
CvonBabgeleitet ist undBvonAabgeleitet ist, dann erbtCdie inBdeklarierten Mitglieder sowie die inAdeklarierten Mitglieder.Eine abgeleitete Klasse erweitert ihre direkte Basisklasse. Eine abgeleitete Klasse kann ihren geerbten Mitgliedern neue hinzufügen, jedoch die Definition eines geerbten Mitglieds nicht entfernen.
Instanzkonstruktoren, Finalisierer und statische Konstruktoren werden nicht vererbt, aber alle anderen Mitglieder, unabhängig von ihrer deklarierten Zugänglichkeit (§7.5). Abhängig von ihrer deklarierten Accessibility sind vererbte Mitglieder jedoch möglicherweise nicht in einer abgeleiteten Klasse zugänglich.
Eine abgeleitete Klasse kann geerbte Member (§7.7.2.3) ausblenden, indem neue Member mit demselben Namen oder derselben Signatur deklariert werden. Das Ausblenden eines geerbten Elements entfernt dieses Element jedoch nicht, sondern verhindert lediglich den direkten Zugriff darauf über die abgeleitete Klasse.
Eine Instanz einer Klasse enthält einen Satz aller Instanzfelder, die in der Klasse und deren Basisklassen deklariert sind, und eine implizite Konvertierung (§10.2.8) besteht aus einem abgeleiteten Klassentyp in einen seiner Basisklassentypen. Daher kann ein Verweis auf eine Instanz einiger abgeleiteter Klassen als Verweis auf eine Instanz einer seiner Basisklassen behandelt werden.
Eine Klasse kann virtuelle Methoden, Eigenschaften, Indexer und Ereignisse deklarieren und abgeleitete Klassen können die Implementierung dieser Funktionsmember überschreiben. Dadurch können Klassen polymorphes Verhalten aufweisen, wobei die aktionen, die von einem Funktionselementaufruf ausgeführt werden, je nach Laufzeittyp der Instanz variieren, über die dieses Funktionselement aufgerufen wird.
Die geerbten Mitglieder eines konstruierten Klassentyps sind die Mitglieder des unmittelbaren Basisklassentyps (§15.2.4.2), der durch Ersetzen der Typargumente des konstruierten Typs für jedes Vorkommen der entsprechenden Typparameter in der Basisklassenspezifikationgefunden wird. Diese Mitglieder wiederum werden umgewandelt, indem für jeden Typ-Parameter in der Mitgliederdeklaration das entsprechende Typ-Argument der Basisklassen-Spezifikationersetzt wird.
Beispiel:
class B<U> { public U F(long index) {...} } class D<T> : B<T[]> { public T G(string s) {...} }Im obigen Code verfügt der konstruierte Typ
D<int>über ein nicht geerbtes öffentliches ElementintG(string s), das durch das Ersetzen des Typargumentsintfür den TypparameterTabgerufen wird.D<int>verfügt außerdem über ein geerbtes Element aus der KlassendeklarationB. Dieses geerbte Mitglied wird ermittelt, indem zunächst der BasisklassentypB<int[]>vonD<int>bestimmt wird, indemintfürTin der BasisklassenspezifikationB<T[]>ersetzt wird. Dann wirdBals Typargument fürint[]durchUinpublic U F(long index)ersetzt, was das geerbte Mitgliedpublic int[] F(long index)ergibt.Endbeispiel
15.3.5 Der neue Modifizierer
Eine class_member_declaration darf ein Mitglied mit demselben Namen oder derselben Signatur wie ein geerbtes Mitglied deklarieren. Wenn dies geschieht, wird gesagt, dass das Mitglied der abgeleiteten Klasse das Mitglied der Basisklasse versteckt. Siehe §7.7.2.3 für eine genaue Spezifikation, wann ein Mitglied ein geerbtes Mitglied ausblendet.
Ein geerbtes Element M wird als verfügbar betrachtet, wenn zugänglich ist und es kein anderes geerbtes zugängliches Element N gibt, das bereits ausblendet. Das implizite Ausblenden eines geerbten Mitglieds wird nicht als Fehler betrachtet, aber ein Compiler zeigt eine Warnung an, es sei denn, die Deklaration des Mitglieds der abgeleiteten Klasse enthält einen new-Modifizierer, um explizit anzugeben, dass das abgeleitete Mitglied das Basismitglied ausblenden soll. Wenn mindestens ein Teil einer Teildeklaration (§15.2.7) eines geschachtelten Typs den new Modifizierer enthält, wird keine Warnung ausgegeben, wenn der geschachtelte Typ ein verfügbares geerbtes Element ausblendet.
Wenn ein new Modifikator in einer Deklaration enthalten ist, die ein verfügbares geerbtes Mitglied nicht ausblendet, wird eine entsprechende Warnung ausgegeben.
15.3.6 Zugriffsmodifizierer
Eine class_member_declaration kann eine der erlaubten Arten der angegebenen Zugänglichkeit (§7.5.2) haben: public, protected internal, protected, private protected, internal oder private. Mit Ausnahme der protected internal und private protected Kombinationen ist es ein Fehler zur Kompilierzeit, mehr als einen Zugriffsmodifizierer anzugeben. Wenn eine class_member_declaration keine Zugriffsmodifikatoren enthält, wird private angenommen.
15.3.7 Bestandteiltypen
Typen, die in der Deklaration eines Elements verwendet werden, werden als Bestandteiltypen dieses Elements bezeichnet. Mögliche Komponententypen sind der Typ einer Konstante, eines Felds, einer Eigenschaft, eines Ereignisses oder eines Indexers, der Rückgabetyp einer Methode oder eines Operators sowie die Parametertypen einer Methode, eines Indexers, eines Operators oder eines Instanzkonstruktors. Die Bestandteiltypen eines Mitglieds sind mindestens ebenso zugänglich wie dieses Mitglied selbst (§7.5.5).
15.3.8 Statische und Instanzmitglieder
Mitglieder einer Klasse sind entweder statische Mitglieder oder Instanzmitglieder.
Hinweis: Im Allgemeinen ist es hilfreich, statische Member als Zugehörigkeit zu Klassen und Instanzmbern als Zugehörigkeit zu Objekten (Instanzen von Klassen) zu betrachten. Hinweisende
Wenn ein Feld, eine Methode, eine Eigenschaft, ein Ereignis, ein Operator oder eine Konstruktordeklaration einen static Modifizierer enthält, deklariert es ein statisches Element. Darüber hinaus deklariert eine Konstante oder Typdeklaration implizit ein statisches Element. Statische Mitglieder haben die folgenden Merkmale:
- Wenn auf ein statisches Element
Min einem member_access (§12.8.7) des FormularsE.Mverwiesen wird,Ewird ein Typ mit einem ElementMbezeichnet. Es ist ein Kompilierzeitfehler, wennEeine Instanz bezeichnet. - Ein statisches Feld in einer nicht generischen Klasse identifiziert genau einen Speicherort. Unabhängig davon, wie viele Instanzen einer nicht generischen Klasse erstellt werden, gibt es immer nur eine Kopie eines statischen Felds. Jeder unterschiedliche geschlossene konstruierte Typ (§8.4.3) verfügt über einen eigenen Satz statischer Felder, unabhängig von der Anzahl der Instanzen des geschlossenen konstruierten Typs.
- Ein statisches Funktionselement (Methode, Eigenschaft, Ereignis, Operator oder Konstruktor) wird nicht für eine bestimmte Instanz ausgeführt, und es handelt sich um einen Kompilierungszeitfehler, der in einem solchen Funktionselement darauf verweist.
Wenn eine Feld-, Methoden-, Eigenschafts-, Ereignis-, Indexer-, Konstruktor- oder Finalizerdeklaration keinen statischen Modifizierer enthält, deklariert sie ein Instanzmemm. (Ein Instanzmitglied wird manchmal als nicht statisches Element bezeichnet.) Instanzmitglieder weisen die folgenden Merkmale auf:
- Wenn auf ein Instanzmitglied
Min einem member_access (§12.8.7) des FormularsE.Mverwiesen wird,Ewird eine Instanz eines Typs bezeichnet, der über ein MitgliedMverfügt. Es handelt sich um einen Bindungszeitfehler, bei dem E einen Typ angibt. - Jede Instanz einer Klasse enthält einen separaten Satz aller Instanzfelder der Klasse.
- Ein Instanzfunktionsmitglied (Methode, Eigenschaft, Indexer, Instanzkonstruktor oder Finalisierer) operiert auf einer bestimmten Instanz der Klasse, und auf diese Instanz kann als
thiszugegriffen werden (§12.8.14).
Beispiel: Im folgenden Beispiel werden die Regeln für den Zugriff auf statische und Instanzmitglieder veranschaulicht.
class Test { int x; static int y; void F() { x = 1; // Ok, same as this.x = 1 y = 1; // Ok, same as Test.y = 1 } static void G() { x = 1; // Error, cannot access this.x y = 1; // Ok, same as Test.y = 1 } static void Main() { Test t = new Test(); t.x = 1; // Ok t.y = 1; // Error, cannot access static member through instance Test.x = 1; // Error, cannot access instance member through type Test.y = 1; // Ok } }Die
FMethode zeigt, dass in einem Instanzfunktionsmember ein simple_name (§12.8.4) für den Zugriff auf Instanzmember und statische Member verwendet werden kann. DieGMethode zeigt, dass es ein Kompilierungszeitfehler ist, in einem statischen Funktionsmitglied über einen simple_name auf ein Instanzelement zuzugreifen. DieMain-Methode verdeutlicht, dass in einem member_access (§12.8.7) Instanzmitglieder über Instanzen zugänglich gemacht werden sollen und statische Mitglieder über Typen zugänglich gemacht werden sollen.Endbeispiel
15.3.9 Geschachtelte Typen
15.3.9.1 Allgemein
Ein typ, der in einer Klasse, Struktur oder Schnittstelle deklariert ist, 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
Bist ein geschachtelter Typ, da sie innerhalb der KlasseAdeklariert ist und die KlasseAein nicht geschachtelter Typ ist, da sie in einer Kompilierungseinheit deklariert wird.Endbeispiel
15.3.9.2 Vollqualifizierter Name
Der vollqualifizierte Name (§7.8.3) für eine verschachtelte Typdeklaration ist S.N, wobei S der vollqualifizierte Name der Typdeklaration ist, in der der Typ N deklariert wird, und N der nicht qualifizierte Name (§7.8.2) der verschachtelten Typdeklaration ist (einschließlich aller generic_dimension_specifier (§12.8.18)).
15.3.9.3 Barrierefreiheit deklariert
Nicht verschachtelte Typen können public oder internal deklarierte Zugänglichkeit haben und haben standardmäßig internal deklarierte Zugänglichkeit. Geschachtelte Typen können auch diese Formen der deklarierten Barrierefreiheit aufweisen, sowie eine oder mehrere zusätzliche Formen der deklarierten Barrierefreiheit, je nachdem, ob der enthaltende Typ eine Klasse, Struktur oder Schnittstelle ist:
- Ein geschachtelter Typ, der in einer Klasse deklariert wird, kann über eine der zulässigen Arten erklärter Zugriffsebenen verfügen und weist, wie andere Klassenmitglieder, standardmäßig die
privateerklärte Zugriffsebene auf. - Ein geschachtelter Typ, der in einer Struktur deklariert ist, kann eine von drei Formen der deklarierten Sichtbarkeit aufweisen (
public,internaloderprivate) und hat, wie andere Strukturmitglieder, standardmäßig die deklarierteprivateSichtbarkeit.
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
Man, die die MethodeMinBaseversteckt.Endbeispiel
15.3.9.5 dieser Zugriff
Ein geschachtelter Typ und sein enthaltender Typ haben keine besondere Beziehung zu this_access (§12.8.14). Insbesondere kann this innerhalb eines verschachtelten Typs nicht verwendet werden, um auf Instanzmitglieder des enthaltenen Typs zu verweisen. In Fällen, in denen ein verschachtelter Typ Zugriff auf die Instanzmember seines enthaltenden Typs benötigt, kann der Zugriff gewährt werden, indem die this für die Instanz des enthaltenden Typs als Konstruktorargument für den verschachtelten Typ angegeben wird.
Beispiel: Das folgende Beispiel
class C { int i = 123; public void F() { Nested n = new Nested(this); n.G(); } public class Nested { C this_c; public Nested(C c) { this_c = c; } public void G() { Console.WriteLine(this_c.i); } } } class Test { static void Main() { C c = new C(); c.F(); } }zeigt diese Technik. Eine Instanz von
Cerzeugt eine Instanz vonNestedund übergibt ihr eigenes this an den Konstruktor vonNested, um den späteren Zugriff auf die Instanzmitglieder vonCzu ermöglichen.Endbeispiel
15.3.9.6 Zugriff auf private und geschützte Elemente des enthaltenen Typs
Ein verschachtelter Typ hat Zugriff auf alle Mitglieder, auf die sein enthaltender Typ zugreifen kann, einschließlich der Mitglieder des enthaltenden Typs, für die private und protected als zugriffsfähig erklärt wurden.
Beispiel: Das Beispiel
class C { private static void F() => Console.WriteLine("C.F"); public class Nested { public static void G() => F(); } } class Test { static void Main() => C.Nested.G(); }zeigt eine Klasse
Cmit einer geschachtelten KlasseNestedan. Innerhalb vonNestedruft die MethodeGdie inFdefinierte statische MethodeCauf, undFhat private deklarierte Zugriffsebene.Endbeispiel
Ein geschachtelter Typ kann auch auf geschützte Elemente zugreifen, die in einem Basistyp des zugehörigen Typs definiert sind.
Beispiel: Im folgenden Code
class Base { protected void F() => Console.WriteLine("Base.F"); } class Derived: Base { public class Nested { public void G() { Derived d = new Derived(); d.F(); // ok } } } class Test { static void Main() { Derived.Nested n = new Derived.Nested(); n.G(); } }die geschachtelte Klasse
Derived.Nestedgreift durch eine Instanz vonFauf die geschützte MethodeDerivedzu, die in der Basisklasse vonBase,Derived, definiert ist.Endbeispiel
15.3.9.7 Geschachtelte Typen in generischen Klassen
Eine generische Klassendeklaration kann geschachtelte Typdeklarationen enthalten. Die Typparameter der eingeschlossenen Klasse können innerhalb der geschachtelten Typen verwendet werden. Eine geschachtelte Typdeklaration kann zusätzliche Typparameter enthalten, die nur für den geschachtelten Typ gelten.
Jede Typdeklaration, die in einer generischen Klassendeklaration enthalten ist, ist implizit eine generische Typdeklaration. Beim Schreiben eines Verweises auf einen Typ, der in einem generischen Typ geschachtelt ist, muss der enthaltende konstruierte Typ, einschließlich seiner Typargumente, benannt werden. Innerhalb der äußeren Klasse kann der verschachtelte Typ jedoch ohne Qualifikation verwendet werden; der Instanztyp der äußeren Klasse kann implizit beim Erstellen des verschachtelten Typs verwendet werden.
Beispiel: Im Folgenden werden drei verschiedene korrekte Wege zur Darstellung eines konstruierten Typs vorgestellt, der aus
Innererstellt wurde; die ersten beiden sind gleichwertig:class Outer<T> { class Inner<U> { public static void F(T t, U u) {...} } static void F(T t) { Outer<T>.Inner<string>.F(t, "abc"); // These two statements have Inner<string>.F(t, "abc"); // the same effect Outer<int>.Inner<string>.F(3, "abc"); // This type is different Outer.Inner<string>.F(t, "abc"); // Error, Outer needs type arg } }Endbeispiel
Obwohl es sich um einen schlechten Programmierstil handelt, kann ein Typparameter in einem geschachtelten Typ einen Element- oder Typparameter ausblenden, der im äußeren Typ deklariert ist.
Beispiel:
class Outer<T> { class Inner<T> // Valid, hides Outer's T { public T t; // Refers to Inner's T } }Endbeispiel
15.3.10 Reservierte Mitgliedsnamen
15.3.10.1 Allgemein
Um die zugrunde liegende C#-Laufzeitimplementierung zu erleichtern, reserviert die Implementierung für jede Quellmitgliedsdeklaration, die eine Eigenschaft, ein Ereignis oder einen Indexer ist, zwei Methodensignaturen basierend auf der Art der Memberdeklaration, ihrem Namen und ihrem Typ (§15.3.10.2, §15.3.10.3, §15.3.10.4). Es handelt sich um einen Kompilierzeitfehler für ein Programm, um ein Mitglied zu deklarieren, dessen Signatur mit einer Signatur übereinstimmt, die von einem Mitglied reserviert ist, das im selben Bereich deklariert ist, auch wenn die zugrunde liegende Laufzeitimplementierung diese Reservierungen nicht verwendet.
Die reservierten Namen führen keine Deklarationen ein, daher nehmen sie nicht an der Mitgliedersuche teil. Die zugeordneten reservierten Methodensignaturen einer Deklaration nehmen jedoch an der Vererbung (§15.3.4) teil und können mit dem new Modifizierer (§15.3.5) ausgeblendet werden.
Hinweis: Die Reservierung dieser Namen dient drei Zwecken:
- Damit die zugrunde liegende Implementierung einen normalen Bezeichner als Methodenname zum Abrufen oder Festlegen des Zugriffs auf das C#-Sprachfeature verwenden kann.
- Damit andere Sprachen mit einem gewöhnlichen Bezeichner als Methodenname zum Abrufen oder Festlegen des Zugriffs auf das C#-Sprachfeature interoperieren können.
- Um sicherzustellen, dass die von einem konformen Compiler akzeptierte Quelle von einem anderen akzeptiert wird, indem die Besonderheiten reservierter Membernamen in allen C#-Implementierungen konsistent sind.
Hinweisende
Die Deklaration eines Finalizers (§15.13) bewirkt auch, dass eine Signatur reserviert wird (§15.3.10.5).
Bestimmte Namen sind für die Verwendung als Operatormethodennamen reserviert (§15.3.10.6).
15.3.10.2 Für Eigenschaften reservierte Mitgliedsnamen
Für eine Eigenschaft P (§15.7) vom Typ Tsind die folgenden Signaturen reserviert:
T get_P();
void set_P(T value);
Beide Signaturen sind reserviert, auch wenn die Eigenschaft schreibgeschützt oder lesegeschützt ist.
Beispiel: Im folgenden Code
class A { public int P { get => 123; } } class B : A { public new int get_P() => 456; public new void set_P(int value) { } } class Test { static void Main() { B b = new B(); A a = b; Console.WriteLine(a.P); Console.WriteLine(b.P); Console.WriteLine(b.get_P()); } }Eine Klasse
Adefiniert eine schreibgeschützte EigenschaftP, wodurch Signaturen fürget_Pundset_PMethoden reserviert werden.ADie KlasseBleitet sich vonAab und verbirgt beide dieser reservierten Signaturen. Das Beispiel ergibt die Ausgabe:123 123 456Endbeispiel
15.3.10.3 Mitgliedsnamen, die für Ereignisse reserviert sind
Für ein Ereignis E (§15.8) des Delegatentyps Tsind die folgenden Signaturen reserviert:
void add_E(T handler);
void remove_E(T handler);
15.3.10.4 Mitgliedsnamen, die für Indexer reserviert sind
Für einen Indexer (§15.9) vom Typ T mit Parameterliste Lsind die folgenden Signaturen reserviert:
T get_Item(L);
void set_Item(L, T value);
Beide Signaturen sind reserviert, auch wenn der Indexer schreibgeschützt oder schreibgeschützt ist.
Darüber hinaus ist der Membername Item reserviert.
15.3.10.5 Für Finalizer reservierte Mitgliedsnamen
Für eine Klasse mit einem Finalizer (§15.13) ist die folgende Signatur reserviert:
void Finalize();
15.3.10.6 Methodennamen, die für Operatoren reserviert sind
Die folgenden Methodennamen sind reserviert. Während viele entsprechende Operatoren in dieser Spezifikation haben, sind einige für die Verwendung durch zukünftige Versionen reserviert, während einige für die Interoperabilität mit anderen Sprachen reserviert sind.
| Methodenname | C#-Operator |
|---|---|
op_Addition |
+ (binär) |
op_AdditionAssignment |
(reserviert) |
op_AddressOf |
(reserviert) |
op_Assign |
(reserviert) |
op_BitwiseAnd |
& (binär) |
op_BitwiseAndAssignment |
(reserviert) |
op_BitwiseOr |
\| |
op_BitwiseOrAssignment |
(reserviert) |
op_CheckedAddition |
(reserviert für die zukünftige Nutzung) |
op_CheckedDecrement |
(reserviert für die zukünftige Nutzung) |
op_CheckedDivision |
(reserviert für die zukünftige Nutzung) |
op_CheckedExplicit |
(reserviert für die zukünftige Nutzung) |
op_CheckedIncrement |
(reserviert für die zukünftige Nutzung) |
op_CheckedMultiply |
(reserviert für die zukünftige Nutzung) |
op_CheckedSubtraction |
(reserviert für die zukünftige Nutzung) |
op_CheckedUnaryNegation |
(reserviert für die zukünftige Nutzung) |
op_Comma |
(reserviert) |
op_Decrement |
-- (Präfix und Postfix) |
op_Division |
/ |
op_DivisionAssignment |
(reserviert) |
op_Equality |
== |
op_ExclusiveOr |
^ |
op_ExclusiveOrAssignment |
(reserviert) |
op_Explicit |
expliziter (verengender) Zwang |
op_False |
false |
op_GreaterThan |
> |
op_GreaterThanOrEqual |
>= |
op_Implicit |
impliziter (sich erweiternder) Zwang |
op_Increment |
++ (Präfix und Postfix) |
op_Inequality |
!= |
op_LeftShift |
<< |
op_LeftShiftAssignment |
(reserviert) |
op_LessThan |
< |
op_LessThanOrEqual |
<= |
op_LogicalAnd |
(reserviert) |
op_LogicalNot |
! |
op_LogicalOr |
(reserviert) |
op_MemberSelection |
(reserviert) |
op_Modulus |
% |
op_ModulusAssignment |
(reserviert) |
op_MultiplicationAssignment |
(reserviert) |
op_Multiply |
* (binär) |
op_OnesComplement |
~ |
op_PointerDereference |
(reserviert) |
op_PointerToMemberSelection |
(reserviert) |
op_RightShift |
>> |
op_RightShiftAssignment |
(reserviert) |
op_SignedRightShift |
(reserviert) |
op_Subtraction |
- (binär) |
op_SubtractionAssignment |
(reserviert) |
op_True |
true |
op_UnaryNegation |
- (unär) |
op_UnaryPlus |
+ (unär) |
op_UnsignedRightShift |
(reserviert für die zukünftige Nutzung) |
op_UnsignedRightShiftAssignment |
(reserviert) |
15.4 Konstanten
Eine Konstante ist ein Klassenelement, das einen Konstantenwert darstellt: ein Wert, der zur Kompilierungszeit berechnet werden kann. Eine constant_declaration führt eine oder mehrere Konstanten eines bestimmten Typs ein.
constant_declaration
: attributes? constant_modifier* 'const' type constant_declarators ';'
;
constant_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;
Ein constant_declaration kann einen Satz von Attributen (§23), einen new Modifizierer (§15.3.5) und eine der zulässigen Arten deklarierter Barrierefreiheit (§15.3.6) enthalten. Die Attribute und Modifizierer gelten für alle Elemente, die vom constant_declaration deklariert wurden. Auch wenn Konstanten als statische Mitglieder betrachtet werden, erfordert oder erlaubt eine Konstanten-Deklaration weder einen Modifizierer noch lässt sie einen zu. Es ist ein Fehler, wenn derselbe Modifizierer mehrfach in einer Konstantendeklaration auftaucht.
Der Typ eines constant_declaration gibt den Typ der elemente an, die durch die Deklaration eingeführt wurden. Auf den Typ folgt eine Liste der constant_declarator s (§13.6.3), von denen jedes ein neuesMitglied einführt. Eine constant_declarator besteht aus einem Bezeichner , der das Element benennt, gefolgt von einem "="-Token, gefolgt von einem constant_expression (§12.24), der den Wert des Elements angibt.
Der in einer Konstantendeklaration angegebene Typ muss sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, ein enum_type oder ein reference_type sein. Jede constant_expression erhält einen Wert des Zieltyps oder eines Typs, der durch eine implizite Konvertierung (§10.2) in den Zieltyp konvertiert werden kann.
Der Typ einer Konstante muss mindestens so zugänglich sein wie die Konstante selbst (§7.5.5).
Der Wert einer Konstanten wird in einem Ausdruck mithilfe eines simple_name (§12.8.4) oder eines member_access (§12.8.7) abgerufen.
Eine Konstante kann sich selbst an einer constant_expression beteiligen. Daher kann eine Konstante in jedem Konstrukt verwendet werden, das eine constant_expression erfordert.
Hinweis: Beispiele für solche Konstrukte sind
caseBezeichnungen,goto caseAnweisungen,enumMemberdeklarationen, Attribute und andere Konstantendeklarationen. Hinweisende
Hinweis: Wie in §12.24 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
stringdie Anwendung desnewOperators ist und da dernewOperator in einem constant_expression nicht zulässig ist, ist der einzige mögliche Wert für Konstanten von reference_typeanderen alsstring.nullHinweisende
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
constundreadonlyunterscheidet sich (§15.5.3.3). Hinweisende
Eine Konstantendeklaration, die mehrere Konstanten deklariert, entspricht mehreren Deklarationen einzelner Konstanten mit denselben Attributen, Modifizierern und Typ.
Beispiel:
class A { public const double X = 1.0, Y = 2.0, Z = 3.0; }entspricht
class A { public const double X = 1.0; public const double Y = 2.0; public const double Z = 3.0; }Endbeispiel
Konstanten dürfen von anderen Konstanten innerhalb desselben Programms abhängig sein, solange die Abhängigkeiten nicht zirkulär sind.
Beispiel: Im folgenden Code
class A { public const int X = B.Z + 1; public const int Y = 10; } class B { public const int Z = A.Y + 1; }ein Compiler muss zuerst
A.Yauswerten und dannB.Zauswerten und schließlichA.Xauswerten, wobei die Werte10,11und12erzeugt werden.Endbeispiel
Konstantendeklarationen können von Konstanten aus anderen Programmen abhängen, aber solche Abhängigkeiten sind nur in einer Richtung möglich.
Beispiel: Wenn auf das obige Beispiel verwiesen wird und
AsowieBin separaten Programmen deklariert wurden, wäre es möglich, dassA.XvonB.Zabhängig ist, aberB.Zkönnte dann nicht gleichzeitig vonA.Yabhängig sein. Endbeispiel
15.5 Felder
15.5.1 Allgemein
Ein Feld ist ein Element, das eine Variable darstellt, die einem Objekt oder einer Klasse zugeordnet ist. Ein field_declaration führt ein oder mehrere Felder eines bestimmten Typs ein.
field_declaration
: attributes? field_modifier* type variable_declarators ';'
;
field_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'readonly'
| 'volatile'
| unsafe_modifier // unsafe code support
;
variable_declarators
: variable_declarator (',' variable_declarator)*
;
variable_declarator
: identifier ('=' variable_initializer)?
;
unsafe_modifier (§24.2) ist nur im unsicheren Code (§24) verfügbar.
Ein field_declaration kann einen Satz von Attributen (§23), einen new Modifizierer (§15.3.5), eine gültige Kombination der vier Zugriffsmodifizierer (§15.3.6) und einen static Modifizierer (§15.5.2) enthalten. Darüber hinaus kann ein field_declaration einen readonly Modifizierer (§15.5.3) oder einen volatile Modifizierer (§15.5.4) enthalten, jedoch nicht beide. Die Attribute und Modifizierer gelten für alle Elemente, die vom field_declaration deklariert wurden. Es ist ein Fehler, wenn derselbe Modifizierer mehrmals in einer field_declaration erscheint.
Der Typ eines field_declaration gibt den Typ der elemente an, die durch die Deklaration eingeführt wurden. Auf den Typ folgt eine Liste der variable_declarator, von denen jedes ein neues Mitglied einführt. Ein variable_declarator besteht aus einem Bezeichner , der das Element benennt, optional gefolgt von einem "="-Token und einem variable_initializer (§15.5.6), der den Anfangswert dieses Elements angibt.
Der Typ eines Felds muss mindestens so zugänglich sein wie das Feld selbst (§7.5.5).
Der Wert eines Felds wird in einem Ausdruck mithilfe eines simple_name (§12.8.4), eines member_access (§12.8.7) oder eines base_access (§12.8.15) abgerufen. Der Wert eines nicht gelesenen Felds wird mithilfe einer Zuordnung geändert (§12.22). Der Wert eines nicht gelesenen Felds kann mit postfix-Inkrement- und Dekrementoperatoren (§12.8.16) und Präfixoperatoren (§12.9.7) 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; }entspricht
class A { public static int X = 1; public static int Y; public static int Z = 100; }Endbeispiel
15.5.2 Statische Felder und Instanzfelder
Wenn eine Felddeklaration einen static Modifizierer enthält, sind die durch die Deklaration eingeführten Felder statische Felder. Wenn kein static Modifizierer vorhanden ist, sind die durch die Deklaration eingeführten Felder Instanzfelder. Statische Felder und Instanzfelder sind zwei der verschiedenen Arten von Variablen (§9), die von C# unterstützt werden, und manchmal werden sie als statische Variablen bzw. Instanzvariablen bezeichnet.
Wie in §15.3.8 erläutert, enthält jede Instanz einer Klasse einen vollständigen Satz der Instanzfelder der Klasse, während es nur einen Satz statischer Felder für jeden nicht generischen oder geschlossenen konstruierten Typ gibt, unabhängig von der Anzahl der Instanzen der Klasse oder des geschlossenen konstruierten Typs.
15.5.3 Schreibgeschützte Felder
15.5.3.1 Allgemein
Wenn eine Felddeklaration einen readonly Modifikator enthält, sind die durch die Deklaration eingeführten Felder nur lesbare Felder. Direkte Zuweisungen zu schreibgeschützten Feldern können nur als Teil dieser Deklaration oder in einem Instanzkonstruktor oder statischen Konstruktor in derselben Klasse auftreten. (In diesen Kontexten kann ein readonly-Feld mehrmals zugewiesen werden.) Insbesondere sind direkte Zuordnungen zu einem Readonly-Feld nur in den folgenden Kontexten zulässig:
- In dem variablen_deklarator , der das Feld einführt (indem ein variabler_initializer in die Deklaration aufgenommen wird).
- Bei einem Beispielfeld, in den Instanzenkonstruktoren der Klasse, die die Felddeklaration enthält, ausgenommen lokale Funktionen und anonyme Funktionen, und nur für die zu erstellende Instanz. Bei einem statischen Feld, im statischen Konstruktor oder in einem statischen Feld oder eigenschaftsinitializer in der Klasse, die die Felddeklaration enthält, ausgenommen lokale Funktionen und anonyme Funktionen. Dies sind auch die einzigen Kontexte, in denen es gültig ist, ein Readonly-Feld als Ausgabe- oder Referenzparameter zu übergeben.
Der Versuch, einem schreibgeschützten Feld zuzuweisen oder es in einem anderen Kontext als Ausgabe- oder Referenzparameter zu übergeben, ist ein Kompilierzeitfehler.
15.5.3.2 Verwenden statischer Readonly-Felder für Konstanten
Ein statisches Readonly-Feld ist nützlich, wenn ein symbolischer Name für einen Konstantenwert gewünscht wird, aber wenn der Typ des Werts in einer Konstdeklaration nicht zulässig ist oder wenn der Wert zur Kompilierung nicht berechnet werden kann.
Beispiel: Im folgenden Code
public class Color { public static readonly Color Black = new Color(0, 0, 0); public static readonly Color White = new Color(255, 255, 255); public static readonly Color Red = new Color(255, 0, 0); public static readonly Color Green = new Color(0, 255, 0); public static readonly Color Blue = new Color(0, 0, 255); private byte red, green, blue; public Color(byte r, byte g, byte b) { red = r; green = g; blue = b; } }die
Black,White,Red,GreenundBlueMember können nicht als Const-Member deklariert werden, da ihre Werte nicht zur Kompilierungszeit berechnet werden können. Das Deklarierenstatic readonlydieser Elemente hat jedoch nahezu die gleiche Wirkung.Endbeispiel
15.5.3.3 Versionsverwaltung von Konstanten und statischen Readonly-Feldern
Konstanten und schreibgeschützte Felder haben eine unterschiedliche Semantik der binären Versionierung. Wenn ein Ausdruck auf eine Konstante verweist, wird der Wert der Konstante zur Kompilierungszeit abgerufen, aber wenn ein Ausdruck auf ein readonly-Feld verweist, wird der Wert des Felds erst während der Laufzeit abgerufen.
Beispiel: Betrachten Sie eine Anwendung, die aus zwei separaten Programmen besteht:
namespace Program1 { public class Utils { public static readonly int x = 1; } }und
namespace Program2 { class Test { static void Main() { Console.WriteLine(Program1.Utils.X); } } }
Program1undProgram2Namespaces bezeichnen zwei Programme, die separat kompiliert werden. DaProgram1.Utils.Xals einstatic readonlyFeld deklariert ist, ist der durch dieConsole.WriteLineAnweisung ausgegebene Wert nicht zur Kompilierungszeit bekannt, sondern wird zur Laufzeit ermittelt. Wenn der Wert vonXgeändert wird undProgram1neu kompiliert wird, gibt dieConsole.WriteLine-Anweisung den neuen Wert aus, auch wennProgram2nicht neu kompiliert wird. Sollte es sich beiXjedoch um eine Konstante handeln, wäre der Wert vonXzum Zeitpunkt der Kompilierung vonProgram2erhalten worden und würde von Änderungen inProgram1unbeeinflusst bleiben, bisProgram2erneut kompiliert wird.Endbeispiel
15.5.4 Veränderliche Felder
Wenn eine field_declaration einen volatile Modifizierer enthält, sind die durch diese Deklaration eingeführten Felder volatile Felder. Für nicht veränderliche Felder können Optimierungstechniken, die Anweisungen neu anordnen, zu unerwarteten und unvorhersehbaren Ergebnissen in Multithread-Programmen führen, die ohne Synchronisierung auf Felder zugreifen, z. B. die von der lock_statement (§13.13). Diese Optimierungen können vom Compiler, vom Laufzeitsystem oder von Hardware ausgeführt werden. Für volatile Felder sind solche Neuanordnungsoptimierungen eingeschränkt.
- Ein Lesen eines veränderlichen Feldes wird als veränderliches Lesen bezeichnet. Ein flüchtiges Lesen hat eine „Akquisitions-Semantik“; das heißt, es tritt garantiert vor allen Verweisen auf den Speicher auf, die danach in der Befehlssequenz auftreten.
- Ein Schreibvorgang eines veränderlichen Felds wird als veränderlicher Schreibzugriff bezeichnet. Ein flüchtiger Schreibvorgang hat eine „Release-Semantik“, d. h. er findet garantiert nach allen Speicherverweisen vor dem Schreibbefehl in der Befehlssequenz statt.
Diese Einschränkungen stellen sicher, dass alle Threads volatile Schreiboperationen, die von einem anderen Thread ausgeführt werden, in der Reihenfolge, in der sie ausgeführt wurden, berücksichtigen. Eine konforme Implementierung ist nicht erforderlich, um eine einzige Gesamtreihenfolge von flüchtigen Schreibvorgängen bereitzustellen, wie sie von allen Ausführungsthreads aus zu sehen ist. Der Typ eines veränderlichen Felds soll einer der folgenden sein:
- Eine reference_type.
- Eine type_parameter , die als Bezugstyp bekannt ist (§15.2.5).
- Der Typ
byte,sbyte,short,ushort,int,uint,char,float,bool,System.IntPtroderSystem.UIntPtr. - Ein enum_type mit einem enum_base-Typ von
byte,sbyte,short,ushort,int, oderuint.
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 = 143In diesem Beispiel startet die Methode
Maineinen neuen Thread, der die MethodeThread2ausführt. Mit dieser Methode wird ein Wert in einem nicht flüchtigen Feld namensresultgespeichert und danntruein dem flüchtigen Feldfinishedgespeichert. Der Hauptthread wartet darauf, dass das Feldfinishedauftruegesetzt wird, und liest dann das Feldresult. Dafinisheddeklariertvolatilewurde, muss der Hauptthread den Wert143aus dem Feldresultlesen. Wenn das Feldfinishednicht deklariertvolatileworden wäre, dann wäre es zulässig, dass der Speicherresultfür den Hauptthread nach der Speicherung infinishedsichtbar ist, und somit könnte der Hauptthread den Wert 0 aus dem Feldresultlesen. Das DeklarierenfinishedalsvolatileFeld 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 = 0da
bundibeide automatisch in Standardwerte initialisiert werden.Endbeispiel
15.5.6 Variableninitialisierer
15.5.6.1 Allgemein
Felddeklarationen können variable_initializers enthalten. Bei statischen Feldern entsprechen Variableninitialisierer Zuordnungsanweisungen, die während der Klasseninitialisierung ausgeführt werden. Bei Instanzfeldern entsprechen die Variableninitialisierer den Zuordnungsanweisungen, die ausgeführt werden, wenn eine Instanz der Klasse erstellt wird.
Beispiel: Das Beispiel
class Test { static double x = Math.Sqrt(2.0); int i = 100; string s = "Hello"; static void Main() { Test a = new Test(); Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}"); } }erzeugt die Ausgabe
x = 1.4142135623730951, i = 100, s = Helloda eine Zuordnung zu
xerfolgt, wenn die statischen Feldinitialisierer ausgeführt werden, und die Zuordnungen zuiundserfolgen, wenn die Instanzfeldinitialisierer ausgeführt werden.Endbeispiel
Die in §15.5.5.5 beschriebene Standardwertinitialisierung erfolgt für alle Felder, einschließlich Feldern mit Variableninitialisierern. Wenn eine Klasse initialisiert wird, werden daher alle statischen Felder in dieser Klasse zuerst mit ihren Standardwerten initialisiert, und dann werden die statischen Feldinitialisierer in textualer Reihenfolge ausgeführt. Ebenso werden beim Erstellen einer Instanz einer Klasse zuerst alle Instanzfelder in dieser Instanz mit ihren Standardwerten initialisiert, und dann werden die Instanzfeldinitialisierer in textualer Reihenfolge ausgeführt. Wenn es Felddeklarationen in mehreren partiellen Typdeklarationen für denselben Typ gibt, ist die Reihenfolge der Teile nicht angegeben. Innerhalb jedes Teils werden die Feldinitialisierer jedoch in der Reihenfolge ausgeführt.
Es ist möglich, dass statische Felder mit variablen Initialisierern im Standardwertzustand beobachtet werden.
Beispiel: Dies wird jedoch dringend als Stilsache abgeraten. Das Beispiel
class Test { static int a = b + 1; static int b = a + 1; static void Main() { Console.WriteLine($"a = {a}, b = {b}"); } }zeigt dieses Verhalten. Trotz der Zirkeldefinitionen von
aundbist das Programm gültig. Es resultiert in der Ausgabea = 1, b = 2da die statischen Felder
aundbauf0(den Standardwert fürint) initialisiert werden, bevor ihre Initialisierer ausgeführt werden. Wenn der Initialisierer füraausgeführt wird, ist der Wert vonbnull, und deshalb wirdamit1initialisiert. Wenn der Initialisierer fürbläuft, ist der Wert von a bereits1und daher wirdbauf2initialisiert.Endbeispiel
15.5.6.2 Initialisierung statischer Felder
Die Initialisierer einer statischen Feldvariablen einer Klasse entsprechen einer Abfolge von Zuordnungen, die in der Textreihenfolge ausgeführt werden, in der sie in der Klassendeklaration angezeigt werden (§15.5.6.1). Innerhalb einer Teilklasse wird die Bedeutung von "Textreihenfolge" durch §15.5.6.1 angegeben. Wenn ein statischer Konstruktor (§15.12) in der Klasse vorhanden ist, erfolgt die Ausführung der statischen Feldinitialisierer unmittelbar vor der Ausführung dieses statischen Konstruktors. Andernfalls werden die statischen Feldinitialisierer zu einer implementierungsabhängigen Zeit vor der ersten Verwendung eines statischen Felds dieser Klasse ausgeführt.
Beispiel: Das Beispiel
class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { public static int X = Test.F("Init A"); } class B { public static int Y = Test.F("Init B"); }könnte entweder die Ausgabe erzeugen:
Init A Init B 1 1oder die Ausgabe:
Init B Init A 1 1da die Ausführung des Initialisierers von
Xund des Initialisierers vonYin beliebiger Reihenfolge erfolgen kann; sie müssen nur vor den Verweisen auf diese Felder stattfinden. Im Beispiel ist jedoch Folgendes zu beachten:class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { static A() {} public static int X = Test.F("Init A"); } class B { static B() {} public static int Y = Test.F("Init B"); }die Ausgabe muss sein:
Init B Init A 1 1da die Regeln für die Ausführung statischer Konstruktoren (wie in §15.12 definiert) angeben, dass
Bder statische Konstruktor (und damitBauch statische Feldinitialisierer) vorAden statischen Konstruktoren und Feldinitialisierern ausgeführt werden muss.Endbeispiel
15.5.6.3 Instanzfeldinitialisierung
Die Instanzfeldvariableninitialisierer einer Klasse entsprechen einer Abfolge von Zuordnungen, die unmittelbar nach dem Eintrag zu einem der Instanzkonstruktoren (§15.11.3) dieser Klasse ausgeführt werden. Innerhalb einer Teilklasse wird die Bedeutung von "Textreihenfolge" durch §15.5.6.1 angegeben. Die Variableninitialisierer werden in der Textreihenfolge ausgeführt, in der sie in der Klassendeklaration (§15.5.6.1) angezeigt werden. Der Erstellungs- und Initialisierungsprozess der Klasseninstanz wird in §15.11 weiter beschrieben.
Ein variabler Initialisierer für ein Instanzfeld kann nicht auf die erstellte Instanz verweisen. Daher ist es ein Kompilierungszeitfehler, auf this in einem Variableninitialisierer zu verweisen, da es ein Kompilierungszeitfehler ist, wenn ein Variableninitialisierer über einen simple_name auf ein Instanzmitglied verweist.
Beispiel: Im folgenden Code
class A { int x = 1; int y = x + 1; // Error, reference to instance member of this }der Variableninitialisierer für
yführt zu einem Kompilierfehler, da er auf ein Mitglied der zu erstellenden Instanz verweist.Endbeispiel
15.6 Methoden
15.6.1 Allgemein
§15.6 und seine Unterclauses decken Methodendeklarationen in Klassen ab. Dieser Text wird durch Informationen über die Deklarierung von Methoden in Denstrukten (§16.4) und Schnittstellen (§19.4.3) ergänzt.
Eine Methode ist ein Member, das eine Berechnung oder eine Aktion implementiert, die durch ein Objekt oder eine Klasse durchgeführt werden kann. Methoden werden mit method_declaration sdeklariert:
method_declaration
: attributes? method_modifiers return_type method_header method_body
| attributes? ref_method_modifiers ref_kind ref_return_type method_header
ref_method_body
;
method_modifiers
: method_modifier* 'partial'?
;
ref_kind
: 'ref'
| 'ref' 'readonly'
;
ref_method_modifiers
: ref_method_modifier*
;
method_header
: member_name '(' parameter_list? ')'
| member_name type_parameter_list '(' parameter_list? ')'
type_parameter_constraints_clause*
;
method_modifier
: ref_method_modifier
| 'async'
;
ref_method_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
return_type
: ref_return_type
| 'void'
;
ref_return_type
: type
;
member_name
: identifier
| interface_type '.' identifier
;
method_body
: block
| '=>' null_conditional_invocation_expression ';'
| '=>' expression ';'
| ';'
;
ref_method_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
Grammatiknotizen:
- unsafe_modifier (§24.2) ist nur im unsicheren Code (§24) verfügbar.
- wenn bei der Erkennung eines method_body sowohl die Alternative null_conditional_invocation_expression als auch die Alternative expression anwendbar sind, wird die erstere gewählt.
Hinweis: Die Überschneidung von Alternativen und Priorität zwischen diesen Alternativen dient ausschließlich der beschreibenden Einfachheit. Die Grammatikregeln könnten ausgearbeitet werden, um die Überschneidung zu entfernen. ANTLR und andere Grammatiksysteme übernehmen den gleichen Komfort und method_body weist die angegebene Semantik automatisch auf. Hinweisende
Ein method_declaration kann eine Reihe von Attributen (§23) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), (new§15.3.5), static (§15.6.3), (§15.6.3), virtual (§1 5.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7), extern (§15.6.8) und async (§15.14). Zusätzlich kann eine method_declaration, die direkt in einem struct_declaration enthalten ist, den readonly-Modifizierer (§16.4.12) haben.
Eine Deklaration verfügt über eine gültige Kombination aus Modifizierern, wenn alle folgenden Bedingungen erfüllt sind:
- Die Deklaration enthält eine gültige Kombination aus Zugriffsmodifizierern (§15.3.6).
- Die Deklaration enthält nicht mehrmals denselben Modifizierer.
- Die Deklaration enthält höchstens einen der folgenden Modifizierer:
static, ,virtualundoverride. - Die Deklaration enthält höchstens einen der folgenden Modifizierer:
newundoverride. - Wenn die Erklärung den Modifikator
abstractenthält, dann enthält die Erklärung keinen der folgenden Modifikatoren:static,virtual,sealedoderextern. - Die Deklaration kann die
abstractUnd-Modifiziereroverrideenthalten, sodass ein abstraktes Element ein virtuelles Element außer Kraft setzen kann. - Wenn die Deklaration den
privateModifizierer enthält, enthält die Deklaration keine der folgenden Modifizierer:virtual, ,overrideoderabstract. - Wenn die Deklaration den
sealedModifizierer enthält, enthält die Deklaration auch denoverrideModifizierer. - Wenn die Deklaration den
partialModifizierer enthält, enthält sie keine der folgenden Modifizierer:new,public,protected,internal,private,virtual,sealed,override,abstractoderextern.
Methoden werden nach dem klassifiziert, was, wenn etwas, sie zurückgeben:
- Wenn
refvorhanden ist, ist die Methode Returns-by-ref und gibt eine Variablenreferenzzurück, die optional schreibgeschützt ist; - Andernfalls, wenn return_type
voidist, ist die Methode wertlos und gibt keinen Wert zurück. - Andernfalls ist die Methode Returns-by-value und gibt einen Wert zurück.
Der Return-Typ einer Return-by-Value- oder Returns-no-Value-Methoden-Deklaration gibt den Typ des Ergebnisses an, das die Methode gegebenenfalls zurückgibt. Nur eine Methode, die keinen Wert zurückgibt, darf den Modifikator partial enthalten (§15.6.9). Wenn die Deklaration den async Modifizierer enthält, muss return_type sein void oder die Methode gibt nach Wert zurück, und der Rückgabetyp ist ein Vorgangstyp (§15.14.1).
Der ref_return_type einer returns-by-ref-Methodendeklaration gibt den Typ der Variablen an, auf die die variable_reference der Methode verweist.
Eine generische Methode ist eine Methode, deren Deklaration eine type_parameter_list enthält. Dadurch werden die Typparameter für die Methode angegeben. Die optionalen type_parameter_constraints_clauses geben die Einschränkungen für die Typparameter an.
Eine generische Methodendeklaration, entweder mit einem override-Modifikator oder für eine explizite Implementierung eines Interface Members, erbt die Typparameter-Beschränkungen von der überschriebenen Methode bzw. dem Interface Member. Solche Erklärungen dürfen nur type_parameter_constraints_clause haben,die die primary_constraintclass s und struct, deren Bedeutung in diesem Kontext in §15.6.5 und §19.6.2 für überschriebene Methoden bzw. explizite Schnittstellenimplementierungen definiert ist.
Die member_name gibt den Namen der Methode an. Es sei denn, die Methode ist eine explizite Schnittstellenmemmimplementierung (§19.6.2), ist die member_name einfach ein Bezeichner.
Bei einer expliziten Implementierung eines Schnittstellenmitglieds besteht der Mitgliedsname aus einem Schnittstellentyp gefolgt von einem "." und einem Bezeichner. In diesem Fall enthält die Erklärung keine anderen Modifizierer als (möglicherweise) extern oder async.
Die optionale parameter_list gibt die Parameter der Methode an (§15.6.2).
Der return_type oder ref_return_type und alle Typen, auf die in der parameter_list einer Methode verwiesen wird, müssen mindestens so zugänglich sein wie die Methode selbst (§7.5.5).
Der Methodenkörper einer Return-by-Value oder Returns-no-Value Methode ist entweder ein Semikolon, ein Blockkörper oder ein Ausdruckskörper. Ein Blocktext besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn die Methode aufgerufen wird. Ein Ausdruckskörper besteht aus =>, gefolgt von einem null_conditional_invocation_expression oder expressionund einem Semikolon und bezeichnet einen einzelnen Ausdruck, der ausgeführt werden soll, wenn die Methode aufgerufen wird.
Bei abstrakten und externen Methoden besteht die method_body einfach aus einem Semikolon. Bei partiellen Methoden kann der Methodenkörper entweder aus einem Semikolon, einem Blockkörper oder einem Ausdruckskörper bestehen. Bei allen anderen Methoden ist der method_body entweder ein Blocktext oder ein Ausdruckstext.
Besteht die method_body aus einem Semikolon, enthält die Deklaration nicht den async Modifizierer.
Der ref_method_body einer returns-by-ref Methode ist entweder ein Semikolon, ein Blockkörper oder ein Ausdruckskörper. Ein Blocktext besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn die Methode aufgerufen wird. Ein Ausdruckskörper besteht aus =>, gefolgt von ref, einer Variablenreferenzund einem Semikolon und bezeichnet eine einzelne Variablenreferenz , die ausgewertet wird, wenn die Methode aufgerufen wird.
Bei abstrakten und externen Methoden besteht der ref_method_body einfach aus einem Semikolon; bei allen anderen Methoden ist der ref_method_body entweder ein Blockkörper oder ein Ausdruckskörper.
Der Name, die Anzahl der Typparameter und die Parameterliste einer Methode definieren die Signatur (§7.6) der Methode. Genauer gesagt besteht die Signatur einer Methode aus ihrem Namen, der Anzahl ihrer Typ-Parameter und der Anzahl, parameter_mode_modifiers (§15.6.2.1) und Typen ihrer Parameter. Der Rückgabetyp ist weder Teil der Signatur einer Methode noch die Namen der Parameter, die Namen der Typparameter oder die Einschränkungen. Wenn ein Parametertyp auf einen Typparameter der Methode verweist, wird die Ordnungsposition des Typparameters (nicht der Name des Typparameters) für die Äquivalenz des Typs verwendet.
Der Name einer Methode unterscheidet sich von den Namen aller anderen Nichtmethoden, die in derselben Klasse deklariert sind. Darüber hinaus unterscheidet sich die Signatur einer Methode von den Signaturen aller anderen Methoden, die in derselben Klasse deklariert sind, und zwei Methoden, die in derselben Klasse deklariert sind, dürfen keine Signaturen haben, die sich ausschließlich von in, outund .ref
Die type_parameter der Methode sind im gesamten method_declaration im Gültigkeitsbereich und können verwendet werden, um Typen in diesem Gültigkeitsbereich in return_type oder ref_return_type, method_body oder ref_method_body und type_parameter_constraints_clause zu bilden, aber nicht in Attributen.
Alle Parameter und Typparameter müssen unterschiedliche Namen haben.
15.6.2 Methodenparameter
15.6.2.1 Allgemein
Die Parameter einer Methode werden ggf. durch die parameter_list der Methode deklariert.
parameter_list
: fixed_parameters
| fixed_parameters ',' parameter_array
| parameter_array
;
fixed_parameters
: fixed_parameter (',' fixed_parameter)*
;
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
default_argument
: '=' expression
;
parameter_modifier
: parameter_mode_modifier
| 'this' parameter_mode_modifier?
| 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 (§23); optionaler in, , out, refoder this Modifizierer; ein Typ; ein Bezeichner; und ein optionaler default_argument. Jede fixed_parameter deklariert einen Parameter des angegebenen Typs mit dem angegebenen Namen. Der this Modifizierer bezeichnet die Methode als Erweiterungsmethode und ist nur für den ersten Parameter einer statischen Methode in einer nicht generischen, nicht geschachtelten statischen Klasse zulässig. Wenn es sich bei dem Parameter um einen struct Typ oder einen Typparameter handelt, der auf einen structParameter beschränkt ist, kann der this Modifizierer entweder mit dem ref Modifizierer oder in dem Modifizierer kombiniert werden, jedoch nicht mit dem out Modifizierer. Erweiterungsmethoden werden weiter in §15.6.10 beschrieben. Ein fixed_parameter mit einem default_argument wird als optionaler Parameter bezeichnet, während ein fixed_parameter ohne default_argument ein erforderlicher Parameter ist. Ein erforderlicher Parameter wird nach einem optionalen Parameter in einem parameter_list nicht angezeigt.
Ein Parameter mit einem ref, out oder this Modifizierer kann keine default_argument haben. Ein Eingabeparameter kann ein Standardargument haben. Der Ausdruck in einem Standardargument muss einer der folgenden sein:
- ein Konstantausdruck
- ein Ausdruck der Form
new S(), bei demSein Werttyp ist - ein Ausdruck der Form
default(S), bei demSein Werttyp ist
Der Ausdruck muss implizit durch eine Identitäts- oder nullbare Konvertierung in den Typ des Parameters konvertierbar sein.
Wenn optionale Parameter in einer implementierenden partiellen Methodendeklaration (§15.6.9) auftreten, sollte eine explizite Schnittstellenmemberimplementierung (§19.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 (§23), einem Modifizierer, einem paramsarray_type und einem Bezeichner. Ein Parameterarray deklariert einen einzelnen Parameter des angegebenen Arraytyps mit dem angegebenen Namen. Die array_type eines Parameterarrays muss ein eindimensionales Array vom Typ (§17.2) sein. Bei einem Methodenaufruf erlaubt ein Parameterarray entweder die Angabe eines einzelnen Arguments des angegebenen Arraytyps oder die Angabe von null oder mehr Argumenten des Typs der Arrayelemente. Parameterarrays werden weiter in §15.6.2.4 beschrieben.
Ein parameter_array kann nach einem optionalen Parameter auftreten, aber keinen Standardwert aufweisen – das Auslassen von Argumenten für eine parameter_array würde stattdessen zur Erstellung eines leeren Arrays führen.
Beispiel: Im Folgenden werden verschiedene Arten von Parametern veranschaulicht:
void M<T>( ref int i, decimal d, bool b = false, bool? n = false, string s = "Hello", object o = null, T t = default(T), params int[] a ) { }In der parameter_list für
M,iist ein erforderlicherrefParameter,dist ein erforderlicher Wertparameter,b,s,oundtsind optionale Wertparameter undaist ein Parameterarray.Endbeispiel
Eine Methodendeklaration erstellt einen separaten Deklarationsraum (§7.3) für Parameter und Typparameter. Namen werden in diesen Deklarationsbereich durch die Typparameterliste und die Parameterliste der Methode eingeführt. Der Text der Methode, falls vorhanden, gilt als innerhalb dieses Deklarationsraums verschachtelt. Es ist ein Fehler, wenn zwei Member eines Methodendeklarationsraums den gleichen Namen haben.
Ein Methodenaufruf (§12.8.10.2) erstellt eine Kopie, spezifisch für diesen Aufruf, die Parameter und lokale Variablen der Methode, und die Argumentliste des Aufrufs weist den neu erstellten Parametern Werte oder Variablenverweise zu. Innerhalb des Blocks einer Methode können Parameter anhand ihrer Bezeichner in simple_name Ausdrücken (§12.8.4) referenziert werden.
Die folgenden Parametertypen sind vorhanden:
- Wertparameter (§15.6.2.2).
- Eingabeparameter (§15.6.2.3.2).
- Ausgabeparameter (§15.6.2.3.4).
- Referenzparameter (§15.6.2.3.3).
- Parameter-Arrays (§15.6.2.4).
Hinweis: Wie in §7.6 beschrieben, sind die
in,outundref-Modifikatoren Teil der Signatur einer Methode, aber derparams-Modifikator ist es nicht. Hinweisende
15.6.2.2 Wertparameter
Ein Parameter, der ohne Modifizierer deklariert ist, ist ein Wertparameter. Ein Wertparameter ist eine lokale Variable, die ihren Anfangswert aus dem entsprechenden Argument abruft, das im Methodenaufruf angegeben wird.
Bestimmte Zuordnungsregeln finden Sie unter §9.2.5.
Das entsprechende Argument in einem Methodenaufruf muss ein Ausdruck sein, der implizit in den Parametertyp (§10.2) konvertierbar ist.
Eine Methode darf einem Wertparameter neue Werte zuweisen. Solche Zuordnungen wirken sich nur auf den lokalen Speicherort aus, der durch den Wertparameter dargestellt wird. Sie haben keine Auswirkungen auf das tatsächliche Argument, das im Methodenaufruf angegeben wird.
15.6.2.3 By-Verweisparameter
15.6.2.3.1 Allgemein
Eingabe-, Ausgabe- und Referenzparameter sind durch-Referenz-Parameters. Ein Nachverweisparameter ist eine lokale Referenzvariable (§9.7). Der anfängliche Referent wird aus dem entsprechenden Argument abgerufen, das im Aufruf der Methode angegeben wird.
Hinweis: Der Referent eines By-Reference-Parameters kann mit dem Verweiszuweisungsoperator (
= ref) geändert werden.
Wenn ein Parameter ein By-Reference-Parameter ist, besteht das entsprechende Argument in einem Methodenaufruf aus dem entsprechenden Schlüsselwort, in, , refoder out, gefolgt von einem variable_reference (§9.5) desselben Typs wie der Parameter. Wenn der Parameter jedoch ein in Parameter ist, kann es sich bei dem Argument um einen Ausdruck handeln, für den eine implizite Konvertierung (§10.2) von diesem Argumentausdruck in den Typ des entsprechenden Parameters vorhanden ist.
By-Reference-Parameter sind für Als Iterator (§15.15) oder asynchrone Funktion (§15.14) deklarierte Funktionen nicht zulässig.
In einer Methode, die mehrere Nachverweisparameter verwendet, ist es möglich, dass mehrere Namen denselben Speicherort darstellen.
15.6.2.3.2 Eingabeparameter
Ein mit einem Modifizierer deklarierter in Parameter ist ein Eingabeparameter. Das Argument, das einem Eingabeparameter entspricht, ist entweder eine Variable, die am Punkt des Methodenaufrufs vorhanden ist, oder eine variable, die durch die Implementierung (§12.6.2.3) im Aufruf der Methode erstellt wurde. Bestimmte Zuordnungsregeln finden Sie unter §9.2.8.
Es handelt sich um einen Kompilierungszeitfehler, um den Wert eines Eingabeparameters zu ändern.
Hinweis: Der Hauptzweck von Eingabeparametern ist für die Effizienz. Wenn der Typ eines Methodenparameters eine große Struktur ist (in Bezug auf den Speicherbedarf), ist es nützlich, beim Aufrufen der Methode das Kopieren des gesamten Wertes des Arguments vermeiden zu können. Eingabeparameter ermöglichen es Methoden, auf vorhandene Werte im Arbeitsspeicher zu verweisen, während gleichzeitig Schutz vor unerwünschten Änderungen an diesen Werten bereitgestellt wird. Hinweisende
15.6.2.3.3 Referenzparameter
Ein parameter, der mit einem ref Modifizierer deklariert ist, ist ein Verweisparameter. Bestimmte Zuordnungsregeln finden Sie unter §9.2.6.
Beispiel: Das Beispiel
class Test { static void Swap(ref int x, ref int y) { int temp = x; x = y; y = temp; } static void Main() { int i = 1, j = 2; Swap(ref i, ref j); Console.WriteLine($"i = {i}, j = {j}"); } }erzeugt die Ausgabe
i = 2, j = 1Für den Aufruf von
SwapinMainstehtxfüriundysteht fürj. Daher hat der Aufruf die Auswirkung, die Werte voniundjzu vertauschen.Endbeispiel
Beispiel: Im folgenden Code
class A { string s; void F(ref string a, ref string b) { s = "One"; a = "Two"; b = "Three"; } void G() { F(ref s, ref s); } }Der Aufruf von
FinGübergibt eine Referenz ansfür sowohlaals auchb. Daher beziehen sich für diesen Aufruf die Namens,aundballe auf denselben Speicherort, und die drei Zuordnungen ändern alle das Instanzfelds.Endbeispiel
Bei einem struct Typ verhält sich das Schlüsselwort innerhalb einer Instanzmethode, des Instanzaccessors (this) oder des Instanzkonstruktors mit einem Konstruktorinitialisierer genau als Referenzparameter des Strukturtyps (§12.8.14).
15.6.2.3.4 Ausgabeparameter
Ein parameter, der mit einem out Modifizierer deklariert ist, ist ein Ausgabeparameter. Bestimmte Zuordnungsregeln finden Sie unter §9.2.7.
Eine als Teilmethode deklarierte Methode (§15.6.9) darf keine Ausgabeparameter aufweisen.
Hinweis: Ausgabeparameter werden in der Regel in Methoden verwendet, die mehrere Rückgabewerte erzeugen. Hinweisende
Beispiel:
class Test { static void SplitPath(string path, out string dir, out string name) { int i = path.Length; while (i > 0) { char ch = path[i - 1]; if (ch == '\\' || ch == '/' || ch == ':') { break; } i--; } dir = path.Substring(0, i); name = path.Substring(i); } static void Main() { string dir, name; SplitPath(@"c:\Windows\System\hello.txt", out dir, out name); Console.WriteLine(dir); Console.WriteLine(name); } }Das Beispiel ergibt die Ausgabe:
c:\Windows\System\ hello.txtBeachten Sie, dass die Variablen
dirundnamemöglicherweise nicht zugewiesen sind, bevor sie anSplitPathübergeben werden, und dass sie nach dem Aufruf als definitiv zugewiesen betrachtet werden.Endbeispiel
15.6.2.4 Parameter-Arrays
Ein Parameter, der mit einem params Modifizierer deklariert ist, ist ein Parameterarray. Wenn eine Parameterliste ein Parameterarray enthält, muss es der letzte Parameter in der Liste sein und es muss sich um einen eindimensionalen Arraytyp handeln.
Beispiel: Die Typen
string[]undstring[][]können als Typ eines Parameterarrays verwendet werden, der Typstring[,]kann jedoch nicht verwendet werden. Endbeispiel
Hinweis: Es ist nicht möglich, den
paramsModifizierer mit den Modifizierernin,outoderrefzu kombinieren. Hinweisende
Ein Parameterarray ermöglicht die Angabe von Argumenten auf eine von zwei Arten in einem Methodenaufruf:
- Das argument für ein Parameterarray kann ein einzelner Ausdruck sein, der implizit in den Parameterarraytyp (§10.2) konvertierbar ist. In diesem Fall fungiert das Parameterarray genau wie ein Wertparameter.
- Alternativ kann der Aufruf null oder mehr Argumente für das Parameterarray angeben, wobei jedes Argument ein Ausdruck ist, der implizit (§10.2) in den Elementtyp des Parameterarrays umgewandelt wird. In diesem Fall erstellt der Aufruf eine Instanz des Parameterarraytyps mit einer Länge, die der Anzahl der Argumente entspricht, initialisiert die Elemente der Arrayinstanz mit den angegebenen Argumentwerten und verwendet die neu erstellte Arrayinstanz als tatsächliches Argument.
Mit Ausnahme einer variablen Anzahl von Argumenten in einem Aufruf entspricht ein Parameterarray genau einem Wertparameter (§15.6.2.2.2) desselben Typs.
Beispiel: Das Beispiel
class Test { static void F(params int[] args) { Console.Write($"Array contains {args.Length} elements:"); foreach (int i in args) { Console.Write($" {i}"); } Console.WriteLine(); } static void Main() { int[] arr = {1, 2, 3}; F(arr); F(10, 20, 30, 40); F(); } }erzeugt die Ausgabe
Array contains 3 elements: 1 2 3 Array contains 4 elements: 10 20 30 40 Array contains 0 elements:Der erste Aufruf des
FArrays übergibt das Arrayarreinfach als Wertparameter. Der zweite Aufruf von F erstellt automatisch ein Vier-Elementint[]mit den angegebenen Elementwerten und übergibt diese Arrayinstanz als Wertparameter. Ebenso erstellt der dritte Aufruf vonFein Nullelementint[]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 FalseDer zweite Aufruf erzeugt
False, da er entsprichtF(new string[] { null })und übergibt ein Array, das einen einzelnen NULL-Verweis enthält.Endbeispiel
Wenn der Typ eines Parameterarrays lautet object[], entsteht eine potenzielle Mehrdeutigkeit zwischen der Normalenform der Methode und der erweiterten Form für einen einzelnen object Parameter. Der Grund für die Mehrdeutigkeit ist, dass ein object[] selbst in den Typ object implizit konvertierbar ist. Die Mehrdeutigkeit stellt jedoch kein Problem dar, da sie bei Bedarf durch Einfügen eines Gipsverbandes behoben werden kann.
Beispiel: Das Beispiel
class Test { static void F(params object[] args) { foreach (object o in args) { Console.Write(o.GetType().FullName); Console.Write(" "); } Console.WriteLine(); } static void Main() { object[] a = {1, "Hello", 123.456}; object o = a; F(a); F((object)a); F(o); F((object[])o); } }erzeugt die Ausgabe
System.Int32 System.String System.Double System.Object[] System.Object[] System.Int32 System.String System.DoubleIn den ersten und letzten Aufrufen von
Fist die normale Form anwendbarF, da eine implizite Konvertierung vom Argumenttyp in den Parametertyp vorhanden ist (beide sind vom Typobject[]). Daher wählt die Überladungsauflösung die normale Form vonF, und das Argument wird als normaler Wertparameter übergeben. In den zweiten und dritten Aufrufen ist die normale Form nichtFanwendbar, da keine implizite Konvertierung vom Argumenttyp in den Parametertyp vorhanden ist (Typobjectkann nicht implizit in Typobject[]konvertiert werden). Allerdings ist die erweiterte Form vonFanwendbar, so dass sie durch Überlastungsauflösung ausgewählt wird. Daher wird durch den Aufruf ein einzelnes Elementobject[]erstellt, und dieses eine Element des Arrays wird mit dem angegebenen Argumentwert initialisiert (der selbst ein Verweis auf einenobject[]ist).Endbeispiel
15.6.3 Statische Methoden und Instanzmethoden
Wenn eine Methodendeklaration einen static Modifizierer enthält, wird diese Methode als statische Methode bezeichnet. Wenn kein static Modifizierer vorhanden ist, wird die Methode als Instanzmethode bezeichnet.
Eine statische Methode operiert nicht auf einer bestimmten Instanz, und es ist ein Kompilierfehler, sich in einer statischen Methode auf this zu beziehen.
Eine Instanzmethode wird für eine bestimmte Instanz einer Klasse ausgeführt, und auf diese Instanz kann zugegriffen werden (this§12.8.14).
Die Unterschiede zwischen statischen und Instanzmitgliedern werden in §15.3.8 weiter erörtert.
15.6.4 Virtuelle Methoden
Wenn eine Instanzmethodendeklaration einen virtuellen Modifizierer enthält, wird diese Methode als virtuelle Methode bezeichnet. Wenn kein virtueller Modifizierer vorhanden ist, wird die Methode als nicht virtuelle Methode bezeichnet.
Die Implementierung einer nicht virtuellen Methode ist invariant: Die Implementierung ist identisch, ob die Methode für eine Instanz der Klasse aufgerufen wird, in der sie deklariert wird, oder eine Instanz einer abgeleiteten Klasse. Im Gegensatz dazu kann die Implementierung einer virtuellen Methode durch abgeleitete Klassen ersetzt werden. Der Prozess der Außerkraftsetzung der Implementierung einer geerbten virtuellen Methode wird als Überschreibung dieser Methode (§15.6.5) bezeichnet.
Bei einem aufruf der virtuellen Methode bestimmt der Laufzeittyp der Instanz, für die dieser Aufruf stattfindet, die tatsächliche Methodenimplementierung, die aufgerufen werden soll. Bei einem Aufruf einer nicht virtuellen Methode ist der Kompilierungs-Zeittyp der Instanz der bestimmende Faktor. Wenn eine Methode namens N mit einer Argumentliste A auf einer Instanz mit einem Kompilierungszeittyp C und einem Laufzeittyp R aufgerufen wird (wobei R entweder C oder eine Klasse, die von C abgeleitet ist), wird der Aufruf wie folgt verarbeitet:
- Zur Bindezeit wird die Überlastauflösung auf
C,NundAangewendet, um eine bestimmte MethodeMaus der Menge der in deklarierten und vonCgeerbten Methoden auszuwählen. Dies wird in §12.8.10.2 beschrieben. - Dann zur Laufzeit:
- Wenn
Meine nicht-virtuelle Methode ist, wirdMaufgerufen. -
MAndernfalls handelt es sich um eine virtuelle Methode, und die meistabgeleitete Implementierung vonMim Hinblick aufRwird aufgerufen.
- Wenn
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
Rdie einführende virtuelle Deklaration vonMenthält, ist dies die am meisten abgeleitete Implementierung vonMin Bezug aufR. - Andernfalls, wenn
Reine Außerkraftsetzung vonMenthält, ist dies die am weitesten abgeleitete Implementierung vonMin Bezug aufR. - Andernfalls entspricht die am stärksten abgeleitete Implementierung von
MhinsichtlichRder am stärksten abgeleiteten Implementierung vonMbezüglich der direkten Basisklasse vonR.
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
Awird eine nicht virtuelle MethodeFund eine virtuelle MethodeGeingeführt. Die KlasseBführt eine neue nicht virtuelle MethodeFein, wodurch die geerbte MethodeFwird, und überschreibt außerdem die geerbte Methode . Das Beispiel ergibt die Ausgabe:A.F B.F B.G B.GBeachten Sie, dass die Anweisung
a.G()B.Gund nichtA.Gaufruft. Dies liegt daran, dass der Laufzeittyp der Instanz (dies istB), nicht der Kompilierungszeittyp der Instanz (was heißtA), die tatsächliche Methodenimplementierung bestimmt, die aufgerufen werden soll.Endbeispiel
Da Methoden geerbte Methoden ausblenden dürfen, ist es möglich, dass eine Klasse mehrere virtuelle Methoden mit derselben Signatur enthält. Dies stellt kein Mehrdeutigkeitsproblem dar, da alle Methoden bis auf die am weitesten entwickelte verborgen sind.
Beispiel: Im folgenden Code
class A { public virtual void F() => Console.WriteLine("A.F"); } class B : A { public override void F() => Console.WriteLine("B.F"); } class C : B { public new virtual void F() => Console.WriteLine("C.F"); } class D : C { public override void F() => Console.WriteLine("D.F"); } class Test { static void Main() { D d = new D(); A a = d; B b = d; C c = d; a.F(); b.F(); c.F(); d.F(); } }Die
C- undD-Klassen enthalten zwei virtuelle Methoden mit der gleichen Signatur: Die eine eingeführt durchAund die andere eingeführt durchC. Die vonCeingeführte Methode versteckt die vonAgeerbte Methode. So überschreibt die Override-Deklaration inDdie vonCeingeführte Methode, und es ist nicht möglich, dassDdie vonAeingeführte Methode überschreibt. Das Beispiel ergibt die Ausgabe:B.F B.F D.F D.FBeachten Sie, dass es möglich ist, die ausgeblendete virtuelle Methode aufzurufen, indem auf eine Instanz eines
Dweniger abgeleiteten Typs zugegriffen wird, in dem die Methode nicht ausgeblendet ist.Endbeispiel
15.6.5 Außerkraftsetzungsmethoden
Wenn eine Instanzmethodendeklaration einen override Modifizierer enthält, wird die Methode als Außerkraftsetzungsmethode bezeichnet. Eine Außerkraftsetzungsmethode setzt eine geerbte virtuelle Methode mit derselben Signatur außer Kraft. Während eine virtuelle Methodendeklaration eine neue Methode einführt , ist eine Überschreibungsmethodedeklaration auf eine vorhandene geerbte virtuelle Methode spezialisiert , indem eine neue Implementierung dieser Methode bereitgestellt wird.
Die Methode, die durch eine Override-Deklaration überschrieben wird, wird als überschriebene Basismethode bezeichnet. Für eine Override-Methode M , die in einer Klasse Cdeklariert ist, wird die überschriebene Basismethode bestimmt, indem jede Basisklasse von Cuntersucht wird, beginnend mit der direkten Basisklasse von C und fortfahrend mit jeder folgenden direkten Basisklasse, bis in einem gegebenen Basisklassentyp mindestens eine zugängliche Methode gefunden wird, die die gleiche Signatur wie M nach Ersetzung der Typargumente hat. Zum Auffinden der überschriebenen Basismethode wird eine Methode als zugänglich betrachtet, wenn sie publicist, protectedist, protected internalist, oder sie entweder internal oder private protectedist und im selben Programm deklariert ist wie C.
Die überschreibende Methode erbt alle type_parameter_constraints_clauses der überschriebenen Basismethode.
Ein Kompilierzeitfehler tritt auf, es sei denn, alle der folgenden Punkte gelten für eine Außerkraftsetzungsdeklaration:
- Eine überschriebene Basismethode kann wie oben beschrieben lokalisiert werden.
- Es gibt genau eine solche überschriebene Basismethode. Diese Einschränkung ist nur wirksam, wenn der Basisklassentyp ein konstruierter Typ ist, bei dem die Ersetzung von Typargumenten die Signatur von zwei Methoden gleich macht.
- Die Außerkraftsetzungsbasismethode ist eine virtuelle, abstrakte oder Außerkraftsetzungsmethode. Mit anderen Worten, die überschriebene Basismethode kann nicht statisch oder nicht virtuell sein.
- Die Außerkraftsetzungsbasismethode ist keine versiegelte Methode.
- Es gibt eine Identitätsumwandlung zwischen dem Rückgabetyp der außer Kraft gesetzten Basismethode und der Außerkraftsetzungsmethode.
- Die Außerkraftsetzungsdeklaration und die Außerkraftsetzungsbasismethode haben die gleiche deklarierte Zugänglichkeit. Anders ausgedrückt: Eine Überschreibdeklaration kann die Zugänglichkeit der virtuellen Methode nicht ändern. Wenn die Außerkraftsetzungsbasismethode jedoch intern geschützt ist und in einer anderen Assembly als der Assembly, die die Außerkraftsetzungsdeklaration enthält, deklariert ist, ist die deklarierte Zugänglichkeit der Außerkraftsetzungsdeklaration geschützt.
- Eine type_parameter_constraints_clause darf nur aus
classoderstructprimary_constraint bestehen, die auf type_parameter angewandt werden, von denen gemäß den geerbten Constraints bekannt ist, dass sie entweder Referenz- oder Werttypen sind. Jeder Typ des FormularsT?in der Signatur der überschreibenden Methode, wobeiTes sich um einen Typparameter handelt, wird wie folgt interpretiert:- Wenn eine
classEinschränkung für den TypparameterThinzugefügt wird,T?ist ein nullabler Verweistyp; andernfalls - Wenn entweder keine zusätzliche Einschränkung vorhanden ist oder eine
structEinschränkung hinzugefügt wird, ist für den TypparameterTeinT?Nullwerttyp vorhanden.
- Wenn eine
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
Beispiel: Im folgenden Beispiel wird veranschaulicht, wie die Außerkraftsetzungsregeln funktionieren, wenn Typparameter beteiligt sind:
#nullable enable class A { public virtual void Foo<T>(T? value) where T : class { } public virtual void Foo<T>(T? value) where T : struct { } } class B: A { public override void Foo<T>(T? value) where T : class { } public override void Foo<T>(T? value) where T : struct { } }Ohne die Typparameter-Beschränkung
where T : classkann die Basismethode mit dem referenztypisierten Typparameter nicht außer Kraft gesetzt werden. Endbeispiel
Eine Überschreibungsdeklaration kann auf die überschriebene Basismethode mit einem Basiszugriff zugreifen (§12.8.15).
Beispiel: Im folgenden Code
class A { int x; public virtual void PrintFields() => Console.WriteLine($"x = {x}"); } class B : A { int y; public override void PrintFields() { base.PrintFields(); Console.WriteLine($"y = {y}"); } }Der Aufruf von
base.PrintFields()ruft inBdie inAdeklarierte PrintFields-Methode auf. Ein base_access deaktiviert den virtuellen Aufrufmechanismus und erachtet die Basismethode als nicht-virtuelle Methodevirtual. Wäre der Aufruf inBals((A)this).PrintFields()geschrieben, würde er die inPrintFieldsdeklarierte Methode rekursiv aufrufen und nicht die inB, daAvirtuell ist und der Laufzeittyp vonPrintFields((A)this)istB.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 inBenthält keinenoverride-Modifier und überschreibt dieF-Methode inAdaher nicht. Vielmehr verbirgt die MethodeFinBdie Methode inA, und es wird eine Warnung ausgegeben, weil die Deklaration keinen neuen Modifikator enthält.Endbeispiel
Beispiel: Im folgenden Code
class A { public virtual void F() {} } class B : A { private new void F() {} // Hides A.F within body of B } class C : B { public override void F() {} // Ok, overrides A.F }die Methode
FinBversteckt die virtuelle MethodeF, die vonAgeerbt wurde. Da das neueFinBprivaten Zugriff hat, umfasst sein Geltungsbereich nur den Klassenkörper vonBund erstreckt sich nicht aufC. Daher darf die Deklaration vonFinCdas vonFgeerbteAaußer Kraft setzen.Endbeispiel
15.6.6 Versiegelte Methoden
Wenn eine Instanzmethodendeklaration einen sealed Modifizierer enthält, wird diese Methode als versiegelte Methode bezeichnet. Eine versiegelte Methode setzt eine geerbte virtuelle Methode mit derselben Signatur außer Kraft. Eine versiegelte Methode muss auch mit dem Modifikator override gekennzeichnet werden. Die Verwendung des sealed Modifizierers verhindert, dass eine abgeleitete Klasse die Methode weiter außer Kraft setzt.
Beispiel: Das Beispiel
class A { public virtual void F() => Console.WriteLine("A.F"); public virtual void G() => Console.WriteLine("A.G"); } class B : A { public sealed override void F() => Console.WriteLine("B.F"); public override void G() => Console.WriteLine("B.G"); } class C : B { public override void G() => Console.WriteLine("C.G"); }Die Klasse
Bbietet zwei Überschreibungsmethoden: eineF-Methode, die densealed-Modifikator hat, und eineG-Methode, die ihn nicht hat.B's Verwendung des Modifikatorssealedverhindert, dassCweiterhin außer Kraft setztF.Endbeispiel
15.6.7 Abstrakte Methoden
Wenn eine Instanzmethodendeklaration einen abstract Modifizierer enthält, wird diese Methode als abstrakte Methode bezeichnet. Obwohl eine abstrakte Methode implizit auch eine virtuelle Methode ist, kann sie nicht über den Modifizierer virtualverfügen.
Eine abstrakte Methodendeklaration führt eine neue virtuelle Methode ein, stellt jedoch keine Implementierung dieser Methode bereit. Stattdessen sind nicht abstrakte abgeleitete Klassen dazu verpflichtet, ihre eigene Implementierung bereitzustellen, indem sie diese Methode überschreiben. Die method_body einer abstrakten Methode besteht einfach aus einem Semikolon.
Abstrakte Methodendeklarationen sind nur in abstrakten Klassen (§15.2.2.2) und Schnittstellen (§19.4.3) zulässig.
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
ShapeKlasse definiert den abstrakten Begriff eines geometrischen Formobjekts, das sich selbst zeichnen kann. DiePaintMethode ist abstrakt, da es keine sinnvolle Fallbackimplementierung für das abstrakte Konzept der Form gibt. DieEllipse- undBox-Klassen sind konkreteShape-Implementierungen. Da diese Klassen nicht abstrakt sind, müssen sie diePaintMethode 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
Adeklariert eine virtuelle Methode, eine KlasseBüberschreibt diese Methode mit einer abstrakten Methode und überschreibtCdie abstrakte Methode, um eine eigene Implementierung bereitzustellen.Endbeispiel
15.6.8 Externe Methoden
Wenn eine Methodendeklaration einen extern Modifizierer enthält, wird die Methode als externe Methode bezeichnet. Externe Methoden werden extern implementiert, in der Regel wird eine andere Sprache als C# verwendet. Da eine externe Methodendeklaration keine tatsächliche Implementierung bereitstellt, besteht der Methodentext einer externen Methode einfach aus einem Semikolon. Eine externe Methode darf nicht generisch sein.
Der Mechanismus, durch den eine Verbindung mit einer externen Methode hergestellt wird, ist implementierungsspezifisch.
Beispiel: Im folgenden Beispiel wird die Verwendung des
externModifizierers und desDllImportAttributs veranschaulicht:class Path { [DllImport("kernel32", SetLastError=true)] static extern bool CreateDirectory(string name, SecurityAttribute sa); [DllImport("kernel32", SetLastError=true)] static extern bool RemoveDirectory(string name); [DllImport("kernel32", SetLastError=true)] static extern int GetCurrentDirectory(int bufSize, StringBuilder buf); [DllImport("kernel32", SetLastError=true)] static extern bool SetCurrentDirectory(string name); }Endbeispiel
15.6.9 Teilmethoden
Wenn eine Methodendeklaration einen partial Modifizierer enthält, wird diese Methode als partielle Methode bezeichnet. Partielle Methoden dürfen nur als Mitglieder von Teiltypen (§15.2.7) deklariert werden und unterliegen einer Reihe von Einschränkungen.
Partielle Methoden können in einem Teil einer Typdeklaration definiert und in einer anderen implementiert werden. Die Umsetzung ist optional; wenn keine Komponente die partielle Methode implementiert, werden die partielle Methodendeklaration und alle Aufrufe davon aus der Typdeklaration entfernt, die sich aus der Kombination der Teile ergibt.
Teilmethoden dürfen keine Zugriffsmodifizierer definieren; sie sind implizit privat. Ihr Rückgabetyp muss sein void, und ihre Parameter dürfen keine Ausgabeparameter sein. Der Bezeichner partial wird nur dann als kontextbezogenes Schlüsselwort (§6.4.4) in einer Methodendeklaration erkannt, wenn er unmittelbar vor dem void Schlüsselwort angezeigt wird. Eine partielle Methode kann keine Schnittstellenmethoden explizit implementieren.
Es gibt zwei Arten von partiellen Methodendeklarationen: Wenn der Textkörper der Methodendeklaration ein Semikolon ist, wird die Deklaration als definierende partielle Methodendeklaration bezeichnet. Wenn der Textkörper kein Semikolon ist, wird die Deklaration als implementierende Partielle Methodendeklaration bezeichnet. In allen Teilen einer Typdeklaration darf nur eine Teilmethodendeklaration mit einer bestimmten Signatur definiert werden, und es darf höchstens nur eine Teilmethodendeklaration mit einer bestimmten Signatur implementiert werden. Wenn eine Teilmethodenerklärung zur Durchführung gegeben wird, muss eine entsprechende definitionsbezogene Teilmethodenerklärung vorhanden sein, und die Erklärungen stimmen wie in den folgenden Angaben angegeben überein:
- Die Deklarationen müssen dieselben Modifizierer haben (auch wenn nicht notwendigerweise in derselben Reihenfolge), Methodenname, Anzahl der Typparameter und Anzahl von Parametern.
- Die entsprechenden Parameter in den Deklarationen müssen die gleichen Modifizierer aufweisen (obwohl nicht notwendigerweise in derselben Reihenfolge) und die gleichen Typen oder Identitätsveränderertypen (Modulounterschiede bei Typparameternamen).
- Die entsprechenden Typparameter in den Deklarationen müssen dieselben Einschränkungen aufweisen (Modulounterschiede bei Typparameternamen).
Eine implementierende partielle Methodendeklaration kann im selben Teil wie die entsprechende definierende partielle Methodendeklaration angezeigt werden.
An der Überladungsauflösung ist nur eine definierende Teilmethode beteiligt. Unabhängig davon, ob eine Implementierungsdeklaration angegeben wird, können Aufrufausdrücke in Aufrufe der partiellen Methode aufgelöst werden. Da eine partielle Methode immer zurückgibt void, sind solche Aufrufausdrücke immer Ausdrucksanweisungen. Da eine partielle Methode implizit privateist, treten solche Anweisungen immer innerhalb eines der Teile der Typdeklaration auf, innerhalb der die partielle Methode deklariert wird.
Hinweis: Die Definition des Abgleichs zum Definieren und Implementieren partieller Methodendeklarationen erfordert nicht, dass Parameternamen übereinstimmen. Dies kann zu überraschenden, wenn auch gut definierten Verhaltensweisen führen, wenn benannte Argumente (§12.6.2.1) verwendet werden. Beispiel: Angenommen, es gibt eine definierende partielle Methodendeklaration für
Min einer Datei und eine implementierende partielle Methodendeklaration in einer anderen Datei:// File P1.cs: partial class P { static partial void M(int x); } // File P2.cs: partial class P { static void Caller() => M(y: 0); static partial void M(int y) {} }ist ungültig , da der Aufruf den Argumentnamen aus der Implementierung und nicht die definierende Partielle Methodendeklaration verwendet.
Hinweisende
Wenn kein Teil einer Partdeklaration eine Implementierungsdeklaration für eine bestimmte partielle Methode enthält, wird jede Ausdrucksanweisung, die sie aufruft, einfach aus der kombinierten Typdeklaration entfernt. Daher hat der Aufrufausdruck, einschließlich aller Unterausdrücke, zur Laufzeit keine Auswirkung. Die partielle Methode selbst wird ebenfalls entfernt und ist kein Mitglied der kombinierten Typdeklaration.
Wenn eine Implementierungsdeklaration für eine bestimmte partielle Methode vorhanden ist, werden die Aufrufe der Partielle Methoden beibehalten. Die partielle Methode führt zu einer Methodendeklaration, die der implementierenden partiellen Methodendeklaration ähnelt, mit Ausnahme der folgenden:
Der
partialModifizierer ist nicht enthalten.Die Attribute in der resultierenden Methodendeklaration sind die kombinierten Attribute der definierenden und implementierenden partiellen Methodendeklaration in nicht angegebener Reihenfolge. Duplikate werden nicht entfernt.
Die Attribute für die Parameter der resultierenden Methodendeklaration sind die kombinierten Attribute der entsprechenden Parameter der Definition und der implementierenden partiellen Methodendeklaration in nicht angegebener Reihenfolge. Duplikate werden nicht entfernt.
Wenn eine definierende Deklaration, aber keine Implementierungsdeklaration für eine partielle Methode Mangegeben wird, gelten die folgenden Einschränkungen:
Es ist ein Kompilierfehler, einen Delegaten von
Mzu erstellen (§12.8.17.5).Es ist ein Kompilierfehler, innerhalb einer anonymen Funktion, die in einen Ausdrucksbaumtyp konvertiert ist, auf
Mzu verweisen (§8.6).Ausdrücke, die im Rahmen eines Aufrufs
Mauftreten, wirken sich nicht auf den endgültigen Zuordnungszustand (§9.4) aus, was möglicherweise zu Kompilierungsfehlern führen kann.Mkann nicht der Einstiegspunkt für eine Anwendung (§7.1) sein.
Partielle Methoden sind nützlich, um einem Teil einer Typdeklaration das Verhalten eines anderen Teils anzupassen, z. B. eine, die von einem Tool generiert wird. Betrachten Sie die folgende partielle Klassendeklaration:
partial class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
partial void OnNameChanging(string newName);
partial void OnNameChanged();
}
Wenn diese Klasse ohne andere Teile kompiliert wird, werden die definierenden partiellen Methodendeklarationen und deren Aufrufe entfernt, und die resultierende kombinierte Klassendeklaration entspricht folgendem:
class Customer
{
string name;
public string Name
{
get => name;
set => name = value;
}
}
Gehen Sie davon aus, dass ein weiterer Teil angegeben wird, der jedoch Implementierungsdeklarationen der partiellen Methoden bereitstellt:
partial class Customer
{
partial void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
partial void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
Anschließend entspricht die resultierende kombinierte Klassendeklaration folgendem:
class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
15.6.10 Erweiterungsmethoden
Wenn der erste Parameter einer Methode den this Modifizierer enthält, wird diese Methode als Erweiterungsmethode bezeichnet. Erweiterungsmethoden dürfen nur in nicht generischen, nicht geschachtelten statischen Klassen deklariert werden. Der erste Parameter einer Erweiterungsmethode ist wie folgt eingeschränkt:
- Er kann nur ein Eingabeparameter sein, wenn er einen Werttyp aufweist.
- Es kann nur ein Verweisparameter sein, wenn er einen Werttyp aufweist oder einen generischen Typ auf die Struktur beschränkt hat.
- Es darf kein Ausgabeparameter sein.
- Es darf kein Zeigertyp sein.
Beispiel: Im Folgenden sehen Sie ein Beispiel für eine statische Klasse, die zwei Erweiterungsmethoden deklariert:
public static class Extensions { public static int ToInt32(this string s) => Int32.Parse(s); public static T[] Slice<T>(this T[] source, int index, int count) { if (index < 0 || count < 0 || source.Length - index < count) { throw new ArgumentException(); } T[] result = new T[count]; Array.Copy(source, index, result, 0, count); return result; } }Endbeispiel
Eine Erweiterungsmethode ist eine normale statische Methode. Außerdem kann eine Erweiterungsmethode, wenn ihre umschließende statische Klasse im Gültigkeitsbereich ist, mit der Syntax für den Aufruf von Instanzmethoden (§12.8.10.3) aufgerufen werden, wobei der Empfängerausdruck als erstes Argument verwendet wird.
Beispiel: Im folgenden Programm werden die oben deklarierten Erweiterungsmethoden verwendet:
static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in strings.Slice(1, 2)) { Console.WriteLine(s.ToInt32()); } } }Die
SliceMethode ist verfügbar aufstring[], und dieToInt32Methode ist verfügbar aufstring, da sie als Erweiterungsmethoden deklariert wurden. Die Bedeutung des Programms ist identisch mit dem folgenden, wobei gewöhnliche statische Methodenaufrufe verwendet werden:static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in Extensions.Slice(strings, 1, 2)) { Console.WriteLine(Extensions.ToInt32(s)); } } }Endbeispiel
15.6.11 Methodentext
Der Methodentext einer Methodendeklaration besteht entweder aus einem Blocktext, einem Ausdruckstext oder einem Semikolon.
Abstrakte und externe Methodendeklarationen stellen keine Methodenimplementierung bereit, sodass ihre Methodentexte einfach aus einem Semikolon bestehen. Bei jeder anderen Methode ist der Methodentext ein Block (§13.3), der die auszuführenden Anweisungen enthält, wenn diese Methode aufgerufen wird.
Der effektive Rückgabetyp einer Methode ist void , wenn der Rückgabetyp ist voidoder wenn die Methode asynchron ist und der Rückgabetyp ist «TaskType» (§15.14.1). Andernfalls ist der effektive Rückgabetyp einer nicht asynchronen Methode der Rückgabetyp, und der effektive Rückgabetyp einer asynchronen Methode mit Rückgabetyp «TaskType»<T>(§15.14.1) lautet T.
Wenn der effektive Rückgabetyp einer Methode void ist und die Methode einen Blockkörper hat, dürfen return Anweisungen (§13.10.5) im Block keinen Ausdruck enthalten. Wenn die Ausführung des Blocks einer void -Methode normal abgeschlossen wird (d. h. die Steuerung vom Ende des Methodenkörpers abläuft), kehrt diese Methode einfach zum Aufrufer zurück.
Wenn der effektive Rückgabetyp einer Methode ist void und die Methode über einen Ausdruckstext verfügt, muss der Ausdruck E ein statement_expression sein, und der Textkörper entspricht exakt einem Blockkörper des Formulars { E; }.
Bei einer Rückgabe-nach-Wert-Methode (§15.6.1) muss jede Rückgabe-Anweisung im Textkörper dieser Methode einen Ausdruck angeben, der implizit in den effektiven Rückgabetyp konvertierbar ist.
Bei einer Return-by-Ref-Methode (§15.6.1) muss jede Return-Anweisung im Körper dieser Methode einen Ausdruck angeben, dessen Typ dem effektiven Rückgabetyp entspricht und einen ref-safe-context von caller-context hat (§9.7.2).
Für Rückgabe-nach-Wert- und Rückgabe-nach-Referenz-Methoden sollte der Endpunkt des Methodenkörpers nicht erreichbar sein. Mit anderen Worten: Die Kontrolle darf nicht über das Ende des Methodentexts hinausfließen.
Beispiel: Im folgenden Code
class A { public int F() {} // Error, return value required public int G() { return 1; } public int H(bool b) { if (b) { return 1; } else { return 0; } } public int I(bool b) => b ? 1 : 0; }die wertgebende
F-Methode führt zu einem Kompilierfehler, da die Kontrolle am Ende des Methodenkörpers abfließen kann. Die MethodenGundHsind korrekt, da alle möglichen Ausführungspfade in einer Rückgabeanweisung enden, die einen Rückgabewert angibt. DieIMethode ist richtig, da ihr Textkörper einem Block mit nur einer einzelnen Rückgabe-Anweisung entspricht.Endbeispiel
15.7 Eigenschaften
15.7.1 Allgemein
Eine Eigenschaft ist ein Element, das Zugriff auf ein Merkmal eines Objekts oder einer Klasse bietet. Beispiele für Eigenschaften sind die Länge einer Zeichenfolge, der Schriftgrad, die Beschriftung eines Fensters und der Name eines Kunden. Eigenschaften sind eine natürliche Erweiterung von Feldern – beide sind benannte Member mit zugeordneten Typen, und die Syntax für den Zugriff auf Felder und Eigenschaften ist identisch. Im Gegensatz zu Feldern bezeichnen Eigenschaften jedoch keine Speicherorte. Stattdessen verfügen Eigenschaften über Accessors zum Angeben der Anweisungen, die beim Lesen oder Schreiben ihrer Werte ausgeführt werden sollen. Eigenschaften bieten somit einen Mechanismus zum Zuordnen von Aktionen zum Lesen und Schreiben von Eigenschaften eines Objekts oder einer Klasse; ferner erlauben sie, diese Merkmale zu berechnen.
Eigenschaften werden mit property_declaration sdeklariert:
property_declaration
: attributes? property_modifier* type member_name property_body
| attributes? property_modifier* ref_kind type member_name ref_property_body
;
property_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
property_body
: '{' accessor_declarations '}' property_initializer?
| '=>' expression ';'
;
property_initializer
: '=' variable_initializer ';'
;
ref_property_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§24.2) ist nur im unsicheren Code (§24) verfügbar.
Eine property_declaration kann eine Reihe von Attributen (§23) 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). Darüber hinaus kann eine property_declaration, die direkt von einer struct_declaration enthalten wird, den readonly Modifizierer (§16.4.11) umfassen.
- Die erste deklariert eine Eigenschaft ohne Referenzwert. Sein Wert hat den Typ Typ. Diese Art von Eigenschaft kann lesbar und/oder schreibbar sein.
- Die zweite deklariert eine Eigenschaft mit Referenzwert. Sein Wert ist eine Variablenreferenz (§9.5), die
readonlysein kann, auf eine Variable vom Typ Typ. Diese Art von Eigenschaft ist nur lesbar.
Eine property_declaration kann eine Reihe von Attributen (§23) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), der new (§15.3.5), static (§15.7.2), (virtual, §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 Schnittstellenmitgliedimplementierung handelt, ist member_name einfach ein Bezeichner. Für eine explizite Schnittstellenmemmimplementierung (§19.6.2) besteht die member_name aus einem interface_type gefolgt von einem "." und einem Bezeichner.
Der Typ einer Eigenschaft muss mindestens so zugänglich sein wie die Eigenschaft selbst (§7.5.5).
Ein property_body kann entweder aus einem Anweisungstext oder einem Ausdruckstext bestehen. In einem Anweisungstext deklarieren accessor_declarations, die in „{“ und „}“ Token eingeschlossen sein müssen, die Accessoren (§15.7.3) der Eigenschaft. Die Accessoren geben die ausführbaren Anweisungen an, die mit dem Lesen und Schreiben der Eigenschaft verknüpft sind.
In einem property_body ist ein Ausdruckstext, der aus => gefolgt von einem AusdruckE und einem Semikolon besteht, genau äquivalent zum Anweisungstext { get { return E; } }und kann daher nur verwendet werden, um schreibgeschützte Eigenschaften zu spezifizieren, bei denen das Ergebnis des get-Accessors durch einen einzigen Ausdruck gegeben ist.
Eine property_initializer kann nur für eine automatisch implementierte Eigenschaft (§15.7.4) angegeben werden und bewirkt die Initialisierung des zugrunde liegenden Felds solcher Eigenschaften mit dem vom Ausdruck angegebenen Wert.
Ein ref_property_body kann entweder aus einem Anweisungskörper oder einem Ausdruckskörper bestehen. In einem Anweisungstext deklariert eine get_accessor_declaration den get-Accessors (§15.7.3) der Eigenschaft. Der Accessor gibt die ausführbaren Anweisungen an, die mit dem Lesen der Eigenschaft verknüpft sind.
In einem ref_property_body, in dem ein Ausdruckskörper besteht aus => gefolgt von ref, einem variable_referenceV und einem Semikolon, entspricht dies genau dem Anweisungskörper { get { return ref V; } }.
Hinweis: Obwohl die Syntax für den Zugriff auf eine Eigenschaft mit dem für ein Feld identisch ist, wird eine Eigenschaft nicht als Variable klassifiziert. Daher ist es nicht möglich, eine Eigenschaft als
in,outoderrefArgument zu übergeben, es sei denn, die Eigenschaft ist ref-valued und gibt daher einen Variablenverweis (§9.7) zurück. Hinweisende
Wenn eine Eigenschaftsdeklaration einen extern Modifizierer enthält, wird die Eigenschaft als externe Eigenschaft bezeichnet. Da eine externe Eigenschaftsdeklaration keine tatsächliche Implementierung liefert, muss jede der Accessor_bodys in ihren accessor_declarations ein Semikolon sein.
15.7.2 Statische und Instanz-Eigenschaften
Wenn eine Eigenschaftsdeklaration einen static Modifizierer enthält, wird die Eigenschaft als statische Eigenschaft bezeichnet. Wenn kein static Modifizierer vorhanden ist, wird die Eigenschaft als Instanz-Eigenschaft bezeichnet.
Eine statische Eigenschaft ist keiner bestimmten Instanz zugeordnet, und es ist ein Kompilierungszeit-Fehler, wenn in den Zugriffsmethoden einer statischen Eigenschaft auf this verwiesen wird.
Eine Instanzeigenschaft ist einer bestimmten Instanz einer Klasse zugeordnet, und auf diese Instanz kann in den Accessoren dieser Eigenschaft als this (§12.8.14) zugegriffen werden.
Die Unterschiede zwischen statischen und Instanzmitgliedern werden in §15.3.8 weiter erörtert.
15.7.3 Accessoren
Hinweis: Diese Unterliste gilt sowohl für Eigenschaften (§15.7) als auch für Indexer (§15.9). Die Unterabschnitt wird in Bezug auf Eigenschaften geschrieben. Ersetzen Sie beim Lesen für Indexer „indexer/indexers“ durch „property/properties“, und sehen Sie sich die Liste der Unterschiede zwischen Eigenschaften und Indexern in §15.9.2 an. Hinweisende
Die accessor_declarations einer Eigenschaft geben die ausführbaren Anweisungen an, die mit dem Schreiben und/oder Lesen dieser Eigenschaft verknüpft sind.
accessor_declarations
: get_accessor_declaration set_accessor_declaration?
| set_accessor_declaration get_accessor_declaration?
;
get_accessor_declaration
: attributes? accessor_modifier? 'get' accessor_body
;
set_accessor_declaration
: attributes? accessor_modifier? 'set' accessor_body
;
accessor_modifier
: 'protected'
| 'internal'
| 'private'
| 'protected' 'internal'
| 'internal' 'protected'
| 'protected' 'private'
| 'private' 'protected'
| 'readonly' // direct struct members only
;
accessor_body
: block
| '=>' expression ';'
| ';'
;
ref_get_accessor_declaration
: attributes? accessor_modifier? 'get' ref_accessor_body
;
ref_accessor_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
Die Accessor-Deklarationen bestehen aus einer get-Accessor-Deklaration, einer set-Accessor-Deklaration oder beidem. Jede Accessordeklaration besteht aus optionalen Attributen, einem optionalen accessor_modifier, dem Token get oder set, gefolgt von einer accessor_body.
Für eine referenzwertige Eigenschaft besteht die ref_get_accessor_declaration aus optionalen Attributen, einem optionalen accessor_modifier, dem Token get, gefolgt von einem ref_accessor_body.
Die Verwendung von accessor_modifiers unterliegt den folgenden Einschränkungen:
- Die accessor_modifier
readonlyist nur in einer property_declaration oder indexer_declaration zulässig, die direkt in einem struct_declaration enthalten ist (§16.4.11, §16.4.13). - Für eine Eigenschaft oder einen Indexer, der keinen
overrideModifikator hat, ist ein accessor_modifier nur dann erlaubt, wenn die Eigenschaft oder der Indexer sowohl einen get- als auch einen set-Accessor hat, und dann auch nur für einen dieser Accessoren erlaubt ist. - Für eine Eigenschaft oder einen Indexer, der einen
override-Modifizierer enthält, muss ein Accessor, falls vorhanden, mit dem accessor_modifier des überschriebenen Accessors übereinstimmen. - Der accessor_modifier muss eine Zugänglichkeit deklarieren, die streng restriktiver ist als die deklarierte Zugänglichkeit der Eigenschaft oder des Indexers selbst. Um genau zu sein:
- Wenn die Eigenschaft oder der Indexer eine deklarierte Zugänglichkeit von
publichat, kann die durch accessor_modifier deklarierte Zugänglichkeit entwederprivate protected,protected internal,internal,protectedoderprivatesein. - Wenn die Eigenschaft oder der Indexer eine deklarierte Zugänglichkeit von
protected internalhat, kann die durch accessor_modifier deklarierte Zugänglichkeit entwederprivate protected,protected private,internal,protectedoderprivatesein. - Wenn die Eigenschaft oder der Indexer die deklarierte Zugänglichkeit von
internaloderprotectedhat, muss die von accessor_modifier deklarierte Zugänglichkeit entwederprivate protectedoderprivatesein. - Wenn die Eigenschaft oder der Indexer eine deklarierte Zugänglichkeit von
private protectedhat, muss die durch accessor_modifier deklarierte Zugänglichkeitprivatesein. - Wenn die Eigenschaft oder der Indexer über eine deklarierte Zugänglichkeit von
privateverfügt, können keine accessor_modifier verwendet werden.
- Wenn die Eigenschaft oder der Indexer eine deklarierte Zugänglichkeit von
Für abstract und extern nicht-ref-bewertete Eigenschaften ist jeder accessor_body für jeden angegebenen Accessor einfach ein Semikolon. Bei einer nicht-abstrakten, nicht-externen Eigenschaft, aber nicht bei einem Indexer, kann der Accessor_body für alle angegebenen Accessoren auch ein Semikolon sein. In diesem Fall ist es eine automatisch implementierte Eigenschaft (§15.7.4). Eine automatisch implementierte Eigenschaft muss mindestens einen get-Accessor haben. Für die Accessoren anderer nicht abstrakter, nicht externer Eigenschaften ist die accessor_body entweder:
- ein Block , der die auszuführenden Anweisungen angibt, wenn der entsprechende Accessor aufgerufen wird; oder
- einen Ausdruckstext, der aus
=>, gefolgt von einem Ausdruck und einem Semikolon, besteht und einen einzelnen Ausdruck bezeichnet, der ausgeführt wird, wenn der entsprechende Accessor aufgerufen wird.
Für abstract und extern ref-bewertete Eigenschaften ist das ref_accessor_body einfach ein Semikolon. Für den Accessor jeder anderen nicht-abstrakten, nicht-externen Eigenschaft ist der ref_accessor_body entweder:
- ein Block , der die auszuführenden Anweisungen angibt, wenn der Get-Accessor aufgerufen wird; oder
- ein Ausdruckskörper, der aus
=>, gefolgt vonref, einem variable_reference und einem Semikolon besteht. Die Variablenreferenz wird ausgewertet, wenn der get-Accessor aufgerufen wird.
Ein Get-Accessor für eine Eigenschaft ohne Bezugswert entspricht einer parameterlosen Methode mit einem Rückgabewert des Eigenschaftstyps. Mit Ausnahme der Verwendung als Ziel einer Zuweisung wird, wenn auf eine solche Eigenschaft in einem Ausdruck verwiesen wird, der get-Accessor aufgerufen, um den Wert der Eigenschaft zu berechnen (§12.2.2).
Der Hauptteil eines get-Accessors für eine nicht referenzierte Eigenschaft muss den in § 15.6.11 beschriebenen Regeln für Methoden zur Wertrückgabe entsprechen. Insbesondere müssen alle return-Anweisungen im Texttext eines get-Accessors einen Ausdruck angeben, der implizit in den Eigenschaftstyp konvertierbar ist. Darüber hinaus darf der Endpunkt eines Get-Accessors nicht erreichbar sein.
Ein get-Accessors für eine ref-valued-Eigenschaft entspricht einer parameterlosen Methode mit einem Rückgabewert einer variable_reference zu einer Variablen des Eigenschaftstyps. Wenn auf eine solche Eigenschaft in einem Ausdruck verwiesen wird, wird der Get-Accessor aufgerufen, um den variable_reference Wert der Eigenschaft zu berechnen. Diese Variablenreferenzwird dann wie jede andere verwendet, um die referenzierte Variable zu lesen oder, bei nicht lesegeschützten Variablenreferenzen, zu schreiben, wie es der Kontext erfordert.
Beispiel: Das folgende Beispiel veranschaulicht eine neu bewertete Eigenschaft als Ziel einer Aufgabe:
class Program { static int field; static ref int Property => ref field; static void Main() { field = 10; Console.WriteLine(Property); // Prints 10 Property = 20; // This invokes the get accessor, then assigns // via the resulting variable reference Console.WriteLine(field); // Prints 20 } }Endbeispiel
Der Hauptteil eines get-Accessors für eine neu bewertete Eigenschaft muss den Regeln für neu bewertete Methoden entsprechen, die in § 15.6.11 beschrieben sind.
Ein Set-Accessor entspricht einer Methode mit einem einzelnen Wertparameter des Eigenschaftstyps und einem void Rückgabetyp. Der implizite Parameter eines Set-Accessors wird immer benannt value. Wenn auf eine Eigenschaft als Ziel einer Zuordnung (§12.22) oder als Operand von ++ oder –- (§12.8.16, §12.9.7) verwiesen wird, wird der Set-Accessor mit einem Argument aufgerufen, das den neuen Wert (§12.22.2) bereitstellt. Der Text eines Set-Accessors muss den Regeln für void Methoden entsprechen, die in §15.6.11beschrieben sind. Insbesondere sind Rückgabeanweisungen im gesetzten Accessor-Text nicht zulässig, um einen Ausdruck anzugeben. Da ein Set-Accessor implizit einen Parameter mit dem Namen valuehat, handelt es sich um einen Kompilierungszeitfehler für eine lokale Variable oder konstante Deklaration in einem Set-Accessor, der über diesen Namen verfügt.
Basierend auf dem Vorhandensein oder Nichtvorhandensein der Get- und Set-Accessors wird eine Eigenschaft wie folgt klassifiziert:
- Eine Eigenschaft, die sowohl einen Get-Accessor als auch einen Set-Accessor enthält, wird als Lese -/Schreibzugriffseigenschaft bezeichnet.
- Eine Eigenschaft, die nur über einen get-Accessors verfügt, wird als schreibgeschützte Eigenschaft bezeichnet. Es ist ein Kompilierzeitfehler, dass eine schreibgeschützte Eigenschaft das Ziel einer Zuweisung ist.
- Eine Eigenschaft, die nur einen Set-Accessor hat, wird als schreibgeschützte Eigenschaftbezeichnet. Außer als Ziel einer Zuweisung ist es ein Kompilierzeitfehler, auf eine schreibgeschützte Eigenschaft in einem Ausdruck zu verweisen.
Hinweis: Die Prä- und Postfix-Operatoren
++und--sowie zusammengesetzte Zuweisungsoperatoren können nicht auf schreibgeschützte Eigenschaften angewendet werden, da diese Operatoren den alten Wert ihres Operanden lesen, bevor sie den neuen schreiben. Hinweisende
Beispiel: Im folgenden Code
public class Button : Control { private string caption; public string Caption { get => caption; set { if (caption != value) { caption = value; Repaint(); } } } public override void Paint(Graphics g, Rectangle r) { // Painting code goes here } }das
ButtonSteuerelement deklariert eine öffentlicheCaptionEigenschaft. Der get-Accessors der Eigenschaft Beschriftung gibt dasstringzurück, das im privatencaption-Feld gespeichert ist. Der set Accessor prüft, ob der neue Wert vom aktuellen Wert abweicht, und wenn ja, speichert er den neuen Wert und färbt das Steuerelement neu. Eigenschaften folgen häufig dem oben gezeigten Muster: Der Get-Accessor gibt einfach einen Wert zurück, der in einemprivateFeld gespeichert ist, und der Set-Accessor ändert diesesprivateFeld und führt dann alle zusätzlichen Aktionen aus, die erforderlich sind, um den Vollständigen Status des Objekts zu aktualisieren. In Anbetracht derButtonobigen Klasse ist Folgendes ein Beispiel für die Verwendung derCaptionEigenschaft:Button okButton = new Button(); okButton.Caption = "OK"; // Invokes set accessor string s = okButton.Caption; // Invokes get accessorHier wird der Set-Accessor aufgerufen, indem der Eigenschaft ein Wert zugewiesen wird, und der Get-Accessor wird aufgerufen, indem auf die Eigenschaft in einem Ausdruck verwiesen wird.
Endbeispiel
Die get- und set-Accessors einer Eigenschaft sind keine separaten Mitglieder, und es ist nicht möglich, die Zugriffsberechtigten einer Eigenschaft separat zu deklarieren.
Beispiel: Das Beispiel
class A { private string name; // Error, duplicate member name public string Name { get => name; } // Error, duplicate member name public string Name { set => name = value; } }deklariert keine einzelne Schreib-Lese-Eigenschaft. Vielmehr werden zwei Eigenschaften mit demselben Namen deklariert, eine schreibgeschützt und eine schreibgeschützt. Da zwei Elemente, die in derselben Klasse deklariert sind, nicht denselben Namen haben können, tritt im Beispiel ein Kompilierungsfehler auf.
Endbeispiel
Wenn eine abgeleitete Klasse eine Eigenschaft mit demselben Namen wie eine geerbte Eigenschaft deklariert, blendet die abgeleitete Eigenschaft die geerbte Eigenschaft sowohl beim Lesen als auch beim Schreiben aus.
Beispiel: Im folgenden Code
class A { public int P { set {...} } } class B : A { public new int P { get {...} } }die Eigenschaft
PinBverbirgt die EigenschaftPinAsowohl beim Lesen als auch beim Schreiben. Demnach sind in den StatementsB b = new B(); b.P = 1; // Error, B.P is read-only ((A)b).P = 1; // Ok, reference to A.Pdie Zuweisung an
b.Pführt zu einem Kompilierfehler, da die schreibgeschützte EigenschaftPinBdie schreibgeschützte EigenschaftPinAausblendet. Beachten Sie jedoch, dass ein Cast verwendet werden kann, um auf die versteckte EigenschaftPzuzugreifen.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
PointOrt 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
LabelKlasse zweiintFelderxundy, um den Speicherort zu speichern. Der Speicherort wird sowohl als EigenschaftXundYals auch als EigenschaftLocationvom TypPointöffentlich verfügbar gemacht. Wenn es in einer zukünftigen Version vonLabeleinfacher wird, den Speicherort intern alsPointzu speichern, kann die Änderung vorgenommen werden, ohne dass sich dies auf die öffentliche Schnittstelle der Klasse auswirkt.class Label { private Point ___location; private string caption; public Label(int x, int y, string caption) { this.___location = new Point(x, y); this.caption = caption; } public int X => ___location.X; public int Y => ___location.Y; public Point Location => ___location; public string Caption => caption; }Hätten
xundystattdessenpublic readonlyFelder sein müssen, wäre es unmöglich gewesen, eine solche Änderung an derLabelKlasse vorzunehmen.Endbeispiel
Hinweis: Das Verfügbarmachen des Zustands über Eigenschaften ist nicht notwendigerweise weniger effizient als das direkte Verfügbarmachen von Feldern. Wenn eine Eigenschaft nicht virtuell ist und nur eine kleine Menge Code enthält, kann die Ausführungsumgebung Aufrufe von Accessoren durch den tatsächlichen Code der Accessoren ersetzen. Dieser Prozess wird als Einlinderung bezeichnet und macht den Zugriff auf Eigenschaften so effizient wie Feldzugriff, behält jedoch die erhöhte Flexibilität von Eigenschaften bei. Hinweisende
Beispiel: Da das Aufrufen eines get-Accessors konzeptionell gleichbedeutend mit dem Lesen des Wertes eines Feldes ist, wird es als schlechter Programmierstil für get-Accessors angesehen, beobachtbare Nebenwirkungen zu haben. Im Beispiel
class Counter { private int next; public int Next => next++; }der Wert der
NextEigenschaft hängt davon ab, wie oft zuvor auf die Eigenschaft zugegriffen wurde. Daher erzeugt der Zugriff auf die Eigenschaft einen feststellbaren Nebeneffekt, und die Eigenschaft sollte stattdessen als Methode implementiert werden.Die Konvention „keine Nebenwirkungen“ für get-Accessors bedeutet nicht, dass get-Accessors immer einfach so geschrieben werden sollten, dass in Feldern gespeicherte Werte zurückgegeben werden. Tatsächlich berechnen Accessoren häufig den Wert einer Eigenschaft, indem sie auf mehrere Felder zugreifen oder Methoden aufrufen. Ein ordnungsgemäß entworfener Get-Accessor führt jedoch keine Aktionen aus, die zu feststellbaren Änderungen im Zustand des Objekts führen.
Endbeispiel
Eigenschaften können verwendet werden, um die Initialisierung einer Ressource bis zu dem Zeitpunkt zu verzögern, an dem sie zuerst referenziert wird.
Beispiel:
public class Console { private static TextReader reader; private static TextWriter writer; private static TextWriter error; public static TextReader In { get { if (reader == null) { reader = new StreamReader(Console.OpenStandardInput()); } return reader; } } public static TextWriter Out { get { if (writer == null) { writer = new StreamWriter(Console.OpenStandardOutput()); } return writer; } } public static TextWriter Error { get { if (error == null) { error = new StreamWriter(Console.OpenStandardError()); } return error; } } ... }Die
ConsoleKlasse enthält drei Eigenschaften,In,OutundError, die die Standardeingabe, Ausgabe und Fehlergeräte darstellen. Indem diese Member als Eigenschaften zugänglich gemacht werden, kann dieConsoleKlasse die Initialisierung verzögern, bis sie tatsächlich verwendet werden. Beispiel: Beim ersten Verweisen auf dieOutEigenschaft wie inConsole.Out.WriteLine("hello, world");wird das zugrunde liegende
TextWriterfür das Ausgabegerät erstellt. Wenn die Anwendung jedoch keinen Verweis auf dieInUnd-EigenschaftenErrormacht, werden keine Objekte für diese Geräte erstellt.Endbeispiel
15.7.4 Automatisch implementierte Eigenschaften
Eine automatisch implementierte Eigenschaft (oder kurz Auto-Eigenschaft) ist eine nicht-abstrakte, nicht-externe, nicht-ref-bewertete Eigenschaft mit nur Semikolon accessor_bodys. Auto-Eigenschaften müssen einen get-Accessors haben und können optional einen set-Accessors haben.
Wenn eine Eigenschaft als automatisch implementierte Eigenschaft angegeben wird, steht automatisch ein ausgeblendetes Sicherungsfeld für die Eigenschaft zur Verfügung, und die Accessoren werden implementiert, um aus diesem Sicherungsfeld zu lesen und zu schreiben. Das ausgeblendete Unterstützungsfeld ist unzugänglich, es kann nur über die automatisch implementierten Eigenschaften-Accessors gelesen und geschrieben werden, auch innerhalb des Containertyps. Wenn die Auto-Eigenschaft keinen set-Accessor hat, wird das hinterlegte Feld als readonly betrachtet (§15.5.3). Genau wie ein readonly -Feld kann auch eine schreibgeschützte Auto-Eigenschaft im Body eines Konstruktors der umschließenden Klasse zugewiesen werden. Eine solche Abtretung geht direkt an den read-only Hintergrundfeld der Immobilie.
Eine Auto-Eigenschaft kann optional mit einer eigenschafts_initialisator, direkt auf das Hintergrundfeld als Variablen_Initialisierer (§17.7).
Beispiel:
public class Point { public int X { get; set; } // Automatically implemented public int Y { get; set; } // Automatically implemented }entspricht der folgenden Deklaration:
public class Point { private int x; private int y; public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } }Endbeispiel
Beispiel: Im folgenden Beispiel
public class ReadOnlyPoint { public int X { get; } public int Y { get; } public ReadOnlyPoint(int x, int y) { X = x; Y = y; } }entspricht der folgenden Deklaration:
public class ReadOnlyPoint { private readonly int __x; private readonly int __y; public int X { get { return __x; } } public int Y { get { return __y; } } public ReadOnlyPoint(int x, int y) { __x = x; __y = y; } }Die Zuweisungen zum schreibgeschützten Feld sind gültig, da sie innerhalb des Konstruktors auftreten.
Endbeispiel
Obwohl das Hintergrundfeld ausgeblendet ist, können diesem Feld über die property_declaration der automatisch implementierten Eigenschaft feldspezifische Attribute direkt zugewiesen werden (§15.7.1).
Beispiel: Der folgende Code
[Serializable] public class Foo { [field: NonSerialized] public string MySecret { get; set; } }führt dazu, dass das feldorientierte Attribut
NonSerializedauf das vom Compiler generierte Sicherungsfeld angewendet wird, als ob der Code wie folgt geschrieben wurde:[Serializable] public class Foo { [NonSerialized] private string _mySecretBackingField; public string MySecret { get { return _mySecretBackingField; } set { _mySecretBackingField = value; } } }Endbeispiel
15.7.5 Barrierefreiheit
Wenn ein Accessor über eine accessor_modifier verfügt, wird die Barrierefreiheitsdomäne (§7.5.3) des Accessors mithilfe der deklarierten Barrierefreiheit der accessor_modifier bestimmt. Wenn ein Accessor über keine accessor_modifier verfügt, wird die Barrierefreiheitsdomäne des Accessors anhand der deklarierten Barrierefreiheit der Eigenschaft oder des Indexers bestimmt.
Das Vorhandensein eines accessor_modifier hat keinen Einfluss auf die Suche nach Mitgliedern (§12.5) oder die Auflösung von Überlasten (§12.6.4). Die Modifizierer für die Eigenschaft oder den Indexer bestimmen immer, an welche Eigenschaft oder welchen Indexer sie unabhängig vom Kontext des Zugriffs gebunden sind.
Nachdem eine bestimmte nicht referenzwertige Eigenschaft oder ein nicht referenzwertiger Indexer ausgewählt wurde, werden die Zugriffsbereiche der beteiligten Accessoren verwendet, um festzustellen, ob diese Verwendung gültig ist.
- Wenn die Nutzung als Wert (§12.2.2) erfolgt, muss der Get-Accessor vorhanden und zugänglich sein.
- Wenn die Verwendung als Ziel einer einfachen Zuordnung (§12.22.2) dient, muss der Set-Accessor vorhanden und zugänglich sein.
- Ist die Verwendung als Ziel der Verbundzuordnung (§12.22.4) oder als Ziel der
++Operatoren--(§12.8.16, §12.9.7), müssen sowohl die Accessoren als auch der festgelegte Accessor vorhanden und zugänglich sein.
Beispiel: Im folgenden Beispiel wird die Eigenschaft
A.Textdurch die EigenschaftB.Textausgeblendet, auch in Kontexten, in denen nur der Set-Accessor aufgerufen wird. Hingegen ist die EigenschaftB.Countfür die KlasseMnicht zugänglich, sodass stattdessen die zugängliche EigenschaftA.Countverwendet wird.class A { public string Text { get => "hello"; set { } } public int Count { get => 5; set { } } } class B : A { private string text = "goodbye"; private int count = 0; public new string Text { get => text; protected set => text = value; } protected new int Count { get => count; set => count = value; } } class M { static void Main() { B b = new B(); b.Count = 12; // Calls A.Count set accessor int i = b.Count; // Calls A.Count get accessor b.Text = "howdy"; // Error, B.Text set accessor not accessible string s = b.Text; // Calls B.Text get accessor } }Endbeispiel
Sobald eine bestimmte referenzwertige Eigenschaft oder ein verweiswertiger Indexer ausgewählt wurde – egal, ob die Verwendung als Wert, als Ziel einer einfachen Zuordnung oder als Ziel einer zusammengesetzten Zuordnung erfolgt – wird die Zugriffsdomain des beteiligten Zugriffsmodifikators verwendet, um festzustellen, ob diese Verwendung gültig ist.
Ein Accessor, der zum Implementieren einer Schnittstelle verwendet wird, darf keine accessor_modifier haben. Wenn nur ein Accessor zum Implementieren einer Schnittstelle verwendet wird, kann der andere Accessor mit einem accessor_modifier deklariert werden:
Beispiel:
public interface I { string Prop { get; } } public class C : I { public string Prop { get => "April"; // Must not have a modifier here internal set {...} // Ok, because I.Prop has no set accessor } }Endbeispiel
15.7.6 Virtuelle, versiegelte, überschreibende und abstrakte Accessors
Hinweis: Diese Unterliste gilt sowohl für Eigenschaften (§15.7) als auch für Indexer (§15.9). Die Unterabschnitt wird in Bezug auf Eigenschaften geschrieben. Ersetzen Sie beim Lesen für Indexer „indexer/indexers“ durch „property/properties“, und sehen Sie sich die Liste der Unterschiede zwischen Eigenschaften und Indexern in §15.9.2 an. Hinweisende
Eine Deklaration einer virtuellen Eigenschaft gibt an, dass die Accessoren der Eigenschaft virtuell sind. Der Modifikator virtual gilt für alle nicht-privaten Accessors einer Eigenschaft. Wenn ein Accessor einer virtuellen Eigenschaft über die privateaccessor_modifier verfügt, ist der private Accessor implizit nicht virtuell.
Eine abstrakte Eigenschaftsdeklaration gibt an, dass die Accessoren der Eigenschaft virtuell sind, aber keine tatsächliche Implementierung der Accessoren bereitstellt. Stattdessen müssen nicht abstrakte abgeleitete Klassen eine eigene Implementierung für die Accessors bereitstellen, indem sie die Eigenschaft überschreiben. Da ein Accessor für eine abstrakte Eigenschaftsdeklaration keine tatsächliche Implementierung bereitstellt, besteht die accessor_body einfach aus einem Semikolon. Eine abstrakte Eigenschaft darf keinen private-Accessor haben.
Eine Eigenschaftsdeklaration, die sowohl die Modifizierer als auch die abstractoverride Eigenschaft enthält, gibt an, dass die Eigenschaft abstrakt ist und eine Basiseigenschaft überschreibt. Die Accessors einer solchen Eigenschaft sind ebenfalls abstrakt.
Abstrakte Eigenschaftendeklarationen sind nur in abstrakten Klassen (§15.2.2.2) und Schnittstellen (§19.4.4) zulässig. Die Accessors einer geerbten virtuellen Eigenschaft können in einer abgeleiteten Klasse überschrieben werden, indem eine Eigenschaftsdeklaration aufgenommen wird, die eine override-Anweisung angibt. Dies wird als überschreibende Eigenschaftsdeklaration bezeichnet. Eine überschreibende Eigenschaftsdeklaration deklariert keine neue Eigenschaft. Stattdessen ist sie einfach auf die Implementierungen der Accessoren einer vorhandenen virtuellen Eigenschaft spezialisiert.
Die Überschreibungsdeklaration und die überschriebene Basiseigenschaft müssen die gleiche deklarierte Zugänglichkeit aufweisen. Mit anderen Worten, eine Außerkraftsetzungsdeklaration ändert nichts an der Zugänglichkeit der Basiseigenschaft. Wenn die Außerkraftsetzungsbasiseigenschaft jedoch intern geschützt ist und in einer anderen Assembly als der Assembly, die die Außerkraftsetzungsdeklaration enthält, deklariert ist, ist die deklarierte Accessibility der Außerkraftsetzungsdeklaration geschützt. Wenn die geerbte Eigenschaft nur über einen einzelnen Accessor verfügt (d. h., wenn die geerbte Eigenschaft entweder schreibgeschützt oder nur schreibend ist), darf die überschriebene Eigenschaft nur diesen Accessor enthalten. Wenn die geerbte Eigenschaft beide Accessoren enthält (d. h., wenn die geerbte Eigenschaft Lese- und Schreibzugriff hat), kann die überschreibende Eigenschaft entweder einen einzelnen Accessor oder beide Accessoren enthalten. Es muss eine Identitätsumwandlung zwischen dem Typ der überschreibenden und der geerbten Eigenschaft erfolgen.
Eine überschreibende Eigenschaftsdeklaration kann den sealed Modifier enthalten. Die Verwendung dieses Modifizierers verhindert, dass eine abgeleitete Klasse die Eigenschaft weiter überschreibt. Die Accessors einer versiegelten Eigenschaft sind ebenfalls versiegelt.
Mit Ausnahme von Unterschieden bei der Deklarations- und Aufrufssyntax verhalten sich virtuelle, versiegelte, überschreibende und abstrakte Accessoren genau wie virtuelle, versiegelte, überschreibende und abstrakte Methoden. Insbesondere gelten die in §15.6.4, §15.6.5, §15.6.6 und §15.6.7 beschriebenen Regeln so, als wären Accessoren Methoden einer entsprechenden Form:
- Ein get-Accessors entspricht einer parameterlosen Methode mit einem Rückgabewert des Eigenschaftstyps und den gleichen Modifikatoren wie die enthaltende Eigenschaft.
- Ein set-Accessor entspricht einer Methode mit einem einzelnen Wertparameter des Eigenschaftstyps, einem Void-Rückgabetyp und den gleichen Modifikatoren wie die enthaltende Eigenschaft.
Beispiel: Im folgenden Code
abstract class A { int y; public virtual int X { get => 0; } public virtual int Y { get => y; set => y = value; } public abstract int Z { get; set; } }
Xist eine virtuelle Nur-Lese-Eigenschaft,Yist eine virtuelle Lese-Schreib-Eigenschaft undZist eine abstrakte Lese-Schreib-Eigenschaft. DaZabstrakt ist, muss die enthaltende Klasse A ebenfalls als abstrakt deklariert werden.Eine Klasse, die von
Ader abgeleitet wird, wird unten gezeigt:class B : A { int z; public override int X { get => base.X + 1; } public override int Y { set => base.Y = value < 0 ? 0: value; } public override int Z { get => z; set => z = value; } }Hier sind die Deklarationen von
X,YundZübergeordnete Eigenschaftsdeklarationen. Jede Eigenschaftsdeklaration stimmt exakt mit den Zugriffsmodifizierern, dem Typ und dem Namen der entsprechenden geerbten Eigenschaft überein. Der get-Accessors vonXund der set-Accessors vonYverwenden das Basis-Schlüsselwort, um auf die geerbten Accessors zuzugreifen. Die Deklaration vonZsetzt beide abstrakten Accessoren außer Kraft. Daher gibt es keine ausstehendenabstract-Funktionsmitglieder inB, undBdarf eine nicht-abstrakte Klasse sein.Endbeispiel
Wenn eine Eigenschaft als Außerkraftsetzung deklariert wird, müssen alle außer Kraft gesetzten Accessors auf den Außerkraftsetzungscode zugreifen können. Darüber hinaus muss die deklarierte Accessibility sowohl der Eigenschaft oder des Indexers selbst als auch der Accessors mit der des überschriebenen Mitglieds und der Accessors übereinstimmen.
Beispiel:
public class B { public virtual int P { get {...} protected set {...} } } public class D: B { public override int P { get {...} // Must not have a modifier here protected set {...} // Must specify protected here } }Endbeispiel
15.8 Veranstaltungen
15.8.1 Allgemein
Ein Ereignis ist ein Member, der es einem Objekt oder einer Klasse ermöglicht, Benachrichtigungen bereitzustellen. Clients können Ereignissen ausführbaren Code hinzufügen, indem Sie Ereignishandler bereitstellen.
Ereignisse werden mit event_declarations deklariert:
event_declaration
: attributes? event_modifier* 'event' type variable_declarators ';'
| attributes? event_modifier* 'event' type member_name
'{' event_accessor_declarations '}'
;
event_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
event_accessor_declarations
: add_accessor_declaration remove_accessor_declaration
| remove_accessor_declaration add_accessor_declaration
;
add_accessor_declaration
: attributes? 'add' block
;
remove_accessor_declaration
: attributes? 'remove' block
;
unsafe_modifier (§24.2) ist nur im unsicheren Code (§24) verfügbar.
Ein event_declaration kann eine Reihe von Attributen (§23) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), (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. "
Darüber hinaus kann ein event_declaration, das direkt in einer struct_declaration enthalten ist, den readonly Modifikator (§16.4.12) umfassen.
Ereignisdeklarationen unterliegen den gleichen Regeln wie Methodendeklarationen (§15.6) in Bezug auf gültige Kombinationen von Modifizierern.
Die Art einer Ereigniserklärung muss eine delegate_type (§8.2.8) sein und dass delegate_type mindestens so zugänglich sein muss wie das Ereignis selbst (§7.5.5).
Eine Ereigniserklärung kann event_accessor_declarations enthalten. Wenn dies jedoch nicht der Fall ist, muss der Compiler für nicht externe, nicht abstrakte Ereignisse diese automatisch bereitstellen (§15.8.2); für extern-Ereignisse werden die Accessoren extern bereitgestellt.
Eine Ereignisdeklaration, die event_accessor_declarations auslässt, definiert ein oder mehrere Ereignisse – eines für jeden variable_declarator. Die Attribute und Modifizierer gelten für alle Mitglieder, die durch eine event_declaration deklariert wurden.
Es handelt sich um einen Kompilierzeitfehler, wenn eine event_declaration sowohl den abstract Modifizierer als auch event_accessor_declarations einschließt.
Wenn eine Ereignisdeklaration einen extern Modifizierer enthält, wird das Ereignis als externes Ereignis bezeichnet. Da eine externe Ereignisdeklaration keine tatsächliche Implementierung bereitstellt, handelt es sich um einen Fehler, der sowohl den extern Modifizierer als auch event_accessor_declarationenthält.
Es ist ein Kompilierzeitfehler, wenn ein variabler_deklarator einer Ereignisdeklaration mit einem abstract oder external Modifikator einen variablen_initializerenthält.
Ein Ereignis kann als linker Operand der Operatoren += und -= verwendet werden. Diese Operatoren werden verwendet, um Ereignishandler anzufügen oder Ereignishandler aus einem Ereignis zu entfernen, und die Zugriffsmodifizierer des Ereignisses steuern die Kontexte, in denen solche Vorgänge zulässig sind.
Die einzigen Vorgänge, die für ein Ereignis durch Code zulässig sind, der sich außerhalb des Typs befindet, in dem dieses Ereignis deklariert wird, sind += und -=. Daher kann ein solcher Code Handler für ein Ereignis hinzufügen und entfernen, nicht direkt die zugrunde liegende Liste der Ereignishandler abrufen oder ändern.
In 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.22.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
ButtonKlasse 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
LoginDialogInstanzkonstruktor zweiButtonInstanzen und fügt Ereignishandler an dieClickEreignisse 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 (§21), der sich auf die Liste der Ereignishandler bezieht, die dem Ereignis hinzugefügt wurden. Wenn keine Ereignishandler hinzugefügt wurden, enthält das Feld null.
Beispiel: Im folgenden Code
public delegate void EventHandler(object sender, EventArgs e); public class Button : Control { public event EventHandler Click; protected void OnClick(EventArgs e) { EventHandler handler = Click; if (handler != null) { handler(this, e); } } public void Reset() => Click = null; }
Clickwird als Feld innerhalb derButtonKlasse verwendet. Wie das Beispiel zeigt, kann das Feld untersucht, geändert und in Stellvertretungsaufrufausdrücken verwendet werden. DieOnClickMethode in derButtonKlasse "löst" dasClickEreignis aus. Das Auslösen eines Ereignisses entspricht exakt dem Aufrufen des Delegaten, der durch das Ereignis repräsentiert wird, es gibt deshalb keine besonderen Sprachkonstrukte zum Auslösen von Ereignissen. Beachten Sie, dass dem Delegat-Aufruf eine Überprüfung vorangestellt wird, die sicherstellt, dass das Delegat nicht Null ist und dass die Überprüfung auf einer lokalen Kopie durchgeführt wird, um die Threadsicherheit zu gewährleisten.Außerhalb der Deklaration der
ButtonKlasse kann dasClickMitglied nur auf der linken Seite der+=und–=Operatoren verwendet werden, wie in ...b.Click += new EventHandler(...);die einen Delegaten an die Aufrufliste des Ereignisses
Clickanhängt, undClick –= new EventHandler(...);wodurch ein Delegat aus der Aufrufliste des
ClickEreignisses entfernt wird.Endbeispiel
Beim Kompilieren eines feldähnlichen Ereignisses erstellt ein Compiler automatisch Speicherplatz für den Delegaten und erstellt Accessoren für das Ereignis, die Ereignishandler dem Delegatfeld hinzufügen oder entfernen. Die Hinzufügungs- und Entfernungsoperationen sind thread-sicher und können (müssen aber nicht) durchgeführt werden, während die Sperre (§13.13) auf das enthaltende Objekt für ein Instanzereignis oder das System.Type Objekt (§12.8.18) für ein statisches Ereignis gehalten wird.
Hinweis: Eine Instanzereignisdeklaration des Formulars:
class X { public event D Ev; }muss so zusammengestellt werden, dass es Folgendes entspricht:
class X { private D __Ev; // field to hold the delegate public event D Ev { add { /* Add the delegate in a thread safe way */ } remove { /* Remove the delegate in a thread safe way */ } } }Innerhalb der Klasse
Xführen Verweise aufEvauf der linken Seite der Operatoren+=und–=dazu, dass die Accessoren zum Hinzufügen und Entfernen aufgerufen werden. Alle anderen Verweise aufEvwerden so kompiliert, dass sie stattdessen auf das ausgeblendete Feld__Evverweisen (§12.8.7). Der Name "__Ev" ist beliebig; das ausgeblendete Feld könnte überhaupt einen Namen oder keinen Namen haben.Hinweisende
15.8.3 Ereignis-Accessors
Hinweis: Ereignisdeklarationen lassen in der Regel event_accessor_declarations aus, wie im
Buttonobigen Beispiel gezeigt. Sie können beispielsweise einbezogen werden, wenn die Speicherkosten eines Felds pro Ereignis nicht zulässig sind. In solchen Fällen kann eine Klasse event_accessor_declarationenthalten und einen privaten Mechanismus zum Speichern der Liste der Ereignishandler verwenden. Hinweisende
Die event_accessor_declarations eines Ereignisses geben die ausführbaren Anweisungen an, die dem Hinzufügen und Entfernen von Ereignishandlern zugeordnet sind.
Die Accessordeklarationen bestehen aus einem add_accessor_declaration und einem remove_accessor_declaration. Jede Accessordeklaration besteht aus dem Token-Hinzufügen oder Entfernen gefolgt von einem Block. Der einem add_accessor_declaration zugeordnete Block gibt die auszuführenden Anweisungen an, wenn ein Ereignishandler hinzugefügt wird, und der einem remove_accessor_declaration zugeordnete Block gibt die auszuführenden Anweisungen an, wenn ein Ereignishandler entfernt wird.
Jede add_accessor_declaration und remove_accessor_declaration entspricht einer Methode mit einem einzelnen Wertparameter des Ereignistyps und einem void Rückgabetyp. Der implizite Parameter eines Ereignisaccessors wird benannt value. Wenn ein Ereignis in einer Ereigniszuweisung verwendet wird, wird der entsprechende Ereignis-Accessor verwendet. Wenn der Zuordnungsoperator += ist, wird der Hinzufügen-Accessor verwendet. Wenn der Zuordnungsoperator –= ist, wird der Entfernen-Accessor verwendet. In beiden Fällen wird der rechte Operand des Zuweisungsoperators als Argument für den Ereignis-Accessor verwendet. Der Block einer add_accessor_declaration oder einer remove_accessor_declaration muss den Regeln für void Methoden entsprechen, wie in §15.6.9 beschrieben. Insbesondere dürfen die return -Anweisungen in einem solchen Block keinen Ausdruck angeben.
Da ein Ereignisaccessor implizit einen Parameter mit dem Namen valuehat, handelt es sich um einen Kompilierungszeitfehler für eine lokale Variable oder Konstante, die in einem Ereignisaccessor deklariert ist, um diesen Namen zu haben.
Beispiel: Im folgenden Code
class Control : Component { // Unique keys for events static readonly object mouseDownEventKey = new object(); static readonly object mouseUpEventKey = new object(); // Return event handler associated with key protected Delegate GetEventHandler(object key) {...} // Add event handler associated with key protected void AddEventHandler(object key, Delegate handler) {...} // Remove event handler associated with key protected void RemoveEventHandler(object key, Delegate handler) {...} // MouseDown event public event MouseEventHandler MouseDown { add { AddEventHandler(mouseDownEventKey, value); } remove { RemoveEventHandler(mouseDownEventKey, value); } } // MouseUp event public event MouseEventHandler MouseUp { add { AddEventHandler(mouseUpEventKey, value); } remove { RemoveEventHandler(mouseUpEventKey, value); } } // Invoke the MouseUp event protected void OnMouseUp(MouseEventArgs args) { MouseEventHandler handler; handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey); if (handler != null) { handler(this, args); } } }die
ControlKlasse implementiert einen internen Speichermechanismus für Ereignisse. DieAddEventHandler-Methode ordnet einen Delegatwert einem Schlüssel zu, dieGetEventHandler-Methode gibt den Delegaten zurück, der derzeit einem Schlüssel zugeordnet ist, und dieRemoveEventHandler-Methode entfernt einen Delegaten als Ereignishandler für das angegebene Ereignis. Vermutlich ist der zugrunde liegende Speichermechanismus so konzipiert, dass es keine Kosten für das Zuordnen eines Nulldelegatwerts zu einem Schlüssel gibt, und somit verbrauchen unbehandelte Ereignisse keinen Speicher.Endbeispiel
15.8.4 Statische Ereignisse und Instanzereignisse
Wenn eine Ereignisdeklaration einen static Modifizierer enthält, wird das Ereignis als statisches Ereignis bezeichnet. Wenn kein static Modifizierer vorhanden ist, wird das Ereignis als Instanzereignis bezeichnet.
Ein statisches Ereignis ist keiner bestimmten Instanz zugeordnet, und es ist ein Fehler zur Kompilierungszeit, in den Accessoren eines statischen Ereignisses auf this zu verweisen.
Ein Instanzereignis ist einer bestimmten Instanz einer Klasse zugeordnet, und auf diese Instanz kann in den Accessoren dieses Ereignisses als this (§12.8.14) zugegriffen werden.
Die Unterschiede zwischen statischen und Instanzmitgliedern werden in §15.3.8 weiter erörtert.
15.8.5 Virtuelle, versiegelte, überschreibende und abstrakte Accessors
Eine virtuelle Ereignisdeklaration gibt an, dass die Accessoren dieses Ereignisses virtuell sind. Der Modifikator virtual gilt für beide Accessors eines Ereignisses.
Eine abstrakte Ereignisdeklaration gibt an, dass die Accessoren des Ereignisses virtuell sind, aber keine tatsächliche Implementierung der Accessoren bereitstellt. Stattdessen müssen nicht abstrakte abgeleitete Klassen eine eigene Implementierung für die Accessors bereitstellen, indem sie das Ereignis überschreiben. Da ein Accessor für eine abstrakte Ereignisdeklaration keine tatsächliche Implementierung bereitstellt, stellt er event_accessor_declarations nicht bereit.
Eine Ereignisdeklaration, die sowohl die abstract- als auch die override-Modifizierer enthält, gibt an, dass das Ereignis abstrakt ist und ein Basisereignis überschreibt. Die Teilnehmer eines solchen Ereignisses sind ebenfalls abstrakt.
Abstrakte Ereignisdeklarationen sind nur in abstrakten Klassen (§15.2.2.2) und Schnittstellen (§19.4.5) zulässig.
Die Accessoren eines geerbten virtuellen Ereignisses können in einer abgeleiteten Klasse überschrieben werden, indem eine Ereignisdeklaration eingeschlossen wird, die einen override Modifizierer angibt. Dies wird als Deklaration eines überschreibenden Ereignisses bezeichnet. Eine überschreibende Ereignisdeklaration deklariert kein neues Ereignis. Stattdessen ist es einfach auf die Implementierungen der Accessoren eines vorhandenen virtuellen Ereignisses spezialisiert.
Eine überschreibende Ereignisdeklaration muss genau die gleichen Zugriffsmodifizierer und den Namen wie das überschriebenes Ereignis angeben, es muss eine Identitätskonvertierung zwischen dem Typ des überschreibenden und des überschriebenen Ereignisses vorhanden sein, und sowohl die add- als auch die remove-Zugriffsoperatoren müssen in der Deklaration angegeben werden.
Eine überschreibende Ereignisdeklaration kann den sealed Modifizierer enthalten. Die Verwendung des this Modifizierers verhindert, dass eine abgeleitete Klasse das Ereignis weiter überschreibt. Die Accessors eines versiegelten Ereignis sind ebenfalls versiegelt.
Es ist ein Kompilierungszeitfehler, wenn eine überschreibende Ereignisdeklaration einen new Modifizierer enthält.
Mit Ausnahme von Unterschieden bei der Deklarations- und Aufrufssyntax verhalten sich virtuelle, versiegelte, überschreibende und abstrakte Accessoren genau wie virtuelle, versiegelte, überschreibende und abstrakte Methoden. Insbesondere gelten die in §15.6.4, §15.6.5, §15.6.6 und §15.6.7 beschriebenen Regeln, als wären Accessoren Methoden eines entsprechenden Formulars. Jeder Accessor entspricht einer Methode mit einem einzelnen Wertparameter des Ereignistyps, einem void Rückgabetyp und denselben Modifizierern wie das enthaltende Ereignis.
15.9 Indexer
15.9.1 Allgemein
Ein Indexer ist ein Element, mit dem ein Objekt auf die gleiche Weise indiziert werden kann wie ein Array. Indexer werden mit indexer_declarations deklariert:
indexer_declaration
: attributes? indexer_modifier* indexer_declarator indexer_body
| attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
;
indexer_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
indexer_declarator
: type 'this' '[' parameter_list ']'
| type interface_type '.' 'this' '[' parameter_list ']'
;
indexer_body
: '{' accessor_declarations '}'
| '=>' expression ';'
;
ref_indexer_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§24.2) ist nur im unsicheren Code (§24) verfügbar.
Eine indexer_declaration kann eine Reihe von Attributen (§23) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), der new (§15.3.5), (virtual enthalten.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7) und extern (§15.6.8) Modifizierer. Zusätzlich kann ein indexer_declaration, der direkt in einem struct_declaration enthalten ist, den readonly Modifizierer (§16.4.12) enthalten.
- Die erste deklariert einen Indexer ohne Referenzwert. Sein Wert hat den Typ Typ. Diese Art von Indexer kann lesbar und/oder schreibbar sein.
- Die zweite deklariert einen Referenzwertindexer. Sein Wert ist eine Variablenreferenz (§9.5), die
readonlysein kann, auf eine Variable vom Typ Typ. Diese Art von Indexer ist nur lesbar.
Ein indexer_declaration kann eine Reihe von Attributen (§23) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), der new (§15.3.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 Begriff Elementtyp, wie er für ein Array definiert ist, auch mit einem Indexer verwendet. Hinweisende
Sofern der Indexer keine explizite Schnittstellenmemberimplementierung ist, folgt dem Typ das Schlüsselwort this. Für eine explizite Schnittstellenmemberimplementierung folgt auf den `type` ein `interface_type`, ein „.“, und das Schlüsselwort `this`. Im Gegensatz zu anderen Mitgliedern haben Indexer keine benutzerdefinierten Namen.
Die parameter_list gibt die Parameter des Indexers an. Die Parameterliste eines Indexers entspricht der einer Methode (§15.6.2), mit der Ausnahme, dass mindestens ein Parameter angegeben werden muss und dass die thisModifizierer refout und Parametermodifizierer nicht zulässig sind.
Der Typ eines Indexers und jeder der typen, auf die in der parameter_list verwiesen wird, muss mindestens so zugänglich sein wie der Indexer selbst (§7.5.5).
Ein indexer_body kann entweder aus einem Anweisungstext (§15.7.1) oder einem Ausdruckstext (§15.6.1) bestehen. In einem Anweisungstext deklarieren accessor_declarations, die in „{“ und „}“ Token eingeschlossen sein müssen, die Accessoren (§15.7.3) des Indexers. Die Accessoren geben die ausführbaren Anweisungen an, die dem Lesen und Schreiben von Indexerelementen zugeordnet sind.
In einem indexer_body ist ein Ausdruckstext, der aus „=>“, gefolgt von einem Ausdruck E und einem Semikolon besteht, genau gleichbedeutend mit dem Anweisungstext { get { return E; } } und kann daher nur verwendet werden, um schreibgeschützte Indexer anzugeben, bei denen das Ergebnis des get-Accessors durch einen einzelnen Ausdruck angegeben wird.
Ein ref_indexer_body kann entweder aus einem Anweisungskörper oder einem Ausdruckskörper bestehen. In einem Anweisungsblock deklariert eine get_accessor_declaration den Get-Accessor (§15.7.3) des Indexers. Der Accessor gibt die ausführbaren Anweisungen an, die mit dem Lesen des Indexers verknüpft sind.
In einem ref_indexer_body ist ein Ausdruckskörper, bestehend aus =>, gefolgt von ref, einem variablen_verweisV und einem Semikolon, genau gleichwertig mit dem Anweisungskörper { get { return ref V; } }.
Hinweis: Obwohl die Syntax für den Zugriff auf ein Indexerelement mit dem für ein Arrayelement identisch ist, wird ein Indexerelement nicht als Variable klassifiziert. Daher ist es nicht möglich, ein Indexerelement als
in,outoderrefArgument zu übergeben, es sei denn, der Indexer ist ref-valued und gibt daher einen Verweis zurück (§9.7). Hinweisende
Die parameter_list eines Indexers definiert die Signatur (§7.6) des Indexers. Insbesondere besteht die Signatur eines Indexers aus der Anzahl und den Typen seiner Parameter. Der Elementtyp und die Namen der Parameter sind nicht Teil der Signatur eines Indexers.
Die Signatur eines Indexers unterscheidet sich von den Signaturen aller anderen in derselben Klasse deklarierten Indexer.
Wenn eine Indexerdeklaration einen extern Modifizierer enthält, wird der Indexer als externer Indexer bezeichnet. Da eine externe Indexerdeklaration keine tatsächliche Implementierung bereitstellt, muss jedes accessor_body in seinen accessor_declarations ein Semikolon sein.
Beispiel: Im folgenden Beispiel wird eine
BitArrayKlasse 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
BitArrayKlasse verbraucht wesentlich weniger Arbeitsspeicher als eine entsprechendebool[](da jeder Wert des Ersteren nur ein Bit anstelle der Letztenbytebelegt), aber sie erlaubt die gleichen Operationen wie einbool[].Die folgende
CountPrimesKlasse verwendet einenBitArrayund 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
BitArraygenau wie für einbool[]ist.Das folgende Beispiel zeigt eine 26×10-Rasterklasse mit einem Indexer mit zwei Parametern. Der erste Parameter muss ein Groß- oder Kleinbuchstabe im Bereich A–Z sein, und die zweite muss eine ganze Zahl im Bereich von 0 bis 9 sein.
class Grid { const int NumRows = 26; const int NumCols = 10; int[,] cells = new int[NumRows, NumCols]; public int this[char row, int col] { get { row = Char.ToUpper(row); if (row < 'A' || row > 'Z') { throw new ArgumentOutOfRangeException("row"); } if (col < 0 || col >= NumCols) { throw new ArgumentOutOfRangeException ("col"); } return cells[row - 'A', col]; } set { row = Char.ToUpper(row); if (row < 'A' || row > 'Z') { throw new ArgumentOutOfRangeException ("row"); } if (col < 0 || col >= NumCols) { throw new ArgumentOutOfRangeException ("col"); } cells[row - 'A', col] = value; } } }Endbeispiel
15.9.2 Indexer- und Eigenschaftsunterschiede
Indexer und Eigenschaften sind im Konzept sehr ähnlich, unterscheiden sich jedoch auf die folgenden Arten:
- Eine Eigenschaft wird anhand ihres Namens identifiziert, während ein Indexer anhand seiner Signatur identifiziert wird.
- Auf eine Eigenschaft wird über eine simple_name (§12.8.4) oder eine member_access (§12.8.7) zugegriffen, während über ein Indexerelement über eine element_access (§12.8.12.4) 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 Namenvalue. - Es ist ein Kompilierungszeitfehler, wenn ein Indexer-Accessor eine lokale Variable oder lokale Konstante mit demselben Namen wie ein Indexerparameter deklariert.
- Bei einer überschreibenden Eigenschaftsdeklaration wird mithilfe der Syntax
base.Pauf die geerbte Eigenschaft zugegriffen, wobeiPder Eigenschaftsname angegeben ist. In einer überschreibenden Indexerdeklaration wird mithilfe der Syntaxbase[E]auf den geerbten Indexer zugegriffen, wobeiEes sich um eine durch Trennzeichen getrennte Liste von Ausdrücken handelt. - Es gibt kein Konzept für einen "automatisch implementierten Indexer". Es ist ein Fehler, wenn ein nicht abstrakter, nicht externer Indexer mit Semikolon accessor_bodys vorhanden ist.
Abgesehen von diesen Unterschieden gelten alle in §15.7.3, §15.7.5 und §15.7.6 definierten Regeln sowohl für Indexer-Accessoren als auch für Eigenschaftsaccessoren.
Diese Ersetzung von Eigenschaft/Eigenschaften durch Indexer/Indexer beim Lesen von §15.7.3, §15.7.5 und §15.7.6 gilt auch für definierte Begriffe. Insbesondere wird Lesen-Schreiben-Eigenschaft zu Lesen-Schreiben-Indexer, Nur-Lesen-Eigenschaft wird zu Nur-Lesen-Indexer, und Nur-Schreiben-Eigenschaft wird zu Nur-Schreiben-Indexer.
15.10 Operatoren
15.10.1 Allgemein
Ein Operator ist ein Element, das die Bedeutung eines Ausdrucksoperators definiert, der auf Instanzen der Klasse angewendet werden kann. Operatoren werden mit operator_declarations deklariert:
operator_declaration
: attributes? operator_modifier+ operator_declarator operator_body
;
operator_modifier
: 'public'
| 'static'
| 'extern'
| unsafe_modifier // unsafe code support
;
operator_declarator
: unary_operator_declarator
| binary_operator_declarator
| conversion_operator_declarator
;
unary_operator_declarator
: type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
;
logical_negation_operator
: '!'
;
overloadable_unary_operator
: '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
;
binary_operator_declarator
: type 'operator' overloadable_binary_operator
'(' fixed_parameter ',' fixed_parameter ')'
;
overloadable_binary_operator
: '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '<<'
| right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
;
conversion_operator_declarator
: 'implicit' 'operator' type '(' fixed_parameter ')'
| 'explicit' 'operator' type '(' fixed_parameter ')'
;
operator_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§24.2) ist nur im unsicheren Code (§24) verfügbar.
Hinweis: Die präfixe logische Negation (§12.9.4) und die postfixen nullvergebenden Operatoren (§12.8.9) werden zwar durch dasselbe lexikalische Token (!) repräsentiert, sind aber unterschiedlich. Letzterer ist kein überlasteter Operator.
Hinweisende
Es gibt drei Kategorien überladener Operatoren: Unäre Operatoren (§15.10.2), binäre Operatoren (§15.10.3) und Konvertierungsoperatoren (§15.10.4).
Die operator_body ist entweder ein Semikolon, ein Blocktext (§15.6.1) oder ein Ausdruckstext (§15.6.1). Ein Blockkörper besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn der Operator aufgerufen wird. Der Block muss den Regeln für wertzurückgebende Methoden entsprechen, die in §15.6.11 beschrieben sind. Ein Ausdruckskörper besteht aus =>, gefolgt von einem Ausdruck und einem Semikolon und gibt einen einzelnen Ausdruck an, der ausgeführt werden soll, wenn der Operator aufgerufen wird.
Bei extern Operatoren besteht die operator_body einfach aus einem Semikolon. Bei allen anderen Operatoren ist die operator_body entweder ein Blocktext oder ein Ausdruckstext.
Die folgenden Regeln gelten für alle Operatordeklarationen:
- Eine Operator-Deklaration enthält sowohl einen
publicals auch einenstaticModifizierer. - Die Parameter eines Operators dürfen keine anderen Modifizierer als
inhaben. - Die Signatur eines Betreibers (§15.10.2, §15.10.3, §15.10.4) unterscheidet sich von den Signaturen aller anderen Betreiber, die in derselben Klasse deklariert sind.
- Alle typen, auf die in einer Betreibererklärung verwiesen wird, sind mindestens so zugänglich wie der Betreiber selbst (§7.5.5).
- Es handelt sich um einen Fehler für denselben Modifizierer, der mehrmals in einer Operatordeklaration angezeigt wird.
Jede Operatorkategorie legt zusätzliche Einschränkungen fest, wie in den folgenden Unterlisten beschrieben.
Wie andere Member werden in einer Basisklasse deklarierte Operatoren von abgeleiteten Klassen geerbt. Da Operatordeklarationen immer die Klasse oder Anweisung erfordern, an der der Operator deklariert wird, um an der Signatur des Operators teilzunehmen, ist es nicht möglich, dass ein in einer abgeleiteten Klasse deklarierter Operator einen in einer Basisklasse deklarierten Operator ausblenden kann. Daher ist der new Modifizierer niemals erforderlich und daher in einer Operatordeklaration nie zulässig.
Weitere Informationen zu unären und binären Operatoren finden Sie unter §12.4.
Weitere Informationen zu Konvertierungsoperatoren finden Sie unter §10.5.
15.10.2 Unäre Operatoren
Die folgenden Regeln gelten für unäre Operatordeklarationen, wobei T der Instanztyp der Klasse oder Struktur, die die Operatordeklaration enthält, bezeichnet wird:
- Ein unärer
+,-,!(nur logische Negation) oder~-Operator muss einen einzelnen Parameter vom TypToderT?annehmen und kann einen beliebigen Typ zurückgeben. - Ein unärer
++- oder---Operator muss einen einzelnen Parameter des TypsToderT?annehmen und denselben Typ oder einen davon abgeleiteten Typ zurückgeben. - Ein unärer
true- oderfalse-Operator soll einen einzigen Parameter vom TypToderT?annehmen und den Typboolzurückgeben.
Die Signatur eines unären Operators besteht aus dem Operatortoken (+, , -!~++--trueoder false) und dem Typ des einzelnen Parameters. Der Rückgabetyp ist weder Teil der Signatur eines unären Operators noch der Name des Parameters.
Für die true und false unäre Operatoren ist eine paarweise Deklaration erforderlich. Wenn eine Klasse einen dieser Operatoren deklariert, ohne die andere zu deklarieren, tritt ein Kompilierungszeitfehler auf. Die Betreiber und true Betreiber false werden in §12.25 weiter beschrieben.
Beispiel: Das folgende Beispiel zeigt eine Implementierung und nachfolgende Verwendung von Operator++ für eine ganzzahlige Vektorklasse:
public class IntVector { public IntVector(int length) {...} public int Length { get { ... } } // Read-only property public int this[int index] { get { ... } set { ... } } // Read-write indexer public static IntVector operator++(IntVector iv) { IntVector temp = new IntVector(iv.Length); for (int i = 0; i < iv.Length; i++) { temp[i] = iv[i] + 1; } return temp; } } class Test { static void Main() { IntVector iv1 = new IntVector(4); // Vector of 4 x 0 IntVector iv2; iv2 = iv1++; // iv2 contains 4 x 0, iv1 contains 4 x 1 iv2 = ++iv1; // iv2 contains 4 x 2, iv1 contains 4 x 2 } }Beachten Sie, wie die Operatormethode den wert zurückgibt, der durch Hinzufügen von 1 zum Operanden erzeugt wird, genau wie die Postfix-Inkrement- und Dekrementoperatoren (§12.8.16) und die Präfixinkrementierungs- und Dekrementoperatoren (§12.9.7). Im Gegensatz zu C++ sollte diese Methode den Wert des Operanden nicht direkt ändern, da dies gegen die Standardsemantik des Postfix-Inkrementoperators (§12.8.16) verstößt.
Endbeispiel
15.10.3 Binäre Operatoren
Die folgenden Regeln gelten für binäre Operatordeklarationen, wobei T der Instanztyp der Klasse oder Struktur, die die Operatordeklaration enthält, bezeichnet wird:
- Ein binärer Nicht-Schiebeoperator muss zwei Parameter annehmen, von denen mindestens einer den Typ
ToderT?haben soll, und kann einen beliebigen Typ zurückgeben. - Ein Binärer
<<oder>>Operator (§12.12) muss zwei Parameter annehmen, von denen der Erste Typ oder das zweite typTT?oder das zweite Typintaufweisen soll oder oderint?oder, 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, einschließlich Funktionsmitgliedsaufrufen, Cast-Ausdrücken und Zuweisungen. Dies wird weiter in §10.2 beschrieben.
Eine Konvertierungsoperatordeklaration, die das explicit Schlüsselwort enthält, führt eine benutzerdefinierte explizite Konvertierung ein. Explizite Konvertierungen können in Cast-Ausdrücken auftreten und werden in §10.3näher beschrieben.
Ein Konvertierungsoperator konvertiert von einem Quelltyp, der durch den Parametertyp des Konvertierungsoperators angegeben ist, in einen Zieltyp, der durch den Rückgabetyp des Konvertierungsoperators angegeben wird.
Für einen bestimmten Quelltyp S und Zieltyp T, wenn S oder T nullable Werttypen sind, lassen S₀ und T₀ auf ihre zugrunde liegenden Typen verweisen; andernfalls sind S₀ und T₀ gleich S und T. Eine Klasse oder Struktur darf eine Konvertierung von einem Quelltyp in einen Zieltyp ST nur deklarieren, wenn alle folgenden Werte zutreffen:
S₀undT₀sind unterschiedliche Typen.Entweder
S₀oderT₀ist der Instanztyp der Klasse oder Struktur, die die Operatordeklaration enthält.Weder
S₀nochT₀ist ein interface_type.Ohne benutzerdefinierte Konvertierungen existiert keine Konvertierung von
SnachToder vonTnachS.
Für die Zwecke dieser Regeln gelten alle Typparameter, die mit S oder T verknüpft sind, als eindeutige Typen, die keine Vererbungsbeziehung mit anderen Typen haben, und alle Einschränkungen für diese Typparameter werden ignoriert.
Beispiel: Im Folgenden:
class C<T> {...} class D<T> : C<T> { public static implicit operator C<int>(D<T> value) {...} // Ok public static implicit operator C<string>(D<T> value) {...} // Ok public static implicit operator C<T>(D<T> value) {...} // Error }Die ersten beiden Operatordeklarationen sind zulässig, da
Tundintsowiestringals eindeutige Typen ohne Beziehung zueinander betrachtet werden. Der dritte Operator ist jedoch ein Fehler, daC<T>die Basisklasse vonD<T>ist.Endbeispiel
Aus der zweiten Regel folgt, dass ein Umrechnungsoperator entweder in oder aus der Klasse oder dem Strukturtyp konvertiert wird, in den der Operator deklariert wird.
Beispiel: Es ist möglich, dass ein Klassen- oder Strukturtyp
Ceine Konvertierung vonCzuintund vonintzuCdefiniert, aber nicht vonintzubool. Endbeispiel
Es ist nicht möglich, eine vordefinierte Konvertierung direkt neu zu definieren. Daher dürfen Konvertierungsoperatoren nicht von oder in object konvertieren, da implizite und explizite Konvertierungen bereits zwischen object und allen anderen Typen vorhanden sind. Ebenso kann weder die Quelle noch die Zieltypen einer Konvertierung ein Basistyp des anderen sein, da dann bereits eine Konvertierung vorhanden wäre. Es ist jedoch möglich, Operatoren für generische Typen zu deklarieren, die für bestimmte Typargumente Konvertierungen angeben, die bereits als vordefinierte Konvertierungen vorhanden sind.
Beispiel:
struct Convertible<T> { public static implicit operator Convertible<T>(T value) {...} public static explicit operator T(Convertible<T> value) {...} }wenn der Typ
objectals TypargumentTangegeben 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
Szum TypTvorhanden ist, werden alle benutzerdefinierten Konvertierungen (implizit oder explizit) vonSzuTignoriert. - Wenn eine vordefinierte explizite Konvertierung (§10.3) vom Typ
Szum TypTvorhanden ist, werden alle benutzerdefinierten expliziten Konvertierungen vonSzuT" ignoriert". Außerdem:- Wenn entweder
SoderTSchnittstellentypen sind, werden benutzerdefinierte implizite Konvertierungen vonSzuTignoriert. - Andernfalls werden benutzerdefinierte implizite Konvertierungen von
SzuTweiterhin berücksichtigt.
- Wenn entweder
Für alle Typen außer objectstehen die vom obigen Typ Convertible<T> deklarierten Operatoren nicht im Widerspruch zu vordefinierten Konvertierungen.
Beispiel:
void F(int i, Convertible<int> n) { i = n; // Error i = (int)n; // User-defined explicit conversion n = i; // User-defined implicit conversion n = (Convertible<int>)i; // User-defined implicit conversion }Für den Typ
objectverbergen die vordefinierten Konvertierungen jedoch die benutzerdefinierten Konvertierungen in allen Fällen außer einem:void F(object o, Convertible<object> n) { o = n; // Pre-defined boxing conversion o = (object)n; // Pre-defined boxing conversion n = o; // User-defined implicit conversion n = (Convertible<object>)o; // Pre-defined unboxing conversion }Endbeispiel
Benutzerdefinierte Konvertierungen sind nicht erlaubt, um von oder nach interface_types zu konvertieren. Insbesondere stellt diese Einschränkung sicher, dass beim Konvertieren in einen interface_type keine benutzerdefinierten Transformationen auftreten und dass eine Konvertierung in einen interface_type nur erfolgreich ist, wenn das konvertierte object tatsächlich den angegebenen interface_type implementiert.
Die Signatur eines Konvertierungsoperators besteht aus dem Quelltyp und dem Zieltyp. (Dies ist die einzige Form des Mitglieds, für das der Rückgabetyp an der Signatur teilnimmt.) Die implizite oder explizite Klassifizierung eines Konvertierungsoperators ist nicht Teil der Signatur des Operators. Daher kann eine Klasse oder Struktur nicht sowohl einen impliziten als auch einen expliziten Konvertierungsoperator mit denselben Quell- und Zieltypen deklarieren.
Hinweis: Im Allgemeinen sollten benutzerdefinierte implizite Konvertierungen so konzipiert werden, dass keine Ausnahmen ausgelöst werden und niemals Informationen verloren gehen. Wenn eine benutzerdefinierte Konvertierung Ausnahmen verursachen kann (z. B. weil das Quellargument außerhalb des Zulässigen liegt) oder Verlust von Informationen (z. B. Verwerfen von Bits mit hoher Reihenfolge), sollte diese Konvertierung als explizite Konvertierung definiert werden. Hinweisende
Beispiel: Im folgenden Code
public struct Digit { byte value; public Digit(byte value) { if (value < 0 || value > 9) { throw new ArgumentException(); } this.value = value; } public static implicit operator byte(Digit d) => d.value; public static explicit operator Digit(byte b) => new Digit(b); }die Konvertierung von
Digitinbyteist implizit, da sie niemals Ausnahmen auslöst oder Informationen verliert, während die Konvertierung vonbytezuDigitexplizit ist, weilDigitnur eine Teilmenge der möglichen Werte einesbytedarstellen kann.Endbeispiel
15.11 Instanzkonstruktoren
15.11.1 Allgemein
Ein Instanzkonstruktor ist ein Member, der die erforderlichen Aktionen zum Initialisieren einer Instanz einer Klasse implementiert. Instanzkonstruktoren werden mit constructor_declarations deklariert:
constructor_declaration
: attributes? constructor_modifier* constructor_declarator constructor_body
;
constructor_modifier
: 'public'
| 'protected'
| 'internal'
| 'private'
| 'extern'
| unsafe_modifier // unsafe code support
;
constructor_declarator
: identifier '(' parameter_list? ')' constructor_initializer?
;
constructor_initializer
: ':' 'base' '(' argument_list? ')'
| ':' 'this' '(' argument_list? ')'
;
constructor_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§24.2) ist nur im unsicheren Code (§24) verfügbar.
Ein constructor_declaration kann eine Reihe von Attributen (§23), eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6) und einen extern (§15.6.8)-Modifizierer enthalten. Eine Konstruktordeklaration darf denselben Modifizierer nicht mehrmals einschließen.
Der Bezeichner einer constructor_declarator muss die Klasse benennen, in der der Instanzkonstruktor deklariert wird. Wenn ein anderer Name angegeben ist, tritt ein Kompilierungszeitfehler auf.
Die optionale parameter_list eines Instanzkonstruktors unterliegt den gleichen Regeln wie die parameter_list einer Methode (§15.6). Da der this Modifizierer für Parameter nur für Erweiterungsmethoden (§15.6.10) gilt, darf kein Parameter im parameter_list eines Konstruktors den this Modifizierer enthalten. Die Parameterliste definiert die Signatur (§7.6) eines Instanzkonstruktors und steuert den Prozess, bei dem die Überladungsauflösung (§12.6.4) einen bestimmten Instanzkonstruktor in einem Aufruf auswählt.
Jeder der typen, auf die im parameter_list eines Instanzkonstruktors verwiesen wird, muss mindestens so zugänglich sein wie der Konstruktor selbst (§7.5.5).
Die optionale constructor_initializer gibt einen anderen Instanzkonstruktor an, der aufgerufen werden soll, bevor die anweisungen im constructor_body dieses Instanzkonstruktors ausgeführt werden. Dies wird weiter in §15.11.2 beschrieben.
Wenn eine Konstruktordeklaration einen extern Modifizierer enthält, wird der Konstruktor als externer Konstruktor bezeichnet. Da eine externe Konstruktordeklaration keine tatsächliche Implementierung bereitstellt, besteht die constructor_body aus einem Semikolon. Für alle anderen Konstruktoren besteht die constructor_body aus einer der beiden
- ein Block, der die Anweisungen angibt, um eine neue Instanz der Klasse zu initialisieren;
- ein Ausdruckskörper, bestehend aus
=>gefolgt von einem Ausdruck und einem Semikolon, und einen einzelnen Ausdruck darstellt, um eine neue Instanz der Klasse zu initialisieren.
Ein Konstruktorkörper , der ein Block oder Ausdruckskörper ist, entspricht genau dem Block einer Instanzmethode mit einem void Rückgabetyp (§15.6.11).
Instanzkonstruktoren werden nicht vererbt. Daher verfügt eine Klasse über keine anderen Instanzkonstruktoren als die tatsächlich in der Klasse deklarierten Instanzen, mit der Ausnahme, dass, wenn eine Klasse keine Instanzkonstruktordeklarationen enthält, automatisch ein Standardinstanzkonstruktor bereitgestellt wird (§15.11.5).
Instanzkonstruktoren werden von object_creation_expressions (§12.8.17.2) und über constructor_initializers aufgerufen.
15.11.2 Konstruktor-Initialisierer
Alle Instanzkonstruktoren (mit Ausnahme der objectKlassenkonstruktoren) enthalten implizit einen Aufruf eines anderen Instanzkonstruktors unmittelbar vor dem constructor_body. Der implizit aufgerufene Konstruktor wird durch die constructor_initializer bestimmt:
- Ein Instanzkonstruktorinitialisierer des Formulars
base(argument_list)(wobei argument_list optional ist) bewirkt, dass ein Instanzkonstruktor aus der direkten Basisklasse aufgerufen wird. Dieser Konstruktor wird mit argument_list und den Überladungsauflösungsregeln von §12.6.4 ausgewählt. Der Satz von Kandidateninstanzkonstruktoren besteht aus allen barrierefreien Instanzkonstruktoren der direkten Basisklasse. Wenn dieser Satz leer ist oder ein einzelner Konstruktor der besten Instanz nicht identifiziert werden kann, tritt ein Kompilierungszeitfehler auf. - Ein Instanzkonstruktorinitialisierer des Formulars
this(argument_list)(wobei argument_list optional ist) ruft einen anderen Instanzkonstruktor aus derselben Klasse auf. Der Konstruktor wird mit argument_list und den Überladungsauflösungsregeln von §12.6.4 ausgewählt. Der Satz von Kandidateninstanzkonstruktoren besteht aus allen Instanzkonstruktoren, die in der Klasse selbst deklariert sind. Wenn der resultierende Satz anwendbarer Instanzkonstruktoren leer ist oder ein einzelner optimaler Instanzkonstruktor nicht identifiziert werden kann, tritt ein Kompilierungszeitfehler auf. Wenn sich eine Instanzkonstruktordeklaration über eine Kette eines oder mehrerer Konstruktorinitialisierer aufruft, tritt ein Kompilierungsfehler auf.
Wenn ein Instanzkonstruktor keinen Konstruktorinitialisierer hat, wird implizit ein Konstruktorinitialisierer des Formulars base() bereitgestellt.
Hinweis: Eine Instanzkonstruktordeklaration des Formulars
C(...) {...}ist genau gleichbedeutend mit
C(...) : base() {...}Hinweisende
Der Umfang der parameter, die vom parameter_list einer Instanzkonstruktordeklaration angegeben werden, enthält den Konstruktorinitialisierer dieser Deklaration. Daher ist es einem Konstruktorinitialisierer gestattet, auf die Parameter des Konstruktors zuzugreifen.
Beispiel:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y) : base(x + y, x - y) {} }Endbeispiel
Ein Instanzkonstruktorinitialisierer kann nicht auf die erstellte Instanz zugreifen. Daher ist es ein Kompilierungszeitfehler, dies in einem Argumentausdruck des Konstruktorinitialisierers zu referenzieren, da es ein Kompilierungszeitfehler für einen Argumentausdruck ist, ein Instanzmitglied über einen simple_name zu referenzieren.
15.11.3 Instanzvariablen-Initialisierer
Wenn ein nicht externer Instanzkonstruktor keinen Konstruktorinitialisierer hat oder über einen Konstruktorinitialisierer des Formulars base(...)verfügt, führt dieser Konstruktor implizit die initialisierungen aus, die von den variable_initializers der in der Klasse deklarierten Instanzfelder angegeben wurden. Dies entspricht einer Abfolge von Zuweisungen, die unmittelbar nach dem Eintrag zum Konstruktor und vor dem impliziten Aufruf des direkten Basisklassenkonstruktors ausgeführt werden. Die Variableninitialisierer werden in der Textreihenfolge ausgeführt, in der sie in der Klassendeklaration (§15.5.6) angezeigt werden.
Variable Initialisierer müssen nicht von externen Instanzkonstruktoren ausgeführt werden.
15.11.4 Konstruktorausführung
Variableninitialisierer werden in Zuordnungsanweisungen umgewandelt, und diese Zuordnungsanweisungen werden vor dem Aufruf des Basisklasseninstanzkonstruktors ausgeführt. Diese Reihenfolge stellt sicher, dass alle Instanzfelder durch ihre Variableninitialisierer initialisiert werden, bevor jede Anweisung ausgeführt wird, die Zugriff auf diese Instanz hat.
Beispiel: In Anbetracht der folgenden Punkte:
class A { public A() { PrintFields(); } public virtual void PrintFields() {} } class B: A { int x = 1; int y; public B() { y = -1; } public override void PrintFields() => Console.WriteLine($"x = {x}, y = {y}"); }Wenn ein neues
B()zum Erstellen einer Instanz vonBverwendet wird, wird die folgende Ausgabe erzeugt:x = 1, y = 0Der Wert von
xist 1, da der Variableninitialisierer ausgeführt wird, bevor der Konstruktor der Basisklassen-Instanz aufgerufen wird. Der Wert vonyist jedoch 0 (dem Standardwert einerint), da die Zuordnung zuyerst ausgeführt wird, nachdem der Basisklassenkonstruktor zurückkehrt. Es ist sinnvoll, sich die Initialisierungen von Instanzvariablen und Konstruktoren als Anweisungen vorzustellen, die automatisch vor dem Konstruktor_bodyeingefügt werden. Das Beispielclass 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 (
baseundthis). Das Beispiel entspricht dem unten gezeigten Code, wobei jeder Kommentar eine automatisch eingefügte Anweisung angibt (die syntax, die für die automatisch eingefügten Konstruktoraufrufe verwendet wird, ist ungültig, dient aber lediglich dazu, den Mechanismus zu veranschaulichen).class A { int x, y, count; public A() { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = 0; } public A(int n) { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = n; } } class B : A { double sqrt2; ArrayList items; int max; public B() : this(100) { B(100); // Invoke B(int) constructor items.Add("default"); } public B(int n) : base(n - 1) { sqrt2 = Math.Sqrt(2.0); // Variable initializer items = new ArrayList(100); // Variable initializer A(n - 1); // Invoke A(int) constructor max = n; } }Endbeispiel
15.11.5 Standardkonstruktoren
Wenn eine Klasse keine Instanzkonstruktordeklarationen enthält, wird automatisch ein Standardinstanzkonstruktor bereitgestellt. Dieser Standardkonstruktor ruft einfach einen Konstruktor der direkten Basisklasse auf, als hätte er einen Konstruktorinitialisierer des Formulars base(). Wenn die Klasse abstrakt ist, ist die deklarierte Barrierefreiheit für den Standardkonstruktor geschützt. Andernfalls ist die deklarierte Barrierefreiheit für den Standardkonstruktor öffentlich.
Hinweis: Daher ist der Standardkonstruktor immer des Formulars.
protected C(): base() {}oder
public C(): base() {}dabei
Chandelt es sich um den Namen der Klasse.Hinweisende
Wenn die Überladungsauflösung keinen eindeutigen besten Kandidaten für den Initialisierer des Basisklassenkonstruktors ermitteln kann, tritt ein Kompilierungszeitfehler auf.
Beispiel: Im folgenden Code
class Message { object sender; string text; }Ein Standardkonstruktor wird bereitgestellt, da die Klasse keine Instanzkonstruktordeklarationen enthält. Das Beispiel ist also genau gleichbedeutend mit
class Message { object sender; string text; public Message() : base() {} }Endbeispiel
15.12 Statische Konstruktoren
Ein statischer Konstruktor ist ein Element, das die zum Initialisieren einer geschlossenen Klasse erforderlichen Aktionen implementiert. Statische Konstruktoren werden mit static_constructor_declaration sdeklariert:
static_constructor_declaration
: attributes? static_constructor_modifiers identifier '(' ')'
static_constructor_body
;
static_constructor_modifiers
: 'static'
| 'static' 'extern' unsafe_modifier?
| 'static' unsafe_modifier 'extern'?
| 'extern' 'static' unsafe_modifier?
| 'extern' unsafe_modifier 'static'
| unsafe_modifier 'static' 'extern'?
| unsafe_modifier 'extern' 'static'
;
static_constructor_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§24.2) ist nur im unsicheren Code (§24) verfügbar.
Ein static_constructor_declaration kann einen Satz von Attributen (§23) und einen extern Modifizierer (§15.6.8) enthalten.
Der Bezeichner eines static_constructor_declaration muss die Klasse benennen, in der der statische Konstruktor deklariert wird. Wenn ein anderer Name angegeben ist, tritt ein Kompilierungszeitfehler auf.
Wenn eine statische Konstruktordeklaration einen extern Modifizierer enthält, wird der statische Konstruktor als externer statischer Konstruktor bezeichnet. Da eine externe statische Konstruktordeklaration keine tatsächliche Implementierung bereitstellt, besteht die static_constructor_body aus einem Semikolon. Für alle anderen statischen Konstruktordeklarationen besteht die static_constructor_body aus einer der beiden
- ein Block, der die auszuführenden Anweisungen angibt, um die Klasse zu initialisieren;
- ein Ausdruckskörper, der aus einem
=>, einem Ausdruck und einem Semikolon besteht, und einen einzelnen Ausdruck angibt, der ausgeführt werden soll, um die Klasse zu initialisieren.
Ein static_constructor_body in Form eines Blocks oder Ausdruckskörpers entspricht genau dem method_body einer statischen Methode mit einem void Rückgabetyp (§15.6.11).
Statische Konstruktoren werden nicht geerbt und können nicht direkt aufgerufen werden.
Der statische Konstruktor für eine geschlossene Klasse wird in einer bestimmten Anwendungsdomäne höchstens einmal ausgeführt. Die Ausführung eines statischen Konstruktors wird durch die ersten der folgenden Ereignisse ausgelöst, die in einer Anwendungsdomäne auftreten:
- Eine Instanz der Klasse wird erstellt.
- Alle statischen Elemente der Klasse werden referenziert.
Wenn eine Klasse die Main Methode (§7.1) enthält, in der die Ausführung beginnt, wird der statische Konstruktor für diese Klasse ausgeführt, bevor die Main Methode aufgerufen wird.
Um einen neuen geschlossenen Klassentyp zu initialisieren, muss zunächst ein neuer Satz statischer Felder (§15.5.2) für diesen bestimmten geschlossenen Typ erstellt werden. Jedes der statischen Felder muss auf seinen Standardwert (§15.5.5.5) initialisiert werden. Folgen Sie diesen Schritten:
- Wenn entweder kein statischer Konstruktor oder ein nicht externer statischer Konstruktor vorhanden ist, dann:
- die statischen Feldinitialisierer (§15.5.6.2) sollen für diese statischen Felder ausgeführt werden;
- dann muss der nicht externe statische Konstruktor (sofern vorhanden) ausgeführt werden.
- Falls andererseits ein externer statischer Konstruktor vorhanden ist, muss dieser ausgeführt werden. Statische Variableninitialisierer müssen nicht von externen statischen Konstruktoren ausgeführt werden.
Beispiel: Das Beispiel
class Test { static void Main() { A.F(); B.F(); } } class A { static A() { Console.WriteLine("Init A"); } public static void F() { Console.WriteLine("A.F"); } } class B { static B() { Console.WriteLine("Init B"); } public static void F() { Console.WriteLine("B.F"); } }muss die Ausgabe erzeugen:
Init A A.F Init B B.Fda die Ausführung des
Astatischen Konstruktors durch den AufrufA.Fausgelöst wird und die Ausführung desBstatischen Konstruktors durch den AufrufB.Fausgelöst wird.Endbeispiel
Es ist möglich, Zirkelabhängigkeiten zu konstruieren, die es statischen Feldern mit Variableninitialisierern ermöglichen, in ihrem Standardwertzustand beobachtet zu werden.
Beispiel: Das Beispiel
class A { public static int X; static A() { X = B.Y + 1; } } class B { public static int Y = A.X + 1; static B() {} static void Main() { Console.WriteLine($"X = {A.X}, Y = {B.Y}"); } }erzeugt die Ausgabe
X = 1, Y = 2Zum Ausführen der
MainMethode führt das System zunächst den Initialisierer fürB.Yaus, bevor es den statischen Konstruktor der KlasseBausführt. Der Initialisierer vonYbewirkt, dass derA-Konstruktor vonstaticausgeführt wird, weil der Wert vonA.Xreferenziert wird. Der statische Konstruktor vonAfährt fort, den Wert vonXzu berechnen und ruft dabei den voreingestellten Wert vonYab, der null ist.A.Xwird daher auf 1 initialisiert. Der Prozess der Ausführung der statischen Feldinitialisierer und des statischen Konstruktors wird dann abgeschlossen und kehrt zurück zur Berechnung des Anfangswerts vonA, dessen Ergebnis 2 wird.Endbeispiel
Da der statische Konstruktor genau einmal für jeden geschlossenen konstruierten Klassentyp ausgeführt wird, ist es praktisch, Laufzeitüberprüfungen für den Typparameter zu erzwingen, der nicht zur Kompilierungszeit über Einschränkungen (§15.2.5) überprüft werden kann.
Beispiel: Der folgende Typ verwendet einen statischen Konstruktor, um zu erzwingen, dass das Typargument eine Enumeration ist:
class Gen<T> where T : struct { static Gen() { if (!typeof(T).IsEnum) { throw new ArgumentException("T must be an enum"); } } }Endbeispiel
15.13 Finalizer
Hinweis: In einer früheren Version dieser Spezifikation wurde das, was jetzt als "Finalizer" bezeichnet wird, als "Destruktor" bezeichnet. Die Erfahrung hat gezeigt, dass der Begriff "Destruktor" Verwirrung verursachte und häufig zu falschen Erwartungen geführt hat, insbesondere für Programmierer, die C++ kennen. In C++ wird ein Destruktor auf bestimmte Weise aufgerufen, während in C# kein Finalisierer ist. Um ein bestimmtes Verhalten von C# zu erzielen, sollte man
Disposeverwenden. Hinweisende
Ein Finalizer ist ein Member, der die erforderlichen Aktionen zum Bereinigen einer Instanz einer Klasse implementiert. Ein Finalizer wird mithilfe eines finalizer_declaration deklariert:
finalizer_declaration
: attributes? '~' identifier '(' ')' finalizer_body
| attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
finalizer_body
| attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
finalizer_body
;
finalizer_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§24.2) ist nur im unsicheren Code (§24) verfügbar.
Ein finalizer_declaration kann eine Reihe von Attributen (§23) enthalten.
Der Bezeichner einer finalizer_declarator muss die Klasse benennen, in der der Finalizer deklariert wird. Wenn ein anderer Name angegeben ist, tritt ein Kompilierungszeitfehler auf.
Wenn eine Finalizerdeklaration einen extern Modifizierer enthält, wird der Finalizer als externer Finalizer bezeichnet. Da eine externe Finalizerdeklaration keine tatsächliche Implementierung bereitstellt, besteht die finalizer_body aus einem Semikolon. Für alle anderen Finalisierer besteht die finalizer_body aus einer der beiden
- ein Block, der die auszuführenden Anweisungen angibt, um eine Instanz der Klasse abzuschließen.
- oder ein Ausdruckskörper, der aus
=>gefolgt von einem Ausdruck und einem Semikolon besteht und einen einzelnen Ausdruck bezeichnet, der ausgeführt wird, um eine Instanz der Klasse zu finalisieren.
Ein finalizer_body , der ein Block oder ein Ausdruckskörper ist, entspricht genau dem method_body einer Instanzmethode mit einem void Rückgabetyp (§15.6.11).
Finalizer werden nicht vererbt. Somit hat eine Klasse keine anderen Finalizer als den, der in dieser Klasse deklariert werden kann.
Hinweis: Da ein Finalizer über keine Parameter verfügen muss, kann er nicht überladen werden, sodass eine Klasse höchstens einen Finalizer haben kann. Hinweisende
Finalizer werden automatisch aufgerufen und können nicht explizit aufgerufen werden. Eine Instanz kann abgeschlossen werden, wenn kein Code mehr für diese Instanz verwendet werden kann. Die Ausführung des Finalizers für die Instanz kann jederzeit erfolgen, nachdem die Instanz zur Fertigstellung berechtigt ist (§7.9). Wenn eine Instanz fertiggestellt ist, werden die Finalizer in der Vererbungskette dieser Instanz in der Reihenfolge vom am weitesten abgeleiteten bis zum am wenigsten abgeleiteten aufgerufen. Ein Finalizer kann für jeden Thread ausgeführt werden. Weitere Erläuterungen zu den Regeln, die bestimmen, wann und wie ein Finalizer ausgeführt wird, finden Sie unter §7.9.
Beispiel: Die Ausgabe des Beispiels
class A { ~A() { Console.WriteLine("A's finalizer"); } } class B : A { ~B() { Console.WriteLine("B's finalizer"); } } class Test { static void Main() { B b = new B(); b = null; GC.Collect(); GC.WaitForPendingFinalizers(); } }auf
B's finalizer A's finalizerda Finalizer in einer Vererbungskette der Reihe nach aufgerufen werden, von den meisten abgeleiteten bis zu den am wenigsten abgeleiteten.
Endbeispiel
Finalizer werden implementiert, indem die virtuelle Methode Finalize auf System.Objectüberschrieben wird. C#-Programme dürfen diese Methode nicht überschreiben oder sie (oder Überschreibungen davon) direkt aufrufen.
Beispiel: Beispiel: Das Programm
class A { override protected void Finalize() {} // Error public void F() { this.Finalize(); // Error } }enthält zwei Fehler.
Endbeispiel
Ein Compiler soll sich so verhalten, als ob diese Methode und ihre Überschreibungen überhaupt nicht existieren.
Beispiel: Demnach ist dieses Programm:
class A { void Finalize() {} // Permitted }ist gültig und die gezeigte Methode blendet die Methode
System.ObjectvonFinalizeaus.Endbeispiel
Eine Erläuterung des Verhaltens, wenn eine Ausnahme von einem Finalizer ausgelöst wird, finden Sie unter §22.4.
15.14 Asynchrone Funktionen
15.14.1 Allgemein
Eine Methode (§15.6), anonyme Funktion (§12.20) oder lokale Funktion (§13.6.4) mit dem async Modifizierer wird als asynchrone Funktion bezeichnet. Im Allgemeinen wird der Begriff "async" verwendet, um jede Art von Funktion zu beschreiben, die den async Modifizierer enthält.
Es handelt sich um einen Kompilierungszeitfehler für die Parameterliste einer asynchronen Funktion, wenn Parameter vom Typ in, out oder ref oder ein beliebiger Parameter eines ref struct Typs angegeben werden.
Die return_type einer asynchronen Methode muss entweder voidein Aufgabentyp oder ein asynchroner Iteratortyp (§15.15) sein. Bei einer asynchronen Methode, die einen Ergebniswert erzeugt, muss ein Aufgabentyp oder ein asynchroner Iteratortyp (§15.15.3) generisch sein. Bei einer asynchronen Methode, die keinen Ergebniswert erzeugt, darf ein Vorgangstyp nicht generisch sein. Solche Typen werden in dieser Spezifikation als «TaskType»<T> und «TaskType» bezeichnet. Der Standardbibliothekstyp System.Threading.Tasks.Task und die aus System.Threading.Tasks.Task<TResult> und System.Threading.Tasks.ValueTask<T> konstruierten Typen sind Aufgabentypen, ebenso wie ein Klassen-, Struktur- oder Schnittstellentyp, der einem Aufgaben-Generator-Typ über das Attribut System.Runtime.CompilerServices.AsyncMethodBuilderAttribute zugeordnet ist. Solche Typen werden in dieser Spezifikation als «TaskBuilderType»<T> und «TaskBuilderType»bezeichnet. Ein Vorgangstyp kann höchstens einen Typparameter aufweisen und kann nicht in einem generischen Typ geschachtelt werden.
Eine asynchrone Methode, die einen Aufgabentyp zurückgibt, wird als task-returning bezeichnet.
Vorgangstypen können in ihrer genauen Definition variieren, aber aus der Sicht der Sprache befindet sich ein Vorgangstyp in einem der Zustände unvollständig, erfolgreich oder fehlerhaft. Eine fehlerhafte Aufgabe zeichnet eine relevante Ausnahme auf. Ein erfolgreich«TaskType»<T> zeichnet ein Ergebnis vom Typ Tauf. Aufgabentypen sind wartend, und Aufgaben können daher die Operanden von Await-Ausdrücken (§12.9.9) sein.
Beispiel: Der Aufgabentyp
MyTask<T>ist dem Aufgaben-Generator-TypMyTaskMethodBuilder<T>und dem Awaiter-TypAwaiter<T>zugeordnet:using System.Runtime.CompilerServices; [AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))] class MyTask<T> { public Awaiter<T> GetAwaiter() { ... } } class Awaiter<T> : INotifyCompletion { public void OnCompleted(Action completion) { ... } public bool IsCompleted { get; } public T GetResult() { ... } }Endbeispiel
Ein Aufgaben-Generator-Typ ist eine Klasse oder ein Strukturtyp, der einem bestimmten Aufgabentyp entspricht (§15.14.2). Der Aufgaben-Generator-Typ muss genau mit der deklarierten Barrierefreiheit des entsprechenden Aufgabentyps übereinstimmen.
Hinweis: Wenn der Aufgabentyp deklariert
internalwird, muss der entsprechende Generatortyp ebenfalls deklariertinternalund in derselben Assembly definiert werden. Wenn der Aufgabentyp in einem anderen Typ geschachtelt ist, muss der Aufgaben-Generator-Typ auch in diesem Typ geschachtelt werden. Hinweisende
Eine asynchrone Funktion hat die Möglichkeit, die Auswertung mithilfe von Await-Ausdrücken (§12.9.9) im Textkörper anzusetzen. Die Auswertung kann später an der Stelle des aussetzenden await-Ausdrucks mit Hilfe eines Wiederaufnahme-Delegatenwieder aufgenommen werden. Der Resumption-Delegat ist vom Typ System.Action. Wenn er aufgerufen wird, wird die Auswertung des asynchronen Funktionsaufrufs ab dem await-Ausdruck an der Stelle fortgesetzt, an der er unterbrochen wurde. Der aktuelle Aufrufer eines asynchronen Funktionsaufrufs ist der ursprüngliche Aufrufer, wenn der Funktionsaufruf nie angehalten wurde oder der letzte Aufrufer des Reaktivierungsdelegats andernfalls.
15.14.2 Muster des Aufgabentyp-Generators
Ein Aufgaben-Generator-Typ kann höchstens einen Typparameter aufweisen und kann nicht in einem generischen Typ geschachtelt werden. Ein Task Builder-Typ muss die folgenden Mitglieder (für nicht-generische Task Builder-Typen hat SetResult keine Parameter) mit deklarierter public Zugänglichkeit haben:
class «TaskBuilderType»<T>
{
public static «TaskBuilderType»<T> Create();
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine;
public void SetStateMachine(IAsyncStateMachine stateMachine);
public void SetException(Exception exception);
public void SetResult(T result);
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine;
public «TaskType»<T> Task { get; }
}
Ein Compiler generiert Code, der den «TaskBuilderType» verwendet, um die Semantik des Anhaltens und Fortsetzens der Auswertung der asynchronen Funktion zu implementieren. Ein Compiler verwendet den «TaskBuilderType» wie folgt:
-
«TaskBuilderType».Create()wird aufgerufen, um eine Instanz von «TaskBuilderType», die in dieser Liste benannt istbuilder, zu erstellen. -
builder.Start(ref stateMachine)wird aufgerufen, um den Builder mit einer vom Compiler erzeugten Zustandsmaschineninstanz zu verknüpfenstateMachine.- Der Ersteller ruft
stateMachine.MoveNext()entweder inStart()oder nach der Rückkehr vonStart()auf, um die Zustandsmaschine voranzutreiben.
- Der Ersteller ruft
- Nachdem
Start()zurückgekehrt ist, ruft dieasync-Methodebuilder.Taskauf, damit die Aufgabe aus der asynchronen Methode zurückkehrt. - Jeder Aufruf von
stateMachine.MoveNext()bringt die Zustandsmaschine voran. - Wenn der Zustandsautomat erfolgreich abgeschlossen wird, wird
builder.SetResult()mit dem Rückgabewert der Methode aufgerufen, falls vorhanden. - Andernfalls, wenn eine Ausnahme
ein der Zustandsmaschine ausgelöst wird, wirdbuilder.SetException(e)aufgerufen. - Wenn der Zustandsautomat einen
await exprAusdruck erreicht, wirdexpr.GetAwaiter()aufgerufen. - Wenn der Waiter
ICriticalNotifyCompletionimplementiert undIsCompletedfalsch ist, ruft der Zustandsautomatbuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)auf.-
AwaitUnsafeOnCompleted()sollteawaiter.UnsafeOnCompleted(action)mit einemActionaufrufen, derstateMachine.MoveNext()aufruft, wenn der Waiter fertig ist.
-
- Andernfalls ruft der Zustandsautomat
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)auf.-
AwaitOnCompleted()sollteawaiter.OnCompleted(action)mit einemActionaufrufen, derstateMachine.MoveNext()aufruft, wenn der Waiter fertig ist.
-
-
SetStateMachine(IAsyncStateMachine)kann von der vom Compiler erzeugtenIAsyncStateMachine-Implementierung aufgerufen werden, um die Instanz des Builders zu identifizieren, die mit einer Zustandsmaschineninstanz verbunden ist, insbesondere in Fällen, in denen die Zustandsmaschine als Werttyp implementiert ist.- Wenn der Builder
stateMachine.SetStateMachine(stateMachine)aufruft, ruftstateMachinebuilder.SetStateMachine(stateMachine)auf der Builder-Instanz auf, die mitstateMachineverbunden ist.
- Wenn der Builder
Hinweis: Sowohl der Parameter als auch das Argument müssen identitätskonvertierbar zu
SetResult(T result)sein für«TaskType»<T> Task { get; }undT. Dies ermöglicht es einem Aufgabentyp-Generator, Typen wie Tupel zu unterstützen, bei denen zwei Typen, die nicht gleich sind, identitätskonvertierbar sind. Hinweisende
15.14.3 Auswertung einer Task-Returning-Async-Funktion
Der Aufruf einer asynchronen Funktion zur Rückgabe einer Aufgabe bewirkt, dass eine Instanz des zurückgegebenen Aufgabentyps generiert wird. Dies wird als Rückgabeaufgabe der asynchronen Funktion bezeichnet. Der Vorgang befindet sich zunächst in einem unvollständigen Zustand.
Der asynchrone Funktionstext wird dann ausgewertet, bis er entweder angehalten wird (durch Erreichen eines Warteausdrucks) oder beendet wird, woraufhin die Steuerung zusammen mit der Rückgabeaufgabe an den Anrufer zurückgegeben wird.
Wenn der Text der asynchronen Funktion beendet wird, wird die Rückgabeaufgabe aus dem unvollständigen Zustand verschoben:
- Wenn der Funktionskörper durch das Erreichen einer Return-Anweisung oder des Endes des Körpers beendet wird, wird jeder Ergebniswert in der Return-Task aufgezeichnet, die in den Zustand Erfolgreich versetzt wird.
- Wenn der Funktionskörper aufgrund einer nicht abgefangenen
OperationCanceledExceptionbeendet wird, wird die Ausnahme in der Rückgabeaufgabe aufgezeichnet, die in den Zustand abgebrochen versetzt wird. - Wenn der Funktionskörper aufgrund einer anderen nicht abgefangenen Ausnahme (§13.10.6) beendet wird, wird die Ausnahme in der Rückgabeaufgabe aufgezeichnet, die in den Zustand faulted versetzt wird.
15.14.4 Auswertung einer asynchronen Funktion, die keinen Wert zurückgibt
Wenn der Rückgabetyp der asynchronen Funktion lautetvoid, unterscheidet sich die Auswertung von der obigen Vorgehensweise: Da keine Aufgabe zurückgegeben wird, kommuniziert die Funktion stattdessen den Abschluss und Ausnahmen mit dem Synchronisierungskontext des aktuellen Threads. Die genaue Definition des Synchronisierungskontexts ist implementierungsabhängig und repräsentiert, an welchem Ort der aktuelle Thread ausgeführt wird. Der Synchronisationskontext wird benachrichtigt, wenn die Auswertung einer void-returning async-Funktion beginnt, erfolgreich abgeschlossen wird oder eine nicht abgefangene Ausnahme ausgelöst wird.
Dadurch kann der Kontext verfolgen, wie viele void-returning async-Funktionen unter ihm laufen, und entscheiden, wie Ausnahmen, die von ihnen ausgehen, propagiert werden sollen.
15.15 Synchrone und asynchrone Iteratoren
15.15.1 Allgemein
Ein Funktionselement (§12.6) oder eine lokale Funktion (§13.6.4), die mit einem Iteratorblock (§13.3) implementiert wurde, wird als Iterator bezeichnet. Ein Iteratorblock kann als Textkörper einer Funktion verwendet werden, solange der Rückgabetyp der entsprechenden Funktion eine der Enumerationsschnittstellen (§15.15.2) oder eine der aufzählbaren Schnittstellen (§15.15.3) ist.
Eine asynchrone Funktion (§15.14) oder lokale Funktion (§13.6.4) wird mithilfe eines Iteratorblocks (§13.3) als asynchroner Iterator bezeichnet. Ein asynchroner Iteratorblock kann als Textkörper einer Funktion verwendet werden, solange der Rückgabetyp der entsprechenden Funktion die asynchrone Enumerationsschnittstelle (§15.15.2) oder die asynchrone aufzählbare Schnittstelle (§15.15.3) ist.
Ein Iteratorblock kann als method_body, operator_body oder accessor_body auftreten, während Ereignisse, Instanzkonstruktoren, statische Konstruktoren und Finalizer nicht als synchrone oder asynchrone Iteratoren implementiert werden dürfen.
Wenn eine Funktion mithilfe eines Iteratorblocks implementiert wird, handelt es sich um einen Kompilierungszeitfehler für die Parameterliste der Funktion, um beliebige in, out, oder Parameter oder ref einen Parameter eines ref struct Typs anzugeben.
Ein asynchroner Iterator unterstützt den Abbruch des asynchronen Vorgangs. Dies wird in §23.5.8 beschrieben.
15.15.2 Enumeratorschnittstellen
Die Enumerationsschnittstellen sind die nicht generische Schnittstelle System.Collections.IEnumerator und die generische Schnittstelle System.Collections.Generic.IEnumerator<T>.
Die asynchrone Enumerationsschnittstelle ist die generische Schnittstelle System.Collections.Generic.IAsyncEnumerator<T>.
Aus Gründen der Kürze werden in diesem Unterabschnitt und seinen gleichgeordneten Abschnitten diese Schnittstellen als IEnumerator, IEnumerator<T> und IAsyncEnumerator<T> bezeichnet.
15.15.3 Aufzählbare Schnittstellen
Die aufzählbaren Schnittstellen sind die nicht generische Schnittstelle System.Collections.IEnumerable und die generischen Schnittstellen System.Collections.Generic.IEnumerable<T>.
Die asynchrone Aufzählungsschnittstelle ist die generische Schnittstelle System.Collections.Generic.IAsyncEnumerable<T>.
Aus Gründen der Kürze werden in diesem Unterabschnitt und seinen gleichgeordneten Abschnitten diese Schnittstellen als IEnumerable, IEnumerable<T> und IAsyncEnumerable<T> bezeichnet.
15.15.4 Ertragtyp
Ein Iterator erzeugt eine Abfolge von Werten, die alle denselben Typ aufweisen. Dieser Typ wird als Ertragtyp des Iterators bezeichnet.
- Der Ertragtyp eines Iterators, der zurückgibt
IEnumeratoroderIEnumerableistobject. - Der Rückgabetyp eines Iterators, der ein
IEnumerator<T>,IAsyncEnumerator<T>,IEnumerable<T>oderIAsyncEnumerable<T>zurückgibt, istT.
15.15.5 Enumeratorobjekte
15.15.5.1 Allgemein
Wenn ein Funktionsmitglied oder eine lokale Funktion, die einen Enumerator-Schnittstellentyp zurückgibt, oder ein asynchroner Enumerator-Schnittstellentyp mithilfe eines Iteratorblocks implementiert wird, führt das Aufrufen der Funktion den Code im Iteratorblock nicht sofort aus. Stattdessen wird ein Enumerationsobjekt erstellt und zurückgegeben. Dieses Objekt kapselt den im Iteratorblock angegebenen Code, und die Ausführung dieses Codes erfolgt, wenn die Methode MoveNext oder MoveNextAsync des Enumerator-Objekts aufgerufen wird. Ein Enumerationsobjekt weist die folgenden Merkmale auf:
- Er implementiert
System.IDisposable,IEnumeratorundIEnumerator<T>, oderSystem.IAsyncDisposableundIAsyncEnumerator<T>, wobeiTder Rückgabetyp des Iterators ist. - Sie wird mit einer Kopie der Argumentwerte (falls vorhanden) initialisiert und instanzwert, der an die Funktion übergeben wird.
- Es verfügt über vier potenzielle Zustände: vorher, laufend, angehalten und danach, und befindet sich anfangs im Zustand vorher.
Ein Enumeratorobjekt ist in der Regel eine Instanz einer compilergenerierten Enumerationsklasse, die den Code im Iteratorblock kapselt und die Enumerationsschnittstellen implementiert, aber andere Implementierungsmethoden sind möglich. Wenn eine Enumeratorklasse vom Compiler generiert wird, wird diese Klasse direkt oder indirekt in der Klasse mit der Funktion geschachtelt, sie verfügt über private Barrierefreiheit und hat einen Namen, der für die Compilerverwendung reserviert ist (§6.4.3).
Ein Enumeratorobjekt kann mehr Schnittstellen implementieren als die oben angegebenen.
Die folgenden Unterklauseln beschreiben das erforderliche Verhalten des Mitglieds, um den Enumerator weiterzuschalten, den aktuellen Wert aus dem Enumerator abzurufen und die vom Enumerator verwendeten Ressourcen zu entsorgen. Diese sind in den folgenden Mitgliedern für synchrone bzw. asynchrone Enumeratoren definiert:
- Um den Enumerator voranzuschreiten:
MoveNextundMoveNextAsync. - So rufen Sie den aktuellen Wert ab:
Current. - So löschen Sie Ressourcen:
DisposeundDisposeAsync.
Enumeratorobjekte unterstützen die IEnumerator.Reset Methode nicht. Wenn Sie diese Methode aufrufen, wird ein System.NotSupportedException Fehler ausgelöst.
Synchrone und asynchrone Iteratorblöcke unterscheiden sich dadurch, dass asynchrone Iteratormitglieder Aufgabentypen zurückgeben und gewartet werden können.
Den Enumerator weiterentwickeln
Die MoveNext- und MoveNextAsync-Methoden eines Enumeratorobjekts kapseln den Code eines Iteratorblocks. Durch das Aufrufen der Methode MoveNext oder MoveNextAsync wird der Code im Iteratorblock ausgeführt und die Current-Eigenschaft im Enumeratorobjekt entsprechend gesetzt.
MoveNext gibt einen bool Wert zurück, dessen Bedeutung unten beschrieben wird.
MoveNextAsync gibt ein ValueTask<bool> (§15.14.3) zurück. Der Ergebniswert der zurückgegebenen MoveNextAsync Aufgabe hat die gleiche Bedeutung wie der Ergebniswert aus MoveNext. In der folgenden Beschreibung gelten die Aktionen, die für MoveNext beschrieben sind, auch für MoveNextAsync mit dem folgenden Unterschied: Wo angegeben ist, dass MoveNexttrue oder false zurückgibt, setzt MoveNextAsync seine Aufgabe auf den Zustand abgeschlossen und legt den Ergebniswert dieser Aufgabe auf den entsprechenden true oder false Wert fest.
Die genaue Aktion, die von MoveNext oder MoveNextAsync ausgeführt wird, hängt vom Status des Enumeratorobjekts ab, wenn sie aufgerufen wird.
- Wenn der Status des Enumerator-Objekts vorist, wird
MoveNextaufgerufen:- Ändert den Zustand in laufend.
- Initialisiert die Parameter (einschließlich
this) des Iteratorblocks an die Argumentwerte und den Instanzwert, die beim Initialisieren des Enumerationsobjekts gespeichert wurden. - Führt den Iteratorblock vom Anfang aus aus, bis die Ausführung unterbrochen wird (wie unten beschrieben).
- Wenn der Status des Enumerator-Objekts laufendist, ist das Ergebnis des Aufrufs von
MoveNextnicht spezifiziert. - Wenn der Status des Enumerator-Objekts suspendedist, wird der Aufruf von MoveNext:
- Ändert den Zustand in laufend.
- Stellt die Werte aller lokalen Variablen und Parameter (einschließlich
this) auf die Werte wieder her, die beim letzten Anhalten des Iteratorblocks gespeichert wurden.Hinweis: Der Inhalt aller Objekte, auf die von diesen Variablen verwiesen wird, kann sich seit dem vorherigen Aufruf von
MoveNextgeändert haben. Hinweisende - Setzt die Ausführung des Iterator-Blocks unmittelbar nach der Renditeanweisung fort, die die Aussetzung der Ausführung verursacht hat, und fährt fort, bis die Ausführung unterbrochen wird (wie unten beschrieben).
- Wenn der Status des Enumerator-Objekts nachist, gibt der Aufruf von
MoveNextfalse zurück.
Wenn MoveNext den Iteratorblock ausführt, kann die Ausführung auf vier Arten unterbrochen werden: Durch eine yield return-Anweisung, durch eine yield break-Anweisung, durch das Erreichen des Endes des Iterator-Blocks und durch eine ausgelöste und aus dem Iterator-Block heraus weitergeleitete Ausnahme.
Hinweis:
MoveNextAsyncWird angehalten, wenn einawaitAusdruck ausgewertet wird, der auf einen Aufgabentyp wartet, der noch nicht abgeschlossen ist. Hinweisende
- Wenn eine
yield return-Anweisung auftaucht (§9.4.4.20):- Der in der Anweisung angegebene Ausdruck wird ausgewertet, implizit in den Ertragtyp konvertiert und der
CurrentEigenschaft des Enumeratorobjekts zugewiesen. - Die Ausführung des Iterator-Texts wird ausgesetzt. Die Werte aller lokalen Variablen und Parameter (einschließlich
this) werden gespeichert, wie die Position dieseryield returnAnweisung. Wenn sich dieyield return-Anweisung innerhalb eines oder mehrerertry-Blöcke befindet, werden die zugehörigen finally-Blöcke zu diesem Zeitpunkt nicht ausgeführt. - Der Zustand des Enumeratorobjekts wird in angehalten geändert.
- Die Methode
MoveNextgibttruean den Aufrufer zurück und zeigt an, dass die Iteration erfolgreich zum nächsten Wert fortgeschritten ist.
- Der in der Anweisung angegebene Ausdruck wird ausgewertet, implizit in den Ertragtyp konvertiert und der
- Wenn eine
yield break-Anweisung auftaucht (§9.4.4.20):- Wenn sich die
yield breakAnweisung innerhalb eines oder mehrerertryBlöcke befindet, werden die zugehörigenfinallyBlöcke ausgeführt. - Der Zustand des Enumerator-Objekts wird auf nachgeändert.
- Die
MoveNextMethode kehrtfalsezum Aufrufer zurück, der angibt, dass die Iteration abgeschlossen ist.
- Wenn sich die
- Wenn das Ende des Iterator-Texts auftritt:
- Der Zustand des Enumerator-Objekts wird auf nachgeändert.
- Die
MoveNextMethode kehrtfalsezum Aufrufer zurück, der angibt, dass die Iteration abgeschlossen ist.
- Wenn eine Ausnahme ausgelöst und aus dem Iteratorblock propagiert wird:
- Entsprechende
finally-Blöcke im Iterator-Body werden von der Ausnahmefortpflanzung ausgeführt. - Der Zustand des Enumerator-Objekts wird auf nachgeändert.
- Die Ausbreitung der Ausnahme setzt sich bis zum Aufrufer der
MoveNextMethode fort.
- Entsprechende
15.15.5.3 Abrufen des aktuellen Werts
Die Eigenschaft eines Enumeratorobjekts Current wird von yield return Anweisungen im Iteratorblock beeinflusst.
Hinweis: Die
CurrentEigenschaft ist eine synchrone Eigenschaft für synchrone und asynchrone Iteratorobjekte. Hinweisende
Wenn sich ein Enumerationsobjekt im angehaltenen Zustand befindet, entspricht der Wert von Current dem Wert, der durch den vorherigen Aufruf von MoveNext festgelegt wurde. Wenn sich ein Enumeratorobjekt im Vorher-, Laufend-, oder Nachher-Zustand befindet, ist das Ergebnis beim Zugriff auf Current nicht definiert.
Für einen Iterator mit einem Ertragstyp, der nicht object ist, entspricht das Ergebnis des Zugriffs auf Current durch die Implementierung IEnumerable des Enumeratorobjekts dem Zugriff auf Current durch die Implementierung IEnumerator<T> des Enumeratorobjekts und der Umwandlung des Ergebnisses in object.
15.15.5.4 Ressourcen löschen
Die Dispose- oder DisposeAsync-Methode wird verwendet, um die Iteration zu bereinigen, indem das Enumerationsobjekt in den Zustand 'nach' versetzt wird.
- Wenn der Zustand des Enumeratorobjekts vorher ist, ändert das Aufrufen von
Disposeden Zustand zu nachher. - Wenn der Status des Enumerator-Objekts laufendist, ist das Ergebnis des Aufrufs von
Disposenicht spezifiziert. - Wenn der Status des Enumerator-Objekts ausgesetztist, wird
Disposeaufgerufen:- Ändert den Zustand in laufend.
- Führt alle finally-Blöcke aus, als ob die zuletzt ausgeführte
yield return-Anweisung eineyield break-Anweisung wäre. Wenn dies dazu führt, dass eine Ausnahme ausgelöst und aus dem Iterator-Körper heraus propagiert wird, wird der Status des Enumerator-Objekts auf nach gesetzt und die Ausnahme wird an den Aufrufer derDispose-Methode weitergegeben. - Ändert den Zustand in "Danach".
- Wenn der Zustand des Enumeratorobjekts nachher ist, hat der Aufruf von
Disposekeine Auswirkungen.
15.15.6 Aufzählbare Objekte
15.15.6.1 Allgemein
Wenn ein Funktionsmitglied oder eine lokale Funktion, die einen enumerationsfähigen Schnittstellentyp zurückgibt, oder ein asynchroner aufzählbarer Schnittstellentyp mithilfe eines Iteratorblocks implementiert wird, führt das Aufrufen der Funktion den Code nicht sofort im Iteratorblock aus. Stattdessen wird ein aufzählbares Objekt erstellt und zurückgegeben.
Ein synchrones aufzählbares IEnumerable Objekt implementiert und IEnumerable<T>, wobei T der Ertragtyp des Iterators angegeben ist. Die GetEnumerator Methode gibt ein Enumerationsobjekt (§15.15.5) zurück. Ein asynchrones aufzählbares IAsyncEnumerable<T> Objekt implementiert, wo T sich der Ertragtyp des Iterators befindet. Die GetAsyncEnumerator Methode gibt ein asynchrones Enumerationsobjekt (§15.15.5) zurück.
Ein aufzählbares Objekt wird mit einer Kopie der Argumentwerte (sofern vorhanden) initialisiert und der Instanzwert, der an die Funktion ü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 die Funktion enthält, geschachtelt, sie verfügt über private Barrierefreiheit und hat 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 zum Beispiel auch
IEnumeratorundIEnumerator<T>implementieren, so dass es sowohl als aufzählbares Objekt als auch als Aufzähler dienen kann. Normalerweise würde eine solche Implementierung ihre eigene Instanz (um Zuweisungen zu sparen) vom ersten Aufruf vonGetEnumeratorzurückgeben. Nachfolgende Aufrufe vonGetEnumerator, falls vorhanden, würden eine neue Klasseninstanz zurückgeben, in der Regel derselben Klasse, sodass Aufrufe verschiedener Enumerationsinstanzen sich nicht gegenseitig beeinflussen. Hinweisende
15.15.6.2 Die GetEnumerator- oder GetAsyncEnumerator-Methode
Ein aufzählbares Objekt stellt eine Implementierung der GetEnumerator-Methoden der IEnumerable- und IEnumerable<T>-Schnittstellen bereit. Die beiden GetEnumerator Methoden verwenden eine gemeinsame Implementierung, die ein verfügbares Enumerationsobjekt erwirbt und zurückgibt. Das Enumerationsobjekt wird mit den Argumentwerten und Instanzwerten initialisiert, die beim Initialisieren des enumerationsfähigen Objekts gespeichert wurden, andernfalls funktioniert das Enumerationsobjekt wie in §15.15.5 beschrieben.
Ein asynchrones aufzählbares Objekt stellt eine Implementierung der GetAsyncEnumerator Methode der IAsyncEnumerable<T> Schnittstelle bereit. Diese Methode gibt ein verfügbares asynchrones Enumerationsobjekt zurück. Das Enumerationsobjekt wird mit den Argumentwerten und Instanzwerten initialisiert, die beim Initialisieren des enumerationsfähigen Objekts gespeichert wurden, einschließlich des optionalen Abbruchtokens, andernfalls funktioniert das Enumerationsobjekt wie in §15.15.5 beschrieben. Eine asynchrone Iteratormethode kann einen Parameter als Abbruchtoken mit System.Runtime.CompilerServices.EnumeratorCancellationAttribute (§23.5.8) markieren. Eine Implementierung stellt einen Mechanismus zum Kombinieren von Abbruchtoken bereit, sodass ein asynchroner Iterator abgebrochen wird, wenn entweder ein Abbruchtoken (das Argument oder GetAsyncEnumerator das Argument, das dem Attribut System.Runtime.CompilerServices.EnumeratorCancellationAttributezugeordnet ist) den Abbruch anfordert.
ECMA C# draft specification