【问题标题】:How to implement intermediate types for implicit methods?如何为隐式方法实现中间类型?
【发布时间】:2011-03-07 13:37:58
【问题描述】:

假设我想在我无法控制的现有类型 A 上提供方法 foo。据我所知,在 Scala 中执行此操作的规范方法是实现从 A 到实现 foo 的某种类型的隐式转换。现在我基本上看到了两种选择。

  1. 为此目的定义一个单独的,甚至可能是隐藏的类:

    protected class Fooable(a : A) {
      def foo(...) = { ... }
    }
    implicit def a2fooable(a : A) = new Fooable(a)
    
  2. 定义一个匿名类内联:

    implicit def a2fooable(a : A) = new { def foo(...) = { ... } }
    

变体 2) 的样板当然更少,尤其是在出现大量类型参数时。另一方面,我认为它应该会产生更多的开销,因为(从概念上)每次转换都会创建一个类,而不是在 1) 中全局创建一个类。

是否有一般准则?有没有区别,因为编译器/VM摆脱了2)的开销?

【问题讨论】:

    标签: scala implicits


    【解决方案1】:

    使用单独的类会更好地提高性能,因为替代方案使用反射。

    考虑一下

    new { def foo(...) = { ... } }
    

    真的

    new AnyRef { def foo(...) = { ... } }
    

    现在,AnyRef 没有方法 foo。在 Scala 中,这个类型实际上是AnyRef { def foo(...): ... },如果你去掉AnyRef,你应该将其识别为一个结构类型

    在编译时,这个时间可以来回传递,并且在任何地方都会知道方法foo是可调用的。但是,JVM 中没有结构类型,添加接口需要代理对象,这会导致一些问题,例如破坏引用相等(即对象与自身的结构类型版本不相等)。

    解决这个问题的方法是对结构类型使用缓存反射调用。

    因此,如果您想将 Pimp My Library 模式用于任何性能敏感的应用程序,请声明一个类。

    【讨论】:

    • 感谢您提供这些见解。不过,我很好奇,这种解释如何与 paolo 的发现相吻合,这表明 JVM 中有 is 一种类型可以在没有反射的情况下使用。还是test$$anon$1只是类名而不是类型?
    • @Raphael 这就像接口和实现之间的区别。 test$$anon$1 类可以被认为是接口的实现,而AnyRef { def foo(...): ... } 将是接口本身。虽然确实在有限的场景中可以用这些匿名类替换反射调用,但对于更一般的场景则不适用。
    • 感谢您的解释。我假设 new {foo = "bar"} 相当于 new Compiler_Please_Create_This_Class_For_Me() 对于 c# 和 java 中的匿名类,而不是 new AnyRef {foo = "bar"},所以我发现很难将匿名类连接到结构类型。
    【解决方案2】:

    我相信 1 和 2 被编译为相同的字节码(除了在案例 2 中生成的类名)。 如果 Fooable 的存在只是为了让您能够将 A 隐式转换为 Fooable(并且您永远不会直接创建和使用 Fooable),那么我会选择选项 2。

    但是,如果您控制 A(意味着 A 不是您不能子类化的 Java 库类),我会考虑使用 trait 而不是隐式转换来向 A 添加行为。

    更新: 我必须重新考虑我的答案。我会使用您的代码的变体 1,因为变体 2 原来是使用反射(Linux 上的 scala 2.8.1)。

    我编译了这两个版本的相同代码,用 jd-gui 将它们反编译为 java,结果如下:

    具有命名类的源代码

    class NamedClass { def Foo : String = "foo" }
    
    object test {
      implicit def StrToFooable(a: String) = new NamedClass 
      def main(args: Array[String]) { println("bar".Foo) }
    }
    

    匿名类的源代码

    object test {
      implicit def StrToFooable(a: String) = new { def Foo : String = "foo" } 
    
      def main(args: Array[String]) { println("bar".Foo) }
    }    
    

    使用 java-gui 编译和反编译为 java。 “命名”版本会生成一个 NamedClass.class,它会被反编译成这个 java:

    public class NamedClass
      implements ScalaObject
    {
      public String Foo()
      {
        return "foo";
      }
    }
    

    匿名生成一个 test$$anon$1 类,该类被反编译为以下 java

    public final class test$$anon$1
    {
      public String Foo()
      {
        return "foo";
      }
    }
    

    几乎相同,除了匿名是“最终的”(他们显然想额外确保您不会逃避尝试和子类化匿名类...)

    但是在调用站点我得到了这个 java 的“命名”版本

    public void main(String[] args) 
    { 
      Predef..MODULE$.println(StrToFooable("bar").Foo());
    }
    

    这个是匿名的

      public void main(String[] args) { 
        Object qual1 = StrToFooable("bar"); Object exceptionResult1 = null;
        try { 
          exceptionResult1 = reflMethod$Method1(qual1.getClass()).invoke(qual1, new Object[0]); 
          Predef..MODULE$.println((String)exceptionResult1); 
          return; 
        } catch (InvocationTargetException localInvocationTargetException) { 
          throw localInvocationTargetException.getCause();
        }
      }
    

    我用谷歌搜索了一下,发现others 报告了同样的事情,但我没有找到更多关于为什么会这样的见解。

    【讨论】:

    • 你能为你的信念提供一些证据吗?我正在考虑添加到我无法控制的类型,否则特征当然是更好的解决方案。我澄清了这方面的问题。
    • @Raphael 我这里没有安装 scala,不能给你证据。在我回家的几个小时内,我将编译这两个变体,检查生成的 .class 并发布结果
    • @Raphael 原来你不相信未经检验的说法是对的 :) 我修改了我的答案。
    • 任何使用结构类型的情况(如第二个示例)都将编译为使用反射,因此会造成性能损失。第一种方法是添加一个类来专门提供扩展,这是执行此操作的规范方法(并且在 Scala 标准库中广泛使用。)
    猜你喜欢
    • 2019-10-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多