将结构体转换为通用接口时是否存在Boxing / Unboxing?

时间:2011-04-22 16:01:48

标签: c# generics boxing

  

可能重复:
  Structs, Interfaces and Boxing

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

  

拳击是将值类型转换为此值类型实现的类型对象或任何接口类型的过程。

但是通用接口呢?

例如,int来自IComparableIComparable<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(或任何在该类型上采用通用接口的函数)是否意味着会有拳击?

感谢。

3 个答案:

答案 0 :(得分:20)

任何时候将结构体转换为接口,它都会被装箱。 IComparable&lt; T&gt;的目的是允许类似的东西:

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

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

答案 1 :(得分:7)

首先,关于值类型,引用类型和装箱的简短(可能是不完整的)入门。

您可以判断某些内容是值类型,因为函数中所做的更改不会在函数外部保留。调用函数时会复制对象的值,并在该函数的末尾抛弃该对象。

您可以判断某些内容是引用类型,因为函数中所做的更改会在函数外部保留。调用函数时不会复制对象的值,并且在该函数结束后存在该对象的值。

如果装箱,则会制作一份副本,并在参考类型中就座。它实际上从值类型更改为引用类型。

请注意,这一切都适用于实例化状态,即任何非静态成员数据。静态成员不是实例状态,与引用类型,值类型或装箱无关。不使用实例化状态的方法和属性(例如,仅使用局部变量或静态成员数据的方法和属性)在引用类型,值类型或发生装箱时的操作方式不同。

有了这些知识,以下是我们如何证明拳击在将结构转换为界面时发生(通用与否)

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,您仍然会有相同的行为。

答案 2 :(得分:5)

答案摘要

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

例如,事实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是结构时进行装箱。第三个函数只是对结构进行打包,不多也不少。

(我猜这是与Java类型擦除泛型实现相比,C#泛型与C#结构相结合显示其优越性的地方。)

Merlyn Morgan-Graham的回答为我提供了一个我将在家里玩的测试的例子。一旦我得到有意义的结果,我就会完成这个摘要(我想我会尝试使用pass-by-reference语义来看看它是如何工作的......)