调用ToString()时在结构上装箱

时间:2009-08-08 14:55:22

标签: c# performance

我经常想知道以下情况是否真的发生在c#

如果我有一个结构但我没有显式覆盖从对象派生的任何方法,如ToString(),GetHashCode()等,那么如果我声明我的struct类的本地实例并调用'ToString( )'on it,我的struct会被装箱,即CLR会将它隐式转换为堆上的对象,然后调用ToString()吗?或者它是否足够聪明地知道该结构没有实现并忽略它?

public struct Vector2D
{
    public float m_x;
    public float m_y;


    ...... etc
}


void SomeFunc()
{
  Vector2D aVec = new Vector2D();
  Console.WriteLine(aVec.ToString()); // <-- does aVec get boxed here?
  ..... 
}

==编辑 - 更新== Mehrdad的link to MSDN虽然有用但却让我感到困惑。 我会引用,看看是否有人可以为我取消这个

  

当一个callvirt方法指令   已被约束前缀   thisType,执行指令   如下:

     

如果thisType是引用类型(如   反对值类型)然后ptr是   取消引用并传递为'this'   指向方法的callvirt的指针。

     

如果thisType是值类型而且   thisType实现方法然后ptr是   通过未经修改的'this'   指向调用方法指令的指针,   用于实现方法   thisType。

     

如果thisType是值类型而且   thisType没有实现方法   然后ptr被解除引用,装箱,和   作为'this'指针传递给   callvirt方法指令。

那么这是否意味着如果我没有在我的结构类型上显式实现ToString(),那么它将落入最后一个案例并被装箱?或者我在某处误解了它?

3 个答案:

答案 0 :(得分:8)

  

如果thisType是值类型而且   thisType没有实现方法   然后ptr被解除引用,装箱,和   作为'this'指针传递给   callvirt方法指令。

     

最后一种情况只有在发生时才会发生   方法是在Object上定义的,   ValueTypeEnum并且未被覆盖   按thisType。在这种情况下,拳击   导致原始对象的副本   要做。

答案是肯定的,值类型是装箱。这就是在自定义结构上覆盖ToString()总是一件好事。

答案 1 :(得分:7)

修改: kek444's answer是正确的。我为误读这个问题而道歉。我在这里留下答案,因为我相信它为未来的读者提供了额外的价值和相关信息。

我也认为来自reference Mehrdad's answer的引用特别引人深思:

  
      
  • 如果thisType是值类型而且   thisType没有实现方法   然后ptr被解除引用,装箱,和   作为'this'指针传递给   callvirt方法指令。
  •   
     

最后一种情况只有在发生时才会发生   方法是在Object上定义的,   ValueType或Enum,不会被覆盖   通过thisType。在这种情况下,拳击   导致原始对象的副本   被制造。但是,因为没有   Object,ValueType和。的方法   枚举修改对象的状态,    无法检测到此事实。

因此,人们不能写一个程序来证明拳击正在发生。通过查看IL并完全理解constrained指令的callvirt前缀,只能识别它。


来自http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.dochttp://msdn.microsoft.com/en-us/vcsharp/aa336809.aspx)的C#语言规范的第11.3.5节:

当结构类型覆盖从System.Object继承的虚方法(例如Equals,GetHashCode或ToString)时,通过struct类型的实例调用虚方法不会导致发生装箱。即使将结构体用作类型参数并且通过类型参数类型的实例进行调用,也是如此。例如:

using System;
struct Counter
{
    int value;
    public override string ToString() {
        value++;
        return value.ToString();
    }
}
class Program
{
    static void Test<T>() where T: new() {
        T x = new T();
        Console.WriteLine(x.ToString());
        Console.WriteLine(x.ToString());
        Console.WriteLine(x.ToString());
    }
    static void Main() {
        Test<Counter>();
    }
}

该计划的输出是:

1
2
3

虽然ToString有副作用的样式很糟糕,但是这个例子表明x.ToString()的三次调用没有发生装箱。

类似地,当访问约束类型参数上的成员时,从不隐式发生装箱。例如,假设接口ICounter包含一个可用于修改值的方法Increment。如果将ICounter用作约束,则调用Increment方法的实现时会引用调用Increment的变量,而不是盒装副本。

using System;
interface ICounter
{
    void Increment();
}
struct Counter: ICounter
{
    int value;
    public override string ToString() {
        return value.ToString();
    }
    void ICounter.Increment() {
        value++;
    }
}
class Program
{
    static void Test<T>() where T: ICounter, new() {
        T x = new T();
        Console.WriteLine(x);
        x.Increment();                      // Modify x
        Console.WriteLine(x);
        ((ICounter)x).Increment();      // Modify boxed copy of x
        Console.WriteLine(x);
    }
    static void Main() {
        Test<Counter>();
    }
}

第一次调用Increment会修改变量x中的值。这不等于对Increment的第二次调用,后者修改了x的盒装副本中的值。因此,该程序的输出是:

0
1
1

有关装箱和拆箱的更多详情,请参阅§4.3。

答案 2 :(得分:4)

如果您的结构(why should it? constrained IL instruction takes care of it)实现了ToStringGetHashCode,则不会将其打包。)当您调用非虚方法(或虚方法)时,它会被装箱在System.Object(其基类)的结构中没有覆盖,即GetType / MemberwiseClone

更新:对不起可能造成的误解。我写的答案是覆盖结构中的方法(这就是为什么我提到非虚拟方法需要装箱,我应该更明确地不要混淆读者,特别是因为我错过了关于不覆盖方法的陈述),好像你没有覆盖它,Object.ToString方法期望它的第一个参数(对this的引用)是一个引用类型(Object实例)。显然,该值必须在该调用中加入框(因为它是基类中的调用。)

然而,重点是,在值类型上调用虚方法的性质不会导致发出box指令(与{{1}上的非虚方法不同)总是导致发出明确的Object指令。)如果必须求助于box实现(如您在更新的问题中提到的那样),它将执行装箱的callvirt指令)就像将结构传递给需要Object.ToString参数的方法一样。