4.3 委托(1)
委托技术是.NET框架中的高级特性之一,也可以说是.NET重要技术之一,是在大多数的技术笔试、面试中必定出现的部分。委托提供了安全的函数回调(CALL BACK)机制,成为程序员设计灵活简洁的.NET程序的重要方法之一。本章节将通过介绍委托概念和分析一些常见的委托技术题。
面试题55 什么是委托
.NET面试中经常会被提问什么是C#中的委托,面试官主要意图是考察应聘者对.NET的几个重要特性的了解。这样的细节问题往往让程序员不能够准确地回答,读者应该在平时学习的过程中注意积累。
【出现频率】★★★★★
【关键考点】
委托(Delegate)
委托的特点
【考题分析】
委托是一种引用类型,它是函数指针的托管版本。在C#中,委托是一种可以把引用存储为函数的类型。委托可以引用实例和静态方法,而函数指针只能引用静态方法。委托的声明非常类似函数,和函数不同的是委托不带函数体,并且需要使用Delegate关键字。委托的声明指定了一个函数签名,其中包含参数列表和一个返回类型。在定义了委托后,就可以声明该委托类型的变量,然后可以将这个变量初始化为与该委托有相同签名的函数进行引用,随后可以使用委托变量调用该函数。
注意:委托虽然与函数指针非常类似,但却不是指针。许多程序员把.NET中的委托理解为安全的函数指针,这样的理解是比较牵强的,委托实现的功能和函数指针非常类似的一点就是提供了程序回调机制。
委托的内部实现机制和函数指针在指向方法这一点上是完全相同的。委托是安全的,主要因为委托和其他所有的.NET成员一样,均是一种类型,都是System.Delegate的某个派生类的一个对象。
现在来分析一个具体的例子,示例代码如下: - using System;
- namespace MyConsole
- {
- class SimpleDelegate
- {
- //定义一个返回为double类型的委托,这个委托有一个整型的输入参数
- public delegate double Delegate_Prod(int a);
- static double fn_Prodvalues(int val1)
- {
- return val1 * val1;
- }
- static void Main(string[] args)
- {
- Delegate_Prod delObj = new Delegate_Prod(fn_Prodvalues);
- //声明一个委托
- Console.Write("请输入数字");
- int v1 = Int32.Parse(Console.ReadLine());
//输入一个整型数值 - double res = delObj(v1);
//调用委托方法fn_Prodvalues - Console.WriteLine("返回值:" + res); //返回结果
- Console.ReadLine();
- }
- }
- }
示例代码中,首先通过public delegate double Delegate_Prod(int a)定义了一种名为Delegate_Prod的新类型,这个类型继承自System.MulticastDelegate。它包含一个名为Invoke()的方法,该方法接收一个字符型的参数且没有任何返回。这些步骤都是由C#编译器自动完成的。然后,声明了一个Delegate_Prod的对象delObj,并且绑定到fn_Prodvalues这个静态方法上。运行结果如下: - 请输入数字9
- 返回值: 81
注意:本质上,委托的调用就是执行了在定义委托时所生成的Invoke()方法。
C#中的委托类都继承自System.Delegate类型。委托类型的声明与方法签名相似,有一个返回值和任意数目任意类型的参数。委托是一种可用于封装命名或匿名方法的引用类型。委托类似于函数指针,但是委托是类型安全和可靠的。
面试题56 C#中被委托的方法必须是静态的吗
关于委托的方法是否必须是静态方法这个考题,考察点非常明确。面试官一般希望了解应聘者是不是仅仅了解如何定义和简单的使用委托,是否真正掌握理解委托的内部原理。
【出现频率】★★★★★
【关键考点】
静态方法与实例方法的区别
分析委托的方法是否必须是静态方法
【考题分析】
在分析这个问题之前,先了解C#几个基本的概念。在C#中最常见的方法就是静态方法和实例方法,它们之间的区别如表4.1所示。
表4.1 静态方法和实例方法的区别
静 态 方 法 |
实 例 方 法 |
Static关键字 |
不需要static关键字 |
类名调用 |
实例对象调用 |
可以访问静态成员 |
可以直接访问静态成员 |
不可以直接访问实例成员,需要实例化 |
可以直接访问静态成员 |
不能直接调用实例方法,需要实例化 |
可以直接调用实例方法和静态方法 |
调用前初始化 |
实例化时初始化 |
如表4.1所示,在C#中静态方法由关键字static来定义。静态方法不需要实例化该对象,即可以通过类名来访问对象。相应地,在静态方法中不能直接访问类型中任何非静态成员。而实例方法,则需要通过具体的实例对象来调用,并且可以访问实例对象中的任何成员对象。
委托绑定实例方法的分析,依据C#实例方法的定义,当一个实例方法被调用时,需要通过实例对象来访问,因此绑定实例方法到委托时,必须同时让委托得到该实例方法的实例对象的信息。这样,.NET才能在委托被回调(CallBack)的时候成功地执行该实例方法。Delegate.Target属性是一个指向目标实例的引用,当绑定实例方法给委托时,该参数会被设置为该方法所在类型的一个实例对象。
委托绑定静态方法的分析。Delegate.Target属性是一个指向目标实例的引用,当绑定一个静态方法给委托时,则该参数会被设置为null。
注意:如果委托调用一个或多个实例方法,则此属性返回调用列表中最后一个实例方法的目标。
【答案】
通过上面的分析读者可以清晰地判别,委托不仅能绑定静态方法,同时也可绑定实例方法。当绑定实例方法时,Delegate.Target属性将会设置成指向该实例方法所属类型的一个实例对象,当绑定静态方法时,Delegate.Target属性将会被设置成null。
4.3 委托(2)
面试题57 什么是多播委托
每个委托都只包含一个方法调用,调用委托的次数与调用方法的次数相同。如果要调用多个方法,就需要多次显式调用这个委托。当然委托也可以包含多个方法,这种委托称为多播委托。下面将详细分析这个问题。
【出现频率】★★★★
【关键考点】
多播委托的基本概念
System.MulticastDelegate 的特性
【考题分析】
在本章前文中,笔者已经详细地介绍了委托的基本概念。而所谓的多播委托,是指引用多个方法的委托,当调用委托时,它连续调用每个方法。在调用过程中,委托必须为同类型,返回类型也必须是void,且不能带输出参数(但可以带引用参数),这样才能将委托的单个实例合并为一个多播委托。除此之外,多番委托的声明和实例化方法都和其他委托没有什么不同。现在来看一个实际的例子,示例代码如下: - using System;
- namespace MyConsole
- {
- class MulticastDelegate
- {
- //定义委托,接收一个字符型参数
- public delegate void hellokittyHander(string msg);
- private void hellokitty1(string msg)
- //定义一个简单
的私有方法hellokitty1() - {
- Console.WriteLine("hello kitty1" + msg);
- }
- private void hellokitty2(string msg)
- //定义一个简单
的私有方法hellokitty2() - {
- Console.WriteLine("hello kitty2" + msg);
- }
- private void hellokitty3(string msg)
- //定义一个简单的
私有方法hellokitty3() - {
- Console.WriteLine("hello kitty3" + msg);
- }
- static void Main(string[] args)
- {
- MulticastDelegate SimpleDelegate = new
MulticastDelegate(); - //声明一个委托变量,并绑定第一个方法
- hellokittyHander hellokitty = new
hellokittyHander(SimpleDele- - gate.hellokitty1);
- hellokitty += new hellokittyHander(SimpleDelegate.hellokitty2);
- //绑定第二个方法
- hellokitty += new hellokittyHander(SimpleDelegate.hellokitty3);
- //绑定第三个方法
- //删除已经调用委托方法
- hellokitty -= new hellokittyHander
(SimpleDelegate.hellokitty2); - if (hellokitty != null)
- hellokitty("这是一个委托demo");
- Console.ReadLine();
- }
- }
- }
简单分析这个例子,示例代码中首先定义了一个带string参数无返回的委托hellokittyHander,在Main()函数中声明了一个hellokittyHander的委托变量hellokitty,并绑定了第一个方法void hellokitty1(string msg)。然后,通过hellokitty += new new hellokittyHander(SimpleDelegate.hellokitty1)初始化了第二个委托,并且绑定了void hellokitty2(string msg) ,同时把第二个委托挂在第一个委托之后。紧接着,挂上第三个委托并绑定到 void hellokitty3(string msg)。这是一种比较简单明了的写法,在开发ASP.NET 或者Windows应用程序时,当一个按钮事件被添加时,Visual Studio就会为程序生成类似的代码。
代码运行结果如下: - hello kitty1 这是一个委托demo
- hello kitty3 这是一个委托demo
在这里读者可能会对这个输出的代码产生困惑,为什么hello kitty2没有被输出。事实上细心的读者会发现在上面的程序中有一句取消绑定的事件,代码如下:
- //删除已经调用委托方法
- hellokitty -= new hellokittyHander
(SimpleDelegate.hellokitty2);
注意:多播委托是指一个委托的链表,而不是指另一类特殊的委托。当执行链上的一个方法,后续委托方法将会依次被执行。System.MulticastDelegate定义了对多播委托的支持。
【答案】
多播委托是指一个由委托串成的链表,当链表上的一个委托被回调时,所有链表上该委托的后续委托将会被顺序执行。但要注意,多播委托必须是同类型的,返回类型必须是void,并且不能带输出参数(但可以带引用参数)。
面试题58 列举一个C#中的委托应用
列举C#中的委托应用是一个比较抽象的问题,面试官旨在考察应聘者对委托的使用经验和设计能力。为了解释这一问题,本小节将带领读者实际体会一个具体的例子。为了能够熟悉地运用委托,读者仍然需要在实际工作学习中不断地积累经验。
【出现频率】★★★★
【关键考点】
委托的设计
委托的应用
【考题分析】
排序系统在大多数系统中都有所应用,在本小节中,笔者将举一个学生成绩排序的例子来说明委托的实际应用。下列代码展示了这个学生成绩排序系统的简单实现方式。
示例中定义了学生类型的基本成员、构造方法,在私有构造方法中,为学生排序委托定义了默认的逻辑,示例代码如下:
- //学生类
- class Student
- {
- private string name; //姓名字段
- private decimal score; //成绩字段
- public Student(string name,decimal score) //构造函数、参数姓名和成绩
- {
- this.name = name;
- this.score = score;
- }
- ///比较方法
- ///<param name="lhs">左侧学生对象</param>
- ///<param name="rhs">右侧学生对象</param>
- ///<returns>右侧学生对象的成绩高则返回true</returns>
- public static bool RhsIsGreater(object lhs, object rhs)
- {
- Student studentL = (Student)lhs;
- Student studentR = (Student)rhs;
- return (studentR.score > studentL.score) ? true : false;
- }
- ///<returns>预定义格式的输出</returns>
- //重写ToString方法,可以使得输出更加清晰,输出预定义格式
- public override string ToString()
- {
- return string.Format(name + " : " + score);
- }
- }
-
4.3 委托(3)
定义成绩排序的委托方法,并将这个方法添加到委托对象之中。以下示例代码使用冒泡法排序法对数组进行排序。 - ///<summary>
- ///排序类,方法Sort为静态方法,使用冒泡法
- ///</summary>
- class ScoreSort
- {
- ///<summary>
- ///冒泡法排序
- ///</summary>
- ///<param name="sortArray">要进行排序的数组</param>
- ///<param name="Method">排序的方法</param>
- static public void Sort(object[] sortArray, CompareOp Method)
- {
- //外围第一次循环
- for (int i = 0; i < sortArray.Length; i++)
- {
- //第二次循环
- for (int j = i + 1; j < sortArray.Length; j++)
- {
- //比较大小并交换
- if (Method(sortArray[j], sortArray[i]))
- {
- object temp = sortArray[i];
- sortArray[i] = sortArray[j];
- sortArray[j] = temp;
- }
- }
- }
- }
- }
最后可以编写测试代码,来测试学生排序的功能,示例代码如下: - using System;
- using System.Collections.Generic;
- using System.Text;
- namespace MyConsole
- {
- class StudentSort
- {
- //定义一个委托
- delegate bool CompareOp(object lhs, object rhs);
- static void Main(string[] args)
- {
- //初始化一个学生数组
- Student[] students =
- {
- new Student("Zhang", 86),
- new Student("Li", 75),
- new Student("Wang", 93),
- new Student("Zhao", 68),
- new Student("Liu", 53),
- new Student("Jia", 100),
- new Student("Pan", 83)
- };
- //使用RhsIsGreater方法进行比较,并用Sort方法排序
- CompareOp studentCompareOp = new CompareOp(Student.RhsIsGrea-
- ter);
- ScoreSort.Sort(students, studentCompareOp);
- //循环输出所有学生的已排序成绩
- for (int i = 0; i < students.Length; i++)
- {
- Console.WriteLine(students[i].ToString());
- }
- Console.ReadLine();
- }
- }
- }
下面是示例代码执行后的输出: - Liu: 53
- Zhao: 68
- Li: 75
- Pan: 83
- Zhang: 86
- Wang: 93
- Jia: 100
【答案】
委托的应用环境通常是任务的执行者把细节任务进行再次分配,执行者明确地知道被执行的工作是什么,而且可以把这些执行细节委托给其他组件、方法或者程序集。
|