Udostępnij za pośrednictwem


8 typów

8.1 Ogólne

Typy języka C# są podzielone na dwie główne kategorie: typy referencyjne i typy wartości. Oba typy wartości i typy referencyjne mogą być typami ogólnymi, które przyjmują co najmniej jeden parametr typu. Parametry typu mogą wyznaczać zarówno typy wartości, jak i typy referencyjne.

type
    : reference_type
    | value_type
    | type_parameter
    | pointer_type     // unsafe code support
    ;

pointer_type (§23.3) jest dostępny tylko w niebezpiecznym kodzie (§23).

Typy wartości różnią się od typów referencyjnych w tym, że zmienne typów wartości bezpośrednio zawierają swoje dane, podczas gdy zmienne typów referencyjnych przechowują odwołania do ich danych, a te ostatnie są nazywane obiektami. W przypadku typów odwołań możliwe jest, aby dwie zmienne odwoły się do tego samego obiektu, co umożliwia operacje na jednej zmiennej, aby wpływały na obiekt, do którego odwołuje się druga zmienna. W przypadku typów wartości zmienne mają własną kopię danych i nie jest możliwe, aby operacje na jednym z nich miały wpływ na drugą.

Uwaga: jeśli zmienna jest parametrem referencyjnym lub wyjściowym, nie ma własnego magazynu, ale odwołuje się do magazynu innej zmiennej. W takim przypadku zmienna ref lub out jest w rzeczywistości aliasem dla innej zmiennej, a nie unikatową zmienną. notatka końcowa

System typów języka C#jest ujednolicony, tak aby można było traktować wartość dowolnego typu jako obiekt. Każdy typ w języku C# bezpośrednio lub pośrednio pochodzi z object typu klasy i object jest ostateczną klasą bazową wszystkich typów. Wartości typów referencyjnych są traktowane jako obiekty po prostu, wyświetlając wartości jako typ object. Wartości typów wartości są traktowane jako obiekty, wykonując operacje boksowania i rozpasania (§8.3.13).

Dla wygody, w całej tej specyfikacji niektóre nazwy typów biblioteki są pisane bez użycia pełnej nazwy kwalifikowanej. Aby uzyskać więcej informacji, zobacz §C.5 .

8.2 Typy referencyjne

8.2.1 Ogólne

Typ odwołania to typ klasy, typ interfejsu, typ tablicy, typ delegata lub dynamic typ. Dla każdego nienullowalnego typu referencyjnego istnieje odpowiedni typ referencyjny dopuszczający wartość null, poprzez dołączenie ? do nazwy typu.

reference_type
    : non_nullable_reference_type
    | nullable_reference_type
    ;

non_nullable_reference_type
    : class_type
    | interface_type
    | array_type
    | delegate_type
    | 'dynamic'
    ;

class_type
    : type_name
    | 'object'
    | 'string'
    ;

interface_type
    : type_name
    ;

array_type
    : non_array_type rank_specifier+
    ;

non_array_type
    : value_type
    | class_type
    | interface_type
    | delegate_type
    | 'dynamic'
    | type_parameter
    | pointer_type      // unsafe code support
    ;

rank_specifier
    : '[' ','* ']'
    ;

delegate_type
    : type_name
    ;

nullable_reference_type
    : non_nullable_reference_type nullable_type_annotation
    ;

nullable_type_annotation
    : '?'
    ;

pointer_type jest dostępny tylko w niebezpiecznym kodzie (§23.3). nullable_reference_type omówiono dalej w §8.9.

Wartość typu odwołania jest odwołaniem do wystąpienia typu, znanego jako obiekt. Wartość null specjalna jest zgodna ze wszystkimi typami referencyjnymi i wskazuje brak wystąpienia.

Typy klas 8.2.2

Typ klasy definiuje strukturę danych zawierającą elementy członkowskie 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 klasy pochodne mogą rozszerzać i specjalizować klasy bazowe. Wystąpienia typów klas są tworzone przy użyciu object_creation_expressions (§12.8.17.2).

Typy klas są opisane w §15.

Niektóre wstępnie zdefiniowane typy klas mają specjalne znaczenie w języku C#, zgodnie z opisem w poniższej tabeli.

Typ klasy Opis
System.Object Nadrzędna klasa bazowa wszystkich innych typów. Zobacz §8.2.3.
System.String Typ ciągu języka C#. Zobacz §8.2.5.
System.ValueType Klasa podstawowa wszystkich typów wartości. Zobacz §8.3.2.
System.Enum Klasa bazowa wszystkich enum typów. Zobacz §19.5.
System.Array Klasa podstawowa wszystkich typów tablic. Zobacz §17.2.2.
System.Delegate Klasa bazowa wszystkich delegate typów. Zobacz §20.1.
System.Exception Klasa podstawowa wszystkich typów wyjątków. Zobacz §21.3.

8.2.3 Typ obiektu

Typ object klasy jest najlepszą klasą bazową wszystkich innych typów. Każdy typ w języku C# bezpośrednio lub pośrednio pochodzi z object typu klasy.

Słowo kluczowe object to po prostu alias dla wstępnie zdefiniowanej klasy System.Object.

8.2.4 Typ dynamiczny

Typ dynamic , taki jak object, może odwoływać się do dowolnego obiektu. Gdy operacje są stosowane do wyrażeń typu dynamic, ich rozpoznawanie jest odroczone do momentu uruchomienia programu. W związku z tym, jeśli nie można legalnie zastosować operacji do przywoływanego obiektu, podczas kompilacji nie zostanie podany żaden błąd. Zamiast tego zostanie zgłoszony wyjątek, gdy rozwiązanie operacji zakończy się niepowodzeniem w czasie wykonywania.

Typ dynamic jest dokładniej opisany w §8.7 i powiązanie dynamiczne w §12.3.1.

8.2.5 Typ ciągu

Typ string jest zapieczętowaną klasą, która dziedziczy bezpośrednio z klasy object. string Wystąpienia klasy reprezentują ciągi znaków Unicode.

string Wartości typu można zapisywać jako literały ciągu (§6.4.5.6).

Słowo kluczowe string to po prostu alias dla wstępnie zdefiniowanej klasy System.String.

Typy interfejsów 8.2.6

Interfejs definiuje kontrakt. Klasa lub struktura, która implementuje interfejs, jest zgodna z jego umową. Interfejs może dziedziczyć z wielu interfejsów podstawowych, a klasa lub struktura może implementować wiele interfejsów.

Typy interfejsów są opisane w §18.

Typy tablic 8.2.7

Tablica to struktura danych zawierająca zero lub więcej zmiennych, do których uzyskuje się dostęp za pośrednictwem obliczonych indeksów. Zmienne zawarte w tablicy, nazywane również elementami tablicy, są tego samego typu, a ten typ jest nazywany typem elementu tablicy.

Typy tablic są opisane w §17.

8.2.8 Typy delegatów

Delegat to struktura danych, która odwołuje się do jednej lub kilku metod. Na przykład metody odnoszą się również do odpowiednich wystąpień obiektów.

Uwaga: najbliższym odpowiednikiem delegata w języku C lub C++ jest wskaźnik funkcji, ale wskaźnik funkcji może odwoływać się tylko do funkcji statycznych, delegat może odwoływać się do metod statycznych i wystąpień. W drugim przypadku delegat przechowuje zarówno odwołanie do punktu wejścia metody, jak i odwołanie do instancji obiektu, na którym wywoływana jest metoda. notatka końcowa

Typy delegatów są opisane w §20.

8.3 Typy wartości

8.3.1 Ogólne

Typ wartości jest typem struktury lub typem wyliczenia. Język C# udostępnia zestaw wstępnie zdefiniowanych typów struktur nazywanych prostymi typami. Proste typy są identyfikowane za pomocą słów kluczowych.

value_type
    : non_nullable_value_type
    | nullable_value_type
    ;

non_nullable_value_type
    : struct_type
    | enum_type
    ;

struct_type
    : type_name
    | simple_type
    | tuple_type
    ;

simple_type
    : numeric_type
    | 'bool'
    ;

numeric_type
    : integral_type
    | floating_point_type
    | 'decimal'
    ;

integral_type
    : 'sbyte'
    | 'byte'
    | 'short'
    | 'ushort'
    | 'int'
    | 'uint'
    | 'long'
    | 'ulong'
    | 'char'
    ;

floating_point_type
    : 'float'
    | 'double'
    ;

tuple_type
    : '(' tuple_type_element (',' tuple_type_element)+ ')'
    ;
    
tuple_type_element
    : type identifier?
    ;
    
enum_type
    : type_name
    ;

nullable_value_type
    : non_nullable_value_type nullable_type_annotation
    ;

W przeciwieństwie do zmiennej typu referencyjnego, zmienna typu wartości może zawierać wartość null tylko wtedy, gdy typ wartości jest typem wartości nulowalnej (§8.3.12). Dla każdego nienullowalnego typu wartości istnieje odpowiadający typ wartości, który może przyjąć wartość null, oznaczający ten sam zestaw wartości oraz wartość null.

