【问题标题】:Is it possible for Scala to have reified generics without changing the JVM?Scala 是否有可能在不更改 JVM 的情况下实现泛型?
【发布时间】:2009-08-31 15:04:33
【问题描述】:

我最近开始学习 Scala,很失望(但并不惊讶)他们的泛型也是通过类型擦除实现的。

我的问题是,Scala 是否有可能具有具体化的泛型,或者 JVM 是否需要以某种方式进行更改?如果 JVM 确实需要改变,究竟需要改变什么?

【问题讨论】:

    标签: java generics scala jvm


    【解决方案1】:

    否 - 如果该字节码不支持具体化的泛型,Scala 就不可能作为与 Java 等效的字节码运行。

    当你问“需要改变什么?”,答案是:字节码规范。目前,字节码不允许定义变量的参数化类型。已决定将break backwards compatibilitygenerics would have to be implemented via type erasure 作为对字节码的修改以支持具体化的泛型。

    为了解决这个问题,Scala 利用其implicit 机制的强大功能定义了一个Manifest,可以在任何范围内导入它以在运行时发现类型信息。清单是实验性的,基本上没有记录,但它们 are coming as part of the library in 2.8。这是Scala reified generics / Manifests上的另一个好资源

    【讨论】:

    • 在你说话时添加链接 :-)
    • 我不知道 Scala 的人正在这样做。这很酷。
    • 小修正:自 2.7.2 以来,清单一直是标准库的实验部分。我不知道它们在 2.8 中是否会减少实验性。
    • IBM X10 在编译成 Java 时提供了具体化的泛型(它也可以编译成 C++)。看看这个:x10.sourceforge.net/documentation/papers/X10Workshop2012/slides/…
    • 这个答案具有误导性 - 没有基本的 JVM 限制阻止您实现具体化的泛型。所有具体化的泛型都要求编译器可以根据需要发出类型专用的类,这在使用当前 JVM 字节码的 JVM 上是完全可能的。选择以 Java 风格实现泛型(使用类型擦除)主要是语言/编译器设计决策(Java 出于保持向后兼容性的原因而采用)。
    【解决方案2】:

    为了补充 oxbow_lakes,Stack Overflow 上有一个关于 how to get around type erasure in Scala 的问题。

    【讨论】:

      【解决方案3】:

      "implicit Manifest" 是一个 Scala 编译器技巧,它不会使 Scala 中的泛型具体化。 Scala 编译器,当它看到一个带有“implicit m:Manifest[A]”参数的函数并且它知道调用站点上 A 的泛型类型时,它将包装 A 的类及其泛型在 Manifest 中输入参数并使其在函数中可用。但是,如果它无法确定 A 的真实类型,那么它就无法创建 Manifest。换句话说,如果内部函数需要,Manifest 必须沿着函数调用链传递。

      scala> def typeName[A](a: A)(implicit m: reflect.Manifest[A]) = m.toString
      typeName: [A](a: A)(implicit m: scala.reflect.Manifest[A])java.lang.String
      
      scala> typeName(List(1))
      res6: java.lang.String = scala.collection.immutable.List[int]
      
      scala> def foo[A](a: A) = typeName(a)
      <console>:5: error: could not find implicit value for parameter m:scala.reflect.Manifest[A].
             def foo[A](a: A) = typeName(a)
                                        ^
      
      scala> def foo[A](a: A)(implicit m: reflect.Manifest[A]) = typeName(a)
      foo: [A](a: A)(implicit m: scala.reflect.Manifest[A])java.lang.String
      
      scala> foo(Set("hello"))
      res8: java.lang.String = scala.collection.immutable.Set[java.lang.String]
      

      【讨论】:

      • 他们为什么不直接在每个具有泛型的方法调用中添加一个 Manifest 呢?清单会导致性能损失吗?
      • 我不确定这里是否有人说过清单使泛型具体化(这似乎是您的暗示)。我说过可以导入 Manifest 以在运行时派生类型信息,而具体化的泛型则需要更改字节码规范。
      • @oxbow_lakes 如果我的回答是这样的话,我很抱歉;我并不是要暗示有人说“清单使泛型具体化”。我只想指出使用 Manifest 的一些限制。
      【解决方案4】:

      补充 oxbow_lakes 的回答:这是不可能的,而且似乎永远不会发生(至少很快)。

      JVM 不支持物化泛型的(可反驳的)原因似乎是:

      • 性能较低。
      • 它破坏了向后兼容性。可以解决复制和修复大量库的问题。
      • 可以使用清单来实现:“解决方案”和最大的障碍。

      参考资料:

      您可以轻松地对其进行基准测试,并看到它对性能的影响非常 显。尤其是内存消耗增加很多。

      我相信要走的路是按照我们的方式进行可选的具体化 开始在 Scala 中使用 Manifests/TypeTags。

      如果可以,并将其与运行时专业化相结合,您可以瞄准 高性能和通用代码。然而,这可能是目标 Scala 2.12 或 2.13。

      【讨论】:

        【解决方案5】:

        一旦 scalac 成为编译器,它就有可能使用实现具体泛型所需的任何数据结构来修饰生成的代码。

        我的意思是 scalac 将有能力看到...

        // definition
        class Klass[T] {
          value : T
        }
        
        //calls
        floats  = Klass[float]
        doubles = Klass[double]
        

        ...并“扩展”成这样的东西:

        // definition
        class Klass_float {
          value : float
        }
        class Klass_double {
          value : double
        }
        
        // calls
        floats  = Klass_float
        doubles = Klass_double
        

        编辑

        重点是:编译器有能力创建所有必要的数据结构,这些数据结构证明在运行时提供额外的类型信息是必要的。一旦这个类型信息可用,Scala 运行时就会利用它,并可以执行我们可以想象的所有类型感知操作。 JVM 是否为具体化的泛型提供字节码并不重要。这项工作不是由 JVM 完成的,而是由 Scala 库完成的。

        如果你已经写了一个符号调试器(我写了!),你知道你基本上可以将编译器在编译时拥有的所有信息“转储”到生成的二进制文件中,采用任何数据组织证明更方便进一步处理。这是完全相同的想法:“转储”Scala 编译器拥有的所有类型信息。

        简而言之,我不明白为什么它不可能,这与 JVM 是否为具体泛型提供本机操作无关。 JVM 字节码与具体化的泛型无关。这种事情是语言规范、编译器特性和运行时库支持的问题。

        另一个修改

        IBM X10 展示了我所说的能力:它将 X10 代码编译为 Java 代码,在 Java 平台上利用具体化的泛型。正如我之前提到的:它可以做到(IBM X10 做到了!)但是这种特性涉及语言规范、编译器支持(或编译器插件)以及运行时库中的足够支持。更多信息请访问:http://x10.sourceforge.net/documentation/papers/X10Workshop2012/slides/Takeuchi.pdf

        【讨论】:

        • 这有两个问题。首先,你乘以你的代码大小。其次,您不知道必须在编译时生成的所有类。例如,您通常/可能仅以编译形式拥有的库中的集合实现。您会希望它与您的新课程 RichardsFoo 一起使用,不是吗?
        • 这很容易通过存储 erased 编译类来绕过。因此,当 scalac 遇到 Klass[SomethingStrange] 时,它会获取类型擦除的 Klass 并实例化 Klass_SomethingStrange。这就是 .NET 在运行时的工作方式,顺便说一句。
        • @Raphael :只有当您认为成本/收益不好时,它才会使代码大小成倍增加。其次,请看fastutil.di.unimi.it。您会看到一个示例,即在通用 API 之上存在专门的实现。这里的问题是代码大小的倍增。正如我之前所说,这可能是也可能不是问题。
        猜你喜欢
        • 2010-10-07
        • 1970-01-01
        • 1970-01-01
        • 2010-12-11
        • 2020-12-01
        • 2013-06-02
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多