Lambda 表达式和表达式树:简介
以易于理解的方式介绍了 C# 3 的 lambda 表达式和表达式树,并描述了它们的优点和用途。还涉及了匿名委托。
引言
本文将向您介绍 lambda 表达式和表达式树——这是 C# 和 .NET 运行时最新版本中的两个相关新功能。您将学习如何创建它们,以及如何使用它们来增强和简化您的 C# 代码。假定您已了解 .NET 框架中委托的概念。
让我们从回顾匿名方法开始,因为它们背后的概念将有助于理解 lambda 表达式。
匿名方法
.NET 2.0 引入了一种新的构造:匿名方法。不必在类中声明一个命名方法,然后在创建委托时按名称引用该方法
bool MatchNumbersBelow10(int n)
{
return n<10;
}
...
int GetNumber(List<int> numbers)
{
//gets the first number smaller than 10 in the list
return numbers.Find(MatchNumbersBelow10);
}
…您可以在使用它的地方直接编写方法
int GetNumber(List<int> numbers)
{
//gets the first number smaller than 10 in the list
return numbers.Find(
delegate(int n)
{
return n<10;
}
);
}
如您所见,在上面的示例中,我们正将一种特殊的无名内联方法作为委托直接传递给 numbers.Find()
方法。这种“匿名方法”语法的优点是:
匿名方法规则
定义匿名方法的规则很简单:
- 不要声明返回类型——它将从委托签名中推断出来。
- 使用
delegate
关键字而不是方法名,因为匿名方法永远只能通过委托访问。 - 声明方法参数以匹配委托的签名,就像声明一个普通方法以作为委托传递时一样。
- 不要声明变量名与声明匿名方法的外部方法中的变量名冲突。
Lambda 表达式
C# 3.0 和 .NET 3.0 运行时引入了一种更强大的构造,它建立在匿名方法概念的基础上。它允许您将内联表达式作为委托传递,语法最简洁。不再使用我们上面声明的匿名方法
delegate (int n)
{
return n<10;
}
…我们可以这样做
n => n<10
它看起来更短、更简洁,不是吗?但它是如何工作的呢?lambda 表达式的基本形式是:
参数列表 => 表达式
在上面的示例中,我们有一个名为 n
的参数,其类型被隐式推断为 int
,然后是 lambda 运算符 (=>
),然后是一个检查 n
是否小于 10
的表达式。我们可以使用此 lambda 表达式作为 Find()
方法的输入
//gets the first number smaller than 10 in the list
int result=numbers.Find( n=> n<10);
为了更好地理解 lambda 表达式语法与匿名方法语法的区别,让我们将示例中的匿名方法
delegate(int n)
{
return n<10;
}
…转换为其 lambda 表达式等效形式
n=> n<10
我们不需要 delegate
关键字,所以将其移除。
(int n)
{
return n<10;
}
我们将大括号替换为 =>
lambda 运算符,使其成为内联 lambda 表达式。
(int n) => return n<10;
不需要 (甚至不合法) return
关键字,因为表达式始终是一行返回值的代码。此外,移除分号,因为 n<10
现在是一个表达式,而不是一个完整的语句。
(int n)=> n<10
现在,这已经是一个可用的 lambda 表达式了——但我们可以进一步简化它。参数的类型也可以由编译器推断,因此我们可以移除参数的类型声明。
(n)=> n<10
由于我们不指定参数的类型,现在也可以去掉括号。
n=> n<10
这样就得到了我们的最终 lambda 表达式!
正如您可能仅从该示例中看到的,lambda 表达式在常规编码中的主要优势在于其语法更具可读性且不那么冗长。随着代码变得越来越复杂,这一点变得越来越重要。例如,当我们只添加一个额外的参数时,请查看匿名方法与 lambda 表达式在长度和可读性上的差异
//anonymous method
numbers.Sort(delegate(int x, int y){ return y-x; });
//lambda expression
numbers.Sort((x,y)=> y-x);
在更复杂的示例中,具有多个委托属性,比较匿名方法
ControlTrigger trigger= new ControlTrigger ();
trigger.When=delegate(Control c, ThemePart t)
{
return c.Enabled && c.MouseOver;
};
trigger.Action=delegate(Control c, ThemePart t)
{
t.Visible=true;
};
trigger.ExitAction=delegate(Control c, ThemePart t)
{
t.Visible=false;
};
…与 lambda 表达式
ControlTrigger trigger=new ControlTrigger();
trigger.When=(c,t)=> c.Enabled && c.MouseOver;
trigger.Action=(c,t)=> t.Visible=true;
trigger.ExitAction=(c,t)=> t.Visible=false;
特性和规则
不能显式指定返回类型和名称 (就像匿名方法一样)。返回类型始终从委托签名中推断出来,并且不需要名称,因为表达式始终作为委托处理。
如果表达式只有一个参数,则可以省略参数列表的括号
n => n<10
…除非其参数具有显式声明的数据类型
//Type explicitly declared for an argument - have to include parentheses!
(string name)=> "Name: " + name
如果表达式有多个参数或没有参数,则必须包含括号。
如果 lambda 表达式被强制转换为的委托签名返回类型为 void
,则 lambda 表达式不必返回值
delegate void EmptyDelegate();
…
EmptyDelegate dlgt= ()=> Console.WriteLine("Lambda without return type!");
lambda 中的代码不必是单个语句。如果将多个语句括在语句块中,则可以包含它们:
Action<Control> action=
control=>
{
control.ForeColor=Color.DarkRed;
control.BackColor=Color.MistyRose;
});
在这种形式下,lambda 更接近匿名方法,但语法更简洁。
LINQ 预览版中的 VS IDE 不支持 lambda 语句块,因此它们将被显示为语法错误。然而,尽管 IDE 不支持,它们仍然可以正确编译和运行。
您可以从表达式中访问外部方法中的局部变量和参数,就像您可以使用匿名方法一样。
void GetMatchesFromList(List<int> matchValues)
{
List<int> numbers=GetNumbers(); //Get a list of numbers to search in.
//Get the first number in the numbers list that is also contained in the
//matchValues list.
int result=numbers.Find(n=> matchValues.Contains(n));
}
用途
lambda 表达式在任何需要将一小段自定义代码传递给组件或方法的地方都很有用。匿名方法在 C# 2.0 中很有用,而在 C# 3.0 中,lambda 表达式则大放异彩。
一些示例包括用于过滤、排序、迭代、转换和搜索列表的表达式 (使用 .NET 2.0 中引入的有用方法)
List<int> numbers=GetNumbers();
//find the first number in the list that is below 10
int match=numbers.Find(n=> n<10);
//print all the numbers in the list to the console
numbers.ForEach(n=> Console.WriteLine(n));
//convert all the numbers in the list to floating-point values
List<float> floatNumbers=numbers.ConvertAll<float>(n=> (float)n);
//sort the numbers in reverse order
numbers.Sort((x, y) => y-x);
//filter out all odd numbers
numbers.RemoveAll(n=> n%2!=0);
…传递给长时间运行方法的进度更新处理程序
metafileConverter.Convert(filename,
percentComplete=> progressBar.Value=percentComplete);
…以及简单的事件处理程序
slider.ValueChanged+= (sender, e)=> label.Text=slider.Value.ToString();
XLinq (MS 用于查询 XML 文档的新技术) 和 Linq (MS 用于查询对象集合的新技术) 大量使用 lambda 表达式。
如何使用 Lambda 表达式
当 .NET 的下一版本发布时,使用 lambda 表达式将像在通常使用匿名方法或委托的地方声明它们一样简单
//passing a method as a delegate
int match=numbers.Find(MatchNumbersUnder10);
//passing an anonymous method as a delegate
int match=numbers.Find(delegate(int n) { return n<10; });
//passing a lambda expression as a delegate
int match=numbers.Find(n=> n<10);
但是,目前您需要采取一些额外的步骤。您必须在计算机上安装 LINQ 预览版,并且必须使用其中一个 LINQ 项目模板创建一个项目,以便 VS 知道使用 LINQ 预览版安装附带的 C# 3 编译器。
Lambda 表达式树
lambda 表达式还有另一个强大的特性,它并不显而易见。lambda 表达式可以被用作表达式树 (定义表达式组件的对象层级结构——运算符、属性访问子表达式等),而不是直接转换为代码。这样,表达式就可以在运行时进行分析。
要使 lambda 表达式被视为表达式树,请将其赋值或强制转换为 Expression<T>
类型,其中 T
是定义表达式签名的委托的类型。
Expression<Predicate<int>> expression = n=> n<10;
上面表达式定义的表达式树看起来像这样:
如您所见,Expression<T>
类有一个名为 Body
的属性,其中包含表达式树的顶层表达式对象。对于上面的表达式,它是一个 BinaryExpression
,其 NodeType
为 ExpressionType.LT
(LessThan)。BinaryExpression 对象有一个 Left
属性,其中包含运算符左侧的子表达式——在这种情况下,是一个 ParameterExpression
,其 Name
属性设置为 "n"
。它还有一个 Right
属性,其中包含运算符右侧的子表达式——在这种情况下,是一个 ConstantExpression
,其值为 10
。
也可以像这样手动创建此表达式树:
Expression<Predicate<int>> expression = Expression.Lambda<Predicate<int>>(
Expression.LT(
Expression.Parameter(typeof(int), "n"),
Expression.Constant(10)
),
Expression.Parameter(typeof(int), "n")
);
表达式树可以使用 Expression<T>
类的 Compile()
方法进行编译并转换为委托
//Get a compiled version of the expression, wrapped in a delegate
Predicate<int> predicate=expression.Compile();
//use the compiled expression
bool isMatch=predicate(8); //isMatch will be set to true
Compile()
方法根据表达式动态编译 IL 代码,将其封装在委托中,以便像其他委托一样调用它,然后返回该委托。
用途
如上所示,表达式树对象的属性可用于获取有关表达式所有部分的详细信息。这些信息可用于将表达式翻译成另一种形式,提取依赖信息,或执行其他有用的操作。Microsoft 的数据库语言集成查询 (DLinq) 技术将在 .NET 运行时下一版本中推出,它基于在运行时将 lambda 表达式翻译成 SQL。
我正在开发一个组件,它允许您通过表达式进行简单的自动绑定 (如下所示)
Binding binding=new Binding<Entry, Label>();
binding.SourceObject=src;
binding.DestObject=dest;
binding.SourceExpression=(src,dest)=> src.Name +
" – added on "+src.Date.ToString();
binding.DestExpression=(dest)=>dest.Text;
binding.Bind();
Binding
对象将分析 SourceExpression
属性中指定的表达式树,查找所有绑定依赖项 (在本例中是源对象的 Name
和 Date
属性),附加到它们的属性更改事件 (NameChanged
和 DateChanged
,或者 PropertyChanged
) 的侦听器,并在事件引发时设置目标表达式中指定的属性,以使目标属性值保持最新。
另一种用法是一个动态过滤器,它会根据过滤器文本框中输入的文本更改自动使列表控件保持最新。您只需要做以下事情来设置动态过滤器:
listControl.Filter=(item)=> item.Name.StartsWith(textBox.Text);
每当 textbox.Text
更改时,将在所有项目上运行过滤器表达式,并将不匹配过滤器表达式的项目隐藏。
表达式树还有另一个潜在的用途是轻量级的动态代码生成。您可以按照我上面描述的方式手动构建表达式树,然后调用 Compile()
来创建一个委托,然后调用它来运行生成的代码。使用表达式树对象生成代码比手动输出 IL 要容易得多。
结论
正如您所见,lambda 表达式和表达式树开启了许多新的可能性!我很高兴能使用它们,并期待看到其他开发人员为它们想出的用途。