【一文了解】C#反射
目录
程序集
反射
1.反射的定义与作用
1.1.反射的定义
1.2.反射的作用
2.反射的核心类
3.反射的基本用法
3.1.获取Type对象
3.2.动态创建对象(通过构造函数)
3.3.动态访问字段和属性
3.4.动态调用方法
3.5.加载外部程序集并反射
4.反射的高级特性
4.1.BindingFlags控制成员查找范围
1)BindingFlags的作用
2)BindingFlags最常用的枚举值
3)示例
4.2.泛型类型反射
4.3.特性(Attribute)反射
5.反射的优缺点
5.1.优点
5.2.缺点
6.常见应用场景
7.总结
本篇文章分享一下C#反射。
程序集
在学习反射前先了解一下程序集,程序集(Assembly)是.NET中代码的基本部署单元,一个程序集对应一个.dll或.exe文件,包含了类型、资源等元数据。
在Unity中,Assembly-CSharp是存放游戏运行时脚本的程序集,所有在Assets目录下(非Editor文件夹)的C#脚本都会被编译到这个程序集中;Asembly-CSharp-Editor是Unity中存放编辑器扩展脚本的程序集,所有位于Assets目录下任意层级的Editor文件夹内的脚本,都会被编译到这个程序集中。
反射
1.反射的定义与作用
1.1.反射的定义
在C#中,反射(Reflection)是指程序在运行时动态获取类型信息(如类、方法、属性等)并操作其成员(如调用方法、访问属性)的能力。简单来说,反射允许程序“观察”和“修改”自身的结构,无需在编译时知道具体类型的细节。使用System.Reflection命名空间。
1.2.反射的作用
1)动态获取类型信息:运行时查看类的名称、继承关系、方法、属性、字段等元数据。
2)动态创建对象:无需在代码中显式声明类型,即可创建该类型的实例。
3)动态调用成员:调用类的方法、访问或修改属性/字段,即使编译时不知道这些成员的存在。
2.反射的核心类
| 类名 | 作用 | 常用方法/属性 |
| Type | 表示类型的元数据(反射的核心入口) | GetMethod()、GetProperty()、GetField() |
| Assembly | 表示程序集(.dll 或 .exe) | LoadFrom()(加载程序集)、GetTypes()(获取所有类型) |
| MethodInfo | 表示方法的元数据 | Invoke()(调用方法) |
| PropertyInfo | 表示属性的元数据 | GetValue()、SetValue()(读写属性) |
| FieldInfo | 表示字段的元数据 | GetValue()、SetValue()(读写字段) |
| ConstructorInfo | 表示构造函数的元数据 | Invoke()(创建对象) |
3.反射的基本用法
3.1.获取Type对象
Type是反射的核心,任何类型(类、结构体、枚举等)都可以通过Type实例获取其元数据。获取Type的3种方式:
1)方式1:通过typeof(类型)获取
2)方式2:通过对象实例的GetType()方法获取
3)方式3:通过Type.GetType(string typeName)获取,动态加载未知类型,其中字符串typeName为“类型全名,程序集名称”
●类型全名:包含命名空间,嵌套类型用+连接(如 Namespace.OuterClass+InnerClass)。
●程序集名称:通常是编译后的.dll或.exe文件名(不含扩展名),而非文件路径。
using System;
using System.Reflection;
using UnityEngine;namespace ReflectionTest
{public class ReflectionTest : MonoBehaviour{public class Person{public string Name { get; set; }public int age;public Person(string name, int age){Name = name;this.age = age;}public void SayHello(){Debug.Log($"Hello, I'm {Name}, {age} years old.");}}private void Start(){//方式1:通过 typeof(类型) 获取Type type1 = typeof(Person);Debug.Log(type1);//方式2:通过对象实例的 GetType() 方法获取Person person = new Person("Alice", 30);Type type2 = person.GetType();Debug.Log(type2);//方式3:通过 Type.GetType(string typeName) 获取,动态加载未知类型,其中字符串 typeName 为(类型全名,程序集名称)Type type3 = Type.GetType("ReflectionTest.ReflectionTest+Person,Assembly-CSharp");Debug.Log(type3);}}
}
若目标类型与调用代码在同一程序集中,可省略程序集名称(仅用类型全名),此时 Type.GetType 会在当前程序集中查找。
Type type3 = Type.GetType("ReflectionTest.ReflectionTest+Person");
若类型在外部程序集(未提前加载),需先加载程序集,再获取类型。
//1.加载外部程序集(指定路径)
Assembly assembly = Assembly.LoadFrom(@"C:\Path\MyAssembly.dll");//2.通过类型全名从加载的程序集中获取类型
Type personType = assembly.GetType("ReflectionTest.ReflectionTest+Person");
3.2.动态创建对象(通过构造函数)
使用ConstructorInfo动态调用构造函数创建实例:
//获取 Person 的构造函数(参数为 string 和 int)
ConstructorInfo ctor = type1.GetConstructor(new Type[] { typeof(string), typeof(int) });//调用构造函数创建实例(参数对应构造函数的参数)
object personInstance = ctor.Invoke(new object[] { "Bob", 25 });//等价于 new Person("Bob", 25)
3.3.动态访问字段和属性
使用FieldInfo和PropertyInfo读写字段和属性:
//访问公共字段 Age
FieldInfo ageField = type1.GetField("age");
int ageValue = (int)ageField.GetValue(personInstance);//获取值:25
Debug.Log($"Age:{ageValue}");
ageField.SetValue(personInstance, 26);//修改值:26
Debug.Log($"Age:{(int)ageField.GetValue(personInstance)}");//访问公共属性 Name
PropertyInfo nameProp = type1.GetProperty("Name");
string nameValue = (string)nameProp.GetValue(personInstance);//获取值:"Bob"
Debug.Log($"Name:{nameValue}");
nameProp.SetValue(personInstance, "Bobby");//修改值:"Bobby"
Debug.Log($"Name:{(string)nameProp.GetValue(personInstance)}");
3.4.动态调用方法
使用MethodInfo调用方法(包括公共、私有方法):
//调用公共方法 SayHello()
MethodInfo sayHelloMethod = type1.GetMethod("SayHello");
sayHelloMethod.Invoke(personInstance, null);//输出:Hello, I'm Bobby, 26 years old.//调用私有方法 GetSecret()(需指定 BindingFlags)
MethodInfo getSecretMethod = type1.GetMethod("GetSecret", BindingFlags.Instance | BindingFlags.NonPublic);
string secret = (string)getSecretMethod.Invoke(personInstance, null);//获取私有方法返回值:"This is a secret."
Debug.Log($"GetSecret():{secret}");
结果:

3.5.加载外部程序集并反射
反射可加载外部.dll程序集,访问其中的类型(常用于插件系统):
//加载外部程序集(如 MyTest.dll)
Assembly assembly = Assembly.LoadFrom("MyTest.dll");//获取程序集中的所有类型
Type[] types = assembly.GetTypes();//遍历类型并使用
foreach (Type type in types)
{Debug.Log("类型名称:" + type.Name);//动态创建实例、调用方法等...
}
4.反射的高级特性
4.1.BindingFlags控制成员查找范围
1)BindingFlags的作用
反射时,若不指定BindingFlags,默认只会查找公共实例成员(非静态、非私有)。通过 BindingFlags,可以:
●筛选成员的访问级别(公共、私有、保护、内部);
●筛选成员的作用域(实例成员、静态成员);
●控制查找范围(当前类型、继承链中的基类);
●控制匹配规则(是否忽略大小写等)。
2)BindingFlags最常用的枚举值
BindingFlags是可组合的(通过位或|操作组合多个枚举值),以下是最常用的枚举值:
| 枚举值 | 说明 | 示例 |
| Public | 包含公共成员(默认包含) | 查找类的公共方法GetPublicMethod()。 |
| NonPublic | 包含非公共成员(私有、保护、内部) | 查找类的私有方法GetPrivateMethod()。 |
| Instance | 包含实例成员(非静态) | 查找类的实例方法(需通过对象实例调用)。 |
| Static | 包含静态成员 | 查找类的静态方法static void StaticMethod()。 |
| DeclaredOnly | 仅查找当前类型声明的成员(不包含基类继承的成员) | 仅查找DerivedClass自身声明的方法,不包含BaseClass中的方法。 |
| FlattenHierarchy | 查找继承链中的公共静态成员(与DeclaredOnly互斥) | 查找DerivedClass及所有基类中的公共静态方法。 |
| IgnoreCase | 查找时忽略成员名称的大小写 | 同时匹配MyMethod和mymethod。 |
3)示例
如获取私有、静态成员:
//获取私有字段(需指定 Instance + NonPublic)
FieldInfo privateField = type1.GetField("privateFieldName", BindingFlags.Instance | BindingFlags.NonPublic);//获取静态方法(需指定 Static + Public)
MethodInfo staticMethod = type1.GetMethod("StaticMethod", BindingFlags.Static | BindingFlags.Public);
4.2.泛型类型反射
处理泛型类或方法时,需通过MakeGenericType或MakeGenericMethod实例化:
//定义泛型类
public class MyGeneric<T>
{ public T Value { get; set; }
}//获取泛型类型并实例化(如 MyGeneric<int>)
Type genericType = typeof(MyGeneric<>);
Type intGenericType = genericType.MakeGenericType(typeof(int));//实例化为 MyGeneric<int>
object genericInstance = Activator.CreateInstance(intGenericType);//创建实例
4.3.特性(Attribute)反射
反射可读取类型或成员上的自定义特性:
//定义自定义特性
[AttributeUsage(AttributeTargets.Class)]
public class MyAttribute : Attribute
{ public string Description { get; } public MyAttribute(string desc) { Description = desc; }
}
//应用特性
[MyAttribute("这是一个测试类")]
public class TestClass { }//反射读取特性
Type testType = typeof(TestClass);
MyAttribute attr = (MyAttribute)Attribute.GetCustomAttribute(testType, typeof(MyAttribute));
Debug.Log(attr.Description);//输出:这是一个测试类
5.反射的优缺点
5.1.优点
1)灵活性高:可动态操作未知类型,适合插件系统、序列化(如JSON序列化)、依赖注入等场景。
2)元数据访问:能获取类型的详细信息(如注释、特性),用于文档生成、代码分析工具。
5.2.缺点
1)性能开销:反射操作比直接调用慢(约慢10-100倍),频繁调用可能影响性能。
2)安全性:反射可访问私有成员,破坏封装性;在部分受限环境(如沙箱)中可能被禁用。
3)代码可读性差:动态调用逻辑不如直接调用直观,调试难度较高。
6.常见应用场景
1)序列化/反序列化:如Newtonsoft.Json或System.Text.Json,通过反射遍历对象成员并转换为JSON。
2)依赖注入(DI)容器:如Autofac、Microsoft.Extensions.DependencyInjection,通过反射创建服务实例并注入依赖。
3)ORM 框架:如Entity Framework,通过反射将数据库表字段映射到类的属性。
4)插件系统:动态加载外部插件(.dll),反射调用插件中的方法。
5)单元测试框架:如xUnit、NUnit,通过反射发现并执行标记了Fact或Test的测试方法。
7.总结
反射是C#中极强大的特性,允许程序在运行时“自省”和动态操作类型。尽管存在性能和封装性的权衡,但在灵活性优先的场景(如框架开发、动态功能)中不可或缺。实际使用时,应尽量减少频繁反射调用,必要时可通过缓存Type或MethodInfo提升性能。
好了,本次的分享到这里就结束啦,希望对你有所帮助~
