dlf
dlf

Reputation: 9403

Why *doesn't* ReSharper tell me “implicitly captured closure”?

This question and its answers explain the concept of implicitly captured closures very well. However, I occasionally see code that seems like it should generate the warning in question that in fact does not. For example:

public static void F()
{
  var rnd1 = new Random();
  var rnd2 = new Random();
  Action a1 = () => G(rnd1);
  Action a2 = () => G(rnd2);
}

private static void G(Random r)
{
}

My expectation was that I'd be warned that a1 implicitly captures rnd2, and a2 implicitly captures rnd1. However, I get no warning at all (the code in the linked question does generate it for me though). Is this a bug on ReSharper's part (v9.2), or does implicit capture not occur here for some reason?

Upvotes: 5

Views: 728

Answers (2)

AGB
AGB

Reputation: 2226

When the compiler captures the local variables used by the anonymous methods in the closure, it does so by generating a helper class specific to the scope of the method containing delegate definition. One one such method exists per scope, even when there are multiple delegates in that scope. See Eric Lippert's explanation here.

Borrowing from your example, consider the following program:

using System;

namespace ConsoleApplication
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            F();
        }

        public static void F()
        {
            var rnd1 = new Random();
            var rnd2 = new Random();
            Action a1 = () => G(rnd1);
            Action a2 = () => G(rnd2);
        }

        private static void G(Random r)
        {
        }
    }
}

Taking a look for the IL generated by the compiler we see the following for the implementation of F():

.method public hidebysig static 
    void F () cil managed 
{
    // Method begins at RVA 0x205c
    // Code size 56 (0x38)
    .maxstack 2
    .locals init (
        [0] class ConsoleApplication.Program/'<>c__DisplayClass1_0' 'CS$<>8__locals0',
        [1] class [mscorlib]System.Action a1,
        [2] class [mscorlib]System.Action a2
    )

    IL_0000: newobj instance void ConsoleApplication.Program/'<>c__DisplayClass1_0'::.ctor()
    IL_0005: stloc.0
    IL_0006: nop
    IL_0007: ldloc.0
    IL_0008: newobj instance void [mscorlib]System.Random::.ctor()
    IL_000d: stfld class [mscorlib]System.Random ConsoleApplication.Program/'<>c__DisplayClass1_0'::rnd1
    IL_0012: ldloc.0
    IL_0013: newobj instance void [mscorlib]System.Random::.ctor()
    IL_0018: stfld class [mscorlib]System.Random ConsoleApplication.Program/'<>c__DisplayClass1_0'::rnd2
    IL_001d: ldloc.0
    IL_001e: ldftn instance void ConsoleApplication.Program/'<>c__DisplayClass1_0'::'<F>b__0'()
    IL_0024: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
    IL_0029: stloc.1
    IL_002a: ldloc.0
    IL_002b: ldftn instance void ConsoleApplication.Program/'<>c__DisplayClass1_0'::'<F>b__1'()
    IL_0031: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
    IL_0036: stloc.2
    IL_0037: ret
} // end of method Program::F

Note the first IL instruction: IL_0000: newobj instance void ConsoleApplication.Program/'<>c__DisplayClass1_0'::.ctor() which is calling the default constructor of the compiler-generated helper class--the one that's responsible for capturing the local variables in the closure.

Here is the IL for the compiler-generated helper class:

.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass1_0'
    extends [mscorlib]System.Object
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Fields
    .field public class [mscorlib]System.Random rnd1
    .field public class [mscorlib]System.Random rnd2

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x20a3
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: nop
        IL_0007: ret
    } // end of method '<>c__DisplayClass1_0'::.ctor

    .method assembly hidebysig 
        instance void '<F>b__0' () cil managed 
    {
        // Method begins at RVA 0x20ac
        // Code size 13 (0xd)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld class [mscorlib]System.Random ConsoleApplication.Program/'<>c__DisplayClass1_0'::rnd1
        IL_0006: call void ConsoleApplication.Program::G(class [mscorlib]System.Random)
        IL_000b: nop
        IL_000c: ret
    } // end of method '<>c__DisplayClass1_0'::'<F>b__0'

    .method assembly hidebysig 
        instance void '<F>b__1' () cil managed 
    {
        // Method begins at RVA 0x20ba
        // Code size 13 (0xd)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld class [mscorlib]System.Random ConsoleApplication.Program/'<>c__DisplayClass1_0'::rnd2
        IL_0006: call void ConsoleApplication.Program::G(class [mscorlib]System.Random)
        IL_000b: nop
        IL_000c: ret
    } // end of method '<>c__DisplayClass1_0'::'<F>b__1'

} // end of class <>c__DisplayClass1_0

Notice that this helper class has fields for both rnd1 and rnd2.

The "final" implementation of F() at the IL-level is similar to the following:

public static void F()
{
    var closureHelper = new ClosureHelper();
    closureHelper.rnd1 = new Random();
    closureHelper.rnd2 = new Random();
    Action a1 = closureHelper.MethodOne;
    Action a2 = closureHelper.MethodTwo;
}

Where ClosureHelper is implemented akin to:

internal class Program
{
    public class ClosureHelper
    {
         public Random rnd1;
         public Random rnd2;

         void MethodOne()
         {
              Program.G(rnd1);
         }

         void MethodTwo()
         {
              Program.G(rnd2);
         }
    }
}

As for why ReSharper doesn't warn you that there is an implicit capture happening in this case, I do not know.

Upvotes: 0

Evk
Evk

Reputation: 101633

I think Resharper for some reason cannot spot implicitly captured variables in this case. You can verify yourself with some disassembler that compiler generates single class with both rnd1 and rnd2. With your example it's not crystal clear, but let's take this example from Eric Lippert blog post (https://blogs.msdn.microsoft.com/ericlippert/2007/06/06/fyi-c-and-vb-closures-are-per-scope/) where he describes an example of dangerous implicit capture:

Func<Cheap> M() {            
    var c = new Cheap();
    var e = new Expensive();
    Func<Expensive> shortlived = () => e;
    Func<Cheap> longlived = () => c;            
    shortlived();
    // use shortlived
    // use longlived
    return longlived;
}

class Cheap {

}

class Expensive {

}

Here it's clear that longlived delegate captures over Expensive variable and it won't be collected until it dies. But (at least for me), Resharper won't warn you about this. Cannot name it "bug" though, but there is certainly a place for an improvement.

Upvotes: 4

Related Questions