当值类型作为通用参数与接口约束传递时,是否会被框定?

问题描述 投票:7回答:3

(为了回答这个问题,我(认为我已经!)做了研究的结果,确定了答案是 "不"。然而,我不得不在几个不同的地方寻找这个问题,所以我认为这个问题还是有价值的。但如果社区投票关闭,我也不会一蹶不振)。)

比如说。

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

void g()
{
   f(4);
}

是... 4 boxed? 我知道将一个值类型显式地投射到它所实现的接口上会触发装箱。

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

我不知道的是 把一个值类型作为通用参数传给一个接口约束是否等同于执行铸造 -- 语言中 "其中T是一个IComparable "就暗示了铸造,但只是简单地把 T 变成 IComparable 这似乎会破坏通用的整个目的!

要澄清的是,我想确定上述代码中这两件事都没有发生。

  1. 当... g 电话 f(4)జజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజ 4 是投向 IComparable 既然有 IComparable 约束 f的参数类型。
  2. 假设(1)不发生,在 f, val.CompareTo(null) 不投 valInt32IComparable 为了调用 CompareTo.

但我想了解一般的情况,而不仅仅是发生了什么。ints和 IComparables.

现在,如果我把下面的代码放到LinqPad中。

void Main()
{
    ((IComparable)4).CompareTo(null);
    f(4);
}

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

然后检查生成的IL:

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  

很明显,在显式投射中,盒状结构会如期出现 但在显式投射中,盒状结构也不明显。f 本身* 或在其电话所在地 Main. 这是一个好消息。然而,这也只是一种类型的一个例子。是否所有情况下都可以假设这种缺乏框框的情况?


*这篇MSDN文章 讨论了 constrained 前缀,并指出,将其与 callvirt 只要被调用的方法是在类型本身实现的(而不是基类),就不会触发值类型的框选。我不确定的是,该类型是否会始终保持 的一个值类型时,我们到这里。

c# generics clr boxing type-constraints
3个回答
6
投票

正如你已经想明白了,当一个 struct 传递给通用方法,它将不会被装箱。

运行时为每个 "类型参数 "创建新的方法。当你调用一个带有值类型的泛型方法时,你实际上是在调用一个为相应值类型创建的专用方法。所以没有必要装箱。

当调用在结构类型中没有直接实现的接口方法时,就会发生框选。Spec在这里调用了这个。

如果thisType是一个值类型,而thisType没有实现方法,那么ptr就会被取消引用,装箱,并作为'this'指针传递给callvirt方法指令。

最后一种情况只有在方法被定义在Object、ValueType或Enum上,并且没有被thisType覆盖的情况下才会发生。在这种情况下,装箱会导致原始对象的复制。但是,由于Object、ValueType和Enum的方法都没有修改对象的状态,所以无法发现这个事实。

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

如何、何时、何地将通用方法具体化?

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


1
投票

一个很简单的测试就是简单地创建一个可突变的struct,并带有一个突变它的接口方法。 从通用方法中调用该接口方法,看看原来的结构是否被突变了。

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 并没有被框住。


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

输出:122

下面是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         
© www.soinside.com 2019 - 2024. All rights reserved.