欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C# yield关键字详解

程序员文章站 2022-07-22 19:06:10
对于yield关键字我们首先看一下msdn的解释: 如果你在语句中使用 yield 关键字,则意味着它在其中出现的方法、运算符或 get 访问器是迭代器。 通过使用 yi...

对于yield关键字我们首先看一下msdn的解释:

如果你在语句中使用 yield 关键字,则意味着它在其中出现的方法、运算符或 get 访问器是迭代器。 通过使用 yield 定义迭代器,可在实现自定义集合类型的 ienumerable ienumerator 模式时无需其他显式类(保留枚举状态的类,有关示例,请参阅 ienumerator<t>)。

yield是一个语法糖

看msdn 的解释总是让人感觉生硬难懂。其实yield关键字很好理解。首先我们对于性质有个了解。yield是一个语法糖。既然yield是在c#中的一个语法糖,那么就说明yield是对一种复杂行为的简化,就是将一段代码简化为一种简单的形式,方便我们程序员使用。

那么yield到底是对什么行为的简化。我们首先来看一下yield的使用场景。

还是来看msdn上的例子。

复制代码 代码如下:

using system;
using system.collections.generic;
using system.linq;
using system.text;
using system.threading.tasks;

namespace consoleapplication2
{
    class program
    {
        static void main(string[] args)
        {
        
            foreach (int i in power(2, 8, ""))
            {
                console.write("{0} ", i);
            }
            console.readkey();
        }


        public static ienumerable<int> power(int number, int exponent, string s)
        {
            int result = 1;

            for (int i = 0; i < exponent; i++)
            {
                result = result * number;
                yield return result;
            }
            yield return 3;
            yield return 4;
            yield return 5;
        }

    }
}

这是msdn上yield的一种使用场景。

我们首先看一下下面的power方法。该静态方法返回一个ienumerablel<int>类型的参数。按照我们平常的做法。应该对数据执行一定操作,然后return一个ienumerablel<int>类型的参数。我们把power方法改造如下:

复制代码 代码如下:

public static ienumerable<int> power(int number, int exponent, string s)
        {
            int result = 1;
            //接口不能实例化,我们这儿new一个实现了ienumerable接口的list
            ienumerable<int> example = new list<int>();
            for (int i = 0; i < exponent; i++)
            {
                result = result * number;
                (example as list<int>).add(result);
            }
            return example;
        }

这是我们平常的思路。但是这样做就有个问题。这儿要new一个list,或者任何实现了ienumerable接口的类型。这样也太麻烦了吧。要知道ienumerable是一个常用的返回类型。每次使用都要new一个list,或者其他实现了该接口的类型。与其使用其他类型,不如我们自己定制一个实现了ienumerable接口专门用来返回ienumerable类型的类型。我们自己定制也很麻烦。所以微软帮我们定制好了。这个类是什么,那就是yield关键字这个语法糖。

语法糖的实现(实现ienumerable<t>接口的类)

我们来看一下yield的反编译代码。

复制代码 代码如下:

namespace consoleapplication2
{
    using system;
    using system.collections;
    using system.collections.generic;
    using system.diagnostics;
    using system.runtime.compilerservices;

    internal class program
    {
        private static void main(string[] args)
        {
            ienumerable<int> enumerable = power(2, 8);
            console.writeline("begin to iterate the collection.");
            foreach (int num in power(2, 8))
            {
                console.write("{0} ", num);
            }
            console.readkey();
        }

        public static ienumerable<int> power(int number, int exponent)
        {
            <power>d__0 d__ = new <power>d__0(-2);
            d__.<>3__number = number;
            d__.<>3__exponent = exponent;
            return d__;
        }

        [compilergenerated]
        private sealed class <power>d__0 : ienumerable<int>, ienumerable, ienumerator<int>, ienumerator, idisposable
        {
            private int <>1__state;
            private int <>2__current;
            public int <>3__exponent;
            public int <>3__number;
            private int <>l__initialthreadid;
            public int <result>5__1;
            public int exponent;
            public int number;

            [debuggerhidden]
            public <power>d__0(int <>1__state)
            {
                this.<>1__state = <>1__state;
                this.<>l__initialthreadid = environment.currentmanagedthreadid;
            }

            private bool movenext()
            {
                switch (this.<>1__state)
                {
                    case 0:
                        this.<>1__state = -1;
                        this.<result>5__1 = 1;
                        console.writeline("begin to invoke getitems() method");
                        this.<>2__current = 3;
                        this.<>1__state = 1;
                        return true;

                    case 1:
                        this.<>1__state = -1;
                        this.<>2__current = 4;
                        this.<>1__state = 2;
                        return true;

                    case 2:
                        this.<>1__state = -1;
                        this.<>2__current = 5;
                        this.<>1__state = 3;
                        return true;

                    case 3:
                        this.<>1__state = -1;
                        break;
                }
                return false;
            }

