Udostępnij za pośrednictwem


6 Struktura leksykalna

6.1 Programy

Program w języku C# składa się z co najmniej jednego pliku źródłowego, znanego formalnie jako jednostki kompilacji (§14.2). Chociaż jednostka kompilacji może mieć korespondencję jeden do jednego z plikiem w systemie plików, taka korespondencja nie jest wymagana.

Mówiąc koncepcyjnie, program jest kompilowany przy użyciu trzech kroków:

  1. Przekształcenie, które konwertuje plik z określonego repertuaru znaków i schematu kodowania na sekwencję znaków Unicode.
  2. Analiza leksykalna, która tłumaczy strumień znaków wejściowych Unicode na strumień tokenów.
  3. Analiza składniowa, która tłumaczy strumień tokenów na kod wykonywalny.

Zgodne implementacje akceptują jednostki kompilacji Unicode zakodowane przy użyciu formularza kodowania UTF-8 (zgodnie z definicją standardu Unicode) i przekształcają je w sekwencję znaków Unicode. Implementacje mogą akceptować i przekształcać dodatkowe schematy kodowania znaków (takie jak UTF-16, UTF-32 lub mapowania znaków innych niż Unicode).

Uwaga: obsługa znaku NULL Unicode (U+0000) jest zdefiniowana przez implementację. Zdecydowanie zaleca się, aby deweloperzy unikali używania tego znaku w kodzie źródłowym ze względu na przenośność i czytelność. Jeśli znak jest wymagany w literałach znaków lub ciągów, można użyć jednej lub obu sekwencji ucieczki \0 lub \u0000. notatka końcowa

Uwaga: Wykracza poza zakres tej specyfikacji, aby zdefiniować sposób przekształcania pliku przy użyciu reprezentacji znaków innych niż Unicode w sekwencję znaków Unicode. Podczas takiej transformacji zaleca się jednak, aby zwykły znak rozdzielania wierszy (lub sekwencja) w innym zestawie znaków został przetłumaczony na dwuznakową sekwencję składającą się z znaku powrotu karetki Unicode (U+000D), po którym następuje znak kanału liniowego Unicode (U+000A). W przeważającej części ta transformacja nie będzie miała widocznych efektów; jednak będzie to miało wpływ na interpretację tokenów ciągów znaków dosłownych (§6.4.5.6). Celem tego zalecenia jest umożliwienie dosłownego literału tekstowego utworzenia tej samej sekwencji znaków, gdy jednostka kompilacji kodu jest przenoszona między systemami obsługującymi różne zestawy znaków inne niż Unicode, szczególnie te, które używają różnych sekwencji znaków do oddzielania wierszy. notatka końcowa

6.2 Gramatyki

6.2.1 Ogólne

Ta specyfikacja przedstawia składnię języka programowania C# przy użyciu dwóch gramatyki. Gramatyka leksykalna (§6.2.3) definiuje sposób łączenia znaków Unicode z terminatorami wierszy, odstępami, komentarzami, tokenami i dyrektywami przetwarzania wstępnego. Gramatyka składniowa (§6.2.4) definiuje sposób łączenia tokenów wynikających z gramatyki leksykalnej w celu utworzenia programów w języku C#.

Wszystkie znaki terminalu należy rozumieć jako odpowiedni znak Unicode z zakresu U+0020 do U+007F, w przeciwieństwie do dowolnych podobnych znaków z innych zakresów znaków Unicode.

6.2.2 Notacja gramatyki

Gramatyki leksykalne i składniowe są przedstawione w formie rozszerzonej Backusa-Naura narzędzia gramatyki ANTLR.

Chociaż jest używana notacja ANTLR, ta specyfikacja nie przedstawia kompletnej "gramatyki referencyjnej" dla języka C#; pisanie leksykatora i analizatora, ręcznie lub przy użyciu narzędzia takiego jak ANTLR, wykracza poza zakres specyfikacji języka. Mając to na uwadze, ta specyfikacja stara się zminimalizować lukę między określoną w specyfikacji gramatyką a wymaganą do utworzenia leksykatora i analizatora w ANTLR.

ANTLR rozróżnia leksykę od składni, nazywając te drugie analizatorami, w swojej notacji przez rozpoczynanie reguł leksykalnych wielką literą, a reguł analizatora małą literą.

Uwaga: Gramatyka leksykalna języka C# (§6.2.3) i gramatyka składniowa (§6.2.4) nie są dokładnie zgodne z podziałem ANTLR na grammery leksykalne i analizatora. Ta mała niezgodność oznacza, że niektóre reguły analizatora ANTLR są używane podczas określania gramatyki leksykalnej języka C#. notatka końcowa

6.2.3 Gramatyka leksykalna

Gramatyka leksykalna języka C# jest przedstawiona w §6.3, §6.4 i §6.5. Symbole końcowe gramatyki leksykalnej są znakami zestawu znaków Unicode, a gramatyka leksykalna określa, jak znaki są łączone w tokeny (§6.4), białe znaki (§6.3.4), komentarze (§6.3.3) i dyrektywy przetwarzania wstępnego (§6.5).

Wiele symboli terminalnych gramatyki składni nie jest jawnie zdefiniowane jako tokeny w obrębie gramatyki leksykalnej. Raczej wykorzystuje się zachowanie ANTLR, zgodnie z którym literały w gramatyce są traktowane jako ukryte tokeny leksykalne. To pozwala, aby słowa kluczowe, operatory itp. były reprezentowane w gramatyce przez swoje literały, a nie nazwy tokenów.

Każda jednostka kompilacji w programie C# jest zgodna z produkcją danych wejściowych gramatyki leksykalnej (§6.3.1).

6.2.4 Gramatyka składniowa

Gramatyka składni języka C# jest przedstawiona w klauzulach, podklauzulach i załącznikach, które następują po tej podklauzuli. Symbole terminalne gramatyki syntaktycznej to tokeny zdefiniowane jawnie przez gramatykę leksykalną i niejawnie przez literały w samej gramatyce (§6.2.3). Gramatyka składniowa określa sposób łączenia tokenów w celu tworzenia programów w języku C#.

Każda jednostka kompilacji w programie C# musi być zgodna z produkcją compilation_unit (§14.2) gramatyki składniowej.

6.2.5 Niejednoznaczności gramatyczne

Produkcje dla:

("niejednoznaczne produkcje") może spowodować powstanie niejednoznaczności w gramatyce wyrażeń.

Te produkcje występują w kontekstach, w których wartość może wystąpić w wyrażeniu i mają co najmniej jedną alternatywę, która kończy się gramatyką "identifier type_argument_list?". Jest to opcjonalna lista_argumentów_typu , która skutkuje możliwą niejednoznacznością.

Przykład: oświadczenie:

F(G<A, B>(7));

można to interpretować jako wywołanie F z dwoma argumentami, G < A i B > (7). Alternatywnie można go interpretować jako wywołanie F z jednym argumentem, który jest wywołaniem metody G ogólnej z dwoma argumentami typu i jednym zwykłym argumentem.

przykład końcowy

