一直以来,我以为 LINQ 是专门用来对不同数据源进行查询的工具,直到我看了这篇十多年前的文章,才发现 LINQ 的功能远不止 Query。这篇文章的内容比较高级,主要写了用 C# 3.0 推出的 LINQ 语法实现了一套“解析器组合子(Parser Combinator)”的过程。那么这个组合子是用来干什么的呢?简单来说,就是把一个个小型的语法解析器组装成一个大的语法解析器。当然了,我本身水平有限,暂时还写不出来这么高级的代码,不过这篇文章中的一段话引起了我的注意:
Any type which implements Select, SelectMany and Where methods supports (part of) the "query pattern" which means we can write C#3.0 queries including multiple froms, an optional where clause and a select clause to process objects of this type.
大意就是,任何实现了 Select,SelectMany 等方法的类型,都是支持类似于 from x in y select x.z 这样的 LINQ 语法的。比如说,如果我们为 Task 类型实现了上面提到的两个方法,那么我们就可以不借助 async/await 来对 Task 进行操作:
// 请在 Xamarin WorkBook 中执行
var taskA = Task.FromResult(12);
var taskB = Task.FromResult(12);
// 使用 async/await 计算 taskA 跟 taskB 的和
var a = await taskA;
var b = await taskB;
var r = a + b;
// 如果为 Task 实现了 LINQ 拓展方法,就可以这么写:
var r = from a in taskA
from b in taskB
select a + b;
那么我们就来看看如何实现一个非常简单的 LINQ to Task 吧。
LINQ to Task首先我们要定义一个 Select 拓展方法,用来实现通过一个 Func<TValue, TResult> 将 Task<TValue> 转换成 Task<TResult> 的功能。
static async Task<TR> Select<TV,TR>(this Task<TV> task, Func<TV, TR> selector) {
var value = await task; // 取出 task 中的值
return selector(value); // 使用 selector 对取出的值进行变换
}
这个函数非常简单,甚至可以简化为一行代码,不过仅仅这是这样就可以让我们写出一个非常简单的 LINQ 语句了:
var taskA = Task.FromResult(12);
var r = from a in taskA select a * a;
那么实际上 C# 编译器是如何工作的呢?我们可以借助下面这个有趣的函数来一探究竟:
void PrintExpr<T1,T2>(Expression<Func<T1, T2>> expr) {
Console.WriteLine(expr.ToString());
}
熟悉 LINQ 的人肯定对 Expression 不陌生,Expressing 给了我们在运行时解析代码结构的能力。在 C# 里面,我们可以非常轻松地把一个 Lambda 转换成一个 Expression,然后调用转换后的 Expression 对象的 ToString() 方法,我们就可以在运行时以字符串的形式获取到 Lambda 的源码。例如:
var taskA = Task.FromResult(12);
PrintExpr((int _) => from a in taskA select a * a);
// 输出: _ => taskA.Select(a => (a * a))
可以看到,Expression 把这段 LINQ 的真面目给我们揭示出来了。那么,更加复杂一点的 LINQ 呢?
var taskA = Task.FromResult(12);
var taskB = Task.FromResult(12);
PrintExpr((int _) =>
from a in taskA
from b in taskB
select a * b
);
如果你尝试运行这段代码,你应该会遇到一个错误——缺少对应的 SelectMany 方法,下面给出的就是这个 SelectMany 方法的实现:
static async Task<TR> SelectMany<TV, TS, TR>(this Task<TV> task, Func<TV, Task<TS>> selector, Func<TV,TS, TR> projector){
var value = await task;
var selected = await selector(value);
return projector(value, selected);
}
这个 SelectMany 实现的功能就是,通过一个 Func<TValue, Task<TResult>> 将 Task<TValue> 转换成 Task<TResult>。有了这个之后,你就可以看到上面的那个较为复杂的 LINQ to Task 语句编译后的结果:
_ => taskA.SelectMany(a => taskB, (a, b) => (a * b))
可以看到,当出现了两个 Task 之后,LINQ 就会使用 SelectMany 来代替 Select。可是我想为什么 LINQ 不像之前那样,用两个 Select 分别处理两个 Task 呢?为了弄清楚这个问题,我试着推导了一番:
// 首先简单粗暴的用两个 Select 来实现这个功能
Task<Task<int>> r = taskA.Select(a => b.Select(b => a + b));
// r 被包裹了两层 Task,我们可以用 SelectMany 来去掉一层 Task 包装
// 这时 TValue 是 Task<int>, TResult 是 int
//
// 那么 Task<Task<int>>
// 将通过 Func<Task<int>, Task<int>>
// 转换成 Task<int>
Task<int> result = r.SelectMany(x => x, (_, x) => x);