c#中的数据类型简介(委托)
什么是委托?
委托是一种类型,它封装了一类方法,这些方法具有相同的方法签名(signature)和返回类型。定义听起来有点拗口,首先可以确定委托是一种数据类型,那么什么是方法签名,其实就是指方法的输入参数列表。来看下面的例子,类deleMthod定义了3个方法,add、minus和multi,他们都具有相同的输入参数列表(int x,int y)和输出参数类型int,那么我们就说这三个方法具有相同的方法签名和返回类型。开发者可以抽象地用 int 某名称(int x,int y) 的一种类型对方法进行封装,在c#中这种抽象的数据类型叫委托,针对上述的几个方法我们可以定义委托 : public delegate int Handler(int x ,int y),public 是一个访问修饰符,delegate关键字表示这是一个委托,int Hander(int x,int y)表示这个委托的名称。
class deleMethod { public int add(int x, int y) { return x + y; } public int minus(int x, int y) { return x - y; } public int multi(int x, int y) { return x * y; } }
怎么使用委托
使用委托大体有四个步骤:
- 定义一个委托类型,上节已经提及。
- 定义委托方法,上节deleMethod类中add、minus、multi都是委托方法,定义的目的就是为了使用它,讲专业点就是为了方法的调用
- 委托变量及赋值,和类型一样,在使用前需要对变量赋值。
- 委托变量的使用。
怎样定义委托变量,还是接着上面的例子。我们已经定义了一个委托类型 public delegate int Handler(int x,int y),和c#语法规范一样定义一个变量并赋值语法是:“类型名 变量名 = new 类型名(方法);”,如上例
“Handler deleCall = new Handler(方法名);“,在.net2.0后对于委托的实例化可以简化为” Handler deleCall = 方法名;“。
委托变量的使用,也就是对委托方法的调用。其语法是”int result1 = deleCall(10,20);“或者使用委托调用方法 Invoke,“int result2 = deleCall.Invoke(10,20);”。
具体如何使用可以看看下面的示例:
class Program { public delegate int Handler(int x, int y); //---定义委托的类型,可以将委托看成一种特殊的数据类型 static void Main(string[] args) { deleMethod dm = new deleMethod(); //---实例化包含委托方法的类型 Handler deleCall = new Handler(dm.add); //---定义委托变量delCall,并出示化赋值 int result1 = deleCall(10, 20); //---实例方法的调用invoke Console.WriteLine("the add resutl is:{0}", result1); deleCall = dm.minus; int result2 = deleCall.Invoke(12, 6); Console.WriteLine("the minus result is:{0}", result2); Console.ReadLine(); } }
如上例所示,定义一个简单的加、减功能如此的复杂,搅来搅去让人头,真是无语,难怪很多朋友谈委托色变晕。在实际使用的过程中,c#还是有很多方式帮我们简化代码。
简化委托
预定义的泛型委托
c#系统最常见的预定义的委托类型有三种,Func<>委托、Action<>委托、Predicate<>委托,Func<>委托是一个有返回值的委托,输入参数可以多达16个;而Action<>委托是一个没有返回值的委托,它的输入参数也可以多达16个;而Predicate<>是一个具有bool返回类型的委托,它只运行一个输入参数。对于有上例的委托类型,我们可以使用预定义的委托类型Fun<int,int,int>来代替,省去我们自己定义一个什么鬼东西 public delegate int Handler(int x,int y)类型,其代码其实可以简化为如下例所示:
namespace DelegateDemo1 { class Program { static void Main(string[] args) { deleMethod dm = new deleMethod(); Func<int, int, int> fun = dm.add; //---使用预定义的委托类型Func<> int result4 = fun(8, 10); Func<int, int, int> fun1 = dm.minus; int result5 = fun1(12, 8); Console.WriteLine("预定义的委托输出{0},{1}", result4, result5); Console.ReadLine(); } } class deleMethod { public int add(int x, int y) { return x + y; } public int minus(int x, int y) { return x - y; } public int multi(int x, int y) { return x * y; } } }
我把委托的方法定义和委托的调用放在一起看,是不是比原先自己定义的一个委托类型简单方便一些?但是这样使用委托还是不怎么清爽,估计在实际应用中很少人会怎么写代码,太不方便了。
匿名方法定义委托
当委托实例的调用和委托方法的定义分开处理,码农们在读程序代码的时候需要来回的去找委托方法去匹配委托变量,看看参数列表和返回值是否正确,这样的程序代码的可读性很差。其实c#还是有方法让我们简化代码:那就是匿名委托,将方法体直接在委托的实例化时给出,之所以叫匿名方法再定义方法的时候省略掉方法的名称,它的定义语法是delegate(参数1,参数2) 后面直接就给出方法体,用大括号将方法体括起。刚看起来比较怪异,接触多了也就习惯了,莫有办法只能去适应c#的语法规范。话说多了是水,还不如看代码来得直接。
namespace DelegateDemo1 { class Program { static void Main(string[] args) { Func<int, int, int> fun = delegate(int x, int y) { return x + y; }; Func<int, int, int> fun1 = delegate(int x, int y) { return x - y; }; int result4 = fun(8, 10); int result5 = fun1(12, 8); Console.WriteLine("预定义的委托输出{0},{1}", result4, result5); Console.ReadLine(); } } }
看看是不是在原来的基础上大幅度减少了代码量,肿么办,是否代码量已经减少到极致了?
lambda表达式
其实对于委托的定义还可以进一步简化,那就是使用lambda表达式,lambda表达式的定义是(参数列表)=>{方法体},=>读作goes to。lambda表达式对参数列表和方法表达式的精简达到极致,对于上面的例子,用λ表达式可以省略掉匿名委托的关键字和参数类型,系统可以进行类型推断,不影响运行,其简化的代码如下。对.net 1.0,2.0最传统的委托定义和使用,是一个巨大的简化,它剔除了所有多余的语句达到极致。
namespace DelegateDemo1 { class Program { static void Main(string[] args) { Func<int, int, int> fun = (x, y) => x + y; Func<int, int, int> fun1 = (x, y) => x - y; int result4 = fun(8, 10); int result5 = fun1(12, 8); Console.WriteLine("预定义的委托输出{0},{1}", result4, result5); Console.ReadLine(); } } }
委托链
前面讲过,委托在本质上仍然是一个类,我们用delegate关键字声明的所有委托都继承自System.MulticastDelegate。后者又是继承自System.Delegate类,System.Delegate类则继承自System.Object。一个委托可以绑定若干相同签名的方法形成一个委托链,委托链是一个委托实例的集合,它允许我们调用这个集合中的委托实例所代表的所有方法(对于有返回值的方法,委托链的返回值为链表中最后一个方法的返回值),在下面的例子我们定义的委托方法都没有返回值。我们可以用 GetInvocationList()方法获取委托链。
class Program { static void Main(string[] args) { //Action 表示没有返回值的一类方法 Action<int, int> actionA = (x, y) => { Console.WriteLine("x是{0},y是{1},他们的平方和是{2}", x, y, x * x + y * y); }; actionA += (x, y) => { Console.WriteLine("x是{0},y是{1},他们的平方差是{2}", x, y, x * x - y * y); }; actionA(10, 5);
foreach (var item in actionA.GetInvocationList())
Console.WriteLine(item.Method);
Console.ReadLine(); } }
什么是事件
经常看到一种定义是:事件是一种特殊的委托,是对委托的封装。其实这种定义是很不严谨的。委托是一种数据类型,但是事件只是委托的实例,不能算是一种数据类型,所以说事件是一种特殊的委托是不准确的。如果这样定义:事件是一种特殊的委托实例,是对委托的封装。那么在C#中事件是如何定义并被使用的呢?其实事件从定义到使用要经过四个阶段。
- 定义事件依赖的委托,并定义事件以及引发事件的方法,该步骤可以定义在事件发布者类中
- 定义事件所依赖的事件方法,该步骤可以定义在事件订阅者类中
- 如果有事件参数,该步骤可以定义在事件参数类中
- 注册事件并引发事件,该步骤一般写在主程序中
一般来讲在事件定义过程中,各个方法的命名没有统一的标准,但我们可以参考微软在c#中的命名规范进行命名
建议事件委托命名为:事件+EventHandler。如果有额外的参数传递需定义自己的参数类型,参数类型的命名规范 :事件+EventArgs。比如,可以定义一个事件委托 public delegate void calcEventHandler(object sender,calcEventArgs e); 。
定义一个事件变量如:public event calcEventHandler calc;
定义一个引发事件的方法如:public void onCalc(object sender,calcEventArgs e){}
建议方法名为 on+事件,就如同我们在开发web程序时,绑定的事件名有onClick,onLoad等一样。
参考看下面的例子,了解定义一个事件的一般过程,如果不需要传递事件的参数可以省去事件参数类的定义,使用系统预定义的EventArgs就可以了。
/// <summary> /// 主程序类 /// </summary> class Program { static void Main(string[] args) { eventPublish myEvent = new eventPublish(); eventSubscription myMethod = new eventSubscription(); //绑定方法 add 和 subtract,又称为对事件方法的注册 -=称为对事件方法的注销 myEvent.calc += myMethod.add; myEvent.calc += myMethod.substract; while (true) { try { Console.WriteLine("请输入第一个整数数"); int numA = Convert.ToInt16(Console.ReadLine()); Console.WriteLine("请输入第二个整数"); int numB = Convert.ToInt16(Console.ReadLine()); calcEventArgs e = new calcEventArgs(numA, numB); //在本例不需要sender参数,随意传递了字符串"sender" myEvent.onCalc("sender", e); } catch (Exception ex) { Console.WriteLine("出现错误," + ex.Message); } } } } /// <summary> /// 定义一个事件发布器类 /// </summary> class eventPublish { //定义一个委托类型,委托名一般是 事件变量名+EventHandler public delegate void calcEventHander(object sender,calcEventArgs e); //定义一个事件变量,其变量名为calc public event calcEventHander calc; //封装事件,对外定义了引发事件的方法,定义的引发方法名一般是 on+事件变量名 public void onCalc(object sender, calcEventArgs e) { if (calc != null) calc(sender,e); } } /// <summary> /// 定义一个事件订阅者类(事件方法类) /// </summary> class eventSubscription { public void add(object sender, calcEventArgs e) { Console.WriteLine("两数相加等于{0}", e.X + e.Y ); } public void substract(object sender, calcEventArgs e) { Console.WriteLine("两数相减等于{0}", e.X - e.Y); } } /// <summary> /// 定义一个事件参数类 /// </summary> class calcEventArgs : EventArgs { private int _x; private int _y; public calcEventArgs(int x, int y) { this._x = x; this._y = y; } public int X { get { return _x; } } public int Y { get { return _y; } } }
我们将事件是对委托的封装,如果用ILDAS反编译可执行文件查看中间代码就可以非常明了的看出事件运行机制
如上图所示,事件calc,经.net运行时编译为中间语言后产生了一个private 的calc属性,同时也自动生成了add_calc和remove_calc方法,用于注册和注销订阅者方法。因为事件是被封装的,尽管calc属性是private,但在所以在发布器类内部可以用 calc(sender,e)这样的方法直接调用;但在主程序中如果这样使用就会出错,只能通过onCalc方法进行间接调用。
后记
本篇文章,一开始提出了什么是委托的疑问,通过引入几个方法来讲述委托是什么以加强对委托概念的理解。第二部分讲述了使用委托的四个步骤,并通过示例阐述了这几个步骤。第三部分讲述了委托使用的简化问题,通过使用泛型委托简化自定义委托,通过使用匿名委托可以简化定义委托方法。匿名委托是在定义委托的时候直接给出方法体,通过使用lambda表达式的类型推断可进一步简化委托的使用。第四部分讲述了委托链,通过绑定方法初始化委托,并通过+=绑定更多的委托方法。第五部分讲述了事件的定义和使用的四个步骤。当然委托的使用场景还有很多,比如通过BeginInvoke和EndInvoke进行异步调用,因不是本篇文章的重点,所以文章中没有提及。