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

嵌套类

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

嵌套类就是在外部类的命名空间内部定义的类。这意味着外部类“拥有”嵌套类。这个特性并不是必须的,但将类嵌套可以使您的代码更加清晰。在下面的示例中,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 意味着 PrivatePlaneAirport 外部完全看不见,因此您不能在 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 包含 ToiletSink

局部类

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

// NestedClasses/LocalClasses.kt
package nestedclasses

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

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

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

AmphibianFroglocalClasses() 外部是不可见的,所以您不能从函数中返回它们。要返回局部类的对象,您必须将其上转型为在函数外部定义的类或接口(假设它扩展了一个类或接口),并且不能在 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() 函数将 seatordinal 值加一,然后使用库函数 coerceAtMost() 来确保新值不会超过 First.ordinal,最后通过索引到 values() 来得到新的 Seat 类型。遵循函数式编程的原则,升级一个 Ticket 会产生一个新的 Ticket,而不是修改旧的 Ticket

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

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

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

// 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 属性以获取要更改的单元格的位置。

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

尝试使用不同的 siderandomSeed 实验一下 FillIt

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