Scala:具有匿名类型的抽象类

时间:2018-04-01 13:52:40

标签: scala anonymous-inner-class

我正在阅读" Scala for the Impatient"在8.8他们说:

  

[..]你可以使用abstract关键字来表示一个不能的类   实例化[..]

abstract class Person { val id: Int ; var name: String }

后来几行:

  

您始终可以使用匿名类型自定义抽象字段:

val fred = new Person {

  val id = 1729

  var name = "Fred"

}

因此,他们用匿名类型人为地实例化了Person类。在现实世界的哪种情况下,人们会想要这样做?

4 个答案:

答案 0 :(得分:4)

在考虑了我自己的答案后,我得出的结论是,它所说的基本上只是:

  

"匿名本地类实例是穷人的功能文字"

提供 +150 赏金以获得有助于扩展这种狭隘视野的答案。

<强> TL; DR

每当您想要将方法的实现视为对象时,您可以实例化一个扩展抽象基类的匿名本地类,实现这些方法,然后像创建基类的任何其他实例一样传递创建的实例。 / p>

<强>概述

此帖子讨论了您可能希望实例化匿名本地类的五种情况。这些例子从非常基础发展到相当先进。

  1. Runnable
  2. 的简单示例
  3. 绘制2d函数的简单示例
  4. 历史上重要的例子Function<X, Y>
  5. 一个高级的现实示例,其中匿名本地类的实例化似乎是不可避免的
  6. 简要讨论您用来介绍问题的代码。
  7. 免责声明:有些代码是非惯用的,因为它会重新发明轮子&#34;并且不会隐藏lambdas或SingleAbstractMethod-syntax中抽象本地类的实例化。

    简单的介绍示例:Runnable

    假设您要编写一个占用一些代码块的方法,并多次执行它:

    def repeat(numTimes: Int, whatToDo: <someCleverType>): Unit = ???
    

    假设您想从头开始重新创建所有内容,并且不想使用标准库中的任何名称参数或接口,那么您将使用什么代替<someCleverType>?您必须提供看起来像这样的基类:

    abstract class MyRunnable {
      def run(): Unit  // abstract method
    }
    

    现在,您可以按如下方式实施repeat方法:

    def repeat(numTimes: Int, r: MyRunnable): Unit = {
      for (i <- 1 to numTimes) {
        r.run()
      }
    }
    

    现在假设您想使用此方法打印&#34; Hello,world!&#34;十次。你如何创造正确的MyRunnable?你可以定义一个类 HelloWorld扩展MyRunnable并实现run方法,但它只会污染命名空间,因为您只想使用它一次。相反,您可以直接实例化匿名类:

    val helloWorld = new MyRunnable {
      def run(): Unit = println("Hello, world!")
    }
    

    然后将其传递给repeat

    repeat(10, helloWorld)
    

    您甚至可以省略helloWorld变量:

    repeat(10, new MyRunnable {
      def run(): Unit = println("Hello, world!")
    })
    

    这是一个典型的例子,说明为什么要实例化匿名本地类。

    稍微有趣的例子:RealFunction

    在前面的例子中,run没有参数,每次都执行相同的代码。

    现在我想稍微修改一下这个例子,以便实现的方法需要一些参数。

    我现在不会提供完整的实现,但假设你有一个函数

    plot(f: RealFunction): Unit = ???
    

    绘制实函数R -> R的图形,其中RealFunction是一个定义为

    的抽象类
    abstract class RealFunction {
      def apply(x: Double): Double
    }
    

    要绘制抛物线,您现在可以执行以下操作:

    val xSquare = new RealFunction {
      def apply(x: Double): Double = x * x
    }
    
    plot(xSquare)
    

    您甚至可以在没有plot的情况下单独测试它:例如,p(42)计算1764.0,这是42的平方。

    常规功能Function[X, Y]

    前面的示例概括为任意函数,可以将类型XY作为域和codomain。从历史的角度来看,这可以说是最重要的例子。考虑以下抽象类:

    abstract class Function[X, Y] {
      def apply(x: X): Y // abstract method
    }
    

    它类似于RealFunction,但现在不是固定Double,而是XY

    根据此界面,您可以重新创建xSquare函数,如下所示:

    val xSquare = new Function[Double, Double] {
      def apply(x: Double) = x * x
    }
    

    实际上,这个例子非常重要,Scala的标准库中填充了不同数量的参数FunctionN[X1,...,XN, Y]的接口N

    这些接口获得了自己的简洁语法,并且在编译器中具有很高的特权。这造成了一个问题&#34;从你的问题的角度来看,因为匿名类的实例化通常隐藏在特殊的内置语法糖之下。在惯用的Scala中,您通常只需编写

    val xSquare = (x: Double) => x * x
    

    而不是

    val xSquare = new Function[Double, Double] {
      def apply(x: Double) = x * x
    }
    

    其他JVM语言的情况类似。例如,甚至Java版本8在java.util.function中引入了许多非常相似的接口。 几年前,你会写出像

    这样的东西
    Function<Integer, Integer> f = new Function<Integer, Integer>() {
      public Integer apply(Integer x) {
        return x * x;
      }
    };
    

    在Java中,因为还没有lambda,每次你想传递某种回调或RunnableFunction时,你必须实现一个扩展抽象类的匿名类。如今,在较新的Java版本中,它被lambdas和SingleAbstractMethod语法隐藏,但原理仍然是相同的:构造实现接口或扩展抽象类的匿名类的实例。

    高级&#34;几乎真实的世界&#34; -example

    您今天编写的代码中不会遇到任何前面的示例,因为匿名本地类的实例化被lambdas的语法糖隐藏。我想提供一个实际的例子,其中匿名本地类的实例化实际上是不可避免的。

    new AbstractClassName(){ } - 语法仍然出现在没有语法糖的地方。例如,因为Scala没有多态lambda的语法,要在像Scalaz或Cats这样的库中构建自然变换,你通常会写出如下内容:

    val nat = new (Foo ~> Bar) {
      def apply[X](x: Foo[X]): Bar[X] = ???
    }
    

    此处,FooBar类似于在不同抽象级别上运行的嵌入式域特定语言,Foo更高级,而Bar更低级别。它再次完全相同,这样的例子无处不在。这是一个几乎&#34;照片般逼真的&#34;实际使用示例:defining an (KVStoreA ~> Id)-interpreter。我希望你能认出那里的new (KVStoreA ~> Id) { def apply(...) ... }部分。不幸的是,这个例子相当先进,但正如我在评论中所提到的,在过去十年中,所有简单且经常使用的示例大部分都被lambdas和Single-Abstract-Method语法所隐藏。

    返回您的示例

    您引用的代码

    abstract class Person(val name: String) {
      def id: Int
    }
    
    val fred = new Person {
      val id = 1729
      var name = "Fred"
    }
    

    似乎没有编译,因为缺少构造函数参数。

    我的猜测是,作者希望证明您可以通过def s覆盖val

    trait P {
      def name: String
    }
    
    val inst = new P {
      val name = "Fred"
    }
    

    虽然知道这是可能的很好,但我不认为这是匿名本地类实例化的最重要用例(因为你可以使用普通的成员变量并传递值而是构造函数)。考虑到空间限制,本书的作者可能只是想快速演示语法,而不是对现实世界的使用进行扩展讨论。

