Unity/C#基础复习(5) 之 浅析观察者、中介者模式在游戏中的应用与delegate原理

0
17

参考资料

【1】 《Unity 3D脚本编程 使用C#语言开发跨平台游戏》陈嘉栋著
【2】 @张子阳【C#中的委托和事件 – Part.1】 http://www.tracefact.net/tech/009.html
【3】 @张子阳【C#中的委托和事件 – Part.2】 http://www.tracefact.net/tech/029.html
【4】 @毛星云【《Effective C#》提炼总结】提高Unity中C#代码质量的22条准则 https://zhuanlan.zhihu.com/p/24553860
【5】 《游戏编程模式》 Robert Nystrom著

基础知识

  1. C#中使用delegate关键字来便捷地完成类似回调函数的机制。

疑难解答

  1. 观察者模式是什么?它在游戏中的应用场景有哪些?
  2. 中介者模式的应用场景?
  3. delegate关键字为我们做了什么?
  4. event与delegate的关系是?

观察者模式

概述

观察者模式是一种定义了对象之间一对多关系的模式。当被观察者改变状态时,它的所有观察者都会收到通知并做出响应。有时也可以将这种关系理解为发布/订阅的模式,即我们所关心的对象(即被观察者)发布消息时,所有订阅该消息的对象都会进行响应,从而做出某些操作。

在《Unity3D脚本编程》一书中有一个很棒的例子来解释这个模式,它是这样描述的[1]

  • 报刊的任务是出版报纸。
  • 顾客可以向报刊订阅报纸。
  • 当报刊出版报纸后,所有订阅该报纸的人都会收到。
  • 当顾客不需要时,也可以取消订阅,取消后,顾客就不会收到报社出版的报纸。
  • 报刊和顾客是两个不同的主体,只要报社存在,不同的订阅者可以订阅或取消订阅。

其中的报社就是我们说的被观察者(Subject),而订阅者则是观察者(Observer)。

何时使用观察者模式?

设计模式不是银弹,所有设计模式都有一个最适合他们的应用场景,滥用设计模式会造成代码冗余严重,后续想要修改更是会力不从心。显然,观察者模式应该也是有他最佳的应用场景的。

感觉目前网上介绍观察者模式在游戏开发中的应用的解释都显得不那么明朗,这里博主结合自己的经验来试着谈谈在哪些场合下,使用观察者模式可以得到不错的效果。当然,目前up还是个初学C#和设计模式的小萌新,可能存在说错或者纰漏的情况,如果大家发现了还请不吝赐教,我会超级感激不尽的!!!

在我看来,观察者模式就是一个将A依赖B的关系转变为B依赖A的关系的模式。所以是否使用观察者模式的第一点,我认为应该是,判断AB之间的依赖关系,并判断出对于哪个对象来说,他更不能容忍在代码中出现依赖多个对象的情况。

这么说可能有点抽象,下面结合具体的例子来看看。

应用场景1–判断AB依赖关系

当游戏中的某个单位HP下降时,它表现在UI上的生命条也应该按比例改变宽度(或高度等属性)。而单位的生命值改变在游戏中是一件非常常见的事情。比如,当单位收到伤害时,甚至是单位释放技能时(点名DNF大红神)。

那么,如果不使用观察者模式,我们可能会写出下面的代码。

import UI.HpUI;
// 游戏单位对象
class Character{

    // Character对象依赖于hp的UI,因为要主动通知该UI更新血条宽度
    HpUI hpView;

    int hp;
    // 收到伤害时执行的方法
    public void Damaged(int damage){
        this.hp -= damage;
        // 主动通知hpUI更新
        hpView.update();
    }

    // 释放技能时执行的方法
    public void SkillExecute(Skill skill){
        // 某些特殊技能需要消耗HP
        if(skill == xxx){
            Damaged(xxxx);
        }
    }
}

这样能不能完成目标呢,也可以,但是我们可以发现在游戏对象Character上,他依赖了Hp的UI,这显得特别突兀,如果后续还有MP的UI,人物属性(攻击力防御力等)的UI,那么Character对象全部都要引用一遍。

