网上讲C#委托和事件
的博文已经非常多了,其中也不乏一些深入浅出、条理清晰的文章。我之所以还是继续写,主要是借机整理学习笔记、归纳总结从而理解更透彻,当然能够以自己的理解和思路给其他人讲明白更好。
另外,太长的文章会让很多读者失去兴趣,所以我决定把这篇分成四个部分来介绍。分别是委托的基础、委托的进阶、事件的基础和事件的进阶。对使用委托与事件要求不高的同学可以跳过进阶部分。
本文接着讲委托的高级知识,上一节请参见C#委托与事件(1)。
4. 委托的高级知识
(1) 委托的类组成
前面我们介绍过了,委托实际上也是一个类,只不过它的对象不是一个普通的变量,而是一个方法。但是我们实际使用时并不需要定义这个类,而只是声明一下委托即可。这是因为当C#
编译器处理委托类型时,会自动产生一个派生自System.MulticastDelegate
的密封类,这个类与它的基类System.Delegate
一起为委托提供必要的基础设施。
下面我们就以上一节中声明的委托delegate void GreetingDelegate(string s);
来看看该类的组成。
public sealed class GreetingDelegate : System.MulticastDelegate
{
public void Invoke(string s);
public IAsyncResult BeginInvoke(string s, AsyncCallback cb, object state);
public void EndInvoke(IAsyncResult result);
}
可以看到,该类中定义了三个公共方法:
(a) Invoke()
,它被用来以同步方式调用委托对象维护的每个方法。所谓同步是指调用者必须等待调用完成才能继续执行。Invoke()
方法定义的参数和返回值完全匹配我们要定义的类GreetingDelegate
。另外,Invoke()
不能直接调用,而是在后台调用。
(b) BeginInvoke()
,用于异步调用,它最前面的参数列表是GreetingDelegate
定义的方法类的参数列表,此外,还有两个参数AsyncCallback
和object
用于异步方法调用。
(c) EndInvoke()
,与BeginInoke()
联合用于异步调用,它的返回值与委托声明的返回值一致,而它的唯一参数则是BeginInvoke()
返回的类型IAsyncResult
接口。
另外,上面我们定义的委托没有返回值,也可以定义返回值类型,这样Invoke()
和EndInvoke()
对应的返回值就不是void
了。委托还可以指向包含任意数量out
或者ref
参数的方法,按道理只有Invoke()
和BeginInvoke()
方法与委托的参数列表有关,需要加上相应的out
或者ref
参数,但是由于异步调用时需要通过EndInvoke()
来返回结果,所以EndInvoke()
的参数列表中需要加上out
或者ref
参数。
下面简单介绍一下委托类的父类System.MulticastDelegate
。
public abstract class MulticastDelegate : Delegate
{
// 返回所指向的方法列表
public sealed override Delegate[] GetInvocationList();
// 重载等于和不等于操作符
public static bool operator ==(MulticastDelegate d1, MulticastDelegate d2);
public static bool operator !=(MulticastDelegate d1, MulticastDelegate d2);
// 用来在内部管理委托所维护的方法列表
private IntPtr _invocationCount;
private object _invocationList;
}
public abstract class Delegate : IConeable, ISerializable
{
// 与函数列表交互的方法
// 给委托维护的列表添加一个方法,在C#中使用重载+=操作符调用此方法
public static Delegate Combine(params Delegate[] delegates);
public static Delegate Combine(Delegate a, Delegate b);
// 从调用列表中移除一个或所有的方法,在C#中使用-=操作符调用此方法
public static Delegate Remove(Delegate source, Delegate value);
public static Delegate RemoveAll(Delegate source, Delegate value);
// 重载操作符
public static bool operator ==(Delegate d1, Delegate d2);
public static bool operator !=(Delegate d1, Delegate d2);
// 扩展委托目标的属性
public MethodInfo Method { get; } //用以表示委托维护的静态方法的详细信息
public object Target { get; } // 如果方法调用是定义在对象级别的,
// Target返回表示委托维护的方法的对象;
// 如果调用的方法时一个静态成员,
// Target返回null
}
(2) 泛型委托
C#
允许我们定义泛型委托,即当我们定义的委托接受的参数可能会不同时,我们可以通过类型参数来构建。下面我们来改写一下上一节中定义的那个委托,使得它不仅支持传入string
,还支持传入整型。
namespace TestDelegate
{
delegate void GreetingDelegate(T arg);
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
// do something (hug or shake hand...)
}
public static void Goodbye(string s)
{
Console.WriteLine(" Goodbye, {0}!", s);
// do something (hug or wave hand...)
}
public static void GreetingTimes(int n)
{
Console.WriteLine(" Greeting {0} times!", n);
}
static void MakeGreeting(T name, GreetingDelegate greeting)
{
greeting(name);
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello; //定义委托的一个对象(将方法绑定到委托)
d1 += Goodbye; // 在d1上再绑定一个委托
GreetingDelegate d2 = GreetingTimes; //定义委托的另一个对象
MakeGreeting("April", d1);
MakeGreeting(99, d2);
}
}
}
输出内容:
Hello, April!
Goodbye, April!
Greeting 99 times!
有了泛型委托,很多方法都可以用一个委托模板表示出来,因此C#
中提供了两个常用的泛型委托Action
和Func
来避免用户手工构建自定义委托的麻烦。
Action
Action
泛型委托定义的方法,参数列表可以多至16个(使用时需要指定各个参数的类型),返回值为void
。
例如,我们有一个方法为static void DisplayMessage(string msg, ConsoleColor txtColor, int printCount)
,若把它作为Action
委托的一个目标,则委托的实例化时需要这样写:
Action
调用委托方法时:
actionTarget("your input string", ConsoleColor.Green, 2);
Func
Func
泛型委托也可以指向多至16个参数的方法,但与Action
不同的是,它具有自定义的返回值,具体用法类似,不再赘述。
(3) 委托的异步调用
在说委托的异步调用之前,我们先对第一节最早的delegate
例子做一个简单的改进,看看它的工作流程。首先,MakeGreeting()
方法中除了调用greeting()
之外,需要再调用一个额外的方法FuncAfterGreeting()
。然后,我们定义了GreetingDelegate
的一个对象d1
,并在d1
上绑定了Hello()
和Goodbye()
方法。最后我们调用MakeGreeting()
方法来看看输出结果。
namespace AsyncDelegateTest
{
delegate void GreetingDelegate(string arg);
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
Console.WriteLine(" Waiting for 1 second");
Thread.Sleep(1000);
Console.WriteLine(" Finished Hello");
}
public static void Goodbye(string s)
{
Console.WriteLine(" Goodbye, {0}!", s);
Console.WriteLine(" Waiting for 2 second");
Thread.Sleep(2000);
Console.WriteLine(" Finished Goodbye");
}
public static void FuncAfterGreeting()
{
Console.WriteLine(" Do some other things...");
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished FuncAfterGreeting");
}
static void MakeGreeting(string name, GreetingDelegate greeting)
{
greeting(name); // 这里相当于是greeting.Invoke(name);
FuncAfterGreeting();
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello; //定义委托的一个对象(将方法绑定到委托)
d1 += Goodbye; //定义委托的另一个对象
MakeGreeting("April", d1);
}
}
}
输出结果:
Hello, April!
Waiting for 1 second
Finished Hello
Goodbye, April!
Waiting for 2 second
Finished Goodbye
Do some other things...
Waiting for 3 second
Finished FuncAfterGreeting
从输出我们可以看到MakeGreeting()
方法中,首先调用了Hello()
方法,并运行完毕;然后调用了Goodbye()
方法,并运行完毕;最后调用FuncAfterGreeting()
,并运行完毕;至此,整个MakeGreeting()
方法运行完毕。
这就是采用同步的方式调用委托,这样委托对象绑定的每个方法要依次执行,而且后者必须等前者执行完毕之后才能开始执行。另外,只有把委托对象绑定的所有方法执行完毕后才能回到MakeGreeting()
方法中继续往下执行。
而在(1)中我们介绍的BeginInvoke()
和EndInvoke()
函数能使委托实现异步调用,所谓异步调用,就是在上例中MakeGreeting()
方法中的线程去执行greeting
方法时利用线程池中的线程去实现调用,自己则继续往下执行。有了BeginInvoke()
和EndInvoke()
这两个函数后,异步调用就很简单了,直接先用greeting
调用BeginInvoke()
函数,然后就可以做其他的事情,结束之间再调用EndInvoke()
即可。
namespace AsyncDelegateTest2
{
delegate void GreetingDelegate(string arg);
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished Hello");
}
public static void FuncAfterGreeting()
{
Console.WriteLine(" Do some other things...");
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished FuncAfterGreeting");
}
static void MakeGreeting(string name, GreetingDelegate greeting)
{
IAsyncResult result = greeting.BeginInvoke(name, null, null);
FuncAfterGreeting();
greeting.EndInvoke(result);
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello; //定义委托的一个对象(将方法绑定到委托)
MakeGreeting("April", d1);
}
}
}
输出结果:
Do some other things...
Hello, April!
Waiting for 3 second
Waiting for 3 second
Finished Hello
Finished FuncAfterGreeting
为什么我这里把Goodbye
方法去掉了,这是因为BeginInvoke()
只能在绑定了单个方法的delegate
上调用,如果我们在d1
上还绑定了其他方法,那么去调用BeginInvoke()
的时候会出现下面的异常:
Unhandled Exception: System.ArgumentException: The delegate must have only one target.
当然如果你一定要绑定多个方法这样用的话,可以先通过GetInvocationList()
获得绑定的方法列表,然后依次调用BeginInvoke()
方法。
namespace AsyncDelegateTest3
{
delegate void GreetingDelegate(string arg);
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished Hello");
}
public static void Goodbye(string s)
{
Console.WriteLine(" Goodbye, {0}!", s);
Console.WriteLine(" Waiting for 2 second");
Thread.Sleep(2000);
Console.WriteLine(" Finished Goodbye");
}
public static void FuncAfterGreeting()
{
Console.WriteLine(" Do some other things...");
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished FuncAfterGreeting");
}
static void MakeGreeting(string name, GreetingDelegate greeting)
{
Delegate[] delArray = greeting.GetInvocationList();
foreach (var d in delArray)
{
var del = (GreetingDelegate)d;
IAsyncResult result = del.BeginInvoke(name, null, null);
}
FuncAfterGreeting();
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello; // 定义委托的一个对象(将方法绑定到委托)
d1 += Goodbye; // 定义委托的另一个对象
MakeGreeting("April", d1);
Console.ReadLine(); // 如果不加这行的话,很可能Hello方法还没执行完程序就退出了,
// 因为我们没有调用EndInvoke()去检查它们的状态
}
}
}
输出结果:
Do some other things...
Hello, April!
Goodbye, April!
Waiting for 2 second
Waiting for 3 second
Waiting for 3 second
Finished Goodbye
Finished FuncAfterGreeting
Finished Hello
回到AsyncDelegateTest2
,这里其实还存在一个问题。那就是MakeGreeting()
方法中的这句话greeting.EndInvoke(result);
,如果Hello()
方法需要执行30s,那么3s后FuncAfterGreeting()
方法就执行完毕了,主线程执行到EndInvoke()
这句话。而这句话就相当于让主线程一直去查询Hello()
方法是否执行完毕。那么问题来了,能不能不要这么麻烦主线程,而是让Hello()
方法执行完毕后自动告诉主线程呢?这就是异步回调。
namespace AsyncDelegateTest4
{
delegate void GreetingDelegate(string arg);
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
Console.WriteLine(" Waiting for 5 second");
Thread.Sleep(5000);
Console.WriteLine(" Finished Hello");
}
public static void FuncAfterGreeting()
{
Console.WriteLine(" Do some other things...");
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished FuncAfterGreeting");
}
static void MakeGreeting(string name, GreetingDelegate greeting)
{
IAsyncResult result = greeting.BeginInvoke(name, new AsyncCallback(FuncForCallBack), "AsycState:OK");
FuncAfterGreeting();
}
static void FuncForCallBack(IAsyncResult result)
{
// AsyncResult should using System.Runtime.Remoting.Messaging
GreetingDelegate handler = (GreetingDelegate)((AsyncResult)result).AsyncDelegate;
handler.EndInvoke(result);
Console.WriteLine(result.AsyncState);
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello;
MakeGreeting("April", d1);
Console.ReadLine();
}
}
}
输出结果:
Do some other things...
Hello, April!
Waiting for 5 second
Waiting for 3 second
Finished FuncAfterGreeting
Finished Hello
AsycState:OK
这里我们定义了一个回调函数FuncForCallBack()
,这样就不需要在MakeGreeting()
方法的最后显示地去调用EndInvoke()
去检查委托方法的执行状态了。
参考文献:
《精通C#》
张子阳的《C# 中的委托和事件》
Delegates Tutorial
也来说说C#异步委托
C#委托的异步调用