23 个属性

23.1 常规

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

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

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

属性通过属性类(§23.2)的声明定义,该声明可以具有位置参数和命名参数(§23.2.3)。 特性使用属性规范(§23.3)附加到 C# 程序中的实体,并且可以在运行时作为属性实例(§23.4)进行检索。

23.2 属性类

23.2.1 常规

从抽象类 System.Attribute 派生的类(无论是直接派生还是间接派生)都是属性类。 属性类的声明定义了一种可放置在程序实体上的新属性类型。 按照约定,属性类的命名带有 Attribute 后缀。 属性的使用可以包含或省略此后缀。

泛型类声明不得将 System.Attribute 用作直接或间接基类。

示例

public class B : Attribute {}
public class C<T> : B {} // Error – generic cannot be an attribute

结束示例

23.2.2 属性用法

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

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

示例:以下示例定义了一个名为 SimpleAttribute 的属性类,该类只能放置在 class_declarationinterface_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 具有一个命名参数(§23.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 
{
    ...
}

结束示例

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

未附加 X 属性的属性类 AttributeUsage,如下所示

class X : Attribute { ... }

等效于以下命令:

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

23.2.3 位置参数和命名参数

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

示例:以下示例定义了一个名为 HelpAttribute 的属性类,该类有一个位置参数 url 和一个命名参数 Topic。 虽然它是非静态和公共的,但属性 Url 不定义命名参数,因为它不是读写或读 init。 还展示了该属性的两种用法:

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

结束示例

23.2.4 属性参数类型

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

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

23.3 属性规范

将以前定义的属性应用于程序实体称为 属性规范。 属性是为程序实体指定的一段附加声明性信息。 可以在全局范围(指定包含程序集或模块上的属性)和 type_declarations (§14.8), class_member_declarations (§15.3), interface_member_declarations (§19. 4)、struct_member_declarations (§16.3)、enum_member_declarations (§20.2)、accessor_declarations (§15.7.3)、event_accessor_declaration s (§15.8.8)、local_function_declarations (§13.6.4)、parameter_list s (§15.6.2)、type_parameter_list §15.2.3)、 lambda_expression s (§12.22.1) 的元素以及 explicit_anonymous_function_parameters 和 implicit_anonymous_function_parameters (§12.22.1) 的元素。

