Udostępnij za pośrednictwem


15 klas

15.1 Ogólne

Klasa to struktura danych, która może zawierać składowe danych (stałe i pola), składowe funkcji (metody, właściwości, zdarzenia, indeksatory, operatory, konstruktory wystąpień, finalizatory i konstruktory statyczne) oraz typy zagnieżdżone. Typy klas obsługują dziedziczenie, mechanizm, w którym klasa pochodna może rozszerzać i specjalizować klasę bazową.

15.2 Deklaracje klas

15.2.1 Ogólne

Class_declaration jest type_declaration (§14.7), który deklaruje nową klasę.

class_declaration
    : attributes? class_modifier* 'partial'? 'class' identifier
        type_parameter_list? class_base? type_parameter_constraints_clause*
        class_body ';'?
    ;

Class_declaration składa się z opcjonalnego zestawu atrybutów(§22), a następnie opcjonalnego zestawu class_modifiers (§15.2.2partial§15.2.7), a następnie słowa kluczowego class i identyfikatora, który nazywa klasę, a następnie opcjonalną type_parameter_list (§15.2.3), a następnie opcjonalną specyfikację class_base (§15.2.4)), po którym następuje opcjonalny zestaw type_parameter_constraints_clause s (§15.2.5), a następnie class_body (§15.2.6), po którym następujeśrednik.

Deklaracja klasy nie dostarcza type_parameter_constraints_clauses, chyba że dostarcza również type_parameter_list.

Deklaracja klasy dostarczająca type_parameter_list jest deklaracją klasy ogólnej. Ponadto każda klasa zagnieżdżona wewnątrz deklaracji klasy ogólnej lub deklaracji struktury ogólnej jest samą deklaracją klasy ogólnej, ponieważ argumenty typu dla typu zawierającego należy podać w celu utworzenia skonstruowanego typu (§8.4).

Modyfikatory klas 15.2.2

15.2.2.1 Ogólne

Class_declaration może opcjonalnie zawierać sekwencję modyfikatorów klas:

class_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'abstract'
    | 'sealed'
    | 'static'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).

Jest to błąd czasu kompilacji dla tego samego modyfikatora, który pojawia się wiele razy w deklaracji klasy.

Modyfikator new jest dozwolony w klasach zagnieżdżonych. Określa, że klasa ukrywa dziedziczony składowy o tej samej nazwie, co opisano w §15.3.5. Jest to błąd czasu kompilacji, new który modyfikator ma być wyświetlany w deklaracji klasy, która nie jest deklaracją klasy zagnieżdżonej.

Modyfikatory public, protectedinternal, i private kontrolują dostępność klasy . W zależności od kontekstu, w którym występuje deklaracja klasy, niektóre z tych modyfikatorów mogą nie być dozwolone (§7.5.2).

W przypadku częściowej deklaracji typu (§15.2.7) zawiera specyfikację ułatwień dostępu (za pośrednictwem publicmodyfikatorów , protected, internali private ), specyfikacja ta zgadza się ze wszystkimi innymi częściami, które zawierają specyfikację ułatwień dostępu. Jeśli żadna część częściowego typu nie zawiera specyfikacji ułatwień dostępu, typ otrzymuje odpowiednie domyślne ułatwienia dostępu (§7.5.2).

Modyfikatory abstract, sealedi static zostały omówione w następujących podklasach.

15.2.2.2 Klasy abstrakcyjne

Modyfikator abstract służy do wskazywania, że klasa jest niekompletna i że ma być używana tylko jako klasa bazowa. Klasa abstrakcyjna różni się od klasy nie abstrakcyjnej w następujący sposób:

  • Nie można utworzyć wystąpienia klasy abstrakcyjnej bezpośrednio i jest to błąd czasu kompilacji, aby użyć new operatora w klasie abstrakcyjnej. Chociaż możliwe jest posiadanie zmiennych i wartości, których typy czasu kompilacji są abstrakcyjne, takie zmienne i wartości muszą być null albo zawierać odwołania do wystąpień klas nie abstrakcyjnych pochodzących z typów abstrakcyjnych.
  • Klasa abstrakcyjna jest dozwolona (ale nie jest wymagana) do zawierania abstrakcyjnych elementów członkowskich.
  • Nie można zapieczętować klasy abstrakcyjnej.

Gdy klasa nie abstrakcyjna pochodzi z klasy abstrakcyjnej, klasa nie abstrakcyjna obejmuje rzeczywiste implementacje wszystkich odziedziczonych składowych abstrakcyjnych, w związku z tym przesłaniając te abstrakcyjne składowe.

Przykład: w poniższym kodzie

abstract class A
{
    public abstract void F();
}

abstract class B : A
{
    public void G() {}
}

class C : B
{
    public override void F()
    {
        // Actual implementation of F
    }
}

klasa A abstrakcyjna wprowadza metodę Fabstrakcyjną . Klasa B wprowadza dodatkową metodę G, ale ponieważ nie zapewnia implementacji F, B również jest zadeklarowana abstrakcyjnie. Przesłonięcia C klas F i zapewniają rzeczywistą implementację. Ponieważ w elemecie nie ma abstrakcyjnych elementów członkowskich C, C jest dozwolony (ale nie jest to wymagane), aby nie były abstrakcyjne.

przykład końcowy

Jeśli co najmniej jedna część części części częściowej deklaracji typu (§15.2.7) klasy zawiera abstract modyfikator, klasa jest abstrakcyjna. W przeciwnym razie klasa nie jest abstrakcyjna.

15.2.2.3 Klasy zapieczętowane

Modyfikator sealed jest używany do zapobiegania wyprowadzaniu z klasy. Błąd czasu kompilacji występuje, jeśli zapieczętowana klasa jest określona jako klasa bazowa innej klasy.

Zapieczętowana klasa nie może być również klasą abstrakcyjną.

Uwaga: sealed Modyfikator jest używany głównie do zapobiegania niezamierzonym wyprowadzaniu, ale umożliwia również pewne optymalizacje czasu wykonywania. W szczególności, ponieważ zapieczętowana klasa nigdy nie ma żadnych klas pochodnych, można przekształcić wywołania składowych funkcji wirtualnych na zapieczętowanych wystąpieniach klas w wywołania niewirtualne. notatka końcowa

Jeśli co najmniej jedna część części części częściowej deklaracji typu (§15.2.7) klasy zawiera sealed modyfikator, klasa jest zapieczętowana. W przeciwnym razie klasa jest niezaużytowana.

15.2.2.4 Klasy statyczne

15.2.2.4.1 Ogólne

Modyfikator static służy do oznaczania klasy zadeklarowanej jako klasy statycznej. Klasę statyczną nie należy utworzyć wystąpienia, nie należy używać jako typu i zawierać tylko statyczne elementy członkowskie. Tylko klasa statyczna może zawierać deklaracje metod rozszerzenia (§15.6.10).

Deklaracja klasy statycznej podlega następującym ograniczeniom:

  • Klasa statyczna nie zawiera sealed klasy ani abstract modyfikatora. (Ponieważ jednak nie można utworzyć wystąpienia ani utworzyć wystąpienia klasy statycznej, zachowuje się tak, jakby była zapieczętowana i abstrakcyjna).
  • Klasa statyczna nie zawiera specyfikacji class_base (§15.2.4) i nie może jawnie określić klasy bazowej ani listy zaimplementowanych interfejsów. Klasa statyczna niejawnie dziedziczy z typu object.
  • Klasa statyczna zawiera tylko statyczne składowe (§15.3.8).

    Uwaga: wszystkie stałe i typy zagnieżdżone są klasyfikowane jako statyczne elementy członkowskie. notatka końcowa

  • Klasa statyczna nie ma składowych z elementami protected, private protectedani protected internal zadeklarowanymi ułatwieniami dostępu.

Jest to błąd czasu kompilacji, który narusza dowolne z tych ograniczeń.

Klasa statyczna nie ma konstruktorów wystąpień. Nie można zadeklarować konstruktora wystąpienia w klasie statycznej, a dla klasy statycznej nie podano domyślnego konstruktora wystąpienia (§15.11.5).

Składowe klasy statycznej nie są automatycznie statyczne, a deklaracje składowe jawnie zawierają static modyfikator (z wyjątkiem stałych i typów zagnieżdżonych). Gdy klasa jest zagnieżdżona w statycznej klasie zewnętrznej, zagnieżdżona klasa nie jest klasą statyczną, chyba że jawnie zawiera static modyfikator.

Jeśli co najmniej jedna część części części częściowej deklaracji typu (§15.2.7) klasy zawiera static modyfikator, klasa jest statyczna. W przeciwnym razie klasa nie jest statyczna.

15.2.2.4.2 Odwołujące się do typów klas statycznych

Namespace_or_type_name (§7.8) może odwoływać się do klasy statycznej, jeśli

  • Namespace_or_type_name jest T w namespace_or_type_name formularza T.Ilub
  • Nazwa namespace_or_type jest T w typeof_expression (§12.8.18) formularza typeof(T).

Primary_expression (§12.8) może odwoływać się do klasy statycznej, jeśli

  • Primary_expression jest E w member_access (§12.8.7) formularza E.I.

W innym kontekście jest to błąd czasu kompilacji, aby odwołać się do klasy statycznej.

Uwaga: na przykład jest to błąd klasy statycznej, która ma być używana jako klasa bazowa, typ składowy (§15.3.7) elementu członkowskiego, argument typu ogólnego lub ograniczenie parametru typu. Podobnie nie można używać klasy statycznej w typie tablicy, nowym wyrażeniu, wyrażeniu rzutowym, wyrażeniu is, wyrażeniu jako wyrażeniu sizeof lub wyrażeniu wartości domyślnej. notatka końcowa

15.2.3 Parametry typu

Parametr typu to prosty identyfikator, który określa symbol zastępczy argumentu typu dostarczonego w celu utworzenia skonstruowanego typu. Przez constrast argument typu (§8.4.2) jest typem, który jest zastępowany dla parametru typu podczas tworzenia skonstruowanego typu.

type_parameter_list
    : '<' type_parameters '>'
    ;

type_parameters
    : attributes? type_parameter
    | type_parameters ',' attributes? type_parameter
    ;

type_parameter jest zdefiniowany w §8.5.

Każdy parametr typu w deklaracji klasy definiuje nazwę w przestrzeni deklaracji (§7.3) tej klasy. W związku z tym nie może mieć takiej samej nazwy jak inny parametr typu tej klasy lub składowej zadeklarowanej w tej klasie. Parametr typu nie może mieć takiej samej nazwy jak sam typ.

Dwie częściowe deklaracje typów ogólnych (w tym samym programie) przyczyniają się do tego samego niezwiązanego typu ogólnego, jeśli mają taką samą w pełni kwalifikowaną nazwę (która zawiera generic_dimension_specifier (§12.8.18) dla liczby parametrów typu) (§7.8.3). Dwie takie częściowe deklaracje typów określają taką samą nazwę dla każdego parametru typu, w kolejności.

15.2.4 Specyfikacja podstawowa klasy

15.2.4.1 Ogólne

Deklaracja klasy może zawierać specyfikację class_base, która definiuje bezpośrednią klasę bazową klasy i interfejsy (§18) bezpośrednio zaimplementowane przez klasę.

class_base
    : ':' class_type
    | ':' interface_type_list
    | ':' class_type ',' interface_type_list
    ;

interface_type_list
    : interface_type (',' interface_type)*
    ;

15.2.4.2 Klasy bazowe

Gdy class_type jest uwzględniona w class_base, określa bezpośrednią klasę bazową zadeklarowanej klasy. Jeśli deklaracja klasy innej niż częściowa nie ma class_base lub jeśli class_base wyświetla tylko typy interfejsów, przyjmuje się, że bezpośrednia klasa bazowa ma wartość object. Gdy deklaracja klasy częściowej zawiera specyfikację klasy bazowej, ta specyfikacja klasy bazowej odwołuje się do tego samego typu, co wszystkie pozostałe części tego typu częściowego, które zawierają specyfikację klasy bazowej. Jeśli żadna część klasy częściowej nie zawiera specyfikacji klasy bazowej, klasa bazowa to object. Klasa dziedziczy elementy członkowskie z bezpośredniej klasy bazowej, zgodnie z opisem w §15.3.4.

Przykład: w poniższym kodzie

class A {}
class B : A {}

Mówi się, że klasa A jest bezpośrednią klasą bazową Bklasy , i B mówi się, że pochodzi z Aklasy . Ponieważ A nie określa jawnie bezpośredniej klasy bazowej, jej bezpośrednia klasa bazowa jest niejawnie object.

przykład końcowy

Dla skonstruowanego typu klasy, w tym typu zagnieżdżonego zadeklarowanego w deklaracji typu ogólnego (§15.3.9.7), jeśli klasa bazowa jest określona w deklaracji klasy ogólnej, klasa bazowa skonstruowanego typu jest uzyskiwana przez podstawianie, dla każdej type_parameter w deklaracji klasy bazowej, odpowiadającej type_argument typu skonstruowanego.

Przykład: biorąc pod uwagę deklaracje klasy ogólnej

class B<U,V> {...}
class G<T> : B<string,T[]> {...}

klasa bazowa skonstruowanego typu G<int> to B<string,int[]>.

przykład końcowy

Klasa bazowa określona w deklaracji klasy może być typem klasy skonstruowanej (§8.4). Klasa bazowa nie może być parametrem typu samodzielnie (§8.5), chociaż może obejmować parametry typu, które znajdują się w zakresie.

Przykład:

class Base<T> {}

// Valid, non-constructed class with constructed base class
class Extend1 : Base<int> {}

// Error, type parameter used as base class
class Extend2<V> : V {}

// Valid, type parameter used as type argument for base class
class Extend3<V> : Base<V> {}

przykład końcowy

Bezpośrednia klasa bazowa typu klasy jest co najmniej tak dostępna, jak sam typ klasy (§7.5.5). Na przykład jest to błąd czasu kompilacji dla klasy publicznej pochodzącej z klasy prywatnej lub wewnętrznej.

Bezpośrednia klasa bazowa typu klasy nie może być żadnym z następujących typów: System.Array, System.Delegate, System.EnumSystem.ValueType lub dynamic typu. Ponadto deklaracja klasy ogólnej nie może być używana System.Attribute jako bezpośrednia lub pośrednia klasa bazowa (§22.2.1).

Podczas określania znaczenia bezpośredniej specyfikacji A klasy bazowej klasy Bprzyjmuje się, że bezpośrednia klasa B bazowa klasy jest tymczasowo zakładana jako object, co gwarantuje, że znaczenie specyfikacji klasy bazowej nie może rekursywnie zależeć od siebie.

Przykład: następujące

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

class Z : X<Z.Y> {}

występuje błąd, ponieważ w specyfikacji X<Z.Y> klasy bazowej bezpośrednia klasa Z bazowa klasy jest uważana za , a zatem (zgodnie z regułami object) nie jest uważana Zza składową Y.

przykład końcowy

Klasy bazowe klasy to bezpośrednia klasa bazowa i jej klasy bazowe. Innymi słowy, zestaw klas bazowych to przechodnie zamknięcie relacji bezpośredniej klasy bazowej.

Przykład: w następujących kwestiach:

class A {...}
class B<T> : A {...}
class C<T> : B<IComparable<T>> {...}
class D<T> : C<T[]> {...}

klasy D<int> bazowe to C<int[]>, , B<IComparable<int[]>>Ai object.

przykład końcowy

Z wyjątkiem klasy każda klasa objectma dokładnie jedną bezpośrednią klasę bazową. Klasa object nie ma bezpośredniej klasy bazowej i jest ostateczną klasą bazową wszystkich innych klas.

Jest to błąd czasu kompilacji dla klasy, która zależy od siebie. W celu tej reguły klasa zależy bezpośrednio od jej bezpośredniej klasy bazowej (jeśli istnieje) i bezpośrednio zależy od najbliższej otaczającej klasy, w której jest zagnieżdżona (jeśli istnieje). Biorąc pod uwagę tę definicję, pełny zestaw klas, od których zależy klasa, to przejściowe zamknięcie obiektu bezpośrednio zależy od relacji.

Przykład: przykład

class A : A {}

jest błędne, ponieważ klasa zależy od siebie. Podobnie przykład

class A : B {}
class B : C {}
class C : A {}

występuje błąd, ponieważ klasy cyklicznie zależą od siebie. Na koniec przykład

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

powoduje wystąpienie błędu czasu kompilacji, ponieważ element A zależy B.C od (bezpośredniej klasy bazowej), która zależy od B (bezpośrednio otaczającej klasy), która zależy cyklicznie od Aklasy .

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 od A (ponieważ A jest zarówno jej bezpośrednią klasą bazową, jak i bezpośrednio otaczającą klasą), ale A nie zależy od B (ponieważ B nie jest ani klasą bazową, ani otaczającą klasą A). W związku z tym przykład jest prawidłowy.

przykład końcowy

Nie można pochodzić z zapieczętowanej klasy.

Przykład: w poniższym kodzie

sealed class A {}
class B : A {} // Error, cannot derive from a sealed class

Klasa B jest w błędzie, ponieważ próbuje pochodzić z zapieczętowanej klasy A.

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 to IA, IBi IC.

przykład końcowy

Zazwyczaj każda część zawiera implementację interfejsów zadeklarowanych w tej części; nie jest to jednak wymagane. Część może zapewnić implementację interfejsu zadeklarowanego w innej części.

Przykład:

partial class X
{
    int IComparable.CompareTo(object o) {...}
}

partial class X : IComparable
{
    ...
}

przykład końcowy

Interfejsy podstawowe określone w deklaracji klasy mogą być konstruowane typy interfejsów (§8.4, §18.2). Interfejs podstawowy nie może być parametrem typu samodzielnie, ale może obejmować parametry typu, które znajdują się w zakresie.

Przykład: Poniższy kod ilustruje, jak klasa może implementować i rozszerzać skonstruowane typy:

class C<U, V> {}
interface I1<V> {}
class D : C<string, int>, I1<string> {}
class E<T> : C<int, T>, I1<T> {}

przykład końcowy

Implementacje interfejsu zostały omówione bardziej szczegółowo w §18.6.

Ograniczenia parametrów typu 15.2.5

Deklaracje typów ogólnych i metod mogą opcjonalnie określać ograniczenia parametrów typu, uwzględniając type_parameter_constraints_clauses.

type_parameter_constraints_clauses
    : type_parameter_constraints_clause
    | type_parameter_constraints_clauses type_parameter_constraints_clause
    ;

type_parameter_constraints_clause
    : 'where' type_parameter ':' type_parameter_constraints
    ;

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type nullable_type_annotation?
    | 'class' nullable_type_annotation?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type nullable_type_annotation?
    | type_parameter nullable_type_annotation?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

constructor_constraint
    : 'new' '(' ')'
    ;

Każda type_parameter_constraints_clause składa się z tokenu where, po którym następuje nazwa parametru typu, a następnie dwukropek i lista ograniczeń dla tego parametru typu. Dla każdego parametru typu może znajdować się co najwyżej jedna where klauzula, a where klauzule można wymienić w dowolnej kolejności. get Podobnie jak tokeny i set w metodzie dostępu właściwości, where token nie jest słowem kluczowym.

Lista ograniczeń podanych w where klauzuli może zawierać dowolny z następujących składników w następującej kolejności: jedno ograniczenie podstawowe, jedno lub więcej ograniczeń pomocniczych oraz ograniczenie konstruktora. new()

Ograniczenie podstawowe może być typem klasy, unmanaged Typ klasy i ograniczenie typu odwołania mogą obejmować nullable_type_annotation.

Ograniczenie pomocnicze może być interface_type lub type_parameter, po którym opcjonalnie następuje nullable_type_annotation. Obecność nullable_type_annotatione* wskazuje, że argument typu może być typem odwołania dopuszczanym do wartości null, który odpowiada typowi odwołania bez wartości null, który spełnia ograniczenie.

Ograniczenie typu odwołania określa, że argument typu używany dla parametru typu jest typem referencyjnym. Wszystkie typy klas, typy interfejsów, typy delegatów, typy tablic i parametry typu znane jako typ odwołania (zgodnie z definicją poniżej) spełniają to ograniczenie.

Typ klasy, ograniczenie typu odwołania i ograniczenia pomocnicze mogą obejmować adnotację typu dopuszczalnego do wartości null. Obecność lub brak tej adnotacji dla parametru typu wskazuje oczekiwania dotyczące wartości null dla argumentu typu:

  • Jeśli ograniczenie nie zawiera adnotacji typu dopuszczalnego do wartości null, argument typu powinien być typem odwołania, który nie może mieć wartości null. Kompilator może wydać ostrzeżenie, jeśli argument typu jest typem odwołania dopuszczanym do wartości null.
  • Jeśli ograniczenie zawiera adnotację typu dopuszczalnego do wartości null, ograniczenie jest spełnione zarówno przez typ odwołania, który nie może mieć wartości null, jak i typ odwołania dopuszczalny do wartości null.

Wartość null argumentu typu nie musi być zgodna z wartością null parametru typu. Kompilator może wydać ostrzeżenie, jeśli wartość null parametru typu nie jest zgodna z wartością null argumentu typu.

Uwaga: Aby określić, że argument typu jest typem odwołania dopuszczanym do wartości null, nie należy dodawać adnotacji typu dopuszczających wartość null jako ograniczenia (użyj T : class lub T : BaseClass), ale użyj T? całej deklaracji ogólnej, aby wskazać odpowiedni typ odwołania dopuszczający wartość null dla argumentu typu. notatka końcowa

Adnotacja typu dopuszczana do wartości null, ?, nie może być używana w nieprzyciągniętym argumencie typu.

W przypadku parametru T typu, gdy argument typu jest typem C?odwołania dopuszczanym do wartości null, wystąpienia T? klasy są interpretowane jako C?, a nie C??.

Przykład: W poniższych przykładach pokazano, jak wartość null argumentu typu ma wpływ na wartość null deklaracji parametru typu:

public class C
{
}

public static class  Extensions
{
    public static void M<T>(this T? arg) where T : notnull
    {

    }
}

public class Test
{
    public void M()
    {
        C? mightBeNull = new C();
        C notNull = new C();

        int number = 5;
        int? missing = null;

        mightBeNull.M(); // arg is C?
        notNull.M(); //  arg is C?
        number.M(); // arg is int?
        missing.M(); // arg is int?
    }
}

Gdy argument typu jest typem nienależącym do wartości null, adnotacja typu wskazuje, ? że parametr jest odpowiednim typem dopuszczalnym wartości null. Gdy argument typu jest już typem odwołania dopuszczanym do wartości null, parametr jest tym samym typem dopuszczalnym do wartości null.

przykład końcowy

Ograniczenie not null określa, że argument typu używany dla parametru typu powinien być typem wartości innej niż null lub typem odwołania bez wartości null. Argument typu, który nie jest typem wartości innej niż null lub typem referencyjnym bez wartości null, jest dozwolony, ale kompilator może wygenerować ostrzeżenie diagnostyczne.

Ograniczenie typu wartości określa, że argument typu używany dla parametru typu musi być typem wartości niepustej. Wszystkie typy struktur bez wartości null, typy wyliczenia i parametry typu, które mają ograniczenie typu wartości, spełniają to ograniczenie. Należy pamiętać, że chociaż klasyfikowany jako typ wartości, typ wartości dopuszczający wartość null (§8.3.12) nie spełnia ograniczenia typu wartości. Parametr typu o ograniczeniu typu wartości nie ma również constructor_constraint, chociaż może być używany jako argument typu dla innego parametru typu z constructor_constraint.

Uwaga: System.Nullable<T> typ określa ograniczenie typu wartości innej niż null dla elementu T. W związku z tym rekursywnie skonstruowane typy formularzy T?? i Nullable<Nullable<T>> są zabronione. notatka końcowa

Ponieważ unmanaged nie jest słowem kluczowym, w primary_constraint ograniczenie niezarządzane jest zawsze niejednoznaczne z class_type. Ze względów zgodności, jeśli wyszukiwanie nazw (§12.8.4) nazwy unmanaged zakończy się powodzeniem class_type, jest traktowane jako . W przeciwnym razie jest traktowana jako ograniczenie niezarządzane.

Ograniczenie typu niezarządzanego określa, że argument typu używany dla parametru typu jest typem niezarządzanym niezarządzanym typem (§8.8).

Typy wskaźników nigdy nie mogą być argumentami typu i nie spełniają żadnych ograniczeń typu, nawet niezarządzanych, mimo że są niezarządzane typy.

Jeśli ograniczenie jest typem klasy, typem interfejsu lub parametrem typu, typ ten określa minimalny "typ podstawowy", który każdy argument typu używany dla tego parametru typu obsługuje. Za każdym razem, gdy jest używany skonstruowany typ lub metoda ogólna, argument typu jest sprawdzany względem ograniczeń parametru typu w czasie kompilacji. Podany argument typu spełnia warunki opisane w §8.4.5.

Ograniczenie class_type spełnia następujące zasady:

  • Typ jest typem klasy.
  • Typ nie powinien mieć nazwy sealed.
  • Typ nie jest jednym z następujących typów: System.Array lub System.ValueType.
  • Typ nie powinien mieć nazwy object.
  • Co najwyżej jedno ograniczenie dla danego parametru typu może być typem klasy.

Typ określony jako ograniczenie interface_type spełnia następujące zasady:

  • Typ musi być typem interfejsu.
  • Typ nie jest określony więcej niż raz w danej where klauzuli.

W obu przypadkach ograniczenie może obejmować dowolny z parametrów typu skojarzonego typu lub deklaracji metody w ramach skonstruowanego typu i może obejmować zadeklarowany typ.

Każdy typ klasy lub interfejsu określony jako ograniczenie parametru typu musi być co najmniej dostępny (§7.5.5) jako typ ogólny lub zadeklarowana metoda.

Typ określony jako ograniczenie type_parameter spełnia następujące zasady:

  • Typ jest parametrem typu.
  • Typ nie jest określony więcej niż raz w danej where klauzuli.

Ponadto nie ma cykli na wykresie zależności parametrów typu, gdzie zależność jest relacją przechodnią zdefiniowaną przez:

  • Jeśli parametr T typu jest używany jako ograniczenie dla parametru S typu, toS zależy od.T
  • Jeśli parametr S typu zależy od parametru typu i T zależy od parametru TU typu, Sto zależy odU.

Biorąc pod uwagę tę relację, jest to błąd czasu kompilacji parametru typu, który zależy od siebie (bezpośrednio lub pośrednio).

Wszelkie ograniczenia są spójne między parametrami typu zależnego. Jeśli parametr S typu zależy od parametru T typu, wówczas:

  • T nie ma ograniczenia typu wartości. W przeciwnym razie jest skutecznie zapieczętowany, T więc S będzie zmuszony do bycia tego samego typu co T, eliminując konieczność korzystania z dwóch parametrów typu.
  • Jeśli S ma ograniczenie typu wartości, T nie ma ograniczenia class_type .
  • Jeśli ma ograniczenie class_type i S ma ABA
  • Jeśli S również zależy od parametru U typu i U ma B, wówczas istnieje konwersja tożsamości lub niejawna konwersja odwołania z do lub niejawna konwersja odwołania z A do B lub niejawna konwersja odwołania z B do A.

Jest to prawidłowe dla S ograniczenia typu wartości i T ograniczenia typu odwołania. Skutecznie ogranicza to T typy System.Object, System.ValueType, System.Enumi dowolny typ interfejsu.

Jeśli klauzula where parametru typu zawiera ograniczenie konstruktora (które ma postać new()), można użyć new operatora do utworzenia wystąpień typu (§12.8.17.2). Każdy argument typu używany dla parametru typu z ograniczeniem konstruktora musi być typem wartości, klasą nie abstrakcyjną o publicznym konstruktorze bez parametrów lub parametrem typu mającym ograniczenie typu wartości lub ograniczenie konstruktora.

Jest to błąd czasu kompilacji dla type_parameter_constraints o primary_constraint lub mieć również unmanaged

Przykład: Poniżej przedstawiono przykłady ograniczeń:

interface IPrintable
{
    void Print();
}

interface IComparable<T>
{
    int CompareTo(T value);
}

interface IKeyProvider<T>
{
    T GetKey();
}

class Printer<T> where T : IPrintable {...}
class SortedList<T> where T : IComparable<T> {...}

class Dictionary<K,V>
    where K : IComparable<K>
    where V : IPrintable, IKeyProvider<K>, new()
{
    ...
}

Poniższy przykład występuje w błędzie, ponieważ powoduje cykliczność na wykresie zależności parametrów typu:

class Circular<S,T>
    where S: T
    where T: S // Error, circularity in dependency graph
{
    ...
}

W poniższych przykładach przedstawiono dodatkowe nieprawidłowe sytuacje:

class Sealed<S,T>
    where S : T
    where T : struct // Error, `T` is sealed
{
    ...
}

class A {...}
class B {...}

class Incompat<S,T>
    where S : A, T
    where T : B // Error, incompatible class-type constraints
{
    ...
}

class StructWithClass<S,T,U>
    where S : struct, T
    where T : U
    where U : A // Error, A incompatible with struct
{
    ...
}

przykład końcowy

Dynamiczne wymazywanie typu C jest Cₓ skonstruowane w następujący sposób:

  • Jeśli C jest typem Outer.Inner zagnieżdżonym, Cₓ jest to typ Outerₓ.Innerₓzagnieżdżony .
  • Jeśli CCₓjest typem skonstruowanym G<A¹, ..., Aⁿ> z argumentami A¹, ..., Aⁿ typu, Cₓ jest skonstruowany typ G<A¹ₓ, ..., Aⁿₓ>.
  • Jeśli C jest typem E[] tablicy, Cₓ jest to typ Eₓ[]tablicy .
  • Jeśli C wartość jest dynamiczna, Cₓ wartość to object.
  • Cₓ W przeciwnym razie wartość to C.

Efektywna klasa bazowa parametru T typu jest definiowana w następujący sposób:

Pozwól R , aby zestaw typów był taki, że:

  • Dla każdego ograniczenia T tego jest parametr typu, R zawiera efektywną klasę bazową.
  • Dla każdego ograniczenia T , który jest typem struktury, R zawiera System.ValueType.
  • Dla każdego ograniczenia T , które jest typem wyliczenia, R zawiera System.Enum.
  • Dla każdego ograniczenia T jest to typ delegata, R zawiera jego dynamiczne wymazywanie.
  • Dla każdego ograniczenia T , które jest typem tablicy, R zawiera System.Array.
  • Dla każdego ograniczenia T jest to typ klasy, R zawiera jego dynamiczne wymazywanie.

Następnie

  • Jeśli T ma ograniczenie typu wartości, efektywną klasą bazową jest System.ValueType.
  • W przeciwnym razie, jeśli R wartość jest pusta, efektywna klasa bazowa to object.
  • W przeciwnym razie efektywna klasa bazowa T klasy jest najbardziej obejmującym typem (§10.5.3) zestawu R. Jeśli zestaw nie ma uwzględnionego typu, efektywna klasa bazowa klasy T to object. Reguły spójności zapewniają, że istnieje najbardziej obejmujący typ.

Jeśli parametr typu jest parametrem typu metody, którego ograniczenia są dziedziczone z metody podstawowej, efektywna klasa bazowa jest obliczana po podstawieniu typu.

