当C#结构体被声明为函数的返回值时,是否会发生装箱?

10

这是一个简单的问题,但我在 Stack Overflow 上还没有找到确定的答案。

    struct MyStruct { int x, y, z; }

    MyStruct GetMyStruct() => new MyStruct();

    static void Main()
    {
        var x = GetMyStruct();      // can boxing/unboxing ever occur?
    }
C# 结构体(值类型)在从函数返回时是否总是被复制到堆栈中,无论其大小如何?我不确定的原因是对于除了 MSIL(例如 x86)之外的一些指令集,返回值通常需要适合于处理器寄存器,而且堆栈并没有直接涉及。
如果是这样,那么是调用站点预分配 CLR 堆栈上的空间用于期望的值返回类型吗?
编辑:针对原始问题的意图,答案为;CLR 永远不会(默默地)装箱一个结构体,仅用于作为传输的返回值。

如果值类型的返回值被装箱,那么当我们将返回值分配给值类型变量时,就会意味着有隐式的拆箱,这似乎是低效的。我对MSIL和CLR几乎一无所知,所以在这方面我无法帮助你。不管怎样,我可以知道是什么促使了你的问题? - blizpasta
@blizpasta:我的原始问题实际上变得无意义了,因为我意识到我错误地认为'ref'(和'out')关键字会导致值类型的装箱(这不是情况)。为避免这种情况,我打算“返回”结构体。但是一旦我记起来‘ref’不会装箱值类型,使用一个输出参数就成为了一个选项。我的值类型大小目前为4 + IntPtr.Size字节,并且有成千上万个这些结构被非常密集地操作。 - Glenn Slayden
2个回答

10

这是JIT编译器的一个重要实现细节。一般而言,如果结构体足够小且成员简单,则返回CPU寄存器。如果太大,则调用代码会在堆栈上保留足够的空间,并将指向该空间的指针作为额外的隐藏参数传递。

除非方法的返回类型为 object ,否则永远不会装箱。

顺便说一句:这也是调试器无法在“自动变量”窗口中显示函数返回值的原因。有时很痛苦。但是调试器无法从JIT编译器获取足够的元数据来确定值的确切位置。编辑:VS2013中已修复此问题。


6

当您想将结构体视为对象时,需要进行装箱操作。因此,如果调用Func并将结果分配给对象,则会进行装箱操作。

例如:

 object o = Func();

将会产生以下IL。
L_0000: call valuetype TestApp.foo TestApp.Program::Func()
L_0005: box TestApp.foo
L_000a: stloc.0 

这表明返回值被装箱了,因为我们将其赋值给类型为 object 的引用。

如果您将其分配给类型为 Foo 的变量,则不会装箱,因此它会被复制并存储在堆栈上。

此外,在这里进行装箱并没有真正帮助您,因为它将涉及创建一个对象来表示结构的值,并且在装箱操作期间实际上是复制值。


1
调用点必须在栈上分配适当数量的字节,这就是为什么.NET设计指南建议struct最好不要超过16个字节的原因。 - Richard Cook
1
@Richard:你有这个信息的来源吗? - Jim Mischel
1
是的,它包含16字节的建议,但并没有说这是因为调用站点必须分配数据。 - Jim Mischel
我会尝试追踪我读到这个信息的具体来源。 - Richard Cook
根据答案,如果您想保证避免bltting大结构体,您应该明确使用refout而不是返回结构体。 - Glenn Slayden
@RichardCook 人们想知道的原因可能是逻辑不一定遵循;假设给定一个1000字节的结构体,在调用者自己的框架中为其分配空间并让被调用者“填充它”是完全有效的,如果我们假设整个结果随后由调用者在原地检查/消耗/处理。也就是说,这样的设计指南需要进一步假设调用者无法或对于不小心从错误位置“提取”大型结构体视而不见,这确实会是一个真正的问题。 - Glenn Slayden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接