属性在 特性节中指定。 属性部分由一对方括号组成,方括号中包含一个或多个属性的逗号分隔列表。 在此类列表中指定属性的顺序以及附加到同一程序实体的部分的排列顺序并不重要。 例如,属性规范 [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 的生成,以及在以下文本中,标识符的拼写应等于 assemblymodule,其中相等性如 §6.4.3 中所定义。 对于 attribute_target 的生成,以及在以下文本中,标识符的拼写应不等于 assemblymodule,使用与上述相同的相等性定义。

属性由 attribute_name 以及可选的位置参数和命名参数列表组成。 位置参数(如果有)位于命名参数之前。 位置参数由 attribute_argument_expression 组成;命名参数由名称、等号和 attribute_argument_expression 组成,它们共同受与简单赋值相同的规则约束。 命名参数的顺序并不重要。

注意:为方便起见,global_attribute_sectionattribute_section 中允许使用尾随逗号,就像 array_initializer (§17.7) 中允许的那样。 尾注

attribute_name 标识属性类。

当属性放在全局级别时,需要 global_attribute_target_specifier。 当 global_attribute_target 等于:

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

不允许 global_attribute_target 有其他值。

标准化的 attribute_target 名称为 eventfieldmethodparampropertyreturntypetypevar。 这些目标名称仅应在以下上下文中使用:

  • event — 事件。
  • field — 字段。 类字段事件(即没有访问器的事件)(§15.8.2) 和自动实现的属性 (§15.7.4) 也可以具有以此为目标的属性。
  • method — 构造函数;终结器;方法;算子;local 函数、属性 get、set 和 init 访问器;索引器获取、设置和 init 访问器;事件添加和删除访问器;和 lambda 表达式。 类字段事件(即没有访问器的事件)也可以具有以此为目标的属性。
  • param — 属性集和 init 访问器、索引器集和 init 访问器、事件添加和删除访问器,以及构造函数、方法、本地函数和运算符中的参数。
  • property — 属性和索引器。
  • return — 委托、方法、本地函数、运算符、属性 get 访问器、索引器 get 访问器和 lambda 表达式。
  • type — 委托、类、结构、枚举和接口。
  • typevar — 类型参数。

某些上下文允许在多个目标上指定属性。 程序可以通过包含 attribute_target_specifier 来显式指定目标。 没有 attribute_target_specifier 时,将应用默认值,但可以使用 attribute_target_specifier 来确认或覆盖默认值。 上下文解析如下:

  • 对于委托声明上的属性,默认目标是委托。 否则,当 attribute_target 等于:
    • type — 目标是委托
    • return — 目标是返回值
  • 对于方法声明上的属性,默认目标是方法。 否则,当 attribute_target 等于:
    • method — 目标是方法
    • return — 目标是返回值
  • 对于本地函数声明的属性,默认目标是本地函数。 否则,当 attribute_target 等于:
    • method — 目标是本地函数
    • return — 目标是返回值
  • 对于运算符声明上的属性,默认目标是运算符。 否则,当 attribute_target 等于:
    • method — 目标是运算符
    • return — 目标是返回值
  • 对于属性或索引器声明的 get 访问器声明上的属性,默认目标是关联的方法。 否则,当 attribute_target 等于:
    • method — 目标是关联的方法
    • return — 目标是返回值
  • 对于在属性或索引器声明的 set 或 init 访问器上指定的属性,默认目标是关联的方法。 否则,当 attribute_target 等于:
    • method — 目标是关联的方法
    • param — 目标是唯一的隐式参数
  • 对于自动实现的属性声明上的属性,默认目标是属性。 否则,当 attribute_target 等于:
    • field — 目标是属性的编译器生成的支持字段
  • 对于省略 event_accessor_declarations 的事件声明上指定的属性,默认目标是事件声明。 否则,当 attribute_target 等于:
    • event — 目标是事件声明
    • field — 目标是字段
    • method — 目标是方法
  • 如果事件声明未省略 event_accessor_declarations,则默认目标是方法。
    • method — 目标是关联的方法
    • param — 目标是唯一的参数
  • 对于 lambda_expression 默认目标为方法的属性。 否则,当 attribute_target 等于:
    • method — 目标是方法
    • return — 目标是返回值

在所有其他上下文中,允许包含 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 派生的类型,则会发生编译时错误。
  • 否则,
    • 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 {}

结束示例

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

示例:示例

[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(单用途属性类)。

结束示例

当且仅当满足以下所有条件时,表达式 Eattribute_argument_expression

  • E类型为属性参数类型(§23.2.4)。
  • 在编译时,E 的值可解析为以下之一:
    • 常数值。
    • 使用 System.Type (§12.8.18) 获得的 对象,该表达式指定非泛型类型、封闭构造类型 (§8.4.3) 或未绑定泛型类型 (§8.4.4),但不是开放类型 (§8.4.3)。
    • 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;
}

结束示例

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

示例:两个部分:

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

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

等效于以下单个声明:

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

结束示例

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

23.4 属性实例

23.4.1 常规

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

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

23.4.2 属性的编译

在程序实体 上指定的、具有属性类 Tpositional_argument_listPnamed_argument_listNE的编译通过以下步骤编译到程序集 A 中:

  • 遵循编译新 形式的 T(P) 的编译时处理步骤。 这些步骤要么导致编译时错误,要么确定可在运行时调用的 C 上的实例构造函数 T
  • 如果 C 不具有公共可访问性,则会发生编译时错误。
  • 对于 中的每个 ArgN
    • Name 设为 named_argumentArg
    • Name应标识非静态读/写公共字段或公共非静态读写或 read-init 属性。T 如果 T 没有此类字段或属性,则会发生编译时错误。
  • 如果 positional_argument_listP 中的任何值或 named_argument_listN 中的某个值的类型 System.String ,并且该值的格式不正确(由 Unicode 标准定义),则编译的值是否等于检索的运行时值(§23.4.3)是实现定义的。

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

  • 将以下信息(用于属性的运行时实例化)存储在编译器输出的程序集中,该程序集是编译包含该属性的程序的结果:属性类 TC 上的实例构造函数 Tpositional_argument_listPnamed_argument_listN 以及关联的程序实体 E,其值在编译时完全解析。

23.4.3 属性实例的运行时检索

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

  • 遵循执行 形式的 new T(P) 的运行时处理步骤,使用实例构造函数 C 和编译时确定的值。 这些步骤要么导致异常,要么生成 O 的实例 T
  • 对于 中的每个 ArgN,按顺序:
    • Name 设为 named_argumentArg。 如果未 Name 标识非静态公共读写字段或非静态公共读写或 read-init 属性 O,则会引发异常。
    • Value 设为计算 Arg 的结果。
    • 如果 Name 标识 O 上的字段,则将此字段设置为 Value
    • 否则,Name 标识 O 上的属性。 将此属性设置为 Value。
    • 结果是 O,即已使用 TP 初始化的属性类 N 的实例。

注意:在 T 中存储 CPNE(并将其与 A 关联)的格式以及指定 E 并从 T 中检索 CPNA(从而在运行时获取属性实例)的机制超出了本规范的范围。 尾注

23.5 保留属性

23.5.1 常规

许多属性在某种程度上会影响语言。 这些属性包括:

  • System.AttributeUsageAttribute§23.5.2),用于描述属性类的使用方式。
  • System.Diagnostics.ConditionalAttribute§23.5.3)是一个多用途属性类,用于定义条件方法和条件属性类。 此属性通过测试条件编译符号来指示条件。
  • System.ObsoleteAttribute§23.5.4),用于将成员标记为已过时。
  • System.Runtime.CompilerServices.AsyncMethodBuilderAttribute§23.5.5),用于为异步方法建立任务生成器。
  • System.Runtime.CompilerServices.CallerLineNumberAttribute§23.5.6.2)、 System.Runtime.CompilerServices.CallerFilePathAttribute§23.5.6.3)、 System.Runtime.CompilerServices.CallerMemberNameAttribute§23.5.6.4)和System.Runtime.CompilerServices.CallerArgumentExpressionAttribute§23.5.6.5),用于向可选参数提供有关调用上下文的信息。
  • System.Runtime.CompilerServices.EnumeratorCancellationAttribute§23.5.8),用于在异步迭代器中指定取消令牌的参数。
  • System.Runtime.CompilerServices.ModuleInitializer§23.5.9),用于将方法标记为模块初始值设定项。
  • System.Runtime.CompilerServices.InterpolatedStringHandlerAttributeSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute,用于声明自定义内插字符串表达式处理程序(§23.5.9.1)和分别调用其构造函数之一。