Te reguły zapewniają, że efektywna klasa bazowa jest zawsze class_type.

Efektywny zestaw interfejsu parametru T typu jest definiowany w następujący sposób:

  • Jeśli T nie ma secondary_constraints, jego efektywny zestaw interfejsu jest pusty.
  • Jeśli T ma interface_type ograniczenia, ale nie type_parameter ograniczeń, jego efektywny zestaw interfejsów jest zestawem dynamicznych wymazywania ograniczeń interface_type.
  • Jeśli T nie ma żadnych interface_type ograniczeń, ale ma type_parameter ograniczenia, jego efektywny zestaw interfejsów jest połączeniem obowiązujących zestawów interfejsów ograniczeń type_parameter.
  • Jeśli T ma zarówno ograniczenia interface_type , jak i type_parameter ograniczenia, jego efektywny zestaw interfejsów jest połączeniem zestawu dynamicznych wymazywania ograniczeń interface_type i skutecznych zestawów interfejsów ograniczeń type_parameter .

Parametr typu jest znany jako typ odwołania, jeśli ma ograniczenie typu odwołania lub jego obowiązującą klasę bazową nie object jest lub System.ValueType. Parametr typu jest znany jako typ odwołania nienależący do wartości null, jeśli jest znany jako typ odwołania i ma ograniczenie typu referencyjnego bez wartości null.

Wartości typu parametru typu ograniczonego mogą służyć do uzyskiwania dostępu do elementów członkowskich wystąpienia implikowanych przez ograniczenia.

Przykład: w następujących kwestiach:

interface IPrintable
{
    void Print();
}

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

metody IPrintable metody można wywołać bezpośrednio, x ponieważ T jest ograniczona do zawsze implementowania IPrintablemetody .

przykład końcowy

Gdy częściowa deklaracja typu ogólnego zawiera ograniczenia, ograniczenia są zgodne ze wszystkimi innymi częściami, które obejmują ograniczenia. W szczególności każda część, która zawiera ograniczenia, musi mieć ograniczenia dla tego samego zestawu parametrów typu, a dla każdego parametru typu zestawy ograniczeń podstawowych, pomocniczych i konstruktorów są równoważne. Dwa zestawy ograniczeń są równoważne, jeśli zawierają te same elementy członkowskie. Jeśli żadna część częściowego typu ogólnego nie określa ograniczeń parametrów typu, parametry typu są traktowane jako nieprzeciągnięty.

Przykład:

partial class Map<K,V>
    where K : IComparable<K>
    where V : IKeyProvider<K>, new()
{
    ...
}

partial class Map<K,V>
    where V : IKeyProvider<K>, new()
    where K : IComparable<K>
{
    ...
}

partial class Map<K,V>
{
    ...
}

jest poprawna, ponieważ te części, które obejmują ograniczenia (pierwsze dwa) skutecznie określają ten sam zestaw ograniczeń podstawowych, pomocniczych i konstruktorów odpowiednio dla tego samego zestawu parametrów typu.

przykład końcowy

Treść klasy 15.2.6

Class_body klasy definiuje składowe tej klasy.

class_body
    : '{' class_member_declaration* '}'
    ;

15.2.7 Deklaracje częściowe

Modyfikator partial jest używany podczas definiowania klasy, struktury lub typu interfejsu w wielu częściach. Modyfikator partial jest kontekstowym słowem kluczowym (§6.4.4) i ma specjalne znaczenie bezpośrednio przed jednym ze słów kluczowych class, structlub interface.

Każda część częściowej deklaracji typu zawiera partial modyfikator i jest zadeklarowana w tej samej przestrzeni nazw lub zawiera typ co inne części. Modyfikator partial wskazuje, że dodatkowe części deklaracji typu mogą istnieć gdzie indziej, ale istnienie takich dodatkowych części nie jest wymaganiem; ważne jest, aby jedyna deklaracja typu zawierała partial modyfikator. Ważne jest, aby tylko jedna deklaracja typu częściowego zawierała klasę bazową lub zaimplementowane interfejsy. Jednak wszystkie deklaracje klasy bazowej lub zaimplementowane interfejsy muszą być zgodne, w tym dopuszczalność null dowolnych argumentów określonego typu.

Wszystkie części typu częściowego należy skompilować razem, tak aby części można było scalić w czasie kompilacji. Typy częściowe nie zezwalają na rozszerzenie już skompilowanych typów.

Typy zagnieżdżone można zadeklarować w wielu częściach przy użyciu partial modyfikatora. Zazwyczaj typ zawierający jest deklarowany przy użyciu partial , a każda część typu zagnieżdżonego jest deklarowana w innej części typu zawierającego.

Przykład: następująca klasa częściowa jest implementowana w dwóch częściach, które znajdują się w różnych jednostkach kompilacji. Pierwsza część to maszyna wygenerowana przez narzędzie do mapowania bazy danych, podczas gdy druga część jest ręcznie utworzona:

public partial class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }
}

// File: Customer2.cs
public partial class Customer
{
    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

Gdy obie powyższe części są kompilowane razem, wynikowy kod zachowuje się tak, jakby klasa została napisana jako pojedyncza jednostka w następujący sposób:

public class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }

    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

przykład końcowy

Obsługa atrybutów określonych w parametrach typu lub typu różnych części deklaracji częściowej jest omawiana w §22.3.

15.3 Składowe klasy

15.3.1 Ogólne

Składowe klasy składają się z składowych wprowadzonych przez jej class_member_declarations i składowych dziedziczone z bezpośredniej klasy bazowej.

class_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    ;

Składowe klasy są podzielone na następujące kategorie:

  • Stałe, które reprezentują stałe wartości skojarzone z klasą (§15.4).
  • Pola, które są zmiennymi klasy (§15.5).
  • Metody, które implementują obliczenia i akcje, które mogą być wykonywane przez klasę (§15.6).
  • Właściwości, które definiują nazwane cechy i akcje związane z odczytywaniem i zapisywaniem tych cech (§15.7).
  • Zdarzenia definiujące powiadomienia, które mogą być generowane przez klasę (§15.8).
  • Indeksatory, które umożliwiają indeksowanie wystąpień klasy w taki sam sposób (składniowo) jak tablice (§15.9).
  • Operatory definiujące operatory wyrażeń, które można zastosować do wystąpień klasy (§15.10).
  • Konstruktory wystąpień, które implementują akcje wymagane do zainicjowania wystąpień klasy (§15.11)
  • Finalizatory, które implementują akcje do wykonania, zanim wystąpienia klasy zostaną trwale odrzucone (§15.13).
  • Konstruktory statyczne, które implementują akcje wymagane do zainicjowania samej klasy (§15.12).
  • Typy reprezentujące typy lokalne dla klasy (§14.7).

Class_declaration tworzy nową przestrzeń deklaracji (§7.3), a type_parameters i class_member_declaration natychmiast zawarte przez class_declaration wprowadzają nowe elementy członkowskie do tej przestrzeni deklaracji. Następujące reguły dotyczą class_member_declarations:

  • Konstruktory wystąpień, finalizatory i konstruktory statyczne mają taką samą nazwę jak natychmiast otaczającej klasy. Wszystkie pozostałe składowe mają nazwy, które różnią się od nazwy natychmiast otaczającej klasy.

  • Nazwa parametru typu w type_parameter_list deklaracji klasy różni się od nazw wszystkich innych parametrów typu w tym samym type_parameter_list i różni się od nazwy klasy i nazw wszystkich składowych klasy.

  • Nazwa typu różni się od nazw wszystkich nietypowych składowych zadeklarowanych w tej samej klasie. Jeżeli co najmniej dwie deklaracje typów mają taką samą w pełni kwalifikowaną nazwę, deklaracje mają partial modyfikator (§15.2.7), a deklaracje te łączą się w celu zdefiniowania pojedynczego typu.

Uwaga: Ponieważ w pełni kwalifikowana nazwa deklaracji typu koduje liczbę parametrów typu, dwa odrębne typy mogą mieć taką samą nazwę, o ile mają różne parametry typu. notatka końcowa

  • Nazwa stałej, pola, właściwości lub zdarzenia różni się od nazw wszystkich pozostałych elementów członkowskich zadeklarowanych w tej samej klasie.

  • Nazwa metody różni się od nazw wszystkich innych metod, które nie są deklarowane w tej samej klasie. Ponadto podpis metody (§7.6) różni się od podpisów wszystkich innych metod zadeklarowanych w tej samej klasie, a dwie metody zadeklarowane w tej samej klasie nie mają podpisów, które różnią się wyłącznie od in, outi ref.

  • 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 i out.

  • Podpis indeksatora różni się od podpisów wszystkich innych indeksatorów zadeklarowanych w tej samej klasie.

  • Podpis operatora różni się od podpisów wszystkich innych operatorów zadeklarowanych w tej samej klasie.

Dziedziczone składowe klasy (§15.3.4) nie są częścią przestrzeni deklaracji klasy.

Uwaga: W związku z tym klasa pochodna może zadeklarować składową o tej samej nazwie lub podpisie co dziedziczony element członkowski (co powoduje ukrycie dziedziczonego elementu członkowskiego). notatka końcowa

Zestaw elementów członkowskich typu zadeklarowanego w wielu częściach (§15.2.7) jest związkiem członków zadeklarowanych w każdej części. Elementy wszystkich części deklaracji typu mają ten sam obszar deklaracji (§7.3), a zakres każdego elementu członkowskiego (§7.7) rozciąga się na elementy wszystkich części. Domena ułatwień dostępu każdego elementu członkowskiego zawsze zawiera wszystkie części otaczającego typu; prywatny członek zadeklarowany w jednej części jest swobodnie dostępny z innej części. Jest to błąd czasu kompilacji, aby zadeklarować ten sam element członkowski w więcej niż jednej części typu, chyba że ten element członkowski jest typem o modyfikatorze partial .

Przykład:

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

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

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

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

przykład końcowy

Kolejność inicjowania pola może być znacząca w kodzie języka C#, a niektóre gwarancje są udostępniane zgodnie z definicją w §15.5.6.1. W przeciwnym razie kolejność elementów członkowskich w obrębie typu jest rzadko znacząca, ale może być znacząca w przypadku współdziałania z innymi językami i środowiskami. W takich przypadkach kolejność elementów członkowskich w ramach typu zadeklarowanego w wielu częściach jest niezdefiniowana.

15.3.2 Typ wystąpienia

Każda deklaracja klasy ma skojarzony typ wystąpienia. W przypadku deklaracji klasy ogólnej typ wystąpienia jest tworzony przez utworzenie skonstruowanego typu (§8.4) z deklaracji typu, przy czym każdy z podanych argumentów typu jest odpowiednim parametrem typu. Ponieważ typ wystąpienia używa parametrów typu, można go używać tylko wtedy, gdy parametry typu znajdują się w zakresie; oznacza to, że wewnątrz deklaracji klasy. Typ wystąpienia jest typem this kodu napisanego wewnątrz deklaracji klasy. W przypadku klas innych niż ogólne typ wystąpienia jest po prostu zadeklarowaną klasą.

Przykład: Poniżej przedstawiono kilka deklaracji klas wraz z ich typami wystąpień:

class A<T>             // instance type: A<T>
{
    class B {}         // instance type: A<T>.B
    class C<U> {}      // instance type: A<T>.C<U>
}
class D {}             // instance type: D

przykład końcowy

15.3.3 Elementy członkowskie skonstruowanych typów

Nie dziedziczone elementy członkowskie typu skonstruowanego są uzyskiwane przez podstawianie dla każdego type_parameter w deklaracji składowej odpowiadającego type_argument typu skonstruowanego. Proces podstawienia opiera się na semantycznym znaczeniu deklaracji typów i nie jest po prostu podstawieniem tekstowym.

Przykład: biorąc pod uwagę deklarację klasy ogólnej

class Gen<T,U>
{
    public T[,] a;
    public void G(int i, T t, Gen<U,T> gt) {...}
    public U Prop { get {...} set {...} }
    public int H(double d) {...}
}

skonstruowany typ Gen<int[],IComparable<string>> ma następujące elementy członkowskie:

public int[,][] a;
public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...}
public IComparable<string> Prop { get {...} set {...} }
public int H(double d) {...}

Typ składowej a w deklaracji Gen klasy ogólnej to "dwuwymiarowa tablica T", więc typ składowej a w skonstruowanym typie powyżej to "dwuwymiarowa tablica jednowymiarowa tablicy int" lub int[,][].

przykład końcowy

W ramach składowych funkcji wystąpienia typ this wystąpienia to typ wystąpienia (§15.3.2) zawierającej deklarację.

Wszystkie elementy członkowskie klasy ogólnej mogą używać parametrów typu z dowolnej otaczającej klasy, bezpośrednio lub w ramach skonstruowanego typu. Gdy określony typ skonstruowany zamknięty (§8.4.3) jest używany w czasie wykonywania, każde użycie parametru typu jest zastępowane argumentem typu dostarczonym do skonstruowanego typu.

Przykład:

class C<V>
{
    public V f1;
    public C<V> f2;

    public C(V x)
    {
        this.f1 = x;
        this.f2 = this;
    }
}

class Application
{
    static void Main()
    {
        C<int> x1 = new C<int>(1);
        Console.WriteLine(x1.f1);              // Prints 1

        C<double> x2 = new C<double>(3.1415);
        Console.WriteLine(x2.f1);              // Prints 3.1415
    }
}

przykład końcowy

15.3.4 Dziedziczenie

Klasa dziedziczy składowe swojej bezpośredniej klasy bazowej. Dziedziczenie oznacza, że klasa niejawnie zawiera wszystkie elementy członkowskie jej bezpośredniej klasy bazowej, z wyjątkiem konstruktorów wystąpień, finalizatorów i konstruktorów statycznych klasy bazowej. Niektóre ważne aspekty dziedziczenia to:

  • Dziedziczenie jest przechodnie. Jeśli C pochodzi z Belementu i B pochodzi z Aelementu , C dziedziczy elementy członkowskie zadeklarowane w B , a także składowe zadeklarowane w A.

  • Klasa pochodna rozszerza swoją bezpośrednią klasę bazową. Klasa pochodna może dodawać nowe składowe do tych, które dziedziczy, ale nie może usunąć definicji dziedziczonego elementu członkowskiego.

  • Konstruktory wystąpień, finalizatory i konstruktory statyczne nie są dziedziczone, ale wszystkie inne elementy członkowskie są, niezależnie od zadeklarowanego ułatwień dostępu (§7.5). Jednak w zależności od zadeklarowanych ułatwień dostępu dziedziczone składowe mogą nie być dostępne w klasie pochodnej.

  • Klasa pochodna może ukryć (§7.7.2.3) dziedziczone składowe, deklarując nowe elementy członkowskie o tej samej nazwie lub podpisie. Jednak ukrycie dziedziczonego elementu członkowskiego nie powoduje usunięcia tego elementu członkowskiego — powoduje jedynie, że ten element członkowski jest niedostępny bezpośrednio za pośrednictwem klasy pochodnej.

  • Wystąpienie klasy zawiera zestaw wszystkich pól wystąpień zadeklarowanych w klasie i jej klasach bazowych, a niejawna konwersja (§10.2.8) istnieje z typu klasy pochodnej do dowolnego z jej typów klas bazowych. W związku z tym odwołanie do wystąpienia jakiejś klasy pochodnej może być traktowane jako odwołanie do wystąpienia dowolnej z jego klas bazowych.

  • Klasa może deklarować metody wirtualne, właściwości, indeksatory i zdarzenia, a klasy pochodne mogą zastąpić implementację tych składowych funkcji. Dzięki temu klasy mogą wykazywać zachowanie polimorficzne, w którym akcje wykonywane przez wywołanie składowe funkcji różnią się w zależności od typu czasu wykonywania wystąpienia, za pomocą którego wywoływany jest ten element członkowski funkcji.

Dziedziczone składowe typu klasy skonstruowanej są składowymi typu klasy bazowej natychmiastowej (§15.2.4.2), która jest znaleziono przez podstawianie argumentów typu skonstruowanego typu dla każdego wystąpienia odpowiednich parametrów typu w base_class_specification. Z kolei te elementy członkowskie są przekształcane przez podstawianie dla każdego type_parameter w deklaracji składowej odpowiadającego type_argumentbase_class_specification.

Przykład:

class B<U>
{
    public U F(long index) {...}
}

class D<T> : B<T[]>
{
    public T G(string s) {...}
}

W powyższym kodzie skonstruowany typ D<int> ma nie dziedziczony element członkowski publiczny intG(string s) uzyskany przez podstawianie argumentu int typu dla parametru Ttypu . D<int> ma również dziedziczony element członkowski z deklaracji Bklasy . Ten dziedziczony element członkowski jest określany najpierw przez określenie typu B<int[]> klasy bazowej D<int> przez zastąpienie intT elementu w specyfikacji B<T[]>klasy bazowej . Następnie, jako argument typu na B, int[] jest zastępowany U w public U F(long index)parametrze , dając dziedziczony element członkowski public int[] F(long index).

przykład końcowy

15.3.5 Nowy modyfikator

Class_member_declaration może zadeklarować element członkowski o tej samej nazwie lub podpisie co dziedziczony element członkowski. W takim przypadku mówi się, że składowa klasy pochodnej ukrywa składową klasy bazowej. Zobacz §7.7.2.3 , aby uzyskać dokładną specyfikację, kiedy element członkowski ukrywa dziedziczony element członkowski.

Dziedziczony element członkowski M jest uważany za M i nie ma innego dziedziczonego elementu członkowskiego N, który już ukrywa Melement . Niejawne ukrywanie dziedziczonej składowej nie jest uznawane za błąd, ale kompilator wydaje ostrzeżenie, chyba że deklaracja składowej klasy pochodnej zawiera modyfikator new, który jawnie wskazuje, że składowa pochodna jest przeznaczona do ukrycia składowej bazowej. Jeśli co najmniej jedna część deklaracji częściowej (§15.2.7) typu zagnieżdżonego zawiera new modyfikator, żadne ostrzeżenie nie zostanie wyświetlone, jeśli typ zagnieżdżony ukrywa dostępny dziedziczony element członkowski.

new Jeśli modyfikator jest uwzględniony w deklaracji, która nie ukrywa dostępnego dziedziczonego elementu członkowskiego, zostanie wyświetlone ostrzeżenie dotyczące tego efektu.

15.3.6 Modyfikatory dostępu

Class_member_declaration private protected internal Z wyjątkiem kombinacji i private protected jest to błąd czasu kompilacji określający więcej niż jeden modyfikator dostępu. Jeśli class_member_declaration nie zawiera żadnych modyfikatorów dostępu, przyjmuje się założenieprivate.

15.3.7 Typy składowe

Typy używane w deklaracji elementu członkowskiego są nazywane typami składowymi tego elementu członkowskiego. Możliwe typy składowe to typ stałej, pola, właściwości, zdarzenia lub indeksatora, zwracany typ metody lub operatora oraz typy parametrów metody, indeksatora, operatora lub konstruktora wystąpienia. Typy składowe członka są co najmniej tak dostępne, jak sam członek (§7.5.5).

15.3.8 Statyczne i składowe wystąpień

Składowe klasy to statyczne elementy członkowskie lub składowe wystąpienia.

Uwaga: ogólnie rzecz biorąc, warto traktować statyczne elementy członkowskie jako należące do klas i składowych wystąpień jako należące do obiektów (wystąpień klas). notatka końcowa

Gdy pole, metoda, właściwość, zdarzenie, operator lub deklaracja konstruktora zawiera static modyfikator, deklaruje statyczny element członkowski. Ponadto stała lub deklaracja typu niejawnie deklaruje statyczny element członkowski. Statyczne elementy członkowskie mają następujące cechy:

  • Gdy statyczny element członkowski M jest przywołyny w member_access (§12.8.7) formularza E.M, E określa typ, który ma element członkowski M. Jest to błąd czasu kompilacji, aby E oznaczyć wystąpienie.
  • Pole statyczne w klasie innej niż ogólna identyfikuje dokładnie jedną lokalizację przechowywania. Niezależnie od tego, ile wystąpień klasy niegenerycznej jest tworzonych, istnieje tylko jedna kopia pola statycznego. Każdy odrębny typ skonstruowany zamknięty (§8.4.3) ma własny zestaw pól statycznych, niezależnie od liczby wystąpień typu zamkniętego.
  • Element członkowski funkcji statycznej (metoda, właściwość, zdarzenie, operator lub konstruktor) nie działa na określonym wystąpieniu i jest to błąd czasu kompilacji, aby odwołać się do tego w takim elemencie członkowskim funkcji.

Gdy pole, metoda, właściwość, zdarzenie, indeksator, konstruktor lub deklaracja finalizatora nie zawiera modyfikatora statycznego, deklaruje element członkowski wystąpienia. (Element członkowski wystąpienia jest czasami nazywany niestacjonanym elementem członkowskim). Elementy członkowskie wystąpienia mają następujące cechy:

  • Jeżeli element członkowski M wystąpienia jest przywołyny w member_access (§12.8.7) formularza E.M, E określa wystąpienie typu, który ma element członkowski M. Jest to błąd czasu powiązania dla E, aby oznaczyć typ.
  • Każde wystąpienie klasy zawiera oddzielny zestaw wszystkich pól wystąpień klasy.
  • Element członkowski funkcji wystąpienia (metoda, właściwość, indeksator, konstruktor wystąpienia lub finalizator) działa na danym wystąpieniu klasy, a do tego wystąpienia można uzyskać dostęp jako this (§12.8.14).

Przykład: Poniższy przykład ilustruje reguły uzyskiwania dostępu do składowych statycznych i wystąpień:

class Test
{
    int x;
    static int y;
    void F()
    {
        x = 1;               // Ok, same as this.x = 1
        y = 1;               // Ok, same as Test.y = 1
    }

    static void G()
    {
        x = 1;               // Error, cannot access this.x
        y = 1;               // Ok, same as Test.y = 1
    }

    static void Main()
    {
        Test t = new Test();
        t.x = 1;       // Ok
        t.y = 1;       // Error, cannot access static member through instance
        Test.x = 1;    // Error, cannot access instance member through type
        Test.y = 1;    // Ok
    }
}

Metoda F pokazuje, że w elemencie członkowskim funkcji wystąpienia można użyć simple_name (§12.8.4) w celu uzyskania dostępu zarówno do składowych wystąpień, jak i statycznych elementów członkowskich. Metoda G pokazuje, że w składowej funkcji statycznej jest to błąd czasu kompilacji, aby uzyskać dostęp do elementu członkowskiego wystąpienia za pośrednictwem simple_name. Metoda Main pokazuje, że w member_access (§12.8.7) członkowie wystąpień mają dostęp za pośrednictwem wystąpień, a statyczne elementy członkowskie są dostępne za pośrednictwem typów.

przykład końcowy

15.3.9 Typy zagnieżdżone

15.3.9.1 Ogólne

Typ zadeklarowany w klasie lub strukturę jest nazywany typem zagnieżdżonym. Typ zadeklarowany w jednostce kompilacji lub przestrzeni nazw jest nazywany typem niezagnieżdżonym.

Przykład: W poniższym przykładzie:

class A
{
    class B
    {
        static void F()
        {
            Console.WriteLine("A.B.F");
        }
    }
}

klasa B jest typem zagnieżdżonym, ponieważ jest zadeklarowana w klasie A, a klasa A jest typem niezagnieżdżonym, ponieważ jest zadeklarowana w jednostce kompilacji.

przykład końcowy

15.3.9.2 W pełni kwalifikowana nazwa

W pełni kwalifikowana nazwa (§7.8.3) dla deklaracji S.N typu zagnieżdżonego, gdzie S jest w pełni kwalifikowaną nazwą deklaracji typu, która N jest zadeklarowana i N jest niekwalifikowaną nazwą (§7.8.2) zagnieżdżonej deklaracji typu (w tym wszelkie generic_dimension_specifier (§12.8.18)).

15.3.9.3 Zadeklarowana dostępność

Typy niezagnieżdżone mogą mieć public lub internal zadeklarować dostępność i domyślnie zadeklarować internal dostępność. Zagnieżdżone typy mogą również mieć te formy zadeklarowanego ułatwień dostępu oraz co najmniej jedną dodatkową formę zadeklarowanego ułatwień dostępu w zależności od tego, czy typ zawierający jest klasą, czy strukturą:

  • Zagnieżdżony typ zadeklarowany w klasie może mieć dowolny z dozwolonych rodzajów zadeklarowanych ułatwień dostępu i, podobnie jak inne składowe klasy, domyślnie zadeklarowane private ułatwienia dostępu.
  • Typ zagnieżdżony zadeklarowany w ramach struktury może mieć dowolną z trzech form zadeklarowanej dostępności (public, internallub private) i, podobnie jak inne elementy członkowskie struktury, domyślnie zadeklarowane private ułatwienia dostępu.

Przykład: przykład

public class List
{
    // Private data structure
    private class Node
    {
        public object Data;
        public Node? Next;

        public Node(object data, Node? next)
        {
            this.Data = data;
            this.Next = next;
        }
    }

    private Node? first = null;
    private Node? last = null;

    // Public interface
    public void AddToFront(object o) {...}
    public void AddToBack(object o) {...}
    public object RemoveFromFront() {...}
    public object RemoveFromBack() {...}
    public int Count { get {...} }
}

deklaruje prywatną klasę Nodezagnieżdżonych .

przykład końcowy

15.3.9.4 Ukrywanie

Typ zagnieżdżony może ukrywać element podstawowy (§7.7.2.2). new Modyfikator (§15.3.5) jest dozwolony w deklaracjach typu zagnieżdżonych, dzięki czemu można jawnie wyrazić ukrywanie.

Przykład: przykład

class Base
{
    public static void M()
    {
        Console.WriteLine("Base.M");
    }
}

class Derived: Base
{
    public new class M
    {
        public static void F()
        {
            Console.WriteLine("Derived.M.F");
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.M.F();
    }
}

wyświetla klasę M zagnieżdżoną, która ukrywa metodę M zdefiniowaną w pliku Base.

przykład końcowy

15.3.9.5 ten dostęp

Typ zagnieżdżony i jego typ zawierający nie mają specjalnej relacji w odniesieniu do this_access (§12.8.14). W szczególności w obrębie this typu zagnieżdżonego nie można odwoływać się do elementów członkowskich wystąpienia typu zawierającego. W przypadkach, gdy typ zagnieżdżony wymaga dostępu do elementów członkowskich wystąpienia jego typu zawierającego, można zapewnić dostęp, podając this dla wystąpienia typu zawierającego jako argument konstruktora typu zagnieżdżonego.

Przykład: poniższy przykład

class C
{
    int i = 123;
    public void F()
    {
        Nested n = new Nested(this);
        n.G();
    }

    public class Nested
    {
        C this_c;

        public Nested(C c)
        {
            this_c = c;
        }

        public void G()
        {
            Console.WriteLine(this_c.i);
        }
    }
}

class Test
{
    static void Main()
    {
        C c = new C();
        c.F();
    }
}

pokazuje tę technikę. Wystąpienie klasy C tworzy wystąpienie Nestedklasy i przekazuje własne polecenie do Nestedkonstruktora w celu zapewnienia późniejszego dostępu do Celementów członkowskich wystąpienia.

przykład końcowy

15.3.9.6 Dostęp do prywatnych i chronionych elementów członkowskich typu zawierającego

Typ zagnieżdżony ma dostęp do wszystkich elementów członkowskich, które są dostępne dla jego typu zawierającego, w tym elementów członkowskich typu private zawierającego i protected zadeklarowanych ułatwień dostępu.

Przykład: przykład

class C
{
    private static void F() => Console.WriteLine("C.F");

    public class Nested
    {
        public static void G() => F();
    }
}

class Test
{
    static void Main() => C.Nested.G();
}

wyświetla klasę C zawierającą klasę Nestedzagnieżdżonych . W programie Nestedmetoda G wywołuje metodę F statyczną zdefiniowaną w Cpliku i F ma prywatną zadeklarowaną dostępność.

przykład końcowy

Typ zagnieżdżony może również uzyskiwać dostęp do chronionych elementów członkowskich zdefiniowanych w podstawowym typie zawierającym typ.

Przykład: w poniższym kodzie

class Base
{
    protected void F() => Console.WriteLine("Base.F");
}

class Derived: Base
{
    public class Nested
    {
        public void G()
        {
            Derived d = new Derived();
            d.F(); // ok
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.Nested n = new Derived.Nested();
        n.G();
    }
}

klasa Derived.Nested zagnieżdżona uzyskuje dostęp do chronionej metody F zdefiniowanej w Derivedklasie Basebazowej , wywołując wystąpienie klasy Derived.

przykład końcowy

15.3.9.7 Typy zagnieżdżone w klasach ogólnych

Deklaracja klasy ogólnej może zawierać deklaracje typu zagnieżdżonego. Parametry typu otaczającej klasy mogą być używane w typach zagnieżdżonych. Deklaracja typu zagnieżdżonego może zawierać dodatkowe parametry typu, które mają zastosowanie tylko do typu zagnieżdżonego.

Każda deklaracja typu zawarta w deklaracji klasy ogólnej jest niejawnie deklaracją typu ogólnego. Podczas pisania odwołania do typu zagnieżdżonego w typie ogólnym nazwa ma nazwę zawierającą skonstruowany typ, w tym argumenty typu. Jednak z wewnątrz klasy zewnętrznej typ zagnieżdżony może być używany bez kwalifikacji; typ wystąpienia klasy zewnętrznej może być używany niejawnie podczas konstruowania typu zagnieżdżonego.

Przykład: Poniżej przedstawiono trzy różne poprawne sposoby odwoływania się do skonstruowanego typu utworzonego na podstawie Inner; pierwsze dwa są równoważne:

class Outer<T>
{
    class Inner<U>
    {
        public static void F(T t, U u) {...}
    }

    static void F(T t)
    {
        Outer<T>.Inner<string>.F(t, "abc");    // These two statements have
        Inner<string>.F(t, "abc");             // the same effect
        Outer<int>.Inner<string>.F(3, "abc");  // This type is different
        Outer.Inner<string>.F(t, "abc");       // Error, Outer needs type arg
    }
}

przykład końcowy

Mimo że jest to zły styl programowania, parametr typu w zagnieżdżonym typie może ukryć element członkowski lub parametr typu zadeklarowany w typie zewnętrznym.

Przykład:

class Outer<T>
{
    class Inner<T>                                  // Valid, hides Outer's T
    {
        public T t;                                 // Refers to Inner's T
    }
}

przykład końcowy

15.3.10 Zastrzeżone nazwy składowych

15.3.10.1 Ogólne

Aby ułatwić implementację w czasie wykonywania w języku C#, dla każdej deklaracji składowej źródłowej, która jest właściwością, zdarzeniem lub indeksatorem, implementacja zastrzega sobie dwie sygnatury metody na podstawie rodzaju deklaracji składowej, jego nazwy i jego typu (§15.3.10.2, §15.3.3, §15.3.10.4). Jest to błąd czasu kompilacji dla programu do deklarowania elementu członkowskiego, którego podpis pasuje do podpisu zarezerwowanego przez element członkowski zadeklarowany w tym samym zakresie, nawet jeśli podstawowa implementacja w czasie wykonywania nie korzysta z tych rezerwacji.

Nazwy zastrzeżone nie wprowadzają deklaracji, w związku z czym nie uczestniczą w wyszukiwaniu elementów członkowskich. Jednak skojarzone z deklaracją sygnatury metody zarezerwowanej uczestniczą w dziedziczeniu (§15.3.4) i mogą być ukryte za pomocą new modyfikatora (§15.3.5).

Uwaga: Rezerwacja tych nazw służy do trzech celów:

