您需要了解的关于 Lambda 表达式的一切






4.84/5 (62投票s)
本文将带您了解彻底理解 lambda 表达式所需的所有步骤。
引言
Lambda 表达式只是编写匿名方法的另一种方式。要理解 lambda 表达式,首先应该理解委托和匿名方法的概念。如果您精通这些概念,那么您可以直接跳到 lambda 表达式部分。
我们将按以下顺序阐述概念
- 委托(Delegates)
- 匿名方法
- Lambda 表达式
Func<>
委托- 委托实战
- Lambda 表达式实战
- Lambda 表达式 - 真实示例
- 闭包
委托(Delegates)
委托是对方法的类型安全引用。换句话说,您可以在 delegate
对象中保存方法的地址。
例如,如果您有一个方法 Addition()
。
public static int Addition(int no1, int no2)
{
return no1 + no2;
}
此方法的 Delegate
声明将如下所示
public delegate int MyDelegate(int no1,int no2);
请注意,delegate
声明必须与方法的签名匹配。(因为 delegate
是对方法的类型安全引用)。
现在您可以创建上述委托类型的对象。在创建 delegate
对象时,我们需要在构造函数中指定要引用的方法的名称。
MyDelegate delObj = new MyDelegate(Addition);
(请注意,这类似于类和对象。我们首先编写类,然后可以创建该类的多个对象。同样,我们首先声明 delegate
类型,然后从中创建 delegate
对象。在委托的情况下,这有点令人困惑,因为 delegate
声明和 delegate
对象都称为 delegate
。)
一旦您在 delObj
中有了“Addition
”方法的引用,您可以像这样调用
int r = delObj.Invoke(2,5);
或
int r = delObj(2,5);
在第二种情况下,编译器会将其转换为 delObj.Invoke(2,5);
完整代码
class Program
{
public delegate int MyDelegate(int no1,int no2);
static void Main(string[] args)
{
MyDelegate delObj = new MyDelegate(Addition);
int r = delObj(2,5);
Console.WriteLine("Result = " + r);
Console.ReadKey();
}
public static int Addition(int no1, int no2)
{
return no1 + no2;
}
}
匿名方法
匿名方法是没有名称的方法。我们可以编写没有名称的小方法块并将其地址分配给 delegate
对象,然后使用此 delegate
对象来调用它。
上面的示例可以使用匿名方法完成,如下所示
class Program
{
public delegate int MyDelegate(int no1,int no2);
static void Main(string[] args)
{
MyDelegate delObj = delegate(int no1, int no2)
{
return no1 + no2;
};
int r = delObj(2,5);
Console.WriteLine("Result = " + r);
Console.ReadKey();
}
}
在这里,我们使用 delegate
关键字来创建匿名方法。此方法与第一个示例中的 Addition()
方法相同,但它没有任何名称。在创建此方法时,我们将其地址分配给 delegate
对象 delObj
。因此,我们可以使用 delObj delegate
对象调用此匿名方法(调用与上一个示例相同)。
Lambda 表达式
Lambda 表达式只是编写匿名方法的另一种方式。上面的代码可以使用 lambda 表达式重写为
class Program
{
public delegate int MyDelegate(int no1,int no2);
static void Main(string[] args)
{
MyDelegate delObj = (no1, no2) => no1 + no2;
int r = delObj(2,5);
Console.WriteLine("Result = " + r);
Console.ReadKey();
}
}
注意事项
- Lambda 表达式包含参数列表和方法体,与匿名方法一样。
=>
运算符之前的是括号中的参数列表,=>
运算符之后的是方法体。 - 无需使用
return
关键字,编译器会为您添加。 - 无需
delegate
关键字 - 参数不需要数据类型。参数将从
delegate
声明 (MyDelegate
) 获取类型信息。
如果 lambda 表达式只有一个参数,则甚至不需要参数周围的括号。
例如,以下是一个计算给定数字平方的 lambda 表达式。
class Program
{
public delegate int MyDelegate(int no);
static void Main(string[] args)
{
MyDelegate delObj = no => no * no;
int r = delObj(4);
Console.WriteLine("Result = " + r);
Console.ReadKey();
}
}
Func<> 委托
Func<>
是框架中预定义的泛型 delegate
,我们可以用它来保存方法的引用。它有多个重载,我们可以根据我们要引用的方法的签名使用其中一个。
在此示例中,我们将使用 Func<T, TResult>
重载。它可以封装带有一个参数和返回值的方法。
T
:参数类型。
TResult
:返回类型。
同样的例子可以使用 Func<T, TResult>
编写为
class Program
{
static void Main(string[] args)
{
Func<int, int> delObj = no => no * no;
int r = delObj(4);
Console.WriteLine("Result = " + r);
Console.ReadKey();
}
}
在上面的代码中,我们不需要显式声明 delegate
(像 MyDelegate
)。相反,我们使用 Func<T, TReturn>
delegate
。此 delegate
简化了代码并消除了声明 delegate
的需要。
委托实战
当我们想将方法作为参数传递给另一个方法时,使用 Delegate
。例如,将方法 fun1()
作为参数传递给方法 fun2()
。
为了更好地理解它,让我们回顾一下我们的第一个例子。
我们有一个 Addition
方法
public static int Addition(int no1, int no2)
{
return no1 + no2;
}
委托声明
public delegate int MyDelegate(int no1, int no2);
Main 方法:这次,我们不直接从 Main
调用 Addition()
方法。仔细检查代码并阅读“注意事项”。
static void Main(string[] args)
{
MyDelegate delObj = new MyDelegate(Addition);
int r = Calculate(delObj);
Console.WriteLine("Result = " + r);
Console.ReadKey();
}
public static int Calculate(MyDelegate delObj)
{
int r = delObj(2, 5);
return r;
}
注意事项
- 首先,我们将
Addition
方法的引用/地址保存在delObj
中 - 从
Main
,我们调用Calculate()
方法。但在调用它时,我们将Addition()
方法的地址(即delObj
)作为参数传递给Calculate()
方法。(Addition()
: 回调方法) - 现在在
Calculate
方法中,我们调用delObj
委托。由于delObj
持有Addition()
方法的地址,它将调用相同的方法。 - 您可以将
Main
中的代码替换为以下代码。(无需创建delegate
对象。您可以直接在Calculate()
方法中指定方法名称。)
static void Main(string[] args)
{
int r = Calculate(Addition);
Console.WriteLine("Result = " + r);
Console.ReadKey();
}
您心中一定有一个大问题,如果我们要从 Calculate()
方法调用 Addition
方法,为什么不这样写呢?
class Program
{
public delegate int MyDelegate(int no1, int no2);
static void Main(string[] args)
{
int r = Calculate();
Console.WriteLine("Result = " + r);
Console.ReadKey();
}
public static int Calculate()
{
int r = Addition(2,5);
return r;
}
public static int Addition(int no1, int no2)
{
return no1 + no2;
}
}
但这样,您将在 Calculate()
方法中硬编码对 Addition()
的调用。如果我们想要进行减法并且这次我们想要调用 Subtraction()
方法而不是 Addition()
怎么办。为了实现这一点,我们必须使用 delegate
方法。
以下示例将使其非常清楚
class Program
{
public delegate int MyDelegate(int no1, int no2);
static void Main(string[] args)
{
int r = Calculate(Addition);
Console.WriteLine("Result = " + r);
r = Calculate(Substraction);
Console.WriteLine("Result = " + r);
Console.ReadKey();
}
public static int Calculate(MyDelegate delObj)
{
int r = delObj(2, 5);
return r;
}
public static int Addition(int no1, int no2)
{
return no1 + no2;
}
public static int Substraction(int no1, int no2)
{
return no1 - no2;
}
}
- 这里,我们添加了一个方法
Substraction()
。 Calculate
方法不硬编码对Addition()
方法的调用。它使用delegate
调用。- 我们可以将任何方法的地址传递给
Calculate()
。如第 3 行所示,我们将其传递了Subtraction()
方法的地址。 - 这些方法通常被称为回调方法。
Lambda 表达式实战
在上述场景中,每当我们要为 Calculate()
方法添加新功能时,我们都需要先编写一个新方法,然后将它的引用传递给 Calculate()
方法。
例如,为了支持乘法,我们首先必须编写 Multiplication()
方法,然后将其引用传递给 Calculate()
方法。如果您的方法足够复杂,包含多行代码,则此方法很好。但对于像 Addition()
和 Substaction()
这样的一行方法,我们能否不创建单独的方法,而是将其作为匿名方法编写?而且我们知道,lambda 表达式提供了一种优雅的方式来编写匿名方法。因此,在上面的代码中,我们可以使用 lambda 表达式重构如下
class Program
{
public delegate int MyDelegate(int no1, int no2);
static void Main(string[] args)
{
int r = Calculate((no1,no2) => no1 + no2);
Console.WriteLine("Result = " + r);
r = Calculate((no1, no2) => no1 - no2);
Console.WriteLine("Result = " + r);
Console.ReadKey();
}
public static int Calculate(MyDelegate delObj)
{
int r = delObj(2, 5);
return r;
}
}
注意事项
- 在这里,在调用
calculate
方法时,我们直接以 lambda 表达式语法编写Addition()
和Substraction()
的主体。 - 匿名方法将在内存中创建,其地址将传递给
Calculate()
方法。Calculate()
方法将把该地址保存在delObj delegate
对象中。并将使用它调用相同的方法。 - 现在,如果您想支持乘法功能,一行代码就足够了
r = Calculate((no1, no2) => no1 * no2);
Lambda 表达式 - 真实示例
上面使用的例子在实际场景中可能不实用,但我只是用它来以最简单的形式解释概念。在实际生活中,lambda 表达式在编写 LINQ 查询时被广泛使用。
以下示例将为您提供一个 lambda 表达式实际用法的简短测试
public class Employee
{
public int EmpId { get; set; }
public string EmpName { get; set; }
public double Salary { get; set; }
public string City { get; set; }
public override string ToString()
{
return String.Format("{0} {1} {2} {3}",EmpId,EmpName,Salary,City);
}
}
public class Program
{
static void Main(string[] args)
{
List<Employee> employees = new List<Employee>();
employees.Add(new Employee
{ EmpId = 1, EmpName = "Mikyway", Salary = 10000, City = "Mumbai" });
employees.Add(new Employee
{ EmpId = 2, EmpName = "Andromeda", Salary = 20000, City = "Newyork" });
employees.Add(new Employee
{ EmpId = 3, EmpName = "Sculptor", Salary = 30000, City = "Mumbai" });
foreach(Employee emp in FilterByCity(employees, "Mumbai"))
{
Console.WriteLine(emp.ToString());
}
Console.ReadKey();
}
public static IEnumerable<Employee> FilterByCity
(IEnumerable<Employee> employees, string filterStr)
{
foreach(Employee emp in employees)
{
if (emp.City == filterStr)
yield return emp;
}
}
}
在上面的代码中:
- 我们有一个带有几个属性的
Employee
类 - 员工列表是
Employee
的集合 Filter
是一个根据City
筛选员工列表的方法
如果我们想根据任何其他字段(即 Name
或 Salary
)筛选列表怎么办?那么,我们不应该编写特定的 FilterByCity()
方法,而应该编写通用的 Filter()
方法,它应该能够使用任何字段进行筛选。
如果我们能将硬编码条件 if (emp.City == filterStr)
从 FiterByCity()
方法中取出,这是可能的。我们可以使用 lambda 表达式如下所示
使用 lambda 表达式的通用 Filter()
public delegate bool EmpDelegate(Employee emp);
public class Program
{
static void Main(string[] args)
{
...
Console.WriteLine("Filter by City");
foreach(Employee emp in Filter(employees, emp => emp.City == "Mumbai"))
{
Console.WriteLine(emp.ToString());
}
Console.WriteLine("\nFilter by Salary");
foreach (Employee emp in Filter(employees, emp => emp.Salary >= 20000))
{
Console.WriteLine(emp.ToString());
}
Console.ReadKey();
}
public static IEnumerable<Employee> Filter(IEnumerable<Employee> employees, EmpDelegate delObj)
{
foreach(Employee emp in employees)
{
if (delObj(emp) == true)
yield return emp;
}
}
}
同样的事情可以使用内置的 Func<>
委托来完成。在这种情况下,不需要显式 delegate
声明 (EmpDelegate
)。
public static IEnumerable<Employee> Filter(IEnumerable<Employee> employees, Func<Employee,bool> delObj)
{
foreach(Employee emp in employees)
{
if (delObj(emp) == true)
yield return emp;
}
}
完整代码
public class Employee
{
public int EmpId { get; set; }
public string EmpName { get; set; }
public double Salary { get; set; }
public string City { get; set; }
public override string ToString()
{
return String.Format("{0} {1} {2} {3}",EmpId,EmpName,Salary,City);
}
}
public class Program
{
static void Main(string[] args)
{
List<Employee> employees = new List<Employee>();
employees.Add(new Employee
{ EmpId = 1, EmpName = "Mikyway", Salary = 10000, City = "Mumbai" });
employees.Add(new Employee
{ EmpId = 2, EmpName = "Andromeda", Salary = 20000, City = "Newyork" });
employees.Add(new Employee
{ EmpId = 3, EmpName = "Sculptor", Salary = 30000, City = "Mumbai" });
Console.WriteLine("Filter by City");
foreach(Employee emp in Filter(employees, emp => emp.City == "Mumbai"))
{
Console.WriteLine(emp.ToString());
}
Console.WriteLine("\nFilter by Salary");
foreach (Employee emp in Filter(employees, emp => emp.Salary >= 20000))
{
Console.WriteLine(emp.ToString());
}
Console.ReadKey();
}
public static IEnumerable<Employee>
Filter(IEnumerable<Employee> employees, Func<Employee,bool> delObj)
{
foreach(Employee emp in employees)
{
if (delObj(emp) == true)
yield return emp;
}
}
}
我希望这个例子能帮助您更好地理解 LINQ 查询。为了使这个例子尽可能简单,并只关注 lambda 表达式,避免了一些行业标准,如包含、扩展方法等。
闭包
我将本节包含在内是为了回应评论中的建议。我希望这会使其更有用。
首先检查以下代码
static void Main(string[] args)
{
int i = 5;
Func<int, int> add = x => x + i;
Console.WriteLine(add(10));
Console.ReadKey();
}
这里我们有一个带一个参数的 lambda 表达式“add”。我们将 10 传递给参数 x。变量 i 已经初始化为 5。如果你运行这段代码,输出将是 15。这很明显。
好的,在阅读完代码块后,如果你有灾难的直觉!!!......那么恭喜你!你很好地理解了 lambda 表达式。如果上面的代码对你来说很明显,你找不出任何奇怪的地方,那么很抱歉地说,你需要再读一遍文章......
好的,别担心,只需阅读以下几点,然后阅读上面的代码块
- Lambda 表达式是匿名方法,即编译器将为 lambda 表达式创建新方法(也在单独的线程上)。
- 在上面的例子中,i 是函数 Main() 的局部变量。如果你还记得你最初的编程课程,我们学到“函数局部变量的作用域和生命周期只在函数内部”,即我们不能在函数外部访问函数的局部变量。
- 在上面的代码中,lambda 表达式正在访问 Main() 的变量 i(没有任何错误!!!)。
所以我们可以说,使用 lambda 表达式,我们可以访问 lambda 表达式块外部的变量。这个概念被称为闭包。
百万美元的问题:闭包的概念是否违反了编程的基本定律?实际上不是。为了理解这个概念,让我们看看幕后发生了什么。每当我们在 lambda 表达式中使用外部变量时,编译器都会创建
- 用于 lambda 表达式的匿名类。(闭包类)
- 类变量,用于保存 lambda 表达式中使用的外部变量的值。(闭包变量)
- 构造函数,用于传递我们在 lambda 表达式中使用的外部变量值。即,如果我们在 lambda 表达式中使用两个外部变量,编译器将创建带有两个参数的构造函数。
- 用于 lambda 表达式本身的匿名方法。
在上述情况下,编译器将创建匿名闭包类,如下所示
public class AnonymousClosureClass
{
private int i;
public AnonymousClosureClass(int i)
{
this.i = i;
}
public int AnonymousMethod(int x)
{
return x + i;
}
}
实际上,Main() 函数的变量 i 对 lambda 表达式不可访问。但它的值被传递给构造函数,然后构造函数将该值赋给类变量 i(闭包变量)。还要注意,匿名方法使用的变量 i 是类变量,而不是 Main() 函数的局部变量 i。简而言之,无需扔掉你的旧编程书 :)
参考文献
- Apress Pro ASP.NET MVC 5
- Wrox Professional C# 2012 and .NET 4.5
- http://msdn.microsoft.com/zh-cn/magazine/cc163362.aspx
- http://msdn.microsoft.com/zh-cn/library/orm-9780596516109-03-09.aspx