15 Klassen
15.1 Allgemein
Eine Klasse ist eine Datenstruktur, die Datenmember (Konstanten und Felder), Funktionsmember (Methoden, Eigenschaften, Ereignisse, Indexer, Operatoren, Instanzkonstruktoren, Finalizer und statische Konstruktoren) und geschachtelte Typen enthalten kann. Klassentypen unterstützen die Vererbung, einen Mechanismus, mit dem eine abgeleitete Klasse eine Basisklasse erweitern und spezialisieren kann.
15.2 Klassendeklarationen
15.2.1 Allgemein
Eine class_declaration ist eine type_declaration (§14.7), die eine neue Klasse deklariert.
class_declaration
: attributes? class_modifier* 'partial'? 'class' identifier
type_parameter_list? class_base? type_parameter_constraints_clause*
class_body ';'?
;
Ein class_declaration besteht aus einem optionalen Satz von Attributen (§22), gefolgt von einem optionalen Satz von class_modifiers (§15.2.2), gefolgt von einem optionalen partial
Modifizierer (§15.2.7), gefolgt von dem Schlüsselwort class
und einem Bezeichner, der die Klasse benennt, gefolgt von einer optionalen type_parameter_list (§15.2.3) gefolgt von einer optionalen class_base Spezifikation (§15.2.4)), gefolgt von einem optionalen Satz von type_parameter_constraints_clause s (§15.2.5), gefolgt von einer class_body (§15.2.6), optional gefolgt von einemSemikolon.
Eine Klassendeklaration stellt keine type_parameter_constraints_clausezur Verfügung, es sei denn, sie stellt auch eine type_parameter_list zur Verfügung.
Eine Klassendeklaration, die eine type_parameter_list bereitstellt, ist eine generische Klassendeklaration. Darüber hinaus ist jede Klasse, die in einer generischen Klassendeklaration oder einer generischen Strukturdeklaration geschachtelt ist, selbst eine generische Klassendeklaration, da Typargumente für den enthaltenden Typ bereitgestellt werden müssen, um einen konstruierten Typ (§8.4) zu erstellen.
15.2.2 Klassenmodifizierer
15.2.2.1 Allgemein
Ein class_declaration kann optional eine Sequenz von Klassenmodifizierern enthalten:
class_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'abstract'
| 'sealed'
| 'static'
| unsafe_modifier // unsafe code support
;
unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
Es handelt sich um einen Kompilierungszeitfehler für denselben Modifizierer, der mehrmals in einer Klassendeklaration angezeigt wird.
Der new
Modifizierer ist für geschachtelte Klassen zulässig. Es gibt an, dass die Klasse ein geerbtes Element unter demselben Namen ausblendet, wie in §15.3.5 beschrieben. Es handelt sich um einen Kompilierungszeitfehler für den Modifizierer, der new
in einer Klassendeklaration angezeigt wird, die keine geschachtelte Klassendeklaration ist.
Die public
, protected
, internal
und private
Modifizierer steuern die Barrierefreiheit der Klasse. Abhängig vom Kontext, in dem die Klassendeklaration auftritt, sind einige dieser Modifizierer möglicherweise nicht zulässig (§7.5.2).
Wenn eine Teiltypdeklaration (§15.2.7) eine Barrierefreiheitsspezifikation (über die public
, , protected
, internal
und private
Modifizierer) enthält, muss diese Spezifikation allen anderen Teilen zustimmen, die eine Barrierefreiheitsspezifikation enthalten. Wenn kein Teil eines Teiltyps eine Barrierefreiheitsspezifikation enthält, erhält der Typ die entsprechende Standardbarrierefreiheit (§7.5.2).
Die abstract
Modifizierer sealed
und static
Modifizierer werden in den folgenden Unterclauses erläutert.
15.2.2.2 Abstrakte Klassen
Der abstract
Modifizierer wird verwendet, um anzugeben, dass eine Klasse unvollständig ist und nur als Basisklasse verwendet werden soll. Eine abstrakte Klasse unterscheidet sich von einer nicht abstrakten Klasse auf folgende Weise:
- Eine abstrakte Klasse kann nicht direkt instanziiert werden, und es handelt sich um einen Kompilierungszeitfehler, um den
new
Operator für eine abstrakte Klasse zu verwenden. Es ist zwar möglich, Variablen und Werte zu haben, deren Kompilierungszeittypen abstrahiert sind, diese Variablen und Werte enthalten jedoch notwendigerweisenull
Verweise auf Instanzen von nicht abstrakten Klassen, die von den abstrakten Typen abgeleitet sind. - Eine abstrakte Klasse ist zulässig (aber nicht erforderlich), abstrakte Member zu enthalten.
- Eine abstrakte Klasse kann nicht versiegelt werden.
Wenn eine nicht abstrakte Klasse von einer abstrakten Klasse abgeleitet wird, muss die nicht abstrakte Klasse tatsächliche Implementierungen aller geerbten abstrakten Member enthalten, wodurch diese abstrakten Member überschrieben werden.
Beispiel: Im folgenden Code
abstract class A { public abstract void F(); } abstract class B : A { public void G() {} } class C : B { public override void F() { // Actual implementation of F } }
die abstrakte Klasse
A
führt eine abstrakte MethodeF
ein. KlasseB
führt eine zusätzliche MethodeG
ein, aber da sie keine Implementierung vonF
,B
muss auch als abstrakt deklariert werden. KlassenüberschreibungenC
F
und stellen eine tatsächliche Implementierung bereit. Da es keine abstrakten Member gibtC
,C
ist es zulässig (aber nicht erforderlich), nicht abstrakt zu sein.Endbeispiel
Wenn ein oder mehrere Teile einer partiellen Typdeklaration (§15.2.7) einer Klasse den abstract
Modifizierer enthalten, ist die Klasse abstrakt. Andernfalls ist die Klasse nicht abstrakt.
15.2.2.3 Versiegelte Klassen
Der sealed
Modifizierer wird verwendet, um die Ableitung von einer Klasse zu verhindern. Wenn eine versiegelte Klasse als Basisklasse einer anderen Klasse angegeben wird, tritt ein Kompilierungszeitfehler auf.
Eine versiegelte Klasse kann nicht auch eine abstrakte Klasse sein.
Hinweis: Der
sealed
Modifizierer wird hauptsächlich verwendet, um unbeabsichtigte Ableitungen zu verhindern, ermöglicht aber auch bestimmte Laufzeitoptimierungen. Da eine versiegelte Klasse nicht über abgeleitete Klassen verfügt, ist es insbesondere möglich, Aufrufe virtueller Funktionsmememmationen für versiegelte Klasseninstanzen in nicht virtuelle Aufrufe zu transformieren. Endnote
Wenn ein oder mehrere Teile einer Partialtypdeklaration (§15.2.7) einer Klasse den sealed
Modifizierer enthalten, wird die Klasse versiegelt. Andernfalls wird die Klasse nicht versiegelt.
15.2.2.4 Statische Klassen
15.2.2.4.1 Allgemein
Der static
Modifizierer wird verwendet, um die Klasse zu kennzeichnen, die als statische Klasse deklariert wird. Eine statische Klasse darf nicht instanziiert werden, darf nicht als Typ verwendet werden und darf nur statische Member enthalten. Nur eine statische Klasse kann Deklarationen von Erweiterungsmethoden (§15.6.10) enthalten.
Eine statische Klassendeklaration unterliegt den folgenden Einschränkungen:
- Eine statische Klasse darf keinen Modifizierer enthalten
sealed
abstract
. (Da eine statische Klasse jedoch nicht instanziiert oder abgeleitet werden kann, verhält sie sich so, als wäre sie sowohl versiegelt als auch abstrahiert.) - Eine statische Klasse darf keine class_base Spezifikation (§15.2.4) enthalten und kann keine Basisklasse oder eine Liste der implementierten Schnittstellen explizit angeben. Eine statische Klasse erbt implizit vom Typ
object
. - Eine statische Klasse darf nur statische Member (§15.3.8) enthalten.
Hinweis: Alle Konstanten und geschachtelten Typen werden als statische Member klassifiziert. Endnote
- Eine statische Klasse darf keine Member mit
protected
,private protected
oderprotected internal
deklarierter Barrierefreiheit haben.
Es handelt sich um einen Kompilierungsfehler, um gegen diese Einschränkungen zu verstoßen.
Eine statische Klasse weist keine Instanzkonstruktoren auf. Es ist nicht möglich, einen Instanzkonstruktor in einer statischen Klasse zu deklarieren, und für eine statische Klasse wird kein Standardinstanzkonstruktor (§15.11.5) bereitgestellt.
Die Member einer statischen Klasse sind nicht automatisch statisch, und die Memberdeklarationen enthalten explizit einen static
Modifizierer (mit Ausnahme von Konstanten und geschachtelten Typen). Wenn eine Klasse in einer statischen äußeren Klasse geschachtelt ist, ist die geschachtelte Klasse keine statische Klasse, es sei denn, sie enthält explizit einen static
Modifizierer.
Wenn mindestens ein Teil einer Teiltypdeklaration (§15.2.7) einer Klasse den static
Modifizierer enthält, ist die Klasse statisch. Andernfalls ist die Klasse nicht statisch.
15.2.2.4.2 Verweisen auf statische Klassentypen
Ein namespace_or_type_name (§7.8) darf auf eine statische Klasse verweisen, wenn
- Die namespace_or_type_name ist die
T
in einer namespace_or_type_name des FormularsT.I
oder - Der namespace_or_type-Name ist der
T
in einem typeof_expression (§12.8.18) des Formularstypeof(T)
.
Ein primary_expression (§12.8) darf auf eine statische Klasse verweisen, wenn
- Die primary_expression ist die
E
in einer member_access (§12.8.7) des FormularsE.I
.
In jedem anderen Kontext ist es ein Kompilierungszeitfehler, um auf eine statische Klasse zu verweisen.
Hinweis: Es handelt sich z. B. um einen Fehler für eine statische Klasse, die als Basisklasse, einen Bestandteiltyp (§15.3.7) eines Elements, ein generisches Typargument oder eine Typparametereinschränkung verwendet werden soll. Ebenso kann eine statische Klasse nicht in einem Arraytyp, einem neuen Ausdruck, einem Umwandlungsausdruck, einem Ausdruck, einem Ausdruck, einem Ausdruck als Ausdruck, einem
sizeof
Ausdruck oder einem Standardwertausdruck verwendet werden. Endnote
15.2.3 Typparameter
Ein Typparameter ist ein einfacher Bezeichner, der einen Platzhalter für ein Typargument angibt, das zum Erstellen eines konstruierten Typs bereitgestellt wird. Bei constrast ist ein Typargument (§8.4.2) der Typ, der beim Erstellen eines konstruierten Typs durch den Typparameter ersetzt wird.
type_parameter_list
: '<' type_parameters '>'
;
type_parameters
: attributes? type_parameter
| type_parameters ',' attributes? type_parameter
;
type_parameter ist in §8.5 definiert.
Jeder Typparameter in einer Klassendeklaration definiert einen Namen im Deklarationsraum (§7.3) dieser Klasse. Daher kann er nicht denselben Namen wie ein anderer Typparameter dieser Klasse oder eines In dieser Klasse deklarierten Elements haben. Ein Typparameter darf nicht denselben Namen wie der Typ selbst haben.
Zwei partielle generische Typdeklarationen (im selben Programm) tragen zum gleichen ungebundenen generischen Typ bei, wenn sie denselben vollqualifizierten Namen haben (einschließlich eines generic_dimension_specifier (§12.8.18) für die Anzahl der Typparameter) (§7.8.3). Zwei solche Teiltypdeklarationen müssen denselben Namen für jeden Typparameter in der Reihenfolge angeben.
15.2.4 Klassenbasisspezifikation
15.2.4.1 Allgemein
Eine Klassendeklaration kann eine class_base Spezifikation enthalten, die die direkte Basisklasse der Klasse und die von der Klasse direkt implementierten Schnittstellen (§18) definiert.
class_base
: ':' class_type
| ':' interface_type_list
| ':' class_type ',' interface_type_list
;
interface_type_list
: interface_type (',' interface_type)*
;
15.2.4.2 Basisklassen
Wenn ein class_type in der class_base enthalten ist, gibt es die direkte Basisklasse der klasse an, die deklariert wird. Wenn eine nicht partielle Klassendeklaration keine class_base aufweist oder wenn die class_base nur Schnittstellentypen auflistet, wird die direkte Basisklasse als angenommen object
. Wenn eine partielle Klassendeklaration eine Basisklassenspezifikation enthält, muss diese Basisklassenspezifikation auf denselben Typ verweisen wie alle anderen Teile dieses Teiltyps, die eine Basisklassenspezifikation enthalten. Wenn kein Teil einer partiellen Klasse eine Basisklassenspezifikation enthält, lautet object
die Basisklasse . Eine Klasse erbt Member von ihrer direkten Basisklasse, wie in §15.3.4 beschrieben.
Beispiel: Im folgenden Code
class A {} class B : A {}
Klasse
A
wird als direkte Basisklasse vonB
, undB
soll vonA
abgeleitet werden . DaA
keine direkte Basisklasse explizit angegeben wird, wird die direkte Basisklasse implizitobject
angegeben.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.Enum
oder System.ValueType
der dynamic
Typ. Darüber hinaus darf eine generische Klassendeklaration nicht als direkte oder indirekte Basisklasse (§22.2.1) verwendet System.Attribute
werden.
Bei der Bestimmung der Bedeutung der direkten Basisklassenspezifikation A
einer Klasse B
wird die direkte Basisklasse B
vorübergehend angenommen object
, was sicherstellt, dass die Bedeutung einer Basisklassenspezifikation nicht rekursiv von sich selbst abhängen kann.
Beispiel: Die folgenden
class X<T> { public class Y{} } class Z : X<Z.Y> {}
ist fehlerhaft, da in der Basisklassenspezifikation
X<Z.Y>
die direkte BasisklasseZ
als giltobject
und daher (durch die Regeln von §7.8)Z
nicht als MitgliedY
betrachtet wird.Endbeispiel
Die Basisklassen einer Klasse sind die direkte Basisklasse und ihre Basisklassen. Mit anderen Worten, der Satz von Basisklassen ist das transitive Schließen der direkten Basisklassenbeziehung.
Beispiel: Im Folgenden:
class A {...} class B<T> : A {...} class C<T> : B<IComparable<T>> {...} class D<T> : C<T[]> {...}
die Basisklassen sind
D<int>
C<int[]>
, ,B<IComparable<int[]>>
,A
undobject
.Endbeispiel
Mit Ausnahme der Klasse object
verfügt jede Klasse über genau eine direkte Basisklasse. Die object
Klasse hat keine direkte Basisklasse und ist die ultimative Basisklasse aller anderen Klassen.
Es handelt sich um einen Kompilierungszeitfehler für eine Klasse, die von sich selbst abhängt. Für diese Regel hängt eine Klasse direkt von ihrer direkten Basisklasse (falls vorhanden) ab und hängt direkt von der nächstgelegenen eingeschlossenen Klasse ab, in der sie geschachtelt ist (falls vorhanden). In Anbetracht dieser Definition hängt der vollständige Satz von Klassen, von denen eine Klasse abhängt, vom transitiven Schließen der direkt abhängigen Beziehung ab .
Beispiel: Das Beispiel
class A : A {}
ist fehlerhaft, da die Klasse von sich selbst abhängt. Ebenso ist das Beispiel
class A : B {} class B : C {} class C : A {}
ist fehlerhaft, da die Klassen zirkulär von sich selbst abhängen. Abschließend wird das Beispiel
class A : B.C {} class B : A { public class C {} }
führt zu einem Kompilierzeitfehler, da A von (seiner direkten Basisklasse) abhängt
B.C
, die von (der unmittelbar eingeschlossenen Klasse) abhängtB
, von der zirkel abhängig istA
.Endbeispiel
Eine Klasse hängt nicht von den klassen ab, die darin geschachtelt sind.
Beispiel: Im folgenden Code
class A { class B : A {} }
B
hängt davon ab (da es sich sowohl umA
die direkte Basisklasse als auch um die unmittelbar eingeschlossene Klasse handelt), aberA
nicht vonB
(daB
es sich weder um eine Basisklasse noch um eine eingeschlossene Klasse handeltA
).A
Daher ist das Beispiel gültig.Endbeispiel
Es ist nicht möglich, von einer versiegelten Klasse abzuleiten.
Beispiel: Im folgenden Code
sealed class A {} class B : A {} // Error, cannot derive from a sealed class
Die Klasse
B
ist fehlerhaft, da sie versucht, von der versiegelten KlasseA
abzuleiten.Endbeispiel
15.2.4.3 Schnittstellenimplementierungen
Eine class_base Spezifikation kann eine Liste von Schnittstellentypen enthalten, in diesem Fall wird die Klasse gesagt, die angegebenen Schnittstellentypen zu implementieren. Für einen konstruierten Klassentyp, einschließlich eines geschachtelten Typs, der in einer generischen Typdeklaration (§15.3.9.7) deklariert ist, wird jeder implementierte Schnittstellentyp durch Ersetzen jedes type_parameter in der angegebenen Schnittstelle, der entsprechenden type_argument des konstruierten Typs abgerufen.
Der Satz von Schnittstellen für einen Typ, der in mehreren Teilen deklariert ist (§15.2.7) ist die Vereinigung der schnittstellen, die auf jedem Teil angegeben sind. Eine bestimmte Schnittstelle kann nur einmal auf jedem Teil benannt werden, aber mehrere Teile können die gleichen Basisschnittstellen benennen. Es darf nur eine Implementierung jedes Mitglieds einer bestimmten Schnittstelle geben.
Beispiel: Im Folgenden:
partial class C : IA, IB {...} partial class C : IC {...} partial class C : IA, IB {...}
der Satz von Basisschnittstellen für die Klasse
C
lautetIA
,IB
undIC
.Endbeispiel
In der Regel stellt jeder Teil eine Implementierung der in diesem Teil deklarierten Schnittstellen bereit; Dies ist jedoch keine Voraussetzung. Ein Teil kann die Implementierung für eine Schnittstelle bereitstellen, die auf einem anderen Teil deklariert ist.
Beispiel:
partial class X { int IComparable.CompareTo(object o) {...} } partial class X : IComparable { ... }
Endbeispiel
Die in einer Klassendeklaration angegebenen Basisschnittstellen können Schnittstellentypen (§8.4, §18.2) konstruiert werden. Eine Basisschnittstelle kann nicht eigenständig ein Typparameter sein, obwohl sie die Typparameter einbeziehen kann, die sich im Bereich befinden.
Beispiel: Der folgende Code veranschaulicht, wie eine Klasse konstruierte Typen implementieren und erweitern kann:
class C<U, V> {} interface I1<V> {} class D : C<string, int>, I1<string> {} class E<T> : C<int, T>, I1<T> {}
Endbeispiel
Schnittstellenimplementierungen werden in §18.6 weiter erörtert.
15.2.5 Einschränkungen des Typparameters
Generische Typ- und Methodendeklarationen können optional Typparametereinschränkungen angeben, indem type_parameter_constraints_clauses eingeschlossen werden.
type_parameter_constraints_clauses
: type_parameter_constraints_clause
| type_parameter_constraints_clauses type_parameter_constraints_clause
;
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
| secondary_constraints (',' constructor_constraint)?
| constructor_constraint
;
primary_constraint
: class_type nullable_type_annotation?
| 'class' nullable_type_annotation?
| 'struct'
| 'notnull'
| 'unmanaged'
;
secondary_constraint
: interface_type nullable_type_annotation?
| type_parameter nullable_type_annotation?
;
secondary_constraints
: secondary_constraint (',' secondary_constraint)*
;
constructor_constraint
: 'new' '(' ')'
;
Jede type_parameter_constraints_clause besteht aus dem Token where
, gefolgt vom Namen eines Typparameters, gefolgt von einem Doppelpunkt und der Liste der Einschränkungen für diesen Typparameter. Es kann höchstens eine where
Klausel für jeden Typparameter geben, und die where
Klauseln können in beliebiger Reihenfolge aufgeführt werden. Wie bei den get
Token set
in einem Eigenschaftsaccessor ist das where
Token kein Schlüsselwort.
Die Liste der in einer where
Klausel angegebenen Einschränkungen kann eine der folgenden Komponenten enthalten, in dieser Reihenfolge: eine einzelne primäre Einschränkung, eine oder mehrere sekundäre Einschränkungen und die Konstruktoreinschränkung. new()
Eine primäre Einschränkung kann ein Klassentyp, die Bezugstypeinschränkung, die Werttypeinschränkung, die nicht NULL-Einschränkungnotnull
struct
oder die nicht verwaltete Typeinschränkung unmanaged
class
sein. Der Klassentyp und die Bezugstypeinschränkung können die nullable_type_annotation enthalten.
Eine sekundäre Einschränkung kann eine interface_type oder type_parameter sein, optional gefolgt von einem nullable_type_annotation. Das Vorhandensein der nullable_type_annotatione* gibt an, dass das Typargument der nullable Bezugstyp sein darf, der einem nicht nullablen Bezugstyp entspricht, der die Einschränkung erfüllt.
Die Einschränkung des Bezugstyps gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein Bezugstyp sein soll. Alle Klassentypen, Schnittstellentypen, Delegattypen, Arraytypen und Typparameter, die als Referenztyp (wie unten definiert) bezeichnet werden, erfüllen diese Einschränkung.
Der Klassentyp, die Verweistypeinschränkung und sekundäre Einschränkungen können die Null-Typanmerkung enthalten. Das Vorhandensein oder Fehlen dieser Anmerkung für den Typparameter gibt die Nullbarkeitserwartungen für das Typargument an:
- Wenn die Einschränkung nicht die Nullwerte-Typanmerkung enthält, wird erwartet, dass das Typargument ein nicht nullabler Bezugstyp ist. Ein Compiler gibt möglicherweise eine Warnung aus, wenn das Typargument ein Nullwertverweistyp ist.
- Wenn die Einschränkung die Nullwerte-Typanmerkung enthält, wird die Einschränkung sowohl durch einen nicht nullablen Verweistyp als auch durch einen nullablen Bezugstyp erfüllt.
Die Nullierbarkeit des Typarguments muss nicht mit der Nullierbarkeit des Typparameters übereinstimmen. Der Compiler gibt möglicherweise eine Warnung aus, wenn die Nullierbarkeit des Typparameters nicht mit der Nullierbarkeit des Typarguments übereinstimmt.
Hinweis: Um anzugeben, dass ein Typargument ein nullabler Bezugstyp ist, fügen Sie die Nullable-Typanmerkung nicht als Einschränkung (Verwendung
T : class
oderT : BaseClass
) hinzu, verwenden SieT?
jedoch die allgemeine Deklaration, um den entsprechenden nullablen Bezugstyp für das Typargument anzugeben. Endnote
Die Nullwerte-Typanmerkung kann ?
nicht für ein nicht eingeschränktes Typargument verwendet werden.
Bei einem TypparameterT
, wenn das Typargument ein nullabler Bezugstyp C?
ist, werden Instanzen von T?
als , nicht C??
interpretiertC?
.
Beispiel: Die folgenden Beispiele zeigen, wie sich die Nullierbarkeit eines Typarguments auf die Nullierbarkeit einer Deklaration des Typparameters auswirkt:
public class C { } public static class Extensions { public static void M<T>(this T? arg) where T : notnull { } } public class Test { public void M() { C? mightBeNull = new C(); C notNull = new C(); int number = 5; int? missing = null; mightBeNull.M(); // arg is C? notNull.M(); // arg is C? number.M(); // arg is int? missing.M(); // arg is int? } }
Wenn das Typargument ein nicht nullabler Typ ist, gibt die
?
Typanmerkung an, dass der Parameter der entsprechende nullable Typ ist. Wenn das Typargument bereits ein nullabler Bezugstyp ist, ist der Parameter derselbe nullable Typ.Endbeispiel
Die Nicht-NULL-Einschränkung gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein nicht nullabler Werttyp oder ein nicht nullabler Bezugstyp sein soll. Ein Typargument, das kein nullabler Werttyp ist oder ein nicht nullabler Verweistyp zulässig ist, aber der Compiler kann eine Diagnosewarnung erzeugen.
Die Werttypeinschränkung gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein nicht nullwertebarer Werttyp sein soll. Alle nicht nullbaren Strukturtypen, Enumerationstypen und Typparameter mit der Werttypeinschränkung erfüllen diese Einschränkung. Beachten Sie, dass ein wertfähiger Werttyp (§8.3.12) zwar als Werttyp klassifiziert wurde, die Werttypeinschränkung jedoch nicht erfüllt. Ein Typparameter mit der Werttypeinschränkung darf nicht auch über die constructor_constraint verfügen, obwohl er als Typargument für einen anderen Typparameter mit einem constructor_constraint verwendet werden kann.
Hinweis: Der
System.Nullable<T>
Typ gibt die Nicht-NULL-Werttypeinschränkung fürT
. Rekursiv konstruierte FormenT??
undNullable<Nullable<T>>
sind daher verboten. Endnote
Da unmanaged
es sich nicht um ein Schlüsselwort handelt, ist in primary_constraint die nicht verwaltete Einschränkung immer syntaktisch mehrdeutig mit class_type. Aus Kompatibilitätsgründen wird eine Namenssuche (§12.8.4) des Namens unmanaged
erfolgreich behandelt.class_type
Andernfalls wird sie als nicht verwaltete Einschränkung behandelt.
Die Nicht verwaltete Typeinschränkung gibt an, dass ein Typargument, das für den Typparameter verwendet wird, ein nicht nullabler nicht verwalteter Typ (§8.8) sein soll.
Zeigertypen dürfen niemals Typargumente sein und erfüllen keine Typeinschränkungen, auch nicht verwaltete Typen, obwohl sie nicht verwaltet werden.
Wenn es sich bei einer Einschränkung um einen Klassentyp, einen Schnittstellentyp oder einen Typparameter handelt, gibt dieser Typ einen minimalen "Basistyp" an, den jedes Typargument, das für diesen Typparameter verwendet wird, unterstützen soll. Jedes Mal, wenn ein konstruierter Typ oder eine generische Methode verwendet wird, wird das Typargument auf die Einschränkungen für den Typparameter zur Kompilierungszeit überprüft. Das angegebene Typargument erfüllt die in §8.4.5 beschriebenen Bedingungen.
Eine class_type Einschränkung erfüllt die folgenden Regeln:
- Der Typ muss ein Klassentyp sein.
- Der Typ darf nicht sein
sealed
. - Der Typ darf keine der folgenden Typen sein:
System.Array
oderSystem.ValueType
. - Der Typ darf nicht sein
object
. - Bei den meisten Einschränkungen für einen bestimmten Typparameter kann es sich um einen Klassentyp handeln.
Ein als interface_type Einschränkung festgelegter Typ muss die folgenden Regeln erfüllen:
- Der Typ muss ein Schnittstellentyp sein.
- Ein Typ darf in einer bestimmten
where
Klausel nicht mehr als einmal angegeben werden.
In beiden Fällen kann die Einschränkung einen der Typparameter des zugeordneten Typs oder der Methodendeklaration als Teil eines konstruierten Typs umfassen und den deklarierten Typ umfassen.
Alle klassen- oder Schnittstellentypen, die als Typparametereinschränkung angegeben sind, müssen mindestens so barrierefrei (§7.5.5) sein, wie der generische Typ oder die methode deklariert wird.
Ein als type_parameter Einschränkung festgelegter Typ muss die folgenden Regeln erfüllen:
- Der Typ muss ein Typparameter sein.
- Ein Typ darf in einer bestimmten
where
Klausel nicht mehr als einmal angegeben werden.
Darüber hinaus gibt es keine Zyklen in der Abhängigkeitsdiagramm von Typparametern, wobei abhängigkeit eine transitive Beziehung ist, die durch Folgendes definiert wird:
- Wenn ein Typparameter
T
als Einschränkung für typparameterS
verwendet wird,S
hängt es davonT
ab. - Wenn ein Typparameter
S
von einem TypparameterT
abhängt undT
von einem TypparameterU
abhängt, hängt esS
dann davon abU
.
Bei dieser Beziehung handelt es sich um einen Kompilierungszeitfehler für einen Typparameter, der von sich selbst (direkt oder indirekt) abhängig ist.
Alle Einschränkungen müssen zwischen abhängigen Typparametern konsistent sein. Wenn der Typparameter S
vom Typparameter T
abhängt, dann:
T
darf die Werttypeinschränkung nicht aufweisen. Andernfalls ist effektiv versiegelt,T
sodassS
er gezwungen wäre, denselben Typ zu haben wieT
, wodurch der Bedarf an zwei Typparametern wegfällt.- Wenn
S
die Werttypeinschränkung vorhanden ist,T
darf keine class_type Einschränkung vorhanden sein. - Wenn eine class_type Einschränkung vorhanden ist und
T
eine class_type EinschränkungB
vorhanden ist, muss es eine Konvertierung der Identität oder impliziten Referenzkonvertierung vonA
zuB
oder eine implizite Verweiskonvertierung vonB
inA
.A
S
- Wenn
S
auch vom TypparameterU
abhängt undU
eine class_type EinschränkungA
aufweist undT
eine class_type EinschränkungB
aufweist, muss es eine Identitätskonvertierung oder implizite Verweiskonvertierung von inB
oder eine implizite Verweiskonvertierung vonA
B
in .A
Es ist gültig, S
damit die Werttypeinschränkung und T
die Bezugstypeinschränkung vorhanden ist. Dies beschränkt T
sich effektiv auf die Typen System.Object
, System.ValueType
, , System.Enum
und alle Schnittstellentypen.
Wenn die where
Klausel für einen Typparameter eine Konstruktoreinschränkung enthält (die das Formular new()
hat), ist es möglich, den new
Operator zum Erstellen von Instanzen des Typs zu verwenden (§12.8.17.2). Jedes Typargument, das für einen Typparameter mit einer Konstruktoreinschränkung verwendet wird, ist ein Werttyp, eine nicht abstrakte Klasse mit einem öffentlichen parameterlosen Konstruktor oder ein Typparameter mit der Werttypeinschränkung oder Konstruktoreinschränkung.
Es handelt sich um einen Kompilierungszeitfehler für type_parameter_constraints mit einer primary_constraint oder struct
unmanaged
einer constructor_constraint.
Beispiel: Im Folgenden sind Beispiele für Einschränkungen aufgeführt:
interface IPrintable { void Print(); } interface IComparable<T> { int CompareTo(T value); } interface IKeyProvider<T> { T GetKey(); } class Printer<T> where T : IPrintable {...} class SortedList<T> where T : IComparable<T> {...} class Dictionary<K,V> where K : IComparable<K> where V : IPrintable, IKeyProvider<K>, new() { ... }
Das folgende Beispiel ist fehlerhaft, da es eine Zirkelzahl im Abhängigkeitsdiagramm der Typparameter verursacht:
class Circular<S,T> where S: T where T: S // Error, circularity in dependency graph { ... }
Die folgenden Beispiele veranschaulichen zusätzliche ungültige Situationen:
class Sealed<S,T> where S : T where T : struct // Error, `T` is sealed { ... } class A {...} class B {...} class Incompat<S,T> where S : A, T where T : B // Error, incompatible class-type constraints { ... } class StructWithClass<S,T,U> where S : struct, T where T : U where U : A // Error, A incompatible with struct { ... }
Endbeispiel
Die dynamische Löschung eines Typs C
Cₓ
wird wie folgt erstellt:
- Wenn
C
es sich um einen geschachtelten TypOuter.Inner
handelt, handelt es sich umCₓ
einen geschachtelten TypOuterₓ.Innerₓ
. - Wenn
C
Cₓ
es sich um einen konstruierten TypG<A¹, ..., Aⁿ>
mit TypargumentenA¹, ..., Aⁿ
handelt, handelt es sich umCₓ
den konstruierten TypG<A¹ₓ, ..., Aⁿₓ>
. - Wenn
C
es sich um einen ArraytypE[]
handelt, handelt es sich umCₓ
den ArraytypEₓ[]
. - Wenn
C
es sich um eine dynamische Datei handelt, istobject
diesCₓ
der Wert . - Andernfalls lautet
Cₓ
C
.
Die effektive Basisklasse eines Typparameters T
wird wie folgt definiert:
Lassen Sie uns R
eine Reihe von Typen so sein, dass:
- Für jede Einschränkung, die
T
ein Typparameter ist,R
enthält die effektive Basisklasse. - Für jede Einschränkung, die
T
ein Strukturtyp ist,R
enthältSystem.ValueType
. - Enthält für jede Einschränkung dieses
T
Typs einen EnumerationstypSystem.Enum
R
. - Für jede Einschränkung, die
T
ein Delegattyp ist,R
enthält die dynamische Löschung. - Für jede Einschränkung, die
T
ein Arraytyp ist,R
enthältSystem.Array
. - Für jede Einschränkung, die
T
ein Klassentyp ist,R
enthält die dynamische Löschung.
Then
- Wenn
T
die Werttypeinschränkung vorhanden ist, istSystem.ValueType
die effektive Basisklasse . R
Andernfalls ist die effektive Basisklasseobject
leer.- Andernfalls ist die effektive Basisklasse
T
der am meisten eingeschlossene Typ (§10.5.3) des SatzesR
. Wenn der Satz keinen eingeschlossenen Typ aufweist, istobject
die effektive Basisklasse vonT
. Die Konsistenzregeln stellen sicher, dass der umfassendste Typ vorhanden ist.
Wenn der Typparameter ein Methodentypparameter ist, dessen Einschränkungen von der Basismethode geerbt werden, wird die effektive Basisklasse nach der Typersetzung berechnet.
Diese Regeln stellen sicher, dass die effektive Basisklasse immer ein class_type ist.
Der effektive Schnittstellensatz eines Typparameters T
wird wie folgt definiert:
- Wenn
T
keine secondary_constraints vorhanden ist, ist der effektive Schnittstellensatz leer. - Wenn
T
interface_type Einschränkungen, aber keine type_parameter Einschränkungen vorhanden sind, ist der effektive Schnittstellensatz die dynamische Löschung ihrer interface_type Einschränkungen. - Wenn
T
keine interface_type Einschränkungen vorhanden sind, aber type_parameter Einschränkungen aufweisen, ist der effektive Schnittstellensatz die Vereinigung der effektiven Schnittstellensätze ihrer type_parameter Einschränkungen. - Wenn
T
sowohl interface_type Einschränkungen als auch type_parameter Einschränkungen vorhanden sind, ist der effektive Schnittstellensatz die Vereinigung der dynamischen Löschungen ihrer interface_type Einschränkungen und die effektiven Schnittstellensätze ihrer type_parameter Einschränkungen.
Ein Typparameter ist als Bezugstyp bekannt, wenn er die Bezugstypeinschränkung aufweist oder seine effektive Basisklasse nicht object
oder System.ValueType
. Ein Typparameter ist als nicht nullabler Bezugstyp bekannt, wenn er als Bezugstyp bekannt ist und die Nicht-NULL-Bezugstypeinschränkung aufweist.
Werte eines eingeschränkten Typparametertyps können verwendet werden, um auf die Instanzmember zuzugreifen, die von den Einschränkungen impliziert werden.
Beispiel: Im Folgenden:
interface IPrintable { void Print(); } class Printer<T> where T : IPrintable { void PrintOne(T x) => x.Print(); }
Die Methoden
IPrintable
können direktx
aufgerufen werden, daT
sie auf die ImplementierungIPrintable
beschränkt ist.Endbeispiel
Wenn eine partielle generische Typdeklaration Einschränkungen enthält, stimmen die Einschränkungen allen anderen Teilen zu, die Einschränkungen enthalten. Insbesondere müssen alle Teile, die Einschränkungen enthalten, Einschränkungen für den gleichen Satz von Typparametern aufweisen, und für jeden Typparameter müssen die Gruppen der primären, sekundären und Konstruktoreinschränkungen gleichwertig sein. Zwei Gruppen von Einschränkungen sind gleichwertig, wenn sie dieselben Member enthalten. Wenn kein Teil eines partiellen generischen Typs Typenparametereinschränkungen angibt, werden die Typparameter als nicht eingeschränkt betrachtet.
Beispiel:
partial class Map<K,V> where K : IComparable<K> where V : IKeyProvider<K>, new() { ... } partial class Map<K,V> where V : IKeyProvider<K>, new() where K : IComparable<K> { ... } partial class Map<K,V> { ... }
ist richtig, da diese Teile, die Einschränkungen (die ersten beiden) enthalten, effektiv denselben Satz von primären, sekundären und Konstruktoreinschränkungen für denselben Satz von Typparametern angeben.
Endbeispiel
15.2.6 Klassentext
Die class_body einer Klasse definiert die Member dieser Klasse.
class_body
: '{' class_member_declaration* '}'
;
15.2.7 Teilerklärungen
Der Modifizierer partial
wird beim Definieren einer Klasse, Struktur oder eines Schnittstellentyps in mehreren Teilen verwendet. Der partial
Modifizierer ist ein kontextbezogenes Schlüsselwort (§6.4.4) und hat nur eine besondere Bedeutung unmittelbar vor einem der Schlüsselwörter class
, , struct
oder interface
.
Jeder Teil einer Teiltypdeklaration muss einen partial
Modifizierer enthalten und muss im selben Namespace oder im selben Typ wie die anderen Teile deklariert werden. Der partial
Modifizierer gibt an, dass an anderer Stelle zusätzliche Teile der Typdeklaration vorhanden sein können, aber das Vorhandensein solcher zusätzlichen Teile ist keine Anforderung; er ist gültig für die einzige Deklaration eines Typs, um den partial
Modifizierer einzuschließen. Es ist nur für eine Deklaration eines Teiltyps gültig, um die Basisklasse oder implementierte Schnittstellen einzuschließen. Alle Deklarationen einer Basisklasse oder implementierter Schnittstellen müssen jedoch übereinstimmen, einschließlich der Nullierbarkeit aller angegebenen Typargumente.
Alle Teile eines Teiltyps müssen zusammen zusammengestellt werden, sodass die Teile zur Kompilierungszeit zusammengeführt werden können. Partielle Typen lassen nicht zu, dass bereits kompilierte Typen erweitert werden.
Geschachtelte Typen können mithilfe des partial
Modifizierers in mehreren Teilen deklariert werden. In der Regel wird der enthaltende Typ auch verwendet partial
, und jeder Teil des geschachtelten Typs wird in einem anderen Teil des enthaltenden Typs deklariert.
Beispiel: Die folgende partielle Klasse wird in zwei Teilen implementiert, die sich in unterschiedlichen Kompilierungseinheiten befinden. Der erste Teil wird von einem Datenbankzuordnungstool generiert, während der zweite Teil manuell erstellt wird:
public partial class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } } // File: Customer2.cs public partial class Customer { public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }
Wenn die beiden obigen Teile zusammen kompiliert werden, verhält sich der resultierende Code wie folgt, als ob die Klasse als einzelne Einheit geschrieben wurde:
public class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }
Endbeispiel
Die Behandlung von Attributen, die für den Typ oder die Typparameter verschiedener Teile einer Teildeklaration angegeben sind, wird in §22.3 erläutert.
15.3 Klassenmitglieder
15.3.1 Allgemein
Die Member einer Klasse bestehen aus den Membern, die durch ihre class_member_declarationund die Von der direkten Basisklasse geerbten Member eingeführt wurden.
class_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| finalizer_declaration
| static_constructor_declaration
| type_declaration
;
Die Mitglieder einer Klasse sind in die folgenden Kategorien unterteilt:
- Konstanten, die konstanten Werte darstellen, die der Klasse zugeordnet sind (§15.4).
- Felder, die die Variablen der Klasse sind (§15.5).
- Methoden, die die Berechnungen und Aktionen implementieren, die von der Klasse ausgeführt werden können (§15.6).
- Eigenschaften, die benannte Merkmale und die Aktionen definieren, die mit dem Lesen und Schreiben dieser Merkmale verbunden sind (§15.7).
- Ereignisse, die Benachrichtigungen definieren, die von der Klasse generiert werden können (§15.8).
- Indexer, die zulassen, dass Instanzen der Klasse auf die gleiche Weise (syntaktisch) wie Arrays (§15.9) indiziert werden können.
- Operatoren, die die Ausdrucksoperatoren definieren, die auf Instanzen der Klasse angewendet werden können (§15.10).
- Instanzkonstruktoren, die die zum Initialisieren von Instanzen der Klasse erforderlichen Aktionen implementieren (§15.11)
- Finalizer, die die auszuführenden Aktionen implementieren, bevor Instanzen der Klasse endgültig verworfen werden (§15.13).
- Statische Konstruktoren, die die zum Initialisieren der Klasse erforderlichen Aktionen implementieren (§15.12).
- Typen, die die Typen darstellen, die für die Klasse lokal sind (§14.7).
Ein class_declaration erstellt einen neuen Deklarationsraum (§7.3) und die type_parameters und die class_member_declarationunmittelbar in der class_declaration neue Member in diesen Deklarationsbereich einführen. Die folgenden Regeln gelten für class_member_declaration:
Instanzkonstruktoren, Finalizer und statische Konstruktoren haben denselben Namen wie die unmittelbar eingeschlossene Klasse. Alle anderen Mitglieder haben Namen, die sich vom Namen der unmittelbar eingeschlossenen Klasse unterscheiden.
Der Name eines Typparameters in der type_parameter_list einer Klassendeklaration unterscheidet sich von den Namen aller anderen Typparameter in demselben type_parameter_list und unterscheidet sich von dem Namen der Klasse und den Namen aller Member der Klasse.
Der Name eines Typs unterscheidet sich von den Namen aller Nichttypmitglieder, die in derselben Klasse deklariert sind. Wenn zwei oder mehr Typdeklarationen denselben vollqualifizierten Namen aufweisen, müssen die Deklarationen den
partial
Modifizierer (§15.2.7) haben und diese Deklarationen kombinieren, um einen einzelnen Typ zu definieren.
Hinweis: Da der vollqualifizierte Name einer Typdeklaration die Anzahl der Typparameter codiert, können zwei unterschiedliche Typen denselben Namen aufweisen, solange sie unterschiedliche Anzahl von Typparametern haben. Endnote
Der Name einer Konstante, eines Felds, einer Eigenschaft oder eines Ereignisses unterscheidet sich von den Namen aller anderen Elemente, die in derselben Klasse deklariert sind.
Der Name einer Methode unterscheidet sich von den Namen aller anderen Nichtmethoden, die in derselben Klasse deklariert sind. Darüber hinaus unterscheidet sich die Signatur (§7.6) einer Methode von den Signaturen aller anderen Methoden, die in derselben Klasse deklariert sind, und zwei Methoden, die in derselben Klasse deklariert sind, dürfen keine Signaturen aufweisen, die sich ausschließlich von
in
,out
, 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
ref
undout
.Die Signatur eines Indexers unterscheidet sich von den Signaturen aller anderen in derselben Klasse deklarierten Indexer.
Die Signatur eines Betreibers unterscheidet sich von den Signaturen aller anderen Betreiber, die in derselben Klasse deklariert sind.
Die geerbten Member einer Klasse (§15.3.4) sind nicht Teil des Deklarationsraums einer Klasse.
Hinweis: Daher kann eine abgeleitete Klasse ein Element mit demselben Namen oder derselben Signatur wie ein geerbtes Element deklarieren (wodurch das geerbte Element tatsächlich ausgeblendet wird). Endnote
Der Satz von Mitgliedern eines Typs, der in mehreren Teilen deklariert ist (§15.2.7) ist die Vereinigung der in jedem Teil deklarierten Mitglieder. Die Körper aller Teile der Typdeklaration haben denselben Deklarationsbereich (§7.3), und der Umfang jedes Mitglieds (§7.7) erstreckt sich auf die Körper aller Teile. Die Barrierefreiheitsdomäne jedes Mitglieds umfasst immer alle Teile des eingeschlossenen Typs; Ein in einem Teil deklariertes privates Mitglied ist frei zugänglich von einem anderen Teil. Es handelt sich um einen Kompilierungsfehler, um dasselbe Element in mehr als einem Teil des Typs zu deklarieren, es sei denn, dieses Element ist ein Typ mit dem partial
Modifizierer.
Beispiel:
partial class A { int x; // Error, cannot declare x more than once partial class Inner // Ok, Inner is a partial type { int y; } } partial class A { int x; // Error, cannot declare x more than once partial class Inner // Ok, Inner is a partial type { int z; } }
Endbeispiel
Die Feldinitialisierungsreihenfolge kann innerhalb des C#-Codes erheblich sein, und einige Garantien werden gemäß §15.5.6.1 bereitgestellt. Andernfalls ist die Reihenfolge von Elementen innerhalb eines Typs selten signifikant, kann aber bei der Interfacierung mit anderen Sprachen und Umgebungen erheblich sein. In diesen Fällen ist die Reihenfolge von Elementen innerhalb eines Typs, der in mehreren Teilen deklariert ist, nicht definiert.
15.3.2 Der Instanztyp
Jede Klassendeklaration weist einen zugeordneten Instanztyp auf. Bei einer generischen Klassendeklaration wird der Instanztyp durch Erstellen eines konstruierten Typs (§8.4) aus der Typdeklaration gebildet, wobei jedes der angegebenen Typargumente der entsprechende Typparameter ist. Da der Instanztyp die Typparameter verwendet, kann er nur verwendet werden, wo sich die Typparameter im Bereich befinden. d. h. innerhalb der Klassendeklaration. Der Instanztyp ist der Typ für this
Code, der in der Klassendeklaration geschrieben wurde. Bei nicht generischen Klassen ist der Instanztyp einfach die deklarierte Klasse.
Beispiel: Im Folgenden werden mehrere Klassendeklarationen zusammen mit ihren Instanztypen gezeigt:
class A<T> // instance type: A<T> { class B {} // instance type: A<T>.B class C<U> {} // instance type: A<T>.C<U> } class D {} // instance type: D
Endbeispiel
15.3.3 Elemente konstruierter Typen
Die nicht geerbten Member eines konstruierten Typs werden durch Substituieren der einzelnen type_parameter in der Memberdeklaration durch die entsprechende type_argument des konstruierten Typs abgerufen. Der Ersetzungsprozess basiert auf der semantischen Bedeutung von Typdeklarationen und ist nicht einfach nur textbezogene Ersetzung.
Beispiel: Aufgrund der generischen Klassendeklaration
class Gen<T,U> { public T[,] a; public void G(int i, T t, Gen<U,T> gt) {...} public U Prop { get {...} set {...} } public int H(double d) {...} }
der konstruierte Typ
Gen<int[],IComparable<string>>
hat die folgenden Elemente:public int[,][] a; public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...} public IComparable<string> Prop { get {...} set {...} } public int H(double d) {...}
Der Typ des Elements
a
in der generischen KlassendeklarationGen
ist "zweidimensionales Array vonT
", sodass der Typ des Elementsa
im obigen konstruierten Typ "zweidimensionales Array eines eindimensionalen Arrays vonint
" oderint[,][]
.Endbeispiel
Innerhalb von Instanzfunktionsmitgliedern ist der Typ des this
Instanztyps (§15.3.2) der enthaltenden Deklaration.
Alle Member einer generischen Klasse können Typparameter aus jeder eingeschlossenen Klasse verwenden, entweder direkt oder als Teil eines konstruierten Typs. Wenn zur Laufzeit ein bestimmter geschlossener konstruierter Typ (§8.4.3) verwendet wird, wird jede Verwendung eines Typparameters durch das Typargument ersetzt, das für den konstruierten Typ bereitgestellt wird.
Beispiel:
class C<V> { public V f1; public C<V> f2; public C(V x) { this.f1 = x; this.f2 = this; } } class Application { static void Main() { C<int> x1 = new C<int>(1); Console.WriteLine(x1.f1); // Prints 1 C<double> x2 = new C<double>(3.1415); Console.WriteLine(x2.f1); // Prints 3.1415 } }
Endbeispiel
15.3.4 Vererbung
Eine Klasse erbt die Member der direkten Basisklasse. Vererbung bedeutet, dass eine Klasse implizit alle Member ihrer direkten Basisklasse enthält, mit Ausnahme der Instanzkonstruktoren, Finalizer und statischen Konstruktoren der Basisklasse. Einige wichtige Aspekte der Vererbung sind:
Vererbung ist transitiv. Wenn
C
von , undB
wird vonA
abgeleitet, dann erbt dieC
in deklariertenB
Member sowie die in deklarierten Member.A
B
Eine abgeleitete Klasse erweitert ihre direkte Basisklasse. Eine abgeleitete Klasse kann den geerbten Membern neue Member hinzufügen, aber die Definition eines geerbten Members kann nicht entfernt werden.
Instanzkonstruktoren, Finalizer und statische Konstruktoren werden nicht geerbt, aber alle anderen Member sind unabhängig von ihrer deklarierten Barrierefreiheit (§7.5). Je nach deklarierter Barrierefreiheit sind geerbte Member jedoch in einer abgeleiteten Klasse möglicherweise nicht zugänglich.
Eine abgeleitete Klasse kann geerbte Member (§7.7.2.3) ausblenden, indem neue Member mit demselben Namen oder derselben Signatur deklariert werden. Durch das Ausblenden eines geerbten Elements wird dieses Element jedoch nicht entfernt, sondern nur, dass dieses Element direkt über die abgeleitete Klasse nicht darauf zugreifen kann.
Eine Instanz einer Klasse enthält einen Satz aller Instanzfelder, die in der Klasse und deren Basisklassen deklariert sind, und eine implizite Konvertierung (§10.2.8) besteht aus einem abgeleiteten Klassentyp in einen seiner Basisklassentypen. Daher kann ein Verweis auf eine Instanz einiger abgeleiteter Klassen als Verweis auf eine Instanz einer seiner Basisklassen behandelt werden.
Eine Klasse kann virtuelle Methoden, Eigenschaften, Indexer und Ereignisse deklarieren und abgeleitete Klassen können die Implementierung dieser Funktionsmember überschreiben. Dadurch können Klassen polymorphes Verhalten aufweisen, wobei die aktionen, die von einem Funktionselementaufruf ausgeführt werden, je nach Laufzeittyp der Instanz variieren, über die dieses Funktionselement aufgerufen wird.
Die geerbten Member eines konstruierten Klassentyps sind die Member des unmittelbaren Basisklassentyps (§15.2.4.2), die durch Substituieren der Typargumente des konstruierten Typs für jedes Vorkommen der entsprechenden Typparameter in der base_class_specification gefunden werden. Diese Member werden wiederum durch Substituieren für jede type_parameter in der Mitgliedsdeklaration durch die entsprechende type_argument der base_class_specification transformiert.
Beispiel:
class B<U> { public U F(long index) {...} } class D<T> : B<T[]> { public T G(string s) {...} }
Im obigen Code verfügt der konstruierte Typ
D<int>
über ein nicht geerbtes Element,G(string s)
int
das durch Das Ersetzen des Typargumentsint
für den TypparameterT
abgerufen wird.D<int>
verfügt außerdem über ein geerbtes Element aus der KlassendeklarationB
. Dieses geerbte Element wird bestimmt, indem zuerst der BasisklassentypB<int[]>
D<int>
durch SubstituierenT
int
in der BasisklassenspezifikationB<T[]>
bestimmt wird. Anschließend wird als TypargumentB
für "int[]
inpublic U F(long index)
" das geerbte Elementpublic int[] F(long index)
zurückgegebenU
.Endbeispiel
15.3.5 Der neue Modifizierer
Ein class_member_declaration darf ein Mitglied mit demselben Namen oder derselben Signatur wie ein geerbtes Mitglied deklarieren. Wenn dies geschieht, wird das abgeleitete Klassenmememmelement zum Ausblenden des Basisklassenelements gesagt. Siehe §7.7.2.3 für eine genaue Spezifikation, wann ein Mitglied ein geerbtes Mitglied ausblendet.
Ein geerbtes Element M
wird als verfügbar betrachtet, wenn M
auf sie zugegriffen werden kann, und es gibt kein anderes geerbtes, barrierefreies Element N, das bereits ausgeblendet wirdM
. Das implizite Ausblenden eines geerbten Elements wird nicht als Fehler betrachtet, führt jedoch dazu, dass der Compiler eine Warnung ausgibt, es sei denn, die Deklaration des abgeleiteten Klassenmembers enthält einen new
Modifizierer, um explizit anzugeben, dass das abgeleitete Element das Basiselement ausblenden soll. Wenn mindestens ein Teil einer Teildeklaration (§15.2.7) eines geschachtelten Typs den new
Modifizierer enthält, wird keine Warnung ausgegeben, wenn der geschachtelte Typ ein verfügbares geerbtes Element ausblendet.
Wenn ein new
Modifizierer in einer Deklaration enthalten ist, die kein verfügbares geerbtes Element ausblendet, wird eine Warnung zu diesem Effekt ausgegeben.
15.3.6 Zugriffsmodifizierer
Eine class_member_declaration kann eine der zulässigen Arten von deklarierter Barrierefreiheit (§7.5.2): public
, , protected
protected internal
, , private protected
, internal
oder private
. Mit Ausnahme der protected internal
Kombinationen ist private protected
es ein Kompilierungszeitfehler, um mehrere Zugriffsmodifizierer anzugeben. Wenn ein class_member_declaration keine Zugriffsmodifizierer enthält, private
wird davon ausgegangen.
15.3.7 Bestandteiltypen
Typen, die in der Deklaration eines Elements verwendet werden, werden als Bestandteiltypen dieses Elements bezeichnet. Mögliche Komponententypen sind der Typ einer Konstante, eines Felds, einer Eigenschaft, eines Ereignisses oder eines Indexers, der Rückgabetyp einer Methode oder eines Operators sowie die Parametertypen einer Methode, eines Indexers, eines Operators oder eines Instanzkonstruktors. Die Bestandteiltypen eines Mitglieds sind mindestens so zugänglich wie dieses Mitglied selbst (§7.5.5).
15.3.8 Statische und Instanzmitglieder
Member einer Klasse sind statische Member oder Instanzmember.
Hinweis: Im Allgemeinen ist es hilfreich, statische Member als Zugehörigkeit zu Klassen und Instanzmbern als Zugehörigkeit zu Objekten (Instanzen von Klassen) zu betrachten. Endnote
Wenn ein Feld, eine Methode, eine Eigenschaft, ein Ereignis, ein Operator oder eine Konstruktordeklaration einen static
Modifizierer enthält, deklariert es ein statisches Element. Darüber hinaus deklariert eine Konstante oder Typdeklaration implizit ein statisches Element. Statische Member weisen die folgenden Merkmale auf:
- Wenn auf ein statisches Element
M
in einem member_access (§12.8.7) des FormularsE.M
verwiesen wird,E
wird ein Typ mit einem ElementM
bezeichnet. Es handelt sich um einen Kompilierungszeitfehler, umE
eine Instanz zu kennzeichnen. - Ein statisches Feld in einer nicht generischen Klasse identifiziert genau einen Speicherort. Unabhängig davon, wie viele Instanzen einer nicht generischen Klasse erstellt werden, gibt es immer nur eine Kopie eines statischen Felds. Jeder unterschiedliche geschlossene konstruierte Typ (§8.4.3) verfügt über einen eigenen Satz statischer Felder, unabhängig von der Anzahl der Instanzen des geschlossenen konstruierten Typs.
- Ein statisches Funktionselement (Methode, Eigenschaft, Ereignis, Operator oder Konstruktor) wird nicht für eine bestimmte Instanz ausgeführt, und es handelt sich um einen Kompilierungszeitfehler, der in einem solchen Funktionselement darauf verweist.
Wenn eine Feld-, Methoden-, Eigenschafts-, Ereignis-, Indexer-, Konstruktor- oder Finalizerdeklaration keinen statischen Modifizierer enthält, deklariert sie ein Instanzmemm. (Ein Instanzmitglied wird manchmal als nicht statisches Element bezeichnet.) Instanzmitglieder weisen die folgenden Merkmale auf:
- Wenn auf ein Instanzmitglied
M
in einem member_access (§12.8.7) des FormularsE.M
verwiesen wird,E
wird eine Instanz eines Typs bezeichnet, der über ein MitgliedM
verfügt. Es handelt sich um einen Bindungszeitfehler, bei dem E einen Typ angibt. - Jede Instanz einer Klasse enthält einen separaten Satz aller Instanzfelder der Klasse.
- Ein Instanzfunktionsmemm (Methode, Eigenschaft, Indexer, Instanzkonstruktor oder Finalizer) wird für eine bestimmte Instanz der Klasse ausgeführt, und auf diese Instanz kann zugegriffen
this
werden (§12.8.14).
Beispiel: Im folgenden Beispiel werden die Regeln für den Zugriff auf statische und Instanzmmber veranschaulicht:
class Test { int x; static int y; void F() { x = 1; // Ok, same as this.x = 1 y = 1; // Ok, same as Test.y = 1 } static void G() { x = 1; // Error, cannot access this.x y = 1; // Ok, same as Test.y = 1 } static void Main() { Test t = new Test(); t.x = 1; // Ok t.y = 1; // Error, cannot access static member through instance Test.x = 1; // Error, cannot access instance member through type Test.y = 1; // Ok } }
Die
F
Methode zeigt, dass in einem Instanzfunktionsmember ein simple_name (§12.8.4) für den Zugriff auf Instanzmember und statische Member verwendet werden kann. DieG
Methode zeigt, dass es sich bei einem statischen Funktionsmememm um einen Kompilierungszeitfehler handelt, um über eine simple_name auf ein Instanzelement zuzugreifen. DieMain
Methode zeigt, dass in einem member_access (§12.8.7) Instanzmember über Instanzen zugegriffen werden soll, und statische Member müssen über Typen aufgerufen werden.Endbeispiel
15.3.9 Geschachtelte Typen
15.3.9.1 Allgemein
Ein in einer Klasse oder Struktur deklarierter Typ wird als geschachtelter Typ bezeichnet. Ein Typ, der in einer Kompilierungseinheit oder einem Namespace deklariert wird, wird als nicht geschachtelter Typ bezeichnet.
Beispiel: Im folgenden Beispiel:
class A { class B { static void F() { Console.WriteLine("A.B.F"); } } }
Die Klasse
B
ist ein geschachtelter Typ, da sie innerhalb der KlasseA
deklariert ist und die KlasseA
ein nicht geschachtelter Typ ist, da sie in einer Kompilierungseinheit deklariert wird.Endbeispiel
15.3.9.2 Vollqualifizierter Name
Der vollqualifizierte Name (§7.8.3) für eine geschachtelte Typdeklaration S.N
, wobei S
der vollqualifizierte Name der Typdeklaration, in der der Typ N
deklariert wird, N
und der nicht qualifizierte Name (§7.8.2) der Deklaration des geschachtelten Typs (einschließlich aller generic_dimension_specifier (§12.8.18)) ist.
15.3.9.3 Barrierefreiheit deklariert
Nicht geschachtelte Typen können Barrierefreiheit aufweisen public
oder internal
deklariert haben und standardmäßig die Barrierefreiheit deklariert haben internal
. Geschachtelte Typen können auch diese Formen der deklarierten Barrierefreiheit aufweisen, sowie eine oder mehrere zusätzliche Formen der deklarierten Barrierefreiheit, je nachdem, ob der enthaltende Typ eine Klasse oder Struktur ist:
- Ein geschachtelter Typ, der in einer Klasse deklariert ist, kann über eine der zulässigen Arten deklarierter Barrierefreiheit verfügen und wie andere Klassenmember standardmäßig die
private
deklarierte Barrierefreiheit aufweisen. - Ein geschachtelter Typ, der in einer Struktur deklariert ist, kann drei Formen der deklarierten Barrierefreiheit (
public
,internal
oderprivate
) aufweisen und, wie andere Strukturmember, standardmäßig deklarierteprivate
Barrierefreiheit aufweisen.
Beispiel: Das Beispiel
public class List { // Private data structure private class Node { public object Data; public Node? Next; public Node(object data, Node? next) { this.Data = data; this.Next = next; } } private Node? first = null; private Node? last = null; // Public interface public void AddToFront(object o) {...} public void AddToBack(object o) {...} public object RemoveFromFront() {...} public object RemoveFromBack() {...} public int Count { get {...} } }
deklariert eine private geschachtelte Klasse
Node
.Endbeispiel
15.3.9.4 Ausblenden
Ein geschachtelter Typ kann ein Basiselement (§7.7.2.2) ausblenden. Der new
Modifizierer (§15.3.5) ist für geschachtelte Typdeklarationen zulässig, sodass das Ausblenden explizit ausgedrückt werden kann.
Beispiel: Das Beispiel
class Base { public static void M() { Console.WriteLine("Base.M"); } } class Derived: Base { public new class M { public static void F() { Console.WriteLine("Derived.M.F"); } } } class Test { static void Main() { Derived.M.F(); } }
zeigt eine geschachtelte Klasse
M
an, die dieM
inBase
.Endbeispiel
15.3.9.5 dieser Zugriff
Ein geschachtelter Typ und sein enthaltender Typ haben keine besondere Beziehung zu this_access (§12.8.14). Insbesondere kann innerhalb eines geschachtelten Typs nicht verwendet werden, this
um auf Instanzenmember des enthaltenden Typs zu verweisen. In Fällen, in denen ein geschachtelter Typ Zugriff auf die Instanzmember des zugehörigen Typs benötigt, kann der Zugriff bereitgestellt werden, indem er die this
Instanz des enthaltenden Typs als Konstruktorargument für den geschachtelten Typ bereitstellt.
Beispiel: Das folgende Beispiel
class C { int i = 123; public void F() { Nested n = new Nested(this); n.G(); } public class Nested { C this_c; public Nested(C c) { this_c = c; } public void G() { Console.WriteLine(this_c.i); } } } class Test { static void Main() { C c = new C(); c.F(); } }
zeigt diese Technik. Eine Instanz von erstellt eine Instanz von
Nested
C
, und übergibt diesNested
an den Konstruktor, um nachfolgenden Zugriff auf die Instanzmember zuC
ermöglichen.Endbeispiel
15.3.9.6 Zugriff auf private und geschützte Member des enthaltenden Typs
Ein geschachtelter Typ hat Zugriff auf alle Member, auf die der enthaltende Typ zugreifen kann, einschließlich Elemente des enthaltenden Typs, die Barrierefreiheit aufweisen und protected
deklariert habenprivate
.
Beispiel: Das Beispiel
class C { private static void F() => Console.WriteLine("C.F"); public class Nested { public static void G() => F(); } } class Test { static void Main() => C.Nested.G(); }
zeigt eine Klasse
C
mit einer geschachtelten KlasseNested
an. InnerhalbNested
der Methode wird die in der Methode definierte statische MethodeG
F
aufgerufenC
undF
die private Barrierefreiheit deklariert.Endbeispiel
Ein geschachtelter Typ kann auch auf geschützte Elemente zugreifen, die in einem Basistyp des zugehörigen Typs definiert sind.
Beispiel: Im folgenden Code
class Base { protected void F() => Console.WriteLine("Base.F"); } class Derived: Base { public class Nested { public void G() { Derived d = new Derived(); d.F(); // ok } } } class Test { static void Main() { Derived.Nested n = new Derived.Nested(); n.G(); } }
die geschachtelte Klasse
Derived.Nested
greift auf die inDerived
der Basisklasse definierte geschützte MethodeF
zu,Base
indem sie eine Instanz vonDerived
.Endbeispiel
15.3.9.7 Geschachtelte Typen in generischen Klassen
Eine generische Klassendeklaration kann geschachtelte Typdeklarationen enthalten. Die Typparameter der eingeschlossenen Klasse können innerhalb der geschachtelten Typen verwendet werden. Eine geschachtelte Typdeklaration kann zusätzliche Typparameter enthalten, die nur für den geschachtelten Typ gelten.
Jede Typdeklaration, die in einer generischen Klassendeklaration enthalten ist, ist implizit eine generische Typdeklaration. Beim Schreiben eines Verweises auf einen Typ, der in einem generischen Typ geschachtelt ist, muss der enthaltende konstruierte Typ, einschließlich seiner Typargumente, benannt werden. Aus der äußeren Klasse kann der geschachtelte Typ jedoch ohne Qualifikation verwendet werden; Der Instanztyp der äußeren Klasse kann implizit beim Erstellen des geschachtelten Typs verwendet werden.
Beispiel: Im Folgenden werden drei verschiedene richtige Methoden zum Verweisen auf einen konstruierten Typ gezeigt, der erstellt
Inner
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 Membernamen
15.3.10.1 Allgemein
Um die zugrunde liegende C#-Laufzeitimplementierung zu erleichtern, reserviert die Implementierung für jede Quellmitgliedsdeklaration, die eine Eigenschaft, ein Ereignis oder einen Indexer ist, zwei Methodensignaturen basierend auf der Art der Memberdeklaration, ihrem Namen und ihrem Typ (§15.3.10.2, §15.3.10.3, §15.3.10.4). Es handelt sich um einen Kompilierzeitfehler für ein Programm, um ein Mitglied zu deklarieren, dessen Signatur mit einer Signatur übereinstimmt, die von einem Mitglied reserviert ist, das im selben Bereich deklariert ist, auch wenn die zugrunde liegende Laufzeitimplementierung diese Reservierungen nicht verwendet.
Die reservierten Namen führen keine Deklarationen ein, daher nehmen sie nicht an der Mitgliedersuche teil. Die zugeordneten reservierten Methodensignaturen einer Deklaration nehmen jedoch an der Vererbung (§15.3.4) teil und können mit dem new
Modifizierer (§15.3.5) ausgeblendet werden.
Hinweis: Die Reservierung dieser Namen dient drei Zwecken:
- 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.
Endnote
Die Deklaration eines Finalizers (§15.13) bewirkt auch, dass eine Signatur reserviert wird (§15.3.10.5).
Bestimmte Namen sind für die Verwendung als Operatormethodennamen reserviert (§15.3.10.6).
15.3.10.2 Membernamen für Eigenschaften reserviert
Für eine Eigenschaft P
(§15.7) vom Typ T
sind die folgenden Signaturen reserviert:
T get_P();
void set_P(T value);
Beide Signaturen sind reserviert, auch wenn die Eigenschaft schreibgeschützt oder schreibgeschützt ist.
Beispiel: Im folgenden Code
class A { public int P { get => 123; } } class B : A { public new int get_P() => 456; public new void set_P(int value) { } } class Test { static void Main() { B b = new B(); A a = b; Console.WriteLine(a.P); Console.WriteLine(b.P); Console.WriteLine(b.get_P()); } }
Eine Klasse
A
definiert eine schreibgeschützte EigenschaftP
, wodurch Signaturen fürget_P
und Methoden reserviert werdenset_P
.A
Die KlasseB
wird von beiden reservierten Signaturen abgeleitetA
und ausgeblendet. Das Beispiel erzeugt die Ausgabe:123 123 456
Endbeispiel
15.3.10.3 Membernamen, die für Ereignisse reserviert sind
Für ein Ereignis E
(§15.8) des Delegatentyps T
sind die folgenden Signaturen reserviert:
void add_E(T handler);
void remove_E(T handler);
15.3.10.4 Membernamen, die für Indexer reserviert sind
Für einen Indexer (§15.9) vom Typ T
mit Parameterliste L
sind die folgenden Signaturen reserviert:
T get_Item(L);
void set_Item(L, T value);
Beide Signaturen sind reserviert, auch wenn der Indexer schreibgeschützt oder schreibgeschützt ist.
Darüber hinaus ist der Membername Item
reserviert.
15.3.10.5 Mitgliedsnamen für Finalizer reserviert
Für eine Klasse mit einem Finalizer (§15.13) ist die folgende Signatur reserviert:
void Finalize();
15.3.10.6 Methodennamen, die für Operatoren reserviert sind
Die folgenden Methodennamen sind reserviert. Während viele entsprechende Operatoren in dieser Spezifikation haben, sind einige für die Verwendung durch zukünftige Versionen reserviert, während einige für die Interoperabilität mit anderen Sprachen reserviert sind.
Methodenname | C#-Operator |
---|---|
op_Addition |
+ (binär) |
op_AdditionAssignment |
(reserviert) |
op_AddressOf |
(reserviert) |
op_Assign |
(reserviert) |
op_BitwiseAnd |
& (binär) |
op_BitwiseAndAssignment |
(reserviert) |
op_BitwiseOr |
\| |
op_BitwiseOrAssignment |
(reserviert) |
op_CheckedAddition |
(reserviert für die zukünftige Nutzung) |
op_CheckedDecrement |
(reserviert für die zukünftige Nutzung) |
op_CheckedDivision |
(reserviert für die zukünftige Nutzung) |
op_CheckedExplicit |
(reserviert für die zukünftige Nutzung) |
op_CheckedIncrement |
(reserviert für die zukünftige Nutzung) |
op_CheckedMultiply |
(reserviert für die zukünftige Nutzung) |
op_CheckedSubtraction |
(reserviert für die zukünftige Nutzung) |
op_CheckedUnaryNegation |
(reserviert für die zukünftige Nutzung) |
op_Comma |
(reserviert) |
op_Decrement |
-- (Präfix und Postfix) |
op_Division |
/ |
op_DivisionAssignment |
(reserviert) |
op_Equality |
== |
op_ExclusiveOr |
^ |
op_ExclusiveOrAssignment |
(reserviert) |
op_Explicit |
explizite (schmalende) Koersion |
op_False |
false |
op_GreaterThan |
> |
op_GreaterThanOrEqual |
>= |
op_Implicit |
implizite (Verbreiterung) Koersion |
op_Increment |
++ (Präfix und Postfix) |
op_Inequality |
!= |
op_LeftShift |
<< |
op_LeftShiftAssignment |
(reserviert) |
op_LessThan |
< |
op_LessThanOrEqual |
<= |
op_LogicalAnd |
(reserviert) |
op_LogicalNot |
! |
op_LogicalOr |
(reserviert) |
op_MemberSelection |
(reserviert) |
op_Modulus |
% |
op_ModulusAssignment |
(reserviert) |
op_MultiplicationAssignment |
(reserviert) |
op_Multiply |
* (binär) |
op_OnesComplement |
~ |
op_PointerDereference |
(reserviert) |
op_PointerToMemberSelection |
(reserviert) |
op_RightShift |
>> |
op_RightShiftAssignment |
(reserviert) |
op_SignedRightShift |
(reserviert) |
op_Subtraction |
- (binär) |
op_SubtractionAssignment |
(reserviert) |
op_True |
true |
op_UnaryNegation |
- (unär) |
op_UnaryPlus |
+ (unär) |
op_UnsignedRightShift |
(reserviert für die zukünftige Nutzung) |
op_UnsignedRightShiftAssignment |
(reserviert) |
15.4 Konstanten
Eine Konstante ist ein Klassenelement, das einen Konstantenwert darstellt: ein Wert, der zur Kompilierungszeit berechnet werden kann. Eine constant_declaration führt eine oder mehrere Konstanten eines bestimmten Typs ein.
constant_declaration
: attributes? constant_modifier* 'const' type constant_declarators ';'
;
constant_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;
Ein constant_declaration kann einen Satz von Attributen (§22), einen new
Modifizierer (§15.3.5) und eine der zulässigen Arten deklarierter Barrierefreiheit (§15.3.6) enthalten. Die Attribute und Modifizierer gelten für alle Elemente, die vom constant_declaration deklariert wurden. Auch wenn Konstanten als statische Member betrachtet werden, erfordert ein constant_declaration weder einen Modifizierer noch lässt einen static
Modifizierer zu. Es handelt sich um einen Fehler für denselben Modifizierer, der mehrmals in einer Konstantendeklaration angezeigt wird.
Der Typ eines constant_declaration gibt den Typ der elemente an, die durch die Deklaration eingeführt wurden. Auf den Typ folgt eine Liste der constant_declarator s (§13.6.3), von denen jedes ein neuesMitglied einführt. Ein constant_declarator besteht aus einem Bezeichner , der das Element benennt, gefolgt von einem "=
"-Token, gefolgt von einem constant_expression (§12.23), der den Wert des Elements angibt.
Der in einer Konstantendeklaration angegebene Typ muss , , byte
, short
, ushort
, int
, uint
, long
, char
decimal
string
ulong
bool
double
float
ein enum_type oder ein reference_type sein.sbyte
Jede constant_expression erhält einen Wert des Zieltyps oder eines Typs, der durch eine implizite Konvertierung (§10.2) in den Zieltyp konvertiert werden kann.
Der Typ einer Konstante muss mindestens so zugänglich sein wie die Konstante selbst (§7.5.5).
Der Wert einer Konstante wird in einem Ausdruck mithilfe eines simple_name (§12.8.4) oder einer member_access (§12.8.7) abgerufen.
Eine Konstante kann sich selbst an einer constant_expression beteiligen. Daher kann eine Konstante in jedem Konstrukt verwendet werden, das eine constant_expression erfordert.
Hinweis: Beispiele für solche Konstrukte sind
case
Bezeichnungen,goto case
Anweisungen,enum
Memberdeklarationen, Attribute und andere Konstantendeklarationen. Endnote
Hinweis: Wie in §12.23 beschrieben, ist ein constant_expression ein Ausdruck, der zur Kompilierungszeit vollständig ausgewertet werden kann. Da die einzige Möglichkeit zum Erstellen eines Nicht-Null-Werts einer anderen reference_type als
string
die Anwendung desnew
Operators ist und da dernew
Operator in einem constant_expression nicht zulässig ist, ist der einzige mögliche Wert für Konstanten von reference_typeanderen alsstring
.null
Endnote
Wenn ein symbolischer Name für einen Konstantenwert gewünscht wird, aber wenn der Typ dieses Werts in einer Konstantendeklaration nicht zulässig ist oder wenn der Wert nicht zur Kompilierungszeit von einem constant_expression berechnet werden kann, kann stattdessen ein Readonly-Feld (§15.5.3) verwendet werden.
Hinweis: Die Versionsverwaltungssemantik von
const
undreadonly
unterscheidet sich (§15.5.3.3). Endnote
Eine Konstantendeklaration, die mehrere Konstanten deklariert, entspricht mehreren Deklarationen einzelner Konstanten mit denselben Attributen, Modifizierern und Typ.
Beispiel:
class A { public const double X = 1.0, Y = 2.0, Z = 3.0; }
für die folgende Syntax:
class A { public const double X = 1.0; public const double Y = 2.0; public const double Z = 3.0; }
Endbeispiel
Konstanten dürfen von anderen Konstanten innerhalb desselben Programms abhängig sein, solange die Abhängigkeiten nicht zirkulär sind. Der Compiler ordnet automatisch die Konstantendeklarationen in der entsprechenden Reihenfolge an.
Beispiel: Im folgenden Code
class A { public const int X = B.Z + 1; public const int Y = 10; } class B { public const int Z = A.Y + 1; }
Der Compiler wertet
A.Y
zuerst aus, wertet dann aus und wertetB.Z
A.X
schließlich aus, erzeugt die Werte10
,11
und12
.Endbeispiel
Konstantendeklarationen können von Konstanten aus anderen Programmen abhängen, aber solche Abhängigkeiten sind nur in einer Richtung möglich.
Beispiel: Wenn auf das obige
A
Beispiel verwiesen undB
in separaten Programmen deklariert wurde, wäre es möglichA.X
, von diesen abhängig zu seinB.Z
, aberB.Z
dann nicht gleichzeitig davon abhängig zu seinA.Y
. Endbeispiel
15.5 Felder
15.5.1 Allgemein
Ein Feld ist ein Element, das eine Variable darstellt, die einem Objekt oder einer Klasse zugeordnet ist. Ein field_declaration führt ein oder mehrere Felder eines bestimmten Typs ein.
field_declaration
: attributes? field_modifier* type variable_declarators ';'
;
field_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'readonly'
| 'volatile'
| unsafe_modifier // unsafe code support
;
variable_declarators
: variable_declarator (',' variable_declarator)*
;
variable_declarator
: identifier ('=' variable_initializer)?
;
unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
Ein field_declaration kann einen Satz von Attributen (§22), einen new
Modifizierer (§15.3.5), eine gültige Kombination der vier Zugriffsmodifizierer (§15.3.6) und einen static
Modifizierer (§15.5.2) enthalten. Darüber hinaus kann ein field_declaration einen readonly
Modifizierer (§15.5.3) oder einen volatile
Modifizierer (§15.5.4) enthalten, jedoch nicht beide. Die Attribute und Modifizierer gelten für alle Elemente, die vom field_declaration deklariert wurden. Es ist ein Fehler für denselben Modifizierer, der mehrmals in einem field_declaration angezeigt wird.
Der Typ eines field_declaration gibt den Typ der elemente an, die durch die Deklaration eingeführt wurden. Auf den Typ folgt eine Liste der variable_declarator, von denen jedes ein neues Mitglied einführt. Ein variable_declarator besteht aus einem Bezeichner , der das Element benennt, optional gefolgt von einem "=
"-Token und einem variable_initializer (§15.5.6), der den Anfangswert dieses Elements angibt.
Der Typ eines Felds muss mindestens so zugänglich sein wie das Feld selbst (§7.5.5).
Der Wert eines Felds wird in einem Ausdruck mithilfe eines simple_name (§12.8.4), eines member_access (§12.8.7) oder eines base_access (§12.8.15) abgerufen. Der Wert eines nicht gelesenen Felds wird mithilfe einer Zuordnung geändert (§12.21). Der Wert eines nicht readonly-Felds kann mit postfix-Inkrement- und Dekrementoperatoren (§12.8.16) und Präfixoperatoren (§12.9.6) abgerufen und geändert werden.
Eine Felddeklaration, die mehrere Felder deklariert, entspricht mehreren Deklarationen einzelner Felder mit denselben Attributen, Modifizierern und Typ.
Beispiel:
class A { public static int X = 1, Y, Z = 100; }
für die folgende Syntax:
class A { public static int X = 1; public static int Y; public static int Z = 100; }
Endbeispiel
15.5.2 Statische Felder und Instanzfelder
Wenn eine Felddeklaration einen static
Modifizierer enthält, sind die durch die Deklaration eingeführten Felder statische Felder. Wenn kein static
Modifizierer vorhanden ist, sind die durch die Deklaration eingeführten Felder Instanzfelder. Statische Felder und Instanzfelder sind zwei der verschiedenen Arten von Variablen (§9), die von C# unterstützt werden, und manchmal werden sie als statische Variablen bzw. Instanzvariablen bezeichnet.
Wie in §15.3.8 erläutert, enthält jede Instanz einer Klasse einen vollständigen Satz der Instanzfelder der Klasse, während es nur einen Satz statischer Felder für jeden nicht generischen oder geschlossenen konstruierten Typ gibt, unabhängig von der Anzahl der Instanzen der Klasse oder des geschlossenen konstruierten Typs.
15.5.3 Readonly-Felder
15.5.3.1 Allgemein
Wenn ein field_declaration einen Modifizierer enthält, sind die durch die Deklaration eingeführten Felder schreibgeschützt.readonly
Direkte Zuordnungen zu schreibgeschützten Feldern können nur als Teil dieser Deklaration oder in einem Instanzkonstruktor oder statischen Konstruktor in derselben Klasse auftreten. (In diesen Kontexten kann ein readonly-Feld mehrmals zugewiesen werden.) Insbesondere sind direkte Zuordnungen zu einem Readonly-Feld nur in den folgenden Kontexten zulässig:
- In der variable_declarator , die das Feld einführt (indem ein variable_initializer in die Deklaration eingeschlossen wird).
- Für ein Beispielfeld, in den Instanzkonstruktoren der Klasse, die die Felddeklaration enthält; für ein statisches Feld im statischen Konstruktor der Klasse, die die Felddeklaration enthält. Dies sind auch die einzigen Kontexte, in denen es gültig ist, ein Readonly-Feld als Ausgabe- oder Referenzparameter zu übergeben.
Der Versuch, einem lesegeschützten Feld zuzuweisen oder als Ausgabe- oder Referenzparameter in einem anderen Kontext zu übergeben, ist ein Kompilierungsfehler.
15.5.3.2 Verwenden statischer Readonly-Felder für Konstanten
Ein statisches Readonly-Feld ist nützlich, wenn ein symbolischer Name für einen Konstantenwert gewünscht wird, aber wenn der Typ des Werts in einer Konstdeklaration nicht zulässig ist oder wenn der Wert zur Kompilierung nicht berechnet werden kann.
Beispiel: Im folgenden Code
public class Color { public static readonly Color Black = new Color(0, 0, 0); public static readonly Color White = new Color(255, 255, 255); public static readonly Color Red = new Color(255, 0, 0); public static readonly Color Green = new Color(0, 255, 0); public static readonly Color Blue = new Color(0, 0, 255); private byte red, green, blue; public Color(byte r, byte g, byte b) { red = r; green = g; blue = b; } }
die
Black
,White
,Red
,Green
undBlue
Member können nicht als Const-Member deklariert werden, da ihre Werte nicht zur Kompilierungszeit berechnet werden können. Das Deklarierenstatic readonly
dieser Elemente hat jedoch viel die gleiche Wirkung.Endbeispiel
15.5.3.3 Versionsverwaltung von Konstanten und statischen Readonly-Feldern
Konstanten und Readonly-Felder weisen unterschiedliche binäre Versionsverwaltungssemantik auf. Wenn ein Ausdruck auf eine Konstante verweist, wird der Wert der Konstante zur Kompilierungszeit abgerufen, aber wenn ein Ausdruck auf ein readonly-Feld verweist, wird der Wert des Felds erst während der Laufzeit abgerufen.
Beispiel: Betrachten Sie eine Anwendung, die aus zwei separaten Programmen besteht:
namespace Program1 { public class Utils { public static readonly int x = 1; } }
und
namespace Program2 { class Test { static void Main() { Console.WriteLine(Program1.Utils.X); } } }
Die
Program1
Namespaces weisenProgram2
zwei Programme auf, die separat kompiliert werden. DaProgram1.Utils.X
sie alsstatic readonly
Feld deklariert wird, wird die Wertausgabe derConsole.WriteLine
Anweisung zur Kompilierungszeit nicht bekannt, sondern zur Laufzeit abgerufen. Wenn der Wert geändertX
undProgram1
neu kompiliert wird, gibt dieConsole.WriteLine
Anweisung den neuen Wert aus, auch wennProgram2
er nicht neu kompiliert wird. WennX
es sich jedoch um eine Konstante handelte, wäre der WertX
zum ZeitpunktProgram2
der Kompilierung erhalten worden und würde von ÄnderungenProgram1
Program2
bis zum erneuten Kompilieren unberührt bleiben.Endbeispiel
15.5.4 Veränderliche Felder
Wenn ein field_declaration einen Modifizierer enthält, sind die durch diese Deklaration eingeführten Felder veränderliche Felder.volatile
Für nicht veränderliche Felder können Optimierungstechniken, die Anweisungen neu anordnen, zu unerwarteten und unvorhersehbaren Ergebnissen in Multithread-Programmen führen, die ohne Synchronisierung auf Felder zugreifen, z. B. die von der lock_statement (§13.13). Diese Optimierungen können vom Compiler, vom Laufzeitsystem oder von Hardware ausgeführt werden. Bei veränderliche Felder sind solche Neuanordnungsoptimierungen eingeschränkt:
- Ein Lesen eines veränderliche Felds wird als veränderliche Lesefunktion bezeichnet. Ein veränderliches Lesen hat "Semantik erwerben"; d. h., es wird garantiert vor allen Verweisen auf den Speicher auftreten, die nach der Anweisungssequenz auftreten.
- Ein Schreibvorgang eines veränderliche Felds wird als veränderliche Schreibzugriff bezeichnet. Ein veränderliche Schreibvorgang hat "Releasesemantik"; d. h., es ist garantiert, dass nach allen Speicherverweisen vor der Schreibanweisung in der Anweisungssequenz geschehen.
Diese Einschränkungen stellen sicher, dass alle Threads volatile-Schreibvorgänge, die von einem anderen Thread ausgeführt werden, in der Reihenfolge ihrer Ausführung berücksichtigen. Eine konforme Implementierung ist nicht erforderlich, um eine einzige Gesamtanzahl von veränderliche Schreibvorgängen bereitzustellen, wie aus allen Threads der Ausführung zu sehen. Die Art eines veränderliche Felds muss eine der folgenden Sein:
- Eine reference_type.
- Eine type_parameter , die als Bezugstyp bekannt ist (§15.2.5).
- Der Typ
byte
, ,sbyte
,short
,int
ushort
,uint
,char
, ,float
,bool
, , oderSystem.IntPtr
System.UIntPtr
. . - Ein enum_type einen enum_base Typ von
byte
, ,sbyte
,short
,ushort
, oderint
uint
.
Beispiel: Das Beispiel
class Test { public static int result; public static volatile bool finished; static void Thread2() { result = 143; finished = true; } static void Main() { finished = false; // Run Thread2() in a new thread new Thread(new ThreadStart(Thread2)).Start(); // Wait for Thread2() to signal that it has a result // by setting finished to true. for (;;) { if (finished) { Console.WriteLine($"result = {result}"); return; } } } }
erzeugt die Ausgabe:
result = 143
In diesem Beispiel startet die Methode
Main
einen neuen Thread, der die MethodeThread2
ausführt. Mit dieser Methode wird ein Wert in einem nicht veränderliche Feld gespeichert, das dannresult
im veränderliche Feldfinished
gespeicherttrue
wird. Der Hauptthread wartet auf das Feld, auf das das Feldfinished
festgelegttrue
ist, und liest dann das Feldresult
vor. Dafinished
deklariertvolatile
wurde, muss der Hauptthread den Wert143
aus dem Feldresult
lesen. Wenn das Feldfinished
nicht deklariertvolatile
wurde, wäre es zulässig, dass der Speicherresult
nach dem Speichernfinished
für den Hauptthread sichtbar ist, und damit der Hauptthread den Wert 0 aus dem Feldresult
liest. Das Deklarierenfinished
alsvolatile
Feld verhindert eine solche Inkonsistenz.Endbeispiel
15.5.5 Feldinitialisierung
Der Anfangswert eines Felds, unabhängig davon, ob es sich um ein statisches Feld oder ein Instanzfeld handelt, ist der Standardwert (§9.3) des Feldtyps. Es ist nicht möglich, den Wert eines Felds zu beobachten, bevor diese Standardinitialisierung erfolgt ist, und ein Feld ist daher nie "nicht initialisiert".
Beispiel: Das Beispiel
class Test { static bool b; int i; static void Main() { Test t = new Test(); Console.WriteLine($"b = {b}, i = {t.i}"); } }
erzeugt die Ausgabe
b = False, i = 0
da
b
undi
beide automatisch in Standardwerte initialisiert werden.Endbeispiel
15.5.6 Variable Initialisierer
15.5.6.1 Allgemein
Felddeklarationen können variable_initializers enthalten. Bei statischen Feldern entsprechen Variableninitialisierer Zuordnungsanweisungen, die während der Klasseninitialisierung ausgeführt werden. Beispielsweise Felder entsprechen variablen Initialisierern Zuordnungsanweisungen, die ausgeführt werden, wenn eine Instanz der Klasse erstellt wird.
Beispiel: Das Beispiel
class Test { static double x = Math.Sqrt(2.0); int i = 100; string s = "Hello"; static void Main() { Test a = new Test(); Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}"); } }
erzeugt die Ausgabe
x = 1.4142135623730951, i = 100, s = Hello
da eine Zuordnung
x
ausgeführt wird, wenn statische Feldinitialisierer ausgeführt und zugewieseni
s
werden, wenn die Instanzfeldinitialisierer ausgeführt werden.Endbeispiel
Die in §15.5.5.5 beschriebene Standardwertinitialisierung erfolgt für alle Felder, einschließlich Feldern mit Variableninitialisierern. Wenn eine Klasse initialisiert wird, werden daher alle statischen Felder in dieser Klasse zuerst mit ihren Standardwerten initialisiert, und dann werden die statischen Feldinitialisierer in textualer Reihenfolge ausgeführt. Ebenso werden beim Erstellen einer Instanz einer Klasse zuerst alle Instanzfelder in dieser Instanz mit ihren Standardwerten initialisiert, und dann werden die Instanzfeldinitialisierer in textualer Reihenfolge ausgeführt. Wenn es Felddeklarationen in mehreren partiellen Typdeklarationen für denselben Typ gibt, ist die Reihenfolge der Teile nicht angegeben. Innerhalb jedes Teils werden die Feldinitialisierer jedoch in der Reihenfolge ausgeführt.
Es ist möglich, dass statische Felder mit variablen Initialisierern im Standardwertzustand beobachtet werden.
Beispiel: Dies wird jedoch dringend als Stilsache abgeraten. Das Beispiel
class Test { static int a = b + 1; static int b = a + 1; static void Main() { Console.WriteLine($"a = {a}, b = {b}"); } }
zeigt dieses Verhalten an. Trotz der Zirkeldefinitionen und
a
b
-definitionen ist das Programm gültig. Es führt zu der Ausgabea = 1, b = 2
da die statischen Felder
a
und initialisiert0
werden (der Standardwert fürint
) bevor ihre Initialisiererb
ausgeführt werden. Wenn der Initialisierer füra
die Ausführung ausgeführt wird, ist der Wert nullb
und wird dahera
initialisiert in1
. Wenn der Initialisierer ausgeführtb
wird, ist der Wert eines bereits1
vorhanden und wird daherb
initialisiert in2
.Endbeispiel
15.5.6.2 Initialisierung statischer Felder
Die Initialisierer einer statischen Feldvariablen einer Klasse entsprechen einer Abfolge von Zuordnungen, die in der Textreihenfolge ausgeführt werden, in der sie in der Klassendeklaration angezeigt werden (§15.5.6.1). Innerhalb einer Teilklasse wird die Bedeutung von "Textreihenfolge" durch §15.5.6.1 angegeben. Wenn ein statischer Konstruktor (§15.12) in der Klasse vorhanden ist, erfolgt die Ausführung der statischen Feldinitialisierer unmittelbar vor der Ausführung dieses statischen Konstruktors. Andernfalls werden die statischen Feldinitialisierer zu einer implementierungsabhängigen Zeit vor der ersten Verwendung eines statischen Felds dieser Klasse ausgeführt.
Beispiel: Das Beispiel
class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { public static int X = Test.F("Init A"); } class B { public static int Y = Test.F("Init B"); }
kann entweder die Ausgabe erzeugen:
Init A Init B 1 1
oder die Ausgabe:
Init B Init A 1 1
da die Ausführung von
X
's initializer' undY
's initializer in beiden Reihenfolge auftreten kann; sie sind nur eingeschränkt, bevor die Verweise auf diese Felder erfolgen. Im Beispiel ist jedoch Folgendes zu beachten:class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { static A() {} public static int X = Test.F("Init A"); } class B { static B() {} public static int Y = Test.F("Init B"); }
die Ausgabe ist zu folgendem Ergebnis zu führen:
Init B Init A 1 1
da die Regeln für die Ausführung statischer Konstruktoren (wie in §15.12 definiert) angeben, dass
B
der statische Konstruktor (und damitB
auch statische Feldinitialisierer) vorA
den statischen Konstruktoren und Feldinitialisierern ausgeführt werden muss.Endbeispiel
15.5.6.3 Instanzfeldinitialisierung
Die Instanzfeldvariableninitialisierer einer Klasse entsprechen einer Abfolge von Zuordnungen, die unmittelbar nach dem Eintrag zu einem der Instanzkonstruktoren (§15.11.3) dieser Klasse ausgeführt werden. Innerhalb einer Teilklasse wird die Bedeutung von "Textreihenfolge" durch §15.5.6.1 angegeben. Die Variableninitialisierer werden in der Textreihenfolge ausgeführt, in der sie in der Klassendeklaration (§15.5.6.1) angezeigt werden. Der Erstellungs- und Initialisierungsprozess der Klasseninstanz wird in §15.11 weiter beschrieben.
Ein variabler Initialisierer für ein Instanzfeld kann nicht auf die erstellte Instanz verweisen. Daher ist es ein Kompilierungszeitfehler, auf den in einem Variableninitialisierer verwiesen this
wird, da es sich um einen Kompilierungszeitfehler für einen Variableninitialisierer handelt, um über eine simple_name auf ein Instanzmemm zu verweisen.
Beispiel: Im folgenden Code
class A { int x = 1; int y = x + 1; // Error, reference to instance member of this }
Der Variable initializer für
y
ergebnisse in einem Kompilierungszeitfehler, da er auf ein Element der zu erstellenden Instanz verweist.Endbeispiel
15.6 Methoden
15.6.1 Allgemein
Eine Methode ist ein Member, das eine Berechnung oder eine Aktion implementiert, die durch ein Objekt oder eine Klasse durchgeführt werden kann. Methoden werden mit method_declaration sdeklariert:
method_declaration
: attributes? method_modifiers return_type method_header method_body
| attributes? ref_method_modifiers ref_kind ref_return_type method_header
ref_method_body
;
method_modifiers
: method_modifier* 'partial'?
;
ref_kind
: 'ref'
| 'ref' 'readonly'
;
ref_method_modifiers
: ref_method_modifier*
;
method_header
: member_name '(' parameter_list? ')'
| member_name type_parameter_list '(' parameter_list? ')'
type_parameter_constraints_clause*
;
method_modifier
: ref_method_modifier
| 'async'
;
ref_method_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
return_type
: ref_return_type
| 'void'
;
ref_return_type
: type
;
member_name
: identifier
| interface_type '.' identifier
;
method_body
: block
| '=>' null_conditional_invocation_expression ';'
| '=>' expression ';'
| ';'
;
ref_method_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
Grammatiknotizen:
- unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
- bei der Anerkennung einer method_body , wenn sowohl die alternativen null_conditional_invocation_expression als auch die Ausdrucksalternativen anwendbar sind, wird der erste Ausgewählt.
Hinweis: Die Überschneidung von Alternativen und Priorität zwischen diesen Alternativen dient ausschließlich der beschreibenden Einfachheit. Die Grammatikregeln könnten ausgearbeitet werden, um die Überschneidung zu entfernen. ANTLR und andere Grammatiksysteme übernehmen den gleichen Komfort und method_body weist die angegebene Semantik automatisch auf. Endnote
Eine method_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), der new
(§15.3.5), static
(§15.6.3), virtual
(§15.3) enthalten.6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7), extern
(§15.6.8) und async
(§15.15) Modifizierer.
Eine Deklaration verfügt über eine gültige Kombination aus Modifizierern, wenn alle folgenden Bedingungen erfüllt sind:
- Die Deklaration enthält eine gültige Kombination aus Zugriffsmodifizierern (§15.3.6).
- Die Deklaration enthält nicht mehrmals denselben Modifizierer.
- Die Deklaration enthält höchstens einen der folgenden Modifizierer:
static
, ,virtual
undoverride
. - Die Deklaration enthält höchstens einen der folgenden Modifizierer:
new
undoverride
. - Wenn die Deklaration den
abstract
Modifizierer enthält, enthält die Deklaration keine der folgenden Modifizierer:static
, , ,virtual
sealed
oderextern
. - Wenn die Deklaration den
private
Modifizierer enthält, enthält die Deklaration keine der folgenden Modifizierer:virtual
, ,override
oderabstract
. - Wenn die Deklaration den
sealed
Modifizierer enthält, enthält die Deklaration auch denoverride
Modifizierer. - Wenn die Deklaration den
partial
Modifizierer enthält, enthält sie keine der folgenden Modifizierer:new
, , ,public
private
sealed
virtual
protected
internal
,override
, , oder .abstract
extern
Methoden werden nach dem klassifiziert, was, wenn etwas, sie zurückgeben:
- Wenn
ref
vorhanden, wird die Methode nach Ref zurückgegeben und gibt einen Variablenverweis zurück, der optional schreibgeschützt ist; - Andernfalls gibt die Methode, wenn return_type ist
void
, den Wert "no-value" zurück und gibt keinen Wert zurück. - Andernfalls wird die Methode zurückgegeben nach Wert und gibt einen Wert zurück.
Die return_type einer Rückgabe-nach-Wert- oder Rückgabe-no-Wert-Methodendeklaration gibt den Typ des Ergebnisses an, falls vorhanden, von der Methode zurückgegeben. Nur eine Rückgabe-no-Wert-Methode kann den partial
Modifizierer (§15.6.9) enthalten. Wenn die Deklaration den async
Modifizierer enthält, muss return_type sein void
oder die Methode gibt nach Wert zurück, und der Rückgabetyp ist ein Vorgangstyp (§15.15.1).
Die ref_return_type einer Rückgabe-nach-Bezug-Methodendeklaration gibt den Typ der Variablen an, auf die von der methode zurückgegebenen variable_reference verwiesen wird.
Eine generische Methode ist eine Methode, deren Deklaration eine type_parameter_list enthält. Dadurch werden die Typparameter für die Methode angegeben. Die optionalen type_parameter_constraints_clause angeben die Einschränkungen für die Typparameter.
Eine generische method_declaration für eine explizite Schnittstellenmemberimplementierung darf keine type_parameter_constraints_clauses aufweisen; die Deklaration erbt alle Einschränkungen von den Einschränkungen für die Schnittstellenmethode.
Ebenso darf eine Methodendeklaration mit dem override
Modifizierer keine type_parameter_constraints_clausehaben, und die Einschränkungen der Typparameter der Methode werden von der virtuellen Methode geerbt, die überschrieben wird.
Die member_name gibt den Namen der Methode an. Es sei denn, die Methode ist eine explizite Schnittstellenmemmimplementierung (§18.6.2), ist die member_name einfach ein Bezeichner.
Bei einer expliziten Schnittstellenmememimplementierung besteht die member_name aus einem interface_type gefolgt von einem ".
" und einem Bezeichner. In diesem Fall enthält die Erklärung keine anderen Modifizierer als (möglicherweise) extern
oder async
.
Die optionale parameter_list gibt die Parameter der Methode an (§15.6.2).
Die return_type oder ref_return_type und alle typen, auf die in der parameter_list einer Methode verwiesen wird, müssen mindestens so zugänglich sein wie die Methode selbst (§7.5.5).
Die method_body einer Rückgabe-nach-Wert- oder Rückgabe-No-Value-Methode ist entweder ein Semikolon, ein Blocktext oder ein Ausdruckstext. Ein Blocktext besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn die Methode aufgerufen wird. Ein Ausdruckstext =>
besteht aus , gefolgt von einem null_conditional_invocation_expression oder Ausdruck und einem Semikolon und zeigt einen einzelnen Ausdruck an, der ausgeführt werden soll, wenn die Methode aufgerufen wird.
Bei abstrakten und externen Methoden besteht die method_body einfach aus einem Semikolon. Bei Teilmethoden kann die method_body entweder aus einem Semikolon, einem Blocktext oder einem Ausdruckstext bestehen. Bei allen anderen Methoden ist der method_body entweder ein Blocktext oder ein Ausdruckstext.
Besteht die method_body aus einem Semikolon, enthält die Deklaration nicht den async
Modifizierer.
Die ref_method_body einer Returns-by-Ref-Methode ist entweder ein Semikolon, ein Blocktext oder ein Ausdruckstext. Ein Blocktext besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn die Methode aufgerufen wird. Ein Ausdruckstext =>
besteht aus , gefolgt von ref
, einem variable_reference und einem Semikolon und gibt einen einzelnen variable_reference an, der ausgewertet werden soll, wenn die Methode aufgerufen wird.
Bei abstrakten und externen Methoden besteht die ref_method_body einfach aus einem Semikolon; für alle anderen Methoden ist die ref_method_body entweder ein Blocktext oder ein Ausdruckstext.
Der Name, die Anzahl der Typparameter und die Parameterliste einer Methode definieren die Signatur (§7.6) der Methode. Insbesondere besteht die Signatur einer Methode aus ihrem Namen, der Anzahl der Typparameter und der Zahl, parameter_mode_modifier s (§15.6.2.1) undtypen ihrer Parameter. Der Rückgabetyp ist weder Teil der Signatur einer Methode noch die Namen der Parameter, die Namen der Typparameter oder die Einschränkungen. Wenn ein Parametertyp auf einen Typparameter der Methode verweist, wird die Ordnungsposition des Typparameters (nicht der Name des Typparameters) für die Äquivalenz des Typs verwendet.
Der Name einer Methode unterscheidet sich von den Namen aller anderen Nichtmethoden, die in derselben Klasse deklariert sind. Darüber hinaus unterscheidet sich die Signatur einer Methode von den Signaturen aller anderen Methoden, die in derselben Klasse deklariert sind, und zwei Methoden, die in derselben Klasse deklariert sind, dürfen keine Signaturen haben, die sich ausschließlich von in
, out
und .ref
Die type_parameterder Methode sind im gesamten method_declaration im Gültigkeitsbereich enthalten und können für Formulartypen in diesem Bereich in return_type oder ref_return_type, method_body oder ref_method_body und type_parameter_constraints_clause, aber nicht in Attributen verwendet werden.
Alle Parameter und Typparameter müssen unterschiedliche Namen haben.
15.6.2 Methodenparameter
15.6.2.1 Allgemein
Die Parameter einer Methode werden ggf. durch die parameter_list der Methode deklariert.
parameter_list
: fixed_parameters
| fixed_parameters ',' parameter_array
| parameter_array
;
fixed_parameters
: fixed_parameter (',' fixed_parameter)*
;
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
default_argument
: '=' expression
;
parameter_modifier
: parameter_mode_modifier
| 'this'
;
parameter_mode_modifier
: 'ref'
| 'out'
| 'in'
;
parameter_array
: attributes? 'params' array_type identifier
;
Die Parameterliste besteht aus einem oder mehreren kommagetrennten Parametern, von denen nur der letzte eine parameter_array sein kann.
Ein fixed_parameter besteht aus einem optionalen Satz von Attributen (§22); einem optionalen in
, , out
ref
, oder this
Modifizierer; einem Typ; einem Bezeichner und einem optionalen default_argument. Jede fixed_parameter deklariert einen Parameter des angegebenen Typs mit dem angegebenen Namen. Der this
Modifizierer bezeichnet die Methode als Erweiterungsmethode und ist nur für den ersten Parameter einer statischen Methode in einer nicht generischen, nicht geschachtelten statischen Klasse zulässig. Wenn es sich bei dem Parameter um einen struct
Typ oder einen Typparameter handelt, der auf einen struct
Parameter 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
Oder out
this
modifizierer kann keine default_argument haben. Ein Eingabeparameter hat möglicherweise eine default_argument. Der Ausdruck in einem default_argument muss eine der folgenden Sein:
- ein constant_expression
- ein Ausdruck des Formulars
new S()
, in demS
es sich um einen Werttyp handelt - ein Ausdruck des Formulars
default(S)
, in demS
es sich um einen Werttyp handelt
Der Ausdruck muss implizit durch eine Identitäts- oder Null-Konvertierung in den Typ des Parameters konvertiert werden.
Wenn optionale Parameter in einer implementierenden partiellen Methodendeklaration (§15.6.9) auftreten, sollte ein expliziter Schnittstellenmemberimplementierung (§18.6.2), eine Indexerdeklaration mit einem einzigen Parameter (§15.9) oder in einer Operatordeklaration (§15.10.1) eine Warnung geben, da diese Member niemals so aufgerufen werden können, dass Argumente weggelassen werden können.
Ein parameter_array besteht aus einem optionalen Satz von Attributen (§22), einem params
Modifizierer, einer array_type und einem Bezeichner. Ein Parameterarray deklariert einen einzelnen Parameter des angegebenen Arraytyps mit dem angegebenen Namen. Die array_type eines Parameterarrays muss ein eindimensionales Arraytyp (§17.2) sein. Bei einem Methodenaufruf lässt ein Parameterarray entweder ein einzelnes Argument des angegebenen Arraytyps anzugeben, oder es kann null oder mehr Argumente des Arrayelementtyps angegeben werden. Parameterarrays werden weiter in §15.6.2.4 beschrieben.
Ein parameter_array kann nach einem optionalen Parameter auftreten, aber keinen Standardwert aufweisen – das Auslassen von Argumenten für eine parameter_array würde stattdessen zur Erstellung eines leeren Arrays führen.
Beispiel: Im Folgenden werden verschiedene Arten von Parametern veranschaulicht:
void M<T>( ref int i, decimal d, bool b = false, bool? n = false, string s = "Hello", object o = null, T t = default(T), params int[] a ) { }
In der parameter_list für , ist ein erforderlicher
ref
Parameter,d
ein erforderlicher Wertparameter,b
,s
,o
und sind optionale Wertparameter unda
t
ein Parameterarray.i
M
Endbeispiel
Eine Methodendeklaration erstellt einen separaten Deklarationsraum (§7.3) für Parameter und Typparameter. Namen werden in diesen Deklarationsbereich durch die Typparameterliste und die Parameterliste der Methode eingeführt. Wenn vorhanden, wird der Textkörper der Methode als in diesem Deklarationsbereich geschachtelt betrachtet. Es ist ein Fehler für zwei Member eines Methodendeklarationsraums, um denselben Namen zu haben.
Ein Methodenaufruf (§12.8.10.2) erstellt eine Kopie, spezifisch für diesen Aufruf, die Parameter und lokale Variablen der Methode, und die Argumentliste des Aufrufs weist den neu erstellten Parametern Werte oder Variablenverweise zu. Innerhalb des Blocks einer Methode können Parameter anhand ihrer Bezeichner in simple_name Ausdrücken (§12.8.4) referenziert werden.
Die folgenden Parametertypen sind vorhanden:
- Wertparameter (§15.6.2.2).
- Eingabeparameter (§15.6.2.3.2).
- Ausgabeparameter (§15.6.2.3.4).
- Referenzparameter (§15.6.2.3.3).
- Parameterarrays (§15.6.2.4).
Hinweis: Wie in §7.6 beschrieben, sind die
in
Modifiziererout
undref
Modifizierer Teil der Signatur einer Methode, derparams
Modifizierer ist jedoch nicht. Endnote
15.6.2.2 Wertparameter
Ein Parameter, der ohne Modifizierer deklariert ist, ist ein Wertparameter. Ein Wertparameter ist eine lokale Variable, die ihren Anfangswert aus dem entsprechenden Argument abruft, das im Methodenaufruf angegeben wird.
Bestimmte Zuordnungsregeln finden Sie unter §9.2.5.
Das entsprechende Argument in einem Methodenaufruf muss ein Ausdruck sein, der implizit in den Parametertyp (§10.2) konvertierbar ist.
Eine Methode darf einem Wertparameter neue Werte zuweisen. Solche Zuordnungen wirken sich nur auf den lokalen Speicherort aus, der durch den Wertparameter dargestellt wird. Sie haben keine Auswirkungen auf das tatsächliche Argument, das im Methodenaufruf angegeben wird.
15.6.2.3 By-Reference-Parameter
15.6.2.3.1 Allgemein
Eingabe-, Ausgabe- und Referenzparameter sind Nachverweisparameter. Ein Nachverweisparameter ist eine lokale Referenzvariable (§9.7). Der anfängliche Referent wird aus dem entsprechenden Argument abgerufen, das im Aufruf der Methode angegeben wird.
Hinweis: Der Referent eines By-Reference-Parameters kann mit dem Verweiszuweisungsoperator (
= ref
) geändert werden.
Wenn ein Parameter ein By-Reference-Parameter ist, besteht das entsprechende Argument in einem Methodenaufruf aus dem entsprechenden Schlüsselwort, in
, , ref
oder out
, gefolgt von einem variable_reference (§9.5) desselben Typs wie der Parameter. Wenn der Parameter jedoch ein in
Parameter ist, kann es sich bei dem Argument um einen Ausdruck handeln, für den eine implizite Konvertierung (§10.2) von diesem Argumentausdruck in den Typ des entsprechenden Parameters vorhanden ist.
By-Reference-Parameter sind für Als Iterator (§15.14) oder asynchrone Funktion (§15.15) deklarierte Funktionen nicht zulässig.
In einer Methode, die mehrere Nachverweisparameter verwendet, ist es möglich, dass mehrere Namen denselben Speicherort darstellen.
15.6.2.3.2 Eingabeparameter
Ein mit einem Modifizierer deklarierter in
Parameter ist ein Eingabeparameter. Das Argument, das einem Eingabeparameter entspricht, ist entweder eine Variable, die am Punkt des Methodenaufrufs vorhanden ist, oder eine variable, die durch die Implementierung (§12.6.2.3) im Aufruf der Methode erstellt wurde. Bestimmte Zuordnungsregeln finden Sie unter §9.2.8.
Es handelt sich um einen Kompilierungszeitfehler, um den Wert eines Eingabeparameters zu ändern.
Hinweis: Der Hauptzweck von Eingabeparametern ist für die Effizienz. Wenn der Typ eines Methodenparameters eine große Struktur (hinsichtlich der Speicheranforderungen) ist, ist es nützlich, den gesamten Wert des Arguments beim Aufrufen der Methode zu vermeiden. Eingabeparameter ermöglichen es Methoden, auf vorhandene Werte im Arbeitsspeicher zu verweisen, während gleichzeitig Schutz vor unerwünschten Änderungen an diesen Werten bereitgestellt wird. Endnote
15.6.2.3.3 Referenzparameter
Ein parameter, der mit einem ref
Modifizierer deklariert ist, ist ein Verweisparameter. Bestimmte Zuordnungsregeln finden Sie unter §9.2.6.
Beispiel: Das Beispiel
class Test { static void Swap(ref int x, ref int y) { int temp = x; x = y; y = temp; } static void Main() { int i = 1, j = 2; Swap(ref i, ref j); Console.WriteLine($"i = {i}, j = {j}"); } }
erzeugt die Ausgabe
i = 2, j = 1
Für den Aufruf von
Swap
inMain
,x
representsi
andy
representsj
. Daher hat der Aufruf die Auswirkung, die Werte voni
undj
.Endbeispiel
Beispiel: Im folgenden Code
class A { string s; void F(ref string a, ref string b) { s = "One"; a = "Two"; b = "Three"; } void G() { F(ref s, ref s); } }
der Aufruf der
F
In-AufrufeG
übergibt einen Verweiss
auf beidea
undb
. Daher beziehen sich für diesen Aufruf die Namens
,a
undb
alle auf denselben Speicherort, und die drei Zuordnungen ändern alle das Instanzfelds
.Endbeispiel
Bei einem struct
Typ verhält sich das this
Schlüsselwort innerhalb einer Instanzmethode, des Instanzaccessors (§12.2.1) oder des Instanzkonstruktors mit einem Konstruktorinitialisierer genau als Referenzparameter des Strukturtyps (§12.8.14).
15.6.2.3.4 Ausgabeparameter
Ein parameter, der mit einem out
Modifizierer deklariert ist, ist ein Ausgabeparameter. Bestimmte Zuordnungsregeln finden Sie unter §9.2.7.
Eine als Teilmethode deklarierte Methode (§15.6.9) darf keine Ausgabeparameter aufweisen.
Hinweis: Ausgabeparameter werden in der Regel in Methoden verwendet, die mehrere Rückgabewerte erzeugen. Endnote
Beispiel:
class Test { static void SplitPath(string path, out string dir, out string name) { int i = path.Length; while (i > 0) { char ch = path[i - 1]; if (ch == '\\' || ch == '/' || ch == ':') { break; } i--; } dir = path.Substring(0, i); name = path.Substring(i); } static void Main() { string dir, name; SplitPath(@"c:\Windows\System\hello.txt", out dir, out name); Console.WriteLine(dir); Console.WriteLine(name); } }
Das Beispiel erzeugt die Ausgabe:
c:\Windows\System\ hello.txt
Beachten Sie, dass die
dir
undname
die Variablen nicht zugewiesen werden können, bevor sie an sie übergebenSplitPath
werden, und dass sie nach dem Aufruf als definitiv zugewiesen gelten.Endbeispiel
15.6.2.4 Parameterarrays
Ein mit einem Modifizierer deklarierter params
Parameter ist ein Parameterarray. Wenn eine Parameterliste ein Parameterarray enthält, muss es sich um den letzten Parameter in der Liste und es muss sich um einen eindimensionalen Arraytyp handelt.
Beispiel: Die Typen
string[]
undstring[][]
können als Typ eines Parameterarrays verwendet werden, der Typstring[,]
kann jedoch nicht verwendet werden. Endbeispiel
Hinweis: Es ist nicht möglich, den
params
Modifizierer mit den Modifizierernin
, oderout
ref
. Endnote
Ein Parameterarray ermöglicht die Angabe von Argumenten auf eine von zwei Arten in einem Methodenaufruf:
- Das argument für ein Parameterarray kann ein einzelner Ausdruck sein, der implizit in den Parameterarraytyp (§10.2) konvertierbar ist. In diesem Fall fungiert das Parameterarray genau wie ein Wertparameter.
- Alternativ kann der Aufruf null oder mehr Argumente für das Parameterarray angeben, wobei jedes Argument ein Ausdruck ist, der implizit (§10.2) in den Elementtyp des Parameterarrays umgewandelt wird. In diesem Fall erstellt der Aufruf eine Instanz des Parameterarraytyps mit einer Länge, die der Anzahl der Argumente entspricht, initialisiert die Elemente der Arrayinstanz mit den angegebenen Argumentwerten und verwendet die neu erstellte Arrayinstanz als tatsächliches Argument.
Mit Ausnahme einer variablen Anzahl von Argumenten in einem Aufruf entspricht ein Parameterarray genau einem Wertparameter (§15.6.2.2.2) desselben Typs.
Beispiel: Das Beispiel
class Test { static void F(params int[] args) { Console.Write($"Array contains {args.Length} elements:"); foreach (int i in args) { Console.Write($" {i}"); } Console.WriteLine(); } static void Main() { int[] arr = {1, 2, 3}; F(arr); F(10, 20, 30, 40); F(); } }
erzeugt die Ausgabe
Array contains 3 elements: 1 2 3 Array contains 4 elements: 10 20 30 40 Array contains 0 elements:
Der erste Aufruf des
F
Arrays übergibt das Arrayarr
einfach 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 einesF
Nullelementsint[]
ein Nullelement und übergibt diese Instanz als Wertparameter. Die zweiten und dritten Aufrufe entsprechen genau dem Schreiben:F(new int[] {10, 20, 30, 40}); F(new int[] {});
Endbeispiel
Bei der Überladungsauflösung kann eine Methode mit einem Parameterarray entweder in normaler Form oder in erweiterter Form (§12.6.4.2) anwendbar sein. Die erweiterte Form einer Methode ist nur verfügbar, wenn die normale Form der Methode nicht anwendbar ist und nur, wenn eine anwendbare Methode mit derselben Signatur wie das erweiterte Formular nicht bereits im selben Typ deklariert ist.
Beispiel: Das Beispiel
class Test { static void F(params object[] a) => Console.WriteLine("F(object[])"); static void F() => Console.WriteLine("F()"); static void F(object a0, object a1) => Console.WriteLine("F(object,object)"); static void Main() { F(); F(1); F(1, 2); F(1, 2, 3); F(1, 2, 3, 4); } }
erzeugt die Ausgabe
F() F(object[]) F(object,object) F(object[]) F(object[])
Im Beispiel sind zwei der möglichen erweiterten Formen der Methode mit einem Parameterarray bereits als reguläre Methoden in der Klasse enthalten. Diese erweiterten Formen werden daher beim Ausführen der Überladungsauflösung nicht berücksichtigt, und die ersten und dritten Methodenaufrufe wählen daher die regulären Methoden aus. Wenn eine Klasse eine Methode mit einem Parameterarray deklariert, ist es nicht ungewöhnlich, auch einige der erweiterten Formulare als normale Methoden einzuschließen. Dadurch ist es möglich, die Zuordnung einer Arrayinstanz zu vermeiden, die auftritt, wenn eine erweiterte Form einer Methode mit einem Parameterarray aufgerufen wird.
Endbeispiel
Ein Array ist ein Verweistyp, sodass der für ein Parameterarray übergebene Wert sein
null
kann.Beispiel: Das Beispiel:
class Test { static void F(params string[] array) => Console.WriteLine(array == null); static void Main() { F(null); F((string) null); } }
erzeugt die Ausgabe:
True False
Der zweite Aufruf erzeugt
False
, da er 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 implizit konvertierbarer Typ object
ist. Die Mehrdeutigkeit stellt jedoch kein Problem dar, da es gelöst werden kann, indem bei Bedarf eine Umwandlung eingefügt wird.
Beispiel: Das Beispiel
class Test { static void F(params object[] args) { foreach (object o in args) { Console.Write(o.GetType().FullName); Console.Write(" "); } Console.WriteLine(); } static void Main() { object[] a = {1, "Hello", 123.456}; object o = a; F(a); F((object)a); F(o); F((object[])o); } }
erzeugt die Ausgabe
System.Int32 System.String System.Double System.Object[] System.Object[] System.Int32 System.String System.Double
In den ersten und letzten Aufrufen von
F
ist 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 nichtF
anwendbar, da keine implizite Konvertierung vom Argumenttyp in den Parametertyp vorhanden ist (Typobject
kann nicht implizit in Typobject[]
konvertiert werden). Die erweiterte Form vonF
ist jedoch anwendbar, sodass sie durch Überladungsauflösung ausgewählt wird. Daher wird ein 1-Elementobject[]
durch den Aufruf erstellt, und das einzelne Element des Arrays wird mit dem angegebenen Argumentwert initialisiert (was selbst ein Verweis auf einenobject[]
).Endbeispiel
15.6.3 Statische Methoden und Instanzmethoden
Wenn eine Methodendeklaration einen static
Modifizierer enthält, wird diese Methode als statische Methode bezeichnet. Wenn kein static
Modifizierer vorhanden ist, wird die Methode als Instanzmethode bezeichnet.
Eine statische Methode funktioniert nicht für eine bestimmte Instanz, und es handelt sich um einen Kompilierungszeitfehler, auf this
den in einer statischen Methode verwiesen wird.
Eine Instanzmethode wird für eine bestimmte Instanz einer Klasse ausgeführt, und auf diese Instanz kann zugegriffen werden (this
§12.8.14).
Die Unterschiede zwischen statischen und Instanzmitgliedern werden in §15.3.8 weiter erörtert.
15.6.4 Virtuelle Methoden
Wenn eine Instanzmethodendeklaration einen virtuellen Modifizierer enthält, wird diese Methode als virtuelle Methode bezeichnet. Wenn kein virtueller Modifizierer vorhanden ist, wird die Methode als nicht virtuelle Methode bezeichnet.
Die Implementierung einer nicht virtuellen Methode ist invariant: Die Implementierung ist identisch, ob die Methode für eine Instanz der Klasse aufgerufen wird, in der sie deklariert wird, oder eine Instanz einer abgeleiteten Klasse. Im Gegensatz dazu kann die Implementierung einer virtuellen Methode durch abgeleitete Klassen ersetzt werden. Der Prozess der Außerkraftsetzung der Implementierung einer geerbten virtuellen Methode wird als Überschreibung dieser Methode (§15.6.5) bezeichnet.
Bei einem aufruf der virtuellen Methode bestimmt der Laufzeittyp der Instanz, für die dieser Aufruf stattfindet, die tatsächliche Methodenimplementierung, die aufgerufen werden soll. Bei einem Aufruf einer nicht virtuellen Methode ist der Kompilierungs-Zeittyp der Instanz der bestimmende Faktor. Wenn eine benannte N
Methode mit einer Argumentliste A
in einer Instanz mit einem Kompilierungszeittyp und einem Laufzeittyp C
R
aufgerufen wird (wobei R
eine C
oder eine klasse abgeleitet C
ist), wird der Aufruf wie folgt verarbeitet:
- Bei Bindungszeit wird die Überladungsauflösung auf , und , um
C
eine bestimmte MethodeM
aus der Gruppe der methoden auszuwählen, die in und geerbt vonC
.A
N
Dies wird in §12.8.10.2 beschrieben. - Dann zur Laufzeit:
- Wenn
M
es sich um eine nicht virtuelle Methode handelt,M
wird diese aufgerufen. M
Andernfalls handelt es sich um eine virtuelle Methode, und die abgeleitete ImplementierungM
in Bezug auf dieseR
Methode wird 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
R
die eingeführte virtuelle ErklärungM
enthalten ist, ist dies die abgeleitetste ImplementierungM
in Bezug aufR
. - Andernfalls, wenn
R
eine Außerkraftsetzung vonM
enthält, ist dies die abgeleitetste ImplementierungM
in Bezug aufR
. - Andernfalls entspricht die am meisten abgeleitete Implementierung
M
in Bezug aufR
die abgeleitete ImplementierungM
in Bezug auf die direkte 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
A
wird eine nicht virtuelle MethodeF
und eine virtuelle MethodeG
eingeführt. Die KlasseB
führt eine neue, nicht virtuelle MethodeF
ein, wodurch die geerbteF
Methode ausgeblendet wird, und überschreibt auch die geerbte MethodeG
. Das Beispiel erzeugt die Ausgabe:A.F B.F B.G B.G
Beachten Sie, dass die Anweisung
a.G()
aufgerufen wirdB.G
, nichtA.G
. 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 abgeleiteten Methoden ausgeblendet sind.
Beispiel: Im folgenden Code
class A { public virtual void F() => Console.WriteLine("A.F"); } class B : A { public override void F() => Console.WriteLine("B.F"); } class C : B { public new virtual void F() => Console.WriteLine("C.F"); } class D : C { public override void F() => Console.WriteLine("D.F"); } class Test { static void Main() { D d = new D(); A a = d; B b = d; C c = d; a.F(); b.F(); c.F(); d.F(); } }
die
C
undD
klassen enthalten zwei virtuelle Methoden mit der gleichen Signatur: Die vonA
und die von .C
Die von der Methode eingeführteC
Methode blendet die vonA
. Daher überschreibt die Überschreibungsdeklaration inD
der vonC
, und es ist nicht möglichD
, die vonA
der Methode eingeführte Methode außer Kraft zu setzen. Das Beispiel erzeugt die Ausgabe:B.F B.F D.F D.F
Beachten Sie, dass es möglich ist, die ausgeblendete virtuelle Methode aufzurufen, indem auf eine Instanz eines
D
weniger abgeleiteten Typs zugegriffen wird, in dem die Methode nicht ausgeblendet ist.Endbeispiel
15.6.5 Override-Methoden
Wenn eine Instanzmethodendeklaration einen override
Modifizierer enthält, wird die Methode als Außerkraftsetzungsmethode bezeichnet. Eine Außerkraftsetzungsmethode setzt eine geerbte virtuelle Methode mit derselben Signatur außer Kraft. Während eine virtuelle Methodendeklaration eine neue Methode einführt , ist eine Überschreibungsmethodedeklaration auf eine vorhandene geerbte virtuelle Methode spezialisiert , indem eine neue Implementierung dieser Methode bereitgestellt wird.
Die Methode, die durch eine Überschreibungsdeklaration überschrieben wird, wird als überschriebene Basismethode für eine in einer Klasse C
deklarierte Außerkraftsetzungsmethode M
bezeichnet, die überschriebene Basismethode wird bestimmt, indem jede Basisklasse C
untersucht wird, beginnend mit der direkten Basisklasse von C
und fortgesetzt mit jeder aufeinander folgenden direkten Basisklasse, bis sich in einem bestimmten Basisklassentyp mindestens eine barrierefreie Methode befindet, die dieselbe Signatur wie M
nach der Ersetzung von Typargumenten aufweist. Für die Suche nach der überschriebenen Basismethode wird eine Methode als barrierefrei betrachtet, wenn es public
sich um , wenn es protected
sich um , wenn es sich handelt protected internal
, oder wenn sie entweder oder private protected
oder und im selben Programm deklariert ist internal
wie C
.
Ein Kompilierungszeitfehler tritt auf, es sei denn, für eine Außerkraftsetzungsdeklaration gilt Folgendes:
- Eine überschriebene Basismethode kann wie oben beschrieben gefunden werden.
- Es gibt genau eine solche überschriebene Basismethode. Diese Einschränkung ist nur wirksam, wenn der Basisklassentyp ein konstruierter Typ ist, bei dem die Ersetzung von Typargumenten die Signatur von zwei Methoden gleich macht.
- Die überschriebene Basismethode ist eine virtuelle, abstrakte oder Außerkraftsetzungsmethode. Mit anderen Worten, die überschriebene Basismethode kann nicht statisch oder nicht virtuell sein.
- Die überschriebene Basismethode ist keine versiegelte Methode.
- Es gibt eine Identitätskonvertierung zwischen dem Rückgabetyp der überschriebenen Basismethode und der Überschreibungsmethode.
- Die Überschreibungsdeklaration und die überschriebene Basismethode weisen die gleiche deklarierte Barrierefreiheit auf. Anders ausgedrückt: Eine Außerkraftsetzungsdeklaration kann die Barrierefreiheit der virtuellen Methode nicht ändern. Wenn die überschriebene Basismethode jedoch intern geschützt ist und sie in einer anderen Assembly als die Assembly deklariert wird, die die Überschreibungsdeklaration enthält, muss die deklarierte Barrierefreiheit der Deklaration überschreiben.
- Die Außerkraftsetzungsdeklaration gibt keine type_parameter_constraints_clauses an. Stattdessen werden die Einschränkungen von der überschriebenen Basismethode geerbt. Einschränkungen, die Typparameter in der überschriebenen Methode sind, können durch Typargumente in der geerbten Einschränkung ersetzt werden. Dies kann zu Einschränkungen führen, die nicht gültig sind, wenn explizit angegeben, z. B. Werttypen oder versiegelte Typen.
Beispiel: Im Folgenden wird veranschaulicht, wie die überschreibenden Regeln für generische Klassen funktionieren:
abstract class C<T> { public virtual T F() {...} public virtual C<T> G() {...} public virtual void H(C<T> x) {...} } class D : C<string> { public override string F() {...} // Ok public override C<string> G() {...} // Ok public override void H(C<T> x) {...} // Error, should be C<string> } class E<T,U> : C<U> { public override U F() {...} // Ok public override C<U> G() {...} // Ok public override void H(C<T> x) {...} // Error, should be C<U> }
Endbeispiel
Eine Außerkraftsetzungsdeklaration kann mithilfe einer base_access (§12.8.15) auf die überschriebene Basismethode zugreifen.
Beispiel: Im folgenden Code
class A { int x; public virtual void PrintFields() => Console.WriteLine($"x = {x}"); } class B : A { int y; public override void PrintFields() { base.PrintFields(); Console.WriteLine($"y = {y}"); } }
der
base.PrintFields()
Aufruf aufruftB
die printFields-Methode, die inA
deklariert ist. Ein base_access deaktiviert den virtuellen Aufrufmechanismus und behandelt einfach die Basismethode als nicht-Methodevirtual
. Wenn der AufrufB
geschrieben wurde, würde sie die in deklarierteB
Methode rekursiv aufrufenPrintFields
, nicht die in , nicht die inA
, daPrintFields
ist virtuell und der Laufzeittyp ist((A)this)
B
.((A)this).PrintFields()
Endbeispiel
Nur durch das Einschließen eines Modifizierers kann eine override
Methode eine andere Methode außer Kraft setzen. In allen anderen Fällen blendet eine Methode mit derselben Signatur wie eine geerbte Methode einfach die geerbte Methode aus.
Beispiel: Im folgenden Code
class A { public virtual void F() {} } class B : A { public virtual void F() {} // Warning, hiding inherited F() }
die
F
Methode inB
enthältoverride
keinen Modifizierer und überschreibt daher dieF
Methode nicht inA
. Stattdessen wird die Methode inB
derF
MethodeA
ausgeblendet, und eine Warnung wird gemeldet, da die Deklaration keinen neuen Modifizierer enthält.Endbeispiel
Beispiel: Im folgenden Code
class A { public virtual void F() {} } class B : A { private new void F() {} // Hides A.F within body of B } class C : B { public override void F() {} // Ok, overrides A.F }
die
F
Methode inB
blendet die virtuelleF
Methode aus, vonA
der geerbt wird. Da das neueF
InB
privaten Zugriff hat, umfasst sein Bereich nur den Klassentext vonB
und wird nicht aufC
. Daher darf die In-DeklarationC
F
dieF
geerbte VonA
.Endbeispiel
15.6.6 Versiegelte Methoden
Wenn eine Instanzmethodendeklaration einen sealed
Modifizierer enthält, wird diese Methode als versiegelte Methode bezeichnet. Eine versiegelte Methode setzt eine geerbte virtuelle Methode mit derselben Signatur außer Kraft. Eine versiegelte Methode muss auch mit dem override
Modifizierer gekennzeichnet werden. Die Verwendung des sealed
Modifizierers verhindert, dass eine abgeleitete Klasse die Methode weiter außer Kraft setzt.
Beispiel: Das Beispiel
class A { public virtual void F() => Console.WriteLine("A.F"); public virtual void G() => Console.WriteLine("A.G"); } class B : A { public sealed override void F() => Console.WriteLine("B.F"); public override void G() => Console.WriteLine("B.G"); } class C : B { public override void G() => Console.WriteLine("C.G"); }
die Klasse
B
stellt zwei Außerkraftsetzungsmethoden bereit: eineF
Methode mitsealed
dem Modifizierer und einerG
Methode, die nicht verwendet wird.B
Die Verwendung dessealed
Modifizierers verhindertC
die weitere AußerkraftsetzungF
.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 virtual
verfügen.
Eine abstrakte Methodendeklaration führt eine neue virtuelle Methode ein, stellt jedoch keine Implementierung dieser Methode bereit. Stattdessen müssen nicht abstrakte abgeleitete Klassen ihre eigene Implementierung bereitstellen, indem diese Methode überschrieben wird. Da eine abstrakte Methode keine tatsächliche Implementierung bereitstellt, besteht der Methodentext einer abstrakten Methode einfach aus einem Semikolon.
Abstrakte Methodendeklarationen sind nur in abstrakten Klassen zulässig (§15.2.2.2.2).
Beispiel: Im folgenden Code
public abstract class Shape { public abstract void Paint(Graphics g, Rectangle r); } public class Ellipse : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawEllipse(r); } public class Box : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawRect(r); }
die
Shape
Klasse definiert den abstrakten Begriff eines geometrischen Formobjekts, das sich selbst zeichnen kann. DiePaint
Methode ist abstrakt, da keine sinnvolle Standardimplementierung vorhanden ist. DieEllipse
Klassen sindBox
konkreteShape
Implementierungen. Da diese Klassen nicht abstrakt sind, müssen sie diePaint
Methode außer Kraft setzen und eine tatsächliche Implementierung bereitstellen.Endbeispiel
Es handelt sich um einen Kompilierungszeitfehler für einen base_access (§12.8.15), um auf eine abstrakte Methode zu verweisen.
Beispiel: Im folgenden Code
abstract class A { public abstract void F(); } class B : A { // Error, base.F is abstract public override void F() => base.F(); }
Für den
base.F()
Aufruf wird ein Kompilierungsfehler gemeldet, da er auf eine abstrakte Methode verweist.Endbeispiel
Eine abstrakte Methodendeklaration darf eine virtuelle Methode außer Kraft setzen. Dadurch kann eine abstrakte Klasse die erneute Implementierung der Methode in abgeleiteten Klassen erzwingen und die ursprüngliche Implementierung der Methode nicht verfügbar machen.
Beispiel: Im folgenden Code
class A { public virtual void F() => Console.WriteLine("A.F"); } abstract class B: A { public abstract override void F(); } class C : B { public override void F() => Console.WriteLine("C.F"); }
die Klasse
A
deklariert eine virtuelle Methode, eine KlasseB
überschreibt diese Methode mit einer abstrakten Methode und überschreibtC
die abstrakte Methode, um eine eigene Implementierung bereitzustellen.Endbeispiel
15.6.8 Externe Methoden
Wenn eine Methodendeklaration einen extern
Modifizierer enthält, wird die Methode als externe Methode bezeichnet. Externe Methoden werden extern implementiert, in der Regel wird eine andere Sprache als C# verwendet. Da eine externe Methodendeklaration keine tatsächliche Implementierung bereitstellt, besteht der Methodentext einer externen Methode einfach aus einem Semikolon. Eine externe Methode darf nicht generisch sein.
Der Mechanismus, mit dem eine Verknüpfung mit einer externen Methode erreicht wird, wird durch implementierungsdefiniert.
Beispiel: Im folgenden Beispiel wird die Verwendung des
extern
Modifizierers und desDllImport
Attributs veranschaulicht:class Path { [DllImport("kernel32", SetLastError=true)] static extern bool CreateDirectory(string name, SecurityAttribute sa); [DllImport("kernel32", SetLastError=true)] static extern bool RemoveDirectory(string name); [DllImport("kernel32", SetLastError=true)] static extern int GetCurrentDirectory(int bufSize, StringBuilder buf); [DllImport("kernel32", SetLastError=true)] static extern bool SetCurrentDirectory(string name); }
Endbeispiel
15.6.9 Teilmethoden
Wenn eine Methodendeklaration einen partial
Modifizierer enthält, wird diese Methode als partielle Methode bezeichnet. Partielle Methoden dürfen nur als Mitglieder von Teiltypen (§15.2.7) deklariert werden und unterliegen einer Reihe von Einschränkungen.
Partielle Methoden können in einem Teil einer Typdeklaration definiert und in einer anderen implementiert werden. Die Umsetzung ist optional; wenn keine Komponente die partielle Methode implementiert, werden die partielle Methodendeklaration und alle Aufrufe davon aus der Typdeklaration entfernt, die sich aus der Kombination der Teile ergibt.
Teilmethoden dürfen keine Zugriffsmodifizierer definieren; sie sind implizit privat. Ihr Rückgabetyp muss sein void
, und ihre Parameter dürfen keine Ausgabeparameter sein. Der Bezeichner wird nur dann als kontextbezogenes Schlüsselwort (§6.4.4) in einer Methodendeklaration erkannt, wenn er unmittelbar vor dem void
Schlüsselwort angezeigt wird. Eine partielle Methode kann keine Schnittstellenmethoden explizit implementieren.
Es gibt zwei Arten von partiellen Methodendeklarationen: Wenn der Textkörper der Methodendeklaration ein Semikolon ist, wird die Deklaration als definierende partielle Methodendeklaration bezeichnet. Wenn der Textkörper kein Semikolon ist, wird die Deklaration als implementierende Partielle Methodendeklaration bezeichnet. In den Teilen einer Typdeklaration kann es nur eine definierende partielle Methodendeklaration mit einer bestimmten Signatur geben, und es kann nur eine implementierung teilweise Methodendeklaration mit einer bestimmten Signatur geben. Wenn eine Teilmethodenerklärung zur Durchführung gegeben wird, muss eine entsprechende definitionsbezogene Teilmethodenerklärung vorhanden sein, und die Erklärungen stimmen wie in den folgenden Angaben angegeben überein:
- Die Deklarationen müssen dieselben Modifizierer haben (auch wenn nicht notwendigerweise in derselben Reihenfolge), Methodenname, Anzahl der Typparameter und Anzahl von Parametern.
- Die entsprechenden Parameter in den Deklarationen müssen die gleichen Modifizierer aufweisen (obwohl nicht notwendigerweise in derselben Reihenfolge) und die gleichen Typen oder Identitätsveränderertypen (Modulounterschiede bei Typparameternamen).
- Die entsprechenden Typparameter in den Deklarationen müssen dieselben Einschränkungen aufweisen (Modulounterschiede bei Typparameternamen).
Eine implementierende partielle Methodendeklaration kann im selben Teil wie die entsprechende definierende partielle Methodendeklaration angezeigt werden.
Nur eine definierende Partielle Methode nimmt an der Überladungsauflösung teil. Unabhängig davon, ob eine Implementierungsdeklaration angegeben wird, können Aufrufausdrücke in Aufrufe der partiellen Methode aufgelöst werden. Da eine partielle Methode immer zurückgibt void
, sind solche Aufrufausdrücke immer Ausdrucksanweisungen. Da eine partielle Methode implizit private
ist, treten solche Anweisungen immer innerhalb eines der Teile der Typdeklaration auf, innerhalb der die partielle Methode deklariert wird.
Hinweis: Die Definition des Abgleichs zum Definieren und Implementieren partieller Methodendeklarationen erfordert keine Übereinstimmung mit Parameternamen. Dies kann zu überraschenden, wenn auch gut definierten Verhaltensweisen führen, wenn benannte Argumente (§12.6.2.1) verwendet werden. Beispiel: Angesichts der definierenden partiellen Methodendeklaration für
M
eine Datei und der implementierenden Partielle Methodendeklaration in einer anderen Datei:// File P1.cs: partial class P { static partial void M(int x); } // File P2.cs: partial class P { static void Caller() => M(y: 0); static partial void M(int y) {} }
ist ungültig , da der Aufruf den Argumentnamen aus der Implementierung und nicht die definierende Partielle Methodendeklaration verwendet.
Endnote
Wenn kein Teil einer Partdeklaration eine Implementierungsdeklaration für eine bestimmte partielle Methode enthält, wird jede Ausdrucksanweisung, die sie aufruft, einfach aus der kombinierten Typdeklaration entfernt. Daher hat der Aufrufausdruck, einschließlich aller Unterausdrücke, zur Laufzeit keine Auswirkung. Die partielle Methode selbst wird ebenfalls entfernt und ist kein Mitglied der kombinierten Typdeklaration.
Wenn eine Implementierungsdeklaration für eine bestimmte partielle Methode vorhanden ist, werden die Aufrufe der Partielle Methoden beibehalten. Die partielle Methode führt zu einer Methodendeklaration, die der implementierenden partiellen Methodendeklaration ähnelt, mit Ausnahme der folgenden:
Der
partial
Modifizierer ist nicht enthalten.Die Attribute in der resultierenden Methodendeklaration sind die kombinierten Attribute der definierenden und implementierenden partiellen Methodendeklaration in nicht angegebener Reihenfolge. Duplikate werden nicht entfernt.
Die Attribute für die Parameter der resultierenden Methodendeklaration sind die kombinierten Attribute der entsprechenden Parameter der Definition und der implementierenden partiellen Methodendeklaration in nicht angegebener Reihenfolge. Duplikate werden nicht entfernt.
Wenn eine definierende Deklaration, aber keine Implementierungsdeklaration für eine partielle Methode M
angegeben wird, gelten die folgenden Einschränkungen:
Es handelt sich um einen Kompilierungsfehler, um einen Delegat aus
M
(§12.8.17.6) zu erstellen.Es handelt sich um einen Kompilierungszeitfehler, der
M
in eine anonyme Funktion verweist, die in einen Ausdrucksstrukturtyp konvertiert wird (§8.6).Ausdrücke, die im Rahmen eines Aufrufs
M
auftreten, wirken sich nicht auf den endgültigen Zuordnungszustand (§9.4) aus, was möglicherweise zu Kompilierungsfehlern führen kann.M
der Einstiegspunkt für eine Bewerbung (§7.1) nicht sein kann.
Partielle Methoden sind nützlich, um einem Teil einer Typdeklaration das Verhalten eines anderen Teils anzupassen, z. B. eine, die von einem Tool generiert wird. Betrachten Sie die folgende partielle Klassendeklaration:
partial class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
partial void OnNameChanging(string newName);
partial void OnNameChanged();
}
Wenn diese Klasse ohne andere Teile kompiliert wird, werden die definierenden partiellen Methodendeklarationen und deren Aufrufe entfernt, und die resultierende kombinierte Klassendeklaration entspricht folgendem:
class Customer
{
string name;
public string Name
{
get => name;
set => name = value;
}
}
Gehen Sie davon aus, dass ein weiterer Teil angegeben wird, der jedoch Implementierungsdeklarationen der partiellen Methoden bereitstellt:
partial class Customer
{
partial void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
partial void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
Anschließend entspricht die resultierende kombinierte Klassendeklaration folgendem:
class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
15.6.10 Erweiterungsmethoden
Wenn der erste Parameter einer Methode den this
Modifizierer enthält, wird diese Methode als Erweiterungsmethode bezeichnet. Erweiterungsmethoden dürfen nur in nicht generischen, nicht geschachtelten statischen Klassen deklariert werden. Der erste Parameter einer Erweiterungsmethode ist wie folgt eingeschränkt:
- Es kann nur ein Eingabeparameter sein, wenn er einen Werttyp aufweist.
- Es kann sich nur um einen Referenzparameter handelt, wenn er einen Werttyp aufweist oder einen generischen Typ auf die Struktur beschränkt hat.
- Es darf kein Zeigertyp sein.
Beispiel: Im Folgenden sehen Sie ein Beispiel für eine statische Klasse, die zwei Erweiterungsmethoden deklariert:
public static class Extensions { public static int ToInt32(this string s) => Int32.Parse(s); public static T[] Slice<T>(this T[] source, int index, int count) { if (index < 0 || count < 0 || source.Length - index < count) { throw new ArgumentException(); } T[] result = new T[count]; Array.Copy(source, index, result, 0, count); return result; } }
Endbeispiel
Eine Erweiterungsmethode ist eine normale statische Methode. Wenn sich die eingeschlossene statische Klasse im Bereich befindet, kann eine Erweiterungsmethode mithilfe der Instanzmethodenaufrufssyntax (§12.8.10.3) mithilfe des Empfängerausdrucks als erstes Argument aufgerufen werden.
Beispiel: Im folgenden Programm werden die oben deklarierten Erweiterungsmethoden verwendet:
static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in strings.Slice(1, 2)) { Console.WriteLine(s.ToInt32()); } } }
Die
Slice
Methode ist für dasstring[]
, und dieToInt32
Methode ist verfügbar fürstring
, da sie als Erweiterungsmethoden deklariert wurden. Die Bedeutung des Programms ist identisch mit dem folgenden, wobei gewöhnliche statische Methodenaufrufe verwendet werden:static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in Extensions.Slice(strings, 1, 2)) { Console.WriteLine(Extensions.ToInt32(s)); } } }
Endbeispiel
15.6.11 Methodentext
Der Methodentext einer Methodendeklaration besteht entweder aus einem Blocktext, einem Ausdruckstext oder einem Semikolon.
Abstrakte und externe Methodendeklarationen stellen keine Methodenimplementierung bereit, sodass ihre Methodentexte einfach aus einem Semikolon bestehen. Bei jeder anderen Methode ist der Methodentext ein Block (§13.3), der die auszuführenden Anweisungen enthält, wenn diese Methode aufgerufen wird.
Der effektive Rückgabetyp einer Methode istvoid
, wenn der Rückgabetyp lautet void
oder die Methode asynchron ist und der Rückgabetyp (§15.15.1) lautet «TaskType»
. Andernfalls ist der effektive Rückgabetyp einer nicht asynchronen Methode der Rückgabetyp, und der effektive Rückgabetyp einer asynchronen Methode mit Rückgabetyp «TaskType»<T>
(§15.15.1) lautet T
.
Wenn der effektive Rückgabetyp einer Methode ist void
und die Methode über einen Blocktext verfügt, return
dürfen Anweisungen (§13.10.5) im Block keinen Ausdruck angeben. Wenn die Ausführung des Blocks einer void-Methode normal abgeschlossen ist (d. h. die Steuerung fließt vom Ende des Methodentexts ab), wird diese Methode einfach an den Aufrufer zurückgegeben.
Wenn der effektive Rückgabetyp einer Methode ist void
und die Methode über einen Ausdruckstext verfügt, muss der Ausdruck E
ein statement_expression sein, und der Textkörper entspricht exakt einem Blockkörper des Formulars { E; }
.
Bei einer Rückgabe-nach-Wert-Methode (§15.6.1) muss jede Rückgabe-Anweisung im Textkörper dieser Methode einen Ausdruck angeben, der implizit in den effektiven Rückgabetyp konvertierbar ist.
Bei einer Rückgabe-by-Ref-Methode (§15.6.1) gibt jede Rückgabeanweisung im Textkörper dieser Methode einen Ausdruck an, dessen Typ der effektiven Rückgabetyp ist und über einen ref-safe-context des Caller-Context (§9.7.2) verfügt.
Für Rückgabe-nach-Wert- und Rückgabemethoden ist der Endpunkt des Methodentexts nicht erreichbar. Mit anderen Worten, die Steuerung darf nicht vom Ende des Methodentexts fließen.
Beispiel: Im folgenden Code
class A { public int F() {} // Error, return value required public int G() { return 1; } public int H(bool b) { if (b) { return 1; } else { return 0; } } public int I(bool b) => b ? 1 : 0; }
die Methode für die Wertrückgabe
F
führt zu einem Kompilierungszeitfehler, da die Steuerung das Ende des Methodentexts beenden kann. DieG
Methoden sindH
richtig, da alle möglichen Ausführungspfade in einer Rückgabe-Anweisung enden, die einen Rückgabewert angibt. DieI
Methode ist richtig, da ihr Textkörper einem Block mit nur einer einzelnen Rückgabe-Anweisung entspricht.Endbeispiel
15.7 Eigenschaften
15.7.1 Allgemein
Eine Eigenschaft ist ein Element, das Zugriff auf ein Merkmal eines Objekts oder einer Klasse bietet. Beispiele für Eigenschaften sind die Länge einer Zeichenfolge, der Schriftgrad, die Beschriftung eines Fensters und der Name eines Kunden. Eigenschaften sind eine natürliche Erweiterung von Feldern – beide sind benannte Member mit zugeordneten Typen, und die Syntax für den Zugriff auf Felder und Eigenschaften ist identisch. Im Gegensatz zu Feldern bezeichnen Eigenschaften jedoch keine Speicherorte. Stattdessen verfügen Eigenschaften über Accessors zum Angeben der Anweisungen, die beim Lesen oder Schreiben ihrer Werte ausgeführt werden sollen. Eigenschaften bieten somit einen Mechanismus zum Zuordnen von Aktionen zum Lesen und Schreiben von Eigenschaften eines Objekts oder einer Klasse; ferner erlauben sie, diese Merkmale zu berechnen.
Eigenschaften werden mit property_declaration sdeklariert:
property_declaration
: attributes? property_modifier* type member_name property_body
| attributes? property_modifier* ref_kind type member_name ref_property_body
;
property_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
property_body
: '{' accessor_declarations '}' property_initializer?
| '=>' expression ';'
;
property_initializer
: '=' variable_initializer ';'
;
ref_property_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
Es gibt zwei Arten von property_declaration:
- Die erste deklariert eine Nicht-Verweis-Eigenschaft. Der Wert weist Typ auf. Diese Art von Eigenschaft kann lesbar und/oder schreibbar sein.
- Die zweite deklariert eine ref-valued-Eigenschaft. Der Wert ist eine variable_reference (§9.5), die eine Variable vom Typtyp sein
readonly
kann. Diese Art von Eigenschaft ist nur lesbar.
Eine property_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), der new
(§15.3.5), static
(§15.7.2), virtual
(§15.6.4, §15.7.6), override
(§15.6.5, §15.7.6), sealed
(§15.6.6), abstract
(§15.6.7, §15.7.6) und extern
(§15.6.8) Modifizierer.
Eigenschaftsdeklarationen unterliegen den gleichen Regeln wie Methodendeklarationen (§15.6) in Bezug auf gültige Kombinationen von Modifizierern.
Der member_name (§15.6.1) gibt den Namen der Eigenschaft an. Sofern es sich bei der Eigenschaft nicht um eine explizite Schnittstellenmemembeimplementierung handelt, ist die member_name einfach ein Bezeichner. Für eine explizite Schnittstellenmememimplementierung (§18.6.2) besteht die member_name aus einer interface_type gefolgt von einem ".
" und einem Bezeichner.
Der Typ einer Immobilie muss mindestens so zugänglich sein wie die Eigenschaft selbst (§7.5.5).
Ein property_body kann entweder aus einem Anweisungstext oder einem Ausdruckstext bestehen. In einem Anweisungstext deklarieren accessor_declarations, die in "{
" und "}
" Token eingeschlossen werden sollen, die Accessoren (§15.7.3) der Eigenschaft. Die Accessoren geben die ausführbaren Anweisungen an, die dem Lesen und Schreiben der Eigenschaft zugeordnet sind.
In einem property_body einem Ausdruckstext, der =>
aus einem Ausdruck E
besteht und ein Semikolon exakt dem Textkörper der Anweisung { get { return E; } }
entspricht, und kann daher nur verwendet werden, um schreibgeschützte Eigenschaften anzugeben, bei denen das Ergebnis des Get-Accessors durch einen einzelnen Ausdruck angegeben wird.
Eine property_initializer kann nur für eine automatisch implementierte Eigenschaft (§15.7.4) angegeben werden und bewirkt die Initialisierung des zugrunde liegenden Felds solcher Eigenschaften mit dem vom Ausdruck angegebenen Wert.
Ein ref_property_body kann entweder aus einem Anweisungstext oder einem Ausdruckstext bestehen. In einem Anweisungstext deklariert ein get_accessor_declaration den Get-Accessor (§15.7.3) der Eigenschaft. Der Accessor gibt die ausführbaren Anweisungen an, die mit dem Lesen der Eigenschaft verknüpft sind.
In einem ref_property_body einem Ausdruckstext, der aus folgt ref
besteht=>
, entspricht ein variable_reference V
und ein Semikolon exakt dem Textkörper der Anweisung{ get { return ref V; } }
.
Hinweis: Obwohl die Syntax für den Zugriff auf eine Eigenschaft mit dem für ein Feld identisch ist, wird eine Eigenschaft nicht als Variable klassifiziert. Daher ist es nicht möglich, eine Eigenschaft als
in
,out
oderref
Argument zu übergeben, es sei denn, die Eigenschaft ist ref-valued und gibt daher einen Variablenverweis (§9.7) zurück. Endnote
Wenn eine Eigenschaftsdeklaration einen extern
Modifizierer enthält, wird die Eigenschaft als externe Eigenschaft bezeichnet. Da eine externe Eigenschaftsdeklaration keine tatsächliche Implementierung bereitstellt, muss jedes accessor_bodys in seinem accessor_declarations ein Semikolon sein.
15.7.2 Statische Und Instanzeigenschaften
Wenn eine Eigenschaftsdeklaration einen static
Modifizierer enthält, wird die Eigenschaft als statische Eigenschaft bezeichnet. Wenn kein static
Modifizierer vorhanden ist, wird die Eigenschaft als Instanzeigenschaft bezeichnet.
Eine statische Eigenschaft ist keiner bestimmten Instanz zugeordnet, und es handelt sich um einen Kompilierungszeitfehler, der in den Accessoren einer statischen Eigenschaft referenziert this
werden soll.
Eine Instanzeigenschaft ist einer bestimmten Instanz einer Klasse zugeordnet, und auf diese Instanz kann in den Accessoren dieser Eigenschaft als this
(§12.8.14) zugegriffen werden.
Die Unterschiede zwischen statischen und Instanzmitgliedern werden in §15.3.8 weiter erörtert.
15.7.3 Accessoren
Hinweis: Diese Klausel gilt sowohl für Eigenschaften (§15.7) als auch für Indexer (§15.9). Die Klausel wird in Bezug auf Eigenschaften geschrieben, wenn das Lesen von Indexern indexer/indexer für Eigenschaften/Eigenschaften ersetzt und die Liste der Unterschiede zwischen Eigenschaften und Indexern in §15.9.2 konsultieren. Endnote
Die accessor_declarations einer Eigenschaft geben die ausführbaren Anweisungen an, die mit dem Schreiben und/oder Lesen dieser Eigenschaft verknüpft sind.
accessor_declarations
: get_accessor_declaration set_accessor_declaration?
| set_accessor_declaration get_accessor_declaration?
;
get_accessor_declaration
: attributes? accessor_modifier? 'get' accessor_body
;
set_accessor_declaration
: attributes? accessor_modifier? 'set' accessor_body
;
accessor_modifier
: 'protected'
| 'internal'
| 'private'
| 'protected' 'internal'
| 'internal' 'protected'
| 'protected' 'private'
| 'private' 'protected'
;
accessor_body
: block
| '=>' expression ';'
| ';'
;
ref_get_accessor_declaration
: attributes? accessor_modifier? 'get' ref_accessor_body
;
ref_accessor_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
Die accessor_declarations bestehen aus einem get_accessor_declaration, einem set_accessor_declaration oder beidem. Jede Accessordeklaration besteht aus optionalen Attributen, einem optionalen accessor_modifier, dem Token get
oder set
, gefolgt von einer accessor_body.
Für eine referenzwertige Eigenschaft besteht die ref_get_accessor_declaration aus optionalen Attributen, einem optionalen accessor_modifier, dem Token get
, gefolgt von einem ref_accessor_body.
Die Verwendung von accessor_modifiers unterliegt den folgenden Einschränkungen:
- Ein accessor_modifier darf nicht in einer Schnittstelle oder in einer expliziten Schnittstellenmememerimplementierung verwendet werden.
- Für eine Eigenschaft oder einen Indexer ohne
override
Modifizierer ist eine accessor_modifier nur zulässig, wenn die Eigenschaft oder der Indexer über einen Get- und Set-Accessor verfügt und dann nur für einen dieser Accessoren zulässig ist. - Für eine Eigenschaft oder einen Indexer, die einen
override
Modifizierer enthält, muss ein Accessor mit dem accessor_modifier übereinstimmen, sofern vorhanden, des überschriebenen Accessors. - Die accessor_modifier deklarieren eine Barrierefreiheit, die streng restriktiver ist als die deklarierte Barrierefreiheit der Eigenschaft oder des Indexers selbst. Um genau zu sein:
- Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit verfügt
public
, kann die von accessor_modifier deklarierte Barrierefreiheit entwederprivate protected
, ,protected internal
, ,internal
,protected
oderprivate
. - Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit verfügt
protected internal
, kann die von accessor_modifier deklarierte Barrierefreiheit entwederprivate protected
, ,protected private
, ,internal
,protected
oderprivate
. - Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit von
internal
oderprotected
verfügt, muss die von accessor_modifier deklarierte Barrierefreiheit entwederprivate protected
oderprivate
. - Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit verfügt, lautet
private
die von accessor_modifier deklarierte Barrierefreiheitprivate protected
. - Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit von
private
verfügt, können keine accessor_modifier verwendet werden.
- Wenn die Eigenschaft oder der Indexer über eine deklarierte Barrierefreiheit verfügt
Für abstract
nicht extern
bezugswerte Eigenschaften ist jede accessor_body für jeden angegebenen Accessor einfach ein Semikolon. Eine nicht abstrakte, nicht externe Eigenschaft, aber kein Indexer, kann auch die accessor_body für alle Accessoren als Semikolon angegeben sein. In diesem Fall handelt es sich um eine automatisch implementierte Eigenschaft (§15.7.4). Eine automatisch implementierte Eigenschaft muss mindestens über einen Get-Accessor verfügen. Für die Accessoren anderer nicht abstrakter, nicht externer Eigenschaften ist die accessor_body entweder:
- ein Block , der die auszuführenden Anweisungen angibt, wenn der entsprechende Accessor aufgerufen wird; oder
- Ein Ausdruckstext, der
=>
aus einem Ausdruck und einem Semikolon besteht und einen einzelnen Ausdruck angibt, der ausgeführt werden soll, wenn der entsprechende Accessor aufgerufen wird.
Bei abstract
eigenschaften und extern
ref-valued ist die ref_accessor_body einfach ein Semikolon. Für den Accessor anderer nicht abstrakter, nicht externer Eigenschaft ist die ref_accessor_body eine der folgenden:
- ein Block , der die auszuführenden Anweisungen angibt, wenn der Get-Accessor aufgerufen wird; oder
- ein Ausdruckstext, gefolgt von
=>
ref
, einem variable_reference und einem Semikolon. Der Variableverweis wird ausgewertet, wenn der Get-Accessor aufgerufen wird.
Ein Get-Accessor für eine Eigenschaft ohne Bezugswert entspricht einer parameterlosen Methode mit einem Rückgabewert des Eigenschaftstyps. Mit Ausnahme des Ziels einer Zuweisung wird, wenn auf eine solche Eigenschaft in einem Ausdruck verwiesen wird, der Accessor zum Abrufen aufgerufen wird, um den Wert der Eigenschaft zu berechnen (§12.2.2).
Der Textkörper eines Get-Accessors für eine nicht refwertierte Eigenschaft muss den Regeln für rückgabefähige Methoden entsprechen, die in §15.6.11 beschrieben sind. Insbesondere müssen alle return
Anweisungen im Textkörper eines Get-Accessors einen Ausdruck angeben, der implizit in den Eigenschaftstyp konvertierbar ist. Darüber hinaus darf der Endpunkt eines Get-Accessors nicht erreichbar sein.
Ein Get-Accessor für eine referenzwertige Eigenschaft entspricht einer parameterlosen Methode mit einem Rückgabewert eines variable_reference einer Variablen des Eigenschaftstyps. Wenn auf eine solche Eigenschaft in einem Ausdruck verwiesen wird, wird der Accessor zum Abrufen aufgerufen, um den variable_reference Wert der Eigenschaft zu berechnen. Dieser Variablenverweis wird dann wie jeder andere verwendet, um die variable Variable_reference zu lesen oder für nicht-readonly variable_references die referenzierte Variable nach Bedarf im Kontext zu schreiben.
Beispiel: Im folgenden Beispiel wird eine Referenzeigenschaft als Ziel einer Zuordnung veranschaulicht:
class Program { static int field; static ref int Property => ref field; static void Main() { field = 10; Console.WriteLine(Property); // Prints 10 Property = 20; // This invokes the get accessor, then assigns // via the resulting variable reference Console.WriteLine(field); // Prints 20 } }
Endbeispiel
Der Textkörper eines Get-Accessors für eine referenzwertige Eigenschaft muss den Regeln für refwertige Methoden entsprechen, die in §15.6.11 beschrieben sind.
Ein Set-Accessor entspricht einer Methode mit einem einzelnen Wertparameter des Eigenschaftstyps und einem void
Rückgabetyp. Der implizite Parameter eines Set-Accessors wird immer benannt value
. Wenn auf eine Eigenschaft als Ziel einer Zuordnung (§12.21) oder als Operand von ++
oder –-
(§12.8.16, §12.9.6) verwiesen wird, wird der Set-Accessor mit einem Argument aufgerufen, das den neuen Wert (§12.21.2) bereitstellt. Die Einrichtung eines Set-Accessors entspricht den in void
§15.6.11 beschriebenen Methoden. Insbesondere dürfen Rückgabeanweisungen im Textkörper des Set-Accessors keinen Ausdruck angeben. Da ein Set-Accessor implizit einen Parameter mit dem Namen value
hat, handelt es sich um einen Kompilierungszeitfehler für eine lokale Variable oder konstante Deklaration in einem Set-Accessor, der über diesen Namen verfügt.
Basierend auf dem Vorhandensein oder Fehlen der Get- und Set-Accessoren wird eine Eigenschaft wie folgt klassifiziert:
- Eine Eigenschaft, die sowohl einen Get-Accessor als auch einen Set-Accessor enthält, wird als Lese -/Schreibzugriffseigenschaft bezeichnet.
- Eine Eigenschaft, die nur über einen Get-Accessor verfügt, wird als schreibgeschützte Eigenschaft bezeichnet. Es handelt sich um einen Kompilierungszeitfehler für eine schreibgeschützte Eigenschaft, die das Ziel einer Zuordnung sein soll.
- Eine Eigenschaft, die nur einen Set-Accessor besitzt, wird als schreibgeschützte Eigenschaft bezeichnet. Mit Ausnahme des Ziels einer Zuordnung handelt es sich um einen Kompilierungszeitfehler, um auf eine schreibgeschützte Eigenschaft in einem Ausdruck zu verweisen.
Hinweis: Die Vor- und Postfix
++
- und Operatoren und--
Verbundzuordnungsoperatoren können nicht auf schreibgeschützte Eigenschaften angewendet werden, da diese Operatoren den alten Wert ihres Operanden vor dem Schreiben des neuen Lesens lesen. Endnote
Beispiel: Im folgenden Code
public class Button : Control { private string caption; public string Caption { get => caption; set { if (caption != value) { caption = value; Repaint(); } } } public override void Paint(Graphics g, Rectangle r) { // Painting code goes here } }
das
Button
Steuerelement deklariert eine öffentlicheCaption
Eigenschaft. Der Get-Accessor der Caption-Eigenschaft gibt dasstring
gespeicherte im privatencaption
Feld zurück. Der Festgelegte Accessor überprüft, ob sich der neue Wert vom aktuellen Wert unterscheidet, und wenn ja, speichert er den neuen Wert und speichert das Steuerelement neu. Eigenschaften folgen häufig dem oben gezeigten Muster: Der Get-Accessor gibt einfach einen Wert zurück, der in einemprivate
Feld gespeichert ist, und der Set-Accessor ändert diesesprivate
Feld und führt dann alle zusätzlichen Aktionen aus, die erforderlich sind, um den Vollständigen Status des Objekts zu aktualisieren. In Anbetracht derButton
obigen Klasse ist Folgendes ein Beispiel für die Verwendung derCaption
Eigenschaft:Button okButton = new Button(); okButton.Caption = "OK"; // Invokes set accessor string s = okButton.Caption; // Invokes get accessor
Hier wird der Set-Accessor aufgerufen, indem der Eigenschaft ein Wert zugewiesen wird, und der Get-Accessor wird aufgerufen, indem auf die Eigenschaft in einem Ausdruck verwiesen wird.
Endbeispiel
Die Get- und Set-Accessoren einer Eigenschaft sind keine unterschiedlichen Member, und es ist nicht möglich, die Accessoren einer Eigenschaft separat zu deklarieren.
Beispiel: Das Beispiel
class A { private string name; // Error, duplicate member name public string Name { get => name; } // Error, duplicate member name public string Name { set => name = value; } }
deklariert keine einzelne Lese-/Schreibzugriffseigenschaft. Stattdessen werden zwei Eigenschaften mit demselben Namen deklariert, eine schreibgeschützt und eine schreibgeschützt. Da zwei Elemente, die in derselben Klasse deklariert sind, nicht denselben Namen haben können, tritt im Beispiel ein Kompilierungsfehler auf.
Endbeispiel
Wenn eine abgeleitete Klasse eine Eigenschaft mit demselben Namen wie eine geerbte Eigenschaft deklariert, blendet die abgeleitete Eigenschaft die geerbte Eigenschaft sowohl beim Lesen als auch beim Schreiben aus.
Beispiel: Im folgenden Code
class A { public int P { set {...} } } class B : A { public new int P { get {...} } }
die
P
EigenschaftA
imB
Hinblick auf Lese- und Schreibvorgänge ausgeblendetP
. Daher in den AussagenB b = new B(); b.P = 1; // Error, B.P is read-only ((A)b).P = 1; // Ok, reference to A.P
die Zuordnung bewirkt
b.P
, dass ein Kompilierungszeitfehler gemeldet wird, da die schreibgeschützteP
Eigenschaft inB
der schreibgeschütztenP
EigenschaftA
ausgeblendet wird. Beachten Sie jedoch, dass eine Umwandlung für den Zugriff auf die ausgeblendeteP
Eigenschaft verwendet werden kann.Endbeispiel
Im Gegensatz zu öffentlichen Feldern stellen Eigenschaften eine Trennung zwischen dem internen Zustand eines Objekts und seiner öffentlichen Schnittstelle bereit.
Beispiel: Betrachten Sie den folgenden Code, der eine Struktur verwendet, um einen
Point
Speicherort darzustellen:class Label { private int x, y; private string caption; public Label(int x, int y, string caption) { this.x = x; this.y = y; this.caption = caption; } public int X => x; public int Y => y; public Point Location => new Point(x, y); public string Caption => caption; }
Hier verwendet die
Label
Klasse zweiint
Felderx
undy
, um den Speicherort zu speichern. Der Speicherort wird sowohl als eigenschaftX
Y
als auch alsLocation
Eigenschaft vom TypPoint
öffentlich verfügbar gemacht. Wenn es in einer zukünftigen Version vonLabel
" einfacher wird, den Standort intern zuPoint
speichern, kann die Änderung vorgenommen werden, ohne dass sich dies auf die öffentliche Schnittstelle der Klasse auswirkt:class Label { private Point location; private string caption; public Label(int x, int y, string caption) { this.location = new Point(x, y); this.caption = caption; } public int X => location.X; public int Y => location.Y; public Point Location => location; public string Caption => caption; }
Hätte und
x
y
stattdessen Felder gewesenpublic readonly
, wäre es unmöglich gewesen, eine solche Änderung an derLabel
Klasse vorzunehmen.Endbeispiel
Hinweis: Das Verfügbarmachen des Zustands über Eigenschaften ist nicht notwendigerweise weniger effizient als das direkte Verfügbarmachen von Feldern. Wenn eine Eigenschaft nicht virtuell ist und nur eine kleine Menge Code enthält, kann die Ausführungsumgebung Aufrufe von Accessoren durch den tatsächlichen Code der Accessoren ersetzen. Dieser Prozess wird als Einlinderung bezeichnet und macht den Zugriff auf Eigenschaften so effizient wie Feldzugriff, behält jedoch die erhöhte Flexibilität von Eigenschaften bei. Endnote
Beispiel: Da das Aufrufen eines Get-Accessors konzeptuell dem Lesen des Werts eines Felds entspricht, gilt es als ungültiger Programmierstil für Accessoren, um feststellbare Nebenwirkungen zu haben. Im Beispiel
class Counter { private int next; public int Next => next++; }
der Wert der
Next
Eigenschaft hängt davon ab, wie oft zuvor auf die Eigenschaft zugegriffen wurde. Daher erzeugt der Zugriff auf die Eigenschaft einen feststellbaren Nebeneffekt, und die Eigenschaft sollte stattdessen als Methode implementiert werden.Die Konvention "keine Nebeneffekte" für Accessoren bedeutet nicht, dass Accessoren immer einfach geschrieben werden sollten, um in Feldern gespeicherte Werte zurückzugeben. Tatsächlich berechnen Accessoren häufig den Wert einer Eigenschaft, indem sie auf mehrere Felder zugreifen oder Methoden aufrufen. Ein ordnungsgemäß entworfener Get-Accessor führt jedoch keine Aktionen aus, die zu feststellbaren Änderungen im Zustand des Objekts führen.
Endbeispiel
Eigenschaften können verwendet werden, um die Initialisierung einer Ressource bis zum Moment zu verzögern, auf die zuerst verwiesen wird.
Beispiel:
public class Console { private static TextReader reader; private static TextWriter writer; private static TextWriter error; public static TextReader In { get { if (reader == null) { reader = new StreamReader(Console.OpenStandardInput()); } return reader; } } public static TextWriter Out { get { if (writer == null) { writer = new StreamWriter(Console.OpenStandardOutput()); } return writer; } } public static TextWriter Error { get { if (error == null) { error = new StreamWriter(Console.OpenStandardError()); } return error; } } ... }
Die
Console
Klasse enthält drei Eigenschaften,In
,Out
undError
, die die Standardeingabe, Ausgabe und Fehlergeräte darstellen. Durch die Bereitstellung dieser Member als Eigenschaften kann die Klasse ihreConsole
Initialisierung verzögern, bis sie tatsächlich verwendet werden. Beispiel: Beim ersten Verweisen auf dieOut
Eigenschaft wie inConsole.Out.WriteLine("hello, world");
das für das Ausgabegerät zugrunde liegende Gerät
TextWriter
erstellt wird. Wenn die Anwendung jedoch keinen Verweis auf dieIn
Und-EigenschaftenError
macht, werden keine Objekte für diese Geräte erstellt.Endbeispiel
15.7.4 Automatisch implementierte Eigenschaften
Eine automatisch implementierte Eigenschaft (oder eine automatische Eigenschaft für kurze Zeit) ist eine nicht abstrakte, nicht externe, nicht refwertierte Eigenschaft mit semikolonsgeschützten accessor_bodys. Auto-Eigenschaften müssen über einen Get-Accessor verfügen und optional über einen Set-Accessor verfügen.
Wenn eine Eigenschaft als automatisch implementierte Eigenschaft angegeben wird, steht automatisch ein ausgeblendetes Sicherungsfeld für die Eigenschaft zur Verfügung, und die Accessoren werden implementiert, um aus diesem Sicherungsfeld zu lesen und zu schreiben. Auf das ausgeblendete Sicherungsfeld kann nicht zugegriffen werden, es kann nur über die automatisch implementierten Eigenschaftenaccessoren gelesen und geschrieben werden, auch innerhalb des enthaltenden Typs. Wenn die auto-Eigenschaft keinen Set-Accessor aufweist, wird das Sicherungsfeld als berücksichtigt readonly
(§15.5.3). Genau wie ein readonly
Feld kann auch eine schreibgeschützte auto-Eigenschaft im Textkörper eines Konstruktors der eingeschlossenen Klasse zugewiesen werden. Eine solche Zuordnung weist direkt dem schreibgeschützten Sicherungsfeld der Eigenschaft zu.
Eine auto-Eigenschaft kann optional eine property_initializer aufweisen, die direkt auf das Sicherungsfeld als variable_initializer (§17.7) angewendet wird.
Beispiel:
public class Point { public int X { get; set; } // Automatically implemented public int Y { get; set; } // Automatically implemented }
entspricht der folgenden Deklaration:
public class Point { private int x; private int y; public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } }
Endbeispiel
Beispiel: Im folgenden Beispiel
public class ReadOnlyPoint { public int X { get; } public int Y { get; } public ReadOnlyPoint(int x, int y) { X = x; Y = y; } }
entspricht der folgenden Deklaration:
public class ReadOnlyPoint { private readonly int __x; private readonly int __y; public int X { get { return __x; } } public int Y { get { return __y; } } public ReadOnlyPoint(int x, int y) { __x = x; __y = y; } }
Die Zuordnungen zum schreibgeschützten Feld sind gültig, da sie innerhalb des Konstruktors auftreten.
Endbeispiel
Obwohl das Sicherungsfeld ausgeblendet ist, kann dieses Feld feldorientierte Attribute über die property_declaration der automatisch implementierten Eigenschaft (§15.7.1) direkt darauf angewendet haben.
Beispiel: Der folgende Code
[Serializable] public class Foo { [field: NonSerialized] public string MySecret { get; set; } }
führt dazu, dass das feldorientierte Attribut
NonSerialized
auf das vom Compiler generierte Sicherungsfeld angewendet wird, als ob der Code wie folgt geschrieben wurde:[Serializable] public class Foo { [NonSerialized] private string _mySecretBackingField; public string MySecret { get { return _mySecretBackingField; } set { _mySecretBackingField = value; } } }
Endbeispiel
15.7.5 Barrierefreiheit
Wenn ein Accessor über eine accessor_modifier verfügt, wird die Barrierefreiheitsdomäne (§7.5.3) des Accessors mithilfe der deklarierten Barrierefreiheit der accessor_modifier bestimmt. Wenn ein Accessor über keine accessor_modifier verfügt, wird die Barrierefreiheitsdomäne des Accessors anhand der deklarierten Barrierefreiheit der Eigenschaft oder des Indexers bestimmt.
Das Vorhandensein einer accessor_modifier betrifft niemals die Membersuche (§12.5) oder Überladungsauflösung (§12.6.4). Die Modifizierer für die Eigenschaft oder den Indexer bestimmen immer, an welche Eigenschaft oder indexer unabhängig vom Kontext des Zugriffs gebunden ist.
Nachdem eine bestimmte nicht bezugsbezogene Eigenschaft oder ein nicht bezugswertiger Indexer ausgewählt wurde, werden die Barrierefreiheitsdomänen der beteiligten Accessoren verwendet, um festzustellen, ob diese Verwendung gültig ist:
- Wenn die Nutzung als Wert (§12.2.2) gilt, muss der Get-Accessor vorhanden und zugänglich sein.
- Wenn die Verwendung als Ziel einer einfachen Zuordnung (§12.21.2) dient, muss der Set-Accessor vorhanden und zugänglich sein.
- Ist die Verwendung als Ziel der Zusammengesetzten Zuordnung (§12.21.4) oder als Ziel der
++
Operatoren--
(§12.8.16, §12.9.6), sind sowohl die Accessoren als auch der festgelegte Accessor vorhanden und zugänglich.
Beispiel: Im folgenden Beispiel wird die Eigenschaft durch die Eigenschaft
A.Text
B.Text
ausgeblendet, auch in Kontexten, in denen nur der Set-Accessor aufgerufen wird. Im Gegensatz dazu kann auf die EigenschaftB.Count
nicht auf die KlasseM
zugegriffen werden, sodass stattdessen die barrierefreie EigenschaftA.Count
verwendet wird.class A { public string Text { get => "hello"; set { } } public int Count { get => 5; set { } } } class B : A { private string text = "goodbye"; private int count = 0; public new string Text { get => text; protected set => text = value; } protected new int Count { get => count; set => count = value; } } class M { static void Main() { B b = new B(); b.Count = 12; // Calls A.Count set accessor int i = b.Count; // Calls A.Count get accessor b.Text = "howdy"; // Error, B.Text set accessor not accessible string s = b.Text; // Calls B.Text get accessor } }
Endbeispiel
Sobald eine bestimmte referenzwertige Eigenschaft oder ein verweiswertiger Indexer ausgewählt wurde; ob es sich bei der Verwendung um einen Wert, das Ziel einer einfachen Zuordnung oder um das Ziel einer zusammengesetzten Zuordnung handelt; Die Barrierefreiheitsdomäne des beteiligten Zugriffsberechtigungsmoduls wird verwendet, um festzustellen, ob diese Verwendung gültig ist.
Ein Accessor, der zum Implementieren einer Schnittstelle verwendet wird, darf keine accessor_modifier haben. Wenn nur ein Accessor zum Implementieren einer Schnittstelle verwendet wird, kann der andere Accessor mit einem accessor_modifier deklariert werden:
Beispiel:
public interface I { string Prop { get; } } public class C : I { public string Prop { get => "April"; // Must not have a modifier here internal set {...} // Ok, because I.Prop has no set accessor } }
Endbeispiel
15.7.6 Virtuelle, versiegelte, außer Kraft setzen und abstrakte Accessoren
Hinweis: Diese Klausel gilt sowohl für Eigenschaften (§15.7) als auch für Indexer (§15.9). Die Klausel wird in Bezug auf Eigenschaften geschrieben, wenn das Lesen von Indexern indexer/indexer für Eigenschaften/Eigenschaften ersetzt und die Liste der Unterschiede zwischen Eigenschaften und Indexern in §15.9.2 konsultieren. Endnote
Eine Deklaration einer virtuellen Eigenschaft gibt an, dass die Accessoren der Eigenschaft virtuell sind. Der virtual
Modifizierer gilt für alle nicht privaten Accessoren einer Eigenschaft. Wenn ein Accessor einer virtuellen Eigenschaft über die private
accessor_modifier verfügt, ist der private Accessor implizit nicht virtuell.
Eine abstrakte Eigenschaftsdeklaration gibt an, dass die Accessoren der Eigenschaft virtuell sind, aber keine tatsächliche Implementierung der Accessoren bereitstellt. Stattdessen müssen nicht abstrakte abgeleitete Klassen ihre eigene Implementierung für die Accessoren bereitstellen, indem die Eigenschaft überschrieben wird. Da ein Accessor für eine abstrakte Eigenschaftsdeklaration keine tatsächliche Implementierung bereitstellt, besteht die accessor_body einfach aus einem Semikolon. Eine abstrakte Eigenschaft darf keinen Accessor haben private
.
Eine Eigenschaftsdeklaration, die sowohl die Modifizierer als auch die abstract
override
Eigenschaft enthält, gibt an, dass die Eigenschaft abstrakt ist und eine Basiseigenschaft überschreibt. Die Accessoren einer solchen Eigenschaft sind auch abstrakt.
Abstrakte Eigenschaftendeklarationen sind nur in abstrakten Klassen zulässig (§15.2.2.2.2). Die Accessoren einer geerbten virtuellen Eigenschaft können in einer abgeleiteten Klasse überschrieben werden, indem eine Eigenschaftsdeklaration eingeschlossen wird, die eine override
Direktive angibt. Dies wird als überschriebene Eigenschaftsdeklaration bezeichnet. Eine überschreibende Eigenschaftsdeklaration deklariert keine neue Eigenschaft. Stattdessen ist sie einfach auf die Implementierungen der Accessoren einer vorhandenen virtuellen Eigenschaft spezialisiert.
Die Überschreibungsdeklaration und die überschriebene Basiseigenschaft sind erforderlich, damit die gleiche deklarierte Barrierefreiheit vorhanden ist. Mit anderen Worten, eine Außerkraftsetzungsdeklaration darf die Barrierefreiheit der Basiseigenschaft nicht ändern. Wenn die überschriebene Basiseigenschaft jedoch intern geschützt ist und sie in einer anderen Assembly deklariert wird als die Assembly, die die Überschreibungsdeklaration enthält, dann muss die deklarierte Barrierefreiheit der Deklaration überschrieben werden. Wenn die geerbte Eigenschaft nur über einen einzelnen Accessor verfügt (d. h., wenn die geerbte Eigenschaft schreibgeschützt oder schreibgeschützt ist), darf die überschriebene Eigenschaft nur diesen Accessor enthalten. Wenn die geerbte Eigenschaft beide Accessoren enthält (d. h., wenn die geerbte Eigenschaft schreibgeschützt ist), kann die überschriebene Eigenschaft entweder einen einzelnen Accessor oder beide Accessoren enthalten. Es muss eine Identitätskonvertierung zwischen dem Typ der Außerkraftsetzung und der geerbten Eigenschaft geben.
Eine überschreibende Eigenschaftsdeklaration kann den sealed
Modifizierer enthalten. Die Verwendung dieses Modifizierers verhindert, dass eine abgeleitete Klasse die Eigenschaft weiter überschreibt. Die Accessoren einer versiegelten Eigenschaft sind ebenfalls versiegelt.
Mit Ausnahme von Unterschieden bei der Deklarations- und Aufrufssyntax verhalten sich virtuelle, versiegelte, überschreibende und abstrakte Accessoren genau wie virtuelle, versiegelte, außer Außerkraftsetzungen und abstrakte Methoden. Insbesondere gelten die in §15.6.4, §15.6.5, §15.6.6 und §15.6.7 beschriebenen Regeln so, als wären Accessoren Methoden einer entsprechenden Form:
- Ein Get-Accessor entspricht einer parameterlosen Methode mit einem Rückgabewert des Eigenschaftstyps und den gleichen Modifizierern wie die enthaltende Eigenschaft.
- Ein Set-Accessor entspricht einer Methode mit einem einzelnen Wertparameter des Eigenschaftstyps, einem void-Rückgabetyp und den gleichen Modifizierern wie die enthaltende Eigenschaft.
Beispiel: Im folgenden Code
abstract class A { int y; public virtual int X { get => 0; } public virtual int Y { get => y; set => y = value; } public abstract int Z { get; set; } }
X
ist eine virtuelle schreibgeschützte Eigenschaft,Y
ist eine virtuelle Lese-/Schreibeigenschaft undZ
eine abstrakte Lese-/Schreibeigenschaft. DaZ
sie abstrahiert ist, muss die enthaltende Klasse A auch abstrakt deklariert werden.Eine Klasse, die von
A
der abgeleitet wird, wird unten gezeigt:class B : A { int z; public override int X { get => base.X + 1; } public override int Y { set => base.Y = value < 0 ? 0: value; } public override int Z { get => z; set => z = value; } }
Hier werden die Deklarationen von
X
,Y
undZ
es werden Eigenschaftsdeklarationen überschrieben. Jede Eigenschaftsdeklaration entspricht exakt den Modifizierern, Typ und Namen der entsprechenden geerbten Eigenschaft. Der AccessorX
abrufen und der Set-Accessor der Verwendung desY
Basisschlüsselworts für den Zugriff auf die geerbten Accessoren. Die Deklaration vonZ
Außerkraftsetzungen beider abstrakter Accessoren – daher gibt es keine herausragendenabstract
Funktionsmember inB
, undB
es ist zulässig, eine nicht abstrakte Klasse zu sein.Endbeispiel
Wenn eine Eigenschaft als Außerkraftsetzung deklariert wird, können alle überschriebenen Accessoren auf den überschreibenden Code zugreifen. Darüber hinaus muss die deklarierte Barrierefreiheit sowohl der Eigenschaft als auch des Indexers selbst und der Accessoren mit der der überschriebenen Member und Accessoren übereinstimmen.
Beispiel:
public class B { public virtual int P { get {...} protected set {...} } } public class D: B { public override int P { get {...} // Must not have a modifier here protected set {...} // Must specify protected here } }
Endbeispiel
15.8 Veranstaltungen
15.8.1 Allgemein
Ein Ereignis ist ein Member, der es einem Objekt oder einer Klasse ermöglicht, Benachrichtigungen bereitzustellen. Clients können Ereignissen ausführbaren Code hinzufügen, indem Sie Ereignishandler bereitstellen.
Ereignisse werden mit event_declarations deklariert:
event_declaration
: attributes? event_modifier* 'event' type variable_declarators ';'
| attributes? event_modifier* 'event' type member_name
'{' event_accessor_declarations '}'
;
event_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
event_accessor_declarations
: add_accessor_declaration remove_accessor_declaration
| remove_accessor_declaration add_accessor_declaration
;
add_accessor_declaration
: attributes? 'add' block
;
remove_accessor_declaration
: attributes? 'remove' block
;
unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
Ein event_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), der new
(§15.3.5), static
(§15.6.3, §15.8.4), virtual
(§15.6.4, §15.8.5), override
(§15.6.5, §15.8.5), sealed
(§15.6.6), abstract
(§15.6.7, §15.8.5) und extern
(§15.6.8) Modifizierer.
Ereignisdeklarationen unterliegen den gleichen Regeln wie Methodendeklarationen (§15.6) in Bezug auf gültige Kombinationen von Modifizierern.
Die Art einer Ereigniserklärung muss eine delegate_type (§8.2.8) sein und dass delegate_type mindestens so zugänglich sein muss wie das Ereignis selbst (§7.5.5).
Eine Ereignisdeklaration kann event_accessor_declarations enthalten. Ist dies jedoch nicht der Fall, wird der Compiler bei nicht extern nicht abstrakten Ereignissen automatisch (§15.8.2) bereitgestellt. Für extern
Ereignisse werden die Accessoren extern bereitgestellt.
Eine Ereignisdeklaration, die event_accessor_declaration ausgelassen wird, definiert ein oder mehrere Ereignisse – eines für jedes der variable_declarators. Die Attribute und Modifizierer gelten für alle Elemente, die von einem solchen event_declaration deklariert wurden.
Es handelt sich um einen Kompilierungszeitfehler für eine event_declaration , um sowohl den abstract
Modifizierer als auch event_accessor_declarations einzuschließen.
Wenn eine Ereignisdeklaration einen extern
Modifizierer enthält, wird das Ereignis als externes Ereignis bezeichnet. Da eine externe Ereignisdeklaration keine tatsächliche Implementierung bereitstellt, handelt es sich um einen Fehler, der sowohl den extern
Modifizierer als auch event_accessor_declarationenthält.
Es handelt sich um einen Kompilierungszeitfehler für eine variable_declarator einer Ereignisdeklaration mit einem abstract
oder external
einem Modifizierer, um eine variable_initializer einzuschließen.
Ein Ereignis kann als linker Operand der +=
Operatoren -=
verwendet werden. Diese Operatoren werden verwendet, um Ereignishandler anzufügen oder Ereignishandler aus einem Ereignis zu entfernen, und die Zugriffsmodifizierer des Ereignisses steuern die Kontexte, in denen solche Vorgänge zulässig sind.
Die einzigen Vorgänge, die für ein Ereignis durch Code zulässig sind, der sich außerhalb des Typs befindet, in dem dieses Ereignis deklariert wird, sind +=
und -=
. Daher kann ein solcher Code Handler für ein Ereignis hinzufügen und entfernen, nicht direkt die zugrunde liegende Liste der Ereignishandler abrufen oder ändern.
Bei einem Vorgang des Formulars x += y
oder x –= y
, wenn es sich um ein Ereignis handelt, x
hat das Ergebnis des Vorgangs Typ void
(§12.21.5) (im Gegensatz zum Typ von x
) mit dem Wert nach x
der Zuordnung, wie für andere die und -=
Operatoren, die +=
für Nicht-Ereignistypen definiert sind). Dadurch wird verhindert, dass externer Code indirekt den zugrunde liegenden Delegat eines Ereignisses untersucht.
Beispiel: Das folgende Beispiel zeigt, wie Ereignishandler an Instanzen der
Button
Klasse angefügt werden:public delegate void EventHandler(object sender, EventArgs e); public class Button : Control { public event EventHandler Click; } public class LoginDialog : Form { Button okButton; Button cancelButton; public LoginDialog() { okButton = new Button(...); okButton.Click += new EventHandler(OkButtonClick); cancelButton = new Button(...); cancelButton.Click += new EventHandler(CancelButtonClick); } void OkButtonClick(object sender, EventArgs e) { // Handle okButton.Click event } void CancelButtonClick(object sender, EventArgs e) { // Handle cancelButton.Click event } }
Hier erstellt der
LoginDialog
Instanzkonstruktor zweiButton
Instanzen und fügt Ereignishandler an dieClick
Ereignisse an.Endbeispiel
15.8.2 Feldähnliche Ereignisse
Innerhalb des Programmtexts der Klasse oder Struktur, die die Deklaration eines Ereignisses enthält, können bestimmte Ereignisse wie Felder verwendet werden. Um auf diese Weise verwendet zu werden, darf ein Ereignis nicht abstrakt oder extern sein und darf nicht ausdrücklich event_accessor_declarations enthalten. Ein solches Ereignis kann in jedem Kontext verwendet werden, der ein Feld zulässt. Das Feld enthält einen Delegaten (§20), der sich auf die Liste der Ereignishandler bezieht, die dem Ereignis hinzugefügt wurden. Wenn keine Ereignishandler hinzugefügt wurden, enthält null
das Feld .
Beispiel: Im folgenden Code
public delegate void EventHandler(object sender, EventArgs e); public class Button : Control { public event EventHandler Click; protected void OnClick(EventArgs e) { EventHandler handler = Click; if (handler != null) { handler(this, e); } } public void Reset() => Click = null; }
Click
wird als Feld innerhalb derButton
Klasse verwendet. Wie das Beispiel zeigt, kann das Feld untersucht, geändert und in Stellvertretungsaufrufausdrücken verwendet werden. DieOnClick
Methode in derButton
Klasse "löst" dasClick
Ereignis aus. Das Auslösen eines Ereignisses entspricht exakt dem Aufrufen des Delegaten, der durch das Ereignis repräsentiert wird, es gibt deshalb keine besonderen Sprachkonstrukte zum Auslösen von Ereignissen. Beachten Sie, dass dem Aufruf der Stellvertretung eine Überprüfung vorausgeht, die sicherstellt, dass der Delegat nicht null ist und dass die Überprüfung in einer lokalen Kopie vorgenommen wird, um die Threadsicherheit sicherzustellen.Außerhalb der Deklaration der
Button
Klasse kann dasClick
Element nur auf der linken Seite der+=
Und-Operatoren–=
verwendet werden, wie inb.Click += new EventHandler(...);
die eine Stellvertretung an die Aufrufliste des
Click
Ereignisses anfügen undClick –= new EventHandler(...);
wodurch ein Delegat aus der Aufrufliste des
Click
Ereignisses entfernt wird.Endbeispiel
Beim Kompilieren eines feldähnlichen Ereignisses erstellt der Compiler automatisch Speicher zum Speichern des Delegaten und erstellt Accessoren für das Ereignis, das Ereignishandler zum Delegatfeld hinzu- oder entfernt. Die Hinzufügungs- und Entfernungsvorgänge sind threadsicher und können (aber nicht erforderlich) ausgeführt werden, während die Sperre (§13.13) für das enthaltende Objekt für ein Instanzereignis oder das System.Type
Objekt (§12.8.18) für ein statisches Ereignis gehalten wird.
Hinweis: Eine Instanzereignisdeklaration des Formulars:
class X { public event D Ev; }
wird zu folgendem Äquivalent zusammengestellt:
class X { private D __Ev; // field to hold the delegate public event D Ev { add { /* Add the delegate in a thread safe way */ } remove { /* Remove the delegate in a thread safe way */ } } }
Innerhalb der Klasse
X
führen VerweiseEv
auf die linke Seite der+=
Operatoren–=
dazu, dass die Accessoren zum Hinzufügen und Entfernen aufgerufen werden. Alle anderen Verweise, dieEv
kompiliert werden, um stattdessen auf das ausgeblendete Feld__Ev
zu verweisen (§12.8.7). Der Name "__Ev
" ist beliebig; das ausgeblendete Feld könnte überhaupt einen Namen oder keinen Namen haben.Endnote
15.8.3 Ereignisaccessoren
Hinweis: Ereignisdeklarationen lassen in der Regel event_accessor_declarations aus, wie im
Button
obigen Beispiel gezeigt. Sie können beispielsweise einbezogen werden, wenn die Speicherkosten eines Felds pro Ereignis nicht zulässig sind. In solchen Fällen kann eine Klasse event_accessor_declarationenthalten und einen privaten Mechanismus zum Speichern der Liste der Ereignishandler verwenden. Endnote
Die event_accessor_declarations eines Ereignisses geben die ausführbaren Anweisungen an, die dem Hinzufügen und Entfernen von Ereignishandlern zugeordnet sind.
Die Accessordeklarationen bestehen aus einem add_accessor_declaration und einem remove_accessor_declaration. Jede Accessordeklaration besteht aus dem Token-Hinzufügen oder Entfernen gefolgt von einem Block. Der einem add_accessor_declaration zugeordnete Block gibt die auszuführenden Anweisungen an, wenn ein Ereignishandler hinzugefügt wird, und der einem remove_accessor_declaration zugeordnete Block gibt die auszuführenden Anweisungen an, wenn ein Ereignishandler entfernt wird.
Jede add_accessor_declaration und remove_accessor_declaration entspricht einer Methode mit einem einzelnen Wertparameter des Ereignistyps und einem void
Rückgabetyp. Der implizite Parameter eines Ereignisaccessors wird benannt value
. Wenn ein Ereignis in einer Ereigniszuweisung verwendet wird, wird der entsprechende Ereignisaccessor verwendet. Wenn der Zuordnungsoperator verwendet wird +=
, wird der Accessor zum Hinzufügen verwendet, und wenn der Zuordnungsoperator verwendet wird –=
, wird der Remove-Accessor verwendet. In beiden Fällen wird der rechte Operand des Zuordnungsoperators als Argument für den Ereignisaccessor verwendet. Der Block einer add_accessor_declaration oder einer remove_accessor_declaration entspricht den void
In §15.6.9 beschriebenen Methoden. Insbesondere return
dürfen Anweisungen in einem solchen Block keinen Ausdruck angeben.
Da ein Ereignisaccessor implizit einen Parameter mit dem Namen value
hat, handelt es sich um einen Kompilierungszeitfehler für eine lokale Variable oder Konstante, die in einem Ereignisaccessor deklariert ist, um diesen Namen zu haben.
Beispiel: Im folgenden Code
class Control : Component { // Unique keys for events static readonly object mouseDownEventKey = new object(); static readonly object mouseUpEventKey = new object(); // Return event handler associated with key protected Delegate GetEventHandler(object key) {...} // Add event handler associated with key protected void AddEventHandler(object key, Delegate handler) {...} // Remove event handler associated with key protected void RemoveEventHandler(object key, Delegate handler) {...} // MouseDown event public event MouseEventHandler MouseDown { add { AddEventHandler(mouseDownEventKey, value); } remove { RemoveEventHandler(mouseDownEventKey, value); } } // MouseUp event public event MouseEventHandler MouseUp { add { AddEventHandler(mouseUpEventKey, value); } remove { RemoveEventHandler(mouseUpEventKey, value); } } // Invoke the MouseUp event protected void OnMouseUp(MouseEventArgs args) { MouseEventHandler handler; handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey); if (handler != null) { handler(this, args); } } }
die
Control
Klasse implementiert einen internen Speichermechanismus für Ereignisse. DieAddEventHandler
Methode ordnet einen Delegatwert einem Schlüssel zu, die Methode gibt dieGetEventHandler
Stellvertretung zurück, die 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 handelt sich um einen Kompilierungszeitfehler, der in den Accessoren eines statischen Ereignisses referenziert this
wird.
Ein Instanzereignis ist einer bestimmten Instanz einer Klasse zugeordnet, und auf diese Instanz kann in den Accessoren dieses Ereignisses als this
(§12.8.14) zugegriffen werden.
Die Unterschiede zwischen statischen und Instanzmitgliedern werden in §15.3.8 weiter erörtert.
15.8.5 Virtuelle, versiegelte, außer Kraft setzen und abstrakte Accessoren
Eine virtuelle Ereignisdeklaration gibt an, dass die Accessoren dieses Ereignisses virtuell sind. Der virtual
Modifizierer gilt für beide Accessoren eines Ereignisses.
Eine abstrakte Ereignisdeklaration gibt an, dass die Accessoren des Ereignisses virtuell sind, aber keine tatsächliche Implementierung der Accessoren bereitstellt. Stattdessen müssen nicht abstrakte abgeleitete Klassen ihre eigene Implementierung für die Accessoren bereitstellen, indem das Ereignis überschrieben wird. Da ein Accessor für eine abstrakte Ereignisdeklaration keine tatsächliche Implementierung bereitstellt, stellt er event_accessor_declarations nicht bereit.
Eine Ereignisdeklaration, die sowohl die Modifizierer als auch die abstract
override
Modifizierer enthält, gibt an, dass das Ereignis abstrakt ist und ein Basisereignis überschreibt. Die Accessoren eines solchen Ereignisses sind auch abstrakt.
Abstrakte Ereignisdeklarationen sind nur in abstrakten Klassen zulässig (§15.2.2.2.2).
Die Accessoren eines geerbten virtuellen Ereignisses können in einer abgeleiteten Klasse überschrieben werden, indem eine Ereignisdeklaration eingeschlossen wird, die einen override
Modifizierer angibt. Dies wird als überschriebene Ereignisdeklaration bezeichnet. Eine überschreibende Ereignisdeklaration deklariert kein neues Ereignis. Stattdessen ist es einfach auf die Implementierungen der Accessoren eines vorhandenen virtuellen Ereignisses spezialisiert.
Eine überschreibende Ereignisdeklaration muss genau die gleichen Barrierefreiheitsmodifizierer und -namen wie das außerkraftsetzungsereignis angeben, es muss eine Identitätskonvertierung zwischen dem Typ des überschriebenen und überschriebenen Ereignisses vorhanden sein, und sowohl die Add- als auch Remove-Accessoren müssen innerhalb der Deklaration angegeben werden.
Eine überschreibende Ereignisdeklaration kann den sealed
Modifizierer enthalten. Die Verwendung des this
Modifizierers verhindert, dass eine abgeleitete Klasse das Ereignis weiter überschreibt. Die Accessoren eines versiegelten Ereignisses sind ebenfalls versiegelt.
Es handelt sich um einen Kompilierungszeitfehler für eine überschreibende Ereignisdeklaration, um einen new
Modifizierer einzuschließen.
Mit Ausnahme von Unterschieden bei der Deklarations- und Aufrufssyntax verhalten sich virtuelle, versiegelte, überschreibende und abstrakte Accessoren genau wie virtuelle, versiegelte, außer Außerkraftsetzungen und abstrakte Methoden. Insbesondere gelten die in §15.6.4, §15.6.5, §15.6.6 und §15.6.7 beschriebenen Regeln, als wären Accessoren Methoden eines entsprechenden Formulars. Jeder Accessor entspricht einer Methode mit einem einzelnen Wertparameter des Ereignistyps, einem void
Rückgabetyp und denselben Modifizierern wie das enthaltende Ereignis.
15.9 Indexer
15.9.1 Allgemein
Ein Indexer ist ein Element, mit dem ein Objekt auf die gleiche Weise indiziert werden kann wie ein Array. Indexer werden mit indexer_declarations deklariert:
indexer_declaration
: attributes? indexer_modifier* indexer_declarator indexer_body
| attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
;
indexer_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
indexer_declarator
: type 'this' '[' parameter_list ']'
| type interface_type '.' 'this' '[' parameter_list ']'
;
indexer_body
: '{' accessor_declarations '}'
| '=>' expression ';'
;
ref_indexer_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
Es gibt zwei Arten von indexer_declaration:
- Der erste deklariert einen Nicht-Verweis-Indexer. Der Wert weist Typ auf. Diese Art von Indexer kann lesbar und/oder schreibbar sein.
- Die zweite deklariert einen Referenzwertindexer. Der Wert ist eine variable_reference (§9.5), die eine Variable vom Typtyp sein
readonly
kann. Diese Art von Indexer ist nur lesbar.
Eine indexer_declaration kann eine Reihe von Attributen (§22) und eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6), der new
(§15.3.5), (§15.5) virtual
enthalten.6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7) und extern
(§15.6.8) Modifizierer.
Indexerdeklarationen unterliegen den gleichen Regeln wie Methodendeklarationen (§15.6) in Bezug auf gültige Kombinationen von Modifizierern, wobei eine Ausnahme darin besteht, dass der static
Modifizierer für eine Indexerdeklaration nicht zulässig ist.
Der Typ einer Indexerdeklaration gibt den Elementtyp des in der Deklaration eingeführten Indexers an.
Hinweis: Da Indexer für die Verwendung in Arrayelement-ähnlichen Kontexten konzipiert sind, wird der Ausdruckselementtyp, der für ein Array definiert ist, auch mit einem Indexer verwendet. Endnote
Sofern der Indexer keine explizite Schnittstellenmememingimplementierung ist, folgt der Typ dem Schlüsselwort this
. Für eine explizite Schnittstellenmememimplementierung folgt der Typ einem interface_type, einem ".
" und dem Schlüsselwortthis
. Im Gegensatz zu anderen Mitgliedern haben Indexer keine benutzerdefinierten Namen.
Die parameter_list gibt die Parameter des Indexers an. Die Parameterliste eines Indexers entspricht der einer Methode (§15.6.2), mit der Ausnahme, dass mindestens ein Parameter angegeben werden muss und dass die this
Modifizierer ref
out
und Parametermodifizierer nicht zulässig sind.
Der Typ eines Indexers und jeder der typen, auf die in der parameter_list verwiesen wird, muss mindestens so zugänglich sein wie der Indexer selbst (§7.5.5).
Ein indexer_body kann entweder aus einem Anweisungstext (§15.7.1) oder einem Ausdruckstext (§15.6.1) bestehen. In einem Anweisungstext deklarieren accessor_declarations, die in "{
" und "}
" Token eingeschlossen werden sollen, die Accessoren (§15.7.3) des Indexers. Die Accessoren geben die ausführbaren Anweisungen an, die dem Lesen und Schreiben von Indexerelementen zugeordnet sind.
In einem indexer_body einen Ausdruckstext, der aus "=>
" gefolgt von einem Ausdruck E
und einem Semikolon besteht, entspricht genau dem Textkörper der Anweisung { get { return E; } }
und kann daher nur verwendet werden, um schreibgeschützte Indexer anzugeben, bei denen das Ergebnis des Get-Accessors durch einen einzelnen Ausdruck angegeben wird.
Ein ref_indexer_body kann entweder aus einem Anweisungstext oder einem Ausdruckstext bestehen. In einem Anweisungstext deklariert ein get_accessor_declaration den get accessor (§15.7.3) des Indexers. Der Accessor gibt die ausführbaren Anweisungen an, die mit dem Lesen des Indexers verknüpft sind.
In einem ref_indexer_body einem Ausdruckstext, der aus gefolgt wird =>
ref
, entspricht ein variable_reference V
und ein Semikolon exakt dem Textkörper der Anweisung{ get { return ref V; } }
.
Hinweis: Obwohl die Syntax für den Zugriff auf ein Indexerelement mit dem für ein Arrayelement identisch ist, wird ein Indexerelement nicht als Variable klassifiziert. Daher ist es nicht möglich, ein Indexerelement als
in
,out
oderref
Argument zu übergeben, es sei denn, der Indexer ist ref-valued und gibt daher einen Verweis zurück (§9.7). Endnote
Die parameter_list eines Indexers definiert die Signatur (§7.6) des Indexers. Insbesondere besteht die Signatur eines Indexers aus der Anzahl und den Typen seiner Parameter. Der Elementtyp und die Namen der Parameter sind nicht Teil der Signatur eines Indexers.
Die Signatur eines Indexers unterscheidet sich von den Signaturen aller anderen in derselben Klasse deklarierten Indexer.
Wenn eine Indexerdeklaration einen extern
Modifizierer enthält, wird der Indexer als externer Indexer bezeichnet. Da eine externe Indexerdeklaration keine tatsächliche Implementierung bereitstellt, muss jedes accessor_body s in seinem accessor_declarations ein Semikolon sein.
Beispiel: Im folgenden Beispiel wird eine
BitArray
Klasse deklariert, die einen Indexer für den Zugriff auf die einzelnen Bits im Bitarray implementiert.class BitArray { int[] bits; int length; public BitArray(int length) { if (length < 0) { throw new ArgumentException(); } bits = new int[((length - 1) >> 5) + 1]; this.length = length; } public int Length => length; public bool this[int index] { get { if (index < 0 || index >= length) { throw new IndexOutOfRangeException(); } return (bits[index >> 5] & 1 << index) != 0; } set { if (index < 0 || index >= length) { throw new IndexOutOfRangeException(); } if (value) { bits[index >> 5] |= 1 << index; } else { bits[index >> 5] &= ~(1 << index); } } } }
Eine Instanz der
BitArray
Klasse verbraucht wesentlich weniger Arbeitsspeicher als ein entsprechendesbool[]
(da jeder Wert des Ehemaligen nur ein Bit anstelle des zweitenbyte
belegt), aber es erlaubt die gleichen Vorgänge wie einbool[]
.Die folgende
CountPrimes
Klasse verwendet einenBitArray
und den klassischen "Sieve"-Algorithmus, um die Anzahl der Primes zwischen 2 und einem bestimmten Maximum zu berechnen:class CountPrimes { static int Count(int max) { BitArray flags = new BitArray(max + 1); int count = 0; for (int i = 2; i <= max; i++) { if (!flags[i]) { for (int j = i * 2; j <= max; j += i) { flags[j] = true; } count++; } } return count; } static void Main(string[] args) { int max = int.Parse(args[0]); int count = Count(max); Console.WriteLine($"Found {count} primes between 2 and {max}"); } }
Beachten Sie, dass die Syntax für den Zugriff auf Elemente des
BitArray
Elements genau wie für einbool[]
.Das folgende Beispiel zeigt eine 26×10-Rasterklasse mit einem Indexer mit zwei Parametern. Der erste Parameter muss ein Groß- oder Kleinbuchstabe im Bereich A–Z sein, und die zweite muss eine ganze Zahl im Bereich von 0 bis 9 sein.
class Grid { const int NumRows = 26; const int NumCols = 10; int[,] cells = new int[NumRows, NumCols]; public int this[char row, int col] { get { row = Char.ToUpper(row); if (row < 'A' || row > 'Z') { throw new ArgumentOutOfRangeException("row"); } if (col < 0 || col >= NumCols) { throw new ArgumentOutOfRangeException ("col"); } return cells[row - 'A', col]; } set { row = Char.ToUpper(row); if (row < 'A' || row > 'Z') { throw new ArgumentOutOfRangeException ("row"); } if (col < 0 || col >= NumCols) { throw new ArgumentOutOfRangeException ("col"); } cells[row - 'A', col] = value; } } }
Endbeispiel
15.9.2 Indexer- und Eigenschaftsunterschiede
Indexer und Eigenschaften sind im Konzept sehr ähnlich, unterscheiden sich jedoch auf die folgenden Arten:
- Eine Eigenschaft wird durch ihren Namen identifiziert, während ein Indexer durch seine Signatur identifiziert wird.
- Auf eine Eigenschaft wird über eine simple_name (§12.8.4) oder eine member_access (§12.8.7) zugegriffen, während auf ein Indexerelement über eine element_access (§12.8.12.3) zugegriffen wird.
- Eine Eigenschaft kann ein statisches Element sein, während ein Indexer immer ein Instanzmemm ist.
- Ein Get-Accessor einer Eigenschaft entspricht einer Methode ohne Parameter, während ein Get-Accessor eines Indexers einer Methode mit derselben Parameterliste wie der Indexer entspricht.
- Ein Set-Accessor einer Eigenschaft entspricht einer Methode mit einem einzigen Parameter namens
value
, während ein Set-Accessor eines Indexers einer Methode mit derselben Parameterliste wie der Indexer entspricht, sowie einem zusätzlichen Parameter mit dem Namenvalue
. - Es handelt sich um einen Kompilierungszeitfehler für einen Indexer-Accessor, um eine lokale Variable oder lokale Konstante mit demselben Namen wie ein Indexerparameter zu deklarieren.
- Bei einer überschreibenden Eigenschaftsdeklaration wird mithilfe der Syntax
base.P
auf die geerbte Eigenschaft zugegriffen, wobeiP
der Eigenschaftsname angegeben ist. In einer überschreibenden Indexerdeklaration wird mithilfe der Syntaxbase[E]
auf den geerbten Indexer zugegriffen, wobeiE
es sich um eine durch Trennzeichen getrennte Liste von Ausdrücken handelt. - Es gibt kein Konzept eines "automatisch implementierten Indexers". Es ist ein Fehler, wenn ein nicht abstrakter, nicht externer Indexer mit Semikolon accessor_bodys vorhanden ist.
Abgesehen von diesen Unterschieden gelten alle in §15.7.3, §15.7.5 und §15.7.6 definierten Regeln sowohl für Indexer-Accessoren als auch für Eigenschaftsaccessoren.
Diese Ersetzung von Eigenschaften/Eigenschaften durch Indexer/Indexer beim Lesen von §15.7.3, §15.7.5 und §15.7.6 gilt auch für definierte Begriffe. Insbesondere wird die Eigenschaft mit Lese-/Schreibzugriff in den Indexer, schreibgeschützte Eigenschaft wird schreibgeschützter Indexer, und schreibgeschützte Eigenschaft wird schreibgeschützter Indexer.
15.10 Operatoren
15.10.1 Allgemein
Ein Operator ist ein Element, das die Bedeutung eines Ausdrucksoperators definiert, der auf Instanzen der Klasse angewendet werden kann. Operatoren werden mit operator_declarations deklariert:
operator_declaration
: attributes? operator_modifier+ operator_declarator operator_body
;
operator_modifier
: 'public'
| 'static'
| 'extern'
| unsafe_modifier // unsafe code support
;
operator_declarator
: unary_operator_declarator
| binary_operator_declarator
| conversion_operator_declarator
;
unary_operator_declarator
: type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
;
overloadable_unary_operator
: '+' | '-' | '!' | '~' | '++' | '--' | 'true' | 'false'
;
binary_operator_declarator
: type 'operator' overloadable_binary_operator
'(' fixed_parameter ',' fixed_parameter ')'
;
overloadable_binary_operator
: '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '<<'
| right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
;
conversion_operator_declarator
: 'implicit' 'operator' type '(' fixed_parameter ')'
| 'explicit' 'operator' type '(' fixed_parameter ')'
;
operator_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
Es gibt drei Kategorien überladener Operatoren: Unäre Operatoren (§15.10.2), binäre Operatoren (§15.10.3) und Konvertierungsoperatoren (§15.10.4).
Die operator_body ist entweder ein Semikolon, ein Blocktext (§15.6.1) oder ein Ausdruckstext (§15.6.1). Ein Blocktext besteht aus einem Block, der die auszuführenden Anweisungen angibt, wenn der Operator aufgerufen wird. Der Block muss den Regeln für rückgabende Methoden entsprechen, die in §15.6.11 beschrieben sind. Ein Ausdruckstext =>
besteht aus einem Ausdruck, gefolgt von einem Ausdruck und einem Semikolon und zeigt einen einzelnen Ausdruck an, der ausgeführt werden soll, wenn der Operator aufgerufen wird.
Bei extern
Operatoren besteht die operator_body einfach aus einem Semikolon. Bei allen anderen Operatoren ist die operator_body entweder ein Blocktext oder ein Ausdruckstext.
Die folgenden Regeln gelten für alle Operatordeklarationen:
- Eine Betreibererklärung enthält sowohl einen
public
Als auch einenstatic
Modifizierer. - Die Parameter eines Operators dürfen keine anderen Modifizierer als
in
. - Die Signatur eines Betreibers (§15.10.2, §15.10.3, §15.10.4) unterscheidet sich von den Signaturen aller anderen Betreiber, die in derselben Klasse deklariert sind.
- Alle typen, auf die in einer Betreibererklärung verwiesen wird, sind mindestens so zugänglich wie der Betreiber selbst (§7.5.5).
- Es handelt sich um einen Fehler für denselben Modifizierer, der mehrmals in einer Operatordeklaration angezeigt wird.
Jede Operatorkategorie legt zusätzliche Einschränkungen fest, wie in den folgenden Unterlisten beschrieben.
Wie andere Member werden in einer Basisklasse deklarierte Operatoren von abgeleiteten Klassen geerbt. Da Operatordeklarationen immer die Klasse oder Anweisung erfordern, an der der Operator deklariert wird, um an der Signatur des Operators teilzunehmen, ist es nicht möglich, dass ein in einer abgeleiteten Klasse deklarierter Operator einen in einer Basisklasse deklarierten Operator ausblenden kann. Daher ist der new
Modifizierer niemals erforderlich und daher in einer Operatordeklaration nie zulässig.
Weitere Informationen zu unären und binären Operatoren finden Sie unter §12.4.
Weitere Informationen zu Konvertierungsoperatoren finden Sie unter §10.5.
15.10.2 Unäre Betreiber
Die folgenden Regeln gelten für unäre Operatordeklarationen, wobei T
der Instanztyp der Klasse oder Struktur, die die Operatordeklaration enthält, bezeichnet wird:
- Ein unärer
+
,-
, ,!
oder~
Operator muss einen einzelnen Parameter vom TypT
T?
oder kann jeden Typ zurückgeben. - Ein unärer
++
oder--
betreiber muss einen einzigen TypparameterT
annehmen oderT?
diesen Typ oder einen daraus abgeleiteten Typ zurückgeben. - Ein unärer
true
oderfalse
betreiber hat einen einzigen Parameter vom Typ oderT?
T
gibt den Typbool
zurück.
Die Signatur eines unären Operators besteht aus dem Operatortoken (+
, , !
++
~
--
-
true
oder false
) und dem Typ des einzelnen Parameters. Der Rückgabetyp ist weder Teil der Signatur eines unären Operators noch der Name des Parameters.
Für die true
und false
unäre Operatoren ist eine paarweise Deklaration erforderlich. Wenn eine Klasse einen dieser Operatoren deklariert, ohne die andere zu deklarieren, tritt ein Kompilierungszeitfehler auf. Die Betreiber und false
Betreiber true
werden in §12.24 weiter beschrieben.
Beispiel: Das folgende Beispiel zeigt eine Implementierung und nachfolgende Verwendung von Operator++ für eine ganzzahlige Vektorklasse:
public class IntVector { public IntVector(int length) {...} public int Length { get { ... } } // Read-only property public int this[int index] { get { ... } set { ... } } // Read-write indexer public static IntVector operator++(IntVector iv) { IntVector temp = new IntVector(iv.Length); for (int i = 0; i < iv.Length; i++) { temp[i] = iv[i] + 1; } return temp; } } class Test { static void Main() { IntVector iv1 = new IntVector(4); // Vector of 4 x 0 IntVector iv2; iv2 = iv1++; // iv2 contains 4 x 0, iv1 contains 4 x 1 iv2 = ++iv1; // iv2 contains 4 x 2, iv1 contains 4 x 2 } }
Beachten Sie, wie die Operatormethode den vom Operanden erzeugten Wert zurückgibt, genau wie die Postfix-Inkrement- und Dekrementoperatoren (§12.8.16) und die Präfixinkrement- und Dekrementoperatoren (§12.9.6). Im Gegensatz zu C++ sollte diese Methode den Wert des Operanden nicht direkt ändern, da dies gegen die Standardsemantik des Postfix-Inkrementoperators (§12.8.16) verstößt.
Endbeispiel
15.10.3 Binäre Operatoren
Die folgenden Regeln gelten für binäre Operatordeklarationen, wobei T
der Instanztyp der Klasse oder Struktur, die die Operatordeklaration enthält, bezeichnet wird:
- Ein binärer Nicht-Schichtoperator muss zwei Parameter annehmen, von denen mindestens ein Typ oder
T
T?
einen Typ aufweisen soll und einen beliebigen Typ zurückgeben kann. - Ein Binärer
<<
oder Operator (§12.11) muss zwei Parameter annehmen, von denen der erste TypT
oder T sein soll, und das zweite, von dem typint
oderint?
, und kann einen>>
beliebigen Typ zurückgeben.
Die Signatur eines binären Operators besteht aus dem Operatortoken (+
, , , *
, /
, %
&
, |
, , >>
!=
>
>=
^
<<
==
<
oder <=
) und den Typen der beiden Parameter. -
Der Rückgabetyp und die Namen der Parameter sind nicht Teil der Signatur eines binären Operators.
Für bestimmte binäre Operatoren ist eine paarweise Deklaration erforderlich. Für jede Erklärung eines der Betreiber eines Paares muss eine übereinstimmende Erklärung des anderen Betreibers des Paares vorliegen. Zwei Operatordeklarationen stimmen überein, wenn Identitätskonvertierungen zwischen ihren Rückgabetypen und den entsprechenden Parametertypen vorhanden sind. Für die folgenden Operatoren ist eine paarweise Deklaration erforderlich:
- Operator
==
und Operator!=
- Operator
>
und Operator<
- Operator
>=
und Operator<=
15.10.4 Konvertierungsoperatoren
Eine Konvertierungsoperatordeklaration führt eine benutzerdefinierte Konvertierung (§10.5) ein, die die vordefinierten impliziten und expliziten Konvertierungen erweitert.
Eine Konvertierungsoperatordeklaration, die das implicit
Schlüsselwort enthält, führt eine benutzerdefinierte implizite Konvertierung ein. Implizite Konvertierungen können in einer Vielzahl von Situationen auftreten, z. B. Funktionsmemembeaufrufe, Umwandlungsausdrücke und Zuweisungen. Dies wird weiter in §10.2 beschrieben.
Eine Konvertierungsoperatordeklaration, die das explicit
Schlüsselwort enthält, führt eine benutzerdefinierte explizite Konvertierung ein. Explizite Konvertierungen können in Umwandlungsausdrücken auftreten und in §10.3 weiter beschrieben werden.
Ein Konvertierungsoperator konvertiert von einem Quelltyp, der durch den Parametertyp des Konvertierungsoperators angegeben ist, in einen Zieltyp, der durch den Rückgabetyp des Konvertierungsoperators angegeben wird.
Für einen bestimmten Quelltyp und Zieltyp S
T
, wenn S
oder T
nullable Werttypen sind, lassen S₀
und T₀
verweisen Sie auf ihre zugrunde liegenden Typen; andernfalls S₀
und T₀
sind gleich bzwT
. gleichS
. Eine Klasse oder Struktur darf eine Konvertierung von einem Quelltyp in einen Zieltyp S
T
nur deklarieren, wenn alle folgenden Werte zutreffen:
S₀
undT₀
sind unterschiedliche Typen.Entweder
S₀
oderT₀
ist der Instanztyp der Klasse oder Struktur, die die Operatordeklaration enthält.Weder
S₀
T₀
noch ist es ein interface_type.Ohne benutzerdefinierte Konvertierungen ist eine Konvertierung nicht von
S
zuT
oder vonT
zu .S
Für die Zwecke dieser Regeln werden alle Typparameter, die mit oder als eindeutige Typen verknüpft S
sind oder T
als eindeutige Typen gelten, die keine Vererbungsbeziehung mit anderen Typen aufweisen, und alle Einschränkungen für diese Typparameter werden ignoriert.
Beispiel: Im Folgenden:
class C<T> {...} class D<T> : C<T> { public static implicit operator C<int>(D<T> value) {...} // Ok public static implicit operator C<string>(D<T> value) {...} // Ok public static implicit operator C<T>(D<T> value) {...} // Error }
die ersten beiden Operatordeklarationen sind zulässig, da
T
bzwint
string
. , bzw. als eindeutige Typen ohne Beziehung betrachtet werden. Der dritte Operator ist jedoch ein Fehler, da es sich umC<T>
die Basisklasse vonD<T>
.Endbeispiel
Aus der zweiten Regel folgt, dass ein Umrechnungsoperator entweder in oder aus der Klasse oder dem Strukturtyp konvertiert wird, in den der Operator deklariert wird.
Beispiel: Es ist möglich, dass ein Klassen- oder Strukturtyp
C
eine Konvertierung vonC
zuint
und vonint
zuC
, aber nicht vonint
zu .bool
Endbeispiel
Es ist nicht möglich, eine vordefinierte Konvertierung direkt neu zu definieren. Daher dürfen Konvertierungsoperatoren nicht von oder in diese object
konvertiert werden, da implizite und explizite Konvertierungen bereits zwischen object
allen anderen Typen vorhanden sind. Ebenso kann weder die Quelle noch die Zieltypen einer Konvertierung ein Basistyp des anderen sein, da dann bereits eine Konvertierung vorhanden wäre. Es ist jedoch möglich, Operatoren für generische Typen zu deklarieren, die für bestimmte Typargumente Konvertierungen angeben, die bereits als vordefinierte Konvertierungen vorhanden sind.
Beispiel:
struct Convertible<T> { public static implicit operator Convertible<T>(T value) {...} public static explicit operator T(Convertible<T> value) {...} }
wenn der Typ
object
als TypargumentT
angegeben wird, deklariert der zweite Operator eine bereits vorhandene Konvertierung (eine implizite und daher auch eine explizite Konvertierung von jedem Typ in ein Typobjekt).Endbeispiel
In Fällen, in denen eine vordefinierte Konvertierung zwischen zwei Typen vorhanden ist, werden alle benutzerdefinierten Konvertierungen zwischen diesen Typen ignoriert. Speziell:
- Wenn eine vordefinierte implizite Konvertierung (§10.2) vom Typ
S
zum TypT
vorhanden ist, werden alle benutzerdefinierten Konvertierungen (implizit oder explizit) vonS
zuT
ignoriert. - Wenn eine vordefinierte explizite Konvertierung (§10.3) vom Typ
S
zum TypT
vorhanden ist, werden alle benutzerdefinierten expliziten Konvertierungen vonS
zuT
" ignoriert". Außerdem:- Wenn es sich um
S
einen Schnittstellentyp handeltT
, werden benutzerdefinierte implizite Konvertierungen vonS
zuT
"zu" ignoriert. - Andernfalls werden benutzerdefinierte implizite Konvertierungen von
S
zuT
- Wenn es sich um
Für alle Typen, aber object
die operatoren, die Convertible<T>
vom obigen Typ deklariert wurden, führen keinen Konflikt mit vordefinierten Konvertierungen.
Beispiel:
void F(int i, Convertible<int> n) { i = n; // Error i = (int)n; // User-defined explicit conversion n = i; // User-defined implicit conversion n = (Convertible<int>)i; // User-defined implicit conversion }
Bei typspezifischen
object
Konvertierungen blenden jedoch die benutzerdefinierten Konvertierungen in allen Fällen, aber einer aus:void F(object o, Convertible<object> n) { o = n; // Pre-defined boxing conversion o = (object)n; // Pre-defined boxing conversion n = o; // User-defined implicit conversion n = (Convertible<object>)o; // Pre-defined unboxing conversion }
Endbeispiel
Benutzerdefinierte Konvertierungen dürfen nicht von oder in interface_types konvertiert werden. Insbesondere stellt diese Einschränkung sicher, dass beim Konvertieren in eine interface_type keine benutzerdefinierten Transformationen auftreten und dass eine Konvertierung in eine interface_type nur erfolgreich ist, wenn die object
konvertierte konvertierte interface_type tatsächlich implementiert.
Die Signatur eines Konvertierungsoperators besteht aus dem Quelltyp und dem Zieltyp. (Dies ist die einzige Form des Mitglieds, für das der Rückgabetyp an der Signatur teilnimmt.) Die implizite oder explizite Klassifizierung eines Konvertierungsoperators ist nicht Teil der Signatur des Operators. Daher kann eine Klasse oder Struktur nicht sowohl einen impliziten als auch einen expliziten Konvertierungsoperator mit denselben Quell- und Zieltypen deklarieren.
Hinweis: Im Allgemeinen sollten benutzerdefinierte implizite Konvertierungen so konzipiert werden, dass keine Ausnahmen ausgelöst werden und niemals Informationen verloren gehen. Wenn eine benutzerdefinierte Konvertierung Ausnahmen verursachen kann (z. B. weil das Quellargument außerhalb des Zulässigen liegt) oder Verlust von Informationen (z. B. Verwerfen von Bits mit hoher Reihenfolge), sollte diese Konvertierung als explizite Konvertierung definiert werden. Endnote
Beispiel: Im folgenden Code
public struct Digit { byte value; public Digit(byte value) { if (value < 0 || value > 9) { throw new ArgumentException(); } this.value = value; } public static implicit operator byte(Digit d) => d.value; public static explicit operator Digit(byte b) => new Digit(b); }
die Konvertierung von
Digit
inbyte
ist implizit, da sie niemals Ausnahmen auslöst oder Informationen verliert, die Konvertierung vonbyte
zuDigit
ist jedoch explizit, daDigit
nur eine Teilmenge der möglichen Werte einer .byte
Endbeispiel
15.11 Instanzkonstruktoren
15.11.1 Allgemein
Ein Instanzkonstruktor ist ein Member, der die erforderlichen Aktionen zum Initialisieren einer Instanz einer Klasse implementiert. Instanzkonstruktoren werden mit constructor_declarations deklariert:
constructor_declaration
: attributes? constructor_modifier* constructor_declarator constructor_body
;
constructor_modifier
: 'public'
| 'protected'
| 'internal'
| 'private'
| 'extern'
| unsafe_modifier // unsafe code support
;
constructor_declarator
: identifier '(' parameter_list? ')' constructor_initializer?
;
constructor_initializer
: ':' 'base' '(' argument_list? ')'
| ':' 'this' '(' argument_list? ')'
;
constructor_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
Eine constructor_declaration kann einen Satz von Attributen (§22), eine der zulässigen Arten von deklarierter Barrierefreiheit (§15.3.6) und einen extern
(§15.6.8)-Modifizierer enthalten. Eine Konstruktordeklaration darf denselben Modifizierer nicht mehrmals einschließen.
Der Bezeichner einer constructor_declarator muss die Klasse benennen, in der der Instanzkonstruktor deklariert wird. Wenn ein anderer Name angegeben ist, tritt ein Kompilierungszeitfehler auf.
Die optionale parameter_list eines Instanzkonstruktors unterliegt den gleichen Regeln wie die parameter_list einer Methode (§15.6). Da der this
Modifizierer für Parameter nur für Erweiterungsmethoden (§15.6.10) gilt, darf kein Parameter im parameter_list eines Konstruktors den this
Modifizierer enthalten. Die Parameterliste definiert die Signatur (§7.6) eines Instanzkonstruktors und steuert den Prozess, bei dem die Überladungsauflösung (§12.6.4) einen bestimmten Instanzkonstruktor in einem Aufruf auswählt.
Jeder der typen, auf die im parameter_list eines Instanzkonstruktors verwiesen wird, muss mindestens so zugänglich sein wie der Konstruktor selbst (§7.5.5).
Die optionale constructor_initializer gibt einen anderen Instanzkonstruktor an, der aufgerufen werden soll, bevor die anweisungen im constructor_body dieses Instanzkonstruktors ausgeführt werden. Dies wird weiter in §15.11.2 beschrieben.
Wenn eine Konstruktordeklaration einen extern
Modifizierer enthält, wird der Konstruktor als externer Konstruktor bezeichnet. Da eine externe Konstruktordeklaration keine tatsächliche Implementierung bereitstellt, besteht die constructor_body aus einem Semikolon. Für alle anderen Konstruktoren besteht die constructor_body aus einer der beiden
- ein Block, der die Anweisungen angibt, um eine neue Instanz der Klasse zu initialisieren;
- ein Ausdruckstext, der
=>
aus einem Ausdruck und einem Semikolon besteht und einen einzelnen Ausdruck angibt, um eine neue Instanz der Klasse zu initialisieren.
Ein constructor_body , der ein Block- oder Ausdruckstext ist, entspricht exakt dem Block einer Instanzmethode mit einem void
Rückgabetyp (§15.6.11).
Instanzkonstruktoren werden nicht geerbt. Daher verfügt eine Klasse über keine anderen Instanzkonstruktoren als die tatsächlich in der Klasse deklarierten Instanzen, mit der Ausnahme, dass, wenn eine Klasse keine Instanzkonstruktordeklarationen enthält, automatisch ein Standardinstanzkonstruktor bereitgestellt wird (§15.11.5).
Instanzkonstruktoren werden von object_creation_expression s (§12.8.17.2) und über constructor_initializers aufgerufen.
15.11.2 Konstruktorinitialisierer
Alle Instanzkonstruktoren (mit Ausnahme der object
Klassenkonstruktoren) enthalten implizit einen Aufruf eines anderen Instanzkonstruktors unmittelbar vor dem constructor_body. Der implizit aufgerufene Konstruktor wird durch die constructor_initializer bestimmt:
- Ein Instanzkonstruktorinitialisierer des Formulars
base(
argument_list)
(wobei argument_list optional ist) bewirkt, dass ein Instanzkonstruktor aus der direkten Basisklasse aufgerufen wird. Dieser Konstruktor wird mit argument_list und den Überladungsauflösungsregeln von §12.6.4 ausgewählt. Der Satz von Kandidateninstanzkonstruktoren besteht aus allen barrierefreien Instanzkonstruktoren der direkten Basisklasse. Wenn dieser Satz leer ist oder ein einzelner Konstruktor der besten Instanz nicht identifiziert werden kann, tritt ein Kompilierungszeitfehler auf. - Ein Instanzkonstruktorinitialisierer des Formulars
this(
argument_list)
(wobei argument_list optional ist) ruft einen anderen Instanzkonstruktor aus derselben Klasse auf. Der Konstruktor wird mit argument_list und den Überladungsauflösungsregeln von §12.6.4 ausgewählt. Der Satz von Kandidateninstanzkonstruktoren besteht aus allen Instanzkonstruktoren, die in der Klasse selbst deklariert sind. Wenn der resultierende Satz anwendbarer Instanzkonstruktoren leer ist oder ein einzelner optimaler Instanzkonstruktor nicht identifiziert werden kann, tritt ein Kompilierungszeitfehler auf. Wenn sich eine Instanzkonstruktordeklaration über eine Kette eines oder mehrerer Konstruktorinitialisierer aufruft, tritt ein Kompilierungsfehler auf.
Wenn ein Instanzkonstruktor keinen Konstruktorinitialisierer hat, wird implizit ein Konstruktorinitialisierer des Formulars base()
bereitgestellt.
Hinweis: Eine Instanzkonstruktordeklaration des Formulars
C(...) {...}
entspricht genau dem
C(...) : base() {...}
Endnote
Der Umfang der parameter, die vom parameter_list einer Instanzkonstruktordeklaration angegeben werden, enthält den Konstruktorinitialisierer dieser Deklaration. Daher ist es einem Konstruktorinitialisierer gestattet, auf die Parameter des Konstruktors zuzugreifen.
Beispiel:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y) : base(x + y, x - y) {} }
Endbeispiel
Ein Instanzkonstruktorinitialisierer kann nicht auf die erstellte Instanz zugreifen. Daher handelt es sich um einen Kompilierzeitfehler, um in einem Argumentausdruck des Konstruktorinitialisierers darauf zu verweisen, da es sich um einen Kompilierungszeitfehler für einen Argumentausdruck handelt, um über einen simple_name auf ein Instanzmemm zu verweisen.
15.11.3 Instanzvariable Initialisierungen
Wenn ein Instanzkonstruktor keinen Konstruktorinitialisierer hat oder über einen Konstruktorinitialisierer des Formulars base(...)
verfügt, führt dieser Konstruktor implizit die initialisierungen aus, die durch die variable_initializers der in der Klasse deklarierten Instanzfelder angegeben werden. Dies entspricht einer Abfolge von Zuweisungen, die unmittelbar nach dem Eintrag zum Konstruktor und vor dem impliziten Aufruf des direkten Basisklassenkonstruktors ausgeführt werden. Die Variableninitialisierer werden in der Textreihenfolge ausgeführt, in der sie in der Klassendeklaration (§15.5.6) angezeigt werden.
15.11.4 Konstruktorausführung
Variableninitialisierer werden in Zuordnungsanweisungen umgewandelt, und diese Zuordnungsanweisungen werden vor dem Aufruf des Basisklasseninstanzkonstruktors ausgeführt. Durch diese Sortierung wird sichergestellt, dass alle Instanzfelder von ihren Variableninitialisierern initialisiert werden, bevor Anweisungen ausgeführt werden, die Zugriff auf diese Instanz haben.
Beispiel: In Anbetracht der folgenden Punkte:
class A { public A() { PrintFields(); } public virtual void PrintFields() {} } class B: A { int x = 1; int y; public B() { y = -1; } public override void PrintFields() => Console.WriteLine($"x = {x}, y = {y}"); }
Wenn eine neue
B()
InstanzB
erstellt wird, wird die folgende Ausgabe erzeugt:x = 1, y = 0
Der Wert von
x
1, da der Variable Initializer ausgeführt wird, bevor der Konstruktor der Basisklasseninstanz aufgerufen wird. Der Wert vony
0 (Standardwert einerint
) ist jedoch, da die Zuordnungy
erst ausgeführt wird, nachdem der Basisklassenkonstruktor zurückgegeben wurde. Es ist nützlich, sich instanzvariable Initialisierer und Konstruktorinitialisierer als Anweisungen vor dem constructor_body zu vorstellen. Das 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 (
base
undthis
). Das Beispiel entspricht dem unten gezeigten Code, wobei jeder Kommentar eine automatisch eingefügte Anweisung angibt (die syntax, die für die automatisch eingefügten Konstruktoraufrufe verwendet wird, ist ungültig, dient aber lediglich dazu, den Mechanismus zu veranschaulichen).class A { int x, y, count; public A() { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = 0; } public A(int n) { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = n; } } class B : A { double sqrt2; ArrayList items; int max; public B() : this(100) { B(100); // Invoke B(int) constructor items.Add("default"); } public B(int n) : base(n - 1) { sqrt2 = Math.Sqrt(2.0); // Variable initializer items = new ArrayList(100); // Variable initializer A(n - 1); // Invoke A(int) constructor max = n; } }
Endbeispiel
15.11.5 Standardkonstruktoren
Wenn eine Klasse keine Instanzkonstruktordeklarationen enthält, wird automatisch ein Standardinstanzkonstruktor bereitgestellt. Dieser Standardkonstruktor ruft einfach einen Konstruktor der direkten Basisklasse auf, als hätte er einen Konstruktorinitialisierer des Formulars base()
. Wenn die Klasse abstrakt ist, ist die deklarierte Barrierefreiheit für den Standardkonstruktor geschützt. Andernfalls ist die deklarierte Barrierefreiheit für den Standardkonstruktor öffentlich.
Hinweis: Daher ist der Standardkonstruktor immer des Formulars.
protected C(): base() {}
or
public C(): base() {}
dabei
C
handelt es sich um den Namen der Klasse.Endnote
Wenn die Überladungsauflösung keinen eindeutigen besten Kandidaten für den Initialisierer des Basisklassenkonstruktors ermitteln kann, tritt ein Kompilierungszeitfehler auf.
Beispiel: Im folgenden Code
class Message { object sender; string text; }
Ein Standardkonstruktor wird bereitgestellt, da die Klasse keine Instanzkonstruktordeklarationen enthält. Das Beispiel entspricht also genau dem
class Message { object sender; string text; public Message() : base() {} }
Endbeispiel
15.12 Statische Konstruktoren
Ein statischer Konstruktor ist ein Element, das die zum Initialisieren einer geschlossenen Klasse erforderlichen Aktionen implementiert. Statische Konstruktoren werden mit static_constructor_declaration sdeklariert:
static_constructor_declaration
: attributes? static_constructor_modifiers identifier '(' ')'
static_constructor_body
;
static_constructor_modifiers
: 'static'
| 'static' 'extern' unsafe_modifier?
| 'static' unsafe_modifier 'extern'?
| 'extern' 'static' unsafe_modifier?
| 'extern' unsafe_modifier 'static'
| unsafe_modifier 'static' 'extern'?
| unsafe_modifier 'extern' 'static'
;
static_constructor_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
Ein static_constructor_declaration kann einen Satz von Attributen (§22) und einen extern
Modifizierer (§15.6.8) enthalten.
Der Bezeichner eines static_constructor_declaration muss die Klasse benennen, in der der statische Konstruktor deklariert wird. Wenn ein anderer Name angegeben ist, tritt ein Kompilierungszeitfehler auf.
Wenn eine statische Konstruktordeklaration einen extern
Modifizierer enthält, wird der statische Konstruktor als externer statischer Konstruktor bezeichnet. Da eine externe statische Konstruktordeklaration keine tatsächliche Implementierung bereitstellt, besteht die static_constructor_body aus einem Semikolon. Für alle anderen statischen Konstruktordeklarationen besteht die static_constructor_body aus einer der beiden
- ein Block, der die auszuführenden Anweisungen angibt, um die Klasse zu initialisieren;
- ein Ausdruckstext, der
=>
aus einem Ausdruck und einem Semikolon besteht, und gibt einen einzelnen Ausdruck an, der ausgeführt werden soll, um die Klasse zu initialisieren.
Ein static_constructor_body , der ein Block - oder Ausdruckstext ist, entspricht exakt dem method_body einer statischen Methode mit einem void
Rückgabetyp (§15.6.11).
Statische Konstruktoren werden nicht geerbt und können nicht direkt aufgerufen werden.
Der statische Konstruktor für eine geschlossene Klasse wird in einer bestimmten Anwendungsdomäne höchstens einmal ausgeführt. Die Ausführung eines statischen Konstruktors wird durch die ersten der folgenden Ereignisse ausgelöst, die in einer Anwendungsdomäne auftreten:
- Eine Instanz der Klasse wird erstellt.
- Auf alle statischen Member der Klasse wird verwiesen.
Wenn eine Klasse die Main
Methode (§7.1) enthält, in der die Ausführung beginnt, wird der statische Konstruktor für diese Klasse ausgeführt, bevor die Main
Methode aufgerufen wird.
Zum Initialisieren eines neuen geschlossenen Klassentyps wird zunächst ein neuer Satz statischer Felder (§15.5.2) für diesen bestimmten geschlossenen Typ erstellt. Jedes der statischen Felder wird auf den Standardwert (§15.5.5.5) initialisiert. Als Nächstes werden die statischen Feldinitialisierer (§15.5.6.2) für diese statischen Felder ausgeführt. Schließlich wird der statische Konstruktor ausgeführt.
Beispiel: Das Beispiel
class Test { static void Main() { A.F(); B.F(); } } class A { static A() { Console.WriteLine("Init A"); } public static void F() { Console.WriteLine("A.F"); } } class B { static B() { Console.WriteLine("Init B"); } public static void F() { Console.WriteLine("B.F"); } }
muss die Ausgabe erzeugen:
Init A A.F Init B B.F
da die Ausführung des
A
statischen Konstruktors durch den AufrufA.F
ausgelöst wird und die Ausführung desB
statischen Konstruktors durch den AufrufB.F
ausgelöst wird.Endbeispiel
Es ist möglich, Zirkelabhängigkeiten zu erstellen, die es statischen Feldern mit Variableninitialisierern ermöglichen, in ihrem Standardwert zu beobachten.
Beispiel: Das Beispiel
class A { public static int X; static A() { X = B.Y + 1; } } class B { public static int Y = A.X + 1; static B() {} static void Main() { Console.WriteLine($"X = {A.X}, Y = {B.Y}"); } }
erzeugt die Ausgabe
X = 1, Y = 2
Zum Ausführen der
Main
Methode führt das System zunächst den Initialisierer fürB.Y
den statischen Konstruktor der KlasseB
aus.Y
Der Initialisierer bewirktA
, dass derstatic
Konstruktor ausgeführt wird, da auf den WertA.X
verwiesen wird. Der statische Konstruktor wiederumA
berechnet den Wert vonX
, und ruft dabei den Standardwert vonY
, der Null ist.A.X
wird daher auf 1 initialisiert. Der Prozess der AusführungA
der statischen Feldinitialisierer und des statischen Konstruktors wird dann abgeschlossen, und es wird zur Berechnung des Anfangswerts zurückgegebenY
, dessen Ergebnis 2 wird.Endbeispiel
Da der statische Konstruktor genau einmal für jeden geschlossenen konstruierten Klassentyp ausgeführt wird, ist es praktisch, Laufzeitüberprüfungen für den Typparameter zu erzwingen, der nicht zur Kompilierungszeit über Einschränkungen (§15.2.5) überprüft werden kann.
Beispiel: Der folgende Typ verwendet einen statischen Konstruktor, um zu erzwingen, dass das Typargument eine Enumeration ist:
class Gen<T> where T : struct { static Gen() { if (!typeof(T).IsEnum) { throw new ArgumentException("T must be an enum"); } } }
Endbeispiel
15.13 Finalizer
Hinweis: In einer früheren Version dieser Spezifikation wurde das, was jetzt als "Finalizer" bezeichnet wird, als "Destruktor" bezeichnet. Die Erfahrung hat gezeigt, dass der Begriff "Destruktor" Verwirrung verursachte und häufig zu falschen Erwartungen geführt hat, insbesondere für Programmierer, die C++ kennen. In C++ wird ein Destruktor auf bestimmte Weise aufgerufen, während in C# kein Finalisierer ist. Um ein bestimmtes Verhalten von C# zu erhalten, sollte eine verwendet
Dispose
werden. Endnote
Ein Finalizer ist ein Member, der die erforderlichen Aktionen zum Bereinigen einer Instanz einer Klasse implementiert. Ein Finalizer wird mithilfe eines finalizer_declaration deklariert:
finalizer_declaration
: attributes? '~' identifier '(' ')' finalizer_body
| attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
finalizer_body
| attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
finalizer_body
;
finalizer_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) ist nur im unsicheren Code (§23) verfügbar.
Ein finalizer_declaration kann einen Satz von Attributen (§22) enthalten.
Der Bezeichner einer finalizer_declarator muss die Klasse benennen, in der der Finalizer deklariert wird. Wenn ein anderer Name angegeben ist, tritt ein Kompilierungszeitfehler auf.
Wenn eine Finalizerdeklaration einen extern
Modifizierer enthält, wird der Finalizer als externer Finalizer bezeichnet. Da eine externe Finalizerdeklaration keine tatsächliche Implementierung bereitstellt, besteht die finalizer_body aus einem Semikolon. Für alle anderen Finalisierer besteht die finalizer_body aus einer der beiden
- ein Block, der die auszuführenden Anweisungen angibt, um eine Instanz der Klasse abzuschließen.
- oder ein Ausdruckstext, der
=>
aus einem Ausdruck und einem Semikolon besteht, und gibt einen einzelnen Ausdruck an, der ausgeführt werden soll, um eine Instanz der Klasse abzuschließen.
Ein finalizer_body , bei dem es sich um einen Block - oder Ausdruckstext handelt, entspricht exakt dem method_body einer Instanzmethode mit einem void
Rückgabetyp (§15.6.11).
Finalizer werden nicht geerbt. Daher hat eine Klasse keine Finalisierer außer dem, der in dieser Klasse deklariert werden kann.
Hinweis: Da ein Finalizer über keine Parameter verfügen muss, kann er nicht überladen werden, sodass eine Klasse höchstens einen Finalizer haben kann. Endnote
Finalizer werden automatisch aufgerufen und können nicht explizit aufgerufen werden. Eine Instanz ist für die Finalisierung berechtigt, wenn es nicht mehr möglich ist, dass code diese Instanz verwenden kann. Die Ausführung des Finalizers für die Instanz kann jederzeit erfolgen, nachdem die Instanz zur Fertigstellung berechtigt ist (§7.9). Wenn eine Instanz abgeschlossen ist, werden die Finalizer in der Vererbungskette dieser Instanz von den meisten abgeleiteten bis zu den am wenigsten abgeleiteten Instanzen aufgerufen. Ein Finalizer kann für jeden Thread ausgeführt werden. Weitere Erläuterungen zu den Regeln, die bestimmen, wann und wie ein Finalizer ausgeführt wird, finden Sie unter §7.9.
Beispiel: Die Ausgabe des Beispiels
class A { ~A() { Console.WriteLine("A's finalizer"); } } class B : A { ~B() { Console.WriteLine("B's finalizer"); } } class Test { static void Main() { B b = new B(); b = null; GC.Collect(); GC.WaitForPendingFinalizers(); } }
stimmt
B's finalizer A's finalizer
da Finalizer in einer Vererbungskette in der Reihenfolge aufgerufen werden, von den meisten abgeleiteten bis zu den am wenigsten abgeleiteten.
Endbeispiel
Finalizer werden implementiert, indem die virtuelle Methode Finalize
System.Object
überschrieben wird. C#-Programme dürfen diese Methode nicht außer Kraft setzen oder sie direkt aufrufen (oder außer Kraft setzen).
Beispiel: Beispiel: Das Programm
class A { override protected void Finalize() {} // Error public void F() { this.Finalize(); // Error } }
enthält zwei Fehler.
Endbeispiel
Der Compiler verhält sich so, als ob diese Methode und Außerkraftsetzungen überhaupt nicht vorhanden sind.
Beispiel: Dieses Programm:
class A { void Finalize() {} // Permitted }
ist gültig, und die angezeigte Methode blendet
System.Object
Finalize
die Methode aus.Endbeispiel
Eine Erläuterung des Verhaltens, wenn eine Ausnahme von einem Finalizer ausgelöst wird, finden Sie unter §21.4.
15.14 Iteratoren
15.14.1 Allgemein
Ein mithilfe eines Iteratorblocks (§13.3) implementiertes Funktionselement (§12.6) wird als Iterator bezeichnet.
Ein Iteratorblock kann als Textkörper eines Funktionselements verwendet werden, solange der Rückgabetyp des entsprechenden Funktionselements eine der Enumerationsschnittstellen (§15.14.2) oder eine der aufzählbaren Schnittstellen (§15.14.3) ist. Es kann als method_body, operator_body oder accessor_body auftreten, während Ereignisse, Instanzkonstruktoren, statische Konstruktoren und Finalizer nicht als Iteratoren implementiert werden dürfen.
Wenn ein Funktionselement mithilfe eines Iteratorblocks implementiert wird, handelt es sich um einen Kompilierungszeitfehler für die Parameterliste des Funktionsmememers, um beliebige in
, out
oder Parameter oder ref
einen Parameter eines ref struct
Typs anzugeben.
15.14.2 Enumeratorschnittstellen
Die Enumerationsschnittstellen sind die nicht generische Schnittstelle System.Collections.IEnumerator
und alle Instanziierungen der generischen Schnittstelle System.Collections.Generic.IEnumerator<T>
. Aus Platzgründen werden in diesem Unterclause und dessen gleichgeordneten Schnittstellen jeweils als IEnumerator
bzw IEnumerator<T>
. gleichgeordnet referenziert.
15.14.3 Aufzählbare Schnittstellen
Die aufzählbaren Schnittstellen sind die nicht generische Schnittstelle System.Collections.IEnumerable
und alle Instanziierungen der generischen Schnittstelle System.Collections.Generic.IEnumerable<T>
. Aus Platzgründen werden in diesem Unterclause und dessen gleichgeordneten Schnittstellen jeweils als IEnumerable
bzw IEnumerable<T>
. gleichgeordnet referenziert.
15.14.4 Ertragtyp
Ein Iterator erzeugt eine Abfolge von Werten, die alle denselben Typ aufweisen. Dieser Typ wird als Ertragtyp des Iterators bezeichnet.
- Der Ertragtyp eines Iterators, der zurückgibt
IEnumerator
oderIEnumerable
istobject
. - Der Ertragtyp eines Iterators, der zurückgibt
IEnumerator<T>
oderIEnumerable<T>
istT
.
15.14.5 Enumeratorobjekte
15.14.5.1 Allgemein
Wenn ein Funktionselement, das einen Enumerator-Schnittstellentyp zurückgibt, mithilfe eines Iteratorblocks implementiert wird, führt das Aufrufen des Funktionsmememers den Code nicht sofort im Iteratorblock aus. Stattdessen wird ein Enumerationsobjekt erstellt und zurückgegeben. Dieses Objekt kapselt den im Iteratorblock angegebenen Code und die Ausführung des Codes im Iteratorblock tritt auf, wenn die Methode des Enumeratorobjekts MoveNext
aufgerufen wird. Ein Enumerationsobjekt weist die folgenden Merkmale auf:
- Es implementiert
IEnumerator
undIEnumerator<T>
, woT
ist der Ertragtyp des Iterators. - Sie implementiert
System.IDisposable
. - Sie wird mit einer Kopie der Argumentwerte (falls vorhanden) initialisiert und instanzwert, der an das Funktionselement übergeben wird.
- Es verfügt über vier potenzielle Zustände, bevor, ausgeführt, angehalten und danach, und befindet sich zunächst im Vorzustand .
Ein Enumeratorobjekt ist in der Regel eine Instanz einer compilergenerierten Enumerationsklasse, die den Code im Iteratorblock kapselt und die Enumerationsschnittstellen implementiert, aber andere Implementierungsmethoden sind möglich. Wenn eine Enumerationsklasse vom Compiler generiert wird, wird diese Klasse direkt oder indirekt in der Klasse, die das Funktionselement enthält, geschachtelt und verfügt über eine private Barrierefreiheit und einen Namen, der für die Compilerverwendung reserviert ist (§6.4.3).
Ein Enumeratorobjekt kann mehr Schnittstellen implementieren als die oben angegebenen.
Die folgenden Unterclauses beschreiben das erforderliche Verhalten der MoveNext
Von Current
Dispose
einem Enumeratorobjekt bereitgestellten Implementierungen und Member der IEnumerator
SchnittstellenimplementierungenIEnumerator<T>
.
Enumeratorobjekte unterstützen die IEnumerator.Reset
Methode nicht. Wenn Sie diese Methode aufrufen, wird ein System.NotSupportedException
Fehler ausgelöst.
15.14.5.2 Die MoveNext-Methode
Die MoveNext
Methode eines Enumeratorobjekts kapselt den Code eines Iteratorblocks. Durch Aufrufen der MoveNext
Methode wird Code im Iteratorblock ausgeführt und die Current
Eigenschaft des Enumeratorobjekts entsprechend festgelegt. Die genaue Aktion, die ausgeführt MoveNext
wird, hängt vom Status des Enumeratorobjekts ab, wenn MoveNext
aufgerufen wird:
- Wenn sich der Zustand des Enumeratorobjekts vor der Enumerator-Objekt befindet, wird Folgendes angezeigt
MoveNext
:- Ändert den Zustand in "Ausgeführt".
- Initialisiert die Parameter (einschließlich
this
) des Iteratorblocks an die Argumentwerte und den Instanzwert, die beim Initialisieren des Enumerationsobjekts gespeichert wurden. - Führt den Iteratorblock vom Anfang aus aus, bis die Ausführung unterbrochen wird (wie unten beschrieben).
- Wenn der Zustand des Enumeratorobjekts ausgeführt wird, wird das Ergebnis des Aufrufens
MoveNext
nicht angegeben. - Wenn der Zustand des Enumeratorobjekts angehalten wird , wird MoveNext aufrufen:
- Ändert den Zustand in "Ausgeführt".
- Stellt die Werte aller lokalen Variablen und Parameter (einschließlich
this
) auf die Beim letzten Anhalten des Iteratorblocks gespeicherten Werte wieder her.Hinweis: Der Inhalt aller Objekte, auf die von diesen Variablen verwiesen wird, kann sich seit dem vorherigen Aufruf
MoveNext
von . Endnote - Setzt die Ausführung des Iteratorblocks unmittelbar nach der Renditerückgabe-Anweisung fort, die das Anhalten der Ausführung verursacht hat und fortgesetzt wird, bis die Ausführung unterbrochen wird (wie unten beschrieben).
- Wenn der Zustand des Enumeratorobjekts folgt, gibt das Aufrufen
MoveNext
"false" zurück.
Wenn MoveNext
der Iteratorblock ausgeführt wird, kann die Ausführung auf vier Arten unterbrochen werden: Durch eine Anweisung durch eine yield return
yield break
Anweisung, indem das Ende des Iteratorblocks auftritt und eine Ausnahme ausgelöst und aus dem Iteratorblock verteilt wird.
- Wenn eine
yield return
Anweisung gefunden wird (§9.4.4.20):- Der in der Anweisung angegebene Ausdruck wird ausgewertet, implizit in den Ertragtyp konvertiert und der
Current
Eigenschaft des Enumeratorobjekts zugewiesen. - Die Ausführung des Iteratortexts wird angehalten. Die Werte aller lokalen Variablen und Parameter (einschließlich
this
) werden gespeichert, wie die Position dieseryield return
Anweisung. Wenn sich dieyield return
Anweisung in einem odertry
mehreren Blöcken befindet, werden die zugeordneten Blöcke zurzeit nicht ausgeführt. - Der Zustand des Enumeratorobjekts wird in angehalten geändert.
- Die
MoveNext
Methode kehrt zum Aufrufer zurücktrue
, der angibt, dass die Iteration erfolgreich zum nächsten Wert erweitert wurde.
- Der in der Anweisung angegebene Ausdruck wird ausgewertet, implizit in den Ertragtyp konvertiert und der
- Wenn eine
yield break
Anweisung gefunden wird (§9.4.4.20):- Wenn sich die
yield break
Anweisung innerhalb eines oder mehrerertry
Blöcke befindet, werden die zugehörigenfinally
Blöcke ausgeführt. - Der Zustand des Enumeratorobjekts wird in "Danach" geändert.
- Die
MoveNext
Methode kehrtfalse
zum Aufrufer zurück, der angibt, dass die Iteration abgeschlossen ist.
- Wenn sich die
- Wenn das Ende des Iteratortexts gefunden wird:
- Der Zustand des Enumeratorobjekts wird in "Danach" geändert.
- Die
MoveNext
Methode kehrtfalse
zum Aufrufer zurück, der angibt, dass die Iteration abgeschlossen ist.
- Wenn eine Ausnahme ausgelöst und aus dem Iteratorblock verteilt wird:
- Die
finally
entsprechenden Blöcke im Iteratortext werden von der Ausnahmeverteilung ausgeführt. - Der Zustand des Enumeratorobjekts wird in "Danach" geändert.
- Die Ausnahmeverteilung wird weiterhin an den Aufrufer der
MoveNext
Methode weitergeleitet.
- Die
15.14.5.3 Die aktuelle Eigenschaft
Die Eigenschaft eines Enumeratorobjekts Current
wird von yield return
Anweisungen im Iteratorblock beeinflusst.
Wenn sich ein Enumerationsobjekt im angehaltenen Zustand befindet, ist der Wert des Current
Werts, der durch den vorherigen Aufruf festgelegt MoveNext
wird. Wenn sich ein Enumeratorobjekt im Vor-, Ausführungs- oder Nachstatus befindet, wird das Ergebnis des Zugriffs Current
nicht angegeben.
Für einen Iterator mit einem anderen Ertragtyp als object
, entspricht das Ergebnis des Zugriffs durch Current
die Implementierung des Enumeratorobjekts IEnumerable
dem Zugriff über Current
die Implementierung des Enumeratorobjekts und dem Umwandeln des Ergebnisses IEnumerator<T>
in object
.
15.14.5.4 Die Dispose-Methode
Die Dispose
Methode wird verwendet, um die Iteration zu bereinigen, indem das Enumerationsobjekt in den After-Zustand versetzt wird.
- Wenn der Zustand des Enumeratorobjekts vorher ist, ändert das Aufrufen
Dispose
des Zustands in "Danach". - Wenn der Zustand des Enumeratorobjekts ausgeführt wird, wird das Ergebnis des Aufrufens
Dispose
nicht angegeben. - Wenn der Zustand des Enumeratorobjekts angehalten wird, wird
Dispose
Folgendes angezeigt:- Ändert den Zustand in "Ausgeführt".
- Führt schließlich Blöcke aus, als wäre die letzte ausgeführte
yield return
Anweisung eineyield break
Anweisung. Wenn dadurch eine Ausnahme ausgelöst und aus dem Iteratortext verteilt wird, wird der Zustand des Enumerationsobjekts auf "After" festgelegt, und die Ausnahme wird an den Aufrufer derDispose
Methode weitergegeben. - Ändert den Zustand in "Danach".
- Wenn der Zustand des Enumeratorobjekts folgt, hat das Aufrufen
Dispose
keine Auswirkungen.
15.14.6 Aufzählbare Objekte
15.14.6.1 Allgemein
Wenn ein Funktionsmitglied, das einen enumerationsfähigen Schnittstellentyp zurückgibt, mithilfe eines Iteratorblocks implementiert wird, führt das Aufrufen des Funktionsmememers den Code nicht sofort im Iteratorblock aus. Stattdessen wird ein aufzählbares Objekt erstellt und zurückgegeben. Die Aufzählungsobjektmethode gibt ein Enumeratorobjekt GetEnumerator
zurück, das den im Iteratorblock angegebenen Code kapselt, und die Ausführung des Codes im Iteratorblock tritt auf, wenn die Methode des Enumeratorobjekts MoveNext
aufgerufen wird. Ein aufzählbares Objekt weist die folgenden Merkmale auf:
- Es implementiert
IEnumerable
undIEnumerable<T>
, woT
ist der Ertragtyp des Iterators. - Sie wird mit einer Kopie der Argumentwerte (falls vorhanden) initialisiert und instanzwert, der an das Funktionselement übergeben wird.
Ein aufzählbares Objekt ist in der Regel eine Instanz einer vom Compiler generierten aufzählbaren Klasse, die den Code im Iteratorblock kapselt und die aufzählbaren Schnittstellen implementiert, aber andere Implementierungsmethoden sind möglich. Wenn eine aufzählbare Klasse vom Compiler generiert wird, wird diese Klasse direkt oder indirekt in der Klasse, die das Funktionselement enthält, geschachtelt und verfügt über eine private Barrierefreiheit und einen Namen, der für die Compilerverwendung reserviert ist (§6.4.3).
Ein aufzählbares Objekt kann mehr Schnittstellen als die oben angegebenen implementieren.
Hinweis: Ein aufzählbares Objekt kann beispielsweise auch implementieren
IEnumerator
undIEnumerator<T>
ermöglichen, dass es sowohl als enumerationsfähig als auch als Enumerator dienen kann. In der Regel würde eine solche Implementierung eine eigene Instanz (zum Speichern von Zuordnungen) vom ersten Aufruf an zurückgebenGetEnumerator
. 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. Es kann nicht dieselbe Instanz zurückgeben, auch wenn der vorherige Enumerationsator bereits über das Ende der Sequenz aufgezählt wurde, da alle zukünftigen Aufrufe eines erschöpften Enumerators Ausnahmen auslösen müssen. Endnote
15.14.6.2 Die GetEnumerator-Methode
Ein aufzählbares Objekt stellt eine Implementierung der GetEnumerator
Methoden und IEnumerable
IEnumerable<T>
Schnittstellen bereit. Die beiden GetEnumerator
Methoden verwenden eine gemeinsame Implementierung, die ein verfügbares Enumerationsobjekt erwirbt und zurückgibt. Das Enumerationsobjekt wird mit den Argumentwerten und Instanzwerten initialisiert, die beim Initialisieren des enumerationsfähigen Objekts gespeichert wurden, andernfalls funktioniert das Enumerationsobjekt wie in §15.14.5 beschrieben.
15.15 Asynchrone Funktionen
15.15.1 Allgemein
Eine Methode (§15.6) oder anonyme Funktion (§12.19) mit dem async
Modifizierer wird als asynchrone Funktion bezeichnet. Im Allgemeinen wird der Begriff "async" verwendet, um jede Art von Funktion zu beschreiben, die den async
Modifizierer enthält.
Es handelt sich um einen Kompilierungszeitfehler für die Parameterliste einer asynchronen Funktion, um einen beliebigen in
Parameter out
oder ref
Parameter eines ref struct
Typs anzugeben.
Die return_type einer asynchronen Methode muss entweder void
oder ein Vorgangstyp sein. Bei einer asynchronen Methode, die einen Ergebniswert erzeugt, muss ein Vorgangstyp generisch sein. Bei einer asynchronen Methode, die keinen Ergebniswert erzeugt, darf ein Vorgangstyp nicht generisch sein. Solche Typen werden in dieser Spezifikation bzw. in dieser Spezifikation «TaskType»<T>
«TaskType»
genannt. Der aus System.Threading.Tasks.Task<TResult>
einem Aufgaben-Generator erstellte Standardbibliothekstyp System.Threading.Tasks.Task
und -typen sind Aufgabentypen sowie ein Klassen-, Struktur- oder Schnittstellentyp, der einem Aufgaben-Generator-Typ über das Attribut System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
zugeordnet ist. Solche Typen werden in dieser Spezifikation als «TaskBuilderType»<T>
und «TaskBuilderType»
. Ein Vorgangstyp kann höchstens einen Typparameter aufweisen und kann nicht in einem generischen Typ geschachtelt werden.
Eine asynchrone Methode, die einen Aufgabentyp zurückgibt, wird als task-returning bezeichnet.
Vorgangstypen können in ihrer genauen Definition variieren, aber aus der Sicht der Sprache befindet sich ein Vorgangstyp in einem der Zustände unvollständig, erfolgreich oder fehlerhaft. Eine fehlerhafte Aufgabe zeichnet eine relevante Ausnahme auf. Ein erfolgreicher «TaskType»<T>
Datensatz zeichnet ein Ergebnis des Typs T
auf. Aufgabentypen sind wartend, und Aufgaben können daher die Operanden von Await-Ausdrücken (§12.9.8) sein.
Beispiel: Der Aufgabentyp
MyTask<T>
ist dem Aufgaben-Generator-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 Vorgangs-Generator-Typ ist eine Klasse oder ein Strukturtyp, der einem bestimmten Aufgabentyp entspricht (§15.15.2). Der Aufgaben-Generator-Typ muss genau mit der deklarierten Barrierefreiheit des entsprechenden Aufgabentyps übereinstimmen.
Hinweis: Wenn der Aufgabentyp deklariert
internal
wird, muss der entsprechende Generatortyp ebenfalls deklariertinternal
und in derselben Assembly definiert werden. Wenn der Vorgangstyp in einem anderen Typ geschachtelt ist, muss der Vorgangstyp auch in diesem Typ geschachtelt sein. Endnote
Eine asynchrone Funktion hat die Möglichkeit, die Auswertung mithilfe von Await-Ausdrücken (§12.9.8) im Textkörper anzusetzen. Die Auswertung kann später an der Stelle des Anhalteausdrucks fortgesetzt werden, indem eine Fortsetzungsdelegat verwendet wird. Der Fortsetzungsdelegat ist vom Typ System.Action
, und wenn er aufgerufen wird, wird die Auswertung des Aufrufs der asynchronen Funktion vom Await-Ausdruck fortgesetzt, wo er unterbrochen wurde. Der aktuelle Aufrufer eines asynchronen Funktionsaufrufs ist der ursprüngliche Aufrufer, wenn der Funktionsaufruf nie angehalten wurde oder der letzte Aufrufer des Reaktivierungsdelegats andernfalls.
15.15.2 Muster des Aufgabentyp-Generators
Ein Aufgaben-Generator-Typ kann höchstens einen Typparameter aufweisen und kann nicht in einem generischen Typ geschachtelt werden. Ein Aufgaben-Generator-Typ muss über die folgenden Member (für nicht generische Aufgaben-Generator-Typen, SetResult
keine Parameter) mit deklarierter public
Barrierefreiheit verfügen:
class «TaskBuilderType»<T>
{
public static «TaskBuilderType»<T> Create();
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine;
public void SetStateMachine(IAsyncStateMachine stateMachine);
public void SetException(Exception exception);
public void SetResult(T result);
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine;
public «TaskType»<T> Task { get; }
}
Der Compiler generiert Code, der den «TaskBuilderType» verwendet, um die Semantik des Anhaltens und Fortsetzens der Auswertung der asynchronen Funktion zu implementieren. Der Compiler verwendet den «TaskBuilderType» wie folgt:
«TaskBuilderType».Create()
wird aufgerufen, um eine Instanz von «TaskBuilderType», die in dieser Liste benannt istbuilder
, zu erstellen.builder.Start(ref stateMachine)
wird aufgerufen, um den Generator einer compilergenerierten Zustandsautomatinstanz zuzuordnen.stateMachine
- Der Baumeister ruft
stateMachine.MoveNext()
entweder einStart()
oder nachStart()
der Rückkehr zur Weiterleitung der Zustandsmaschine auf.
- Der Baumeister ruft
- Nach
Start()
dem Zurückgeben ruft dieasync
Methode die Aufgabe aufbuilder.Task
, die von der asynchronen Methode zurückgegeben werden soll. - Jeder Aufruf führt
stateMachine.MoveNext()
den Zustandsautomaten weiter. - Wenn der Zustandsautomat erfolgreich abgeschlossen wird,
builder.SetResult()
wird er mit dem Rückgabewert der Methode (falls vorhanden) aufgerufen. - Andernfalls wird, wenn eine Ausnahme
e
im Zustandscomputer ausgelöst wird,builder.SetException(e)
aufgerufen. - Wenn der Zustandsautomat einen
await expr
Ausdruck erreicht,expr.GetAwaiter()
wird er aufgerufen. - Wenn der Awaiter implementiert
ICriticalNotifyCompletion
undIsCompleted
falsch ist, wird der Zustandsautomatbuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
aufgerufen.AwaitUnsafeOnCompleted()
sollte mit einemAction
Anruf aufgerufenawaiter.UnsafeOnCompleted(action)
werden, der aufgerufenstateMachine.MoveNext()
wird, wenn der Awaiter abgeschlossen ist.
- Andernfalls ruft der Zustandsautomat
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
auf.AwaitOnCompleted()
sollte mit einemAction
Anruf aufgerufenawaiter.OnCompleted(action)
werden, der aufgerufenstateMachine.MoveNext()
wird, wenn der Awaiter abgeschlossen ist.
SetStateMachine(IAsyncStateMachine)
kann von der compilergeneriertenIAsyncStateMachine
Implementierung aufgerufen werden, um die Instanz des Generators zu identifizieren, der einer Zustandsautomatinstanz zugeordnet ist, insbesondere für Fälle, in denen der Zustandsautomat als Werttyp implementiert wird.- Wenn der Generator aufruft
stateMachine.SetStateMachine(stateMachine)
, ruftbuilder.SetStateMachine(stateMachine)
derstateMachine
Generator die generator-Instanz auf, die zugeordnetstateMachine
ist.
- Wenn der Generator aufruft
Hinweis: Für beide
SetResult(T result)
und«TaskType»<T> Task { get; }
, muss der Parameter bzw. das Argument identitätsvertierbar sein.T
Auf diese Weise kann ein Aufgabentyp-Generator Typen wie Tupel unterstützen, wobei zwei Typen, die nicht identisch sind, identitätsverwandelt sind. Endnote
15.15.3 Auswertung einer asynchronen Funktion
Der Aufruf einer asynchronen Funktion zur Rückgabe einer Aufgabe bewirkt, dass eine Instanz des zurückgegebenen Aufgabentyps generiert wird. Dies wird als Rückgabeaufgabe der asynchronen Funktion bezeichnet. Der Vorgang befindet sich zunächst in einem unvollständigen Zustand.
Der asynchrone Funktionstext wird dann ausgewertet, bis er angehalten wird (durch Erreichen eines Await-Ausdrucks) oder beendet wird, an welchem Punkt das Steuerelement an den Aufrufer zurückgegeben wird, zusammen mit der Rückgabeaufgabe.
Wenn der Textkörper der asynchronen Funktion beendet wird, wird die Rückgabeaufgabe aus dem unvollständigen Zustand verschoben:
- Wenn der Funktionstext als Ergebnis des Erreichens einer Rückgabe-Anweisung oder des Endes des Textkörpers beendet wird, wird jeder Ergebniswert in der Rückgabeaufgabe aufgezeichnet, die in einen erfolgreichen Zustand versetzt wird.
- Wenn der Funktionstext aufgrund eines nicht abgeschlossenen
OperationCanceledException
Vorgangs beendet wird, wird die Ausnahme in der Rückgabeaufgabe aufgezeichnet, die in den abgebrochenen Zustand versetzt wird. - Wenn der Funktionstext als Ergebnis einer anderen untaktischen Ausnahme (§13.10.6) beendet wird, wird die Ausnahme in der Rückgabeaufgabe aufgezeichnet, die in einen fehlerhaften Zustand versetzt wird.
15.15.4 Auswertung einer asynchronen Funktion für "void-returning"
Wenn der Rückgabetyp der asynchronen Funktion lautetvoid
, unterscheidet sich die Auswertung von der obigen Vorgehensweise: Da keine Aufgabe zurückgegeben wird, kommuniziert die Funktion stattdessen den Abschluss und Ausnahmen mit dem Synchronisierungskontext des aktuellen Threads. Die genaue Definition des Synchronisierungskontexts ist implementierungsabhängig, aber eine Darstellung von "wo" der aktuelle Thread ausgeführt wird. Der Synchronisierungskontext wird benachrichtigt, wenn die Auswertung einer void
"-returning"-asynchronen Funktion beginnt, erfolgreich abgeschlossen wird oder bewirkt, dass eine ungenaue Ausnahme ausgelöst wird.
Dadurch kann der Kontext nachverfolgen, wie viele void
asynchrone Funktionen ausgeführt werden und wie Ausnahmen verteilt werden, die aus ihnen stammen.
ECMA C# draft specification