首页 / C# 入门教程 / 常用内置特性

C# 入门教程

常用内置特性

本教程共 100 篇 · 第 78 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程特性AttributeFlagsConditional

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("这是正常逻辑");
Note

Conditional 方法必须是 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——编译器”删掉调用”后返回值无从谈起。

其它易错点:

  1. CallerArgumentExpression 的参数必须可选且有默认值:形如 string? expr = null,编译器才往里填表达式文本,少了默认值会编译报错。
  2. Flags 忘记写 [Flags]:即使值用了 2 的幂,没标 [Flags]ToString() 只会输出数字而非名字组合。
  3. 误以为 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)把参数表达式文本喂给诊断方法。它们都编译进元数据,等着被编译器或反射读取。下一章我们就用反射去”读”这些标签和类型信息。