关于java:您如何向六年级学生解释Scala的抽象类功能?

How would you explain Scala's abstract class feature to a 6th grader?

本问题已经有最佳答案,请猛点这里访问。

我试图从O'Reilly的编程scala中理解这个代码示例。我是一个JavaScript程序员,书中的大部分解释都是Java背景。我正在寻找一个抽象类及其用途的简单、高级解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package shapes {
    class Point(val x: Double, val y: Double) {
        override def toString() ="Point(" + x +"," + y +")"
    }

    abstract class Shape() {
        def draw(): Unit
    }

    class Circle(val center: Point, val radius: Double) extends Shape {
        def draw() = println("Circle.draw:" + this)
        override def toString() ="Circle(" + center +"," + radius +")"
    }
}

抽象的,没有太多细节的。这是一种正式的表达方式,"我们含糊其辞"。

说"我有一种上班的交通工具",比说"我有一辆上班的车"更抽象。当然,在某个地方,有些东西能确切知道你要干什么。这是因为不必在任何地方确切地知道什么。这个想法叫做抽象。

它是如何使用的:

在大多数OOP语言中,抽象类或父类是一个集中可重用的通用方法并为代码驻留在更具体或子类上的更指定方法提供接口的地方。

因此,如果我提供了一个名为Transportation的抽象类,上面有一个takeMeToWork()方法,那么您可以调用takeMeToWork()来处理从传输中继承的任何东西,并期望最终得到工作。你不知道你是带着一个Car还是一个Bicycle去工作,但你会去工作。Transportation只承诺会有一个takeMeToWork()方法。它不会定义它是如何工作的,事实上,直到它被提供一个CarBicycle,它才会工作。

如果你要求每种形式的Transportation都有相同的杯托,你可以在运输类中放一次useCupHolder()方法,而不必再写了。它总是以同样的方式工作。取决于语言或语言版本,界面或"特征"可能无法使用该技巧。除了提供默认的实现之外,抽象类与特性没有太大的区别。这个问题处理这些差异。

欣赏这个比喻的问题在于,除非你处在一个有用的环境中,否则很难理解它的意义。现在听起来好像有很多花哨的、难以理解的东西,这些东西只会让解决任何问题变得更加困难。这是事实。在您发现自己处理的代码足够复杂以至于可以利用它并掌握抽象之前,它只会使事情变得更困难。但一旦你得到它,一切都会变得简单。尤其是当你不单独写代码的时候。下一个比喻不是经典的,但它是我最喜欢的:

为什么我们车上有引擎盖?

(或者给非美国人的小骨头)

没有它这辆车运行得很好。没有它,所有的酷引擎都更容易得到。那是为了什么?如果没有引擎盖,我可以坐在发动机上,在机架和销轴上插入一个投票,抓住油门,然后驾驶汽车。现在我可以做一些很酷的事情,比如以每小时50英里的速度换油。

这些年来,我们发现,如果不在脸上沾一根油尺,人们真的会更舒服地驾驶。所以我们把引擎盖放在车上,并提供了加热座椅、方向盘和油门踏板。这让我们感到舒适,防止我们的裤腿被风扇皮带缠住。

在软件中,我们用抽象提供相同的东西。它有许多形式、抽象类、特征、外观模式等,甚至卑微的方法也是一种抽象形式。

你解决的问题越复杂,使用一些明智的抽象就越好。而且,你的车带着引擎盖看起来更酷。


由于Shape可能是trait,而不是abstract class,所以这个特定的例子不是最好的。好的。

继承做了两件独立但相关的事情:它允许不同的值实现一个公共接口,并允许不同的类共享实现代码。好的。公共接口

假设我们有一个绘图程序需要处理一系列不同的形状——SquareCircleEquilateralTriangle等等。在过去糟糕的日子里,我们可能会用一堆if/else声明来做这件事,比如:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def drawShapes(shapes: List[Shape]) =
  for { shape <- shapes } {
    if(isCircle(shape))
      drawDot(shape.asInstanceOf[Circle].center)
      ...
    else if(isSquare(shape))
      drawStraghtLine(shape.asInstanceOf[Square].topLeft, shape.asInstanceOf[Square].topRight)
    ...
  }

