定义和读取自定义属性

属性以声明性的方式将信息与代码相关联。 它们还可以提供可应用于各种目标的可重用元素。 考虑 ObsoleteAttribute。 它可以应用于类、结构、方法、构造函数等。 它 声明 元素已过时。 然后,由 C# 编译器来查找此属性,并执行相应的操作。

本教程介绍如何将属性添加到代码、如何创建和使用自己的属性,以及如何使用内置到 .NET 中的某些属性。

先决条件

需要将计算机设置为运行 .NET。 可以在 .NET 下载 页上找到安装说明。 可以在 Windows、Ubuntu Linux、macOS 或 Docker 容器上运行此应用程序。 需要安装喜欢的代码编辑器。 以下说明使用 Visual Studio Code,它是开源跨平台编辑器。 但是,可以使用任何熟悉的工具。

创建应用

安装完所有工具后,请创建新的 .NET 控制台应用。 若要使用命令行生成器,请在偏好的 shell 中执行以下命令:

dotnet new console

此命令将创建基础 .NET 项目文件。 运行dotnet restore以还原编译此项目所需的依赖项。

无需运行 dotnet restore,因为它由所有需要还原的命令隐式运行,如 dotnet newdotnet builddotnet rundotnet testdotnet publishdotnet pack。 若要禁用隐式还原,请使用 --no-restore 选项。

在某些显式还原有意义的场景中,dotnet restore 命令仍然有用,比如在 Azure DevOps Services 中进行持续集成构建时,或在需要显式控制还原发生时间的生成系统中。

有关如何管理 NuGet 源的信息,请参阅 dotnet restore 文档

若要执行程序,请使用 dotnet run。 此时,应该可以在控制台中看到“Hello, World”输出。

将属性添加到代码

在 C# 中,特性是从 Attribute 基类继承的类。 继承自 Attribute 的任何类都可以用作其他代码片段的“标记”。 例如,有一个名为 的属性 ObsoleteAttribute。 此属性指示代码已过时,不应再使用。 例如,使用方括号将此属性放在类上。

[Obsolete]
public class MyClass
{
}

虽然类被称为ObsoleteAttribute,但在代码中只需使用[Obsolete]。 大多数 C# 代码遵循此约定。 如果选择,可以使用全名 [ObsoleteAttribute]

标记类已过时时,最好提供一些信息,说明其过时的原因和/或替代方案。 将字符串参数包含在已过时属性中,以提供此说明。

[Obsolete("ThisClass is obsolete. Use ThisClass2 instead.")]
public class ThisClass
{
}

字符串作为参数 ObsoleteAttribute 传递给构造函数,就像正在编写 var attr = new ObsoleteAttribute("some string")一样。

特性构造函数的参数仅限于简单类型/文本: bool, int, double, string, Type, enums, etc 和这些类型的数组。 不能使用表达式或变量。 你可以随意使用位置参数或命名参数。

创建自己的属性

通过定义继承自 Attribute 基类的新类来创建特性。

public class MySpecialAttribute : Attribute
{
}

使用上述代码,可以使用 [MySpecial] (或 [MySpecialAttribute]) 作为代码库中的其他位置的属性。

[MySpecial]
public class SomeOtherClass
{
}

.NET 基类库中的属性,例如 ObsoleteAttribute 触发编译器中的某些行为。 但是,你创建的任何属性仅充当元数据,并且不会导致执行属性类中的任何代码。 是否在代码的其他位置使用此元数据由你自行决定。

这里有一个“陷阱”需要注意。 如前所述,在使用属性时,只能将某些类型作为参数传递。 但是,创建属性类型时,C# 编译器不会阻止你创建这些参数。 在以下示例中,你已使用正确编译的构造函数创建了一个属性。

public class GotchaAttribute : Attribute
{
    public GotchaAttribute(Foo myClass, string str)
    {
    }
}

但是,无法将此构造函数与属性语法一起使用。

[Gotcha(new Foo(), "test")] // does not compile
public class AttributeFail
{
}

上述代码会导致编译器错误,例如 Attribute constructor parameter 'myClass' has type 'Foo', which is not a valid attribute parameter type

如何限制属性使用

属性可用于以下“目标”。 前面的示例展示了这些特性在类上的用法,但也可用于:

  • 集会
  • 班级
  • 构造函数
  • 委托
  • 枚举
  • 事件 / 活动
  • 领域
  • GenericParameter
  • 接口
  • 方法
  • 模块
  • 参数
  • 资产
  • ReturnValue
  • 结构

