yield return 和 LINQ Select 之间的结果有何不同?

6

我一直认为这两种方法很相似:

public static IEnumerable<Func<int>> GetFunctions()
{
     for(int i = 1; i <= 10; i++)
         yield return new Func<int>(() => i);
}

public static IEnumerable<Func<int>> GetFunctionsLinq()
{
     return Enumerable.Range(1, 10).Select(i => new Func<int>(() => i));
}

当将它们转换为List<Func<int>>时,它们会产生不同的结果:

List<Func<int>> yieldList = GetFunctions().ToList();
List<Func<int>> linqList = GetFunctionsLinq().ToList();

foreach(var func in yieldList)
   Console.WriteLine("[YIELD] {0}", func());

Console.WriteLine("==================");

foreach(var func in linqList)
   Console.WriteLine("[LINQ] {0}", func());

输出结果为:
[YIELD] 11
[YIELD] 11
[YIELD] 11
[YIELD] 11
[YIELD] 11
[YIELD] 11
[YIELD] 11
[YIELD] 11
[YIELD] 11
[YIELD] 11
==================
[LINQ] 1
[LINQ] 2
[LINQ] 3
[LINQ] 4
[LINQ] 5
[LINQ] 6
[LINQ] 7
[LINQ] 8
[LINQ] 9
[LINQ] 10

为什么会这样?
2个回答

6

这是闭包问题。您需要在循环内部存储变量以解决此问题。

for (int i = 1; i <= 10; i++)
{
    var i1 = i;
    yield return new Func<int>(() => i1);
}

实际上, new Func<int>(() => i); 在循环中使用计数器的确切值,而不是副本。因此,在循环结束后,您始终会得到11,因为它是设置为计数器的最后一个值。


请注意,自C# 5.0起已经“修复”了这个问题。https://dev59.com/iGct5IYBdhLWcg3wa8s_ - juharr
@juharr 这是在foreach循环中进行的更改。for循环保持不变。 - Jakub Lortz
@JakubLortz,今天绝对是周五,因为我的阅读理解能力已经掉到了地上。 - juharr

1
for(int i = 1; i <= 10; i++) 中的 i 是每个循环中相同的变量,只是值不断改变。 () => i 是一个闭包。当它被调用时,它使用当前的 i 值,而不是创建 Func 时的值。
在调用返回的函数之前,您首先枚举 GetFunctions,因此每个函数中的 i 已经是 11。
如果您在从枚举器获取它们后立即调用这些函数,则会得到与 LINQ 版本相同的结果:
foreach (var f in GetFunctions())
    Console.WriteLine("[YIELD2] {0}", f());

无论如何,在循环变量上创建闭包都不是一个好主意。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接