6 词法结构

6.1 程序

C# 程序由一个或多个源文件组成,称为编译单元§14.2)。 虽然编译单元可能与文件系统中的文件具有一对一对应关系,但不需要此类通信。

从概念上讲,程序是使用三个步骤编译的:

  1. 转换,它将文件从特定字符存储库和编码方案转换为 Unicode 字符序列。
  2. 词法分析,用于将 Unicode 输入字符流转换为令牌流。
  3. 语法分析,它将令牌流转换为可执行代码。

符合标准的实现应接受使用 UTF-8 编码形式编码的 Unicode 编译单元(由 Unicode 标准定义),并将其转换为 Unicode 字符序列。 实现可以选择接受和转换其他字符编码方案(例如 UTF-16、UTF-32 或非 Unicode 字符映射)。

注意:Unicode NULL 字符(U+0000)的处理是实现定义的。 强烈建议开发人员避免在源代码中使用此字符,以便实现可移植性和可读性。 当字符或字符串文本中需要字符时,转义序列或\u0000可改用转义序列\0end note

注意:超出此规范的范围,可以定义如何使用 Unicode 以外的字符表示形式将文件转换为 Unicode 字符序列。 但是,在此类转换期间,建议将其他字符集中的常规换行符(或序列)转换为由 Unicode 回车符(U+000D)后接 Unicode 换行符(U+000A)组成的双字符序列。 在大多数情况下,此转换将没有任何可见效果;但是,它将影响逐字字符串文本标记(§6.4.5.6)的解释。 此建议的目的是允许逐字字符串文本在支持不同非 Unicode 字符集的系统之间移动时生成相同的字符序列,特别是那些使用不同字符序列进行换行的字符序列。 end note

6.2 语法

6.2.1 常规

此规范使用两种语法呈现 C# 编程语言的语法。 词法语法§6.2.3)定义 Unicode 字符如何组合成行终止符、空格、注释、标记和预处理指令。 语法§6.2.4)定义词法语法生成的标记如何组合成 C# 程序。

所有终端字符都将被理解为从 U+0020 到 U+007F 的相应 Unicode 字符,而不是其他 Unicode 字符范围中的任何类似字符。

6.2.2 语法表示法

词法和语法语法显示在 ANTLR 语法工具的扩展 Backus-Naur 形式中。

使用 ANTLR 表示法时,此规范没有为 C# 提供完整的 ANTLR 就绪“参考语法” ;手动或使用 ANTLR 等工具编写词法和分析器超出了语言规范的范围。 根据该限定,此规范尝试将指定语法与在 ANTLR 中生成词法和分析程序所需的差距降至最低。

ANTLR 通过以大写字母开头的词法规则和带小写字母分析器规则来区分词法和语法分析器,从而区分词法和语法。

注意:C# 词法语法(§6.2.3)和语法语法(§6.2.4)与 ANTLR 划分的词法和分析器语法不完全对应。 这种小不匹配意味着指定 C# 词法语法时会使用一些 ANTLR 分析器规则。 end note

6.2.3 词法语法

C# 的词法语法显示在 §6.3§6.4§6.5 中。 词法语法的终端符号是 Unicode 字符集的字符,词法语法指定字符如何组合为标记(§6.4)、空格(§6.3.4)、注释(§6.3.3)和预处理指令(§6.5)。

语法的许多终端符号未显式定义为词法语法中的标记。 相反,利用语法中的文本字符串提取为隐式词法标记的 ANTLR 行为;这允许关键字、运算符等通过语法中的文本表示形式而不是标记名称来表示。

C# 程序中的每个编译单元应符合词法语法(§6.3.1)的输入生产。

6.2.4 语法

C# 的语法语法显示在子句、子句和后跟此子句的附件中。 语法的终端符号是由词法语法显式定义的标记,由语法本身(§6.2.3)中的文本字符串隐式定义。 语法指定如何组合标记以形成 C# 程序。

C# 程序中的每个编译单元应符合 语法的 compilation_unit生产(§14.2)。

6.2.5 语法歧义

simple_name(§12.8.4)和member_access§12.8.7)的生产可能导致表达式语法中的歧义。

示例:语句:

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

可以解释为使用两个参数的 F 调用, G < A 以及 B > (7)。 或者,它可以解释为使用一个参数的 F 调用,即调用具有两个类型参数和一个常规参数的泛型方法 G

end 示例

