在 Kotlin 中使用类和对象

1. 前言

在本 Codelab 中,您将学习如何在 Kotlin 中使用类和对象。

类提供了用于构造对象的蓝图。对象是类的实例,其中包含相应对象的专属数据。对象和类实例可互换使用。

打个比方,假设您要建造一栋房子。“类”就好比建筑师的设计方案(也称为蓝图)。蓝图不是真正的房子,而是关于如何建造房子的说明。房子是根据蓝图建造的实际事物或物体。

就像房屋蓝图规划了多个房间,而每个房间都有自己的设计和用途一样,每个类也都有着各自的设计和用途。若要了解如何设计类,您必须熟悉面向对象的编程 (OOP),通过该框架学习如何将数据、逻辑和行为封装到对象中。

OOP 可帮助您将复杂的实际问题简化为较小的对象。OOP 涵盖以下四个基本概念,您会在本 Codelab 后面的部分中详细了解各个概念:

  • 封装:将相关属性和针对这些属性执行操作的方法封装在类中。以手机为例,它封装了摄像头、显示屏、存储卡以及其他一些硬件和软件组件。您不必担心这些组件的内部连接方式。
  • 抽象:封装的扩展,其目的是尽可能隐藏内部实现逻辑。例如,如果您要使用手机拍照,只需打开相机应用,将手机对准要拍摄的场景,然后点击按钮即可。您不需要了解相机应用的构建方式或手机上相机硬件的实际运作方式。简而言之,相机应用的内部机制以及移动设备相机的拍照方式已经过抽象,可让您专心执行重要的任务。
  • 继承:可让您通过建立父子关系来基于其他类的特性和行为构建类。例如,不同的制造商生产各种运行 Android OS 的移动设备,但每种设备的界面都不同。换言之,制造商会继承 Android OS 的功能,并在这个基础上构建自己的自定义功能。
  • 多态性:Polymorphism(多态性)这个单词是希腊语词根“poly-”(意为许多)和“morphism”(意为形态)的合成词。多态性是指以单一、通用的方式使用不同对象的能力。例如,当您将蓝牙音箱连接到手机后,手机只需要知道目前有设备可通过蓝牙播放音频。虽然可供您选择的蓝牙音箱有很多种,但手机不必知道各个音箱的具体使用方式。

最后,您将了解属性委托,它们提供的可重复使用的代码能让您用简洁的语法来管理属性值。在本 Codelab 中,您将构建智能家居应用的类结构,并在构建过程中学习这些概念。

前提条件

  • 了解如何在 Kotlin 园地中打开、修改和运行代码。
  • 了解 Kotlin 编程基础知识,包括变量、函数以及 println() 和 main() 函数

学习内容

  • OOP 概览。
  • 什么是类?
  • 如何使用构造函数、函数和属性定义类?
  • 如何实例化对象?
  • 什么是继承?
  • IS-A 关系和 HAS-A 关系之间的差异。
  • 如何替换属性和函数?
  • 什么是可见性修饰符?
  • 什么是委托以及如何使用 by 委托?

构建内容

  • 智能家居应用的类结构。
  • 代表智能设备(如智能电视和智能灯)的类。

所需条件

  • 可连接到互联网的计算机和网络浏览器

2. 定义类

定义类时,您需要指定该类的所有对象都应具有的属性和方法。

类定义以 class 关键字开头,后面依次跟名称和一对大括号。左大括号之前的语法部分也称为类标头。在大括号之间,您可以指定类的属性和函数。您很快就会学到属性和函数。类定义的语法如以下示意图所示:

该语法是以类关键字开头,后跟名称和一对左/右大括号。大括号之间包含用于描述蓝图的类主体。

以下是建议遵循的类命名惯例:

  • 可以选择任何想要的类名称,但不要将 Kotlin 关键字用作类名称,例如 fun 关键字。
  • 类名称采用 PascalCase 大小写形式编写,因此每个单词都以大写字母开头,且各个单词之间没有空格。以“SmartDevice”为例,每个单词的第一个字母都大写,且单词之间没有空格。

类由以下三大部分组成:

  • 属性:用于指定类对象属性的变量。
  • 方法:包含类的行为和操作的函数。
  • 构造函数:一种特殊的成员函数,用于在定义类的整个程序中创建类的实例。

这不是您第一次使用类。在之前的 Codelab 中,您已经了解 Int、Float、String 和 Double 等数据类型。在 Kotlin 中,这些数据类型被定义为类。在定义如以下代码段所示的变量时,您将创建 Int 类的对象(该类使用值 1 进行实例化):

val number: Int = 1

定义 SmartDevice 类:

  1. 在 Kotlin 园地中,将内容替换为空的 main() 函数:
fun main() {
}
  1. 在 main() 函数前面的代码行上,定义一个主体包含 // empty body 注释的 SmartDevice 类:
class SmartDevice {
    // empty body
}

fun main() {
}

3. 创建类的实例

如您之前所学,类是对象的蓝图。Kotlin 运行时使用类(即蓝图)来创建该特定类型的对象。有了 SmartDevice 类,您就有了表示智能设备的蓝图。为了在程序中呈现出一个真实的智能设备,您需要创建一个 SmartDevice 对象实例。实例化语法是以类名称开头,后跟一对圆括号,如下图所示:

