一、委托
在包括 C# 在内的许多编程语言中,委托的概念是一个非常强大的功能。我相信,讨论 C# 高级编程离不开委托。在这一章中,你将学习委托以及为什么委托是必不可少的。
让我们回忆一下类和对象的基本原理。为了创建一个对象——比方说,obA
从一个类 A 中,你可以写一些像下面这样的东西。
A obA=new A();
在这里,对象引用obA
指向 a 的一个对象,与此类似,委托也是引用类型,但关键区别在于它们指向方法。简单地说,委托是一个知道如何调用方法的对象。委托是从System.Delegate
类派生的。
让我们从另一个角度来看。你知道变量是什么,它是如何表现的。你已经看到可以放不同的布尔值(true/false
)、字符串(或、字)、数字(整数、双精度等。)在各自类型的变量中。但是当你使用委托的时候,你可以把一个方法赋给一个变量并传递它。
简而言之,通过使用委托,您可以像对待对象一样对待您的方法。因此,您可以将委托存储在变量中,将其作为方法参数传递,并从方法中返回。
委托的使用有助于促进类型安全。(从广义上讲,术语类型安全只是告诉你,如果一种类型与另一种类型不兼容,你就不能将它们赋值给另一种类型。类型安全检查可以在编译时和运行时出现)。这就是为什么委托经常被称为类型安全函数指针的原因。
在演示 1 中,一个名为 Sum 的方法接受两个 integer (int)参数并返回一个整数,如下所示。
public static int Sum(int a, int b)
{
return a + b;
}
在这种情况下,您可以声明一个委托来指向 Sum 方法,如下所示。
DelegateWithTwoIntParameterReturnInt delOb = new DelegateWithTwoIntParameterReturnInt (Sum);
但在此之前,您需要定义DelegateWithTwoIntParameterReturnInt
委托,它必须具有相同的签名,如下所示。
delegate int DelegateWithTwoIntParameterReturnInt(int x, int y);
Sum 方法和DelegateWithTwoIntParameterReturnInt
委托的返回类型、参数以及它们对应的顺序是相同的。为了可读性更好,我为我的委托选择了一个长名字。您可以随时选择自己的委托姓名。
首先要明白的重要一点是,一旦有了DelegateWithTwoIntParameterReturnInt
,就可以用它来跟踪任何一个以两个整数为输入参数,返回一个整数的方法;例如,计算两个整数的和、两个整数的差、两个整数的乘法、两个整数的除法等等。
Points to Remember
-
委托实例包含方法的细节,而不是数据。
-
对于匹配委托签名的方法,可以使用委托。例如,顾名思义,
DelegateWithTwoIntParameterReturnInt
兼容任何接受两个 int 参数并返回一个 int 的方法。 -
当您使用委托来调用方法时,在较高的层次上,整个过程可以分为两个部分。在第一部分,您(调用方)调用委托,在第二部分,委托调用您的目标方法。这种机制将调用方从目标方法中分离出来。
定义
委托是从System.Delegate
派生的引用类型,它的实例用于调用具有匹配签名和返回类型的方法。在这一章的后面,你将了解到差异,你将发现在这个上下文中,单词兼容比单词匹配更合适。我在努力让事情尽可能简单。
delegate 一词的字典含义是“委托或代理人”C# 编程中的委托表示具有匹配签名的方法。这是委托声明的一般形式。
<modifier> delegate <return type> (parameter list);
以下是委托声明的示例。
delegate void DelegateWithNoParameter();
public delegate int MyDelegateWithOneIntParameter(int i);
public delegate double MakeTotal(double firstNo, double secondNo);
delegate int DelegateWithTwoIntParameterReturnInt(int x, int y);
您可能会注意到,这些方法类似于没有主体的方法。但是,当编译器看到关键字delegate
时,它知道您使用的是从System.Delegate
派生的类型。
从委托开始,在下面的例子中,我向您展示了两种情况。第一种情况对你来说很熟悉。您只需调用一个方法,而无需使用委托。在第二种情况下,您使用委托来调用方法。
演示 1
在本演示中,请注意以下代码段。
// Creating a delegate instance
// DelegateWithTwoIntParameterReturnInt delOb = new DelegateWithTwoIntParameterReturnInt(Sum);
// Or, simply write as follows:
DelegateWithTwoIntParameterReturnInt delOb = Sum;
我保留了注释,说明我在创建委托实例时使用了简写形式。你可以使用任何一个。
当您使用delOb(25,75)
而不是delOb.Invoke(25,75)
时,您也可以使代码长度更短。这也是为什么我还保留了下面的评论。
// delOb(25,75) is shorthand for delOb.Invoke(25,75)
当您使用缩写形式时(即,您将方法名分配给委托实例,而不使用 new 运算符或显式调用委托的构造函数),您使用的是一种称为方法组转换的功能。从 2.0 版开始就允许这种形式。
现在让我们来看看完整的示例以及相应的输出和分析。
using System;
namespace DelegateExample1
{
delegate int DelegateWithTwoIntParameterReturnInt(int x, int y);
class Program
{
public static int Sum(int a, int b)
{
return a + b;
}
static void Main(string[] args)
{
Console.WriteLine("***A simple delegate demo.***");
Console.WriteLine("\n Calling Sum(..) method without using a delegate:");
Console.WriteLine("Sum of 10 and 20 is : {0}", Sum(10, 20));
//Creating a delegate instance
//DelegateWithTwoIntParameterReturnInt delOb = new DelegateWithTwoIntParameterReturnInt(Sum);
//Or,simply write as follows:
DelegateWithTwoIntParameterReturnInt delOb = Sum;
Console.WriteLine("\nCalling Sum(..) method using a delegate.");
int total = delOb(10, 20);
Console.WriteLine("Sum of 10 and 20 is: {0}", total);
/* Alternative way to calculate the aggregate of the numbers.*/
//delOb(25,75) is shorthand for delOb.Invoke(25,75)
Console.WriteLine("\nUsing Invoke() method on delegate instance, calculating sum of 25 and 75.");
total = delOb.Invoke(25,75);
Console.WriteLine("Sum of 25 and 75 is: {0}", total);
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***A simple delegate demo.***
Calling Sum(..) method without using a delegate:
Sum of 10 and 20 is : 30
Calling Sum(..) method using a delegate.
Sum of 10 and 20 is: 30
Using Invoke() method on delegate instance, calculating sum of 25 and 75.
Sum of 25 and 75 is: 100
分析
让我们仔细看看代码。为了更容易理解,图 1-1 展示了 IL 代码的部分截图。 1
图 1-1
委托示例 1 的 IL 代码的部分屏幕截图
注意,当你创建一个委托时,C# 编译器把它变成一个从MulticastDelegate
扩展的类。让我们再深入一层。如果你看到了MulticastDelegate
的实现,你会发现它是从System.Delegate
类派生出来的。供大家参考,图 1-2 呈现了来自 Visual Studio 2019 的部分截图。
图 1-2
Visual Studio IDE 2019 中 MulticastDelegate 类的部分屏幕截图
图 1-3 显示了演示 1 中Main
方法的 IL 代码。
图 1-3
先前演示中的Main
方法的 IL 代码的部分屏幕截图
在图 1-3 中,箭头指向的那一行表示delOb(10,20)
是delOb.Invoke(10,20).
的语法快捷方式
Points to Remember
-
那个。NET 框架定义了委托和组播委托类。当你创建一个委托时,C# 编译器生成一个从 MulticastDelegate 派生的类,后者从 Delegate 类派生。
-
Only the C# compiler can create a class that derives from the Delegate class or the
MulticastDelegate
class, but you cannot do the same. In other words, these delegate types are implicitly sealed. You will get a compile-time error if you write something like the following.class MyClass : Delegate { }
或者,
class MyClass : MulticastDelegate { }
-
在演示中,您看到了
delOb(10,20)
是delOb.Invoke(10,20)
的语法快捷方式。因此,在实际编程中,最好在调用操作之前进行空检查。 -
委托方法也被称为可调用实体。
问答环节
1.1 在演示 1 中,您在 Program 类之外定义了委托。这是强制性的吗?
不。因为它是一个类类型,你可以在类内部,类外部,或者在名字空间的开始定义它。
1.2 你说只有 C# 编译器可以创建从委托类或者 MulticastDelegate
类 派生的类,但是你不能这么做。你的意思是这些委托类型是隐式密封的吗?
是的。
1.3 委托的使用仅限于静态方法吗?
您可以使用委托引用静态和非静态方法。委托不关心调用该方法的对象类型。所以,这位委托
delegate int MyDelegate(int aNumber);
它可以引用实例方法。
public int Factorial(int i)
{
// method body
}
也可以参考下面的静态方法。
public static int MyStaticMethod(int a)
{
// method body
}
但是当您在委托的上下文中使用静态方法或非静态方法时,有一些重要的区别。您将很快看到关于这一点的案例研究。
比较静态方法和实例方法
我已经说过,您可以将静态方法和实例方法分配给委托对象。为了演示这一点,我修改了演示 1。我添加了一个新类OutsideProgram
,并在其中放置了一个名为CalculateSum
的实例方法。我已经将静态方法Sum
和实例方法CalculateSum
分配给委托实例delOb
,并分析了每种情况。
在每种情况下,您都会看到以下代码行。
Console.WriteLine("delOb.Target={0}", delOb.Target);
Console.WriteLine("delOb.Target==null? {0}", delOb.Target == null);
Console.WriteLine("delOb.Method={0}",delOb.Method);
这些代码行的输出表明,当您将非静态方法分配给委托对象时,该对象不仅维护对该方法的引用,还维护对该方法所属的实例的引用。
委托类中的目标属性可用于验证这一点。这就是为什么在这个上下文中比较静态方法和实例方法时,您可能会注意到前两行的输出不同。为了便于参考,我向您展示了 Visual Studio 中对目标属性的描述,如下所示。
// Summary:
// Gets the class instance on which the current delegate invokes //the instance method.
//
// Returns:
//The object on which the current delegate invokes the instance //method, if the delegate represents an instance method; null //if the delegate represents a static method.
[NullableAttribute(2)]
public object? Target { get; }
这个来自 Visual Studio 的描述还说,如果你给委托对象delOb
分配一个静态方法,那么delOb.Target
将包含null
。
演示 2
using System;
namespace DelegateExample2
{
delegate int DelegateWithTwoIntParameterReturnInt(int x, int y);
class Program
{
public static int Sum(int a, int b)
{
return a + b;
}
static void Main(string[] args)
{
Console.WriteLine("***Comparing the behavior of a static method and instance method when assign them to a delegate instance.***");
Console.WriteLine("Assigning a static method to a delegate object.");
//Assigning a static method to a delegate object.
DelegateWithTwoIntParameterReturnInt delOb = Sum;
Console.WriteLine("Calling Sum(..) method of Program Class using a delegate.");
int total = delOb(10, 20);
Console.WriteLine("Sum of 10 and 20 is: {0}", total);
Console.WriteLine("delOb.Target={0}", delOb.Target);
Console.WriteLine("delOb.Target==null? {0}", delOb.Target == null);//True
Console.WriteLine("delOb.Method={0}", delOb.Method);
OutSideProgram outsideOb = new OutSideProgram();
Console.WriteLine("\nAssigning an instance method to a delegate object.");
//Assigning an instance method to a delegate object.
delOb = outsideOb.CalculateSum;
Console.WriteLine("Calling CalculateSum(..) method of OutsideProgram class using a delegate.");
total = delOb(50, 70);
Console.WriteLine("Sum of 50 and 70 is: {0}", total);
Console.WriteLine("delOb.Target={0}", delOb.Target);//delOb.Target=DelegateEx1.OutSideProgramClass
Console.WriteLine("delOb.Target==null? {0}", delOb.Target == null);//False
Console.WriteLine("delOb.Method={0}", delOb.Method);
Console.ReadKey();
}
}
class OutSideProgram
{
public int CalculateSum(int x, int y)
{
return x + y;
}
}
}
输出
这是输出。我加粗了几行以引起你的注意。
***Comparing the behavior of a static method and instance method when assign them to a delegate instance.***
Assigning a static method to a delegate object.
Calling Sum(..) method of Program Class using a delegate.
Sum of 10 and 20 is: 30
delOb.Target=
delOb.Target==null? True
delOb.Method=Int32 Sum(Int32, Int32)
Assigning an instance method to a delegate object.
Calling CalculateSum(..) method of OutsideProgram class using a delegate.
Sum of 50 and 70 is: 120
delOb.Target=DelegateExample2.OutSideProgram
delOb.Target==null? False
delOb.Method=Int32 CalculateSum(Int32, Int32)
使用多播代理
通过使用委托实例,可以引用多个目标方法。您可以通过使用 += 操作符来实现这一点。当一个委托用于封装一个匹配签名的多个方法时,它就是一个组播委托。这些委托是System.MulticastDelegate
的子类型,?? 是System.Delegate
的子类。
在下面的示例中,您以三个方法为目标。为了演示一般情况,我将静态和实例方法结合到委托对象中。使用了以下带有支持性注释的代码段。
// Target a static method
MultiDelegate multiDel = MethodOne;
// Target another static method
multiDel += MethodTwo;
// Target an instance method
multiDel += new OutsideProgram().MethodThree;
在这种情况下,按照您在调用链中添加委托的顺序调用委托。当您调用multiDel()
时,这三个方法都会被调用。
Points to Remember
-
下面两行代码在功能上是等效的。
multiDel += MethodTwo; //Same as the following line multiDel = multiDel+MethodTwo;
-
当您使用多播委托时,委托按照您在调用链中添加它们的顺序被调用。
您可以通过使用 += 操作符来增加方法链。类似地,您可以通过使用 -= 操作符来减少链。为了演示这一点,在下面的例子中我第二次调用multiDel()
之前,我使用下面的代码行从链中删除了MethodTwo
。
multiDel -= MethodTwo;
现在来看下面的例子,它展示了使用多播委托的完整演示。
演示 3
using System;
namespace MulticastDelegateExample1
{
delegate void MultiDelegate();
class Program
{
public static void MethodOne()
{
Console.WriteLine("A static method of Program class- MethodOne() executed.");
}
public static void MethodTwo()
{
Console.WriteLine("A static method of Program class- MethodTwo() executed.");
}
static void Main(string[] args)
{
Console.WriteLine("***Example of a Multicast Delegate.***");
// Target a static method
MultiDelegate multiDel = MethodOne;
// Target another static method
multiDel += MethodTwo;
//Target an instance method
multiDel += new OutsideProgram().MethodThree;
multiDel();
//Reducing the delegate chain
Console.WriteLine("\nReducing the length of delegate chain by discarding MethodTwo now.");
multiDel -= MethodTwo;
//The following invocation will call MethodOne and MethodThree now.
multiDel();
Console.ReadKey();
}
}
class OutsideProgram
{
public void MethodThree()
{
Console.WriteLine("An instance method of OutsideProgram class is executed.");
}
}
}
输出
以下是运行该程序的输出。
***Example of a Multicast Delegate.***
A static method of Program class- MethodOne() executed.
A static method of Program class- MethodTwo() executed.
An instance method of OutsideProgram class is executed.
Reducing the length of delegate chain by discarding MethodTwo now.
A static method of Program class- MethodOne() executed.
An instance method of OutsideProgram class is executed.
分析
在演示 3 中,您看到了目标方法具有 void 返回类型。这是因为多播委托通常用于具有 void 返回类型的方法。
问答环节
1.4 你说过多播委托经常用于具有 void 返回类型 的 方法。这是什么原因呢?
多播委托以调用列表中的多个方法为目标。但是,单个方法或委托调用只能返回单个值。如果在多播委托调用中使用多个具有非 void 返回类型的方法,您将从调用列表中的最后一个方法获得返回值。尽管也调用了其他方法,但这些值都被丢弃了。下面的例子让你对此有一个更清晰的了解。
演示 4
using System;
namespace MulticastDelegateExample2
{
delegate int MultiDelegate();
class Program
{
public static int MethodOne()
{
Console.WriteLine("A static method of Program class- MethodOne() executed.");
return 1;
}
public static int MethodTwo()
{
Console.WriteLine("A static method of Program class- MethodTwo() executed.");
return 2;
}
public static int MethodThree()
{
Console.WriteLine("A static method of Program class- MethodThree() executed.");
return 3;
}
static void Main(string[] args)
{
Console.WriteLine("***A case study with a multicast delegate when we target non-void methods.***");
// Target MethodOne
MultiDelegate multiDel = MethodOne;
// Target MethodTwo
multiDel += MethodTwo;
// Target MethodThree
multiDel += MethodThree;
int finalValue=multiDel();
Console.WriteLine("The final value is {0}", finalValue);
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***A case study with a multicast delegate when we target non-void methods.***
A static method of Program class- MethodOne() executed.
A static method of Program class- MethodTwo() executed.
A static method of Program class- MethodThree() executed.
The final value is 3
分析
调用列表中的三个方法(MethodOne()
、MethodTwo()
和MethodThree()
)被调用,但最终返回值是 3,它来自MethodThree
。
问答环节
我知道多播委托对于具有非 void 返回类型的方法没有用,因为中间返回值被丢弃了。但是我相信没有什么能阻止我储存这些价值观并以不同的方式使用它们。这是正确的吗?
绝对的。你总是可以收集那些价值,并随心所欲地使用它们;但很少有人做到。此外,在撰写本文时,C# 语言规范中还没有这方面的语法捷径。因此,如果对具有非 void 返回类型的方法使用多播委托,中间返回值将会丢失,这通常被认为是功能损失。
此外,您需要特别注意异常处理。如果调用列表中的方法抛出异常,其他方法将没有机会处理它。
你能提供一个例子来说明当我使用多播委托时,为什么异常处理是一个问题吗?
让我们将演示 3 中的MethodOne()
修改如下。
public static void MethodOne()
{
Console.WriteLine("A static method of Program class- MethodOne() executed.");
// For Q&A 1.6
// Let's say, some code causes an exception
// like the following
int a = 10, b = 0,c;
c = a / b;
Console.WriteLine("c={0}",c);
}
现在再次执行程序。这一次,您将得到下面的异常,结果,调用列表中的下一个方法将不会执行。这就是MethodTwo()
不会运行的原因;它没有机会处理异常。图 1-4 是来自 Visual Studio 的运行时截图。
图 1-4
Visual Studio IDE 中的运行时错误屏幕截图
1.7 在演示 1 中,您使用了以下代码行:
DelegateWithTwoIntParameterReturnInt delOb = Sum;
现在我很担心。如果我重载 Sum 方法会发生什么?
没关系。委托的作用类似于类型安全的函数指针,因为它们可以准确地跟踪完整的方法签名(例如,参数的数量、参数的类型、方法的返回类型)。
当您使用委托并具有重载方法时,编译器可以为您绑定正确的方法。为了研究这个问题,考虑下面的例子,其中的Sum
方法是重载的(我使用了静态方法,但是您也可以使用实例方法)。Sum 方法有两个重载版本。一种情况下,Sum
方法接受两个 int 参数,另一种情况下,接受三个 int 参数;但是DelegateWithTwoIntParameterReturnInt
可以适当地绑定预定的方法。
演示 5
using System;
namespace CaseStudyWithOverloadedMethods
{
delegate int DelegateWithTwoIntParameterReturnInt(int x, int y);
class Program
{
public static int Sum(int a, int b)
{
return a + b;
}
public static int Sum(int a, int b, int c)
{
return a + b + c;
}
static void Main(string[] args)
{
Console.WriteLine("***A case study with overloaded methods.***");
DelegateWithTwoIntParameterReturnInt delOb = Sum;
Console.WriteLine("\nCalling Sum(..) method using a delegate.");
int total = delOb(10, 20);
Console.WriteLine("Sum of 10 and 20 is: {0}", total);
Console.ReadKey();
}
}
}
输出
运行这个程序时,您会得到以下输出。
***A case study with overloaded methods.***
Calling Sum(..) method using a delegate.
Sum of 10 and 20 is: 30
分析
需要注意的是,如果没有正确的重载版本,就会出现编译时错误。例如,如果您注释掉预期的方法,如下所示,
//public static int Sum(int a, int b)
//{
// return a + b;
//}
您将得到以下编译错误:
No Overload for 'Sum' matches delegate 'DelegateWithTwoIntParameterReturnInt'
图 1-5 是来自 Visual Studio IDE 的部分截图。
图 1-5
Visual Studio IDE 中的编译时错误屏幕截图
问答环节
1.8 如何常用委托?
您会看到在事件处理和回调方法中使用委托(尤其是在异步编程中)。我将在本书后面的章节中讨论这一点。
1.9 我可以使用委托来指向构造函数吗?
不会。但是通过编程,您可以实现类似的效果。例如,考虑演示 2。让我们为OutsideProgram
类提供一个公共构造函数。经过这样的修改,看起来是这样的。
class OutSideProgram
{
//For Q&A 1.9
public OutSideProgram()
{
Console.WriteLine("\nOutSideProgram constructor is called.");
}
public int CalculateSum(int x, int y)
{
return x + y;
}
}
让我们定义一个委托,如下所示。
delegate OutSideProgram ConsGenerator();
现在,在 Main 中,你可以写下面几行(我在这里用了一个 lambda 表达式。你将在第三章中学习 lambda 表达式。
// For Q&A 1.9
ConsGenerator consGenerator =() =>
{
return new OutSideProgram();
};
consGenerator();
如果您现在执行程序,您将在输出中看到消息“OutSideProgram 构造函数被调用”。简而言之,你可以使用一个方法来模仿构造函数的行为。我在那里使用了 lambda 表达式,因为我还没有引入任何可以做同样事情的新方法。
1.10 我了解到在方法重载中,方法的返回类型并不重要,但是在委托的上下文中,它看起来很重要。这是正确的吗?
是的。这是需要记住的重要一点。
委托的差异
当实例化委托时,可以为它分配一个方法,该方法具有比最初指定的返回类型“更派生”的返回类型。这种支持在 C # 2.0 版及更高版本中可用。另一方面,逆变允许方法的参数类型比委托类型派生得少。总的来说,协方差和逆变称为方法组方差。
为了更好地理解,让我们从数学开始,从数学的角度来探讨重要的术语。让我们假设你有一个整数域。
对于情况 1,假设你有一个函数,f(x)=x+2(对于所有, x 属于整数)。如果 x ≤ y ,那么你也可以说f(x)≤f(y)对于所有 x 。投影(函数 f )保持大小的方向(我的意思是,在使用函数之前,如果左手边的部分小于(或大于)右手边的部分,在应用函数之后,同样会保持)。
对于情况 2,我们再考虑另一个函数:f(x)=–x(对于所有, x 属于整数)。在这种情况下,可以看到 10 ≤ 20 但 f (10) ≥ f (20)(自f(10)=–10,f(20)=–20 和–10>–20)。所以,投影是反方向的。
对于情况 3,我们来考虑以下函数,f(x)=xx(对于所有, x 属于整数)。在这种情况下,可以看到–1≤0 和f*(–1)>f(0)。另一方面,1 < 2 和f(1)<f(2)。投影(功能 f )既不保持尺寸方向,也不反转尺寸方向。
在情况 1 中,函数 f 是协变的;在情况 2 中,函数 f 是逆变的;而在情况 3 中,函数 f 是不变的。
在 C# 编程中,可以用匹配的签名将方法分配给委托。但是可能有这样的情况,当你的方法的返回类型与委托的返回类型不完全匹配,但是你发现这个方法的返回类型是委托的返回类型的派生类型。在这种情况下,协方差允许您将方法与委托相匹配。因此,简单地说,协方差允许您匹配具有比委托中定义的“原始返回类型”更“派生”的返回类型的方法。
逆变处理参数。它允许一个方法拥有一个比委托类型派生程度低的参数类型。
Points to Remember
让我们记住以下几点。
-
协方差允许你在需要父类型的地方传递一个派生类型;对于委托,您可以将这个概念应用于返回类型。
-
Contravariance 允许你使用比最初指定的更通用(更少派生)的类型。使用委托,可以将带有基类参数的方法分配给期望获取派生类参数的委托。
-
不变性允许你只使用最初指定的类型。它既不是协变的也不是逆变的。
协方差和逆变统称为方差。
协方差的概念从 C#1.0 开始就支持数组。你可以这样写:
Console.WriteLine("***Covariance in arrays(C#1.0 onwards)***");
// It is not type safe
object[] myObjArray = new string[5];
// Following line will cause run-time error
myObjArray[0] = 10;
但是这段代码将导致运行时错误,输出如下内容。
System.ArrayTypeMismatchException: 'Attempted to access an element as a type incompatible with the array.'
委托中的协方差
从 2.0 版开始,委托就支持协变和逆变。对泛型类型参数、泛型接口和泛型委托的支持始于 C# 4.0。我还没有讨论泛型类型。本节讨论非泛型委托,从协方差开始。在接下来的例子中,Bus
类派生自Vehicle
类。所以,你很容易理解我用Vehicle
作为基础类型,用Bus
作为派生类型。
演示 6
using System;
namespace CovarianceWithNonGenericDelegate
{
class Vehicle
{
public Vehicle CreateVehicle()
{
Vehicle myVehicle = new Vehicle();
Console.WriteLine(" Inside Vehicle.CreateVehicle, a vehicle object is created.");
return myVehicle;
}
}
class Bus : Vehicle
{
public Bus CreateBus()
{
Bus myBus = new Bus();
Console.WriteLine(" Inside Bus.CreateBus, a bus object is created.");
return myBus;
}
}
class Program
{
public delegate Vehicle VehicleDelegate();
static void Main(string[] args)
{
Vehicle vehicleOb = new Vehicle();
Bus busOb = new Bus();
Console.WriteLine("***Testing covariance with delegates. It is allowed C# 2.0 onwards.***\n");
// Normal case:
/* VehicleDelegate is expecting a method with return type Vehicle.*/
VehicleDelegate vehicleDelegate1 = vehicleOb.CreateVehicle;
vehicleDelegate1();
/* VehicleDelegate is expecting a method with return type Vehicle(i.e. a basetype) but you're assigning a method with return type Bus( a derived type) Covariance allows this kind of assignment.*/
VehicleDelegate vehicleDelegate2 = busOb.CreateBus;
vehicleDelegate2();
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Testing covariance with delegates. It is allowed C# 2.0 onwards.***
Inside Vehicle.CreateVehicle, a vehicle object is created.
Inside Bus.CreateBus, a bus object is created.
分析
请注意这一行代码以及前面程序中的支持注释。
/* VehicleDelegate is expecting a method with return type
Vehicle(i.e. a basetype)but you're assigning a method with
return type Bus( a derived type)
Covariance allows this kind of assignment.*/
VehicleDelegate vehicleDelegate2 = busOb.CreateBus;
编译器没有抱怨这一行,因为协方差提供了这种灵活性。
委托的矛盾
逆变与参数有关。假设委托可以指向接受派生类型参数的方法。使用 contravariance,可以使用同一个委托指向接受基类型参数的方法。
演示 7
using System;
namespace ContravarianceWithNonGenegicDelegate
{
class Vehicle
{
public void ShowVehicle(Vehicle myVehicle)
{
Console.WriteLine("Vehicle.ShowVehicle is called.");
Console.WriteLine("myVehicle.GetHashCode() is: {0}", myVehicle.GetHashCode());
}
}
class Bus : Vehicle
{
public void ShowBus(Bus myBus)
{
Console.WriteLine("Bus.ShowBus is called.");
Console.WriteLine("myBus.GetHashCode() is: {0}", myBus.GetHashCode());
}
}
class Program
{
public delegate void BusDelegate(Bus bus);
static void Main(string[] args)
{
Console.WriteLine("***Demonstration-7.Exploring Contravariance with non-generic delegates***");
Vehicle myVehicle = new Vehicle();
Bus myBus = new Bus();
//Normal case
BusDelegate busDelegate = myBus.ShowBus;
busDelegate(myBus);
// Special case
// Contravariance:
/*
* Note that the following delegate expected a method that accepts a Bus(derived) object parameter but still it can point to the method that accepts Vehicle(base) object parameter
*/
BusDelegate anotherBusDelegate = myVehicle.ShowVehicle;
anotherBusDelegate(myBus);
// Additional note:you cannot pass vehicle object here
// anotherBusDelegate(myVehicle);//error
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Demonstration-7.Exploring Contravariance with non-generic delegates***
Bus.ShowBus is called.
myBus.GetHashCode() is: 58225482
Vehicle.ShowVehicle is called.
myVehicle.GetHashCode() is: 58225482
分析
您可以看到在前面的例子中,BusDelegate
接受一个Bus
类型参数。仍然使用 contravariance,当实例化一个BusDelegate
对象时,可以指向一个接受Vehicle
类型参数的方法。因此,逆变允许以下类型的赋值。
BusDelegate anotherBusDelegate = myVehicle.ShowVehicle;
在这两种情况下,我将同一个对象传递给了两个委托对象。因此,您会在输出中看到相同的哈希代码。本例中保留了支持性注释,以帮助您理解。
问答环节
1.11 您使用了术语 方法组方差 。为什么叫方法组?
MSDN 强调以下几点。
-
方法组,它是成员查找产生的一组重载方法。
-
方法组允许出现在 invocation _ expression(In location expressions)、delegate _ creation _ expression(delegate creation expressions)以及 is 运算符的左侧 - ,并且可以隐式转换为兼容的委托类型( m 方法组转换)。在任何其他上下文中,被分类为方法组的表达式会导致编译时错误。
使用重载方法的演示 5 案例研究包括以下代码行。
DelegateWithTwoIntParameterReturnInt delOb = Sum;
这里,Sum
指的是一个方法组。当您使用这种语句时(即,方法参数没有括号),组中的所有方法在相同的上下文中都可用,但是方法组转换可以创建调用预期方法的委托。但是在参数中包含括号的情况下,方法调用可以容易且明确地识别出来。
最后的话
您总是可以创建和使用自己的委托,但是在实际编程中,使用现成的构造可能有助于节省时间和精力。在这种情况下,Func
、Action
和Predicate
委托非常有用。但是当你在本书后面学习高级主题时,你可以有效地使用它们;比如 lambda 表达式和泛型编程。让我们暂时跳过这个话题,跳到下一个话题:事件。
摘要
本章涵盖了以下关键问题。
-
什么是委托?
-
什么是多播代理?
-
什么时候应该使用多播委托?
-
当您用委托定位静态方法和实例方法时,如何区分这些方法?
-
如何使用委托实现协变和逆变?
-
委托通常是如何使用的?
你知道当我们编译。net 程序使用任何。Net 听话的语言像 C#,最初我们的源代码会被转换成一个中间代码,这就是所谓的 MSIL(微软中间语言)。这个 IL 代码由 CLR(公共语言运行时)解释。在程序执行时,这个 IL 代码将被转换成二进制可执行二进制代码或本机代码。
CLR 是一个框架层,它位于操作系统之上,处理。net 应用。程序必须通过 CLR,这样就不会与操作系统直接通信。
二、事件
对事件的支持被认为是 C# 中最激动人心的特性之一。
以下是事件的一些基本特征。我建议您在使用事件编码之前,反复阅读这些要点。
-
事件的支柱是委托,所以在使用事件之前了解委托是很重要的。
-
使用事件时,一段代码可以向另一段代码发送通知。
-
事件通常在 GUI 应用中使用。例如,当您单击一个按钮或选择一个单选按钮时,您可能会注意到 UI 布局中一些有趣的变化。
-
在发布者-订阅者模型中,一个对象引发一个通知(事件),一个或多个对象侦听这些事件。引发事件的对象称为发送者(或发布者或广播者),接收事件的对象称为接收者(或订阅者)。发送者不关心接收者如何解释事件。它可能不关心谁在注册以接收或取消注册以停止接收事件或通知。你可以把这和脸书或者推特联系起来。如果您关注某人,您可以在此人更新个人资料时收到通知。如果您不想收到通知,您可以随时取消订阅。简而言之,订户可以决定何时开始收听事件或何时停止收听事件。(用编程术语来说,就是什么时候注册事件,什么时候注销事件)。
-
英寸 NET 中,事件被实现为多播委托。
-
发布者包含委托。订阅者在发布者的委托上使用+=进行注册,在该委托上使用-=进行注销。所以,当我们将+=或-=应用于一个事件时,有一个特殊的含义(换句话说,它们不是赋值的快捷键)。
-
订户彼此不通信。因此,您可以构建一个松散耦合的系统。这通常是事件驱动架构的关键目标。
-
在 GUI 应用中,Visual Studio IDE 可以让您在处理事件时更加轻松。(我相信,既然这些概念是 C# 的核心,不如从基础开始学。)
-
那个。NET framework 提供了一个支持标准事件设计模式的泛型委托,如下所示:
public delegate void EventHandler<TEventArgs>(object sendersource, TEventArgs e), where TEventArgs : EventArgs;.
我还没有讨论泛型,所以你现在可以跳过这一点。但是有趣的是,为了支持向后兼容性,在。NET framework 遵循非泛型自定义委托模式。
-
下面是一个事件声明的示例:
public event EventHandler MyIntChanged;
这只是表明
MyIntChanged
是事件的名称,而EventHandler
是相应的代表。 -
修饰符不需要是公共的。你可以为你的事件选择非公开的修饰语,比如
private
、protected
、internal
等等。在这种情况下,你也可以使用关键字static
、virtual
、override
、abstract
、sealed
和new
。
演示 1
现在您已经准备好编码了。在声明事件之前,您需要一个委托。在示例中,您会看到下面的代码行。
public event EventHandler MyIntChanged;
但是您看不到委托声明,因为我使用了预定义的EventHandler
委托。
现在让我们关注我们的实现。有两类:Sender
和Receiver
。Sender
扮演广播员的角色;当您更改myInt
实例值时,它会引发MyIntChanged
事件。Receiver
类扮演消费者的角色。它有一个方法叫做GetNotificationFromSender
。要从发件人处获得通知,请注意下面的代码行。
// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
这里的sender
是一个Sender
类对象,receiver
是一个Receiver
类对象。最终,接收者不再对从发送者那里获得进一步的通知感兴趣,并使用下面的代码取消订阅事件。
// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
值得注意的是,发送者可以向自己发送通知。为了演示这一点,在最后几行of Main
中,您会看到下面的代码。
// Sender will receive its own notification now onwards
sender.MyIntChanged += sender.GetNotificationItself;
using System;
namespace EventEx1
{
class Sender
{
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
//Whenever we set a new value, the event will fire.
OnMyIntChanged();
}
}
//EventHandler is a predefined delegate which is used to //handle simple events.
//It has the following signature:
//delegate void System.EventHandler(object sender,System.EventArgs e)
//where the sender tells who is sending the event and
//EventArgs is used to store information about the event.
public event EventHandler MyIntChanged;
public void OnMyIntChanged()
{
if(MyIntChanged!=null )
{
MyIntChanged(this, EventArgs.Empty);
}
}
public void GetNotificationItself(Object sender, System.EventArgs e)
{
Console.WriteLine("Sender himself send a notification: I have changed myInt value to {0} ", myInt);
}
}
class Receiver
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring events.***");
Sender sender = new Sender();
Receiver receiver = new Receiver();
//Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
//Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
//No notification sent for the receiver now.
sender.MyInt = 3;
//Sender will receive its own notification now onwards.
sender.MyIntChanged += sender.GetNotificationItself;
sender.MyInt = 4;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Exploring events.***
Receiver receives a notification: Sender recently has changed the myInt value.
Receiver receives a notification: Sender recently has changed the myInt value.
Sender himself send a notification: I have changed myInt value to 4
分析
最初,我使用MyInt
属性更改了发送者的myInt
值。当我将该值更改为 1 或 2 时,Receiver 对象(receiver
)收到了通知,因为它订阅了该事件。然后receiver
退订了。因此,当我将值改为 3 时,receiver
没有任何通知。然后sender
订阅事件通知。结果,当我将值改为 4 时,sender
收到了通知。
Note
在现实应用中,一旦你订阅了一个事件,你也应该在离开前退订该事件;否则,您可能会看到内存泄漏的影响。
问答环节
2.1 我可以在特定事件上使用任何方法吗?
不。它应该与委托签名相匹配。例如,让我们假设Receiver
类有另一个名为UnRelatedMethod
的方法,如下所示。
public void UnRelatedMethod()
{
Console.WriteLine(" An unrelated method. ");
}
在演示 1 中,如果您通过使用语句用MyIntChanged
附加了这个方法
sender.MyIntChanged += receiver.UnRelatedMethod;//Error
您将得到以下编译时错误:
CS0123 No overload for 'UnRelatedMethod' matches delegate 'EventHandler'
创建自定义事件
在演示 1 中,您看到了一个内置的委托,但是在许多情况下,您可能需要自己的事件来处理特定的场景。让我们来练习一个关于自定义事件的程序。为了使这个例子简单明了,我们假设发送者不需要给自己发送任何通知。所以,现在Sender
类中没有类似GetNotificationItself
的方法。
为了使更改与前面的示例保持一致,让我们按照以下步骤操作。
-
创建代理人。按照惯例,选择带
EventHandler
后缀的代理名称;大概如下:delegate void MyIntChangedEventHandler(Object sender, EventArgs eventArgs);
-
定义您的活动。按照惯例,您可以去掉代理名称的后缀
EventHandler
并设置您的事件名称。public event MyIntChangedEventHandler MyIntChanged;
-
引发事件。让我们在 Sender 类中使用下面的方法。一般情况下,不做方法
public
,建议你做方法protected virtual
。protected virtual void OnMyIntChanged() { if (MyIntChanged != null) { MyIntChanged(this, EventArgs.Empty); } }
-
处理事件。让我们使用一个
Receiver
类,它有下面的方法来处理被引发的事件。让我们保持与演示 1 中的相同。class Receiver { public void GetNotificationFromSender(Object sender, System.EventArgs e) { Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . "); } }
演示 2
现在进行完整的演示。
using System;
namespace EventsEx2
{
//Step 1-Create a delegate.
//You can pick an name (this name will be your event name)
//which has the suffix EventHandler.For example, in the following case
//'MyIntChanged' is the event name which has the suffix 'EventHandler'
delegate void MyIntChangedEventHandler(Object sender, EventArgs eventArgs);
//Create a Sender or Publisher for the event.
class Sender
{
//Step-2: Create the event based on your delegate.
public event MyIntChangedEventHandler MyIntChanged;
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
//Raise the event.
//Whenever we set a new value, the event will fire.
OnMyIntChanged();
}
}
/*
Step-3.
In the standard practise, the method name is the event name with a prefix 'On'.For example, MyIntChanged(event name) is prefixed with 'On' here.
Also, in normal practises, instead of making the method 'public',
you make the method 'protected virtual'.
*/
protected virtual void OnMyIntChanged()
{
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
}
}
//Step-4: Create a Receiver or Subscriber for the event.
class Receiver
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring a custom event.***");
Sender sender = new Sender();
Receiver receiver = new Receiver();
//Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
//Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
//No notification sent for the receiver now.
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Exploring a custom event.***
Receiver receives a notification: Sender recently has changed the myInt value .
Receiver receives a notification: Sender recently has changed the myInt value .
分析
您可以看到,通过使用MyInt
属性,我正在更改myInt
的值。当该值设置为 1 或 2 时,接收方会收到通知,但是当myInt
值更改为 3 时,接收方没有收到通知,因为事件通知被取消订阅。
将数据传递给事件参数
让我们再来看看OnMyIntChanged
方法。在前两个演示中,我在方法中使用了下面一行代码。
MyIntChanged(this, EventArgs.Empty);
我没有在事件参数中传递任何东西。但是在现实编程中,你可能需要传递一些有意义的东西。让我们在演示 3 中分析这样一个案例。
演示 3
在这个演示中,我遵循了这些步骤。
-
创建
EventArgs
的子类。这个类有一个JobNo property
来设置jobNo
实例变量的值。 -
修改
OnMyIntChanged
方法,用事件封装预期数据(在本例中是job number
)。现在这个方法看起来如下:protected virtual void OnMyIntChanged() { if (MyIntChanged != null) { // Combine your data with the event argument JobNoEventArgs jobNoEventArgs = new JobNoEventArgs(); jobNoEventArgs.JobNo = myInt; MyIntChanged(this, jobNoEventArgs); }}
-
在这次演示中,我保持了相同的步骤。
这是完整的演示。
using System;
namespace EventsEx3
{
// Create a subclass of System.EventArgs
class JobNoEventArgs : EventArgs
{
int jobNo = 0;
public int JobNo
{
get { return jobNo; }
set { jobNo = value; }
}
}
// Create a delegate.
delegate void MyIntChangedEventHandler(Object sender, JobNoEventArgs eventArgs);
// Create a Sender or Publisher for the event.
class Sender
{
// Create the event based on your delegate.
public event MyIntChangedEventHandler MyIntChanged;
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
// Raise the event.
// Whenever you set a new value, the event will fire.
OnMyIntChanged();
}
}
/*
In the standard practise, the method name is the event name with a prefix 'On'.For example, MyIntChanged(event name) is prefixed with 'On' here.Also, in normal practises, instead of making the method 'public',you make the method 'protected virtual'.
*/
protected virtual void OnMyIntChanged()
{
if (MyIntChanged != null)
{ // Combine your data with the event argument
JobNoEventArgs jobNoEventArgs = new JobNoEventArgs();
jobNoEventArgs.JobNo = myInt;
MyIntChanged(this, jobNoEventArgs);
}
}
}
// Create a Receiver or Subscriber for the event.
class Receiver
{
public void GetNotificationFromSender(Object sender, JobNoEventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value to {0}.",e.JobNo);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Passing data in the event argument.***");
Sender sender = new Sender();
Receiver receiver = new Receiver();
// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
// No notification sent for the receiver now.
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Passing data in the event argument.***
Receiver receives a notification: Sender recently has changed the myInt value to 1.
Receiver receives a notification: Sender recently has changed the myInt value to 2.
使用事件访问器
让我们对演示 3 做一些有趣的修改。而不是使用
public event MyIntChangedEventHandler MyIntChanged;
使用下面的代码段。
private MyIntChangedEventHandler myIntChanged;
public event MyIntChangedEventHandler MyIntChanged
{
add
{
myIntChanged += value;
}
remove
{
myIntChanged -= value;
}
}
为了适应这种变化,让我们如下更新OnMyIntChanged
方法。
protected virtual void OnMyIntChanged()
{
if (myIntChanged != null)
{
// Combine your data with the event argument
JobNoEventArgs jobNoEventArgs = new JobNoEventArgs();
jobNoEventArgs.JobNo = myInt;
myIntChanged(this, jobNoEventArgs);
}
}
现在如果你执行这个程序,你会得到同样的输出。这怎么可能?编译器的工作方式类似于您声明事件时的方式。让我们回到事件的基本原理。
事件是一种特殊的多播委托,您只能从包含该事件的类中调用它。接收者可以订阅该事件,并使用其中的方法处理该事件。因此,接收者在订阅事件时传递方法引用。因此,此方法通过事件访问器添加到委托的订阅列表中。这些事件访问器类似于属性访问器,只是它们被命名为add
和remove
。
通常,您不需要提供自定义事件访问器。但是当您定义它们时,您是在指示 C# 编译器不要为您生成默认的字段和访问器。
在撰写本文时,基于。NET 框架目标 c# 7.3;鉴于。NET 核心应用面向 C# 8.0。如果您在。NET Framework(我们将其重命名为EventEx3DotNetFramework
)并研究 IL 代码,您会注意到 IL 代码中出现了add_<EventName>
和remove_<EventName>
。图 2-1 是 IL 代码的部分截图。
图 2-1
IL 代码的部分截图
演示 4
我们来做一个完整的演示,如下。
using System;
namespace EventsEx4
{
//Create a subclass of System.EventArgs
class JobNoEventArgs : EventArgs
{
int jobNo = 0;
public int JobNo
{
get { return jobNo; }
set { jobNo = value; }
}
}
// Create a delegate.
delegate void MyIntChangedEventHandler(Object sender, JobNoEventArgs eventArgs);
// Create a Sender or Publisher for the event.
class Sender
{
// Create the event based on your delegate.
#region equivalent code
// public event MyIntChangedEventHandler MyIntChanged;
private MyIntChangedEventHandler myIntChanged;
public event MyIntChangedEventHandler MyIntChanged
{
add
{
Console.WriteLine("***Inside add accessor.Entry point.***");
myIntChanged += value;
}
remove
{
myIntChanged -= value;
Console.WriteLine("***Inside remove accessor.Exit point.***");
}
}
#endregion
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
// Raise the event.
// Whenever we set a new value, the event will fire.
OnMyIntChanged();
}
}
protected virtual void OnMyIntChanged()
{
// if (MyIntChanged != null)
if (myIntChanged != null)
{
// Combine your data with the event argument
JobNoEventArgs jobNoEventArgs = new JobNoEventArgs();
jobNoEventArgs.JobNo = myInt;
// MyIntChanged(this, jobNoEventArgs);
myIntChanged(this, jobNoEventArgs);
}
}
}
// Create a Receiver or Subscriber for the event.
class Receiver
{
public void GetNotificationFromSender(Object sender, JobNoEventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value to {0}.", e.JobNo);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using event accessors.***");
Sender sender = new Sender();
Receiver receiver = new Receiver();
// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
// No notification sent for the receiver now.
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Using event accessors.***
***Inside add accessor.Entry point.***
Receiver receives a notification: Sender recently has changed the myInt value to 1.
Receiver receives a notification: Sender recently has changed the myInt value to 2.
***Inside remove accessor.Exit point.***
分析
当您使用事件访问器时,请记住一个重要的建议:实现锁定机制。例如,当您编写以下代码段时,可以改进演示 4。
public object lockObject = new object();
private MyIntChangedEventHandler myIntChanged;
public event MyIntChangedEventHandler MyIntChanged
{
add
{
lock (lockObject)
{
Console.WriteLine("***Inside add accessor.Entry point.***");
myIntChanged += value;
}
}
remove
{
lock (lockObject)
{
myIntChanged -= value;
Console.WriteLine("***Inside remove accessor.Exit point.***");
}
}
}
问答环节
2.2 使用用户定义的事件访问器的主要好处是什么?
让我们仔细看看下面这段代码。
private MyIntChangedEventHandler myIntChanged;
public event MyIntChangedEventHandler MyIntChanged
{
add
{
myIntChanged += value;
}
remove
{
myIntChanged -= value;
}
}
注意,这些事件访问器类似于属性访问器,除了它们被命名为add
和remove
。这里你在你的委托周围使用了一个类似属性的包装。因此,只有包含类可以直接调用委托;外人不能这么做。这促进了更好的安全性和对代码的控制。
处理界面事件
接口可以包含事件。当您实现接口方法或属性时,您需要遵循相同的规则。以下示例显示了这样的实现。
演示 5
在这个例子中,IMyInterface
有一个MyIntChanged
事件。我使用了Sender
和Receiver
,它们与前面的例子相同。唯一不同的是,这一次,Sender
类实现了IMyInterface
接口。
using System;
namespace EventEx5
{
interface IMyInterface
{
// An interface event
event EventHandler MyIntChanged;
}
class Sender : IMyInterface
{
// Declare the event here and raise from your intended location
public event EventHandler MyIntChanged;
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
// Setting a new value prior to raise the event.
myInt = value;
OnMyIntChanged();
}
}
protected virtual void OnMyIntChanged()
{
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
}
}
class Receiver
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring an event with an interface.***");
Sender sender = new Sender();
Receiver receiver = new Receiver();
// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
// No notification sent for the receiver now.
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Exploring an event with an interface.***
Receiver receives a notification: Sender recently has changed the myInt value .
Receiver receives a notification: Sender recently has changed the myInt value .
问答环节
2.3 当接口事件同名时,我的类如何实现多个接口?
是的,这种情况很有意思。当您的类实现多个具有公共名称事件的接口时,您需要遵循显式接口实现技术。但是有一个重要的限制,即在这种情况下,您需要提供添加和移除事件访问器。通常,编译器可以提供这些访问器,但在这种情况下,它不能。下一节提供了完整的演示。
处理显式接口事件
为了简单起见,这个例子与前面的例子一致。我们假设现在你有两个接口:IBeforeInterface
和IAfterInterface
。进一步假设每个包含一个名为MyIntChanged.
的事件
Sender
类实现了这些接口。现在你有两个接收者:ReceiverBefore
和ReceiverAfter
。当myInt
改变时,这些接收者类想要得到通知。在这个例子中,ReceiverBefore
对象在myInt
改变之前得到通知,而ReceiverAfter
对象在myInt
改变之后得到通知。
在演示 4 中,您看到了如何实现事件访问器。这里遵循相同的机制。这一次,我遵循了微软的建议,所以您可以看到锁在事件访问器中的使用。
演示 6
完成下面的完整演示。
using System;
namespace EventEx6
{
interface IBeforeInterface
{
public event EventHandler MyIntChanged;
}
interface IAfterInterface
{
public event EventHandler MyIntChanged;
}
class Sender : IBeforeInterface, IAfterInterface
{
// Creating two separate events for two interface events
public event EventHandler BeforeMyIntChanged;
public event EventHandler AfterMyIntChanged;
// Microsoft recommends this, i.e. to use a lock inside accessors
object objectLock = new Object();
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
// Fire an event before we make a change to myInt.
OnMyIntChangedBefore();
Console.WriteLine("Making a change to myInt from {0} to {1}.",myInt,value);
myInt = value;
// Fire an event after we make a change to myInt.
OnMyIntChangedAfter();
}
}
// Explicit interface implementation required.
// Associate IBeforeInterface's event with
// BeforeMyIntChanged
event EventHandler IBeforeInterface.MyIntChanged
{
add
{
lock (objectLock)
{
BeforeMyIntChanged += value;
}
}
remove
{
lock (objectLock)
{
BeforeMyIntChanged -= value;
}
}
}
// Explicit interface implementation required.
// Associate IAfterInterface's event with
// AfterMyIntChanged
event EventHandler IAfterInterface.MyIntChanged
{
add
{
lock (objectLock)
{
AfterMyIntChanged += value;
}
}
remove
{
lock (objectLock)
{
AfterMyIntChanged -= value;
}
}
}
// This method uses BeforeMyIntChanged event
protected virtual void OnMyIntChangedBefore()
{
if (BeforeMyIntChanged != null)
{
BeforeMyIntChanged(this, EventArgs.Empty);
}
}
// This method uses AfterMyIntChanged event
protected virtual void OnMyIntChangedAfter()
{
if (AfterMyIntChanged != null)
{
AfterMyIntChanged(this, EventArgs.Empty);
}
}
}
// First receiver: ReceiverBefore class
class ReceiverBefore
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("ReceiverBefore receives : Sender is about to change the myInt value . ");
}
}
// Second receiver: ReceiverAfter class
class ReceiverAfter
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("ReceiverAfter receives : Sender recently has changed the myInt value . ");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Handling explicit interface events.***");
Sender sender = new Sender();
ReceiverBefore receiverBefore = new ReceiverBefore();
ReceiverAfter receiverAfter = new ReceiverAfter();
// Receiver's are registering for getting //notifications from Sender
sender.BeforeMyIntChanged += receiverBefore.GetNotificationFromSender;
sender.AfterMyIntChanged += receiverAfter.GetNotificationFromSender;
sender.MyInt = 1;
Console.WriteLine("");
sender.MyInt = 2;
// Unregistering now
sender.BeforeMyIntChanged -= receiverBefore.GetNotificationFromSender;
sender.AfterMyIntChanged -= receiverAfter.GetNotificationFromSender;
Console.WriteLine("");
// No notification sent for the receivers now.
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Handling explicit interface events.***
ReceiverBefore receives : Sender is about to change the myInt value .
Making a change to myInt from 0 to 1.
ReceiverAfter receives : Sender recently has changed the myInt value .
ReceiverBefore receives : Sender is about to change the myInt value .
Making a change to myInt from 1 to 2.
ReceiverAfter receives : Sender recently has changed the myInt value .
Making a change to myInt from 2 to 3.
问答环节
2.4 代理是事件的支柱,一般来说,当我们为事件编写代码以及注册和注销这些事件时,我们遵循观察者设计模式。这是正确的吗?
是的。
在这一章的开始,你说当我写一个关于某个事件的程序时,我也可以使用“new”关键字。能举个例子吗?
我基本上用的是简写形式。例如,在演示 1 中,当我注册事件时,您会看到下面一行代码。
sender.MyIntChanged += receiver.GetNotificationFromSender;
现在,如果您回忆一下在第一章的委托上下文中使用的缩写形式,您可以编写等价的代码,如下所示。
sender.MyIntChanged += new EventHandler(receiver.GetNotificationFromSender);
除此之外,考虑另一种情况,发送者类包含一个密封的事件。如果您有 Sender 的派生类,则它不能使用事件。相反,派生类可以使用“new”关键字来指示它没有重写基类事件。
你能举一个抽象事件的例子吗?
见演示 7。
演示 7
微软表示,对于一个抽象事件,你不会得到编译器生成的add
和remove
事件访问器块。所以,你的派生类需要提供自己的实现。让我们简化一下,稍微修改一下演示 1。像演示 2 一样,让我们假设在这个例子中,发送者不需要向自己发送通知。在这个演示中,Sender
类中没有GetNotificationItself
方法。
现在我们来关注关键部分。Sender 类包含一个抽象事件,如下所示。
public abstract event EventHandler MyIntChanged;
由于该类包含一个抽象事件,因此该类本身也变得抽象。
我现在将介绍另一个名为ConcreteSender
的类,它派生自 Sender。它重写事件并完成事件调用过程。
下面是ConcreteSender
的实现。
class ConcreteSender : Sender
{
public override event EventHandler MyIntChanged;
protected override void OnMyIntChanged()
{
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
}
}
现在我们来看一下完整的程序和输出。
using System;
namespace EventsEx7
{
abstract class Sender
{
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
// Whenever we set a new value, the event will fire.
OnMyIntChanged();
}
}
// Abstract event.The containing class becomes abstract for this.
public abstract event EventHandler MyIntChanged;
protected virtual void OnMyIntChanged()
{
Console.WriteLine("Sender.OnMyIntChanged");
}
}
class ConcreteSender : Sender
{
public override event EventHandler MyIntChanged;
protected override void OnMyIntChanged()
{
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
}
}
class Receiver
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring an abstract event.***");
Sender sender = new ConcreteSender();
Receiver receiver = new Receiver();
// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
// No notification sent for the receiver now.
sender.MyInt = 3;
Console.ReadKey();
}
}}
输出
以下是运行该程序的输出。
***Exploring an abstract event.***
Receiver receives a notification: Sender recently has changed the myInt value .
Receiver receives a notification: Sender recently has changed the myInt value .
问答环节
2.7 我知道 EventHandler
是一个预定义的代表。但是在很多地方,我看到人们在广义上使用术语 事件处理程序 。有什么特别的含义与之相关吗?
简单地说,事件处理程序是一个过程,当一个特定的事件发生时,你决定做什么。例如,当用户点击 GUI 应用中的按钮时。请注意,您的事件可以有多个处理程序,同时,处理事件的方法也可以动态变化。在本章中,你看到了事件是如何工作的,特别是 Receiver 类是如何处理事件的。但是如果您使用像 Visual Studio 中的 Windows 窗体设计器这样的现成构造,就可以非常容易地编写事件代码。
有一个如何在 GUI 应用中添加事件处理程序的例子会很有帮助。
让我们看看演示 8。
演示 8
在这个演示中,我创建了一个简单的 UI 应用来演示一个简单的事件处理机制。做这件事的步骤如下。
-
创建 Windows 窗体应用。
-
From the Toolbox, drag a button onto the form. Let’s name it Test. Figure 2-2 shows what it may look like.
图 2-2
放置在 Form1 上的测试按钮
-
Select the button. Open the Properties window and click the Events button. Name the Click event
TestBtnClickHandler
(see Figure 2-3).图 2-3
将点击事件名称设置为
TestBtnClickHandler
-
双击测试按钮。这将打开 Form1.cs 文件,您可以在其中为事件处理程序编写以下代码。
private void TestBtnClickHandler(object sender, EventArgs e) { MessageBox.Show("Hello Reader."); }
输出
运行您的应用并单击 Test 按钮。您会看到如图 2-4 所示的输出。(为了更好的截图,我在 Form1 上拖动了消息框窗口。)
图 2-4
单击“测试”按钮时,从 Visual Studio 输出屏幕截图
Note
演示 8 于年执行。但在. NET Framework 中没有。NET 核心。在撰写本文时,可视化设计器被标记为的“预览功能”。NET 核心应用,它受到了很多问题的困扰(更多信息,请访问 https://github.com/dotnet/winforms/blob/master/Documentation/designer-releases/0.1/knownissues.md
)。在解决方案资源管理器中单击 Form1.cs 文件时,在. NET 核心应用中看不到 Form1.cs[Design]。
最后的话
在演示 2 中,您看到了下面的代码段。
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
实际上,在所有示例中,在引发事件之前都会看到这种空检查。这很重要,因为如果事件没有监听器(或接收器),您可能会遇到一个名为NullReferenceException
的异常。在这种情况下,Visual Studio 会向您显示如图 2-5 所示的屏幕。
图 2-5
由于缺少事件侦听器和正确的空检查,发生了 NullReferenceException
在引发事件之前,空检查非常重要。但是你可以假设在一个真实的应用中,如果你需要做一些空值检查,这会让你的代码变得笨拙。在这种情况下,您可以使用从 C# 6.0 开始就有的功能。您可以使用空条件操作符来避免突然的意外。
我使用这个操作符提供了另一个代码段。(我保留了带注释的死代码,以便您可以同时比较两个代码段)。
//if (MyIntChanged != null)
//{
// MyIntChanged(this, EventArgs.Empty);
//}
//Alternate code
MyIntChanged?.Invoke(this, EventArgs.Empty);
这都是关于事件的。现在让我们继续第三章,在这里你将学习使用 C# 中另一个强大的特性:lambda 表达式。
摘要
本章讨论了以下关键问题。
-
什么是事件?如何使用内置的事件支持?
-
如何编写自定义事件?
-
如何将数据传递给事件参数?
-
如何使用事件访问器?它们为什么有用?
-
如何使用不同的界面事件?
-
如何将不同的修饰语和关键词应用到一个事件中?
-
如何在简单的 UI 应用中实现事件处理机制?