我试图理解来自O' Reilly编程Scala的代码示例。我是一名JavaScript程序员,本书中的大多数解释都假设是Java背景。我正在寻找一个简单的,高级别的抽象类解释以及它们用于什么。
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 + ")"
}
}
答案 0 :(得分:4)
抽象如同没有太多细节。这是一种说“我们模糊不清”的正式方式。
说,“我有一种交通方式可供我上班。”更抽象的是,“我有一辆车可以上班”。当然,某些地方确实知道你正在做什么。这无需知道到处都是什么。这个想法叫做抽象。
如何使用:
大多数OOP语言中的抽象或父类是集中可重用通用方法的地方,并为更多指定方法提供接口,其代码驻留在更具体或子类上。
因此,如果我提供了一个名为Transportation
的抽象类,其中包含takeMeToWork()
方法,则可以对从Transport继承的任何内容调用takeMeToWork()
,并期望最终开始工作。你不会知道你是在做Car
还是Bicycle
工作但是你会去上班。 Transportation
只会承诺会有takeMeToWork()
方法。它不会定义它是如何工作的,实际上只有提供Car
或Bicycle
才会起作用。
如果你要求每种形式的Transportation
都有相同的杯架,你可以在运输课上放一个useCupHolder()
方法,而不必再写一次。它总是以完全相同的方式工作。取决于接口或“特征”可能无法使用的语言或语言版本。除了提供默认实现之外,抽象类与特征没有太大区别。 This question处理这些差异。
欣赏这个比喻的问题是,在你证明有用的情况之前,很难看出这一点。现在看起来很可能听起来很难理解那些只能解决任何问题的东西。这确实是事实。直到你发现自己使用足够复杂的代码来利用这个和掌握的抽象,它才会让事情变得更难。一旦你得到它虽然它使一切更容易。特别是当你不单独编写代码时。下一个比喻不是经典的,但它是我最喜欢的:
为什么我们在汽车上有引擎盖?
(或bonets为非美国人)
没有它,汽车运行良好。如果没有它,所有酷炫的引擎都会更容易实现。那有什么用呢?如果没有发动机罩,我可以坐在发动机缸体上,将一个调查结果塞进机架和销钉,抓住油门,然后开动汽车。现在我可以做很酷的事情,比如以每小时50英里换油。
多年来,我们发现人们在没有蘸棒的情况下驾驶更舒服。因此,我们将引擎盖放在汽车上,并提供加热座椅,方向盘和汽油兜售。这使我们感到舒适并且阻止我们将裤腿夹在风扇带中。在软件中,我们提供与抽象相同的东西。它有多种形式,抽象类,特征,外观模式等。即使是简陋的方法也是一种抽象形式。
你解决的问题越复杂,你使用一些明智的抽象就越好。而且,你的车看起来比较凉爽。
答案 1 :(得分:2)
抽象类只提供一个定义的接口,一些方法。抽象类的任何子类都可以被视为该类的特定实现或细化。
这允许您定义一个采用Shape
参数的方法,然后该方法的主体可以使用该接口,例如调用形状的draw
方法,无论给出的形状类型如何。
就类型系统而言,要求Shape
确保静态(在编译时)您只能传递满足Shape
接口的对象,因此保证包含draw
{1}}方法。
就个人而言,我更喜欢使用 traits 而不是抽象类,后者在Scala中有一点Java味道。不同之处在于抽象类可能具有构造函数参数。另一方面,具体的实现类可以自由地实现多个特征,而它只能扩展一个类(抽象或不抽象)。
答案 2 :(得分:2)
此特定示例不是最好的,因为Shape
应该是trait
,而不是abstract class
。
继承执行两个独立但相关的事情:它允许不同的值实现公共接口,并且它允许不同的类共享实现代码。
假设我们有一个绘图程序需要使用一堆不同形状的东西 - Square
,Circle
,EquilateralTriangle
等等。在过去的糟糕时期,我们可能会使用一堆if/else
语句执行此操作,例如:
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中,我们实际上使用了模式匹配,但暂时不要担心这一点)
这是一种重复的模式;将重复的&#34;找出正确的形状类型,然后调用正确的方法&#34;逻辑。我们可以自己编写这个想法(虚拟函数表):
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)
但这实际上是一种常见的想法,它已经融入了语言。当我们写类似
的东西时trait Shape {
def draw(): Unit
def area(): Double
}
class Circle extends Shape {
val center: (Double, Double)
val radius: Double
def draw() = {...}
def area() = {...}
}
&#34;引擎盖下&#34;这是做非常相似的事情;它创建了一个包含此Circle.class
和draw()
方法的特殊值area()
。当您按Circle
创建{{1>}的实例时,以及普通的字段 val circle = new Circle()
和center
,这个radius
有一个神奇的隐藏字段Circle
。
当你致电circle.__type = Circle.class
时,这有点等同于shape.draw()
(不是真正的语法)。这很棒,因为这意味着如果shape.__type.draw(shape)
是shape
,则调用将是Square
(再次,不是真正的语法),但如果它是{{1}然后调用将是Square.class.draw(shape)
。注意如何使用正确类型的值调用类(它不可能调用Circle
,因为Circle.class.draw(shape)
总是进行正确的实现。)
现在,很多语言在没有Square.class.draw(circle)
部分的情况下都有类似的东西。例如,在Python中,我可以这样做:
circle.draw()
当我拨打trait
时,它会调用正确的内容。但如果我有其他课程:
class Square:
def draw(self): ...
class Circle:
def draw(self): ...
然后我可以调用shape.draw()
,我会在运行时收到错误。 Scala是类型安全的语言(或多或少):此方法可以正常工作:
class Thursday: ...
虽然这种方法不会编译:
new Thursday().draw()
Scala的类型系统非常强大,你可以用它来证明你的代码的各种各样的东西,但至少,它保证的一个好处是&#34;你永远不会叫不存在的方法&#34;。但是当我们想要在未知类型的形状上调用我们的def doSomething(s: Square): s.draw()
方法时,这会产生一些问题。在某些语言中(例如,我相信Ceylon),您实际上可以编写这样的方法(无效的Scala语法):
def doSomething(t: Thursday): t.draw()
但即使这不是我们想要的:如果有人写自己的draw()
课程,我们希望能够将其包含在我们传递给{{1}的列表中},只要它有def drawAll(shapes: List[Circle or Square or EquilateralTriangle]) = ...
方法。
这就是Star
进来的地方。
drawAll
大致意味着&#34;我保证draw()
有trait
方法。 (回想一下,这确实意味着&#34;我保证trait Shape {
def draw(): Unit
def area(): Double
}
class Circle extends Shape {...}
包含值Circle
)。编译器将强制执行您的承诺,如果它没有实现给定的方法,则拒绝编译def draw(): Unit
。然后我们可以做到:
Circle.class
并且编译器要求draw: Circle => Unit
中的每个Circle
都来自具有def drawAll(shapes: List[Shape]) = ...
方法的类型。所以shape
是&#34;安全&#34;,我们的方法保证只调用实际存在的方法。
(实际上Scala也有更强大的方法来实现相同的效果,类型类型,但现在不要担心。)
这更简单,但也更容易&#34; - 这是一个纯粹实用的东西。
假设我们有一些与对象状态相关的公共代码。例如,我们可能有一堆不同的动物可以吃东西:
shapes
我们可以将它放在def draw(): Unit
:
shape.__type.draw(shape)
请注意,这与我们在前一种情况下编写的内容相同,因此我们也可以使用相同的方式:
class Horse {
private var stomachContent: Double = ...
def eat(food: Food) = {
//calorie calculation
stomachContent += calories
}
}
class Dog {
def eat(food: Food) = ...
}
但希望你能看到我们的意图是不同的;即使trait
是&#34;内部&#34;我们也可以做同样的事情。任何外部函数都无法调用的方法。
trait HasStomach {
var stomachContent: Double
def eat(food: Food) = ...
}
class Horse extends HasStomach
class Dog extends HasStomach
扩展 def feed(allAnimals: List[HasStomach]) = for {animal <- allAnimals} ...
,我们应该撰写 eat
Horse
:
HasStomach
这种观点有一定的道理,但在实践中(根据我的经验),它往往导致代码比传统的OO&#34;方法,特别是当你想为一个大的,复杂的对象制作两种不同的类型时,两种类型之间有一些细微的差别。
到目前为止,我所说的所有内容同样适用于Stomach
和Horse
es(在某种程度上同样适用于class Stomach {
val content: Double = ...
def eat(food: Food) = ...
}
class Horse {
val stomach: Stomach
def eat(food: Food) = stomach.eat(food)
}
es,但不要进入那个)。
在很多情况下,trait
和abstract class
都可以使用,有些人建议使用差异来声明意图:如果你想实现常用界面,使用class
,如果您想共享实施代码,请使用trait
。但在我看来,最重要的区别在于构造函数和多重继承。
Scala允许多重继承;一个班级可以abstract class
几个家长:
trait
由于显而易见的原因,这很有用,但在钻石继承的情况下会出现问题,特别是当你有调用超类方法的方法时。有关Python中出现的一些问题,请参阅Python's Super Considered Harmful,并注意到在实践中,大多数问题都发生在构造函数中,因为这些是通常想要调用超类方法的方法
Scala有一个优雅的解决方案:abstract class
es可能有构造函数,但extend
可能没有。一个类可以从任意数量的class Horse extends HasStomach, HasLegs, ...
继承,但abstract class
必须是第一个父类。这意味着任何类只有一个带有构造函数的父类,所以它总是很明显哪个方法是&#34;超类构造函数&#34;。
所以在实际代码中,我的建议是尽可能使用trait
,并且只对需要构造函数的东西使用trait
。