答案 1 :(得分:2)

  

这是我第二次尝试回答同一个问题。在我之前的尝试中,我只能提出单抽象方法示例。我想通过提供更多需要覆盖多种方法的示例来纠正这个缺点。

以下是一些示例,其中一个人可能希望覆盖抽象本地类中的多个方法,并且重写的方法彼此紧密耦合,因此将它们分开几乎没有任何意义。我真的试图提出“不可简化”的例子,其中没有办法定义多个连贯的方法。

类似图形的数据结构

考虑定义的有向图:

  • 节点集
  • 边缘设置
  • 从边到节点的函数source
  • 从边到节点的函数target

如果我们隐式定义节点和边集,我们可以将图表示为具有两个类型成员和四个方法的类的实例:

trait Digraph {
  type E
  type N
  def isNode(n: N): Boolean
  def isEdge(e: E): Boolean
  def source(e: E): N
  def target(e: E): N
}

例如,下面定义了一个无限图,看起来像实线的正部分,从单位间隔粘合:

val g = new Digraph {
  type E = (Int, Int)
  type N = Int
  def isNode(n: Int) = n >= 0
  def isEdge(e: (Int, Int)) = e._1 >= 0 && e._2 == e._1 + 1
  def source(e: (Int, Int)) = e._1
  def target(e: (Int, Int)) = e._2
}

