Delegate() vs. Delegate.Invoke()
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!
#1 by Lee Chidgey on May 26th, 2016 ·
I like function style :)
#2 by Ava Nasiri on April 3rd, 2017 ·
Thanks!
#3 by David Maciejewski on May 26th, 2017 ·
I prefer the Invoke() version in combination with the null conditional operator.
Example:
OnMyEvent?.Invoke();
#4 by jackson on May 26th, 2017 ·
Hopefully we’ll get support for the null-conditional operator with C# 6 support in the 2017.1.0 release this July (according to the current roadmap).
#5 by Luke on June 4th, 2018 ·
I like the del.Invoke() because people reading through the code can more easily see that a delegate is being used!
#6 by Eleocraft on January 7th, 2022 ·
I use del() because it’s shorter
#7 by Linus Zoe on October 4th, 2024 ·
https://learn.microsoft.com/en-us/dotnet/api/system.delegate?view=net-8.0
Note
The common language runtime provides an Invoke method for each delegate type, with the same signature as the delegate. You do not have to call this method explicitly from C#, Visual Basic, or Visual C++, because the compilers call it automatically. The Invoke method is useful in reflection when you want to find the signature of the delegate type.