如果可将令牌序列(在上下文中)分析为simple_name(§12.8.4)、member_access§12.8.7)或pointer_member_access§23.6.3),以type_argument_list§8.4.2)结束,则检查结束令牌后紧接的>令牌是否为

  • ( ) ] } : ; , . ? == != | ^ && || & [
  • 关系运算符 < <= >= is as之一;或
  • 查询表达式内显示的上下文查询关键字;或
  • 在某些上下文中, 标识符 被视为消除歧义的令牌。 这些上下文是在分析元组文本的第一个iscaseout元素时(在这种情况下,标记前面(或标识符后跟元,组文本)或:元组文本的后续元素时紧接被消除的标记序列。

如果以下令牌位于此列表或此类上下文中的标识符中,则会将type_argument_list保留为simple_name的一部分member_accesspointer_member访问,并且丢弃了令牌序列的任何其他可能分析。 否则,即使没有其他可能的令牌序列分析,type_argument_list也不被视为simple_namemember_accesspointer_member_access的一部分

注意:在分析namespace_or_type_name(§7.8)中的type_argument_list,不会应用这些规则。 end note

示例:语句:

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

根据此规则,将解释为使用一个参数调用 F ,即调用具有两个类型参数和一个常规参数的泛型方法 G 。 语句

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

将每个参数解释为调用两个 F 参数。 语句

x = F<A> + y;

将解释为小于运算符、大于运算符和一元加运算符,就像已编写语句一样,而不是作为带有二元加运算符后跟二元加运算符的 type_argument_list simple_namex = (F < A) > (+y) 在语句中

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

由于type_argument_list后存在消除标记&&,令牌C<T>被解释为具有type_argument_list的namespace_or_type_name

(A < B, C > D)表达式是包含两个元素的元组,每个元素都是比较。

(A<B,C> D, E)表达式是包含两个元素的元组,其中第一个是声明表达式。

调用 M(A < B, C > D, E) 有三个参数。

调用 M(out A<B,C> D, E) 有两个参数,第一个 out 参数是声明。

e is A<B> C表达式使用声明模式。

事例标签 case A<B> C: 使用声明模式。

end 示例

识别relational_expression(§12.12.1)时,如果“relational_expressionis类型”和“relational_expression is constant_pattern”替代项都适用,并且类型解析为可访问类型,则应选择“relational_expression is 类型”替代项。

6.3 词法分析

6.3.1 常规

为方便起见,词法语法定义并引用以下命名的词法标记:

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

虽然这些是词法规则,但这些名称采用全大写字母进行拼写,以将它们与普通词法规则名称区分开来。

注意:这些便利规则是通常不为文本字符串定义的令牌提供显式令牌名称的例外情况。 end note

输入生产定义 C# 编译单元的词法结构。

input
    : input_section?
    ;

input_section
    : input_section_part+
    ;

input_section_part
    : input_element* New_Line
    | PP_Directive
    ;

input_element
    : Whitespace
    | Comment
    | token
    ;

注意:上述语法由 ANTLR 分析规则描述,它定义 C# 编译单元的词法结构,而不是词法标记。 end note

五个基本元素构成 C# 编译单元的词法结构:行终止符(§6.3.2)、空格(§6.3.4)、注释(§6.3.3)、标记(§6.4)和预处理指令(§6.5)。 在这些基本元素中,只有标记在 C# 程序的语法语法(§6.2.4)中很重要。

C# 编译单元的词法处理包括将文件减少为一系列令牌,这些令牌将成为语法分析的输入。 行终止符、空格和注释可用于分隔标记,预处理指令可能会导致编译单元的节被跳过,否则这些词法元素不会影响 C# 程序的语法结构。

当多个词法语法生产与编译单元中的字符序列匹配时,词法处理始终形成尽可能长的词法元素。

示例:字符序列//作为单行注释的开头进行处理,因为该词法元素比单个/标记长。 end 示例

某些令牌由一组词法规则定义;主规则和一个或多个子规则。 后者在语法中标记,用于 fragment 指示规则定义另一个标记的一部分。 片段规则在词法规则的从上到下的顺序中不考虑。

注意:在 ANTLR fragment 中,是生成此处定义的相同行为的关键字。 end note

6.3.2 行终止符

行终止符将 C# 编译单元的字符划分为行。

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

为了与添加文件结束标记的源代码编辑工具兼容,并使编译单元能够被视为正确终止的行序列,请将以下转换应用于 C# 程序中的每个编译单元:

  • 如果编译单元的最后一个字符是 Control-Z 字符(U+001A),则删除此字符。
  • 如果编译单元的非空,并且编译单元的最后一个字符不是回车符(U+000D)、换行符(U+000A)、下一行字符(U+0085)、行分隔符(U+0028)、行分隔符(U+2028)或段落分隔符(U+2029),则向编译单元的末尾添加回车符(U+000D)。

注意:额外的回车允许程序以没有终止New_LinePP_Directive(§6.5)结束。 end note

6.3.3 注释

支持两种形式的注释:带分隔符的注释和单行注释。

带分隔符的注释以字符开头,以字符/**/结尾。 带分隔符的注释可以占据行、单行或多行的一部分。

示例:示例

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

包含分隔注释。

end 示例

单行注释以字符//开头,并扩展到行尾。

示例:示例

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

显示若干单行注释。

end 示例

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
    ;

注释不嵌套。 字符序列 /**/ 在单行注释中没有特殊含义,字符序列 ///* 在分隔注释中没有特殊含义。

注释不会在字符和字符串文本中处理。

注意:必须仔细解释这些规则。 例如,在下面的示例中,在结束之前A开始的带分隔符的注释在B两者之间结束。C() 原因是

// B */ C();

实际上不是单行注释,因为在//带分隔符的注释中没有特殊含义,因此*/在该行中具有其通常的特殊含义。

同样,在前面E结束之前D开始的带分隔符的注释。 原因是 "D */ " 实际上不是字符串文本,因为初始双引号字符出现在带分隔符的注释中。

在单行注释中没有特殊含义的/**/一个有用后果是,可以通过在每个//行的开头注释掉源代码行块。 一般情况下,它不能放在 /* 这些行和 */ 这些行之后,因为这不会正确封装块中的分隔注释,而且通常可能会完全更改此类分隔注释的结构。

示例代码:

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

end note

Single_Line_Comment s 和具有特定格式Delimited_Comment可用作文档注释,如 §D 中所述

6.3.4 空白

空格定义为 Unicode 类 Z(包括空格字符)以及水平制表符、垂直制表符和窗体源字符的任何字符。

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

6.4 令牌

6.4.1 常规

有多种类型的 令牌:标识符、关键字、文本、运算符和标点符号。 空格和注释不是标记,尽管它们充当令牌的分隔符。

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

注意:这是一个 ANTLR 分析器规则,它不定义词法令牌,而是定义令牌类型的集合。 end note

6.4.2 Unicode 字符转义序列

Unicode 转义序列表示 Unicode 码位。 Unicode 转义序列在标识符(§6.4.3)、字符文本(§6.4.5.5)、常规字符串文本(§6.4.5.6)和内插正则字符串表达式(§12.8.3)中进行处理。 Unicode 转义序列不会在任何其他位置处理(例如,形成运算符、标点符或关键字)。

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
    ;

Unicode 字符转义序列表示由“\u”或“\U”字符后的十六进制数构成的单个 Unicode 码点。 由于 C# 在字符和字符串值中使用 Unicode 码位的 16 位编码,因此范围U+10000U+10FFFF中的 Unicode 代码点使用两个 Unicode 代理项代码单元表示。 上述 U+FFFF Unicode 码位在字符文本中不允许。 上面的 U+10FFFF Unicode 码位无效,不支持。

不会执行多个翻译。 例如,字符串文本 "\u005Cu005C" 等效于 "\u005C" 而不是 "\"

注意:Unicode 值为 \u005C 字符“\”。 end note

示例:示例

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

显示多个用法 \u0066,即字母“”f的转义序列。 该程序等效于

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

end 示例

6.4.3 标识符

此子引用中给出的标识符规则与 Unicode 标准附件 15 建议的规则完全对应,只不过允许下划线作为初始字符(与 C 编程语言中的传统一样),允许在标识符中使用 Unicode 转义序列,并允许“@”字符作为前缀,以允许关键字用作标识符。

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
    ;

注意:

  • 有关上述 Unicode 字符类的信息,请参阅 Unicode 标准版。
  • 片段 Available_Identifier 需要排除关键字和上下文关键字。 如果此规范中的语法使用 ANTLR 进行处理,则 ANTLR 的语义会自动处理此排除:
    • 关键字和上下文关键字作为文本字符串出现在语法中。
    • ANTLR 从这些文本字符串创建隐式词法令牌规则。
    • ANTLR 会在语法中的显式词法规则之前考虑这些隐式规则。
    • 因此,片段 Available_Identifier 与关键字或上下文关键字不匹配,因为这些关键字前面是词法规则。
  • 片段 Escaped_Identifier 包括转义关键字和上下文关键字,因为它们是从词法处理开始 @ 的较长标记的一部分,并且词法处理始终形成最长的词法元素(§6.3.1)。
  • 实现如何对允许 Unicode_Escape_Sequence 值实施限制是实现问题。

end note

示例:有效标识符的示例包括 identifier1_identifier2以及 @ifend 示例

符合程序的标识符应采用 Unicode 规范化表单 C 定义的规范格式,由 Unicode 标准附件 15 定义。 遇到非规范化表单 C 中的标识符时的行为是实现定义的;但是,不需要诊断。

前缀“@”允许使用关键字作为标识符,这在与其他编程语言交互时非常有用。 该字符 @ 实际上不是标识符的一部分,因此标识符可能在其他语言中显示为普通标识符,没有前缀。 带有前缀的@标识符称为逐字标识符

注意:允许对不是关键字的标识符使用 @ 前缀,但强烈建议不要将其用作样式问题。 end note

示例:示例:

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

使用名为“”“的静态方法定义名为”class“的类,该方法采用名为”boolstatic的参数。 请注意,由于关键字中不允许 Unicode 转义,因此令牌“cl\u0061ss”是标识符,并且与“@class”相同。

end 示例

如果应用以下转换后相同的标识符,则两个标识符被视为相同,顺序如下:

  • 删除前缀“@”(如果使用)。
  • 每个 Unicode_Escape_Sequence 都转换为其对应的 Unicode 字符。
  • 删除任何 Formatting_Character

命名 _ 标识符的语义取决于其出现的上下文:

  • 它可以表示命名的程序元素,例如变量、类或方法,或
  • 它可以表示放弃(§9.2.9.1)。

包含两个连续下划线字符的标识符(U+005F)保留供实现使用;但是,如果定义了此类标识符,则无需诊断。

注意:例如,实现可以提供以两个下划线开头的扩展关键字。 end note

6.4.4 关键字

关键字是保留的类似标识符的字符序列,不能用作标识符,除非前面有@字符。

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

上下文关键字是特定上下文中具有特殊意义的字符的标识符序列,但未保留,并且可用作这些上下文之外的标识符,以及字符开头@时。

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

注意:规则 关键字contextual_keyword 是分析器规则,因为它们不引入新的令牌类型。 所有关键字和上下文关键字都由隐式词法规则定义,因为它们作为语法中的文本字符串(§6.2.3)发生。 end note

在大多数情况下,上下文关键字的语法位置使得它们永远无法与普通标识符用法混淆。 例如,在属性声明中, get 标识符 set 具有特殊含义(§15.7.3)。 这些位置中不允许使用或getset不允许使用标识符的标识符,因此此用法不会与将这些字词用作标识符的用法冲突。

在某些情况下,语法不足以区分上下文关键字用法与标识符。 在所有此类情况下,都会指定如何消除两者之间的歧义。 例如,隐式类型本地变量声明(§13.6.2)中的上下文关键字var可能与调用var的声明类型冲突,在这种情况下,声明的名称优先于将标识符用作上下文关键字。

另一个此类消除歧义的示例是上下文关键字(§12.9.8.1),该关键字await仅在声明async的方法内部时被视为关键字,但可用作其他位置的标识符。

与关键字一样,上下文关键字可以用作普通标识符,方法是使用 @ 字符作为前缀。

注意:用作上下文关键字时,这些标识符不能包含 Unicode_Escape_Sequenceend note

6.4.5 文本

6.4.5.1 常规

文本§12.8.2)是值的源代码表示形式。

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

注意文本 是分析程序规则,因为它将其他标记类型分组,但不引入新的标记类型。 end note

6.4.5.2 布尔文本

有两个布尔文本值: truefalse

boolean_literal
    : TRUE
    | FALSE
    ;

注意boolean_literal 是分析程序规则,因为它将其他令牌类型分组,并且不会引入新的令牌类型。 end note

boolean_literal的类型bool

6.4.5.3 整数文本

整数文本用于写入类型intuintlongulong类型的值。 整数文本有三种可能的形式:十进制、十六进制和二进制。

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

整数文本的类型按如下方式确定:

  • 如果文本没有后缀,则其值可以表示为以下类型的第一个类型:int、、uintlongulong
  • 如果文本是U后缀的,或者u,它具有其值可以表示的这些类型中的第一种:uint, 。 ulong
  • 如果文本是L后缀的,或者l,它具有其值可以表示的这些类型中的第一种:long, 。 ulong
  • 如果文本后缀为 UL、、UluLulLuLU、或lUlu,则为类型ulong

如果整数文本表示的值超出类型范围 ulong ,则会发生编译时错误。

注意:作为样式问题,建议在编写类型long文本时使用“L”而不是“”l“,因为很容易将字母”l“与数字”1混淆。 end note

若要允许尽可能 int 小且 long 值写入为整数文本,存在以下两个规则:

  • 当一元减号运算符标记(§12.9.3)之后的一元减号运算符标记(§12.9.3)后,表示值 2147483648 (2ーー) 的 int −2147483648 类型的Integer_Literal不显示Integer_Type_Suffix。 在所有其他情况下,此类 Integer_Literal 的类型 uint
  • Integer_Literal 当表示值9223372036854775808(2⁶ー)且没有Integer_Type_SuffixInteger_Type_SuffixLl紧跟在一元减号运算符标记(§12.9.3)之后的标记时,结果(这两个标记)是具有值−9223372036854775808(2⁶ー)类型的long常量。 在所有其他情况下,此类 Integer_Literal 的类型 ulong

示例:

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

end 示例

6.4.5.4 真实文本

实际文本用于写入类型的floatdouble值,以及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'
    ;

如果未指定Real_Type_Suffix,则Real_Literal的类型double。 否则, Real_Type_Suffix 将确定实际文本的类型,如下所示:

  • 后缀 Ff 类型 float为实文本。

    示例:文本 1f1.5f文本 1e10f123.456F 所有类型 floatend 示例

  • 后缀 Dd 类型 double为实文本。

    示例:文本 1d1.5d文本 1e10d123.456D 所有类型 doubleend 示例

  • 后缀 Mm 类型 decimal为实文本。

    示例:文本 1m1.5m文本 1e10m123.456M 所有类型 decimalend 示例
    此文本通过使用银行家舍入(§8.3.8)将精确值舍入到decimal最接近的可表示值,从而转换为值。 除非对值进行舍入,否则将保留文本中任何明显的刻度。 注意:因此,将分析文本2.900m以形成decimal带符号0、系数和刻度29003end note

如果指定文本的大小太大,无法以指示类型表示,则会发生编译时错误。

注意:特别是, Real_Literal 永远不会产生浮点无穷大。 但是,非零 Real_Literal 可以舍入为零。 end note

类型 float 的实际文本的值,或者 double 通过使用 IEC 60559“舍入到最接近”模式来确定,该模式与“偶数”(具有最小有效位零的值)和所有数字都被视为有效。

注意:在实际文本中,小数位数在小数点之后始终是必需的。 例如, 1.3F 是一个真实文本,但 1.F 不是。 end note

示例:

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

end 示例

6.4.5.5 字符文本

字符文本表示单个字符,由引号中的字符组成,如中所示 '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?
    ;

注意:字符中后跟反斜杠字符(\)的字符必须是下列字符之一:'、、"t\0aurUfnb、 。 xv 否则,将发生编译时错误。 end note

注意:由于 \x 十六进制数字\x的可变数目,Hexadecimal_Escape_Sequence生产的使用可能会出错且难以读取。 例如,在代码中:

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

它可能首先显示两个字符串中的前导字符相同(U+0009即制表符)。 事实上,第二个字符串以单词“Bad”中的所有三个字母开头 U+9BAD 是有效的十六进制数字。 作为样式问题,建议 \x 避免使用特定的转义序列(\t 在本例中)或固定长度 \u 转义序列。

end note

十六进制转义序列表示单个 Unicode UTF-16 代码单元,其值由“”\x后十六进制数构成。

如果字符文本表示的值大于 U+FFFF,则会发生编译时错误。

字符文本中的 Unicode 转义序列(§6.4.2)应位于范围 U+0000U+FFFF

简单的转义序列表示 Unicode 字符,如下表所述。

转义序列 字符名称 Unicode 码位
\' 单引号 U+0027
\" 双引号 U+0022
\\ 反斜杠 U+005C
\0 Null U+0000
\a Alert U+0007
\b Backspace U+0008
\f 换页 U+000C
\n 换行 U+000A
\r 回车 U+000D
\t 水平制表符 U+0009
\v 垂直制表符 U+000B

Character_Literal的类型char

6.4.5.6 字符串文本

C# 支持两种形式的字符串文本:常规字符串文本逐字字符串文本 常规字符串文本由零个或多个字符组成,括在双引号中 "hello",可以包括简单的转义序列(如 \t 制表符),以及十六进制和 Unicode 转义序列。

逐字字符串文本由后跟双引号字符、零个或多个字符和右双引号字符组成的 @ 字符组成。

示例:一个简单的示例是 @"hello"end 示例

在逐字字符串文本中,分隔符之间的字符被逐字解释,唯一的例外是Quote_Escape_Sequence,表示一个双引号字符。 具体而言,简单的转义序列和十六进制和 Unicode 转义序列不会在逐字字符串文本中处理。 逐字字符串文本可以跨越多行。

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

示例:示例

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

显示各种字符串文本。 最后一个字符串文本 j是跨越多行的逐字字符串文本。 引号之间的字符(包括新行字符)之间的字符将保留逐字保留,每对双引号字符将替换为一个这样的字符。

end 示例

注意:逐字字符串文本中的任何换行符都是生成的字符串的一部分。 如果用于形成换行符的确切字符在语义上与应用程序相关,则任何将源代码中的换行符转换为不同格式(例如“”\n和“之间)\r\n的工具都将更改应用程序行为。 在这种情况下,开发人员应小心。 end note

注意:由于十六进制转义序列可以具有可变数量的十六进制数字,因此字符串文本 "\x123" 包含具有十六进制值的 123单个字符。 若要创建包含十六进制值12后跟字符3的字符串,可以编写或"\x12" + "3"改写。"\x00123" end note

String_Literal的类型string

每个字符串文本不一定会导致新的字符串实例。 如果两个或多个字符串文本与字符串相等运算符(§12.12.8)等效,则这些字符串文本将引用同一字符串实例。

示例:例如,由

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

这是因为 True 这两个文本引用相同的字符串实例。

end 示例

6.4.5.7 Null 文本

null_literal
    : NULL
    ;

注意null_literal 是分析器规则,因为它不引入新的令牌类型。 end note

null_literal表示值null。 它没有类型,但可以通过 null 文本转换(§10.2.7)转换为任何引用类型或可以为 null 的值类型。

6.4.6 运算符和标点符号

有多种运算符和标点符号。 表达式中使用运算符来描述涉及一个或多个操作数的操作。

示例:表达式 a + b 使用 + 运算符添加两个操作数 abend 示例

标点符号用于分组和分隔。

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

right_shift
    : '>'  '>'
    ;

right_shift_assignment
    : '>' '>='
    ;

注意right_shiftright_shift_assignment 是分析器规则,因为它们不引入新的令牌类型,而是表示两个标记的序列。 operator_or_punctuator规则仅用于描述性目的,并且不会在语法中的其他位置使用。 end note

right_shift 由两个令牌 >>。 同样, right_shift_assignment 由两个标记 >>=。 与语法中的其他生产不同,每个作品中都不允许使用任何类型的字符(甚至空格)。 为了正确处理 type_parameter_lists§15.2.3),对这些生产进行特殊处理。

