C# 委托和事件详解
引言
委托和事件在C#中是一个重要的概念,但是对于很多刚接触C#编程的小伙伴来说可能不太友好,很抽象,本文我会由浅入深的讲讲什么是委托、为什么要使用委托、事件的由来、委托和事件及对Observer设计模式。有些地方可能讲的不好或是有错误的话,请见谅
让方法作为方法的参数
先不要管这个小标题是如何的抽象和拗口,先写下面两个非常简单的语句。
public void Eat(string name)
{
DogEat(name);
}
public void DogEat(string name)
{
Console.WriteLine(name + "吃骨头");
}
语句很简单,当传递动物名,如“狗”,在Eat函数方法中,将会调用DogEat方法,再次传递name参数,DogEat向屏幕输出“狗吃骨头”。
现在假设要写个猫的吃东西的方法。
public void CatEat(string name)
{
Console.WriteLine(name + "吃鱼");
}
这时候,Eat也要改一改了,要判断使用哪个吃东西的方法合适(狗吃骨头,猫吃鱼),使用枚举来实现判断的依据。
public enum Animal{
Dog,Cat
}
public void Eat(string name,Animal _animal)
{
switch (_animal)
{
case Animal.Dog:
DogEat(name);
break;
case Animal.Cat:
CatEat(name);
break;
}
}
OK,这样问题虽然解决了,但是这个解决方案的拓展性却很差,后面如果说还有鸡吃大米,松鼠吃松果,鱼吃小虾米,这使得需要不断的修改Eat()方法,去适应新的需求。
在考虑新的方法前,先看看Eat的方法签名:
public void Eat(string name,Animal _animal)
看 string name,string 是参数类型,name 是参数变量,当我们赋给name"狗",就表示"狗",赋值"猫"就表示"猫"这个值。哎,这尼玛不是废话吗,这™谁不会啊,我刚学程序就会了。
一样的道理,有没有一种方法像 string name 的流程一样,我先不管参数类型,当给变量赋值DogEat的时候,代表的是DogEat()的方法,赋值CatEat时,代表CatEat()的方法。就将这个变量命名为AnimalEatFood吧,使用这个变量好似与给name赋值是一样的,只不过它代表的是方法?好像确实是这样的,在使用Eat()这个方法的时候,给AnimalEatFood这个参数也附上值,得到想要的方法。AnimalEatFood是一个方法(如DogEat、CatEat),在Eat方法体内使用这个方法,如:
public void Eat(string name,### AnimalEatFood) { AnimalEatFood(name); }
这里的 ### 代表的应该是方法的参数类型,但是目前的问题是不知道这个代表着方法的AnimalEatFood参数是个什么类型?
Note:这里不需要使用枚举或是if什么区分方法了,因为不管是枚举或是什么,其作用是为了区分使用哪个方法,但是AnimalEatFood已经代表了使用哪个方法!!!
想必各位彦祖和亦非肯定想到了,这里引出委托来解决问题。委托的英文是Delegate,与其翻译成委托,我个人更倾向于理解成代表,推出一个代表来代表想要使用到的方法。
AnimalEatFood的 参数类型定义 应该可以使其能够确定所代表的方法,或许有一点点难懂,但可以想想String类型接受的是带引号("")的值,bool类型是true和false,这里AnimalEatFood变量的参数类型的作用和String类型和bool类型是一样的。委托的出现:定义了AnimalEatFood参数所能代表的方法,也就是AnimalEatFood的参数类型。(有点拗口,但是可以类比String类型、int类型想想,你或许可以更好的理解这个概念 ^_^!!!)
现在开始委托的定义:
public delegate void AnimalFoodDelegate(string name);
public void DogEat(string name);
和DogEat()方法签名对比,除了加上了delegate关键字外,基本上是一样的。
接着改写Eat()方法。
public void Eat(string name, AnimalFoodDelegate AnimalEatFood) { AnimalEatFood(name); }
现在附上整个的示例代码:
namespace test
{
public delegate void AnimalFoodDelegate(string name);
class AnimalEat
{
public static void DogEat(string name)
{
Console.WriteLine(name + "吃骨头");
}
public static void CatEat(string name)
{
Console.WriteLine(name + "吃鱼");
}
private static void Eat(string name, AnimalFoodDelegate AnimalEatFood)
{
AnimalEatFood(name);
}
static void Main(string[] args)
{
Eat("狗", DogEat);
Eat("大黄",CatEat);
}
}
}
委托是一个类,定义了方法的类型,实现让方法当作方法的参数,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序有更好的可拓展性。
将方法绑定到委托
当然在上面的例子中不一定要直接在Eat()方法中给 name、AnimalEatFood赋值,可以像这样使用(因为string和AnimakFoodDelegate的地位是一样的):
static void Main(string[] args) { string name1, name2; AnimalFoodDelegate food1, food2; name1 = "狗"; name2 = "猫"; food1 = DogEat; food2 = CatEat; Eat(name1, food1); Eat(name2, food2); }
如预期的一样,程序没有一点问题,按计划一样输出。
但是委托不同于string的一个特性是:可以将多个方法赋给同一个委托,或者将多个方法绑定到同一个委托,当调用这个委托时,依次调用其所绑定的方法。
static void Main(string[] args) { AnimalFoodDelegate food1; food1 = DogEat; // 先给委托类型的变量赋值 food1 += CatEat; // 给此委托变量再绑定一个方法 Eat("大黄", food1); }
输出:
委托可以绑定一个方法,也应该有办法可以取消对方法的绑定,很容易知道是"-=":
static void Main(string[] args)
{
AnimalFoodDelegate food1 = new AnimalFoodDelegate(DogEat);
food1 += CatEat; //给委托再绑定一个方法
Eat("狗",food1);
Console.WriteLine();
//输出:
//狗吃骨头
//狗吃鱼
food1 -= CatEat; //取消对CatEat方法的绑定
if (food1 != null)
{
Eat("狗", food1);
}
//输出:狗吃骨头
}
使用委托可以将多个方法绑定到同一个委托变量上,调用此变量会依次执行所有绑定的方法。
事件由来
之前的程序中,为了方便,我们将所有的方法都写在一个类中,但是在实际应用中,会将Eat写在一个类中,DogEat、CatEat写在另一个类中。因此会这样写:
namespace test
{
public delegate void AnimalFoodDelegate(string name);
//EatManager类
public class EatManager
{
public void Eat(string name, AnimalFoodDelegate AnimalEatFood)
{
AnimalEatFood(name);
}
}
class AnimalEat
{
public static void DogEat(string name)
{
Console.WriteLine(name + "吃骨头");
}
public static void CatEat(string name)
{
Console.WriteLine(name + "吃鱼");
}
static void Main(string[] args)
{
EatManager em = new EatManager();
em.Eat("大黄", DogEat);
em.Eat("小咪", CatEat);
}
}
}
如同预期的那样输出:
大黄吃骨头
小咪吃鱼
多个方法绑定到委托变量也很简单,如下:
static void Main(string[] args) { EatManager em = new EatManager(); AnimalFoodDelegate food1; food1 = DogEat; food1 += CatEat; em.Eat("大黄", food1); } //输出: //大黄吃骨头 //大黄吃鱼
刚才我们干了一件什么事,基于面向对象设计,就是写了一个EatManager类,有一个Eat的方法。使用的时候先实例化一个对象,然后还是按照原来的样子,声明委托类型的变量,赋值、绑定方法等等操作实现需求。那么,将这个委托变量封装到EatManager类中似乎也可以。像这样:
public class EatManager
{
//内部声明委托变量
public AnimalFoodDelegate? food1;
public void Eat(string name, AnimalFoodDelegate AnimalEatFood)
{
AnimalEatFood(name);
}
}
static void Main(string[] args)
{
EatManager em = new EatManager();
em.food1 = DogEat;
em.food1 += CatEat;
em.Eat("大黄",em.food1);
}
但是问题有来了,我觉得这样很难受
em.Eat("大黄",em.food1);
调用em.Eat方法时,再次传递了em的food1字段,我不想这样。
不妨按如下操作进行修改:
public class EatManager
{
//内部声明委托变量
public AnimalFoodDelegate food1;
public void Eat(string name)
{
if(food1 != null) //如果有方法注册委托变量
{
food1(name); //food1是一种方法,通过委托调用方法
}
}
}
static void Main(string[] args)
{
EatManager em = new EatManager();
em.food1 = DogEat;
em.food1 += CatEat;
em.Eat("大黄");
}
这里达到了我们的预期效果,但是还有存在的问题:
什么问题呢?food1和string类型的变量没什么区别。
之前food1字段修饰的是public,这会使得可以对其随意进行赋值,操作,破坏了对象的封装性。
如果改为private会怎么样?会看不见、访问不到,这不搞笑吗,呵呵。
要是food1不是一个委托变量而是一个string类型,就可以使用属性对字段进行封装了。
于是,Event就可以解决问题了,它封装了委托类型的变量,使得在类内部,总是private,类的外部,注册"+="和注销"-="的访问修饰符与声明事件时使用的访问修饰符相同。
public class EatManager { //定义委托,定义了可以代表的方法类型 public delegate void AnimalFoodDelegate(string name) //声明一个事件 public event AnimalFoodDelegate food1; public void Eat(string name) { //if (food1 != null) // food1(name); food1?.Invoke(name); } }
event
关键字用于声明一个事件,类似于声明一个进行了封装的委托变量。事件的本质是一个特殊类型的委托,但通过 event
关键字限制了外部代码的访问权限,确保只有事件所在类(发布者)可以触发它,而外部代码只能订阅或取消订阅。
em.food1 = DogEat;就会出现编译错误
委托、事件和观察者设计模式
例子:这里举一个马里奥吃金币的例子。当马里奥吃到一个金币,金币数就会加1,当金币达到100时,马里奥的生命会加1,然后重新计数。
马里奥吃到一个金币,金币数加1,那怎么通知金币数显示呢?
这里先了解一下Observer设计模式,Observer设计模式中主要包括两类对象:
Subject:监视对象,它包含着其他对象所感兴趣的内容。这里的马里奥就是一个监视对象,我对这个小家伙吃到多少个金币感兴趣,每当他吃到一个金币,界面金币显示就会加1。
Observer:监视者,监视Subject,当Subject中的某事件发生时,会告知Observer,而Observer会采取相应的行动。界面金币显示就是Observer。
在这个例子中,事情发生顺序如下:
1、UI金币显示对于马里奥的金币数感兴趣(注册)
2、马里奥吃到一个金币,UI显示金币加1
Observer设计模式是为了定义对象间的一种一对多的依赖关系,以便于当一个对象的状态改变时,其他依赖于它的对象会被自动告知并更新。
using System;
// 定义委托类型,用于表示金币数量变化的通知
public delegate void CoinChangedHandler(int newCoinCount);
// 马里奥类
public class Mario
{
// 定义事件,使用CoinChangedHandler委托
public event CoinChangedHandler CoinChanged;
// 金币数量属性
private int _coinCount;
public int CoinCount
{
get => _coinCount;
private set
{
_coinCount = value;
// 触发事件,通知订阅者金币数量变化
OnCoinChanged(_coinCount);
}
}
// 构造函数
public Mario()
{
_coinCount = 0;
}
// 吃金币的方法
public void EatCoin()
{
CoinCount++; // 增加金币数量
}
// 触发事件的方法
protected virtual void OnCoinChanged(int newCoinCount)
{
// 检查是否有订阅者,如果有则触发事件
CoinChanged?.Invoke(newCoinCount);
}
}
// UI类
public class UI
{
// 构造函数,传入马里奥对象并订阅事件
public UI(Mario mario)
{
mario.CoinChanged += UpdateCoinDisplay; // 订阅事件
}
// 事件处理方法,更新金币显示
private void UpdateCoinDisplay(int newCoinCount)
{
Console.WriteLine($"UI: Mario has {newCoinCount} coins.");
}
}
// 测试类
public class Program
{
public static void Main()
{
Mario mario = new Mario(); // 创建马里奥对象
UI ui = new UI(mario); // 创建UI对象并订阅马里奥的事件
// 模拟马里奥吃金币
mario.EatCoin(); // 输出:UI: Mario has 1 coins.
mario.EatCoin(); // 输出:UI: Mario has 2 coins.
mario.EatCoin(); // 输出:UI: Mario has 3 coins.
}
}
详细的使用还是得多多练习啊………
!!!