.NET进阶——深入理解特性(1)特性入门

95 阅读4分钟

一、特性的本质:代码的 “智能标签”

1. 什么是特性?

特性是 .NET 中用于给代码元素(类、方法、属性等)附加元数据的机制,可以类比为:

  • 普通注释:写给人看的,编译时被忽略;
  • 特性:写给程序 / 框架看的,编译时嵌入程序集的元数据区,运行时可通过反射读取。

2. 特性的核心价值

  • 解耦配置:将配置信息(如路由、权限)直接写在代码上,无需单独配置文件;
  • 框架扩展:为框架提供灵活的扩展点(如 ASP.NET Core 的 [Route]、EF Core 的 [Table]);
  • 运行时决策:程序可根据特性动态调整行为(如根据 [Log] 特性自动记录日志)。

二、先看 “现成的”:内置特性

.NET 自带了很多常用特性,先从这些简单的入手,感受特性的用法。

1. [Obsolete]:标记代码已过时

作用:编译时提示警告或错误,告诉开发者代码已过时。语法[Obsolete("提示信息", 是否报错)]

示例

class Program
{
    static void Main()
    {
        OldMethod(); // 编译时会有警告:“OldMethod() 已过时:请使用 NewMethod()”
        NewMethod();
    }

    [Obsolete("请使用 NewMethod()", false)] // false=警告,true=编译错误
    static void OldMethod()
    {
        Console.WriteLine("这是旧方法");
    }

    static void NewMethod()
    {
        Console.WriteLine("这是新方法");
    }
}

2. [Serializable]:标记可序列化

作用:标记类可以被序列化(如二进制序列化、JSON 序列化)。语法[Serializable]

示例

[Serializable] // 标记 Person 类可序列化
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

3. [Conditional]:条件编译

作用:根据条件编译符号决定是否保留方法调用。语法[Conditional("符号名")]

示例

#define DEBUG // 定义编译符号 DEBUG

class Program
{
    static void Main()
    {
        Log("调试信息"); // 只有定义了 DEBUG 符号才会执行
        Console.WriteLine("主逻辑");
    }

    [Conditional("DEBUG")]
    static void Log(string message)
    {
        Console.WriteLine($"[DEBUG] {message}");
    }
}

三、特性的基础语法

1. 应用特性的规则

  • 位置:写在代码元素(类、方法等)的上方,用 [] 包裹;
  • 省略后缀:特性类名以 Attribute 结尾时,应用时可省略(如 [AuthorAttribute] 可简写为 [Author]);
  • 多特性:同一元素可应用多个特性,用 , 分隔或分行写。

2. 特性的参数

特性支持两种参数:

  • 位置参数:必须按顺序传入,对应特性类的构造函数参数
  • 命名参数:可选,对应特性类的公共属性 / 字段,格式为 属性名=值

示例

// 位置参数:name(必填);命名参数:Email、Year(可选)
[Author("张三", Email = "zhangsan@example.com", Year = 2024)]
class Person { }

四、自定义特性:从 0 到 1 实现

自定义特性是核心,完整步骤如下:

步骤 1:定义特性类(继承 Attribute)

特性类必须继承 .NET 提供的 System.Attribute 基类,并使用 [AttributeUsage] 限制其使用范围。

示例:定义一个 [Author] 特性,用于标记代码作者。

using System;

// [AttributeUsage] 限制特性的应用范围和行为
[AttributeUsage(
    AttributeTargets.Class | AttributeTargets.Method, // 可应用到类或方法
    AllowMultiple = true,                            // 允许同一元素多次应用
    Inherited = false                                // 不允许子类继承
)]
public class AuthorAttribute : Attribute // 命名约定:以 Attribute 结尾
{
    // 位置参数:构造函数传入(必填)
    public string Name { get; }
    
    // 命名参数:公共属性(可选)
    public string Email { get; set; }
    public int Year { get; set; } = DateTime.Now.Year; // 默认值

    // 构造函数(接收位置参数)
    public AuthorAttribute(string name)
    {
        Name = name;
    }
}

步骤 2:应用自定义特性

在目标代码元素前使用 [特性名] 标记。

示例:将 [Author] 应用到类和方法。

// 应用到类(位置参数 + 命名参数)
[Author("张三", Email = "zhangsan@example.com", Year = 2024)]
public class UserService
{
    // 多次应用(因为 AllowMultiple = true)
    [Author("李四", Email = "lisi@example.com")]
    [Author("王五")]
    public void AddUser(string name)
    {
        Console.WriteLine($"添加用户:{name}");
    }
}

步骤 3:通过反射读取特性

使用反射 API(结合之前学的 Type 类型)读取特性元数据。