Przypisanie do zmiennej typu wartości powoduje utworzenie kopii przypisanej wartości. Różni się to od przypisania do zmiennej typu odwołania, która kopiuje odwołanie, ale nie obiekt zidentyfikowany przez odwołanie.

8.3.2 Typ System.ValueType

Wszystkie typy wartości niejawnie dziedziczą z classSystem.ValueTypeklasy , która z kolei dziedziczy z klasy object. Nie można utworzyć żadnego typu pochodzącego z typu wartości, a typy wartości są zatem niejawnie zapieczętowane (§15.2.2.3).

Należy pamiętać, że System.ValueType nie jest to value_type. Zamiast tego jest to class_type , z którego wszystkie value_typesą automatycznie uzyskiwane.

8.3.3 Konstruktory domyślne

Wszystkie typy wartości niejawnie deklarują publiczny konstruktor wystąpienia bez parametrów nazywany konstruktorem domyślnym. Konstruktor domyślny zwraca wystąpienie zainicjowane zero znane jako wartość domyślna dla typu wartości:

  • Dla wszystkich simple_types wartość domyślna to wartość wygenerowana przez wzorzec bitowy wszystkich zer:
    • W przypadku sbyte, byte, short, ushort, int, uint, long i ulong wartość domyślna to 0.
    • W przypadku charparametru wartość domyślna to '\x0000'.
    • W przypadku floatparametru wartość domyślna to 0.0f.
    • W przypadku doubleparametru wartość domyślna to 0.0d.
    • W przypadku decimalparametru wartość domyślna to 0m (czyli wartość zero ze skalą 0).
    • W przypadku boolparametru wartość domyślna to false.
    • W przypadku enum_typeE wartość domyślna to 0, przekonwertowana na typ E.
  • W przypadku struct_type wartość domyślna jest wartością wygenerowaną przez ustawienie wszystkich pól typu wartości na wartość domyślną i wszystkich pól typu odwołania na null.
  • W przypadku nullable_value_type wartością domyślną jest wystąpienie, dla którego HasValue właściwość ma wartość false. Wartość domyślna jest również znana jako wartość null typu wartości, które mogą być null. Próba odczytania Value właściwości takiej wartości powoduje zgłoszenie wyjątku typu System.InvalidOperationException (§8.3.12).

Podobnie jak każdy inny konstruktor wystąpienia, domyślny konstruktor typu wartości jest wywoływany przy użyciu new operatora .

Uwaga: Ze względów wydajności to wymaganie nie jest przeznaczone do faktycznego wygenerowania wywołania konstruktora przez implementację. W przypadku typów wartości wyrażenie wartości domyślnej (§12.8.21) daje taki sam wynik, jak przy użyciu konstruktora domyślnego. notatka końcowa

Przykład: W poniższym kodzie zmienne ij i k są inicjowane do zera.

class A
{
    void F()
    {
        int i = 0;
        int j = new int();
        int k = default(int);
    }
}

przykład końcowy

Ponieważ każdy typ wartości niejawnie ma publiczny konstruktor wystąpienia bez parametrów, nie jest możliwe, aby typ struktury zawierał jawną deklarację konstruktora bez parametrów. Typ struktury jest jednak dozwolony do deklarowania sparametryzowanych konstruktorów wystąpień (§16.4.9).

Typy struktury 8.3.4

Typ struktury to typ wartości, który może deklarować stałe, pola, metody, właściwości, zdarzenia, indeksatory, operatory, konstruktory wystąpień, konstruktory statyczne i typy zagnieżdżone. Deklaracja typów struktur jest opisana w §16.

8.3.5 Typy proste

Język C# udostępnia zestaw wstępnie zdefiniowanych typów struct nazywanych prostymi typami. Proste typy są identyfikowane za pomocą słów kluczowych, ale te słowa kluczowe są po prostu aliasami dla wstępnie zdefiniowanych typów struct w przestrzeni nazw System, jak opisano w poniższej tabeli.

Słowo kluczowe typ z aliasem
sbyte System.SByte
byte System.Byte
short System.Int16
ushort System.UInt16
int System.Int32
uint System.UInt32
long System.Int64
ulong System.UInt64
char System.Char
float System.Single
double System.Double
bool System.Boolean
decimal System.Decimal

Ponieważ prosty typ aliasuje typ struktury, każdy prosty typ ma elementy członkowskie.

Przykład: int ma członków zadeklarowanych w System.Int32 oraz członków odziedziczonych z System.Object, a następujące instrukcje są dozwolone.

int i = int.MaxValue;      // System.Int32.MaxValue constant
string s = i.ToString();   // System.Int32.ToString() instance method
string t = 123.ToString(); // System.Int32.ToString() instance method

przykład końcowy

Uwaga: Proste typy różnią się od innych typów struktur, które zezwalają na niektóre dodatkowe operacje:

  • Większość prostych typów zezwala na tworzenie wartości przez pisanie literałów (§6.4.5), chociaż język C# nie udostępnia literałów typów struktur w ogóle. Przykład: 123 jest literałem typu int i 'a' jest literałem typu char. przykład końcowy
  • Gdy operandy wyrażenia są prostymi stałymi typami, kompilator może ocenić wyrażenie w czasie kompilacji. Takie wyrażenie jest nazywane constant_expression (§12.23). Wyrażenia obejmujące operatory zdefiniowane przez inne typy struktur nie są uważane za wyrażenia stałe
  • Deklaracje const umożliwiają deklarowanie stałych typów prostych (§15.4). Nie można mieć stałych innych typów struktur, ale podobny efekt jest zapewniany przez statyczne pola readonly.
  • Konwersje obejmujące proste typy mogą uczestniczyć w ocenie operatorów konwersji zdefiniowanych przez inne typy struktur, ale operator konwersji zdefiniowany przez użytkownika nigdy nie może uczestniczyć w ocenie innego operatora konwersji zdefiniowanego przez użytkownika (§10.5.3).

uwaga końcowa.

Typy całkowite 8.3.6

Język C# obsługuje dziewięć typów całkowitych: sbyte, byte, short, ushort, int, uint, long, ulong i char. Typy całkowite mają następujące rozmiary i zakresy wartości:

  • Typ sbyte reprezentuje podpisane 8-bitowe liczby całkowite z wartościami od -128 do 127, włącznie.
  • Typ byte reprezentuje niepodpisane 8-bitowe liczby całkowite z wartościami od 0 do 255, włącznie.
  • Typ short reprezentuje podpisane 16-bitowe liczby całkowite z wartościami od -32768 do 32767, włącznie.
  • Typ ushort reprezentuje niepodpisane 16-bitowe liczby całkowite z wartościami od 0 do 65535, włącznie.
  • Typ int reprezentuje podpisane 32-bitowe liczby całkowite z wartościami od -2147483648 do 2147483647, włącznie.
  • Typ uint reprezentuje niepodpisane 32-bitowe liczby całkowite z wartościami od 0 do 4294967295, włącznie.
  • Typ long reprezentuje podpisane 64-bitowe liczby całkowite z wartościami od -9223372036854775808 do 9223372036854775807, włącznie.
  • Typ ulong reprezentuje niepodpisane 64-bitowe liczby całkowite z wartościami od 0 do 18446744073709551615, włącznie.
  • Typ char reprezentuje niepodpisane 16-bitowe liczby całkowite z wartościami od 0 do 65535, włącznie. Zestaw możliwych wartości dla char typu odpowiada zestawowi znaków Unicode.

    Uwaga: Chociaż char ma taką samą reprezentację jak ushort, nie wszystkie operacje dozwolone na jednym typie są dozwolone w drugim. notatka końcowa

Wszystkie podpisane typy całkowite są reprezentowane przy użyciu formatu dopełnień dwóch.

Operatory integral_type jednoargumentowe i binarne zawsze działają z 32-bitową precyzją ze znakiem, 32-bitową precyzją bez znaku, 64-bitową precyzją ze znakiem, lub 64-bitową precyzją bez znaku, zgodnie z opisem w §12.4.7.

Typ char jest klasyfikowany jako typ całkowity, ale różni się od innych typów całkowitych na dwa sposoby:

  • Nie ma wstępnie zdefiniowanych niejawnych konwersji z innych typów na char typ. W szczególności, mimo że typy byte i ushort mają zakresy wartości, które są w pełni reprezentowane przy użyciu typu char, nie istnieją niejawne konwersje z sbyte, byte lub ushort do char.
  • Stałe typu char powinny być zapisywane jako character_literal lub jako integer_literal w połączeniu z rzutowaniem do typu char.

Przykład: (char)10 jest taki sam jak '\x000A'. przykład końcowy

Operatory i instrukcje checked i unchecked służą do sprawdzania przepełnienia dla operacji arytmetycznych i konwersji typu całkowitego (§12.8.20). W kontekście checked przepełnienie powoduje błąd czasu kompilacji lub zgłoszenie System.OverflowException. W kontekście unchecked przepełnienia są ignorowane, a wszystkie bity o wyższym rzędzie, które nie mieszczą się w docelowym typie, są odrzucane.

