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óra deklaruje nową klasę.
class_declaration
: attributes? class_modifier* 'partial'? 'class' identifier
type_parameter_list? class_base? type_parameter_constraints_clause*
class_body ';'?
;
Deklaracja klasy (
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
Deklaracja klasy może opcjonalnie zawierać sekwencję modyfikatorów klasy:
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 element o tej samej nazwie, jak opisano w §15.3.5. Jest to błąd czasu kompilacji, gdy modyfikator new
pojawia się w deklaracji klasy, która nie jest deklaracją zagnieżdżonej klasy.
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).
Jeśli częściowa deklaracja typu (§15.2.7) zawiera specyfikację dostępności (za pośrednictwem modyfikatorów public
, protected
, internal
i private
), specyfikacja ta powinna być zgodna ze wszystkimi innymi częściami, które zawierają specyfikację dostępności. 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, a użycie operatora
new
w klasie abstrakcyjnej stanowi błąd podczas kompilacji. 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 może (ale nie musi) zawierać abstrakcyjnych członków.
- 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. KlasaC
przesłaniaF
i zapewnia rzeczywistą implementację. Ponieważ wC
nie ma członów abstrakcyjnych,C
może być nieabstrakcyjny (ale nie jest to wymagane).przykład końcowy
Jeśli jedna lub więcej części deklaracji częściowego typu (§15.2.7) klasy zawiera modyfikator abstract
, 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 niezamierzonemu dziedziczeniu, ale umożliwia również pewne optymalizacje w czasie wykonania. 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ęściowej deklaracji typu (§15.2.7) klasy zawiera modyfikator sealed
, klasa jest zamknięta. W przeciwnym razie klasa jest niezapieczętowana.
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. Klasy statycznej nie należy instancjonować, nie należy używać jej jako typu i powinna zawierać tylko statyczne elementy. Tylko klasa statyczna może zawierać deklaracje metod rozszerzenia (§15.6.10).
Deklaracja klasy statycznej podlega następującym ograniczeniom:
- Klasa statyczna nie powinna zawierać modyfikatora
sealed
aniabstract
. (Ponieważ jednak nie można utworzyć wystąpienia ani dziedziczyć z klasy statycznej, zachowuje się ona tak, jakby była jednocześnie zamknięta 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 członkowie. notatka końcowa
- Klasa statyczna nie powinna mieć członków z określoną widocznością
protected
,private protected
aniprotected internal
.
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 i deklaracje tych składowych muszą jawnie zawierać modyfikator static
(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ęś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ływanie się do typów klas statycznych
namespace_or_type_name (§7.8) może odnosić się do klasy statycznej, jeśli
- Namespace_or_type_name jest
T
w namespace_or_type_name w postaciT.I
lub... - Nazwa namespace_or_type-name jest
T
w typeof_expression (§12.8.18) w postacitypeof(T)
.
Podstawowe_wyrażenie (§12.8) może odwoływać się do statycznej klasy, jeśli
-
primary_expression jest
E
w member_access (§12.8.7) w formieE.I
.
W każdym innym kontekście to błąd czasu kompilacji odwołać się do klasy statycznej.
Uwaga: Na przykład, błędem jest użycie klasy statycznej jako klasy bazowej, składnika typu (§15.3.7) członka, argumentu typu generycznego lub ograniczenia dla parametru typu. Podobnie, nie można używać klasy statycznej w typie tablicy, nowym wyrażeniu, wyrażeniu rzutowym, wyrażeniu is, wyrażeniu as, 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. W przeciwieństwie, argument typu (§8.4.2) jest typem, który jest podstawiany za parametr typu podczas tworzenia skonstruowanego typu.
type_parameter_list
: '<' decorated_type_parameter (',' decorated_type_parameter)* '>'
;
decorated_type_parameter
: 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 otwartego 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żdego type_parameter w deklaracji klasy bazowej, odpowiadającego 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 danej klasy nie może być żadnym z następujących typów: System.Array
, System.Delegate
, System.Enum
, System.ValueType
lub typu dynamic
. 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 klasy bazowej A
dla klasy B
, tymczasowo przyjmuje się, że bezpośrednia klasa bazowa B
to object
, co gwarantuje, że znaczenie specyfikacji klasy bazowej nie może zależeć rekursywnie 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 klasy bazowej
bezpośrednia klasa bazowa jest uważana za , a zatem (zgodnie z regułami §7.8 )nie jest uważana za składową . 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 object
każda klasa ma dokładnie jedną bezpośrednią klasę bazową. Klasa object
nie ma bezpośredniej klasy bazowej i jest ostateczną klasą bazową wszystkich innych klas.
Błąd czasu kompilacji występuje, gdy klasa zależy od siebie samej. 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 domknięcie relacji bezpośredniego zależności od.
Przykład: przykład
class A : A {}
jest błędne, ponieważ klasa zależy od siebie. Podobnie, jak 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ż klasa A zależy od
B.C
(swojej bezpośredniej klasy bazowej), która zależy odB
(bezpośrednio otaczającej klasy), która cyklicznie zależy odA
.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 dziedziczyć po zapieczętowanej klasie.
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
Podstawowe interfejsy określone w deklaracji klasy mogą być typami konstruowanymi 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_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. Podobnie jak tokeny get
i set
w metodzie dostępu do właściwości, token where
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, ograniczeniem rodzaju referencyjnego, ograniczeniem rodzaju wartościowego, ograniczeniem na brak wartości null lub ograniczeniem na typy niezarządzane. Typ klasy i ograniczenie typu odwołania mogą obejmować nullable_type_annotation.
Ograniczenie pomocnicze może być typu interface_type lub type_parameter, opcjonalnie z dodatkiem 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 dopuszczającego wartość 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 dopuszczającego wartość null, oczekuje się, że argument typu będzie 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 dopuszczającym 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 nullable, ?
, nie może być używana w niesprecyzowanym argumencie typu.
W przypadku parametru T
, gdy argumentem typu jest typ referencyjny dopuszczający wartości null C?
, wystąpienia T?
są interpretowane jako C?
, zamiast 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, który nie dopuszcza wartości null,
?
adnotacja wskazuje, że parametr jest odpowiednim typem umożliwiającym wartość null. Gdy argument typu jest już typem odwołania, który może przyjmować wartość null, parametr jest tym samym typem odwołania, który może przyjmować wartość null.przykład końcowy
Ograniczenie not null określa, że argument typu używany dla parametru typu powinien być typem wartości niemogącym przyjmować wartości null lub typem referencyjnym niemogącym przyjmować wartości null. Argument typu, który nie jest typem wartości nieprzyjmującym wartości null lub typem referencyjnym nieprzyjmującym wartości null, jest dozwolony, ale kompilator może wygenerować ostrzeżenie diagnostyczne.
Ponieważ notnull
nie jest słowem kluczowym, w primary_constraint ograniczenie „not null” jest zawsze niejednoznaczne w stosunku do class_type. Ze względów kompatybilności, jeśli wyszukiwanie nazwy (§12.8.4) powiedzie się, jest traktowane jako class_type
. W przeciwnym razie jest traktowane jako ograniczenie niezerowe.
Ograniczenie typu wartości określa, że argument typu używany dla parametru typu musi być typem wartości niepustej. Wszystkie typy struktur nieakceptujące 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 mający ograniczenie typu wartości nie może mieć 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
Ograniczenie typu niezarządzanego określa, że argument typu używany dla parametru typu musi być nienullable typem niezarządzanym (§8.8).
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 nazwy (§12.8.4) zakończy się pomyślnie, traktuje się je jako class_type
. W przeciwnym razie jest traktowana jako ograniczenie niezarządzane.
Typy wskaźników nigdy nie mogą być argumentami typu i nie spełniają żadnych ograniczeń typowych, nawet tych dotyczących typów niezarządzanych, mimo że same są typami niezarządzanymi.
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 powinien być 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
jest używany jako ograniczenie dla parametruS
typu, toS
zależy odT
. - Jeśli parametr typu
S
zależy od parametru typuT
, aT
zależy od parametru typuU
, toS
zależy odU
.
W tej relacji jest błędem czasu kompilacji, aby parametr typu zależał od samego 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,T
jest skutecznie zapieczętowany, więcS
będzie musiał być tego samego typu coT
, co eliminuje potrzebę użycia dwóch parametrów typu. - Jeśli
S
ma ograniczenie typu wartości,T
nie ma ograniczenia class_type . - Jeśli
S
ma ograniczenie class_typeA
iT
ma ograniczenie class_typeB
, to powinna istnieć konwersja tożsamościowa lub niejawna konwersja referencyjna zA
doB
lub niejawna konwersja referencyjna zB
doA
. - Jeśli
S
również zależy od parametru typuU
iU
ma ograniczenie class_typeA
orazT
ma ograniczenie class_typeB
, to powinna istnieć konwersja tożsamości lub niejawna konwersja odwołania zA
doB
lub niejawna konwersja odwołania zB
doA
.
Dopuszczalne jest, aby S
miało ograniczenie typu wartości, a T
miało ograniczenie 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, jeśli type_parameter_constraints mają primary_constraint równy struct
lub unmanaged
, a także constructor_constraint.
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 jest błędny, ponieważ powoduje cykliczność w grafie 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 typem skonstruowanymG<A¹ₓ, ..., Aⁿₓ>
. - Jeśli
C
jest typemE[]
tablicy,Cₓ
jest to typEₓ[]
tablicy . - Jeśli
C
wartość jest dynamiczna,Cₓ
wartość toobject
. - W przeciwnym razie,
Cₓ
toC
.
Efektywna klasa bazowa parametru T
typu jest definiowana w następujący sposób:
Niech R
będzie zbiorem typów takim, że:
- Dla każdego ograniczenia
T
, które jest parametrem typu,R
zawiera jego 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 typu delegata
T
,R
zawiera jego dynamiczne wymazanie. - Dla każdego ograniczenia
T
, które jest typem tablicy,R
zawieraSystem.Array
. - Dla każdego ograniczenia typu klasy
T
R
zawiera jego dynamiczne wymazanie.
Następnie
- Jeśli
T
ma ograniczenie typu wartości, efektywną klasą bazową jestSystem.ValueType
. - W przeciwnym razie, jeśli
R
jest pusty, efektywna klasa bazowa toobject
. - W przeciwnym razie efektywna klasa bazowa
T
jest typem najbardziej ogólnym (§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 typ o najbardziej szerokim zakresie.
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 interfejsów dla parametru 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 ograniczenia interface_type, ale nie ograniczenia type_parameter, jego efektywny zestaw interfejsów jest zbiorem dynamicznych wymazań jego ograniczeń interface_type. - Jeśli
T
nie ma żadnych ograniczeń interface_type, ale ma ograniczenia type_parameter, jego efektywny zestaw interfejsów jest połączeniem efektywnych zestawów interfejsów jego ograniczeń type_parameter. - Jeśli
T
ma zarówno ograniczenia typu interface_type, jak i ograniczenia type_parameter, jego efektywny zestaw interfejsów jest unią zestawu dynamicznych wymazań jego ograniczeń typu interface_type oraz efektywnych zbiorów interfejsowych jego ograniczeń type_parameter.
Parametr typu jest uznawany za typ referencyjny, jeśli posiada ograniczenie dla typów referencyjnych lub jego efektywna klasa bazowa nie jest object
lub System.ValueType
. Parametr typu jest znany jako referencyjny typ niemający wartości null, jeśli jest typem odwołania i posiada ograniczenie referencyjnego typu bez wartości null.
Wartości typu parametru typu ograniczonego mogą być używane do dostępu do członków instancji wynikających z ograniczeń.
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
można wywołać bezpośrednio nax
, ponieważT
jest zmuszony do zawsze implementowaniaIPrintable
.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. Jeśli żaden element częściowego typu ogólnego nie określa ograniczeń parametrów typu, to parametry te są traktowane jako nieograniczone.
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
15.2.6 Treść klasy
class_body klasy definiuje elementy tej klasy.
class_body
: '{' class_member_declaration* '}'
;
15.2.7 Częściowe deklaracje typów
Modyfikator partial
jest używany podczas definiowania klasy, struktury lub typu interfejsu w wielu częściach. Modyfikator partial
jest słowem kluczowym kontekstowym (§6.4.4) i ma specjalne znaczenie bezpośrednio przed słowami kluczowymi class
, struct
i interface
. (Typ częściowy może zawierać deklaracje metody częściowej (§15.6.9).
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 zaimplementowanych interfejsów muszą być zgodne, w tym nullowalność wszelkich określonych argumentów typu.
Wszystkie części typu częściowego należy skompilować razem, aby można było je scalić podczas kompilacji. Typy częściowe nie zezwalają w szczególności na rozszerzenie już skompilowanych typów.
Typy zagnieżdżone mogą być deklarowane w wielu częściach przy użyciu modyfikatora partial
. Zazwyczaj typ zawierający jest również deklarowany przy użyciu partial
, a każdy element typu zagnieżdżonego jest deklarowany 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ęść jest generowana przez narzędzie do mapowania baz danych, podczas gdy druga część jest opracowana ręcznie.
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 na typie lub jego parametrach typu w różnych częściach częściowej deklaracji typu jest omawiana w §22.3.
15.3 Składowe klasy
15.3.1 Ogólne
Członkowie klasy składają się z członków wprowadzonych przez jej class_member_declaration oraz członków odziedziczonych 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
;
Członkowie klasy są podzieleni 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)
- Finalizery, które implementują akcje do wykonania, zanim instancje klasy zostaną trwale usunięte (§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_parameter oraz class_member_declaration bezpośrednio zawarte w class_declaration wprowadzają nowe elementy do tej przestrzeni deklaracji. Następujące reguły mają zastosowanie do class_member_declarations:
Konstruktory instancji, finalizatory i konstruktory statyczne muszą mieć tę samą nazwę co klasa je obejmująca. Wszystkie pozostałe członkowie mają nazwy, które różnią się od nazwy bezpośrednio 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
Zbiór członków typu zadeklarowanego w wielu częściach (§15.2.7) jest sumą 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 dostępności każdego składnika zawsze zawiera wszystkie części otaczającego typu danych; prywatny składnik zadeklarowany w jednej części może być swobodnie dostępny z innej części. Jest to błąd czasu kompilacji, gdy zadeklaruje się tego samego członka w więcej niż jednej części tego samego typu, chyba że ten członek ma modyfikator partial
.
Przykład:
partial class A { int x; // Error, cannot declare x more than once partial void M(); // Ok, defining partial method declaration partial class Inner // Ok, Inner is a partial type { int y; } } partial class A { int x; // Error, cannot declare x more than once partial void M() { } // Ok, implementing partial method declaration partial class Inner // Ok, Inner is a partial type { int z; } }
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 wewnątrz typu jest rzadko istotna, ale może nabrać znaczenia w przypadku współdziałania z innymi językami i środowiskami. W takich przypadkach kolejność elementów składowych w typie zadeklarowanym 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 deklaracji klasy ogólnejGen
to "dwuwymiarowa tablicaT
", więc typ składoweja
w skonstruowanym typie powyżej to "dwuwymiarowa tablica tablicy jednowymiarowejint
" lubint[,][]
.przykład końcowy
W ramach elementów funkcji instancyjnej typem this
jest typ instancji (§15.3.2) zawierającej deklaracji.
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
, aB
pochodzi zA
, toC
dziedziczy członków zadeklarowanych wB
oraz członków zadeklarowanych wA
.Klasa pochodna rozszerza swoją bezpośrednią klasę bazową. Klasa pochodna może dodawać nowe składowe do tych, które dziedziczy, ale nie jest w stanie usunąć definicji dziedziczonego członka.
Konstruktory wystąpień, finalizatory i konstruktory statyczne nie są dziedziczone, ale wszyscy inni członkowie są, niezależnie od zadeklarowanego poziomu dostępu (§7.5). Jednak w zależności od zadeklarowanej dostępności, odziedziczone elementy 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 wszystkie pola instancji zadeklarowane w klasie i jej klasach bazowych, i istnieje niejawna konwersja (§10.2.8) z typu klasy pochodnej do któregoś z typów jej 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 bezpośredniego typu klasy bazowej (§15.2.4.2), który znajduje się poprzez podstawianie argumentów typu skonstruowanego za każde wystąpienie odpowiednich parametrów typu w base_class_specification. Z kolei ci członkowie są przekształcani przez podstawienie, dla każdego type_parameter w deklaracji członka, odpowiadającego type_argument specyfikacji klasy bazowej.
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 nieodziedziczony członek publicznyint
G(string s)
uzyskany przez podstawienie argumentu typuint
dla parametru typuT
.D<int>
ma również element członkowski odziedziczony z deklaracji klasyB
. Ten dziedziczony element członkowski jest określany poprzez najpierw ustalenie typuB<int[]>
klasy bazowejD<int>
, przez zastąpienieT
elementemint
w specyfikacji klasy bazowejB<T[]>
. Następnie, jako argument typu dlaB
,int[]
jest zastępowanyU
w parametrzepublic U F(long index)
, 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ć członka z tą samą nazwą lub sygnaturą co dziedziczony członek. W takim przypadku mówi się, że składowa klasy pochodnej ukrywa składową klasy bazowej. Zobacz §7.7.2.3 dla dokładnej specyfikacji, kiedy członek ukrywa odziedziczonego członka.
Dziedziczony członek M
jest uważany za dostępny, jeśli M
jest osiągalny i nie ma innego dziedziczonego dostępnego członka N, który już ukrywa M
. 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, zostanie wyświetlone ostrzeżenie w tej sprawie.
15.3.6 Modyfikatory dostępu
Deklaracja członka klasy (class_member_declaration) może mieć dowolny z dozwolonych rodzajów zadeklarowanej dostępności (§7.5.2): public
, protected internal
, protected
, private protected
, internal
, lub private
. Z wyjątkiem kombinacji protected internal
i private protected
, jest to błąd czasu kompilacji, aby określić 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 instancji
Składowe klasy to statyczni członkowie lub członkowie instancyjni.
Uwaga: ogólnie rzecz biorąc, warto traktować statyczne elementy członkowskie jako należące do klas, a elementy członkowskie instancji jako należące do obiektów (instancji 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
. Podczas kompilacji występuje błąd, gdyE
reprezentuje instancję. - 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. (Członek instancji jest czasami nazywany niestatycznym elementem członkowskim). Członkowie instancji mają następujące cechy:
- Jeżeli element członkowski wystąpienia
M
jest przywołany w member_access (§12.8.7) w postaciE.M
,E
określa wystąpienie typu, który ma element członkowskiM
. Jest to błąd czasu wiązania, gdy E oznacza 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 metodzie członka instancji można użyć simple_name (§12.8.4) do uzyskania dostępu zarówno do składowych instancji, jak i do składowych statycznych. MetodaG
pokazuje, że w składowej funkcji statycznej błąd czasu kompilacji występuje, gdy próbujemy uzyskać dostęp do elementu wystąpienia za pośrednictwem simple_name. MetodaMain
pokazuje, że w member_access (§12.8.7) członkowie instancji powinni być dostępni przez instancje, a członkowie statyczni przez typy.przykład końcowy
15.3.9 Typy zagnieżdżone
15.3.9.1 Ogólne
Typ zadeklarowany w klasie lub strukturze nazywa się 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 typu zagnieżdżonego jest S.N
, gdzie S
jest w pełni kwalifikowaną nazwą deklaracji typu, w której zadeklarowano typ N
, a N
stanowi niekwalifikowaną nazwę (§7.8.2) deklaracji typu zagnieżdżonego (w tym dowolnego specyfikatora_wymiaru_ogólnego (§12.8.18)).
15.3.9.3 Zadeklarowana dostępność
Typy niezagnieżdżone mogą mieć public
lub internal
zadeklarowaną dostępność i domyślnie mieć internal
zadeklarowaną dostępność. Zagnieżdżone typy mogą również mieć te formy zadeklarowanej ochrony dostępu oraz jedną lub więcej dodatkowych form zadeklarowanej ochrony dostępu, w zależności od tego, czy typ, który zawiera, jest klasą czy strukturą.
- Zagnieżdżony typ zadeklarowany w klasie może mieć dowolny z dozwolonych rodzajów zadeklarowanej dostępności i, podobnie jak inne elementy klasy, domyślnie ma (z)definiowane
private
poziomy dostępności. - Typ zagnieżdżony, który jest zadeklarowany w strukturze, może mieć dowolną z trzech form zadeklarowanej dostępności (
public
,internal
, lubprivate
) i, podobnie jak inne elementy członkowskie struktury, domyślnie ma zadeklarowaną dostępność jakoprivate
.
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ą zagnieżdżoną klasę
Node
.przykład końcowy
15.3.9.4 Ukrywanie
Typ zagnieżdżony może ukrywać element bazowy (§7.7.2.2).
new
Modyfikator (§15.3.5) jest dozwolony przy deklaracjach typów zagnieżdżonych, co pozwala na jawne wyrażenie ukrywania.
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ą wBase
.przykład końcowy
15.3.9.5 ten dostęp
Typ zagnieżdżony i typ, który go zawiera, nie mają specjalnej relacji, jeśli chodzi o this_access (§12.8.14). W szczególności w obrębie this
typu zagnieżdżonego nie można używać do odwoływania się do elementów instancji typu otaczają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ę. Instancja
C
tworzy instancjęNested
, i przekazuje swoje this do konstruktoraNested
w celu zapewnienia późniejszego dostępu do elementów członkowskich instancjiC
.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 typu zawierającego, w tym tych członków, które mają zadeklarowane poziomy dostępu, jak private
i protected
.
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
, która zawiera zagnieżdżoną klasęNested
. WNested
metodaG
wywołuje statyczną metodęF
, zdefiniowaną wC
, aF
ma prywatnie zdeklarowaną dostępność.przykład końcowy
Typ zagnieżdżony może również uzyskiwać dostęp do chronionych składowych zdefiniowanych w typie bazowym zawierającym go.
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(); } }
Zagnieżdżona klasa
Derived.Nested
uzyskuje dostęp do chronionej metodyF
zdefiniowanej w klasie bazowejDerived
Base
, poprzez wywołanie instancjiDerived
.przykład końcowy
15.3.9.7 Typy zagnieżdżone w klasach ogólnych
Deklaracja klasy generycznej może zawierać deklaracje typów zagnieżdżonych. 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 generycznym należy uwzględnić nazwę konstrukty, łącznie z argumentami typu. Jednak wewnątrz klasy zewnętrznej typ zagnieżdżony może być używany bez dodatkowego oznaczenia; typ instancji klasy zewnętrznej może być używany niejawnie przy tworzeniu 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ć człon lub zadeklarowany parametr typu 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 członków
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). Błędem kompilacji w programie jest deklarowanie członka, którego sygnatura pasuje do sygnatury zarezerwowanej przez innego członka zadeklarowanego w tym samym zakresie, nawet jeśli implementacja w czasie wykonywania nie wykorzystuje tych rezerwacji.
Nazwy zastrzeżone nie wprowadzają deklaracji, dlatego nie uczestniczą w wyszukiwaniu członków. 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 kod źródłowy zaakceptowany przez jeden zgodny kompilator jest akceptowany przez inny, poprzez ujednolicenie nazw zarezerwowanych członków 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
(§15.7) typu T
zastrzeżone są następujące podpisy:
T get_P();
void set_P(T value);
Oba podpisy są zarezerwowane, nawet gdy 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
, która jest tylko do odczytu, rezerwując w ten sposób podpisy dla metodget_P
iset_P
.A
klasaB
dziedziczy poA
i ukrywa obie z tych zastrzeżonych sygnatur. Przykład generuje dane wyjściowe:123 123 456
przykład końcowy
15.3.10.3 Nazwy członków 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 członków 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 członka Item
jest zarezerwowana.
15.3.10.5 Nazwy członków 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 plik) |
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ężające) przymuszenie |
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'
;
Deklaracja stałej może zawierać zestaw atrybutów (§22), new
(§15.3.5), i dowolny z dozwolonych rodzajów zadeklarowanej dostępności (§15.3.6). Atrybuty i modyfikatory mają zastosowanie do wszystkich elementów członkowskich zadeklarowanych przez constant_declaration. Mimo że stałe są uznawane za statyczne członki, constant_declaration nie wymaga ani nie dopuszcza użycia modyfikatora static
. Jest to błąd dla tego samego modyfikatora, który pojawia się wielokrotnie w deklaracji stałej.
Typ deklaracji constant_declaration określa typ elementów wprowadzonych przez deklarację. Po typie następuje lista constant_declaratorów (§13.6.3), z których każdy wprowadza nowy element. Deklarator stałej składa się z identyfikatora, który nazywa element członkowski, po którym następuje znacznik "=
", a następnie wyrażenie stałej (§12.23), które daje wartość elementu członkowskiego.
Typ
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 reference_type innej niż
string
jest zastosowanie operatoranew
, a ponieważ operatornew
nie jest dozwolony w wyrażeniu stałymnew
, jedyną możliwą wartością dla stałych reference_typey innych niż jeststring
. 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 wersjonowania
const
ireadonly
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: nawiązując do powyższego przykładu, jeśli
A
iB
zostały zadeklarowane w oddzielnych programach, możliwe byłoby, abyA.X
zależało odB.Z
, aleB.Z
nie mogłoby równocześnie zależeć 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).
Deklaracja pola może zawierać zestaw atrybutów (§22), modyfikator new
(§15.3.5), prawidłową kombinację czterech modyfikatorów dostępu (§15.3.6) i modyfikator static
(§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 członków zadeklarowanych przez field_declaration. Jest błędem, gdy ten sam modyfikator pojawia się wielokrotnie w field_declaration.
Typfield_declaration określa typ elementów wprowadzonych przez deklarację. Po typie następuje lista variable_declarators, z których każdy wprowadza nowego członka. A variable_declarator składa się z identyfikatora, który nazywa ten człon, opcjonalnie po którym znajduje się token "=
" i variable_initializer (§15.5.6), który nadaje początkową wartość temu członowi.
Typ pola jest co najmniej tak dostępny, jak samo pole (§7.5.5).
Wartość pola uzyskiwana jest 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 pola instancji
Gdy deklaracja pola zawiera static
modyfikator, pola wprowadzone przez deklarację to pola statyczne. Jeśli modyfikator static
nie jest obecny, pola wprowadzone przez deklarację są polami instancji. 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ć wielokrotnie tylko w określonych kontekstach.) W szczególności, bezpośrednie przypisania do pola tylko do odczytu są dozwolone wyłącznie w następujących sytuacjach:
- W variable_declarator, który wprowadza pole (poprzez uwzględnienie variable_initializer w deklaracji).
- Dla pola instancyjnego, w konstruktorach instancyjnych 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 tylko do odczytu lub przekazania jego jako parametru wyjściowego lub referencyjnego w dowolnym innym kontekście jest błędem czasu kompilacji.
15.5.3.2 Używanie stałych pól readonly dla stałych wartości
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; } }
Elementów członkowskich
Black
,White
,Red
,Green
iBlue
nie można zadeklarować jako 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 Wersjonowanie stałych i pól statycznych tylko do odczytu
Stałe i pola tylko do odczytu mają różną semantykę wersjonowania binarnego. 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 tylko do odczytu, wartość pola nie jest uzyskiwana aż 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 nazw
Program1
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
zostałaby uzyskana w czasie kompilacjiProgram2
i pozostałaby bez zmian, pomimo zmian wProgram1
, aż do momentu ponownego skompilowaniaProgram2
.przykład końcowy
15.5.4 Pola nietrwałe
Gdy field_declaration zawiera modyfikator volatile
, pola wprowadzone przez tę deklarację są polami nietrwałymi. W przypadku pól nietrwałych techniki optymalizacji, które zmieniają kolejność instrukcji, mogą prowadzić do nieoczekiwanych i nieprzewidywalnych wyników w programach wielowątkowych, uzyskujących 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 zmiennej oznaczonej jako volatile ma "semantykę akwizycji"; oznacza to, że jest gwarantowane, iż nastąpi przed wszelkimi odwołaniami do pamięci, które następują po nim w sekwencji instrukcji.
- Zapis pola zmiennego jest nazywany zapisem zmiennymvolatile write. Zapis volatile ma "semantykę wydania"; oznacza to, że jest gwarantowany po wszelkich odwołaniach do pamięci przed instrukcją zapisu w sekwencji instrukcji.
Te ograniczenia zapewniają, że wszystkie wątki będą obserwować zmienne zapisy wykonywane przez dowolny inny wątek w kolejności, w jakiej zostały wykonane. Zgodna implementacja nie musi zapewniać pojedynczej całkowitej kolejności zapisów zmiennych ulotnych z punktu widzenia wszystkich wątków wykonywania. Typ pola lotnego musi być jednym z następujących:
- typ_odniesienia.
- type_parameter, który jest znany jako typ referencyjny (§15.2.5).
- Typ
byte
,sbyte
,short
,ushort
,int
,uint
,char
,float
,bool
,System.IntPtr
lubSystem.UIntPtr
. - Typ enum_type mający typ bazowy enum_base w jednym z wartości:
byte
,sbyte
,short
,ushort
,int
lubuint
.
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 zadeklarowane jakovolatile
, to dopuszczalne byłoby, aby zapis doresult
był widoczny dla głównego wątku po zapisie dofinished
, a tym samym aby główny wątek mógł odczytać wartość 0 z polaresult
. Deklarowaniefinished
jakovolatile
pola uniemożliwia takie niespójności.przykład końcowy
15.5.5 Inicjowanie pola
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
Inicjalizatory 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 do
x
ma miejsce, gdy wykonywane są inicjalizatory pól statycznych, a przypisania doi
is
występują, gdy wykonywane są inicjalizatory pól instancji.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 definicji
a
ib
, program jest prawidłowy. Rezultatem jest wynik wyjściowya = 1, b = 2
ponieważ pola statyczne
a
ib
są inicjowane do0
(wartość domyślna dlaint
) przed wykonaniem ich inicjatorów. Gdy inicjalizator dlaa
działa, wartośćb
jest zero, więca
jest inicjowany na1
. Gdy inicjator dlab
działa, wartość a jest już1
, a więcb
jest zainicjowany na2
.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 wyniku:
Init A Init B 1 1
lub dane wyjściowe:
Init B Init A 1 1
ponieważ wykonanie inicjatora
X
i inicjatoraY
może wystąpić w dowolnej kolejności; muszą wystąpić 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 konstruktor statyczny
B
(a zatem także inicjatory pól statycznychB
) będą uruchamiane przed konstruktorem statycznymA
oraz jego inicjatorami pól.przykład końcowy
Inicjowanie pola wystąpienia 15.5.6.3
Inicjalizatory zmiennych pol obiektów klasy odpowiadają sekwencji przypisań wykonywanych natychmiast przy wejściu do dowolnego konstruktora obiektu (§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.
Inicjalizator zmiennej dla pola instancji nie może odwoływać się do tworzonej instancji. W związku z tym odwoływanie się do this
w inicjatorze zmiennej jest błędem czasu kompilacji, podobnie jak odwoływanie się do dowolnego członka instancji za pośrednictwem simple_name jest błędem czasu kompilacji w inicjatorze zmiennej.
Przykład: w poniższym kodzie
class A { int x = 1; int y = x + 1; // Error, reference to instance member of this }
inicjalizator zmiennej dla
y
powoduje błąd czasu kompilacji, ponieważ odwołuje się do elementu instancji, która jest tworzona.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ę i priorytet między alternatywami tutaj jest stosowane wyłącznie dla wygody opisowej; reguły gramatyczne można opracować, aby usunąć tę nakładkę. 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 zadeklarowanej dostępności (§15.3.6), new
(§15.3.5), static
(§15.6.3), virtual
(§15.6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7), extern
(§15.6.8) 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 modyfikator
sealed
, to również zawiera modyfikatoroverride
. - Jeśli deklaracja zawiera
partial
modyfikator, nie zawiera żadnego z następujących modyfikatorów:new
,public
,protected
,internal
,private
,virtual
,sealed
,override
,abstract
lubextern
.
Metody są klasyfikowane zgodnie z tym, co zwracają, jeśli w ogóle coś zwracają.
- Jeśli
ref
jest obecny, metoda jest wykonywana jako zwracanie przez referencję i zwraca referencję do zmiennej, która może być opcjonalnie tylko do odczytu; - W przeciwnym razie, jeśli return_type to
void
, metoda nie zwraca wartości. - W przeciwnym razie metoda jest przez wartość i zwraca wartość.
return_type w deklaracji metody "zwracającej przez wartość" lub "niezwracającej wartości" określa typ wyniku, jeśli taki istnieje, zwracany przez metodę. Tylko metoda zwracana bez wartości może zawierać partial
modyfikator (§15.6.9). Jeśli deklaracja zawiera async
modyfikator, to return_type musi być void
lub metoda zwracająca przez wartość, a zwracany typ jest typem zadania (§15.15.1).
Deklaracja metody typu zwracającego przez referencję ref_return_type określa typ zmiennej, do której odnosi się variable_reference zwracane 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.
Dla ogólnego method_declaration implementującego jawny element członkowski interfejsu nie mogą istnieć żadne type_parameter_constraints_clause; deklaracja dziedziczy wszelkie ograniczenia wynikające z ograniczeń metody interfejsu.
Podobnie deklaracja metody z override
modyfikatorem nie powinna mieć żadnych type_parameter_constraints_clauses, a ograniczenia parametrów typu metody są dziedziczone z nadpisywanej 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 członka interfejsu, member_name składa się z interface_type oraz następującego po nim ".
" i identyfikatora. 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).
return_type lub ref_return_type, a każdy z typów, do których odwołuje się parameter_list metody, musi być co najmniej tak dostępny jak sama metoda (§7.5.5).
Metoda zwracająca po wartości lub nie zwracająca wartości ma ciało, które jest średnikiem, blokiem lub wyrażeniem. 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 średnika, i oznacza pojedyncze wyrażenie do wykonania podczas wywołania metody.
W przypadku metod abstrakcyjnych i zewnętrznych, method_body składa się po prostu ze średnika. W przypadku metod częściowych method_body może składać się ze ś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
, zmiennej variable_reference oraz średnika i wskazuje na pojedynczy variable_reference, który będzie oceniany, gdy metoda jest wywoływana.
W przypadku metod abstrakcyjnych i zewnętrznych ref_method_body składa się po prostu ze średnika; dla wszystkich innych metod ref_method_body jest ciałem bloku lub 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, liczby modyfikatorów_trybu_parametru (§15.6.2.1) oraz typów jej parametrów. Zwracany typ nie jest częścią podpisu metody, podobnie jak nazwy parametrów, nazw typów parametrów ani 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
.
Parametry typu metody znajdują się w zakresie deklaracji metody i mogą być używane do tworzenia typów w tym zakresie w return_type lub ref_return_type, method_body lub ref_method_body, oraz w klauzulach ograniczeń parametrów typów, 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 tej 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.
Stały_parametr składa się z opcjonalnego zestawu atrybutów (§22); opcjonalnego modyfikatora in
, out
, ref
lub this
; typu , identyfikatora i opcjonalnego argumentu_domniemyślnego . 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
, modyfikator this
może zostać połączony z modyfikatorem ref
lub in
, ale nie z 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:
- stałe_wyrażenie
- 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
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. Typ array_type tablicy parametrów powinien być 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 dla
M
,i
jest wymaganym parametremref
,d
jest wymaganym parametrem wartości,b
,s
,o
it
są opcjonalnymi parametrami wartości, aa
jest tablicą parametrów.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 uznawana za zagnieżdżoną w tej przestrzeni deklaracyjnej. To jest błąd, gdy dwa elementy w przestrzeni deklaracji metody mają 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
).
Kiedy parametr jest przekazywany przez odniesienie, odpowiedni argument w wywołaniu metody składa się z odpowiedniego słowa kluczowego in
, 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, możliwe jest, aby wiele nazw reprezentowało tę samą lokację pamięci.
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, gdy zmienia się 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
Swap
wMain
,x
reprezentujei
, ay
reprezentujej
. 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
wG
przekazuje odwołanie dos
zarówno dlaa
, jak ib
. W związku z tym, dla tego wywołania, nazwys
,a
, ib
wszystkie odwołują się do tej samej lokalizacji pamięci, a trzy przypisania modyfikują poles
wystąpienia.przykład końcowy
struct
W przypadku typu w metodzie instancji, akcesorze instancji (§12.2.1) lub konstruktorze instancji z inicjatorem konstruktora, słowo kluczowe this
zachowuje się dokładnie jak 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); } }
Przykład generuje dane wyjściowe:
c:\Windows\System\ hello.txt
Należy pamiętać, że
dir
iname
zmienne mogą być nieprzypisane przed przekazaniem ich doSplitPath
, i że są uważane za na pewno 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 F automatycznie tworzy czteroelementową tablicęint[]
z podanymi wartościami elementów i przekazuje to wystąpienie tablicy jako wartościowy parametr. Podobnie, trzecie wywołanieF
tworzy pusty elementint[]
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 rozszerzone formy nie są zatem brane pod uwagę podczas rozwiązywania przeciążenia, a pierwsze i trzecie wywołania metody wybierają zwykłe metody. Gdy klasa deklaruje metodę z parametrem tablicowym, nie jest niczym niezwykłym, aby uwzględnić także niektóre jej rozwinięte formy jako zwykłe metody. W ten sposób można uniknąć alokacji instancji tablicy, która występuje po wywołaniu wersji rozszerzonej 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
, ponieważ jest równoważneF(new string[] { null })
i przekazuje tablicę zawierającą pojedyncze odwołanie 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 object[]
jest sam w sobie niejawnie konwertowany na typ object
. Niejednoznaczność nie stanowi jednak problemu, ponieważ można ją 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 pierwszym i ostatnim wywołaniu
F
normalna formaF
ma zastosowanie, ponieważ istnieje niejawna konwersja z 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
ma zastosowanie, dlatego jest wybierana poprzez rozstrzyganie 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 instancyjne
Gdy deklaracja metody zawiera static
modyfikator, ta metoda jest uważana za metodę statyczną. Jeśli modyfikator nie jest static
obecny, mówi się, że metoda jest metodą instancji.
Metoda statyczna nie działa na określonym wystąpieniu, a odwołanie do this
w metodzie statycznej jest błędem kompilacji.
Metoda instancji działa na konkretnej instancji klasy, a do tej instancji można uzyskać dostęp jako this
(§12.8.14).
Różnice między członkami statycznymi i członkami instancji są dalej omawiane w §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. Z kolei implementacja metody wirtualnej może być 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 to wywołanie jest wykonywane, określa rzeczywistą implementację metody, którą należy wywołać. W wywołaniu metody niewirtualnej czynnikiem decydującym jest typ czasu kompilacji wystąpienia. Precyzyjnie mówiąc, gdy na wystąpieniu, którego typ w czasie kompilacji to C
, a typ w czasie wykonywania to R
, wywołana zostaje metoda o nazwie N
z listą argumentów A
, (gdzie R
jest C
lub klasa pochodna od C
), wywołanie jest przetwarzane w następujący sposób:
- W czasie powiązania rozwiązywanie przeciążeń jest stosowane do
C
,N
, iA
, aby wybrać określoną metodęM
z zestawu metod zadeklarowanych i dziedziczonych przezC
. Jest to opisane w §12.8.10.2. - Następnie w czasie wykonywania:
- Jeśli
M
jest metodą niewirtuacyjną,M
jest wywoływana. - W przeciwnym razie,
M
jest metodą wirtualną, a wywoływana jest najbardziej szczegółowa implementacjaM
w odniesieniu doR
.
- 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
, to jest to najbardziej pochodna implementacjaM
w odniesieniu doR
. - W przeciwnym razie, jeśli
R
zawiera przesłonięcie elementuM
, jest to najbardziej pochodna implementacjaM
w odniesieniu do elementuR
. - W przeciwnym razie najbardziej pochodna implementacja
M
względemR
jest taka sama jak najbardziej pochodna implementacjaM
w odniesieniu do bezpośredniej klasy bazowejR
.
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ą niewirtualną metodęF
, w związku z czym ukrywa dziedziczoną metodęF
, a także zastępuje dziedziczoną metodęG
. Przykład generuje 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 metody poza najbardziej pochodną 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(); } }
Klasy
C
iD
zawierają dwie metody wirtualne z tym samym podpisem: jedna wprowadzona przezA
i jedna wprowadzona przezC
. Metoda wprowadzona przezC
ukrywa metodę dziedziczoną zA
klasy. W związku z tym deklaracja przesłonięcia wD
przesłania metodę wprowadzoną przezC
, i nie jest możliwe, abyD
przesłonił metodę wprowadzoną przezA
. Przykład produkuje wynik: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 instancji zawiera override
modyfikator, mówi się, że metoda jest metodą nadpisującą. Metoda nadpisująca zastępuje dziedziczoną metodę wirtualną 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 nadpisywana metoda bazowa. Dla metody M
przesłonięcia zadeklarowanej w klasie C
, metoda bazowa, która jest zastępowana, jest określana przez zbadanie każdej klasy bazowej C
klasy, począwszy od bezpośredniej klasy bazowej C
i kontynuowanie z każdą kolejną bezpośrednią klasą bazową, dopóki w danej klasie bazowej M
nie znajduje się co najmniej jedna dostępna metoda, która ma ten sam podpis co M
po podstawieniu typy argumentów. Na potrzeby lokalizowania zastąpionej metody bazowej metoda jest uważana za dostępną, jeśli jest public
, jeśli jest protected
, jeśli jest protected internal
, lub jeśli jest internal
lub private protected
i jest zadeklarowana w tym samym programie co C
.
Błąd podczas kompilacji występuje, chyba że wszystkie następujące warunki są spełnione dla deklaracji nadpisania:
- Zastąpiona metoda podstawowa może być zidentyfikowana jak opisano powyżej.
- 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łaniająca. 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.
- Deklaracja przesłonięcia i przesłonięta metoda bazowa 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ęta metoda bazowa jest chroniona wewnętrznie i jest zadeklarowana w innym zestawie niż ten, który zawiera deklarację zastąpienia, wówczas deklarowana dostępność deklaracji zastąpienia powinna być chroniona.
- Deklaracja zastąpienia nie określa żadnej klauzuli ograniczeń parametrów typu. 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
base.PrintFields()
wB
wywołuje metodę PrintFields zadeklarowaną wA
. Base_access wyłącza mechanizm wywołania wirtualnego i po prostu traktuje metodę podstawową jak zwykłą metodę bez uwzględnienia jej wirtualnych właściwościvirtual
. Gdyby wywołanie wB
zostało napisane((A)this).PrintFields()
, rekursywnie wywołałoby metodęPrintFields
zadeklarowaną wB
, a nie tę zadeklarowaną wA
, ponieważPrintFields
jest wirtualna, a typ czasu wykonywania((A)this)
toB
.przykład końcowy
Tylko poprzez dołączenie modyfikatora override
metoda może zastąpić inną metodę. We wszystkich innych przypadkach metoda z tą samą sygnaturą co dziedziczona metoda po prostu ukrywa tę 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 wB
nie zawiera modyfikatoraoverride
i dlatego nie zastępuje metodyF
wA
. Zamiast tego metoda wF
ukrywa metodę wB
, a ostrzeżenie jest zgłaszane wA
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
F
wB
ukrywa odziedziczoną metodę wirtualnąF
zA
. Ponieważ nowyF
wB
ma dostęp prywatny, jego zakres obejmuje tylko ciało klasyB
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. Użycie modyfikatorasealed
przezB
zapobiega dalszemu zastępowaniuC
przezF
.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ą abstrakcyjne, muszą zastąpić metodęPaint
i dostarczyć rzeczywistą implementację.przykład końcowy
Jest to błąd czasu kompilacji, gdy base_access (§12.8.15) odwołuje 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(); }
Błąd czasu kompilacji jest zgłaszany dla wywołania
base.F()
, ponieważ odnosi się ono 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 składa się tylko z średnika. Metoda zewnętrzna nie jest ogólna.
Mechanizm łączenia 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. Identyfikator partial
jest rozpoznawany jako słowo kluczowe kontekstowe (§6.4.4) w deklaracji metody tylko wtedy, gdy pojawia się bezpośrednio przed słowem kluczowym void
. 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, mówi się, że deklaracja jest deklaracją implementującej metody częściowej. W częściach deklaracji typu może istnieć tylko jedna definiująca deklaracja metody częściowej z danym podpisem oraz co najwyżej jedna deklaracja implementująca metodę częściową z tym samym podpisem. Jeżeli zostanie podana deklaracja częściowej metody implementującej, powinna istnieć odpowiednia deklaracja definiująca częściową metodę, i deklaracje powinny być zgodne z następującymi specyfikacjami:
- 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 powinny mieć te same modyfikatory (chociaż niekoniecznie w tej samej kolejności) i takie same typy lub typy możliwe do konwersji pod względem tożsamości (z uwzględnieniem różnic w nazwach parametrów typu).
- Odpowiednie parametry typu w deklaracjach muszą mieć te same ograniczenia (z uwzględnieniem różnic w nazwach parametrów typu).
Implementacja deklaracji metody częściowej może być umieszczona w tej samej części, co deklaracja definiująca metodę częściową.
Tylko zdefiniowana metoda częściowa uczestniczy w rozpoznawaniu przeciążeń. W związku z tym, niezależnie od tego, czy podano deklarację implementacyjną, wyrażenia wywołania mogą być rozpoznawane jako wywołania metody częściowej. Ponieważ metoda częściowa zawsze zwraca void
, takie wyrażenia będą wyrażeniami instrukcji. Ponadto, ponieważ metoda częściowa jest niejawnie private
, takie wyrażenia zawsze mają miejsce 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łujące, w tym wszelkie wyrażenia podrzędne, nie ma żadnego wpływu podczas wykonywania. Sama metoda częściowa jest również usuwana i nie będzie członkiem łączonej deklaracji typu.
Jeśli deklaracja implementowania istnieje dla danej metody częściowej, wywołania metod częściowych są zachowywane. Metoda częściowa prowadzi do deklaracji metody zbliżonej do deklaracji implementacyjnej częściowej metody, z wyjątkiem następujących różnic:
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 podczas próby utworzenia delegata z
M
(§12.8.17.5).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 dostępna jest inna 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 powinien być 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, jeśli jej otaczająca klasa statyczna znajduje się w zakresie, metoda rozszerzająca może być wywołana przy użyciu składni wywoływania metody instancyjnej (§12.8.10.3), używając wyrażenia odbiorcy 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
15.6.11 Treść metody
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 void
, jeśli typ zwrotny to void
, lub jeśli metoda jest asynchroniczna i typ zwrotny 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 efektywny typ zwracany metody to void
i metoda posiada treść bloku, instrukcje return
(§13.10.5) w tym bloku nie powinny zawierać 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; }
.
Dla metody zwracającej przez wartość (§15.6.1), każda instrukcja return w treści tej metody określa wyrażenie, które jest niejawnie konwertowane na efektywny typ zwracany.
W przypadku metody zwracania przez odwołanie (§15.6.1), każda instrukcja return w treści tej metody musi określać wyrażenie, którego typ jest skutecznym typem zwrotu i ma kontekst ref-safekontekstu wywołującego (§9.7.2).
W przypadku metod zwracanych według wartości i zwracanych przez referencję, punkt końcowy ciała metody nie powinien być 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 zwracająca wartość
F
powoduje błąd czasu kompilacji, ponieważ kontrola może opuścić ciało 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 — obie struktury są nazwanymi elementami członkowskimi powiązanymi z 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ą akcesory, które określają instrukcje do wykonania, 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ść o wartości innej niż ref. 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), który może być
readonly
, dla zmiennej typu type. 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 zadeklarowanej dostępności (§15.3.6), new
(§15.3.5), static
(§15.7.2), virtual
(§15.6.4, §15.7.6), override
(§15.6.5, §15.7.6), sealed
(§15.6.6), abstract
(§15.6.7, §15.7.6), 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 właściwości musi być przynajmniej tak samo 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 oświadczenia, accessor_declarations, które są ujęte w tokenach "{
" i "}
", deklarują metody dostępu (§15.7.3) danej właściwości. Metody dostępu określają instrukcje wykonywalne skojarzone z odczytywaniem i zapisywaniem właściwości.
W property_body ciało wyrażenia składające się z =>
po którym następuje wyrażenieE
oraz średnik, jest dokładnym odpowiednikiem ciała instrukcji { get { return E; } }
, i dlatego może być używane wyłącznie do określania właściwości tylko do odczytu, gdzie wynik akcesora get jest podawany jako 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. Akcesor określa instrukcje wykonywalne związane z odczytywaniem właściwości.
W ref_property_body wyrażenia składającego się z =>
po którym następuje ref
, variable_referenceV
i średnika, jest dokładnym 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_body w accessor_declarations powinien być przedstawiony jako średnik.
15.7.2 Właściwości statyczne i instancji
Gdy deklaracja właściwości zawiera static
modyfikator, właściwość jest uważana za właściwość statyczną. Jeśli nie ma modyfikatora static
, właściwość jest uważana za właściwość instancji.
Właściwość statyczna nie jest skojarzona z określonym wystąpieniem, i odwoływanie się do this
w metodach dostępu właściwości statycznej jest błędem czasu kompilacji.
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 a elementami instancji zostały omówione w §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 wyrażana w kontekście właściwości; podczas odczytywania zastąp "właściwość/właściwości" wyrażeniem "indeksator/indeksatory" i sprawdź listę różnic między właściwościami a indeksatorami wymienionymi w §15.9.2. notatka końcowa
Deklaracje akcesorów właściwości określają instrukcje wykonywalne związane z zapisem i/lub odczytem 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 o wartości ref, 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:
- Nie należy używać accessor_modifier w interfejsie ani w jawnej implementacji składowej interfejsu.
- W przypadku właściwości lub indeksatora, który nie posiada modyfikatora
override
, accessor_modifier jest dozwolony tylko wtedy, gdy właściwość lub indeksator ma zarówno akcesor get, jak i set, a następnie dozwolone jest tylko dla jednego z tych akcesorów. - Dla właściwości lub indeksatora, który zawiera
override
modyfikator, akcesor powinien odpowiadać accessor_modifier akcesora, który jest zastępowany, jeśli posiada taki modyfikator. -
accessor_modifier musi zadeklarować 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ść
public
, dostępność zadeklarowana przez accessor_modifier może byćprivate protected
,protected internal
,internal
,protected
lubprivate
. - Jeśli właściwość lub indeksator ma zadeklarowaną dostępność
protected internal
, dostępność zadeklarowana przez accessor_modifier może być alboprivate protected
,protected private
,internal
,protected
lubprivate
. - Jeśli właściwość lub indeksator ma zadeklarowaną dostępność jako
internal
lubprotected
, to dostępność zadeklarowana przez accessor_modifier musi być alboprivate protected
, alboprivate
. - Jeśli właściwość lub indeksator ma zadeklarowaną dostępność
private protected
, to dostępność zadeklarowana przez accessor_modifier powinna byćprivate
. - Jeśli właściwość lub indeksator ma zadeklarowaną dostępność
private
, żaden accessor_modifier nie może być użyty.
- Jeśli właściwość lub indeksator ma zadeklarowaną dostępność
W przypadku właściwości nieposiadających wartości referencyjnych abstract
, extern
, ciało akcesora accessor_body dla każdego określonego akcesora jest po prostu średnikiem. Właściwość, która nie jest abstrakcyjna ani zewnętrzna, ale nie jest także indeksatorem, może również mieć accessor_body dla wszystkich akcesorów określony średnikiem; w takim 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 akcesora; lub
- treść wyrażenia, która składa się z
i wyrażenia , a także średnika, i oznacza pojedyncze wyrażenie do wykonania przy wywołaniu odpowiedniego akcesora.
W przypadku właściwości o wartości ref abstract
i extern
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, kiedy wywoływany jest akcesor get; lub
- Treść wyrażenia składa się z
=>
, następnieref
, odniesienia do zmiennej variable_reference i średnika. Odwołanie do zmiennej jest oceniane, gdy wywoływany jest akcesor 'get'.
Akcesor get dla właściwości o wartości niebędącej referencją odpowiada metodzie bez parametrów z wartością zwracaną o typie właściwości. Z wyjątkiem przypadku, gdy właściwość ta jest celem przypisania, w wyrażeniu przywołanie tej właściwości spowoduje wywołanie jej metody dostępu get 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 akcesora typu "get" nie powinien być osiągalny.
Akcesor get dla właściwości wartości-odwołaniowej odpowiada metodzie bez parametrów ze zwracaną wartością variable_reference do zmiennej tego samego typu co właściwość. Gdy taka właściwość jest przywoływana w wyrażeniu, jego akcesor get jest wywoływany 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.
Akcesor set odpowiada metodzie z jednym parametrem wartości typu właściwości i typem zwracanym void
. Niejawny parametr akcesora zestawu ma zawsze nazwę value
. Gdy właściwość jest przywoływana jako cel przypisania (§12.21), lub jako operand dla operatora ++
lub –-
(§12.8.16, §12.9.6), akcesor ustawiający jest wywoływany z argumentem, który dostarcza nową wartość (§12.21.2). Treść akcesora typu set musi być zgodna z zasadami metod opisanymi w §15.6.11. W szczególności instrukcje return w treści akcesora set nie mogą określać wyrażenia. Ponieważ akcesor set niejawnie ma parametr o nazwie value
, deklarowanie zmiennej lokalnej lub stałej o tej samej nazwie w akcesorze set prowadzi do błędu w czasie kompilacji.
Zależnie od obecności lub braku metod dostępu get i set, właściwość klasyfikuje się w następujący sposób:
- Właściwość, która zawiera zarówno metodę dostępu get, jak i set, nazywana jest właściwością odczytu-zapisu.
- Właściwość, która ma tylko metodę get, nazywana jest właściwością tylko do odczytu. Błąd w czasie kompilacji występuje, jeśli właściwość tylko do odczytu jest celem 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 celu przypisania, odwołanie się do właściwości tylko do zapisu w wyrażeniu jest błędem czasu kompilacji.
Uwaga: Operatory prefiksowe i postfiksowe
++
oraz operatory przypisania złożonego nie mogą być stosowane do właściwości tylko do zapisu, ponieważ te operatory odczytują starą wartość swojego operandu przed zapisaniem nowej. 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
. Akcesor get właściwości Caption zwracastring
przechowywane w polu prywatnymcaption
. Akcesor dostępu sprawdza, czy nowa wartość różni się od bieżącej wartości, a jeśli tak, przechowuje nową wartość i odświeża kontrolkę. Właściwości często są zgodne ze wzorcem pokazanym powyżej: Akcesor get po prostu zwraca wartość przechowywaną w poluprivate
, natomiast akcesor set modyfikuje to poleprivate
i następnie wykonuje wszelkie dodatkowe akcje wymagane do pełnego zaktualizowania stanu obiektu.Button
Biorąc pod uwagę powyższą 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
Tutaj akcesor set jest wywoływany przez przypisanie wartości do właściwości, a akcesor get 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ść wB
ukrywaP
właściwość wA
zarówno przy odczycie, jak i zapisie. W ten sposób w stwierdzeniachB b = new B(); b.P = 1; // Error, B.P is read-only ((A)b).P = 1; // Ok, reference to A.P
Przypisanie do
b.P
powoduje zgłoszenie błędu czasu kompilacji, ponieważ właściwość tylko do odczytuP
wB
ukrywa właściwość tylko do zapisuP
wA
. 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; }
Tutaj klasa
Label
używa dwóch pólint
,x
iy
, aby przechowywać swoją lokalizację. Lokalizacja jest publicznie ujawniona zarówno jako właściwośćX
iY
, jak i jako właściwośćLocation
typuPoint
. 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
iy
zamiast tego były polamipublic readonly
, byłoby niemożliwe wprowadzenie takiej zmiany w klasieLabel
.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 akcesorów rzeczywistym kodem akcesorów. Proces ten jest znany jako wstawianie w linię i sprawia, że dostęp do właściwości jest równie wydajny jak dostęp do pól, a jednocześnie zachowuje zwiększoną elastyczność właściwości. notatka końcowa
Przykład: Ponieważ wywołanie akcesora get jest koncepcyjnie równoważne z odczytem wartości pola, uważa się za zły styl programowania, jeśli akcesory get mają obserwowalne skutki uboczne. 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 akcesorów dostępu nie oznacza, że get akcesory nie musi być zawsze pisane tak, by po prostu zwracać wartości przechowywane 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 akcesory pobierające nie wykonują żadnych operacji, 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łaniu się do właściwościOut
, jak wConsole.Out.WriteLine("hello, world");
Tworzony jest podstawowy element
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 nieabstrakcyjną, nieextern, niemającą wartości ref właściwością, której accessor_body zawierają tylko średniki. Właściwości automatyczne mają akcesor get i mogą mieć opcjonalnie akcesor set.
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 zapasowego pola tylko do odczytu właściwości.
Właściwość automatyczna może opcjonalnie mieć property_initializer, który jest stosowany bezpośrednio do pola pomocniczego 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
Chociaż pole zapasowe jest ukryte, może mieć przypisane atrybuty docelowe bezpośrednio dzięki automatycznie wdrożonej 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, że atrybut ukierunkowany na pole
NonSerialized
jest zastosowany do pola pomocniczego 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 akcesor ma accessor_modifier, domena dostępności (§7.5.3) akcesora jest określana przy użyciu zadeklarowanej dostępności accessor_modifier. Jeśli akcesor nie ma accessor_modifier, domena dostępności akcesora jest określana na podstawie określonej dostępności właściwości lub indeksatora.
Obecność accessor_modifier nigdy nie wpływa na wyszukiwanie składowych (§12.5) ani na rozstrzyganie 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 właściwości lub indeksatora, nie mających wartości ref, domeny dostępności specyficznych akcesorów są używane do określenia, czy takie użycie jest prawidłowe.
- Jeśli użycie jest jako wartość (§12.2.2), akcesor "get" musi istnieć i być dostępny.
- Jeśli użycie jest celem prostego przypisania (§12.21.2), akcesor ustawiający musi istnieć i być dostępny.
- 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 akcesory pobierające, jak i ustawiające muszą istnieć i być dostępne.
Przykład: W poniższym przykładzie właściwość
A.Text
jest ukryta przez właściwośćB.Text
, nawet w kontekstach, w których wywoływany jest tylko akcesor set. 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 ref-valued — niezależnie od tego, czy użycie występuje jako wartość, jako cel prostego przypisania, czy jako cel przypisania złożonego — domena dostępności zaangażowanego akcesora get jest używana do określenia, czy to użycie jest prawidłowe.
Akcesorium używane do implementowania interfejsu nie powinno mieć accessor_modifier. Jeśli do zaimplementowania interfejsu jest używany tylko jeden akcesor, drugi akcesor może być zadeklarowany 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 sformułowana w kontekście właściwości. Podczas czytania klauzuli w odniesieniu do indeksatorów, zastąp terminy indeksator/indeksatory zamiast właściwość/właściwości i zapoznaj się z listą różnic między właściwościami a indeksatorami podaną 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 akcesor właściwości wirtualnej ma private
accessor_modifier, akcesor prywatny nie jest domyślnie wirtualny.
Deklaracja właściwości abstrakcyjnej deklaruje, że akcesory właściwości są wirtualne, ale nie dostarcza rzeczywistej implementacji tych akcesorów. 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ż akcesor abstrakcyjnej deklaracji właściwości nie zapewnia rzeczywistej implementacji, jego accessor_body po prostu składa się z średnika. Właściwość abstrakcyjna nie może mieć 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). Akcesory dziedziczonej właściwości wirtualnej można zastąpić w klasie pochodnej poprzez dołączenie deklaracji właściwości, która określa dyrektywę override
. Jest to nazywane deklaracją właściwości nadrzędnej. Deklaracja zastępująca właściwość 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ęta właściwość podstawowa jest chroniona wewnętrznie i jest zadeklarowana w innym asemblażu niż asemblaż zawierający deklarację zastąpienia, wtedy deklarowana dostępność powinna być 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. Między typem właściwości zastępującej i dziedziczonej powinna istnieć konwersja tożsamościowa.
Deklaracja nadpisująca właściwość może zawierać modyfikator sealed
. 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, akcesory wirtualne, zapieczętowane, zastępowane i abstrakcyjne zachowują się dokładnie tak jak odpowiednie 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:
- Akcesor get odpowiada metodzie bez parametrów, która zwraca wartość typu właściwości i ma te same modyfikatory jak właściwość, którą zawiera.
- 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, klasa A, która ją zawiera, również powinna być zadeklarowana jako abstrakcyjna.Poniżej przedstawiono klasę pochodzącą z
A
.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. pl-PL: Metoda dostępu get zX
i metoda dostępu set zY
używają słowa kluczowego base, aby uzyskać dostęp do odziedziczonych metod dostępu. DeklaracjaZ
przesłania zarówno abstrakcyjne metody dostępu — w związku z tym nie ma żadnych brakującychabstract
składowych funkcji wB
, iB
może być nieabstrakcyjną klasą.przykład końcowy
Gdy właściwość jest zadeklarowana jako przesłonięta, wszelkie przesłonięte akcesory muszą być dostępne dla kodu przesłaniającego. Ponadto deklarowana dostępność zarówno samej właściwości lub indeksatora, jak i odpowiednich akcesorów, powinna odpowiadać dostępności przesłoniętego członka i jego akcesoró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.6.4, §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_declaration, definiuje jedno lub więcej zdarzeń — jedno dla każdego z variable_declaratorów. Atrybuty i modyfikatory mają zastosowanie do wszystkich członków deklarowanych przez takie event_declaration.
Jest to błąd czasu kompilacji, gdy
Gdy deklaracja zdarzenia zawiera modyfikator extern
, zdarzenie nazywa się zewnętrznym zdarzeniem. Ponieważ deklaracja zdarzenia zewnętrznego nie zawiera rzeczywistej implementacji, jest błędem, aby zawierała zarówno extern
modyfikator, jak i event_accessor_declaration.
Jest to błąd kompilacji, jeśli variable_declarator deklaracji zdarzenia z modyfikatorem abstract
lub external
zawiera variable_initializer.
Zdarzenie może być stosowane jako lewy operand operatorów +=
i -=
. Te operatory są używane odpowiednio do dołączania programów obsługi zdarzeń do zdarzenia lub usuwania programów obsługi zdarzeń ze zdarzenia, a modyfikatory dostępu do zdarzeń kontrolują konteksty, w których takie operacje są dozwolone.
Jedynymi operacjami, które są dozwolone na zdarzeniu przez kod znajdujący się poza typem, w którym to zdarzenie jest zadeklarowane, są +=
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 w formie x += y
lub x –= y
, gdy x
jest zdarzeniem, wynik operacji ma typ void
(§12.21.5) (w przeciwieństwie do typu x
, z wartością x
po przypisaniu, jak w przypadku innych operatorów +=
i -=
zdefiniowanych na 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 dwa wystąpieniaButton
i dołącza programy obsługi zdarzeń do zdarzeńClick
.przykład końcowy
15.8.2 Zdarzenia przypominające pola
W tekście programu klasy lub struktury, która zawiera deklarację zdarzenia, niektóre zdarzenia mogą być używane jak pola. Aby można było wykorzystać zdarzenie w ten sposób, nie może być ono abstrakcyjne ani oznaczone jako extern, i nie powinno jawnie zawierać event_accessor_declaration. Takie zdarzenie może być używane w dowolnym kontekście, który dopuszcza pole. Pole zawiera delegata (§20), który odnosi się do listy obsługiwaczy zdarzeń dodanych do zdarzenia. Jeśli nie dodano żadnych procedur obsługi zdarzeń, pole zawiera null
.
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
w klasieButton
"wywołuje" zdarzenieClick
. 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ą klasy
Button
członkaClick
można używać tylko po lewej stronie operatorów+=
i–=
, jak вb.Click += new EventHandler(...);
dołącza delegata do listy wywołań zdarzenia
Click
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 muszą) być wykonywane podczas trzymania blokady (§13.13) na obiekcie zawierającym dla zdarzenia instancyjnego lub na obiekcie (§12.8.18) dla zdarzenia statycznego.
Uwaga: W związku z tym deklaracja zdarzenia wystąpienia w formie:
class X { public event D Ev; }
powinno być skompilowane na coś równoważnego z:
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 poEv
po lewej stronie operatorów+=
i–=
powodują wywołanie akcesorów dodawania i usuwania. Wszystkie inne odwołania doEv
są kompilowane w celu odwołania się do ukrytego pola__Ev
(§12.8.7). Nazwa "__Ev
" jest dowolna; ukryte pole może mieć dowolną nazwę lub żadną nazwę.notatka końcowa
15.8.3 Dostęp do 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 przechowywania 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 deklaracje akcesorów zdarzeń określają instrukcje wykonywalne skojarzone z dodawaniem i usuwaniem obsługiwaczy 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 zdarzenia, używany jest odpowiedni akcesor zdarzenia. W szczególności, jeśli operatorem przypisania jest +=
, używany jest akcesor dodawania, a jeśli operatorem przypisania jest –=
, używany jest akcesor usuwania. W obu przypadkach prawy operand operatora przypisania jest używany jako argument akcesora zdarzenia. Blok add_accessor_declaration lub remove_accessor_declaration musi być zgodny z zasadami dotyczącymi metod opisanych w §15.6.9. W szczególności return
instrukcje w takim bloku nie mogą określać wyrażenia.
Ponieważ akcesor zdarzenia niejawnie ma parametr o nazwie value
, jest to błąd czasu kompilacji, jeśli zmienna lokalna lub stała zadeklarowana w akcesorze zdarzenia ma 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 przechowywania został zaprojektowany tak, aby nie było kosztowne skojarzenie delegata null z kluczem, a tym samym nieobsługiwane zdarzenia nie zajmują miejsca do przechowywania.przykład końcowy
15.8.4 Zdarzenia statyczne i wystąpienia
Gdy deklaracja zdarzenia zawiera static
modyfikator, zdarzenie jest podobno zdarzeniem statycznym. Kiedy nie ma static
modyfikatora, mówi się, że zdarzenie jest zdarzeniem instancji.
Zdarzenie statyczne nie jest związane z konkretnym wystąpieniem, a odwołanie się do this
w akcesorach zdarzenia statycznego skutkuje błędem w czasie kompilacji.
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 a elementami instancji zostały omówione w §15.3.8.
15.8.5 Wirtualne, zapieczętowane, zastępowane i abstrakcyjne metody dostępu
Deklaracja zdarzenia wirtualnego określa, że akcesory tego zdarzenia są wirtualne. Modyfikator virtual
ma zastosowanie do obu akcesorów zdarzenia.
Deklaracja zdarzenia abstrakcyjnego określa, że akcesory zdarzenia są wirtualne, ale nie dostarcza rzeczywistej implementacji tych akcesorów. 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).
Akcesory dziedziczonego zdarzenia typu wirtualnego mogą zostać zastąpione w klasie pochodnej przez dołączenie deklaracji zdarzenia, która określa modyfikator override
. Jest to nazywane deklaracją zdarzenia zastępującą. Nadpisanie deklaracji zdarzenia nie tworzy nowego zdarzenia. Zamiast tego po prostu specjalizuje się w implementacjach akcesoriów istniejącego zdarzenia wirtualnego.
Deklaracja zdarzenia zastępującego powinna określać dokładnie te same modyfikatory dostępu i nazwę, co zdarzenie zastępowane, między typem zdarzenia zastępującego a typem wydarzenia zastępowanego powinna występować konwersja tożsamości, a zarówno akcesor dodający, jak i usuwający powinny zostać określone w tej deklaracji.
Deklaracja zastępująca zdarzenie może obejmować sealed
modyfikator.
this
Użycie modyfikatora uniemożliwia dalsze zastępowanie zdarzenia przez klasę pochodną. Dostępy do zabezpieczonego zdarzenia są również zabezpieczone.
Jest to błąd kompilacji, gdy w przesłaniającej deklaracji zdarzenia zawiera się modyfikator new
.
Z wyjątkiem różnic w składni deklaracji i wywołania, wirtualne, zapieczętowane, zastępowane i abstrakcyjne akcesory zachowują się tak samo 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 akcesory były metodami odpowiedniego rodzaju. Każdy akcesor odpowiada metodzie z pojedynczym parametrem wartości typu zdarzenia, typem zwracanym void
i tymi samymi modyfikatorami co zdarzenie, które zawiera.
15.9 Indeksatory
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 type. Ten rodzaj indeksatora może być czytelny i/lub zapisywalny.
- Drugi deklaruje indeksator o wartości ref. Jego wartość to variable_reference (§9.5), który może odnosić się do zmiennej typu type. Ten rodzaj indeksatora jest dostępny tylko do odczytu.
Indexer_declaration może zawierać zestaw atrybutów (§22) oraz dowolny z dozwolonych rodzajów deklarowanej dostępności (§15.3.6), new
(§15.3.5), virtual
(§15.6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7) i extern
(§15.6.8) modyfikatorów.
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, typ jest następnie ze słowem kluczowym this
. W przypadku jawnej implementacji elementu członkowskiego interfejsu, typ jest następnie poprzedzony przez interface_type, ".
" i słowo kluczowe this
. W przeciwieństwie do innych członków, 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).
Treść indeksatora (indexer_body) może składać się z ciała instrukcji (§15.7.1) lub ciała wyrażenia (§15.6.1). W treści instrukcji, accessor_declarations, które muszą być ujęte w tokenach „{
” i „}
”, deklarują akcesory (§15.7.3) indeksatora. Metody dostępu określają polecenia do wykonania związane z odczytywaniem i zapisywaniem elementów indeksatora.
W indexer_body treść wyrażenia składająca się z „=>
”, po której następuje wyrażenieE
i średnik, jest dokładnym odpowiednikiem treści instrukcji { get { return E; } }
, i dlatego może być używana tylko do określania indeksatorów wyłącznie do odczytu, gdzie wynik akcesora get 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. Akcesor określa instrukcje wykonywalne związane z odczytem indeksatora.
W ref_indexer_body treść wyrażenia składająca się z =>
, po którym następuje ref
, variable_referenceV
i średnik jest dokładnym odpowiednikiem treści instrukcji { get { return ref V; } }
.
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 nazywany jest 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 klasy
BitArray
zużywa znacznie mniej pamięci niż odpowiadająca mu klasabool[]
(ponieważ każda wartość pierwszej zajmuje tylko jeden bit zamiast jednegobyte
), ale umożliwia te same operacje cobool[]
.W poniższej klasie
CountPrimes
używa się klasycznego algorytmu "sita"BitArray
do obliczenia liczby liczb 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 musi być wielką lub małą literą w zakresie A–Z, a drugi musi być liczbą całkowitą 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 członkiem, natomiast indeksator jest zawsze członkiem instancji.
- Akcesor get właściwości odpowiada metodzie bez parametrów, natomiast akcesor get indeksatora odpowiada metodzie z tą samą listą parametrów co indeksator.
- Akcesor ustawiający właściwości odpowiada metodzie z pojedynczym parametrem o nazwie
value
, natomiast akcesor ustawiający indeksatora odpowiada metodzie z tą samą listą parametrów co indeksator oraz dodatkowym parametrem o nazwievalue
. - Występuje błąd czasu kompilacji, gdy metoda dostępu indeksatora posiada lokalną zmienną lub stałą o tej samej nazwie co 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 jest nie tylko nieabstrakcyjny, ale także niezewnętrzny, z dwukropkami 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/cech indeksatorem/indeksatorami podczas odczytywania §15.7.3, §15.7.5 i §15.7.6 odnosi się również do zdefiniowanych terminów. W szczególności właściwość do odczytu i zapisu staje się indeksatorem do odczytu i zapisu, 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.
15.10 Operatory
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 =>
, po którym następuje wyrażenie i średnik, i oznacza pojedyncze wyrażenie do wykonania przy 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 powinna zawierać zarówno modyfikator
public
, jak istatic
. - 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 inni członkowie, 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 powinien przyjmować jeden parametr typuT
lubT?
i może zwrócić dowolny typ. - Jednoargumentowy
++
lub--
operator powinien przyjmować jeden parametr typuT
lubT?
, i powinien zwracać ten sam typ lub typ pochodzący z niego. - Jednoargumentowy
true
lubfalse
operator powinien mieć jeden parametr typuT
lubT?
, a 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 jednoargumentowe true
i false
wymagają deklaracji parami. 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ść uzyskaną przez dodanie 1 do operandu, podobnie jak operatory postfiksyjne inkrementacji i dekrementacji (§12.8.16) oraz operatory prefiksyjne inkrementacji 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ą semantykę operatora inkrementacji postfiksowej (§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>>
(§12.11) bierze dwa parametry, z których pierwszy ma typT
lubT?
, a drugi ma typint
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 w parach. Dla każdej deklaracji jednego operatora pary musi istnieć zgodna deklaracja innego operatora pary. Dwa deklaracje operatorów pasują do siebie, jeśli istnieją konwersje tożsamości między ich typami zwracanymi a odpowiadającymi im typami parametrów. Następujące operatory wymagają deklarowania w parach:
- 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 S
i typu docelowego T
, jeśli S
lub T
są typami dopuszczającymi wartości null, niech S₀
i T₀
odwołują się do ich typów bazowych; w przeciwnym razie S₀
i T₀
są odpowiednio równe S
i T
. 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
S₀
, aniT₀
nie jest interface_type.Z wyłączeniem konwersji zdefiniowanych przez użytkownika, nie istnieje konwersja z
S
doT
ani 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
doint
i zint
doC
, 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 (istnieje konwersja niejawna, a tym samym jawna, z dowolnego typu na typ object).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 istnieje wstępnie zdefiniowana niejawna konwersja (§10.2) z typu
S
do typuT
, wszystkie konwersje zdefiniowane przez użytkownika (niejawne lub jawne) zS
doT
są ignorowane. - Jeśli istnieje wstępnie zdefiniowana jawna konwersja (§10.3) z typu
S
na typT
, to wszystkie jawne konwersje zdefiniowane przez użytkownika zS
naT
są ignorowane. Ponadto:- Jeśli którykolwiek z
S
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 którykolwiek z
Dla wszystkich typów z wyjątkiem object
operatory zadeklarowane powyżej przez typ Convertible<T>
nie powodują konfliktu z 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 oprócz jednego.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
nabyte
jest niejawna, ponieważ nigdy nie zgłasza wyjątków ani nie traci informacji, ale konwersja zbyte
naDigit
jest jawna, ponieważDigit
może reprezentować tylko podzbiór możliwych wartościbyte
.przykład końcowy
15.11 Konstruktory wystąpień
15.11.1 Ogólne
Konstruktor instance constructor jest członkiem, 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).
Deklaracja konstruktora może zawierać zestaw atrybutów (§22), dowolny z dozwolonych rodzajów zadeklarowanej dostępności (§15.3.6) oraz modyfikator extern
(§15.6.8). Deklaracja konstruktora nie może zawierać tego samego modyfikatora wiele razy.
Identyfikator constructor_declarator powinien nazywać klasę, w której zadeklarowany jest konstruktor instancji. 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, które są wymienione w parameter_list konstruktora wystąpienia, musi być co najmniej tak samo 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 nazywany jest 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
=>
, po którym następuje wyrażenie i średnik, i oznacza pojedyncze wyrażenie, aby zainicjować nowe wystąpienie klasy.
"Ciało konstrukcyjne, które jest ciałem bloku lub wyrażenia, odpowiada dokładnie blokowi metody instancji 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 wyrażenia tworzenia obiektów (§12.8.17.2) oraz przez inicjalizatory konstruktora.
Inicjalizatory 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 ciałem konstruktora. Konstruktor, który ma być niejawnie wywoływany, jest określany przez constructor_initializer:
- Inicjalizator konstruktora instancji w postaci
base(
argument_list)
(gdzie argument_list jest opcjonalny) powoduje wywołanie konstruktora instancji z najbliższej klasy bazowej. Ten konstruktor jest wybierany przy użyciu argument_list i reguł rozpoznawania przeciążenia §12.6.4. Zestaw konstruktorów obiektów kandydujących składa się ze wszystkich dostępnych konstruktorów obiektów bezpośredniej klasy bazowej. Jeśli ten zestaw jest pusty lub nie można zidentyfikować jednego najlepszego konstruktora wystąpienia, wystąpi błąd czasu kompilacji. - Inicjalizator konstruktora instancji w formie
this(
argument_list)
(gdzie argument_list jest opcjonalna) wywołuje inny konstruktor instancji z tej samej klasy. Konstruktor jest wybierany przy użyciu argument_list i reguł rozpoznawania przeciążenia §12.6.4. Zestaw kandydatów konstruktorów instancji składa się ze wszystkich konstruktorów instancji 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 obiektu odwołuje się do samej siebie za pośrednictwem łańcucha co najmniej jednego inicjalizatora konstruktora, wystąpi błąd czasu kompilacji.
Jeśli konstruktor wystąpienia nie ma inicjatora konstruktora, niejawnie udostępniany jest inicjator konstruktora w postaci base()
.
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 określonych przez parameter_list deklaracji konstruktora instancji obejmuje inicjator tego konstruktora. 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.
Inicjalizatory zmiennych wystąpień 15.11.3
Gdy konstruktor wystąpienia nie ma inicjatora konstruktora lub ma inicjator konstruktora w postaci base(...)
, ten konstruktor niejawnie wykonuje inicjalizacje określone przez inicjalizator zmiennej pól wystąpienia zadeklarowanych w tej 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 nowy
B()
używany jest do tworzenia wystąpieniaB
, generowane są następujące dane wyjściowe:x = 1, y = 0
Wartość
x
wynosi 1, ponieważ inicjator zmiennej jest wykonywany przed wywołaniem konstruktora instancji klasy bazowej. Jednak wartośćy
to 0 (wartość domyślna klasyint
), ponieważ przypisanie doy
nie jest wykonywane dopiero po powrocie konstruktora klasy bazowej. Warto myśleć o inicjatorach zmiennych wystąpień i inicjatorach konstruktorów jako o instrukcjach, 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, tak jakby miał inicjator konstruktora w formie 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 postaci
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ć jednoznacznie najlepszego kandydata dla 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 instancji. Zatem 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 członkiem, 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 powinien nazywać 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 nazywany jest 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
- ciało wyrażenia, które składa się z
=>
następującego po nim wyrażenia i średnika, i wskazuje pojedyncze wyrażenie do wykonania w celu zainicjowania klasy.
Ciało_konstruktora_statycznego, które jest blokiem lub ciałem wyrażeniowym, odpowiada dokładnie ciału_metody statycznej metody z typem zwracanym 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:
- Tworzone jest wystąpienie klasy.
- Odwołano się do dowolnych statycznych elementów członkowskich klasy.
Jeśli klasa zawiera metodę Main
(§7.1), w której rozpoczyna się wykonywanie, konstruktor statyczny dla tej klasy jest wywoływany przed wywołaniem metody Main
.
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 końcu wykonywany jest 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 uruchomić metodę
Main
, system najpierw wywołuje inicjator dla klasyB.Y
, przed uruchomieniem konstruktora statycznego klasyB
.Y
inicjalizator powoduje uruchomienie konstruktoraA
static
, ponieważ przywołano wartośćA.X
. 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 na 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 deterministyczny, 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 członek, 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 powinien nazywać klasę, w której finalizator jest zadeklarowany. Jeśli zostanie określona inna nazwa, wystąpi błąd czasu kompilacji.
Gdy deklaracja finalizatora zawiera extern
modyfikator, finalizator jest określany jako zewnętrzny finalizator. Ponieważ zewnętrzna deklaracja finalizatora nie zawiera rzeczywistej implementacji, jego finalizer_body zawiera jedynie średnik. 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
=>
, a następnie wyrażenia i średnika, i wskazuje pojedyncze wyrażenie do wykonania w celu sfinalizowania instancji klasy.
Finalizer_body, który jest blokiem lub treścią wyrażenia, odpowiada dokładnie method_body metody instancji z typem zwracanym void
(§15.6.11).
Finalizatory nie są dziedziczone. W związku z tym klasa nie ma finalizatorów innych niż ten, który może być zadeklarowany w tej klasie.
Uwaga: Ponieważ finalizator musi być bez parametrów, nie można go przeciążać, 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ć. Instancja kwalifikuje się do finalizacji, gdy nie jest już możliwe jej użycie przez jakikolwiek kod. 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(); } }
jest
B's finalizer A's finalizer
ponieważ finalizatory w łańcuchu dziedziczenia są wywoływane w kolejności, od najbardziej pochodnych do najmniej pochodnych.
przykład końcowy
Finalizatory są implementowane przez zastąpienie wirtualnej metody Finalize
na 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, lista parametrów tego elementu nie może zawierać parametrów in
, out
, ref
ani parametru typu ref struct
— w przeciwnym razie wystąpi błąd czasu kompilacji.
Interfejsy modułu wyliczającego 15.14.2
Interfejsy modułów wyliczających to interfejs niegeneryczny System.Collections.IEnumerator
i wszystkie wystąpienia interfejsu ogólnego System.Collections.Generic.IEnumerator<T>
. Ze względu na zwięzłość, w tej podklauzuli i podobnych podklauzulach te interfejsy są przywoływane 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 podpunkcie i jego odpowiednikach te interfejsy są przywoływane 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 wyniku iteratora, który zwraca
IEnumerator
lubIEnumerable
, toobject
. - 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 wyliczający jest tworzony i zwracany. Ten obiekt hermetyzuje kod określony w bloku iteratora, a wykonanie kodu w bloku iteratora ma miejsce, gdy metoda obiektu wyliczającego jest wywoływana. 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 przed.
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 podpunktach opisano wymagane zachowanie członków MoveNext
, Current
i Dispose
w interfejsach IEnumerator
i IEnumerator<T>
dostarczone przez obiekt modułu wyliczającego.
Obiekty wyliczające nie obsługują metody IEnumerator.Reset
. Wywołanie tej metody powoduje zgłoszenie wyjątku 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
zależy od stanu obiektu wyliczającego, gdy jest wywoływany MoveNext
:
- 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 działający.
- 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 - pl-PL: Wznawia wykonywanie bloku iteratora bezpośrednio po instrukcji yield return, która spowodowała zawieszenie wykonywania, i trwa aż do momentu przerwania wykonywania (zgodnie z poniższym opisem).
- Jeśli stan obiektu modułu wyliczającego jest po, wywołanie
MoveNext
zwraca wartość false.
Po wykonaniu bloku iteratora przez MoveNext
wykonywanie może zostać przerwane na cztery sposoby: przez instrukcję yield return
, przez instrukcję yield break
, napotykając koniec bloku iteratora, i przez zgłoszenie wyjątku oraz jego propagację poza blok iteratora.
- W momencie napotkania instrukcji
yield return
(§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 ciała iteratora jest zawieszone. Wartości wszystkich zmiennych lokalnych i parametrów (w tym
this
) są zapisywane, podobnie jak lokalizacja tejyield return
instrukcji. Jeśli instrukcjayield return
znajduje się w co najmniej jednymtry
bloku, powiązane bloki finally nie są wykonywane w tym czasie. - Stan obiektu modułu wyliczającego jest zmieniany na zawieszony.
- Metoda
MoveNext
zwracatrue
do wywołującego, wskazując, że iteracja pomyślnie przeszła do następnej wartości.
- Wyrażenie podane w instrukcji jest obliczane, niejawnie konwertowane na typ plonu i przypisywane do
- Po napotkaniu instrukcji
yield break
(§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
do swojego wywołującego, wskazując, że iteracja została ukończona.
-
- Po napotkaniu końca ciała iteratora:
- Stan obiektu modułu wyliczającego jest zmieniany na później.
- Metoda
MoveNext
zwracafalse
do wywołującego, wskazując, że iteracja została ukończona.
- Kiedy wyjątek jest zgłoszony i rozprzestrzenia się poza blokiem iteratora:
- Odpowiednie
finally
bloki w ciele iteratora zostały wykonane poprzez propagację wyjątku. - Stan obiektu modułu wyliczającego jest zmieniany na później.
- Propagacja wyjątku kontynuuje się do wywołującego metodę
MoveNext
.
- Odpowiednie
15.14.5.3 Bieżąca właściwość
Właściwość obiektu wyliczającego Current
jest wpływana przez instrukcje yield return
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 stanie przed, uruchomiony lub po, wynik dostępu Current
jest nieokreślony.
W przypadku iteratora o typie wyniku innym niż object
, wynik uzyskiwania dostępu do Current
za pośrednictwem implementacji obiektu wyliczającego odpowiada dostępności do Current
przez implementację obiektu IEnumerator<T>
wyliczającego i rzutowania wyniku na object
.
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 wyliczającego jest zawieszony, wywołanie
Dispose
:- Zmienia stan na uruchomiony.
- Wykonuje wszystkie bloki finally tak, jakby ostatnio wykonana instrukcja
yield return
była instrukcjąyield break
. Jeśli spowoduje to zgłoszenie wyjątku i jego propagację z treści iteratora, stan obiektu enumeratora jest ustawiony na after, a wyjątek jest propagowany do wywołującego metodęDispose
. - Zmienia stan na after.
- Jeśli stan obiektu enumeratora 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. - Inicjuje się od kopii wartości argumentów (jeśli istnieją) i wartości instancji przekazywanej do członka 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 zwraca swoje własne wystąpienie (aby oszczędzić na alokacjach) od pierwszego wywołania metodyGetEnumerator
. 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 (async
modyfikator.
Jest to błąd czasu kompilacji, jeśli lista parametrów funkcji asynchronicznej określa jakiekolwiek parametry in
, out
lub ref
, lub jakiegokolwiek parametru typu ref struct
.
return_type metody asynchronicznej to void
lub 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 oraz typy tworzone z System.Threading.Tasks.Task<TResult>
są typami 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.
Metodę asynchroniczną, która zwraca typ zadania, określa się jako zwracającą zadanie.
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 nieukończone, ukończone lub niepowodzenia. Zadanie z błędem rejestruje istotny wyjątek. Rejestrowanie wyniku typu T
succeeded«TaskType»<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ńMyTaskMethodBuilder<T>
i typem awaiteraAwaiter<T>
.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 buildera 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. Oceny mogą zostać później wznowione w miejscu wyrażenia await, które zostało wstrzymane za pomocą delegata wznowienia. Delegat wznowienia jest typu System.Action
, a po wywołaniu ewaluacja wywołania funkcji asynchronicznej zostanie wznowiona przy wyrażeniu 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ących członków (w przypadku typów konstruktorów zadań innych niż ogólne, SetResult
nie mają parametrów) z zadeklarowaną dostępnością public
:
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()
jest wywoływany w celu utworzenia wystąpienia «TaskBuilderType» o nazwiebuilder
na tej liście. -
builder.Start(ref stateMachine)
jest wywoływany, aby skojarzyć obiekt konstruktora z instancją maszyny stanów wygenerowaną przez kompilator,stateMachine
.- Budowniczy powinien wywołać
stateMachine.MoveNext()
wStart()
lub po tym, jakStart()
zakończy działanie, aby przejść do kolejnego stanu maszyny stanów.
- Budowniczy powinien wywołać
- Po powrocie
Start()
metodaasync
wywołujebuilder.Task
dla zadania, aby wróciło z metody asynchronicznej. - Każde wywołanie
stateMachine.MoveNext()
spowoduje 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
e
zostanie zgłoszony w maszynie stanów,builder.SetException(e)
zostanie wywołany. - Jeśli maszyna stanu osiągnie
await expr
wyrażenie,expr.GetAwaiter()
jest wywoływane. - Jeśli program awaiter implementuje
ICriticalNotifyCompletion
iIsCompleted
ma wartość false, maszyna stanu wywołujebuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
.-
AwaitUnsafeOnCompleted()
powinien wywołaćawaiter.UnsafeOnCompleted(action)
zAction
, który wywołujestateMachine.MoveNext()
po zakończeniu awaitera.
-
- W przeciwnym razie maszyna stanu wywołuje element
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
.-
AwaitOnCompleted()
powinien wywołaćawaiter.OnCompleted(action)
z funkcjąAction
, która wywołastateMachine.MoveNext()
w momencie zakończenia awaitera.
-
-
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)
, tostateMachine
wywołabuilder.SetStateMachine(stateMachine)
na instancji konstruktora skojarzonej zstateMachine
.
- Jeśli konstruktor wywoła
Uwaga: Zarówno parametr
SetResult(T result)
, jak i argument«TaskType»<T> Task { get; }
muszą być odpowiednio możliwe do identycznej konwersji naT
. To pozwala konstruktorowi typów zadań obsługiwać typy, takie jak krotki, gdzie dwa różne typy są wzajemnie konwertowalne w kontekście tożsamości. notatka końcowa
15.15.3 Ocena asynchronicznej funkcji zwracającej zadanie
Wywołanie funkcji asynchronicznej zwracającej zadanie powoduje wygenerowanie instancji zwracanego typu zadania. Jest to nazywane zadaniem zwrotnym funkcji asynchronicznej. Zadanie jest początkowo w stanie niekompletnym.
Treść funkcji asynchronicznej jest następnie oceniana do momentu wstrzymania (przez osiągnięcie wyrażenia await) lub zakończenia, w którym to momencie kontrola jest zwracana do wywołującego wraz z zadaniem zwracanym.
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 zostaje przerwana z powodu nieuchwyconego
OperationCanceledException
, wyjątek jest rejestrowany w zadaniu wynikowym, które jest przenoszone do stanu anulowanego. - 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 asynchronicznej funkcji zwracającej void
Jeśli zwracany typ funkcji asynchronicznej to void
, ocena różni się od powyższego w następujący sposób: ponieważ nie jest zwracane żadne zadanie, funkcja komunikuje zakończenie 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 uruchomienie asynchronicznej funkcji zwracającej void
rozpoczyna się, kończy się pomyślnie lub powoduje zgłoszenie nieuchwyconego wyjątku.
Dzięki temu kontekst może śledzić, ile funkcji asynchronicznych zwracających void
jest uruchomionych pod jego kontrolą i zdecydować, jak propagować wyjątki pochodzące z tych funkcji.
ECMA C# draft specification