Kotlin基础知识_06-变量延迟初始化_密封类

Kotlin基础知识_06-变量延迟初始化&密封类

1. 使用 lateinit 关键字对变量进行延迟初始化

之前有介绍过, kotlin 不能很好的判断全局变量是否为null的情形,比如下面的code:

private var name: String? = null

fun updateName(newName: String) {
    name = newName
}

fun main() {
    updateName("ZhangSan")
    println("The name length: ${name.length}")
}

即便我们确认会在打印 name的长度前调用 updateName("ZhangSan")方法给全局变量 name赋值,但是编译器还是会报错:

image-20231102163957968

它希望我们能够使用判空操作 ?.来获取长度,即:

println("The name length: ${name?.length}")

这样其实很繁琐,因为有些时候,有些变量在定义的时候就是不想给它初始化,初始化会放到某个阶段去做,比如在Android Activity#onCreate()方法执行findViewById()操作,按照Kotlin的语法,这个时候要么给 findViewById()的控件声明为null, 要么就是给它指定一个默认值,声明为null太麻烦,而且后面使用的时候还是需要用到 ?.操作符去判断;而给控件给定一个默认值更不合理,因为界面都还没有onCreate哪来的初始值。

kotlin 允许给变量添加一个 lateinit关键字来做延迟初始化:

ex:

private lateinit var name: String

fun updateName(newName: String) {
    name = newName
}

fun main() {
    updateName("ZhangSan")
    println("The name length: ${name.length}")
}

运行:

The name length: 8

可以看到,这样就不需要在使用 name.length的时候再使用判空操作符了。

不过要注意,既然已告诉编译器这个变量会延迟初始化,那就一定要做初始化,比如把 updateName("ZhangSan") 这行代码去掉,那代码还是会崩溃。

private lateinit var name: String

fun updateName(newName: String) {
    name = newName
}

fun main() {
//    updateName("ZhangSan")
    println("The name length: ${name.length}")
}

运行,编译器直接报错:

image-20231102165011475

当然,也可以通过代码来判断一个全局变量是否已经完成了初始化,这样在某些时候能够有效地避免重复对某一个变量进行初始化操作:

ex:

private lateinit var name: String

fun updateName(newName: String) {
    name = newName
}

fun main() {
    // updateName("ZhangSan")
    if (!::name.isInitialized) {
        println("Tips: name has not init")
        name = "ZhangSan"
    } else {
        println("name has init")
    }
    println("The name length: ${name.length}")
}

运行:

Tips: name has not init
The name length: 8

具体语法就是这样,::name.isInitialized, 我们对结果取反,判断该变量是否已进行过初始化。

2. 密封类 sealed class

有时候会有这样一个场景:经常需要对输入的类型做判断,然后做具体处理,

比如我的视频播放器支持多种编解码器(FFmpegCodec, MediaCodec, GStreamerCodec):

    interface Codec {}

    class FFmpegCodec implements Codec {}

    class MediaCodec implements Codec {}

    class GStreamerCodec implements Codec {}

当用户播放视频时,他可能会选择切换其中一种编解码器来播放视频,这个时候就可能会走到类似于下面这样的代码块里面:

    /**
     * 根据输入的解码器来解码视频
     *
     * @param codec 输入的编码器类型
     * @return 解码的结果, 0代表成功,其它值代表失败
     */
    public static int decodeVideo(Codec codec) {
        if (codec instanceof FFmpegCodec) {
            System.out.println("Codec is FFmpegCodec");
            return 0;
        } else if (codec instanceof MediaCodec) {
            System.out.println("Codec is MediaCodec");
            return 0;
        } else if (codec instanceof GStreamerCodec) {
            System.out.println("Codec is GStreamerCodec");
            return 0;
        } else {
            System.out.println("Unsupport codec, decode fail");
            return -1;
        }
    }

我们会对已支持的编码器做判断,然后进行解码,不支持的编码器就直接返回 -1;