注意:在将泛型添加到 C# 之前, >> >>= 两者都是单个令牌。 但是,泛型语法使用 < 字符 > 来分隔类型参数和类型参数。 通常需要使用嵌套构造类型,例如 List<Dictionary<string, int>>。 两个operator_or_punctuator的定义已更改,而不是要求程序员分隔>空格>和空格。 end note

6.5 预处理指令

6.5.1 常规

预处理指令提供有条件地跳过编译单元部分、报告错误和警告条件、描述源代码的不同区域以及设置可为 null 上下文的功能。

注意:术语“预处理指令”仅用于与 C 和C++编程语言的一致性。 在 C# 中,没有单独的预处理步骤;预处理指令作为词法分析阶段的一部分进行处理。 end note

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
    ;

注意:

  • 预处理器语法定义用于所有预处理指令的单个词法标记 PP_Directive 。 每个预处理指令的语义都在此语言规范中定义,但不能实现它们。
  • PP_Start片段必须仅在行开头识别,getCharPositionInLine() == 0上述 ANTLR 词法谓词建议实现此目的的一种方法,并且仅提供信息,实现可能使用不同的策略。

end note

以下预处理指令可用:

  • #define#undef,分别用于定义和取消定义条件编译符号(§6.5.4)。
  • #if#elif#else#endif,用于跳过源代码的条件部分(§6.5.5)。
  • #line,用于控制针对错误和警告发出的行号(§6.5.8)。
  • #error,用于发出错误(§6.5.6)。
  • #region#endregion,用于显式标记源代码部分(§6.5.7)。
  • #nullable,用于指定可为 null 的上下文(§6.5.9)。
  • #pragma,用于向编译器指定可选上下文信息(§6.5.10)。