1d25bc4f71c31fc9.png

若要使用某个对象,您需要创建该对象,并将其赋给变量,方法与定义变量的方式类似。您可以使用 val 关键字来创建不可变变量,使用 var 关键字来创建可变变量。val 或 var 关键字后依次跟变量名称、= 赋值运算符和类对象的实例化。语法如下图所示:

f58430542f2081a9.png

将 SmartDevice 类实例化为对象:

  • 在 main() 函数中,使用 val 关键字创建名为 smartTvDevice 的变量,并将其初始化为 SmartDevice 类的实例:
fun main() {
    val smartTvDevice = SmartDevice()
}

4. 定义类方法

在第 1 单元中,您学习了:

  • 函数的定义会使用 fun 关键字,后跟一对圆括号和一对大括号。大括号包含的代码会提供执行任务时所需的指令。
  • 调用函数时,系统会执行该函数中包含的代码。

类可以执行的操作在类中被定义为函数。例如,假设您拥有智能设备、智能电视或智能灯,这些设备都可通过手机来开启和关闭。在编程过程中,智能设备对应于 SmartDevice 类,并且可以用 turnOn() 和 turnOff() 函数表示开关该设备的操作(分别用于实现开启和关闭行为)。

在类中定义函数的语法与您之前学习的语法相同。唯一的区别在于,该函数是放在类主体中。在类主体中定义函数时,该函数称为成员函数或方法,用于表示类的行为。在本 Codelab 的剩余部分中,出现在类主体内的函数一律称为方法。

在 SmartDevice 类中定义 turnOn() 和 turnOff() 方法:

  1. 在 SmartDevice 类的主体中,定义主体为空的 turnOn() 方法:
class SmartDevice {
    fun turnOn() {

    }
}
  1. 在 turnOn() 方法的主体中,添加 println() 语句,然后向其传递 "Smart device is turned on." 字符串:
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }
}
  1. 在 turnOn() 方法之后,添加用于输出 "Smart device is turned off." 字符串的 turnOff() 方法:
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

对对象调用方法

到目前为止,您定义了一个类来作为智能设备的蓝图、创建了该类的实例,并将该实例赋给了变量。现在,您可以使用 SmartDevice 类的方法来开启和关闭设备。

如需调用类中的方法,做法与您在之前的 Codelab 中从 main() 函数调用其他函数的方式类似。例如,如果您需要从 turnOn() 方法调用 turnOff() 方法,可以编写与以下代码段类似的内容:

class SmartDevice {
    fun turnOn() {
        // A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
        turnOff()
        ...
    }

    ...
}

若要在类的外部调用类方法,请以类对象开头,后面依次跟 . 运算符、函数名称和一对圆括号。可视情况在圆括号中包含方法所需的实参。语法如下图所示:

fc609c15952551ce.png

对该对象调用 turnOn() 和 turnOff() 方法:

  1. 在 main() 函数中 smartTvDevice 变量后面的代码行上,调用 turnOn() 方法:
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
}
  1. 在 turnOn() 方法后面的代码行上,调用 turnOff() 方法:
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. 运行代码。

输出如下所示:

Smart device is turned on.
Smart device is turned off.

5. 定义类属性

在第 1 单元中,您学习了变量,了解到变量是单个数据的容器。此外,您还学习了如何使用 val 关键字创建只读变量,以及如何使用 var 关键字创建可变变量。

方法用于定义类可以执行的操作,而属性用于定义类的特性或数据属性。例如,智能设备具有以下属性:

  • 名称:设备的名称。
  • 类别:智能设备的类型,例如娱乐、公共事业或烹饪。
  • 设备状态:设备是处于开启、关闭、在线还是离线状态。当设备连接到互联网时,会被视为处于在线状态;否则,就会被视为处于离线状态。

从根本上来讲,属性是在类主体(而非函数主体)中定义的变量。也就是说,用于定义属性和变量的语法都相同。您可以使用 val 关键字定义不可变属性,使用 var 关键字定义可变属性。

实现上述特性作为 SmartDevice 类的属性:

  1. 在 turnOn() 方法前面的代码行上,定义 name 属性并为其赋予 "Android TV" 字符串:
class SmartDevice {

    val name = "Android TV"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. 在 name 属性后面的代码行上,定义 category 属性并为其赋予 "Entertainment" 字符串,然后定义 deviceStatus 属性并为其赋予 "online" 字符串:
class SmartDevice {

