C# 编译器解释的杂项属性

属性 ConditionalObsoleteAttributeUsageAsyncMethodBuilderInterpolatedStringHandlerModuleInitializer 可应用于代码中的元素。 它们为这些元素添加语义。 编译器使用这些语义来更改其输出,并报告使用你的代码的开发人员可能犯的错误。

Conditional 特性

Conditional 特性使得方法执行依赖于预处理标识符。 Conditional 属性是 ConditionalAttribute 的别名,可以应用于方法或特性类。

在以下示例中,Conditional 应用于启用或禁用显示特定于程序的诊断信息的方法:

#define TRACE_ON
using System.Diagnostics;

namespace AttributeExamples;

public class Trace
{
    [Conditional("TRACE_ON")]
    public static void Msg(string msg)
    {
        Console.WriteLine(msg);
    }
}

public class TraceExample
{
    public static void Main()
    {
        Trace.Msg("Now in Main...");
        Console.WriteLine("Done.");
    }
}

如果未定义 TRACE_ON 标识符,则不会显示跟踪输出。 在交互式窗口中自己探索。

Conditional 特性通常与 DEBUG 标识符一起使用,以启用调试生成(而非发布生成)中的跟踪和日志记录功能,如下例所示:

[Conditional("DEBUG")]
static void DebugMethod()
{
}

当调用标记为条件的方法时,指定的预处理符号是否存在将决定编译器是包含还是省略对该方法的调用。 如果定义了符号,则将包括调用;否则,将忽略该调用。 条件方法必须是类或结构声明中的方法,而且必须具有 void 返回类型。 与将方法封闭在 #if…#endif 块内相比,Conditional 更简洁且较不容易出错。

如果某个方法具有多个 Conditional 特性,则如果定义了一个或多个条件符号(通过使用 OR 运算符将这些符号逻辑链接在一起),编译器会包含对该方法的调用。 在以下示例中,存在 AB 将导致方法调用:

[Conditional("A"), Conditional("B")]
static void DoIfAorB()
{
    // ...
}

使用带有特性类的 Conditional

Conditional 特性还可应用于特性类定义。 在以下示例中,如果定义了 DEBUG,则自定义特性 Documentation 将仅向元数据添加信息。

[Conditional("DEBUG")]
public class DocumentationAttribute : System.Attribute
{
    string text;

    public DocumentationAttribute(string text)
    {
        this.text = text;
    }
}

class SampleClass
{
    // This attribute will only be included if DEBUG is defined.
    [Documentation("This method displays an integer.")]
    static void DoWork(int i)
    {
        System.Console.WriteLine(i.ToString());
    }
}

Obsolete 特性

Obsolete 特性将代码元素标记为不再推荐使用。 使用标记为已过时的实体会生成警告或错误。 Obsolete 特性是一次性特性,可以应用于任何允许特性的实体。 ObsoleteObsoleteAttribute 的别名。

在以下示例中,对类 A 和方法 B.OldMethod 应用了 Obsolete 特性。 因为应用于 B.OldMethod 的特性构造函数的第二个参数设置为 true,所以此方法将导致编译器错误,而使用类 A 只会生成警告。 但是,调用 B.NewMethod 不会生成任何警告或错误。 例如,将其与先前的定义一起使用时,以下代码会生成两个警告和一个错误:


namespace AttributeExamples
{
    [Obsolete("use class B")]
    public class A
    {
        public void Method() { }
    }

    public class B
    {
        [Obsolete("use NewMethod", true)]
        public void OldMethod() { }

        public void NewMethod() { }
    }

    public static class ObsoleteProgram
    {
        public static void Main()
        {
            // Generates 2 warnings:
            A a = new A();

            // Generate no errors or warnings:
            B b = new B();
            b.NewMethod();

            // Generates an error, compilation fails.
            // b.OldMethod();
        }
    }
}

作为特性构造函数的第一个参数提供的字符串将作为警告或错误的一部分显示。 将生成类 A 的两个警告:一个用于声明类引用,另一个用于类构造函数。 Obsolete 特性可以在不带参数的情况下使用,但建议说明改为使用哪个项目。

在 C# 10 中,可以使用常量字符串内插和 nameof 运算符来确保名称匹配:

