当前位置:坤哥网-kungge-c#知识点梳理之委托和事件

c#知识点梳理之委托和事件

2017/6/27 23:22:43 坤哥网阅读(1066) 评论(0)


委托介绍


委托是将方法作为参数传递给方法,详见参考:《c#中的委托》

在《c#知识点梳理之lambda和Linq》中在介绍Lambda表达式也使用了委托。


下面介绍委托的几种使用方式,定义几个委托(参数和返回值都不同),新建一个DelegateTest类用于测试:

public delegate void TestDelegage1();
public delegate string TestDelegage2();
public delegate void TestDelegage3(string p1, int p2);
public delegate DelegateTest TestDelegage4(string p1, int p2);
public delegate void TestDelegage(string p);

public class DelegateTest
{
    public void Test()
    {
        #region 委托使用方式
        {
            TestDelegage1 d1 = new TestDelegage1(this.DoSomething);//创建一个委托的实例
            d1.Invoke();//委托实例的调用
            d1();

            TestDelegage2 d2 = new TestDelegage2(this.DoSomething2);
            string str = d2.Invoke();

            TestDelegage3 d3 = new TestDelegage3(this.DoSomething);
            d3.Invoke("love you", 520);
        }
        #endregion         

    }
    public TestDelegage1 duoBoTestDelegate;
    public void DoSomething()
    {
        Console.WriteLine("DoSomething ");
    }
    public void DoSomething(string p1, int p2)
    {
        Console.WriteLine("DoSomething({0},{1})", p1, p2);
    }
    public string DoSomething2()
    {
        Console.WriteLine("DoSomething2");
        return "ok";
    }
    public DelegateTest DoSomething3(string p1, int p2)
    {
        Console.WriteLine("DoSomething({0},{1})", p1, p2);
        return new DelegateTest();
    }
}


多播委托


前面介绍的一个委托只能包含一个方法调用,如果要调用多个方法则需要多次显式调用这个委托。多播委托就可以实现包含多个方法,执行顺序按照先注册先执行原则,委托的签名必须返回void,否则只能得到委托调用方法的最后一个结果。

下面演示多播委托的使用,在演示之前先创建一个实体类Employee为职员类:

public class Employee
{
    public static void PrintPPT()
    {
        Console.WriteLine("Employee 打印PPT");
    }
    public static void WriteCode()
    {
        Console.WriteLine("Employee 写代码");
    }
}


在Test方法中添加多播委托使用的代码:

Console.WriteLine("**********开始给委托实例注册方法***********\r\n");

//+=表示给委托实例注册方法,执行顺序按照先注册先执行原则
duoBoTestDelegate += DoSomething;
duoBoTestDelegate += Employee.PrintPPT;
duoBoTestDelegate += Employee.WriteCode;
duoBoTestDelegate += () => { Console.WriteLine("匿名方法"); };
duoBoTestDelegate += Employee.WriteCode;
duoBoTestDelegate.Invoke();

Console.WriteLine("\r\n\r\n");

Console.WriteLine("**********开始给委托实例移除方法***********\r\n");

//-=表示移除委托实例后面的方法,从最后注册的开始匹配,找到第一个完全匹配的移除,没哟匹配不产生异常
duoBoTestDelegate -= Employee.WriteCode;
duoBoTestDelegate -= DoSomething;
duoBoTestDelegate.Invoke();

输出结果:

77d3d839-de20-4718-8fff-d7a8ef8831ce.png


事件


阐述事件之前,先用委托实现一个功能:老板秘书通知公司职员老板要来了,让他们停止与工作无关的事情。这功能可以用观察者模式设计,下面为了引出事件,将这种模式设计尽可能简单。

添加一个接口ITest1:

public interface ITest1
{
    void DoSomething();
}

添加三个不同类型的职员类,实现ITest1接口,他们都有一个方法用于模拟停止当前做的事:

public class StaffA: ITest1
{
    public string Name { get; set; }
    public StaffA(string name)
    {
        this.Name = name;
    }

    public void DoSomething()
    {
        CloseVideo();
    }
    public void CloseVideo()
    {
        Console.WriteLine("{0} CloseVideo 关闭视频",this.Name);
    }
    public void CloseChatWindow()
    {
        Console.WriteLine("{0} CloseChatWindow 关闭聊天窗口",this.Name);
    }
}
public class StaffB : ITest1
{
    public string Name { get; set; }
    public StaffB(string name)
    {
        this.Name = name;
    }

