天天看点

迭代器(C#和Java)

迭代器是一种设计模式,不管是Java还是C#,基本都是和foreach配合使用的,Java又略有不同。迭代器存在的目的是不给用户看到原始数据,客户需要数据的时候,由提供者提供给客户一个迭代器,客户通过这个迭代器来拿数据。不管内部的数据结构如何,内部的遍历如何实现,对外的迭代器的格式都是固定的。所以数据具体什么样的,由什么格式来存储,存储在什么位置,客户是不知情的。迭代器的作用就是查询,而不是修改,如果直接把原始数据丢给客户,那么客户就可以随意修改原始数据了,如果不想让客户随意修改原始数据,那么就要拷贝一份,这会导致内存开销加大。有些不必要的操作也没必要,比如要获取第一个,那么只需要迭代一次就可以了,但是如果选择传统的方式,则需要先拷贝一份数据源,然后拿到第一个,或者写一个first()方法获取到第一个,这都不如迭代器方便。

总结迭代器的作用:

1)数据对外不可见,用户不可以随意修改原始数据源。

2)有些数据结构比较复杂,比如二叉树结构的数据,用户遍历起来麻烦,那么则对外提供一个迭代器,内部如果对这部分数据进行遍历,用户不再需要去关心。

3)有些场景下,迭代的过程需要复杂的操作,例如读取磁盘下的所有文件 ,那么提供给用户一个迭代器,视用户遍历的情况来取数据,需要多少取多少,不用一次性从磁盘里面读出所有的信息。

本文将会针对c#和java两种语言的迭代器使用进行讲解。

C#:

c#创建迭代器的关键字是yield,要求要进行迭代的类继承IEnumerable接口,比如MyEnumerator这个类:

public class MyEnumerator : IEnumerable
    {
        private int[] nums = new int[5] { 1, 2, 3, 4, 5 };
        public IEnumerator GetEnumerator()
        {
            foreach (var v in nums)
                yield return v;
        }
    }

    public class MyEnumerator : IEnumerable
    {
        public IEnumerator GetEnumerator()
        {
            yield return 1;
            yield return 2;
            yield return 3;
            yield return 4;
            yield return 5;
        }
    }

        //使用代码
        static void Main(string[] args)
        {
            var myEnumerator = new MyEnumerator();
            //这里不需要调用GetEnumerator方法。
            foreach (var v in myEnumerator)
            {
                Console.WriteLine(v);
            }
            System.Console.WriteLine("Press any key to exit.");
            System.Console.ReadKey();
        }
           

上面的两种写法结果是完全一样的。

yield 关键字在这里的作用是向编译器指示它所在的方法是迭代器块。 编译器生成一个类来实现迭代器块中表示的行为。 在迭代器块中,yield 关键字与 return 关键字结合使用,向枚举器对象提供值。

 尽管这里以方法的形式编写迭代器,但编译器会将其转换为一个实际上是状态机的嵌套类。 只要客户端代码中的 foreach 循环继续进行,此类就会跟踪迭代器的位置。

所以关键的点在于用yield告诉编译器某一块的代码是迭代器,然后编译器会在编译时将其进行转换成实际上是状态机的嵌套类。

这就是为什么返回之后继续迭代,能够接着之前的位置继续往下查找结果的原因。另外在使用的时候要使用foreach语句,而不是其他的循环语句。

在为类或结构创建迭代器时,不必实现整个 IEnumerator 接口。 当编译器检测到迭代器时,它将自动生成 IEnumerator 或 IEnumerator<T> 接口的 Current、MoveNext 和 Dispose 方法。

下面是官方提供的一个泛型列表迭代器的例子(https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2010/ee5kxzk0(v=vs.100)):

using System.Collections;
using System.Collections.Generic;

namespace GenericIteratorExample
{
    public class Stack<T> : IEnumerable<T>
    {
        private T[] values = new T[100];
        private int top = 0;

        public void Push(T t) { values[top++] = t; }
        public T Pop() { return values[--top]; }

