委托(四) "委托与事件的区别" 以及 "观察者模式"

🌴前言

最开始知道委托和事件是在 SiKi 的 C# 课程上(很抱歉, 大学就只上了 C 语言课, Java 课听了一节...😅), 由于之前没有接触过类似的语法, 又或是学习单片机的时候 C 语言语法在脑海中根深蒂固了 , 听课时就像是在听天书, 再加上之后也没有使用过这方面的语法, 基本全忘记了. 之前一段时间简单的了解了一下委托和事件, 然后网上都是用 "观察者模式" 去讲的...然而文章中连观察者都没有仔细去讲...所以这样更晕了好嘛...😑 于是自己抽时间多看了几篇文章, 做一下总结. 先说一下观察者模式吧, 毕竟委托和事件的区别用观察者去讲真的十分合适!

🍁声明

阅读下文需要对 "委托" 有基本了解, 如果未达到此基本要求, 请先打怪升级, 达到要求后再来挑战本副本! 推荐练级副本:

  1. 委托(一) 委托的基本用法

  2. 委托(二) 委托与回调和回调函数

  3. 委托(三) 委托的初始化

👀观察者模式

"模式" 是什么?

  • "模式" 这个东西和之前所学的大部分东西都不是一回事, "模式" 是一种套路, 对, 你没有听错, 套路, 或者说模板! 它是走在我们前面的那些码神们常年编写代码所总结出来的一种在特定场合下特别好用的代码模板. 就像是英语作文模板指导我们以怎样的结构去写英语作文一样, "模式" 是指导我们以一种怎样的结构去编写代码, 以达到减小编程时工作量以及减小日后维护成本的效果.

观察者模式的内容是什么?

  1. 需要描述的是一种 "一对多" 的依赖关系!

  2. 当 "一" 的一方状态发生改变时, "多" 的一方中的全部成员都能够收到通知!

  3. "多" 的一方收到通知后, 全部的成员都会自动进行更新操作, 而非被动!

这就是 "观察者模式" 了, "一" 的一方叫做 "被观察者", "多" 的一方叫做 "观察者", 也叫做: "发布-订阅模型", 这时分别叫做 "发布器" 和 "订阅器".

  • Q: 为什么没有代码?
  • A: 因为模式只是一种套路, 一种模板, 一种思想, 自然是没有代码的! 只要代码是按照这个模板去编写的, 就可以说使用了 "观察者模式".

观察者模式的代码实现

接下来就是代码实现环节了, 一共写了 3 种 链表, 委托, 事件 代码实现, 都是使用的 csharp 语言.

Q: 需要记忆的代码是哪个呢?

A: 没有! 对, 就是没有! 真正要记忆的是前面 观察者模式的内容是什么 中所提到的 3 点. 看完代码后可不要本末倒置了哦!

下面代码实现的场景就是当被观察者使用 "发令枪" 开枪时, 观察者们全部开跑~

链表实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using System;
using System.Collections.Generic;

namespace Exercise
{
/// <summary>
/// 被观察者
/// </summary>
public class Publisher_List
{
// 保存所有的观察者对象
private readonly List<Subscriber_List> subscriberList = new List<Subscriber_List>();

// 进入赛道
public void EnterGame(Subscriber_List subscriber)
{
subscriberList.Add(subscriber);
}

// 发令枪
public void StartingGun()
{
Console.WriteLine("预备~ . . . 砰! \n");

// 遍历所有的观察者 (这一步就是在通知每一个观察者)
foreach (Subscriber_List t in subscriberList)
{
t.Run(); // 自动更新
}
}
}

/// <summary>
/// 观察者
/// </summary>
public class Subscriber_List
{
// 运动员姓名
private readonly string name;

// 初始化
public Subscriber_List(Publisher_List publisher, string name)
{
this.name = name;
publisher.EnterGame(this);
}

// 开跑
public void Run()
{
Console.WriteLine(name + " 开跑啦~ \n");
}
}

public static class Program_List
{
public static void Main()
{
Publisher_List publisherList = new Publisher_List();
new Subscriber_List(publisherList, "艾莉");
new Subscriber_List(publisherList, "克里斯");

publisherList.StartingGun();
Console.ReadKey();
}
}
}
1
2
3
4
5
6
7
// 程序输出:
预备~ . . . 砰!

艾莉 开跑啦~

克里斯 开跑啦~

上面就是一个最简单的观察者模式, 使用链表实现.

特性 实现方式
1. 一对多 被观察者只有一个实例, 使用链表保存所有的观察者实例
2. 全部通知 在方法中遍历所有的观察者, 每一个都进行特定的操作
3. 自动更新 在 "被观察者" 中调用 "观察者" 的方法, 对外界隐藏调用逻辑

再来看一下 Main 方法:

1
2
3
4
5
6
7
8
9
public static void Main()
{
Publisher_List publisherList = new Publisher_List();
new Subscriber_List(publisherList, "艾莉");
new Subscriber_List(publisherList, "克里斯");

publisherList.StartingGun();
Console.ReadKey();
}