预处理指令始终占用单独的源代码行,并且始终以 # 字符和预处理指令名称开头。 字符之前 # 以及字符和指令名称之间 # 可能会出现空格。

包含一个#define#undef#if#else#elif#endif#line#endregion#nullable指令的源行可以以单行注释结尾。 包含预处理指令的源行不允许使用带分隔符的注释( /* */ 注释样式)。

预处理指令不是 C# 语法的一部分。 但是,预处理指令可用于包含或排除令牌序列,并以此方式影响 C# 程序的含义。

示例:编译后,程序

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

生成与程序完全相同的令牌序列

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

因此,从词法上讲,这两个程序是完全不同的,语法上是相同的。

end 示例

6.5.2 条件编译符号

条件编译功能通过#if#elif#else预处理表达式(§6.5.3)和#endif条件编译符号进行控制。

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

请注意 ,实现如何强制限制允许 Basic_Identifier 值是实现问题。 end note

如果应用以下转换后,两个条件编译符号被视为相同,顺序如下:

  • 每个 Unicode_Escape_Sequence 都转换为其对应的 Unicode 字符。
  • 删除任何 Formatting_Characters

条件编译符号有两种可能的状态:已定义未定义 在编译单元的词法处理开始时,除非已由外部机制(如命令行编译器选项)显式定义条件编译符号,否则条件编译符号是未定义的。 #define处理指令时,在该指令中指定的条件编译符号在该编译单元中定义。 该符号将保持定义,直到处理同一 #undef 符号的指令,或直到到达编译单元的末尾。 这意味着 #define ,一 #undef 个编译单元中的指令对同一程序中的其他编译单元没有任何影响。

