特性 Attribute 基础
本教程共 100 篇 · 第 77 篇 · 更新于 2026-07-31 · 约 10 分钟阅读
77. 特性 Attribute 基础
本节目标:学完你明白特性是”贴给编译器或运行时看的标签”,会写方括号语法,并认识 [Obsolete]、[Serializable] 等常见标注。
你可能注意过有些写法是用方括号 [ ] 括起来的,比如 [Obsolete]。这就是特性(attribute)。它不改变程序运行逻辑,而是往代码上”贴标签”,把额外信息(元数据)写进程序集,供编译器或运行时读取。
先用个比方:商品上的标签
特性就像贴在商品上的标签。一瓶药,瓶身写着”孕妇慎用""每日一次”——这些信息不改变药本身,但药店(编译器)和医生(运行时)会读它来做事。代码也一样:[Obsolete] 像”本方法已停产”的告示,[Serializable] 像”本类可快递(序列化)“的标记。
关键是:标签自己不会动,得有人去读它才有意义。
特性是什么
特性本质上是一个类(继承自 System.Attribute),用在方括号里。它描述”这段代码有什么特殊性质”,但不直接变成执行语句。比如标了 [Obsolete] 的方法,编译器会提示”已过时”。
namespace CSharpDemo;
var calc = new Calculator();
Console.WriteLine(calc.Add(1, 2));
// calc.OldAdd(1, 2); // 编译时产生警告
class Calculator
{
[Obsolete("请改用 Add 方法")]
public int OldAdd(int a, int b) => a + b;
public int Add(int a, int b) => a + b;
}
Note特性的信息会被写进程序集的元数据里。它”安静地待着”,直到编译器或反射代码主动去读它——这就是它和”普通代码”最大的不同。
方括号语法与命名约定
写特性时把类(或其名字,去掉 Attribute 后缀)放在方括号里,紧贴在被标注的元素上方。按约定,所有特性类的名字都以 Attribute 结尾,但使用时可以省略。
namespace CSharpDemo;
[Serializable] // 等同于 [SerializableAttribute]
class Person
{
public string Name = "";
public int Age;
}
也就是说 [Serializable] 和 [SerializableAttribute] 完全等价,SerializableAttribute 才是它在 .NET 库里的真名。编译器认得这个”省略后缀”的约定。
[Obsolete]:标记已过时
[Obsolete] 告诉调用者”这个成员不推荐用了”。它有两个可选参数:说明文字,以及一个 bool 表示”是否当作错误”。
namespace CSharpDemo;
class Demo
{
[Obsolete("即将移除,请用 NewMethod")]
public void OldMethod() { }
[Obsolete("禁止使用", true)] // true = 编译直接报错
public void Forbidden() { }
}
// new Demo().Forbidden(); // 编译直接报错
Tip维护老代码时,
[Obsolete]是温和退场的好工具:先标警告让调用方慢慢改,确认没人用再删。设成true则一步到位变编译错误。
[Serializable]:标记可序列化
[Serializable] 标在类上,表示这个类的对象可以被”序列化”(转成字节流保存或传输)。它是最早一批内置特性之一。
namespace CSharpDemo;
[Serializable]
class Config
{
public string Host = "localhost";
public int Port = 8080;
}
Note现代 .NET 更多用 System.Text.Json 等显式序列化器,
[Serializable]在二进制/远程场景仍有用。知道它”是个标签”即可。
特性能贴在哪:目标(target)
特性几乎能贴在任何代码元素上:类、方法、属性、参数,甚至整个程序集。想明确指定贴在哪,用 目标: 特性 的写法。
using System.Reflection;
namespace CSharpDemo;
[assembly: AssemblyTitle("我的示例")] // 贴在整个程序集上
class Sample
{
[method: Obsolete("方法过时")] // 明确贴在方法上
public void Old() { }
}
常见的目标值有:assembly(程序集)、class/type、method、property、field、param、return、module。不写时,特性默认贴在它紧挨着的那个元素上。
特性也能带参数
特性本质是个类,所以它的构造参数(位置参数)和公开属性(命名参数)都能在方括号里传。位置参数必须写在前、按顺序;命名参数可选、顺序随意。
namespace CSharpDemo;
[MyTag("管理员", Note = "拥有全部权限")]
class Admin { }
class MyTagAttribute : Attribute
{
public string Note { get; set; }
public MyTagAttribute(string role) => Role = role;
public string Role { get; }
}
Note位置参数对应构造函数参数,命名参数对应属性/字段。这一规则和”方法调用”几乎一致,只是写在方括号里。
多个特性可以叠加
一个元素上能贴多个特性,逐个写在上方或用逗号分隔都行。顺序通常不影响结果。
namespace CSharpDemo;
[Obsolete("已弃用")]
[Serializable]
class Legacy { }
特性 vs 注释 vs 普通代码
| 方式 | 谁读它 | 能否影响编译/运行 |
|---|---|---|
// 注释 | 只有人读 | 不能 |
| 普通语句 | 编译器照执行 | 直接执行 |
| 特性 | 编译器或反射 | 间接(被读取后才生效) |
注释只是给人看的”便利贴”;普通代码是立即执行的指令;特性则是”写进元数据的说明书”——它自己不做事,但能让编译器报警告、让框架按标签装配。这也解释了为什么特性常被称作”声明式编程”的基石。
常见坑与注意事项
Warning自定义特性时,类必须直接或间接继承
System.Attribute,否则它只是个普通类、方括号里用不了。写class MyTag : Attribute是铁律。
Warning别把”省略后缀”搞反:
[Serializable]等价于[SerializableAttribute],但你不能凭空发明一个不存在的特性名。只有 .NET 内置或你自己定义的XxxAttribute才能这么用。
其它易错点:
- 贴错目标:把只该贴在方法上的特性误贴到类上,编译器可能直接报错或根本不生效。不确定时就用
目标: 特性显式指定。 - 忘了读它:贴了特性却没人(编译器/反射)去读,它就只是个安静的标注,不会产生任何效果。
- 位置参数顺序错:位置参数必须按构造函数声明的顺序写,混了顺序编译会报错。
常见内置特性清单
除了已讲的 [Obsolete]、[Serializable],.NET 还内置很多:
| 特性 | 作用 |
|---|---|
| Obsolete | 标过时 |
| Serializable | 标可序列化 |
| Flags | 枚举按位组合 |
| Conditional | 按编译符号裁剪调用 |
| DllImport | 调用非托管函数 |
| AttributeUsage | 限制自定义特性的使用范围 |
下一章会挑几个高频的细讲,这里先混个脸熟。
特性到底被谁读取
特性自己不会”主动做事”。它被写进元数据后,要么被编译器读(如 Obsolete 产生警告),要么被运行时通过反射读(如框架扫描你的标签)。理解”谁来读”很重要:贴了特性却没人读,它就只是个安静的标注。
Tip自定义特性本身也是类(继承 Attribute)。想自己写一个,只需
class XxxAttribute : Attribute,下一章的 AttributeUsage 就是控制它的开关。
适用场景一览
- 给编译器提示:
[Obsolete]、可空注解([NotNull]等)。 - 给序列化器看:
[Serializable]、[JsonPropertyName]。 - 给测试/框架看:单元测试的
[Fact]、Web 框架的路由特性。 - 给自己的工具看:自定义
[Plugin]、[Authorize]等,配合反射做通用逻辑。
简单说:凡是”想给代码贴说明、又不希望它变成一条具体执行语句”的地方,特性就派上用场。
小结
特性是用 [ ] 写的”元数据标签”,编译进程序集,被编译器或反射读取而不直接执行。[Obsolete] 标过时、[Serializable] 标可序列化,名字可省 Attribute 后缀,还能用 目标: 指定贴的位置。下一章我们看几个最常用的内置特性怎么玩。