C#语言规范(2.5 预处理指令) (转摘)
预处理指令提供按条件跳过源文件中的节、报告错误和警告条件,以及描绘源代码的不同区域的能力。使用术语“预处理指令”只是为了与 C 和 C++ 编程语言保持一致。在 C# 中没有单独的预处理步骤;预处理指令按词法分析阶段的一部分处理。
pp-directive:(pp 指令:)
pp-declaration(pp 声明)
pp-conditional(pp 条件)
pp-line(pp 行)
pp-diagnostic(pp 诊断)
pp-region(pp 区域)
下面是可用的预处理指令:
· #define 和 #undef,分别用于定义和取消定义条件编译符号(第 2.5.3 节)。
· #if、#elif、#else 和 #endif,用于按条件跳过源代码中的节(第 2.5.4 节)。
· #line,用于控制行号(在发布错误和警告信息时使用)(第 2.5.7 节)。
· #error 和 #warning,分别用于发出错误和警告(第 2.5.5 节)。
· #region 和 #endregion,用于显式标记源代码中的节(第 2.5.6 节)。
预处理指令总是占用源代码中的单独一行,并且总是以 # 字符和预处理指令名称开头。# 字符的前面以及 # 字符与指令名称之间可以出现空白符。
包含 #define、#undef、#if、#elif、#else、#endif 或 #line 指令的源代码行可以用单行注释结束。在包含预处理指令的源行上不允许使用带分隔符的注释(/* */ 样式的注释)。
预处理指令既不是标记,也不是 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() {}
}
因此,尽管上述两个程序在词法分析中完全不同,但它们在句法分析中是相同的。
2.5.1 条件编译符号
#if、#elif、#else 和 #endif 指令提供的条件编译功能是通过预处理表达式(第 2.5.1 节)和条件编译符号来控制的。
conditional-symbol:(条件符号:)
除 true 和 false 外的任何标识符或关键字
条件编译符号有两种可能的状态:已定义的或未定义的。在源文件词法处理开始时,条件编译符号除非已由外部机制(如命令行编译器选项)显式定义,否则是未定义的。当处理 #define 指令时,在指令中指定的条件编译符号在那个源文件中成为已定义的。此后,该符号就一直保持已定义的状态,直到处理一条关于同一符号的 #undef 指令,或者到达源文件的结尾。这意味着一个源文件中的 #define 和 #undef 指令对同一程序中的其他源文件没有任何影响。
当在预处理表达式中引用时,已定义的条件编译符号具有布尔值 true,未定义的条件编译符号具有布尔值 false。不要求在预处理表达式中引用条件编译符号之前显式声明它们。相反,未声明的符号只是未定义的,因此具有值 false。
条件编译符号的命名空间与 C# 程序中的所有其他命名实体截然不同。只能在 #define 和 #undef 指令以及预处理表达式中引用条件编译符号。
2.5.2 预处理表达式
预处理表达式可以出现在 #if 和 #elif 指令中。在预处理表达式中允许使用 !、==、!=、&& 和 || 运算符,并且可以使用括号进行分组。
pp-expression:(pp 表达式:)
whitespaceopt pp-or-expression whitespaceopt(空白可选 pp 或表达式 空白可选)
pp-or-expression:(pp 或表达式:)
pp-and-expression(pp 与表达式)
pp-or-expression whitespaceopt || whitespaceopt pp-and-expression(pp 或表达式 空白可选 || 空白可选 pp 与表达式)
pp-and-expression:(pp 与表达式:)
pp-equality-expression(pp 相等表达式)
pp-and-expression whitespaceopt && whitespaceopt pp-equality-expression(pp 与表达式 空白可选 && 空白可选 pp 相等表达式)
pp-equality-expression:(pp 相等表达式:)
pp-unary-expression(pp 一元表达式)
pp-equality-expression whitespaceopt == whitespaceopt pp-unary-expression(pp 相等表达式 空白可选 == 空白可选 pp 一元表达式)
pp-equality-expression whitespaceopt != whitespaceopt pp-unary-expression(pp 相等表达式 空白可选 != 空白可选 pp 一元表达式)
pp-unary-expression:(pp 一元表达式:)
pp-primary-expression(pp 基本表达式)
! whitespaceopt pp-unary-expression(! 空白可选 pp 一元表达式)
pp-primary-expression:(pp 基本表达式:)
true
false
conditional-symbol(条件符号)
( whitespaceopt pp-expression whitespaceopt )((空白可选 pp 表达式 空白可选 ))
当在预处理表达式中引用时,已定义的条件编译符号具有布尔值 true,未定义的条件编译符号具有布尔值 false。
预处理表达式的计算总是产生一个布尔值。预处理表达式的计算规则与常数表达式(第 7.15 节)相同,唯一的例外是:在这里,唯一可引用的用户定义实体是条件编译符号。
2.5.3 声明指令
声明指令用于定义或取消定义条件编译符号。
pp-declaration:(pp 声明:)
whitespaceopt # whitespaceopt define whitespace conditional-symbol pp-new-line(空白可选 # 空白可选 define 空白 条件符号 pp 新行)
whitespaceopt # whitespaceopt undef whitespace conditional-symbol pp-new-line(空白可选 # 空白可选 undef 空白 条件符号 pp 新行)
pp-new-line:(pp 新行:)
whitespaceopt single-line-commentopt new-line(空白可选 单行注释可选 新行)
对 #define 指令的处理使给定的条件编译符号成为已定义的(从跟在指令后面的源代码行开始)。类似地,对 #undef 指令的处理使给定的条件编译符号成为未定义的(从跟在指令后面的源代码行开始)。
源文件中的任何 #define 和 #undef 指令都必须出现在源文件中第一个“标记”(第 2.4 节)的前面,否则将发生编译时错误。直观地讲,#define 和 #undef 指令必须位于源文件中所有“实代码”的前面。
示例:
#define Enterprise
#if Professional || Enterprise
#define Advanced
#endif
namespace Megacorp.Data
{
#if Advanced
class PivotTable {...}
#endif
}
是有效的,这是因为 #define 指令位于源文件中第一个标记(namespace 关键字)的前面。
下面的示例产生编译时错误,因为 #define 指令在实代码后面出现:
#define A
namespace N
{
#define B
#if B
class Class1 {}
#endif
}
#define 指令可用于重复地定义一个已定义的条件编译符号,而不必对该符号插入任何 #undef。下面的示例定义一个条件编译符号 A,然后再次定义它。
#define A
#define A
#undef 指令可用于取消定义一个本来已经是未定义的条件编译符号。下面的示例定义一个条件编译符号 A,然后两次取消定义该符号;第二个 #undef 没有作用但仍是有效的。
#define A
#undef A
#undef A
2.5.4 条件编译指令
条件编译指令用于按条件包含或排除源文件中的某些部分。
pp-conditional:(pp 条件:)
pp-if-section pp-elif-sectionsopt pp-else-sectionopt pp-endif(pp if 节 pp elif 节可选 pp else 节可选 pp endif)
pp-if-section:(pp if 节:)
whitespaceopt # whitespaceopt if whitespace pp-expression pp-new-line conditional-sectionopt(空白可选 # 空白可选 if 空白 pp 表达式 pp 新行 条件节可选)
pp-elif-sections:(pp elif 节:)
pp-elif-section(pp elif 节)
pp-elif-sections pp-elif-section(pp elif 节 pp elif 节)
pp-elif-section:(pp elif 节:)
whitespaceopt # whitespaceopt elif whitespace pp-expression pp-new-line conditional-sectionopt(空白可选 # 空白可选 elif 空白 pp 表达式 pp 新行 条件节可选)
pp-else-section:(pp-else 节:)
whitespaceopt # whitespaceopt else pp-new-line conditional-sectionopt(空白可选 # 空白可选 else pp 新行 条件节可选)
pp-endif:
whitespaceopt # whitespaceopt endif pp-new-line(空白可选 # 空白可选 endif pp 新行)
conditional-section:(条件节:)
input-section(输入节)
skipped-section(跳过节)
skipped-section:(跳过节:)
skipped-section-part(跳过节部分)
skipped-section skipped-section-part(跳过节 跳过节部分)
skipped-section-part:(跳过节部分:)
skipped-charactersopt new-line(跳过字符可选 新行)
pp-directive(pp 指令)
skipped-characters:(跳过字符:)
whitespaceopt not-number-sign input-charactersopt(空白可选 非数字符号 输入字符可选)
not-number-sign:(非数字符号:)
除 # 外的任何输入字符
按照语法的规定,条件编译指令必须写成集的形式,集的组成依次为:一个 #if 指令、一个或多个 #elif 指令(或没有)、一个或多个 #else 指令(或没有)和一个 #endif 指令。指令之间是源代码的条件节。每节代码直接位于它前面的那个指令控制。条件节本身可以包含嵌套的条件编译指令,前提是这些指令构成完整的指令集。
“pp 条件”最多只能选择一个它所包含的“条件节”去做通常的词法处理:
· 按顺序计算 #if 和 #elif 指令的“pp 表达式”直到获得值 true。如果表达式的结果为 true,则选择对应指令的“条件节”。
· 如果所有“pp 表达式”的结果都为 false 并且存在 #else 指令,则选择 #else 指令的“条件节”。
· 否则不选择任何“条件节”。
选定的“条件节”(若有)按正常的“输入节”处理:节中包含的源代码必须符合词法文法;从节中的源代码生成标记;节中的预处理指令具有规定的效果。
剩余的“条件节”(若有)按“跳过节”处理:除了预处理指令,节中的源代码不必一定要符合词法文法;不从节中的源代码生成任何词法标记;节中的预处理指令必须在词法上正确,但不另外处理。在按“跳过节”处理的“条件节”中,任何嵌套的“条件节”(包含在嵌套的 #if...#endif 和 #region...#endregion 构造中)也按“跳过节”处理。
下面的示例阐释如何嵌套条件编译指令:
#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 表达式的计算。示例:
#if X
/*
#else
/* */ class Q { }
#endif
总是产生同样的标记流 (class Q { }),不管是否定义了 X。如果定义了 X,由于多行注释的缘故,只处理 #if 和 #endif 指令。如果未定义 X,则这三个指令(#if、#else、#endif)是指令集的组成部分。
2.5.5 诊断指令
诊断指令用于显式生成错误信息和警告消息,这些信息的报告方式与其他编译时错误和警告相同。
pp-diagnostic:(pp 诊断:)
whitespaceopt # whitespaceopt error pp-message(空白可选 # 空白可选 error pp 消息)
whitespaceopt # whitespaceopt warning pp-message(空白可选 # 空白可选 warning pp 消息)
pp-message:(pp 消息:)
new-line(新行)
whitespace input-charactersopt new-line(空白 输入字符可选 新行)
示例:
#warning Code review needed before check-in
#if Debug && Retail
#error A build can't be both debug and retail
#endif
class Test {...}
总是产生一个警告(“Code review needed before check-in”),如果同时定义条件符号 Debug 和 Retail,则产生一个编译时错误(“A build can't be both debug and retail”)。注意 pp-message(pp 消息)可以包含任意文本;具体说来,它可以包含格式不正确的标记,比如“can't”中的单引号就是这样。
2.5.6 区域指令
区域指令用于显式标记源代码的区域。
pp-region:(pp 区域:)
pp-start-region conditional-sectionopt pp-end-region(pp 开始区域 条件节可选 pp 结束区域)
pp-start-region:(pp 开始区域:)
whitespaceopt # whitespaceopt region pp-message(空白可选 # 空白可选 region pp 消息)
pp-end-region:(pp 结束区域:)
whitespaceopt # whitespaceopt endregion pp-message(空白可选 # 空白可选 endregion pp 消息)
区域不具有任何附加的语义含义;区域旨在由程序员或自动工具用来标记源代码中的节。#region 或 #endregion 指令中指定的消息同样不具有任何语义含义;它只是用于标识区域。匹配的 #region 和 #endregion 指令可能具有不同的“pp 消息”。
区域的词法处理:
#region
...
#endregion
与以下形式的条件编译指令的词法处理完全对应:
#if true
...
#endif
2.5.7 行指令
行指令可用于改变编译器在输出(如警告和错误)中报告的行号和源文件名称。
行指令最通用于从某些其他文本输入生成 C# 源代码的元编程工具。
pp-line:(pp 行:)
whitespaceopt # whitespaceopt line whitespace line-indicator pp-new-line(空白可选 # 空白可选 line 空白 行指示符 pp 新行)
line-indicator:(行指示符:)
decimal-digits whitespace file-name(十进制数字 空白 文件名)
decimal-digits(十进制数字)
default
hidden
file-name:(文件名:)
" file-name-characters "(" 文件名字符 ")
file-name-characters:(文件名字符:)
file-name-character(文件名字符)
file-name-characters file-name-character(文件名字符 文件名字符)
file-name-character:(文件名字符:)
除 " 外的任何输入字符
当不存在 #line 指令时,编译器在它的输出中报告真实的行号和源文件名称。#line 指令最通用于从某些其他文本输入生成 C# 源代码的元编程工具。当处理的 #line 指令包含不是 default 的行指示符时,编译器将该指令“后面”的行视为具有给定的行号(如果指定了,还包括文件名)。
#line default 指令消除前面所有 #line 指令的影响。编译器报告后续行的真实行信息,就像尚未处理任何 #line 指令一样。
#line hidden 指令对错误信息中报告的文件号和行号无效,但对源代码级调试确实有效。调试时,#line hidden 指令和后面的 #line 指令(不是 #line hidden)之间的所有行都没有行号信息。在调试器中逐句执行代码时,将全部跳过这些行。
注意,file-name(文件名)与常规字符串的不同之处在于不处理转义字符;“\”字符在 file-name(文件名)中只是表示一个普通的反斜杆字符。