可以为 Null 的静态分析属性(§23.5.7)可以改善为 null 能力和 null 状态(§8.9.5)生成的警告的正确性。

执行环境可以提供影响 C# 程序执行的其他实现定义的属性。

23.5.2 AttributeUsage 属性

属性 AttributeUsage 用于描述属性类的使用方式。

AttributeUsage 属性修饰的类必须直接或间接从 System.Attribute 派生。 否则,会发生编译时错误。

注意:有关使用此属性的示例,请参阅 §23.2.2尾注

23.5.3 条件属性

23.5.3.1 常规

该特性 Conditional 支持 条件方法的定义、 条件本地函数条件属性类es。

23.5.3.2 条件方法

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

示例

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

Eg.M 声明为与两个条件编译符号 ALPHABETA 相关联的条件方法。

结束示例

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

条件方法受以下限制:

  • 条件方法必须是 class_declarationstruct_declaration 中的方法。 如果在接口声明的方法上指定了 Conditional 属性,则会发生编译时错误。
  • 条件方法不应是属性、索引器或事件的访问器。
  • 条件方法的返回类型必须为 void
  • 条件方法不得用 override 修饰符标记。 但是,条件方法可以用 virtual 修饰符标记。 此类方法的重写是隐式条件的,不得显式用 Conditional 属性标记。
  • 条件方法不得是接口方法的实现。 否则,会发生编译时错误。
  • 条件方法的参数不得是输出参数。

