C#特性(Attribute)与反射(Reflection)


一、特性详解

1.特性概述和定义方式

特性(Attribute)是用于在运行时传递程序中各种元素(类、方法、结构、枚举、组件等)行为信息的声明性标签。它通过方括号 [ ] 来描述,可以为程序添加元数据(Metadata)。

元数据:保存在程序集中有关程序及其类型的数据,主要用来描述C#中各种元素(类、方法、构造函数、属性等)。

特性本质上是一个类,必须满足以下条件:

// 1. 命名建议以Attribute结尾,使用大驼峰命名
// 2. 必须继承基类Attribute
// 3. 使用AttributeUsage特性来控制自定义特性的应用范围

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
                AllowMultiple = true, 
                Inherited = false)]
internal class MyAttribute : Attribute
{
    public string Version { get; set; }
    public string Name { get; set; }
    
    public MyAttribute(string version, string name)
    {
        Version = version;
        Name = name;
    }
}

2. AttributeUsage特性

AttributeUsage 用于描述如何使用自定义特性,规定特性可应用到的项目类型:

参数 说明 默认值
validon 特性可被放置的语言元素(AttributeTargets枚举值组合) AttributeTargets.All
AllowMultiple 是否允许多次使用该特性 false
Inherited 是否可被派生类继承 false

AttributeTargets 常用枚举值

说明
Class
Method 方法
Field 字段
Property 属性
Constructor 构造函数
Event 事件
Interface 接口

3. 内置特性

3.1 SerializableAttribute(序列化特性)

作用:标记类型可以被序列化,即可以将对象转换为字节流进行存储或传输。

示例

[Serializable]  // 标记类可序列化
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

使用场景:对象持久化、网络传输、深拷贝等。

3.2 ConditionalAttribute(条件特性)

作用:标记条件方法,其执行依赖于指定的预处理标识符。用于条件编译。

#define Debug   // 定义编译符号
#undef Debug  // 取消编译符号

using System.Diagnostics;

[ConditionalAttribute("Debug")] // 条件编译特性
public static void T1()
{
    Console.WriteLine("1111");
}

工作原理

  • 当定义了 Debug 编译符号时,T1() 方法会被编译进项目
  • 当未定义 Debug 编译符号时,T1() 方法调用会被编译器忽略

应用场景:调试日志输出、测试代码隔离等。

3.3 ObsoleteAttribute(废弃特性)

作用:标记不应被使用的程序实体,通知编译器丢弃某个特定的目标元素。

// 参数1:废弃提示信息
// 参数2:是否编译错误(true=错误,false=警告)
[Obsolete("提示Test1有可能以后会被丢弃,会被test2方法进行替代", false)]
static void Test1()
{
    Console.WriteLine("Test1");
}

语法

[Obsolete(message)]           // 仅警告
[Obsolete(message, iserror)]  // 警告或错误

应用场景:API版本升级、方法废弃提示等。

3.4 程序集级别特性(了解)

程序集级别特性用于描述整个程序集的元数据,通常定义在 AssemblyInfo.cs 文件中。

// 程序集标题
[assembly: AssemblyTitle("6自定义特性")]
// 程序集描述
[assembly: AssemblyDescription("")]
// 程序集配置
[assembly: AssemblyConfiguration("")]
// 公司名称
[assembly: AssemblyCompany("微软中国")]
// 产品名称
[assembly: AssemblyProduct("6自定义特性")]
// 版权信息
[assembly: AssemblyCopyright("Copyright © 微软中国 2026")]
// 商标
[assembly: AssemblyTrademark("")]
// 文化信息
[assembly: AssemblyCulture("")]

// COM可见性
[assembly: ComVisible(false)]

// GUID(用于COM类型库)
[assembly: Guid("e243efae-b57c-4bfd-bfdf-6769e4696177")]

// 程序集版本(主版本.次版本.生成号.修订号)
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

程序集级别特性说明

