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

问题描述 投票:12回答:4

我正在阅读“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类。在现实世界的哪种情况下,人们会想要这样做?

scala anonymous-inner-class
4个回答
4
投票

在考虑了我自己的答案之后,我得出的结论是,所有这些基本上只是:

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

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


TL; DR

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


概观

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

  1. Runnable的简单示例
  2. 绘制2d函数的简单示例
  3. 一个历史上重要的例子Function<X, Y>
  4. 一个先进的现实世界示例,其中匿名本地类的实例化似乎是不可避免的
  5. 简要讨论您用来介绍问题的代码。

免责声明:一些代码是非惯用的,因为它“重新发明了轮子”,并没有隐藏lambdas或SingleAbstractMethod语法中抽象本地类的实例化。


简单的介绍示例: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()
  }
}

现在假设您要使用此方法打印“Hello,world!”十次。你如何创造正确的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

这些接口获得了自己的简洁语法,并且在编译器中具有很高的特权。从问题的角度来看,这会产生“问题”,因为匿名类的实例化通常隐藏在特殊的内置语法糖之下。在惯用的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中,因为还没有lambdas,每次你想传递某种回调或RunnableFunction时,你必须实现一个扩展抽象类的匿名类。如今,在较新的Java版本中,它被lambdas和SingleAbstractMethod语法隐藏,但原理仍然是相同的:构造实现接口或扩展抽象类的匿名类的实例。


一个先进的“几乎是现实世界”的例子

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

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

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

在这里,FooBar将类似嵌入式领域特定语言,在不同的抽象层次上运行,而Foo更高级别,而Bar更低级别。这又是完全相同的原则,这样的例子无处不在。这是一个几乎“照片般逼真”的实际用法示例: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"
}

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

我的猜测是作者想要证明你可以通过defs覆盖vals:

trait P {
  def name: String
}

val inst = new P {
  val name = "Fred"
}

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


2
投票

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


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

Graph-like datastructures

考虑由以下定义的有向图:

  • 节点集
  • 边缘集
  • 从边到节点的函数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

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

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

   --->
*        *
   --->

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

Eliminators for mutually recursive datastructures

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

考虑以下一种语言的抽象语法,它允许我们用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-instances表示它们没有任何意义。以下是此解释器的一个小例子:

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)

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


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


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. 更可靠/可维护的测试 - 使用匿名实例方法必须实现所有抽象成员+编译器根据添加到基类/特征的抽象成员检查实现,而对于存根,没有这样的帮助可用。

当然,有一些缺点,例如匿名类型实例方法不能用于提供模拟/间谍 - 即允许对它们进行的调用断言的测试双精度。


0
投票

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.