注意:具有 AttributeUsage§23.2.2AttributeTargets.Method 的属性通常可应用于属性、索引器和事件的访问器。 上述限制禁止使用此 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 声明为条件方法。 Class2Test 方法调用此方法。 由于已定义条件编译符号 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
    }
}

Class2Class3 各自包含对条件方法 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 可以消除此类问题。

结束示例

23.5.3.3 条件本地函数

静态本地函数可以与条件方法(§23.5.3.2)具有相同的条件性。

如果使非静态本地函数有条件,则会发生编译时错误。

23.5.3.4 条件属性类

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

示例

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

TestAttribute 声明为与条件编译符号 ALPHABETA 相关联的条件属性类。

结束示例

如果在规范点定义了一个或多个关联的条件编译符号,则包含条件属性的特性规范(§23.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。 由于此符号在 Class1 的上下文中定义,但在 Class2 中未定义,因此包含 Class1 上的 Test 属性规范,而省略 Test 上的 Class2 属性规范。

结束示例

23.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();
    }
}

AObsolete 属性修饰。 在 A 中每次使用 Main 都会产生一个警告,其中包含指定的消息“此类已过时;请改用类 B”。

结束示例

23.5.5 AsyncMethodBuilder 属性

此属性在 §15.14.1 中介绍。

23.5.6 调用方信息属性

23.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、、CallerMemberNameCallerFilePathCallerArgumentExpression。 考虑以下参数声明:

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

CallerLineNumber 优先,其他三个属性将被忽略。 如果CallerLineNumber省略,CallerFilePath将优先,并且CallerMemberNameCallerArgumentExpression将被忽略。 这些属性的词法顺序无关紧要。

23.5.6.2 CallerLineNumber 属性

当从常量值 System.Runtime.CompilerServices.CallerLineNumberAttribute 到参数类型存在标准隐式转换 (§10.4.2) 时,属性 int.MaxValue 允许用于可选参数。 这确保任何不超过该值的非负行号都可以正确传递。

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

如果调用跨多行,则所选行由实现定义。

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

23.5.6.3 CallerFilePath 属性

当从 System.Runtime.CompilerServices.CallerFilePathAttribute 到参数类型存在标准隐式转换 (§10.4.2) 时,属性 string 允许用于可选参数。

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

文件路径的格式由实现定义。

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

23.5.6.4 CallerMemberName 属性

当从 System.Runtime.CompilerServices.CallerMemberNameAttribute 到参数类型存在标准隐式转换 (§10.4.2) 时,属性 string 允许用于可选参数。