我们通常想要一次覆盖所有方法的原因是函数必须满足一大堆连贯条件,例如:

* for each `e` in domain of `source` and `target`, `isEdge(e)` must hold
* for each `n` in codomain of `source` and `target`, `isNode(n)` must hold

因此,定义此类无限图的最自然方式是通过实例化本地匿名类。

备注:如果你喜欢一般的抽象废话,你会很容易地认识到这只是微小类别上 presheaf 的特例,只有两个对象和两个平行箭头:

   --->
*        *
   --->

因此,该示例很容易推广到所有这样的数据结构,而不仅仅是图形。函数的定义是对重写方法强加一致性要求。

相互递归数据结构的消除器

另一个例子:用于复杂的相互递归结构的类似折叠的消除器。

考虑以下一种语言的抽象语法,它允许我们用2d-vectors和scalars写下简单的表达式:

sealed trait VecExpr
case class VecConst(x: Double, y: Double) extends VecExpr
case class VecAdd(v1: VecExpr, v2: VecExpr) extends VecExpr
case class VecSub(v1: VecExpr, v2: VecExpr) extends VecExpr
case class VecMul(v1: VecExpr, a: ScalarExpr) extends VecExpr

sealed trait ScalarExpr
case class ScalarConst(d: Double) extends ScalarExpr
case class DotProduct(v1: VecExpr, v2: VecExpr) extends ScalarExpr

如果我们尝试定义一个可以评估这种表达式的解释器,我们很快就会注意到有很多重复:基本上,我们只是继续调用相同的递归eval - 看似不合适的方法依赖于除了类型之外的任何东西。我们可以通过为解释器提供以下基类来隐藏一些样板:

trait Evaluator[S, V] {
  def vecConst(x: Double, y: Double): V
  def vecAdd(v1: V, v2: V): V
  def vecSub(v1: V, v2: V): V
  def vecMul(v: V, s: S): V

  def scalarConst(x: Double): S
  def dotProduct(v1: V, v2: V): S

  def eval(v: VecExpr): V = v match {
    case VecConst(x, y) => vecConst(x, y)
    case VecAdd(v1, v2) => vecAdd(eval(v1), eval(v2))
    case VecSub(v1, v2) => vecSub(eval(v1), eval(v2))
    case VecMul(v, s) => vecMul(eval(v), eval(s))
  }

  def eval(s: ScalarExpr): S = s match {
    case ScalarConst(d: Double) => scalarConst(d)
    case DotProduct(v1, v2) => dotProduct(eval(v1), eval(v2))
  }
}

现在,解释器的实现者可以直接使用完全评估的向量和标量,而无需递归调用。例如,这是一个实现,它将所有内容评估为double和tuples:

val ev = new Evaluator[Double, (Double, Double)] {
  def vecConst(x: Double, y: Double) = (x, y)
  def vecAdd(v1: (Double, Double), v2: (Double, Double)): (Double, Double) = (v1._1 + v2._1, v1._2 + v2._2)
  def vecSub(v1: (Double, Double), v2: (Double, Double)): (Double, Double) = (v1._1 - v2._1, v1._2 - v2._2)
  def vecMul(v: (Double, Double), s: Double): (Double, Double) = (v._1 * s, v._2 * s)

  def scalarConst(x: Double): Double = x
  def dotProduct(v1: (Double, Double), v2: (Double, Double)): Double = v1._1 * v2._1 + v1._2 * v2._2
}

