22.1 常规
C# 语言的很大一部分使程序员能够指定有关程序中定义的实体的声明性信息。 例如,类中方法的可访问性是通过使用 method_modifierpublic
、protected
、internal
和 private
对其进行修饰来指定的。
C# 使程序员能够创建新的声明性信息类型,称为属性。 然后,程序员可以将属性附加到各种程序实体,并在运行时环境中检索属性信息。
注意:例如,框架可能会定义一个
HelpAttribute
属性,该属性可放置在某些程序元素(如类和方法)上,以提供从这些程序元素到其文档的映射。 尾注
属性通过属性类的声明 (§22.2) 来定义,属性类可以具有位置参数和命名参数 (§22.2.3)。 在 C# 程序中,使用属性规范 (§22.3) 将属性附加到实体,并可在运行时作为属性实例 (§22.4) 检索。
22.2 属性类
22.2.1 常规
从抽象类 System.Attribute
派生的类(无论是直接派生还是间接派生)都是属性类。 属性类的声明定义了一种可放置在程序实体上的新属性类型。 按照约定,属性类的命名带有 Attribute
后缀。 属性的使用可以包含或省略此后缀。
泛型类声明不得将 System.Attribute
用作直接或间接基类。
示例:
public class B : Attribute {} public class C<T> : B {} // Error – generic cannot be an attribute
结束示例
22.2.2 属性用法
属性 AttributeUsage
(§22.5.2) 用于描述属性类的使用方式。
AttributeUsage
具有一个位置参数 (§22.2.3),使属性类能够指定可在其上使用该属性类的程序实体类型。
示例:以下示例定义了一个名为
SimpleAttribute
的属性类,该类只能放置在 class_declaration 和 interface_declaration 上,并展示了Simple
属性的几种用法。[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] public class SimpleAttribute : Attribute { ... } [Simple] class Class1 {...} [Simple] interface Interface1 {...}
尽管此属性以名称
SimpleAttribute
定义,但使用该属性时,可以省略Attribute
后缀,从而得到短名称Simple
。 因此,上面的示例在语义上等同于以下内容[SimpleAttribute] class Class1 {...} [SimpleAttribute] interface Interface1 {...}
结束示例
AttributeUsage
具有一个名为 的命名参数 (AllowMultiple
),该参数指示是否可以为给定实体多次指定该属性。 如果属性类的 AllowMultiple
为 true,则该属性类是多用途属性类,可在一个实体上多次指定。 如果属性类的 AllowMultiple
为 false 或未指定,则该属性类是单用途属性类,在一个实体上最多可指定一次。
示例:以下示例定义了一个名为
AuthorAttribute
的多用途属性类,并展示了一个使用了两次Author
属性的类声明:[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class AuthorAttribute : Attribute { public string Name { get; } public AuthorAttribute(string name) => Name = name; } [Author("Brian Kernighan"), Author("Dennis Ritchie")] class Class1 { ... }
结束示例
AttributeUsage
具有另一个名为 的命名参数 (Inherited
),该参数指示当属性在基类上指定时,从该基类派生的类是否也会继承该属性。 如果属性类的 Inherited
为 true,则该属性是可继承的。 如果属性类的 Inherited
为 false,则该属性不可继承。 如果未指定,其默认值为 true。
未附加 X
属性的属性类 AttributeUsage
,如下所示
class X : Attribute { ... }
等效于以下命令:
[AttributeUsage(
AttributeTargets.All,
AllowMultiple = false,
Inherited = true)
]
class X : Attribute { ... }
22.2.3 位置参数和命名参数
属性类可以有位置参数和命名参数。 属性类的每个公共实例构造函数都为该属性类定义了有效的位置参数序列。 属性类的每个非静态公共读写字段和属性都为该属性类定义了一个命名参数。 要让属性定义命名参数,该属性必须同时具有公共的 get 访问器和公共的 set 访问器。
示例:以下示例定义了一个名为
HelpAttribute
的属性类,该类有一个位置参数url
和一个命名参数Topic
。 尽管属性Url
是非静态且公共的,但它不定义命名参数,因为它不是读写的。 还展示了该属性的两种用法:[AttributeUsage(AttributeTargets.Class)] public class HelpAttribute : Attribute { public HelpAttribute(string url) // url is a positional parameter { ... } // Topic is a named parameter public string Topic { get; set; } public string Url { get; } } [Help("http://www.mycompany.com/xxx/Class1.htm")] class Class1 { } [Help("http://www.mycompany.com/xxx/Misc.htm", Topic ="Class2")] class Class2 { }
结束示例
22.2.4 属性参数类型
属性类的位置参数和命名参数的类型仅限于属性参数类型,这些类型包括:
- 以下类型之一:
bool
、byte
、char
、double
、float
、int
、long
、sbyte
、short
、string
、uint
、ulong
、ushort
。 -
object
类型。 -
System.Type
类型。 - 枚举类型。
- 上述类型的一维数组。
- 不具有这些类型之一的构造函数参数或公共字段不得在属性规范中用作位置参数或命名参数。
22.3 属性规范
属性规范是将先前定义的属性应用于程序实体。 属性是为程序实体指定的一段附加声明性信息。 属性可以在全局范围(用于在包含的程序集或模块上指定属性)以及 type_declaration (§14.7)、class_member_declaration (§15.3)、interface_member_declaration (§18.4)、struct_member_declaration (§16.3)、enum_member_declaration (§19.2)、accessor_declaration (§15.7.3)、event_accessor_declaration (§15.8)、parameter_list 的元素 (§15.6.2) 和 type_parameter_list 的元素 (§15.2.3)。
属性在属性部分中指定。 属性部分由一对方括号组成,方括号中包含一个或多个属性的逗号分隔列表。 在此类列表中指定属性的顺序以及附加到同一程序实体的部分的排列顺序并不重要。 例如,属性规范 [A][B]
、[B][A]
、[A, B]
、和 [B, A]
是等效的。
global_attributes
: global_attribute_section+
;
global_attribute_section
: '[' global_attribute_target_specifier attribute_list ']'
;
global_attribute_target_specifier
: global_attribute_target ':'
;
global_attribute_target
: identifier
;
attributes
: attribute_section+
;
attribute_section
: '[' attribute_target_specifier? attribute_list ']'
;
attribute_target_specifier
: attribute_target ':'
;
attribute_target
: identifier
| keyword
;
attribute_list
: attribute (',' attribute)* ','?
;
attribute
: attribute_name attribute_arguments?
;
attribute_name
: type_name
;
attribute_arguments
: '(' ')'
| '(' positional_argument_list (',' named_argument_list)? ')'
| '(' named_argument_list ')'
;
positional_argument_list
: positional_argument (',' positional_argument)*
;
positional_argument
: argument_name? attribute_argument_expression
;
named_argument_list
: named_argument (',' named_argument)*
;
named_argument
: identifier '=' attribute_argument_expression
;
attribute_argument_expression
: non_assignment_expression
;
对于 global_attribute_target 的生成,以及在以下文本中,标识符的拼写应等于 assembly
或 module
,其中相等性如 §6.4.3 中所定义。 对于 attribute_target 的生成,以及在以下文本中,标识符的拼写应不等于 assembly
或 module
,使用与上述相同的相等性定义。
属性由 attribute_name 以及可选的位置参数和命名参数列表组成。 位置参数(如果有)位于命名参数之前。 位置参数由 attribute_argument_expression 组成;命名参数由名称、等号和 attribute_argument_expression 组成,它们共同受与简单赋值相同的规则约束。 命名参数的顺序并不重要。
注意:为方便起见,global_attribute_section 和 attribute_section 中允许使用尾随逗号,就像 array_initializer (§17.7) 中允许的那样。 尾注
attribute_name 标识属性类。
当属性放在全局级别时,需要 global_attribute_target_specifier。 当 global_attribute_target 等于:
-
assembly
— 目标是包含的程序集 -
module
— 目标是包含的模块
不允许 global_attribute_target 有其他值。
标准化的 attribute_target 名称为 event
、field
、method
、param
、property
、return
、type
和 typevar
。 这些目标名称仅应在以下上下文中使用:
-
event
— 事件。 -
field
— 字段。 类字段事件(即没有访问器的事件)(§15.8.2) 和自动实现的属性 (§15.7.4) 也可以具有以此为目标的属性。 -
method
— 构造函数、终结器、方法、运算符、属性的 get 和 set 访问器、索引器的 get 和 set 访问器以及事件的 add 和 remove 访问器。 类字段事件(即没有访问器的事件)也可以具有以此为目标的属性。 -
param
— 属性的 set 访问器、索引器的 set 访问器、事件的 add 和 remove 访问器以及构造函数、方法和运算符中的参数。 -
property
— 属性和索引器。 -
return
— 委托、方法、运算符、属性的 get 访问器和索引器的 get 访问器。 -
type
— 委托、类、结构、枚举和接口。 -
typevar
— 类型参数。
某些上下文允许在多个目标上指定属性。 程序可以通过包含 attribute_target_specifier 来显式指定目标。 没有 attribute_target_specifier 时,将应用默认值,但可以使用 attribute_target_specifier 来确认或覆盖默认值。 上下文解析如下:
- 对于委托声明上的属性,默认目标是委托。 否则,当 attribute_target 等于:
-
type
— 目标是委托 -
return
— 目标是返回值
-
- 对于方法声明上的属性,默认目标是方法。 否则,当 attribute_target 等于:
-
method
— 目标是方法 -
return
— 目标是返回值
-
- 对于运算符声明上的属性,默认目标是运算符。 否则,当 attribute_target 等于:
-
method
— 目标是运算符 -
return
— 目标是返回值
-
- 对于属性或索引器声明的 get 访问器声明上的属性,默认目标是关联的方法。 否则,当 attribute_target 等于:
-
method
— 目标是关联的方法 -
return
— 目标是返回值
-
- 对于属性或索引器声明的 set 访问器上指定的属性,默认目标是关联的方法。 否则,当 attribute_target 等于:
-
method
— 目标是关联的方法 -
param
— 目标是唯一的隐式参数
-
- 对于自动实现的属性声明上的属性,默认目标是属性。 否则,当 attribute_target 等于:
-
field
— 目标是属性的编译器生成的支持字段
-
- 对于省略 event_accessor_declarations 的事件声明上指定的属性,默认目标是事件声明。 否则,当 attribute_target 等于:
-
event
— 目标是事件声明 -
field
— 目标是字段 -
method
— 目标是方法
-
- 如果事件声明未省略 event_accessor_declarations,则默认目标是方法。
-
method
— 目标是关联的方法 -
param
— 目标是唯一的参数
-
在所有其他上下文中,允许包含 attribute_target_specifier,但并非必需。
示例:类声明可以包含或省略说明符
type
:[type: Author("Brian Kernighan")] class Class1 {} [Author("Dennis Ritchie")] class Class2 {}
结束示例。
实现可以接受其他 attribute_target,其用途由实现定义。 不识别此类 attribute_target 的实现应发出警告并忽略包含的 attribute_section。
按照约定,属性类的命名带有 Attribute
后缀。
attribute_name 可以包含或省略此后缀。 具体而言,attribute_name 的解析如下:
- 如果 attribute_name 的最右侧标识符是逐字标识符 (§6.4.3),则 attribute_name 被解析为 type_name (§7.8)。 如果结果不是从
System.Attribute
派生的类型,则会发生编译时错误。 - 否则,
如果上述两个步骤中恰好有一个步骤得到从 System.Attribute
派生的类型,则该类型为 attribute_name 的结果。 否则,会发生编译时错误。
示例:如果发现同时存在带此后缀和不带此后缀的属性类,则存在歧义,并会导致编译时错误。 如果 attribute_name 的拼写使其最右侧的标识符是逐字标识符 (§6.4.3),则仅匹配不带后缀的属性,从而能够解决此类歧义。 示例
[AttributeUsage(AttributeTargets.All)] public class Example : Attribute {} [AttributeUsage(AttributeTargets.All)] public class ExampleAttribute : Attribute {} [Example] // Error: ambiguity class Class1 {} [ExampleAttribute] // Refers to ExampleAttribute class Class2 {} [@Example] // Refers to Example class Class3 {} [@ExampleAttribute] // Refers to ExampleAttribute class Class4 {}
展示了两个名为
Example
和ExampleAttribute
的属性类。 属性[Example]
存在歧义,因为它可能指Example
或ExampleAttribute
。 在这种罕见情况下,使用逐字标识符可以指定确切意图。 属性[ExampleAttribute]
没有歧义(虽然如果存在名为ExampleAttributeAttribute
的属性类,它就会有歧义!)。 如果移除类Example
的声明,则两个属性都指名为ExampleAttribute
的属性类,如下所示:[AttributeUsage(AttributeTargets.All)] public class ExampleAttribute : Attribute {} [Example] // Refers to ExampleAttribute class Class1 {} [ExampleAttribute] // Refers to ExampleAttribute class Class2 {} [@Example] // Error: no attribute named “Example” class Class3 {}
结束示例
在同一实体上多次使用单用途属性类是编译时错误。
示例:示例
[AttributeUsage(AttributeTargets.Class)] public class HelpStringAttribute : Attribute { public HelpStringAttribute(string value) { Value = value; } public string Value { get; } } [HelpString("Description of Class1")] [HelpString("Another description of Class1")] // multiple uses not allowed public class Class1 {}
会导致编译时错误,因为它尝试在
HelpString
的声明上多次使用Class1
(单用途属性类)。结束示例
当且仅当满足以下所有条件时,表达式 E
是 attribute_argument_expression:
-
E
的类型是属性参数类型 (§22.2.4)。 - 在编译时,
E
的值可解析为以下之一:
示例:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field)] public class TestAttribute : Attribute { public int P1 { get; set; } public Type P2 { get; set; } public object P3 { get; set; } } [Test(P1 = 1234, P3 = new int[]{1, 3, 5}, P2 = typeof(float))] class MyClass {} class C<T> { [Test(P2 = typeof(T))] // Error – T not a closed type. int x1; [Test(P2 = typeof(C<T>))] // Error – C<;T>; not a closed type. int x2; [Test(P2 = typeof(C<int>))] // Ok int x3; [Test(P2 = typeof(C<>))] // Ok int x4; }
结束示例
在多个部分中声明的类型的属性是通过以未指定的顺序组合其每个部分的属性来确定的。 如果同一属性放在多个部分上,则等效于在该类型上多次指定该属性。
示例:两个部分:
[Attr1, Attr2("hello")] partial class A {} [Attr3, Attr2("goodbye")] partial class A {}
等效于以下单个声明:
[Attr1, Attr2("hello"), Attr3, Attr2("goodbye")] class A {}
结束示例
类型参数上的属性以相同的方式组合。
22.4 属性实例
22.4.1 常规
属性实例是在运行时表示属性的实例。 属性由属性类、位置参数和命名参数定义。 属性实例是使用位置参数和命名参数初始化的属性类的实例。
属性实例的检索涉及编译时和运行时处理,如下述子子句所述。
22.4.2 属性的编译
在程序实体 上指定的、具有属性类 T
、positional_argument_listP
、named_argument_listN
的E
的编译通过以下步骤编译到程序集 A
中:
- 遵循编译新 形式的
T(P)
的编译时处理步骤。 这些步骤要么导致编译时错误,要么确定可在运行时调用的C
上的实例构造函数T
。 - 如果
C
不具有公共可访问性,则会发生编译时错误。 - 对于 中的每个
Arg
N
:- 将
Name
设为 named_argument 的Arg
。 -
Name
应标识T
上的非静态读写公共字段或属性。 如果T
没有此类字段或属性,则会发生编译时错误。
- 将
- 如果 positional_argument_list
P
中的任何值或 named_argument_listN
中的任何值的类型为System.String
,且该值不是 Unicode 标准所定义的格式良好的值,则编译的值是否等于检索到的运行时值 (§22.4.3) 由实现定义。注意:例如,包含高代理 UTF-16 代码单元且其后没有紧跟低代理代码单元的字符串格式不良好。 尾注
- 将以下信息(用于属性的运行时实例化)存储在编译器输出的程序集中,该程序集是编译包含该属性的程序的结果:属性类
T
、C
上的实例构造函数T
、positional_argument_listP
、named_argument_listN
以及关联的程序实体E
,其值在编译时完全解析。
22.4.3 属性实例的运行时检索
使用 §22.4.2 中定义的术语,可通过以下步骤在运行时从程序集 T
中检索由 C
、P
、N
和 E
表示并与 A
关联的属性实例:
- 遵循执行 形式的
new T(P)
的运行时处理步骤,使用实例构造函数C
和编译时确定的值。 这些步骤要么导致异常,要么生成O
的实例T
。 - 对于 中的每个
Arg
N
,按顺序:- 将
Name
设为 named_argument 的Arg
。 如果Name
未标识O
上的非静态公共读写字段或属性,则引发异常。 - 将
Value
设为计算 的Arg
的结果。 - 如果
Name
标识O
上的字段,则将此字段设置为Value
。 - 否则,Name 标识
O
上的属性。 将此属性设置为 Value。 - 结果是
O
,即已使用T
和P
初始化的属性类N
的实例。
- 将
注意:在
T
中存储C
、P
、N
、E
(并将其与A
关联)的格式以及指定E
并从T
中检索C
、P
、N
、A
(从而在运行时获取属性实例)的机制超出了本规范的范围。 尾注
22.5 保留属性
22.5.1 常规
许多属性在某种程度上会影响语言。 这些属性包括:
-
System.AttributeUsageAttribute
(§22.5.2),用于描述属性类的使用方式。 -
System.Diagnostics.ConditionalAttribute
(§22.5.3),是一个多用途属性类,用于定义条件方法和条件属性类。 此属性通过测试条件编译符号来指示条件。 -
System.ObsoleteAttribute
(§22.5.4),用于将成员标记为已过时。 -
System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
(§22.5.5),用于为异步方法建立任务生成器。 -
System.Runtime.CompilerServices.CallerLineNumberAttribute
(§22.5.6.2)、System.Runtime.CompilerServices.CallerFilePathAttribute
(§22.5.6.3) 和System.Runtime.CompilerServices.CallerMemberNameAttribute
(§22.5.6.4),用于向可选参数提供有关调用上下文的信息。
可为 null 静态分析属性 (§22.5.7) 可以提高为可为 null 性和 null 状态生成的警告的正确性 (§8.9.5)。
执行环境可以提供影响 C# 程序执行的其他实现定义的属性。
22.5.2 AttributeUsage 属性
属性 AttributeUsage
用于描述属性类的使用方式。
用 AttributeUsage
属性修饰的类必须直接或间接从 System.Attribute
派生。 否则,会发生编译时错误。
注意:有关使用此属性的示例,请参阅 §22.2.2。 尾注
22.5.3 条件属性
22.5.3.1 常规
属性 Conditional
支持 conditional methods 和 conditional attribute classes。
22.5.3.2 条件方法
用 Conditional
属性修饰的方法是条件方法。 因此,每个条件方法都与在其 Conditional
属性中声明的条件编译符号相关联。
示例:
class Eg { [Conditional("ALPHA")] [Conditional("BETA")] public static void M() { // ... } }
将
Eg.M
声明为与两个条件编译符号ALPHA
和BETA
相关联的条件方法。结束示例
如果在调用点定义了一个或多个相关联的条件编译符号,则包含对条件方法的调用,否则省略该调用。
条件方法受以下限制:
- 条件方法必须是 class_declaration 或 struct_declaration 中的方法。 如果在接口声明的方法上指定了
Conditional
属性,则会发生编译时错误。 - 条件方法的返回类型必须为
void
。 - 条件方法不得用
override
修饰符标记。 但是,条件方法可以用virtual
修饰符标记。 此类方法的重写是隐式条件的,不得显式用Conditional
属性标记。 - 条件方法不得是接口方法的实现。 否则,会发生编译时错误。
- 条件方法的参数不得是输出参数。
此外,如果从条件方法创建委托,会发生编译时错误。
示例:示例
#define DEBUG using System; using System.Diagnostics; class Class1 { [Conditional("DEBUG")] public static void M() { Console.WriteLine("Executed Class1.M"); } } class Class2 { public static void Test() { Class1.M(); } }
将
Class1.M
声明为条件方法。Class2
的Test
方法调用此方法。 由于已定义条件编译符号DEBUG
,如果调用Class2.Test
,它将调用M
。 如果未定义符号DEBUG
,则Class2.Test
不会调用Class1.M
。结束示例
重要的是要理解,对条件方法的调用的包含或排除由调用点的条件编译符号控制。
示例:在以下代码中
// File Class1.cs: using System; using System.Diagnostics; class Class1 { [Conditional("DEBUG")] public static void F() { Console.WriteLine("Executed Class1.F"); } } // File Class2.cs: #define DEBUG class Class2 { public static void G() { Class1.F(); // F is called } } // File Class3.cs: #undef DEBUG class Class3 { public static void H() { Class1.F(); // F is not called } }
类
Class2
和Class3
各自包含对条件方法Class1.F
的调用,该方法是否执行取决于是否定义了DEBUG
。 由于此符号在Class2
的上下文中定义,但在Class3
中未定义,因此包含F
中对Class2
的调用,而省略F
中对Class3
的调用。结束示例
继承链中条件方法的使用可能会造成混淆。 通过 base
对条件方法的调用(形式为 base.M
)需遵循正常的条件方法调用规则。
示例:在以下代码中
// File Class1.cs using System; using System.Diagnostics; class Class1 { [Conditional("DEBUG")] public virtual void M() => Console.WriteLine("Class1.M executed"); } // File Class2.cs class Class2 : Class1 { public override void M() { Console.WriteLine("Class2.M executed"); base.M(); // base.M is not called! } } // File Class3.cs #define DEBUG class Class3 { public static void Main() { Class2 c = new Class2(); c.M(); // M is called } }
Class2
包含对其基类中定义的M
的调用。 此调用被省略,因为基方法是条件性的,取决于符号DEBUG
的存在,而该符号未定义。 因此,该方法仅向控制台输出“Class2.M executed
”。 明智地使用 pp_declaration 可以消除此类问题。结束示例
22.5.3.3 条件属性类
用一个或多个 属性修饰的属性类 (Conditional
) 是条件属性类。 因此,条件属性类与在其 Conditional
属性中声明的条件编译符号相关联。
示例:
[Conditional("ALPHA")] [Conditional("BETA")] public class TestAttribute : Attribute {}
将
TestAttribute
声明为与条件编译符号ALPHA
和BETA
相关联的条件属性类。结束示例
如果在规范点定义了一个或多个相关联的条件编译符号,则包含条件属性的属性规范 (§22.3),否则省略该属性规范。
重要的是要注意,条件属性类的属性规范的包含或排除由规范点的条件编译符号控制。
示例:在示例中
// File Test.cs: using System; using System.Diagnostics; [Conditional("DEBUG")] public class TestAttribute : Attribute {} // File Class1.cs: #define DEBUG [Test] // TestAttribute is specified class Class1 {} // File Class2.cs: #undef DEBUG [Test] // TestAttribute is not specified class Class2 {}
类
Class1
和Class2
各自用属性Test
修饰,该属性是否生效取决于是否定义了DEBUG
。 由于此符号在Class1
的上下文中定义,但在Class2
中未定义,因此包含Class1
上的 Test 属性规范,而省略Test
上的Class2
属性规范。结束示例
22.5.4 已过时属性
属性 Obsolete
用于标记不应再使用的类型和类型成员。
如果程序使用用 Obsolete
属性修饰的类型或成员,编译器应发出警告或错误。 具体而言,如果未提供错误参数,或者提供了错误参数并且具有值 false
,编译器应发出警告。 如果指定了错误参数并具有值 true
,编译器应发出错误。
示例:在以下代码中
[Obsolete("This class is obsolete; use class B instead")] class A { public void F() {} } class B { public void F() {} } class Test { static void Main() { A a = new A(); // Warning a.F(); } }
类
A
用Obsolete
属性修饰。 在A
中每次使用Main
都会产生一个警告,其中包含指定的消息“此类已过时;请改用类B
”。结束示例
22.5.5 AsyncMethodBuilder 属性
此属性在 §15.14.1 中介绍。
22.5.6 调用方信息属性
22.5.6.1 常规
出于日志记录和报告等目的,函数成员有时需要获取有关调用代码的某些编译时信息。 调用方信息属性提供了一种透明传递此类信息的方式。
当可选参数用某个调用方信息属性批注时,在调用中省略相应的参数不一定会导致替换默认参数值。 相反,如果有关调用上下文的指定信息可用,则该信息将作为参数值传递。
示例:
public void Log( [CallerLineNumber] int line = -1, [CallerFilePath] string path = null, [CallerMemberName] string name = null ) { Console.WriteLine((line < 0) ? "No line" : "Line "+ line); Console.WriteLine((path == null) ? "No file path" : path); Console.WriteLine((name == null) ? "No member name" : name); }
不带参数调用
Log()
会打印调用的行号和文件路径,以及发生调用的成员的名称。结束示例
调用方信息属性可出现在任何位置的可选参数上,包括委托声明中。 但是,特定的调用方信息属性对其可批注的参数类型有限制,因此从替换值到参数类型始终存在隐式转换。
在分部方法声明的定义部分和实现部分的参数上使用相同的调用方信息属性是错误的。 仅应用定义部分中的调用方信息属性,而仅在实现部分中出现的调用方信息属性将被忽略。
调用方信息不影响重载解析。 由于带属性的可选参数仍从调用方的源代码中省略,重载解析对这些参数的忽略方式与对其他省略的可选参数的忽略方式相同 (§12.6.4)。
仅当在源代码中显式调用函数时,才会替换调用方信息。 隐式调用(如隐式父构造函数调用)没有源位置,不会替换调用方信息。 此外,动态绑定的调用不会替换调用方信息。 在这种情况下,如果省略带调用方信息属性的参数,则改用该参数的指定默认值。
一个例外是查询表达式。 这些被视为语法扩展,如果它们展开的调用省略了带调用方信息属性的可选参数,则会替换调用方信息。 所使用的位置是生成调用的查询子句的位置。
如果在给定参数上指定了多个调用方信息属性,则按以下顺序识别它们:CallerLineNumber
、CallerFilePath
、CallerMemberName
。 考虑以下参数声明:
[CallerMemberName, CallerFilePath, CallerLineNumber] object p = ...
CallerLineNumber
优先,另外两个属性被忽略。 如果省略 CallerLineNumber
,则 CallerFilePath
优先,而 CallerMemberName
会被忽略。 这些属性的词法顺序无关紧要。
22.5.6.2 CallerLineNumber 属性
当从常量值 System.Runtime.CompilerServices.CallerLineNumberAttribute
到参数类型存在标准隐式转换 (§10.4.2) 时,属性 int.MaxValue
允许用于可选参数。 这确保任何不超过该值的非负行号都可以正确传递。
如果来自源代码中某个位置的函数调用省略了带 CallerLineNumberAttribute
的可选参数,则表示该位置行号的数值文字将用作调用的参数,而非默认参数值。
如果调用跨多行,则所选行由实现定义。
行号可能会受 #line
指令 (§6.5.8) 影响。
22.5.6.3 CallerFilePath 属性
当从 System.Runtime.CompilerServices.CallerFilePathAttribute
到参数类型存在标准隐式转换 (§10.4.2) 时,属性 string
允许用于可选参数。
如果来自源代码中某个位置的函数调用省略了带 CallerFilePathAttribute
的可选参数,则表示该位置文件路径的字符串字面量将用作调用的参数,而非默认参数值。
文件路径的格式由实现定义。
文件路径可能会受 #line
指令 (§6.5.8) 影响。
22.5.6.4 CallerMemberName 属性
当从 System.Runtime.CompilerServices.CallerMemberNameAttribute
到参数类型存在标准隐式转换 (§10.4.2) 时,属性 string
允许用于可选参数。
如果来自函数成员体内或应用于函数成员本身及其返回类型、参数或类型参数的属性内的源代码位置的函数调用省略了带 CallerMemberNameAttribute
的可选参数,则表示该成员名称的字符串字面量将用作调用的参数,而非默认参数值。
对于在泛型方法内发生的调用,仅使用方法名称本身,不带类型参数列表。
对于在显式接口成员实现内发生的调用,仅使用方法名称本身,不带前面的接口限定。
对于在属性或事件访问器内发生的调用,所使用的成员名称是属性或事件本身的名称。
对于在索引器访问器内发生的调用,所使用的成员名称是索引器成员上 IndexerNameAttribute
(§22.6) 提供的名称(如果存在),否则是默认名称 Item
。
对于在字段或事件初始值设定项内发生的调用,所使用的成员名称是正在初始化的字段或事件的名称。
对于在实例构造函数、静态构造函数、终结器和运算符的声明内发生的调用,所使用的成员名称由实现定义。
22.5.7 代码分析属性
22.5.7.1 常规
此子条款中的属性用于提供附加信息,以支持提供可空性和空状态诊断的编译器(§8.9.5)。 编译器无需执行任何 null 状态诊断。 这些属性的存在与否不影响语言或程序的行为。 不提供 null 状态诊断的编译器应读取并忽略这些属性的存在。 提供 null 状态诊断的编译器在使用这些属性为其诊断提供信息时,应使用本子条款中定义的含义。
代码分析属性在命名空间 System.Diagnostics.CodeAnalysis
中声明。
属性 | 含义 |
---|---|
AllowNull (§22.5.7.2) |
不可为 null 的参数可以为 null。 |
DisallowNull (§22.5.7.3) |
可为 null 的参数不应为 null。 |
MaybeNull (§22.5.7.6) |
不可为 null 的返回值可以为 null。 |
NotNull (§22.5.7.8) |
可为 null 的返回值永远不会为 null。 |
MaybeNullWhen (§22.5.7.7) |
当方法返回指定的 bool 值时,不可为 null 的参数可以为 null。 |
NotNullWhen (§22.5.7.10) |
当方法返回指定的 bool 值时,可为 null 的参数不会为 null。 |
NotNullIfNotNull (§22.5.7.9) |
如果指定参数的参数不为 null,则返回值不为 null。 |
DoesNotReturn (§22.5.7.4) |
此方法从不返回。 |
DoesNotReturnIf (§22.5.7.5) |
如果关联的 bool 参数具有指定值,则此方法永远不会返回。 |
§22.5.7.1 中的以下子项是有条件的规范性的。
22.5.7.2 AllowNull 属性
指定即使相应类型不允许,也允许 null 值作为输入。
示例:考虑以下读写属性,该属性从不返回
null
,因为它有合理的默认值。 但是,用户可以向 set 访问器提供 null,以将该属性设置为该默认值。#nullable enable public class X { [AllowNull] public string ScreenName { get => _screenName; set => _screenName = value ?? GenerateRandomScreenName(); } private string _screenName = GenerateRandomScreenName(); private static string GenerateRandomScreenName() => ...; }
给定对该属性的 set 访问器的以下使用
var v = new X(); v.ScreenName = null; // may warn without attribute AllowNull
如果没有该属性,编译器可能会生成警告,因为非空类型属性似乎被设置为了 null 值。 该属性的存在会抑制该警告。 结束示例
22.5.7.3 DisallowNull 属性
指定即使相应类型允许,也不允许 null 值作为输入。
示例:考虑以下属性,其中 null 是默认值,但客户端只能将其设置为非 null 值。
#nullable enable public class X { [DisallowNull] public string? ReviewComment { get => _comment; set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null"); } private string? _comment = default; }
get 访问器可以返回
null
的默认值,因此编译器可能会在访问它之前警告必须对其进行检查。 此外,它会警告调用方,即使它可能为 null,调用方也不应显式将其设置为 null。 结束示例
22.5.7.4 DoesNotReturn 属性
指定给定方法从不返回。
示例:请考虑以下示例:
public class X { [DoesNotReturn] private void FailFast() => throw new InvalidOperationException(); public void SetState(object? containedField) { if ((!isInitialized) || (containedField == null)) { FailFast(); } // null check not needed. _field = containedField; } private bool isInitialized = false; private object _field; }
属性的存在通过多种方式帮助编译器。 首先,如果存在方法可以在不引发异常的情况下退出的路径,编译器可能会发出警告。 其次,在调用该方法之后,编译器可以压制任何代码中的可为 null 的警告,直到找到适当的 catch 子句。 第三,无法访问的代码不会影响任何 null 状态。
该属性不会基于此属性的存在更改可达性 (§13.2) 或明确赋值 (§9.4) 分析。 它仅用于影响可为 null 性警告。 结束示例
22.5.7.5 DoesNotReturnIf 属性
指定如果关联的 bool
参数具有指定值,则给定方法从不返回。
示例:请考虑以下示例:
#nullable enable public class X { private void ThrowIfNull([DoesNotReturnIf(true)] bool isNull, string argumentName) { if (!isNull) { throw new ArgumentException(argumentName, $"argument {argumentName} can't be null"); } } public void SetFieldState(object containedField) { ThrowIfNull(containedField == null, nameof(containedField)); // unreachable code when "isInitialized" is false: _field = containedField; } private bool isInitialized = false; private object _field = default!; }
结束示例
22.5.7.6 MaybeNull 属性
指定不可为 null 的返回值可能为 null。
示例:考虑以下泛型方法:
#nullable enable public T? Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate) { ... }
此代码的思路是,如果
T
被string
替换,T?
会成为可为 null 的批注。 但是,此代码不合法,因为T
未被约束为引用类型。 但是,添加此属性可解决该问题:#nullable enable [return: MaybeNull] public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate) { ... }
该属性告知调用方,契约暗示不可为 null 的类型,但返回值实际上可能为
null
。 结束示例
22.5.7.7 MaybeNullWhen 属性
指定当方法返回指定的 null
值时,不可为 null 的参数可能为 bool
。 这与 MaybeNull
属性 (§22.5.7.6) 类似,但包含指定返回值的参数。
22.5.7.8 NotNull 属性
指定如果方法返回(而非引发异常),可为 null 的值将永远不会为 null
。
示例:请考虑以下示例:
#nullable enable public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "") => _ = value ?? throw new ArgumentNullException(valueExpression); public static void LogMessage(string? message) { ThrowWhenNull(message, nameof(message)); Console.WriteLine(message.Length); }
启用 null 引用类型时,方法
ThrowWhenNull
编译时不会有警告。 当该方法返回时,value
参数保证不为null
。 但是,使用 null 引用调用ThrowWhenNull
是可接受的。 结束示例
22.5.7.9 NotNullIfNotNull 属性
指定如果指定参数的参数不为 null
,则返回值不为 null
。
示例:返回值的 null 状态可能取决于一个或多个参数的 null 状态。 在某些参数不为
null
时,可以使用NotNullIfNotNull
属性,以帮助编译器分析该方法始终返回非 null 值。 请考虑以下方法:#nullable enable string GetTopLevelDomainFromFullUrl(string url) { ... }
如果
url
参数不为null
,则不返回null
。 启用可为 null 的引用时,只要 API 从不接受 null 参数,该签名就能正常工作。 但是,如果参数可能为 null,则返回值也可能为 null。 要正确表达该契约,请按以下方式批注此方法:#nullable enable [return: NotNullIfNotNull("url")] string? GetTopLevelDomainFromFullUrl(string? url) { ... }
结束示例
22.5.7.10 NotNullWhen 属性
指定当方法返回指定的 null
值时,可为 null 的参数不会为 bool
。
示例:库方法
String.IsNullOrEmpty(String)
在参数为true
或空字符串时返回null
。 这是一种 null 检查形式:如果方法返回false
,调用方无需对参数进行 null 检查。 要使此类方法支持可为 null 性,请将参数类型设为可为 null 的引用类型,并添加 NotNullWhen 属性:#nullable enable bool IsNullOrEmpty([NotNullWhen(false)] string? value) { ... }
结束示例
22.6 互操作属性
为了与其他语言互操作,索引器可以使用索引属性来实现。 如果索引器没有 IndexerName
属性,则默认使用名称 Item
。
IndexerName
属性使开发人员能够覆盖此默认值并指定不同的名称。
示例:默认情况下,索引器的名称为
Item
。 可以按以下方式覆盖:[System.Runtime.CompilerServices.IndexerName("TheItem")] public int this[int index] { get { ... } set { ... } }
现在,索引器的名称为
TheItem
。结束示例