将结构体转换为通用接口时,是否存在装箱/拆箱?

19

可能是重复问题:
结构、接口和装箱


来自 MSDN:http://msdn.microsoft.com/en-us/library/yz2be5wk.aspx

装箱是将值类型转换为类型 object 或实现该值类型的任何接口类型的过程。

但泛型接口呢?

例如,int 类型既从 IComparable 派生,也从 IComparable<int> 派生。

假设我有以下代码:

void foo(IComparable value)    { /* etc. */ }
void bar(IComparable<T> value) { /* etc. */ }

void gizmo()
{
   int i = 42;

   bar(i); // is `i` boxed? I'd say YES
   foo(i); // is `i` boxed? I fear it is (but I hope for NO)
}

调用 bar 函数(或任何接收非泛型接口的函数),是否意味着会发生装箱?

调用 foo 函数(或任何接收泛型接口的函数),是否意味着会发生装箱?

谢谢。


7
通用接口类型是接口类型。它们并没有特殊的魔力可以防止装箱。 - Eric Lippert
3个回答

24

任何时候将结构体转换为接口时,都会进行装箱。IComparable<T> 的目的是允许类似于以下内容:

void bar<T>(T value) where T : IComparable<T> { /* etc. */ }

当以这种方式使用时,结构体将作为结构体传递(通过通用类型参数),而不是作为接口传递,因此不必进行装箱。请注意,根据结构体的大小,有时按值传递可能更好,有时按引用传递可能更好,但当然,如果正在使用现有的接口(如IComparable),则必须按照接口要求传递。

2
当一个结构体被转换为接口时,它就会被装箱。之后的内容有些离题,但同时也让我意识到了关于泛型、结构体和接口的一些重要区别,这是一个简单完整答案所无法达到的,即 bar<T>(IComparable<T> value)bar<T>(T value) where T : IComparable<T> 之间的区别,以及为什么只要我们正确使用泛型,接口对于结构体仍然非常重要。+1。 - paercebal
1
@paercebal:在我看来,重要的不是答案是否精确回答了所问的问题,而是它是否提供了对原始提问者和其他点击问题的人有用的信息。我想你不仅想知道特定语法是否会导致装箱,还想知道是否有另一种编写代码的替代方法。顺便说一下,如果我想找出某些东西是否会导致装箱,我会编写一个简单的程序来创建一个指向新对象(否则未使用)的WeakReference,然后... - supercat
我运行了一个可能需要包装一百万次的操作,然后查看WeakReference是否仍然有效。如果操作需要任何堆分配,则会发生垃圾回收并使WeakReference失效。如果没有,则不会。请注意,可能有更好的方法来检测垃圾回收,但这种方法非常快速和易于测试。 - supercat

8

首先,简单介绍值类型、引用类型和装箱的概念(可能不完全)。

如果一个对象是值类型,在函数内做出的更改不会在函数外生效。当调用函数时,对象的值被复制,函数结束后即被丢弃。

如果一个对象是引用类型,在函数内做出的更改会在函数外生效。当调用函数时,对象的值不会被复制,函数结束后对象仍然存在。

如果一个对象被装箱了,它会被复制一次,并放置在引用类型中。这实际上将其从值类型转换为引用类型。

需要注意的是,这些适用于实例状态,即任何非静态成员数据。静态成员不是实例状态,与引用类型、值类型或装箱无关。对于不使用实例状态的方法和属性(例如仅使用本地变量或静态成员数据的方法和属性),它们在应用于引用类型、值类型或进行装箱时不会有所不同。

有了这些知识后,我们可以证明将结构体转换为接口(通用或非通用)时确实发生了装箱

using System;

interface ISomeInterface<T>
{
    void Foo();
    T MyValue { get; }
}

struct SomeStruct : ISomeInterface<int>
{
    public void Foo()
    {
        this.myValue++;
    }

    public int MyValue
    {
        get { return myValue; }
    }

    private int myValue;
}

class Program
{
    static void SomeFunction(ISomeInterface<int> value)
    {
        value.Foo();
    }