特性 作用
AssemblyTitle 程序集标题
AssemblyDescription 程序集描述信息
AssemblyConfiguration 程序集配置(如Debug/Release)
AssemblyCompany 公司名称
AssemblyProduct 产品名称
AssemblyCopyright 版权信息
AssemblyTrademark 商标信息
AssemblyCulture 文化/语言信息
ComVisible 是否对COM组件可见
Guid COM类型库的唯一标识符
AssemblyVersion 程序集版本号
AssemblyFileVersion 文件版本号

4. 自定义特性

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
                AllowMultiple = true, 
                Inherited = false)]
internal class MyAttribute : Attribute
{
    public string Version { get; set; }  // 版本
    public string Name { get; set; }     // 名称
    public string Message { get; set; }  // 描述信息
    public string CallTime { get; set; } // 时间
    
    public MyAttribute(string version, string name, string message, string callTime)
    {
        Version = version;
        Name = name;
        Message = message;
        CallTime = callTime;
    }
}
[My("1.0", "Jack", "入口类", "2026-7-1")]
internal class Program
{
    [MyAttribute("1.0", "Jack", "Test1方法", "2026-7-1")]
    [MyAttribute("2.0", "Rose", "Test1方法", "2026-7-2")]  // AllowMultiple=true 允许多次使用
    public static void Test1()
    {
        Console.WriteLine("Test1");
    }
}

注意:特性名称使用时可省略 Attribute 后缀(My 等价于 MyAttribute)。

5. 自定义控件常用特性

在Windows Forms自定义控件开发中,常用以下特性来控制属性面板的显示:

[Browsable(true)]          // 是否在属性面板中展示(true=展示,false=隐藏)
[Category("佛得角")]        // 属性分类名称
[Description("账号的内容")]  // 属性描述信息
[DefaultValue("佛得角")]    // 默认值(等于默认值显示普通文本,否则加粗)
public string Account
{
    get { return textBox1.Text; }
    set { textBox1.Text = value; }
}

// 多个特性可用逗号分隔
[Description("密码"), Category("阿根廷"), DefaultValue("123"), Obsolete("不建议使用此属性")]
public string Password
{
    get { return textBox2.Text; }
    set { textBox2.Text = value; }
}

控件特性说明

特性 作用
Browsable 控制属性是否在属性窗口中出现
Category 为属性分类,默认放到"杂项"
Description 给属性添加描述信息
DefaultValue 设置属性默认值,VS属性窗口中与默认值不同会显示粗体
DesignerCategory("Code") 告诉VS这是纯代码类,不是可拖拽设计界面

二、反射详解

1.反射概述

反射是指在程序运行中,查看、操作自身或者其他程序集的元数据的各种信息(类、方法、属性、变量、对象等)的行为。反射允许你在运行时检查和操作程序集、类型和对象的信息。

反射提供以下核心能力:

  1. 在运行时查看特性(Attribute)信息
  2. 审查集合中的各种类型,以及实例化这些类型
  3. 延迟绑定方法和属性
  4. 在运行时创建新类型并执行任务

2. 反射常用类

命名空间 功能
Type System 表示类型声明(类、接口、数组、值类型等)
Assembly System.Reflection 表示程序集,可加载和访问程序集信息
MethodInfo System.Reflection 表示方法的元数据
PropertyInfo System.Reflection 表示属性的元数据
FieldInfo System.Reflection 表示字段的元数据
ConstructorInfo System.Reflection 表示构造函数的元数据
EventInfo System.Reflection 表示事件的元数据
MemberInfo System.Reflection 所有成员类型的抽象基类

3. 获取类型(Type)

方式一:使用 typeof() 操作符(编译时确定)

Type type = typeof(int);         // System.Int32
Type type = typeof(Program);     // 命名空间.Program

方式二:使用 GetType() 方法(运行时获取)

object obj = new Student();
Type type = obj.GetType();       // 获取对象的实际类型

