英文原文:
https://www.jacksondunstan.com/articles/3283
在阅读其他人的C#代码时,我总是看到一些程序员像调用函数-del()那样调用委托,而另一些程序员则使用Delegate类的Invoke方法:del.Invoke()。这两者有什么不同吗?其中一个比另一个好吗?
译者:先说结论两者一样。
以下是今天的两位候选人:
// Create a delegate (using System.Action and a no-op lambda)
Action del = () => {};
// Call with function style
del();
// Call with Invoke
del.Invoke();
“函数样式”版本使用重载()运算符,而调用版本使用Delegate.Invoke方法。这两个函数都将参数(如果有的话)放在圆括号内,就像正常的函数调用一样。下面是一个带有参数的版本来显示这一点:
// 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);
现在,让我们创建一个非常小的MonoBehaviour,并让Unity 5.2编译它:
using System;
using UnityEngine;
class TestScript : MonoBehaviour
{
Action del = () => {};
Action<int> intDel = i => {};
void Start()
{
del();
del.Invoke();
intDel(3);
intDel.Invoke(3);
}
}
您可以在项目中找到编译后的DLL:Library/ScriptAssemblies/Assembly-CSharp.dll。现在,让我们将该DLL反编译,看看我们能找到什么不同之处。首先,我们使用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);
}
}
这看起来与我们的原始源代码几乎完全相同。这里有两个细微的区别:类上有显式的内部(internal)访问说明符,以及使用匿名委托函数而不是对委托字段使用lambdas。主要的区别,也是我们关心的,是delegate如何调用。如您所见,调用版本已被“函数调用”版本替换。我们键入的两个版本都已转换为相同的反编译代码。
为了确保我们的结果,让我们使用dotPeek再次反编译DLL:
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)
{
}
}
这个版本比我们从ILSpy得到的输出要长得多,但这种冗长可能会揭示两个委托调用版本之间的差异。我们再次看到该类被显式标记为内部(internal),但委托字段不再是匿名委托函数。相反,我们有私有的静态函数,这些函数被分配给为我们生成的构造函数中的字段。这都比我们编写的代码或ILSpy反编译复杂得多,但在功能上是等效的。当涉及到委托的实际调用时,我们看到了与ILSpy完全相同的情况:Invoke版本已被转换为使用“函数样式”。
最后,让我们看一下使用Microsoft的ILDASM的实际IL汇编代码。为便于阅读,我对其进行了注释:
.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
这里我们看到了相反的情况:两个版本最终都调用了Invoke,而不是使用“函数样式”。似乎两个反编译器都选择将调用版本转换为使用“函数风格”版本的C#代码。
我们可以得出结论,“函数样式”版本和调用版本编译为同一字节码。它们应该执行相同的功能,使用相同的内存量,并创建完全相同的可执行文件大小。唯一的区别在于调用委托所使用的语法。您更喜欢像调用函数一样调用它们还是使用Invoke方法?请在评论中告诉我是哪一个!