22 个属性

22.1 常规

大部分 C# 语言使程序员能够指定有关程序中定义的实体的声明性信息。 例如,类中方法的可访问性是通过用 method_modifierspublicprotectedinternalprivate修饰方法指定的。

C# 使程序员能够发明新的声明性信息,称为 属性。 然后,程序员可以将属性附加到各种程序实体,并在运行时环境中检索属性信息。

注意:例如,框架可以定义 HelpAttribute 可放置在某些程序元素(如类和方法)上的属性,以提供从这些程序元素到其文档的映射。 end note

特性通过属性类(§22.2)的声明来定义,该声明可以具有位置参数和命名参数(§22.2.3)。 特性使用属性规范(§22.3)附加到 C# 程序中的实体,并且可以在运行时作为属性实例(§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

end 示例

22.2.2 属性用法

特性 AttributeUsage§22.5.2)用于描述如何使用特性类。

AttributeUsage 具有一个位置参数(§22.2.3),它使特性类能够指定可以使用它的程序实体的类型。

示例:以下示例定义一个名为“仅class_declaration”和“interface_declaration”的属性类SimpleAttribute,并显示属性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 {...}

end 示例

AttributeUsage 具有一个命名参数(§22.2.3), 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 
{
    ...
}

end 示例

AttributeUsage 调用了另一个命名参数 (§22.2.3), Inherited该参数指示属性在基类上指定时是否也由派生自该基类的类继承。 如果 Inherited 属性类为 true,则继承该属性。 如果 Inherited 属性类为 false,则不会继承该属性。 如果未指定,则其默认值为 true。

属性类 XAttributeUsage 附加属性,如中所示

class X : Attribute { ... }

等效于以下内容:

[AttributeUsage(
   AttributeTargets.All,
   AllowMultiple = false,
   Inherited = true)
]
class X : Attribute { ... }

22.2.3 位置参数和命名参数

属性类可以具有位置参数s 和命名参数 特性类的每个公共实例构造函数定义该属性类的有效位置参数序列。 属性类的每个非静态公共读写字段和属性都定义特性类的命名参数。 要使属性定义命名参数,该属性应同时具有公共 get 访问器和公共集访问器。

示例:以下示例定义一个名为具有一个位置参数url和一个命名参数Topic的属性类HelpAttribute。 虽然它是非静态和公共的,但属性 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
{
}

end 示例

22.2.4 属性参数类型

属性类的位置参数和命名参数的类型仅限于 属性参数类型,这些类型包括:

  • 以下类型之一:bool、、、charbytefloatdoubleintlongsbyte、、shortstring、、 。 ushortuintulong
  • object 类型。
  • System.Type 类型。
  • 枚举类型。
  • 上述类型的单维数组。
  • 没有这些类型的构造函数参数或公共字段不得用作属性规范中的位置或命名参数。

22.3 属性规范

属性规范 是将以前定义的属性应用于程序实体。 属性是为程序实体指定的附加声明性信息片段。 可以在全局范围(指定包含程序集或模块上的属性)和 type_declaration s (§14.7)、 class_member_declarations (§15.3), interface_member_declarations (§15.318.4)、struct_member_declarations (§16.3)、enum_member_declarations (§19.2)、accessor_declarations (§15.7.3),event_accessor_declarations (§15.8)、parameter_lists (§15.6.2) 的元素和 type_parameter_lists (§15.2.3) 的元素。

属性在属性节指定。 属性节由一对方括号组成,这些方括号括在一个或多个属性的逗号分隔列表中。 在此类列表中指定属性的顺序以及附加到同一程序实体的节的排列顺序并不重要。 例如,属性规范、[B][A]属性[A, B]规范[A][B][B, A]等效。

global_attributes
    : global_attribute_section+
    ;

global_attribute_section
    : '[' global_attribute_target_specifier attribute_list ']'
    | '[' 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_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,在下面的文本中,标识符的拼写应等于assemblymodule相等,其中相等性是在 §6.4.3定义的。 对于生产attribute_target,在下面的文本中,标识符的拼写应与assemblymodule上述相同或使用相同的相等定义。

