CSharp反射学习笔记

一、反射是什么(大白话)

先记住一句话:

反射,就是让程序在运行的时候,能够"看见自己、检查自己、修改自己"。

平时我们写代码,都是"编译前就知道类型长什么样"。比如写 ItemInfo item = new ItemInfo();,编译器早就知道 ItemInfo 有哪些属性、方法。

但有些场景,我们编译的时候根本不知道会用到什么类型,只能到程序跑起来才知道。这时候就需要反射:在运行时去读取一个类型的"说明书"(元数据),知道它有哪些属性、方法,甚至动态创建对象、调用方法。

一个生活化比喻

把"类型"想成一个人:

  • 直接写代码调用,相当于你认识这个人,直接喊他名字让他干活;
  • 反射,相当于你不认识这个人,但你拿到了他的身份证和档案,通过档案知道他的姓名、能力、会做什么,然后按档案去指挥他干活。

什么是元数据

程序集里的每一个类型(类、结构、委托、接口、枚举等)都带有一份"档案",记录着它的成员信息(构造函数、方法、字段、属性、事件等),这份档案就叫元数据(metadata)

通过反射,就能在运行时读取这份元数据,于是"对每一个类型都了如指掌"。

二、反射用到的三个核心类

作用
System.Type 类型的"信息类",获取类的构造函数、方法、字段、属性等信息
System.Reflection.Assembly 程序集类,加载程序集,加载后用 Type 获取其中类的信息
System.Activator 快速实例化对象,把 Type 对象实例化为对应类型的对象

三个类各司其职,配合起来用:

先用 Assembly 加载程序集 → 再用 Type 获取某个类型的信息 → 最后用 Activator 创建出对象实例。

三、反射的用途

  • 运行时查看类型元数据;
  • 检查程序集中的各种类型,并动态实例化它们;
  • 运行时动态读写对象属性值、调用方法、动态加载程序集;
  • 运行时访问代码中的特性(Attribute)。

四、动态实例化(Activator)

正常情况下创建对象是 new ItemInfo()。但如果编译时拿不到类型(只知道类型的完整名字符串),就要用反射动态创建。

先定义一个测试类,后面都围绕它演示:

public class ItemInfo
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Remark { get; set; }

    public string itemType;          // 公有字段

    public string GetItemInfo()
    {
        return Id + "," + Name + "," + Remark;
    }
}

方式一:通过 Type 对象创建

// 1. 根据"完整名称"拿到 Type 对象
// 完整名称 = 命名空间.类名,例如 "Zhaoxi.AdvancedCourse.Models.ItemInfo"
string fullName = "Zhaoxi.AdvancedCourse.Models.ItemInfo";
Type itemType = Type.GetType(fullName);

// 2. 调用无参构造函数动态创建实例
object itemInfo = Activator.CreateInstance(itemType);

// 3. 转成具体类型使用
ItemInfo item02 = (ItemInfo)itemInfo;

方式二:直接传程序集名 + 完整名称

// 返回值是个"包装对象",需要用 Unwrap() 打开
ObjectHandle objItem2 = Activator.CreateInstance("Zhaoxi.AdvancedCourse", fullName);
ItemInfo item2 = (ItemInfo)objItem2.Unwrap();

五、查看类型元数据

拿到 Type 对象后,就能读取它的属性、字段、方法、构造函数等信息。

5.1 获取类型对象的两种方式

// 方式一:typeof(已知类型)
Type itemType = typeof(ItemInfo);

// 方式二:Type.GetType("完整名称字符串")
Type itemType2 = Type.GetType("Zhaoxi.AdvancedCourse.Models.ItemInfo");

5.2 读写属性(PropertyInfo)

Type itemType = typeof(ItemInfo);

// 获取所有公有属性
PropertyInfo[] pros = itemType.GetProperties();
foreach (var pro in pros)
{
    Console.WriteLine(pro.Name);          // 输出 Id、Name、Remark
}

