考虑这样的课程:
class Foo<T>
{
private T myField;
public void Set(T x)
{
myField = x;
}
}
然后您使用T
等于int
,bool
(值类型)和String
,List
进行实例化。
如何创建实例化以及它们的外观?
我对Java和C#感兴趣。
从我读到的内容中,Java会创建一个基本类型的通用类,而在C#中,int
和bool
会有两个类,String
只有一个类, List
因为它们是参考值(这是真的吗?静态字段怎么样?)。
答案 0 :(得分:8)
请注意,我假设是常规CLR - 没有AOT等。
在IL级别:Foo<T>
有一个定义;无论T
是什么,IL都是相同的(共享的)。
在JIT级别:对于所有(共享)引用类型参数,一次一次一次,每次> em>(单独)值类型参数。因此,Foo<string>
和Foo<List<...>>
的JIT是共享的,Foo<int>
和Foo<bool>
的每个JIT都是单独的JIT。
创建的对象/实例数与new Foo<...>(...)
次调用(或Activator.CreateInstance(...)
等)的数量相同。
答案 1 :(得分:4)
在JVM中,只有一个类:由类型擦除产生的类。
在CLR中,为类型参数的每个值构造一个闭合类型,每个值都有自己不同的静态字段副本;具有引用类型参数的封闭类型之间的JIT代码共享是一种实现优化。因此,虽然Foo<String>
和Foo<List>
可能引用相同的JIT翻译方法实现,但它们不是同一类型,并且具有不同的静态字段。
答案 2 :(得分:3)
Java Generics由“类型擦除”实现。
编译器创建一个类Foo<T>
,它基本上具有代码Foo<Object>
。它不像C ++模板,其中基于类型实例化新类。编译比C ++版本更快,但确实阻止了一些在编译时进行的优化。我相信这个系统的主要原因是希望保持与前Generics Java的兼容性。
类型参数例如 Integer
中的Foo<Integer>
仅在编译时用于在程序员尝试传递Bar
时发出错误,并允许编译器假设返回T
的函数将返回T
,而在前Generics Java中,程序员必须转换结果。值得注意的是,Foo<Integer>
的任何方法都不会实际检查它们的参数是Integer
,除非程序员明确地写出来。
对于静态字段,因为对于不同的特化只有一个Foo
而不是单独的Foo
,所以类型变量是“非静态的”并且试图声明类型为{的静态成员{1}}没有意义。在我的编译器上,它失败并出现错误“无法从静态上下文引用非静态类型变量T”。
答案 3 :(得分:2)
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
。
以上代码可以在C#中正常工作,分别打印:
3
3
这是一个字符串
这是一个字符串
在Java中,编译器将Foo
类创建为原始类型,忽略类型定义中的任何通用信息。这称为类型擦除(also explained in Samuel Edwin Ward's answer)。编译器将尝试检测(或更好地说 - 猜测)类型使用并尝试通过在生成的代码中添加类型转换来补偿类型擦除,分析类的用途以实现此目的。因此,通常,结果将类似于Foo
类型是通用的。问题是类型Foo
只存在一次,其staticMember
是同一个Foo<Integer>
和Foo<String>
之间共享的实例。
在.NET中,Foo<int>
和Foo<string>
声明导致在编译期间生成不同类型(实际上在JIT阶段,Marc Gravell和Jeffrey Hantin在他们的各自的答案)。因此,Foo<string>
类与Foo<int>
完全不同。这导致每个属性都有自己的staticMember
属性,因此保证Foo<string>.staticMember
始终为string
类型,而Foo<int>.staticMember
是完全不同的成员int
类型。