Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How local variables are handled when referenced in another scope?

Tags:

c#

I've been writing things like this in my implementations:

public void SomeMethod(int someValue, List<int> someValues)
{
  Task generatedTask = null;

  {
    int anotherValue = 2;
    object valuesRef = someValues;
    generatedTask = new Task(delegate{
      anotherValue += someValue + GetSum(valuesRef);
      Console.WriteLine(anotherValue);
    });
  }

  generatedTask.Start();
}

However, I don't know exactly what's happening here...

Maybe everything was "copied" to the delegate. Or maybe, like reference types, all value types will have a copy associated to the Task delegate until it exists?

I'm just trying to understand what exactly happens in latest C# versions for performance matters.

like image 252
kbtz Avatar asked Jul 29 '13 08:07

kbtz


People also ask

What happens to a local variable when it goes out of scope?

These variables are referred to as local because they exist (and are visible) only during the lifetime of the method. They are said to have local scope. When the method ends, the variable goes out of scope and is destroyed. C# divides the world of types into value types and reference types.

How are local variables referenced?

A local variable reference in the function or block in which it is declared overrides the same variable name in the larger scope. In programming languages with only two levels of visibility, local variables are contrasted with global variables.

Can a local variable be used outside its scope?

Local Variables Variables defined within a function or block are said to be local to those functions. Anything between '{' and '}' is said to inside a block. Local variables do not exist outside the block in which they are declared, i.e. they can not be accessed or used outside that block.

What happens when a variable of reference data type goes out of scope?

Nothing physical happens. A typical implementation will allocate enough space in the program stack to store all variables at the deepest level of block nesting in the current function. This space is typically allocated in the stack in one shot at the function startup and released back at the function exit.


1 Answers

Excellent question; captured variables and closure contexts. Decompiling it shows that the current compiler creates 2 capture context objects here:

public void SomeMethod(int someValue, List<int> someValues)
{
    Task task;
    <>c__DisplayClass3 class2; // <== compiler generated type; unpronounceable
    <>c__DisplayClass1 class3; // <== compiler generated type; unpronounceable
    class3 = new <>c__DisplayClass1(); // outer-scope context
    class3.someValue = someValue;
    task = null;
    class2 = new <>c__DisplayClass3(); // <== inner-scope context
    class2.CS$<>8__locals2 = class3; // <== bind the contexts
    class2.anotherValue = 2;
    class2.valuesRef = someValues;
    task = new Task(new Action(class2.<SomeMethod>b__0));
    task.Start();
    return;
}

If your objective is to minimise context objects, you could perform the closures manually:

public void SomeMethod2(int someValue, List<int> someValues)
{
    Task generatedTask = null;
    {
        var ctx = new MyCaptureContext();
        ctx.anotherValue = 2;
        ctx.valuesRef = someValues;
        ctx.someValue = someValue;
        generatedTask = new Task(ctx.SomeMethod);
    }

    generatedTask.Start();
}

class MyCaptureContext
{
    // kept as fields to mimic the compiler
    public int anotherValue;
    public int someValue;
    public object valuesRef;
    public void SomeMethod()
    {
        anotherValue += someValue + GetSum(valuesRef);
        Console.WriteLine(anotherValue);
    }
}

You can also avoid a delegate creation per-task by caching a single delegate that passes in the state separately:

public void SomeMethod(int someValue, List<int> someValues)
{
    Task generatedTask = null;
    {
        var ctx = new MyCaptureContext();
        ctx.anotherValue = 2;
        ctx.valuesRef = someValues;
        ctx.someValue = someValue;
        generatedTask = new Task(MyCaptureContext.SomeMethod, ctx);
    }

    generatedTask.Start();
}
class MyCaptureContext
{
    // kept as fields to mimic the compiler
    public int anotherValue;
    public int someValue;
    public object valuesRef;
    public static readonly Action<object> SomeMethod = SomeMethodImpl;
    private static void SomeMethodImpl(object state)
    {
        var ctx = (MyCaptureContext)state;
        ctx.anotherValue += ctx.someValue + GetSum(ctx.valuesRef);
        Console.WriteLine(ctx.anotherValue);
    }
}

or (cleaner, IMO):

public void SomeMethod(int someValue, List<int> someValues)
{
    Task generatedTask = null;
    {
        var ctx = new MyCaptureContext();
        ctx.anotherValue = 2;
        ctx.valuesRef = someValues;
        ctx.someValue = someValue;
        generatedTask = ctx.CreateTask();
    }

    generatedTask.Start();
}
class MyCaptureContext
{
    // kept as fields to mimic the compiler
    public int anotherValue;
    public int someValue;
    public object valuesRef;
    public Task CreateTask()
    {
        return new Task(someMethod, this);
    }
    private static readonly Action<object> someMethod = SomeMethod;
    private static void SomeMethod(object state)
    {
        var ctx = (MyCaptureContext)state;
        ctx.anotherValue += ctx.someValue + GetSum(ctx.valuesRef);
        Console.WriteLine(ctx.anotherValue);
    }
}
like image 115
Marc Gravell Avatar answered Oct 20 '22 19:10

Marc Gravell