    val name = "Android TV"
    val category = "Entertainment"
    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. 在 smartTvDevice 变量后面的代码行上,调用 println() 函数,然后向其传递 "Device name is: ${smartTvDevice.name}" 字符串:
fun main() {
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. 运行代码。

输出如下所示:

Device name is: Android TV
Smart device is turned on.
Smart device is turned off.

属性中的 getter 和 setter 函数

属性的用途比变量更广泛。例如,假设您创建了一个类结构来表示智能电视。您会执行的常见操作之一是调高和调低音量。如需在编程中表示此操作,您可以创建一个名为 speakerVolume 的属性,其中包含电视音箱当前设置的音量,但音量值有范围限制。可设置的音量下限为 0,上限为 100。若要确保 speakerVolume 属性始终不超过 100 或低于 0,您可以编写 setter 函数。在更新属性值时,您需要检查该值是否处于 0 到 100 的范围内。再举一例,假设您必须确保名称始终大写。您可以实现 getter 函数,将 name 属性转换为大写。

在深入了解如何实现这些属性之前,您需要了解用于声明这些属性的完整语法。定义可变属性的完整语法是以变量定义开头,后跟可选的 get() 和 set() 函数。语法如下图所示:

f2cf50a63485599f.png

如果您没有为属性定义 getter 和 setter 函数,Kotlin 编译器会在内部创建这些函数。例如,如果您使用 var 关键字来定义 speakerVolume 属性并为其赋予值 2,编译器会自动生成 getter 和 setter 函数,如以下代码段所示:

var speakerVolume = 2
    get() = field  
    set(value) {
        field = value    
    }

您不会看到这几行代码,因为它们是由编译器在后台添加的。

immutable不可变属性的完整语法有以下两处差异:

  • 以 val 关键字开头。
  • val 类型的变量为只读变量,因此不含 set() 函数。

Kotlin 属性使用后备字段在内存中存储值。从根本上来讲,后备字段是在属性内部定义的类变量。后备字段的作用域限定为相应属性,这意味着您只能通过 get() 或 set() 属性函数访问该字段。

如果想读取 get() 函数中的属性值或更新 set() 函数中的值,您需要使用对应属性的后备字段。该字段是由 Kotlin 编译器自动生成,并通过 field 标识符来引用。

例如,如果您要更新 set() 函数中的属性值,可使用 set() 函数的形参(称为 value 形参),并将其赋给 field 变量,如以下代码段所示:

var speakerVolume = 2
    set(value) {
        field = value    
    }

例如,若要确保赋给 speakerVolume 属性的值介于 0 到 100 之间,您可以实现 setter 函数,如以下代码段所示:

var speakerVolume = 2
    set(value) {
        if (value in 0..100) {
            field = value
        }
    }

您可以在 set() 函数中使用 in 关键字,并在后面加上值的范围,以检查 Int 值是否处于 0 到 100 的范围内。如果该值在预期范围内,系统便会更新 field 值;否则,该属性的值保持不变。

在本 Codelab 的“实现类之间的关系”部分中,您会在类中包含这个属性,因此现在无需在代码中添加 setter 函数。

6. 定义构造函数

构造函数的主要用途是指定类对象的创建方式。换言之,构造函数用于初始化对象,使其可供使用。您在实例化对象时就已执行此操作。在实例化类的对象时,系统会执行构造函数中的代码。您可以定义包含形参或不含形参的构造函数。

默认构造函数

默认构造函数不含形参。定义默认构造函数的做法如以下代码段所示:

class SmartDevice constructor() {
    ...
}

Kotlin 旨在简化代码,因此,如果构造函数中没有任何注解或可见性修饰符(您将在稍后学习这部分内容),您可以移除 constructor 关键字。如果构造函数中没有任何形参,您还可以移除圆括号,如以下代码段所示:

class SmartDevice {
    ...
}

Kotlin 编译器会自动生成默认构造函数。您不会在自己的代码中看到自动生成的默认构造函数,因为编译器会在后台进行添加。

定义形参化构造函数

在 SmartDevice 类中,name 和 category 属性不可变。您需要确保 SmartDevice 类的所有实例都会初始化 name 和 category 属性。在当前实现中,name 和 category 属性的值都采用硬编码。也就是说,所有智能设备都是以 "Android TV" 字符串命名,并使用 "Entertainment" 字符串进行分类。

若要保持不变性,同时避免使用硬编码值,请使用形参化构造函数进行初始化:

  • 在 SmartDevice 类中,将 name 和 category 属性移至构造函数中,且不赋予默认值:
class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

现在,该构造函数可接受形参来设置其属性,因此,为此类实例化对象的方式也会随之更改。实例化对象的完整语法如下图所示:

bbe674861ec370b6.png

代码表示形式如下:

SmartDevice("Android TV", "Entertainment")

构造函数的这两个实参都是字符串,因此我们不清楚应该为哪个形参赋值。解决此问题的做法与传递函数实参的方式类似,只需创建包含具名实参的构造函数即可,如以下代码段所示:

SmartDevice(name = "Android TV", category = "Entertainment")

Kotlin 中的构造函数主要有两类:

  • 主要构造函数:一个类只能有一个主要构造函数(在类标头中定义)。主要构造函数可以是默认构造函数,也可以是形参化构造函数。主要构造函数没有主体,表示其中不能包含任何代码。
  • 辅助构造函数:一个类可以有多个辅助构造函数。您可以定义包含形参或不含形参的辅助构造函数。辅助构造函数可以初始化类,具有包含初始化逻辑的主体。如果类有主要构造函数,则每个辅助构造函数都需要初始化该主要构造函数。

您可以使用主要构造函数来初始化类标头中的属性。传递给构造函数的实参会赋给属性。定义主要构造函数的语法是以类名称开头,后面依次跟 constructor 关键字和一对圆括号。圆括号中包含主要构造函数的形参。如果有多个形参,请用英文逗号分隔形参定义。定义主要构造函数的完整语法如下图所示:

aa05214860533041.png

辅助构造函数包含在类的主体中,其语法包括以下三个部分:

  • 辅助构造函数声明:辅助构造函数定义以 constructor 关键字开头,后跟圆括号。可视情况在圆括号中包含辅助构造函数所需的形参。
  • 主要构造函数初始化:初始化以冒号开头,后面依次跟 this 关键字和一对圆括号。可视情况在圆括号中包含主要构造函数所需的形参。
  • 辅助构造函数主体:在主要构造函数的初始化后跟一对大括号,其中包含辅助构造函数的主体。

语法如下图所示:

2dc13ef136009e98.png

例如,假设您想集成由智能设备提供商开发的 API。不过,该 API 会返回 Int 类型的状态代码来指明初始设备状态。如果设备处于离线状态,该 API 会返回 0 值;如果设备处于在线状态,则返回 1 值。对于任何其他整数值,系统会将状态视为“未知”。您可以在 SmartDevice 类中创建辅助构造函数,以将此 statusCode 形参转换为字符串表示形式,如以下代码段所示:

class SmartDevice(val name: String, val category: String) {
    var deviceStatus = "online"

    constructor(name: String, category: String, statusCode: Int) : this(name, category) {
        deviceStatus = when (statusCode) {
            0 -> "offline"
            1 -> "online"
            else -> "unknown"
        }
    }
    ...
}

7. 实现类之间的关系

继承可让您基于其他类的特性和行为构建类。您可以借助这种强大机制编写可重复使用的代码,并在类之间建立关系。

例如,市面上有许多智能设备,如智能电视、智能灯和智能开关。当您以编程语言的形式表示智能设备时,这类设备会有一些通用属性(例如名称、类别和状态)和通用行为(例如能够开启或关闭)。

不过,每种智能设备开启或关闭的方式并不相同。例如,若要开启电视,您可能需要先开启显示屏,然后设置最近一次的已知音量和频道。相比之下,开灯则只需调高或调低亮度即可。

此外,每种智能设备都各自具备其他功能和操作供用户使用。例如,您可以在电视上调节音量和更换频道;在使用灯具时,您可以调节亮度或颜色。

简而言之,所有智能设备都有不同的功能,但也有一些共同的特性。您可以将这些通用特性复制到每个智能设备类中,也可以用继承的方式让代码可重复使用。

为此,您需要创建 SmartDevice 父级类,并定义这些通用属性和行为。然后,您可以创建子级类(例如 SmartTvDevice 和 SmartLightDevice 类)来继承父级类的属性。

用编程的术语来说,SmartTvDevice 和 SmartLightDevice 类会扩展 SmartDevice 父级类。父级类也称为父类,子级类则称为子类。这些类之间的关系如下图所示:

表示类之间继承关系的示意图。

不过,在 Kotlin 中,所有类默认都是最终类,也就是说您无法扩展这些类,因此必须定义类之间的关系。

定义 SmartDevice 父类及其子类之间的关系:

  1. 在 SmartDevice 父类中的 class 关键字前面添加 open 关键字,使其具有扩展性:
open class SmartDevice(val name: String, val category: String) {
    ...
}

open 关键字会告知编译器此类可供扩展,因此其他类现在可对其进行扩展。

就像您迄今为止做的那样,创建子类的语法是从创建类标头开始。在构造函数的右圆括号后面,依次跟空格、冒号、另一个空格、父类名称以及一对圆括号。如有必要,可在圆括号中包含父类构造函数所需的形参。语法如下图所示:

1ac63b66e6b5c224.png

  1. 创建会扩展 SmartDevice 父类的 SmartTvDevice 子类:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}

SmartTvDevice 的 constructor 定义没有指定属性是可变的还是不可变的,这意味着,deviceName 和 deviceCategory 形参只是 constructor 形参,而不是类属性。您无法在类中使用这些形参,只能将其传递给父类构造函数。

