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

  • 相关阅读:
    svn command line tag
    MDbg.exe(.NET Framework 命令行调试程序)
    Microsoft Web Deployment Tool
    sql server CI
    VS 2010 One Click Deployment Issue “Application Validation did not succeed. Unable to continue”
    mshtml
    大厂程序员站错队被架空,只拿着五折工资!苟活和离职,如何选择?
    揭秘!Windows 为什么会蓝屏?微软程序员竟说是这个原因...
    喂!千万别忘了这个C语言知识!(~0 == -1 问题)
    Linux 比 Windows 更好,谁反对?我有13个赞成理由
  • 原文地址:https://www.cnblogs.com/baiqiantao/p/5817373.html
Copyright © 2011-2022 走看看