C#中的IEnumerator 和 yield

目录

1.非泛型的IEnumerator 和 IEnumerable 的实现:

2.C#1举例如何实现可迭代对象:

3.C#2举例实现:使用yield关键字简化

4.yield break 相当于普通方法中的return

5.finally块 被 实现为 Dispose

6.实际开发中实现迭代器的例子

参考:


如果你了解迭代器模式,那么就很好理解 IEnumerator 和 IEnumerable。

如果你不了解迭代器模式,可以通过理解 IEnumerator 和 IEnumerable去更好的理解。

.NET中  通过 IEnumerator 和 IEnumerable 以及他们的泛型等价物 来封装 Iterator 迭代器模式。

迭代器模式:允许你访问一个数据项序列,而无需关心序列内部的组织结构

如果看懂了迭代器模式,再看下面的IEnumerator和IEnumerable就很好懂。

我们把IEnumerator叫“迭代器”IEnumerable叫“可迭代的”,一般聚合对象要继承这个IEnumerable,因为聚合对象一般都是可迭代的。


1.不带<>的IEnumerator 和 IEnumerable(非泛型)

IEnumberator,枚举器(迭代器) , 字面意思,用来一个一个枚举。

其关键成员有:当前对象current,是否有下一个movenext

IEnumerable,可枚举的, 字面意思,继承了我就变成了一个可枚举的对象。

其只有一个成员 GetEnumerator,返回枚举器类型。

这俩都是接口类型,具体内部接口函数签名为:

//  枚举器接口 IEnumerator
public interface IEnumerator
{
    object Current { get; }
    // 如果是返回 false,就是结束迭代器块
    bool MoveNext();
    void Reset();
}
//  可枚举的接口 IEnumerable, 返回枚举器接口
public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

如果说某个类实现了IEnumerable可枚举的,那么我们说这个类就是可迭代对象(即可枚举对象,因为可以通过GetEnumerator返回枚举器,从而进行枚举)。

比如下面的dict, 实现 迭代访问:

(IEnumerable.GetEnumerator ,IEnumerator.MoveNext, IEnumerator.Current

// dict是实现了 IEnumerable 接口的可迭代对象。
// iterator 是枚举器
var iterator = dict.GetEnumerator();
while(iterator.MoveNext())
{
  item = iterator.Current;
  .....
}

也可以使用下面这个foreach,其实就是上面的简化写法,不过就看不出来本质是什么了。

不过一般都会直接用foreach进行迭代,简单。

foreach其实是GoF Iterator模式的实现。

foreach(var item in dict)


2.C#1举例如何实现可迭代对象(类似上述例子中的dict):

上面例子中的dict是C#提供好的数据结构,那么我们自己要实现类似于dict这种的可迭代对象具体要怎么实现呢?

下面实现一个IterationSample 类,让它成为是一个为可迭代对象。

首先肯定要继承IEnumerable接口,然后再需要实现:

    1)一个获得枚举器的函数,GetEnumerator函数

    2) 一个枚举器,IterationSampleIterator,继承IEnumberator

下面的代码都很简单,很好理解的

class IterationSample : IEnumerable
{
    object[] values;
    int startingPoint;

    public IterationSample(object[] values, int startingPoint)
    {
        this.values = values;
        this.startingPoint = startingPoint;
    }

    public IEnumerator GetEnumerator()
    {
        return new IterationSampleIterator(this);
    }
    // 迭代器实现
    class IterationSampleIterator : IEnumerator
    {
        // 重要数据成员,来获取当前值,下一个值,是否结束。在构造函数中赋值。
        IterationSample parent;
        // 标记当前所迭代的位置。
        int position;
        internal IterationSampleIterator(IterationSample parent)
        {
            this.parent = parent;
            position = -1;
          }

        public bool MoveNext()
        {
            if (position != parent.values.Length)
            {
                position++;
            }
            return position < parent.values.Length;
        }