示例:读取 UserService 类和 AddUser 方法上的 [Author] 特性。

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        // 1. 获取类型
        Type serviceType = typeof(UserService);

        // 2. 读取类上的特性
        Console.WriteLine("=== 类上的 Author 特性 ===");
        // GetCustomAttributes:获取所有指定类型的特性实例,false=不检查继承链
        AuthorAttribute[] classAuthors = (AuthorAttribute[])serviceType.GetCustomAttributes(typeof(AuthorAttribute), false);
        foreach (var author in classAuthors)
        {
            Console.WriteLine($"作者:{author.Name},邮箱:{author.Email},年份:{author.Year}");
        }

        // 3. 读取方法上的特性
        Console.WriteLine("\n=== 方法上的 Author 特性 ===");
        MethodInfo addUserMethod = serviceType.GetMethod("AddUser");
        AuthorAttribute[] methodAuthors = (AuthorAttribute[])addUserMethod.GetCustomAttributes(typeof(AuthorAttribute), false);
        foreach (var author in methodAuthors)
        {
            Console.WriteLine($"作者:{author.Name},邮箱:{author.Email},年份:{author.Year}");
        }
    }
}

运行结果

=== 类上的 Author 特性 ===
作者:张三,邮箱:zhangsan@example.com,年份:2024

=== 方法上的 Author 特性 ===
作者:李四,邮箱:lisi@example.com,年份:2024
作者:王五,邮箱:,年份:2024

五、特性的高级用法

1. 检查特性是否存在:IsDefined

如果只需判断特性是否存在,无需读取其属性,使用 IsDefined 方法更高效(避免实例化特性对象)。

示例

// 检查 UserService 类是否应用了 AuthorAttribute
// 第一个参数:特性的类型,第二个参数:是否检查继承链
bool hasAuthor = serviceType.IsDefined(typeof(AuthorAttribute), false);
Console.WriteLine($"类是否有 Author 特性:{hasAuthor}"); // 输出:True

2. AttributeUsage 的关键参数

  • AttributeTargets:限制特性可应用的目标(如 ClassMethodProperty 等);
  • AllowMultiple:是否允许同一元素多次应用该特性(默认 false);
  • Inherited:特性是否可被子类继承(默认 true)。

示例:特性不允许继承

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

[NoInherit]
class BaseClass { }

class DerivedClass : BaseClass { } // 子类不会继承 [NoInherit] 特性

// 检查子类是否有特性
bool derivedHasAttr = typeof(DerivedClass).IsDefined(typeof(NoInheritAttribute), true);
Console.WriteLine(derivedHasAttr); // 输出:False

3. 特性的实际应用场景:AOP 日志

结合反射和特性,可以实现简单的面向切面编程(AOP) ,比如自动记录方法调用日志。

示例

// 1. 定义日志特性
[AttributeUsage(AttributeTargets.Method)]
public class LogAttribute : Attribute
{
    public string Message { get; set; } = "方法执行";
}

// 2. 应用特性到方法
class OrderService
{
    [Log(Message = "创建订单")]
    public void CreateOrder(int orderId)
    {
        Console.WriteLine($"订单 {orderId} 创建成功");
    }
}

// 3. 反射调用方法并自动记录日志
class Program
{
    static void Main()
    {
        OrderService orderService = new OrderService();
        MethodInfo method = typeof(OrderService).GetMethod("CreateOrder");

        // 检查是否有 Log 特性
        if (method.IsDefined(typeof(LogAttribute), false))
        {
            LogAttribute logAttr = (LogAttribute)method.GetCustomAttribute(typeof(LogAttribute));
            Console.WriteLine($"[日志] {logAttr.Message} 开始");
        }

        // 执行方法
        method.Invoke(orderService, new object[] { 1001 });

        if (method.IsDefined(typeof(LogAttribute), false))
        {
            LogAttribute logAttr = (LogAttribute)method.GetCustomAttribute(typeof(LogAttribute));
            Console.WriteLine($"[日志] {logAttr.Message} 结束");
        }
    }
}

运行结果

[日志] 创建订单 开始
订单 1001 创建成功
[日志] 创建订单 结束

六、特性的注意事项

  1. 参数必须是编译时常量:特性的参数只能是字符串、数值、枚举等编译时就能确定的值,不能是变量或运行时计算结果;
  2. 特性实例化时机:特性实例是运行时通过反射动态创建的,而非编译时预创建;
  3. 性能问题:反射读取特性比直接代码调用慢,高频场景建议缓存特性实例;
  4. 命名约定:特性类名以 Attribute 结尾,应用时可省略,这是 .NET 的约定俗成。

七、总结:特性的核心知识点

  1. 本质:编译时嵌入元数据的 “智能标签”,运行时通过反射读取;
  2. 语法[特性名(位置参数, 命名参数=值)],可省略 Attribute 后缀;
  3. 自定义特性:继承 Attribute,使用 [AttributeUsage] 限制范围;
  4. 反射读取GetCustomAttributes() 获取特性实例,IsDefined() 检查是否存在;
  5. 高级参数AllowMultiple(允许多次应用)、Inherited(是否继承);
  6. 应用场景:框架扩展、AOP、配置解耦、编译时验证。

通过以上步骤,你已经从 “什么是特性” 到 “自定义特性并反射读取”,再到 “实际应用场景”,完整掌握了特性的核心知识。结合之前学的反射和 Type 类型,特性将成为你开发灵活、可扩展 .NET 应用的有力工具!