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

我经常想知道以下场景是否真的发生在c#中

如果我有一个结构但我没有显式覆盖从对象派生的任何方法,如ToString(),GetHashCode()等,那么如果我声明我的struct类的本地实例并调用’ToString()’它,我的结构会被装箱,即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 链接到MSDN ,虽然有用但让我感到困惑。 我会引用,看看是否有人可以为我取消这个

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

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

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

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

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

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

最后一种情况只有在ObjectValueTypeEnum上定义方法并且不被thisType覆盖时thisType 。 在这种情况下,装箱会导致原始对象的副本。

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

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

我也认为Mehrdad答案中引用的引用特别引人深思:

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

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

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


来自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.Objectinheritance的虚方法(例如Equals,GetHashCode或ToString)时,通过struct类型的实例调用虚方法不会导致发生装箱。 即使将结构体用作类型参数并且通过类型参数类型的实例进行调用,也是如此。 例如:

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

该计划的输出是:

 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() 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(); } } 

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

 0 1 1 

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

当你调用ToStringGetHashCode如果它是由你的struct实现的话,它就不会被盒装( 为什么它呢? constrained IL指令会处理它 。)当你调用非虚方法(或者结构中没有覆盖的虚方法)时,它会被装箱)在System.Object (它的基类)上,即GetType / MemberwiseClone

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

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