据我了解,泛型是一种优雅的解决方案,可以解决诸如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'
我的假设出了什么问题?泛型可以调用无框的值的虚拟方法吗?
答案 0 :(得分:9)
据我所知,泛型是解决诸如
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#编译器验证是否满足约束。 这不会更改方法的调用约定。该方法需要两个MyInt
,并且您已经为其传递了两个MyInt
,它们是值类型,因此它们按值进行传递。
似乎newobj不会在堆上创建值实例,对于值,它会在堆栈上创建它们。
请确保清楚:它将在IL程序的抽象评估堆栈上创建它们。抖动是否将代码转换为将值放入当前线程的实际堆栈中的代码,这是抖动的实现细节。例如,它可以选择将它们放入寄存器中,或放入具有堆栈逻辑属性但实际上存储在堆中的数据结构中。
add
也不包含框内说明
是的,您只是没有看到它们。它包含一个受约束的callvirt,它是一个条件框。
受约束的callvirt具有以下语义:
在堆栈上必须有对接收者的引用。其中有:ldarga
将接收者的地址放在堆栈中。如果接收者是引用类型,则包含引用的变量的地址将在堆栈上。如果是值类型,则保存该值类型的变量的地址将在堆栈上。 (同样,这是我们在此处考虑的虚拟机堆栈。)
参数必须在堆栈上。他们是; INum<MyInt>.add
的参数是一个MyInt
,同样,它通过值传递,并且该值位于ldarg
的堆栈中。
如果接收者是引用类型,则我们取消引用刚刚创建的双重引用以获取引用,并且虚拟调用会正常进行。 (当然,抖动可以自由地优化这种双重引用!记住,我描述的所有这些语义都是IL程序的虚拟机的,而不是运行它的真实机器的!)
如果接收方是值类型,并且该值类型实现了您正在调用的方法,则通常会调用该值类型的方法:即,不对值进行装箱。 这是您的示例所在的情况,因此我们避免装箱。
如果接收方是未实现您要调用的方法的值类型,则将值类型装箱,然后以对接收方的引用作为参考来调用该方法。 锻炼读者:创建适合这种情况的程序。
我的假设出了什么问题?
您已经假设通过接口对值类型的方法的调用必须使接收方处于框内,但这不是事实。
泛型可以在不装箱的情况下调用值的虚拟方法吗?
是的