C#/.Net 泛型如何知道它们的参数类型?
How do C#/.Net generics know their parameter types?
在 C# 中,泛型函数或 class 知道其泛型参数的类型。这意味着动态类型信息,如 is
或 as
可用(与 Java 不同,后者不可用)。
我很好奇,编译器如何向泛型方法提供这种类型信息?对于 classes,我可以想象实例可以简单地具有指向该类型的指针,但对于泛型函数我不确定,也许只是一个隐藏参数?
如果泛型被保留到 IL 级别,我相信它们是,那么我想知道在那个级别是如何完成的。
clr 运行时在第一次执行时及时分别编译每个方法。你可以看到这一点,如果你在多行方法中的某处使用类型并且缺少定义该类型的 dll。在方法的第一行设置一个断点。在调用该方法时,会抛出类型加载异常。调试器未命中断点。现在将方法分成三个子方法。中间的应该包含缺少类型的行。现在您可以使用调试器进入方法,也可以进入第一个新方法,但是在调用第二个方法时会抛出异常。这是因为该方法是在首次调用时编译的,然后 compiler/linker 才发现缺少的类型。
回答您的问题:正如其他人所指出的,IL 支持泛型。在执行时,当您第一次创建 List 时,会编译构造函数代码(用 int 替换类型参数)。如果您随后第一次创建 List,代码将再次编译,并使用字符串作为类型参数。您可以看到它好像具有具体类型的具体 类 是在运行时动态生成的。
由于您已编辑问题以将其从 C# 编译器扩展到 JIT 编译器,下面是该过程的概述,以 List<T>
为例。
正如我们所确定的,List<T>
class 只有一个 IL 表示。此表示有一个类型参数,对应于 C# 代码中看到的 T
类型参数。正如 Holger Thiemann 在他的评论中所说,当您将 List<>
class 与给定类型参数一起使用时,JIT 编译器会为该类型参数创建 class 的本机代码表示。
但是,对于引用类型,它只编译一次本机代码并将其重新用于所有其他引用类型。这是可能的,因为在虚拟执行系统(VES,通常称为 "runtime")中, 只有一种引用类型, 在规范中称为 O
(见 I.12.1 段,table I.6,在标准中:http://www.ecma-international.org/publications/standards/Ecma-335.htm)。此类型定义为 "native size object reference to managed memory."
换句话说,VES 的(虚拟)评估堆栈中的所有对象都由一个 "object reference"(实际上是一个指针)表示,就其本身而言,它本质上是无类型的。那么 VES 如何确保我们不使用不兼容类型的成员呢?是什么阻止我们在 System.Random
的实例上调用 string.Length
属性?
为了加强类型安全,VES 使用描述每个对象引用的静态类型的元数据,将方法调用的接收者的类型与方法的元数据令牌标识的类型进行比较(这适用于其他成员类型的访问以及)。
例如,要调用对象的class方法,对该对象的引用必须在虚拟计算堆栈的顶部。由于方法的元数据和对 "stack transition" 的分析,此引用的静态类型是已知的——每个 IL 指令引起的堆栈状态变化。 call
或 callvirt
指令然后通过包含表示该方法的元数据标记来指示要调用的方法,这当然指示定义该方法的类型。
VES "verifies" 编译之前的代码,将引用的类型与方法的类型进行比较。如果类型不兼容,验证失败,程序崩溃。
这对泛型类型参数和非泛型类型参数一样有效。为了实现这一点,VES 限制了可以在引用上调用的方法type 是不受约束的泛型类型参数。唯一允许的方法是在 System.Object
上定义的方法,因为所有对象都是该类型的实例。
对于受约束的参数类型,该类型的引用可以接收对约束类型定义的方法的调用。例如,如果您编写一个方法,其中限制类型 T
派生自 ICollection
,则可以在类型 [=12] 的引用上调用 ICollection.Count
getter =]. VES 知道调用此 getter 是安全的,因为它确保存储到堆栈中该位置的任何引用将是实现 ICollection
接口的某种类型的实例。因此,无论对象的实际类型是什么,JIT 编译器都可以使用相同的本机代码。
还要考虑依赖于泛型类型参数的字段。在 List<T>
的情况下,有一个 T[]
类型的数组保存列表中的元素。请记住,实际的内存数组将是 O
对象引用的数组。无论数组是 List<string>
还是 List<FileInfo>
.
的成员,构造该数组或读取或写入其元素的本机代码看起来都一样
因此,在 List<T>
等无约束泛型类型的范围内,T
引用与 System.Object
引用一样好。不过,泛型的优点是 VES 会用类型实参替换调用方作用域中的类型参数。换句话说,即使 List<string>
和 List<FileInfo>
在内部 相同地对待它们的元素 ,调用者也会看到 Find
方法 returns一个string
,而另一个returns一个FileInfo
.
最后,因为所有这些都是通过 IL 中的元数据实现的,并且因为 VES 在加载和 JIT 编译类型时使用元数据,所以可以在 运行 时间通过反射提取信息.
您询问了转换(包括 is
和 as
)如何作用于泛型类型参数的变量。由于所有对象都存储有关其自身类型的元数据,因此所有转换的工作方式与使用变量类型 object
相同。询问对象的类型并做出运行时决定。
当然这个技巧只对引用类型有效。对于值类型,JIT 为用于实例化泛型类型参数的每种值类型编译一个专门的本机方法。在那个专门的方法中 T
的类型是完全已知的。不再需要 "magic"。因此,值类型参数是 "boring" 的情况。对于 JIT,它看起来根本就没有泛型类型参数。
typeof(T)
如何工作?该值作为隐藏参数传递给通用方法。这也是 someObj as T
能够工作的方式。我很确定它被编译为对运行时助手的调用(例如 RuntimeCastHelper(someObj, typeof(T))
)。
how does the compiler provides this type information to the generic methods?
tl;博士
它通过有效地复制与它一起使用的每个唯一类型的方法来提供类型信息。
现在,对于那些想阅读更多内容的人...;)
一旦你得到一个小例子,答案实际上很简单。
让我们从这个开始:
public static class NonGenericStaticClass
{
public static string GenericMethod<T>(T value)
{
if(value is Foo)
{
return "Foo";
}
else if(value is Bar)
{
return "Bar";
}
else
{
return string.Format("It's a {0}!", typeof(T).Name);
}
}
}
// ...
static void Main()
{
// Prints "It's a Int32!"
Console.WriteLine(NonGenericStaticClass.GenericMethod<int>(100));
// Prints "Foo"
Console.WriteLine(NonGenericStaticClass.GenericMethod<Foo>(new Foo()))
// Prints "It's a Int32!"
Console.WriteLine(NonGenericStaticClass.GenericMethod<int>(20));
}
现在,正如其他人已经指出的那样,IL 本身就支持泛型,因此 C# 编译器实际上并没有对这个示例做太多事情。然而,当即时编译器出现将 IL 转换为机器代码时,它必须将通用代码转换为非通用代码。
为此,.Net 即时编译器有效地复制了与它一起使用的每种不同类型的方法。
如果生成的代码是在 C# 中,它可能看起来像这样:
public static class NonGenericStaticClass
{
// The JIT Compiler might rename these methods after their
// representative types to avoid any weird overload issues, but I'm not sure
public static string GenericMethod(Int32 value)
{
// Note that the JIT Compiler might optimize much of this away
// since the first 2 "if" statements are always going to be false
if(value is Foo)
{
return "Foo";
}
else if(value is Bar)
{
return "Bar";
}
else
{
return string.Format("It's a {0}!", typeof(Int32).Name);
}
}
public static string GenericMethod(Foo value)
{
if(value is Foo)
{
return "Foo";
}
else if(value is Bar)
{
return "Bar";
}
else
{
return string.Format("It's a {0}!", typeof(Foo).Name);
}
}
}
// ...
static void Main()
{
// Notice how we don't need to specify the type parameters any more.
// (of course you could've used generic inference, but that's beside the point),
// That is because they are essentially, but not necessarily, overloads of each other
// Prints "It's a Int32!"
Console.WriteLine(NonGenericStaticClass.GenericMethod(100));
// Prints "Foo"
Console.WriteLine(NonGenericStaticClass.GenericMethod(new Foo()))
// Prints "It's a Int32!"
Console.WriteLine(NonGenericStaticClass.GenericMethod(20));
}
一旦您生成了非泛型方法,您就可以通过 static dispatch.
的巧妙使用确切地知道您正在处理的是什么类型
现在,我表示转换的方式和实际完成转换的方式之间显然会有差异,但这就是要点。
此外,同样的过程也适用于泛型类型。
For some contrast,Java 编译器 "cheats" 泛型。 Java 不是像 .Net 那样生成新的类型和方法,而是在您希望值属于某种类型的地方插入强制转换。
因此,我们的 typeof(T)
在 Java 世界中是不可能的,相反我们必须使用 getClass()
方法。
在 C# 中,泛型函数或 class 知道其泛型参数的类型。这意味着动态类型信息,如 is
或 as
可用(与 Java 不同,后者不可用)。
我很好奇,编译器如何向泛型方法提供这种类型信息?对于 classes,我可以想象实例可以简单地具有指向该类型的指针,但对于泛型函数我不确定,也许只是一个隐藏参数?
如果泛型被保留到 IL 级别,我相信它们是,那么我想知道在那个级别是如何完成的。
clr 运行时在第一次执行时及时分别编译每个方法。你可以看到这一点,如果你在多行方法中的某处使用类型并且缺少定义该类型的 dll。在方法的第一行设置一个断点。在调用该方法时,会抛出类型加载异常。调试器未命中断点。现在将方法分成三个子方法。中间的应该包含缺少类型的行。现在您可以使用调试器进入方法,也可以进入第一个新方法,但是在调用第二个方法时会抛出异常。这是因为该方法是在首次调用时编译的,然后 compiler/linker 才发现缺少的类型。
回答您的问题:正如其他人所指出的,IL 支持泛型。在执行时,当您第一次创建 List 时,会编译构造函数代码(用 int 替换类型参数)。如果您随后第一次创建 List,代码将再次编译,并使用字符串作为类型参数。您可以看到它好像具有具体类型的具体 类 是在运行时动态生成的。
由于您已编辑问题以将其从 C# 编译器扩展到 JIT 编译器,下面是该过程的概述,以 List<T>
为例。
正如我们所确定的,List<T>
class 只有一个 IL 表示。此表示有一个类型参数,对应于 C# 代码中看到的 T
类型参数。正如 Holger Thiemann 在他的评论中所说,当您将 List<>
class 与给定类型参数一起使用时,JIT 编译器会为该类型参数创建 class 的本机代码表示。
但是,对于引用类型,它只编译一次本机代码并将其重新用于所有其他引用类型。这是可能的,因为在虚拟执行系统(VES,通常称为 "runtime")中, 只有一种引用类型, 在规范中称为 O
(见 I.12.1 段,table I.6,在标准中:http://www.ecma-international.org/publications/standards/Ecma-335.htm)。此类型定义为 "native size object reference to managed memory."
换句话说,VES 的(虚拟)评估堆栈中的所有对象都由一个 "object reference"(实际上是一个指针)表示,就其本身而言,它本质上是无类型的。那么 VES 如何确保我们不使用不兼容类型的成员呢?是什么阻止我们在 System.Random
的实例上调用 string.Length
属性?
为了加强类型安全,VES 使用描述每个对象引用的静态类型的元数据,将方法调用的接收者的类型与方法的元数据令牌标识的类型进行比较(这适用于其他成员类型的访问以及)。
例如,要调用对象的class方法,对该对象的引用必须在虚拟计算堆栈的顶部。由于方法的元数据和对 "stack transition" 的分析,此引用的静态类型是已知的——每个 IL 指令引起的堆栈状态变化。 call
或 callvirt
指令然后通过包含表示该方法的元数据标记来指示要调用的方法,这当然指示定义该方法的类型。
VES "verifies" 编译之前的代码,将引用的类型与方法的类型进行比较。如果类型不兼容,验证失败,程序崩溃。
这对泛型类型参数和非泛型类型参数一样有效。为了实现这一点,VES 限制了可以在引用上调用的方法type 是不受约束的泛型类型参数。唯一允许的方法是在 System.Object
上定义的方法,因为所有对象都是该类型的实例。
对于受约束的参数类型,该类型的引用可以接收对约束类型定义的方法的调用。例如,如果您编写一个方法,其中限制类型 T
派生自 ICollection
,则可以在类型 [=12] 的引用上调用 ICollection.Count
getter =]. VES 知道调用此 getter 是安全的,因为它确保存储到堆栈中该位置的任何引用将是实现 ICollection
接口的某种类型的实例。因此,无论对象的实际类型是什么,JIT 编译器都可以使用相同的本机代码。
还要考虑依赖于泛型类型参数的字段。在 List<T>
的情况下,有一个 T[]
类型的数组保存列表中的元素。请记住,实际的内存数组将是 O
对象引用的数组。无论数组是 List<string>
还是 List<FileInfo>
.
因此,在 List<T>
等无约束泛型类型的范围内,T
引用与 System.Object
引用一样好。不过,泛型的优点是 VES 会用类型实参替换调用方作用域中的类型参数。换句话说,即使 List<string>
和 List<FileInfo>
在内部 相同地对待它们的元素 ,调用者也会看到 Find
方法 returns一个string
,而另一个returns一个FileInfo
.
最后,因为所有这些都是通过 IL 中的元数据实现的,并且因为 VES 在加载和 JIT 编译类型时使用元数据,所以可以在 运行 时间通过反射提取信息.
您询问了转换(包括 is
和 as
)如何作用于泛型类型参数的变量。由于所有对象都存储有关其自身类型的元数据,因此所有转换的工作方式与使用变量类型 object
相同。询问对象的类型并做出运行时决定。
当然这个技巧只对引用类型有效。对于值类型,JIT 为用于实例化泛型类型参数的每种值类型编译一个专门的本机方法。在那个专门的方法中 T
的类型是完全已知的。不再需要 "magic"。因此,值类型参数是 "boring" 的情况。对于 JIT,它看起来根本就没有泛型类型参数。
typeof(T)
如何工作?该值作为隐藏参数传递给通用方法。这也是 someObj as T
能够工作的方式。我很确定它被编译为对运行时助手的调用(例如 RuntimeCastHelper(someObj, typeof(T))
)。
how does the compiler provides this type information to the generic methods?
tl;博士 它通过有效地复制与它一起使用的每个唯一类型的方法来提供类型信息。
现在,对于那些想阅读更多内容的人...;) 一旦你得到一个小例子,答案实际上很简单。
让我们从这个开始:
public static class NonGenericStaticClass
{
public static string GenericMethod<T>(T value)
{
if(value is Foo)
{
return "Foo";
}
else if(value is Bar)
{
return "Bar";
}
else
{
return string.Format("It's a {0}!", typeof(T).Name);
}
}
}
// ...
static void Main()
{
// Prints "It's a Int32!"
Console.WriteLine(NonGenericStaticClass.GenericMethod<int>(100));
// Prints "Foo"
Console.WriteLine(NonGenericStaticClass.GenericMethod<Foo>(new Foo()))
// Prints "It's a Int32!"
Console.WriteLine(NonGenericStaticClass.GenericMethod<int>(20));
}
现在,正如其他人已经指出的那样,IL 本身就支持泛型,因此 C# 编译器实际上并没有对这个示例做太多事情。然而,当即时编译器出现将 IL 转换为机器代码时,它必须将通用代码转换为非通用代码。 为此,.Net 即时编译器有效地复制了与它一起使用的每种不同类型的方法。
如果生成的代码是在 C# 中,它可能看起来像这样:
public static class NonGenericStaticClass
{
// The JIT Compiler might rename these methods after their
// representative types to avoid any weird overload issues, but I'm not sure
public static string GenericMethod(Int32 value)
{
// Note that the JIT Compiler might optimize much of this away
// since the first 2 "if" statements are always going to be false
if(value is Foo)
{
return "Foo";
}
else if(value is Bar)
{
return "Bar";
}
else
{
return string.Format("It's a {0}!", typeof(Int32).Name);
}
}
public static string GenericMethod(Foo value)
{
if(value is Foo)
{
return "Foo";
}
else if(value is Bar)
{
return "Bar";
}
else
{
return string.Format("It's a {0}!", typeof(Foo).Name);
}
}
}
// ...
static void Main()
{
// Notice how we don't need to specify the type parameters any more.
// (of course you could've used generic inference, but that's beside the point),
// That is because they are essentially, but not necessarily, overloads of each other
// Prints "It's a Int32!"
Console.WriteLine(NonGenericStaticClass.GenericMethod(100));
// Prints "Foo"
Console.WriteLine(NonGenericStaticClass.GenericMethod(new Foo()))
// Prints "It's a Int32!"
Console.WriteLine(NonGenericStaticClass.GenericMethod(20));
}
一旦您生成了非泛型方法,您就可以通过 static dispatch.
的巧妙使用确切地知道您正在处理的是什么类型现在,我表示转换的方式和实际完成转换的方式之间显然会有差异,但这就是要点。 此外,同样的过程也适用于泛型类型。
For some contrast,Java 编译器 "cheats" 泛型。 Java 不是像 .Net 那样生成新的类型和方法,而是在您希望值属于某种类型的地方插入强制转换。
因此,我们的 typeof(T)
在 Java 世界中是不可能的,相反我们必须使用 getClass()
方法。