public class B
{
    [Obsolete($"use {nameof(NewMethod)} instead", true)]
    public void OldMethod() { }

    public void NewMethod() { }
}

SetsRequiredMembers 特性

SetsRequiredMembers 属性通知编译器构造函数设置了该类或结构中的所有 required 成员。 编译器假定任何具有 System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute 属性的构造函数都会初始化所有 required 成员。 调用此类构造函数的任何代码都不需要对象初始值设定项来设置所需的成员。 这主要用于位置记录和主要构造函数。

AttributeUsage 特性

AttributeUsage 特性确定自定义特性类的使用方式。 AttributeUsageAttribute 是应用到自定义特性定义的特性。 AttributeUsage 特性帮助控制:

  • 可能应用到的具体程序元素特性。 除非使用限制,否则特性可能应用到以下任意程序元素:
    • 程序集
    • 模块
    • 字段
    • 事件
    • 方法
    • Param
    • 属性
    • 返回
    • 类型
  • 某特性是否可多次应用于单个程序元素。
  • 特性是否由派生类继承。

显式应用时,默认设置如以下示例所示:

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

在此示例中,NewAttribute 类可应用于任何受支持的程序元素。 但是它对每个实体仅能应用一次。 特性应用于基类时,它由派生类继承。

AllowMultipleInherited 参数是可选的,因此以下代码具有相同效果:

[AttributeUsage(AttributeTargets.All)]
class NewAttribute : Attribute { }

第一个 AttributeUsageAttribute 参数必须是 AttributeTargets 枚举的一个或多个元素。 可将多个目标类型与 OR 运算符链接在一起,如下例所示:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
class NewPropertyOrFieldAttribute : Attribute { }

特性可应用于属性或自动实现的属性的支持字段。 特性应用于属性,除非在特性上指定 field 说明符。 都在以下示例中进行了演示:

class MyClass
{
    // Attribute attached to property:
    [NewPropertyOrField]
    public string Name { get; set; } = string.Empty;

    // Attribute attached to backing field:
    [field: NewPropertyOrField]
    public string Description { get; set; } = string.Empty;
}

如果 AllowMultiple 参数为 true,那么结果特性可多次应用于单个实体,如以下示例所示:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
class MultiUse : Attribute { }

[MultiUse]
[MultiUse]
class Class1 { }

[MultiUse, MultiUse]
class Class2 { }

在本例中,MultiUseAttribute 可重复应用,因为 AllowMultiple 设置为 true。 所显示的两种用于应用多个特性的格式均有效。

如果 Inheritedfalse,那么该特性不是由特性类派生的类继承。 例如:

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
class NonInheritedAttribute : Attribute { }

[NonInherited]
class BClass { }

class DClass : BClass { }

在本例中,NonInheritedAttribute 不会通过继承应用于 DClass

你还可以使用这些关键字来指定应在何处应用特性。 例如,可以使用 field: 说明符将特性添加到自动实现的属性的支持字段中。 或者,可以使用 field:property:param: 说明符将特性应用于根据位置记录生成的任何元素。 有关示例,请参阅属性定义的位置语法

AsyncMethodBuilder 特性

System.Runtime.CompilerServices.AsyncMethodBuilderAttribute 特性添加到可为异步返回类型的类型。 该特性指定会在异步方法返回指定类型时生成异步方法实现的类型。 AsyncMethodBuilder 属性可应用于符合以下条件的类型:

AsyncMethodBuilder 特性的构造函数指定关联的生成器的类型。 生成器必须实现以下可访问的成员:

  • 一个静态 Create() 方法,可返回生成器的类型。

  • 一个可读的 Task 属性,可返回异步返回类型。

  • 一个 void SetException(Exception) 方法,可在任务出错时设置异常。

  • void SetResult()void SetResult(T result) 方法,可将任务标记为已完成,并选择性地设置任务的结果

  • 一个具有以下 API 签名的 Start 方法:

    void Start<TStateMachine>(ref TStateMachine stateMachine)
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • 具有以下签名的 AwaitOnCompleted 方法:

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion
        where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • 具有以下签名的 AwaitUnsafeOnCompleted 方法:

          public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
              where TAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    

若要了解异步方法生成器,可以阅读有关 .NET 提供的以下生成器的信息:

