C# 使用IEnumerable,yield 返回结果,同时使用foreach时,在循环内修改变量的值无效(一)

在项目中遇到了一个比较奇怪的问题,在foreach循环中修改列表的值后没有生效,后面使用时还是获取列表时的值,原因是因为使用了 yield return 。下面让我们来探究下其中的原因:

首先来看下 yield return 官方的解释

使用 yield return 语句可一次返回一个元素。
通过 foreach 语句或 LINQ 查询来使用迭代器方法。 foreach 循环的每次迭代都会调用迭代器方法。 迭代器方法运行到 yield return 语句时,会返回一个 expression,并保留当前在代码中的位置。 当下次调用迭代器函数时执行从该位置重新启动。

在使用中效果就是:当对使用yield return的函数的返回值进行遍历的时候,不是一次获取所有返回结果,而是一次只返回一个结果,当循环到下一个变量时,从yield return位置重新开始执行代码,到再次yield return返回下一个结果。来看下代码(注释中的数字是代码执行的顺序,多次执行时用 | 隔开):

var list = GetEnumerable(); //此时并不会执行GetEnumerable()
foreach (var test in list) //1|4|7 当执行到in时才会进入GetEnumerable()方法
{
    test.atr1 = 0;//3|6|9
}

public IEnumerable<ListTest> GetEnumerable()
{
    for (int i = 0; i < 2; i++)
    {
        yield return new ListTest()
        {
            atr1 = i + 1
        };//2|5|8
    }
}

从上面的代码中可以看出当真正使用待列表中变量时,才会真正执行代码获取该变量。
yield return的下一个特点就是在循环中修改变量值不生效的原因:

var list = GetEnumerable(); //此时并不会执行GetEnumerable()
foreach (var test in list) //1|4|7 当执行到in时才会进入GetEnumerable()方法
{
    test.atr1 = 0;//3|6|9
}
foreach (var test in list) // 10|13|16
{
    test.atr1 = 0;//12||15|18
}

public IEnumerable<ListTest> GetEnumerable()
{
    for (int i = 0; i < 2; i++)
    {
        yield return new ListTest()
        {
            atr1 = i + 1
        };//2|5|8|11|14|17
    }
}

这里其实有个奇怪的点,虽然我们全部循环了list一次,获取了所有的返回结果,但是再次循环list时,还是会执行GetEnumerable()方法,重新回去所有的结果,也就是说我们每次使用函数返回结果的时候都是一个新的变量,所以在循环中修改变量修改的只是这次的变量,并不会影响到以后的变量。
这里其实我们可以想到另一个问题,就是函数内部有比较耗时的操作而且函数调用的地方又多次使用了返回结果,这时就会非常的影响性能,下面是一个实验:

Stopwatch watch = new Stopwatch();
watch.Start();
var list = GetEnumerable();
for (int i = 0; i < 100; i++)
{
    list.ToList().ForEach(o => Console.Write(o.atr1));               
}
watch.Stop();
Console.WriteLine("花费时间:" + watch.ElapsedMilliseconds);

public IEnumerable<ListTest> GetEnumerable()
{
    Thread.Sleep(10);//加上这一步来加长时间,来使效果更明显

    for (int i = 0; i < 2; i++)
    {
        yield return new ListTest()
        {
            atr1 = i + 1
        };
    }
}

下面是函数执行的时间:
用时
所以多次使用时时很耗性能的。
如果我们不想要这种效果的话怎么解决呢?
一个方法是不用yield return,还一个方法就是在获取到返回结果时把它转换为List:

var list = GetEnumerable().ToList();

我们再看下时间:
用时
这种用法时函数只用执行一次就可以了,可以很大程度的节省性能,也可以解决在循环中改变变量的值的问题。
MSDN对yield的解释如下;

在语句中使用 yield 关键字,表示在该关键字所在的方法、运算符或 get 访问器是迭代器。 通过使用 yield 定义迭代器,可在实现自定义集合类型的 IEnumerable 和 IEnumerator 模式时无需其他显式类。

yield的目的是为了更方便的使用迭代器,当返回结果只是用一次时还是很好用的。

原文地址:https://www.cnblogs.com/hanxl/p/IEnumerable_yield.html