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

zeb*_*box 12 c# performance

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

如果我有一个结构但我没有显式覆盖从对象派生的任何方法,如ToString(),GetHashCode()等,那么如果我声明我的struct类的本地实例并调用'ToString()'它,我的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?
  ..... 
}
Run Code Online (Sandbox Code Playgroud)

==编辑 - 更新== Mehrdad 与MSDN链接,虽然有用但让我感到困惑.我会引用,看看是否有人可以为我取消这个

当callvirt方法指令以约束thisType作为前缀时,指令执行如下:

如果thisType是引用类型(而不是值类型),则取消引用ptr并将其作为'this'指针传递给方法的callvirt.

如果thisType是一个值类型而thisType实现了方法,那么ptr将被未修改地传递为调用方法指令的'this'指针,用于通过thisType实现方法.

如果thisType是一个值类型而thisType没有实现方法,则ptr被解除引用,装箱,并作为'this'指针传递给callvirt方法指令.

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

Ken*_* K. 9

如果thisType是值类型并且 thisType没有实现方法,则ptr被解除引用,装箱,并作为"this"指针传递给callvirt方法指令.

当方法定义只能出现最后这种情况下 Object, ValueTypeEnum,而不是由覆盖thisType.在这种情况下,装箱会导致原始对象的副本.

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


GBe*_*gen 7

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

我也认为,从这个报价参考迈赫达德的回答特别耐人寻味:

  • 如果thisType是一个值类型而thisType没有实现方法,则ptr被解除引用,装箱,并作为'this'指针传递给callvirt方法指令.

最后一种情况只有在Object,ValueType或Enum上定义方法并且不被thisType覆盖时才会发生.在这种情况下,装箱会导致原始对象的副本.但是,由于Object,ValueType和Enum的方法都没有修改对象的状态, 因此无法检测到此事实.

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


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

当结构类型覆盖从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>();
    }
}
Run Code Online (Sandbox Code Playgroud)

该计划的输出是:

1
2
3
Run Code Online (Sandbox Code Playgroud)

虽然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>();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

0
1
1
Run Code Online (Sandbox Code Playgroud)

有关装箱和拆箱的更多详细信息,请参见§4.3.


Meh*_*ari 5

不,当你调用ToString或者GetHashCode它是由你的结构实现时,它不会被装箱(为什么应该这样?constrainedIL指令会处理它。)当你调用非虚拟方法(或结构中未重写的虚拟方法)时,它会被装箱System.Object(它的基类),即GetType/ MemberwiseClone

更新:对于可能造成的误解深表歉意。我在写答案时考虑到了重写结构中的方法(这就是为什么我提到非虚拟方法需要装箱,我应该更明确地不要让读者感到困惑,特别是因为我错过了您关于不重写该方法的声明),就好像您不重写它,该Object.ToString方法期望它的第一个参数(对 的引用this)是引用类型(Object实例)。显然,该值必须在该调用中装箱(因为它是基类中的调用。)

然而,关键是,在值类型上调用虚拟方法的本质不会导致发出box指令(与非虚拟方法不同,Object它总是会导致发出显式box指令。)callvirt如果满足以下条件,则指令将执行装箱:它必须求助于实现Object.ToString(正如您在更新的问题中提到的那样),就像您将结构传递给需要object参数的方法一样。