C#编程语言之委托与事件(一)—— C/C++函数指针和C#委托初步

时间:2023-01-15 01:29:42

  相信正在学习C#的人都有学习过C或C++的经验,本文要讲的第一个要点是C#中的委托(delegate,有些资料也叫代表)。什么是委托,很多人都能自然而然地想到C/C++中的函数指针,事实上很多书和资料都以此来引出C#中委托的概念,在此我建议如果没有接触过C/C++的同学可以先了解一下相关的知识再来继续C#的学习,毕竟作为编程语言的基础,语言都是招式,思维想法才是内功。有了扎实的基础,后期学习起来才能够事半功倍。

  首先我们通过一个简单的例子快速复习一下C/C++函数指针:

 1 #include<iostream>
2 using namespace std;
3 int func(string name){
4 cout<<"My name is "<<name<<endl;
5 }
6 void call(int(*fun)(string)){
7 fun("Evan Lin");
8 }
9 int main(int args,char ** argv){
10 call(func);
11 }

  重点是 int(*fun)(string) 这个语句,指定一个函数指针的形参,就如同我们定义一个变量 char ch 一样,但要求是此处函数指针的返回值和参数列表都必须与即将传进来的函数地址严格匹配,不然会产生[Invalid Conversion Error],且此处的声明方式只能用指针,即这个 (*fun) ,因为事实上函数指针只是通过一个函数的入口去操作一个函数,虽然可以通过 typedef int (fun)(string); fun *fp 的方式去表示一个函数,但最终也是要定义一个函数的指针,所以此处无法不通过指针而去调用一个函数,至少目前阶段我没有了解到,有了解的朋友可以说出来共同探讨。

  C#中的委托和C/C++中函数指针的对比

  1、C/C++函数指针是通过寻找函数的入口来调用一个函数,C#委托是把函数名当做一个参数传入一个委托对象当中,委托是类型,函数指针是指针。

  2、C/C++函数指针的返回类型和参数列表是作为匹配函数参数的标志,而C#委托有签名(Signature)的概念。

  3、C/C++函数指针直接操作内存的某个地址,而C#委托托管在.Net Framwork下,是一种强类型

  委托的签名(Signature)由委托的返回类型和参数列表组成,看起来和C/C++函数指针的返回类型和参数列表并无多大区别,但作为一门强大的语言,委托的签名的作用不仅仅是作为一种限定作用,其他作用会由下文提及。下面我们按部就班地一步步来认识委托(delegate)

  一、下面是一则例子用于介绍委托的使用方法

 1 using System;
2 namespace ConsoleApplication {
3 class DelegateTest {
4 public delegate void myDelegate(string name);
5 public static void func(string name) {
6 Console.WriteLine("My name is " + name);
7 }
8 static void Main() {
9 myDelegate _myDe = new myDelegate(DelegateTest.func);
10 _myDe("Evan Lin");
11 }
12 }
13 }

  使用关键字delegate声明一个委托类型,声明形式主要是【delegate + 返回类型 + 委托名 + 参数列表】

  像普通类型一样定义一个委托变量,生成委托对象时必须把签名相应的函数作为参数传入委托对象当中,然后进行调用。

  二、委托的快捷语法,可以直接把函数名赋值给委托变量

1 myDelegate _myDe = DelegateTest.func;
2 _myDe("Evan Lin");

  委托和函数(与签名相应)之间存在着隐式转换

  三、多播(Multicast)委托

  多播委托表示可以通过+=和-=的运算符号来添加或者删除到委托队列当中,当执行这个委托的时候会按依次执行添加到委托队列当中的所有委托,当使用多播委托时,委托的返回类型必须为void,否则运行时只会执行最后一个添加到委托队列的委托。

  此处需要注意一点,当添加两个相同的函数时,在委托队列当中实质上添加了两个委托,但当减去一个委托时,如果该委托实体在委托队列中存在时,则把这份委托删除,但如果该委托实体在委托队列中不存在时,委托队列不做任何改变,且不会发生编译时异常。当委托队列为空,然后执行这个多播委托时,会抛出NullReferenceException。

  下面看一小段代码加以理解:

 1 using System;