Jeśli sekwencja tokenów może być analizowana w kontekście, jako jedna z jednoznacznych produkcji, w tym opcjonalny type_argument_list (§8.4.2), token zamykający > należy zbadać i jeśli jest on:

  • jeden z ( ) ] } : ; , . ? == != | ^ && || & [; lub
  • jeden z operatorów relacyjnych < <= >= is as; lub
  • kontekstowe słowo kluczowe zapytania pojawiające się w wyrażeniu zapytania.

następnie type_argument_list zostaną przechowane jako część jednoznacznej produkcji, a wszelkie inne możliwe analizy sekwencji tokenów zostaną odrzucone. W przeciwnym razie tokeny analizowane jako type_argument_list nie są uważane za część niejednoznacznej produkcji, nawet jeśli nie ma innej możliwej analizy tych tokenów.

Uwaga: te zasady uściślania nie są stosowane podczas analizowania innych produkcji, nawet jeśli podobnie kończą się w ciągu "identifier type_argument_list?"; takie produkcje są analizowane w normalny sposób. Przykłady to: namespace_or_type_name (§7.8); named_entity (§12.8.23); null_conditional_projection_initializer (§12.8.8); i qualified_alias_member (§14.8.1). notatka końcowa

Przykład: stwierdzenie:

F(G<A, B>(7));

zgodnie z tą regułą zostanie zinterpretowane jako wywołanie F z jednym argumentem, który jest wywołaniem metody ogólnej G z dwoma argumentami typu i jednym zwykłym argumentem. Oświadczenia

F(G<A, B>7);
F(G<A, B>>7);

każda z nich będzie interpretowana jako wywołanie metody F z dwoma argumentami. Oświadczenie

x = F<A> + y;

będzie interpretowany jako operator mniejszości, operator większości oraz operator jednoargumentowy plus, tak jakby instrukcja została napisana x = (F < A) > (+y), a nie jako simple_name z type_argument_list a następnie operatorem binarnym plus. W oświadczeniu

x = y is C<T> && z;

tokeny C<T> są interpretowane jako namespace_or_type_name z type_argument_list ze względu na obecność rozstrzygającego tokenu && po type_argument_list.

Wyrażenie (A < B, C > D) jest krotką z dwoma elementami, z których każdy jest porównaniem.

Wyrażenie (A<B,C> D, E) jest krotką z dwoma elementami, z których pierwszy jest wyrażeniem deklaracji.

Wywołanie M(A < B, C > D, E) ma trzy argumenty.

Wywołanie M(out A<B,C> D, E) ma dwa argumenty, z których pierwsza jest deklaracją out .

e is A<B> C Wyrażenie używa wzorca deklaracji.

Etykieta case A<B> C: case używa wzorca deklaracji.

przykład końcowy

W przypadku rozpoznania relational_expression (§12.12.1), jeśli zarówno opcja „relational_expressionistypu” jak i opcja „relational_expressioniswzorca” są możliwe do zastosowania, a typ jest uznawany za dostępny, wtedy należy wybrać alternatywę „relational_expressionistypu”.

6.3 Analiza leksykalna

6.3.1 Ogólne

Dla wygody gramatyka leksykalna definiuje i odwołuje się do następujących nazwanych tokenów leksykalnych:

DEFAULT  : 'default' ;
NULL     : 'null' ;
TRUE     : 'true' ;
FALSE    : 'false' ;
ASTERISK : '*' ;
SLASH    : '/' ;

Chociaż są to reguły leksykatora, te nazwy są pisane we wszystkich wielkich literach, aby odróżnić je od zwykłych nazw reguł leksykatora.

Uwaga: Te reguły wygody są wyjątkami od zwykłej praktyki nie podawania jawnych nazw tokenów dla tokenów zdefiniowanych przez ciągi literału. notatka końcowa

Produkcja wejściowa definiuje strukturę leksykalną jednostki kompilacji języka C#.

input
    : input_section?
    ;

input_section
    : input_section_part+
    ;

input_section_part
    : input_element* New_Line
    | PP_Directive
    ;

input_element
    : Whitespace
    | Comment
    | token
    ;

Uwaga: Powyższa gramatyka jest opisana przez reguły analizowania ANTLR, definiuje strukturę leksykatyczną jednostki kompilacji języka C#, a nie tokeny leksykalne. notatka końcowa

Pięć podstawowych elementów składa się na strukturę leksykalną jednostki kompilacji C#: terminatory wierszy (§6.3.2), białe znaki (§6.3.4), komentarze (§6.3.3), tokeny (§6.4) i dyrektywy przetwarzania wstępnego (§6.5). Spośród tych podstawowych elementów tylko tokeny są istotne w gramatyki składni programu C# (§6.2.4).

Przetwarzanie leksykalne jednostki kompilacji języka C# polega na zmniejszeniu pliku do sekwencji tokenów, która staje się danymi wejściowymi analizy składniowej. Terminatory wierszy, białe znaki i komentarze mogą służyć do oddzielania tokenów, a dyrektywy przetwarzania wstępnego mogą powodować pomijanie sekcji jednostki kompilacji, ale w przeciwnym razie te elementy leksykalne nie mają wpływu na strukturę składniową programu języka C#.

Gdy kilka produkcji gramatyki leksykalnej pasuje do sekwencji znaków w jednostce kompilacji, przetwarzanie leksykalne zawsze tworzy najdłuższy możliwy element leksykacyjny.

Przykład: Sekwencja // znaków jest przetwarzana jako początek komentarza jednowierszowego, ponieważ ten element leksyktyczny jest dłuższy niż pojedynczy / token. przykład końcowy

Niektóre tokeny są definiowane przez zestaw reguł leksykalnych; główna reguła i co najmniej jedna reguła podrzędna. Te ostatnie są oznaczone w gramatyce fragment, aby wskazać, że reguła definiuje część innego tokenu. Reguły fragmentów nie są uwzględniane w kolejności od góry do dołu reguł leksykalnych.

Uwaga: W ANTLR fragment jest słowem kluczowym, które generuje to samo zachowanie zdefiniowane tutaj. notatka końcowa

6.3.2 Terminatory wierszy

Terminatory wierszy dzielą znaki jednostki kompilacji języka C# na wiersze.

New_Line
    : New_Line_Character
    | '\u000D\u000A'    // carriage return, line feed
    ;

Aby zapewnić zgodność z narzędziami do edycji kodu źródłowego, które dodają znaczniki końca pliku, oraz umożliwić traktowanie jednostki kompilacji jako sekwencję prawidłowo zakończonych wierszy, następujące przekształcenia są stosowane w określonej kolejności do każdej jednostki kompilacji w programie C#:

  • Jeśli ostatnim znakiem jednostki kompilacji jest znak Control-Z (U+001A), ten znak zostanie usunięty.
  • Znak powrotu karetki (U+000D) jest dodawany na końcu jednostki kompilacji, jeśli ta jednostka kompilacji jest niepusta i jeśli ostatni znak jednostki kompilacji nie jest znakiem powrotu karetki (U+000D), kanałem informacyjnym wiersza (U+000A), znakiem następnego wiersza (U+0085), separatorem wiersza (U+2028) lub separatorem akapitu (U+2029).

Uwaga: Dodatkowy zwrot karetki pozwala programowi zakończyć się w PP_Directive (§6.5), który nie ma zakończenia New_Line. notatka końcowa

Komentarze 6.3.3

Obsługiwane są dwie formy komentarzy: komentarze rozdzielane i komentarze jednowierszowe.

Rozdzielany komentarz zaczyna się od znaków /* i kończy się znakami */. Komentarze rozdzielone mogą zajmować część wiersza, pojedynczego wiersza lub wielu wierszy.

Przykład: przykład

/* Hello, world program
   This program writes "hello, world" to the console
*/
class Hello
{
    static void Main()
    {
        System.Console.WriteLine("hello, world");
    }
}

zawiera ograniczony komentarz.

przykład końcowy

Komentarz jednowierszowy zaczyna się od znaków // i rozciąga się na koniec wiersza.

Przykład: przykład

// Hello, world program
// This program writes "hello, world" to the console
//
class Hello // any name will do for this class
{
    static void Main() // this method must be named "Main"
    {
        System.Console.WriteLine("hello, world");
    }
}

pokazuje kilka komentarzy jednowierszowych.

przykład końcowy

Comment
    : Single_Line_Comment
    | Delimited_Comment
    ;

fragment Single_Line_Comment
    : '//' Input_Character*
    ;

fragment Input_Character
    // anything but New_Line_Character
    : ~('\u000D' | '\u000A'   | '\u0085' | '\u2028' | '\u2029')
    ;

fragment New_Line_Character
    : '\u000D'  // carriage return
    | '\u000A'  // line feed
    | '\u0085'  // next line
    | '\u2028'  // line separator
    | '\u2029'  // paragraph separator
    ;

fragment Delimited_Comment
    : '/*' Delimited_Comment_Section* ASTERISK+ '/'
    ;

fragment Delimited_Comment_Section
    : SLASH
    | ASTERISK* Not_Slash_Or_Asterisk
    ;

fragment Not_Slash_Or_Asterisk
    : ~('/' | '*')    // Any except SLASH or ASTERISK
    ;

Komentarze nie są zagnieżdżone. Sekwencje znaków /* i */ nie mają specjalnego znaczenia w komentarzu jednowierszowym, a sekwencje znaków // i /* nie mają specjalnego znaczenia w komentarzu z ogranicznikami.

Komentarze nie są przetwarzane w literałach znakowych i tekstowych.

Uwaga: te reguły muszą być dokładnie interpretowane. Na przykład, w poniższym przykładzie, rozdzielany komentarz, który rozpoczyna się przed A, kończy się pomiędzy B i C(). Przyczyną jest to, że

// B */ C();

nie jest w rzeczywistości komentarzem jednowierszowym, ponieważ // nie ma specjalnego znaczenia w ograniczonym komentarzu, dlatego */ ma swoje zwykłe specjalne znaczenie w tym wierszu.

Podobnie, wydzielony komentarz rozpoczynający się przed D kończy się przed E. Przyczyną jest to, że "D */ " w rzeczywistości nie jest literałem ciągu, ponieważ początkowy znak podwójnego cudzysłowu pojawia się wewnątrz ograniczonego komentarza.

Przydatną konsekwencją faktu, że /* i */ nie mają specjalnego znaczenia w komentarzu jednowierszowym, jest możliwość zakomentowania bloku wierszy kodu źródłowego poprzez umieszczenie // na początku każdego wiersza. Ogólnie rzecz biorąc, umieszczenie /* przed tymi wierszami i */ po nich nie działa poprawnie, ponieważ nie obejmuje właściwie komentarzy w bloku i ogólnie może całkowicie zmienić strukturę takich odgraniczonych komentarzy.

Przykładowy kod:

static void Main()
{
    /* A
    // B */ C();
    Console.WriteLine(/* "D */ "E");
}

notatka końcowa

Jednolinijkowe_komentarze i Komentarze_ograniczone o określonych formatach mogą być używane jako komentarze dokumentacyjne, zgodnie z opisem w §D.

6.3.4 Biała spacja

Odstęp jest definiowany jako każdy znak należący do klasy Unicode Zs (w tym znak spacji), a także znak tabulatora poziomego, znak tabulatora pionowego i znak przejścia na nową stronę.

Whitespace
    : [\p{Zs}]  // any character with Unicode class Zs
    | '\u0009'  // horizontal tab
    | '\u000B'  // vertical tab
    | '\u000C'  // form feed
    ;

6.4 Tokeny

6.4.1 Ogólne

Istnieje kilka rodzajów tokenów: identyfikatory, słowa kluczowe, literały, operatory i znaki interpunkcyjne. Białe znaki i komentarze nie są tokenami, choć działają jako separatory tokenów.

token
    : identifier
    | keyword
    | Integer_Literal
    | Real_Literal
    | Character_Literal
    | String_Literal
    | operator_or_punctuator
    ;

Uwaga: jest to reguła analizatora ANTLR, która nie definiuje tokenu leksyktycznego, ale raczej kolekcji rodzajów tokenów. notatka końcowa

6.4.2 Sekwencje ucieczki znaków Unicode

Sekwencja ucieczki Unicode reprezentuje punkt kodu Unicode. Sekwencje ucieczki Unicode są przetwarzane w identyfikatorach (§6.4.3), literałach znaków (§6.4.5.5), zwykłych literałach ciągu (§6.4.5.6) i interpolowanych wyrażeniach ciągów regularnych (§12.8.3). Sekwencja ucieczki Unicode nie jest przetwarzana w żadnej innej lokalizacji (na przykład w celu utworzenia operatora, znaku interpunkcyjnego lub słowa kluczowego).

fragment Unicode_Escape_Sequence
    : '\\u' Hex_Digit Hex_Digit Hex_Digit Hex_Digit
    | '\\U' Hex_Digit Hex_Digit Hex_Digit Hex_Digit
            Hex_Digit Hex_Digit Hex_Digit Hex_Digit
    ;

Sekwencja ucieczki znaków Unicode reprezentuje pojedynczy punkt kodu Unicode utworzony przez liczbę szesnastkową po znakach "\u" lub "\U". Ponieważ język C# używa 16-bitowego kodowania punktów kodu Unicode w wartościach znaków i ciągów, punkt kodu Unicode w zakresie U+10000U+10FFFF jest reprezentowany przy użyciu dwóch jednostek kodu zastępczego Unicode. Punkty kodu Unicode powyżej U+FFFF nie są dozwolone w literałach znaków. Powyższe U+10FFFF punkty kodu Unicode są nieprawidłowe i nie są obsługiwane.

Nie przeprowadza się wielu tłumaczeń. Na przykład literał ciągu "\u005Cu005C" jest odpowiednikiem "\u005C", a nie "\".

Uwaga: Wartość \u005C Unicode jest znakiem "\". notatka końcowa

Przykład: przykład

class Class1
{
    static void Test(bool \u0066)
    {
        char c = '\u0066';
        if (\u0066)
        {
            System.Console.WriteLine(c.ToString());
        }
    }
}

pokazuje kilka zastosowań \u0066, które są sekwencją wyjścia dla litery „f”. Program jest odpowiednikiem

class Class1
{
    static void Test(bool f)
    {
        char c = 'f';
        if (f)
        {
            System.Console.WriteLine(c.ToString());
        }
    }
}

przykład końcowy

6.4.3 Identyfikatory

Reguły dotyczące identyfikatorów podanych w tym podklasie odpowiadają dokładnie tym, które są zalecane przez załącznik Standard Unicode 15 z wyjątkiem tego, że podkreślenie jest dozwolone jako znak początkowy (podobnie jak w języku programowania C), sekwencje ucieczki Unicode są dozwolone w identyfikatorach, a znak "@" jest dozwolony jako prefiks, aby umożliwić używanie słów kluczowych jako identyfikatorów.

identifier
    : Simple_Identifier
    | contextual_keyword
    ;

Simple_Identifier
    : Available_Identifier
    | Escaped_Identifier
    ;

fragment Available_Identifier
    // excluding keywords or contextual keywords, see note below
    : Basic_Identifier
    ;

fragment Escaped_Identifier
    // Includes keywords and contextual keywords prefixed by '@'.
    // See note below.
    : '@' Basic_Identifier
    ;

fragment Basic_Identifier
    : Identifier_Start_Character Identifier_Part_Character*
    ;

fragment Identifier_Start_Character
    : Letter_Character
    | Underscore_Character
    ;

fragment Underscore_Character
    : '_'               // underscore
    | '\\u005' [fF]     // Unicode_Escape_Sequence for underscore
    | '\\U0000005' [fF] // Unicode_Escape_Sequence for underscore
    ;

fragment Identifier_Part_Character
    : Letter_Character
    | Decimal_Digit_Character
    | Connecting_Character
    | Combining_Character
    | Formatting_Character
    ;

fragment Letter_Character
    // Category Letter, all subcategories; category Number, subcategory letter.
    : [\p{L}\p{Nl}]
    // Only escapes for categories L & Nl allowed. See note below.
    | Unicode_Escape_Sequence
    ;

fragment Combining_Character
    // Category Mark, subcategories non-spacing and spacing combining.
    : [\p{Mn}\p{Mc}]
    // Only escapes for categories Mn & Mc allowed. See note below.
    | Unicode_Escape_Sequence
    ;

fragment Decimal_Digit_Character
    // Category Number, subcategory decimal digit.
    : [\p{Nd}]
    // Only escapes for category Nd allowed. See note below.
    | Unicode_Escape_Sequence
    ;

fragment Connecting_Character
    // Category Punctuation, subcategory connector.
    : [\p{Pc}]
    // Only escapes for category Pc allowed. See note below.
    | Unicode_Escape_Sequence
    ;

fragment Formatting_Character
    // Category Other, subcategory format.
    : [\p{Cf}]
    // Only escapes for category Cf allowed, see note below.
    | Unicode_Escape_Sequence
    ;

Uwaga:

  • Aby uzyskać informacje na temat klas znaków Unicode wymienionych powyżej, zobacz Standard Unicode.
  • Fragment Available_Identifier wymaga wykluczenia słów kluczowych i słów kluczowych kontekstowych. Jeśli gramatyka w tej specyfikacji jest przetwarzana przez ANTLR, to wykluczenie jest obsługiwane automatycznie przez semantykę ANTLR.
    • Słowa kluczowe i kontekstowe słowa kluczowe występują w gramatyce jako dosłowne łańcuchy znaków.
    • Narzędzie ANTLR tworzy niejawne reguły tokenów leksykalnych z tych ciągów znaków.
    • ANTLR uwzględnia te niejawne reguły przed jawnymi regułami leksykalnymi w gramatyce.
    • W związku z tym fragment Available_Identifier nie będzie pasować do słów kluczowych ani słów kluczowych kontekstowych, ponieważ reguły leksykalne dla tych słów je poprzedzają.
  • Fragment Escaped_Identifier zawiera wyrażenia ucieczki i kontekstowe słowa kluczowe, ponieważ są one częścią dłuższego tokenu, rozpoczynając się od @, a przetwarzanie leksykalne zawsze tworzy najdłuższy możliwy element leksykalny (§6.3.1).
  • W jaki sposób implementacja wymusza ograniczenia dotyczące dozwolonych wartości Unicode_Escape_Sequence jest problemem z implementacją.

notatka końcowa

Przykład: Przykłady prawidłowych identyfikatorów to identifier1, _identifier2i @if. przykład końcowy

Identyfikator w programie zgodnym ma format kanoniczny zdefiniowany przez formularz normalizacji Unicode C, zgodnie z definicją w standardzie Unicode Załącznik 15. Zachowanie podczas napotkania identyfikatora, który nie znajduje się w formularzu normalizacji C, jest definiowane przez implementację; jednak diagnostyka nie jest wymagana.

Prefiks "@" umożliwia używanie słów kluczowych jako identyfikatorów, co jest przydatne podczas łączenia się z innymi językami programowania. Znak @ nie jest faktycznie częścią identyfikatora, więc identyfikator może być widoczny w innych językach jako normalny identyfikator, bez prefiksu. Identyfikator z prefiksem @ jest nazywany identyfikatorem dosłownym.

Uwaga: używanie prefiksu @ dla identyfikatorów, które nie są słowami kluczowymi, jest dozwolone, ale zdecydowanie zniechęcane jako kwestia stylu. notatka końcowa

Przykład: przykład:

class @class
{
    public static void @static(bool @bool)
    {
        if (@bool)
        {
            System.Console.WriteLine("true");
        }
        else
        {
            System.Console.WriteLine("false");
        }
    }
}

class Class1
{
    static void M()
    {
        cl\u0061ss.st\u0061tic(true);
    }
}

definiuje klasę o nazwie "class" ze statyczną metodą o nazwie "static", która przyjmuje parametr o nazwie "bool". Należy pamiętać, że ponieważ znaki ucieczki Unicode nie są dozwolone w słowach kluczowych, token "cl\u0061ss" jest identyfikatorem i jest tym samym identyfikatorem co "@class".

przykład końcowy

Dwa identyfikatory są traktowane tak samo, jeśli są identyczne po zastosowaniu następujących przekształceń:

  • Prefiks "@", jeśli jest używany, jest usuwany.
  • Każda Unicode_Escape_Sequence jest przekształcana na odpowiadający znak Unicode.
  • Wszystkie Formatting_Character są usuwane.

Semantyka identyfikatora o nazwie _ zależy od kontekstu, w którym się pojawia:

  • Może on oznaczać nazwany element programu, taki jak zmienna, klasa lub metoda lub
  • Może to oznaczać odrzucenie (§9.2.9.2).

Identyfikatory zawierające dwa kolejne znaki podkreślenia (U+005F) są zarezerwowane do użytku przez implementację; jednak nie jest wymagana żadna diagnostyka, jeśli taki identyfikator jest zdefiniowany.

Uwaga: na przykład implementacja może zawierać rozszerzone słowa kluczowe rozpoczynające się od dwóch podkreśleń. notatka końcowa

Słowa kluczowe 6.4.4

Słowo kluczowe jest sekwencją podobną do identyfikatora znaków, która jest zarezerwowana i nie może być używana jako identyfikator, z wyjątkiem sytuacji, gdy znak jest poprzedzony znakiem @ .

keyword
    : 'abstract' | 'as'       | 'base'       | 'bool'      | 'break'
    | 'byte'     | 'case'     | 'catch'      | 'char'      | 'checked'
    | 'class'    | 'const'    | 'continue'   | 'decimal'   | DEFAULT
    | 'delegate' | 'do'       | 'double'     | 'else'      | 'enum'
    | 'event'    | 'explicit' | 'extern'     | FALSE       | 'finally'
    | 'fixed'    | 'float'    | 'for'        | 'foreach'   | 'goto'
    | 'if'       | 'implicit' | 'in'         | 'int'       | 'interface'
    | 'internal' | 'is'       | 'lock'       | 'long'      | 'namespace'
    | 'new'      | NULL       | 'object'     | 'operator'  | 'out'
    | 'override' | 'params'   | 'private'    | 'protected' | 'public'
    | 'readonly' | 'ref'      | 'return'     | 'sbyte'     | 'sealed'
    | 'short'    | 'sizeof'   | 'stackalloc' | 'static'    | 'string'
    | 'struct'   | 'switch'   | 'this'       | 'throw'     | TRUE
    | 'try'      | 'typeof'   | 'uint'       | 'ulong'     | 'unchecked'
    | 'unsafe'   | 'ushort'   | 'using'      | 'virtual'   | 'void'
    | 'volatile' | 'while'
    ;

Kontekstowe słowo kluczowe to sekwencja znaków przypominająca identyfikator, która ma specjalne znaczenie w niektórych kontekstach, ale nie jest zarezerwowana i może być używana jako identyfikator poza tymi kontekstami, a także w przypadku poprzedzania znakiem@.

contextual_keyword
    : 'add'     | 'alias'      | 'ascending' | 'async'     | 'await'
    | 'by'      | 'descending' | 'dynamic'   | 'equals'    | 'from'
    | 'get'     | 'global'     | 'group'     | 'into'      | 'join'
    | 'let'     | 'nameof'     | 'notnull'   | 'on'        | 'orderby'
    | 'partial' | 'remove'     | 'select'    | 'set'       | 'unmanaged'
    | 'value'   | 'var'        | 'when'      | 'where'     | 'yield'
    ;

Uwaga: reguły keyword oraz contextual_keyword są regułami syntaktycznymi, ponieważ nie wprowadzają nowych rodzajów tokenów. Wszystkie słowa kluczowe i słowa kluczowe kontekstowe są definiowane przez niejawne reguły leksykalne, ponieważ występują one jako ciągi literału w gramatyce (§6.2.3). notatka końcowa

W większości przypadków składniowa lokalizacja słów kluczowych kontekstowych jest taka, że nigdy nie można ich mylić ze zwykłym użyciem identyfikatora. Na przykład w deklaracji właściwości identyfikatory get i set mają specjalne znaczenie (§15.7.3). Identyfikator inny niż get lub set nigdy nie jest dozwolony w tych lokalizacjach, więc to użycie nie powoduje konfliktu z użyciem tych słów jako identyfikatorów.

W niektórych przypadkach gramatyka nie wystarczy, aby odróżnić kontekstowe użycie słowa kluczowego od identyfikatorów. We wszystkich takich przypadkach zostanie określony sposób rozróżniania między dwiema opcjami. Na przykład słowo kluczowe var kontekstowe w niejawnie typizowanej deklaracji zmiennych lokalnych (§13.6.2) może powodować konflikt z zadeklarowanym typem o nazwie var, w tym przypadku zadeklarowana nazwa ma pierwszeństwo przed użyciem identyfikatora jako kontekstowego słowa kluczowego.

Innym przykładem takiego uściślania jest kontekstowe słowo kluczowe await (§12.9.8.1), które jest uważane za słowo kluczowe tylko wtedy, gdy wewnątrz zadeklarowanej asyncmetody , ale może być używane jako identyfikator gdzie indziej.

Podobnie jak w przypadku słów kluczowych słowa kluczowe kontekstowe mogą być używane jako zwykłe identyfikatory, prefiksując je znakiem @ .

Uwaga: jeśli są używane jako słowa kluczowe kontekstowe, te identyfikatory nie mogą zawierać Unicode_Escape_Sequences. notatka końcowa

Literały 6.4.5

6.4.5.1 Ogólne

Literał (§12.8.2) to reprezentacja wartości w kodzie źródłowym.

literal
    : boolean_literal
    | Integer_Literal
    | Real_Literal
    | Character_Literal
    | String_Literal
    | null_literal
    ;

Uwaga: literał jest regułą analizatora, gdyż grupuje inne rodzaje tokenów i nie wprowadza nowego rodzaju tokenu. notatka końcowa

6.4.5.2 Literały logiczne

Istnieją dwie wartości literału logicznego: true i false.

boolean_literal
    : TRUE
    | FALSE
    ;

Uwaga: boolean_literal jest regułą analizatora, ponieważ grupuje inne rodzaje tokenów i nie wprowadza nowego rodzaju tokenu. notatka końcowa

Typ literału logicznego boolean_literal to bool.

Literały liczb całkowitych 6.4.5.3

Literały liczb całkowitych służą do zapisywania wartości typów int, , uintlongi ulong. Literały liczb całkowitych mają trzy możliwe formy: dziesiętne, szesnastkowe i binarne.

Integer_Literal
    : Decimal_Integer_Literal
    | Hexadecimal_Integer_Literal
    | Binary_Integer_Literal
    ;

fragment Decimal_Integer_Literal
    : Decimal_Digit Decorated_Decimal_Digit* Integer_Type_Suffix?
    ;

fragment Decorated_Decimal_Digit
    : '_'* Decimal_Digit
    ;

fragment Decimal_Digit
    : '0'..'9'
    ;

fragment Integer_Type_Suffix
    : 'U' | 'u' | 'L' | 'l' |
      'UL' | 'Ul' | 'uL' | 'ul' | 'LU' | 'Lu' | 'lU' | 'lu'
    ;

fragment Hexadecimal_Integer_Literal
    : ('0x' | '0X') Decorated_Hex_Digit+ Integer_Type_Suffix?
    ;

fragment Decorated_Hex_Digit
    : '_'* Hex_Digit
    ;

fragment Hex_Digit
    : '0'..'9' | 'A'..'F' | 'a'..'f'
    ;

fragment Binary_Integer_Literal
    : ('0b' | '0B') Decorated_Binary_Digit+ Integer_Type_Suffix?
    ;

fragment Decorated_Binary_Digit
    : '_'* Binary_Digit
    ;

fragment Binary_Digit
    : '0' | '1'
    ;

Typ literału liczby całkowitej jest określany w następujący sposób:

  • Jeśli literał nie ma sufiksu, ma pierwszy z tych typów, w których można przedstawić jego wartość: int, , uint, long. ulong
  • Jeśli literał ma sufiks U lub u, ma pierwszy z tych typów, w których można przedstawić jego wartość: uint, ulong.
  • Jeśli literał ma sufiks L lub l, ma pierwszy z tych typów, w których można przedstawić jego wartość: long, ulong.
  • Jeśli literał ma sufiks UL, Ul, uL, ul, LU, Lu, lU lub lu, jest typu ulong.

Jeśli wartość reprezentowana przez literał liczby całkowitej znajduje się poza zakresem typu ulong, wystąpi błąd w czasie kompilacji.

Uwaga: W przypadku stylu sugerowane jest użycie znaku "L" zamiast "l" podczas pisania literałów typu long, ponieważ łatwo jest pomylić literę "" z cyfrą "l1". notatka końcowa

Aby zezwolić na zapisanie najmniejszych możliwych int wartości i long jako literałów liczb całkowitych, istnieją następujące dwie reguły:

  • Gdy Integer_Literal reprezentująca wartość 2147483648 (2³¹) i nie Integer_Type_Suffix pojawia się jako token bezpośrednio po jednoargumentowym tokenie operatora minus (§12.9.3), wynik (obu tokenów) jest stałą typu int z wartością −2147483648 (−2³¹). We wszystkich innych sytuacjach taki Integer_Literal jest typu uint.
  • Gdy Integer_Literal reprezentuje wartość 9223372036854775808 (2⁶³) i żaden Integer_Type_Suffix ani Integer_Type_SuffixL nie pojawiają się jako tokeny bezpośrednio po unarnym operatorem minus (l), wynik (obu tokenów) jest stałą typu o wartości long (−2⁶³). We wszystkich innych sytuacjach taki Integer_Literal jest typu ulong.

Przykład:

123                  // decimal, int
10_543_765Lu         // decimal, ulong
1_2__3___4____5      // decimal, int
_123                 // not a numeric literal; identifier due to leading _
123_                 // invalid; no trailing _allowed

0xFf                 // hex, int
0X1b_a0_44_fEL       // hex, long
0x1ade_3FE1_29AaUL   // hex, ulong
0x_abc               // hex, int
_0x123               // not a numeric literal; identifier due to leading _
0xabc_               // invalid; no trailing _ allowed

0b101                // binary, int
0B1001_1010u         // binary, uint
0b1111_1111_0000UL   // binary, ulong
0B__111              // binary, int
__0B111              // not a numeric literal; identifier due to leading _
0B111__              // invalid; no trailing _ allowed

przykład końcowy

6.4.5.4 Literały rzeczywiste

Literały rzeczywiste są używane do zapisywania wartości typów float, doublei decimal.

Real_Literal
    : Decimal_Digit Decorated_Decimal_Digit* '.'
      Decimal_Digit Decorated_Decimal_Digit* Exponent_Part? Real_Type_Suffix?
    | '.' Decimal_Digit Decorated_Decimal_Digit* Exponent_Part? Real_Type_Suffix?
    | Decimal_Digit Decorated_Decimal_Digit* Exponent_Part Real_Type_Suffix?
    | Decimal_Digit Decorated_Decimal_Digit* Real_Type_Suffix
    ;

fragment Exponent_Part
    : ('e' | 'E') Sign? Decimal_Digit Decorated_Decimal_Digit*
    ;

fragment Sign
    : '+' | '-'
    ;

fragment Real_Type_Suffix
    : 'F' | 'f' | 'D' | 'd' | 'M' | 'm'
    ;

Jeśli Real_Type_Suffix nie jest określony, typ Real_Literal to double. W przeciwnym razie Real_Type_Suffix określa typ literału rzeczywistego w następujący sposób:

  • Literał liczby rzeczywistej z sufiksem F lub f ma typ float.

    Przykład: literały 1f, 1.5f, 1e10fi 123.456F są typu float. przykład końcowy

  • Literał rzeczywisty z sufiksem D lub d ma typ double.

    Przykład: literały 1d, 1.5d, 1e10di 123.456D są typu double. przykład końcowy

  • Literał rzeczywisty z sufiksem M lub m jest typu decimal.

    Przykład: literały 1m, 1.5m, 1e10mi 123.456M są typu decimal. koniec przykładu Ten literał jest konwertowany na wartość decimal przez pobranie dokładnej wartości, a w razie potrzeby zaokrąglenie do najbliższej wartości reprezentowanej, stosując zaokrąglanie zgodne z zasadami bankierskimi (§8.3.8). Każda skala, która jest widoczna w literałach, jest zachowywana, chyba że wartość jest zaokrąglona. Uwaga: W związku z tym literał 2.900m zostanie przeanalizowany w celu utworzenia decimal znaku 0, współczynnika 2900i skali 3. notatka końcowa

Jeśli wielkość określonego literału jest zbyt duża, aby być reprezentowana w wskazanym typie, wystąpi błąd czasu kompilacji.

Uwaga: W szczególności Real_Literal nigdy nie będzie produkować nieskończoności zmiennoprzecinkowych. Niezerowa Real_Literal może być jednak zaokrąglona do zera. notatka końcowa

Wartość rzeczywistego literału typu float lub double jest określana przy użyciu trybu IEC 60559 „zaokrąglanie do najbliższego” z ustaleniem remisu poprzez zaokrąglenie do wartości parzystej (wartość z zerowym najmniej znaczącym bitem), przy czym wszystkie cyfry są uważane za znaczące.

Uwaga: W literałach liczby rzeczywistej cyfry dziesiętne są zawsze wymagane po kropce dziesiętnej. Na przykład 1.3F jest prawdziwym literałem, ale 1.F nie jest. notatka końcowa

Przykład:

1.234_567      // double
.3e5f          // float
2_345E-2_0     // double
15D            // double
19.73M         // decimal
1.F            // parsed as a member access of F due to non-digit after .
1_.2F          // invalid; no trailing _ allowed in integer part
1._234         // parsed as a member access of _234 due to non-digit after .
1.234_         // invalid; no trailing _ allowed in fraction
.3e_5F         // invalid; no leading _ allowed in exponent
.3e5_F         // invalid; no trailing _ allowed in exponent

przykład końcowy

6.4.5.5 Literały znaków

Literał znaku reprezentuje pojedynczy znak i składa się z ujętego w cudzysłów znaku, jak w 'a'.

Character_Literal
    : '\'' Character '\''
    ;

fragment Character
    : Single_Character
    | Simple_Escape_Sequence
    | Hexadecimal_Escape_Sequence
    | Unicode_Escape_Sequence
    ;

fragment Single_Character
    // anything but ', \, and New_Line_Character
    : ~['\\\u000D\u000A\u0085\u2028\u2029]
    ;

fragment Simple_Escape_Sequence
    : '\\\'' | '\\"' | '\\\\' | '\\0' | '\\a' | '\\b' |
      '\\f' | '\\n' | '\\r' | '\\t' | '\\v'
    ;

fragment Hexadecimal_Escape_Sequence
    : '\\x' Hex_Digit Hex_Digit? Hex_Digit? Hex_Digit?
    ;

Uwaga: Znak, który występuje po znaku ukośnika odwrotnego (\) w znaku musi być jednym z następujących znaków: ', ", \, 0, a, b, f, n, r, t, u, U, x, v. W przeciwnym razie wystąpi błąd czasu kompilacji. notatka końcowa

Uwaga: użycie \xHexadecimal_Escape_Sequence produkcji może być podatne na błędy i trudne do odczytania ze względu na zmienną liczbę cyfr szesnastkowe po \x. Na przykład w kodzie:

string good = "\x9Good text";
string bad = "\x9Bad text";

Początkowo może się wydawać, że znak wiodący jest taki sam (U+0009, znak tabulacji) w obu ciągach. W rzeczywistości drugi ciąg zaczyna się od U+9BAD, a wszystkie trzy litery w słowie "Bad" są prawidłowymi cyframi szesnastkowymi. Zaleca się unikać stosowania \x na rzecz konkretnych sekwencji ucieczki (\t w tym przykładzie) albo sekwencji ucieczki o stałej długości \u.

notatka końcowa

Sekwencja ucieczki szesnastkowej reprezentuje pojedynczą jednostkę kodu Unicode UTF-16 z wartością utworzoną przez liczbę szesnastkową po "\x".

Jeśli wartość reprezentowana przez literał znaku jest większa niż U+FFFF, wystąpi błąd czasu kompilacji.

Sekwencja ucieczki Unicode (§6.4.2) w literał znaku musi znajdować się w przedziale od U+0000 do U+FFFF.

Prosta sekwencja ucieczki reprezentuje znak Unicode, zgodnie z opisem w poniższej tabeli.

Sekwencja ucieczki Nazwa znaku Punkt kodowy Unicode
\' Pojedynczy cudzysłów U+0027
\" Podwójny cudzysłów U+0022
\\ Ukośnik odwrotny U+005C
\0 null U+0000
\a Alert U+0007
\b klawisz Backspace U+0008
\f Źródło danych formularzy U+000C
\n Nowy wiersz U+000A
\r Powrót karetki U+000D
\t Zakładka pozioma U+0009
\v Tabulacja pionowa U+000B

Typ Character_Literal jest char.

6.4.5.6 Literały ciągu

Język C# obsługuje dwie formy literałów ciągów znakowych: zwykłe literały ciągów znakowych i literały dosłowne ciągów znakowych. Zwykły literał ciągu składa się z zera lub większej liczby znaków ujętych w podwójny cudzysłów, jak w "hello", i może zawierać zarówno proste sekwencje ucieczki (takie jak \t dla znaku tabulacji), jak i szesnastkowe oraz Unicode sekwencje ucieczki.

Literał ciągu dosłownego @ składa się z znaku, po którym następuje znak podwójnego cudzysłowu, zero lub więcej znaków oraz znak podwójnego cudzysłowu zamykającego.

Przykład: prostym przykładem jest @"hello". przykład końcowy

W literału ciągu dosłownego znaki zawarte pomiędzy ogranicznikami są interpretowane dosłownie, a jedynym wyjątkiem jest Quote_Escape_Sequence, który reprezentuje jeden znak podwójnego cudzysłowia. W szczególności proste ciągi ucieczki, a ciągi ucieczki szesnastkowe i Unicode nie są przetwarzane w dosłownych literałach tekstowych. Literał ciągu dosłownego może obejmować wiele wierszy.

String_Literal
    : Regular_String_Literal
    | Verbatim_String_Literal
    ;

fragment Regular_String_Literal
    : '"' Regular_String_Literal_Character* '"'
    ;

fragment Regular_String_Literal_Character
    : Single_Regular_String_Literal_Character
    | Simple_Escape_Sequence
    | Hexadecimal_Escape_Sequence
    | Unicode_Escape_Sequence
    ;

fragment Single_Regular_String_Literal_Character
    // anything but ", \, and New_Line_Character
    : ~["\\\u000D\u000A\u0085\u2028\u2029]
    ;

fragment Verbatim_String_Literal
    : '@"' Verbatim_String_Literal_Character* '"'
    ;

fragment Verbatim_String_Literal_Character
    : Single_Verbatim_String_Literal_Character
    | Quote_Escape_Sequence
    ;

fragment Single_Verbatim_String_Literal_Character
    : ~["]     // anything but quotation mark (U+0022)
    ;

fragment Quote_Escape_Sequence
    : '""'
    ;

Przykład: przykład

string a = "Happy birthday, Joel"; // Happy birthday, Joel
string b = @"Happy birthday, Joel"; // Happy birthday, Joel
string c = "hello \t world"; // hello world
string d = @"hello \t world"; // hello \t world
string e = "Joe said \"Hello\" to me"; // Joe said "Hello" to me
string f = @"Joe said ""Hello"" to me"; // Joe said "Hello" to me
string g = "\\\\server\\share\\file.txt"; // \\server\share\file.txt
string h = @"\\server\share\file.txt"; // \\server\share\file.txt
string i = "one\r\ntwo\r\nthree";
string j = @"one
two
three";

wyświetla różne literały ciągów znaków. Ostatni literał ciągu, j, to literał ciągu dosłownego, który obejmuje wiele wierszy. Znaki między znakami cudzysłowu, w tym odstępami, takimi jak nowe znaki wiersza, są zachowywane dosłownie, a każda para znaków podwójnego cudzysłowu jest zastępowana jednym takim znakiem.

przykład końcowy

Uwaga: wszystkie podziały wierszy w literałach ciągu dosłownego są częścią wynikowego ciągu. Jeśli dokładne znaki używane do tworzenia podziałów wierszy są semantycznie istotne dla aplikacji, wszystkie narzędzia, które tłumaczą podziały wierszy w kodzie źródłowym na różne formaty (między "\n" i "\r\n", na przykład), zmieni zachowanie aplikacji. Deweloperzy powinni zachować ostrożność w takich sytuacjach. notatka końcowa

Uwaga: Ponieważ szesnastkowa sekwencja ucieczki może mieć zmienną liczbę cyfr szesnastkowych, literał ciągu znaków "\x123" zawiera pojedynczy znak o wartości szesnastkowej 123. Aby utworzyć ciąg zawierający znak o wartości szesnastkowej 12, po którym następuje znak 3, można napisać "\x00123" lub "\x12" + "3" zamiast tego. notatka końcowa

Typ String_Literal to string.

Każdy literał ciągu nie musi powodować wystąpienia nowego ciągu. Gdy co najmniej dwa literały ciągu, które są równoważne operatorowi równości ciągów (§12.12.8), pojawiają się w tym samym zestawie, te literały ciągu odwołują się do tego samego wystąpienia ciągu.

Przykład: na przykład dane wyjściowe generowane przez program

class Test
{
    static void Main()
    {
        object a = "hello";
        object b = "hello";
        System.Console.WriteLine(a == b);
    }
}

Jest to True spowodowane tym, że dwa literały odwołują się do tego samego wystąpienia ciągu.

przykład końcowy

6.4.5.7 Literał null

null_literal
    : NULL
    ;

Uwaga: null_literal jest regułą analizatora, ponieważ nie wprowadza nowego rodzaju tokenu. notatka końcowa

null_literal reprezentuje wartość null. Nie ma typu, ale można go przekonwertować na dowolny typ referencyjny lub typ wartości dopuszczający null za pomocą konwersji literału null (§10.2.7).

6.4.6 Operatory i znaki interpunkcyjne

Istnieje kilka rodzajów operatorów i znaków interpunkcyjnych. Operatory są używane w wyrażeniach do opisywania operacji obejmujących co najmniej jeden operand.

Przykład: wyrażenie a + b używa + operatora , aby dodać dwa operandy a i b. przykład końcowy

Znaki interpunkcyjne służą do grupowania i oddzielania.

operator_or_punctuator
    : '{'  | '}'  | '['  | ']'  | '('   | ')'  | '.'  | ','  | ':'  | ';'
    | '+'  | '-'  | ASTERISK    | SLASH | '%'  | '&'  | '|'  | '^'  | '!' | '~'
    | '='  | '<'  | '>'  | '?'  | '??'  | '::' | '++' | '--' | '&&' | '||'
    | '->' | '==' | '!=' | '<=' | '>='  | '+=' | '-=' | '*=' | '/=' | '%='
    | '&=' | '|=' | '^=' | '<<' | '<<=' | '=>' | '??='
    ;

right_shift
    : '>'  '>'
    ;

right_shift_assignment
    : '>' '>='
    ;

Uwaga: right_shift i right_shift_assignment są regułami analizatora, ponieważ nie wprowadzają nowego rodzaju tokenu, ale reprezentują sekwencję dwóch tokenów. Reguła operator_or_punctuator istnieje tylko do celów opisowych i nie jest używana w innych miejscach gramatyki. notatka końcowa

right_shift składa się z dwóch tokenów > i >. Podobnie right_shift_assignment składa się z dwóch tokenów > i >=. W przeciwieństwie do innych produkcji w gramatyce składniowej żadne znaki żadnego rodzaju (nawet białe znaki) nie są dozwolone między dwoma tokenami w każdej z tych produkcji. Te produkcje są traktowane specjalnie w celu umożliwienia prawidłowej obsługi type_parameter_lists (§15.2.3).

Uwaga: przed dodaniem typów ogólnych do języka C# >> i >>= były pojedynczymi tokenami. Jednak składnia dla typów ogólnych używa < znaków i > do rozdzielania parametrów typu i argumentów typu. Często pożądane jest użycie zagnieżdżonych skonstruowanych typów, takich jak List<Dictionary<string, int>>. Zamiast wymagać od programisty oddzielenia > i > przez spację, definicja dwóch operator_or_punctuator została zmieniona. notatka końcowa

6.5 Dyrektywy przetwarzania wstępnego

6.5.1 Ogólne

Dyrektywy przetwarzania wstępnego zapewniają możliwość warunkowego pomijania sekcji jednostek kompilacji, zgłaszania błędów i warunków ostrzegawczych, oznaczania odrębnych regionów kodu źródłowego i ustawiania kontekstu dopuszczalnego wartości null.

Uwaga: Termin "dyrektywy przetwarzania wstępnego" jest używany tylko do spójności z językami programowania C i C++. W języku C#nie ma oddzielnego kroku przetwarzania wstępnego; Dyrektywy przetwarzania wstępnego są przetwarzane w ramach fazy analizy leksykalnej. notatka końcowa

PP_Directive
    : PP_Start PP_Kind PP_New_Line
    ;

fragment PP_Kind
    : PP_Declaration
    | PP_Conditional
    | PP_Line
    | PP_Diagnostic
    | PP_Region
    | PP_Pragma
    | PP_Nullable
    ;

// Only recognised at the beginning of a line
fragment PP_Start
    // See note below.
    : { getCharPositionInLine() == 0 }? PP_Whitespace? '#' PP_Whitespace?
    ;

fragment PP_Whitespace
    : ( [\p{Zs}]  // any character with Unicode class Zs
      | '\u0009'  // horizontal tab
      | '\u000B'  // vertical tab
      | '\u000C'  // form feed
      )+
    ;

fragment PP_New_Line
    : PP_Whitespace? Single_Line_Comment? New_Line
    ;

Uwaga:

  • Gramatyka przetwarzania wstępnego definiuje pojedynczy token PP_Directive leksykalny używany dla wszystkich dyrektyw przetwarzania wstępnego. Semantyka każdej dyrektywy przetwarzania wstępnego jest definiowana w tej specyfikacji języka, ale nie sposób ich implementowania.
  • Fragment PP_Start musi być rozpoznawany tylko na początku linii, getCharPositionInLine() == 0 powyższy predykat leksykalny ANTLR sugeruje jeden ze sposobów, w jaki można to osiągnąć i jest tylko informacyjny, implementacja może używać innej strategii.

notatka końcowa

Dostępne są następujące dyrektywy przetwarzania wstępnego:

  • #define i #undef, które służą do definiowania i niezdefiniowania, odpowiednio, symboli kompilacji warunkowej (§6.5.4).
  • #if, , #elif#elsei #endif, które są używane do pomijania warunkowo sekcji kodu źródłowego (§6.5.5).
  • #line, który służy do kontrolowania numerów wierszy emitowanych w przypadku błędów i ostrzeżeń (§6.5.8).
  • #error, który służy do zgłaszania błędów (§6.5.6).
  • #region i #endregion, które są używane do jawnego oznaczania sekcji kodu źródłowego (§6.5.7).
  • #nullable, który służy do określania kontekstu odnoszącego się do wartości null (§6.5.9).
  • #pragma, który służy do określania opcjonalnych informacji kontekstowych kompilatora (§6.5.10).

Dyrektywa przetwarzania wstępnego zawsze zajmuje oddzielny wiersz kodu źródłowego i zawsze zaczyna się od # znaku i nazwy dyrektywy przetwarzania wstępnego. Białe znaki mogą występować przed znakiem # i między znakiem # a nazwą dyrektywy.

Wiersz źródłowy zawierający dyrektywę #define, #undef, #if, #elif, #else, #endif, #line, #endregion lub #nullable może kończyć się komentarzem jednowierszowym. Komentarze rozdzielane ( /* */ styl komentarzy) nie są dozwolone w wierszach źródłowych zawierających dyrektywy przetwarzania wstępnego.

Dyrektywy przetwarzania wstępnego nie są częścią gramatyki składni języka C#. Jednak dyrektywy przetwarzania wstępnego mogą służyć do dołączania lub wykluczania sekwencji tokenów i mogą w ten sposób wpływać na znaczenie programu w języku C#.

Przykład: po skompilowaniu program

#define A
#undef B
class C
{
#if A
    void F() {}
#else
    void G() {}
#endif
#if B
    void H() {}
#else
    void I() {}
#endif
}

wyniki w dokładnie tej samej sekwencji tokenów co program

class C
{
    void F() {}
    void I() {}
}

W związku z tym, podczas gdy leksykacyjnie, dwa programy są zupełnie różne, składniowo, są identyczne.

przykład końcowy

6.5.2 Symbole kompilacji warunkowej

Funkcjonalność kompilacji warunkowej zapewniana przez dyrektywy #if, #elif, #else i #endif jest kontrolowana za pomocą wyrażenia wstępnego przetwarzania (§6.5.3) i symboli kompilacji warunkowej.

fragment PP_Conditional_Symbol
    // Must not be equal to tokens TRUE or FALSE. See note below.
    : Basic_Identifier
    ;

Uwaga W jaki sposób implementacja wymusza ograniczenie dozwolonych wartości Basic_Identifier jest problemem z implementacją. notatka końcowa

Dwa symbole kompilacji warunkowej są traktowane tak samo, jeśli są identyczne po zastosowaniu następujących przekształceń w następującej kolejności:

  • Każda Unicode_Escape_Sequence jest przekształcana w odpowiadający mu znak Unicode.
  • Wszystkie Formatting_Characters są usuwane.

Symbol kompilacji warunkowej ma dwa możliwe stany: zdefiniowane lub niezdefiniowane. Na początku leksykalnego przetwarzania jednostki kompilacji symbol kompilacji warunkowej jest niezdefiniowany, chyba że został jawnie zdefiniowany przez mechanizm zewnętrzny (taki jak opcja kompilatora wiersza polecenia). Po przetworzeniu #define dyrektywy symbol kompilacji warunkowej wymieniony w tej dyrektywie staje się zdefiniowany w tej jednostce kompilacji. Symbol pozostaje zdefiniowany do momentu, gdy dyrektywa #undef dla tego samego symbolu zostanie przetworzona, lub do końca jednostki kompilacji. Implikacją tego jest to, że dyrektywy #define i #undef w jednej jednostce kompilacji nie mają wpływu na inne jednostki kompilacji w tym samym programie.

W przypadku odwołania w wyrażeniu przetwarzania wstępnego (§6.5.3), zdefiniowany symbol kompilacji warunkowej ma wartość logiczną true, a niezdefiniowany symbol kompilacji warunkowej ma wartość logiczną false. Nie ma wymogu jawnego zadeklarowania symboli kompilacji warunkowej przed ich odwołaniem do wyrażeń przetwarzania wstępnego. Zamiast tego symbole niezadeklarowane są po prostu niezdefiniowane i w związku z tym mają wartość false.

Przestrzeń nazw dla symboli kompilacji warunkowej jest odrębna i oddzielona od wszystkich innych nazwanych jednostek w programie języka C#. Symbole kompilacji warunkowej mogą być przywoływane tylko w dyrektywach #define i #undef oraz w wyrażeniach wstępnego przetwarzania.

6.5.3 Wyrażenia przetwarzania wstępnego

Wyrażenia wstępnego przetwarzania mogą występować w dyrektywach #if i #elif. Operatory ! (tylko negacja logiczna prefiksu), ==, !=, &&i || są dozwolone w wyrażeniach przetwarzania wstępnego i nawiasy mogą być używane do grupowania.

fragment PP_Expression
    : PP_Whitespace? PP_Or_Expression PP_Whitespace?
    ;

fragment PP_Or_Expression
    : PP_And_Expression (PP_Whitespace? '||' PP_Whitespace? PP_And_Expression)*
    ;

fragment PP_And_Expression
    : PP_Equality_Expression (PP_Whitespace? '&&' PP_Whitespace?
      PP_Equality_Expression)*
    ;

fragment PP_Equality_Expression
    : PP_Unary_Expression (PP_Whitespace? ('==' | '!=') PP_Whitespace?
      PP_Unary_Expression)*
    ;

fragment PP_Unary_Expression
    : PP_Primary_Expression
    | '!' PP_Whitespace? PP_Unary_Expression
    ;

fragment PP_Primary_Expression
    : TRUE
    | FALSE
    | PP_Conditional_Symbol
    | '(' PP_Whitespace? PP_Expression PP_Whitespace? ')'
    ;

W przypadku odwołania w wyrażeniu preprocesora zdefiniowany symbol kompilacji warunkowej ma wartość logiczną true, a niezdefiniowany symbol kompilacji warunkowej ma wartość logiczną false.

Obliczanie wyrażenia wstępnego przetwarzania zawsze daje wartość logiczną. Reguły obliczania wyrażenia wstępnego przetwarzania są takie same jak w przypadku wyrażenia stałego (§12.23), z wyjątkiem tego, że jedynymi jednostkami zdefiniowanymi przez użytkownika, do których można się odwoływać, są symbole kompilacji warunkowej.

6.5.4 Dyrektywy definicji

Dyrektywy definicji służą do definiowania lub niezdefiniowania symboli kompilacji warunkowej.

fragment PP_Declaration
    : 'define' PP_Whitespace PP_Conditional_Symbol
    | 'undef' PP_Whitespace PP_Conditional_Symbol
    ;

Przetwarzanie dyrektywy #define powoduje zdefiniowanie danego symbolu kompilacji warunkowej, począwszy od wiersza źródłowego następującego po dyrektywie. Podobnie przetwarzanie #undef dyrektywy powoduje, że dany symbol kompilacji warunkowej staje się niezdefiniowany, począwszy od wiersza źródłowego zgodnego z dyrektywą.

Wszelkie #define i #undef dyrektywy w jednostce kompilacji powinny wystąpić przed pierwszym tokenem (§6.4) w jednostce kompilacji; w przeciwnym razie wystąpi błąd czasu kompilacji. Intuicyjnie rzecz ujmując, dyrektywy #define i #undef powinny poprzedzać każdy "prawdziwy kod" w jednostce kompilacji.

Przykład: przykład:

#define Enterprise
#if Professional || Enterprise
#define Advanced
#endif
namespace Megacorp.Data
{
#if Advanced
    class PivotTable {...}
#endif
}

są prawidłowe, ponieważ #define dyrektywy poprzedzają pierwszy token (namespace słowo kluczowe) w jednostce kompilacji.

przykład końcowy

Przykład: Poniższy przykład powoduje błąd czasu kompilacji, ponieważ #define jest zgodny z rzeczywistym kodem:

#define A
namespace N
{
#define B
#if B
    class Class1 {}
#endif
}

przykład końcowy

Element #define może zdefiniować symbol kompilacji warunkowej, który jest już zdefiniowany, bez jakichkolwiek pośredniców #undef dla tego symbolu.

Przykład: Poniższy przykład definiuje symbol kompilacji warunkowej A, a następnie definiuje go ponownie.

#define A
#define A

W przypadku kompilatorów, które umożliwiają definiowanie symboli kompilacji warunkowej jako opcji kompilacji, alternatywnym sposobem wystąpienia takiej ponownej definicji jest zdefiniowanie symbolu jako opcji kompilatora, a także w źródle.

przykład końcowy

Element #undef może usunąć definicję symbolu kompilacji warunkowej, który nie jest zdefiniowany.

Przykład: W poniższym przykładzie zdefiniowano symbol A kompilacji warunkowej, a następnie dwukrotnie go nie zdefiniowano; chociaż drugi #undef nie ma żadnego efektu, nadal jest prawidłowy.

#define A
#undef A
#undef A

przykład końcowy

6.5.5 Dyrektywy kompilacji warunkowej

Dyrektywy kompilacji warunkowej są używane do warunkowego dołączania lub wykluczania części jednostki kompilacji.

fragment PP_Conditional
    : PP_If_Section
    | PP_Elif_Section
    | PP_Else_Section
    | PP_Endif
    ;

fragment PP_If_Section
    : 'if' PP_Whitespace PP_Expression
    ;

fragment PP_Elif_Section
    : 'elif' PP_Whitespace PP_Expression
    ;

fragment PP_Else_Section
    : 'else'
    ;

fragment PP_Endif
    : 'endif'
    ;

Dyrektywy kompilacji warunkowej powinny być zapisane w grupach składających się, w określonej kolejności, z dyrektywy #if, zerowej lub większej liczby dyrektyw #elif, zerowej lub jednej dyrektywy #else oraz dyrektywy #endif. Między dyrektywami znajdują się sekcje warunkowe kodu źródłowego. Każda sekcja jest kontrolowana przez bezpośrednio poprzednią dyrektywę. Sekcja warunkowa może zawierać zagnieżdżone dyrektywy kompilacji warunkowej, pod warunkiem że te dyrektywy tworzą pełne grupy.

Co najwyżej jedna z zawartych sekcji warunkowych jest wybierana do normalnego przetwarzania leksyktycznego:

  • PP_Expression dyrektyw #if i #elif są oceniane w kolejności, aż jeden z nich zwróci true. Jeśli wyrażenie zwraca wartość true, wybierana jest sekcja warunkowa następująca po odpowiedniej dyrektywie.
  • Jeśli wszystkie PP_Expression przynoszą false, i jeśli obecna jest dyrektywa #else, sekcja warunkowa po dyrektywie #else jest wybierana.
  • W przeciwnym razie nie wybrano sekcji warunkowej.

Wybrana sekcja warunkowa, jeśli istnieje, jest przetwarzana jako normalny input_section: kod źródłowy zawarty w sekcji jest zgodny z gramatyką leksykatyczną; tokeny są generowane z kodu źródłowego w sekcji; a dyrektywy przetwarzania wstępnego w sekcji mają określone skutki.

Wszystkie pozostałe sekcje warunkowe są pomijane i żadne tokeny, z wyjątkiem tych dla dyrektyw przetwarzania wstępnego, są generowane na podstawie kodu źródłowego. W związku z tym pominięty kod źródłowy, z wyjątkiem dyrektyw przetwarzania wstępnego, może być niepoprawny pod względem leksykalnym. Pominięte dyrektywy przetwarzania wstępnego są poprawne leksykalnie, ale nie są przetwarzane w inny sposób. W pomijanej sekcji warunkowej są również pomijane wszystkie zagnieżdżone sekcje warunkowe (zawarte w zagnieżdżonych konstrukcjach #if...#endif).

Uwaga: Powyższa gramatyka nie przechwytuje dodatku, że sekcje warunkowe między dyrektywami przetwarzania wstępnego mogą być źle sformułowane leksykalnie. W związku z tym gramatyka nie jest gotowa na ANTLR, ponieważ obsługuje tylko poprawne dane wejściowe. notatka końcowa

Przykład: Poniższy przykład ilustruje sposób zagnieżdżania dyrektyw kompilacji warunkowej:

#define Debug // Debugging on
#undef Trace // Tracing off
class PurchaseTransaction
{
    void Commit()
    {
#if Debug
        CheckConsistency();
    #if Trace
        WriteToLog(this.ToString());
    #endif
#endif
        CommitHelper();
    }
    ...
}

Z wyjątkiem dyrektyw wstępnego przetwarzania pominięty kod źródłowy nie podlega analizie leksykalnej. Na przykład następujący przykład jest prawidłowy pomimo niezakończonego komentarza w sekcji #else:

#define Debug // Debugging on
class PurchaseTransaction
{
    void Commit()
    {
#if Debug
        CheckConsistency();
#else
        /* Do something else
#endif
    }
    ...
}

Należy jednak pamiętać, że dyrektywy przetwarzania wstępnego muszą być poprawne leksykalnie nawet w pominiętych sekcjach kodu źródłowego.

Dyrektywy przetwarzania wstępnego nie są przetwarzane, gdy pojawiają się wewnątrz wielowierszowych elementów wejściowych. Na przykład program:

class Hello
{
    static void Main()
    {
        System.Console.WriteLine(@"hello,
#if Debug
        world
#else
        Nebraska
#endif
        ");
    }
}

wyniki w wyjściu:

hello,
#if Debug
        world
#else
        Nebraska
#endif

W szczególnych przypadkach zestaw dyrektyw wstępnego przetwarzania, które są przetwarzane, może zależeć od ewaluacji pp_expression. Przykład:

#if X
    /*
#else
    /* */ class Q { }
#endif

zawsze tworzy ten sam strumień tokenu (classQ{} ), niezależnie od tego, czy X jest zdefiniowany. Jeżeli X jest zdefiniowane, przetwarza się tylko dyrektywy #if i #endif, ze względu na komentarz wielowierszowy. Jeśli X jest niezdefiniowany, trzy dyrektywy (#if, #else, #endif) są częścią zestawu dyrektywy.

przykład końcowy

6.5.6 Dyrektywy diagnostyczne

Dyrektywy diagnostyczne są używane do generowania jawnie komunikatów o błędach i ostrzeżeniach, które są zgłaszane w taki sam sposób, jak inne błędy i ostrzeżenia w czasie kompilacji.

fragment PP_Diagnostic
    : 'error' PP_Message?
    | 'warning' PP_Message?
    ;

fragment PP_Message
    : PP_Whitespace Input_Character*
    ;

Przykład: przykład

#if Debug && Retail
    #error A build can't be both debug and retail
#endif
class Test {...}

Tworzy błąd czasu kompilacji („Kompilacja nie może być jednocześnie debug i release”), jeśli zdefiniowane są symbole kompilacji warunkowej Debug i Retail. Należy pamiętać, że PP_Message może zawierać dowolny tekst; w szczególności nie musi zawierać dobrze sformułowanych tokenów, jak pokazano w pojedynczym cudzysłowie w słowie can't.

przykład końcowy

Dyrektywy regionów 6.5.7

Dyrektywy regionu są używane do oznaczania jawnie regionów kodu źródłowego.

fragment PP_Region
    : PP_Start_Region
    | PP_End_Region
    ;

fragment PP_Start_Region
    : 'region' PP_Message?
    ;

fragment PP_End_Region
    : 'endregion' PP_Message?
    ;

Żadne znaczenie semantyczne nie jest dołączone do regionu; regiony są przeznaczone do użycia przez programistę lub przez zautomatyzowane narzędzia do oznaczania sekcji kodu źródłowego. Istnieje jedna #endregion dyrektywa zgodna z każdą #region dyrektywą. Komunikat określony w dyrektywie #region lub #endregion również nie ma znaczenia semantycznego; służy jedynie do identyfikacji regionu. Dopasowywanie dyrektyw #region i #endregion może mieć różne PP_Messages.

Przetwarzanie leksykalne regionu:

#region
...
#endregion

odpowiada dokładnie przetwarzaniu leksykalnemu dyrektywy kompilacji w formie warunkowej.

#if true
...
#endif

Uwaga: oznacza to, że region może zawierać jeden lub więcej #if/.../#endif, lub być zawarty w sekcji warunkowej w ramach #if/.../#endif; ale region nie może nakładać się tylko na część #if/.../#endif, lub zaczynać się i kończyć w różnych sekcjach warunkowych. notatka końcowa

6.5.8 Dyrektywy linii

Dyrektywy linii mogą służyć do zmiany numerów linii i nazw jednostek kompilacji, które są zgłaszane przez kompilator w wynikach, takich jak ostrzeżenia i błędy. Te wartości są również używane przez atrybuty informacji o dzwoniącym (§22.5.6).

Uwaga: dyrektywy wiersza są najczęściej używane w narzędziach metaprogramowania, które generują kod źródłowy języka C# na podstawie innych danych wejściowych tekstowych. notatka końcowa

fragment PP_Line
    : 'line' PP_Whitespace PP_Line_Indicator
    ;

fragment PP_Line_Indicator
    : Decimal_Digit+ PP_Whitespace PP_Compilation_Unit_Name
    | Decimal_Digit+
    | DEFAULT
    | 'hidden'
    ;

fragment PP_Compilation_Unit_Name
    : '"' PP_Compilation_Unit_Name_Character* '"'
    ;

fragment PP_Compilation_Unit_Name_Character
    // Any Input_Character except "
    : ~('\u000D' | '\u000A'   | '\u0085' | '\u2028' | '\u2029' | '"')
    ;

Jeśli nie ma żadnych dyrektyw #line, kompilator zgłasza prawdziwe numery wierszy i nazwy jednostek kompilacji w danych wyjściowych. Podczas przetwarzania dyrektywy #line zawierającej PP_Line_Indicator, która nie jest default, kompilator traktuje wiersz po dyrektywy jako o podanej liczbie wierszy (i nazwie jednostki kompilacji, jeśli określono).

Dozwolona wartość maksymalna Decimal_Digit+ jest zdefiniowana przez implementację.

#line default Dyrektywa cofa efekt wszystkich poprzednich #line dyrektyw. Kompilator zgłasza dokładne informacje o liniach dla kolejnych linii, tak, jakby żadna z dyrektyw #line nie została przetworzona.

#line hidden Dyrektywa nie ma wpływu na jednostkę kompilacji i numery wierszy zgłaszane w komunikatach o błędach ani na te powstałe przy użyciu CallerLineNumberAttribute (§22.5.6.2). Ma to na celu wpływ na narzędzia debugowania na poziomie źródła, tak aby podczas debugowania wszystkie wiersze między #line hidden dyrektywą a kolejną #line dyrektywą, która nie jest #line hidden, nie miały informacji o numerze wiersza i były całkowicie pomijane podczas przechodzenia przez kod.

Uwaga: chociaż PP_Compilation_Unit_Name może zawierać tekst, który wygląda jak sekwencja ucieczki, taki tekst nie jest sekwencją ucieczki. W tym kontekście znak "\" po prostu wyznacza zwykły znak ukośnika odwrotnego. notatka końcowa

6.5.9 Dyrektywa Nullable

Dyrektywa nullable kontroluje kontekst nullable, zgodnie z poniższym opisem.

fragment PP_Nullable
    : 'nullable' PP_Whitespace PP_Nullable_Action
      (PP_Whitespace PP_Nullable_Target)?
    ;
fragment PP_Nullable_Action
    : 'disable'
    | 'enable'
    | 'restore'
    ;
fragment PP_Nullable_Target
    : 'warnings'
    | 'annotations'
    ;

Dyrektywa nullable ustawia dostępne flagi dla kolejnych wierszy kodu, aż zostanie zastąpiona przez inną dyrektywę nullable lub do momentu osiągnięcia końca kompilacji _unit. Kontekst dopuszczający wartość null zawiera dwie flagi: adnotacje i ostrzeżenia. Skutek działania każdej opcji dyrektywy nullable jest następujący:

  • #nullable disable: Wyłącza zarówno adnotacje obsługujące wartości null, jak i flagi ostrzeżeń obsługujące wartości null.
  • #nullable enable: włącza zarówno adnotacje dopuszczające wartość null, jak i flagi ostrzeżeń dopuszczające wartość null.
  • #nullable restore: przywraca zarówno adnotacje, jak i flagi ostrzeżeń do stanu określonego przez mechanizm zewnętrzny, jeśli istnieje.
  • #nullable disable annotations: wyłącza flagę adnotacji obsługujących wartości null. Flaga ostrzeżeń dla typów dopuszczających wartość null pozostaje niezmieniona.
  • #nullable enable annotations: włącza flagę adnotacji dopuszczanych do wartości null. Flaga ostrzeżeń dotyczących wartości null pozostaje bez zmian.
  • #nullable restore annotations: przywraca flagę adnotacji dopuszczanych do wartości null do stanu określonego przez mechanizm zewnętrzny, jeśli istnieje. Flaga ostrzeżeń dotyczących wartości null pozostaje bez zmian.
  • #nullable disable warnings: wyłącza flagę ostrzeżeń związanych z wartościami null. Flaga adnotacji nullable pozostaje bez zmian.
  • #nullable enable warnings: Włącza flagę ostrzeżeń o wartości null. Nie ma to wpływu na flagę adnotacji dla wartości null.
  • #nullable restore warnings: Przywraca flagę ostrzeżeń o wartościach null do stanu określonego przez mechanizm zewnętrzny, o ile taki istnieje. Nie ma to wpływu na flagę adnotacji dopuszczanych do wartości null.

Stan wyrażeń dopuszczanych do wartości null jest śledzony przez cały czas. Stan flagi adnotacji oraz obecność lub brak adnotacji dopuszczającej wartość null, ?, określa początkowy stan null deklaracji zmiennej. Ostrzeżenia są wystawiane tylko wtedy, gdy flaga ostrzeżeń jest włączona.

Przykład: przykład

#nullable disable
string x = null;
string y = "";
#nullable enable
Console.WriteLine(x.Length); // Warning
Console.WriteLine(y.Length);

tworzy ostrzeżenie w czasie kompilacji ("że x jest null"). Stan dopuszczalności null x jest śledzony wszędzie. Po włączeniu flagi ostrzeżeń zostanie wyświetlone ostrzeżenie.

przykład końcowy

6.5.10 Dyrektywy Pragma

Dyrektywa #pragma przetwarzania wstępnego służy do określania informacji kontekstowych dla kompilatora.

Uwaga: na przykład kompilator może dostarczać #pragma dyrektywy, które

  • Włącz lub wyłącz określone komunikaty ostrzegawcze podczas kompilowania kolejnego kodu.
  • Określ optymalizacje, które mają być stosowane do kolejnego kodu.
  • Określ informacje, które mają być używane przez debuger.

notatka końcowa

fragment PP_Pragma
    : 'pragma' PP_Pragma_Text?
    ;

fragment PP_Pragma_Text
    : PP_Whitespace Input_Character*
    ;

Input_Character w PP_Pragma_Text są interpretowane przez kompilator w sposób zdefiniowany przez implementację. Informacje podane w #pragma dyrektywie nie zmieniają semantyki programu. #pragma Dyrektywa powinna zmieniać tylko to zachowanie kompilatora, które wykracza poza zakres tej specyfikacji języka. Jeśli kompilator nie może zinterpretować Input_Characters, kompilator może wygenerować ostrzeżenie; nie generuje jednak błędu czasu kompilacji.

Uwaga: PP_Pragma_Text może zawierać dowolny tekst; w szczególności nie musi zawierać dobrze sformułowanych tokenów. notatka końcowa