Is this object-lifetime-extending-closure a C# compiler bug?

C#Memory LeaksLambdaClosuresObject Lifetime

C# Problem Overview


I was answering a question about the possibility of closures (legitimately) extending object-lifetimes when I ran into some extremely curious code-gen on the part of the C# compiler (4.0 if that matters).

The shortest repro I can find is the following:

  1. Create a lambda that captures a local while calling a static method of the containing type.
  2. Assign the generated delegate-reference to an instance field of the containing object.

Result: The compiler creates a closure-object that references the object that created the lambda, when it has no reason to - the 'inner' target of the delegate is a static method, and the lambda-creating-object's instance members needn't be (and aren't) touched when the delegate is executed. Effectively, the compiler is acting like the programmer has captured this without reason.

class Foo
{
    private Action _field;

    public void InstanceMethod()
    {
        var capturedVariable = Math.Pow(42, 1);

        _field = () => StaticMethod(capturedVariable);
    }

    private static void StaticMethod(double arg) { }
}

The generated code from a release build (decompiled to 'simpler' C#) looks like this:

public void InstanceMethod()
{
   
    <>c__DisplayClass1 CS$<>8__locals2 = new <>c__DisplayClass1();

    CS$<>8__locals2.<>4__this = this; // What's this doing here?

    CS$<>8__locals2.capturedVariable = Math.Pow(42.0, 1.0);
    this._field = new Action(CS$<>8__locals2.<InstanceMethod>b__0);
}

[CompilerGenerated]
private sealed class <>c__DisplayClass1
{
    // Fields
    public Foo <>4__this; // Never read, only written to.
    public double capturedVariable;

    // Methods
    public void <InstanceMethod>b__0()
    {
        Foo.StaticMethod(this.capturedVariable);
    }
}

Observe that <>4__this field of the closure object is populated with an object reference but is never read from (there is no reason).

So what's going on here? Does the language-specification allow for it? Is this a compiler bug / oddity or is there a good reason (that I'm clearly missing) for the closure to reference the object? This makes me anxious because this looks like a recipe for closure-happy programmers (like me) to unwittingly introduce strange memory-leaks (imagine if the delegate were used as an event-handler) into programs.

C# Solutions


Solution 1 - C#

That sure looks like a bug. Thanks for bringing it to my attention. I'll look into it. It is possible that it has already been found and fixed.

Solution 2 - C#

It seems to be a bug or unnecessary:

I run you exemple in IL lang:

.method public hidebysig 
	instance void InstanceMethod () cil managed 
{
	// Method begins at RVA 0x2074
	// Code size 63 (0x3f)
	.maxstack 4
	.locals init (
		[0] class ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'   'CS$<>8__locals2'
	)

	IL_0000: newobj instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::.ctor()
	IL_0005: stloc.0
	IL_0006: ldloc.0
	IL_0007: ldarg.0
	IL_0008: stfld class ConsoleApplication1.Program/Foo ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<>4__this' //Make ref to this
	IL_000d: nop
	IL_000e: ldloc.0
	IL_000f: ldc.r8 42
	IL_0018: ldc.r8 1
	IL_0021: call float64 [mscorlib]System.Math::Pow(float64, float64)
	IL_0026: stfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
	IL_002b: ldarg.0
	IL_002c: ldloc.0
	IL_002d: ldftn instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<InstanceMethod>b__0'()
	IL_0033: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
	IL_0038: stfld class [mscorlib]System.Action ConsoleApplication1.Program/Foo::_field
	IL_003d: nop
	IL_003e: ret
} // end of method Foo::InstanceMethod

Example 2:

class Program
{
    static void Main(string[] args)
    {
    }


    class Foo
    {
        private Action _field;

        public void InstanceMethod()
        {
            var capturedVariable = Math.Pow(42, 1);

            _field = () => Foo2.StaticMethod(capturedVariable);  //Foo2

        }

        private static void StaticMethod(double arg) { }
    }

    class Foo2
    {

        internal static void StaticMethod(double arg) { }
    }


}

in cl: (Note !! now the this reference is gone !)

public hidebysig 
		instance void InstanceMethod () cil managed 
	{
		// Method begins at RVA 0x2074
		// Code size 56 (0x38)
		.maxstack 4
		.locals init (
			[0] class ConsoleApplication1.Program/Foo/'<>c__DisplayClass1' 'CS$<>8__locals2'
		)

		IL_0000: newobj instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::.ctor()
		IL_0005: stloc.0
		IL_0006: nop //No this pointer
		IL_0007: ldloc.0
		IL_0008: ldc.r8 42
		IL_0011: ldc.r8 1
		IL_001a: call float64 [mscorlib]System.Math::Pow(float64, float64)
		IL_001f: stfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
		IL_0024: ldarg.0 //No This ref
		IL_0025: ldloc.0
		IL_0026: ldftn instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<InstanceMethod>b__0'()
		IL_002c: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
		IL_0031: stfld class [mscorlib]System.Action ConsoleApplication1.Program/Foo::_field
		IL_0036: nop
		IL_0037: ret
	}

Exemple 3:

class Program
{
    static void Main(string[] args)
    {
    }

    static void Test(double arg)
    {

    }

    class Foo
    {
        private Action _field;

        public void InstanceMethod()
        {
            var capturedVariable = Math.Pow(42, 1);

            _field = () => Test(capturedVariable);  

        }

        private static void StaticMethod(double arg) { }
    }


}

in IL: (This pointer is back)

IL_0006: ldloc.0
IL_0007: ldarg.0
IL_0008: stfld class ConsoleApplication1.Program/Foo ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<>4__this' //Back again.

And in all three cases the method-b__0() - look the same:

instance void '<InstanceMethod>b__0' () cil managed 
	{
		// Method begins at RVA 0x2066
		// Code size 13 (0xd)
		.maxstack 8

		IL_0000: ldarg.0
		IL_0001: ldfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
                   IL_0006: call void ConsoleApplication1.Program/Foo::StaticMethod(float64) //Your example
                    IL_0006: call void ConsoleApplication1.Program/Foo2::StaticMethod(float64)//Example 2
		IL_0006: call void ConsoleApplication1.Program::Test(float64) //Example 3
		IL_000b: nop
		IL_000c: ret
	}

And in all 3 cases there is an reference to an static method, so it makes it more odd. So after this litle analys, i will say its an bug / for no good. !

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAniView Question on Stackoverflow
Solution 1 - C#Eric LippertView Answer on Stackoverflow
Solution 2 - C#NiklasView Answer on Stackoverflow