// 获取指定名称的属性
PropertyInfo proName = itemType.GetProperty("Name");

// 创建对象并赋值,然后读取属性值
ItemInfo itemInfo = new ItemInfo() { Id = 101, Name = "收入", Remark = "收入名目" };
string itemName = proName.GetValue(itemInfo).ToString();   // 读:得到 "收入"

// 修改属性值
proName.SetValue(itemInfo, "支出");                        // 写:Name 改成 "支出"

5.3 读写字段(FieldInfo)

Type itemType = typeof(ItemInfo);
ItemInfo item = new ItemInfo();

// 获取所有公有字段
FieldInfo[] fields = itemType.GetFields();

// 获取指定名称的公有字段
FieldInfo fType = itemType.GetField("itemType");
fType.SetValue(item, "通过工资发放");                       // 写
string typeStr = fType.GetValue(item).ToString();          // 读

// 获取私有字段,需要指定 BindingFlags
FieldInfo fPrivate = itemType.GetField("somePrivate",
    BindingFlags.NonPublic | BindingFlags.Instance);

要点:默认的 GetFields() / GetProperties() 只返回公有(public)成员。要拿私有成员,需加 BindingFlags.NonPublic 等标志。

5.4 调用方法(MethodInfo)

Type itemType = typeof(ItemInfo);
ItemInfo itemInfo = new ItemInfo() { Id = 1, Name = "收入", Remark = "工资" };

// 获取所有公共方法
MethodInfo[] methods = itemType.GetMethods();

// 获取无参方法 GetItemInfo,并调用
MethodInfo getInfo = itemType.GetMethod("GetItemInfo");
object reStr = getInfo.Invoke(itemInfo, null);              // 参数传 null

// 调用带一个参数的方法:先按参数类型找到对应重载
MethodInfo mCal = itemType.GetMethod("CalTotalAmount", new Type[] { typeof(decimal) });
decimal result = (decimal)mCal.Invoke(itemInfo, new object[] { 100m });

// 调用泛型方法:先获取再"补上"具体类型
MethodInfo createObj = typeof(GenericMethodClass).GetMethod("CreateObj");
MethodInfo createObjNew = createObj.MakeGenericMethod(typeof(IncomeInfo));
IncomeInfo income = (IncomeInfo)createObjNew.Invoke(null, null);

5.5 构造函数(ConstructorInfo)

Type incomeType = typeof(IncomeInfo);

// 获取所有公共构造函数
var constructors = incomeType.GetConstructors();

// 获取无参构造函数并创建实例
var cons01 = incomeType.GetConstructor(new Type[] { });
object obj1 = cons01.Invoke(null);

// 带一个 int 参数的构造函数
var cons02 = incomeType.GetConstructor(new Type[] { typeof(int) });
object obj2 = cons02.Invoke(new object[] { 1 });

// 带两个参数(int、decimal)的构造函数
var cons03 = incomeType.GetConstructor(new Type[] { typeof(int), typeof(decimal) });
object obj3 = cons03.Invoke(new object[] { 1, 2000m });

六、动态加载程序集(Assembly)

程序集(Assembly)就是编译出来的 .dll.exe 文件。加载了程序集,就能用反射访问里面的类型。

Assembly ass = null;

// 方式一:不写 dll 后缀(.NET Framework 可行,.NET 8 需要先引用项目)
ass = Assembly.Load("Zhaoxi.Models");

// 方式二:写完整绝对路径
ass = Assembly.LoadFile(@"D:\工作\基础班Vip05\Zhaoxi.AdvancedCourse\bin\Debug\Zhaoxi.Models.dll");

// 方式三:要写 dll 后缀名
ass = Assembly.LoadFrom("Zhaoxi.Models.dll");

三种方式区别:

  • Assembly.Load("名称"):按程序集名称加载(从已引用的程序集里找);
  • Assembly.LoadFile("完整路径"):按绝对路径加载;
  • Assembly.LoadFrom("路径或文件名"):按路径加载,会自动处理依赖。