如果来自函数成员体内或应用于函数成员本身及其返回类型、参数或类型参数的属性内的源代码位置的函数调用省略了带 CallerMemberNameAttribute 的可选参数,则表示该成员名称的字符串字面量将用作调用的参数,而非默认参数值。 (对于从顶级语句(§7.1.3)调用的函数,成员名称由实现生成。

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

对于在显式接口成员实现内发生的调用,仅使用方法名称本身,不带前面的接口限定。

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

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

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

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

对于在本地函数或匿名函数中发生的调用,将使用调用该函数的成员方法的名称。

示例:请考虑以下示例:

class Program
{
    static void Main()
    {
        F1();

        void F1([CallerMemberName] string? name = null)
        {
            Console.WriteLine($"F1 MemberName: |{name}|");
            F2();
        }

        static void F2([CallerMemberName] string? name = null)
        {
            Console.WriteLine($"F2 MemberName: |{name}|");
        }
    }
}

生成输出的

F1 MemberName: |Main|
F2 MemberName: |Main|

此属性提供调用函数成员的名称,对于本地函数 F1 是方法 Main。 即使 F2F1本地函数调用,但局部函数 不是 函数成员,因此报告的调用方 F2 也是 Main结束示例

23.5.6.5 CallerArgumentExpression 属性

该属性 System.Runtime.CompilerServices.CallerArgumentExpressionAttribute 应用于 目标参数,并可能导致将同级参数的源代码文本捕获为字符串,此处称为 捕获的字符串

除非它是扩展方法中的第一个参数,否则目标参数应具有 default_argument

请考虑以下方法声明:

using System;
using System.Runtime.CompilerServices;
#nullable enable
class Test
{
    public static void M(int val = 0, [CallerArgumentExpression("val")] string? text = null)
    {
        Console.WriteLine($"val = {val}, text = <{text}>");
    }
}

其中,目标参数text和同级参数是val,其相应的参数的源代码文本可以在调用时M捕获text

属性构造函数采用类型 string为参数。 该字符串

  • 应包含同级参数的名称;否则,将忽略该属性。
  • 应省略具有该前缀的参数名称的前导 @

parameter_list可能包含多个目标参数。

目标参数的类型应有一个标准转换。string

注意: 这意味着不允许用户定义转换 string ,实际上意味着此类参数的类型必须是 stringobject由其实现的 string接口。 尾注

如果为目标参数传递显式参数,则不会捕获任何字符串,并且该参数接受该参数的值。 否则,根据以下规则,与同级参数对应的参数的文本将转换为捕获的字符串:

  • 删除任何最外部分组括号之前和之后,将删除前导和尾随空格。
  • 删除任何前导空格和尾随空格之前和之后,都会删除最外部的分组括号。
  • 所有其他 input_element保留逐字(包括空格、注释、 Unicode_Escape_Sequences 和 @ 标识符前缀)。

然后,捕获的字符串作为对应于目标参数的参数传递。 但是,如果省略同级参数的参数,则目标参数将采用其 default_argument 值。

示例:鉴于上述声明 M ,请考虑对以下调用 M

Test.M();
Test.M(123);
Test.M(123, null);
Test.M(123, "xyz");
Test.M(  1  +      2 );
Test.M(( ( (123) + 0) ) );
int local = 10;
Test.M(l\u006fcal /*...*/ + // xxx
  5);

生成的输出为

val = 0, text = <>
val = 123, text = <123>
val = 123, text = <>
val = 123, text = <xyz>
val = 3, text = <1  +      2>
val = 123, text = <(123) + 0>
val = 15, text = <l\u006fcal /*...*/ + // xxx
  5>

结束示例

23.5.7 代码分析属性

23.5.7.1 常规

此子条款中的属性用于提供附加信息,以支持提供可空性和空状态诊断的编译器(§8.9.5)。 执行任何 null 状态诊断不需要编译器。 这些属性的存在与否不影响语言或程序的行为。 不提供 null 状态诊断的编译器应读取并忽略这些属性的存在。 提供 null 状态诊断的编译器在使用这些属性为其诊断提供信息时,应使用本子条款中定义的含义。

代码分析属性在命名空间 System.Diagnostics.CodeAnalysis 中声明。

属性 含义
AllowNull§23.5.7.2 不可为 null 的参数可以为 null。
DisallowNull§23.5.7.3 可为 null 的参数不应为 null。
MaybeNull§23.5.7.6 不可为 null 的返回值可以为 null。
NotNull§23.5.7.10 可为 null 的返回值永远不会为 null。
MaybeNullWhen§23.5.7.7 当方法返回指定的 bool 值时,不可为 null 的参数可以为 null。
NotNullWhen§23.5.7.12 当方法返回指定 bool 值时,可为 null 的参数不会为 null。
NotNullIfNotNull§23.5.7.11 如果指定参数的参数不为 null,则返回值不为 null。
MemberNotNull§23.5.7.8 当方法返回时,列出的成员不会为 null。
MemberNotNullWhen§23.5.7.9 当方法返回指定 bool 值时,列出的成员不会为 null。
DoesNotReturn§23.5.7.4 此方法从不返回。
DoesNotReturnIf§23.5.7.5 如果关联的 bool 参数具有指定值,则此方法永远不会返回。

§23.5.7 中的以下子项是有条件的规范性的。

23.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 值。 该属性的存在会抑制该警告。 结束示例

23.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。 结束示例

23.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 性警告。 结束示例

23.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} cannot 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!;
}

结束示例

23.5.7.6 可能Null 属性

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

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

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

如果没有属性,编译器可能会生成警告(如果该方法可能返回 null)。 该属性的存在会抑制该警告。 结束示例

