variable scope question

    void Start()
    {
        StartCoroutine(LerpAnchor(buttonSlots[2], buttonSlots[0].homeAnchorPos, 2f));
    }

    IEnumerator LerpAnchor(ButtonSlot selectedSlot, Vector2 endPos, float duration)
    {
        RectTransform lerpedTransform = selectedSlot.groupTransform;
        Vector2 currentPosition = Vector2.zero;
        Debug.Log(currentPosition);
        StartCoroutine(Interpolate(lerpedTransform.anchorMax, endPos, 2f, (x) => currentPosition = x));

        Debug.Log(currentPosition);

        yield return null;
    }
   
    IEnumerator Interpolate(Vector2 startPos, Vector2 endPos, float duration, Vector2Callback callBack)
    {

        Vector2 currentAnchorPos = Vector2.one;
        Debug.Log(currentAnchorPos);
        callBack(currentAnchorPos);
        yield return null;
    }

(sorry for not taking the time to make a stand-alone example)

I just want to make sure I understand. This anonymous function does actually change the Vector2 in LerpAnchor’s scope(!) This is great.

I was expecting it not to. I was expecting to need the Vector2 currentPos to be in the class scope (which kinda sucks).

I guess this delegate / lambda expression / anonymous function can change currentPosition because its scope is derived from LerpAnchor’s scope, where it is defined.

This might be more obvious if I hadn’t skipped writing anonymous functions and gone straight to the abbreviated syntax. Am I thinking correctly? Looks like Vector2’s are passed by value, so I know it’s not a reference thing.

So what’s actually happening here when you create an ‘anonymous function’ is that the compiler creates an implicit class nested in the class you’ve written.

That class is used as the ‘state’ of the anonymous function.

Any variables you access in the anonymous function are actually stored as a class member field of this implicit nested class.

This way when it’s called as a callback, it’s modifying the same class field on an referenced object in the heap rather than on the stack by value.

We can see this here:

using System;

namespace Console01
{
    class ExampleCallback
    {

        public void DoWork()
        {
            float someValue = 0f;
            IndirectModify((v) => someValue += v);
            Console.WriteLine(someValue);
        }

        private void IndirectModify(System.Action<float> callback)
        {
            callback(5f);
        }

    }
}

Now lests see what the IL (the intermediate language C# is compiled into for the mono/.net runtime to process… it’s not exactly human friendly readable):

// Type: Console01.ExampleCallback
// Assembly: Console01, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: D88AEA8D-32B1-48F4-88AB-8A9AB2DC7DE1
// Location: C:\Users\lordo\Documents\Visual Studio 2015\Projects\Console01\Console01\bin\Debug\Console01.exe
// Sequence point data from C:\Users\lordo\Documents\Visual Studio 2015\Projects\Console01\Console01\bin\Debug\Console01.pdb

.class private auto ansi beforefieldinit
  Console01.ExampleCallback
    extends [mscorlib]System.Object
{

  .class nested private sealed auto ansi beforefieldinit
    '<>c__DisplayClass0_0'
      extends [mscorlib]System.Object
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public float32 someValue

    .method public hidebysig specialname rtspecialname instance void
      .ctor() cil managed
    {
      .maxstack 8

      IL_0000: ldarg.0      // this
      IL_0001: call         instance void [mscorlib]System.Object::.ctor()
      IL_0006: nop        
      IL_0007: ret        

    } // end of method '<>c__DisplayClass0_0'::.ctor

    .method assembly hidebysig instance void
      '<DoWork>b__0'(
        float32 v
      ) cil managed
    {
      .maxstack 8

      // [15 35 - 15 49]
      IL_0000: ldarg.0      // this
      IL_0001: ldarg.0      // this
      IL_0002: ldfld        float32 Console01.ExampleCallback/'<>c__DisplayClass0_0'::someValue
      IL_0007: ldarg.1      // v
      IL_0008: add        
      IL_0009: stfld        float32 Console01.ExampleCallback/'<>c__DisplayClass0_0'::someValue
      IL_000e: ret        

    } // end of method '<>c__DisplayClass0_0'::'<DoWork>b__0'
  } // end of class '<>c__DisplayClass0_0'

  .method public hidebysig instance void
    DoWork() cil managed
  {
    .maxstack 3
    .locals init (
      [0] class Console01.ExampleCallback/'<>c__DisplayClass0_0' 'CS$<>8__locals0'
    )

    IL_0000: newobj       instance void Console01.ExampleCallback/'<>c__DisplayClass0_0'::.ctor()
    IL_0005: stloc.0      // 'CS$<>8__locals0'

    // [13 9 - 13 10]
    IL_0006: nop        

    // [14 13 - 14 34]
    IL_0007: ldloc.0      // 'CS$<>8__locals0'
    IL_0008: ldc.r4       0.0
    IL_000d: stfld        float32 Console01.ExampleCallback/'<>c__DisplayClass0_0'::someValue

    // [15 13 - 15 51]
    IL_0012: ldarg.0      // this
    IL_0013: ldloc.0      // 'CS$<>8__locals0'
    IL_0014: ldftn        instance void Console01.ExampleCallback/'<>c__DisplayClass0_0'::'<DoWork>b__0'(float32)
    IL_001a: newobj       instance void class [mscorlib]System.Action`1<float32>::.ctor(object, native int)
    IL_001f: call         instance void Console01.ExampleCallback::IndirectModify(class [mscorlib]System.Action`1<float32>)
    IL_0024: nop        

    // [16 13 - 16 42]
    IL_0025: ldloc.0      // 'CS$<>8__locals0'
    IL_0026: ldfld        float32 Console01.ExampleCallback/'<>c__DisplayClass0_0'::someValue
    IL_002b: call         void [mscorlib]System.Console::WriteLine(float32)
    IL_0030: nop        

    // [17 9 - 17 10]
    IL_0031: ret        

  } // end of method ExampleCallback::smile:oWork

  .method private hidebysig instance void
    IndirectModify(
      class [mscorlib]System.Action`1<float32> callback
    ) cil managed
  {
    .maxstack 8

    // [20 9 - 20 10]
    IL_0000: nop        

    // [21 13 - 21 26]
    IL_0001: ldarg.1      // callback
    IL_0002: ldc.r4       5
    IL_0007: callvirt     instance void class [mscorlib]System.Action`1<float32>::Invoke(!0/*float32*/)
    IL_000c: nop        

    // [22 9 - 22 10]
    IL_000d: ret        

  } // end of method ExampleCallback::IndirectModify

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [mscorlib]System.Object::.ctor()
    IL_0006: nop        
    IL_0007: ret        

  } // end of method ExampleCallback::.ctor
} // end of class Console01.ExampleCallback

