LINQ能够很方便的进行数据的查询,使用LINQ对数据集进行查询的形式很像使用SQL语句对数据库中的表进行查询,而与之不同的是,LINQ能够面向更多的对象,这些对象包括数组、集合以及数据库,LINQ对数组的查询示例代码如下所示。
static void Main(string[] args)
{
string[] str = { "你好","今天的","天气真不错","生活很阳光"}; //创建数组
var s = from n in str select n; //编写查询字串
foreach (var n in s) //遍历查询对象
{
Console.WriteLine(n.ToString()); //输出对象值
}
Console.ReadKey(); //等待用户按键
}
上述代码对数组str进行了查询,这种方式很像SQL语句。的确LINQ的查询方式和SQL语句很像,其语法和基本内容都没有什么太大的差别,但是LINQ提供了更好的查询的解决方案。LINQ能够查询更多对象(例如上述代码中的数组)而无法使用SQL语句进行查询。另外,LINQ查询语句还能够使用WHERE等关键字进行查询,示例代码如下所示。
static void Main(string[] args)
{
string[] str = { "你好","今天的","天气真不错","生活很阳光"}; //创建数组
var s = from n in str where n.Length > 3 select n; //使用条件查询
foreach (var n in s) //遍历查询对象
{
Console.WriteLine(n.ToString()); //输出对象值
}
Console.ReadKey(); //等待用户按键
}
上述代码修改了LINQ查询语句,为LINQ查询语句增加了条件查询,该条件的意义为查询字符串长度大于3的字符串,运行后如图20-1所示。
图20-1 LINQ查询语句
LINQ构架
在.NET 3.5中,LINQ(Language Integrated Query)已经成为了编程语言的一部分,开发人员已经能够使用Visual Studio 2008创建使用LINQ的应用程序。LINQ对基于.NET平台的编程语言提供了标准的查询操作。在.NET 3.5中,LINQ的基本构架如图20-2所示。
图20-2 LINQ基本构架
如图20-2所示,LINQ能够对不同的对象进行查询。在.NET 3.5中,微软提供了不同的命名空间以支持不同的数据库配合LINQ进行数据查询。在LINQ框架中,处于最上方的就是LINQ应用程序,LINQ应用程序基于.NET框架而存在的,LINQ能够支持C#、VB等.NET平台下的宿主语言进行LINQ查询。在LINQ框架中,还包括Linq Enabled ADO.NET层,该层提供了LINQ查询操作并能够提供数据访问和整合功能。
LINQ包括五个部分,这五个部分分别是LINQ to Objects、LINQ to DataSet、LINQ to SQL、LINQ to Entities、LINQ to XML,在.NET开发中最常用的是LINQto SQL和LINQ to XML,本书也详细介绍LINQ的这两个部分。
LINQ与Web应用程序
在ASP.NET应用程序开发中,常常需要涉及到数据的显式和整合,使用ASP.NET2.0中提供的控件能够编写用户控件,开发人员还能够选择开发自定义控件进行数据显示和整合,但是在数据显示和整合过程中,开发人员往往需要大量的连接、关闭连接等操作,而且传统的方法也破坏了面向对象的特性,使用LINQ能够方便的使用面向对象的方法进行数据库操作。
20.2.1 创建使用LINQ的Web应用程序
创建LINQ的Web应用程序非常的容易,只要创建Web应用程序时选择的平台是基于.NETFramework 3.5的就能够创建使用LINQ的Web应用程序,如图20-4所示。
图20-4 选择.NET Framework 3.5
当创建一个基于系统.NETFramework 3.5的应用程序,系统就能够自动为应用程序创建LINQ所需要的命名空间,示例代码如下所示。
using System.Xml.Linq; //使用LINQ命名空间
using System.Linq; //使用LINQ命名空间
上述命名空间提供了应用程序中使用LINQ所需要的基础类和枚举,在ASP.NET应用程序中就能够使用LINQ查询语句进行查询,示例代码如下所示。
protected void Page_Load(object sender, EventArgs e)
{
string[] str = { "我爱C#", "我喜欢C#", "我做C#开发", "基于.NET平台", "LINQ应用" }; //数据集
var s = from n in str where n.Contains("C#") select n; //执行LINQ查询
foreach (var t in s) //遍历对象
{
Response.Write(t.ToString() + "<br/>"); //输出查询结果
}
}
上述代码在ASP.NET页面中执行了一段LINQ查询,查询字符串中包含“C#”的字符串,运行后如图20-5所示。
图20-5 ASP.NET执行LINQ查询
在ASP.NET中能够使用LINQ进行数据集的查询,Visual Studio 2008已经将LINQ整合成为编程语言中的一部分,基于.NET Framework 3.5的应用程序都可以使用LINQ特性进行数据访问和整合。
20.2.2 基本的LINQ数据查询
使用LINQ能够对数据集进行查询,在ASP.NET中,可以创建一个新的LINQ数据库进行数据集查询,右击现有项目,单击【添加新项】选项,选择【LINQto SQL类】选项,如图20-6所示。
图20-6 创建LINQ to SQL类
创建一个LINQ toSQL类,能够映射一个数据库,实现数据对象的创建,如图20-7所示。创建一个LINQ toSQL类后,可以直接在服务资源管理器中拖动相应的表到LINQ to SQL类文件中,如图20-8所示。
图20-7 服务资源管理器 图20-8 拖动一个表
开发人员能够直接将服务资源管理器中的表拖动到LINQ to SQL类中,在LINQ to SQL类文件中就会呈现一个表的视图。在视图中,开发人员能够在视图中添加属性和关联,并且能够在LINQ to SQL类文件中可以设置多个表,进行可视化关联操作。
创建一个LINQ toSQL类文件后,LINQ toSQL类就将数据进行对象化,这里的对象化就是以面向对象的思想针对一个数据集建立一个相应的类,开发人员能够使用LINQ to SQL创建的类进行数据库查询和整合操作,示例代码如下所示。
protected void Page_Load(object sender, EventArgs e)
{
MyDataDataContext data = new MyDataDataContext(); //使用LINQ类
var s = from n in data.mynews where n.ID==1 select n; //执行查询
foreach (var t in s) //遍历对象
{
Response.Write(t.TITLE.ToString() + "<br/>"); //输出对象
}
}
上述创建了一个MyData.dbml的LINQ to SQL文件,开发人员能够直接使用该类的对象提供数据操作。上述代码使用了LINQto SQL文件提供的类进行数据查询,LINQ查询语句示例代码如下所示。
var s = from n in data.mynews where n.ID==1 select n; //编写查询语句
上述代码使用了LINQ查询语句查询了一个mynews表中ID为1的行,使用LINQ toSQL文件提供的对象能够快速的进行数据集中对象的操作。创建一个MyData.dbml的LINQ to SQL文件,其中MyDataDataContext为类的名称,该类提供LINQ to SQL操作方法,示例代码如下所示。
MyDataDataContext data = new MyDataDataContext(); //使用LINQ类
上述代码使用了LINQ toSQL文件提供的类创建了一个对象data,data对象包含数据中表的集合,通过“.”操作符可以选择相应的表,示例代码如下所示。
data.mynews //选择相应表
使用LINQ查询后运行结果如图20-9所示。
图20-9 LINQ执行数据库查询
使用LINQ技术能够方便的进行数据库查询和整合操作,LINQ不仅能够实现类似SQL语句的查询操作,还能够支持.NET编程方法进行数据查询条件语句的编写。使用LINQ技术进行数据查询的顺序如下所示:
q 创建LINQ to SQL文件:创建一个LINQ to SQL类文件进行数据集封装。
q 拖动数据表:将数据表拖动到LINQ to SQL类文件中,可以进行数据表的可视化操作。
q 使用LINQ to SQL类文件:使用LINQ to SQL类文件提供的数据集的封装进行数据操作。
使用LINQ to SQL类文件能够极快的创建一个LINQ到SQL数据库的映射并进行数据集对象的封装,开发人员能够使用面向对象的方法进行数据集操作并提供快速开发的解决方案。
20.2.3 IEnumerable和IEnumerable<T>接口
IEnumerable和IEnumerable<T>接口在.NET中是非常重要的接口,它允许开发人员定义foreach语句功能的实现并支持非泛型方法的简单的迭代,IEnumerable和IEnumerable<T>接口是.NET Framework中最基本的集合访问器,这两个接口对于LINQ的理解是非常重要的。
在面向对象的开发过程中,常常需要创建若干对象,并进行对象的操作和查询,在创建对象前,首先需要声明一个类为对象提供描述,示例代码如下所示。
using System;
using System.Collections.Generic;
using System.Linq; //使用LINQ命名控件
using System.Text;
namespace IEnumeratorSample
{
classPerson //定义一个Person类
{
public string Name; //定义Person的名字
public string Age; //定义Person的年龄
public Person(string name, string age) //为Person初始化(构造函数)
{
Name = name; //配置Name值
Age = age; //配置Age值
}
}
上述代码定义了一个Person类并抽象一个Person类的属性,这些属性包括Name和Age。Name和Age属性分别用于描述Person的名字和年龄,用于数据初始化。初始化之后的数据就需要创建一系列Person对象,通过这些对象的相应属性能够进行对象的访问和遍历,示例代码如下所示。
classProgram
{
static void Main(string[] args)
{
Person[] per = new Person[2] //创建并初始化2个Person对象
{
new Person("guojing","21"), //通过构造函数构造对象
new Person("muqing","21"), //通过构造函数构造对象
};
foreach (Person p in per) //遍历对象
Console.WriteLine("Name is " + p.Name + " and Age is" + p.Age);
Console.ReadKey();
}
}
}
上述代码创建并初始化了2个Person对象,并通过foreach语法进行对象的遍历。但是上述代码是在数组中进行查询的,就是说如果要创建多个对象,则必须创建一个对象的数组,如上述代码中的Per变量,而如果需要直接对对象的集合进行查询,却不能够实现查询功能。例如增加一个构造函数,该构造函数用户构造一组Person对象,示例代码如下所示。
private Person[] per;
public Person(Person[] array) //重载构造函数,迭代对象
{
per = new Person[array.Length]; //创建对象
for (int i = 0; i < array.Length; i++) //遍历初始化对象
{
per[i] = array[i]; //数组赋值
}
}
上述构造函数动态的构造了一组People类的对象,那么应该也能够使用foreach语句进行遍历,示例代码如下所示。
Person personlist = new Person(per); //创建对象
foreach(Person p in personlist) //遍历对象
{
Console.WriteLine("Name is " + p.Name + " and Age is" + p.Age);
}
在上述代码的foreach语句中,直接在Person类的集合中进行查询,系统则会报错“ConsoleApplication1.Person”不包含“GetEnumerator”的公共定义,因此foreach语句不能作用于“ConsoleApplication1.Person”类型的变量,因为Person类并不支持foreach语句进行遍历。为了让相应的类能够支持foreach语句执行遍历操作,则需要实现派生自类IEnumerable并实现IEnumerable接口,示例代码如下所示。
public IEnumerator GetEnumerator() //实现接口
{
return new GetEnum(_people);
}
为了让自定义类型能够支持foreach语句,则必须对Person类的构造函数进行编写并实现接口,示例代码如下所示。
classPerson:IEnumerable //派生自IEnumerable,同样定义一个Personl类
{
public string Name; //创建字段
public string Age; //创建字段
public Person(string name, string age) //字段初始化
{
Name = name; //配置Name值
Age = age; //配置Age值
}
public IEnumerator GetEnumerator() //实现接口
{
return new PersonEnum(per); //返回方法
}
}
上述代码重构了Person类并实现了接口,接口实现的具体方法如下所示。
classPersonEnum : IEnumerator //实现foreach语句内部,并派生
{
public Person[] _per; //实现数组
int position = -1; //设置“指针”
publicPersonEnum(Person[] list)
{
_per= list; //实现list
}
publicbool MoveNext() //实现向前移动
{
position++; //位置增加
return (position < _per.Length); //返回布尔值
}
publicvoid Reset() //位置重置
{
position = -1; //重置指针为-1
publicobject Current //实现接口方法
{
get
{
try
{
return _per[position]; //返回对象
}
catch (IndexOutOfRangeException) //捕获异常
{
throw new InvalidOperationException(); //抛出异常信息
}
}
}
}
上述代码实现了foreach语句的功能,当开发Person类初始化后就可以直接使用Personal类对象的集合进行LINQ查询,示例代码如下所示。
static void Main(string[] args)
{
Person[] per = new Person[2] //同样初始化并定义2个Person对象
{
new Person("guojing","21"), //构造创建新的对象
new Person("muqing","21"), //构造创建新的对象
};
Person personlist = new Person(per); //初始化对象集合
foreach (Person p in personlist) //使用foreach语句
Console.WriteLine("Name is " + p.Name + " and Age is" + p.Age);
Console.ReadKey();
}
从上述代码中可以看出,初始化Person对象时初始化的是一个对象的集合,在该对象的集合中可以通过LINQ直接进行对象的操作,这样做即封装了Person对象也能够让编码更加易读。在.NET Framework 3.5中,LINQ支持数组的查询,开发人员不必自己手动创建IEnumerable和IEnumerable<T>接口以支持某个类型的foreach编程方法,但是IEnumerable和IEnumerable<T>是LINQ中非常重要的接口,在LINQ中也大量的使用IEnumerable和IEnumerable<T>进行封装,示例代码如下所示。
public static IEnumerable<TSource>Where<TSource>
(this IEnumerable<TSource>source,Func<TSource, Boolean> predicate) //内部实现
{
foreach (TSource element in source) //内部遍历传递的集合
{
if (predicate(element))
yield return element; //返回集合信息
}
}
上述代码为LINQ内部的封装,从代码中可以看到,在LINQ内部也大量的使用了IEnumerable和IEnumerable<T>接口实现LINQ查询。IEnumerable原本就是.NET Framework中最基本的集合访问器,而LINQ是面向关系(有序N元组集合)的,自然也就是面向IEnumerable<T>的,所以了解IEnumerable和IEnumerable<T>对LINQ的理解是有一定帮助的。
20.2.4 IQueryProvider和IQueryable<T>接口
IQueryable和IQueryable<T>同样是LINQ中非常重要的接口,在LINQ查询语句中,IQueryable和IQueryable<T>接口为LINQ查询语句进行解释和翻译工作,开发人员能够通过重写IQueryable和IQueryable<T>接口以实现用不同的方法进行不同的LINQ查询语句的解释。
IQueryable<T>继承于IEnumerable<T>和IQueryable接口,在IQueryable中包括两个重要的属性,这两个属性分别为Expression和Provider。Expression和Provider分别表示获取与IQueryable 的实例关联的表达式目录树和获取与数据源关联的查询提供程序,Provider作为其查询的翻译程序实现LINQ查询语句的解释。通过IQueryable和IQueryable<T>接口,开发人员能够自定义LINQ Provider。
注意:Provider可以被看做是一个提供者,用于提供LINQ中某个语句的解释工具,在LINQ中通过编程的方法能够实现自定义Provider。
在IQueryable和IQueryable<T>接口中,还需要另外一个接口,这个接口就是IQueryProvider,该接口用于分解表达式,实现LINQ查询语句的解释工作,这个接口也是整个算法的核心。IQueryable<T>接口在MSDN中的定义如下所示。
public interface IQueryable<T> :IEnumerable<T>, IQueryable, IEnumerable
{
}
public interface IQueryable : IEnumerable
{
TypeElementType { get; } //获取元素类型
ExpressionExpression { get; } //获取表达式
IQueryProvider Provider { get; } //获取提供者
}
上述代码定义了IQueryable<T>接口的规范,用于保持数据源和查询状态,IQueryProvider在MSDN中定义如下所示。
public interface IQueryProvider
{
IQueryableCreateQuery(Expression expression); //创建可执行对象
IQueryable<TElement> CreateQuery<TElement>(Expressionexpression); //创建可执行对象
objectExecute(Expression expression); //计算表达式
TResultExecute<TResult>(Expression expression); //计算表达式
}
IQueryProvider用于LINQ查询语句的核心算法的实现,包括分解表达式和表达式计算等。为了能够创建自定义LINQ Provider,可以编写接口的实现。示例代码如下所示。
public IQueryable<TElement> CreateQuery<TElement>(Expressionexpression)
{
query.expression = expression; //声明表达式
return (IQueryable<TElement>)query; //返回query对象
}
上述代码用于构造一个可用来执行表达式计算的IQueryable 对象,在接口中可以看到需要实现两个相同的执行表达式的IQueryable 对象,另一个则是执行表达式对象的集合,其实现代码如下所示。
public IQueryable CreateQuery(Expression expression)
{
return CreateQuery<T>(expression); //返回表达式的集合
}
而作为表达式解释和翻译的核心接口,则需要通过算法实现相应Execute方法,示例代码如下所示。
publicTResult Execute<TResult>(Expression expression)
{
var exp = expression as MethodCallExpression; //创建表达式对象
var data = ((exp.Arguments[0] as ConstantExpression).Value asMyQuery<T>).Data;
var func = (exp.Arguments[1] as UnaryExpression).Operand as Expression
<System.Func<T,bool>>;
var lambda = Expression.Lambda<Func<T, bool>>(func.Body,func.Parameters[0]);
var r = data.Where(lambda.Compile()); //编译表达式
return (TResult)r.GetEnumerator();
}
上述代码通过使用lambda表达式进行表达式的计算,实现了LINQ中查询的解释功能。在LINQ中,对于表达式的翻译和执行过程都是通过IQueryProvider和IQueryable<T>接口来实现的。IQueryProvider和IQueryable<T>实现用户表达式的翻译和解释,在LINQ应用程序中,通常无需通过IQueryProvider和IQueryable<T>实现自定义LINQ Provider,因为LINQ已经提供强大表达式查询和计算功能。了解IQueryProvider和IQueryable<T>接口有助于了解LINQ内部是如何执行的。
20.2.5 LINQ相关的命名空间
LINQ开发为开发人员提供了便利,可以让开发人员以统一的方式对IEnumerable<T>接口的对象、数据库、数据集以及XML文档进行访问。从整体上来说,LINQ是这一系列访问技术的统称,对于不同的数据库和对象都有自己的LINQ名称,例如LINQ to SQL、LINQ to Object等等。当使用LINQ操作不同的对象时,可能使用不同的命名空间。常用的命名空间如下所示。
q System.Data.Linq: 该命名空间包含支持与 LINQ to SQL 应用程序中的关系数据库进行交互的类。
q System.Data.Linq.Mapping:该命名空间包含用于生成表示关系数据库的结构和内容的 LINQ to SQL 对象模型的类。
q System.Data.Linq.SqlClient:该命名空间包含与SQL Server 进行通信的提供程序类,以及包含查询帮助器方法的类。
q System.Linq:该命名空间提供支持使用语言集成查询 (LINQ)进行查询的类和接口。
q System.Linq.Expression:该命名空间包含一些类、接口和枚举,它们使语言级别的代码表达式能够表示为表达式树形式的对象。
q System.Xml.Linq:包含 LINQ to XML 的类,LINQ to XML 是内存中的 XML 编程接口。
LINQ中常用的命名空间为开发人员提供LINQ到数据库和对象的简单的解决方案,开发人员能够通过这些命名空间提供的类进行数据查询和整理,这些命名空间统一了相应的对象的查询方法,如数据集和数据库都可以使用类似的LINQ语句进行查询操作。
20.3 Lambda表达式
Lambda表达式是一种高效的类似于函数式编程的表达式,Lambda简化了开发中需要编写的代码量。Lambda表达式是由.NET 2.0演化过来的,也是LINQ的基础,熟练的掌握Lambda表达式能够快速的上手LINQ应用开发。
20.3.1 匿名方法
在了解Lambda表达式之前,需要了解什么是匿名方法,匿名方法简单的说就是没有名字的方法,而通常情况下的方法定义是需要名字的,示例代码如下所示。
public int sum(int a, int b) //创建方法
{
return a + b; //返回值
}
上面这个方法就是一个常规方法,这个方法需要方法修饰符(public)、返回类型(int)方法名称(sum)和参数列表。而匿名方法可以看作是一个委托的扩展,是一个没有命名的方法,示例代码如下所示。
delegate int Sum(int a,int b); //声明匿名方法
protected void Page_Load(object sender, EventArgs e)
{
Sum s = delegate(int a,int b) //使用匿名方法
{
return a + b; //返回值
};
}
上述代码声明了一个匿名方法Sum但是没有实现匿名方法的操作的实现,在声明匿名方法对象时,可以通过参数格式创建一个匿名方法。匿名方法能够通过传递的参数进行一系列操作,示例代码如下所示。
Response.Write(s(5,6).ToString());
上述代码使用了s(5,6)方法进行两个数的加减,匿名方法虽然没有名称,但是同样可以使用“(”“)”号进行方法的使用。
注意:虽然匿名方法没有名称,但是编译器在编译过程中,还是会为该方法定义一个名称,只是在开发过程中这个名称是不被开发人员所看见的。
除此之外,匿名方法还能够使用一个现有的方法作为其方法的委托,示例代码如下所示。
delegateint Sum(int a,int b); //方法委托
public int retSum(int a, int b) //普通方法
{
return a + b;
}
上述代码声明了一个匿名方法,并声明了一个普通的方法,在代码中使用匿名方法代码如下所示。
protected void Page_Load(object sender, EventArgs e)
{
Sum s = retSum; //使用匿名方法
int result = s(10, 10);
}
从上述代码中可以看出,匿名方法是一个没有名称的方法,但是匿名方法可以将方法名作为参数进行传递,如上述代码中变量s就是一个匿名方法,这个匿名方法的方法体被声明为retSum,当编译器进行编译时,匿名方法会使用retSum执行其方法体并进行运算。匿名方法最明显的好处就是可以降低常规方法编写时的工作量,另外一个好处就是可以访问调用者的变量,降低传参数的复杂度。
20.3.2 Lambda表达式基础
在了解了匿名方法后,就能够开始了解Lambda表达式,Lambda表达式在一定程度上就是匿名方法的另一种表现形式。为了方便对Lambda表达式的解释,首先需要创建一个People类,示例代码如下所示。
publicclass People
{
public int age { get; set; } //设置属性
public string name { get; set; } //设置属性
public People(int age,string name) //设置属性(构造函数构造)
{
this.age = age; //初始化属性值age
this.name = name; //初始化属性值name
}
}
上述代码定义了一个People类,并包含一个默认的构造函数能够为People对象进行年龄和名字的定义。在应用程序设计中,很多情况下需要创建对象的集合,创建对象的集合有利于对对象进行操作和排序等操作,以便在集合中筛选相应的对象。使用List进行泛型编程,可以创建一个对象的集合,示例代码如下所示。
List<People> people = new List<People>(); //创建泛型对象
People p1 = new People(21,"guojing"); //创建一个对象
People p2 = new People(21, "wujunmin"); //创建一个对象
People p3 = new People(20, "muqing"); //创建一个对象
People p4 = new People(23, "lupan"); //创建一个对象
people.Add(p1); //添加一个对象
people.Add(p2); //添加一个对象
people.Add(p3); //添加一个对象
people.Add(p4); //添加一个对象
上述代码创建了4个对象,这4个对象分别初始化了年龄和名字,并添加到List列表中。当应用程序需要对列表中的对象进行筛选时,例如需要筛选年龄大于20岁的人时,就需要从列表中筛选,示例代码如下所示。
IEnumerable<People> results = people.Where(delegate(People p) {return p.age > 20; });//匿名方法
上述代码通过使用IEnumerable接口创建了一个result集合,并且该集合中填充的是年龄大于20的People对象。细心的读者就能够发现在这里使用了一个匿名方法进行筛选,因为该方法没有名称,该匿名方法通过使用People类对象的age字段进行筛选。
虽然上述代码中执行了筛选操作,但是使用匿名方法往往不太容易理解和阅读,而Lambda表达式相比于匿名方法而言更加容易理解和阅读,示例代码如下所示。
IEnumerable<People> results = people.Where(People => People.age> 20); //Lambda
上述代码同样返回了一个People对象的集合给变量results,但是其编写的方法更加容易阅读,这里可以看出Lambda表达式在编写的格式上和匿名方法非常相似。其实当编译器开始编译并运行,Lambda表达式最终也表现为匿名方法。
使用匿名方法实际上并不是创建了没有名称的方法,实际上编译器会创建一个方法,这个方法对于开发人员来说是看不见的,该方法会将People类的对象中符合p.age>20条件的对象返回并填充到集合中。相同的是,使用Lambda表达式,当编译器编译时,Lambda表达式同样会被编译成一个匿名方法进行相应的操作,但是相比于匿名方法而言,Lambda表达式更容易阅读,Lambda表达式的格式如下所示。
(参数列表)=>表达式或者语句块
如上述代码中,参数列表就是People类,表达式和语句块就是People.age>20,使用Lambda表达式能够让人很容易的理解该语句究竟是如何执行的,虽然匿名方法提供了同样的功能,却并不容易理解。相比之下People => People.age > 20却能够很好的理解为“返回一个年纪大于20的人”。其实Lambda表达式并没有什么高深的技术,Lambda表达式可以看作是匿名方法的另一种表现形式。其实Lambda表达式经过反编译后,与匿名方法并没有什么区别。
20.3.3 Lambda表达式格式
Lambda表达式是匿名方法的另一种表现形式。比较Lambda表达式和匿名方法,在匿名方法中,“(”,“)”内是方法的参数的集合,这就对应了Lambda表达式中“(参数列表)”,而匿名方法中“{”,“}”内是方法的语句块,这也对应了Lambda表达式“=>”符号右边的表达式和语句块项。由于Lambda表达式是一种匿名方法,所以Lambda表达式也包含一些基本格式,这些基本格式如下所示。
Lambda表达式可以有多个参数,一个参数,或者无参数。其参数类型可以隐式或者显式。示例代码如下所示:
(x, y) => x * y //多参数,隐式类型=> 表达式
x => x * 5 //单参数,隐式类型=>表达式
x => { return x * 5; } //单参数,隐式类型=>语句块
(int x) => x * 5 //单参数,显式类型=>表达式
(int x) => { return x * 5; } //单参数,显式类型=>语句块
() => Console.WriteLine() //无参数
上述格式都是Lambda表达式的合法格式,在编写Lambda表达式时,可以忽略参数的类型,因为编译器能够根据上下文直接推断参数的类型,示例代码如下所示。
(x, y) => x + y //多参数,隐式类型=> 表达式
Lambda表达式的主体可以是表达式也可以是语句块,这样就节约了代码的编写。
注意:Lambda表达式与匿名方法的另一个不同是,Lambda表达式的主体可以是表达式也可以是语句块,而匿名方法中不能包含表达式。
Lambda表达式中的表达式和表达式体都能够被转换成表达式树,这在表达式树的构造上会起到很好的作用,表达式树也是LINQ中最基本最重要的概念。
20.3.4 Lambda表达式树
Lambda表达式树也是LINQ中最重要的一个概念,Lambda表达式树允许开发人员像处理数据一样对Lambda表达式进行修改。理解Lambda表达式树的概念并不困难,Lambda表达式树就是将Lambda表达式转换成树状结构,在使用Lambda表达式树之前还需要使用System.Linq.Expressions命名空间,示例代码如下所示。
using System.Linq.Expressions; //使用命名空间
Lambda表达式树的基本形式有两种,这两种形式代码如下所示。
Func<int, int> func = pra => pra * pra; //创建表达式树
Expression<Func<int,int>> expression = pra => pra * pra; //创建表达式树
Lambda表达式树就是将Lambda表达式转换成树状结构,示例代码如下所示。
Func<int, int> func = (pra => pra * pra); //创建表达式
Response.Write(func(8).ToString()); //执行表达式
Response.Write("<hr/>");
上述代码直接用Lambda表达式初始化Func委托,运行后返回的结果为64,同样使用Expression类也可以实现相同的效果,示例代码如下所示。
Expression<Func<int, int>> expression = pra => pra * pra; //创建表达式
Func<int, int> func1 =expression.Compile(); //编译表达式
Response.Write(func1(8).ToString());
上述代码运行后同样返回64,运行后如图20-10所示。使用Func类和Expression类创建Lambda表达式运行结果基本相同,但是Func方法和Expression方法是有区别的,如Lambda表达式pra => pra * pra,Expression首先会分析该表达式并将表达式转换成树状结构,如图20-11所示。
图20-10 Lambda表达式树 图20-11 Lambda表达式树格式
当编译器编译Lambda表达式时,如果Lambda表达式使用的是Func方法,则编译器会将Lambda表达式直接编译成匿名方法,而如果Lambda表达式使用的是Expression方法,则编译器会将Lambda表达式进行分析、处理然后得到一种数据结构。
20.3.5 访问Lambda表达式树
既然在LINQ应用开发中常常需要解析Lambda表达式,则就不能避免的对Lambda表达式树进行访问,访问Lambda表达式的方法非常简单,直接将表达式输出即可,示例代码如下所示。
Expression<Func<int, int>> expression = pra => pra * pra; //创建表达式树
Func<int, int> func1 = expression.Compile(); //表达式树编译
Response.Write(func1(8).ToString()); //执行表达式
Response.Write(expression.ToString()); //执行表达式
上述代码直接使用Expression类的对象进行表达式输出,这时候读者可能会想到,是否能够像Expression类的对象一样直接将Func对象进行输出,答案是否定的,而如果直接使用Func对象是不能够输出表达式的,如图20-12所示。
图20-12 Lambda表达式输出
正如图20-12所示,直接输出Expression类的对象的值能够进行表达式的输出,而如果输出Func类的对象只会输出系统信息,如System.Func`2[System.Int32,System.Int32],这并不是期待的结果。这也从另一个角度说明了Func方法和Expression方法是有区别的。