C#迭代器——由foreach说开去

C#迭代器——由foreach说开去

foreach在数组集合的遍历时会被经常用到,例如:

   string[] strs = new string[] { "red", "green","blue" };   
   foreach (var item in strs)
        {
            Console.WriteLine(item);
        }  

使用foreach做遍历确实很方便,然而并不是每一种类型都能使用foreach进行遍历操作,只有实现了IEnumerable接口的类(也叫做可枚举类型)才能进行foreach的遍历操作,集合和数组已经实现了这个接口,所以能进行foreach的遍历操作

IEnumerable和IEnumerator

IEnumerable叫做可枚举接口,它的成员只有一个

  • GetEnumerator()

返回一个枚举器对象,即实现了IEnumerator接口的类的实例,实现IEnumerator接口的枚举器包含3个函数成员:

  • Current属性
  • MoveNext()方法
  • Reset()方法

Current属性为只读属性,返回枚举序列中的当前位置,MoveNext()把枚举器的位置前进到下一项,返回布尔值,新的位置若是有效的,返回true,否则返回false,ReSet()将位置重置为原始状态。
举个例子,实现自己的可枚举类型,先实现IEnumerator枚举器类型:

    class Enumerator1 : IEnumerator {
    private int _position = -1;
    private T[] t;
    public Enumerator1(T[] a) {
        t = new T[a.Length];
        for (int i = 0; i < t.Length; i++) {
            t[i] = a[i];
        }
    }    
    public T Current
    {
        get
        {
            if (_position == -1) {
                throw new InvalidOperationException();
            }
            return t[_position];
        }
    }
    object IEnumerator.Current
    {
        get
        {
            if (_position == -1)
            {
                throw new InvalidOperationException();
            }
            return t[_position];
        }
    }
    public void Dispose()
    {         
    }
       public bool MoveNext()
    {
        Console.WriteLine("Call Move Next");
        if (_position >= t.Length)
            return false;
        else
        {
            _position++;
            return _position < t.Length;
        }           
    }
    public void Reset()
    {
        _position = -1;
    }
}    

这里实现的是IEnumerator泛型枚举器类,泛型枚举器类的实现中包含一个非泛型的Current属性,返回Object对象的引用,枚举器开始位置的状态为第一个元素之前,_position为-1,因此当我们取到Current之前,应先调用一次MoveNext(),才能得到可枚举项中的第一项,当位置到达可枚举项的最后一项,再次调用MoveNext(),会返回false,取值过程终止,除非位置被重置。
再来实现IEnumerable枚举类型:

class Enumeratable1 : IEnumerable
{
    private T[] t;
    public Enumeratable1(T[] a) {
        t = new T[a.Length];
        for (int i = 0; i < t.Length; i++) {
            t[i] = a[i];
        }
    }
    public IEnumerator GetEnumerator()
    {
        return new Enumerator1(t);
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return new Enumerator1(t);
    }
}`

这里实现的IEnumerable泛型枚举类,泛型枚举类的实现中包含一个非泛型的GetEnumerator方法,GetEnumerator返回的为之前创建的Enumerator1的泛型构造类的实例,使用泛型数组作为参数传入构造函数,创建Enumerator1的枚举器对象。
下面使用foreach对Enumerable1的实例进行遍历:

  static void Main() {
        string[] strs = new string[] { "red", "green", "blue" };
        int[] nums = new int[] { 1, 2, 3, 4, 5 };
        Enumeratable1 strEnum = new Enumeratable1(strs);       
        Enumeratable1 numEnum = new Enumeratable1(nums);
        foreach (var temp in strEnum)
        {
            Console.WriteLine(temp);
        }
        Console.ReadKey();
    }     

输出结果:


在实现MoveNext()方法时,添加了一行输出,从输出结果可以看出,用foreach做遍历时,先调用了MoveNext()方法,再调用Current属性获得当前项,当取到最后一项时,并没有退出枚举过程,而是再次调用MoveNext()方法,返回值为false后,才退出枚举过程。

迭代器 和 yield return

当我们自己实现可枚举类型,在foreach中完成遍历时,需要手动实现IEnumerable枚举接口IEnumerator枚举器接口。C#2.0以后提供了更为简单的方式去创建可枚举类型和枚举器,那就是迭代器,迭代器会生成枚举类型和枚举器类型。
举个栗子:

 public  IEnumerator Color() //迭代器
    {
        yield return "red";
        yield return "green";
        yield return "blue";
    }   

以上代码就是一个迭代器创建枚举器的过程,如果手动创建枚举器,是需要实现IEnumerator的接口的,而在这里,并没有实现CurrentMoveNext(),Reset这些成员,取而代之使用的是yield return语句,简单来说迭代器就是使用一个或多个yield return语句告诉编译器创建枚举器类,yield return语句指定了枚举器中下一个可枚举项,完整的代码:

class Program{   
    static void Main()
    {
        ColorEnumerable color = new ColorEnumerable();
        foreach (var item in color)
        {
            Console.WriteLine(item);
        }
        Console.ReadKey();
    }}  
class ColorEnumerable {
    public IEnumerator GetEnumerator()
    {
        return Color();
    }
    public IEnumerator Color()
    {
        yield return "red";
        yield return "green";
        yield return "blue";
    }}   

ColorEnumeable类实现了GetEnumerator()方法,为可枚举类型。迭代器可以返回一个枚举器类型,也可以返回一个可枚举类型,例如:

 public IEnumerable Color() {
        yield return "red2";
        yield return "green2";
        yield return "blue2";
    }  

以上代码是利用迭代器返回一个IEnumerable的可枚举类型,完整的代码如下:

class Program{   
    static void Main()
    {
        ColorEnumerable2 color2 = new ColorEnumerable2();
        foreach (var item in color2.Color())
        {
            Console.WriteLine(item);
        }
        Console.ReadKey();
    }} 
  class ColorEnumerable2 {
    public IEnumerable Color() {
        yield return "red2";
        yield return "green2";
        yield return "blue2";
    }
}   

在foreach的语句中用的是color2.Color(),而不是像上一个例子中直接使用的color,这是因为ColorEnumerable类中已经公开实现了GetEnumerator()方法,但在ColorEnumerable2类中并没有公开实现,所以不能使用foreach直接遍历ColorEnumerable2类的实例,但ColorEnumerable2的方法Color()使用迭代器创建了可枚举类,已经实现了GetEnumerator()方法,所以使用color2.Color()可以得到可枚举类在foreach中进行遍历。我们对ColorEnumerable2作如下修改:

class ColorEnumerable2 {
    public IEnumerator GetEnumerator() {
        return Color().GetEnumerator();
    }
    public IEnumerable Color() {
        yield return "red2";
        yield return "green2";
        yield return "blue2";
    }
}   

便可以在foreach的语句中直接使用ColorEnumerable2的实例进行遍历了。总结一下就是:

  • yield reutrn可以根据返回类型告诉编译器创建可枚举类或者是枚举器
  • yield return 语句指定了枚举器对象中的下一个可枚举项

迭代器的执行顺序

当创建完一个可枚举类型(不管是手动实现或是使用迭代器创建),枚举器实际上可以看做是包含4个状态的状态机(参考C#图解教程):

  • Before 首次调用MoveNext()时的状态,初始位置在第一个可枚举项之前
  • Running 调用MoveNext后进入该状态。在Running状态下,枚举器检测下一项的位置,当遇到yield return时会进入挂起状态,直到遇到下一个MoveNext(),当遇到yield break时或迭代器结束时,会退出状态
  • Suspended 暂时挂起状态,等待下一次MoveNext()调用时唤醒
  • After 已经到最后的位置,没有可枚举项

如下图所示:


通过代码来具体看这个过程:

class Program
{
    static readonly String Padding = new String(' ', 35);
    static IEnumerable Enumerable() {
        try {
            Console.WriteLine("{0}进入Enumerable方法", Padding);
            for (int i = 0; i < 4; i++)
            {
                Console.WriteLine("{0}yield return {1} 开始",Padding,i);
                yield return i;
                Console.WriteLine("{0}yield return {1} 结束",Padding,i);
            }
            Console.WriteLine("{0}最后一个yield return 开始", Padding);
            yield return -1;
            Console.WriteLine("{0}最后一个yield return 结束", Padding);
        }
        finally
        {
            Console.WriteLine("{0}Enumerable方法结束",Padding);
        }
    }
    static void Main(string[] args)
    {
        IEnumerable ie = Enumerable();
        IEnumerator ietor = ie.GetEnumerator();
        Console.WriteLine("开始迭代");  
        while (true) {
            Console.WriteLine("调用MoveNext()之前");
            bool result=ietor.MoveNext();
            Console.WriteLine("调用MoveNext()之后,值为:{0}", result);
            if (!result) {
                break;
            }
            Console.WriteLine("Current值为:{0}",ietor.Current);
        }
        Console.ReadKey();
    }
}   

为了调用枚举器中的MoveNext()方法,使用while循环进行遍历,输出的结果:

从输出结果可以看出:

  • 直到调用MoveNext()后才进入迭代器的方法
  • 遇到yield return,进入暂停挂起状态,,回到开始调用的地方,获取Current,位置被移到下一项
  • 再次遇到MoveNext()方法,暂停挂起的状态回到运行状态,此时再次遇到yield return,重复暂停挂起的过程
  • 当进行到最后一项时,迭代器并没有立即结束,而是又执行了一次MoveNext()方法,此时MoveNext()返回的值为false,执行finally中的代码,退出迭代器

迭代器的退出除了当迭代器状态结束时会发生,使用yield break也会使迭代器退出:

 try {
            Console.WriteLine("{0}进入Enumerable方法", Padding);
            for (int i = 0; i < 4; i++)
            {
                Console.WriteLine("{0}yield return {1} 开始",Padding,i);
                yield return i;
                Console.WriteLine("{0}yield return {1} 结束",Padding,i);
            }
            yield break;
            Console.WriteLine("{0}最后一个yield return 开始", Padding);
            yield return -1;
            Console.WriteLine("{0}最后一个yield return 结束", Padding);
        }
        finally
        {
            Console.WriteLine("{0}Enumerable方法结束", Padding);
        }   

输出结果:

在迭代器的foreach循环结束后使用yield break语句,yield break 语句后的迭代过程并没有进行,而是把MoveNext()的返回值设置为false,调用MoveNext()方法后进入到finally中,结束迭代过程。关于迭代器的退出:

  • 在正常迭代进行到最后一项时,迭代没有结束MoveNext()的值被设置为false,再次调用MoveNext(),结束迭代过程
  • 遇到yield break语句,MoveNext()的值被设置为false,再次调用MoveNext(),结束迭代过程
  • yield return语句只是让迭代器状态暂停挂起,等待下一次的MoveNext()调用,继续进行yield之后的语句

Unity中的StartCoroutine与yield return

使用过Unity的延时WaitForSeconds()的朋友们一定不会对StartCoroutine和yield return这两个关键字感到陌生。UnityGems.com给出了协程的定义:
A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done.
即协程是一个分部执行,遇到条件(yield return 语句)会挂起,直到条件满足才会被唤醒继续执行后面的代码。
先来个例子:

bool isStartCall = false;
bool isUpdateCall = false;
bool isLateUpdateCall = false;
void Start () {
    if (!isStartCall) {
        Debug.Log("Start Call Begin");
        StartCoroutine("StartCoroutines");
        Debug.Log("Start Call End");
        isStartCall = true;
    }
}
IEnumerator StartCoroutines() {
    Debug.Log("StartCoroutine Call Begin");
    yield return null;
    Debug.Log("StartCoroutine Call End");
}
void Update() {
    if (!isUpdateCall) {
        Debug.Log("Update Call Begin");
        StartCoroutine("UpdateCoroutines");
        Debug.Log("Update Call End");
        isUpdateCall = true;
    }     
}
IEnumerator UpdateCoroutines()
{
    Debug.Log("UpdateCoroutine Call Begin");
    yield return null;
    Debug.Log("UpdateCoroutine Call End");
}
void LateUpdate() {
    if (!isLateUpdateCall) {
        Debug.Log("LateUpdate Call Begin");
        StartCoroutine("LateUpdateCoroutines");
        Debug.Log("LateUpdate Call End");
        isLateUpdateCall = true;
    }
}
IEnumerator LateUpdateCoroutines()
{
    Debug.Log("LateUpdateCoroutine Call Begin");
    yield return null;
    Debug.Log("LateUpdateCoroutine Call End");
}  

Start(),Update()LateUpdate()中分别使用StartCoroutine()方法,传入的参数是一个实现了枚举器的方法名的字符串,枚举器的实现使用的迭代器,通过yield return语句完成,下面是输出结果:

输出结果中包含较多的信息,先来看看Start()方法中的StartCoroutine的执行顺序:

  • 输出Start Call Begin
  • 进入StartCoroutines()方法,输出StartCoroutine Call Begin
  • 遇到yield return语句,暂时挂起,回到调用的地方
  • 输出Start Call End
  • Start()方法执行完毕

当Start()方法执行完毕的时候,程序并没有回到StartCoroutines()内,而是按照上面的过程走完Update(),LataUpdate()方法之后才回到StartCoroutines()内,之后是UpdateCoroutines()和LateUpdateCoroutines(),这是为什么呢?这里涉及到Coroutine的执行顺序,Unity中,Start(),Update(),LateUpdate()依次执行,这没什么好说的,Coroutine的执行是在每一帧的LateUpdate()方法之后,这里需要注意一下,在每个Coroutine的yield return语句中传入的是null,程序会停留一帧,因此是在直到LateUpdate()方法执行完之后才会进入到StartCoroutine()的方法内。
之前我们看到的迭代器退出是在迭代器执行到MoveNext()方法时返回的值为false的情况下才会退出,但是刚才的示例代码中并没有看到MoveNext()的取用,原因在于这个调用和检测过程由Unity完成了:

  • Unity在每帧调用 协程(迭代器)MoveNext() 方法,如果返回 true ,就从当前位置继续往下执行。
  • 在每个Coroutines里只有一个可列举项,Unity在LateUpdate()执行完后开始调用每个Coroutines中的MoveNext(),(执行一次后MoveNext()的返回值已经为fasle),Coroutines结束暂停挂起,回到运行状态,输出Debug.Log()后,下一帧再次调用MoveNext(),由于已经是false值,Coroutines退出迭代。

Unity中在创建IEnumerator枚举器时,yield return 后面可以有如下表达式:

  • null - the coroutine executes the next time that it is eligible
  • WaitForEndOfFrame - the coroutine executes on the frame, after all of the rendering and GUI is complete
  • WaitForFixedUpdate - causes this coroutine to execute at the next physics step, after all physics is calculated
  • WaitForSeconds - causes the coroutine not to execute for a given game time period
  • WWW - waits for a web request to complete (resumes as if WaitForSeconds or null)
  • Another coroutine - in which case the new coroutine will run to completion before the yielder is resumed

其中有关延时的处理在StartCoroutine()中被特殊处理了,Unity在每一帧调用这几个类的MoveNext之前会先判断延时的条件是否已经满足,如果满足,才会执行后面的MoveNext()操作,如果不满足,会跳出迭代器的方法下一帧再次进行检测,直到满足延时条件,执行MoveNext()操作,当MoveNext()的值为false,退出迭代器。关于被特殊处理的过程,可以参考Coroutine,你究竟干了什么这篇博客。

Coroutine的停止

Unity中Coroutine停止掉的方法,通过将附有脚本的gameObject.SetActive(false)可以停止掉Corotine,就像C#中在迭代器使用yield break一样,不过再重新设置为gameObject.SetActive(true)时,Coroutine并不会恢复。例如,将Update()中的代码修改为:

void Update() {
    if (!isUpdateCall) {
        Debug.Log("Update Call Begin");
        StartCoroutine("UpdateCoroutines");
        Debug.Log("Update Call End");
        isUpdateCall = true;
        gameObject.SetActive(false);
    }       

添加了一行gameObject.SetActive(false),再来看一下输出结果:

当代码执行到UpdateCoroutines()时,输出”UpdateCoroutine Call Begin”,遇到yield return迭代器进入暂停挂起状态,回到Update()中,输出”Update Call End”,遇到gameObject.SetActive(false),此时开启的Coroutines全部终止,后面的正常方法LateUpdate()和三个Coroutine里面的输出无法进行。
这是gameObject.SetActive(false)放在Coroutine的外部,如果放在内部会是什么情况?修改之前的代码,将gameObject.SetActive(false)放到UpdateCoroutines里面:

 IEnumerator UpdateCoroutines()
{
    Debug.Log("UpdateCoroutine Call Begin");
    yield return null;
    gameObject.SetActive(false);
    Debug.Log("UpdateCoroutine Call End");
}  

再来看看输出结果:

输出结果是11行,只有最后一个LateUpdateCoroutines中yield return后面的LateUpdateCoroutine Call End没有并执行,通过之前的分析过程,这里的输出结果并不难解释,当代码再次回到UpdateCorotineds的yield return语句之后,前面的输出应该是“StartCoroutines Call End”,这里需要注意的是,尽管 gameObject.SetActive(false)在Debug.Log(“UpdateCoroutine Call End”)语句之前,但是”UpdateCoroutine Call End”还是会输出,也就是说,当gameObject.SetActive(false)在迭代器内部时,代码并不是一走到 gameObject.SetActive(false);就立即终止,而是会离开迭代器之前将离开前的代码执行完。

参考

  • Coroutine,你究竟干了什么
  • Unity协程(Coroutine)原理深入剖析

你可能感兴趣的:(学习笔记,C#)