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óra deklaruje nową klasę.

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

Deklaracja klasy (class_declaration) składa się z opcjonalnego zestawu atrybutów (§22), a następnie opcjonalnego zestawu modyfikatorów klasy (class_modifier) (§15.2.2), po którym opcjonalnie występuje modyfikator (§15.2.7), po którym następuje słowo kluczowe i identyfikator , który nadaje nazwę klasie, a następnie opcjonalna lista parametrów typu (§15.2.3), po której następuje opcjonalna specyfikacja bazowa klasy (§15.2.4), a następnie opcjonalny zestaw klauzul ograniczeń parametrów typu (§15.2.5), po którym następuje ciało klasy (§15.2.6), opcjonalnie zakończone średnikiem.

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

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

Modyfikatory klas 15.2.2

15.2.2.1 Ogólne

Deklaracja klasy może opcjonalnie zawierać sekwencję modyfikatorów klasy:

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

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

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

Modyfikator new jest dozwolony w klasach zagnieżdżonych. Określa, że klasa ukrywa dziedziczony element o tej samej nazwie, jak opisano w §15.3.5. Jest to błąd czasu kompilacji, gdy modyfikator new pojawia się w deklaracji klasy, która nie jest deklaracją zagnieżdżonej klasy.

Modyfikatory public, 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).

Jeśli częściowa deklaracja typu (§15.2.7) zawiera specyfikację dostępności (za pośrednictwem modyfikatorów public, protected, internal i private), specyfikacja ta powinna być zgodna ze wszystkimi innymi częściami, które zawierają specyfikację dostępności. Jeśli żadna część częściowego typu nie zawiera specyfikacji ułatwień dostępu, typ otrzymuje odpowiednie domyślne ułatwienia dostępu (§7.5.2).

Modyfikatory abstract, 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, a użycie operatora new w klasie abstrakcyjnej stanowi błąd podczas kompilacji. Chociaż możliwe jest posiadanie zmiennych i wartości, których typy czasu kompilacji są abstrakcyjne, takie zmienne i wartości muszą być null albo zawierać odwołania do wystąpień klas nie abstrakcyjnych pochodzących z typów abstrakcyjnych.
  • Klasa abstrakcyjna może (ale nie musi) zawierać abstrakcyjnych członków.
  • Nie można zapieczętować klasy abstrakcyjnej.

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

Przykład: w poniższym kodzie

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

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

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

klasa A abstrakcyjna wprowadza metodę Fabstrakcyjną . Klasa B wprowadza dodatkową metodę G, ale ponieważ nie zapewnia implementacji F, B również jest zadeklarowana abstrakcyjnie. Klasa C przesłania F i zapewnia rzeczywistą implementację. Ponieważ w C nie ma członów abstrakcyjnych, C może być nieabstrakcyjny (ale nie jest to wymagane).

przykład końcowy

Jeśli jedna lub więcej części deklaracji częściowego typu (§15.2.7) klasy zawiera modyfikator abstract, klasa jest abstrakcyjna. W przeciwnym razie klasa nie jest abstrakcyjna.

15.2.2.3 Klasy zapieczętowane

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

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

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

Jeśli co najmniej jedna część częściowej deklaracji typu (§15.2.7) klasy zawiera modyfikator sealed, klasa jest zamknięta. W przeciwnym razie klasa jest niezapieczętowana.

15.2.2.4 Klasy statyczne

15.2.2.4.1 Ogólne

Modyfikator static służy do oznaczania klasy zadeklarowanej jako klasy statycznej. Klasy statycznej nie należy instancjonować, nie należy używać jej jako typu i powinna zawierać tylko statyczne elementy. Tylko klasa statyczna może zawierać deklaracje metod rozszerzenia (§15.6.10).

Deklaracja klasy statycznej podlega następującym ograniczeniom:

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

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

  • Klasa statyczna nie powinna mieć członków z określoną widocznością protected, private protected ani protected internal.

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

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

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

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

15.2.2.4.2 Odwoływanie się do typów klas statycznych

namespace_or_type_name (§7.8) może odnosić się do klasy statycznej, jeśli

  • Namespace_or_type_name jest T w namespace_or_type_name w postaci T.I lub...
  • Nazwa namespace_or_type-name jest T w typeof_expression (§12.8.18) w postaci typeof(T).

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

  • primary_expression jest E w member_access (§12.8.7) w formie E.I.

W każdym innym kontekście to błąd czasu kompilacji odwołać się do klasy statycznej.

Uwaga: Na przykład, błędem jest użycie klasy statycznej jako klasy bazowej, składnika typu (§15.3.7) członka, argumentu typu generycznego lub ograniczenia dla parametru typu. Podobnie, nie można używać klasy statycznej w typie tablicy, nowym wyrażeniu, wyrażeniu rzutowym, wyrażeniu is, wyrażeniu as, wyrażeniu sizeof, lub wyrażeniu wartości domyślnej. notatka końcowa

15.2.3 Parametry typu

Parametr typu to prosty identyfikator, który określa symbol zastępczy argumentu typu dostarczonego w celu utworzenia skonstruowanego typu. W przeciwieństwie, argument typu (§8.4.2) jest typem, który jest podstawiany za parametr typu podczas tworzenia skonstruowanego typu.

type_parameter_list
    : '<' decorated_type_parameter (',' decorated_type_parameter)* '>'
    ;

decorated_type_parameter
    : attributes? type_parameter
    ;

type_parameter jest zdefiniowany w §8.5.

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

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

15.2.4 Specyfikacja podstawowa klasy

15.2.4.1 Ogólne

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

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

interface_type_list
    : interface_type (',' interface_type)*
    ;

15.2.4.2 Klasy bazowe

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

Przykład: w poniższym kodzie

class A {}
class B : A {}

Mówi się, że klasa A jest bezpośrednią klasą bazową 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żdego type_parameter w deklaracji klasy bazowej, odpowiadającego type_argument typu skonstruowanego.

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

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

klasa bazowa skonstruowanego typu G<int> 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 danej klasy nie może być żadnym z następujących typów: System.Array, System.Delegate, System.Enum, System.ValueType lub typu dynamic. Ponadto deklaracja klasy ogólnej nie może być używana System.Attribute jako bezpośrednia lub pośrednia klasa bazowa (§22.2.1).

Podczas określania znaczenia bezpośredniej specyfikacji klasy bazowej A dla klasy B, tymczasowo przyjmuje się, że bezpośrednia klasa bazowa B to object, co gwarantuje, że znaczenie specyfikacji klasy bazowej nie może zależeć rekursywnie od siebie.

Przykład: następujące

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

class Z : X<Z.Y> {}

występuje błąd, ponieważ w specyfikacji klasy bazowej bezpośrednia klasa bazowa jest uważana za , a zatem (zgodnie z regułami §7.8) nie jest uważana za składową .

przykład końcowy

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

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

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

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

przykład końcowy

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

Błąd czasu kompilacji występuje, gdy klasa zależy od siebie samej. W celu tej reguły klasa zależy bezpośrednio od jej bezpośredniej klasy bazowej (jeśli istnieje) i bezpośrednio zależy od najbliższej otaczającej klasy, w której jest zagnieżdżona (jeśli istnieje). Biorąc pod uwagę tę definicję, pełny zestaw klas, od których zależy klasa, to przejściowe domknięcie relacji bezpośredniego zależności od.

Przykład: przykład

class A : A {}

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

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

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

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

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

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 dziedziczyć po zapieczętowanej klasie.

Przykład: w poniższym kodzie

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

Klasa B jest w błędzie, ponieważ próbuje pochodzić z zapieczętowanej 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

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

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

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

przykład końcowy

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

Ograniczenia parametrów typu 15.2.5

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

type_parameter_constraints_clause
    : 'where' type_parameter ':' type_parameter_constraints
    ;

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

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

secondary_constraint
    : interface_type nullable_type_annotation?
    | type_parameter nullable_type_annotation?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

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

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

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

Ograniczenie podstawowe może być typem klasy, ograniczeniem rodzaju referencyjnego, ograniczeniem rodzaju wartościowego, ograniczeniem na brak wartości null lub ograniczeniem na typy niezarządzane. Typ klasy i ograniczenie typu odwołania mogą obejmować nullable_type_annotation.

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

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

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

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

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

Uwaga: Aby określić, że argument typu jest typem odwołania dopuszczanym do wartości null, nie należy dodawać adnotacji typu dopuszczających wartość null jako ograniczenia (użyj T : class 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 nullable, ?, nie może być używana w niesprecyzowanym argumencie typu.

W przypadku parametru T, gdy argumentem typu jest typ referencyjny dopuszczający wartości null C?, wystąpienia T? są interpretowane jako C?, zamiast C??.

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

public class C
{
}

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

    }
}

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

        int number = 5;
        int? missing = null;

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

Gdy argument typu jest typem, który nie dopuszcza wartości null, ? adnotacja wskazuje, że parametr jest odpowiednim typem umożliwiającym wartość null. Gdy argument typu jest już typem odwołania, który może przyjmować wartość null, parametr jest tym samym typem odwołania, który może przyjmować wartość null.

przykład końcowy

Ograniczenie not null określa, że argument typu używany dla parametru typu powinien być typem wartości niemogącym przyjmować wartości null lub typem referencyjnym niemogącym przyjmować wartości null. Argument typu, który nie jest typem wartości nieprzyjmującym wartości null lub typem referencyjnym nieprzyjmującym wartości null, jest dozwolony, ale kompilator może wygenerować ostrzeżenie diagnostyczne.

Ponieważ notnull nie jest słowem kluczowym, w primary_constraint ograniczenie „not null” jest zawsze niejednoznaczne w stosunku do class_type. Ze względów kompatybilności, jeśli wyszukiwanie nazwy (§12.8.4) powiedzie się, jest traktowane jako class_type. W przeciwnym razie jest traktowane jako ograniczenie niezerowe.

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

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

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

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

Typy wskaźników nigdy nie mogą być argumentami typu i nie spełniają żadnych ograniczeń typowych, nawet tych dotyczących typów niezarządzanych, mimo że same są typami niezarządzanymi.

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

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

  • Typ powinien być typem klasy.
  • Typ nie powinien mieć nazwy sealed.
  • Typ nie jest jednym z następujących typów: System.Array 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 jest używany jako ograniczenie dla parametru S typu, to Szależy odT.
  • Jeśli parametr typu S zależy od parametru typu T, a T zależy od parametru typu U, to Szależy odU.

W tej relacji jest błędem czasu kompilacji, aby parametr typu zależał od samego siebie (bezpośrednio lub pośrednio).

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

  • T nie ma ograniczenia typu wartości. W przeciwnym razie, T jest skutecznie zapieczętowany, więc S będzie musiał być tego samego typu co T, co eliminuje potrzebę użycia dwóch parametrów typu.
  • Jeśli S ma ograniczenie typu wartości, T nie ma ograniczenia class_type .
  • Jeśli S ma ograniczenie class_typeA i T ma ograniczenie class_typeB, to powinna istnieć konwersja tożsamościowa lub niejawna konwersja referencyjna z A do B lub niejawna konwersja referencyjna z B do A.
  • Jeśli S również zależy od parametru typu U i U ma ograniczenie class_typeA oraz T ma ograniczenie class_typeB, to powinna istnieć konwersja tożsamości lub niejawna konwersja odwołania z A do B lub niejawna konwersja odwołania z B do A.

Dopuszczalne jest, aby S miało ograniczenie typu wartości, a T miało ograniczenie typu odwołania. Skutecznie ogranicza to T typy System.Object, System.ValueType, System.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, jeśli type_parameter_constraints mają primary_constraint równy struct lub unmanaged, a także constructor_constraint.

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

interface IPrintable
{
    void Print();
}

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

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

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

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

Poniższy przykład jest błędny, ponieważ powoduje cykliczność w grafie zależności parametrów typu.

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

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

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

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

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

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

przykład końcowy

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

  • Jeśli C jest 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 typem skonstruowanym 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.
  • W przeciwnym razie, Cₓ to C.

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

Niech R będzie zbiorem typów takim, że:

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