8.3.7 Typy zmiennoprzecinkowe

Język C# obsługuje dwa typy zmiennoprzecinkowe: float i double. Typy float i double są reprezentowane przy użyciu 32-bitowej pojedynczej precyzji i 64-bitowej podwójnej precyzji formatów IEC 60559, które zapewniają następujące zestawy wartości:

  • Zero dodatnie i zero ujemne. W większości sytuacji dodatnie zero i zero ujemne zachowują się identycznie jak wartość prosta zero, ale niektóre operacje rozróżniają między nimi (§12.10.3).
  • Nieskończoność dodatnia i nieskończoność ujemna. Nieskończoności są produkowane przez takie operacje, jak dzielenie liczby niezerowej przez zero.

    Przykład: 1.0 / 0.0 daje nieskończoność dodatnią i –1.0 / 0.0 daje nieskończoność ujemną. przykład końcowy

  • Wartość Not-a-Number, często używana jako skrót NaN. Sieci NaN są generowane przez nieprawidłowe operacje zmiennoprzecinkowe, takie jak dzielenie zera przez zero.
  • Skończony zbiór niezerowych wartości postaci s × m × 2e, gdzie s jest 1 lub −1, a m i e są określone przez określony typ zmiennoprzecinkowy: dla float, 0 <m< 2²⁴ i −149 ≤ e ≤ 104, a dla double, 0 <m< 2⁵³ i −1075 ≤ e ≤ 970. Zdenormalizowane liczby zmiennoprzecinkowe są uznawane za prawidłowe wartości inne niż zero. Język C# nie wymaga ani nie zabrania, aby zgodna implementacja obsługiwała zdenormalizowane liczby zmiennoprzecinkowe.

Typ float może reprezentować wartości z zakresu od około 1,5 × 10⁻⁴⁵ do 3,4 × 10³⁸ z dokładnością 7 cyfr.

Typ double może reprezentować wartości z zakresu od około 5,0 × 10⁻³²⁴ do 1,7 × 10³⁸ z dokładnością 15–16 cyfr.

Jeśli którykolwiek z operandów operatora binarnego jest typem zmiennoprzecinkowym, stosowane są standardowe promocje liczbowe, zgodnie z opisem w §12.4.7, a operacja jest wykonywana z float lub double precyzją.

Operatory zmiennoprzecinkowe, w tym operatory przypisania, nigdy nie generują wyjątków. Zamiast tego w wyjątkowych sytuacjach operacje zmiennoprzecinkowe generują zero, nieskończoność lub NaN, zgodnie z poniższym opisem:

  • Wynik operacji zmiennoprzecinkowej jest zaokrąglany do najbliższej domyślnej wartości, która jest reprezentowana w docelowym formacie.
  • Jeśli wielkość wyniku operacji zmiennoprzecinkowej jest zbyt mała dla formatu docelowego, wynik operacji staje się zerem dodatnim lub zerem ujemnym.
  • Jeśli wielkość wyniku operacji zmiennoprzecinkowych jest zbyt duża dla formatu docelowego, wynik operacji staje się dodatnią nieskończonością lub nieskończonością ujemną.
  • Jeśli operacja zmiennoprzecinkowa jest nieprawidłowa, wynik operacji staje się NaN.
  • Jeśli jeden lub oba operandy operacji zmiennoprzecinkowych to NaN, wynik operacji staje się NaN.

Operacje zmiennoprzecinkowe mogą być wykonywane z wyższą precyzją niż typ wyniku operacji. Aby wymusić wartość typu zmiennoprzecinkowego do dokładnej precyzji jej typu, można użyć jawnego rzutowania (§12.9.7).

Przykład: Niektóre architektury sprzętowe obsługują rozszerzony lub długi podwójny typ zmiennoprzecinkowy z większym zakresem i dokładnością niż double typ, i niejawnie wykonują wszystkie operacje zmiennoprzecinkowe przy użyciu tego wyższego typu precyzji. Aby takie architektury sprzętowe mogły realizować operacje zmiennoprzecinkowe z niższą precyzją, wymaga to znacznego pogorszenia wydajności. Zamiast zmuszać implementację do rezygnacji zarówno z wydajności, jak i precyzji, język C# pozwala na użycie typu o wyższej precyzji dla wszystkich operacji zmiennoprzecinkowych. Poza dostarczaniem bardziej precyzyjnych wyników rzadko ma to jakiekolwiek wymierne skutki. Jednak w wyrażeniach formularza x * y / z, gdzie mnożenie generuje wynik, który znajduje się poza double zakresem, ale następny podział powoduje tymczasowy wynik z powrotem do double zakresu, fakt, że wyrażenie jest oceniane w wyższym formacie zakresu, może spowodować wygenerowanie skończonego wyniku zamiast nieskończoności. przykład końcowy

8.3.8 Typ dziesiętny

Typ decimal to 128-bitowy typ danych odpowiedni do obliczeń finansowych i pieniężnych. Typ decimal może reprezentować wartości, w tym wartości z zakresu co najmniej -7,9 × 10⁻²⁸ do 7,9 × 10²⁸ z dokładnością co najmniej 28-cyfrową.

Skończony zestaw wartości typu decimal ma postać (–1)v × c × 10⁻e, gdzie znak v wynosi 0 lub 1, współczynnik c jest podawany przez 0 ≤ c<Cmax, a skala e jest taka, że Emine e ≤ Emax, gdzie Cmax wynosi co najmniej 1 × 10²⁸, Emin ≤ 0, i Emax ≥ 28. Typ decimal nie musi obsługiwać zanajękowanych zer, nieskończoności ani wartości NaN.

Wartość A decimal jest reprezentowana jako liczba całkowita skalowana przez potęgę dziesięciu. Dla decimal-ów o wartości bezwzględnej mniejszej niż 1.0m, wartość jest dokładna co najmniej do 28 miejsca dziesiętnego. Dla decimals z wartością bezwzględną większą lub równą 1.0m, wartość jest dokładna do co najmniej 28 cyfr. W przeciwieństwie do typów danych float i double, liczby ułamkowe dziesiętne, takie jak 0.1, mogą być dokładnie reprezentowane w formacie dziesiętnym. float i double w reprezentacjach, takie liczby często mają nieskończone rozwinięcia binarne, co sprawia, że te reprezentacje są bardziej podatne na błędy zaokrąglania.

Jeśli którykolwiek operand operatora binarnego jest typu decimal, stosowane są standardowe promocje liczbowe, zgodnie z opisem w §12.4.7, a operacja jest wykonywana z dokładnością .

Wynikiem operacji na wartościach typu decimal jest to, co wynikałoby z obliczenia dokładnego wyniku (zachowanie skali zgodnie z definicją dla każdego operatora), a następnie zaokrąglenie w celu dopasowania do reprezentacji. Wyniki są zaokrąglane do najbliższej wartości reprezentującej, a gdy wynik jest równie zbliżony do dwóch godnych reprezentowania wartości, do wartości, która ma liczbę parzystą w najmniej znaczącej pozycji cyfry (jest to nazywane "zaokrąglanie bankiera"). Oznacza to, że wyniki są dokładne do co najmniej 28 miejsca dziesiętnego. Należy pamiętać, że zaokrąglanie może spowodować wygenerowanie wartości zerowej z wartości innej niż zero.

Jeśli operacja arytmetyczna decimal generuje wynik, którego wielkość jest zbyt duża dla formatu decimal, System.OverflowException zostaje zgłoszony.

Typ decimal ma większą precyzję, ale może mieć mniejszy zakres niż typy zmiennoprzecinkowe. W związku z tym konwersje z typów zmiennoprzecinkowych do decimal mogą generować wyjątki przepełnienia, a konwersje z decimal do typów zmiennoprzecinkowych mogą spowodować utratę dokładności lub wyjątki przepełnienia. Z tych powodów nie istnieją niejawne konwersje między typami zmiennoprzecinkowymi a decimal, a bez jawnych rzutów występuje błąd kompilacji, gdy operandy zmiennoprzecinkowe i decimal są bezpośrednio mieszane w tym samym wyrażeniu.

8.3.9 Typ wartości logicznej

Typ bool reprezentuje logiczne wartości typu Boolean. Możliwe wartości typu bool to true i false. Reprezentacja false jest opisana w §8.3.3. Chociaż reprezentacja elementu true jest nieokreślona, różni się ona od tej z .false

Między i innymi typami bool wartości nie istnieją żadne standardowe konwersje. W szczególności bool typ jest odrębny i oddzielony od typów całkowitych, a bool wartość nie może być używana zamiast wartości całkowitej, i na odwrót.

Uwaga: W językach C i C++ wartość całkowita lub zmiennoprzecinkowa wynosząca zero lub wskaźnik o wartości null mogą być przekonwertowane na wartość logiczną false, a wartość całkowita lub zmiennoprzecinkowa niezerowa lub wskaźnik nienullowy mogą być przekonwertowane na wartość logiczną true. W języku C# takie konwersje są wykonywane przez jawne porównanie wartości całkowitej lub zmiennoprzecinkowej z zerem lub jawnie porównując odwołanie do obiektu .null notatka końcowa

