C# 基础
c# 委托基础
概述
在开发过程中根据代码框架,动态的调用函数就是一种必要需求,C++ 的函数指针承担了传递函数的功能,在C#中也提供了多种解决方案.
Lambda表达式
(input-parameters) => { < sequence-of-statements > }
使用Lambda声明运算符=>,从其主体中分离Lambda参数列表。若要创建Lambda表达式,需要在Lambda运算符左侧指定输入参数(如果有参数时),然后在另一侧输入表达式或语句块。
任何Lambda表达式都可以转换为委托类型,可以转换的委托类型由参数和返回值的类型定义。如果Lambda表达式不返回值,则可以将其转换为Action委托类型之一;否则,可将其转换为Func委托类型之一。例如,有两个参数且不返回值的Lambda表达式可转化为Action<T1, T2>委托。有一个参数且不返回值的Lambda表达式可转换为Func<T, TResult>委托。
例如:Lambda表达式x => x * x,指定名为x的参数,并返回x的平方值,并将表达式x => x * x分配给委托类型的变量。
一、Delegate
定义:
直接翻译就是委托,本质上是像C++一样传递一个函数指针,通过delegate关键字声明,同时也要标注出返回值类型和参数类型。它的本质其实是一个包含指向特定函数的对象而不是一个函数的引用,正是因为这一特点,我们才能将他作为参数传递.
Func和Action其实就是内置好的delegate
一般而言Delegate和Event系统配合起来是大多数人采取的解决方案.
性能:
由于起函数指针的本质,Invoke委托的方法并没有很大的开销,等同于Call了一个虚函数,是call一个非虚函数开销的1.5~2倍. 虚函数就是指那些被virtual修饰的函数,包括实现接口里的所有函数和所有override. 增加的开销是因为运行的时候进程需要查询一张函数表来找到该函数的位置,并不显著(绝对值太小).
缺点:
委托对象需要再编译钱就进行赋值(Action act = Method;),实际上在编译器对代码进行了再翻译,可以认为有一层很浅的语法糖. 所以委托虽然可以动态调用函数,但是不能动态赋值.
如果使用匿名函数对委托进行赋值,GC的处理会比较难以预料.虽然官方表示委托对象赋值null的时候,系统会自动调回匿名函数,不过据说不靠谱.
代码如下(示例):
public delegate void Add_DEL (int iVal); /// 单参无返回值委托
private void DelegateBase()
{
Add_DEL aStdAdd_DEL = Add_void /// 标准委托
Add_DEL aLambdaAdd_DEl = delegate(int iVal) /// 匿名委托
{
Console.WriteLine("\n1. Lambda delegate: " + iVal.ToString());
};
aStdAdd_DEL?.Invoke(100);
aLambdaAdd_DEl?.Invoke(200);
}
public void Add_void (int iVal) /// 单参方法
{
Console.WriteLine("\n1.单参方法, 返回值(void): ");
}
二、Action:
Action仍然是委托,也就是说是对象,只不过各自省略了一些条件。
Action可以为所有没有返回值类型的函数提供委托,通过范型参数对委托的输入参数类型进行限制,
Action委托具有Action、Action<T1,T2>、Action<T1,T2,T3>……Action<T1,……T16>多达16个的重载,其中传入参数均采用泛型中的类型参数T,涵盖了几乎所有可能存在的无返回值的委托类型
代码如下(示例):
public delegate void Add_DEL (int iVal); /// 单参无返回值委托
private void DelegateBase()
{
Action aAction_Method = Add_void; /// 无参匿名委托(Void) - 方法表达式
Action aAction_Std = delegate () { Console.WriteLine("1.无参匿名委托-传统表达式: 无返回值:"); }; /// 无参匿名委托(Void) - 传统表达式
Action aAction_Lambda = () => Console.WriteLine("1.无参匿名委托-Lambda表达式: 无返回值:"); /// 无参匿名委托(Void) - Lambda表达式
aAction_Method?.Invoke();
aAction_Std?.Invoke();
aAction_Lambda?.Invoke();
}
public void Add_void (int iVal) /// 单参方法
{
Console.WriteLine("\n1.Action方法, 无返回值: ");
}
三、Func
Action 与 Func是.NET类库中增加的内置委托,以便更加简洁方便的使用委托。
Func 委托则将返回值类型也使用泛型参数确定。
用法:Func< Tresult>、Func<T,Tresult>、Func<T1,T2,T3……,Tresult>17种类型重载,T1……T16为出入参数,Tresult为返回类型
1.引入库
代码如下(示例):
public delegate int Add_DEL (int iVal); /// 单参<Int>
private void FuncTest_Click()
{
Func<int, int> aFunc0 = FuncTest; /// 实例化一个委托
Func<int, int> aFunc1 = new Func<int,int>(FuncTest); /// 实例化一个委托
Func<int, int> aFunc2 = x => x += 1; /// 实例化一个委托 - Lambda表达式
aFunc0?.Invoke(200); /// 调用委托
aFunc1?.Invoke(200); /// 调用委托
var aValue = aFunc2(300);
}
private int FuncTest(int iA)
{
return iA;
}
四、Reflection
定义:
Reflection,就是使用C#的反射来调用函数.一般是通过对象的GetType().GetMethod(xxx)之类的手段调用,获得一个MethodInfo的对象,包括了函数名,参数表等等对象.
优点:
我们可以真正做到动态指定函数(使用string传递函数名)。除此之外,反射还可以获取并调用私有API。想想看,当你用了某个把重要底层逻辑私有掉的dll库的时候,使用反射就可以轻而易举获取到这些内容,同样有些库的新特性可能有于不稳定而用private暂时禁止开发者使用,用Reflection就可以提前看到这些东西啦.
性能:
反射的本质是加载相关的进程集,然后查询相关的元数据(Field域,Method方法等等),再到对应实例化对象,如果是非静态方法的话.从描述中就堪忧看出,这是一个相当负责的过程,尤其牵扯到多个表的查询和字符串比对,性能低下,同时由于存储更多的信息(对比委托的函数指针),内存占用也很高.
优化:
Delegate.CreateDelegate()这个静态函数。这个函数允许你输入一个MethodInfo对象来创建一个委托类型对象。如果你再传入函数所属的类型实例,就可以获得一个非静态的方法的委托,这样性能就提升到了委托的层次,同时也可以进行动态赋值。
- List item
代码如下(示例):
该处使用的url网络请求的数据。
五、UnityAction
UnityAction是Unity3D自带库的一个类, 是其对Action的一种实现, 大致上和System的Action并无区别,主要是为了能再Inspector面板上编辑而做的序列化处理,这样当有一个public的UnityEvent对象时,就可以在面板上添加删除UnityAction了.
不过测试之后的UnityAction的效率要比委托机制慢4~5倍.
Unity目标的NGUI,UGUI里的EventSystem都是基于这个机制实现的,如果应用System.Action就需要考虑在Inspector上显示的问题.<通过反射获取给定脚本,可以实现>
总结
进程编写的灵活性与性能始终是互逆的,在这种情况下我们只能根据需要进写取舍.
比如游戏运行时对性能要求搞, 那么久必须用Delegate, 而在编辑器显示的时候, 未来动态获取未知脚本的所有符合条件的函数,就不得不要反射机制,这个时候对性能要求那么高了.为了连接这两种不一样的需求,我们又使用Delegate.CrateDelegate()方法进行转换,总而言之,不能一味的使用一种用法,根据情况保持灵活开发的编辑思维.
Delegate至少0个参数,至多32个参数,可以无返回值,也可以指定返回值类型
Func可以接受0个至16个传入参数,必须具有返回值
Action可以接受0个至16个传入参数,无返回值
下列规则适用于Lambda表达式中的变量范围:
捕获的变量将不会作为垃圾回收,直至引用变量的委托符合垃圾回收的条件;
在封闭方法中看不到Lambda表达式内引入的变量;
Lambda表达式无法从封闭方法中直接捕获in、ref或out参数;
Lambda表达式中的return语句不会导致封闭方法返回;
如果相应跳转语句的目标位于Lambda表达式块之外,Lambda表达式不得包含goto、break或continue语句。同样,如果目标在块内部,在Lambda表达式块外部使用跳转语句也是错误的。