C# 委托、匿名方法和 Lambda 表达式 – 我的天哪!






4.94/5 (191投票s)
通过一个从 .NET 1.1 开始的引导性示例,解释了创建委托的不同方法。
引言
在 .NET 中,委托、匿名方法和 Lambda 表达式可能会让人非常困惑。我认为下面的代码示例证明了这一点。`First` 的六个调用中有哪一个可以编译?哪一个返回我们想要的答案,即 ID 为 5 的 Customer?顺便说一下,答案是 `First` 的所有六个调用不仅可以编译,而且都找到了正确的客户,并且在功能上是等效的。如果您对为什么会这样感到困惑,那么这篇文章就是为您准备的。
class Customer
{
public int ID { get; set; }
public static bool Test(Customer x)
{
return x.ID == 5;
}
}
...
List<Customer> custs = new List<Customer>();
custs.Add(new Customer() { ID = 1 });
custs.Add(new Customer() { ID = 5 });
custs.First(new Func<Customer, bool>(delegate(Customer x) { return x.ID == 5; }));
custs.First(new Func<Customer, bool>((Customer x) => x.ID == 5));
custs.First(delegate(Customer x) { return x.ID == 5; });
custs.First((Customer x) => x.ID == 5);
custs.First(x => x.ID == 5);
custs.First(Customer.Test);
设置——什么是委托?
好的,假设您有一个购物车类,用于处理客户的订单。管理层决定根据销量、价格等给予人们折扣。作为其中的一部分,他们实现了一个您必须用于计算订单的因素。好的,没问题,您只需声明一个变量来保存“魔术折扣”,然后继续编写您的算法。
class Program
{
static void Main(string[] args)
{
new ShoppingCart().Process();
}
}
class ShoppingCart
{
public void Process()
{
int magicDiscount = 5;
// ...
}
}
第二天,管理层又做出了决定,根据一天中的不同时间来更改折扣金额;我懂,太棒了。这很容易,所以您只需在代码中进行更改。
class ShoppingCart
{
public void Process()
{
int magicDiscount = 5;
if (DateTime.Now.Hour < 12)
{
magicDiscount = 10;
}
}
}
又过了一天,管理层再次改变了事情,并将更多的逻辑(或非逻辑)添加到折扣算法中。“够了,”您对自己说。我怎样才能将这个荒谬的算法从我的代码中移除,让别人来维护逻辑呢?您想要做的是将责任移交,或委托给别人。幸运的是,.NET 有一种机制可以做到这一点,您猜对了,就是委托。
委托(Delegates)
如果您有 C/C++ 背景,描述委托的最佳方式就是称它们为函数指针。对于其他人来说,可以认为它们是一种传递方法的方式,就像您传递值和对象一样。例如,下面的三行代码体现了相同的基本原理:您正在将一个数据块传递给 `Process` 方法,但没有使用它。
// passing an integer value for the Process method to use
Process( 5 );
// passing a reference to an ArrayList object for the Process method to use
Process( new ArrayList() );
// passing a method reference for the Process method to call
Process( discountDelegate );
好的,那么 `discountDelegate` 是什么?我该如何创建它?`Process` 方法是如何使用委托的?我们需要做的第一件事是声明一个委托类型,就像声明一个类一样。
delegate int DiscountDelegate();
这意味着我们现在有了一个名为 `DiscountDelegate` 的委托类型,我们可以像使用类、结构等一样使用它。它不接受任何参数,但返回一个整数。然而,就像类一样,它在创建实例之前并没有什么用处。创建委托实例的技巧是记住委托不过是对方法的引用。关键在于要认识到,即使 `DiscountDelegate` 没有构造函数,在创建它时,也有一个隐式构造函数,它需要一个与该签名(无参数,返回 int
)匹配的方法。您如何“给予”构造函数一个方法?嗯,.NET 允许您直接输入方法名称,就像调用方法一样;您所要做的就是省略括号。
DiscountDelegate discount = new DiscountDelegate(class.method);
在继续之前,让我们回到我们的示例并将各个部分组合起来。我们将添加一个 `Calculator` 类来帮助我们处理折扣算法,并提供一些方法供我们的委托指向。
delegate int DiscountDelegate();
class Program
{
static void Main(string[] args)
{
Calculator calc = new Calculator();
DiscountDelegate discount = null;
if (DateTime.Now.Hour < 12)
{
discount = new DiscountDelegate(calc.Morning);
}
else if (DateTime.Now.Hour < 20)
{
discount = new DiscountDelegate(calc.Afternoon);
}
else
{
discount = new DiscountDelegate(calc.Night);
}
new ShoppingCart().Process(discount);
}
}
class Calculator
{
public int Morning()
{
return 5;
}
public int Afternoon()
{
return 10;
}
public int Night()
{
return 15;
}
}
class ShoppingCart
{
public void Process(DiscountDelegate discount)
{
int magicDiscount = discount();
// ...
}
}
如您所见,我们在 `Calculator` 类中为每个逻辑分支创建了一个方法。我们在 `Main` 方法中创建了 `Calculator` 的实例和 `DiscountDelegate` 的实例,它们协同工作以设置我们要调用的目标方法。
太棒了,现在我们不必担心 `Process` 方法中的逻辑了,我们只需调用给定的委托。请记住,我们不关心委托是如何创建的(甚至何时创建),我们只需要在需要值时像调用其他任何方法一样调用它。如您所见,另一种思考委托的方式是它推迟了方法的执行。计算器方法是在过去某个时间点选择的,但直到我们调用 `discount()` 才实际执行。看看我们的解决方案,仍然有很多丑陋的代码。对于 `Calculator` 类中的每个返回值,我们需要一个不同的方法吗?当然不必;让我们来整合一下这些混乱。
delegate int DiscountDelegate();
class Program
{
static void Main(string[] args)
{
new ShoppingCart().Process(new DiscountDelegate(Calculator.Calculate));
}
}
class Calculator
{
public static int Calculate()
{
int discount = 0;
if (DateTime.Now.Hour < 12)
{
discount = 5;
}
else if (DateTime.Now.Hour < 20)
{
discount = 10;
}
else
{
discount = 15;
}
return discount;
}
}
class ShoppingCart
{
public void Process(DiscountDelegate discount)
{
int magicDiscount = discount();
// ...
}
}
搞定,好多了。您会注意到我们通过将 `Calculate` 方法设为静态,并且不费心在 `Main` 方法中保留 `DiscountDelegate` 的引用来清理代码。好的,现在您对委托所知的一切都了解了,对吧?嗯,如果这是 2004 年,.NET 1.1 的话,答案就是“是的”,但幸运的是,框架自那时以来已经成熟了。
灯光,摄像,开始 - 或者说 - 我们想要 Func!
.NET 2.0 引入了泛型,微软通过提供 `Action<T>` 类迈出了泛型委托的第一步。老实说,我认为它在很大程度上被忽视了,因为泛型本身就需要一段时间才能进入我们大多数人的思维。后来,在 3.5 中,他们很慷慨地为我们提供了一些常用的委托,这样我们就不会一直需要自己定义了。他们扩展了 `Action` 并添加了 `Func`,它们之间唯一的区别是 `Func` 匹配返回值的方法,而 `Action` 匹配不返回值的方法。
这意味着我们不需要声明我们的 `DiscountDelegate`,而是可以使用 `Func<int>` 来代替。为了演示参数的工作原理,假设管理层再次更改了我们的算法,现在有一个特殊折扣需要我们考虑。这很简单,我们只需在 `Calculate` 方法中请求一个布尔值。
我们的委托签名现在变成 `Func<bool, int>`。注意 `Calculate` 方法现在接受一个布尔参数,并且我们使用布尔值调用 `discount()`。
class Program
{
static void Main(string[] args)
{
new ShoppingCart().Process(new Func<bool, int>(Calculator.Calculate));
}
}
class Calculator
{
public static int Calculate(bool special)
{
int discount = 0;
if (DateTime.Now.Hour < 12)
{
discount = 5;
}
else if (DateTime.Now.Hour < 20)
{
discount = 10;
}
else if (special)
{
discount = 20;
}
else
{
discount = 15;
}
return discount;
}
}
class ShoppingCart
{
public void Process(Func<bool,int> discount)
{
int magicDiscount = discount(false);
int magicDiscount2 = discount(true);
}
}
不错,我们又省了一行代码,现在我们完成了吗?当然没有,我们可以通过使用类型推断来节省更多时间。只要我们传递的方法具有预期委托的正确签名,.NET 就允许我们完全省略 `Func<bool, int>` 的显式创建。
// works because Process expects a method that takes a bool and returns int
new ShoppingCart().Process(Calculator.Calculate);
到目前为止,我们已经通过首先省略自定义委托的需要,然后省略显式创建 `Func` 委托的需要来精简了我们的代码。还有什么可以做的来减少行数吗?嗯,既然我们才刚写到文章的一半,答案显然是“是的”。
匿名方法
匿名方法允许您声明一个方法体而无需为其命名。在后台,它们作为“普通”方法存在;但是,在代码中没有办法显式调用它们。匿名方法只能在使用委托时创建,事实上,它们是使用 `delegate` 关键字创建的。
class Program
{
static void Main(string[] args)
{
new ShoppingCart().Process(
new Func<bool, int>(delegate(bool x) { return x ? 10 : 5; }
));
}
}
如您所见,我们完全消除了对 `Calculator` 类的需求。您可以在花括号之间放置任意多或任意少的逻辑,就像在任何其他方法中一样。如果您难以理解这是如何工作的,请假定声明 `delegate(bool x)` 实际上是一个方法签名而不是关键字。想象一下那段代码在类中。`delegate(bool x) { return 5; }` 是一个合法的方法声明(是的,我们必须添加返回类型);碰巧 `delegate` 是一个保留字,在这种情况下,它使方法成为匿名的。
好吧,我相信您现在知道还有更多的精简方法。当然,我们可以省略显式声明 `Func` 委托的需要;当我们使用 `delegate` 关键字时,.NET 会为我们处理。
class Program
{
static void Main(string[] args)
{
new ShoppingCart().Process(
delegate(bool x) { return x ? 10 : 5; }
);
}
}
当使用期望委托作为参数的 .NET 方法和响应事件时,可以看到匿名方法的真正强大之处。以前,您必须为每种可能的操作创建一个方法。现在,您可以直接在行内创建它们,避免弄乱您的命名空间。
// creates an anonymous comparer
custs.Sort(delegate(Customer c1, Customer c2)
{
return Comparer<int>.Default.Compare(c1.ID, c2.ID);
});
// creates an anonymous event handler
button1.Click += delegate(object o, EventArgs e)
{ MessageBox.Show("Click!"); };
Lambda 表达式
来自 MSDN:“Lambda 表达式是一个匿名函数,可以包含表达式和语句,并且可以用于创建委托或表达式树类型。”好的,您应该理解“用于创建委托”部分,但关于这个“表达式”呢?嗯,老实说,表达式和表达式树超出了本文的范围。现在我们只需要了解它们的一点是,表达式是代码(是的,您的 C# 代码),在运行的 .NET 应用程序中表示为数据和/或对象。引用强大的 Jon Skeet 的话:“表达式树是一种表达逻辑的方式,以便其他代码可以对其进行内省。当 lambda 表达式转换为表达式树时,编译器不会发出 lambda 表达式的 IL;它会发出将构建表示相同逻辑的表达式树的 IL。”
我们需要关注的是,lambda 表达式取代了匿名方法并增加了许多功能。回顾我们最后一个示例,我们已经将代码精简到基本上在一行中创建了整个折扣算法。
class Program
{
static void Main(string[] args)
{
new ShoppingCart().Process(
delegate(bool x) { return x ? 10 : 5; }
);
}
}
您相信我们可以做得更短吗?Lambda 表达式使用“转到”运算符 =>
来指示要将哪些参数传递给表达式。编译器更进一步,允许我们省略类型并自行推断它们。如果您有两个或多个参数,则需要使用括号:`(x,y) =>`。但是,如果只有一个,则不需要括号:`x =>`。
static void Main(string[] args)
{
Func<bool, int> del = x => x ? 10 : 5;
new ShoppingCart().Process(del);
}
// even shorter...
static void Main(string[] args)
{
new ShoppingCart().Process(x => x ? 10 : 5);
}
是的,就是这样。`x` 的布尔类型是被推断出来的,返回值类型也是被推断出来的,因为 `Process` 接受一个 `Func<bool, int>`。如果我们想实现像之前那样的完整代码块,我们只需要添加花括号。
static void Main(string[] args)
{
new ShoppingCart().Process(
x => {
int discount = 0;
if (DateTime.Now.Hour < 12)
{
discount = 5;
}
else if (DateTime.Now.Hour < 20)
{
discount = 10;
}
else if(x)
{
discount = 20;
}
else
{
discount = 15;
}
return discount;
});
}
还有最后一件事……
使用花括号和不使用花括号之间有一个重要的区别。当您使用它们时,您正在创建“语句 lambda”,否则就是“表达式 lambda”。语句 lambda 可以执行多个语句(因此需要花括号)并且不能创建表达式树。您很可能只在处理 `IQueryable` 接口时遇到此问题。下面的示例显示了问题。
List<string> list = new List<string>();
IQueryable<string> query = list.AsQueryable();
list.Add("one");
list.Add("two");
list.Add("three");
string foo = list.First(x => x.EndsWith("o"));
string bar = query.First(x => x.EndsWith("o"));
// foo and bar are now both 'two' as expected
foo = list.First(x => { return x.EndsWith("e"); }); //no error
bar = query.First(x => { return x.EndsWith("e"); }); //error
bar = query.First((Func<string,bool>)(x => { return x.EndsWith("e"); })); //no error
第二个 `bar` 的赋值在编译时失败。这是因为 `IQueryable.First` 期望一个表达式作为参数,而扩展方法 `List<T>.First` 期望一个委托。您可以通过强制 lambda 评估为委托(并使用 `First` 的方法重载)来做到这一点,就像我在第三次赋值给 `bar` 时所做的那样。
很难在这里结束讨论,但我认为我必须这样做。Lambda 基本上分为两类:创建匿名方法和委托的那种,以及创建表达式的那种。表达式是另一篇文章的内容,并且不一定是 .NET 开发人员的必需知识(尽管它们在 LINQ 中的实现肯定是的)。
结论
我希望本文能够实现目标,那就是澄清文章开头那六个令人困惑的委托调用,并解释委托、匿名方法和 Lambda 表达式之间的相互作用。
历史
- 2009/12/17:初始版本。
- 2009/12/19:修复了拼写错误和泛型委托添加时间线。