4.创建实例的三种方式:

 // 方法1:直接使用CreateInstance带参数
object[] constructorParams1 = new object[] { "admin", "123456" };
object account1 = assembly.CreateInstance("ClassLibrary.Account", false,
    BindingFlags.Default, null, constructorParams1, null, null);

// 方法2:使用构造函数信息
ConstructorInfo constructor = accountType.GetConstructor(new Type[] { typeof(string), typeof(string) });
object[] constructorParams2 = new object[] { "user", "pass" };
object account2 = constructor.Invoke(constructorParams2);

// 方法3:使用Activator(更简单)
object account3 = Activator.CreateInstance(accountType, "admin", "123456");

4. 使用反射调用方法

// 1. 获取类型
Type t1 = typeof(Program);

// 2. 获取方法
MethodInfo info = t1.GetMethod("Test1");

// 3. 调用方法(参数1:调用对象,静态方法传null;参数2:方法参数数组)
if (info != null)
{
    info.Invoke(null, null);
}

5. 使用反射获取特性

// 获取方法上的特性集合
// 参数1:特性类型;参数2:是否搜索继承链
object[] attrs = info.GetCustomAttributes(typeof(MyAttribute), false);

foreach (MyAttribute attr in attrs)
{
    Console.WriteLine(attr.Name + ":" + attr.Message);
}

6. 加载外部程序集(Assembly)

// 1. 定义DLL路径
string path = Path.Combine(Environment.CurrentDirectory, "../../libs/ClassLibrary1.dll");

// 2. 加载程序集
Assembly sbly = Assembly.LoadFile(path);

// 3. 创建类实例(参数:命名空间.类名)
object class1 = sbly.CreateInstance("ClassLibrary1.Class1");

// 4. 获取类型
Type type = class1.GetType();

// 5. 获取所有成员
MemberInfo[] arr = type.GetMembers();

foreach (MemberInfo item in arr)
{
    // 获取属性
    if (item.MemberType == MemberTypes.Property)
    {
        PropertyInfo info = item as PropertyInfo;
        Console.WriteLine(info.GetValue(class1));
    }
    
    // 获取方法
    if (item.MemberType == MemberTypes.Method)
    {
        MethodInfo info = item as MethodInfo;
        if (info.Name == "Test1")
        {
            info.Invoke(class1, null);
        }
    }
}

7. 反射操作字段和属性

操作字段示例

// 获取类型
Type type = typeof(Student);

// 通过程序集创建实例
Assembly assembly = Assembly.Load("当前程序集名称");
object instance = assembly.CreateInstance("命名空间.Student");

// 获取公开字段
FieldInfo fieldInfo = type.GetField("myId");
int myId = (int)fieldInfo.GetValue(instance);
Console.WriteLine(myId);

// 设置字段值
fieldInfo.SetValue(instance, 123);

// 获取所有公开字段
FieldInfo[] fieldInfos = type.GetFields();
foreach (var field in fieldInfos)
{
    Console.WriteLine(field.Name + ": " + field.GetValue(instance));
}

操作属性示例

// 获取属性
PropertyInfo propertyInfo = type.GetProperty("Name");

// 设置属性值
propertyInfo.SetValue(instance, "张三");

// 获取属性值
string name = (string)propertyInfo.GetValue(instance);
Console.WriteLine(name);

// 获取所有公开属性
PropertyInfo[] propertyInfos = type.GetProperties();
foreach (var prop in propertyInfos)
{
    Console.WriteLine(prop.Name);
    if (prop.Name == "Name")
    {
        prop.SetValue(instance, "李四");
    }
}

8. 使用BindingFlags访问私有成员

默认情况下,反射只能访问公共成员。要访问私有成员,需要使用 BindingFlags 参数。

// 获取私有字段
FieldInfo privateField = type.GetField("_privateField", 
    BindingFlags.Instance | BindingFlags.NonPublic);

