无需装箱值实例的接口的泛型和用法

LmT*_*oon 9 c# generics jit compiler-optimization

据我了解,泛型是一种优雅的解决方案,可以解决在通用集合中出现的额外装箱/拆箱过程的问题List.但我无法理解泛型如何解决在泛型函数中使用接口的问题.换句话说,如果我想传递一个实现泛型方法接口的值实例,那么会执行装箱吗?编译器如何处理这种情况?

据我所知,为了使用接口方法,值实例应该被加框,因为调用"虚拟"函数需要包含在引用对象中的"私有"信息(它包含在所有引用对象中(它还具有同步)块))

这就是为什么我决定分析IL一个简单程序的代码,看看是否在泛型函数中使用了任何装箱操作:

public class main_class
{
    public interface INum<a> { a add(a other); }
    public struct MyInt : INum<MyInt>
    {
        public MyInt(int _my_int) { Num = _my_int; }
        public MyInt add(MyInt other) => new MyInt(Num + other.Num);
        public int Num { get; }
    }

    public static a add<a>(a lhs, a rhs) where a : INum<a> => lhs.add(rhs);

    public static void Main()
    {
        Console.WriteLine(add(new MyInt(1), new MyInt(2)).Num);
    }
}
Run Code Online (Sandbox Code Playgroud)

我认为这add(new MyInt(1), new MyInt(2))将使用装箱操作,因为添加泛型方法使用INum<a>接口(否则编译器如何在没有装箱的情况下发出值实例的虚方法调用?).但我很惊讶.这是一段IL代码Main:

IL_0000: ldc.i4.1
IL_0001: newobj instance void main_class/MyInt::.ctor(int32)
IL_0006: ldc.i4.2
IL_0007: newobj instance void main_class/MyInt::.ctor(int32)
IL_000c: call !!0 main_class::'add'<valuetype main_class/MyInt>(!!0, !!0)
IL_0011: stloc.0
Run Code Online (Sandbox Code Playgroud)

此类列表没有box说明.似乎newobj不会在堆上创建值实例,因为它在堆栈上创建它们的值.以下是文档中的说明:

(ECMA-335标准(公共语言基础结构)III.4.21)通常不使用newobj创建值类型.它们通常使用newarr(对于从零开始的一维数组)或作为对象的字段分配为参数或局部变量.分配后,使用initobj初始化它们.但是,newobj指令可用于在堆栈上创建值类型的新实例,然后可以作为参数传递,存储在本地等等.

所以,我决定检查一下这个add功能.它非常有趣,因为它不包含盒子指令:

.method public hidebysig static 
!!a 'add'<(class main_class/INum`1<!!a>) a> (
    !!a lhs,
    !!a rhs
) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 15 (0xf)
    .maxstack 8

    IL_0000: ldarga.s lhs
    IL_0002: ldarg.1
    IL_0003: constrained. !!a
    IL_0009: callvirt instance !0 class main_class/INum`1<!!a>::'add'(!0)
    IL_000e: ret
} // end of method main_class::'add'
Run Code Online (Sandbox Code Playgroud)

我的假设有什么问题?泛型是否可以在没有装箱的情况下调用虚拟值方法?

Eri*_*ert 9

据我了解,泛型是一种优雅的解决方案,可以解决在通用集合中出现的额外装箱/拆箱过程的问题List<T>.

消除拳击是泛型的副设计方案,是的.但正如Damien在评论中指出的那样,更一般的功能是实现更简洁,更类型安全的代码.

如果我想传递一个实现泛型方法接口的值实例,是否会执行装箱?

有时候是.但由于拳击很昂贵,CLR会寻找避免它的方法.

我认为这add(new MyInt(1), new MyInt(2))将使用装箱操作,因为add generic方法使用INum<a>接口

我明白为什么你做了这个演绎,但这是错误的.您调用的方法的主体如何使用该信息是无关紧要的.问题是:你打电话的方法的签名是什么?C#类型推断确定您正在调用add<MyInt>,因此签名等同于调用:

public static MyInt add(MyInt lhs, MyInt rhs)
Run Code Online (Sandbox Code Playgroud)

现在,你正确地指出存在约束.C#编译器验证是否满足约束条件. 这不会改变方法的调用约定.该方法需要两个MyInts,你已经传递了两个MyInts,它们是值类型,因此它们是按值传递的.

似乎newobj不会在堆上创建值实例,因为它在堆栈上创建它们的值.

确保这一点很清楚:它在IL程序抽象评估堆栈上创建它们.抖动是否将代码转换为将值放在当前线程的实际堆栈上的代码是抖动的实现细节.例如,它可以选择将它们放在寄存器中,或者放入具有堆栈逻辑属性但实际存储在堆上的数据结构中,或者其他任何东西.

add 也不包含盒子说明

是的,它只是没有看到它们.它包含一个受约束的callvirt,它是一个条件框.

约束的callvirt具有语义:

  • 必须在堆栈上引用接收器.有:ldarga将接收器的地址放在堆栈上.如果接收器是引用类型,则包含引用的变量的地址将在堆栈上.如果它是值类型,则保存值类型的变量的地址将在堆栈上.(同样,这是我们在这里推论的虚拟机的堆栈.)

  • 参数必须在堆栈上.他们是; 参数INum<MyInt>.add是a MyInt,再次,它是通过值传递的,而值是在堆栈中的ldarg.

  • 如果接收者是引用类型,我们然后取消引用我们刚刚创建的双引用以获得引用,并且虚拟调用正常发生.(当然,抖动可以自由地优化掉这个双引用!请记住,我所描述的所有这些语义都是IL程序的虚拟机,而不是你运行它的真机器!)

  • 如果接收者是一个值类型,并且值类型实现了您正在调用的方法,则正常调用值类型的方法:即,不对该值进行装箱. 这是你的例子所在的情况,所以我们避免拳击.

  • 如果接收者是一个没有实现您正在调用的方法的值类型,则将值类型装箱,并且通过引用该框作为接收者来调用该方法.练习给读者:创建一个属于这种情况的程序.

我的假设有什么问题?

您已经假设通过接口调用值类型的方法必须将接收器打包,但事实并非如此.

泛型是否可以在没有装箱的情况下调用虚拟值方法?

是.