    public void DoSomething()
    {
        CloseChatWindow();
    }
    public void CloseChatWindow()
    {
        Console.WriteLine("{0} CloseChatWindow 关闭聊天窗口", this.Name);
    }
}
public class StaffC : ITest1
{
    public string Name { get; set; }
    public StaffC(string name)
    {
        this.Name = name;
    }

    public void DoSomething()
    {
        CloseTaoBao();
    }
    public void CloseTaoBao()
    {
        Console.WriteLine("{0} CloseTaoBao 关闭淘宝", this.Name);
    }
}

新建一个秘书类Secretary用于作为作为观察者模式中的发布者:

        public class Secretary
    {
        private List<ITest1> staffList;
        public void AddStaffList(ITest1 itest1)
        {
            if (staffList == null)
            {
                staffList = new List<ITest1>();
            }
            staffList.Add(itest1);
        }

        public void Notify()
        {
            if (staffList != null)
            {
                foreach(var item in staffList)
                {
                    item.DoSomething();
                }
            }
        }
    }

在Secretary中AddStaffList表示将订阅者加入订阅者集合中,Notify表示发布通知给所有的订阅者。

Main方法中调用只需要将需要通知的职员加入订阅者即可,然后再调用通知方法:

                                Secretary secretary = new Secretary();
                secretary.AddStaffList(new StaffA("Jack"));
                secretary.AddStaffList(new StaffB("Tom"));
                secretary.AddStaffList(new StaffC("Lily"));
                secretary.Notify();

输出结果:

dc6e7189-0eba-4d7e-aec6-b49c4f35d3f0.png

为了实现这个功能,前面的介绍的多播委托就可以做到,不用新增方法将订阅者一个个加入集合中,然后再遍历,下面用多播委托实现该功能。

添加委托:

              public delegate void Test1Delegate();
        public Test1Delegate test1Delegate;

然后在调用的时候,要添加订阅者,直接注册订阅者调用的方法即可:

                Secretary secretary = new Secretary();
                secretary.test1Delegate += new StaffA("Jack").CloseVideo;
                secretary.test1Delegate += new StaffB("Tom").CloseChatWindow;
                secretary.test1Delegate += new StaffC("Lily").CloseTaoBao;
                secretary.test1Delegate();

这也能实现上面的功能,但是既然多播委托能注册和移除方法,为什么还要事件了?其实事件是对委托的再一次封装,因为委托在外部可以直接调用,无法做到统一管理,若是委托的话,外部调用者就可以使用"="对委托对象重新赋值,甚至将其设为null,安全性得不到保证,事件就是为了解决这个问题。事件声明就是在委托变量前加event关键字:

  public delegate void Test1Delegate();
  public event Test1Delegate Test1Event;

调用的时候和多播委托调用类似,只是最后执行的时候不能通过事件变量直接类在外部执行,而可以借助事件所在类的内部方法,在这个内部方法中执行:

               public void NotifyV2()
        {
            if (Test1Event != null)
                Test1Event();
        }

外部调用如下:

                                Secretary secretary = new Secretary();
                secretary.Test1Event += new StaffA("Jack").CloseVideo;
                secretary.Test1Event += new StaffB("Tom").CloseChatWindow;
                secretary.Test1Event += new StaffC("Lily").CloseTaoBao;
                secretary.NotifyV2();

前面提到过.net framework默认为我们定义了两个委托变量,分别是无返回值的Action和有返回值的Func:

  public delegate void Action();
 public delegate TResult Func<out TResult>();

上面也可以不用定义委托类型,直接使用Action即可:

public event Action EventTest;


创建标准模式的事件


在.net中定义了一套事件的标准模式,此模式保证了.net框架里的事件和开发者自己创建的事件代码一致。

在创建标准模式的事件之前,我们先来创建一个自定义的常规事件例子,到时再将该例子改造成符合.net框架标准模式的事件。

假设我们要实现这样一个功能,创建一个温度报警控制系统,当外界温度达到了系统预先设置的最高温度时,则发出报警执行一系列的动作。

温度控制类代码实现如下:

public class TemperatureControl
{
    public event Action<int,int> TemperatureAlarm;

    public int HighestTemperature { get; set; }

    protected int _temperature = 0;
    public int Temperature
    {
        get
        {
            return _temperature;
        }
        set
        {
            _temperature = value;
            if (_temperature >= this.HighestTemperature)//温度到达指定的值则触发事件
            {
                TemperatureAlarm(this.HighestTemperature,_temperature);
            }
        }
    }
}

