C# 学习笔记:迭代器

 C#中,如果实现遍历一个数组,除了for循环,还可以是foreach循环。在foreach循环中,我们只需要创建一个同类型的值,来表示我们遍历后的值就可以了。但是实际上,只有实现了IEnumerable接口的类型,才能使用foreach遍历。

那么什么是迭代器呢:

我们先手动实现以下迭代,我们使用迭代器写个和foreach类似的功能来遍历一个字符串,输出它每个字符。在foreach前面调用它:

        static void Main()
        {
            string str = "ABCDEFG";
            foreachFunc(str);
            foreach (char a in str)
            {
                Console.WriteLine("官方foreach里的循环是:" + a);
            }

        }
        static void foreachFunc(string str)
        {
            IEnumerator e = str.GetEnumerator();
            while (e.MoveNext())
            {
                Console.WriteLine("民间foreach里的循环是:" + e.Current);
            }
        }

实现的效果是一样的:

我们发现民间的foreach也是可以完成工作的。

string 里面有一个GetEnumerator方法,这个方法返回一个IEnumerator的对象。一个官方定义的数据元素的数组,一般都继承了这个IEnumerableIEnumerator接口,来配合foreach实现遍历的操作。

枚举接口:IEnumerableIEnumerator

枚举接口IEnumerable和IEnumerator​​​是迭代器模式(iterator pattern)​在C#中的实现​​​。它们实现在集合上进行简单迭代的效果。

1.IEnumerable接口定义了一个可以返回IEnumerator类型对象的方法:GetIEnumerator。

2.IEnumerator接口在它内部的字段和方法主要有三个:

  • current字段,它是只读(属性只有get)的。
  • MoveNext函数,对集合上实现循环迭代的效果。返回一个bool值,来表示是否可以继续迭代。
  • Reset函数,表示将枚举数设置为其初始位置,该位置位于集合中第一个元素之前。

代码中有可能出现两个不同的迭代器对同一个序列进行迭代,我们需要两个状态能被正确的处理,所以C#把枚举接口分为IEnumerator和IEnumerable。而为了不违背单一职责原则,IEnumerable本身没有实现MoveNext方法。

我们可以自定义类来手动实现枚举接口的功能:

我们先定义IEnumerable的接口的类,里面存放一个数组:

    class GameEnumerable : IEnumerable
    {
        private string[] Games = new string[5] { "彩虹六号", "赛博朋克", "骑马与砍杀", "神界原罪", "刺客信条" };

        public IEnumerator GetEnumerator()
        {
            return new GameEnumerator(Games);
        }
    }

然后我们再定义实现IEnumerator接口的类,同样的,用数组索引的加减来实现迭代:

    class GameEnumerator : IEnumerator
    {
        private string[] Games;
        private int position = -1;
        //用于遍历的标志索引,一般默认值为-1,以便于第一次输出就能输出0
        public GameEnumerator(string[] gamenames)
        {
            Games = new string[gamenames.Length];
            for (int i = 0; i < Games.Length; i++)
            {
                Games[i] = gamenames[i];
            }
        }

        public object Current
        {
            //Current只读,且要注意索引position越界的情况
            get
            {
                if(position>=Games.Length)
                {
                    return null;
                }
                return Games[position];
            }
        }
        bool IEnumerator.MoveNext()
        {
            
            if (position < Games.Length) 
            {
                position++;
                return true;
            }
            return false;
        }

        void IEnumerator.Reset()
        {
            position = -1;
        }
    }

然后我们在主函数中创建IEnumerable的实例,然后使用IEnumerator来接受它。

注意,实现IEnumerator接口的类的MoveNext方法使用了接口约束,所以只有IEnumerator接口的对象接受GameEnumerator 类的实例才能访问到MoveNext方法。

然后我们在主函数中调用MoveNext方法就可以实现遍历了:

        static void Main()
        {
            GameEnumerable enumerable = new GameEnumerable();

            IEnumerator game = enumerable.GetEnumerator();
            
            while(game.MoveNext())
            {
                if (game.Current == null)
                {
                    break;
                }
                Console.WriteLine("当前数组里的游戏是" + game.Current);
            }
        }

效果和我们想象的一样:

但是很明显,这样来进行遍历太繁琐了,但在C# 1.0里,一切都是这么发生的,如果想要加一点灵活性,可以使用IEnumerable和IEnumerator的泛型版本,我们上面的改一改就变成这样:

    class Program
    {
        static void Main()
        {
            Console.WriteLine("请输入需要的数组长度:");
            int Length = int.Parse(Console.ReadLine());
            int[] array = new int[Length];

            for (int i = 0; i < Length; i++)
            {
                Console.WriteLine("请输入第" + (i + 1) + "个数");
                array[i] = int.Parse(Console.ReadLine());
            }

            GameEnumerable<int> enumerable = new GameEnumerable<int>(array);

            IEnumerator game = enumerable.GetEnumerator();
            
            while(game.MoveNext())
            {
                if (game.Current == null)
                {
                    break;
                }
                Console.WriteLine("当前数组里的是" + game.Current);
            }
        }

    }
    class GameEnumerable<T> : IEnumerable<T>
    {
        private T[] Games;
        public GameEnumerable(T[] games)
        {
            Games = new T[games.Length];
            for (int i = 0; i < Games.Length; i++)
            {
                Games[i] = games[i];
            }
        }
        public IEnumerator GetEnumerator(int i)
        {
            return null;
        }
        public IEnumerator<T> GetEnumerator()
        {
            return new GameEnumerator<T>(Games);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<T>)Games).GetEnumerator();
        }
    }
    class GameEnumerator<T> : IEnumerator<T> 
    {
        private T[] Games;
        private int position = -1;
        //用于遍历的标志索引,一般默认值为-1,以便于第一次输出就能输出0
        public GameEnumerator(T[] gamenames)
        {
            Games = new T[gamenames.Length];
            for (int i = 0; i < Games.Length; i++)
            {
                Games[i] = gamenames[i];
            }
        }
        

        public object Current
        {
            //Current只读,且要注意索引position越界的情况
            get
            {
                if(position>=Games.Length)
                {
                    return null;
                }
                return Games[position];
            }
        }

        T IEnumerator<T>.Current
        {
            get
            {
                if (position >= Games.Length)
                {
                    return default(T);
                }
                return Games[position];
            }
        }

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        bool IEnumerator.MoveNext()
        {
            
            if (position < Games.Length) 
            {
                position++;
                return true;
            }
            return false;
        }

        void IEnumerator.Reset()
        {
            position = -1;
        }
    }

有点长,不过功能还是一样的,区别在于我们可以自定义类型,我们这里尝试了一下int型然后输出:

不过要注意的是:由于IEnumerable<T>和IEnumerator<T>都是各自继承自IEnumerable和IEnumerator的,受制于接口的继承原则,实现泛型枚举接口的类对于同一个字段或函数可能要声明两个版本,两个版本的区别在于返回值的不同,可以不使用,但是不能不声明。例如我们上文中的Current,实际上写了两个版本,一个是Object的,一个是泛型的。

迭代器和yield语句

上面的遍历虽然说功能实现了,但是逻辑比较复杂,而且由于定义了接口的原因,里面的字段和方法的要求特别多。C#2.0以后引入了迭代器,简化了上述的流程。

迭代器的声明格式为:

        IEnumerable/IEnumerator FunctionName()
        {
            yield return ...
        }

它的返回值是IEnumerableIEnumerator的对象,后面跟随的是函数的名字,然后可以在一个逻辑分支里多次执行yield return 语句来返回,不同的是,在yield return执行完毕以后,函数并不会销毁,而是“休克”,等待返回值的IEnumerator执行下一次MoveNext();

迭代器返回的IEnumerator对象没有手动实现例如上文中的MoveNext、Current的方法。它使用一个或多个yield return语句告诉编译器创建枚举器类,yield return语句指定了枚举器中下一个可枚举项,迭代器在每次调用MoveNext函数时,会顺着上一次的枚举项(yield return)按照我们自己写的逻辑执行到下一个枚举项去。

在迭代器中需要注意的是:

  • IEnumeratorIEnumerable如果是非泛型版本,yield return返回的Current值是Object类型
  • 泛型IEnumerator<T>IEnumerable<T>yield return返回的Current值是T类型,例如IEnumerable<string>返回值是string类型的
  • 当执行到yield return的时候,后面返回值会传入Current内部。
  • 迭代器不会在调用迭代器的时候(即下文中调用Function(Length))就开始执行,而是会在第一次调用MoveNext方法的时候开始执行。
  • 若迭代器返回IEnumerable对象,那么每执行一次MoveNext()。根据IEnumerable生成的IEnumerator对象会执行一次IEnumerable对象的GetIEnumerator方法,来准备下一个执行的数据。