七、与泛型结合封装通用类

反射 + 泛型是黄金搭档。泛型负责"通用类型",反射负责"在运行时读取类型信息",两者结合就能写出非常灵活的工具类。

下面这个 BaseAct<T>,能对任意类型 T 做两件通用的事:把对象属性拼成字符串、按 id 查找对象。

public class BaseAct<T>
{
    public List<T> list = new List<T>();

    public void Add(T t)
    {
        list.Add(t);
    }

    // 把对象的所有属性拼成一行字符串,如 "Id:1,Name:收入,Remark:工资"
    public string GetInfo(T t)
    {
        string str = "";
        Type type = typeof(T);
        var props = type.GetProperties();
        foreach (var pro in props)
        {
            if (str != "") str += ",";
            str += pro.Name + ":" + pro.GetValue(t).ToString();
        }
        str += "\r\n";
        return str;
    }

    // 按指定的 id 字段名查找对象
    public T FindModel(int id, string idName)
    {
        Type type = typeof(T);
        PropertyInfo pId = type.GetProperty(idName);
        if (pId != null)
        {
            foreach (T t in list)
            {
                if ((int)pId.GetValue(t) == id)
                    return t;
            }
        }
        return default;    // 找不到返回默认值(引用类型为 null)
    }
}

// 使用
BaseAct<IncomeInfo> incomeBase = new BaseAct<IncomeInfo>();
string reStr = incomeBase.GetInfo(income);
IncomeInfo incomeObj02 = incomeBase.FindModel(1, "Id");

这里的巧妙之处:GetInfo 里不知道 T 具体是什么,但通过 typeof(T).GetProperties() 反射出它的所有属性,就能统一处理,不管 TIncomeInfo 还是别的类型都能用。

八、反射的优缺点

优点:

  1. 提高程序的灵活性和扩展性;
  2. 降低耦合性,提高自适应能力;
  3. 允许程序创建和控制任意类的对象,无需提前硬编码目标类。

缺点:

  1. 性能问题:反射基本是"解释执行",访问字段、调用方法比直接代码慢很多。所以反射主要用在框架、ORM、依赖注入等对灵活性和扩展性要求高的系统上,普通业务代码不建议滥用;
  2. 可读性差:反射绕过了源代码,会让程序内部逻辑变模糊,维护起来比直接代码更复杂、更难理解。

九、一个完整可跑的综合示例

using System;
using System.Reflection;

class Program
{
    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public string SayHello()
        {
            return "你好,我是 " + Name;
        }
    }

    static void Main()
    {
        // 1. 通过反射动态创建对象
        Type stuType = typeof(Student);
        object stuObj = Activator.CreateInstance(stuType);

        // 2. 反射读写属性
        PropertyInfo propId = stuType.GetProperty("Id");
        PropertyInfo propName = stuType.GetProperty("Name");
        propId.SetValue(stuObj, 1);
        propName.SetValue(stuObj, "小明");

        // 3. 反射调用方法
        MethodInfo sayHello = stuType.GetMethod("SayHello");
        string msg = (string)sayHello.Invoke(stuObj, null);

        // 4. 转成具体类型,用常规方式验证
        Student stu = (Student)stuObj;
        Console.WriteLine(stu.Id);        // 输出 1
        Console.WriteLine(stu.Name);      // 输出 小明
        Console.WriteLine(msg);           // 输出 你好,我是 小明
    }
}

十、面试题

1. 什么是反射?有什么作用?

:反射是程序在运行时访问、检测和修改自身状态或行为的能力。通过反射可以读取类型的元数据(属性、方法、字段等),动态创建实例、调用方法、加载程序集、读写属性,以及访问特性。它主要用于框架、ORM、依赖注入、插件系统等需要高灵活性的场景。

