委托与事件

委托是一个密封类,继承自System.MulticastDelegate,里面内置了invoke等几个方法,简单说它就是一个能把方法当参数传递的对象,而且还知道怎么调用这个方法,同时也是粒度更小的“接口”(约束了指向方法的签名)。可以认为委托是持有一个或多个方法的对象。当然,正常情况下你不会想要“执行”一个对象,但委托与典型的对象不同。可以执行委托,这时委托会执行它所“持有”的方法。
委托是面向对象的、类型安全的,是引用类型。

委托的声明、实例化和调用

委托是引用类型,因此有引用和对象。在委托类型声明之后,我们可以声明变量并创建类型的对象。

public delegate void NoReturnNoPara();//1 声明委托
//把方法包装成对象,invoke的时候自动执行方法
NoReturnNoPara method = new NoReturnNoPara(this.DoNothing);//2 委托的实例化
method.Invoke();//3 委托实例的调用
method();

委托解耦

委托把方法包装成对象,invoke的时候自动执行方法。

如果我们需要通过判断条件来进行筛选,可以通过委托包装方法。
如下,我们在studentList中通过多个判断条件将满足条件的放入容器

{
    //找出年龄大于25
    List<Student> result = new List<Student>();//准备容器
    foreach (Student student in studentList)//遍历数据源
    {
        if (student.Age > 25)//判断条件
        {
            result.Add(student);//满足条件的放入容器
        }
    }
    Console.WriteLine($"结果一共有{result.Count()}个");
}

可以将判断逻辑传递进来+实现共用逻辑委托解耦,减少重复代码

public class ListExtend
{
    public delegate bool ThanDelegate(Student student);
    private bool Than(Student student)
    {
        return student.Age > 25;
    }

    public void Show()
    {
        ThanDelegate method = new ThanDelegate(this.Than);
        List<Student> result = this.GetListDelegate(studentList, method);
        Console.WriteLine($"结果一共有{result.Count()}个");
    }

    private List<Student> GetListDelegate(List<Student> source, ThanDelegate method)
    {
        List<Student> result = new List<Student>();
        foreach (Student student in source)
        {
            if (method.Invoke(student))
            {
                result.Add(student);
            }
        }
        return result;
    }
}

另外,委托可以做通用的异常处理,其中Action对应任何逻辑。

/// <summary>
/// 通用的异常处理
/// </summary>
/// <param name="act">对应任何的逻辑</param>
public static void SafeInvoke(Action act)
{
    try
    {
        act.Invoke();
    }
    catch (Exception ex)//按异常类型区分处理
    {
        Console.WriteLine(ex.Message);
    }
}

异步多线程

委托也可以与多线程结合,异步调用。

WithReturnNoPara method = new WithReturnNoPara(this.GetSomething);
int iResult = method.Invoke();
iResult = method();
var result = method.BeginInvoke(null, null);//异步调用
method.EndInvoke(result);

多播委托

链式委托也被称为“多播委托”,其本质是 一个由多个委托组成的链表 。我们知道,所有的自定义委托都继承自System.MulticastDelegate类,这个类就是为链式委托而设计的。当两个及以上的委托被链接到一个委托链时,调用头部的委托将导致该链上的所有委托方法都被执行。
像上面实例化委托的时候,一个委托类型的变量只能保存一个方法,使用多播委托,一个委托类型的变量可以保存多个方法,多播委托可以增加、减少委托,Invoke的时候可以按顺序执行。
+= 为委托实例按顺序增加方法,形成方法链,Invoke时,按顺序依次执行,如果多播委托带返回值,结果则以最后的为准。

当然,在使用+=运算符时,实际发生的是创建了一个新的委托,其调用列表是委托加上方法的组合。然后创建并更新新的委托。另外,+= 和 -= 对null是不会报错的;且 -= 只针对委托中具有同一个实例,无法移除不同实例的方法。注意:多播委托不能异步调用(即调用BeginInvoke()),因为多播委托里面有很多方法,异步调用的时候不知道该怎样执行

Student studentNew = new Student();

NoReturnNoPara method = new NoReturnNoPara(this.DoNothing);
method += new NoReturnNoPara(this.DoNothing);
method += new NoReturnNoPara(DoNothingStatic);
method += new NoReturnNoPara(Student.StudyAdvanced);
method += new NoReturnNoPara(new Student().Study);
method += new NoReturnNoPara(studentNew.Study);
method.Invoke();

//method.BeginInvoke(null, null);//error,多播委托是不能异步的
foreach (NoReturnNoPara item in method.GetInvocationList())
{
    item.BeginInvoke(null, null);
}
//-= 为委托实例移除方法,从方法链的尾部开始匹配,遇到第一个完全吻合的,移除且只移除一个,没有也不异常
method -= new NoReturnNoPara(this.DoNothing);
method -= new NoReturnNoPara(DoNothingStatic);
method -= new NoReturnNoPara(Student.StudyAdvanced);
method -= new NoReturnNoPara(new Student().Study);//不是同一个实例,所以是不同的方法
method -= new NoReturnNoPara(studentNew.Study);
method.Invoke();

有一种通用的应用场景。当我们需要设定一个发布者, 导致一系列的触发动作,并且动作可能会发生变化和扩展,则可以通过委托共用相同的触发逻辑。

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

namespace MyDelegateEvent.Event
{
    /// <summary>
    /// 发布者
    /// </summary>
    public delegate void MiaoDelegate();

