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

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

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() { }
}

Experimental 属性

从 C# 12 开始,可以使用 System.Diagnostics.CodeAnalysis.ExperimentalAttribute 来标记类型、方法和程序集以指示实验性功能。 如果访问使用 ExperimentalAttribute 注释的方法或类型,编译器将发出警告。 用 Experimental 特性标记的程序集或模块中声明的所有类型都是实验性的。 如果访问其中任何一种类型,编译器都会发出警告。 可以禁用这些警告以试用实验性功能。

警告

实验性功能可能会随时更改。 API 可能会更改,或者可能会在未来的更新中被删除。 包括实验性功能是库作者获取有关未来开发的想法和概念反馈的一种方式。 使用标记为实验性的任何功能时,请格外小心。

可以在功能规范中阅读有关 Experimental 属性的更多详细信息。

SetsRequiredMembers 属性

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

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 属性

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 属性

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>

请参阅