C# 3.0(C# Orcas——魔鬼)在C# 2.0的基础上引入了很多语言扩展,用以支持高级别的函数式风格类库的创建和使用。这些扩展使得结构性API构造具有与其他领域(如关系数据库和XML)中查询语言同等的表达能力。这些扩展包括:
在一个具有隐式类型的局部变量声明(implicitly typed local variable declaration)中,被声明的局部变量的类型是通过初始化该变量的表达式推断出来的。当使用局部变量声指示符符var来代替类型,并且当前作用域内没有名为var的类型时,这个声明便成为一个具有隐式类型的局部变量声明。例如:
var i = 5; |
上面这些具有隐式类型的局部变量声明和下面这些具有显式类型的声明完全一致:
int i = 5; |
一个具有隐式类型的局部变量声明中的局部变量声明器(Declarator)必须遵循下列约束:
下面是不正确的具有隐式类型的局部变量声明示例:
var x; // 错误,没有用来推断类型的初始化器
var y = {1, 2, 3}; // 错误,不允许使用集合初始化器
var z = null; // 错误,不允许出现空类型
出于向下兼容的原因,当一个局部变量声明指示符以var作为类型,但当前作用域中有一个名为var的类型时,这个声明使用的是该类型;然而,(编译器)会针对这种模糊的语义给出一个警告。不过由于var违反了类型名字首字母必须大写这条约定,这种情况应该不大会出现。
for语句的for-initializer和using语句的resource-acquisition可以是一个具有隐式类型的局部变量声明。同样,foreach语句中的迭代变量也可以被声明为具有隐式类型的局部变量,在这种情况下,迭代变量的类型通过待遍历的集合的元素类型来推断。
int[] numbers = {1, 3, 5, 7, 9}; |
在上面的例子中n的类型被推断为int——numbers的元素类型。
扩展方法(Extension Methods)是一种静态方法,可以通过实例方法的语法进行调用。从最终效果上看,扩展方法使得扩展一个现有类型和构造一个具有附加方法的类型变成了现实。
注意 扩展方法很难发觉,并且比起实例方法在功能性上有很大限制。出于这些原因,我们建议保守地使用扩展方法,仅在实例方法不大可行或根本不可行的时候才使用。 |
扩展成员的其他类型,如属性、事件和运算符都在考虑之中,但目前并未支持。
2.1 声明扩展方法
扩展方法通过在方法的第一个参数上指定关键字this作为一个修饰符来声明。扩展方法只能声明在静态类中。下面的示例是一个声明了两个扩展方法的静态类:
namespace Acme.Utilities { public static class Extensions { public static int ToInt32(this string s) { return Int32.Parse(s); }
public static T[] Slice<T>(this T[] source, int index, int count) { if(index < 0 || count < 0 || source.Length - index < count) throw new ArugmentException(); T[] result = new T[count]; Array.Copy(source, index, result, 0, count); return result; } } } |
扩展方法和正常的静态方法具有完全相同的功能。另外,一旦导入了扩展方法,就可以用调用实例方法的语法来调用扩展方法。
2.2 导入扩展方法
扩展方法使用using-namespace-directives导入。除了导入一个命名空间中的类型以外,一个using-namespace-directive还可以导入一个命名空间中所有的静态类中所有的扩展方法。最后,导入的扩展方法表现为其第一个参数的类型的附加方法,并且其优先级比一般的实例方法低。例如,当使用using-namespace-directive导入了上面例子中的Acme.Utilities命名空间时:
using Acme.Utilities;
就可以使用调用实例方法的语法来调用静态类Extensions中的扩展方法了:
string s = "1234"; int i = s.ToInt32(); // 和Extensions.ToInt32(s)一样 int[] digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int[] a = digits.Slice(4, 3); // 和Extensions.Slice(digits, 4, 3)一样 |
2.3 扩展方法的调用
下面描述了扩展方法调用的详细规则。在下面这些形式的方法调用中:
expr . identifier ( ) expr . identifier ( args ) expr . identifier < typeargs > ( ) expr . identifier < typeargs > ( args ) |
如果按照正常的过程没有发现可用的实例方法(确切地说,当待调用的候选方法集合为空时),就会尝试构造一个扩展方法调用。这些方法调用首先被重写为下面相应的形式:
identifier ( expr ) identifier ( expr , args ) identifier < typeargs > ( expr ) identifier < typeargs > ( expr , args ) |
然后将重写后的形式作为一个静态方法调用进行处理,identifier按照下列顺序进行解析:首先是命名空间生命中最接近的声明,然后是每一个接近的命名空间,最后是包含这些代码的编译单元,其间不断尝试重写过的方法调用,这些方法来自一个方法组,该组由using-namespace-directives导入的命名空间中所有可见的identifier所提供的可见的扩展方法构成。第一个产生了非空候选方法集合的方法组是对冲洗过的方法调用的一个选择。如果所有的尝试都产生了空的候选方法集合,就会出现一个编译期错误。
上述规则意味着实例方法的优先级胜于扩展方法,并且最后引入的命名空间中的扩展方法的优先级胜于较先引入的命名空间中的扩展方法。例如:
using N1; namespace N1 { public static class E { public static void F(this object obj, int i) { } public static void F(this object obj, string s) { } } }
class A { }
class B { public void F(int i) { } }
class C { public void F(object obj) { } }
class X { static void Test(A a, B b, C c) { a.F(1); // E.F(object, int) a.F("Hello"); // E.F(object, string)
b.F(1); // B.F(int) b.F("Hello"); // E.F(object, string)
c.F(1); // C.F(object) c.F("Hello"); // C.F(object) } } |
在这个例子中,B的方法优先于第一个扩展方法,而C的方法优先于所有两个扩展方法。
C# 2.0中引入了匿名方法,允许在期望出现委托的时候以“内联(in-line)”的代码替代之。尽管匿名方法提供了函数式编程语言中的很多表达能力,但匿名方法的语法实在是太罗嗦了,并且很不自然。拉姆达表达式(Lambda expressions)为书写匿名方法提供了一种更加简单、更加函数化的语法。
拉姆达表达式的书写方式是一个参数列表后跟=>记号,然后跟一个表达式或一个语句块。
expression: non-assignment-expression: lambda-expression: lambda-parameter-list: explicitly-typed-lambda-parameter-list: explicitly-typed-lambda-parameter: implicitly-typed-lambda-parameter-list: implicitly-typed-lambda-parameter: lambda-expression-body: |
拉姆达表达式的参数可以具有显式的或隐式的类型。在一个具有显式类型的参数列表中,每个参数的类型都是显式声明的。在一个具有隐式类型的参数列表中,参数的类型是从拉姆达表达式出现的上下文中推断出来的——具体来说,是当拉姆达表达式被转换为一个兼容的委托类型时,该委托类型提供了参数的类型。
当拉姆达表达式只有一个具有隐式类型的参数时,参数列表中的括号可以省略。换句话说,下面这种形式的拉姆达表达式:
( param ) => expr 可以简写为: param => expr 下面给出的是拉姆达表达式的一些例子: x => x + 1 // 隐式类型,以表达式作为拉姆达表达式体 x => { return x + 1; } // 显式类型,以语句块作为拉姆达表达式体 (int x) => x + 1 // 显式类型,以表达式作为拉姆达表达式体 (int x) => { return x + 1; } // 显式类型,以语句块作为拉姆达表达式体 (x, y) => x * y // 多个参数 () => Console.WriteLine() // 没有参数 |
通常,C# 2.0规范中提到的匿名方法规范同样适用于拉姆达表达式。拉姆达表达式是匿名方法在功能行上的超集,提供了下列附加的功能:
注意 PDC 2005技术预览版编译器并不支持以一个语句块作为表达式体的拉姆达表达式。当必需一个语句块时,请使用C# 2.0中的匿名方法语法。 |
3.1 拉姆达表达式转换
和匿名方法表达式类似,拉姆达表达式可以归类为一种拥有特定转换规则的值。这种值没有类型,但可以被隐式地转换为一个兼容的委托类型。特别地,当满足下列条件时,委托类型D兼容于拉姆达表达式L:
后面的例子将使用一个范型委托Func<A, R>,表示一个函数,它具有一个类型为A的参数,返回值类型为R:
delegate R Func<A, R>(A arg);
在下面的赋值中:
Func<int, int> f1 = x => x + 1; // Ok Func<int, double> f2 = x => x + 1; // Ok Func<double, int> f3 = x => x + 1; // Error |
每个拉姆达表达式的参数和返回值类型通过将拉姆达达表达式赋给的变量的类型来检测。第一个赋值将拉姆达表达式成功地转换为了委托类型Func<int, int>,因为x的类型是int,x + 1是一个有效的表达式,并且可以被隐式地转换为int。同样,第二个赋值成功地将拉姆达表达式转换为了委托类型Func<int, double>,因为x + 1的结果(类型为int)可以被隐式地转换为double类型。然而,第三个赋值将会产生一个编译期错误,因为x给定的类型是double,x + 1的结果(类型为double)不能被隐式地转换为int。
3.2 类型推断
当在没有指定类型参数的情况下调用一个范型方法时,一个类型推断过程回去尝试为该调用推断类型参数。被作为参数传递给范型方法的拉姆达表达式也会参与这个类型推断过程。
最先发生的类型推断独立于所有参数。在这个初始阶段,不会从作为参数的拉姆达表达式推断出任何东西。然而,在初始阶段之后,将通过一个迭代过程从拉姆达表达式进行推断。特别地,当下列条件之一为真时将会完成推断:
对于每一个这样的参数,都是通过关联P的返回值类型和从L推断出的返回值类型来从其上进行推断的,并且新的推断将被添加到累积的推断集合中。这个过程一直重复,直到无法进行更多的推断为止。
在类型推断和重载抉择中,拉姆达表达式L的“推断出来的返回值类型”通过以下步骤进行检测:
作为包含了拉姆达表达式的类型推断的例子,请考虑System.Query.Sequence类中声明的Select扩展方法:
namespace System.Query { public static class Sequence { public static IEnumerable<S> Select<T, S>( this IEnumerable<T> source, Func<T, S> selector) { foreach(T element in source) yield return selector(element); } } } |
假设使用using语句导入了System.Query命名空间,并且定义了一个Customer类,具有一个类型为string的属性Name,Select方法可以用于从一个Customer列表中选择名字:
List<Customer> customers = GetCustomerList();
IEnumerable<string> names = customers.Select(c => c.Name);
对扩展方法Select的调用将被处理为一个静态方法调用:
IEnumerable<string> names = Sequence.Select(customers, c => c.Name);
由于没有显式地指定类型参数,将通过类型推断来推导类型参数。首先,customers参数被关联到source参数,T被推断为Customer。然后运用上面提到的拉姆达表达式类型推断过程,C的类型是Customer,表达式c.Name将被关联到selector参数的返回值类型,因此推断S是string。因此,这个调用等价于:
Sequence.Select<Customer, string>(customers, (Customer c) => c.Name)
并且其返回值类型为IEnumerable<string>。
下面的例子演示了拉姆达表达式的类型推断是如何允许类型信息在一个范型方法调用的参数之间“流动”的。对于给定的方法:
static Z F<X, Y, Z>(X value, Func<X, Y> f1, Func<Y, Z> f2) {
return f2(f1(value));
}
下面这个调用:
double seconds = F("1:15:30", s => TimeSpan.Parse(s), t => TotalSeconds);
的类型推断过程是这样的:首先,参数"1:15:30"被关联到value参数,推断X为string。然后,第一个拉姆达表达式的参数s具有推断出来的类型string,表达式TimeSpan.Parse(s)被关联到f1的返回值类型,推断Y是System.TimeSpan。最后,第二个拉姆达表达式的参数t具有推断出来的类型System.TimeSpan,并且表达式t.TotalSeconds被关联到f2的返回值类型,推断Z为double。因此这个调用的结果类型是double。
3.3 重载抉择
参数列表中的拉姆达表达式将影响到特定情形下的重载抉择(也称重载分析,重载解析等,即从几个重载方法中选择最合适的方法进行调用的过程,译者注)。
下面是新添加的规则:对于拉姆达表达式L,且其具有推断出来的返回值类型,当委托类型D1和委托类型D2具有完全相同的参数列表,并且将L的推断出来的返回值类型隐式转换为D1的返回值类型要优于将L的推断出来的返回值类型隐式转换为D2的返回值类型时,称L到D1的隐式转换优于L到D2的隐式转换。如果这些条件都不为真,则两个转换都不是最优的。
下面的例子讲解了这一规则。
class ItemList<T> : List<T> { public int Sum<T>(Func<T, int> selector) { int sum = 0; foreach(T item in this) sum += selector(item); return sum; }
public double Sum<T>(Func<T, double> selector) { double sum = 0; foreach(T item in this) sum += selector(item); return sum; } } |
ItemList<T>有两个Sum方法。每个都带有一个selector参数,用于从列表项目中依次选取值进行求和。选择的值或者是int或者是double,结果也相应的是int或double。
可以使用Sum方法来根据一份产品明细表对一个订单进行求和:
class Detail { public int UnitCount; public double UnitPrice; ... }
void ComputeSums() { ItemList<Detail> orderDetails = GetOrderDetails(...); int totalUnits = orderDetails.Sum(d => d.UnitCount); double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount); ... } |
在对orderDetails.Sum的第一个调用中,两个Sum方法都是可以的,因为拉姆达表达式d => d.UnitCount与Func<Detail, int>和Func<Detail, double>都兼容。然而,重载抉择选用了第一个Sum方法,因为转换到Func<Detail, int>要优于转换到Func<Detail, double>。
在对orderDetails.Sum的第二个调用中,只有第二个Sum方法是可用的,因为拉姆达表达式d => d.UnitPrice * d.UnitCount产生的值的类型是double。因此重载抉择选用第二个Sum方法进行调用。
一个对象创建表达式可以包含一个对象或集合初始化器,用于初始化新创建的对象的成员或新创建的集合的元素。
object-creation-expression: object-or-collection-initializer: |
一个对象创建表达式可以省略构造器参数列表,并将其连同圆括号一起替换为一个对象或集合初始化器。省略构造器参数列表并将其连同圆括号一起替换为一个对象或集合初始化器等价于指定一个空的参数列表。
在执行一个带有对象或集合初始化器的对象创建表达式时,首先调用实例构造器,然后执行对象或集合初始化器指定的成员或元素初始化。
对象或集合初始化器不能引用正在初始化的对象实例。
4.1 对象初始化器
对象初始化器指定了对象的一个或多个域或属性的值。
object-initializer: member-initializer-list: member-initializer: initializer-value: |
对象初始化器由一系列的成员初始化器构成,包围在{和}记号中,并用逗号进行分隔。每个成员初始化器以对象的一个可访问的域或属性的名字开始,后跟一个等号,之后是一个表达式或一个对象或集合初始化器。如果对象初始化其中包括了对同一个域或属性的多于一个的成员初始化器,将会发生错误。
在等号后面指定了表达式的成员初始化器的处理与域和属性的赋值一致。
在等号后面指定了对象初始化器的成员初始化器也是对一个嵌套对象的初始化。与为域或属性赋一个新值不同,对象初始化器中的赋值被视为对域或属性的成员进行赋值。一个具有值类型的属性不能通过这种构造来进行初始化。
在等号后面指定了集合初始化器的成员初始化器也是对一个嵌套集合的初始化。与为域或属性赋一个新的集合不同,初始化器中给定的元素将被添加到域或属性所引用的集合中。该域或属性必须是一个满足下一节所指定的需求的集合类型。
下面的类表是一个具有两个坐标值的点:
public class Point { int x, y;
public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } } |
Point的一个实例可以像下面这样创建和初始化:
var a = new Point { X = 0, Y = 1 }; |
其等价于:
var a = new Point(); a.X = 0; a.Y = 1; |
下面的类表是一个具有两个点的矩形:
public class Rectangle { Point p1, p2;
public Point P1 { get { return p1; } set { p1 = value; } } public Point P2 { get { return p2; } set { p2 = value; } } } |
可以像下面这样创建和初始化一个Rectangle:
var r = new Rectangle { P1 = new Point { X = 0, Y = 1 }, P2 = new Point { X = 2, Y = 3 } }; |
其等价于:
var r = new Rectangle(); var __p1 = new Point(); __p1.X = 0; __p1.Y = 1; r.P1 = __p1; var __p2 = new Point(); __p2.X = 2; __p2.Y = 3; r.P2 = __p2; |
其中的__p1和__p2是临时变量,在其他地方不可见也不可访问。
如果Rectangle的构造器分配了两个嵌套的Point实例:
public class Rectangle { Point p1 = new Point(); Point p2 = new Point();
public Point P1 { get { return p1; } } public Point P2 { get { return p2; } } } |
下面的构造可以用来初始化内嵌的Point实例,而不是为其赋以新值:
var r = new Rectangle { P1 = { X = 0, Y = 1 }, P2 = { X = 2, Y = 3 } }; |
其等价于:
var r = new Rectangle(); r.P1.X = 0; r.P1.Y = 1; r.P2.X = 2; r.P2.Y = 3; |
4.2 集合初始化器
集合初始化器指定了集合的元素。
collection-initializer: element-initializer-list: element-initializer: |
一个集合初始化器由一系列的元素初始化器构成,包围在{和}记号之间,并使用逗号进行分隔。每个元素初始化器指定一个元素,该元素将被添加到待初始化的集合对象中。为了避免与成员初始化器混淆,元素初始化器不能是赋值表达式。
下面是包含了集合初始化器的对象创建表达式的一个例子:
List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; |
可以应用集合初始化器的对象的类型必须实现了System.Collections.Generic.ICollections<T>并指定了确定的T。此外,必须存在从每个元素初始化器的类型到T的隐式转换。如果这些条件不能满足,就会产生一个编译期错误。集合初始化器将依次对每个指定的元素调用ICollection<T>.Add(T)。
下面的类表是一个具有一个名字和一组电话号码的通讯录:
public class Contact { string name; List<string> phoneNumbers = new List<string>();
public string Name { get { return name; } set { name = value; } } public List<string> PhoneNumbers { get { return phoneNumbers; } } } |
可以像下面这样创建和初始化一个List<Contact>:
var contacts = new List<Contact> { new Contact { Name = "Chris Smith", PhoneNumbers = { "206-555-0101", "425-882-8080" } }, new Contact { Name = "Bob Harris", PhoneNumbers = { "650-555-0199" } } }; |
其等价于:
var contacts = new List<Contact>(); var __c1 = new Contact(); __c1.Name = "Chris Smith"; __c1.PhoneNumbers.Add("206-555-0101"); __c1.PhoneNumbers.Add("425-882-8080"); contacts.Add(__c1); var __c2 = new Contact(); __c2.Name = "Bob Harris"; __c2.PhoneNumbers.Add("650-555-0199"); contacts.Add(__c2); |
其中__c1和__c2是临时变量,在其他地方不可见且不可访问。
C# 3.0允许将new运算符用于一个匿名对象初始化器来创建一个匿名类型的对象。
primary-no-array-creation-expression: anonymous-object-creation-expression: anonymous-object-initializer: member-declarator-list: member-declarator: |
一个匿名对象初始化器声明了一个匿名类型并返回了该类型的一个实例。匿名类型是一个没有名字并且直接继承自object的类类型。匿名类型的成员是一系列可读/写属性,这些属性依次通过创建该类型的实例时使用的对象初始化器进行推断。特殊地,具有下面形式的一个匿名对象初始化器:
new { p1 = e1 , p2 = e2, ... pn = en } |
声明了一个具有下面形式的匿名类型:
class __Anonymous1 { private T1 f1; private T2 f2; ... private Tn fn;
public T1 p1 { get { return f1; } set { f1 = value; } } public T2 p2 { get { return f2; } set { f2 = value; } } ... public Tn pn { get { return fn; } set { fn = value; } } } |
其中的每个Tx是对应的表达式ex的类型。如果匿名对象初始化器中的某个表达式具有空类型,会发生一个编译期错误。
匿名类型的名字由编译器自动生成,并且不能在程序文本中引用。
在同一个程序中,两个具有相同名字、相同类型和相同顺序的属性的匿名对象初始化器将产生同一个匿名类型的实例。(这个定义包括了属性的顺序,这是因为在某些环境中顺序是可见的而且是非常重要的,比如反射。)
下面的例子:
var p1 = new { Name = "Lawnmower", Price = 495.00 }; var p2 = new { Name = "Shovel", Price = 26.95 }; p1 = p2; |
其中最后一行中的赋值是允许的,因为p1和p2具有相同的匿名类型。
一个成员初始化器可以缩写为一个简单名字或一个成员访问。这时称该成员初始化器为发散性初始化器(Projection Initializer),也是对具有相同名字的属性的声明和赋值的简写。特别地,具有下面形式的成员声明器:
identifier expr . identifier |
与下面的对应形式完全等价:
identifier = identifier identifier = expr . identifier |
因此,在一个发散性的初始化器中,identifier同时选择了所赋的值的值和域或属性。直观上看,发散性的初始化器反映出的不仅仅是一个值,还包括这个值的名字。
数组创建表达式的语法被扩展为支持具有隐式类型的数组创建表达式:
array-creation-expression: ... new [ ] array-initializer |
在一个具有隐式类型的数组创建表达式中,数组实例的类型通过数组初始化器中指定的元素来推断。特别地,数组初始化器中的表达式类型形成的集合中,必须恰好有一个类型使得其他类型都可以隐式地转换为该类型,并且该类型不是空类型,则创建一个具有该类型的数组。如果无法推断出恰好一个这样的类型,或者推断出来的类型是空类型,会发生一个编译期错误。
下面是具有隐式类型的数组创建表达式的一些例子:
var a = new[] { 1, 10, 100, 1000 }; // int[] var b = new[] { 1, 1.5, 2, 2.5 }; // double[] var c = new[] { "hello", null, "world" }; // string[] var d = new[] { 1, "one", 2, "two" }; // Error |
最后一个表达式会产生一个编译期错误,因为int和string都不能隐式地转换为另一个。这时就必须使用一个具有显式类型的数组创建表达式,例如指定其类型为object[]。另一种可选方法是,某个元素可以被转换为一个通用基本类型,这个类型就将成为推导出的类型。
具有隐式类型的数组创建表达式可以与匿名对象初始化器合并,来创建匿名类型数据结构。例如:
var contacts = new[] { new { Name = "Chris Smith", PhoneNumbers = new[] { "206-555-0101", "425-882-8080" } }, new { Name = "Bob Harris", PhoneNumbers = new[] { "650-555-0199" } } }; |
查询表达式(Query Expression)为查询提供了一种语言集成的语法,这种语法类似于关系和分级查询语言,如SQL和XQuery。
query-expression: from-clause: from-generators: from-generator: query-body: from-or-where-clauses: from-or-where-clause: where-clause: orderby-clause: ordering-clauses: ordering-clause: ordering-direction: select-or-group-clause: select-clause: group-clause: into-clause: |
一个查询表达式以一个from子句开始,以一个select或group子句结束。起始的from子句后可以跟零个或多个from或where子句。每个from子句都是一个生成器,该生成器引入了一个可以覆盖整个序列的迭代变量;而每个where子句都是一个过滤器,该过滤器用于从结果中排出项目。最终的select或group子句根据迭代变量来指定结果的表现形式。select或group子句前面还可以有一个orderby子句,用以指定结果的顺序。最后,可以用一个into子句通过将一个查询的结果作为一个子查询的生成器来“联结”两个查询。
在查询表达式中,具有多个生成器的from子句严格等价于多个顺序的只具有一个生成器的from子句。
7.1 查询表达式的翻译
C# 3.0语言并没有为查询表达式指定确切的执行语义,而是将查询表达式翻译为对附着于查询表达式模式(Query Expression Pattern)的方法的调用。特别地,查询表达式分别被翻译为对名为Where、Select、SelectMany、OrderBy、OrderByDescending、ThenBy、ThenByDescending和GroupBy的方法的调用,这些方法有着预期的签名和返回值类型。这些方法既可以是待查询对象的实例方法,也可以是对象外部的扩展方法。这些方法进行着实际的查询工作。
将查询表达式翻译为方法调用的过程是一个语法映射过程,发生在任何类型绑定或重载抉择的执行之前。翻译的结果可以保证语法正确,但不一定保证产生语义正确的C#代码。在查询表达式翻译之后,产生的方法调用作为一般的方法调用进行处理,这时会依次发现错误,如方法不存在、参数类型错误或对一个范型方法的类型推断失败等。
后面的一系列示例依次演示了查询表达式的翻译。在后面的某一节中给出了翻译规则的正式描述。
7.1.1 where子句
查询表达式中的一个where子句:
from c in customers where c.City == "London" select c |
将被翻译为对一个Where方法的调用,其参数为合并了迭代变量和where子句中的表达式所得到的拉姆达表达式:
customers. Where(c => c.City == "London") |
7.1.2 select子句
上面的例子演示了选择了最内部的迭代变量的select子句是如何通过翻译为方法调用被消除的。
一个选择了并非最内部的迭代变量的select子句:
from c in customers where c.City == "Longdon" select c.Name |
将被翻译为一个Select方法调用,其参数是一个拉姆达表达式:
customers. Where(c => c.City == "London"). Select(c => c.Name) |
7.1.3 group子句
一个group子句:
from c in customers group c.Name by c.Country |
将被翻译为对GroupBy方法的调用:
customers. GroupBy(c => c.Country, c => c.Name) |
7.1.4 orderby子句
一个orderby子句:
from c in customers orderby c.Name select new { c.Name, c.Phone } |
将被翻译为一个对OrderBy方法的调用,或者当指定了descending指示符时,被翻译为一个对OrderByDescending方法的调用:
customers. OrderBy(c => c.Name). Select(c => new { c.Name, c.Phone }) |
另一个orderby子句:
from c in customers orderby c.Country, c.Balance descending select new { c.Name, c.Country, c.Balance } |
将被翻译为对ThenBy和ThenByDescending方法的调用:
customers. OrderBy(c => c.Country). ThenByDescending(c => c.Balance). Select(c => new { c.Name, c.Country, c.Balance }) |
7.1.5 多重生成器
多重生成器:
from c in customers where c.City == "London" from o in c.Orders where o.OrderDate.Year == 2005 select new { c.Name, o.OrderID, o.Total } |
将被翻译为对所有非最内部生成器的SelectMany方法调用:
customers. Where(c => c.City == "London"). SelectMany(c => c.Orders. Where(o => o.OrderDate.Year == 2005). Select(o => new { c.Name, o.OrderID, o.Total }) ) |
当多重生成器被一个orderby子句合并起来:
from c in customers, o in c.Orders where o.OrderDate.Year == 2005 orderby o.Total descending select new { c.Name, o.OrderID, o.Total } |
一个附加的Select将被注入,用于收集排序表达式和最终的结果序列。让OrderBy可以操作整个序列是有必要的。OrderBy之后,最终的结果将被提取出来:
customers. SelectMany(c => c.Orders. Where(o => o.OrderDate.Year == 2005). Select(o => new { k1 = o.Total, v = new { c.Name, o.OrderID, o.Total } }) ). OrderByDescending(x => x.k1). Select(x => x.v) |
7.1.6 into子句
一个into子句:
from c in customers group c by c.Country into g select new { Country = g.Key, CustCount = g.Group.Count() } |
是嵌套查询的一种很简单的形式:
from g in from c in customers group c by c.Country select new { Country = g.Key, CustCount = g.Group.Count() } |
将被翻译为:
customers. GroupBy(c => c.Country). Select(g => new { Country = g.Key, CustCount = g.Group.Count() }) |
7.2 查询表达式模式
查询表达式模式(Query Expression Pattern)建立了类型可以实现的方法的一套模式,用以支持查询表达式。因为查询表达式会被通过语法映射来翻译为方法调用,因此类型在如何实现其查询表达式模式上尤为灵活。例如,模式的这些方法可以被实现为实例方法或扩展方法,因为两者具有完全一样的调用语法;而方法的参数也可以是委托或表达式树,因为拉姆达表达式可以转换为这两者。
下面给出了支持查询表达式模式的范型类型C<T>的推荐形式。范型类型用于演示参数和结果类型之间正确的关系,也可以将模式实现为非范型类型。
delegate R Func<A, R>(A arg);
class C<T> { public C<T> Where(Func<T, bool> predicate); public C<S> Select<S>(Func<T, S> selector); public C<S> SelectMany<S>(Func<T, C<S>> selector); public O<T> OrderBy<K>(Func<T, K> keyExpr); public O<T> OrderByDescending<K>(Func<T, K> keyExpr); public C<G<K, T>> GroupBy<K>(Func<T, K> keyExpr); public C<G<K, E>> GroupBy<K, E>(Func<T, K> keyExpr, Func<T, E> elemExpr); }
class O<T> : C<T> { public O<T> ThenBy<K>(Func<T, K> keySelector); public O<T> ThenByDescending<K>(Func<T, K> keySelector); }
class G<K, T> { public K Key { get; } public C<T> Group { get; } } |
上面的方法是用了一个范型委托类型Func<A, R>,也可以使用等价的其他委托或表达式树类型,只要参数和结果类型之间存在正确的关系即可。
注意在推荐的C<T>和O<T>之间的关系中,要保证ThenBy和ThenByDescending方法只能用在OrderBy或OrderByDescending的结果上。同时请注意GroupBy结果的推荐形式,应该是一组具有Key和Group属性的(匿名类型实例)序列。
标准查询运算符(Standard Query Operators,在另外一个规范中描述)提供了查询表达式的一个实现,这个实现可以用于所有实现了System.Collections.Generic.IEnumerable<T>接口的类型。
7.3 正式的翻译规则
对一个查询表达式的处理将重复、依次地应用下列翻译规则。每个翻译都一直应用这些规则直到不再发生任何给定的模式。
注意将会产生对OrderBy和ThenBy的调用的翻译,如果相应的排序子句制定了descending指示符,将产生对OrderByDescending或ThenByDescending的调用。
q1 into x q2
将被翻译为:
from x in (q1) q2
from g1, g2, ... gn
将被翻译为:
from g1 from g2 ... from gn
from x in e where f
将被翻译为:
from x in (e).Where(x => f)
from x1 in e1 from x2 in e2 ... orderby k1, k2 ... select v
将被翻译为:
(from x1 in e1 from x2 in e2 ...
select new { K1 = k1, K2 = k2 ..., V = v })
.OrderBy(x => x.K1).ThenBy(x => x.K2)...
.Select(x => x.V)
from x1 in e1 from x2 in e2 ... orderby k1, k2 ... group v by g
将被翻译为:
(from x1 in e1 from x2 in e2 ...
select new { K1 = k1, K2 = k2 ..., V = v, G = g })
.OrderBy(x => x.K1).ThenBy(x => x.K2) ...
.GroupBy(x => x.G, x => x.V)
from x in e from x1 in e1 ... select v
将被翻译为:
(e).SelectMany(x => from x1 in e1 ... select v)
from x in e from x1 in e1 ... group v by g
将被翻译为:
(e).SelectMany(x => from x1 in e1 ... group v by g)
from x in e select v
将被翻译为:
(e).Select(x => v)
当v就是标识符x时,翻译将被简化为:
(e)
from x in e group v by g
将被翻译为
(e).GroupBy(x => g, x => v)
当v就是标识符x时,翻译将被简化为:
(e).GroupBy(x => g)
from x in e orderby k1, k2 ... select v
将被翻译为:
(e).OrderBy(x => k1).ThenBy(x => k2) ...
.Select(x => v)
当v就是标识符x时,翻译将被简化为:
(e).OrderBy(x => k1).ThenBy(x => k2) ...
from x in e orderby k1, k2 ... group v by g
将被翻译为:
(e).OrderBy(x => k1).ThenBy(x => k2) ...
.GroupBy(x => g, x => v)
当v就是标识符x时,翻译将被简化为:
(e).OrderBy(x => k1).ThenBy(x => k2) ...
.GroupBy(x => g)
表达式树允许将拉姆达表达式表现为数据结构而不是可执行代码。一个可以转换为委托类型D的拉姆达表达式也可以转换为一个类型为System.Query.Expression<D>的表达式树。将一个拉姆达表达式转换为委托类型导致可执行代码被委托所生成和引用,而将其转换为一个表达式树类型将导致创建了表达式树实例的代码被发出(Emit)。表达式树是拉姆达表达式的一种高效的内存中(in-memory)数据表现形式,并且使得表达式的结构变得透明和明显。
下面的例子将一个拉姆达表达式分别表现为了可执行代码和表达式树。由于存在到Func<int, int>的转换,因此存在到Expression<Func<int, int>>的转换。
Func<int, int> f = x => x + 1; // 代码
Expression<Func<int, int>> e = x => x + 1; // 数据
在这些赋值完成之后,委托f标识一个返回x + 1的方法,而表达式树e表示一个描述了表达式x + 1的数据结构。
注意 表达式树的结构将被转换为一份单独的规范。该规范在PDC 2005技术预览版中并不适用。 |