你如何解释Scala的抽象类功能给6年级学生? [英] How would you explain Scala's abstract class feature to a 6th grader?

查看:113
本文介绍了你如何解释Scala的抽象类功能给6年级学生?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图理解这个代码示例从O'Reilly的编程Scala。我是一个JavaScript程序员,大多数的解释在这本书假设一个Java背景。我正在寻找一个简单的,高级别的解释抽象类和它们用于什么。

I'm trying to understand this code example from O'Reilly's Programming Scala. I'm a JavaScript programmer and most of the explanations in the book assume a Java background. I'm looking for a simple, high-level explanation of abstract classes and what they're used for.

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 + ")"
    }
}


推荐答案

这个特殊的例子不是最好的,因为 Shape trait ,而不是抽象类

This particular example is not the best since Shape should probably be a trait, not an abstract class.

两个独立但相关的事物:它允许不同的值实现一个公共接口,并且允许不同的类共享实现代码。

Inheritance does two separate but related things: it lets different values implement a common interface, and it lets different classes share implementation code.

假设我们有一个绘图程序需要做一堆不同形状的事情 - Square Circle EquilateralTriangle 等。在糟糕的旧时代,我们可以使用一堆 if / else 语句来执行此操作,例如:

Suppose we've got a drawing program that needs to do things with a bunch of different shapes - Square, Circle, EquilateralTriangle and so on. In the bad old days, we might do this with a bunch of if/else statements, something like:

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中,我们实际使用了模式匹配关于那一刻)

(in Scala we'd actually use a pattern match, but let's not worry about that for the moment)

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

This is a kind of repetitive pattern; it would be nice to isolate the repetitive "figure out the correct type of shape, then call the right method" logic. We could write this idea (a virtual function table) ourselves:

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)

共同的想法,它是内置的语言。当我们写像

But this is actually so common an idea that it's built into the language. When we write something like

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() 方法。当您通过 val circle = new Circle()创建 Circle 实例以及普通的字段 center radius ,这个 有一个神奇的隐藏字段 circle .__ type = Circle.class

"under the hood" this is doing something very similar; it's creating a special value Circle.class which contains this draw() and area() method. When you create an instance of Circle by val circle = new Circle(), as well as the ordinary fields center and radius, this Circle has a magic, hidden field circle.__type = Circle.class.

你调用 shape.draw(),这等同于 shape .__ type.draw(shape)真正的语法)。这是伟大的,因为这意味着如果 shape Square ,那么调用将是 Square.class.draw(shape)(再次,不是真正的语法),但如果它是一个 Circle ,那么调用将是 Circle.class.draw(shape)。注意一个类总是被正确类型的值调用(不可能调用 Square.class.draw(circle),因为 circle。 draw()总是转到正确的实现)。

When you call shape.draw(), this is sort of equivalent to shape.__type.draw(shape) (not real syntax). Which is great, because it means that if shape is a Square, then the call will be Square.class.draw(shape) (again, not real syntax), but if it's a Circle then the call will be Circle.class.draw(shape). Notice how a class always gets called with a value of the correct type (it's impossible to call Square.class.draw(circle), because circle.draw() always goes to the correct implementation).

现在,许多语言有一些这样的东西,没有 trait 部分。例如,在Python中,我可以做:

Now, lots of languages have something a bit like this without the trait part. For example, in Python, I can do:

class Square:
  def draw(self): ...
class Circle:
  def draw(self): ...

当我调用 shape.draw()时,它会调用正确的事情。但如果我有其他类:

and when I call shape.draw(), it will call the right thing. But if I have some other class:

class Thursday: ...

然后我可以调用 new Thursday()。draw()在运行时。 Scala是一种类型安全的语言(或多或少):此方法工作正常:

then I can call new Thursday().draw(), and I'll get an error at runtime. Scala is a type-safe language (more or less): this method works fine:

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

/ p>

while this method won't compile:

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

Scala的类型系统非常强大,你可以使用它来证明你的代码的各种事情,但至少,它保证的一个好的事情是你永远不会调用不存在的方法。但是,当我们想要在未知类型的形状上调用 draw()方法时,会出现一些问题。在某些语言(例如我相信锡兰)你可以写一个这样的方法(无效的Scala语法):

Scala's type system is very powerful and you can use it to prove all sorts of things about your code, but at a minimum, one of the nice things it guarantees is "you will never call a method that doesn't exist". But that presents a bit of a problem when we want to call our draw() method on an unknown type of shape. In some languages (e.g. I believe Ceylon) you can actually write a method like this (invalid Scala syntax):

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

我们想要:如果有人写了自己的 Star 类,我们希望能够将它包含在我们传递给 drawAll ,只要它具有 draw()方法

But even that's not really what we want: if someone writes their own Star class, we'd like to be able to include that in the list we pass to drawAll, as long as it has a draw() method.

这是 trait 的来源。

trait Shape {
  def draw(): Unit
  def area(): Double
}

class Circle extends Shape {...}

表示大致我保证 Circle 有一个 def draw ():Unit 方法(回想一下,这真的意味着我承诺 Circle.class 包含一个值 => Unit )。编译器将强制执行你的承诺,拒绝编译 Circle 如果它不实现给定的方法。然后我们可以做:

means roughly "I promise that Circle has a def draw(): Unit method. (Recall that this really means "I promise Circle.class contains a value draw: Circle => Unit). The compiler will enforce your promise, refusing to compile Circle if it doesn't implement the given methods. Then we can do:

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

并且编译器要求中的每个 shape 来自具有 def draw():Unit 方法的类型。所以 shape .__ type.draw(shape)是safe,我们的方法保证只有实际存在的方法。

and the compiler requires that every shape in shapes is from a type with a def draw(): Unit method. So shape.__type.draw(shape) is "safe", and our method is guaranteed to only call methods that actually exist.

(事实上,Scala还有一个更强大的方法来实现相同的效果,类型类模式,但现在不用担心了。)

(In fact Scala also has a more powerful way of achieving the same effect, the typeclass pattern, but let's not worry about that for now.)

这更简单,但也messier - 这是一个纯实用的东西。

This is simpler, but also "messier" - it's a purely practical thing.

假设我们有一些常见的代码与对象的状态。例如,我们可能有一堆不同的动物可以吃东西:

Suppose we have some common code that goes with an object's state. For example, we might have a bunch of different animals that can eat things:

class Horse {
  private var stomachContent: Double = ...
  def eat(food: Food) = {
     //calorie calculation
     stomachContent += calories
  }
}
class Dog {
  def eat(food: Food) = ...
}

而不是两次编写相同的代码,我们可以把它放在 trait

Rather than writing the same code twice, we can put it in a trait:

trait HasStomach {
  var stomachContent: Double
  def eat(food: Food) = ...
}
class Horse extends HasStomach
class Dog extends HasStomach

请注意,这是我们在前一种情况下写的相同的东西,所以我们也可以使用以相同的方式:

Notice that this is the same thing we wrote in the previous case, and so we can also use it the same way:

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

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

But hopefully you can see that our intent is different; we might do the same thing even if eat was an "internal" method that couldn't be called by any outside functions.

有些人批评传统的OO继承,因为它混合了这两个含义。没有办法说我只想分享这段代码,我不想让其他函数调用它。这些人倾向于认为共享代码应通过组合发生:而不是说我们的 Horse 延伸 HasStomach ,我们应该将 Stomach 组成 Horse

Some people have criticised "traditional" OO inheritance because it "mixes up" these two meanings. There's no way to say "I just want to share this code, I don't want to let other functions call it". These people tend to argue that sharing code should happen through composition: rather than saying that our Horse extends HasStomach, we should compose a Stomach into our Horse:

class Stomach {
  val content: Double = ...
  def eat(food: Food) = ...
}
class Horse {
  val stomach: Stomach
  def eat(food: Food) = stomach.eat(food)
}

这个观点有一些道理,但在实践中在更长的代码比传统的OO的方法,特别是当你想为两个类型之间有一些小的,微小的差异的一个大的,复杂的对象做两个不同的类型。

There is some truth to this view, but in practice (in my experience) it tends to result in longer code than the "traditional OO" approach, particularly when you want to make two different types for a large, complex object with some small, minor difference between the two types.

到目前为止,我所说的一切都适用于 trait s和抽象类 es(在一定程度上也到 class es,但我们不要这样做。)

So far everything I've said applies equally to traits and abstract classes (and to a certain extent also to classes, but let's not go into that).

在许多情况下, trait 抽象类并且有些人建议使用差异来声明意图:如果你想实现一个共同的接口,使用 trait ,如果您要共享实现代码,请使用抽象类。但在我看来,最重要的区别在于构造函数多重继承

For many cases, both a trait and an abstract class will work, and some people advise using the difference to declare intent: if you want to implement a common interface, use a trait, and if you want to share implementation code, use an abstract class. But in my opinion the most important difference is about constructors and multiple inheritance.

继承;一个类可以 extend 几个父母:

class Horse extends HasStomach, HasLegs, ...

这是有用的,显而易见的原因,但可能会在钻石继承情况,特别是当您有调用超类方法的方法时。有关Python中出现的某些问题,请参见 Python的超级考虑的有害性,并注意在实践中,大多数的问题发生在构造函数,因为这些是通常想要调用超类方法的方法。

This is useful for obvious reasons, but can have problems in diamond inheritance cases, particularly when you have methods that call a superclass method. See Python's Super Considered Harmful for some of the problems that arise in Python, and note that in practice, most of the problems happen with constructors, because these are the methods that usually want to call a superclass method.

Scala有一个优雅的解决方案:抽象类可能有构造函数,但 trait 可能不会。类可以从任何数量的 trait 继承,但是抽象类必须是第一个父类。这意味着任何类都只有一个具有构造函数的父类,所以总是很明显哪个方法是超类构造函数。

Scala has an elegant solution for this: abstract classes may have constructors, but traits may not. A class may inherit from any number of traits, but an abstract class must be the first parent. This means that any class has exactly one parent with a constructor, so it's always obvious which method is the "superclass constructor".

实用的代码,我的建议是总是使用 trait ,如果可能,只使用抽象类有一个构造函数。

So in practical code, my advice is to always use traits where possible, and only use abstract class for something that needs to have a constructor.

这篇关于你如何解释Scala的抽象类功能给6年级学生?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