UNITY_委托和事件

UNITY_委托和事件

参考资料: Unity3D脚本编程-使用C#语言开发跨平台游戏-陈嘉栋

观察者模式

主题(Subject)管理某些数据,当主题的数据发生改变时,会通知已经注册(Register)的观察者(Observer),而这些已经注册的观察者会受到数据改变的通知并作出相应反应。

观察者模式定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。

Unity提供的机制:SendMessage()和BroadcastMessage()

缺点:
  过于依赖反射机制(reflection)来查找消息对应的被调用函数
  1. 频繁使用反射会影响性能
  2. 更会大大增加代码的维护成本 -- 字符串标识对应方法
  3. 能够调用private的方法 -- 若有一个是有方法在声明的类中没有被使用,那正常情况下都会把它认为是废代码从而删除,这时隐患就出现了

C#的委托机制

c#中提供的回调函数的机制便是委托(类型安全)

委托的使用

public class DelegateScript: MonoBehaviour{
    internal delegate void MyDelegate(int num); // 声明委托类型(参数列表+返回类型)\
    MyDelegate myDelegate; // 声明变量

    void Start(){
        myDelegate = PrintNum; // 给委托类型MyDelegate的实例赋值引用的方法
        myDelegate(50);

        myDelegate = DoubleNum;
        myDelegate(50);
    }

    void PrintNum(int num){
        ...
    }
    void DoubleNum(int num){
        ...
    }
}

这里myDelegate = PrintNum; 将一个方法"赋值"给了一个委托
  在c#2中为委托引入了方法组转换机制,支持从方法到兼容的委托类型的隐式转换
  之所以成为方法"组"转换,则是因为方法的重载

若有delegate void Delegate1(int num)
 和delegate void Delegate2(int num, int num2)
且有方法 void PrintNum(int num)
  和  void PrintNum(int num, int num2)
  则  myDelegate1 = PrintNum;
     myDelegate2 = PrintNum;
  向  myDelegate1或myDelegate2赋值时,都可以使用PrintNum作为方法组(因为重载了多个方法)
  而编译器会自动选择合适的重载

委托参数的逆变性
  逆变性: 可以是类型的基类
  即委托对应方法的参数可以是委托的参数类型的基类

委托返回类型的协变性
  协变性: 可以是类型派生出来的一个派生类
  即委托对应方法的返回类型可以是委托的返回类型的一个派生类

逆变性和协变性仅针对引用类型,若是值类型或void则不支持

委托的编译器内部实现机理

(略) -- p154~164

委托链

委托调用多个方法 -- 委托链
委托链是委托对象的集合 -- 可以利用委托链来调用集合中的委托所代表的全部方法

public class DelegateScript : MonoBehaviour {
    delegate void MyDelegate(int num);

    void Start(){
        MyDelegate myDelegate1 = new MyDelegate(PrintNum1);
        MyDelegate myDelegate2 = new MyDelegate(PrintNum2);
        MyDelegate myDelegate3 = new MyDelegate(PrintNum3);

        MyDelegate myDelegates = null;
        myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1);
        myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate2);
        myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate3);

        Print(10, myDelegates);
    }

    void Print(int num, MyDelegate md){
        if(md != null){
            md(value);
        }
    }

    void PrintNum1(int num) { Debug.Log("1 result Num: " + num); }
    void PrintNum2(int num) { Debug.Log("2 result Num: " + num); }
    void PrintNum3(int num) { Debug.Log("3 result Num: " + num); }
}

刚开始时myDelegates = null; 表示没有对应要回调的方法
第一次myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1);时myDelegates引用了myDelegate1所引用的委托实例
第二次时,myDelegates内部实现的_invocationList字段被初始化,并且_invocationList[0]指向和_invocationList[1]分别指向两个委托实例myDelegate1和myDelegate2
第三次是将一个委托实例myDelegate3合并到一个委托链中。编译器内部发生的与第二次的大同小异。需要注意的是,第二次得到的委托链中的_invocationList所引用的委托实例数组不再需要,被垃圾回收
将myDelegates变量(委托链)作为参数传入Print(),Print方法中的代码会隐式调用myDelegates所引用的委托实例的Invoke()方法,
  此时会执行一个循环来遍历_invocationList中的所有委托实例并按顺序调用每个委托实例中包装的回调方法,即PrintNum1(), PrintNum2()和PrintNum3()