            [debuggerhidden]
            ienumerator<int> ienumerable<int>.getenumerator()
            {
                program.<power>d__0 d__;
                if ((environment.currentmanagedthreadid == this.<>l__initialthreadid) && (this.<>1__state == -2))
                {
                    this.<>1__state = 0;
                    d__ = this;
                }
                else
                {
                    d__ = new program.<power>d__0(0);
                }
                d__.number = this.<>3__number;
                d__.exponent = this.<>3__exponent;
                return d__;
            }

            [debuggerhidden]
            ienumerator ienumerable.getenumerator()
            {
                return this.system.collections.generic.ienumerable<system.int32>.getenumerator();
            }

            [debuggerhidden]
            void ienumerator.reset()
            {
                throw new notsupportedexception();
            }

            void idisposable.dispose()
            {
            }

            int ienumerator<int>.current
            {
                [debuggerhidden]
                get
                {
                    return this.<>2__current;
                }
            }

            object ienumerator.current
            {
                [debuggerhidden]
                get
                {
                    return this.<>2__current;
                }
            }
        }
    }
}

反编译代码有三部分,其中程序的入口点   private static void main(string[] args)    power方法  public static ienumerable<int> power(int number, int exponent) 和我们自己写的代码一样,但是反编译代码中还多了一个密封类

  private sealed class <power>d__0 : ienumerable<int>, ienumerable, ienumerator<int>, ienumerator, idisposable

现在情况已经明了了。yield这个语法糖实现了一个实现 ienumerable<int>接口的类来返回我们需要到 ienumerable<int>类型的数据。

我们再看一下反编译后的power方法

复制代码 代码如下:

public static ienumerable<int> power(int number, int exponent)
        {
            <power>d__0 d__ = new <power>d__0(-2);
            d__.<>3__number = number;
            d__.<>3__exponent = exponent;
            return d__;
        }

此时就确认,的确是使用了实现枚举接口的类来返回我们需要的数据类型。

每次yield return <expression>;就会像该类的实例中添加 一条数据。当yield break;的时候停止添加。

至此yield的用法就很清楚了。当我们需要返回ienumerable类型的时候,直接yield返回数据就可以了。也不用new一个list,或其他类型。所以yield是一个典型的语法糖。

yield使用中的特殊情况

我们看到编译器将我们yield的数据添加到了一个集合中。power方法在编译器中实例化了一个实现枚举接口的类型。但是我们在power方法中写一些方法,编译器会如何处理

复制代码 代码如下:

using system;
using system.collections.generic;
using system.linq;
using system.text;
using system.threading.tasks;
namespace consoleapplication2
{
    class program
    {
        static void main(string[] args)
        {
            //这儿调用了方法。
            var test = power(2, 8, "");
            console.writeline("begin to iterate the collection.");
            //display powers of 2 up to the exponent of 8:
            foreach (int i in power(2, 8, ""))
            {
                console.write("{0} ", i);
            }
            console.readkey();
        }
        public static ienumerable<int> power(int number, int exponent, string s)
        {
            int result = 1;
            if (string.isnullorempty(s))
            {
                //throw new exception("这是一个异常");
                console.writeline("begin to invoke getitems() method");
            }

            for (int i = 0; i < exponent; i++)
            {
                result = result * number;
                yield return result;
            }
            yield return 3;
            yield return 4;
            yield return 5;
        }
    }
}


按照我们的理解当我们 var test = power(2, 8, "");的时候确实调用了power方法。此时应该程序打印console.writeline("begin to invoke getitems() method");然后继续执行 console.writeline("begin to iterate the collection.");方法。所以打印顺序应该是

 begin to invoke getitems() method

begin to iterate the collection.

但是我们运行的时候却发现

C# yield关键字详解

打印顺序和我们想象的不同。此时还是去看反编译代码。

复制代码 代码如下:

namespace consoleapplication2
{
    using system;
    using system.collections;
    using system.collections.generic;
    using system.diagnostics;
    using system.runtime.compilerservices;

    internal class program
    {
        private static void main(string[] args)
        {
            ienumerable<int> enumerable = power(2, 8, "");
            console.writeline("begin to iterate the collection.");
            foreach (int num in power(2, 8, ""))
            {
                console.write("{0} ", num);
            }
            console.readkey();
        }

