Closures allow you to save the local variables of a function and access them later in a callback. Think of how lambdas can access the local variables of the function they’re declared in, even though the lambda itself is another function. Unfortunately, creating a lambda like this creates garbage for the GC to collect and you have no control over that process. Today’s article introduces an alternative that allows you to take control over the GC and still use nice, type-safe closures. Read on to learn how!

First, we have to understand a little about how closures work in C#. Consider this function:

bool Contains(int val, List<int> list)
{
	return list.Find(i => i == val) >= 0;
}

List.Find takes a delegate of type Predicate<T> which is defined as bool Predicate(T). We pass in a lambda i => i == val which satisfies that signature by taking a T/int and returning a bool. The magic is in the “closure”. Somehow the lambda function we’ve just written is able to access the val local variable of the enclosing function: Contains.

To make this possible, the C# compiler does a lot of work behind the scenes. What it actually does is create a new class with a field for each local variable the closure needs to access. It then puts the lambda function into that class as an instance function. If you use a decompiler like ILSpy on your Library/ScriptAssemblies/Assembly-CSharp.dll and look at the IL bytecode then you can see the generated class. Here’s what the class for Contains looks like (with annotations by me):

// Jackson: This is the class for the lambda in TestScript.Contains
.class nested private auto ansi sealed beforefieldinit '<Contains>c__AnonStorey0'
	extends [mscorlib]System.Object
{
	.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
		01 00 00 00
	)
	// Fields
	.field assembly int32 val // Jackson: This is the 'val' local variable from Contains
 
	// Methods
	// Jackson: This is a no-op constructor generated by the compiler
	.method public hidebysig specialname rtspecialname 
		instance void .ctor () cil managed 
	{
		// Method begins at RVA 0x2e08
		// Code size 7 (0x7)
		.maxstack 8
 
		IL_0000: ldarg.0
		IL_0001: call instance void [mscorlib]System.Object::.ctor()
		IL_0006: ret
	} // end of method '<Contains>c__AnonStorey0'::.ctor
 
	// Jackson: This is the function for the lambda in Contains. It takes an int and returns a bool.
	.method assembly hidebysig 
		instance bool '<>m__0' (
			int32 i
		) cil managed 
	{
		// Method begins at RVA 0x2e10
		// Code size 17 (0x11)
		.maxstack 2
		.locals init (
			[0] bool
		)
 
		IL_0000: ldarg.1
		IL_0001: ldarg.0
		IL_0002: ldfld int32 TestScript/'<Contains>c__AnonStorey0'::val // Jackson: It uses its val field
		IL_0007: ceq
		IL_0009: stloc.0
		IL_000a: br IL_000f
 
		IL_000f: ldloc.0
		IL_0010: ret
	} // end of method '<Contains>c__AnonStorey0'::'<>m__0'
 
} // end of class <Contains>c__AnonStorey0

Now let’s look at Contains itself, again with annotations by me:

.method private hidebysig 
	instance bool Contains (
		int32 val,
		class [mscorlib]System.Collections.Generic.List`1<int32> list
	) cil managed 
{
	// Method begins at RVA 0x2da4
	// Code size 46 (0x2e)
	.maxstack 3
	.locals init (
		[0] class TestScript/'<Contains>c__AnonStorey0',
		[1] bool
	)
 
	// Jackson: The compiler instantiates the class it made for the lambda every time
	IL_0000: newobj instance void TestScript/'<Contains>c__AnonStorey0'::.ctor()
	IL_0005: stloc.0
	IL_0006: ldloc.0
	IL_0007: ldarg.1
	// Jackson: The val local variable is copied to the val field of the lambda class
	IL_0008: stfld int32 TestScript/'<Contains>c__AnonStorey0'::val
	IL_000d: nop
	IL_000e: ldarg.2
	IL_000f: ldloc.0
	// Jackson: The instance function on the lambda class is used as the predicate
	IL_0010: ldftn instance bool TestScript/'<Contains>c__AnonStorey0'::'<>m__0'(int32)
	// Jackson: The compiler instantiates a Predicate delegate to wrap the instance function
	IL_0016: newobj instance void class [mscorlib]System.Predicate`1<int32>::.ctor(object, native int)
	IL_001b: callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1<int32>::Find(class [mscorlib]System.Predicate`1<!0>)
	IL_0020: ldc.i4.0
	IL_0021: cltates 
	IL_0023: ldc.i4.0
	IL_0024: ceq
	IL_0026: stloc.1
	IL_0027: br IL_002c
 
	IL_002c: ldloc.1
	IL_002d: ret
} // end of method TestScript::Contains

So far we’ve seen that using a closure has the following effects:

  1. Every closure lambda creates a class
  2. Using the lambda creates a new instance of the class
  3. Using the lambda copies all the local variables the lambda uses to the class instance
  4. Using the lambda creates a new delegate instance to wrap the lambda class’ instance function

Steps #2 and #4 create a total of 124 bytes of garbage because they’re instantiating new class objects. This is all code generated by the compiler, so we don’t really have any control over it. Which leads us to the point of today’s article: let’s get control.

So what if we were to manually create a lambda class? If we did that then we’d have control over when it gets instantiated and when it gets released to the GC. We could keep a single static instance of it around and reuse it or keep an object pool of them to have multiple at once. In either case we’d never release any references to them so there would never be any GC, unlike lambdas which are released automatically by compiler-generated code.

Since each lambda class needs to hold its own set of local variables, we can’t create a single class. Instead, we can create an interface similar to the delegate definition. Here’s the delegate again:

delegate bool Predicate<T>(T obj);

And here’s the interface version we’ll create:

interface IPredicate<T>
{
	bool Invoke(T obj);
}

Obviously the List class doesn’t know about the interface we just created, so we’ll have to make our own version of Find in order to replicate the Contains function above. For your own code, you can just start with the interface version and skip the delegates. So here’s a version of Find that uses IPredicate:

using System.Collections.Generic;
 
public static partial class ListExtensions
{
	public static int Find<T>(this List<T> list, IPredicate<T> pred)
	{
		for (int i = 0, count = list.Count; i < count; ++i)
		{
			if (pred(list[i])
			{
				return i;
			}
		}
		return -1;
	}
}

Now that we have that, let’s rewrite Contains in the most straightforward way:

class ContainsPredicate : IPredicate<int>
{
	public int Val;
 
	public bool Invoke(int obj)
	{
		return obj == Val;
	}
}
 
bool Contains(int val, List<int> list)
{
	ContainsPredicate pred = new ContainsPredicate { Val = val };
	return list.Find(pred) >= 0;
}

Now that we have our own class, we have control over how it gets instantiated. There’s really no need to instantiate it every time Contains is called. For example, we can hold a single static reference to one:

class TestScript
{
	private static readonly ContainsPredicate containsPredicate = new ContainsPredicate();
 
	bool Contains(int val, List<int> list)
	{
		containsPredicate.Val = val;
		return list.Find(containsPredicate) >= 0;
	}
}

Now there’s only one allocation for all calls to Contains. Since containsPredicate is readonly, it’s not even possible to release the reference to it. It will never be garbage-collected, which is great for avoiding frame spikes.

Alternatively, we could keep a pool of class instances. This is more appropriate for callback-type scenarios where you have an arbitrary number of them going at once. For simplicity, I’ll show a simple version of a pool using the same Contains function:

class TestScript
{
	private static readonly Stack<ContainsPredicate> containsPredicatePool
		= new Stack<ContainsPredicate>();
 
	bool Contains(int val, List<int> list)
	{
		// Need a predicate. Get from the pool if there are any. Otherwise create one.
		ContainsPredicate pred = containsPredicatePool.Count > 0
			? containsPredicatePool.Pop()
			: new ContainsPredicate();
 
		// Use the predicate
		pred.Val = val;
		int index = list.Find(pred) >= 0;
 
		// Done with the predicate. Put it into the pool.
		containsPredicatePool.Push(pred);
 
		return index;
	}
}

Notice that here too we never release any class instances. They always just go back into the pool, so none of them will ever be garbage-collected. Likewise, the pool itself is never released so there will be no GC for it, either.

It’s notable that we haven’t given up some key elements of delegates. We still have the type safety of generics. There’s no need to cast plain object variables to use this technique. If the predicate class is nested within another class, it can have access to its private fields. So there’s no need to make everything public to use this technique.

But there are definitely some differences between this technique with lambda closures, so let’s compare them. The major advantage of interfaces over lambdas is that you have total control over their lifecycle. You can easily make sure that they’re never garbage-collected and therefore avoid GC frame spikes. Second, calling an instance function is faster than calling a delegate, so there’s a minor speedup. Third, the code is now more straightforward because there’s nothing getting auto-generated by the compiler: we can see all of the code in the source files.

Of course the major downside is that we have to manually create our own predicate class. That means more boilerplate code to type out these classes and copy the local variables to their fields. We can forget to copy some local variables, so lambdas are less error-prone. Lambdas are also more compatible with the .NET and Unity APIs, so you won’t need to create wrappers to use them there.

As typical in programming, this technique offers a trade-off. Are you concerned about GC enough to make your own interfaces? Let me know in the comments if this is something you’d consider… or are already doing!