Note how the very first declaration inside our ExampleCallback class is this:

  .class nested private sealed auto ansi beforefieldinit
    '<>c__DisplayClass0_0'
      extends [mscorlib]System.Object
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public float32 someValue

A nested class named this weird gobbily goope ‘<>c__DisplayClass0_0’ with a float32 field named ‘someValue’.

Back in our C# code that’s the variable in the DoWork method.

This ‘<>c__DisplayClass0_0’ class is instantiated in our DoWork method, and that field is what actually gets modified.

We could manually do this ourselves in plain C# like so:

using System;

namespace Console01
{
    class ExampleCallbackUnwrapped
    {

        private class DoWorkStateObject
        {
            public float someValue;

            public void Invoke(float v)
            {
                someValue += v;
            }
        }

        public void DoWork()
        {
            DoWorkStateObject obj = new DoWorkStateObject();
            obj.someValue = 0f;
            IndirectModify(obj.Invoke);
            Console.WriteLine(obj.someValue);
        }

        private void IndirectModify(System.Action<float> callback)
        {
            callback(5f);
        }

    }
}

So basically… our value type is getting boxed and referenced via this weird nested class.

1 Like

AH! I feel bad with such a short reply to such a long post. Thank you for taking the time.

The last example clears this up very well, thank you. I get it now. I don’t think I totally would have either, and it’s good that I know that.

I was only using a lambda instead of a System.Action delegate because I wanted to learn how to actually use my own lambda expressions. It’s taken three days, two books, countless hours of bookmarking, googling, staring into space, yelling at my cat, and throwing expo markers off my balcony, but I think I’ve got it.

Okay, it’s creating this reference on the heap: I wouldn’t want to have two hundred mans all doing this to make their brains work (which I wasn’t planning on) but it’s a thought. These parameter delegate expressions actually have a significant run-time cost, as each one is (literally?) instantiating a class object.

Significant in the sense that they’d add up anyway, PC’s will handle my four UI buttons just fine.

OFFTOPIC PS I wish I had gone to a real college with an actual program… I’m glad I had one solid instructor, but there was no way he was gonna be able to cover all this alone. Oh well, I learned how to learn… I don’t think it was worth it though :frowning: the American public colleges all do a good job but they each only have certain good programs and man I just wasn’t old enough or mature enough to be making those decisions. I digress.

So yeah thanks again.

Do note, passing in a delegate also allocates on the heap as well. A ‘System.Action’ object is just that, an object.

Though yes, an anonymous method (or lambda) will take up slightly more space for both the delegate AND the state information.

Just don’t go overboard (several created every frame), and you should be good.

Also:

I’m a community college dropout myself. I was working 80 hours a week at a gas station (mostly overnight), sleeping in the cooler, and paying out of pocket just to go to the shitty local community college here in Florida.

After a sprint of several funerals (friends and family fallen woe to drugs), and missing so many classes I had to keep withdrawing and retaking classes (you miss 2 of 4 exams, there really isn’t any passing after that… the dean started getting suspicious, had to bring in death certificates to prove it), I was getting bored in class. So I’d just start reading further into my text books for the following semester.

Before I knew it I was 2 years ahead of where I actually was in class just by reading/learning it myself… in just a matter of months.

I subsequently dropped out and said screw it. Sat down with all the programming books I could get my hands on, joined multiple forums like this here (though unity wasn’t a thing back then, it was other forums then). And I just dedicated what time I would have been doing schoolwork to that instead.

Worked out just fine for me.

And is why I’m still an active member on forums all over the web.

Communities like these are a great place to share and learn.

Yep! All just different incantations of the same spell :wink:

I always wanted to make games. I’m gonna do it, with a lot of patience. (and CGcookie)