我们看个例子:

        static void Main()
        {
            Console.WriteLine("请输入您需要的数组的长度");
            int Length = int.Parse(Console.ReadLine());
            int[] Array = new int[Length];

            IEnumerable enumerable = Function(Length);

            IEnumerator enumerator = enumerable.GetEnumerator();

            int i = 0;
            while (enumerator.MoveNext())
            {
                Array[i] = (int)enumerator.Current;
                i++;
            }

            Console.WriteLine("数组里的值是:");
            foreach (int t in Array)
            {
                Console.Write(t.ToString()+"   ");
            }
            Console.WriteLine("  ");

        }
        static IEnumerable Function(int Length)
        {
            for (int i = 0; i < Length; i++)
            {
                Console.WriteLine("请输入您需要放在数组里的值:");
                int x = int.Parse(Console.ReadLine());
                Console.WriteLine("此时我们要放入数组的值是:" + x);
                yield return x;
            }
        }

这个例子中,我们迭代器中有一个for循环,每次循环都yield return一次,返回的值放入我们的数组中。每次我们输入一个值,可以通过Current来传递给我们在主函数里声明的数组。当然,我们也可以不用IEnumerable返回,可以直接使用IEnumerator来返回,代码还可以精简一丢丢。

看一下结果:

我们再看一个例子:

在这个例子中,我们实时监测MoveNext的返回值情况,我们设定一个迭代器中循环的值X,当我们迭代器中循环超出5的时候我们将执行yield break,即迭代终止:

        static void Main()
        {
            Console.WriteLine("输入你想从何时迭代结束");
            int x = int.Parse(Console.ReadLine());
            
            IEnumerable<int> ienumerable = TestStateChange(x);
            
            IEnumerator<int> ienumerator = ienumerable.GetEnumerator();

            Console.WriteLine("主函数:第一次调用MoveNext,迭代器开始运行");
            bool Next = ienumerator.MoveNext();
            Console.WriteLine("主函数:是否有数据" + Next + ",Current:" + ienumerator.Current);

            Console.WriteLine("主函数:第二次调用MoveNext");
            Next = ienumerator.MoveNext();
            Console.WriteLine("主函数:是否有数据" + Next + ",Current:" + ienumerator.Current);

            Console.WriteLine("主函数:第三次调用MoveNext");
            Next = ienumerator.MoveNext();
            Console.WriteLine("主函数:是否有数据" + Next + ",Current:" + ienumerator.Current);
        }


        static IEnumerable<int> TestStateChange(int count)
        {
            Console.WriteLine("迭代器:我是第一行代码");
            Console.WriteLine("迭代器:我是第一个YieldReturn前的");
            yield return 1;
            Console.WriteLine("迭代器:我是第一个YieldReturn后的代码");

            for (int i = 0; i < count; i++)
            {
                Console.WriteLine("迭代器:这是第" + i + "次了");
                if (i > 5)
                {
                    yield break;
                }
            }

            Console.WriteLine("迭代器:我是第二个YieldReturn前的代码");
            yield return 2;
            Console.WriteLine("迭代器:我是第二个YieldReturn后的代码");
        }

 我们首先先设定只循环3次,小于5,不会迭代终止,输出为:

我们可以观察到:

  • 迭代器在没有执行MoveNext之前函数是不会开始的,只有调用了第一次MoveNext迭代器才会开始运行。
  • 迭代器运行之前的Current值是该类型的默认值,比如我们使用int类型,CurrentMoveNext执行前的值就是0。
  • 且在迭代器里的值在yield return后不会销毁,它会存在直至迭代器逻辑结束。
  • 当迭代器逻辑运行到最后一个yield returnMoveNext返回值也是true,这样能保证最后一个yield return之后的逻辑能够顺利运行,当这最后的逻辑执行完,我们再次执行MoveNext才会是false

我们如果输入大于5的值,导致yield break会如何呢。

我们看到:

执行了yield break后,迭代器立即停止,MoveNext立马返回false,且Current保留在最后一次return的值上。

