C# 中委托的力量
文档描述了 C# 中的委托以及委托在不同场景下的用法。
引言
大家好,这次我想写点关于 C# 特性的内容。我们大多数人(或者说,大多数中级开发者)并没有充分利用 C# 的所有特性,有很多特性我们甚至从未听说过,也从未想象过。有时,我们对 C# 功能的无知会让我们写出 100 行代码,而实际上只需要一个 LOC 就能更轻松、更有效地满足我们的需求。所以在这篇文章中,我想解释 C# 中一个强大的特性——委托。
委托(Delegates)
如果有人问你,什么是委托?用一句话来说,就是“委托是一个函数指针”。或者我们可以说,委托是一种类型或一个对象,它保存着函数的引用。
要让一个委托指向一个函数,我们必须满足以下规则:要被委托指向的函数的签名必须与指向它的委托的签名相同。
让我们实际看看委托如何指向一个函数。在这个例子中,我将创建一个 ASP.NET 应用程序来演示如何使用委托。我们都厌倦了在几乎所有的教程和文章中看到控制台应用程序的黑屏,所以出于目的,我选择了 ASP.NET。
我将创建一个计算圆面积的应用程序。
看起来不错,对吧?
然后,我们将在应用程序中编写一个计算面积的函数。
在上面的截图 you can see the method to calculate the area. You might be thinking, "what’s new in this?" Yes, nothing is new in that :). If we execute the application we will be getting an output area corresponding to the input. Here comes the hero.
定义和使用委托有三个步骤。
- 声明
- 实例化
- 调用
在上面的快照中,您可以看到我声明了一个名为 DelCalcArea
的委托,然后在按钮单击事件中创建了一个委托对象(实例化)并将其分配给 Calculatearea
方法。我希望到目前为止一切都清楚了。我们不是直接从按钮单击事件中调用 CalculateArea
方法来计算面积,而是调用附加了我们方法的委托。您可以看到在下一行中,我们通过 Invoke
方法(调用)调用了 DelCalcArea
委托对象,您还可以通过查看 Invoke
的工具提示看到更多内容,它接受像我们的 Calculate
方法一样的 int
变量,并返回一个 double 值。然后,就像任何其他普通方法调用一样,只需将委托调用赋值给我们要显示结果的标签。
怎么样?很简单,对吧?有一点需要注意,我们不是在调用委托,而是在调用委托。
多播委托
如果你尝试阅读过关于委托的内容,你可能听说过“多播委托”这个术语,对吧?那么什么是多播呢?一句话就能回答这个问题:“在一行代码中调用多个方法”,或者“多播委托是委托的链表。”
我们如何在一行代码中调用多个方法或一个以上的方法?要进行多播,我们必须将具有相同签名的多个方法分配给一个委托。我们来看一个例子;我有一个需求,需要在单个 LOC 中执行 2 个方法,即一个计算圆面积的方法,另一个计算正方形面积的方法。还需要一个输入值来驱动这两个方法。所以我们的代码看起来会像这样:
查看上面的代码,您可以看到委托的签名与方法的签名匹配,并且使用 **+=** 符号将函数附加到委托。让我们看看输出。
是的,它成功执行了 :) 就像我们使用 **+=** 添加函数一样,我们也可以使用 **-=** 删除已添加的方法。
使用相同的示例,让我们看看如何从委托中删除已添加的方法。从上面的示例中,我们有两个方法已添加到委托中,现在我将从委托 delCalc
中删除 CalculateSquareArea
方法,所以上面的代码看起来会是这样:
看看我在上面的快照中是如何删除方法的?那么我们预期的输出是什么?CalculateSquareArea
方法不会被执行,并且结果不会显示在输出中,对吧?
正如预期的那样,该函数未执行,结果也未显示。
用户控件事件处理程序
在前一节中,我试图让你对委托是什么有一个总体的了解。在这一节中,我将说明委托与事件的关系。
如果存在一个事件,那么必须有一个事件处理程序来处理该事件。当我们使用 ASP.NET 应用程序中的用户控件将事件参数拉到父页面时,通常需要委托。让我们看一个场景并处理它。
我想创建一个圆的面积计算应用程序,并且我还决定将面积计算器作为用户控件。面积计算器包含一个用于输入圆半径的文本框和一个用于计算面积的按钮,计算出的面积应显示在父页面的标签中,即用户控件本身没有显示结果的 प्रावधान。那么问题是,我们能否从用户控件中获取计算出的面积到我们的父页面?
答案是肯定的。我们可以借助 委托
和 事件
来实现这一点。
我们开始创建应用程序吧?
首先在您的解决方案中添加一个 ASP.NET 项目;我添加了一个名为 CsharpFeatures
的项目。
在该项目中,我添加了一个名为 ChildUserControll.ascx 的用户控件。
不要在意您在解决方案中看到的另一个名为 Calculator 的项目;我们现在不会使用它。
让我们像之前提到的那样设计用户控件的 UI。UI 会是这样的:
让我们来处理用户控件的内部,以输出结果。为了进行计算,我们需要一个事件,对吧?那么在这里,我们可以在计算按钮单击事件的事件处理程序中进行计算。好的,那么。我们在使用用户控件的父页面中会收到按钮单击事件吗?不……我们不会收到来自用户控件的事件或输出。
为了暴露用户控件的事件,我们必须在用户控件中添加一个额外的事件。如果我们添加一个事件,我们就需要一个事件处理程序,对吧?如果我们实现一个事件处理程序,我们就需要事件参数,对吧?那么我们需要实现哪些东西呢?
我们需要实现:事件、事件处理程序和事件参数。好的,让我们看看我们将如何实现所有这些以及如何使用委托。
如前所述,我已经创建了一个 .ascx 控件。在 ascx 控件内部,我将添加一个事件。
在上面的快照中,我在 page-load 事件上方的类中添加了一个名为 MyEvent
的事件;这个事件将被公开给消耗此用户控件的公共页面或 aspx 页面。你注意到 MyEvent
的类型了吗?它是 MyEventHandler
。你认为这是一个系统定义的类型吗?不,不是。MyEventHandler
是一个委托。我们正在做的是将名为 MyEventHandler
的事件处理程序附加到名为 MyEvent
的事件上。那么 MyEvent
的输入参数是什么?它将是 MyEventHandler
类型,对吧?:) 我希望你知道如何声明委托。无论哪种情况,我都会给你看快照,这样你就能更好地理解它。
看到这个委托了吗?它的签名看起来是否和你熟悉的东西相似?是的。就在 Page_Load
事件参数下方,你可以看到一个不同之处,那就是 EventArgs
,而在我们的委托输入中是 MyEventArgs
。EventArgs 是系统定义的事件参数,MyEventArgs 是我们定义的。那么 MyEventArgs 中有什么呢?MyEvent
事件的输入类型是什么?
MyEventArgs 将包含在执行功能后要从用户控件返回的数据。
所以在这个例子中,要返回的数据是什么?是 Area
,对吧?
接下来我将展示 MyEventArgs
类。这个类必须继承自 System.EventArgs
类。该类包含一个属性:area。
area 的类型是 double。为了理解和简化,我以反向顺序开始构建。那么正确的顺序是什么?首先创建 MyEventArgs
,然后是 MyEventHandler
,最后是 MyEvent
,它使用以上所有三个东西。我希望你明白了。这里我们已经创建了所需的所有基础设施,但实际上什么也没做。但是如果我们不开始计算,不给任何东西赋值,不这样做的话,我们怎么能把结果拿出来呢?让我们去做吧。
正如我之前所说,我们是通过 MyEventArgs
来获取结果的。我们必须将计算出的面积设置到用户控件的 MyEventArgs
的 Area
属性中。所以去这样做吧。
所以在上面的快照中,您可以看到我已将计算出的 Area 赋值给了 MyEventArgs
类。接下来要做的是将这个结果从控件中取出。但是如何取出呢?要将结果从控件中取出,唯一的方法就是通过事件,所以用我们创建的参数调用事件。例如:
这就是我们在按钮单击事件中通过提供 EventArgs
来调用事件的方式。我使用了这个控件,放了一个调试器,并执行了它,只是为了向您展示从控件中传递出来的内容。在这里,您可以看到 objEventArgs
包含什么。看起来很不错,对吧?
现在我将告诉您如何使用 .ascx
控件并在 Default.aspx 页面中显示的 Label 中获取值。我希望您知道如何在 .aspx
页面中放置 .ascx
控件。只需将控件拖放到您想要的位置即可。然后执行控件时,我们的默认页面将如下所示:
让我们转到 Default.aspx 页面 Page_Load
事件并进行必要的更改。一旦我们将 UserControll 添加到父页面,实际上发生的事情是在该父页面中创建了用户控件的对象。在下面的快照中,您可以看到这一点。通过在页面中添加 ChildUserControll
,我得到了 ChildUserControll 1
对象。
您还可以在对象的 intellisense 中看到我们在用户控件中声明的事件。所以让我们去为页面中的事件添加事件处理程序。
在工具提示中,您将看到 **按 TAB 键插入**。按两次 Tab 键,VS 就会为我们创建一个事件处理程序存根。您还可以看到将要创建的事件处理程序的类型是 MyEventHandler
类型,这是我们为 MyEvent
处理创建的委托。一切看起来都很好:)
在按 Tab 键后,事件处理程序存根已生成,我开始处理以将面积显示在父页面的标签中。请参考上面的快照。在 intellisense 中,Area
属性的类型是 double。真的很棒,不是吗?
完整的代码如下所示。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Diagnostics;
namespace CsharpFeatures
{
public partial class _Default : System.Web.UI.Page
{
protected
void Page_Load(object
sender, EventArgs e)
{
ChildUserControll1.MyEvent+=new MyEventHandler(ChildUserControll1_MyEvent);
}
void
ChildUserControll1_MyEvent(object sender, MyEventArgs e)
{
lblResult.Text = e.Area.ToString();
}
}}
查看输出,检查结果是否显示在父页面的标签中。
哇!这是一个多么惊人的机制,不是吗?委托简直太棒了。
好的,在这里我将写一个简单直接的场景,我们可以使用委托。你有没有想过通过委托实现应用程序的松耦合?是的,我们也可以在一定程度上使用委托来开发松耦合的应用程序。我将展示我们如何做到这一点。:)
你有没有注意到我们解决方案资源管理器中的一个名为 Calculator
的项目?我将利用这个项目,我们将创建一个简单的计算器,它执行三种算术运算,如加、减、乘,使用两个整数输入值。为此,我在我们的解决方案中创建了另一个项目(名为 calculator)作为一层,并将计算逻辑写在了 Calculator Layer
中。
让我们看看我如何编写 Calculator Layer。在该层中,我创建了一个名为 MyCalculator
的类。我将在这里编写计算逻辑。那么在简单的计算器中,解耦或松耦合的作用是什么?在这里,这些术语有很大的空间。让我们把一切放在一边,想想如何创建一个具有两层的应用程序,即表示层和业务层。
我们将在 Calculator 层编写三个公共方法,然后根据我们的操作从消费客户端调用这些方法。这需要 if
条件或 switch
语句的帮助。那么如果我们想为我们的应用程序添加更多功能,我们会怎么做?我们必须去 Calculator 层编写一个方法(例如 Divide
),然后再次在消费应用程序中进行更改以适应 Calculator
层所做的更改。
UI 层和 Calculator 层(BLL)是紧耦合的。此外,消费端最终会知道 Calculator 层的所有方法或操作,因为我们使用的是 Public
方法。那么我们如何避免这种情况呢?有几种不同的方法可以处理这种情况。我将解释如何使用委托来实现这一点。
让我们看看我们正在使用的应用程序的 UI。看起来像下面的快照。
两个文本框,可以用来输入数值并执行算术运算,结果显示在下拉列表中。看起来很酷。
让我们进入 Calculator Layer
看看我做了哪些处理。如前所述,我们将借助委托来解决这个问题,所以我们肯定需要一个委托,并且还要确保访问修饰符必须是私有的。这样 Calculator Layer
才能隐藏其中的方法,对吧?所以让我们去看看 Calculator Layer
中有什么。
所以您可以看到一个名为 DelegateMyCalculator
的委托,它接受两个整数类型的输入参数并返回整数类型的数据。您还可以看到类中定义了三个私有方法。下一步,让我们将委托附加到方法。这是棘手的部分,和以前一样,我们不是直接将方法分配给委托,而是创建一个返回类型为委托(DelegateMyCalculator
)的方法,该方法还接受一个 string
类型的输入参数。
所以,查看上面的快照,您可以看到标记的代码行。在第一个方法中,委托方法名为 Calculate
,它接受一个字符串类型的输入参数,然后在该方法内部,我们创建了委托(DelegateMyCalculator
)的一个对象,并将其赋值为 null。您还可以清楚地看到,委托的赋值只执行一次,并且根据 Calculate
方法的输入,只赋给一个方法。让我们去 UI 层消费这个方法。
这里有一个问题,可能会让你大开眼界:我们如何调用一个委托方法?我们如何从 UI 向委托方法提供输入值?该方法只接受一个输入参数,对吧?去看看 UI 层的处理。
我所做的是创建了一个 MyCalculator
类的对象。让我们看看对象在 intellisense 中显示的所有方法。
您可以看到一个名为 Calculate
的方法,它的类型是 DelegateMyCalculator
,并且在工具提示中也可见。没有显示其他方法,因此 UI 不会知道 Calculator 层(BLL)中的所有操作。这是其中一个意图。我们将把操作作为从下拉列表中选择的值传递给 Calculate
方法。
接下来,我们为方法传递输入参数。让我们来看看 Calculate
方法调用的 intellisense 中有什么。
在这里您可以看到这个魔法。有一个名为 Invoke
的方法,它接受两个输入参数,两者都是整数。如果我们通过将文本框的值作为参数传递给 Invoke
来使用这个方法,会发生什么?LOC 会是什么样子?
我做的另一件事是将返回值赋给 UI 层中的一个标签。去执行应用程序,看看会发生什么。
看到了吗?它运行正常 10 + 10= 20
;结果显示在已赋值的标签中。我们可以说该应用程序是松耦合的,因为如果我们想添加一个新功能,我们可以去 Calculator 层添加一个方法(例如 divide
),然后在 UI 中做一个微小的更改,即在 下拉 列表中添加一个操作(Divide)。您可以添加方法到当前层,而在前一层或后一层只需做很小的改动。
结论
委托确实非常强大。
谢谢