属性由attribute_name和位置参数和命名参数的可选列表组成。 位置参数(如果存在)位于命名参数之前。 位置参数由attribute_argument_expression组成;命名参数由名称组成,后跟等号,后跟attribute_argument_expression,一起受简单赋值相同的规则约束。 命名参数的顺序并不重要。

注意:为方便起见,global_attribute_section和attribute_section中允许尾随逗号,就像在array_initializer(§17.7)中允许一样。 end note

attribute_name标识属性类。

将属性置于全局级别时, 需要global_attribute_target_specifier当global_attribute_target等于:

  • assembly — 目标为包含程序集
  • module — 目标为包含模块

不允许global_attribute_target的其他值

标准化 attribute_target 名称为 eventfieldmethodparampropertyreturn、、 typetypevar。 这些目标名称只能在以下上下文中使用:

  • event — 事件。
  • field — 字段。 类似字段的事件(即一个没有访问器)(§15.8.2)和自动实现的属性(§15.7.4)也可以具有具有此目标的属性。
  • method — 构造函数、终结器、方法、运算符、属性获取和设置访问器、索引器获取和设置访问器,以及事件添加和删除访问器。 类似字段的事件(即没有访问器)也可以具有具有此目标的属性。
  • param — 属性集访问器、索引器集访问器、事件添加和删除访问器,以及构造函数、方法和运算符中的参数。
  • 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 {}

end 示例

实现可以接受其他 attribute_target,其用途是定义的实现。 无法识别此类 attribute_target 的实现应发出警告并忽略包含 attribute_section

按照约定,属性类以后缀 Attribute命名。 attribute_name可以包含或省略此后缀。 具体而言, attribute_name 解析如下:

  • 如果attribute_name最右侧的标识符是逐字标识符(§6.4.3),则attribute_name解析为type_name§7.8)。 如果结果不是派生自 System.Attribute的类型,则会发生编译时错误。
  • 否则为
    • attribute_name解析为type_name§7.8),但禁止显示任何错误。 如果此解析成功,并生成派生自 System.Attribute 的类型,则该类型是此步骤的结果。
    • 这些字符Attribute将追加到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 {}