        public object Current
        {
            get
            {
                if (position == -1 ||
                    position == parent.values.Length)
                {
                    throw new InvalidOperationException();
                }
                int index = (position + parent.startingPoint);
                index = index % parent.values.Length;
                return parent.values[index];
            }
        }

        public void Reset()
        {
            position = -1;
        }
    }
}

这样就能像下面这样进行迭代:

// sample 是 IterationSample 对象
var iterator = sample.GetEnumerator();
while(iterator.MoveNext())
{
  item = iterator.Current;
  .....
}
或者用
foreach(var item in sample) 
{
    .....
}

上述的实现迭代器的代码容易理解,但是实现代码繁琐。


3.C#2举例实现:使用yield关键字简化

如果要实现一个可迭代对象,像上面这么实现,写一大串代码,那可太麻烦了,而且对于不同的可迭代对象,其实很多都是类似的,就是有重复代码,那么可以让编译器给我们实现:

同样实现上述例子中的功能,C#2中可以使用如下进行简化:

    1)GetEnumerator函数重新实现,包含yield return 语句。

    2)IterationSampleIterator 枚举器类,没有实现, 删除不需要了。

class IterationSample : IEnumerable
{
    object[] values;
    int startingPoint;

    public IterationSample(object[] values, int startingPoint)
    {
        this.values = values;
        this.startingPoint = startingPoint;
    }

    public IEnumerator GetEnumerator()
    {
      for (int index = 0; index <values.Length; index++)
      {
        yield return values[(index + startingPoint) % values.Length];
      }
    }
}

yield关键字是一个语法糖,背后其实生成了一个新的类,肯定是一个枚举器。而枚举器的具体实现MoveNext Current。 就看GetEnumerator中的函数体了。

具体实现原理可以看下面这篇文章,详细易懂:c# yield关键字原理详解 - blueberryzzz - 博客园

更详细的可以看这个例子(来自这里):

原始的Test代码:

using System;  
using System.Collections;  
  
class Test  
{  
    static IEnumerator GetCounter()  
    {  
        for (int count = 0; count < 10; count++)  
        {  
            yield return count;  
        }  
    }  
}  

真正的Test类:C#编译器针对包含yield的函数生成了一个新的类 <GetCounter>d__0

internal class Test  
{  
    // Note how this doesn't execute any of our original code  
    private static IEnumerator GetCounter()  
    {  
        return new <GetCounter>d__0(0);  
    }  
  
    // Nested type automatically created by the compiler to implement the iterator  
    [CompilerGenerated]  
    private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable  
    {  
        // Fields: there'll always be a "state" and "current", but the "count"  
        // comes from the local variable in our iterator block.  
        private int <>1__state;  
        private object <>2__current;  
        public int <count>5__1;  
  
        [DebuggerHidden]  
        public <GetCounter>d__0(int <>1__state)  
        {  
            this.<>1__state = <>1__state;  
        }  
  
        // Almost all of the real work happens here  
        private bool MoveNext()  
        {  
            switch (this.<>1__state)  
            {  
                case 0:  
                    this.<>1__state = -1;  
                    this.<count>5__1 = 0;  
                    while (this.<count>5__1 < 10)        //这里针对循环处理  
                    {  
                        this.<>2__current = this.<count>5__1;  
                        this.<>1__state = 1;  
                        return true;  
                    Label_004B:  
                        this.<>1__state = -1;  
                        this.<count>5__1++;  
                    }  
                    break;  
  
                case 1:  
                    goto Label_004B;  
            }  
            return false;  
        }  
  
        [DebuggerHidden]  
        void IEnumerator.Reset()  
        {  
            throw new NotSupportedException();  
        }  
  
        void IDisposable.Dispose()  
        {  
        }  
  
        object IEnumerator<object>.Current  
        {  
            [DebuggerHidden]  
            get  
            {  
                return this.<>2__current;  
            }  
        }  
  
        object IEnumerator.Current  
        {  
            [DebuggerHidden]  
            get  
            {  
                return this.<>2__current;  
            }  
        }  
    }  
}  

