Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

类型检查

在 Kotlin 中,您可以根据对象的类型轻松进行操作。通常,这种活动属于多态的领域,因此类型检查可以启用有趣的设计选择。

传统上,类型检查用于特殊情况。例如,大多数昆虫都能飞行,但有少数几种不能飞行。在 basic() 中,我们使用类型检查来选择那些不能飞行的昆虫:

// TypeChecking/Insects.kt
package typechecking
import atomictest.eq

interface Insect {
  fun walk() = "$name: walk"
  fun fly() = "$name: fly"
}

class HouseFly : Insect

class Flea : Insect {
  override fun fly() =
    throw Exception("Flea cannot fly")
  fun crawl() = "Flea: crawl"
}

fun Insect.basic() =
  walk() + " " +
  if (this is Flea)
    crawl()
  else
    fly()

interface SwimmingInsect : Insect {
  fun swim() = "$name: swim"
}

interface WaterWalker : Insect {
  fun walkWater() =
    "$name: walk on water"
}

class WaterBeetle : SwimmingInsect
class WaterStrider : WaterWalker
class WhirligigBeetle :
  SwimmingInsect, WaterWalker

fun Insect.water() =
  when(this) {
    is SwimmingInsect -> swim()
    is WaterWalker -> walkWater()
    else -> "$name: drown"
  }

fun main() {
  val insects = listOf(
    HouseFly(), Flea(), WaterStrider(),
    WaterBeetle(), WhirligigBeetle()
  )
  insects.map { it.basic() } eq
    "[HouseFly: walk HouseFly: fly, " +
    "Flea: walk Flea: crawl, " +
    "WaterStrider: walk WaterStrider: fly, " +
    "WaterBeetle: walk WaterBeetle: fly, " +
    "WhirligigBeetle: walk " +
    "WhirligigBeetle: fly]"
  insects.map { it.water() } eq
    "[HouseFly: drown, Flea: drown, " +
    "WaterStrider: walk on water, " +
    "WaterBeetle: swim, " +
    "WhirligigBeetle: swim]"
}

也有极少数的昆虫可以在水上行走或在水下游泳。同样,将这些特殊情况的行为放入基类以支持这一小部分类型是没有意义的。相反,Insect.water() 包含一个 when 表达式,该表达式会为特殊行为选择那些子类型,并假设其他一切都是标准行为。

选择一些孤立的类型进行特殊处理是类型检查的典型用例。请注意,向系统添加新类型不会影响现有代码(除非新类型也需要特殊处理)。

为了简化代码,name 会生成由问题下的 this 指向的对象类型:

// TypeChecking/AnyName.kt
package typechecking

val Any.name
  get() = this::class.simpleName

name 接受一个 Any,并使用 ::class 获取关联的类引用,然后生成该类的 simpleName

现在考虑一个“形状”示例的变体:

// TypeChecking/TypeCheck1.kt
package typechecking
import atomictest.eq

interface Shape {
  fun draw(): String
}

class Circle : Shape {
  override fun draw() = "Circle: Draw"
}

class Square : Shape {
  override fun draw() = "Square: Draw"
  fun rotate() = "Square: Rotate"
}

fun turn(s: Shape) = when(s) {
  is Square -> s.rotate()
  else -> ""
}

fun main() {
  val shapes = listOf(Circle(), Square())
  shapes.map { it.draw() } eq
    "[Circle: Draw, Square: Draw]"
  shapes.map { turn(it) } eq
    "[, Square: Rotate]"
}

有几个原因可能导致您将 rotate() 添加到 Square 而不是 Shape

  • Shape 接口不在您的控制范围内,因此您无法修改它。
  • 旋转 Square 看起来像是一个特殊情况,不应该为 Shape 接口增加负担或复杂性。
  • 您只是试图通过添加 Square 来快速解决问题,而不想费劲将 rotate() 放入 Shape 中,并在所有子类型中实现它。

当您必须通过添加更多类型来发展您的系统时,情况开始变得混乱:

// TypeChecking/TypeCheck2.kt
package typechecking
import atomictest.eq

class Triangle : Shape {
  override fun draw() = "Triangle: Draw"
  fun rotate() = "Triangle: Rotate"
}

fun turn2(s: Shape) = when(s) {
  is Square -> s.rotate()
  is Triangle -> s.rotate()
  else -> ""
}

fun main() {
  val shapes =
    listOf(Circle(), Square(), Triangle())
  shapes.map { it.draw() } eq
    "[Circle: Draw, Square: Draw, " +
    "Triangle: Draw]"
  shapes.map { turn(it) } eq
    "[, Square: Rotate, ]"
  shapes.map { turn2(it) } eq
    "[, Square: Rotate, Triangle: Rotate]"
}

shapes.map { it.draw() } 中的多态调用会适应新的 Triangle 类,而无需任何更改或错误。此外,Kotlin 不允许除非它实现了 draw(),否则就无法使用 Triangle