在这里,我们必须以连贯的方式覆盖六个方法,并且因为它们都是非常紧密耦合的,所以通过单独的Function - 实例来表示它们没有任何意义。以下是这个解释器的一个小例子:

val expr = VecSub(
  VecConst(5, 5),
  VecMul(
    VecConst(0, 1),
    DotProduct(
      VecSub(
        VecConst(5, 5),
        VecConst(0, 2)
      ),
      VecConst(0, 1)
    )
  )
)

println(ev.eval(expr))

这会成功地将点(5,5)投射到通过(0, 2)并使用法线向量(0, 1)的平面上,并输出:

(5.0,2.0)

在这里,似乎是相互递归使得很难解开函数族,因为解释器必须作为一个整体起作用。

所以,我想得出的结论是,匿名本地类型肯定存在超出单抽象方法的用例。

答案 2 :(得分:1)

实例化匿名类型的另一个例子是实例化特征。

scala> :paste
// Entering paste mode (ctrl-D to finish)

trait ServiceProvider {
  def toString(int: Int): String
  def fromString(string: String): Int
}

val provider = new ServiceProvider {
  override def toString(int: Int) = int.toString
  override def fromString(string: String): Int = string.toInt
}
// Exiting paste mode, now interpreting.

defined trait ServiceProvider
provider: ServiceProvider = $anon$1@33b0687

最后一行显示实例化特征并实例化抽象类具有相同的结果 - 创建了匿名本地类型的实例。

这项功能在测试时非常方便 - 它可以在不使用任何第三方库的情况下提供存根和假货,例如Mockito,scalamock等。

继续上一个例子

class Converter(provider: ServiceProvider) {
  def convert(string: String): Int = provider.fromString(string)
  def convert(int: Int): String = provider.toString(int)
}

// somewhere in ConverterSpec
// it("should convert between int and string")
val provider = new ServiceProvider {
  override def toString(int: Int) = int.toString
  override def fromString(string: String): Int = string.toInt
}
val converter = new Converter(provider)
converter.convert("42") shouldBe 42
converter.convert(1024) shouldBe "1024"
converter.convert(converter.convert("42")) shouldBe "42"

// it("should propagate downstream exceptions")
val throwingProvider = new ServiceProvider {
  override def toString(int: Int) = throw new RuntimeException("123")
  override def fromString(string: String): Int = throw new RuntimeException("456")
}
val converter = new Converter(throwingProvider)
a[RuntimeException] shouldBe thrownBy { converter.convert(42) }
a[RuntimeException] shouldBe thrownBy { converter.convert("1024") }

与使用一些适当的存根/模拟库相比,这种方法的好处是:

  1. 轻松提供有状态测试双打
  2. 使用起来有点简单 - 取决于选择测试双库 - 巨大与Mockito相比,与scalamock相比差别不大
  3. 更可靠/可维护的测试 - 使用匿名实例方法必须实现所有抽象成员+您可以使用编译器来检查针对抽象成员添加到基类/特征的实现,而使用存根没有这样的帮助。
  4. 当然,有一些缺点,例如匿名类型实例方法不能用于提供模拟/间谍 - 即允许对它们进行的调用断言的测试双精度。

答案 3 :(得分:0)

使用匿名类instatiation语法没有现实世界的要求。您始终可以创建自己的扩展Person的类,然后将其实例化一次以获取fred值。

您可以将此语法视为创建一次性类的单个实例的快捷方式,而无需为该类提供名称。

它与lambdas(a.k.a。匿名函数)提供的方便性相同。如果你只使用一次这个函数,为什么我们需要在别处定义它并给它一个名字,当我们可以直接描述它内联?