美文网首页
Kotlin 之空安全

Kotlin 之空安全

作者: tandeneck | 来源:发表于2020-06-15 23:26 被阅读0次

    背景

    在 Java 语境下,使用对象总是让我感到明显的不安全感,这个对象要判空吗?这个对象肯定不会为空,不用加判断了吧?经过血淋淋的事实之后,在使用对象之前我总会加上判空处理,如果调用的层级有点深,代码就显得“恶臭”了。

    而 Kotlin 提供了严格的可为 null 规则,旨在从我们的代码中消除 NullPointerException,默认情况下,对象的引用不能包含 null 值。

    使用

    安全使用

    默认情况下,我们创建的所有变量都是不允许为空的,必须给其指定一个值,如果给它赋值为 null,就会报错。如下:

    class NullTest {
        var str: String = null//出错,默认情况下不能为空
    
        var name:String ="tandeneck"
    
        fun assignNull(){
            name = null //出错,不能赋值为空
        }
    }
    

    当然,以上是默认情况下,某些情况下我们允许允许为空的变量,那么这时候就需要 \color{red}{?} 的加持变为可空类型。如下:

    var name:String? = null
    

    但是由此会带来空指针异常,所以在 Android Studio 如果直接调用的时候 IDE 会报错:

    class User {
        var name:String = "tandeneck"
    }
    
    fun main() {
       var user:User? = null
        println(user.name)
    }
    //报错信息:Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type User?
    

    那我们做下判空处理会怎样?这就要分情况讨论了:

    情况一:

    class MainActivity : AppCompatActivity() {
    
        var textView: TextView? = null
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
        }
    
        private fun test() {
            if (textView != null) {
                textView.textSize = 20f
                //上面一行代码报错:
                //Smart cast to 'TextView' is impossible,
                // because 'textView' is a mutable property 
                // that could have been changed by this time
            }
        }
    }
    

    根据报错信息得知是由于 textView 是可变的,在调用的时候有可能它已经变为空了,因为在多线程情况下,其他线程是有可能把它变为空的。

    那啥,我们把它改为不可变的不就行了吗?即把 var 改为 val,如下:

     val textView: TextView? = null
    

    这样报错是不会报错了,但是没有意义,因为 textView 不能被重新赋值,永远是空的。

    情况二:

    class MainActivity : AppCompatActivity() {
        
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
        }
    
        private fun test() {
            var textView: TextView? = null
            if (textView != null) {
                textView.textSize = 20f
                //上面一行代码不会报错
            }
        }
    }
    

    不会报错的原因是 textView 是一个局部变量,保证了调用时不会有另一个线程改变它的值。

    其实,Kotlin 提供了很方便的机制,\color{red}{?.}

    textView?.textSize = 20f
    

    这个写法同样会对变量做一次非空确认之后再调用方法,并且它可以做到线程安全,这种写法叫做 Safe Call。

    \color{red}{!!} 操作符

    除此之外,还有一种双感叹号 !! 的用法:

     textView!!.textSize = 20f
    

    这种写法叫做 non-null asserted call,即非空断言,如果为空的情况则会抛出异常,因此慎用。

    Elvis 操作符,(\color{red}{?:}

    Elvis 操作符能够大大简化 if-else 表达式,如下:

    fun main() {
        var b: String? = "length"//定义了一个可能为null的字符串变量str
        val length1: Int = if (b != null) b.length else 0
        val length2: Int = b?.length ?: 0
    }
    
    安全类型转换, \color{red}{as?}

    Kotlin 可以使用 as 关键字来进行类型转换,如果对象不是目标类型,那么类型转换可能会导致 ClassCastException。这时哦我们选择 as? ,如果尝试转换不成功则会返回 null:

    fun main() {
        var str = "string"
        val num: Int? = str as? Int
        println(num)
        //输出 null
    }
    

    原理

    了解空安全的使用之后,下面让我们来看看其背后的原理,做到知其所以然。以下面的代码为例:

    fun test1(str: String) = str.toUpperCase()
    fun test2(str: String?) = str?.toUpperCase()
    fun test3(str: String?) = str!!.toUpperCase()
    

    然后我们查看它们对应的字节码,操作方法:Tools -> Kotlin -> Show Kotlin Bytecode,然后点击 Decompile 反编译字节码得到以下代码:

    public final class TestKt {
       @NotNull
       public static final String test1(@NotNull String str) {
          Intrinsics.checkParameterIsNotNull(str, "str");
          boolean var2 = false;
          String var10000 = str.toUpperCase();
          Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toUpperCase()");
          return var10000;
       }
    
       @Nullable
       public static final String test2(@Nullable String str) {
          String var10000;
          if (str != null) {
             boolean var2 = false;
             if (str == null) {
                throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
             }
    
             var10000 = str.toUpperCase();
             Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toUpperCase()");
          } else {
             var10000 = null;
          }
    
          return var10000;
       }
    
       @NotNull
       public static final String test3(@Nullable String str) {
          if (str == null) {
             Intrinsics.throwNpe();
          }
    
          boolean var2 = false;
          if (str == null) {
             throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
          } else {
             String var10000 = str.toUpperCase();
             Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toUpperCase()");
             return var10000;
          }
       }
    }
    

    我们先看 test1 方法:
    首先给参数 str 加上 @NotNull 注解
    然后调用 Intrinsics.checkParameterIsNotNull(str, "str") 方法,其实现如下:

        public static void checkParameterIsNotNull(Object value, String paramName) {
            if (value == null) {
                throwParameterIsNullException(paramName);
            }
        }
        
        private static void throwParameterIsNullException(String paramName) {
            StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
    
            // #0 Thread.getStackTrace()
            // #1 Intrinsics.throwParameterIsNullException
            // #2 Intrinsics.checkParameterIsNotNull
            // #3 our caller
            StackTraceElement caller = stackTraceElements[3];
            String className = caller.getClassName();
            String methodName = caller.getMethodName();
    
            IllegalArgumentException exception =
                    new IllegalArgumentException("Parameter specified as non-null is null: " +
                                                 "method " + className + "." + methodName +
                                                 ", parameter " + paramName);
            throw sanitizeStackTrace(exception);
        }   
    

    如果参数为空,则会抛出异常。

    最后调用 toUpperCase() 方法并返回结果。

    test2 方法与 test1 不同的地方是注解变为 @Nullable,传入的参数为 null 情况则会返回 null,否则调用相应的方法。

    test3 方法判断参数为空时会直接抛出空指针异常,否则调用相应的逻辑。

    由此,我们知道 Kotlin 空安全背后的原理:

    • 1.非空类型的属性编译器添加@NotNull注解,可空类型添加@Nullable注解;
    • 2.非空类型直接对参数进行判空,如果为空直接抛出异常;
    • 3.可空类型,如果是?.判空,不空才执行后续代码,否则返回null;如果是!!,空的话直接抛出NPE异常。

    注意事项

    Kotlin 并不是绝对的空安全,以下情况不做特殊处理可能会抛出空指针异常:

    • 使用前面提到的 !! 操作符,
    • 与 Java 互操作,如下:
    public class User {
    
        public Student student;
    
        public static final class Student {
            public String name;
        }
    }
    
    fun main() {
        fun printStudentName(user: User) {
            println(user.student.name)
        }
    
        printStudentName(User())
        //报空指针异常
    }
    

    解决的方法也比较简单:

        fun printStudentName(user: User) {
            println(user.student?.name)
            //这样就输出 null,而不是报异常了
        }
    

    总结

    Kotlin 空安全能帮助我们编写高效安全的代码,了解它背后的原理能使我们运用得更加顺手。同时,也要注意一些坑,保证代码的稳健性。

    参考

    相关文章

      网友评论

          本文标题:Kotlin 之空安全

          本文链接:https://www.haomeiwen.com/subject/zoecxktx.html