8.3.10 Typy wyliczeniowe

Typ wyliczenia jest odrębnym typem z nazwanymi stałymi. Każdy typ wyliczenia ma typ bazowy, który powinien być byte, sbyte, short, ushort, int, uint, long lub ulong. Zestaw wartości typu wyliczenia jest taki sam jak zestaw wartości bazowego typu. Wartości typu wyliczenia nie są ograniczone do wartości nazwanych stałych. Typy wyliczenia są definiowane za pomocą deklaracji wyliczenia (§19.2).

8.3.11 Typy krotek

Typ krotki reprezentuje uporządkowaną, stałej długości sekwencję wartości z opcjonalnymi nazwami i indywidualnymi typami. Liczba elementów w typie krotki jest określana jako jego arność. Typ krotki jest reprezentowany jako (T1 I1, ..., Tn In) dla n ≥ 2, gdzie identyfikatory I1...In są opcjonalnymi nazwami elementów krotki.

Ta składnia jest skrótem dla typu skonstruowanego z typów T1...Tn z System.ValueTuple<...>, który stanowi zestaw typów ogólnych strukturalnych, zdolnych bezpośrednio wyrażać typy krotek o dowolnej maksymalnej liczbie argumentów między dwoma a siedmioma, łącznie. Nie ma potrzeby, aby istniała System.ValueTuple<...> deklaracja, która bezpośrednio pasuje do liczby argumentów dowolnego typu krotki z odpowiadającą liczbą parametrów typowych. Zamiast tego krotka o liczbie elementów większej niż siedem jest reprezentowana za pomocą ogólnego typu System.ValueTuple<T1, ..., T7, TRest> struktury, który oprócz elementów krotki ma pole Rest zawierające zagnieżdżoną wartość pozostałych elementów, używając innego typu System.ValueTuple<...>. Takie zagnieżdżanie może być obserwowane na różne sposoby, np. poprzez obecność Rest pola. Jeśli wymagane jest tylko jedno dodatkowe pole, używany jest typ System.ValueTuple<T1> struktury ogólnej; ten typ nie jest traktowany jako typ krotki. Jeśli wymagane jest więcej niż siedem dodatkowych pól, System.ValueTuple<T1, ..., T7, TRest> jest używany rekursywnie.

Nazwy elementów w typie krotki muszą być unikalne. Nazwa elementu krotki w postaci ItemX, gdzie X jest dowolną sekwencją niepoprzedzonych przez 0 cyfr dziesiętnych, które mogą reprezentować pozycję elementu krotki, jest dozwolona tylko w pozycji oznaczonej przez X.

Opcjonalne nazwy elementów nie są reprezentowane w typach ValueTuple<...> i w środowisku uruchomieniowym nie są przechowywane w reprezentacji krotki wartości. Konwersje tożsamości (§10.2.2) istnieją między krotkami, które mają sekwencje typów elementów konwertowalne na zasadzie tożsamości.

Operatora new nie można zastosować ze składnią typów krotek . Wartości krotki można tworzyć na podstawie wyrażeń krotki (§12.8.6) lub stosując operator new bezpośrednio do typu skonstruowanego z ValueTuple<...>.

Elementy krotki są polami publicznymi o nazwach Item1, Item2 itp., do których można uzyskać dostęp za pomocą odwołania do elementu krotki w wartości krotki (§12.8.7). Ponadto jeśli typ krotki ma nazwę danego elementu, ta nazwa może służyć do uzyskiwania dostępu do danego elementu.

Uwaga: nawet jeśli duże krotki są reprezentowane przy użyciu wartości zagnieżdżonych System.ValueTuple<...>, dostęp do każdego elementu krotki można nadal uzyskiwać bezpośrednio za pomocą Item... nazwy odpowiadającej jego pozycji. notatka końcowa

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

(int, string) pair1 = (1, "One");
(int, string word) pair2 = (2, "Two");
(int number, string word) pair3 = (3, "Three");
(int Item1, string Item2) pair4 = (4, "Four");
// Error: "Item" names do not match their position
(int Item2, string Item123) pair5 = (5, "Five");
(int, string) pair6 = new ValueTuple<int, string>(6, "Six");
ValueTuple<int, string> pair7 = (7, "Seven");
Console.WriteLine($"{pair2.Item1}, {pair2.Item2}, {pair2.word}");

Typy krotek dla pair1, pair2 i pair3 są prawidłowe, z nazwami dla żadnych, niektórych lub wszystkich elementów typu krotki.

Typ krotki dla pair4 jest prawidłowy, ponieważ nazwy Item1 i Item2 pasują do swoich pozycji, natomiast typ krotki dla pair5 jest niedozwolony, ponieważ nazwy Item2 i Item123 nie pasują.

Deklaracje dla pair6 i pair7 pokazują, że typy krotek są zamienne ze skonstruowanymi typami w formie ValueTuple<...>, i że operator new jest dozwolony z tą drugą składnią.

Ostatni wiersz pokazuje, że elementy krotki mogą być dostępne za pomocą nazwy Item odpowiadającej ich pozycji, a także poprzez odpowiednią nazwę elementu krotki, jeśli jest ona częścią typu. przykład końcowy

8.3.12 Typy wartości mogące przyjmować wartość null

Typ wartości dopuszczający null może reprezentować wszystkie wartości swojego typu bazowego oraz dodatkową wartość null. Typ wartości nullable jest zapisywany T?, gdzie T jest typem bazowym. Składnia System.Nullable<T> jest skróconą formą System.Nullable<T>, a obie formy mogą być używane zamiennie.

Z drugiej strony, typ wartości bez wartości null jest dowolnym typem wartości innym niż System.Nullable<T> i jego skrótem T? (dla dowolnego T), plus dowolnym parametrem typu, który jest ograniczony jako typ wartości bez wartości null (czyli dowolny parametr typu z ograniczeniem typu wartości (§15.2.5)). Typ System.Nullable<T> określa ograniczenie typu wartości dla T, co oznacza, że bazowy typ typu wartości dopuszczanej do wartości null może być dowolnym typem wartości innej niż null. Podstawowy typ wartości dopuszczającej wartość null nie może być typem dopuszczającym wartość null ani typem referencyjnym. Na przykład int?? jest nieprawidłowym typem. Typy referencyjne zdolne do przyjmowania wartości null są objęte §8.9.

Wystąpienie wartościowego typu T?, który dopuszcza wartość null, ma dwie publiczne właściwości tylko do odczytu.

  • HasValue Właściwość typu bool
  • Value Właściwość typu T

Wystąpienie, dla którego HasValue jest true, jest uważane za mające wartość różną od null. Wystąpienie inne niż null zawiera znaną wartość i Value zwraca wartość.

Wystąpienie, dla którego HasValue jest uznawane za false, jest uznawane za null. Wystąpienie o wartości null ma niezdefiniowaną wartość. Próba odczytania Value w przypadku wystąpienia o wartości null powoduje wyrzucenie wyjątku System.InvalidOperationException. Proces uzyskiwania dostępu do właściwości Value wystąpienia typu dopuszczającego wartość null jest określany jako unwrapping.

Oprócz konstruktora domyślnego każdy typ wartości nullable ma konstruktor publiczny z jednym parametrem typu T?. Biorąc pod uwagę wartość x typu T, wywołanie konstruktora formularza

new T?(x)

Tworzy wystąpienie nie będące wartością null T?, dla którego właściwość Value ma wartość x. Proces tworzenia nie-nullowego wystąpienia typu wartość nullable dla danej wartości jest określany jako zawijanie.

Konwersje niejawne można dokonywać od null literału do T? (§10.2.7) oraz od T do T? (§10.2.6).

Typ T? wartości, która może być null, nie implementuje żadnych interfejsów (§18). W szczególności oznacza to, że nie implementuje żadnego interfejsu, który wykonuje typ bazowy T .

8.3.13 Boxing i rozpakowywanie

Koncepcja boksowania i rozpakowania zapewnia most między value_types i reference_types, zezwalając na konwersję dowolnej wartości value_type na i z typu object. Boxing i unboxing umożliwiają ujednolicony widok systemu typów, w którym wartość dowolnego typu może być ostatecznie traktowana jako object.

Boxing jest opisany bardziej szczegółowo w §10.2.9 i rozpakowywanie jest opisane w §10.3.7.

8.4 Typy skonstruowane

8.4.1 Ogólne

Sama deklaracja typu ogólnego określa typ ogólny , który jest używany jako "strategia" do tworzenia wielu różnych typów, stosując argumenty typu. Argumenty typu są zapisywane w nawiasach kątowych (< i >) bezpośrednio po nazwie typu ogólnego. Typ zawierający co najmniej jeden argument typu jest nazywany typem skonstruowanym. Typ skonstruowany może być używany w większości miejsc w języku, w którym może pojawić się nazwa typu. Niezwiązany typ ogólny może być używany tylko w typeof_expression (§12.8.18).