23.5.7.7 可能NullWhen 属性

指定当方法返回指定的 null 值时,不可为 null 的参数可能为 bool。 这类似于 MaybeNull 特性(§23.5.7.6),但包含指定返回值的参数。

23.5.7.8 MemberNotNull 属性

指定给定成员在方法返回时不会 null

示例:帮助程序方法可能包括用于 MemberNotNull 列出分配给该方法中非 null 值的任何字段的属性。 分析构造函数以确定是否已初始化所有不可为 null 的引用字段的编译器,然后可以使用此属性来发现这些帮助程序方法设置了哪些字段。 请看下面的示例:

#nullable enable
public class Container
{
    private string _uniqueIdentifier; // must be initialized.
    private string? _optionalMessage;

    public Container()
    {
        Helper();
    }

    public Container(string message)
    {
        Helper();
        _optionalMessage = message;
    }

    [MemberNotNull(nameof(_uniqueIdentifier))]
    private void Helper()
    {
        _uniqueIdentifier = DateTime.Now.Ticks.ToString();
    }
}

可以为属性的构造函数提供多个字段名称作为参数。 结束示例

23.5.7.9 MemberNotNullWhen 属性

指定当方法返回指定null值时,列出的成员不会bool

示例:此属性类似于MemberNotNull§23.5.7.8),但采用MemberNotNullWhenbool参数除外。 MemberNotNullWhen 适用于帮助程序方法返回 bool 指示其是否初始化字段的情况。 结束示例

23.5.7.10 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 是可以接受的。 结束示例

23.5.7.11 NotNullIfNotNull 属性

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

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

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

如果未返回参数urlnullnull则不返回。 启用可为 null 的引用时,只要 API 从不接受 null 参数,该签名就能正常工作。 但是,如果参数可能为 null,则返回值也可能为 null。 要正确表达该契约,请按以下方式批注此方法:

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

结束示例

23.5.7.12 NotNullWhen 属性

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

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

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

结束示例

23.5.8 EnumeratorCancellation 属性

指定表示 CancellationToken 异步迭代器的参数(§15.15)。 此参数的参数应与传递给 IAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken)的参数结合使用。 此组合令牌应由 (IAsyncEnumerator<T>.MoveNextAsync()) 轮询。 令牌应组合成单个令牌,就像 by CancellationToken.CreateLinkedTokenSource 及其 Token 属性一样。 如果取消了两个源令牌之一,则会取消组合令牌。 组合标记被视为该方法正文中异步迭代器方法 (§15.15) 的参数。

如果属性应用于多个参数, System.Runtime.CompilerServices.EnumeratorCancellation 则为错误。 如果出现以下情况,编译器可能会生成警告:

  • EnumeratorCancellation 特性应用于类型以外的 CancellationToken参数,
  • 或者,如果 EnumeratorCancellation 特性应用于不是异步迭代器的方法的参数(§15.15),
  • 或者, EnumeratorCancellation 如果特性应用于返回异步枚举器接口(§15.15.2)的方法上的参数,而不是异步可枚举接口(§15.15.3)。

当没有特性具有此参数时,迭代器将无法访问 CancellationToken 参数 GetAsyncEnumerator

示例:该方法 GetStringsAsync() 是一个异步迭代器。 在执行任何作以检索下一个值之前,它会检查取消令牌以确定是否应取消迭代。 如果请求取消,则不采取进一步作。

public static async Task ExampleCombination()
{
    var sourceOne = new CancellationTokenSource();
    var sourceTwo = new CancellationTokenSource();
    await using (IAsyncEnumerator<string> enumerator =
        GetStringsAsync(sourceOne.Token).GetAsyncEnumerator(sourceTwo.Token))
    {
        while (await enumerator.MoveNextAsync())
        {
            string number = enumerator.Current;
            if (number == "8") sourceOne.Cancel();
            if (number == "5") sourceTwo.Cancel();
            Console.WriteLine(number);
        }
    }
}

static async IAsyncEnumerable<string> GetStringsAsync(
  [EnumeratorCancellation] CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        if (token.IsCancellationRequested) yield break;
        await Task.Delay(1000, token);
        yield return i.ToString();
    }
}

