当作为具有接口约束的通用参数传递时,值类型是否装箱?

时间:2014-08-26 14:39:35

标签: c# generics clr boxing type-constraints

(作为研究回答这个问题的结果,我(我想我已经!)确定答案是“不”。但是,我必须在几个不同的地方看看,所以我认为这个问题仍有价值。但如果社区投票结束,我不会感到沮丧。)

例如:

void f<T>(T val) where T : IComparable
{
   val.CompareTo(null);
}

void g()
{
   f(4);
}

4装箱了吗?我知道明确地将值类型转换为它实现触发装箱的接口:

((IComparable)4).CompareTo(null); // The Int32 "4" is boxed

我不知道的是,将值类型作为具有接口约束的泛型参数传递是否等于执行强制转换 - 语言“其中T是IC Comparable”类似于建议转换,但只是转动{{ 1}}进入T似乎会破坏通用的全部目的!

为了澄清,我想确保上述代码中都没有发生这些事情:

  1. IComparable调用g时,由于f(4)的参数类型存在4约束,IComparable会转换为IComparable
  2. 假设(1)未发生,f内,f未将val.CompareTo(null)val投射至Int32以致{{1} }}。
  3. 但我想了解一般情况;不仅仅是IComparableCompareTo s会发生什么。

    现在,如果我将以下代码放入LinqPad:

    int

    然后检查生成的IL:

    IComparable

    很明显,拳击是按照预期的显式转换进行的,但void Main() { ((IComparable)4).CompareTo(null); f(4); } void f<T>(T val) where T : IComparable { val.CompareTo(null); } 本身 * IL_0001: ldc.i4.4 IL_0002: box System.Int32 IL_0007: ldnull IL_0008: callvirt System.IComparable.CompareTo IL_000D: pop IL_000E: ldarg.0 IL_000F: ldc.i4.4 IL_0010: call UserQuery.f f: IL_0000: nop IL_0001: ldarga.s 01 IL_0003: ldnull IL_0004: constrained. 01 00 00 1B IL_000A: callvirt System.IComparable.CompareTo IL_000F: pop IL_0010: ret 中的呼叫站点都没有拳击。这是个好消息。但是,这也只是一种类型的一个例子。这种缺乏拳击的东西是否适用于所有情况?


    * This MSDN article讨论了f前缀,并声明只要被调用的方法,将其与Main结合使用就不会触发值类型的装箱在类型本身上实现(而不是基类)。我不确定的是,当我们到达这里时,类型是否总是 一个值类型。

3 个答案:

答案 0 :(得分:6)

正如您所知,当struct传递给通用方法时,它不会被装箱。

Runtime为每个&#34; Type Argument&#34;创建新方法。当您使用值类型调用泛型方法时,您实际上正在调用为各个值类型创建的专用方法。所以不需要拳击。

当调用未在结构类型中直接实现的接口方法时,将发生装箱。 Spec在这里调用它:

  

如果thisType是值类型,并且thisType没有实现方法   然后ptr被解除引用,装箱,并作为&#39;这个&#39;指针   callvirt方法指令。

     

最后一种情况只有在Object上定义方法时才会发生,   ValueType或Enum,不会被thisType覆盖。在这种情况下,   拳击会导致原始对象的副本。然而,   因为Object,ValueType和Enum的方法都没有修改   对象的状态,这个事实无法被发现。

所以,只要你明确[1]在你的struct本身实现接口成员,就不会发生装箱。

How, when and where are generic methods made concrete?

1.不要与Explicit接口实现混淆。这就是说你的接口方法应该在struct本身而不是它的基本类型中实现。

答案 1 :(得分:1)

一个简单的测试就是简单地创建一个可变结构,其中包含一个可以改变它的接口方法。从泛型方法中调用该接口方法,并查看原始结构是否已发生变异。

public interface IMutable
{
    void Mutate();
    int Value { get; }
}

public struct Evil : IMutable
{
    public int value;

    public void Mutate()
    {
        value = 9;
    }

    public int Value { get { return value; } }
}

public static void Foo<T>(T mutable)
    where T : IMutable
{
    mutable.Mutate();
    Console.WriteLine(mutable.Value);
}

