泛型方法是如何、何时以及在何处变得具体的?

这个问题让我想知道泛型方法的具体实现实际上是从哪里来的。我已经试过谷歌,但没有想出正确的搜索。

如果我们举个简单的例子:

class Program
{
public static T GetDefault<T>()
{
return default(T);
}


static void Main(string[] args)
{
int i = GetDefault<int>();
double d = GetDefault<double>();
string s = GetDefault<string>();
}
}

在我的脑海中,我总是假设在某个时刻,它会导致一个包含3个必要的具体实现的实现,比如,在幼稚的伪混乱中,我们会有这样一个逻辑上的具体实现,其中使用的特定类型会导致正确的堆栈分配等等。

class Program
{
static void Main(string[] args)
{
int i = GetDefaultSystemInt32();
double d = GetDefaultSystemFloat64();
string s = GetDefaultSystemString();
}


static int GetDefaultSystemInt32()
{
int i = 0;
return i;
}
static double GetDefaultSystemFloat64()
{
double d = 0.0;
return d;
}
static string GetDefaultSystemString()
{
string s = null;
return s;
}
}

查看泛型程序的 IL,它仍然是用泛型类型表示的:

.method public hidebysig static !!T  GetDefault<T>() cil managed
{
// Code size       15 (0xf)
.maxstack  1
.locals init ([0] !!T CS$1$0000,
[1] !!T CS$0$0001)
IL_0000:  nop
IL_0001:  ldloca.s   CS$0$0001
IL_0003:  initobj    !!T
IL_0009:  ldloc.1
IL_000a:  stloc.0
IL_000b:  br.s       IL_000d
IL_000d:  ldloc.0
IL_000e:  ret
} // end of method Program::GetDefault

那么,它是如何以及在什么时候决定在堆栈上分配一个 int、一个 double 和一个字符串并返回给调用者的呢?这是一个 JIT 过程的操作吗?我是不是完全想错了?

1981 次浏览

在 C # 中,泛型类型和方法的概念受到运行时本身的支持。C # 编译器不需要实际创建一个通用方法的具体版本。

实际的“具体的”泛型方法是由 JIT 在运行时创建的,并且不存在于 IL 中。第一次将泛型方法与类型一起使用时,JIT 将查看它是否已被创建,如果没有,则为该泛型类型构造适当的方法。

这是泛型和 C + + 中的模板之类的东西之间的根本区别之一。这也是泛型有很多局限性的主要原因——因为编译器实际上并没有为类型创建运行时实现,所以接口限制是通过编译时限制来处理的,这使得泛型在潜在用例方面比 C + + 中的模板有更多的局限性。但是,由于它们在运行时本身受到支持,因此可以以 C + + 和其他编译时创建的模板实现不支持的方式从库中创建泛型类型和使用。

泛型方法的实际机器代码一如既往地是在方法被抖动时创建的。在这一点上,抖动首先检查是否一个合适的候选人被抖动之前。这种情况非常普遍,具体运行时类型 T 是引用类型的方法的代码只需要生成一次,并且适用于每个可能的引用类型 T。T 上的约束确保这个机器代码总是有效的,之前由 C # 编译器检查过。

可能会为值类型的 T 生成额外的副本,它们的机器代码不同,因为 T 值不再是简单的指针。

所以,是的,在你的情况下,你会得到三种截然不同的方法。<string>版本可用于任何引用类型,但是没有其他版本。<int><double>版本符合“值类型为 T”的类别。

否则,这些方法的返回值将以不同的方式传递回调用方,这是一个优秀的示例。在 x64抖动时,字符串版本返回 RAX 寄存器的值,就像任何返回的指针值一样,int 版本返回 EAX 寄存器,double 版本返回 XMM0寄存器。