  1. Aby umożliwić podstawowej implementacji używanie zwykłego identyfikatora jako nazwy metody do uzyskiwania lub ustawiania dostępu do funkcji języka C#.
  2. 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#.
  3. Aby zapewnić, że źródło zaakceptowane przez jeden zgodny kompilator jest akceptowany przez inny, przez zapewnienie spójności określonych nazw zarezerwowanych elementów członkowskich we wszystkich implementacjach języka C#.

notatka końcowa

Deklaracja finalizatora (§15.13) powoduje również zarezerwowanie podpisu (§15.3.10.5).

Niektóre nazwy są zarezerwowane do użycia jako nazwy metod operatora (§15.3.10.6).

15.3.10.2 Nazwy składowe zarezerwowane dla właściwości

Dla właściwości P typu (§15.7) Tzastrzeżone są następujące podpisy:

T get_P();
void set_P(T value);

Oba podpisy są zarezerwowane, nawet jeśli właściwość jest tylko do odczytu lub tylko do zapisu.

Przykład: w poniższym kodzie

class A
{
    public int P
    {
        get => 123;
    }
}

class B : A
{
    public new int get_P() => 456;

    public new void set_P(int value)
    {
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        A a = b;
        Console.WriteLine(a.P);
        Console.WriteLine(b.P);
        Console.WriteLine(b.get_P());
    }
}

Klasa A definiuje właściwość Ptylko do odczytu , dlatego rezerwując podpisy dla get_P metod i .set_P A klasa B pochodzi z A obu tych podpisów zarezerwowanych i ukrywa je. W przykładzie są generowane dane wyjściowe:

123
123
456

przykład końcowy

15.3.10.3 Nazwy składowe zarezerwowane dla zdarzeń

W przypadku zdarzenia E (§15.8) typu Tdelegata zastrzeżone są następujące podpisy:

void add_E(T handler);
void remove_E(T handler);

15.3.10.4 Nazwy składowe zarezerwowane dla indeksatorów

W przypadku indeksatora (§15.9) typu T z listą Lparametrów zastrzeżone są następujące podpisy:

T get_Item(L);
void set_Item(L, T value);

Oba podpisy są zarezerwowane, nawet jeśli indeksator jest tylko do odczytu lub tylko do zapisu.

Ponadto nazwa Item elementu członkowskiego jest zarezerwowana.

15.3.10.5 Nazwy składowe zarezerwowane dla finalizatorów

W przypadku klasy zawierającej finalizator (§15.13) zarezerwowany jest następujący podpis:

void Finalize();

15.3.10.6 Nazwy metod zarezerwowane dla operatorów

Następujące nazwy metod są zarezerwowane. Chociaż wiele z nich ma odpowiednie operatory w tej specyfikacji, niektóre są zarezerwowane do użytku przez przyszłe wersje, podczas gdy niektóre są zarezerwowane do współdziałania z innymi językami.

Nazwa metody C# Operator
op_Addition + (binarny)
op_AdditionAssignment (zarezerwowane)
op_AddressOf (zarezerwowane)
op_Assign (zarezerwowane)
op_BitwiseAnd & (binarny)
op_BitwiseAndAssignment (zarezerwowane)
op_BitwiseOr \|
op_BitwiseOrAssignment (zarezerwowane)
op_CheckedAddition (zarezerwowane do użytku w przyszłości)
op_CheckedDecrement (zarezerwowane do użytku w przyszłości)
op_CheckedDivision (zarezerwowane do użytku w przyszłości)
op_CheckedExplicit (zarezerwowane do użytku w przyszłości)
op_CheckedIncrement (zarezerwowane do użytku w przyszłości)
op_CheckedMultiply (zarezerwowane do użytku w przyszłości)
op_CheckedSubtraction (zarezerwowane do użytku w przyszłości)
op_CheckedUnaryNegation (zarezerwowane do użytku w przyszłości)
op_Comma (zarezerwowane)
op_Decrement -- (prefiks i postfiks)
op_Division /
op_DivisionAssignment (zarezerwowane)
op_Equality ==
op_ExclusiveOr ^
op_ExclusiveOrAssignment (zarezerwowane)
op_Explicit jawne (zawężenie)
op_False false
op_GreaterThan >
op_GreaterThanOrEqual >=
op_Implicit przymus niejawny (rozszerzający)
op_Increment ++ (prefiks i postfiks)
op_Inequality !=
op_LeftShift <<
op_LeftShiftAssignment (zarezerwowane)
op_LessThan <
op_LessThanOrEqual <=
op_LogicalAnd (zarezerwowane)
op_LogicalNot !
op_LogicalOr (zarezerwowane)
op_MemberSelection (zarezerwowane)
op_Modulus %
op_ModulusAssignment (zarezerwowane)
op_MultiplicationAssignment (zarezerwowane)
op_Multiply * (binarny)
op_OnesComplement ~
op_PointerDereference (zarezerwowane)
op_PointerToMemberSelection (zarezerwowane)
op_RightShift >>
op_RightShiftAssignment (zarezerwowane)
op_SignedRightShift (zarezerwowane)
op_Subtraction - (binarny)
op_SubtractionAssignment (zarezerwowane)
op_True true
op_UnaryNegation - (jednoargumentowy)
op_UnaryPlus + (jednoargumentowy)
op_UnsignedRightShift (zarezerwowane do użytku w przyszłości)
op_UnsignedRightShiftAssignment (zarezerwowane)

15.4 Stałe

Stała to składowa klasy, która reprezentuje wartość stałą: wartość, którą można obliczyć w czasie kompilacji. Constant_declaration wprowadza co najmniej jedną stałą danego typu.

constant_declaration
    : attributes? constant_modifier* 'const' type constant_declarators ';'
    ;

constant_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    ;

Constant_declaration może zawierać zestaw atrybutów (§22), modyfikator (new) i dowolny z dozwolonych rodzajów zadeklarowanych ułatwień dostępu (§15.3.6). Atrybuty i modyfikatory mają zastosowanie do wszystkich elementów członkowskich zadeklarowanych przez constant_declaration. Mimo że stałe są uważane za statyczne elementy członkowskie, constant_declaration ani nie wymaga ani nie zezwala na static modyfikator. Jest to błąd dla tego samego modyfikatora, który pojawia się wielokrotnie w deklaracji stałej.

Typconstant_declaration określa typ elementów członkowskich wprowadzonych przez deklarację. Po typie następuje lista constant_declarator s (§13.6.3), z których każdywprowadza nowego członka. Constant_declarator składa się z identyfikatora, który nazywa element członkowski, po którym następuje token "=", a następnie constant_expression (§12.23), który daje wartość elementu członkowskiego.

Typ określony w deklaracji stałej to , , sbytebyteshortushortintuintlongulongcharfloatdoubledecimalboolenum_type lub string Każda constant_expression zwraca wartość typu docelowego lub typu, który można przekonwertować na typ docelowy przez niejawną konwersję (§10.2).

Typ stałej jest co najmniej tak dostępny, jak sama stała (§7.5.5).

Wartość stałej jest uzyskiwana w wyrażeniu przy użyciu simple_name (§12.8.4) lub member_access (§12.8.7).

Stała może uczestniczyć w constant_expression. W związku z tym stała może być używana w dowolnej konstrukcji, która wymaga constant_expression.

Uwaga: Przykłady takich konstrukcji obejmują case etykiety, goto case instrukcje, enum deklaracje składowe, atrybuty i inne deklaracje stałe. notatka końcowa

Uwaga: zgodnie z opisem w §12.23 constant_expression jest wyrażeniem, które można w pełni ocenić w czasie kompilacji. Ponieważ jedynym sposobem utworzenia wartości innej string zastosowanie new operatora, a ponieważ new operator nie jest dozwolony w constant_expression, jedyną możliwą wartością dla stałych reference_types innych niż string jest null. notatka końcowa

Gdy żądana jest nazwa symboliczna dla wartości stałej, ale gdy typ tej wartości nie jest dozwolony w deklaracji stałej lub gdy nie można obliczyć wartości w czasie kompilacji przez constant_expression, można użyć pola readonly (§15.5.3).

Uwaga: semantyka wersji i constreadonly 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ści 10, 11i 12.

przykład końcowy

Deklaracje stałe mogą zależeć od stałych z innych programów, ale takie zależności są możliwe tylko w jednym kierunku.

Przykład: odwołanie się do powyższego przykładu, jeśli A i B zostało zadeklarowane w oddzielnych programach, możliwe A.X byłoby, aby zależeć od B.Zelementu , ale B.Z nie mogłoby to być jednocześnie zależne od A.Y. przykład końcowy

15.5 Pola

15.5.1 Ogólne

Pole to składowa reprezentująca zmienną skojarzona z obiektem lub klasą. Field_declaration wprowadza co najmniej jedno pole danego typu.

field_declaration
    : attributes? field_modifier* type variable_declarators ';'
    ;

field_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'readonly'
    | 'volatile'
    | unsafe_modifier   // unsafe code support
    ;

variable_declarators
    : variable_declarator (',' variable_declarator)*
    ;

variable_declarator
    : identifier ('=' variable_initializer)?
    ;

unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).

Field_declaration może zawierać zestaw atrybutów(§22), new modyfikator (§15.3.5), prawidłową kombinację czterech modyfikatorów dostępu (§15.3.6) i static modyfikatora (§15.5.2). Ponadto field_declaration może zawierać readonly modyfikator (§15.5.3) lub volatile modyfikator (§15.5.4), ale nie oba. Atrybuty i modyfikatory mają zastosowanie do wszystkich elementów członkowskich zadeklarowanych przez field_declaration. Jest to błąd tego samego modyfikatora, który pojawia się wiele razy w field_declaration.

Typfield_declaration określa typ elementów członkowskich wprowadzonych przez deklarację. Po typie następuje lista variable_declarators, z których każdy wprowadza nowy element członkowski. Variable_declarator składa się z identyfikatora, który nazywa ten element członkowski, opcjonalnie następuje token "=" i variable_initializer (§15.5.6), który daje początkową wartość tego elementu członkowskiego.

Typ pola jest co najmniej tak dostępny, jak samo pole (§7.5.5).

Wartość pola jest uzyskiwana w wyrażeniu przy użyciu simple_name (§12.8.4), member_access (§12.8.7) lub base_access (§12.8.15). Wartość pola nieczytanego jest modyfikowana przy użyciu przypisania (§12.21). Wartość pola nieczytanego można uzyskać i zmodyfikować przy użyciu operatorów przyrostowych i dekrementacyjnych (§12.8.16) oraz operatorów przyrostku i dekrementacji (§12.9.6).

Deklaracja pola, która deklaruje wiele pól, jest równoważna wielokrotnym deklaracjom pojedynczych pól z tymi samymi atrybutami, modyfikatorami i typem.

Przykład:

class A
{
    public static int X = 1, Y, Z = 100;
}

jest równoważny

class A
{
    public static int X = 1;
    public static int Y;
    public static int Z = 100;
}

przykład końcowy

15.5.2 Pola statyczne i wystąpienia

Gdy deklaracja pola zawiera static modyfikator, pola wprowadzone przez deklarację to pola statyczne. Jeśli modyfikator nie static jest obecny, pola wprowadzone przez deklarację są polami wystąpienia. Pola statyczne i pola wystąpienia to dwa z kilku rodzajów zmiennych (§9) obsługiwanych przez język C#, a czasami są nazywane zmiennymi statycznymi i zmiennymi wystąpienia, odpowiednio.

Zgodnie z opisem w §15.3.8 każde wystąpienie klasy zawiera pełny zestaw pól wystąpienia klasy, podczas gdy istnieje tylko jeden zestaw pól statycznych dla każdej klasy niegenerycznej lub typu zamkniętego, niezależnie od liczby wystąpień klasy lub typu skonstruowanego zamkniętego.

15.5.3 Pola tylko do odczytu

15.5.3.1 Ogólne

Gdy field_declaration zawiera readonly modyfikator, pola wprowadzone przez deklarację są polami tylko do odczytu. Bezpośrednie przypisania do pól tylko do odczytu mogą występować w ramach tej deklaracji lub w konstruktorze wystąpienia lub konstruktorze statycznym w tej samej klasie. (Pole tylko do odczytu można przypisać do wielu razy w tych kontekstach). W szczególności bezpośrednie przypisania do pola readonly są dozwolone tylko w następujących kontekstach:

  • W variable_declarator wprowadzającym pole (w tym variable_initializer w deklaracji).
  • W polu wystąpienia w konstruktorach wystąpienia klasy zawierającej deklarację pola; dla pola statycznego w konstruktorze statycznym klasy zawierającej deklarację pola. Są to również jedyne konteksty, w których ważne jest przekazanie pola readonly jako parametru wyjściowego lub referencyjnego.

Próba przypisania do pola readonly lub przekazania go jako parametru wyjściowego lub referencyjnego w innym kontekście jest błędem czasu kompilacji.

15.5.3.2 Używanie statycznych pól readonly dla stałych

Statyczne pole readonly jest przydatne, gdy żądana jest nazwa symboliczna stałej wartości, ale gdy typ wartości nie jest dozwolony w deklaracji const lub gdy nie można obliczyć wartości w czasie kompilacji.

Przykład: w poniższym kodzie

public class Color
{
    public static readonly Color Black = new Color(0, 0, 0);
    public static readonly Color White = new Color(255, 255, 255);
    public static readonly Color Red = new Color(255, 0, 0);
    public static readonly Color Green = new Color(0, 255, 0);
    public static readonly Color Blue = new Color(0, 0, 255);

    private byte red, green, blue;

    public Color(byte r, byte g, byte b)
    {
        red = r;
        green = g;
        blue = b;
    }
}

nie Blackmożna zadeklarować elementów członkowskich , White, RedGreen, i Blue jako elementów członkowskich const, ponieważ ich wartości nie mogą być obliczane w czasie kompilacji. Jednak deklarowanie ich static readonly zamiast tego ma taki sam efekt.

przykład końcowy

15.5.3.3 Przechowywanie wersji stałych i statycznych pól tylko do odczytu

Stałe i pola odczytu mają różne semantyka przechowywania wersji binarnych. Gdy wyrażenie odwołuje się do stałej, wartość stałej jest uzyskiwana w czasie kompilacji, ale gdy wyrażenie odwołuje się do pola readonly, wartość pola nie jest uzyskiwana do czasu wykonywania.

Przykład: Rozważ aplikację składającą się z dwóch oddzielnych programów:

namespace Program1
{
    public class Utils
    {
        public static readonly int x = 1;
    }
}

oraz

namespace Program2
{
    class Test
    {
        static void Main()
        {
            Console.WriteLine(Program1.Utils.X);
        }
    }
}

Przestrzenie Program1 nazw i Program2 oznaczają dwa programy, które są kompilowane oddzielnie. Ponieważ Program1.Utils.X jest zadeklarowana jako pole, wartość wyjściowa static readonly instrukcji Console.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 i Program1 zostanie ponownie skompilowana, instrukcja zwróci nową wartość, Console.WriteLine nawet jeśli Program2 nie zostanie ponownie skompilowana. Jednak gdyby X była stała, wartość X elementu zostałaby uzyskana w czasie Program2 kompilacji i pozostałaby bez zmian do Program1Program2 momentu ponownego skompilowania.

przykład końcowy

15.5.4 Pola nietrwałe

Gdy field_declaration zawiera modyfikator, pola wprowadzone przez deklarację volatile nietrwałym. W przypadku pól nietrwałych techniki optymalizacji, które zmieniają kolejność instrukcji, mogą prowadzić do nieoczekiwanych i nieprzewidywalnych wyników w programach wielowątkowe, które uzyskują dostęp do pól bez synchronizacji, takich jak dostarczone przez lock_statement (§13.13). Te optymalizacje mogą być wykonywane przez kompilator, przez system czasu wykonywania lub przez sprzęt. W przypadku pól nietrwałych takie optymalizacje zmiany kolejności są ograniczone:

  • Odczyt pola lotnego jest nazywany odczytem lotnym. Odczyt volatile ma "uzyskiwanie semantyki"; oznacza to, że jest gwarantowane przed wszelkimi odwołaniami do pamięci, które występują po niej w sekwencji instrukcji.
  • Zapis pola lotnego jest nazywany nietrwałym zapisem. Zapis volatile ma "semantyka wydania"; oznacza to, że jest to gwarantowane po wszelkich odwołaniach do pamięci przed instrukcją zapisu w sekwencji instrukcji.

Te ograniczenia zapewniają, że wszystkie wątki będą obserwować nietrwałe zapisy wykonywane przez dowolny inny wątek w kolejności, w jakiej zostały wykonane. Implementacja zgodna nie jest wymagana, aby zapewnić pojedynczą całkowitą kolejność nietrwałych zapisów, jak pokazano we wszystkich wątkach wykonywania. Typ pola lotnego musi być jednym z następujących:

  • Reference_type.
  • Type_parameter, który jest znany jako typ odwołania (§15.2.5).
  • Typ byte, sbyteshortushortintuintcharfloatboolSystem.IntPtrlub .System.UIntPtr
  • Enum_type uint

Przykład: przykład

class Test
{
    public static int result;
    public static volatile bool finished;

    static void Thread2()
    {
        result = 143;
        finished = true;
    }

    static void Main()
    {
        finished = false;

        // Run Thread2() in a new thread
        new Thread(new ThreadStart(Thread2)).Start();    

        // Wait for Thread2() to signal that it has a result
        // by setting finished to true.
        for (;;)
        {
            if (finished)
            {
                Console.WriteLine($"result = {result}");
                return;
            }
        }
    }
}

generuje dane wyjściowe:

result = 143

W tym przykładzie metoda Main uruchamia nowy wątek, który uruchamia metodę Thread2. Ta metoda przechowuje wartość w polu nietrwałym o nazwie result, a następnie przechowuje true w polu finishedvolatile . Główny wątek czeka na ustawienie pola finished na true, a następnie odczytuje pole result. Ponieważ finished został zadeklarowany volatile, główny wątek odczytuje wartość 143 z pola result. Jeśli pole finished nie zostało zadeklarowane volatile, dopuszczalne byłoby, aby result magazyn był widoczny dla głównego wątku po magazynie do finished, a tym samym dla głównego wątku do odczytu wartości 0 z pola result. Deklarowanie finished jako volatile pola uniemożliwia takie niespójności.

przykład końcowy

Inicjowanie pola 15.5.5

Początkowa wartość pola, niezależnie od tego, czy jest to pole statyczne, czy pole wystąpienia, jest wartością domyślną (§9.3) typu pola. Nie można obserwować wartości pola przed wystąpieniem tego domyślnego inicjowania, a pole nigdy nie jest "niezainicjowane".

Przykład: przykład

class Test
{
    static bool b;
    int i;

    static void Main()
    {
        Test t = new Test();
        Console.WriteLine($"b = {b}, i = {t.i}");
    }
}

generuje dane wyjściowe

b = False, i = 0

ponieważ b i i są automatycznie inicjowane do wartości domyślnych.

przykład końcowy

Inicjatory zmiennych 15.5.6

15.5.6.1 Ogólne

Deklaracje pól mogą zawierać variable_initializers. W przypadku pól statycznych inicjatory zmiennych odpowiadają instrukcjom przypisania wykonywanym podczas inicjowania klasy. Na przykład inicjatory zmiennych odpowiadają instrukcjom przypisania wykonywanym podczas tworzenia wystąpienia klasy.

Przykład: przykład

class Test
{
    static double x = Math.Sqrt(2.0);
    int i = 100;
    string s = "Hello";

    static void Main()
    {
        Test a = new Test();
        Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}");
    }
}

generuje dane wyjściowe

x = 1.4142135623730951, i = 100, s = Hello

ponieważ przypisanie ma x miejsce, gdy inicjatory pól statycznych wykonują i przypisań do i i s występują, gdy inicjatory pól wystąpienia są wykonywane.

przykład końcowy

Inicjowanie wartości domyślnej opisane w §15.5.5 występuje dla wszystkich pól, w tym pól, które mają inicjatory zmiennych. W związku z tym po zainicjowaniu klasy wszystkie pola statyczne w tej klasie są najpierw inicjowane do ich wartości domyślnych, a następnie inicjatory pól statycznych są wykonywane w kolejności tekstowej. Podobnie po utworzeniu wystąpienia klasy wszystkie pola wystąpienia w tym wystąpieniu są najpierw inicjowane do ich wartości domyślnych, a następnie inicjatory pól wystąpienia są wykonywane w kolejności tekstowej. W przypadku deklaracji pól w wielu deklaracjach typu częściowego dla tego samego typu kolejność części jest nieokreślona. Jednak w każdej części inicjatory pól są wykonywane w kolejności.

Istnieje możliwość zaobserwowania pól statycznych z inicjatorami zmiennych w ich domyślnym stanie wartości.

Przykład: Jest to jednak zdecydowanie zniechęcone jako kwestia stylu. Przykład

class Test
{
    static int a = b + 1;
    static int b = a + 1;

    static void Main()
    {
        Console.WriteLine($"a = {a}, b = {b}");
    }
}

wykazuje to zachowanie. Pomimo okrągłych a definicji i bprogram jest prawidłowy. Powoduje to wyświetlenie danych wyjściowych

a = 1, b = 2

ponieważ pola a statyczne i b są inicjowane do 0 (wartość domyślna dla int) przed ich inicjatorami są wykonywane. Gdy inicjator dla a przebiegów ma wartość b zero, a więc a jest inicjowany na 1wartość . Gdy inicjator dla b przebiegów, wartość elementu jest już 1wartością , a więc b jest inicjowana na wartość 2.

przykład końcowy

15.5.6.2 Inicjowanie pola statycznego

Inicjatory zmiennej pola statycznego klasy odpowiadają sekwencji przypisań wykonywanych w kolejności tekstowej, w której pojawiają się w deklaracji klasy (§15.5.6.1). W klasie częściowej znaczenie "kolejności tekstowej" określa §15.5.6.1. Jeśli w klasie istnieje konstruktor statyczny (§15.12), wykonanie inicjatorów pól statycznych odbywa się bezpośrednio przed wykonaniem tego konstruktora statycznego. W przeciwnym razie inicjatory pól statycznych są wykonywane w czasie zależnym od implementacji przed pierwszym użyciem pola statycznego tej klasy.

Przykład: przykład

class Test
{
    static void Main()
    {
        Console.WriteLine($"{B.Y} {A.X}");
    }

    public static int F(string s)
    {
        Console.WriteLine(s);
        return 1;
    }
}

class A
{
    public static int X = Test.F("Init A");
}

class B
{
    public static int Y = Test.F("Init B");
}

może spowodować wygenerowanie danych wyjściowych:

Init A
Init B
1 1

lub dane wyjściowe:

Init B
Init A
1 1

ponieważ wykonanie Xinicjatora i Yinicjatora może wystąpić w każdej kolejności; są one ograniczone tylko przed odwołaniami do tych pól. Jednak w przykładzie:

class Test
{
    static void Main()
    {
        Console.WriteLine($"{B.Y} {A.X}");
    }

    public static int F(string s)
    {
        Console.WriteLine(s);
        return 1;
    }
}

class A
{
    static A() {}
    public static int X = Test.F("Init A");
}

class B
{
    static B() {}
    public static int Y = Test.F("Init B");
}

dane wyjściowe to:

Init B
Init A
1 1

ponieważ reguły dotyczące wykonywania konstruktorów statycznych (zgodnie z definicją w §15.12) zapewniają, że Bkonstruktor statyczny (i dlatego Binicjatory pól statycznych) będą uruchamiane przed Ainicjatorami statycznymi i inicjatorami pól.

przykład końcowy

Inicjowanie pola wystąpienia 15.5.6.3

Inicjatory zmiennej pola wystąpienia klasy odpowiadają sekwencji przypisań wykonywanych natychmiast po wpisie do dowolnego konstruktora wystąpienia (§15.11.3) tej klasy. W klasie częściowej znaczenie "kolejności tekstowej" określa §15.5.6.1. Inicjatory zmiennych są wykonywane w kolejności tekstowej, w której są wyświetlane w deklaracji klasy (§15.5.6.1). Proces tworzenia i inicjowania wystąpienia klasy jest opisany dalej w §15.11.

Inicjator zmiennej dla pola wystąpienia nie może odwoływać się do tworzonego wystąpienia. W związku z tym jest to błąd czasu kompilacji, do którego należy odwołanie this w inicjatorze zmiennej, ponieważ jest to błąd czasu kompilacji dla inicjatora zmiennej w celu odwołowania się do dowolnego elementu członkowskiego wystąpienia za pośrednictwem simple_name.

Przykład: w poniższym kodzie

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

inicjator zmiennej dla y wyników błędu czasu kompilacji, ponieważ odwołuje się do elementu członkowskiego tworzonego wystąpienia.

przykład końcowy

15.6 Metody

15.6.1 Ogólne

Metoda jest elementem członkowskim, który implementuje obliczenia lub akcję, która może być wykonywana przez obiekt lub klasę. Metody są deklarowane przy użyciu method_declarations:

method_declaration
    : attributes? method_modifiers return_type method_header method_body
    | attributes? ref_method_modifiers ref_kind ref_return_type method_header
      ref_method_body
    ;

method_modifiers
    : method_modifier* 'partial'?
    ;

ref_kind
    : 'ref'
    | 'ref' 'readonly'
    ;

ref_method_modifiers
    : ref_method_modifier*
    ;

method_header
    : member_name '(' parameter_list? ')'
    | member_name type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause*
    ;

method_modifier
    : ref_method_modifier
    | 'async'
    ;

ref_method_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

return_type
    : ref_return_type
    | 'void'
    ;

ref_return_type
    : type
    ;

member_name
    : identifier
    | interface_type '.' identifier
    ;

method_body
    : block
    | '=>' null_conditional_invocation_expression ';'
    | '=>' expression ';'
    | ';'
    ;

ref_method_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

Uwagi gramatyczne:

  • unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).
  • w przypadku uznania method_body, jeśli stosowane są zarówno null_conditional_invocation_expression, jak i alternatywy wyrażeń, wówczas należy wybrać pierwszy.

Uwaga: Nakładanie się między nimi i priorytet między nimi jest przeznaczone wyłącznie dla wygody opisowej; reguły gramatyczne można opracować, aby usunąć nakładające się. ANTLR i inne systemy gramatyczne przyjmują tę samą wygodę i dlatego method_body ma określone semantyki automatycznie. notatka końcowa

Method_declaration może zawierać zestaw atrybutów (§22) i jeden z dozwolonych rodzajów zadeklarowanych ułatwień dostępu (§15.3.6), new(§15.3.5), (§15.6.3), static (§15.3). virtual 6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7), extern (§15.6.8) i async (§15.15) modyfikatory.

Deklaracja ma prawidłową kombinację modyfikatorów, jeśli wszystkie następujące elementy są prawdziwe:

  • Deklaracja zawiera prawidłową kombinację modyfikatorów dostępu (§15.3.6).
  • Deklaracja nie zawiera tego samego modyfikatora wiele razy.
  • Deklaracja zawiera co najwyżej jeden z następujących modyfikatorów: static, virtuali override.
  • Deklaracja zawiera co najwyżej jeden z następujących modyfikatorów: new i override.
  • Jeśli deklaracja zawiera abstract modyfikator, deklaracja nie zawiera żadnego z następujących modyfikatorów: static, , virtualsealedlub extern.
  • Jeśli deklaracja zawiera private modyfikator, deklaracja nie zawiera żadnego z następujących modyfikatorów: virtual, lub overrideabstract.
  • Jeśli deklaracja zawiera sealed modyfikator, deklaracja zawiera override również modyfikator.
  • Jeśli deklaracja zawiera partial modyfikator, nie zawiera żadnego z następujących modyfikatorów: new, , , publicprotectedinternalprivatevirtualsealedoverrideabstractlub .extern

Metody są klasyfikowane zgodnie z tym, co, jeśli w ogóle, zwracają:

  • Jeśli ref jest obecny, metoda jest zwracana przez ref i zwraca odwołanie do zmiennej, czyli opcjonalnie tylko do odczytu;
  • W przeciwnym razie, jeśli return_type to void, metoda zwraca wartość bez wartości i nie zwraca wartości;
  • W przeciwnym razie metoda zwraca wartość by-value i zwraca wartość.

Return_type deklaracji metody return-by-value lub return-no-value określa typ wyniku, jeśli istnieje, zwracany przez metodę. Tylko metoda zwracana bez wartości może zawierać partial modyfikator (§15.6.9). Jeśli deklaracja zawiera async modyfikator, return_type musi być void lub metoda zwracana przez wartość, a zwracany typ jest typem zadania (§15.15.1).

Ref_return_type deklaracji metody return-by-ref określa typ zmiennej przywoływanej przez variable_reference zwracaną przez metodę.

Metoda ogólna to metoda, której deklaracja zawiera type_parameter_list. Określa parametry typu dla metody . Opcjonalne type_parameter_constraints_clauseokreślają ograniczenia dla parametrów typu.

Ogólny method_declaration dla implementacji jawnego elementu członkowskiego interfejsu nie ma żadnych type_parameter_constraints_clauses; deklaracja dziedziczy wszelkie ograniczenia z ograniczeń metody interfejsu.

Podobnie deklaracja metody z override modyfikatorem nie ma żadnych type_parameter_constraints_clauses, a ograniczenia parametrów typu metody są dziedziczone z zastępowania metody wirtualnej.

Member_name określa nazwę metody. Chyba że metoda jest jawną implementacją składową interfejsu (§18.6.2), member_name jest po prostu identyfikatorem.

W przypadku jawnej implementacji składowej interfejsu member_name składa się z interface_type , po którym następuje "." i identyfikator. W takim przypadku deklaracja nie zawiera żadnych modyfikatorów innych niż (ewentualnie) extern ani async.

Opcjonalny parameter_list określa parametry metody (§15.6.2).

Return_type lub ref_return_type, a każdy z typów, do których odwołuje się parameter_list metody, jest co najmniej tak dostępny jak sama metoda (§7.5.5).

Method_body metody zwracanej po wartości lub zwracanej wartości jest średnikiem, treścią bloku lub treścią wyrażenia. Treść bloku składa się z bloku, który określa instrukcje do wykonania po wywołaniu metody. Treść wyrażenia składa się z =>elementu , po którym następuje null_conditional_invocation_expression lub wyrażenie oraz średnik, i oznacza pojedyncze wyrażenie do wykonania podczas wywoływanej metody.

W przypadku metod abstrakcyjnych i extern method_body składa się po prostu ze średnika. W przypadku metod częściowych method_body może składać się z średnika, treści bloku lub treści wyrażenia. W przypadku wszystkich innych metod method_body jest treścią bloku lub treścią wyrażenia.

