一、特性的本质:代码的 “智能标签”
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:限制特性可应用的目标(如
Class、Method、Property等); - 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 创建成功
[日志] 创建订单 结束
六、特性的注意事项
- 参数必须是编译时常量:特性的参数只能是字符串、数值、枚举等编译时就能确定的值,不能是变量或运行时计算结果;
- 特性实例化时机:特性实例是运行时通过反射动态创建的,而非编译时预创建;
- 性能问题:反射读取特性比直接代码调用慢,高频场景建议缓存特性实例;
- 命名约定:特性类名以
Attribute结尾,应用时可省略,这是 .NET 的约定俗成。
七、总结:特性的核心知识点
- 本质:编译时嵌入元数据的 “智能标签”,运行时通过反射读取;
- 语法:
[特性名(位置参数, 命名参数=值)],可省略Attribute后缀; - 自定义特性:继承
Attribute,使用[AttributeUsage]限制范围; - 反射读取:
GetCustomAttributes()获取特性实例,IsDefined()检查是否存在; - 高级参数:
AllowMultiple(允许多次应用)、Inherited(是否继承); - 应用场景:框架扩展、AOP、配置解耦、编译时验证。
通过以上步骤,你已经从 “什么是特性” 到 “自定义特性并反射读取”,再到 “实际应用场景”,完整掌握了特性的核心知识。结合之前学的反射和 Type 类型,特性将成为你开发灵活、可扩展 .NET 应用的有力工具!