        // These make Stack<T> implement IEnumerable<T> allowing
        // a stack to be used in a foreach statement.
        public IEnumerator<T> GetEnumerator()
        {
            for (int i = top - 1; i >= 0; i-- )
            {
                yield return values[i];
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        // Iterate from top to bottom.
        public IEnumerable<T> TopToBottom
        {
            get
            {
                // Since we implement IEnumerable<T>
                // and the default iteration is top to bottom,
                // just return the object.
                return this;
            }
        }

        // Iterate from bottom to top.
        public IEnumerable<T> BottomToTop
        {
            get
            {
                for (int i = 0; i < top; i++)
                {
                    yield return values[i];
                }
            }
        }

        // A parameterized iterator that return n items from the top
        public IEnumerable<T> TopN(int n)
        {
            // in this example we return less than N if necessary 
            int j = n >= top ? 0 : top - n;

            for (int i = top; --i >= j; )
            {
                yield return values[i];
            }
        }
    }

    // This code uses a stack and the TopToBottom and BottomToTop properties 
    // to enumerate the elements of the stack.
    class Test
    {
        static void Main()
        {
            Stack<int> s = new Stack<int>();
            for (int i = 0; i < 10; i++)
            {
                s.Push(i);
            }

            // Prints: 9 8 7 6 5 4 3 2 1 0
            // Foreach legal since s implements IEnumerable<int>
            foreach (int n in s)
            {
                System.Console.Write("{0} ", n);
            }
            System.Console.WriteLine();

            // Prints: 9 8 7 6 5 4 3 2 1 0
            // Foreach legal since s.TopToBottom returns IEnumerable<int>
            foreach (int n in s.TopToBottom)
            {
                System.Console.Write("{0} ", n);
            }
            System.Console.WriteLine();

            // Prints: 0 1 2 3 4 5 6 7 8 9
            // Foreach legal since s.BottomToTop returns IEnumerable<int>
            foreach (int n in s.BottomToTop)
            {
                System.Console.Write("{0} ", n);
            }
            System.Console.WriteLine();

            // Prints: 9 8 7 6 5 4 3
            // Foreach legal since s.TopN returns IEnumerable<int>
            foreach (int n in s.TopN(7))
            {
                System.Console.Write("{0} ", n);
            }
            System.Console.WriteLine();

            // Keep the console window open in debug mode.
            System.Console.WriteLine("Press any key to exit.");
            System.Console.ReadKey();
        }
    }
}
/* Output:
    9 8 7 6 5 4 3 2 1 0
    9 8 7 6 5 4 3 2 1 0
    0 1 2 3 4 5 6 7 8 9
    9 8 7 6 5 4 3
*/
           

这个例子基本包含了迭代器的常规操作,迭代器的作用就是迭代数据,不要把它想得太复杂。

Java:

//实现Iterable接口后,这个类就可以被foreach循环了
public class MyEnumerator implements Iterable<Integer>{
    private Integer[] list=new Integer[]{1,2,3,4,5};
    private Integer[] list1=new Integer[]{10,20,30,40,50};
    //当这个类被foreach循环的时候,会首先调用这个方法获取迭代器,然后进行循环
    @Override
    public Iterator<Integer> iterator() {
        return new Itr();
    }
    //接受lamda表达式的foreach循环,有默认实现,可以不实现
    @Override
    public void forEach(Consumer<? super Integer> action) {
        Objects.requireNonNull(action);
        for (Integer t : list) {
            action.accept(t);
        }
    }
    //具体的迭代器类的实现
    // 利用的是成员内部类的特性, 可以方便的拿到上层类的数据集,然后在这个基础上进行迭代
    class Itr implements Iterator<Integer>
    {
        private int index=0;
        @Override
        public boolean hasNext() {
            return index<list1.length;
        }
        @Override
        public Integer next() {
            return list1[index++];
        }
    }
}
           

上面是java的一个迭代器的标准实现,使用的是成员内部类去实现,成员内部类编译器会自动帮你生成一个构造方法,将生层类作为参数传递给内部类,这样内部类就可以很方便地拿到上层类的变量了。如果不了解内部类的话,可以看我的另外一篇博客https://blog.csdn.net/dap769815768/article/details/87625620

使用上面的自定义迭代器:

public class Main {
    public static void main(String[] args) {
        System.out.println("hello world");
        MyEnumerator myEnumerator=new MyEnumerator();
        for (Integer i:myEnumerator)
        {
            System.out.println(i);
        }
        myEnumerator.forEach(x->System.out.println(x));
    }
}
           

输出结果:

hello world
10
20
30
40
50
1
2
3
4
5
           

总结下来就是,如果类实现了Iterable,那么这个类就可以被foreach循环,同时还可以调用forEach方法进行循环,forEach方法接口有默认实现,同时也可以写自己的实现,这个列子里我写了一个自己的实现。

要想被迭代就必须添加自己的迭代器类,也就是本例中的内部类Itr,它实现Iterator接口,并实现了next和hasNext方法,这样这个迭代器才能正常使用。

当这个类被foreach循环,其过程是,首先调用iterator方法获取到这个类的迭代器,然后利用这个迭代器将数据遍历一遍。

如果你使用java反编译工具,就可以看到,原本的foreach循环被改掉了,这里我使用idea自带的反编译工具,查看到的代码变成了下面的这样:

迭代器(C#和Java)

即foreach循环只是一个语法糖,最终编译的字节文件,使用的还是传统的获取迭代器的方法。

继续阅读