Jeżeli method_body składa się z średnika, deklaracja nie zawiera async modyfikatora.

Ref_method_body metody zwracanej przez ref jest średnikiem, treścią bloku lub treścią wyrażenia. Treść bloku składa się z bloku, który określa instrukcje do wykonania po wywołaniu metody. Treść wyrażenia składa się z =>, a następnie ref, variable_reference i średnika, i określa jeden variable_reference do oceny, gdy metoda jest wywoływana.

W przypadku metod abstrakcyjnych i ekstern ref_method_body składa się po prostu ze średnika; dla wszystkich innych metod ref_method_body jest treścią bloku lub treścią wyrażenia.

Nazwa, liczba parametrów typu i lista parametrów metody definiuje podpis (§7.6) metody. W szczególności podpis metody składa się z jej nazwy, liczby parametrów typu i liczby, parameter_mode_modifier s (§15.6.2.1) i typówjej parametrów. Zwracany typ nie jest częścią podpisu metody ani nazwy parametrów, nazw parametrów typu lub ograniczeń. Gdy typ parametru odwołuje się do parametru typu metody, pozycja porządkowa parametru typu (a nie nazwa parametru typu) jest używana do równoważności typu.

Nazwa metody różni się od nazw wszystkich innych metod, które nie są deklarowane w tej samej klasie. Ponadto podpis metody różni się od podpisów wszystkich innych metod zadeklarowanych w tej samej klasie, a dwie metody zadeklarowane w tej samej klasie nie mają podpisów, które różnią się wyłącznie wartościami in, outi ref.

Type_parameter metody znajdują się w zakresie method_declaration i mogą służyć do tworzenia typów w tym zakresie w return_type lub ref_return_type, method_body lub ref_method_body, a type_parameter_constraints_clause, ale nie w atrybutach.

Wszystkie parametry i parametry typu mają różne nazwy.

Parametry metody 15.6.2

15.6.2.1 Ogólne

Parametry metody , jeśli istnieją, są deklarowane przez parameter_list metody.

parameter_list
    : fixed_parameters
    | fixed_parameters ',' parameter_array
    | parameter_array
    ;

fixed_parameters
    : fixed_parameter (',' fixed_parameter)*
    ;

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

default_argument
    : '=' expression
    ;

parameter_modifier
    : parameter_mode_modifier
    | 'this'
    ;

parameter_mode_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

parameter_array
    : attributes? 'params' array_type identifier
    ;

Lista parametrów składa się z co najmniej jednego parametru rozdzielanego przecinkami, z których tylko ostatni może być parameter_array.

Fixed_parameter składa się z opcjonalnego zestawu atrybutówin, out, reflub this modyfikatora; typu, identyfikatora i opcjonalnego default_argument. Każdy fixed_parameter deklaruje parametr danego typu o podanej nazwie. this Modyfikator wyznacza metodę jako metodę rozszerzenia i jest dozwolony tylko dla pierwszego parametru metody statycznej w klasie statycznej innej niż ogólna, nienagnieżdżona. Jeśli parametr jest typem struct lub parametrem typu ograniczonym do structklasy , this modyfikator może zostać połączony z ref modyfikatorem lub in , ale nie modyfikatorem out . Metody rozszerzeń zostały szczegółowo opisane w §15.6.10. Fixed_parameter z default_argument jest znany jako parametr opcjonalny, natomiast fixed_parameter bez default_argument jest wymaganym parametrem. Wymagany parametr nie pojawia się po opcjonalnym parametrze w parameter_list.

Parametr z modyfikatorem refout lub this nie może mieć default_argument. Parametr wejściowy może mieć default_argument. Wyrażenie w default_argument jest jednym z następujących:

  • constant_expression
  • wyrażenie formularza new S() , w którym S jest typem wartości
  • wyrażenie formularza default(S) , w którym S jest typem wartości

Wyrażenie jest niejawnie konwertowane przez tożsamość lub konwersję dopuszczaną do wartości null do typu parametru.

Jeśli parametry opcjonalne występują w implementacji deklaracji metody częściowej (§15.6.9), jawna implementacja elementu członkowskiego interfejsu (§18.6.2), deklaracja indeksatora z jednym parametrem (§1 15.9) lub w deklaracji operatora (§15.10.1) kompilator powinien dać ostrzeżenie, ponieważ te elementy członkowskie nigdy nie mogą być wywoływane w sposób umożliwiający pominięcie argumentów.

Parameter_array składa się z opcjonalnego zestawu atrybutów (§22), params modyfikatora, array_type i identyfikatora. Tablica parametrów deklaruje pojedynczy parametr danego typu tablicy o podanej nazwie. Array_type tablicy parametrów jest jednowymiarowym typem tablicy (§17.2). W wywołaniu metody tablica parametrów zezwala na określenie pojedynczego argumentu danego typu tablicy lub dopuszcza zero lub więcej argumentów typu elementu tablicy. Tablice parametrów są opisane dalej w §15.6.2.4.

Parameter_array może wystąpić po opcjonalnym parametrze, ale nie może mieć wartości domyślnej — pominięcie argumentów parameter_array spowodowałoby utworzenie pustej tablicy.

Przykład: Poniżej przedstawiono różne rodzaje parametrów:

void M<T>(
    ref int i,
    decimal d,
    bool b = false,
    bool? n = false,
    string s = "Hello",
    object o = null,
    T t = default(T),
    params int[] a
) { }

W parameter_listMjest wymaganym i parametrem, ref jest wymaganym parametrem wartości, d, bs, i o są opcjonalnymi parametrami wartości i t jest tablicą parametrówa.

przykład końcowy

Deklaracja metody tworzy oddzielną przestrzeń deklaracji (§7.3) dla parametrów i parametrów typu. Nazwy są wprowadzane do tej przestrzeni deklaracji przez listę parametrów typu i listę parametrów metody. Treść metody, jeśli istnieje, jest uważana za zagnieżdżona w tej przestrzeni deklaracji. Jest to błąd dwóch elementów członkowskich przestrzeni deklaracji metody, aby mieć taką samą nazwę.

Wywołanie metody (§12.8.10.2) tworzy kopię, specyficzną dla tego wywołania, parametrów i zmiennych lokalnych metody, a lista argumentów wywołania przypisuje wartości lub odwołania do zmiennych do nowo utworzonych parametrów. W bloku metody parametry mogą być przywoływane przez ich identyfikatory w wyrażeniach simple_name (§12.8.4).

Istnieją następujące rodzaje parametrów:

Uwaga: Zgodnie z opisem w §7.6 modyfikatory in, outi ref są częścią podpisu metody, ale params modyfikator nie jest. notatka końcowa

15.6.2.2 Parametry wartości

Parametr zadeklarowany bez modyfikatorów jest parametrem wartości. Parametr wartości to zmienna lokalna, która pobiera jego wartość początkową z odpowiedniego argumentu podanego w wywołaniu metody.

Aby uzyskać określone reguły przypisania, zobacz §9.2.5.

Odpowiedni argument w wywołaniu metody jest wyrażeniem niejawnie konwertowanym (§10.2) do typu parametru.

Metoda może przypisywać nowe wartości do parametru wartości. Takie przypisania mają wpływ tylko na lokalną lokalizację magazynu reprezentowaną przez parametr wartości — nie mają wpływu na rzeczywisty argument podany w wywołaniu metody.

15.6.2.3 Parametry referencyjne

15.6.2.3.1 Ogólne

Parametry wejściowe, wyjściowe i referencyjne są parametramireferencyjnymi. Parametr by-reference jest lokalną zmienną referencyjną (§9.7); początkowy referent jest uzyskiwany z odpowiedniego argumentu podanego w wywołaniu metody.

Uwaga: odwołanie do parametru by-reference można zmienić przy użyciu operatora przypisania ref (= ref).

Gdy parametr jest parametrem by-reference, odpowiedni argument w wywołaniu metody składa się z odpowiedniego słowa kluczowego , , lub , a następnie in (ref) tego samego typu co parametr.out Jednak gdy parametr jest parametremin, argument może być wyrażeniem, dla którego istnieje niejawna konwersja (§10.2) z tego wyrażenia argumentu na typ odpowiedniego parametru.

Parametry by-reference nie są dozwolone w funkcjach zadeklarowanych jako iterator (§15.14) lub funkcji asynchronicznych (§15.15).

W metodzie, która przyjmuje wiele parametrów referencyjnych, istnieje możliwość, aby wiele nazw reprezentowało tę samą lokalizację magazynu.

15.6.2.3.2 Parametry wejściowe

Parametr zadeklarowany za pomocą in modyfikatora jest parametrem wejściowym. Argument odpowiadający parametrowi wejściowemu jest zmienną istniejącą w punkcie wywołania metody lub argumentem utworzonym przez implementację (§12.6.2.3) w wywołaniu metody. Aby uzyskać określone reguły przypisania, zobacz §9.2.8.

Jest to błąd czasu kompilacji, aby zmodyfikować wartość parametru wejściowego.

Uwaga: Podstawowym celem parametrów wejściowych jest wydajność. Jeśli typ parametru metody jest dużą strukturą (jeśli chodzi o wymagania dotyczące pamięci), warto unikać kopiowania całej wartości argumentu podczas wywoływania metody. Parametry wejściowe umożliwiają metody odwoływania się do istniejących wartości w pamięci, zapewniając jednocześnie ochronę przed niepożądanymi zmianami tych wartości. notatka końcowa

15.6.2.3.3 Parametry odwołania

Parametr zadeklarowany za pomocą ref modyfikatora jest parametrem referencyjnym. Aby uzyskać określone reguły przypisania, zobacz §9.2.6.

Przykład: przykład

class Test
{
    static void Swap(ref int x, ref int y)
    {
        int temp = x;
        x = y;
        y = temp;
    }

    static void Main()
    {
        int i = 1, j = 2;
        Swap(ref i, ref j);
        Console.WriteLine($"i = {i}, j = {j}");
    }
}

generuje dane wyjściowe

i = 2, j = 1

W przypadku wywołania elementu w elemecie Swap reprezentuje Mainx i i reprezentuje wartość y.j W związku z tym wywołanie ma wpływ na zamianę wartości i i j.

przykład końcowy

Przykład: w poniższym kodzie

class A
{
    string s;
    void F(ref string a, ref string b)
    {
        s = "One";
        a = "Two";
        b = "Three";
    }

    void G()
    {
        F(ref s, ref s);
    }
}

wywołanie F metody in G przekazuje odwołanie do s elementu zarówno dla , jak a i b. W związku z tym dla tego wywołania nazwy s, ai b wszystkie odwołują się do tej samej lokalizacji magazynu, a trzy przypisania modyfikują pole swystąpienia .

przykład końcowy

struct W przypadku typu w metodzie wystąpienia metoda dostępu wystąpienia (§12.2.1) lub konstruktor wystąpienia z inicjatorem this konstruktora słowo kluczowe zachowuje się dokładnie jako parametr referencyjny typu struktury (§12.8.14).

15.6.2.3.4 Parametry wyjściowe

Parametr zadeklarowany za pomocą out modyfikatora jest parametrem wyjściowym. W przypadku określonych reguł przypisania zobacz §9.2.7.

Metoda zadeklarowana jako metoda częściowa (§15.6.9) nie ma parametrów wyjściowych.

Uwaga: Parametry wyjściowe są zwykle używane w metodach, które generują wiele zwracanych wartości. notatka końcowa

Przykład:

class Test
{
    static void SplitPath(string path, out string dir, out string name)
    {
        int i = path.Length;
        while (i > 0)
        {
            char ch = path[i - 1];
            if (ch == '\\' || ch == '/' || ch == ':')
            {
                break;
            }
            i--;
        }
        dir = path.Substring(0, i);
        name = path.Substring(i);
    }

    static void Main()
    {
        string dir, name;
        SplitPath(@"c:\Windows\System\hello.txt", out dir, out name);
        Console.WriteLine(dir);
        Console.WriteLine(name);
    }
}

W przykładzie są generowane dane wyjściowe:

c:\Windows\System\
hello.txt

Należy pamiętać, że dir zmienne i name mogą być nieprzypisane przed przekazaniem ich do SplitPathelementu i że są uważane za zdecydowanie przypisane po wywołaniu.

przykład końcowy

15.6.2.4 Tablice parametrów

Parametr zadeklarowany za pomocą params modyfikatora jest tablicą parametrów. Jeśli lista parametrów zawiera tablicę parametrów, jest to ostatni parametr na liście i musi być typu tablicy jednowymiarowej.

Przykład: typy string[] i string[][] mogą być używane jako typ tablicy parametrów, ale typ string[,] nie może. przykład końcowy

Uwaga: nie można połączyć params modyfikatora z modyfikatorami in, outlub ref. notatka końcowa

Tablica parametrów umożliwia określenie argumentów na jeden z dwóch sposobów wywołania metody:

  • Argument podany dla tablicy parametrów może być pojedynczym wyrażeniem, które jest niejawnie konwertowane (§10.2) do typu tablicy parametrów. W tym przypadku tablica parametrów działa dokładnie jak parametr wartości.
  • Alternatywnie wywołanie może określać zero lub więcej argumentów dla tablicy parametrów, gdzie każdy argument jest wyrażeniem niejawnie konwertowanym (§10.2) do typu elementu tablicy parametrów. W tym przypadku wywołanie tworzy wystąpienie typu tablicy parametrów o długości odpowiadającej liczbie argumentów, inicjuje elementy wystąpienia tablicy z podanymi wartościami argumentów i używa nowo utworzonego wystąpienia tablicy jako rzeczywistego argumentu.

Z wyjątkiem zezwalania na zmienną liczbę argumentów w wywołaniu, tablica parametrów jest dokładnie równoważna parametrowi wartości (§15.6.2.2) tego samego typu.

Przykład: przykład

class Test
{
    static void F(params int[] args)
    {
        Console.Write($"Array contains {args.Length} elements:");
        foreach (int i in args)
        {
            Console.Write($" {i}");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        int[] arr = {1, 2, 3};
        F(arr);
        F(10, 20, 30, 40);
        F();
    }
}

generuje dane wyjściowe

Array contains 3 elements: 1 2 3
Array contains 4 elements: 10 20 30 40
Array contains 0 elements:

Pierwsze wywołanie F po prostu przekazuje tablicę arr jako parametr wartości. Drugie wywołanie języka F automatycznie tworzy cztery elementy int[] z podanymi wartościami elementu i przekazuje to wystąpienie tablicy jako parametr wartości. Podobnie trzecie wywołanie F metody tworzy element int[] zero i przekazuje to wystąpienie jako parametr wartości. Drugie i trzecie wywołania są dokładnie równoważne pisaniu:

F(new int[] {10, 20, 30, 40});
F(new int[] {});

przykład końcowy

Podczas rozwiązywania przeciążeń metoda z tablicą parametrów może mieć zastosowanie w postaci normalnej lub w postaci rozszerzonej (§12.6.4.2). Rozszerzona forma metody jest dostępna tylko wtedy, gdy normalna forma metody nie ma zastosowania i tylko wtedy, gdy odpowiednia metoda z tym samym podpisem co rozszerzony formularz nie jest jeszcze zadeklarowana w tym samym typie.

Przykład: przykład

class Test
{
    static void F(params object[] a) =>
        Console.WriteLine("F(object[])");

    static void F() =>
        Console.WriteLine("F()");

    static void F(object a0, object a1) =>
        Console.WriteLine("F(object,object)");

    static void Main()
    {
        F();
        F(1);
        F(1, 2);
        F(1, 2, 3);
        F(1, 2, 3, 4);
    }
}

generuje dane wyjściowe

F()
F(object[])
F(object,object)
F(object[])
F(object[])

W tym przykładzie dwie z możliwych rozszerzonych form metody z tablicą parametrów są już uwzględnione w klasie jako zwykłe metody. Te rozwinięte formularze nie są zatem brane pod uwagę podczas wykonywania rozpoznawania przeciążenia, a pierwsze i trzecie wywołania metody wybierają w ten sposób zwykłe metody. Gdy klasa deklaruje metodę z tablicą parametrów, nie jest rzadkością, aby uwzględnić niektóre rozwinięte formularze jako zwykłe metody. W ten sposób można uniknąć alokacji wystąpienia tablicy, które występuje po wywołaniu rozszerzonej formy metody z tablicą parametrów.

przykład końcowy

Tablica jest typem odwołania, więc wartość przekazana dla tablicy parametrów może mieć wartość null.

Przykład: przykład:

class Test
{
    static void F(params string[] array) =>
        Console.WriteLine(array == null);

    static void Main()
    {
        F(null);
        F((string) null);
    }
}

generuje dane wyjściowe:

True
False

Drugie wywołanie powoduje False , że jest równoważne F(new string[] { null }) i przekazuje tablicę zawierającą pojedyncze odwołanie o wartości null.

przykład końcowy

Gdy typ tablicy parametrów to object[], potencjalna niejednoznaczność występuje między normalną formą metody a rozwiniętym formularzem dla pojedynczego object parametru. Przyczyną niejednoznaczności jest to, że sam jest object[] niejawnie konwertowany na typ object. Niejednoznaczność nie stanowi jednak problemu, ponieważ można go rozwiązać, wstawiając rzut w razie potrzeby.

Przykład: przykład

class Test
{
    static void F(params object[] args)
    {
        foreach (object o in args)
        {
            Console.Write(o.GetType().FullName);
            Console.Write(" ");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        object[] a = {1, "Hello", 123.456};
        object o = a;
        F(a);
        F((object)a);
        F(o);
        F((object[])o);
    }
}

generuje dane wyjściowe

System.Int32 System.String System.Double
System.Object[]
System.Object[]
System.Int32 System.String System.Double

W pierwszych i ostatnich wywołaniach Fmetody ma zastosowanie normalna forma F , ponieważ istnieje niejawna konwersja typu argumentu do typu parametru (oba są typu object[]). 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 forma F nie ma zastosowania, ponieważ nie istnieje niejawna konwersja typu argumentu na typ parametru (typ object nie może być niejawnie konwertowany na typ object[]). Jednak rozszerzona forma F programu ma zastosowanie, dlatego jest wybierana przez rozwiązanie przeciążenia. W rezultacie jeden element object[] 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 obiektu object[]).

przykład końcowy

15.6.3 Metody statyczne i wystąpienia

Gdy deklaracja metody zawiera static modyfikator, ta metoda jest uważana za metodę statyczną. Jeśli modyfikator nie static jest obecny, mówi się, że metoda jest metodą wystąpienia.

Metoda statyczna nie działa na określonym wystąpieniu i jest to błąd czasu kompilacji, do których należy odwołać this się w metodzie statycznej.

Metoda wystąpienia działa na danym wystąpieniu klasy i można uzyskać do tego wystąpienia dostęp jako this (§12.8.14).

Różnice między elementami statycznymi i elementami członkowskimi wystąpień zostały omówione w temacie §15.3.8.

15.6.4 Metody wirtualne

Gdy deklaracja metody wystąpienia zawiera modyfikator wirtualny, ta metoda jest uważana za metodę wirtualną. Jeśli nie ma modyfikatora wirtualnego, mówi się, że metoda jest metodą niewirtuacyjną.

Implementacja metody innej niż wirtualna jest niezmienna: Implementacja jest taka sama, czy metoda jest wywoływana na wystąpieniu klasy, w której jest zadeklarowana, czy wystąpienie klasy pochodnej. Natomiast implementacja metody wirtualnej może zostać zastąpiona przez klasy pochodne. Proces zastępowania implementacji dziedziczonej metody wirtualnej jest znany jako zastępowanie tej metody (§15.6.5).

W wywołaniu metody wirtualnej typ czasu wykonywania wystąpienia, dla którego odbywa się wywołanie, określa rzeczywistą implementację metody do wywołania. W wywołaniu metody niewirtualnej typ czasu kompilacji wystąpienia jest czynnikiem określającym. W precyzyjnych kategoriach, gdy metoda o nazwie N jest wywoływana z listą A argumentów na wystąpieniu z typem czasu kompilacji i typem CR czasu wykonywania (gdzie R jest C albo klasa pochodna z C), wywołanie jest przetwarzane w następujący sposób:

  • W czasie powiązania rozpoznawanie przeciążeń jest stosowane do C, Ni A, aby wybrać określoną metodę M z zestawu metod zadeklarowanych w i dziedziczone przez C. Jest to opisane w §12.8.10.2.
  • Następnie w czasie wykonywania:
    • Jeśli M jest metodą niewirtuacyjną, M jest wywoływana.
    • M W przeciwnym razie jest metodą wirtualną, a najbardziej pochodna implementacja w M odniesieniu do R metody jest wywoływana.

Dla każdej metody wirtualnej zadeklarowanej w klasie lub dziedziczonej przez klasę istnieje najbardziej pochodna implementacja metody w odniesieniu do tej klasy. Najbardziej pochodna implementacja metody M wirtualnej w odniesieniu do klasy R jest określana w następujący sposób:

  • Jeśli R zawiera wprowadzającą wirtualną deklarację M, jest to najbardziej pochodna implementacja w M odniesieniu do Relementu .
  • W przeciwnym razie, jeśli R zawiera przesłonięcia elementu M, jest to najbardziej pochodna implementacja w M odniesieniu do Relementu .
  • W przeciwnym razie najbardziej pochodna implementacja w M odniesieniu do R elementu jest taka sama jak najbardziej pochodna implementacja M w odniesieniu do bezpośredniej klasy bazowej Rklasy .

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ę Gwirtualną . Klasa B wprowadza nową metodę Fniewirtuacyjną , w związku z czym ukrywa dziedziczone Fmetody , a także zastępuje dziedziczona metoda G. W przykładzie są generowane dane wyjściowe:

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

Zwróć uwagę, że instrukcja a.G() wywołuje metodę B.G, a nie A.G. Jest to spowodowane tym, że typ czasu wykonywania wystąpienia (czyli B), a nie typ czasu kompilacji wystąpienia (czyli A), określa rzeczywistą implementację metody do wywołania.

przykład końcowy

Ponieważ metody mogą ukrywać dziedziczone metody, istnieje możliwość, aby klasa zawierała kilka metod wirtualnych z tym samym podpisem. Nie stanowi to problemu niejednoznaczności, ponieważ wszystkie, ale najbardziej pochodna metoda są ukryte.

Przykład: w poniższym kodzie

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

class B : A
{
    public override void F() => Console.WriteLine("B.F");
}

class C : B
{
    public new virtual void F() => Console.WriteLine("C.F");
}

class D : C
{
    public override void F() => Console.WriteLine("D.F");
}

class Test
{
    static void Main()
    {
        D d = new D();
        A a = d;
        B b = d;
        C c = d;
        a.F();
        b.F();
        c.F();
        d.F();
    }
}

C klasy i D zawierają dwie metody wirtualne z tym samym podpisem: jedna wprowadzona przez A i wprowadzona przez C. Metoda wprowadzona przez C program ukrywa metodę dziedziczona z Aklasy . W związku z tym deklaracja przesłonięć w D zastąpieniu metody wprowadzonej przez Cmetodę , i nie jest możliwe D zastąpienie metody wprowadzonej przez Ametodę . W przykładzie są generowane dane wyjściowe:

B.F
B.F
D.F
D.F

Należy pamiętać, że istnieje możliwość wywołania ukrytej metody wirtualnej przez uzyskanie dostępu do wystąpienia D za pośrednictwem mniej pochodnego typu, w którym metoda nie jest ukryta.

przykład końcowy

15.6.5 Metody zastępowania

Gdy deklaracja metody wystąpienia zawiera override modyfikator, mówi się, że metoda jest metodą zastąpienia. Metoda zastąpienia zastępuje dziedziczona metoda wirtualna z tym samym podpisem. Podczas gdy deklaracja metody wirtualnej wprowadza nową metodę, deklaracja metody zastąpienia specjalizuje się w istniejącej odziedziczonej metodzie wirtualnej, zapewniając nową implementację tej metody.

Metoda zastępowana przez deklarację przesłonięcia jest znana jako metoda przesłonięć podstawowa Dla metody M przesłonięcia zadeklarowanej w klasie , metoda podstawy przesłaniania jest określana przez zbadanie każdej klasy bazowej Cklasy , począwszy od bezpośredniej klasy C bazowej i kontynuowanie z każdą kolejną bezpośrednią klasą bazową, dopóki w danej klasie Cbazowej nie znajduje się co najmniej jedna dostępna metoda, która ma ten sam podpis co M po podstawieniu argumentów typów. Na potrzeby lokalizowania zastąpionej metody bazowej metoda jest uważana za dostępną, jeśli jest publicto , jeśli jest protected, jeśli jest , lub protected internal, lub internalprivate protected jest zadeklarowana w tym samym programie co C.

Błąd czasu kompilacji występuje, chyba że wszystkie następujące elementy są prawdziwe dla deklaracji zastąpienia:

  • Zastąpiona metoda podstawowa może znajdować się zgodnie z powyższym opisem.
  • Istnieje dokładnie jedna taka przesłonięta metoda podstawowa. To ograniczenie ma wpływ tylko wtedy, gdy typ klasy bazowej jest typem skonstruowanym, w którym podstawienie argumentów typu sprawia, że podpis dwóch metod jest taki sam.
  • Zastępowana metoda podstawowa to metoda wirtualna, abstrakcyjna lub przesłonięć. Innymi słowy, zastępowana metoda podstawowa nie może być statyczna ani niewirtuatyczna.
  • Zastępowana metoda podstawowa nie jest metodą zapieczętowaną.
  • Istnieje konwersja tożsamości między zwracanym typem zastępowanej metody bazowej a metodą zastąpienia.
  • Przesłonięć deklarację i zastąpioną metodę bazową mają taką samą zadeklarowaną dostępność. Innymi słowy, deklaracja zastąpienia nie może zmienić dostępności metody wirtualnej. Jeśli jednak przesłoniętą metodę bazową jest chroniona wewnętrznie i jest zadeklarowana w innym zestawie niż zestaw zawierający deklarację zastąpienia, deklarowana dostępność deklaracji zastąpienia jest chroniona.
  • Deklaracja zastąpienia nie określa żadnych type_parameter_constraints_clauses. Zamiast tego ograniczenia są dziedziczone z zastępowanej metody bazowej. Ograniczenia, które są parametrami typu w metodzie przesłoniętej, mogą zostać zastąpione przez argumenty typu w odziedziczonym ograniczeniu. Może to prowadzić do ograniczeń, które nie są prawidłowe w przypadku jawnego określenia, takich jak typy wartości lub typy zapieczętowane.

Przykład: Poniżej pokazano, jak działają reguły zastępowania dla klas ogólnych:

abstract class C<T>
{
    public virtual T F() {...}
    public virtual C<T> G() {...}
    public virtual void H(C<T> x) {...}
}

class D : C<string>
{
    public override string F() {...}            // Ok
    public override C<string> G() {...}         // Ok
    public override void H(C<T> x) {...}        // Error, should be C<string>
}

class E<T,U> : C<U>
{
    public override U F() {...}                 // Ok
    public override C<U> G() {...}              // Ok
    public override void H(C<T> x) {...}        // Error, should be C<U>
}

przykład końcowy

Deklaracja przesłonięcia może uzyskać dostęp do zastępowanej metody podstawowej przy użyciu base_access (§12.8.15).

Przykład: w poniższym kodzie

class A
{
    int x;

    public virtual void PrintFields() => Console.WriteLine($"x = {x}");
}

class B : A
{
    int y;

    public override void PrintFields()
    {
        base.PrintFields();
        Console.WriteLine($"y = {y}");
    }
}

wywołanie w metodzie base.PrintFields()B wywołuje metodę PrintFields zadeklarowaną w pliku A. Base_access wyłącza mechanizm wywołania wirtualnego i po prostu traktuje metodę podstawową jako metodę innąvirtual niż. Gdyby wywołanie w elemecie zostało napisane B, rekursywnie wywołałoby ((A)this).PrintFields() metodę zadeklarowaną w , a nie zadeklarowanej w PrintFieldsBelemecie , ponieważ A jest wirtualna, a typ PrintFields czasu wykonywania to ((A)this).B

przykład końcowy

Tylko przez dołączenie override modyfikatora może zastąpić inną metodę. We wszystkich innych przypadkach metoda z tym samym podpisem co dziedziczona metoda po prostu ukrywa dziedziczona metodę.

Przykład: w poniższym kodzie

class A
{
    public virtual void F() {}
}

class B : A
{
    public virtual void F() {} // Warning, hiding inherited F()
}

F metoda w pliku B nie zawiera override modyfikatora i dlatego nie zastępuje metody w metodzie F .A Zamiast tego metoda w F elemecie ukrywa metodę w Belemecie , a ostrzeżenie jest zgłaszane, A ponieważ deklaracja nie zawiera nowego modyfikatora.

przykład końcowy

Przykład: w poniższym kodzie

class A
{
    public virtual void F() {}
}

class B : A
{
    private new void F() {} // Hides A.F within body of B
}

class C : B
{
    public override void F() {} // Ok, overrides A.F
}

metoda w metodzie FB ukrywa metodę wirtualną F dziedziczona z Aklasy . Ponieważ nowy F element w systemie B ma dostęp prywatny, jego zakres obejmuje tylko treść B klasy i nie rozszerza się na C. W związku z tym deklaracja F in C jest dozwolona, aby zastąpić dziedziczone z FA.

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 ma sealed modyfikator i metodę G , która nie. BUżycie sealed modyfikatora uniemożliwia C dalsze zastępowanie Felementu .

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 abstrakcyjna Paint , ponieważ nie ma znaczącej implementacji domyślnej. Klasy Ellipse i Box to konkretne Shape implementacje. Ponieważ te klasy nie są abstrakcyjne Paint , są one wymagane do zastąpienia metody i zapewnienia rzeczywistej implementacji.

przykład końcowy

Jest to błąd czasu kompilacji dla base_access (§12.8.15) w celu odwołania się do metody abstrakcyjnej.

Przykład: w poniższym kodzie

abstract class A
{
    public abstract void F();
}

class B : A
{
    // Error, base.F is abstract
    public override void F() => base.F();
}

zgłaszany jest błąd czasu kompilacji dla base.F() wywołania, ponieważ odwołuje się do metody abstrakcyjnej.

przykład końcowy

Deklaracja metody abstrakcyjnej może zastąpić metodę wirtualną. Dzięki temu klasa abstrakcyjna może wymusić ponowne wdrożenie metody w klasach pochodnych i sprawia, że oryginalna implementacja metody jest niedostępna.

Przykład: w poniższym kodzie

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

abstract class B: A
{
    public abstract override void F();
}

class C : B
{
    public override void F() => Console.WriteLine("C.F");
}

klasa A deklaruje metodę wirtualną, klasa B zastępuje tę metodę metodą abstrakcyjną, a klasa C zastępuje metodę abstrakcyjną w celu zapewnienia własnej implementacji.

przykład końcowy

15.6.8 Metody zewnętrzne

Gdy deklaracja metody zawiera extern modyfikator, metoda jest uważana za metodę zewnętrzną. Metody zewnętrzne są implementowane zewnętrznie, zazwyczaj przy użyciu języka innego niż C#. Ponieważ deklaracja metody zewnętrznej nie zawiera rzeczywistej implementacji, treść metody zewnętrznej po prostu składa się z średnika. Metoda zewnętrzna nie jest ogólna.

Mechanizm, za pomocą którego jest osiągane połączenie z metodą zewnętrzną, jest definiowany przez implementację.

Przykład: W poniższym przykładzie pokazano użycie extern modyfikatora i atrybutu DllImport :

class Path
{
    [DllImport("kernel32", SetLastError=true)]
    static extern bool CreateDirectory(string name, SecurityAttribute sa);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool RemoveDirectory(string name);

