Kotlin 之空安全

背景

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

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345