15 klas
15.1 Ogólne
Klasa to struktura danych, która może zawierać składowe danych (stałe i pola), składowe funkcji (metody, właściwości, zdarzenia, indeksatory, operatory, konstruktory wystąpień, finalizatory i konstruktory statyczne) oraz typy zagnieżdżone. Typy klas obsługują dziedziczenie, mechanizm, w którym klasa pochodna może rozszerzać i specjalizować klasę bazową.
15.2 Deklaracje klas
15.2.1 Ogólne
Class_declaration jest type_declaration (§14.7), który deklaruje nową klasę.
class_declaration
: attributes? class_modifier* 'partial'? 'class' identifier
type_parameter_list? class_base? type_parameter_constraints_clause*
class_body ';'?
;
Class_declaration składa się z opcjonalnego zestawu atrybutów(§22), a następnie opcjonalnego zestawu class_modifiers (§15.2.2partial
§15.2.7), a następnie słowa kluczowego class
i identyfikatora, który nazywa klasę, a następnie opcjonalną type_parameter_list (§15.2.3), a następnie opcjonalną specyfikację class_base (§15.2.4)), po którym następuje opcjonalny zestaw type_parameter_constraints_clause s (§15.2.5), a następnie class_body (§15.2.6), po którym następujeśrednik.
Deklaracja klasy nie dostarcza type_parameter_constraints_clauses, chyba że dostarcza również type_parameter_list.
Deklaracja klasy dostarczająca type_parameter_list jest deklaracją klasy ogólnej. Ponadto każda klasa zagnieżdżona wewnątrz deklaracji klasy ogólnej lub deklaracji struktury ogólnej jest samą deklaracją klasy ogólnej, ponieważ argumenty typu dla typu zawierającego należy podać w celu utworzenia skonstruowanego typu (§8.4).
Modyfikatory klas 15.2.2
15.2.2.1 Ogólne
Class_declaration może opcjonalnie zawierać sekwencję modyfikatorów klas:
class_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'abstract'
| 'sealed'
| 'static'
| unsafe_modifier // unsafe code support
;
unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).
Jest to błąd czasu kompilacji dla tego samego modyfikatora, który pojawia się wiele razy w deklaracji klasy.
Modyfikator new
jest dozwolony w klasach zagnieżdżonych. Określa, że klasa ukrywa dziedziczony składowy o tej samej nazwie, co opisano w §15.3.5. Jest to błąd czasu kompilacji, new
który modyfikator ma być wyświetlany w deklaracji klasy, która nie jest deklaracją klasy zagnieżdżonej.
Modyfikatory public
, protected
internal
, i private
kontrolują dostępność klasy . W zależności od kontekstu, w którym występuje deklaracja klasy, niektóre z tych modyfikatorów mogą nie być dozwolone (§7.5.2).
W przypadku częściowej deklaracji typu (§15.2.7) zawiera specyfikację ułatwień dostępu (za pośrednictwem public
modyfikatorów , protected
, internal
i private
), specyfikacja ta zgadza się ze wszystkimi innymi częściami, które zawierają specyfikację ułatwień dostępu. Jeśli żadna część częściowego typu nie zawiera specyfikacji ułatwień dostępu, typ otrzymuje odpowiednie domyślne ułatwienia dostępu (§7.5.2).
Modyfikatory abstract
, sealed
i static
zostały omówione w następujących podklasach.
15.2.2.2 Klasy abstrakcyjne
Modyfikator abstract
służy do wskazywania, że klasa jest niekompletna i że ma być używana tylko jako klasa bazowa. Klasa abstrakcyjna różni się od klasy nie abstrakcyjnej w następujący sposób:
- Nie można utworzyć wystąpienia klasy abstrakcyjnej bezpośrednio i jest to błąd czasu kompilacji, aby użyć
new
operatora w klasie abstrakcyjnej. Chociaż możliwe jest posiadanie zmiennych i wartości, których typy czasu kompilacji są abstrakcyjne, takie zmienne i wartości muszą byćnull
albo zawierać odwołania do wystąpień klas nie abstrakcyjnych pochodzących z typów abstrakcyjnych. - Klasa abstrakcyjna jest dozwolona (ale nie jest wymagana) do zawierania abstrakcyjnych elementów członkowskich.
- Nie można zapieczętować klasy abstrakcyjnej.
Gdy klasa nie abstrakcyjna pochodzi z klasy abstrakcyjnej, klasa nie abstrakcyjna obejmuje rzeczywiste implementacje wszystkich odziedziczonych składowych abstrakcyjnych, w związku z tym przesłaniając te abstrakcyjne składowe.
Przykład: w poniższym kodzie
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 } }
klasa
A
abstrakcyjna wprowadza metodęF
abstrakcyjną . KlasaB
wprowadza dodatkową metodęG
, ale ponieważ nie zapewnia implementacjiF
,B
również jest zadeklarowana abstrakcyjnie. PrzesłonięciaC
klasF
i zapewniają rzeczywistą implementację. Ponieważ w elemecie nie ma abstrakcyjnych elementów członkowskichC
,C
jest dozwolony (ale nie jest to wymagane), aby nie były abstrakcyjne.przykład końcowy
Jeśli co najmniej jedna część części części częściowej deklaracji typu (§15.2.7) klasy zawiera abstract
modyfikator, klasa jest abstrakcyjna. W przeciwnym razie klasa nie jest abstrakcyjna.
15.2.2.3 Klasy zapieczętowane
Modyfikator sealed
jest używany do zapobiegania wyprowadzaniu z klasy. Błąd czasu kompilacji występuje, jeśli zapieczętowana klasa jest określona jako klasa bazowa innej klasy.
Zapieczętowana klasa nie może być również klasą abstrakcyjną.
Uwaga:
sealed
Modyfikator jest używany głównie do zapobiegania niezamierzonym wyprowadzaniu, ale umożliwia również pewne optymalizacje czasu wykonywania. W szczególności, ponieważ zapieczętowana klasa nigdy nie ma żadnych klas pochodnych, można przekształcić wywołania składowych funkcji wirtualnych na zapieczętowanych wystąpieniach klas w wywołania niewirtualne. notatka końcowa
Jeśli co najmniej jedna część części części częściowej deklaracji typu (§15.2.7) klasy zawiera sealed
modyfikator, klasa jest zapieczętowana. W przeciwnym razie klasa jest niezaużytowana.
15.2.2.4 Klasy statyczne
15.2.2.4.1 Ogólne
Modyfikator static
służy do oznaczania klasy zadeklarowanej jako klasy statycznej. Klasę statyczną nie należy utworzyć wystąpienia, nie należy używać jako typu i zawierać tylko statyczne elementy członkowskie. Tylko klasa statyczna może zawierać deklaracje metod rozszerzenia (§15.6.10).
Deklaracja klasy statycznej podlega następującym ograniczeniom:
- Klasa statyczna nie zawiera
sealed
klasy aniabstract
modyfikatora. (Ponieważ jednak nie można utworzyć wystąpienia ani utworzyć wystąpienia klasy statycznej, zachowuje się tak, jakby była zapieczętowana i abstrakcyjna). - Klasa statyczna nie zawiera specyfikacji class_base (§15.2.4) i nie może jawnie określić klasy bazowej ani listy zaimplementowanych interfejsów. Klasa statyczna niejawnie dziedziczy z typu
object
. - Klasa statyczna zawiera tylko statyczne składowe (§15.3.8).
Uwaga: wszystkie stałe i typy zagnieżdżone są klasyfikowane jako statyczne elementy członkowskie. notatka końcowa
- Klasa statyczna nie ma składowych z elementami
protected
,private protected
aniprotected internal
zadeklarowanymi ułatwieniami dostępu.
Jest to błąd czasu kompilacji, który narusza dowolne z tych ograniczeń.
Klasa statyczna nie ma konstruktorów wystąpień. Nie można zadeklarować konstruktora wystąpienia w klasie statycznej, a dla klasy statycznej nie podano domyślnego konstruktora wystąpienia (§15.11.5).
Składowe klasy statycznej nie są automatycznie statyczne, a deklaracje składowe jawnie zawierają static
modyfikator (z wyjątkiem stałych i typów zagnieżdżonych). Gdy klasa jest zagnieżdżona w statycznej klasie zewnętrznej, zagnieżdżona klasa nie jest klasą statyczną, chyba że jawnie zawiera static
modyfikator.
Jeśli co najmniej jedna część części części częściowej deklaracji typu (§15.2.7) klasy zawiera static
modyfikator, klasa jest statyczna. W przeciwnym razie klasa nie jest statyczna.
15.2.2.4.2 Odwołujące się do typów klas statycznych
Namespace_or_type_name (§7.8) może odwoływać się do klasy statycznej, jeśli
- Namespace_or_type_name jest
T
w namespace_or_type_name formularzaT.I
lub - Nazwa namespace_or_type jest
T
w typeof_expression (§12.8.18) formularzatypeof(T)
.
Primary_expression (§12.8) może odwoływać się do klasy statycznej, jeśli
- Primary_expression jest
E
w member_access (§12.8.7) formularzaE.I
.
W innym kontekście jest to błąd czasu kompilacji, aby odwołać się do klasy statycznej.
Uwaga: na przykład jest to błąd klasy statycznej, która ma być używana jako klasa bazowa, typ składowy (§15.3.7) elementu członkowskiego, argument typu ogólnego lub ograniczenie parametru typu. Podobnie nie można używać klasy statycznej w typie tablicy, nowym wyrażeniu, wyrażeniu rzutowym, wyrażeniu is, wyrażeniu jako wyrażeniu
sizeof
lub wyrażeniu wartości domyślnej. notatka końcowa
15.2.3 Parametry typu
Parametr typu to prosty identyfikator, który określa symbol zastępczy argumentu typu dostarczonego w celu utworzenia skonstruowanego typu. Przez constrast argument typu (§8.4.2) jest typem, który jest zastępowany dla parametru typu podczas tworzenia skonstruowanego typu.
type_parameter_list
: '<' type_parameters '>'
;
type_parameters
: attributes? type_parameter
| type_parameters ',' attributes? type_parameter
;
type_parameter jest zdefiniowany w §8.5.
Każdy parametr typu w deklaracji klasy definiuje nazwę w przestrzeni deklaracji (§7.3) tej klasy. W związku z tym nie może mieć takiej samej nazwy jak inny parametr typu tej klasy lub składowej zadeklarowanej w tej klasie. Parametr typu nie może mieć takiej samej nazwy jak sam typ.
Dwie częściowe deklaracje typów ogólnych (w tym samym programie) przyczyniają się do tego samego niezwiązanego typu ogólnego, jeśli mają taką samą w pełni kwalifikowaną nazwę (która zawiera generic_dimension_specifier (§12.8.18) dla liczby parametrów typu) (§7.8.3). Dwie takie częściowe deklaracje typów określają taką samą nazwę dla każdego parametru typu, w kolejności.
15.2.4 Specyfikacja podstawowa klasy
15.2.4.1 Ogólne
Deklaracja klasy może zawierać specyfikację class_base, która definiuje bezpośrednią klasę bazową klasy i interfejsy (§18) bezpośrednio zaimplementowane przez klasę.
class_base
: ':' class_type
| ':' interface_type_list
| ':' class_type ',' interface_type_list
;
interface_type_list
: interface_type (',' interface_type)*
;
15.2.4.2 Klasy bazowe
Gdy class_type jest uwzględniona w class_base, określa bezpośrednią klasę bazową zadeklarowanej klasy. Jeśli deklaracja klasy innej niż częściowa nie ma class_base lub jeśli class_base wyświetla tylko typy interfejsów, przyjmuje się, że bezpośrednia klasa bazowa ma wartość object
. Gdy deklaracja klasy częściowej zawiera specyfikację klasy bazowej, ta specyfikacja klasy bazowej odwołuje się do tego samego typu, co wszystkie pozostałe części tego typu częściowego, które zawierają specyfikację klasy bazowej. Jeśli żadna część klasy częściowej nie zawiera specyfikacji klasy bazowej, klasa bazowa to object
. Klasa dziedziczy elementy członkowskie z bezpośredniej klasy bazowej, zgodnie z opisem w §15.3.4.
Przykład: w poniższym kodzie
class A {} class B : A {}
Mówi się, że klasa
A
jest bezpośrednią klasą bazowąB
klasy , iB
mówi się, że pochodzi zA
klasy . PonieważA
nie określa jawnie bezpośredniej klasy bazowej, jej bezpośrednia klasa bazowa jest niejawnieobject
.przykład końcowy
Dla skonstruowanego typu klasy, w tym typu zagnieżdżonego zadeklarowanego w deklaracji typu ogólnego (§15.3.9.7), jeśli klasa bazowa jest określona w deklaracji klasy ogólnej, klasa bazowa skonstruowanego typu jest uzyskiwana przez podstawianie, dla każdej type_parameter w deklaracji klasy bazowej, odpowiadającej type_argument typu skonstruowanego.
Przykład: biorąc pod uwagę deklaracje klasy ogólnej
class B<U,V> {...} class G<T> : B<string,T[]> {...}
klasa bazowa skonstruowanego typu
G<int>
toB<string,int[]>
.przykład końcowy
Klasa bazowa określona w deklaracji klasy może być typem klasy skonstruowanej (§8.4). Klasa bazowa nie może być parametrem typu samodzielnie (§8.5), chociaż może obejmować parametry typu, które znajdują się w zakresie.
Przykład:
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> {}
przykład końcowy
Bezpośrednia klasa bazowa typu klasy jest co najmniej tak dostępna, jak sam typ klasy (§7.5.5). Na przykład jest to błąd czasu kompilacji dla klasy publicznej pochodzącej z klasy prywatnej lub wewnętrznej.
Bezpośrednia klasa bazowa typu klasy nie może być żadnym z następujących typów: System.Array
, System.Delegate
, System.Enum
System.ValueType
lub dynamic
typu. Ponadto deklaracja klasy ogólnej nie może być używana System.Attribute
jako bezpośrednia lub pośrednia klasa bazowa (§22.2.1).
Podczas określania znaczenia bezpośredniej specyfikacji A
klasy bazowej klasy B
przyjmuje się, że bezpośrednia klasa B
bazowa klasy jest tymczasowo zakładana jako object
, co gwarantuje, że znaczenie specyfikacji klasy bazowej nie może rekursywnie zależeć od siebie.
Przykład: następujące
class X<T> { public class Y{} } class Z : X<Z.Y> {}
występuje błąd, ponieważ w specyfikacji
X<Z.Y>
klasy bazowej bezpośrednia klasaZ
bazowa klasy jest uważana za , a zatem (zgodnie z regułamiobject
) nie jest uważanaZ
za składowąY
.przykład końcowy
Klasy bazowe klasy to bezpośrednia klasa bazowa i jej klasy bazowe. Innymi słowy, zestaw klas bazowych to przechodnie zamknięcie relacji bezpośredniej klasy bazowej.
Przykład: w następujących kwestiach:
class A {...} class B<T> : A {...} class C<T> : B<IComparable<T>> {...} class D<T> : C<T[]> {...}
klasy
D<int>
bazowe toC<int[]>
, ,B<IComparable<int[]>>
A
iobject
.przykład końcowy
Z wyjątkiem klasy każda klasa object
ma dokładnie jedną bezpośrednią klasę bazową. Klasa object
nie ma bezpośredniej klasy bazowej i jest ostateczną klasą bazową wszystkich innych klas.
Jest to błąd czasu kompilacji dla klasy, która zależy od siebie. W celu tej reguły klasa zależy bezpośrednio od jej bezpośredniej klasy bazowej (jeśli istnieje) i bezpośrednio zależy od najbliższej otaczającej klasy, w której jest zagnieżdżona (jeśli istnieje). Biorąc pod uwagę tę definicję, pełny zestaw klas, od których zależy klasa, to przejściowe zamknięcie obiektu bezpośrednio zależy od relacji.
Przykład: przykład
class A : A {}
jest błędne, ponieważ klasa zależy od siebie. Podobnie przykład
class A : B {} class B : C {} class C : A {}
występuje błąd, ponieważ klasy cyklicznie zależą od siebie. Na koniec przykład
class A : B.C {} class B : A { public class C {} }
powoduje wystąpienie błędu czasu kompilacji, ponieważ element A zależy
B.C
od (bezpośredniej klasy bazowej), która zależy odB
(bezpośrednio otaczającej klasy), która zależy cyklicznie odA
klasy .przykład końcowy
Klasa nie zależy od klas, które są w niej zagnieżdżone.
Przykład: w poniższym kodzie
class A { class B : A {} }
B
zależy odA
(ponieważA
jest zarówno jej bezpośrednią klasą bazową, jak i bezpośrednio otaczającą klasą), aleA
nie zależy odB
(ponieważB
nie jest ani klasą bazową, ani otaczającą klasąA
). W związku z tym przykład jest prawidłowy.przykład końcowy
Nie można pochodzić z zapieczętowanej klasy.
Przykład: w poniższym kodzie
sealed class A {} class B : A {} // Error, cannot derive from a sealed class
Klasa
B
jest w błędzie, ponieważ próbuje pochodzić z zapieczętowanej klasyA
.przykład końcowy
15.2.4.3 Implementacje interfejsu
Specyfikacja class_base może zawierać listę typów interfejsów, w tym przypadku mówi się, że klasa implementuje dane typy interfejsów. Dla skonstruowanego typu klasy, w tym typu zagnieżdżonego zadeklarowanego w deklaracji typu ogólnego (§15.3.9.7), każdy zaimplementowany typ interfejsu jest uzyskiwany przez podstawianie, dla każdego type_parameter w danym interfejsie, odpowiadających type_argument typu skonstruowanego.
Zestaw interfejsów dla typu zadeklarowanego w wielu częściach (§15.2.7) to związek interfejsów określonych w każdej części. Określony interfejs może być nazwany tylko raz w każdej części, ale wiele części może nazwać te same interfejsy podstawowe. Istnieje tylko jedna implementacja każdego elementu członkowskiego danego interfejsu.
Przykład: w następujących kwestiach:
partial class C : IA, IB {...} partial class C : IC {...} partial class C : IA, IB {...}
zestaw interfejsów podstawowych dla klasy
C
toIA
,IB
iIC
.przykład końcowy
Zazwyczaj każda część zawiera implementację interfejsów zadeklarowanych w tej części; nie jest to jednak wymagane. Część może zapewnić implementację interfejsu zadeklarowanego w innej części.
Przykład:
partial class X { int IComparable.CompareTo(object o) {...} } partial class X : IComparable { ... }
przykład końcowy
Interfejsy podstawowe określone w deklaracji klasy mogą być konstruowane typy interfejsów (§8.4, §18.2). Interfejs podstawowy nie może być parametrem typu samodzielnie, ale może obejmować parametry typu, które znajdują się w zakresie.
Przykład: Poniższy kod ilustruje, jak klasa może implementować i rozszerzać skonstruowane typy:
class C<U, V> {} interface I1<V> {} class D : C<string, int>, I1<string> {} class E<T> : C<int, T>, I1<T> {}
przykład końcowy
Implementacje interfejsu zostały omówione bardziej szczegółowo w §18.6.
Ograniczenia parametrów typu 15.2.5
Deklaracje typów ogólnych i metod mogą opcjonalnie określać ograniczenia parametrów typu, uwzględniając type_parameter_constraints_clauses.
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' '(' ')'
;
Każda type_parameter_constraints_clause składa się z tokenu where
, po którym następuje nazwa parametru typu, a następnie dwukropek i lista ograniczeń dla tego parametru typu. Dla każdego parametru typu może znajdować się co najwyżej jedna where
klauzula, a where
klauzule można wymienić w dowolnej kolejności.
get
Podobnie jak tokeny i set
w metodzie dostępu właściwości, where
token nie jest słowem kluczowym.
Lista ograniczeń podanych w where
klauzuli może zawierać dowolny z następujących składników w następującej kolejności: jedno ograniczenie podstawowe, jedno lub więcej ograniczeń pomocniczych oraz ograniczenie konstruktora. new()
Ograniczenie podstawowe może być typem klasy, unmanaged
Typ klasy i ograniczenie typu odwołania mogą obejmować nullable_type_annotation.
Ograniczenie pomocnicze może być interface_type lub type_parameter, po którym opcjonalnie następuje nullable_type_annotation. Obecność nullable_type_annotatione* wskazuje, że argument typu może być typem odwołania dopuszczanym do wartości null, który odpowiada typowi odwołania bez wartości null, który spełnia ograniczenie.
Ograniczenie typu odwołania określa, że argument typu używany dla parametru typu jest typem referencyjnym. Wszystkie typy klas, typy interfejsów, typy delegatów, typy tablic i parametry typu znane jako typ odwołania (zgodnie z definicją poniżej) spełniają to ograniczenie.
Typ klasy, ograniczenie typu odwołania i ograniczenia pomocnicze mogą obejmować adnotację typu dopuszczalnego do wartości null. Obecność lub brak tej adnotacji dla parametru typu wskazuje oczekiwania dotyczące wartości null dla argumentu typu:
- Jeśli ograniczenie nie zawiera adnotacji typu dopuszczalnego do wartości null, argument typu powinien być typem odwołania, który nie może mieć wartości null. Kompilator może wydać ostrzeżenie, jeśli argument typu jest typem odwołania dopuszczanym do wartości null.
- Jeśli ograniczenie zawiera adnotację typu dopuszczalnego do wartości null, ograniczenie jest spełnione zarówno przez typ odwołania, który nie może mieć wartości null, jak i typ odwołania dopuszczalny do wartości null.
Wartość null argumentu typu nie musi być zgodna z wartością null parametru typu. Kompilator może wydać ostrzeżenie, jeśli wartość null parametru typu nie jest zgodna z wartością null argumentu typu.
Uwaga: Aby określić, że argument typu jest typem odwołania dopuszczanym do wartości null, nie należy dodawać adnotacji typu dopuszczających wartość null jako ograniczenia (użyj
T : class
lubT : BaseClass
), ale użyjT?
całej deklaracji ogólnej, aby wskazać odpowiedni typ odwołania dopuszczający wartość null dla argumentu typu. notatka końcowa
Adnotacja typu dopuszczana do wartości null, ?
, nie może być używana w nieprzyciągniętym argumencie typu.
W przypadku parametru T
typu, gdy argument typu jest typem C?
odwołania dopuszczanym do wartości null, wystąpienia T?
klasy są interpretowane jako C?
, a nie C??
.
Przykład: W poniższych przykładach pokazano, jak wartość null argumentu typu ma wpływ na wartość null deklaracji parametru typu:
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? } }
Gdy argument typu jest typem nienależącym do wartości null, adnotacja typu wskazuje,
?
że parametr jest odpowiednim typem dopuszczalnym wartości null. Gdy argument typu jest już typem odwołania dopuszczanym do wartości null, parametr jest tym samym typem dopuszczalnym do wartości null.przykład końcowy
Ograniczenie not null określa, że argument typu używany dla parametru typu powinien być typem wartości innej niż null lub typem odwołania bez wartości null. Argument typu, który nie jest typem wartości innej niż null lub typem referencyjnym bez wartości null, jest dozwolony, ale kompilator może wygenerować ostrzeżenie diagnostyczne.
Ograniczenie typu wartości określa, że argument typu używany dla parametru typu musi być typem wartości niepustej. Wszystkie typy struktur bez wartości null, typy wyliczenia i parametry typu, które mają ograniczenie typu wartości, spełniają to ograniczenie. Należy pamiętać, że chociaż klasyfikowany jako typ wartości, typ wartości dopuszczający wartość null (§8.3.12) nie spełnia ograniczenia typu wartości. Parametr typu o ograniczeniu typu wartości nie ma również constructor_constraint, chociaż może być używany jako argument typu dla innego parametru typu z constructor_constraint.
Uwaga:
System.Nullable<T>
typ określa ograniczenie typu wartości innej niż null dla elementuT
. W związku z tym rekursywnie skonstruowane typy formularzyT??
iNullable<Nullable<T>>
są zabronione. notatka końcowa
Ponieważ unmanaged
nie jest słowem kluczowym, w primary_constraint ograniczenie niezarządzane jest zawsze niejednoznaczne z class_type. Ze względów zgodności, jeśli wyszukiwanie nazw (§12.8.4) nazwy unmanaged
zakończy się powodzeniem class_type
, jest traktowane jako . W przeciwnym razie jest traktowana jako ograniczenie niezarządzane.
Ograniczenie typu niezarządzanego określa, że argument typu używany dla parametru typu jest typem niezarządzanym niezarządzanym typem (§8.8).
Typy wskaźników nigdy nie mogą być argumentami typu i nie spełniają żadnych ograniczeń typu, nawet niezarządzanych, mimo że są niezarządzane typy.
Jeśli ograniczenie jest typem klasy, typem interfejsu lub parametrem typu, typ ten określa minimalny "typ podstawowy", który każdy argument typu używany dla tego parametru typu obsługuje. Za każdym razem, gdy jest używany skonstruowany typ lub metoda ogólna, argument typu jest sprawdzany względem ograniczeń parametru typu w czasie kompilacji. Podany argument typu spełnia warunki opisane w §8.4.5.
Ograniczenie class_type spełnia następujące zasady:
- Typ jest typem klasy.
- Typ nie powinien mieć nazwy
sealed
. - Typ nie jest jednym z następujących typów:
System.Array
lubSystem.ValueType
. - Typ nie powinien mieć nazwy
object
. - Co najwyżej jedno ograniczenie dla danego parametru typu może być typem klasy.
Typ określony jako ograniczenie interface_type spełnia następujące zasady:
- Typ musi być typem interfejsu.
- Typ nie jest określony więcej niż raz w danej
where
klauzuli.
W obu przypadkach ograniczenie może obejmować dowolny z parametrów typu skojarzonego typu lub deklaracji metody w ramach skonstruowanego typu i może obejmować zadeklarowany typ.
Każdy typ klasy lub interfejsu określony jako ograniczenie parametru typu musi być co najmniej dostępny (§7.5.5) jako typ ogólny lub zadeklarowana metoda.
Typ określony jako ograniczenie type_parameter spełnia następujące zasady:
- Typ jest parametrem typu.
- Typ nie jest określony więcej niż raz w danej
where
klauzuli.
Ponadto nie ma cykli na wykresie zależności parametrów typu, gdzie zależność jest relacją przechodnią zdefiniowaną przez:
- Jeśli parametr
T
typu jest używany jako ograniczenie dla parametruS
typu, toS
zależy od.T
- Jeśli parametr
S
typu zależy od parametru typu iT
zależy od parametruT
U
typu,S
to zależy odU
.
Biorąc pod uwagę tę relację, jest to błąd czasu kompilacji parametru typu, który zależy od siebie (bezpośrednio lub pośrednio).
Wszelkie ograniczenia są spójne między parametrami typu zależnego. Jeśli parametr S
typu zależy od parametru T
typu, wówczas:
-
T
nie ma ograniczenia typu wartości. W przeciwnym razie jest skutecznie zapieczętowany,T
więcS
będzie zmuszony do bycia tego samego typu coT
, eliminując konieczność korzystania z dwóch parametrów typu. - Jeśli
S
ma ograniczenie typu wartości,T
nie ma ograniczenia class_type . - Jeśli ma ograniczenie class_type i
S
maA
B
A
- Jeśli
S
również zależy od parametruU
typu iU
maB
, wówczas istnieje konwersja tożsamości lub niejawna konwersja odwołania z do lub niejawna konwersja odwołania zA
doB
lub niejawna konwersja odwołania zB
doA
.
Jest to prawidłowe dla S
ograniczenia typu wartości i T
ograniczenia typu odwołania. Skutecznie ogranicza to T
typy System.Object
, System.ValueType
, System.Enum
i dowolny typ interfejsu.
Jeśli klauzula where
parametru typu zawiera ograniczenie konstruktora (które ma postać new()
), można użyć new
operatora do utworzenia wystąpień typu (§12.8.17.2). Każdy argument typu używany dla parametru typu z ograniczeniem konstruktora musi być typem wartości, klasą nie abstrakcyjną o publicznym konstruktorze bez parametrów lub parametrem typu mającym ograniczenie typu wartości lub ograniczenie konstruktora.
Jest to błąd czasu kompilacji dla type_parameter_constraints o primary_constraint lub mieć również unmanaged
Przykład: Poniżej przedstawiono przykłady ograniczeń:
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() { ... }
Poniższy przykład występuje w błędzie, ponieważ powoduje cykliczność na wykresie zależności parametrów typu:
class Circular<S,T> where S: T where T: S // Error, circularity in dependency graph { ... }
W poniższych przykładach przedstawiono dodatkowe nieprawidłowe sytuacje:
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 { ... }
przykład końcowy
Dynamiczne wymazywanie typu C
jest Cₓ
skonstruowane w następujący sposób:
- Jeśli
C
jest typemOuter.Inner
zagnieżdżonym,Cₓ
jest to typOuterₓ.Innerₓ
zagnieżdżony . - Jeśli
C
Cₓ
jest typem skonstruowanymG<A¹, ..., Aⁿ>
z argumentamiA¹, ..., Aⁿ
typu,Cₓ
jest skonstruowany typG<A¹ₓ, ..., Aⁿₓ>
. - Jeśli
C
jest typemE[]
tablicy,Cₓ
jest to typEₓ[]
tablicy . - Jeśli
C
wartość jest dynamiczna,Cₓ
wartość toobject
. -
Cₓ
W przeciwnym razie wartość toC
.
Efektywna klasa bazowa parametru T
typu jest definiowana w następujący sposób:
Pozwól R
, aby zestaw typów był taki, że:
- Dla każdego ograniczenia
T
tego jest parametr typu,R
zawiera efektywną klasę bazową. - Dla każdego ograniczenia
T
, który jest typem struktury,R
zawieraSystem.ValueType
. - Dla każdego ograniczenia
T
, które jest typem wyliczenia,R
zawieraSystem.Enum
. - Dla każdego ograniczenia
T
jest to typ delegata,R
zawiera jego dynamiczne wymazywanie. - Dla każdego ograniczenia
T
, które jest typem tablicy,R
zawieraSystem.Array
. - Dla każdego ograniczenia
T
jest to typ klasy,R
zawiera jego dynamiczne wymazywanie.
Następnie
- Jeśli
T
ma ograniczenie typu wartości, efektywną klasą bazową jestSystem.ValueType
. - W przeciwnym razie, jeśli
R
wartość jest pusta, efektywna klasa bazowa toobject
. - W przeciwnym razie efektywna klasa bazowa
T
klasy jest najbardziej obejmującym typem (§10.5.3) zestawuR
. Jeśli zestaw nie ma uwzględnionego typu, efektywna klasa bazowa klasyT
toobject
. Reguły spójności zapewniają, że istnieje najbardziej obejmujący typ.
Jeśli parametr typu jest parametrem typu metody, którego ograniczenia są dziedziczone z metody podstawowej, efektywna klasa bazowa jest obliczana po podstawieniu typu.
Te reguły zapewniają, że efektywna klasa bazowa jest zawsze class_type.
Efektywny zestaw interfejsu parametru T
typu jest definiowany w następujący sposób:
- Jeśli
T
nie ma secondary_constraints, jego efektywny zestaw interfejsu jest pusty. - Jeśli
T
ma interface_type ograniczenia, ale nie type_parameter ograniczeń, jego efektywny zestaw interfejsów jest zestawem dynamicznych wymazywania ograniczeń interface_type. - Jeśli
T
nie ma żadnych interface_type ograniczeń, ale ma type_parameter ograniczenia, jego efektywny zestaw interfejsów jest połączeniem obowiązujących zestawów interfejsów ograniczeń type_parameter. - Jeśli
T
ma zarówno ograniczenia interface_type , jak i type_parameter ograniczenia, jego efektywny zestaw interfejsów jest połączeniem zestawu dynamicznych wymazywania ograniczeń interface_type i skutecznych zestawów interfejsów ograniczeń type_parameter .
Parametr typu jest znany jako typ odwołania, jeśli ma ograniczenie typu odwołania lub jego obowiązującą klasę bazową nie object
jest lub System.ValueType
. Parametr typu jest znany jako typ odwołania nienależący do wartości null, jeśli jest znany jako typ odwołania i ma ograniczenie typu referencyjnego bez wartości null.
Wartości typu parametru typu ograniczonego mogą służyć do uzyskiwania dostępu do elementów członkowskich wystąpienia implikowanych przez ograniczenia.
Przykład: w następujących kwestiach:
interface IPrintable { void Print(); } class Printer<T> where T : IPrintable { void PrintOne(T x) => x.Print(); }
metody
IPrintable
metody można wywołać bezpośrednio,x
ponieważT
jest ograniczona do zawsze implementowaniaIPrintable
metody .przykład końcowy
Gdy częściowa deklaracja typu ogólnego zawiera ograniczenia, ograniczenia są zgodne ze wszystkimi innymi częściami, które obejmują ograniczenia. W szczególności każda część, która zawiera ograniczenia, musi mieć ograniczenia dla tego samego zestawu parametrów typu, a dla każdego parametru typu zestawy ograniczeń podstawowych, pomocniczych i konstruktorów są równoważne. Dwa zestawy ograniczeń są równoważne, jeśli zawierają te same elementy członkowskie. Jeśli żadna część częściowego typu ogólnego nie określa ograniczeń parametrów typu, parametry typu są traktowane jako nieprzeciągnięty.
Przykład:
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> { ... }
jest poprawna, ponieważ te części, które obejmują ograniczenia (pierwsze dwa) skutecznie określają ten sam zestaw ograniczeń podstawowych, pomocniczych i konstruktorów odpowiednio dla tego samego zestawu parametrów typu.
przykład końcowy
Treść klasy 15.2.6
Class_body klasy definiuje składowe tej klasy.
class_body
: '{' class_member_declaration* '}'
;
15.2.7 Deklaracje częściowe
Modyfikator partial
jest używany podczas definiowania klasy, struktury lub typu interfejsu w wielu częściach. Modyfikator partial
jest kontekstowym słowem kluczowym (§6.4.4) i ma specjalne znaczenie bezpośrednio przed jednym ze słów kluczowych class
, struct
lub interface
.
Każda część częściowej deklaracji typu zawiera partial
modyfikator i jest zadeklarowana w tej samej przestrzeni nazw lub zawiera typ co inne części. Modyfikator partial
wskazuje, że dodatkowe części deklaracji typu mogą istnieć gdzie indziej, ale istnienie takich dodatkowych części nie jest wymaganiem; ważne jest, aby jedyna deklaracja typu zawierała partial
modyfikator. Ważne jest, aby tylko jedna deklaracja typu częściowego zawierała klasę bazową lub zaimplementowane interfejsy. Jednak wszystkie deklaracje klasy bazowej lub zaimplementowane interfejsy muszą być zgodne, w tym dopuszczalność null dowolnych argumentów określonego typu.
Wszystkie części typu częściowego należy skompilować razem, tak aby części można było scalić w czasie kompilacji. Typy częściowe nie zezwalają na rozszerzenie już skompilowanych typów.
Typy zagnieżdżone można zadeklarować w wielu częściach przy użyciu partial
modyfikatora. Zazwyczaj typ zawierający jest deklarowany przy użyciu partial
, a każda część typu zagnieżdżonego jest deklarowana w innej części typu zawierającego.
Przykład: następująca klasa częściowa jest implementowana w dwóch częściach, które znajdują się w różnych jednostkach kompilacji. Pierwsza część to maszyna wygenerowana przez narzędzie do mapowania bazy danych, podczas gdy druga część jest ręcznie utworzona:
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; }
Gdy obie powyższe części są kompilowane razem, wynikowy kod zachowuje się tak, jakby klasa została napisana jako pojedyncza jednostka w następujący sposób:
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; }
przykład końcowy
Obsługa atrybutów określonych w parametrach typu lub typu różnych części deklaracji częściowej jest omawiana w §22.3.
15.3 Składowe klasy
15.3.1 Ogólne
Składowe klasy składają się z składowych wprowadzonych przez jej class_member_declarations i składowych dziedziczone z bezpośredniej klasy bazowej.
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
;
Składowe klasy są podzielone na następujące kategorie:
- Stałe, które reprezentują stałe wartości skojarzone z klasą (§15.4).
- Pola, które są zmiennymi klasy (§15.5).
- Metody, które implementują obliczenia i akcje, które mogą być wykonywane przez klasę (§15.6).
- Właściwości, które definiują nazwane cechy i akcje związane z odczytywaniem i zapisywaniem tych cech (§15.7).
- Zdarzenia definiujące powiadomienia, które mogą być generowane przez klasę (§15.8).
- Indeksatory, które umożliwiają indeksowanie wystąpień klasy w taki sam sposób (składniowo) jak tablice (§15.9).
- Operatory definiujące operatory wyrażeń, które można zastosować do wystąpień klasy (§15.10).
- Konstruktory wystąpień, które implementują akcje wymagane do zainicjowania wystąpień klasy (§15.11)
- Finalizatory, które implementują akcje do wykonania, zanim wystąpienia klasy zostaną trwale odrzucone (§15.13).
- Konstruktory statyczne, które implementują akcje wymagane do zainicjowania samej klasy (§15.12).
- Typy reprezentujące typy lokalne dla klasy (§14.7).
Class_declaration tworzy nową przestrzeń deklaracji (§7.3), a type_parameters i class_member_declaration natychmiast zawarte przez class_declaration wprowadzają nowe elementy członkowskie do tej przestrzeni deklaracji. Następujące reguły dotyczą class_member_declarations:
Konstruktory wystąpień, finalizatory i konstruktory statyczne mają taką samą nazwę jak natychmiast otaczającej klasy. Wszystkie pozostałe składowe mają nazwy, które różnią się od nazwy natychmiast otaczającej klasy.
Nazwa parametru typu w type_parameter_list deklaracji klasy różni się od nazw wszystkich innych parametrów typu w tym samym type_parameter_list i różni się od nazwy klasy i nazw wszystkich składowych klasy.
Nazwa typu różni się od nazw wszystkich nietypowych składowych zadeklarowanych w tej samej klasie. Jeżeli co najmniej dwie deklaracje typów mają taką samą w pełni kwalifikowaną nazwę, deklaracje mają
partial
modyfikator (§15.2.7), a deklaracje te łączą się w celu zdefiniowania pojedynczego typu.
Uwaga: Ponieważ w pełni kwalifikowana nazwa deklaracji typu koduje liczbę parametrów typu, dwa odrębne typy mogą mieć taką samą nazwę, o ile mają różne parametry typu. notatka końcowa
Nazwa stałej, pola, właściwości lub zdarzenia różni się od nazw wszystkich pozostałych elementów członkowskich zadeklarowanych w tej samej klasie.
Nazwa metody różni się od nazw wszystkich innych metod, które nie są deklarowane w tej samej klasie. Ponadto podpis metody (§7.6) różni się od podpisów wszystkich innych metod zadeklarowanych w tej samej klasie, a dwie metody zadeklarowane w tej samej klasie nie mają podpisów, które różnią się wyłącznie od
in
,out
iref
.Podpis konstruktora wystąpienia różni się od podpisów wszystkich innych konstruktorów wystąpień zadeklarowanych w tej samej klasie, a dwa konstruktory zadeklarowane w tej samej klasie nie mają podpisów, które różnią się wyłącznie od
ref
iout
.Podpis indeksatora różni się od podpisów wszystkich innych indeksatorów zadeklarowanych w tej samej klasie.
Podpis operatora różni się od podpisów wszystkich innych operatorów zadeklarowanych w tej samej klasie.
Dziedziczone składowe klasy (§15.3.4) nie są częścią przestrzeni deklaracji klasy.
Uwaga: W związku z tym klasa pochodna może zadeklarować składową o tej samej nazwie lub podpisie co dziedziczony element członkowski (co powoduje ukrycie dziedziczonego elementu członkowskiego). notatka końcowa
Zestaw elementów członkowskich typu zadeklarowanego w wielu częściach (§15.2.7) jest związkiem członków zadeklarowanych w każdej części. Elementy wszystkich części deklaracji typu mają ten sam obszar deklaracji (§7.3), a zakres każdego elementu członkowskiego (§7.7) rozciąga się na elementy wszystkich części. Domena ułatwień dostępu każdego elementu członkowskiego zawsze zawiera wszystkie części otaczającego typu; prywatny członek zadeklarowany w jednej części jest swobodnie dostępny z innej części. Jest to błąd czasu kompilacji, aby zadeklarować ten sam element członkowski w więcej niż jednej części typu, chyba że ten element członkowski jest typem o modyfikatorze partial
.
Przykład:
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; } }
przykład końcowy
Kolejność inicjowania pola może być znacząca w kodzie języka C#, a niektóre gwarancje są udostępniane zgodnie z definicją w §15.5.6.1. W przeciwnym razie kolejność elementów członkowskich w obrębie typu jest rzadko znacząca, ale może być znacząca w przypadku współdziałania z innymi językami i środowiskami. W takich przypadkach kolejność elementów członkowskich w ramach typu zadeklarowanego w wielu częściach jest niezdefiniowana.
15.3.2 Typ wystąpienia
Każda deklaracja klasy ma skojarzony typ wystąpienia. W przypadku deklaracji klasy ogólnej typ wystąpienia jest tworzony przez utworzenie skonstruowanego typu (§8.4) z deklaracji typu, przy czym każdy z podanych argumentów typu jest odpowiednim parametrem typu. Ponieważ typ wystąpienia używa parametrów typu, można go używać tylko wtedy, gdy parametry typu znajdują się w zakresie; oznacza to, że wewnątrz deklaracji klasy. Typ wystąpienia jest typem this
kodu napisanego wewnątrz deklaracji klasy. W przypadku klas innych niż ogólne typ wystąpienia jest po prostu zadeklarowaną klasą.
Przykład: Poniżej przedstawiono kilka deklaracji klas wraz z ich typami wystąpień:
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
przykład końcowy
15.3.3 Elementy członkowskie skonstruowanych typów
Nie dziedziczone elementy członkowskie typu skonstruowanego są uzyskiwane przez podstawianie dla każdego type_parameter w deklaracji składowej odpowiadającego type_argument typu skonstruowanego. Proces podstawienia opiera się na semantycznym znaczeniu deklaracji typów i nie jest po prostu podstawieniem tekstowym.
Przykład: biorąc pod uwagę deklarację klasy ogólnej
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) {...} }
skonstruowany typ
Gen<int[],IComparable<string>>
ma następujące elementy członkowskie: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) {...}
Typ składowej
a
w deklaracjiGen
klasy ogólnej to "dwuwymiarowa tablicaT
", więc typ składoweja
w skonstruowanym typie powyżej to "dwuwymiarowa tablica jednowymiarowa tablicyint
" lubint[,][]
.przykład końcowy
W ramach składowych funkcji wystąpienia typ this
wystąpienia to typ wystąpienia (§15.3.2) zawierającej deklarację.
Wszystkie elementy członkowskie klasy ogólnej mogą używać parametrów typu z dowolnej otaczającej klasy, bezpośrednio lub w ramach skonstruowanego typu. Gdy określony typ skonstruowany zamknięty (§8.4.3) jest używany w czasie wykonywania, każde użycie parametru typu jest zastępowane argumentem typu dostarczonym do skonstruowanego typu.
Przykład:
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 } }
przykład końcowy
15.3.4 Dziedziczenie
Klasa dziedziczy składowe swojej bezpośredniej klasy bazowej. Dziedziczenie oznacza, że klasa niejawnie zawiera wszystkie elementy członkowskie jej bezpośredniej klasy bazowej, z wyjątkiem konstruktorów wystąpień, finalizatorów i konstruktorów statycznych klasy bazowej. Niektóre ważne aspekty dziedziczenia to:
Dziedziczenie jest przechodnie. Jeśli
C
pochodzi zB
elementu iB
pochodzi zA
elementu ,C
dziedziczy elementy członkowskie zadeklarowane wB
, a także składowe zadeklarowane wA
.Klasa pochodna rozszerza swoją bezpośrednią klasę bazową. Klasa pochodna może dodawać nowe składowe do tych, które dziedziczy, ale nie może usunąć definicji dziedziczonego elementu członkowskiego.
Konstruktory wystąpień, finalizatory i konstruktory statyczne nie są dziedziczone, ale wszystkie inne elementy członkowskie są, niezależnie od zadeklarowanego ułatwień dostępu (§7.5). Jednak w zależności od zadeklarowanych ułatwień dostępu dziedziczone składowe mogą nie być dostępne w klasie pochodnej.
Klasa pochodna może ukryć (§7.7.2.3) dziedziczone składowe, deklarując nowe elementy członkowskie o tej samej nazwie lub podpisie. Jednak ukrycie dziedziczonego elementu członkowskiego nie powoduje usunięcia tego elementu członkowskiego — powoduje jedynie, że ten element członkowski jest niedostępny bezpośrednio za pośrednictwem klasy pochodnej.
Wystąpienie klasy zawiera zestaw wszystkich pól wystąpień zadeklarowanych w klasie i jej klasach bazowych, a niejawna konwersja (§10.2.8) istnieje z typu klasy pochodnej do dowolnego z jej typów klas bazowych. W związku z tym odwołanie do wystąpienia jakiejś klasy pochodnej może być traktowane jako odwołanie do wystąpienia dowolnej z jego klas bazowych.
Klasa może deklarować metody wirtualne, właściwości, indeksatory i zdarzenia, a klasy pochodne mogą zastąpić implementację tych składowych funkcji. Dzięki temu klasy mogą wykazywać zachowanie polimorficzne, w którym akcje wykonywane przez wywołanie składowe funkcji różnią się w zależności od typu czasu wykonywania wystąpienia, za pomocą którego wywoływany jest ten element członkowski funkcji.
Dziedziczone składowe typu klasy skonstruowanej są składowymi typu klasy bazowej natychmiastowej (§15.2.4.2), która jest znaleziono przez podstawianie argumentów typu skonstruowanego typu dla każdego wystąpienia odpowiednich parametrów typu w base_class_specification. Z kolei te elementy członkowskie są przekształcane przez podstawianie dla każdego type_parameter w deklaracji składowej odpowiadającego type_argumentbase_class_specification.
Przykład:
class B<U> { public U F(long index) {...} } class D<T> : B<T[]> { public T G(string s) {...} }
W powyższym kodzie skonstruowany typ
D<int>
ma nie dziedziczony element członkowski publicznyint
G(string s)
uzyskany przez podstawianie argumentuint
typu dla parametruT
typu .D<int>
ma również dziedziczony element członkowski z deklaracjiB
klasy . Ten dziedziczony element członkowski jest określany najpierw przez określenie typuB<int[]>
klasy bazowejD<int>
przez zastąpienieint
T
elementu w specyfikacjiB<T[]>
klasy bazowej . Następnie, jako argument typu naB
,int[]
jest zastępowanyU
wpublic U F(long index)
parametrze , dając dziedziczony element członkowskipublic int[] F(long index)
.przykład końcowy
15.3.5 Nowy modyfikator
Class_member_declaration może zadeklarować element członkowski o tej samej nazwie lub podpisie co dziedziczony element członkowski. W takim przypadku mówi się, że składowa klasy pochodnej ukrywa składową klasy bazowej. Zobacz §7.7.2.3 , aby uzyskać dokładną specyfikację, kiedy element członkowski ukrywa dziedziczony element członkowski.
Dziedziczony element członkowski M
jest uważany za M
i nie ma innego dziedziczonego elementu członkowskiego N, który już ukrywa M
element . Niejawne ukrywanie dziedziczonej składowej nie jest uznawane za błąd, ale kompilator wydaje ostrzeżenie, chyba że deklaracja składowej klasy pochodnej zawiera modyfikator new
, który jawnie wskazuje, że składowa pochodna jest przeznaczona do ukrycia składowej bazowej. Jeśli co najmniej jedna część deklaracji częściowej (§15.2.7) typu zagnieżdżonego zawiera new
modyfikator, żadne ostrzeżenie nie zostanie wyświetlone, jeśli typ zagnieżdżony ukrywa dostępny dziedziczony element członkowski.
new
Jeśli modyfikator jest uwzględniony w deklaracji, która nie ukrywa dostępnego dziedziczonego elementu członkowskiego, zostanie wyświetlone ostrzeżenie dotyczące tego efektu.
15.3.6 Modyfikatory dostępu
Class_member_declaration private
protected internal
Z wyjątkiem kombinacji i private protected
jest to błąd czasu kompilacji określający więcej niż jeden modyfikator dostępu.
Jeśli class_member_declaration nie zawiera żadnych modyfikatorów dostępu, przyjmuje się założenieprivate
.
15.3.7 Typy składowe
Typy używane w deklaracji elementu członkowskiego są nazywane typami składowymi tego elementu członkowskiego. Możliwe typy składowe to typ stałej, pola, właściwości, zdarzenia lub indeksatora, zwracany typ metody lub operatora oraz typy parametrów metody, indeksatora, operatora lub konstruktora wystąpienia. Typy składowe członka są co najmniej tak dostępne, jak sam członek (§7.5.5).
15.3.8 Statyczne i składowe wystąpień
Składowe klasy to statyczne elementy członkowskie lub składowe wystąpienia.
Uwaga: ogólnie rzecz biorąc, warto traktować statyczne elementy członkowskie jako należące do klas i składowych wystąpień jako należące do obiektów (wystąpień klas). notatka końcowa
Gdy pole, metoda, właściwość, zdarzenie, operator lub deklaracja konstruktora zawiera static
modyfikator, deklaruje statyczny element członkowski. Ponadto stała lub deklaracja typu niejawnie deklaruje statyczny element członkowski. Statyczne elementy członkowskie mają następujące cechy:
- Gdy statyczny element członkowski
M
jest przywołyny w member_access (§12.8.7) formularzaE.M
,E
określa typ, który ma element członkowskiM
. Jest to błąd czasu kompilacji, abyE
oznaczyć wystąpienie. - Pole statyczne w klasie innej niż ogólna identyfikuje dokładnie jedną lokalizację przechowywania. Niezależnie od tego, ile wystąpień klasy niegenerycznej jest tworzonych, istnieje tylko jedna kopia pola statycznego. Każdy odrębny typ skonstruowany zamknięty (§8.4.3) ma własny zestaw pól statycznych, niezależnie od liczby wystąpień typu zamkniętego.
- Element członkowski funkcji statycznej (metoda, właściwość, zdarzenie, operator lub konstruktor) nie działa na określonym wystąpieniu i jest to błąd czasu kompilacji, aby odwołać się do tego w takim elemencie członkowskim funkcji.
Gdy pole, metoda, właściwość, zdarzenie, indeksator, konstruktor lub deklaracja finalizatora nie zawiera modyfikatora statycznego, deklaruje element członkowski wystąpienia. (Element członkowski wystąpienia jest czasami nazywany niestacjonanym elementem członkowskim). Elementy członkowskie wystąpienia mają następujące cechy:
- Jeżeli element członkowski
M
wystąpienia jest przywołyny w member_access (§12.8.7) formularzaE.M
,E
określa wystąpienie typu, który ma element członkowskiM
. Jest to błąd czasu powiązania dla E, aby oznaczyć typ. - Każde wystąpienie klasy zawiera oddzielny zestaw wszystkich pól wystąpień klasy.
- Element członkowski funkcji wystąpienia (metoda, właściwość, indeksator, konstruktor wystąpienia lub finalizator) działa na danym wystąpieniu klasy, a do tego wystąpienia można uzyskać dostęp jako
this
(§12.8.14).
Przykład: Poniższy przykład ilustruje reguły uzyskiwania dostępu do składowych statycznych i wystąpień:
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 } }
Metoda
F
pokazuje, że w elemencie członkowskim funkcji wystąpienia można użyć simple_name (§12.8.4) w celu uzyskania dostępu zarówno do składowych wystąpień, jak i statycznych elementów członkowskich. MetodaG
pokazuje, że w składowej funkcji statycznej jest to błąd czasu kompilacji, aby uzyskać dostęp do elementu członkowskiego wystąpienia za pośrednictwem simple_name. MetodaMain
pokazuje, że w member_access (§12.8.7) członkowie wystąpień mają dostęp za pośrednictwem wystąpień, a statyczne elementy członkowskie są dostępne za pośrednictwem typów.przykład końcowy
15.3.9 Typy zagnieżdżone
15.3.9.1 Ogólne
Typ zadeklarowany w klasie lub strukturę jest nazywany typem zagnieżdżonym. Typ zadeklarowany w jednostce kompilacji lub przestrzeni nazw jest nazywany typem niezagnieżdżonym.
Przykład: W poniższym przykładzie:
class A { class B { static void F() { Console.WriteLine("A.B.F"); } } }
klasa
B
jest typem zagnieżdżonym, ponieważ jest zadeklarowana w klasieA
, a klasaA
jest typem niezagnieżdżonym, ponieważ jest zadeklarowana w jednostce kompilacji.przykład końcowy
15.3.9.2 W pełni kwalifikowana nazwa
W pełni kwalifikowana nazwa (§7.8.3) dla deklaracji S.N
typu zagnieżdżonego, gdzie S
jest w pełni kwalifikowaną nazwą deklaracji typu, która N
jest zadeklarowana i N
jest niekwalifikowaną nazwą (§7.8.2) zagnieżdżonej deklaracji typu (w tym wszelkie generic_dimension_specifier (§12.8.18)).
15.3.9.3 Zadeklarowana dostępność
Typy niezagnieżdżone mogą mieć public
lub internal
zadeklarować dostępność i domyślnie zadeklarować internal
dostępność. Zagnieżdżone typy mogą również mieć te formy zadeklarowanego ułatwień dostępu oraz co najmniej jedną dodatkową formę zadeklarowanego ułatwień dostępu w zależności od tego, czy typ zawierający jest klasą, czy strukturą:
- Zagnieżdżony typ zadeklarowany w klasie może mieć dowolny z dozwolonych rodzajów zadeklarowanych ułatwień dostępu i, podobnie jak inne składowe klasy, domyślnie zadeklarowane
private
ułatwienia dostępu. - Typ zagnieżdżony zadeklarowany w ramach struktury może mieć dowolną z trzech form zadeklarowanej dostępności (
public
,internal
lubprivate
) i, podobnie jak inne elementy członkowskie struktury, domyślnie zadeklarowaneprivate
ułatwienia dostępu.
Przykład: przykład
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 {...} } }
deklaruje prywatną klasę
Node
zagnieżdżonych .przykład końcowy
15.3.9.4 Ukrywanie
Typ zagnieżdżony może ukrywać element podstawowy (§7.7.2.2).
new
Modyfikator (§15.3.5) jest dozwolony w deklaracjach typu zagnieżdżonych, dzięki czemu można jawnie wyrazić ukrywanie.
Przykład: przykład
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(); } }
wyświetla klasę
M
zagnieżdżoną, która ukrywa metodęM
zdefiniowaną w plikuBase
.przykład końcowy
15.3.9.5 ten dostęp
Typ zagnieżdżony i jego typ zawierający nie mają specjalnej relacji w odniesieniu do this_access (§12.8.14). W szczególności w obrębie this
typu zagnieżdżonego nie można odwoływać się do elementów członkowskich wystąpienia typu zawierającego. W przypadkach, gdy typ zagnieżdżony wymaga dostępu do elementów członkowskich wystąpienia jego typu zawierającego, można zapewnić dostęp, podając this
dla wystąpienia typu zawierającego jako argument konstruktora typu zagnieżdżonego.
Przykład: poniższy przykład
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(); } }
pokazuje tę technikę. Wystąpienie klasy
C
tworzy wystąpienieNested
klasy i przekazuje własne polecenie doNested
konstruktora w celu zapewnienia późniejszego dostępu doC
elementów członkowskich wystąpienia.przykład końcowy
15.3.9.6 Dostęp do prywatnych i chronionych elementów członkowskich typu zawierającego
Typ zagnieżdżony ma dostęp do wszystkich elementów członkowskich, które są dostępne dla jego typu zawierającego, w tym elementów członkowskich typu private
zawierającego i protected
zadeklarowanych ułatwień dostępu.
Przykład: przykład
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(); }
wyświetla klasę
C
zawierającą klasęNested
zagnieżdżonych . W programieNested
metodaG
wywołuje metodęF
statyczną zdefiniowaną wC
pliku iF
ma prywatną zadeklarowaną dostępność.przykład końcowy
Typ zagnieżdżony może również uzyskiwać dostęp do chronionych elementów członkowskich zdefiniowanych w podstawowym typie zawierającym typ.
Przykład: w poniższym kodzie
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(); } }
klasa
Derived.Nested
zagnieżdżona uzyskuje dostęp do chronionej metodyF
zdefiniowanej wDerived
klasieBase
bazowej , wywołując wystąpienie klasyDerived
.przykład końcowy
15.3.9.7 Typy zagnieżdżone w klasach ogólnych
Deklaracja klasy ogólnej może zawierać deklaracje typu zagnieżdżonego. Parametry typu otaczającej klasy mogą być używane w typach zagnieżdżonych. Deklaracja typu zagnieżdżonego może zawierać dodatkowe parametry typu, które mają zastosowanie tylko do typu zagnieżdżonego.
Każda deklaracja typu zawarta w deklaracji klasy ogólnej jest niejawnie deklaracją typu ogólnego. Podczas pisania odwołania do typu zagnieżdżonego w typie ogólnym nazwa ma nazwę zawierającą skonstruowany typ, w tym argumenty typu. Jednak z wewnątrz klasy zewnętrznej typ zagnieżdżony może być używany bez kwalifikacji; typ wystąpienia klasy zewnętrznej może być używany niejawnie podczas konstruowania typu zagnieżdżonego.
Przykład: Poniżej przedstawiono trzy różne poprawne sposoby odwoływania się do skonstruowanego typu utworzonego na podstawie
Inner
; pierwsze dwa są równoważne: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 } }
przykład końcowy
Mimo że jest to zły styl programowania, parametr typu w zagnieżdżonym typie może ukryć element członkowski lub parametr typu zadeklarowany w typie zewnętrznym.
Przykład:
class Outer<T> { class Inner<T> // Valid, hides Outer's T { public T t; // Refers to Inner's T } }
przykład końcowy
15.3.10 Zastrzeżone nazwy składowych
15.3.10.1 Ogólne
Aby ułatwić implementację w czasie wykonywania w języku C#, dla każdej deklaracji składowej źródłowej, która jest właściwością, zdarzeniem lub indeksatorem, implementacja zastrzega sobie dwie sygnatury metody na podstawie rodzaju deklaracji składowej, jego nazwy i jego typu (§15.3.10.2, §15.3.3, §15.3.10.4). Jest to błąd czasu kompilacji dla programu do deklarowania elementu członkowskiego, którego podpis pasuje do podpisu zarezerwowanego przez element członkowski zadeklarowany w tym samym zakresie, nawet jeśli podstawowa implementacja w czasie wykonywania nie korzysta z tych rezerwacji.
Nazwy zastrzeżone nie wprowadzają deklaracji, w związku z czym nie uczestniczą w wyszukiwaniu elementów członkowskich. Jednak skojarzone z deklaracją sygnatury metody zarezerwowanej uczestniczą w dziedziczeniu (§15.3.4) i mogą być ukryte za pomocą new
modyfikatora (§15.3.5).
Uwaga: Rezerwacja tych nazw służy do trzech celów:
- Aby umożliwić podstawowej implementacji używanie zwykłego identyfikatora jako nazwy metody do uzyskiwania lub ustawiania dostępu do funkcji języka C#.
- Aby umożliwić współdziałanie innych języków przy użyciu zwykłego identyfikatora jako nazwy metody uzyskiwania lub ustawiania dostępu do funkcji języka C#.
- Aby zapewnić, że źródło zaakceptowane przez jeden zgodny kompilator jest akceptowany przez inny, przez zapewnienie spójności określonych nazw zarezerwowanych elementów członkowskich we wszystkich implementacjach języka C#.
notatka końcowa
Deklaracja finalizatora (§15.13) powoduje również zarezerwowanie podpisu (§15.3.10.5).
Niektóre nazwy są zarezerwowane do użycia jako nazwy metod operatora (§15.3.10.6).
15.3.10.2 Nazwy składowe zarezerwowane dla właściwości
Dla właściwości P
typu (§15.7) T
zastrzeżone są następujące podpisy:
T get_P();
void set_P(T value);
Oba podpisy są zarezerwowane, nawet jeśli właściwość jest tylko do odczytu lub tylko do zapisu.
Przykład: w poniższym kodzie
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()); } }
Klasa
A
definiuje właściwośćP
tylko do odczytu , dlatego rezerwując podpisy dlaget_P
metod i .set_P
A
klasaB
pochodzi zA
obu tych podpisów zarezerwowanych i ukrywa je. W przykładzie są generowane dane wyjściowe:123 123 456
przykład końcowy
15.3.10.3 Nazwy składowe zarezerwowane dla zdarzeń
W przypadku zdarzenia E
(§15.8) typu T
delegata zastrzeżone są następujące podpisy:
void add_E(T handler);
void remove_E(T handler);
15.3.10.4 Nazwy składowe zarezerwowane dla indeksatorów
W przypadku indeksatora (§15.9) typu T
z listą L
parametrów zastrzeżone są następujące podpisy:
T get_Item(L);
void set_Item(L, T value);
Oba podpisy są zarezerwowane, nawet jeśli indeksator jest tylko do odczytu lub tylko do zapisu.
Ponadto nazwa Item
elementu członkowskiego jest zarezerwowana.
15.3.10.5 Nazwy składowe zarezerwowane dla finalizatorów
W przypadku klasy zawierającej finalizator (§15.13) zarezerwowany jest następujący podpis:
void Finalize();
15.3.10.6 Nazwy metod zarezerwowane dla operatorów
Następujące nazwy metod są zarezerwowane. Chociaż wiele z nich ma odpowiednie operatory w tej specyfikacji, niektóre są zarezerwowane do użytku przez przyszłe wersje, podczas gdy niektóre są zarezerwowane do współdziałania z innymi językami.
Nazwa metody | C# Operator |
---|---|
op_Addition |
+ (binarny) |
op_AdditionAssignment |
(zarezerwowane) |
op_AddressOf |
(zarezerwowane) |
op_Assign |
(zarezerwowane) |
op_BitwiseAnd |
& (binarny) |
op_BitwiseAndAssignment |
(zarezerwowane) |
op_BitwiseOr |
\| |
op_BitwiseOrAssignment |
(zarezerwowane) |
op_CheckedAddition |
(zarezerwowane do użytku w przyszłości) |
op_CheckedDecrement |
(zarezerwowane do użytku w przyszłości) |
op_CheckedDivision |
(zarezerwowane do użytku w przyszłości) |
op_CheckedExplicit |
(zarezerwowane do użytku w przyszłości) |
op_CheckedIncrement |
(zarezerwowane do użytku w przyszłości) |
op_CheckedMultiply |
(zarezerwowane do użytku w przyszłości) |
op_CheckedSubtraction |
(zarezerwowane do użytku w przyszłości) |
op_CheckedUnaryNegation |
(zarezerwowane do użytku w przyszłości) |
op_Comma |
(zarezerwowane) |
op_Decrement |
-- (prefiks i postfiks) |
op_Division |
/ |
op_DivisionAssignment |
(zarezerwowane) |
op_Equality |
== |
op_ExclusiveOr |
^ |
op_ExclusiveOrAssignment |
(zarezerwowane) |
op_Explicit |
jawne (zawężenie) |
op_False |
false |
op_GreaterThan |
> |
op_GreaterThanOrEqual |
>= |
op_Implicit |
przymus niejawny (rozszerzający) |
op_Increment |
++ (prefiks i postfiks) |
op_Inequality |
!= |
op_LeftShift |
<< |
op_LeftShiftAssignment |
(zarezerwowane) |
op_LessThan |
< |
op_LessThanOrEqual |
<= |
op_LogicalAnd |
(zarezerwowane) |
op_LogicalNot |
! |
op_LogicalOr |
(zarezerwowane) |
op_MemberSelection |
(zarezerwowane) |
op_Modulus |
% |
op_ModulusAssignment |
(zarezerwowane) |
op_MultiplicationAssignment |
(zarezerwowane) |
op_Multiply |
* (binarny) |
op_OnesComplement |
~ |
op_PointerDereference |
(zarezerwowane) |
op_PointerToMemberSelection |
(zarezerwowane) |
op_RightShift |
>> |
op_RightShiftAssignment |
(zarezerwowane) |
op_SignedRightShift |
(zarezerwowane) |
op_Subtraction |
- (binarny) |
op_SubtractionAssignment |
(zarezerwowane) |
op_True |
true |
op_UnaryNegation |
- (jednoargumentowy) |
op_UnaryPlus |
+ (jednoargumentowy) |
op_UnsignedRightShift |
(zarezerwowane do użytku w przyszłości) |
op_UnsignedRightShiftAssignment |
(zarezerwowane) |
15.4 Stałe
Stała to składowa klasy, która reprezentuje wartość stałą: wartość, którą można obliczyć w czasie kompilacji. Constant_declaration wprowadza co najmniej jedną stałą danego typu.
constant_declaration
: attributes? constant_modifier* 'const' type constant_declarators ';'
;
constant_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;
Constant_declaration może zawierać zestaw atrybutów (§22), modyfikator (new
) i dowolny z dozwolonych rodzajów zadeklarowanych ułatwień dostępu (§15.3.6). Atrybuty i modyfikatory mają zastosowanie do wszystkich elementów członkowskich zadeklarowanych przez constant_declaration. Mimo że stałe są uważane za statyczne elementy członkowskie, constant_declaration ani nie wymaga ani nie zezwala na static
modyfikator. Jest to błąd dla tego samego modyfikatora, który pojawia się wielokrotnie w deklaracji stałej.
Typconstant_declaration określa typ elementów członkowskich wprowadzonych przez deklarację. Po typie następuje lista constant_declarator s (§13.6.3), z których każdywprowadza nowego członka. Constant_declarator składa się z identyfikatora, który nazywa element członkowski, po którym następuje token "=
", a następnie constant_expression (§12.23), który daje wartość elementu członkowskiego.
Typ określony w deklaracji stałej to , , sbyte
byte
short
ushort
int
uint
long
ulong
char
float
double
decimal
bool
enum_type lub string
Każda constant_expression zwraca wartość typu docelowego lub typu, który można przekonwertować na typ docelowy przez niejawną konwersję (§10.2).
Typ stałej jest co najmniej tak dostępny, jak sama stała (§7.5.5).
Wartość stałej jest uzyskiwana w wyrażeniu przy użyciu simple_name (§12.8.4) lub member_access (§12.8.7).
Stała może uczestniczyć w constant_expression. W związku z tym stała może być używana w dowolnej konstrukcji, która wymaga constant_expression.
Uwaga: Przykłady takich konstrukcji obejmują
case
etykiety,goto case
instrukcje,enum
deklaracje składowe, atrybuty i inne deklaracje stałe. notatka końcowa
Uwaga: zgodnie z opisem w §12.23 constant_expression jest wyrażeniem, które można w pełni ocenić w czasie kompilacji. Ponieważ jedynym sposobem utworzenia wartości innej
string
zastosowanienew
operatora, a ponieważnew
operator nie jest dozwolony w constant_expression, jedyną możliwą wartością dla stałych reference_types innych niżstring
jestnull
. notatka końcowa
Gdy żądana jest nazwa symboliczna dla wartości stałej, ale gdy typ tej wartości nie jest dozwolony w deklaracji stałej lub gdy nie można obliczyć wartości w czasie kompilacji przez constant_expression, można użyć pola readonly (§15.5.3).
Uwaga: semantyka wersji i
const
readonly
różnią się (§15.5.3.3). notatka końcowa
Stała deklaracja, która deklaruje wiele stałych, jest równoważna wielokrotnym deklaracjom pojedynczych stałych z tymi samymi atrybutami, modyfikatorami i typem.
Przykład:
class A { public const double X = 1.0, Y = 2.0, Z = 3.0; }
jest równoważny
class A { public const double X = 1.0; public const double Y = 2.0; public const double Z = 3.0; }
przykład końcowy
Stałe mogą zależeć od innych stałych w ramach tego samego programu, o ile zależności nie mają charakteru cyklicznego.
Przykład: w poniższym kodzie
class A { public const int X = B.Z + 1; public const int Y = 10; } class B { public const int Z = A.Y + 1; }
kompilator musi najpierw ocenić
A.Y
, a następnie ocenićB.Z
, a następnie ocenićA.X
, tworząc wartości10
,11
i12
.przykład końcowy
Deklaracje stałe mogą zależeć od stałych z innych programów, ale takie zależności są możliwe tylko w jednym kierunku.
Przykład: odwołanie się do powyższego przykładu, jeśli
A
iB
zostało zadeklarowane w oddzielnych programach, możliweA.X
byłoby, aby zależeć odB.Z
elementu , aleB.Z
nie mogłoby to być jednocześnie zależne odA.Y
. przykład końcowy
15.5 Pola
15.5.1 Ogólne
Pole to składowa reprezentująca zmienną skojarzona z obiektem lub klasą. Field_declaration wprowadza co najmniej jedno pole danego typu.
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) jest dostępny tylko w niebezpiecznym kodzie (§23).
Field_declaration może zawierać zestaw atrybutów(§22), new
modyfikator (§15.3.5), prawidłową kombinację czterech modyfikatorów dostępu (§15.3.6) i static
modyfikatora (§15.5.2). Ponadto field_declaration może zawierać readonly
modyfikator (§15.5.3) lub volatile
modyfikator (§15.5.4), ale nie oba. Atrybuty i modyfikatory mają zastosowanie do wszystkich elementów członkowskich zadeklarowanych przez field_declaration. Jest to błąd tego samego modyfikatora, który pojawia się wiele razy w field_declaration.
Typfield_declaration określa typ elementów członkowskich wprowadzonych przez deklarację. Po typie następuje lista variable_declarators, z których każdy wprowadza nowy element członkowski. Variable_declarator składa się z identyfikatora, który nazywa ten element członkowski, opcjonalnie następuje token "=
" i variable_initializer (§15.5.6), który daje początkową wartość tego elementu członkowskiego.
Typ pola jest co najmniej tak dostępny, jak samo pole (§7.5.5).
Wartość pola jest uzyskiwana w wyrażeniu przy użyciu simple_name (§12.8.4), member_access (§12.8.7) lub base_access (§12.8.15). Wartość pola nieczytanego jest modyfikowana przy użyciu przypisania (§12.21). Wartość pola nieczytanego można uzyskać i zmodyfikować przy użyciu operatorów przyrostowych i dekrementacyjnych (§12.8.16) oraz operatorów przyrostku i dekrementacji (§12.9.6).
Deklaracja pola, która deklaruje wiele pól, jest równoważna wielokrotnym deklaracjom pojedynczych pól z tymi samymi atrybutami, modyfikatorami i typem.
Przykład:
class A { public static int X = 1, Y, Z = 100; }
jest równoważny
class A { public static int X = 1; public static int Y; public static int Z = 100; }
przykład końcowy
15.5.2 Pola statyczne i wystąpienia
Gdy deklaracja pola zawiera static
modyfikator, pola wprowadzone przez deklarację to pola statyczne. Jeśli modyfikator nie static
jest obecny, pola wprowadzone przez deklarację są polami wystąpienia. Pola statyczne i pola wystąpienia to dwa z kilku rodzajów zmiennych (§9) obsługiwanych przez język C#, a czasami są nazywane zmiennymi statycznymi i zmiennymi wystąpienia, odpowiednio.
Zgodnie z opisem w §15.3.8 każde wystąpienie klasy zawiera pełny zestaw pól wystąpienia klasy, podczas gdy istnieje tylko jeden zestaw pól statycznych dla każdej klasy niegenerycznej lub typu zamkniętego, niezależnie od liczby wystąpień klasy lub typu skonstruowanego zamkniętego.
15.5.3 Pola tylko do odczytu
15.5.3.1 Ogólne
Gdy field_declaration zawiera readonly
modyfikator, pola wprowadzone przez deklarację są polami tylko do odczytu. Bezpośrednie przypisania do pól tylko do odczytu mogą występować w ramach tej deklaracji lub w konstruktorze wystąpienia lub konstruktorze statycznym w tej samej klasie. (Pole tylko do odczytu można przypisać do wielu razy w tych kontekstach). W szczególności bezpośrednie przypisania do pola readonly są dozwolone tylko w następujących kontekstach:
- W variable_declarator wprowadzającym pole (w tym variable_initializer w deklaracji).
- W polu wystąpienia w konstruktorach wystąpienia klasy zawierającej deklarację pola; dla pola statycznego w konstruktorze statycznym klasy zawierającej deklarację pola. Są to również jedyne konteksty, w których ważne jest przekazanie pola readonly jako parametru wyjściowego lub referencyjnego.
Próba przypisania do pola readonly lub przekazania go jako parametru wyjściowego lub referencyjnego w innym kontekście jest błędem czasu kompilacji.
15.5.3.2 Używanie statycznych pól readonly dla stałych
Statyczne pole readonly jest przydatne, gdy żądana jest nazwa symboliczna stałej wartości, ale gdy typ wartości nie jest dozwolony w deklaracji const lub gdy nie można obliczyć wartości w czasie kompilacji.
Przykład: w poniższym kodzie
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; } }
nie
Black
można zadeklarować elementów członkowskich ,White
,Red
Green
, iBlue
jako elementów członkowskich const, ponieważ ich wartości nie mogą być obliczane w czasie kompilacji. Jednak deklarowanie ichstatic readonly
zamiast tego ma taki sam efekt.przykład końcowy
15.5.3.3 Przechowywanie wersji stałych i statycznych pól tylko do odczytu
Stałe i pola odczytu mają różne semantyka przechowywania wersji binarnych. Gdy wyrażenie odwołuje się do stałej, wartość stałej jest uzyskiwana w czasie kompilacji, ale gdy wyrażenie odwołuje się do pola readonly, wartość pola nie jest uzyskiwana do czasu wykonywania.
Przykład: Rozważ aplikację składającą się z dwóch oddzielnych programów:
namespace Program1 { public class Utils { public static readonly int x = 1; } }
oraz
namespace Program2 { class Test { static void Main() { Console.WriteLine(Program1.Utils.X); } } }
Przestrzenie
Program1
nazw iProgram2
oznaczają dwa programy, które są kompilowane oddzielnie. PonieważProgram1.Utils.X
jest zadeklarowana jako pole, wartość wyjściowastatic readonly
instrukcjiConsole.WriteLine
nie jest znana w czasie kompilacji, ale raczej jest uzyskiwana w czasie wykonywania. W związku z tym, jeśli wartośćX
elementu zostanie zmieniona iProgram1
zostanie ponownie skompilowana, instrukcja zwróci nową wartość,Console.WriteLine
nawet jeśliProgram2
nie zostanie ponownie skompilowana. Jednak gdybyX
była stała, wartośćX
elementu zostałaby uzyskana w czasieProgram2
kompilacji i pozostałaby bez zmian doProgram1
Program2
momentu ponownego skompilowania.przykład końcowy
15.5.4 Pola nietrwałe
Gdy field_declaration zawiera modyfikator, pola wprowadzone przez deklarację sąvolatile
nietrwałym. W przypadku pól nietrwałych techniki optymalizacji, które zmieniają kolejność instrukcji, mogą prowadzić do nieoczekiwanych i nieprzewidywalnych wyników w programach wielowątkowe, które uzyskują dostęp do pól bez synchronizacji, takich jak dostarczone przez lock_statement (§13.13). Te optymalizacje mogą być wykonywane przez kompilator, przez system czasu wykonywania lub przez sprzęt. W przypadku pól nietrwałych takie optymalizacje zmiany kolejności są ograniczone:
- Odczyt pola lotnego jest nazywany odczytem lotnym. Odczyt volatile ma "uzyskiwanie semantyki"; oznacza to, że jest gwarantowane przed wszelkimi odwołaniami do pamięci, które występują po niej w sekwencji instrukcji.
- Zapis pola lotnego jest nazywany nietrwałym zapisem. Zapis volatile ma "semantyka wydania"; oznacza to, że jest to gwarantowane po wszelkich odwołaniach do pamięci przed instrukcją zapisu w sekwencji instrukcji.
Te ograniczenia zapewniają, że wszystkie wątki będą obserwować nietrwałe zapisy wykonywane przez dowolny inny wątek w kolejności, w jakiej zostały wykonane. Implementacja zgodna nie jest wymagana, aby zapewnić pojedynczą całkowitą kolejność nietrwałych zapisów, jak pokazano we wszystkich wątkach wykonywania. Typ pola lotnego musi być jednym z następujących:
- Reference_type.
- Type_parameter, który jest znany jako typ odwołania (§15.2.5).
- Typ
byte
,sbyte
short
ushort
int
uint
char
float
bool
System.IntPtr
lub .System.UIntPtr
- Enum_type
uint
Przykład: przykład
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; } } } }
generuje dane wyjściowe:
result = 143
W tym przykładzie metoda
Main
uruchamia nowy wątek, który uruchamia metodęThread2
. Ta metoda przechowuje wartość w polu nietrwałym o nazwieresult
, a następnie przechowujetrue
w polufinished
volatile . Główny wątek czeka na ustawienie polafinished
natrue
, a następnie odczytuje poleresult
. Ponieważfinished
został zadeklarowanyvolatile
, główny wątek odczytuje wartość143
z polaresult
. Jeśli polefinished
nie zostało zadeklarowanevolatile
, dopuszczalne byłoby, abyresult
magazyn był widoczny dla głównego wątku po magazynie dofinished
, a tym samym dla głównego wątku do odczytu wartości 0 z polaresult
. Deklarowaniefinished
jakovolatile
pola uniemożliwia takie niespójności.przykład końcowy
Inicjowanie pola 15.5.5
Początkowa wartość pola, niezależnie od tego, czy jest to pole statyczne, czy pole wystąpienia, jest wartością domyślną (§9.3) typu pola. Nie można obserwować wartości pola przed wystąpieniem tego domyślnego inicjowania, a pole nigdy nie jest "niezainicjowane".
Przykład: przykład
class Test { static bool b; int i; static void Main() { Test t = new Test(); Console.WriteLine($"b = {b}, i = {t.i}"); } }
generuje dane wyjściowe
b = False, i = 0
ponieważ
b
ii
są automatycznie inicjowane do wartości domyślnych.przykład końcowy
Inicjatory zmiennych 15.5.6
15.5.6.1 Ogólne
Deklaracje pól mogą zawierać variable_initializers. W przypadku pól statycznych inicjatory zmiennych odpowiadają instrukcjom przypisania wykonywanym podczas inicjowania klasy. Na przykład inicjatory zmiennych odpowiadają instrukcjom przypisania wykonywanym podczas tworzenia wystąpienia klasy.
Przykład: przykład
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}"); } }
generuje dane wyjściowe
x = 1.4142135623730951, i = 100, s = Hello
ponieważ przypisanie ma
x
miejsce, gdy inicjatory pól statycznych wykonują i przypisań doi
is
występują, gdy inicjatory pól wystąpienia są wykonywane.przykład końcowy
Inicjowanie wartości domyślnej opisane w §15.5.5 występuje dla wszystkich pól, w tym pól, które mają inicjatory zmiennych. W związku z tym po zainicjowaniu klasy wszystkie pola statyczne w tej klasie są najpierw inicjowane do ich wartości domyślnych, a następnie inicjatory pól statycznych są wykonywane w kolejności tekstowej. Podobnie po utworzeniu wystąpienia klasy wszystkie pola wystąpienia w tym wystąpieniu są najpierw inicjowane do ich wartości domyślnych, a następnie inicjatory pól wystąpienia są wykonywane w kolejności tekstowej. W przypadku deklaracji pól w wielu deklaracjach typu częściowego dla tego samego typu kolejność części jest nieokreślona. Jednak w każdej części inicjatory pól są wykonywane w kolejności.
Istnieje możliwość zaobserwowania pól statycznych z inicjatorami zmiennych w ich domyślnym stanie wartości.
Przykład: Jest to jednak zdecydowanie zniechęcone jako kwestia stylu. Przykład
class Test { static int a = b + 1; static int b = a + 1; static void Main() { Console.WriteLine($"a = {a}, b = {b}"); } }
wykazuje to zachowanie. Pomimo okrągłych
a
definicji ib
program jest prawidłowy. Powoduje to wyświetlenie danych wyjściowycha = 1, b = 2
ponieważ pola
a
statyczne ib
są inicjowane do0
(wartość domyślna dlaint
) przed ich inicjatorami są wykonywane. Gdy inicjator dlaa
przebiegów ma wartośćb
zero, a więca
jest inicjowany na1
wartość . Gdy inicjator dlab
przebiegów, wartość elementu jest już1
wartością , a więcb
jest inicjowana na wartość2
.przykład końcowy
15.5.6.2 Inicjowanie pola statycznego
Inicjatory zmiennej pola statycznego klasy odpowiadają sekwencji przypisań wykonywanych w kolejności tekstowej, w której pojawiają się w deklaracji klasy (§15.5.6.1). W klasie częściowej znaczenie "kolejności tekstowej" określa §15.5.6.1. Jeśli w klasie istnieje konstruktor statyczny (§15.12), wykonanie inicjatorów pól statycznych odbywa się bezpośrednio przed wykonaniem tego konstruktora statycznego. W przeciwnym razie inicjatory pól statycznych są wykonywane w czasie zależnym od implementacji przed pierwszym użyciem pola statycznego tej klasy.
Przykład: przykład
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"); }
może spowodować wygenerowanie danych wyjściowych:
Init A Init B 1 1
lub dane wyjściowe:
Init B Init A 1 1
ponieważ wykonanie
X
inicjatora iY
inicjatora może wystąpić w każdej kolejności; są one ograniczone tylko przed odwołaniami do tych pól. Jednak w przykładzie: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"); }
dane wyjściowe to:
Init B Init A 1 1
ponieważ reguły dotyczące wykonywania konstruktorów statycznych (zgodnie z definicją w §15.12) zapewniają, że
B
konstruktor statyczny (i dlategoB
inicjatory pól statycznych) będą uruchamiane przedA
inicjatorami statycznymi i inicjatorami pól.przykład końcowy
Inicjowanie pola wystąpienia 15.5.6.3
Inicjatory zmiennej pola wystąpienia klasy odpowiadają sekwencji przypisań wykonywanych natychmiast po wpisie do dowolnego konstruktora wystąpienia (§15.11.3) tej klasy. W klasie częściowej znaczenie "kolejności tekstowej" określa §15.5.6.1. Inicjatory zmiennych są wykonywane w kolejności tekstowej, w której są wyświetlane w deklaracji klasy (§15.5.6.1). Proces tworzenia i inicjowania wystąpienia klasy jest opisany dalej w §15.11.
Inicjator zmiennej dla pola wystąpienia nie może odwoływać się do tworzonego wystąpienia. W związku z tym jest to błąd czasu kompilacji, do którego należy odwołanie this
w inicjatorze zmiennej, ponieważ jest to błąd czasu kompilacji dla inicjatora zmiennej w celu odwołowania się do dowolnego elementu członkowskiego wystąpienia za pośrednictwem simple_name.
Przykład: w poniższym kodzie
class A { int x = 1; int y = x + 1; // Error, reference to instance member of this }
inicjator zmiennej dla
y
wyników błędu czasu kompilacji, ponieważ odwołuje się do elementu członkowskiego tworzonego wystąpienia.przykład końcowy
15.6 Metody
15.6.1 Ogólne
Metoda jest elementem członkowskim, który implementuje obliczenia lub akcję, która może być wykonywana przez obiekt lub klasę. Metody są deklarowane przy użyciu method_declarations:
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 ';'
| ';'
;
Uwagi gramatyczne:
- unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).
- w przypadku uznania method_body, jeśli stosowane są zarówno null_conditional_invocation_expression, jak i alternatywy wyrażeń, wówczas należy wybrać pierwszy.
Uwaga: Nakładanie się między nimi i priorytet między nimi jest przeznaczone wyłącznie dla wygody opisowej; reguły gramatyczne można opracować, aby usunąć nakładające się. ANTLR i inne systemy gramatyczne przyjmują tę samą wygodę i dlatego method_body ma określone semantyki automatycznie. notatka końcowa
Method_declaration może zawierać zestaw atrybutów (§22) i jeden z dozwolonych rodzajów zadeklarowanych ułatwień dostępu (§15.3.6), new
(§15.3.5), (§15.6.3), static
(§15.3). virtual
6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7), extern
(§15.6.8) i async
(§15.15) modyfikatory.
Deklaracja ma prawidłową kombinację modyfikatorów, jeśli wszystkie następujące elementy są prawdziwe:
- Deklaracja zawiera prawidłową kombinację modyfikatorów dostępu (§15.3.6).
- Deklaracja nie zawiera tego samego modyfikatora wiele razy.
- Deklaracja zawiera co najwyżej jeden z następujących modyfikatorów:
static
,virtual
ioverride
. - Deklaracja zawiera co najwyżej jeden z następujących modyfikatorów:
new
ioverride
. - Jeśli deklaracja zawiera
abstract
modyfikator, deklaracja nie zawiera żadnego z następujących modyfikatorów:static
, ,virtual
sealed
lubextern
. - Jeśli deklaracja zawiera
private
modyfikator, deklaracja nie zawiera żadnego z następujących modyfikatorów:virtual
, luboverride
abstract
. - Jeśli deklaracja zawiera
sealed
modyfikator, deklaracja zawieraoverride
również modyfikator. - Jeśli deklaracja zawiera
partial
modyfikator, nie zawiera żadnego z następujących modyfikatorów:new
, , ,public
protected
internal
private
virtual
sealed
override
abstract
lub .extern
Metody są klasyfikowane zgodnie z tym, co, jeśli w ogóle, zwracają:
- Jeśli
ref
jest obecny, metoda jest zwracana przez ref i zwraca odwołanie do zmiennej, czyli opcjonalnie tylko do odczytu; - W przeciwnym razie, jeśli return_type to
void
, metoda zwraca wartość bez wartości i nie zwraca wartości; - W przeciwnym razie metoda zwraca wartość by-value i zwraca wartość.
Return_type deklaracji metody return-by-value lub return-no-value określa typ wyniku, jeśli istnieje, zwracany przez metodę. Tylko metoda zwracana bez wartości może zawierać partial
modyfikator (§15.6.9). Jeśli deklaracja zawiera async
modyfikator, return_type musi być void
lub metoda zwracana przez wartość, a zwracany typ jest typem zadania (§15.15.1).
Ref_return_type deklaracji metody return-by-ref określa typ zmiennej przywoływanej przez variable_reference zwracaną przez metodę.
Metoda ogólna to metoda, której deklaracja zawiera type_parameter_list. Określa parametry typu dla metody . Opcjonalne type_parameter_constraints_clauseokreślają ograniczenia dla parametrów typu.
Ogólny method_declaration dla implementacji jawnego elementu członkowskiego interfejsu nie ma żadnych type_parameter_constraints_clauses; deklaracja dziedziczy wszelkie ograniczenia z ograniczeń metody interfejsu.
Podobnie deklaracja metody z override
modyfikatorem nie ma żadnych type_parameter_constraints_clauses, a ograniczenia parametrów typu metody są dziedziczone z zastępowania metody wirtualnej.
Member_name określa nazwę metody. Chyba że metoda jest jawną implementacją składową interfejsu (§18.6.2), member_name jest po prostu identyfikatorem.
W przypadku jawnej implementacji składowej interfejsu member_name składa się z interface_type , po którym następuje ".
" i identyfikator. W takim przypadku deklaracja nie zawiera żadnych modyfikatorów innych niż (ewentualnie) extern
ani async
.
Opcjonalny parameter_list określa parametry metody (§15.6.2).
Method_body metody zwracanej po wartości lub zwracanej wartości jest średnikiem, treścią bloku lub treścią wyrażenia. Treść bloku składa się z bloku, który określa instrukcje do wykonania po wywołaniu metody. Treść wyrażenia składa się z =>
elementu , po którym następuje null_conditional_invocation_expression lub wyrażenie oraz średnik, i oznacza pojedyncze wyrażenie do wykonania podczas wywoływanej metody.
W przypadku metod abstrakcyjnych i extern method_body składa się po prostu ze średnika. W przypadku metod częściowych method_body może składać się z średnika, treści bloku lub treści wyrażenia. W przypadku wszystkich innych metod method_body jest treścią bloku lub treścią wyrażenia.
Jeżeli method_body składa się z średnika, deklaracja nie zawiera async
modyfikatora.
Ref_method_body metody zwracanej przez ref jest średnikiem, treścią bloku lub treścią wyrażenia. Treść bloku składa się z bloku, który określa instrukcje do wykonania po wywołaniu metody. Treść wyrażenia składa się z =>
, a następnie ref
, variable_reference i średnika, i określa jeden variable_reference do oceny, gdy metoda jest wywoływana.
W przypadku metod abstrakcyjnych i ekstern ref_method_body składa się po prostu ze średnika; dla wszystkich innych metod ref_method_body jest treścią bloku lub treścią wyrażenia.
Nazwa, liczba parametrów typu i lista parametrów metody definiuje podpis (§7.6) metody. W szczególności podpis metody składa się z jej nazwy, liczby parametrów typu i liczby, parameter_mode_modifier s (§15.6.2.1) i typówjej parametrów. Zwracany typ nie jest częścią podpisu metody ani nazwy parametrów, nazw parametrów typu lub ograniczeń. Gdy typ parametru odwołuje się do parametru typu metody, pozycja porządkowa parametru typu (a nie nazwa parametru typu) jest używana do równoważności typu.
Nazwa metody różni się od nazw wszystkich innych metod, które nie są deklarowane w tej samej klasie. Ponadto podpis metody różni się od podpisów wszystkich innych metod zadeklarowanych w tej samej klasie, a dwie metody zadeklarowane w tej samej klasie nie mają podpisów, które różnią się wyłącznie wartościami in
, out
i ref
.
Type_parameter metody znajdują się w zakresie method_declaration i mogą służyć do tworzenia typów w tym zakresie w return_type lub ref_return_type, method_body lub ref_method_body, a type_parameter_constraints_clause, ale nie w atrybutach.
Wszystkie parametry i parametry typu mają różne nazwy.
Parametry metody 15.6.2
15.6.2.1 Ogólne
Parametry metody , jeśli istnieją, są deklarowane przez parameter_list metody.
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
;
Lista parametrów składa się z co najmniej jednego parametru rozdzielanego przecinkami, z których tylko ostatni może być parameter_array.
Fixed_parameter składa się z opcjonalnego zestawu atrybutówin
, out
, ref
lub this
modyfikatora; typu, identyfikatora i opcjonalnego default_argument. Każdy fixed_parameter deklaruje parametr danego typu o podanej nazwie.
this
Modyfikator wyznacza metodę jako metodę rozszerzenia i jest dozwolony tylko dla pierwszego parametru metody statycznej w klasie statycznej innej niż ogólna, nienagnieżdżona. Jeśli parametr jest typem struct
lub parametrem typu ograniczonym do struct
klasy , this
modyfikator może zostać połączony z ref
modyfikatorem lub in
, ale nie modyfikatorem out
. Metody rozszerzeń zostały szczegółowo opisane w §15.6.10. Fixed_parameter z default_argument jest znany jako parametr opcjonalny, natomiast fixed_parameter bez default_argument jest wymaganym parametrem. Wymagany parametr nie pojawia się po opcjonalnym parametrze w parameter_list.
Parametr z modyfikatorem ref
out
lub this
nie może mieć default_argument. Parametr wejściowy może mieć default_argument. Wyrażenie w default_argument jest jednym z następujących:
- constant_expression
- wyrażenie formularza
new S()
, w którymS
jest typem wartości - wyrażenie formularza
default(S)
, w którymS
jest typem wartości
Wyrażenie jest niejawnie konwertowane przez tożsamość lub konwersję dopuszczaną do wartości null do typu parametru.
Jeśli parametry opcjonalne występują w implementacji deklaracji metody częściowej (§15.6.9), jawna implementacja elementu członkowskiego interfejsu (§18.6.2), deklaracja indeksatora z jednym parametrem (§1 15.9) lub w deklaracji operatora (§15.10.1) kompilator powinien dać ostrzeżenie, ponieważ te elementy członkowskie nigdy nie mogą być wywoływane w sposób umożliwiający pominięcie argumentów.
Parameter_array składa się z opcjonalnego zestawu atrybutów (§22), params
modyfikatora, array_type i identyfikatora. Tablica parametrów deklaruje pojedynczy parametr danego typu tablicy o podanej nazwie. Array_type tablicy parametrów jest jednowymiarowym typem tablicy (§17.2). W wywołaniu metody tablica parametrów zezwala na określenie pojedynczego argumentu danego typu tablicy lub dopuszcza zero lub więcej argumentów typu elementu tablicy. Tablice parametrów są opisane dalej w §15.6.2.4.
Parameter_array może wystąpić po opcjonalnym parametrze, ale nie może mieć wartości domyślnej — pominięcie argumentów parameter_array spowodowałoby utworzenie pustej tablicy.
Przykład: Poniżej przedstawiono różne rodzaje parametrów:
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 ) { }
W parameter_list
M
jest wymaganymi
parametrem,ref
jest wymaganym parametrem wartości,d
,b
s
, io
są opcjonalnymi parametrami wartości it
jest tablicą parametrówa
.przykład końcowy
Deklaracja metody tworzy oddzielną przestrzeń deklaracji (§7.3) dla parametrów i parametrów typu. Nazwy są wprowadzane do tej przestrzeni deklaracji przez listę parametrów typu i listę parametrów metody. Treść metody, jeśli istnieje, jest uważana za zagnieżdżona w tej przestrzeni deklaracji. Jest to błąd dwóch elementów członkowskich przestrzeni deklaracji metody, aby mieć taką samą nazwę.
Wywołanie metody (§12.8.10.2) tworzy kopię, specyficzną dla tego wywołania, parametrów i zmiennych lokalnych metody, a lista argumentów wywołania przypisuje wartości lub odwołania do zmiennych do nowo utworzonych parametrów. W bloku metody parametry mogą być przywoływane przez ich identyfikatory w wyrażeniach simple_name (§12.8.4).
Istnieją następujące rodzaje parametrów:
- Parametry wartości (§15.6.2.2).
- Parametry wejściowe (§15.6.2.3.2).
- Parametry wyjściowe (§15.6.2.3.4).
- Parametry referencyjne (§15.6.2.3.3).
- Tablice parametrów (§15.6.2.4).
Uwaga: Zgodnie z opisem w §7.6 modyfikatory
in
,out
iref
są częścią podpisu metody, aleparams
modyfikator nie jest. notatka końcowa
15.6.2.2 Parametry wartości
Parametr zadeklarowany bez modyfikatorów jest parametrem wartości. Parametr wartości to zmienna lokalna, która pobiera jego wartość początkową z odpowiedniego argumentu podanego w wywołaniu metody.
Aby uzyskać określone reguły przypisania, zobacz §9.2.5.
Odpowiedni argument w wywołaniu metody jest wyrażeniem niejawnie konwertowanym (§10.2) do typu parametru.
Metoda może przypisywać nowe wartości do parametru wartości. Takie przypisania mają wpływ tylko na lokalną lokalizację magazynu reprezentowaną przez parametr wartości — nie mają wpływu na rzeczywisty argument podany w wywołaniu metody.
15.6.2.3 Parametry referencyjne
15.6.2.3.1 Ogólne
Parametry wejściowe, wyjściowe i referencyjne są parametramireferencyjnymi. Parametr by-reference jest lokalną zmienną referencyjną (§9.7); początkowy referent jest uzyskiwany z odpowiedniego argumentu podanego w wywołaniu metody.
Uwaga: odwołanie do parametru by-reference można zmienić przy użyciu operatora przypisania ref (
= ref
).
Gdy parametr jest parametrem by-reference, odpowiedni argument w wywołaniu metody składa się z odpowiedniego słowa kluczowego , , lub , a następnie in
(ref
) tego samego typu co parametr.out
Jednak gdy parametr jest parametremin
, argument może być wyrażeniem, dla którego istnieje niejawna konwersja (§10.2) z tego wyrażenia argumentu na typ odpowiedniego parametru.
Parametry by-reference nie są dozwolone w funkcjach zadeklarowanych jako iterator (§15.14) lub funkcji asynchronicznych (§15.15).
W metodzie, która przyjmuje wiele parametrów referencyjnych, istnieje możliwość, aby wiele nazw reprezentowało tę samą lokalizację magazynu.
15.6.2.3.2 Parametry wejściowe
Parametr zadeklarowany za pomocą in
modyfikatora jest parametrem wejściowym. Argument odpowiadający parametrowi wejściowemu jest zmienną istniejącą w punkcie wywołania metody lub argumentem utworzonym przez implementację (§12.6.2.3) w wywołaniu metody. Aby uzyskać określone reguły przypisania, zobacz §9.2.8.
Jest to błąd czasu kompilacji, aby zmodyfikować wartość parametru wejściowego.
Uwaga: Podstawowym celem parametrów wejściowych jest wydajność. Jeśli typ parametru metody jest dużą strukturą (jeśli chodzi o wymagania dotyczące pamięci), warto unikać kopiowania całej wartości argumentu podczas wywoływania metody. Parametry wejściowe umożliwiają metody odwoływania się do istniejących wartości w pamięci, zapewniając jednocześnie ochronę przed niepożądanymi zmianami tych wartości. notatka końcowa
15.6.2.3.3 Parametry odwołania
Parametr zadeklarowany za pomocą ref
modyfikatora jest parametrem referencyjnym. Aby uzyskać określone reguły przypisania, zobacz §9.2.6.
Przykład: przykład
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}"); } }
generuje dane wyjściowe
i = 2, j = 1
W przypadku wywołania elementu w elemecie
Swap
reprezentujeMain
x
ii
reprezentuje wartośćy
.j
W związku z tym wywołanie ma wpływ na zamianę wartościi
ij
.przykład końcowy
Przykład: w poniższym kodzie
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); } }
wywołanie
F
metody inG
przekazuje odwołanie dos
elementu zarówno dla , jaka
ib
. W związku z tym dla tego wywołania nazwys
,a
ib
wszystkie odwołują się do tej samej lokalizacji magazynu, a trzy przypisania modyfikują poles
wystąpienia .przykład końcowy
struct
W przypadku typu w metodzie wystąpienia metoda dostępu wystąpienia (§12.2.1) lub konstruktor wystąpienia z inicjatorem this
konstruktora słowo kluczowe zachowuje się dokładnie jako parametr referencyjny typu struktury (§12.8.14).
15.6.2.3.4 Parametry wyjściowe
Parametr zadeklarowany za pomocą out
modyfikatora jest parametrem wyjściowym. W przypadku określonych reguł przypisania zobacz §9.2.7.
Metoda zadeklarowana jako metoda częściowa (§15.6.9) nie ma parametrów wyjściowych.
Uwaga: Parametry wyjściowe są zwykle używane w metodach, które generują wiele zwracanych wartości. notatka końcowa
Przykład:
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); } }
W przykładzie są generowane dane wyjściowe:
c:\Windows\System\ hello.txt
Należy pamiętać, że
dir
zmienne iname
mogą być nieprzypisane przed przekazaniem ich doSplitPath
elementu i że są uważane za zdecydowanie przypisane po wywołaniu.przykład końcowy
15.6.2.4 Tablice parametrów
Parametr zadeklarowany za pomocą params
modyfikatora jest tablicą parametrów. Jeśli lista parametrów zawiera tablicę parametrów, jest to ostatni parametr na liście i musi być typu tablicy jednowymiarowej.
Przykład: typy
string[]
istring[][]
mogą być używane jako typ tablicy parametrów, ale typstring[,]
nie może. przykład końcowy
Uwaga: nie można połączyć
params
modyfikatora z modyfikatoramiin
,out
lubref
. notatka końcowa
Tablica parametrów umożliwia określenie argumentów na jeden z dwóch sposobów wywołania metody:
- Argument podany dla tablicy parametrów może być pojedynczym wyrażeniem, które jest niejawnie konwertowane (§10.2) do typu tablicy parametrów. W tym przypadku tablica parametrów działa dokładnie jak parametr wartości.
- Alternatywnie wywołanie może określać zero lub więcej argumentów dla tablicy parametrów, gdzie każdy argument jest wyrażeniem niejawnie konwertowanym (§10.2) do typu elementu tablicy parametrów. W tym przypadku wywołanie tworzy wystąpienie typu tablicy parametrów o długości odpowiadającej liczbie argumentów, inicjuje elementy wystąpienia tablicy z podanymi wartościami argumentów i używa nowo utworzonego wystąpienia tablicy jako rzeczywistego argumentu.
Z wyjątkiem zezwalania na zmienną liczbę argumentów w wywołaniu, tablica parametrów jest dokładnie równoważna parametrowi wartości (§15.6.2.2) tego samego typu.
Przykład: przykład
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(); } }
generuje dane wyjściowe
Array contains 3 elements: 1 2 3 Array contains 4 elements: 10 20 30 40 Array contains 0 elements:
Pierwsze wywołanie
F
po prostu przekazuje tablicęarr
jako parametr wartości. Drugie wywołanie języka F automatycznie tworzy cztery elementyint[]
z podanymi wartościami elementu i przekazuje to wystąpienie tablicy jako parametr wartości. Podobnie trzecie wywołanieF
metody tworzy elementint[]
zero i przekazuje to wystąpienie jako parametr wartości. Drugie i trzecie wywołania są dokładnie równoważne pisaniu:F(new int[] {10, 20, 30, 40}); F(new int[] {});
przykład końcowy
Podczas rozwiązywania przeciążeń metoda z tablicą parametrów może mieć zastosowanie w postaci normalnej lub w postaci rozszerzonej (§12.6.4.2). Rozszerzona forma metody jest dostępna tylko wtedy, gdy normalna forma metody nie ma zastosowania i tylko wtedy, gdy odpowiednia metoda z tym samym podpisem co rozszerzony formularz nie jest jeszcze zadeklarowana w tym samym typie.
Przykład: przykład
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); } }
generuje dane wyjściowe
F() F(object[]) F(object,object) F(object[]) F(object[])
W tym przykładzie dwie z możliwych rozszerzonych form metody z tablicą parametrów są już uwzględnione w klasie jako zwykłe metody. Te rozwinięte formularze nie są zatem brane pod uwagę podczas wykonywania rozpoznawania przeciążenia, a pierwsze i trzecie wywołania metody wybierają w ten sposób zwykłe metody. Gdy klasa deklaruje metodę z tablicą parametrów, nie jest rzadkością, aby uwzględnić niektóre rozwinięte formularze jako zwykłe metody. W ten sposób można uniknąć alokacji wystąpienia tablicy, które występuje po wywołaniu rozszerzonej formy metody z tablicą parametrów.
przykład końcowy
Tablica jest typem odwołania, więc wartość przekazana dla tablicy parametrów może mieć wartość
null
.Przykład: przykład:
class Test { static void F(params string[] array) => Console.WriteLine(array == null); static void Main() { F(null); F((string) null); } }
generuje dane wyjściowe:
True False
Drugie wywołanie powoduje
False
, że jest równoważneF(new string[] { null })
i przekazuje tablicę zawierającą pojedyncze odwołanie o wartości null.przykład końcowy
Gdy typ tablicy parametrów to object[]
, potencjalna niejednoznaczność występuje między normalną formą metody a rozwiniętym formularzem dla pojedynczego object
parametru. Przyczyną niejednoznaczności jest to, że sam jest object[]
niejawnie konwertowany na typ object
. Niejednoznaczność nie stanowi jednak problemu, ponieważ można go rozwiązać, wstawiając rzut w razie potrzeby.
Przykład: przykład
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); } }
generuje dane wyjściowe
System.Int32 System.String System.Double System.Object[] System.Object[] System.Int32 System.String System.Double
W pierwszych i ostatnich wywołaniach
F
metody ma zastosowanie normalna formaF
, ponieważ istnieje niejawna konwersja typu argumentu do typu parametru (oba są typuobject[]
). W związku z tym rozdzielczość przeciążenia wybiera normalną formęF
, a argument jest przekazywany jako parametr wartości regularnej. W drugim i trzecim wywołaniu normalna formaF
nie ma zastosowania, ponieważ nie istnieje niejawna konwersja typu argumentu na typ parametru (typobject
nie może być niejawnie konwertowany na typobject[]
). Jednak rozszerzona formaF
programu ma zastosowanie, dlatego jest wybierana przez rozwiązanie przeciążenia. W rezultacie jeden elementobject[]
jest tworzony przez wywołanie, a pojedynczy element tablicy jest inicjowany przy użyciu podanej wartości argumentu (która sama jest odwołaniem do obiektuobject[]
).przykład końcowy
15.6.3 Metody statyczne i wystąpienia
Gdy deklaracja metody zawiera static
modyfikator, ta metoda jest uważana za metodę statyczną. Jeśli modyfikator nie static
jest obecny, mówi się, że metoda jest metodą wystąpienia.
Metoda statyczna nie działa na określonym wystąpieniu i jest to błąd czasu kompilacji, do których należy odwołać this
się w metodzie statycznej.
Metoda wystąpienia działa na danym wystąpieniu klasy i można uzyskać do tego wystąpienia dostęp jako this
(§12.8.14).
Różnice między elementami statycznymi i elementami członkowskimi wystąpień zostały omówione w temacie §15.3.8.
15.6.4 Metody wirtualne
Gdy deklaracja metody wystąpienia zawiera modyfikator wirtualny, ta metoda jest uważana za metodę wirtualną. Jeśli nie ma modyfikatora wirtualnego, mówi się, że metoda jest metodą niewirtuacyjną.
Implementacja metody innej niż wirtualna jest niezmienna: Implementacja jest taka sama, czy metoda jest wywoływana na wystąpieniu klasy, w której jest zadeklarowana, czy wystąpienie klasy pochodnej. Natomiast implementacja metody wirtualnej może zostać zastąpiona przez klasy pochodne. Proces zastępowania implementacji dziedziczonej metody wirtualnej jest znany jako zastępowanie tej metody (§15.6.5).
W wywołaniu metody wirtualnej typ czasu wykonywania wystąpienia, dla którego odbywa się wywołanie, określa rzeczywistą implementację metody do wywołania. W wywołaniu metody niewirtualnej typ czasu kompilacji wystąpienia jest czynnikiem określającym. W precyzyjnych kategoriach, gdy metoda o nazwie N
jest wywoływana z listą A
argumentów na wystąpieniu z typem czasu kompilacji i typem C
R
czasu wykonywania (gdzie R
jest C
albo klasa pochodna z C
), wywołanie jest przetwarzane w następujący sposób:
- W czasie powiązania rozpoznawanie przeciążeń jest stosowane do
C
,N
iA
, aby wybrać określoną metodęM
z zestawu metod zadeklarowanych w i dziedziczone przezC
. Jest to opisane w §12.8.10.2. - Następnie w czasie wykonywania:
- Jeśli
M
jest metodą niewirtuacyjną,M
jest wywoływana. -
M
W przeciwnym razie jest metodą wirtualną, a najbardziej pochodna implementacja wM
odniesieniu doR
metody jest wywoływana.
- Jeśli
Dla każdej metody wirtualnej zadeklarowanej w klasie lub dziedziczonej przez klasę istnieje najbardziej pochodna implementacja metody w odniesieniu do tej klasy. Najbardziej pochodna implementacja metody M
wirtualnej w odniesieniu do klasy R
jest określana w następujący sposób:
- Jeśli
R
zawiera wprowadzającą wirtualną deklaracjęM
, jest to najbardziej pochodna implementacja wM
odniesieniu doR
elementu . - W przeciwnym razie, jeśli
R
zawiera przesłonięcia elementuM
, jest to najbardziej pochodna implementacja wM
odniesieniu doR
elementu . - W przeciwnym razie najbardziej pochodna implementacja w
M
odniesieniu doR
elementu jest taka sama jak najbardziej pochodna implementacjaM
w odniesieniu do bezpośredniej klasy bazowejR
klasy .
Przykład: Poniższy przykład ilustruje różnice między metodami wirtualnymi i niewirtualowymi:
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(); } }
W tym przykładzie
A
przedstawiono metodęF
niewirtuacyjną i metodęG
wirtualną . KlasaB
wprowadza nową metodęF
niewirtuacyjną , w związku z czym ukrywa dziedziczoneF
metody , a także zastępuje dziedziczona metodaG
. W przykładzie są generowane dane wyjściowe:A.F B.F B.G B.G
Zwróć uwagę, że instrukcja
a.G()
wywołuje metodęB.G
, a nieA.G
. Jest to spowodowane tym, że typ czasu wykonywania wystąpienia (czyliB
), a nie typ czasu kompilacji wystąpienia (czyliA
), określa rzeczywistą implementację metody do wywołania.przykład końcowy
Ponieważ metody mogą ukrywać dziedziczone metody, istnieje możliwość, aby klasa zawierała kilka metod wirtualnych z tym samym podpisem. Nie stanowi to problemu niejednoznaczności, ponieważ wszystkie, ale najbardziej pochodna metoda są ukryte.
Przykład: w poniższym kodzie
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(); } }
C
klasy iD
zawierają dwie metody wirtualne z tym samym podpisem: jedna wprowadzona przezA
i wprowadzona przezC
. Metoda wprowadzona przezC
program ukrywa metodę dziedziczona zA
klasy . W związku z tym deklaracja przesłonięć wD
zastąpieniu metody wprowadzonej przezC
metodę , i nie jest możliweD
zastąpienie metody wprowadzonej przezA
metodę . W przykładzie są generowane dane wyjściowe:B.F B.F D.F D.F
Należy pamiętać, że istnieje możliwość wywołania ukrytej metody wirtualnej przez uzyskanie dostępu do wystąpienia
D
za pośrednictwem mniej pochodnego typu, w którym metoda nie jest ukryta.przykład końcowy
15.6.5 Metody zastępowania
Gdy deklaracja metody wystąpienia zawiera override
modyfikator, mówi się, że metoda jest metodą zastąpienia. Metoda zastąpienia zastępuje dziedziczona metoda wirtualna z tym samym podpisem. Podczas gdy deklaracja metody wirtualnej wprowadza nową metodę, deklaracja metody zastąpienia specjalizuje się w istniejącej odziedziczonej metodzie wirtualnej, zapewniając nową implementację tej metody.
Metoda zastępowana przez deklarację przesłonięcia jest znana jako metoda przesłonięć podstawowa Dla metody M
przesłonięcia zadeklarowanej w klasie , metoda podstawy przesłaniania jest określana przez zbadanie każdej klasy bazowej C
klasy , począwszy od bezpośredniej klasy C
bazowej i kontynuowanie z każdą kolejną bezpośrednią klasą bazową, dopóki w danej klasie C
bazowej nie znajduje się co najmniej jedna dostępna metoda, która ma ten sam podpis co M
po podstawieniu argumentów typów. Na potrzeby lokalizowania zastąpionej metody bazowej metoda jest uważana za dostępną, jeśli jest public
to , jeśli jest protected
, jeśli jest , lub protected internal
, lub internal
private protected
jest zadeklarowana w tym samym programie co C
.
Błąd czasu kompilacji występuje, chyba że wszystkie następujące elementy są prawdziwe dla deklaracji zastąpienia:
- Zastąpiona metoda podstawowa może znajdować się zgodnie z powyższym opisem.
- Istnieje dokładnie jedna taka przesłonięta metoda podstawowa. To ograniczenie ma wpływ tylko wtedy, gdy typ klasy bazowej jest typem skonstruowanym, w którym podstawienie argumentów typu sprawia, że podpis dwóch metod jest taki sam.
- Zastępowana metoda podstawowa to metoda wirtualna, abstrakcyjna lub przesłonięć. Innymi słowy, zastępowana metoda podstawowa nie może być statyczna ani niewirtuatyczna.
- Zastępowana metoda podstawowa nie jest metodą zapieczętowaną.
- Istnieje konwersja tożsamości między zwracanym typem zastępowanej metody bazowej a metodą zastąpienia.
- Przesłonięć deklarację i zastąpioną metodę bazową mają taką samą zadeklarowaną dostępność. Innymi słowy, deklaracja zastąpienia nie może zmienić dostępności metody wirtualnej. Jeśli jednak przesłoniętą metodę bazową jest chroniona wewnętrznie i jest zadeklarowana w innym zestawie niż zestaw zawierający deklarację zastąpienia, deklarowana dostępność deklaracji zastąpienia jest chroniona.
- Deklaracja zastąpienia nie określa żadnych type_parameter_constraints_clauses. Zamiast tego ograniczenia są dziedziczone z zastępowanej metody bazowej. Ograniczenia, które są parametrami typu w metodzie przesłoniętej, mogą zostać zastąpione przez argumenty typu w odziedziczonym ograniczeniu. Może to prowadzić do ograniczeń, które nie są prawidłowe w przypadku jawnego określenia, takich jak typy wartości lub typy zapieczętowane.
Przykład: Poniżej pokazano, jak działają reguły zastępowania dla klas ogólnych:
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> }
przykład końcowy
Deklaracja przesłonięcia może uzyskać dostęp do zastępowanej metody podstawowej przy użyciu base_access (§12.8.15).
Przykład: w poniższym kodzie
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}"); } }
wywołanie w metodzie
base.PrintFields()
B
wywołuje metodę PrintFields zadeklarowaną w plikuA
. Base_access wyłącza mechanizm wywołania wirtualnego i po prostu traktuje metodę podstawową jako metodę innąvirtual
niż. Gdyby wywołanie w elemecie zostało napisaneB
, rekursywnie wywołałoby((A)this).PrintFields()
metodę zadeklarowaną w , a nie zadeklarowanej wPrintFields
B
elemecie , ponieważA
jest wirtualna, a typPrintFields
czasu wykonywania to((A)this)
.B
przykład końcowy
Tylko przez dołączenie override
modyfikatora może zastąpić inną metodę. We wszystkich innych przypadkach metoda z tym samym podpisem co dziedziczona metoda po prostu ukrywa dziedziczona metodę.
Przykład: w poniższym kodzie
class A { public virtual void F() {} } class B : A { public virtual void F() {} // Warning, hiding inherited F() }
F
metoda w plikuB
nie zawieraoverride
modyfikatora i dlatego nie zastępuje metody w metodzieF
.A
Zamiast tego metoda wF
elemecie ukrywa metodę wB
elemecie , a ostrzeżenie jest zgłaszane,A
ponieważ deklaracja nie zawiera nowego modyfikatora.przykład końcowy
Przykład: w poniższym kodzie
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 }
metoda w metodzie
F
B
ukrywa metodę wirtualnąF
dziedziczona zA
klasy . Ponieważ nowyF
element w systemieB
ma dostęp prywatny, jego zakres obejmuje tylko treśćB
klasy i nie rozszerza się naC
. W związku z tym deklaracjaF
inC
jest dozwolona, aby zastąpić dziedziczone zF
A
.przykład końcowy
15.6.6 Metody zapieczętowane
Gdy deklaracja metody wystąpienia zawiera sealed
modyfikator, ta metoda jest uważana za metodę zapieczętowaną. Zapieczętowana metoda zastępuje dziedziczona metoda wirtualna z tym samym podpisem. Metodę zapieczętowaną należy również oznaczyć modyfikatorem override
.
sealed
Użycie modyfikatora uniemożliwia dalsze zastępowanie metody przez klasę pochodną.
Przykład: przykład
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"); }
klasa
B
udostępnia dwie metody zastąpienia: metodęF
, która masealed
modyfikator i metodęG
, która nie.B
Użyciesealed
modyfikatora uniemożliwiaC
dalsze zastępowanieF
elementu .przykład końcowy
15.6.7 Metody abstrakcyjne
Gdy deklaracja metody wystąpienia zawiera abstract
modyfikator, ta metoda jest uważana za abstrakcyjną metodę. Mimo że metoda abstrakcyjna jest niejawnie również metodą wirtualną, nie może mieć modyfikatora virtual
.
Deklaracja metody abstrakcyjnej wprowadza nową metodę wirtualną, ale nie zapewnia implementacji tej metody. Zamiast tego klasy pochodne nie abstrakcyjne są wymagane do zapewnienia własnej implementacji przez zastąpienie tej metody. Ponieważ metoda abstrakcyjna nie zapewnia rzeczywistej implementacji, treść metody abstrakcyjnej po prostu składa się ze średnika.
Deklaracje metod abstrakcyjnych są dozwolone tylko w klasach abstrakcyjnych (§15.2.2.2).
Przykład: w poniższym kodzie
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); }
Shape
klasa definiuje abstrakcyjne pojęcie obiektu kształtu geometrycznego, który może się malować. Metoda jest abstrakcyjnaPaint
, ponieważ nie ma znaczącej implementacji domyślnej. KlasyEllipse
iBox
to konkretneShape
implementacje. Ponieważ te klasy nie są abstrakcyjnePaint
, są one wymagane do zastąpienia metody i zapewnienia rzeczywistej implementacji.przykład końcowy
Jest to błąd czasu kompilacji dla base_access (§12.8.15) w celu odwołania się do metody abstrakcyjnej.
Przykład: w poniższym kodzie
abstract class A { public abstract void F(); } class B : A { // Error, base.F is abstract public override void F() => base.F(); }
zgłaszany jest błąd czasu kompilacji dla
base.F()
wywołania, ponieważ odwołuje się do metody abstrakcyjnej.przykład końcowy
Deklaracja metody abstrakcyjnej może zastąpić metodę wirtualną. Dzięki temu klasa abstrakcyjna może wymusić ponowne wdrożenie metody w klasach pochodnych i sprawia, że oryginalna implementacja metody jest niedostępna.
Przykład: w poniższym kodzie
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"); }
klasa
A
deklaruje metodę wirtualną, klasaB
zastępuje tę metodę metodą abstrakcyjną, a klasaC
zastępuje metodę abstrakcyjną w celu zapewnienia własnej implementacji.przykład końcowy
15.6.8 Metody zewnętrzne
Gdy deklaracja metody zawiera extern
modyfikator, metoda jest uważana za metodę zewnętrzną. Metody zewnętrzne są implementowane zewnętrznie, zazwyczaj przy użyciu języka innego niż C#. Ponieważ deklaracja metody zewnętrznej nie zawiera rzeczywistej implementacji, treść metody zewnętrznej po prostu składa się z średnika. Metoda zewnętrzna nie jest ogólna.
Mechanizm, za pomocą którego jest osiągane połączenie z metodą zewnętrzną, jest definiowany przez implementację.
Przykład: W poniższym przykładzie pokazano użycie
extern
modyfikatora i atrybutuDllImport
: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); }
przykład końcowy
15.6.9 Metody częściowe
Gdy deklaracja metody zawiera partial
modyfikator, ta metoda jest uważana za metodę częściową. Metody częściowe mogą być deklarowane tylko jako elementy członkowskie typów częściowych (§15.2.7) i podlegają wielu ograniczeniom.
Metody częściowe można zdefiniować w jednej części deklaracji typu i zaimplementować w innej. Implementacja jest opcjonalna; jeśli żadna część nie implementuje metody częściowej, deklaracja metody częściowej i wszystkie wywołania są usuwane z deklaracji typu wynikającej z kombinacji części.
Metody częściowe nie definiują modyfikatorów dostępu; są niejawnie prywatne. Ich zwracany typ to void
, a ich parametry nie są parametrami wyjściowymi. Część identyfikatora jest rozpoznawana jako kontekstowe słowo kluczowe (§6.4.4) w deklaracji metody tylko wtedy, gdy pojawia się bezpośrednio przed void
słowem kluczowym. Metoda częściowa nie może jawnie implementować metod interfejsu.
Istnieją dwa rodzaje deklaracji metody częściowej: jeśli treść deklaracji metody jest średnikiem, deklaracja jest deklarowana jako definiująca deklarację metody częściowej. Jeśli treść jest inna niż średnik, deklarację mówi się, że jest deklaracją częściową metody implementacji. W częściach deklaracji typu może istnieć tylko jedna deklaracja metody częściowej z danym podpisem i może istnieć tylko jedna implementacja częściowej deklaracji metody z danym podpisem. Jeżeli zostanie podana deklaracja częściowej metody wykonawczej, istnieje odpowiednia deklaracja metody częściowej, a deklaracje są zgodne z następującymi:
- Deklaracje mają takie same modyfikatory (chociaż niekoniecznie w tej samej kolejności), nazwę metody, liczbę parametrów typu i liczbę parametrów.
- Odpowiednie parametry w deklaracjach mają takie same modyfikatory (chociaż niekoniecznie w tej samej kolejności) i tego samego typu lub typy kabrioletów tożsamości (różnice modulo w nazwach parametrów typu).
- Odpowiednie parametry typu w deklaracjach mają te same ograniczenia (modulo różnice w nazwach parametrów typu).
Implementacja deklaracji metody częściowej może pojawić się w tej samej części co odpowiednia deklaracja metody częściowej definiującej.
Tylko zdefiniowana metoda częściowa uczestniczy w rozpoznawaniu przeciążeń. W związku z tym, czy podano deklarację implementa, wyrażenia wywołania mogą być rozpoznawane jako wywołania metody częściowej. Ponieważ metoda częściowa zawsze zwraca void
wartość , takie wyrażenia wywołania zawsze będą instrukcjami wyrażeń. Ponadto, ponieważ metoda częściowa jest niejawnie private
, takie instrukcje zawsze będą występować w jednej z części deklaracji typu, w której zadeklarowana jest metoda częściowa.
Uwaga: Definicja dopasowywania i implementowania deklaracji metody częściowej nie wymaga dopasowania nazw parametrów. Może to spowodować zaskakujące, choć dobrze zdefiniowane zachowanie podczas stosowania argumentów nazwanych (§12.6.2.1). Na przykład biorąc pod uwagę definiowanie deklaracji metody częściowej dla
M
w jednym pliku i implementowanie częściowej deklaracji metody w innym pliku:// 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) {} }
jest nieprawidłowy , ponieważ wywołanie używa nazwy argumentu z implementacji, a nie definiującej częściowej deklaracji metody.
notatka końcowa
Jeśli żadna część częściowej deklaracji typu nie zawiera deklaracji implementowania dla danej metody częściowej, każda instrukcja wyrażenia wywołująca ją zostanie po prostu usunięta z deklaracji połączonego typu. W związku z tym wyrażenie wywołania, w tym wszelkie wyrażenia podrzędne, nie ma wpływu w czasie wykonywania. Sama metoda częściowa jest również usuwana i nie będzie elementem członkowskim połączonej deklaracji typu.
Jeśli deklaracja implementowania istnieje dla danej metody częściowej, wywołania metod częściowych są zachowywane. Metoda częściowa powoduje powstanie deklaracji metody podobnej do implementacji deklaracji częściowej metody z wyjątkiem następujących:
Modyfikator
partial
nie jest uwzględniony.Atrybuty w wynikowej deklaracji metody są połączonymi atrybutami definiowania i implementowania częściowej deklaracji metody w nieokreślonej kolejności. Duplikaty nie są usuwane.
Atrybuty parametrów wynikowej deklaracji metody to połączone atrybuty odpowiednich parametrów definiowania i implementowania częściowej deklaracji metody w nieokreślonej kolejności. Duplikaty nie są usuwane.
Jeśli deklaracja definiująca, ale nie deklaracja implementowania jest podana dla metody M
częściowej, obowiązują następujące ograniczenia:
Jest to błąd czasu kompilacji tworzenia delegata z (
M
§12.8.17.6).Jest to błąd czasu kompilacji, aby odwoływać się do
M
wewnątrz funkcji anonimowej, która jest konwertowana na typ drzewa wyrażeń (§8.6).Wyrażenia występujące w ramach wywołania
M
nie wpływają na określony stan przypisania (§9.4), co może potencjalnie prowadzić do błędów czasu kompilacji.M
nie może być punktem wejścia dla aplikacji (§7.1).
Metody częściowe są przydatne do umożliwienia jednej części deklaracji typu w celu dostosowania zachowania innej części, np. takiej, która jest generowana przez narzędzie. Rozważ następującą deklarację klasy częściowej:
partial class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
partial void OnNameChanging(string newName);
partial void OnNameChanged();
}
Jeśli ta klasa zostanie skompilowana bez żadnych innych części, zdefiniowane deklaracje metody częściowej i ich wywołania zostaną usunięte, a wynikowa deklaracja klasy połączonej będzie równoważna następującym:
class Customer
{
string name;
public string Name
{
get => name;
set => name = value;
}
}
Załóżmy jednak, że podano inną część, która udostępnia deklaracje implementujące metody częściowe:
partial class Customer
{
partial void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
partial void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
Następnie wynikowa deklaracja klasy połączonej będzie równoważna następującemu:
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 Metody rozszerzenia
Gdy pierwszy parametr metody zawiera this
modyfikator, ta metoda jest określana jako metoda rozszerzenia. Metody rozszerzenia są deklarowane tylko w niegenerycznych, nienagnieżdżonych klasach statycznych. Pierwszy parametr metody rozszerzenia jest ograniczony w następujący sposób:
- Może to być parametr wejściowy tylko wtedy, gdy ma typ wartości
- Może to być parametr referencyjny tylko wtedy, gdy ma typ wartości lub ma typ ogólny ograniczony do struktury
- Nie jest to typ wskaźnika.
Przykład: Poniżej przedstawiono przykład klasy statycznej, która deklaruje dwie metody rozszerzenia:
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; } }
przykład końcowy
Metoda rozszerzenia jest regularną metodą statyczną. Ponadto, gdy otaczana klasa statyczna znajduje się w zakresie, metoda rozszerzenia może być wywoływana przy użyciu składni wywołania metody wystąpienia (§12.8.10.3), używając wyrażenia odbiornika jako pierwszego argumentu.
Przykład: Poniższy program używa metod rozszerzeń zadeklarowanych powyżej:
static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in strings.Slice(1, 2)) { Console.WriteLine(s.ToInt32()); } } }
Metoda
Slice
jest dostępna w metodziestring[]
, aToInt32
metoda jest dostępna w metodziestring
, ponieważ zostały one zadeklarowane jako metody rozszerzenia. Znaczenie programu jest takie samo jak poniżej, używając zwykłych wywołań metod statycznych: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)); } } }
przykład końcowy
Treść metody 15.6.11
Treść metody deklaracji metody składa się z treści bloku, treści wyrażenia lub średnika.
Deklaracje metody abstrakcyjnej i zewnętrznej nie zapewniają implementacji metody, więc ich jednostki metody po prostu składają się ze średnika. W przypadku każdej innej metody treść metody jest blokiem (§13.3), który zawiera instrukcje do wykonania po wywołaniu tej metody.
Efektywny typ zwracany metody to, jeśli zwracany typ to void
void
, lub jeśli metoda jest asynchronizna, a zwracany typ to «TaskType»
(§15.15.1). W przeciwnym razie efektywny typ zwracany metody innej niż asynchroniczny jest typem zwracanym, a efektywnym typem zwracanym metody asynchronicznej z typem «TaskType»<T>
zwrotnym (§15.15.1) jest T
.
Jeżeli skuteczny typ zwracany metody jest void
i metoda ma treść bloku, return
instrukcje (§13.10.5) w bloku nie określają wyrażenia. Jeśli wykonanie bloku metody void zakończy się normalnie (tj. kontrolka przepływa poza koniec treści metody), ta metoda po prostu powróci do obiektu wywołującego.
Gdy skuteczny typ zwracany metody jest void
i metoda ma treść wyrażenia, wyrażenie E
jest statement_expression, a treść jest dokładnie równoważna treści bloku formularza { E; }
.
W przypadku metody zwracanej po wartości (§15.6.1) każda instrukcja zwracana w treści tej metody określa wyrażenie niejawnie konwertowane na efektywny typ zwracany.
W przypadku metody return-by-ref (§15.6.1) każda instrukcja zwracana w treści tej metody określa wyrażenie, którego typem jest skuteczny typ zwracany, i ma kontekst ref-safe-context kontekstu wywołującego (§9.7.2).
W przypadku metod zwracanych według wartości i zwracanych przez ref punkt końcowy treści metody nie jest osiągalny. Innymi słowy, kontrolka nie może przepływać poza koniec treści metody.
Przykład: w poniższym kodzie
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; }
metoda zwracana
F
przez wartość powoduje błąd czasu kompilacji, ponieważ kontrolka może przepływać poza koniec treści metody. MetodyG
iH
są poprawne, ponieważ wszystkie możliwe ścieżki wykonywania kończą się instrukcją return, która określa wartość zwracaną. MetodaI
jest poprawna, ponieważ jej treść jest równoważna blokowi z tylko jedną instrukcją return w niej.przykład końcowy
Właściwości 15.7
15.7.1 Ogólne
Właściwość jest elementem członkowskim, który zapewnia dostęp do właściwości obiektu lub klasy. Przykłady właściwości obejmują długość ciągu, rozmiar czcionki, podpis okna i nazwę klienta. Właściwości są naturalnym rozszerzeniem pól — oba są nazwanymi elementami członkowskimi skojarzonymi typami, a składnia uzyskiwania dostępu do pól i właściwości jest taka sama. Jednak w przeciwieństwie do pól właściwości nie oznaczają lokalizacji przechowywania. Zamiast tego właściwości mają metody dostępu określające instrukcje , które mają być wykonywane, gdy ich wartości są odczytywane lub zapisywane. Właściwości zapewniają zatem mechanizm kojarzenia akcji z odczytem i zapisem cech obiektu lub klasy; ponadto pozwalają one na obliczanie takich cech.
Właściwości są deklarowane przy użyciu property_declarations:
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) jest dostępny tylko w niebezpiecznym kodzie (§23).
Istnieją dwa rodzaje property_declaration:
- Pierwszy deklaruje właściwość inną niż ref-valued. Jego wartość ma typ typu. Tego rodzaju właściwość może być czytelna i/lub zapisywalna.
- Drugi deklaruje właściwość ref-valued. Jego wartość to variable_reference (§9.5
readonly
Tego rodzaju właściwość jest dostępna tylko do odczytu.
Property_declaration może zawierać zestaw atrybutów(§22) i dowolny z dozwolonych rodzajów zadeklarowanych ułatwień dostępu (§15.3.6),new
(§15.3.5), (§15.7.2virtual
, §15.7.6), override
(§15.6.5, §15.7.6), sealed
(§15.6.6), abstract
(§15.6.7, §15.7.6) i extern
(§15.6.8) modyfikatory.
Deklaracje właściwości podlegają tym samym regułom co deklaracje metod (§15.6) w odniesieniu do prawidłowych kombinacji modyfikatorów.
Member_name (§15.6.1) określa nazwę właściwości. Jeśli właściwość nie jest jawną implementacją składową interfejsu, member_name jest po prostu identyfikatorem. W przypadku jawnej implementacji składowej interfejsu (§18.6.2) member_name składa się z interface_type , po którym następuje ".
" i identyfikator.
Typ nieruchomości jest co najmniej tak dostępny, jak sama właściwość (§7.5.5).
Property_body może składać się z treści instrukcji lub treści wyrażenia. W treści instrukcji, accessor_declarations, które są ujęte w{
"" i "}
" tokeny, deklarują metody dostępu (§15.7.3) właściwości. Metody dostępu określają instrukcje wykonywalne skojarzone z odczytywaniem i zapisywaniem właściwości.
W property_body treści wyrażeniaE
, a średnik jest dokładnie odpowiednikiem treści { get { return E; } }
instrukcji i dlatego może służyć tylko do określania właściwości tylko do odczytu, gdy wynik metody get jest podawany przez pojedyncze wyrażenie.
Property_initializer można podać tylko dla właściwości zaimplementowanej automatycznie (§15.7.4) i powoduje zainicjowanie pola bazowego takich właściwości z wartością podaną przez wyrażenie.
Ref_property_body może składać się z treści instrukcji lub treści wyrażenia. W treści instrukcji get_accessor_declaration deklaruje akcesorium get (§15.7.3) właściwości. Metodę dostępu określa instrukcje wykonywalne skojarzone z odczytywaniem właściwości.
W ref_property_body treści wyrażenia składającej =>
się z elementu ref
, variable_referenceV
i średnik jest dokładnie odpowiednikiem treści { get { return ref V; } }
instrukcji .
Uwaga: Mimo że składnia uzyskiwania dostępu do właściwości jest taka sama jak w przypadku pola, właściwość nie jest klasyfikowana jako zmienna. W związku z tym nie można przekazać właściwości jako
in
,out
lubref
argumentu, chyba że właściwość jest wartością ref i dlatego zwraca odwołanie do zmiennej (§9.7). notatka końcowa
Gdy deklaracja właściwości zawiera extern
modyfikator, właściwość jest uważana za właściwość zewnętrzną. Ze względu na to, że deklaracja właściwości zewnętrznej nie zapewnia rzeczywistej implementacji, każdy z accessor_bodyw jego accessor_declarations jest średnikiem.
15.7.2 Właściwości statyczne i wystąpienia
Gdy deklaracja właściwości zawiera static
modyfikator, właściwość jest uważana za właściwość statyczną. Jeśli modyfikator nie static
istnieje, właściwość jest uważana za właściwość wystąpienia.
Właściwość statyczna nie jest skojarzona z określonym wystąpieniem i jest to błąd czasu kompilacji, do których należy odwołać się this
w metodzie dostępu właściwości statycznej.
Właściwość wystąpienia jest skojarzona z danym wystąpieniem klasy i można uzyskać do tego wystąpienia dostęp jako this
(§12.8.14) w metodach dostępu tej właściwości.
Różnice między elementami statycznymi i elementami członkowskimi wystąpień zostały omówione w temacie §15.3.8.
15.7.3 Akcesoria
Uwaga: Ta klauzula dotyczy obu właściwości (§15.7) i indeksatorów (§15.9). Klauzula jest zapisywana pod względem właściwości, podczas odczytywania indeksatorów zastąp indeksator/indeksatory właściwości/właściwości i zapoznaj się z listą różnic między właściwościami i indeksatorami podanymi w §15.9.2. notatka końcowa
Accessor_declarations właściwości określają instrukcje wykonywalne skojarzone z zapisem i/lub odczytywaniem tej właściwości.
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 ';'
| ';'
;
Accessor_declarations składają się z get_accessor_declaration, set_accessor_declaration lub obu tych elementów. Każda deklaracja dostępu składa się z opcjonalnych atrybutów, opcjonalnych accessor_modifier, tokenu get
lub set
, a następnie accessor_body.
W przypadku właściwości ref valued ref_get_accessor_declaration składa się z opcjonalnych atrybutów, opcjonalnego accessor_modifier tokenu get
, a następnie ref_accessor_body.
Korzystanie z accessor_modifiers podlega następującym ograniczeniom:
- Accessor_modifier nie należy używać w interfejsie ani w jawnej implementacji składowej interfejsu.
- W przypadku właściwości lub indeksatora, który nie
override
ma modyfikatora, accessor_modifier jest dozwolony tylko wtedy, gdy właściwość lub indeksator ma zarówno metodę get, jak i set, a następnie jest dozwolona tylko na jednym z tych metod dostępu. - Dla właściwości lub indeksatora, który zawiera
override
modyfikator, akcesorium musi odpowiadać accessor_modifier, jeśli w ogóle, akcesorium jest zastępowane. - Accessor_modifier deklaruje dostępność, która jest ściśle bardziej restrykcyjna niż deklarowana dostępność samej właściwości lub indeksatora. Aby być precyzyjnym:
- Jeśli właściwość lub indeksator ma zadeklarowaną dostępność , ułatwienia dostępu
public
zadeklarowane przez accessor_modifier mogą być alboprivate protected
, ,protected internal
,internal
,protected
lubprivate
. - Jeśli właściwość lub indeksator ma zadeklarowaną dostępność , ułatwienia dostępu
protected internal
zadeklarowane przez accessor_modifier mogą być alboprivate protected
, ,protected private
,internal
,protected
lubprivate
. - Jeśli właściwość lub indeksator ma zadeklarowaną dostępność lub
internal
, dostępnośćprotected
zadeklarowana przez accessor_modifier musi być alboprivate protected
lubprivate
. - Jeśli właściwość lub indeksator ma zadeklarowaną dostępność , ułatwienia dostępu
private protected
zadeklarowane przez accessor_modifier toprivate
. - Jeśli właściwość lub indeksator ma zadeklarowaną dostępność
private
, nie można użyć accessor_modifier .
- Jeśli właściwość lub indeksator ma zadeklarowaną dostępność , ułatwienia dostępu
W przypadku abstract
właściwości innych extern
niż ref każda accessor_body dla każdego określonego metody dostępu jest po prostu średnikiem. Nie abstrakcyjna, nie-extern właściwość, ale nie indeksator, może również mieć accessor_body dla wszystkich metod dostępu określonych średnikiem, w tym przypadku jest to automatycznie zaimplementowana właściwość (§15.7.4). Właściwość zaimplementowana automatycznie musi mieć co najmniej metodę dostępu. W przypadku metod dostępu innych właściwości innych niż abstrakcyjne, inne niż extern, accessor_body jest albo:
- blok określający instrukcje, które mają być wykonywane po wywołaniu odpowiedniego metody dostępu; lub
- treść wyrażenia, która składa się z następującego
=>
po nim wyrażenia i średnika, i określa pojedyncze wyrażenie do wykonania po wywołaniu odpowiedniego metody dostępu.
W przypadku abstract
właściwości i extern
wartości ref ref_accessor_body jest po prostu średnikiem. W przypadku metody dostępu do jakiejkolwiek innej właściwości niesstrakcyjnej, innej niż extern, ref_accessor_body jest albo:
- blok określający instrukcje, które mają być wykonywane po wywołaniu metody get dostępu; lub
- treść wyrażenia, która składa się z następujących
=>
elementówref
: , variable_reference i średnik. Odwołanie do zmiennej jest oceniane po wywołaniu metody uzyskiwania dostępu.
Metoda get get dla właściwości innej niż ref odpowiada metodzie bez parametrów z zwracaną wartością typu właściwości. Z wyjątkiem elementu docelowego przypisania, gdy taka właściwość jest przywoływana w wyrażeniu, jego metodę get metody dostępu jest wywoływana w celu obliczenia wartości właściwości (§12.2.2).
Treść metody get accessor dla właściwości innej niż ref jest zgodna z zasadami metod zwracania wartości opisanych w §15.6.11. W szczególności wszystkie return
instrukcje w treści metody get accessor określają wyrażenie niejawnie konwertowane na typ właściwości. Ponadto punkt końcowy metody uzyskiwania dostępu nie jest osiągalny.
Metoda get get dla właściwości ref-valued odpowiada metodzie bez parametrów z zwracaną wartością variable_reference do zmiennej typu właściwości. Gdy taka właściwość jest przywoływana w wyrażeniu, jego metodę get jest wywoływana w celu obliczenia wartości variable_reference właściwości. Odwołanie do tej zmiennej, podobnie jak inne, jest następnie używane do odczytu lub, w przypadku nieczytanych variable_references, zapisuj przywołyną zmienną zgodnie z wymaganiami kontekstu.
Przykład: Poniższy przykład ilustruje właściwość ref-valued jako element docelowy przypisania:
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 } }
przykład końcowy
Treść metody get dla właściwości o wartości ref jest zgodna z zasadami metod o wartości ref opisanej w §15.6.11.
Metoda dostępu zestawu odpowiada metodzie z pojedynczym parametrem wartości typu właściwości i typem zwracanym void
. Niejawny parametr metody dostępu zestawu ma zawsze nazwę value
. Gdy właściwość jest przywoływana jako element docelowy przypisania (§12.21) lub jako operand lub ++
(§12.8.16, –-
), zestaw metod dostępu jest wywoływany z argumentem, który dostarcza nową wartość (§12.21.2). Treść zestawu dostępu jest zgodna z zasadami void
metod opisanymi w §15.6.11. W szczególności instrukcje return w treści zestawu akcesoriów nie mogą określać wyrażenia. Ponieważ zestaw metod dostępu niejawnie ma parametr o nazwie value
, jest to błąd czasu kompilacji dla zmiennej lokalnej lub deklaracji stałej w zestawie dostępu, aby mieć tę nazwę.
Na podstawie obecności lub braku metod dostępu get i set, właściwość jest klasyfikowana w następujący sposób:
- Właściwość, która zawiera zarówno metodę dostępu get, jak i zestaw akcesoriów jest mówi się, że jest właściwością read-write.
- Właściwość, która ma tylko metodę get, mówi się, że jest właściwością tylko do odczytu. Jest to błąd czasu kompilacji dla właściwości tylko do odczytu, która ma być elementem docelowym przypisania.
- Właściwość, która ma tylko zestaw akcesoriów, mówi się, że jest właściwością tylko do zapisu. Z wyjątkiem wartości docelowej przypisania, jest to błąd czasu kompilacji, aby odwołać się do właściwości tylko do zapisu w wyrażeniu.
Uwaga: Operatory pre-i postfiks
++
i--
operatory i operatory przypisania złożonego nie mogą być stosowane do właściwości tylko do zapisu, ponieważ te operatory odczytują starą wartość operandu przed zapisem nowego. notatka końcowa
Przykład: w poniższym kodzie
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 } }
kontrolka
Button
deklaruje właściwość publicznąCaption
. Metoda get dostępu właściwości Caption zwracastring
wartość przechowywaną w polu prywatnymcaption
. Zestaw metod dostępu sprawdza, czy nowa wartość różni się od bieżącej wartości, a jeśli tak, przechowuje nową wartość i przemaluje kontrolkę. Właściwości często są zgodne ze wzorcemprivate
pokazanym powyżej: Metoda pobierania po prostu zwraca wartość przechowywaną w polu, a zestaw metod dostępu modyfikuje toprivate
pole, a następnie wykonuje wszelkie dodatkowe akcje wymagane do zaktualizowania stanu obiektu.Button
Biorąc pod uwagę powyżej klasę, poniżej przedstawiono przykład użyciaCaption
właściwości :Button okButton = new Button(); okButton.Caption = "OK"; // Invokes set accessor string s = okButton.Caption; // Invokes get accessor
W tym miejscu metodę dostępu zestawu jest wywoływana przez przypisanie wartości do właściwości, a metodę get jest wywoływana przez odwoływanie się do właściwości w wyrażeniu.
przykład końcowy
Metody pobierania i ustawiania właściwości nie są odrębnymi elementami członkowskimi i nie można oddzielnie zadeklarować akcesoriów właściwości.
Przykład: przykład
class A { private string name; // Error, duplicate member name public string Name { get => name; } // Error, duplicate member name public string Name { set => name = value; } }
nie deklaruje pojedynczej właściwości odczytu i zapisu. Zamiast tego deklaruje dwie właściwości o tej samej nazwie, jeden tylko do odczytu i jeden tylko do zapisu. Ponieważ dwa elementy członkowskie zadeklarowane w tej samej klasie nie mogą mieć tej samej nazwy, przykład powoduje wystąpienie błędu czasu kompilacji.
przykład końcowy
Gdy klasa pochodna deklaruje właściwość o tej samej nazwie co dziedziczona właściwość, właściwość pochodna ukrywa dziedziczona właściwość w odniesieniu zarówno do odczytu, jak i zapisu.
Przykład: w poniższym kodzie
class A { public int P { set {...} } } class B : A { public new int P { get {...} } }
P
właściwość w obiekcieB
ukrywaP
właściwość wA
odniesieniu zarówno do czytania, jak i zapisu. W ten sposób w instrukcjachB b = new B(); b.P = 1; // Error, B.P is read-only ((A)b).P = 1; // Ok, reference to A.P
przypisanie powoduje
b.P
zgłaszanie błędu czasu kompilacji, ponieważ właściwość tylko do odczytu ukrywaP
właściwość tylkoB
P
do zapisu w plikuA
. Należy jednak pamiętać, że rzutowanie może służyć do uzyskiwania dostępu do ukrytejP
właściwości.przykład końcowy
W przeciwieństwie do pól publicznych właściwości zapewniają separację między stanem wewnętrznym obiektu a jego interfejsem publicznym.
Przykład: Rozważmy następujący kod, który używa
Point
struktury do reprezentowania lokalizacji: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; }
W tym
Label
miejscu klasa używa dwóchint
pólx
iy
, aby przechowywać swoją lokalizację. Lokalizacja jest publicznie uwidoczniona zarówno jako właściwość, jakX
i i jakoY
właściwość typuLocation
Point
. Jeśli w przyszłejLabel
wersji programu stanie się wygodniejsze przechowywanie lokalizacjiPoint
wewnętrznie, można wprowadzić zmianę bez wpływu na publiczny interfejs klasy: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; }
Gdyby
x
pola iy
zamiastpublic readonly
tego były polami, byłoby niemożliwe wprowadzenie takiej zmiany wLabel
klasie.przykład końcowy
Uwaga: uwidacznianie stanu za pomocą właściwości niekoniecznie jest mniej wydajne niż bezpośrednie uwidacznianie pól. W szczególności, gdy właściwość jest niewirtualna i zawiera tylko niewielką ilość kodu, środowisko wykonawcze może zastąpić wywołania metod dostępu rzeczywistym kodem akcesoriów. Ten proces jest znany jako podkreślenie i sprawia, że dostęp do właściwości jest tak wydajny, jak dostęp do pól, ale zachowuje zwiększoną elastyczność właściwości. notatka końcowa
Przykład: Ponieważ wywoływanie metody uzyskiwania dostępu jest koncepcyjnie równoważne z odczytywaniem wartości pola, jest uważany za zły styl programowania, aby uzyskać metody dostępu do obserwowanych skutków ubocznych. W przykładzie
class Counter { private int next; public int Next => next++; }
wartość
Next
właściwości zależy od liczby przypadków, w których wcześniej uzyskiwano dostęp do właściwości. W związku z tym uzyskanie dostępu do właściwości powoduje zauważalny efekt uboczny, a właściwość powinna zostać zaimplementowana jako metoda.Konwencja "brak skutków ubocznych" dla metod dostępu nie oznacza, że uzyskiwanie akcesoriów powinno być zawsze zapisywane po prostu w celu zwracania wartości przechowywanych w polach. W rzeczywistości metody uzyskiwania dostępu często obliczają wartość właściwości przez uzyskanie dostępu do wielu pól lub wywoływanie metod. Jednak prawidłowo zaprojektowane metody uzyskiwania dostępu nie wykonują żadnych akcji, które powodują zauważalne zmiany w stanie obiektu.
przykład końcowy
Właściwości mogą służyć do opóźniania inicjowania zasobu do momentu pierwszego przywołowania.
Przykład:
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; } } ... }
Klasa
Console
zawiera trzy właściwości,In
,Out
iError
, które reprezentują odpowiednio standardowe urządzenia wejściowe, wyjściowe i błędów. Uwidaczniając te elementy członkowskie jako właściwości,Console
klasa może opóźnić ich inicjowanie, dopóki nie zostaną one rzeczywiście użyte. Na przykład przy pierwszym odwoływaniaOut
się do właściwości , jak wConsole.Out.WriteLine("hello, world");
tworzony jest element bazowy
TextWriter
dla urządzenia wyjściowego. Jeśli jednak aplikacja nie odwołuje się doIn
właściwości iError
, dla tych urządzeń nie są tworzone żadne obiekty.przykład końcowy
15.7.4 Automatycznie zaimplementowane właściwości
Automatycznie zaimplementowana właściwość (lub właściwość automatyczna w skrócie) jest nie abstrakcyjną, inną niż extern, niekorzystaną z właściwością niekorzystaną z właściwością tylko średnikami accessor_bodys. Właściwości automatyczne mają dostęp do metody dostępu i mogą opcjonalnie mieć zestaw akcesoriów.
Gdy właściwość jest określona jako automatycznie zaimplementowana właściwość, ukryte pole zapasowe jest automatycznie dostępne dla właściwości, a metody dostępu są implementowane do odczytu i zapisu w tym polu pomocniczym. Ukryte pole zapasowe jest niedostępne, można je odczytywać i zapisywać tylko za pośrednictwem automatycznie zaimplementowanych metod dostępu do właściwości, nawet w obrębie typu zawierającego. Jeśli właściwość automatyczna nie ma ustawionego dostępu, pole zapasowe jest uznawane za readonly
(§15.5.3). Podobnie jak readonly
w przypadku pola, właściwość automatyczna tylko do odczytu może być również przypisana w treści konstruktora otaczającej klasy. Takie przypisanie przypisuje bezpośrednio do pola kopii zapasowej tylko do odczytu właściwości.
Właściwość automatyczna może opcjonalnie mieć property_initializer, która jest stosowana bezpośrednio do pola zapasowego jako variable_initializer (§17.7).
Przykład:
public class Point { public int X { get; set; } // Automatically implemented public int Y { get; set; } // Automatically implemented }
jest odpowiednikiem następującej deklaracji:
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; } } }
przykład końcowy
Przykład: w następujących
public class ReadOnlyPoint { public int X { get; } public int Y { get; } public ReadOnlyPoint(int x, int y) { X = x; Y = y; } }
jest odpowiednikiem następującej deklaracji:
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; } }
Przypisania do pola tylko do odczytu są prawidłowe, ponieważ występują w konstruktorze.
przykład końcowy
Mimo że pole zapasowe jest ukryte, to pole może mieć atrybuty docelowe pola stosowane bezpośrednio do niego za pośrednictwem automatycznie zaimplementowanej właściwości property_declaration (§15.7.1).
Przykład: następujący kod
[Serializable] public class Foo { [field: NonSerialized] public string MySecret { get; set; } }
powoduje zastosowanie atrybutu
NonSerialized
docelowego pola do pola generowanego przez kompilator, tak jakby kod został napisany w następujący sposób:[Serializable] public class Foo { [NonSerialized] private string _mySecretBackingField; public string MySecret { get { return _mySecretBackingField; } set { _mySecretBackingField = value; } } }
przykład końcowy
15.7.5 Ułatwienia dostępu
Jeśli akcesorium ma accessor_modifier, domena ułatwień dostępu (§7.5.3) metody dostępu jest określana przy użyciu zadeklarowanej dostępności accessor_modifier. Jeśli akcesorium nie ma accessor_modifier, domena ułatwień dostępu dostępu jest określana na podstawie zadeklarowanej dostępności właściwości lub indeksatora.
Obecność accessor_modifier nigdy nie wpływa na wyszukiwanie składowych (§12.5) lub rozpoznawanie przeciążenia (§12.6.4). Modyfikatory właściwości lub indeksatora zawsze określają, z którą właściwością lub indeksatorem jest powiązana, niezależnie od kontekstu dostępu.
Po wybraniu określonej właściwości innej niż ref lub indeksatora innego niż ref- wartość, domeny ułatwień dostępu określonych metod dostępu są używane do określenia, czy użycie jest prawidłowe:
- Jeśli użycie jest wartością (§12.2.2), dostęp do pobierania musi istnieć i być dostępny.
- Jeśli użycie jest celem prostego przypisania (§12.21.2), akcesorium zestawu musi istnieć i być dostępne.
- Jeśli użycie jest celem przypisania złożonego (§12.21.4) lub jako cel
++
operatorów lub--
(§12.8.16, §12.9.6), zarówno metody dostępu do pobrania, jak i zestawu akcesoriów muszą istnieć i być dostępne.
Przykład: W poniższym przykładzie właściwość jest ukryta przez właściwość
A.Text
B.Text
, nawet w kontekstach, w których wywoływana jest tylko funkcja dostępu zestawu. Natomiast właściwośćB.Count
nie jest dostępna dla klasyM
, więc zamiast tego jest używana właściwośćA.Count
dostępna.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 } }
przykład końcowy
Po wybraniu określonej właściwości ref-valued lub indeksatora o wartości ref; czy użycie jest wartością, celem prostego przypisania, czy celem przypisania złożonego; domena ułatwień dostępu zaangażowanych metod dostępu służy do określania, czy użycie jest prawidłowe.
Akcesorium używane do implementowania interfejsu nie ma accessor_modifier. Jeśli do zaimplementowania interfejsu jest używany tylko jeden akcesor dostępu, inne metody dostępu mogą być zadeklarowane za pomocą accessor_modifier:
Przykład:
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 } }
przykład końcowy
15.7.6 Wirtualne, zapieczętowane, zastępowane i abstrakcyjne metody dostępu
Uwaga: Ta klauzula dotyczy obu właściwości (§15.7) i indeksatorów (§15.9). Klauzula jest zapisywana pod względem właściwości, podczas odczytywania indeksatorów zastąp indeksator/indeksatory właściwości/właściwości i zapoznaj się z listą różnic między właściwościami i indeksatorami podanymi w §15.9.2. notatka końcowa
Deklaracja właściwości wirtualnej określa, że metody dostępu właściwości są wirtualne. Modyfikator virtual
ma zastosowanie do wszystkich metod dostępu innych niż prywatne właściwości. Gdy akcesorium właściwości wirtualnej ma private
accessor_modifier, dostęp prywatny nie jest niejawnie wirtualny.
Deklaracja właściwości abstrakcyjnej określa, że metody dostępu właściwości są wirtualne, ale nie zapewnia rzeczywistej implementacji metod dostępu. Zamiast tego klasy pochodne nie abstrakcyjne są wymagane do zapewnienia własnej implementacji dla metod dostępu przez zastąpienie właściwości. Ponieważ metoda dostępu do deklaracji właściwości abstrakcyjnej nie zapewnia rzeczywistej implementacji, jej accessor_body po prostu składa się z średnika. Właściwość abstrakcyjna nie ma private
metody dostępu.
Deklaracja właściwości, która zawiera zarówno abstract
modyfikatory , jak i override
określa, że właściwość jest abstrakcyjna i zastępuje właściwość podstawową. Metody dostępu takiej właściwości są również abstrakcyjne.
Deklaracje właściwości abstrakcyjnych są dozwolone tylko w klasach abstrakcyjnych (§15.2.2.2). Metody dostępu dziedziczonej właściwości wirtualnej można zastąpić w klasie pochodnej, dołączając deklarację właściwości, która określa dyrektywę override
. Jest to nazywane zastępowaniem deklaracji właściwości. Zastępowanie deklaracji właściwości nie deklaruje nowej właściwości. Zamiast tego po prostu specjalizuje się w implementacjach metod dostępu istniejącej właściwości wirtualnej.
Aby mieć taką samą zadeklarowaną dostępność, wymagana jest deklaracja zastąpienia i zastąpiona właściwość podstawowa. Innymi słowy, deklaracja zastąpienia nie zmienia dostępności właściwości podstawowej. Jeśli jednak przesłoniętą właściwość podstawową jest chroniona wewnętrznych i jest zadeklarowana w innym zestawie niż zestaw zawierający deklarację zastąpienia, deklarowana dostępność deklaracji zastąpienia jest chroniona. Jeśli właściwość dziedziczona ma tylko jedną metodę dostępu (tj. jeśli dziedziczona właściwość jest tylko do odczytu lub tylko do zapisu), właściwość zastępowania obejmuje tylko to akcesorium. Jeśli dziedziczona właściwość zawiera obie metody dostępu (tj. jeśli dziedziczona właściwość jest odczyt-zapis), właściwość zastępowania może zawierać jedno akcesorium lub oba metody dostępu. Istnieje konwersja tożsamości między typem zastępowania i dziedziczonej właściwości.
Zastępowanie deklaracji właściwości może zawierać sealed
modyfikator. Użycie tego modyfikatora uniemożliwia dalsze zastępowanie właściwości przez klasę pochodną. Akcesoria zapieczętowanej właściwości są również zapieczętowane.
Z wyjątkiem różnic w składni deklaracji i wywołania, wirtualnych, zapieczętowanych, przesłonięć i abstrakcyjnych metod zachowuje się dokładnie tak jak metody wirtualne, zapieczętowane, zastępowane i abstrakcyjne. W szczególności zasady opisane w §15.6.4, §15.6.5, §15.6.6 i §15.6.7 mają zastosowanie tak, jakby metody dostępu były odpowiednimi formami:
- Metoda get get odpowiada metodzie bez parametrów z wartością zwracaną typu właściwości i tymi samymi modyfikatorami, co właściwość zawierająca.
- Metoda dostępu zestawu odpowiada metodzie z pojedynczym parametrem wartości typu właściwości, typem zwracanym void i tymi samymi modyfikatorami, co właściwość zawierająca.
Przykład: w poniższym kodzie
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
jest wirtualną właściwością tylko do odczytu,Y
jest wirtualną właściwością odczytu i zapisu iZ
jest abstrakcyjną właściwością read-write. PonieważZ
jest abstrakcyjna, zawierająca klasę A również jest zadeklarowana abstrakcyjnie.Poniżej przedstawiono klasę pochodzącą z
A
klasy: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; } }
W tym miejscu deklaracje
X
,Y
iZ
zastępują deklaracje właściwości. Każda deklaracja właściwości dokładnie odpowiada modyfikatorom ułatwień dostępu, typowi i nazwie odpowiadającej jej właściwości dziedziczonej. Get accessor ofX
i set accessor of use the base keyword to access the inherited accessors (Uzyskiwanie dostępu do metody dostępu) i set accessor ofY
use the base keyword to access the inherited accessors (Uzyskiwanie dostępu do odziedziczonych metod dostępu). DeklaracjaZ
przesłonięć zarówno abstrakcyjnych metod dostępu — w związku z tym nie ma żadnych wybitnychabstract
składowych funkcji wB
systemie iB
może być klasą nie abstrakcyjną.przykład końcowy
Gdy właściwość jest zadeklarowana jako przesłonięć, wszelkie przesłonięte metody dostępu są dostępne dla kodu zastępowania. Ponadto deklarowana dostępność zarówno właściwości, jak i indeksatora, i akcesoriów, jest zgodna z elementem przesłoniętego elementu członkowskiego i akcesoriów.
Przykład:
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 } }
przykład końcowy
Zdarzenia 15.8
15.8.1 Ogólne
Zdarzenie jest elementem członkowskim, który umożliwia obiektowi lub klasie dostarczanie powiadomień. Klienci mogą dołączać kod wykonywalny dla zdarzeń, dostarczając programy obsługi zdarzeń.
Zdarzenia są deklarowane przy użyciu event_declarations:
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) jest dostępny tylko w niebezpiecznym kodzie (§23).
Event_declaration może zawierać zestaw atrybutów(§22) i dowolny z dozwolonych rodzajów deklarowanej dostępności (§15.3.6), new
(§15.3.5), static
(§15.6.3, §15.8.4), (virtual
§15.8.5), override
(§15.6.5, §15.8.5), sealed
(§15.6.6), abstract
(§15.6.7, §15.8.5) i extern
(§15.6.8) modyfikatory.
Deklaracje zdarzeń podlegają tym samym regułom co deklaracje metod (§15.6) w odniesieniu do prawidłowych kombinacji modyfikatorów.
Rodzaj deklaracji zdarzenia jest delegate_type (§8.2.8), a delegate_type jest co najmniej tak dostępny, jak samo wydarzenie (§7.5.5).
Deklaracja zdarzenia może zawierać event_accessor_declarations. Jeśli jednak nie, w przypadku zdarzeń, które nie są extern ani abstrakcyjne, kompilator dostarcza je automatycznie (§15.8.2); w przypadku zdarzeń extern
metody dostępu są udostępniane zewnętrznie.
Deklaracja zdarzenia, która pomija event_accessor_declarationdefiniuje co najmniej jedno zdarzenie — jedno dla każdego variable_declarator s. Atrybuty i modyfikatory mają zastosowanie do wszystkich elementów członkowskich zadeklarowanych przez taki event_declaration.
Jest to błąd czasu kompilacji dla event_declaration dołączania zarówno abstract
modyfikatora, jak i event_accessor_declarations.
Gdy deklaracja zdarzenia zawiera extern
modyfikator, zdarzenie jest mówi się, że zdarzenie jest zdarzeniem zewnętrznym. Ponieważ deklaracja zdarzenia zewnętrznego nie zawiera rzeczywistej implementacji, jest to błąd, który zawiera zarówno extern
modyfikator, jak i event_accessor_declarations.
Jest to błąd czasu kompilacji dla variable_declarator deklaracji zdarzenia z modyfikatorem abstract
lub external
w celu uwzględnienia variable_initializer.
Zdarzenie może być używane jako lewy operand operatorów +=
i -=
. Te operatory są używane odpowiednio do dołączania programów obsługi zdarzeń do programu lub usuwania programów obsługi zdarzeń ze zdarzenia oraz modyfikatorów dostępu do zdarzeń kontrolują konteksty, w których takie operacje są dozwolone.
Jedynymi operacjami, które są dozwolone na zdarzeniu przez kod, który znajduje się poza typem, w którym to zdarzenie jest zadeklarowane, to +=
i -=
. W związku z tym, mimo że taki kod może dodawać i usuwać programy obsługi dla zdarzenia, nie może on bezpośrednio uzyskać ani zmodyfikować źródłowej listy programów obsługi zdarzeń.
W operacji formularza x += y
lub , gdy x –= y
jest zdarzeniem wynik operacji ma typ x
(§12.21.5void
w przeciwieństwie do typu , z wartością x
po przypisaniu, jak w przypadku innych x
operatorów i +=
zdefiniowanych w typach -=
innych niż zdarzenia). Zapobiega to pośredniemu zbadaniu bazowego delegata zdarzenia przez kod zewnętrzny.
Przykład: W poniższym przykładzie pokazano, jak programy obsługi zdarzeń są dołączane do wystąpień
Button
klasy: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 } }
LoginDialog
W tym miejscu konstruktor wystąpienia tworzy dwaButton
wystąpienia i dołącza programy obsługi zdarzeń do zdarzeńClick
.przykład końcowy
15.8.2 Zdarzenia podobne do pól
W tekście programu klasy lub struktury, która zawiera deklarację zdarzenia, niektóre zdarzenia mogą być używane jak pola. Do wykorzystania w ten sposób zdarzenie nie jest abstrakcyjne ani extern, a nie uwzględnia jawnie event_accessor_declarations. Takie zdarzenie może być używane w dowolnym kontekście, który zezwala na pole. Pole zawiera delegata (§20), który odwołuje się do listy procedur obsługi zdarzeń, które zostały dodane do zdarzenia. Jeśli nie dodano żadnych procedur obsługi zdarzeń, pole zawiera null
wartość .
Przykład: w poniższym kodzie
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
jest używany jako pole wButton
klasie . Jak pokazano w przykładzie, pole można zbadać, zmodyfikować i użyć w wyrażeniach wywołania delegata. MetodaOnClick
wButton
klasie "wywołuje"Click
zdarzenie. Pojęcie wywoływania zdarzenia jest dokładnie równoważne wywołaniu delegata reprezentowanego przez zdarzenie — w związku z tym nie ma specjalnych konstrukcji językowych do wywoływania zdarzeń. Należy pamiętać, że wywołanie delegata jest poprzedzone sprawdzaniem, które gwarantuje, że delegat nie ma wartości null i że sprawdzanie jest wykonywane na kopii lokalnej w celu zapewnienia bezpieczeństwa wątku.Poza deklaracją
Button
klasyClick
składowa może być używana tylko po lewej stronie operatorów+=
i–=
, jak wb.Click += new EventHandler(...);
dołącza delegata do listy
Click
wywołań zdarzenia iClick –= new EventHandler(...);
który usuwa delegata z listy
Click
wywołań zdarzenia.przykład końcowy
pl-PL: Podczas kompilowania zdarzenia przypominającego pole kompilator automatycznie tworzy miejsce do przechowywania delegata i tworzy akcesory dla zdarzenia, które dodają lub usuwają procedury obsługi zdarzeń w polu delegata. Operacje dodawania i usuwania są bezpieczne wątkami i mogą (ale nie są wymagane) być wykonywane podczas przechowywania blokady (§13.13System.Type
§12.8.18) dla zdarzenia statycznego.
Uwaga: W związku z tym deklaracja zdarzenia wystąpienia formularza:
class X { public event D Ev; }
należy skompilować coś równoważnego:
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 */ } } }
W klasie
X
odwołania do odwołania poEv
lewej stronie+=
operatorów i–=
powodują wywołanie metody dodawania i usuwania metod dostępu. Wszystkie inne odwołania doEv
elementu są kompilowane w celu odwołowania się do pola__Ev
ukrytego (§12.8.7). Nazwa "__Ev
" jest dowolna; ukryte pole może mieć dowolną nazwę lub żadną nazwę.notatka końcowa
15.8.3 Akcesory zdarzeń
Uwaga: Deklaracje zdarzeń zazwyczaj pomijają event_accessor_declarations, jak w powyższym przykładzie
Button
. Mogą one być na przykład uwzględniane, jeśli koszt magazynu jednego pola na zdarzenie nie jest akceptowalny. W takich przypadkach klasa może zawierać event_accessor_declarations i użyć mechanizmu prywatnego do przechowywania listy procedur obsługi zdarzeń. notatka końcowa
Event_accessor_declarations zdarzenia określają instrukcje wykonywalne skojarzone z dodawaniem i usuwaniem programów obsługi zdarzeń.
Deklaracje dostępu składają się z add_accessor_declaration i remove_accessor_declaration. Każda deklaracja dostępu składa się z dodawania lub usuwania tokenu, po którym następuje blok. Blok skojarzony z add_accessor_declaration określa instrukcje do wykonania po dodaniu programu obsługi zdarzeń, a blok skojarzony z remove_accessor_declaration określa instrukcje do wykonania po usunięciu programu obsługi zdarzeń.
Każda add_accessor_declaration i remove_accessor_declaration odpowiada metodzie z pojedynczym parametrem wartości typu zdarzenia i typem zwracanym void
. Niejawny parametr metody dostępu zdarzeń nosi nazwę value
. Gdy zdarzenie jest używane w przypisaniu zdarzeń, używane jest odpowiednie akcesorium zdarzenia. W szczególności, jeśli operator przypisania jest +=
używany, dodaj metodę dostępu, a jeśli operator przypisania jest –=
używany, jest używany metodę usuwania dostępu. W obu przypadkach prawy operand operatora przypisania jest używany jako argument metody dostępu zdarzenia. Blok add_accessor_declaration lub remove_accessor_declaration jest zgodny z zasadami dotyczącymi metod opisanych w §15.6.9.void
W szczególności return
instrukcje w takim bloku nie mogą określać wyrażenia.
Ponieważ akcesorium zdarzenia niejawnie ma parametr o nazwie value
, jest to błąd czasu kompilacji dla zmiennej lokalnej lub stałej zadeklarowanej w metodzie dostępu zdarzenia, aby mieć tę nazwę.
Przykład: w poniższym kodzie
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); } } }
Control
klasa implementuje wewnętrzny mechanizm przechowywania zdarzeń. MetodaAddEventHandler
kojarzy wartość delegata z kluczem,GetEventHandler
metoda zwraca delegata aktualnie skojarzonego z kluczem, aRemoveEventHandler
metoda usuwa delegata jako program obsługi zdarzeń dla określonego zdarzenia. Prawdopodobnie podstawowy mechanizm magazynu został zaprojektowany tak, aby nie istniał koszt skojarzenia wartości delegata o wartości null z kluczem, a tym samym nieobsługiwane zdarzenia nie zużywają miejsca do magazynowania.przykład końcowy
15.8.4 Zdarzenia statyczne i wystąpienia
Gdy deklaracja zdarzenia zawiera static
modyfikator, zdarzenie jest podobno zdarzeniem statycznym. Jeśli modyfikator nie static
jest obecny, zdarzenie jest mówi się, że jest zdarzeniem wystąpienia.
Zdarzenie statyczne nie jest skojarzone z konkretnym wystąpieniem i jest to błąd czasu kompilacji, który należy odwołać się do this
metody dostępu zdarzenia statycznego.
Zdarzenie wystąpienia jest skojarzone z danym wystąpieniem klasy, a do tego wystąpienia można uzyskać dostęp jako this
(§12.8.14) w akcesorach tego zdarzenia.
Różnice między elementami statycznymi i elementami członkowskimi wystąpień zostały omówione w temacie §15.3.8.
15.8.5 Wirtualne, zapieczętowane, zastępowane i abstrakcyjne metody dostępu
Deklaracja zdarzenia wirtualnego określa, że metody dostępu tego zdarzenia są wirtualne. Modyfikator virtual
ma zastosowanie do obu metod dostępu zdarzenia.
Abstrakcyjna deklaracja zdarzenia określa, że metody dostępu zdarzenia są wirtualne, ale nie zapewnia rzeczywistej implementacji metod dostępu. Zamiast tego klasy pochodne nie abstrakcyjne są wymagane do zapewnienia własnej implementacji dla metod dostępu przez zastąpienie zdarzenia. Ponieważ akcesorium do deklaracji zdarzeń abstrakcyjnych nie zapewnia rzeczywistej implementacji, nie dostarcza event_accessor_declarations.
Deklaracja zdarzenia, która zawiera zarówno abstract
modyfikatory, jak i override
określa, że zdarzenie jest abstrakcyjne i zastępuje zdarzenie podstawowe. Metody dostępu do takiego zdarzenia są również abstrakcyjne.
Deklaracje zdarzeń abstrakcyjnych są dozwolone tylko w klasach abstrakcyjnych (§15.2.2.2).
Metody dostępu dziedziczonego zdarzenia wirtualnego mogą zostać zastąpione w klasie pochodnej przez dołączenie deklaracji zdarzenia, która określa override
modyfikator. Jest to nazywane zastępowaniem deklaracji zdarzenia. Zastępowanie deklaracji zdarzenia nie deklaruje nowego zdarzenia. Zamiast tego po prostu specjalizuje się w implementacjach akcesoriów istniejącego zdarzenia wirtualnego.
Deklaracja zdarzenia zastępowania określa dokładnie te same modyfikatory ułatwień dostępu i nazwę co zdarzenie zastępowane. W deklaracji należy określić konwersję tożsamości między typem przesłonięcia a przesłoniętą zdarzeniem, a zarówno dodatkiem, jak i usuwaniem metod dostępu.
Zastępowanie deklaracji zdarzenia może obejmować sealed
modyfikator.
this
Użycie modyfikatora uniemożliwia dalsze zastępowanie zdarzenia przez klasę pochodną. Akcesoria zapieczętowanego zdarzenia są również zapieczętowane.
Jest to błąd czasu kompilacji dla przesłaniającej deklaracji zdarzenia w celu uwzględnienia new
modyfikatora.
Z wyjątkiem różnic w składni deklaracji i wywołania, wirtualnych, zapieczętowanych, przesłonięć i abstrakcyjnych metod zachowuje się dokładnie tak jak metody wirtualne, zapieczętowane, zastępowane i abstrakcyjne. W szczególności zasady opisane w §15.6.4, §15.6.5, §15.6.6 i §15.6.7 mają zastosowanie tak, jakby metody dostępu były odpowiednimi formami. Każda metoda dostępu odpowiada metodzie z pojedynczym parametrem wartości typu zdarzenia, typem zwracanym void
i tymi samymi modyfikatorami co zawierające zdarzenie.
Indeksatory 15.9
15.9.1 Ogólne
Indeksator jest elementem członkowskim, który umożliwia indeksowanie obiektu w taki sam sposób jak tablica. Indeksatory są deklarowane przy użyciu indexer_declarations:
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) jest dostępny tylko w niebezpiecznym kodzie (§23).
Istnieją dwa rodzaje indexer_declaration:
- Pierwszy deklaruje indeksator nienależący do wartości ref. Jego wartość ma typ typu. Ten rodzaj indeksatora może być czytelny i/lub zapisywalny.
- Drugi deklaruje indeksator o wartości ref. Jego wartość to variable_reference (§9.5
readonly
Ten rodzaj indeksatora jest dostępny tylko do odczytu.
Indexer_declaration może zawierać zestaw atrybutów (§22) oraz dowolny z dozwolonych rodzajów zadeklarowanych ułatwień dostępu (§15.3.6), new
(§15.3.5), (§15.5). virtual
6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7) i extern
(§15.6.8) modyfikatory.
Deklaracje indeksatora podlegają tym samym regułom co deklaracje metod (§15.6) w odniesieniu do prawidłowych kombinacji modyfikatorów, z jednym wyjątkiem jest to, że static
modyfikator nie jest dozwolony w deklaracji indeksatora.
Typ deklaracji indeksatora określa typ elementu indeksatora wprowadzonego przez deklarację.
Uwaga: Ponieważ indeksatory są przeznaczone do użycia w kontekstach przypominających element tablicy, termin typ elementu zdefiniowany dla tablicy jest również używany z indeksatorem. notatka końcowa
Chyba że indeksator jest jawną implementacją elementu członkowskiego interfejsu, po którym następuje słowo kluczowe this
. W przypadku implementacji jawnego elementu członkowskiego interfejsu typ następuje interface_type, "" i słowo kluczowe .
.this
W przeciwieństwie do innych elementów członkowskich indeksatory nie mają nazw zdefiniowanych przez użytkownika.
Parameter_list określa parametry indeksatora. Lista parametrów indeksatora odpowiada tej metodzie (§15.6.2), z tą różnicą, że określono co najmniej jeden parametr i że this
modyfikatory parametrów , ref
i out
nie są dozwolone.
Typ indeksatora i każdy z typów, do których odwołuje się parameter_list, jest co najmniej tak dostępny, jak sam indeksator (§7.5.5).
Indexer_body może składać się z treści instrukcji (§15.7.1) lub treści wyrażenia (§15.6.1). W treści instrukcji, accessor_declarations, które są ujęte w tokeny "{
" i "}
", deklarują akcesoriów (§15.7.3) indeksatora. Metody dostępu określają instrukcje wykonywalne skojarzone z odczytywaniem i zapisywaniem elementów indeksatora.
W indexer_body treści wyrażenia składającej się z wyrazu "=>
", po którym następuje wyrażenieE
, a średnik jest dokładnie odpowiednikiem treści { get { return E; } }
instrukcji , i dlatego może służyć tylko do określania indeksatorów tylko do odczytu, gdzie wynik metody uzyskiwania dostępu jest podawany przez pojedyncze wyrażenie.
Ref_indexer_body może składać się z treści instrukcji lub treści wyrażenia. W treści instrukcji get_accessor_declaration deklaruje metodę get (§15.7.3) indeksatora. Metodę dostępu określa instrukcje wykonywalne skojarzone z odczytywaniem indeksatora.
W ref_indexer_body treści wyrażenia składającej =>
się z elementu ref
, variable_referenceV
i średnik jest dokładnie odpowiednikiem treści { get { return ref V; } }
instrukcji .
Uwaga: mimo że składnia uzyskiwania dostępu do elementu indeksatora jest taka sama jak w przypadku elementu tablicy, element indeksatora nie jest klasyfikowany jako zmienna. W związku z tym nie można przekazać elementu indeksatora jako argumentu
in
,out
lubref
, chyba że indeksator jest ref-valued i dlatego zwraca odwołanie (§9.7). notatka końcowa
Parameter_list indeksatora definiuje podpis (§7.6) indeksatora. W szczególności podpis indeksatora składa się z liczby i typów jego parametrów. Typ elementu i nazwy parametrów nie są częścią podpisu indeksatora.
Podpis indeksatora różni się od podpisów wszystkich innych indeksatorów zadeklarowanych w tej samej klasie.
Gdy deklaracja indeksatora zawiera extern
modyfikator, indeksator jest podobno indeksatorem zewnętrznym. Ponieważ deklaracja indeksatora zewnętrznego nie zapewnia rzeczywistej implementacji, każda z accessor_bodys w jej accessor_declarations jest średnikiem.
Przykład: Poniższy przykład deklaruje klasę
BitArray
, która implementuje indeksator na potrzeby uzyskiwania dostępu do poszczególnych bitów w tablicy bitowej.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); } } } }
Wystąpienie
BitArray
klasy zużywa znacznie mniej pamięci niż odpowiadającąbool[]
mu wartość (ponieważ każda wartość poprzedniego zajmuje tylko jeden bit zamiast jednegobyte
z nich), ale zezwala na te same operacje cobool[]
.W poniższej
CountPrimes
klasie użyto algorytmu iBitArray
klasycznego "sita" do obliczenia liczby pierwszych z zakresu od 2 do podanej wartości maksymalnej: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}"); } }
Należy pamiętać, że składnia uzyskiwania dostępu do elementów elementu
BitArray
jest dokładnie taka sama jak w przypadku elementubool[]
.W poniższym przykładzie przedstawiono klasę siatki 26×10 zawierającą indeksator z dwoma parametrami. Pierwszy parametr jest wymagany do wielkich lub małych liter w zakresie A–Z, a drugi jest wymagany jako liczba całkowita w zakresie od 0 do 9.
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; } } }
przykład końcowy
15.9.2 Indeksator i różnice właściwości
Indeksatory i właściwości są bardzo podobne w koncepcji, ale różnią się w następujący sposób:
- Właściwość jest identyfikowana przez jego nazwę, podczas gdy indeksator jest identyfikowany przez jego podpis.
- Dostęp do właściwości można uzyskać za pośrednictwem simple_name (§12.8.4) lub member_access (§12.8.7), natomiast dostęp do elementu indeksatora odbywa się za pośrednictwem element_access (§12.8.12.3).
- Właściwość może być statycznym elementem członkowskim, natomiast indeksator jest zawsze członkiem wystąpienia.
- Metoda get dostępu do właściwości odpowiada metodzie bez parametrów, natomiast metoda get dostępu indeksatora odpowiada metodzie z tą samą listą parametrów co indeksator.
- Metoda dostępu zestawu odpowiada metodzie z pojedynczym parametrem o nazwie
value
, natomiast metoda dostępu zestawu indeksatora odpowiada metodzie z tą samą listą parametrów co indeksator, a także dodatkowy parametr o nazwievalue
. - Jest to błąd czasu kompilacji dla metody dostępu indeksatora, aby zadeklarować zmienną lokalną lub stałą lokalną o takiej samej nazwie jak parametr indeksatora.
- W deklaracji właściwości zastępowania dostęp do dziedziczonej właściwości jest uzyskiwany przy użyciu składni
base.P
, gdzieP
jest nazwą właściwości. W deklaracji indeksatora zastępowania dziedziczony indeksator jest uzyskiwany przy użyciu składnibase[E]
, gdzieE
jest rozdzielaną przecinkami listą wyrażeń. - Nie ma pojęcia "automatycznie zaimplementowanego indeksatora". Błąd polega na tym, że indeksator nie jest abstrakcyjny, a nie zewnętrzny z średnikami accessor_bodys.
Oprócz tych różnic, wszystkie reguły zdefiniowane w §15.7.3, §15.7.5 i §15.7.6 mają zastosowanie do akcesoriów indeksatora, a także akcesoriów do właściwości.
Zastąpienie właściwości/właściwości indeksatorem/indeksatorami podczas odczytywania §15.7.3, §15.7.5 i §15.7.6 dotyczy również zdefiniowanych terminów. W szczególności właściwość read-write staje się indeksatorem tylko do odczytu, właściwość tylko do odczytu staje się indeksatorem tylko do odczytu, a właściwość tylko do zapisu staje się indeksatorem tylko do zapisu.
Operatory 15.10
15.10.1 Ogólne
Operator jest elementem członkowskim, który definiuje znaczenie operatora wyrażenia, który można zastosować do wystąpień klasy. Operatory są deklarowane przy użyciu operator_declarations:
operator_declaration
: attributes? operator_modifier+ operator_declarator operator_body
;
operator_modifier
: 'public'
| 'static'
| 'extern'
| unsafe_modifier // unsafe code support
;
operator_declarator
: unary_operator_declarator
| binary_operator_declarator
| conversion_operator_declarator
;
unary_operator_declarator
: type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
;
logical_negation_operator
: '!'
;
overloadable_unary_operator
: '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
;
binary_operator_declarator
: type 'operator' overloadable_binary_operator
'(' fixed_parameter ',' fixed_parameter ')'
;
overloadable_binary_operator
: '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '<<'
| right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
;
conversion_operator_declarator
: 'implicit' 'operator' type '(' fixed_parameter ')'
| 'explicit' 'operator' type '(' fixed_parameter ')'
;
operator_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).
Uwaga: Operatory negacji logicznej prefiksu (§12.9.4) i postfix null-forgiving (§12.8.9), reprezentowane przez ten sam token leksykacyjny (!
), są odrębne. Ten ostatni nie jest operatorem przeciążalnym.
notatka końcowa
Istnieją trzy kategorie operatorów przeciążalnych: operatory jednoargumentowe (§15.10.2), operatory binarne (§15.10.3) i operatory konwersji (§15.10.4).
Operator_body to średnik, treść bloku (§15.6.1) lub treść wyrażenia (§15.6.1). Treść bloku składa się z bloku, który określa instrukcje do wykonania po wywołaniu operatora. Blok jest zgodny z zasadami metod zwracania wartości opisanych w §15.6.11. Treść wyrażenia składa się z następującego =>
po nim wyrażenia i średnika oraz oznacza pojedyncze wyrażenie do wykonania po wywołaniu operatora.
W przypadku extern
operatorów operator_body składa się po prostu ze średnika. W przypadku wszystkich innych operatorów operator_body jest treścią bloku lub treścią wyrażenia.
Następujące reguły mają zastosowanie do wszystkich deklaracji operatorów:
- Deklaracja operatora zawiera zarówno element, jak
public
istatic
modyfikator. - Parametry operatora nie mają żadnych modyfikatorów innych niż
in
. - Podpis operatora (§15.10.2, §15.10.3, §15.10.4) różni się od podpisów wszystkich innych operatorów zadeklarowanych w tej samej klasie.
- Wszystkie typy, do których odwołuje się deklaracja operatora, są co najmniej tak dostępne, jak sam operator (§7.5.5).
- Jest to błąd dla tego samego modyfikatora, który pojawia się wiele razy w deklaracji operatora.
Każda kategoria operatorów nakłada dodatkowe ograniczenia, zgodnie z opisem w poniższych podklasach.
Podobnie jak inne elementy członkowskie, operatory zadeklarowane w klasie bazowej są dziedziczone przez klasy pochodne. Ponieważ deklaracje operatorów zawsze wymagają klasy lub struktury, w której operator jest zadeklarowany do udziału w podpisie operatora, nie jest możliwe, aby operator zadeklarowany w klasie pochodnej ukrywał operator zadeklarowany w klasie bazowej.
new
W związku z tym modyfikator nigdy nie jest wymagany i dlatego nigdy nie jest dozwolony w deklaracji operatora.
Dodatkowe informacje na temat operatorów jednoargumentowych i binarnych można znaleźć w §12.4.
Dodatkowe informacje na temat operatorów konwersji można znaleźć w §10.5.
15.10.2 Operatory jednoargumentowe
Następujące reguły dotyczą deklaracji operatorów jednoargumentowych, gdzie T
określa typ wystąpienia klasy lub struktury zawierającej deklarację operatora:
- Jednoargumentowy
+
, (-
!
tylko negacja logiczna) lub~
operator musi przyjmować jeden parametr typuT
lubT?
może zwrócić dowolny typ. - Jednoargumentowy
++
lub--
operator bierze jeden parametr typuT
lubT?
zwraca ten sam typ lub typ pochodzący z niego. - Jednoargumentowy
true
lubfalse
operator ma jeden parametr typuT
lubT?
zwraca typbool
.
Podpis operatora jednoargumentowego składa się z tokenu operatora (+
, -
, !
~
++
--
true
lub false
) i typu pojedynczego parametru. Zwracany typ nie jest częścią podpisu operatora jednoargumentowego ani nie jest nazwą parametru.
Operatory true
jednoargumentowe i false
wymagają deklaracji pary. Błąd czasu kompilacji występuje, jeśli klasa deklaruje jeden z tych operatorów bez deklarowania drugiego. Operatory true
i false
zostały opisane dalej w §12.24.
Przykład: Poniższy przykład przedstawia implementację i kolejne użycie operatora++ dla klasy wektorów całkowitych:
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 } }
Zwróć uwagę, jak metoda operatora zwraca wartość wygenerowaną przez dodanie wartości 1 do operandu, podobnie jak operatory przyrostka i dekrementacji (§12.8.16) oraz operatory przyrostu i dekrementacji (§12.9.6). W przeciwieństwie do języka C++, ta metoda nie powinna modyfikować wartości operandu bezpośrednio, ponieważ naruszałoby to standardową semantyka operatora przyrostka (§12.8.16).
przykład końcowy
15.10.3 Operatory binarne
Następujące reguły dotyczą deklaracji operatorów binarnych, gdzie T
określa typ wystąpienia klasy lub struktury zawierającej deklarację operatora:
- Operator binarny bez zmian musi przyjmować dwa parametry, z których co najmniej jeden ma typ
T
lubT?
, i może zwrócić dowolny typ. - Operator binarny lub
<<
operator>>
(§12.11) ma dwa parametry, z których pierwszy ma typ lub T? i drugi z nich ma typT
int
lubint?
, i może zwrócić dowolny typ.
Podpis operatora binarnego składa się z tokenu operatora (+
, -
, *
/
%
&
|
^
<<
>>
==
!=
>
<
>=
lub <=
) i typów dwóch parametrów. Zwracany typ i nazwy parametrów nie są częścią podpisu operatora binarnego.
Niektóre operatory binarne wymagają deklaracji pary. Dla każdej deklaracji jednego operatora pary musi istnieć zgodna deklaracja innego operatora pary. Dwa deklaracje operatorów są zgodne, jeśli istnieją konwersje tożsamości między ich typami zwracania i odpowiadającymi im typami parametrów. Następujące operatory wymagają deklaracji pary:
- operator
==
i operator!=
- operator
>
i operator<
- operator
>=
i operator<=
15.10.4 Operatory konwersji
Deklaracja operatora konwersji wprowadza konwersję zdefiniowaną przez użytkownika (§10.5), która rozszerza wstępnie zdefiniowane niejawne i jawne konwersje.
Deklaracja operatora konwersji zawierająca implicit
słowo kluczowe wprowadza zdefiniowaną przez użytkownika niejawną konwersję. Niejawne konwersje mogą wystąpić w różnych sytuacjach, w tym wywołania składowych funkcji, wyrażenia rzutowania i przypisania. Opisano to dalej w §10.2.
Deklaracja operatora konwersji zawierająca explicit
słowo kluczowe wprowadza jawną konwersję zdefiniowaną przez użytkownika. Jawne konwersje mogą występować w wyrażeniach rzutowanych i są opisane dalej w §10.3.
Operator konwersji konwertuje z typu źródła wskazanego przez typ parametru operatora konwersji na typ docelowy wskazany przez typ zwracany operatora konwersji.
Dla danego typu źródłowego i typu S
T
docelowego , jeśli S
lub T
są typami wartości dopuszczania wartości null, niech S₀
i T₀
odwołują się do ich typów bazowych; w przeciwnym razie S₀
i T₀
są równe S
i T
odpowiednio. Klasa lub struktura może zadeklarować konwersję z typu źródłowego na typ S
T
docelowy tylko wtedy, gdy spełnione są wszystkie następujące elementy:
S₀
iT₀
są różnymi typami.Albo
S₀
T₀
jest typem wystąpienia klasy lub struktury zawierającej deklarację operatora.Ani nie
S₀
T₀
jest interface_type.Wykluczanie konwersji zdefiniowanych przez użytkownika, konwersja nie istnieje z
S
doT
lub zT
doS
.
Na potrzeby tych reguł wszelkie parametry typu skojarzone z S
lub T
są uważane za unikatowe typy, które nie mają relacji dziedziczenia z innymi typami, a wszelkie ograniczenia dotyczące tych parametrów typu są ignorowane.
Przykład: w następujących kwestiach:
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 }
pierwsze dwie deklaracje operatorów są dozwolone, ponieważ
T
iint
istring
, są uznawane za unikatowe typy bez relacji. Jednak trzeci operator jest błędem, ponieważC<T>
jest klasą bazową klasyD<T>
.przykład końcowy
Z drugiej reguły wynika, że operator konwersji konwertuje na lub z klasy lub typu struktury, w którym jest zadeklarowany operator.
Przykład: istnieje możliwość, aby klasa lub typ
C
struktury zdefiniować konwersję zC
do i odint
doint
C
, ale nie zint
dobool
. przykład końcowy
Nie można bezpośrednio ponownie zdefiniować wstępnie zdefiniowanej konwersji. W związku z tym operatory konwersji nie mogą konwertować z lub na object
, ponieważ jawne i niejawne konwersje już istnieją między object
wszystkimi innymi typami. Podobnie ani źródło, ani typy docelowe konwersji nie mogą być podstawowym typem drugiej, ponieważ konwersja już istnieje.
Można jednak zadeklarować operatory dla typów ogólnych, które dla określonych argumentów typu określają konwersje, które już istnieją jako wstępnie zdefiniowane konwersje.
Przykład:
struct Convertible<T> { public static implicit operator Convertible<T>(T value) {...} public static explicit operator T(Convertible<T> value) {...} }
gdy typ
object
jest określony jako argument typu dlaT
, drugi operator deklaruje konwersję, która już istnieje (niejawna, a tym samym jawna, istnieje konwersja z dowolnego typu na obiekt typu).przykład końcowy
W przypadkach, gdy istnieje wstępnie zdefiniowana konwersja między dwoma typami, wszelkie konwersje zdefiniowane przez użytkownika między tymi typami są ignorowane. Szczególnie:
- Jeśli wstępnie zdefiniowana niejawna konwersja (§10.2
T
niejawne lub jawne) zS
doT
są ignorowane. - Jeśli wstępnie zdefiniowana jawna konwersja (§10.3
T
Ponadto:- Jeśli typ
S
interfejsu lubT
jest typem interfejsu, niejawne konwersje zdefiniowane przez użytkownika zS
doT
są ignorowane. - W przeciwnym razie konwersje niejawne zdefiniowane przez użytkownika z
S
doT
są nadal brane pod uwagę.
- Jeśli typ
Dla wszystkich typów, ale object
operatory zadeklarowane przez Convertible<T>
typ powyżej nie powodują konfliktu ze wstępnie zdefiniowanymi konwersjami.
Przykład:
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 }
Jednak w przypadku typów
object
konwersje wstępnie zdefiniowane ukrywają konwersje zdefiniowane przez użytkownika we wszystkich przypadkach, ale jedno: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 }
przykład końcowy
Konwersje zdefiniowane przez użytkownika nie mogą być konwertowane z lub na interface_types. W szczególności to ograniczenie gwarantuje, że podczas konwersji na interface_type nie wystąpią żadne przekształcenia zdefiniowane przez użytkownika, a konwersja na interface_type powiedzie się tylko wtedy, gdy object
konwersja rzeczywiście implementuje określony interface_type.
Podpis operatora konwersji składa się z typu źródłowego i typu docelowego. (Jest to jedyna forma elementu członkowskiego, dla którego zwracany typ uczestniczy w podpisie). Niejawna lub jawna klasyfikacja operatora konwersji nie jest częścią podpisu operatora. W związku z tym klasa lub struktura nie może zadeklarować zarówno niejawnego, jak i jawnego operatora konwersji z tymi samymi typami źródłowymi i docelowymi.
Uwaga: Ogólnie rzecz biorąc, niejawne konwersje zdefiniowane przez użytkownika powinny być zaprojektowane tak, aby nigdy nie zgłaszać wyjątków i nigdy nie tracić informacji. Jeśli konwersja zdefiniowana przez użytkownika może spowodować powstanie wyjątków (na przykład dlatego, że argument źródłowy jest poza zakresem) lub utrata informacji (takich jak odrzucanie bitów o wysokiej kolejności), ta konwersja powinna być zdefiniowana jako jawna konwersja. notatka końcowa
Przykład: w poniższym kodzie
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); }
konwersja z
Digit
na jest niejawna, ponieważ nigdy nie zgłasza wyjątków lub traci informacje, ale konwersja zbyte
nabyte
jest jawna, ponieważDigit
może reprezentować tylko podzbiór możliwych wartościDigit
byte
.przykład końcowy
15.11 Konstruktory wystąpień
15.11.1 Ogólne
Konstruktor wystąpienia jest elementem członkowskim, który implementuje akcje wymagane do zainicjowania wystąpienia klasy. Konstruktory wystąpień są deklarowane przy użyciu constructor_declarations:
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) jest dostępny tylko w niebezpiecznym kodzie (§23).
Constructor_declaration może zawierać zestaw atrybutów (§22), dowolny z dozwolonych rodzajów zadeklarowanej dostępności (§15.3.6) i extern
modyfikator (§15.6.8). Deklaracja konstruktora nie może zawierać tego samego modyfikatora wiele razy.
Identyfikator constructor_declarator określa klasę, w której zadeklarowany jest konstruktor wystąpienia. Jeśli zostanie określona inna nazwa, wystąpi błąd czasu kompilacji.
Opcjonalne parameter_list konstruktora wystąpienia podlega tym samym regułom co parameter_list metody (§15.6).
this
Ponieważ modyfikator parametrów ma zastosowanie tylko do metod rozszerzeń (§15.6.10), żaden parametr w parameter_list konstruktora nie zawiera this
modyfikatora. Lista parametrów definiuje podpis (§7.6) konstruktora wystąpienia i zarządza procesem, w którym rozpoznawanie przeciążenia (§12.6.4) wybiera konkretny konstruktor wystąpienia w wywołaniu.
Każdy z typów, do których odwołuje się parameter_list konstruktora wystąpienia, jest co najmniej tak dostępny, jak sam konstruktor (§7.5.5).
Opcjonalny constructor_initializer określa inny konstruktor wystąpienia do wywołania przed wykonaniem instrukcji podanych w constructor_body tego konstruktora wystąpienia. Opisano to dalej w §15.11.2.
Gdy deklaracja konstruktora zawiera extern
modyfikator, konstruktor jest podobno konstruktorem zewnętrznym. Ponieważ deklaracja konstruktora zewnętrznego nie zawiera rzeczywistej implementacji, jego constructor_body składa się z średnika. W przypadku wszystkich innych konstruktorów constructor_body składa się z jednego z następujących elementów:
- blok, który określa instrukcje inicjowania nowego wystąpienia klasy; lub
- treść wyrażenia, która składa się z następującego
=>
po nim wyrażenia i średnika, i określa pojedyncze wyrażenie, aby zainicjować nowe wystąpienie klasy.
Constructor_body, który jest treścią bloku lub wyrażenia, odpowiada dokładnie blokowi metody wystąpienia z typem zwracanym void
(§15.6.11).
Konstruktory wystąpień nie są dziedziczone. W związku z tym klasa nie ma konstruktorów wystąpień innych niż te rzeczywiście zadeklarowane w klasie, z wyjątkiem, że jeśli klasa nie zawiera deklaracji konstruktora wystąpienia, domyślny konstruktor wystąpienia jest udostępniany automatycznie (§15.11.5).
Konstruktory wystąpień są wywoływane przez object_creation_expression s (§12.8.17.2) i przez constructor_initializers.
Inicjatory konstruktorów 15.11.2
Wszystkie konstruktory wystąpień (z wyjątkiem tych dla klasy object
) niejawnie obejmują wywołanie innego konstruktora wystąpienia bezpośrednio przed constructor_body. Konstruktor, który ma być niejawnie wywoływany, jest określany przez constructor_initializer:
- Inicjator konstruktora wystąpienia formularza
base(
argument_list)
(gdzie argument_list jest opcjonalny) powoduje wywołanie konstruktora wystąpienia z bezpośredniej klasy bazowej. Ten konstruktor jest wybierany przy użyciu argument_list i reguł rozpoznawania przeciążenia §12.6.4. Zestaw konstruktorów wystąpienia kandydata składa się ze wszystkich dostępnych konstruktorów wystąpień bezpośrednich klasy bazowej. Jeśli ten zestaw jest pusty lub nie można zidentyfikować jednego najlepszego konstruktora wystąpienia, wystąpi błąd czasu kompilacji. - Inicjator konstruktora wystąpienia formularza
this(
argument_list)
(gdzie argument_list jest opcjonalny) wywołuje inny konstruktor wystąpienia z tej samej klasy. Konstruktor jest wybierany przy użyciu argument_list i reguł rozpoznawania przeciążenia §12.6.4. Zestaw konstruktorów wystąpienia kandydata składa się ze wszystkich konstruktorów wystąpień zadeklarowanych w samej klasie. Jeśli wynikowy zestaw odpowiednich konstruktorów wystąpień jest pusty lub nie można zidentyfikować jednego najlepszego konstruktora wystąpienia, wystąpi błąd czasu kompilacji. Jeśli deklaracja konstruktora wystąpienia wywołuje się za pośrednictwem łańcucha co najmniej jednego inicjatora konstruktora, wystąpi błąd czasu kompilacji.
Jeśli konstruktor wystąpienia nie ma inicjatora konstruktora, inicjator konstruktora formularza base()
jest niejawnie udostępniany.
Uwaga: W związku z tym deklaracja konstruktora wystąpienia formularza
C(...) {...}
jest dokładnie równoważne
C(...) : base() {...}
notatka końcowa
Zakres parametrów podanych przez parameter_list deklaracji konstruktora wystąpienia obejmuje inicjator konstruktora tej deklaracji. W związku z tym inicjator konstruktora może uzyskać dostęp do parametrów konstruktora.
Przykład:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y) : base(x + y, x - y) {} }
przykład końcowy
Inicjator konstruktora wystąpienia nie może uzyskać dostępu do tworzonego wystąpienia. W związku z tym jest to błąd czasu kompilacji, aby odwołać się do tego w wyrażeniu argumentu inicjatora konstruktora, ponieważ jest to błąd czasu kompilacji dla wyrażenia argumentu w celu odwołania się do dowolnego elementu członkowskiego wystąpienia za pośrednictwem simple_name.
Inicjatory zmiennych wystąpień 15.11.3
Gdy konstruktor wystąpienia nie ma inicjatora konstruktora lub ma inicjator konstruktora formularza base(...)
, konstruktor niejawnie wykonuje inicjalizacji określone przez variable_initializers pól wystąpienia zadeklarowanych w swojej klasie. Odpowiada to sekwencji przypisań, które są wykonywane natychmiast po wejściu do konstruktora i przed niejawnym wywołaniem bezpośredniego konstruktora klasy bazowej. Inicjatory zmiennych są wykonywane w kolejności tekstowej, w której są wyświetlane w deklaracji klasy (§15.5.6).
15.11.4 Wykonywanie konstruktora
Inicjatory zmiennych są przekształcane w instrukcje przypisania, a te instrukcje przypisania są wykonywane przed wywołaniem konstruktora wystąpienia klasy bazowej. Ta kolejność gwarantuje, że wszystkie pola wystąpień są inicjowane przez inicjatory zmiennych przed wykonaniem wszelkich instrukcji, które mają dostęp do tego wystąpienia.
Przykład: biorąc pod uwagę następujące kwestie:
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}"); }
gdy nowe
B()
jest używane do tworzenia wystąpieniaB
programu , generowane są następujące dane wyjściowe:x = 1, y = 0
Wartość to
x
1, ponieważ inicjator zmiennej jest wykonywany przed wywołaniem konstruktora wystąpienia klasy bazowej. Jednak wartośćy
to 0 (wartość domyślna klasyint
), ponieważ przypisanie doy
nie jest wykonywane dopiero po powrocie konstruktora klasy bazowej. Warto traktować inicjatory zmiennych wystąpień i inicjatory konstruktorów jako instrukcje, które są automatycznie wstawiane przed constructor_body. Przykładclass 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; } }
zawiera kilka inicjatorów zmiennych; zawiera również inicjatory konstruktorów obu formularzy (
base
ithis
). Przykład odpowiada kodowi pokazanym poniżej, gdzie każdy komentarz wskazuje automatycznie wstawioną instrukcję (składnia używana dla automatycznie wstawionych wywołań konstruktorów jest nieprawidłowa, ale służy tylko do zilustrowania mechanizmu).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; } }
przykład końcowy
15.11.5 Konstruktory domyślne
Jeśli klasa nie zawiera deklaracji konstruktora wystąpienia, zostanie automatycznie udostępniony domyślny konstruktor wystąpienia. Ten domyślny konstruktor po prostu wywołuje konstruktor bezpośredniej klasy bazowej, jakby miał inicjator konstruktora formularza base()
. Jeśli klasa jest abstrakcyjna, zadeklarowana dostępność dla konstruktora domyślnego jest chroniona. W przeciwnym razie zadeklarowane ułatwienia dostępu dla konstruktora domyślnego są publiczne.
Uwaga: W związku z tym domyślny konstruktor jest zawsze w formularzu
protected C(): base() {}
lub
public C(): base() {}
gdzie
C
to nazwa klasy.notatka końcowa
Jeśli rozpoznawanie przeciążenia nie może określić unikatowego najlepszego kandydata do inicjatora konstruktora klasy bazowej, wystąpi błąd czasu kompilacji.
Przykład: w poniższym kodzie
class Message { object sender; string text; }
Jest dostarczany konstruktor domyślny, ponieważ klasa nie zawiera deklaracji konstruktora wystąpienia. W związku z tym przykład jest dokładnie odpowiednikiem
class Message { object sender; string text; public Message() : base() {} }
przykład końcowy
15.12 Konstruktory statyczne
Konstruktor statyczny jest elementem członkowskim, który implementuje akcje wymagane do zainicjowania zamkniętej klasy. Konstruktory statyczne są deklarowane przy użyciu static_constructor_declarations:
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) jest dostępny tylko w niebezpiecznym kodzie (§23).
Static_constructor_declaration może zawierać zestaw atrybutów(§22) i extern
modyfikator (§15.6.8).
Identyfikator static_constructor_declaration określa klasę, w której zadeklarowany jest konstruktor statyczny. Jeśli zostanie określona inna nazwa, wystąpi błąd czasu kompilacji.
Gdy deklaracja konstruktora statycznego zawiera extern
modyfikator, konstruktor statyczny jest podobno zewnętrznym konstruktorem statycznym. Ponieważ zewnętrzna deklaracja konstruktora statycznego nie zapewnia rzeczywistej implementacji, jego static_constructor_body składa się z średnika. W przypadku wszystkich innych deklaracji konstruktorów statycznych static_constructor_body składa się z jednego z następujących elementów:
- blok, który określa instrukcje do wykonania w celu zainicjowania klasy; lub
- treść wyrażenia, która składa się z
=>
wyrażenia i średnika, i określa pojedyncze wyrażenie do wykonania w celu zainicjowania klasy.
Static_constructor_body, który jest treść bloku lub wyrażenia, odpowiada dokładnie method_body metody statycznej z typem zwrotnym void
(§15.6.11).
Konstruktory statyczne nie są dziedziczone i nie mogą być wywoływane bezpośrednio.
Konstruktor statyczny dla zamkniętej klasy jest wykonywany co najwyżej raz w danej domenie aplikacji. Wykonanie konstruktora statycznego jest wyzwalane przez pierwsze z następujących zdarzeń w domenie aplikacji:
- Zostanie utworzone wystąpienie klasy.
- Odwołano się do dowolnych statycznych elementów członkowskich klasy.
Jeśli klasa zawiera Main
metodę (§7.1), w której rozpoczyna się wykonywanie, konstruktor statyczny dla tej klasy jest wykonywany przed Main
wywołaniem metody.
Aby zainicjować nowy typ zamkniętej klasy, najpierw zostanie utworzony nowy zestaw pól statycznych (§15.5.2) dla tego określonego typu zamkniętego. Każde z pól statycznych jest inicjowane do wartości domyślnej (§15.5.5). Następnie dla tych pól statycznych są wykonywane inicjatory pól statycznych (§15.5.6.2). Na koniec jest wykonywany konstruktor statyczny.
Przykład: przykład
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"); } }
musi wygenerować dane wyjściowe:
Init A A.F Init B B.F
ponieważ wykonanie
A
statycznego konstruktora jest wyzwalane przez wywołanieA.F
metody , a wykonanieB
konstruktora statycznego jest wyzwalane przez wywołanie metodyB.F
.przykład końcowy
Istnieje możliwość konstruowania zależności cyklicznych, które umożliwiają obserwowanie pól statycznych z inicjatorami zmiennych w ich domyślnym stanie wartości.
Przykład: przykład
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}"); } }
generuje dane wyjściowe
X = 1, Y = 2
Aby wykonać metodę
Main
, system najpierw uruchamia inicjator dlaB.Y
klasy , przed konstruktorem statycznym klasyB
.Y
Inicjator 's powodujeA
uruchomienie konstruktorastatic
, ponieważ wartośćA.X
jest przywołynięta. Konstruktor statycznyA
z kolei przechodzi do obliczenia wartościX
, a w ten sposób pobiera wartośćY
domyślną , która jest równa zero.A.X
jest zatem inicjowany do 1. Następnie proces uruchamianiaA
inicjatorów pól statycznych i konstruktora statycznego kończy się, wracając do obliczenia początkowejY
wartości , którego wynik staje się 2.przykład końcowy
Ponieważ konstruktor statyczny jest wykonywany dokładnie raz dla każdego zamkniętego typu klasy skonstruowanej, jest to wygodne miejsce do wymuszania kontroli czasu wykonywania dla parametru typu, którego nie można sprawdzić w czasie kompilacji za pośrednictwem ograniczeń (§15.2.5).
Przykład: Następujący typ używa konstruktora statycznego, aby wymusić, że argument typu jest wyliczeniem:
class Gen<T> where T : struct { static Gen() { if (!typeof(T).IsEnum) { throw new ArgumentException("T must be an enum"); } } }
przykład końcowy
15.13 Finalizatory
Uwaga: We wcześniejszej wersji tej specyfikacji, co jest teraz nazywane "finalizatorem" było nazywane "destruktorem". Doświadczenie wykazało, że termin "destruktor" spowodował zamieszanie i często wynikał z nieprawidłowych oczekiwań, zwłaszcza dla programistów znających język C++. W języku C++destruktor jest wywoływany w sposób determinat, natomiast w języku C#finalizator nie jest. Aby uzyskać określenie zachowania z języka C#, należy użyć polecenia
Dispose
. notatka końcowa
Finalizator to element członkowski, który implementuje akcje wymagane do sfinalizowania wystąpienia klasy. Finalizator jest deklarowany przy użyciu finalizer_declaration:
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) jest dostępny tylko w niebezpiecznym kodzie (§23).
Finalizer_declaration może zawierać zestaw atrybutów(§22).
Identyfikator finalizer_declarator określa klasę, w której zadeklarowany jest finalizator. Jeśli zostanie określona inna nazwa, wystąpi błąd czasu kompilacji.
Gdy deklaracja finalizatora zawiera extern
modyfikator, finalizator mówi się, że jest zewnętrznym finalizatorem. Ponieważ zewnętrzna deklaracja finalizatora nie zawiera rzeczywistej implementacji, jego finalizer_body składa się z średnika. W przypadku wszystkich innych finalizatorów finalizer_body składa się z jednego z następujących elementów:
- blok, który określa instrukcje do wykonania w celu sfinalizowania wystąpienia klasy.
- lub treść wyrażenia, która składa się z
=>
wyrażenia i średnika, i określa pojedyncze wyrażenie do wykonania w celu sfinalizowania wystąpienia klasy.
Finalizer_body, który jest treść bloku lub wyrażenia, odpowiada dokładnie method_body metody wystąpienia z typem zwracanym void
(§15.6.11).
Finalizatory nie są dziedziczone. W związku z tym klasa nie ma finalizatorów innych niż ta, która może być zadeklarowana w tej klasie.
Uwaga: ponieważ finalizator jest wymagany do braku parametrów, nie można go przeciążyć, więc klasa może mieć co najwyżej jeden finalizator. notatka końcowa
Finalizatory są wywoływane automatycznie i nie można ich jawnie wywołać. Wystąpienie kwalifikuje się do finalizacji, gdy nie będzie już możliwe użycie jakiegokolwiek kodu. Wykonanie finalizatora wystąpienia może nastąpić w dowolnym momencie po tym, jak wystąpienie kwalifikuje się do finalizacji (§7.9). Po sfinalizowaniu wystąpienia finalizatory w łańcuchu dziedziczenia tego wystąpienia są wywoływane w kolejności od większości pochodnych do najmniej pochodnych. Finalizator może być wykonywany w dowolnym wątku. Aby uzyskać dalszą dyskusję na temat zasad, które określają, kiedy i jak jest wykonywany finalizator, zobacz §7.9.
Przykład: dane wyjściowe przykładu
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(); } }
is
B's finalizer A's finalizer
ponieważ finalizatory w łańcuchu dziedziczenia są wywoływane w kolejności, z większości pochodnych do najmniej pochodnych.
przykład końcowy
Finalizatory są implementowane przez zastąpienie metody Finalize
wirtualnej w pliku System.Object
. Programy języka C# nie mogą zastąpić tej metody ani bezpośrednio ją wywołać (lub zastąpić).
Przykład: na przykład program
class A { override protected void Finalize() {} // Error public void F() { this.Finalize(); // Error } }
zawiera dwa błędy.
przykład końcowy
Kompilator powinien zachowywać się tak, jakby ta metoda oraz jej nadpisania w ogóle nie istniały.
Przykład: W związku z tym ten program:
class A { void Finalize() {} // Permitted }
jest prawidłowa, a pokazana metoda ukrywa
System.Object
metodęFinalize
.przykład końcowy
Aby zapoznać się z omówieniem zachowania podczas zgłaszania wyjątku z finalizatora, zobacz §21.4.
15.14 Iteratory
15.14.1 Ogólne
Element członkowski funkcji (§12.6) implementowany przy użyciu bloku iteratora (§13.3) jest nazywany iteratorem.
Blok iteratora może być używany jako treść składowej funkcji, o ile zwracany typ odpowiadającego mu elementu członkowskiego funkcji jest jednym z interfejsów modułu wyliczającego (§15.14.2) lub jednego z interfejsów wyliczalnych (§15.14.3). Może wystąpić jako method_body, operator_body lub accessor_body, natomiast zdarzenia, konstruktory wystąpień, konstruktory statyczne i finalizatory nie są implementowane jako iteratory.
Gdy element członkowski funkcji jest implementowany przy użyciu bloku iteratora, jest to błąd czasu kompilacji dla listy parametrów elementu członkowskiego funkcji w celu określenia dowolnych in
parametrów , , out
lub ref
parametru ref struct
typu.
Interfejsy modułu wyliczającego 15.14.2
Interfejsy modułu wyliczającego to interfejs System.Collections.IEnumerator
niegeneryczny i wszystkie wystąpienia interfejsu System.Collections.Generic.IEnumerator<T>
ogólnego . Ze względu na zwięzłość, w tym podklasie i jego elementy równorzędne te interfejsy są przywołyzane odpowiednio jako IEnumerator
i IEnumerator<T>
.
15.14.3 Interfejsy wyliczalne
Interfejsy wyliczalne to interfejs System.Collections.IEnumerable
niegeneryczny i wszystkie wystąpienia interfejsu System.Collections.Generic.IEnumerable<T>
ogólnego . Ze względu na zwięzłość, w tym podklasie i jego elementy równorzędne te interfejsy są przywołyzane odpowiednio jako IEnumerable
i IEnumerable<T>
.
15.14.4 Typ wydajności
Iterator tworzy sekwencję wartości— wszystkie tego samego typu. Ten typ jest nazywany typem wydajności iteratora.
- Typ wydajności iteratora, który zwraca
IEnumerator
wartość lubIEnumerable
ma wartośćobject
. - Typ wydajności iteratora, który zwraca
IEnumerator<T>
wartość lubIEnumerable<T>
ma wartośćT
.
15.14.5 Obiekty modułu wyliczającego
15.14.5.1 Ogólne
Gdy element członkowski funkcji zwracający typ interfejsu modułu wyliczającego jest implementowany przy użyciu bloku iteratora, wywołanie elementu członkowskiego funkcji nie powoduje natychmiastowego wykonania kodu w bloku iteratora.
Zamiast tego obiekt modułu wyliczającego jest tworzony i zwracany. Ten obiekt hermetyzuje kod określony w bloku iteratora, a wykonanie kodu w bloku iteratora ma miejsce, gdy wywoływana MoveNext
jest metoda obiektu wyliczającego. Obiekt modułu wyliczającego ma następujące cechy:
- Implementuje
IEnumerator
iIEnumerator<T>
, gdzieT
jest typem wydajności iteratora. - Implementuje
System.IDisposable
. - Jest inicjowany przy użyciu kopii wartości argumentu (jeśli istnieje) i wartości wystąpienia przekazanej do elementu członkowskiego funkcji.
- Ma cztery potencjalne stany, przed, uruchomione, zawieszone i po, i początkowo jest w stanie poprzedzającym.
Obiekt modułu wyliczającego jest zazwyczaj wystąpieniem klasy modułu wyliczającego generowanego przez kompilator, który hermetyzuje kod w bloku iteratora i implementuje interfejsy modułu wyliczającego, ale możliwe są inne metody implementacji. Jeśli klasa modułu wyliczającego jest generowana przez kompilator, ta klasa zostanie zagnieżdżona bezpośrednio lub pośrednio w klasie zawierającej składową funkcji, będzie mieć dostęp prywatny i będzie mieć nazwę zarezerwowaną do użycia kompilatora (§6.4.3).
Obiekt modułu wyliczającego może implementować więcej interfejsów niż określone powyżej.
W poniższych podkatazach opisano wymagane zachowanie MoveNext
implementacji interfejsu Current
i Dispose
, i IEnumerator
IEnumerator<T>
dostarczonych przez obiekt modułu wyliczającego.
Obiekty modułu IEnumerator.Reset
wyliczającego nie obsługują metody . Wywołanie tej metody powoduje zgłoszenie elementu System.NotSupportedException
.
15.14.5.2 Metoda MoveNext
Metoda MoveNext
obiektu wyliczającego hermetyzuje kod bloku iteratora. Wywołanie MoveNext
metody powoduje wykonanie kodu w bloku iteratora i ustawienie Current
właściwości obiektu modułu wyliczającego zgodnie z potrzebami. Dokładna akcja wykonywana przez MoveNext
program zależy od stanu obiektu wyliczającego, gdy MoveNext
jest wywoływany:
- Jeśli stan obiektu modułu wyliczającego jest wcześniej, wywołanie metody
MoveNext
:- Zmienia stan na uruchomiony.
- Inicjuje parametry (w tym
this
) bloku iteratora do wartości argumentu i wartości wystąpienia zapisanej podczas inicjowania obiektu modułu wyliczającego. - Wykonuje blok iteratora od początku do momentu przerwania wykonywania (zgodnie z poniższym opisem).
- Jeśli stan obiektu modułu wyliczającego jest uruchomiony, wynik wywołania
MoveNext
jest nieokreślony. - Jeśli stan obiektu modułu wyliczającego jest zawieszony, wywołanie metody MoveNext:
- Zmienia stan na uruchomiony.
- Przywraca wartości wszystkich zmiennych lokalnych i parametrów (w tym
this
) do wartości zapisanych podczas ostatniego zawieszenia wykonywania bloku iteratora.Uwaga: zawartość wszystkich obiektów, do których odwołuje się te zmienne, mogła ulec zmianie od poprzedniego wywołania metody
MoveNext
. notatka końcowa - Wznawia wykonywanie bloku iteratora bezpośrednio po instrukcji zwrotu wydajności, która spowodowała zawieszenie wykonywania i trwa do momentu przerwania wykonywania (zgodnie z poniższym opisem).
- Jeśli stan obiektu modułu wyliczającego jest następujący, wywołanie
MoveNext
zwraca wartość false.
Po MoveNext
wykonaniu bloku iteratora wykonywanie może zostać przerwane na cztery sposoby: przez instrukcję, instrukcję yield return
yield break
, napotkając koniec bloku iteratora, a także przez zgłoszenie i propagację wyjątku z bloku iteratora.
-
yield return
Po napotkaniu instrukcji (§9.4.4.20):- Wyrażenie podane w instrukcji jest obliczane, niejawnie konwertowane na typ plonu i przypisywane do
Current
właściwości obiektu wyliczającego. - Wykonanie treści iteratora jest zawieszone. Wartości wszystkich zmiennych lokalnych i parametrów (w tym
this
) są zapisywane, podobnie jak lokalizacja tejyield return
instrukcji.yield return
Jeśli instrukcja znajduje się w co najmniej jednymtry
bloku, skojarzone bloki ostatecznie nie są wykonywane w tej chwili. - Stan obiektu modułu wyliczającego jest zmieniany na zawieszony.
- Metoda
MoveNext
zwracatrue
element wywołujący, wskazując, że iteracja została pomyślnie zaawansowana do następnej wartości.
- Wyrażenie podane w instrukcji jest obliczane, niejawnie konwertowane na typ plonu i przypisywane do
-
yield break
Po napotkaniu instrukcji (§9.4.4.20):-
yield break
Jeśli instrukcja znajduje się w co najmniej jednymtry
bloku, skojarzonefinally
bloki są wykonywane. - Stan obiektu modułu wyliczającego jest zmieniany na później.
- Metoda
MoveNext
zwracafalse
element wywołujący, wskazując, że iteracja została ukończona.
-
- Po napotkaniu końca treści iteratora:
- Stan obiektu modułu wyliczającego jest zmieniany na później.
- Metoda
MoveNext
zwracafalse
element wywołujący, wskazując, że iteracja została ukończona.
- Gdy wyjątek jest zgłaszany i propagowany z bloku iteratora:
- Odpowiednie
finally
bloki w treści iteratora zostaną wykonane przez propagację wyjątku. - Stan obiektu modułu wyliczającego jest zmieniany na później.
- Propagacja wyjątku nadal wywołuje metodę
MoveNext
.
- Odpowiednie
15.14.5.3 Bieżąca właściwość
Właściwość obiektu modułu Current
wyliczającego ma wpływ na yield return
instrukcje w bloku iteratora.
Gdy obiekt modułu wyliczającego znajduje się w stanie wstrzymania, wartość Current
jest wartością ustawioną przez poprzednie wywołanie metody .MoveNext
Gdy obiekt modułu wyliczającego znajduje się w przed, uruchomieniu lub po stanach, wynik uzyskiwania dostępu Current
jest nieokreślony.
W przypadku iteratora o typie wydajności innym niż object
wynik uzyskiwania dostępu Current
za pośrednictwem implementacji obiektu modułu wyliczającego odpowiada dostępowi za pośrednictwem implementacji obiektu IEnumerable
Current
wyliczającego i rzutowania IEnumerator<T>
wyniku na object
wartość .
15.14.5.4 Metoda Dispose
Metoda Dispose
służy do czyszczenia iteracji przez przeniesienie obiektu modułu wyliczającego do stanu after .
- Jeśli stan obiektu modułu wyliczającego jest wcześniej, wywołanie
Dispose
zmienia stan na po. - Jeśli stan obiektu modułu wyliczającego jest uruchomiony, wynik wywołania
Dispose
jest nieokreślony. - Jeśli stan obiektu modułu wyliczającego jest zawieszony, wywołanie metody
Dispose
:- Zmienia stan na uruchomiony.
- Wykonuje wszystkie bloki w końcu tak, jakby ostatnia wykonana
yield return
instrukcja była instrukcjąyield break
. Jeśli spowoduje to zgłoszenie wyjątku i propagację z treści iteratora, stan obiektu modułu wyliczającego jest ustawiony na wartość after , a wyjątek jest propagowany do obiektu wywołującegoDispose
metody. - Zmienia stan na after.
- Jeśli stan obiektu modułu wyliczającego jest po, wywołanie
Dispose
nie ma wpływu.
15.14.6 Obiekty wyliczalne
15.14.6.1 Ogólne
Gdy element członkowski funkcji zwracający typ interfejsu wyliczalnego jest implementowany przy użyciu bloku iteratora, wywołanie elementu członkowskiego funkcji nie powoduje natychmiastowego wykonania kodu w bloku iteratora.
Zamiast tego jest tworzony i zwracany obiekt wyliczalny. Metoda obiektu GetEnumerator
wyliczalnego zwraca obiekt wyliczający, który hermetyzuje kod określony w bloku iteratora, a wykonanie kodu w bloku iteratora następuje po wywołaniu metody obiektu MoveNext
wyliczającego. Obiekt wyliczalny ma następujące cechy:
- Implementuje
IEnumerable
iIEnumerable<T>
, gdzieT
jest typem wydajności iteratora. - Jest inicjowany przy użyciu kopii wartości argumentu (jeśli istnieje) i wartości wystąpienia przekazanej do elementu członkowskiego funkcji.
Obiekt wyliczalny jest zazwyczaj wystąpieniem klasy wyliczalnej generowanej przez kompilator, która hermetyzuje kod w bloku iteratora i implementuje interfejsy wyliczalne, ale możliwe są inne metody implementacji. Jeśli klasa wyliczalna jest generowana przez kompilator, ta klasa zostanie zagnieżdżona bezpośrednio lub pośrednio w klasie zawierającej składową funkcji, będzie mieć dostęp prywatny i będzie mieć nazwę zarezerwowaną do użycia kompilatora (§6.4.3).
Obiekt wyliczalny może implementować więcej interfejsów niż określone powyżej.
Uwaga: na przykład obiekt wyliczalny może również implementować
IEnumerator
iIEnumerator<T>
, umożliwiając mu obsługę zarówno jako wyliczania, jak i modułu wyliczającego. Zazwyczaj taka implementacja zwróci własne wystąpienie (aby zapisać alokacje) z pierwszego wywołania metody .GetEnumerator
Kolejne wywołaniaGetEnumerator
klasy , jeśli istnieją, zwracają nowe wystąpienie klasy, zazwyczaj tej samej klasy, tak aby wywołania do różnych wystąpień modułu wyliczającego nie miały wpływu na siebie. Nie może zwrócić tego samego wystąpienia, nawet jeśli poprzedni moduł wyliczający wyliczył już koniec sekwencji, ponieważ wszystkie przyszłe wywołania modułu wyliczającego muszą zgłaszać wyjątki. notatka końcowa
15.14.6.2 Metoda Getenumerator
Obiekt wyliczalny zapewnia implementację GetEnumerator
metod interfejsów IEnumerable
i IEnumerable<T>
.
GetEnumerator
Dwie metody współdzielą wspólną implementację, która uzyskuje i zwraca dostępny obiekt modułu wyliczającego. Obiekt modułu wyliczającego jest inicjowany przy użyciu wartości argumentu i wartości wystąpienia zapisanej podczas inicjowania obiektu wyliczalnego, ale w przeciwnym razie obiekt wyliczający działa zgodnie z opisem w §15.14.5.
15.15 Funkcje asynchroniczne
15.15.1 Ogólne
Metoda (§15.6) lub funkcja anonimowa (§12.19) z modyfikatorem jest nazywana funkcjąasync
. Ogólnie rzecz biorąc, termin async służy do opisywania dowolnego rodzaju funkcji, która ma async
modyfikator.
Jest to błąd czasu kompilacji dla listy parametrów funkcji asynchronicznych w celu określenia dowolnych in
parametrów , , out
lub ref
dowolnego parametru ref struct
typu.
Return_type metody asynchronicznej to void
albo typ zadania. W przypadku metody asynchronicznej, która generuje wartość wynikową, typ zadania jest ogólny. W przypadku metody asynchronicznej, która nie generuje wartości wynikowej, typ zadania nie jest ogólny. Takie typy są określane odpowiednio w tej specyfikacji jako «TaskType»<T>
i «TaskType»
. Typ System.Threading.Tasks.Task
biblioteki standardowej i typy tworzone z System.Threading.Tasks.Task<TResult>
programu to typy zadań, a także klasa, struktura lub typ interfejsu skojarzony z typem konstruktora zadań za pośrednictwem atrybutu .System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
Takie typy są określane w tej specyfikacji jako «TaskBuilderType»<T>
i «TaskBuilderType»
. Typ zadania może mieć co najwyżej jeden parametr typu i nie można go zagnieżdżać w typie ogólnym.
Mówi się , że metoda asynchronizna zwracająca typ zadania jest zwracana.
Typy zadań mogą się różnić w ich dokładnej definicji, ale z punktu widzenia języka typ zadania jest w jednym ze stanów niekompletnych, zakończonych powodzeniem lub błędami. Uszkodzone zadanie rejestruje istotne wyjątki. Powodzenie «TaskType»<T>
T
Typy zadań są oczekiwane, a zadania mogą zatem być operandami wyrażeń await (§12.9.8).
Przykład: typ
MyTask<T>
zadania jest skojarzony z typem konstruktora zadań i typemMyTaskMethodBuilder<T>
Awaiter<T>
awaiter: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() { ... } }
przykład końcowy
Typ konstruktora zadań to klasa lub typ struktury, który odpowiada określonemu typowi zadania (§15.15.2). Typ konstruktora zadań dokładnie odpowiada zadeklarowanej dostępności odpowiadającego mu typu zadania.
Uwaga: Jeśli typ zadania jest zadeklarowany
internal
, odpowiedni typ konstruktora musi być również zadeklarowanyinternal
i zdefiniowany w tym samym zestawie. Jeśli typ zadania jest zagnieżdżony wewnątrz innego typu, typ buider zadania musi być również zagnieżdżony w tym samym typie. notatka końcowa
Funkcja asynchroniowa ma możliwość wstrzymania oceny za pomocą wyrażeń await (§12.9.8) w treści. Ocena może zostać później wznowiona w momencie wstrzymania wyrażenia await za pomocą delegata wznowienia. Delegat wznowienia jest typu System.Action
, a po wywołaniu ocena wywołania funkcji asynchronicznych zostanie wznowiona z wyrażenia await, w którym została przerwana. Bieżący obiekt wywołujący wywołanie funkcji asynchronicznych jest oryginalnym obiektem wywołującym, jeśli wywołanie funkcji nigdy nie zostało zawieszone lub ostatnio wywołujący delegat wznowienia w przeciwnym razie.
15.15.2 Wzorzec konstruktora typów zadań
Typ konstruktora zadań może mieć co najwyżej jeden parametr typu i nie można go zagnieżdżać w typie ogólnym. Typ konstruktora zadań musi mieć następujące elementy członkowskie (w przypadku typów konstruktorów zadań innych niż ogólne, SetResult
nie mają parametrów) z zadeklarowaną public
dostępnością:
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; }
}
Kompilator generuje kod, który używa "TaskBuilderType" do implementowania semantyki zawieszania i wznawiania oceny funkcji asynchronicznej. Kompilator używa "TaskBuilderType" w następujący sposób:
-
«TaskBuilderType».Create()
Element jest wywoływany w celu utworzenia wystąpienia obiektu «TaskBuilderType», o nazwie nabuilder
tej liście. -
builder.Start(ref stateMachine)
jest wywoływany w celu skojarzenia konstruktora z wystąpieniem maszyny stanu wygenerowanej przez kompilator.stateMachine
- Budowniczy powinien zadzwonić
stateMachine.MoveNext()
doStart()
maszyny państwowej lub poStart()
jej powrocie.
- Budowniczy powinien zadzwonić
- Po
Start()
powrocieasync
builder.Task
metoda wywołuje zadanie, które ma wrócić z metody asynchronicznej. - Każde wywołanie metody spowoduje
stateMachine.MoveNext()
przejście maszyny stanu. - Jeśli maszyna stanu zakończy się pomyślnie,
builder.SetResult()
zostanie wywołana z wartością zwracaną przez metodę , jeśli istnieje. - W przeciwnym razie, jeśli wyjątek jest
e
zgłaszany na maszynie stanu,builder.SetException(e)
jest wywoływany. - Jeśli maszyna stanu osiągnie
await expr
wyrażenie,expr.GetAwaiter()
jest wywoływana. - Jeśli program awaiter implementuje
ICriticalNotifyCompletion
wartość iIsCompleted
ma wartość false, maszyna stanu wywołuje metodębuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
.-
AwaitUnsafeOnCompleted()
element powinien wywołaćawaiter.UnsafeOnCompleted(action)
metodę , któraAction
wywołujestateMachine.MoveNext()
polecenie po zakończeniu funkcji awaiter.
-
- W przeciwnym razie maszyna stanu wywołuje element
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
.-
AwaitOnCompleted()
element powinien wywołaćawaiter.OnCompleted(action)
metodę , któraAction
wywołujestateMachine.MoveNext()
polecenie po zakończeniu funkcji awaiter.
-
-
SetStateMachine(IAsyncStateMachine)
może być wywoływana przez implementację wygenerowanąIAsyncStateMachine
przez kompilator w celu zidentyfikowania wystąpienia konstruktora skojarzonego z wystąpieniem maszyny stanu, szczególnie w przypadkach, gdy maszyna stanu jest implementowana jako typ wartości.- Jeśli konstruktor wywoła
stateMachine.SetStateMachine(stateMachine)
metodę ,stateMachine
wywoła wystąpieniebuilder.SetStateMachine(stateMachine)
konstruktora skojarzone z elementemstateMachine
.
- Jeśli konstruktor wywoła
Uwaga: dla parametrów
SetResult(T result)
i«TaskType»<T> Task { get; }
, parametr i argument muszą być odpowiednio konwertowaneT
na wartość . Umożliwia to konstruktorowi typów zadań obsługę typów, takich jak krotki, w których dwa typy, które nie są takie same, to tożsamość konwertowana. notatka końcowa
15.15.3 Ocena funkcji asynchronicznego zwracającej zadanie
Wywołanie funkcji asynchronicznego zwracanej przez zadanie powoduje wygenerowanie wystąpienia zwracanego typu zadania. Jest to nazywane zadaniem zwrotnym funkcji asynchronicznego. Zadanie jest początkowo w stanie niekompletnym.
Treść funkcji asynchronicznych jest następnie oceniana do momentu wstrzymania (przez osiągnięcie wyrażenia await) lub zakończenia, w którym kontrolka punktu jest zwracana do wywołującego wraz z zadaniem zwrotnym.
Gdy treść funkcji asynchronicznych zakończy działanie, zadanie zwracane zostanie przeniesione z niekompletnego stanu:
- Jeśli treść funkcji kończy się w wyniku osiągnięcia instrukcji return lub końca treści, każda wartość wyniku jest rejestrowana w zadaniu zwrotnym, który jest umieszczany w stanie powodzenia.
- Jeśli treść funkcji kończy się z powodu nieuchwyconego
OperationCanceledException
elementu , wyjątek jest rejestrowany w zadaniu zwrotnym, które jest umieszczane w stanie anulowanym . - Jeśli treść funkcji zakończy się w wyniku jakichkolwiek innych nieuchwyconych wyjątków (§13.10.6), wyjątek jest rejestrowany w zadaniu zwrotnym, które jest umieszczane w stanie błędu.
15.15.4 Ocena funkcji asynchronicznego zwracającej pustkę
Jeśli zwracany typ funkcji asynchronizowanej to void
, ocena różni się od powyższego w następujący sposób: ponieważ żadne zadanie nie jest zwracane, funkcja przekazuje uzupełnianie i wyjątki kontekstowi synchronizacji bieżącego wątku. Dokładna definicja kontekstu synchronizacji jest zależna od implementacji, ale jest reprezentacją "gdzie" bieżący wątek jest uruchomiony. Kontekst synchronizacji jest powiadamiany, gdy ocena void
funkcji asynchronicznego rozpoczyna się, kończy się pomyślnie lub powoduje zgłoszenie nieuchwyconego wyjątku.
Dzięki temu kontekst może śledzić liczbę void
uruchomionych w nim funkcji asynchronicznych i zdecydować, jak propagować wyjątki wychodzące z nich.
ECMA C# draft specification