扯一扯 C#委托和事件?策略模式?接口回调?

早前学习委托的时候,写过一点东西,今天带着新的思考和认知,再记点东西。这篇文章扯到设计模式中的策略模式,观察者模式,还有.NET的特性之一——委托。真的,请相信我,我只是在扯淡......html

场景练习

还记得这两我的吗?李雷和韩梅梅,他们见面在打招呼...假设李雷和韩梅梅在中文课上打招呼和英文可上打招呼方式不同。下面定义两个方法来表示打招呼:java

//中文方式打招呼
    public void ChineseGreeting(string name){
        Console.WriteLine("吃饭了吗?"+name);
    }

    //英文方式打招呼
    public void EnglishGreeting(string name){
        Console.WriteLine("How are you,"+name);
    }

如今,咱们选择哪一种打招呼的方式,由上中文课仍是英文课来决定。再定义一个变量 lesson 表示上的课程,则打招呼方法以下:算法

public void GreetPeople(string name){
        switch(lesson){
            case Chinese:
                ChineseGreeting(name);
                break;
            case English:
                EnglishGreeting(name);
                break;
            default:
                break;
        }
    }

刚入行的coder也很容易想到这样的代码来模拟该场景。上中文课,则lesson=Chinese ,若是上英文课,则 lesson=English ,你可能还会想到定义一个枚举去表示课程类型。很简单,完整的程序以及Test代码就不给出了。如今问题来了,假设之后又多了日文课,韩文课,俄语课,咱们还须要在该类中不断增长新的语种打招呼的方式,还须要增长枚举的取值(若是你使用了枚举定义课程的话),而且每增长一种,GreetPeople 方法还须要不断修改,这个类会愈来愈臃肿。显然,这个类违反了单一职责的原则,显得很混乱,不利于维护。
为了解决这个问题,咱们有请策略模式登场:设计模式

策略模式

先上代码:数据结构

public class English : IGreetPeople
    {
        public void IGreetPeople.GreetPeople(string name)
        {
            Console.WriteLine("How are you, " + name);
        }
    }

    public class Chinese : IGreetPeople
    {
        public void GreetPeople(string name)
        {
            Console.WriteLine("吃饭了吗?" + name);
        }
    }

    public class English : IGreetPeople
    {
        void IGreetPeople.GreetPeople(string name)
        {
            Console.WriteLine("How are you, " + name);
        }
    }

    public class Greeting
    {
        private IGreetPeople mGreetPeopleImpl;

        public Greeting() { }
        
        public Greeting(IGreetPeople greetPeople)
        {
            this.mGreetPeopleImpl = greetPeople;
        }

        public void SetGreetPeople(IGreetPeople greetPeople)
        {
            this.mGreetPeopleImpl = greetPeople;
        }

        public void GreetPeople(string name)
        {
            mGreetPeopleImpl.GreetPeople(name);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Greeting greeting1 = new Greeting(new Chinese());
            Greeting greeting2 = new Greeting();
            greeting2.SetGreetPeople(new English());

            greeting1.GreetPeople("李雷");
            greeting2.GreetPeople("HanMeimei");

            Console.ReadKey();

        }
    }

运行结果以下:并发

吃了饭了吗?李雷
    How are you,HanMeimei

这里定义了一个打招呼的接口,不一样的语种,分别实现这个接口。具体打招呼的方法,则调用打招呼的类 Greeting 里面的 GreetPeople 方法。在 Greeting 类里面将不一样的语种对象复制给它的 mGreetPeopleImpl 属性。这样每一个类的功能单一,对于不一样的语种打招呼方式,咱们只需在建立 Greeting 对象时给它的 mGreetPeopleImpl 属性附上对应的值。less


策略模式的结构:

-抽象策略类(Strategy):定义全部支持的算法的公共接口。 Context使用这个接口来调用某ConcreteStrategy定义的算法。
-具体策略类(ConcreteStrategy):以Strategy接口实现某具体算法。
-环境类(Context):用一个ConcreteStrategy对象来配置。维护一个对Strategy对象的引用。可定义一个接口来让Strategy访问它的数据。学习

策略模式的适用场景

• 许多相关的类仅仅是行为有异。 “策略”提供了一种用多个行为中的一个行为来配置一个类的方法。即一个系统须要动态地在几种算法中选择一种。
• 须要使用一个算法的不一样变体。例如,你可能会定义一些反映不一样的空间 /时间权衡的算法。当这些变体实现为一个算法的类层次时 ,可使用策略模式。
• 算法使用客户不该该知道的数据。可以使用策略模式以免暴露复杂的、与算法相关的数据结构。
• 一个类定义了多种行为 , 而且这些行为在这个类的操做中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。测试