    static void Main(string[] args)
    {
        SomeStruct test1 = new SomeStruct();
        ISomeInterface<int> test2 = test1;

        // Call with struct directly
        SomeFunction(test1);
        Console.WriteLine(test1.MyValue);
        SomeFunction(test1);
        Console.WriteLine(test1.MyValue);

        // Call with struct converted to interface
        SomeFunction(test2);
        Console.WriteLine(test2.MyValue);
        SomeFunction(test2);
        Console.WriteLine(test2.MyValue);
    }
}

输出结果如下:

0
0
1
2

这意味着只有在转换时才会发生装箱:
  • 前两个调用在每次调用时都进行了装箱。
  • 后两个调用已经有了装箱副本,所以每次调用时不会发生装箱。
我不会在这里重复所有的代码,但如果你将ISomeInterface<T>更改为ISomeInterface,你仍然会有相同的行为。

1
我不认为我同意你对值类型和引用类型的描述。假设你有一个静态字段结构体C { public static int x; }和一个方法F() { C.x = 123; }。C.x的更改会在调用F之外持续存在,但C和C.x都不是引用类型。 - Eric Lippert
@Eric:好观点 :) 我的描述确实只适用于实例状态。我会修改答案,尽量让它更清晰。或者你认为我过于简化了这个问题? - Merlyn Morgan-Graham
@Eric Lippert:没错,但在您描述的情况下,静态字段只是一个隐藏在结构体中的被吹嘘的全局变量,因此并不真正属于装箱/非装箱问题的一部分。我错了吗? - paercebal
@Merlyn Morgan-Graham:这个演示中声明方法为 ISomeInterface<int>.Foo 而不是 Foo 很重要吗? - paercebal
@paercebal:我觉得我在写示例代码的中途分心了。那种语法被称为隐式声明,如果你不想用接口实现弄脏具体类的公共接口或者需要为不同的接口使用不同的方法实现,那么它是很有用的。使用它时,必须将其转换为接口类型才能使用那些方法。出于某种原因,我认为这是必要的。实际上并不是,我很高兴 :) 我已经修复了示例代码。 - Merlyn Morgan-Graham
显示剩余2条评论

7

答案摘要

我对通用接口和装箱/拆箱的困惑来自于我知道C#泛型使我们能够生成更有效的代码。

例如,int 实现了 IComparable<T>IComparable 这一事实对我来说意味着:

  • IComparable 应该与旧的、非泛型的代码一起使用,但会导致装箱/拆箱
  • IComparable<T> 应该与泛型启用的代码一起使用,可以避免装箱/拆箱

Eric Lippert 的评论如此简单、清晰和直接:

通用接口类型是接口类型。它们没有任何特殊之处,可以神奇地防止装箱。

从现在开始,我毫不怀疑将结构体转换为接口会意味着装箱。

但是,IComparable<T> 怎么比 IComparable 更有效率呢?

这就是 supercat 的答案(由 Lasse V. Karlsen 编辑)指向的泛型更像 C++ 模板的事实:

IComparable 的目的是允许像这样的东西:

   void bar<T>(T value) where T : IComparable<T> { /* etc. */ }

这与以下内容有很大不同:

   void bar(IComparable<T> value) { /* etc. */ }

甚至可以这样做:
   void bar(IComparable value) { /* etc. */ }

我的猜测是,对于第一个原型,运行时将为每种类型生成一个函数,因此在处理结构时避免装箱问题。
而对于第二个原型,运行时只会生成带有接口作为参数的函数,因此当T是结构时会进行装箱。第三个函数只是装箱结构,不多也不少。
(我猜这就是C#泛型与C#结构相结合比Java类型擦除泛型实现更优越的地方。)
Merlyn Morgan-Graham的答案为我提供了一个我将在家中测试的测试示例。 我将尽快完成这个摘要,一旦我有有意义的结果就会更新(我猜我会尝试使用按引用传递语义来看看所有这些是如何工作的……)

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