In reading others’ C# code I consistently see some programmers call delegates like a function—del()—and others use the Invoke method of the Delegate class: del.Invoke(). Is there any difference between the two? Is one better than the other? Today’s article finds out!

Here are today’s candidates:

// Create a delegate (using System.Action and a no-op lambda)
Action del = () => {};
 
// Call with function style
del();
 
// Call with Invoke
del.Invoke();

The “function style” version uses the overloaded () operator while the Invoke version uses the Delegate.Invoke method. Both take the arguments, if any, inside the parentheses as with normal function calls. Here’s a version with a parameter to show that:

// Create a delegate (using System.Action and a no-op lambda)
Action<int> del = i => {};
 
// Call with function style
del(3);
 
// Call with Invoke
del.Invoke(3);

Now let’s make a very small MonoBehaviour and let Unity 5.2 compile it:

using System;
 
using UnityEngine;
 
class TestScript : MonoBehaviour
{
	Action del = () => {};
	Action<int> intDel = i => {};
 
	void Start()
	{
		del();
		del.Invoke();
		intDel(3);
		intDel.Invoke(3);
	}
}

You can find the compiled DLL in your project here: Library/ScriptAssemblies/Assembly-CSharp.dll. Now let’s take that DLL and decompile it to see what differences we can find. First, let’s use ILSpy:

using System;
using UnityEngine;
 
internal class TestScript : MonoBehaviour
{
	private Action del = delegate
	{
	};
 
	private Action<int> intDel = delegate(int i)
	{
	};
 
	private void Start()
	{
		this.del();
		this.del();
		this.intDel(3);
		this.intDel(3);
	}
}

This looks almost exactly the same as our original source code. There are two trivial differences: an explicit internal access specifier on the class and the use of anonymous delegate functions instead of lambdas for the delegate fields. The main difference, and the one we care about, is how the delegates are called. As you can see, the Invoke version has been replaced with the “function call” version. Both versions that we typed have been converted into the same decompiled code.

To make sure of our results, let’s decompile the DLL again with dotPeek:

internal class TestScript : MonoBehaviour
{
  private Action del;
  private Action<int> intDel;
  [CompilerGenerated]
  private static Action <>f__am$cache2;
  [CompilerGenerated]
  private static Action<int> <>f__am$cache3;
 
  public TestScript()
  {
    if (TestScript.<>f__am$cache2 == null)
    {
      // ISSUE: method pointer
      TestScript.<>f__am$cache2 = new Action((object) null, __methodptr(<del>m__0));
    }
    this.del = TestScript.<>f__am$cache2;
    if (TestScript.<>f__am$cache3 == null)
    {
      // ISSUE: method pointer
      TestScript.<>f__am$cache3 = new Action<int>((object) null, __methodptr(<intDel>m__1));
    }
    this.intDel = TestScript.<>f__am$cache3;
    base..ctor();
  }
 
  private void Start()
  {
    this.del();
    this.del();
    this.intDel(3);
    this.intDel(3);
  }
 
  [CompilerGenerated]
  private static void <del>m__0()
  {
  }
 
  [CompilerGenerated]
  private static void <intDel>m__1(int i)
  {
  }
}

This version is a lot longer than the output we get from ILSpy, but that verbosity may reveal a difference between the two delegate call versions. Again we see the class is explicitly marked as internal but the delegate fields are no longer anonymous delegate functions. Instead, we have private static functions that are assigned to the fields in a constructor that has been generated for us. That’s all much more complex than either the code we wrote or the ILSpy decompilation, but functionally equivalent. When it comes to the actual calling of the delegates, we see the exact same story as with ILSpy: the Invoke version has been converted to use “function style”.

Finally, let’s look at the actual IL assembly code using Microsoft’s ILDASM. I’ve annotated it for easier reading:

.method private hidebysig instance void  Start() cil managed
{
  // Code size       47 (0x2f)
  .maxstack  9
 
  // Get the del field and call Invoke() on it
  IL_0000:  ldarg.0
  IL_0001:  ldfld      class [System.Core]System.Action TestScript::del
  IL_0006:  callvirt   instance void [System.Core]System.Action::Invoke()
 
  // Get the del field and call Invoke() on it
  IL_000b:  ldarg.0
  IL_000c:  ldfld      class [System.Core]System.Action TestScript::del
  IL_0011:  callvirt   instance void [System.Core]System.Action::Invoke()
 
  // Get the intDel field and call Invoke(3) on it
  IL_0016:  ldarg.0
  IL_0017:  ldfld      class [mscorlib]System.Action`1<int32> TestScript::intDel
  IL_001c:  ldc.i4.3
  IL_001d:  callvirt   instance void class [mscorlib]System.Action`1<int32>::Invoke(!0)
 
  // Get the intDel field and call Invoke(3) on it
  IL_0022:  ldarg.0
  IL_0023:  ldfld      class [mscorlib]System.Action`1<int32> TestScript::intDel
  IL_0028:  ldc.i4.3
  IL_0029:  callvirt   instance void class [mscorlib]System.Action`1<int32>::Invoke(!0)
 
  // return;
  IL_002e:  ret
} // end of method TestScript::Start

Here we see the opposite: both versions end up calling Invoke rather than using the “function style”. It seems that both decompilers opted to convert the Invoke version into C# code that used the “function style” version.

We can conclude that the “function style” version and the Invoke version compile to the same bytecode. They should both perform the same, use the same amount of memory, and create exactly the same executable sizes. The only differences are in the syntax that you use to call the delegate. Do you prefer calling them like a function or using the Invoke method? Let me know which in the comments!