Kotlin 之空安全

背景

在 Java 語境下桐玻,使用對象總是讓我感到明顯的不安全感简识,這個對象要判空嗎拔妥?這個對象肯定不會為空忿危,不用加判斷了吧?經(jīng)過血淋淋的事實之后没龙,在使用對象之前我總會加上判空處理铺厨,如果調(diào)用的層級有點深,代碼就顯得“惡臭”了硬纤。

而 Kotlin 提供了嚴格的可為 null 規(guī)則解滓,旨在從我們的代碼中消除 NullPointerException,默認情況下筝家,對象的引用不能包含 null 值洼裤。

使用

安全使用

默認情況下,我們創(chuàng)建的所有變量都是不允許為空的溪王,必須給其指定一個值腮鞍,如果給它賦值為 null值骇,就會報錯。如下:

class NullTest {
    var str: String = null//出錯移国,默認情況下不能為空

    var name:String ="tandeneck"

    fun assignNull(){
        name = null //出錯吱瘩,不能賦值為空
    }
}

當然,以上是默認情況下迹缀,某些情況下我們允許允許為空的變量使碾,那么這時候就需要 \color{red}{?} 的加持變?yōu)榭煽疹愋汀H缦拢?/p>

var name:String? = null

但是由此會帶來空指針異常祝懂,所以在 Android Studio 如果直接調(diào)用的時候 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
        }
    }
}

根據(jù)報錯信息得知是由于 textView 是可變的,在調(diào)用的時候有可能它已經(jīng)變?yōu)榭樟搜馀睿驗樵诙嗑€程情況下兄朋,其他線程是有可能把它變?yōu)榭盏摹?/p>

那啥,我們把它改為不可變的不就行了嗎怜械?即把 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 是一個局部變量障本,保證了調(diào)用時不會有另一個線程改變它的值教届。

其實,Kotlin 提供了很方便的機制驾霜,\color{red}{?.}

textView?.textSize = 20f

這個寫法同樣會對變量做一次非空確認之后再調(diào)用方法案训,并且它可以做到線程安全,這種寫法叫做 Safe Call粪糙。

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

除此之外强霎,還有一種雙感嘆號 !! 的用法:

 textView!!.textSize = 20f

這種寫法叫做 non-null asserted call,即非空斷言蓉冈,如果為空的情況則會拋出異常城舞,因此慎用。

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

Elvis 操作符能夠大大簡化 if-else 表達式家夺,如下:

fun main() {
    var b: String? = "length"http://定義了一個可能為null的字符串變量str
    val length1: Int = if (b != null) b.length else 0
    val length2: Int = b?.length ?: 0
}
安全類型轉(zhuǎn)換, \color{red}{as?}

Kotlin 可以使用 as 關(guān)鍵字來進行類型轉(zhuǎn)換,如果對象不是目標類型伐弹,那么類型轉(zhuǎn)換可能會導(dǎo)致 ClassCastException拉馋。這時哦我們選擇 as? ,如果嘗試轉(zhuǎn)換不成功則會返回 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()

然后我們查看它們對應(yīng)的字節(jié)碼柠逞,操作方法:Tools -> Kotlin -> Show Kotlin Bytecode,然后點擊 Decompile 反編譯字節(jié)碼得到以下代碼:

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 方法:
首先給參數(shù) str 加上 @NotNull 注解
然后調(diào)用 Intrinsics.checkParameterIsNotNull(str, "str") 方法景馁,其實現(xiàn)如下:

    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);
    }   

如果參數(shù)為空,則會拋出異常逗鸣。

最后調(diào)用 toUpperCase() 方法并返回結(jié)果合住。

test2 方法與 test1 不同的地方是注解變?yōu)?@Nullable,傳入的參數(shù)為 null 情況則會返回 null撒璧,否則調(diào)用相應(yīng)的方法透葛。

test3 方法判斷參數(shù)為空時會直接拋出空指針異常,否則調(diào)用相應(yīng)的邏輯卿樱。

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

  • 1.非空類型的屬性編譯器添加@NotNull注解,可空類型添加@Nullable注解繁调;
  • 2.非空類型直接對參數(shù)進行判空萨蚕,如果為空直接拋出異常;
  • 3.可空類型蹄胰,如果是?.判空岳遥,不空才執(zhí)行后續(xù)代碼,否則返回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庆猫,而不是報異常了
    }

總結(jié)

Kotlin 空安全能幫助我們編寫高效安全的代碼好渠,了解它背后的原理能使我們運用得更加順手。同時节视,也要注意一些坑拳锚,保證代碼的穩(wěn)健性。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末霍掺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杆烁,老刑警劉巖牙丽,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異兔魂,居然都是意外死亡烤芦,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門析校,熙熙樓的掌柜王于貴愁眉苦臉地迎上來构罗,“玉大人,你說我怎么就攤上這事智玻∷爝螅” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵吊奢,是天一觀的道長盖彭。 經(jīng)常有香客問我,道長页滚,這世上最難降的妖魔是什么召边? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮裹驰,結(jié)果婚禮上掌实,老公的妹妹穿的比我還像新娘。我一直安慰自己邦马,他們只是感情好贱鼻,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著滋将,像睡著了一般邻悬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上随闽,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天父丰,我揣著相機與錄音,去河邊找鬼掘宪。 笑死蛾扇,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的魏滚。 我是一名探鬼主播镀首,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鼠次!你這毒婦竟也來了更哄?” 一聲冷哼從身側(cè)響起芋齿,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎成翩,沒想到半個月后觅捆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡麻敌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年栅炒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片术羔。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡赢赊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出聂示,到底是詐尸還是另有隱情,我是刑警寧澤簇秒,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布鱼喉,位于F島的核電站,受9級特大地震影響趋观,放射性物質(zhì)發(fā)生泄漏扛禽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一皱坛、第九天 我趴在偏房一處隱蔽的房頂上張望编曼。 院中可真熱鬧,春花似錦剩辟、人聲如沸掐场。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽熊户。三九已至,卻和暖如春吭服,著一層夾襖步出監(jiān)牢的瞬間嚷堡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工艇棕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蝌戒,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓沼琉,卻偏偏與公主長得像北苟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子打瘪,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345