C# 委托、事件和Observer设计模式

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.
     }
 }

详细的使用还是得多多练习啊………

!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值