常用内置特性
本教程共 100 篇 · 第 78 篇 · 更新于 2026-07-31 · 约 11 分钟阅读
78. 常用内置特性
本节目标:学完你会用 Obsolete 退场旧代码、用 Conditional 控编译、用 Flags 让枚举支持位组合,并了解 CallerArgumentExpression 的诊断威力。
上一章认识了特性的基本语法。这章挑四个实战里最常见、也最能体现”特性改变行为”的内置特性,挨个用起来。
Obsolete:温和地淘汰旧成员
上一章提过它的基础用法。这里补两个细节:第二个 bool 参数设 true 时,调用它不再是警告而是编译错误;用 nameof 给出替代项,改名时编译器会同步。
namespace CSharpDemo;
class UserService
{
[Obsolete("请改用 " + nameof(GetUserAsync), true)]
public void GetUser() { }
public void GetUserAsync() { }
}
// new UserService().GetUser(); // 编译直接报错
Tip库作者最爱用
[Obsolete]:先警告、稳定后再升级成错误,给调用方充分的迁移时间。开源库的”软弃用→硬弃用”就是这套节奏。
Conditional:按编译符号决定是否出现
[Conditional("符号名")] 标在方法上,效果是:如果该符号没定义,编译器会直接把调用这行删掉。常用于调试日志,发布版自动消失。
namespace CSharpDemo;
[Conditional("DEBUG")]
void Trace(string msg) => Console.WriteLine($"[调试] {msg}");
Trace("这一行只在 DEBUG 下编译进去");
Console.WriteLine("这是正常逻辑");
NoteConditional 方法必须是
void、不能是重写方法。它比手写#if DEBUG ... #endif更干净,也不会因为漏写#endif而出错。调用被”删掉”,连方法本体都可能不进最终程序。
Flags:让枚举支持位组合
普通枚举每个值互斥。加上 [Flags],再让每个值取 2 的幂,就能用位或 | 组合多个选项,HasFlag 判断是否包含某一项。
namespace CSharpDemo;
[Flags]
enum Permission
{
None = 0,
Read = 1,
Write = 2,
Execute = 4
}
Permission p = Permission.Read | Permission.Write;
Console.WriteLine(p); // Read, Write
Console.WriteLine($"数值 = {(int)p}"); // 3
Console.WriteLine($"能读吗?{p.HasFlag(Permission.Read)}");
Tip用
[Flags]时,值必须是 2 的幂(1、2、4、8…),否则组合会重叠出错。ToString()会贴心地列出所有命中的名字。
CallerArgumentExpression:把表达式文本也报出来(C# 10)
这是 C# 10 引入的”调用方信息”特性之一。它让方法自动拿到”调用处那个参数写的是什么表达式”,做参数校验时特别好用——报错信息直接告诉你哪段条件没过。
namespace CSharpDemo;
int age = 15;
Check(age > 18); // 输出:条件不成立:age > 18
void Check(bool condition,
[CallerArgumentExpression("condition")] string? expr = null)
{
if (!condition)
Console.WriteLine($"条件不成立:{expr}");
}
Note它常被写进校验库(如 Guard 类)。配合上一章学的”抛出异常”,你能给调用方极其精确的报错,而不只是”参数错误”。
自定义特性时用什么限制:AttributeUsage
当你自己写特性(见第 77 章),可以用 [AttributeUsage] 规定它能贴在哪、能否重复贴、是否被子类继承。
namespace CSharpDemo;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
class MyTagAttribute : Attribute
{
public string Note { get; }
public MyTagAttribute(string note) => Note = note;
}
class Demo
{
[MyTag("关键路径")]
public void Run() { }
}
AttributeUsage:给自定义特性立规矩
当你写自己的特性时,[AttributeUsage] 控制三件事:能贴在哪(AttributeTargets)、能否重复贴(AllowMultiple)、是否被子类继承(Inherited)。
namespace CSharpDemo;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
class MyTagAttribute : Attribute
{
public MyTagAttribute(string note) { }
}
Note不写 AttributeUsage 时,默认”可贴任何元素、只能贴一次、会被继承”。多数自定义特性用默认就够了。
Obsolete 的文案与替代项
写 [Obsolete] 时,第一条信息最好直接告诉调用方”该用谁”,用 nameof 指向替代方法,改名时编译器会同步:这样即便将来方法改名,提示文本也不会过期。
Conditional 的多个符号是”或”关系
若一个方法叠了多个 [Conditional],只要其中任意一个符号被定义,调用就会被保留;符号之间是逻辑”或”。
namespace CSharpDemo;
[Conditional("A")]
[Conditional("B")]
void Trace(string m) => Console.WriteLine(m);
// 定义了 A 或 B 其一,Trace 调用才会编译进程序
调用方信息的其他成员
CallerArgumentExpression 之外,C# 还有几个”调用方信息”特性,常用于日志与通知:
CallerMemberName:自动填入调用方的成员名(如属性名)。CallerFilePath:源文件完整路径。CallerLineNumber:调用处的行号。
它们让诊断信息更精准,且不怕重命名破坏字符串。
namespace CSharpDemo;
Log("启动完成"); // 自动打印出调用的方法名
void Log(string msg,
[System.Runtime.CompilerServices.CallerMemberName] string member = "")
{
Console.WriteLine($"[{member}] {msg}");
}
Tip这些特性大多是”写给编译器或框架看的”。你日常写业务少直接用 Conditional/Flags,但读开源库、看框架代码时处处是它们,认识它们等于读懂别人代码的元语言。
常见坑与注意事项
Warning
[Flags]的枚举值必须是 2 的幂(1、2、4、8…)。如果写成 1、2、3,那么Read | Write的数值会等于Execute,三者彻底乱套,位判断全部失真。需要时显式写出None = 0,方便表示”什么都没有”。
Warning
[Conditional]只能标在返回void的方法上,且不能用于重写(override)方法。若方法本要返回计算结果,就别用 Conditional——编译器”删掉调用”后返回值无从谈起。
其它易错点:
- CallerArgumentExpression 的参数必须可选且有默认值:形如
string? expr = null,编译器才往里填表达式文本,少了默认值会编译报错。 - Flags 忘记写 [Flags]:即使值用了 2 的幂,没标
[Flags]时ToString()只会输出数字而非名字组合。 - 误以为 Conditional 是”运行时开关”:它是编译期裁剪,发布版里那行调用根本不存在,不是”运行时不执行”。
四个特性的适用场景对比
| 特性 | 作用时机 | 典型用途 |
|---|---|---|
| Obsolete | 编译期提示/报错 | 旧 API 退场 |
| Conditional | 编译期裁剪调用 | 调试日志 |
| Flags | 运行期位运算 | 权限/选项组合 |
| CallerArgumentExpression | 运行期诊断 | 参数校验报错 |
可以看到,它们并非都在同一阶段生效:Obsolete、Conditional 是”编译期”发力的,Flags、CallerArgumentExpression 则是”运行期”体现价值。理解这一点,才知道该用哪个去解决哪类问题。
Flags 进阶:组合、判断与剔除
会组合还不够,实战里常要判断”是否包含某项”,以及”去掉某项”:
namespace CSharpDemo;
[Flags]
enum Permission { None = 0, Read = 1, Write = 2, Execute = 4 }
Permission p = Permission.Read | Permission.Write;
bool canRead = p.HasFlag(Permission.Read); // True
bool canExec = p.HasFlag(Permission.Execute); // False
// 剔除 Write:用"位与取反"
p &= ~Permission.Write;
Console.WriteLine(p); // Read
p &= ~Permission.Write 是最常用的”减法”写法:把对应位清 0。记住判断用 HasFlag,移除用 & ~某位,别手写魔法数字。
用 CallerArgumentExpression 写个 Guard 助手
把上一节的思路包成一个可复用方法,就是很多库里的”守卫(Guard)“:条件不成立时抛异常,并自动带上失败的表达式原文。
namespace CSharpDemo;
int age = 15;
Ensure(age > 18, "年龄需满 18"); // 报:条件不成立 (age > 18)
void Ensure(bool condition, string msg,
[System.Runtime.CompilerServices.CallerArgumentExpression("condition")] string? expr = null)
{
if (!condition)
throw new ArgumentException($"{msg};失败条件:{expr}");
}
有了它,业务代码里写 Ensure(x > 0, "x 必须为正"),一旦出错日志会精确指出是 x > 0 没过,而不是笼统的”参数错误”。这正是该特性真正的价值所在。
特性也能贴在属性和参数上
上一章讲了”目标(target)“,这里用实例巩固:可以把特性贴到单个属性,甚至方法的参数上。
namespace CSharpDemo;
class Account
{
[Obsolete("改用 UserName 属性")]
public string Name { get; set; } = "";
public string UserName { get; set; } = "";
public void Withdraw([Positive] decimal amount) { }
}
class PositiveAttribute : Attribute { }
注意 Withdraw 的参数 amount 上贴了 [Positive]:框架(或你自己写的反射扫描)就能在调用前校验”金额必须为正”。这说明特性不只是”类级别的标签”,可以精细到属性、参数这种小粒度,做校验、文档、路由都靠它。
再谈适用边界
特性很灵活,但别滥用。经验是:
- 能用普通代码/重载/泛型清晰表达的,就别上特性。
- 特性最适合”横切关注点”:过期提示、序列化、权限、路由、测试标记——这些和具体逻辑正交、却要统一处理的场景。
- 自定义特性基本都配合反射读取才生效,单独贴着没意义。
Tip读到开源库里满屏的
[HttpGet]、[Fact]、[Authorize]时,心里要有数:它们本质都是”贴给框架反射读取的标签”,框架在启动或调用时扫描这些标签来装配行为。
小结
Obsolete 负责退场旧代码,Conditional 按编译符号裁剪调用,Flags 让枚举支持位组合,CallerArgumentExpression(C# 10)把参数表达式文本喂给诊断方法。它们都编译进元数据,等着被编译器或反射读取。下一章我们就用反射去”读”这些标签和类型信息。