在 C# 10 及更高版本中,可以向异步方法应用 AsyncMethodBuilder 属性,用于替代该类型的生成器。

InterpolatedStringHandlerInterpolatedStringHandlerArguments 属性

从 C# 10 开始,可以使用这些属性指定类型为内插字符串处理程序。 在你使用内插字符串作为 string 参数的自变量的情况下,.NET 6 库已包含 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler。 你可能还有其他要控制内插字符串处理方式的实例。 你需要将 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute 应用到实现处理程序的类型。 你需要将 System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute 应用到类型的构造函数的参数。

若要详细了解如何生成内插字符串处理程序,可以参阅内插字符串改进的 C# 10 语言规范。

ModuleInitializer 特性

从 C# 9 开始,ModuleInitializer 属性标记程序集加载时运行时调用的方法。 ModuleInitializerModuleInitializerAttribute 的别名。

ModuleInitializer 属性只能应用于以下方法:

  • 静态方法。
  • 无参数方法。
  • 返回 void
  • 能够从包含模块(即 internalpublic)访问的方法。
  • 不是泛型的方法。
  • 没有包含在泛型类中的方法。
  • 不是本地函数的方法。

ModuleInitializer 属性可应用于多种方法。 在这种情况下,运行时调用它们的顺序是确定的,但未指定。

下面的示例阐释了如何使用多个模块初始化表达式方法。 Init1Init2 方法在 Main 之前运行,并且每种方法都将一个字符串添加到 Text 属性。 因此,当 Main 运行时,Text 属性已具有来自两个初始化表达式方法中的字符串。

using System;

internal class ModuleInitializerExampleMain
{
    public static void Main()
    {
        Console.WriteLine(ModuleInitializerExampleModule.Text);
        //output: Hello from Init1! Hello from Init2!
    }
}
using System.Runtime.CompilerServices;

internal class ModuleInitializerExampleModule
{
    public static string? Text { get; set; }

    [ModuleInitializer]
    public static void Init1()
    {
        Text += "Hello from Init1! ";
    }

    [ModuleInitializer]
    public static void Init2()
    {
        Text += "Hello from Init2! ";
    }
}

源代码生成器有时需要生成初始化代码。 模块初始化表达式为该代码提供了一个标准位置。 在大多数情况下,应编写静态构造函数,而不是模块初始值设定项。

SkipLocalsInit 特性

从 C# 9 开始,SkipLocalsInit 属性可防止编译器在发出到元数据时设置 .locals init 标志。 SkipLocalsInit 属性是一个单用途属性,可应用于方法、属性、类、结构、接口或模块,但不能应用于程序集。 SkipLocalsInitSkipLocalsInitAttribute 的别名。

.locals init 标志会导致 CLR 将方法中声明的所有局部变量初始化为其默认值。 由于编译器还可以确保在为变量赋值之前永远不使用变量,因此通常不需要使用 .locals init。 但是,在某些情况下,额外的零初始化可能会对性能产生显著影响,例如使用 stackalloc 在堆栈上分配一个数组时。 在这些情况下,可添加 SkipLocalsInit 属性。 如果直接应用于方法,该属性会影响该方法及其所有嵌套函数,包括 lambda 和局部函数。 如果应用于类型或模块,则它会影响嵌套在内的所有方法。 此属性不会影响抽象方法,但会影响为实现生成的代码。

此属性需要 AllowUnsafeBlocks 编译器选项。 这一要求表明,在某些情况下,代码可以查看未分配的内存(例如,读取未初始化的堆栈分配的内存)。

下面的示例阐释 SkipLocalsInit 属性对使用 stackalloc 的方法的影响。 该方法显示分配整数数组后内存中的任何内容。

[SkipLocalsInit]
static void ReadUninitializedMemory()
{
    Span<int> numbers = stackalloc int[120];
    for (int i = 0; i < 120; i++)
    {
        Console.WriteLine(numbers[i]);
    }
}
// output depends on initial contents of memory, for example:
//0
//0
//0
//168
//0
//-1271631451
//32767
//38
//0
//0
//0
//38
// Remaining rows omitted for brevity.

若要亲自尝试此代码,请在 .csproj 文件中设置 AllowUnsafeBlocks 编译器选项:

<PropertyGroup>
  ...
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

请参阅