def calculateEmptySpace(shapes: List[Shape]) =
  val shapeAreas = for { shape <- shapes } yield {
    if(isCircle(shape)) (shape.asInstanceOf[Circle].radius ** 2) * Math.PI
    else if(isSquare(shape)) ...
  }

(在scala中,我们实际上使用的是模式匹配,但暂时不要担心这个问题)好的。

这是一种重复的模式;最好隔离重复的"找出正确的形状类型,然后调用正确的方法"逻辑。我们可以自己编写这个想法(一个虚拟函数表):好的。

1
2
3
4
5
6
7
8
9
10
11
case class ShapeFunctions[T](draw: T => Unit, area: T => Double)
object ShapeFunctions {
  val circleFunctions = new ShapeFunctions[Circle]({c: Circle => ...}, {c: Circle => ...})
  val squareFunctions = new ShapeFunctions[Square](...)
  def forShape(shape: Any) = if(isCircle(shape)) circleFunctions
    else if(isSquare(shape)) squareFunctions
    else ...
}
def drawShapes(shapes: List[Shape]) =
  for {shape <- shapes}
    ShapeFunctions.forShape(shape).draw(shape)

但这实际上是一个很常见的想法,它是建立在语言中的。当我们写一些像好的。

1
2
3
4
5
6
7
8
9
10
trait Shape {
  def draw(): Unit
  def area(): Double
}
class Circle extends Shape {
  val center: (Double, Double)
  val radius: Double
  def draw() = {...}
  def area() = {...}
}

"引擎盖下"这是做一些非常相似的事情;它创造了一个特殊的价值Circle.class,其中包含了draw()area()方法。当你用val circle = new Circle()创建一个Circle的实例,以及普通字段centerradius时,这个Circle有一个神奇的隐藏字段circle.__type = Circle.class。好的。

当您调用shape.draw()时,这类似于shape.__type.draw(shape)(不是真正的语法)。这很好,因为它意味着如果ShapeSquare,那么调用将是Square.class.draw(shape)(同样,不是真正的语法),但是如果是Circle,那么调用将是Circle.class.draw(shape)。请注意如何始终使用正确类型的值调用类(不可能调用Square.class.draw(circle),因为circle.draw()总是指向正确的实现)。好的。

现在,很多语言都有这样的东西,没有trait部分。例如,在python中,我可以做到:好的。

1
2
3
4
class Square:
  def draw(self): ...
class Circle:
  def draw(self): ...

当我给shape.draw()打电话时,它会说对了。但如果我有别的课:好的。

1
class Thursday: ...

然后我可以调用new Thursday().draw(),在运行时会得到一个错误。scala是一种类型安全的语言(或多或少):此方法工作正常:好的。

1
def doSomething(s: Square): s.draw()

虽然此方法无法编译:好的。

1
def doSomething(t: Thursday): t.draw()

scala的类型系统非常强大,您可以使用它来证明您的代码的所有方面,但至少,它保证的一个好处是"您永远不会调用不存在的方法"。但当我们想在未知形状类型上调用draw()方法时,这会带来一些问题。在某些语言中(例如,我相信锡兰),您实际上可以编写这样的方法(无效的scala语法):好的。

1
def drawAll(shapes: List[Circle or Square or EquilateralTriangle]) = ...

但即使这不是我们真正想要的:如果有人写他们自己的Star类,我们希望能够将它包括在我们传递给drawAll的列表中,只要它有draw()方法。好的。

所以,这就是trait进来的地方。好的。

1
2
3
4
5
6
trait Shape {
  def draw(): Unit
  def area(): Double
}

class Circle extends Shape {...}

大致意思是"我保证Circledef draw(): Unit方法。(回想一下,这实际上意味着"我保证Circle.class包含一个值draw: Circle => Unit)"。编译器将执行您的承诺,如果不实现给定的方法,则拒绝编译Circle。然后我们可以做:好的。

1
def drawAll(shapes: List[Shape]) = ...