我们可以画一张图来表示IEnumerable和IEnumerator和迭代器的关系:

 

迭代器背后的状态机

迭代器在我们看不见的地方,实际上的原理就是一个状态机,迭代器有四种可能状态,分别是Before状态、Running状态、Suspended状态、After状态。这四个状态的的转换是这样的:

由图我们可以看到,是

  • 第一次运行MoveNext才会进入Running状态,即迭代器开始执行。
  • yield return状态让迭代器暂停挂起,直到我们再次执行MoveNext。
  • 逻辑结束或yield break才会进入after状态,此时MoveNext返回值是false。

在编译器的内部,我们的迭代器实际上生成了一个类,该类继承了IEnumerator接口,并且在内部创建了有关于迭代器的Current字段和MoveNext函数,MoveNext函数实际上是一个很大的Switch语句,它实现yield return功能实际上靠着goto语句半路插入才能使迭代器能从yield return语句处执行。

泛型迭代器中的Finally语句

我们平常普通情况下的return关键字的用法一般有两个:

  • 给调用者提供返回值。
  • 终止方法的执行,在退出时执行合适的finally代码块。

在C#中由于Finally语句比return语句的优先级高,所以Try-Catch语句可以由return语句退出,但是finally里面的逻辑是不会跳过的,但是对于泛型迭代器来说,相较于非泛型的迭代器,泛型迭代器继承了IDisposable接口,由此也多了一个Dispose方法。

在泛型迭代器中,如果存在Finally语句,如果不使用Foreach语句或者显式调用Dispose方法,将不会执行finally逻辑块。

由于foreach会在它自身的finally语句中调用IEnumerator所提供的Dispose方法。在迭代器迭代完成之前,若yield return 语句处于try-catch逻辑块中,如果调用了泛型迭代器上面的Dispose方法,则会执行Finally逻辑块。(注意,此时迭代器不会因此终止)。

我们可以理解为,只要对迭代器使用了Foreach语句,那么迭代器中的Finally语句将会按照正常的方式工作。

但是,如果没有调用Dispose方法,泛型迭代器将不会调用Finally逻辑块。我们写一个例子看看:

    class Program
    {
        static void Main()
        {
            DateTime stop = DateTime.Now.AddSeconds(5);
            foreach (int i in CountWithTimeLimit(stop)) 
            {
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("得到了" + i);
                if (i > 10)
                {
                    Console.WriteLine("迭代器终止.....");
                    return;
                }
                Thread.Sleep(300);
            }
        }
        static IEnumerable<int> CountWithTimeLimit(DateTime limit)
        {
            try
            {
                for (int i = 1; i <= 100; i++)
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine("当前时间是" + DateTime.Now);
                    if (DateTime.Now > limit)
                    {
                        yield break;
                    }
                    yield return i;
                }
            }
            finally
            {
                Console.WriteLine("迭代器finally块已经调用!");
            }
        }
    }

那么我们看到的结果是:

如果我们不用foreach语句,而是使用for循环执行同样的逻辑,我们把上面主函数的逻辑注释掉,修改成自定义的for循环,即以下的样子:

        static void Main()
        {
            DateTime stop = DateTime.Now.AddSeconds(5);
            IEnumerable<int> ienum = CountWithTimeLimit(stop);
            IEnumerator<int> ienumer = ienum.GetEnumerator();
            
            for (int i = ienumer.Current; ; i = ienumer.Current)
            {
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("得到了" + i);
                if (i > 10)
                {
                    Console.WriteLine("迭代器终止");
                    return;
                }
                Thread.Sleep(300);
                ienumer.MoveNext();
            }
         } 

 那么很显然,finally语句块将不会被调用。

但是,如果是非泛型迭代器,那么处于try逻辑块的yield return一定会执行finally逻辑块。需要注意的是,非泛型迭代器没有继承IDisposable接口。

同样的,C#的迭代器也需要我们注意以下几点:

  • 迭代器的参数列表不能用传值参数out和引用参数ref来修饰。
  • 在C# 2.0以后的迭代器的Reset属性不可靠,在我们自己定义的迭代器块中,Reset是没有实现的。
  • Current的值在迭代器结束后将一直存在直至GC将它清除。我们可以认为Current属性总是在迭代器开始前为默认值,在迭代器结束后一直为最后的yield return 生成值。
  • 7
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值