在预处理表达式(§6.5.3)中引用时,定义的条件编译符号具有布尔值 true,未定义的条件编译符号具有布尔值 false。 在预处理表达式中引用条件编译符号之前,无需显式声明条件编译符号。 相反,未声明的符号只是未定义,因此具有值 false

条件编译符号的命名空间不同于 C# 程序中所有其他命名实体。 条件编译符号只能在指令和#undef预处理表达式中引用#define

6.5.3 预处理表达式

预处理表达式可能发生在指令中#if#elif 运算符!(仅前缀逻辑求反)、==!=&&||在预处理表达式中允许,括号可用于分组。

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

在预处理表达式中引用时,定义的条件编译符号具有布尔值 true,未定义的条件编译符号具有布尔值 false

预处理表达式的计算始终生成布尔值。 预处理表达式的计算规则与常量表达式(§12.23)的计算规则相同,但唯一可引用的用户定义实体是条件编译符号。

6.5.4 定义指令

定义指令用于定义或取消定义条件编译符号。

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

指令的 #define 处理会导致定义给定的条件编译符号,从指令后面的源行开始。 同样,指令的 #undef 处理会导致给定的条件编译符号变得未定义,从指令后面的源行开始。

#define编译单元中的任何指令#undef应在编译单元的第一个令牌§6.4)之前发生;否则会发生编译时错误。 在直观的术语中#undef#define指令应位于编译单元中的任何“实际代码”之前。

