C# 函数式编程:LINQ

一直以来,我以为 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);

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/f5cc450b18cf816a73d43c0544f93dd5.html