  1. 在 SmartTvDevice 子类主体中,添加您在学习 getter 和 setter 函数时创建的 speakerVolume 属性:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. 定义被赋予 1 值的 channelNumber 属性,并包含指定 0..200 范围的 setter 函数:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
}
  1. 定义会调高音量并输出 "Speaker volume increased to $speakerVolume." 字符串的 increaseSpeakerVolume() 方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    } 
}
  1. 添加会增加频道号并输出 "Channel number increased to $channelNumber." 字符串的 nextChannel() 方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
    
    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }
}
  1. 在 SmartTvDevice 子类后面的代码行上,定义会扩展 SmartDevice 父类的 SmartLightDevice 子类:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}
  1. 在 SmartLightDevice 子类主体中,定义被赋予 0 值的 brightnessLevel 属性,并包含指定 0..100 范围的 setter 函数:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. 定义会调高灯具亮度并输出 "Brightness increased to $brightnessLevel." 字符串的 increaseBrightness() 方法:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }
}

类之间的关系

如果使用继承,您就需要在两个类之间建立关系(称为“IS-A 关系”)。如果对象是继承自某个类,也会是该类的实例。在 HAS-A 关系中,对象可以拥有其他类的实例,且实际上不必是该类本身的实例。这些关系的简要表示形式如以下示意图所示:

HAS-A 和 IS-A 关系的简要表示形式。

IS-A 关系

如果在 SmartDevice 父类和 SmartTvDevice 子类之间指定 IS-A 关系,即表示 SmartDevice 父类可以执行的操作,SmartTvDevice 子类也可执行。这种关系是单向的,因此可以说每个智能电视“都是”智能设备,但不能说每个智能设备“都是”智能电视。IS-A 关系的代码表示形式如以下代码段所示:

// Smart TV IS-A smart device.
class SmartTvDevice : SmartDevice() {
}

请不要只为了实现代码的可重用性而使用继承。在做出决定之前,请检查这两个类彼此是否相关。如果两者表现出某种关系,请检查是否确实符合 IS-A 关系的定义。不妨问问自己,“我可以说子类是父类吗?”例如,Android“是”操作系统。

HAS-A 关系

HAS-A 关系是指定两个类之间的关系的另一种方式。例如,您可能要使用住宅中的智能电视。在这种情况下,智能电视和住宅之间存在某种关系。住宅中包含智能设备,即住宅“拥有”智能设备。两个类之间的 HAS-A 关系也称为“组合”。

到目前为止,您已经创建了几个智能设备,现在可以创建包含智能设备的 SmartHome 类了。您可以通过 SmartHome 类与智能设备进行互动。

使用 HAS-A 关系定义 SmartHome 类:

  1. 在 SmartLightDevice 类和 main() 函数之间,定义 SmartHome 类:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

}

class SmartHome {
}

fun main() { 
    ...
}
  1. 在 SmartHome 类构造函数中,使用 val 关键字创建 SmartTvDevice 类型的 smartTvDevice 属性:
// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {

}
  1. 在 SmartHome 类的主体中,定义会对 smartTvDevice 属性调用 turnOn() 方法的 turnOnTv() 方法:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }
}
  1. 在 turnOnTv() 方法之后的代码行上,定义会对 smartTvDevice 属性调用 turnOff() 方法的 turnOffTv() 方法:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

}
  1. 在 turnOffTv() 方法后面的代码行上,定义会对 smartTvDevice 属性调用 increaseSpeakerVolume() 方法的 increaseTvVolume() 方法,然后定义会对 smartTvDevice 属性调用 nextChannel() 方法的 changeTvChannelToNext() 方法:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }
}
  1. 在 SmartHome 类构造函数中,将 smartTvDevice 属性形参移至其专属代码行中,后跟英文逗号:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
) {

    ...

}
  1. 在 smartTvDevice 属性后面的代码行上,使用 val 关键字定义 SmartLightDevice 类型的 smartLightDevice 属性:
// The SmartHome class HAS-A smart TV device and smart light.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

}
  1. 在 SmartHome 主体中,定义会对 smartLightDevice 对象调用 turnOn() 方法的 turnOnLight() 方法,以及会对 smartLightDevice 对象调用 turnOff() 方法的 turnOffLight() 方法:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }
}
  1. 在 turnOffLight() 方法后面的代码行上,定义会对 smartLightDevice 属性调用 increaseBrightness() 方法的 increaseLightBrightness() 方法:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }
}
  1. 在 increaseLightBrightness() 方法后面的代码行上,定义会调用 turnOffTv() 和 turnOffLight() 方法的 turnOffAllDevices() 方法:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

替换子类中的父类方法

如前所述,虽然所有智能设备都支持开启和关闭功能,但它们的执行方式各有不同。如需提供这种特定于设备的行为,您需要替换父类中定义的 turnOn() 和 turnOff() 方法。替换意味着要拦截操作,通常是手动控制。替换方法时,子类中的方法会中断父类中定义的方法的执行,并提供其自有的执行内容。

替换 SmartDevice 类中的 turnOn() 和 turnOff() 方法:

  1. 在 SmartDevice 父类主体中,找到每个方法的 fun 关键字,并在前面添加 open 关键字:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        // function body
    }

    open fun turnOff() {
        // function body
    }
}
  1. 在 SmartLightDevice 类的主体中,定义主体为空的 turnOn() 方法:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
    }
}
  1. 在 turnOn() 方法的主体中,将 deviceStatus 属性设为字符串“on”,将 brightnessLevel 属性设为值“2”并添加 println() 语句,然后向它传递一个 "$name turned on. The brightness level is $brightnessLevel." 字符串:
    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }
  1. 在 SmartLightDevice 类的主体中,定义主体为空的 turnOff() 方法:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
    }
}
  1. 在 turnOff() 方法的主体中,将 deviceStatus 属性设为字符串“off”,将 brightnessLevel 属性设为值“0”并添加 println() 语句,然后向它传递一个 "Smart Light turned off" 字符串:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}
  1. 在 SmartLightDevice 子类中,找到 turnOn() 和 turnOff() 方法的 fun 关键字,并在前面添加 override 关键字:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