可以看出,其实就是把 原始的GetCounter函数里的内容,都放入到了MoveNext中了。

类似的,也可以看下面这个例子(来自《深入理解C#》)

注意:前面的例子,包含yield return 的函数的返回值都是 IEnumerator,但是这个例子返回的是 IEnumeratable。)

下面实现了一个CreateEnumerable函数,但是此函数中 并没有实现GetEnumerator,以及实现MoveNext 和 Current函数,但是我们却可以在Main函数中,进行调用,说明其实是编译器给你生成了一个实现了这些接口的类。

如果是CreateEnumerable返回 IEnumerable,编译器实现的类应该是继承自IEnumerator IEnumeratable

如果是CreateEnumerable返回 IEnumerator,编译器实现的类应该是只继承自IEnumerator 

class IteratorWorkflow
{
    static readonly string Padding = new string(' ', 30);
    // 也可以返回 IEnumerator<int>,相应的Main里面就不需要先获取 iterable 了。
    static IEnumerable<int> CreateEnumerable()
    {
        Console.WriteLine("{0}Start of CreateEnumerable()", Padding);
        for (int i = 0; i < 3; i++)
        {
            Console.WriteLine("{0}About to yield {1}", Padding, i);
            // !!!遇到yield return 代码就停止执行,再下一次调用MoveNext的时候 继续执行。
            yield return i;
            Console.WriteLine("{0}After yield", Padding);
        }

        Console.WriteLine("{0}Yielding final value", Padding);
        yield return -1;

        Console.WriteLine("{0}End of GetEnumerator()", Padding);
    }

    static void Main()
    {
        // 此处不会真正调用CreateEnumerable()函数
        IEnumerable<int> iterable = CreateEnumerable();
        IEnumerator<int> iterator = iterable.GetEnumerator();

        Console.WriteLine("Starting to iterate");
        // 也可以用foreach来替代。这里展示了foreach的类似过程。
        while (true)
        {
            Console.WriteLine("Calling MoveNext()...");
            //  此处开始真正调用CreateEnumerable()函数
            bool result = iterator.MoveNext();
            Console.WriteLine("... MoveNext result={0}", result);
            if (!result)
            {
                break;
            }
            Console.WriteLine("Fetching Current...");
            Console.WriteLine("... Current result={0}", iterator.Current);
        }
    }
}

自己运行一下上述代码,看输出信息就知道了 编译器自动生成的迭代器的 工作原理了。

关于yield的一些注意事项:

1.可以在方法属性索引器中使用 yield 来实现迭代器

 使用yield,函数返回类型必须是 IEnumberable<T>、IEnumberable、IEnumberator<T>和IEnumberator中的一个。

 使用yield,函数参数不能是 ref out

2. try 和 catch 语句里面不能出现yield return 
3.不能在匿名方法中用迭代器代码块,也就是不能使用yield


4.yield break 相当于普通方法中的return

    class YieldBreak
    {
        static IEnumerable<int> CountWithTimeLimit(DateTime limit)
        {
            for (int i = 1; i <= 100; i++)
            {
                if (DateTime.Now >= limit)
                {
                    // 大于2s,迭代块就退出了
                    yield break;
                }
                yield return i;
            }
        }

        static void Main()
        {
            DateTime stop = DateTime.Now.AddSeconds(2);
            foreach (int i in CountWithTimeLimit(stop))
            {
                Console.WriteLine("Received {0}", i);
                Thread.Sleep(300);
            }
        }
    }

也就是除了for里面的条件,想增加一个结束条件,就使用yield break即可。


5.finally块 被 实现为 Dispose

(这里为啥要写finally块,都忘记了为啥了,下次再遇到了再补充说明吧)

应该是编译器实现的类 实现了 IDisposable接口  上述链接中的例子也是这样,所以退出迭代器块的时候,就会调用Dispose函数。