策略模式是一种设计模式,核心思想是面向对象的特性。java,C# 等面向对象的语言均可以利用这种思想有效解决上面打招呼的场景问题。接下来要说的是 .NET 平台的一种特性——委托一样能够解决此类问题。为了更好的理解委托,先来简单讲讲接口回调。this

接口回调

所谓的接口回调,简单讲就是在一个类中去调用另外一个类的方法,在该方法中再回调调用者者自己的的方法。也许我表达的不够准备,来个实际例子说明吧:
假设李雷要作算术运算,要求两个数相除,李雷不会除法,它让韩梅梅帮他计算,韩梅梅算出结果以后,再由李雷来处理结果。上代码:

定义了一个接口

public interface ICalculateCallBack
    {
        void onSuccess(int result);
        void onFailed();
    }

    public class HanMeimei
    {

        private ICalculateCallBack callback;

        public HanMeimei(ICalculateCallBack callback)
        {
            this.callback = callback;
        }

        public void doWork(int num1, int num2)
        {
            if (num2 == 0)
            {
                callback.onFailed();
            }
            else
            {
                int result = num1 / num2;
                callback.onSuccess(result);
            }
        }
    }

     public class LiLei
    {

        HanMeimei mHanMeimei = new HanMeimei(new MyCalCallback());

        public void Calculate(int num1,int num2)
        {
            mHanMeimei.doWork(num1, num2);
        }

        class MyCalCallback : ICalculateCallBack
        {
            void ICalculateCallBack.onFailed()
            {
                Console.WriteLine("除数不能为0");
            }

            void ICalculateCallBack.onSuccess(int result)
            {
                Console.WriteLine("结果是:" + result);
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            new LiLei().Calculate(7, 2);
            Console.ReadLine();

            new LiLei().Calculate(3, 0);
            Console.ReadLine();
        }
    }

执行结果是

结果是:3

    除数不能为0

首先,咱们定义了一个接口,用来处理计算成功和计算失败(除数为0)的状况。李雷的 calculate 方法实际调用的是韩梅梅的 doWork 方法,将要计算的两个数传过去。而后,韩梅梅计算以后,在经过 ICalculatecallback 中的方法将结果返回给李雷。ICalculatecallback 中的方法是在它的实现类 MyCalCallback 中具体实现的(若是是java,能够不用定义这个类采用实现接口的匿名内部类的方式)。这个过程实际能够看作是回调李雷的方法。由于是李雷才是决定拿到计算结果干什么(这里只是纯粹地在控制台打印出来)的主角。这就是接口回调的整个过程,也很简单。
看到这里,你可能会问,这不是画蛇添足吗?彻底能够不定义这个 ICalculateCallback 接口,在李雷的类中定义一个 int 型的变量去接收调用韩梅梅的方法以后的返回值,而后拿着这个返回值,想干什么就干什么。没错,这个例子中的确能够这么作。试想一下,假设韩梅梅去计算结果的这个过程比较耗时,通常这种状况,咱们会开一个新线程去执行。因为并发过程当中,咱们并不知道韩梅梅何时会计算出结果,这时候接直接调用的方式就存在问题了,极可能你拿这计算结果去作处理的时候,发现计算尚未完成,而接口回调就能很好地解决这个问题。
好吧,这个场景和第一个场景并无什么关系,扯得有点远了,可是请放下砖头,由于我一开始就声明了,我只是在扯淡。

委托

委托是 .NET 平台的一种特性,能够好不夸张的说,不学会委托,等于不会 .NET。两年前我还有一部分工做须要用 C# 完成的时候,看过一些 C# 的书籍,不少入门的书籍都根本不会介绍委托和事件。看过一些博客什么的,当时感受本身学懂了委托和事件。两年过去了,虽然这两年没再继续使用 C# , 可是两年时间的积累,对面向对象思想理解显然比两年前更上了一个层次。再回过头来看委托和事件,我以为当时的理解可能真的很浅(也许再过两年,又会以为如今的理解很浅)。因此下面所说的委托只是基于我如今的理解,通俗点讲,就是又在扯淡。
回到第一个打招呼的场景,前面说不用策略模式也能够解决。解决问题的根本思想是咱们针对不一样的语种,调用了不一样的打招呼方式。试想,若是咱们可以把方法名做为参数传递给另外一个方法,虽然这听起来有点绕口,但这样咱们就知道该调用什么方法了。

//中文方式打招呼
    public void ChineseGreeting(string name){
        Console.WriteLine("吃饭了吗?"+name);
    }

    //英文方式打招呼
    public void EnglishGreeting(string name){
        Console.WriteLine("How are you,"+name);
    }

把这两个方法搬下来,下面假设有这样一个方法:

public void GreetPeople(String name, ???  XXXGreeting){
        XXXGreeting(name);
    }

这里 ??? 表示某种类型,形参 XXXGreeting 即表明要传入的 ChineseGreeting 或者 EnglishGreeting,参数传什么,实际就调用什么。这个不难理解,就是我前面说的把方法名做为参数传递进去,这样当是中文课的时候,我只须要调用

GreetPeople("李雷",ChineseGreeting);

当是英文课的时候,我就调用

GreetPeople("HanMeimei",EnglishGreeting);

代码很简洁,避免了每次去修改 if-else 或者 switch-case 。那么问题来了,这个 ??? 究竟是个什么类型。能看我扯到如今的人都能猜到——没错,就是委托。
能够把委托当作一种类型来看待。为了方便说明,仍是先上代码。完整代码以下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StrategyTest
{

    class Program
    {
        delegate void GreetingDelegate(String name);

        static void Main(string[] args)
        {
            GreetPeople("李雷", new Program().ChineseGreeting);
            GreetPeople("HanMeimei", new Program().EnglishGreeting);
            Console.ReadLine();
        }


        static void GreetPeople(String name, GreetingDelegate XXXGreeting)
        {
            XXXGreeting(name);
        }


        public void ChineseGreeting(String name)
        {
            Console.WriteLine("吃饭了吗?" + name);
        }

        public void EnglishGreeting(string name)
        {
            Console.WriteLine("How are you," + name);
        }

    }
}

运行结果和前面同样,

吃饭了吗?李雷
    How are you,HanMeimei

代码开始,利用 delegate 关键字,声明了委托类型:
delegate void GreetingDelegate(String name);
委托的声明,与方法的声明相似,带有任意个数的参数,而且有返回值,固然前面还能够加上相应的权限修饰符。须要注意的是,声明委托时的参数和返回值与咱们须要传入和使用的方法参数个数和类型以及返回值保持一致。好比这个例子中中文打招呼的方法和英文打招呼的方法,都接收一个字符串类型的参数,而且都没有返回值,那么咱们声明委托的时候,也须要接收一个字符串类型的参数,而且没有返回值。
从面相对象的角度来说,既然咱们把委托当作一种类型来看,那么咱们能够直接使用委托的实例对象去调用方法。
咱们修改代码:

class Program
    {
        delegate void GreetingDelegate(String name);

        static void Main(string[] args)
        {
            //GreetPeople("李雷", new Program().ChineseGreeting);
            //GreetPeople("HanMeimei", new Program().EnglishGreeting);

            GreetingDelegate greetingDelegate = new GreetingDelegate(new Program().ChineseGreeting);
            greetingDelegate("李雷");

            Console.ReadLine();
        }

        //static void GreetPeople(String name, GreetingDelegate XXXGreeting)
        //{
        //    XXXGreeting(name);
        //}

        public void ChineseGreeting(String name)
        {
            Console.WriteLine("吃饭了吗?" + name);
        }

        public void EnglishGreeting(string name)
        {
            Console.WriteLine("How are you," + name);
        }

    }

执行结果:

吃饭了吗?李雷

委托表明了一类方法,这里咱们就相似初始化一个类的对象同样,声明了委托 GreetingDelegate 的一个对象 greetingDelegate ,同时,给它绑定了一个方法 ChinsesGreeting 。给委托绑定方法,这个术语,就是委托最核心的内容。而且,咱们能够给一个委托同时绑定多个方法,调用的时候,会一次执行这些方法。给委托绑定方法使用 += 符号。再次修改代码:

class Program
    {
        delegate void GreetingDelegate(String name);

        static void Main(string[] args)
        {
            //GreetPeople("李雷", new Program().ChineseGreeting);
            //GreetPeople("HanMeimei", new Program().EnglishGreeting);

            GreetingDelegate greetingDelegate = new GreetingDelegate(new Program().ChineseGreeting);
            greetingDelegate += new Program().EnglishGreeting;
            greetingDelegate("李雷");
            Console.ReadLine();
        }

        //static void GreetPeople(String name, GreetingDelegate XXXGreeting)
        //{
        //    XXXGreeting(name);
        //}

        public void ChineseGreeting(String name)
        {
            Console.WriteLine("吃饭了吗?" + name);
        }

        public void EnglishGreeting(string name)
        {
            Console.WriteLine("How are you," + name);
        }
    }

此次执行的结果是:

吃饭了吗?李雷
    How are you,李雷

能够给委托绑定方法,固然也能够给委托解除绑定方法,用 -= 符号。咱们再加一行代码,把中文打招呼的方式给取消掉:

static void Main(string[] args)
{
    //GreetPeople("李雷", new Program().ChineseGreeting);
    //GreetPeople("HanMeimei", new Program().EnglishGreeting);
            
    GreetingDelegate greetingDelegate = new GreetingDelegate(new Program().ChineseGreeting);
    greetingDelegate += new Program().EnglishGreeting;
    greetingDelegate -= new Program().ChineseGreeting;
    greetingDelegate("李雷");
    Console.ReadLine();
}

执行结果是:

吃饭了吗?李雷
    How are you,李雷

What's the F**K?
结果为何没有变化,不是应该只有英文打招呼的方式吗?别急。多看一眼就会发现,咱们经过 -= 取消绑定的方法,并非以前初始化时绑定上的那个方法,咱们这里分别 new 了三个对象。因此咱们后来取消绑定的是一个新对象的方法,而这个方法根本就没有绑定过。再稍微修改一下:

static void Main(string[] args)
{
    //GreetPeople("李雷", new Program().ChineseGreeting);
    //GreetPeople("HanMeimei", new Program().EnglishGreeting);

    Program p = new Program();
    GreetingDelegate greetingDelegate = new GreetingDelegate(p.ChineseGreeting);
    greetingDelegate += p.EnglishGreeting;
    greetingDelegate -= p.ChineseGreeting;
    greetingDelegate("李雷");
    Console.ReadLine();
}

此次的执行结果就对了:

How are you,李雷

这就是委托最基本的应用了,委托能够实现接口回调同样的效果,被调用的方法能够回调调用者的方法,具体的方法体实现由调用者本身实现。下面可能会想到这样作,声明委托的时候,不绑定方法,而后所有经过 += 来绑定方法,这样看起来,彷佛更协调。理想中的代码应该想下面这样:

static void Main(string[] args)
{
    //GreetPeople("李雷", new Program().ChineseGreeting);
    //GreetPeople("HanMeimei", new Program().EnglishGreeting);

    Program p = new Program();
    GreetingDelegate greetingDelegate = new GreetingDelegate();
    greetingDelegate += p.ChineseGreeting;
    greetingDelegate += p.EnglishGreeting;
    greetingDelegate -= p.ChineseGreeting;
    greetingDelegate("李雷");
    Console.ReadLine();
}

很遗憾,这样的代码不能经过编译,报错:GreetingDelegate不包含采用0个参数的构造方法。 而下面的代码是能够的:

static void Main(string[] args)
{
    //GreetPeople("李雷", new Program().ChineseGreeting);
    //GreetPeople("HanMeimei", new Program().EnglishGreeting);

    Program p = new Program();
    GreetingDelegate greetingDelegate;
    greetingDelegate = p.ChineseGreeting;
    greetingDelegate += p.EnglishGreeting;
    greetingDelegate("李雷");
    Console.ReadLine();
}

面向对象三大特性之一就是封装性,该私有的私有,该公开的公开。 好比咱们自定义一个类的时候,通常字段采用 private 修饰,不让外面引用该字段,外界须要操做该字段,咱们会根据须要添加相应公开的 get 和 set 方法。显然上面这段代码违背了对象的封装性。为了解决这个问题,C# 中引入了一个新的关键字 event ——对应概念,事件。

事件

能够说事件是对委托的一种拓展。事件实现了对委托对象的封装。说明事件以前,先来了解下 .Net Framework 的关于委托和事件的编码规范要求:
委托类型的名称都应该以EventHandler结束。
委托的原型定义:有一个void返回值,并接受两个输入参数:一个Object 类型,一个 EventArgs类型(或继承自EventArgs)。
事件的命名为 委托去掉 EventHandler以后剩余的部分。
继承自EventArgs的类型应该以EventArgs结尾。

为了更好地说明这个事件的做用,而且以要求的规范命名,下面换一个例子来讲明问题,以.NET的事件驱动模型来讲明问题吧,如今要作的事是,点击一个按钮,而后打印出相关的信息。
首先,定义一个 MyButton 的类。代码以下:该类

namespace EventTest
{
    //定义一个委托类型,用来调用按钮的点击事件
    public delegate void ClickEventHandler(Object sender,MyEventArgs e);

    //定义MyButton类
    public class MyButton
    {
        //定义委托实例
        public ClickEventHandler Click;

        //定义按键的信息
        private string msg;

        public MyButton(string msg)
        {
            this.msg = msg;
        }

        //按键的点击方法
        public void OnClick()
        {
            if (Click != null)
            {
                Click(this,new MyEventArgs(msg));
            }
        }
    }
}

再看,MyEventArgs 是什么:

namespace EventTest
{
    public class MyEventArgs:EventArgs
    {
        private string msg;

        public MyEventArgs(string msg)
        {
            this.msg = msg;
        }

        public string getMsg()
        {
            return this.msg;
        }
        
        public void setMsg(string msg)
        {
            this.msg = msg;
        }

    }
}

MyEventArgs继承自 EventArgs。接下来再看,咱们的主程序:

namespace EventTest
{
    class Program
    {
        static void Main(string[] args)
        {
            MyButton mb = new MyButton("A");
            mb.Click = new ClickEventHandler(Button_Click);
            mb.OnClick();
            mb.Click(mb, new MyEventArgs("封装性很差"));
            Console.ReadLine();
        }

        private static void Button_Click(Object sender, MyEventArgs e)
        {
            Console.WriteLine(e.getMsg());
        }
    }
}

输出结果以下:

A
    封装性很差

个人本意是好比,mb 这个对象表明 A 键,执行 OnClick 方法,打印出 A,这里咱们看到了,下面竟然能够绕过 MyButton 的 OnClick 方法,直接执行了委托所绑定的方法,咱们能够随意更改这个委托。
下面将 event 这个关键字,在声明委托实例以前加上。

namespace EventTest
{
    public delegate void ClickEventHandler(Object sender,MyEventArgs e);

    public class MyButton
    {
        public event ClickEventHandler Click;
        private string msg;

        public MyButton(string msg)
        {
            this.msg = msg;
        }

        public void OnClick()
        {
            if (Click != null)

            {
                Click(this,new MyEventArgs(msg));
            }
        }
    }
}

而后代码立马报错了,错误信息以下:


这个错误信息很明确,事件只能给它绑定方法和接触绑定方法,不能直接赋值,也不可以再直接调用了。改正错误后的代码以下:

namespace EventTest
{
    class Program
    {
        static void Main(string[] args)
        {
            MyButton mb = new MyButton("A");
            mb.Click += Button_Click;
            mb.OnClick();
            Console.ReadLine();
        }

        private static void Button_Click(Object sender, MyEventArgs e)
        {
            Console.WriteLine(e.getMsg());
        }
    }
}

这时候运行结果只有 A

到这里,event 的做用也就一目了然了。你那么还有最后一个疑问,定义委托的时候,这两个参数的做用,其中第二个参数 EventArgs 已经用到,能够体会一下。第一个参数 Object 是什么,有什么做用。其实这个例子中彻底能够只用一个string类型的参数,这么写是为了按照委托的原型写法来写。为了进一步说明问题,咱们在 MyButton 类中增长一个方法,并修改主程序代码:

namespace EventTest
{
    public delegate void ClickEventHandler(Object sender,MyEventArgs e);

    public class MyButton
    {
        public event ClickEventHandler Click;
        private string msg;

        public MyButton(string msg)
        {
            this.msg = msg;
        }

        public void OnClick()
        {
            if (Click != null)
            {
                Click(this,new MyEventArgs(msg));
            }
        }

        public void print()
        {
            Console.WriteLine("这是一个测试!");
        }
    }
}

namespace EventTest
{
    class Program
    {
        static void Main(string[] args)
        {
            MyButton mb = new MyButton("A");
            mb.Click += Button_Click;
            mb.OnClick();
            Console.ReadLine();
        }

        private static void Button_Click(Object sender, MyEventArgs e)
        {
            ((MyButton)sender).print();
            Console.WriteLine(e.getMsg());
        }
    }
}

结果不言而喻。本例中这两个参数,委托声明原型中的Object类型的参数表明了MyButton对象,主程序能够经过它访问触发事件的对象(Heater)。
EventArgs 对象包含了主程序所感兴趣的数据,在本例中是MyButton的字段 msg。若是你熟悉观察者模式的推拉模型,那么这两个参数,正好跟观察者模式推拉两种模型须要传递的参数所表达的意义吻合。这也说明了利用委托能够很容易实现观察者模式。
若是你还不熟悉观察者模式,能够看下我另外一篇文章里扯的内容:和观察者模式来一次约谈

相关文章
相关标签/搜索