C#’s support for closures includes lambdas ((a, b) => a+b) and delegates (delegate(int a, int b){return a+b;}). They’re extremely handy tools that many developers use on a daily basis. For example, it’s really convenient when using List.Find to pass in a lambda like item => item.Id == IdToFind. They also make great callbacks for asynchronous operations. However you’re using them, understanding how they work behind the scenes will help you understand how they behave and give you insight when optimizing your code. Today’s article delves into the topic to gain just this understanding, so read on to learn some more about closures!

A closure captures the local variables of a function for its usage. The key concept in understanding them is to define what it means to “capture” a local variable. Consider the following function that uses a lambda:

using System;
using UnityEngine;
 
public class ClosureTests
{
	// Note that 'str' is a local variable
	public Func<string> MakeDelegate(string str)
	{
		// Lambda that uses the local variable 'str'
		return () => str;
	}
 
	public void Test()
	{
		// Make the delegate
		var del = MakeDelegate("hello");
 
		// Call the delegate to get the string
		var str = del();
 
		// Print the string
		Debug.Log(str);
	}
}

Let’s start with Test. It calls MakeDelegate, passes in “hello”, and gets a Func<string> delegate back. It calls it and a string is returned. It doesn’t need to pass the string that gets returned when calling the delegate, so how does the delegate know what string to return?

The answer, of course, is in MakeDelegate. Its parameter—str—is also a local variable. The lambda it creates reads this local variable. However, when MakeDelegate returns the local variables of the function are popped off the stack. So how is the lambda able to access a local variable that isn’t on the stack when it is called later on in Test?

It’s especially helpful to think at this point how you would achieve this task without the use of lambdas or delegates that are capable of doing this for you. So if you didn’t have them, how would you be able to return a delegate that returned the local variable? The answer lies in creating a class to hold a copy of the local variable. As long as the delegate references an instance of that class, it won’t be garbage collected. The local variable copy can be safely stored there until it’s needed later. Here’s an example of how that would look:

using System;
 
public class ClosureTests
{
	public Func<string> MakeDelegate(string str)
	{
		// Make the class to hold the local variable in
		var helper = new MakeDelegateHelper();
 
		// Copy the local variable
		helper.str = str;
 
		// Return an instance method of the class as a delegate
		return helper.GetStr;
	}
}
 
class MakeDelegateHelper
{
	// Holds a copy of the local variable
	public string str;
 
	// Gets the local variable copy
	public string GetStr()
	{
		return str;
	}
}

This implementation works, but is horribly verbose. You have to type a whole new class full of boilerplate code and then maintain it as you change what the function does. For example, if you added a second string paramter to MakeDelegate then you’d have to create a second string field to MakeDelegateHelper and remember to copy the local variable to it in MakeDelegate.

To get around this, lambdas and delegates cause the compiler to automatically create a class like this one. Here’s a decompiled version showing what the compiler generated. I’ve modified it a little for readability and added some comments to help explain what’s happening.

public class ClosureTests
{
	// Helper class generated by the compiler
	[CompilerGenerated]
	private sealed class <MakeDelegate>c__AnonStorey1
	{
		// Copy of the local variable
		internal string str;
 
		// Get the copy of the local variable
		internal string <>m__3()
		{
			return this.str;
		}
	}
 
	public Func<string> MakeDelegate(string str)
	{
		// Create the helper class
		ClosureTests.<MakeDelegate>c__AnonStorey1 localCAnonStorey1
			= new ClosureTests.<MakeDelegate>c__AnonStorey1();
 
		// Copy the local variable to its field
		localCAnonStorey1.str = str;
 
		// Return its instance method as a delegate
		return new Func<string>(
			(object)localCAnonStorey1,
			__methodptr(<>m__3)
		)();
	}
}

Aside from the awful naming, the compiler has generated pretty much the exact same code we would have written. That’s great because it’s a lot less error-prone and a lot easier for us to read, write, and maintain!

That’s basically all there is to how closures are implemented. However, there are several implications of such a system. Let’s start by dispelling a myth that goes like this: closures capture local variables at the time they’re declared. Now that you’ve seen the above implementation of closures, you may understand why that’s not the case. Here’s an example illustrating why. First, here’s the lambda version:

public Func<string> MakeDelegate(string str)
{
	// Make the delegate
	Func<string> del = () => str;
 
	// Overwrite the local variable the delegate uses
	str = "something else";
 
	// Return the delegate
	return del;
}
 
var del = MakeDelegate("hello");
var str = del();
Debug.Log(str);

This prints “something else”, not “hello”. That’s because the myth is false: local variables are not captured at the time the lambda or delegate is created. To see why, let’s look at what the compiler might generate:

public Func<string> MakeDelegate(string str)
{
	// Make the class to hold the local variable in
	var helper = new MakeDelegateHelper();
 
	// Copy the local variable
	helper.str = str;
 
	// Overwrite the local variable copy
	helper.str = "something else";
 
	// Return an instance method of the class as a delegate
	return helper.GetStr;
}
 
class MakeDelegateHelper
{
	// Holds a copy of the local variable
	public string str;
 
	// Gets the local variable copy
	public string GetStr()
	{
		return str;
	}
}

As you can see, there is only one str field in the helper class. Overwriting the local variable simply overwrites that one field.

One particular manifestation of this problem that seems to come up frequently is with loops, particularly foreach. Remember that loop variables are just local variables that are getting overwritten every loop iteration and you’ll see immediately why the following code doesn’t work as intended:

class MyScript : MonoBehaviour
{
	// Simple function to fetch the text of a URL
	IEnumerator GetUrl(string url, Action<string> callback)
	{
		var www = new WWW(url);
		yield return www;
		callback(www.text);
	}
 
	void DownloadWebsites(string[] urls)
	{
		foreach (var url in urls)
		{
			var enumerator = GetUrl(
				url,
				text => Debug.Log("Text of " + url + ": " + text)
			);
			StartCoroutine(enumerator);
		}
	}
}

Notice that the lambda in DownloadWebsites captures url. However, url is a local variable that is overwritten every time the foreach loop iterates. This means that the helper class’ copy of url will be overwritten over and over and finally end up with the last string in urls. When the downloads finish, every Debug.Log will report the same URL.

The solution to this issue is to create a local variable within the loop so that the compiler will generate a helper class per iteration of the loop:

void DownloadWebsites(string[] urls)
{
	foreach (var url in urls)
	{
		// Make a copy
		var urlToGet = url;
 
		var enumerator = GetUrl(
			urlToGet,
			// Use the copy in the lambda
			text => Debug.Log("Text of " + urlToGet + ": " + text)
		);
		StartCoroutine(enumerator);
	}
}

That solves the issue, but you should still keep in mind that an object is being instantiated for every iteration of the loop. This could be potentially expensive if you have a lot of iterations of the loop. For very high iteration counts, you may want to consider alternatives to closures like lambdas and delegates.

That’s all for today’s discussion of how closures are implemented in C#. Keep this in mind as you use closures and you’ll avoid some common pitfalls as seen above. If you’ve got any tips for working with them, feel free to leave a comment!