// 获取私有方法
MethodInfo privateMethod = type.GetMethod("PrivateMethod", 
    BindingFlags.Instance | BindingFlags.NonPublic);

// 获取静态成员
MethodInfo staticMethod = type.GetMethod("StaticMethod", 
    BindingFlags.Static | BindingFlags.Public);

常用BindingFlags组合

组合 说明
BindingFlags.Instance | BindingFlags.Public 实例公共成员
BindingFlags.Instance | BindingFlags.NonPublic 实例私有成员
BindingFlags.Static | BindingFlags.Public 静态公共成员
BindingFlags.Static | BindingFlags.NonPublic 静态私有成员
BindingFlags.Public | BindingFlags.NonPublic 所有访问级别成员

9. 反射获取嵌套类型

// 获取嵌套类型
Type nestedType = type.GetNestedType("NestedClassName");

// 创建嵌套类型实例
object nestedInstance = assembly.CreateInstance(nestedType.FullName);

10. 完整反射示例:Student类操作

目标类定义

public class Student
{
    // 嵌套类型
    public class SmallStudent
    {
        public string Name { get; set; } = "小学生";
    }
    
    // 字段
    public int myId = 10;
    public string MyName = "默认姓名";
    private int _id;
    
    // 属性
    public int Id { get; set; }
    public string Name { get; set; }
    
    // 方法
    [Obsolete("过时的方法")]
    public void Method2(string a, string b)
    {
        Console.WriteLine($"公开方法: {a}{b}");
    }
    
    public static void Method3()
    {
        Console.WriteLine("静态方法");
    }
}

反射操作完整流程

// 1. 获取类型
Type studentType = typeof(Student);

// 2. 获取程序集并创建实例
Assembly assembly = Assembly.Load("当前程序集名称");
object student = assembly.CreateInstance("命名空间.Student");

// 3. 获取并操作字段
FieldInfo fieldInfo = studentType.GetField("myId");
int id = (int)fieldInfo.GetValue(student);
Console.WriteLine($"字段值: {id}");

// 4. 获取所有字段
FieldInfo[] fields = studentType.GetFields();
foreach (var field in fields)
{
    Console.WriteLine($"字段: {field.Name}, 值: {field.GetValue(student)}");
}

// 5. 获取并操作属性
PropertyInfo propInfo = studentType.GetProperty("Name");
propInfo.SetValue(student, "张三");
string name = (string)propInfo.GetValue(student);
Console.WriteLine($"属性值: {name}");

// 6. 获取所有属性
PropertyInfo[] props = studentType.GetProperties();
foreach (var prop in props)
{
    Console.WriteLine($"属性: {prop.Name}");
}

// 7. 获取并调用方法
MethodInfo method = studentType.GetMethod("Method2");
method.Invoke(student, new object[] { "Hello", "World" });

// 8. 获取嵌套类型
Type nestedType = studentType.GetNestedType("SmallStudent");
object smallStudent = assembly.CreateInstance(nestedType.FullName);

// 9. 获取类型信息
Console.WriteLine($"类型名称: {studentType.Name}");
Console.WriteLine($"命名空间: {studentType.Namespace}");
Console.WriteLine($"完整名称: {studentType.FullName}");

11. Assembly加载方式对比

反射中加载程序集有三种常用方式,它们的区别如下:

方法 说明 适用场景
Assembly.Load(assemblyName) 根据程序集名称加载,会搜索GAC、应用程序目录等 加载已引用的程序集
Assembly.LoadFrom(path) 根据文件路径加载,会解析依赖并加载 加载外部DLL及其依赖
Assembly.LoadFile(path) 根据文件路径加载,不解析依赖 只加载单个DLL文件

使用示例

// 方式1:根据程序集名称加载(需已引用)
Assembly assembly1 = Assembly.Load("ClassLibrary1");