结束示例

23.5.9 ModuleInitializer 属性

该特性 ModuleInitializer 用于将方法标记为 模块初始值设定项。 在初始化包含模块期间调用此类方法。 模块可能有多个初始值设定项,这些初始值设定项按实现定义的顺序调用。

模块初始值设定项中允许哪些代码没有限制。

模块初始值设定项应具有以下特征:

  • method_modifierstatic
  • parameter_list
  • return_typevoid
  • type_parameter_list
  • 在具有type_parameter_list的class_declaration内不声明。
  • 可从包含模块(即具有访问修饰符 internalpublic)进行访问。
  • 不是本地函数。

23.5.9.1 自定义内插字符串表达式处理程序

23.5.9.1.1 声明自定义处理程序

请考虑以下程序,它实现简单的消息记录器:

using System;
public class Logger
{
    public void LogMessage(string msg)
    {
        Console.WriteLine(msg);
    }
}
public class Program
{
    static void Main()
    {
        var logger = new Logger();
        int val = 255;
        logger.LogMessage($"val = {{{val,4:X}}}; 2 * val = {2 * val}.");
    }
}

生成的输出如下所示:

val = {  FF}; 2 * val = 510.

在调用LogMessage中,内插字符串表达式参数的目标是具有类型的string参数msg。 因此,根据 §12.8.3,将调用默认内插字符串表达式处理程序。 以下子句(§23.5.9.1.1)演示如何使用自定义处理程序。

为了向上述程序提供自定义处理,需要 自定义内插字符串表达式处理程序 。 下面是添加了自定义处理程序的消息记录器(尽管它的行为不如默认处理程序一样,但它为自定义提供了挂钩):

using System;
using System.Text;
using System.Runtime.CompilerServices;

[InterpolatedStringHandler]
public ref struct LogInterpolatedStringHandler
{
    StringBuilder builder; // Storage for the built-up string
    public LogInterpolatedStringHandler(int literalLength, int formattedCount)
    {
        builder = new StringBuilder(literalLength);
    }
    public void AppendLiteral(string s)
    {
        builder.Append(s);
    }
    public void AppendFormatted<T>(T t)
    {
        builder.Append(t?.ToString());
    }
    public void AppendFormatted<T>(T t, string format) where T : IFormattable
    {
        builder.Append(t?.ToString(format, null));
    }
    public void AppendFormatted<T>(T t, int alignment, string format)
        where T : IFormattable
    {
        builder.Append(String.Format("{0" + "," + alignment + ":" + format + "}", t));
    }
    public override string ToString() => builder.ToString();
}

public class Logger
{
    public void LogMessage(string msg)
    {
        Console.WriteLine(msg);
    }
    public void LogMessage(LogInterpolatedStringHandler builder)
    {
        Console.WriteLine(builder.ToString());
    }
}

public class Program
{
    static void Main()
    {
        var logger = new Logger();
        int val = 255;
        logger.LogMessage($"val = {{{val,4:X}}}; 2 * val = {2 * val}.");
    }
}

生成的输出如下所示:

val = {  FF}; 2 * val = 510.

System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute属性的类型据说是适用的内插字符串处理程序类型