override 关键字会告知 Kotlin 运行时去执行子类所定义方法中包含的代码。

  1. 在 SmartTvDevice 类的主体中,定义主体为空的 turnOn() 方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
        
    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
        
    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }
    
    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    fun turnOn() {
    }
}
  1. 在 turnOn() 方法的主体中,将 deviceStatus 属性设为字符串“on”并添加 println() 语句,然后向它传递一个 "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " + "set to $channelNumber." 字符串:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }
}
  1. 在 SmartTvDevice 类的正文中的 turnOn() 方法后面,定义正文为空的 turnOff() 方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
    }
}
  1. 在 turnOff() 方法的主体中,将 deviceStatus 属性设为字符串“off”并添加 println() 语句,然后向它传递一个 "$name turned off" 字符串:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. 在 SmartTvDevice 类中,找到 turnOn() 和 turnOff() 方法的 fun 关键字,并在前面添加 override 关键字:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. 在 main() 函数中,使用 var 关键字定义 SmartDevice 类型的 smartDevice 变量,该变量会实例化接受 "Android TV" 实参和 "Entertainment" 实参的 SmartTvDevice 对象:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
}
  1. 在 smartDevice 变量后面的代码行上,对 smartDevice 对象调用 turnOn() 方法:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
}
  1. 运行代码。

输出如下所示:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
  1. 在调用 turnOn() 方法后面的代码行上,重新赋予 smartDevice 变量以实例化一个接受 "Google Light" 实参和 "Utility" 实参的 SmartLightDevice 类,然后对 smartDevice 对象引用调用 turnOn() 方法:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
    
    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}
  1. 运行代码。

输出如下所示:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

这是一个关于多态性的示例。代码会对 SmartDevice 类型的变量调用 turnOn() 方法,并能根据变量的实际值来执行 turnOn() 方法的不同实现。

使用 super 关键字在子类中重复使用父类代码

如果仔细观察 turnOn() 和 turnOff() 方法,您就会发现,每次在 SmartTvDevice 和 SmartLightDevice 子类中调用方法时,更新 deviceStatus 变量的方式都类似,这是因为有重复的代码。因此,在更新 SmartDevice 类中的状态时,您可以重复使用该代码。

若要从子类调用父类中被替换的方法,您需使用 super 关键字。从父类调用方法与从类外部调用该方法类似。请不要在对象和方法之间使用 . 运算符,而是使用 super 关键字,后者会告知 Kotlin 编译器对父类(而不是子类)调用方法。

从父类调用方法的语法是以 super 关键字开头,后面依次跟 . 运算符、函数名称和一对圆括号。可视情况在圆括号中包含相应实参。语法如下图所示:

18cc94fefe9851e0.png

重复使用 SmartDevice 父类中的代码:

  1. 从 turnOn() 和 turnOff() 方法中移除 println() 语句,并将重复的代码从 SmartTvDevice 和 SmartLightDevice 子类移至 SmartDevice 父类:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}
  1. 在 SmartTvDevice 和 SmartLightDevice 子类中,使用 super 关键字从 SmartDevice 类中调用方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

替换子类中的父类属性

与方法类似,您也可以使用相同的步骤替换属性。

替换 deviceType 属性:

  1. 在 SmartDevice 父类中 deviceStatus 属性后面的代码行上,使用 open 和 val 关键字定义 deviceType 属性,并将其设置为 "unknown" 字符串:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open val deviceType = "unknown"
    ...
}
  1. 在 SmartTvDevice 类中,使用 override 和 val 关键字定义 deviceType 属性,并将其设置为 "Smart TV" 字符串:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    ...
}
  1. 在 SmartLightDevice 类中,使用 override 和 val 关键字定义 deviceType 属性,并将其设置为"Smart Light"字符串:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    ...

}

8. 可见性修饰符

可见性修饰符在实现封装方面发挥着以下重要作用:

  • 可让您在类中隐藏自己的属性和方法,防止在类外未经授权的访问。
  • 可让您在软件包中隐藏类和接口,防止在软件包外未经授权的访问。

Kotlin 提供了以下四种可见性修饰符:

  • public:默认的可见性修饰符。可让系统在任何位置访问声明。对于您想在类外部使用的属性和方法,请标记为 public。
  • private:可让系统在相同类或源文件中访问声明。

某些属性和方法可能仅在类的内部使用,而且您不一定想让其他类使用。您可以使用 private 可见性修饰符标记这些属性和方法,以确保其他类不会意外访问它们。

  • protected:可让系统在子类中访问声明。对于您想在定义它们的类及其子类中使用的属性和方法,请使用 protected 可见性修饰符进行标记。
  • internal:可让系统在相同模块中访问声明。internal 修饰符与 private 类似,但您可以从类的外部访问内部属性和方法,只要是在相同模块中进行访问即可。

定义相应类后,该类便会公开显示,并可供所有导入该类的软件包访问;也就是说,除非您指定可见性修饰符,否则该类默认处于公开状态。同样,在类中定义或声明属性和方法后,默认情况下,您可以通过类对象在类的外部访问它们。请务必为代码定义适当的可见性,这主要是为了隐藏其他类不需要访问的属性和方法。

例如,想一想驾驶员是如何操控汽车的。默认情况下,我们会隐藏汽车各个零部件的具体细节以及汽车的内部运行方式。汽车的设计理念是提供尽可能直观的操控方式。您肯定不希望汽车的操控方式像商用飞机那样复杂,就如同您不希望其他开发者或未来的自己对要使用类的哪些属性和方法感到困惑。