原始的 turn() 在添加 Triangle 时不会出现问题,但它也不会产生我们想要的结果。为了生成所需的行为,turn() 必须变为 turn2()

假设您的系统开始累积更多类似于 turn() 的函数。Shape 逻辑现在分散在所有这些函数中,而不是在 Shape 层次结构内部集中。如果您添加更多的 Shape 类型

,您必须搜索包含在 Shape 类型上切换的每个函数,并将其修改为包括新的情况。如果您错过了其中的任何函数,编译器将无法捕获到它。

turn()turn2() 展示了通常被称为类型检查编码的内容,这意味着在您的系统中测试每种类型。 (如果您只寻找一种或几种特殊类型,通常不被认为是类型检查编码)。

在传统的面向对象语言中,类型检查编码通常被认为是一种反模式,因为它会引发一个或多个代码片段,这些代码片段必须在您添加或更改系统中的类型时保持警惕维护和更新。另一方面,多态将这些更改封装到您添加或修改的类型中,然后这些更改会透明地传播到您的系统中。

请注意,此问题仅在系统需要通过添加更多的 Shape 类型来发展时才会发生。如果这不是您的系统如何发展的方式,您就不会遇到这个问题。如果这是一个问题,通常不会突然发生,而会随着您的系统不断发展而变得越来越困难。

我们将看到 Kotlin 如何通过使用 sealed 类显著减轻了这个问题。这个解决方案并不完美,但类型检查成为了一个更合理的设计选择。

辅助函数中的类型检查

BeverageContainer 的本质是容纳和提供饮料。将回收视为辅助函数似乎是有道理的:

// TypeChecking/BeverageContainer.kt
package typechecking
import atomictest.eq

interface BeverageContainer {
  fun open(): String
  fun pour(): String
}

class Can : BeverageContainer {
  override fun open() = "Pop Top"
  override fun pour() = "Can: Pour"
}

open class Bottle : BeverageContainer {
  override fun open() = "Remove Cap"
  override fun pour() = "Bottle: Pour"
}

class GlassBottle : Bottle()
class PlasticBottle : Bottle()

fun BeverageContainer.recycle() =
  when(this) {
    is Can -> "Recycle Can"
    is GlassBottle -> "Recycle Glass"
    else -> "Landfill"
  }

fun main() {
  val refrigerator = listOf(
    Can(), GlassBottle(), PlasticBottle()
  )
  refrigerator.map { it.open() } eq
    "[Pop Top, Remove Cap, Remove Cap]"
  refrigerator.map { it.recycle() } eq
    "[Recycle Can, Recycle Glass, " +
    "Landfill]"
}

通过将 recycle() 定义为辅助函数,它将不同的回收行为捕获在一个单独的位置,而不是通过将 recycle() 作为成员函数分布在 BeverageContainer 层次结构中。

使用 when 对类型进行操作是干净且直观的,但设计仍然存在问题。当您添加新类型时,recycle() 悄悄地使用 else 子句。由于这一点,对于类型检查函数(如 recycle()),可能会错过必要的更改。我们希望编译器在我们忘记了类型检查时告诉我们,就像当我们实现接口或继承抽象类时,它会告诉我们忘记了覆盖一个函数一样。

在这里,sealed 类提供了显着的改进。将 Shape 设置为 sealed 类意味着在 turn() 中的 when(去掉 else 后)要求检查每种类型。接口不能是 sealed,因此我们必须将 Shape 重写为类:

// TypeChecking/TypeCheck3.kt
package typechecking3
import atomictest.eq
import typechecking.name

sealed class Shape {
  fun draw() = "$name: Draw"
}

class Circle : Shape()

class Square : Shape() {
  fun rotate() = "Square: Rotate"
}

class Triangle : Shape() {
  fun rotate() = "Triangle: Rotate"
}

fun turn(s: Shape) = when(s) {
  is Circle -> ""
  is Square -> s.rotate()
  is Triangle -> s.rotate()
}

fun main() {
  val shapes = listOf(Circle(), Square())
  shapes.map { it.draw() } eq
    "[Circle: Draw, Square: Draw]"
  shapes.map { turn(it) } eq
    "[, Square: Rotate]"
}

如果我们添加一个新的 Shape,编译器会告诉我们在 turn() 中添加一个新的类型检查路径。

但是,让我们看看当我们尝试将 sealed 应用于 BeverageContainer 问题时会发生什么。在此过程中,我们创建了额外的 CanBottle 子类型:

// TypeChecking/BeverageContainer2.kt
package typechecking2
import atomictest.eq

sealed class BeverageContainer {
  abstract fun open(): String
  abstract fun pour(): String
}

sealed class Can : BeverageContainer() {
  override fun open() = "Pop Top"
  override fun pour() = "Can: Pour"
}