Typy konstruowane mogą być również używane w wyrażeniach jako proste nazwy (§12.8.4) lub podczas uzyskiwania dostępu do elementu członkowskiego (§12.8.7).

Podczas oceniania namespace_or_type_name są brane pod uwagę tylko typy ogólne z prawidłową liczbą parametrów typu. W związku z tym można użyć tego samego identyfikatora do identyfikowania różnych typów, o ile typy mają różne liczby parametrów typu. Jest to przydatne podczas mieszania ogólnych i niegenerycznych klas w tym samym programie.

Przykład:

namespace Widgets
{
    class Queue {...}
    class Queue<TElement> {...}
}

namespace MyApplication
{
    using Widgets;

    class X
    {
        Queue q1;      // Non-generic Widgets.Queue
        Queue<int> q2; // Generic Widgets.Queue
    }
}

przykład końcowy

Szczegółowe reguły wyszukiwania nazw w namespace_or_type_name produkcji opisano w §7.8. Rozwiązanie niejednoznaczności w tych produkcjach jest opisane w §6.2.5. Type_name może zidentyfikować skonstruowany typ, mimo że nie określa bezpośrednio parametrów typu. Może się tak zdarzyć, gdy typ jest zagnieżdżony w deklaracji ogólnej class , a typ wystąpienia zawierającej deklarację jest niejawnie używany do wyszukiwania nazw (§15.3.9.7).

Przykład:

class Outer<T>
{
    public class Inner {...}

    public Inner i; // Type of i is Outer<T>.Inner
}

przykład końcowy

Typ, który nie jest wyliczeniowy, nie może być używany jako unmanaged_type (§8.8).

8.4.2 Argumenty typu

Każdy argument na liście argumentów typu jest po prostu typem.

type_argument_list
    : '<' type_argument (',' type_argument)* '>'
    ;

type_argument
    : type
    | type_parameter nullable_type_annotation?
    ;

Każdy argument typu spełnia wszelkie ograniczenia dotyczące odpowiedniego parametru typu (§15.2.5). Argument typu odwołania, którego wartość null nie jest zgodna z wartością null parametru typu, spełnia ograniczenie; można jednak wydać ostrzeżenie.

8.4.3 Otwarte i zamknięte typy

Wszystkie typy mogą być klasyfikowane jako typy otwarte lub zamknięte. Typ otwarty to typ, który obejmuje parametry typu. Szczególnie:

  • Parametr typu definiuje typ otwarty.
  • Typ tablicy jest typem otwartym, jeśli i tylko wtedy, gdy jego typ elementu jest typem otwartym.
  • Skonstruowany typ jest typem otwartym, jeśli i tylko wtedy, gdy co najmniej jeden argument typu jest typem otwartym. Skonstruowany typ zagnieżdżony jest typem otwartym wtedy i tylko wtedy, gdy co najmniej jeden z jego argumentów typu lub jeden z argumentów typu jego typów zawierających jest typem otwartym.

Zamknięty typ to typ, który nie jest typem otwartym.

W czasie wykonywania cały kod w ramach deklaracji typu ogólnego jest wykonywany w kontekście zamkniętego skonstruowanego typu, który został utworzony przez zastosowanie argumentów typu do deklaracji ogólnej. Każdy parametr typu w typie ogólnym jest powiązany z określonym typem czasu wykonywania. Przetwarzanie wszystkich instrukcji i wyrażeń w czasie wykonywania zawsze odbywa się z typami zamkniętymi, a typy otwarte występują tylko w procesie kompilacji.

Dwa zamknięte typy skonstruowane są konwertowane identycznie (§10.2.2), jeśli są skonstruowane z tego samego niezwiązanego typu ogólnego, a konwersja tożsamości istnieje między odpowiadającymi argumentami typów. Odpowiednie argumenty typu mogą same w sobie być zamknięte typy skonstruowane lub krotki, które są zamienialne przez tożsamość. Zamknięte konstrukcje typów, które są tożsamościowo przekształcalne, dzielą jeden zestaw zmiennych statycznych. W przeciwnym razie każdy zamknięty typ skonstruowany ma własny zestaw zmiennych statycznych. Ponieważ typ otwarty nie istnieje w czasie wykonywania, nie ma zmiennych statycznych związanych z typem otwartym.

8.4.4 Typy związane i niezwiązane

Termin typ niepowiązany odnosi się do typu niegenerykowego lub niezwiązanego typu ogólnego. Termin typ ograniczenia odnosi się do typu niegenerykowego lub skonstruowanego typu.

Typ niepowiązany odnosi się do jednostki zadeklarowanej przez deklarację typu. Niezwiązany typ ogólny nie jest typem i nie może być używany jako typ zmiennej, argumentu lub wartości zwracanej albo jako typ podstawowy. Jedyną konstrukcją, w której można odwoływać się do typu ogólnego bez wiązania, jest typeof wyrażenie (§12.8.18).

8.4.5 Satysfakcjonujące ograniczenia

Przy każdym odwołaniu się do skonstruowanego typu lub metody ogólnej podane argumenty typu są sprawdzane względem ograniczeń parametrów typu zadeklarowanych dla typu ogólnego lub metody (§15.2.5). Dla każdej where klauzuli argument A typu odpowiadający nazwanego parametrowi typu jest sprawdzany względem każdego ograniczenia w następujący sposób:

  • Jeśli ograniczenie jest typem, typem class interfejsu lub parametrem typu, niech C reprezentuje to ograniczenie za pomocą podanych argumentów typu zastąpionych dowolnymi parametrami typu, które pojawiają się w ograniczeniu. Aby spełnić ograniczenie, musi być spełnione, że typ A jest konwertowany na typ C za pomocą jednego z następujących sposobów:
    • Konwersja tożsamości (§10.2.2)
    • Niejawna konwersja odwołania (§10.2.8)
    • Konwersja boksu (§10.2.9), pod warunkiem, że typ A jest typem wartości niepustej.
    • Domyślne odwołanie, opakowywanie lub konwersja parametru typu A na parametr typu C.
  • Jeśli ograniczenie jest ograniczeniem typu referencyjnego (class), typ A spełnia jeden z następujących warunków:
    • A jest typem interfejsu, typem klasy, typem delegata, typem tablicy lub typem dynamicznym.

    Uwaga: System.ValueType i System.Enum są typami referencyjnymi, które spełniają to ograniczenie. notatka końcowa

    • A jest parametrem typu, który jest znany jako typ odwołania (§8.2).
  • Jeśli ograniczenie jest ograniczeniem typu wartości (struct), typ A spełnia jeden z następujących warunków:
    • A jest typem struct lub typem enum, ale nie jest typem wartości dopuszczającej wartości null.

    Uwaga: System.ValueType i System.Enum są typami referencyjnymi, które nie spełniają tego ograniczenia. notatka końcowa

    • A to parametr typu, który ma ograniczenie typu wartości (§15.2.5).
  • Jeśli ograniczenie jest ograniczeniem new()konstruktora, typ A nie powinien być abstract i ma publiczny konstruktor bez parametrów. Jest to spełnione, jeśli spełniony jest jeden z następujących warunków:
    • A jest typem wartości, ponieważ wszystkie typy wartości mają publiczny konstruktor domyślny (§8.3.3).
    • A jest parametrem typu mającym ograniczenie konstruktora (§15.2.5).
    • A to parametr typu, który ma ograniczenie typu wartości (§15.2.5).
    • A jest elementem class , który nie jest abstrakcyjny i zawiera jawnie zadeklarowany publiczny konstruktor bez parametrów.
    • A nie jest abstract i ma konstruktor domyślny (§15.11.5).

Błąd czasu kompilacji występuje, jeśli co najmniej jedno ograniczenie parametru typu nie jest spełnione przez podane argumenty typu.

Ponieważ parametry typu nie są dziedziczone, ograniczenia nigdy nie są dziedziczone.

Przykład: W poniższym D należy określić ograniczenie dla typu parametru T, aby T spełniało ograniczenie nałożone przez bazę classB<T>. W przeciwieństwie do tego, classE nie musi określać ograniczenia, ponieważ List<T> implementuje IEnumerable dla dowolnego T.

class B<T> where T: IEnumerable {...}
class D<T> : B<T> where T: IEnumerable {...}
class E<T> : B<List<T>> {...}

przykład końcowy

8.5 Parametry typu

Parametr typu to identyfikator określający typ wartości lub typ odwołania, z którego parametr jest powiązany w czasie wykonywania.

type_parameter
    : identifier
    ;

Ponieważ parametr typu można zainstancjonować z wieloma argumentami różnych typów, operacje i ograniczenia parametrów typu są nieco inne niż w przypadku innych typów.