创建特性类时,默认情况下,C# 允许对任何可能的属性目标使用该属性。 如果要将属性限制为某些目标,可以使用 AttributeUsageAttribute 属性类执行此作。 没错,就是将特性应用于特性!

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class MyAttributeForClassAndStructOnly : Attribute
{
}

如果尝试将上述属性置于不是类或结构的内容上,则会收到编译器错误,例如 Attribute 'MyAttributeForClassAndStructOnly' is not valid on this declaration type. It is only valid on 'class, struct' declarations

public class Foo
{
    // if the below attribute was uncommented, it would cause a compiler error
    // [MyAttributeForClassAndStructOnly]
    public Foo()
    { }
}

如何使用附加到代码元素的属性

属性充当元数据。 没有一些外部力量,他们实际上什么也不做。

若要查找和处理属性,需要反射。 反射允许在 C# 中编写检查其他代码的代码。 例如,可以使用反射获取有关类的信息(在代码头添加 using System.Reflection; ):

TypeInfo typeInfo = typeof(MyClass).GetTypeInfo();
Console.WriteLine("The assembly qualified name of MyClass is " + typeInfo.AssemblyQualifiedName);

输出如下内容:The assembly qualified name of MyClass is ConsoleApplication.MyClass, attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

拥有TypeInfo对象(或MemberInfoFieldInfo对象或其他对象)后,可以使用该方法GetCustomAttributes。 此方法返回对象的集合 Attribute 。 还可以使用 GetCustomAttribute 和指定属性类型。

下面的示例展示了对 GetCustomAttributes(在上文中,它包含 MemberInfo 特性)的 MyClass 实例使用 [Obsolete]

var attrs = typeInfo.GetCustomAttributes();
foreach(var attr in attrs)
    Console.WriteLine("Attribute on MyClass: " + attr.GetType().Name);

输出到控制台:Attribute on MyClass: ObsoleteAttribute。 尝试将其他属性添加到 MyClass

请务必注意,这些 Attribute 对象的实例化有延迟。 也就是说,只有在使用GetCustomAttributeGetCustomAttributes时,它们才会被实例化。 这些对象每次都会实例化。 在一行中调用 GetCustomAttributes 两次将返回两个不同的实例 ObsoleteAttribute

运行时中的常见属性

许多工具和框架都使用属性。 NUnit 使用像 [Test][TestFixture] 这样的属性,这些属性由 NUnit 测试运行程序使用。 ASP.NET MVC 使用类似[Authorize] 的属性,并提供一个动作筛选器框架来处理在 MVC 操作中的交叉关注。 PostSharp 使用特性语法在 C# 中允许面向方面的编程。

下面是一些内置于 .NET Core 基类库的显著属性:

  • [Obsolete]。 以上示例中使用了此示例,它位于命名空间中 System 。 提供有关更改代码库的声明性文档非常有用。 消息可以采用字符串的形式提供,另一个布尔参数可用于从编译器警告升级到编译器错误。
  • [Conditional]。 此属性位于命名空间中 System.Diagnostics 。 此属性可以应用于方法(或属性类)。 必须将字符串传递给构造函数。 如果该字符串与指令不匹配 #define ,则 C# 编译器将删除对该方法的任何调用(但不删除该方法本身)。 通常,将此方法用于调试(诊断)目的。
  • [CallerMemberName]。 此属性可用于参数,并位于命名空间中 System.Runtime.CompilerServicesCallerMemberName 是一个属性,用于注入调用另一种方法的方法的名称。 在各种 UI 框架中实现 INotifyPropertyChanged 时,这是一种消除“神秘字符串”的方法。 示例:
public class MyUIClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public void RaisePropertyChanged([CallerMemberName] string propertyName = default!)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string? _name;
    public string? Name
    {
        get { return _name;}
        set
        {
            if (value != _name)
            {
                _name = value;
                RaisePropertyChanged();   // notice that "Name" is not needed here explicitly
            }
        }
    }
}

在上面的代码中,无需有文本 "Name" 字符串。 使用 CallerMemberName 会防止拼写错误相关的 bug,还会导致更流畅的重构/重命名。 特性赋予了 C# 声明性能力,但它们是代码的元数据形式,不能独立运行。