特性,本质上就是一张"便利贴"或"标签"。
想象一下,你在超市里看到各种各样的商品:
-
你可以直接看商品本身(这是类里面的代码逻辑)。
-
你还可以看到商品包装上贴着的额外贴纸,比如"打折促销 "、"易碎品轻拿轻放 "、"保质期 30 天"。
这些额外的贴纸,就是特性!它不改变商品(代码)内部的结构,它只是给这个商品附加了一些"额外的信息(元数据 Metadata)"。
体验 C# 自带的"便利贴"
C# 编译器自己就认识很多便利贴。最经典的一个叫做 [Obsolete](已过时)。
假设你写了一个方法,后来你觉得这个方法写得很烂,写了一个新的。但你怕别的同事还在用老方法,你就可以给老方法贴上一张"便利贴":
怎么自己造一张便利贴?(自定义特性)
只会用系统自带的没什么了不起,真正的高手都会自己造便利贴。 造便利贴非常简单,只需要两步:
-
建一个普通的类,让它继承自
System.Attribute。 -
名字的后缀必须以
Attribute结尾(这是微软的规矩)。
假设我们正在开发一个系统,我们需要给不同的方法贴上"权限标签",规定谁能调用它:
public class Calculator
{
// 给老方法贴上 [Obsolete] 特性
[Obsolete("这个老加法太慢了,请不要用了,改用 NewAdd 方法!", false)]
public void OldAdd(int a, int b)
{
Console.WriteLine(a + b);
}
public void NewAdd(int a, int b)
{
Console.WriteLine($"更快的加法:{a + b}");
}
}
public class Program
{
public static void Main()
{
Calculator calc = new Calculator();
// 当你在代码里敲出 OldAdd 的时候,
// 编译器会看到那张便利贴,并在你代码下面画一条绿色的波浪线警告你!
calc.OldAdd(1, 2);
}
}
在这个例子里,特性做了一件事:给编译器发暗号,让编译器在编译的时候给程序员发警告。
using System;
// 1. 制造一张便利贴:叫 RoleAttribute
// 约定俗成:类名以 Attribute 结尾
public class RoleAttribute : Attribute
{
public string RoleName { get; set; }
// 构造函数:当别人贴这张便利贴时,必须写上角色名
public RoleAttribute(string roleName)
{
this.RoleName = roleName;
}
}
// 2. 把便利贴贴到我们的业务代码上
public class SystemManager
{
// 注意:用的时候,C# 允许你把后缀 Attribute 省略掉,直接写 [Role("...")]
[Role("Admin")] // 只有管理员能删库
public void DeleteDatabase()
{
Console.WriteLine("数据库已删除!");
}
[Role("User")] // 普通用户只能看
public void ViewData()
{
Console.WriteLine("这是您的数据。");
}
}
👑 第四步:特性是怎么起作用的?(终极杀招:结合反射)
这是最重要的一环!
很多人学完第三步就懵了:"我贴上了 [Role("Admin")],然后呢?我怎么知道它有没有起作用?"
真理:特性(便利贴)自己是没有任何执行能力的!它就是个死物。它必须配合"反射(Reflection)"这把扫描枪,才能发挥出巨大的威力。
还记得你之前学的 System.Type 类吗?这就是扫描枪!
public class Program
{
public static void Main()
{
// 假设当前登录的用户是普通用户
string currentUserRole = "User";
// 1. 拿出反射扫描枪,拿到 SystemManager 的 Type 图纸
Type type = typeof(SystemManager);
// 2. 拿到 DeleteDatabase 这个方法的图纸
var methodInfo = type.GetMethod("DeleteDatabase");
// 3. ✨ 核心代码:用扫描枪去看看这个方法上面,有没有贴着 RoleAttribute 这种便利贴?
// GetCustomAttribute 是专门用来撕下便利贴的方法
RoleAttribute attribute = (RoleAttribute)Attribute.GetCustomAttribute(methodInfo, typeof(RoleAttribute));
if (attribute != null) // 如果贴了这种便利贴
{
// 看看便利贴上写的是啥级别?
if (attribute.RoleName == currentUserRole)
{
Console.WriteLine("权限验证通过,开始执行方法!");
// methodInfo.Invoke(...); // 利用反射执行方法
}
else
{
Console.WriteLine($"警告!当前用户是 {currentUserRole},但这个方法需要 {attribute.RoleName} 权限!拒绝执行!");
}
}
}
}
首先定义一个特性:
//定义一个Remark特性,定义的时候必须要以Attribute结尾,并且继承Attribute
public class RemarkAttribute : Attribute {
public string Name { get; set; }
public RemarkAttribute(string name) {
this.Name=name;
}
}
//使用的时候可以省略Attribute
//为Course类添加一个Remark特性。
[Remark("课程信息实体类")]
public class Course
{
public Course() { }
[Remark("课程名")]
public string Name { get; set; }
[Remark("课程编号")]
public int courseId { get; set; }
[Remark("课程信息实体类")]
public string CourseName { get; set; }
}
特性的调用:
Course c1=new Course();
var courseType = typeof(Course); //使用type
object[] attrs=courseType.GetCustomAttributes(inherit: false);
var remarkAttribute2= courseType.GetCustomAttribute<RemarkAttribute>();
string remarkAttribut3= remarkAttribute2.Name;
Console.WriteLine($"remarkAttribute的remark信息注释为:"+ remarkAttribut3);
特性不需要实例化对象,只需要又这个类即可,即可根据类来寻找到这个类中的特性标签。
核心概念:特性(Attribute)是贴在"图纸"上的,而不是贴在"房子"上的!
这是新手最容易踩的认知误区:
类(Class / Type):相当于一张建筑图纸。
对象(Instance / Object):相当于根据图纸盖出来的真实的房子。
属性(Property,比如 Name, Age):是房子里的家具。每个房子可以有不同的家具(张三的 Name 和李四的 Name 是不一样的)。
特性(Attribute,比如 [Remark]):是直接印在图纸右上角的一行批注(比如:"此图纸由皇家设计院审批")。
因为特性是印在"图纸"上的,所以只要图纸定下来了,特性就永远固定了。不管你根据这张图纸盖了 1 个房子(new Course()),还是盖了 10000 个房子,或者一个房子都不盖,那张图纸上的批注(特性)都在那里,绝不会改变。
🔍 拆解你的代码
让我们逐行看看你的代码到底在干什么:
// 1. 你花钱请工人,根据 Course 图纸,盖了一栋真实的房子,起名叫 customer。
// (但这栋房子在后面的代码里,一次都没被用过!)
Course customer = new Course();
// 2. 你根本没去管那个房子,而是直接跑去档案室,把 Course 的"原始图纸(Type)"调了出来。
var courseType = typeof(Course);
// 3. 你拿着扫描枪,对着"图纸"一顿扫,撕下了上面的 [Remark] 便利贴。
var remarkAttribute2 = courseType.GetCustomAttribute<RemarkAttribute>();
// 4. 你读取了便利贴上的名字并打印。
string remarkAttribut3 = remarkAttribute2.Name;
Console.WriteLine($"remarkAttribute的remark信息注释为:"+ remarkAttribut3);
IEnumerable<VIPAttribute> MemberInfo.GetCustomAttributes<VIPAttribute>()
(扩展):说明这是一个扩展方法 ,不是MemberInfo原生自带的,而是System.Reflection命名空间提供的 "外挂方法",所有继承MemberInfo的类型(Type、MethodInfo、PropertyInfo等)都能直接调用。IEnumerable<VIPAttribute>:返回值类型,是一个可遍历的集合,里面的元素全都是VIPAttribute类型的对象,直接强类型返回,不用自己做类型转换。
2. 核心作用说明
检索应用于指定成员的指定类型的自定义特性集合。
翻译成人话:
- 这个方法的核心作用,就是从一个类 / 方法 / 属性等成员上,精准筛选出你指定类型的所有自定义特性。
- 比如你给
Course类标记了[VIP]、[Obsolete]、[Serializable]三个特性,调用GetCustomAttributes<VIPAttribute>()只会返回[VIP]特性的集合,其他两个会被自动忽略。
3. 返回结果说明
将应用于与 element 并与 T 匹配的自定义特性的集合,如果此类特性不存在,则为空集合。
- 这里的
element就是你调用方法的那个成员(比如你代码里的courseType,也就是typeof(Course))。 - 如果成员上标记了多个
VIPAttribute(比如允许重复标记),集合里就会有多个元素;如果没有标记任何VIPAttribute,它不会返回null,而是返回一个空的IEnumerable<VIPAttribute>,你可以直接用foreach遍历,不会报空引用异常。
4. 可能抛出的异常
ArgumentNullException:如果你调用方法的MemberInfo对象是null(比如courseType为null),会抛出这个异常。NotSupportedException:当这个成员类型不支持反射(比如某些动态生成的类型)时抛出。TypeLoadException:当VIPAttribute这个特性的类型无法加载(比如特性所在的程序集缺失)时抛出。
三、和你之前代码的关联 & 记忆技巧
关联你之前的代码
你写的 courseType.GetCustomAttributes<VIPAttribute>(),和这个提示是完全对应的:
courseType是Type类型,继承自MemberInfo,所以能调用这个扩展方法。- 调用后直接拿到
IEnumerable<VIPAttribute>,不用再写if (o is VIPAttribute)做类型判断,代码更简洁安全。
记忆技巧(下次一眼看懂)
- 看返回值:
IEnumerable<VIPAttribute>→ 知道这是强类型的特性集合。 - 看方法名:
GetCustomAttributes<T>()→ 知道这是获取指定类型的所有特性 ,Attributes带复数,说明可能返回多个。 - 看说明:"检索指定类型的自定义特性集合" → 直接理解为 "我只要
T这种类型的标签,其他的都不要"。
在 C# 的世界里,只要是写在代码里的东西,都有一个专门的反射类来描述它:
-
描述类 的图纸,叫
Type。 -
描述方法 的图纸,叫
MethodInfo。 -
描述属性 的图纸,叫
PropertyInfo。 -
描述字段 的图纸,叫
FieldInfo。
而这四大护法,全都认同一个老祖宗(继承自同一个父类)------那就是 MemberInfo(成员信息)!
因为特性的便利贴,不仅可以贴在"类"上,还可以贴在"方法"上,甚至可以贴在"属性"上。 微软的架构师心想:"我总不能给 Type 写一个撕便利贴的方法,再给 MethodInfo 写一个一模一样的撕便利贴的方法吧?太麻烦了!"
于是,微软直接把 GetCustomAttributes 这个撕便利贴的方法,写在了它们共同的老祖宗 MemberInfo 身上。
这样一来,就像你说的,所有继承自 MemberInfo 的子类,天然就拥有了这个方法!
💻 实际代码验证你的猜想:
C#
// 1. 撕下类(Type)身上的便利贴
Type myClass = typeof(Course);
var classAttrs = myClass.GetCustomAttributes<VIPAttribute>();
// 2. 撕下方法(MethodInfo)身上的便利贴
MethodInfo myMethod = myClass.GetMethod("Study");
var methodAttrs = myMethod.GetCustomAttributes<VIPAttribute>();
// 3. 撕下属性(PropertyInfo)身上的便利贴
PropertyInfo myProp = myClass.GetProperty("Name");
var propAttrs = myProp.GetCustomAttributes<VIPAttribute>();
你看,无论是类、方法还是属性,它们调用的都是同一个老祖宗提供的方法!你现在的思维,已经和当年设计 C# 底层框架的微软工程师完全同步了。
在 C# 和绝大多数强类型语言中,方法签名的标准书写格式永远是: [返回值类型] [归属的类名].[方法名]([参数列表])
所以当你看到 IEnumerable<VIPAttribute> MemberInfo.GetCustomAttributes<VIPAttribute>() 时:
-
IEnumerable<VIPAttribute>就是返回值。 -
MemberInfo是这个方法所属的类。 -
GetCustomAttributes<VIPAttribute>()是方法名和泛型参数。