        public static ienumerable<int> power(int number, int exponent, string s)
        {
            <power>d__0 d__ = new <power>d__0(-2);
            d__.<>3__number = number;
            d__.<>3__exponent = exponent;
            d__.<>3__s = s;
            return d__;
        }

        [compilergenerated]
        private sealed class <power>d__0 : ienumerable<int>, ienumerable, ienumerator<int>, ienumerator, idisposable
        {
            private int <>1__state;
            private int <>2__current;
            public int <>3__exponent;
            public int <>3__number;
            public string <>3__s;
            private int <>l__initialthreadid;
            public int <i>5__2;
            public int <result>5__1;
            public int exponent;
            public int number;
            public string s;

            [debuggerhidden]
            public <power>d__0(int <>1__state)
            {
                this.<>1__state = <>1__state;
                this.<>l__initialthreadid = environment.currentmanagedthreadid;
            }

            private bool movenext()
            {
                switch (this.<>1__state)
                {
                    case 0:
                        this.<>1__state = -1;
                        this.<result>5__1 = 1;
                        if (string.isnullorempty(this.s))
                        {
                            console.writeline("begin to invoke getitems() method");
                        }
                        this.<i>5__2 = 0;
                        while (this.<i>5__2 < this.exponent)
                        {
                            this.<result>5__1 *= this.number;
                            this.<>2__current = this.<result>5__1;
                            this.<>1__state = 1;
                            return true;
                        label_009d:
                            this.<>1__state = -1;
                            this.<i>5__2++;
                        }
                        this.<>2__current = 3;
                        this.<>1__state = 2;
                        return true;

                    case 1:
                        goto label_009d;

                    case 2:
                        this.<>1__state = -1;
                        this.<>2__current = 4;
                        this.<>1__state = 3;
                        return true;

                    case 3:
                        this.<>1__state = -1;
                        this.<>2__current = 5;
                        this.<>1__state = 4;
                        return true;

                    case 4:
                        this.<>1__state = -1;
                        break;
                }
                return false;
            }

            [debuggerhidden]
            ienumerator<int> ienumerable<int>.getenumerator()
            {
                program.<power>d__0 d__;
                if ((environment.currentmanagedthreadid == this.<>l__initialthreadid) && (this.<>1__state == -2))
                {
                    this.<>1__state = 0;
                    d__ = this;
                }
                else
                {
                    d__ = new program.<power>d__0(0);
                }
                d__.number = this.<>3__number;
                d__.exponent = this.<>3__exponent;
                d__.s = this.<>3__s;
                return d__;
            }

            [debuggerhidden]
            ienumerator ienumerable.getenumerator()
            {
                return this.system.collections.generic.ienumerable<system.int32>.getenumerator();
            }

            [debuggerhidden]
            void ienumerator.reset()
            {
                throw new notsupportedexception();
            }

            void idisposable.dispose()
            {
            }

            int ienumerator<int>.current
            {
                [debuggerhidden]
                get
                {
                    return this.<>2__current;
                }
            }

            object ienumerator.current
            {
                [debuggerhidden]
                get
                {
                    return this.<>2__current;
                }
            }
        }
    }
}

我们看到power方法

复制代码 代码如下:

public static ienumerable<int> power(int number, int exponent, string s)
        {
            <power>d__0 d__ = new <power>d__0(-2);
            d__.<>3__number = number;
            d__.<>3__exponent = exponent;
            d__.<>3__s = s;
            return d__;
        }

还是还我们没有加打印方法之前一样。我们的打印方法并没有出现在power方法中,而是被封装进了实现枚举接口的类方法  private bool movenext()中。所以方法不会立即被执行,而是在我们使用数据的时候被执行。如果对此机制不了解,就容易出现另外一些意想不到的问题。例如在power方法中添加一些验证程序,如果不符合条件就抛出一个异常。这样的异常检查不会被执行。只有我们使用数据的时候才会执行。这样就失去了检查数据的意义。

具体的例子可以看artech博主的文章

另外使用yield还有一些注意事项:

你不能在具有以下特点的方法中包含 yield return 或 yield break 语句:

匿名方法。 有关详细信息,请参阅匿名方法(c# 编程指南)。

包含不安全的块的方法。 有关详细信息,请参阅unsafe(c# 参考)。

异常处理

不能将 yield return 语句置于 try-catch 块中。 可将 yield return 语句置于 try-finally 语句的 try 块中。

yield break 语句可以位于 try 块或 catch 块,但不能位于 finally 块。

如果 foreach 主体(在迭代器方法之外)引发异常,则将执行迭代器方法中的 finally 块。