【问题标题】:Differences between how C# and VB handle named parameters?C#和VB处理命名参数的区别?
【发布时间】:2010-02-25 04:23:43
【问题描述】:

既然 C# 支持命名参数,我正在检查它是否以与 VB 相同的方式实现,发现有细微差别。以这样的库函数为例:

public static void Foo(string a, string b)
{
    Console.WriteLine(string.Format("a: {0}, b: {1}", a, b));
}

在 C# 中,如果你这样称呼它:

Foo(a: "a", b: "b");

编译器产生以下 IL 指令:

.locals init (
    [0] string CS$0$0000,
    [1] string CS$0$0001)
L_0000: nop 
L_0001: ldstr "a"
L_0006: stloc.0 
L_0007: ldstr "b"
L_000c: stloc.1 
L_000d: ldloc.0 
L_000e: ldloc.1 
L_000f: call void [TestLibrary]TestLibrary.Test::Foo(string, string)
L_0014: nop 
L_0015: ret 

转换为以下 C# 代码:

string CS$0$0000 = "a";
string CS$0$0001 = "b";
Test.Foo(CS$0$0000, CS$0$0001);

在VB中,如果你这样称呼它:

Foo(a:="a", b:="b")

编译器产生以下 IL 指令:

L_0000: nop 
L_0001: ldstr "a"
L_0006: ldstr "b"
L_000b: call void [TestLibrary]TestLibrary.Test::Foo(string, string)
L_0010: nop 
L_0011: nop 
L_0012: ret 

转换为以下 VB 代码:

Foo("a", "b");

VB 实现它的方式需要更少的指令调用,那么 C# 实现它的方式有什么优势吗?如果不使用命名参数,C# 会产生与 VB 相同的结果。


编辑:既然我们已经确定额外指令在发布模式下会消失,那么它们在调试模式下出现的特殊原因是什么? VB在两种模式下的行为相同,C#在没有命名参数的情况下正常调用方法时不会插入额外的指令(包括使用可选参数时)。

【问题讨论】:

  • 在参数上切换顺序,Foo(b:="b", a:="a") 看区别(提示:副作用顺序)
  • 什么是等效的VB代码?
  • @adrianm 一个有趣的想法,但我刚刚尝试过(出于好奇),它生成的 IL 与 Foo(a: "a", b: "b");
  • @Marc 这可能是因为字符串没有副作用。如果您将它们更改为具有副作用的函数调用(即 Foo(b: Method1(), a: Method2())),则发布版本无法删除临时对象。
  • C# 规范说参数是从左到右计算的。 VB 没有这样的限制。

标签: c# .net vb.net c#-4.0


【解决方案1】:

它们出现在调试模式中有什么特殊原因吗?

区别在于:

  • 将一个临时值压入堆栈,使用它,丢弃它,然后
  • 将临时值存储到特定堆栈槽中,将其复制到堆栈上,使用它,丢弃它,但临时值的原始副本保留在堆栈槽中

这种差异的明显效果是垃圾收集器不能像清理值那样积极。在第一种情况下,可以在调用返回后立即收集该值。在第二种情况下,只有在当前方法返回(或槽被重新使用)后才收集该值。

降低垃圾收集器的积极性通常有助于调试场景。

隐含的问题是:

为什么C#和VB有区别?

C# 和 VB 编译器是由不同的人编写的,他们对各自的代码生成器的工作方式做出了不同的选择。

更新:回复:您的评论

在 C# 编译器中,未优化的 IL 生成本质上与我们对该功能的内部表示具有相同的结构。当我们看到一个命名参数时:

M(y : Q(), x : R());

方法在哪里,比如说

void M(int x, int y) { }

我们在内部表示这一点,就像你写的一样

int ytemp = Q();
int xtemp = R();
M(xtemp, ytemp);

因为我们想要保留 Q 和 R 的副作用的从左到右的评估。这是一种合理的内部表示,当我们在非优化模式下对其进行代码生成时,我们只是直接从内部表示几乎没有任何修改。

当我们运行优化器时,我们会检测到各种各样的东西——比如没有人将这些不可见的局部变量用于任何事情。然后我们可以从代码生成中消除本地人。

我对VB的内部表示知之甚少;自 1995 年以来我就没有从事过 VB 编译器的工作,而且我听说它在过去 15 年中可能发生了轻微的变化。我想他们会做类似的事情,但我不知道他们如何表示命名参数或他们的代码生成器如何处理它们的细节。

我的观点是,据我所知,这种差异并不能说明重要的语义差异。相反,它说明未优化的构建只是吐出我们碰巧生成的任何我们知道具有所需语义的高级内部表示。

【讨论】:

  • @Eric 这是一个很好的解释,谢谢!至于隐含的问题,我的意思更多的是“他们为什么做出这些选择”,因为我认为这样做是有充分理由的。
  • +1 用于强调从左到右的评估,你们似乎在 C# 中虔诚地遵循以保持一致(也指您的 F() + G() * H() 示例另一个答案)
【解决方案2】:

编译器生成以下 C# 代码:

否 - 编译器生成 IL,然后您将其转换为 C#。与 C# 代码的任何相似之处纯属偶然(并非所有生成的 IL 都可以编写为 C#)。所有这些“nop”的存在告诉我您处于“调试”模式。我会在“发布”模式下重试——它会对这些事情产生很大的影响。

我在发布模式下启动它,使用:

static void Main()
{
    Foo(a: "a", b: "b");
}

给予:

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 8
    L_0000: ldstr "a"
    L_0005: ldstr "b"
    L_000a: call void ConsoleApplication1.Program::Foo(string, string)
    L_000f: ret 
}

一模一样。

【讨论】:

  • 你说得对,我的语言倒退了……很好。我在发布模式下再次尝试了它(我以为我已经尝试过了,但似乎我没有尝试过)。 C# IL 指令与该模式下的 VB 指令相匹配。这仍然让我想知道在调试模式下生成额外指令的原因是什么,因为 VB 似乎在两种模式下都以更短的方式执行此操作,但很高兴知道这在发布模式下不是问题。
  • @gshackles - 我猜它在内部使用了最简单的解释,即在翻译阶段(即您的问题中的版本)保证正确性,然后再依赖(可选)编译器中的阶段以对其进行优化。具有非常字面的代码翻译使调试器更简单、更可靠。
猜你喜欢
  • 2019-10-12
  • 1970-01-01
  • 1970-01-01
  • 2023-03-21
  • 1970-01-01
  • 2013-05-08
  • 1970-01-01
  • 2023-04-03
  • 2016-12-27
相关资源
最近更新 更多