Condividi tramite


6 Struttura lessicale

6.1 Programmi

Un programma C# è costituito da uno o più file di origine, noti formalmente come unità di compilazione (§14.2). Anche se un'unità di compilazione potrebbe avere una corrispondenza uno-a-uno con un file in un file system, tale corrispondenza non è necessaria.

Concettualmente, un programma viene compilato usando tre passaggi:

  1. Trasformazione, che converte un file da un particolare repertorio di caratteri e schema di codifica in una sequenza di caratteri Unicode.
  2. Analisi lessicale, che converte un flusso di caratteri di input Unicode in un flusso di token.
  3. Analisi sintattica, che converte il flusso di token in codice eseguibile.

Le implementazioni conformi accettano unità di compilazione Unicode codificate con il modulo di codifica UTF-8 (come definito dallo standard Unicode) e convertirle in una sequenza di caratteri Unicode. Le implementazioni possono scegliere di accettare e trasformare schemi di codifica dei caratteri aggiuntivi (ad esempio UTF-16, UTF-32 o mapping di caratteri non Unicode).

Nota: la gestione del carattere NULL Unicode (U+0000) è definita dall'implementazione. È consigliabile evitare che gli sviluppatori usino questo carattere nel codice sorgente, per motivi di portabilità e leggibilità. Quando il carattere è necessario all'interno di un carattere o un valore letterale stringa, è possibile usare le sequenze \0 di escape o \u0000 . nota finale

Nota: non rientra nell'ambito di questa specifica per definire come un file che usa una rappresentazione di caratteri diversa da Unicode potrebbe essere trasformato in una sequenza di caratteri Unicode. Durante tale trasformazione, tuttavia, è consigliabile convertire il normale carattere di separazione riga (o sequenza) nell'altro set di caratteri nella sequenza a due caratteri costituito dal carattere di ritorno a capo Unicode (U+000D) seguito dal carattere di avanzamento riga Unicode (U+000A). Per la maggior parte questa trasformazione non avrà effetti visibili; Tuttavia, influirà sull'interpretazione dei token letterali stringa verbatim (§6.4.5.6). Lo scopo di questa raccomandazione è consentire a un valore letterale stringa verbatim di produrre la stessa sequenza di caratteri quando la relativa unità di compilazione viene spostata tra sistemi che supportano set di caratteri non Unicode diversi, in particolare quelli che usano sequenze di caratteri diverse per la separazione delle righe. nota finale

6.2 Grammatiche

6.2.1 Generale

Questa specifica presenta la sintassi del linguaggio di programmazione C# usando due grammatiche. La grammatica lessicale (§6.2.3) definisce il modo in cui i caratteri Unicode vengono combinati per formare caratteri di terminazione di riga, spazi vuoti, commenti, token e direttive di pre-elaborazione. La grammatica sintattica (§6.2.4) definisce il modo in cui i token risultanti dalla grammatica lessicale vengono combinati per formare programmi C#.

Tutti i caratteri del terminale devono essere considerati come il carattere Unicode appropriato dall'intervallo U+0020 a U+007F, anziché qualsiasi carattere simile di altri intervalli di caratteri Unicode.

6.2.2 Notazione grammaticale

Le grammatiche lessicali e sintattiche sono presentate nel formato Extended Backus-Naur dello strumento grammaticale ANTLR.

Anche se viene usata la notazione ANTLR, questa specifica non presenta una "grammatica di riferimento ANTLR" completa per C#; scrivere un lexer e un parser, a mano o usando uno strumento come ANTLR, non rientra nell'ambito di una specifica del linguaggio. Con tale qualificazione, questa specifica tenta di ridurre al minimo il divario tra la grammatica specificata e quella necessaria per creare un lesser e un parser in ANTLR.

ANTLR distingue tra le regole lessicali e sintattiche, definite parser da ANTLR, le grammatiche nella notazione avviando regole lessicali con una lettera maiuscola e regole parser con una lettera minuscola.

Nota: la grammatica lessicale C# (§6.2.3) e la grammatica sintattica (§6.2.4) non sono esattamente in corrispondenza con la divisione ANTLR in grammer lessicali e parser. Questa piccola mancata corrispondenza significa che alcune regole del parser ANTLR vengono usate quando si specifica la grammatica lessicale C#. nota finale

6.2.3 Grammatica lessicale

La grammatica lessicale di C# è presentata in §6.3, §6.4 e §6.5. I simboli terminale della grammatica lessicale sono i caratteri del set di caratteri Unicode e la grammatica lessicale specifica come i caratteri vengono combinati ai token di modulo (§6.4), spazi vuoti (§6.3.4), commenti (§6.3.3) e direttive di pre-elaborazione (§6.5).

Molti dei simboli terminale della grammatica sintattica non vengono definiti in modo esplicito come token nella grammatica lessicale. È invece possibile sfruttare il comportamento ANTLR che le stringhe letterali nella grammatica vengono estratte come token lessicali impliciti; ciò consente di rappresentare parole chiave, operatori e così via nella grammatica in base alla relativa rappresentazione letterale anziché a un nome di token.

Ogni unità di compilazione in un programma C# deve essere conforme alla produzione di input della grammatica lessicale (§6.3.1).

6.2.4 Grammatica sintattica

La grammatica sintattica di C# è presentata nelle clausole, nelle sottoclause e nelle clausole che seguono questa sottoclausa. I simboli terminale della grammatica sintattica sono i token definiti in modo esplicito dalla grammatica lessicale e implicitamente dalle stringhe letterali nella grammatica stessa (§6.2.3). La grammatica sintattica specifica il modo in cui i token vengono combinati per formare programmi C#.

Ogni unità di compilazione in un programma C# deve essere conforme alla compilation_unit produzione (§14.2) della grammatica sintattica.

6.2.5 Ambiguità grammaticali

Le produzioni per simple_name (§12.8.4) e member_access (§12.8.7) possono generare ambiguità nella grammatica delle espressioni.

Esempio: Istruzione :

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

può essere interpretato come una chiamata a F con due argomenti, G < A e B > (7). In alternativa, può essere interpretato come una chiamata a F con un argomento, ovvero una chiamata a un metodo G generico con due argomenti di tipo e un argomento regolare.

esempio finale

