18
实现,优点,陷阱以及题外话

Deferred execution

Embed Size (px)

Citation preview

Page 1: Deferred execution

实现,优点,陷阱以及题外话

Page 2: Deferred execution

假设一个场景: 我们需要获取并遍历一个包含大量元素的序列,从中找出我们需要的某个元素。

在此为了简单起见,我们假设该序列包含1000

万个int值,我们需要找到的是100万这个值。

Page 3: Deferred execution

要从一个序列中过滤出一个值则首先需要生成这

个序列。在此我们随意挑选三个序列类型:

int[]

Collection<int>

List<int>

Page 4: Deferred execution

要实现延迟执行(Deferred Execution)有两种方式可选:

自己创建实现了IEnumerable<T>接口的类型

使用yield关键字

这两种方式具体是如何实现延迟执行的?我们稍

后根据代码讲解。

Page 5: Deferred execution

第一种方式中的三个已有类型的实现都相当简单,请看代码。

生成数组的代码: 生成Collection<int>的代码:

Page 6: Deferred execution

生成List<int>的代码:

Page 7: Deferred execution

自己创建实现了IEnumerable<T>的类型的方式是

最复杂,最难写的。这个方式的基本思路就是构造

一个状态机,由于要在多个状态之间做切换,所以很容易出错。请看代码:

由于代码过长,在此只给出代码结构

图。这个类型同时实现了

IEnumerable<T>和IEnumerator<T>两个接口。其MoveNext方法和Current属性每被调用一次时,才即时生成一个元素,这样就避免了一次性填充整个序列,从而实现了延迟执行。

Page 8: Deferred execution

使用yield关键字是最简单,最偷懒的方式。

实际上,yield背后对应的实现和我们讲到的上

一种方式基本是一样的。编译器会把包含yield的代码块构造成一个同时实现了

IEnumerable<T>和IEnumerator<T>的类型。

Page 9: Deferred execution

测试中有几个辅助方法( TestTime,IterateSequence和TestSpeed)需要简单说明,

请看代码:

TestTime的代码:

TestTime接受一个Action类型的参数,在方法体

内执行action并为其计时,最后输出所耗时间。

Page 10: Deferred execution

IterateSequence的代码:

这段代码很简单:迭代一个序列,当找到要找的值之后则

break出去。

Page 11: Deferred execution

TestSpeed的代码:

这段代码测试所有五种实现方式的效率。把创建序列和过滤序列的代码包裹在lambda表达式中传入TestTime。

Page 12: Deferred execution

从1000万个元素中筛选

从1亿个元素中筛选

可以发现,当序列中元素数量增加时,前三种实现方式的耗

时量都在呈线性增长。而后两种实现方式的耗时量则

基本没有变化。

Page 13: Deferred execution

从前面的测试结果中可以看出,延迟执行的最明

显的优势即在于不会立即创建整个序列,而是在调用方索取时才即时生成元素。

这也正好解释了为什么将序列容量从1000万增加为1亿时延迟执行的方式执行时间基本不变。因为延迟执行的方法总是只生成100万个元素而已。

Page 14: Deferred execution

由于用来生成序列的算法被封装在了状态机内,

所以每次用foreach迭代这个序列时,整个序列都会被重新生成一次。

如果需要避免这种行为,可以通过在延迟执行的

返回结果上调用ToArray()或ToList()。然后在每次迭代中都使用已经填充好的Array或List。

其实这种特性在有的场景下是很有益的,比如生成序列的算法依赖于某些外部的变化条件(数据库,网络数据或者系统时间)。

Page 15: Deferred execution

我们每天都会用到的foreach究竟是如何实现的呢?

可以看出,一个foreach的“空转”循环基本等价于一个while循环加一个

try/finally代码块。

请注意在finally代码块中调用了

Dispose方法。如果这个foreach作用于一个延迟执行方法的返回值

上,那么对Dispose的调用就相当于

把状态机的状态清零。

Page 16: Deferred execution

前面讲foreach的题外话其实是为了讲解序列的

重新生成做基础。

前面已经讲过,foreach的尾部会调用迭代器的

Dispose方法,把状态机的状态清零。这样,如

果有下一个foreach来迭代同一个序列的话,则

会将封装在状态机内的生成元素的算法重新执行一遍,也就相当于重新生成了整个序列。

这样说或许过于晦涩,请看下一页的图解。

Page 17: Deferred execution

请看这把春田步枪,你装入子弹(调用GetEnumerator),撞针顶住了第一颗子弹(第一次调用MoveNext),开枪(访问Current属性),然后撞针顶住下一颗子弹(又一次调

用MoveNext),反复开枪(反复调用MoveNext并访问

Current属性),直到子弹耗尽(MoveNext返回了false),枪膛打开了(调用了

Dispose)。然后再装入子弹

开始下一轮的射击(序列的重新生成)。

Page 18: Deferred execution