• C# 委托和事件 与 观察者模式(发布-订阅模式)讲解 by天命


    使用面向对象的思想 用c#控制台代码模拟猫抓老鼠

    我们先来分析一下猫抓老鼠的过程

    1.猫叫了

    2.所有老鼠听到叫声,知道是哪只猫来了

    3.老鼠们逃跑,边逃边喊:"xx猫来了,快跑啊!我是老鼠xxx"

    一  双向耦合的代码

    首先需要一个猫类Cat 一个老鼠类Rat 和一个测试类Program

    老鼠类的代码如下

    //老鼠类
    public class Rat
    {
        public string Name { get; set; } //老鼠的名字
        public Cat MyCat { get; set; } //老鼠遇到的猫
    
        //老鼠逃跑的方法
        public void Run()
        {
            Console.WriteLine(MyCat.Name +
                "猫来了,大家快跑!!我是" + Name);
            //打印出信息 包含了猫的名字和老鼠本身的名字
        }
    
        //带参和无参构造
        public Rat() { }
        public Rat(string name, Cat cat)
        {
            this.Name = name;
            this.MyCat = cat;
        }
    }

    要让猫叫的时候依次打印出老鼠的逃跑方法,需要在Cat类里添加一个存放Rat对象的集合

    Cat类的代码如下

    public class Cat
    {
        public string Name { get; set; } //猫的名字
        List<Rat> list = new List<Rat>(); //存放Rat对象的集合
    
        //为集合增加老鼠
        public void Add(Rat rat)
        {
            list.Add(rat);
        }
    
        //移除
        public void Remove(Rat rat)
        {
            list.Remove(rat);
        }
    
        //猫叫的方法
        public void Shout()
        {
            Console.WriteLine("喵喵喵!");
            //如果集合非空,循环执行每只老鼠的Run()方法
            if (list != null)
            {
                foreach (Rat item in list)
                {
                    item.Run();
                }
            }
        }
    
        public Cat() { }
        public Cat(string name)
        {
            this.Name = name;
        }
    }

    在Main方法中,我们需要构建几个Rat对象和一个Cat对象,将Rat对象添加到Cat对象的集合中,调用Cat对象的Shout方法

    代码如下

    static void Main(string[] args)
    {
        //构建一个Cat对象和两个Rat对象 老鼠遇到的猫是构建的cat
        Cat cat = new Cat("Tom");
        Rat rat1 = new Rat("Jerry", cat);
        Rat rat2 = new Rat("TaoQi", cat);
    
        //调用猫类的Add方法添加老鼠对象
        cat.Add(rat1);
        cat.Add(rat2);
    
        //调用猫的Shout方法
        cat.Shout();
    
        Console.ReadKey();
    }

    运行结果如下

     

    这样的代码缺陷很明显,Cat类和Rat类紧密耦合

    猫可能不止要抓老鼠 还要抓小鸟

    当然不止是猫会抓 也可能狗拿耗子多管闲事

    于是我们可以把猫和狗提出来 继承自一个抽象类Pet

    抓捕的小动物老鼠和小鸟没有什么关系 但是都能(逃)跑

    先不去管小鸟是飞,我们把它们称作 可以跑的 都实现一个IRunable接口

    二  观察者 模式(发布-订阅模式)

    修改后的代码如下

    新增抽象类Pet ,猫类继承自Pet   (猫类的代码变化不大 略去不写

    public abstract class Pet
    {
        public List<IRunable> list = new List<IRunable>();
        public void Add(IRunable i)
        {
            list.Add(i);
        }
    
        public void Remove(IRunable i)
        {
            list.Remove(i);
        }
    
        public abstract void Shout();
    }

    接口IRubable 里面定义一个Run方法

    public interface IRunable
    {
    void Run();
    }

    老鼠Rat和鸟Bird两个类都实现了这个接口

    以Bird为例 代码如下

    class Bird : IRunable
    {
        //鸟的名字和遇到的猫
        public string Name { get; set; }
        public Cat MyCat { get; set; }
    
        public void Run()
        {
            Console.WriteLine(MyCat.Name + "猫来了,快跑吧!我是小鸟" + Name);
        }
    
        public Bird() { }
        public Bird(string name, Cat cat)
        {
            this.Name = name;
            this.MyCat = cat;
        }
    }

    Rat类的代码几乎没有变化

    那么在Main方法中也只需要稍作修改,增加一个Bird对象 略去不写 执行后的结果如下

     以上猫抓老鼠的例子实际上就是用了一个设计模式:观察者模式

    观察者模式又名发布-订阅模式(Publish-Subscribe)

    1.Subject类 (通知者 主题)
    //抽象类 里面有一个Observer类集合
    把所有对观察者对象的引用保存在一个聚集里,每个主题都可以有多个观察者.抽象主题提供一个接口,可以增加和删除观察着对象

    2.Observer类 (观察者)
    //抽象类
    抽象观察者,为所有的具体观察者定义一个接口,在得到主题的更新时提醒自己

    3.ConcreteObserver类
    //父类是Observer类
    具体观察者,实现抽象观察者角色所需求的更新接口,以便使本身的状态与主题的状态相协调

    4.ConcreteSubject类
    //父类是Subject
    具体主题,将有关状态存入具体观察者对象,在具体主题的内部状态改变时,给所有登记过的观察者发出通知

    观察者模式的特点

    1.一个主题可以有任意数量依赖他的观察者,一旦主题的状态发生改变,所有观察者都可以得到通知
    2.主题发出通知不需要知道具体观察者
    3.具体观察者也不需要知道其他观察者的存在

    但是

    将一个系统分割成一系列相互作用的类有一个很不好的副作用,就是需要维护相关对象间的一致性,使得各个类紧密耦合,这样会给维护,扩展和重用都带来不便

    应用时机:

    当一个对象的改变需要同时改变其他对象的时候使用观察模式
    不知道具体有多少对象有待改变时,应考虑使用观察者模式
    一个抽象模型有两个方面,其中一个方面依赖于另一个方面

    三 委托

    举个栗子 正如之前所说,老鼠会跑Run,小鸟会飞Fly,这根本是两个毫不相干的方法

    但是的确有相同点--它们的返回值类型都是空,传进的参数列表也都为空

    我们怎么样能把这两个不相关的类Bird和Rat的对象都装到Cat中去,再判断是哪个类依次调用它们的方法?

    其实我们可以直接拿出它们的方法来装到Cat中去.

    //委托
    委托是什么?
    和类一样是一种用户自定义类型
    委托提供了方法的抽象

    委托存储的是一系列具有相同签名和返回值类型的方法的地址,调用委托的时候,委托包含的所有方法将被执行

    1.定义委托
    访问修饰符 delegate 返回值类型 委托名(参数列表);

    public delegate void MyDel(int x);

    2.声明和初始化
    委托名 委托对象名;
    委托对象名=new 委托名(类名.方法名);

    MyDel del;
    del=new MyDel(Cat.Shout);

    委托名=方法名
    del=Cat.Shout;

    3.委托的运算
    委托可以使用额外的运算符来组合.这个运算最终会创建一个新的委托,其调用列表是两个操作数的委托调用列表的副本连接.委托是恒定的,操作数委托创建后不会被改变,委托组合拷贝的是操作数的副本
    MyDel del2=Cat.Catch;
    MyDel del3=del+del2;

    使用+=为委托新增方法
    使用-=为委托移除方法
    del+=Cat.Catch;
    del-=Cat.Shout;

    4.委托调用

    if(del!=null){
        del();//委托调用
    }

    5.匿名方法
    匿名方法是在初始化委托时内联声明的方法
    delegate(参数){代码块}

    delegate int MyDel(int x);
    MyDel del=delegate(int x){
        return x;
    };

    //匿名方法不声明返回值

    Lambda表达式
    用来简化匿名方法的语法

    MyDel del1 = (int x) => {return x;};

    在参数列表和匿名方法之间放置Lambda运算符=>
    Lambda运算符读作 goes to

    MyDel del1 = x => {return x};
    MyDel del1 = x => x;

    定义委托像定义枚举一样,可以定义在类的外部

    然后在类中可以创建委托的对象

    比如 修改后的我们的程序

    (只是一个测试 不写的很详细了 还是拿猫和老鼠举例子 猫类和老鼠类 以及定义的委托的代码如下)

    //定义一个委托 名叫CatShoutEventHandler 没有返回值 参数列表为空
    public delegate void CatShoutEventHandler();
    
    //猫类
    public class Cat
    {
        //在猫类里定义一个该委托的对象CatShout
        public CatShoutEventHandler CatShout;
    
        public void Shout()
        {
            Console.WriteLine("喵喵喵");
            //判断委托内是否为空,若不为空,执行该委托
            if (CatShout != null)
            {
                CatShout();
            }
        }
    }
    
    //老鼠类
    public class Rat
    {
        public string Name { get; set; } //老鼠的名字
    
        //老鼠的逃跑方法
        public void Run()
        {
            Console.WriteLine(Name + "跑了!");
        }
    
        //无参和带参构造
        public Rat() { }
        public Rat(string name)
        {
            this.Name = name;
        }
    }

    那么接下来就是我们的Main方法

    static void Main(string[] args)
    {
        //构建一个猫对象和两个老鼠对象
        Cat cat = new Cat();
        Rat rat1 = new Rat("Jerry");
        Rat rat2 = new Rat("TaoQi");
    
        //向cat的委托对象CatShout中依次添加老鼠对象的Run方法
        //注!!添加的是整个方法 不需要加括号
        cat.CatShout += rat1.Run;
        cat.CatShout += rat2.Run;
    
        //调用cat的Shout方法
        cat.Shout();
        Console.ReadKey();
    }

    运行结果是这样的

    然而 然而 然而

    比如说我来做一个很贱的操作

    在Main方法中来一个 cat.CatShout=null;

    好了 不管Cat类中的CatShout有没有初始值或者有没有赋值过都没了

    我们知道面向对象的的三大特征 封装,继承,多态

    我们一样可以把委托封装起来,以控制它的访问权限

    这就是事件

     

    四 事件

    事件(Event)是一个用户操作,或是一些特定的出现情况.应用程序需要在事件发生时响应事件

    事件在类中声明且生成,且通过使用同一个类或者其他类中的委托与时间处理程序关联

    声明事件
    在类的内部声明事件,首先必须声明该事件的委托类型

    public delegate void CatShoutEventHandler();

    然后声明事件本身,使用 event 关键字

    public event CatShoutEventHandler CatShout;

    上面的代码定义了一个名为CatShoutEventHandler的委托和一个CatShout的事件,该事件在生成时会调用委托

    声明事件后可以实例化事件,注册函数到事件解除事件函数注册方法

    CatShout+=new CatShoutEventHandler(rat.Run);
    CatShout+=rat2.Run;//将函数Run注册到事件CatSHout上

     在使用事件的时候,如果在封装类的外部,则该事件只能出现在+=或-=的左边

    所以 用事件封装过的代码我们再来一遍

    额..好像顺理成章的就写了下去

    代码就这里变了一行 唯一的变化就是多了个event关键字

    我们在这里使用了一个高大上的工具 reflector

    将我们的代码反编译之后,可以看到CatShout这个事件其实做了两件事

    里面有两个方法 一个是add_CatShout(CatShoutEventHandler)  另一个是remov_Cat(CatShoutEventHandler)

    分别对应这个事件的运算符号 +=  和 -=

    所以其实事件就是相当于对委托的封装

    五 Object sender和EventArgs e

    然而我又要找问题了,我们翻回去看要求

    有一点是老鼠知道猫的名字,要调用猫对象的Name属性,我们现在试着给猫加上这个属性

    public string Name { get; set; }

    我们要排除Cat类与Rat类的耦合,所以不能在Rat类中存放一个Cat对象

    当然我们可以在老鼠的Run方法中增加传进去一个Cat对象,但是这样需要定义一个这个程序自己使用的委托类型

    系统已经有一些定义好的委托类型

    public delegate void Action();
    public delegate void EventHandler(object sender, EventArgs e);

    第一个委托叫Action 它是没有参数 没有返回值类型的

    第二个叫做EventHandler 它有两个参数 Object类型的sender 和 EventArgs类型的e

    第一个还好,第二个,我去,这是什么东西啊?

    其实第二个这个委托类型我们都十分熟悉 因为在winForm窗体应用程序中,控件生成的方法就带着这两个参数 

    我们一般把触发事件的整个对象封装成Object类型 做第一个参数

    而第二个参数呢 我们首先需要知道什么是EventArgs

        [Serializable, ComVisible(true), __DynamicallyInvokable]
        public class EventArgs
        {
            [__DynamicallyInvokable]
            public static readonly EventArgs Empty = new EventArgs();
        }

    这是代码 首先我们知道了,这是个类

    是系统定义的类,里面只有一个静态的readonly的自身变量 为空

    也就是通过调用这个静态方法来返回一个新的(空的)EventArgs本身

    要弄懂它,我们来先看一个例子

    这是WinForm窗体中的TreeView的事件对应在窗体类中生成的方法

     private void tvList_AfterSelect(object sender, TreeViewEventArgs e)

    我们可以看到 诶 诶 里面传进去的第一个参数没错是object sender 然而第二个变成了TreeViewEventArgs类型的e

    那这个TreeViewEventArgs是什么东西呢

    首先没错的是它继承自EventArgs

    然后它加多了许多方法,我们知道的是它可以返回我们当前选中的那个节点(而sender则代表的是整个TreeView控件)

    sa,我们可以把要用到的一些属性封装到EventArgs之中 来排除两个类的耦合

    应用到猫和鼠的例子中去,我们最后的代码是这样子的

    //自定义的CatShoutEventArgs类,继承自EventArgs
    //用作保存Cat对象的Name属性,还可以扩展其他的功能
    public class CatShoutEventArgs : EventArgs
    {
        public string CatName { get; set; }
        public CatShoutEventArgs(string name)
        {
            this.CatName = name;
        }
    }
    
    //定义一个委托 名叫CatShoutEventHandler 
    public delegate void CatShoutEventHandler
                (object sender,CatShoutEventArgs e);
    
    //猫类
    public class Cat
    {
        public string Name { get; set; } //猫的名字
    
        //在猫类里定义一个事件CatShout,返回值类型是定义的委托
        public event CatShoutEventHandler CatShout;
    
        public void Shout()
        {
            Console.WriteLine("喵喵喵");
            //判断委托内是否为空,若不为空,执行该委托
            if (CatShout != null)
            {
                //new一个CatShoutEventArgs类,传入参数是自身的Name
                CatShoutEventArgs e = new CatShoutEventArgs(Name);
                //执行CatShout事件,传入自身和e
                CatShout(this, e);
            }
        }
    
        //无参和带参构造
        public Cat() { }
        public Cat(string name)
        {
            this.Name = name;
        }
    }
    
    //老鼠类
    public class Rat
    {
        public string Name { get; set; } //老鼠的名字
    
        //老鼠的逃跑方法
        public void Run(object sender, CatShoutEventArgs e)
        {
            //打出一句话,包括了猫的名字和老鼠的名字
            Console.WriteLine(e.CatName + "来了! " + Name + "跑了!");
        }
    
        //无参和带参构造
        public Rat() { }
        public Rat(string name)
        {
            this.Name = name;
        }
    }

    嗯 然后Program类中的Main方法并不需要改动

    为了视觉效果,我再放上来一遍

    static void Main(string[] args)
    {
        //构建一个猫对象和两个老鼠对象
        Cat cat = new Cat("Tom");
        Rat rat1 = new Rat("Jerry");
        Rat rat2 = new Rat("TaoQi");
    
        //向cat的委托对象CatShout中依次添加老鼠对象的Run方法
        //注!!添加的是整个方法 不需要加括号
        cat.CatShout += rat1.Run;
        cat.CatShout += rat2.Run;
    
        //调用cat的Shout方法
        cat.Shout();
        Console.ReadKey();
    }

    好了 好了 最后的最后 看看我们程序的运行效果吧

    by天命 2016.11.9

  • 相关阅读:
    团队项目冲刺第五天
    团队项目冲刺第四天
    团队项目冲刺第三天
    团队项目冲刺第二天
    团队项目冲刺第一天
    团队任务命题
    java报错the superclass was not found 解决方案
    Buildings 分类: ACM 多校 2015-07-23 22:09
    1009 数字1的数量 分类: 51nod 2015-07-20 21:44 3人阅读 评
    1284 2 3 5 7的倍数 分类: 51nod 2015-07-18 22:06 6人阅读
  • 原文地址:https://www.cnblogs.com/swordtm/p/6049184.html
Copyright © 2020-2023  润新知