3.3.6 开放类与特殊类
原文链接: https://kotlinlang.org/docs/kotlin-tour-intermediate-open-special-classes.html
3.3.6 开放类与特殊类
本章将介绍开放类、它们与接口如何配合,以及 Kotlin 中其他一些特殊类型的类。
开放类
如果你无法使用接口或抽象类,可以显式地把一个类声明为 open,使其可被继承。为此,请在类声明之前使用 open 关键字:
1
| open class Vehicle(val make: String, val model: String)
|
要创建一个继承自另一个类的类,请在类头之后加一个冒号,后面跟上你想继承的父类构造器的调用。在下面这个示例中,Car 类继承自 Vehicle 类:
1
2
3
4
5
6
7
8
9
10
11
12
| 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。
重写继承来的行为
如果你想从某个类继承,但修改其中一部分行为,可以重写继承来的行为。
默认情况下,无法重写父类的成员函数或属性。与抽象类一样,你需要添加特殊关键字。
成员函数
要让父类中的某个函数可以被重写,请在父类中该函数的声明之前使用 open 关键字:
1
| open fun displayInfo() {}
|
要重写继承来的成员函数,请在子类中该函数的声明之前使用 override 关键字:
1
| override fun displayInfo() {}
|
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| 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 的属性,并想在之后重写它。重写属性的语法与重写成员函数完全相同。你可以这样写:
1
2
3
4
5
6
7
| 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 子类时声明它的值:
1
2
3
| 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")
|
直接访问属性、而不是重写它们,能让代码更简单、更易读。在父类中一次性声明属性,并通过构造器传递它们的值,就可以免去子类中不必要的重写。
关于类继承和重写类行为的更多信息,请参阅继承。
开放类与接口
你可以创建一个既继承某个类、又实现多个接口的类。在这种情况下,你必须在冒号之后先声明父类,然后才列出各个接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // Define interfaces
interface EcoFriendly {
val emissionLevel: String
}
interface ElectricVehicle {
val batteryCapacity: Double
}
// Parent class
open class Vehicle(val make: String, val model: String)
// Child class
open class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model)
// New class that inherits from Car and implements two interfaces
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 还有一些为特定目的设计的特殊类,例如限制某些行为,或减少创建小对象带来的性能开销。
密封类
有时你可能想限制继承。你可以用密封类做到这一点。密封类是一种特殊的抽象类。一旦你把某个类声明为密封类,就只能在同一包内为它创建子类,无法在这个范围之外继承该密封类。
提示: 包是一组相关的类和函数的集合,通常位于同一个目录中。要了解 Kotlin 中包的更多内容,请参阅包与导入。
要创建密封类,请使用 sealed 关键字:
密封类与 when 表达式结合使用时特别有用。通过 when 表达式,你可以为所有可能的子类定义行为。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 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 密封类,并把它自己构造器中的 catName 参数用作 Mammal 类的 name 参数。Human 类继承自 Mammal 密封类,并把它自己构造器中的 humanName 参数用作 Mammal 类的 name 参数。它的构造器中还有 job 参数。greetMammal() 函数接受 Mammal 类型的参数并返回一个字符串。- 在
greetMammal() 函数体中有一个 when 表达式,它使用 is 运算符检查 mammal 的类型,并决定执行哪个动作。 main() 函数用一个 Cat 类实例调用 greetMammal() 函数,该实例的 name 参数是 Snowy。
提示: 本次导览会在空安全一章中更详细地讨论 is 运算符。
关于密封类及其推荐使用场景的更多信息,请参阅密封类与密封接口。
枚举类
当你想在类中表示一组有限且互不相同的取值时,枚举类会很有用。枚举类包含枚举常量,这些常量本身就是该枚举类的实例。
要创建枚举类,请使用 enum 关键字:
假设你想创建一个包含进程各种状态的枚举类。每个枚举常量之间必须用逗号 , 分隔:
1
2
3
| enum class State {
IDLE, RUNNING, FINISHED
}
|
State 枚举类有枚举常量:IDLE、RUNNING 和 FINISHED。要访问枚举常量,请使用类名,后面跟一个 . 和枚举常量的名称:
1
| val state = State.RUNNING
|
你可以把这个枚举类与 when 表达式结合使用,根据枚举常量的值定义要执行的动作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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 值。创建枚举常量时,你必须用这个属性来初始化它:
1
2
3
4
5
6
| enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF),
YELLOW(0xFFFF00)
}
|
注意: Kotlin 把十六进制数以整数存储,因此 rgb 属性的类型是 Int 而不是 String。
要给这个类添加成员函数,请用分号 ; 把它与枚举常量分隔开:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| 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
// Calls containsRed() function on enum constant
println(red.containsRed())
// true
// Calls containsRed() function on enum constants via class names
println(Color.BLUE.containsRed())
// false
println(Color.YELLOW.containsRed())
// true
}
|
在这个示例中,containsRed() 成员函数用 this 关键字访问枚举常量的 rgb 属性值,并检查该十六进制值的前几位是否包含 FF,从而返回一个布尔值。
更多信息请参阅枚举类。
内联值类
有时在代码中,你可能想从类创建一些小对象,并且只是短暂地使用它们。这种做法可能带来性能开销。内联值类是一种特殊类型的类,可以避免这种性能开销。不过,它们只能包含值。
要创建内联值类,请使用 value 关键字和 @JvmInline 注解:
1
2
| @JvmInline
value class Email
|
提示: @JvmInline 注解指示 Kotlin 在编译时优化代码。要了解更多内容,请参阅注解。
内联值类必须在类头中初始化一个属性。
假设你想创建一个用来承载邮箱地址的类:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // The address property is initialized in the class header.
@JvmInline
value class Email(val address: String)
fun sendEmail(email: Email) {
println("Sending email to ${email.address}")
}
fun main() {
val myEmail = Email("example@example.com")
sendEmail(myEmail)
// Sending email to example@example.com
}
|
在这个示例中:
Email 是一个内联值类,它的类头中只有一个属性:address。sendEmail() 函数接受 Email 类型的对象,并向标准输出打印一个字符串。main() 函数:- 创建一个名为
myEmail 的 Email 类实例。 - 在
myEmail 对象上调用 sendEmail() 函数。
通过使用内联值类,你让这个类被内联,可以在代码中直接使用它而不必创建对象。这能显著减少内存占用并提升代码的运行时性能。
关于内联值类的更多信息,请参阅内联值类。
练习
习题 1
你经营一家快递服务,需要一种跟踪包裹状态的方式。请创建一个名为 DeliveryStatus 的密封类,其中包含用于表示以下状态的数据类:Pending、InTransit、Delivered、Canceled。补全 DeliveryStatus 类的声明,使 main() 函数中的代码能够正常运行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| 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.
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| 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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| 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]
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| 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]
}
|
示例解答
另请参阅
上一步
下一步