Se una sequenza di token può essere analizzata (nel contesto) come simple_name (§12.8.4), member_access (§12.8.7) o pointer_member_access (§23.6.3) che termina con un type_argument_list (§8.4.2), il token immediatamente successivo al token di chiusura viene esaminato, per verificare se è>

  • Uno di ( ) ] } : ; , . ? == != | ^ && || & [; o
  • Uno degli operatori < <= >= is asrelazionali ; o
  • Parola chiave di query contestuale visualizzata all'interno di un'espressione di query; o
  • In determinati contesti, l'identificatore viene considerato come un token di ambiguità. Tali contesti sono la posizione in cui la sequenza di token da disambiguare è immediatamente preceduta da una delle parole chiave is, case o outo si verifica durante l'analisi del primo elemento di un valore letterale di tupla (nel qual caso i token sono preceduti da ( o : e l'identificatore è seguito da un ,) o da un elemento successivo di un valore letterale di tupla.

Se il token seguente è incluso in questo elenco o un identificatore in tale contesto, il type_argument_list viene conservato come parte del simple_name, member_access o pointer_member-access e qualsiasi altra possibile analisi della sequenza di token viene rimossa. In caso contrario, il type_argument_list non viene considerato parte del simple_name, member_access o pointer_member_access, anche se non è possibile analizzare la sequenza di token.

Nota: queste regole non vengono applicate durante l'analisi di un type_argument_list in un namespace_or_type_name (§7.8). nota finale

Esempio: Istruzione :

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

in base a questa regola, viene interpretato come una chiamata a F con un argomento, ovvero una chiamata a un metodo G generico con due argomenti di tipo e un argomento regolare. Istruzioni

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

ognuno verrà interpretato come una chiamata a F con due argomenti. Istruzione

x = F<A> + y;

verrà interpretato come operatore minore di, operatore maggiore di e operatore unario-plus, come se l'istruzione fosse stata scritta x = (F < A) > (+y), anziché come simple_name con un type_argument_list seguito da un operatore binary-plus. Nell'istruzione

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

i token vengono interpretati C<T> come un namespace_or_type_name con un type_argument_list a causa della presenza del token && disambiguazione dopo il type_argument_list.

L'espressione (A < B, C > D) è una tupla con due elementi, ognuno con un confronto.

L'espressione (A<B,C> D, E) è una tupla con due elementi, la cui prima è un'espressione di dichiarazione.

La chiamata M(A < B, C > D, E) ha tre argomenti.

La chiamata M(out A<B,C> D, E) ha due argomenti, il primo dei quali è una out dichiarazione.

L'espressione e is A<B> C usa un modello di dichiarazione.

L'etichetta case A<B> C: case usa un modello di dichiarazione.

esempio finale

Quando si riconosce un relational_expression (§12.12.1is

6.3 Analisi lessicale

6.3.1 Generale

Per praticità, la grammatica lessicale definisce e fa riferimento ai token lexer denominati seguenti:

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

Anche se si tratta di regole lessxer, questi nomi vengono digitati in lettere maiuscole per distinguerli dai nomi normali delle regole lessxer.

Nota: queste regole di praticità sono eccezioni alla consueta pratica di non fornire nomi di token espliciti per i token definiti da stringhe letterali. nota finale

La produzione di input definisce la struttura lessicale di un'unità di compilazione C#.

input
    : input_section?
    ;

input_section
    : input_section_part+
    ;

input_section_part
    : input_element* New_Line
    | PP_Directive
    ;

input_element
    : Whitespace
    | Comment
    | token
    ;

Nota: la grammatica precedente è descritta dalle regole di analisi ANTLR, definisce la struttura lessicale di un'unità di compilazione C# e non token lessicali. nota finale

Cinque elementi di base costituiscono la struttura lessicale di un'unità di compilazione C#: caratteri di terminazione riga (§6.3.2), spazi vuoti (§6.3.4), commenti (§6.3.3), token (§6.4) e direttive di pre-elaborazione (§6.5). Di questi elementi di base, solo i token sono significativi nella grammatica sintattica di un programma C# (§6.2.4).

L'elaborazione lessicale di un'unità di compilazione C# consiste nel ridurre il file in una sequenza di token che diventa l'input per l'analisi sintattica. I caratteri di terminazione riga, gli spazi vuoti e i commenti possono essere usati per separare i token e le direttive di pre-elaborazione possono causare l'annullamento delle sezioni dell'unità di compilazione, ma in caso contrario questi elementi lessicali non hanno alcun impatto sulla struttura sintattica di un programma C#.

Quando diverse produzioni grammatiche lessicali corrispondono a una sequenza di caratteri in un'unità di compilazione, l'elaborazione lessicale costituisce sempre l'elemento lessicale più lungo possibile.

Esempio: la sequenza // di caratteri viene elaborata come inizio di un commento a riga singola perché tale elemento lessicale è più lungo di un singolo / token. esempio finale

Alcuni token sono definiti da un set di regole lessicali; una regola principale e una o più regole secondarie. Quest'ultimo è contrassegnato nella grammatica per fragment indicare che la regola definisce parte di un altro token. Le regole del frammento non vengono considerate nell'ordinamento dall'alto verso il basso delle regole lessicali.

Nota: in ANTLR fragment è una parola chiave che produce lo stesso comportamento definito qui. nota finale

6.3.2 Caratteri di terminazione riga

I caratteri di terminazione di riga dividono i caratteri di un'unità di compilazione C# in righe.

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

Per la compatibilità con gli strumenti di modifica del codice sorgente che aggiungono marcatori di fine file e per consentire la visualizzazione di un'unità di compilazione come sequenza di righe terminate correttamente, vengono applicate le trasformazioni seguenti, per ogni unità di compilazione in un programma C#:

  • Se l'ultimo carattere dell'unità di compilazione è un carattere Control-Z (U+001A), questo carattere viene eliminato.
  • Un carattere di ritorno a capo (U+000D) viene aggiunto alla fine dell'unità di compilazione se tale unità di compilazione non è vuota e se l'ultimo carattere dell'unità di compilazione non è un ritorno a capo (U+000D), un avanzamento riga (U+000A), un carattere di riga successivo (U+0085), un separatore di riga (U+2028) o un separatore di paragrafo (U+2029).

Nota: il ritorno a capo aggiuntivo consente a un programma di terminare in un PP_Directive (§6.5) che non ha un New_Line irreversibile. nota finale

6.3.3 Commenti

Sono supportate due forme di commenti: commenti delimitati e commenti a riga singola.

Un commento delimitato inizia con i caratteri /* e termina con i caratteri */. I commenti delimitati possono occupare una parte di una riga, una singola riga o più righe.

Esempio: esempio

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

include un commento delimitato.

esempio finale

Un commento a riga singola inizia con i caratteri // ed estende fino alla fine della riga.

Esempio: esempio

// 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");
    }
}

mostra vari commenti su riga singola.

esempio finale

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
    ;

I commenti non vengono annidati. Le sequenze di caratteri /* e */ non hanno un significato speciale in un commento su riga singola, così come le sequenze di caratteri // e /* non hanno un significato speciale in un commento delimitato.

I commenti non vengono elaborati all'interno di valori letterali di tipo carattere e stringa.

Nota: queste regole devono essere interpretate attentamente. Nell'esempio seguente, ad esempio, il commento delimitato che inizia prima A di terminare tra B e C(). Il motivo è che

// B */ C();

non è in realtà un commento a riga singola, poiché // non ha alcun significato speciale all'interno di un commento delimitato, e quindi */ ha il suo significato speciale consueto in tale riga.

Analogamente, il commento delimitato inizia prima di terminare prima DEdi . Il motivo è che "D */ " in realtà non è un valore letterale stringa, poiché il carattere virgolette doppie iniziale viene visualizzato all'interno di un commento delimitato.

Una conseguenza utile di e /* non avere un significato speciale all'interno di */ un commento a riga singola è che un blocco di righe di codice sorgente può essere commentato inserendo // all'inizio di ogni riga. In generale, non funziona mettere /* prima di tali righe e */ dopo di esse, poiché questo non incapsula correttamente i commenti delimitati nel blocco e in generale può modificare completamente la struttura di tali commenti delimitati.

Codice di esempio:

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

nota finale

Single_Line_Comment e Delimited_Commentche hanno formati specifici possono essere usati come commenti della documentazione, come descritto in §D.

6.3.4 Spazio vuoto

Lo spazio vuoto viene definito come qualsiasi carattere con la classe Unicode Zs (che include lo spazio) e il carattere di tabulazione orizzontale, il carattere di tabulazione verticale e il carattere del feed di moduli.

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

6.4 Token

6.4.1 Generale

Esistono diversi tipi di token: identificatori, parole chiave, valori letterali, operatori e segni di punteggiatura. Gli spazi vuoti e i commenti non sono token, anche se fungono da separatori per i token.

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

Nota: si tratta di una regola del parser ANTLR, non definisce un token lessicale, ma piuttosto la raccolta di tipi di token. nota finale

6.4.2 Sequenze di escape dei caratteri Unicode

Una sequenza di escape Unicode rappresenta un punto di codice Unicode. Le sequenze di escape Unicode vengono elaborate negli identificatori (§6.4.3), nei valori letterali carattere (§6.4.5.5), nei valori letterali stringa regolari (§6.4.5.6) e nelle espressioni di stringa regolari interpolate (§12.8.3). Una sequenza di escape Unicode non viene elaborata in nessun'altra posizione, ad esempio per formare un operatore, un punctuator o una parola chiave.

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
    ;

Una sequenza di escape carattere Unicode rappresenta il singolo punto di codice Unicode formato dal numero esadecimale che segue i caratteri "\u" o "\U". Poiché C# usa una codifica a 16 bit di punti di codice Unicode in valori di caratteri e stringhe, un punto di codice Unicode nell'intervallo U+10000 a U+10FFFF viene rappresentato usando due unità di codice surrogato Unicode. I punti di codice Unicode precedenti U+FFFF non sono consentiti nei valori letterali carattere. I punti di codice Unicode precedenti U+10FFFF non sono validi e non sono supportati.

Non vengono eseguite più traduzioni. Ad esempio, il valore letterale "\u005Cu005C" stringa è equivalente a anziché "\u005C"a "\" .

Nota: il valore \u005C Unicode è il carattere "\". nota finale

Esempio: esempio

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

mostra diversi usi di , ovvero la sequenza di \u0066escape per la lettera "f". Il programma equivale a

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

esempio finale

6.4.3 Identificatori

Le regole per gli identificatori specificati in questa sottochiave corrispondono esattamente a quelle consigliate dall'allegato standard Unicode 15, ad eccezione del fatto che il carattere di sottolineatura è consentito come carattere iniziale (come è tradizionale nel linguaggio di programmazione C), le sequenze di escape Unicode sono consentite negli identificatori e il carattere "@" è consentito come prefisso per consentire l'uso delle parole chiave come identificatori.

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
    ;

Nota:

  • Per informazioni sulle classi di caratteri Unicode indicate in precedenza, vedere Lo standard Unicode.
  • Il frammento Available_Identifier richiede l'esclusione di parole chiave e parole chiave contestuali. Se la grammatica in questa specifica viene elaborata con ANTLR, questa esclusione viene gestita automaticamente dalla semantica di ANTLR:
    • Le parole chiave e le parole chiave contestuali si verificano nella grammatica come stringhe letterali.
    • ANTLR crea regole di token lessicali implicite create da queste stringhe letterali.
    • ANTLR considera queste regole implicite prima delle regole lessicali esplicite nella grammatica.
    • Di conseguenza, il frammento Available_Identifier non corrisponderà a parole chiave o parole chiave contestuali come regole lessicali per quelle che lo precedono.
  • Il frammento Escaped_Identifier include parole chiave escape e parole chiave contestuali perché fanno parte del token più lungo a partire da un'elaborazione @ lessicale e costituisce sempre l'elemento lessicale più lungo possibile (§6.3.1).
  • Il modo in cui un'implementazione applica le restrizioni per i valori Unicode_Escape_Sequence consentiti è un problema di implementazione.

nota finale

Esempio: esempi di identificatori validi sono identifier1, _identifier2e @if. esempio finale

Un identificatore in un programma conforme deve essere nel formato canonico definito dal Formato di normalizzazione Unicode C, come definito dall'allegato standard Unicode 15. Il comportamento quando si verifica un identificatore non nel formato di normalizzazione C è definito dall'implementazione; Tuttavia, non è necessaria una diagnostica.

Il prefisso "@" consente l'uso di parole chiave come identificatori, utile quando si interagisce con altri linguaggi di programmazione. Il carattere @ non fa effettivamente parte dell'identificatore, quindi l'identificatore potrebbe essere visualizzato in altre lingue come identificatore normale, senza il prefisso. Un identificatore con un @ prefisso viene chiamato identificatore verbatim.

Nota: l'uso @ del prefisso per gli identificatori che non sono parole chiave è consentito, ma fortemente sconsigliato in materia di stile. nota finale

Esempio: Esempio:

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);
    }
}