Uwaga: należą do nich:

  • Parametru typu nie można używać bezpośrednio do deklarowania klasy bazowej (§15.2.4.2) ani interfejsu (§18.2.4).
  • Reguły wyszukiwania elementów członkowskich dla parametrów typu zależą od zastosowanych ograniczeń, o ile takie istnieją dla parametru typu. Zostały one szczegółowo opisane w §12.5.
  • Dostępne konwersje parametru typu zależą od ograniczeń, jeśli istnieją, zastosowanych do parametru typu. Są one szczegółowo opisane w §10.2.12 i §10.3.8.
  • Literał null nie może być konwertowany na typ podany przez parametr typu, chyba że parametr typu jest znany jako typ referencyjny (§10.2.12). Można jednak użyć wyrażenia domyślnego (§12.8.21). Ponadto wartość o typie podanym przez parametr typu może być porównywana z wartością null przy użyciu parametru == i != (§12.12.7), chyba że parametr typu ma ograniczenie typu wartości.
  • Wyrażenie new (§12.8.17.2) może być używane tylko z parametrem typu, jeśli parametr typu jest ograniczony przez constructor_constraint lub ograniczenie typu wartości (§15.2.5).
  • Parametr typu nie może być używany w dowolnym miejscu w atrybucie.
  • Nie można użyć parametru typu w dostępie składowym (§12.8.7) ani nazwy typu (§7.8) do identyfikacji statycznego elementu członkowskiego lub typu zagnieżdżonego.
  • Nie można użyć parametru typu jako unmanaged_type (§8.8).

notatka końcowa

Jako typ parametry typu są wyłącznie konstrukcją czasu kompilacji. W czasie wykonywania każdy parametr typu jest powiązany z typem czasu wykonywania określonym przez podanie argumentu typu do deklaracji typu ogólnego. W związku z tym typ zmiennej zadeklarowanej przy użyciu parametru typu będzie w czasie wykonywania typu zamkniętego typu §8.4.3. Wykonywanie w czasie wykonywania wszystkich instrukcji i wyrażeń obejmujących parametry typu używa typu, który został podany jako argument typu dla tego parametru.

8.6 Typy drzewa wyrażeń

Drzewa wyrażeń umożliwiają reprezentację wyrażeń lambda jako struktury danych zamiast kodu wykonywalnego. Drzewa wyrażeń to wartości typów drzewa wyrażeń formularza System.Linq.Expressions.Expression<TDelegate>, gdzie TDelegate jest dowolnym typem delegata. W pozostałej części tej specyfikacji te typy będą określane przy użyciu skrótu Expression<TDelegate>.

Jeśli istnieje konwersja z wyrażenia lambda na typ delegata D, to istnieje również konwersja na typ drzewa wyrażeń Expression<TDelegate>. Podczas gdy konwersja wyrażenia lambda na typ delegata generuje delegata, który odwołuje się do kodu wykonywalnego dla wyrażenia lambda, konwersja na typ drzewa wyrażeń tworzy reprezentację drzewa wyrażeń tego wyrażenia lambda. Więcej szczegółów tej konwersji podano w §10.7.3.

Przykład: Poniższy program reprezentuje wyrażenie lambda zarówno jako kod wykonywalny, jak i jako drzewo wyrażeń. Ponieważ istnieje konwersja na Func<int,int>, istnieje również konwersja na Expression<Func<int,int>>:

Func<int,int> del = x => x + 1;             // Code
Expression<Func<int,int>> exp = x => x + 1; // Data

Po tych przydziałach delegat del odwołuje się do metody zwracającej x + 1, a drzewo wyrażeń exp odwołuje się do struktury danych, która opisuje wyrażenie x => x + 1.

przykład końcowy

Expression<TDelegate> zapewnia metodę wystąpienia Compile, która tworzy delegata typu TDelegate:

Func<int,int> del2 = exp.Compile();

Wywołanie tego delegata powoduje wykonanie kodu reprezentowanego przez drzewo wyrażeń. W związku z tym, biorąc pod uwagę powyższe definicje, del i del2 są równoważne, a następujące dwie instrukcje będą miały taki sam efekt:

int i1 = del(1);
int i2 = del2(1);

Po wykonaniu tego kodu zarówno i1, jak i i2 będą miały wartość 2.

Powierzchnia API dostarczana przez Expression<TDelegate> jest definiowana przez implementację, poza wymaganiem dla opisanej powyżej metody Compile.

Uwaga: Chociaż szczegóły interfejsu API podane dla drzew wyrażeń są zdefiniowane przez implementację, oczekuje się, że implementacja:

  • Umożliwienie inspekcji struktury drzewa wyrażeń i odpowiedzi na nie, które powstało w wyniku konwersji wyrażenia lambda
  • Włącz tworzenie drzew wyrażeń programowo w kodzie użytkownika

notatka końcowa

8.7 Typ dynamiczny

Typ dynamic używa powiązania dynamicznego, jak opisano szczegółowo w §12.3.2, w przeciwieństwie do powiązania statycznego, który jest używany przez wszystkie inne typy.

Typ dynamic jest uznawany za identyczny object z wyjątkiem następujących kwestii:

  • Operacje na wyrażeniach typu dynamic mogą być dynamicznie powiązane (§12.3.3).
  • Wnioskowanie typu (§12.6.3) będzie preferować dynamic nad object, jeśli obie są kandydatami.
  • dynamic nie można użyć jako
    • typ w wyrażeniu tworzenia obiektu (§12.8.17.2)
    • klasa_bazowa (§15.2.4)
    • typ_predefiniowany w dostęp_do_członka (§12.8.7.1)
    • operand operatora typeof
    • argument atrybutu
    • ograniczenie
    • typ metody rozszerzenia
    • jakakolwiek część argumentu typu w struct_interfaces (§16.2.5) lub interface_type_list (§15.2.4.1).

Ze względu na tę równoważność, zachodzi następujące twierdzenie:

  • Istnieje niejawna konwersja tożsamości
    • między object i dynamic
    • między skonstruowanymi typami, które są takie same przy zastąpieniu dynamic przez object
    • między typami tupli, które są identyczne przy zamianie dynamic na object
  • Niejawne i jawne konwersje do i z object mają również zastosowanie do i z dynamic.
  • Podpisy, które są takie same podczas zastępowania dynamic za pomocą object , są uznawane za ten sam podpis.
  • Typ dynamic jest nie do odróżnienia od typu object w czasie wykonywania.
  • Wyrażenie typu dynamic jest określane jako wyrażenie dynamiczne.

8.8 Typy niezarządzane

unmanaged_type
    : value_type
    | pointer_type     // unsafe code support
    ;

unmanaged_type to dowolny typ, który nie jest ani reference_type, ani type_parameter, który nie jest ograniczony do tego, aby nie być niezarządzanym, i nie zawiera pól instancji, których typ nie jest unmanaged_type. Innymi słowy, unmanaged_type jest jednym z następujących:

  • sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, lub bool.
  • Dowolny enum_type.
  • Każda zdefiniowana przez użytkownika struktura struct_type, która zawiera tylko pola instancji typu unmanaged_type.
  • Dowolny parametr typu, który musi być niezarządzany.
  • Każde pointer_type (§23.3).

8.9 Typy referencyjne i nullowalność

8.9.1 Ogólne

Typ odwołania dopuszczającego wartość null jest oznaczany przez dołączenie nullable_type_annotation (?) do typu odwołania bez wartości null. Nie ma żadnej semantycznej różnicy między typem odwołania nienależącym do wartości null a odpowiadającym mu typem dopuszczanym do wartości null, oba mogą być odwołaniem do obiektu lub null. Obecność lub brak nullable_type_annotation deklaruje, czy wyrażenie ma zezwalać na wartości null, czy nie. Kompilator może zapewnić diagnostykę, gdy wyrażenie nie jest używane zgodnie z tym zamiarem. Stan null wyrażenia jest zdefiniowany w §8.9.5. Istnieje konwersja tożsamości pomiędzy typem odwołania dopuszczającym null a jego odpowiadającym typem odwołania niedopuszczającym null (§10.2.2).

Istnieją dwie formy nullowalności dla typów referencyjnych:

  • nullable: można przypisać null. Domyślny stan null to może mieć wartość null.
  • niemające wartości null: Odwołaniu niemającemu wartości null nie należy przypisywać wartości null null. Domyślny stan null nie ma wartości null.

Uwagi: Typy R i R? są reprezentowane przez ten sam typ bazowy, R. Zmienna tego typu bazowego może zawierać odwołanie do obiektu lub wartość null, która wskazuje "brak odwołania". notatka końcowa

Rozróżnienie składniowe między nullowalnym typem referencyjnym a odpowiadającym mu nienullowalnym typem referencyjnym pozwala kompilatorowi generować diagnostykę. Kompilator musi zezwolić na nullable_type_annotation zgodnie z definicją w §8.2.1. Diagnostyka musi być ograniczona do ostrzeżeń. Ani obecność ani brak adnotacji dopuszczanych do wartości null, ani stan kontekstu dopuszczalnego do wartości null nie może zmienić czasu kompilacji lub zachowania środowiska uruchomieniowego programu z wyjątkiem zmian w żadnych komunikatach diagnostycznych generowanych w czasie kompilacji.

8.9.2 Typy odwołań bez wartości null