可见性修饰符可协助您将代码的相关部分提供给项目中的其他类使用,并确保实现不会在无意间遭到使用,从而让代码易于理解且不易出错。

在声明类、方法或属性时,应将可见性修饰符放在声明语法之前,如下图所示:

dcc4f6693bf719a9.png

为属性指定可见性修饰符

为属性指定可见性修饰符的语法是以 private、protected 或 internal 修饰符开头,后跟定义属性的语法。语法如下图所示:

47807a890d237744.png

例如,您可以查看以下代码段,了解如何将 deviceStatus 属性设为私有:

open class SmartDevice(val name: String, val category: String) {

    ...

    private var deviceStatus = "online"

    ...
}

您也可以将可见性修饰符设置为 setter 函数,并将修饰符放在 set 关键字之前。语法如下图所示:

cea29a49b7b26786.png

对于 SmartDevice 类,deviceStatus 属性的值应能通过类对象在类的外部进行读取。不过,只有该类及其子类可以更新或写入这个值。若要实现这项要求,您需要对 deviceStatus 属性的 set() 函数使用 protected 修饰符。

对 deviceStatus 属性的 set() 函数使用 protected 修饰符:

  1. 在 SmartDevice 父类的 deviceStatus 属性中,将 protected 修饰符添加到 set() 函数中:
open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set(value) {
           field = value
       }

    ...
}

您不会在 set() 函数中执行任何操作或检查,只需将 value 形参赋给 field 变量即可。如您之前所学,这与属性 setter 的默认实现类似。在本例中,您可以省略 set() 函数的圆括号和主体:

open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set

    ...
}
  1. 在 SmartHome 类中定义 deviceTurnOnCount 属性,将其值设为 0,并包含私有 setter 函数:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    ...
}
  1. 在 turnOnTv() 和 turnOnLight() 方法中添加后跟 ++ 算术运算符的 deviceTurnOnCount 属性,然后在 turnOffTv() 和 turnOffLight() 方法中添加后跟 -- 算术运算符的 deviceTurnOnCount 属性:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }
    
    ...

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    ...

}

为方法指定可见性修饰符

为方法指定可见性修饰符的语法是以 private、protected 或 internal 修饰符开头,后跟定义方法的语法。语法如下图所示:

e0a60ddc26b841de.png

例如,您可以查看以下代码段,了解如何在 SmartTvDevice 类中为 nextChannel() 方法指定 protected 修饰符:

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    protected fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }      

    ...
}

为构造函数指定可见性修饰符

为构造函数指定可见性修饰符的语法与定义主要构造函数的语法类似,但存在以下两点差异:

  • 修饰符是在类名称之后、constructor 关键字之前的位置指定。
  • 为主要构造函数指定修饰符时,即使函数内没有任何形参,也必须保留 constructor 关键字和圆括号。

语法如下图所示:

6832575eba67f059.png

例如,您可以查看以下代码段,了解如何在 SmartDevice 构造函数中添加 protected 修饰符:

open class SmartDevice protected constructor (val name: String, val category: String) {

    ...

}

为类指定可见性修饰符

为类指定可见性修饰符的语法是以 private、protected 或 internal 修饰符开头,后跟定义类的语法。语法如下图所示:

3ab4aa1c94a24a69.png

例如,您可以查看以下代码段,了解如何为 SmartDevice 类指定 internal 修饰符:

internal open class SmartDevice(val name: String, val category: String) {

    ...

}

理想情况下,您应努力严控属性和方法的可见性,因此请尽可能通过 private 修饰符来声明属性和方法。如果您无法确保它们私有,请使用 protected 修饰符;如果您无法确保它们受到保护,请使用 internal 修饰符;如果您无法确保它们仅在内部使用,请使用 public 修饰符。

指定适当的可见性修饰符

您可以参考下表,了解能让类或构造函数的属性或方法可供访问的位置,据此确定合适的可见性修饰符:

修饰符

可在相同类中访问

可在子类中访问

可在相同模块中访问

可在模块之外访问

private

✔

𝗫

𝗫

𝗫

protected

✔

✔

𝗫

𝗫

internal

✔

✔

✔

𝗫

public

✔

✔

✔

✔

在 SmartTvDevice 子类中,不应允许从类的外部控制 speakerVolume 和 channelNumber 属性。这些属性应仅通过 increaseSpeakerVolume() 和 nextChannel() 方法控制。

同样,在 SmartLightDevice 子类中,应仅通过 increaseLightBrightness() 方法来控制 brightnessLevel 属性。

在 SmartTvDevice 和 SmartLightDevice 子类中添加适当的可见性修饰符:

  1. 在 SmartTvDevice 类中,向 speakerVolume 和 channelNumber 属性添加 private 可见性修饰符:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    private var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    private var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    ...
}
  1. 在 SmartLightDevice 类中,向 brightnessLevel 属性添加 private 修饰符:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    private var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    ...
}

9. 定义属性委托

在上一部分中,您已了解 Kotlin 中的属性使用后备字段在内存中存储值。您可以使用 field 标识符来引用该字段。

