Reputation: 9403
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
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
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