Deferred execution

1,620
-1

Published on

Deferred execution

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total Views
1,620
On Slideshare
0
From Embeds
0
Number of Embeds
4
Actions
Shares
0
Downloads
9
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Deferred execution

  1. 1. 浅析延迟执行<br />实现,优点,陷阱以及题外话<br />
  2. 2. 背景<br />假设一个场景: 我们需要获取并遍历一个包含大量元素的序列,从中找出我们需要的某个元素。<br />在此为了简单起见,我们假设该序列包含1000万个int值,我们需要找到的是100万这个值。<br />
  3. 3. 第一类实现方式:使用已有类型<br />要从一个序列中过滤出一个值则首先需要生成这个序列。在此我们随意挑选三个序列类型:<br />int[]<br />Collection<int><br />List<int><br />
  4. 4. 第二种方式:延迟执行<br />要实现延迟执行(Deferred Execution)有两种方式可选:<br />自己创建实现了IEnumerable<T>接口的类型<br />使用yield关键字<br />这两种方式具体是如何实现延迟执行的?我们稍后根据代码讲解。<br />
  5. 5. 实现(1)<br />第一种方式中的三个已有类型的实现都相当简单,请看代码。<br />生成数组的代码:<br />生成Collection<int>的代码:<br />
  6. 6. 实现(1)<br />生成List<int>的代码:<br />
  7. 7. 实现(2)<br />自己创建实现了IEnumerable<T>的类型的方式是最复杂,最难写的。这个方式的基本思路就是构造一个状态机,由于要在多个状态之间做切换,所以很容易出错。请看代码:<br />由于代码过长,在此只给出代码结构图。这个类型同时实现了IEnumerable<T>和IEnumerator<T>两个接口。其MoveNext方法和Current属性每被调用一次时,才即时生成一个元素,这样就避免了一次性填充整个序列,从而实现了延迟执行。<br />
  8. 8. 实现(3)<br />使用yield关键字是最简单,最偷懒的方式。<br />实际上,yield背后对应的实现和我们讲到的上一种方式基本是一样的。编译器会把包含yield的代码块构造成一个同时实现了IEnumerable<T>和IEnumerator<T>的类型。<br />
  9. 9. 测试(1)<br />测试中有几个辅助方法(TestTime,IterateSequence和TestSpeed)需要简单说明,请看代码:<br />TestTime的代码:<br />TestTime接受一个Action类型的参数,在方法体内执行action并为其计时,最后输出所耗时间。<br />
  10. 10. 测试(1)<br />IterateSequence的代码:<br />这段代码很简单:迭代一个序列,当找到要找的值之后则break出去。<br />
  11. 11. 测试(1)<br />TestSpeed的代码:<br />这段代码测试所有五种实现方式的效率。把创建序列和过滤序列的代码包裹在lambda表达式中传入TestTime。<br />
  12. 12. 测试(2)<br />从1000万个元素中筛选<br />从1亿个元素中筛选<br />可以发现,当序列中元素数量增加时,前三种实现方式的耗时量都在呈线性增长。<br />而后两种实现方式的耗时量则基本没有变化。<br />
  13. 13. 总结(1):延迟执行的好处<br />从前面的测试结果中可以看出,延迟执行的最明显的优势即在于不会立即创建整个序列,而是在调用方索取时才即时生成元素。<br />这也正好解释了为什么将序列容量从1000万增加为1亿时延迟执行的方式执行时间基本不变。因为延迟执行的方法总是只生成100万个元素而已。<br />
  14. 14. 总结(2):可能的陷阱<br />由于用来生成序列的算法被封装在了状态机内,所以每次用foreach迭代这个序列时,整个序列都会被重新生成一次。<br />如果需要避免这种行为,可以通过在延迟执行的返回结果上调用ToArray()或ToList()。然后在每次迭代中都使用已经填充好的Array或List。<br />其实这种特性在有的场景下是很有益的,比如生成序列的算法依赖于某些外部的变化条件(数据库,网络数据或者系统时间)。<br />
  15. 15. 题外话(1):foreach<br />我们每天都会用到的foreach究竟是如何实现的呢?<br />可以看出,一个foreach的“空转”循环基本等价于一个while循环加一个try/finally代码块。<br />请注意在finally代码块中调用了Dispose方法。如果这个foreach作用于一个延迟执行方法的返回值上,那么对Dispose的调用就相当于把状态机的状态清零。<br />
  16. 16. 题外话(2):序列的重新生成<br />前面讲foreach的题外话其实是为了讲解序列的重新生成做基础。<br />前面已经讲过,foreach的尾部会调用迭代器的Dispose方法,把状态机的状态清零。这样,如果有下一个foreach来迭代同一个序列的话,则会将封装在状态机内的生成元素的算法重新执行一遍,也就相当于重新生成了整个序列。<br />这样说或许过于晦涩,请看下一页的图解。<br />
  17. 17. 题外话(2):序列的重新生成<br />请看这把春田步枪,你装入子弹(调用GetEnumerator),撞针顶住了第一颗子弹(第一次调用MoveNext),开枪(访问Current属性),然后撞针顶住下一颗子弹(又一次调用MoveNext),反复开枪(反复调用MoveNext并访问Current属性),直到子弹耗尽(MoveNext返回了false),枪膛打开了(调用了Dispose)。然后再装入子弹开始下一轮的射击(序列的重新生成)。<br />
  18. 18. 谢谢<br />
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×