65.9K
CodeProject 正在变化。 阅读更多。
Home

理解 LINQ (C#)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (89投票s)

2007年6月12日

CPOL

6分钟阅读

viewsIcon

575426

downloadIcon

8445

一篇描述 LINQ 新语言特性的文章。

Figure 1

Figure 2

目录

引言

本文讨论的是LINQ,我认为它是Orcas中最令人兴奋的特性之一。LINQ将查询的概念提升为.NET中的一等编程概念。要查询的数据可以采取XML (LINQ to XML)、数据库 (LINQ启用的ADO.NET: LINQ to SQL, LINQ to Dataset 和 LINQ to Entities) 和对象 (LINQ to Objects) 的形式。LINQ也是高度可扩展的,允许您构建自定义的LINQ启用的数据提供程序 (例如:LINQ to Amazon, LINQ to NHibernate, LINQ to LDAP)。

LINQ Architecture (MSDN Magazine - June 2007)

我将讨论C# 3.0中引入的一些新的语言特性和改进,正是这些特性赋予了LINQ全部强大的功能,使其能够编写出类似这样的代码:

var result = from c in Customers
             where c.City == Boston"
             orderby c.LastName descending
             select new { c.FirstName, c.LastName, c.Address };

请记住,如果您想尝试LINQ或自己动手尝试示例,您需要下载Visual Studio Orcas Beta 1
如果您不想下载Visual Studio,可以查看LINQ Preview (May 2006 CTP),它可以在Visual Studio 2005之上运行 (Beta 1在LINQ如何工作的方面与May CTP有一些变化)。

新的语言特性

I. 自动属性

public class Point {
    private int _x, _y;
    public int X {
        get { return _x; }
        set { _x = value; }
    }
    public int Y {
        get { return _y; }
        set { _y = value; }
    }
}

上面的代码只是定义了一个具有基本属性的类。现在,使用Orcas中的新C#编译器,我们可以使用自动属性编写一个更简洁的版本,它会自动生成具有get/set操作的private字段。

public class Point {
    public int X { get; set; }
    public int Y { get; set; }
}

上面的代码甚至更具可读性,也不那么冗长。
请注意,此特性与LINQ无关。我只是觉得将其列入其他新的语言特性中比较合适。

II. 局部变量类型推断

通过此特性,声明的局部变量的类型将从用于初始化变量的表达式中推断出来。这是通过var关键字实现的 (熟悉脚本语言的人可能会对其感到熟悉,但实际上它有很大的不同)。它允许我们编写以下代码:

var num = 50;
var str = "simple string";
var obj = new myType();
var numbers = new int[] {1,2,3};
var dic = new Dictionary<int,myType>();

编译器将生成与我们编译时相同的IL:

int num = 50;
string str = "simple string";
myType obj = new myType();
int[] numbers = new int[] {1,2,3};
Dictionary<int,myType> dic = new Dictionary<int,myType>();

请注意,这里没有未类型化的变量引用或晚期绑定,而是编译器从赋值的右侧推断并声明变量的类型。因此,var关键字生成了一个强类型变量引用。

III. 对象初始化器和集合初始化器

让我们使用前面定义的同一个Point类,并假设我们要定义该类的一个实例。我们将不得不创建对象并开始设置其属性,代码将如下所示:

Point p = new Point();
p.X = 0;
p.Y = 0;

可以使用对象初始化器重写此代码,并合并为:

Point p = new Point() { X = 0, Y = 0 };

此特性也可以用于集合。看看这个例子:

List<Point> points = new List<Point> {
    new Point { X = 2,  Y = 5 },
    new Point { X = 1, Y = -10 },
    new Point { X = 3, Y = 0 }
};

请注意,编译器将生成一个与上面代码等效的长代码版本。它会调用Add()方法一次添加一个元素到集合中。

IV. 匿名类型

此语言特性使我们能够在不显式定义该类型的类声明的情况下定义内联类型。换句话说,假设我们想使用一个Point对象而不定义Point类 (它是匿名的)。我们将使用前面讨论的相同的对象初始化器语法,但省略类型名称:

var p = new {X = 0, Y = 2};

在Orcas内部,您将获得完整的IntelliSense支持。因此,当您使用变量p时,您将获得该匿名类型具有的属性列表。

V. Lambda表达式

C# 2.0引入了匿名方法,它允许在需要委托值的地方“内联”编写代码块。虽然匿名方法提供了函数式编程语言的强大功能,但其语法相当冗长。Lambda表达式为编写匿名方法提供了一种更简洁、更函数式的语法。Lambda表达式的写法是参数列表 (可以是隐式类型的),后跟=>标记,后跟一个表达式或一个语句块。

例如,让我们定义一个delegate类型MyDeleg为:

delegate R MyDeleg<A,R>(A arg);

然后我们可以使用匿名方法编写:

MyDeleg<int,bool> IsPositive = delegate(int num) {
                                   return num > 0;
                               };

或者我们可以使用新的Lambda表达式来编写:

MyDeleg<int,bool> IsPositive = num => num > 0;

VI. 扩展方法

扩展方法使得在不继承现有类型和构造类型的情况下,或者不重新编译原始类型,就能用附加方法来扩展它们。因此,与其为对象编写辅助方法,不如让它们成为对象本身的一部分。

例如,假设我们想检查一个string是否是有效的电子邮件地址。我们将通过编写一个接受string作为参数并返回true/false的函数来实现。使用扩展方法,我们可以这样做:

public static class MyExtensions {
    public static bool IsValidEmailAddress(this string s) {
        Regex regex = new Regex( @"^[w-.]+@([w-]+.)+[w-]{2,4}$" );
        return regex.IsMatch(s);
    }
}

我们定义了一个static类,其中包含一个包含扩展方法的static方法。请注意,上面的static方法在第一个参数string类型的参数之前有一个this关键字。这告诉编译器,这个特定的扩展方法应该被添加到string类型的对象中。然后我们可以像调用成员函数一样从string调用它:

using MyExtensions;

string email = Request.QueryString["email"];
if ( email.IsValidEmailAddress() ) {
    // ...
}

值得一提的是,LINQ语法利用了内置的扩展方法 (例如:where(), orderby(), select(), sum(), average() 等等),这些方法位于Orcas中新的System.Linq namespace中,并定义了标准的查询运算符,这些运算符可以用于关系数据库、XML以及任何实现IEnumerable<T>的.NET对象。

VII. 查询语法

查询表达式提供了一种语言集成查询语法,类似于SQL和XQuery等关系型和分层查询语言。它是使用LINQ查询运算符 (即from...where...select) 编写查询的简写方式。Visual Studio为查询语法提供了完整的IntelliSense和编译时检查支持。
当C#编译器遇到查询语法表达式时,它实际上将其转换为显式的方法调用代码,这些代码使用扩展方法和Lambda表达式。

为了解释这一点,我举一个例子:

var result = from c in Customers
             where c.City.StartsWith("B")
             orderby c.LastName
             select new { c.FirstName, c.LastName, c.Address };

上面的代码等同于以下内容:

var result = Customers.Where( c => c.City.StartsWith("B") )
                      .OrderBy( c => c.LastName  )
                      .Select( c => new { c.FirstName, c.LastName, c.Address } );

使用查询语法的优点是代码更易于阅读。
另外请注意,查询表达式以from子句开头,并以selectgroup子句结尾。

最终注释

我们在C# v3.0中看到的大多数语言特性 (如变量类型推断、对象初始化器、匿名类型和Lambda表达式) 都只是“编译器技巧”/“语法糖”,这意味着编译器生成的IL与长代码版本相同,因此它们独立于框架 (NetFX) 和运行时 (CLR)。然而,它们确实需要一些框架支持,特别是“绿色位” (.NET Framework v3.5) 的System.Core.dll程序集。这就是为什么扩展方法 (实际上在编译时工作 (语法糖)) 仍然依赖于System.Core.dll中引入的System.Runtime.CompilerServices.ExtensionAttribute
另一方面,查询表达式语法只是映射到存在于System.LinqSystem.Data.LinqSystem.Xml.Linq namespace中的扩展方法实现。

参考文献与资源

历史

  • 2007年6月12日:初始帖子
© . All rights reserved.