没有值实例装箱的接口的泛型和用法

Generics and usage of interfaces without boxing of value instances

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

据我所知,为了使用接口方法,值实例应该被装箱,因为 "virtual" 函数的调用需要引用对象中包含的 "private" 信息(它包含在所有参考对象(它也有一个同步块))

这就是为什么我决定分析一个简单程序的 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> 接口(否则编译器如何在不装箱的情况下发出值实例的虚方法调用??)。但是我很惊讶。这是Main的一段IL代码:

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 standard (Common Language Infrastructure) III.4.21) Value types are not usually created using newobj. They are usually allocated either as arguments or local variables, using newarr (for zero-based, one-dimensional arrays), or as fields of objects. Once allocated, they are initialized using initobj. However, the newobj instruction can be used to create a new instance of a value type on the stack, that can then be passed as an argument, stored in a local, etc.

所以,我决定检查一下 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'

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

As I understand, generics is an elegant solution to resolve issues with extra boxing/unboxing procedures which occur within generic collections like List<T>.

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

if I want to pass a value instance which implements an interface of a generic method, will boxing be performed?

有时,是的。但由于装箱成本很高,CLR 寻找避免它的方法。

I thought that add(new MyInt(1), new MyInt(2)) will use boxing operations because the add generic method uses the INum<a> interface

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

public static MyInt add(MyInt lhs, MyInt rhs)

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

It seems like newobj does not create a value instance on heap, for values it creates them on stack.

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

add does not contain box instructions either

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

constrained callvirt 具有以下语义:

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

  • 参数必须在堆栈上。他们是; INum<MyInt>.add 的参数是一个 MyInt,同样,它是按值传递的,值在 ldarg.

  • 的堆栈上
  • 如果接收者是引用类型,那么我们就解引用刚刚创建的双引用来获取引用,虚拟调用正常发生。 (当然,抖动可以免费优化掉这种双重引用!请记住,我描述的所有这些语义都是 IL 程序的虚拟机的,而不是你 运行 它所在的真实机器的! )

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

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

What's wrong with my assumptions?

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

Can generics invoke virtual methods of values without boxing?

是的。