文章目录
C#委托
C#委托与C函数指针
C#的委托与C语言的函数指针都可以实现把一个方法作为参数传进另一个方法之中。
C#的委托的例子:
using System;
using System.Collections.Generic;
using System.Text;
namespace Delegate
{
//定义委托,也既是定义一个方法的参数和返回类型(签名)。与C语言定义函数指针类似。
//任何注册到此委托的方法都要有同样的签名。
//委托类似一个类,任何可以声明类的地方都可以声明委托。
public delegate void GreetingDelegate(string name);
class Program
{
private static void EnglishGreeting(string name)
{
Console.WriteLine("Good Morning, " + name);
}
private static void ChineseGreeting(string name)
{
Console.WriteLine("早上好, " + name);
}
//符合GreetingDelegate委托类型的签名的函数都可作为参数传进下面的方法。
private static void Greeting(string name, GreetingDelegate MakeGreeting)
{
MakeGreeting(name);
}
static void Main(string[] args)
{
Greeting("Jim", EnglishGreeting);
Greeting("吉姆", ChineseGreeting);
}
}
}
以上代码的C语言版本
#include<stdio.h>
int main()
{
//函数指针,相当于C#的委托,可指向一个拥有返回类型为空,一个参数为字符串的方法
void (*GreetingPointer)(char *);
void EnglishGreeting(char *);
void ChineseGreeting(char *);
//函数指针作为Greeting方法的参数
void Greeting(char *, void (*GreetingPointer)(char *p));
//函数指针赋值时不用写参数
GreetingPointer=EnglishGreeting;
Greeting("Jim",EnglishGreeting);
GreetingPointer=ChineseGreeting;
Greeting("吉姆",ChineseGreeting);
return 0;
}
void EnglishGreeting(char *p){
printf("Good Moring,%s\n",p);
}
void ChineseGreeting(char *p){
printf("Good Moring,%s\n",p);
}
void Greeting(char *p,void (*GreetingPointer)(char *p2)){
GreetingPointer(p);
}
C#委托的实例化
既然类似类,就需要声明与实例,不同于一般类C#给了委托一些语法糖,实例化一个委托有多种方法:
1,像一般类一样实例化
GreetingDelegate delegate1=new GreetingDelegate(EnglishGreeting);
2,等号赋值,从C#2.0开始会隐式的自动new一个委托
GreetingDelegate delegate1=EnglishGreeting;
3,匿名方法,这种方法可省去EnglishGreeting方法的声明
GreetingDelegate delegate1=delegate(string name){Console.WriteLine("Good Morning, " + name);};
4,Lambda表达式,匿名方法的升级简易版本
GreetingDelegate delegate1=(name)=>{Console.WriteLine("Good Morning, " + name);};
5,Func 泛型委托,可省去GreetingDelegate的声明,可用于有返回值的委托
Func<string,int>delegate1=((name)=>{Console.WriteLine("Good Morning, " + name);return 0;});
6,Action泛型委托,可省去委托的声明,可用于无返回值的委托
Action<string>delegate1=(name)=>{Console.WriteLine("Good Morning, " + name);};
7,多播委托(委托链):
委托可以包含多个方法,这种委托称为多播委托。除了=赋值,还可以进一步用‘+’,‘+=’和’-’,‘-=’进行方法的捆绑和解除。一般来讲多个绑定方法的调用顺序是与绑定顺序一致的,但是官方文档并未确认,不是100%可靠。
static void Main(string[] args)
{
GreetingDelegate delegate1;
//在多播委托中等号赋值会取消之前的所有方法绑定
delegate1=EnglishGreeting;
delegate1+=ChineseGreeting;
Greeting("Jim", delegate1);
}
C#事件Event
先看一个用委托实现的发布者与订阅者模式
面向对象的编程之中,各种不同的类要进行连接与通讯。利用委托与事件可以在各个类之间建立起一种一对一或一对多的监视关系。拥有事件和“触发事件方法"的类可以理解为一个“发布类”,注册,捆绑在事件上的方法称为“事件处理方法”,“事件处理方法”一般不在“发布类”里面而是在其他的类之中,这些拥有“事件处理方法”的类可以理解为“订阅类”。一旦“发布类”发生某种状态改变,它的所有的“订阅类”将会被自动通知并更新—触发事件并调用它捆绑的“事件处理方法”。
发布类不需要知道订阅类的内容,且订阅类可自行决定是否订阅发布类,因此这种监视相当灵活,并有良好的可扩展性。
//发布类
public class Pub
{
public Action OnChange{ get; set; }
public void Raise()
{
if(OnChange!=null)
{
OnChange();
}
}
}
public void CreateAndRaise()
{
Pub p = new Pub();
//捆绑事件处理方法
p.OnChange += () => Console.WriteLine("Event raised to method 1");
p.OnChange += () => Console.WriteLine("Event raised to method 2");
p.Riase();
}
以上的订阅者模式中委托delegate的缺点:
1,上面代码中的委托如果声明为private,其他类的对象无法对它进行函数绑定,丧失了功能。如果声明为public,任意对象用=等号赋值会取消之前的函数绑定,所有外部对象也可以任意控制该委托,破坏了Pub类的封装。
2,OnChange委托可以被任意对象触发调用,破坏了订阅者与发布者的分工。
事件event的改进
event可以看做是一个delegate的升级版,解决了上面的两个问题。
1,event无法用=直接赋值,只可以用+=,-=。一个订阅者的行为无法影响其他订阅者。
2,event只可以在它所在的类之内触发调用,只有发布者才有发布权。
一个使用事件event的简单例子:
using System;
using System.Collections.Generic;
using System.Text;
namespace Delegate
{
//定义发布类
public class Jim{
//订阅类对发布类感兴趣的信息
private string name="Jim";
//声明委托。如果事件使用Action或Function声明也可省去该处delegate声明
private delegate void GreetingHandler(string name2);
//声明事件
public event GreetingHandler GreetingEvent;
//定义“事件触发方法”,可封装一些逻辑
public void ShowUp(){
//防止空引用异常
if(GreetingEvent!=null){
//触发事件,向所有订阅类发布信息
GreetingEvent(name);
}
}
}
//定义订阅类
public class Tom{
//定义它的“事件处理方法”
public void Greeting(string name){
Console.WriteLine ("Hello,{0}",name);
}
}
public class Lisa{
public void Greeting(string name){
Console.WriteLine ("Hi,{0}",name);
}
}
//测试
class TheScene{
static void Main(){
//实例化订阅类,发布类
Jim jim=new Jim();
Tom tom=new Tom();
Lisa lisa=new Lisa();
//订阅类的“事件处理方法”注册发布类的事件
jim.GreetingEvent+=tom.Greeting;
jim.GreetingEvent+=lisa.Greeting;
//调用事件触发方法。发布类发布信息。Jim(发布者) ShowUp出现,他并不知道有谁(订阅者)会看到他并跟他打招呼。
jim.ShowUp();
}
}
}
一个更完善,规范的事件event使用案例:
相对于上文中的案例,接下来的代码进一步解决下面几个问题
1,事件声明使用EventHandler,省去了委托的定义并且兼容各个类型的发布信息。
2,将发布信息升级成了一个类GreetingEventArgs,有了更强的可扩展性和封装。
3,在事件触发方法外又包装了一层,以实现事件触发方法的可重写,为多态做铺垫。
4,事件触发方法内考虑了多线程的一些危险case,更加鲁棒。
5,信息发布不只传递了发布信息还有发布者自身的类,加强了订阅者之后的代码的可扩展性。
using System;
using System.Collections.Generic;
namespace WorldTest
{
//定义Eventargs类,发布类通过它来存储,发布信息,事件触发后订阅类会收到该类的实例
public class GreetingEventArgs : EventArgs
{
//构造函数可重写,实现接收多种参数
public GreetingEventArgs(string s)
{
name = s;
}
private string name;
public string Name
{
get { return name;}
set;
//也可以选择对修改开放
//set { name = value;}
}
}
//定义发布类
public class Jim{
//订阅类对发布类感兴趣的信息
private string myname="Jim";
//声明EventHandler<T>类型的事件,省去了委托的定义
public event EventHandler<GreetingEventArgs> GreetingEvent;
//在“事件触发方法”外包了一层,此方法内可封装一定逻辑
public void ShowUp(){
OnRaiseGreetingEvent(new GreetingEventArgs(myname));
}
//定义“事件触发方法”,定义为protected virtual,衍生类可重写
protected virtual void OnRaiseGreetingEvent(GreetingEventArgs e){
//做一个GreetingEvent事件的临时拷贝,防止多线程模式下某个订阅者在下面的handler!=null检测后和时间触发之间的逻辑流中解除订阅
EventHandler<GreetingEventArgs> handler=RaiseGreetingEvent;
//防止空引用异常
if(handler!=null){
//触发事件。将自身和订阅类感兴趣的信息发布出去
handler(this,e);
}
}
}
//定义订阅类
public class Tom{
//订阅者id
private string id;
//直接在订阅者实例化环节完成事件捆绑
public Tom(string ID, Jim jim){
id = ID;
//订阅事件
jim.RaiseGreetingEvent+=Greeting;
}
//定义“事件处理方法”
public void Greeting(object sender,GreetingEventArgs e){
Console.WriteLine ("Hello,{0}",e.Name);
}
}
public class Lisa{
private string id;
public Lisa(string ID, Jim jim){
id = ID;
jim.RaiseGreetingEvent+=Greeting;
}
public void Greeting(object sender,GreetingEventArgs e){
Console.WriteLine ("Hi,{0}",e.Name);
}
}
//测试
class TheScene{
static void Main(){
//实例化订阅类,发布类
Jim jim=new Jim();
Tom tom=new Tom(“订阅者01”,jim);
Lisa lisa=new Lisa(“订阅者02”,jim);
//调用事件触发方法
jim.ShowUp();
}
}
}
事件event的“属性访问器”
event可以使用一个自定义访问器custom event accessor,它类似于属性访问器,可以在事件注册与解绑前封装一些逻辑。
public class Jim{
...
private event EventHandler<GreetingEventArgs> greetingEvent;
public event EventHandler<GreetingEventArgs> GreetingEvent
{
//方法绑定
add
{
//多线程安全
lock(greetingEvent)
{
greetingEvent += value;
}
}
//方法解绑
remove
{
lock(greetingEvent)
{
greetingEvent -= value;
}
}
}
...
}
多个绑定的事件处理方法中某个方法出现异常的解决办法
在上文的案例中,如果Tom的事件处理方法发生了异常,Lisa的事件处理方法是不会调用的,在有很多订阅者的情况,必然要规避这种风险。下面的代码可以保证所有事件处理方法执行结束后再触发异常。
public class Jim{
...
protected virtual void OnRaiseGreetingEvent(GreetingEventArgs e){
var exceptions = new List<Exception>();
foreach (Delegate handler in greetingEvent.GetInvocationList())
{
try
{
handler.DynamicInvoke(this, e);
}
catch(Exception ex)
{
exceptions.Add(ex);
}
}
if(exceptions.Any())
{
throw new AggregateException(exceptions);
}
}
}
...
//测试
class TheScene{
static void Main(){
//实例化订阅类,发布类
Jim jim=new Jim();
Tom tom=new Tom(“订阅者01”,jim);
Lisa lisa=new Lisa(“订阅者02”,jim);
//调用事件触发方法
try{
jim.ShowUp();
}
catch(AggregateException ex)
{
//处理异常
...
}
}
}
...
参考:
MSDN:发布符合 .NET Framework 准则的事件(C# 编程指南) --Microsoft
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/events/how-to-publish-events-that-conform-to-net-framework-guidelines
Programming in C# Exam Ref 70-483 --Wouter de Kort
维护日志:
2020-1-8:review,增删查改