对应Delegate.Combine(), 也提供了Remove()方法用于移除委托实例
  Remove()每次仅仅移除一个匹配的委托实例,而不是所有和目标委托实例匹配的委托实例
  myDelegates = (MyDelegate)Delegate.Remove(myDelegates, new MyDelegate(PrintNum2));

当Remove方法被调用时,会从后向前扫描myDelegates中的委托实例数组,并对比委托实例的_target和_methodPtr的值是否与需要Remove的对应字段值相同。
若匹配,则删除。如果委托实例数组为空,则返回null;如果委托实例数组长度为1,则直接返回那个委托实例;
否则,会创建一个新的委托实例(与Combine出委托链类似),对应的_invocationList会引用由删除了目标委托实例后剩余委托实例组成的委托实例数组。

C#编译器为委托类型的实例重载了 += 和 -= 操作符,对应Delegate.Combine()和Delegate.Remove()

事件

观察者模式可以通过事件机制来实现

C#中的事件机制是以委托作为基础的

一个定义了事件成员的类型需要提供这些功能来实现交互机制
  1. 方法能够订阅对某事件的关注
  2. 方法能够取消订阅对该事件的关注
  3. 事件发生时,订阅了该事件的方法会收到通知

实例

一个游戏单位(BaseUnit类)被攻击而掉血,那么掉血(OnSubHp事件)就可以被作为一个事件。
订阅了该事件的对象在游戏单位掉血时,会收到游戏单位掉血的通知。

具体需求:掉血时,需要显示掉血信息,掉血信息中有多个值。

思路:为了区分开游戏单位和显示信息的逻辑(降低逻辑的耦合性),将掉血信息的显示逻辑交给模块BattleInfoComponent
--> 即游戏单位的掉血事件OnSubHp发生时,通知BattleInfoComponent模块来处理显示功能。

实现:

1. 定义委托类型(回调方法原型) -- 事件是以委托为基础的

public delegate void SubHpHandler(BaseUnit source, float subHp, DamageType damageType, HpShowType showType);
-- source: 受伤害的单位;subHp: 伤害;damageType: 伤害方式;showType: 显示方式

2. 定义事件成员

使用关键字event定义事件成员
public event SubHpHandler OnSubHp;

表示事件OnSubHp的类型为SubHpHandler,意味着事件OnSubHp的所有订阅者都必须提供和SubHpHandler委托类型所确定的方法原型相匹配的回调方法,即void Method(BaseUnit .., float .., DamageType .., HpShowType ..);

3. 事件的触发

这里的BaseUnit可以视为一个基类,派生出比如英雄类、士兵类等。
因此,触发事件的方法可以定义为一个虚方法
本例中,OnSubHp事件是受到攻击而导致的,因此

protected virtual void OnBeAttacked(float damage, bool isCritical, bool isMissed){
    DamageType damageType = DamageType.Normal;
    HpShowType showType = HpShowType.Damege;
    if(isCritical) damageType = DamageType.Critical;
    if(isMissed) showType = HpShowType.Miss;
    // 如果有方法订阅了OnSubHp事件,则调用(通知)
    if(OnSubHp != null) {
        OnSubHp(this, damage, damageType, showType);
    }
}

而BaseUnit的派生类可以通过重写OnBeAttack()来控制事件的触发

优化:业务单一原则

OnBeAttack()方法应该仅仅用来触发事件
因此
BeAttack()方法用来将敌人的攻击伤害转化为掉血事件的触发

public void BeAttack() {
    bool isCritical = Random.value > 0.5f;
    bool isMissed = Random.value > 0.5f;
    float damage = 10000f;

    OnBeAttacked(damage, isCritical, isMissed);
} 

4. 事件的订阅和观察者的回调方法

