来看看LINQ 起始演变及其对C#技术设计影响
简单地说,LINQ 是支持以类型安全方式查询数据的一系列语言扩展;它在代号为“Orcas”的下一个版本 Visual Studio 中发布。待查询数据的形式可以是 XML(LINQ 到 XML)、数据库(启用 LINQ 的 ADO.NET,其中包括 LINQ 到 SQL、LINQ 到 Dataset 和 LINQ 到 Entities)和对象 (LINQ 到 Objects) 等。
让我们看一些代码。在即将发布的“Orcas”版 C# 中,LINQ 查询可能如下所示:
var overdrawnQuery = from account in db.Accounts where account.Balance < 0 select new { account.Name, account.Address }; |
从以上示例中立即可以看出该语法类似于 SQL。几年前,Anders Hejlsberg(C# 的首席设计师)和 Peter Golde 曾考虑扩展 C# 以更好地集成数据查询。Peter 时任 C# 编译器开发主管,当时正在研究扩展 C# 编译器的可能性,特别是支持可验证 SQL 之类特定于域的语言语法的加载项。另一方面,Anders 则在设想更深入、更特定级别的集成。他当时正在构思一组“序列运算符”,能在实现 IEnumerable 的任何集合以及实现 IQueryable 的远程类型查询上运行。最终,序列运算符的构思获得了大多数支持,并且 Anders 于 2004 年初向比尔·盖茨的 Thinkweek 递交了一份关于本构思的文件。反馈对此给予了充分肯定。在设计初期,简单查询的语法如下所示:
sequence locals = customers.where(ZipCode == 98112); |
假设上述示例是 C# 的理想查询语法。在没有任何语言扩展的情况下,该查询在 C# 2.0 中又会是什么样子?
IEnumerable locals = EnumerableExtensions.Where(customers, delegate(Customer c) { return c.ZipCode == 98112; }); |
Lambda 表达式
Lambda 表达式是一种语言功能,在许多方面类似于匿名方法。事实上,如果 lambda 表达式首先被引入语言,那么就不会有对匿名方法的需要了。这里的基本概念是可以将代码视为数据。在 C# 1.0 中,通常可以将字符串、整数、引用类型等传递给方法,以便方法对那些值进行操作。匿名方法和 lambda 表达式扩展了值的范围,以包含代码块。此概念常见于函数式编程中。
我们再借用以上示例,并用 lambda 表达式替换匿名方法:
IEnumerable locals = EnumerableExtensions.Where(customers, c => c.ZipCode == 91822); |
public static IEnumerable Where( IEnumerable items, Func predicate) |
与匿名方法一样,Lambda 表达式也支持变量捕获。例如,对于在 lambda 表达式主体内包含 lambda 表达式的方法,可以引用其参数或局部变量:
public IEnumerable LocalCusts( IEnumerable customers, int zipCode) { return EnumerableExtensions.Where(customers, c => c.ZipCode == zipCode); } |
return EnumerableExtensions.Where(customers, (Customer c) => { int zip = zipCode; return c.ZipCode == zip; }); |
IEnumerable locals = EnumerableExtensions.Where(customers, c => c.ZipCode == 91822); |
IEnumerable locals = EnumerableExtensions.Select( EnumerableExtensions.Where(customers, c => c.ZipCode == 91822), c => c.Name); |
sequence locals = customers.where(ZipCode == 98112).select(Name); |
扩展方法
结果证明,更好的语法将以被称为扩展方法的语言功能形式出现。扩展方法基本上属于可通过实例语法调用的静态方法。上述查询问题的根源是我们试图向 IEnumerable
假设我们转而将 Where 方法编写为扩展方法。那么,查询可重新编写为:
IEnumerable locals = customers.Where(c => c.ZipCode == 91822); |
public static IEnumerable Where( this IEnumerable items, Func predicate) |
扩展方法
显然,扩展 方法有助于简化我们的查询示例,但除此之外,这些方法是不是一种广泛有用的语言功能呢?事实证明扩展方法有多种用途。其中一个最常见的用途可能是提供共享接口实现。例如,假设您有以下接口:
interface IDog { // Barks for 2 seconds void Bark(); void Bark(int seconds); } |
interface IDog { void Bark(int seconds); } |
static class DogExtensions { // Barks for 2 seconds public static void Bark(this IDog dog) { dog.Bark(2); } } |
Close [x]
我们现在拥有了用于编写筛选子句的非常接近理想的语法,但“Orcas”版 C# 仅限于此吗?并不全然。让我们对示例稍作扩展,相对于整个客户对象,我们只投影出客户名称。如我前面所述,理想的语法应采用如下形式:
sequence locals = customers.where(ZipCode == 98112).select(Name); |
IEnumerable locals = customers.Where(c => c.ZipCode == 91822).Select(c => c.Name); |
当投影只是单一字段时,该方法确实很有效。但是,假设我们不仅要返回客户的名称,还要返回客户的地址。理想的语法则应如下所示:
locals = customers.where(ZipCode == 98112).select(Name, Address); |
如果我们想继续使用我们现有的语法来返回名称和地址,我们很快便会面临问题,即不存在仅包含 Name 和 Address 的类型。虽然我们仍然可以编写此查询,但是必须引入该类型:
class CustomerTuple { public string Name; public string Address; public CustomerTuple(string name, string address) { this.Name = name; this.Address = address; } } |
IEnumerable locals = customers.Where(c => c.ZipCode == 91822) .Select(c => new CustomerTuple(c.Name, c.Address)); |
这正是匿名类型要解决的问题。此功能主要允许在无需指定名称的情况下创建结构化类型。如果我们使用匿名类型重新编写上述查询,其代码如下所示:
locals = customers.Where(c => c.ZipCode == 91822) .Select(c => new { c.Name, c.Address }); |
class { public string Name; public string Address; } |
locals = customers.Where(c => c.ZipCode == 91822) .Select(c => new { FullName = c.FirstName + “ “ + c.LastName, HomeAddress = c.Address }); |
这样我们又向理想世界前进了一步,但仍存在一个问题。您将发现,我在任何使用匿名类型的地方都策略性地省略了局部变量的类型。显然我们不能声明匿名类型的名称,那我们如何使用它们?
隐式类型化部变量
还有另一种语言功能被称为隐式类型化局部变量(或简称为 var),它负责指示编译器推断局部变量的类型。例如:
var integer = 1; |
var integer = 1; integer = “hello”; |
在上述查询示例中,我们现在可以编写完整的赋值,如下所示:
var locals = customers .Where(c => c.ZipCode == 91822) .Select(c => new { FullName = c.FirstName + “ “ + c.LastName, HomeAddress = c.Address }); |
隐式类型化局部变量只是:方法内部的局部变量。它们无法超出方法、属性、索引器或其他块的边界,因为该类型无法显式声明,而且“var”对于字段或参数类型而言是非法的。
事实证明,隐式类型化局部变量在查询的环境之外非常便利。例如,它有助于简化复杂的通用实例化:
var customerListLookup = new Dictionary>(); |
有趣的是,我们发现,随着越来越多的人使用过此语法,经常会出现允许投影超越方法边界的需求。如我们以前所看到的,这是可能的,只要从 Select 内部调用对象的构造函数来构建对象即可。但是,如果没有用来准确接受您需要设置的值的构造函数,会发生什么呢?
对象初始值
为解决这一问题,即将发布的“Orcas”版本提供了一种被称为对象初始值的 C# 语言功能。对象初始值主要允许在单一表达式中为多个属性或字段赋值。例如,创建对象的常见模式是:
Customer customer = new Customer(); customer.Name = “Roger”; customer.Address = “1 Wilco Way”; |
Customer customer = new Customer() { Name = “Roger”, Address = “1 Wilco Way” }; |
var locals = customers .Where(c => c.ZipCode == 91822) .Select(c => new CustomerTuple { Name = c.Name, Address = c.Address }); |
我们现在已经拥有在 C# 中创建查询的简洁语法。尽管如此,我们还有一种可扩展途径,可通过扩展方法以及一组本身非常有用的语言功能来添加新的运算符(Distinct、OrderBy、Sum 等)。
语言设计团队现在有了数种可赖以获得反馈的原型。因此,我们与许多富于 C# 和 SQL 经验的参与者组织了一项可用性研究。几乎所有反馈都是肯定的,但明显疏忽了某些东西。具体而言,开发人员难以应用他们的 SQL 知识,因为我们认为理想的语法与他们擅长领域的专门技术并不很符合。
查询表达式
于是,语言设计团队设计了一种与 SQL 更为相近的语法,称为查询表达式。例如,针对我们的示例的查询表达式可如下所示:
var locals = from c in customers where c.ZipCode == 91822 select new { FullName = c.FirstName + “ “ + c.LastName, HomeAddress = c.Address }; |
var locals = customers .Where(c => c.ZipCode == 91822) .Select(c => new { FullName = c.FirstName + “ “ + c.LastName, HomeAddress = c.Address }); |
var locals = (from c in customers where c.ZipCode == 91822 select new { FullName = c.FirstName + “ “ + c.LastName, HomeAddress = c.Address}) .Count(); |
通过该种方法,我们已经设法在结束时达到了开始时的IC交易网目标(我对这一点始终觉得非常满意)。下一版本的 C# 的语法历经数年时间的发展,尝试了许多新的语言功能,才最终到达近乎于 2004 年冬提议的原始语法的境界。查询表达式的加入以 C# 即将发布的版本的其他语言功能为基础,并促使许多查询情况更便于具有 SQL 背景的开发人员阅读和理解。