访问闭包警告中的foreach变量


85

我收到以下警告:

访问闭包中的foreach变量。使用不同版本的编译器进行编译时,可能会有不同的行为。

这是我的编辑器中的样子:

悬停弹出窗口中出现上述错误消息

我知道如何解决此警告,但我想知道为什么会收到此警告?

这是关于“ CLR”版本的吗?与“ IL”有关吗?



1
TL; DR答案:在查询表达式的末尾添加.ToList()或.ToArray(),它将摆脱警告
JoelFan 2015年

Answers:


136

此警告分为两部分。首先是...

访问闭包中的foreach变量

...这本身不是无效的,但乍看之下是违反直觉的。做正确的事也很困难。(以至于我在下面链接到的文章都将其描述为“有害”。)

进行查询,请注意,您摘录的代码基本上是C#编译器(在C#5之前)为foreach1生成的代码的扩展形式:

我[不]理解[以下内容无效]的原因:

string s; while (enumerator.MoveNext()) { s = enumerator.Current; ...

好吧,在语法上是有效的。而且,如果您在循环中所做的所有事情都使用了values那么一切都很好。但是,封闭s会导致违反直觉的行为。看下面的代码:

var countingActions = new List<Action>();

var numbers = from n in Enumerable.Range(1, 5)
              select n.ToString(CultureInfo.InvariantCulture);

using (var enumerator = numbers.GetEnumerator())
{
    string s;

    while (enumerator.MoveNext())
    {
        s = enumerator.Current;

        Console.WriteLine("Creating an action where s == {0}", s);
        Action action = () => Console.WriteLine("s == {0}", s);

        countingActions.Add(action);
    }
}

如果运行此代码,将获得以下控制台输出:

Creating an action where s == 1
Creating an action where s == 2
Creating an action where s == 3
Creating an action where s == 4
Creating an action where s == 5

这就是您的期望。

要查看您可能不希望看到的内容,请在上面的代码之后立即运行以下代码:

foreach (var action in countingActions)
    action();

您将获得以下控制台输出:

s == 5
s == 5
s == 5
s == 5
s == 5

为什么?因为我们创建了五个功能完全相同的功能:打印s(我们已经结束的)值。实际上,它们是相同的功能(“打印s”,“打印s”,“打印s” ...)。

在使用它们的那一刻,它们完全按照我们的要求:打印的值s。如果您查看的最后一个已知值s,则会看到它是5。因此,我们s == 5在控制台上打印了五次。

正是我们所要的,但可能不是我们想要的。

警告的第二部分...

使用不同版本的编译器进行编译时,可能会有不同的行为。

...就是它了。从C#5开始,编译器会生成不同的代码,以防止通过发生这种情况foreach

因此,以下代码在不同版本的编译器下将产生不同的结果:

foreach (var n in numbers)
{
    Action action = () => Console.WriteLine("n == {0}", n);
    countingActions.Add(action);
}

因此,它还会产生R#警告:)

上面的第一个代码段在所有版本的编译器中都将表现出相同的行为,因为我没有使用foreach(相反,我已将其扩展为C#5之前的编译器所采用的方式)。

这是CLR版本吗?

我不太确定您在这里问什么。

埃里克·利珀特(Eric Lippert)的帖子说,更改发生在“ C#5”中。所以大概您必须以.NET 4.5或更高版本为目标 使用C#5或更高版本的编译器来获取新的行为,而在此之前的所有内容都将获取旧的行为。

但需要明确的是,它是编译器的功能,而不是.NET Framework版本。

与IL相关吗?

不同的代码会产生不同的IL,因此从某种意义上说,生成的IL会有后果。

1 foreach是比您在评论中发布的代码更常见的构造。该问题通常是通过使用引起的foreach,而不是通过手动枚举引起的。因此,对foreachC#5进行的更改有助于防止此问题,但不能完全避免。


7
我实际上已经尝试过使用同一目标(.Net 3.5)在不同的编译器上获得不同结果的foreach循环。我使用了VS2010(我相信它依次使用与.net 4.0关联的编译器)和VS2012(我相信是.net 4.5编译器)。从原则上讲,这意味着如果您使用VS2013并编辑一个针对.Net 3.5的项目,并在安装了较旧框架的构建服务器上进行构建,则您在计算机上看到的程序结果可能与部署的构建结果不同。
Ykok 2014年

好的答案,但不确定“ foreach”的相关性。使用手动枚举,甚至是简单的for(int i = 0; i <collection.Size; i ++)循环,都不会发生这种情况吗?闭包超出范围似乎是一个问题,或更准确地说,是人们了解闭包超出其在内部定义的范围时的行为方式的问题。
布拉德(Brad)

foreach这里的东西来自于问题的内容。没错,它可以以多种更普遍的方式发生。
ta.speot.is

1
为什么R#仍然警告我,它不读取目标框架,我将其设置为4.5。
Johnny_D 2014年

1
“因此,您必须以.NET 4.5或更高版本为目标”此语句不正确。您所针对的.NET版本对此没有影响,如果您使用C#5(VS 2012或更高版本)进行编译,则在.NET 2.0、3.5和4中行为也会更改。这就是为什么您仅在.NET 4.0或更早版本上收到此警告的原因,如果目标是4.5,则不会收到该警告,因为您不能在C#4或更早版本的编译器上编译4.5。
Scott Chamberlain

12

第一个答案很好,所以我想我只想添加一件事。

之所以收到警告,是因为在示例代码中,reflectedModel被分配了一个IEnumerable,它将仅在枚举时进行评估,并且如果您将reflectingModel分配给范围更广的对象,则枚举本身可能在循环之外发生。

如果你改变了

...Where(x => x.Name == property.Value)

...Where(x => x.Name == property.Value).ToList()

则在foreach循环内将为reflectedModel分配一个确定的列表,因此您不会收到警告,因为枚举肯定会在循环内发生,而不是在循环外发生。


我读了很多很长的解释,这些解释并没有解决我的问题,然后简短的解释了。谢谢!
查尔斯·克莱顿

我读了接受的答案,只是想着“如果不绑定变量,它将如何闭合?” 但是现在我知道这是关于评估发生的时间,谢谢!
杰罗姆

是的,这是显而易见的通用解决方案。速度慢,占用大量内存,但我认为在所有情况下都可以100%正常工作。
Al Kepp '16

8

块作用域变量应解决该警告。

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.