您当前的位置:厦门人家v2008网站技术dotNET技术 → 文章内容

LINQ的演变及其对 C#设计的影响

  • 作者:未知    来源:网络整理    发布时间:2007-9-4 16:36:00
  • 字体大小:

  简单地说,LINQ 是支持以类型安全方式查询数据的一系列语言扩展;它将在代号为“Orcas”的下一个版本 Visual Studio 中发布。待查询数据的形式可以是 XML(LINQ 到 XML)、数据库(启用 LINQ 的 ADO.NET,其中包括 LINQ 到 SQL、LINQ 到 Dataset 和 LINQ 到 Entities)和对象 (LINQ 到 Objects) 等。LINQ 体系结构如图 1 所示。

  图 1 LINQ 体系结构 (单击该图像获得较小视图)


  图 1 LINQ 体系结构 (单击该图像获得较大视图)

  让我们看一些代码。在即将发布的“Orcas”版 C# 中,LINQ 查询可能如下所示:

  var overdrawnQuery = from account in db.Accounts
  where account.Balance < 0
  select new { account.Name, account.Address };

  当使用 foreach 遍历此查询的结果时,返回的每个元素都将包含一个余额小于 0 的帐户的名称和地址。

  从以上示例中立即可以看出该语法类似于 SQL。几年前,Anders Hejlsberg(C# 的首席设计师)和 Peter Golde 曾考虑扩展 C# 以更好地集成数据查询。Peter 时任 C# 编译器开发主管,当时正在研究扩展 C# 编译器的可能性,特别是支持可验证 SQL 之类特定于域的语言语法的加载项。另一方面,Anders 则在设想更深入、更特定级别的集成。他当时正在构思一组“序列运算符”,能在实现 IEnumerable 的任何集合以及实现 IQueryable 的远程类型查询上运行。最终,序列运算符的构思获得了大多数支持,并且 Anders 于 2004 年初向比尔·盖茨的 Thinkweek 递交了一份关于本构思的文件。反馈对此给予了充分肯定。在设计初期,简单查询的语法如下所示:

  sequence locals = customers.where(ZipCode == 98112);

  在此例中,Sequence 是 IEnumerable 的别名;“where”一词是编译器能理解的一种特殊运算符。Where 运算符的实现是一种接受 predicate 委托(即 bool Pred(T item) 形式的委托)的普通 C# 静态方法。本构思的目的是让编辑器具备与运算符有关的特殊知识。这样将允许编译器正确调用静态方法并创建代码,将委托与表达式联系起来。

  假设上述示例是 C# 的理想查询语法。在没有任何语言扩展的情况下,该查询在 C# 2.0 中又会是什么样子?

IEnumerable locals = EnumerableExtensions.Where(customers,delegate(Customer c)
{
 return c.ZipCode == 98112;
});

  这个代码惊人地冗长,而且更糟糕的是,需要非常仔细地研究才能找到相关的筛选器 (ZipCode == 98112)。这只是一个简单的例子;试想一下,如果使用数个筛选器、投影等,要读懂代码该有多难。冗长的根源在于匿名方法所要求的语法。在理想的查询中,除了要计算的表达式,表达式不会提出任何要求。随后,编译器将尝试推断上下文;例如,ZipCode 实际上引用了 Customer 上定义的 ZipCode。如何解决这一问题?将特定运算符的知识硬编码到语言中并不能令语言设计团队满意,因此他们开始为匿名方法寻求替代语法。他们要求该语法应极其简练,但又不必比匿名方法当前所需的编译器要求更多的知识。最终,他们发明了 lambda 表达式。

  Lambda 表达式

  Lambda 表达式是一种语言功能,在许多方面类似于匿名方法。事实上,如果 lambda 表达式首先被引入语言,那么就不会有对匿名方法的需要了。这里的基本概念是可以将代码视为数据。在 C# 1.0 中,通常可以将字符串、整数、引用类型等传递给方法,以便方法对那些值进行操作。匿名方法和 lambda 表达式扩展了值的范围,以包含代码块。此概念常见于函数式编程中。

  我们再借用以上示例,并用 lambda 表达式替换匿名方法:

IEnumerable locals =EnumerableExtensions.Where(customers, c => c.ZipCode == 91822);

  有几个需要注意的地方。对于初学者而言,lambda 表达式简明扼要的原因有很多。首先,没有使用委托关键字来引入构造。取而代之的是一个新的运算符 =>,通知编译器这不是正则表达式。其次,Customer 类型是从使用中推断出来的。在此例中,Where 方法的签名如下所示:

public static IEnumerable Where(IEnumerable items, Func<T, bool> predicate)

  编译器能够推断“c”是指客户,因为 Where 方法的第一个参数是 IEnumerable,因此 T 事实上必须是 Customer。利用这种知识,编译器还可验证 Customer 具有一个 ZipCode 成员。最后,没有指定的返回关键字。在语法形式中,返回成员被省略,但这只是为了语法便利。表达式的结果仍将视为返回值。

  与匿名方法一样,Lambda 表达式也支持变量捕获。例如,对于在 lambda 表达式主体内包含 lambda 表达式的方法,可以引用其参数或局部变量:

public IEnumerable LocalCusts(IEnumerable customers, int zipCode)
{
 return EnumerableExtensions.Where(customers,c => c.ZipCode == zipCode);
}

  最后,Lambda 表达式支持更冗长的语法,允许您显式指定类型,以及执行多条语句。例如:

return EnumerableExtensions.Where(customers,(Customer c) => { int zip = zipCode; return c.ZipCode == zip; });

  好消息是,我们向原始文章中提议的理想语法迈进了一大步,并且我们能够利用一个通常能在查询运算符以外发挥作用的语言功能来实现这一目标。让我们再次看一下我们目前所处的阶段:

IEnumerable locals =EnumerableExtensions.Where(customers, c => c.ZipCode == 91822);

  这里存在一个明显的问题。客户目前必须了解此 EnumerableExtensions 类,而不是考虑可在 Customer 上执行的操作。另外,在多个运算符的情况下,使用者必须逆转其思维以编写正确的语法。例如:

IEnumerable locals =EnumerableExtensions.Select(
  EnumerableExtensions.Where(customers, c => c.ZipCode == 91822),c => c.Name);

  请注意,Select 属于外部方法,尽管它是在 Where 方法结果的基础上运行的。理想的语法应该更类似以下代码:

sequence locals =customers.where(ZipCode == 98112).select(Name);

  因此,是否可利用另一种语言功能来进一步接近实现理想语法呢?

  扩展方法

  结果证明,更好的语法将以被称为扩展方法的语言功能形式出现。扩展方法基本上属于可通过实例语法调用的静态方法。上述查询问题的根源是我们试图向 IEnumerable 添加方法。但如果我们要添加运算符,如 Where、Select 等,则所有现有和未来的实现器都必须实现那些方法。尽管那些实现绝大多数都是相同的。在 C# 中共享“接口实现”的唯一方法是使用静态方法,这是我们处理以前使用的 EnumerableExtensions 类的一个成功方法。

  假设我们转而将 Where 方法编写为扩展方法。那么,查询可重新编写为:

IEnumerable locals =customers.Where(c => c.ZipCode == 91822);

  对于此简单查询,该语法近乎完美。但将 Where 方法编写为扩展方法的真正含义是什么呢?其实非常简单。基本上,因为静态方法的签名发生更改,因此“this”修饰符就被添加到第一个参数:

public static IEnumerable Where(this IEnumerable items, Func<T, bool> predicate)

  此外,必须在静态类中声明该方法。静态类是一种只能包含静态成员,并在类声明中用静态修饰符表示的类。这就它的全部含义。此声明指示编译器允许在任何实现 IEnumerable 的类型上用与实例方法相同的语法调用 Where。但是,必须能够从当前作用域访问 Where 方法。当包含类型处于作用域内时,方法也在作用域内。因此,可以通过 Using 指令将扩展方法引入作用域。(有关详细信息,请参见侧栏上的“扩展方法”。)

  扩展方法

  显然,扩展 方法有助于简化我们的查询示例,但除此之外,这些方法是不是一种广泛有用的语言功能呢?事实证明扩展方法有多种用途。其中一个最常见的用途可能是提供共享接口实现。例如,假设您有以下接口:

interface IDog
{
 // Barks for 2 seconds
 void Bark();
 void Bark(int seconds);
}

  此接口要求每个实现器都应编写适用于两种重载的实现。有了“Orcas”版 C#,接口变得很简单:

interface IDog
{
 void Bark(int seconds);
}

  扩展方法可添加到另一个类:

static class DogExtensions
{
 // Barks for 2 seconds
 public static void Bark(this IDog dog)
 {
  dog.Bark(2);
 }
}

  接口实现器现在只需实现单一方法,但接口客户端却可以自由调用任一重载。
厦门市公安局公共信息网络安全监察处