definisce una classe denominata "class" con un metodo statico denominato "static" che accetta un parametro denominato "bool". Si noti che poiché i caratteri di escape Unicode non sono consentiti nelle parole chiave, il token "cl\u0061ss" è un identificatore ed è lo stesso identificatore di "@class".

esempio finale

Due identificatori vengono considerati uguali se sono identici dopo l'applicazione delle trasformazioni seguenti, in ordine:

  • Il prefisso "@", se usato, viene rimosso.
  • Ogni Unicode_Escape_Sequence viene trasformato nel carattere Unicode corrispondente.
  • Eventuali Formatting_Charactervengono rimossi.

La semantica di un identificatore denominato _ dipende dal contesto in cui viene visualizzato:

  • Può indicare un elemento programma denominato, ad esempio una variabile, una classe o un metodo o
  • Può indicare uno scarto (§9.2.9.2).

Gli identificatori contenenti due caratteri di sottolineatura consecutivi (U+005F) sono riservati per l'uso da parte dell'implementazione. Tuttavia, non è necessaria alcuna diagnostica se tale identificatore è definito.

Nota: ad esempio, un'implementazione potrebbe fornire parole chiave estese che iniziano con due caratteri di sottolineatura. nota finale

6.4.4 Parole chiave

Una parola chiave è una sequenza di caratteri simile all'identificatore riservata e non può essere usata come identificatore tranne quando è preceduto dal @ carattere.

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'
    ;