此时对外界的 Main 方法而言, 我仅仅开了一下 "发令枪", 艾莉和克里斯中的 Run 方法就 自动 被调用了. 如果不这么写, Main 方法就必须先调用发令枪方法, 然后再调用艾莉和克里斯的 Run 方法.

委托实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using System;

namespace Exercise
{
public static class Program
{
public static void Main()
{
Publisher publisher = new Publisher();
new Subscriber(publisher, "艾莉");
new Subscriber(publisher, "克里斯");

// 发令枪
publisher.StartingGun();
Console.ReadKey();
}
}

/// <summary>
/// 被观察者
/// </summary>
public class Publisher
{
// 委托, 保存所有的观察者
public Action PublisherDelegate;

// 发令枪
public void StartingGun()
{
Console.WriteLine("预备~ . . . 砰! \n");
PublisherDelegate?.Invoke(); // 判断 PublisherDelegate 是否是空, 如果不是空, 则调用里面的 Invoke 方法.
}
}

/// <summary>
/// 观察者
/// </summary>
public class Subscriber
{
// 运动员姓名
private readonly string name;

/// <summary>
/// 构造器
/// </summary>
/// <param name="publisher">发布器</param>
/// <param name="name">订阅器名称</param>
public Subscriber(Publisher publisher, string name)
{
this.name = name;
publisher.PublisherDelegate += Run; // 自动注册
}

// 开跑
public void Run()
{
Console.WriteLine(name + " 开跑啦~ \n");
}
}
}
1
2
3
4
5
6
7
// 程序输出:
预备~ . . . 砰!

艾莉 开跑啦~

克里斯 开跑啦~

使用委托实现观察者模式与使用链表时的不同

  1. 不再使用链表保存所有的观察者, 而是使用委托的多播特性进行保存.

  2. 通知所有观察者的步骤不再使用遍历, 而是使用委托的多播特性进行逐个通知.

  3. 注册观察者时不再使用链表的 Add() 方法, 而是使用委托的 +=-=.

直接使用委托实现观察者模式有几个不安要素:

  1. 如果被观察者 Publisher 中的委托是 public 修饰, 那么外部便可以直接访问, 此时如果外部代码中使用了 = 进行委托的注册, 那么委托中已有的注册将被全部清空, 这种隐患是十分恐怖的.

  2. 如果被观察者 Publisher 中的委托是 public 修饰, 那么外部便可以直接调用此委托, 此时观察者们便无法收到任何通知, 这个隐患同样是致命的.

使用事件实现观察者模式便解决了上面两个致命隐患.

事件实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using System;

namespace Exercise
{
public static class Program
{
public static void Main()
{
Publisher publisher = new Publisher();
Subscriber subscriberA = new Subscriber(publisher, "艾莉");
Subscriber subscriberB = new Subscriber(publisher, "克里斯");

// 发令枪
publisher.StartingGun();
Console.ReadKey();

// 对事件直接赋值 (不允许)
publisher.PublisherEvent = subscriberA.Run;
// 对委托直接赋值 (允许)
publisher.PublisherDelegate = subscriberA.Run;

// 外部直接调用事件 (不允许)
publisher.PublisherEvent();
// 外部直接调用委托 (允许)
publisher.PublisherDelegate();

Console.ReadKey();
}
}

/// <summary>
/// 被观察者
/// </summary>
public class Publisher
{
// 事件, 保存所有的观察者
public event Action PublisherEvent;
public Action PublisherDelegate;

// 发令枪
public void StartingGun()
{
Console.WriteLine("预备~ . . . 砰! \n");
PublisherEvent?.Invoke(); // 判断 PublisherEvent 是否是空, 如果不是空, 则调用里面的 Invoke 方法.
}
}

/// <summary>
/// 观察者
/// </summary>
public class Subscriber
{
// 运动员姓名
private readonly string name;

/// <summary>
/// 构造器
/// </summary>
/// <param name="publisher">发布器</param>
/// <param name="name">订阅器名称</param>
public Subscriber(Publisher publisher, string name)
{
this.name = name;
publisher.PublisherEvent += Run; // 自动注册
publisher.PublisherEvent = Run;
}

// 开跑
public void Run()
{
Console.WriteLine(name + " 开跑啦~ \n");
}
}
}

上面的代码直接粘贴到 VS 中其实是报错的, 其中第 18 行, 第 23 行以及第 65 行会报错! 报错信息是一样的:

CS0070 事件 "Publisher.PublisherEvent" 只能出现在 += 或 -= 的左边(从类型 "Publisher" 中使用时除外)

从这里也就可以得出结论, 委托和事件的不同.

🍀委托和事件的异同

  1. 一个使用 public 修饰的委托实例, 在声明类的外部可以直接调用. 而事件即使修饰为 public, 也仅能在声明类内部调用, 外部调用时编译器会直接报错, 发现隐患.

  2. 委托可以使用 = 进行赋值, 但是事件不可以, 无论任何时候, 事件都仅能使用 += 和 -= 进行注册, 使用 = 时编译器会直接报错, 发现隐患.

  3. 除此以外, 委托和事件一致.

🌈参考链接

  • 菜鸟教程 - 观察者模式

  • Graphic Design Patterns - 观察者模式

  • C# 委托与事件区别简单总结