C# 编译器解释的杂项属性
属性 Conditional
、Obsolete
、AttributeUsage
、AsyncMethodBuilder
、InterpolatedStringHandler
和 ModuleInitializer
可应用于代码中的元素。 它们为这些元素添加语义。 编译器使用这些语义来更改其输出,并报告使用你的代码的开发人员可能犯的错误。
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 运算符将这些符号逻辑链接在一起),编译器会包含对该方法的调用。 在以下示例中,存在 A
或 B
将导致方法调用:
[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
特性是一次性特性,可以应用于任何允许特性的实体。 Obsolete
是 ObsoleteAttribute 的别名。
在以下示例中,对类 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
类可应用于任何受支持的程序元素。 但是它对每个实体仅能应用一次。 特性应用于基类时,它由派生类继承。
AllowMultiple 和 Inherited 参数是可选的,因此以下代码具有相同效果:
[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
。 所显示的两种用于应用多个特性的格式均有效。
如果 Inherited 为 false
,那么该特性不是由特性类派生的类继承。 例如:
[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
属性可应用于符合以下条件的类型:
- 具有可访问的
GetAwaiter
方法。 GetAwaiter
方法返回的对象实现 System.Runtime.CompilerServices.ICriticalNotifyCompletion 接口。
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 提供的以下生成器的信息:
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder<TResult>
- System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder
- System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder<TResult>
在 C# 10 及更高版本中,可以向异步方法应用 AsyncMethodBuilder
属性,用于替代该类型的生成器。
InterpolatedStringHandler
和 InterpolatedStringHandlerArguments
属性
从 C# 10 开始,可以使用这些属性指定类型为内插字符串处理程序。 在你使用内插字符串作为 string
参数的自变量的情况下,.NET 6 库已包含 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler。 你可能还有其他要控制内插字符串处理方式的实例。 你需要将 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute 应用到实现处理程序的类型。 你需要将 System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute 应用到类型的构造函数的参数。
若要详细了解如何生成内插字符串处理程序,可以参阅内插字符串改进的 C# 10 语言规范。
ModuleInitializer
特性
从 C# 9 开始,ModuleInitializer
属性标记程序集加载时运行时调用的方法。 ModuleInitializer
是 ModuleInitializerAttribute 的别名。
ModuleInitializer
属性只能应用于以下方法:
- 静态方法。
- 无参数方法。
- 返回
void
。 - 能够从包含模块(即
internal
或public
)访问的方法。 - 不是泛型的方法。
- 没有包含在泛型类中的方法。
- 不是本地函数的方法。
ModuleInitializer
属性可应用于多种方法。 在这种情况下,运行时调用它们的顺序是确定的,但未指定。
下面的示例阐释了如何使用多个模块初始化表达式方法。 Init1
和 Init2
方法在 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
属性是一个单用途属性,可应用于方法、属性、类、结构、接口或模块,但不能应用于程序集。 SkipLocalsInit
是 SkipLocalsInitAttribute 的别名。
.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>