Una parola chiave contestuale è una sequenza di caratteri simile a un identificatore che ha un significato speciale in determinati contesti, ma non è riservata e può essere usata come identificatore al di fuori di tali contesti, nonché quando preceduto dal @ carattere.

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

Nota: la parola chiave rules e contextual_keyword sono regole del parser perché non introducono nuovi tipi di token. Tutte le parole chiave e le parole chiave contestuali sono definite da regole lessicali implicite che si verificano come stringhe letterali nella grammatica (§6.2.3). nota finale

Nella maggior parte dei casi, la posizione sintattica delle parole chiave contestuali è tale che non possono mai essere confuse con l'utilizzo ordinario degli identificatori. Ad esempio, all'interno di una dichiarazione di proprietà, gli get identificatori e set hanno un significato speciale (§15.7.3). Un identificatore diverso da get o set non è mai consentito in queste posizioni, pertanto questo uso non è in conflitto con l'uso di queste parole come identificatori.

In alcuni casi la grammatica non è sufficiente per distinguere l'utilizzo delle parole chiave contestuali dagli identificatori. In tutti questi casi verrà specificato come disambiguare tra i due. Ad esempio, la parola chiave var contestuale nelle dichiarazioni di variabili locali tipizzate in modo implicito (§13.6.2) potrebbe essere in conflitto con un tipo dichiarato denominato var, nel qual caso il nome dichiarato ha la precedenza sull'uso dell'identificatore come parola chiave contestuale.

Un altro esempio di disambiguazione è la parola chiave await contestuale (§12.9.8.1), considerata una parola chiave solo quando si trova all'interno di un metodo dichiarato async, ma può essere usata come identificatore altrove.

Analogamente alle parole chiave, le parole chiave contestuali possono essere usate come identificatori ordinari anteponendo loro il @ carattere.

Nota: se usato come parole chiave contestuali, questi identificatori non possono contenere Unicode_Escape_Sequences. nota finale

6.4.5 Valori letterali

6.4.5.1 Generale

Un valore letterale (§12.8.2) è una rappresentazione del codice sorgente di un valore.

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

Nota: il valore letterale è una regola del parser perché raggruppa altri tipi di token e non introduce un nuovo tipo di token. nota finale

6.4.5.2 Valori letterali booleani

Esistono due valori letterali booleani: true e false.

boolean_literal
    : TRUE
    | FALSE
    ;

Nota: boolean_literal è una regola del parser perché raggruppa altri tipi di token e non introduce un nuovo tipo di token. nota finale

Il tipo di un boolean_literal è bool.

6.4.5.3 Valori letterali integer

I valori letterali integer vengono usati per scrivere valori di tipi int, uint, longe ulong. I valori letterali integer hanno tre possibili forme: decimal, esadecimale e binaria.

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'
    ;

Il tipo di un valore letterale integer viene determinato nel modo seguente:

  • Se il valore letterale non ha alcun suffisso, ha il primo di questi tipi in cui può essere rappresentato il relativo valore: int, uint, long, ulong.
  • Se il valore letterale è suffisso da U o u, ha il primo di questi tipi in cui il valore può essere rappresentato: uint, ulong.
  • Se il valore letterale è suffisso da L o l, ha il primo di questi tipi in cui il valore può essere rappresentato: long, ulong.
  • Se il valore letterale è suffisso da UL, Ul, uLul, LU, Lu, lU, o lu, è di tipo ulong.

Se il valore rappresentato da un valore letterale integer non rientra nell'intervallo del ulong tipo, si verifica un errore in fase di compilazione.

Nota: per quanto riguarda lo stile, è consigliabile usare "L" anziché "l" quando si scrivono valori letterali di tipo long, poiché è facile confondere la lettera "l" con la cifra "1". nota finale

