首页 > 解决方案 > 没有装箱值实例的接口的泛型和使用

问题描述

据我了解,泛型是一种优雅的解决方案,可以解决出现在泛型集合(如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);
    }
}

我认为这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

此类列表没有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'

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

标签: c#genericscompiler-optimizationjit

解决方案


据我了解,泛型是一种优雅的解决方案,可以解决出现在泛型集合(如List<T>.

消除拳击是仿制药的设计方案,是的。但正如 Damien 在评论中指出的那样,更通用的功能是启用更简洁、更类型安全的代码。

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

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

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

我明白你为什么要扣除,但这是错误的。您调用的方法的主体如何使用这些信息是无关紧要的。问题是:您正在调用的方法的签名是什么?C# 类型推断确定您正在调用add<MyInt>,因此签名等效于调用:

public static MyInt add(MyInt lhs, MyInt rhs)

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

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

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

add也不包含盒子说明

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

受约束的 callvirt 具有以下语义:

  • 堆栈上必须有对接收者的引用。有:ldarga将接收者的地址放入堆栈。如果接收者是引用类型,则包含引用的变量的地址将在堆栈上。如果是值类型,那么保存该值类型的变量的地址会在栈上。(同样,这是我们在这里推理的虚拟机堆栈。)

  • 参数必须在堆栈上。他们是; 的参数INum<MyInt>.add是 a MyInt,同样,它是按值传递的,并且该值在来自 的堆栈上ldarg

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

  • 如果接收者是一个值类型并且该值类型实现了您正在调用的方法,那么该值类型的方法将被正常调用:也就是说,不将值装箱。 您的示例就是这种情况,因此我们避免使用拳击。

  • 如果接收者是一个没有实现你正在调用的方法的值类型,那么这个值类型被装箱,并且该方法被调用并引用作为接收者的盒子。给读者的练习:创建一个属于这种情况的程序。

我的假设有什么问题?

您假设通过接口对值类型的方法调用必须将接收器装箱,但事实并非如此。

泛型可以在不装箱的情况下调用值的虚拟方法吗?

是的。


推荐阅读