想象一下,当你开开心心创建了一个新的单位,想要让他去打怪时,发现,报错了。原因是没有给这个新单位添加UI的依赖,这时,就可以发现,每次新建游戏单位对象都需要为他添加所有UI的依赖。

实际上,游戏对象是不是必须要依赖UI呢,感觉也不是,单位就是单位,就算没有UI,他受伤了也会扣血也会死亡,不会说没有了UI就会报错,这个单位就死不了了(滑稽)。

那么,可以判断实际上如果是UI依赖游戏对象是不是更合理呢?UI没有了他绑定的游戏对象,当然就无法表现出特定的效果啦。

下面是使用观察者模式之后的代码。

// nowHp表示受伤后的血量,damage表示此次伤害数值
public delegate OnCharacterDamageHandler(int nowHp,int damage);

// 游戏单位对象
class Character{
    // 委托,当单位受伤时,向所有订阅这个事件的订阅者发送消息
    public OnCharacterDamageHandler onDamage;

    int hp;
    // 收到伤害时执行的方法
    public void Damaged(int damage){
        this.hp -= damage;
        if(onDamage!=null)
            // 发布消息——这个对象受伤了
            onDamage(hp,damage);
    }

    // 释放技能时执行的方法
    public void SkillExecute(Skill skill){
        // 某些特殊技能需要消耗HP
        if(skill == xxx){
            Damaged(xxxx);
        }
    }
}

class HpUI{
    // hp条,单位hp改变时,自动调整宽度
    Image hpbar;

    // UI依赖于游戏对象
    Character character;

    // 根据当前hp改变血条UI的方法
    private void update(int hp,int damage){...}

    // UI初始化的方法
    public void Init(){
        // 血条UI订阅单位受伤事件
        character.onDamage += update;
    }
}

这样一来,依赖关系就从Character依赖HpUI,变成了HPUI依赖Character,HpUI监听了Character的受伤事件,这样好不好呢?感觉见仁见智把,还是那句话,要判断AB对象谁更不能容忍依赖于其他对象。

应用场景1简单总结

大家可以发现,经过上面的操作,实际上对象之间的耦合并没有消失,只是从一种形式(A依赖B)变为了另一种形式(B依赖A)。

应用场景2——是否具备一对多的关系?

判断是否使用观察者模式,我认为,第二个要点是,消息的发布者和订阅者是否具备一对多的关系。

依旧以前面的受伤事件举例(onDamge),如果此时又有一个需求要求我们做一个成就系统,当单位第一次死亡时,显示出成就特效(类似于各类MOBA游戏的First Blood),那么我们还是可以不让Character对象依赖这个成就系统。

为什么呢?依旧是用onDamage发布消息,成就系统订阅这个消息呗。当单位受伤时,成就系统订阅受伤消息,从而判断单位的HP是否减少到了0,如果为0,那么就判断单位死亡,此时再判断单位是否是第一次死亡,再使用相应方法。

下面上伪代码。

class AchievementSystem{
    // 成就系统依赖于游戏对象
    Character character;

    // 根据对应事件触发first blood成就
    public void update(int nowhp,int damage){...}

    public void Init(){
        character.onDamage = update;
    }
}

可以看到我们甚至没有改动Character类一行,因为这本来就跟这个类没有太大关系~~~

如果接下来又有一个类似的需求呢?继续订阅就行。举个例子,如果策划此时要求做一个单位血量减少到20%以下,增加防御力的被动技能,怎么做呢?

依旧是由被动技能订阅受伤事件,判断生命值是否到达20%以下,如果是,那么触发技能效果。

简单总结

上述情况就是我所认为的一对多情况,也就是一个消息发布出来,他会有多个订阅者,每个订阅者都会做出不同的响应,这时就可以考虑使用观察者模式消除一部分耦合(并不能完全消除)。

中介者模式

概述

如果说观察者模式只能消除一部分耦合,那么中介者模式就是可以完全消除两个对象的依赖情况。在《游戏编程模式》一书中,对中介者模式(也常被称为服务定位型模式)的定义如下[5]