// 方式2:根据路径加载(会解析依赖)
string path1 = @"C:\libs\ClassLibrary1.dll";
Assembly assembly2 = Assembly.LoadFrom(path1);

// 方式3:根据路径加载(不解析依赖)
string path2 = @"C:\libs\ClassLibrary1.dll";
Assembly assembly3 = Assembly.LoadFile(path2);

注意

  • LoadFile 不会加载依赖项,适合只需要单个DLL的场景
  • LoadFrom 会自动加载依赖的DLL,但可能导致同一程序集被加载多次
  • Load 适合加载项目中已引用的程序集

12. 性能注意事项

反射的缺点

  1. 性能问题:反射是解释操作,远慢于直接代码调用,涉及装箱拆箱操作
  2. 维护问题:反射绕过了源代码,代码逻辑不够直观,增加维护难度

性能优化建议

  1. 缓存Type对象:避免重复获取类型信息
  2. 减少反射调用次数:将反射结果缓存起来
  3. 使用泛型约束:在编译时确定类型,减少运行时反射
  4. 考虑代码生成:对于性能敏感场景,可在编译时生成代码

13. 反射重要API汇总

API 返回类型 功能
Type.GetField(name) FieldInfo 获取指定字段
Type.GetFields() FieldInfo[] 获取所有字段
Type.GetProperty(name) PropertyInfo 获取指定属性
Type.GetProperties() PropertyInfo[] 获取所有属性
Type.GetMethod(name) MethodInfo 获取指定方法
Type.GetMethods() MethodInfo[] 获取所有方法
Type.GetConstructor() ConstructorInfo 获取构造函数
Type.GetEvent(name) EventInfo 获取事件
Type.GetCustomAttribute(type) Attribute 获取特性
Type.GetCustomAttributes(type) Attribute[] 获取特性数组
Type.GetMembers() MemberInfo[] 获取所有成员
Type.GetNestedType(name) Type 获取嵌套类型
PropertyInfo.GetValue(obj) object 获取属性值
PropertyInfo.SetValue(obj, value) void 设置属性值
FieldInfo.GetValue(obj) object 获取字段值
FieldInfo.SetValue(obj, value) void 设置字段值
MethodInfo.Invoke(obj, params) object 调用方法

三、实际应用案例

案例1:通过反射读取外部DLL

场景:项目需要动态加载外部类库,无需提前引用。

实现步骤

// 1. 加载程序集
string path = Path.Combine(Environment.CurrentDirectory, "../../libs/ClassLibrary1.dll");
Assembly assembly = Assembly.LoadFile(path);

// 2. 创建实例
object instance = assembly.CreateInstance("ClassLibrary1.Class1");

// 3. 获取类型并操作
Type type = instance.GetType();
PropertyInfo prop = type.GetProperty("Name");
string value = (string)prop.GetValue(instance);

案例2:配置文件驱动的依赖注入(WinForms实际应用)

场景:在WinForms应用中,通过配置文件动态加载数据访问层,实现运行时切换数据源。

配置文件App.config):

<configuration>
    <appSettings>
        <add key="DAL" value="ClassLibrary1.Class1" />
    </appSettings>
</configuration>

WinForms窗体实现

using System.Configuration;
using System.Reflection;
using System.Windows.Forms;

public partial class Form1 : Form
{
    string assemblyFilePath = Path.Combine(Environment.CurrentDirectory, "../../libs/ClassLibrary1.dll");
    Type type = null;
    object classInstance = null;

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        // 加载程序集
        Assembly assembly = Assembly.LoadFile(assemblyFilePath);
        
        // 从配置文件读取类型名称
        string typeName = ConfigurationManager.AppSettings["DAL"];
        
        // 创建实例
        classInstance = assembly.CreateInstance(typeName);
        
