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 关键字:

1
sealed class Mammal

密封类与 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
enum class State

假设你想创建一个包含进程各种状态的枚举类。每个枚举常量之间必须用逗号 , 分隔:

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]
}

示例解答

另请参阅

上一步

下一步