Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Deferred execution

1,846 views

Published on

Deferred execution

Published in: Technology
  • Be the first to comment

  • Be the first to like this

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 />

×