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

C#

浏览数:211

2019-5-10

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

场景练习

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

    //中文方式打招呼
    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 属性附上对应的值。

策略模式的结构:

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

策略模式的适用场景

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

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

接口回调

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

定义了一个接口

    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。如果你熟悉观察者模式的推拉模型,那么这两个参数,正好跟观察者模式推拉两种模型需要传递的参数所表达的意义吻合。这也说明了利用委托可以很容易实现观察者模式。
如果你还不熟悉观察者模式,可以看下我另一篇文章里扯的内容:和观察者模式来一次约谈

作者:SharpCJ