    public class Cat
    {
        public void Miao()
        {
            Console.WriteLine("{0} Miao", this.GetType().Name);
            new Mouse().Run();
            new Baby().Cry();
            new Mother().Wispher();
        }

        public MiaoDelegate MiaoDelegateHandler;
        public void MiaoNew()
        {
            Console.WriteLine("{0} MiaoNew", this.GetType().Name);
            if (this.MiaoDelegateHandler != null)
            {
                this.MiaoDelegateHandler.Invoke();
            }
        }
      }
}

之后便可以通过委托,为Cat添加动作

Cat cat = new Cat();
cat.MiaoDelegateHandler += new MiaoDelegate(new Mouse().Run);
cat.MiaoDelegateHandler += new MiaoDelegate(new Baby().Cry);
cat.MiaoDelegateHandler += new MiaoDelegate(new Mother().Wispher);
cat.MiaoDelegateHandler += new MiaoDelegate(new Brother().Turn);
cat.MiaoDelegateHandler += new MiaoDelegate(new Father().Roar);
cat.MiaoDelegateHandler += new MiaoDelegate(new Neighbor().Awake);
cat.MiaoDelegateHandler += new MiaoDelegate(new Stealer().Hide);
cat.MiaoDelegateHandler += new MiaoDelegate(new Dog().Wang);

同理,也可以通过事件

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

namespace MyDelegateEvent.Event
{
    public class Cat
    {
        public event MiaoDelegate MiaoDelegateHandlerEvent;
        public void MiaoNewEvent()
        {
            Console.WriteLine("{0} MiaoNewEvent", this.GetType().Name);
            if (this.MiaoDelegateHandlerEvent != null)
            {
                this.MiaoDelegateHandlerEvent.Invoke();
            }
        }
    }
}

事件

当我们使用委托场景时,我们很希望有这样两个角色出现:广播者和订阅者。我们需要这两个角色来实现订阅和广播这种很常见的场景。
广播者这个角色应该有这样的功能:包括一个委托字段,通过调用委托来发出广播。而订阅者应该有这样的功能:可以通过调用 += 和 -= 来决定何时开始或停止订阅。事件就是描述这种场景模式的一个词。事件是委托的一个子集,为了满足“广播/订阅”模式的需求而生。
声明一个事件很简单,只需在声明一个委托对象时加上event关键字就行。

public event MiaoDelegate MiaoDelegateHandlerEvent;

事件是委托类型的一个实例,并且做出了安全限制,可以避免委托的实例直接invoke或者对委托直接赋值,从而限制变量被外部调用和直接赋值。在一种特殊的例子中,子类也不能直接调用父类的事件。
因此,事件:可以把一堆可变的动作/行为封装出去,交给第三方来指定。和预定义一样,程序设计的时候,我们可以把程序分成两部分一部分是固定的,直接写死;还有不固定的地方,通过一个事件去开放接口,外部可以随意扩展动作。
但事件有一系列规则和约束用以保证程序的安全可控,事件只有 += 和 -= 操作,这样订阅者只能有订阅或取消订阅操作,没有权限执行其它操作。如果是委托,那么订阅者就可以使用 = 来对委托对象重新赋值(其它订阅者全部被取消订阅),甚至将其设置为null,甚至订阅者还可以直接调用委托,这些都是很危险的操作,广播者就失去了独享控制权。
事件保证了程序的安全性和健壮性

事件的标准模式

.NET 框架为事件编程定义了一个标准模式。设定这个标准是为了让.NET框架和用户代码保持一致。System.EventArgs是标准模式的核心,它是一个没有任何成员,用于传递事件参数的基类。

using System;


public class PriceChangedEventArgs : System.EventArgs
{
    public readonly decimal OldPrice;
    public readonly decimal NewPrice;
    public PriceChangedEventArgs(decimal oldPrice, decimal newPrice) { OldPrice = oldPrice; NewPrice = newPrice; }
}
public class IPhone6
{
    decimal price;
    public event EventHandler<PriceChangedEventArgs> PriceChanged;
    protected virtual void OnPriceChanged(PriceChangedEventArgs e)
    {
        if (PriceChanged != null) PriceChanged(this, e);
    }
    public decimal Price
    {
        get { return price; }
        set
        {
            if (price == value)
                return;
            decimal oldPrice = price;
            price = value;             // 如果调用列表不为空,则触发。
            if (PriceChanged != null)
                OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
        }
    }
}
class Program
{
    static void Main()
    {
        IPhone6 iphone6 = new IPhone6() { Price = 5288M };
        // 订阅事件
        iphone6.PriceChanged += iphone6_PriceChanged;          // 调整价格(事件发生)
        iphone6.Price = 3999;
        Console.ReadKey();
    }
    static void iphone6_PriceChanged(object sender, PriceChangedEventArgs e)
    {
        Console.WriteLine("年终大促销,iPhone 6 只卖 " + e.NewPrice + " 元, 原价 " + e.OldPrice + " 元,快来抢!");
    }
}

静态方法和实例方法对于委托的区别

当一个类的实例的方法被赋给一个委托对象时,在上下文中不仅要维护这个方法,还要维护这个方法所在的实例。System.Delegate 类的Target属性指向的就是这个实例。对于静态方法,System.Delegate 类的Target属性是Null,所以将静态方法赋值给委托时性能更优。

参考:
委托和事件
【C#系列】你应该知道的委托和事件

posted @ 2021-01-04 20:18  Jamest  阅读(134)  评论(0编辑  收藏  举报