        // 获取类型
        type = classInstance.GetType();
    }

    private void btnGetInfo_Click(object sender, EventArgs e)
    {
        // 获取所有属性
        PropertyInfo[] properties = type.GetProperties();
        
        foreach (PropertyInfo prop in properties)
        {
            if (prop.Name == "Id")
            {
                lblId.Text = prop.Name;
                txtId.Text = prop.GetValue(classInstance).ToString();
            }
            else if (prop.Name == "Name")
            {
                lblName.Text = prop.Name;
                txtName.Text = prop.GetValue(classInstance).ToString();
            }
        }
    }

    private void btnUpdate_Click(object sender, EventArgs e)
    {
        // 更新属性值
        PropertyInfo[] properties = type.GetProperties();
        
        foreach (PropertyInfo prop in properties)
        {
            if (prop.Name == "Id")
            {
                prop.SetValue(classInstance, int.Parse(txtId.Text));
            }
            else if (prop.Name == "Name")
            {
                prop.SetValue(classInstance, txtName.Text);
            }
        }
        
        MessageBox.Show("更新成功!");
    }
}

优势:无需重新编译,只需修改配置文件即可切换数据访问层实现。

案例3:自定义特性实现版本管理

场景:为类和方法添加版本信息,通过反射读取版本号。

[My("1.0", "Jack", "入口类", "2026-7-1")]
class Program
{
    [My("2.0", "Rose", "Test1方法", "2026-7-2")]
    public static void Test1() { }
}

// 通过反射获取版本信息
MethodInfo method = typeof(Program).GetMethod("Test1");
MyAttribute attr = (MyAttribute)method.GetCustomAttribute(typeof(MyAttribute));
Console.WriteLine($"版本: {attr.Version}, 作者: {attr.Name}");

四、总结

特性(Attribute)要点

  1. 特性是类:必须继承 Attribute 基类
  2. 元数据载体:编译后存入程序集元数据,运行时可通过反射读取
  3. 内置特性ConditionalObsoleteAttributeUsage
  4. 自定义特性:通过继承 Attribute 并重写构造函数实现
  5. 控件特性BrowsableCategoryDescriptionDefaultValue 用于WinForms设计器

反射(Reflection)要点

  1. 核心类TypeAssemblyMethodInfoPropertyInfoFieldInfo
  2. 获取类型typeof() 编译时获取,GetType() 运行时获取
  3. 加载程序集Assembly.LoadFile() 加载外部DLL,Assembly.Load() 加载已引用程序集
  4. 操作成员:通过 GetMethod()GetProperty()GetField() 等获取并调用
  5. 访问私有成员:使用 BindingFlags 参数组合
  6. 性能考虑:反射性能较低,建议缓存结果或减少调用次数

特性与反射的关系

  • 特性:负责添加元数据
  • 反射:负责获取元数据

两者相辅相成,特性为程序元素附加信息,反射在运行时读取这些信息并做出相应决策。

反射优缺点

优点

优点 说明
灵活性高 可在运行时动态加载和操作类型,无需提前硬编码
扩展性强 支持插件化架构,动态加载外部模块
解耦性好 降低模块间耦合,提高自适应能力
元数据访问 可读取程序集元数据,实现自动化工具

缺点

缺点 说明
性能较差 反射是解释操作,涉及装箱拆箱,远慢于直接调用
代码复杂 反射代码比直接代码更复杂,可读性差
维护困难 绕过源代码,增加维护难度
安全风险 可访问私有成员,可能破坏封装性

最佳实践

  1. 特性用于声明式配置:将配置信息附加到代码元素上
  2. 反射用于运行时发现:在需要动态加载和操作类型时使用
  3. 结合使用:特性提供元数据,反射读取并利用这些元数据
  4. 性能优化:缓存反射结果,避免频繁调用
  5. 安全性:反射可访问私有成员,需注意安全风险

关键区别

特性 注释
编译后保留在程序集元数据中 编译时被编译器丢弃
运行时可通过反射读取 仅用于代码说明,不影响执行
可影响程序运行逻辑 纯粹的文档说明

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