编译器要求shapes中的每个shape都来自一个使用def draw(): Unit方法的类型。所以shape.__type.draw(shape)是"安全的",我们的方法保证只调用实际存在的方法。好的。

(事实上,scala还有一个更强大的方法来达到同样的效果,即typeclass模式,但现在我们不必担心这个问题。)好的。共享实施

这更简单,但也更"凌乱"——这是一个纯粹的实际问题。好的。

假设我们有一些符合对象状态的公共代码。例如,我们可能有一群不同的动物可以吃东西:好的。

1
2
3
4
5
6
7
8
9
10
class Horse {
  private var stomachContent: Double = ...
  def eat(food: Food) = {
     //calorie calculation
     stomachContent += calories
  }
}
class Dog {
  def eat(food: Food) = ...
}

我们可以把它放在一个trait中,而不是两次编写相同的代码:好的。

1
2
3
4
5
6
trait HasStomach {
  var stomachContent: Double
  def eat(food: Food) = ...
}
class Horse extends HasStomach
class Dog extends HasStomach

请注意,这与我们在前一个案例中编写的内容相同,因此我们也可以用同样的方式使用它:好的。

1
def feed(allAnimals: List[HasStomach]) = for {animal <- allAnimals} ...

但希望您能看到我们的意图是不同的;即使eat是一个"内部"方法,任何外部函数都无法调用,我们也可能做同样的事情。好的。

有些人批评"传统的"OO继承,因为它"混合"了这两种含义。没有办法说"我只想共享这段代码,不想让其他函数调用它"。这些人倾向于认为共享代码应该通过组合来实现:与其说我们的Horse扩展了HasStomach,我们应该将Stomach组合到我们的Horse中:好的。

1
2
3
4
5
6
7
8
class Stomach {
  val content: Double = ...
  def eat(food: Food) = ...
}
class Horse {
  val stomach: Stomach
  def eat(food: Food) = stomach.eat(food)
}

这种观点是有道理的,但在实践中(在我的经验中),它往往会导致比"传统的OO"方法更长的代码,特别是当您想要为大型、复杂的对象生成两种不同的类型,并且这两种类型之间存在一些微小的差异时。好的。抽象类与特征

到目前为止,我所说的一切都同样适用于traits和abstract classes(在某种程度上也适用于classes,但我们不要再谈这个问题)。好的。

在许多情况下,traitabstract class都可以工作,有些人建议使用差异来声明意图:如果要实现公共接口,请使用trait,如果要共享实现代码,请使用abstract class。但在我看来,最重要的区别在于构造函数和多重继承。好的。

scala允许多重继承;一个类可以有多个父类:好的。

1
class Horse extends HasStomach, HasLegs, ...

这很有用,原因很明显,但在菱形继承情况下可能会有问题,特别是当您有调用超类方法的方法时。请参阅python的super-consided有害于python中出现的一些问题,并注意在实践中,大多数问题都发生在构造函数中,因为这些方法通常都希望调用超类方法。好的。

scala对此有一个很好的解决方案:abstract classes可能有构造函数,但traits可能没有。一个类可以从任何数量的trait继承,但abstract class必须是第一个父类。这意味着任何类都只有一个带有构造函数的父类,所以很明显哪个方法是"超类构造函数"。好的。

所以在实际代码中,我的建议是尽可能始终使用trait,并且只对需要有构造函数的东西使用abstract class。好的。好啊。


抽象类只提供一个定义的接口和许多方法。抽象类的任何子类都可以看作是该类的特定实现或细化。

这允许您定义一个采用Shape参数的方法,然后该方法的主体可以使用该接口,例如,调用形状的draw方法,而不管给定的形状类型如何。

在类型系统方面,请求Shape以静态方式(在编译时)确保只能传递满足Shape接口的对象,因此保证它包含draw方法。

就个人而言,我更倾向于使用特征而不是抽象类,后者在斯卡拉中对我有一点Java气味。区别在于抽象类可能有构造函数参数。另一方面,具体的实现类可以自由地实现多个特性,而它只能扩展一个类(抽象的或非抽象的)。