在第一篇文章中,我讨论了如何用IronPython来模拟C#的语言扩展。在这篇文章中,我将进一步讨论如何用IronPython来构造LINQ查询。如果您读过《LINQ in Action》,您会发现我是依据此书来组织本系列文章的。我的第一篇文章对应《LINQ in Action》的第2章“C#和VB的语言增强”,本文对应第3章“LINQ构建块”。
1. IEnumerable<T>
在意图上,LINQ to Objects旨在查询内存中的数据源;在技术上,这些数据源是实现了System.Collections.Generic.IEnumerable<T>接口的对象。C#程序员常常使用.NET Framework提供了泛型容器作为数据源对象,它们位于System.Collections.Generic名空间下并实现了IEnumerable<T>。那么,IronPython程序员经常使用Python标准容器,包括列表(list)、元组(tuple)、集合(set)和字典(dict),实现了IEnumerable<T>吗?在IronPython 2.x的命令行上运行如下语句,就可以知道答案。
>>> import System
>>> ie_object = System.Collections.Generic.IEnumerable[System.Object]
>>> isinstance(list(),ie_object)
True
>>> isinstance(tuple(), ie_object)
True
>>> isinstance(set(), ie_object)
True
>>> isinstance(dict(), ie_object)
False
由运行结果可知,list、tuple和set实现了IEnumerable<Object>,而dict没有实现。实际上,C#也面临相似的问题。在.NET Framework中存在大量没有实现IEnumerable<T>的非泛型容器,程序员也会实现自定义的容器。如何将这些容器方便地配接(adapt)到IEnumerable<T>呢?C#设计者给出的答案是迭代器。
2. 迭代器(Iterator)
迭代器是一个用于遍历集合元素的对象。由于这是一种非常有用的设计模式,.NET Framework提供了迭代器接口IEnumerable(以及相应的范型接口IEnumerable<T>),C#语言则提供了关键字yield,以便直接构造实现了IEnumerable的迭代器类型。利用迭代器,C#程序就可以将非泛型容器、自定义容器和序列配接到IEnumerable<T>上。例如,Enumerable为非泛型容器提供了扩展方法Cast,它的一种可能实现如下。
1: public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source)
2: {
3: foreach (object elem in source)
4: {
5: yield return (TResult)elem;
6: }
7: }
在该实现中,Cast利用foreach迭代非泛型容器,将其中的元素强制转换为目标类型TResult的对象,然后用yield构造出实现了IEnumerable<T>的迭代器。这段代码简单明了,也很容易应用到其他需要IEnumerable<T>的情景中。
与C#类似,Python提供了关键字yield用于生成迭代器。此外,Python还提供迭代器的另一种形式:生成器(Generator)。利用它们可以将IronPython中的dict和自定义序列配接到IEnumerable<T>。在IronPython 2.x的命令行上运行如下语句,可知yield和生成器所返回的对象都实现了IEnumerable<Object>。
1: >>> import System
2: >>> ie_object = System.Collections.Generic.IEnumerable[System.Object]
3: >>> def seq(num):
4: ... for i in range(num):
5: ... yield i
6: ...
7: >>> isinstance(seq(1), ie_object)
8: True
9: >>> isinstance((i for i in range(1)) # (i for i in range(1)) is a generator,
10: ... # whose result is the same with seq(1).
11: ... , ie_object)
12: True
3. 查询操作符(Query Operator)
LINQ to Objects的查询操作符是定义在System.Linq.Enumerable类中的扩展方法。其签名形如:
1: public static int Count<TSource>
2: (this IEnumerable<TSource> source);
3:
4: public static IEnumerable<TResult> Select<TSource, TResult>
5: (this IEnumerable<TSource> source, Func<TSource, TResult> selector);
6:
7: public static IEnumerable<TSource> Where<TSource>
8: (this IEnumerable<TSource> source, Func<TSource, bool> predicate);
由于它们都是泛型函数,IronPython在调用它们时必须指定TSource等类型参数的具体类型。Harry Peirson在他的IronPython and Linq to XML中提供了一组辅助函数来简化IronPython的调用代码。
1: def Count(col):
2: return Enumerable.Count[object](col)
3:
4: def Select(col, fun):
5: return Enumerable.Select[object, object](col, Func[object, object](fun))
6:
7: def Where(col, fun):
8: return Enumerable.Where[object](col, Func[object, bool](fun))
以辅助方法Where(col, fun)为例,它将Enumerable.Where的类型参数TSource指定为object(即System.Object),也就是将该函数的第一个参数具现为IEnumerable<Object>,这样就可以将IronPython中的容器和生成器传递给该参数。然后它把IronPython中的函子fun包装为System.Func的对象,并将该对象传递给Enumerable.Where的第二个参数。这样包装的原因是,Enumerable.Where只接受Func对象,而不接受IronPython定义的函数和lambda表达式。
有了这样一组辅助方法,我们就可在IronPython中调用LINQ to Objects了。例如,以下这条C#语句
int count = Process.GetProcesses()
.Where(process => process.WorkingSet64 > 20*1024*1024)
.Count();
就可以用IronPython实现为
processes = Process.GetProcesses()
processes = Where(processes, lambda p: p.WorkingSet64 > 20*1024*1024)
cnt = Count(processes)
4. 查询表达式(Query Expression)
查询表达式是C#编译器提供的用于简化查询代码的语法糖。C#编译器会将查询表达式翻译为对扩展方法的调用。例如查询表达式
var processes =
from process in Process.GetProcesses()
where where process.WorkingSet64 > 20*1024*1024
orderby process.WorkingSet64
select new { process.Id, Name = process.ProcessName };
会被翻译为
var processes =
Process.GetProcesses()
.Where(process => process.WorkingSet64 > 20 * 1024 * 1024)
.OrderBy(process => process.WorkingSet64)
.Select(process => new { process.Id, Name = process.ProcessName });
由于IronPython编译器不支持查询表达式,在IronPython中无法写出SQL-Like的查询语句。但是,利用powershell.py(在IronPython自带的Tutorial目录下可以找到该文件)所提供的思路,我们可以写出“流水线”风格的查询。
processes = (
From(Process.GetProcesses())
.Where(lambda p: p.WorkingSet64 > 20*1024*1024)
.OrderBy(lambda p: p.WorkingSet64)
.Select(lambda p: makeobj(Id = p.Id, Name = p.ProcessName))
)
流水线是一种非常有用的“隐喻”,在Unix Shell、Windows PowerShell等环境中得到广泛的使用。程序员们熟悉它、喜爱它。更重要的是,它符合LINQ流式供应、延迟求值(deferred evaluation)的特点。从某种角度,它比查询表达式更好地表现了查询操作的语义。
在本系列的第三篇文章中,我将基于已有的讨论给出完整的解决方案,在IronPython中提供流水线风格的LINQ to Objects查询。