为某服务提供一个全局访问入口来避免使用者与该服务的具体实现类之间产生耦合。

在我看来,中介者模式的主要作用就是将蜘蛛网式的引用关系变为N个对象依赖于中介者,中介者为这N个对象提供服务,也就是将原本多对多的关系变成了多对一,一对多的关系。这样说起来可能有点抽象,下面举一个具体的例子来说明一下。

在游戏中,我们经常要制作一种提示型的UI,它经常要做以下这几件事:

  1. 在玩家购买物品时,判断玩家的资源是否足够,如果不够,提示玩家
  2. 玩家释放技能时,判断玩家mp是否足够,如果不够,提示玩家
  3. 当玩家想要进行某种被禁止的操作时,提示玩家,如攻击无敌的敌人,对友军释放技能等等
    …..
    诸如此类,就不一一列举了,这种UI还是挺常见的。他会零散的分布在游戏系统的各个角落,可能在另一个UI上操作时,采取了某种操作,他就要跳出来提示你。

如果不使用中介者模式,那么可能代码中就会出现如下引用关系:

如果使用中介者模式,那么依赖关系就可以转变成下面这样。

添加中介者后,所有原本直接引用(或间接引用)提示UI的对象,全都变成了直接引用中介者,当他们有消息要发布时(比如玩家做了某个不允许的操作),就直接向中介者发布消息。此时订阅了这个消息的tipsUI就可以自动获得这个消息并处理他。从而解耦了各个对象与TipsUI。

何时使用中介者模式?

前面说到,中介者模式可以最大限度的消除耦合,使对象的依赖关系之间变小。那么,是不是游戏里所有地方有依赖关系的地方,都可以用中介者模式呢?比如将所有观察者模式都替换成中介者模式。答案应该是:不能。前面说到,设计模式不是万能的,中介者模式有它的优点自然就会有它的缺点。

中介者模式本质上其实是与单例模式极其相似,仔细观察前面的案例,其实我们也可以用静态方法(类)或单例模式来解决。不过对于Unity的MonoBehavior类来说有点特殊,这个继承自MonoBehavior的TipsUI对象并不能设置成静态,不过我们也能将TipsUI设置为单例模式来解决它零散地分布在游戏系统这一问题。

中介者模式的一大缺点就在于:他将耦合变得不直观。阅读使用中介者模式的代码,往往难以理解谁依赖他,这也是他的性质决定的,中介者并不知道谁会发布消息(即并不知道服务被谁定位),我们需要满代码的找谁向中介者发布了消息,谁又处理了这个消息。这与直接的对象引用比起来,更不容易看出来。

根据《游戏编程模式》一书所说,中介者模式的第一个应用场景应该是这样的[5]

当手动将一个对象传来传去显得毫无理由或者使得代码难以阅读时,可以尝试考虑使用这个设计模式。毕竟将一个环境属性传递10层函数以便让一个底层函数能够访问,这会使代码增加毫无意义的复杂度。

在游戏开发中,感觉有几个鲜明的例子可以说明这点,比如音频管理,音频需要在任何需要它的地方播放,但是如果把音频管理器对象在每份代码之间传来传去就显得毫无理由,这时,就可以考虑中介者模式(或单例模式)。

除此之外还有许多的例子,比如日志系统,前面提到的提示UI等等。

中介者模式与单例模式的区别?

前面提到中介者模式与单例模式极其相似,他们都是全局能够访问的对象。当我们使用其中之一的时候,应该考虑哪一个更适合需求。

在《游戏编程模式》中没有过多的讨论如何选择使用这两个模式,根据菜鸡博主这些年来单薄的项目经验和浅薄的认知,我认为中介者模式比之单例模式多了一层健壮性。

这是因为,有些在C#中的中介者模式是使用委托进行设计的,当订阅一个消息的时候,实际上就是向该委托+=一个响应方法,而发布消息时,实际上就是直接调用这个委托方法。

这样一来,如果我们用中介者模式设计音频管理器,那么就算此时我们音频管理器出错了,无法在游戏中播放声音,游戏也能正常运行,或者说,即使我们将音频管理器的代码全部删除,再写一个功能更强大的音频系统,只要这个新的音频系统响应的消息是一样的(与之前那个一样),那么这个中介者模式就依旧没有出错,依然能正常运行。