Następnie

  • Jeśli T ma ograniczenie typu wartości, efektywną klasą bazową jest System.ValueType.
  • W przeciwnym razie, jeśli R jest pusty, efektywna klasa bazowa to object.
  • W przeciwnym razie efektywna klasa bazowa T jest typem najbardziej ogólnym (§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 typ o najbardziej szerokim zakresie.

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

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

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

  • Jeśli T nie ma secondary_constraints, jego efektywny zestaw interfejsu jest pusty.
  • Jeśli T ma ograniczenia interface_type, ale nie ograniczenia type_parameter, jego efektywny zestaw interfejsów jest zbiorem dynamicznych wymazań jego ograniczeń interface_type.
  • Jeśli T nie ma żadnych ograniczeń interface_type, ale ma ograniczenia type_parameter, jego efektywny zestaw interfejsów jest połączeniem efektywnych zestawów interfejsów jego ograniczeń type_parameter.
  • Jeśli T ma zarówno ograniczenia typu interface_type, jak i ograniczenia type_parameter, jego efektywny zestaw interfejsów jest unią zestawu dynamicznych wymazań jego ograniczeń typu interface_type oraz efektywnych zbiorów interfejsowych jego ograniczeń type_parameter.

Parametr typu jest uznawany za typ referencyjny, jeśli posiada ograniczenie dla typów referencyjnych lub jego efektywna klasa bazowa nie jest object lub System.ValueType. Parametr typu jest znany jako referencyjny typ niemający wartości null, jeśli jest typem odwołania i posiada ograniczenie referencyjnego typu bez wartości null.

Wartości typu parametru typu ograniczonego mogą być używane do dostępu do członków instancji wynikających z ograniczeń.

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

interface IPrintable
{
    void Print();
}

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

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

przykład końcowy

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

Przykład:

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

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

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

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

przykład końcowy

15.2.6 Treść klasy

class_body klasy definiuje elementy tej klasy.

class_body
    : '{' class_member_declaration* '}'
    ;

15.2.7 Częściowe deklaracje typów

Modyfikator partial jest używany podczas definiowania klasy, struktury lub typu interfejsu w wielu częściach. Modyfikator partial jest słowem kluczowym kontekstowym (§6.4.4) i ma specjalne znaczenie bezpośrednio przed słowami kluczowymi class, structi interface. (Typ częściowy może zawierać deklaracje metody częściowej (§15.6.9).

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

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

Typy zagnieżdżone mogą być deklarowane w wielu częściach przy użyciu modyfikatora partial. Zazwyczaj typ zawierający jest również deklarowany przy użyciu partial, a każdy element typu zagnieżdżonego jest deklarowany w innej części typu zawierającego.

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

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

    public Customer()
    {
        ...
    }
}

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

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

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

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

    public Customer()
    {
        ...
    }

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

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

przykład końcowy

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

15.3 Składowe klasy

15.3.1 Ogólne

Członkowie klasy składają się z członków wprowadzonych przez jej class_member_declaration oraz członków odziedziczonych z bezpośredniej klasy bazowej.

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

Członkowie klasy są podzieleni na następujące kategorie:

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

class_declaration tworzy nową przestrzeń deklaracji (§7.3), a type_parameter oraz class_member_declaration bezpośrednio zawarte w class_declaration wprowadzają nowe elementy do tej przestrzeni deklaracji. Następujące reguły mają zastosowanie do class_member_declarations:

  • Konstruktory instancji, finalizatory i konstruktory statyczne muszą mieć tę samą nazwę co klasa je obejmująca. Wszystkie pozostałe członkowie mają nazwy, które różnią się od nazwy bezpośrednio otaczającej klasy.

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

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

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

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

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

Zbiór członków typu zadeklarowanego w wielu częściach (§15.2.7) jest sumą członków zadeklarowanych w każdej części. Elementy wszystkich części deklaracji typu mają ten sam obszar deklaracji (§7.3), a zakres każdego elementu członkowskiego (§7.7) rozciąga się na elementy wszystkich części. Domena dostępności każdego składnika zawsze zawiera wszystkie części otaczającego typu danych; prywatny składnik zadeklarowany w jednej części może być swobodnie dostępny z innej części. Jest to błąd czasu kompilacji, gdy zadeklaruje się tego samego członka w więcej niż jednej części tego samego typu, chyba że ten członek ma modyfikator partial.

Przykład:

partial class A
{
    int x;                   // Error, cannot declare x more than once
    partial void M();        // Ok, defining partial method declaration

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

partial class A
{
    int x;                   // Error, cannot declare x more than once
    partial void M() { }     // Ok, implementing partial method declaration

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

przykład końcowy

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

15.3.2 Typ wystąpienia

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

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

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

przykład końcowy

15.3.3 Elementy członkowskie skonstruowanych typów

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

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

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

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

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

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

przykład końcowy

W ramach elementów funkcji instancyjnej typem this jest typ instancji (§15.3.2) zawierającej deklaracji.

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

Przykład:

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

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

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

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

przykład końcowy

15.3.4 Dziedziczenie

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

  • Dziedziczenie jest przechodnie. Jeśli C pochodzi z B, a B pochodzi z A, to C dziedziczy członków zadeklarowanych w B oraz członków zadeklarowanych 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 jest w stanie usunąć definicji dziedziczonego członka.

  • Konstruktory wystąpień, finalizatory i konstruktory statyczne nie są dziedziczone, ale wszyscy inni członkowie są, niezależnie od zadeklarowanego poziomu dostępu (§7.5). Jednak w zależności od zadeklarowanej dostępności, odziedziczone elementy mogą nie być dostępne w klasie pochodnej.

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

  • Wystąpienie klasy zawiera wszystkie pola instancji zadeklarowane w klasie i jej klasach bazowych, i istnieje niejawna konwersja (§10.2.8) z typu klasy pochodnej do któregoś z typów jej klas bazowych. W związku z tym odwołanie do wystąpienia jakiejś klasy pochodnej może być traktowane jako odwołanie do wystąpienia dowolnej z jego klas bazowych.

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

Dziedziczone składowe typu klasy skonstruowanej są składowymi bezpośredniego typu klasy bazowej (§15.2.4.2), który znajduje się poprzez podstawianie argumentów typu skonstruowanego za każde wystąpienie odpowiednich parametrów typu w base_class_specification. Z kolei ci członkowie są przekształcani przez podstawienie, dla każdego type_parameter w deklaracji członka, odpowiadającego type_argument specyfikacji klasy bazowej.

Przykład:

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

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

W powyższym kodzie skonstruowany typ D<int> ma nieodziedziczony członek publiczny intG(string s) uzyskany przez podstawienie argumentu typu int dla parametru typu T. D<int> ma również element członkowski odziedziczony z deklaracji klasy B. Ten dziedziczony element członkowski jest określany poprzez najpierw ustalenie typu B<int[]> klasy bazowej D<int>, przez zastąpienie T elementem int w specyfikacji klasy bazowej B<T[]>. Następnie, jako argument typu dla B, int[] jest zastępowany U w parametrze public U F(long index), 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ć członka z tą samą nazwą lub sygnaturą co dziedziczony członek. W takim przypadku mówi się, że składowa klasy pochodnej ukrywa składową klasy bazowej. Zobacz §7.7.2.3 dla dokładnej specyfikacji, kiedy członek ukrywa odziedziczonego członka.

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

new Jeśli modyfikator jest uwzględniony w deklaracji, która nie ukrywa dostępnego dziedziczonego elementu, zostanie wyświetlone ostrzeżenie w tej sprawie.

15.3.6 Modyfikatory dostępu

Deklaracja członka klasy (class_member_declaration) może mieć dowolny z dozwolonych rodzajów zadeklarowanej dostępności (§7.5.2): public, protected internal, protected, private protected, internal, lub private. Z wyjątkiem kombinacji protected internal i private protected, jest to błąd czasu kompilacji, aby określić więcej niż jeden modyfikator dostępu. Jeśli class_member_declaration nie zawiera żadnych modyfikatorów dostępu, przyjmuje się założenieprivate.

15.3.7 Typy składowe

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

15.3.8 Statyczne i składowe instancji

Składowe klasy to statyczni członkowie lub członkowie instancyjni.

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

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

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

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

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

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

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

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

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

Metoda F pokazuje, że w metodzie członka instancji można użyć simple_name (§12.8.4) do uzyskania dostępu zarówno do składowych instancji, jak i do składowych statycznych. Metoda G pokazuje, że w składowej funkcji statycznej błąd czasu kompilacji występuje, gdy próbujemy uzyskać dostęp do elementu wystąpienia za pośrednictwem simple_name. Metoda Main pokazuje, że w member_access (§12.8.7) członkowie instancji powinni być dostępni przez instancje, a członkowie statyczni przez typy.

przykład końcowy

15.3.9 Typy zagnieżdżone

15.3.9.1 Ogólne

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

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

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

klasa B jest typem zagnieżdżonym, ponieważ jest zadeklarowana w 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 typu zagnieżdżonego jest S.N, gdzie S jest w pełni kwalifikowaną nazwą deklaracji typu, w której zadeklarowano typ N, a N stanowi niekwalifikowaną nazwę (§7.8.2) deklaracji typu zagnieżdżonego (w tym dowolnego specyfikatora_wymiaru_ogólnego (§12.8.18)).

15.3.9.3 Zadeklarowana dostępność

Typy niezagnieżdżone mogą mieć public lub internal zadeklarowaną dostępność i domyślnie mieć internal zadeklarowaną dostępność. Zagnieżdżone typy mogą również mieć te formy zadeklarowanej ochrony dostępu oraz jedną lub więcej dodatkowych form zadeklarowanej ochrony dostępu, w zależności od tego, czy typ, który zawiera, jest klasą czy strukturą.

  • Zagnieżdżony typ zadeklarowany w klasie może mieć dowolny z dozwolonych rodzajów zadeklarowanej dostępności i, podobnie jak inne elementy klasy, domyślnie ma (z)definiowane private poziomy dostępności.
  • Typ zagnieżdżony, który jest zadeklarowany w strukturze, może mieć dowolną z trzech form zadeklarowanej dostępności (public, internal, lub private) i, podobnie jak inne elementy członkowskie struktury, domyślnie ma zadeklarowaną dostępność jako private.

Przykład: przykład

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

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

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

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

deklaruje prywatną zagnieżdżoną klasę Node.

przykład końcowy

15.3.9.4 Ukrywanie

Typ zagnieżdżony może ukrywać element bazowy (§7.7.2.2). new Modyfikator (§15.3.5) jest dozwolony przy deklaracjach typów zagnieżdżonych, co pozwala na jawne wyrażenie ukrywania.

Przykład: przykład

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

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

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

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

przykład końcowy

15.3.9.5 ten dostęp

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

Przykład: poniższy przykład

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

    public class Nested
    {
        C this_c;

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

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

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

pokazuje tę technikę. Instancja C tworzy instancję Nested, i przekazuje swoje this do konstruktora Nested w celu zapewnienia późniejszego dostępu do elementów członkowskich instancji C.

przykład końcowy

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

Typ zagnieżdżony ma dostęp do wszystkich elementów członkowskich, które są dostępne dla typu zawierającego, w tym tych członków, które mają zadeklarowane poziomy dostępu, jak private i protected.

Przykład: przykład

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

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

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

wyświetla klasę C, która zawiera zagnieżdżoną klasę Nested. W Nested metoda G wywołuje statyczną metodę F, zdefiniowaną w C, a F ma prywatnie zdeklarowaną dostępność.

przykład końcowy

Typ zagnieżdżony może również uzyskiwać dostęp do chronionych składowych zdefiniowanych w typie bazowym zawierającym go.

Przykład: w poniższym kodzie

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

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

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

Zagnieżdżona klasa Derived.Nested uzyskuje dostęp do chronionej metody F zdefiniowanej w klasie bazowej DerivedBase, poprzez wywołanie instancji Derived.

przykład końcowy

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

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

Każda deklaracja typu zawarta w deklaracji klasy ogólnej jest niejawnie deklaracją typu ogólnego. Podczas pisania odwołania do typu zagnieżdżonego w typie generycznym należy uwzględnić nazwę konstrukty, łącznie z argumentami typu. Jednak wewnątrz klasy zewnętrznej typ zagnieżdżony może być używany bez dodatkowego oznaczenia; typ instancji klasy zewnętrznej może być używany niejawnie przy tworzeniu typu zagnieżdżonego.

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

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

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

przykład końcowy

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

Przykład:

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

przykład końcowy

15.3.10 Zastrzeżone nazwy członków

15.3.10.1 Ogólne

Aby ułatwić implementację w czasie wykonywania w języku C#, dla każdej deklaracji składowej źródłowej, która jest właściwością, zdarzeniem lub indeksatorem, implementacja zastrzega sobie dwie sygnatury metody na podstawie rodzaju deklaracji składowej, jego nazwy i jego typu (§15.3.10.2, §15.3.3, §15.3.10.4). Błędem kompilacji w programie jest deklarowanie członka, którego sygnatura pasuje do sygnatury zarezerwowanej przez innego członka zadeklarowanego w tym samym zakresie, nawet jeśli implementacja w czasie wykonywania nie wykorzystuje tych rezerwacji.

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

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

  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 kod źródłowy zaakceptowany przez jeden zgodny kompilator jest akceptowany przez inny, poprzez ujednolicenie nazw zarezerwowanych członków we wszystkich implementacjach języka C#.

notatka końcowa

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

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

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

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

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

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

Przykład: w poniższym kodzie

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

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

    public new void set_P(int value)
    {
    }
}

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

Klasa A definiuje właściwość P, która jest tylko do odczytu, rezerwując w ten sposób podpisy dla metod get_P i set_P. A klasa B dziedziczy po A i ukrywa obie z tych zastrzeżonych sygnatur. Przykład generuje dane wyjściowe:

123
123
456

przykład końcowy

15.3.10.3 Nazwy członków zarezerwowane dla zdarzeń

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

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

15.3.10.4 Nazwy członków zarezerwowane dla indeksatorów

W przypadku indeksatora (§15.9) typu T z listą 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 członka Item jest zarezerwowana.

15.3.10.5 Nazwy członków zarezerwowane dla finalizatorów

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

void Finalize();

15.3.10.6 Nazwy metod zarezerwowane dla operatorów

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

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

15.4 Stałe

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

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

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

Deklaracja stałej może zawierać zestaw atrybutów (§22), new (§15.3.5), i dowolny z dozwolonych rodzajów zadeklarowanej dostępności (§15.3.6). Atrybuty i modyfikatory mają zastosowanie do wszystkich elementów członkowskich zadeklarowanych przez constant_declaration. Mimo że stałe są uznawane za statyczne członki, constant_declaration nie wymaga ani nie dopuszcza użycia modyfikatora static. Jest to błąd dla tego samego modyfikatora, który pojawia się wielokrotnie w deklaracji stałej.

Typ deklaracji constant_declaration określa typ elementów wprowadzonych przez deklarację. Po typie następuje lista constant_declaratorów (§13.6.3), z których każdy wprowadza nowy element. Deklarator stałej składa się z identyfikatora, który nazywa element członkowski, po którym następuje znacznik "=", a następnie wyrażenie stałej (§12.23), które daje wartość elementu członkowskiego.

Typ określony w deklaracji stałej powinien być , , , , , , , , , , , , , , enum_type lub reference_type. 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 reference_type innej niż string jest zastosowanie operatora new, a ponieważ operator new nie jest dozwolony w wyrażeniu stałym new, jedyną możliwą wartością dla stałych reference_typey innych niż jest string. notatka końcowa

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

Uwaga: Semantyka wersjonowania const i readonly różni się (§15.5.3.3). notatka końcowa

Stała deklaracja, która deklaruje wiele stałych, jest równoważna wielokrotnym deklaracjom pojedynczych stałych z tymi samymi atrybutami, modyfikatorami i typem.

Przykład:

class A
{
    public const double X = 1.0, Y = 2.0, Z = 3.0;
}

jest równoważny

class A
{
    public const double X = 1.0;
    public const double Y = 2.0;
    public const double Z = 3.0;
}

przykład końcowy

Stałe mogą zależeć od innych stałych w ramach tego samego programu, o ile zależności nie mają charakteru cyklicznego.

Przykład: w poniższym kodzie

class A
{
    public const int X = B.Z + 1;
    public const int Y = 10;
}

class B
{
    public const int Z = A.Y + 1;
}

kompilator musi najpierw ocenić A.Y, a następnie ocenić B.Z, a następnie ocenić A.X, tworząc wartoś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: nawiązując do powyższego przykładu, jeśli A i B zostały zadeklarowane w oddzielnych programach, możliwe byłoby, aby A.X zależało od B.Z, ale B.Z nie mogłoby równocześnie zależeć 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).

Deklaracja pola może zawierać zestaw atrybutów (§22), modyfikator new (§15.3.5), prawidłową kombinację czterech modyfikatorów dostępu (§15.3.6) i modyfikator static (§15.5.2). Ponadto field_declaration może zawierać readonly modyfikator (§15.5.3) lub volatile modyfikator (§15.5.4), ale nie oba. Atrybuty i modyfikatory mają zastosowanie do wszystkich członków zadeklarowanych przez field_declaration. Jest błędem, gdy ten sam modyfikator pojawia się wielokrotnie w field_declaration.

Typfield_declaration określa typ elementów wprowadzonych przez deklarację. Po typie następuje lista variable_declarators, z których każdy wprowadza nowego członka. A variable_declarator składa się z identyfikatora, który nazywa ten człon, opcjonalnie po którym znajduje się token "=" i variable_initializer (§15.5.6), który nadaje początkową wartość temu członowi.

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

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

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

Przykład:

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

jest równoważny

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

przykład końcowy

15.5.2 Pola statyczne i pola instancji

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

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

15.5.3 Pola tylko do odczytu

15.5.3.1 Ogólne

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

  • W variable_declarator, który wprowadza pole (poprzez uwzględnienie variable_initializer w deklaracji).
  • Dla pola instancyjnego, w konstruktorach instancyjnych klasy zawierającej deklarację pola; dla pola statycznego, w konstruktorze statycznym klasy zawierającej deklarację pola. Są to również jedyne konteksty, w których ważne jest przekazanie pola readonly jako parametru wyjściowego lub referencyjnego.

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

15.5.3.2 Używanie stałych pól readonly dla stałych wartości

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

Przykład: w poniższym kodzie

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

    private byte red, green, blue;

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

Elementów członkowskich Black, White, Red, Green i Blue nie można zadeklarować jako 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 Wersjonowanie stałych i pól statycznych tylko do odczytu

Stałe i pola tylko do odczytu mają różną semantykę wersjonowania binarnego. Gdy wyrażenie odwołuje się do stałej, wartość stałej jest uzyskiwana w czasie kompilacji, ale gdy wyrażenie odwołuje się do pola tylko do odczytu, wartość pola nie jest uzyskiwana aż do czasu wykonywania.

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

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

oraz

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

Przestrzenie nazw Program1 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 zostałaby uzyskana w czasie kompilacji Program2 i pozostałaby bez zmian, pomimo zmian w Program1, aż do momentu ponownego skompilowania Program2.

przykład końcowy

15.5.4 Pola nietrwałe

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

  • Odczyt pola lotnego jest nazywany odczytem lotnym. Odczyt zmiennej oznaczonej jako volatile ma "semantykę akwizycji"; oznacza to, że jest gwarantowane, iż nastąpi przed wszelkimi odwołaniami do pamięci, które następują po nim w sekwencji instrukcji.
  • Zapis pola zmiennego jest nazywany zapisem zmiennymvolatile write. Zapis volatile ma "semantykę wydania"; oznacza to, że jest gwarantowany po wszelkich odwołaniach do pamięci przed instrukcją zapisu w sekwencji instrukcji.

Te ograniczenia zapewniają, że wszystkie wątki będą obserwować zmienne zapisy wykonywane przez dowolny inny wątek w kolejności, w jakiej zostały wykonane. Zgodna implementacja nie musi zapewniać pojedynczej całkowitej kolejności zapisów zmiennych ulotnych z punktu widzenia wszystkich wątków wykonywania. Typ pola lotnego musi być jednym z następujących:

  • typ_odniesienia.
  • type_parameter, który jest znany jako typ referencyjny (§15.2.5).
  • Typ byte, sbyte, short, ushort, int, uint, char, float, bool, System.IntPtr lub System.UIntPtr.
  • Typ enum_type mający typ bazowy enum_base w jednym z wartości: byte, sbyte, short, ushort, int lub 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 jako volatile, to dopuszczalne byłoby, aby zapis do result był widoczny dla głównego wątku po zapisie do finished, a tym samym aby główny wątek mógł odczytać wartość 0 z pola result. Deklarowanie finished jako volatile pola uniemożliwia takie niespójności.

przykład końcowy

15.5.5 Inicjowanie pola

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

Przykład: przykład

class Test
{
    static bool b;
    int i;

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

generuje dane wyjściowe

b = False, i = 0

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

przykład końcowy

Inicjalizatory zmiennych 15.5.6

15.5.6.1 Ogólne

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

Przykład: przykład

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

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

generuje dane wyjściowe

x = 1.4142135623730951, i = 100, s = Hello

ponieważ przypisanie do x ma miejsce, gdy wykonywane są inicjalizatory pól statycznych, a przypisania do i i s występują, gdy wykonywane są inicjalizatory pól instancji.

przykład końcowy

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

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

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

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

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

wykazuje to zachowanie. Pomimo okrągłych definicji a i b, program jest prawidłowy. Rezultatem jest wynik wyjściowy

a = 1, b = 2

ponieważ pola statyczne a i b są inicjowane do 0 (wartość domyślna dla int) przed wykonaniem ich inicjatorów. Gdy inicjalizator dla a działa, wartość b jest zero, więc a jest inicjowany na 1. Gdy inicjator dla b działa, wartość a jest już 1, a więc b jest zainicjowany na 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 wyniku:

Init A
Init B
1 1

lub dane wyjściowe:

Init B
Init A
1 1

ponieważ wykonanie inicjatora X i inicjatora Y może wystąpić w dowolnej kolejności; muszą wystąpić tylko przed odwołaniami do tych pól. Jednak w przykładzie:

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

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

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

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

dane wyjściowe to:

Init B
Init A
1 1

ponieważ reguły dotyczące wykonywania konstruktorów statycznych (zgodnie z definicją w §15.12) zapewniają, że konstruktor statyczny B (a zatem także inicjatory pól statycznych B) będą uruchamiane przed konstruktorem statycznym A oraz jego inicjatorami pól.

przykład końcowy

Inicjowanie pola wystąpienia 15.5.6.3

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

Inicjalizator zmiennej dla pola instancji nie może odwoływać się do tworzonej instancji. W związku z tym odwoływanie się do this w inicjatorze zmiennej jest błędem czasu kompilacji, podobnie jak odwoływanie się do dowolnego członka instancji za pośrednictwem simple_name jest błędem czasu kompilacji w inicjatorze zmiennej.

Przykład: w poniższym kodzie

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

inicjalizator zmiennej dla y powoduje błąd czasu kompilacji, ponieważ odwołuje się do elementu instancji, która jest tworzona.

przykład końcowy

15.6 Metody

15.6.1 Ogólne

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

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

method_modifiers
    : method_modifier* 'partial'?
    ;

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

ref_method_modifiers
    : ref_method_modifier*
    ;

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

method_modifier
    : ref_method_modifier
    | 'async'
    ;

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

return_type
    : ref_return_type
    | 'void'
    ;

ref_return_type
    : type
    ;

member_name
    : identifier
    | interface_type '.' identifier
    ;

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

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

Uwagi gramatyczne:

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

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

Method_declaration może zawierać zestaw atrybutów (§22) i jeden z dozwolonych rodzajów zadeklarowanej dostępności (§15.3.6), new (§15.3.5), static (§15.6.3), virtual (§15.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7), extern (§15.6.8) i async (§15.15) modyfikatory.

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

  • Deklaracja zawiera prawidłową kombinację modyfikatorów dostępu (§15.3.6).
  • Deklaracja nie zawiera tego samego modyfikatora wiele razy.
  • Deklaracja zawiera co najwyżej jeden z następujących modyfikatorów: static, 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 modyfikator sealed, to również zawiera modyfikator override.
  • Jeśli deklaracja zawiera partial modyfikator, nie zawiera żadnego z następujących modyfikatorów: new, public, protected, internal, private, virtual, sealed, override, abstract lub extern.

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

  • Jeśli ref jest obecny, metoda jest wykonywana jako zwracanie przez referencję i zwraca referencję do zmiennej, która może być opcjonalnie tylko do odczytu;
  • W przeciwnym razie, jeśli return_type to void, metoda nie zwraca wartości.
  • W przeciwnym razie metoda jest przez wartość i zwraca wartość.

return_type w deklaracji metody "zwracającej przez wartość" lub "niezwracającej wartości" określa typ wyniku, jeśli taki istnieje, zwracany przez metodę. Tylko metoda zwracana bez wartości może zawierać partial modyfikator (§15.6.9). Jeśli deklaracja zawiera async modyfikator, to return_type musi być void lub metoda zwracająca przez wartość, a zwracany typ jest typem zadania (§15.15.1).

Deklaracja metody typu zwracającego przez referencję ref_return_type określa typ zmiennej, do której odnosi się variable_reference zwracane przez metodę.

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

Dla ogólnego method_declaration implementującego jawny element członkowski interfejsu nie mogą istnieć żadne type_parameter_constraints_clause; deklaracja dziedziczy wszelkie ograniczenia wynikające z ograniczeń metody interfejsu.

Podobnie deklaracja metody z override modyfikatorem nie powinna mieć żadnych type_parameter_constraints_clauses, a ograniczenia parametrów typu metody są dziedziczone z nadpisywanej metody wirtualnej.

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

W przypadku jawnej implementacji członka interfejsu, member_name składa się z interface_type oraz następującego po nim "." i identyfikatora. W takim przypadku deklaracja nie zawiera żadnych modyfikatorów innych niż (ewentualnie) extern ani async.

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

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

Metoda zwracająca po wartości lub nie zwracająca wartości ma ciało, które jest średnikiem, blokiem lub wyrażeniem. Treść bloku składa się z bloku, który określa instrukcje do wykonania po wywołaniu metody. Treść wyrażenia składa się z elementu =>, po którym następuje null_conditional_invocation_expression lub wyrażenie, oraz średnika, i oznacza pojedyncze wyrażenie do wykonania podczas wywołania metody.

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

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

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

W przypadku metod abstrakcyjnych i zewnętrznych ref_method_body składa się po prostu ze średnika; dla wszystkich innych metod ref_method_body jest ciałem bloku lub wyrażenia.

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

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

Parametry typu metody znajdują się w zakresie deklaracji metody i mogą być używane do tworzenia typów w tym zakresie w return_type lub ref_return_type, method_body lub ref_method_body, oraz w klauzulach ograniczeń parametrów typów, ale nie w atrybutach.

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

Parametry metody 15.6.2

15.6.2.1 Ogólne

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

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

fixed_parameters
    : fixed_parameter (',' fixed_parameter)*
    ;

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

default_argument
    : '=' expression
    ;

parameter_modifier
    : parameter_mode_modifier
    | 'this'
    ;

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

parameter_array
    : attributes? 'params' array_type identifier
    ;

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

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

Parametr z modyfikatorem 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:

  • stałe_wyrażenie
  • 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 powinno być możliwe do niejawnego przekształcenia za pomocą konwersji tożsamości lub konwersji dopuszczającej wartość 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. Typ array_type tablicy parametrów powinien być jednowymiarowym typem tablicy (§17.2). W wywołaniu metody tablica parametrów zezwala na określenie pojedynczego argumentu danego typu tablicy lub dopuszcza zero lub więcej argumentów typu elementu tablicy. Tablice parametrów są opisane dalej w §15.6.2.4.

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

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

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

W parameter_list dla M, i jest wymaganym parametrem ref, d jest wymaganym parametrem wartości, b, s, o i t są opcjonalnymi parametrami wartości, a a jest tablicą parametrów.

przykład końcowy

Deklaracja metody tworzy oddzielną przestrzeń deklaracji (§7.3) dla parametrów i parametrów typu. Nazwy są wprowadzane do tej przestrzeni deklaracji przez listę parametrów typu i listę parametrów metody. Treść metody, jeśli istnieje, jest uznawana za zagnieżdżoną w tej przestrzeni deklaracyjnej. To jest błąd, gdy dwa elementy w przestrzeni deklaracji metody mają taką samą nazwę.

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

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

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).

Kiedy parametr jest przekazywany przez odniesienie, odpowiedni argument w wywołaniu metody składa się z odpowiedniego słowa kluczowego , lub , a następnie odniesienia_do_zmiennej (§9.5) tego samego typu co parametr. 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, możliwe jest, aby wiele nazw reprezentowało tę samą lokację pamięci.

15.6.2.3.2 Parametry wejściowe

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

Jest to błąd czasu kompilacji, gdy zmienia się wartość parametru wejściowego.

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

15.6.2.3.3 Parametry odwołania

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

Przykład: przykład

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

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

generuje dane wyjściowe

i = 2, j = 1

W przypadku wywołania Swap w Main, x reprezentuje i, a y reprezentuje 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 w G przekazuje odwołanie do s zarówno dla a, jak i b. W związku z tym, dla tego wywołania, nazwy s, a, i b wszystkie odwołują się do tej samej lokalizacji pamięci, a trzy przypisania modyfikują pole s wystąpienia.

przykład końcowy

struct W przypadku typu w metodzie instancji, akcesorze instancji (§12.2.1) lub konstruktorze instancji z inicjatorem konstruktora, słowo kluczowe this zachowuje się dokładnie jak parametr referencyjny typu struktury (§12.8.14).

15.6.2.3.4 Parametry wyjściowe

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

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

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

Przykład:

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

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

Przykład generuje dane wyjściowe:

c:\Windows\System\
hello.txt

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

przykład końcowy

15.6.2.4 Tablice parametrów

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

Przykład: typy string[] 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 F automatycznie tworzy czteroelementową tablicę int[] z podanymi wartościami elementów i przekazuje to wystąpienie tablicy jako wartościowy parametr. Podobnie, trzecie wywołanie F tworzy pusty element int[] i przekazuje to wystąpienie jako parametr wartości. Drugie i trzecie wywołania są dokładnie równoważne pisaniu:

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

przykład końcowy

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

Przykład: przykład

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

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

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

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

generuje dane wyjściowe

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

W tym przykładzie dwie z możliwych rozszerzonych form metody z tablicą parametrów są już uwzględnione w klasie jako zwykłe metody. Te rozszerzone formy nie są zatem brane pod uwagę podczas rozwiązywania przeciążenia, a pierwsze i trzecie wywołania metody wybierają zwykłe metody. Gdy klasa deklaruje metodę z parametrem tablicowym, nie jest niczym niezwykłym, aby uwzględnić także niektóre jej rozwinięte formy jako zwykłe metody. W ten sposób można uniknąć alokacji instancji tablicy, która występuje po wywołaniu wersji rozszerzonej metody z tablicą parametrów.

przykład końcowy

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

Przykład: przykład:

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

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

generuje dane wyjściowe:

True
False

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

przykład końcowy

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

Przykład: przykład

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

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

generuje dane wyjściowe

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

W pierwszym i ostatnim wywołaniu F normalna forma F ma zastosowanie, ponieważ istnieje niejawna konwersja z 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 ma zastosowanie, dlatego jest wybierana poprzez rozstrzyganie 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 instancyjne

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

Metoda statyczna nie działa na określonym wystąpieniu, a odwołanie do this w metodzie statycznej jest błędem kompilacji.

Metoda instancji działa na konkretnej instancji klasy, a do tej instancji można uzyskać dostęp jako this (§12.8.14).

Różnice między członkami statycznymi i członkami instancji są dalej omawiane w §15.3.8.

15.6.4 Metody wirtualne

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

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

W wywołaniu metody wirtualnej typ czasu wykonywania wystąpienia, dla którego to wywołanie jest wykonywane, określa rzeczywistą implementację metody, którą należy wywołać. W wywołaniu metody niewirtualnej czynnikiem decydującym jest typ czasu kompilacji wystąpienia. Precyzyjnie mówiąc, gdy na wystąpieniu, którego typ w czasie kompilacji to C, a typ w czasie wykonywania to R, wywołana zostaje metoda o nazwie N z listą argumentów A, (gdzie R jest C lub klasa pochodna od C), wywołanie jest przetwarzane w następujący sposób:

  • W czasie powiązania rozwiązywanie przeciążeń jest stosowane do C, N, i A, aby wybrać określoną metodę M z zestawu metod zadeklarowanych i dziedziczonych 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.
    • W przeciwnym razie, M jest metodą wirtualną, a wywoływana jest najbardziej szczegółowa implementacja M w odniesieniu do R.

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

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

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ą niewirtualną metodę F, w związku z czym ukrywa dziedziczoną metodę F, a także zastępuje dziedziczoną metodę G. Przykład generuje dane wyjściowe:

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

Zwróć uwagę, że instrukcja a.G() wywołuje metodę B.G, a 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 metody poza najbardziej pochodną są ukryte.

Przykład: w poniższym kodzie

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

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

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

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

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

Klasy C i D zawierają dwie metody wirtualne z tym samym podpisem: jedna wprowadzona przez A i jedna wprowadzona przez C. Metoda wprowadzona przez C ukrywa metodę dziedziczoną z A klasy. W związku z tym deklaracja przesłonięcia w D przesłania metodę wprowadzoną przez C, i nie jest możliwe, aby D przesłonił metodę wprowadzoną przez A. Przykład produkuje wynik:

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

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

przykład końcowy

15.6.5 Metody zastępowania

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

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

Błąd podczas kompilacji występuje, chyba że wszystkie następujące warunki są spełnione dla deklaracji nadpisania:

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

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

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

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

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

przykład końcowy

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

Przykład: w poniższym kodzie

class A
{
    int x;

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

class B : A
{
    int y;

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

wywołanie base.PrintFields() w B wywołuje metodę PrintFields zadeklarowaną w A. Base_access wyłącza mechanizm wywołania wirtualnego i po prostu traktuje metodę podstawową jak zwykłą metodę bez uwzględnienia jej wirtualnych właściwościvirtual. Gdyby wywołanie w B zostało napisane ((A)this).PrintFields(), rekursywnie wywołałoby metodę PrintFields zadeklarowaną w B, a nie tę zadeklarowaną w A, ponieważ PrintFields jest wirtualna, a typ czasu wykonywania ((A)this) to B.

przykład końcowy

Tylko poprzez dołączenie modyfikatora override metoda może zastąpić inną metodę. We wszystkich innych przypadkach metoda z tą samą sygnaturą co dziedziczona metoda po prostu ukrywa tę metodę.

Przykład: w poniższym kodzie

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

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

F metoda w B nie zawiera modyfikatora override i dlatego nie zastępuje metody F w A. Zamiast tego metoda w F ukrywa metodę w B, a ostrzeżenie jest zgłaszane w 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 F w B ukrywa odziedziczoną metodę wirtualną F z A. Ponieważ nowy F w B ma dostęp prywatny, jego zakres obejmuje tylko ciało klasy B 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. Użycie modyfikatora sealed przez B zapobiega dalszemu zastępowaniu C przez F.

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, muszą zastąpić metodę Paint i dostarczyć rzeczywistą implementację.

przykład końcowy

Jest to błąd czasu kompilacji, gdy base_access (§12.8.15) odwołuje się do metody abstrakcyjnej.

Przykład: w poniższym kodzie

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

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

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

przykład końcowy

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

Przykład: w poniższym kodzie

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

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

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

klasa A deklaruje metodę wirtualną, 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 składa się tylko z średnika. Metoda zewnętrzna nie jest ogólna.

Mechanizm łączenia z metodą zewnętrzną jest definiowany przez implementację.

Przykład: W poniższym przykładzie pokazano użycie extern modyfikatora i 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. Identyfikator partial jest rozpoznawany jako słowo kluczowe kontekstowe (§6.4.4) w deklaracji metody tylko wtedy, gdy pojawia się bezpośrednio przed słowem kluczowym void. Metoda częściowa nie może jawnie implementować metod interfejsu.

Istnieją dwa rodzaje deklaracji metody częściowej: jeśli treść deklaracji metody jest średnikiem, deklaracja jest deklarowana jako definiująca deklarację metody częściowej. Jeśli treść jest inna niż średnik, mówi się, że deklaracja jest deklaracją implementującej metody częściowej. W częściach deklaracji typu może istnieć tylko jedna definiująca deklaracja metody częściowej z danym podpisem oraz co najwyżej jedna deklaracja implementująca metodę częściową z tym samym podpisem. Jeżeli zostanie podana deklaracja częściowej metody implementującej, powinna istnieć odpowiednia deklaracja definiująca częściową metodę, i deklaracje powinny być zgodne z następującymi specyfikacjami:

  • Deklaracje mają takie same modyfikatory (chociaż niekoniecznie w tej samej kolejności), nazwę metody, liczbę parametrów typu i liczbę parametrów.
  • Odpowiednie parametry w deklaracjach powinny mieć te same modyfikatory (chociaż niekoniecznie w tej samej kolejności) i takie same typy lub typy możliwe do konwersji pod względem tożsamości (z uwzględnieniem różnic w nazwach parametrów typu).
  • Odpowiednie parametry typu w deklaracjach muszą mieć te same ograniczenia (z uwzględnieniem różnic w nazwach parametrów typu).

Implementacja deklaracji metody częściowej może być umieszczona w tej samej części, co deklaracja definiująca metodę częściową.

Tylko zdefiniowana metoda częściowa uczestniczy w rozpoznawaniu przeciążeń. W związku z tym, niezależnie od tego, czy podano deklarację implementacyjną, wyrażenia wywołania mogą być rozpoznawane jako wywołania metody częściowej. Ponieważ metoda częściowa zawsze zwraca void, takie wyrażenia będą wyrażeniami instrukcji. Ponadto, ponieważ metoda częściowa jest niejawnie private, takie wyrażenia zawsze mają miejsce w jednej z części deklaracji typu, w której zadeklarowana jest metoda częściowa.

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

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

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

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

notatka końcowa

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

Jeśli deklaracja implementowania istnieje dla danej metody częściowej, wywołania metod częściowych są zachowywane. Metoda częściowa prowadzi do deklaracji metody zbliżonej do deklaracji implementacyjnej częściowej metody, z wyjątkiem następujących różnic:

  • Modyfikator partial nie jest uwzględniony.

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

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

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

  • Jest to błąd czasu kompilacji podczas próby utworzenia delegata z M (§12.8.17.5).

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

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

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

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

partial class Customer
{
    string name;

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

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

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

class Customer
{
    string name;

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

Załóżmy jednak, że dostępna jest inna część, która udostępnia deklaracje implementujące metody częściowe.

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

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

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

class Customer
{
    string name;

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

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

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

15.6.10 Metody rozszerzenia

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

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

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

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

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

przykład końcowy

Metoda rozszerzenia jest regularną metodą statyczną. Ponadto, jeśli jej otaczająca klasa statyczna znajduje się w zakresie, metoda rozszerzająca może być wywołana przy użyciu składni wywoływania metody instancyjnej (§12.8.10.3), używając wyrażenia odbiorcy jako pierwszego argumentu.

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

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

Metoda Slice jest dostępna w 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

15.6.11 Treść metody

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

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

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

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

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

Dla metody zwracającej przez wartość (§15.6.1), każda instrukcja return w treści tej metody określa wyrażenie, które jest niejawnie konwertowane na efektywny typ zwracany.

W przypadku metody zwracania przez odwołanie (§15.6.1), każda instrukcja return w treści tej metody musi określać wyrażenie, którego typ jest skutecznym typem zwrotu i ma kontekst ref-safekontekstu wywołującego (§9.7.2).

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

Przykład: w poniższym kodzie

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

    public int G()
    {
        return 1;
    }

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

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

metoda zwracająca wartość F powoduje błąd czasu kompilacji, ponieważ kontrola może opuścić ciało metody. 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 — obie struktury są nazwanymi elementami członkowskimi powiązanymi z typami, a składnia uzyskiwania dostępu do pól i właściwości jest taka sama. Jednak w przeciwieństwie do pól właściwości nie oznaczają lokalizacji przechowywania. Zamiast tego właściwości mają akcesory, które określają instrukcje do wykonania, gdy ich wartości są odczytywane lub zapisywane. Właściwości zapewniają zatem mechanizm kojarzenia akcji z odczytem i zapisem cech obiektu lub klasy; ponadto pozwalają one na obliczanie takich cech.

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

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

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

property_initializer
    : '=' variable_initializer ';'
    ;

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

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

Istnieją dwa rodzaje property_declaration:

  • Pierwszy deklaruje właściwość o wartości innej niż ref. Jego wartość ma typ typu. Tego rodzaju właściwość może być czytelna i/lub zapisywalna.
  • Drugi deklaruje właściwość ref-valued. Jego wartość to variable_reference (§9.5), który może być readonly, dla zmiennej typu type. Tego rodzaju właściwość jest dostępna tylko do odczytu.

Property_declaration może zawierać zestaw atrybutów (§22) i dowolny z dozwolonych rodzajów zadeklarowanej dostępności (§15.3.6), new (§15.3.5), static (§15.7.2), virtual (§15.6.4, §15.7.6), override (§15.6.5, §15.7.6), sealed (§15.6.6), abstract (§15.6.7, §15.7.6), i extern (§15.6.8) modyfikatory.

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

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

Typ właściwości musi być przynajmniej tak samo dostępny jak sama właściwość (§7.5.5).

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

W property_body ciało wyrażenia składające się z => po którym następuje wyrażenieE oraz średnik, jest dokładnym odpowiednikiem ciała instrukcji { get { return E; } }, i dlatego może być używane wyłącznie do określania właściwości tylko do odczytu, gdzie wynik akcesora get jest podawany jako pojedyncze wyrażenie.

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

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

W ref_property_body wyrażenia składającego się z => po którym następuje ref, variable_referenceV i średnika, jest dokładnym odpowiednikiem treści { get { return ref V; } }instrukcji.

Uwaga: Mimo że składnia uzyskiwania dostępu do właściwości jest taka sama jak w przypadku pola, właściwość nie jest klasyfikowana jako zmienna. W związku z tym nie można przekazać właściwości jako in, 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_body w accessor_declarations powinien być przedstawiony jako średnik.

15.7.2 Właściwości statyczne i instancji

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

Właściwość statyczna nie jest skojarzona z określonym wystąpieniem, i odwoływanie się do this w metodach dostępu właściwości statycznej jest błędem czasu kompilacji.

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

Różnice między elementami statycznymi a elementami instancji zostały omówione w §15.3.8.

15.7.3 Akcesoria

Uwaga: Ta klauzula dotyczy obu właściwości (§15.7) i indeksatorów (§15.9). Klauzula jest wyrażana w kontekście właściwości; podczas odczytywania zastąp "właściwość/właściwości" wyrażeniem "indeksator/indeksatory" i sprawdź listę różnic między właściwościami a indeksatorami wymienionymi w §15.9.2. notatka końcowa

Deklaracje akcesorów właściwości określają instrukcje wykonywalne związane z zapisem i/lub odczytem tej właściwości.

accessor_declarations
    : get_accessor_declaration set_accessor_declaration?
    | set_accessor_declaration get_accessor_declaration?
    ;

get_accessor_declaration
    : attributes? accessor_modifier? 'get' accessor_body
    ;

set_accessor_declaration
    : attributes? accessor_modifier? 'set' accessor_body
    ;

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

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

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

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

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

Korzystanie z accessor_modifiers podlega następującym ograniczeniom:

  • Nie należy używać accessor_modifier w interfejsie ani w jawnej implementacji składowej interfejsu.
  • W przypadku właściwości lub indeksatora, który nie posiada modyfikatora override, accessor_modifier jest dozwolony tylko wtedy, gdy właściwość lub indeksator ma zarówno akcesor get, jak i set, a następnie dozwolone jest tylko dla jednego z tych akcesorów.
  • Dla właściwości lub indeksatora, który zawiera override modyfikator, akcesor powinien odpowiadać accessor_modifier akcesora, który jest zastępowany, jeśli posiada taki modyfikator.
  • accessor_modifier musi zadeklarować dostępność, która jest ściśle bardziej restrykcyjna niż deklarowana dostępność samej właściwości lub indeksatora. Aby być precyzyjnym:
    • Jeśli właściwość lub indeksator ma zadeklarowaną dostępność public, dostępność zadeklarowana przez accessor_modifier może być private protected, protected internal, internal, protected lub private.
    • Jeśli właściwość lub indeksator ma zadeklarowaną dostępność protected internal, dostępność zadeklarowana przez accessor_modifier może być albo private protected, protected private, internal, protected lub private.
    • Jeśli właściwość lub indeksator ma zadeklarowaną dostępność jako internal lub protected, to dostępność zadeklarowana przez accessor_modifier musi być albo private protected, albo private.
    • Jeśli właściwość lub indeksator ma zadeklarowaną dostępność private protected, to dostępność zadeklarowana przez accessor_modifier powinna być private.
    • Jeśli właściwość lub indeksator ma zadeklarowaną dostępność private, żaden accessor_modifier nie może być użyty.

W przypadku właściwości nieposiadających wartości referencyjnych abstract, extern, ciało akcesora accessor_body dla każdego określonego akcesora jest po prostu średnikiem. Właściwość, która nie jest abstrakcyjna ani zewnętrzna, ale nie jest także indeksatorem, może również mieć accessor_body dla wszystkich akcesorów określony średnikiem; w takim przypadku jest to automatycznie zaimplementowana właściwość (§15.7.4). Właściwość zaimplementowana automatycznie musi mieć co najmniej metodę dostępu. W przypadku metod dostępu innych właściwości innych niż abstrakcyjne, inne niż extern, accessor_body jest albo:

  • blok określający instrukcje, które mają być wykonywane po wywołaniu odpowiedniego akcesora; lub
  • treść wyrażenia, która składa się z i wyrażenia , a także średnika, i oznacza pojedyncze wyrażenie do wykonania przy wywołaniu odpowiedniego akcesora.

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

  • blok określający instrukcje, które mają być wykonywane, kiedy wywoływany jest akcesor get; lub
  • Treść wyrażenia składa się z =>, następnie ref, odniesienia do zmiennej variable_reference i średnika. Odwołanie do zmiennej jest oceniane, gdy wywoływany jest akcesor 'get'.

Akcesor get dla właściwości o wartości niebędącej referencją odpowiada metodzie bez parametrów z wartością zwracaną o typie właściwości. Z wyjątkiem przypadku, gdy właściwość ta jest celem przypisania, w wyrażeniu przywołanie tej właściwości spowoduje wywołanie jej metody dostępu get w celu obliczenia wartości właściwości (§12.2.2).

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

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

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

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

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

przykład końcowy

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

Akcesor set odpowiada metodzie z jednym parametrem wartości typu właściwości i typem zwracanym void. Niejawny parametr akcesora zestawu ma zawsze nazwę value. Gdy właściwość jest przywoływana jako cel przypisania (§12.21), lub jako operand dla operatora ++ lub –- (§12.8.16, §12.9.6), akcesor ustawiający jest wywoływany z argumentem, który dostarcza nową wartość (§12.21.2). Treść akcesora typu set musi być zgodna z zasadami metod opisanymi w §15.6.11. W szczególności instrukcje return w treści akcesora set nie mogą określać wyrażenia. Ponieważ akcesor set niejawnie ma parametr o nazwie value, deklarowanie zmiennej lokalnej lub stałej o tej samej nazwie w akcesorze set prowadzi do błędu w czasie kompilacji.

Zależnie od obecności lub braku metod dostępu get i set, właściwość klasyfikuje się w następujący sposób:

  • Właściwość, która zawiera zarówno metodę dostępu get, jak i set, nazywana jest właściwością odczytu-zapisu.
  • Właściwość, która ma tylko metodę get, nazywana jest właściwością tylko do odczytu. Błąd w czasie kompilacji występuje, jeśli właściwość tylko do odczytu jest celem przypisania.
  • Właściwość, która ma tylko zestaw akcesoriów, mówi się, że jest właściwością tylko do zapisu. Z wyjątkiem celu przypisania, odwołanie się do właściwości tylko do zapisu w wyrażeniu jest błędem czasu kompilacji.

Uwaga: Operatory prefiksowe i postfiksowe ++ oraz operatory przypisania złożonego nie mogą być stosowane do właściwości tylko do zapisu, ponieważ te operatory odczytują starą wartość swojego operandu przed zapisaniem nowej. notatka końcowa

Przykład: w poniższym kodzie

public class Button : Control
{
    private string caption;

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

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

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

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

Tutaj akcesor set jest wywoływany przez przypisanie wartości do właściwości, a akcesor get przez odwoływanie się do właściwości w wyrażeniu.

przykład końcowy

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

Przykład: przykład

class A
{
    private string name;

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

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

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

przykład końcowy

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

Przykład: w poniższym kodzie

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

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

P właściwość w B ukrywa P właściwość w A zarówno przy odczycie, jak i zapisie. W ten sposób w stwierdzeniach

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

Przypisanie do b.P powoduje zgłoszenie błędu czasu kompilacji, ponieważ właściwość tylko do odczytu P w B ukrywa właściwość tylko do zapisu P w 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;
}

Tutaj klasa Label używa dwóch pól int, x i y, aby przechowywać swoją lokalizację. Lokalizacja jest publicznie ujawniona zarówno jako właściwość X i Y, jak i jako właściwość Location typu Point. 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 i y zamiast tego były polami public readonly, byłoby niemożliwe wprowadzenie takiej zmiany w klasie Label.

przykład końcowy

Uwaga: uwidacznianie stanu za pomocą właściwości niekoniecznie jest mniej wydajne niż bezpośrednie uwidacznianie pól. W szczególności, gdy właściwość jest niewirtualna i zawiera tylko niewielką ilość kodu, środowisko wykonawcze może zastąpić wywołania akcesorów rzeczywistym kodem akcesorów. Proces ten jest znany jako wstawianie w linię i sprawia, że dostęp do właściwości jest równie wydajny jak dostęp do pól, a jednocześnie zachowuje zwiększoną elastyczność właściwości. notatka końcowa

Przykład: Ponieważ wywołanie akcesora get jest koncepcyjnie równoważne z odczytem wartości pola, uważa się za zły styl programowania, jeśli akcesory get mają obserwowalne skutki uboczne. W przykładzie

class Counter
{
    private int next;

    public int Next => next++;
}

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

Konwencja "brak skutków ubocznych" dla akcesorów dostępu nie oznacza, że get akcesory nie musi być zawsze pisane tak, by po prostu zwracać wartości przechowywane w polach. W rzeczywistości metody uzyskiwania dostępu często obliczają wartość właściwości przez uzyskanie dostępu do wielu pól lub wywoływanie metod. Jednak prawidłowo zaprojektowane akcesory pobierające nie wykonują żadnych operacji, które powodują zauważalne zmiany w stanie obiektu.

przykład końcowy

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

Przykład:

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

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

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

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

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

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

Tworzony jest podstawowy element 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 nieabstrakcyjną, nieextern, niemającą wartości ref właściwością, której accessor_body zawierają tylko średniki. Właściwości automatyczne mają akcesor get i mogą mieć opcjonalnie akcesor set.

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

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

Przykład:

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

jest odpowiednikiem następującej deklaracji:

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

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

przykład końcowy

Przykład: w następujących

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

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

jest odpowiednikiem następującej deklaracji:

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

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

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

przykład końcowy

Chociaż pole zapasowe jest ukryte, może mieć przypisane atrybuty docelowe bezpośrednio dzięki automatycznie wdrożonej właściwości property_declaration (§15.7.1).

Przykład: następujący kod

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

powoduje, że atrybut ukierunkowany na pole NonSerialized jest zastosowany do pola pomocniczego generowanego przez kompilator, tak jakby kod został napisany w następujący sposób:

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

przykład końcowy

15.7.5 Ułatwienia dostępu

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

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

Po wybraniu właściwości lub indeksatora, nie mających wartości ref, domeny dostępności specyficznych akcesorów są używane do określenia, czy takie użycie jest prawidłowe.

  • Jeśli użycie jest jako wartość (§12.2.2), akcesor "get" musi istnieć i być dostępny.
  • Jeśli użycie jest celem prostego przypisania (§12.21.2), akcesor ustawiający musi istnieć i być dostępny.
  • Jeśli użycie jest celem przypisania złożonego (§12.21.4), lub jako cel operatorów ++ lub -- (§12.8.16, §12.9.6), zarówno akcesory pobierające, jak i ustawiające muszą istnieć i być dostępne.

Przykład: W poniższym przykładzie właściwość A.Text jest ukryta przez właściwość B.Text, nawet w kontekstach, w których wywoływany jest tylko akcesor set. Natomiast właściwość B.Count nie jest dostępna dla 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 ref-valued — niezależnie od tego, czy użycie występuje jako wartość, jako cel prostego przypisania, czy jako cel przypisania złożonego — domena dostępności zaangażowanego akcesora get jest używana do określenia, czy to użycie jest prawidłowe.

Akcesorium używane do implementowania interfejsu nie powinno mieć accessor_modifier. Jeśli do zaimplementowania interfejsu jest używany tylko jeden akcesor, drugi akcesor może być zadeklarowany za pomocą accessor_modifier:

Przykład:

public interface I
{
    string Prop { get; }
}

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

przykład końcowy

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

Uwaga: Ta klauzula dotyczy obu właściwości (§15.7) i indeksatorów (§15.9). Klauzula jest sformułowana w kontekście właściwości. Podczas czytania klauzuli w odniesieniu do indeksatorów, zastąp terminy indeksator/indeksatory zamiast właściwość/właściwości i zapoznaj się z listą różnic między właściwościami a indeksatorami podaną w §15.9.2. notatka końcowa

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

Deklaracja właściwości abstrakcyjnej deklaruje, że akcesory właściwości są wirtualne, ale nie dostarcza rzeczywistej implementacji tych akcesorów. Zamiast tego klasy pochodne nie abstrakcyjne są wymagane do zapewnienia własnej implementacji dla metod dostępu przez zastąpienie właściwości. Ponieważ akcesor abstrakcyjnej deklaracji właściwości nie zapewnia rzeczywistej implementacji, jego accessor_body po prostu składa się z średnika. Właściwość abstrakcyjna nie może mieć private metody dostępu.

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

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

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

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

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

  • Akcesor get odpowiada metodzie bez parametrów, która zwraca wartość typu właściwości i ma te same modyfikatory jak właściwość, którą zawiera.
  • Metoda dostępu zestawu odpowiada metodzie z pojedynczym parametrem wartości typu właściwości, typem zwracanym void i tymi samymi modyfikatorami, co właściwość zawierająca.

Przykład: w poniższym kodzie

abstract class A
{
    int y;

    public virtual int X
    {
        get => 0;
    }

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

    public abstract int Z { get; set; }
}

X jest wirtualną właściwością tylko do odczytu, Y jest wirtualną właściwością odczytu i zapisu i Z jest abstrakcyjną właściwością read-write. Ponieważ Z jest abstrakcyjna, klasa A, która ją zawiera, również powinna być zadeklarowana jako abstrakcyjna.

Poniżej przedstawiono klasę pochodzącą z A.

class B : A
{
    int z;

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

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

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

W tym miejscu deklaracje X, 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. pl-PL: Metoda dostępu get z X i metoda dostępu set z Y używają słowa kluczowego base, aby uzyskać dostęp do odziedziczonych metod dostępu. Deklaracja Z przesłania zarówno abstrakcyjne metody dostępu — w związku z tym nie ma żadnych brakujących abstract składowych funkcji w B, i B może być nieabstrakcyjną klasą.

przykład końcowy

Gdy właściwość jest zadeklarowana jako przesłonięta, wszelkie przesłonięte akcesory muszą być dostępne dla kodu przesłaniającego. Ponadto deklarowana dostępność zarówno samej właściwości lub indeksatora, jak i odpowiednich akcesorów, powinna odpowiadać dostępności przesłoniętego członka i jego akcesorów.

Przykład:

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

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

przykład końcowy

Zdarzenia 15.8

15.8.1 Ogólne

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

Zdarzenia są deklarowane przy użyciu event_declarations:

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

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

event_accessor_declarations
    : add_accessor_declaration remove_accessor_declaration
    | remove_accessor_declaration add_accessor_declaration
    ;

add_accessor_declaration
    : attributes? 'add' block
    ;

remove_accessor_declaration
    : attributes? 'remove' block
    ;

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

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

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

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

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

Deklaracja zdarzenia, która pomija event_accessor_declaration, definiuje jedno lub więcej zdarzeń — jedno dla każdego z variable_declaratorów. Atrybuty i modyfikatory mają zastosowanie do wszystkich członków deklarowanych przez takie event_declaration.

Jest to błąd czasu kompilacji, gdy event_declaration zawiera zarówno modyfikator , jak i deklaracje akcesorów zdarzeń .

Gdy deklaracja zdarzenia zawiera modyfikator extern, zdarzenie nazywa się zewnętrznym zdarzeniem. Ponieważ deklaracja zdarzenia zewnętrznego nie zawiera rzeczywistej implementacji, jest błędem, aby zawierała zarówno extern modyfikator, jak i event_accessor_declaration.

Jest to błąd kompilacji, jeśli variable_declarator deklaracji zdarzenia z modyfikatorem abstract lub external zawiera variable_initializer.

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

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

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

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

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

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

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

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

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

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

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

przykład końcowy

15.8.2 Zdarzenia przypominające pola

W tekście programu klasy lub struktury, która zawiera deklarację zdarzenia, niektóre zdarzenia mogą być używane jak pola. Aby można było wykorzystać zdarzenie w ten sposób, nie może być ono abstrakcyjne ani oznaczone jako extern, i nie powinno jawnie zawierać event_accessor_declaration. Takie zdarzenie może być używane w dowolnym kontekście, który dopuszcza pole. Pole zawiera delegata (§20), który odnosi się do listy obsługiwaczy zdarzeń dodanych do zdarzenia. Jeśli nie dodano żadnych procedur obsługi zdarzeń, pole zawiera null.

Przykład: w poniższym kodzie

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

public class Button : Control
{
    public event EventHandler Click;

    protected void OnClick(EventArgs e)
    {
        EventHandler handler = Click;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    public void Reset() => Click = null;
}

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

Poza deklaracją klasy Button członka Click można używać tylko po lewej stronie operatorów += i –=, jak в

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

dołącza delegata do listy wywołań zdarzenia Click 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 muszą) być wykonywane podczas trzymania blokady (§13.13) na obiekcie zawierającym dla zdarzenia instancyjnego lub na obiekcie (§12.8.18) dla zdarzenia statycznego.

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

class X
{
    public event D Ev;
}

powinno być skompilowane na coś równoważnego z:

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

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

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

notatka końcowa

15.8.3 Dostęp do zdarzeń

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

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

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

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

Ponieważ akcesor zdarzenia niejawnie ma parametr o nazwie value, jest to błąd czasu kompilacji, jeśli zmienna lokalna lub stała zadeklarowana w akcesorze zdarzenia ma tę nazwę.

Przykład: w poniższym kodzie


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

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

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

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

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

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

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

Control klasa implementuje wewnętrzny mechanizm przechowywania zdarzeń. 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 przechowywania został zaprojektowany tak, aby nie było kosztowne skojarzenie delegata null z kluczem, a tym samym nieobsługiwane zdarzenia nie zajmują miejsca do przechowywania.

przykład końcowy

15.8.4 Zdarzenia statyczne i wystąpienia

Gdy deklaracja zdarzenia zawiera static modyfikator, zdarzenie jest podobno zdarzeniem statycznym. Kiedy nie ma static modyfikatora, mówi się, że zdarzenie jest zdarzeniem instancji.

Zdarzenie statyczne nie jest związane z konkretnym wystąpieniem, a odwołanie się do this w akcesorach zdarzenia statycznego skutkuje błędem w czasie kompilacji.

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

Różnice między elementami statycznymi a elementami instancji zostały omówione w §15.3.8.

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

Deklaracja zdarzenia wirtualnego określa, że akcesory tego zdarzenia są wirtualne. Modyfikator virtual ma zastosowanie do obu akcesorów zdarzenia.

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

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

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

Akcesory dziedziczonego zdarzenia typu wirtualnego mogą zostać zastąpione w klasie pochodnej przez dołączenie deklaracji zdarzenia, która określa modyfikator override. Jest to nazywane deklaracją zdarzenia zastępującą. Nadpisanie deklaracji zdarzenia nie tworzy nowego zdarzenia. Zamiast tego po prostu specjalizuje się w implementacjach akcesoriów istniejącego zdarzenia wirtualnego.

Deklaracja zdarzenia zastępującego powinna określać dokładnie te same modyfikatory dostępu i nazwę, co zdarzenie zastępowane, między typem zdarzenia zastępującego a typem wydarzenia zastępowanego powinna występować konwersja tożsamości, a zarówno akcesor dodający, jak i usuwający powinny zostać określone w tej deklaracji.

Deklaracja zastępująca zdarzenie może obejmować sealed modyfikator. this Użycie modyfikatora uniemożliwia dalsze zastępowanie zdarzenia przez klasę pochodną. Dostępy do zabezpieczonego zdarzenia są również zabezpieczone.

Jest to błąd kompilacji, gdy w przesłaniającej deklaracji zdarzenia zawiera się modyfikator new.

Z wyjątkiem różnic w składni deklaracji i wywołania, wirtualne, zapieczętowane, zastępowane i abstrakcyjne akcesory zachowują się tak samo jak metody wirtualne, zapieczętowane, zastępowane i abstrakcyjne. W szczególności zasady opisane w §15.6.4, §15.6.5, §15.6.6 i §15.6.7 mają zastosowanie tak, jakby akcesory były metodami odpowiedniego rodzaju. Każdy akcesor odpowiada metodzie z pojedynczym parametrem wartości typu zdarzenia, typem zwracanym void i tymi samymi modyfikatorami co zdarzenie, które zawiera.

15.9 Indeksatory

15.9.1 Ogólne

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

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

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

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

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

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

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

Istnieją dwa rodzaje indexer_declaration:

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

Indexer_declaration może zawierać zestaw atrybutów (§22) oraz dowolny z dozwolonych rodzajów deklarowanej dostępności (§15.3.6), new (§15.3.5), virtual (§15.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7) i extern (§15.6.8) modyfikatorów.

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

Typ deklaracji indeksatora określa typ elementu indeksatora wprowadzonego przez deklarację.

Uwaga: Ponieważ indeksatory są przeznaczone do użycia w kontekstach przypominających element tablicy, termin typ elementu zdefiniowany dla tablicy jest również używany z indeksatorem. notatka końcowa

Chyba że indeksator jest jawną implementacją elementu członkowskiego interfejsu, typ jest następnie ze słowem kluczowym this. W przypadku jawnej implementacji elementu członkowskiego interfejsu, typ jest następnie poprzedzony przez interface_type, "." i słowo kluczowe this. W przeciwieństwie do innych członków, indeksatory nie mają nazw zdefiniowanych przez użytkownika.

Parameter_list określa parametry indeksatora. Lista parametrów indeksatora odpowiada tej metodzie (§15.6.2), z tą różnicą, że określono co najmniej jeden parametr i że 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).

Treść indeksatora (indexer_body) może składać się z ciała instrukcji (§15.7.1) lub ciała wyrażenia (§15.6.1). W treści instrukcji, accessor_declarations, które muszą być ujęte w tokenach „{” i „}”, deklarują akcesory (§15.7.3) indeksatora. Metody dostępu określają polecenia do wykonania związane z odczytywaniem i zapisywaniem elementów indeksatora.

W indexer_body treść wyrażenia składająca się z „=>”, po której następuje wyrażenieE i średnik, jest dokładnym odpowiednikiem treści instrukcji { get { return E; } }, i dlatego może być używana tylko do określania indeksatorów wyłącznie do odczytu, gdzie wynik akcesora get jest podawany przez pojedyncze wyrażenie.

ref_indexer_body może składać się z treści instrukcji lub treści wyrażenia. W treści instrukcji get_accessor_declaration deklaruje metodę get (§15.7.3) indeksatora. Akcesor określa instrukcje wykonywalne związane z odczytem indeksatora.

W ref_indexer_body treść wyrażenia składająca się z =>, po którym następuje ref, variable_referenceV i średnik jest dokładnym odpowiednikiem treści instrukcji { get { return ref V; } }.

Uwaga: mimo że składnia uzyskiwania dostępu do elementu indeksatora jest taka sama jak w przypadku elementu tablicy, element indeksatora nie jest klasyfikowany jako zmienna. W związku z tym nie można przekazać elementu indeksatora jako argumentu in, 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 nazywany jest indeksatorem zewnętrznym. Ponieważ deklaracja indeksatora zewnętrznego nie zapewnia rzeczywistej implementacji, każda z accessor_bodys w jej accessor_declarations jest średnikiem.

Przykład: Poniższy przykład deklaruje klasę BitArray , która implementuje indeksator na potrzeby uzyskiwania dostępu do poszczególnych bitów w tablicy bitowej.

class BitArray
{
    int[] bits;
    int length;

    public BitArray(int length)
    {
        if (length < 0)
        {
            throw new ArgumentException();
        }
        bits = new int[((length - 1) >> 5) + 1];
        this.length = length;
    }

    public int Length => length;

    public bool this[int index]
    {
        get
        {
            if (index < 0 || index >= length)
            {
                throw new IndexOutOfRangeException();
            }
            return (bits[index >> 5] & 1 << index) != 0;
        }
        set
        {
            if (index < 0 || index >= length)
            {
                throw new IndexOutOfRangeException();
            }
            if (value)
            {
                bits[index >> 5] |= 1 << index;
            }
            else
            {
                bits[index >> 5] &= ~(1 << index);
            }
        }
    }
}

Wystąpienie klasy BitArray zużywa znacznie mniej pamięci niż odpowiadająca mu klasa bool[] (ponieważ każda wartość pierwszej zajmuje tylko jeden bit zamiast jednego byte), ale umożliwia te same operacje co bool[].

W poniższej klasie CountPrimes używa się klasycznego algorytmu "sita" BitArray do obliczenia liczby liczb pierwszych z zakresu od 2 do podanej wartości maksymalnej.

class CountPrimes
{
    static int Count(int max)
    {
        BitArray flags = new BitArray(max + 1);
        int count = 0;
        for (int i = 2; i <= max; i++)
        {
            if (!flags[i])
            {
                for (int j = i * 2; j <= max; j += i)
                {
                    flags[j] = true;
                }
                count++;
            }
        }
        return count;
    }

    static void Main(string[] args)
    {
        int max = int.Parse(args[0]);
        int count = Count(max);
        Console.WriteLine($"Found {count} primes between 2 and {max}");
    }
}

Należy pamiętać, że składnia uzyskiwania dostępu do elementów elementu BitArray jest dokładnie taka sama jak w przypadku elementu bool[].

W poniższym przykładzie przedstawiono klasę siatki 26×10 zawierającą indeksator z dwoma parametrami. Pierwszy parametr musi być wielką lub małą literą w zakresie A–Z, a drugi musi być liczbą całkowitą w zakresie od 0 do 9.

class Grid
{
    const int NumRows = 26;
    const int NumCols = 10;
    int[,] cells = new int[NumRows, NumCols];

    public int this[char row, int col]
    {
        get
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            return cells[row - 'A', col];
        }
        set
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException ("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            cells[row - 'A', col] = value;
        }
    }
}

przykład końcowy

15.9.2 Indeksator i różnice właściwości

Indeksatory i właściwości są bardzo podobne w koncepcji, ale różnią się w następujący sposób:

  • Właściwość jest identyfikowana przez jego nazwę, podczas gdy indeksator jest identyfikowany przez jego podpis.
  • Dostęp do właściwości można uzyskać za pośrednictwem simple_name (§12.8.4) lub member_access (§12.8.7), natomiast dostęp do elementu indeksatora odbywa się za pośrednictwem element_access (§12.8.12.3).
  • Właściwość może być statycznym członkiem, natomiast indeksator jest zawsze członkiem instancji.
  • Akcesor get właściwości odpowiada metodzie bez parametrów, natomiast akcesor get indeksatora odpowiada metodzie z tą samą listą parametrów co indeksator.
  • Akcesor ustawiający właściwości odpowiada metodzie z pojedynczym parametrem o nazwie value, natomiast akcesor ustawiający indeksatora odpowiada metodzie z tą samą listą parametrów co indeksator oraz dodatkowym parametrem o nazwie value.
  • Występuje błąd czasu kompilacji, gdy metoda dostępu indeksatora posiada lokalną zmienną lub stałą o tej samej nazwie co parametr indeksatora.
  • W deklaracji właściwości zastępowania dostęp do dziedziczonej właściwości jest uzyskiwany przy użyciu składni base.P, 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 jest nie tylko nieabstrakcyjny, ale także niezewnętrzny, z dwukropkami accessor_bodys.

Oprócz tych różnic, wszystkie reguły zdefiniowane w §15.7.3, §15.7.5 i §15.7.6 mają zastosowanie do akcesoriów indeksatora, a także akcesoriów do właściwości.

Zastąpienie właściwości/cech indeksatorem/indeksatorami podczas odczytywania §15.7.3, §15.7.5 i §15.7.6 odnosi się również do zdefiniowanych terminów. W szczególności właściwość do odczytu i zapisu staje się indeksatorem do odczytu i zapisu, właściwość tylko do odczytu staje się indeksatorem tylko do odczytu, a właściwość tylko do zapisu staje się indeksatorem tylko do zapisu.

15.10 Operatory

15.10.1 Ogólne

Operator jest elementem członkowskim, który definiuje znaczenie operatora wyrażenia, który można zastosować do wystąpień klasy. Operatory są deklarowane przy użyciu operator_declarations:

operator_declaration
    : attributes? operator_modifier+ operator_declarator operator_body
    ;

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

operator_declarator
    : unary_operator_declarator
    | binary_operator_declarator
    | conversion_operator_declarator
    ;

unary_operator_declarator
    : type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
    ;

logical_negation_operator
    : '!'
    ;

overloadable_unary_operator
    : '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
    ;

binary_operator_declarator
    : type 'operator' overloadable_binary_operator
        '(' fixed_parameter ',' fixed_parameter ')'
    ;

overloadable_binary_operator
    : '+'  | '-'  | '*'  | '/'  | '%'  | '&' | '|' | '^'  | '<<' 
    | right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
    ;

conversion_operator_declarator
    : 'implicit' 'operator' type '(' fixed_parameter ')'
    | 'explicit' 'operator' type '(' fixed_parameter ')'
    ;

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

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

Uwaga: Operatory negacji logicznej prefiksu (§12.9.4) i postfix null-forgiving (§12.8.9), reprezentowane przez ten sam token leksykacyjny (!), są odrębne. Ten ostatni nie jest operatorem przeciążalnym. notatka końcowa

Istnieją trzy kategorie operatorów przeciążalnych: operatory jednoargumentowe (§15.10.2), operatory binarne (§15.10.3) i operatory konwersji (§15.10.4).

Operator_body to średnik, treść bloku (§15.6.1) lub treść wyrażenia (§15.6.1). Treść bloku składa się z bloku, który określa instrukcje do wykonania po wywołaniu operatora. Blok jest zgodny z zasadami metod zwracania wartości opisanych w §15.6.11. Treść wyrażenia składa się z =>, po którym następuje wyrażenie i średnik, i oznacza pojedyncze wyrażenie do wykonania przy wywołaniu operatora.

W przypadku extern operatorów operator_body składa się po prostu ze średnika. W przypadku wszystkich innych operatorów operator_body jest treścią bloku lub treścią wyrażenia.

Następujące reguły mają zastosowanie do wszystkich deklaracji operatorów:

  • Deklaracja operatora powinna zawierać zarówno modyfikator public, jak i static.
  • Parametry operatora nie mają żadnych modyfikatorów innych niż in.
  • Podpis operatora (§15.10.2, §15.10.3, §15.10.4) różni się od podpisów wszystkich innych operatorów zadeklarowanych w tej samej klasie.
  • Wszystkie typy, do których odwołuje się deklaracja operatora, są co najmniej tak dostępne, jak sam operator (§7.5.5).
  • Jest to błąd dla tego samego modyfikatora, który pojawia się wiele razy w deklaracji operatora.

Każda kategoria operatorów nakłada dodatkowe ograniczenia, zgodnie z opisem w poniższych podklasach.

Podobnie jak inni członkowie, operatory zadeklarowane w klasie bazowej są dziedziczone przez klasy pochodne. Ponieważ deklaracje operatorów zawsze wymagają klasy lub struktury, w której operator jest zadeklarowany do udziału w podpisie operatora, nie jest możliwe, aby operator zadeklarowany w klasie pochodnej ukrywał operator zadeklarowany w klasie bazowej. new W związku z tym modyfikator nigdy nie jest wymagany i dlatego nigdy nie jest dozwolony w deklaracji operatora.

Dodatkowe informacje na temat operatorów jednoargumentowych i binarnych można znaleźć w §12.4.

Dodatkowe informacje na temat operatorów konwersji można znaleźć w §10.5.

15.10.2 Operatory jednoargumentowe

Następujące reguły dotyczą deklaracji operatorów jednoargumentowych, gdzie T określa typ wystąpienia klasy lub struktury zawierającej deklarację operatora:

  • Jednoargumentowy +, -, ! (tylko negacja logiczna) lub ~ operator powinien przyjmować jeden parametr typu T lub T? i może zwrócić dowolny typ.
  • Jednoargumentowy ++ lub -- operator powinien przyjmować jeden parametr typu T lub T?, i powinien zwracać ten sam typ lub typ pochodzący z niego.
  • Jednoargumentowy true lub false operator powinien mieć jeden parametr typu T lub T?, a 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 jednoargumentowe true i false wymagają deklaracji parami. Błąd czasu kompilacji występuje, jeśli klasa deklaruje jeden z tych operatorów bez deklarowania drugiego. Operatory true i false zostały opisane dalej w §12.24.

Przykład: Poniższy przykład przedstawia implementację i kolejne użycie operatora++ dla klasy wektorów całkowitych:

public class IntVector
{
    public IntVector(int length) {...}
    public int Length { get { ... } }                      // Read-only property
    public int this[int index] { get { ... } set { ... } } // Read-write indexer

    public static IntVector operator++(IntVector iv)
    {
        IntVector temp = new IntVector(iv.Length);
        for (int i = 0; i < iv.Length; i++)
        {
            temp[i] = iv[i] + 1;
        }
        return temp;
    }
}

class Test
{
    static void Main()
    {
        IntVector iv1 = new IntVector(4); // Vector of 4 x 0
        IntVector iv2;
        iv2 = iv1++;              // iv2 contains 4 x 0, iv1 contains 4 x 1
        iv2 = ++iv1;              // iv2 contains 4 x 2, iv1 contains 4 x 2
    }
}

Zwróć uwagę, jak metoda operatora zwraca wartość uzyskaną przez dodanie 1 do operandu, podobnie jak operatory postfiksyjne inkrementacji i dekrementacji (§12.8.16) oraz operatory prefiksyjne inkrementacji i dekrementacji (§12.9.6). W przeciwieństwie do języka C++, ta metoda nie powinna modyfikować wartości operandu bezpośrednio, ponieważ naruszałoby to standardową semantykę operatora inkrementacji postfiksowej (§12.8.16).

przykład końcowy

15.10.3 Operatory binarne

Następujące reguły dotyczą deklaracji operatorów binarnych, gdzie T określa typ wystąpienia klasy lub struktury zawierającej deklarację operatora:

  • Operator binarny bez zmian musi przyjmować dwa parametry, z których co najmniej jeden ma typ T lub T?, i może zwrócić dowolny typ.
  • Operator << binarny lub >> (§12.11) bierze dwa parametry, z których pierwszy ma typ T lub T?, a drugi ma typ int 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 w parach. Dla każdej deklaracji jednego operatora pary musi istnieć zgodna deklaracja innego operatora pary. Dwa deklaracje operatorów pasują do siebie, jeśli istnieją konwersje tożsamości między ich typami zwracanymi a odpowiadającymi im typami parametrów. Następujące operatory wymagają deklarowania w parach:

  • operator == i operator !=
  • operator > i operator <
  • operator >= i operator <=

15.10.4 Operatory konwersji

Deklaracja operatora konwersji wprowadza konwersję zdefiniowaną przez użytkownika (§10.5), która rozszerza wstępnie zdefiniowane niejawne i jawne konwersje.

Deklaracja operatora konwersji zawierająca implicit słowo kluczowe wprowadza zdefiniowaną przez użytkownika niejawną konwersję. Niejawne konwersje mogą wystąpić w różnych sytuacjach, w tym wywołania składowych funkcji, wyrażenia rzutowania i przypisania. Opisano to dalej w §10.2.

Deklaracja operatora konwersji zawierająca explicit słowo kluczowe wprowadza jawną konwersję zdefiniowaną przez użytkownika. Jawne konwersje mogą występować w wyrażeniach rzutowanych i są opisane dalej w §10.3.

Operator konwersji konwertuje z typu źródła wskazanego przez typ parametru operatora konwersji na typ docelowy wskazany przez typ zwracany operatora konwersji.

Dla danego typu źródłowego S i typu docelowego T, jeśli S lub T są typami dopuszczającymi wartości null, niech S₀ i T₀ odwołują się do ich typów bazowych; w przeciwnym razie S₀ i T₀ są odpowiednio równe S i T. Klasa lub struktura może zadeklarować konwersję z typu źródłowego na typ 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 S₀, ani T₀ nie jest interface_type.

  • Z wyłączeniem konwersji zdefiniowanych przez użytkownika, nie istnieje konwersja z S do T ani 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 int i z int do C, 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 (istnieje konwersja niejawna, a tym samym jawna, z dowolnego typu na typ object).

przykład końcowy

W przypadkach, gdy istnieje wstępnie zdefiniowana konwersja między dwoma typami, wszelkie konwersje zdefiniowane przez użytkownika między tymi typami są ignorowane. Szczególnie:

  • Jeśli istnieje wstępnie zdefiniowana niejawna konwersja (§10.2) z typu S do typu T, wszystkie konwersje zdefiniowane przez użytkownika (niejawne lub jawne) z S do T są ignorowane.
  • Jeśli istnieje wstępnie zdefiniowana jawna konwersja (§10.3) z typu S na typ T, to wszystkie jawne konwersje zdefiniowane przez użytkownika z S na T są ignorowane. Ponadto:
    • Jeśli którykolwiek z S 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 z wyjątkiem object operatory zadeklarowane powyżej przez typ Convertible<T> nie powodują konfliktu z wstępnie zdefiniowanymi konwersjami.

Przykład:

void F(int i, Convertible<int> n)
{
    i = n;                    // Error
    i = (int)n;               // User-defined explicit conversion
    n = i;                    // User-defined implicit conversion
    n = (Convertible<int>)i;  // User-defined implicit conversion
}

Jednak w przypadku typów object konwersje wstępnie zdefiniowane ukrywają konwersje zdefiniowane przez użytkownika we wszystkich przypadkach oprócz jednego.

void F(object o, Convertible<object> n)
{
    o = n;                       // Pre-defined boxing conversion
    o = (object)n;               // Pre-defined boxing conversion
    n = o;                       // User-defined implicit conversion
    n = (Convertible<object>)o;  // Pre-defined unboxing conversion
}

przykład końcowy

Konwersje zdefiniowane przez użytkownika nie mogą być konwertowane z lub na interface_types. W szczególności to ograniczenie gwarantuje, że podczas konwersji na interface_type nie wystąpią żadne przekształcenia zdefiniowane przez użytkownika, a konwersja na interface_type powiedzie się tylko wtedy, gdy object konwersja rzeczywiście implementuje określony interface_type.

Podpis operatora konwersji składa się z typu źródłowego i typu docelowego. (Jest to jedyna forma elementu członkowskiego, dla którego zwracany typ uczestniczy w podpisie). Niejawna lub jawna klasyfikacja operatora konwersji nie jest częścią podpisu operatora. W związku z tym klasa lub struktura nie może zadeklarować zarówno niejawnego, jak i jawnego operatora konwersji z tymi samymi typami źródłowymi i docelowymi.

Uwaga: Ogólnie rzecz biorąc, niejawne konwersje zdefiniowane przez użytkownika powinny być zaprojektowane tak, aby nigdy nie zgłaszać wyjątków i nigdy nie tracić informacji. Jeśli konwersja zdefiniowana przez użytkownika może spowodować powstanie wyjątków (na przykład dlatego, że argument źródłowy jest poza zakresem) lub utrata informacji (takich jak odrzucanie bitów o wysokiej kolejności), ta konwersja powinna być zdefiniowana jako jawna konwersja. notatka końcowa

Przykład: w poniższym kodzie

public struct Digit
{
    byte value;

    public Digit(byte value)
    {
        if (value < 0 || value > 9)
        {
            throw new ArgumentException();
        }
        this.value = value;
    }

    public static implicit operator byte(Digit d) => d.value;
    public static explicit operator Digit(byte b) => new Digit(b);
}

konwersja z Digit na byte jest niejawna, ponieważ nigdy nie zgłasza wyjątków ani nie traci informacji, ale konwersja z byte na Digit jest jawna, ponieważ Digit może reprezentować tylko podzbiór możliwych wartości byte.

przykład końcowy

15.11 Konstruktory wystąpień

15.11.1 Ogólne

Konstruktor instance constructor jest członkiem, który implementuje akcje wymagane do zainicjowania wystąpienia klasy. Konstruktory wystąpień są deklarowane przy użyciu constructor_declarations:

constructor_declaration
    : attributes? constructor_modifier* constructor_declarator constructor_body
    ;

constructor_modifier
    : 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

constructor_declarator
    : identifier '(' parameter_list? ')' constructor_initializer?
    ;

constructor_initializer
    : ':' 'base' '(' argument_list? ')'
    | ':' 'this' '(' argument_list? ')'
    ;

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

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

Deklaracja konstruktora może zawierać zestaw atrybutów (§22), dowolny z dozwolonych rodzajów zadeklarowanej dostępności (§15.3.6) oraz modyfikator extern (§15.6.8). Deklaracja konstruktora nie może zawierać tego samego modyfikatora wiele razy.

Identyfikator constructor_declarator powinien nazywać klasę, w której zadeklarowany jest konstruktor instancji. Jeśli zostanie określona inna nazwa, wystąpi błąd czasu kompilacji.

Opcjonalne parameter_list konstruktora wystąpienia podlega tym samym regułom co parameter_list metody (§15.6). this Ponieważ modyfikator parametrów ma zastosowanie tylko do metod rozszerzeń (§15.6.10), żaden parametr w parameter_list konstruktora nie zawiera this modyfikatora. Lista parametrów definiuje podpis (§7.6) konstruktora wystąpienia i zarządza procesem, w którym rozpoznawanie przeciążenia (§12.6.4) wybiera konkretny konstruktor wystąpienia w wywołaniu.

Każdy z typów, które są wymienione w parameter_list konstruktora wystąpienia, musi być co najmniej tak samo dostępny, jak sam konstruktor (§7.5.5).

Opcjonalny constructor_initializer określa inny konstruktor wystąpienia do wywołania przed wykonaniem instrukcji podanych w constructor_body tego konstruktora wystąpienia. Opisano to dalej w §15.11.2.

Gdy deklaracja konstruktora zawiera extern modyfikator, konstruktor nazywany jest konstruktorem zewnętrznym. Ponieważ deklaracja konstruktora zewnętrznego nie zawiera rzeczywistej implementacji, jego constructor_body składa się z średnika. W przypadku wszystkich innych konstruktorów constructor_body składa się z jednego z następujących elementów:

  • blok, który określa instrukcje inicjowania nowego wystąpienia klasy; lub
  • treść wyrażenia, która składa się z =>, po którym następuje wyrażenie i średnik, i oznacza pojedyncze wyrażenie, aby zainicjować nowe wystąpienie klasy.

"Ciało konstrukcyjne, które jest ciałem bloku lub wyrażenia, odpowiada dokładnie blokowi metody instancji z typem zwracanym void (§15.6.11)."

Konstruktory wystąpień nie są dziedziczone. W związku z tym klasa nie ma konstruktorów wystąpień innych niż te rzeczywiście zadeklarowane w klasie, z wyjątkiem, że jeśli klasa nie zawiera deklaracji konstruktora wystąpienia, domyślny konstruktor wystąpienia jest udostępniany automatycznie (§15.11.5).

Konstruktory wystąpień są wywoływane przez wyrażenia tworzenia obiektów (§12.8.17.2) oraz przez inicjalizatory konstruktora.

Inicjalizatory konstruktorów 15.11.2

Wszystkie konstruktory wystąpień (z wyjątkiem tych dla klasy object) niejawnie obejmują wywołanie innego konstruktora wystąpienia bezpośrednio przed ciałem konstruktora. Konstruktor, który ma być niejawnie wywoływany, jest określany przez constructor_initializer:

  • Inicjalizator konstruktora instancji w postaci base(argument_list) (gdzie argument_list jest opcjonalny) powoduje wywołanie konstruktora instancji z najbliższej klasy bazowej. Ten konstruktor jest wybierany przy użyciu argument_list i reguł rozpoznawania przeciążenia §12.6.4. Zestaw konstruktorów obiektów kandydujących składa się ze wszystkich dostępnych konstruktorów obiektów bezpośredniej klasy bazowej. Jeśli ten zestaw jest pusty lub nie można zidentyfikować jednego najlepszego konstruktora wystąpienia, wystąpi błąd czasu kompilacji.
  • Inicjalizator konstruktora instancji w formie this(argument_list) (gdzie argument_list jest opcjonalna) wywołuje inny konstruktor instancji z tej samej klasy. Konstruktor jest wybierany przy użyciu argument_list i reguł rozpoznawania przeciążenia §12.6.4. Zestaw kandydatów konstruktorów instancji składa się ze wszystkich konstruktorów instancji zadeklarowanych w samej klasie. Jeśli wynikowy zestaw odpowiednich konstruktorów wystąpień jest pusty lub nie można zidentyfikować jednego najlepszego konstruktora wystąpienia, wystąpi błąd czasu kompilacji. Jeśli deklaracja konstruktora obiektu odwołuje się do samej siebie za pośrednictwem łańcucha co najmniej jednego inicjalizatora konstruktora, wystąpi błąd czasu kompilacji.

Jeśli konstruktor wystąpienia nie ma inicjatora konstruktora, niejawnie udostępniany jest inicjator konstruktora w postaci base().

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

C(...) {...}

jest dokładnie równoważne

C(...) : base() {...}

notatka końcowa

Zakres parametrów określonych przez parameter_list deklaracji konstruktora instancji obejmuje inicjator tego konstruktora. W związku z tym inicjator konstruktora może uzyskać dostęp do parametrów konstruktora.

Przykład:

class A
{
    public A(int x, int y) {}
}

class B: A
{
    public B(int x, int y) : base(x + y, x - y) {}
}

przykład końcowy

Inicjator konstruktora wystąpienia nie może uzyskać dostępu do tworzonego wystąpienia. W związku z tym jest to błąd czasu kompilacji, aby odwołać się do tego w wyrażeniu argumentu inicjatora konstruktora, ponieważ jest to błąd czasu kompilacji dla wyrażenia argumentu w celu odwołania się do dowolnego elementu członkowskiego wystąpienia za pośrednictwem simple_name.

Inicjalizatory zmiennych wystąpień 15.11.3

Gdy konstruktor wystąpienia nie ma inicjatora konstruktora lub ma inicjator konstruktora w postaci base(...), ten konstruktor niejawnie wykonuje inicjalizacje określone przez inicjalizator zmiennej pól wystąpienia zadeklarowanych w tej klasie. Odpowiada to sekwencji przypisań, które są wykonywane natychmiast po wejściu do konstruktora i przed niejawnym wywołaniem bezpośredniego konstruktora klasy bazowej. Inicjatory zmiennych są wykonywane w kolejności tekstowej, w której są wyświetlane w deklaracji klasy (§15.5.6).

15.11.4 Wykonywanie konstruktora

Inicjatory zmiennych są przekształcane w instrukcje przypisania, a te instrukcje przypisania są wykonywane przed wywołaniem konstruktora wystąpienia klasy bazowej. Ta kolejność gwarantuje, że wszystkie pola wystąpień są inicjowane przez inicjatory zmiennych przed wykonaniem wszelkich instrukcji, które mają dostęp do tego wystąpienia.

Przykład: biorąc pod uwagę następujące kwestie:

class A
{
    public A()
    {
        PrintFields();
    }

    public virtual void PrintFields() {}
}
class B: A
{
    int x = 1;
    int y;

    public B()
    {
        y = -1;
    }

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

gdy nowy B() używany jest do tworzenia wystąpienia B, generowane są następujące dane wyjściowe:

x = 1, y = 0

Wartość x wynosi 1, ponieważ inicjator zmiennej jest wykonywany przed wywołaniem konstruktora instancji klasy bazowej. Jednak wartość y to 0 (wartość domyślna klasy int), ponieważ przypisanie do y nie jest wykonywane dopiero po powrocie konstruktora klasy bazowej. Warto myśleć o inicjatorach zmiennych wystąpień i inicjatorach konstruktorów jako o instrukcjach, które są automatycznie wstawiane przed constructor_body. Przykł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, tak jakby miał inicjator konstruktora w formie base(). Jeśli klasa jest abstrakcyjna, zadeklarowana dostępność dla konstruktora domyślnego jest chroniona. W przeciwnym razie zadeklarowane ułatwienia dostępu dla konstruktora domyślnego są publiczne.

Uwaga: W związku z tym domyślny konstruktor jest zawsze w postaci

protected C(): base() {}

lub

public C(): base() {}

gdzie C to nazwa klasy.

notatka końcowa

Jeśli rozpoznawanie przeciążenia nie może określić jednoznacznie najlepszego kandydata dla inicjatora konstruktora klasy bazowej, wystąpi błąd czasu kompilacji.

Przykład: w poniższym kodzie

class Message
{
    object sender;
    string text;
}

Jest dostarczany konstruktor domyślny, ponieważ klasa nie zawiera deklaracji konstruktora instancji. Zatem przykład jest dokładnie odpowiednikiem

class Message
{
    object sender;
    string text;

    public Message() : base() {}
}

przykład końcowy

15.12 Konstruktory statyczne

Konstruktor statyczny jest członkiem, który implementuje akcje wymagane do zainicjowania zamkniętej klasy. Konstruktory statyczne są deklarowane przy użyciu static_constructor_declarations:

static_constructor_declaration
    : attributes? static_constructor_modifiers identifier '(' ')'
        static_constructor_body
    ;

static_constructor_modifiers
    : 'static'
    | 'static' 'extern' unsafe_modifier?
    | 'static' unsafe_modifier 'extern'?
    | 'extern' 'static' unsafe_modifier?
    | 'extern' unsafe_modifier 'static'
    | unsafe_modifier 'static' 'extern'?
    | unsafe_modifier 'extern' 'static'
    ;

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

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

static_constructor_declaration może zawierać zestaw atrybutów (§22) i extern modyfikator (§15.6.8).

Identyfikator static_constructor_declaration powinien nazywać klasę, w której zadeklarowany jest konstruktor statyczny. Jeśli zostanie określona inna nazwa, wystąpi błąd czasu kompilacji.

Gdy deklaracja konstruktora statycznego zawiera extern modyfikator, konstruktor statyczny nazywany jest zewnętrznym konstruktorem statycznym. Ponieważ zewnętrzna deklaracja konstruktora statycznego nie zapewnia rzeczywistej implementacji, jego static_constructor_body składa się z średnika. W przypadku wszystkich innych deklaracji konstruktorów statycznych static_constructor_body składa się z jednego z następujących elementów:

  • blok, który określa instrukcje do wykonania w celu zainicjowania klasy; lub
  • ciało wyrażenia, które składa się z => następującego po nim wyrażenia i średnika, i wskazuje pojedyncze wyrażenie do wykonania w celu zainicjowania klasy.

Ciało_konstruktora_statycznego, które jest blokiem lub ciałem wyrażeniowym, odpowiada dokładnie ciału_metody statycznej metody z typem zwracanym void (§15.6.11).

Konstruktory statyczne nie są dziedziczone i nie mogą być wywoływane bezpośrednio.

Konstruktor statyczny dla zamkniętej klasy jest wykonywany co najwyżej raz w danej domenie aplikacji. Wykonanie konstruktora statycznego jest wyzwalane przez pierwsze z następujących zdarzeń w domenie aplikacji:

  • Tworzone jest wystąpienie klasy.
  • Odwołano się do dowolnych statycznych elementów członkowskich klasy.

Jeśli klasa zawiera metodę Main (§7.1), w której rozpoczyna się wykonywanie, konstruktor statyczny dla tej klasy jest wywoływany przed wywołaniem metody Main.

Aby zainicjować nowy typ zamkniętej klasy, najpierw zostanie utworzony nowy zestaw pól statycznych (§15.5.2) dla tego określonego typu zamkniętego. Każde z pól statycznych jest inicjowane do wartości domyślnej (§15.5.5). Następnie dla tych pól statycznych są wykonywane inicjatory pól statycznych (§15.5.6.2). Na końcu wykonywany jest konstruktor statyczny.

Przykład: przykład

class Test
{
    static void Main()
    {
        A.F();
        B.F();
    }
}

class A
{
    static A()
    {
        Console.WriteLine("Init A");
    }

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

class B
{
    static B()
    {
        Console.WriteLine("Init B");
    }

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

musi wygenerować dane wyjściowe:

Init A
A.F
Init B
B.F

ponieważ wykonanie 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 uruchomić metodę Main, system najpierw wywołuje inicjator dla klasy B.Y, przed uruchomieniem konstruktora statycznego klasy B. Y inicjalizator powoduje uruchomienie konstruktora Astatic, ponieważ przywołano wartość A.X. 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 na 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 deterministyczny, natomiast w języku C# finalizator nie jest. Aby uzyskać określenie zachowania z języka C#, należy użyć polecenia Dispose. notatka końcowa

Finalizator to członek, który implementuje akcje wymagane do sfinalizowania wystąpienia klasy. Finalizator jest deklarowany przy użyciu finalizer_declaration:

finalizer_declaration
    : attributes? '~' identifier '(' ')' finalizer_body
    | attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
      finalizer_body
    | attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
      finalizer_body
    ;

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

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

Finalizer_declaration może zawierać zestaw atrybutów(§22).

Identyfikator finalizer_declarator powinien nazywać klasę, w której finalizator jest zadeklarowany. Jeśli zostanie określona inna nazwa, wystąpi błąd czasu kompilacji.

Gdy deklaracja finalizatora zawiera extern modyfikator, finalizator jest określany jako zewnętrzny finalizator. Ponieważ zewnętrzna deklaracja finalizatora nie zawiera rzeczywistej implementacji, jego finalizer_body zawiera jedynie średnik. W przypadku wszystkich innych finalizatorów finalizer_body składa się z jednego z następujących elementów:

  • blok, który określa instrukcje do wykonania w celu sfinalizowania wystąpienia klasy.
  • lub treść wyrażenia, która składa się z =>, a następnie wyrażenia i średnika, i wskazuje pojedyncze wyrażenie do wykonania w celu sfinalizowania instancji klasy.

Finalizer_body, który jest blokiem lub treścią wyrażenia, odpowiada dokładnie method_body metody instancji z typem zwracanym void (§15.6.11).

Finalizatory nie są dziedziczone. W związku z tym klasa nie ma finalizatorów innych niż ten, który może być zadeklarowany w tej klasie.

Uwaga: Ponieważ finalizator musi być bez parametrów, nie można go przeciążać, więc klasa może mieć co najwyżej jeden finalizator. notatka końcowa

Finalizatory są wywoływane automatycznie i nie można ich jawnie wywołać. Instancja kwalifikuje się do finalizacji, gdy nie jest już możliwe jej użycie przez jakikolwiek kod. Wykonanie finalizatora wystąpienia może nastąpić w dowolnym momencie po tym, jak wystąpienie kwalifikuje się do finalizacji (§7.9). Po sfinalizowaniu wystąpienia finalizatory w łańcuchu dziedziczenia tego wystąpienia są wywoływane w kolejności od większości pochodnych do najmniej pochodnych. Finalizator może być wykonywany w dowolnym wątku. Aby uzyskać dalszą dyskusję na temat zasad, które określają, kiedy i jak jest wykonywany finalizator, zobacz §7.9.

Przykład: dane wyjściowe przykładu

class A
{
    ~A()
    {
        Console.WriteLine("A's finalizer");
    }
}

class B : A
{
    ~B()
    {
        Console.WriteLine("B's finalizer");
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

jest

B's finalizer
A's finalizer

ponieważ finalizatory w łańcuchu dziedziczenia są wywoływane w kolejności, od najbardziej pochodnych do najmniej pochodnych.

przykład końcowy

Finalizatory są implementowane przez zastąpienie wirtualnej metody Finalize na System.Object. Programy języka C# nie mogą zastąpić tej metody ani bezpośrednio ją wywołać (lub zastąpić).

Przykład: na przykład program

class A
{
    override protected void Finalize() {}  // Error
    public void F()
    {
        this.Finalize();                   // Error
    }
}

zawiera dwa błędy.

przykład końcowy

Kompilator powinien zachowywać się tak, jakby ta metoda oraz jej nadpisania w ogóle nie istniały.

Przykład: W związku z tym ten program:

class A
{
    void Finalize() {}  // Permitted
}

jest prawidłowa, a pokazana metoda ukrywa System.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, lista parametrów tego elementu nie może zawierać parametrów in, out, ref ani parametru typu ref struct — w przeciwnym razie wystąpi błąd czasu kompilacji.

Interfejsy modułu wyliczającego 15.14.2

Interfejsy modułów wyliczających to interfejs niegeneryczny System.Collections.IEnumerator i wszystkie wystąpienia interfejsu ogólnego System.Collections.Generic.IEnumerator<T>. Ze względu na zwięzłość, w tej podklauzuli i podobnych podklauzulach te interfejsy są przywoływane odpowiednio jako IEnumerator i IEnumerator<T>.

15.14.3 Interfejsy wyliczalne

Interfejsy wyliczalne to interfejs System.Collections.IEnumerable niegeneryczny i wszystkie wystąpienia interfejsu System.Collections.Generic.IEnumerable<T>ogólnego . Ze względu na zwięzłość, w tym podpunkcie i jego odpowiednikach te interfejsy są przywoływane odpowiednio jako IEnumerable i IEnumerable<T>.

15.14.4 Typ wydajności

Iterator tworzy sekwencję wartości— wszystkie tego samego typu. Ten typ jest nazywany typem wydajności iteratora.

  • Typ wyniku iteratora, który zwraca IEnumerator lub IEnumerable, to 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 wyliczający jest tworzony i zwracany. Ten obiekt hermetyzuje kod określony w bloku iteratora, a wykonanie kodu w bloku iteratora ma miejsce, gdy metoda obiektu wyliczającego jest wywoływana. Obiekt modułu wyliczającego ma następujące cechy:

  • Implementuje IEnumerator 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 przed.

Obiekt modułu wyliczającego jest zazwyczaj wystąpieniem klasy modułu wyliczającego generowanego przez kompilator, który hermetyzuje kod w bloku iteratora i implementuje interfejsy modułu wyliczającego, ale możliwe są inne metody implementacji. Jeśli klasa modułu wyliczającego jest generowana przez kompilator, ta klasa zostanie zagnieżdżona bezpośrednio lub pośrednio w klasie zawierającej składową funkcji, będzie mieć dostęp prywatny i będzie mieć nazwę zarezerwowaną do użycia kompilatora (§6.4.3).

Obiekt modułu wyliczającego może implementować więcej interfejsów niż określone powyżej.

W poniższych podpunktach opisano wymagane zachowanie członków MoveNext, Current i Dispose w interfejsach IEnumerator i IEnumerator<T> dostarczone przez obiekt modułu wyliczającego.

Obiekty wyliczające nie obsługują metody IEnumerator.Reset. Wywołanie tej metody powoduje zgłoszenie wyjątku System.NotSupportedException.

15.14.5.2 Metoda MoveNext

Metoda MoveNext obiektu wyliczającego hermetyzuje kod bloku iteratora. Wywołanie MoveNext metody powoduje wykonanie kodu w bloku iteratora i ustawienie Current właściwości obiektu modułu wyliczającego zgodnie z potrzebami. Dokładna akcja wykonywana przez MoveNext zależy od stanu obiektu wyliczającego, gdy jest wywoływany MoveNext:

  • Jeśli stan obiektu modułu wyliczającego jest wcześniej, wywołanie metody MoveNext:
    • Zmienia stan na uruchomiony.
    • Inicjuje parametry (w tym this) bloku iteratora do wartości argumentu i wartości wystąpienia zapisanej podczas inicjowania obiektu modułu wyliczającego.
    • Wykonuje blok iteratora od początku do momentu przerwania wykonywania (zgodnie z poniższym opisem).
  • Jeśli stan obiektu modułu wyliczającego jest uruchomiony, wynik wywołania MoveNext jest nieokreślony.
  • Jeśli stan obiektu modułu wyliczającego jest zawieszony, wywołanie metody MoveNext:
    • Zmienia stan na działający.
    • Przywraca wartości wszystkich zmiennych lokalnych i parametrów (w tym this) do wartości zapisanych podczas ostatniego zawieszenia wykonywania bloku iteratora.

      Uwaga: zawartość wszystkich obiektów, do których odwołuje się te zmienne, mogła ulec zmianie od poprzedniego wywołania metody MoveNext. notatka końcowa

    • pl-PL: Wznawia wykonywanie bloku iteratora bezpośrednio po instrukcji yield return, która spowodowała zawieszenie wykonywania, i trwa aż do momentu przerwania wykonywania (zgodnie z poniższym opisem).
  • Jeśli stan obiektu modułu wyliczającego jest po, wywołanie MoveNext zwraca wartość false.

Po wykonaniu bloku iteratora przez MoveNext wykonywanie może zostać przerwane na cztery sposoby: przez instrukcję yield return, przez instrukcję yield break, napotykając koniec bloku iteratora, i przez zgłoszenie wyjątku oraz jego propagację poza blok iteratora.

  • W momencie napotkania instrukcji yield return (§9.4.4.20):
    • Wyrażenie podane w instrukcji jest obliczane, niejawnie konwertowane na typ plonu i przypisywane do Current właściwości obiektu wyliczającego.
    • Wykonanie ciała iteratora jest zawieszone. Wartości wszystkich zmiennych lokalnych i parametrów (w tym this) są zapisywane, podobnie jak lokalizacja tej yield return instrukcji. Jeśli instrukcja yield return znajduje się w co najmniej jednym try bloku, powiązane bloki finally nie są wykonywane w tym czasie.
    • Stan obiektu modułu wyliczającego jest zmieniany na zawieszony.
    • Metoda MoveNext zwraca true do wywołującego, wskazując, że iteracja pomyślnie przeszła do następnej wartości.
  • Po napotkaniu instrukcji yield break (§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 do swojego wywołującego, wskazując, że iteracja została ukończona.
  • Po napotkaniu końca ciała iteratora:
    • Stan obiektu modułu wyliczającego jest zmieniany na później.
    • Metoda MoveNext zwraca false do wywołującego, wskazując, że iteracja została ukończona.
  • Kiedy wyjątek jest zgłoszony i rozprzestrzenia się poza blokiem iteratora:
    • Odpowiednie finally bloki w ciele iteratora zostały wykonane poprzez propagację wyjątku.
    • Stan obiektu modułu wyliczającego jest zmieniany na później.
    • Propagacja wyjątku kontynuuje się do wywołującego metodę MoveNext.

15.14.5.3 Bieżąca właściwość

Właściwość obiektu wyliczającego Current jest wpływana przez instrukcje yield return w bloku iteratora.

Gdy obiekt modułu wyliczającego znajduje się w stanie wstrzymania, wartość Current jest wartością ustawioną przez poprzednie wywołanie metody .MoveNext Gdy obiekt modułu wyliczającego znajduje się w stanie przed, uruchomiony lub po, wynik dostępu Current jest nieokreślony.

W przypadku iteratora o typie wyniku innym niż object, wynik uzyskiwania dostępu do Current za pośrednictwem implementacji obiektu wyliczającego odpowiada dostępności do Current przez implementację obiektu IEnumerator<T> wyliczającego i rzutowania wyniku na object.

15.14.5.4 Metoda Dispose

Metoda Dispose służy do czyszczenia iteracji przez przeniesienie obiektu modułu wyliczającego do stanu after .

  • Jeśli stan obiektu modułu wyliczającego jest wcześniej, wywołanie Dispose zmienia stan na po.
  • Jeśli stan obiektu modułu wyliczającego jest uruchomiony, wynik wywołania Dispose jest nieokreślony.
  • Jeśli stan obiektu wyliczającego jest zawieszony, wywołanie Dispose:
    • Zmienia stan na uruchomiony.
    • Wykonuje wszystkie bloki finally tak, jakby ostatnio wykonana instrukcja yield return była instrukcją yield break. Jeśli spowoduje to zgłoszenie wyjątku i jego propagację z treści iteratora, stan obiektu enumeratora jest ustawiony na after, a wyjątek jest propagowany do wywołującego metodę Dispose.
    • Zmienia stan na after.
  • Jeśli stan obiektu enumeratora jest po, wywołanie Dispose nie ma wpływu.

15.14.6 Obiekty wyliczalne

15.14.6.1 Ogólne

Gdy element członkowski funkcji zwracający typ interfejsu wyliczalnego jest implementowany przy użyciu bloku iteratora, wywołanie elementu członkowskiego funkcji nie powoduje natychmiastowego wykonania kodu w bloku iteratora. Zamiast tego jest tworzony i zwracany obiekt wyliczalny. Metoda obiektu GetEnumerator wyliczalnego zwraca obiekt wyliczający, który hermetyzuje kod określony w bloku iteratora, a wykonanie kodu w bloku iteratora następuje po wywołaniu metody obiektu MoveNext wyliczającego. Obiekt wyliczalny ma następujące cechy:

  • Implementuje IEnumerable i IEnumerable<T>, gdzie T jest typem wydajności iteratora.
  • Inicjuje się od kopii wartości argumentów (jeśli istnieją) i wartości instancji przekazywanej do członka funkcji.

Obiekt wyliczalny jest zazwyczaj wystąpieniem klasy wyliczalnej generowanej przez kompilator, która hermetyzuje kod w bloku iteratora i implementuje interfejsy wyliczalne, ale możliwe są inne metody implementacji. Jeśli klasa wyliczalna jest generowana przez kompilator, ta klasa zostanie zagnieżdżona bezpośrednio lub pośrednio w klasie zawierającej składową funkcji, będzie mieć dostęp prywatny i będzie mieć nazwę zarezerwowaną do użycia kompilatora (§6.4.3).

Obiekt wyliczalny może implementować więcej interfejsów niż określone powyżej.

Uwaga: na przykład obiekt wyliczalny może również implementować IEnumerator i IEnumerator<T>, umożliwiając mu obsługę zarówno jako wyliczania, jak i modułu wyliczającego. Zazwyczaj taka implementacja zwraca swoje własne wystąpienie (aby oszczędzić na alokacjach) od pierwszego wywołania 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ą asynchroniczną. 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, jeśli lista parametrów funkcji asynchronicznej określa jakiekolwiek parametry in, out lub ref, lub jakiegokolwiek parametru typu ref struct.

return_type metody asynchronicznej to void lub typ zadania. W przypadku metody asynchronicznej, która generuje wartość wynikową, typ zadania jest ogólny. W przypadku metody asynchronicznej, która nie generuje wartości wynikowej, typ zadania nie jest ogólny. Takie typy są określane odpowiednio w tej specyfikacji jako «TaskType»<T> i «TaskType». Typ System.Threading.Tasks.Task biblioteki standardowej oraz typy tworzone z System.Threading.Tasks.Task<TResult> są typami zadań, a także klasa, struktura lub typ interfejsu skojarzony z typem konstruktora zadań za pośrednictwem atrybutu System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Takie typy są określane w tej specyfikacji jako «TaskBuilderType»<T> i «TaskBuilderType». Typ zadania może mieć co najwyżej jeden parametr typu i nie można go zagnieżdżać w typie ogólnym.

Metodę asynchroniczną, która zwraca typ zadania, określa się jako zwracającą zadanie.

Typy zadań mogą się różnić w ich dokładnej definicji, ale z punktu widzenia języka typ zadania jest w jednym ze stanów nieukończone, ukończone lub niepowodzenia. Zadanie z błędem rejestruje istotny wyjątek. Rejestrowanie wyniku typu Tsucceeded«TaskType»<T>. Typy zadań są oczekiwane, a zadania mogą zatem być operandami wyrażeń await (§12.9.8).

Przykład: typ MyTask<T> zadania jest skojarzony z typem konstruktora zadań MyTaskMethodBuilder<T> i typem awaitera Awaiter<T>.

using System.Runtime.CompilerServices; 
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
class MyTask<T>
{
    public Awaiter<T> GetAwaiter() { ... }
}

class Awaiter<T> : INotifyCompletion
{
    public void OnCompleted(Action completion) { ... }
    public bool IsCompleted { get; }
    public T GetResult() { ... }
}

przykład końcowy

Typ konstruktora zadań to klasa lub typ struktury, który odpowiada określonemu typowi zadania (§15.15.2). Typ konstruktora zadań dokładnie odpowiada zadeklarowanej dostępności odpowiadającego mu typu zadania.

Uwaga: Jeśli typ zadania jest zadeklarowany internal, odpowiedni typ konstruktora musi być również zadeklarowany internal i zdefiniowany w tym samym zestawie. Jeśli typ zadania jest zagnieżdżony wewnątrz innego typu, typ buildera zadania musi być również zagnieżdżony w tym samym typie. notatka końcowa

Funkcja asynchroniowa ma możliwość wstrzymania oceny za pomocą wyrażeń await (§12.9.8) w treści. Oceny mogą zostać później wznowione w miejscu wyrażenia await, które zostało wstrzymane za pomocą delegata wznowienia. Delegat wznowienia jest typu System.Action, a po wywołaniu ewaluacja wywołania funkcji asynchronicznej zostanie wznowiona przy wyrażeniu await, w którym została przerwana. Bieżący obiekt wywołujący wywołanie funkcji asynchronicznych jest oryginalnym obiektem wywołującym, jeśli wywołanie funkcji nigdy nie zostało zawieszone lub ostatnio wywołujący delegat wznowienia w przeciwnym razie.

15.15.2 Wzorzec konstruktora typów zadań

Typ konstruktora zadań może mieć co najwyżej jeden parametr typu i nie można go zagnieżdżać w typie ogólnym. Typ konstruktora zadań musi mieć następujących członków (w przypadku typów konstruktorów zadań innych niż ogólne, SetResult nie mają parametrów) z zadeklarowaną dostępnością public:

class «TaskBuilderType»<T>
{
    public static «TaskBuilderType»<T> Create();
    public void Start<TStateMachine>(ref TStateMachine stateMachine)
                where TStateMachine : IAsyncStateMachine;
    public void SetStateMachine(IAsyncStateMachine stateMachine);
    public void SetException(Exception exception);
    public void SetResult(T result);
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public «TaskType»<T> Task { get; }
}

Kompilator generuje kod, który używa "TaskBuilderType" do implementowania semantyki zawieszania i wznawiania oceny funkcji asynchronicznej. Kompilator używa "TaskBuilderType" w następujący sposób:

  • «TaskBuilderType».Create() jest wywoływany w celu utworzenia wystąpienia «TaskBuilderType» o nazwie builder na tej liście.
  • builder.Start(ref stateMachine) jest wywoływany, aby skojarzyć obiekt konstruktora z instancją maszyny stanów wygenerowaną przez kompilator, stateMachine.
    • Budowniczy powinien wywołać stateMachine.MoveNext() w Start() lub po tym, jak Start() zakończy działanie, aby przejść do kolejnego stanu maszyny stanów.
  • Po powrocie Start() metoda async wywołuje builder.Task dla zadania, aby wróciło z metody asynchronicznej.
  • Każde wywołanie stateMachine.MoveNext() spowoduje przejście maszyny stanu.
  • Jeśli maszyna stanu zakończy się pomyślnie, builder.SetResult() zostanie wywołana z wartością zwracaną przez metodę, jeśli istnieje.
  • W przeciwnym razie, jeśli wyjątek e zostanie zgłoszony w maszynie stanów, builder.SetException(e) zostanie wywołany.
  • Jeśli maszyna stanu osiągnie await expr wyrażenie, expr.GetAwaiter() jest wywoływane.
  • Jeśli program awaiter implementuje ICriticalNotifyCompletion i IsCompleted ma wartość false, maszyna stanu wywołuje builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine).
    • AwaitUnsafeOnCompleted() powinien wywołać awaiter.UnsafeOnCompleted(action) z Action, który wywołuje stateMachine.MoveNext() po zakończeniu awaitera.
  • W przeciwnym razie maszyna stanu wywołuje element builder.AwaitOnCompleted(ref awaiter, ref stateMachine).
    • AwaitOnCompleted() powinien wywołać awaiter.OnCompleted(action) z funkcją Action, która wywoła stateMachine.MoveNext() w momencie zakończenia awaitera.
  • SetStateMachine(IAsyncStateMachine) może być wywoływana przez implementację wygenerowaną IAsyncStateMachine przez kompilator w celu zidentyfikowania wystąpienia konstruktora skojarzonego z wystąpieniem maszyny stanu, szczególnie w przypadkach, gdy maszyna stanu jest implementowana jako typ wartości.
    • Jeśli konstruktor wywoła stateMachine.SetStateMachine(stateMachine), to stateMachine wywoła builder.SetStateMachine(stateMachine) na instancji konstruktora skojarzonej zstateMachine.

Uwaga: Zarówno parametr SetResult(T result), jak i argument «TaskType»<T> Task { get; } muszą być odpowiednio możliwe do identycznej konwersji na T. To pozwala konstruktorowi typów zadań obsługiwać typy, takie jak krotki, gdzie dwa różne typy są wzajemnie konwertowalne w kontekście tożsamości. notatka końcowa

15.15.3 Ocena asynchronicznej funkcji zwracającej zadanie

Wywołanie funkcji asynchronicznej zwracającej zadanie powoduje wygenerowanie instancji zwracanego typu zadania. Jest to nazywane zadaniem zwrotnym funkcji asynchronicznej. Zadanie jest początkowo w stanie niekompletnym.

Treść funkcji asynchronicznej jest następnie oceniana do momentu wstrzymania (przez osiągnięcie wyrażenia await) lub zakończenia, w którym to momencie kontrola jest zwracana do wywołującego wraz z zadaniem zwracanym.

Gdy treść funkcji asynchronicznych zakończy działanie, zadanie zwracane zostanie przeniesione z niekompletnego stanu:

  • Jeśli treść funkcji kończy się w wyniku osiągnięcia instrukcji return lub końca treści, każda wartość wyniku jest rejestrowana w zadaniu zwrotnym, który jest umieszczany w stanie powodzenia.
  • Jeśli treść funkcji zostaje przerwana z powodu nieuchwyconego OperationCanceledException, wyjątek jest rejestrowany w zadaniu wynikowym, które jest przenoszone do stanu anulowanego.
  • Jeśli treść funkcji zakończy się w wyniku jakichkolwiek innych nieuchwyconych wyjątków (§13.10.6), wyjątek jest rejestrowany w zadaniu zwrotnym, które jest umieszczane w stanie błędu.

15.15.4 Ocena asynchronicznej funkcji zwracającej void

Jeśli zwracany typ funkcji asynchronicznej to void, ocena różni się od powyższego w następujący sposób: ponieważ nie jest zwracane żadne zadanie, funkcja komunikuje zakończenie i wyjątki kontekstowi synchronizacji bieżącego wątku. Dokładna definicja kontekstu synchronizacji jest zależna od implementacji, ale jest reprezentacją "gdzie" bieżący wątek jest uruchomiony. Kontekst synchronizacji jest powiadamiany, gdy uruchomienie asynchronicznej funkcji zwracającej void rozpoczyna się, kończy się pomyślnie lub powoduje zgłoszenie nieuchwyconego wyjątku.

Dzięki temu kontekst może śledzić, ile funkcji asynchronicznych zwracających void jest uruchomionych pod jego kontrolą i zdecydować, jak propagować wyjątki pochodzące z tych funkcji.