Niezmienny typ referencyjny jest typem referencyjnym w formie T, gdzie T jest nazwą typu. Domyślny stan null zmiennej, która nie może zawierać wartości null, nie jest równa null. Ostrzeżenia mogą być generowane, gdy używane jest wyrażenie, które może mieć wartość null, tam, gdzie wymagana jest wartość nie-null.

8.9.3 Typy referencyjne dopuszczające wartość null

Typ odwołania formularza T? (na przykład string?) jest typem odwołania dopuszczanym do wartości null. Domyślny stan null zmiennej dopuszczanej do wartości null może mieć wartość null. Adnotacja ? wskazuje, że zmienne tego typu mogą przyjmować wartość null. Kompilator może rozpoznać te intencje w celu wystawienia ostrzeżeń. Gdy kontekst adnotacji dopuszczający wartość null jest wyłączony, użycie tej adnotacji może wygenerować ostrzeżenie.

Kontekst dopuszczający wartości null 8.9.4

8.9.4.1 Ogólne

Każdy wiersz kodu źródłowego ma kontekst z możliwością wartości null. Flagi adnotacji i ostrzeżeń dla kontrolek kontekstu adnotacji dozwalających null (§8.9.4.3) i ostrzeżeń dozwalających null (§8.9.4.4), odpowiednio. Każda flaga może być włączona lub wyłączona. Kompilator może używać analizy przepływu statycznego do określania stanu null dowolnej zmiennej referencyjnej. Stan null zmiennej referencyjnej (§8.9.5) nie ma wartości null, może mieć wartość null, a może wartość domyślną.

Kontekst dopuszczalny do wartości null może być określony w kodzie źródłowym za pośrednictwem dyrektyw dopuszczanych do wartości null (§6.5.9) i/lub za pośrednictwem określonego mechanizmu specyficznego dla implementacji zewnętrznego kodu źródłowego. Jeśli oba podejścia są używane, dyrektywy pozwalające na wartości null zastępują ustawienia dokonane przez mechanizm zewnętrzny.

Domyślny stan kontekstu dopuszczanego do wartości null jest zdefiniowany przez implementację.

W całej tej specyfikacji zakłada się, że cały kod języka C#, który nie zawiera dyrektyw nullable ani nie ma określonego bieżącego stanu kontekstu nullable, został skompilowany przy użyciu kontekstu nullable, w którym włączono zarówno adnotacje, jak i ostrzeżenia.

Uwaga: Kontekst dopuszczalny do wartości null, w którym obie flagi są wyłączone, jest zgodny z poprzednim standardowym zachowaniem typów referencyjnych. notatka końcowa

Wyłączenie funkcji Nullable

Gdy flagi ostrzeżenia i adnotacji są wyłączone, kontekst dopuszczający wartość null jest wyłączony.

Gdy kontekst dopuszczalny do wartości null jest wyłączony:

  • Nie zostanie wygenerowane żadne ostrzeżenie, gdy zmienna typu odwołania bez adnotacji jest inicjowana lub przypisywana wartość null.
  • Nie będzie wygenerowane ostrzeżenie, gdy zmienna typu referencyjnego może mieć wartość null.
  • W przypadku dowolnego typu T odwołania, adnotacja ? w T? generuje komunikat, a typ T? jest taki sam jak T.
  • W przypadku ograniczenia where T : C?parametru dowolnego typu adnotacja ? w pliku C? generuje komunikat, a typ C? jest taki sam jak C.
  • W przypadku ograniczenia where T : U?parametru dowolnego typu adnotacja ? w pliku U? generuje komunikat, a typ U? jest taki sam jak U.
  • Ograniczenie ogólne class? generuje komunikat ostrzegawczy. Parametr typu musi być typem referencyjnym.

    Uwaga: ten komunikat jest scharakteryzowany jako "informacyjny", a nie "ostrzeżenie", tak aby nie mylić go ze stanem ustawienia ostrzeżenia dopuszczalnego do wartości null, które nie jest powiązane. notatka końcowa

  • Operator ! ignorujący wartość null (§12.8.9) nie ma wpływu.

Przykład:

#nullable disable annotations
string? s1 = null;    // Informational message; ? is ignored
string s2 = null;     // OK; null initialization of a reference
s2 = null;            // OK; null assignment to a reference
char c1 = s2[1];      // OK; no warning on dereference of a possible null;
                      //     throws NullReferenceException
c1 = s2![1];          // OK; ! is ignored

przykład końcowy

8.9.4.3 Adnotacje obsługujące wartości null

Kiedy flaga ostrzeżeń jest wyłączona, a flaga adnotacji jest włączona, kontekst wartości null to adnotacje.

Gdy kontekst, w którym dopuszczalne są wartości null, jest określony przez adnotacje:

  • W przypadku dowolnego typu T odwołania adnotacja ? w T? wskazuje, że T? to typ dopuszczający wartość null, natomiast nieoznaczone T nie dopuszcza wartości null.
  • Nie są generowane żadne ostrzeżenia diagnostyczne związane z wartością null.
  • Operator ! tworzący stan null (§12.8.9) może zmienić analizowany stan null operandu i wygenerowane ostrzeżenia diagnostyczne podczas kompilacji.

Przykład:

#nullable disable warnings
#nullable enable annotations
string? s1 = null;    // OK; ? makes s2 nullable
string s2 = null;     // OK; warnings are disabled
s2 = null;            // OK; warnings are disabled
char c1 = s2[1];      // OK; warnings are disabled; throws NullReferenceException
c1 = s2![1];          // No warnings

przykład końcowy

Ostrzeżenia dotyczące typu nullable

Gdy flaga ostrzeżenia jest włączona, a flaga adnotacji jest wyłączona, kontekst dopuszczający wartość null jest ostrzegawczy.

Gdy kontekst nullable jest ostrzeżenia, kompilator może wygenerować diagnostykę w następujących przypadkach:

  • Zmienna referencyjna, która została określona jako może mieć wartość null, jest wyłuszczana.
  • Zmienna referencyjna typu innego niż null jest przypisywana do wyrażenia, które może mieć wartość null.
  • Element ? jest używany do oznaczenia typu odwołania, które może przyjmować wartość null.
  • Operator ! ignorujący wartość null (§12.8.9) służy do ustawiania operandu jako niepusty.

Przykład:

#nullable disable annotations
#nullable enable warnings
string? s1 = null;    // OK; ? makes s2 nullable
string s2 = null;     // OK; null-state of s2 is "maybe null"
s2 = null;            // OK; null-state of s2 is "maybe null"
char c1 = s2[1];      // Warning; dereference of a possible null;
                      //          throws NullReferenceException
c1 = s2![1];          // The warning is suppressed

przykład końcowy

8.9.4.5 Włączenie obsługi typów nullable

Gdy zarówno flaga ostrzeżenia, jak i flaga adnotacji są włączone, kontekst dopuszczający wartość jest włączony.

Gdy kontekst dopuszczający wartości null jest włączony:

  • W przypadku dowolnego typu T odwołania, adnotacja ? w elemencie T? sprawia, że T? jest typem dopuszczającym wartości null, natomiast T bez adnotacji jest nieodpuszczający wartości null.
  • Kompilator może używać analizy przepływu statycznego do określania stanu null dowolnej zmiennej referencyjnej. Gdy są włączone ostrzeżenia o wartości null, stan null dla zmiennej referencyjnej (§8.9.5) jest nie ma wartości 'null', może mieć wartość null, lub może być wartością domyślną oraz
  • Operator ! zapewniający nietraktowanie jako null (§12.8.9) ustawia stan operandu na nie null.
  • Kompilator może wydać ostrzeżenie, jeśli wartość null parametru typu nie jest zgodna z wartością null odpowiedniego argumentu typu.

8.9.5 Wartości null i stany null

8.9.5.1 Ogólne

Kompilator nie jest wymagany do przeprowadzenia żadnej analizy statycznej ani nie jest wymagany do wygenerowania żadnych ostrzeżeń diagnostycznych związanych z wartością null.

Pozostała część tej podklauzuli jest warunkowo normatywna.

Analiza przepływu 8.9.5.2

Kompilator, który generuje ostrzeżenia diagnostyczne, jest zgodny z tymi regułami.

Każde wyrażenie ma jeden z trzech stanów null:

  • może mieć wartość null: wartość wyrażenia może mieć wartość null.
  • być może wartość domyślna: wartość wyrażenia może mieć wartość domyślną dla tego typu.
  • not null: wartość wyrażenia nie jest wartością null.

Domyślny stan null wyrażenia jest określany przez jego typ i stan flagi adnotacji po zadeklarowaniu:

  • Domyślny stan null dla typu odwołania dopuszczającego wartość null to:
    • Może ma wartość null, gdy jego deklaracja znajduje się w tekście, w którym jest włączona flaga adnotacji.
    • Nie jest nullem, gdy deklaracja znajduje się w tekście, w którym flaga adnotacji jest nieaktywna.
  • Domyślny stan typu odwołania niemającego wartości null nie jest wartością null.

Uwaga: Stan może być domyślny jest używany z nieskrępowanymi parametrami typu, gdy typ jest niemogący być nullem, taki jak string, a wyrażenie default(T) jest wartością null. Ponieważ wartość null nie znajduje się w domenie dla typu nieprzyjmującego wartości null, stan może być domyślny. notatka końcowa

