C#基础篇 | 理解委托和事件
委托
委托类似于 C++ 中的函数指针(一个指向内存位置的指针)。委托是 C# 中类型安全的,可以订阅一个或多个具有相同签名方法的函数指针。
简单理解,委托是一种可以把函数当做参数传递的类型。很多情况下,某个函数需要动态地去调用某一类函数,这时候我们就在参数列表放一个委托当做函数的占位符。
在某些场景下,使用委托来调用方法能达到减少代码量,实现某种功能的用途。
自定义委托
声明和执行一个自定义委托,一般是通过如下步骤完成:
- 利用关键字
delegate
声明一个委托类型,它必须与需要传递方法具有相同的参数以及返回类型。 - 创建委托对象,并且将需要传递的方法作为参数传递给委托对象。
- 通过上述创建的委托对象来实现该委托绑定方法的调用。
如下的一段代码,完成了一次应用委托的演示,包括声明到调用整个过程。
//step01:使用 delegate 关键字声明委托
public delegate int CalcSumDelegate(int a, int b);
class Program
{
static void Main(string[] args)
{
//step03:实例化这个委托,并引用方法
CalcSumDelegate del = new CalcSumDelegate(CalcSum);
//step04:调用委托
int result = del(5, 5);
Console.WriteLine("5+5=" + result);
}
//step02:声明一个方法和委托类型对应
public static int CalcSum(int a, int b)
{
return a + b;
}
}
使用 ILSpy 反编译上面的代码生成的程序集,截图如下:
可以看到委托继承关系: System.MulticastDelegate
继承自 System.Delegate
继承自 System.Object
。
委托类型会生成3个方法:BeginInvoke
、EndInvoke
、Invoke
。委托会通过这 3 个方法在内部实现调用。Invoke
是同步调用,即上面代码del(5, 5)
执行时,编译器会调用del.Invoke(5,5)
。BeginInvoke
是异步调用,假如上述委托以异步的方式执行,则要调用dal.BeginInvoke(5,5)
。
BeginInvoke
和EndInvoke
是 .NET 中使用异步方式调用同步方法的两个重要方法,具体用法详见微软 官方文档。
多播委托
一个委托可以引用多个方法,包含多个方法的委托就叫多播委托。下面通过一个示例来继续了解。
//step01:声明委托类型
public delegate void PrintDelegate();
public class Program
{
public static void Main(string[] args)
{
//step03:实例化委托,并绑定第1个方法
PrintDelegate del = Func1;
//绑定第2个方法
del += Func2;
//绑定第3个方法
del += Func3;
//step04:调用委托
del();
//控制台输出结果:
//调用第1个方法!
//调用第2个方法!
//调用第3个方法!
}
//step02:声明和委托对应签名的3个方法
public static void Func1()
{
Console.WriteLine("调用第1个方法!");
}
public static void Func2()
{
Console.WriteLine("调用第2个方法!");
}
public static void Func3()
{
Console.WriteLine("调用第3个方法!");
}
}
可以看出,多播委托的声明过程是和自定义委托一样的。可以理解为,多播委托就是自定义委托在实例化时通过+=
符号多绑定了两个方法。
Q:为什么能给委托绑定多个方法呢?
这就要看看官方是如何对System.MulticastDelegate
定义的。
MulticastDelegate
拥有一个带有链接的委托列表,该列表称为调用列表,它包含一个或多个元素。在调用多路广播委托时,将按照调用列表中的委托出现的顺序来同步调用这些委托。如果在该列表的执行过程中发生错误,则会引发异常。(--摘自MSDN)
Q:为什么使用 += 号就能实现绑定呢?
先来看上述程序集反编译后的调用委托的代码:
可以看出,+=
号的的本质是调用了Delegate.Combine
方法,该方法将两个委托连接在一起,并返回合并后的委托对象。
Q:多播委托能引用多个具有返回值的方法吗?
答案是:可以。委托的方法可以是无返回值的,也可以是有返回值的。不过,对于有返回值的方法需要我们从委托列表上手动调用。否则,就只能得到委托调用的最后一个方法的结果。下面通过两段代码验证下:
public delegate string GetStrDelegate();
public class Program
{
public static void Main(string[] args)
{
GetStrDelegate del = Func1;
del += Func2;
del += Func3;
string result = del();
Console.WriteLine(result);
//控制台输出结果:
//You called me from Func3
}
public static string Func1()
{
return "You called me from Func1!";
}
public static string Func2()
{
return "You called me from Func2!";
}
public static string Func3()
{
return "You called me from Func3!";
}
}
正确做法:利用GetInvocationList
获得委托列表上所有方法,循环依次执行委托,并处理委托返回值。
public delegate string GetStrDelegate();
public class Program
{
public static void Main(string[] args)
{
GetStrDelegate del = Func1;
del += Func2;
del += Func3;
//获取委托链上所有方法
Delegate[] delList = del.GetInvocationList();
//遍历,分别处理每个方法的返回值
foreach (GetStrDelegate item in delList)
{
//执行当前委托
string result = item();
Console.WriteLine(result);
//控制台输出结果:
//You called me from Func1
//You called me from Func2
//You called me from Func3
}
Console.ReadKey();
}
public static string Func1()
{
return "You called me from Func1";
}
public static string Func2()
{
return "You called me from Func2";
}
public static string Func3()
{
return "You called me from Func3";
}
}
匿名方法
匿名方法是 C#2.0 引入的一个新特性,用来简化委托的声明。假如委托引用的方法只使用一次,那么就没有必要声明这个方法,这时用匿名方法表示即可。
//step01:定义委托类型
public delegate string ProStrDelegate(string str);
public class Program
{
public static void Main(string[] args)
{
//step02:将匿名方法指定给委托对象
ProStrDelegate del = delegate(string str) { return str.ToUpper(); };
string result = del("KaSlFkaDhkjHe");
Console.WriteLine(result);
Console.ReadKey();
//输出:KASLFKAFHKJHE
}
}
匿名方法只是 C# 提供的一个语法糖,方便开发人员使用。在性能上与命名方法几乎无异。
匿名方法通常在下面情况下使用:
- 委托需要指定一个临时方法,该方法使用次数极少。
- 这个方法的代码很短,甚至可能比方法声明都短的情况下使用。
Lambda 表达式
Lambda 表达式是 C#3.0 引入的一个新特性,它提供了完成和匿名方法相同目标的更加简洁的格式。下面代码展示了用 Lambda 表达式简化上述匿名方法的步骤。
public delegate string ProStrDelegate(string str);
public class Program
{
public static void Main(string[] args)
{
//匿名委托
//ProStrDelegate del = delegate(string str) { return str.ToUpper(); };
//简化1
//ProStrDelegate del1 = (string str) => { return str.ToUpper(); };
//简化2
//ProStrDelegate del2 = (str) => { return str.ToUpper(); };
//简化3
ProStrDelegate del3 = str => str.ToUpper();
string result = del3("KaSlFkaDhkjHe");
Console.WriteLine(result);
Console.ReadKey();
//输出:KASLFKAFHKJHE
}
}
- 简化1:去掉
delegate
关键字,用=>
符号表示参数列表和方法体之间的关系。 - 简化2:去掉方法的参数类型,假如只有一个参数,参数列表小括号
()
也可省略。 - 简化3:如果方法体中的代码块只有一行,可以去掉
return
,去掉方法体的大括号{}
。
内置委托
上述几种委托的使用,都没能离开定义委托类型这一步骤。微软干脆直接把定义委托这一步骤封装好,形成三个泛型类:Action<T>
、Func<T>
、Predicate<T>
,这样就省去了定义的步骤,推荐使用。
public class Program
{
public static void Main(string[] args)
{
//Action
Action<string> action = delegate(string str) { Console.WriteLine("你好!" + str); };
action("GG");
//Func
Func<int, int, int> func = delegate(int x, int y) { return x + y; };
Console.WriteLine("计算结果:" + func(5, 6));
//Predicate
Predicate<bool> per = delegate(bool isTrue) { return isTrue == true; };
Console.WriteLine(per(true));
}
}
它们的区别如下:
Action<T>
:允许封装的方法有多个参数,不能有返回值。Func<T>
:允许封装的方法有多个参数,必须有返回值。Predicate<T>
:允许封装的方法有一个参数,返回值必须为bool
类型。
事件
委托是一种类型,事件依赖于委托,故事件可以理解为是委托的一种特殊实例。
它和普通的委托实例有什么区别呢?委托可以在任意位置定义和调用,但是事件只能定义在类的内部,只允许在当前类中调用。所以说,事件是一种类型安全的委托。
定义事件
通过一个简单的场景来演示下事件的使用。
/// <summary>
/// 音乐播放器
/// </summary>
public class MusicPlayer
{
//step01:定义 音乐播放结束 事件
public event EventHandler<EventArgs> PlayOverEvent;
public string Name { get; set; }
public MusicPlayer(string name)
{
this.Name = name;
}
//step02:定义一个触发事件的方法
public void PlaySong()
{
//模拟播放
Console.WriteLine("正在播放歌曲:" + this.Name);
for (int i = 0; i < 20; i++)
{
Console.Write(".");
Thread.Sleep(100);
}
//播放结束,则触发PlayOverEvent事件
if (PlayOverEvent != null)
{
PlayOverEvent(this, null);
}
}
}
public class Program
{
static void Main(string[] args)
{
//创建音乐播放器对象
MusicPlayer player = new MusicPlayer("自由飞翔");
//step03:注册事件
player.PlayOverEvent += player_PlayOverEvent;
//播放歌曲,结束后触发事件
player.PlaySong();
Console.ReadKey();
}
static void player_PlayOverEvent(object sender,EventArgs e)
{
MusicPlayer player = sender as MusicPlayer;
Console.WriteLine("\r\n{0}播完了!", player.Name);
}
}
上面代码运行结果:
事件使用的几个步骤:
- 用
event
关键字定义事件,事件必须要依赖一个委托类型。 - 在类内部定义触发事件的方法。
- 在类外部注册事件并引发事件。
上述代码在MusicPlayer
类定义了一个事件成员PlayOverEvent
,我们说事件依赖于委托、是委托的特殊实例,所以EventHandler<EventArgs>
肯定是一个委托类型,转到定义验证下。
EventHandler
是微软封装好的事件委托,该委托没有返回值类型,有两个参数,sender
一般指的是事件所在类的实例,TEventArgs
事件参数,如果有需要创建,要显示继承System.EventArgs
。
事件的本质
MusicPlayer player = new MusicPlayer("自由飞翔");
//注册事件
player.PlayOverEvent += player_PlayOverEvent;
player.PlaySong();
从上面代码我们观察到,事件要通过+=
符号来注册。我们猜想,事件是不是像多播委托一样通过Delegate.Combine
方法可以绑定多个方法?还是通过反编译工具查看下。
我们看到PlayOverEvent
事件内部生成了两个方法:add_PlayOverEvent
和remove_PlayOverEvent
。
add_PlayOverEvent
方法内部调用Delegate.Combine
把事件处理方法绑定到委托列表。remove_PlayOverEvent
方法内部调用Delegate.Remove
从委托列表上移除指定方法。
其实,事件本质上就是一个多播委托。