IEnumerable<T>
IEnumerable<T> 是整个 .NET 框架中最核心、最伟大的接口设计,没有之一。
📖 一、 名字全称与核心定义
在拆解它之前,我们先把它的英文全称"扒光":
-
I:代表 Interface(接口)。在 C# 中,所有接口的名字都以大写字母 I 开头。 -
Enumerable:意为 "可枚举的 / 可遍历的"(来自单词 Enumerate,即"一个一个点数")。 -
<T>:代表 Type (泛型类型),代表这个集合里装的到底是什么类型的元素(比如Student,string)。
一句话核心定义: IEnumerable<T> 是一个协议(契约) 。任何类(比如数组 T[]、列表 List<T>、字典 Dictionary<TKey, TValue>)只要签署了这份协议,就意味着它向全天下宣布:"我的内部装了一堆东西,并且我支持你用循环(foreach)把它们一个一个拿出来看!"
二、 背景原理:它到底是怎么工作的?(迭代器模式)
这是面试大厂最爱问的底层原理。IEnumerable<T> 本身其实非常"空虚",它里面连一个存放数据的变量都没有。它只规定了一个极其简单的方法:
C#
public interface IEnumerable<T>
{
// 只有一个方法:要一个"枚举器(迭代器)"
IEnumerator<T> GetEnumerator();
}
大白话比喻:
-
IEnumerable<T>就像是一台电视机(集合容器)。 -
GetEnumerator()就像是电视机上的一个按钮,你一按,它就会弹出一个遥控器(IEnumerator<T>)。 -
你拿到这个遥控器(
IEnumerator<T>)之后,遥控器上有两个核心功能:-
MoveNext():按下"下一台"按钮,看看还有没有下一个节目。 -
Current:看着屏幕,获取当前正在播放的节目。
-
🎩 揭秘 C# 的终极语法糖:foreach 的真面目
你平时写的 foreach 循环,C# 编译器在底层其实是不认识的。当你点击编译时,编译器会把 foreach 极其残忍地拆解成"遥控器模式"。
你写的极简代码:
C#
List<string> names = new List<string> { "张三", "李四" };
// 你以为的遍历
foreach (string name in names)
{
Console.WriteLine(name);
}
编译器在底层偷偷转换成的真实代码(不要跳步,仔细看):
C#
// 1. 按下电视机上的按钮,获取遥控器
IEnumerator<string> enumerator = names.GetEnumerator();
try
{
// 2. 疯狂按遥控器上的"下一台"按钮(只要返回 true 就说明还有数据)
while (enumerator.MoveNext())
{
// 3. 看屏幕,获取当前的数据
string name = enumerator.Current;
Console.WriteLine(name);
}
}
finally
{
// 4. 用完遥控器,把它销毁掉(释放内存)
if (enumerator != null)
{
enumerator.Dispose();
}
}
总结: foreach 只是表象,IEnumerable 提供遥控器,IEnumerator 负责真正遍历,这才是底层的"迭代器模式(Iterator Pattern)"!
🚀 三、 延伸拓展:LINQ 与 延迟执行(Lazy Evaluation)
为什么你之前学 OrderBy、Where、Select 时,它们的返回值清一色全是 IEnumerable<T>?
因为微软在设计 LINQ 时,加入了一个极其科幻的机制:延迟执行(也叫惰性求值)。
当你写下这行代码时:
C#
IEnumerable<UserInfo> query = UserList.Where(u => u.Age > 18).OrderBy(u => u.Id);
❌ 坑二:在遍历时修改集合(InvalidOperationException)
这是新手必踩的坑。遥控器在遍历数据的时候,要求底层数据绝对不能发生变化。
C#
List<string> names = new List<string> { "张三", "李四", "王五" };
// ❌ 在遍历的过程中,试图往里面加人或者踢人
foreach (string name in names)
{
if (name == "李四")
{
names.Remove(name); // 💥 程序当场崩溃!报错:集合已修改;可能无法执行枚举操作。
}
}
✅ 正确避坑法:
-
使用普通的
for循环(倒序删除)。 -
或者用
ToList()复制一个副本,遍历副本,修改原件:foreach(var name in names.ToList())
📊 知识点全景架构 (文本脑图)
为了让你建立结构化的记忆,可以将以下层次刻在脑海里:
-
IEnumerable (核心基石)
-
职责:提供一个访问集合的标准化入口。
-
核心方法 :
GetEnumerator()(索要遥控器)。 -
底层支撑 :
IEnumerator<T>(遥控器实体,负责MoveNext和Current)。 -
顶层表现 :
foreach语法糖、LINQ 查询链。 -
高级特性 :延迟执行 (不到最后关头绝不干活)、
yield return状态机。
-
你现在看集合相关的源码,应该再也没有任何死角了。所有看似魔法的操作,扒开一层皮,全都是基于这个极简的遥控器接口构建的庞大帝国。