2 namespace ConsoleApplication1 {
3 class DelegateTest {
4 public delegate void AnimalDelegate();
5 public static void Cat() {
6 Console.WriteLine("Miao Miao");
7 }
8 public static void Dog() {
9 Console.WriteLine("Wang Wang");
10 }
11 static void Main() {
12 AnimalDelegate _aniD;
13 AnimalDelegate _catD = new AnimalDelegate(DelegateTest.Cat);
14 AnimalDelegate _dogD = new AnimalDelegate(DelegateTest.Dog);
15 _aniD = _catD + _dogD;
16 _aniD();//Miao Miao \n Wang Wang
17 _aniD -= _catD;
18 _aniD();//Wang Wang
19 }
20 }
21 }

  运行会依次打印“Miao Miao”和“Wang Wang”两行结果,然后再打印“Wang Wang”。

  四、匿名方法和Lambda表达式

  可以注意到使用委托真正起到作用的仅仅是委托的签名,为了提高开发效率,于是有了匿名方法(= =纯属猜想,欢迎斧正),具体实现方法如下:

 1 using System;
2 namespace ConsoleApplication1 {
3 class DelegateTest {
4 public delegate String MyDelegate(int arg);
5 static void Main() {
6 MyDelegate _myDe = delegate (int arg) {
7 return arg > 0 ? "More than zero" : "Less than or equals zero";
8 };
9 Console.WriteLine(_myDe(0));
10 Console.WriteLine(_myDe(1));
11 }
12 }
13 }

  如代码所示,用【delegate关键字+参数列表+方法体】构成一个委托匿名方法,此处隐藏了具体的函数名称,匿名方法的返回值可有可无(根据委托签名),函数体的反花括号后要加分号,然后使用正常方法调用委托。说到这里相信大家都可以猜想到实际的输出结果,分别是Less than or equals zero和More than zero两行结果。

  Lambda表达式具有比较特殊的写法,同样是为了提高开发效率,降低函数名的重复率等原因,以下通过一个实例进行了解:

 1 using System;
2 namespace ConsoleApplication {
3 class DelegateTest {
4 public delegate String MyDelegate(int arg);
5 static void Main() {
6 MyDelegate _myDe = (arg) => {
7 return arg > 0 ? "More than zero" : "Less than or equals zero";
8 };
9 }
10 }
11 }

  实际效果等同于上一个匿名方法,在Lambda表达式中连参数类型都省去了,因为在定义一个委托类型的时候已经限定了委托的参数类型,以以上代码为例,其中参数arg的类型必须是int,返回类型必须是String。

  五、委托泛型

  如果对应于不同的函数返回类型和函数参数列表,需要声明大量不同签名的委托。泛型委托的出现是为了能适应不同类型的函数,提高代码的复用率,以下通过一个简单的例子来加深理解。

 1 using System;
2 namespace ConsoleApplication {
3 class DelegateTest {
4 public delegate T1 myDelegate<T1, T2>(T1 arg1, T2 arg2);
5 public static string func1(string name,int num) {
6 return "My name is " + name + ",and my favorite number is " + num;
7 }
8 static void Main() {
9 myDelegate<string, int> _myDe = func1;
10 Console.WriteLine(_myDe("Evan Lin",13));
11 }
12 }
13 }

  其中的<T1,T2>代表两种自定义类型,同时分别作为委托的两种类型的参数,并且该委托返回T1类型的返回值。通过 myDelegate<string, int> _myDe 来限定<T1,T2>的具体类型。

  当想定义一个泛型委托,但又想在类型方面做一些限制,可以用到where关键字

  泛型委托约束大约包括几种形式:

1 public delegate T1 myDelegate<T1,T2>(T1 arg1,T2 arg2)where T1:ClassA
2 public delegate T1 myDelegate<T1,T2>(T1 arg1,T2 arg2)where T1:ClassA,InterfaceA
3 public delegate T1 myDelegate<T1,T2>(T1 arg1,T2 arg2)where T1:ClassA where T2:ClassB
4 public delegate T1 myDelegate<T1,T2>(T1 arg1,T2 arg2)where T1:T2

  where后面的表达式代表类型T1只能派生于ClassA类或者是ClassA本身,T2同理。而ClassA,ClassB,InterfaceA等也有一些限制条件,官方文档的解释是:A type used as a constraint must be an interface,a non-sealed class or a type parameter。也就是说作为限制条件的只能是某个接口,非密封(non-sealed)类或者某个参数的类型(即第四句语句所示),除此之外的类型都不能作为泛型约束的类型,否则回显示Invalid constraint错误。

  至此,以上均是个人学习C#委托时候的拙见,难免会有纰漏和不妥之处,欢迎指出斧正。