    [DllImport("kernel32", SetLastError=true)]
    static extern int GetCurrentDirectory(int bufSize, StringBuilder buf);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool SetCurrentDirectory(string name);
}

przykład końcowy

15.6.9 Metody częściowe

Gdy deklaracja metody zawiera partial modyfikator, ta metoda jest uważana za metodę częściową. Metody częściowe mogą być deklarowane tylko jako elementy członkowskie typów częściowych (§15.2.7) i podlegają wielu ograniczeniom.

Metody częściowe można zdefiniować w jednej części deklaracji typu i zaimplementować w innej. Implementacja jest opcjonalna; jeśli żadna część nie implementuje metody częściowej, deklaracja metody częściowej i wszystkie wywołania są usuwane z deklaracji typu wynikającej z kombinacji części.

Metody częściowe nie definiują modyfikatorów dostępu; są niejawnie prywatne. Ich zwracany typ to void, a ich parametry nie są parametrami wyjściowymi. Część identyfikatora jest rozpoznawana jako kontekstowe słowo kluczowe (§6.4.4) w deklaracji metody tylko wtedy, gdy pojawia się bezpośrednio przed void słowem kluczowym. Metoda częściowa nie może jawnie implementować metod interfejsu.

Istnieją dwa rodzaje deklaracji metody częściowej: jeśli treść deklaracji metody jest średnikiem, deklaracja jest deklarowana jako definiująca deklarację metody częściowej. Jeśli treść jest inna niż średnik, deklarację mówi się, że jest deklaracją częściową metody implementacji. W częściach deklaracji typu może istnieć tylko jedna deklaracja metody częściowej z danym podpisem i może istnieć tylko jedna implementacja częściowej deklaracji metody z danym podpisem. Jeżeli zostanie podana deklaracja częściowej metody wykonawczej, istnieje odpowiednia deklaracja metody częściowej, a deklaracje są zgodne z następującymi:

  • Deklaracje mają takie same modyfikatory (chociaż niekoniecznie w tej samej kolejności), nazwę metody, liczbę parametrów typu i liczbę parametrów.
  • Odpowiednie parametry w deklaracjach mają takie same modyfikatory (chociaż niekoniecznie w tej samej kolejności) i tego samego typu lub typy kabrioletów tożsamości (różnice modulo w nazwach parametrów typu).
  • Odpowiednie parametry typu w deklaracjach mają te same ograniczenia (modulo różnice w nazwach parametrów typu).

Implementacja deklaracji metody częściowej może pojawić się w tej samej części co odpowiednia deklaracja metody częściowej definiującej.

Tylko zdefiniowana metoda częściowa uczestniczy w rozpoznawaniu przeciążeń. W związku z tym, czy podano deklarację implementa, wyrażenia wywołania mogą być rozpoznawane jako wywołania metody częściowej. Ponieważ metoda częściowa zawsze zwraca voidwartość , takie wyrażenia wywołania zawsze będą instrukcjami wyrażeń. Ponadto, ponieważ metoda częściowa jest niejawnie private, takie instrukcje zawsze będą występować w jednej z części deklaracji typu, w której zadeklarowana jest metoda częściowa.

Uwaga: Definicja dopasowywania i implementowania deklaracji metody częściowej nie wymaga dopasowania nazw parametrów. Może to spowodować zaskakujące, choć dobrze zdefiniowane zachowanie podczas stosowania argumentów nazwanych (§12.6.2.1). Na przykład biorąc pod uwagę definiowanie deklaracji metody częściowej dla M w jednym pliku i implementowanie częściowej deklaracji metody w innym pliku:

// File P1.cs:
partial class P
{
    static partial void M(int x);
}

// File P2.cs:
partial class P
{
    static void Caller() => M(y: 0);
    static partial void M(int y) {}
}

jest nieprawidłowy , ponieważ wywołanie używa nazwy argumentu z implementacji, a nie definiującej częściowej deklaracji metody.

notatka końcowa

Jeśli żadna część częściowej deklaracji typu nie zawiera deklaracji implementowania dla danej metody częściowej, każda instrukcja wyrażenia wywołująca ją zostanie po prostu usunięta z deklaracji połączonego typu. W związku z tym wyrażenie wywołania, w tym wszelkie wyrażenia podrzędne, nie ma wpływu w czasie wykonywania. Sama metoda częściowa jest również usuwana i nie będzie elementem członkowskim połączonej deklaracji typu.

Jeśli deklaracja implementowania istnieje dla danej metody częściowej, wywołania metod częściowych są zachowywane. Metoda częściowa powoduje powstanie deklaracji metody podobnej do implementacji deklaracji częściowej metody z wyjątkiem następujących:

  • Modyfikator partial nie jest uwzględniony.

  • Atrybuty w wynikowej deklaracji metody są połączonymi atrybutami definiowania i implementowania częściowej deklaracji metody w nieokreślonej kolejności. Duplikaty nie są usuwane.

  • Atrybuty parametrów wynikowej deklaracji metody to połączone atrybuty odpowiednich parametrów definiowania i implementowania częściowej deklaracji metody w nieokreślonej kolejności. Duplikaty nie są usuwane.

Jeśli deklaracja definiująca, ale nie deklaracja implementowania jest podana dla metody Mczęściowej, obowiązują następujące ograniczenia:

  • Jest to błąd czasu kompilacji tworzenia delegata z (M§12.8.17.6).

  • Jest to błąd czasu kompilacji, aby odwoływać się do M wewnątrz funkcji anonimowej, która jest konwertowana na typ drzewa wyrażeń (§8.6).

  • Wyrażenia występujące w ramach wywołania M nie wpływają na określony stan przypisania (§9.4), co może potencjalnie prowadzić do błędów czasu kompilacji.

  • M nie może być punktem wejścia dla aplikacji (§7.1).

Metody częściowe są przydatne do umożliwienia jednej części deklaracji typu w celu dostosowania zachowania innej części, np. takiej, która jest generowana przez narzędzie. Rozważ następującą deklarację klasy częściowej:

partial class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    partial void OnNameChanging(string newName);
    partial void OnNameChanged();
}

Jeśli ta klasa zostanie skompilowana bez żadnych innych części, zdefiniowane deklaracje metody częściowej i ich wywołania zostaną usunięte, a wynikowa deklaracja klasy połączonej będzie równoważna następującym:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set => name = value;
    }
}

Załóżmy jednak, że podano inną część, która udostępnia deklaracje implementujące metody częściowe:

partial class Customer
{
    partial void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    partial void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

Następnie wynikowa deklaracja klasy połączonej będzie równoważna następującemu:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

15.6.10 Metody rozszerzenia

Gdy pierwszy parametr metody zawiera this modyfikator, ta metoda jest określana jako metoda rozszerzenia. Metody rozszerzenia są deklarowane tylko w niegenerycznych, nienagnieżdżonych klasach statycznych. Pierwszy parametr metody rozszerzenia jest ograniczony w następujący sposób:

  • Może to być parametr wejściowy tylko wtedy, gdy ma typ wartości
  • Może to być parametr referencyjny tylko wtedy, gdy ma typ wartości lub ma typ ogólny ograniczony do struktury
  • Nie jest to typ wskaźnika.

Przykład: Poniżej przedstawiono przykład klasy statycznej, która deklaruje dwie metody rozszerzenia:

public static class Extensions
{
    public static int ToInt32(this string s) => Int32.Parse(s);

    public static T[] Slice<T>(this T[] source, int index, int count)
    {
        if (index < 0 || count < 0 || source.Length - index < count)
        {
            throw new ArgumentException();
        }
        T[] result = new T[count];
        Array.Copy(source, index, result, 0, count);
        return result;
    }
}

przykład końcowy

Metoda rozszerzenia jest regularną metodą statyczną. Ponadto, gdy otaczana klasa statyczna znajduje się w zakresie, metoda rozszerzenia może być wywoływana przy użyciu składni wywołania metody wystąpienia (§12.8.10.3), używając wyrażenia odbiornika jako pierwszego argumentu.

Przykład: Poniższy program używa metod rozszerzeń zadeklarowanych powyżej:

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in strings.Slice(1, 2))
        {
            Console.WriteLine(s.ToInt32());
        }
    }
}

Metoda Slice jest dostępna w metodzie string[], a ToInt32 metoda jest dostępna w metodzie string, ponieważ zostały one zadeklarowane jako metody rozszerzenia. Znaczenie programu jest takie samo jak poniżej, używając zwykłych wywołań metod statycznych:

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in Extensions.Slice(strings, 1, 2))
        {
            Console.WriteLine(Extensions.ToInt32(s));
        }
    }
}

przykład końcowy

Treść metody 15.6.11

Treść metody deklaracji metody składa się z treści bloku, treści wyrażenia lub średnika.

Deklaracje metody abstrakcyjnej i zewnętrznej nie zapewniają implementacji metody, więc ich jednostki metody po prostu składają się ze średnika. W przypadku każdej innej metody treść metody jest blokiem (§13.3), który zawiera instrukcje do wykonania po wywołaniu tej metody.

Efektywny typ zwracany metody to, jeśli zwracany typ to voidvoid, lub jeśli metoda jest asynchronizna, a zwracany typ to «TaskType» (§15.15.1). W przeciwnym razie efektywny typ zwracany metody innej niż asynchroniczny jest typem zwracanym, a efektywnym typem zwracanym metody asynchronicznej z typem «TaskType»<T>zwrotnym (§15.15.1) jest T.

Jeżeli skuteczny typ zwracany metody jest void i metoda ma treść bloku, return instrukcje (§13.10.5) w bloku nie określają wyrażenia. Jeśli wykonanie bloku metody void zakończy się normalnie (tj. kontrolka przepływa poza koniec treści metody), ta metoda po prostu powróci do obiektu wywołującego.

Gdy skuteczny typ zwracany metody jest void i metoda ma treść wyrażenia, wyrażenie E jest statement_expression, a treść jest dokładnie równoważna treści bloku formularza { E; }.

W przypadku metody zwracanej po wartości (§15.6.1) każda instrukcja zwracana w treści tej metody określa wyrażenie niejawnie konwertowane na efektywny typ zwracany.

W przypadku metody return-by-ref (§15.6.1) każda instrukcja zwracana w treści tej metody określa wyrażenie, którego typem jest skuteczny typ zwracany, i ma kontekst ref-safe-context kontekstu wywołującego (§9.7.2).

W przypadku metod zwracanych według wartości i zwracanych przez ref punkt końcowy treści metody nie jest osiągalny. Innymi słowy, kontrolka nie może przepływać poza koniec treści metody.

Przykład: w poniższym kodzie

class A
{
    public int F() {} // Error, return value required

    public int G()
    {
        return 1;
    }

    public int H(bool b)
    {
        if (b)
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

    public int I(bool b) => b ? 1 : 0;
}

metoda zwracana F przez wartość powoduje błąd czasu kompilacji, ponieważ kontrolka może przepływać poza koniec treści metody. Metody G i H są poprawne, ponieważ wszystkie możliwe ścieżki wykonywania kończą się instrukcją return, która określa wartość zwracaną. Metoda I jest poprawna, ponieważ jej treść jest równoważna blokowi z tylko jedną instrukcją return w niej.

przykład końcowy

Właściwości 15.7

15.7.1 Ogólne

Właściwość jest elementem członkowskim, który zapewnia dostęp do właściwości obiektu lub klasy. Przykłady właściwości obejmują długość ciągu, rozmiar czcionki, podpis okna i nazwę klienta. Właściwości są naturalnym rozszerzeniem pól — oba są nazwanymi elementami członkowskimi skojarzonymi typami, a składnia uzyskiwania dostępu do pól i właściwości jest taka sama. Jednak w przeciwieństwie do pól właściwości nie oznaczają lokalizacji przechowywania. Zamiast tego właściwości mają metody dostępu określające instrukcje , które mają być wykonywane, gdy ich wartości są odczytywane lub zapisywane. Właściwości zapewniają zatem mechanizm kojarzenia akcji z odczytem i zapisem cech obiektu lub klasy; ponadto pozwalają one na obliczanie takich cech.

Właściwości są deklarowane przy użyciu property_declarations:

property_declaration
    : attributes? property_modifier* type member_name property_body
    | attributes? property_modifier* ref_kind type member_name ref_property_body
    ;    

property_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;
    
property_body
    : '{' accessor_declarations '}' property_initializer?
    | '=>' expression ';'
    ;

property_initializer
    : '=' variable_initializer ';'
    ;

ref_property_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).

Istnieją dwa rodzaje property_declaration:

  • Pierwszy deklaruje właściwość inną niż ref-valued. Jego wartość ma typ typu. Tego rodzaju właściwość może być czytelna i/lub zapisywalna.
  • Drugi deklaruje właściwość ref-valued. Jego wartość to variable_reference (§9.5readonly Tego rodzaju właściwość jest dostępna tylko do odczytu.

Property_declaration może zawierać zestaw atrybutów(§22) i dowolny z dozwolonych rodzajów zadeklarowanych ułatwień dostępu (§15.3.6),new (§15.3.5), (§15.7.2virtual, §15.7.6), override (§15.6.5, §15.7.6), sealed (§15.6.6), abstract (§15.6.7, §15.7.6) i extern (§15.6.8) modyfikatory.

Deklaracje właściwości podlegają tym samym regułom co deklaracje metod (§15.6) w odniesieniu do prawidłowych kombinacji modyfikatorów.

Member_name (§15.6.1) określa nazwę właściwości. Jeśli właściwość nie jest jawną implementacją składową interfejsu, member_name jest po prostu identyfikatorem. W przypadku jawnej implementacji składowej interfejsu (§18.6.2) member_name składa się z interface_type , po którym następuje "." i identyfikator.

Typ nieruchomości jest co najmniej tak dostępny, jak sama właściwość (§7.5.5).

Property_body może składać się z treści instrukcji lub treści wyrażenia. W treści instrukcji, accessor_declarations, które są ujęte w{ "" i "}" tokeny, deklarują metody dostępu (§15.7.3) właściwości. Metody dostępu określają instrukcje wykonywalne skojarzone z odczytywaniem i zapisywaniem właściwości.

W property_body treści wyrażeniaE, a średnik jest dokładnie odpowiednikiem treści { get { return E; } }instrukcji i dlatego może służyć tylko do określania właściwości tylko do odczytu, gdy wynik metody get jest podawany przez pojedyncze wyrażenie.

Property_initializer można podać tylko dla właściwości zaimplementowanej automatycznie (§15.7.4) i powoduje zainicjowanie pola bazowego takich właściwości z wartością podaną przez wyrażenie.

Ref_property_body może składać się z treści instrukcji lub treści wyrażenia. W treści instrukcji get_accessor_declaration deklaruje akcesorium get (§15.7.3) właściwości. Metodę dostępu określa instrukcje wykonywalne skojarzone z odczytywaniem właściwości.

W ref_property_body treści wyrażenia składającej => się z elementu ref, variable_referenceV i średnik jest dokładnie odpowiednikiem treści { get { return ref V; } }instrukcji .

Uwaga: Mimo że składnia uzyskiwania dostępu do właściwości jest taka sama jak w przypadku pola, właściwość nie jest klasyfikowana jako zmienna. W związku z tym nie można przekazać właściwości jako in, outlub ref argumentu, chyba że właściwość jest wartością ref i dlatego zwraca odwołanie do zmiennej (§9.7). notatka końcowa

Gdy deklaracja właściwości zawiera extern modyfikator, właściwość jest uważana za właściwość zewnętrzną. Ze względu na to, że deklaracja właściwości zewnętrznej nie zapewnia rzeczywistej implementacji, każdy z accessor_bodyw jego accessor_declarations jest średnikiem.

15.7.2 Właściwości statyczne i wystąpienia

Gdy deklaracja właściwości zawiera static modyfikator, właściwość jest uważana za właściwość statyczną. Jeśli modyfikator nie static istnieje, właściwość jest uważana za właściwość wystąpienia.

Właściwość statyczna nie jest skojarzona z określonym wystąpieniem i jest to błąd czasu kompilacji, do których należy odwołać się this w metodzie dostępu właściwości statycznej.

Właściwość wystąpienia jest skojarzona z danym wystąpieniem klasy i można uzyskać do tego wystąpienia dostęp jako this (§12.8.14) w metodach dostępu tej właściwości.

Różnice między elementami statycznymi i elementami członkowskimi wystąpień zostały omówione w temacie §15.3.8.

15.7.3 Akcesoria

Uwaga: Ta klauzula dotyczy obu właściwości (§15.7) i indeksatorów (§15.9). Klauzula jest zapisywana pod względem właściwości, podczas odczytywania indeksatorów zastąp indeksator/indeksatory właściwości/właściwości i zapoznaj się z listą różnic między właściwościami i indeksatorami podanymi w §15.9.2. notatka końcowa

Accessor_declarations właściwości określają instrukcje wykonywalne skojarzone z zapisem i/lub odczytywaniem tej właściwości.

accessor_declarations
    : get_accessor_declaration set_accessor_declaration?
    | set_accessor_declaration get_accessor_declaration?
    ;

get_accessor_declaration
    : attributes? accessor_modifier? 'get' accessor_body
    ;

set_accessor_declaration
    : attributes? accessor_modifier? 'set' accessor_body
    ;

accessor_modifier
    : 'protected'
    | 'internal'
    | 'private'
    | 'protected' 'internal'
    | 'internal' 'protected'
    | 'protected' 'private'
    | 'private' 'protected'
    ;

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

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

Accessor_declarations składają się z get_accessor_declaration, set_accessor_declaration lub obu tych elementów. Każda deklaracja dostępu składa się z opcjonalnych atrybutów, opcjonalnych accessor_modifier, tokenu get lub set, a następnie accessor_body.

W przypadku właściwości ref valued ref_get_accessor_declaration składa się z opcjonalnych atrybutów, opcjonalnego accessor_modifier tokenu get, a następnie ref_accessor_body.

Korzystanie z accessor_modifiers podlega następującym ograniczeniom:

  • Accessor_modifier nie należy używać w interfejsie ani w jawnej implementacji składowej interfejsu.
  • W przypadku właściwości lub indeksatora, który nie override ma modyfikatora, accessor_modifier jest dozwolony tylko wtedy, gdy właściwość lub indeksator ma zarówno metodę get, jak i set, a następnie jest dozwolona tylko na jednym z tych metod dostępu.
  • Dla właściwości lub indeksatora, który zawiera override modyfikator, akcesorium musi odpowiadać accessor_modifier, jeśli w ogóle, akcesorium jest zastępowane.
  • Accessor_modifier deklaruje dostępność, która jest ściśle bardziej restrykcyjna niż deklarowana dostępność samej właściwości lub indeksatora. Aby być precyzyjnym:
    • Jeśli właściwość lub indeksator ma zadeklarowaną dostępność , ułatwienia dostępu publiczadeklarowane przez accessor_modifier mogą być albo private protected, , protected internal, internal, protectedlub private.
    • Jeśli właściwość lub indeksator ma zadeklarowaną dostępność , ułatwienia dostępu protected internalzadeklarowane przez accessor_modifier mogą być albo private protected, , protected private, internal, protectedlub private.
    • Jeśli właściwość lub indeksator ma zadeklarowaną dostępność lub internal, dostępność protected zadeklarowana przez accessor_modifier musi być albo private protected lub private.
    • Jeśli właściwość lub indeksator ma zadeklarowaną dostępność , ułatwienia dostępu private protectedzadeklarowane przez accessor_modifier to private.
    • Jeśli właściwość lub indeksator ma zadeklarowaną dostępność private, nie można użyć accessor_modifier .

W przypadku abstract właściwości innych extern niż ref każda accessor_body dla każdego określonego metody dostępu jest po prostu średnikiem. Nie abstrakcyjna, nie-extern właściwość, ale nie indeksator, może również mieć accessor_body dla wszystkich metod dostępu określonych średnikiem, w tym przypadku jest to automatycznie zaimplementowana właściwość (§15.7.4). Właściwość zaimplementowana automatycznie musi mieć co najmniej metodę dostępu. W przypadku metod dostępu innych właściwości innych niż abstrakcyjne, inne niż extern, accessor_body jest albo:

  • blok określający instrukcje, które mają być wykonywane po wywołaniu odpowiedniego metody dostępu; lub
  • treść wyrażenia, która składa się z następującego =>po nim wyrażenia i średnika, i określa pojedyncze wyrażenie do wykonania po wywołaniu odpowiedniego metody dostępu.

W przypadku abstract właściwości i extern wartości ref ref_accessor_body jest po prostu średnikiem. W przypadku metody dostępu do jakiejkolwiek innej właściwości niesstrakcyjnej, innej niż extern, ref_accessor_body jest albo:

  • blok określający instrukcje, które mają być wykonywane po wywołaniu metody get dostępu; lub
  • treść wyrażenia, która składa się z następujących =>elementówref: , variable_reference i średnik. Odwołanie do zmiennej jest oceniane po wywołaniu metody uzyskiwania dostępu.

Metoda get get dla właściwości innej niż ref odpowiada metodzie bez parametrów z zwracaną wartością typu właściwości. Z wyjątkiem elementu docelowego przypisania, gdy taka właściwość jest przywoływana w wyrażeniu, jego metodę get metody dostępu jest wywoływana w celu obliczenia wartości właściwości (§12.2.2).

Treść metody get accessor dla właściwości innej niż ref jest zgodna z zasadami metod zwracania wartości opisanych w §15.6.11. W szczególności wszystkie return instrukcje w treści metody get accessor określają wyrażenie niejawnie konwertowane na typ właściwości. Ponadto punkt końcowy metody uzyskiwania dostępu nie jest osiągalny.

Metoda get get dla właściwości ref-valued odpowiada metodzie bez parametrów z zwracaną wartością variable_reference do zmiennej typu właściwości. Gdy taka właściwość jest przywoływana w wyrażeniu, jego metodę get jest wywoływana w celu obliczenia wartości variable_reference właściwości. Odwołanie do tej zmiennej, podobnie jak inne, jest następnie używane do odczytu lub, w przypadku nieczytanych variable_references, zapisuj przywołyną zmienną zgodnie z wymaganiami kontekstu.

Przykład: Poniższy przykład ilustruje właściwość ref-valued jako element docelowy przypisania:

class Program
{
    static int field;
    static ref int Property => ref field;

    static void Main()
    {
        field = 10;
        Console.WriteLine(Property); // Prints 10
        Property = 20;               // This invokes the get accessor, then assigns
                                     // via the resulting variable reference
        Console.WriteLine(field);    // Prints 20
    }
}

przykład końcowy

Treść metody get dla właściwości o wartości ref jest zgodna z zasadami metod o wartości ref opisanej w §15.6.11.

Metoda dostępu zestawu odpowiada metodzie z pojedynczym parametrem wartości typu właściwości i typem zwracanym void . Niejawny parametr metody dostępu zestawu ma zawsze nazwę value. Gdy właściwość jest przywoływana jako element docelowy przypisania (§12.21) lub jako operand lub ++ (§12.8.16, –-), zestaw metod dostępu jest wywoływany z argumentem, który dostarcza nową wartość (§12.21.2). Treść zestawu dostępu jest zgodna z zasadami void metod opisanymi w §15.6.11. W szczególności instrukcje return w treści zestawu akcesoriów nie mogą określać wyrażenia. Ponieważ zestaw metod dostępu niejawnie ma parametr o nazwie value, jest to błąd czasu kompilacji dla zmiennej lokalnej lub deklaracji stałej w zestawie dostępu, aby mieć tę nazwę.

Na podstawie obecności lub braku metod dostępu get i set, właściwość jest klasyfikowana w następujący sposób:

  • Właściwość, która zawiera zarówno metodę dostępu get, jak i zestaw akcesoriów jest mówi się, że jest właściwością read-write.
  • Właściwość, która ma tylko metodę get, mówi się, że jest właściwością tylko do odczytu. Jest to błąd czasu kompilacji dla właściwości tylko do odczytu, która ma być elementem docelowym przypisania.
  • Właściwość, która ma tylko zestaw akcesoriów, mówi się, że jest właściwością tylko do zapisu. Z wyjątkiem wartości docelowej przypisania, jest to błąd czasu kompilacji, aby odwołać się do właściwości tylko do zapisu w wyrażeniu.

Uwaga: Operatory pre-i postfiks ++ i -- operatory i operatory przypisania złożonego nie mogą być stosowane do właściwości tylko do zapisu, ponieważ te operatory odczytują starą wartość operandu przed zapisem nowego. notatka końcowa

Przykład: w poniższym kodzie

public class Button : Control
{
    private string caption;

    public string Caption
    {
        get => caption;
        set
        {
            if (caption != value)
            {
                caption = value;
                Repaint();
            }
        }
    }

    public override void Paint(Graphics g, Rectangle r)
    {
        // Painting code goes here
    }
}

kontrolka Button deklaruje właściwość publiczną Caption . Metoda get dostępu właściwości Caption zwraca string wartość przechowywaną w polu prywatnym caption . Zestaw metod dostępu sprawdza, czy nowa wartość różni się od bieżącej wartości, a jeśli tak, przechowuje nową wartość i przemaluje kontrolkę. Właściwości często są zgodne ze wzorcem private pokazanym powyżej: Metoda pobierania po prostu zwraca wartość przechowywaną w polu, a zestaw metod dostępu modyfikuje to private pole, a następnie wykonuje wszelkie dodatkowe akcje wymagane do zaktualizowania stanu obiektu. Button Biorąc pod uwagę powyżej klasę, poniżej przedstawiono przykład użycia Caption właściwości :

Button okButton = new Button();
okButton.Caption = "OK"; // Invokes set accessor
string s = okButton.Caption; // Invokes get accessor

W tym miejscu metodę dostępu zestawu jest wywoływana przez przypisanie wartości do właściwości, a metodę get jest wywoływana przez odwoływanie się do właściwości w wyrażeniu.

przykład końcowy

Metody pobierania i ustawiania właściwości nie są odrębnymi elementami członkowskimi i nie można oddzielnie zadeklarować akcesoriów właściwości.

Przykład: przykład

class A
{
    private string name;

    // Error, duplicate member name
    public string Name
    { 
        get => name;
    }

    // Error, duplicate member name
    public string Name
    { 
        set => name = value;
    }
}

nie deklaruje pojedynczej właściwości odczytu i zapisu. Zamiast tego deklaruje dwie właściwości o tej samej nazwie, jeden tylko do odczytu i jeden tylko do zapisu. Ponieważ dwa elementy członkowskie zadeklarowane w tej samej klasie nie mogą mieć tej samej nazwy, przykład powoduje wystąpienie błędu czasu kompilacji.

przykład końcowy

Gdy klasa pochodna deklaruje właściwość o tej samej nazwie co dziedziczona właściwość, właściwość pochodna ukrywa dziedziczona właściwość w odniesieniu zarówno do odczytu, jak i zapisu.

Przykład: w poniższym kodzie

class A
{
    public int P
    {
        set {...}
    }
}

class B : A
{
    public new int P
    {
        get {...}
    }
}

P właściwość w obiekcie B ukrywa P właściwość w A odniesieniu zarówno do czytania, jak i zapisu. W ten sposób w instrukcjach

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

przypisanie powoduje b.P zgłaszanie błędu czasu kompilacji, ponieważ właściwość tylko do odczytu ukrywa P właściwość tylko BP do zapisu w pliku A. Należy jednak pamiętać, że rzutowanie może służyć do uzyskiwania dostępu do ukrytej P właściwości.

przykład końcowy

W przeciwieństwie do pól publicznych właściwości zapewniają separację między stanem wewnętrznym obiektu a jego interfejsem publicznym.

Przykład: Rozważmy następujący kod, który używa Point struktury do reprezentowania lokalizacji:

class Label
{
    private int x, y;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.x = x;
        this.y = y;
        this.caption = caption;
    }

    public int X => x;
    public int Y => y;
    public Point Location => new Point(x, y);
    public string Caption => caption;
}

W tym Label miejscu klasa używa dwóch int pól x i y, aby przechowywać swoją lokalizację. Lokalizacja jest publicznie uwidoczniona zarówno jako właściwość, jak X i i jako Y właściwość typu LocationPoint . Jeśli w przyszłej Labelwersji programu stanie się wygodniejsze przechowywanie lokalizacji Point wewnętrznie, można wprowadzić zmianę bez wpływu na publiczny interfejs klasy:

class Label
{
    private Point location;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.location = new Point(x, y);
        this.caption = caption;
    }

    public int X => location.X;
    public int Y => location.Y;
    public Point Location => location;
    public string Caption => caption;
}

Gdyby x pola i y zamiast public readonly tego były polami, byłoby niemożliwe wprowadzenie takiej zmiany w Label klasie.

przykład końcowy

Uwaga: uwidacznianie stanu za pomocą właściwości niekoniecznie jest mniej wydajne niż bezpośrednie uwidacznianie pól. W szczególności, gdy właściwość jest niewirtualna i zawiera tylko niewielką ilość kodu, środowisko wykonawcze może zastąpić wywołania metod dostępu rzeczywistym kodem akcesoriów. Ten proces jest znany jako podkreślenie i sprawia, że dostęp do właściwości jest tak wydajny, jak dostęp do pól, ale zachowuje zwiększoną elastyczność właściwości. notatka końcowa

Przykład: Ponieważ wywoływanie metody uzyskiwania dostępu jest koncepcyjnie równoważne z odczytywaniem wartości pola, jest uważany za zły styl programowania, aby uzyskać metody dostępu do obserwowanych skutków ubocznych. W przykładzie

class Counter
{
    private int next;

    public int Next => next++;
}

wartość Next właściwości zależy od liczby przypadków, w których wcześniej uzyskiwano dostęp do właściwości. W związku z tym uzyskanie dostępu do właściwości powoduje zauważalny efekt uboczny, a właściwość powinna zostać zaimplementowana jako metoda.

Konwencja "brak skutków ubocznych" dla metod dostępu nie oznacza, że uzyskiwanie akcesoriów powinno być zawsze zapisywane po prostu w celu zwracania wartości przechowywanych w polach. W rzeczywistości metody uzyskiwania dostępu często obliczają wartość właściwości przez uzyskanie dostępu do wielu pól lub wywoływanie metod. Jednak prawidłowo zaprojektowane metody uzyskiwania dostępu nie wykonują żadnych akcji, które powodują zauważalne zmiany w stanie obiektu.

przykład końcowy

Właściwości mogą służyć do opóźniania inicjowania zasobu do momentu pierwszego przywołowania.

Przykład:

public class Console
{
    private static TextReader reader;
    private static TextWriter writer;
    private static TextWriter error;

    public static TextReader In
    {
        get
        {
            if (reader == null)
            {
                reader = new StreamReader(Console.OpenStandardInput());
            }
            return reader;
        }
    }