Per consentire la scrittura dei valori più piccoli possibili int e long come valori letterali integer, esistono le due regole seguenti:

  • Quando un Integer_Literal che rappresenta il valore 2147483648 (2¹¹) e non viene visualizzato alcun Integer_Type_Suffix come token immediatamente successivo a un token operatore unario meno (§12.9.3), il risultato (di entrambi i token) è una costante di tipo int con il valore −2147483648 (−2","). In tutte le altre situazioni, tale Integer_Literal è di tipo uint.
  • Quando un Integer_Literal che rappresenta il valore 9223372036854775808 (2⁶²) e nessun Integer_Type_Suffix o il Integer_Type_SuffixL o l viene visualizzato come token immediatamente dopo un token meno unario (§12.9.3), il risultato (di entrambi i token) è una costante di tipo long con il valore −9223372036854775808 (−2⁶"). In tutte le altre situazioni, tale Integer_Literal è di tipo ulong.

Esempio:

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

esempio finale

6.4.5.4 Valori letterali reali

I valori letterali reali vengono usati per scrivere valori di tipi float, doublee 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'
    ;

Se non viene specificato alcun Real_Type_Suffix , il tipo del Real_Literal è double. In caso contrario, il Real_Type_Suffix determina il tipo di valore letterale reale, come indicato di seguito:

  • Un valore letterale reale suffisso da F o f è di tipo float.

    Esempio: i valori letterali 1f, 1.5f, 1e10fe 123.456F sono tutti di tipo float. esempio finale

  • Un valore letterale reale suffisso da D o d è di tipo double.

    Esempio: i valori letterali 1d, 1.5d, 1e10de 123.456D sono tutti di tipo double. esempio finale

  • Un valore letterale reale suffisso da M o m è di tipo decimal.

    Esempio: i valori letterali 1m, 1.5m, 1e10me 123.456M sono tutti di tipo decimal. esempio finale
    Questo valore letterale viene convertito in un decimal valore prendendo il valore esatto e, se necessario, arrotondando al valore rappresentabile più vicino utilizzando l'arrotondamento del banchiere (§8.3.8). Qualsiasi scala apparente nel valore letterale viene mantenuta a meno che il valore non venga arrotondato. Nota: di conseguenza, il valore letterale 2.900m verrà analizzato per formare con il decimal segno 0, coefficiente 2900e scala 3. nota finale

Se la grandezza del valore letterale specificato è troppo grande per essere rappresentata nel tipo indicato, si verifica un errore in fase di compilazione.

Nota: in particolare, un Real_Literal non produrrà mai un infinito a virgola mobile. Un Real_Literal diverso da zero può tuttavia essere arrotondato a zero. nota finale

Il valore di un valore letterale reale di tipo float o double è determinato usando la modalità IEC 60559 "round to nearest" con legami interrotti a "even" (un valore con lo zero bit meno significativo) e tutte le cifre considerate significative.

Nota: in un valore letterale reale, le cifre decimali sono sempre necessarie dopo il separatore decimale. Ad esempio, 1.3F è un valore letterale reale, ma 1.F non lo è. nota finale

Esempio:

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

esempio finale

6.4.5.5 Valori letterali carattere

Un valore letterale carattere rappresenta un singolo carattere ed è costituito da un carattere tra virgolette, come in '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?
    ;

Nota: un carattere che segue un carattere barra rovesciata (\) in un carattere deve essere uno dei caratteri seguenti: ', ", \0ab, , . fnrtuUxv In caso contrario, si verifica un errore in fase di compilazione. nota finale

Nota: l'uso \xdella Hexadecimal_Escape_Sequence produzione può essere soggetto a errori e difficile da leggere a causa del numero variabile di cifre esadecimali che seguono .\x Ad esempio, nel codice:

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

potrebbe apparire in un primo momento che il carattere iniziale è lo stesso (U+0009, un carattere di tabulazioni) in entrambe le stringhe. Infatti la seconda stringa inizia con U+9BAD come tutte e tre le lettere nella parola "Bad" sono cifre esadecimali valide. Per quanto riguarda lo stile, è consigliabile \x evitare a favore di sequenze di escape specifiche (\t in questo esempio) o della sequenza di escape a lunghezza \u fissa.

nota finale

Una sequenza di escape esadecimale rappresenta una singola unità di codice Unicode UTF-16, con il valore formato dal numero esadecimale che segue "\x".

Se il valore rappresentato da un valore letterale carattere è maggiore di U+FFFF, si verifica un errore in fase di compilazione.

Una sequenza di escape Unicode (§6.4.2) in un valore letterale carattere deve trovarsi nell'intervallo U+0000 a U+FFFF.

Una sequenza di escape semplice rappresenta un carattere Unicode, come descritto nella tabella seguente.

Sequenza di escape Nome carattere Punto di codice Unicode
\' Virgoletta singola U + 0027
\" Virgoletta doppia U + 0022
\\ Barra rovesciata U + 005C
\0 Null U+0000
\a Alert U+0007
\b Backspace U + 0008
\f Avanzamento carta U + 000C
\n Nuova riga U + 000A
\r Ritorno a capo U + 000D
\t Tabulazione orizzontale U + 0009
\v Tabulazione verticale U+000B

Il tipo di un Character_Literal è char.

6.4.5.6 Valori letterali stringa

C# supporta due forme di valori letterali stringa: valori letterali stringa regolari e valori letterali stringa verbatim. Un valore letterale stringa regolare è costituito da zero o più caratteri racchiusi tra virgolette doppie, come in "hello"e può includere sia sequenze di escape semplici ,ad esempio \t per il carattere di tabulazione, sia sequenze di escape esadecimali e Unicode.

Un valore letterale stringa verbatim è costituito da un @ carattere seguito da un carattere virgolette doppie, da zero o più caratteri e da un carattere di virgolette doppie di chiusura.

Esempio: un semplice esempio è @"hello". esempio finale

In un valore letterale stringa verbatim, i caratteri tra i delimitatori vengono interpretati verbatim, con l'unica eccezione rappresentata da un Quote_Escape_Sequence, che rappresenta un carattere di virgolette doppie. In particolare, le sequenze di escape semplici e le sequenze di escape esadecimali e Unicode non vengono elaborate in valori letterali stringa verbatim. Un valore letterale stringa verbatim può estendersi su più righe.

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
    : '""'
    ;

Esempio: esempio

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";

mostra un'ampia gamma di valori letterali stringa. L'ultimo valore letterale stringa, j, è un valore letterale stringa verbatim che si estende su più righe. I caratteri tra virgolette, inclusi gli spazi vuoti, ad esempio i nuovi caratteri di riga, vengono mantenuti verbatim e ogni coppia di caratteri virgolette doppie viene sostituita da un carattere di questo tipo.

esempio finale

Nota: qualsiasi interruzione di riga all'interno di valori letterali stringa verbatim fa parte della stringa risultante. Se i caratteri esatti usati per formare interruzioni di riga sono semanticamente rilevanti per un'applicazione, tutti gli strumenti che convertono interruzioni di riga nel codice sorgente in formati diversi (tra "\n" e "\r\n", ad esempio) modificheranno il comportamento dell'applicazione. Gli sviluppatori devono prestare attenzione in tali situazioni. nota finale

Nota: poiché una sequenza di escape esadecimale può avere un numero variabile di cifre esadecimali, il valore letterale "\x123" stringa contiene un singolo carattere con valore 123esadecimale . Per creare una stringa contenente il carattere con valore 12 esadecimale seguito dal carattere 3, è possibile scrivere "\x00123" o "\x12" + "3" invece. nota finale

Il tipo di un String_Literal è string.

Ogni valore letterale stringa non comporta necessariamente una nuova istanza di stringa. Quando due o più valori letterali stringa equivalenti in base all'operatore di uguaglianza di stringhe (§12.12.8), vengono visualizzati nello stesso assembly, questi valori letterali stringa fanno riferimento alla stessa istanza di stringa.

Esempio: ad esempio, l'output prodotto da

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

è True perché i due valori letterali fanno riferimento alla stessa istanza di stringa.

esempio finale

6.4.5.7 Valore letterale Null

null_literal
    : NULL
    ;

Nota: null_literal è una regola del parser perché non introduce un nuovo tipo di token. nota finale

Un null_literal rappresenta un null valore. Non dispone di un tipo, ma può essere convertito in qualsiasi tipo riferimento o valore nullable tramite una conversione letterale Null (§10.2.7).

6.4.6 Operatori e segni di punteggiatura

Esistono diversi tipi di operatori e segni di punteggiatura. Gli operatori vengono usati nelle espressioni per descrivere le operazioni che includono uno o più operandi.

Esempio: l'espressione a + b usa l'operatore + per aggiungere i due operandi a e b. esempio finale

I segni di punteggiatura consentono invece di raggruppare e separare.

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

right_shift
    : '>'  '>'
    ;

right_shift_assignment
    : '>' '>='
    ;

Nota: right_shift e right_shift_assignment sono regole del parser perché non introducono un nuovo tipo di token, ma rappresentano una sequenza di due token. La regola operator_or_punctuator esiste solo a scopo descrittivo e non viene usata altrove nella grammatica. nota finale

right_shift è costituito dai due token > e >. Analogamente, right_shift_assignment è costituito dai due token > e >=. A differenza di altre produzioni nella grammatica sintattica, non sono consentiti caratteri di alcun tipo (nemmeno spazi vuoti) tra i due token in ognuna di queste produzioni. Queste produzioni vengono trattate appositamente per consentire la corretta gestione delle type_parameter_lists (§15.2.3).

Nota: prima dell'aggiunta di generics a C# >> e >>= erano entrambi token singoli. Tuttavia, la sintassi per i generics usa i < caratteri e > per delimitare i parametri di tipo e gli argomenti di tipo. Spesso è consigliabile usare tipi costruiti annidati, ad esempio List<Dictionary<string, int>>. Invece di richiedere al programmatore di separare > e > da uno spazio, la definizione dei due operator_or_punctuators è stata modificata. nota finale

6.5 Direttive di pre-elaborazione

6.5.1 Generale

Le direttive di pre-elaborazione consentono di ignorare in modo condizionale le sezioni delle unità di compilazione, di segnalare le condizioni di errore e di avviso, di delineare aree distinte del codice sorgente e di impostare il contesto nullable.

Nota: il termine "direttive di pre-elaborazione" viene usato solo per coerenza con i linguaggi di programmazione C e C++. In C# non esiste un passaggio di pre-elaborazione separato; Le direttive di pre-elaborazione vengono elaborate come parte della fase di analisi lessicale. nota finale

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
    ;

Nota:

  • La grammatica preprocessore definisce un singolo token PP_Directive lessicale usato per tutte le direttive di pre-elaborazione. La semantica di ognuna delle direttive di pre-elaborazione è definita in questa specifica del linguaggio, ma non come implementarle.
  • Il PP_Start frammento deve essere riconosciuto solo all'inizio di una riga, il getCharPositionInLine() == 0 predicato lessicale ANTLR precedente suggerisce un modo in cui questo può essere raggiunto ed è informativo solo, un'implementazione può utilizzare una strategia diversa.

nota finale

Sono disponibili le direttive di pre-elaborazione seguenti:

  • #define e #undef, usati rispettivamente per definire e annullare la definizione dei simboli di compilazione condizionale (§6.5.4).
  • #if, #elif, #elsee #endif, che vengono usati per ignorare sezioni condizionali del codice sorgente (§6.5.5).
  • #line, usato per controllare i numeri di riga generati per gli errori e gli avvisi (§6.5.8).
  • #error, usato per emettere errori (§6.5.6).
  • #region e #endregion, utilizzati per contrassegnare in modo esplicito le sezioni del codice sorgente (§6.5.7).
  • #nullable, utilizzato per specificare il contesto nullable (§6.5.9).
  • #pragma, utilizzato per specificare informazioni contestuali facoltative per un compilatore (§6.5.10).

Una direttiva di pre-elaborazione occupa sempre una riga separata di codice sorgente e inizia sempre con un carattere e un # nome di direttiva di pre-elaborazione. Lo spazio vuoto può verificarsi prima del # carattere e tra il # carattere e il nome della direttiva.

Una riga di origine contenente una #definedirettiva , #undef#if, #elif, #else#endif#line#endregiono #nullable può terminare con un commento a riga singola. I commenti delimitati (lo /* */ stile dei commenti) non sono consentiti nelle righe di origine contenenti direttive di pre-elaborazione.

Le direttive di pre-elaborazione non fanno parte della grammatica sintattica di C#. Tuttavia, le direttive di pre-elaborazione possono essere usate per includere o escludere sequenze di token e in questo modo influiscono sul significato di un programma C#.

Esempio: durante la compilazione, il programma

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

restituisce la stessa sequenza di token del programma

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

Pertanto, mentre lessicalmente, i due programmi sono molto diversi, sintatticamente, sono identici.

esempio finale

6.5.2 Simboli di compilazione condizionale

La funzionalità di compilazione condizionale fornita dalle #ifdirettive , #elif, #elsee #endif è controllata tramite espressioni di pre-elaborazione (§6.5.3) e simboli di compilazione condizionale.

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

Nota Come un'implementazione applica la restrizione ai valori Basic_Identifier consentiti è un problema di implementazione. nota finale

Due simboli di compilazione condizionale vengono considerati uguali se sono identici dopo l'applicazione delle trasformazioni seguenti, in ordine:

  • Ogni Unicode_Escape_Sequence viene trasformato nel carattere Unicode corrispondente.
  • Tutte le Formatting_Characters vengono rimosse.

Un simbolo di compilazione condizionale ha due possibili stati: definiti o non definiti. All'inizio dell'elaborazione lessicale di un'unità di compilazione, un simbolo di compilazione condizionale non è definito a meno che non sia stato definito in modo esplicito da un meccanismo esterno (ad esempio un'opzione del compilatore della riga di comando). Quando viene elaborata una #define direttiva, il simbolo di compilazione condizionale denominato in tale direttiva diventa definito nell'unità di compilazione. Il simbolo rimane definito fino a quando non viene elaborata una #undef direttiva per lo stesso simbolo o fino al raggiungimento della fine dell'unità di compilazione. Un'implicazione di questo è che #define e #undef le direttive in un'unità di compilazione non hanno alcun effetto su altre unità di compilazione nello stesso programma.