class YieldBreakAndTryFinally
{
    static IEnumerable<int> CountWithTimeLimit(DateTime limit)
    {
        try
        {
            for (int i = 1; i <= 100; i++)
            {
                if (DateTime.Now >= limit)
                {
                    yield break;
                }
                yield return i;
            }
        }
        finally
        {
            // 停止使用迭代器块会执行finally,即Dispose的调用会触发finally块的执行。
            Console.WriteLine("Stopping!");
        }
    }

    static void Main()
    {
        DateTime stop = DateTime.Now.AddSeconds(2);
        foreach (int i in CountWithTimeLimit(stop))
        {
            Console.WriteLine("Received {0}", i);
            if(i > 3)
            {
              return;
            }
            Thread.Sleep(300);
        }
    }
}


6.实际开发中实现迭代器的例子

1.timetable 是一个时刻表的类。

方法一:用for循环 来表示时刻表中的每一天。

for (DateTime day = timetable.StartDate; day <= timetable.EndDate; day = day.AddDays(1))
{}

 方法二:实现迭代器,用foreach来表示时刻表中的每一天。

foreach (var day in timetable.DateRange)
{

}
// 需要实现DateRange是一个可迭代的集合,timetable类中的一个属性。
public IEnumerable <DateTime> DateRange
{
  get
  {
      // 原始的代码被放到了这里。
      for (DateTime day = timetable.StartDate; day <= timetable.EndDate; day = day.AddDays(1))
      {
          yield return day;
      }
  }
}

2.迭代文件中的行

原始方法:每次读文件中的行都需要的写如下的类似代码,繁琐

// reader阅读器 需要释放 
using(TextReader reader = File.OpenText(filename))
{
  string line;
  while((line = reader.ReadLine())!= null)
  {
    //针对line进行某些处理。
  }
}

改进方法基础迭代器版本

foreach (string line in ReadLines("test.txt"))
{
  //针对line进行某些处理。
}

public static IEnumerable<string> ReadLines(string filename)
{
  // using 扮演了 try/finally 块的角色。具体实现了什么呢?
  // 在到达文件末尾或在中途调用 IEnumerabtor<string>.Dispose方法时,将进入finally块。
  // 同样,是把原始的代码移动到这里
  using (TextReader reader = File.OpenText(filename))
  {
      string line;
      while ((line = reader.ReadLine()) != null)
      {
          yield return line;
      }
  }
}       

改进迭代器版本:

如果不是打开文本获取阅读器而是通过网络流读取文本,或者不是用utf8编码格式,应该怎么扩展

(通过网络流读取文本 是怎么样的?)
所以直接传入 TextReader参数 不好,这里传入一个 Func<TextReader> 类型的参数。

public static IEnumerable<string> ReadLines(Func<TextReader> provider)
{
  //同样调用了 using
  using (TextReader reader = provider())
  {
      string line;
      while ((line = reader.ReadLine()) != null)
      {
          yield return line;
      }
  }
}
// 调用上面
public static IEnumerable<string> ReadLines(string filename, Encoding encoding)
{
  // 复习:使用了匿名方法,捕获所在方法的参数。
  return ReadLines(delegate
  {
    return File.OpenText(filename, encoding);
  });
}
// 调用上面,默认使用utf8
public static IEnumerable<string> ReadLines(string filename)
{
  return ReadLines(filename, Encoding.UTF8);
}

参考:

《深入理解C#》

c# yield关键字原理详解 - blueberryzzz - 博客园


Unity中的协程StartCoroutine

《Unity3D游戏开发》

先举个例子

跟上面C#不同,这里的yield return后面不是一个数值,而是一个类型为继承自YieldInstruction的类。(new WaitForSeconds(1f) 就是一个继承了 YieldInstruction类型的对象实例 )

而下面这个表示的就是每过1s创建一个Cube,创建100个Cube

除了yield return后面跟的数字不一样以外,使用方法也不一样,这里用的是StartCoroutine。

