委托和事件一直被视为C#的难点,.NET的面试题中经常出现这些知识点。本节通过常见的面试考题辅以典型实例剖析,对知识点深度讲解。
考点:委托的含义,委托和引用方法的联系。
出现频率:★★★★★
解答
本实例通过用户选择不同格式显示输入的文字,解决办法是首先建立委托类型,其签名可以匹配多个格式化字符,然后在Main()方法中创建委托对象,通过判断用户的输入,将委托对象进行相应地初始化,从而完成不同方法的引用。在目录下新建一个程序文件,并命名为DelegateTest.cs,编写代码如代码7.1所示。
代码7.1 简单使用C#的委托:DelegateTest.cs
using System;
namespace NET.CHP6
{
public class DelegateTest
{
//定义一个MyDel委托类型,其签名接收两个string类型参数,返回类型也为string
public delegate string MyDel(string nm, string pwd);
static void Main(string[] args)
{
//声明MyDel类型的md变量
MyDel md;
Console.WriteLine("请输入2个参数,并用逗号分隔");
//将用户输入值赋值给InputValue变量
string InputValue = Console.ReadLine();
//获取英文逗号在InputValue变量中的索引值,并赋值给Pos变量
int Pos = InputValue.IndexOf(",");
//将英文逗号前面部分的子字符串赋值给ValueA变量
string ValueA = InputValue.Substring(0, Pos);
//将英文逗号后面部分的子字符串赋值给ValueB变量
string ValueB = InputValue.Substring(Pos + 1);
Console.WriteLine("请输入A,B,C,D,以确定所调用方法");
//将用户输入值转换为大写,并赋值给InputMethod方法
string InputMethod = Console.ReadLine().ToUpper();
//判断InputMethod变量的值
switch (InputMethod)
{
case "A":
//如果用户输入为A或a,则创建MyDel委托对象,其引用变量为md,指向formatA方法
md = new MyDel(formatA);
break;
case "B":
//如果用户输入为B或b,则创建MyDel委托对象,其引用变量为md,指向formatB方法
md = new MyDel(formatB);
break;
case "C":
//如果用户输入为C或c,则创建MyDel委托对象,其引用变量为md,指向Another类的formatC静态方法
md = new MyDel(Another.formatC);
break;
case "D":
//如果用户输入为D或d,则首先创建创建Another对象,引用为ano变量
//然后创建MyDel委托对象,其引用变量为md,指向ano对象的formatD方法
Another ano = new Another();
md = new MyDel(ano.formatD);
break;
default:
//其他情况下,创建MyDel委托对象,其引用变量为md,指向formatA方法
md = new MyDel(formatA);
break;
}
//调用md委托对象,将返回值赋值给result变量
string result = md(ValueA, ValueB);
Console.WriteLine("/n/t==========以下为委托方法执行结果==========");
//输出result变量值
Console.WriteLine(result);
}
//定义两个静态方法formatA和formatB,符合MyDel委托类型的签名
static string formatA(string a, string b)
{
string words = "I am the first static method./nMy name is " + a + "/nMy password is " + b;
words += "/n我是第一个静态方法。/n我的名字是: " + a + "/n我的密码是: " + b;
return words;
}
static string formatB(string a, string b)
{
string words = "I am the second static method./nMy name is " + a + "/nMy password is " + b;
words += "/n我是第二个静态方法。/n我的名字是: " + a + "/n我的密码是: " + b;
return words;
}
}
//定义外部类,其内含静态方法和实例方法符合MyDel委托类型签名
class Another
{
internal static string formatC(string a, string b)
{
string words = "I am the third static method,I belong to Another class./nMy name is " + a + "/nMy password is " + b;
words += "/n我是第三个静态方法,我属于 Another 类。/n我的名字是: " + a + "/n我的密码是: " + b;
return words;
}
internal string formatD(string a, string b)
{
string words = "/n我是1个实例方法,我所属的对象实例是 Another 类所构造的。/n我的名字是: " + a + "/n我的密码是: " + b;
return words;
}
}
}
在Main()方法以外定义了1个委托类型,其名称为MyDel,其返回值为String类型,并接收2个String类型的参数。另外还声明了2个静态方法和1个类,其中Another类含有2个方法,分别为静态方法formatC()和实例方法formatD(),这2个方法都用internal修饰。
在Main()方法中,首先声明MyDel委托类型的对象,并命名为md。通过Console.ReadLine()方法,接收用户的输入并存储到InputValue变量中,通过“,”将InputValue分隔为2个字符串,分别存储于ValueA和ValueB;然后接收用户的输入并存储于InputMethod变量中,通过switch语句判定用户输入是否为A、B、C、D或其他。不同的InputMethod值通过以下代码语句初始化md的引用:
md = new MyDel(调用方法);
最后直接输出md的方法调用。在命令行下编译DelegateTest.cs,执行DelegateTest程序,程序提示“请输入两个参数,并用逗号分隔”。输入相应的值后,程序再次提示“请输入A,B,C,D,以确定所调用方法”,输入“abcd”,即4个选择以外的字母,程序运行结果如图7.1所示。
图7.1 委托执行结果
由上可知,程序中的switch分支语句执行了default条件下的代码,即调用第1个静态方法。再次运行DelegateTest程序,程序提示调用方法时,输入“d”,运行结果如图7.2所示。
如果首先输入“YeQing,password”字符串,然后输入“d”,程序返回D字母对应的结果。由结果可知,委托类型对象md引用了Anothoer类的实例方法formatD()。
图7.2 委托执行结果
解析
很多C#书籍介绍过,C#对C++的借鉴过程中去掉了指针部分,但是在学习到C#委托类型的知识点时,很多C++的程序员会想起指针。不过C#中的委托并不完全等同于指针,相对于C++中的指针,委托是面向对象的、类型安全的类型,在编程中使用更加方便、可靠。
说明:CLR能够保证委托指向一个有效的方法,而不会指向无效地址或者越界地址。
可将委托看作一种新的对象类型,委托主要用于.NET Framework中的事件处理程序和回调函数。委托类型可定义一个签名,并且它只能持有与它的签名相匹配的方法引用。所有委托类型对象都有方法调用列表,它是在调用委托对象时所执行方法的一个链接列表。方法调用列表所链接的方法可以是静态方法,也可以是实例方法。对于实例方法,列表中所对应的是一个实例方法和其所属实例,而对于静态方法,列表中只对应一个方法。
说明:列表中的方法即为与委托类型定义签名相匹配的方法。
当委托类型匹配方法的返回类型和参数列表时,这个委托类型即和此方法兼容。另外,方法的名称等特性不需要和委托类型名称匹配。用delegate 关键字可以定义一个委托,通知编译器这是一个委托类型。委托类型声明的形式类似,没有函数体的函数,但是有相应的返回值和参数列表,其格式如以下代码所示:
public delegate 返回类型 委托名称(参数类型1 参数1,参数类型2 参数2,... );
通过委托定义中的返回值类型和参数列表即可匹配其他方法,而委托名称是这种委托类型的名称。delegate关键字定义时,临时定义了一个派生于System.MulticastDelegate类的密封类,这个类与其基类System.Delegate均同为委托提供必要的基础成员。在使用委托时声明该委托类型的对象,然后将这个对象初始化为该委托对象签名匹配的方法引用。其结果是该委托对象调用了相应的方法,这个委托对象也可当作这个方法使用。delegate关键字定义的委托类型名称是MyDel,创建该委托类型的对象,其对象被初始化后拥有方法调用列表,分析过程如图7.3所示。
|
|
图7.3 委托的分析图
考点:了解多点委托的创建方法,选择多点委托所引用方法返回值。
出现频率:★★
解答
本题创建多点委托,实现用户输入内容后,4个方法逐一被引用,达到“一触即发”的效果。解决方案是建立一个委托对象,根据不同方法的引用创建多个委托类型对象,并累加至同一个对象中。在目录下新建一个程序文件,并命名为MultiDel.cs,编写代码如代码7.2所示。
代码7.2 C#的多点委托:MultiDel.cs
using System;
namespace NET.CHP6
{
class MultiDel
{
//定义1个MyHandler委托类型,其签名接收1个string类型参数,返回类型为void
public delegate void MyHandler(string message);
static void Main(string[] args)
{
//声明MyHandler类型的mh变量
MyHandler mh;
//创建MyHandler对象,指向MethodA方法
mh = new MyHandler(MethodA);
//再次创建MyHandler对象,指向MethodB方法
mh += new MyHandler(MethodB);
//第3次创建MyHandler对象,指向MethodC方法
mh += new MyHandler(MethodC);
//第4次创建MyHandler对象,指向MethodC方法
mh += new MyHandler(OutSide.MethodD);
Console.WriteLine("请输入参数:");
//接收用户输入值并赋值给InputValue变量
string InputValue = Console.ReadLine();
mh(InputValue);
}
//定义3个静态方法,符合MyHandler委托类型的签名
static void MethodA(string content)
{
Console.WriteLine("/n(1)这是第1个方法执行结果,你输入的内容大写形式为:" + content.ToUpperInvariant());
}
static void MethodB(string content)
{
Console.WriteLine("/n(2)这是第2个方法执行结果/n你输入的内容小写形式为:" + content.ToLowerInvariant());
}
static void MethodC(string content)
{
Console.WriteLine("/n(3)这是第3个方法执行结果/n你输入的内容为" + content.GetType()+"类型数据");
}
}
//定义外部类,其内含静态方法MethodD符合MyHandler委托类型签名
class OutSide
{
internal static void MethodD(string content)
{
Console.WriteLine("/n(4)这是第4个方法(外部类的静态方法)执行结果/n你输入的内容有" + content.Length + "个字符");
}
}
}
多点委托也称为多路广播,可以保证指定委托类型的对象能够触发多个方法的执行,简化程序中多个方法的联合使用,并且便于事件驱动编程的编写。在命令行下编译MultiDel.cs,执行MultiDel程序,其结果如图7.4所示。
图7.4 C#的多点委托执行结果
注意:多点委托的委托类型必须保证相同,另外,多点委托所引用的多个方法都有返回值的情况下,只有最后被调用的方法才有返回值。
解析
在上节的学习中,了解到委托的基本使用方法。而一个委托类型对象只能指向一个方法,用多点委托可以链式地引用多个方法。简单地说,多点委托可将多个委托对象组合起来使用,类似于糖葫芦串在一起的方式,并且多个方法的引用有先后顺序。多个委托组合有以下两种方法
(1)声明多个委托类型对象引用变量,分别指向对应的方法,将委托类型对象引用变量用加号加入到委托类型对象引用变量中,如以下代码所示:
委托类型对象名称 = new 委托类型(方法引用名称);
委托类型对象名称2 = new 委托类型(方法引用名称2);
对象名称 = 对象名称 +对象名称2;
注意:加入顺序决定了方法引用的顺序。
(2)创建多个对象,使用“+=”运算符累加到同一个委托类型对象的引用变量中,如以下代码所示:
委托类型对象名称 = new 委托类型(方法引用名称);
对象名称+ = new 委托类型(方法引用名称2);
相应地,如果需要将所指方法从方法调用列表中删除,可以使用“-=”运算符,如以下代码所示:
委托类型 a = new 委托类型(方法引用名称1);
委托类型 b= new 委托类型(方法引用名称2);
a+ = b;
a- = b;
以上代码首先将方法引用名称1和方法引用名称2加入a对象的方法调用列表,然后将方法引用名称2的委托删除。
说明:“+=”运算符和“-=”运算符被重载,编译时这2个运算符分别转换为调用Delegate类的Combine()静态方法和Remove()静态方法。
考点:理解C#的事件机制,事件的创建方法和事件与委托的联系。
出现频率:★★★★
解答
本题要求接收用户输入,以触发事件形式执行订阅该事件的方法。解决方法是创建一个类,在类中添加一个公开属性Text,属性Text被赋值时,将触发事件Get。此时事件Get的事件处理方法被执行。在目录下新建一个程序文件,并命名为EventDel.cs,编写代码如代码7.3所示。
代码7.3 C#的事件机制:EventDel.cs
using System;
class EventDel
{
static void Main(string[] args)
{
//创建Name类的对象myname
Name myname = new Name();
//向myname对象的Get事件注册事件处理方法myname_get
myname.Get += new Name.myEventHandler(myname_get);
Console.Write("/n请输入你的名字:");
//接收用户的输入值并赋值给input变量
string input = Console.ReadLine();
//将input变量赋值给myname对象的Text属性
myname.Text = input;
}
//定义用于订阅事件的myname_get方法
//自定义事件信息类为Name类的嵌套类
static void myname_get(object sender, Name.NameEventArgs e)
{
//输出事件信息和事件发布者的属性
Console.WriteLine("/n/t=========事件处理方法=========");
Console.WriteLine("事件信息:{0}", e.ToString());
Console.WriteLine("事件发布者是:{0}", sender.ToString());
Console.WriteLine("你输入的名字是:{0}", ((Name)sender).Text);
}
}
class Name
{
private string _name;
//定义myEventHandler委托类型
public delegate void myEventHandler(object sender, NameEventArgs e);
//定义Get事件
public event myEventHandler Get;
//定义可读写的Text属性
internal string Text
{
get
{
return this._name;
}
set
{
this._name = value;
//调用OnGet方法,并传递NameEventArgs类对象
this.OnGet(new NameEventArgs("Text属性被更改了"));
}
}
//定义OnGet方法,接收1个EventArgs类型的参数
void OnGet(NameEventArgs e)
{
//触发Get事件,传递2个参数
this.Get(this, e);
}
//重写ToString()方法
public override string ToString()
{
return "Name类的对象";
}
//自定义事件信息类,继承于EventArgs类
public class NameEventArgs : EventArgs
{
string _args;
//重载构造函数,用于将参数值赋值给_args字段
public NameEventArgs(string s)
{
_args = s;
}
//重写ToString()方法,返回_args字段
public override string ToString()
{
return _args;
}
}
}
以上程序功能非常简单,即显示用户输入,事件触发后调用事件处理方法。在命令行下编译EventDel.cs,执行EventDel程序,程序提示“请输入你的名字”,输入“叶青”,运行结果如图7.5所示。
图7.5 C#的事件机制
当用户输入值后,myname对象的Text属性被赋值,程序执行OnGet()方法,并传递自定义事件类NameEventArgs的对象。而OnGet()方法将触发Get事件,并传递对象自身以及NameEventArgs类的对象作为参数。订阅了Get事件的myname_get()方法被通知,主程序立即调用myname_get()方法,在方法中输出自定义事件类的字符串,以及事件发布者的字符串形式和属性值。
说明:必须理解事件和委托的联系,才能掌握事件机制的本质。
解析
大部分应用程序包括JavaScript、ActionScript等都有异步事件处理机制,而在C#中是由多点委托和事件来实现这种机制的。
这种设计模式可以称为“发布者/订阅者模式”,发布者发布事件,多个类订阅这个事件(类必须包含一个相应的事件处理方法)。当该事件被触发时,系统通知每个订阅者事件发生。触发事件所调用的事件处理方法是由委托来实现。在这种情况下,必须注意以下几点。
(1)委托类型的签名必须有两个参数,分别是触发事件的对象和事件信息。
(2)事件信息必须是由EventArgs类派生。
这样在写触发事件的对象类时不必知道事件信息对象类。事件信息对象类可以在运行时订阅或解除订阅特定的事件。简单地说,事件就是当对象或类(发布者)状态发生改变时,对象或类发出信息通知订阅者,发布者也被称为“事件源”。
说明:在其他语言的事件机制中也有类似的模式,如Java采用接口,在运行时使用多态的方式实现对事件接收者响应函数的调用。
编写简单的事件机制需要先定义委托类型,然后通过委托类型定义事件,最后事件处理方法订阅事件。假设定义了名为MyDel委托类型,事件名称为onclick,定义部分如以下代码所示:
public delegate 返回类型 MyDel(object sender, EventArgs e);
public event MyDel onclick
MyDel 委托对象名称 += new MyDel(事件处理方法);
//事件处理方法订阅onclick事件
onclick += 委托对象名称;
//事件处理方法取消订阅onclick事件
onclick -= 委托对象名称;
从以上代码可以看出,事件实质上是一种特殊的委托,通过多点委托的方法被多个方法订阅。当事件触发时,相应的事件处理方法将会被引用。
考点:匿名方法的作用,匿名方法的使用方法。
出现频率:★★★
解答
匿名方法用于简化事件注册方法的编写过程,可以直接将方法体的代码和委托对象相关联而不需要单独定义这个方法。本例程序仍然实现EventDel.cs的相同功能,只是将事件处理方法更改为匿名方法,并且在Name类中将触发事件的代码直接编写在Text属性的set访问器中。在目录下新建一个程序文件,并命名为AnoMethod.cs,编写代码如代码7.4所示。
代码7.4 C#的匿名方法:AnoMethod.cs
using System;
class AnoMethod
{
static void Main(string[] args)
{
//创建Name类的对象myname
Name myname = new Name();
//向myname对象的Get事件注册匿名事件处理方法
myname.Get += delegate(object sender, Name.NameEventArgs e)
{
Console.WriteLine("/n/t=========匿名事件处理方法=========");
Console.WriteLine("事件信息:{0}", e.ToString());
Console.WriteLine("事件发布者是:{0}", sender.ToString());
Console.WriteLine("你输入的名字是:{0}", ((Name)sender).Text);
};
Console.Write("/n请输入你的名字:");
//接收用户的输入值并赋值给input变量
string input = Console.ReadLine();
//将input变量赋值给myname对象的Text属性
myname.Text = input;
}
}
class Name
{
private string _name;
//定义myEventHandler委托类型
public delegate void myEventHandler(object sender, NameEventArgs e);
//定义Get事件
public event myEventHandler Get;
//定义可读写的Text属性
internal string Text
{
get
{
return this._name;
}
set
{
this._name = value;
//调用OnGet方法,并传递NameEventArgs类对象
this.Get(this, new NameEventArgs("Text属性被更改了"));
}
}
//重写ToString()方法
public override string ToString()
{
return "Name类的对象";
}
//自定义事件信息类,继承于EventArgs类
public class NameEventArgs : EventArgs
{
string _args;
//重载构造函数,用于将参数值赋值给_args字段
public NameEventArgs(string s)
{
_args = s;
}
//重写ToString()方法,返回_args字段
public override string ToString()
{
return _args;
}
}
}
本示例程序功能和EventDel.cs完全一样,用于显示用户输入,事件触发后调用匿名事件处理方法。在命令行下编译AnoMethod.cs,执行AnoMethod程序,程序将提示“请输入你的名字”,输入“叶青”,运行结果如图7.6所示。
图7.6 C#的匿名方法
本程序运行结果与EventDel.cs完全一致,只是匿名方法的编写更为高效。回顾List<T>对象的FindAll()方法,该方法的参数是一个泛型委托(委托类型为Predicate),编写方法如以下代码所示:
using System;
using System..Collections.Generic
//定义1个List<T>类的对象MyList ,类型参数T为int
List<int> MyList = new List<int>();
添加部分int类型子项到MyList 对象中;
List<int> MyList2 = MyList2.FindAll(delegate(匹配方法参数){匹配方法体代码});
foreach (int i in MyList2)
{
输出int子项i的信息;
}
以上代码将Predicate泛型委托对象的编写进行了大量简化,不需要单独定义匹配方法。
注意:是否采用匿名方法根据情况来定,如某些方法体代码比较复杂,或者方法可用于委托对象以外时,不可以采用匿名方法。
解析
C#的匿名方法可以使委托及事件的代码编写更加精简高效。一般情况下,注册事件处理方法,首先需要定义处理方法,如果该方法仅用于订阅特定的事件,则可使用匿名方法。假设定义了名为MyDel委托类型,事件名称为onclick,其注册事件处理方法如以下代码所示:
static void Main(string[] args)
{
//定义MyDel委托类型
public delegate 返回类型 MyDel(object sender, EventArgs e);
//定义onclick事件
public event MyDel onclick
//事件处理方法订阅onclick事件
onclick+= new MyDel(事件处理方法);
其他代码;
}
//单独定义事件处理方法
static 返回类型 事件处理方法(object sender, EventArgs e)
{
事件处理方法体代码;
}
以上代码中的事件处理方法往往只用于委托对象,而匿名方法可以使事件处理方法不需要单独定义,如以下代码所示:
static void Main(string[] args)
{
//定义委托及关联事件
public delegate 返回类型 MyDel(object sender, EventArgs e);
public event MyDel onclick
//事件处理方法订阅onclick事件
onclick+= delegate(object sender, EventArgs e){事件处理方法体代码};
其他代码;
}
以上代码的事件处理方法没有名称,被称为匿名方法。匿名方法的方法体代码直接和委托对象关联,不需要指定委托对象名称。
说明:实际编译时,匿名方法的名称由编译器自动分配。
考点:Lambda表达式的作用, Lambda表达式的使用方法。
出现频率:★
解答
Lambda表达式可以进一步简化编写的代码,在匿名方法的基础上,甚至可以隐藏delegate关键字。不过Lambda表达式不可滥用,应根据程序需要而使用。本例程序仍然实现AnoMethod.cs相同的功能,只是将匿名方法部分更改为Lambda表达式。在目录下新建一个程序文件,并命名为Lambda.cs,编写代码如代码7.5所示。
代码7.5 C#的Lambda表达式:AnoMethod.cs
using System;
class Lambda
{
static void Main(string[] args)
{
//创建Name类的对象myname
Name myname = new Name();
//向myname对象的Get事件注册匿名事件处理方法
myname.Get += (object sender, Name.NameEventArgs e)=>
{
Console.WriteLine("/n/t=========Lambda表达式=========");
Console.WriteLine("事件信息:{0}", e.ToString());
Console.WriteLine("事件发布者是:{0}", sender.ToString());
Console.WriteLine("你输入的名字是:{0}", ((Name)sender).Text);
};
Console.Write("/n请输入你的名字:");
//接收用户的输入值并赋值给input变量
string input = Console.ReadLine();
//将input变量赋值给myname对象的Text属性
myname.Text = input;
}
}
class Name
{
private string _name;
//定义myEventHandler委托类型
public delegate void myEventHandler(object sender, NameEventArgs e);
//定义Get事件
public event myEventHandler Get;
//定义可读写的Text属性
internal string Text
{
get
{
return this._name;
}
set
{
this._name = value;
//调用OnGet方法,并传递NameEventArgs类对象
this.Get(this, new NameEventArgs("Text属性被更改了"));
}
}
//重写ToString()方法
public override string ToString()
{
return "Name类的对象";
}
//自定义事件信息类,继承于EventArgs类
public class NameEventArgs : EventArgs
{
string _args;
//重载构造函数,用于将参数值赋值给_args字段
public NameEventArgs(string s)
{
_args = s;
}
//重写ToString()方法,返回_args字段
public override string ToString()
{
return _args;
}
}
}
本示例程序功能和AnoMethod.cs完全一样,显示用户输入,事件触发后调用Lambda表达式。在命令行下编译Lambda.cs,执行Lambda程序,程序提示“请输入你的名字”,输入“叶青”,运行结果如图7.7所示。
图7.7 C#的Lambda表达式
本程序运行结果与AnoMethod.cs完全一致,但是相对于匿名方法,Lambda表达式的编写更为高效。
解析
Lambda表达式是C#3.0新增加的特性,在它的帮助下代码编写更加简洁。这里以匿名方法编写事件注册方法为例,说明如何简化代码。首先定义MyDel委托类型,关联的事件为onclick,onclick事件注册事件处理方法如以下代码所示:
static void Main(string[] args)
{
//定义委托及关联事件
public delegate 返回类型 MyDel(object sender, EventArgs e);
public event MyDel onclick
//事件处理方法订阅onclick事件
onclick+= delegate(object sender, EventArgs e){事件处理方法体代码};
其他代码;
}
以上代码的事件处理方法部分使用了匿名方法定义,好像已经非常精简了,而Lambda表达式可以将其进一步精简,修改为以下代码所示:
static void Main(string[] args)
{
//定义委托及关联事件
public delegate 返回类型 MyDel(object sender, EventArgs e);
public event MyDel onclick
//事件处理方法订阅onclick事件
onclick+= (object sender, EventArgs e)=>{事件处理方法体代码};
其他代码;
}
由以上代码可知,Lambda表达式将delegate也隐藏了,大大简化了代码的编写。Lambda表达式的基本编写方法如以下代码所示:
(参数列表)=>{表达式};
表达式即事件处理方法体代码,参数列表是可选的,即完全可以省略参数列表的编写。Lambda表达式可以完全代替匿名方法,不过并非所有事件注册方法都需要使用Lambda表达式。根据程序的需要,如果需要降低方法体和程序逻辑代码的耦合、提升方法的可重用性,程序开发中仍然以单独定义事件处理方法为主。