如何在C#中实现中介者模式

前面提到,中介者模式大多依靠委托实现,下面是基本的代码框架(参考自网上)。

public delegate void CallBack();
public delegate void CallBack<T>(T arg);
public delegate void CallBack<T, X>(T arg, X arg1);
public delegate void CallBack<T, X, Y>(T arg, X arg1, Y arg2);
public delegate void CallBack<T, X, Y, Z>(T arg, X arg1, Y arg2, Z arg3);
public delegate void CallBack<T, X, Y, Z, W>(T arg, X arg1, Y arg2, Z arg3, W arg4);

/// <summary>
/// 充当所有UI视图对象的中介者,单例类,具有两个功能
/// 
/// 1. 订阅事件:
///     向某种事件发起订阅,参数是一个delegate委托函数,
///     表示当某个事件发生后,调用该函数
/// 2. 发布事件:
///     发布事件相当于我们没有用中介者之前的的OnXXX委托,
///     在某个事件发生时,调用该中介者的发布事件方法,
///     用以调用所有订阅该事件的对象的方法
/// </summary>
public class MessageAggregator {

    // 单例类,使用饿汗模式加载,防止在多线程环境下出错
    public static readonly MessageAggregator Instance = new MessageAggregator();
    private MessageAggregator() { }

    private Dictionary<string, Delegate> _messages;

    public Dictionary<string, Delegate> Messages {
        get {
            if (_messages == null) _messages = new Dictionary<string, Delegate>();
            return _messages;
        }
    }

    /// <summary>
    /// 当订阅事件时调用
    /// </summary>
    /// <param name="string"></param>
    /// <param name="callback"></param>
    private void OnListenerAdding(string string,Delegate callback) {
        //判断字典里面是否包含该事件码
        if (!Messages.ContainsKey(string)) {
            Messages.Add(string, null);
        }
        Delegate d = Messages[string];
        if (d != null && d.GetType() != callback.GetType()) {
            throw new Exception(string.Format("尝试为事件{0}添加不同类型的委托,当前事件所对应的委托是{1},要添加的委托是{2}", string, d.GetType(), callback.GetType()));
        }
    }


    /// <summary>
    /// 当取消订阅事件时调用
    /// </summary>
    /// <param name="string"></param>
    /// <param name="callBack"></param>
    private void OnListenerRemoving(string string,Delegate callBack) {
        if (Messages.ContainsKey(string)) {
            Delegate d = Messages[string];
            if (d == null) {
                throw new Exception(string.Format("移除监听事件错误:事件{0}没有对应的委托", string));
            } else if (d.GetType() != callBack.GetType()) {
                throw new Exception(string.Format("移除监听事件错误:尝试为事件{0}移除不同类型的委托,当前事件所对应的委托为{1},要移除的委托是{2}", string, d.GetType(), callBack.GetType()));
            }
        } else {
            throw new Exception(string.Format("移除监听事件错误:没有事件码{0}", string));
        }
    }

    /// <summary>
    /// 无参的监听事件(即订阅事件)的方法
    /// </summary>
    /// <param name="string"></param>
    /// <param name="callBack"></param>
    public void AddListener(string string,CallBack callBack) {
        OnListenerAdding(string, callBack);
        Messages[string] = (CallBack)Messages[string] + callBack;
    }

    // 1参的监听事件(即订阅事件)的方法
    public void AddListener<T>(string string, CallBack<T> callBack) {...}

    // 2参的监听事件(即订阅事件)的方法
    public void AddListener<T,X>(string string, CallBack<T,X> callBack) {...}

    // 3参的监听事件(即订阅事件)的方法
    public void AddListener<T,X,V>(string string, CallBack<T,X,V> callBack) {...}

    // 4参的监听事件(即订阅事件)的方法
    public void AddListener<T,X,Y,Z>(string string, CallBack<T,X,Y,Z> callBack) {...}

