65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (191投票s)

2009年12月17日

CPOL

9分钟阅读

viewsIcon

543583

通过一个从 .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:修复了拼写错误和泛型委托添加时间线。
© . All rights reserved.