当您查看到目前为止编写的代码时,可以看看重复的代码,以检查 SmartTvDevice 和 SmartLightDevice 类中 speakerVolume、channelNumber 以及 brightnessLevel 属性的值是否处于指定范围内。您可以在 setter 函数中,通过委托重复使用这段检查范围的代码。委托可代替使用字段、getter 和 setter 函数的方式来管理该值。

创建属性委托的语法是以变量声明开头,后面依次跟 by 关键字以及用于为属性处理 getter 和 setter 函数的委托对象。语法如下图所示:

928547ad52768115.png

若要实现您可以委托实现的目标类,您必须熟悉接口。接口是实现它的类必须遵循的协议,侧重于操作的“内容”,而不是操作的“方式”。简而言之,接口可帮助您实现抽象。

例如,在建造房子之前,您会告知建筑师自己想要什么。这可能包括卧室、儿童房、起居室、厨房和几间浴室。简而言之,在您指定“需求”后,建筑师会指定“满足需求的方式”。创建接口的语法如以下示意图所示:

bfe3fd1cd8c45b2a.png

您已经学习了如何扩展类和替换类的功能。对于接口,类会实现接口。类会针对接口中声明的方法和属性提供实现详情。您将使用 ReadWriteProperty 接口执行类似操作来创建委托。在下一单元中,您将详细了解接口。

如果想为 var 类型创建委托类,您需要实现 ReadWriteProperty 接口。同样,您需要为 val 类型实现 ReadOnlyProperty 接口。

为 var 类型创建委托:

  1. 在 main() 函数之前,创建会实现 ReadWriteProperty<Any?, Int> 接口的 RangeRegulator 类:
class RangeRegulator() : ReadWriteProperty<Any?, Int> {

}

fun main() {
    ...
}

不必担心尖括号以及其中的内容。它们属于通用类型,您会在下一单元中进行了解。

  1. 在 RangeRegulator 类的主要构造函数中,添加 initialValue 形参、私有 minValue 属性和私有 maxValue 属性,并将所有这些都设为 Int 类型:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

}
  1. 在 RangeRegulator 类的主体中,替换 getValue() 和 setValue() 方法:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

这些方法会充当属性的 getter 和 setter 函数。

  1. 在 SmartDevice 类前面的代码行上,导入 ReadWriteProperty 和 KProperty 接口:
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {
    ...
}

...

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

...
  1. 在 RangeRegulator 类中 getValue() 方法前面的代码行上,定义 fieldData 属性,并使用 initialValue 形参对其进行初始化:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

此属性会充当变量的后备字段。

  1. 在 getValue() 方法的主体中,返回 fieldData 属性:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}
  1. 在 setValue() 方法的主体中,检查要赋予的 value 形参是否处于 minValue..maxValue 范围内,然后再将其赋给 fieldData 属性:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}
  1. 在 SmartTvDevice 类中,使用委托类来定义 speakerVolume 和 channelNumber 属性:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    ...

}
  1. 在 SmartLightDevice 类中,使用委托类来定义 brightnessLevel 属性:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    ...

}

10. 测试解决方案

解决方案代码如以下代码段所示:

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"
        protected set

    open val deviceType = "unknown"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}

class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}

fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()

    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}

输出如下所示:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

11. 尝试新的挑战

  • 在 SmartDevice 类中,定义一个可输出 "Device name: $name, category: $category, type: $deviceType" 字符串的 printDeviceInfo() 方法。
  • 在 SmartTvDevice 类中,定义一个可降低音量的 decreaseVolume() 方法以及一个可前往上一个频道的 previousChannel() 方法。
  • 在 SmartLightDevice 类中,定义一个可降低亮度的 decreaseBrightness() 方法。
  • 在 SmartHome 类中,确保仅在每台设备的 deviceStatus 属性都设置为 "on" 字符串时,系统才执行所有操作。此外,请确保 deviceTurnOnCount 属性会正确更新。

完成实现后:

  • 在 SmartHome 类中,定义 decreaseTvVolume()、changeTvChannelToPrevious()、printSmartTvInfo()、printSmartLightInfo() 和 decreaseLightBrightness() 方法。
  • 从 SmartHome 类的 SmartTvDevice 和 SmartLightDevice 类中调用适当的方法。
  • 在 main() 函数中,调用这些添加的方法进行测试。

12. 总结

恭喜!您学习了如何定义类和实例化对象。此外,您还学习了如何在类之间创建关系以及如何创建属性委托。

摘要

  • OOP 有四大原则:封装、抽象、继承和多态性。
  • 类是使用 class 关键字定义的,并包含属性和方法。
  • 属性与变量类似,不同之处在于属性可包含自定义 getter 和 setter。
  • 构造函数用于指定如何实例化类的对象。
  • 在定义主要构造函数时,可以省略 constructor 关键字。
  • 继承可让您更轻松地重复使用代码。
  • IS-A 关系指的是继承。
  • HAS-A 关系指的是组合。
  • 可见性修饰符在实现封装方面发挥着重要作用。
  • Kotlin 提供了四种可见性修饰符:public、private、protected 和 internal 修饰符。
  • 属性委托可让您在多个类中重复使用 getter 和 setter 代码。

了解更多内容