c# - 这种闭包组合行为是 C# 编译器错误吗?

标签 c# lambda closures .net-4.6

我在调查一些奇怪的对象生命周期问题时,发现了 C# 编译器的这种非常令人费解的行为:

考虑以下测试类:

class Test
{
    delegate Stream CreateStream();

    CreateStream TestMethod( IEnumerable<string> data )
    {
        string file = "dummy.txt";
        var hashSet = new HashSet<string>();

        var count = data.Count( s => hashSet.Add( s ) );

        CreateStream createStream = () => File.OpenRead( file );

        return createStream;
    }
}

编译器生成以下内容:

internal class Test
{
  public Test()
  {
    base..ctor();
  }

  private Test.CreateStream TestMethod(IEnumerable<string> data)
  {
    Test.<>c__DisplayClass1_0 cDisplayClass10 = new Test.<>c__DisplayClass1_0();
    cDisplayClass10.file = "dummy.txt";
    cDisplayClass10.hashSet = new HashSet<string>();
    Enumerable.Count<string>(data, new Func<string, bool>((object) cDisplayClass10, __methodptr(<TestMethod>b__0)));
    return new Test.CreateStream((object) cDisplayClass10, __methodptr(<TestMethod>b__1));
  }

  private delegate Stream CreateStream();

  [CompilerGenerated]
  private sealed class <>c__DisplayClass1_0
  {
    public HashSet<string> hashSet;
    public string file;

    public <>c__DisplayClass1_0()
    {
      base..ctor();
    }

    internal bool <TestMethod>b__0(string s)
    {
      return this.hashSet.Add(s);
    }

    internal Stream <TestMethod>b__1()
    {
      return (Stream) File.OpenRead(this.file);
    }
  }
}

原始类包含两个 lambda:s => hashSet.Add( s )() => File.OpenRead( file ) .第一个关闭局部变量 hashSet ,第二个关闭局部变量 file .但是,编译器会生成一个闭包实现类 <>c__DisplayClass1_0包含 hashSetfile .因此,返回的 CreateStream委托(delegate)包含并保持对 hashSet 的引用应该可用于 GC 一次的对象 TestMethod返回。

在我遇到此问题的实际场景中,一个非常大的(即 >100mb)对象被错误地包含在内。

我的具体问题是:

  1. 这是一个错误吗?如果不是,为什么这种行为被认为是可取的?

更新:

C# 5 规范 7.15.5.1 说:

When an outer variable is referenced by an anonymous function, the outer variable is said to have been captured by the anonymous function. Ordinarily, the lifetime of a local variable is limited to execution of the block or statement with which it is associated (§5.1.7). However, the lifetime of a captured outer variable is extended at least until the delegate or expression tree created from the anonymous function becomes eligible for garbage collection.

这在某种程度上似乎是开放的解释,并且没有明确禁止 lambda 捕获它不引用的变量。然而,this question涵盖了一个相关的场景,@eric-lippert 认为这是一个错误。恕我直言,我认为编译器提供的组合闭包实现是一个很好的优化,但该优化不应该用于编译器可以合理检测到的 lambda 可能具有超出当前堆栈帧的生命周期。


  1. 如何在不完全放弃使用 lambda 的情况下针对此进行编码?尤其是我如何防御性地编写代码,以便将来的代码更改不会突然导致同一方法中其他一些未更改的 lambda 开始包含它不应该包含的内容?

更新:

我提供的代码示例是出于必要而设计的。显然,将 lambda 创建重构为一个单独的方法可以解决这个问题。我的问题不是关于设计最佳实践(@peter-duniho 也涵盖了)。相反,鉴于 TestMethod 的内容就目前而言,我想知道是否有任何方法可以强制编译器排除 createStream来自组合闭包实现的 lambda。


郑重声明,我的目标是 .NET 4.6 和 VS 2015。

最佳答案

Is this a bug?

没有。编译器符合此处的规范。

Why is this behaviour considered desirable?

这是不可取的。这是非常不幸的,正如你在这里发现的,正如我在 2007 年所描述的那样:

http://blogs.msdn.com/b/ericlippert/archive/2007/06/06/fyi-c-and-vb-closures-are-per-scope.aspx

自 C# 3.0 以来,C# 编译器团队已考虑在每个版本中修复此问题,而且它的优先级一直不够高。考虑在 Roslyn github 站点上输入一个问题(如果还没有;很可能有)。

我个人希望看到这个问题得到解决;就目前而言,这是一个很大的“问题”。

How do I code against this without abandoning the use of lambdas all together?

变量就是被捕获的东西。完成后,您可以将 hashset 变量设置为 null。然后唯一消耗的内存是变量的内存,四个字节,而不是它所指的东西的内存,它将被收集。

关于c# - 这种闭包组合行为是 C# 编译器错误吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33894603/

相关文章:

c# - 转换 base64 字符串时出错

c# - 如何在某些任务非常昂贵的任务中负载平衡并行性?

C#:用属性代替公共(public)变量不是很麻烦吗?

python - 如何强制 Python 在循环内创建新变量/新范围?

php - 使 PHP 闭包函数对 PHP 5.2 安全

c# - 如何使用 OleDbDataAdapter 只读取 Excel 文件的第一行?

c# - 将 2 个参数的 Lambda 表达式转换为 1 个参数的 Lambda 表达式(指定一个参数)

Java 使用 lambda 表达式对一个对象中的字段求和

c++11 信号系统

swift - Swift 中 auto 和 escaping 闭包的区别和目的是什么?