一览LINQ






4.81/5 (40投票s)
2005年9月21日
10分钟阅读

172812

465
对新的语言集成查询(LINQ)框架的概述。
引言
对于今天的大多数程序员来说,我们的工作要求我们将某种数据集成到我们的应用程序中。我们常常需要从多种数据源获取数据,无论它们是内存中的集合、像 SQL Server 或 Access 这样的数据库、XML 文件、Active Directory、文件系统等等。使用当今的语言和技术,获取这些数据通常很繁琐。对于数据库来说,使用 ADO.NET 就是一堆管道代码,很快就会变得非常无聊。处理 XML 的情况更糟,因为 System.Xml
命名空间使用起来非常笨重。而且,所有数据源都有不同的查询数据的方式。数据库用 SQL,XML 用 XQuery,Active Directory 用 LDAP 查询等等。简而言之,当今的数据访问现状一团糟。
LINQ 登场
微软的同仁们并非没有意识到当今数据访问现状的问题。因此,既然 C# 2.0 即将发布,他们以 LINQ 项目的形式向我们展示了 C# 3.0 的面貌。LINQ 代表语言集成查询(Language Integrated Query)框架。LINQ 项目的既定目标是“为 .NET Framework 添加通用的查询功能,这些功能适用于所有信息源,而不仅仅是关系型数据或 XML 数据”。LINQ 项目的美妙之处有两点。
首先,LINQ 直接集成到你喜欢的语言中。因为底层的 API 只是一组 .NET 类,其操作方式与任何其他 .NET 类一样,所以语言设计者可以将这些类暴露的功能直接集成到语言中。其次,或许也是最重要的一点,LINQ 中的查询功能不仅仅局限于 SQL 或 XML 数据。任何实现 IEnumerable<T> 的类都可以使用 LINQ 进行查询。这应该会让你感到无比喜悦。或者也许只是我比较奇怪。
我们来看看 LINQ
在本文中,我只想关注 LINQ 的语言特性。太多人将 LINQ 与 DLinq 和 XLinq 混淆了。它们并非一回事。LINQ 是一套 DLinq 和 XLinq 所遵循的语言特性和类模式。话虽如此,在本文中,我们将只处理内存中的数据集合。所以,言归正传,让我们看一个完全没有实际用途的基本 LINQ 程序(这种程序是最好的,不是吗?)
using System;
using System.Query;
namespace LinqArticle
{
public static class GratuitousLinqExample
{
public static void Main()
{
// The most active list on CP
var mostActive = new string[] {
"Christian Graus",
"Paul Watson",
"Nishant Sivakumar",
"Roger Wright",
"Jörgen Sigvardsson",
"David Wulff",
"ColinDavies",
"Chris Losinger",
"peterchen",
"Shog9" };
// Get only the people whose name begins with D
var namesWithD = from poster in mostActive
where poster.StartsWith("D")
select poster;
// Print each person out
foreach(var individual in namesWithD)
{
Console.WriteLine(individual);
}
}
}
}
就是这样。现在,你可能看着这段代码说:“这完全没有实际用途!” 我想提醒你:这正是重点。我们这里有一个最活跃的 CPians(CodeProject 用户)列表。然后我们用一种看起来有点像 SQL 的奇特查询,只获取名字以字母 D 开头的 CPians。然后我们将他们的名字输出到控制台。在这个例子中,我们得到了大家最喜欢的 Tivertonian,David Wulff。这里并没有太多特别之处。你可能坐在那里想,完全可以用下面这样的代码来替换整个东西
// Print each person out
foreach(var someone in mostActive)
{
if(someone.StartsWith("D"))
{
Console.WriteLine(someone);
}
}
你是对的,你可以用那段代码替换它。但那样就不酷了,因为它没有前一个例子中那种刻意使用的 LINQ。
深入探究
现在让我们更深入地、更仔细地看看 LINQ。我们也会看看 C# 3.0 中的新语言特性。我想首先指出,这些新的语言特性是运行在 .NET 2.0 CLR 上的。这是关键,因为与 C# 2.0 的迭代器、匿名方法等特性不同,实现这些 3.0 特性并不需要深入的底层工作。这很可能意味着 C# 3.0 和 LINQ 的发布周期会更短。(至少,传闻是这么说的。)总之,既然我们已经说清楚了这一点,让我们仔细看看我们的 LINQ 代码。首先,我们有一个很酷的新 System.Query
命名空间引用
using System.Query;
这个命名空间就是你开始使用 LINQ 所需要的一切。它里面包含了大量的宝藏、无数的知识宝典,以及超乎你想象的力量……或者可能只是一些类和委托。
类型推断
然后我们有一个奇怪的 var
关键字,它到处出现。观众中的 JavaScript 开发者现在应该会感到很亲切。var
是 C# 3.0 中引入的一个新关键字,有特殊的含义。首先,我们来谈谈 var
不是什么。var
不是一个变体数据类型(JavaScript 开发者现在又不亲切了),也不是 object 的另一个关键字。var
用于向编译器发出信号,表明我们正在使用 C# 3.0 中新的局部变量类型推断。所以,与 JavaScript 中 var
意味着变量可以存放任何我们想要的东西不同,这个 var
关键字告诉 C# 编译器从赋值中推断类型。这是什么意思呢?好吧,让我们看一小段代码来演示
var myInt = 5;
var myString = "This is pretty stringy";
var myGuid = new System.Guid();
在上面的例子中,编译器看到了 var
关键字,查看了对 myInt
的赋值,并确定它应该是一个 Int32,然后将 5 赋给它。当它看到我们将字符串赋给 myString
变量时,它确定 myString
应该是 System.String
类型。myGuid
也是如此。很酷,对吧?如果你接下来尝试做一些愚蠢的事情,比如
myInt = "Haha, let's see if we can trick the compiler!";
我们将会得到一个友好的编译器错误消息,告诉我们有多么愚蠢:无法将类型 'string
' 隐式转换为 'int
'。(我敢肯定,如果编译器看过《大人物拿破仑》,它现在肯定会说:“天哪!真是个白痴!”)
标准查询运算符
现在,继续往下看,我们可以看到在我们有了字符串数组之后,有这么一段奇特的代码
// Get only the people whose name begins with D
var namesWithD = from poster in mostActive
where poster.StartsWith("D")
select poster;
这才是真正有趣的地方。我们在这里看到的是一个变量被赋给了一个看起来很像 SQL 查询的东西。它有 select
(虽然位置不对)、from
、where
;这就是 SQL,对吗?不。这些关键字是 LINQ 的一些标准查询运算符。当 C# 编译器看到这些关键字时,它会将它们映射到一组执行相应操作的方法上。或者,你也可以像这样写上面的查询
// Get only the people whose name begins with D
var namesWithD = mostActive
.Where(person => person.StartsWith("D"))
.Select(person => person);
这就是 LINQ 人员所说的“显式点表示法”。这是完全相同的查询,你可以用任何一种方式来写查询。当然,这也引出了一个附带问题,就是那些有趣的“=>”符号是什么。
Lambda 表达式
那些是 C# 3.0 的另一个特性:Lambda 表达式。Lambda 表达式是 C# 2.0 匿名方法的自然演进。本质上,Lambda 表达式是一种方便的语法,我们用它来将一段代码(匿名方法)赋给一个变量(委托)。在这种情况下,我们在上面查询中使用的委托在 System.Query
命名空间中定义如下
public delegate T Func<T>();
public delegate T Func<A0, T>(A0 arg0);
所以这段代码片段
person => person.StartsWith("D")
可以写成
Func<string, bool> person = delegate (string s) {
return s.StartsWith("D");
};
比第一种方式紧凑多了,不是吗?Lambda 表达式基本上只是匿名方法的语法糖,你在为这些查询运算符创建过滤器时,可以使用它们中的任何一种,甚至可以使用常规的命名方法。不过,Lambda 表达式的好处是,根据它们的使用方式,可以被编译成 IL 或表达式树。这些东西对于当前的讨论来说有点太深了。总而言之,Lambda 表达式非常酷。下一个主题!
扩展方法
敏锐的读者会注意到,到目前为止,还没有讨论过标准查询运算符映射到的这些方法来自哪里。我之前提到 LINQ 适用于任何实现了 IEnumerable<T>
的东西。因此,人们可以合理地假设这些方法存在于 C# 3.0 中 IEnumerable<T>
接口的新定义中。然而,这个假设是错误的。这些方法位于 System.Query.Sequence
类中(其源代码在 LINQ 预览版安装包中可以找到),是 C# 3.0 中一个名为扩展方法的新特性的一部分。
扩展方法是一种扩展现有类型的新方法。基本上,它的工作原理是在第一个参数上添加一个“this
”修饰符,像这样:(示例代码无耻地从 Sequence
类中偷来。)
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source, Func<T, bool> predicate) {
foreach (T element in source) {
if (predicate(element)) yield return element;
}
}
这里没有什么特别的,除了第一个参数上的那个“this
”修饰符。编译器看到这个修饰符,就把它当作指定类型上的一个新方法。所以现在 IEnumerable<T>
得到了 Where()
方法。很酷,对吧?需要记住的是,“真正”的方法有更高的优先级。如果你在一个对象上调用 Where()
,编译器会先去对象本身上寻找 Where()
。如果 Where()
不存在,它才会去寻找一个扩展方法。显然,虽然这个特性很酷且非常强大,但扩展方法应该极其谨慎地使用。Anders Hejlsberg 在 PDC 的 LINQ 演讲中警告我们,不要给我们喜欢的 10 个方法添加到 System.Object
上。这个特性可能是最容易让你搬起石头砸自己脚的特性。
一个更有趣的 LINQ 示例
现在我们已经看过了 LINQ 和 C# 3.0 的基础知识,让我们来看一个稍微有趣一点的例子。首先,让我们为自己定义一个新的 Poster
类
public class Poster
{
public string name;
public int numberOfPosts;
public int numberOfArticles;
public Poster(string name,
int numberOfPosts, int numberOfArticles)
{
this.name = name;
this.numberOfPosts = numberOfPosts;
this.numberOfArticles = numberOfArticles;
}
}
现在让我们修改前面的例子来使用我们的新 Poster
类(显然这些值会改变)
public static void Main()
{
// The most active list on CP, with
// names, posts, and message count
var mostActive = new Poster[] {
new Poster("Christian Graus", 22215, 32),
new Poster("Paul Watson", 20185, 7),
new Poster("Nishant Sivakumar", 18608, 99),
new Poster("Roger Wright", 16790, 1),
new Poster("Jörgen Sigvardsson", 14118, 7),
new Poster("David Wulff", 13748, 4),
new Poster("ColinDavies", 12919, 0),
new Poster("Chris Losinger", 11970, 18),
new Poster("peterchen", 11163, 9),
new Poster("Shog9", 10605, 3)
};
// Get only the people who have ridiculously
// large post counts
var peopleWithoutLives = from poster in mostActive
where poster.numberOfPosts > 15000
select new {poster.name, poster.numberOfPosts};
// Print each person out
foreach(var individual in peopleWithoutLives)
{
Console.WriteLine("{0} has posted {1} messages",
individual.name,
individual.numberOfPosts);
}
}
匿名类型
现在,我们有了一个按消息数和文章数排列的最活跃 CPians 数组。在我们的查询中,我们指定只想要那些发帖超过 15000 的 CPians……但是 select
子句不同。因为我们只想要他们的名字和消息数,而不是他们发表的文章数,所以我们只指定了这两个字段。这是 C# 3.0 的一个新特性,叫做匿名类型(现在 .NET 里怎么这么多匿名?天哪!)。通常我们只想要我们查询的集合中的某些字段,所以这是一种很好的、简单的方法来只查询出那些字段。但是,你可能会问,那个类型叫什么名字?嗯,CLR 会给它分配一个名字。而且很可能是一个非常难念的名字。但只要接受它是一个新类型,并且只包含你所请求的那些字段就可以了。
看一些更高级的特性
让我们给这个例子加点料,引入一些新的运算符:groupby
和 orderby
。
// Group the people with really large post counts
var peopleWithoutLives = from poster in mostActive
group poster by (poster.numberOfPosts / 5000) into postGroup
orderby postGroup.Key descending
select postGroup;
// Print each person out in their respective group
Console.WriteLine("Posters by group");
foreach(var group in peopleWithoutLives)
{
Console.WriteLine("{0}-{1}",
(group.Key + 1) * 5000,
group.Key * 5000);
foreach(var person in group.Group)
{
Console.WriteLine("\t{0}", person.name);
}
}
所以我们看到,我们能够根据某个标准:键(Key),将人们分组。在这个例子中,我们的标准是他们发的帖子数除以 5000,这样我们就可以看到谁属于哪个 5000 帖子的区间。标准表达式的值然后存储在分组的 Key 字段中。这个查询与其他查询的不同之处在于返回值。这个查询返回的是分组,而分组中又包含了 Poster
项。很巧妙,不是吗?
关于 LINQ 的最后几句话
好了,这就是对 LINQ 的一个快速浏览。总而言之,我们审视了当今数据访问现状的相当悲惨的状况。然后我们看到了 LINQ 和 C# 3.0 中的新语言特性如何通过给我们一套一致的标准查询运算符来解决这些问题,我们可以用这些运算符来查询任何实现了 IEnumerable<T>
的集合。在本文中,我们只关注了内存中的数据集合,以避免大多数人在将 LINQ 与 DLinq 和 XLinq 混合使用时产生的混淆,但请放心,有一种方法可以使用 LINQ 访问关系型数据和 XML 数据。否则就没有多大意义了,不是吗?此外,因为 LINQ 只是一组遵循标准查询运算符命名约定的方法,所以任何人都可以实现他们自己的基于 LINQ 的集合来访问任何其他类型的数据。例如,WinFS 团队将使其产品支持 LINQ。
如果你和我一样对 LINQ 感到无比兴奋,并想阅读更多相关内容,我建议你去 LINQ 预览网站。在那里,你可以下载 LINQ 预览包,它会集成到 Visual Studio 2005 Beta 2 中以提供新的 LINQ 功能,你还可以阅读更多关于 DLinq 和 XLinq 以及新的 C# 3.0 规范的内容。