Post

委托和回调的区别

转自某评论区解答

简单讲一下委托和回调,以及C#的泛型委托

回调和委托是两个概念。

委托是C#里对函数的类型的描述。是对函数的一种抽象。

举个例子:

声明一个int a,我们可以一直改变他的值,但他的类型一直是 int。

类比一下,我们声明一个函数 void func1(){},但是这个函数就不能变了,在编译器眼里,func1()就代表执行该函数,func1就代表该函数。这很不公平。

而委托就是函数的类型声明,我们声明一个委托 delegate void SayHello(string msg),这是一类函数的类型,表示没返回值,接受一个string参数的函数。当我们用 SayHello myhello; 声明了一个变量后,这个变量存的不是值,不是对象,而是一个函数,一个无返回值,只有一个string参数的函数。

(C语言中,要实现类似的函数类型,需要使用 typedef + 指针类型,因此很多人认为C语言是面向过程,其实是不准确的。C语言是语义完备的,他的结构体,函数指针,各种宏指令,完全可以作为“面向对象”语言来使用,只是不够方便直观而已)

通常初学者很容易认为对于编程语言,变量构成语句,语句构成函数,变量、函数构成类,有这样的层次关系。

回调是一种设计模式或者说叫设计思想。也是“控制反转” 的一种方式。

举个例子:

我们有一个函数 A,每次执行完成,会打印出日志在控制台,因此在函数A的Return前面,都会有一句 Console.WriteLine 来打印。

但如果有一天需求变了,要求把日志输出为文本文件,那我们这时候就要修改函数A,把最后一句改成输出到文件。

为了方便的执行这种:在完成后执行指定操作 的模式,我们把最后输出日志的方法作为参数onFinishedCallBack 传递给A,这样在函数A末尾只需要调用 onFinishedCallBack() 就行。

那么控制反转又是什么意思?上面这种反转了个啥?

你会发现,当需求改变的时候,我们从原来的修改函数A 变成 修改参数 也就是变成调用方的事儿了。这就是 反转 的内涵。

那么泛型委托又是啥?

我们回到上面的例子。当我们给函数传递参数的时候,是需要传指定类型的参数的。比如两数相加的 Add(int,int) 函数,必须传递两个int类型。但是int相加和float相加或者其他“数类型”相加都是同样的函数语句,那为啥要重复定义这么多Add?

这就是泛型的意义。我们定义 Add<T>(T t1, T t2) 这样的函数就可以了。这样当我们加两个float,就调用的时候指定就好。

那同理,函数也是有类型的,编译器通过函数返回值和参数列表来区分不同的函数类型。

因此对于上上上面的需要输出日志回调,不管输出到控制台还是文本文件,都需要“日志内容”,因此我们合理的认为这个日志输出回调应该接受一个string参数,返回void。因此我们声明一种 “日志输出回调”

1
delegate void LoggingCallBack(string log)

这样函数A的声明变成:

1
void A(int a, int b, LoggingCallBack someCallBack)

每次声明这样的 委托 实在太麻烦了。回调函数没有返回值,只是参数数量不同,我难道不可以用泛型T来表示参数类型吗:

1
2
3
delegate void CallBack<T>(T t1)

delegate void CallBack<T1, T2>(T1 t1, T2 t2)

很好,微软已经贴心的为你考虑到这一层,不过他们把这种没有返回值的委托称之为 Action

而且,在系统库中为你定义了高达16个参数的委托泛型:

1
Action<T1,T2,T3,...T16>(...)

这样你函数A也不用先声明委托了,直接

1
void A(int a, int b, Action<string> someCallBack) 

同理,对于有返回值的委托,微软同样准备了高达16个参数外加一个返回值的泛型委托供你使用.

1
Func<T1,T2,...T16,TResult>(...)

最后说一点,函数返回值和参数是一样的。有兴趣的可以了解一下泛型中的逆变和协变。相信你会更加头疼。

This post is licensed under CC BY 4.0 by the author.