86

我收到以下警告:

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

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

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

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

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

4

3 回答 3

136

这个警告有两个部分。第一个是...

在闭包中访问 foreach 变量

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

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

我 [不] 明白为什么 [以下内容] 无效:

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

嗯,它在语法上是有效的。如果你在循环中所做的只是使用 的s那么一切都很好。但是关闭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不是通过手动枚举出现的。这就是为什么 C# 5 中的更改foreach有助于防止这个问题,但不是完全的。

于 2013-02-17T02:18:55.627 回答
12

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

您收到警告是因为,在您的示例代码中,reflectModel 被分配了一个 IEnumerable,它只会在枚举时被评估,如果您将 reflectModel 分配给范围更广的东西,枚举本身可能发生在循环之外.

如果你改变了

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

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

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

于 2015-06-03T01:07:23.427 回答
8

块范围的变量应该解决警告。

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
于 2016-05-26T12:34:06.350 回答