迭代器 Enumerator 和 yield

19 篇文章 0 订阅
15 篇文章 0 订阅

文 / 李博(光宇广贞)

       迭代器的意义

       对于纯 OOP 开发来说,数据被单独包装是很重要的手段,很多场景下也是必要的手段。这就产生了集合类和用来操作集合类的迭代器。

       以往,小型或者临时数据在内存的存在是可以用数组、列表、树(堆)等裸数据结构来操作的。但这种红果果的存在在大型数据(库)操作的时候存在一个重要的问题就是难以实现数据和逻辑、前台 UI 等的脱耦。

       要实现脱耦,首先就要对红果果的数据结构完成包装,使之成为承载外界数据(库)文件数据的独立实体,并与逻辑层——也就是操作数据与其它部件,如前台 UI 通信的算法层——分离。而后包装完的数据类,或称集合类还要易于逻辑层对它操作。

       怎么样能让集合类的数据集易于外部操作呢?你不可能事先估计出所有的外部需求数据的方式,你能提供的只能是像遍历、排序、最值等最基本的数据结构算法。即使你能给出尽可能想得到的所有操作数据集的算法,也可能外部一辈子都用不上。外部希望能直接获取数据集合来完成所需要的操作。怎么办? Public 数据结构么?这就需要迭代器了。

       迭代器的作用就是提供一种遍历数据集的方法。这就好像让集合类变得“透明”,也就是说我让你穿上衣服,但衣服不能妨碍我依然可以看到你的裸体。这种变态的需求是很必要且实用的。迭代器使集合类内部的数据结构“透明”,并可使之像裸体数据结构一样被搞。


       迭代器的使用

       看下面几个简单的例子:

public class FckEnum : IEnumerable
{
    int[] list = new int[] { 0, 1, 2, 3, 4, 5 };

    public IEnumerator GetEnumerator()
    {
        foreach (var i in list) yield return i;
    }
}

class Program
{
    static void Main(string[] args)
    {
        foreach (var i in new FckEnum()) Console.Write(i + " ");
    }
}

// 输出是 0 1 2 3 4 5

       实现了 IEnumerable 接口的方法 GetEnumerator() 的集合类 FckEnum 对像可以直接被外部枚举。FckEnum 类内部有一个 list 数据结构,虽然 FckEnum 将之封装起来,但是该 list 却操作起来对 Main 函数里面的 foreach 枚举过程是透明的。这个 GetEnumerator() 方法就是迭代器。

       内部数据的透明性会让人想起“索引器”,但索引器的存在并不是针对集合类和数据集的,它只是字段儿(CLR 属性)之外另一种呈现类内部数据的访问方式,而且事实上,你并不知道呈现给你的数据是哪个部分的,因此是毫无透明性可言的。

       迭代器还可以指定数据的遍历方式,比如说指定迭代范围:

public class FckEnum : IEnumerable
{
    public IEnumerable SelfDefinedEnumerator(int start, int end)
    {
        for (int i = start; i <= end; ++i) yield return i;
    }

    public IEnumerator GetEnumerator()
    {
        for (int i = 0; i < 12; ++i) yield return i;
    }
}

class Program
{
    static void Main(string[] args)
    {
        FckEnum fckEnum = new FckEnum();
        foreach (var i in fckEnum.GetEnumerator(0, 4)) Console.Write(i + " ");
    }
}

       注意这里的迭代器代码直接换成了 for / yield return,也就是说,迭代器可以即时生成数据。当然,也可以将 for 展开,直接罗列 yield return 组。