Quando viene fatto riferimento in un'espressione di pre-elaborazione (§6.5.3), un simbolo di compilazione condizionale definito ha il valore truebooleano e un simbolo di compilazione condizionale non definito ha il valore falsebooleano . Non è necessario dichiarare in modo esplicito i simboli di compilazione condizionale prima di farvi riferimento nelle espressioni di pre-elaborazione. I simboli non dichiarati sono invece semplicemente indefiniti e quindi hanno il valore false.

Lo spazio dei nomi per i simboli di compilazione condizionale è distinto e separato da tutte le altre entità denominate in un programma C#. È possibile fare riferimento ai simboli di compilazione condizionale solo nelle #define direttive e #undef nelle espressioni di pre-elaborazione.

6.5.3 Pre-elaborazione delle espressioni

Le espressioni di pre-elaborazione possono verificarsi in #if direttive e #elif . Gli operatori ! (solo negazione logica del prefisso), ==, !=&&, e || sono consentiti nelle espressioni di pre-elaborazione e le parentesi possono essere usate per il raggruppamento.

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? ')'
    ;

Quando viene fatto riferimento in un'espressione di pre-elaborazione, un simbolo di compilazione condizionale definito ha il valore truebooleano e un simbolo di compilazione condizionale non definito ha il valore falsebooleano .