static void Main(string[] args2)
{
    Evil evil = new Evil() { value = 2 };
    Foo(evil);
}

这里我们看到9打印出来,这意味着实际变量是变异的,而不是副本,所以struct没有被装箱。

答案 2 :(得分:0)

我以Servy给出的答案为基础,我相信我的答案更具解释性,它证明了所要求的行为。

代码创建实现接口方法的结构和类。此方法试图使它们变异。该代码从该通用方法调用该接口方法,然后将该结构转换为该接口,然后再为该类调用。输出是非常不言自明的,它表明传递的结构在未强制转换为接口之前不会被装箱。另外,我添加了一些IL代码来查看何时发生装箱。

using System;

namespace ConsoleApp
{
    public interface IMutable
    {
        void Mutate();
        int Value { get; }
    }

    public struct EvilStruct: IMutable
    {
        public int value;

        public void Mutate()
        {
            value++;
        }

        public int Value { get { return value; } }
    }

    public class EvilClass : IMutable
    {
        public int value;

        public void Mutate()
        {
            value++;
        }

        public int Value { get { return value; } }
    }

    class Program
    {
        public static void Foo<T>(T mutable)
            where T: IMutable
        {
            mutable.Mutate();
        }

        static void Main(string[] args)
        {
            EvilStruct Struct = new EvilStruct() { value = 1 };
            Foo(Struct);
            //Shows 1 after calling Mutate on value type 
            Console.WriteLine(Struct.Value);

            IMutable YetAnotherStruct = new EvilStruct() { value = 1 };
            Foo(YetAnotherStruct);
            //Shows 2 after calling Mutate on value type
            Console.WriteLine(YetAnotherStruct.Value);

            EvilClass Class = new EvilClass() { value = 1 };
            Foo(Class);
            //Shows 2 after calling Mutate on ref type 
            Console.WriteLine(Class.Value);

            Console.ReadLine();
        }
    }
}  

输出: 1个 2 2

这是Main方法的IL代码。您可以在IL_0038看到拳击发生:

Program.Main:
IL_0000:  nop         
IL_0001:  ldloca.s    03 
IL_0003:  initobj     UserQuery.EvilStruct
IL_0009:  ldloca.s    03 
IL_000B:  ldc.i4.1    
IL_000C:  stfld       UserQuery+EvilStruct.value
IL_0011:  ldloc.3     
IL_0012:  stloc.0     // Struct
IL_0013:  ldloc.0     // Struct
IL_0014:  call        UserQuery+Program.Foo<EvilStruct>
IL_0019:  nop         
IL_001A:  ldloca.s    00 // Struct
IL_001C:  call        UserQuery+EvilStruct.get_Value
IL_0021:  call        System.Console.WriteLine
IL_0026:  nop         
IL_0027:  ldloca.s    03 
IL_0029:  initobj     UserQuery.EvilStruct
IL_002F:  ldloca.s    03 
IL_0031:  ldc.i4.1    
IL_0032:  stfld       UserQuery+EvilStruct.value
IL_0037:  ldloc.3     
IL_0038:  box         UserQuery.EvilStruct
IL_003D:  stloc.1     // YetAnotherStruct
IL_003E:  ldloc.1     // YetAnotherStruct
IL_003F:  call        UserQuery+Program.Foo<IMutable>
IL_0044:  nop         
IL_0045:  ldloc.1     // YetAnotherStruct
IL_0046:  callvirt    UserQuery+IMutable.get_Value
IL_004B:  call        System.Console.WriteLine
IL_0050:  nop         
IL_0051:  newobj      UserQuery+EvilClass..ctor
IL_0056:  dup         
IL_0057:  ldc.i4.1    
IL_0058:  stfld       UserQuery+EvilClass.value
IL_005D:  stloc.2     // Class
IL_005E:  ldloc.2     // Class
IL_005F:  call        UserQuery+Program.Foo<EvilClass>
IL_0064:  nop         
IL_0065:  ldloc.2     // Class
IL_0066:  callvirt    UserQuery+EvilClass.get_Value
IL_006B:  call        System.Console.WriteLine
IL_0070:  nop         
IL_0071:  call        System.Console.ReadLine
IL_0076:  pop