You Had Me at Delegate (我从委托开始)






4.67/5 (16投票s)
事件之外的委托的力量。
引言
我一直找不到一个关于委托的好的教程……不知何故,讨论总是在进行到一半的时候就转向了对事件的讨论。委托本身可以是一个强大的工具,可以用来增加类的抽象性,也可以用于动态方法调用。在我们开始编写代码之前,让我们先尝试创建一个关于委托是什么的可用定义。
委托 (Delegate):(在思考委托时,我能想到以下几点。)
- 一种类型 (例如:
StringBuilder
、DBContext
、List
) - 一种类型安全的方法指针 (委托实例存储方法的引用)
- 看起来类似于方法声明
- 然而,委托的签名包括返回类型,而方法签名只包含方法名称以及参数的类型和数量。
- 允许方法在很大程度上像任何其他 C# 对象一样被对待:允许它们被赋值给其他变量并作为参数传递。
所以我们有:委托是一种特殊类型 (类),它定义了一个方法签名,该签名的实例可以用来引用另一个遵循其定义的方法。(这个定义将通过示例变得更加清晰)。要定义一个委托,实际上非常简单。我们只需要在典型的方法签名中加上“delegate
”关键字和返回类型。
public delegate void ProcessInventoryHandler( String text );
上面的代码定义了一个可以引用具有一个 String 类型参数且返回 void 的方法的委托。以下方法都可以使用上述委托进行引用。
void AddToInventory( String text )
void FooBar( String foo )
正如你所见,方法和参数名称并不重要——重要的是签名。换个角度思考,想象一下你的老板给了你公司的法人公章,并要求你在与新客户签订合同时代表她行事。你老板是否是一个 6 英尺 5 英寸高、体重是你两倍多、声音比你更低沉的女人并不重要,这里重要的是你们两个具有相同的签名 (法人公章)。然而,委托的代码定义有点欺骗性,因为它看起来更像 C/C++ 函数声明而不是类定义。CLR 在后台做了繁重的工作,将每个委托转换为一个合适的类定义。我们可以通过查看上面委托生成的 MSIL (Microsoft Intermediate Language) 代码来验证这一点。
.class public auto ansi sealed MyDotNet.ProcessInventoryHandler
extends [mscorlib]System.MulticastDelegate
{
// Methods
.method public hidebysig specialname rtspecialname
instance void .ctor (
object 'object',
native int 'method'
) runtime managed
{
} // end of method ProcessInventoryHandler::.ctor
.method public hidebysig newslot virtual
instance void Invoke (
string text
) runtime managed
{
} // end of method ProcessInventoryHandler::Invoke
.method public hidebysig newslot virtual
instance class [mscorlib]System.IAsyncResult BeginInvoke (
string text,
class [mscorlib]System.AsyncCallback callback,
object 'object'
) runtime managed
{
} // end of method ProcessInventoryHandler::BeginInvoke
.method public hidebysig newslot virtual
instance void EndInvoke (
class [mscorlib]System.IAsyncResult result
) runtime managed
{
} // end of method ProcessInventoryHandler::EndInvoke
} // end of class MyDotNet.ProcessInventoryHandler
尽管 MSIL 的细节超出了本教程的范围,但我们可以在这里看到,ProcessInventoryHandler
委托是一个继承自一个名为“MulticastDelegate
”的类的 `public sealed` 类。但是,与典型的类不同,委托类的实例被称为“Delegate
”而不是“Object
”。要创建委托实例,我们必须首先将我们想要链接到委托的方法的引用传递进去。这比听起来容易得多。正如我们所知,在 C# 中,调用方法的最常见形式是:MethodName (int x);
,其中“int x
”代表从零到 2^16 - 1 的任意数量的参数 [里程可能有所不同,MSIL 限制了可加载到 unsigned int16 的参数数量]。在这里,运行时知道我们正在调用一个方法,因为有开括号和闭括号。然而,如果我们只写方法的名称,而不写括号或参数,我们可以获得传递给委托所需的函数引用。在 C# 中,这个函数引用被称为方法组,因为单个方法名称可以,但不一定必须,引用多个不同的方法名称到方法签名组合。CLR 将匹配委托类型到相应的方法重载。然而,CLR 的这种匹配让我们想到几个重要的附注。
- 方法组在委托的世界之外没有意义。(截至 C# 6)
- CLR 在委托到方法的赋值方面不喜欢歧义。
为了更好地理解我所说的关于委托赋值中的歧义,让我们来看以下几个代码示例。
static void Main()
{
var dollarValue = ShowMeTheMoney(new Dollar());
Console.WriteLine ("The dollar amount: {0}{1}", dollarValue.Symbol, dollarValue.Amount);
var wonValue = ShowMeTheMoney(new Won());
Console.WriteLine ("The won amount: {0}{1}", wonValue.Symbol, wonValue.Amount);
}
static Dollar ShowMeTheMoney(Dollar d)
{
d.Amount = 5m;
return d;
}
static Won ShowMeTheMoney(Won w)
{
w.Amount = 5000m;
return w;
}
//The currency class, has just one property for the amount
//for the purpose of this example... It will have two derived
//classes (currencies) Dollar and Won
public class Currency { public decimal Amount {get; set;} }
public class Dollar : Currency
{
private readonly string _symbol = "$";
public string Symbol { get { return _symbol; } }
}
public class Won : Currency
{
private readonly string _symbol = "W";
public string Symbol { get { return _symbol; } }
}
上面的代码可以正常编译和运行。输出当然是
The dollar amount: $5
The won amount: W5000
现在让我们看看当我们通过使用委托,为我们上面简单的程序增加一个额外的抽象层时会发生什么。为此,我们只需要添加一个委托声明并修改 Main 方法如下 (为了简洁,类定义被省略了)。
delegate Currency MoneyHandler(Currency c);
static void Main()
{
MoneyHandler theBank = ShowMeTheMoney;
var dollarValue = theBank(new Dollar());
Console.WriteLine ("The dollar amount: {0}{1}", dollarValue.Symbol, dollarValue.Amount);
var wonValue = theBank(new Won());
Console.WriteLine ("The won amount: {0}{1}", wonValue.Symbol, wonValue.Amount);
}
static Dollar ShowMeTheMoney(Dollar d)
{
d.Amount = 5m;
return d;
}
static Won ShowMeTheMoney(Won w)
{
w.Amount = 5000m;
return w;
}
如果我们尝试编译上面的代码,编译器将发出 BANG 错误 [不要通过,不要收集 $200],并给出友好的“没有适用于 'Method Group' 的重载匹配委托 'Delegate Name' ”错误消息。我们可能会试图抗议,并对自己说,但是等等,Dollar 和 Won 都派生自 Currency,这应该是可以的。然而,问题就出在这里,**“both”应该工作。这只是 CLR 在匹配方法组到委托类型时,对于方法参数不允许协变 (拥有更派生的类型) 的大约半打原因之一。然而,在歧义和多重性这两个概念之间必须做出重要的区分。换句话说,当方法签名被正确定义且清晰时,一对多的关系是有效的。
delegate Currency MoneyHandler(Currency c);
static void Main()
{
MoneyHandler theBank;
//We are allowed to chain methods to a single delegate instance
//by using the += operator [x = x + y]
theBank = ShowMeTheMoneyDollar;
theBank += ShowMeTheMoneyWon;
//We make just one call to the delegate instance, but both methods
//will be called. Note, only the return value of the last method
//called will be set to 'myCurrency'.
var myCurrency = theBank(new Currency());
}
static Dollar ShowMeTheMoneyDollar(Currency c)
{
c = new Dollar();
c.Amount = 5m;
Console.WriteLine ("The dollar amount: ${0}", c.Amount);
return c as Dollar;
}
static Won ShowMeTheMoneyWon(Currency c)
{
c = new Won();
c.Amount = 5000m;
Console.WriteLine ("The won amount: W{0}", c.Amount);
return c as Won;
}
上面代码的输出与我们在上面更简单的非委托情况下的输出完全相同,只是
The dollar amount: $5
The won amount: W5000
眼尖的人可能还会注意到上面的代码还有其他东西,两个方法的返回类型都是委托返回类型的派生类型。换句话说,Dollar 和 Won 都继承自 Currency。但是等等,我刚才不是在前面几段提到了为什么那不好吗?再次,在你抱怨之前,让我们看看真正发生了什么,以及在返回类型协变和参数协变之间的一些微妙差异。
- 可能最容易发现的区别是,现在我们有两个不同的方法组,所以没有歧义。
- 返回类型默认是“out”参数,而参数默认是“in”参数。out 类型在设计上是不可变的——即使我们返回一个更基础的类型,数据完整性也得到了维护。实际上并没有精度损失,因为对象永远不会改变,只有引用类型。“任何玫瑰,名字变了,香气依旧…”好吧,更准确地说,我们只是称玫瑰为花。
好吧,不要只听我的话,让我们在 Main 方法中添加几行代码。
delegate Currency MoneyHandler(Currency c);
static void Main()
{
MoneyHandler theBank;
//We are allowed to chain methods to a single delegate instance
//by using the += operator [x = x + y]
theBank = ShowMeTheMoneyDollar;
theBank += ShowMeTheMoneyWon;
//We make just one call to the delegate instance, but both methods
//will be called. Note, only the return value of the last method
//called will be set to 'myCurrency'.
var myCurrency = theBank(new Currency());
Console.WriteLine (((Won)myCurrency).Symbol); // W
Console.WriteLine (myCurrency.GetType()); // Won
Console.WriteLine (((Dollar)myCurrency).Symbol); //Error: Unable to cast object of
//type 'Won' to type 'Dollar'.
}
[*注意:如果我们尝试对值类型(如 double
和 float
)这样做,编译器将发出 BANG 错误:没有引用可以传递]。
Object
类包含一个“GetType
”方法,该方法返回对象的类型。由于所有对象实例都派生自 Object
,我们可以调用 'myCurrency
' 上的 GetType
方法,并看到它实际上是派生的 'Won
'。例如,如果我们尝试将 'myCurrency
' 转换为 'Dollar
',则会引发错误。现在让我们来讨论委托的最后一个主题,也就是说,除了事件之外,**我们为什么应该使用它们**?回答这个问题的最好方法可能是使用以下虽然过于简化的类比:类对应接口,方法对应……委托。事实上,许多委托-方法用例可以用等效的接口-类实现来替换。但是这样做会产生冗长的代码,降低可读性,并让你暴露在不必要的“副作用”的潜在风险中。