2. 反射用到哪些核心类?分别做什么?

  • System.Type:类型信息类,获取构造函数、方法、字段、属性、事件等信息;
  • System.Reflection.Assembly:程序集类,加载程序集;
  • System.Activator:快速实例化对象的类,把 Type 对象实例化为具体对象。

3. 如何获取一个类型的 Type 对象?

:常用两种方式:

  • typeof(类名):编译时已知类型;
  • Type.GetType("命名空间.类名"):根据完整名称字符串获取,适合类型在编译时未知的场景。

此外还有 obj.GetType():从已有对象实例获取其类型。

4. 反射如何动态创建对象?

:用 Activator.CreateInstance(type) 调用无参构造函数创建;或用 type.GetConstructor(new Type[] { ... }) 拿到指定构造函数后,再 Invoke(参数数组) 创建。CreateInstance 返回 object,需要强转成具体类型使用。

5. GetFields() 和 GetProperties() 默认能拿到私有成员吗?怎么拿私有成员?

:默认只能拿到公有(public)成员。要拿私有成员,需加 BindingFlags 标志,例如:

FieldInfo f = type.GetField("字段名", BindingFlags.NonPublic | BindingFlags.Instance);

6. 反射调用方法是怎么做的?带参数怎么处理?

:先用 type.GetMethod("方法名") 拿到 MethodInfo,再调用 Invoke(对象实例, 参数数组)

  • 无参方法:Invoke(obj, null)
  • 有参方法:先按参数类型数组定位重载 GetMethod("方法名", new Type[]{ typeof(int) }),再 Invoke(obj, new object[]{ 参数 })

7. 反射调用泛型方法怎么处理?

:泛型方法需要先"填充"具体类型。先 GetMethod("方法名") 拿到 MethodInfo,再用 MakeGenericMethod(具体类型) 得到确定类型的 MethodInfo,最后 Invoke 调用。

MethodInfo m = typeof(X).GetMethod("CreateObj");
MethodInfo m2 = m.MakeGenericMethod(typeof(IncomeInfo));
IncomeInfo obj = (IncomeInfo)m2.Invoke(null, null);

8. 反射有哪些缺点?

  • 性能差:反射是解释执行,访问字段、调用方法远慢于直接代码;
  • 可读性差:绕过源代码,逻辑模糊,维护困难、代码更复杂。

因此普通业务代码不建议滥用反射,主要用在框架、ORM 等对灵活性要求高的场景。

9. 反射和泛型结合有什么好处?举一个例子。

:泛型负责"类型通用化",反射负责"运行时读取类型信息",结合后可写出高度通用的工具类。例如 BaseAct<T>:用 typeof(T).GetProperties() 反射出 T 的所有属性,于是 GetInfo 能对任意类型把属性拼成字符串、FindModel 能按任意 id 字段查找对象,而不用为每种类型单独写一套。

10. 什么是元数据?反射和元数据什么关系?

:元数据是程序集里记录类型成员信息的数据(构造函数、方法、字段、属性、事件等)。反射就是在运行时读取元数据的机制,从而了解类型的结构并动态操作它。

11. Assembly.Load、LoadFile、LoadFrom 有什么区别?

  • Assembly.Load("名称"):按程序集名称加载,从已引用的程序集里查找;
  • Assembly.LoadFile("绝对路径"):按完整路径加载;
  • Assembly.LoadFrom("路径或文件名"):按路径加载,会自动处理相关依赖。

12. 反射性能差,怎么优化或规避?

  • 能不用反射就不用,普通逻辑直接写代码;
  • 缓存反射结果(如 PropertyInfoMethodInfoConstructorInfo),避免重复反射查找;
  • 对高频调用场景,可用表达式树(Expression)编译成委托,或用 Emit 生成动态方法,把反射的"解释执行"变成"直接调用"。
posted @ 2026-08-15 16:22  春煦景明  阅读(7)  评论(0)    收藏  举报