    public static TextWriter Out
    {
        get
        {
            if (writer == null)
            {
                writer = new StreamWriter(Console.OpenStandardOutput());
            }
            return writer;
        }
    }

    public static TextWriter Error
    {
        get
        {
            if (error == null)
            {
                error = new StreamWriter(Console.OpenStandardError());
            }
            return error;
        }
    }
...
}

Klasa Console zawiera trzy właściwości, In, Outi Error, 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ływania Out się do właściwości , jak w

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

tworzony jest element bazowy TextWriter dla urządzenia wyjściowego. Jeśli jednak aplikacja nie odwołuje się do In właściwości i Error , dla tych urządzeń nie są tworzone żadne obiekty.

przykład końcowy

15.7.4 Automatycznie zaimplementowane właściwości

Automatycznie zaimplementowana właściwość (lub właściwość automatyczna w skrócie) jest nie abstrakcyjną, inną niż extern, niekorzystaną z właściwością niekorzystaną z właściwością tylko średnikami accessor_bodys. Właściwości automatyczne mają dostęp do metody dostępu i mogą opcjonalnie mieć zestaw akcesoriów.

Gdy właściwość jest określona jako automatycznie zaimplementowana właściwość, ukryte pole zapasowe jest automatycznie dostępne dla właściwości, a metody dostępu są implementowane do odczytu i zapisu w tym polu pomocniczym. Ukryte pole zapasowe jest niedostępne, można je odczytywać i zapisywać tylko za pośrednictwem automatycznie zaimplementowanych metod dostępu do właściwości, nawet w obrębie typu zawierającego. Jeśli właściwość automatyczna nie ma ustawionego dostępu, pole zapasowe jest uznawane za readonly (§15.5.3). Podobnie jak readonly w przypadku pola, właściwość automatyczna tylko do odczytu może być również przypisana w treści konstruktora otaczającej klasy. Takie przypisanie przypisuje bezpośrednio do pola kopii zapasowej tylko do odczytu właściwości.

Właściwość automatyczna może opcjonalnie mieć property_initializer, która jest stosowana bezpośrednio do pola zapasowego jako variable_initializer (§17.7).

Przykład:

public class Point
{
    public int X { get; set; } // Automatically implemented
    public int Y { get; set; } // Automatically implemented
}

jest odpowiednikiem następującej deklaracji:

public class Point
{
    private int x;
    private int y;

    public int X { get { return x; } set { x = value; } }
    public int Y { get { return y; } set { y = value; } }
}

przykład końcowy

Przykład: w następujących

public class ReadOnlyPoint
{
    public int X { get; }
    public int Y { get; }

    public ReadOnlyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

jest odpowiednikiem następującej deklaracji:

public class ReadOnlyPoint
{
    private readonly int __x;
    private readonly int __y;
    public int X { get { return __x; } }
    public int Y { get { return __y; } }

    public ReadOnlyPoint(int x, int y)
    {
        __x = x;
        __y = y;
    }
}

Przypisania do pola tylko do odczytu są prawidłowe, ponieważ występują w konstruktorze.

przykład końcowy

Mimo że pole zapasowe jest ukryte, to pole może mieć atrybuty docelowe pola stosowane bezpośrednio do niego za pośrednictwem automatycznie zaimplementowanej właściwości property_declaration (§15.7.1).

Przykład: następujący kod

[Serializable]
public class Foo
{
    [field: NonSerialized]
    public string MySecret { get; set; }
}

powoduje zastosowanie atrybutu NonSerialized docelowego pola do pola generowanego przez kompilator, tak jakby kod został napisany w następujący sposób:

[Serializable]
public class Foo
{
    [NonSerialized]
    private string _mySecretBackingField;
    public string MySecret
    {
        get { return _mySecretBackingField; }
        set { _mySecretBackingField = value; }
    }
}

przykład końcowy

15.7.5 Ułatwienia dostępu

Jeśli akcesorium ma accessor_modifier, domena ułatwień dostępu (§7.5.3) metody dostępu jest określana przy użyciu zadeklarowanej dostępności accessor_modifier. Jeśli akcesorium nie ma accessor_modifier, domena ułatwień dostępu dostępu jest określana na podstawie zadeklarowanej dostępności właściwości lub indeksatora.

Obecność accessor_modifier nigdy nie wpływa na wyszukiwanie składowych (§12.5) lub rozpoznawanie przeciążenia (§12.6.4). Modyfikatory właściwości lub indeksatora zawsze określają, z którą właściwością lub indeksatorem jest powiązana, niezależnie od kontekstu dostępu.

Po wybraniu określonej właściwości innej niż ref lub indeksatora innego niż ref- wartość, domeny ułatwień dostępu określonych metod dostępu są używane do określenia, czy użycie jest prawidłowe:

  • Jeśli użycie jest wartością (§12.2.2), dostęp do pobierania musi istnieć i być dostępny.
  • Jeśli użycie jest celem prostego przypisania (§12.21.2), akcesorium zestawu musi istnieć i być dostępne.
  • Jeśli użycie jest celem przypisania złożonego (§12.21.4) lub jako cel ++ operatorów lub -- (§12.8.16, §12.9.6), zarówno metody dostępu do pobrania, jak i zestawu akcesoriów muszą istnieć i być dostępne.

Przykład: W poniższym przykładzie właściwość jest ukryta przez właściwość A.TextB.Text, nawet w kontekstach, w których wywoływana jest tylko funkcja dostępu zestawu. Natomiast właściwość B.Count nie jest dostępna dla klasy M, więc zamiast tego jest używana właściwość A.Count dostępna.

class A
{
    public string Text
    {
        get => "hello";
        set { }
    }

    public int Count
    {
        get => 5;
        set { }
    }
}

class B : A
{
    private string text = "goodbye";
    private int count = 0;

    public new string Text
    {
        get => text;
        protected set => text = value;
    }

    protected new int Count
    {
        get => count;
        set => count = value;
    }
}

class M
{
    static void Main()
    {
        B b = new B();
        b.Count = 12;       // Calls A.Count set accessor
        int i = b.Count;    // Calls A.Count get accessor
        b.Text = "howdy";   // Error, B.Text set accessor not accessible
        string s = b.Text;  // Calls B.Text get accessor
    }
}

przykład końcowy

Po wybraniu określonej właściwości ref-valued lub indeksatora o wartości ref; czy użycie jest wartością, celem prostego przypisania, czy celem przypisania złożonego; domena ułatwień dostępu zaangażowanych metod dostępu służy do określania, czy użycie jest prawidłowe.

Akcesorium używane do implementowania interfejsu nie ma accessor_modifier. Jeśli do zaimplementowania interfejsu jest używany tylko jeden akcesor dostępu, inne metody dostępu mogą być zadeklarowane za pomocą accessor_modifier:

Przykład:

public interface I
{
    string Prop { get; }
}

public class C : I
{
    public string Prop
    {
        get => "April";     // Must not have a modifier here
        internal set {...}  // Ok, because I.Prop has no set accessor
    }
}

przykład końcowy

15.7.6 Wirtualne, zapieczętowane, zastępowane i abstrakcyjne metody dostępu

Uwaga: Ta klauzula dotyczy obu właściwości (§15.7) i indeksatorów (§15.9). Klauzula jest zapisywana pod względem właściwości, podczas odczytywania indeksatorów zastąp indeksator/indeksatory właściwości/właściwości i zapoznaj się z listą różnic między właściwościami i indeksatorami podanymi w §15.9.2. notatka końcowa

Deklaracja właściwości wirtualnej określa, że metody dostępu właściwości są wirtualne. Modyfikator virtual ma zastosowanie do wszystkich metod dostępu innych niż prywatne właściwości. Gdy akcesorium właściwości wirtualnej ma privateaccessor_modifier, dostęp prywatny nie jest niejawnie wirtualny.

Deklaracja właściwości abstrakcyjnej określa, że metody dostępu właściwości są wirtualne, ale nie zapewnia rzeczywistej implementacji metod dostępu. Zamiast tego klasy pochodne nie abstrakcyjne są wymagane do zapewnienia własnej implementacji dla metod dostępu przez zastąpienie właściwości. Ponieważ metoda dostępu do deklaracji właściwości abstrakcyjnej nie zapewnia rzeczywistej implementacji, jej accessor_body po prostu składa się z średnika. Właściwość abstrakcyjna nie ma private metody dostępu.

Deklaracja właściwości, która zawiera zarówno abstract modyfikatory , jak i override określa, że właściwość jest abstrakcyjna i zastępuje właściwość podstawową. Metody dostępu takiej właściwości są również abstrakcyjne.

Deklaracje właściwości abstrakcyjnych są dozwolone tylko w klasach abstrakcyjnych (§15.2.2.2). Metody dostępu dziedziczonej właściwości wirtualnej można zastąpić w klasie pochodnej, dołączając deklarację właściwości, która określa dyrektywę override . Jest to nazywane zastępowaniem deklaracji właściwości. Zastępowanie deklaracji właściwości nie deklaruje nowej właściwości. Zamiast tego po prostu specjalizuje się w implementacjach metod dostępu istniejącej właściwości wirtualnej.

Aby mieć taką samą zadeklarowaną dostępność, wymagana jest deklaracja zastąpienia i zastąpiona właściwość podstawowa. Innymi słowy, deklaracja zastąpienia nie zmienia dostępności właściwości podstawowej. Jeśli jednak przesłoniętą właściwość podstawową jest chroniona wewnętrznych i jest zadeklarowana w innym zestawie niż zestaw zawierający deklarację zastąpienia, deklarowana dostępność deklaracji zastąpienia jest chroniona. Jeśli właściwość dziedziczona ma tylko jedną metodę dostępu (tj. jeśli dziedziczona właściwość jest tylko do odczytu lub tylko do zapisu), właściwość zastępowania obejmuje tylko to akcesorium. Jeśli dziedziczona właściwość zawiera obie metody dostępu (tj. jeśli dziedziczona właściwość jest odczyt-zapis), właściwość zastępowania może zawierać jedno akcesorium lub oba metody dostępu. Istnieje konwersja tożsamości między typem zastępowania i dziedziczonej właściwości.

Zastępowanie deklaracji właściwości może zawierać sealed modyfikator. Użycie tego modyfikatora uniemożliwia dalsze zastępowanie właściwości przez klasę pochodną. Akcesoria zapieczętowanej właściwości są również zapieczętowane.

Z wyjątkiem różnic w składni deklaracji i wywołania, wirtualnych, zapieczętowanych, przesłonięć i abstrakcyjnych metod zachowuje się dokładnie tak jak metody wirtualne, zapieczętowane, zastępowane i abstrakcyjne. W szczególności zasady opisane w §15.6.4, §15.6.5, §15.6.6 i §15.6.7 mają zastosowanie tak, jakby metody dostępu były odpowiednimi formami:

  • Metoda get get odpowiada metodzie bez parametrów z wartością zwracaną typu właściwości i tymi samymi modyfikatorami, co właściwość zawierająca.
  • Metoda dostępu zestawu odpowiada metodzie z pojedynczym parametrem wartości typu właściwości, typem zwracanym void i tymi samymi modyfikatorami, co właściwość zawierająca.

Przykład: w poniższym kodzie

abstract class A
{
    int y;

    public virtual int X
    {
        get => 0;
    }

    public virtual int Y
    {
        get => y;
        set => y = value;
    }

    public abstract int Z { get; set; }
}

X jest wirtualną właściwością tylko do odczytu, Y jest wirtualną właściwością odczytu i zapisu i Z jest abstrakcyjną właściwością read-write. Ponieważ Z jest abstrakcyjna, zawierająca klasę A również jest zadeklarowana abstrakcyjnie.

Poniżej przedstawiono klasę pochodzącą z A klasy:

class B : A
{
    int z;

    public override int X
    {
        get => base.X + 1;
    }

    public override int Y
    {
        set => base.Y = value < 0 ? 0: value;
    }

    public override int Z
    {
        get => z;
        set => z = value;
    }
}

W tym miejscu deklaracje X, Yi Z zastępują deklaracje właściwości. Każda deklaracja właściwości dokładnie odpowiada modyfikatorom ułatwień dostępu, typowi i nazwie odpowiadającej jej właściwości dziedziczonej. Get accessor of X i set accessor of use the base keyword to access the inherited accessors (Uzyskiwanie dostępu do metody dostępu) i set accessor of Y use the base keyword to access the inherited accessors (Uzyskiwanie dostępu do odziedziczonych metod dostępu). Deklaracja Z przesłonięć zarówno abstrakcyjnych metod dostępu — w związku z tym nie ma żadnych wybitnych abstract składowych funkcji w Bsystemie i B może być klasą nie abstrakcyjną.

przykład końcowy

Gdy właściwość jest zadeklarowana jako przesłonięć, wszelkie przesłonięte metody dostępu są dostępne dla kodu zastępowania. Ponadto deklarowana dostępność zarówno właściwości, jak i indeksatora, i akcesoriów, jest zgodna z elementem przesłoniętego elementu członkowskiego i akcesoriów.

Przykład:

public class B
{
    public virtual int P
    {
        get {...}
        protected set {...}
    }
}

public class D: B
{
    public override int P
    {
        get {...}            // Must not have a modifier here
        protected set {...}  // Must specify protected here
    }
}

przykład końcowy

Zdarzenia 15.8

15.8.1 Ogólne

Zdarzenie jest elementem członkowskim, który umożliwia obiektowi lub klasie dostarczanie powiadomień. Klienci mogą dołączać kod wykonywalny dla zdarzeń, dostarczając programy obsługi zdarzeń.

Zdarzenia są deklarowane przy użyciu event_declarations:

event_declaration
    : attributes? event_modifier* 'event' type variable_declarators ';'
    | attributes? event_modifier* 'event' type member_name
        '{' event_accessor_declarations '}'
    ;

event_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

event_accessor_declarations
    : add_accessor_declaration remove_accessor_declaration
    | remove_accessor_declaration add_accessor_declaration
    ;

add_accessor_declaration
    : attributes? 'add' block
    ;

remove_accessor_declaration
    : attributes? 'remove' block
    ;

unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).

Event_declaration może zawierać zestaw atrybutów(§22) i dowolny z dozwolonych rodzajów deklarowanej dostępności (§15.3.6), new (§15.3.5), static (§15.6.3, §15.8.4), (virtual§15.8.5), override (§15.6.5, §15.8.5), sealed (§15.6.6), abstract (§15.6.7, §15.8.5) i extern (§15.6.8) modyfikatory.

Deklaracje zdarzeń podlegają tym samym regułom co deklaracje metod (§15.6) w odniesieniu do prawidłowych kombinacji modyfikatorów.

Rodzaj deklaracji zdarzenia jest delegate_type (§8.2.8), a delegate_type jest co najmniej tak dostępny, jak samo wydarzenie (§7.5.5).

Deklaracja zdarzenia może zawierać event_accessor_declarations. Jeśli jednak nie, w przypadku zdarzeń, które nie są extern ani abstrakcyjne, kompilator dostarcza je automatycznie (§15.8.2); w przypadku zdarzeń extern metody dostępu są udostępniane zewnętrznie.

Deklaracja zdarzenia, która pomija event_accessor_declarationdefiniuje co najmniej jedno zdarzenie — jedno dla każdego variable_declarator s. Atrybuty i modyfikatory mają zastosowanie do wszystkich elementów członkowskich zadeklarowanych przez taki event_declaration.

Jest to błąd czasu kompilacji dla event_declaration dołączania zarówno abstract modyfikatora, jak i event_accessor_declarations.

Gdy deklaracja zdarzenia zawiera extern modyfikator, zdarzenie jest mówi się, że zdarzenie jest zdarzeniem zewnętrznym. Ponieważ deklaracja zdarzenia zewnętrznego nie zawiera rzeczywistej implementacji, jest to błąd, który zawiera zarówno extern modyfikator, jak i event_accessor_declarations.

Jest to błąd czasu kompilacji dla variable_declarator deklaracji zdarzenia z modyfikatorem abstract lub external w celu uwzględnienia variable_initializer.

Zdarzenie może być używane jako lewy operand operatorów += i -= . Te operatory są używane odpowiednio do dołączania programów obsługi zdarzeń do programu lub usuwania programów obsługi zdarzeń ze zdarzenia oraz modyfikatorów dostępu do zdarzeń kontrolują konteksty, w których takie operacje są dozwolone.

Jedynymi operacjami, które są dozwolone na zdarzeniu przez kod, który znajduje się poza typem, w którym to zdarzenie jest zadeklarowane, to += i -=. W związku z tym, mimo że taki kod może dodawać i usuwać programy obsługi dla zdarzenia, nie może on bezpośrednio uzyskać ani zmodyfikować źródłowej listy programów obsługi zdarzeń.

W operacji formularza x += y lub , gdy x –= y jest zdarzeniem wynik operacji ma typ x (§12.21.5voidw przeciwieństwie do typu , z wartością x po przypisaniu, jak w przypadku innych x operatorów i += zdefiniowanych w typach -=innych niż zdarzenia). Zapobiega to pośredniemu zbadaniu bazowego delegata zdarzenia przez kod zewnętrzny.

Przykład: W poniższym przykładzie pokazano, jak programy obsługi zdarzeń są dołączane do wystąpień Button klasy:

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;
}

public class LoginDialog : Form
{
    Button okButton;
    Button cancelButton;

    public LoginDialog()
    {
        okButton = new Button(...);
        okButton.Click += new EventHandler(OkButtonClick);
        cancelButton = new Button(...);
        cancelButton.Click += new EventHandler(CancelButtonClick);
    }

    void OkButtonClick(object sender, EventArgs e)
    {
        // Handle okButton.Click event
    }

    void CancelButtonClick(object sender, EventArgs e)
    {
        // Handle cancelButton.Click event
    }
}

LoginDialog W tym miejscu konstruktor wystąpienia tworzy dwa Button wystąpienia i dołącza programy obsługi zdarzeń do zdarzeńClick.

przykład końcowy

15.8.2 Zdarzenia podobne do pól

W tekście programu klasy lub struktury, która zawiera deklarację zdarzenia, niektóre zdarzenia mogą być używane jak pola. Do wykorzystania w ten sposób zdarzenie nie jest abstrakcyjne ani extern, a nie uwzględnia jawnie event_accessor_declarations. Takie zdarzenie może być używane w dowolnym kontekście, który zezwala na pole. Pole zawiera delegata (§20), który odwołuje się do listy procedur obsługi zdarzeń, które zostały dodane do zdarzenia. Jeśli nie dodano żadnych procedur obsługi zdarzeń, pole zawiera nullwartość .

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 w Button klasie . Jak pokazano w przykładzie, pole można zbadać, zmodyfikować i użyć w wyrażeniach wywołania delegata. Metoda OnClick w Button klasie "wywołuje" Click zdarzenie. Pojęcie wywoływania zdarzenia jest dokładnie równoważne wywołaniu delegata reprezentowanego przez zdarzenie — w związku z tym nie ma specjalnych konstrukcji językowych do wywoływania zdarzeń. Należy pamiętać, że wywołanie delegata jest poprzedzone sprawdzaniem, które gwarantuje, że delegat nie ma wartości null i że sprawdzanie jest wykonywane na kopii lokalnej w celu zapewnienia bezpieczeństwa wątku.

Poza deklaracją Button klasy Click składowa może być używana tylko po lewej stronie operatorów += i –= , jak w

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

dołącza delegata do listy Click wywołań zdarzenia i

Click –= new EventHandler(...);

który usuwa delegata z listy Click wywołań zdarzenia.

przykład końcowy

pl-PL: Podczas kompilowania zdarzenia przypominającego pole kompilator automatycznie tworzy miejsce do przechowywania delegata i tworzy akcesory dla zdarzenia, które dodają lub usuwają procedury obsługi zdarzeń w polu delegata. Operacje dodawania i usuwania są bezpieczne wątkami i mogą (ale nie są wymagane) być wykonywane podczas przechowywania blokady (§13.13System.Type§12.8.18) dla zdarzenia statycznego.

Uwaga: W związku z tym deklaracja zdarzenia wystąpienia formularza:

class X
{
    public event D Ev;
}

należy skompilować coś równoważnego:

class X
{
    private D __Ev; // field to hold the delegate

    public event D Ev
    {
        add
        {
            /* Add the delegate in a thread safe way */
        }
        remove
        {
            /* Remove the delegate in a thread safe way */
        }
    }
}

W klasie Xodwołania do odwołania po Ev lewej stronie += operatorów i –= powodują wywołanie metody dodawania i usuwania metod dostępu. Wszystkie inne odwołania do Ev elementu są kompilowane w celu odwołowania się do pola __Ev ukrytego (§12.8.7). Nazwa "__Ev" jest dowolna; ukryte pole może mieć dowolną nazwę lub żadną nazwę.

notatka końcowa

15.8.3 Akcesory zdarzeń

Uwaga: Deklaracje zdarzeń zazwyczaj pomijają event_accessor_declarations, jak w powyższym przykładzie Button . Mogą one być na przykład uwzględniane, jeśli koszt magazynu jednego pola na zdarzenie nie jest akceptowalny. W takich przypadkach klasa może zawierać event_accessor_declarations i użyć mechanizmu prywatnego do przechowywania listy procedur obsługi zdarzeń. notatka końcowa

Event_accessor_declarations zdarzenia określają instrukcje wykonywalne skojarzone z dodawaniem i usuwaniem programów obsługi zdarzeń.

Deklaracje dostępu składają się z add_accessor_declaration i remove_accessor_declaration. Każda deklaracja dostępu składa się z dodawania lub usuwania tokenu, po którym następuje blok. Blok skojarzony z add_accessor_declaration określa instrukcje do wykonania po dodaniu programu obsługi zdarzeń, a blok skojarzony z remove_accessor_declaration określa instrukcje do wykonania po usunięciu programu obsługi zdarzeń.

Każda add_accessor_declaration i remove_accessor_declaration odpowiada metodzie z pojedynczym parametrem wartości typu zdarzenia i typem zwracanym void . Niejawny parametr metody dostępu zdarzeń nosi nazwę value. Gdy zdarzenie jest używane w przypisaniu zdarzeń, używane jest odpowiednie akcesorium zdarzenia. W szczególności, jeśli operator przypisania jest += używany, dodaj metodę dostępu, a jeśli operator przypisania jest –= używany, jest używany metodę usuwania dostępu. W obu przypadkach prawy operand operatora przypisania jest używany jako argument metody dostępu zdarzenia. Blok add_accessor_declaration lub remove_accessor_declaration jest zgodny z zasadami dotyczącymi metod opisanych w §15.6.9.void W szczególności return instrukcje w takim bloku nie mogą określać wyrażenia.

Ponieważ akcesorium zdarzenia niejawnie ma parametr o nazwie value, jest to błąd czasu kompilacji dla zmiennej lokalnej lub stałej zadeklarowanej w metodzie dostępu zdarzenia, aby mieć tę nazwę.

Przykład: w poniższym kodzie


class Control : Component
{
    // Unique keys for events
    static readonly object mouseDownEventKey = new object();
    static readonly object mouseUpEventKey = new object();

    // Return event handler associated with key
    protected Delegate GetEventHandler(object key) {...}

    // Add event handler associated with key
    protected void AddEventHandler(object key, Delegate handler) {...}

    // Remove event handler associated with key
    protected void RemoveEventHandler(object key, Delegate handler) {...}

    // MouseDown event
    public event MouseEventHandler MouseDown
    {
        add { AddEventHandler(mouseDownEventKey, value); }
        remove { RemoveEventHandler(mouseDownEventKey, value); }
    }

    // MouseUp event
    public event MouseEventHandler MouseUp
    {
        add { AddEventHandler(mouseUpEventKey, value); }
        remove { RemoveEventHandler(mouseUpEventKey, value); }
    }

    // Invoke the MouseUp event
    protected void OnMouseUp(MouseEventArgs args)
    {
        MouseEventHandler handler;
        handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey);
        if (handler != null)
        {
            handler(this, args);
        }
    }
}

Control klasa implementuje wewnętrzny mechanizm przechowywania zdarzeń. Metoda AddEventHandler kojarzy wartość delegata z kluczem, GetEventHandler metoda zwraca delegata aktualnie skojarzonego z kluczem, a RemoveEventHandler metoda usuwa delegata jako program obsługi zdarzeń dla określonego zdarzenia. Prawdopodobnie podstawowy mechanizm magazynu został zaprojektowany tak, aby nie istniał koszt skojarzenia wartości delegata o wartości null z kluczem, a tym samym nieobsługiwane zdarzenia nie zużywają miejsca do magazynowania.

przykład końcowy

15.8.4 Zdarzenia statyczne i wystąpienia

Gdy deklaracja zdarzenia zawiera static modyfikator, zdarzenie jest podobno zdarzeniem statycznym. Jeśli modyfikator nie static jest obecny, zdarzenie jest mówi się, że jest zdarzeniem wystąpienia.

Zdarzenie statyczne nie jest skojarzone z konkretnym wystąpieniem i jest to błąd czasu kompilacji, który należy odwołać się do this metody dostępu zdarzenia statycznego.

Zdarzenie wystąpienia jest skojarzone z danym wystąpieniem klasy, a do tego wystąpienia można uzyskać dostęp jako this (§12.8.14) w akcesorach tego zdarzenia.

Różnice między elementami statycznymi i elementami członkowskimi wystąpień zostały omówione w temacie §15.3.8.

15.8.5 Wirtualne, zapieczętowane, zastępowane i abstrakcyjne metody dostępu

Deklaracja zdarzenia wirtualnego określa, że metody dostępu tego zdarzenia są wirtualne. Modyfikator virtual ma zastosowanie do obu metod dostępu zdarzenia.

Abstrakcyjna deklaracja zdarzenia określa, że metody dostępu zdarzenia są wirtualne, ale nie zapewnia rzeczywistej implementacji metod dostępu. Zamiast tego klasy pochodne nie abstrakcyjne są wymagane do zapewnienia własnej implementacji dla metod dostępu przez zastąpienie zdarzenia. Ponieważ akcesorium do deklaracji zdarzeń abstrakcyjnych nie zapewnia rzeczywistej implementacji, nie dostarcza event_accessor_declarations.

Deklaracja zdarzenia, która zawiera zarówno abstract modyfikatory, jak i override określa, że zdarzenie jest abstrakcyjne i zastępuje zdarzenie podstawowe. Metody dostępu do takiego zdarzenia są również abstrakcyjne.

Deklaracje zdarzeń abstrakcyjnych są dozwolone tylko w klasach abstrakcyjnych (§15.2.2.2).

Metody dostępu dziedziczonego zdarzenia wirtualnego mogą zostać zastąpione w klasie pochodnej przez dołączenie deklaracji zdarzenia, która określa override modyfikator. Jest to nazywane zastępowaniem deklaracji zdarzenia. Zastępowanie deklaracji zdarzenia nie deklaruje nowego zdarzenia. Zamiast tego po prostu specjalizuje się w implementacjach akcesoriów istniejącego zdarzenia wirtualnego.

Deklaracja zdarzenia zastępowania określa dokładnie te same modyfikatory ułatwień dostępu i nazwę co zdarzenie zastępowane. W deklaracji należy określić konwersję tożsamości między typem przesłonięcia a przesłoniętą zdarzeniem, a zarówno dodatkiem, jak i usuwaniem metod dostępu.

Zastępowanie deklaracji zdarzenia może obejmować sealed modyfikator. this Użycie modyfikatora uniemożliwia dalsze zastępowanie zdarzenia przez klasę pochodną. Akcesoria zapieczętowanego zdarzenia są również zapieczętowane.

Jest to błąd czasu kompilacji dla przesłaniającej deklaracji zdarzenia w celu uwzględnienia new modyfikatora.

Z wyjątkiem różnic w składni deklaracji i wywołania, wirtualnych, zapieczętowanych, przesłonięć i abstrakcyjnych metod zachowuje się dokładnie tak jak metody wirtualne, zapieczętowane, zastępowane i abstrakcyjne. W szczególności zasady opisane w §15.6.4, §15.6.5, §15.6.6 i §15.6.7 mają zastosowanie tak, jakby metody dostępu były odpowiednimi formami. Każda metoda dostępu odpowiada metodzie z pojedynczym parametrem wartości typu zdarzenia, typem zwracanym void i tymi samymi modyfikatorami co zawierające zdarzenie.

Indeksatory 15.9

15.9.1 Ogólne

Indeksator jest elementem członkowskim, który umożliwia indeksowanie obiektu w taki sam sposób jak tablica. Indeksatory są deklarowane przy użyciu indexer_declarations:

indexer_declaration
    : attributes? indexer_modifier* indexer_declarator indexer_body
    | attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
    ;

indexer_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

indexer_declarator
    : type 'this' '[' parameter_list ']'
    | type interface_type '.' 'this' '[' parameter_list ']'
    ;

indexer_body
    : '{' accessor_declarations '}' 
    | '=>' expression ';'
    ;  

ref_indexer_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).

Istnieją dwa rodzaje indexer_declaration:

  • Pierwszy deklaruje indeksator nienależący do wartości ref. Jego wartość ma typ typu. Ten rodzaj indeksatora może być czytelny i/lub zapisywalny.
  • Drugi deklaruje indeksator o wartości ref. Jego wartość to variable_reference (§9.5readonly Ten rodzaj indeksatora jest dostępny tylko do odczytu.

Indexer_declaration może zawierać zestaw atrybutów (§22) oraz dowolny z dozwolonych rodzajów zadeklarowanych ułatwień dostępu (§15.3.6), new (§15.3.5), (§15.5). virtual 6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7) i extern (§15.6.8) modyfikatory.

Deklaracje indeksatora podlegają tym samym regułom co deklaracje metod (§15.6) w odniesieniu do prawidłowych kombinacji modyfikatorów, z jednym wyjątkiem jest to, że static modyfikator nie jest dozwolony w deklaracji indeksatora.

Typ deklaracji indeksatora określa typ elementu indeksatora wprowadzonego przez deklarację.

Uwaga: Ponieważ indeksatory są przeznaczone do użycia w kontekstach przypominających element tablicy, termin typ elementu zdefiniowany dla tablicy jest również używany z indeksatorem. notatka końcowa

Chyba że indeksator jest jawną implementacją elementu członkowskiego interfejsu, po którym następuje słowo kluczowe this. W przypadku implementacji jawnego elementu członkowskiego interfejsu typ następuje interface_type, "" i słowo kluczowe ..this W przeciwieństwie do innych elementów członkowskich indeksatory nie mają nazw zdefiniowanych przez użytkownika.

Parameter_list określa parametry indeksatora. Lista parametrów indeksatora odpowiada tej metodzie (§15.6.2), z tą różnicą, że określono co najmniej jeden parametr i że thismodyfikatory parametrów , refi out nie są dozwolone.

Typ indeksatora i każdy z typów, do których odwołuje się parameter_list, jest co najmniej tak dostępny, jak sam indeksator (§7.5.5).

Indexer_body może składać się z treści instrukcji (§15.7.1) lub treści wyrażenia (§15.6.1). W treści instrukcji, accessor_declarations, które są ujęte w tokeny "{" i "}", deklarują akcesoriów (§15.7.3) indeksatora. Metody dostępu określają instrukcje wykonywalne skojarzone z odczytywaniem i zapisywaniem elementów indeksatora.