public class Script_04_04 : MonoBehaviour
{
    private void Start()
    {

        StartCoroutine(CreateCube());
    }
    IEnumerator CreateCube()
    {
        for(int i = 0; i < 100; i++)
        {
            GameObject.CreatePrimitive(PrimitiveType.Cube).transform.position = Vector3.one * i;
            yield return new WaitForSeconds(1f);
        }
    }
}

那么StartCoroutine的函数签名是?

StartCoroutine是 Monobehavior类的函数,有3个重载函数

public Coroutine StartCoroutine(string methodName);
public Coroutine StartCoroutine(IEnumerator routine);
public Coroutine StartCoroutine(string methodName, [DefaultValue("null")] object value);

StartCoroutine的第一个和第三个methodName 。都是一个返回类型是IEnumerator的方法。

StartCoroutine的返回类型是Coroutine类。

所以三个重载函数其实本质上都需要:

输入参数:IEnumerator 迭代器

返回参数: Coroutine(继承自YieldInstruction,看这个名字就跟Yield有关系。)这个Coroutine返回类型用在yield return的后面。

    public sealed class Coroutine : YieldInstruction
    {
        ~Coroutine();
    }

总结:Coroutine继承自YieldInstruction,可以用在yield return后面

除了这个Coroutine类,还是其他的一些类也是继承自YieldInstruction,WaitForSeconds,WaitForFixedUpdate类,WaitForEndOfFrame类等

用在yield return 后面,是要suspend its execution (yield) until the given YieldInstruction finishes.

StartCoroutine本质

代码运行到StartCoroutine以后,Unity在每帧都会去轮训这个yield条件是否满足。满足则继续往下执行。每帧就执行yield之前的,或者yield后面的条件是否满足。

(unity的脚本是单线程的,用的就是协程Coroutine来模拟多线程。)

看下面网址的例子:

Unity - Scripting API: Coroutine

例子1:阻塞start函数。start函数里面调用一个yield return。start函数也可以是返回IEnumerator的。

例子2:开启一个协程。正常运行start函数

协程的作用:定时器

从第一个例子,1s创建一个cube就可以看出协程可以方便的做一个定时器的事件,例子中是1s创建一个。

1)定时器类型1——定多少时间以后做什么事情

为了把定时器逻辑具体的Mono脚本剥离,可以定义一个单例的定时器类,用来实现多少时间以后做什么事情。

这样不管在Mono脚本还是普通脚本中都可以调用了

//开启定时器:可以同时开启多个,记得取消对应的就行

Coroutine coroutine=WaitTimeManager.WaitTime(5f,delegate{Debug.Log("等待5s后回调");});

//取消

WaitTimeManager.CancelWait(ref coroutine);

具体实现 

public class WaitTimeManager
{

    private static TaskBehavior m_Task;
    static WaitTimeManager()
    {
        GameObject go = new GameObject("#WaitTimeManager#");
        GameObject.DontDestroyOnLoad(go);
        m_Task = go.AddComponent<TaskBehavior>();
    }


    static public Coroutine WaitTime(float time, UnityAction callback)
    {
        return m_Task.StartCoroutine(Coroutine(time, callback));
    }

    static public void CancelWait(ref Coroutine coroutine)
    {
        if (coroutine != null)
        {
            m_Task.StopCoroutine(coroutine);
            coroutine = null;
        }
    }


    static IEnumerator Coroutine(float time, UnityAction callback)
    {
        yield return new WaitForSeconds(time);
        if(callback != null)
        {
            callback();
        }
    }

    // 仅仅为了使用其StartCoroutine函数,和 StopCoroutine
    class TaskBehavior : MonoBehaviour { };
}

2)定时器类型2——定每隔多少时间就做一次某个事情。

10s结束。每过1s回调一次。

yield return new CustomWait(10f,1f,delegate() {Debug.Log("每过1s回调一次"); });

背后实现CustomWait类

