概述
在游戏开发中(其实别的领域也一样啦),为了保证代码框架的灵活,低耦合,拓展性强,许多时候需要动态地调用函数。在C++
里面,一种特殊的指针,函数指针,承担了传递函数的功能。Javascript
里也有高级函数这么一说来将函数也作为参数使用。C#
作为一门拥有许多特性的现代编程语言来说,提供了多种解决方法。
比较
以下内容不会特别具体介绍各个方法该如何使用,而是从实现机制这一角度来分析运行效率等方面(如果有错,请指出😂)。
使用Delegate机制
Delegate
,直接翻译就是委托,本质上是像C++
一样传递一个函数指针,通过delegate
关键字声明,同时也要标注出返回值类型和参数类型。它的本质其实是一个包含指向特定函数的对象而不是一个函数的引用,正是因为这一特点,我们才能将他作为参数传递,由此也可以说吗,C#
仍然是一门强类型,静态编译的语言。然而由于创建一个Delegate
需要声明的内容过多,C#
的System
库里又提供了两种方便的简化创建方式,即Action
委托类和Func
委托类。这两个东西仍然是委托,也就是说是对象,只不过各自省略了一些条件。Action
可以为所有没有返回值类型的函数提供委托,通过范型参数对委托的输入参数类型进行限制。而Func
委托则将返回值类型也使用泛型参数确定。由于这两个简化的委托都需要使用泛型参数来约束条件,所以只能通过Overload来提供不同参数长度的委托,并且System
库里最多只给输入参数重载到了16个(Func
还多一个给返回值参数用)。
由于其函数指针的本质,Invoke委托的方法并没有很大的开销,等同于call了一个虚函数,是call一个非虚函数开销的1.5~2倍。虚函数就是指那些被virtual
修饰的函数,包括实现的接口里的所有函数和所有override
。增加的开销是因为运行的时候程序需要查询一张函数表来找到该函数的位置,并不显著(绝对值太小)。一般来说,Delegate
和Event
系统配合起来是大多数人采取的解决方案。
然而委托也有缺点,委托对象需要在编译前就进行赋值(如Action action = Method;
),实际上在编译的时候编译器对代码进行了再翻译,可以认为有一层很浅的语法糖。所以委托虽然可以动态调用函数,但是不能动态赋值。其次,如果使用匿名函数对委托进行赋值,GC的处理会比较难以预料。虽然方表示当委托对象赋值null
的时候,系统会自动销毁匿名函数,不过据说不太靠谱。
使用Reflection机制
Reflection
,也就是使用C#
的反射来调用函数。一般是通过对象的GetType().GetMethod(xxx)
之类的手段调用,获得一个MethodInfo
的对象,包括了函数名,参数表等等对象。反射本质是加载相关的程序集,然后查询相关的类的元数据(Field
域,Method
方法等等),在再到对应实例化对象如果是非静态方法的话。从我的描述就可以看出,这是一个相当复杂的过程,尤其牵扯到多个表的查询和字符串比对,性能非常低下,同时由于存储着更多的信息(比起委托的函数指针而言),内存占用也高。
不过反射真的就没有啥好处吗?作为一名IB学生,既然学了ToK,我们就要会使用批判性思维,从事物的两面来考虑问题。由于反射查询了指点的类的所有描述属性,我们可以真正做到动态指定函数(使用string传递函数名)。除此之外,反射还可以获取并调用私有API
。想想看,当你用了某个把重要底层逻辑私有掉的dll库的时候,使用反射就可以轻而易举获取到这些内容,同样有些库的新特性可能有于不稳定而用private暂时禁止开发者使用,用Reflection
就可以提前看到这些东西啦,是不是非常一颗赛艇。
那么反射的效率该怎么提高呢?一种是使用il.Emit()
函数动态生成相关代码,这样再次调用这段代码,就可以或得近似原生编译出来的效果了。做法就不详细介绍了,因为这不是我个人推荐的一种。通过这种方法优化,性能大概比编译代码慢20倍的样子,并不是最理想的。那么,我们该如何进一步解决这个问题呢?答案是用Delegate.CreateDelegate()
这个静态函数。这个函数允许你输入一个MethodInfo
对象来创建一个委托类型对象。如果你再传入函数所属的类型实例,就可以获得一个非静态的方法的委托,这样性能就提升到了委托的层次,同时也可以进行动态赋值。
使用UnityAction
UnityAction
是Unity3D自带库的一个类,是其对Action
的一种实现,大致上和System
的Action
并无区别,主要是为了能在Inspector
面板上编辑而做的序列化处理,这样当有一个public
的UnityEvent
对象时,就可以在面板上添加删除UnityAction
了。不过经过测试之后UnityAction
的效率不得不让人吐槽,差不多比委托机制要慢4~5倍、、、明明都是基于委托的呀,非常神奇。Unity
目前的NGUI
,UGUI
里的EventSystem
都是基于这个机制实现的,可以从侧面说明为什么Unity
的原生UI库效率不高了(笑)。Anyway,我自己都是使用System.Action
来解决问题的,那么如何在inspector
上显示就成了一个问题,我最后通过反射获取给定脚本的所以方法实现了这一个,具体内容后面再解释吧。
总结
程序编写的灵活性与性能始终是互逆的,在这种情况下我们只能根据需要进行取舍。比如游戏运行时对性能要求高,那么就必须得用Delegate
,而在编辑器显示的时候,为了动态获取未知脚本的所有符合条件的函数,就不得不要反射机制,这个时候对性能要求就没那么高了。为了连接这两种不一样的需求,我们又使用Delegate.CreateDelegate()
方法进行转换,总而言之,不能一味地需一种用法就什么情况下都用,灵活地变化是必不可少的