文章目录
前一篇博客我简单回顾了C#中迭代器的实现,这一篇我将从LINQ扩展开始,解析迭代器延迟执行的原理,并实现LINQ的Where和Select扩展,简单了解流式处理的思想
如果想跳过查看其他内容,可以查看同系列另外两篇文章链接
C#迭代器的实现和应用(一)——基础篇
C#迭代器的实现和应用(二)——延迟执行、流式处理与两个基本LINQ扩展的实现
一、拾遗——从管道折返的思考
我之前有在《对“管道”的进一步理解》一文中实现过一个自制的简单管道,用于逐步抽象以说明管道思想的使用,但是实际上我那次的实现的Where
、Select
函数与LINQ中提供的函数仍然有所区别:
我的实现本质上是对每一次操作都进行迭代并创建新的集合,而在LINQ中提供的Where
和Select
函数组合后的行为是不一样的。
要知道哪里不一样,从代码的具体输出就能很容易地看出来。
1. 两种实现的行为区别测试
首先这里是在上一篇文章中的代码,我进行了微小的改动,把形参名从List
改成了IEnumerable
,函数名也做了修改,方便区分。
单纯从代码上可以看出可以发现每一次都直接进行迭代然后返回了值。
public static class ExcuteUtils
{
public static IEnumerable<T> ExcuteWhere<T>(this IEnumerable<T> collection, Func<T, bool> pass)
{
List<T> result = new List<T>();
foreach (var item in collection)
{
if (pass(item))
{
result.Add(item);
}
}
return result;
}
public static IEnumerable<TResult> ExcuteSelect<TInput, TResult>(this IEnumerable<TInput> collection, Func<TInput, TResult> selector)
{
List<TResult> result = new List<TResult>();
foreach (var item in collection)
{
result.Add(selector(item));
}
return result;
}
}
这是是我们的测试代码,
static void TestDiff()
{
int[] numbers = { 1, 2, 3, 4, 5, 6 };
Func<int,bool> whereFunc = (i) =>
{
Console.WriteLine("where : " + i);
return i % 2 == 0;
};
Func<int, string> selectFunc = (i) =>
{
Console.WriteLine("select : " + i);
return i.ToString();
};
Console.WriteLine("----------------------------");
var myExcute = numbers.ExcuteWhere(whereFunc).ExcuteSelect(selectFunc);
Console.WriteLine(myExcute.Print());
Console.WriteLine("----------------------------");
var linq = numbers.Where(whereFunc).Select(selectFunc);
Console.WriteLine(linq.Print());
Console.WriteLine("----------------------------");
}
在代码里我首先创建了一个数组作为迭代的数据源,然后分别创建了两个用于where和select迭代的函数,为了查看函数的行为,在每个函数中我们都输出了当前处理的值。
此外,我还使用了一个print()
函数,这个函数也是一个扩展函数,它会对一个集合中的所有对象逐个调用ToString()
并使用,
分隔,然后输出,这个函数的代码如下:
public static string Print<T>(this IEnumerable<T> list)
{
return '[' + string.Join(',', list.ToArray()) + ']';
}
运行我们的代码,可以得到下面这样的输出:
----------------------------
where : 1
where : 2
where : 3
where : 4
where : 5
where : 6
select : 2
select : 4
select : 6
[2,4,6]
----------------------------
where : 1
where : 2
select : 2
where : 3
where : 4
select : 4
where : 5
where : 6
select : 6
[2,4,6]
----------------------------
从输出结果可以很容易看到,两种实现的最终结果没有区别,但执行过程是不一样的:
我自己的实现是按顺序先对整个序列调用了
where
,再对之后的序列调用了select
;
LINQ的实现是先调用符合条件的where
,一旦找到符合条件的值,就立刻调用select
;
2. 我自己实现的函数的时序图
在这里,每一步的操作都非常简单,一步一步依次推进就完成了。
3. LINQ实现的时序图
我这里只展示了1和2这两个数字的计算流程,由于后面四个数字也是在重复这个循环,就不再重复了。
在整个时序图中,一次计算就是一次完整的调用所有的计算,比起我在之前文章写的简单“管道”,这才是一个真正的“管道”,一条真正的流水线。
4. 两种实现的核心机制区别——延迟执行
可能很多人都知道LINQ里有一个概念,叫延迟执行
(MSDN文档中叫“惰性计算”),在这里,造成这里运行区别的就是这个延迟执行
。
但是这个延迟执行是如何实现的?
二、延迟执行的本质
1. 延迟执行的本质
在C#迭代器的实现和应用(一)——基础篇里,我们实现了一个自己的MyRange
迭代器,输出结果如下:
/*输出
new range
MyRangeIterator MoveNext : 1
print : 2
MyRangeIterator MoveNext : 2
print : 3
MyRangeIterator MoveNext : 3
print : 4
MyRangeIterator MoveNext : 4
print : 5
MyRangeIterator MoveNext : 5
*/
观察这个输出结果,会发现一个有趣的情况:
迭代器是在输出new range
之前创建的,但它每一次MoveNext
的输出却出现在了new range
之后——换句话说,迭代器里的函数只在被迭代时才被调用,而不是在迭代器被创建时被调用,迭代器里的函数被延迟调用了!
为什么呢?
仔细回忆迭代器的创建过程,我们在创建迭代器的时候,并没有调用过迭代器用用于迭代的MoveNext
函数,直到我们使用Foreach
函数时,才在这个函数里被调用了。
而这,就是延迟执行的根本原因——本质上还是在哪里执行,就在哪里调用,但由于我们提前把计算的过程封装在了迭代器中,所以在迭代器的迭代过程中,自然就被执行了调用,而这个实现的本质,就是——创建与调用分离。
LINQ中的扩展方法也是利用了这样的原理来实现了延迟调用。
2. 基于延迟执行原理的发散——流式处理
基于延迟执行的原理,我们不仅可以把一些数据封装在迭代器中,甚至可以把具体的操作封装在迭代器里;
更进一步,如果我们把迭代器也看作一个数据,那么我们还能把封装了操作的迭代器封装在迭代器里,最后只需要调用最外层的迭代器,就可以自动依次调用封装的操作了。
- 把一个处理操作放到迭代器里,让这个处理操作在MoveNext中调用,那么这个函数就会跟随MoveNext在被迭代时调用
- 把一个迭代器A放进另一个迭代器B里,并且使迭代器B从迭代器A中获取数据,那么迭代器B在迭代时就会将驱动A进行迭代,这样就实现了一次遍历对集合的所有元素进行处理
基于迭代器层层封装的前提,结合上面的时序图,如果我们拥有一个被用于迭代的集合,那么我们每一次的操作都只针对当前具体的这一个集合中的对象,那么即使这个集合有无限多个数据,我们本质上也只通过一次迭代就完成了所有的操作——而这也是流式处理的核心特点之一。
三、实现我们自己的where和select
这里的车速可能有一点快,需要一点点理解时间和前置知识。
前置知识1: C#的迭代器基础——如果你对C#迭代器的理解还不够熟悉,对yield的功能也还不理解的话,建议返回上一篇,或者查阅一些资料,掌握相关的基础内容。
前置知识2:C#泛型函数
前置知识3:C#静态扩展
直接上代码:
public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> list, Func<T, bool> predicate)
{
foreach (var it in list)
{
if (predicate(it)) yield return it;
}
}
public static IEnumerable<TResult> MySelect<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> exchangeFunc)
{
foreach (var item in source)
{
yield return exchangeFunc(item);
}
}
两个函数的思路都很简单,因为最后运行迭代器时,每一次迭代都会自动向集合中请求下一个操作对象,于是我们利用yield return
自动保存上下文的机制,每一次都使用传入的操作函数对前一个迭代器中传入的值进行处理,并且此函数最后返回的也是一个迭代器,这样就形成了一个迭代器的自动调用栈;
当这一个操作处理完成后,最外层的迭代器又对当前的迭代器进行请求一下个迭代的数据,那么由于yield return
自动保存的状态,可以很容易地再对集合的下一个数据进行迭代。
如果你一下不能理解这两个函数的实现机制或者对完全手写实现这两个迭代器感兴趣,我在本文末尾也附有自己完全手写实现的Where和Select两个迭代器。
代码测试如下:
int[] numbers = { 1, 2, 3, 4, 5, 6 };
Func<int, bool> whereFunc = (i) =>
{
Console.WriteLine("where : " + i);
return i % 2 == 0;
};
Func<int, string> selectFunc = (i) =>
{
Console.WriteLine("select : " + i);
return i.ToString();
};
var myExcute = numbers.MyWhere(whereFunc).MySelect(selectFunc);
Console.WriteLine(myExcute.Print());
where : 1
where : 2
select : 2
where : 3
where : 4
select : 4
where : 5
where : 6
select : 6
[2,4,6]
注意输出内容,调用时序与LINQ扩展保持了一致。
附录:完全通过实现迭代器手写的where和select
- Select迭代器
class SelectEnumerable<TSource, TResult> : IEnumerable<TResult>
{
IEnumerable<TSource> source;
Func<TSource, TResult> exchangeFunc;
public SelectEnumerable(IEnumerable<TSource> source, Func<TSource, TResult> exchangeFunc)
{
this.source = source;
this.exchangeFunc = exchangeFunc;
}
public IEnumerator<TResult> GetEnumerator()
{
return new SelectEnumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
class SelectEnumerator : IEnumerator<TResult>
{
SelectEnumerable<TSource, TResult> parent;
IEnumerator<TSource> source;
public SelectEnumerator(SelectEnumerable<TSource, TResult> parent)
{
this.parent = parent;
this.source = parent.source.GetEnumerator();
}
public TResult Current => parent.exchangeFunc(this.source.Current);
object IEnumerator.Current => Current;
public void Dispose()
{
source.Dispose();
}
public bool MoveNext()
{
return source.MoveNext();
}
public void Reset()
{
source.Reset();
}
}
}
- Where迭代器
public class WhereEnumerable<T> : IEnumerable<T>
{
IEnumerable<T> source;
Predicate<T> filter;
public WhereEnumerable(IEnumerable<T> source, Predicate<T> predicate)
{
this.source = source;
this.filter = predicate;
}
public IEnumerator<T> GetEnumerator()
{
return new WhereEnumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
class WhereEnumerator : IEnumerator<T>
{
WhereEnumerable<T> parent;
IEnumerator<T> sourceEnumerator;
public WhereEnumerator(WhereEnumerable<T> parent)
{
this.parent = parent;
this.sourceEnumerator = parent.source.GetEnumerator();
}
public T Current => sourceEnumerator.Current;
object IEnumerator.Current => Current;
public void Dispose()
{
sourceEnumerator.Dispose();
}
public bool MoveNext()
{
while (sourceEnumerator.MoveNext())
{
if (parent.filter(sourceEnumerator.Current)) return true;
}
return false;
}
public void Reset()
{
sourceEnumerator.Reset();
}
}
}