class SteelCan : Can()
class AluminumCan : Can()

sealed class Bottle : BeverageContainer() {
  override fun open() = "Remove Cap"
  override fun pour() = "Bottle: Pour"
}

class GlassBottle : Bottle()
sealed class PlasticBottle : Bottle()
class PETBottle : PlasticBottle()
class HDPEBottle : PlasticBottle()

fun BeverageContainer.recycle() =
  when(this) {
    is Can -> "Recycle Can"
    is Bottle -> "Recycle Bottle"
  }

fun BeverageContainer.recycle2() =
  when(this) {
    is Can -> when(this) {
      is SteelCan -> "Recycle Steel"
      is AluminumCan -> "Recycle Aluminum"
    }
    is Bottle -> when(this) {
      is GlassBottle -> "Recycle Glass"
      is PlasticBottle -> when(this) {
        is PETBottle -> "Recycle PET"
        is HDPEBottle -> "Recycle HDPE"
     

 }
    }
  }

fun main() {
  val refrigerator = listOf(
    SteelCan(), AluminumCan(),
    GlassBottle(),
    PETBottle(), HDPEBottle()
  )
  refrigerator.map { it.open() } eq
    "[Pop Top, Pop Top, Remove Cap, " +
    "Remove Cap, Remove Cap]"
  refrigerator.map { it.recycle() } eq
    "[Recycle Can, Recycle Can, " +
    "Recycle Bottle, Recycle Bottle, " +
    "Recycle Bottle]"
  refrigerator.map { it.recycle2() } eq
    "[Recycle Steel, Recycle Aluminum, " +
    "Recycle Glass, " +
    "Recycle PET, Recycle HDPE]"
}

请注意,中间类 CanBottle 也必须是 sealed 类,以使此方法起作用。

只要类是 BeverageContainer 的直接子类,编译器就会保证在 recycle() 中的 when 是全面的。但是,像 GlassBottleAluminumCan 这样的子类并没有进行检查。为了解决这个问题,我们必须在 BeverageContainer2.kt 中的 recycle2() 中显式包含所见的嵌套 when 表达式,此时编译器确实需要进行全面的类型检查(尝试注释一个特定的 CanBottle 类型以验证此点)。

要创建一个健壮的类型检查解决方案,您必须严格在类层次结构的每个中间级别上使用 sealed,同时确保每个子类级别都有一个相应的嵌套 when。在这种情况下,如果您添加了 CanBottle 的新子类型,编译器将确保 recycle2() 检查每个子类型。

虽然不如多态那么干净,但这与先前的面向对象语言相比是一个重要的改进,允许您选择是编写多态成员函数还是辅助函数。请注意,只有在您有多个层次的继承时才会出现这个问题。

为了进行比较,让我们将 recycle() 带入 BeverageContainer 中,可以再次将其设置为 interface

// TypeChecking/BeverageContainer3.kt
package typechecking3
import atomictest.eq
import typechecking.name

interface BeverageContainer {
  fun open(): String
  fun pour() = "$name: Pour"
  fun recycle(): String
}

abstract class Can : BeverageContainer {
  override fun open() = "Pop Top"
}

class SteelCan : Can() {
  override fun recycle() = "Recycle Steel"
}

class AluminumCan : Can() {
  override fun recycle() = "Recycle Aluminum"
}

abstract class Bottle : BeverageContainer {
  override fun open() = "Remove Cap"
}

class GlassBottle : Bottle() {
  override fun recycle() = "Recycle Glass"
}

abstract class PlasticBottle : Bottle()

class PETBottle : PlasticBottle() {
  override fun recycle() = "Recycle PET"
}

class HDPEBottle : PlasticBottle() {
  override fun recycle() = "Recycle HDPE"
}

fun main() {
  val refrigerator = listOf(
    SteelCan(), AluminumCan(),
    GlassBottle(),
    PETBottle(), HDPEBottle()
  )
  refrigerator.map { it.open() } eq
    "[Pop Top, Pop Top, Remove Cap, " +
    "Remove Cap, Remove Cap]"
  refrigerator.map { it.recycle() } eq
    "[Recycle Steel, Recycle Aluminum, " +
    "Recycle Glass, " +
    "Recycle PET, Recycle HDPE]"
}

通过将 CanBottle 设置为 abstract 类,我们强制其子类在相同的方式中重写 recycle(),就像编译器在 BeverageContainer2.kt 中强制每种类型在 recycle2() 中进行检查一样。

现在,recycle() 的行为分布在类之间,这可能是可以接受的设计决策。如果您决定回收行为经常更改,而且希望将其集中在一个地方,那么使用 BeverageContainer2.kt 中的辅助类型检查的 recycle2() 可能更适合您的需求,Kotlin 的特性使这成为了合理的选择。

练习和解决方案可以在 www.AtomicKotlin.com 上找到。