显示两个名为 ExampleExampleAttribute. 的属性类。 该属性[Example]不明确,因为它可以引用或ExampleExampleAttribute引用 。 使用逐字标识符可在此类罕见情况下指定确切意向。 该属性 [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 {}

end 示例

在同一实体上多次使用单用属性类是编译时错误。

示例:示例

[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

end 示例

如果以下所有语句均为 true,则表达式 Eattribute_argument_expression

  • E类型为特性参数类型(§22.2.4)。
  • 在编译时,可以将该值 E 解析为下列值之一:
    • 常数值。
    • System.Type使用 typeof_expression指定非泛型类型、封闭构造类型(§8.4.3)或未绑定泛型类型(§8.4.4)而不是开放类型(§8.4.4)获取的对象(§12.8.18)。
    • attribute_argument_expression的一维数组

示例:

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

end 示例

在多个部件中声明的类型的属性通过按未指定的顺序组合其每个部分的属性来确定。 如果同一属性放置在多个部件上,则等效于在类型上多次指定该属性。

示例:这两个部分:

[Attr1, Attr2("hello")]
partial class A {}

[Attr3, Attr2("goodbye")]
partial class A {}

等效于以下单个声明:

[Attr1, Attr2("hello"), Attr3, Attr2("goodbye")]
class A {}

end 示例

类型参数的属性以相同的方式组合。

22.4 属性实例

22.4.1 常规

属性实例是表示运行时属性的实例。 使用特性类、位置参数和命名参数定义特性。 属性实例是使用位置参数和命名参数初始化的属性类的实例。

检索属性实例涉及编译时和运行时处理,如以下子集中所述。

22.4.2 属性的编译

使用特性类Tpositional_argument_listPnamed_argument_listN的属性编译,并通过E以下步骤编译到程序集A中:

  • 按照编译时处理步骤来编译新窗体T(P)的object_creation_expression。 这些步骤会导致编译时错误,或确定可以在运行时调用的实例构造函数CT
  • 如果没有 C 公共辅助功能,则会发生编译时错误。
  • 对于每个named_argumentArgN
    • 让我们Name成为named_argumentArg标识符
    • Name应标识非静态读/写公共字段或属性。T 如果没有 T 此类字段或属性,则会发生编译时错误。
  • 如果positional_argument_listP中的任何值或named_argument_listN中的某个值的类型System.String,并且值的格式不正确,则编译的值是否等于检索的运行时值(§22.4.3),则由实现定义。

    注意:例如,包含高代理项 UTF-16 代码单元的字符串,该字符串不紧跟低代理项代码单元的格式不正确。 end note

  • 将以下信息(对于属性的运行时实例化)存储在编译器编译包含特性的程序的程序集输出中:属性类T、实例构造函数CTpositional_argument_list、named_argument_listPN和关联的程序实体E,这些值在编译时完全解析。

22.4.3 属性实例的运行时检索

使用 §22.4.2 中定义的术语,可以使用以下步骤在运行时从程序集A检索由 TCP、 和N关联的E属性实例:

  • 按照运行时处理步骤执行表单new T(P)object_creation_expression,使用编译时确定的实例构造函数C和值。 这些步骤会导致异常,或生成一T个实例O
  • 对于每个 named_argument Arg N,顺序为:
    • 让我们Name成为named_argumentArg标识符。 如果未 Name 标识其上的 O非静态公共读写字段或属性,则会引发异常。
    • 让我们Value计算attribute_argument_expression的结果Arg
    • 如果 Name 标识其上的 O字段,请将此字段设置为 Value
    • 否则,Name 将标识其上的 O属性。 将此属性设置为“值”。
    • 结果是O,已使用positional_argument_listPnamed_argument_listN初始化的属性类T的实例。

注意:存储TCN P(以及将其与E关联)A的格式以及用于指定E和检索TPCNA(因此在运行时获取属性实例的方式)的机制超出了此规范的范围。 end note

示例:在 CLI 的实现中,Help可以使用以下程序检索通过编译 §22.2.3 中的示例程序创建的程序集中的属性实例:

public sealed class InterrogateHelpUrls
{
    public static void Main(string[] args)
    {
        Type helpType = typeof(HelpAttribute);
        string assemblyName = args[0];
        foreach (Type t in Assembly.Load(assemblyName).GetTypes()) 
        {
            Console.WriteLine($"Type : {t}");
            var attributes = t.GetCustomAttributes(helpType, false);
            var helpers = (HelpAttribute[]) attributes;
            foreach (var helper in helpers)
            {
                Console.WriteLine($"\tUrl : {helper.Url}");
            }
        }
    }
}

end 示例

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.2end note

22.5.3 条件属性

22.5.3.1 常规

该属性Conditional启用条件方法和条件属性类的定义。

22.5.3.2 条件方法

使用 Conditional 特性修饰的方法是一个条件方法。 因此,每个条件方法都与其属性中 Conditional 声明的条件编译符号相关联。

示例:

class Eg
{
    [Conditional("ALPHA")]
    [Conditional("BETA")]
    public static void M()
    {
        // ...
    }
}

Eg.M声明为与两个条件编译符号ALPHABETA.

end 示例

如果在调用点定义了一个或多个关联的条件编译符号,则包含对条件方法的调用,否则省略调用。

条件方法受以下限制的约束:

  • 条件方法应是class_declarationstruct_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's Test 方法调用此方法。 由于定义了条件编译符号 DEBUG ,因此如果 Class2.Test 调用,它将调用 M。 如果未定义符号 DEBUG ,则 Class2.Test 不会调用 Class1.M

end 示例

请务必了解,对条件方法的包含或排除由调用点的条件编译符号控制。

示例:在以下代码中

// 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
    }
}

Class2Class3 每个类都包含对条件方法 Class1.F的调用,这是基于是否 DEBUG 定义的条件方法。 由于此符号是在上下文Class2中定义的,但不包括Class3对 in 的调用F,同时省略对F中的Class2Class3调用。