    // 5参的监听事件(即订阅事件)的方法
    public void AddListener<T, X, Y, Z,W>(string string, CallBack<T, X, Y, Z,W> callBack) {...}

    // 无参的移除监听事件的方法
    public void RemoveListener(string string,CallBack callBack) {
        OnListenerRemoving(string, callBack);
        Messages[string] = (CallBack)Messages[string] - callBack;
    }

    // 1参的移除监听事件的方法
    public void RemoveListener<T>(string string, CallBack<T> callBack) {...}


    // 2参的移除监听事件的方法
    public void RemoveListener<T, X>(string string, CallBack<T, X> callBack) {...}

    // 3参的移除监听事件的方法
    public void RemoveListener<T, X, V>(string string, CallBack<T, X, V> callBack) {...}

    // 4参的移除监听事件的方法
    public void RemoveListener<T, X, Y, Z>(string string, CallBack<T, X, Y, Z> callBack) {...}

    // 5参的移除监听事件的方法
    public void RemoveListener<T, X, Y, Z,W>(string string, CallBack<T, X, Y, Z,W> callBack) {...}

    // 无参的广播监听事件
    public void Broadcast(string string) {
        Delegate d;
        if (Messages.TryGetValue(string, out d)) {
            CallBack callBack = d as CallBack;
            if (callBack != null)
                callBack();
            else
                throw new Exception(string.Format("广播事件错误:事件{0}对应委托有不同的类型", string));
        }
    }

    // 1参的广播监听事件
    public void Broadcast<T>(string string,T arg0) {...}

    // 2参的广播监听事件
    public void Broadcast<T,V>(string string, T arg0,V arg1) {...}

    // 3参的广播监听事件
    public void Broadcast<T,V,X>(string string, T arg0,V arg1,X arg2) {...}

    // 4参的广播监听事件
    public void Broadcast<T, V, X,Z>(string string, T arg0, V arg1, X arg2,Z arg3) {...}

    // 5参的广播监听事件
    public void Broadcast<T, V, X, Z,W>(string string, T arg0, V arg1, X arg2, Z arg3,W arg4) {...}
}

以前面的TipsUI为例子,我们下面实现在人物释放技能时如果mp不够,对玩家进行提示。

// 单位对象
class Character{
    int mp;
    // 释放技能
    public void ExecuteSkill(Skill skill){
        if(this.mp < skill.mp)
            // 向中介者发布施法mp不够的消息
            MessageAggregator.Instance.Broadcast< int,int >("ExecuteSkill",mp,skill.mp);
    }
}

// 提示UI对象
Class TipsUI{
    // 要提示的字符串信息
    private string tips;
    
    public void update(int nowmp,int skillmp){
        tips = string.format("当前mp为:%d,技能要消耗的mp:%d,mp不够,不能释放技能",nowmp,skillmp);
    }

    // 对各类消息进行订阅的方法
    public void Bind(){
        // 订阅施法的消息
        MessageAggregator.Instance.AddListener<int,int>("ExecuteSkill",update);
    }
}

这个时候,当玩家释放技能时,如果mp不够,就会触发提示UI出来提示不能释放技能。

delegate做了什么?

在C#中delegate是一个非常方便的关键字,它一般用于回调函数机制,利用它我们很容易就能写出低耦合的观察者模式。他有些类似与C++中的函数指针,但是相比于函数指针,C#中的委托天然支持多播委托(即有多个回调方法,也就是委托链),以及类型安全,写起来相当方便舒服。

那么delegate到底为我们做了什么呢?delegate关键字后面声明的到底是类型还是变量呢?老实说,博主初学委托的时候,经常会写出像下面这样傻傻的代码。

// 错误代码
class Main{

    delegate void Func();

    public void init(){
        // 为委托添加回调方法
        Func+=Func1;
    }

    void Func1(){}
}

大家发现了把~
菜鸟博主sjm经常把委托看成是函数指针一样的东西了,然后以为声明的是一个类似指针的变量,所以就直接指向目标方法了。这样当然就报错啦~~

这就是不了解delegate背后工作惹的祸~C#编译器在后面为我们做了相当多的工作,要理解委托,就得去看看由C#代码生成的中间代码IL。