La valutazione di un'espressione di pre-elaborazione restituisce sempre un valore booleano. Le regole di valutazione per un'espressione di pre-elaborazione sono identiche a quelle per un'espressione costante (§12.23), ad eccezione del fatto che le uniche entità definite dall'utente a cui è possibile fare riferimento sono simboli di compilazione condizionale.

6.5.4 Direttive di definizione

Le direttive di definizione vengono usate per definire o annullare la definizione dei simboli di compilazione condizionale.

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

L'elaborazione di una #define direttiva determina la definizione del simbolo di compilazione condizionale specificato, a partire dalla riga di origine che segue la direttiva . Analogamente, l'elaborazione di una #undef direttiva fa sì che il simbolo di compilazione condizionale specificato diventi indefinito, a partire dalla riga di origine che segue la direttiva .

Qualsiasi #define direttiva e #undef in un'unità di compilazione deve avvenire prima del primo token (§6.4) nell'unità di compilazione; in caso contrario, si verifica un errore in fase di compilazione. In termini intuitivi, #define e #undef le direttive precedono qualsiasi "codice reale" nell'unità di compilazione.

Esempio: Esempio:

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

è valido perché le #define direttive precedono il primo token (parola namespace chiave) nell'unità di compilazione.

esempio finale

Esempio: l'esempio seguente genera un errore in fase di compilazione perché un #define segue il codice reale:

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

esempio finale

Un #define oggetto può definire un simbolo di compilazione condizionale già definito, senza che vi sia alcun intervento #undef per tale simbolo.

Esempio: l'esempio seguente definisce un simbolo di compilazione condizionale A e quindi lo definisce di nuovo.

#define A
#define A

Per i compilatori che consentono di definire i simboli di compilazione condizionale come opzioni di compilazione, un modo alternativo per tale ridefinizione consiste nel definire il simbolo come opzione del compilatore e nell'origine.

esempio finale

Un #undef può "undefine" un simbolo di compilazione condizionale non definito.

Esempio: l'esempio seguente definisce un simbolo A di compilazione condizionale e lo annulla due volte. Anche se il secondo #undef non ha alcun effetto, è ancora valido.

#define A
#undef A
#undef A

esempio finale

6.5.5 Direttive di compilazione condizionale

Le direttive di compilazione condizionale vengono usate per includere o escludere in modo condizionale parti di un'unità di compilazione.

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'
    ;

Le direttive di compilazione condizionale devono essere scritte in gruppi costituiti da, in ordine, una #if direttiva, zero o più #elif direttive, zero o una #else direttiva e una #endif direttiva. Tra le direttive sono presenti sezioni condizionali del codice sorgente. Ogni sezione è controllata dalla direttiva immediatamente precedente. Una sezione condizionale può contenere direttive di compilazione condizionale annidate, a condizione che queste direttive formino gruppi completi.

Al massimo una delle sezioni condizionali contenute è selezionata per l'elaborazione lessicale normale:

  • I PP_Expressiondelle #if direttive e #elif vengono valutati in ordine fino a quando non viene restituito true. Se un'espressione restituisce true, viene selezionata la sezione condizionale che segue la direttiva corrispondente.
  • Se tutte le #elsedirettiva .
  • In caso contrario, non è selezionata alcuna sezione condizionale.

La sezione condizionale selezionata, se presente, viene elaborata come normale input_section: il codice sorgente contenuto nella sezione deve rispettare la grammatica lessicale; i token vengono generati dal codice sorgente nella sezione e le direttive di pre-elaborazione nella sezione hanno gli effetti prescritti.

Tutte le sezioni condizionali rimanenti vengono ignorate e non vengono generati token, ad eccezione di quelli per le direttive di pre-elaborazione, dal codice sorgente. Pertanto, il codice sorgente ignorato, ad eccezione delle direttive di pre-elaborazione, può essere lessicalmente errato. Le direttive di pre-elaborazione ignorate devono essere lessicalmente corrette, ma non vengono elaborate in altro modo. All'interno di una sezione condizionale che viene ignorata anche qualsiasi sezione condizionale annidata (contenuta in costrutti annidati) viene ignorata #if...#endif .

Nota: la grammatica precedente non acquisisce la tolleranza che le sezioni condizionali tra le direttive di pre-elaborazione possono essere lessicalmente in formato non corretto. Pertanto, la grammatica non è pronta per ANTLR perché supporta solo input lessicalmente corretto. nota finale

Esempio: l'esempio seguente illustra il modo in cui le direttive di compilazione condizionale possono annidare:

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

Ad eccezione delle direttive di pre-elaborazione, il codice sorgente ignorato non è soggetto all'analisi lessicale. Ad esempio, il codice seguente è valido nonostante il commento non eliminato nella #else sezione :

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

Si noti, tuttavia, che le direttive di pre-elaborazione devono essere corrette in modo lessicale anche nelle sezioni ignorate del codice sorgente.

Le direttive di pre-elaborazione non vengono elaborate quando vengono visualizzate all'interno di elementi di input su più righe. Ad esempio, il programma:

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

restituisce l'output:

hello,
#if Debug
        world
#else
        Nebraska
#endif

In casi particolari, il set di direttive di pre-elaborazione elaborate può dipendere dalla valutazione del pp_expression. Esempio:

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

produce sempre lo stesso flusso di token (classQ{} ), indipendentemente dal fatto che sia definito o meno.X Se X è definito, le uniche direttive elaborate sono #if e #endif, a causa del commento su più righe. Se X non è definito, tre direttive (#if, #else, #endif) fanno parte del set di direttive.

esempio finale

6.5.6 Direttive di diagnostica

Le direttive di diagnostica vengono usate per generare messaggi di errore e di avviso espliciti segnalati nello stesso modo degli altri errori e avvisi in fase di compilazione.

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

fragment PP_Message
    : PP_Whitespace Input_Character*
    ;

Esempio: esempio

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

