在泛型类实例化时创建了多少实例



考虑这样的类:

class Foo<T>
{
   private T myField;
   public void Set(T x)
   {
      myField = x;
   }
}

然后实例化T等于int, bool(值类型)和String, List

如何创建实例化以及它们看起来如何?

我对Java和c#感兴趣。

从我读到的Java将创建一个通用类,基本上是casting,而在c#中,intbool将有两个类,StringList将有一个类,因为它们是参考值(这是真的吗?静态场呢?)。

这里我假设是常规的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

上面的代码在c#中可以很好地工作,分别打印:

3
3
这是字符串
这是一个字符串

为什么?

Java如何工作

在Java中,编译器将Foo类创建为原始类型,忽略类型定义中的任何泛型信息。这被称为类型擦除(在Samuel Edwin Ward的回答中也有解释)。编译器将尝试检测(或者更好地说——猜测)类型使用,并尝试通过在生成的代码中添加类型强制转换来补偿类型擦除,分析类的使用以实现这一点。因此,一般情况下,结果将与类型Foo是泛型一样。问题是类型Foo只存在一次,并且它的staticMemberFoo<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>.staticMemberint类型的完全不同的成员。

最新更新