Udostępnij za pośrednictwem


12 wyrażeń

12.1 Ogólne

Wyrażenie to sekwencja operatorów i operandów. Ta klauzula definiuje składnię, kolejność obliczania operandów i operatorów oraz znaczenie wyrażeń.

12.2 Klasyfikacje wyrażeń

12.2.1 Ogólne

Wynik wyrażenia jest klasyfikowany jako jeden z następujących:

  • Wartość. Każda wartość ma skojarzony typ.
  • Zmienna. Jeśli nie określono inaczej, zmienna jest jawnie typowana i ma skojarzony typ, czyli zadeklarowany typ zmiennej. Niejawnie typizowana zmienna nie ma skojarzonego typu.
  • Literał o wartości null. Wyrażenie z tą klasyfikacją można niejawnie przekonwertować na typ odwołania lub typ wartości nullowalnej.
  • Funkcja anonimowa. Wyrażenie z tą klasyfikacją można niejawnie przekonwertować na zgodny typ delegata lub typ drzewa wyrażeń.
  • Krotka. Każda krotka ma stałą liczbę elementów, z których każda ma wyrażenie i opcjonalną nazwę elementu krotki.
  • Dostęp do właściwości. Każdy dostęp do właściwości ma skojarzony typ, czyli typ właściwości. Ponadto dostęp do właściwości może mieć skojarzone wyrażenie obiektu. Kiedy zostanie wywołany akcesor dostępu do właściwości instancji, wynik oceny wyrażenia instancji staje się instancją reprezentowaną przez this (§12.8.14).
  • Dostęp do indeksatora. Każdy dostęp indeksatora ma skojarzony typ, a mianowicie typ elementu indeksatora. Ponadto dostęp indeksatora ma skojarzoną listę argumentów i skojarzone wyrażenie wystąpienia. Po wywołaniu akcesora indeksatora, wynik oceny wyrażenia instancji staje się instancją reprezentowaną przez this (§12.8.14), a wynik oceny listy argumentów staje się listą parametrów wywołania.
  • Nic. Dzieje się tak, gdy wyrażenie jest wywołaniem metody z zwracanym typem void. Wyrażenie sklasyfikowane jako nic nie jest prawidłowe tylko w kontekście statement_expression (§13.7) lub jako treść lambda_expression (§12.19).

W przypadku wyrażeń, które występują jako podwyrażenia większych wyrażeń, z uwzględnionymi ograniczeniami, wynik może być również sklasyfikowany jako jeden z następujących:

  • Przestrzeń nazw. Wyrażenie z tą klasyfikacją może pojawić się tylko po lewej stronie member_access (§12.8.7). W jakimkolwiek innym kontekście wyrażenie zaklasyfikowane jako przestrzeń nazw powoduje błąd czasu kompilacji.
  • Typ. Wyrażenie z tą klasyfikacją może pojawić się tylko po lewej stronie member_access (§12.8.7). W każdym innym kontekście wyrażenie sklasyfikowane jako typ powoduje błąd czasu kompilacji.
  • Grupa metod, która jest zestawem przeciążonych metod wynikających z wyszukiwania składowego (§12.5). Grupa metod może mieć skojarzone wyrażenie wystąpienia i skojarzoną listę argumentów typu. Po wywołaniu metody wystąpienia wynik oceny wyrażenia wystąpienia staje się wystąpieniem reprezentowanym przez this (§12.8.14). Grupa metod jest dozwolona w invocation_expression (§12.8.10) lub delegate_creation_expression (§12.8.17.6) i może być niejawnie przekonwertowana na zgodny typ delegata (§10.8). W każdym innym kontekście wyrażenie sklasyfikowane jako grupa metod powoduje błąd czasu kompilacji.
  • Dostęp do wydarzenia. Każdy dostęp do zdarzenia jest powiązany z określonym typem, czyli typem tego zdarzenia. Ponadto dostęp do zdarzenia może mieć skojarzone wyrażenie wystąpienia. Dostęp do zdarzeń może występować jako lewy operand operatorów += i -= (§12.21.5). W każdym innym kontekście wyrażenie sklasyfikowane jako dostęp do zdarzenia powoduje błąd czasu kompilacji. Gdy zostanie wywołany dostępnik dostępu do zdarzenia instancji, wynik oceny wyrażenia instancji staje się instancją reprezentowaną przez this (§12.8.14).
  • Wyrażenie throw może być używane w różnych kontekstach do zgłaszania wyjątku w wyrażeniu. Wyrażenie rzutu może zostać przekonwertowane przez niejawną konwersję na dowolny typ.

Dostęp do właściwości lub dostęp indeksatora jest zawsze ponownie klasyfikowany jako wartość, wykonując wywołanie metody get lub metody set. Określona metoda dostępu jest determinowana przez kontekst właściwości lub dostępu indeksatora: Jeśli dostęp jest celem przypisania, wywoływany jest akcesor set w celu przypisania nowej wartości (§12.21.2). W przeciwnym razie, akcesor pobierający jest wywoływany, aby uzyskać bieżącą wartość (§12.2.2).

wystąpienie dostępu to dostęp do właściwości w wystąpieniu, dostęp do zdarzenia w wystąpieniu lub dostęp do indeksatora.

12.2.2 Wartości wyrażeń

Większość konstrukcji, które obejmują wyrażenie, ostatecznie wymaga wyrażenia, aby oznaczyć wartość . W takich przypadkach, jeśli rzeczywiste wyrażenie oznacza przestrzeń nazw, typ, grupę metod lub nic, wystąpi błąd czasu kompilacji. Jeśli jednak wyrażenie oznacza dostęp do właściwości, dostęp indeksatora lub zmienną, wartość właściwości, indeksatora lub zmiennej jest niejawnie zastępowana:

  • Wartość zmiennej jest po prostu wartością przechowywaną obecnie w lokalizacji przechowywania identyfikowanej przez zmienną. Zmienna jest uznawana za zdecydowanie przypisaną (§9.4) przed uzyskaniem jej wartości lub w przeciwnym razie wystąpi błąd czasu kompilacji.
  • Wartość wyrażenia dostępu do właściwości jest uzyskiwana przez wywołanie metody uzyskiwania dostępu do właściwości. Jeśli właściwość nie ma akcesora get, wystąpi błąd czasu kompilacji. W przeciwnym razie wykonywane jest wywołanie elementu członkowskiego funkcji (§12.6.6), a wynik wywołania staje się wartością wyrażenia dostępu do właściwości.
  • Wartość wyrażenia dostępu indeksatora jest uzyskiwana przez wywołanie akcesora pobierającego indeksatora. Jeśli indeksator nie ma akcesora get, wystąpi błąd czasu kompilacji. W przeciwnym razie wykonywane jest wywołanie elementu członkowskiego funkcji (§12.6.6) z listą argumentów skojarzoną z wyrażeniem dostępu do indeksatora, a wynik tego wywołania staje się wartością tego wyrażenia.
  • Wartość wyrażenia krotkowego jest uzyskiwana przez zastosowanie niejawnej konwersji krotkowej (§10.2.13) do typu wyrażenia krotkowego. Błędem jest uzyskiwanie wartości wyrażenia krotki, które nie ma typu.

12.3 Powiązanie statyczne i dynamiczne

12.3.1 Ogólne

powiązanie to proces określania, do czego odnosi się operacja, na podstawie typu lub wartości wyrażeń (argumentów, operandów, odbiorników). Na przykład powiązanie wywołania metody jest określane na podstawie typu odbiornika i argumentów. Powiązanie operatora jest określane na podstawie typu operandów.

W języku C# powiązanie operacji jest zwykle określane w czasie kompilacji na podstawie typu zdefiniowanego w czasie kompilacji jego podwyrażeń. Podobnie, jeśli wyrażenie zawiera błąd, błąd zostanie wykryty i zgłoszony w czasie kompilacji. Takie podejście jest nazywane powiązaniem statycznym.

Jeśli jednak wyrażenie jest wyrażeniem dynamicznym (tj. ma typ dynamic), oznacza to, że każde powiązanie, w których uczestniczy, powinno być oparte na typie czasu wykonywania, a nie typie, który ma w czasie kompilacji. Powiązanie takiej operacji jest zatem odroczone do momentu, w którym operacja ma zostać wykonana w trakcie działania programu. Jest to nazywane powiązaniem dynamicznym .

Gdy operacja jest dynamicznie powiązana, podczas kompilacji wykonywane jest niewielkie lub żadne sprawdzanie. Zamiast tego, jeśli powiązanie w czasie wykonywania zakończy się niepowodzeniem, błędy są zgłaszane jako wyjątki w czasie wykonywania.

Następujące operacje w języku C# podlegają powiązaniu:

  • Dostęp do członków: e.M
  • Wywołanie metody: e.M(e₁,...,eᵥ)
  • Wywołanie delegata: e(e₁,...,eᵥ)
  • Dostęp do elementów: e[e₁,...,eᵥ]
  • Tworzenie obiektu: nowe C(e₁,...,eᵥ)
  • Przeciążone operatory jednoargumentowe: +, -, ! (tylko negacja logiczna), ~, ++, --, true, false
  • Przeciążone operatory binarne: +, -, *, /, %, &, &&, |, ||, ??, ^, <<, >>, ==, !=, >, <, >=, <=
  • Operatory przypisania: =, = ref, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
  • Niejawne i jawne konwersje

Jeśli nie są wykorzystywane wyrażenia dynamiczne, język C# domyślnie stosuje powiązanie statyczne, co oznacza, że w procesie wyboru używane są typy podwyrażeń ustalane w czasie kompilacji. Jeśli jednak jedno z podwyrażeń w operacjach wymienionych powyżej jest wyrażeniem dynamicznym, operacja jest zamiast tego dynamicznie powiązana.

Jest to błąd czasu kompilacji, jeśli wywołanie metody jest dynamicznie powiązane, a dowolny z parametrów, w tym odbiornika, są parametrami wejściowymi.

12.3.2 Czas wiązania

Powiązanie statyczne odbywa się w czasie kompilacji, natomiast powiązanie dynamiczne odbywa się w czasie wykonywania. W poniższych podpunktach termin czas wiązania odnosi się do czasu kompilacji lub czasu wykonywania, w zależności od tego, kiedy ma miejsce wiązanie.

Przykład: Poniżej przedstawiono pojęcia statycznego i dynamicznego powiązania oraz czasu powiązania:

object o = 5;
dynamic d = 5;
Console.WriteLine(5); // static binding to Console.WriteLine(int)
Console.WriteLine(o); // static binding to Console.WriteLine(object)
Console.WriteLine(d); // dynamic binding to Console.WriteLine(int)

Dwa pierwsze wywołania są statycznie powiązane: przeciążenie Console.WriteLine jest wybierane na podstawie typu czasu kompilacji argumentu. W związku z tym czas powiązania jest czas kompilacji.

Trzecie wywołanie jest dynamicznie powiązane: przeciążenie Console.WriteLine jest wybierane na podstawie typu czasu wykonywania argumentu. Dzieje się tak, ponieważ argument jest wyrażeniem dynamicznym — jego typ czasu kompilacji jest dynamiczny. W związku z tym czas powiązania trzeciego wywołania jest czasu wykonywania.

koniec przykładu

12.3.3 Powiązanie dynamiczne

Ten podpunkt jest informacyjny.

Powiązanie dynamiczne umożliwia programom języka C# interakcję z obiektami dynamicznymi, tj. obiektami, które nie są zgodne z normalnymi regułami systemu typów języka C#. Obiekty dynamiczne mogą być obiektami z innych języków programowania z różnymi systemami typów lub mogą być obiektami, które są konfigurowane programowo w celu zaimplementowania własnych semantyki powiązań dla różnych operacji.

Mechanizm, za pomocą którego obiekt dynamiczny implementuje własną semantykę, jest określany przez implementację. Określony interfejs — ponownie zdefiniowany przez implementację — jest implementowany przez obiekty dynamiczne, aby sygnalizować środowisku uruchomieniowemu C#, że mają specjalne znaczenie. Kiedy tylko operacje na obiekcie dynamicznym są dynamicznie powiązane, własna semantyka powiązań, zamiast tej z języka C#, jak określono w tej specyfikacji, przejmuje kontrolę.

Chociaż celem powiązania dynamicznego jest umożliwienie współdziałania z obiektami dynamicznymi, język C# umożliwia powiązanie dynamiczne na wszystkich obiektach, niezależnie od tego, czy są dynamiczne, czy nie. Pozwala to na bezproblemową integrację obiektów dynamicznych, ponieważ wyniki operacji na nich mogą nie być obiektami dynamicznymi, ale nadal są typu nieznanego programisty w czasie kompilacji. Ponadto powiązanie dynamiczne może pomóc wyeliminować kod oparty na odbiciu, który jest podatny na błędy, nawet wtedy, gdy żadne obiekty nie są obiektami dynamicznymi.

12.3.4 Typy podwyrażeń

Gdy operacja jest statycznie powiązana, typ wyrażenia podrzędnego (np. odbiornik i argument, indeks lub operand) jest zawsze uważany za typ czasu kompilacji tego wyrażenia.

Gdy operacja jest dynamicznie powiązana, typ podexpressionu jest określany na różne sposoby w zależności od typu czasu kompilacji podexpressionu:

  • Podwyrażenie dynamicznego typu w czasie kompilacji jest uważane za typ wartości, którą wyrażenie przyjmuje podczas wykonywania programu.
  • Podwyrażenie, którego typ w czasie kompilacji jest parametrem typu, jest uważane za typ, z którym parametr jest powiązany podczas wykonywania.
  • W przeciwnym razie podwyrażenie jest uznawane za typ czasu kompilacji.

12.4 Operatory

12.4.1 Ogólne

Wyrażenia są tworzone na podstawie operandów i operatorów. Operatory wyrażenia wskazują, które operacje mają być stosowane do operandów.

Przykład: przykłady operatorów obejmują +, -, *, /i new. Przykłady operandów obejmują literały, pola, zmienne lokalne i wyrażenia. koniec przykładu

Istnieją trzy rodzaje operatorów:

  • Operatory jednoargumentowe. Operatory jednoargumentowe przyjmują jeden operand i używają notacji prefiksowej (na przykład –x) lub notacji postfiksowej (na przykład x++).
  • Operatory binarne. Operatory binarne przyjmują dwa operandy i używają notacji infiksu (na przykład x + y).
  • Operator trójargumentowy. Istnieje tylko jeden operator trójargumentowy, ?:; przyjmuje trzy operandy i używa notacji infiksowej (c ? x : y).

Kolejność obliczania operatorów w wyrażeniu jest określana przez priorytet i asocjatywność operatorów (§12.4.2).

Operandy w wyrażeniu są obliczane od lewej do prawej.

Przykład: w F(i) + G(i++) * H(i)metoda F jest wywoływana przy użyciu starej wartości i, a następnie metoda G jest wywoływana ze starą wartością i, a na koniec metoda H jest wywoływana z nową wartością i. Jest to oddzielone od i niezwiązane z pierwszeństwem operatora. koniec przykładu

Niektóre operatory mogą być przeciążone. Przeciążenie operatora (§12.4.3) zezwala na określenie implementacji operatorów zdefiniowanych przez użytkownika dla operacji, w których jeden lub oba operandy są typu klasy lub struktury zdefiniowanej przez użytkownika.

12.4.2 Pierwszeństwo operatora i asocjatywność

Gdy wyrażenie zawiera wiele operatorów, pierwszeństwo operatorów kontroluje kolejność oceniania poszczególnych operatorów.

Uwaga: na przykład wyrażenie x + y * z jest oceniane jako x + (y * z), ponieważ operator * ma wyższy priorytet niż operator + binarny. uwaga końcowa

Pierwszeństwo operatora jest ustanawiane przez definicję jego skojarzonej produkcji gramatycznej.

Uwaga: na przykład additive_expression składa się z sekwencji multiplicative_expressionoddzielonych operatorami + lub -, co daje operatorom + i - niższe pierwszeństwo niż operatory *, /i %. uwaga końcowa

Uwaga: Poniższa tabela zawiera podsumowanie wszystkich operatorów według pierwszeństwa od najwyższego do najniższego:

podklasy Kategoria Operatory
§12.8 Podstawowy x.y x?.y f(x) a[x] a?[x] x++ x-- x! new typeof default checked unchecked delegate stackalloc
§12.9 Jednoargumentowy + - !x ~ ++x --x (T)x await x
§12.10 Multiplikatywny * / %
§12.10 Dodatek + -
§12.11 Zmiana << >>
§12.12 Testowanie relacyjne i typowe < > <= >= is as
§12.12 Równość == !=
§12.13 Logiczne AND &
§12.13 Logiczny XOR ^
§12.13 Logiczne OR \|
§12.14 Warunkowe I &&
§12.14 Warunkowe LUB \|\|
§12.15 i §12.16 Łączenie wartości null i wyrażenie rzutowania ?? throw x
§12.18 Warunkowy ?:
§12.21 i §12.19 Przypisanie i wyrażenie lambda = = ref *= /= %= += -= <<= >>= &= ^= \|= =>

uwaga końcowa

Gdy operand występuje między dwoma operatorami o tym samym priorytecie, asocjatywność operatorów kontroluje kolejność wykonywania operacji:

  • Z wyjątkiem operatorów przypisania i operatora łączenia wartości null wszystkie operatory binarne są lewej asocjacji, co oznacza, że operacje są wykonywane od lewej do prawej.

    Przykład: x + y + z oceniany jest jako (x + y) + z. koniec przykładu

  • Operatory przypisania, operator łączenia wartości null i operator warunkowy () są kojarzące, co oznacza, że operacje są wykonywane od prawej do lewej.

    Przykład: x = y = z oceniany jest jako x = (y = z). koniec przykładu

Pierwszeństwo i kojarzenie można kontrolować przy użyciu nawiasów.

Przykład: x + y * z najpierw mnoży y przez z, a następnie dodaje wynik do x, ale (x + y) * z najpierw dodaje x i y, a następnie mnoży wynik przez z. koniec przykładu

Przeciążenie operatora 12.4.3

Wszystkie operatory jednoargumentowe i binarne mają wstępnie zdefiniowane implementacje. Ponadto implementacje zdefiniowane przez użytkownika można wprowadzać za pomocą deklaracji operatorów (§15.10) w klasach i strukturach. Implementacje operatorów zdefiniowanych przez użytkownika zawsze mają pierwszeństwo przed wstępnie zdefiniowanymi implementacjami operatorów: tylko wtedy, gdy nie istnieją implementacje operatorów zdefiniowanych przez użytkownika, będą brane pod uwagę wstępnie zdefiniowane implementacje operatorów, zgodnie z opisem w §12.4.4 i §12.4.5.

operatory jednoargumentowe, które można przeciążać, to:

+ - ! (tylko negacja logiczna) ~ ++ -- true false

Uwaga: Chociaż true i false nie są używane jawnie w wyrażeniach (i dlatego nie są uwzględniane w tabeli pierwszeństwa w §12.4.2), są traktowane jako operatory, ponieważ są wywoływane w kilku kontekstach wyrażeń: Wyrażenia logiczne (§12.24) i wyrażenia obejmujące warunkowe (§12.18) i warunkowe operatory logiczne (§12.14). uwaga końcowa

Uwaga: operator ignorujący wartość null (postfiks !, §12.8.9) nie jest operatorem przeciążalnym. uwaga końcowa

Przeciążalne operatory binarne są następujące:

+  -  *  /  %  &  |  ^  <<  >>  ==  !=  >  <  <=  >=

Tylko wymienione powyżej operatory mogą być przeciążone. W szczególności nie można przeciążyć dostępu do składowych, wywołania metody ani operatorów =, &&, ||, ??, ?:, =>, checked, unchecked, new, typeof, default, asi is.

Gdy operator binarny jest przeciążony, odpowiedni operator przypisania złożonego, jeśli istnieje, jest również niejawnie przeciążony.

Przykład: przeciążenie operatora * jest również przeciążeniem operatora *=. Opisano to dalej w §12.21. koniec przykładu

Nie można przeciążyć samego operatora przypisania (=). Przypisanie zawsze polega na prostym zapisaniu wartości do zmiennej (§12.21.2).

Operacje rzutu, takie jak (T)x, są przeciążone przez zapewnienie konwersji zdefiniowanych przez użytkownika (§10.5).

Uwaga: konwersje zdefiniowane przez użytkownika nie mają wpływu na zachowanie operatorów is ani as. uwaga końcowa

Dostęp do elementów, takich jak a[x], nie jest uważany za przeciążalny operator. Zamiast tego indeksowanie zdefiniowane przez użytkownika jest obsługiwane za pośrednictwem indeksatorów (§15.9).

W wyrażeniach operatory są przywoływane przy użyciu notacji operatorów, a w deklaracjach operatory są przywoływane przy użyciu notacji funkcjonalnej. W poniższej tabeli przedstawiono zależność między operatorami a notacjami funkcyjnymi dla operatorów jednoargumentowych i binarnych. W pierwszym wpisie «op» oznacza dowolny przeciążalny operator prefiksowy unarny. W drugim wpisie "op" oznacza jednoargumentowe postfiksowe operatory ++ i --. W trzecim wpisie "op" oznacza dowolnego przeciążonego operatora binarnego.

Uwaga: na przykład przeciążenia operatorów ++ i -- zobacz §15.10.2. uwaga końcowa

Notacja Operatora notacja funkcjonalna
«op» x operator «op»(x)
x «op» operator «op»(x)
x «op» y operator «op»(x, y)

Deklaracje operatorów zdefiniowane przez użytkownika zawsze wymagają co najmniej jednego z parametrów klasy lub typu struktury zawierającego deklarację operatora.

Uwaga: w związku z tym nie jest możliwe, aby operator zdefiniowany przez użytkownika miał taki sam podpis jak wstępnie zdefiniowany operator. uwaga końcowa

Deklaracje operatorów zdefiniowane przez użytkownika nie mogą modyfikować składni, pierwszeństwa ani kojarzenia operatora.

Przykład: operator / jest zawsze operatorem binarnym, zawsze ma poziom pierwszeństwa określony w §12.4.2i zawsze jest w lewo asocjacyjny. koniec przykładu

Uwaga: Chociaż operator zdefiniowany przez użytkownika może wykonać dowolne obliczenia, implementacje generujące wyniki inne niż oczekiwane intuicyjnie, są zdecydowanie odradzane. Na przykład implementacja operatora == powinna porównać dwa operandy pod kątem równości i zwrócić odpowiedni wynik bool. uwaga końcowa

Opisy poszczególnych operatorów w §12.9 do §12.21 określają wstępnie zdefiniowane implementacje operatorów i wszelkie dodatkowe reguły, które mają zastosowanie do każdego operatora. Opisy korzystają z terminów rozwiązywania przeciążeń operatora jednoargumentowego, rozwiązywania przeciążeń operatorów binarnych, promocji numerycznejoraz definicji operatorów podniesionych, których definicje znajdują się w poniższych podklauzulach.

12.4.4 Rozpoznawanie przeciążenia operatora unarnego

Operacja postaci «op» x lub x «op», gdzie «op» jest przeciążalnym operatorem jednoargumentowym, a x jest wyrażeniem typu X, jest przetwarzana w następujący sposób:

  • Zestaw operatorów zdefiniowanych przez użytkownika udostępnianych przez X dla operacji operator «op»(x) jest określany przy użyciu reguł §12.4.6.
  • Jeśli zestaw kandydatów operatorów zdefiniowanych przez użytkownika nie jest pusty, staje się to zestawem operatorów kandydatów dla operacji. W przeciwnym razie wstępnie zdefiniowane implementacje binarne operator «op», w tym ich podniesione formy, stają się zestawem operatorów kandydatowych dla operacji. Wstępnie zdefiniowane implementacje danego operatora są określone w opisie operatora. Wstępnie zdefiniowane operatory udostępniane przez typ wyliczenia lub delegata są uwzględniane tylko w tym zestawie, gdy typ czasu powiązania — lub typ bazowy, jeśli jest to typ dopuszczalny wartości null — jednego z operandów jest typem wyliczenia lub delegata.
  • Reguły rozpoznawania przeciążeń §12.6.4 są stosowane do zestawu operatorów kandydatów w celu wybrania najlepszego operatora w odniesieniu do listy argumentów (x), a ten operator staje się wynikiem procesu rozpoznawania przeciążenia. Jeśli rozpoznawanie przeciążenia nie wybierze jednego najlepszego operatora, wystąpi błąd czasu powiązania.

12.4.5 Rozpoznawanie przeciążenia operatora binarnego

Operacja formularza x «op» y, gdzie «op» jest przeciążalnym operatorem binarnym, x jest wyrażeniem typu X, a y jest wyrażeniem typu Y, jest przetwarzane w następujący sposób:

  • Określa się zestaw kandydatów na operatorów zdefiniowanych przez użytkownika udostępnianych przez X i Y dla operacji operator «op»(x, y). Zestaw składa się z unii operatorów kandydatów dostarczonych przez X i operatorów kandydatów dostarczonych przez Y, z których każdy jest określany przy użyciu zasad §12.4.6. W przypadku połączonego zestawu kandydaci są scalani w następujący sposób:
    • Jeśli X i Y są wzajemnie konwertowalne jako tożsamości lub jeśli X i Y pochodzą ze wspólnego typu podstawowego, to współdzielone operatory kandydatów występują tylko raz w zestawie połączonym.
    • Jeśli istnieje konwersja tożsamości między X a Y, operator «op»Y dostarczony przez Y ma taki sam typ zwracany jak «op»X dostarczony przez X, a typy operandów «op»Y mają konwersję tożsamości na odpowiadające typy operandów «op»X wtedy tylko «op»X występuje w zestawie.
  • Jeśli zestaw kandydatów operatorów zdefiniowanych przez użytkownika nie jest pusty, staje się to zestawem operatorów kandydatów dla operacji. W przeciwnym razie wstępnie zdefiniowane implementacje binarne operator «op», w tym ich podniesione formy, stają się zestawem operatorów kandydatowych dla operacji. Wstępnie zdefiniowane implementacje danego operatora są określone w opisie operatora. Dla określonych z góry operatorów enum i delegatów uwzględnia się wyłącznie operatory dostarczane przez typ wyliczenia lub delegata, który jest typem powiązanym w czasie jednej z operandów.
  • Reguły rozpoznawania przeciążeń §12.6.4 są stosowane do zestawu operatorów kandydatów w celu wybrania najlepszego operatora w odniesieniu do listy argumentów (x, y), a ten operator staje się wynikiem procesu rozpoznawania przeciążenia. Jeśli rozpoznawanie przeciążenia nie wybierze jednego najlepszego operatora, wystąpi błąd czasu powiązania.

12.4.6 Operatory zdefiniowane przez użytkownika

Biorąc pod uwagę typ T i operację operator «op»(A), gdzie «op» jest operatorem przeciążalnym, a A jest listą argumentów, zestaw operatorów zdefiniowanych przez użytkownika dostarczonych przez T dla operatora «op»(A) jest określany w następujący sposób:

  • Określ typ T₀. Jeśli T jest typem wartości dopuszczanej do wartości null, T₀ jest jego typem bazowym; w przeciwnym razie T₀ jest równa T.
  • Dla wszystkich deklaracji operator «op» w T₀ i wszystkich podniesionych form takich operatorów, jeśli co najmniej jeden operator jest możliwy do zastosowania (§12.6.4.2) w odniesieniu do listy argumentów A, to zestaw kandydatów na operatorów składa się ze wszystkich takich stosownych operatorów w T₀.
  • W przeciwnym razie, jeśli T₀ jest object, zestaw operatorów do wyboru jest pusty.
  • W przeciwnym razie zestaw operatorów kandydatów udostępnianych przez T₀ jest zestawem operatorów kandydatów dostarczonych przez bezpośrednią klasę bazową T₀lub efektywną klasę bazową T₀, jeśli T₀ jest parametrem typu.

12.4.7 Promocje liczbowe

12.4.7.1 Ogólne

Ten podpunkt jest informacyjny.

§12.4.7 i jego podpunkty są podsumowaniem połączonego efektu:

Podwyższenie poziomu liczbowego polega na automatycznym wykonywaniu pewnych niejawnych konwersji operandów wstępnie zdefiniowanych operatorów jednoargumentowych i binarnych liczbowych. Promocja numeryczna nie jest odrębnym mechanizmem, ale raczej efektem zastosowania rozwiązywania przeciążenia do wstępnie zdefiniowanych operatorów. Promocja liczbowa nie wpływa bezpośrednio na ocenę operatorów zdefiniowanych przez użytkownika, chociaż operatory te można zaimplementować tak, aby wykazywały podobne efekty.

Jako przykład podwyższania poziomu liczbowego należy wziąć pod uwagę predefiniowane implementacje binarnego operatora *:

int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);

Gdy reguły rozpoznawania przeciążeń (§12.6.4) są stosowane do tego zestawu operatorów, efekt polega na wybraniu pierwszego z operatorów, dla których istnieją niejawne konwersje z typów operandów.

Przykład: w przypadku operacji b * s, gdzie b jest byte, a s jest short, rozwiązywanie przeciążenia wybiera operator *(int, int) jako najlepszy operator. W związku z tym efekt polega na tym, że b i s są konwertowane na int, a typ wyniku jest int. Podobnie w przypadku operacji i * d, gdzie i jest int, a d jest double, overload decyzja wybiera operator *(double, double) na najlepszego operatora. koniec przykładu

Koniec tekstu informacyjnego.

12.4.7.2 Jednoargumentowe promocje liczbowe

Ten podpunkt jest informacyjny.

Jednoargumentowa promocja liczbowa występuje dla operandów wstępnie zdefiniowanych operatorów +, -i ~ jednoargumentowych. Promocja jednoargumentowa polega po prostu na konwertowaniu operandów z typów sbyte, byte, short, ushortlub char na typ int. Ponadto dla operatora jednoargumentowego - jednoargumentowa promocja liczbowa przekształca operandy typu uint na typ long.

Koniec tekstu informacyjnego.

12.4.7.3 Promocje liczbowe binarne

Ten podpunkt jest informacyjny.

Podwyższanie liczby binarnej występuje dla operandów wstępnie zdefiniowanych +, -, *, /, %, &, |, ^, ==, !=, >, <, >=i <= operatorów binarnych. Promocja numeryczna binarna niejawnie konwertuje oba operandy na wspólny typ, który w przypadku operatorów nierelacyjnych staje się również typem wyniku operacji. Promocja liczbowa binarna składa się z zastosowania następujących reguł w kolejności, w której są one wyświetlane tutaj:

  • Jeśli którykolwiek z operandów ma typ decimal, drugi operand jest konwertowany na typ decimal, lub wystąpi błąd czasu powiązania, jeśli drugi operand ma typ float lub double.
  • W przeciwnym razie jeśli którykolwiek operand ma typ double, drugi operand jest konwertowany na typ double.
  • W przeciwnym razie jeśli którykolwiek operand ma typ float, drugi operand jest konwertowany na typ float.
  • W przeciwnym razie, jeśli jeden z operandów ma typ ulong, drugi operand jest konwertowany na typ ulong, lub następuje błąd czasu powiązania, jeśli inny operand jest typu type sbyte, short, intlub long.
  • W przeciwnym razie jeśli którykolwiek operand ma typ long, drugi operand jest konwertowany na typ long.
  • W przeciwnym razie, jeśli operand ma typ uint, a drugi operand ma typ sbyte, shortlub int, oba operandy są konwertowane na typ long.
  • W przeciwnym razie jeśli którykolwiek operand ma typ uint, drugi operand jest konwertowany na typ uint.
  • W przeciwnym razie oba operandy są konwertowane na typ int.

Uwaga: pierwsza reguła nie zezwala na wszystkie operacje, które mieszają typ decimal z typami double i float. Reguła wynika z faktu, że nie ma niejawnych konwersji między typem decimal a typami double i float. uwaga końcowa

Uwaga: Należy również zauważyć, że operand nie może być typu ulong, gdy drugi operand jest oznaczonego typu całkowitego. Przyczyną jest to, że żaden typ całkowity nie istnieje, który może reprezentować pełny zakres ulong, a także podpisane typy całkowite. uwaga końcowa

W obu powyższych przypadkach wyrażenie rzutowania może służyć do jawnego przekonwertowania jednego operandu na typ zgodny z drugim operatorem.

Przykład: w poniższym kodzie

decimal AddPercent(decimal x, double percent) =>
    x * (1.0 + percent / 100.0);

Występuje błąd czasu powiązania, ponieważ nie można pomnożyć decimal przez double. Błąd został rozwiązany przez jawne przekonwertowanie drugiego operandu na decimalw następujący sposób:

decimal AddPercent(decimal x, double percent) =>
    x * (decimal)(1.0 + percent / 100.0);

koniec przykładu

Koniec tekstu informacyjnego.

12.4.8 Operatory zniesione

Operatory uniesione umożliwiają używanie wstępnie zdefiniowanych i zdefiniowanych przez użytkownika operatorów, które działają na typach wartości niebędących nullem, z formami, które dopuszczają null dla tych typów. Operatory podniesione są tworzone na podstawie wstępnie zdefiniowanych i zdefiniowanych przez użytkownika operatorów spełniających określone wymagania, jak opisano poniżej.

  • W przypadku operatorów jednoargumentowych +, ++, -, --, !(negacja logiczna) i ~, istnieje zniesiona forma operatora, jeśli operand i typy wyników są typami wartości niepustych. Podniesiona forma jest konstruowana przez dodanie pojedynczego modyfikatora ? do typów operandów i wyników. Operator zniesiony generuje wartość null, jeśli operand ma wartość null. W przeciwnym razie operator lifted rozpakowuje operand, stosuje operatora podstawowego i zawija wynik.
  • W przypadku operatorów binarnych +, -, *, /, %, &, |, ^, <<i >>, istnieje zniesiona forma operatora, jeśli argument operand i typy wyników są typami wartości niepustych. Podniesiona forma jest tworzona przez dodanie pojedynczego modyfikatora ? do każdego operandu i typu wyniku. Operator podniesiony generuje wartość null, jeśli jeden lub oba operandy są null (z wyjątkiem operatorów & oraz | typu bool?, jak opisano w §12.13.5). W przeciwnym razie operator podniesiony rozdziela operandy, stosuje operator bazowy i opakowuje wynik.
  • W przypadku operatorów równości == i !=istnieje zniesiona forma operatora, jeśli typy operandów są typami wartości niepustych i jeśli typ wyniku jest bool. Podniesiona forma jest konstruowana przez dodanie pojedynczego modyfikatora ? do każdego typu operandu. Podniesiony operator traktuje dwie wartości null jako równe, a wartość null jako nierówną żadnej wartości innej niżnull. Jeśli oba operandy nie sąnull, podniesiony operator odpakowuje operandy i stosuje operator bazowy, aby wygenerować wynik bool.
  • W przypadku operatorów relacyjnych <, >, <=i >=istnieje zniesiona forma operatora, jeśli typy operandów są typami wartości niepustych, a jeśli typ wyniku jest bool. Podniesiona forma jest konstruowana przez dodanie pojedynczego modyfikatora ? do każdego typu operandu. Operator podniesiony generuje wartość false, jeśli jeden lub oba operandy są null. W przeciwnym razie operator podniesiony (lifted) rozpakowuje operandy i stosuje operator bazowy, aby wygenerować wynik bool.

12.5 Wyszukiwanie członków

12.5.1 Ogólne

Wyszukiwanie członka to proces, w którym określa się znaczenie nazwy w kontekście typu. Wyszukiwanie członka może nastąpić podczas ewaluacji simple_name (§12.8.4) lub member_access (§12.8.7) w wyrażeniu. Jeśli simple_name lub member_access występuje jako primary_expressioninvocation_expression (§12.8.10.2), mówi się, że członek jest wywoływany.

Jeśli członek jest metodą lub zdarzeniem albo jest stałym, polem lub właściwością typu delegata (§20) lub typu dynamic (§8.2.4), określa się go jako wywoływalny.

Wyszukiwanie elementu członkowskiego uwzględnia nie tylko nazwę elementu członkowskiego, ale także liczbę parametrów typu, które ma element członkowski i czy jest dostępny. Na potrzeby wyszukiwania elementów członkowskich, metody generyczne i zagnieżdżone typy generyczne mają liczbę parametrów typów wskazanych w swoich deklaracjach, podczas gdy wszystkie pozostałe elementy członkowskie mają zerową liczbę parametrów typów.

Wyszukiwanie elementu członkowskiego nazwy N z argumentami typu K w typie T jest przetwarzane w następujący sposób:

  • Najpierw określa się zbiór dostępnych członków nazwany N:
    • Jeśli T jest parametrem typu, zestaw jest związkiem zestawów dostępnych elementów członkowskich o nazwie N w każdym z typów określonych jako ograniczenie podstawowe lub ograniczenie pomocnicze (§15.2.5) dla T, wraz z zestawem dostępnych elementów członkowskich o nazwie N w object.
    • W przeciwnym razie zestaw składa się ze wszystkich dostępnych elementów (§7.5) nazwanych N w T, w tym dziedziczonych elementów i dostępnych elementów o nazwie N w object. Jeśli T jest typem skonstruowanym, zestaw elementów członkowskich jest uzyskiwany przez podstawianie argumentów typu zgodnie z opisem w §15.3.3. Członkowie zawierający modyfikator override zostają wykluczeni z zestawu.
  • Następnie, jeśli K ma wartość zero, wszystkie zagnieżdżone typy, których deklaracje zawierają parametry typu, zostaną usunięte. Jeśli K nie jest równa zero, wszyscy członkowie z inną liczbą parametrów typu zostaną usunięci. Jeśli K jest zero, metody o parametrach typu nie są usuwane, ponieważ proces wnioskowania typu (§12.6.3) może być w stanie wywnioskować argumenty typu.
  • Następnie, jeśli członek jest wywoływany, wszystkie niewywołalne członki zostaną usunięte z kolekcji.
  • Następnie członkowie ukrywani przez innych członków są usuwani z zestawu. Dla każdego członka S.M w zestawie, gdzie S jest typem, w którym zadeklarowano członka M, stosowane są następujące reguły:
    • Jeśli M jest stałym, polem, właściwością, zdarzeniem lub elementem członkowskim wyliczania, wszystkie elementy członkowskie zadeklarowane w podstawowym typie S zostaną usunięte z zestawu.
    • Jeśli M jest deklaracją typu, wszystkie typy inne niż zadeklarowane w podstawowym typie S zostaną usunięte z zestawu, a wszystkie deklaracje typów o tej samej liczbie parametrów typu co M zadeklarowane w podstawowym typie S zostaną usunięte z zestawu.
    • Jeśli M jest metodą, to wszystkie składniki, które nie są metodami i są zadeklarowane w typie bazowym związanym z S, zostaną usunięte z zestawu.
  • Następnie składowe interfejsu ukryte przez składowe klasy są usuwane z zestawu. Ten krok ma wpływ tylko wtedy, gdy T jest parametrem typu, a T ma zarówno efektywną klasę bazową inną niż object, jak i niepusty zestaw efektywnych interfejsów (§15.2.5). Dla każdego elementu S.M w zbiorze, gdzie S to typ, w którym element M jest zadeklarowany, stosuje się każdą regułę, jeśli S stanowi deklarację klasy, inną niż object:
    • Jeśli M jest stałą, polem, właściwością, zdarzeniem, elementem członkowskim wyliczenia lub deklaracją typu, wszystkie elementy członkowskie zadeklarowane w deklaracji interfejsu zostaną usunięte z zestawu.
    • Jeśli M jest metodą, wszystkie elementy nienależące do metody zadeklarowane w deklaracji interfejsu zostaną usunięte z zestawu, a wszystkie metody z tym samym podpisem co M zadeklarowane w deklaracji interfejsu zostaną usunięte z zestawu.
  • Na koniec, po usunięciu ukrytych członków, określany jest wynik wyszukiwania:
    • Jeśli zestaw składa się z jednego elementu członkowskiego, który nie jest metodą, ten element członkowski jest wynikiem wyszukiwania.
    • W przeciwnym razie, jeśli zestaw zawiera tylko metody, ta grupa metod jest wynikiem wyszukiwania.
    • W przeciwnym razie wyszukiwanie jest niejednoznaczne i występuje błąd czasu powiązania.

W przypadku wyszukiwania członków w typach innych niż parametry typów i interfejsy oraz wyszukiwania członków w interfejsach, które są ściśle jednokrotnego dziedziczenia (każdy interfejs w łańcuchu dziedziczenia ma dokładnie zero lub jeden bezpośredni interfejs bazowy), efektem reguł wyszukiwania jest po prostu to, że członkowie pochodni ukrywają członków bazowych o tej samej nazwie lub podpisie. Takie wyszukiwania pojedynczego dziedziczenia nigdy nie są niejednoznaczne. Niejednoznaczności, które mogą wynikać z odwołań do członków w interfejsach wielokrotnego dziedziczenia, są opisane w §18.4.6.

Uwaga: ta faza stanowi tylko jeden rodzaj niejednoznaczności. Jeśli rezultatem wyszukiwania składowego jest grupa metod, dalsze użycia tej grupy metod mogą zakończyć się niepowodzeniem z powodu niejednoznaczności, na przykład opisanym w §12.6.4.1 i §12.6.6.2. uwaga końcowa

12.5.2 Typy podstawowe

Do celów wyszukiwania członków, typ T jest uważany za mający następujące typy bazowe.

  • Jeśli T jest object lub dynamic, T nie ma typu podstawowego.
  • Jeśli T jest enum_type, podstawowymi typami T są typy klas System.Enum, System.ValueTypei object.
  • Jeśli T jest struct_type, to podstawowymi typami T są te typy klas System.ValueType i object.

    Uwaga: nullable_value_type jest struct_type (§8.3.1). uwaga końcowa

  • Jeśli T jest class_type, podstawowe typy T są klasami podstawowymi T, w tym typ klasy object.
  • Jeśli T jest interface_type, to podstawowe typy T są podstawowymi interfejsami T oraz klasowym typem object.
  • Jeśli T jest typu tablicy , to podstawowe typy T są typami klas System.Array i object.
  • Jeśli T jest typem delegata , podstawowe typy dla T to typy klas System.Delegate i object.

Elementy funkcji

12.6.1 Ogólne

Członkowie funkcji to elementy zawierające instrukcje wykonywalne. Składowe funkcji są zawsze członkami typów i nie mogą być członkami przestrzeni nazw. Język C# definiuje następujące kategorie elementów członkowskich funkcji:

  • Metody
  • Właściwości
  • Zdarzenia
  • Indeksujący
  • Operatory zdefiniowane przez użytkownika
  • Konstruktory wystąpień
  • Konstruktory statyczne
  • Finalizatory

Z wyjątkiem finalizatorów i konstruktorów statycznych (których nie można wywołać jawnie), instrukcje zawarte w elementach członkowskich funkcji są wykonywane za pośrednictwem wywołań składowych funkcji. Faktyczna składnia wywołania członka funkcji zależy od konkretnej kategorii członka funkcji.

Lista argumentów (§12.6.2) wywołania składowej funkcji zawiera rzeczywiste wartości lub odwołania do zmiennych parametrów elementu członkowskiego funkcji.

Wywołania metod ogólnych mogą używać wnioskowania typu w celu określenia zestawu argumentów typu, które mają zostać przekazane do metody. Ten proces jest opisany w §12.6.3.

Wywołania metod, indeksatorów, operatorów i konstruktorów instancji wykorzystują rozpoznawanie przeciążeń w celu określenia, który z członków funkcji z zestawu kandydatów należy wywołać. Ten proces jest opisany w §12.6.4.

Po zidentyfikowaniu określonego członka funkcji w trakcie wiązania, możliwe poprzez rozpoznanie przeciążenia, rzeczywisty proces wywoływania członka funkcji jest opisany w §12.6.6.

Uwaga: Poniższa tabela zawiera podsumowanie przetwarzania, które odbywa się w konstrukcjach obejmujących sześć kategorii składowych funkcji, które można jawnie wywołać. W tabeli e, x, yi value wskazują wyrażenia sklasyfikowane jako zmienne lub wartości, T wskazuje wyrażenie sklasyfikowane jako typ, F jest prostą nazwą metody, a P jest prostą nazwą właściwości.

Konstruować Przykład Opis
Wywołanie metody F(x, y) Rozpoznawanie przeciążenia jest stosowane, aby wybrać najlepszą metodę F w zawierającej klasie lub strukturze. Metoda jest wywoływana z listą argumentów (x, y). Jeśli metoda nie jest static, wyrażenie wystąpienia jest this.
T.F(x, y) Rozpoznawanie przeciążenia jest stosowane w celu wybrania najlepszej metody F w klasie lub struktury T. Błąd czasu wiązania występuje, jeśli metoda nie jest static. Metoda jest wywoływana z listą argumentów (x, y).
e.F(x, y) Rozpoznawanie przeciążenia jest stosowane w celu wybrania najlepszej metody F w klasie, strukturze lub interfejsie określonym przez typ e. Błąd związany z czasem wiązania występuje, jeśli metoda jest static. Metoda jest wywoływana z użyciem wyrażenia instancji e oraz listy argumentów (x, y).
Dostęp do właściwości P Wywołany jest akcesor pobierający właściwości P w zawierającej klasie lub strukturze. Błąd czasu kompilacji występuje, jeśli P jest tylko do zapisu. Jeśli P nie jest static, wyrażenie wystąpienia jest this.
P = value Akcesor ustawiający właściwości P w zawierającej klasie lub strukturze zostaje wywołany z listą argumentów (value). Błąd czasu kompilacji występuje, jeśli P jest ustawione jako tylko do odczytu. Jeśli P nie jest static, wyrażenie wystąpienia jest this.
T.P Wywoływany jest akcesor get właściwości P w klasie lub strukturze T. Błąd występuje w czasie kompilacji, jeśli P nie jest static lub jeśli P jest tylko do zapisu.
T.P = value Akcesor ustawiający właściwości P w klasie T lub strukturze jest wywoływany z listą argumentów (value). Błąd czasu kompilacji występuje, jeśli P nie jest static lub jeśli P jest ustawione jako tylko do odczytu.
e.P Akcesor get właściwości P w klasie, strukturze lub interfejsie danej przez typ E jest wywoływany przy użyciu wyrażenia instancyjnego e. Błąd czasu powiązania występuje, jeśli P jest static lub jeśli P jest tylko do zapisu.
e.P = value Akcesor set właściwości P w klasie, strukturze lub interfejsie określonym przez typ E zostaje wywołany z użyciem wyrażenia instancji e i listy argumentów (value). Błąd czasu powiązania występuje, jeśli P jest static lub jeśli P jest tylko do odczytu.
Dostęp do zdarzeń E += value Wywoływany jest akcesor dodawania zdarzenia E w klasie lub strukturze zawierającej. Jeśli E nie jest static, wyrażenie wystąpienia jest this.
E -= value Wywołano akcesor usuwania zdarzenia E w klasie lub strukturze zawierającej. Jeśli E nie jest static, wyrażenie wystąpienia jest this.
T.E += value Wywoływany jest akcesor dodający zdarzenia E w klasie T lub strukturze. Błąd czasu powiązania występuje, jeśli E nie jest static.
T.E -= value Wywołano mutator usuwający zdarzenia E w klasie T lub strukturze. Błąd czasu powiązania występuje, jeśli E nie jest static.
e.E += value Akcesor dodawania zdarzenia E w klasie, strukturze lub interfejsie określonym przez typ E jest wywoływane z wyrażeniem wystąpienia e. Błąd czasu wiązania występuje, jeśli E jest static.
e.E -= value Akcesor usuwania zdarzenia E w klasie, strukturze lub interfejsie danego typu E jest wywoływany z użyciem wyrażenia instancji e. Błąd czasu wiązania występuje, jeśli E jest static.
Dostęp indeksatora e[x, y] Rozpoznawanie przeciążenia jest stosowane w celu wybrania najlepszego indeksatora w klasie, strukturze lub interfejsie określonym przez typ e. Akcesor pobierający indeksatora jest wywoływany za pomocą wyrażenia wystąpienia e i listy argumentów (x, y). Błąd czasu powiązania występuje, jeśli indeksator jest przeznaczony wyłącznie do zapisu.
e[x, y] = value Rozpoznawanie przeciążenia jest stosowane w celu wybrania najlepszego indeksatora w klasie, strukturze lub interfejsie określonym przez typ e. Akcesor ustawiający indeksatora jest wywoływany z wyrażeniem instancji e i listą argumentów (x, y, value). Błąd na etapie wiązania występuje, jeśli indeksator jest tylko do odczytu.
Wywołanie operatora -x Rozdzielczość przeciążenia jest stosowana w celu wybrania najlepszego operatora jednoargumentowego w klasie lub struktury podanej przez typ x. Wybrany operator jest wywoływany z listą argumentów (x).
x + y Rozpoznawanie przeciążeń jest stosowane w celu wybrania najlepszego operatora binarnego w klasach lub strukturach podanych przez typy x i y. Wybrany operator jest wywoływany z listą argumentów (x, y).
Wywołanie konstruktora wystąpienia new T(x, y) Rozpoznawanie przeciążenia jest stosowane w celu wybrania najlepszego konstruktora instancji w klasie lub strukturze T. Konstruktor instancji jest wywoływany z listą argumentów (x, y).

uwaga końcowa

12.6.2 Listy argumentów

12.6.2.1 Ogólne

Każda składowa funkcji i wywołanie delegata zawierają listę argumentów, która dostarcza rzeczywiste wartości lub odwołania do zmiennych dla parametrów elementu funkcji. Składnia określająca listę argumentów wywołania elementu członkowskiego funkcji zależy od kategorii składowej funkcji:

  • Na przykład w konstruktorach, metodach, indeksatorach i delegatach argumenty są określane jako argument_list, jak opisano poniżej. W przypadku indeksatorów, podczas wywoływania akcesora set, lista argumentów dodatkowo zawiera wyrażenie określone jako operand po prawej stronie operatora przypisania.

    Uwaga: Ten dodatkowy argument nie jest używany do rozstrzygania przeciążenia, a jedynie podczas wywołania akcesora set. uwaga końcowa

  • W przypadku właściwości lista argumentów jest pusta przy wywoływaniu akcesora get i składa się z wyrażenia określonego jako prawy operand operatora przypisania podczas wywoływania akcesora set.
  • W przypadku zdarzeń lista argumentów składa się z wyrażenia określonego jako prawy operand operatora += lub -=.
  • W przypadku operatorów zdefiniowanych przez użytkownika lista argumentów składa się z pojedynczego operandu operatora jednoargumentowego lub dwóch operandów operatora binarnego.

Argumenty właściwości (§15.7) i zdarzenia (§15.8) są zawsze przekazywane jako parametry wartości (§15.6.2.2). Argumenty operatorów zdefiniowanych przez użytkownika (§15.10) są zawsze przekazywane jako parametry wartości (§15.6.2.2) lub parametry wejściowe (§9.2.8). Argumenty indeksatorów (§15.9) są zawsze przekazywane jako parametry wartości (§15.6.2.2), parametry wejściowe (§9.2.8) lub tablice parametrów (§15.6.2.4). Parametry wyjściowe i referencyjne nie są obsługiwane dla tych kategorii członków funkcji.

Argumenty konstruktora wystąpienia, metody, indeksatora lub wywołania delegata są określane w postaci argument_list:

argument_list
    : argument (',' argument)*
    ;

argument
    : argument_name? argument_value
    ;

argument_name
    : identifier ':'
    ;

argument_value
    : expression
    | 'in' variable_reference
    | 'ref' variable_reference
    | 'out' variable_reference
    ;

argument_list składa się z co najmniej jednego argumentus oddzielonego przecinkami. Każdy argument składa się z opcjonalnego argument_name, po którym następuje argument_value. Argument z nazwą argument_name jest nazywany argumentem nazwanym , natomiast argument bez nazwy argument_name jest argumentem pozycyjnym .

argument_value może przyjąć jedną z następujących form:

  • Wyrażenie , wskazujące, że argument jest przekazywany jako parametr wartości lub jest przekształcany w parametr wejściowy, a następnie przekazywany jako taki, co określone jest w (§12.6.4.2 ) i opisane w §12.6.2.3.
  • Słowo kluczowe in, po którym następuje odwołanie_do_zmiennej (§9.5), wskazujące, że argument jest podawany jako parametr wejściowy (§15.6.2.3.2). Zmienna musi być zdecydowanie przypisana (§9.4), zanim będzie można ją przekazać jako parametr wejściowy.
  • Słowo kluczowe ref następnie variable_reference (§9.5), wskazując, że argument jest przekazywany jako parametr referencyjny (§15.6.2.3.3). Zmienna musi być zdecydowanie przypisana (§9.4), zanim będzie można ją przekazać jako parametr referencyjny.
  • Słowo kluczowe out wraz z odwołaniem do zmiennej (§9.5), wskazujące, że argument jest przekazywany jako parametr wyjściowy (§15.6.2.3.4). Zmienna jest uznawana za zdecydowanie przypisaną (§9.4) po wywołaniu elementu członkowskiego funkcji, w którym zmienna jest przekazywana jako parametr wyjściowy.

Forma określa tryb przekazywania parametrów argumentu: wartość, wejście, odwołanielub wyjście, odpowiednio. Jednak jak wspomniano powyżej, argument z trybem przekazywania wartości może zostać przekształcony w jeden z trybem przekazywania danych wejściowych.

Przekazywanie pola nietrwałego (§15.5.4) jako parametr wejściowy, wyjściowy lub referencyjny powoduje ostrzeżenie, ponieważ pole może nie być traktowane jako nietrwałe przez wywołaną metodę.

12.6.2.2 Odpowiednie parametry

Dla każdego argumentu na liście argumentów musi istnieć odpowiadający parametr w wywoływanym członku funkcji lub delegacie.

Lista parametrów używana poniżej jest określona następująco:

  • W przypadku metod wirtualnych i indeksatorów zdefiniowanych w klasach lista parametrów jest wybierana z pierwszej deklaracji lub zastępowania elementu członkowskiego funkcji znalezionego podczas rozpoczynania od statycznego typu odbiornika i przeszukiwania jego klas bazowych.
  • W przypadku metod częściowych używana jest lista parametrów definiującej deklarację metody częściowej.
  • Dla wszystkich pozostałych elementów funkcyjnych oraz delegatów istnieje tylko jedna lista parametrów, która jest stosowana.

Pozycja argumentu lub parametru jest definiowana jako liczba argumentów lub parametrów poprzedzających go na liście argumentów lub na liście parametrów.

Odpowiednie parametry argumentów składowych funkcji są ustanawiane w następujący sposób:

  • Argumenty w argument_list konstruktorów instancji, metod, indeksatorów i delegatów:
    • Argument pozycyjny, w którym parametr występuje na tej samej pozycji na liście parametrów, odpowiada tego parametru, chyba że parametr jest tablicą parametrów, a element członkowski funkcji jest wywoływany w postaci rozwiniętej.
    • Argument pozycyjny elementu członkowskiego funkcji z tablicą parametrów wywoływaną w rozszerzonej formie, która występuje w lub po pozycji tablicy parametrów na liście parametrów, odpowiada elementowi w tablicy parametrów.
    • Nazwany argument odpowiada parametrowi o tej samej nazwie na liście parametrów.
    • W przypadku indeksatorów, podczas wywoływania akcesora set, wyrażenie określone jako prawy operand operatora przypisania odpowiada niejawnemu parametrowi value deklaracji akcesora set.
  • W przypadku właściwości podczas wywoływania akcesora 'get' nie ma żadnych argumentów. Podczas wywoływania akcesora ustawiającego wyrażenie określone jako prawy operand operatora przypisania odpowiada niejawnie przekazywanemu parametrowi wartości w deklaracji akcesora ustawiającego.
  • W przypadku operatorów jednoargumentowych zdefiniowanych przez użytkownika (w tym konwersji) pojedynczy operand odpowiada pojedynczemu parametrowi deklaracji operatora.
  • W przypadku operatorów binarnych zdefiniowanych przez użytkownika lewy operand odpowiada pierwszemu parametrowi, a prawy operand odpowiada drugiemu parametrowi deklaracji operatora.
  • Argument nienazwany nie odpowiada żadnemu parametrowi, gdy pojawia się po nazwanym argumencie, który jest poza swoją pozycją, lub po nazwanym argumencie, który odpowiada tablicy parametrów.

    Uwaga: uniemożliwia to wywoływanie void M(bool a = true, bool b = true, bool c = true); przez M(c: false, valueB);. Pierwszy argument jest używany poza pozycją (argument jest używany w pierwszej pozycji, ale parametr o nazwie c znajduje się na trzeciej pozycji), więc należy nazwać następujące argumenty. Innymi słowy, argumenty nazwane inne niż końcowe są dozwolone tylko wtedy, gdy nazwa i pozycja powodują znalezienie tego samego odpowiedniego parametru. uwaga końcowa

12.6.2.3 Ocena czasu wykonywania list argumentów

Podczas przetwarzania w czasie wykonywania wywołania elementu członkowskiego funkcji (§12.6.6), wyrażenia lub odwołania zmiennych listy argumentów są oceniane w kolejności od lewej do prawej w następujący sposób:

  • W przypadku argumentu typu wartość, jeśli tryb przekazywania parametru to wartość

    • wyrażenie argumentu jest obliczane i wykonywana jest niejawna konwersja (§10.2) do odpowiedniego typu parametru. Wynikowa wartość staje się początkową wartością parametru wartości w wywołaniu elementu członkowskiego funkcji.

    • w przeciwnym razie tryb przekazywania parametru to tryb wejściowy. Jeśli argument jest odwołaniem do zmiennej i istnieje konwersja tożsamości (§10.2.2) między typem argumentu a typem parametru, wynikowa lokalizacja magazynu staje się lokalizacją magazynu reprezentowaną przez parametr w wywołaniu elementu funkcji. W przeciwnym razie zostanie utworzona lokalizacja magazynu o tym samym typie co odpowiedni parametr. Wyrażenie argumentu jest obliczane i wykonywana jest niejawna konwersja (§10.2) do odpowiedniego typu parametru. Wynikowa wartość jest przechowywana w tej lokalizacji magazynu. Ta lokalizacja magazynu jest reprezentowana przez parametr wejściowy w wywołaniu członka funkcji.

      Przykład: przy uwzględnieniu następujących deklaracji i wywołań metody:

      static void M1(in int p1) { ... }
      int i = 10;
      M1(i);         // i is passed as an input argument
      M1(i + 5);     // transformed to a temporary input argument
      

      W wywołaniu metody M1(i) sam i jest przekazywany jako argument wejściowy, ponieważ jest klasyfikowany jako zmienna i ma ten sam typ int co parametr wejściowy. W wywołaniu metody M1(i + 5) zostanie utworzona zmienna int bez nazwy, zainicjowana przy użyciu wartości argumentu, a następnie przekazana jako argument wejściowy. Zobacz §12.6.4.2 i §12.6.4.4.

      koniec przykładu

  • W przypadku argumentu wejściowego, wyjściowego lub odwołania, referencja zmiennej jest oceniana, a wynikowa lokalizacja pamięci staje się lokalizacją pamięci, która jest przypisana do parametru w wywołaniu członka funkcji. W przypadku argumentu wejściowego lub referencyjnego zmienna musi być zdecydowanie przypisana w momencie wywołania metody. Jeśli odwołanie do zmiennej jest podane jako argument wyjściowy lub jest elementem tablicy reference_type, wykonywane jest sprawdzanie czasu wykonywania, aby upewnić się, że typ elementu tablicy jest identyczny z typem parametru. Jeśli to sprawdzenie zakończy się niepowodzeniem, zostanie wygenerowany System.ArrayTypeMismatchException.

Uwaga: to sprawdzanie w czasie wykonywania jest wymagane z powodu kowariancji tablic (§17.6). uwaga końcowa

Przykład: w poniższym kodzie

class Test
{
    static void F(ref object x) {...}

    static void Main()
    {
        object[] a = new object[10];
        object[] b = new string[10];
        F(ref a[0]); // Ok
        F(ref b[1]); // ArrayTypeMismatchException
    }
}

drugie wywołanie F powoduje wyrzucenie System.ArrayTypeMismatchException, ponieważ typ rzeczywisty elementu b to string, a nie object.

koniec przykładu

Metody, indeksatory i konstruktory wystąpień mogą zadeklarować ich najbardziej odpowiedni parametr jako tablicę parametrów (§15.6.2.4). Tacy członkowie funkcji są wywoływani w postaci normalnej lub rozszerzonej, w zależności od zastosowania (§12.6.4.2):

  • Gdy składowa funkcji z tablicą parametrów jest wywoływana w postaci normalnej, argument podany dla tablicy parametrów jest 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.
  • Gdy element członkowski funkcji z tablicą parametrów jest wywoływany w rozszerzonej formie, wywołanie określa zero lub więcej argumentów pozycyjnych 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.

Wyrażenia listy argumentów są zawsze oceniane w kolejności tekstowej.

przykładowo: Tak więc, przykład

class Test
{
    static void F(int x, int y = -1, int z = -2) =>
        Console.WriteLine($"x = {x}, y = {y}, z = {z}");

    static void Main()
    {
        int i = 0;
        F(i++, i++, i++);
        F(z: i++, x: i++);
    }
}

generuje dane wyjściowe

x = 0, y = 1, z = 2
x = 4, y = -1, z = 3

koniec przykładu

Gdy element członkowski funkcji z tablicą parametrów jest wywoływany w rozszerzonej formie z co najmniej jednym rozwiniętym argumentem, wywołanie jest przetwarzane tak, jakby wyrażenie tworzenia tablicy z inicjatorem tablicy (§12.8.17.5) zostało wstawione wokół rozszerzonych argumentów. Pusta tablica jest przekazywana, gdy nie ma argumentów dla tablicy parametrów; Nie określono, czy przekazane odwołanie jest do nowo przydzielonej, czy istniejącej pustej tablicy.

Przykład: Podana deklaracja

void F(int x, int y, params object[] args);

następujące wywołania rozszerzonej formy metody

F(10, 20, 30, 40);
F(10, 20, 1, "hello", 3.0);

odpowiada dokładnie

F(10, 20, new object[] { 30, 40 });
F(10, 20, new object[] { 1, "hello", 3.0 });

koniec przykładu

Gdy argumenty są pomijane z elementu członkowskiego funkcji z odpowiednimi opcjonalnymi parametrami, domyślne argumenty deklaracji składowej funkcji są niejawnie przekazywane. (Może to obejmować utworzenie lokalizacji magazynu, zgodnie z powyższym opisem).

Uwaga: Ponieważ są one zawsze stałe, ich ocena nie będzie mieć wpływu na ocenę pozostałych argumentów. uwaga końcowa

12.6.3 Wnioskowanie typu

12.6.3.1 Ogólne

Gdy metoda ogólna jest wywoływana bez określania argumentów typu, proces wnioskowania typu próbuje określić argumenty typu dla wywołania. Obecność wnioskowania typu umożliwia użycie wygodniejszej składni do wywoływania metody ogólnej i umożliwia programistom unikanie określania nadmiarowych informacji o typie.

Przykład:

class Chooser
{
    static Random rand = new Random();

    public static T Choose<T>(T first, T second) =>
        rand.Next(2) == 0 ? first : second;
}

class A
{
    static void M()
    {
        int i = Chooser.Choose(5, 213); // Calls Choose<int>
        string s = Chooser.Choose("apple", "banana"); // Calls Choose<string>
    }
}

Za pomocą wnioskowania typu argumenty typu int i string są określane z argumentów do metody.

koniec przykładu

Wnioskowanie typu odbywa się w ramach przetwarzania w czasie powiązania wywołania metody (§12.8.10.2) i odbywa się przed krokiem rozwiązywania przeciążenia wywołania. Gdy określona grupa metod jest określona w wywołaniu metody i nie określono żadnych argumentów typu w ramach wywołania metody, wnioskowanie typu jest stosowane do każdej metody ogólnej w grupie metod. Jeśli wnioskowanie typu powiedzie się, wnioskowane argumenty typu są używane do określenia typów argumentów podczas późniejszego rozpoznawania przeciążeń. Jeśli rozpoznawanie przeciążenia wybierze metodę ogólną jako metodę do wywołania, argumenty typu wnioskowanego są używane jako argumenty typu dla wywołania. Jeśli wnioskowanie typu dla określonej metody nie powiedzie się, ta metoda nie uczestniczy w rozwiązywaniu przeciążenia. Błąd wnioskowania typu samo w sobie nie powoduje błędu czasu wiązania. Jednak często prowadzi to do błędu czasu wiązania, gdy rozwiązanie przeciążenia nie znajduje żadnych odpowiednich metod.

Jeśli każdy podany argument nie odpowiada dokładnie jednemu parametrowi w metodzie (§12.6.2.2), lub nie ma opcjonalnego parametru bez odpowiedniego argumentu, wnioskowanie natychmiast kończy się niepowodzeniem. W przeciwnym razie załóżmy, że metoda ogólna ma następujący podpis:

Tₑ M<X₁...Xᵥ>(T₁ p₁ ... Tₓ pₓ)

Wywołanie metody o postaci M(E₁ ...Eₓ) polega na zadaniu wnioskowania typu, które wymaga znalezienia unikatowych argumentów typu S₁...Sᵥ dla każdego z parametrów typu X₁...Xᵥ, tak aby wywołanie M<S₁...Sᵥ>(E₁...Eₓ) było prawidłowe.

Proces wnioskowania typu jest opisany poniżej jako algorytm. Kompilator zgodny może zostać zaimplementowany przy użyciu alternatywnego podejścia, pod warunkiem, że osiągnie ten sam wynik we wszystkich przypadkach.

Podczas procesu wnioskowania każdy parametr typu Xᵢ jest albo ustalony do określonego typu Sᵢ, albo niewiązany, z przypisanym zestawem ograniczeń . Każde z ograniczeń to jakiś typ T. Początkowo każda zmienna typu Xᵢ jest niefiksowana z pustym zestawem granic.

Wnioskowanie typu odbywa się w fazach. Każda faza będzie próbowała wywnioskować argumenty typu dla większej liczby zmiennych typu na podstawie wyników poprzedniej fazy. Pierwsza faza powoduje początkowe wnioskowanie granic, podczas gdy druga faza ustala zmienne typu na określone typy i wnioskuje dalsze ograniczenia. Druga faza może być powtarzana kilka razy.

Uwaga: wnioskowanie typu jest również używane w innych kontekstach, w tym do konwersji grup metod (§12.6.3.14) i znalezienie najlepszego wspólnego typu zestawu wyrażeń (§12.6.3.15). uwaga końcowa

12.6.3.2 Pierwsza faza

Dla każdego argumentu metody Eᵢ:

  • Jeśli Eᵢ jest funkcją anonimową, jawne wnioskowanie typu parametru (§12.6.3.8) jest wykonywane zEᵢdoTᵢ
  • W przeciwnym razie, jeśli ma typ , a odpowiadający mu parametr jest parametrem wartości (§15.6.2.2), (§12.6.3.10) jest zdo.
  • W przeciwnym razie, jeśli Eᵢ ma typ U, a odpowiedni parametr jest parametrem referencyjnym (§15.6.2.3.3) lub parametr wyjściowy (§15.6.2 3.4) następnie dokładne wnioskowanie (§12.6.3.9) jest wykonywane zUdoTᵢ.
  • W przeciwnym razie, jeśli Eᵢ ma typ U, a odpowiadający mu parametr jest parametrem wejściowym (§15.6.2.3.2) i Eᵢ jest argumentem wejściowym, wówczas dokładnym wnioskowaniem (§12.6.3.9) jest zUdoTᵢ.
  • W przeciwnym razie, jeśli Eᵢ ma typ U, a odpowiedni parametr jest parametrem wejściowym (§15.6.2.3.2), niższego wnioskowania granic (§12.6.3.10) jest zUdoTᵢ.
  • W przeciwnym razie dla tego argumentu nie jest wykonywane żadne wnioskowanie.

12.6.3.3 Druga faza

Druga faza jest kontynuowana w następujący sposób:

  • Wszystkie niefiksowane zmienne typuXᵢ, które nie zależą od (§12.6.3.6) wszystkie Xₑ są stałe (§12.6.3.12).
  • Jeśli takie zmienne typu nie istnieją, wszystkie niefiksowane zmienne typuXᵢstałe, dla których wszystkie następujące blokady:
    • Istnieje co najmniej jedna zmienna typu Xₑ, która zależy odXᵢ
    • Xᵢ ma niepusty zbiór ograniczeń
  • Jeśli takie zmienne typu nie istnieją i nadal istnieją niefiksowane zmienne typu, wnioskowanie typu kończy się niepowodzeniem.
  • W przeciwnym razie, jeśli nie istnieją żadne dodatkowe niewiązane zmienne typu, wnioskowanie typu powiedzie się.
  • W przeciwnym razie dla wszystkich argumentów Eᵢ z odpowiednim typem parametru Tᵢ, w których typy danych wyjściowych (§12.6.3.5) zawierają niefiksowane zmienne typu Xₑ, ale typy wejściowe (§12.6.3.4) nie, typ danych wyjściowych (§12.6.3.7) jest zEᵢdoTᵢ. Następnie druga faza jest powtarzana.

Typy danych wejściowych 12.6.3.4

Jeśli E jest grupą metod lub niejawnie typowaną funkcją anonimową, a T jest typem delegata lub typem drzewa wyrażeń, to wszystkie typy parametrów T są typami wejściowymi Ez typemT.

Typy danych wyjściowych 12.6.3.5

Jeśli E jest grupą metod lub funkcją anonimową, a T jest typem delegata lub typem drzewa wyrażeń, zwracany typ T jest typem danych wyjściowych typuEz typemT.

12.6.3.6 Zależność

niefiksowana zmienna typuXᵢzależy bezpośrednio odniefiksowanej zmiennej typuXₑ, jeśli dla niektórych Eᵥ argumentów z typem TᵥXₑ występuje w typie wejściowymEᵥ z typem Tᵥ i Xᵢ występuje w typie wyjściowymEᵥ z typem Tᵥ.

Xₑ zależy odXᵢ, czy Xₑzależy bezpośrednio odXᵢ, czy Xᵢzależy bezpośrednio odXᵥ i Xᵥzależy odXₑ. W związku z tym "zależy od" jest domknięciem przechodnim, ale nie domknięciem refleksyjnym "zależy bezpośrednio od".

12.6.3.7 Wnioskowanie typu danych wyjściowych

Wnioskowanie typu wyjściowego jest wykonywane z wyrażenia Edo typu T w następujący sposób:

  • Jeśli E jest funkcją anonimową z wywnioskowanym typem zwracanym U (§12.6.3.13) i T jest typem delegata lub typem drzewa wyrażeń z typem zwracanym Tₓ, następnie wnioskowania o niższej granicy (§12.6.3.10) jest zUdoTₓ.
  • W przeciwnym razie jeśli jest grupą metod, a jest typem typu delegata lub drzewa wyrażeń z typami parametrów i zwracanym typem , a rozpoznawanie przeciążenia z typami zwraca jedną metodę z typem zwracanym , jest wykonywane zdo.
  • W przeciwnym razie, jeśli E jest wyrażeniem z typem U, wnioskowania niższego jest wykonywane zUdoT.
  • W przeciwnym razie nie są wykonywane żadne wnioskowania.

12.6.3.8 Jawne wnioskowania typu parametru

jawnego wnioskowania typu parametru jest wykonywane z wyrażenia Ew celu typu T w następujący sposób:

  • Jeśli E jest jawnie wpisaną funkcją anonimową z typami parametrów U₁...Uᵥ, a T jest typem delegowanym lub typem drzewa wyrażeń z typami parametrów V₁...Vᵥ, a następnie dla każdego Uᵢdokładnego wnioskowania (§12.6.3.9) jest zUᵢ odpowiedniego Vᵢ.

12.6.3.9 Dokładne wnioskowania

Dokładne wnioskowanie z typu Udo typu V jest wykonywane w następujący sposób:

  • Jeśli V jest jednym z niefiksowanychXᵢ, to U zostanie dodany do zestawu dokładnych granic dla Xᵢ.
  • W przeciwnym razie zestawy V₁...Vₑ i U₁...Uₑ są określane przez sprawdzenie, czy ma zastosowanie dowolny z następujących przypadków:
    • V jest typem tablicy V₁[...], a U jest typem tablicy U₁[...] tej samej rangi
    • V jest typem V₁?, a U jest typem U₁
    • V jest skonstruowanym typem C<V₁...Vₑ>, a U jest skonstruowanym typem C<U₁...Uₑ>
      Jeśli którykolwiek z tych przypadków ma zastosowanie, dokładne wnioskowanie jest wykonywane z każdego Uᵢ do odpowiedniego Vᵢ.
  • W przeciwnym razie nie są wykonywane żadne wnioskowania.

12.6.3.10 Wnioskowanie dotyczące dolnej granicy

Wnioskowanie od typu Udo typu V jest wykonywane w następujący sposób:

  • Jeśli V jest jednym z niefiksowanychXᵢ, U zostanie dodany do zestawu dolnych granic dla Xᵢ.
  • W przeciwnym razie, jeśli V jest typem V₁?, a U jest typem U₁?, wtedy następuje wnioskowanie dolnej granicy z U₁ do V₁.
  • W przeciwnym razie zestawy U₁...Uₑ i V₁...Vₑ są określane przez sprawdzenie, czy ma zastosowanie dowolny z następujących przypadków:
    • V jest typem tablicy V₁[...], a U jest typem tablicy U₁[...]tej samej rangi
    • V jest jednym z IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> lub IList<V₁>, a U jest jednowymiarowym typem tablicy U₁[]
    • V jest skonstruowanym typem class, takim jak struct, interface, delegate lub C<V₁...Vₑ>, i istnieje unikalny typ C<U₁...Uₑ>, dla którego U (lub, jeśli U jest typem parameter, jego efektywną klasą bazową lub dowolnym członkiem skutecznego zestawu interfejsów) jest identyczny z inherits (bezpośrednio lub pośrednio) albo implementuje (bezpośrednio lub pośrednio) C<U₁...Uₑ>.
    • (Ograniczenie "unikatowości" oznacza, że w przypadku interfejsu C<T>{} class U: C<X>, C<Y>{}, podczas wnioskowania z U do C<T> nie jest wykonywane żadne wnioskowanie, ponieważ U₁ może być X lub Y.)
      Jeśli którykolwiek z tych przypadków ma zastosowanie, wnioskowanie jest wykonywane z każdego Uᵢ do odpowiedniego Vᵢ w następujący sposób:
    • Jeśli Uᵢ nie jest znany jako typ odwołania, zostanie wykonane dokładne wnioskowanie
    • W przeciwnym razie jeśli U jest typem tablicy, wykonywane jest wnioskowanie o granicy dolnej.
    • W przeciwnym razie, jeśli V jest C<V₁...Vₑ>, to wnioskowanie zależy od parametru typu i-th w C:
      • Jeśli jest kowariantny, wykonuje się wnioskowanie o niższą granicę.
      • Jeśli jest kontrawariantny, wnioskowanie górnej granicy zostaje przeprowadzone.
      • Jeśli jest niezmienny, wykonywane jest dokładne wnioskowanie .
  • W przeciwnym razie nie są wykonywane żadne wnioskowania.

12.6.3.11 Wnioskowanie górnej granicy

Wnioskowanie górnej granicy od typu Udo typu V jest wykonywane w następujący sposób:

  • Jeśli V jest jednym z niefiksowanychXᵢ, U zostanie dodany do zestawu górnych granic dla Xᵢ.
  • W przeciwnym razie zestawy V₁...Vₑ i U₁...Uₑ są określane przez sprawdzenie, czy ma zastosowanie dowolny z następujących przypadków:
    • U jest typem tablicy U₁[...], a V jest typem tablicy V₁[...]tej samej rangi
    • U jest jednym z IEnumerable<Uₑ>, ICollection<Uₑ>, IReadOnlyList<Uₑ>, IReadOnlyCollection<Uₑ> lub IList<Uₑ>, a V jest jednowymiarowym typem tablicy Vₑ[]
    • U jest typem U1?, a V jest typem V1?
    • U jest konstruowana klasa, struktura, interfejs lub typ delegata C<U₁...Uₑ>, a V jest typem class, struct, interface lub delegate, który jest identical, inherits z (bezpośrednio lub pośrednio) lub implementuje (bezpośrednio lub pośrednio) unikatowy typ C<V₁...Vₑ>
    • (Ograniczenie "unikatowości" oznacza, że biorąc pod uwagę interfejs C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{}, podczas wnioskowania z C<U₁> do V<Q>nie jest wykonywane żadne wnioskowanie. Wnioskowanie nie jest wykonywane z U₁ do X<Q> lub Y<Q>).
      Jeśli którykolwiek z tych przypadków ma zastosowanie, wnioskowanie jest wykonywane z każdego Uᵢ do odpowiedniego Vᵢ w następujący sposób:
    • Jeśli Uᵢ nie jest znany jako typ odwołania, zostanie wykonane dokładne wnioskowanie
    • W przeciwnym razie, jeśli V jest typem tablicy, zostanie wykonane wnioskowanie górnej granicy
    • W przeciwnym razie, jeśli U jest C<U₁...Uₑ>, to wnioskowanie zależy od parametru typu i-th w C:
      • Jeśli jest kowariantny, wykonywane jest wnioskowanie górnej granicy .
      • Jeśli jest kontrawariantny, wykonywane jest wnioskowanie o niższej granicy .
      • Jeśli jest niezmienny, wykonywane jest dokładne wnioskowanie .
  • W przeciwnym razie nie są wykonywane żadne wnioskowania.

12.6.3.12 Naprawianie

Niefiksowana zmienna typu z zestawem granic jest stała w następujący sposób:

  • Zestaw typów kandydatów Uₑ zaczyna się jako zestaw wszystkich typów w zestawie granic dla Xᵢ.
  • Każda granica dla Xᵢ jest badana po kolei: Dla każdej dokładnej granicy U dla Xᵢ wszystkie typy Uₑ, które nie są identyczne z U, są usuwane z zestawu kandydatów. Dla każdej niższej granicy U z Xᵢ, wszystkie typy Uₑ, które nie mają niejawnej konwersji z U, są usuwane z zestawu kandydatów. Dla każdej górnej granicy U zestawu Xᵢ, typy Uₑ, dla których nie istnieje niejawna konwersja na U, są usuwane z zestawu kandydatów.
  • Jeśli wśród pozostałych typów kandydatów Uₑ istnieje unikatowy typ V, do którego istnieje niejawna konwersja ze wszystkich pozostałych typów kandydatów, to Xᵢ jest ustalony jako V.
  • W przeciwnym razie wnioskowanie typu kończy się niepowodzeniem.

12.6.3.13 Wnioskowany typ zwracany

Wnioskowany zwracany typ funkcji anonimowej F jest używany podczas wnioskowania typu i rozpoznawania przeciążenia. Wywnioskowany typ zwracany można określić tylko dla funkcji anonimowej, w której znane są wszystkie typy parametrów, ponieważ są one jawnie określone, dostępne poprzez konwersję funkcji anonimowej lub wywnioskowane podczas inferencji typu na otaczającym wywołaniu metody generycznej.

wywnioskowany typ rzeczywistego zwracania jest określany w następujący sposób:

  • Jeśli treść F jest wyrażeniem , które ma typ, to efektywny wnioskowany typ zwrotny F jest typem tego wyrażenia.
  • Jeśli treść F jest blokiem , a zestaw wyrażeń w instrukcjach return bloku ma najlepszy wspólny typ T (§12.6.3.15), to wnioskowany efektywny typ zwracany F jest T.
  • W przeciwnym razie nie można wywnioskować efektywnego typu zwracanego dla F.

wnioskowany typ zwracany jest określany w następujący sposób:

  • Jeśli F jest asynchroniczny, a treść F jest wyrażeniem sklasyfikowanym jako „nic” (§12.2) lub blokiem, w którym żadna z instrukcji return nie ma wyrażeń, wnioskowany typ zwracany to «TaskType» (§15.15.1).
  • Jeśli F jest asynchroniczny i ma wnioskowany typ zwrotu T, wnioskowany typ zwrotu jest «TaskType»<T>»(§15.15.1).
  • Jeśli F nie jest asynchroniczny i ma wywnioskowany efektywny typ zwracany T, wnioskowany typ zwracany jest T.
  • W przeciwnym razie nie można wywnioskować typu zwracanego dla F.

Przykład: jako przykład wnioskowania typu obejmującego funkcje anonimowe należy wziąć pod uwagę metodę rozszerzenia Select zadeklarowaną w klasie System.Linq.Enumerable:

namespace System.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TResult> Select<TSource,TResult>(
            this IEnumerable<TSource> source,
            Func<TSource,TResult> selector)
        {
            foreach (TSource element in source)
            {
                yield return selector(element);
            }
        }
   }
}

Przy założeniu, że przestrzeń nazw System.Linq została zaimportowana przy użyciu dyrektywy using namespace i biorąc pod uwagę klasę Customer z właściwością Name typu string, można użyć metody Select, aby wybrać nazwy klientów:

List<Customer> customers = GetCustomerList();
IEnumerable<string> names = customers.Select(c => c.Name);

Wywołanie metody rozszerzenia (§12.8.10.3) Select jest przetwarzane przez ponowne zapisywanie wywołania metody statycznej:

IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);

Ponieważ argumenty typu nie zostały jawnie określone, wnioskowanie typu służy do wnioskowania argumentów typu. Najpierw argument klienta jest powiązany z parametrem źródłowym, co sugeruje, że TSource powinno być Customer. Następnie, korzystając z opisanego powyżej procesu wnioskowania typu funkcji anonimowej, c otrzymuje typ Customer, a wyrażenie c.Name odnosi się do typu zwrotnego parametru selektora, wnioskując, że TResult jest string. W związku z tym wywołanie jest równoważne

Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)

a wynik jest typu IEnumerable<string>.

W poniższym przykładzie pokazano, w jaki sposób wnioskowanie typu dla funkcji anonimowych pozwala informacjom o typie przepływać między argumentami w wywołaniu metody ogólnej. Biorąc pod uwagę następującą metodę i wywołanie:

class A
{
    static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2)
    {
        return f2(f1(value));
    }

    static void M()
    {
        double hours = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalHours);
    }
}

wnioskowanie typu dla wywołania następuje w następujący sposób: Najpierw argument "1:15:30" jest powiązany z parametrem wartości, wnioskowując, że X jest ciągiem. Następnie parametr pierwszej funkcji anonimowej, s, otrzymuje wywnioskowany typ string, a wyrażenie TimeSpan.Parse(s) jest powiązane z typem zwrotnym f1, wnioskując, że Y jest System.TimeSpan. Na koniec, parametr drugiej funkcji anonimowej, t, ma przypisany wywnioskowany typ System.TimeSpan, a wyrażenie t.TotalHours jest powiązane z typem zwrotnym f2, co pozwala wnioskować, że Z jest double. W związku z tym wynik wywołania jest typu double.

koniec przykładu

12.6.3.14 Wnioskowanie typów dla konwersji grup metod

Podobnie jak wywołania metod ogólnych, wnioskowanie typu stosuje się również, gdy grupa metod M zawierająca metodę ogólną jest konwertowana na dany typ delegata D (§10.8). Biorąc pod uwagę metodę

Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)

a grupa metod M przypisana do typu delegata D zadaniem wnioskowania typu jest znalezienie argumentów typu S₁...Sᵥ tak, aby wyrażenie:

M<S₁...Sᵥ>

staje się zgodny (§20.2) z D.

W przeciwieństwie do algorytmu wnioskowania typu dla wywołań metod ogólnych, w tym przypadku istnieją tylko typy argumentów , bez wyrażeń argumentów. W szczególności nie ma funkcji anonimowych i dlatego nie ma potrzeby wielu faz wnioskowania.

Zamiast tego wszystkie Xᵢ są traktowane jako niefiksowane, a wnioskowania niższego jest wykonywane z każdego typu argumentu UₑD odpowiadający typ parametru TₑM. Jeśli dla któregokolwiek z Xᵢ nie znaleziono żadnych granic, wnioskowanie typu nie powiedzie się. W przeciwnym razie wszystkie Xᵢstałe odpowiadające Sᵢ, które są wynikiem wnioskowania typu.

12.6.3.15 Znajdowanie najlepszego typowego typu zestawu wyrażeń

W niektórych przypadkach typ wspólny musi zostać wywnioskowany dla zestawu wyrażeń. W szczególności typy elementów niejawnie typizowanych tablic oraz zwracane typy funkcji anonimowych z ciałami bloków są znajdowane w ten sposób.

Najlepszy typ dla zbioru wyrażeń E₁...Eᵥ jest określany w następujący sposób:

  • Wprowadzono nową niefiksowaną zmienną typuX.
  • Dla każdego wyrażenia Ei przeprowadzane jest wnioskowanie typu danych wyjściowych (§12.6.3.7) od niego do X.
  • X jest stały (§12.6.3.12), jeśli to możliwe, a wynikowy typ jest najlepszym typem typowym.
  • W przeciwnym razie wnioskowanie kończy się niepowodzeniem.

Uwaga: Intuicyjnie to wnioskowanie jest równoważne wywołaniu metody void M<X>(X x₁ ... X xᵥ) z Eᵢ jako argumentami i wnioskowaniem X. uwaga końcowa

12.6.4 Rozpoznawanie przeciążenia

12.6.4.1 Ogólne

Rozwiązywanie przeciążeń to mechanizm czasu wiązania do wyboru najlepszego członka funkcji do wywołania, biorąc pod uwagę listę argumentów i zestaw kandydatów członków funkcji. Rozwiązanie przeciążenia wybiera człon funkcji do wywołania w następujących odrębnych kontekstach w języku C#:

  • Wywołanie metody o nazwie określonej w invocation_expression (§12.8.10).
  • Wywołanie konstruktora wystąpienia nazwanego w object_creation_expression (§12.8.17.2).
  • Wywołanie akcesora indeksatora poprzez element_access (§12.8.12).
  • Wywołanie wstępnie zdefiniowanego lub zdefiniowanego przez użytkownika operatora przywoływanego w wyrażeniu (§12.4.4 i §12.4.5).

Każdy z tych kontekstów definiuje zestaw elementów członkowskich funkcji kandydata i listę argumentów we własny unikatowy sposób. Na przykład zestaw kandydatów do wywołania metody nie obejmuje metod oznaczonych jako 'override' (§12.5), a metody w klasie bazowej nie są kandydatami, jeśli jakakolwiek metoda w klasie pochodnej może być zastosowana (§12.8.10.2).

Po zidentyfikowaniu członków funkcji kandydującej i listy argumentów wybór najlepszego członka funkcji jest taki sam we wszystkich przypadkach:

  • Po pierwsze, zestaw członków funkcji kandydujących jest zawężony do tych, które są odpowiednie dla danej listy argumentów (§12.6.4.2). Jeśli ten ograniczony zestaw jest pusty, wystąpi błąd czasu kompilacji.
  • Następnie znajduje się najlepszy element członkowski funkcji z zestawu odpowiednich członków funkcji kandydata. Jeśli zestaw zawiera tylko jeden członek funkcji, wówczas jest on najlepszym członkiem funkcji. W przeciwnym razie najlepszym elementem członkowskim funkcji jest jeden element członkowski funkcji, który jest lepszy niż wszystkie inne elementy członkowskie funkcji w odniesieniu do danej listy argumentów, pod warunkiem, że każdy element członkowski funkcji jest porównywany ze wszystkimi innymi elementami członkowskimi funkcji przy użyciu reguł w §12.6.4.3. Jeśli nie ma dokładnie jednego członka funkcji, który jest lepszy niż wszyscy inni członkowie funkcji, to wywołanie członka funkcji jest niejednoznaczne i występuje błąd w czasie wiązania.

Następujące podpunkty definiują dokładne znaczenie terminów odpowiednich składowych funkcji i lepszej składowej funkcji.

12.6.4.2 Stosowna funkcja członkowska

Mówi się, że członek funkcji jest odpowiednim członkiem funkcji w odniesieniu do listy argumentów A, gdy spełnione są wszystkie następujące warunki:

  • Każdy argument w A odpowiada parametrowi w deklaracji składowej funkcji zgodnie z opisem w §12.6.2.2, co najwyżej jeden argument odpowiada każdemu parametrowi, a każdy parametr, do którego żaden argument nie odpowiada, jest opcjonalnym parametrem.
  • Dla każdego argumentu w Atryb przekazywania parametrów argumentu jest identyczny z trybem przekazywania parametrów odpowiedniego parametru i
    • dla parametru wartości lub tablicy parametrów niejawna konwersja (§10.2) istnieje z wyrażenia argumentu do typu odpowiedniego parametru lub
    • dla parametru referencyjnego lub wyjściowego istnieje konwersja tożsamości między typem wyrażenia argumentu (jeśli istnieje) a typem odpowiedniego parametru lub
    • parametr wejściowy, kiedy odpowiedni argument ma modyfikator in, istnieje identyczna konwersja między typem wyrażenia argumentu (jeśli takie istnieje) a typem odpowiedniego parametru lub
    • Dla parametru wejściowego, gdy odpowiadający mu argument pomija modyfikator in, istnieje niejawna konwersja (§10.2) z wyrażenia argumentu na typ tego parametru.

W przypadku członka funkcji, który zawiera tablicę parametrów, jeśli członek funkcji jest stosowalny zgodnie z powyższymi regułami, mówi się, że jest stosowalny w swojej normalnej postaci. Jeśli członek funkcji zawierający tablicę parametrów nie ma zastosowania w postaci normalnej, ten członek funkcji może zamiast tego mieć zastosowanie w rozszerzonej postaci:

  • Formularz rozwinięty jest konstruowany przez zastąpienie tablicy parametrów w deklaracji członka funkcji zerowym lub większą liczbą parametrów wartościowych o typie elementu tablicy parametrów w taki sposób, aby liczba argumentów na liście argumentów A odpowiadała łącznej liczbie parametrów. Jeśli A ma mniej argumentów niż liczba stałych parametrów w deklaracji składowej funkcji, rozszerzona forma składowej funkcji nie może być skonstruowana i w związku z tym nie ma zastosowania.
  • W przeciwnym razie rozszerzony formularz ma zastosowanie, jeśli dla każdego argumentu w Ajest spełniony jeden z następujących warunków:
    • tryb przekazywania parametrów argumentu jest identyczny z trybem przekazywania parametrów odpowiedniego parametru i
      • dla parametru stałej wartości lub parametru wartości utworzonego przez rozszerzenie, niejawna konwersja (§10.2) istnieje z wyrażenia argumentu do typu odpowiedniego parametru lub
      • dla parametru by-reference typ wyrażenia argumentu jest identyczny z typem odpowiedniego parametru.
    • tryb przekazywania parametrów argumentu jest wartością, a tryb przekazywania parametrów odpowiedniego parametru jest wejściowy, a niejawna konwersja (§10.2) istnieje z wyrażenia argumentu na typ odpowiedniego parametru

Gdy niejawna konwersja typu argumentu na typ parametru wejściowego jest dynamiczną niejawną konwersją (§10.2.10), wyniki są niezdefiniowane.

Przykład: przy uwzględnieniu następujących deklaracji i wywołań metody:

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }
public static void M2(in int p1) { ... }
public static void Test()
{
    int i = 10; uint ui = 34U;

    M1(in i);   // M1(in int) is applicable
    M1(in ui);  // no exact type match, so M1(in int) is not applicable
    M1(i);      // M1(int) and M1(in int) are applicable
    M1(i + 5);  // M1(int) and M1(in int) are applicable
    M1(100u);   // no implicit conversion exists, so M1(int) is not applicable

    M2(in i);   // M2(in int) is applicable
    M2(i);      // M2(in int) is applicable
    M2(i + 5);  // M2(in int) is applicable
}

koniec przykładu

  • Metoda statyczna ma zastosowanie tylko wtedy, gdy grupa metod wynika z simple_name lub member_access za pośrednictwem typu.
  • Metoda instancyjna ma zastosowanie tylko wtedy, gdy grupa metod wynika z simple_name, member_access za pośrednictwem zmiennej lub wartości, lub base_access.
    • Jeśli grupa metod wynika z simple_name, metoda instancji ma zastosowanie tylko wtedy, o ile this dostęp jest dozwolony §12.8.14.
  • Gdy grupa metod wynika z member_access, która może być dostępna zarówno za pośrednictwem wystąpienia, jak i typu, zgodnie z opisem w §12.8.7.2, mają zastosowanie zarówno metody instancyjne, jak i metody statyczne.
  • Metoda ogólna, której argumenty typu (jawnie określone lub wnioskowane) nie spełniają ich ograniczeń, nie ma zastosowania.
  • W kontekście konwersji grupy metod musi istnieć konwersja identyczności (§10.2.2) lub niejawna konwersja referencyjna (§10.2.8) z typu zwracanego metody do typu zwracanego delegata. W przeciwnym razie metoda kandydacka nie ma zastosowania.

12.6.4.3 Lepszy element funkcji

Na potrzeby określenia lepszego członka funkcji, konstruowana jest uproszczona lista argumentów A, zawierająca tylko wyrażenia argumentowe w kolejności, w jakiej pojawiają się na oryginalnej liście argumentów, z pominięciem wszystkich argumentów out lub ref.

Listy parametrów dla każdego z członków funkcji kandydatów są tworzone w następujący sposób:

  • Forma rozszerzona jest używana, jeśli członek funkcji był stosowany tylko w formie rozszerzonej.
  • Parametry opcjonalne bez odpowiednich argumentów są usuwane z listy parametrów
  • Parametry referencyjne i wyjściowe są usuwane z listy parametrów
  • Parametry są zmieniane tak, aby występowały w tej samej pozycji co odpowiedni argument na liście argumentów.

Biorąc pod uwagę listę argumentów z zestawem wyrażeń argumentów i dwoma odpowiednimi elementami członkowskimi funkcji i z typami parametrów i , jest definiowana jako lepiej funkcji niż , jeśli

  • dla każdego argumentu niejawna konwersja z Eᵥ na Qᵥ nie jest lepsza niż niejawna konwersja z Eᵥ na Pᵥi
  • dla co najmniej jednego argumentu konwersja z Eᵥ na Pᵥ jest lepsza niż konwersja z Eᵥ na Qᵥ.

W przypadku, gdy sekwencje typów parametrów {P₁, P₂, ..., Pᵥ} i {Q₁, Q₂, ..., Qᵥ} są równoważne (tj. każda Pᵢ ma konwersję bezpośrednią do odpowiadającego Qᵢ), stosuje się następujące reguły rozstrzygania remisów, jedna po drugiej, aby wybrać lepszego członka funkcji.

  • Jeśli Mᵢ jest metodą niegeneryjną, a Mₑ jest metodą ogólną, Mᵢ jest lepsza niż Mₑ.
  • W przeciwnym razie, jeśli Mᵢ ma zastosowanie w postaci normalnej i Mₑ ma tablicę params i ma zastosowanie tylko w rozszerzonej formie, Mᵢ jest lepszy niż Mₑ.
  • W przeciwnym razie, jeśli obie metody mają tablice params i mają zastosowanie tylko w ich rozszerzonych formularzach, a jeśli tablica parametrów Mᵢ ma mniej elementów niż tablica params Mₑ, Mᵢ jest lepsza niż Mₑ.
  • W przeciwnym razie, jeśli Mᵥ ma bardziej szczegółowe typy parametrów niż Mₓ, Mᵥ jest lepszy niż Mₓ. Niech {R1, R2, ..., Rn} i {S1, S2, ..., Sn} reprezentują niezainstantowane i nierozwinięte typy parametrów Mᵥ i Mₓ. Mᵥtypy parametrów są bardziej szczegółowe niż Mₓ, jeśli dla każdego parametru Rx nie jest mniej szczegółowy niż Sx, a dla co najmniej jednego parametru Rx jest bardziej szczegółowy niż Sx:
    • Parametr typu jest mniej specyficzny niż parametr inny niż typ.
    • Rekursywnie skonstruowany typ jest bardziej szczegółowy niż inny typ skonstruowany (z tą samą liczbą argumentów typu), jeśli co najmniej jeden argument typu jest bardziej szczegółowy i żaden argument typu nie jest mniej specyficzny niż odpowiadający mu argument typu w drugiej.
    • Typ tablicy jest bardziej szczegółowy niż inny typ tablicy (o tej samej liczbie wymiarów), jeśli typ elementu pierwszego jest bardziej szczegółowy niż typ elementu drugiego.
  • W przeciwnym razie, jeśli jeden element jest operatorem niepodniesionym, a drugi operatorem podniesionym, to lepszy jest operator niepodniesiony.
  • Jeśli nie znaleziono ani jednego członka funkcji, który byłby lepszy, a wszystkie parametry Mᵥ mają odpowiedni argument, podczas gdy dla co najmniej jednego opcjonalnego parametru w Mₓmuszą być użyte argumenty domyślne, wtedy Mᵥ jest lepszy niż Mₓ.
  • Jeśli w przypadku co najmniej jednego parametru Mᵥ używa lepszego wyboru przekazywania parametrów (§12.6.4.4) niż odpowiedni parametr w Mₓ i żaden z parametrów w Mₓ używa lepszego wyboru przekazywania parametrów niż Mᵥ, Mᵥ jest lepszy niż Mₓ.
  • W przeciwnym razie, żaden element funkcji nie jest lepszy.

12.6.4.4 Lepszy tryb przekazywania parametrów

Dozwolone jest posiadanie odpowiednich parametrów w dwóch przeciążonych metodach, które różnią się tylko trybem przekazywania parametrów, pod warunkiem że jeden z dwóch parametrów przekazuje wartość.

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }

Biorąc pod uwagę int i = 10;, zgodnie z §12.6.4.2, wywołania M1(i) i M1(i + 5) powodują zastosowanie obu przeciążeń. W takich przypadkach metoda z trybem przekazywania parametrów jest lepszym wyborem trybu przekazywania parametrów.

Uwaga: nie ma takiego wyboru dla argumentów trybów przekazywania danych wejściowych, wyjściowych lub odwołań, ponieważ te argumenty są zgodne tylko z dokładnie tymi samymi trybami przekazywania parametrów. uwaga końcowa

12.6.4.5 Lepsza konwersja z wyrażenia

Biorąc pod uwagę niejawną konwersję C₁, która konwertuje z wyrażenia E na typ T₁, oraz niejawną konwersję C₂, która konwertuje z wyrażenia E na typ T₂, C₁ jest lepszą konwersją niż C₂, jeśli spełniony jest jeden z następujących warunków:

  • E dokładnie pasuje do T₁ i E nie pasuje dokładnie do T₂ (§12.6.4.6)
  • E dokładnie pasuje albo do obu, albo do żadnego z T₁ i T₂, a T₁ jest lepszym celem konwersji niż T₂ (§12.6.4.7)
  • E jest grupą metod (§12.2), T₁ jest zgodna (§20.4) z pojedynczą najlepszą metodą z grupy metod konwersji C₁, a T₂ nie jest zgodna z jedną najlepszą metodą z grupy metod konwersji C₂

12.6.4.6 Dokładnie pasujące wyrażenie

Biorąc pod uwagę wyrażenie E i typ T, Edokładnie pasuje doT, jeśli zachodzi jeden z następujących warunków:

  • E ma typ S, a konwersja identyczności istnieje z S do T
  • E jest funkcją anonimową, T jest typem delegata D lub typem drzewa wyrażeń Expression<D> i spełnia jeden z następujących warunków:
    • Wywnioskowany typ zwracany X istnieje dla E w kontekście listy parametrów D (§12.6.3.12), a konwersja identyczności istnieje z X do zwracanego typu D
    • E jest async lambda bez wartości zwracanej, a D ma typ zwracany, który jest niegeneryczny «TaskType»
    • Albo E nie jest asynchroniczny, a D ma typ zwracany Y, lub E jest asynchroniczny, a D ma typ zwracany «TaskType»<Y>(§15.15.1), i spełniony jest jeden z następujących warunków:
      • Treść E jest wyrażeniem, które pasuje dokładnie do Y
      • Treść E to blok, w którym każda instrukcja return zwraca wyrażenie, które dokładnie odpowiada Y

12.6.4.7 Lepszy cel konwersji

Biorąc pod uwagę dwa typy T₁ i T₂, T₁ jest lepszym celem konwersji niż T₂, jeśli zachodzi jeden z następujących warunków:

  • Istnieje niejawna konwersja z T₁ na T₂ i nie istnieje niejawna konwersja z T₂ na T₁
  • T₁ jest «TaskType»<S₁>(§15.15.1), T₂ jest «TaskType»<S₂>, a S₁ jest lepszym celem konwersji niż S₂
  • T₁ jest «TaskType»<S₁>(§15.15.1), T₂ jest «TaskType»<S₂>, a T₁ jest bardziej wyspecjalizowany niż T₂
  • T₁ jest S₁ lub S₁?, gdzie S₁ jest typem całkowitoliczbowym ze znakiem, a T₂ jest S₂ lub S₂?, gdzie S₂ jest bezznakowym typem całkowitoliczbowym. Specyficznie:
    • S₁ jest sbyte, a S₂ jest byte, ushort, uintlub ulong
    • S₁ jest short i S₂ jest ushort, uintlub ulong
    • S₁ jest int, a S₂ jest uintlub ulong
    • S₁ jest long, a S₂ jest ulong

12.6.4.8 Przeciążanie w klasach ogólnych

Uwaga: Podczas gdy podpisy zadeklarowane są unikatowe (§8.6), istnieje możliwość, że podstawienie argumentów typu powoduje identyczne podpisy. W takiej sytuacji rozwiązanie przeciążenia wybierze najbardziej specyficzne (§12.6.4.3) oryginalnych podpisów (przed zastąpieniem argumentów typu), jeśli istnieje, i w przeciwnym razie zgłosi błąd. uwaga końcowa

Przykład: W poniższych przykładach przedstawiono prawidłowe i nieprawidłowe przeciążenia zgodnie z tą regułą:

public interface I1<T> { ... }
public interface I2<T> { ... }

public abstract class G1<U>
{
    public abstract int F1(U u);           // Overload resolution for G<int>.F1
    public abstract int F1(int i);         // will pick non-generic

    public abstract void F2(I1<U> a);      // Valid overload
    public abstract void F2(I2<U> a);
}

abstract class G2<U,V>
{
    public abstract void F3(U u, V v);     // Valid, but overload resolution for
    public abstract void F3(V v, U u);     // G2<int,int>.F3 will fail

    public abstract void F4(U u, I1<V> v); // Valid, but overload resolution for
    public abstract void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail

    public abstract void F5(U u1, I1<V> v2);   // Valid overload
    public abstract void F5(V v1, U u2);

    public abstract void F6(ref U u);      // Valid overload
    public abstract void F6(out V v);
}

koniec przykładu

12.6.5 Sprawdzanie wywołania dynamicznego członka w czasie kompilacji

Mimo że rozpoznawanie przeciążenia operacji dynamicznie powiązanej odbywa się w czasie wykonywania, czasami w czasie kompilacji jest możliwe poznanie listy elementów członkowskich funkcji, z których zostanie wybrane przeciążenie:

  • W przypadku wywołania delegacji (§12.8.10.4) lista jest pojedynczym członkiem funkcji z takim samym wykazem parametrów co delegate_type wywołania.
  • W przypadku wywołania metody (§12.8.10.2) dla typu lub wartości, której typ statyczny nie jest dynamiczny, zestaw metod dostępnych w grupie metod jest znany w czasie kompilacji.
  • W przypadku wyrażenia tworzenia obiektu (§12.8.17.2) zestaw dostępnych konstruktorów w typie jest znany w czasie kompilacji.
  • W przypadku dostępu indeksatora (§12.8.12.3) zestaw dostępnych indeksatorów w odbiorniku jest znany w czasie kompilacji.

W takich przypadkach na każdej funkcji w znanym zestawie funkcji przeprowadzane jest ograniczone sprawdzenie w czasie kompilacji, aby sprawdzić, czy można na pewno stwierdzić, że nigdy nie będzie wywoływana w czasie wykonywania. Dla każdego elementu członkowskiego funkcji F jest konstruowana zmodyfikowana lista parametrów i argumentów:

  • Po pierwsze, jeśli F jest metodą ogólną i podano argumenty typu, są one zastępowane parametrami typu na liście parametrów. Jeśli jednak nie podano argumentów typu, takie podstawienie nie następuje.
  • Następnie każdy parametr, którego typ jest otwarty (tj. zawiera parametr typu; zobacz §8.4.3), jest pominięty, wraz z odpowiednimi parametrami.

Aby F przeszedł kontrolę, wszystkie następujące warunki muszą być spełnione:

  • Zmodyfikowana lista parametrów dla F ma zastosowanie do zmodyfikowanej listy argumentów w zakresie §12.6.4.2.
  • Wszystkie skonstruowane typy na liście zmodyfikowanych parametrów spełniają swoje ograniczenia (§8.4.5).
  • Jeśli parametry typu F zostały zastąpione w powyższym kroku, ich ograniczenia są spełnione.
  • Jeśli F jest metodą statyczną, grupa metod nie powinna wynikać z member_access, którego odbiornik jest znany w czasie kompilacji jako zmienna lub wartość.
  • Jeśli F jest metodą wystąpienia, grupa metod nie wynika z member_access, którego odbiornik jest znany w czasie kompilacji jako typ.

Jeśli żaden kandydat nie przejdzie tego testu, wystąpi błąd czasu kompilacji.

Wywołanie składowej funkcji 12.6.6

12.6.6.1 Ogólne

W tym podpunkcie opisano proces, który odbywa się w czasie wykonywania w celu wywołania określonego członka funkcji. Zakłada się, że proces czasu powiązania określił już konkretny element członkowski do wywołania, być może stosując rozpoznawanie przeciążenia do zestawu elementów członkowskich funkcji kandydata.

Dla opisu procesu wywoływania, członkowie funkcji są podzieleni na dwie kategorie:

  • Członkowie funkcji statycznych. Są to metody statyczne, metody dostępu do właściwości statycznych i operatory zdefiniowane przez użytkownika. Statyczne elementy członkowskie funkcji są zawsze niewirtualne.
  • Członkowie funkcji instancji Są to metody wystąpień, konstruktory wystąpień, metody dostępu do właściwości wystąpienia i metody dostępu indeksatora. Członkowie funkcji instancji są niewirtualni lub wirtualni i są zawsze wywoływani na danej instancji. Instancja jest obliczana za pomocą wyrażenia instancji i staje się dostępna w elemencie członkowskim funkcji jako this (§12.8.14). W przypadku konstruktora wystąpienia wyrażenie wystąpienia jest traktowane jako nowo przydzielony obiekt.

Przetwarzanie w czasie wykonywania wywołania składnika funkcji składa się z następujących kroków, w których M jest składnikiem funkcji, a jeśli M jest członkiem instancji, E jest wyrażeniem instancji.

  • Jeśli M jest elementem członkowskim funkcji statycznej:

    • Lista argumentów jest oceniana zgodnie z opisem w §12.6.2.
    • M jest wywoływany.
  • W przeciwnym razie, jeśli typ E jest typem wartości V, a M jest zadeklarowany lub zastąpiony w V:

    • E jest oceniana. Jeśli ta ocena spowoduje wyjątek, nie zostaną wykonane żadne dalsze kroki. W przypadku konstruktora wystąpienia ten proces składa się z przydzielania pamięci (zazwyczaj ze stosu wykonywania) dla nowego obiektu. W tym przypadku E jest klasyfikowana jako zmienna.
    • Jeśli E nie jest klasyfikowana jako zmienna lub jeśli V nie jest typem struktury readonly (§16.2.2), a E jest jednym z:
      • parametr wejściowy (§15.6.2.3.2) lub
      • pole readonly (§15.5.3) lub
      • zmienna referencyjna readonly lub wartość zwracana (§9.7),

    następnie jest tworzona tymczasowa zmienna lokalna typu E, a wartość E jest przypisywana do tej zmiennej. E jest następnie ponownie sklasyfikowany jako odwołanie do tej tymczasowej zmiennej lokalnej. Zmienna tymczasowa jest dostępna jako this w M, ale nie w żaden inny sposób. W związku z tym tylko wtedy, gdy można napisać E, obiekt wywołujący może obserwować zmiany, które M wprowadza do this.

    • Lista argumentów jest oceniana zgodnie z opisem w §12.6.2.
    • M jest wywoływany. Zmienna, do której odwołuje się E, staje się zmienną przywoływaną przez this.
  • Inaczej:

    • E jest oceniana. Jeśli ta ocena spowoduje wyjątek, nie zostaną wykonane żadne dalsze kroki.
    • Lista argumentów jest oceniana zgodnie z opisem w §12.6.2.
    • Jeśli typ E jest value_type, konwersja boksu (§10.2.9) jest wykonywana w celu przekonwertowania E na class_type, a E jest uważana za class_type w poniższych krokach. Jeśli value_type jest enum_type, to class_type jest System.Enum;, w przeciwnym razie jest System.ValueType.
    • Wartość E jest sprawdzana jako prawidłowa. Jeśli wartość E ma wartość null, zostanie zgłoszony System.NullReferenceException i nie zostaną wykonane żadne dalsze kroki.
    • Określa się implementację członka funkcji do wywołania.
      • Jeśli typ określony w czasie powiązania E jest interfejsem, element funkcji, który należy wywołać, to implementacja M zapewniona przez typ czasu wykonywania instancji, do której odwołuje się E. Ten członek funkcji jest ustalany poprzez zastosowanie reguł mapowania interfejsu (§18.6.5) w celu określenia implementacji M dostarczanego przez typ czasu wykonywania instancji, do której odwołuje się E.
      • W przeciwnym razie, jeśli M jest elementem członkowskim funkcji wirtualnej, elementem członkowskim funkcji do wywołania jest implementacja M zapewniana przez typ czasu wykonywania wystąpienia, do którego odwołuje się E. Ten członek funkcji jest określany poprzez zastosowanie reguł określania najbardziej pochodnej implementacji (§15.6.4) M w odniesieniu do typu czasu wykonywania instancji, na którą wskazuje E.
      • W przeciwnym razie M jest elementem członkowskim funkcji niewirtualnej, a wywoływaną funkcją jest sam M.
    • Wywoływana jest członkowska implementacja funkcji określona w powyższym kroku. Obiekt, do którego odwołuje się E, staje się obiektem odwołującym się do tego.

Wynikiem wywołania konstruktora wystąpienia (§12.8.17.2) jest tworzona wartość. Wynikiem wywołania dowolnego innego składnika funkcji jest wartość, o ile istnieje, zwrócona (§13.10.5) z jej ciała.

12.6.6.2 Wywołania na opakowanych wystąpieniach

Składowa funkcji zaimplementowana w value_type może być wywołana przez opakowane wystąpienie tej value_type w następujących sytuacjach:

  • Gdy element członkowski funkcji jest przesłonięciem metody dziedziczonej z typu class_type i jest wywoływany za pomocą wyrażenia wystąpienia tego class_type.

    Uwaga: class_type zawsze będzie jednym z System.Object, System.ValueType lub System.Enumuwaga końcowa

  • Gdy element członkowski funkcji jest implementacją elementu członkowskiego funkcji interfejsu i jest wywoływany przez wyrażenie instancji interface_type.
  • Gdy element członkowski funkcji jest wywoływany za pośrednictwem delegata.

W takich sytuacjach opakowana instancja jest uznawana za zawierającą zmienną value_type, a ta zmienna staje się zmienną, do której odnosi się to przy wywołaniu członka funkcji.

Uwaga: w szczególności oznacza to, że gdy element funkcji jest wywoływany na opakowanym wystąpieniu, możliwe jest zmodyfikowanie wartości zawartej w opakowanym wystąpieniu. uwaga końcowa

12.7 Dekonstrukcja

Dekonstrukcja to proces polegający na tym, że wyrażenie przekształca się w krotkę poszczególnych wyrażeń. Dekonstrukcja jest używana, gdy element docelowy prostego przypisania jest wyrażeniem krotki, aby uzyskać wartości do przypisania do każdego z elementów tej krotki.

Wyrażenie E jest rozłożone do wyrażenia krotkowego z n elementami w następujący sposób:

  • Jeśli E jest wyrażeniem krotki z n elementami, wynikiem dekonstrukcji jest samo wyrażenie E.
  • W przeciwnym razie, jeśli E ma typ krotki (T1, ..., Tn) z elementami n, E jest obliczana w zmiennej tymczasowej __v, a wynikiem dekonstrukcji jest wyrażenie (__v.Item1, ..., __v.Itemn).
  • W przeciwnym razie, jeśli wyrażenie E.Deconstruct(out var __v1, ..., out var __vn) zostanie w czasie kompilacji rozpoznane jako unikatowa instancja lub metoda rozszerzenia, wyrażenie to zostaje ocenione, a wynikiem dekonstrukcji jest wyrażenie (__v1, ..., __vn). Taka metoda jest nazywana dekonstruktorem.
  • W przeciwnym razie nie można zdekonstruować E.

W tym miejscu __v i __v1, ..., __vn odnoszą się do innych niewidocznych i niedostępnych zmiennych tymczasowych.

Uwaga: nie można zdekonstruować wyrażenia typu dynamic. uwaga końcowa

12.8 Wyrażenia podstawowe

12.8.1 Ogólne

Wyrażenia podstawowe obejmują najprostsze formy wyrażeń.

primary_expression
    : primary_no_array_creation_expression
    | array_creation_expression
    ;

primary_no_array_creation_expression
    : literal
    | interpolated_string_expression
    | simple_name
    | parenthesized_expression
    | tuple_expression
    | member_access
    | null_conditional_member_access
    | invocation_expression
    | element_access
    | null_conditional_element_access
    | this_access
    | base_access
    | post_increment_expression
    | post_decrement_expression
    | null_forgiving_expression
    | object_creation_expression
    | delegate_creation_expression
    | anonymous_object_creation_expression
    | typeof_expression
    | sizeof_expression
    | checked_expression
    | unchecked_expression
    | default_value_expression
    | nameof_expression    
    | anonymous_method_expression
    | pointer_member_access     // unsafe code support
    | pointer_element_access    // unsafe code support
    | stackalloc_expression
    ;

Uwaga: te reguły gramatyczne nie są przystosowane do ANTLR, ponieważ są częścią zestawu wzajemnie leworęcznych reguł cyklicznych (primary_expression, primary_no_array_creation_expression, member_access, invocation_expression, element_access, post_increment_expression, post_decrement_expression, null_forgiving_expression, pointer_member_access i pointer_element_access), które ANTLR nie obsługuje. Standardowe techniki mogą służyć do przekształcania gramatyki w celu usunięcia wzajemnej rekursji po lewej stronie. Nie zostało to zrobione, ponieważ nie wszystkie strategie analizy wymagają tego (np. analizator LALR nie) i w ten sposób zaciemnia strukturę i opis. uwaga końcowa

pointer_member_access (§23.6.3) i pointer_element_access (§23.6.4) są dostępne tylko w niebezpiecznym kodzie (§23).

Wyrażenia podstawowe są dzielone między array_creation_expressions i primary_no_array_creation_expressions. Traktowanie array_creation_expression w ten sposób, a nie wymienianie go wraz z innymi prostymi formami wyrażeń, pozwala gramatyce zapobiegać potencjalnie mylącemu kodowi, takim jak

object o = new int[3][1];

które w przeciwnym razie byłyby interpretowane jako

object o = (new int[3])[1];

12.8.2 Literały

wyrażenie podstawowe, które składa się z literału (§6.4.5), jest klasyfikowane jako wartość.

12.8.3 Wyrażenia ciągów interpolowanych

interpolated_string_expression składa się z $, $@lub @$, po których następuje tekst oznaczony przez " znaków. W cytowanym tekście jest zero lub więcej interpolacji oddzielonych znakami { i }, z których każda zawiera wyrażenie i opcjonalne parametry formatowania.

Wyrażenia ciągów interpolowanych mają dwie formy: regularną (interpolated_regular_string_expression) i dosłowną (interpolated_verbatim_string_expression), które są leksykalicznie podobne do, ale różnią się semantycznie od dwóch form literałów ciągów (§6.4.5.6).

interpolated_string_expression
    : interpolated_regular_string_expression
    | interpolated_verbatim_string_expression
    ;

// interpolated regular string expressions

interpolated_regular_string_expression
    : Interpolated_Regular_String_Start Interpolated_Regular_String_Mid?
      ('{' regular_interpolation '}' Interpolated_Regular_String_Mid?)*
      Interpolated_Regular_String_End
    ;

regular_interpolation
    : expression (',' interpolation_minimum_width)?
      Regular_Interpolation_Format?
    ;

interpolation_minimum_width
    : constant_expression
    ;

Interpolated_Regular_String_Start
    : '$"'
    ;

// the following three lexical rules are context sensitive, see details below

Interpolated_Regular_String_Mid
    : Interpolated_Regular_String_Element+
    ;

Regular_Interpolation_Format
    : ':' Interpolated_Regular_String_Element+
    ;

Interpolated_Regular_String_End
    : '"'
    ;

fragment Interpolated_Regular_String_Element
    : Interpolated_Regular_String_Character
    | Simple_Escape_Sequence
    | Hexadecimal_Escape_Sequence
    | Unicode_Escape_Sequence
    | Open_Brace_Escape_Sequence
    | Close_Brace_Escape_Sequence
    ;

fragment Interpolated_Regular_String_Character
    // Any character except " (U+0022), \\ (U+005C),
    // { (U+007B), } (U+007D), and New_Line_Character.
    : ~["\\{}\u000D\u000A\u0085\u2028\u2029]
    ;

// interpolated verbatim string expressions

interpolated_verbatim_string_expression
    : Interpolated_Verbatim_String_Start Interpolated_Verbatim_String_Mid?
      ('{' verbatim_interpolation '}' Interpolated_Verbatim_String_Mid?)*
      Interpolated_Verbatim_String_End
    ;

verbatim_interpolation
    : expression (',' interpolation_minimum_width)?
      Verbatim_Interpolation_Format?
    ;

Interpolated_Verbatim_String_Start
    : '$@"'
    | '@$"'
    ;

// the following three lexical rules are context sensitive, see details below

Interpolated_Verbatim_String_Mid
    : Interpolated_Verbatim_String_Element+
    ;

Verbatim_Interpolation_Format
    : ':' Interpolated_Verbatim_String_Element+
    ;

Interpolated_Verbatim_String_End
    : '"'
    ;

fragment Interpolated_Verbatim_String_Element
    : Interpolated_Verbatim_String_Character
    | Quote_Escape_Sequence
    | Open_Brace_Escape_Sequence
    | Close_Brace_Escape_Sequence
    ;

fragment Interpolated_Verbatim_String_Character
    : ~["{}]    // Any character except " (U+0022), { (U+007B) and } (U+007D)
    ;

// lexical fragments used by both regular and verbatim interpolated strings

fragment Open_Brace_Escape_Sequence
    : '{{'
    ;

fragment Close_Brace_Escape_Sequence
    : '}}'
    ;

Sześć reguł leksykalnych zdefiniowanych powyżej jest kontekstowych w następujący sposób:

Reguła wymagania kontekstowe
Interpolated_Regular_String_Mid Rozpoznawane tylko po Interpolated_Regular_String_Start, pomiędzy kolejnymi interpolacjami, a przed odpowiednim Interpolated_Regular_String_End.
Regular_Interpolation_Format Uznawane tylko w regular_interpolation i gdy dwukropek na początku (:) nie jest zagnieżdżony w żadnym rodzaju nawiasu (okrągłych/klamrowych/kwadratowych).
Interpolated_Regular_String_End Rozpoznawane tylko po Interpolated_Regular_String_Start i tylko wtedy, gdy wszystkie interweniujące tokeny albo są tokenami Interpolated_Regular_String_Mid, albo mogą być częścią regular_interpolation, w tym tokenów dla dowolnych interpolated_regular_string_expressionzawartych w takich interpolacjach.
Interpolated_Verbatim_String_MidVerbatim_Interpolation_FormatInterpolated_Verbatim_String_End Uznanie tych trzech reguł jest zgodne z odpowiednimi regułami powyżej, z których każda wymieniona regularna reguła gramatyki zastąpiona odpowiednią dosłowną.

Uwaga: powyższe reguły są kontekstowe, ponieważ ich definicje nakładają się na inne tokeny w języku. uwaga końcowa

Uwaga: Powyższa gramatyka nie jest gotowa do użycia z ANTLR ze względu na kontekstowe reguły leksykalne. Podobnie jak w przypadku innych generatorów leksykalnych, ANTLR obsługuje reguły leksykalne wrażliwe na kontekst, na przykład przy użyciu trybów leksykalnych , ale jest to szczegół implementacji, i dlatego nie jest częścią tej specyfikacji. uwaga końcowa

interpolated_string_expression jest klasyfikowana jako wartość. Jeśli jest on natychmiast konwertowany na System.IFormattable lub System.FormattableString z niejawną konwersją ciągów interpolowanych (§10.2.5), wyrażenie ciągu interpolowanego ma ten typ. W przeciwnym razie ma typ string.

Uwaga: Różnice między możliwymi typami wyrażenia interpolated_string_expression można określić na podstawie dokumentacji System.String (§C.2) i System.FormattableString (§C.3). uwaga końcowa

Znaczenie interpolacji, zarówno regular_interpolation, jak i verbatim_interpolation, ma formatować wartość wyrażenia jako string zgodnie z formatem określonym przez Regular_Interpolation_Format lub Verbatim_Interpolation_Formatlub zgodnie z domyślnym formatem dla typu wyrażenia . Sformatowany ciąg jest następnie, jeśli istnieje, modyfikowany przez interpolation_minimum_width, aby utworzyć ostateczną string do interpolowania w interpolated_string_expression.

pl-PL: Uwaga: sposób określania domyślnego formatu typu jest szczegółowo opisany w dokumentacji dla System.String (§C.2) i System.FormattableString (§C.3). Opisy formatów standardowych, które są identyczne dla Regular_Interpolation_Format i Verbatim_Interpolation_Format, można znaleźć w dokumentacji System.IFormattable (§C.4) i w innych typach w bibliotece standardowej (§C). uwaga końcowa

W interpolation_minimum_widthconstant_expression powinna mieć niejawną konwersję do int. Niech szerokość pola będzie wartością bezwzględną tego wyrażenia stałej, a wyrównanie niech będzie znakiem (dodatnim lub ujemnym) wartości tego wyrażenia stałej:

  • Jeśli wartość szerokości pola jest mniejsza lub równa długości sformatowanego ciągu, sformatowany ciąg nie jest modyfikowany.
  • W przeciwnym razie sformatowany ciąg jest uzupełniony spacjami, tak aby jego długość była równa szerokości pola.
    • Jeśli wyrównanie jest dodatnie, sformatowany ciąg jest wyrównany do prawej przez dodanie wypełnienia na początku.
    • W przeciwnym razie jest wyrównany do lewej poprzez dodanie wypełnienia.

Ogólne znaczenie interpolated_string_expression, w tym powyższe formatowanie i wypełnienie interpolacji, jest definiowane przez konwersję wyrażenia na wywołanie metody: jeśli typ wyrażenia to System.IFormattable lub System.FormattableString, metodą jest System.Runtime.CompilerServices.FormattableStringFactory.Create (§C.3), która zwraca wartość typu System.FormattableString; w przeciwnym razie typem jest string, a metodą jest string.Format (§C.2), która zwraca wartość typu string.

W obu przypadkach lista argumentów wywołania składa się z literału ciągu formatu ze specyfikacjami formatu dla każdej interpolacji oraz z argumentów dla każdego wyrażenia odpowiadającego specyfikacjom formatu.

Literał ciągu formatu jest konstruowany w następujący sposób, gdzie N jest liczbą interpolacji w wyrażeniu ciągu interpolowanego . Literał ciągu formatu składa się z następującej kolejności:

  • Znaki Interpolated_Regular_String_Start lub Interpolated_Verbatim_String_Start
  • Znaki Interpolated_Regular_String_Mid lub Interpolated_Verbatim_String_Mid, o ile istnieją
  • Następnie, jeśli N ≥ 1 dla każdej liczby I z 0 do N-1:
    • Specyfikacja zastępnika:
      • Lewy nawias klamrowy ({) znak
      • Reprezentacja dziesiętna liczby I
      • Następnie, jeśli odpowiadająca regular_interpolation lub verbatim_interpolation ma interpolation_minimum_width, pojawia się przecinek (,), po którym następuje reprezentacja dziesiętna wartości constant_expression
      • Znaki Regular_Interpolation_Format lub Verbatim_Interpolation_Format, jeśli istnieją, odpowiednich regular_interpolation lub verbatim_interpolation
      • Znak prawego nawiasu klamrowego (})
    • Znaki Interpolated_Regular_String_Mid lub Interpolated_Verbatim_String_Mid bezpośrednio po odpowiedniej interpolacji, jeśli istnieją
  • Na koniec znaki Interpolated_Regular_String_End lub Interpolated_Verbatim_String_End.

Kolejne argumenty to wyrażenia z interpolacji, w kolejności, jeśli istnieją.

Gdy interpolated_string_expression zawiera wiele interpolacji, wyrażenia w tych interpolacjach są oceniane w kolejności tekstowej od lewej do prawej.

Przykład:

W tym przykładzie użyto następujących funkcji specyfikacji formatu:

  • specyfikacja formatu X, która formatuje liczby całkowite jako wielkie cyfry szesnastkowe,
  • domyślnym formatem wartości string jest sama wartość,
  • wartości wyrównania dodatniego, które wyrównują do prawej w ramach określonej minimalnej szerokości pola
  • ujemne wartości wyrównania, które wyrównują do lewej w określonej minimalnej szerokości pola,
  • zdefiniowane stałe dla interpolation_minimum_widthi
  • {{ i }} są formatowane odpowiednio jako { i }.

Dane:

string text = "red";
int number = 14;
const int width = -4;

Wtedy:

wyrażenie ciągu interpolowanego równoważne znaczenie jako string wartości
$"{text}" string.Format("{0}", text) "red"
$"{{text}}" string.Format("{{text}}) "{text}"
$"{ text , 4 }" string.Format("{0,4}", text) " red"
$"{ text , width }" string.Format("{0,-4}", text) "red "
$"{number:X}" string.Format("{0:X}", number) "E"
$"{text + '?'} {number % 3}" string.Format("{0} {1}", text + '?', number % 3) "red? 2"
$"{text + $"[{number}]"}" string.Format("{0}", text + string.Format("[{0}]", number)) "red[14]"
$"{(number==0?"Zero":"Non-zero")}" string.Format("{0}", (number==0?"Zero":"Non-zero")) "Non-zero"

koniec przykładu

12.8.4 Proste nazwy

simple_name składa się z identyfikatora, po którym opcjonalnie następuje lista argumentów typu:

simple_name
    : identifier type_argument_list?
    ;

simple_name jest I formularza lub I<A₁, ..., Aₑ>formularza, gdzie I jest pojedynczym identyfikatorem, a I<A₁, ..., Aₑ> jest opcjonalnym type_argument_list. Jeśli nie określono type_argument_list, należy przyjąć, że e ma wartość zero. simple_name jest oceniany i klasyfikowany w następujący sposób:

  • Jeśli e ma wartość zero, a simple_name pojawia się w przestrzeni deklaracji zmiennej lokalnej (§7.3), która bezpośrednio zawiera zmienną lokalną, parametr lub stałą o nazwie I, simple_name odnosi się do tej zmiennej lokalnej, parametru lub stałej i jest klasyfikowana jako zmienna lub wartość.
  • Jeśli e ma wartość zero, a simple_name pojawia się w deklaracji metody ogólnej, ale poza atrybutami method_declaration, a jeśli ta deklaracja zawiera parametr typu o nazwie I, simple_name odnosi się do tego parametru typu.
  • W przeciwnym razie dla każdego typu wystąpienia T (§15.3.2), począwszy od typu wystąpienia deklaracji bezpośrednio otaczającego typu i kontynuując z typem wystąpienia każdej otaczającej klasy lub deklaracji struktury (jeśli taka istnieje):
    • Jeśli e ma wartość zero, a deklaracja T zawiera parametr typu o nazwie I, simple_name odnosi się do tego parametru typu.
    • W przeciwnym razie, jeśli wyszukiwanie członka (§12.5) I w T z argumentami typu e generuje dopasowanie:
      • Jeśli T jest typem wystąpienia natychmiast otaczającej klasy lub typu struktury, a wyszukiwanie identyfikuje jedną lub więcej metod, wynikiem jest grupa metod ze skojarzonym wyrażeniem wystąpienia this. Jeśli określono listę argumentów typu, jest ona używana w wywoływaniu metody ogólnej (§12.8.10.2).
      • W przeciwnym razie, jeśli T jest typem wystąpienia bezpośrednio otaczającej klasy lub typu struktury, gdy odnośnik identyfikuje element członkowski wystąpienia i odwołanie występuje w bloku konstruktora wystąpienia, metody wystąpienia lub metody dostępu wystąpienia (§12.2.1), wynik jest taki sam jak dostęp do elementu członkowskiego (§12.8.7) w formularzu this.I. Może się to zdarzyć tylko wtedy, gdy e wynosi zero.
      • W przeciwnym razie wynik jest taki sam jak dostęp do elementu członkowskiego (§12.8.7) w formie T.I lub T.I<A₁, ..., Aₑ>.
  • W przeciwnym razie dla każdej przestrzeni nazw N, począwszy od przestrzeni nazw, w której występuje simple_name, kontynuując każdą otaczającą przestrzeń nazw (jeśli istnieje), a kończąc na globalnej przestrzeni nazw, następujące kroki są oceniane do momentu zlokalizowania jednostki:
    • Jeśli e ma wartość zero, a I jest nazwą przestrzeni nazw w N, wówczas:
      • Jeśli lokalizacja, w której występuje simple_name, jest ujęta przez deklarację przestrzeni nazw dla N, a deklaracja przestrzeni nazw zawiera extern_alias_directive lub using_alias_directive, która kojarzy nazwę I z przestrzenią nazw lub typem, simple_name jest niejednoznaczny i występuje błąd czasu kompilacji.
      • W przeciwnym razie simple_name odnosi się do przestrzeni nazw o nazwie I w N.
    • W przeciwnym razie jeśli N zawiera dostępny typ o nazwie I i parametrach typu e, wówczas:
      • Jeśli e ma wartość zero, a lokalizacja, w której występuje simple_name, jest zawarta w deklaracji przestrzeni nazw dla N, a deklaracja przestrzeni nazw zawiera extern_alias_directive lub using_alias_directive, które kojarzą nazwę I z przestrzenią nazw lub typem, wtedy simple_name jest niejednoznaczna i powoduje błąd czasu kompilacji.
      • W przeciwnym razie namespace_or_type_name odnosi się do typu skonstruowanego przy użyciu podanych argumentów typu.
    • W przeciwnym razie, jeśli lokalizacja, w której występuje simple_name, jest ujęta przez deklarację przestrzeni nazw dla N:
      • Jeśli e ma wartość zero, a deklaracja przestrzeni nazw zawiera extern_alias_directive lub using_alias_directive, które kojarzą nazwę I z zaimportowaną przestrzenią nazw lub typem, to simple_name odnosi się do tej przestrzeni nazw lub typu.
      • W przeciwnym razie, jeśli przestrzenie nazw zaimportowane poprzez using_namespace_directives w deklaracji przestrzeni nazw zawierają dokładnie jeden typ o nazwie I i z e parametrami typu, to simple_name odnosi się do tego typu, skonstruowanego przy użyciu podanych argumentów dotyczących typów.
      • W przeciwnym razie, jeśli przestrzenie nazw zaimportowane przez using_namespace_directivedeklaracji przestrzeni nazw zawierają więcej niż jeden typ o nazwie I i parametry typu e, simple_name jest niejednoznaczny i wystąpi błąd czasu kompilacji.

    Uwaga: cały krok jest dokładnie równoległy do odpowiedniego kroku przetwarzania namespace_or_type_name (§7.8). uwaga końcowa

  • W przeciwnym razie, jeśli ma wartość zero, a jest identyfikatorem , simple_name jest prostymodrzucania, który jest formą wyrażenia deklaracji (§12.17).
  • W przeciwnym razie simple_name jest niezdefiniowany i występuje błąd czasu kompilacji.

12.8.5 Wyrażenia w nawiasach

parenthesized_expression składa się z wyrażenia ujętego w nawiasy.

parenthesized_expression
    : '(' expression ')'
    ;

parenthesized_expression jest obliczana przez ocenę wyrażenia w nawiasach. Jeśli wyrażenie w nawiasach oznacza przestrzeń nazw lub typ, wystąpi błąd czasu kompilacji. W przeciwnym razie wynikiem parenthesized_expression jest ocena zawartego wyrażenia .

12.8.6 Wyrażenia krotki

tuple_expression reprezentuje krotkę i składa się z co najmniej dwóch rozdzielonych przecinkami i opcjonalnie nazwanych wyrażeń ujętych w nawiasy. deconstruction_expression to skrócona składnia krotki zawierającej niejawnie wpisane wyrażenia deklaracji.

tuple_expression
    : '(' tuple_element (',' tuple_element)+ ')'
    | deconstruction_expression
    ;
    
tuple_element
    : (identifier ':')? expression
    ;
    
deconstruction_expression
    : 'var' deconstruction_tuple
    ;
    
deconstruction_tuple
    : '(' deconstruction_element (',' deconstruction_element)+ ')'
    ;

deconstruction_element
    : deconstruction_tuple
    | identifier
    ;

tuple_expression jest klasyfikowana jako krotka.

deconstruction_expressionvar (e1, ..., en) jest skrótem od tuple_expression(var e1, ..., var en) i działa na tej samej zasadzie. Ma to zastosowanie rekursywnie do wszystkich zagnieżdżonych deconstruction_tuples w deconstruction_expression. Każdy identyfikator zagnieżdżony w deconstruction_expression w ten sposób wprowadza wyrażenie deklaracji (§12.17). W wyniku tego deconstruction_expression może wystąpić tylko po lewej stronie prostego przypisania.

Wyrażenia krotki mają typ wtedy i tylko wtedy, gdy każde z jego wyrażenie elementu Ei ma typ Ti. Typ musi być typem krotki o tej samej liczbie argumentów co wyrażenie krotki, gdzie każdy element jest podawany następująco:

  • Jeśli element krotki w odpowiedniej pozycji ma nazwę Ni, to element typu tej krotki powinien być Ti Ni.
  • W przeciwnym razie, jeśli Ei ma formę Ni lub E.Ni lub E?.Ni, to element typu krotki będzie Ti Ni, , chyba że dotyczy któregokolwiek z następujących:
    • Inny element wyrażenia krotki ma nazwę Ni, lub
    • Inny element krotki bez nazwy ma wyrażenie elementu krotki w formie Ni, E.Ni lub E?.Ni.
    • Ni ma postać ItemX, gdzie X jest sekwencją cyfr dziesiętnych, które nie są inicjowane przez0i które mogą reprezentować pozycję elementu krotki, a X nie reprezentuje pozycji elementu.
  • W przeciwnym razie element typu krotki to będzie Ti.

Wyrażenie krotki jest oceniane poprzez obliczenie każdego z wyrażeń elementu w kolejności od lewej do prawej.

Wartość krotki można uzyskać z wyrażenia krotki, konwertując ją na typ krotki (§10.2.13), przeklasyfikowując ją na wartość (§12.2.2) lub używając jej jako celu przypisania dekonstrukcji (§12.21.2).

Przykład:

(int i, string) t1 = (i: 1, "One");
(long l, string) t2 = (l: 2, null);
var t3 = (i: 3, "Three");          // (int i, string)
var t4 = (i: 4, null);             // Error: no type

W tym przykładzie wszystkie cztery wyrażenia krotek są prawidłowe. Pierwsze dwa, t1 i t2, nie używają typu wyrażenia krotki, ale zamiast tego stosują niejawną konwersję krotki. W przypadku t2niejawna konwersja krotki opiera się na niejawnych konwersjach z 2 na long i z null do string. Trzecie wyrażenie krotki ma typ (int i, string), dlatego można je ponownie sklasyfikować jako wartość tego typu. Z drugiej strony deklaracja t4jest błędem: Wyrażenie krotki nie ma typu, ponieważ jego drugi element nie ma typu.

if ((x, y).Equals((1, 2))) { ... };

W tym przykładzie pokazano, że krotki mogą czasami prowadzić do wielu warstw nawiasów, zwłaszcza gdy wyrażenie krotki jest jedynym argumentem wywołania metody.

koniec przykładu

12.8.7 Dostęp do składowych

12.8.7.1 Ogólne

member_access składa się z primary_expression, predefined_typelub qualified_alias_member, po których następuje token ".", a dalej identyfikator , opcjonalnie zakończony przez type_argument_list.

member_access
    : primary_expression '.' identifier type_argument_list?
    | predefined_type '.' identifier type_argument_list?
    | qualified_alias_member '.' identifier type_argument_list?
    ;

predefined_type
    : 'bool' | 'byte' | 'char' | 'decimal' | 'double' | 'float' | 'int'
    | 'long' | 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong'
    | 'ushort'
    ;

Produkcja qualified_alias_member jest zdefiniowana w §14.8.

member_access ma postać E.I lub E.I<A₁, ..., Aₑ>, w przypadku gdy E jest wyrażeniem podstawowym, typem predefiniowanym lub zakwalifikowanym członkiem aliasu,I jest pojedynczym identyfikatorem, a <A₁, ..., Aₑ> jest opcjonalną listą argumentów typu. Jeśli nie określono type_argument_list, należy przyjąć, że e ma wartość zero.

member_access z primary_expression typu dynamic jest dynamicznie powiązana (§12.3.3). W tym przypadku kompilator klasyfikuje dostęp do elementu jako dostęp do właściwości typu dynamic. Poniższe reguły do określenia znaczenia member_access są stosowane w czasie wykonywania, używając typu wykorzystywanego w czasie wykonania zamiast typu wykorzystywanego w czasie kompilacji primary_expression. Jeśli ta klasyfikacja w czasie wykonywania prowadzi do grupy metod, to dostęp do składowych powinien być primary_expressioninvocation_expression.

member_access ocenia się i klasyfikuje w następujący sposób:

  • Jeśli e ma wartość zero, a E jest przestrzenią nazw, a E zawiera zagnieżdżoną przestrzeń nazw o nazwie I, rezultatem jest ta przestrzeń nazw.
  • W przeciwnym razie, jeśli E jest przestrzenią nazw, a E zawiera dostępny typ o nazwie I z K parametrami typu, wówczas wynikowym typem jest ten skonstruowany z podanymi argumentami typu.
  • Jeśli E jest klasyfikowany jako typ, jeśli E nie jest parametrem typu, a jeśli przeszukiwanie składnika (§12.5) I w E z parametrami typu K daje wynik, E.I jest oceniane i klasyfikowane w następujący sposób:

    Uwaga: gdy wynik takiego wyszukiwania składowego jest grupą metod i K wynosi zero, grupa metod może zawierać metody o parametrach typu. Dzięki temu takie metody mogą być brane pod uwagę podczas wnioskowania argumentów typu. uwaga końcowa

    • Jeśli I zidentyfikuje typ, wynikiem jest typ skonstruowany z dowolnymi argumentami typu.
    • Jeśli I zidentyfikuje jedną lub więcej metod, wynik jest grupą metod bez skojarzonego wyrażenia wystąpienia.
    • Jeśli I zidentyfikuje właściwość statyczną, wynik jest dostępem do właściwości bez skojarzonego wyrażenia wystąpienia.
    • Jeśli I zidentyfikuje pole statyczne:
      • Jeśli pole jest tylko do odczytu, a odwołanie występuje poza konstruktorem statycznym klasy lub struktury, w której to pole jest zadeklarowane, wtedy wynik jest wartością, a mianowicie wartością statycznego pola I w E.
      • W przeciwnym razie wynikiem jest zmienna, czyli pole statyczne I w E.
    • Jeśli I zidentyfikuje zdarzenie statyczne:
      • Jeśli odwołanie występuje w klasie lub strukturze, w której zadeklarowano zdarzenie, a zdarzenie zostało zadeklarowane bez event_accessor_declarations (§15.8.1), E.I jest traktowane dokładnie tak, jakby I było polem statycznym.
      • W przeciwnym razie wynik to dostęp do zdarzenia bez skojarzonego wyrażenia instancji.
    • Jeśli I identyfikuje stałą, wynik jest wartością, a mianowicie wartością tej stałej.
    • Jeśli I zidentyfikuje element członkowski wyliczenia, wynik jest wartością, a mianowicie wartością tego elementu członkowskiego wyliczenia.
    • W przeciwnym razie E.I jest nieprawidłowym odwołaniem do składowych i występuje błąd czasu kompilacji.
  • Jeśli E jest dostępem do właściwości, dostępem indeksatora, zmienną lub wartością, którego typ to T, a wyszukiwanie składowej (§12.5) I w T z argumentami typu K generuje dopasowanie, a następnie E.I jest oceniany i klasyfikowany w następujący sposób:
    • Po pierwsze, jeśli E jest dostępem do właściwości lub indeksatora, zostanie uzyskana wartość właściwości lub indeksatora (§12.2.2) i E zostanie ponownie sklasyfikowana jako wartość.
    • Jeśli I zidentyfikuje co najmniej jedną metodę, wynikiem jest grupa metod ze skojarzonym wyrażeniem wystąpienia E.
    • Jeśli I identyfikuje właściwość instancji, to wynikiem jest dostęp do tej właściwości z przypisanym wyrażeniem instancji E i przypisanym typem, który jest typem tej właściwości. Jeśli T jest typem klasy, skojarzony typ jest wybierany z pierwszej deklaracji lub zastępowania właściwości znalezionej podczas rozpoczynania od Ti przeszukiwania jej klas bazowych.
    • Jeśli T jest class_type i I identyfikuje pole wystąpienia tego class_type:
      • Jeśli wartość E jest null, wtedy zostanie rzucony System.NullReferenceException.
      • W przeciwnym razie, jeśli pole jest tylko do odczytu i odwołanie występuje poza konstruktorem wystąpienia klasy, w której pole jest zadeklarowane, to wynikiem jest wartość tego pola I w obiekcie, do którego odnosi się E.
      • W przeciwnym razie wynikiem jest zmienna, czyli pole I w obiekcie, do których odwołuje się E.
    • Jeśli T jest struct_type i I identyfikuje pole wystąpienia tego struct_type:
      • Jeśli E jest wartością lub jeśli pole jest tylko do odczytu, a odwołanie do pola występuje poza konstruktorem instancji struktury, w której pole jest zadeklarowane, to wynik jest wartością, a mianowicie wartością pola I w instancji struktury podanej przez E.
      • W przeciwnym razie wynik jest zmienną, czyli pole I w wystąpieniu struktury podanym przez E.
    • Jeśli I zidentyfikuje zdarzenie wystąpienia:
      • Jeśli odwołanie występuje w klasie lub struktur, w której zadeklarowano zdarzenie, a zdarzenie zostało zadeklarowane bez event_accessor_declarations (§15.8.1), a odwołanie nie występuje jako lewa strona operatora a += lub -=, E.I jest przetwarzane dokładnie tak, jakby I było polem wystąpienia.
      • W przeciwnym razie wynikiem jest dostęp do zdarzenia z przypisanym wyrażeniem instancji E.
  • W przeciwnym razie podejmowana jest próba przetworzenia E.I jako wywołania metody rozszerzenia (§12.8.10.3). Jeśli to się nie powiedzie, E.I jest nieprawidłowym odwołaniem do elementu członkowskiego, co powoduje błąd powiązania w czasie.

12.8.7.2 Identyczne proste nazwy i nazwy typów

W przypadku dostępu do elementu członkowskiego formularza E.I, jeśli E jest pojedynczym identyfikatorem, a znaczenie E jako simple_name (§12.8.4) jest stałym, polem, właściwością, zmienną lokalną lub parametrem o tym samym typie co znaczenie E co type_name (§7.8.1), następnie dozwolone są oba możliwe znaczenia E. Wyszukiwanie członka E.I nigdy nie jest niejednoznaczne, ponieważ I musi być członkiem typu E w obu przypadkach. Innymi słowy, reguła po prostu zezwala na dostęp do statycznych elementów członkowskich i zagnieżdżonych typów E, w sytuacji gdy w przeciwnym razie wystąpiłby błąd czasu kompilacji.

Przykład:

struct Color
{
    public static readonly Color White = new Color(...);
    public static readonly Color Black = new Color(...);
    public Color Complement() => new Color(...);
}

class A
{
    public «Color» Color;              // Field Color of type Color

    void F()
    {
        Color = «Color».Black;         // Refers to Color.Black static member
        Color = Color.Complement();  // Invokes Complement() on Color field
    }

    static void G()
    {
        «Color» c = «Color».White;       // Refers to Color.White static member
    }
}

Tylko w celach wyjaśniających, w klasie A te wystąpienia identyfikatora Color, które odwołują się do typu Color, są oddzielane za pomocą «...», a te, które odwołują się do pola Color, nie są.

koniec przykładu

12.8.8 Dostęp warunkowy o wartości null

null_conditional_member_access jest wersją warunkową member_access (§12.8.7) i jest to błąd czasu powiązania, jeśli typ wyniku jest void. W przypadku wyrażenia warunkowego o wartości null, w którym typ wyniku może być void zobacz (§12.8.11).

null_conditional_member_access składa się z primary_expression, po którym następują dwa tokeny „?” i „.”, a następnie identyfikator z opcjonalnym type_argument_list, po którym następuje zero lub więcej dependent_access, z których dowolny może być poprzedzony operatorem null_forgiving_operator.

null_conditional_member_access
    : primary_expression '?' '.' identifier type_argument_list?
      (null_forgiving_operator? dependent_access)*
    ;
    
dependent_access
    : '.' identifier type_argument_list?    // member access
    | '[' argument_list ']'                 // element access
    | '(' argument_list? ')'                // invocation
    ;

null_conditional_projection_initializer
    : primary_expression '?' '.' identifier type_argument_list?
    ;

Wyrażenie null_conditional_member_accessE ma formę P?.A. Znaczenie E jest określane w następujący sposób:

  • Jeśli typ P jest typem wartości dopuszczającym wartość null:

    Niech T będzie typem P.Value.A.

    • Jeśli T jest parametrem typu, który nie jest znany jako typ referencyjny lub typ wartości niemogący przyjąć wartości null, wystąpi błąd czasu kompilacji.

    • Jeśli T jest typem wartości, który nie dopuszcza wartości null, to typ E jest T?, i znaczenie E jest takie samo jak znaczenie:

      ((object)P == null) ? (T?)null : P.Value.A
      

      Z tą różnicą, że P jest obliczana tylko raz.

    • W przeciwnym razie typ E jest T, a znaczenie E jest takie samo jak znaczenie:

      ((object)P == null) ? (T)null : P.Value.A
      

      Z tą różnicą, że P jest obliczana tylko raz.

  • Inaczej:

    Niech T być typem wyrażenia P.A.

    • Jeśli T jest parametrem typu, który nie jest znany jako typ referencyjny lub typ wartości niemogący przyjąć wartości null, wystąpi błąd czasu kompilacji.

    • Jeśli T jest typem wartości, który nie dopuszcza wartości null, to typ E jest T?, i znaczenie E jest takie samo jak znaczenie:

      ((object)P == null) ? (T?)null : P.A
      

      Z tą różnicą, że P jest obliczana tylko raz.

    • W przeciwnym razie typ E jest T, a znaczenie E jest takie samo jak znaczenie:

      ((object)P == null) ? (T)null : P.A
      

      Z tą różnicą, że P jest obliczana tylko raz.

Uwaga: w wyrażeniu formularza:

P?.A₀?.A₁

następnie, jeżeli P jest obliczany na null, ani A₀, ani A₁ nie są obliczane. Dotyczy to także sytuacji, gdy wyrażenie stanowi sekwencję null_conditional_member_access lub null_conditional_element_access§12.8.13 operacji.

uwaga końcowa

null_conditional_projection_initializer stanowi ograniczenie null_conditional_member_access i ma tę samą semantykę. Występuje on tylko jako inicjator projekcji w wyrażeniu tworzenia obiektu anonimowego (§12.8.17.7).

12.8.9 Wyrażenia typu null forgiving

12.8.9.1 Ogólne

Wartość, typ, klasyfikacja wyrażenia o wartości null (§12.2) i bezpieczny kontekst (§16.4.12) to wartość, typ, klasyfikacja i bezpieczny kontekst jego primary_expression.

null_forgiving_expression
    : primary_expression null_forgiving_operator
    ;

null_forgiving_operator
    : '!'
    ;

Uwaga: Postfiksowy operator ignorowania wartości null i prefiksowy operator negacji logicznej (§12.9.4), mimo że są reprezentowane przez ten sam token leksykalny (!), są odrębne. Tylko ten ostatni może być zastępowany (§15.10), definicja operatora akceptującego null jest stała. uwaga końcowa

Jest to błąd czasu kompilacji stosowanie operatora odrzucania null więcej niż raz do tego samego wyrażenia, niezależnie od nawiasów interweniujących.

Przykładowy: wszystkie następujące elementy są nieprawidłowe:

var p = q!!;            // error: applying null_forgiving_operator more than once
var s = ( ( m(t) ! ) )! // error: null_forgiving_operator applied twice to m(t)

koniec przykładu

Pozostała część tego podpunktu i następujące podpunkty równorzędne są warunkowo normatywne.

Kompilator wykonujący statyczną analizę stanu null (§8.9.5) musi być zgodny z następującą specyfikacją.

Operator niwelujący null to pseudooperacja wykonywana podczas kompilacji, która służy do informowania kompilatora o analizie statycznego stanu null. Ma dwa zastosowania: przysłonić określenie kompilatora, że wyrażenie może mieć wartość null, oraz przysłonić ostrzeżenie kompilatora dotyczące nullowalności.

Zastosowanie operatora forgiving o wartości null do wyrażenia, dla którego statyczna analiza stanu null kompilatora nie generuje żadnych ostrzeżeń, nie jest błędem.

12.8.9.2 Zastąpienie określenia "może być null"

W niektórych okolicznościach statyczna analiza stanu null kompilatora może określić, że wyrażenie ma stan null może mieć wartość null i wydać ostrzeżenie diagnostyczne, gdy inne informacje wskazują, że wyrażenie nie może mieć wartości null. Zastosowanie operatora wyciszającego null do takiego wyrażenia informuje statyczną analizę stanu null kompilatora, że stan null jest w , a nie w stanie null; co zapobiega ostrzeżeniu diagnostycznemu i może wpływać na bieżącą analizę.

Przykład: rozważ następujące kwestie:

#nullable enable
public static void M()
{
    Person? p = Find("John");                  // returns Person?
    if (IsValid(p))
    {
       Console.WriteLine($"Found {p!.Name}");  // p can't be null
    }
}

public static bool IsValid(Person? person) =>
    person != null && person.Name != null;

Jeśli IsValid zwraca true, można bezpiecznie przeprowadzić dereferencję p, aby uzyskać dostęp do jego właściwości Name, a ostrzeżenie "dereferencja możliwej wartości null" można pominąć za pomocą !.

koniec przykładu

Przykład: Operator odpuszczający null powinien być używany z ostrożnością, należy rozważyć:

#nullable enable
int B(int? x)
{
    int y = (int)x!; // quash warning, throw at runtime if x is null
    return y;
}

W tym miejscu operator wybaczający wartość null jest stosowany do typu wartości i pomija wszelkie ostrzeżenia w x. Jeśli jednak x jest null w czasie wykonywania, zostanie zgłoszony wyjątek, ponieważ null nie można rzutować na int.

koniec przykładu

12.8.9.3 Przesłanianie innych ostrzeżeń analizy null

Oprócz zastępowania może być nullem ustaleń, jak było to wspomniane wcześniej, mogą istnieć inne okoliczności, w których konieczne jest zastąpienie wyników statycznej analizy przez kompilator, które wskazują, że wyrażenie wymaga co najmniej jednego ostrzeżenia. Zastosowanie operatora pozwalającego na wartości null do takiego wyrażenia żąda, aby kompilator nie wystawiał żadnych ostrzeżeń dla tego wyrażenia. W odpowiedzi kompilator może nie wydawać ostrzeżeń, a także zmodyfikować jego dalszą analizę.

Przykład: rozważ następujące kwestie:

#nullable enable
public static void Assign(out string? lv, string? rv) { lv = rv; }

public string M(string? t)
{
    string s;
    Assign(out s!, t ?? "«argument was null»");
    return s;
}

Typy parametrów metody Assign, lv & rv, są string?, a lv jako parametr wyjściowy, który wykonuje proste przypisanie.

Metoda M przekazuje zmienną s, typu string, jako parametr wyjściowy Assign. Kompilator wydaje ostrzeżenie, ponieważ s nie jest zmienną, która może przyjmować wartość null. Ponieważ drugi argument Assignnie może mieć wartości null, używany jest operator pomijający null, aby usunąć ostrzeżenie.

koniec przykładu

Koniec warunkowo normatywnego tekstu.

12.8.10 Wyrażenia wywołania

12.8.10.1 Ogólne

Wyrażenie invocation_expression jest używane do wywoływania metody.

invocation_expression
    : primary_expression '(' argument_list? ')'
    ;

primary_expression może być null_forgiving_expression tylko wtedy, gdy ma delegate_type.

invocation_expression jest dynamicznie powiązany (§12.3.3), jeśli spełniony jest co najmniej jeden z następujących warunków:

  • primary_expression ma typ czasu kompilacji dynamic.
  • Co najmniej jeden argument opcjonalnego argument_list ma typ czasu kompilacji dynamic.

W tym przypadku kompilator klasyfikuje invocation_expression jako wartość typu dynamic. Poniższe reguły pozwalające określić znaczenie invocation_expression są następnie stosowane w czasie wykonywania, wykorzystuje się typ czasu wykonywania zamiast typu czasu kompilacji primary_expression i argumentów, które mają typ czasu kompilacji dynamic. Jeśli primary_expression nie ma typu czasu kompilacji dynamic, wywołanie metody przechodzi ograniczoną kontrolę czasu kompilacji zgodnie z opisem w §12.6.5.

primary_expression lub wartość invocation_expression musi być grupą metod lub wartością typu delegate_type. Jeśli primary_expression jest grupą metod, invocation_expression jest wywołaniem metody (§12.8.10.2). Jeśli primary_expression jest wartością typu delegate_type, invocation_expression jest wywołaniem delegata (§12.8.10.4). Jeśli primary_expression nie jest ani grupą metod, ani wartością typu delegate_type, wystąpi błąd w czasie wiązania.

Opcjonalny argument_list (§12.6.2) zawiera wartości lub odwołania do zmiennych dla parametrów metody.

Wynik oceny wyrażenia wywołania jest klasyfikowany w następujący sposób:

  • Jeśli invocation_expression wywołuje metodę, która nie zwraca wartości (§15.6.1) lub delegata, który nie zwraca wartości, wynik jest pusty. Wyrażenie sklasyfikowane jako nic nie jest dozwolone tylko w kontekście statement_expression (§13.7) lub jako treść lambda_expression (§12.19). W przeciwnym razie występuje błąd czasu wiązania.
  • W przeciwnym razie, jeśli wyrażenie invocation_expression wywołuje metodę zwracającą przez ref (§15.6.1) lub delegata zwracającego przez ref, wynikiem jest zmienna o typie związanym z typem zwracanym metody lub delegata. Jeśli wywołanie dotyczy metody instancji, a odbiornik jest typu klasy T, skojarzony typ jest wybierany z pierwszej deklaracji lub nadpisania metody znalezionej, zaczynając od klasy T i przeszukując jej klasy bazowe.
  • W przeciwnym razie wyrażenie wywołujące wywołuje metodę zwracającą przez wartość (§15.6.1) lub delegata zwracającego wartość, a wynik jest wartością o typie skojarzonym z typem zwracanym przez metodę lub delegata. Jeśli wywołanie dotyczy metody instancji, a odbiornik jest typu klasy T, skojarzony typ jest wybierany z pierwszej deklaracji lub nadpisania metody znalezionej, zaczynając od klasy T i przeszukując jej klasy bazowe.

Wywołania metod 12.8.10.2

W przypadku wywołania metody primary_expression wyrażenie invocation_expression powinno być określane jako grupa metod. Grupa metod identyfikuje jedną metodę do wywołania lub zestaw przeciążonych metod, z których należy wybrać określoną metodę do wywołania. W tym drugim przypadku określenie określonej metody wywoływania jest oparte na kontekście dostarczonym przez typy argumentów w argument_list.

Przetwarzanie czasu wiązania wywołania metody w formie M(A), gdzie M jest zestawem metod (prawdopodobnie obejmującym type_argument_list), a A jest opcjonalnym argument_list, składa się z następujących kroków:

  • Konstruowany jest zestaw metod kandydatów dla wywołania metody. Dla każdej metody F skojarzonej z grupą metod M:
    • Jeśli F nie jest ogólny, F jest kandydatem, gdy:
      • M nie ma listy argumentów typu i
      • F ma zastosowanie w odniesieniu do A (§12.6.4.2).
    • Jeśli F jest ogólny i M nie ma listy argumentów typu, F jest kandydatem, gdy:
      • Wnioskowanie typu (§12.6.3) kończy się powodzeniem, w wyniku czego wywnioskowano listę argumentów typu dla wywołania i
      • Po zastąpieniu argumentów typu wnioskowanego dla odpowiednich parametrów typu metody wszystkie typy skonstruowane na liście parametrów F spełniają swoje ograniczenia (§8.4.5), a lista parametrów F ma zastosowanie w odniesieniu do A (§12.6.4.2)
    • Jeśli F jest ogólny i M zawiera listę argumentów typu, F jest kandydatem, gdy:
      • F ma taką samą liczbę parametrów typu metody, jak podano na liście argumentów typu i
      • Po zastąpieniu argumentów typu odpowiednimi parametrami typu metody wszystkie skonstruowane typy na liście parametrów F spełniają swoje ograniczenia (§8.4.5), a lista parametrów F ma zastosowanie w odniesieniu do A (§12.6.4.2).
  • Zestaw metod kandydatów jest ograniczony do zawierania tylko metod z najbardziej pochodnych typów: dla każdej metody C.F w zestawie, gdzie C jest typem, w którym metoda F jest zadeklarowana, wszystkie metody zadeklarowane w podstawowym typie C są usuwane z zestawu. Ponadto, jeśli C jest typem klasy innym niż object, wszystkie metody zadeklarowane w typie interfejsu są usuwane z zestawu.

    Uwaga: ta ostatnia reguła ma wpływ tylko wtedy, gdy grupa metodowa była wynikiem wyszukiwania składowego dla parametru typu o efektywnej klasie bazowej innej niż object i niepustego zestawu efektywnych interfejsów. uwaga końcowa

  • Jeśli wynikowy zestaw metod kandydatów jest pusty, dalsze przetwarzanie w kolejnych krokach jest porzucane, a zamiast tego podejmowana jest próba przetworzenia wywołania jako wywołania metody rozszerzenia (§12.8.10.3). Jeśli to się nie powiedzie, nie istnieją odpowiednie metody i wystąpi błąd czasu powiązania.
  • Najlepsza metoda z zestawu metod kandydatów jest identyfikowana za pomocą reguł rozwiązywania przeciążenia §12.6.4. Jeśli nie można zidentyfikować jednej najlepszej metody, wywołanie metody jest niejednoznaczne i występuje błąd czasu powiązania. Podczas rozwiązywania przeciążeń parametry metody ogólnej są uwzględniane po podstawianiu argumentów typu (podanych lub wywnioskowanych) dla odpowiednich parametrów typu metody.

Po wybraniu i zweryfikowaniu metody na etapie wiązania przez powyższe kroki, rzeczywiste wywołanie w czasie wykonania jest przetwarzane zgodnie z regułami wywołania elementu członkowskiego funkcji opisanymi w §12.6.6.

Uwaga: Intuicyjny efekt reguł rozpoznawania opisanych powyżej jest następujący: Aby zlokalizować określoną metodę wywoływaną przez wywołanie metody, zacznij od typu określonego przez wywołanie metody i kontynuuj cały łańcuch dziedziczenia aż do momentu, gdy zostanie znaleziona co najmniej jedna odpowiednia, dostępna, niezastępująca deklaracja metody. Następnie wykonaj wnioskowanie typu i rozwiązywanie przeciążenia dla zestawu odpowiednich, dostępnych, nieprzesłanianych metod zadeklarowanych w tym typie i wywołaj w ten sposób wybraną metodę. Jeśli nie znaleziono metody, spróbuj przetworzyć wywołanie jako wywołanie metody rozszerzenia. uwaga końcowa

Wywołania metody rozszerzenia 12.8.10.3

W wywołaniu metody (§12.6.6.2) jednej z formularzy

«expr» . «identifier» ( )  
«expr» . «identifier» ( «args» )  
«expr» . «identifier» < «typeargs» > ( )  
«expr» . «identifier» < «typeargs» > ( «args» )

Jeśli normalne przetwarzanie wywołania nie znajdzie żadnych odpowiednich metod, zostanie podjęta próba zinterpretowania konstrukcji jako wywołania metody rozszerzenia. Jeśli «expr» lub dowolny z «args» ma typ kompilacji dynamic, metody rozszerzenia nie będą stosowane.

Celem jest znalezienie najlepszego type_nameC, aby można było wykonać odpowiednie wywołanie metody statycznej:

C . «identifier» ( «expr» )  
C . «identifier» ( «expr» , «args» )  
C . «identifier» < «typeargs» > ( «expr» )  
C . «identifier» < «typeargs» > ( «expr» , «args» )

Metoda rozszerzenia Cᵢ.Mₑ jest kwalifikuje się, jeśli:

  • Cᵢ to niegeneryczna, nienagnieżdżona klasa
  • Nazwa Mₑ to identyfikator
  • Mₑ jest dostępny i ma zastosowanie w przypadku zastosowania do argumentów jako metody statycznej, jak pokazano powyżej
  • Niejawna tożsamość, odwołanie lub konwersja boksu istnieje z do typu pierwszego parametru Mₑ.

Wyszukiwanie C przebiega w następujący sposób:

  • Począwszy od najbliższej otaczającej deklaracji przestrzeni nazw, kontynuując przez każdą otaczającą deklarację przestrzeni nazw, a kończąc na jednostce kompilacji, podejmowane są kolejne próby znalezienia potencjalnego zestawu metod rozszerzeń.
    • Jeśli dana przestrzeń nazw lub jednostka kompilacji zawiera bezpośrednio deklaracje typów nieogólnych Cᵢ z uprawnionymi metodami rozszerzenia Mₑ, to zestaw tych metod rozszerzenia jest zestawem kandydatów.
    • Jeśli przestrzenie nazw importowane przy użyciu dyrektyw przestrzeni nazw w danej przestrzeni nazw lub jednostce kompilacji bezpośrednio zawierają deklaracje typów innych niż ogólne Cᵢ z uprawnionymi metodami rozszerzenia Mₑ, zestaw tych metod rozszerzenia jest zestawem kandydatów.
  • Jeśli żaden zestaw kandydatów nie zostanie znaleziony w żadnej otaczającej deklaracji przestrzeni nazw lub jednostce kompilacji, wystąpi błąd czasu kompilacji.
  • W przeciwnym razie rozwiązanie przeciążenia jest stosowane do zestawu kandydatów zgodnie z opisem w §12.6.4. Jeśli nie zostanie znaleziona żadna najlepsza metoda, wystąpi błąd czasu kompilacji.
  • C jest typem, w którym najlepsza metoda jest zadeklarowana jako metoda rozszerzenia.

Kiedy C jest używany jako obiekt docelowy, wywołanie metody jest traktowane jako wywołanie metody statycznej (§12.6.6).

Uwaga: w przeciwieństwie do wywołania metody wystąpienia nie jest zgłaszany wyjątek, gdy wyrażenie oblicza odwołanie o wartości null. Zamiast tego ta wartość null jest przekazywana do metody rozszerzenia, tak jak w przypadku zwykłego wywołania metody statycznej. Implementacja metody rozszerzenia decyduje o tym, jak reagować na takie wywołanie. uwaga końcowa

Powyższe reguły oznaczają, że metody instancji mają pierwszeństwo przed metodami rozszerzeń, że metody rozszerzeń dostępne w deklaracjach przestrzeni nazw wewnętrznych mają pierwszeństwo przed metodami rozszerzeń dostępnymi w deklaracjach przestrzeni nazw zewnętrznych, a metody rozszerzeń zadeklarowane bezpośrednio w przestrzeni nazw mają pierwszeństwo przed metodami rozszerzeń importowanymi do tej samej przestrzeni nazw przy użyciu dyrektywy przestrzeni nazw.

Przykład:

public static class E
{
    public static void F(this object obj, int i) { }
    public static void F(this object obj, string s) { }
}

class A { }

class B
{
    public void F(int i) { }
}

class C
{
    public void F(object obj) { }
}

class X
{
    static void Test(A a, B b, C c)
    {
        a.F(1);            // E.F(object, int)
        a.F("hello");      // E.F(object, string)
        b.F(1);            // B.F(int)
        b.F("hello");      // E.F(object, string)
        c.F(1);            // C.F(object)
        c.F("hello");      // C.F(object)
    }
}

W tym przykładzie metoda Bma pierwszeństwo przed pierwszą metodą rozszerzenia, a Cmetoda ma pierwszeństwo przed obiem metodami rozszerzenia.

public static class C
{
    public static void F(this int i) => Console.WriteLine($"C.F({i})");
    public static void G(this int i) => Console.WriteLine($"C.G({i})");
    public static void H(this int i) => Console.WriteLine($"C.H({i})");
}

namespace N1
{
    public static class D
    {
        public static void F(this int i) => Console.WriteLine($"D.F({i})");
        public static void G(this int i) => Console.WriteLine($"D.G({i})");
    }
}

namespace N2
{
    using N1;

    public static class E
    {
        public static void F(this int i) => Console.WriteLine($"E.F({i})");
    }

    class Test
    {
        static void Main(string[] args)
        {
            1.F();
            2.G();
            3.H();
        }
    }
}

Dane wyjściowe tego przykładu to:

E.F(1)
D.G(2)
C.H(3)

D.G ma pierwszeństwo przed C.G, a E.F ma pierwszeństwo przed zarówno D.F, jak i C.F.

koniec przykładu

12.8.10.4 Wywołania delegata

W przypadku wywołania delegata primary_expressioninvocation_expression powinno być wartością delegate_type. Ponadto biorąc pod uwagę, że delegate_type być członkiem funkcji z tą samą listą parametrów co delegate_type, delegate_type mają zastosowanie (§12.6.4.2) w odniesieniu do argument_listinvocation_expression.

Przetwarzanie w czasie wykonywania wywołania delegata dla formularza D(A), gdzie D jest wyrażeniem pierwotnym (primary_expression) typu delegata (delegate_type), a A jest opcjonalną listą argumentów (argument_list), obejmuje następujące kroki:

  • D jest oceniana. Jeśli ta ocena powoduje wyjątek, nie są wykonywane żadne dalsze kroki.
  • Zostanie obliczona lista argumentów A. Jeśli ta ocena powoduje wyjątek, nie są wykonywane żadne dalsze kroki.
  • Wartość D jest sprawdzana jako prawidłowa. Jeśli wartość D jest równa null, zostanie wyrzucony System.NullReferenceException i żadne dalsze kroki nie zostaną wykonane.
  • W przeciwnym razie D jest odwołaniem do wystąpienia delegata. Wywołania elementów członkowskich funkcji (§12.6.6) są wykonywane na każdym z wywoływanych obiektów na liście wywołań elementów delegata. W przypadku wywoływalnych jednostek składających się z wystąpienia i metody wystąpienia, instancja używana do wywołania to ta zawarta w jednostce wywołującej.

Aby uzyskać szczegółowe informacje o wielu listach wywołań bez parametrów, zobacz §20.6.

12.8.11 Wyrażenie warunkowe wywołania przy null

null_conditional_invocation_expression jest składniowo albo wyrażeniem null_conditional_member_access (§12.8.8), lub wyrażeniem null_conditional_element_access (§12.8.13), gdzie końcowy dependent_access jest wyrażeniem wywoływania (§12.8.10).

null_conditional_invocation_expression występuje w kontekście statement_expression (§13.7), anonymous_function_body (§12.19.1) lub method_body (§15.6.1).

W przeciwieństwie do składni równoważnej null_conditional_member_access lub null_conditional_element_access, null_conditional_invocation_expression może być klasyfikowana jako nic.

null_conditional_invocation_expression
    : null_conditional_member_access null_forgiving_operator? '(' argument_list? ')'
    | null_conditional_element_access null_forgiving_operator? '(' argument_list? ')'
    ;

Opcjonalny null_forgiving_operator można uwzględnić tylko wtedy, gdy null_conditional_member_access lub null_conditional_element_access ma delegate_type.

Wyrażenie null_conditional_invocation_expressionE ma postać P?A; gdzie A jest resztą syntaktycznie równoważnej null_conditional_member_access lub null_conditional_element_access, A w związku z tym rozpocznie się od . lub [. Niech PA oznacza połączenie P i A.

Gdy E występuje jako wyrażenie , znaczenie E jest takie samo jak znaczenie wyrażenia .

if ((object)P != null) PA

z tą różnicą, że P jest obliczana tylko raz.

Gdy E występuje jako anonymous_function_body lub method_body znaczenie E zależy od jego klasyfikacji:

  • Jeśli E jest klasyfikowana jako nic, jego znaczenie jest takie samo jak znaczenie bloku :

    { if ((object)P != null) PA; }
    

    z tą różnicą, że P jest obliczana tylko raz.

  • W przeciwnym razie znaczenie E jest takie samo jak znaczenie bloku :

    { return E; }
    

    i z kolei znaczenie bloku zależy od tego, czy E jest syntaktycznie równoważne z null_conditional_member_access (§12.8.8) albo z null_conditional_element_access (§12.8.13).

12.8.12 Dostęp do elementów

12.8.12.1 Ogólne

element_access składa się z primary_no_array_creation_expression, po którym następuje token "[", argument_list, a na końcu token "]". argument_list składa się z co najmniej jednego argumentus oddzielonego przecinkami.

element_access
    : primary_no_array_creation_expression '[' argument_list ']'
    ;

Argument argument_list w element_access nie może zawierać argumentów out ani ref.

element_access jest dynamicznie związany (§12.3.3), jeśli co najmniej jeden z następujących warunków:

  • Wyrażenie primary_no_array_creation_expression ma typ ustalany podczas kompilacji dynamic.
  • Co najmniej jedno wyrażenie argument_list ma typ czasu kompilacji dynamic, a primary_no_array_creation_expression nie ma typu tablicy.

W tym przypadku kompilator klasyfikuje element_access jako wartość typu dynamic. Poniższe reguły określające znaczenie element_access są następnie stosowane w czasie wykonywania przy użyciu typu czasu wykonywania zamiast typu czasu kompilacji tych z primary_no_array_creation_expression i argument_list wyrażeń, które mają typ czasu kompilacji dynamic. Jeśli primary_no_array_creation_expression nie ma typu czasu kompilacji dynamic, dostęp do elementu podlega ograniczonemu sprawdzeniu w czasie kompilacji zgodnie z opisem w §12.6.5.

Jeśli primary_no_array_creation_expressionelement_access jest wartością array_type, element_access jest dostępem do tablicy (§12.8.12.2). W przeciwnym razie primary_no_array_creation_expression powinien być zmienną lub wartością klasy, struktury bądź typu interfejsu, który posiada co najmniej jednego członka indeksatora; w takim przypadku element_access jest dostępem indeksatora (§12.8.12.3).

12.8.12.2 Dostęp do tablicy

W przypadku dostępu do tablicy primary_no_array_creation_expression w element_access powinno być wartością array_type. Ponadto argument_list w dostępie do tablicy nie może zawierać nazwanych argumentów. Liczba wyrażeń w argument_list jest taka sama jak ranga array_type, a każde wyrażenie jest typu int, uint, longlub ulong, lub niejawnie konwertowane na co najmniej jeden z tych typów.

Wynikiem oceny dostępu do tablicy jest zmienna typu elementu tablicy, a mianowicie element tablicy wybrany przez wartości wyrażeń w argument_list.

Przetwarzanie w czasie wykonywania dostępu do elementu tablicy w formacie P[A], gdzie P jest wyrażeniem primary_no_array_creation_expression typu array_type i A jest listą argument_list, składa się z następujących kroków:

  • P jest oceniana. Jeśli ta ocena powoduje wyjątek, nie są wykonywane żadne dalsze kroki.
  • Wyrażenia indeksu argument_list są oceniane w kolejności od lewej do prawej. Po przeprowadzeniu oceny każdego wyrażenia indeksu wykonywana jest niejawna konwersja (§10.2) do jednego z następujących typów: int, uint, long, ulong. Pierwszy typ na tej liście, dla którego istnieje niejawna konwersja, jest wybierany. Jeśli na przykład wyrażenie indeksu jest typu short jest wykonywana niejawna konwersja na int, ponieważ możliwe są niejawne konwersje z short na int i z short do long. Jeśli ocena wyrażenia indeksu lub kolejnej niejawnej konwersji powoduje wyjątek, żadne dalsze wyrażenia indeksu nie są oceniane i nie są wykonywane żadne dalsze kroki.
  • Wartość P jest sprawdzana jako prawidłowa. Jeśli wartość P jest równa null, zostanie wyrzucony System.NullReferenceException i żadne dalsze kroki nie zostaną wykonane.
  • Wartość każdego wyrażenia w argument_list jest sprawdzana względem rzeczywistych granic każdego wymiaru wystąpienia tablicy, do których odwołuje się P. Jeśli co najmniej jedna wartość jest poza zakresem, zostanie rzucony System.IndexOutOfRangeException i dalsze kroki nie będą wykonane.
  • Lokalizacja elementu tablicy podana przez wyrażenia indeksu jest obliczana, a ta lokalizacja staje się wynikiem dostępu do tablicy.

12.8.12.3 Dostęp do indeksatora

W przypadku dostępu indeksatora, "primary_no_array_creation_expression" z "element_access" musi być zmienną lub wartością klasy, struktury lub typu interfejsu, a ten typ musi implementować co najmniej jeden indeksator, który jest odpowiedni w stosunku do "argument_list" z "element_access".

Przetwarzanie czasu powiązania dostępu indeksatora do formularza P[A], gdzie P jest primary_no_array_creation_expression klasy, struktury lub typu interfejsu T, a A jest argument_list, składa się z następujących kroków:

  • Tworzony jest zestaw indeksatorów udostępnianych przez T. Zestaw składa się ze wszystkich indeksatorów zadeklarowanych w T lub podstawowego typu T, które nie są zastępowane deklaracjami i są dostępne w bieżącym kontekście (§7.5).
  • Zestaw jest zredukowany do tych indeksatorów, które mają zastosowanie i nie są ukryte przez innych indeksatorów. Następujące reguły są stosowane do każdego indeksatora S.I w zestawie, gdzie S jest typem, w którym indeksator I jest zadeklarowany:
    • Jeśli I nie ma zastosowania w odniesieniu do A (§12.6.4.2), I zostanie usunięta z zestawu.
    • Jeśli I ma zastosowanie w odniesieniu do A (§12.6.4.2), wszystkie indeksatory zadeklarowane w podstawowym typie S zostaną usunięte z zestawu.
    • Jeśli I ma zastosowanie w odniesieniu do A (§12.6.4.2) i S jest typem klasy innym niż object, wszystkie indeksatory zadeklarowane w interfejsie są usuwane z zestawu.
  • Jeśli wynikowy zestaw indeksatorów kandydatów jest pusty, nie ma odpowiednich indeksatorów i wystąpi błąd czasu wiązania.
  • Najlepszy indeksator zestawu indeksatorów kandydatów jest identyfikowany przy użyciu reguł rozpoznawania przeciążenia §12.6.4. Jeśli nie można zidentyfikować jednego najlepszego indeksatora, dostęp indeksatora jest niejednoznaczny i wystąpi błąd w czasie wiązania.
  • Wyrażenia indeksu argument_list są oceniane w kolejności od lewej do prawej. Wynikiem przetwarzania dostępu indeksatora jest wyrażenie sklasyfikowane jako dostęp indeksatora. Wyrażenie dostępu indeksatora odwołuje się do indeksatora określonego w powyższym kroku i ma skojarzone wyrażenie instancji P i skojarzoną listę argumentów Aoraz skojarzony typ, który jest typem indeksatora. Jeśli T jest typem klasy, skojarzony typ jest wybierany z pierwszej deklaracji lub zastępowania indeksatora znalezionego podczas rozpoczynania od T i przeszukiwania jego klas bazowych.

W zależności od kontekstu, w którym jest używany, dostęp do indeksatora powoduje wywołanie pobierającego akcesora lub ustawiającego akcesora indeksatora. Jeśli dostęp indeksatora jest celem przypisania, akcesor set jest wywoływany w celu przypisania nowej wartości (§12.21.2). We wszystkich innych przypadkach wywoływany jest akcesor, aby uzyskać bieżącą wartość (§12.2.2).

12.8.13 Dostęp warunkowy o wartości null

null_conditional_element_access składa się z primary_no_array_creation_expression, po którym następują dwa tokeny "?" i "[", następnie argument_list, potem token "]", po którym występuje zero lub więcej dependent_access, z których każdy może być poprzedzony operatorem null_forgiving_operator.

null_conditional_element_access
    : primary_no_array_creation_expression '?' '[' argument_list ']'
      (null_forgiving_operator? dependent_access)*
    ;

null_conditional_element_access jest wersją warunkową element_access (§12.8.12) i jest to błąd czasu powiązania, jeśli typ wyniku jest void. W przypadku wyrażenia warunkowego o wartości null, w którym typ wyniku może być void zobacz (§12.8.11).

Wyrażenie null_conditional_element_accessE ma postać P?[A]B; gdzie Bdependent_accesses, jeśli istnieją. Znaczenie E jest określane w następujący sposób:

  • Jeśli typ P jest typem wartości dopuszczającym wartość null:

    Niech T być typem wyrażenia P.Value[A]B.

    • Jeśli T jest parametrem typu, który nie jest znany jako typ referencyjny lub typ wartości niemogący przyjąć wartości null, wystąpi błąd czasu kompilacji.

    • Jeśli T jest typem wartości, który nie dopuszcza wartości null, to typ E jest T?, i znaczenie E jest takie samo jak znaczenie:

      ((object)P == null) ? (T?)null : P.Value[A]B
      

      Z tą różnicą, że P jest obliczana tylko raz.

    • W przeciwnym razie typ E jest T, a znaczenie E jest takie samo jak znaczenie:

      ((object)P == null) ? null : P.Value[A]B
      

      Z tą różnicą, że P jest obliczana tylko raz.

  • Inaczej:

    Niech T być typem wyrażenia P[A]B.

    • Jeśli T jest parametrem typu, który nie jest znany jako typ referencyjny lub typ wartości niemogący przyjąć wartości null, wystąpi błąd czasu kompilacji.

    • Jeśli T jest typem wartości, który nie dopuszcza wartości null, to typ E jest T?, i znaczenie E jest takie samo jak znaczenie:

      ((object)P == null) ? (T?)null : P[A]B
      

      Z tą różnicą, że P jest obliczana tylko raz.

    • W przeciwnym razie typ E jest T, a znaczenie E jest takie samo jak znaczenie:

      ((object)P == null) ? null : P[A]B
      

      Z tą różnicą, że P jest obliczana tylko raz.

Uwaga: w wyrażeniu formularza:

P?[A₀]?[A₁]

jeśli P ocenia null nie są oceniane ani A₀, ani A₁. To samo jest prawdą, gdy wyrażenie jest sekwencją pod rząd operacji null_conditional_element_access lub null_conditional_member_access§12.8.8.

uwaga końcowa

12.8.14 Ten dostęp

this_access składa się ze słowa kluczowego this.

this_access
    : 'this'
    ;

this_access jest dozwolona tylko w bloku konstruktora wystąpienia, metody wystąpienia, metody dostępu wystąpienia (§12.2.1) lub finalizatora. Ma jedno z następujących znaczenia:

  • Gdy this jest używane w wyrażeniu_pierwotnym w konstruktorze instancji klasy, jest klasyfikowane jako wartość. Typ wartości jest typem wystąpienia (§15.3.2) klasy, w której występuje użycie, a wartość jest odwołaniem do tworzonego obiektu.
  • Gdy this jest używana w wyrażeniu podstawowym w metodzie instancji lub akcesorze instancji klasy, jest klasyfikowana jako wartość. Typ wartości jest typem wystąpienia (§15.3.2) klasy, w której następuje użycie, a wartość jest odwołaniem do obiektu, dla którego wywoływano metodę lub akcesor.
  • Gdy this jest używana w primary_expression w konstruktorze wystąpienia struktury, jest klasyfikowana jako zmienna. Typ zmiennej to typ wystąpienia (§15.3.2) struktury, w której występuje użycie, a zmienna reprezentuje konstruowaną strukturę.
    • Jeśli deklaracja konstruktora nie ma inicjatora konstruktora, zmienna this zachowuje się dokładnie tak samo jak parametr wyjściowy typu struktury. W szczególności oznacza to, że zmienna musi być jednoznacznie przypisana w każdej ścieżce wykonania konstruktora instancji.
    • W przeciwnym razie zmienna this zachowuje się dokładnie tak samo jak parametr ref typu struktury. W szczególności oznacza to, że zmienna jest uznawana za początkowo przypisaną.
  • Gdy this jest używana w wyrażeniu podstawowym w metodzie instancji lub akcesorze instancji struktury, jest klasyfikowana jako zmienna. Typ zmiennej to typ wystąpienia (§15.3.2) struktury, w której występuje użycie.
    • Jeśli metoda lub metoda dostępu nie jest iteratorem (§15.14) lub funkcji asynchronicznej (§15.15), zmienna this reprezentuje strukturę, dla której wywoływano metodę lub metodę dostępu.
      • Jeśli struktura jest readonly struct, zmienna this zachowuje się dokładnie tak samo jak parametr wejściowy typu struktury
      • W przeciwnym razie zmienna this zachowuje się dokładnie tak samo jak parametr ref typu struktury
    • Jeśli metoda lub metoda dostępu jest iteratorem lub funkcją asynchronikową, zmienna this reprezentuje kopii struktury, dla której wywołano metodę lub metodę dostępu, i zachowuje się dokładnie tak samo jak wartość parametr typu struktury.

Użycie this w primary_expression w kontekście innym niż wymienione powyżej jest błędem czasu kompilacji. W szczególności nie można odwoływać się do this w metodzie statycznej, statycznym akcesorze właściwości lub w variable_initializer deklaracji pola.

12.8.15 Dostęp podstawowy

base_access składa się z bazy słów kluczowych, a następnie tokenu "." oraz identyfikatora i opcjonalnego type_argument_list lub argument_list ujętego w nawiasy kwadratowe:

base_access
    : 'base' '.' identifier type_argument_list?
    | 'base' '[' argument_list ']'
    ;

base_access służy do dostępu do składowych klas bazowych, które są ukryte przez podobnie nazwane składowe w bieżącej klasie lub strukturze. base_access jest dozwolona tylko w treści konstruktora instancji, metody instancji, akcesora instancji (§12.2.1) lub finalizatora. Gdy base.I występuje w klasie lub strukturze, będę oznaczać element członkowski klasy bazowej tej klasy lub struktury. Podobnie, gdy base[E] występuje w klasie, odpowiedni indeksator musi istnieć w klasie bazowej.

W czasie wiązania wyrażenia base_access w formie base.I i base[E] są oceniane dokładnie tak, jak gdyby były zapisane ((B)this).I i ((B)this)[E], gdzie B jest klasą bazową klasy lub struktury, w której konstrukcja występuje. W związku z tym base.I i base[E] odpowiadają this.I i this[E], z wyjątkiem tego, że this jest postrzegane jako wystąpienie klasy bazowej.

Gdy base_access odwołuje się do wirtualnego członka funkcji (metody, właściwości lub indeksatora), ustalenie członka funkcji do wywołania w czasie wykonywania (§12.6.6) ulega zmianie. Element członkowski funkcji wywoływany jest określany przez znalezienie najbardziej pochodnej implementacji (§15.6.4) elementu członkowskiego funkcji w odniesieniu do B (zamiast w odniesieniu do typu czasu wykonywania this, jak zwykle w dostępie niepodstawowym). W związku z tym w ramach przesłonięcia elementu członkowskiego funkcji wirtualnej można użyć base_access do wywołania dziedziczonej implementacji elementu członkowskiego funkcji. Jeśli element członkowski funkcji, do którego odwołuje się base_access, jest abstrakcyjny, pojawi się błąd czasu wiązania.

Uwaga: w przeciwieństwie do thisbase nie jest wyrażeniem samym w sobie. Jest to słowo kluczowe używane tylko w kontekście base_access lub constructor_initializer (§15.11.2). uwaga końcowa

12.8.16 Operatory przyrostku i dekrementacji

post_increment_expression
    : primary_expression '++'
    ;

post_decrement_expression
    : primary_expression '--'
    ;

Operand operacji przyrostka lub dekrementacji jest wyrażeniem sklasyfikowanym jako zmienna, dostęp do właściwości lub dostęp indeksatora. Wynikiem działania jest wartość tego samego typu co operandu.

Jeśli primary_expression ma typ czasu kompilacji dynamic operator jest dynamicznie powiązany (§12.3.3), post_increment_expression lub post_decrement_expression ma typ czasu kompilacji dynamic, a następujące reguły są stosowane w czasie wykonywania przy użyciu typu czasu wykonywania primary_expression.

Jeśli operand operacji postfiksowej inkrementacji lub dekrementacji jest właściwością lub indeksatorem, właściwość lub indeksator musi mieć zarówno metodę get, jak i metodę set. Jeśli tak nie jest, wystąpi błąd powiązania czasowego.

Rozpoznawanie przeciążenia operatora jednoargumentowego (§12.4.4) jest stosowane w celu wybrania określonej implementacji operatora. Istnieją wstępnie zdefiniowane operatory ++ i -- dla następujących typów: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimali dowolnego typu wyliczeniowego. Wstępnie zdefiniowane operatory ++ zwracają wartość wygenerowaną przez dodanie 1 do operandu, a wstępnie zdefiniowane operatory -- zwracają wartość wygenerowaną przez odjęcie 1 z operandu. Jeśli wynik tego dodawania lub odejmowania jest poza zakresem typu rezultatu, a typ wyniku jest typem całkowitym lub typem wyliczenia, w kontekście sprawdzanym, zgłaszany jest System.OverflowException.

Istnieje niejawna konwersja z typu zwrotnego wybranego operatora jednoargumentowego na typ primary_expression; w przeciwnym razie wystąpi błąd podczas kompilacji.

Przetwarzanie podczas wykonywania operacji przyrostu lub dekrementu postfiksowego formy x++ lub x-- składa się z następujących kroków:

  • Jeśli x jest klasyfikowana jako zmienna:
    • x jest obliczana w celu utworzenia zmiennej.
    • Wartość x jest zapisana.
    • Zapisana wartość x jest konwertowana na typ operandu wybranego operatora, a operator jest wywoływany z tą wartością jako argumentem.
    • Wartość zwracana przez operatora jest konwertowana na typ x i przechowywana w lokalizacji podanej przez wcześniejszą ocenę x.
    • Zapisana wartość x staje się wynikiem operacji.
  • Jeśli x jest klasyfikowana jako właściwość lub dostęp indeksatora:
    • Wyrażenie wystąpienia (jeśli x nie jest static) oraz lista argumentów (jeśli x jest dostępem indeksatora) skojarzona z x są ewaluowane, a ich wyniki są następnie używane w kolejnych wywołaniach metod get i set.
    • Metoda pobierania x jest wywoływana, a zwrócona wartość jest zapisywana.
    • Zapisana wartość x jest konwertowana na typ operandu wybranego operatora, a operator jest wywoływany z tą wartością jako argumentem.
    • Wartość zwracana przez operatora jest konwertowana na typ x, a akcesor ustawiający x jest wywoływany z tą wartością przekazaną jako argument.
    • Zapisana wartość x staje się wynikiem operacji.

Operatory ++ i -- obsługują również notację prefiksów (§12.9.6). Wynikiem x++ lub x-- jest wartość xprzed operacji, natomiast wynikiem ++x lub --x jest wartość xpo operacji. W obu przypadkach sama x ma tę samą wartość po operacji.

Implementację operatora ++ lub operatora -- można wywołać, używając notacji postfiksowej lub prefiksowej. Nie można mieć oddzielnych implementacji operatorów dla dwóch notacji.

12.8.17 Nowy operator

12.8.17.1 Ogólne

Operator new służy do tworzenia nowych wystąpień typów.

Istnieją trzy formy nowych wyrażeń:

  • Wyrażenia tworzenia obiektów i anonimowe wyrażenia tworzenia obiektów są używane do tworzenia nowych wystąpień typów klas i typów wartości.
  • Wyrażenia tworzenia tablic są używane do tworzenia nowych wystąpień typów tablic.
  • Wyrażenia tworzenia delegata są używane do uzyskania wystąpień typu delegata.

Operator new oznacza utworzenie wystąpienia typu, ale niekoniecznie oznacza alokację pamięci. W szczególności wystąpienia typów wartości nie wymagają dodatkowej pamięci poza zmiennymi, w których się znajdują, i nie występują alokacje, gdy new jest używana do tworzenia wystąpień typów wartości.

Uwaga: wyrażenia tworzenia delegata nie zawsze prowadzą do utworzenia nowych instancji. Gdy wyrażenie jest przetwarzane w taki sam sposób jak konwersja grupy metod (§10.8) lub konwersja funkcji anonimowej (§10.7) może to spowodować ponowne użycie istniejącego wystąpienia delegata. uwaga końcowa

12.8.17.2 Wyrażenia tworzenia obiektów

wyrażenie tworzenia obiektu służy do tworzenia nowej instancji typu klasy lub typu wartości.

object_creation_expression
    : 'new' type '(' argument_list? ')' object_or_collection_initializer?
    | 'new' type object_or_collection_initializer
    ;

object_or_collection_initializer
    : object_initializer
    | collection_initializer
    ;

Wyrażenie tworzenia obiektu typu musi być typu klasa, wartośćlub parametr typu. Typ nie może być tuple_type ani abstrakcyjnym ani statycznym class_type.

Opcjonalne argument_list (§12.6.2) jest dozwolone tylko wtedy, gdy typ jest class_type lub struct_type.

Wyrażenie tworzenia obiektu może pominąć listę argumentów konstruktora i otaczające nawiasy, pod warunkiem, że zawiera inicjator obiektu lub inicjator kolekcji. Pominięcie listy argumentów konstruktora i ujęcie nawiasów jest równoważne określeniu pustej listy argumentów.

Przetwarzanie wyrażenia tworzenia obiektu, które zawiera inicjator obiektu lub inicjator kolekcji składa się z pierwszego przetwarzania konstruktora wystąpienia, a następnie przetwarzania inicjalizacji elementu członkowskiego lub elementu określonego przez inicjator obiektu (§12.8.17.3) lub inicjator kolekcji (§12.8.17.4).

Jeśli którykolwiek z argumentów w opcjonalnym argument_list ma typ kompilacji dynamic, wówczas object_creation_expression jest dynamicznie powiązany (§12.3.3) i stosuje się następujące reguły w czasie wykonywania, używając typu wykonywania tych argumentów argument_list, które mają typ kompilacji dynamic. Jednak tworzenie obiektu jest poddawane ograniczonej kontroli czasu kompilacji zgodnie z opisem w §12.6.5.

Przetwarzanie w czasie powiązania object_creation_expression w formie nowego T(A), gdzie T jest typem klasylub typem wartości, a A jest opcjonalną listą argumentów, składa się z szeregu kroków:

  • Jeśli T jest value_type i A nie jest obecny:
    • object_creation_expression jest wywołaniem konstruktora domyślnego. Wynikiem object_creation_expression jest wartość typu T, innymi słowy, wartość domyślna dla T zdefiniowana w §8.3.3.
  • W przeciwnym razie, jeśli T jest type_parameter i A nie jest obecny:
    • Jeśli nie określono żadnego ograniczenia typu wartości lub ograniczenia konstruktora (§15.2.5) dla Twystępuje błąd czasu powiązania.
    • Wynikiem object_creation_expression jest wartość typu czasu wykonywania, z którą został powiązany parametr typu, a mianowicie wynik wywołania domyślnego konstruktora tego typu. Typ czasu wykonywania może być typem odwołania lub typem wartości.
  • W przeciwnym razie, jeśli T jest class_type lub struct_type:
    • Jeśli T jest abstrakcyjnym lub statycznym class_type, wystąpi błąd kompilacji.
    • Konstruktor wystąpienia do wywołania jest określany przy użyciu reguł rozpoznawania przeciążenia §12.6.4. Zbiór kandydackich konstruktorów instancji składa się ze wszystkich dostępnych konstruktorów instancji zadeklarowanych w T, które są stosowalne względem A (§12.6.4.2). Jeśli zestaw konstruktorów instancji kandydatów jest pusty lub nie można zidentyfikować jednego najlepszego konstruktora instancji, wystąpi błąd czasu wiązania.
    • Wynikiem object_creation_expression jest wartość typu T, czyli wartość wygenerowana przez wywołanie konstruktora wystąpienia określonego w powyższym kroku.
    • W przeciwnym razie object_creation_expression jest nieprawidłowy i występuje błąd w czasie wiązania.

Nawet jeśli object_creation_expression jest dynamicznie powiązana, typ czasu kompilacji jest nadal T.

Przetwarzanie w czasie wykonywania object_creation_expression formularza nowego T(A), gdzie T jest class_type lub struct_type i A jest opcjonalnym argument_list, składa się z następujących kroków:

  • Jeśli T jest typ_klasy:
    • Przydzielane jest nowe wystąpienie klasy T. Jeśli nie ma wystarczającej ilości dostępnej pamięci do przydzielenia nowego wystąpienia, zostanie zgłoszony System.OutOfMemoryException i żadne dalsze kroki nie zostaną wykonane.
    • Wszystkie pola nowego wystąpienia są inicjowane do ich wartości domyślnych (§9.3).
    • Konstruktor instancji jest wywoływany zgodnie z regułami wywoływania członków funkcji (§12.6.6). Odwołanie do nowo przydzielonego wystąpienia jest automatycznie przekazywane do konstruktora wystąpienia i można uzyskać do niego dostęp z tego konstruktora jako this.
  • Jeśli T jest strukturą typu:
    • Wystąpienie typu T jest tworzone przez przydzielanie tymczasowej zmiennej lokalnej. Ponieważ konstruktor instancji struktury typu jest wymagany, aby przypisać jednoznaczną wartość do każdego pola tworzonej instancji, nie jest konieczne inicjowanie zmiennej tymczasowej.
    • Konstruktor instancji jest wywoływany zgodnie z regułami wywoływania członków funkcji (§12.6.6). Odwołanie do nowo przydzielonego wystąpienia jest automatycznie przekazywane do konstruktora wystąpienia i można uzyskać do niego dostęp z tego konstruktora jako this.

12.8.17.3 Inicjatory obiektów

Inicjator obiektu określa wartości dla zera lub większej liczby pól, właściwości lub indeksowanych elementów obiektu.

object_initializer
    : '{' member_initializer_list? '}'
    | '{' member_initializer_list ',' '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : initializer_target '=' initializer_value
    ;

initializer_target
    : identifier
    | '[' argument_list ']'
    ;

initializer_value
    : expression
    | object_or_collection_initializer
    ;

Inicjator obiektu składa się z sekwencji inicjatorów składowych, ujętej w tokeny { i } i rozdzielone przecinkami. Każdy member_initializer powinien wyznaczać cel inicjalizacji. Identyfikator określa dostępne pole lub właściwość obiektu inicjowanego, natomiast argument_list ujęte w nawiasy kwadratowe określa argumenty dostępnego indeksatora na inicjowanym obiekcie. Błędem inicjatora obiektu jest zawarcie więcej niż jednego inicjatora składowego dla tego samego pola lub właściwości.

Uwaga: Chociaż inicjator obiektu nie może ustawić tego samego pola lub właściwości więcej niż raz, nie ma takich ograniczeń dla indeksatorów. Inicjator obiektu może zawierać wiele obiektów docelowych inicjatora odwołujące się do indeksatorów, a nawet wielokrotnie używać tych samych argumentów indeksatora. uwaga końcowa

Po każdym initializer_target następuje znak równości oraz wyrażenie, inicjalizator obiektu lub inicjalizator kolekcji. Nie jest możliwe, aby wyrażenia w inicjatorze obiektu odwoływały się do nowo utworzonego obiektu, który inicjuje.

Inicjalizator członkowski, określający wyrażenie po znaku równości, jest przetwarzany tak samo jak przypisanie (§12.21.2) do obiektu docelowego.

Inicjalizator składowy, który określa inicjalizację obiektu po znaku równości, jest zagnieżdżonym inicjalizatorem obiektu , tj. inicjalizacją obiektu osadzonego. Zamiast przypisywać nową wartość do pola lub właściwości, przypisania w zagnieżdżonym inicjatorze obiektów są traktowane jako przypisania do elementów członkowskich pola lub właściwości. Zagnieżdżone inicjatory obiektów nie mogą być stosowane do właściwości z typem wartościowym lub do pól tylko do odczytu z typem wartościowym.

Inicjalizator członkowski, który określa inicjalizator kolekcji za pomocą znaku równości, jest inicjalizacją osadzonej kolekcji. Zamiast przypisywać nową kolekcję do pola docelowego, właściwości lub indeksatora, elementy podane w inicjatorze są dodawane do kolekcji przywoływanej przez element docelowy. Celem jest typ kolekcji spełniający wymagania określone w §12.8.17.4.

Gdy docelowy element inicjalizatora odwołuje się do indeksatora, argumenty tego indeksatora powinny być oceniane dokładnie raz. W związku z tym, nawet jeśli argumenty nigdy nie są używane (np. z powodu pustego inicjatora zagnieżdżonego), są oceniane pod kątem ich skutków ubocznych.

Przykład: Następująca klasa reprezentuje punkt z dwiema współrzędnymi:

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

Wystąpienie Point można utworzyć i zainicjować w następujący sposób:

Point a = new Point { X = 0, Y = 1 };

Ma to taki sam efekt jak

Point __a = new Point();
__a.X = 0;
__a.Y = 1;
Point a = __a;

gdzie __a jest inaczej niewidoczną i niedostępną zmienną tymczasową.

Poniższa klasa przedstawia prostokąt utworzony na podstawie dwóch punktów oraz tworzenie i inicjowanie wystąpienia Rectangle:

public class Rectangle
{
    public Point P1 { get; set; }
    public Point P2 { get; set; }
}

Wystąpienie Rectangle można utworzyć i zainicjować w następujący sposób:

Rectangle r = new Rectangle
{
    P1 = new Point { X = 0, Y = 1 },
    P2 = new Point { X = 2, Y = 3 }
};

Ma to taki sam efekt jak

Rectangle __r = new Rectangle();
Point __p1 = new Point();
__p1.X = 0;
__p1.Y = 1;
__r.P1 = __p1;
Point __p2 = new Point();
__p2.X = 2;
__p2.Y = 3;
__r.P2 = __p2;
Rectangle r = __r;

gdzie __r, __p1 i __p2 są zmiennymi tymczasowymi, które w przeciwnym razie są niewidoczne i niedostępne.

Jeśli konstruktor Rectangleprzydziela dwa wystąpienia osadzone w Point, można je użyć do inicjowania osadzonych wystąpień Point zamiast przypisywania nowych wystąpień.

public class Rectangle
{
    public Point P1 { get; } = new Point();
    public Point P2 { get; } = new Point();
}

Następująca konstrukcja może służyć do inicjowania osadzonych wystąpień Point zamiast przypisywania nowych wystąpień:

Rectangle r = new Rectangle
{
    P1 = { X = 0, Y = 1 },
    P2 = { X = 2, Y = 3 }
};

Ma to taki sam efekt jak

Rectangle __r = new Rectangle();
__r.P1.X = 0;
__r.P1.Y = 1;
__r.P2.X = 2;
__r.P2.Y = 3;
Rectangle r = __r;

koniec przykładu

12.8.17.4 Inicjatory kolekcji

Inicjator kolekcji określa elementy kolekcji.

collection_initializer
    : '{' element_initializer_list '}'
    | '{' element_initializer_list ',' '}'
    ;

element_initializer_list
    : element_initializer (',' element_initializer)*
    ;

element_initializer
    : non_assignment_expression
    | '{' expression_list '}'
    ;

expression_list
    : expression
    | expression_list ',' expression
    ;

Inicjator kolekcji to sekwencja inicjatorów elementów, otoczona przez tokeny { i }, i oddzielona przecinkami. Każdy inicjator elementu określa element do dodania do inicjowanego obiektu kolekcji i składa się z listy wyrażeń ujętych w { i } tokenów i rozdzielonych przecinkami. Inicjator elementu z jednym wyrażeniem może być zapisywany bez nawiasów klamrowych, ale nie może być wyrażeniem przypisania, aby uniknąć niejednoznaczności przy użyciu inicjatorów składowych. Produkcja non_assignment_expression jest zdefiniowana w §12.22.

Przykład: Poniżej przedstawiono przykład wyrażenia tworzenia obiektu zawierającego inicjator kolekcji:

List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

koniec przykładu

Obiekt kolekcji, do którego zastosowano inicjator kolekcji, musi być typu implementującego System.Collections.IEnumerable, w przeciwnym razie wystąpi błąd czasu kompilacji. Dla każdego określonego elementu w kolejności od lewej do prawej jest stosowane standardowe wyszukiwanie składowych w celu znalezienia składowej o nazwie Add. Jeśli wynik wyszukiwania elementu członkowskiego nie jest grupą metod, pojawi się błąd czasu kompilacji. W przeciwnym razie rozwiązanie przeciążenia jest stosowane z listą wyrażeń inicjatora elementu jako listy argumentów, a inicjator kolekcji wywołuje wynikową metodę. W związku z tym obiekt kolekcji zawiera odpowiednie wystąpienie lub metodę rozszerzenia o nazwie Add dla każdego inicjatora elementu.

Przykład:Poniżej przedstawiono klasę reprezentującą kontakt z nazwą i listą numerów telefonów oraz tworzenie i inicjowanie List<Contact>:

public class Contact
{
    public string Name { get; set; }
    public List<string> PhoneNumbers { get; } = new List<string>();
}

class A
{
    static void M()
    {
        var contacts = new List<Contact>
        {
            new Contact
            {
                Name = "Chris Smith",
                PhoneNumbers = { "206-555-0101", "425-882-8080" }
            },
            new Contact
            {
                Name = "Bob Harris",
                PhoneNumbers = { "650-555-0199" }
            }
        };
    }
}

który ma taki sam efekt jak

var __clist = new List<Contact>();
Contact __c1 = new Contact();
__c1.Name = "Chris Smith";
__c1.PhoneNumbers.Add("206-555-0101");
__c1.PhoneNumbers.Add("425-882-8080");
__clist.Add(__c1);
Contact __c2 = new Contact();
__c2.Name = "Bob Harris";
__c2.PhoneNumbers.Add("650-555-0199");
__clist.Add(__c2);
var contacts = __clist;

gdzie __clist, __c1 i __c2 są zmiennymi tymczasowymi, które w przeciwnym razie są niewidoczne i niedostępne.

koniec przykładu

12.8.17.5 Wyrażenia tworzenia tablicy

Wyrażenie array_creation_expression służy do utworzenia nowego wystąpienia typu array_type.

array_creation_expression
    : 'new' non_array_type '[' expression_list ']' rank_specifier*
      array_initializer?
    | 'new' array_type array_initializer
    | 'new' rank_specifier array_initializer
    ;

Wyrażenie tworzenia tablicy pierwszej formy przydziela instancję tablicy typu, który jest wynikiem usunięcia każdego z poszczególnych wyrażeń z listy wyrażeń.

Przykład: wyrażenie tworzenia tablicy new int[10,20] generuje wystąpienie tablicy typu int[,], a wyrażenie tworzenia nowej tablicy int[10][,] generuje wystąpienie tablicy typu int[][,]. koniec przykładu

Każde wyrażenie na liście wyrażeń ma typ int, uint, longlub ulonglub niejawnie konwertowane na co najmniej jeden z tych typów. Wartość każdego wyrażenia określa długość odpowiedniego wymiaru w nowo przydzielonym wystąpieniu tablicy. Ponieważ długość wymiaru tablicy musi być nieujemna, wyrażenie stałe z wartością ujemną na liście wyrażeń stanowi błąd czasu kompilacji.

Z wyjątkiem niebezpiecznego kontekstu (§23.2), układ tablic jest nieokreślony.

Jeśli wyrażenie tworzenia tablicy pierwszego formularza zawiera inicjator tablicy, każde wyrażenie na liście wyrażeń jest stałą, a ranga i długość wymiarów określona przez listę wyrażeń jest zgodna z wartościami inicjatora tablicy.

W wyrażeniu tworzenia tablicy drugiej lub trzeciej formy wymiar określonego typu tablicy lub specyfikatora wymiaru musi odpowiadać wymiarowi inicjalizatora tablicy. Indywidualne długości wymiarów są wnioskowane na podstawie liczby elementów w każdym z odpowiednich poziomów zagnieżdżenia inicjalizatora tablicy. W związku z tym wyrażenie inicjujące w następującej deklaracji

var a = new int[,] {{0, 1}, {2, 3}, {4, 5}};

dokładnie odpowiada

var a = new int[3, 2] {{0, 1}, {2, 3}, {4, 5}};

Wyrażenie tworzenia tablicy trzeciego typu jest określane jako niejawnie wpisane wyrażenie tworzenia tablicy . Jest to podobne do drugiej formy, z tą różnicą, że typ elementu tablicy nie jest jawnie podany, lecz określany jako najlepszy wspólny typ (§12.6.3.15) zestawu wyrażeń w inicjatorze tablicy. W przypadku tablicy wielowymiarowej, tj. jednej, w której rank_specifier zawiera co najmniej jeden przecinek, ten zestaw składa się ze wszystkich wyrażeń znalezionych w zagnieżdżonych array_initializers.

Inicjalizatory tablic zostały opisane w §17.7.

Wynik oceny wyrażenia tworzenia tablicy jest klasyfikowany jako wartość, a mianowicie odwołanie do nowo przydzielonego wystąpienia tablicy. Przetwarzanie w czasie wykonywania wyrażenia tworzenia tablicy składa się z następujących kroków:

  • Wyrażenia dotyczące długości wymiarów expression_list są obliczane w kolejności od lewej do prawej. Po ocenie każdego wyrażenia wykonywana jest niejawna konwersja (§10.2) do jednego z następujących typów: int, uint, long, ulong. Pierwszy typ na tej liście, dla którego istnieje niejawna konwersja, jest wybierany. Jeśli ocena wyrażenia lub kolejnej niejawnej konwersji powoduje wyjątek, żadne dalsze wyrażenia nie są oceniane i nie są wykonywane żadne dalsze kroki.
  • Obliczone wartości długości wymiarów są weryfikowane w następujący sposób: Jeśli co najmniej jedna z wartości jest mniejsza niż zero, zostanie zgłoszony System.OverflowException i nie zostaną wykonane żadne dalsze kroki.
  • Przydzielone jest wystąpienie tablicy o podanej długości wymiarów. Jeśli nie ma wystarczającej ilości dostępnej pamięci do przydzielenia nowego wystąpienia, zostanie zgłoszony System.OutOfMemoryException i żadne dalsze kroki nie zostaną wykonane.
  • Wszystkie elementy nowego wystąpienia tablicy są inicjowane do wartości domyślnych (§9.3).
  • Jeśli wyrażenie tworzenia tablicy zawiera inicjator tablicy, każde wyrażenie w inicjatorze tablicy jest oceniane i przypisywane do odpowiadającego mu elementu tablicy. Ewaluacje i przypisania są wykonywane w kolejności zapisywania wyrażeń w inicjalizatorze tablicy — innymi słowy, elementy są inicjowane w kolejności rosnących indeksów, z rosnącym najpierw wymiarem najbardziej na prawo. Jeśli ocena danego wyrażenia lub późniejszego przypisania do odpowiedniego elementu tablicy powoduje wyjątek, nie zostaną zainicjowane żadne dalsze elementy (a pozostałe elementy będą w ten sposób miały wartości domyślne).

Wyrażenie tworzenia tablicy umożliwia utworzenie instancji tablicy z elementami typu tablicy, ale elementy takiej tablicy są inicjowane ręcznie.

Przykład: Oświadczenie

int[][] a = new int[100][];

tworzy tablicę jednowymiarową z 100 elementami typu int[]. Początkowa wartość każdego elementu to null. Nie można jednocześnie utworzyć tablicy i zainicjować podtablicy, a instrukcja

int[][] a = new int[100][5]; // Error

powoduje wystąpienie błędu czasu kompilacji. Zamiast tego można ręcznie zainicjować podtablice, tak jak w

int[][] a = new int[100][];
for (int i = 0; i < 100; i++)
{
    a[i] = new int[5];
}

koniec przykładu

Uwaga: Jeśli tablica tablic ma kształt „prostokątny”, to znaczy, że gdy podtablice mają taką samą długość, bardziej wydajne jest użycie tablicy wielowymiarowej. W powyższym przykładzie utworzenie tablicy tablic generuje 101 obiektów — jedną zewnętrzną tablicę i 100 podrzędnych tablic. Natomiast

int[,] a = new int[100, 5];

Tworzy tylko pojedynczy obiekt, tablicę dwuwymiarową i wykonuje alokację w jednej instrukcji.

uwaga końcowa

Przykładowy: Poniżej przedstawiono przykłady niejawnie typowanych wyrażeń tworzenia tablic:

var a = new[] { 1, 10, 100, 1000 };                     // int[]
var b = new[] { 1, 1.5, 2, 2.5 };                       // double[]
var c = new[,] { { "hello", null }, { "world", "!" } }; // string[,]
var d = new[] { 1, "one", 2, "two" };                   // Error

Ostatnie wyrażenie powoduje błąd czasu kompilacji, ponieważ ani int, ani string nie są niejawnie konwertowane jedne na drugie, więc nie ma najlepszego wspólnego typu. W tym przypadku należy użyć jawnie wpisanego wyrażenia tworzenia tablicy, na przykład określając typ, który ma być object[]. Alternatywnie jeden z elementów można rzutować do wspólnego typu podstawowego, który następnie stałby się wywnioskowanym typem elementu.

koniec przykładu

Niejawnie wpisane wyrażenia tworzenia tablicy można łączyć z inicjatorami obiektów anonimowych (§12.8.17.7) w celu utworzenia anonimowo typiowanych struktur danych.

Przykład:

var contacts = new[]
{
    new
    {
        Name = "Chris Smith",
        PhoneNumbers = new[] { "206-555-0101", "425-882-8080" }
    },
    new 
    {
        Name = "Bob Harris",
       PhoneNumbers = new[] { "650-555-0199" }
    }
};

koniec przykładu

12.8.17.6 Wyrażenia tworzenia delegata

delegate_creation_expression służy do uzyskiwania wystąpienia delegate_type.

delegate_creation_expression
    : 'new' delegate_type '(' expression ')'
    ;

Argumentem wyrażenia tworzenia delegata może być grupa metod, funkcja anonimowa lub wartość typu ustalanego w czasie kompilacji dynamic albo typu delegata . Jeśli argument jest grupą metod, identyfikuje metodę i, w przypadku metody instancji, obiekt, dla którego ma zostać utworzony delegat. Jeśli argument jest funkcją anonimową, bezpośrednio definiuje parametry i treść metody obiektu docelowego delegata. Jeśli argument jest wartością, identyfikuje wystąpienie delegata, z którego ma zostać utworzona kopia.

Jeśli wyrażenie ma typ czasu kompilacji dynamic, wyrażenie tworzenia delegata jest dynamicznie powiązane (§12.8.17.6), a poniższe reguły są stosowane podczas wykonywania przy użyciu typu wykonawczego wyrażenia . W przeciwnym razie reguły są stosowane w czasie kompilacji.

Przetwarzanie w czasie powiązania delegate_creation_expression formularza nowego D(E), gdzie D jest delegate_type, a E jest wyrażeniem , składają się z następujących kroków:

  • Jeśli E jest grupą metod, wyrażenie tworzenia delegata jest przetwarzane w taki sam sposób jak konwersja grupy metod (§10.8) z E do D.

  • Jeśli E jest funkcją anonimową, wyrażenie tworzenia delegata jest przetwarzane w taki sam sposób jak konwersja funkcji anonimowej (§10.7) z E do D.

  • Jeśli E jest wartością, E jest zgodna (§20,2) z D, a wynikiem jest odwołanie do nowo utworzonego delegata z listą wywołań jednokrotnych, która wywołuje E.

Przetwarzanie w czasie wykonywania wyrażenia tworzenia delegata o postaci new D(E), gdzie D jest typu delegata , a E jest wyrażeniem , składa się z następujących kroków:

  • Jeśli E jest grupą metod, wyrażenie tworzenia delegata jest oceniane jako konwersja grupy metod (§10.8) z E do D.
  • Jeśli E jest funkcją anonimową, tworzenie delegata jest oceniane jako anonimowa konwersja funkcji z E na D (§10.7).
  • Jeśli E jest wartością typu delegata :
    • E jest oceniana. Jeśli ta ocena powoduje wyjątek, nie są wykonywane żadne dalsze kroki.
    • Jeśli wartość E jest równa null, zostanie wyrzucony System.NullReferenceException i żadne dalsze kroki nie zostaną wykonane.
    • Przydzielone jest nowe wystąpienie typu delegata D. Jeśli nie ma wystarczającej ilości dostępnej pamięci do przydzielenia nowego wystąpienia, zostanie zgłoszony System.OutOfMemoryException i żadne dalsze kroki nie zostaną wykonane.
    • Nowe wystąpienie delegata jest inicjowane przy użyciu listy wywołań jednokrotnych, która wywołuje E.

Lista wywołań delegata jest określana w momencie tworzenia jego wystąpienia i pozostaje niezmienna przez cały czas istnienia tego delegata. Innymi słowy, nie można zmienić docelowych jednostek wywołujących delegata po jego utworzeniu.

Uwaga: Pamiętaj, że gdy dwa delegaty są łączone lub jeden jest usuwany z innego, powstaje nowy delegat; żaden z istniejących delegatów nie zmienia swojej zawartości. uwaga końcowa

Nie można utworzyć delegata, który odwołuje się do właściwości, indeksatora, operatora zdefiniowanego przez użytkownika, konstruktora wystąpienia, finalizatora lub konstruktora statycznego.

Przykład: zgodnie z powyższym opisem, gdy delegat jest tworzony na podstawie grupy metod, lista parametrów i typ zwracany delegata określają, które z przeciążonych metod wybrać. W przykładzie

delegate double DoubleFunc(double x);

class A
{
    DoubleFunc f = new DoubleFunc(Square);

    static float Square(float x) => x * x;
    static double Square(double x) => x * x;
}

pole A.f jest inicjowane za pomocą delegata, który odwołuje się do drugiej metody Square, ponieważ ta metoda dokładnie odpowiada liście parametrów i zwraca typ DoubleFunc. Gdyby druga metoda Square nie była obecna, wystąpił błąd czasu kompilacji.

koniec przykładu

12.8.17.7 Anonimowe wyrażenia tworzenia obiektów

anonymous_object_creation_expression służy do tworzenia obiektu typu anonimowego.

anonymous_object_creation_expression
    : 'new' anonymous_object_initializer
    ;

anonymous_object_initializer
    : '{' member_declarator_list? '}'
    | '{' member_declarator_list ',' '}'
    ;

member_declarator_list
    : member_declarator (',' member_declarator)*
    ;

member_declarator
    : simple_name
    | member_access
    | null_conditional_projection_initializer
    | base_access
    | identifier '=' expression
    ;

Inicjator obiektów anonimowych deklaruje typ anonimowy i zwraca wystąpienie tego typu. Typ anonimowy to typ klasy bez nazw, który dziedziczy bezpośrednio z object. Elementy członkowskie typu anonimowego są sekwencją właściwości tylko do odczytu wywnioskowanych z inicjatora obiektu anonimowego używanego do tworzenia wystąpienia typu. W szczególności inicjator obiektu anonimowego formularza

new { p₁=e₁,p₂=e₂, ... pv=ev}

deklaruje anonimowy typ formularza

class __Anonymous1
{
    private readonly «T1» «f1»;
    private readonly «T2» «f2»;
    ...
    private readonly «Tn» «fn»;

    public __Anonymous1(«T1» «a1», «T2» «a2»,..., «Tn» «an»)
    {
        «f1» = «a1»;
        «f2» = «a2»;
        ...
        «fn» = «an»;
    }

    public «T1» «p1» { get { return «f1»; } }
    public «T2» «p2» { get { return «f2»; } }
    ...
    public «Tn» «pn» { get { return «fn»; } }
    public override bool Equals(object __o) { ... }
    public override int GetHashCode() { ... }
}

gdzie każdy «Tx» jest typem odpowiadającego wyrażenia «ex». Wyrażenie używane w member_declarator powinno mieć typ. W związku z tym jest to błąd czasu kompilacji wyrażenia w member_declarator, które jest null lub funkcją anonimową.

Nazwy typu anonimowego i parametru do metody Equals są generowane automatycznie przez kompilator i nie można odwoływać się do nich w tekście programu.

W ramach tego samego programu, dwa anonimowe inicjatory obiektów, które określają sekwencję właściwości o tych samych nazwach i typach określonych w czasie kompilacji w tej samej kolejności, spowodują utworzenie wystąpień tego samego typu anonimowego.

Przykład: w przykładzie

var p1 = new { Name = "Lawnmower", Price = 495.00 };
var p2 = new { Name = "Shovel", Price = 26.95 };
p1 = p2;

W ostatnim wierszu przypisanie jest dozwolone, ponieważ p1 i p2 są takiego samego typu anonimowego.

koniec przykładu

Metody Equals i GetHashcode dla typów anonimowych zastępują metody dziedziczone z objecti są zdefiniowane pod względem Equals i GetHashcode właściwości, tak aby dwa wystąpienia tego samego typu anonimowego są równe i tylko wtedy, gdy wszystkie ich właściwości są równe.

Deklarator składowy może być skrócony do prostej nazwy (§12.8.4), dostępu do składowej (§12.8.7), inicjator projekcji warunkowej o wartości null §12.8.8 lub dostępu podstawowego (§12.8.15). Nazywa się to inicjatorem projekcji oznaczonym jako i jest skrótem na deklarację i przypisanie do właściwości o tej samej nazwie. W szczególności deklaratory składowe formularzy

«identifier», «expr» . «identifier» i «expr» ? . «identifier»

są dokładnie równoważne następującym, odpowiednio:

«identifer» = «identifier», «identifier» = «expr» . «identifier» i «identifier» = «expr» ? . «identifier»

W związku z tym w inicjatorze projekcji identyfikator wybiera zarówno wartość, jak i pole lub właściwość, do której przypisano wartość. Intuicyjnie inicjator projekcji nie tylko przedstawia wartość, ale także nazwę tej wartości.

12.8.18 Operator typeof

Operator typeof służy do uzyskiwania obiektu System.Type dla typu.

typeof_expression
    : 'typeof' '(' type ')'
    | 'typeof' '(' unbound_type_name ')'
    | 'typeof' '(' 'void' ')'
    ;

unbound_type_name
    : identifier generic_dimension_specifier?
    | identifier '::' identifier generic_dimension_specifier?
    | unbound_type_name '.' identifier generic_dimension_specifier?
    ;

generic_dimension_specifier
    : '<' comma* '>'
    ;

comma
    : ','
    ;

Pierwsza forma typeof_expression składa się z typeof słowa kluczowego, po którym następuje typ w nawiasach. Wynikiem wyrażenia tego formularza jest obiekt System.Type dla wskazanego typu. Dla danego typu istnieje tylko jeden obiekt System.Type. Oznacza to, że w przypadku typu Twartość typeof(T) == typeof(T) jest zawsze prawdziwa. Typ nie może być dynamic.

Druga forma typeof_expression składa się ze słowa kluczowego typeof, po którym następuje nawiasowa nazwa niepowiązanego typu.

Uwaga: unbound_type_name jest bardzo podobny do type_name (§7.8), z tą różnicą, że unbound_type_name zawiera generic_dimension_specifier, podczas gdy type_name zawiera type_argument_list. uwaga końcowa

Gdy operand typeof_expression jest sekwencją tokenów spełniającą gramatyki zarówno unbound_type_name, jak również type_name, czyli gdy nie zawiera ani generic_dimension_specifier ani type_argument_list, sekwencja tokenów jest uważana za type_name. Znaczenie unbound_type_name jest określane w następujący sposób:

  • Przekonwertuj sekwencję tokenów na type_name, zastępując każdy generic_dimension_specifier listą type_argument_list, która ma taką samą liczbę przecinków i zawiera słowo kluczowe object, co każdy type_argument.
  • Oceń wynikowy type_name, ignorując wszystkie ograniczenia parametrów typu.
  • unbound_type_name rozwiązywany jest jako niezwiązany typ ogólny skojarzony z wynikowym typem skonstruowanym (§8.4).

Nieprawidłowe jest, aby nazwa typu była typem odwołania dopuszczającym wartość null.

Wynikiem typeof_expression jest obiekt System.Type dla wynikowego niezwiązanego typu ogólnego.

Trzecia forma typeof_expression składa się z słowa kluczowego typeof, po którym następuje słowo kluczowe void w nawiasach. Wynikiem wyrażenia tego formularza jest obiekt System.Type, który reprezentuje brak typu. Obiekt typu zwracany przez typeof(void) różni się od obiektu typu zwracanego dla dowolnego typu.

Uwaga: Ten specjalny obiekt System.Type jest przydatny w bibliotekach klas, które umożliwiają odbicie na metody w języku, gdzie te metody chcą reprezentować typ zwracany dowolnej metody, w tym metody void, z wystąpieniem System.Type. uwaga końcowa

Operator typeof może być używany na parametrze typu. Jest to błąd czasu kompilacji, jeśli nazwa typu jest znane jako typ referencyjny dopuszczający wartość null. Wynikiem jest obiekt System.Type dla typu czasu wykonywania, który został powiązany z parametrem typu. Jeśli typ czasu wykonywania jest typem odwołania dopuszczanym do wartości null, wynik jest odpowiadającym niepustomym typem odwołania. Operator typeof może być również stosowany do skonstruowanego typu lub typu ogólnego niepowiązanego (§8.4.4). Obiekt System.Type dla typu ogólnego niezwiązanego nie jest taki sam jak obiekt System.Type typu wystąpienia (§15.3.2). Typ wystąpienia jest zawsze zamkniętym typem skonstruowanym w czasie wykonywania, więc jego obiekt System.Type zależy od używanych argumentów typu czasu wykonywania. Niezwiązany typ ogólny, z drugiej strony, nie ma argumentów typu i zwraca ten sam obiekt System.Type niezależnie od argumentów typu środowiska uruchomieniowego.

Przykład: przykład

class X<T>
{
    public static void PrintTypes()
    {
        Type[] t =
        {
            typeof(int),
            typeof(System.Int32),
            typeof(string),
            typeof(double[]),
            typeof(void),
            typeof(T),
            typeof(X<T>),
            typeof(X<X<T>>),
            typeof(X<>)
        };
        for (int i = 0; i < t.Length; i++)
        {
            Console.WriteLine(t[i]);
        }
    }
}

class Test
{
    static void Main()
    {
        X<int>.PrintTypes();
    }
}

tworzy następujące dane wyjściowe:

System.Int32
System.Int32
System.String
System.Double[]
System.Void
System.Int32
X`1[System.Int32]
X`1[X`1[System.Int32]]
X`1[T]

Należy pamiętać, że int i System.Int32 są tego samego typu. Wynik typeof(X<>) nie zależy od argumentu typu, ale wynik typeof(X<T>) zależy.

koniec przykładu

12.8.19 Operator sizeof

Operator sizeof zwraca liczbę 8-bitowych bajtów zajmowanych przez zmienną danego typu. Typ określony jako operand dla sizeof musi być typu niezarządzanego (unmanaged_type) (§8.8).

sizeof_expression
    : 'sizeof' '(' unmanaged_type ')'
    ;

W przypadku niektórych wstępnie zdefiniowanych typów operator sizeof zwraca stałą wartość int, jak pokazano w poniższej tabeli:

ekspresja wynik
sizeof(sbyte) 1
sizeof(byte) 1
sizeof(short) 2
sizeof(ushort) 2
sizeof(int) 4
sizeof(uint) 4
sizeof(long) 8
sizeof(ulong) 8
sizeof(char) 2
sizeof(float) 4
sizeof(double) 8
sizeof(bool) 1
sizeof(decimal) 16

W przypadku typu wyliczeniowego Twynik wyrażenia sizeof(T) jest stałą wartością równą rozmiarowi jego typu bazowego, jak podano powyżej. Dla wszystkich innych typów operandu operator sizeof jest określony w §23.6.9.

12.8.20 Zaznaczone i niezaznaczone operatory

Operatory checked i unchecked służą do kontrolowania kontekstu sprawdzania przepełnienia dla operacji arytmetycznych i konwersji typu całkowitego.

checked_expression
    : 'checked' '(' expression ')'
    ;

unchecked_expression
    : 'unchecked' '(' expression ')'
    ;

Operator checked oblicza zawarte wyrażenie w zaznaczonym kontekście, a operator unchecked oblicza zawarte wyrażenie w nieznakowanym kontekście. checked_expression lub unchecked_expression odpowiada dokładnie parenthesized_expression (§12.8.5), z wyjątkiem tego, że zawarte wyrażenie jest oceniane w danym kontekście sprawdzania przepełnienia.

Kontekst sprawdzania przepełnienia można również kontrolować za pomocą instrukcji checked i unchecked (§13.12).

Następujące operacje mają wpływ na kontekst sprawdzania przepełnienia ustanowiony przez zaznaczone i niezaznaczone operatory i instrukcje:

  • Wstępnie zdefiniowane operatory ++ i -- (§12.8.16 i §12.9.6), gdy operand jest typu całkowitego lub wyliczeniowego.
  • Operator jednoargumentowy - wstępnie zdefiniowany (§12.9.3), gdy operand jest typu całkowitego.
  • Wstępnie zdefiniowane operatory binarne +, -, *i / (§12.10), gdy oba operandy są typami całkowitymi lub wyliczeniowymi.
  • Jawne konwersje liczbowe (§10.3.2) z jednego typu wyliczenia lub całkowitego do innego typu wyliczenia lub całkowitego albo z float lub double do typu wyliczenia lub całkowitego.

Gdy jedna z powyższych operacji generuje wynik zbyt duży do reprezentowania w typie docelowym, kontekst, w którym wykonywana jest operacja, kontroluje wynikowe zachowanie:

  • W kontekście checked, jeśli operacja jest wyrażeniem stałym (§12.23), występuje błąd czasu kompilacji. W przeciwnym razie, gdy operacja jest wykonywana w trakcie działania, System.OverflowException zostanie zgłoszony.
  • W kontekście unchecked wynik jest obcinany przez odrzucenie wszystkich bitów o wysokiej kolejności, które nie mieszczą się w typie docelowym.

W przypadku wyrażeń niestałych (§12.23) (wyrażenia, które są oceniane w czasie wykonywania), które nie są ujęte w żadne operatory checked lub unchecked ani instrukcje, domyślny kontekst sprawdzania przepełnienia jest wyłączony, chyba że czynniki zewnętrzne (takie jak przełączniki kompilatora i konfiguracja środowiska wykonawczego) wymagają sprawdzenia.

W przypadku wyrażeń stałych (§12.23) (wyrażenia, które mogą być w pełni oceniane w czasie kompilacji), domyślny kontekst sprawdzania przepełnienia jest zawsze sprawdzany. O ile wyrażenie stałe nie zostanie wyraźnie ustawione w kontekście unchecked, przepełnienia występujące podczas oceny kompilacji wyrażenia zawsze powodują błędy czasu kompilacji.

Ciało funkcji anonimowej nie jest wpływane przez konteksty checked lub unchecked, w których występuje funkcja anonimowa.

Przykład: w poniższym kodzie

class Test
{
    static readonly int x = 1000000;
    static readonly int y = 1000000;

    static int F() => checked(x * y);    // Throws OverflowException
    static int G() => unchecked(x * y);  // Returns -727379968
    static int H() => x * y;             // Depends on default
}

nie są zgłaszane żadne błędy czasu kompilacji, ponieważ żadne z wyrażeń nie może być oceniane w czasie kompilacji. W czasie wykonywania metoda F zgłasza System.OverflowException, a metoda G zwraca wartość –727379968 (niższe 32 bity wyniku poza zakresem). Zachowanie metody H zależy od domyślnego kontekstu sprawdzania przepełnienia podczas kompilacji; jest to albo takie same jak F, albo takie same jak G.

koniec przykładu

Przykład: w poniższym kodzie

class Test
{
    const int x = 1000000;
    const int y = 1000000;

    static int F() => checked(x * y);    // Compile-time error, overflow
    static int G() => unchecked(x * y);  // Returns -727379968
    static int H() => x * y;             // Compile-time error, overflow
}

Przepełnienia, które występują podczas oceniania wyrażeń stałych w F i H, powodują błędy kompilacji, ponieważ wyrażenia te są oceniane w kontekście checked. Przepełnienie występuje również podczas obliczania wyrażenia stałego w G, ale ponieważ ocena odbywa się w kontekście unchecked, przepełnienie nie jest zgłaszane.

koniec przykładu

Operatory checked i unchecked mają wpływ tylko na kontekst sprawdzania przepełnienia dla tych operacji, które są tekstowo zawarte w tokenach "(" i ")". Operatory nie mają wpływu na elementy członkowskie funkcji, które są wywoływane w wyniku oceny zawartego wyrażenia.

Przykład: w poniższym kodzie

class Test
{
    static int Multiply(int x, int y) => x * y;

    static int F() => checked(Multiply(1000000, 1000000));
}

użycie checked w F nie ma wpływu na ocenę x * y w Multiply, dlatego x * y jest obliczany w domyślnym kontekście sprawdzania przepełnienia.

koniec przykładu

Operator unchecked jest wygodny podczas pisania stałych typów całkowitych ze znakiem w notacji szesnastkowej.

Przykład:

class Test
{
    public const int AllBits = unchecked((int)0xFFFFFFFF);
    public const int HighBit = unchecked((int)0x80000000);
}

Obie powyższe stałe szesnastkowe są typu uint. Ponieważ stałe znajdują się poza zakresem int, bez operatora unchecked rzutowania do int spowodowałyby błędy czasu kompilacji.

koniec przykładu

Uwaga: operatory i instrukcje checkedunchecked umożliwiają programistom kontrolowanie wybranych aspektów obliczeń liczbowych. Jednak zachowanie niektórych operatorów liczbowych zależy od typów danych operandów. Na przykład pomnożenie dwóch liczb dziesiętnych zawsze powoduje wyjątek przepełnienia, nawet w jawnie niekontrolowanej konstrukcji. Podobnie, pomnożenie dwóch liczb zmiennoprzecinkowych nigdy nie powoduje wyjątku z powodu przepełnienia, nawet w jawnie sprawdzanej konstrukcji. Ponadto inne operatory nigdy nie mają wpływu na tryb sprawdzania, czy jest to ustawienie domyślne, czy jawne. uwaga końcowa

12.8.21 Wyrażenia wartości domyślnej

Domyślne wyrażenie wartości służy do uzyskiwania wartości domyślnej (§9.3) typu.

default_value_expression
    : explictly_typed_default
    | default_literal
    ;

explictly_typed_default
    : 'default' '(' type ')'
    ;

default_literal
    : 'default'
    ;

default_literal reprezentuje wartość domyślną (§9.3). Nie ma określonego typu, ale można go przekonwertować na dowolny typ za pomocą domyślnej konwersji literału (§10.2.16).

Wynikiem default_value_expression jest wartość domyślna (§9.3) jawnego typu w explictly_typed_defaultlub typ docelowy konwersji dla default_value_expression.

default_value_expression jest wyrażeniem stałym (§12.23), jeśli typ jest jednym z:

  • typ referencyjny
  • parametr typu, który znany jest jako typ referencyjny (§8.2);
  • jeden z następujących typów wartości: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool,; lub
  • dowolny typ wyliczenia.

12.8.22 Alokacja stosu

Wyrażenie alokacji stosu przydziela blok pamięci ze stosu wykonywawczego. Stos wykonawczy to obszar pamięci, w którym są przechowywane zmienne lokalne. Stos wykonywania nie jest częścią zarządzanego sterta. Pamięć używana do przechowywania zmiennych lokalnych jest automatycznie odzyskiwane po powrocie bieżącej funkcji.

Reguły bezpiecznego kontekstu dla wyrażenia alokacji stosu są opisane w §16.4.12.7.

stackalloc_expression
    : 'stackalloc' unmanaged_type '[' expression ']'
    | 'stackalloc' unmanaged_type? '[' constant_expression? ']' stackalloc_initializer
    ;

stackalloc_initializer
     : '{' stackalloc_initializer_element_list '}'
     ;

stackalloc_initializer_element_list
     : stackalloc_element_initializer (',' stackalloc_element_initializer)* ','?
     ;
    
stackalloc_element_initializer
    : expression
    ;

unmanaged_type (§8.8) wskazuje typ elementów, które będą przechowywane w nowo przydzielonej lokalizacji, a wyrażenie wskazuje liczbę tych elementów. Te elementy określają wymagany rozmiar alokacji. Typ wyrażenia jest niejawnie konwertowany na typ int.

Ponieważ rozmiar alokacji stosu nie może być ujemny, zgłoszenie liczby elementów jako wyrażenia stałego constant_expression, które daje wynik ujemny, jest błędem w czasie kompilacji.

W czasie wykonywania, jeśli liczba elementów do przydzielenia jest wartością ujemną, zachowanie jest niezdefiniowane. Jeśli ma wartość zero, nie zostanie wykonana żadna alokacja, a zwracana wartość jest zdefiniowana przez implementację. Jeśli nie ma wystarczającej ilości dostępnej pamięci, aby przydzielić elementy, zostanie wyrzucony System.StackOverflowException.

Gdy stackalloc_initializer jest obecny:

  • Jeśli unmanaged_type zostanie pominięty, zostanie on wywnioskowany zgodnie z regułami dla najlepszego typu wspólnego (§12.6.3.15) dla zestawu stackalloc_element_initializers.
  • Jeśli constant_expression zostanie pominięta, zostanie wywnioskowana liczba stackalloc_element_initializers.
  • Jeśli constant_expression jest obecny, musi być równa liczbie stackalloc_element_initializers.

Każdy stackalloc_element_initializer będzie miał niejawną konwersję na unmanaged_type (§10.2). stackalloc_element_initializerinicjuje elementy w przydzielonej pamięci w kolejności rosnącej, zaczynając od elementu o indeksie zero. W przypadku braku stackalloc_initializerzawartość nowo przydzielonej pamięci jest niezdefiniowana.

Jeśli stackalloc_expression występuje bezpośrednio jako wyrażenie inicjujące local_variable_declaration (§13.6.2), gdzie local_variable_type jest albo typem wskaźnika (§23.3) lub wywnioskowany (var), wówczas wynik stackalloc_expression jest wskaźnikiem typu T* (§23.9). W takim przypadku stackalloc_expression musi występować w niebezpiecznym kodzie. W przeciwnym razie wynikiem stackalloc_expression jest wystąpienie typu Span<T>, gdzie T jest unmanaged_type:

  • Span<T> (§C.3) jest typem struktury ref (§16.2.3), który przedstawia blok pamięci, tutaj blok przydzielony przez stackalloc_expression, jako kolekcji indeksowalnej typu (T) elementów.
  • Właściwość Length wyniku zwraca liczbę przydzielonych elementów.
  • Indeksator wyniku (§15.9) zwraca odwołanie_do_zmiennej (§9.5) do elementu przydzielonego bloku i sprawdza zakres.

Inicjalizatory alokacji stosu nie są dozwolone w blokach catch lub finally (§13.11).

Uwaga: nie ma możliwości jawnego zwolnienia pamięci przydzielonej przy użyciu stackalloc. uwaga końcowa

Wszystkie bloki pamięci przydzielone do stosu utworzone podczas wykonywania elementu członkowskiego funkcji są automatycznie odrzucane po powrocie tego elementu członkowskiego funkcji.

Z wyjątkiem operatora stackalloc, język C# nie udostępnia wstępnie zdefiniowanych konstrukcji do zarządzania pamięcią niezbieraną przez odśmiecacz. Takie usługi są zwykle udostępniane przez biblioteki klas pomocniczych lub importowane bezpośrednio z bazowego systemu operacyjnego.

Przykład:

// Memory uninitialized
Span<int> span1 = stackalloc int[3];
// Memory initialized
Span<int> span2 = stackalloc int[3] { -10, -15, -30 };
// Type int is inferred
Span<int> span3 = stackalloc[] { 11, 12, 13 };
// Error; result is int*, not allowed in a safe context
var span4 = stackalloc[] { 11, 12, 13 };
// Error; no conversion from Span<int> to Span<long>
Span<long> span5 = stackalloc[] { 11, 12, 13 };
// Converts 11 and 13, and returns Span<long> 
Span<long> span6 = stackalloc[] { 11, 12L, 13 };
// Converts all and returns Span<long>
Span<long> span7 = stackalloc long[] { 11, 12, 13 };
// Implicit conversion of Span<T>
ReadOnlySpan<int> span8 = stackalloc int[] { 10, 22, 30 };
// Implicit conversion of Span<T>
Widget<double> span9 = stackalloc double[] { 1.2, 5.6 };

public class Widget<T>
{
    public static implicit operator Widget<T>(Span<double> sp) { return null; }
}

W przypadku span8stackalloc powoduje Span<int>, który jest konwertowany przez operator niejawny na ReadOnlySpan<int>. Podobnie dla span9wynikowy Span<double> jest konwertowany na typ zdefiniowany przez użytkownika Widget<double> przy użyciu konwersji, jak pokazano. koniec przykładu

12.8.23 Operator nameof

nameof_expression służy do uzyskiwania nazwy jednostki programu jako ciągu stałego.

nameof_expression
    : 'nameof' '(' named_entity ')'
    ;
    
named_entity
    : named_entity_target ('.' identifier type_argument_list?)*
    ;
    
named_entity_target
    : simple_name
    | 'this'
    | 'base'
    | predefined_type 
    | qualified_alias_member
    ;

Ponieważ nameof nie jest słowem kluczowym, nameof_expression zawsze jest składniowo niejednoznaczny z wywołaniem prostej nazwy nameof. Ze względów zgodności, jeśli wyszukiwanie nazw (§12.8.4) nazwy nameof powiedzie się, wyrażenie jest traktowane jako invocation_expression — niezależnie od tego, czy wywołanie jest prawidłowe. W przeciwnym razie jest to nameof_expression.

Proste wyszukiwania nazw i dostępu do składowych są wykonywane w czasie kompilacji na named_entity, zgodnie z regułami opisanymi w §12.8.4 i §12.8.7. Jednak jeśli wyszukiwanie opisane w §12.8.4 i §12.8.7 powoduje błąd, ponieważ element członkowski wystąpienia został znaleziony w kontekście statycznym, nameof_expression nie generuje takiego błędu.

Jest to błąd czasu kompilacji dla named_entity wyznaczenia grupy metod, aby mieć type_argument_list. Jest to błąd czasu kompilacji dla named_entity_target, by miał typ dynamic.

nameof_expression jest stałym wyrażeniem typu stringi nie ma wpływu na środowisko uruchomieniowe. W szczególności jego named_entity nie jest oceniana i jest ignorowana do celów analizy określonego przypisania (§9.4.4.22). Jego wartość jest ostatnim identyfikatorem named_entity przed opcjonalną końcową listą argumentów typu , przekształconym w następujący sposób:

  • Prefiks "@", jeśli jest używany, zostanie usunięty.
  • Każda sekwencja unikania Unicode jest przekształcana w odpowiadający znak Unicode.
  • Wszystkie znaki formatujące są usuwane.

Są to te same przekształcenia stosowane w §6.4.3 podczas testowania równości między identyfikatorami.

Przykład: Poniżej przedstawiono wyniki różnych wyrażeń nameof, w założeniu, że typ ogólny List<T> jest zadeklarowany w przestrzeni nazw System.Collections.Generic:

using TestAlias = System.String;

class Program
{
    static void Main()
    {
        var point = (x: 3, y: 4);

        string n1 = nameof(System);                      // "System"
        string n2 = nameof(System.Collections.Generic);  // "Generic"
        string n3 = nameof(point);                       // "point"
        string n4 = nameof(point.x);                     // "x"
        string n5 = nameof(Program);                     // "Program"
        string n6 = nameof(System.Int32);                // "Int32"
        string n7 = nameof(TestAlias);                   // "TestAlias"
        string n8 = nameof(List<int>);                   // "List"
        string n9 = nameof(Program.InstanceMethod);      // "InstanceMethod"
        string n10 = nameof(Program.GenericMethod);      // "GenericMethod"
        string n11 = nameof(Program.NestedClass);        // "NestedClass"

        // Invalid
        // string x1 = nameof(List<>);            // Empty type argument list
        // string x2 = nameof(List<T>);           // T is not in scope
        // string x3 = nameof(GenericMethod<>);   // Empty type argument list
        // string x4 = nameof(GenericMethod<T>);  // T is not in scope
        // string x5 = nameof(int);               // Keywords not permitted
        // Type arguments not permitted for method group
        // string x6 = nameof(GenericMethod<Program>);
    }

    void InstanceMethod() { }

    void GenericMethod<T>()
    {
        string n1 = nameof(List<T>); // "List"
        string n2 = nameof(T);       // "T"
    }

    class NestedClass { }
}

Potencjalnie zaskakujące części tego przykładu to mapowanie nameof(System.Collections.Generic) do „Generic” zamiast pełnej przestrzeni nazw oraz nameof(TestAlias) do „TestAlias”, a nie „String”. koniec przykładu

12.8.24 Wyrażenia metody anonimowej

anonimowe_wyrażenie_metodowe jest jednym z dwóch sposobów definiowania funkcji anonimowej. Zostały one opisane w §12.19.

12.9 Operatory jednoargumentowe

12.9.1 Ogólne

Operatory +, -, ! (tylko negacja logiczna §12.9.4), ~, ++, --, rzutowanie i await to operatory jednoargumentowe.

Uwaga: operator postfix o wartości null-forgiving (§12.8.9), !, ze względu na jego charakterystykę kompilacji oraz brak możliwości przeciążania, jest wykluczony z powyższej listy. uwaga końcowa

unary_expression
    : primary_expression
    | '+' unary_expression
    | '-' unary_expression
    | logical_negation_operator unary_expression
    | '~' unary_expression
    | pre_increment_expression
    | pre_decrement_expression
    | cast_expression
    | await_expression
    | pointer_indirection_expression    // unsafe code support
    | addressof_expression              // unsafe code support
    ;

pointer_indirection_expression (§23.6.2) i addressof_expression (§23.6.5) są dostępne tylko w niebezpiecznym kodzie (§23).

Jeśli operand unary_expression ma typ czasu kompilacji dynamic, jest on dynamicznie powiązany (§12.3.3). W tym przypadku typ czasu kompilacji unary_expression jest dynamic, a rozwiązanie opisane poniżej będzie odbywać się w czasie wykonywania przy użyciu typu czasu wykonywania operandu.

Operator jednoargumentowy plus 12.9.2

W przypadku operacji postaci +x, rozpoznawanie przeciążenia operatora jednoargumentowego (§12.4.4) jest stosowane w celu wybrania konkretnej implementacji operatora. Operand jest konwertowany na typ parametru wybranego operatora, a typ wyniku jest zwracanym typem operatora. Wstępnie zdefiniowane operatory jednoargumentowe plus to:

int operator +(int x);
uint operator +(uint x);
long operator +(long x);
ulong operator +(ulong x);
float operator +(float x);
double operator +(double x);
decimal operator +(decimal x);

Dla każdego z tych operatorów wynik jest po prostu wartością operandu.

Podniesione (§12.4.8) formy niepodniesionych wstępnie zdefiniowanych jednoargumentowych operatorów plus zdefiniowanych powyżej są również wstępnie zdefiniowanymi.

12.9.3 Jednoargumentowy operator minus

W przypadku operacji postaci –x, rozpoznawanie przeciążenia operatora jednoargumentowego (§12.4.4) jest stosowane w celu wybrania konkretnej implementacji operatora. Operand jest konwertowany na typ parametru wybranego operatora, a typ wyniku jest zwracanym typem operatora. Wstępnie zdefiniowane jednoargumentowe operatory minus to:

  • Negacja liczb całkowitych:

    int operator –(int x);
    long operator –(long x);
    

    Wynik jest obliczany przez odjęcie X od zera. Jeśli wartość X jest najmniejszą reprezentującą wartością typu operandu (−2³² dla int lub −2⁶³ dla long), nie można przedstawić matematycznej negacji X w typie operandu. Jeśli to występuje w kontekście checked, rzucany jest System.OverflowException; jeśli to się dzieje w kontekście unchecked, wynik to wartość operandu, a przepełnienie nie jest zgłaszane.

    Jeśli operand operatora negacji jest typu uint, jest konwertowany na typ long, a typ wyniku jest long. Wyjątkiem jest reguła zezwalająca na zapisanie wartości int−2147483648 (−2³²) jako literału liczby całkowitej dziesiętnej (§6.4.5.3).

    Jeśli operand operatora negacji jest typu ulong, wystąpi błąd kompilacji. Wyjątkiem jest reguła zezwalająca na zapis wartości long−9223372036854775808 (−2⁶³) jako literał liczby całkowitej dziesiętnej (§6.4.5.3)

  • Negacja zmiennoprzecinkowa:

    float operator –(float x);
    double operator –(double x);
    

    Wynikiem jest wartość X z odwróconym znakiem. Jeśli x jest NaN, wynik jest również NaN.

  • Negacja dziesiętna:

    decimal operator –(decimal x);
    

    Wynik jest obliczany przez odjęcie X od zera. Negacja dziesiętna jest równoważna użyciu jednoargumentowego operatora minus typu System.Decimal.

Podniesione (§12.4.8) formy zdefiniowanych powyżej niepodniesionych jednoargumentowych operatorów minus są również wstępnie zdefiniowane.

12.9.4 Operator negacji logicznej

W przypadku operacji postaci !x, rozpoznawanie przeciążenia operatora jednoargumentowego (§12.4.4) jest stosowane w celu wybrania konkretnej implementacji operatora. Operand jest konwertowany na typ parametru wybranego operatora, a typ wyniku jest zwracanym typem operatora. Istnieje tylko jeden wstępnie zdefiniowany operator negacji logicznej:

bool operator !(bool x);

Ten operator oblicza logiczną negację operandu: jeśli operand jest true, wynik jest false. Jeśli operand jest false, wynik jest true.

Lifted (§12.4.8) formularze nieznieszonego operatora negacji logicznej zdefiniowanej powyżej są również wstępnie zdefiniowane.

Uwaga: logiczna negacja prefiksowa i postfiksowe operatory umożliwiające pustą wartość (§12.8.9), choć reprezentowane przez ten sam token leksykalny (!), są odrębne. uwaga końcowa

Operator uzupełniania bitowego 12.9.5

W przypadku operacji postaci ~x, rozpoznawanie przeciążenia operatora jednoargumentowego (§12.4.4) jest stosowane w celu wybrania konkretnej implementacji operatora. Operand jest konwertowany na typ parametru wybranego operatora, a typ wyniku jest zwracanym typem operatora. Wstępnie zdefiniowane operatory uzupełniania bitowego to:

int operator ~(int x);
uint operator ~(uint x);
long operator ~(long x);
ulong operator ~(ulong x);

Dla każdego z tych operatorów wynik operacji jest bitowym uzupełnieniem x.

Każdy typ wyliczenia E niejawnie udostępnia następujący operator uzupełniania bitowego:

E operator ~(E x);

Wynik oceny ~x, gdzie X jest wyrażeniem typu wyliczenia E z podstawowym typem U, jest dokładnie taki sam jak ocena (E)(~(U)x), z tą różnicą, że konwersja na E jest zawsze wykonywana tak, jakby w kontekście unchecked (§12.8.20).

Podniesione (§12.4.8) wersje wstępnie zdefiniowanych podstawowych operatorów uzupełniania bitowego zdefiniowane powyżej są również wstępnie zdefiniowane.

12.9.6 Operatory przyrostków i dekrementacji

pre_increment_expression
    : '++' unary_expression
    ;

pre_decrement_expression
    : '--' unary_expression
    ;

Operand operacji inkrementacji lub dekrementacji prefiksowej powinien być wyrażeniem sklasyfikowanym jako zmienna, odwołanie do właściwości lub dostęp do indeksatora. Wynikiem działania jest wartość tego samego typu co operandu.

Jeśli operand operacji przyrostka lub dekrementacji jest właściwością lub indeksatorem dostępu, właściwość lub indeksator musi mieć zarówno metodę get, jak i zestaw dostępu. Jeśli tak nie jest, wystąpi błąd powiązania czasowego.

Rozpoznawanie przeciążenia operatora jednoargumentowego (§12.4.4) jest stosowane w celu wybrania określonej implementacji operatora. Istnieją wstępnie zdefiniowane operatory ++ i -- dla następujących typów: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimali dowolnego typu wyliczeniowego. Wstępnie zdefiniowane operatory ++ zwracają wartość wygenerowaną przez dodanie 1 do operandu, a wstępnie zdefiniowane operatory -- zwracają wartość wygenerowaną przez odjęcie 1 z operandu. W kontekście checked, jeśli wynik tego dodawania lub odejmowania znajduje się poza zakresem typu wyniku i typ wyniku jest typem całkowitym lub typem wyliczenia, zgłaszany jest wyjątek System.OverflowException.

Musi nastąpić niejawna konwersja z typu zwrotnego wybranego operatora jednoargumentowego do typu unary_expression, w przeciwnym razie pojawi się błąd kompilacji.

Przetwarzanie w czasie działania operacji przyrostu prefiksu lub dekrementacji w formie ++x lub --x składa się z następujących kroków:

  • Jeśli x jest klasyfikowana jako zmienna:
    • x jest obliczana w celu utworzenia zmiennej.
    • Wartość x jest konwertowana na typ operandu wybranego operatora, a operator jest wywoływany z tą wartością jako argumentem.
    • Wartość zwracana przez operatora jest konwertowana na typ x. Wynikowa wartość jest przechowywana w lokalizacji podanej przez ocenę x.
    • i staje się wynikiem operacji.
  • Jeśli x jest klasyfikowana jako właściwość lub dostęp indeksatora:
    • Wyrażenie wystąpienia (jeśli x nie jest static) oraz lista argumentów (jeśli x jest dostępem indeksatora) skojarzona z x są ewaluowane, a ich wyniki są następnie używane w kolejnych wywołaniach metod get i set.
    • Wywołano metodę get x.
    • Wartość zwracana przez metodę pobierania jest konwertowana na typ operandu wybranego operatora, a operator jest wywoływany z tą wartością jako argumentem.
    • Wartość zwracana przez operatora jest konwertowana na typ x. Akcesor ustawiający x jest wywoływany z tą wartością jako argumentem o wartości.
    • Ta wartość staje się również wynikiem operacji.

Operatory ++ i -- obsługują również notację postfiksu (§12.8.16). Wynikiem x++ lub x-- jest wartość x przed operacją, natomiast wynikiem ++x lub --x jest wartość x po operacji. W obu przypadkach sama x ma tę samą wartość po operacji.

Implementację operatora ++ lub operatora -- można wywołać, używając notacji postfiksowej lub prefiksowej. Nie można mieć oddzielnych implementacji operatorów dla dwóch notacji.

Uniesione (§12.4.8) formy nieuniesionych wstępnie zdefiniowanych operatorów inkrementacji i dekrementacji, które zostały zdefiniowane powyżej, są również wstępnie zdefiniowane.

12.9.7 Wyrażenia rzutowe

cast_expression służy do jawnego konwertowania wyrażenia na dany typ.

cast_expression
    : '(' type ')' unary_expression
    ;

cast_expression w formie (T)E, gdzie T jest typem, a E jest wyrażeniem jednoargumentowym , wykonuje jawną konwersję (§10.3) wartości E do typu T. Jeśli nie istnieje jawna konwersja z E do T, wystąpi błąd czasu powiązania. W przeciwnym razie wynikiem jest wartość wygenerowana przez jawną konwersję. Wynik jest zawsze klasyfikowany jako wartość, nawet jeśli E oznacza zmienną.

Gramatyka wyrażenia cast_expression powoduje pewne niejednoznaczności składniowe.

Przykład: wyrażenie (x)–y może być interpretowane jako cast_expression (rzut –y do wpisywania x) lub jako additive_expression połączone z parenthesized_expression (która oblicza wartość x – y). koniec przykładu

Aby rozwiązać cast_expression niejednoznaczności, istnieje następująca reguła: Sekwencja co najmniej jednego tokenu (§6.4) ujęta w nawiasy jest traktowana jako początek cast_expression tylko wtedy, gdy co najmniej jedna z następujących wartości jest prawdziwa:

  • Sekwencja tokenów jest poprawną gramatyką dla typu, ale nie dla wyrażenia.
  • Sekwencja tokenów jest poprawną składnią dla typu, i token bezpośrednio po zamykających nawiasach jest tokenem "~", tokenem "!", tokenem "(", identyfikatorem (§6.4.3), literałem (§6.4.5) lub dowolnym słowem kluczowym (§6.4.4) z wyjątkiem as i is.

Termin "poprawna gramatyka" powyżej oznacza tylko, że sekwencja tokenów jest zgodna z określoną produkcją gramatyczną. W szczególności nie uwzględnia rzeczywistego znaczenia żadnych identyfikatorów składowych.

Przykład: jeśli x i y są identyfikatorami, x.y jest poprawną gramatyką dla typu, nawet jeśli x.y w rzeczywistości nie oznacza typu. koniec przykładu

Uwaga: z reguły uściślania wynika, że jeśli x i y są identyfikatorami, (x)y, (x)(y)i (x)(-y)cast_expressions, ale (x)-y nie jest, nawet jeśli x identyfikuje typ. Jeśli jednak x jest słowem kluczowym identyfikującym wstępnie zdefiniowany typ (taki jak int), wszystkie cztery formularze są cast_expressions (ponieważ takie słowo kluczowe nie może być wyrażeniem samym w sobie). uwaga końcowa

12.9.8 Await — wyrażenia

12.9.8.1 Ogólne

Operator await służy do wstrzymania oceny otaczającej funkcji asynchronicznej do momentu zakończenia operacji asynchronicznej reprezentowanej przez operand.

await_expression
    : 'await' unary_expression
    ;

await_expression jest dozwolona tylko w treści funkcji asynchronicznej (§15.15). W najbliższej otaczającej funkcji asynchronicznej await_expression nie może występować w następujących miejscach:

  • Wewnątrz zagnieżdżonej funkcji anonimowej (nieasynchronicznej)
  • Wewnątrz bloku lock_statement
  • W konwersji funkcji anonimowej na typ drzewa wyrażeń (§10.7.3)
  • W niebezpiecznym kontekście

Uwaga: await_expression nie może wystąpić w większości miejsc w query_expression, ponieważ są one syntatycznie przekształcone w celu używania niesynchronicznych wyrażeń lambda. uwaga końcowa

Wewnątrz funkcji asynchronicznej nie należy używać await jako available_identifier, chociaż identyfikator dosłowny @await może być użyty. W związku z tym nie ma niejednoznaczności składni między await_expressions i różnymi wyrażeniami obejmującymi identyfikatory. Poza funkcjami asynchronicznych await pełni rolę normalnego identyfikatora.

Operand await_expression nazywa się zadaniem . Reprezentuje on operację asynchroniczną, która może lub nie może zostać ukończona w momencie oceny await_expression. Celem operatora await jest wstrzymanie wykonywania otaczającej funkcji asynchronicznej do momentu ukończenia tego zadania, a następnie uzyskania jego wyniku.

12.9.8.2 Oczekiwane wyrażenia

Zadanie await_expression musi być oczekiwalne. Wyrażenie t jest oczekiwalne, jeśli jest spełniony jeden z następujących warunków:

  • t jest typu czasu kompilacji dynamic
  • t ma dostępną metodę instancji lub metodę rozszerzającą o nazwie GetAwaiter bez parametrów ani parametrów typu ze zwracanym typem A, dla którego wszystkie poniższe warunki są spełnione:
    • A implementuje interfejs System.Runtime.CompilerServices.INotifyCompletion (zwany dalej INotifyCompletion na potrzeby zwięzłości)
    • A ma dostępną, czytelną właściwość wystąpienia IsCompleted typu bool
    • A ma dostępną metodę instancji GetResult bez parametrów ani parametrów typu

Celem metody GetAwaiter jest uzyskanie awaiter dla zadania. Typ A nazywany jest typem awaiter dla wyrażenia await.

Celem właściwości IsCompleted jest określenie, czy zadanie zostało już ukończone. Jeśli tak, nie ma potrzeby zawieszania oceny.

Celem metody INotifyCompletion.OnCompleted jest zarejestrowanie "kontynuacji" zadania; tj. delegat (typu System.Action), który zostanie wywołany po zakończeniu zadania.

Celem metody GetResult jest uzyskanie wyniku zadania po jego zakończeniu. Wynik ten może być pomyślnym zakończeniem, możliwie z wartością wyniku, lub może to być wyjątek rzucany przez metodę GetResult.

12.9.8.3 Klasyfikacja wyrażeń await

Wyrażenie await t jest klasyfikowane tak samo jak wyrażenie (t).GetAwaiter().GetResult(). W związku z tym, jeśli zwracany typ GetResult jest void, await_expression jest klasyfikowany jako nic. Jeśli ma typ zwracany inny niżvoidT, wyrażenie oczekujące jest klasyfikowane jako wartość typu T.

12.9.8.4 Ocena czasu wykonywania wyrażeń await

W czasie wykonywania wyrażenie await t jest oceniane w następujący sposób:

  • Awaiter a jest uzyskiwany przez obliczenie wyrażenia (t).GetAwaiter().
  • bool b uzyskuje się przez obliczenie wyrażenia (a).IsCompleted.
  • Jeśli b jest false, ocena zależy od tego, czy a implementuje interfejs System.Runtime.CompilerServices.ICriticalNotifyCompletion (określaną dalej jako ICriticalNotifyCompletion dla zwięzłości). Ta kontrola jest wykonywana w momencie wiązania; czyli w czasie wykonywania, jeśli a ma typ określony w czasie kompilacji jako dynamic, lub w czasie kompilacji w przeciwnym razie. Niech r oznacza delegata wznowienia (§15.15):
    • Jeśli a nie implementuje ICriticalNotifyCompletion, zostanie obliczone wyrażenie ((a) as INotifyCompletion).OnCompleted(r).
    • Jeśli a implementuje ICriticalNotifyCompletion, zostanie obliczone wyrażenie ((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r).
    • Ocena jest następnie zawieszona, a kontrolka jest zwracana do bieżącego obiektu wywołującego funkcji async.
  • Albo natychmiast po (jeśli b jest true), lub po późniejszym wywołaniu delegata wznowienia (jeśli b jest false), wyrażenie (a).GetResult() jest obliczane. Jeśli zwracana jest jakaś wartość, to jest ona wynikiem wyrażenia await_expression. W przeciwnym razie wynik nie jest niczym.

Implementacja metod interfejsu INotifyCompletion.OnCompleted i ICriticalNotifyCompletion.UnsafeOnCompleted powinna spowodować, że delegat r zostanie wywołany co najwyżej raz. W przeciwnym razie zachowanie funkcji asynchronicznej otaczającej jest niezdefiniowane.

12.10 Operatory arytmetyczne

12.10.1 Ogólne

Operatory *, /, %, +i - są nazywane operatorami arytmetycznymi.

multiplicative_expression
    : unary_expression
    | multiplicative_expression '*' unary_expression
    | multiplicative_expression '/' unary_expression
    | multiplicative_expression '%' unary_expression
    ;

additive_expression
    : multiplicative_expression
    | additive_expression '+' multiplicative_expression
    | additive_expression '-' multiplicative_expression
    ;

Jeśli operand operatora arytmetycznego ma typ czasu kompilacji dynamic, wyrażenie jest dynamicznie powiązane (§12.3.3). W tym przypadku typ czasu kompilacji wyrażenia jest dynamic, a rozwiązanie opisane poniżej będzie odbywać się w czasie wykonywania przy użyciu typu czasu wykonywania tych operandów, które mają typ czasu kompilacji dynamic.

Operator mnożenia 12.10.2

W przypadku operacji formularza x * yfunkcja rozpoznawania przeciążenia operatora binarnego (§12.4.5) jest stosowana w celu wybrania określonej implementacji operatora. Operandy są konwertowane na typy parametrów wybranego operatora, a typ wyniku jest zwracanym typem operatora.

Poniżej wymieniono wstępnie zdefiniowane operatory mnożenia. Wszystkie operatory obliczają produkt x oraz y.

  • Mnożenie całkowite:

    int operator *(int x, int y);
    uint operator *(uint x, uint y);
    long operator *(long x, long y);
    ulong operator *(ulong x, ulong y);
    

    W kontekście checked, jeśli produkt znajduje się poza zakresem typu wyniku, zostanie zgłoszony System.OverflowException. W kontekście unchecked przepełnienia nie są zgłaszane, a wszystkie znaczące bity o wysokiej kolejności poza zakresem typu wyniku są odrzucane.

  • Mnożenie zmiennoprzecinkowe:

    float operator *(float x, float y);
    double operator *(double x, double y);
    

    Produkt jest obliczany zgodnie z regułami arytmetyki IEC 60559. W poniższej tabeli wymieniono wyniki wszystkich możliwych kombinacji wartości niezerowych, zer, nieskończoności i sieci NaNs. W tabeli x i y są dodatnimi wartościami skończonymi. z jest wynikiem x * y, zaokrąglone do najbliższej reprezentowalnej wartości. Jeśli wartość wyniku jest zbyt duża dla docelowego typu, z jest nieskończonością. Ze względu na zaokrąglenie, z może być zerem, mimo że ani x, ani y nie są zerami.

    +y -y +0 -0 +∞ -∞ NaN
    +x +z -z +0 -0 +∞ -∞ NaN
    -x -z +z -0 +0 -∞ +∞ NaN
    +0 +0 -0 +0 -0 NaN NaN NaN
    -0 -0 +0 -0 +0 NaN NaN NaN
    +∞ +∞ -∞ NaN NaN +∞ -∞ NaN
    -∞ -∞ +∞ NaN NaN -∞ +∞ NaN
    NaN NaN NaN NaN NaN NaN NaN NaN

    (Z wyjątkiem sytuacji, w których w tabelach zmiennoprzecinkowych w §12.10.2§12.10.6 użycie wartości "+" oznacza, że wartość jest dodatnia; użycie wartości "-" oznacza, że wartość jest ujemna; a brak znaku oznacza, że wartość może być dodatnia lub ujemna lub nie ma znaku (NaN).

  • Mnożenie dziesiętne:

    decimal operator *(decimal x, decimal y);
    

    Jeśli wynikowa wartość jest zbyt duża, aby przedstawić ją w formacie dziesiętnym, zgłaszany jest System.OverflowException. Ze względu na zaokrąglenie wynik może być zerowy, mimo że żaden operand nie jest zerowy. Skala wyniku, przed zaokrągleniami, jest sumą skali dwóch operandów. Mnożenie dziesiętne jest równoważne użyciu operatora mnożenia typu System.Decimal.

Podniesione (§12.4.8) formy niepodniesionych wcześniej zdefiniowanych operatorów mnożenia zdefiniowanych powyżej są również wstępnie określone.

Operator dzielenia 12.10.3

W przypadku operacji formularza x / yfunkcja rozpoznawania przeciążenia operatora binarnego (§12.4.5) jest stosowana w celu wybrania określonej implementacji operatora. Operandy są konwertowane na typy parametrów wybranego operatora, a typ wyniku jest zwracanym typem operatora.

Poniżej wymieniono wstępnie zdefiniowane operatory dzielenia. Wszyscy operatorzy obliczają iloraz x i y.

  • Dzielenie liczb całkowitych:

    int operator /(int x, int y);
    uint operator /(uint x, uint y);
    long operator /(long x, long y);
    ulong operator /(ulong x, ulong y);
    

    Jeśli wartość prawego operandu wynosi zero, zostanie zgłoszony System.DivideByZeroException.

    Podział zaokrągla wynik do zera. W związku z tym wartość bezwzględna wyniku jest największą możliwą liczbą całkowitą, która jest mniejsza lub równa wartości bezwzględnej ilorazu dwóch operandów. Wynik jest zerowy lub dodatni, gdy dwa operandy mają ten sam znak i zero lub ujemne, gdy dwa operandy mają przeciwstawne znaki.

    Jeśli lewy operand jest najmniejszą reprezentowaną wartością int lub long, a prawy operand jest –1, występuje przepełnienie. W kontekście checked, powoduje to zgłoszenie System.ArithmeticException (lub jego podklasy). W kontekście unchecked definiowane przez implementację jest to, czy zgłaszana jest System.ArithmeticException (lub jej podklasa), czy przepełnienie pozostaje niezauważone, a wartość wynikowa jest wartością lewego operandu.

  • Dzielenie zmiennoprzecinkowe:

    float operator /(float x, float y);
    double operator /(double x, double y);
    

    Iloraz jest obliczany zgodnie z regułami arytmetyki IEC 60559. W poniższej tabeli wymieniono wyniki wszystkich możliwych kombinacji wartości niezerowych, zer, nieskończoności i sieci NaNs. W tabeli x i y są dodatnimi wartościami skończonymi. z jest wynikiem x / y, zaokrąglone do najbliższej reprezentowalnej wartości.

    +y -y +0 -0 +∞ -∞ NaN
    +x +z -z +∞ -∞ +0 -0 NaN
    -x -z +z -∞ +∞ -0 +0 NaN
    +0 +0 -0 NaN NaN +0 -0 NaN
    -0 -0 +0 NaN NaN -0 +0 NaN
    +∞ +∞ -∞ +∞ -∞ NaN NaN NaN
    -∞ -∞ +∞ -∞ +∞ NaN NaN NaN
    NaN NaN NaN NaN NaN NaN NaN NaN
  • Dzielenie dziesiętne:

    decimal operator /(decimal x, decimal y);
    

    Jeśli wartość prawego operandu wynosi zero, zostanie zgłoszony System.DivideByZeroException. Jeśli wynikowa wartość jest zbyt duża, aby przedstawić ją w formacie dziesiętnym, zgłaszany jest System.OverflowException. Ze względu na zaokrąglenie wynik może być zerowy, mimo że pierwszy operand nie jest zerowy. Skala wyniku, przed zaokrągleniami, jest najbliżej preferowanej skali, która zachowa wynik równy dokładnemu wynikowi. Preferowana skala to skala x mniejszej skali y.

    Dzielenie dziesiętne jest równoważne użyciu operatora dzielenia typu System.Decimal.

Podniesione (§12.4.8) formularze niepodniesionych wstępnie zdefiniowanych operatorów dzielenia ustalone powyżej są również wstępnie zdefiniowane.

12.10.4 Operator reszty

W przypadku operacji formularza x % yfunkcja rozpoznawania przeciążenia operatora binarnego (§12.4.5) jest stosowana w celu wybrania określonej implementacji operatora. Operandy są konwertowane na typy parametrów wybranego operatora, a typ wyniku jest zwracanym typem operatora.

Poniżej wymieniono wstępnie zdefiniowane operatory pozostałe. Operatory obliczają pozostałą część podziału między x a y.

  • Pozostała liczba całkowita:

    int operator %(int x, int y);
    uint operator %(uint x, uint y);
    long operator %(long x, long y);
    ulong operator %(ulong x, ulong y);
    

    Wynikiem x % y jest wartość wygenerowana przez x – (x / y) * y. Jeśli y ma wartość zero, zostanie zgłoszony System.DivideByZeroException.

    Jeśli lewy operand jest najmniejszą wartością int lub long, a prawy operand jest –1, System.OverflowException jest rzucany tylko wtedy, gdy x / y zgłosi wyjątek.

  • Reszta zmiennoprzecinkowa

    float operator %(float x, float y);
    double operator %(double x, double y);
    

    W poniższej tabeli wymieniono wyniki wszystkich możliwych kombinacji wartości niezerowych, zer, nieskończoności i sieci NaNs. W tabeli x i y są dodatnimi wartościami skończonymi. z jest wynikiem x % y i jest obliczany jako x – n * y, gdzie n jest największą możliwą liczbą całkowitą mniejszą lub równą x / y. Ta metoda obliczania pozostałej części jest analogiczna do tej używanej dla operacji całkowitych, ale różni się od definicji IEC 60559 (w której n jest liczbą całkowitą znajdującą się najbliżej x / y).

    +y -y +0 -0 +∞ -∞ NaN
    +x +z +z NaN NaN +x +x NaN
    -x -z -z NaN NaN -x -x NaN
    +0 +0 +0 NaN NaN +0 +0 NaN
    -0 -0 -0 NaN NaN -0 -0 NaN
    +∞ NaN NaN NaN NaN NaN NaN NaN
    -∞ NaN NaN NaN NaN NaN NaN NaN
    NaN NaN NaN NaN NaN NaN NaN NaN
  • Reszta dziesiętna

    decimal operator %(decimal x, decimal y);
    

    Jeśli wartość prawego operandu wynosi zero, zostanie zgłoszony System.DivideByZeroException. Jest określone przez implementację, kiedy System.ArithmeticException (lub jego podklasa) jest zgłaszane. Implementacja zgodna z normą nie powinna zgłaszać wyjątku dla x % y w żadnym przypadku, gdy x / y nie zgłasza wyjątku. Skala wyniku, przed zaokrągleniami, jest większa od skali dwóch operandów, a znak wyniku, jeśli niezerowy, jest taki sam jak w przypadku x.

    Pozostała część dziesiętna jest równoważna użyciu operatora reszty typu System.Decimal.

    Uwaga: te reguły zapewniają, że dla wszystkich typów wynik nigdy nie ma odwrotnego znaku lewego operandu. uwaga końcowa

Rozszerzone (§12.4.8) formy uprzednio zdefiniowanych operatorów reszty, które nie są rozszerzone, są również uprzednio zdefiniowane powyżej.

12.10.5 Operator dodawania

W przypadku operacji formularza x + yfunkcja rozpoznawania przeciążenia operatora binarnego (§12.4.5) jest stosowana w celu wybrania określonej implementacji operatora. Operandy są konwertowane na typy parametrów wybranego operatora, a typ wyniku jest zwracanym typem operatora.

Poniżej wymieniono wstępnie zdefiniowane operatory dodawania. Dla typów liczbowych i wyliczeniowych zdefiniowane operatory dodawania obliczają sumę dwóch operandów. Gdy jeden lub oba operandy są typu string, wstępnie zdefiniowane operatory dodawania łączą reprezentację ciągu znaków operandów.

  • Dodawanie liczb całkowitych

    int operator +(int x, int y);
    uint operator +(uint x, uint y);
    long operator +(long x, long y);
    ulong operator +(ulong x, ulong y
    

    W kontekście checked, jeśli suma znajduje się poza zakresem dozwolonym dla danego typu wyniku, rzucany jest System.OverflowException. W kontekście unchecked przepełnienia nie są zgłaszane, a wszystkie znaczące bity o wysokiej kolejności poza zakresem typu wyniku są odrzucane.

  • Dodawanie zmiennoprzecinkowe:

    float operator +(float x, float y);
    double operator +(double x, double y);
    

    Suma jest obliczana zgodnie z regułami arytmetyki IEC 60559. W poniższej tabeli wymieniono wyniki wszystkich możliwych kombinacji wartości niezerowych, zer, nieskończoności i sieci NaNs. W tabeli x i y są wartościami niezerowymi, a z jest wynikiem x + y. Jeśli x i y mają taką samą wielkość, ale przeciwległe znaki, z jest dodatnim zerem. Jeśli x + y jest zbyt duża do reprezentowania w typie docelowym, z jest nieskończonością z tym samym znakiem co x + y.

    y +0 -0 +∞ -∞ NaN
    x z x x +∞ -∞ NaN
    +0 y +0 +0 +∞ –∞ NaN
    -0 y +0 -0 +∞ -∞ NaN
    +∞ +∞ +∞ +∞ +∞ NaN NaN
    -∞ -∞ -∞ -∞ NaN -∞ NaN
    NaN NaN NaN NaN NaN NaN NaN
  • Dodawanie dziesiętne:

    decimal operator +(decimal x, decimal y);
    

    Jeśli wynikowa wartość jest zbyt duża, aby przedstawić ją w formacie dziesiętnym, zgłaszany jest System.OverflowException. Skala wyniku, przed jakimkolwiek zaokrągleniem, jest równa większej ze skali dwóch operandów.

    Dodawanie dziesiętne jest równoważne użyciu operatora dodawania typu System.Decimal.

  • Dodawanie wyliczenia. Każdy typ wyliczenia niejawnie udostępnia następujące wstępnie zdefiniowane operatory, gdzie E jest typem wyliczenia, a U jest podstawowym typem E:

    E operator +(E x, U y);
    E operator +(U x, E y);
    

    W czasie wykonywania te operatory są oceniane dokładnie jako (E)((U)x + (U)y).

  • Konkatenacja ciągów znaków

    string operator +(string x, string y);
    string operator +(string x, object y);
    string operator +(object x, string y);
    

    Te przeciążenia binarnego operatora + wykonują łączenie ciągów. Jeśli operand łączenia ciągów jest null, jest zastępowany pusty ciąg. W przeciwnym razie każdy operand inny niżstring jest konwertowany na reprezentację ciągu przez wywołanie metody wirtualnej ToString dziedziczonej z typu object. Jeśli ToString zwraca null, zostanie zastąpiony pusty ciąg.

    Przykład:

    class Test
    {
        static void Main()
        {
            string s = null;
            Console.WriteLine("s = >" + s + "<");  // Displays s = ><
    
            int i = 1;
            Console.WriteLine("i = " + i);         // Displays i = 1
    
            float f = 1.2300E+15F;
            Console.WriteLine("f = " + f);         // Displays f = 1.23E+15
    
            decimal d = 2.900m;
            Console.WriteLine("d = " + d);         // Displays d = 2.900
       }
    }
    

    Dane wyjściowe wyświetlane w komentarzach są typowym wynikiem systemu US-English. Dokładne dane wyjściowe mogą zależeć od ustawień regionalnych środowiska wykonawczego. Sam operator łączenia ciągów zachowuje się tak samo w każdym przypadku, ale metody ToString niejawnie wywoływane podczas wykonywania mogą mieć wpływ na ustawienia regionalne.

    koniec przykładu

    Wynikiem operatora łączenia ciągów jest string, który składa się z znaków lewego operandu, a następnie znaków prawego operandu. Operator łączenia ciągów nigdy nie zwraca wartości null. System.OutOfMemoryException może zostać zgłoszony, jeśli dostępnej pamięci jest za mało, aby możliwe było przydzielenie wynikowego ciągu.

  • Kombinacja delegatów. Każdy typ delegata niejawnie udostępnia następujący wstępnie zdefiniowany operator, gdzie D jest typem delegata:

    D operator +(D x, D y);
    

    Jeśli pierwszy operand jest null, wynikiem operacji jest wartość drugiego operandu (nawet jeśli jest to również null). W przeciwnym razie, jeśli drugi operand jest null, wynikiem operacji jest wartość pierwszego operandu. W przeciwnym razie wynikiem operacji jest nowe wystąpienie delegata, którego lista wywołań składa się z elementów na liście wywołań pierwszego operandu, a następnie elementów na liście wywołań drugiego operandu. Oznacza to, że lista wywołań wynikowego delegata jest łączeniem list wywołań dwóch operandów.

    Uwaga: Przykłady kombinacji delegatów można znaleźć §12.10.6 i §20.6. Ponieważ System.Delegate nie jest typem delegata, operator + nie został dla niego zdefiniowany. koniec przypisu

Podniesione (§12.4.8) formy niepodniesionych wstępnie zdefiniowanych operatorów dodawania zdefiniowanych powyżej również są wstępnie zdefiniowane.

12.10.6 Operator odejmowania

W przypadku operacji formularza x – yfunkcja rozpoznawania przeciążenia operatora binarnego (§12.4.5) jest stosowana w celu wybrania określonej implementacji operatora. Operandy są konwertowane na typy parametrów wybranego operatora, a typ wyniku jest zwracanym typem operatora.

Poniżej wymieniono wstępnie zdefiniowane operatory odejmowania. Wszystkie operatory odejmują y z x.

  • Odejmowanie liczb całkowitych:

    int operator –(int x, int y);
    uint operator –(uint x, uint y);
    long operator –(long x, long y);
    ulong operator –(ulong x, ulong y
    

    W kontekście checked, jeśli różnica znajduje się poza zakresem typu wyników, zostanie zgłoszony System.OverflowException. W kontekście unchecked przepełnienia nie są zgłaszane, a wszystkie znaczące bity o wysokiej kolejności poza zakresem typu wyniku są odrzucane.

  • Odejmowanie liczb zmiennoprzecinkowych:

    float operator –(float x, float y);
    double operator –(double x, double y);
    

    Różnica jest obliczana zgodnie z regułami arytmetyki IEC 60559. W poniższej tabeli wymieniono wyniki wszystkich możliwych kombinacji wartości niezerowych, zer, nieskończoności i sieci NaNs. W tabeli x i y są wartościami niezerowymi, a z jest wynikiem x – y. Jeśli x i y są równe, z jest dodatnie zero. Jeśli x – y jest zbyt duża do reprezentowania w typie docelowym, z jest nieskończonością z tym samym znakiem co x – y.

    y +0 -0 +∞ -∞ NaN
    x z x x -∞ +∞ NaN
    +0 -y +0 +0 -∞ +∞ NaN
    -0 -y -0 +0 -∞ +∞ NaN
    +∞ +∞ +∞ +∞ NaN +∞ NaN
    -∞ -∞ -∞ -∞ -∞ NaN NaN
    NaN NaN NaN NaN NaN NaN NaN

    (W powyższej tabeli wpisy -y oznaczają negację względem i y, a nie, że wartość jest ujemna.)

  • Odejmowanie dziesiętne:

    decimal operator –(decimal x, decimal y);
    

    Jeśli wynikowa wartość jest zbyt duża, aby przedstawić ją w formacie dziesiętnym, zgłaszany jest System.OverflowException. Skala wyniku, przed jakimkolwiek zaokrągleniem, jest równa większej ze skali dwóch operandów.

    Odejmowanie dziesiętne jest równoważne użyciu operatora odejmowania typu System.Decimal.

  • Odejmowanie enumeracji. Każdy typ wyliczenia niejawnie udostępnia następujący wstępnie zdefiniowany operator, gdzie E jest typem wyliczenia, a U jest podstawowym typem E:

    U operator –(E x, E y);
    

    Ten operator jest oceniany dokładnie tak, jak (U)((U)x – (U)y). Innymi słowy, operator oblicza różnicę między wartościami porządkowymi x i y, a typem wyniku jest podstawowy typ wyliczenia.

    E operator –(E x, U y);
    

    Ten operator jest oceniany dokładnie tak, jak (E)((U)x – y). Innymi słowy, operator odejmuje wartość z bazowego typu wyliczenia, co daje wartość wyliczenia.

  • Deleguj usunięcie. Każdy typ delegata niejawnie udostępnia następujący wstępnie zdefiniowany operator, gdzie D jest typem delegata:

    D operator –(D x, D y);
    

    Semantyka jest następująca:

    • Jeśli pierwszy operand jest null, wynikiem operacji jest null.
    • W przeciwnym razie, jeśli drugi operand jest null, wynikiem operacji jest wartość pierwszego operandu.
    • W przeciwnym razie oba operandy reprezentują niepuste listy wywołań (§20.2).
      • Jeśli listy są równe w wyniku porównania, określone przez operatora równości delegatów (§12.12.9), wynikiem operacji jest null.
      • W przeciwnym razie wynikiem operacji jest nowa lista wywołań składająca się z listy pierwszego operandu z usuniętymi wpisami drugiego operandu, pod warunkiem, że lista drugiego operandu jest podlistą pierwszego. (Aby określić równość podlisty, odpowiednie wpisy są porównywane tak jak dla operatora równości delegata). Jeśli lista drugiego operanda pasuje do wielu podlist ciągłych wpisów na liście pierwszego operanda, ostatnia pasująca podlista ciągłych wpisów zostanie usunięta.
      • W przeciwnym razie wynikiem operacji jest wartość lewego operandu.

    Żadna z list operandów (jeśli istnieje) nie została zmieniona w procesie.

    Przykład:

    delegate void D(int x);
    
    class C
    {
        public static void M1(int i) { ... }
        public static void M2(int i) { ... }
    }
    
    class Test
    {
        static void Main()
        {
            D cd1 = new D(C.M1);
            D cd2 = new D(C.M2);
            D list = null;
    
            list = null - cd1;                             // null
            list = (cd1 + cd2 + cd2 + cd1) - null;         // M1 + M2 + M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - cd1;          // M1 + M2 + M2
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2);  // M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd2);  // M1 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd1);  // M1 + M2
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd1);  // M1 + M2 + M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2 + cd2 + cd1);  // null
        }
    }
    

    koniec przykładu

Podniesione (§12.4.8) formy niepodniesionych wstępnie zdefiniowanych operatorów odejmowania zdefiniowanych powyżej są również wstępnie zdefiniowane.

12.11 Operatory przesunięcia

Operatory << i >> służą do wykonywania operacji przesunięcia bitowego.

shift_expression
    : additive_expression
    | shift_expression '<<' additive_expression
    | shift_expression right_shift additive_expression
    ;

Jeśli operand shift_expression ma typ w czasie kompilacji dynamic, wtedy wyrażenie jest powiązane dynamicznie (§12.3.3). W tym przypadku typ czasu kompilacji wyrażenia jest dynamic, a rozwiązanie opisane poniżej będzie odbywać się w czasie wykonywania przy użyciu typu czasu wykonywania tych operandów, które mają typ czasu kompilacji dynamic.

W przypadku operacji formularza x << count lub x >> countfunkcja rozpoznawania przeciążenia operatora binarnego (§12.4.5) jest stosowana w celu wybrania określonej implementacji operatora. Operandy są konwertowane na typy parametrów wybranego operatora, a typ wyniku jest zwracanym typem operatora.

Podczas deklarowania przeciążonego operatora przesunięcia typ pierwszego operandu zawsze jest klasą lub strukturą zawierającą deklarację operatora, a typ drugiego operandu zawsze jest int.

Poniżej wymieniono wstępnie zdefiniowane operatory przesunięcia.

  • Przesuń w lewo

    int operator <<(int x, int count);
    uint operator <<(uint x, int count);
    long operator <<(long x, int count);
    ulong operator <<(ulong x, int count);
    

    Operator << przesuwa x w lewo o liczbę bitów obliczoną zgodnie z poniższym opisem.

    Bity o wysokiej kolejności poza zakresem typu wyniku x są odrzucane, pozostałe bity są przesunięte w lewo, a puste pozycje bitów o niskiej kolejności są ustawione na zero.

  • Shift w prawo:

    int operator >>(int x, int count);
    uint operator >>(uint x, int count);
    long operator >>(long x, int count);
    ulong operator >>(ulong x, int count);
    

    Operator >> zmienia x w prawo przez kilka bitów obliczonych zgodnie z poniższym opisem.

    Gdy x jest typu int lub long, bity o niskiej kolejności x są odrzucane, pozostałe bity są przesunięte w prawo, a puste pozycje bitów o wysokiej kolejności są ustawione na zero, jeśli x jest nieujemna i ustawiona na jedną, jeśli x jest ujemna.

    Gdy x jest typu uint lub ulong, bity o niskiej kolejności x są odrzucane, pozostałe bity są przesunięte w prawo, a puste pozycje bitów o wysokiej kolejności są ustawione na zero.

W przypadku wstępnie zdefiniowanych operatorów liczba bitów do przesunięcia jest obliczana w następujący sposób:

  • Gdy typ x jest int lub uint, liczba zmian jest podawana przez pięć bitów o niskiej kolejności count. Innymi słowy, licznik zmian jest obliczany z count & 0x1F.
  • Gdy typ x jest long lub ulong, liczba zmian jest podawana przez sześć bitów o niskiej kolejności count. Innymi słowy, licznik zmian jest obliczany z count & 0x3F.

Jeśli wynikowa liczba przesunięć wynosi zero, operatory przesunięcia zwracają po prostu wartość x.

Operacje przesunięcia nigdy nie powodują przepełnienia i generują te same wyniki w sprawdzonych i niesprawdzonych kontekstach.

Gdy lewy operand operatora >> jest znakiem typu całkowitego, operator wykonuje arytmetyczne przesunięcie w prawo, gdzie wartość najbardziej znaczącego bitu (bit znaku) operandu jest propagowana do pustych pozycji bitów o wysokiej kolejności. Gdy lewy operand operatora >> jest typem całkowitym bez znaku, operator wykonuje logiczne przesunięcie w prawo , gdzie puste pozycje bitowe o wysokiej kolejności są zawsze ustawione na zero. Aby wykonać operację odwrotną do tej wynikającej z typu operandu, można użyć jawnego rzutowania.

Przykład: jeśli x jest zmienną typu int, operacja unchecked ((int)((uint)x >> y)) wykonuje przesunięcie logiczne po prawej stronie x. koniec przykładu

Podniesione (§12.4.8) postaci niepodniesionych, wstępnie zdefiniowanych operatorów przesunięcia zdefiniowane powyżej są również wstępnie zdefiniowane.

12.12 Operatory relacyjne i testujące typy

12.12.1 Ogólne

Operatory ==, !=, <, >, <=, >=, isi as są nazywane operatorami testowania relacyjnego i typowego.

relational_expression
    : shift_expression
    | relational_expression '<' shift_expression
    | relational_expression '>' shift_expression
    | relational_expression '<=' shift_expression
    | relational_expression '>=' shift_expression
    | relational_expression 'is' type
    | relational_expression 'is' pattern
    | relational_expression 'as' type
    ;

equality_expression
    : relational_expression
    | equality_expression '==' relational_expression
    | equality_expression '!=' relational_expression
    ;

Uwaga: Wyszukiwanie prawego operandu operatora is powinno najpierw sprawdzić, czy jest jako typ , a następnie jako wyrażenie , które może obejmować wiele tokenów. W przypadku, gdy operand jest wyrażeniem , wyrażenie wzorcowe musi mieć priorytet co najmniej tak wysoki, jak shift_expression. uwaga końcowa

Operator is jest opisany w §12.12.12.12, a operator as jest opisany w §12.12.13.

Operatory ==, !=, <, >, <= i >= są operatorami porównania .

Jeśli default_literal (§12.8.21) jest używany jako operand <, >, <=lub operator >=, występuje błąd czasu kompilacji. Jeśli default_literal jest używana jako oba operandy operatora == albo !=, wystąpi błąd czasu kompilacji. Jeśli default_literal jest używany jako lewy operand operatora is lub as, wystąpi błąd czasu kompilacji.

Jeśli operand operatora porównania ma typ czasu kompilacji dynamic, wyrażenie jest dynamicznie powiązane (§12.3.3). W tym przypadku typ czasu kompilacji wyrażenia jest dynamic, a rozwiązanie opisane poniżej będzie odbywać się w czasie wykonywania przy użyciu typu czasu wykonywania tych operandów, które mają typ czasu kompilacji dynamic.

W przypadku operacji formularza x «op» y, gdzie «op» jest operatorem porównania, rozpoznawanie przeciążenia (§12.4.5) jest stosowane w celu wybrania określonej implementacji operatora. Operandy są konwertowane na typy parametrów wybranego operatora, a typ wyniku jest zwracanym typem operatora. Jeśli oba operandy w wyrażeniu równości są tymi literałami null, nie jest wykonywane rozpoznawanie przeciążenia, a wyrażenie przyjmuje stałą wartość true lub false, w zależności od tego, czy operatorem jest == czy !=.

Wstępnie zdefiniowane operatory porównania są opisane w następujących podklasach. Wszystkie wstępnie zdefiniowane operatory porównania zwracają wynik typu bool, zgodnie z opisem w poniższej tabeli.

operacja wynik
x == y true jeśli x jest równa y, false w przeciwnym razie
x != y true jeśli x nie jest równa y, false w przeciwnym razie
x < y true, jeśli x jest mniejsza niż y, false w przeciwnym razie
x > y true jeśli x jest większa niż y, false w przeciwnym razie
x <= y true jeśli x jest mniejsza lub równa y, false w przeciwnym razie
x >= y true jeśli x jest większa lub równa y, false w przeciwnym razie

12.12.2 Operatory porównania liczb całkowitych

Wstępnie zdefiniowane operatory porównania liczb całkowitych to:

bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);

bool operator !=(int x, int y);
bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);

bool operator <(int x, int y);
bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);

bool operator >(int x, int y);
bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);

bool operator <=(int x, int y);
bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);

bool operator >=(int x, int y);
bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);

Każdy z tych operatorów porównuje wartości liczbowe dwóch operandów liczb całkowitych i zwraca wartość bool wskazującą, czy określona relacja jest true, czy false.

Podniesione (§12.4.8) formy wstępnie zdefiniowanych operatorów porównania liczb całkowitych niezawierających podniesień, zdefiniowane powyżej, są również wstępnie zdefiniowane.

12.12.3 Operatory porównań zmiennoprzecinkowych

Wstępnie zdefiniowane operatory porównywania liczb zmiennoprzecinkowych to:

bool operator ==(float x, float y);
bool operator ==(double x, double y);

bool operator !=(float x, float y);
bool operator !=(double x, double y);

bool operator <(float x, float y);
bool operator <(double x, double y);

bool operator >(float x, float y);
bool operator >(double x, double y);

bool operator <=(float x, float y);
bool operator <=(double x, double y);

bool operator >=(float x, float y);
bool operator >=(double x, double y);

Operatorzy porównują operandy zgodnie z regułami standardu IEC 60559:

Jeśli którykolwiek operand ma wartość NaN, wynik jest false dla wszystkich operatorów z wyjątkiem !=, dla których wynik jest true. W przypadku dwóch operandów x != y zawsze generuje ten sam wynik co !(x == y). Jednak gdy jeden lub oba operandy są naN, operatory <, >, <=i >= nie generują takie same wyniki jak logiczne negację operatora przeciwnego.

Przykład: jeśli którykolwiek z x i y to NaN, x < y jest false, ale !(x >= y) jest true. koniec przykładu

Jeśli żaden operand nie ma wartości NaN, operatory porównują wartości dwóch operandów zmiennoprzecinkowych w odniesieniu do kolejności

–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞

gdzie min i max są najmniejszymi i największymi dodatnimi wartościami skończonymi, które mogą być reprezentowane w danym formacie zmiennoprzecinkowym. Istotne skutki tego porządkowania są następujące:

  • Wartości ujemne i dodatnie są traktowane jako równe.
  • Nieskończoność ujemna jest uznawana za mniejszą niż wszystkie inne wartości, ale równą innej nieskończoności ujemnej.
  • Nieskończoność dodatnia jest uważana za większą niż wszystkie inne wartości, ale równą innej nieskończoności dodatniej.

Przeniesione (§12.4.8) formy wstępnie zdefiniowanych operatorów porównania zmiennoprzecinkowego, które nie są przeniesione, również są wstępnie zdefiniowane.

12.12.4 Operatory porównania dziesiętnego

Wstępnie zdefiniowane operatory porównania dziesiętnego to:

bool operator ==(decimal x, decimal y);
bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);

Każdy z tych operatorów porównuje wartości liczbowe dwóch operandów dziesiętnych i zwraca wartość bool wskazującą, czy określona relacja jest true, czy false. Każde porównanie dziesiętne jest równoważne użyciu odpowiedniego operatora relacyjnego lub równości typu System.Decimal.

Podniesione (§12.4.8) wersje niepodniesionych wstępnie zdefiniowanych operatorów porównania dziesiętnego, zdefiniowanych powyżej, są również wstępnie zdefiniowane.

12.12.5 Operatory równości logicznej

Z góry zdefiniowane operatory równości typu Boolean to:

bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);

Wynik == jest true, jeśli x i ytrue lub x i yfalse. W przeciwnym razie wynik jest false.

Wynik != jest false, jeśli x i ytrue lub x i yfalse. W przeciwnym razie wynik jest true. Gdy operandy są typu bool, operator != generuje taki sam wynik jak operator ^.

Podniesione (§12.4.8) formy niepodniesionych wstępnie zdefiniowanych operatory równości boolowskiej zdefiniowane powyżej są również wstępnie zdefiniowane.

12.12.6 Operatory porównania wyliczenia

Każdy typ wyliczenia niejawnie udostępnia następujące wstępnie zdefiniowane operatory porównania

bool operator ==(E x, E y);
bool operator !=(E x, E y);

bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);

Wynikiem oceny x «op» y, gdzie x i y są wyrażeniami typu wyliczenia E z podstawowym typem U, a «op» jest jednym z operatorów porównania, jest dokładnie taki sam jak ocena ((U)x) «op» ((U)y). Innymi słowy, operatory porównania typów wyliczenia po prostu porównują bazowe wartości całkowite dwóch operandów.

Podniesione (§12.4.8) warianty niepodniesionych z góry zdefiniowanych operatorów porównań wyliczeniowych zdefiniowanych powyżej są również z góry zdefiniowane.

12.12.7 Operatory równości typów odwołań

Każdy typ klasy C niejawnie udostępnia następujące wstępnie zdefiniowane operatory równości dla typów referencyjnych:

bool operator ==(C x, C y);
bool operator !=(C x, C y);

chyba że istnieją wstępnie zdefiniowane operatory równości dla C (na przykład gdy C jest string lub System.Delegate).

Operatory zwracają wynik porównania dwóch odwołań pod kątem równości lub nierówności. operator == zwraca true tylko wtedy, gdy x i y odnoszą się do tej samej instancji lub są null, a operator != zwraca true tylko wtedy, gdy operator == z tymi samymi operandami zwróci false.

Oprócz normalnych zasad stosowania (§12.6.4.2), wstępnie zdefiniowane operatory porównania typów referencyjnych wymagają jednego z następujących, aby były stosowane:

  • Oba operandy są wartościami typu znanego jako reference_type lub jako literał null. Ponadto istnieje konwersja tożsamości lub jawna konwersja referencji (§10.3.5) z każdego z operandów do typu drugiego operandu.
  • Jednym z operandów jest literał null, a drugim jest wartość typu T, gdzie T jest parametrem typu , który nie jest znany jako typ wartości i nie ma ograniczenia typu wartości.
    • Jeśli w czasie wykonywania T jest typem wartości niemającym wartości null, wynik == jest false, a wynik != jest true.
    • Jeśli w czasie wykonywania T jest typem wartości dopuszczającym null, wynik jest obliczany z właściwości HasValue operandu, zgodnie z opisem w sekcji (§12.12.10).
    • Jeśli w czasie wykonywania T jest typem referencyjnym, wynik jest true, jeśli operand jest null, a w przeciwnym razie false.

O ile jeden z tych warunków nie zostanie spełniony, wystąpi błąd czasu powiązania.

Uwaga: Istotne implikacje tych reguł są następujące:

  • Jest to błąd w czasie wiązania użyć predefiniowanych operatorów równości typów referencyjnych do porównywania dwóch odwołań, o których wiadomo, że są różne w czasie wiązania. Jeśli na przykład typy czasu powiązania operandów są dwoma typami klas, a jeśli żaden z nich nie pochodzi z drugiej, nie będzie możliwe, aby dwa operandy odwoływały się do tego samego obiektu. W związku z tym operacja jest uważana za błąd czasu wiązania.
  • Zdefiniowane z góry operatory równości typów referencyjnych nie zezwalają na porównywanie operandów typów wartościowych (z wyjątkiem sytuacji, gdy parametry szablonu typów są porównywane z null, co jest obsługiwane w sposób szczególny).
  • Operandy wstępnie zdefiniowanych operatorów równości typów odwołań nigdy nie są w polu. Wykonywanie takich operacji opakowywania byłoby bez znaczenia, ponieważ odwołania do nowo przydzielonych wystąpień opakowanych różniłyby się od wszystkich innych odwołań.

W przypadku operacji w formie x == y lub x != y, jeśli istnieje dowolna zdefiniowana przez użytkownika operator == lub operator !=, reguły rozwiązywania przeciążenia operatora (§12.4.5) wybiorą tego operatora zamiast wstępnie zdefiniowanego operatora równości typu referencyjnego. Zawsze można wybrać wbudowany operator równości typów referencyjnych, jawnie rzutując jeden lub oba operandy na typ object.

uwaga końcowa

Przykład: Poniższy przykład sprawdza, czy argument o typie bez ograniczeń jest null.

class C<T>
{
   void F(T x)
   {
      if (x == null)
      {
          throw new ArgumentNullException();
      }
      ...
   }
}

Konstrukcja x == null jest dozwolona, mimo że T może reprezentować typ wartości nieprzechowujący wartości null, a wynik jest po prostu zdefiniowany jako false, gdy T jest typem wartości nieprzechowującym wartości null.

koniec przykładu

W przypadku operacji w formie x == y lub x != y, jeśli istnieje jakakolwiek odpowiednia operator == lub operator !=, reguły rozwiązywania przeciążenia operatora (§12.4.5) wybiorą ten operator zamiast predefiniowanego operatora równości dla typów referencyjnych.

Uwaga: można zawsze wybrać wstępnie zdefiniowany operator równości dla typów referencyjnych, jawnie rzutując oba operandy na typ object. uwaga końcowa

Przykład: przykład

class Test
{
    static void Main()
    {
        string s = "Test";
        string t = string.Copy(s);
        Console.WriteLine(s == t);
        Console.WriteLine((object)s == t);
        Console.WriteLine(s == (object)t);
        Console.WriteLine((object)s == (object)t);
    }
}

generuje dane wyjściowe

True
False
False
False

Zmienne s i t odwołują się do dwóch odrębnych wystąpień ciągów zawierających te same znaki. Wynik pierwszego porównania to True, ponieważ wybierany jest z góry zdefiniowany operator równości ciągów (§12.12.8), gdy oba operandy są typu string. Pozostałe porównania dają w wyniku False, ponieważ przeciążenie operator == w typie string nie ma zastosowania, gdy którykolwiek operand ma typ czasu powiązania object.

Należy pamiętać, że powyższe techniki nie mają znaczenia dla typów wartości. Przykład

class Test
{
    static void Main()
    {
        int i = 123;
        int j = 123;
        Console.WriteLine((object)i == (object)j);
    }
}

Dane wyjściowe to False, ponieważ rzutowania tworzą odwołania do dwóch oddzielnych instancji zapakowanych wartości int.

koniec przykładu

12.12.8 Operatory równości ciągów

Wstępnie zdefiniowane operatory równości ciągów to:

bool operator ==(string x, string y);
bool operator !=(string x, string y);

Dwie string wartości są traktowane jako równe, gdy jedna z następujących wartości ma wartość true:

  • Obie wartości są null.
  • Obie wartości to nie-null odwołania do wystąpień ciągów, które mają identyczne długości i identyczne znaki w każdej pozycji znaku.

Operatory równości ciągów porównują wartości ciągów, a nie odwołania do ciągów. Gdy dwa oddzielne wystąpienia ciągów zawierają dokładnie tę samą sekwencję znaków, wartości ciągów są równe, ale odwołania są różne.

Uwaga: Zgodnie z opisem w §12.12.7operatory równości typów odwołań mogą służyć do porównywania odwołań do ciągów zamiast wartości ciągu. uwaga końcowa

12.12.9 Delegowanie operatorów równości

Wstępnie zdefiniowane operatory równości delegatów to:

bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);

Dwa wystąpienia delegatów są traktowane jako równe w następujący sposób:

  • Jeśli którakolwiek z instancji delegatów jest null, są równe, jeśli i tylko wtedy, gdy obie są null.
  • Jeśli delegaty mają różne typy czasu wykonywania, nigdy nie są równi.
  • Jeśli oba wystąpienia delegatów mają listę wywołań (§20.2), wystąpienia te są równe, jeśli i tylko wtedy, gdy ich listy wywołań są takie same, a każdy wpis na liście wywołań jednego z nich jest równy (zgodnie z definicją poniżej) do odpowiadającego wpisu w kolejności na liście wywołań drugiej.

Następujące reguły określają równość wpisów listy wywołań:

  • Jeśli dwa wpisy listy wywołań odwołują się do tej samej metody statycznej, wpisy są równe.
  • Jeśli dwa wpisy listy wywołań odwołują się do tej samej metody niestatycznej na tym samym obiekcie docelowym (zdefiniowanym przez operatory równości odwołań), wpisy są równe.
  • Wpisy listy wywołań, utworzone z ocen semantycznie identycznych funkcji anonimowych (§12.19) z tym samym (prawdopodobnie pustym) zestawem przechwyconych instancji zmiennych zewnętrznych, mogą być równe (ale nie muszą).

Jeśli rozpoznawanie przeciążenia operatora prowadzi do wybrania jednego z operatorów równości delegatów, a typy czasu powiązania obu operandów są typami delegatów zgodnie z opisem w §20 zamiast System.Delegate, i jeśli nie istnieje konwersja tożsamości między typami operandów w czasie powiązania, występuje błąd czasu powiązania.

Uwaga: ta reguła uniemożliwia porównywanie, które nigdy nie mogą uwzględniać wartości innych niżnull z powodu odwoływania się do wystąpień różnych typów delegatów. uwaga końcowa

12.12.10 Operatory równości pomiędzy typami wartości dopuszczonymi do null a literałem null

Operatory == i != zezwalają na to, aby jeden operand był wartością typu wartości dopuszczanej wartości null, a drugi jako literał null, nawet jeśli dla operacji nie istnieje żaden wstępnie zdefiniowany operator lub operator zdefiniowany przez użytkownika (w postaci nieznoszonej lub podniesionej).

W przypadku operacji jednej z form

x == null    null == x    x != null    null != x

gdzie x jest wyrażeniem typu wartości, która może być null, jeśli rozwiązywanie przeciążenia operatora (§12.4.5) nie znajduje odpowiedniego operatora, wynik jest obliczany z właściwości HasValuex. W szczególności pierwsze dwie formy są tłumaczone na !x.HasValue, a ostatnie dwie formy są tłumaczone na x.HasValue.

12.12.11 Operatory równości krotek

Operatory równości krotki są stosowane parowo do elementów operandów krotki w porządku leksykalnym.

Jeśli każdy z operandów x i y operatora == lub != jest klasyfikowany jako krotka lub jako wartość z typem krotki (§8.3.11), operator jest operatorem równości krotki .

Jeśli operand e jest klasyfikowany jako krotka, elementy e1...en powinny być wynikami oceny wyrażeń elementów wyrażenia krotki. W przeciwnym razie jeśli e jest wartością typu krotki, elementy będą t.Item1...t.Itemn, gdy t jest wynikiem oceny e.

Operandy x i y operatora równości krotki muszą mieć taką samą krotność, w przeciwnym razie wystąpi błąd czasu kompilacji. Dla każdej pary elementów xi i yistosuje się ten sam operator równości i zwraca wynik typu bool, dynamic, typu, który ma niejawną konwersję na bool, lub typ definiujący operatory true i false.

Operator równości krotki x == y jest oceniany w następujący sposób:

  • Lewy operand x jest oceniany.
  • Operand z prawej strony y jest obliczany.
  • Dla każdej pary elementów xi i yi w kolejności leksykalnej:
    • Operator xi == yi jest obliczany, a wynik typu bool jest uzyskiwany w następujący sposób:
      • Jeśli porównanie przyniosło bool, to jest to wynik.
      • W przeciwnym razie jeśli porównanie daje dynamic operator false jest dynamicznie wywoływany, a wynikowa wartość bool jest negowana za pomocą operatora logicznego negacji (!).
      • W przeciwnym razie, jeśli typ porównania ma niejawną konwersję na bool, ta konwersja jest stosowana.
      • W przeciwnym razie, jeśli typ porównania ma operator false, ten operator jest wywoływany, a wynikowa wartość bool jest negowana za pomocą operatora logicznego negacji (!).
    • Jeśli wynik bool wynosi false, nie przeprowadza się dalszej oceny, a wynik operatora równości krotki to false.
  • Jeśli wszystkie porównania elementów przyniosły true, wynikiem operatora równości krotki jest true.

Operator równości krotki x != y jest oceniany w następujący sposób:

  • Lewy operand x jest oceniany.
  • Operand z prawej strony y jest obliczany.
  • Dla każdej pary elementów xi i yi w kolejności leksykalnej:
    • Operator xi != yi jest obliczany, a wynik typu bool jest uzyskiwany w następujący sposób:
      • Jeśli porównanie przyniosło bool, to jest to wynik.
      • W przeciwnym razie, jeśli wynik porównania to dynamic, operator true jest dynamicznie wywoływany na nim, a otrzymana wartość bool jest wynikiem.
      • W przeciwnym razie, jeśli typ porównania ma niejawną konwersję na bool, ta konwersja jest stosowana.
      • W przeciwnym razie, jeśli typ porównania ma operator true, ten operator jest wywoływany, a wynikowa wartość bool jest wynikiem.
    • Jeśli wynik bool wynosi true, nie przeprowadza się dalszej oceny, a wynik operatora równości krotki to true.
  • Jeśli wszystkie porównania elementów przyniosły false, wynikiem operatora równości krotki jest false.

12.12.12 Operator „is”

Istnieją dwie formy operatora is. Jednym z nich jest operator typu 'jest-typ' , który ma typ po prawej stronie. Drugi to operator is-pattern, który ma wzorzec po prawej stronie.

12.12.12.1 Operator typu is

Operator is-type służy do sprawdzania, czy czas wykonywania obiektu jest zgodny z określonym typem. Sprawdzanie jest wykonywane w czasie działania. Wynikiem operacji E is T, gdzie E jest wyrażeniem, a T jest typem innym niż dynamic, jest wartość logiczna wskazująca, czy E jest różne od null i można pomyślnie przekonwertować na typ T przez konwersję odwołania, konwersję bokserską, konwersję rozpakowania, konwersję owijania lub konwersję rozpakowywania.

Operacja jest oceniana w następujący sposób:

  1. Jeśli E jest anonimową funkcją lub grupą metod, wystąpi błąd czasu kompilacji
  2. Jeśli E jest literałem null lub jeśli wartość E wynosi null, rezultatem jest false.
  3. Inaczej:
  4. Niech R być typem środowiska uruchomieniowego E.
  5. Niech D będzie wyprowadzony z R w następujący sposób:
  6. Jeśli R jest typem wartości dopasowanej do null, D jest pierwotnym typem R.
  7. W przeciwnym razie D jest R.
  8. Wynik zależy od D i T w następujący sposób:
  9. Jeśli T jest typem referencyjnym, wynik jest true, jeśli:
    • istnieje konwersja tożsamości między D a T,
    • D jest typem odwołania i istnieje niejawna konwersja odwołania z D do T lub
    • Albo: D jest typem wartości, a konwersja typu boxing z D na T jest możliwa.
      Lub: D jest typem wartości, a T jest typem interfejsu zaimplementowanym przez D.
  10. Jeśli T jest typem wartości dopuszczającej null, wynik to true, jeśli D jest podstawowym typem T.
  11. Jeśli T jest typem wartości innej niż null, wynik jest true, jeśli D i T są tego samego typu.
  12. W przeciwnym razie wynik jest false.

Konwersje zdefiniowane przez użytkownika nie są brane pod uwagę przez operator is.

Uwaga: ponieważ operator is jest oceniany w czasie wykonywania, wszystkie argumenty typu zostały zastąpione i nie ma otwartych typów (§8.4.3) do rozważenia. uwaga końcowa

Uwaga: Operator is można zrozumieć pod względem typów i konwersji w czasie kompilacji w następujący sposób, gdzie C jest typem czasu kompilacji E:

  • Jeśli typ czasu kompilacji e jest taki sam jak T, lub jeśli istnieje niejawna konwersja odwołania (§10.2.8), konwersja przez boksowanie (§10.2.9), konwersja przez zawijanie (§10.6) lub jawna konwersja przez rozpakowywanie (§10.6) z typu czasu kompilacji E do T:
    • Jeśli C jest typu wartości innej niż null, wynik operacji jest true.
    • W przeciwnym razie wynik operacji jest odpowiednikiem oceny E != null.
  • W przeciwnym razie, jeśli jawna konwersja odwołania (§10.3.5) lub konwersja rozpakowywania (§10.3.7) istnieje z C do T, lub jeśli C lub T jest typem otwartym (§8.4.3), kontrole w czasie wykonywania, jak powyżej, są wykonywane.
  • W przeciwnym razie brak odniesienia, boxing, zawijanie lub rozpakowywanie konwersji E na typ T jest możliwy, a wynikiem operacji jest false. Kompilator może implementować optymalizacje na podstawie typu czasu kompilacji.

uwaga końcowa

12.12.12.2 Operator is-pattern

Operator is-pattern służy do sprawdzania, czy wartość obliczona przez wyrażenie pasuje danego wzorca (§11). Sprawdzanie jest wykonywane w czasie działania. Wynik operatora is-pattern ma wartość true, jeśli wartość jest zgodna ze wzorcem; w przeciwnym razie jest to fałsz.

W przypadku wyrażenia postaci E is P, gdzie E jest wyrażeniem relacyjnym typu T, a P jest wzorcem, jest to błąd kompilacji, jeśli zachodzi którykolwiek z następujących przypadków:

  • E nie wyznacza wartości lub nie ma typu.
  • Wzorzec P nie ma zastosowania (§11.2) do typu T.

12.12.13 Operator as

Operator as służy do jawnego konwertowania wartości na dany typ odwołania lub typ wartości, która może być null. W przeciwieństwie do wyrażenia rzutowania (§12.9.7), operator as nigdy nie zgłasza wyjątku. Zamiast tego, jeśli wskazana konwersja nie jest możliwa, wynikowa wartość jest null.

W operacji w formie E as T, E powinno być wyrażeniem, a T powinno być typem referencyjnym, parametrem typu, o którym wiadomo, że jest typem referencyjnym, lub typem wartości dopuszczającym wartość null. Ponadto co najmniej jeden z następujących warunków ma wartość true lub w przeciwnym razie występuje błąd czasu kompilacji:

  • Tożsamość (§10.2.2), niejawna wartość null (§10.2.6), niejawne odwołanie (§10.2.8), boxing (§10.2.9), jawne dopuszczanie wartości null (§10.3.4), jawne odwołanie (§10.3.5) lub opakowywanie (§8.3.12) konwersja istnieje z E do T.
  • Typ E lub T jest typem otwartym.
  • E jest literałem null.

Jeśli typ czasu kompilacji E nie jest dynamic, operacja E as T generuje ten sam wynik co

E is T ? (T)(E) : (T)null

z tą różnicą, że E jest obliczana tylko raz. Można oczekiwać, że kompilator zoptymalizuje E as T, wykonując co najwyżej jedno sprawdzanie typu środowiska uruchomieniowego, w przeciwieństwie do dwóch sprawdzań typu implikowanych przez powyższe rozszerzenie.

Jeśli typ czasu kompilacji E jest dynamic, to w przeciwieństwie do operatora rzutowania, operator as nie jest dynamicznie wiązany (§12.3.3). W związku z tym rozszerzenie w tym przypadku jest następujące:

E is T ? (T)(object)(E) : (T)null

Należy pamiętać, że niektóre konwersje, takie jak konwersje zdefiniowane przez użytkownika, nie są możliwe z operatorem as i zamiast tego powinny być wykonywane przy użyciu wyrażeń rzutowania.

Przykład: w przykładzie

class X
{
    public string F(object o)
    {
        return o as string;  // OK, string is a reference type
    }

    public T G<T>(object o)
        where T : Attribute
    {
        return o as T;       // Ok, T has a class constraint
    }

    public U H<U>(object o)
    {
        return o as U;       // Error, U is unconstrained
    }
}

Parametr typu T w G jest znany jako parametr o typie referencyjnym, ponieważ ma ograniczenie klasy. Parametr typu U dla H nie jest poprawny; w związku z tym korzystanie z operatora as w H jest niedozwolone.

koniec przykładu

12.13 Operatory logiczne

12.13.1 Ogólne

Operatory &,^i | są nazywane operatorami logicznymi.

and_expression
    : equality_expression
    | and_expression '&' equality_expression
    ;

exclusive_or_expression
    : and_expression
    | exclusive_or_expression '^' and_expression
    ;

inclusive_or_expression
    : exclusive_or_expression
    | inclusive_or_expression '|' exclusive_or_expression
    ;

Jeśli operand operatora logicznego ma typ czasu kompilacji dynamic, wyrażenie jest dynamicznie powiązane (§12.3.3). W tym przypadku typ czasu kompilacji wyrażenia jest dynamic, a rozwiązanie opisane poniżej będzie odbywać się w czasie wykonywania przy użyciu typu czasu wykonywania tych operandów, które mają typ czasu kompilacji dynamic.

W przypadku operacji formularza x «op» y, gdzie «op» jest jednym z operatorów logicznych, rozpoznawanie przeciążenia (§12.4.5) jest stosowane w celu wybrania określonej implementacji operatora. Operandy są konwertowane na typy parametrów wybranego operatora, a typ wyniku jest zwracanym typem operatora.

Wstępnie zdefiniowane operatory logiczne są opisane w następujących podklasach.

12.13.2 Operatory logiczne dla liczb całkowitych

Wstępnie zdefiniowane operatory logiczne liczby całkowitej to:

int operator &(int x, int y);
uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);

int operator |(int x, int y);
uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);

int operator ^(int x, int y);
uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);

Operator & oblicza bitowe logiczne AND dwóch operandów, operator | oblicza bitowo logiczne OR dwóch operandów, a operator ^ oblicza bitowe logiczne wyłączność OR dwóch operandów. Nie ma możliwości przepełnienia podczas tych operacji.

Podniesione (§12.4.8) formy wstępnie zdefiniowanych operatorów logicznych dla liczb całkowitych, zdefiniowanych powyżej, są również wstępnie zdefiniowane.

12.13.3 Operatory logiczne wyliczania

Każdy typ wyliczenia E niejawnie udostępnia następujące wstępnie zdefiniowane operatory logiczne:

E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);

Wynikiem ewaluacji x «op» y, gdzie x i y są wyrażeniami typu wyliczenia E z podstawowym typem U, a «op» jest jednym z operatorów logicznych, jest dokładnie takie samo jak ocena (E)((U)x «op» (U)y). Innymi słowy, operatory logiczne typu wyliczenia po prostu wykonują operację logiczną na podstawowym typie dwóch operandów.

Podniesione (§12.4.8) formy wcześniej zdefiniowanych operatorów logicznych dla typów wyliczeniowych, które nie są podniesione, są również wstępnie zdefiniowane.

12.13.4 Operatory logiczne Boolean

Wstępnie zdefiniowane operatory logiczne to:

bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);

Resultatem x & y jest true, jeśli zarówno x, jak i ytrue. W przeciwnym razie wynik jest false.

Wynik x | y jest true, jeśli x lub y jest true. W przeciwnym razie wynik jest false.

Wynikiem x ^ y jest true, jeśli x jest true, a y jest falselub x jest false, a y jest true. W przeciwnym razie wynik jest false. Gdy operandy są typu bool, operator ^ oblicza ten sam wynik co operator !=.

12.13.5 Boolowska dopuszczająca wartość null & i | operatorzy

Typ logiczny nullable bool? może reprezentować trzy wartości: true, falsei null.

Podobnie jak w przypadku innych operatorów binarnych, zniesione formy operatorów logicznych & i | (§12.13.4) są również wstępnie zdefiniowane:

bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);

Semantyka zniesionych operatorów & i | jest definiowana przez następującą tabelę:

x y x & y x \| y
true true true true
true false false true
true null null true
false true false true
false false false false
false null false null
null true null true
null false false null
null null null null

Uwaga: typ bool? jest koncepcyjnie podobny do trójwartościowego typu używanego w wyrażeniach logicznych w języku SQL. W powyższej tabeli przedstawiono te same semantyki co SQL, podczas gdy stosowanie reguł §12.4.8 do operatorów & i | tego nie robiłoby. Reguły §12.4.8 już zapewniają semantykę podobną do SQL dla operatora zdźwigniętego ^. uwaga końcowa

12.14 Warunkowe operatory logiczne

12.14.1 Ogólne

Operatory && i || są nazywane operatorami logicznymi warunkowych. Są one również nazywane "zwarciowymi" operatorami logicznymi.

conditional_and_expression
    : inclusive_or_expression
    | conditional_and_expression '&&' inclusive_or_expression
    ;

conditional_or_expression
    : conditional_and_expression
    | conditional_or_expression '||' conditional_and_expression
    ;

Operatory && i || to wersje warunkowe operatorów & i |:

  • Operacja x && y odpowiada operacji x & y, z tą różnicą, że y jest obliczana tylko wtedy, gdy x nie jest false.
  • Operacja x || y odpowiada operacji x | y, z tą różnicą, że y jest obliczana tylko wtedy, gdy x nie jest true.

Uwaga: Powodem, dla którego zwarciowanie używa warunków "nieprawda" oraz "niefałsz", jest umożliwienie operatorom warunkowym definiowanym przez użytkownika określenie, kiedy zastosować zwarciowanie. Typy zdefiniowane przez użytkownika mogą być w stanie, w którym operator true zwraca false, a operator false zwraca false. W takich przypadkach ani &&, ani || nie spowodowałoby zwarcia. uwaga końcowa

Jeśli operand operatora logicznego warunkowego ma typ czasu kompilacji dynamic, wyrażenie jest dynamicznie powiązane (§12.3.3). W tym przypadku typ czasu kompilacji wyrażenia jest dynamic, a rozwiązanie opisane poniżej będzie odbywać się w czasie wykonywania przy użyciu typu czasu wykonywania tych operandów, które mają typ czasu kompilacji dynamic.

Operacja formularza x && y lub x || y jest przetwarzana przez zastosowanie rozpoznawania przeciążenia (§12.4.5), tak jakby operacja została zapisana x & y lub x | y. Wtedy

  • Jeśli rozpoznawanie przeciążeń nie może znaleźć jednego najlepszego operatora lub jeśli rozpoznawanie przeciążenia wybierze jeden ze wstępnie zdefiniowanych operatorów logicznych liczby całkowitej lub operatorów logicznych dopuszczanych wartości null (§12.13.5), wystąpi błąd czasu powiązania.
  • W przeciwnym razie, jeśli wybrany operator jest jednym ze wstępnie zdefiniowanych operatorów logicznych (§12.13.4), operacja jest przetwarzana zgodnie z opisem w §12.14.2.
  • W przeciwnym razie wybrany operator jest operatorem zdefiniowanym przez użytkownika, a operacja jest przetwarzana zgodnie z opisem w §12.14.3.

Nie można bezpośrednio przeciążyć operatorów logicznych warunkowych. Jednak ponieważ warunkowe operatory logiczne są oceniane pod względem zwykłych operatorów logicznych, przeciążenia zwykłych operatorów logicznych są, z pewnymi ograniczeniami, również uważane za przeciążenia warunkowych operatorów logicznych. Opisano to w §12.14.3.

12.14.2 Warunkowe operatory logiczne typu Boolean

Gdy operandy && lub || są typu bool, lub gdy operandy są typami, które nie definiują odpowiednich operator & lub operator |, ale definiują niejawne konwersje na bool, operacja jest przetwarzana w następujący sposób:

  • Operacja x && y jest obliczana jako x ? y : false. Innymi słowy, x jest najpierw obliczany i konwertowany na typ bool. Następnie, jeśli x jest true, y jest obliczany i konwertowany na typ bool, a to staje się wynikiem operacji. W przeciwnym razie wynikiem operacji jest false.
  • Operacja x || y jest obliczana jako x ? true : y. Innymi słowy, x jest najpierw obliczany i konwertowany na typ bool. Następnie, jeśli x jest true, wynikiem operacji jest true. W przeciwnym razie y jest obliczana i konwertowana na typ bool, a to staje się wynikiem operacji.

12.14.3 Warunkowe operatory logiczne zdefiniowane przez użytkownika

Gdy operandy && lub || są typami, które deklarują odpowiednie operator & zdefiniowane przez użytkownika lub operator |, oba następujące elementy są prawdziwe, w przypadku gdy T jest typem, w którym zadeklarowany jest wybrany operator:

  • Typ zwracany i typ każdego parametru wybranego operatora musi być T. Innymi słowy, operator oblicza logiczne AND lub logiczne OR dwóch operandów typu T, i zwraca wynik typu T.
  • T zawiera deklaracje operator true i operator false.

Błąd czasu powiązania występuje, jeśli którekolwiek z tych wymagań nie zostanie spełnione. W przeciwnym razie operacja && lub || jest obliczana przez połączenie zdefiniowanej przez użytkownika operator true lub operator false z wybranym operatorem zdefiniowanym przez użytkownika:

  • Operacja x && y jest oceniana jako T.false(x) ? x : T.&(x, y), gdzie T.false(x) jest wywołaniem operator false zadeklarowanym w T, a T.&(x, y) jest wywołaniem wybranego operator &. Innymi słowy, x jest najpierw oceniane, a operator false jest wywoływana w wyniku w celu określenia, czy x jest zdecydowanie fałszywe. Następnie, jeśli x jest zdecydowanie fałsz, wynikiem operacji jest wartość wcześniej obliczona dla x. W przeciwnym razie y jest obliczana, a wybrana operator & jest wywoływana na wcześniej obliczonej wartości dla x i wartości obliczonej dla y w celu wygenerowania wyniku operacji.
  • Operacja x || y jest oceniana jako T.true(x) ? x : T.|(x, y), gdzie T.true(x) jest wywołaniem operator true zadeklarowanym w T, a T.|(x, y) jest wywołaniem wybranego operator |. Innymi słowy, x jest najpierw oceniane, a operator true jest wywoływana w wyniku, aby określić, czy x jest zdecydowanie prawdziwe. Następnie, jeśli x jest zdecydowanie prawdziwe, wynikiem operacji jest wartość wcześniej obliczona dla x. W przeciwnym razie y jest obliczana, a wybrana operator | jest wywoływana na wcześniej obliczonej wartości dla x i wartości obliczonej dla y w celu wygenerowania wyniku operacji.

W jednej z tych operacji wyrażenie podane przez x jest obliczane tylko raz, a wyrażenie podane przez y nie jest obliczane lub obliczane dokładnie raz.

12.15 Operator łączenia wartości null

Operator ?? jest nazywany operatorem łączenia wartości null.

null_coalescing_expression
    : conditional_or_expression
    | conditional_or_expression '??' null_coalescing_expression
    | throw_expression
    ;

W wyrażeniu łączenia wartości null w formie a ?? b, jeśli a nie jestnull, wynik to a; w przeciwnym razie wynik to b. Operacja oblicza b tylko wtedy, gdy a jest null.

Operator łączenia wartości null jest prawostronnie łączący, co oznacza, że operacje są grupowane od prawej do lewej.

Przykład: wyrażenie w formie a ?? b ?? c jest obliczane jako a ?? (b ?? c). Ogólnie rzecz biorąc, wyrażenie w formie E1 ?? E2 ?? ... ?? EN zwraca pierwszy z operandów, który nie jestnull, lub null, jeśli wszystkie operandy są null. koniec przykładu

Typ wyrażenia a ?? b zależy od tego, które niejawne konwersje są dostępne dla operandów. W kolejności preferencji typ a ?? b jest A₀, Alub B, gdzie A jest typem a (pod warunkiem, że a ma typ), B jest typem b(pod warunkiem, że b ma typ), a A₀ jest podstawowym typem A, jeśli A jest typem wartości dopuszczalnej lub A w przeciwnym razie. W szczególności a ?? b jest przetwarzany w następujący sposób:

  • Jeśli A istnieje i nie jest typem wartości dopuszczającym wartości null lub typem referencyjnym, wystąpi błąd czasu kompilacji.
  • W przeciwnym razie, jeśli A istnieje i b jest wyrażeniem dynamicznym, typ wyniku jest dynamic. W czasie wykonywania a jest najpierw oceniane. Jeśli a nie jest null, a jest konwertowany na dynamic, a to staje się wynikiem. W przeciwnym razie obliczane jest b, które staje się wynikiem.
  • W przeciwnym razie, jeśli A istnieje i jest typem wartości akceptującym wartości null, a istnieje niejawna konwersja z b do A₀, typ wyniku jest A₀. W czasie wykonywania a jest najpierw oceniane. Jeśli a nie jest null, a zostanie rozpasany, aby wpisać A₀, co stanie się wynikiem. W przeciwnym razie b jest oceniana i konwertowana na typ A₀, co staje się wynikiem.
  • W przeciwnym razie, jeśli A istnieje i istnieje niejawna konwersja z b na A, typ wyniku jest A. Podczas wykonywania program a jest najpierw oceniany. Jeśli a nie ma wartości null, a staje się wynikiem. W przeciwnym razie b jest oceniana i konwertowana na typ A, co staje się wynikiem.
  • W przeciwnym razie, jeśli A istnieje i jest typem dopuszczalnej wartości null, b ma typ B i jeśli istnieje niejawna konwersja z A₀ do B, wtedy typ wyniku jest B. W czasie wykonywania a jest najpierw oceniane. Jeśli a nie jest null, a jest rozpakowany do typu A₀ i konwertowany na typ B, a to staje się wynikiem. W przeciwnym razie b jest obliczane i staje się wynikiem.
  • W przeciwnym razie, jeśli b ma typ B, a niejawna konwersja istnieje z a do B, typ wyniku jest B. W czasie wykonywania a jest najpierw oceniane. Jeśli a nie jest null, a jest konwertowany na typ B, a to staje się wynikiem. W przeciwnym razie b jest obliczane i staje się wynikiem.

W przeciwnym razie a i b są niezgodne, a występuje błąd czasu kompilacji.

12.16 Operator wyrażenia throw

throw_expression
    : 'throw' null_coalescing_expression
    ;

throw_expression zgłasza wartość wygenerowaną przez ocenę null_coalescing_expression. Wyrażenie jest niejawnie konwertowane na System.Exception, a wynik oceny wyrażenia jest konwertowany na System.Exception przed zgłoszeniem. Zachowanie w czasie wykonywania oceny wyrażenia throw jest takie samo jak określone dla instrukcji throw (§13.10.6).

throw_expression nie ma typu. throw_expression jest konwertowany na każdy typ przez niejawną konwersję rzutu .

Wyrażenie throw może występować tylko w następujących kontekstach składniowych:

  • Jako drugi lub trzeci operand ternarnego operatora warunkowego (?:).
  • Jako drugi operand operatora łączenia wartości null (??).
  • Jako ciało wyrażeniowe lambda lub elementu.

12.17 Wyrażenia deklaracji

Wyrażenie deklaracji deklaruje zmienną lokalną.

declaration_expression
    : local_variable_type identifier
    ;

local_variable_type
    : type
    | 'var'
    ;

simple_name_ jest również uważany za wyrażenie deklaracji, jeśli wyszukiwanie prostej nazwy nie znalazło skojarzonej deklaracji (§12.8.4). W przypadku użycia jako wyrażenia deklaracji jest nazywane prostym odrzucaniem. Jest to semantycznie równoważne var _, ale jest dozwolone w więcej miejscach.

Wyrażenie deklaracji odbywa się tylko w następujących kontekstach składniowych:

  • Jako outargument_value w argument_list.
  • Jako proste odrzucenie _ składające się z lewej strony prostego przypisania (§12.21.2).
  • Jako tuple_element w co najmniej jednej rekursywnie zagnieżdżonej tuple_expression, z których najbardziej zewnętrzna składa się z lewej strony przypisania dekonstrukcyjnego. deconstruction_expression powoduje powstanie wyrażeń deklaracji w tej pozycji, mimo że wyrażenia deklaracji nie są składniowo obecne.

Uwaga: To oznacza, że wyrażenie deklaracji nie może być objęte nawiasem. uwaga końcowa

Jest to błąd, gdy niejawnie typizowana zmienna zadeklarowana za pomocą declaration_expression jest referencjonowana w argument_list, w którym jest zadeklarowana.

Jest to błąd zmiennej zadeklarowanej przy użyciu declaration_expression, do której ma się odwoływać w ramach dekonstrukcji przypisania, w którym występuje.

Wyrażenie deklaracji, które jest prostym odrzuceniem lub gdzie local_variable_type jest identyfikatorem var, jest klasyfikowane jako zmienna o niejawnie określonym typie . Wyrażenie nie ma typu, a typ zmiennej lokalnej jest wnioskowany na podstawie kontekstu składni w następujący sposób:

  • W argument_list wnioskowany typ zmiennej jest zadeklarowanym typem odpowiedniego parametru.
  • Jako lewa strona prostego przypisania, wywnioskowany typ zmiennej jest tym samym, co typ prawej strony przypisania.
  • W przypadku wyrażenia typu tuple_expression po lewej stronie prostego przypisania wywnioskowany typ zmiennej jest taki sam jak typ odpowiadającego elementu krotki po prawej stronie przypisania (po dekonstrukcji).

W przeciwnym razie wyrażenie deklaracji jest klasyfikowane jako jawnie wpisaną zmienną, a typ wyrażenia oraz zadeklarowanej zmiennej musi być określony przez local_variable_type.

Wyrażenie deklaracji z identyfikatorem _ jest odrzucane (§9.2.9.2) i nie wprowadza nazwy zmiennej. Wyrażenie deklaracji z identyfikatorem innym niż _ wprowadza tę nazwę do najbliższej otaczającej przestrzeni deklaracji zmiennej lokalnej (§7.3).

Przykład:

string M(out int i, string s, out bool b) { ... }

var s1 = M(out int i1, "One", out var b1);
Console.WriteLine($"{i1}, {b1}, {s1}");
// Error: i2 referenced within declaring argument list
var s2 = M(out var i2, M(out i2, "Two", out bool b2), out b2);
var s3 = M(out int _, "Three", out var _);

Deklaracja s1 pokazuje zarówno jawnie, jak i niejawnie wpisane wyrażenia deklaracji. Wnioskowany typ b1 jest bool, ponieważ jest to typ odpowiedniego parametru wyjściowego w M1. Kolejny WriteLine może uzyskiwać dostęp do i1 i b1, które zostały wprowadzone do otaczającego zakresu.

Deklaracja s2 pokazuje próbę użycia i2 w zagnieżdżonym wywołaniu funkcji M, co jest niedozwolone, ponieważ referencja występuje na liście argumentów, gdzie zadeklarowano i2. Z drugiej strony odwołanie do b2 w ostatnim argumencie jest dozwolone, ponieważ występuje po końcu zagnieżdżonej listy argumentów, gdzie b2 został zadeklarowany.

Deklaracja s3 pokazuje użycie zarówno niejawnie, jak i jawnie wpisanych wyrażeń deklaracji, które są odrzucane. Ponieważ odrzucenia nie deklarują nazwanej zmiennej, dozwolone są wiele wystąpień identyfikatora _.

(int i1, int _, (var i2, var _), _) = (1, 2, (3, 4), 5);

W tym przykładzie pokazano użycie niejawnie i jawnie typizowanych wyrażeń deklaracji zarówno dla zmiennych, jak i odrzucanych w przypisaniu dekonstrukcyjnym. simple_name_ jest odpowiednikiem var _, gdy nie znaleziono deklaracji _.

void M1(out int i) { ... }

void M2(string _)
{
    M1(out _);      // Error: `_` is a string
    M1(out var _);
}

W tych przykładach pokazano użycie var _ w celu zapewnienia niejawnie typizowanego odrzucenia, gdy _ jest niedostępna, ponieważ wyznacza zmienną w otaczającym zakresie.

koniec przykładu

12.18 Operator warunkowy

Operator ?: jest nazywany operatorem warunkowym. Czasami nazywa się go również operatorem ternarnym.

conditional_expression
    : null_coalescing_expression
    | null_coalescing_expression '?' expression ':' expression
    | null_coalescing_expression '?' 'ref' variable_reference ':'
      'ref' variable_reference
    ;

Wyrażenie throw (§12.16) nie jest dozwolone w operatorze warunkowym, jeśli ref jest obecne.

Wyrażenie warunkowe formularza b ? x : y najpierw oblicza warunek b. Następnie, jeśli b jest true, x jest oceniany i staje się wynikiem operacji. W przeciwnym razie y jest obliczane i stanowi wynik operacji. Wyrażenie warunkowe nigdy nie ocenia zarówno x, jak i y.

Operator warunkowy jest asocjacyjny od prawej do lewej, co oznacza, że operacje są grupowane od prawej do lewej.

Przykład: wyrażenie w formie a ? b : c ? d : e jest obliczane jako a ? b : (c ? d : e). koniec przykładu

Pierwszy operand operatora ?: jest wyrażeniem, które można niejawnie przekonwertować na boollub wyrażenie typu implementujące operator true. Jeśli żadne z tych wymagań nie zostanie spełnione, wystąpi błąd czasu kompilacji.

Jeśli ref jest obecny:

  • Powinna istnieć konwersja tożsamości między typami dwóch variable_reference, a typ wyniku może być dowolnym z tych typów. Jeśli dowolny typ jest dynamic, wnioskowanie typu preferuje dynamic (§8.7). Jeśli dowolny z typów jest typem krotki (§8.3.11), wnioskowanie typu uwzględnia nazwy elementów, gdy nazwy te są zgodne w tej samej pozycji porządkowej w obu krotkach.
  • Wynik jest odwołaniem do zmiennej, która jest zapisywalna, jeśli oba variable_references są możliwe do zapisu.

Uwaga: gdy ref jest obecny, wyrażenie warunkowe zwraca odwołanie do zmiennej, które można przypisać do zmiennej referencyjnej przy użyciu operatora = ref lub przekazane jako parametr referencyjny/wejściowy/wyjściowy. uwaga końcowa

Jeśli ref nie istnieje, drugi i trzeci operandy, x i yoperatora ?: kontrolują typ wyrażenia warunkowego:

  • Jeśli x ma typ X, a y ma typ Y,
    • Jeśli istnieje konwersja identycznościowa między X a Y, wynikiem jest najlepszy wspólny typ zbioru wyrażeń (§12.6.3.15). Jeśli dowolny typ jest dynamic, wnioskowanie typu preferuje dynamic (§8.7). Jeśli dowolny z typów jest typem krotki (§8.3.11), wnioskowanie typu uwzględnia nazwy elementów, gdy nazwy te są zgodne w tej samej pozycji porządkowej w obu krotkach.
    • W przeciwnym razie, jeśli konwersja niejawna (§10.2) istnieje z X do Y, ale nie z Y do X, Y jest typem wyrażenia warunkowego.
    • W przeciwnym razie, jeśli istnieje niejawna konwersja wyliczenia (§10.2.4) z X do Y, Y jest typem wyrażenia warunkowego.
    • W przeciwnym razie, jeśli istnieje niejawna konwersja wyliczenia (§10.2.4) z Y do X, X jest typem wyrażenia warunkowego.
    • W przeciwnym razie, jeśli konwersja niejawna (§10.2) istnieje z Y do X, ale nie z X do Y, X jest typem wyrażenia warunkowego.
    • W przeciwnym razie nie można określić typu wyrażenia i występuje błąd czasu kompilacji.
  • Jeśli tylko jeden z x i y ma typ, a zarówno x, jak i y są niejawnie konwertowane na ten typ, jest to typ wyrażenia warunkowego.
  • W przeciwnym razie nie można określić typu wyrażenia i występuje błąd czasu kompilacji.

Przetwarzanie w czasie wykonywania wyrażenia warunkowego ref w postaci b ? ref x : ref y składa się z następujących kroków:

  • Najpierw b jest obliczana, a wartość bool dla b jest określana:
    • Jeśli istnieje niejawna konwersja typu b na bool, ta niejawna konwersja jest wykonywana w celu wygenerowania wartości bool.
    • W przeciwnym razie operator true, którą definiuje typ b, zostaje wywołana w celu utworzenia wartości bool.
  • Jeśli wartość bool wygenerowana przez powyższy krok jest true, zostanie obliczona x, a wynikowe odwołanie do zmiennej stanie się wynikiem wyrażenia warunkowego.
  • W przeciwnym razie y jest oceniane, a odniesienie do zmiennej wynikowej staje się wynikiem wyrażenia warunkowego.

Przetwarzanie w trakcie działania wyrażenia warunkowego w postaci b ? x : y składa się z następujących kroków:

  • Najpierw b jest obliczana, a wartość bool dla b jest określana:
    • Jeśli istnieje niejawna konwersja typu b na bool, ta niejawna konwersja jest wykonywana w celu wygenerowania wartości bool.
    • W przeciwnym razie operator true, którą definiuje typ b, zostaje wywołana w celu utworzenia wartości bool.
  • Jeśli wartość bool wygenerowana przez powyższy krok jest true, x jest obliczana i konwertowana na typ wyrażenia warunkowego, a to staje się wynikiem wyrażenia warunkowego.
  • W przeciwnym razie y jest obliczana i konwertowana na typ wyrażenia warunkowego, a to staje się wynikiem wyrażenia warunkowego.

12.19 Anonimowe wyrażenia funkcji

12.19.1 Ogólne

Funkcja anonimowa jest wyrażeniem reprezentującym definicję metody "w wierszu". Funkcja anonimowa nie ma wartości ani typu w sobie, ale jest konwertowana na zgodny typ delegata lub drzewa wyrażeń. Ocena konwersji funkcji anonimowej zależy od typu docelowego konwersji: jeśli jest to typ delegata, konwersja jest obliczana na wartość delegata odwołującą się do metody zdefiniowanej przez funkcję anonimową. Jeśli jest to typ drzewa wyrażeń, konwersja jest obliczana na drzewo wyrażeń, które reprezentuje strukturę metody jako strukturę obiektu.

Uwaga: Ze względów historycznych istnieją dwa składniowe warianty funkcji anonimowych, a mianowicie wyrażenia lambdai wyrażenia metody anonimowej. Dla prawie wszystkich celów lambda_expressionsą bardziej zwięzłe i wyraziste niż anonymous_method_expression, które pozostają w języku ze względu na zgodność wsteczną. uwaga końcowa

lambda_expression
    : 'async'? anonymous_function_signature '=>' anonymous_function_body
    ;

anonymous_method_expression
    : 'async'? 'delegate' explicit_anonymous_function_signature? block
    ;

anonymous_function_signature
    : explicit_anonymous_function_signature
    | implicit_anonymous_function_signature
    ;

explicit_anonymous_function_signature
    : '(' explicit_anonymous_function_parameter_list? ')'
    ;

explicit_anonymous_function_parameter_list
    : explicit_anonymous_function_parameter
      (',' explicit_anonymous_function_parameter)*
    ;

explicit_anonymous_function_parameter
    : anonymous_function_parameter_modifier? type identifier
    ;

anonymous_function_parameter_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

implicit_anonymous_function_signature
    : '(' implicit_anonymous_function_parameter_list? ')'
    | implicit_anonymous_function_parameter
    ;

implicit_anonymous_function_parameter_list
    : implicit_anonymous_function_parameter
      (',' implicit_anonymous_function_parameter)*
    ;

implicit_anonymous_function_parameter
    : identifier
    ;

anonymous_function_body
    : null_conditional_invocation_expression
    | expression
    | 'ref' variable_reference
    | block
    ;

Przy rozpoznawaniu anonymous_function_body, jeśli stosowane są zarówno null_conditional_invocation_expression, jak i wyrażenie alternatywy, należy wybrać pierwszy.

Uwaga: Nakładanie się oraz priorytet pomiędzy alternatywami są wyłącznie dla wygody opisowej; reguły gramatyczne można opracować, aby usunąć nakładania się. ANTLR i inne systemy gramatyczne przyjmują tę samą wygodę i dlatego anonymous_function_body ma określone semantyki automatycznie. uwaga końcowa

Uwaga: w przypadku traktowania jako wyrażenia formuła składniowa, taka jak x?.M(), byłaby błędem, jeśli typ wyniku M jest void (§12.8.13). Jednak w przypadku traktowania jako null_conditional_invocation_expressiontyp wyniku może być void. uwaga końcowa

Przykład: typ wyniku List<T>.Reverse to void. W poniższym kodzie treść wyrażenia anonimowego jest null_conditional_invocation_expression, więc nie jest to błąd.

Action<List<int>> a = x => x?.Reverse();

koniec przykładu

Operator => ma takie samo pierwszeństwo jak przypisanie (=) i jest prawostronnie łączny.

Funkcja anonimowa z modyfikatorem async jest funkcją asynchronikową i jest zgodna z regułami opisanymi w §15.15.

Parametry funkcji anonimowej w postaci lambda_expression mogą być jawnie lub niejawnie wpisywane. Na liście jawnie wpisanych parametrów typ każdego parametru jest jawnie określony. W niejawnie typizowanej liście parametrów typy parametrów są domyślnie określane na podstawie kontekstu, w którym występuje funkcja anonimowa — w szczególności, gdy funkcja anonimowa jest zamieniana na odpowiedni typ delegata lub typ drzewa wyrażeń, ten typ określa typy parametrów (§10.7).

W lambda_expression z jednym, niejawnie wpisanym parametrem nawiasy mogą zostać pominięte z listy parametrów. Innymi słowy, anonimowa funkcja formularza

( «param» ) => «expr»

można skrócić do

«param» => «expr»

Lista parametrów funkcji anonimowej w postaci anonymous_method_expression jest opcjonalna. W przypadku podania parametrów należy jawnie wpisać. Jeśli nie, funkcja anonimowa może zostać przekształcona w delegata z dowolną listą parametrów pod warunkiem, że nie zawiera ona parametrów wyjściowych.

Ciało bloku funkcji anonimowej jest zawsze osiągalne (§13.2).

Przykład: poniżej przedstawiono kilka przykładów funkcji anonimowych:

x => x + 1                             // Implicitly typed, expression body
x => { return x + 1; }                 // Implicitly typed, block body
(int x) => x + 1                       // Explicitly typed, expression body
(int x) => { return x + 1; }           // Explicitly typed, block body
(x, y) => x * y                        // Multiple parameters
() => Console.WriteLine()              // No parameters
async (t1,t2) => await t1 + await t2   // Async
delegate (int x) { return x + 1; }     // Anonymous method expression
delegate { return 1 + 1; }             // Parameter list omitted

koniec przykładu

Zachowanie lambda_expressions i anonymous_method_expressions jest takie samo, z wyjątkiem następujących kwestii:

  • anonymous_method_expressionumożliwia całkowite pominięcie listy parametrów, co pozwala na konwersję do typów delegatów z dowolną listą parametrów wartości.
  • lambda_expressionpozwala na pomijanie i wnioskowanie typów parametrów, natomiast anonymous_method_expressionwymaga ich jawnego określenia.
  • Treść lambda_expression może być wyrażeniem lub blokiem, natomiast treść anonymous_method_expression jest blokiem.
  • Tylko lambda_expressionmają konwersje na zgodne typy drzewa wyrażeń (§8.6).

12.19.2 Sygnatury funkcji anonimowych

anonymous_function_signature określa nazwy i opcjonalnie typy parametrów dla funkcji anonimowej. Zakres parametrów funkcji anonimowej to anonymous_function_body (§7.7). Razem z listą parametrów (jeśli jest podana) ciało metody anonimowej stanowi przestrzeń deklaracji (§7.3). W związku z tym nazwanie parametru funkcji anonimowej tak samo, jak zmiennej lokalnej, stałej lokalnej lub innego parametru, którego zakres obejmuje anonymous_method_expression lub lambda_expression, stanowi błąd czasu kompilacji.

Jeśli funkcja anonimowa ma explicit_anonymous_function_signature, zestaw zgodnych typów delegatów i typów drzewa wyrażeń jest ograniczony do tych, które mają te same typy parametrów i modyfikatory w tej samej kolejności (§10.7). W przeciwieństwie do konwersji grup metod (§10.8), kontrawariancja anonimowych typów parametrów funkcji nie jest obsługiwana. Jeśli funkcja anonimowa nie ma anonymous_function_signature, wtedy zestaw zgodnych typów delegatów i drzew wyrażeń jest ograniczony do tych, które nie mają parametrów wyjściowych.

Należy pamiętać, że anonymous_function_signature nie może zawierać atrybutów ani tablicy parametrów. Niemniej jednak anonymous_function_signature może być zgodna z typem delegata, którego lista argumentów zawiera tablicę parametrów.

Należy również pamiętać, że konwersja na typ drzewa wyrażeń, nawet jeśli jest zgodna, może nadal zakończyć się niepowodzeniem w czasie kompilacji (§8.6).

12.19.3 Anonimowe jednostki funkcji

Treść (wyrażenie lub blok ) funkcji anonimowej podlega następującym regułom:

  • Jeśli funkcja anonimowa zawiera podpis, parametry określone w podpisie są dostępne w treści. Jeśli funkcja anonimowa nie ma podpisu, można ją przekonwertować na typ delegata lub typ wyrażenia o parametrach (§10.7), ale nie można uzyskać dostępu do parametrów w treści.
  • Z wyjątkiem parametrów by-reference określonych w podpisie (jeśli istnieje) najbliższej otaczającej funkcji anonimowej, jest to błąd czasu kompilacji dla treści dostępu do parametru by-reference.
  • Z wyjątkiem parametrów określonych w sygnaturze (jeśli istnieje) najbliższej otaczającej funkcji anonimowej, w ciele występuje błąd kompilacji, gdy próbuje się uzyskać dostęp do parametru typu ref struct.
  • Gdy typ this jest typem struktury, próba uzyskania dostępu do thisstanowi błąd czasu kompilacji. To prawda, niezależnie od tego, czy dostęp jest wyraźny (jak w this.x), czy niejawny (jak w x, gdzie x jest elementem instancji struktury). Ta reguła po prostu zabrania takiego dostępu i nie ma wpływu na to, czy wyszukiwanie zwraca członka struktury.
  • Ciało ma dostęp do zmiennych zewnętrznych (§12.19.6) funkcji anonimowej. Dostęp do zmiennej zewnętrznej odniesie się do egzemplarza zmiennej, który jest aktywny w momencie oceny wyrażenia lambda_expression lub anonymous_method_expression (§12.19.7).
  • Jest to błąd czasu kompilacji dla treści zawierającej instrukcję goto, instrukcję break lub instrukcję continue, której element docelowy znajduje się poza treścią lub w treści zawartej funkcji anonimowej.
  • Instrukcja return w treści zwraca kontrolę z wywołania najbliższej otaczającej funkcji anonimowej, a nie z otaczającej funkcji członkowskiej.

Jest jawnie nieokreślone, czy istnieje jakikolwiek sposób wykonania bloku funkcji anonimowej inny niż poprzez ewaluację i wywołanie lambda_expression lub anonymous_method_expression. W szczególności kompilator może zdecydować się na zaimplementowanie funkcji anonimowej przez synchronizowanie co najmniej jednej nazwanej metody lub typów. Nazwy wszelkich takich syntetyzowanych elementów mają postać zarezerwowaną do użytku kompilatora (§6.4.3).

12.19.4 Rozpoznawanie przeciążenia

Funkcje anonimowe w liście argumentów uczestniczą w wywnioskowaniu typów i rozwiązywaniu przeciążenia. Zapoznaj się z §12.6.3 i §12.6.4, aby uzyskać dokładne zasady.

Przykład: Poniższy przykład ilustruje wpływ funkcji anonimowych na rozpoznawanie przeciążeń.

class ItemList<T> : List<T>
{
    public int Sum(Func<T, int> selector)
    {
        int sum = 0;
        foreach (T item in this)
        {
            sum += selector(item);
        }
        return sum;
    }

    public double Sum(Func<T, double> selector)
    {
        double sum = 0;
        foreach (T item in this)
        {
            sum += selector(item);
        }
        return sum;
    }
}

Klasa ItemList<T> ma dwie metody Sum. Każdy przyjmuje argument selector, który wyodrębnia wartość do zsumowania z elementu listy. Wyodrębniona wartość może być albo int, albo double, a wynikowa suma jest odpowiednio albo int, albo double.

Metody Sum mogą na przykład służyć do obliczania sum z listy wierszy szczegółowych w zamówieniu.

class Detail
{
    public int UnitCount;
    public double UnitPrice;
    ...
}

class A
{
    void ComputeSums()
    {
        ItemList<Detail> orderDetails = GetOrderDetails( ... );
        int totalUnits = orderDetails.Sum(d => d.UnitCount);
        double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount);
        ...
    }

    ItemList<Detail> GetOrderDetails( ... )
    {
        ...
    }
}

W pierwszym wywołaniu orderDetails.Summają zastosowanie obie metody Sum, ponieważ funkcja anonimowa d => d.UnitCount jest zgodna zarówno z Func<Detail,int>, jak i Func<Detail,double>. Mechanizm rozwiązywania przeciążeń wybiera pierwszą metodę Sum, ponieważ konwersja na Func<Detail,int> jest lepsza niż konwersja na Func<Detail,double>.

W drugim wywołaniu orderDetails.Summa zastosowanie tylko druga metoda Sum, ponieważ funkcja anonimowa d => d.UnitPrice * d.UnitCount generuje wartość typu double. W związku z tym rozpoznawanie przeciążeń wybiera drugą metodę Sum dla tego wywołania.

koniec przykładu

12.19.5 Funkcje anonimowe i powiązanie dynamiczne

Funkcja anonimowa nie może być odbiornikiem, argumentem ani operandem operacji dynamicznie powiązanej.

12.19.6 Zmienne zewnętrzne

12.19.6.1 Ogólne

Każda lokalna zmienna, parametr wartości lub tablica parametrów, której zakres zawiera lambda_expression lub anonymous_method_expression, jest nazywany zmienną zewnętrzną funkcji anonimowej. W składowej funkcji wystąpienia klasy ta wartość jest traktowana jako parametr wartości i jest zewnętrzną zmienną dowolnej funkcji anonimowej zawartej w składowej funkcji.

12.19.6.2 Przechwycone zmienne zewnętrzne

Gdy zmienna zewnętrzna jest przywoływana przez funkcję anonimową, mówi się, że zmienna zewnętrzna została przechwycona przez funkcję anonimową. Zazwyczaj okres istnienia zmiennej lokalnej jest ograniczony do wykonywania bloku lub instrukcji, z którą jest skojarzona (§9.2.9.1). Jednak okres istnienia przechwyconej zmiennej zewnętrznej jest dłuższy co najmniej do momentu, aż delegat lub drzewo wyrażeń utworzone na podstawie funkcji anonimowej będzie kwalifikować się do zbierania śmieci.

Przykład: w przykładzie

delegate int D();

class Test
{
    static D F()
    {
        int x = 0;
        D result = () => ++x;
        return result;
    }

    static void Main()
    {
        D d = F();
        Console.WriteLine(d());
        Console.WriteLine(d());
        Console.WriteLine(d());
    }
}

lokalna zmienna x jest przechwytywana przez funkcję anonimową, a okres istnienia x zostanie przedłużony przynajmniej do czasu, aż delegat zwrócony z F będzie kwalifikował się do zbierania śmieci. Ponieważ każde wywołanie funkcji anonimowej operuje na tym samym wystąpieniu x, wynik działania przykładu to:

1
2
3

koniec przykładu

Gdy zmienna lokalna lub parametr wartości jest przechwytywany przez funkcję anonimową, zmienna lokalna lub parametr nie jest już uznawana za zmienną stałą (§23.4), ale jest zamiast tego uważana za zmienną zmienną do przenoszenia. Jednak przechwycone zmienne zewnętrzne nie mogą być używane w instrukcji fixed (§23.7), więc nie można użyć adresu przechwyconej zmiennej zewnętrznej.

Uwaga: w przeciwieństwie do nieuchwyconej zmiennej, przechwycona zmienna lokalna może być jednocześnie widoczna dla wielu wątków wykonywania. uwaga końcowa

12.19.6.3 Utworzenie wystąpienia zmiennych lokalnych

Zmienna lokalna jest uważana za zainicjalizowana, gdy wykonanie wchodzi w zakres zmiennej.

Przykład: na przykład, gdy wywoływana jest następująca metoda, zmienna lokalna x jest tworzona i inicjowana trzy razy — raz dla każdej iteracji pętli.

static void F()
{
    for (int i = 0; i < 3; i++)
    {
        int x = i * 2 + 1;
        ...
    }
}

Jednak przeniesienie deklaracji x poza pętlę powoduje utworzenie pojedynczego egzemplarza x:

static void F()
{
    int x;
    for (int i = 0; i < 3; i++)
    {
        x = i * 2 + 1;
        ...
    }
}

koniec przykładu

W przypadku braku przechwycenia nie ma możliwości dokładnego obserwowania, jak często jest tworzone wystąpienie zmiennej lokalnej — ponieważ okresy istnienia wystąpień wystąpień są rozłączne, istnieje możliwość, aby każde wystąpienie po prostu używało tej samej lokalizacji przechowywania. Jednak gdy funkcja anonimowa przechwytuje zmienną lokalną, efekty instancjonowania stają się widoczne.

Przykład: przykład

delegate void D();
class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        for (int i = 0; i < 3; i++)
        {
            int x = i * 2 + 1;
            result[i] = () => Console.WriteLine(x);
        }
        return result;
    }

    static void Main()
    {
        foreach (D d in F())
        {
            d();
        }
    }
}

generuje dane wyjściowe:

1
3
5

Jednak po przeniesieniu deklaracji x poza pętlą:

delegate void D();

class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        int x;
        for (int i = 0; i < 3; i++)
        {
            x = i * 2 + 1;
            result[i] = () => Console.WriteLine(x);
        }
        return result;
   }

   static void Main()
   {
       foreach (D d in F())
       {
           d();
       }
   }
}

dane wyjściowe to:

5
5
5

Należy pamiętać, że kompilatorowi wolno (ale nie jest wymagane) dokonać optymalizacji trzech wystąpień w jedną instancję delegata (§10.7.2).

koniec przykładu

Jeśli pętla for deklaruje zmienną iteracji, ta zmienna jest uważana za zadeklarowaną poza pętlą.

Przykład: Zatem, jeśli przykład zostanie zmieniony w celu uchwycenia zmiennej iteracyjnej:

delegate void D();

class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        for (int i = 0; i < 3; i++)
        {
            result[i] = () => Console.WriteLine(i);
        }
        return result;
   }

   static void Main()
   {
       foreach (D d in F())
       {
           d();
       }
   }
}

przechwytywane jest tylko jedno wystąpienie zmiennej iteracji, co powoduje wygenerowanie danych wyjściowych:

3
3
3

koniec przykładu

Możliwe jest, że delegaty funkcji anonimowych będą współdzielić niektóre przechwycone zmienne, ale mieć oddzielne wystąpienia innych.

Przykład: na przykład jeśli F zostanie zmieniona na

static D[] F()
{
    D[] result = new D[3];
    int x = 0;
    for (int i = 0; i < 3; i++)
    {
        int y = 0;
        result[i] = () => Console.WriteLine($"{++x} {++y}");
    }
    return result;
}

trzej delegaci przechwytują to samo wystąpienie x, ale oddzielne wystąpienia y, a dane wyjściowe to:

1 1
2 1
3 1

koniec przykładu

Oddzielne funkcje anonimowe mogą przechwytywać to samo wystąpienie zmiennej zewnętrznej.

Przykład: w przykładzie:

delegate void Setter(int value);
delegate int Getter();

class Test
{
    static void Main()
    {
        int x = 0;
        Setter s = (int value) => x = value;
        Getter g = () => x;
        s(5);
        Console.WriteLine(g());
        s(10);
        Console.WriteLine(g());
    }
}

dwie funkcje anonimowe przechwytują to samo wystąpienie zmiennej lokalnej x, a tym samym mogą "komunikować się" za pośrednictwem tej zmiennej. Dane wyjściowe przykładu to:

5
10

koniec przykładu

12.19.7 Ocena anonimowych wyrażeń funkcji

Funkcja anonimowa F zawsze jest konwertowana na typ delegata D lub typ drzewa wyrażeń E, bezpośrednio lub przez wykonanie wyrażenia tworzenia delegata new D(F). Ta konwersja określa wynik funkcji anonimowej zgodnie z opisem w §10.7.

Przykład implementacji 12.19.8

Ten podpunkt jest informacyjny.

W tym podpunkcie opisano możliwą implementację przekształceń funkcji anonimowych w kontekście innych konstrukcji C#. Implementacja opisana w tym miejscu opiera się na tych samych zasadach używanych przez kompilator komercyjnego języka C#, ale nie jest to implementacja upoważniona ani jedyną możliwą. Tylko krótko wspomina konwersje na drzewa wyrażeń, ponieważ ich dokładna semantyka wykracza poza zakres tej specyfikacji.

Pozostała część tej podklauzuli zawiera kilka przykładów kodu, które zawierają funkcje anonimowe o różnorodnych cechach. Dla każdego przykładu jest udostępniane odpowiednie tłumaczenie kodu, które używa tylko innych konstrukcji języka C#. W przykładach identyfikator D jest używany do reprezentacji następującego typu delegata:

public delegate void D();

Najprostszą formą funkcji anonimowej jest taka, która nie przechwytuje żadnych zmiennych zewnętrznych:

delegate void D();

class Test
{
    static void F()
    {
        D d = () => Console.WriteLine("test");
    }
}

Można to przetłumaczyć na wystąpienie delegata, które odwołuje się do metody statycznej wygenerowanej przez kompilator, w której znajduje się kod funkcji anonimowej:

delegate void D();

class Test
{
    static void F()
    {
        D d = new D(__Method1);
    }

    static void __Method1()
    {
        Console.WriteLine("test");
    }
}

W poniższym przykładzie funkcja anonimowa odnosi się do członków instancji this:

delegate void D();

class Test
{
    int x;

    void F()
    {
        D d = () => Console.WriteLine(x);
    }
}

Można to przetłumaczyć na metodę wystąpienia wygenerowanego przez kompilator zawierający kod funkcji anonimowej:

delegate void D();

class Test
{
   int x;

   void F()
   {
       D d = new D(__Method1);
   }

   void __Method1()
   {
       Console.WriteLine(x);
   }
}

W tym przykładzie funkcja anonimowa przechwytuje zmienną lokalną:

delegate void D();

class Test
{
    void F()
    {
        int y = 123;
        D d = () => Console.WriteLine(y);
    }
}

Okres istnienia zmiennej lokalnej musi być teraz rozszerzony do co najmniej okresu istnienia delegata funkcji anonimowej. Można to osiągnąć przez "wciągnąć" zmienną lokalną do pola klasy wygenerowanej przez kompilator. Utworzenie wystąpienia zmiennej lokalnej (§12.19.6.3), a następnie odpowiada utworzeniu wystąpienia klasy wygenerowanej przez kompilator, a uzyskiwanie dostępu do zmiennej lokalnej odpowiada uzyskiwaniu dostępu do pola w wystąpieniu klasy wygenerowanej przez kompilator. Ponadto funkcja anonimowa staje się metodą wystąpienia klasy generowanej przez kompilator:

delegate void D();

class Test
{
    void F()
    {
        __Locals1 __locals1 = new __Locals1();
        __locals1.y = 123;
        D d = new D(__locals1.__Method1);
    }

    class __Locals1
    {
        public int y;

        public void __Method1()
        {
            Console.WriteLine(y);
        }
    }
}

Na koniec następująca funkcja anonimowa przechwytuje this, a także dwie zmienne lokalne z różnymi okresami istnienia:

delegate void D();

class Test
{
   int x;

   void F()
   {
       int y = 123;
       for (int i = 0; i < 10; i++)
       {
           int z = i * 2;
           D d = () => Console.WriteLine(x + y + z);
       }
   }
}

W tym miejscu jest tworzona klasa wygenerowana przez kompilator dla każdego bloku, w którym przechwytywane są zmienne lokalne, tak aby zmienne lokalne w różnych blokach mogły mieć niezależne czasy życia. Wystąpienie __Locals2, klasy wygenerowanej przez kompilator dla bloku wewnętrznego, zawiera zmienną lokalną z oraz pole odnoszące się do wystąpienia __Locals1. Wystąpienie __Locals1, klasy wygenerowanej przez kompilator dla bloku zewnętrznego, zawiera zmienną lokalną y i pole, które odwołuje się do this otaczającego członka funkcji. Dzięki tym strukturom danych można uzyskać dostęp do wszystkich przechwyconych zmiennych zewnętrznych za pośrednictwem wystąpienia __Local2, a kod funkcji anonimowej może zostać zaimplementowany jako metoda wystąpienia tej klasy.

delegate void D();

class Test
{
    int x;

    void F()
    {
        __Locals1 __locals1 = new __Locals1();
        __locals1.__this = this;
        __locals1.y = 123;
        for (int i = 0; i < 10; i++)
        {
            __Locals2 __locals2 = new __Locals2();
            __locals2.__locals1 = __locals1;
            __locals2.z = i * 2;
            D d = new D(__locals2.__Method1);
        }
    }

    class __Locals1
    {
        public Test __this;
        public int y;
    }

    class __Locals2
    {
        public __Locals1 __locals1;
        public int z;

        public void __Method1()
        {
            Console.WriteLine(__locals1.__this.x + __locals1.y + z);
        }
    }
}

Ta sama technika zastosowana tutaj do przechwytywania zmiennych lokalnych może być również używana podczas konwertowania funkcji anonimowych na drzewa wyrażeń: odwołania do obiektów generowanych przez kompilatora mogą być przechowywane w drzewie wyrażeń, a dostęp do zmiennych lokalnych może być reprezentowany jako dostęp do pól na tych obiektach. Zaletą tego podejścia jest to, że umożliwia współużytkowanie lokalnych zmiennych "wyprowadzonych" między delegatami a drzewami wyrażeń.

Koniec tekstu informacyjnego.

12.20 Wyrażenia zapytań

12.20.1 Ogólne

Wyrażenia zapytań zapewniają składnię zintegrowaną ze językiem dla zapytań podobnych do języków zapytań relacyjnych i hierarchicznych, takich jak SQL i XQuery.

query_expression
    : from_clause query_body
    ;

from_clause
    : 'from' type? identifier 'in' expression
    ;

query_body
    : query_body_clauses? select_or_group_clause query_continuation?
    ;

query_body_clauses
    : query_body_clause
    | query_body_clauses query_body_clause
    ;

query_body_clause
    : from_clause
    | let_clause
    | where_clause
    | join_clause
    | join_into_clause
    | orderby_clause
    ;

let_clause
    : 'let' identifier '=' expression
    ;

where_clause
    : 'where' boolean_expression
    ;

join_clause
    : 'join' type? identifier 'in' expression 'on' expression
      'equals' expression
    ;

join_into_clause
    : 'join' type? identifier 'in' expression 'on' expression
      'equals' expression 'into' identifier
    ;

orderby_clause
    : 'orderby' orderings
    ;

orderings
    : ordering (',' ordering)*
    ;

ordering
    : expression ordering_direction?
    ;

ordering_direction
    : 'ascending'
    | 'descending'
    ;

select_or_group_clause
    : select_clause
    | group_clause
    ;

select_clause
    : 'select' expression
    ;

group_clause
    : 'group' expression 'by' expression
    ;

query_continuation
    : 'into' identifier query_body
    ;

Wyrażenie zapytania rozpoczyna się od klauzuli from i kończy się klauzulą select lub group. Po początkowej klauzuli from może występować zero lub więcej klauzul from, let, where, join lub orderby. Każda klauzula from jest generatorem wprowadzającym zmienną zakresu , która przechodzi przez elementy sekwencji . Każda klauzula let wprowadza zmienną zakresu reprezentującą wartość obliczoną za pomocą poprzednich zmiennych zakresu. Każda klauzula where to filtr wykluczający elementy z wyniku. Każda klauzula join porównuje określone klucze sekwencji źródłowej z kluczami innej sekwencji, co daje pasujące pary. Każda klauzula orderby zmienia kolejność elementów zgodnie z określonymi kryteriami. Końcowa klauzula select lub group określa kształt wyniku pod względem zmiennych zakresu. Na koniec można użyć klauzuli into do "łączenia" zapytań, traktując wyniki jednego zapytania jako generator w kolejnym zapytaniu.

12.20.2 Niejednoznaczności w wyrażeniach zapytań

Wyrażenia zapytań używają kilku słów kluczowych kontekstowych (§6.4.4): ascending, by, descending, equals, from, group, into, join, let, on, orderby, select i where.

Aby uniknąć niejednoznaczności, które mogą wynikać z użycia tych identyfikatorów zarówno jako słów kluczowych, jak i prostych nazw tych identyfikatorów są traktowane jako słowa kluczowe w dowolnym miejscu w wyrażeniu zapytania, chyba że są one poprzedzone prefiksem "@" (§6.4.4), w którym przypadku są traktowane jako identyfikatory. W tym celu wyrażenie zapytania to dowolne wyrażenie rozpoczynające się od "fromidentyfikatora", a następnie dowolny token z wyjątkiem ";", "=" lub ",".

12.20.3 Tłumaczenie wyrażenia zapytania

12.20.3.1 Ogólne

Język C# nie określa semantyki wykonywania wyrażeń zapytań. Wyrażenia zapytania są tłumaczone na wywołania metod, które są zgodne ze wzorcem wyrażenia zapytania (§12.20.4). W szczególności wyrażenia zapytań są tłumaczone na wywołania metod o nazwie Where, Select, SelectMany, Join, GroupJoin, OrderBy, OrderByDescending, ThenBy, ThenByDescending, GroupByi Cast. Te metody powinny mieć określone podpisy i typy zwracane, zgodnie z opisem w §12.20.4. Metody te mogą być metodami instancji obiektu, do którego odwołuje się zapytanie, lub metodami rozszerzenia, które są definiowane poza obiektem. Te metody implementują rzeczywiste wykonanie zapytania.

Tłumaczenie z wyrażeń zapytania na wywołania metody jest mapowaniem składniowym, które występuje przed wykonaniem jakiegokolwiek powiązania typu lub rozpoznawania przeciążenia. Po tłumaczeniu wyrażeń zapytań wywołania metody wynikowej są przetwarzane jako zwykłe wywołania metod, co może z kolei spowodować wykrycie błędów czasu kompilacji. Te warunki błędu obejmują metody, które nie istnieją, argumenty nieprawidłowych typów i metody ogólne, w których wnioskowanie typu kończy się niepowodzeniem.

Wyrażenie zapytania jest przetwarzane wielokrotnie przez zastosowanie następujących tłumaczeń aż do momentu, gdy dalsze redukcje nie są już możliwe. Tłumaczenia są wymienione w kolejności aplikacji: każda sekcja zakłada, że tłumaczenia w poprzednich sekcjach zostały wykonane wyczerpująco, a po wyczerpaniu sekcja nie zostanie później ponownie podjęta w przetwarzaniu tego samego wyrażenia zapytania.

Błędem czasu kompilacji w wyrażeniu zapytania jest uwzględnienie przypisania do zmiennej zakresu lub użycie zmiennej zakresu jako argumentu dla parametru referencyjnego lub wyjściowego.

Niektóre tłumaczenia wprowadzają zmienne zakresu z przezroczystymi identyfikatorami oznaczone przez *. Zostały one opisane w §12.20.3.8.

12.20.3.2 Wyrażenia zapytań z kontynuacjami

Wyrażenie zapytania z kontynuacją po treści zapytania

from «x1» in «e1» «b1» into «x2» «b2»

jest tłumaczony na

from «x2» in ( from «x1» in «e1» «b1» ) «b2»

Tłumaczenia w poniższych sekcjach zakładają, że zapytania nie mają kontynuacji.

Przykład: Przykład:

from c in customers
group c by c.Country into g
select new { Country = g.Key, CustCount = g.Count() }

jest tłumaczony na:

from g in
   (from c in customers
   group c by c.Country)
select new { Country = g.Key, CustCount = g.Count() }

ostateczne tłumaczenie, o którym mowa:

customers.
GroupBy(c => c.Country).
Select(g => new { Country = g.Key, CustCount = g.Count() })

koniec przykładu

12.20.3.3 Jawne typy zmiennych zakresu

Klauzula from, która jawnie określa typ zmiennej zakresu

from «T» «x» in «e»

jest tłumaczony na

from «x» in ( «e» ) . Cast < «T» > ( )

Klauzula join, która jawnie określa typ zmiennej zakresu

join «T» «x» in «e» on «k1» equals «k2»

jest tłumaczony na

join «x» in ( «e» ) . Cast < «T» > ( ) on «k1» equals «k2»

Tłumaczenia w poniższych sekcjach zakładają, że zapytania nie mają jawnych typów zmiennych zakresu.

Przykład: przykład

from Customer c in customers
where c.City == "London"
select c

jest tłumaczony na

from c in (customers).Cast<Customer>()
where c.City == "London"
select c

końcowe tłumaczenie, które jest

customers.
Cast<Customer>().
Where(c => c.City == "London")

koniec przykładu

Uwaga: jawne typy zmiennych zakresu są przydatne do wykonywania zapytań dotyczących kolekcji, które implementują interfejs IEnumerable niegeneryczny, ale nie ogólny interfejs IEnumerable<T>. W powyższym przykładzie byłoby tak, gdyby klienci byli typu ArrayList. uwaga końcowa

12.20.3.4 Zdegenerowane wyrażenia zapytań

Wyrażenie zapytania formularza

from «x» in «e» select «x»

jest tłumaczony na

( «e» ) . Select ( «x» => «x» )

Przykład: przykład

from c in customers
select c

jest tłumaczony na

(customers).Select(c => c)

koniec przykładu

Zdegenerowane wyrażenie zapytania to takie, które w sposób trywialny wybiera elementy źródła.

Uwaga: późniejsze fazy tłumaczenia (§12.20.3.6 i §12.20.3.7) usuwają zgenerowane zapytania wprowadzone przez inne kroki tłumaczenia, zastępując je źródłem. Należy jednak upewnić się, że wynik wyrażenia zapytania nigdy nie jest obiektem źródłowym. W przeciwnym razie zwrócenie wyniku takiego zapytania może przypadkowo uwidocznić dane prywatne (np. tablicę elementów) do obiektu wywołującego. W związku z tym ten krok chroni degenerowane zapytania napisane bezpośrednio w kodzie źródłowym przez jawne wywołanie Select w źródle. Następnie do implementatorów Select i innych operatorów zapytań, aby upewnić się, że te metody nigdy nie zwracają samego obiektu źródłowego. uwaga końcowa

12.20.3.5 Od, let, where, join and orderby klauzule

Wyrażenie zapytania z drugą klauzulą from, po której następuje klauzula select

from «x1» in «e1»  
from «x2» in «e2»  
select «v»

jest tłumaczony na

( «e1» ) . SelectMany( «x1» => «e2» , ( «x1» , «x2» ) => «v» )

Przykład: przykład

from c in customers
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }

jest tłumaczony na

(customers).
SelectMany(c => c.Orders,
(c,o) => new { c.Name, o.OrderID, o.Total }
)

koniec przykładu

Wyrażenie zapytania z drugą klauzulą from, po której następuje treść zapytania Q zawierające niepusty zestaw klauzul treści zapytania:

from «x1» in «e1»
from «x2» in «e2»
Q

jest tłumaczony na

from * in («e1») . SelectMany( «x1» => «e2» ,
                              ( «x1» , «x2» ) => new { «x1» , «x2» } )
Q

Przykład: przykład

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

jest tłumaczony na

from * in (customers).
   SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

końcowe tłumaczenie, które jest

customers.
SelectMany(c => c.Orders, (c,o) => new { c, o }).
OrderByDescending(x => x.o.Total).
Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })

gdzie x jest identyfikatorem wygenerowanym przez kompilator, który jest w przeciwnym razie niewidoczny i niedostępny.

koniec przykładu

Wyrażenie let wraz z poprzednią klauzulą from:

from «x» in «e»  
let «y» = «f»  
...

jest tłumaczony na

from * in ( «e» ) . Select ( «x» => new { «x» , «y» = «f» } )  
...

Przykład: przykład

from o in orders
let t = o.Details.Sum(d => d.UnitPrice * d.Quantity)
where t >= 1000
select new { o.OrderID, Total = t }

jest tłumaczony na

from * in (orders).Select(
    o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
where t >= 1000
select new { o.OrderID, Total = t }

końcowe tłumaczenie, które jest

orders
    .Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
    .Where(x => x.t >= 1000)
    .Select(x => new { x.o.OrderID, Total = x.t })

gdzie x jest identyfikatorem wygenerowanym przez kompilator, który jest w przeciwnym razie niewidoczny i niedostępny.

koniec przykładu

Wyrażenie where wraz z poprzednią klauzulą from:

from «x» in «e»  
where «f»  
...

jest tłumaczony na

from «x» in ( «e» ) . Where ( «x» => «f» )  
...

Klauzula join, po której natychmiast następuje klauzula select

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2»  
select «v»

jest tłumaczony na

( «e1» ) . Join( «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «x2» ) => «v» )

Przykład: przykład

from c in customersh
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }

jest tłumaczony na

(customers).Join(
   orders,
   c => c.CustomerID, o => o.CustomerID,
   (c, o) => new { c.Name, o.OrderDate, o.Total })

koniec przykładu

Klauzula join, po której następuje klauzula treści zapytania:

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2»  
...

jest tłumaczony na

from * in ( «e1» ) . Join(  
«e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «x2» ) => new { «x1» , «x2» })  
...

Klauzula join-into, po której natychmiast następuje klauzula select

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into «g»  
select «v»

jest tłumaczony na

( «e1» ) . GroupJoin( «e2» , «x1» => «k1» , «x2» => «k2» ,
                     ( «x1» , «g» ) => «v» )

Klauzula join into, po której następuje klauzula treści zapytania

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into *g»  
...

jest tłumaczony na

from * in ( «e1» ) . GroupJoin(  
   «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «g» ) => new { «x1» , «g» })
...

Przykład: przykład

from c in customers
join o in orders on c.CustomerID equals o.CustomerID into co
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }

jest tłumaczony na

from * in (customers).GroupJoin(
    orders,
    c => c.CustomerID,
    o => o.CustomerID,
    (c, co) => new { c, co })
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }

końcowe tłumaczenie, które jest

customers
    .GroupJoin(
        orders,
        c => c.CustomerID,
        o => o.CustomerID,
        (c, co) => new { c, co })
    .Select(x => new { x, n = x.co.Count() })
    .Where(y => y.n >= 10)
    .Select(y => new { y.x.c.Name, OrderCount = y.n })

gdzie x i y to identyfikatory generowane przez kompilator, które w przeciwnym razie są niewidoczne i niedostępne.

koniec przykładu

Klauzula orderby i jej poprzednia klauzula from:

from «x» in «e»  
orderby «k1» , «k2» , ... , «kn»  
...

jest tłumaczony na

from «x» in ( «e» ) .
OrderBy ( «x» => «k1» ) .
ThenBy ( «x» => «k2» ) .
... .
ThenBy ( «x» => «kn» )
...

Jeśli klauzula ordering określa malejący wskaźnik kierunku, zamiast tego jest generowane wywołanie OrderByDescending lub ThenByDescending.

Przykład: przykład

from o in orders
orderby o.Customer.Name, o.Total descending
select o

ma ostateczne tłumaczenie

(orders)
    .OrderBy(o => o.Customer.Name)
    .ThenByDescending(o => o.Total)

koniec przykładu

W poniższych tłumaczeniach przyjęto założenie, że w każdym wyrażeniu zapytania nie ma klauzul let, where, join ani orderby ani więcej niż jedna klauzula początkowa from.

12.20.3.6 Klauzule wyboru

Wyrażenie zapytania formularza

from «x» in «e» select «v»

jest tłumaczony na

( «e» ) . Select ( «x» => «v» )

z wyjątkiem sytuacji, gdy «v» jest identyfikatorem «x», tłumaczenie jest po prostu

( «e» )

Przykład: przykład

from c in customers.Where(c => c.City == "London")
select c

jest po prostu przekładany na

(customers).Where(c => c.City == "London")

koniec przykładu

12.20.3.7 Klauzule grupy

Klauzula group

from «x» in «e» group «v» by «k»

jest tłumaczony na

( «e» ) . GroupBy ( «x» => «k» , «x» => «v» )

z wyjątkiem, gdy «v» jest identyfikatorem «x», tłumaczenie jest

( «e» ) . GroupBy ( «x» => «k» )

Przykład: przykład

from c in customers
group c.Name by c.Country

jest tłumaczony na

(customers).GroupBy(c => c.Country, c => c.Name)

koniec przykładu

12.20.3.8 Identyfikatory przezroczyste

Niektóre tłumaczenia wprowadzają zmienne zakresu z identyfikatorami przezroczystymi oznaczone przez *. Identyfikatory przezroczyste istnieją tylko jako krok pośredni w procesie tłumaczenia wyrażeń zapytania.

Gdy tłumaczenie zapytania wprowadza przezroczysty identyfikator, dalsze kroki tłumaczenia propagują przezroczysty identyfikator do funkcji anonimowych i inicjatorów obiektów anonimowych. W tych kontekstach identyfikatory przezroczyste mają następujące zachowanie:

  • Gdy przezroczysty identyfikator występuje jako parametr w funkcji anonimowej, elementy członkowskie skojarzonego typu anonimowego są automatycznie dostępne w treści funkcji anonimowej.
  • Gdy element członkowski z przezroczystym identyfikatorem jest w zasięgu, jego członkowie również są w zasięgu.
  • Gdy identyfikator przezroczysty występuje jako deklarator członka w inicjatorze obiektu anonimowego, dodaje członka z przezroczystym identyfikatorem.

W opisanych powyżej krokach tłumaczenia identyfikatory przezroczyste są zawsze wprowadzane razem z typami anonimowymi, z zamiarem przechwytywania wielu zmiennych zakresu jako elementów członkowskich pojedynczego obiektu. Implementacja języka C# może używać innego mechanizmu niż typy anonimowe do grupowania wielu zmiennych zakresu. W poniższych przykładach tłumaczenia przyjęto założenie, że są używane typy anonimowe i przedstawiono jedno możliwe tłumaczenie identyfikatorów przezroczystych.

Przykład: przykład

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.Total }

jest tłumaczony na

from * in (customers).SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.Total }

co jest dalej tłumaczone na

customers
    .SelectMany(c => c.Orders, (c,o) => new { c, o })
    .OrderByDescending(* => o.Total)
    .Select(\* => new { c.Name, o.Total })

które, gdy identyfikatory przezroczyste są wymazane, jest równoważne

customers
    .SelectMany(c => c.Orders, (c,o) => new { c, o })
    .OrderByDescending(x => x.o.Total)
    .Select(x => new { x.c.Name, x.o.Total })

gdzie x jest identyfikatorem wygenerowanym przez kompilator, który jest w przeciwnym razie niewidoczny i niedostępny.

Przykład

from c in customers
join o in orders on c.CustomerID equals o.CustomerID
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

jest tłumaczony na

from * in (customers).Join(
    orders,
    c => c.CustomerID,
    o => o.CustomerID,
    (c, o) => new { c, o })
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

które jest dodatkowo zredukowane do

customers
    .Join(orders, c => c.CustomerID,
        o => o.CustomerID, (c, o) => new { c, o })
    .Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d })
    .Join(products, * => d.ProductID, p => p.ProductID,
        (*, p) => new { c.Name, o.OrderDate, p.ProductName })

końcowe tłumaczenie, które jest

customers
    .Join(orders, c => c.CustomerID,
        o => o.CustomerID, (c, o) => new { c, o })
    .Join(details, x => x.o.OrderID, d => d.OrderID, (x, d) => new { x, d })
    .Join(products, y => y.d.ProductID, p => p.ProductID,
        (y, p) => new { y.x.c.Name, y.x.o.OrderDate, p.ProductName })

gdzie x i y są identyfikatorami generowanymi przez kompilator, które w przeciwnym razie są niewidoczne i niedostępne. koniec przykładu

12.20.4 Wzorzec wyrażenia zapytania

Wzorzec wyrażeń-kwerend ustanawia wzorzec metod, które typy mogą implementować w celu obsługi wyrażeń kwerendy.

Typ ogólny C<T> obsługuje wzorzec wyrażenia zapytania, jeśli jego publiczne metody składowe i metody rozszerzenia dostępne publicznie mogą zostać zastąpione przez następującą definicję klasy. Elementy członkowskie i dostępne metody rozszerzeń są określane jako "kształt" typu ogólnego C<T>. Typ ogólny jest używany w celu zilustrowania odpowiednich relacji między typami parametrów i zwracanych, ale istnieje możliwość zaimplementowania wzorca dla typów innych niż ogólne.

delegate R Func<T1,R>(T1 arg1);
delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);

class C
{
    public C<T> Cast<T>() { ... }
}

class C<T> : C
{
    public C<T> Where(Func<T,bool> predicate) { ... }
    public C<U> Select<U>(Func<T,U> selector) { ... }
    public C<V> SelectMany<U,V>(Func<T,C<U>> selector,
        Func<T,U,V> resultSelector) { ... }
    public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
        Func<U,K> innerKeySelector, Func<T,U,V> resultSelector) { ... }
    public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
        Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector) { ... }
    public O<T> OrderBy<K>(Func<T,K> keySelector) { ... }
    public O<T> OrderByDescending<K>(Func<T,K> keySelector) { ... }
    public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector) { ... }
    public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector,
        Func<T,E> elementSelector) { ... }
}

class O<T> : C<T>
{
    public O<T> ThenBy<K>(Func<T,K> keySelector) { ... }
    public O<T> ThenByDescending<K>(Func<T,K> keySelector) { ... }
}

class G<K,T> : C<T>
{
    public K Key { get; }
}

Powyższe metody używają typów delegatów ogólnych Func<T1, R> i Func<T1, T2, R>, ale mogą one równie dobrze używać innych typów delegatów lub drzewa wyrażeń z tymi samymi relacjami pomiędzy typami parametrów i wartości zwracanych.

Uwaga: Zalecana relacja między C<T> i O<T> gwarantuje, że metody ThenBy i ThenByDescending są dostępne tylko w wyniku OrderBy lub OrderByDescending. uwaga końcowa

Uwaga: Zalecany kształt wyniku GroupBy— sekwencja sekwencji, w której każda sekwencja wewnętrzna ma dodatkową właściwość Key. uwaga końcowa

Uwaga: ponieważ wyrażenia zapytania są tłumaczone na wywołania metod za pomocą mapowania składniowego, typy mają znaczną elastyczność w sposobie implementowania dowolnego lub całego wzorca wyrażenia zapytania. Na przykład metody wzorca można zaimplementować jako metody instancyjne lub metody rozszerzeń, ponieważ obie mają tę samą składnię wywołania. Metody te mogą przyjmować delegaty lub drzewa wyrażeń, ponieważ funkcje anonimowe mogą być przekształcane w oba. Typy implementujące tylko niektóre wzorce wyrażeń zapytania obsługują tylko tłumaczenia wyrażeń zapytań mapujące na obsługiwane przez ten typ metody. uwaga końcowa

Uwaga: Przestrzeń nazw System.Linq zapewnia implementację wzorca wyrażenia zapytania dla dowolnego typu, który implementuje interfejs System.Collections.Generic.IEnumerable<T>. uwaga końcowa

12.21 Operatory przypisania

12.21.1 Ogólne

Wszystkie, ale jeden z operatorów przypisania przypisuje nową wartość do zmiennej, właściwości, zdarzenia lub elementu indeksatora. Wyjątek, = ref, przypisuje referencję zmiennej (§9.5) zmiennej odniesienia (§9.7).

assignment
    : unary_expression assignment_operator expression
    ;

assignment_operator
    : '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<='
    | right_shift_assignment
    ;

Lewy operand przypisania powinno być wyrażeniem typu zmiennej lub, z wyjątkiem = ref, dostępem do właściwości, indeksatora, zdarzenia lub krotki. Wyrażenie deklaracji nie jest bezpośrednio dozwolone jako lewy operand, ale może wystąpić jako krok w procesie oceny przypisania dekonstrukcyjnego.

Operator = jest nazywany operatorem prostego przypisania . Przypisuje wartość lub wartości prawego operandu do zmiennej, właściwości, elementu indeksatora lub elementów krotki nadanych przez lewy operand. Lewy operand operatora przypisania prostego nie może być dostępem do zdarzenia (z wyjątkiem opisanym w §15.8.2). Prosty operator przypisania jest opisany w §12.21.2.

Operator = ref jest nazywany operatorem przypisania ref . Sprawia, że prawy operand, który powinien być variable_reference (§9.5), staje się deskryptorem zmiennej referencyjnej wskazanej przez lewy operand. Operator przypisania ref jest opisany w §12.21.3.

Operatory przypisania inne niż operatory = i = ref są nazywane operatorami przypisania złożonego . Te operatory wykonują wskazaną operację na dwóch operandach, a następnie przypisują wynikową wartość do zmiennej, właściwości lub elementu indeksatora podanego przez lewy operand. Operatory przypisania złożonego są opisane w §12.21.4.

Operatory += i -= z wyrażeniem dostępu do zdarzenia jako lewym operandem są nazywane operatorami przypisania zdarzenia . Żaden inny operator przypisania nie jest prawidłowy, gdy dostęp do zdarzenia jest lewym operatorem. Operatory przypisania zdarzeń są opisane w §12.21.5.

Operatory przypisania są skojarzone od prawej do lewej, co oznacza, że operacje są grupowane od prawej do lewej.

Przykład: wyrażenie w formie a = b = c jest obliczane jako a = (b = c). koniec przykładu

12.21.2 Proste przypisanie

Operator = jest nazywany operatorem prostego przypisania.

Jeśli lewy operand prostego przypisania ma postać E.P lub E[Ei] gdzie E ma typ czasu kompilacji dynamic, przypisanie jest dynamicznie powiązane (§12.3.3). W tym przypadku typ kompilacyjny wyrażenia przypisania to dynamic, a rozwiązanie opisane poniżej będzie miało miejsce podczas wykonywania programu w oparciu o typ wykonawczy E. Jeśli lewy operand ma postać E[Ei], w której co najmniej jeden element Ei ma typ czasu kompilacji dynamic, a typ czasu kompilacji E nie jest tablicą, wynikowy dostęp indeksatora jest dynamicznie powiązany, ale z ograniczonym sprawdzaniem czasu kompilacji (§12.6.5).

Proste przypisanie, w którym lewy operand jest klasyfikowany jako krotka, jest również nazywane przypisaniem dekonstrukcyjnym. Jeśli którykolwiek z elementów krotki lewego operandu ma nazwę elementu, wystąpi błąd czasu kompilacji. Jeśli którykolwiek z elementów krotki lewego operandu jest declaration_expression, a którykolwiek inny element nie jest declaration_expression lub prostym pominięciem, wystąpi błąd czasu kompilacji.

Typ zwykłego przypisania x = y jest typem przypisania xdo y, który jest określany rekursywnie w następujący sposób:

  • Jeśli x jest wyrażeniem krotki (x1, ..., xn), a y można zdekonstruować do wyrażenia krotki (y1, ..., yn) z n elementami (§12.7), i każde przypisanie do xiyi ma typ Ti, to przypisanie ma typ (T1, ..., Tn).
  • W przeciwnym razie jeśli x jest klasyfikowana jako zmienna, zmienna nie jest readonly, x ma typ T, a y ma niejawną konwersję na T, przypisanie ma typ T.
  • W przeciwnym razie, jeśli x jest klasyfikowaną jako niejawnie typizowaną zmienną (tj. niejawnie typizowanego wyrażenia deklaracji), a y ma typ T, to wnioskowany typ zmiennej to T, a przypisanie ma typ T.
  • W przeciwnym razie, jeśli x jest klasyfikowane jako dostęp do właściwości lub indeksatora, właściwość lub indeksator ma akcesor ustawiający, do którego można uzyskać dostęp, x ma typ T, a y ma niejawną konwersję na T, to przypisanie ma typ T.
  • W przeciwnym razie przypisanie jest nieprawidłowe i występuje błąd związany z czasem wiązania.

Przetwarzanie w czasie wykonywania prostego przypisania w postaci x = y z typem T odbywa się przez przypisanie do x, y z typem T, które składa się z następujących kroków rekurencyjnych:

  • x jest oceniana, jeśli jeszcze nie została oceniona.
  • Jeśli x jest klasyfikowana jako zmienna, y jest obliczana i w razie potrzeby przekonwertowana na T za pomocą niejawnej konwersji (§10.2).
    • Jeśli zmienna oznaczona przez x jest elementem tablicy typu reference_type, wykonywane jest sprawdzenie w czasie wykonywania, aby upewnić się, że wartość obliczona dla y jest zgodna z instancją tablicy, której x jest elementem. Sprawdzanie powiedzie się, jeśli y jest null, lub jeśli istnieje niejawna konwersja odwołania (§10.2.8) z typu wystąpienia, na które wskazuje y, do rzeczywistego typu elementu tablicy, zawierającej x. W przeciwnym razie zostanie zgłoszony System.ArrayTypeMismatchException.
    • Wartość wynikająca z ewaluacji i konwersji y jest przechowywana w lokalizacji wynikającej z ewaluacji xi jest zwracana jako wynik przypisania.
  • Jeśli x jest klasyfikowana jako właściwość lub dostęp indeksatora:
    • y jest obliczana i w razie potrzeby przekonwertowana na T za pomocą niejawnej konwersji (§10.2).
    • Akcesor ustawiający x jest wywoływany z wartością, którą uzyskuje się w wyniku oceny i konwersji y jako argument wartości.
    • Wartość wynikająca z oceny i konwersji y jest wynikiem przypisania.
  • Jeśli x jest klasyfikowany jako krotka (x1, ..., xn) o arności n:
    • y jest dekonstruktorowany elementami n do wyrażenia krotki e.
    • Krotka wynikowa t zostaje utworzona poprzez konwersję e na T przy użyciu niejawnej konwersji krotki.
    • dla każdego xi w kolejności od lewej do prawej wykonuje się przypisanie xi do t.Itemi, z wyjątkiem sytuacji, w której xi nie są ponownie oceniane.
    • t jest zwracany w wyniku przypisania.

Uwaga: jeśli typ czasu kompilacji x jest dynamic i istnieje niejawna konwersja typu czasu kompilacji y do dynamic, nie jest wymagane rozwiązanie środowiska uruchomieniowego. uwaga końcowa

Uwaga: Reguły współrzędności tablic (§17.6) pozwalają, aby wartość typu tablicowego A[] była odwołaniem do instancji typu tablicowego B[], pod warunkiem że istnieje niejawna konwersja odwołania z B do A. Ze względu na te reguły, przypisanie do elementu tablicy reference_type wymaga sprawdzenia w czasie wykonywania, aby upewnić się, że wartość, która jest przypisywana, jest zgodna z wystąpieniem tablicy. W przykładzie

string[] sa = new string[10];
object[] oa = sa;
oa[0] = null;              // OK
oa[1] = "Hello";           // OK
oa[2] = new ArrayList();   // ArrayTypeMismatchException

Ostatnie przypisanie powoduje rzucenie System.ArrayTypeMismatchException, ponieważ odwołanie do ArrayList nie może być przechowywane w elemencie string[].

uwaga końcowa

Gdy właściwość lub indeksator zadeklarowany w struct_type jest celem przypisania, wyrażenie wystąpienia skojarzone z właściwością lub dostępem indeksatora jest klasyfikowane jako zmienna. Jeśli wyrażenie wystąpienia jest uznawane za wartość, wystąpi błąd czasu wiązania.

Uwaga: z powodu §12.8.7ta sama reguła dotyczy również pól. uwaga końcowa

Przykładowy: biorąc pod uwagę deklaracje:

struct Point
{
   int x, y;

   public Point(int x, int y)
   {
      this.x = x;
      this.y = y;
   }

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

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

struct Rectangle
{
    Point a, b;

    public Rectangle(Point a, Point b)
    {
        this.a = a;
        this.b = b;
    }

    public Point A
    {
        get { return a; }
        set { a = value; }
    }

    public Point B
    {
        get { return b; }
        set { b = value; }
    }
}

w przykładzie

Point p = new Point();
p.X = 100;
p.Y = 100;
Rectangle r = new Rectangle();
r.A = new Point(10, 10);
r.B = p;

przypisania do p.X, p.Y, r.Ai r.B są dozwolone, ponieważ p i r są zmiennymi. Jednak w przykładzie

Rectangle r = new Rectangle();
r.A.X = 10;
r.A.Y = 10;
r.B.X = 100;
r.B.Y = 100;

wszystkie przypisania są nieprawidłowe, ponieważ r.A i r.B nie są zmiennymi.

koniec przykładu

12.21.3 Przydział referencji

Operator = ref jest znany jako operator przypisania ref.

Lewy operand jest wyrażeniem, które wiąże się ze zmienną referencyjną (§9.7), parametrem referencyjnym (innym niż this), parametrem wyjściowym lub parametrem wejściowym. Prawy operand powinien być wyrażeniem, które daje variable_reference określające wartość tego samego typu co lewy operand.

Jest to błąd czasu kompilacji, jeśli kontekst ref-safe-context (§9.7.2) lewego operanda jest szerszy niż kontekst ref-safe-context prawego operanda.

Prawy operand powinien być jednoznacznie przypisany w momencie przypisania przez odwołanie.

Gdy lewy operand wiąże się z parametrem wyjściowym, jest to błąd, jeśli ten parametr wyjściowy nie został zdecydowanie przypisany na początku operatora przypisania ref.

Jeśli lewy operand jest zapisywalnym odwołaniem (tj. wyznacza coś innego niż lokalny lub wejściowy parametr ref readonly), to prawy operand musi być zapisywalnym odwołaniem_do_zmiennej . Jeśli prawa zmienna operandu jest zapisywalna, lewy operand może być zapisywalny lub tylko do odczytu ref.

Operacja sprawia, że lewy operand staje się aliasem dla zmiennej określonej przez prawy operand. Alias może być tylko do odczytu, nawet jeśli właściwa zmienna operandu jest zapisywalna.

Operator przypisania ref zwraca odwołanie do zmiennej przypisanego typu. Jest zapisywalny, jeśli lewy operand jest zapisywalny.

Operator przypisania ref nie powinien odczytywać lokalizacji przechowywania, na które wskazuje prawy operand.

Przykład: oto kilka przykładów użycia = ref:

public static int M1() { ... }
public static ref int M2() { ... }
public static ref uint M2u() { ... }
public static ref readonly int M3() { ... }
public static void Test()
{
int v = 42;
ref int r1 = ref v; // OK, r1 refers to v, which has value 42
r1 = ref M1();      // Error; M1 returns a value, not a reference
r1 = ref M2();      // OK; makes an alias
r1 = ref M2u();     // Error; lhs and rhs have different types
r1 = ref M3();    // error; M3 returns a ref readonly, which r1 cannot honor
ref readonly int r2 = ref v; // OK; make readonly alias to ref
r2 = ref M2();      // OK; makes an alias, adding read-only protection
r2 = ref M3();      // OK; makes an alias and honors the read-only
r2 = ref (r1 = ref M2());  // OK; r1 is an alias to a writable variable,
              // r2 is an alias (with read-only access) to the same variable
}

koniec przykładu

Uwaga: podczas odczytywania kodu przy użyciu operatora = ref może być kuszące odczytanie części ref jako części operandu. Jest to szczególnie mylące, gdy operand jest wyrażeniem warunkowym ?:. Na przykład podczas odczytywania ref int a = ref b ? ref x : ref y; ważne jest, aby odczytywać to jako operator = ref, a b ? ref x : ref y jest właściwym operandem: ref int a = ref (b ? ref x : ref y);. Co ważne, wyrażenie ref b nie jest częścią tej instrukcji, mimo że może wydawać się tak na pierwszy rzut oka. uwaga końcowa

12.21.4 Przypisanie złożone

Jeśli lewy operand przypisania złożonego ma postać E.P lub E[Ei] gdzie E ma typ czasu kompilacji dynamic, przypisanie jest dynamicznie powiązane (§12.3.3). W tym przypadku typ kompilacyjny wyrażenia przypisania to dynamic, a rozwiązanie opisane poniżej będzie miało miejsce podczas wykonywania programu w oparciu o typ wykonawczy E. Jeśli lewy operand ma postać E[Ei], w której co najmniej jeden element Ei ma typ czasu kompilacji dynamic, a typ czasu kompilacji E nie jest tablicą, wynikowy dostęp indeksatora jest dynamicznie powiązany, ale z ograniczonym sprawdzaniem czasu kompilacji (§12.6.5).

Operacja w formie x «op»= y jest przetwarzana poprzez zastosowanie rozpoznawania przeciążenia operatora binarnego (§12.4.5), tak jakby operacja była zapisana jako x «op» y. Wtedy

  • Jeśli typ zwracany wybranego operatora jest niejawnie konwertowany na typ x, operacja jest obliczana jako x = x «op» y, z tą różnicą, że x jest obliczana tylko raz.
  • W przeciwnym razie, jeśli wybrany operator jest wstępnie zdefiniowanym operatorem, jeśli zwracany typ wybranego operatora jest jawnie konwertowalny na typ x, a jeśli y jest niejawnie konwertowalny na typ x lub operator jest operatorem przesunięcia, wtedy operacja jest oceniana jako x = (T)(x «op» y), gdzie T jest typem x, z wyjątkiem tego, że x jest obliczany tylko raz.
  • W przeciwnym razie przypisanie złożone jest nieprawidłowe i występuje błąd związany z czasem powiązania.

Termin "oceniane tylko raz" oznacza, że w ocenie x «op» ywyniki jakichkolwiek wyrażeń składowych x są tymczasowo zapisywane, a następnie ponownie używane podczas wykonywania przypisania do x.

Przykład: w przypisaniu A()[B()] += C(), gdzie A jest metodą zwracającą int[], a B i C są metodami zwracającymi int, metody są wywoływane tylko raz, w kolejności A, B, C. koniec przykładu

Gdy lewy operand przypisania złożonego jest dostępem do właściwości lub indeksatorem, właściwość lub indeksator musi mieć zarówno metodę dostępu get, jak i metodę dostępu set. Jeśli tak nie jest, wystąpi błąd powiązania czasowego.

Druga reguła powyżej pozwala na ocenę x «op»= y jako x = (T)(x «op» y) w niektórych kontekstach. Reguła istnieje tak, że wstępnie zdefiniowane operatory mogą być używane jako operatory złożone, gdy lewy operand jest typu sbyte, byte, short, ushortlub char. Nawet jeśli oba argumenty są jednym z tych typów, wstępnie zdefiniowane operatory generują wynik typu int, zgodnie z opisem w §12.4.7.3. W związku z tym bez rzutowania nie byłoby możliwe przypisanie wyniku do lewego operandu.

Intuicyjnym efektem reguły dla wstępnie zdefiniowanych operatorów jest po prostu to, że x «op»= y jest dozwolona, jeśli dozwolone są obie x «op» y i x = y.

Przykład: w poniższym kodzie

byte b = 0;
char ch = '\0';
int i = 0;
b += 1;           // OK
b += 1000;        // Error, b = 1000 not permitted
b += i;           // Error, b = i not permitted
b += (byte)i;     // OK
ch += 1;          // Error, ch = 1 not permitted
ch += (char)1;    // OK

Intuicyjnym powodem każdego błędu jest to, że podobne proste przypisanie byłoby również błędne.

koniec przykładu

Uwaga: To również oznacza, że operacje złożonego przypisania obsługują podniesione operatory. Ponieważ przypisanie złożone x «op»= y jest oceniane albo jako x = x «op» y, albo x = (T)(x «op» y), reguły oceny niejawnie obejmują podniesione operatory. uwaga końcowa

12.21.5 Przypisanie zdarzenia

Jeśli lewy operand operatora a += or -= jest klasyfikowany jako dostęp do zdarzeń, wyrażenie jest oceniane w następujący sposób:

  • Wyrażenie wystąpienia, jeśli istnieje, jest oceniane w dostępie do zdarzenia.
  • Prawy operand operatora += lub -= jest obliczany, a w razie potrzeby przekonwertowany na typ lewego operandu za pomocą konwersji niejawnej (§10.2).
  • Wywołano akcesor zdarzenia, przekazując listę argumentów składającą się z wartości obliczonej w poprzednim kroku. Jeśli operator został +=, wywoływana jest funkcja dodawania metody dostępu; jeśli operator został -=, wywoływana jest funkcja usuwania metody dostępu.

Wyrażenie przypisania zdarzenia nie zwraca wartości. W związku z tym wyrażenie przypisania zdarzeń jest prawidłowe tylko w kontekście statement_expression (§13.7).

Wyrażenie 12.22

Wyrażenie to wyrażenie bez przypisania lub przypisanie .

expression
    : non_assignment_expression
    | assignment
    ;

non_assignment_expression
    : declaration_expression
    | conditional_expression
    | lambda_expression
    | query_expression
    ;

12.23 Wyrażenia stałe

Wyrażenie stałe jest wyrażeniem, które jest w pełni oceniane w czasie kompilacji.

constant_expression
    : expression
    ;

Wyrażenie stałe powinno mieć wartość null lub jeden z następujących typów:

  • sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string;
  • typ wyliczenia; lub
  • wyrażenie wartości domyślnej (§12.8.21) dla typu odwołania.

Tylko następujące konstrukcje są dozwolone w wyrażeniach stałych:

  • Literały (w tym literał null).
  • Odwołania do const elementów typów danych klas i struktur.
  • Odwołania do elementów typów wyliczenia.
  • Odwołania do lokalnych stałych.
  • Wyrażenia w nawiasach, które są wyrażeniami stałymi.
  • Wyrażenia rzutowe.
  • wyrażenia checked i unchecked
  • nameof wyrażenia.
  • Wstępnie zdefiniowane +, -, ! (negacja logiczna) i operatory jednoargumentowe ~.
  • Wstępnie zdefiniowane +, -, *, /, %, <<, >>, &, |, ^, &&, ||, ==, !=, <, >, <=i >= operatory binarne.
  • Operator warunkowy ?:.
  • Operator ! forgiving o wartości null (§12.8.9).
  • sizeof wyrażenia, pod warunkiem, że niezarządzany typ jest jednym z typów określonych w §23.6.9, dla których sizeof zwraca stałą wartość.
  • Wyrażenia wartości domyślnej, pod warunkiem, że typ jest jednym z typów wymienionych powyżej.

Następujące konwersje są dozwolone w wyrażeniach stałych:

  • Konwersje tożsamości
  • Konwersje liczbowe
  • Konwersje wyliczenia
  • Konwersje wyrażeń stałych
  • Niejawne i jawne konwersje odwołań, pod warunkiem, że źródłem konwersji jest wyrażenie stałe, które oblicza wartość null.

Uwaga: Inne konwersje, w tym boxing, unboxing i niejawne konwersje odwołań wartości innych niżnull są niedozwolone w wyrażeniach stałych. uwaga końcowa

Przykład: w poniższym kodzie

class C
{
    const object i = 5;         // error: boxing conversion not permitted
    const object str = "hello"; // error: implicit reference conversion
}

inicjowanie i jest błędem, ponieważ wymagana jest konwersja boksu. Inicjowanie str jest błędem, ponieważ wymagana jest niejawna konwersja odwołania z wartości innej niżnull.

koniec przykładu

Zawsze, gdy wyrażenie spełnia wymagania wymienione powyżej, wyrażenie jest obliczane w czasie kompilacji. Jest to prawda, nawet jeśli wyrażenie jest podwyrażeniem większego procesu, który zawiera zmienne konstrukcje.

Obliczanie w czasie kompilacji wyrażeń stałych używa tych samych reguł co obliczanie w czasie wykonywania wyrażeń niestałych, z tą różnicą, że w przypadku, gdy ocena czasu wykonywania zgłosiła wyjątek, ocena czasu kompilacji powoduje wystąpienie błędu czasu kompilacji.

Jeśli wyrażenie stałe nie zostanie jawnie umieszczone w kontekście unchecked, przepełnienia występujące w operacjach arytmetycznych typu całkowitego i konwersji podczas obliczania wyrażeń w czasie kompilacji zawsze powodują błędy czasu kompilacji (§12.8.20).

Wyrażenia stałe są wymagane w kontekstach wymienionych poniżej i jest to wskazane w gramatyce przy użyciu constant_expression. W tych kontekstach występuje błąd czasu kompilacji, jeśli wyrażenie nie może być w pełni ocenione w czasie kompilacji.

  • Deklaracje stałe (§15.4)
  • Deklaracje elementów wyliczenia (§19.4)
  • Domyślne argumenty list parametrów (§15.6.2)
  • case etykiety instrukcji switch (§13.8.3).
  • oświadczenia goto case (§13.10.4)
  • Długości wymiarów w wyrażeniu tworzenia tablicy (§12.8.17.5), które zawiera inicjalizator.
  • Atrybuty (§22)
  • W constant_pattern (§11.2.3)

Niejawna konwersja wyrażenia stałego (§10.2.11) zezwala na konwersję wyrażenia stałego typu int na sbyte, byte, short, ushort, uintlub ulong, pod warunkiem, że wartość wyrażenia stałego mieści się w zakresie typu docelowego.

12.24 Wyrażenia logiczne

boolean_expression to wyrażenie, które daje wynik typu bool; bezpośrednio lub poprzez zastosowanie operator true w określonych kontekstach, jak opisano poniżej:

boolean_expression
    : expression
    ;

Kontrolowanie wyrażenia warunkowego if_statement (§13.8.2), while_statement (§13.9.2), do_statement(§13.9.3) lub for_statement (§13.9.4) jest boolean_expression. Wyrażenie warunkowe sterujące operatora ?: (§12.18) podlega tym samym regułom co boolean_expression, ale z powodu kolejności operatorów jest klasyfikowane jako null_coalescing_expression.

Aby można było utworzyć wartość typu , wymagany jest Ebool w następujący sposób:

  • Jeśli E jest niejawnie konwertowany na bool, wówczas w czasie wykonywania jest stosowana niejawna konwersja.
  • Alternatywnie, rozpoznawanie przeciążenia operatora jednoargumentowego (§12.4.4) jest używane do znalezienia unikalnej, najlepszej implementacji operator true na E, a ta implementacja jest stosowana w czasie wykonywania.
  • Jeśli taki operator nie zostanie znaleziony, wystąpi błąd czasu powiązania.