【问题标题】:Access to foreach variable in closure warning在关闭警告中访问 foreach 变量
【发布时间】:2023-03-21 09:39:01
【问题描述】:

我收到以下警告:

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

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

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

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

【问题讨论】:

标签: c# .net


【解决方案1】:

此警告分为两部分。第一个是……

在闭包中访问 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

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

警告的第二部分...

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

...就是这样。 Starting with C# 5, the compiler generates different code that "prevents" this from happening via foreach.

因此下面的代码在不同版本的编译器下会产生不同的结果:

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

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

上面我的第一个代码 sn-p 将在所有版本的编译器中表现出相同的行为,因为我没有使用 foreach(相反,我已经按照 C# 5 之前的编译器的方式对其进行了扩展)。

这是针对 CLR 版本的吗?

我不太清楚你在这里问什么。

Eric Lippert 的帖子说更改发生在“C# 5”中。所以大概你必须以 .NET 4.5 或更高版本为目标,使用 C# 5 或更高版本的编译器来获得新的行为,而在此之前的一切都会获得旧的行为。

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

与 IL 有关系吗?

不同的代码会产生不同的 IL,因此从这个意义上说,生成的 IL 会产生影响。

1foreach 是比您在评论中发布的代码更常见的构造。该问题通常是通过使用foreach 而出现的,而不是通过手动枚举。这就是为什么在 C# 5 中对 foreach 的更改有助于防止这个问题,但不是完全的。

【讨论】:

  • 我实际上已经在不同的编译器上尝试了 foreach 循环,使用相同的目标(.Net 3.5)得到不同的结果。我使用了 VS2010(我相信它又使用与 .net 4.0 相关的编译器)和 VS2012(我相信是 .net 4.5 编译器)。原则上,这意味着如果您使用 VS2013 并编辑针对 .Net 3.5 的项目并在安装了稍旧框架的构建服务器上构建它,您可能会在您的机器上看到您的程序与部署的构建不同的结果。
  • 很好的答案,但不确定“foreach”的相关性。手动枚举甚至是简单的 for (int i= 0; i
  • 这里foreach的东西来自问题的内容。你是对的,它可以以各种更普遍的方式发生。
  • 为什么 R# 仍然警告我,它不读取目标框架,我已经设置为 4.5。
  • “所以大概你必须以 .NET 4.5 或更高版本为目标” 这句话是不正确的。您所针对的 .NET 版本对此没有影响,如果您使用 C# 5(VS 2012 或更高版本)进行编译,.NET 2.0、3.5 和 4 中的行为也会发生变化。这就是为什么您只会在 .NET 4.0 或更早版本上收到此警告,如果您以 4.5 为目标,则不会收到警告,因为您无法在 C#4 或更早版本的编译器上编译 4.5。
【解决方案2】:

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

您收到警告是因为,在您的示例代码中,reflectModel 被分配了一个 IEnumerable,它只会在枚举时进行评估,如果您将 reflectModel 分配给具有范围更广。

如果你改变了

...Where(x =&gt; x.Name == property.Value)

...Where(x =&gt; x.Name == property.Value).ToList()

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

【讨论】:

  • 我读了很多很长的解释,但并没有为我解决这个问题,然后是一个简短的解释。谢谢!
  • 我阅读了接受的答案,只是想“如果它不绑定变量,它是如何关闭的?”但现在我知道这是关于何时进行评估,谢谢!
  • 是的,这是显而易见的通用解决方案。速度慢,占用大量内存,但我认为它确实 100% 适用于所有情况。
【解决方案3】:

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

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}

【讨论】:

    猜你喜欢
    • 2012-09-14
    • 1970-01-01
    • 2011-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-24
    • 1970-01-01
    • 2013-02-17
    相关资源
    最近更新 更多