嵌套类
嵌套类可以在对象内创建更精细的结构。
嵌套类就是在外部类的命名空间内部定义的类。这意味着外部类“拥有”嵌套类。这个特性并不是必须的,但将类嵌套可以使您的代码更加清晰。在下面的示例中,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 上找到。