示例:示例:

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

有效,因为 #define 指令位于编译单元中的第一个标记( namespace 关键字)之前。

end 示例

示例:以下示例导致编译时错误,因为 #define 遵循实际代码:

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

end 示例

A #define 可以定义已定义的条件编译符号,而无需 #undef 干预该符号。

示例:下面的示例定义条件编译符号 A,然后再次定义它。

#define A
#define A

对于允许将条件编译符号定义为编译选项的编译器,此类重新定义的另一种方法是在源中将符号定义为编译器选项。

end 示例

#undef可能“取消定义”条件编译符号。

示例:下面的示例定义了一个条件编译符号 A ,然后取消定义它两次;尽管第二 #undef 个符号没有效果,但它仍然有效。

#define A
#undef A
#undef A

end 示例

6.5.5 条件编译指令

条件编译指令用于有条件地包含或排除编译单元的部分。

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

条件编译指令应按顺序、顺序、 #if 零个或多个 #elif 指令、零或一个 #else 指令和一个 #endif 指令组成。 指令之间是 源代码的条件部分 。 每个部分都由前面的指令控制。 条件节本身可能包含嵌套的条件编译指令,前提是这些指令形成完整的组。

最多选择一个包含的条件节进行正常词法处理:

  • 按顺序计算PP_Expression#if#elif指令,直到生成一个true。 如果表达式生成 true,则选择相应指令后面的条件节。
  • 如果所有 PP_Expression生成 false,并且存在指令 #else ,则选择指令后面的 #else 条件节。
  • 否则,未选择任何条件节。