public IEnumerator GetEnumerator()
{  
    yield return 0;
    yield return 1;
    yield return 2;
}

       上述迭代器代码是通过 foreach/for + yield return 组合来实现的。实现了 IEnumerable 接口的数据集合类可以通过定义各种迭代器使外部对其内部数据集遍历。


       说说这个 yield

       说说这个 yield,目前它只有两种用法:yield return  和 yield break。yield 必须和 return 或 break 放在一起才可以使用。yield return 就是 continue。yield return 是在迭代继续条件满足的情况下,取出一个元素就上递给外部操作,而不是一下子把内部的数据集一齐端给外部的 foreach,这其实就是迭代器的“延迟性”——不错,LINQ 实义的扩展方法的内部实现使用的就是 yield 迭代器。

       在 C# 里面,yield 是个语法糖,文末参考里面《All About Iterators》详细介绍了编译器是如何将 yield 还原成 IEnumerableIEnumerator 的。文章也解释了为何使用 yield 返回的迭代器无法 Reset,因为 yield 生成的 IEnumerator 里面的 Reset() 直接扔出一个异常。或许是微软的工程师懒,或者是他们认为 yield 迭代器是用不着 Reset 的。使用 yield 生成的 IEnumerator 的实现里面的 state 变量很有意思,顾名思义,它记录着当前迭代器的状态:-2 时表示迭代器创建;0 表示迭代器准备好;-1 表示当前项迭代完成;1 表示迭代器可以继续迭代,即 0 < index < Count。在 MoveNext() 中,若迭代器可迭代,即 0 或 1 状态时,迭代一项,状态置 -1,而后检查边界,若可继续,则重置 为 1。若边界溢出,状态无法重置,则在下一轮 MoveNext() 时,检查状态为 –1,直接退出。

       yield 迭代器和 LINQ 的代价

       看这段儿代码:

static IEnumerable<T> Concat<T>(
      this IEnumerable<T> sequence1,
            IEnumerable<T> sequence2)

      foreach (var item in sequence1)
           yield return item;
      foreach (var item in sequence2)
           yield return item;
}

       这个是 LINQ 里 Concat<T> 方法的实现。复杂度是多少?线性的么? 考虑如下代码块:

IEnumerable<int> ones = new[] { 1 };

for (int i = 0; i < loops; ++i)
    ones = ones.Concat(ones);

foreach (var i in ones) ;

       这种代码应该不鲜见。for 块模拟了多层 LINQ,也就是多层 yield。foreach 的效率如下图:

       结果是二次曲线。想像中,Concat 应该串起的是一条线,但对这条线的遍历却并非线性。《All About Iterators》一文对此已作详细介绍。一定要避免深层递归过程中使用 LINQ,毕竟 Concat 算法在 LINQ 扩展方法中算是轻量的,就已经这德性了。


       F# 里面的 yield 和 yield!

       F# 里面迭代很常用,比如生成一个序列或列表表示平方:[ for i in 1..10 do yield i * i ],也可以简写为:[ for i in 1..10 –> i * i ]。

       讨论 F# 里面的 yield,就要先谈到 Seq 模块。Seq 的意义就是处理一个较大的且并不需要使用所有元素的数据集,典型的,如对 Fibbonacci 序列的记录,或者简单的如自然数序列。要生成所有这样的数据集,可以从我家连到宇宙的尽头。因此只能是枚举到哪儿,算到哪儿,对于 Seq 来说,就是 yield 到哪儿。对应的,其实 F# 里面 Seq 就是 .net 里面的 IEnumerable 的别名。

       F# 里面还有一个 yield!,它用于在递归过程中,将下一层返回上来的序列拆并到当前层的序列中。C# 不需要它,是因为在 C# 里面,找不到什么方法来表示 { 1, 2, 3, … } 这样无限集合的东西。而且存在表示这一无限集合的想法对于 C# 来说本身就是很不可理喻的。什么需要?数学需要,支持函数式编程的 F# 就需要,因此 F# 需要多层迭代生成连续 Seq 的方法,需要 yield! 操作。

       无论是单层还是多层迭代,都只是告诉 Seq 生成元素集的方式,在对它开始枚举之前,元素集是不会生成的,枚举到哪儿,生成到哪儿,yield 到哪儿。这就是 yield 的力量。

       举一个二叉树中序遍历的算法示例吧。这个例子是微软官方给的,在文末“参考”里面的“序列(F#)”链接里面就可以找到。此例完美展现了 yield 和 yield! 的用法。

type Tree<'a> =
   | Tree of 'a * Tree<'a> * Tree<'a>
   | Leaf of 'a

let rec inorder tree =
    seq {
      match tree with
          | Tree(x, left, right) ->
               yield!inorder left
               yield x
               yield!inorder right
          | Leaf x -> yield x
    }  
      
let mytree = Tree(6, Tree(2, Leaf(1), Leaf(3)), Leaf(9))
let seq1 = inorder mytree
printfn "%A" seq1

       参考:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值