继承
继承 是一种通过重新使用和修改现有类来创建新类的机制。
对象通过属性存储数据,并通过成员函数执行操作。每个对象都占据唯一的存储位置,因此一个对象的属性可以与其他每个对象具有不同的值。对象还属于一个称为类的类别,该类别确定了其对象的形式(属性和函数)。因此,对象的外观类似于形成它的类。
创建和调试一个类可能需要大量的工作。如果您想要创建一个与现有类类似但具有一些变化的类,怎么办?从头开始构建一个新类似乎有些浪费。面向对象的语言提供了一种称为继承的重用机制。
继承遵循生物遗传的概念。您会说:“我想从现有类创建一个新类,但加入一些添加和修改。”
继承的语法与实现接口类似。要从现有类 Base
继承新类 Derived
,请使用 :
(冒号):
// Inheritance/BasicInheritance.kt
package inheritance
open class Base
class Derived : Base()
接下来的原子解释了在继承期间 Base
后面为什么要加括号。
术语基类和派生类(或父类和子类,或超类和子类)经常用于描述继承关系。
基类必须是 open
的。非 open
的类不允许继承 - 默认情况下它是封闭的。这与大多数其他面向对象的语言不同。例如,在 Java 中,一个类会自动可继承,除非您通过将该类声明为 final
来明确禁止继承。尽管 Kotlin 允许这样做,但 final
修饰符是多余的,因为每个类默认情况下实际上都是 final
的:
// Inheritance/OpenAndFinalClasses.kt
package inheritance
// 这个类可以被继承:
open class Parent
class Child : Parent()
// Child 不是 open 的,所以这会失败:
// class GrandChild : Child()
// 这个类无法被继承:
final class Single
// 与使用 'final' 一样:
class AnotherSingle
Kotlin 强制您通过使用 open
关键字来明确类是为继承而设计的,从而澄清您的意图。
在下面的示例中,GreatApe
是一个基类,并且具有两个具有固定值的属性。派生类 Bonobo
,Chimpanzee
和 BonoboB
是与其父类相同的新类型:
// Inheritance/GreatApe.kt
package inheritance.ape1
import atomictest.eq
open class GreatApe {
val weight = 100.0
val age = 12
}
open class Bonobo : GreatApe()
class Chimpanzee : GreatApe()
class BonoboB : Bonobo()
fun GreatApe.info() = "wt: $weight age: $age"
fun main() {
GreatApe().info() eq "wt: 100.0 age: 12"
Bonobo().info() eq "wt: 100.0 age: 12"
Chimpanzee().info() eq "wt: 100.0 age: 12"
BonoboB().info() eq "wt: 100.0 age: 12"
}
info()
是 GreatApe
的扩展函数,因此您自然可以在 GreatApe
上调用它。但请注意,您还可以在 Bonobo
、Chimpanzee
或 BonoboB
上调用 info()
!尽管后三者是不同的类型,Kotlin 仍然会接受它们,就像它们是 GreatApe
的相同类型一样。这适用于继承的任何级别 - BonoboB
距离 GreatApe
有两个继承级别。
继承保证了从 GreatApe
继承的任何内容都是 GreatApe
。所有作用于派生类对象的代码都知道 GreatApe
是它们的核心,因此 GreatApe
中的任何函数和属性在其子类中也将可用。
继承使您能够编写一段代码(info()
函数),不仅可以与一个类一起使用,还可以与继承该类的每个类一起使用。因此,继承为代码简化和重用提供了机会。
GreatApe.kt
有些过于简单,因为所有的类都是相同的。当您开始覆盖函数时,继承变得有趣,这意味着在派生类中重新定义基类中的函数以在派生类中执行不同的操作。
让我们看看 GreatApe.kt
的另一个版本。这次我们包括在子类中修改的成员函数:
// Inheritance/GreatApe2.kt
package inheritance.ape2
import atomictest.eq
open class GreatApe {
protected var energy = 0
open fun call() = "Hoo!"
open fun eat() {
energy += 10
}
fun climb(x: Int) {
energy -= x
}
fun energyLevel() = "Energy: $energy"
}
class Bonobo : GreatApe() {
override fun call() = "Eep!"
override fun eat() {
// 修改基类的变量:
energy += 10
// 调用基类的版本:
super.eat()
}
// 添加一个函数:
fun run() = "Bonobo run"
}
class Chimpanzee : GreatApe() {
// 新属性
:
val additionalEnergy = 20
override fun call() = "Yawp!"
override fun eat() {
energy += additionalEnergy
super.eat()
}
// 添加一个函数:
fun jump() = "Chimp jump"
}
fun talk(ape: GreatApe): String {
// ape.run() // 不是 GreatApe 函数
// ape.jump() // 也不是这个
ape.eat()
ape.climb(10)
return "${ape.call()} ${ape.energyLevel()}"
}
fun main() {
// 无法访问 'energy':
// GreatApe().energy
talk(GreatApe()) eq "Hoo! Energy: 0"
talk(Bonobo()) eq "Eep! Energy: 10"
talk(Chimpanzee()) eq "Yawp! Energy: 20"
}
每个 GreatApe
都有一个 call()
。它们在吃东西时存储 energy
,并在爬行时消耗能量。
如 限制可见性 中所述,派生类无法访问基类的 private
成员。有时,基类的创建者可能希望将特定成员赋予派生类的访问权限,但不授予外界的访问权限。这就是 protected
的作用:protected
成员对外界是封闭的,但可以在子类中访问或覆盖。
如果我们将 energy
声明为 private
,则无法在每次使用 GreatApe
时更改它,这是很好的,但我们在子类中也无法访问它。将其设置为 protected
允许我们在子类中保持它可访问,但对外界是不可见的。
call()
在 Bonobo
和 Chimpanzee
中的定义方式与在 GreatApe
中的定义方式相同。它没有参数,类型推断确定其返回一个 String
。
Bonobo
和 Chimpanzee
都应该对 call()
有不同的行为,所以我们想要更改它们的 call()
定义。如果您在派生类中创建一个与基类中的函数相同的函数签名,您将用新行为替换基类中定义的行为。这被称为覆盖。
当 Kotlin 在派生类中看到与基类中相同的函数签名时,它会认为您犯了一个错误,这被称为意外覆盖。如果您编写了一个与基类中的函数同名的函数,您会收到一个错误消息,提示您忘记了 override
关键字。Kotlin 假设您无意中选择了相同的名称、参数和返回类型,除非您使用 override
关键字(您首次在 Constructors 中看到过)来表示“是的,我打算这样做。” override
关键字还有助于阅读代码,这样您就不必比较签名来注意覆盖。
Kotlin 在覆盖函数时还施加了额外的约束。就像您不能继承一个基类,除非该基类是 open
的一样,您不能覆盖基类中的函数,除非该函数在基类中被定义为 open
。请注意,climb()
和 energyLevel()
都不是 open
,因此它们不能被覆盖。在 Kotlin 中,没有明确的意图,就无法实现继承和覆盖。
特别有趣的是,将 Bonobo
或 Chimpanzee
视为普通的 GreatApe
并在 talk()
中进行处理。在 talk()
内部,call()
在每种情况下都会产生正确的行为。talk()
不知何故知道对象的确切类型,并产生适当的 call()
变体。这就是多态性。
在 talk()
内部,您只能调用 GreatApe
的成员函数,因为 talk()
的参数是 GreatApe
。即使 Bonobo
定义了 run()
,Chimpanzee
定义了 jump()
,这两个函数都不是 GreatApe
的一部分。
当您覆盖函数时,通常希望调用基类版本的该函数(一方面是为了重用代码),如在 eat()
的覆盖中所见。这会产生一个困境:如果您简单地调用 eat()
,您会调用当前所在的同一个函数(正如我们在递归中所见)。为了调用基类版本的 eat()
,请使用 super
关键字,缩写为“superclass”。
练习和解答可以在 www.AtomicKotlin.com 找到。