定义了两个方法,当温度达到预定值时则会先后执行,客户端代码如下:

class Program
{
    static void Main(string[] args)
    {

        TemperatureControl tc = new TemperatureControl();
        tc.HighestTemperature = 100;
        tc.TemperatureAlarm += FirstAlart;
        tc.TemperatureAlarm += SecondAlart;
        Console.WriteLine("将温度设为80");
        tc.Temperature = 80;
        Console.WriteLine("将温度设为100");
        tc.Temperature = 100;
        
        Console.ReadKey();
    }
}
static void FirstAlarm(int highestTemperature,int temperature)
{
    Console.WriteLine($"系统设置最高温度={highestTemperature} 当前温度={temperature},开始报警,哔哔哔哔哔!");
}
static void SeconAlarm(int highestTemperature, int temperature)
{
    Console.WriteLine($"系统设置最高温度={highestTemperature} 当前温度为{temperature},开始喷水,滋滋滋滋!");
}

客户端调用时设置了报警温度为100度,先后模拟将当前温度设置为80度和100度,在80度时没有触发事件,当达到100度时触发了事件,输出如下:

231ee7e8-10bd-4292-be3d-3f271d5901ca.png

下面把上面的例子改造一下。

通过创建继承自框架提供的System.EventArgs类TemperatureEventArgs用来包含事件数据,用于参数传递:

public class TemperatureEventArgs : EventArgs
{
    public int HighestTemperature { get; set; }
    public int Temperature { get; set; }
    public TemperatureEventArgs(int highestTemperature,int temperature)
    {
        this.HighestTemperature = highestTemperature;
        this.Temperature = temperature;
    }
}

在温度控制类中定义了一个EventHandler类型的变量,该类表示将在事件提供数据时处理该事件的方法,这是一个泛型类,泛型的参数就是指定包含事件的类,如上面的TemperatureEventArgs,这个类是.net框架为我们事先定义好的一个通用委托,原型就是:

public delegate void EventHandler<TEventArgs> (object source, TEventArgs e) where TEventArgs : EventArgs;

我们也可以自己定义委托,但是这个委托必须满足下面三个条件:

(1)返回类型为void。

(2)包含两个参数,第一个是object,第二个是继承EventArgs的类。

(3)名称以EventHandler为结尾。

然后创建一个受保护的虚方法OnTemperatureAlarm用来触发事件,方法名称以On开头,参数就是TemperatureEventArgs类型。温度控制类的完整代码如下:

public class TemperatureControlV2
{
    public EventHandler<TemperatureEventArgs> TemperatureAlarm;

    protected virtual void OnTemperatureAlarm(TemperatureEventArgs e)
    {
        if (TemperatureAlarm != null)
        {
            TemperatureAlarm(this,e);
        }
    }

    public int HighestTemperature { get; set; }

    protected int _temperature = 0;
    public int Temperature
    {
        get
        {
            return _temperature;
        }
        set
        {
            _temperature = value;
            if (_temperature >= this.HighestTemperature)//温度到达指定的值则触发事件
            {
                OnTemperatureAlarm(new TemperatureEventArgs(this.HighestTemperature,_temperature));
            }
        }
    }
}

触发事件执行的方法有两个参数,第一个是object类型,第二个就是我们定义的TemperatureEventArgs,用来传递参数,客户端代码如下:

class Program
{
    static void Main(string[] args)
    {

        TemperatureControlV2 tc = new TemperatureControlV2();
        tc.HighestTemperature = 90;
        tc.TemperatureAlarmEventHandler += FirstAlarmV2;
        tc.TemperatureAlarmEventHandler += SeconAlarmV2;
        Console.WriteLine("将温度设为80");
        tc.Temperature = 80;
        Console.WriteLine("将温度设为100");
        tc.Temperature = 100;
        
        Console.ReadKey();
    }
}
static void FirstAlarmV2(object sender, TemperatureEventArgs e)
{
    Console.WriteLine($"系统设置最高温度={e.HighestTemperature} 当前温度={e.Temperature},开始报警,哔哔哔哔哔!");
}
static void SeconAlarmV2(object sender, TemperatureEventArgs e)
{
    Console.WriteLine($"系统设置最高温度={e.HighestTemperature} 当前温度为{e.Temperature},开始喷水,滋滋滋滋!");
}

输出:

ffd12c03-5887-4e21-9769-42b29d1fa319.png


(完)


分类: C#

有话要说? =>【不用注册,直接登录】,然后刷新本页面来发表您的观点(●'◡'●)