前言
在我平时做项目的时候,由于我们做的项目都是很简单的,所以不怎么接触反射机制。最早了解反射机制是关于Invoke的时候,知道可以通过方法名来直接进行Invoke调用,但是由于反射调用存在性能开销较大的问题,因此就没打算深入了解
不过反射作为C#的高级特性,可以不用,但是不能不了解
反射
反射是.NET中的重要机制,通过反射可以得到*.exe或*.dll等程序集内部的接口、类、方法、字段、属性、特性等信息,还可以动态创建出类型实例并执行其中的方法。
反射指程序可以访问、检测和修改它本身状态或行为的一种能力。
程序集包含模块,而模块包含类型,类型又包含成员。反射则提供了封装程序集、模块和类型的对象。
可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性。
简单来说,反射能实现的功能极其强大,可以直接通过读取exe或者dll程序集获取其中的接口、类、方法、字段、属性、特性等信息。
通过反射获取类型
反射获取类型的方式有三种:
- 通过typeof获取某个值的类型
Type personType=typeof(Person);
2.通过一个对象获取该对象所对应的类的类型
Type=Person.GetType();
3.通过类的名称字符串获取对应的类型
Type strType =Type.GetType("Person");
注意,上述说的三种方法不止包括获取class,只需要换成对应的方法就能获取接口、方法、字段、属性、特性等等信息。这意味这只要使用反射就可以获取代码中的几乎任何信息。甚至私有的变量成员和方法都能获取
只需查看上文就可以知道反射的功能有多全面,返回所需的类型的信息,根据访问修饰符获取类型成员信息,通过反射直接构造实例化对象,通过反射获取类中的所有属性,字段,事件,方法,构造函数等等。私有的都可以随便访问。
优点:
- 反射提高了程序的灵活性和扩展性。
- 降低耦合性,提高自适应能力。
- 它允许程序创建和控制任何类的对象,无需提前硬编码目标类。
缺点:
- 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。
- 使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。
实际上反射的优点也是它的缺点。为什么我们不用反射来解耦,不用反射做拓展呢?除了反射本身调用时需要查找解释造成过高的性能开销之外,反射本身绕过了程序内部逻辑,可读性太差了。如果我们要使用反射调用函数,不还是需要知道函数方法实现了什么吗?而解耦这一目的完全可以从设计模式上来解决。
Unity中的反射
反射在Unity中实现的功能主要是:
- 使用反射,我们可以动态的访问代码中的成员,或是进行动态实例化。例如我们想要实现游戏中的控制台Debug功能,让用户可以使用简单的指令就能创建一些游戏实例,例如用户可以用指令
add ObjName 100
来为场景中增加100个对应名称的游戏物体实例,我们就可以用反射机制,获取ObjName
字符串对应的Type并生成物体:
Type type = typeof(ObjName);
object instance = Activator.CreateInstance(type);
- 另一种想法是,使用反射,我们可以实现一些热更新的功能。例如对于若要生成一个物体,我们可以把它封装在dll程序集中,并通过反射机制,用物体的名称来直接实例化dll中的该物体。而如果此时客户端要实现不停机热更新该物体的数值,只需下载替换dll文件即可,因为物体名称并没变,我们通过反射机制获取直接获取更新的成员并更新数值。
合理使用反射机制,可以简单的实现一些麻烦的功能,而且将程序集之间进行分离,也有助于减少程序的耦合性。
用反射在Unity中动态加载
想要在unity中创建并加载程序集,我们需要在文件夹内生成一个Assembly Definition
我们会发现创建了一个拼图icon的文件,这个文件就是我们的程序集,但它目前是未编译的状态,格式是asmdef
,只有在被导出后才会被编译为dll
官方文档——程序集定义
Unity程序集定义(Assembly Definition File)功能详解
我们在与它同目录下所创建的脚本都会被编译到这个程序集中
在面板中可以查看它的属性,首先程序集的名称是在面板上的Name定义的,而不是该文件本身的名称
这里显示了三个选项(高版本还有其他选项),AutoReferenced代表了该程序集会自动引用其他程序集,导致其他程序集更新后该程序集也被自动重编译,如果我们不希望这个程序集在其他程序集更新后被重编译,就关闭它
override References代表了我们指定该程序集会引用哪些程序集,并在Assembly Definition References里选择添加对应的Dll
最下面的面板Platforms约定在导出到哪些平台时该程序集会被编译
Define Constraints代表了该程序集会在哪些宏被定义的时候被编译,只有当代码中使用了指定宏时才会使用该程序集。例如我下面的代码:
using System.IO;
using System.Reflection;
using UnityEngine;
public class TestClass : MonoBehaviour
{
private string _localPath;
private void Start()
{
#if UNITY_EDITOR
// 我不知道如何在项目中直接加载未编译的程序集,只能导出后加载了
_localPath = Path.Combine(Path.GetDirectoryName(Application.dataPath),"Apps");
string[] DataFloder = Directory.GetDirectories(_localPath, "*_Data");
_localPath = Path.Combine(DataFloder[0], "Managed", "Test.dll");
#else
LocalPath = Path.Combine(Application.dataPath, "Managed", "Test.dll");
#endif
// 可笑的是程序集只能加载不能卸载,导致程序关闭后程序集依然被访问
Assembly _assembly = Assembly.LoadFrom(_localPath);
var t = _assembly.GetType("TestReflect");
gameObject.AddComponent(t);
}
}
public class TestReflect : MonoBehaviour
{
void Start()
{
Debug.Log("反射成功调用");
}
}
由于我定义了!UNITY_EDITOR
,也就是非编辑器中被编译,经测试,导出时会正常编译dll,然后在编辑器状态,代码是正常执行的。
但是如果定义的是UNITY_EDITOR
,则导出时不会编译为Dll
,猜想是由于导出时的bulidPipeline使用了!UNITY_EDITOR
宏,因此若定义了!UNITY_EDITOR
的引用约束,则导出时会编译。
当然我们还可以定义其他的编译引用约束,根据具体使用情况来判断
导出后的Dll
路径在GameScence_Data\Managed\
路径下
执行结果:
导出后的结果也是一样的