public class CustomWait:CustomYieldInstruction
{
    public override bool keepWaiting
    {
        get
        {
            // 时间和次数可能不是那么得准确
            if (Time.time - m_StartTime >= m_Time)
            {
                return false;
            } else if (Time.time - m_LastTime >= m_Interval){
                m_LastTime = Time.time;
                m_IntervalCallBack();
            }
            return true; 
        }
    }

    // 记录时间用来判断是否要结束协程,以及一个回调函数接口
    float m_StartTime;
    float m_LastTime;
    float m_Interval;
    float m_Time;

    UnityAction m_IntervalCallBack;

    public CustomWait(float time, float interval, UnityAction callback)
    {
        // 经过多少时间取消协程
        m_Time = time;
        // 每隔多少时间回调一次
        m_Interval = interval;
        // 回调函数
        m_IntervalCallBack = callback;

        // 辅助记录时间
        m_StartTime = Time.time;
        m_LastTime = Time.time;
    }
}

Unity如何处理协程Coroutine的

Unity在每一帧(Frame)都会去处理对象上的协程。Unity主要是在Update后去处理协程

通过设置MonoBehaviour脚本的enabled对协程是没有影响的,但如果 gameObject.SetActive(false) 则已经启动的协程则完全停止了,即使在Inspector把gameObject 激活还是没有继续执行。也就说协程虽然是在MonoBehvaviour启动的(StartCoroutine)但是协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响,但跟MonoBehaviour脚本一样受gameObject 控制,也应该是和MonoBehaviour脚本一样每帧“轮询” yield 的条件是否满足。

Unity在每帧做的工作就是:调用 协程(迭代器)MoveNext() 方法,如果返回 true ,就从当前位置继续往下执行。

When you make a call to StartCoroutine(IEnumerator) you are handing the resulting IEnumerator to the underlying unity engine.

StartCoroutine() builds a Coroutine object, runs the first step of the IEnumerator and gets the first yielded value. That will be one of a few things, either "break", some YieldInstruction like "Coroutine", "WaitForSeconds", "WaitForEndOfFrame", "WWW", or something else unity doesn't know about. The Coroutine is stored somewhere for the engine to look at later.

  • WWW - after Updates happen for all game objects; check the isDone flag. If true, call the IEnumerator's MoveNext() function;
  • WaitForSeconds - after Updates happen for all game objects; check if the time has elapsed, if it has, call MoveNext();
  • null or some unknown value - after Updates happen for all game objects; Call MoveNext()
  • WaitForEndOfFrame - after Render happens for all cameras; Call MoveNext

MoveNext returns false if the last thing yielded was "break" of the end of the function that returned the IEnumerator was reach. If this is the case, unity removes the IEnumerator from the coroutines list.

One common misconception cleared up: Coroutines do not execute in parallel to your code. They run in the same thread as everything else in your scripts, so editing the same values in Coroutines and Update is safe.

yield 后面可以有的表达式:

       a) null - the coroutine executes the next time that it is eligible

       b) WaitForEndOfFrame - the coroutine executes on the frame, after all of the rendering and GUI is complete

       c) WaitForFixedUpdate - causes this coroutine to execute at the next physics step, after all physics is calculated

       d) WaitForSeconds - causes the coroutine not to execute for a given game time period

       e) WWW - waits for a web request to complete (resumes as if WaitForSeconds or null)

       f) Another coroutine - in which case the new coroutine will run to completion before the yielder is resumed

值得注意的是 WaitForSeconds()受Time.timeScale影响,当Time.timeScale = 0f 时,yield return new WaitForSecond(x) 将不会满足。

http://dsqiu.iteye.com/blog/2029701

http://dsqiu.iteye.com/blog/2049743

Coroutines – More than you want to know | Twisted Oak Studios Blog

  • 19
    点赞
  • 66
    收藏
  • 打赏
    打赏
  • 1
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
评论 1

打赏作者

ivy_0709

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值