end 示例

在继承链中使用条件方法可能会令人困惑。 通过表单base.Mbase条件方法进行的调用受普通条件方法调用规则的约束。

示例:在以下代码中

// 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可以消除此类问题。

end 示例

22.5.3.3 条件属性类

用一个或多个Conditional属性修饰的属性类(§22.2)是条件属性类。 因此,条件属性类与其属性中 Conditional 声明的条件编译符号相关联。

示例:

[Conditional("ALPHA")]
[Conditional("BETA")]
public class TestAttribute : Attribute {}

TestAttribute声明为与条件编译符号ALPHABETA.

end 示例

如果在规范点定义了一个或多个关联的条件编译符号,则包含条件属性的特性规范(§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 {}

Class1Class2 每个 Test修饰的属性,这是基于是否 DEBUG 定义的有条件的。 由于此符号是在 Class2上下文Class1中定义的,因此将包含 Test 属性Class1的规范,同时省略该Test属性Class2的规范。

end 示例

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 修饰。 每次使用AMain都会导致包含指定消息的警告,“此类已过时;请改用类B

end 示例

22.5.5 AsyncMethodBuilder 属性

此属性在 §15.15.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() 调用将打印调用的行号和文件路径,以及调用在其中发生调用的成员的名称。

end 示例

调用方信息属性可以在任意位置(包括委托声明中)在可选参数上发生。 但是,特定的调用方信息属性对它们可以属性的参数类型有限制,以便始终有从替换值到参数类型的隐式转换。

定义和实现分部方法声明的一部分的参数上具有相同的调用方信息属性是错误的。 仅应用定义部件中的调用方信息属性,而仅在实现部件中发生的调用方信息属性将被忽略。

调用方信息不会影响重载解析。 由于属性化可选参数仍从调用方源代码中省略,重载解析会以忽略其他省略的可选参数(§12.6.4)的方式忽略这些参数。

仅当在源代码中显式调用函数时,调用方信息才会被替换。 隐式调用(如隐式父构造函数调用)没有源位置,并且不会替换调用方信息。 此外,动态绑定的调用不会替换调用方信息。 在这种情况下省略调用方信息特性化参数时,将改用参数的指定默认值。

一个例外是查询表达式。 这些扩展被视为语法扩展,如果它们扩展以省略带有调用方信息属性的可选参数,则将替换调用方信息。 使用的位置是从中生成调用的查询子句的位置。

如果在给定参数上指定了多个调用方信息属性,则按以下顺序识别这些属性: CallerLineNumberCallerFilePathCallerMemberName 请考虑以下参数声明:

[CallerMemberName, CallerFilePath, CallerLineNumber] object p = ...

CallerLineNumber 优先,将忽略其他两个属性。 如果 CallerLineNumber 省略, CallerFilePath 则优先,将被 CallerMemberName 忽略。 这些属性的词法排序无关紧要。

22.5.6.2 CallerLineNumber 属性

当从常量值int.MaxValue到参数类型的标准隐式转换(§10.4.2)时,可对可选参数使用特性System.Runtime.CompilerServices.CallerLineNumberAttribute。 这可确保可以传递值的任何非负行号,而不会出错。

如果源代码中某个位置的函数调用省略了可选参数, CallerLineNumberAttribute则表示该位置的行号的数字文本用作调用的参数,而不是默认参数值。

如果调用跨越多行,则选择的行依赖于实现。

行号可能受 #line 指令(§6.5.8)的影响。

22.5.6.3 CallerFilePath 属性

当有标准隐式转换(§10.4.2)从string参数的类型转换为参数时,可对可选参数使用特性System.Runtime.CompilerServices.CallerFilePathAttribute

如果源代码中某个位置的函数调用省略了可选参数, CallerFilePathAttribute则表示该位置的文件路径的字符串文本用作调用的参数,而不是默认参数值。

文件路径的格式依赖于实现。

文件路径可能受 #line 指令(§6.5.8)的影响。

22.5.6.4 CallerMemberName 属性

当有标准隐式转换(§10.4.2)从string参数的类型转换为参数时,可对可选参数使用特性System.Runtime.CompilerServices.CallerMemberNameAttribute

如果函数从函数成员正文中的位置或应用于函数成员本身或其返回类型的属性中调用函数,则源代码中的参数或类型参数省略一个可选参数 CallerMemberNameAttribute,则表示该成员的名称的字符串文本将用作调用的参数,而不是默认参数值。

对于泛型方法中发生的调用,仅使用方法名称本身,而不使用类型参数列表。

对于在显式接口成员实现中发生的调用,仅使用方法名称本身,而不使用上述接口限定。

对于在属性或事件访问器中发生的调用,使用的成员名称是属性或事件本身的成员名称。

对于索引器访问器中发生的调用,所使用的成员名称由索引器成员(如果存在)或默认名称Item(§22.6)提供IndexerNameAttribute

对于在字段或事件初始值设定项内发生的调用,使用的成员名称是正在初始化的字段或事件的名称。

对于在实例构造函数声明内发生的调用,静态构造函数、终结器和运算符所使用的成员名称依赖于实现。

22.5.7 代码分析属性

22.5.7.1 常规

本节中的属性用于提供其他信息来支持提供可为 null 性和 null 状态诊断(§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 的类型化属性似乎设置为 null 值。 该属性的存在将禁止显示该警告。 end 示例

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。 end 示例

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 性警告。 end 示例

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

end 示例

22.5.7.6 可能Null 属性

指定不可为 null 的返回值可以为 null。

示例:请考虑以下泛型方法:

#nullable enable
public T? Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate) { ... }

此代码的想法是,如果T替换为stringT?可为 null 的批注。 但是,此代码不合法,因为 T 不受约束为引用类型。 但是,添加此属性可解决问题:

#nullable enable
[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate) { ... }

该属性通知调用方协定表示不可为 null 的类型,但返回值实际上可能为 nullend 示例

22.5.7.7 可能NullWhen 属性

指定当方法返回指定bool值时,可能为null不可为 null 的参数。 这类似于 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 是可以接受的。 end 示例

22.5.7.9 NotNullIfNotNull 属性

指定如果指定参数的参数不是 ,则返回值 null 不是 null

示例:返回值的 null 状态可能取决于一个或多个参数的 null 状态。 当某些参数不是nullNotNullIfNotNull该属性时,当方法始终返回非 null 值时,为了协助编译器的分析。 请考虑以下方法:

#nullable enable
string GetTopLevelDomainFromFullUrl(string url) { ... }

如果未返回参数urlnullnull则不返回。 启用可为 null 引用后,该签名正常工作,前提是 API 永远不会接受 null 参数。 但是,如果参数可以为 null,则返回值也可能为 null。 若要正确表达该协定,请对此方法进行批注,如下所示:

#nullable enable
[return: NotNullIfNotNull("url")]
string? GetTopLevelDomainFromFullUrl(string? url) { ... }

end 示例

22.5.7.10 NotNullWhen 属性

指定当方法返回指定bool值时,不能null为 null 参数。

示例:当参数为null空字符串或空字符串时,库方法String.IsNullOrEmpty(String)true返回。 它是 null 检查的一种形式:如果方法返回 false,调用方不需要对参数进行 null 检查。 若要使类似此可为 null 的方法感知,请将参数类型设置为可为 null 的引用类型,并添加 NotNullWhen 属性:

#nullable enable
bool IsNullOrEmpty([NotNullWhen(false)] string? value) { ... }

end 示例

22.6 互操作的属性

若要与其他语言进行互操作,可以使用索引属性实现索引器。 如果索引器不存在 IndexerName 任何属性,则默认使用该名称 Item 。 该 IndexerName 属性使开发人员能够重写此默认值并指定其他名称。

示例:默认情况下,索引器的名称为 Item。 这可以重写,如下所示:

[System.Runtime.CompilerServices.IndexerName("TheItem")]
public int this[int index]
{
    get { ... }
    set { ... }
}

现在,索引器的名称为 TheItem.

end 示例