7. Query Expressions(查询表达式)
查询表达式提供了与SQL这样的关系化和分级的查询语言相类似的语言集成的语法。一个查询表达式是以from子句开头以select或者group子句结束,这个初始的from子句可以在其后跟随任意多个from、let、where或者join子句。
那么查询表达式中的这些子句都是做什么的呢?from子句是一个引入了一个覆盖整个序列的范围变量的生成器;let子句计算一个值并传入一个标识符来显示这个值;where子句是一个从结果集中排除元素的过滤器;join子句用于比对源序列和另一个序列中指定的键并找出匹配的对;select或group子句根据范围变量来指定结果的形式;into子句可以将一个查询的结果作为后续查询中的生成器并以此来拼接两个查询。
前面提到了每一条查询语句都必须是以from开头的,在from后面可以接除“;”、“,”和“=”以外所有的符号,如果要在查询语句中使用这些标识符,需要在前面加上“@”符号。
C# 3.0并没有给查询表达式指定明确的执行语义,准确地说,C# 3.0把查询语句翻译为对依附于查询表达式模式的方法的调用,这些方法可以是对象实例方法也可以是扩展方法,它们执行的才是实际的查询工作。查询表达式翻译为方法调用的过程是发生在类型绑定或者过载抉择之前的句法映射过程,翻译可以保证语法正确但不能确保生成的一定是语法正确的C#代码。在翻译之后,作为结果的方法调用会被作为一般的方法调用,并且这可能会依次找出错误,例如方法不存在、参数中存在错误的类型或者对范型方法的类型推断失败之类的。
一条查询语句的执行是以重复地应用后续翻译直到不再有更深一层可进行的缩减。这些翻译根据优先权排列,每个部分都假定前一部分中的翻译已经被完全地执行。明确的翻译采用透明标志符(Transparent identifiers)“*”来注入范围变量。
接下来我们还是来看码说话吧,这样可能更便于理解和掌握。前面提到,查询表达式并不具备确切的执行语义,而是用来被翻译成采用查询表达式模式的方法调用,那我们就来看看这里所说的翻译到底是什么样子的吧。
1: public static void Query()
2: {
3: Dictionary<string, int> students = new Dictionary<string, int>();
4: students.Add("ZeroCool", 24);
5: students.Add("Michael", 21);
6: students.Add("Frieda", 22);
7:
8: IEnumerable<KeyValuePair<string, int>> queryResult = from student in students
9: where student.Value <= 23
10: orderby student.Value descending
11: select student;
12:
13: foreach (var student in queryResult)
14: {
15: Console.WriteLine(student.Key + " is " + student.Value + " years old.");
16: }
17:
18: Console.ReadLine();
19: }
上面这段代码要做的事情是从students这个集合里面找出所有年龄小于24岁的学生并按年龄降序排序,我们着重来看一下第8到第11行代码。我想所有写过SQL查询语句的朋友都会对这句话感到似曾相识。是的,它非常像SQL查询语句,但又略有差别,我们来看一下这句话会被C# 3.0翻译成怎样的方法就会了解它是如何工作的了。
1: IEnumerable<KeyValuePair<string, int>> queryResult
2: = students.Where(student => student.Value <= 23).OrderByDescending(student => student.Value).Select(student => student);
上面这行代码就是查询语句的翻译结果,这行代码放在原先程序中替换掉第8到第11行代码依然是可以得出同样的结果的,或许这看上去还不太好理解吧,那么我们用先前介绍过的表达式树来翻译。
1: Func<KeyValuePair<string, int>, bool> filter = student => student.Value <= 23;
2: Func<KeyValuePair<string, int>, int> extract = student => student.Value;
3: Func<KeyValuePair<string, int>, KeyValuePair<string, int>> result = student => student;
4:
5: IEnumerable<KeyValuePair<string, int>> queryResult = students.Where(filter).OrderByDescending(extract).Select(result);
这样,虽然代码量多了一点,但的确理解起来方便了很多。翻译的结果是直接调用原数据集合对象的Where方法选出符合条件的候选项,然后在用OrderByDescending方法进行降序排序,最后通过Select方法把这些结果返回回来。上面的例子虽然比较简单,但是可以让我们很好地了解查询表达式的语法以及翻译的方式,只要我们了解了最基本也是最核心的概念,那么即使面对复杂的查询表达式也可以迎刃而解了。
前面提到过一个概念叫做“透明标识符”,可能其定义比较抽象大家不太好理解,现在我们借助一个翻译过程来解释一下到底什么事透明标识符吧。
1: from customer in customers
2: from order in customer.Orders
3: orderby order.Total descending
4: select new { customer.Name, order.Total };
我们假设有上面这句查询语句,它的工作是从消费者的集合里找出每个消费者的名字和他的订购总量,并按照每个消费者订购总量对他们进行降序排序。接下来我们来看看这句查询表达式将会被翻译成什么样子把。
1: from * in
2: from customer in customers
3: from order in customer.Orders
4: select new { customer, order }
5: orderby order.Total descending
6: select new { customer.Name, order.Total }
现在透明标识符*已经出现了,不过这还只是中间步骤,我们继续往下翻译:
1: customers.
2: SelectMany(customer => customer.Orders.Select(order => new { customer, order })).
3: OrderByDescending(* => order.Total).Select(* => new { customer.Name, order.Total })
如果我们把透明标识符去掉,那么上面几行代码就等效于:
1: customers.
2: SelectMany(customer => customer.Orders.Select(o => new { customer, order })).
3: OrderByDescending(x => x.order.Total).Select(x => new { x.customer.Name, x.order.Total })
其中的x是编译器生成的一个开发人员看不见摸不着的标识符。透明标识符不是一个确切的语言特性,它只作为查询表达式翻译过程中的一个中间步骤。
当查询表达式注入一个透明标识符时,接下来的翻译步骤会将这个透明标识符传入到Lambda表达式和匿名对象初始器中去。在这样的上下文中,透明标识符具有以下的行为:
- 当一个透明标识符作为Lambda表达式中的参数出现的时候,相关联的匿名类型的成员将自动处于Lambda表达式体中的范围内;
- 当一个含有透明标识符的成员处于当前范围内,那么这个成员的成员也都处于当前范围内;
- 当一个透明标识符作为一个匿名对象初始器中的成员声明符出现时,它会引入一个带有透明标识符的成员;
好了,查询表达式就介绍到这里吧,也许你觉得查询表达式有很多概念读起来比较抽象,不那么便于理解,但是我相信,只要你动手多练习一下,很多概念都能很好地理解的。《C# 3.0 探索之旅》系列技术介绍文章也就到此为止了,我相信C# 3.0为我们广大开发人员带来的这些新特性都能够是我们在很大程度上提高开发效率,同时也增加了很多开发的乐趣,随着微软不断有面向各个层面和领域的新技术问世,我相信.NET平台也会逐渐成为一个更加广泛、强大的技术平台。接下来的时间我会和大家一起共同探讨一下微软的另一个重头戏——LINQ项目,敬请关注!