委托与事件
委托是一个密封类,继承自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,所以将静态方法赋值给委托时性能更优。