嵌套类

嵌套类可以在对象内创建更精细的结构。

嵌套类就是在外部类的命名空间内部定义的类。这意味着外部类“拥有”嵌套类。这个特性并不是必须的,但将类嵌套可以使您的代码更加清晰。在下面的示例中,Plane 嵌套在 Airport 内部:

// NestedClasses/Airport.kt
package nestedclasses
import atomictest.eq
import nestedclasses.Airport.Plane

class Airport(private val code: String) {
  open class Plane {
    // 可以访问 private 属性:
    fun contact(airport: Airport) =
      "Contacting ${airport.code}"
  }
  private class PrivatePlane : Plane()
  fun privatePlane(): Plane = PrivatePlane()
}

fun main() {
  val denver = Airport("DEN")
  var plane = Plane()                   // [1]
  plane.contact(denver) eq "Contacting DEN"
  // 无法进行如下操作:
  // val privatePlane = Airport.PrivatePlane()
  val frankfurt = Airport("FRA")
  plane = frankfurt.privatePlane()
  // 无法进行如下操作:
  // val p = plane as PrivatePlane      // [2]
  plane.contact(frankfurt) eq "Contacting FRA"
}

在 contact() 函数中,嵌套类 Plane 可以访问 airport 参数中的 private 属性 code,而普通的类则无法访问。除此之外,Plane 仅仅是位于 Airport 命名空间内部的一个普通类。

创建 Plane 对象并不需要 Airport 对象,但是如果您在 Airport 类体外部创建它,通常需要在 [1] 处限定构造函数调用。通过导入 nestedclasses.Airport.Plane,我们避免了这种限定。

嵌套类可以是 private 的,就像 PrivatePlane 一样。将它设置为 private 意味着 PrivatePlane 在 Airport 外部完全看不见,因此您不能在 Airport 外部调用 PrivatePlane 的构造函数。如果您在成员函数中定义并返回一个 PrivatePlane,如 privatePlane() 中所示,结果必须上转型为一个 public 类型(假设它扩展了一个 public 类型),并且不能将其下转型为 private 类型,就像 [2] 处所示。

这里还有一个嵌套的示例,Cleanable 是封闭类 House 和所有嵌套类的基类。clean() 遍历 parts 列表并为每个部分调用 clean(),从而产生一种递归:

// NestedClasses/NestedHouse.kt
package nestedclasses
import atomictest.*

abstract class Cleanable(val id: String) {
  open val parts: List<Cleanable> = listOf()
  fun clean(): String {
    val text = "$id clean"
    if (parts.isEmpty()) return text
    return "${parts.joinToString(
      " ", "(", ")",
      transform = Cleanable::clean)} $text\n"
  }
}

class House : Cleanable("House") {
  override val parts = listOf(
    Bedroom("Master Bedroom"),
    Bedroom("Guest Bedroom")
  )
  class Bedroom(id: String) : Cleanable(id) {
    override val parts =
      listOf(Closet(), Bathroom())
    class Closet : Cleanable("Closet") {
      override val parts =
        listOf(Shelf(), Shelf())
      class Shelf : Cleanable("Shelf")
    }
    class Bathroom : Cleanable("Bathroom") {
      override val parts =
        listOf(Toilet(), Sink())
      class Toilet : Cleanable("Toilet")
      class Sink : Cleanable("Sink")
    }
  }
}

fun main() {
  House().clean() eq """
  (((Shelf clean Shelf clean) Closet clean
   (Toilet clean Sink clean) Bathroom clean
  ) Master Bedroom clean
   ((Shelf clean Shelf clean) Closet clean
   (Toilet clean Sink clean) Bathroom clean
  ) Guest Bedroom clean
  ) House clean
  """
}

注意多级嵌套的情况。例如,Bedroom 包含 Bathroom,而 Bathroom 包含 Toilet 和 Sink。

局部类

在函数内部定义的类称为局部类:

// NestedClasses/LocalClasses.kt
package nestedclasses

fun localClasses() {
  open class Amphibian
  class Frog : Amphibian()
  val amphibian: Amphibian = Frog()
}

Amphibian 看起来更适合是一个接口而不是一个 open 类。但是,不允许使用局部接口。

局部的 open 类应该很少见;如果您需要这样一个类,那么您要创建的东西可能足够重要,以至于应该创建一个普通类。

Amphibian 和 Frog 在 localClasses() 外部是不可见的,所以您不能从函数中返回它们。要返回局部类的对象,您必须将其上转型为在函数外部定义的类或接口(假设它扩展了一个类或接口),并且不能在 main() 中将其下转型为 Frog,因为 Frog 不可用,所以 Kotlin 报告尝试使用 Frog 作为“未解析的引用”。

接口内部的类

类可以嵌套在接口内部:

// NestedClasses/WithinInterface.kt
package nestedclasses
import atomictest.eq

interface Item {
  val type: Type
  data class Type(val type: String)
}

class Bolt(type: String) : Item {
  override val type = Item.Type(type)
}

fun main() {
  val items = listOf(
    Bolt("Slotted"), Bolt("Hex")
  )
  items.map(Item::type) eq
    "[Type(type=Slotted), Type(type=Hex)]"
}

在 Bolt 类中,必须重写并使用限

定类名 Item.Type 来分配 val type。

嵌套枚举

枚举是类,因此它们可以嵌套在其他类内部:

// NestedClasses/Ticket.kt
package nestedclasses
import atomictest.eq
import nestedclasses.Ticket.Seat.*

class Ticket(
  val name: String,
  val seat: Seat = Coach
) {
  enum class Seat {
    Coach,
    Premium,
    Business,
    First
  }
  fun upgrade(): Ticket {
    val newSeat = values()[
      (seat.ordinal + 1)
      .coerceAtMost(First.ordinal)
    ]
    return Ticket(name, newSeat)
  }
  fun meal() = when(seat) {
    Coach -> "Bag Meal"
    Premium -> "Bag Meal with Cookie"
    Business -> "Hot Meal"
    First -> "Private Chef"
  }
  override fun toString() = "$seat"
}

fun main() {
  val tickets = listOf(
    Ticket("Jerry"),
    Ticket("Summer", Premium),
    Ticket("Squanchy", Business),
    Ticket("Beth", First)
  )
  tickets.map(Ticket::meal) eq
    "[Bag Meal, Bag Meal with Cookie, " +
    "Hot Meal, Private Chef]"
  tickets.map(Ticket::upgrade) eq
    "[Premium, Business, First, First]"
  tickets eq
    "[Coach, Premium, Business, First]"
  tickets.map(Ticket::meal) eq
    "[Bag Meal, Bag Meal with Cookie, " +
    "Hot Meal, Private Chef]"
}

upgrade() 函数将 seat 的 ordinal 值加一,然后使用库函数 coerceAtMost() 来确保新值不会超过 First.ordinal,最后通过索引到 values() 来得到新的 Seat 类型。遵循函数式编程的原则,升级一个 Ticket 会产生一个新的 Ticket,而不是修改旧的 Ticket。

meal() 使用 when 测试每种 Seat 类型,这暗示我们可以使用多态来替代这种做法。

枚举不能嵌套在函数内部,并且不能继承其他类(包括其他枚举)。

接口可以包含嵌套枚举。FillIt 是一个类似游戏的模拟,它使用随机选择的 X 和 O 标记填充一个方形网格:

// NestedClasses/FillIt.kt
package nestedclasses
import nestedclasses.Game.State.*
import nestedclasses.Game.Mark.*
import kotlin.random.Random
import atomictest.*

interface Game {
  enum class State { Playing, Finished }
  enum class Mark { Blank, X ,O }
}

class FillIt(
  val side: Int = 3, randomSeed: Int = 0
): Game {
  val rand = Random(randomSeed)
  private var state = Playing
  private val grid =
    MutableList(side * side) { Blank }
  private var player = X
  fun turn() {
    val blanks = grid.withIndex()
      .filter { it.value == Blank }
    if(blanks.isEmpty()) {
      state = Finished
    } else {
      grid[blanks.random(rand).index] = player
      player = if (player == X) O else X
    }
  }
  fun play() {
    while(state != Finished)
      turn()
  }
  override fun toString() =
    grid.chunked(side).joinToString("\n")
}

fun main() {
  val game = FillIt(8, 17)
  game.play()
  game eq """
  [O, X, O, X, O, X, X, X]
  [X, O, O, O, O, O, X, X]
  [O, O, X, O, O, O, X, X]
  [X, O, O, O, O, O, X, O]
  [X, X, O, O, X, X, X, O]
  [X, X, O, O, X, X, O, X]
  [O, X, X, O, O, O, X, O]
  [X, O, X, X, X, O, X, X]
  """
}

为了测试的目的,我们使用 randomSeed 来为 Random 对象设置种子,以便每次程序运行时产生相同的输出。grid 的每个元素都初始化为 Blank。在 turn() 函数中,首先找到所有包含 Blank 的单元格以及它们的索引。如果没有更多的 Blank 单元格,那么模拟就完成了。否则,我们使用带有种子生成器的 random() 来选择一个 Blank 单元格。由于我们之前使用了 withIndex(),我们必须选择 index 属性以获取要更改的单元格的位置。

为了以二维网格的形式显示 List,toString() 使用库函数 chunked() 将 List 分成长度为 side 的块,然后使用换行符将它们连接在一起。

尝试使用不同的 side 和 randomSeed 实验一下 FillIt。

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