中级: 开放类和特殊类
本章中,你将学习开放类、它们如何与接口协同工作以及 Kotlin 中可用的其他特殊类型的类。
开放类
如果你无法使用接口或抽象类,可以通过将类声明为 open 来显式地使其可继承。 为此,请在类声明前使用 open
关键字:
open class Vehicle
要创建一个继承自另一个类的类,请在类头后添加一个冒号,然后调用你希望继承的父类的构造函数:
class Car : Vehicle
在此示例中,Car
类继承自 Vehicle
类:
open class Vehicle(val make: String, val model: String)
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model)
fun main() {
// Creates an instance of the Car class
val car = Car("Toyota", "Corolla", 4)
// Prints the details of the car
println("Car Info: Make - ${car.make}, Model - ${car.model}, Number of doors - ${car.numberOfDoors}")
// Car Info: Make - Toyota, Model - Corolla, Number of doors - 4
}
就像创建普通类实例一样,如果你的类继承自父类,那么它必须初始化在父类头中声明的所有参数。因此在此示例中,Car
类的 car
实例初始化了父类参数:make
和 model
。
覆盖继承行为
如果你希望从一个类继承但需要改变其某些行为,可以覆盖(override)继承的行为。
默认情况下,无法覆盖父类的成员函数或属性。就像抽象类一样,你需要添加特殊关键字。
成员函数
为了允许父类中的函数被覆盖,请在其在父类中的声明前使用 open
关键字:
open fun displayInfo() {}
要覆盖继承的成员函数,请在子类中的函数声明前使用 override
关键字:
override fun displayInfo() {}
例如:
open class Vehicle(val make: String, val model: String) {
open fun displayInfo() {
println("Vehicle Info: Make - $make, Model - $model")
}
}
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) {
override fun displayInfo() {
println("Car Info: Make - $make, Model - $model, Number of Doors - $numberOfDoors")
}
}
fun main() {
val car1 = Car("Toyota", "Corolla", 4)
val car2 = Car("Honda", "Civic", 2)
// Uses the overridden displayInfo() function
car1.displayInfo()
// Car Info: Make - Toyota, Model - Corolla, Number of Doors - 4
car2.displayInfo()
// Car Info: Make - Honda, Model - Civic, Number of Doors - 2
}
此示例:
- 创建了两个继承自
Vehicle
类的Car
类实例:car1
和car2
。 - 覆盖了
Car
类中的displayInfo()
函数,使其也打印车门数量。 - 调用
car1
和car2
实例上的被覆盖的displayInfo()
函数。
属性
在 Kotlin 中,通过使用 open
关键字使属性可继承并在之后覆盖它,这并非常见做法。大多数情况下,你会使用抽象类或接口,其中属性默认是可继承的。
开放类内部的属性可被其子类访问。通常,最好直接访问它们,而不是用新属性来覆盖它们。
例如,假设你有一个名为 transmissionType
的属性,你希望稍后覆盖它。覆盖属性的语法与覆盖成员函数的语法完全相同。你可以这样做:
open class Vehicle(val make: String, val model: String) {
open val transmissionType: String = "Manual"
}
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) {
override val transmissionType: String = "Automatic"
}
然而,这不是一个好的实践。相反,你可以将该属性添加到你的可继承类的构造函数中,并在创建 Car
子类时声明其值:
open class Vehicle(val make: String, val model: String, val transmissionType: String = "Manual")
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model, "Automatic")
直接访问属性而不是覆盖它们,可以使代码更简洁、更易读。通过在父类中一次性声明属性并通过构造函数传递它们的值,你消除了子类中不必要的覆盖。
有关类继承和覆盖类行为的更多信息,请参阅继承。
开放类和接口
你可以创建一个既继承了类又实现了多个接口的类。在这种情况下,你必须先在冒号后声明父类,然后列出接口:
// 定义接口
interface EcoFriendly {
val emissionLevel: String
}
interface ElectricVehicle {
val batteryCapacity: Double
}
// 父类
open class Vehicle(val make: String, val model: String)
// 子类
open class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model)
// 继承自 Car 并实现两个接口的新类
class ElectricCar(
make: String,
model: String,
numberOfDoors: Int,
val capacity: Double,
val emission: String
) : Car(make, model, numberOfDoors), EcoFriendly, ElectricVehicle {
override val batteryCapacity: Double = capacity
override val emissionLevel: String = emission
}
特殊类
除了抽象类、开放类和数据类,Kotlin 还提供了特殊类型的类,用于各种目的,例如限制特定行为或减少创建小型对象的性能影响。
密封类
有时你可能希望限制继承。你可以使用密封类(sealed classes)来做到这一点。密封类是一种特殊的抽象类。一旦你声明一个类是密封的,你只能在同一个包中创建它的子类。在此范围之外无法从密封类继承。
TIP
包是相关类和函数的代码集合,通常位于一个目录中。要了解有关 Kotlin 中包的更多信息,请参阅包和导入。
要创建密封类,请使用 sealed
关键字:
sealed class Mammal
密封类与 when
表达式结合使用时特别有用。通过使用 when
表达式,你可以为所有可能的子类定义行为。例如:
sealed class Mammal(val name: String)
class Cat(val catName: String) : Mammal(catName)
class Human(val humanName: String, val job: String) : Mammal(humanName)
fun greetMammal(mammal: Mammal): String {
when (mammal) {
is Human -> return "Hello ${mammal.name}; You're working as a ${mammal.job}"
is Cat -> return "Hello ${mammal.name}"
}
}
fun main() {
println(greetMammal(Cat("Snowy")))
// Hello Snowy
}
在此示例中:
- 有一个名为
Mammal
的密封类,其构造函数中包含name
参数。 Cat
类继承自Mammal
密封类,并使用Mammal
类中的name
参数作为其自身构造函数中的catName
参数。Human
类继承自Mammal
密封类,并使用Mammal
类中的name
参数作为其自身构造函数中的humanName
参数。它在其构造函数中也包含job
参数。greetMammal()
函数接受Mammal
类型的参数并返回一个字符串。- 在
greetMammal()
函数体内,有一个when
表达式,它使用is
运算符来检查mammal
的类型并决定执行哪个操作。 main()
函数使用Cat
类的一个实例和名为Snowy
的name
参数调用greetMammal()
函数。
NOTE
本教程在空安全章节中更详细地讨论了 is
运算符。
有关密封类及其推荐用例的更多信息,请参阅密封类和接口。
枚举类
当你希望在一个类中表示有限的离散值集时,枚举类(Enum classes)非常有用。枚举类包含枚举常量,这些常量本身就是枚举类的实例。
要创建枚举类,请使用 enum
关键字:
enum class State
假设你希望创建一个包含不同过程状态的枚举类。每个枚举常量必须用逗号 ,
分隔:
enum class State {
IDLE, RUNNING, FINISHED
}
State
枚举类包含枚举常量:IDLE
、RUNNING
和 FINISHED
。要访问枚举常量,请使用类名后跟 .
和枚举常量的名称:
val state = State.RUNNING
你可以使用此枚举类与 when
表达式结合,根据枚举常量的值定义要执行的操作:
enum class State {
IDLE, RUNNING, FINISHED
}
fun main() {
val state = State.RUNNING
val message = when (state) {
State.IDLE -> "It's idle"
State.RUNNING -> "It's running"
State.FINISHED -> "It's finished"
}
println(message)
// It's running
}
枚举类可以像普通类一样拥有属性和成员函数。
例如,假设你正在处理 HTML 并希望创建一个包含一些颜色的枚举类。
你希望每种颜色都有一个属性,我们称之为 rgb
,其中包含其十六进制的 RGB 值。
创建枚举常量时,你必须用此属性来初始化它:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF),
YELLOW(0xFFFF00)
}
Kotlin 将十六进制数存储为整数,因此 rgb
属性的类型为 Int
,而不是 String
。
要向此类添加成员函数,请使用分号 ;
将其与枚举常量分隔开:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF),
YELLOW(0xFFFF00);
fun containsRed() = (this.rgb and 0xFF0000 != 0)
}
fun main() {
val red = Color.RED
// 在枚举常量上调用 containsRed() 函数
println(red.containsRed())
// true
// 通过类名在枚举常量上调用 containsRed() 函数
println(Color.BLUE.containsRed())
// false
println(Color.YELLOW.containsRed())
// true
}
在此示例中,containsRed()
成员函数使用 this
关键字访问枚举常量的 rgb
属性值,并检查十六进制值是否包含 FF
作为其最高位,以返回布尔值。
有关更多信息,请参阅枚举类。
内联值类
有时在你的代码中,你可能希望从类中创建小型对象并短暂使用它们。这种方法可能会对性能产生影响。内联值类(Inline value classes)是一种特殊类型的类,可以避免这种性能影响。然而,它们只能包含值。
要创建内联值类,请使用 value
关键字和 @JvmInline
注解:
@JvmInline
value class Email
TIP
@JvmInline
注解指示 Kotlin 在编译时优化代码。要了解更多信息,请参阅注解。
内联值类必须在类头中初始化一个单一属性。
假设你希望创建一个收集电子邮件地址的类:
// address 属性在类头中初始化。
@JvmInline
value class Email(val address: String)
fun sendEmail(email: Email) {
println("Sending email to ${email.address}")
}
fun main() {
val myEmail = Email("[email protected]")
sendEmail(myEmail)
// Sending email to [email protected]
}
在此示例中:
Email
是一个内联值类,其类头中包含一个属性:address
。sendEmail()
函数接受Email
类型的对象,并将字符串打印到标准输出。main()
函数:- 创建一个名为
email
的Email
类实例。 - 在
email
对象上调用sendEmail()
函数。
- 创建一个名为
通过使用内联值类,你可以使该类内联化,并可以直接在代码中使用它而无需创建对象。这可以显著减少内存占用并提高代码的运行时性能。
有关内联值类的更多信息,请参阅内联值类。
实践
练习 1
你管理着一个配送服务,需要一种方法来跟踪包裹的状态。创建一个名为 DeliveryStatus
的密封类,其中包含数据类来表示以下状态:Pending
(待处理)、InTransit
(运输中)、Delivered
(已送达)、Canceled
(已取消)。完善 DeliveryStatus
类声明,使 main()
函数中的代码能够成功运行:
|---|---|
sealed class // Write your code here
fun printDeliveryStatus(status: DeliveryStatus) {
when (status) {
is DeliveryStatus.Pending -> {
println("The package is pending pickup from ${status.sender}.")
}
is DeliveryStatus.InTransit -> {
println("The package is in transit and expected to arrive by ${status.estimatedDeliveryDate}.")
}
is DeliveryStatus.Delivered -> {
println("The package was delivered to ${status.recipient} on ${status.deliveryDate}.")
}
is DeliveryStatus.Canceled -> {
println("The delivery was canceled due to: ${status.reason}.")
}
}
}
fun main() {
val status1: DeliveryStatus = DeliveryStatus.Pending("Alice")
val status2: DeliveryStatus = DeliveryStatus.InTransit("2024-11-20")
val status3: DeliveryStatus = DeliveryStatus.Delivered("2024-11-18", "Bob")
val status4: DeliveryStatus = DeliveryStatus.Canceled("Address not found")
printDeliveryStatus(status1)
// The package is pending pickup from Alice.
printDeliveryStatus(status2)
// The package is in transit and expected to arrive by 2024-11-20.
printDeliveryStatus(status3)
// The package was delivered to Bob on 2024-11-18.
printDeliveryStatus(status4)
// The delivery was canceled due to: Address not found.
}
|---|---|
sealed class DeliveryStatus {
data class Pending(val sender: String) : DeliveryStatus()
data class InTransit(val estimatedDeliveryDate: String) : DeliveryStatus()
data class Delivered(val deliveryDate: String, val recipient: String) : DeliveryStatus()
data class Canceled(val reason: String) : DeliveryStatus()
}
fun printDeliveryStatus(status: DeliveryStatus) {
when (status) {
is DeliveryStatus.Pending -> {
println("The package is pending pickup from ${status.sender}.")
}
is DeliveryStatus.InTransit -> {
println("The package is in transit and expected to arrive by ${status.estimatedDeliveryDate}.")
}
is DeliveryStatus.Delivered -> {
println("The package was delivered to ${status.recipient} on ${status.deliveryDate}.")
}
is DeliveryStatus.Canceled -> {
println("The delivery was canceled due to: ${status.reason}.")
}
}
}
fun main() {
val status1: DeliveryStatus = DeliveryStatus.Pending("Alice")
val status2: DeliveryStatus = DeliveryStatus.InTransit("2024-11-20")
val status3: DeliveryStatus = DeliveryStatus.Delivered("2024-11-18", "Bob")
val status4: DeliveryStatus = DeliveryStatus.Canceled("Address not found")
printDeliveryStatus(status1)
// The package is pending pickup from Alice.
printDeliveryStatus(status2)
// The package is in transit and expected to arrive by 2024-11-20.
printDeliveryStatus(status3)
// The package was delivered to Bob on 2024-11-18.
printDeliveryStatus(status4)
// The delivery was canceled due to: Address not found.
}
练习 2
在你的程序中,你希望能够处理不同状态和类型的错误。你有一个密封类来捕获不同状态,这些状态在数据类或对象中声明。通过创建一个名为 Problem
的枚举类来表示不同问题类型:NETWORK
(网络)、TIMEOUT
(超时)和 UNKNOWN
(未知),从而完善以下代码。
|---|---|
sealed class Status {
data object Loading : Status()
data class Error(val problem: Problem) : Status() {
// Write your code here
}
data class OK(val data: List<String>) : Status()
}
fun handleStatus(status: Status) {
when (status) {
is Status.Loading -> println("Loading...")
is Status.OK -> println("Data received: ${status.data}")
is Status.Error -> when (status.problem) {
Status.Error.Problem.NETWORK -> println("Network issue")
Status.Error.Problem.TIMEOUT -> println("Request timed out")
Status.Error.Problem.UNKNOWN -> println("Unknown error occurred")
}
}
}
fun main() {
val status1: Status = Status.Error(Status.Error.Problem.NETWORK)
val status2: Status = Status.OK(listOf("Data1", "Data2"))
handleStatus(status1)
// Network issue
handleStatus(status2)
// Data received: [Data1, Data2]
}
|---|---|
sealed class Status {
data object Loading : Status()
data class Error(val problem: Problem) : Status() {
enum class Problem {
NETWORK,
TIMEOUT,
UNKNOWN
}
}
data class OK(val data: List<String>) : Status()
}
fun handleStatus(status: Status) {
when (status) {
is Status.Loading -> println("Loading...")
is Status.OK -> println("Data received: ${status.data}")
is Status.Error -> when (status.problem) {
Status.Error.Problem.NETWORK -> println("Network issue")
Status.Error.Problem.TIMEOUT -> println("Request timed out")
Status.Error.Problem.UNKNOWN -> println("Unknown error occurred")
}
}
}
fun main() {
val status1: Status = Status.Error(Status.Error.Problem.NETWORK)
val status2: Status = Status.OK(listOf("Data1", "Data2"))
handleStatus(status1)
// Network issue
handleStatus(status2)
// Data received: [Data1, Data2]
}