这本来没有问题,只是这个else语句可能永远都走不进去,因为界面上可能只有三个选项供用户选择:

  • FFmpegCodec
  • MediaCodec
  • GStreamerCodec

所以无论如何都不可能选到 else这种情况,写else只是因为 if语句块的要求,必须要有else这种代码块,否则少了else代码块的返回值,编译器报错。

这样看,这个else代码块其实是多余的。

另外,这样写还会有一个隐含的问题,比如我现在新支持了一种编码器:HardwareCodec:

    interface Codec {}

    class FFmpegCodec implements Codec {}

    class MediaCodec implements Codec {}

    class GStreamerCodec implements Codec {}

	class HardwareCodec implements Codec {}

刚好代码里做编码器类型判断的地方有很多处,其中有一处并没有判断到 HardwareCodec这种case, 这样就可能导致代码有异常情况发生,本来预期要对 HardwareCodec这种类型做处理,结果稀里糊涂走到之前定义的else代码块里,然后程序就报错了。

kotlin 提供了一个名为密封类 sealed class 的语法结构来优化此问题。

新建一个名为 Codec.kt的kotlin file, 在其中添加如下code:

sealed class Codec

class FFmpegCodec : Codec()
class MediaCodec : Codec()
class GStreamerCodec : Codec()

然后在另一个kotlin file 中编写测试code:

/**
 * 根据输入的解码器来解码视频
 *
 * @param codec 输入的编码器类型
 * @return 解码的结果, 0代表成功,其它值代表失败
 */
fun decodeVideo(codec: Codec): Int {
    when (codec) {
        is FFmpegCodec -> {
            println("Codec is FFmpegCodec")
            return 0
        }

        is MediaCodec -> {
            println("Codec is MediaCodec")
            return 0
        }

        is GStreamerCodec -> {
            println("Codec is GStreamerCodec")
            return 0
        }
    }
}

有没有发现,不用写 else 语句块了,因为我们将 Codec声明为密封类后,只让 FFmpegCodecMediaCodecGStreamerCodec 继承了它,等于告诉了kotlin编译器,我这个密封类,只会有以上三个子类,不会再有其它类型了,所以自然也不着写 else语句块了。

另外,如果你新加了一个类型 HardwareCodec

sealed class Codec

class FFmpegCodec : Codec()
class MediaCodec : Codec()
class GStreamerCodec : Codec()
class HardwareCodec: Codec()

如果在代码里有某个判断Codec类型的地方,你忘记添加 HardwareCodec类型了,编译器就直接报错了:

image-20231103163044351

提示你when表达式还有条件未做判断,必须添加 HardwareCodec 条件判断或 else 条件判断才可以,这样你就不用担心代码里有某个地方你忘记添加了。

这里我们添加 HardwareCodec 条件判断,编译器就可以正常通过编译了。

/**
 * 根据输入的解码器来解码视频
 *
 * @param codec 输入的编码器类型
 * @return 解码的结果, 0代表成功,其它值代表失败
 */
fun decodeVideo(codec: Codec): Int {
    when (codec) {
        is FFmpegCodec -> {
            println("Codec is FFmpegCodec")
            return 0
        }

        is MediaCodec -> {
            println("Codec is MediaCodec")
            return 0
        }

        is GStreamerCodec -> {
            println("Codec is GStreamerCodec")
            return 0
        }

        is HardwareCodec -> {
            println("Codec is HardwareCodec")
            return 0
        }
    }
}

密封类一般用来表示受限的类继承结构,当一个值为有限的几种类型, 而不能有任何其他类型时用密封类就会很有用。

这里需要注意,其实FFmpegCodec, MediaCodecGStreamerCodec, 这三个类你不写在同一个文件中也是可以的,早期的kotlin 版本是不支持的,目前我测试的kotlin 版本(1.9.0)是支持把密封类的子类写到其它文件当中去的,事实上将三个类的代码写到同一个文件中也显得比较臃肿。

<完>

posted @ 2023-11-03 16:47  夜行过客  阅读(72)  评论(0编辑  收藏  举报