zoukankan      html  css  js  c++  java
  • Kotlin 朱涛 思维4 空安全思维 平台类型 非空断言

    本文地址


    目录

    Kotlin 空安全思维

    Java 的空安全思维

    Java 中规避 NPE 的几个方案:

    • 判空:这是防御式编程的一种体现,应用范围也很广泛
    • 注解:借助 @Nullable@NotNull 之类的注解,IDE 可以帮我们规避 NPE
    • 封装数据:例如 1.8 中引入的 Optional,这种手段的核心思路就是封装数据,不再直接使用 null

    Java 解决 NPE 的三种思路都无法令人满意。

    其实,我们需要的是一种简洁,且能为每一种类型都标明可空性的方式。这样一来,我们自然而然就能想到一个更好的方案,那就是:从类型系统下手。

    Kotlin 的空安全思维

    Kotlin 的类型系统与 Java 有很大的不同。在 Kotlin 当中,同样是字符串类型,却有三种表示方法。

    • String:不可为空的字符串
    • String?:可能为空的字符串
    • String!:不知道是不是可能为空的字符串 -- 平台类型

    Kotlin 这样的类型系统,让开发者必须明确规定每一个变量类型是否可能为空,通过这样的方式,Kotlin 编译器就能帮我们规避 NPE 了。

    在不与 Kotlin 以外的环境进行交互的情况下,仅仅只是纯 Kotlin 开发当中,Kotlin 编译器已经可以帮我们消灭 NPE 了。不过,与 Java 等其他语言环境打交道时,问题就比较复杂了。

    Kotlin 中的平台类型

    Java 中不存在可空类型这个概念,因此,在 Kotlin 中,我们把 Java 中的所有未知可空性的类型,都看做是平台类型。平台类型用 ! 来表示,比如String!

    class Test {
    	@Nullable
    	public static String getNullableString(@Nullable String s) {
    		return s + "Nullable"; // 使用 @Nullable 修饰,代表参数和返回值可能为空
    	}
    
    	@NotNull
    	public static String getNotNullString(@NotNull String s) {
    		return s + "NotNull"; // 使用 @NotNull 修饰,代表参数和返回值不可能为空
    	}
    
    	public static String getMsg(String s) {
    		return s + "Kotlin";  // 直接返回了一个字符串,但是没有用可空注解标注
    	}
    }
    

    由于 Java 中的 getMsg() 方法没有任何可空注解,因此,它在 Kotlin 中会被认为是平台类型 String!

    对于平台类型,Kotlin 会认为,它 既能被当作可空类型(例如 String?),也可以被当作不可空类型(例如 String)。

    以上方法在 Kotlin 调用的时候,就出现以下几种情况:

    fun main() {
        val s1: String? = Test.getNullableString(null) // 可传 null,返回值为可空类型
        val s2: String = Test.getNotNullString("Hey,") // 不可传 null,返回值为不可空类型
    
        val s3: String? = Test.getMsg(null) // 返回值可以为【可空类型】,也可以为【不可空类型】
        val s4: String = Test.getMsg("bqt") // 返回值可以为【可空类型】,也可以为【不可空类型】
    }
    

    Kotlin 空安全的第一条准则:警惕 Kotlin 以外的数据类型

    • 从语言角度上看:Kotlin 不仅会和 Java 交互,还可以与其他语言交互,如果其他语言没有可空的类型系统,那么我们就一定要警惕起来
    • 从环境角度上看:Kotlin 可以与其他外界环境交互,这些外界的环境中的数据,往往也是没有可空类型系统的,这时候我们也要警惕

    Kotlin 中的非空断言

    Kotlin 空安全的第二条准则:绝不使用非空断言

    • Kotlin 空安全调用语法:?.
    • Kotlin 非空安全的调用语法:!!.,这样的语法也叫做非空断言

    下面代码中,如果使用非空断言,强行调用可为空的 String? 类型的成员,就会产生空指针异常。

    fun testNPE(msg: String?) {
        println(msg?.length)  // 空安全调用语法,打印 null
        println(msg!!.length) // 非空断言,非空安全的调用语法,报 NPE
    }
    
    fun main() {
        testNPE(null)
    }
    

    在 Kotlin 代码中,我们应坚持 绝不使用非空断言非空断言代码主要在两种情况下会被引入:

    • 使用IDE 的 Convert Java File To Kotlin File 功能时,工具会自动生成带有非空断言的代码
    • 某些场景下,Smart Cast 会失效,导致我们即使判空了,也免不了是要继续使用非空断言的代码

    IDE 的代码转换功能

    当我们借助 IDE 的 Convert Java File To Kotlin File 时,这个工具可能会自动帮我们生成带有非空断言的代码。

    public class JavaConvertExample {
    	private String name = null;
    	void init() {
    		name = "";
    	}
    	void test() {
    		if (name != null) {
    			System.out.println(name.length());
    		}
    	}
    }
    

    上述 Java 代码通过 IDE 的转换成 Kotlin 代码后为:

    class JavaConvertExample {
        private var name: String? = null
        fun init() {
            name = ""
        }
    
        fun test() {
            if (name != null) {
                println(name!!.length) // 非空断言
            }
        }
    }
    

    可以看到,转成 Kotlin 代码以后,test() 方法中出现了非空断言。如果我们在转换完代码以后,没有 review,尽可能将非空断言带到生产环境中。

    Smart Cast 失效原因

    Kotlin 是支持 Smart Cast 的,如果我们已经在 if 中判断了 name 不等于空,那么,就可以被转换成非空类型了,例如:

    import kotlin.random.Random
    
    fun getName() = if (Random.nextBoolean()) null else "bqt"
    
    fun main() {
        val name: String? = getName() // name 是局部变量,至于是 var 还是 val 都可以
        if (name != null) {
            val tem: String = name    // 判断非空后,就可以被转换成【非空类型】了
            println(name.length)
        }
    }
    

    但是,上面 IDE 自动转换的 Kotlin 代码中,如果我们将转换出来的非空断言语法删除掉,IDE 就报错了:

    Smart cast to 'String' is impossible 不可能发生的, because 'name' is a mutable property that could have been changed by this time

    我们将代码简化一下:

    import kotlin.random.Random
    
    var city: String? = if (Random.nextBoolean()) null else "sz" // city 是全局变量(或成员属性)
    fun main() {
        if (city != null) {
            println(city.length) // 编译报错
        }
    }
    

    可以发现,两者的核心区别是,上面的 name 是局部变量,而下面的 city 是可变的全局变量。这就导致,即使前一行代码中 city 已经判空了,后一行代码运行时,city 也可能已经被改变了(比如多线程)。所以此时没办法使用 Smart Cast

    如何避免使用非空断言

    Kotlin 空安全的第三条准则:尽可能使用非空类型

    借助 lateinit、懒加载,我们可以做到灵活初始化的同时,还能消灭可空类型。

    ① 改为函数传参的形式

    fun test(name: String?) {    // 改为函数参数
        if (name != null) {
            println(name.length) // 函数参数支持 Smart Cast
        }
    }
    

    函数的参数是不可变的,因此,当我们将外部的成员变量或者全局变量,以函数参数的形式传进来后,就可以用于 Smart Cast 了。

    ② 改为使用不可变变量

    class JavaConvertExample {
        private val name: String? = null // 将可变变量 var 改为不可变变量 val
        fun test() {
            if (name != null) {
                println(name.length)     // 不可变变量支持 Smart Cast
            }
        }
    }
    

    ③ 借助临时的不可变变量

    class JavaConvertExample {
        private var name: String? = null
        fun test() {
            val _name = name          // 定义一个临时的【不可变变量】
            if (_name != null) {
                println(_name.length) // 使用临时变量的【不可变变量】
            }
        }
    }
    

    ④ 借助标准函数 let

    class JavaConvertExample {
        private var name: String? = null
        fun test() {
            name?.let { println(it.length) } // 使用标准函数 let
        }
    }
    

    这种方式和第三种方式,从本质上来讲是相似的,但是使用 let 的实现更加优雅。

    ⑤ 借助 lateinit 关键字

    class JavaConvertExample {
        private lateinit var name: String // 【稍后初始化】【不可空类型】的变量
        fun init() {
            name = "Tom"
        }
        fun test() {
            if (::name.isInitialized) println(name.length) else println("not init")
        }
    }
    
    fun main() {
        val example = JavaConvertExample()
        example.test() // not init
        example.init()
        example.test() // 3
    }
    

    这种思路其实是完全抛弃可空性的。由于它的类型是不可为空的,因此我们初始化的时候,必须传入一个非空的值,这就能保证:只要 name 初始化了,它的值就一定不为空。在这种情况下,我们就将判空问题变成了一个判断是否初始化的问题。

    ⑥ 使用 by lazy 委托

    class JavaConvertExample {
        private val name: String by lazy { init() } // 【不可变】的【非空】属性
        private fun init() = "Tom"
        fun test() {
            println(name.length)
        }
    }
    

    我们将 name 这个变量声明为了不可变的非空属性,并且,借助 Kotlin 的懒加载委托来完成初始化。借助这种方式,我们可以尽可能地延迟初始化,同时,也消灭了可变性、可空性。

    明确泛型的可空性

    Kotlin 空安全的第四条准则:明确泛型可空性

    fun <T> saveSomething(data: T) { // 注意,泛型 T 是可为空的类型
        val set = sortedSetOf<T>()   // 对应 Java TreeSet
        set.add(data)                // TreeSet 内部无法存储 null
    }
    
    fun main() {
        saveSomething("bqt") // 泛型实参自动推导为 String
        saveSomething(null)  // 编译通过,运行时报 NPE
    }
    

    上面的代码中,我们定义的泛型参数 T,是可为空的类型。所以,我们可以将 null 作为参数传进去的,并且编译器也不会报错。紧接着,由于 TreeSet 内部无法存储 null,所以我们的代码在 set.add(data) 这里,会产生空指针异常。

    实际上,我们的 T 是等价于 <T: Any?> 的,这也就意味着,泛型的 T 是可以接收 null 作为实参的。

    fun <T> saveSomething(data: T) {}
    // 等价于
    fun <T: Any?> saveSomething(data: T) {}
    

    所以,正确的写法是,为泛型 T 增加上界 Any,这样当我们尝试传入 null 的时候,编译器就会报错,让这个问题在编译期就能暴露出来。

    fun <T: Any> saveSomething(data: T) { // 增加泛型的边界限制【Any】,不增加时,默认为【Any?】
        val set = sortedSetOf<T>()
        set.add(data)
    }
    

    小结

    Kotlin 的空安全思维,主要有四大准则:

    • 警惕 Kotlin 与外界的交互
    • 绝不使用非空断言 !!.
    • 尽可能使用非空类型
    • 明确泛型的可空性

    2016-08-29

  • 相关阅读:
    CCF CSP 201709-1 打酱油 (贪心)
    CCF CSP 201712-1 最小差值
    CCF CSP 201612-1 中间数
    CCF CSP 201609-1 最大波动
    CCF CSP 201604-1 折点计数
    CCF CSP 201512-1 数位之和
    CCF CSP 201509-1 数列分段
    CCF CSP 201503-1 图像旋转 (降维)
    CCF CSP 201412-1 门禁系统
    CCF CSP 201409-1 相邻数对
  • 原文地址:https://www.cnblogs.com/baiqiantao/p/5817373.html
Copyright © 2011-2022 走看看