以下C#函数:

T ResultOfFunc<T>(Func<T> f)
{
    return f();
}

毫不奇怪地编译为:
IL_0000:  ldarg.1
IL_0001:  callvirt    05 00 00 0A
IL_0006:  ret

但是等效的F#函数:
let resultOfFunc func = func()

编译为此:
IL_0000:  nop
IL_0001:  ldarg.0
IL_0002:  ldnull
IL_0003:  tail.
IL_0005:  callvirt    04 00 00 0A
IL_000A:  ret

(均处于 Release模式)。一开始我没有太多好奇,但是有趣的是额外的ldnulltail.指令。

我的猜测(可能是错误的)是,如果函数是ldnull,则void是必需的,因此它仍然返回某些内容(unit),但这并不能解释tail.指令的目的。如果该函数确实将某些内容压入堆栈会发生什么,难道该函数卡住了不会弹出的额外null吗?

最佳答案

C#和F#版本有一个重要的区别:C#函数没有任何参数,但是F#版本只有一个类型为unit的参数。该unit值将显示为ldnull(因为null被用作唯一unit()的表示)。

如果要将第二个函数转换为C#,它将看起来像这样:

T ResultOfFunc<T>( Func<Unit, T> f ) {
   return f( null );
}

至于.tail指令-所谓的“尾调用优化”。
在常规函数调用期间,返回地址被压入堆栈(CPU堆栈),然后调用该函数。完成该功能后,它将执行“返回”指令,该指令将返回地址弹出堆栈,并在此转移控制权。
但是,当函数A调用函数B,然后立即返回函数B的返回值,而无需执行其他任何操作时,CPU可以跳过将额外的返回地址压入堆栈,并执行“跳转”到B而不是“称呼”。这样,当B执行“return”指令时,CPU将从堆栈中弹出返回地址,该地址将不指向A,而是指向首先调用A的人。
考虑它的另一种方法是:函数A不在返回之前调用函数B,而是返回而不是返回,因此委托(delegate)返回B的荣誉。

因此,实际上,这种魔术技术使我们可以在不消耗堆栈上任何位置的情况下进行调用,这意味着您可以任意执行许多此类调用而不会冒堆栈溢出的风险。这在函数式编程中非常重要,因为它可以有效地实现递归算法。

之所以称为“尾部调用”,是因为对B的调用发生在A的末尾。

10-07 15:59