下面是一个简单的使用委托的代码示例。

using System;
public delegate void Func(int a);

public class C {
    Func func;
    public void M() {
        func += aa;
        
        func(11);
    }
    void aa(int a){
        Console.WriteLine(a);
    }
}

我们可以去 https://sharplab.io 中查看C#代码生成的中间代码是什么样的。

由上面代码生成的IL(中间)代码大概是下面这样的。

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi sealed Func
    extends [mscorlib]System.MulticastDelegate
{
    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            object 'object',
            native int 'method'
        ) runtime managed 
    {
    } // end of method Func::.ctor

    .method public hidebysig newslot virtual 
        instance void Invoke (
            int32 a
        ) runtime managed 
    {
    } // end of method Func::Invoke

    .method public hidebysig newslot virtual 
        instance class [mscorlib]System.IAsyncResult BeginInvoke (
            int32 a,
            class [mscorlib]System.AsyncCallback callback,
            object 'object'
        ) runtime managed 
    {
    } // end of method Func::BeginInvoke

    .method public hidebysig newslot virtual 
        instance void EndInvoke (
            class [mscorlib]System.IAsyncResult result
        ) runtime managed 
    {
    } // end of method Func::EndInvoke

} // end of class Func

.class public auto ansi beforefieldinit C
    extends [mscorlib]System.Object
{
    // Fields
    .field private class Func func

    // Methods
    .method public hidebysig 
        instance void M () cil managed 
    {
        .....
        IL_0014: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
        .....
        IL_002b: callvirt instance void Func::Invoke(int32)
        ....
    } // end of method C::M

    .method private hidebysig 
        instance void aa (
            int32 a
        ) cil managed 
    {...} // end of method C::aa
} // end of class C

大家可以看到,神奇的事情发生了,在我们用delegate声明Func时,一个名为Func的类在本地被声明了!由此,我们也可以发现delegate的秘密,那就是,在delegate定义的地方,编译器会自动帮我们声明一个继承于MulticastDelegate(多播委托)的类型。事实上,如果继续追溯,我们还可以发现MulticastDelegate继承于Delegate,这个Delegate类有个Combine方法,主要用于将两份委托连接起来形成委托链。

因此,当我们以后看到delegate xxx时,可以自动将其等价为class xxx : MulticastDelegate。因为delegate是用于声明类型的,所以类型能用的修饰符他理论上也能用,如public、private、internal等等。

而对于这个新声明的类型,有两个方法是值得注意的,分别是他的构造方法和Invoke方法。

它的构造方法的签名如下:

.method public hidebysig specialname rtspecialname 
        instance void .ctor (
            object 'object',
            native int 'method'
        )

可以看到这里有两个参数,分别是一个object对象和一个整型,当委托添加了一个实例方法时,这个object就是该实例方法所操作的对象,而如果是静态方法,那么这个参数就为null。
而整型method可以看作是一个指针,指向了我们要调用的函数,即保存了该函数的地址。在《Unity3D脚本编程》中介绍该参数是运行时用来标识要回调的方法,是一个引用回调函数的句柄~

而Invoke方法顾名思义,就是用来调用回调函数的方法。

除此之外,delegate还有很多语法糖,比如说,当我们初始化时,不必填写object参数,编译器会自动帮我们完成。还有我们可以像调用一个正常方法一样调用委托,就像上面代码一样,我们声明了一个Func类型的委托变量func,可以像调用正常的方法一样通过func()来调用它。这其中编译器会将这种代码翻译为func.invoke(xxx);

event是什么?它与delegate关系是?

除了delegate外,我们还经常会看到像下面这样的代码。

public delegate OnCharacterDamageHandler(int nowHp,int damage);

// 游戏单位对象
class Character{
    // 委托,当单位受伤时,向所有订阅这个事件的订阅者发送消息
    public event OnCharacterDamageHandler onDamage;

    int hp;
    // 收到伤害时执行的方法
    public void Damaged(int damage){
        this.hp -= damage;
        if(onDamage!=null)
            // 发布消息——这个对象受伤了
            onDamage(hp,damage);
    }