所选的条件节(如果有)作为正常 input_section进行处理:该节中包含的源代码应遵循词法语法;令牌是从节中的源代码生成的;并且该部分中的预处理指令具有规定的效果。

将跳过所有剩余的条件节,并且不会从源代码生成任何标记,除非这些标记用于预处理指令。 因此,跳过的源代码(预处理指令除外)在词法上可能不正确。 跳过的预处理指令应按词法正确,但不进行其他处理。 在正在跳过任何嵌套条件节(包含在嵌套 #if...#endif 构造中)的条件节中,也将被跳过。

注意:上述语法不捕获预处理指令之间的条件节在词法上可能格式不正确。 因此,语法并非 ANTLR 就绪,因为它仅支持词法正确的输入。 end note

示例:以下示例说明了条件编译指令如何嵌套:

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

除了预处理指令外,跳过的源代码不受词法分析的约束。 例如,尽管节 #else 中未确定的注释,但以下内容有效:

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

但是,请注意,即使在源代码的跳过部分,预处理指令也需要在词法上正确。

在多行输入元素内显示预处理指令时,不会处理它们。 例如,程序:

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

结果输出:

hello,
#if Debug
        world
#else
        Nebraska
#endif

在特殊情况下,处理的预处理指令集可能取决于pp_expression计算。 示例:

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

始终生成相同的令牌流(class }Q {),而不考虑是否X定义。 如果X已定义,则唯一处理的指令是#if#endif多行注释。 如果未 X 定义,则三个指令 (#if#else#endif) 是指令集的一部分。

end 示例

6.5.6 诊断指令

诊断指令用于生成与其他编译时错误和警告相同的方式报告的显式错误和警告消息。

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

fragment PP_Message
    : PP_Whitespace Input_Character*
    ;

示例:示例

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

如果条件编译符号和已定义两个编译符号DebugRetail,则生成编译时错误(“生成不能同时调试和零售”。 请注意, PP_Message 可以包含任意文本;具体而言,它不需要包含格式正确的标记,如单词 can't中的单引号所示。

end 示例

6.5.7 区域指令

区域指令用于标记源代码的显式区域。

fragment PP_Region
    : PP_Start_Region
    | PP_End_Region
    ;

fragment PP_Start_Region
    : 'region' PP_Message?
    ;

fragment PP_End_Region
    : 'endregion' PP_Message?
    ;

没有语义含义附加到区域;区域供程序员或自动化工具用来标记源代码的一部分。 每个指令应有一个 #endregion 匹配 #region 的指令。 同样,在或#region#endregion指令中指定的消息没有语义含义;它只是用于标识区域。 匹配 #region#endregion 指令可能具有不同的 PP_Message

区域的词法处理:

#region
...
#endregion

与表单的条件编译指令的词法处理完全对应:

#if true
...
#endif

注意:这意味着一个区域可以包含一个或多个 #if/.../#endif,或者包含在 /.../#endif中的条件节中#if;但是某个区域不能与 /.../#endif的一部分#if重叠,或者在不同的条件节中开始和结束。 end note

6.5.8 行指令

行指令可用于更改编译器在输出中报告的行号和编译单元名称,例如警告和错误。 调用方信息属性也使用这些值(§22.5.6)。

注意:行指令最常用于元编程工具,这些工具从一些其他文本输入生成 C# 源代码。 end note

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

当不存在 #line 指令时,编译器在其输出中报告真正的行号和编译单元名称。 当处理#line包含defaultPP_Line_Indicator的指令时,编译器会将指令后面的视为具有给定行号(如果指定)和编译单元名称。

允许的 Decimal_Digit+ 最大值是实现定义的。

指令 #line default 会撤消上述 #line 所有指令的效果。 编译器报告后续行的真实行信息,就像没有 #line 处理任何指令一样。

指令 #line hidden 对错误消息中报告的编译单元和行号没有影响,或者使用 CallerLineNumberAttribute§22.5.6.2) 生成)。 它旨在影响源级调试工具,以便在调试时,指令和后续#line指令之间的#line hidden所有行(即不是#line hidden)没有行号信息,并在单步执行代码时完全跳过。

注意:尽管 PP_Compilation_Unit_Name 可能包含类似于转义序列的文本,但此类文本不是转义序列;在此上下文中,“”\字符只是指定普通反斜杠字符。 end note

6.5.9 可为 Null 指令

可以为 null 的指令控制可为 null 的上下文,如下所示。

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

可以为 null 的指令设置后续代码行的可用标志,直到另一个可以为 null 的指令重写它,或直到到达编译_unit结束为止。 可为 null 的上下文包含两个标志: 批注警告。 每种形式的可为 null 指令的效果如下:

  • #nullable disable:禁用可以为 null 的批注和可为 null 的警告标志。
  • #nullable enable:启用可以为 null 的批注和可为 null 的警告标志。
  • #nullable restore:将批注和警告标志还原到外部机制指定的状态(如果有)。
  • #nullable disable annotations:禁用可以为 null 的批注标志。 可以为 null 的警告标志不受影响。
  • #nullable enable annotations:启用可以为 null 的批注标志。 可以为 null 的警告标志不受影响。
  • #nullable restore annotations:将可以为 null 的批注标志还原到外部机制指定的状态(如果有)。 可以为 null 的警告标志不受影响。
  • #nullable disable warnings:禁用可以为 null 的警告标志。 可以为 null 的批注标志不受影响。
  • #nullable enable warnings:启用可以为 null 的警告标志。 可以为 null 的批注标志不受影响。
  • #nullable restore warnings:将可以为 null 的警告标志还原到外部机制指定的状态(如果有)。 可以为 null 的批注标志不受影响。

表达式的可为 null 状态随时跟踪。 批注标志的状态以及不存在可为 null 的批注 ?,确定变量声明的初始 null 状态。 仅当启用警告标志时才会发出警告。

示例:示例

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

生成编译时警告(“原样xnull”)。 随处跟踪可为 null 的状态 x 。 启用警告标志时发出警告。

end 示例

6.5.10 Pragma 指令

#pragma预处理指令用于指定编译器的上下文信息。

注意:例如,编译器可能提供 #pragma 指令

  • 编译后续代码时启用或禁用特定警告消息。
  • 指定要应用于后续代码的优化。
  • 指定要由调试器使用的信息。

end note

fragment PP_Pragma
    : 'pragma' PP_Pragma_Text?
    ;

fragment PP_Pragma_Text
    : PP_Whitespace Input_Character*
    ;

PP_Pragma_Text中的Input_Character以实现定义的方式由编译器解释。 指令中 #pragma 提供的信息不应更改程序语义。 指令 #pragma 应仅更改超出此语言规范范围的编译器行为。 如果编译器无法解释 Input_Characters,编译器可以生成警告;但是,它不应生成编译时错误。

注意PP_Pragma_Text 可以包含任意文本;具体而言,它不需要包含格式正确的标记。 end note