之前提到的BattleInfoComponent类是用来进行伤害信息显示的
而伤害信息显示的时机是在BaseUnit受到伤害的时候
因此BattleInfoComponent需要订阅BaseUnit.OnSubHp事件。

public class BattleInfoComponent: MonoBehaviour {
    public BaseUnit baseUnit;
    ...

    private void AddListener () {
        // 订阅事件this.unit.OnSubHp
        this.unit.OnSubHp += new BaseUnit.SubHpHandler(this.OnSubHp);
        // 可简写为
        unit.OnSubHp += OnSubHp;
    }

    private void RemoveListener () {
        // 不要忘记取消事件的订阅
        this.unit.OnSubHp -= new BaseUnit.SubHpHandler(this.OnSubHp);
    }

    private void OnSubHp(BaseUnit source, float damage, DamageType dmgType, HpShowType showType) {
        // 实现伤害信息的显示功能
        Debug.Log(source.name + ....);
    }
}

c#的+=操作符可用于注册事件
  this.unit.OnSubHp += new BaseUnit.SubHpHandler(Method);
  可简写为
  this.unit.OnSubHp += Method;
  上述两行代码的内部在编译器内部其实都等效于
  this.unit.add_OnSubHp(new BaseUnit.SubHpHandler(this.OnSubHp));

  在订阅事件时,编译器内部需要调用事件的add_OnSubHp方法来向事件内部添加新的委托对象

取消回调事件的订阅也相似,使用-=操作符
  在编译器内部等效于
  this.unit.remove_OnSubHp(new BaseUnit.SubHpHandler(this.OnSubHp));

总结:

当UnitBase受到攻击时,执行UnitBase.BeAttack()
  --> UnitBase.OnBeAttacked()被调用
  --> 触发UnitBase.OnSubHp事件
  由于BattleInfoComponent.OnSubHp()方法订阅了UnitBase.OnSubHp事件
  --> 因此在UnitBase.OnSubHp事件触发时,BattleInfoComponent.OnSubHp()方法被调用
    而OnSubHp()方法会受到来自UnitBase.OnSubHp事件传来的参数source等
    基于这些参数,OnSubHp()方法得以将伤害信息显示出来

事件的编译器内部实现机理

(略) -- p169~172、175

优点:

在事件机制中,事件成员(OnSubHp)才拥有数据,
  这些数据不属于观察者,但是观察者需要依赖Subject的这些数据做出响应
  如果有很多不同的观察者通过订阅同一个Subject,Subject的数据变化而触发了事件时,所有观察者会受到相应通知

通过事件机制可以将对象之间的相互依赖降到最低 -- 松耦合
  观察者模式的意义也在于此,让主题和观察者之间实现松耦合的设计模式
  当两个对象之间松耦合,即使不清楚彼此的细节,也可以进行交互

在有新类型的观察者出现时,主题(Subject)的代码无需修改,而新类型只需要实现匹配的回调方法即可注册成观察者

原文地址:https://www.cnblogs.com/FudgeBear/p/10367495.html

时间: 2024-11-06 11:21:23

UNITY_委托和事件的相关文章

C#高级知识点概要(1) - 委托和事件

作者:linybo 要成为大牛,必然要有扎实的基本功,不然时间再长项目再多也很难有大的提升.本系列讲的C# 高级知识点,是非常值得去撑握的,不仅可以让你写代码时游刃有余,而且去研究和学习一些开源项目时,也不会显得那么吃力了. 希望大家记住,这里讲的所有的知识点,不仅仅是了解了就可以了,还要会灵活用,一定要多思考,撑握其中的编程思想. 本文讲的是委托和事件,这两个词可能你早就耳熟能详,但你是否真正撑握了呢? 本系列讲的C#高级知识点都是要求开发时能达到可以徒手写出来的水平(不依赖搜索引擎.找笔记等

委托与事件

