Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detailed Explanation of Variable Capture in Closures

I've seen countless posts on how variable capture pulls in variables for the creation of the closure, however they all seem to stop short of specific details and call the whole thing "compiler magic".

I'm looking for a clear-cut explanation of:

  1. How local variables are actually captured.
  2. The difference (if any) between capturing value types vs. reference types.
  3. And whether there is any boxing occurring with respect to value types.

My preference would be for an answer in terms of values and pointers (closer to the heart of what happens internally), though I will accept a clear answer involving values and references as well.

like image 667
DuckMaestro Avatar asked Mar 25 '11 21:03

DuckMaestro


People also ask

What is variable capture?

Variable capture occurs when macroexpansion causes a name clash: when some symbol ends up referring to a variable from another context. Inadvertent variable capture can cause extremely subtle bugs. This chapter is about how to foresee and avoid them.

What is capturing values in closures?

Capturing Values. A closure can capture constants and variables from the surrounding context in which it's defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists.

How do closures capture references to variable by default?

In Swift, closures capture the variables they reference: variables declared outside of the closure but that you use inside the closure are retained by the closure by default, to ensure they are still alive when the closure is executed.

What is capture list in closures?

Capture lists: The list of values that you want to remain unchanged at the time the closure is created. Capturing values: If you use external values inside your closure, Swift stores them alongside the closure. This way they persist even when the external part of the code no longer exists.


1 Answers

  1. Is tricky. Will come onto it in a minute.
  2. There's no difference - in both cases, it's the variable itself which is captured.
  3. Nope, no boxing occurs.

It's probably easiest to demonstrate how the capturing works via an example...

Here's some code using a lambda expression which captures a single variable:

using System;  class Test {     static void Main()     {         Action action = CreateShowAndIncrementAction();         action();         action();     }      static Action CreateShowAndIncrementAction()     {         Random rng = new Random();         int counter = rng.Next(10);         Console.WriteLine("Initial value for counter: {0}", counter);         return () =>         {             Console.WriteLine(counter);             counter++;         };     } } 

Now here's what the compiler's doing for you - except that it would use "unspeakable" names which couldn't really occur in C#.

using System;  class Test {     static void Main()     {         Action action = CreateShowAndIncrementAction();         action();         action();     }      static Action CreateShowAndIncrementAction()     {         ActionHelper helper = new ActionHelper();                 Random rng = new Random();         helper.counter = rng.Next(10);         Console.WriteLine("Initial value for counter: {0}", helper.counter);          // Converts method group to a delegate, whose target will be a         // reference to the instance of ActionHelper         return helper.DoAction;     }      class ActionHelper     {         // Just for simplicity, make it public. I don't know if the         // C# compiler really does.         public int counter;          public void DoAction()         {             Console.WriteLine(counter);             counter++;         }     } } 

If you capture variables declared in a loop, you'd end up with a new instance of ActionHelper for each iteration of the loop - so you'd effectively capture different "instances" of the variables.

It gets more complicated when you capture variables from different scopes... let me know if you really want that sort of level of detail, or you could just write some code, decompile it in Reflector and follow it through :)

Note how:

  • There's no boxing involved
  • There are no pointers involved, or any other unsafe code

EDIT: Here's an example of two delegates sharing a variable. One delegate shows the current value of counter, the other increments it:

using System;  class Program {     static void Main(string[] args)     {         var tuple = CreateShowAndIncrementActions();         var show = tuple.Item1;         var increment = tuple.Item2;          show(); // Prints 0         show(); // Still prints 0         increment();         show(); // Now prints 1     }      static Tuple<Action, Action> CreateShowAndIncrementActions()     {         int counter = 0;         Action show = () => { Console.WriteLine(counter); };         Action increment = () => { counter++; };         return Tuple.Create(show, increment);     } } 

... and the expansion:

using System;  class Program {     static void Main(string[] args)     {         var tuple = CreateShowAndIncrementActions();         var show = tuple.Item1;         var increment = tuple.Item2;          show(); // Prints 0         show(); // Still prints 0         increment();         show(); // Now prints 1     }      static Tuple<Action, Action> CreateShowAndIncrementActions()     {         ActionHelper helper = new ActionHelper();         helper.counter = 0;         Action show = helper.Show;         Action increment = helper.Increment;         return Tuple.Create(show, increment);     }      class ActionHelper     {         public int counter;          public void Show()         {             Console.WriteLine(counter);         }          public void Increment()         {             counter++;         }     } } 
like image 172
Jon Skeet Avatar answered Sep 25 '22 14:09

Jon Skeet