考虑这样的类:
class Foo<T>
{
private T myField;
public void Set(T x)
{
myField = x;
}
}
然后实例化T
等于int
, bool
(值类型)和String
, List
。
如何创建实例化以及它们看起来如何?
我对Java和c#感兴趣。
从我读到的Java将创建一个通用类,基本上是casting,而在c#中,int
和bool
将有两个类,String
和List
将有一个类,因为它们是参考值(这是真的吗?静态场呢?)。
这里我假设是常规的CLR -没有AOT等
在IL级别:Foo<T>
有一个定义;无论T
, IL是相同的(共享的)。
在JIT级别:类型(每个泛型参数)一次对于所有(共享)引用类型参数,一次每个(单独)值类型参数。因此,Foo<string>
和Foo<List<...>>
的JIT是共享的,Foo<int>
和Foo<bool>
的JIT是单独的。
创建的对象/实例的数量与new Foo<...>(...)
调用的数量相同(或Activator.CreateInstance(...)
等)。
在JVM中,有且只有一个类:由类型擦除产生的类。
在CLR中,为类型形参的每个值构造一个封闭类型,每个值都有自己不同的静态字段副本;具有引用类型参数的闭类型之间的JIT代码共享是一种实现优化。因此,虽然Foo<String>
和Foo<List>
可能引用相同的jit翻译方法实现,但它们不是相同的类型,并且具有不同的静态字段。
Java泛型是通过"类型擦除"实现的。
编译器创建一个类Foo<T>
,它本质上具有Foo<Object>
所具有的代码。它不像c++模板那样根据类型实例化一个新类。与c++版本相比,它的编译速度更快,但在编译时却无法进行某些优化。我相信这个系统的主要理由是希望保持与泛型之前的Java的兼容性。
类型参数,例如。 Foo<Integer>
中的Integer
仅在编译时使用,如果程序员试图传递Bar
in,则会发出错误,并允许编译器假设返回T
的函数将返回T
,而在前泛型Java中,程序员必须强制转换结果。同样值得注意的是,Foo<Integer>
的所有方法都不会检查它们的参数是否为Integer
s,除非程序员显式地写了这一点。
对于静态字段,因为只有一个Foo
而不是不同专门化的单独的Foo
,所以类型变量是"非静态的",并且试图声明T
类型的静态成员是没有意义的。在我的编译器上,它失败了,错误"非静态类型变量T不能从静态上下文中引用"。
Java和。net处理泛型的方式不同。让我们看一下下面的代码:
public class Foo<T>
{
private static Object staticMember;
public T getStaticMember() {
return (T) staticMember;
}
private T instanceMember;
public T getInstanceMember() {
return instanceMember;
}
public Foo(T value)
{
if (staticMember == null)
{
staticMember = value;
}
this.instanceMember = value;
}
}
就质量而言,这不是很好的代码,但为了示例,它是Java和c#的工作代码。
在Java中,运行时只知道Foo
类。所以下面的代码
Foo<Integer> foo = new Foo<Integer>(3);
System.out.println(foo.getStaticMember()); // >> 3
System.out.println(foo.getInstanceMember()); // >> 3
将被编译成类似的东西:
Foo foo = new Foo(3);
System.out.println(((Integer) foo.getInstanceMember()).toString()); // 3
System.out.println(((Integer) foo.getStaticMember()).toString()); // 3
可以看到,泛型被转换为静态成员的类型强制转换。
下面的代码将在Java中失败并导致运行时异常:
Foo<Integer> foo1 = new Foo<Integer>(3);
Foo<String> foo2 = new Foo<String>("This is a string");
System.out.println(foo1.getInstanceMember()); // >> 3
System.out.println(foo2.getInstanceMember()); // >> This is a string
System.out.println(foo1.getStaticMember()); // >> 3
System.out.println(foo2.getStaticMember()); // Invalid cast exception
,因为它将被视为:
Foo foo1 = new Foo(3);
Foo foo2 = new Foo("This is a string");
System.out.println(((Integer) foo1.getInstanceMember()).toString()); // >> 3
System.out.println(((String) foo2.getInstanceMember()).toString());
// >> This is a string
System.out.println(((Integer) foo1.getStaticMember()).toString()); // >> 3
System.out.println(((String) foo2.getStaticMember()).toString());
// Invalid cast exception
最后一行将尝试将静态成员Integer
强制转换为String
。
3
3
这是字符串
这是一个字符串
为什么?
Java如何工作
在Java中,编译器将Foo
类创建为原始类型,忽略类型定义中的任何泛型信息。这被称为类型擦除(在Samuel Edwin Ward的回答中也有解释)。编译器将尝试检测(或者更好地说——猜测)类型使用,并尝试通过在生成的代码中添加类型强制转换来补偿类型擦除,分析类的使用以实现这一点。因此,一般情况下,结果将与类型Foo
是泛型一样。问题是类型Foo
只存在一次,并且它的staticMember
是Foo<Integer>
和Foo<String>
共享的同一个实例。
.NET如何工作
在。net中,Foo<int>
和Foo<string>
声明导致在编译期间生成不同的类型(实际上是在JIT阶段,Marc Gravell和Jeffrey Hantin在他们各自的回答中澄清了这一点)。因此,Foo<string>
类与Foo<int>
类是完全不同的类型。这导致它们每个都有自己的staticMember
属性,因此可以保证Foo<string>.staticMember
始终是string
类型,而Foo<int>.staticMember
是int
类型的完全不同的成员。