委托在底层就是一个函数的指针,委托是事件的基础. 你可以传递引用类型.值类型.但是你有没有需要传一个方法呢?传方法的过程就是委托. 消息类: public class Message { /// <summary> /// 传引用类型 /// </summary> /// <param name="msg"></param> public static void Send(string msg) { Console.WriteLine(&

C#学习(一):委托和事件

预备知识 在学习委托和事件之前,我们需要知道的是,很多程序都有一个共同的需求,即当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知. 而发布者/订阅者模式可以满足这种需求.简单来说,在这种模式中,发布者定义了一系列程序的其他部分可能感兴趣的事件.其他类可以"注册",以便再这些事件发生时发布者可以通知它们.这些订阅者类通过向发布者提供一个方法来"注册"以获取通知.当事件发生时,发布者"触发事件",然后执行订阅者提交的所有事件.

C#中委托和事件

目 录 1.1 理解委托 2 1.1.1 将方法作为方法的参数 2 1.1.2 将方法绑定到委托 4 1.2 事件的由来 6 1.2.1 更好的封装性 6 1.2.2 限制类型能力 9 1.3 委托的编译代码 10 1.4 .NET 框架中的委托和事件 11 1.4.1 范例说明 11 1.4.2 Observer 设计模式简介 12 1.4.3 实现范例的Observer 设计模式 13 1.4.4 .NET 框架中的委托与事件 14 1.5 委托进阶 16 1.5.1 为什么委托定义的返回值

C#委托和事件定义和使用

委托 定义委托的语法和定义方法比较相似,只是比方法多了一个关键字delegate ,我们都知道方法就是将类型参数化,所谓的类型参数化就是说该方法接受一个参数,而该参数是某种类型的参数,比如int.string等等:而委托是将方 法参数化,说了上面的那个类型参数化之后,相信你也能猜到方法参数化的意思了,对,就是将方法作为一个参数传到一个委托中. 首先来看看声明委托的语句: public deletate void MyDelegate(); public:访问修饰符  delegate:关键字 

[转载]C#深入分析委托与事件

原文出处: 作者:风尘浪子 原文链接:http://www.cnblogs.com/leslies2/archive/2012/03/22/2389318.html 同类链接:http://www.cnblogs.com/SkySoot/archive/2012/04/05/2433639.html 引言 本篇文章将为你介绍一下 Delegate 的使用方式,逐渐揭开 C# 当中事件(Event)的由来,它能使处理委托类型的过程变得更加简单.还将为您解释委托的协变与逆变,以及如何使用 Deleg

[转载]C#委托和事件(Delegate、Event、EventHandler、EventArgs)

原文链接:http://blog.csdn.net/zwj7612356/article/details/8272520 14.1.委托 当要把方法作为实参传送给其他方法的形参时,形参需要使用委托.委托是一个类型,是一个函数指针类型,这个类型将该委托的实例化对象所能指向的函数的细节封装起来了,即规定了所能指向的函数的签名,也就是限制了所能指向的函数的参数和返回值.当实例化委托的时候,委托对象会指向某一个匹配的函数,实质就是将函数的地址赋值给了该委托的对象,然后就可以通过该委托对象来调用所指向的函

C#语法之委托和事件

从大学就开始做C#这块,也做C#几年了,最近又从ios转回.Net,继续做C#,之前也没有写博客的习惯,写博客也是从我做ios的时候开始的,现在既然又做回了.net,那就写点关于.Net的博客,可能在大牛眼里这些都是简单基础的,不过回过头看我当时初学的时候觉得委托事件是不容易理解的,我这里也是想着联系着OC,两者有比较的学习下.毕竟都是面向对象语言,思想是相通的. 委托在OC中类似block,都是指向一个函数,其实他没和C++的函数指针类似.但委托还是和函数指针不太一样,委托是完全面向对象的,是

C#综合揭秘——深入分析委托与事件

本篇文章将为你介绍一下 Delegate 的使用方式,逐渐揭开 C# 当中事件(Event)的由来,它能使处理委托类型的过程变得更加简单.还将为您解释委托的协变与逆变,以及如何使用 Delegate 使 Observer(观察者)模式的使用变得更加简单.在事件的介绍上,会讲述事件的使用方式,并以ASP.NET的用户控件为例子,介绍一下自定义事件的使用.最后一节,将介绍Predicate<T>.Action<T>.Func<T,TResult>多种泛型委托的使用和Lamb