W indexer_body treści wyrażenia składającej się z wyrazu "=>", po którym następuje wyrażenieE, a średnik jest dokładnie odpowiednikiem treści { get { return E; } }instrukcji , i dlatego może służyć tylko do określania indeksatorów tylko do odczytu, gdzie wynik metody uzyskiwania dostępu jest podawany przez pojedyncze wyrażenie.

Ref_indexer_body może składać się z treści instrukcji lub treści wyrażenia. W treści instrukcji get_accessor_declaration deklaruje metodę get (§15.7.3) indeksatora. Metodę dostępu określa instrukcje wykonywalne skojarzone z odczytywaniem indeksatora.

W ref_indexer_body treści wyrażenia składającej => się z elementu ref, variable_referenceV i średnik jest dokładnie odpowiednikiem treści { get { return ref V; } }instrukcji .

Uwaga: mimo że składnia uzyskiwania dostępu do elementu indeksatora jest taka sama jak w przypadku elementu tablicy, element indeksatora nie jest klasyfikowany jako zmienna. W związku z tym nie można przekazać elementu indeksatora jako argumentu in, outlub ref , chyba że indeksator jest ref-valued i dlatego zwraca odwołanie (§9.7). notatka końcowa

Parameter_list indeksatora definiuje podpis (§7.6) indeksatora. W szczególności podpis indeksatora składa się z liczby i typów jego parametrów. Typ elementu i nazwy parametrów nie są częścią podpisu indeksatora.

Podpis indeksatora różni się od podpisów wszystkich innych indeksatorów zadeklarowanych w tej samej klasie.

Gdy deklaracja indeksatora zawiera extern modyfikator, indeksator jest podobno indeksatorem zewnętrznym. Ponieważ deklaracja indeksatora zewnętrznego nie zapewnia rzeczywistej implementacji, każda z accessor_bodys w jej accessor_declarations jest średnikiem.

Przykład: Poniższy przykład deklaruje klasę BitArray , która implementuje indeksator na potrzeby uzyskiwania dostępu do poszczególnych bitów w tablicy bitowej.

class BitArray
{
    int[] bits;
    int length;

    public BitArray(int length)
    {
        if (length < 0)
        {
            throw new ArgumentException();
        }
        bits = new int[((length - 1) >> 5) + 1];
        this.length = length;
    }

    public int Length => length;

    public bool this[int index]
    {
        get
        {
            if (index < 0 || index >= length)
            {
                throw new IndexOutOfRangeException();
            }
            return (bits[index >> 5] & 1 << index) != 0;
        }
        set
        {
            if (index < 0 || index >= length)
            {
                throw new IndexOutOfRangeException();
            }
            if (value)
            {
                bits[index >> 5] |= 1 << index;
            }
            else
            {
                bits[index >> 5] &= ~(1 << index);
            }
        }
    }
}

Wystąpienie BitArray klasy zużywa znacznie mniej pamięci niż odpowiadającą bool[] mu wartość (ponieważ każda wartość poprzedniego zajmuje tylko jeden bit zamiast jednego bytez nich), ale zezwala na te same operacje co bool[].

W poniższej CountPrimes klasie użyto algorytmu i BitArray klasycznego "sita" do obliczenia liczby pierwszych z zakresu od 2 do podanej wartości maksymalnej:

class CountPrimes
{
    static int Count(int max)
    {
        BitArray flags = new BitArray(max + 1);
        int count = 0;
        for (int i = 2; i <= max; i++)
        {
            if (!flags[i])
            {
                for (int j = i * 2; j <= max; j += i)
                {
                    flags[j] = true;
                }
                count++;
            }
        }
        return count;
    }

    static void Main(string[] args)
    {
        int max = int.Parse(args[0]);
        int count = Count(max);
        Console.WriteLine($"Found {count} primes between 2 and {max}");
    }
}

Należy pamiętać, że składnia uzyskiwania dostępu do elementów elementu BitArray jest dokładnie taka sama jak w przypadku elementu bool[].

W poniższym przykładzie przedstawiono klasę siatki 26×10 zawierającą indeksator z dwoma parametrami. Pierwszy parametr jest wymagany do wielkich lub małych liter w zakresie A–Z, a drugi jest wymagany jako liczba całkowita w zakresie od 0 do 9.

class Grid
{
    const int NumRows = 26;
    const int NumCols = 10;
    int[,] cells = new int[NumRows, NumCols];

    public int this[char row, int col]
    {
        get
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            return cells[row - 'A', col];
        }
        set
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException ("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            cells[row - 'A', col] = value;
        }
    }
}

przykład końcowy

15.9.2 Indeksator i różnice właściwości

Indeksatory i właściwości są bardzo podobne w koncepcji, ale różnią się w następujący sposób:

  • Właściwość jest identyfikowana przez jego nazwę, podczas gdy indeksator jest identyfikowany przez jego podpis.
  • Dostęp do właściwości można uzyskać za pośrednictwem simple_name (§12.8.4) lub member_access (§12.8.7), natomiast dostęp do elementu indeksatora odbywa się za pośrednictwem element_access (§12.8.12.3).
  • Właściwość może być statycznym elementem członkowskim, natomiast indeksator jest zawsze członkiem wystąpienia.
  • Metoda get dostępu do właściwości odpowiada metodzie bez parametrów, natomiast metoda get dostępu indeksatora odpowiada metodzie z tą samą listą parametrów co indeksator.
  • Metoda dostępu zestawu odpowiada metodzie z pojedynczym parametrem o nazwie value, natomiast metoda dostępu zestawu indeksatora odpowiada metodzie z tą samą listą parametrów co indeksator, a także dodatkowy parametr o nazwie value.
  • Jest to błąd czasu kompilacji dla metody dostępu indeksatora, aby zadeklarować zmienną lokalną lub stałą lokalną o takiej samej nazwie jak parametr indeksatora.
  • W deklaracji właściwości zastępowania dostęp do dziedziczonej właściwości jest uzyskiwany przy użyciu składni base.P, gdzie P jest nazwą właściwości. W deklaracji indeksatora zastępowania dziedziczony indeksator jest uzyskiwany przy użyciu składni base[E], gdzie E jest rozdzielaną przecinkami listą wyrażeń.
  • Nie ma pojęcia "automatycznie zaimplementowanego indeksatora". Błąd polega na tym, że indeksator nie jest abstrakcyjny, a nie zewnętrzny z średnikami accessor_bodys.

Oprócz tych różnic, wszystkie reguły zdefiniowane w §15.7.3, §15.7.5 i §15.7.6 mają zastosowanie do akcesoriów indeksatora, a także akcesoriów do właściwości.

Zastąpienie właściwości/właściwości indeksatorem/indeksatorami podczas odczytywania §15.7.3, §15.7.5 i §15.7.6 dotyczy również zdefiniowanych terminów. W szczególności właściwość read-write staje się indeksatorem tylko do odczytu, właściwość tylko do odczytu staje się indeksatorem tylko do odczytu, a właściwość tylko do zapisu staje się indeksatorem tylko do zapisu.

Operatory 15.10

15.10.1 Ogólne

Operator jest elementem członkowskim, który definiuje znaczenie operatora wyrażenia, który można zastosować do wystąpień klasy. Operatory są deklarowane przy użyciu operator_declarations:

operator_declaration
    : attributes? operator_modifier+ operator_declarator operator_body
    ;

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

operator_declarator
    : unary_operator_declarator
    | binary_operator_declarator
    | conversion_operator_declarator
    ;

unary_operator_declarator
    : type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
    ;

logical_negation_operator
    : '!'
    ;

overloadable_unary_operator
    : '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
    ;

binary_operator_declarator
    : type 'operator' overloadable_binary_operator
        '(' fixed_parameter ',' fixed_parameter ')'
    ;

overloadable_binary_operator
    : '+'  | '-'  | '*'  | '/'  | '%'  | '&' | '|' | '^'  | '<<' 
    | right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
    ;

conversion_operator_declarator
    : 'implicit' 'operator' type '(' fixed_parameter ')'
    | 'explicit' 'operator' type '(' fixed_parameter ')'
    ;

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

unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).

Uwaga: Operatory negacji logicznej prefiksu (§12.9.4) i postfix null-forgiving (§12.8.9), reprezentowane przez ten sam token leksykacyjny (!), są odrębne. Ten ostatni nie jest operatorem przeciążalnym. notatka końcowa

Istnieją trzy kategorie operatorów przeciążalnych: operatory jednoargumentowe (§15.10.2), operatory binarne (§15.10.3) i operatory konwersji (§15.10.4).

Operator_body to średnik, treść bloku (§15.6.1) lub treść wyrażenia (§15.6.1). Treść bloku składa się z bloku, który określa instrukcje do wykonania po wywołaniu operatora. Blok jest zgodny z zasadami metod zwracania wartości opisanych w §15.6.11. Treść wyrażenia składa się z następującego => po nim wyrażenia i średnika oraz oznacza pojedyncze wyrażenie do wykonania po wywołaniu operatora.

W przypadku extern operatorów operator_body składa się po prostu ze średnika. W przypadku wszystkich innych operatorów operator_body jest treścią bloku lub treścią wyrażenia.

Następujące reguły mają zastosowanie do wszystkich deklaracji operatorów:

  • Deklaracja operatora zawiera zarówno element, jak public i static modyfikator.
  • Parametry operatora nie mają żadnych modyfikatorów innych niż in.
  • Podpis operatora (§15.10.2, §15.10.3, §15.10.4) różni się od podpisów wszystkich innych operatorów zadeklarowanych w tej samej klasie.
  • Wszystkie typy, do których odwołuje się deklaracja operatora, są co najmniej tak dostępne, jak sam operator (§7.5.5).
  • Jest to błąd dla tego samego modyfikatora, który pojawia się wiele razy w deklaracji operatora.

Każda kategoria operatorów nakłada dodatkowe ograniczenia, zgodnie z opisem w poniższych podklasach.

Podobnie jak inne elementy członkowskie, operatory zadeklarowane w klasie bazowej są dziedziczone przez klasy pochodne. Ponieważ deklaracje operatorów zawsze wymagają klasy lub struktury, w której operator jest zadeklarowany do udziału w podpisie operatora, nie jest możliwe, aby operator zadeklarowany w klasie pochodnej ukrywał operator zadeklarowany w klasie bazowej. new W związku z tym modyfikator nigdy nie jest wymagany i dlatego nigdy nie jest dozwolony w deklaracji operatora.

Dodatkowe informacje na temat operatorów jednoargumentowych i binarnych można znaleźć w §12.4.

Dodatkowe informacje na temat operatorów konwersji można znaleźć w §10.5.

15.10.2 Operatory jednoargumentowe

Następujące reguły dotyczą deklaracji operatorów jednoargumentowych, gdzie T określa typ wystąpienia klasy lub struktury zawierającej deklarację operatora:

  • Jednoargumentowy +, ( -!tylko negacja logiczna) lub ~ operator musi przyjmować jeden parametr typu T lub T? może zwrócić dowolny typ.
  • Jednoargumentowy ++ lub -- operator bierze jeden parametr typu T lub T? zwraca ten sam typ lub typ pochodzący z niego.
  • Jednoargumentowy true lub false operator ma jeden parametr typu T lub T? zwraca typ bool.

Podpis operatora jednoargumentowego składa się z tokenu operatora (+, -, !~++--truelub false) i typu pojedynczego parametru. Zwracany typ nie jest częścią podpisu operatora jednoargumentowego ani nie jest nazwą parametru.

Operatory true jednoargumentowe i false wymagają deklaracji pary. Błąd czasu kompilacji występuje, jeśli klasa deklaruje jeden z tych operatorów bez deklarowania drugiego. Operatory true i false zostały opisane dalej w §12.24.

Przykład: Poniższy przykład przedstawia implementację i kolejne użycie operatora++ dla klasy wektorów całkowitych:

public class IntVector
{
    public IntVector(int length) {...}
    public int Length { get { ... } }                      // Read-only property
    public int this[int index] { get { ... } set { ... } } // Read-write indexer

    public static IntVector operator++(IntVector iv)
    {
        IntVector temp = new IntVector(iv.Length);
        for (int i = 0; i < iv.Length; i++)
        {
            temp[i] = iv[i] + 1;
        }
        return temp;
    }
}

class Test
{
    static void Main()
    {
        IntVector iv1 = new IntVector(4); // Vector of 4 x 0
        IntVector iv2;
        iv2 = iv1++;              // iv2 contains 4 x 0, iv1 contains 4 x 1
        iv2 = ++iv1;              // iv2 contains 4 x 2, iv1 contains 4 x 2
    }
}

Zwróć uwagę, jak metoda operatora zwraca wartość wygenerowaną przez dodanie wartości 1 do operandu, podobnie jak operatory przyrostka i dekrementacji (§12.8.16) oraz operatory przyrostu i dekrementacji (§12.9.6). W przeciwieństwie do języka C++, ta metoda nie powinna modyfikować wartości operandu bezpośrednio, ponieważ naruszałoby to standardową semantyka operatora przyrostka (§12.8.16).

przykład końcowy

15.10.3 Operatory binarne

Następujące reguły dotyczą deklaracji operatorów binarnych, gdzie T określa typ wystąpienia klasy lub struktury zawierającej deklarację operatora:

  • Operator binarny bez zmian musi przyjmować dwa parametry, z których co najmniej jeden ma typ T lub T?, i może zwrócić dowolny typ.
  • Operator binarny lub << operator >> (§12.11) ma dwa parametry, z których pierwszy ma typ lub T? i drugi z nich ma typ Tint lub int?, i może zwrócić dowolny typ.

Podpis operatora binarnego składa się z tokenu operatora (+, -, */%&|^<<>>==!=><>=lub <=) i typów dwóch parametrów. Zwracany typ i nazwy parametrów nie są częścią podpisu operatora binarnego.

Niektóre operatory binarne wymagają deklaracji pary. Dla każdej deklaracji jednego operatora pary musi istnieć zgodna deklaracja innego operatora pary. Dwa deklaracje operatorów są zgodne, jeśli istnieją konwersje tożsamości między ich typami zwracania i odpowiadającymi im typami parametrów. Następujące operatory wymagają deklaracji pary:

  • operator == i operator !=
  • operator > i operator <
  • operator >= i operator <=

15.10.4 Operatory konwersji

Deklaracja operatora konwersji wprowadza konwersję zdefiniowaną przez użytkownika (§10.5), która rozszerza wstępnie zdefiniowane niejawne i jawne konwersje.

Deklaracja operatora konwersji zawierająca implicit słowo kluczowe wprowadza zdefiniowaną przez użytkownika niejawną konwersję. Niejawne konwersje mogą wystąpić w różnych sytuacjach, w tym wywołania składowych funkcji, wyrażenia rzutowania i przypisania. Opisano to dalej w §10.2.

Deklaracja operatora konwersji zawierająca explicit słowo kluczowe wprowadza jawną konwersję zdefiniowaną przez użytkownika. Jawne konwersje mogą występować w wyrażeniach rzutowanych i są opisane dalej w §10.3.

Operator konwersji konwertuje z typu źródła wskazanego przez typ parametru operatora konwersji na typ docelowy wskazany przez typ zwracany operatora konwersji.

Dla danego typu źródłowego i typu STdocelowego , jeśli S lub T są typami wartości dopuszczania wartości null, niech S₀ i T₀ odwołują się do ich typów bazowych; w przeciwnym razie S₀ i T₀ są równe S i T odpowiednio. Klasa lub struktura może zadeklarować konwersję z typu źródłowego na typ ST docelowy tylko wtedy, gdy spełnione są wszystkie następujące elementy:

  • S₀ i T₀ są różnymi typami.

  • Albo S₀T₀ jest typem wystąpienia klasy lub struktury zawierającej deklarację operatora.

  • Ani nie S₀T₀ jest interface_type.

  • Wykluczanie konwersji zdefiniowanych przez użytkownika, konwersja nie istnieje z S do T lub z T do S.

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 i int i string, są uznawane za unikatowe typy bez relacji. Jednak trzeci operator jest błędem, ponieważ C<T> jest klasą bazową klasy D<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ę z C do i od int do intC, ale nie z int do bool. 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 dla T, drugi operator deklaruje konwersję, która już istnieje (niejawna, a tym samym jawna, istnieje konwersja z dowolnego typu na obiekt typu).

przykład końcowy

W przypadkach, gdy istnieje wstępnie zdefiniowana konwersja między dwoma typami, wszelkie konwersje zdefiniowane przez użytkownika między tymi typami są ignorowane. Szczególnie:

  • Jeśli wstępnie zdefiniowana niejawna konwersja (§10.2Tniejawne lub jawne) z S do T są ignorowane.
  • Jeśli wstępnie zdefiniowana jawna konwersja (§10.3T Ponadto:
    • Jeśli typ S interfejsu lub T jest typem interfejsu, niejawne konwersje zdefiniowane przez użytkownika z S do T są ignorowane.
    • W przeciwnym razie konwersje niejawne zdefiniowane przez użytkownika z S do T są nadal brane pod uwagę.

Dla wszystkich typów, ale objectoperatory zadeklarowane przez Convertible<T> typ powyżej nie powodują konfliktu ze wstępnie zdefiniowanymi konwersjami.

Przykład:

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

Jednak w przypadku typów objectkonwersje wstępnie zdefiniowane ukrywają konwersje zdefiniowane przez użytkownika we wszystkich przypadkach, ale jedno:

void F(object o, Convertible<object> n)
{
    o = n;                       // Pre-defined boxing conversion
    o = (object)n;               // Pre-defined boxing conversion
    n = o;                       // User-defined implicit conversion
    n = (Convertible<object>)o;  // Pre-defined unboxing conversion
}

przykład końcowy

Konwersje zdefiniowane przez użytkownika nie mogą być konwertowane z lub na interface_types. W szczególności to ograniczenie gwarantuje, że podczas konwersji na interface_type nie wystąpią żadne przekształcenia zdefiniowane przez użytkownika, a konwersja na interface_type powiedzie się tylko wtedy, gdy object konwersja rzeczywiście implementuje określony interface_type.

Podpis operatora konwersji składa się z typu źródłowego i typu docelowego. (Jest to jedyna forma elementu członkowskiego, dla którego zwracany typ uczestniczy w podpisie). Niejawna lub jawna klasyfikacja operatora konwersji nie jest częścią podpisu operatora. W związku z tym klasa lub struktura nie może zadeklarować zarówno niejawnego, jak i jawnego operatora konwersji z tymi samymi typami źródłowymi i docelowymi.

Uwaga: Ogólnie rzecz biorąc, niejawne konwersje zdefiniowane przez użytkownika powinny być zaprojektowane tak, aby nigdy nie zgłaszać wyjątków i nigdy nie tracić informacji. Jeśli konwersja zdefiniowana przez użytkownika może spowodować powstanie wyjątków (na przykład dlatego, że argument źródłowy jest poza zakresem) lub utrata informacji (takich jak odrzucanie bitów o wysokiej kolejności), ta konwersja powinna być zdefiniowana jako jawna konwersja. notatka końcowa

Przykład: w poniższym kodzie

public struct Digit
{
    byte value;

    public Digit(byte value)
    {
        if (value < 0 || value > 9)
        {
            throw new ArgumentException();
        }
        this.value = value;
    }

    public static implicit operator byte(Digit d) => d.value;
    public static explicit operator Digit(byte b) => new Digit(b);
}

konwersja z Digit na jest niejawna, ponieważ nigdy nie zgłasza wyjątków lub traci informacje, ale konwersja z byte na byte jest jawna, ponieważ Digit może reprezentować tylko podzbiór możliwych wartości Digitbyte .

przykład końcowy

15.11 Konstruktory wystąpień

15.11.1 Ogólne

Konstruktor wystąpienia jest elementem członkowskim, który implementuje akcje wymagane do zainicjowania wystąpienia klasy. Konstruktory wystąpień są deklarowane przy użyciu constructor_declarations:

constructor_declaration
    : attributes? constructor_modifier* constructor_declarator constructor_body
    ;

constructor_modifier
    : 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

constructor_declarator
    : identifier '(' parameter_list? ')' constructor_initializer?
    ;

constructor_initializer
    : ':' 'base' '(' argument_list? ')'
    | ':' 'this' '(' argument_list? ')'
    ;

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

unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).

Constructor_declaration może zawierać zestaw atrybutów (§22), dowolny z dozwolonych rodzajów zadeklarowanej dostępności (§15.3.6) i extern modyfikator (§15.6.8). Deklaracja konstruktora nie może zawierać tego samego modyfikatora wiele razy.

Identyfikator constructor_declarator określa klasę, w której zadeklarowany jest konstruktor wystąpienia. Jeśli zostanie określona inna nazwa, wystąpi błąd czasu kompilacji.

Opcjonalne parameter_list konstruktora wystąpienia podlega tym samym regułom co parameter_list metody (§15.6). this Ponieważ modyfikator parametrów ma zastosowanie tylko do metod rozszerzeń (§15.6.10), żaden parametr w parameter_list konstruktora nie zawiera this modyfikatora. Lista parametrów definiuje podpis (§7.6) konstruktora wystąpienia i zarządza procesem, w którym rozpoznawanie przeciążenia (§12.6.4) wybiera konkretny konstruktor wystąpienia w wywołaniu.

Każdy z typów, do których odwołuje się parameter_list konstruktora wystąpienia, jest co najmniej tak dostępny, jak sam konstruktor (§7.5.5).

Opcjonalny constructor_initializer określa inny konstruktor wystąpienia do wywołania przed wykonaniem instrukcji podanych w constructor_body tego konstruktora wystąpienia. Opisano to dalej w §15.11.2.

Gdy deklaracja konstruktora zawiera extern modyfikator, konstruktor jest podobno konstruktorem zewnętrznym. Ponieważ deklaracja konstruktora zewnętrznego nie zawiera rzeczywistej implementacji, jego constructor_body składa się z średnika. W przypadku wszystkich innych konstruktorów constructor_body składa się z jednego z następujących elementów:

  • blok, który określa instrukcje inicjowania nowego wystąpienia klasy; lub
  • treść wyrażenia, która składa się z następującego =>po nim wyrażenia i średnika, i określa pojedyncze wyrażenie, aby zainicjować nowe wystąpienie klasy.

Constructor_body, który jest treścią bloku lub wyrażenia, odpowiada dokładnie blokowi metody wystąpienia z typem zwracanym void (§15.6.11).

Konstruktory wystąpień nie są dziedziczone. W związku z tym klasa nie ma konstruktorów wystąpień innych niż te rzeczywiście zadeklarowane w klasie, z wyjątkiem, że jeśli klasa nie zawiera deklaracji konstruktora wystąpienia, domyślny konstruktor wystąpienia jest udostępniany automatycznie (§15.11.5).

Konstruktory wystąpień są wywoływane przez object_creation_expression s (§12.8.17.2) i przez constructor_initializers.

Inicjatory konstruktorów 15.11.2

Wszystkie konstruktory wystąpień (z wyjątkiem tych dla klasy object) niejawnie obejmują wywołanie innego konstruktora wystąpienia bezpośrednio przed constructor_body. Konstruktor, który ma być niejawnie wywoływany, jest określany przez constructor_initializer:

  • Inicjator konstruktora wystąpienia formularza base(argument_list) (gdzie argument_list jest opcjonalny) powoduje wywołanie konstruktora wystąpienia z bezpośredniej klasy bazowej. Ten konstruktor jest wybierany przy użyciu argument_list i reguł rozpoznawania przeciążenia §12.6.4. Zestaw konstruktorów wystąpienia kandydata składa się ze wszystkich dostępnych konstruktorów wystąpień bezpośrednich klasy bazowej. Jeśli ten zestaw jest pusty lub nie można zidentyfikować jednego najlepszego konstruktora wystąpienia, wystąpi błąd czasu kompilacji.
  • Inicjator konstruktora wystąpienia formularza this(argument_list) (gdzie argument_list jest opcjonalny) wywołuje inny konstruktor wystąpienia z tej samej klasy. Konstruktor jest wybierany przy użyciu argument_list i reguł rozpoznawania przeciążenia §12.6.4. Zestaw konstruktorów wystąpienia kandydata składa się ze wszystkich konstruktorów wystąpień zadeklarowanych w samej klasie. Jeśli wynikowy zestaw odpowiednich konstruktorów wystąpień jest pusty lub nie można zidentyfikować jednego najlepszego konstruktora wystąpienia, wystąpi błąd czasu kompilacji. Jeśli deklaracja konstruktora wystąpienia wywołuje się za pośrednictwem łańcucha co najmniej jednego inicjatora konstruktora, wystąpi błąd czasu kompilacji.

Jeśli konstruktor wystąpienia nie ma inicjatora konstruktora, inicjator konstruktora formularza base() jest niejawnie udostępniany.

Uwaga: W związku z tym deklaracja konstruktora wystąpienia formularza

C(...) {...}

jest dokładnie równoważne

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

notatka końcowa

Zakres parametrów podanych przez parameter_list deklaracji konstruktora wystąpienia obejmuje inicjator konstruktora tej deklaracji. W związku z tym inicjator konstruktora może uzyskać dostęp do parametrów konstruktora.

Przykład:

class A
{
    public A(int x, int y) {}
}

class B: A
{
    public B(int x, int y) : base(x + y, x - y) {}
}

przykład końcowy

Inicjator konstruktora wystąpienia nie może uzyskać dostępu do tworzonego wystąpienia. W związku z tym jest to błąd czasu kompilacji, aby odwołać się do tego w wyrażeniu argumentu inicjatora konstruktora, ponieważ jest to błąd czasu kompilacji dla wyrażenia argumentu w celu odwołania się do dowolnego elementu członkowskiego wystąpienia za pośrednictwem simple_name.

Inicjatory zmiennych wystąpień 15.11.3

Gdy konstruktor wystąpienia nie ma inicjatora konstruktora lub ma inicjator konstruktora formularza base(...), konstruktor niejawnie wykonuje inicjalizacji określone przez variable_initializers pól wystąpienia zadeklarowanych w swojej klasie. Odpowiada to sekwencji przypisań, które są wykonywane natychmiast po wejściu do konstruktora i przed niejawnym wywołaniem bezpośredniego konstruktora klasy bazowej. Inicjatory zmiennych są wykonywane w kolejności tekstowej, w której są wyświetlane w deklaracji klasy (§15.5.6).

15.11.4 Wykonywanie konstruktora

Inicjatory zmiennych są przekształcane w instrukcje przypisania, a te instrukcje przypisania są wykonywane przed wywołaniem konstruktora wystąpienia klasy bazowej. Ta kolejność gwarantuje, że wszystkie pola wystąpień są inicjowane przez inicjatory zmiennych przed wykonaniem wszelkich instrukcji, które mają dostęp do tego wystąpienia.

Przykład: biorąc pod uwagę następujące kwestie:

class A
{
    public A()
    {
        PrintFields();
    }

    public virtual void PrintFields() {}
}
class B: A
{
    int x = 1;
    int y;

    public B()
    {
        y = -1;
    }

    public override void PrintFields() =>
        Console.WriteLine($"x = {x}, y = {y}");
}

gdy nowe B() jest używane do tworzenia wystąpienia Bprogramu , generowane są następujące dane wyjściowe:

x = 1, y = 0

Wartość to x 1, ponieważ inicjator zmiennej jest wykonywany przed wywołaniem konstruktora wystąpienia klasy bazowej. Jednak wartość y to 0 (wartość domyślna klasy int), ponieważ przypisanie do y nie jest wykonywane dopiero po powrocie konstruktora klasy bazowej. Warto traktować inicjatory zmiennych wystąpień i inicjatory konstruktorów jako instrukcje, które są automatycznie wstawiane przed constructor_body. Przykład

class A
{
    int x = 1, y = -1, count;

    public A()
    {
        count = 0;
    }

    public A(int n)
    {
        count = n;
    }
}

class B : A
{
    double sqrt2 = Math.Sqrt(2.0);
    ArrayList items = new ArrayList(100);
    int max;

    public B(): this(100)
    {
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        max = n;
    }
}

zawiera kilka inicjatorów zmiennych; zawiera również inicjatory konstruktorów obu formularzy (base i this). Przykład odpowiada kodowi pokazanym poniżej, gdzie każdy komentarz wskazuje automatycznie wstawioną instrukcję (składnia używana dla automatycznie wstawionych wywołań konstruktorów jest nieprawidłowa, ale służy tylko do zilustrowania mechanizmu).

class A
{
    int x, y, count;
    public A()
    {
        x = 1;      // Variable initializer
        y = -1;     // Variable initializer
        object();   // Invoke object() constructor
        count = 0;
    }

    public A(int n)
    {
        x = 1;      // Variable initializer
        y = -1;     // Variable initializer
        object();   // Invoke object() constructor
        count = n;
    }
}

class B : A
{
    double sqrt2;
    ArrayList items;
    int max;
    public B() : this(100)
    {
        B(100);                      // Invoke B(int) constructor
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        sqrt2 = Math.Sqrt(2.0);      // Variable initializer
        items = new ArrayList(100);  // Variable initializer
        A(n - 1);                    // Invoke A(int) constructor
        max = n;
    }
}

przykład końcowy

15.11.5 Konstruktory domyślne

Jeśli klasa nie zawiera deklaracji konstruktora wystąpienia, zostanie automatycznie udostępniony domyślny konstruktor wystąpienia. Ten domyślny konstruktor po prostu wywołuje konstruktor bezpośredniej klasy bazowej, jakby miał inicjator konstruktora formularza base(). Jeśli klasa jest abstrakcyjna, zadeklarowana dostępność dla konstruktora domyślnego jest chroniona. W przeciwnym razie zadeklarowane ułatwienia dostępu dla konstruktora domyślnego są publiczne.

Uwaga: W związku z tym domyślny konstruktor jest zawsze w formularzu

protected C(): base() {}

lub

public C(): base() {}

gdzie C to nazwa klasy.

notatka końcowa

Jeśli rozpoznawanie przeciążenia nie może określić unikatowego najlepszego kandydata do inicjatora konstruktora klasy bazowej, wystąpi błąd czasu kompilacji.

Przykład: w poniższym kodzie

class Message
{
    object sender;
    string text;
}

Jest dostarczany konstruktor domyślny, ponieważ klasa nie zawiera deklaracji konstruktora wystąpienia. W związku z tym przykład jest dokładnie odpowiednikiem

class Message
{
    object sender;
    string text;

    public Message() : base() {}
}

przykład końcowy

15.12 Konstruktory statyczne

Konstruktor statyczny jest elementem członkowskim, który implementuje akcje wymagane do zainicjowania zamkniętej klasy. Konstruktory statyczne są deklarowane przy użyciu static_constructor_declarations:

static_constructor_declaration
    : attributes? static_constructor_modifiers identifier '(' ')'
        static_constructor_body
    ;

static_constructor_modifiers
    : 'static'
    | 'static' 'extern' unsafe_modifier?
    | 'static' unsafe_modifier 'extern'?
    | 'extern' 'static' unsafe_modifier?
    | 'extern' unsafe_modifier 'static'
    | unsafe_modifier 'static' 'extern'?
    | unsafe_modifier 'extern' 'static'
    ;

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

unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).

Static_constructor_declaration może zawierać zestaw atrybutów(§22) i extern modyfikator (§15.6.8).

Identyfikator static_constructor_declaration określa klasę, w której zadeklarowany jest konstruktor statyczny. Jeśli zostanie określona inna nazwa, wystąpi błąd czasu kompilacji.

Gdy deklaracja konstruktora statycznego zawiera extern modyfikator, konstruktor statyczny jest podobno zewnętrznym konstruktorem statycznym. Ponieważ zewnętrzna deklaracja konstruktora statycznego nie zapewnia rzeczywistej implementacji, jego static_constructor_body składa się z średnika. W przypadku wszystkich innych deklaracji konstruktorów statycznych static_constructor_body składa się z jednego z następujących elementów:

  • blok, który określa instrukcje do wykonania w celu zainicjowania klasy; lub
  • treść wyrażenia, która składa się z => wyrażenia i średnika, i określa pojedyncze wyrażenie do wykonania w celu zainicjowania klasy.

Static_constructor_body, który jest treść bloku lub wyrażenia, odpowiada dokładnie method_body metody statycznej z typem zwrotnym void (§15.6.11).

Konstruktory statyczne nie są dziedziczone i nie mogą być wywoływane bezpośrednio.

Konstruktor statyczny dla zamkniętej klasy jest wykonywany co najwyżej raz w danej domenie aplikacji. Wykonanie konstruktora statycznego jest wyzwalane przez pierwsze z następujących zdarzeń w domenie aplikacji:

  • Zostanie utworzone wystąpienie klasy.
  • Odwołano się do dowolnych statycznych elementów członkowskich klasy.

Jeśli klasa zawiera Main metodę (§7.1), w której rozpoczyna się wykonywanie, konstruktor statyczny dla tej klasy jest wykonywany przed Main wywołaniem metody.

Aby zainicjować nowy typ zamkniętej klasy, najpierw zostanie utworzony nowy zestaw pól statycznych (§15.5.2) dla tego określonego typu zamkniętego. Każde z pól statycznych jest inicjowane do wartości domyślnej (§15.5.5). Następnie dla tych pól statycznych są wykonywane inicjatory pól statycznych (§15.5.6.2). Na koniec jest wykonywany konstruktor statyczny.

Przykład: przykład

class Test
{
    static void Main()
    {
        A.F();
        B.F();
    }
}

class A
{
    static A()
    {
        Console.WriteLine("Init A");
    }

    public static void F()
    {
        Console.WriteLine("A.F");
    }
}

class B
{
    static B()
    {
        Console.WriteLine("Init B");
    }

    public static void F()
    {
        Console.WriteLine("B.F");
    }
}

musi wygenerować dane wyjściowe:

Init A
A.F
Init B
B.F

ponieważ wykonanie Astatycznego konstruktora jest wyzwalane przez wywołanie A.Fmetody , a wykonanie Bkonstruktora statycznego jest wyzwalane przez wywołanie metody B.F.

przykład końcowy

Istnieje możliwość konstruowania zależności cyklicznych, które umożliwiają obserwowanie pól statycznych z inicjatorami zmiennych w ich domyślnym stanie wartości.

Przykład: przykład

class A
{
    public static int X;

    static A()
    {
        X = B.Y + 1;
    }
}

class B
{
    public static int Y = A.X + 1;

    static B() {}

    static void Main()
    {
        Console.WriteLine($"X = {A.X}, Y = {B.Y}");
    }
}

generuje dane wyjściowe

X = 1, Y = 2

Aby wykonać metodę Main , system najpierw uruchamia inicjator dla B.Yklasy , przed konstruktorem statycznym klasy B. YInicjator 's powoduje Auruchomienie konstruktora static , ponieważ wartość A.X jest przywołynięta. Konstruktor statyczny A z kolei przechodzi do obliczenia wartości X, a w ten sposób pobiera wartość Ydomyślną , która jest równa zero. A.X jest zatem inicjowany do 1. Następnie proces uruchamiania Ainicjatorów pól statycznych i konstruktora statycznego kończy się, wracając do obliczenia początkowej Ywartości , którego wynik staje się 2.

przykład końcowy

Ponieważ konstruktor statyczny jest wykonywany dokładnie raz dla każdego zamkniętego typu klasy skonstruowanej, jest to wygodne miejsce do wymuszania kontroli czasu wykonywania dla parametru typu, którego nie można sprawdzić w czasie kompilacji za pośrednictwem ograniczeń (§15.2.5).

Przykład: Następujący typ używa konstruktora statycznego, aby wymusić, że argument typu jest wyliczeniem:

class Gen<T> where T : struct
{
    static Gen()
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("T must be an enum");
        }
    }
}

przykład końcowy

15.13 Finalizatory

Uwaga: We wcześniejszej wersji tej specyfikacji, co jest teraz nazywane "finalizatorem" było nazywane "destruktorem". Doświadczenie wykazało, że termin "destruktor" spowodował zamieszanie i często wynikał z nieprawidłowych oczekiwań, zwłaszcza dla programistów znających język C++. W języku C++destruktor jest wywoływany w sposób determinat, natomiast w języku C#finalizator nie jest. Aby uzyskać określenie zachowania z języka C#, należy użyć polecenia Dispose. notatka końcowa

Finalizator to element członkowski, który implementuje akcje wymagane do sfinalizowania wystąpienia klasy. Finalizator jest deklarowany przy użyciu finalizer_declaration:

finalizer_declaration
    : attributes? '~' identifier '(' ')' finalizer_body
    | attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
      finalizer_body
    | attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
      finalizer_body
    ;

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

unsafe_modifier (§23.2) jest dostępny tylko w niebezpiecznym kodzie (§23).

Finalizer_declaration może zawierać zestaw atrybutów(§22).

Identyfikator finalizer_declarator określa klasę, w której zadeklarowany jest finalizator. Jeśli zostanie określona inna nazwa, wystąpi błąd czasu kompilacji.

Gdy deklaracja finalizatora zawiera extern modyfikator, finalizator mówi się, że jest zewnętrznym finalizatorem. Ponieważ zewnętrzna deklaracja finalizatora nie zawiera rzeczywistej implementacji, jego finalizer_body składa się z średnika. W przypadku wszystkich innych finalizatorów finalizer_body składa się z jednego z następujących elementów:

  • blok, który określa instrukcje do wykonania w celu sfinalizowania wystąpienia klasy.
  • lub treść wyrażenia, która składa się z => wyrażenia i średnika, i określa pojedyncze wyrażenie do wykonania w celu sfinalizowania wystąpienia klasy.

Finalizer_body, który jest treść bloku lub wyrażenia, odpowiada dokładnie method_body metody wystąpienia z typem zwracanym void (§15.6.11).

Finalizatory nie są dziedziczone. W związku z tym klasa nie ma finalizatorów innych niż ta, która może być zadeklarowana w tej klasie.

Uwaga: ponieważ finalizator jest wymagany do braku parametrów, nie można go przeciążyć, więc klasa może mieć co najwyżej jeden finalizator. notatka końcowa

Finalizatory są wywoływane automatycznie i nie można ich jawnie wywołać. Wystąpienie kwalifikuje się do finalizacji, gdy nie będzie już możliwe użycie jakiegokolwiek kodu. Wykonanie finalizatora wystąpienia może nastąpić w dowolnym momencie po tym, jak wystąpienie kwalifikuje się do finalizacji (§7.9). Po sfinalizowaniu wystąpienia finalizatory w łańcuchu dziedziczenia tego wystąpienia są wywoływane w kolejności od większości pochodnych do najmniej pochodnych. Finalizator może być wykonywany w dowolnym wątku. Aby uzyskać dalszą dyskusję na temat zasad, które określają, kiedy i jak jest wykonywany finalizator, zobacz §7.9.

Przykład: dane wyjściowe przykładu

class A
{
    ~A()
    {
        Console.WriteLine("A's finalizer");
    }
}

class B : A
{
    ~B()
    {
        Console.WriteLine("B's finalizer");
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

is

B's finalizer
A's finalizer

ponieważ finalizatory w łańcuchu dziedziczenia są wywoływane w kolejności, z większości pochodnych do najmniej pochodnych.

przykład końcowy

Finalizatory są implementowane przez zastąpienie metody Finalize wirtualnej w pliku System.Object. Programy języka C# nie mogą zastąpić tej metody ani bezpośrednio ją wywołać (lub zastąpić).

Przykład: na przykład program

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

zawiera dwa błędy.

przykład końcowy

Kompilator powinien zachowywać się tak, jakby ta metoda oraz jej nadpisania w ogóle nie istniały.

Przykład: W związku z tym ten program:

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

jest prawidłowa, a pokazana metoda ukrywa System.Objectmetodę Finalize .

przykład końcowy

Aby zapoznać się z omówieniem zachowania podczas zgłaszania wyjątku z finalizatora, zobacz §21.4.

15.14 Iteratory

15.14.1 Ogólne

Element członkowski funkcji (§12.6) implementowany przy użyciu bloku iteratora (§13.3) jest nazywany iteratorem.

Blok iteratora może być używany jako treść składowej funkcji, o ile zwracany typ odpowiadającego mu elementu członkowskiego funkcji jest jednym z interfejsów modułu wyliczającego (§15.14.2) lub jednego z interfejsów wyliczalnych (§15.14.3). Może wystąpić jako method_body, operator_body lub accessor_body, natomiast zdarzenia, konstruktory wystąpień, konstruktory statyczne i finalizatory nie są implementowane jako iteratory.

Gdy element członkowski funkcji jest implementowany przy użyciu bloku iteratora, jest to błąd czasu kompilacji dla listy parametrów elementu członkowskiego funkcji w celu określenia dowolnych inparametrów , , outlub ref parametru ref struct typu.

Interfejsy modułu wyliczającego 15.14.2

Interfejsy modułu wyliczającego to interfejs System.Collections.IEnumerator niegeneryczny i wszystkie wystąpienia interfejsu System.Collections.Generic.IEnumerator<T>ogólnego . Ze względu na zwięzłość, w tym podklasie i jego elementy równorzędne te interfejsy są przywołyzane odpowiednio jako IEnumerator i IEnumerator<T>.

15.14.3 Interfejsy wyliczalne

Interfejsy wyliczalne to interfejs System.Collections.IEnumerable niegeneryczny i wszystkie wystąpienia interfejsu System.Collections.Generic.IEnumerable<T>ogólnego . Ze względu na zwięzłość, w tym podklasie i jego elementy równorzędne te interfejsy są przywołyzane odpowiednio jako IEnumerable i IEnumerable<T>.

15.14.4 Typ wydajności

Iterator tworzy sekwencję wartości— wszystkie tego samego typu. Ten typ jest nazywany typem wydajności iteratora.

  • Typ wydajności iteratora, który zwraca IEnumerator wartość lub IEnumerable ma wartość object.
  • Typ wydajności iteratora, który zwraca IEnumerator<T> wartość lub IEnumerable<T> ma wartość T.

15.14.5 Obiekty modułu wyliczającego

15.14.5.1 Ogólne

Gdy element członkowski funkcji zwracający typ interfejsu modułu wyliczającego jest implementowany przy użyciu bloku iteratora, wywołanie elementu członkowskiego funkcji nie powoduje natychmiastowego wykonania kodu w bloku iteratora. Zamiast tego obiekt modułu wyliczającego jest tworzony i zwracany. Ten obiekt hermetyzuje kod określony w bloku iteratora, a wykonanie kodu w bloku iteratora ma miejsce, gdy wywoływana MoveNext jest metoda obiektu wyliczającego. Obiekt modułu wyliczającego ma następujące cechy:

  • Implementuje IEnumerator i IEnumerator<T>, gdzie T jest typem wydajności iteratora.
  • Implementuje System.IDisposable.
  • Jest inicjowany przy użyciu kopii wartości argumentu (jeśli istnieje) i wartości wystąpienia przekazanej do elementu członkowskiego funkcji.
  • Ma cztery potencjalne stany, przed, uruchomione, zawieszone i po, i początkowo jest w stanie poprzedzającym.

Obiekt modułu wyliczającego jest zazwyczaj wystąpieniem klasy modułu wyliczającego generowanego przez kompilator, który hermetyzuje kod w bloku iteratora i implementuje interfejsy modułu wyliczającego, ale możliwe są inne metody implementacji. Jeśli klasa modułu wyliczającego jest generowana przez kompilator, ta klasa zostanie zagnieżdżona bezpośrednio lub pośrednio w klasie zawierającej składową funkcji, będzie mieć dostęp prywatny i będzie mieć nazwę zarezerwowaną do użycia kompilatora (§6.4.3).

Obiekt modułu wyliczającego może implementować więcej interfejsów niż określone powyżej.

W poniższych podkatazach opisano wymagane zachowanie MoveNextimplementacji interfejsu Currenti Dispose , i IEnumeratorIEnumerator<T> dostarczonych przez obiekt modułu wyliczającego.

Obiekty modułu IEnumerator.Reset wyliczającego nie obsługują metody . Wywołanie tej metody powoduje zgłoszenie elementu System.NotSupportedException .

15.14.5.2 Metoda MoveNext

Metoda MoveNext obiektu wyliczającego hermetyzuje kod bloku iteratora. Wywołanie MoveNext metody powoduje wykonanie kodu w bloku iteratora i ustawienie Current właściwości obiektu modułu wyliczającego zgodnie z potrzebami. Dokładna akcja wykonywana przez MoveNext program zależy od stanu obiektu wyliczającego, gdy MoveNext jest wywoływany:

  • Jeśli stan obiektu modułu wyliczającego jest wcześniej, wywołanie metody MoveNext:
    • Zmienia stan na uruchomiony.
    • Inicjuje parametry (w tym this) bloku iteratora do wartości argumentu i wartości wystąpienia zapisanej podczas inicjowania obiektu modułu wyliczającego.
    • Wykonuje blok iteratora od początku do momentu przerwania wykonywania (zgodnie z poniższym opisem).
  • Jeśli stan obiektu modułu wyliczającego jest uruchomiony, wynik wywołania MoveNext jest nieokreślony.
  • Jeśli stan obiektu modułu wyliczającego jest zawieszony, wywołanie metody MoveNext:
    • Zmienia stan na uruchomiony.
    • Przywraca wartości wszystkich zmiennych lokalnych i parametrów (w tym this) do wartości zapisanych podczas ostatniego zawieszenia wykonywania bloku iteratora.

      Uwaga: zawartość wszystkich obiektów, do których odwołuje się te zmienne, mogła ulec zmianie od poprzedniego wywołania metody MoveNext. notatka końcowa

    • Wznawia wykonywanie bloku iteratora bezpośrednio po instrukcji zwrotu wydajności, która spowodowała zawieszenie wykonywania i trwa do momentu przerwania wykonywania (zgodnie z poniższym opisem).
  • Jeśli stan obiektu modułu wyliczającego jest następujący, wywołanie MoveNext zwraca wartość false.

Po MoveNext wykonaniu bloku iteratora wykonywanie może zostać przerwane na cztery sposoby: przez instrukcję, instrukcję yield returnyield break , napotkając koniec bloku iteratora, a także przez zgłoszenie i propagację wyjątku z bloku iteratora.

  • yield return Po napotkaniu instrukcji (§9.4.4.20):
    • Wyrażenie podane w instrukcji jest obliczane, niejawnie konwertowane na typ plonu i przypisywane do Current właściwości obiektu wyliczającego.
    • Wykonanie treści iteratora jest zawieszone. Wartości wszystkich zmiennych lokalnych i parametrów (w tym this) są zapisywane, podobnie jak lokalizacja tej yield return instrukcji. yield return Jeśli instrukcja znajduje się w co najmniej jednym try bloku, skojarzone bloki ostatecznie niewykonywane w tej chwili.
    • Stan obiektu modułu wyliczającego jest zmieniany na zawieszony.
    • Metoda MoveNext zwraca true element wywołujący, wskazując, że iteracja została pomyślnie zaawansowana do następnej wartości.
  • yield break Po napotkaniu instrukcji (§9.4.4.20):
    • yield break Jeśli instrukcja znajduje się w co najmniej jednym try bloku, skojarzone finally bloki są wykonywane.
    • Stan obiektu modułu wyliczającego jest zmieniany na później.
    • Metoda MoveNext zwraca false element wywołujący, wskazując, że iteracja została ukończona.
  • Po napotkaniu końca treści iteratora:
    • Stan obiektu modułu wyliczającego jest zmieniany na później.
    • Metoda MoveNext zwraca false element wywołujący, wskazując, że iteracja została ukończona.
  • Gdy wyjątek jest zgłaszany i propagowany z bloku iteratora:
    • Odpowiednie finally bloki w treści iteratora zostaną wykonane przez propagację wyjątku.
    • Stan obiektu modułu wyliczającego jest zmieniany na później.
    • Propagacja wyjątku nadal wywołuje metodę MoveNext .

15.14.5.3 Bieżąca właściwość

Właściwość obiektu modułu Current wyliczającego ma wpływ na yield return instrukcje w bloku iteratora.

Gdy obiekt modułu wyliczającego znajduje się w stanie wstrzymania, wartość Current jest wartością ustawioną przez poprzednie wywołanie metody .MoveNext Gdy obiekt modułu wyliczającego znajduje się w przed, uruchomieniu lub po stanach, wynik uzyskiwania dostępu Current jest nieokreślony.

W przypadku iteratora o typie wydajności innym niż objectwynik uzyskiwania dostępu Current za pośrednictwem implementacji obiektu modułu wyliczającego odpowiada dostępowi za pośrednictwem implementacji obiektu IEnumerableCurrent wyliczającego i rzutowania IEnumerator<T> wyniku na objectwartość .

15.14.5.4 Metoda Dispose

Metoda Dispose służy do czyszczenia iteracji przez przeniesienie obiektu modułu wyliczającego do stanu after .

  • Jeśli stan obiektu modułu wyliczającego jest wcześniej, wywołanie Dispose zmienia stan na po.
  • Jeśli stan obiektu modułu wyliczającego jest uruchomiony, wynik wywołania Dispose jest nieokreślony.
  • Jeśli stan obiektu modułu wyliczającego jest zawieszony, wywołanie metody Dispose:
    • Zmienia stan na uruchomiony.
    • Wykonuje wszystkie bloki w końcu tak, jakby ostatnia wykonana yield return instrukcja była instrukcją yield break . Jeśli spowoduje to zgłoszenie wyjątku i propagację z treści iteratora, stan obiektu modułu wyliczającego jest ustawiony na wartość after , a wyjątek jest propagowany do obiektu wywołującego Dispose metody.
    • Zmienia stan na after.
  • Jeśli stan obiektu modułu wyliczającego jest po, wywołanie Dispose nie ma wpływu.

15.14.6 Obiekty wyliczalne

15.14.6.1 Ogólne

Gdy element członkowski funkcji zwracający typ interfejsu wyliczalnego jest implementowany przy użyciu bloku iteratora, wywołanie elementu członkowskiego funkcji nie powoduje natychmiastowego wykonania kodu w bloku iteratora. Zamiast tego jest tworzony i zwracany obiekt wyliczalny. Metoda obiektu GetEnumerator wyliczalnego zwraca obiekt wyliczający, który hermetyzuje kod określony w bloku iteratora, a wykonanie kodu w bloku iteratora następuje po wywołaniu metody obiektu MoveNext wyliczającego. Obiekt wyliczalny ma następujące cechy:

  • Implementuje IEnumerable i IEnumerable<T>, gdzie T jest typem wydajności iteratora.
  • Jest inicjowany przy użyciu kopii wartości argumentu (jeśli istnieje) i wartości wystąpienia przekazanej do elementu członkowskiego funkcji.

Obiekt wyliczalny jest zazwyczaj wystąpieniem klasy wyliczalnej generowanej przez kompilator, która hermetyzuje kod w bloku iteratora i implementuje interfejsy wyliczalne, ale możliwe są inne metody implementacji. Jeśli klasa wyliczalna jest generowana przez kompilator, ta klasa zostanie zagnieżdżona bezpośrednio lub pośrednio w klasie zawierającej składową funkcji, będzie mieć dostęp prywatny i będzie mieć nazwę zarezerwowaną do użycia kompilatora (§6.4.3).

Obiekt wyliczalny może implementować więcej interfejsów niż określone powyżej.

Uwaga: na przykład obiekt wyliczalny może również implementować IEnumerator i IEnumerator<T>, umożliwiając mu obsługę zarówno jako wyliczania, jak i modułu wyliczającego. Zazwyczaj taka implementacja zwróci własne wystąpienie (aby zapisać alokacje) z pierwszego wywołania metody .GetEnumerator Kolejne wywołania GetEnumeratorklasy , jeśli istnieją, zwracają nowe wystąpienie klasy, zazwyczaj tej samej klasy, tak aby wywołania do różnych wystąpień modułu wyliczającego nie miały wpływu na siebie. Nie może zwrócić tego samego wystąpienia, nawet jeśli poprzedni moduł wyliczający wyliczył już koniec sekwencji, ponieważ wszystkie przyszłe wywołania modułu wyliczającego muszą zgłaszać wyjątki. notatka końcowa

15.14.6.2 Metoda Getenumerator

Obiekt wyliczalny zapewnia implementację GetEnumerator metod interfejsów IEnumerable i IEnumerable<T> . GetEnumerator Dwie metody współdzielą wspólną implementację, która uzyskuje i zwraca dostępny obiekt modułu wyliczającego. Obiekt modułu wyliczającego jest inicjowany przy użyciu wartości argumentu i wartości wystąpienia zapisanej podczas inicjowania obiektu wyliczalnego, ale w przeciwnym razie obiekt wyliczający działa zgodnie z opisem w §15.14.5.

15.15 Funkcje asynchroniczne

15.15.1 Ogólne

Metoda (§15.6) lub funkcja anonimowa (§12.19) z modyfikatorem jest nazywana funkcjąasync. Ogólnie rzecz biorąc, termin async służy do opisywania dowolnego rodzaju funkcji, która ma async modyfikator.

Jest to błąd czasu kompilacji dla listy parametrów funkcji asynchronicznych w celu określenia dowolnych inparametrów , , outlub ref dowolnego parametru ref struct typu.

Return_type metody asynchronicznej to void albo typ zadania. W przypadku metody asynchronicznej, która generuje wartość wynikową, typ zadania jest ogólny. W przypadku metody asynchronicznej, która nie generuje wartości wynikowej, typ zadania nie jest ogólny. Takie typy są określane odpowiednio w tej specyfikacji jako «TaskType»<T> i «TaskType». Typ System.Threading.Tasks.Task biblioteki standardowej i typy tworzone z System.Threading.Tasks.Task<TResult> programu to typy zadań, a także klasa, struktura lub typ interfejsu skojarzony z typem konstruktora zadań za pośrednictwem atrybutu .System.Runtime.CompilerServices.AsyncMethodBuilderAttribute Takie typy są określane w tej specyfikacji jako «TaskBuilderType»<T> i «TaskBuilderType». Typ zadania może mieć co najwyżej jeden parametr typu i nie można go zagnieżdżać w typie ogólnym.

Mówi się , że metoda asynchronizna zwracająca typ zadania jest zwracana.

Typy zadań mogą się różnić w ich dokładnej definicji, ale z punktu widzenia języka typ zadania jest w jednym ze stanów niekompletnych, zakończonych powodzeniem lub błędami. Uszkodzone zadanie rejestruje istotne wyjątki. Powodzenie «TaskType»<T>T Typy zadań są oczekiwane, a zadania mogą zatem być operandami wyrażeń await (§12.9.8).

Przykład: typ MyTask<T> zadania jest skojarzony z typem konstruktora zadań i typem MyTaskMethodBuilder<T>Awaiter<T>awaiter:

using System.Runtime.CompilerServices; 
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
class MyTask<T>
{
    public Awaiter<T> GetAwaiter() { ... }
}

class Awaiter<T> : INotifyCompletion
{
    public void OnCompleted(Action completion) { ... }
    public bool IsCompleted { get; }
    public T GetResult() { ... }
}

przykład końcowy

Typ konstruktora zadań to klasa lub typ struktury, który odpowiada określonemu typowi zadania (§15.15.2). Typ konstruktora zadań dokładnie odpowiada zadeklarowanej dostępności odpowiadającego mu typu zadania.

Uwaga: Jeśli typ zadania jest zadeklarowany internal, odpowiedni typ konstruktora musi być również zadeklarowany internal i zdefiniowany w tym samym zestawie. Jeśli typ zadania jest zagnieżdżony wewnątrz innego typu, typ buider zadania musi być również zagnieżdżony w tym samym typie. notatka końcowa

Funkcja asynchroniowa ma możliwość wstrzymania oceny za pomocą wyrażeń await (§12.9.8) w treści. Ocena może zostać później wznowiona w momencie wstrzymania wyrażenia await za pomocą delegata wznowienia. Delegat wznowienia jest typu System.Action, a po wywołaniu ocena wywołania funkcji asynchronicznych zostanie wznowiona z wyrażenia await, w którym została przerwana. Bieżący obiekt wywołujący wywołanie funkcji asynchronicznych jest oryginalnym obiektem wywołującym, jeśli wywołanie funkcji nigdy nie zostało zawieszone lub ostatnio wywołujący delegat wznowienia w przeciwnym razie.

15.15.2 Wzorzec konstruktora typów zadań

Typ konstruktora zadań może mieć co najwyżej jeden parametr typu i nie można go zagnieżdżać w typie ogólnym. Typ konstruktora zadań musi mieć następujące elementy członkowskie (w przypadku typów konstruktorów zadań innych niż ogólne, SetResult nie mają parametrów) z zadeklarowaną public dostępnością:

class «TaskBuilderType»<T>
{
    public static «TaskBuilderType»<T> Create();
    public void Start<TStateMachine>(ref TStateMachine stateMachine)
                where TStateMachine : IAsyncStateMachine;
    public void SetStateMachine(IAsyncStateMachine stateMachine);
    public void SetException(Exception exception);
    public void SetResult(T result);
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public «TaskType»<T> Task { get; }
}

Kompilator generuje kod, który używa "TaskBuilderType" do implementowania semantyki zawieszania i wznawiania oceny funkcji asynchronicznej. Kompilator używa "TaskBuilderType" w następujący sposób:

  • «TaskBuilderType».Create() Element jest wywoływany w celu utworzenia wystąpienia obiektu «TaskBuilderType», o nazwie na builder tej liście.
  • builder.Start(ref stateMachine)jest wywoływany w celu skojarzenia konstruktora z wystąpieniem maszyny stanu wygenerowanej przez kompilator. stateMachine
    • Budowniczy powinien zadzwonić stateMachine.MoveNext() do Start() maszyny państwowej lub po Start() jej powrocie.
  • Po Start() powrocie asyncbuilder.Task metoda wywołuje zadanie, które ma wrócić z metody asynchronicznej.
  • Każde wywołanie metody spowoduje stateMachine.MoveNext() przejście maszyny stanu.
  • Jeśli maszyna stanu zakończy się pomyślnie, builder.SetResult() zostanie wywołana z wartością zwracaną przez metodę , jeśli istnieje.
  • W przeciwnym razie, jeśli wyjątek jest e zgłaszany na maszynie stanu, builder.SetException(e) jest wywoływany.
  • Jeśli maszyna stanu osiągnie await expr wyrażenie, expr.GetAwaiter() jest wywoływana.
  • Jeśli program awaiter implementuje ICriticalNotifyCompletion wartość i IsCompleted ma wartość false, maszyna stanu wywołuje metodę builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine).
    • AwaitUnsafeOnCompleted() element powinien wywołać awaiter.UnsafeOnCompleted(action) metodę , która Action wywołuje stateMachine.MoveNext() polecenie po zakończeniu funkcji awaiter.
  • W przeciwnym razie maszyna stanu wywołuje element builder.AwaitOnCompleted(ref awaiter, ref stateMachine).
    • AwaitOnCompleted() element powinien wywołać awaiter.OnCompleted(action) metodę , która Action wywołuje stateMachine.MoveNext() polecenie po zakończeniu funkcji awaiter.
  • SetStateMachine(IAsyncStateMachine) może być wywoływana przez implementację wygenerowaną IAsyncStateMachine przez kompilator w celu zidentyfikowania wystąpienia konstruktora skojarzonego z wystąpieniem maszyny stanu, szczególnie w przypadkach, gdy maszyna stanu jest implementowana jako typ wartości.
    • Jeśli konstruktor wywoła stateMachine.SetStateMachine(stateMachine)metodę , stateMachine wywoła wystąpienie builder.SetStateMachine(stateMachine) konstruktora skojarzone z elementemstateMachine.

Uwaga: dla parametrów SetResult(T result) i «TaskType»<T> Task { get; }, parametr i argument muszą być odpowiednio konwertowane Tna wartość . Umożliwia to konstruktorowi typów zadań obsługę typów, takich jak krotki, w których dwa typy, które nie są takie same, to tożsamość konwertowana. notatka końcowa

15.15.3 Ocena funkcji asynchronicznego zwracającej zadanie

Wywołanie funkcji asynchronicznego zwracanej przez zadanie powoduje wygenerowanie wystąpienia zwracanego typu zadania. Jest to nazywane zadaniem zwrotnym funkcji asynchronicznego. Zadanie jest początkowo w stanie niekompletnym.

Treść funkcji asynchronicznych jest następnie oceniana do momentu wstrzymania (przez osiągnięcie wyrażenia await) lub zakończenia, w którym kontrolka punktu jest zwracana do wywołującego wraz z zadaniem zwrotnym.

Gdy treść funkcji asynchronicznych zakończy działanie, zadanie zwracane zostanie przeniesione z niekompletnego stanu:

  • Jeśli treść funkcji kończy się w wyniku osiągnięcia instrukcji return lub końca treści, każda wartość wyniku jest rejestrowana w zadaniu zwrotnym, który jest umieszczany w stanie powodzenia.
  • Jeśli treść funkcji kończy się z powodu nieuchwyconego OperationCanceledExceptionelementu , wyjątek jest rejestrowany w zadaniu zwrotnym, które jest umieszczane w stanie anulowanym .
  • Jeśli treść funkcji zakończy się w wyniku jakichkolwiek innych nieuchwyconych wyjątków (§13.10.6), wyjątek jest rejestrowany w zadaniu zwrotnym, które jest umieszczane w stanie błędu.

15.15.4 Ocena funkcji asynchronicznego zwracającej pustkę

Jeśli zwracany typ funkcji asynchronizowanej to void, ocena różni się od powyższego w następujący sposób: ponieważ żadne zadanie nie jest zwracane, funkcja przekazuje uzupełnianie i wyjątki kontekstowi synchronizacji bieżącego wątku. Dokładna definicja kontekstu synchronizacji jest zależna od implementacji, ale jest reprezentacją "gdzie" bieżący wątek jest uruchomiony. Kontekst synchronizacji jest powiadamiany, gdy ocena voidfunkcji asynchronicznego rozpoczyna się, kończy się pomyślnie lub powoduje zgłoszenie nieuchwyconego wyjątku.

Dzięki temu kontekst może śledzić liczbę voiduruchomionych w nim funkcji asynchronicznych i zdecydować, jak propagować wyjątki wychodzące z nich.