genera un errore in fase di compilazione ("Una compilazione non può essere sia di debug che di vendita al dettaglio") se i simboli Debug di compilazione condizionale e Retail sono entrambi definiti. Si noti che un PP_Message può contenere testo arbitrario; in particolare, non deve contenere token ben formati, come illustrato dalla virgoletta singola nella parola can't.

esempio finale

Direttive dell'area 6.5.7

Le direttive region vengono usate per contrassegnare in modo esplicito le aree del codice sorgente.

fragment PP_Region
    : PP_Start_Region
    | PP_End_Region
    ;

fragment PP_Start_Region
    : 'region' PP_Message?
    ;

fragment PP_End_Region
    : 'endregion' PP_Message?
    ;

Nessun significato semantico è associato a un'area; le aree sono destinate all'uso da parte del programmatore o da strumenti automatizzati per contrassegnare una sezione del codice sorgente. Esiste una #endregion direttiva che corrisponde a ogni #region direttiva. Il messaggio specificato in una #region direttiva o #endregion non ha un significato semantico, ma serve semplicemente per identificare l'area. La corrispondenza #region e #endregion le direttive possono avere PP_Messagediversi.

Elaborazione lessicale di un'area:

#region
...
#endregion

corrisponde esattamente all'elaborazione lessicale di una direttiva di compilazione condizionale del modulo:

#if true
...
#endif

Nota: ciò significa che un'area può includere uno o più #if/.../#endifo essere contenuti con una sezione condizionale all'interno di un #if/.../#endif; ma un'area non può sovrapporsi a una sola parte di / #if.../#endif, o start & end in sezioni condizionali diverse. nota finale

6.5.8 Direttive line

Le direttive line possono essere utilizzate per modificare i numeri di riga e i nomi delle unità di compilazione riportati da un compilatore nell'output, come avvisi ed errori. Questi valori vengono usati anche dagli attributi caller-info (§22.5.6).

Nota: le direttive line sono più comunemente usate negli strumenti di meta-programmazione che generano codice sorgente C# da un altro input di testo. nota finale

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' | '"')
    ;

Quando non sono presenti direttive #line, un compilatore riporta numeri di riga reali e nomi delle unità di compilazione nell'output. Quando si elabora una direttiva #line che include un PP_Line_Indicator che non è default, un compilatore considera la riga dopo la direttiva come il numero di riga specificato (e il nome dell'unità di compilazione, se specificato).

Il valore massimo consentito per Decimal_Digit+ è definito dall'implementazione.

Una #line default direttiva annulla l'effetto di tutte le direttive precedenti #line . Un compilatore segnala informazioni precise sulle righe seguenti, esattamente come se non fosse stata elaborata alcuna direttiva #line.

Una #line hidden direttiva non ha alcun effetto sull'unità di compilazione e sui numeri di riga segnalati nei messaggi di errore o prodotti dall'uso di CallerLineNumberAttribute (§22.5.6.2). È progettato per influire sugli strumenti di debug a livello di origine in modo che, durante il debug, tutte le righe tra una #line hidden direttiva e la direttiva successiva #line (che non #line hiddensia ) non abbiano informazioni sul numero di riga e vengano ignorate interamente durante l'esecuzione del codice.

Nota: anche se un PP_Compilation_Unit_Name potrebbe contenere testo simile a una sequenza di escape, tale testo non è una sequenza di escape. In questo contesto un carattere '\' designa semplicemente un carattere barra rovesciata normale. nota finale

6.5.9 Direttiva nullable

La direttiva nullable controlla il contesto nullable, come descritto di seguito.

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'
    ;

Una direttiva nullable imposta i flag disponibili per le righe di codice successive, fino a quando non ne esegue l'override un'altra direttiva nullable o fino al raggiungimento della fine del _unit di compilazione. Il contesto nullable contiene due flag: annotazioni e avvisi. L'effetto di ogni forma di direttiva nullable è il seguente:

  • #nullable disable: disabilita le annotazioni nullable e i flag di avviso nullable.
  • #nullable enable: abilita sia annotazioni nullable che flag di avviso nullable.
  • #nullable restore: ripristina sia le annotazioni che i flag di avviso allo stato specificato dal meccanismo esterno, se presente.
  • #nullable disable annotations: disabilita il flag di annotazioni nullable. Il flag di avvisi nullable non è interessato.
  • #nullable enable annotations: abilita il flag di annotazioni nullable. Il flag di avvisi nullable non è interessato.
  • #nullable restore annotations: ripristina il flag di annotazioni nullable allo stato specificato dal meccanismo esterno, se presente. Il flag di avvisi nullable non è interessato.
  • #nullable disable warnings: disabilita il flag di avvisi nullable. Il flag di annotazioni nullable non è interessato.
  • #nullable enable warnings: abilita il flag di avvisi nullable. Il flag di annotazioni nullable non è interessato.
  • #nullable restore warnings: ripristina il flag di avvisi nullable allo stato specificato dal meccanismo esterno, se presente. Il flag di annotazioni nullable non è interessato.

Lo stato nullable delle espressioni viene monitorato sempre. Lo stato del flag di annotazione e la presenza o l'assenza di un'annotazione nullable, ?, determina lo stato null iniziale di una dichiarazione di variabile. Gli avvisi vengono generati solo quando il flag di avviso è abilitato.

Esempio: esempio

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

genera un avviso in fase di compilazione ("così come x è null"). Lo stato nullable di x viene rilevato ovunque. Quando il flag di avvisi è abilitato, viene generato un avviso.

esempio finale

6.5.10 Direttive Pragma

La #pragma direttiva di pre-elaborazione viene usata per specificare le informazioni contestuali per un compilatore.

Nota: ad esempio, un compilatore potrebbe fornire #pragma direttive che

  • Abilitare o disabilitare determinati messaggi di avviso durante la compilazione del codice successivo.
  • Specificare le ottimizzazioni da applicare al codice successivo.
  • Specificare le informazioni da utilizzare da un debugger.

nota finale

fragment PP_Pragma
    : 'pragma' PP_Pragma_Text?
    ;

fragment PP_Pragma_Text
    : PP_Whitespace Input_Character*
    ;

I Input_Characternel PP_Pragma_Text sono interpretati da un compilatore in un modo definito dall'implementazione. Le informazioni fornite in una #pragma direttiva non modificano la semantica del programma. Una #pragma direttiva modifica solo il comportamento del compilatore che non rientra nell'ambito di questa specifica del linguaggio. Se un compilatore non riesce a interpretare il Input_Characters, un compilatore può generare un avviso; tuttavia, non genera un errore in fase di compilazione.

Nota: PP_Pragma_Text può contenere testo arbitrario, in particolare non deve contenere token ben formati. nota finale