离散匿名方法共享一个类?

我在这里和Eric Lippert的Ref课一起玩。 我注意到在IL中看起来两个匿名方法都使用相同的生成类,即使这意味着类有一个额外的变量。

虽然只使用一个新的类定义似乎有点合理,但令我感到奇怪的是,只创建了一个c__DisplayClass2实例。 这似乎意味着Ref两个实例都引用相同的c__DisplayClass2并不意味着在收集vart1之前无法收集y ,这可能比joik返回后发生得晚得多? 毕竟,不能保证一些白痴不会写一个函数(直接在IL中)直接访问y通过vart1 joik返回。 也许这甚至可以用reflection而不是疯狂的IL来完成。

 sealed class Ref { public delegate T Func(); private readonly Func getter; public Ref(Func getter) { this.getter = getter; } public T Value { get { return getter(); } } } static Ref joik() { int[] y = new int[50000]; int x = 5; Ref vart1 = new Ref(delegate() { return x; }); Ref vart2 = new Ref(delegate() { return y; }); return vart1; } 

运行IL DASM确认vart1vart2都使用了__DisplayClass2 ,其中包含x和y的公共字段。 笑话的IL:

 .method private hidebysig static class Program/Ref`1 joik() cil managed { // Code size 72 (0x48) .maxstack 3 .locals init ([0] class Program/Ref`1 vart1, [1] class Program/Ref`1 vart2, [2] class Program/'c__DisplayClass2' '8__locals3', [3] class Program/Ref`1 CS$1$0000) IL_0000: newobj instance void Program/'c__DisplayClass2'::.ctor() IL_0005: stloc.2 IL_0006: nop IL_0007: ldloc.2 IL_0008: ldc.i4 0xc350 IL_000d: newarr [mscorlib]System.Int32 IL_0012: stfld int32[] Program/'c__DisplayClass2'::y IL_0017: ldloc.2 IL_0018: ldc.i4.5 IL_0019: stfld int32 Program/'c__DisplayClass2'::x IL_001e: ldloc.2 IL_001f: ldftn instance int32 Program/'c__DisplayClass2'::'b__0'() IL_0025: newobj instance void class Program/Ref`1/Func`1::.ctor(object, native int) IL_002a: newobj instance void class Program/Ref`1::.ctor(class Program/Ref`1/Func`1) IL_002f: stloc.0 IL_0030: ldloc.2 IL_0031: ldftn instance int32[] Program/'c__DisplayClass2'::'b__1'() IL_0037: newobj instance void class Program/Ref`1/Func`1::.ctor(object, native int) IL_003c: newobj instance void class Program/Ref`1::.ctor(class Program/Ref`1/Func`1) IL_0041: stloc.1 IL_0042: ldloc.0 IL_0043: stloc.3 IL_0044: br.s IL_0046 IL_0046: ldloc.3 IL_0047: ret } // end of method Program::joik 

是的,匿名方法的MS实现有效地为每个范围的范围创建一个隐藏类,它需要从中捕获变量,并从该范围捕获所有相关变量。 我相信这是为了简单起见,但确实可以不必要地增加某些对象的生命周期。

每个匿名方法捕获它实际感兴趣的变量会更优雅。但是,这可能会使生活变得更加复杂……如果一个匿名方法捕获xy ,一个捕获x ,一个捕获y ,你需要三个类:一个用于捕获x ,一个用于捕获y ,一个用于组合两个(但不仅仅是有两个变量)。 棘手的一点是,对于任何单个变量实例化,该变量需要只存在于一个位置,以便引用它的所有内容看到相同的值,无论它是什么变化。

这并没有违反规范,但它可能被认为是不幸的 – 我不知道它是否真的在现实生活中被咬过,但它肯定是可能的。

好消息是,如果C#团队决定改进这一点,他们应该能够以完全向后兼容的方式这样做,除非一些布偶依赖于不必要地延长生命周期。

乔恩当然是对的。 这通常导致的问题是:

 void M() { Expensive e = GetExpensive(); Cheap c = GetCheap(); D longLife = ()=>...c...; D shortLife = ()=>...e...; ... } 

所以我们有一个昂贵的资源,其寿命现在取决于longLife的生命周期,即使shortLife是早期收集的。

这是不幸的,但很常见。 JScript和VB中闭包的实现具有相同的问题。

我想在假设的未来版本的C#中解决它,但我不保证。 显而易见的方法是根据捕获的lambdas来识别闭包变量的等价类,并为每个等价类生成一个闭包类,而不是单个闭包类。

我们也可以通过分析写入的封闭变量来做些什么。 正如Jon指出的那样,我们目前受限于捕获变量而不是值。 如果我们确定在创建闭包后从未写入的变量,并且将这些变量设置为封闭值而不是封闭变量,那么我们的代码生成策略可以更灵活。