若要限定为自定义内插字符串表达式处理程序,类或结构类型应具有以下特征:

  • 用属性 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute标记 。
  • 具有一个可访问的构造函数,其前两个参数具有类型 int。 (可能遵循其他参数,这些参数用于向/从处理程序传递信息。这些内容在 §23.5.9.1.3 中讨论。可以声明可选的最终参数来阻止处理程序处理内插字符串。这在 §23.5.9.1.2 中讨论。

当编译器生成的代码调用构造函数时,第一个参数将设置为内插字符串表达式中内插字符串表达式段(§12.8.3)的长度之和,第二个参数设置为内插数。 (对于 ($"val = {{{val,4:X}}}; 2 * val = {2 * val}.",这些值分别为 21 和 2。

  • 具有签名的可访问方法,该签名 void AppendLiteral(string s)被调用以处理单个内插字符串表达式文本段。
  • 调用一组可访问的重载方法,其中一个 AppendFormatted方法基于该内插的内容处理单个内插。 其签名如下所示:
    • void AppendFormatted<T>(T t),它处理内插没有显式格式或对齐方式,如以下情况 {2 * val}
    • void AppendFormatted<T>(T t, string format) where T : System.IFormattable,它处理内插具有显式格式,但没有对齐方式,如以下情况 {val:X4}所示。
    • void AppendFormatted<T>(T t, int alignment, string format) where T : System.IFormattable,处理内插具有显式格式和对齐方式,如以下情况 {val,4:X}所示。
  • 具有签名 override string ToString()的公共方法,该方法返回生成的字符串。

注意:它不是省略任何 AppendFormatted 重载的编译时错误,但如果处理程序最可靠,它应支持默认处理程序识别的所有格式。 尾注

采用自定义处理程序而不是string自定义处理程序的新重载LogMessage,并检索由该处理程序格式化的字符串。 如果存在此类重载,并且内插字符串表达式不是常量(§12.8.3),则编译器将生成代码来调用采用处理程序的处理程序。 在这种情况下,编译器将生成代码

  • 调用处理程序构造函数
  • 内插字符串表达式中的词法顺序
    • 将每个内插字符串表达式段传递给 AppendLiteral
    • 将每个内插传递到相应的 AppendFormatted 方法。
  • 将最终字符串作为内插字符串表达式的值返回。
  • 执行 . 的 LogMessage正文
23.5.9.1.2 抑制自定义处理程序

如果处理程序构造函数具有类型为 out 参数的最终参数 bool ,则当该构造函数被调用该参数的值时。 如果为 true,则行为与省略该参数一样。 但是,如果为 false,则不会进一步处理内插字符串表达式;也就是说,处理程序 被抑制。 具体而言,不计算内插表达式,并且不调用方法AppendLiteralAppendFormatted

public LogInterpolatedStringHandler(int literalLength, int formattedCount,
    out bool processString)
{
    if (some_condition)
    {
        processString = false;
        return;
    }
    else 
    {
        processString = true;
        // continue construction
    }
}

注意:内插字符串表达式中的内插可能包含副作用(由、--赋值和某些方法调用的结果++)。 如果处理程序被抑制,则不会计算内插字符串表达式中的副作用。 如果未抑制处理程序,将计算内插字符串表达式中的所有副作用。 尾注

23.5.9.1.3 向/从自定义处理程序传递信息

将其他信息传递给自定义处理程序并从中接收信息非常有用。 这是通过属性 System.Runtime.CompilerServices.InterpolatedStringHandlerArgument完成的。 请考虑以下消息记录器程序的新重载:

public class Logger
{
    // …
    public void LogMessage(bool flag, int count,
        [InterpolatedStringHandlerArgument("count","flag","")] 
        LogInterpolatedStringHandler builder)
    {
        // …
    }
}

public ref struct LogInterpolatedStringHandler
{
    // …
    public LogInterpolatedStringHandler(int literalLength, int formattedCount,
        int count, bool flag, Logger logger)
    {
        // …
    }
}

特性 InterpolatedStringHandlerArgument 应用于处理程序参数,该参数应遵循要传递给处理程序的参数的声明。 属性构造函数参数应是一个逗号分隔的列表,其中包含用于命名要传递的参数及其顺序的零个或多个字符串。 空字符串指定从中调用处理程序的实例。 因此,上述属性构造函数调用需要 "count","flag","" 匹配的处理程序构造函数。 如果属性构造函数参数列表为空,则行为就像省略该属性一样。

如果还声明参数 out bool 以允许阻止处理程序(§23.5.9.1.2),该参数应为最后一个参数。

用于互作的 23.6 属性

为了与其他语言互操作,索引器可以使用索引属性来实现。 如果索引器没有 IndexerName 属性,则默认使用名称 ItemIndexerName 属性使开发人员能够覆盖此默认值并指定不同的名称。

示例:默认情况下,索引器的名称为 Item。 可以按以下方式覆盖:

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

现在,索引器的名称为 TheItem

结束示例