    // 释放技能时执行的方法
    public void SkillExecute(Skill skill){
        // 某些特殊技能需要消耗HP
        if(skill == xxx){
            Damaged(xxxx);
        }
    }
}

class HpUI{
    // hp条,单位hp改变时,自动调整宽度
    Image hpbar;

    // UI依赖于游戏对象
    Character character;

    // 根据当前hp改变血条UI的方法
    private void update(int hp,int damage){...}

    // UI初始化的方法
    public void Init(){
        // 血条UI订阅单位受伤事件
        character.onDamage += update;
    }
}

阅读上述代码,可以发现event关键字似乎和delegate能达到同样的效果。那么这两个关键字到底有什么区别呢?

event的由来

这要从event的由来说起。我们已经知道了delegate关键字实际上是声明了一个类型,而Character类的内部则是声明了一个OnCharacterDamageHandler类型的变量,可以向这个变量添加或消除回调方法。

那么,onDamage作为类中的字段,就要考虑他访问修饰符的问题,我们知道类中的属性并不应该都是public,有些属性不应该暴露在外面。因为外部有可能会对这个属性做出一些奇怪的改动,比如将其赋值为null。

对于委托类型来说,实际上我们在外部所需要的操作只是+=和-=而已。

然而,当你真的想把onDamage改成private或internal时候,你会发现,这个变量根本不能改成除了public以外的访问修饰符。为啥呢?因为把它改成非public后,外部就不能该这个委托类型添加方法了啊!而我们设计委托,不就是为了能在外部向他注册方法么。如果把他设为private,那就相当于他完全失效了~

但是,我们又不想将onDamge设为public,因为"在客户端可以对它进行随意的赋值等操作,严重破坏对象的封装性"[2]

为此,C#提供event来解决这个问题。当我们使用event封装委托类型的变量时,该委托变量在外部只接受Add和Remove操作,而不能被随意的赋值,增强了安全性。

试着将上面的代码

character.onDamage += update;

更改为

character.onDamage = update;

可以发现编译错误~~

event做了什么?

public event OnCharacterDamageHandler onDamage;

那么,在上面这行语句中,到底发生了什么我们不知道的事呢?

依旧是老方法,看看生成的中间代码。

为了防止生成的中间代码过长,下面是一个简单的使用event的示例。

using System;
public delegate void Func(int a);

public class C {
    public event Func func;

    public void M() {
        func += aa;
        
        func(11);
    }
    void aa(int a){
        Console.WriteLine(a);
    }
}

他生成的中间代码如下所示。

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi sealed Func
    extends [mscorlib]System.MulticastDelegate
{...} // end of class Func

.class public auto ansi beforefieldinit C
    extends [mscorlib]System.Object
{
    // Fields
    .field private class Func func
    ...

    // Methods
    .method public hidebysig specialname 
        instance void add_func (
            class Func 'value'
        ) cil managed 
    {...} // end of method C::add_func

    .method public hidebysig specialname 
        instance void remove_func (
            class Func 'value'
        ) cil managed 
    {...} // end of method C::remove_func

    .method public hidebysig 
        instance void M () cil managed 
    {
        ...
        IL_000e: call instance void C::add_func(class Func)
        ...
        IL_0015: ldfld class Func C::func
        IL_001a: ldc.i4.s 11
        IL_001c: callvirt instance void Func::Invoke(int32)
        ...
    } // end of method C::M
    ...
    // Events
    .event Func func
    {
        .addon instance void C::add_func(class Func)
        .removeon instance void C::remove_func(class Func)
    }
} // end of class C

我略去了一部分与本次示例无关的代码。大家可以看到,在public event这一行中,虽然我们用的是public来声明委托变量,但最后编译器还是将其当做private变量,同时编译器还为类中增加了两个方法,分别是add_func和remove_func,用于向委托添加或删除方法。

这样,就相当于在类中封装了Func类型,仅仅暴露了增加和删除方法的接口给外部,增强了安全性~~

<

发布回复

请输入评论!
请输入你的名字