Diagnostykę można utworzyć, gdy zmienna (§9.2.1) typu odniesienia niezdolnego do przyjęcia wartości null jest inicjowana lub przypisywana do wyrażenia, które może mieć wartość null, w sytuacji, gdy ta zmienna jest zadeklarowana w tekście z włączoną flagą adnotacji.

Przykład: Rozważmy następującą metodę, w której parametr ma wartość null, a ta wartość jest przypisywana do typu niezwiązanego z wartością null:

#nullable enable
public class C
{
    public void M(string? p)
    {
        // Warning: Assignment of maybe null value to non-nullable variable
        string s = p;
    }
}

Kompilator może wydać ostrzeżenie, w którym parametr, który może mieć wartość null, jest przypisany do zmiennej, która nie powinna mieć wartości null. Jeśli parametr jest sprawdzany pod kątem wartości null przed przypisaniem, kompilator może użyć tego w analizie stanu nullowalności i nie wydać ostrzeżenia.

#nullable enable
public class C
{
    public void M(string? p)
    {
        if (p != null)
        {
            string s = p; // No warning
            // Use s
        }
    }
}

przykład końcowy

Kompilator może zaktualizować stan null zmiennej w ramach analizy.

Przykładowy: kompilator może wybrać aktualizację stanu na podstawie dowolnych instrukcji w programie:

#nullable enable
public void M(string? p)
{
    int length = p.Length; // Warning: p is maybe null

    string s = p; // No warning. p is not null

    if (s != null)
    {
        int l2 = s.Length; // No warning. s is not null
    }
    int l3 = s.Length; // Warning. s is maybe null
}

W poprzednim przykładzie kompilator może zdecydować, że po instrukcji int length = p.Length;stan p jest nie-null. Gdyby to miało wartość null, instrukcja ta zgłosiłaby wartość NullReferenceException. Jest to podobne do zachowania, jakie wystąpiłoby, gdyby kod został poprzedzony if (p == null) throw NullReferenceException();, z tym wyjątkiem, że kod w obecnej formie może wygenerować ostrzeżenie, informujące, że wyjątek może zostać zgłoszony niejawnie. przykład końcowy

W dalszej części metody kod sprawdza, czy s nie jest odwołaniem o wartości null. Stan zerowy/niczny s może stać się zerowy/jak null po zamknięciu bloku sprawdzającego wartość null. Kompilator może wnioskować, że s może mieć wartość null, ponieważ kod został napisany, aby założyć, że może mieć wartość null. Ogólnie rzecz biorąc, gdy kod zawiera sprawdzanie wartości null, kompilator może wnioskować, że wartość mogłaby być null.

Przykład: każde z następujących wyrażeń zawiera jakąś formę sprawdzania wartości null. Stan nulości o może ulec zmianie z nie-null na null po każdej z poniższych instrukcji.

#nullable enable
public void M(string s)
{
    int length = s.Length; // No warning. s is not null

    _ = s == null; // Null check by testing equality. The null state of s is maybe null
    length = s.Length; // Warning, and changes the null state of s to not null

    _ = s?.Length; // The ?. is a null check and changes the null state of s to maybe null
    if (s.Length > 4) // Warning. Changes null state of s to not null
    {
        _ = s?[4]; // ?[] is a null check and changes the null state of s to maybe null
        _ = s.Length; // Warning. s is maybe null
    }
}

Deklaracje zarówno auto-właściwości, jak i zdarzeń podobnych do pól korzystają z pola zapasowego wygenerowanego przez kompilator. Analiza stanu null może wnioskować, że przypisanie do zdarzenia lub właściwości jest przypisaniem do pola zapasowego wygenerowanego przez kompilator.

Przykład: kompilator może określić, że zapisywanie właściwości automatycznej lub zdarzenia podobnego do pola zapisuje odpowiednie pole pomocnicze wygenerowane przez kompilator. Stan null właściwości jest zgodny z wartością pola zapasowego.

class Test
{
    public string P
    {
        get;
        set;
    }

    public Test() {} // Warning. "P" not set to a non-null value.

    static void Main()
    {
        var t = new Test();
        int len = t.P.Length; // No warning. Null state is not null.
    }
}

W poprzednim przykładzie konstruktor nie ustawia P na wartość niepustą, a kompilator może wyświetlić ostrzeżenie. Nie ma ostrzeżenia, gdy uzyskuje się dostęp do właściwości P, ponieważ typ tej właściwości jest typem odwołania bez wartości null. przykład końcowy

Kompilator może traktować właściwość (§15.7) jako zmienną ze stanem lub jako niezależne akcesory dostępu get i set (§15.7.3).

Przykład: kompilator może wybrać, czy zapisywanie we właściwości zmienia stan null odczytywania właściwości, czy odczytywanie właściwości zmienia stan null tej właściwości.

class Test
{
    private string? _field;
    public string? DisappearingProperty
    {
        get
        {
            string tmp = _field;
            _field = null;
            return tmp;
        }
        set
        {
            _field = value;
        }
    }

    static void Main()
    {
        var t = new Test();
        if (t.DisappearingProperty != null)
        {
            int len = t.DisappearingProperty.Length; // No warning. A compiler can assume property is stateful
        }
    }
}

W poprzednim przykładzie pole pomocnicze dla DisappearingProperty zostaje ustawione na null podczas odczytu. Jednak kompilator może założyć, że odczytywanie właściwości nie zmienia stanu null tego wyrażenia. przykład końcowy

Kompilator może użyć dowolnego wyrażenia, które wyłusza zmienną, właściwość lub zdarzenie, aby ustawić stan null na wartość nie null. Gdyby miało wartość null, wyrażenie dereferencji rzuciłoby wyjątek NullReferenceException:

Przykład:


public class C
{
    private C? child;

    public void M()
    {
        _ = child.child.child; // Warning. Dereference possible null value
        var greatGrandChild = child.child.child; // No warning. 
    }
}

przykład końcowy

8.9.5.3 Konwersje typów

Kompilator, który generuje ostrzeżenia diagnostyczne, jest zgodny z tymi regułami.

Uwaga: Różnice w adnotacjach nullowalności najwyższego poziomu lub zagnieżdżonych adnotacjach nullowalności w typach nie mają wpływu na to, czy konwersja między typami jest dozwolona, ponieważ nie ma różnicy semantycznej między typem referencyjnym nienullowalnym a odpowiadającym mu typem nullowalnym (§8.9.1). notatka końcowa

Kompilator może wydać ostrzeżenie, gdy anotacje nullowalności różnią się między dwoma typami, na najwyższym poziomie lub zagnieżdżone, podczas gdy konwersja jest zawężająca.

Przykład: typy różniące się adnotacjami najwyższego poziomu

#nullable enable
public class C
{
    public void M1(string p)
    {
        _ = (string?)p; // No warning, widening
    }

    public void M2(string? p)
    {
        _ = (string)p; // Warning, narrowing
        _ = (string)p!; // No warning, suppressed
    }
}

przykład końcowy

Przykład: typy o zróżnicowanych zagnieżdżonych adnotacjach nullowości

#nullable enable
public class C
{
    public void M1((string, string) p)
    {
        _ = ((string?, string?))p; // No warning, widening
    }

    public void M2((string?, string?) p)
    {
        _ = ((string, string))p; // Warning, narrowing
        _ = ((string, string))p!; // No warning, suppressed
    }
}

przykład końcowy

Kompilator może przestrzegać reguł wariancji interfejsu (§18.2.3.3), wariancji delegata (§20.4) i kowariancji tablicy (§17.6) podczas określania, czy ma zostać wyświetlone ostrzeżenie dotyczące konwersji typów.

#nullable enable
public class C
{
    public void M1(IEnumerable<string> p)
    {
        IEnumerable<string?> v1 = p; // No warning
    }

    public void M2(IEnumerable<string?> p)
    {
        IEnumerable<string> v1 = p; // Warning
        IEnumerable<string> v2 = p!; // No warning
    }

    public void M3(Action<string?> p)
    {
        Action<string> v1 = p; // No warning
    }

    public void M4(Action<string> p)
    {
        Action<string?> v1 = p; // Warning
        Action<string?> v2 = p!; // No warning
    }

    public void M5(string[] p)
    {
        string?[] v1 = p; // No warning
    }

    public void M6(string?[] p)
    {
        string[] v1 = p; // Warning
        string[] v2 = p!; // No warning
    }
}

przykład końcowy

Kompilator może wydać ostrzeżenie, gdy nullowalność różni się w obie strony w typach, które nie zezwalają na konwersję wariantu.

#nullable enable
public class C
{
    public void M1(List<string> p)
    {
        List<string?> v1 = p; // Warning
        List<string?> v2 = p!; // No warning
    }

    public void M2(List<string?> p)
    {
        List<string> v1 = p; // Warning
        List<string> v2 = p!; // No warning
    }
}

przykład końcowy

Koniec warunkowo normatywnego tekstu