4. 變數聲明和變數狀態設計

  • 2020 年 2 月 11 日
  • 筆記

本篇文章將會介紹如何通過正確的變數狀態設計來達到簡化程式碼邏輯的效果。

本篇並不是針對 Kotlin 的語言特性介紹,但它比語言特性更為重要。上一篇文章講的是空安全特性,它允許你方便的處理對象可能為空的情況。但他價值更大的另一面在於,Kotlin 可以聲明不可能為空的對象

1. 非空類型

對象不可能為空意味著程式複雜度的降低。而且這不是一般的降低,因為我們開發過程很多時候都是在處理「這個變數可能為空」的情況。在 Java 的環境里,我們出於對調用的 SDK 的不信任,總是要去判斷以下是否為空,以保平安,這樣處理的代價就是,增加了大量的異常分支程式碼。如果一個變數他永遠都不可能為空,那其實是一件很快樂的事!一個對象可能的狀態減少了,程式邏輯會變得更簡單清晰,程式碼的可維護性會大大的提高。我們應該盡量將一個變數聲明為非空類型。

Java 提供了 @NonNull 和 @Nullable 註解來滿足對象狀態的空設計。但由於默認只會產生警告級別的提示(相信我,很多程式設計師不看 warning),以及使用的繁瑣,它最終落得和 final 一樣的使用頻率。

你很可能會擔心非空類型會帶來記憶體泄漏。因為在 Java 很多釋放操作都會將引用的變數設置為空,這是個很常見的防止記憶體泄漏的辦法。但代價是將程式狀態複雜化。我們確實應該慎重考慮一個變數是否可以一直被持有,但大部分情況我們是可以不用擔心的。如 Android 開發基本只要考慮 Activity 是否間接被單例這樣生命周期過長的對象持有即可。我還依稀記得剛學 Android 的時候,有些網上教程還會教你在 onDestroy 的時候將 onClickListner 設置為 null 防止記憶體泄漏。。

2. lateinit

說到盡量聲明為非空類型,有人就會提出質疑了:非空類型說來簡單,但部分依賴外部調用完成初始化的變數,無法聲明為非空類型啊?Activity 的初始化,就是通過 onCreateView 回調初始化的,各種 UI 對象只能在 onCreate 回調的時候被賦值。

針對這種情況,可以使用 Kotlin 的 lateinit 關鍵字。lateinit 人如其名,它表示這個對象會在稍後被初始化。它還有兩條限制:

  1. 無法用 val 修飾,只能用 var 修飾;
  2. 必須為非空類型。

1 很好理解,val 意義是聲明後無法再被重新賦值,就和 final 一樣。而 lateinit 變數要在稍後才被賦值,所以必須是 var。var 也意味著 lateinit 變數可以被多次賦值,可被多次賦值可能是你想要的,也有可能是你不想要的。

2 的話,設想一下,如果是可空類型,也沒必要用 lateinit 了,直接初始化為 null 即可。所以 2 也是合理的。

如果一個變數被聲明為 lateinit,你可以不用在聲明時初始化它,在任意地方把它當作非空類型直接使用。注意了,此時如果你在初始化這個變數前就使用了該變數,則會丟出一個 RuntimeException:

UninitializedPropertyAccessException: lateinit property has not been initialized

意思就是你還沒初始化這個變數就使用它了。所以使用 lateinit 關鍵字,就需要你自己保證調用順序,保證調用時變數已經被初始化,Kotlin 不再幫你把關了。這看起來像是一個把 Kotlin 空安全廢掉,退化為原來 Java 的無空檢查的行為。這樣就很沒意思了,但其實不是這樣,lateinit 有他特有的表意,即:這個變數在稍後會被初始化,且以後都不再為空。以後不再為空即是他和可空變數的區別,從狀態複雜度來看,lateinit 變數是介於非空變數和可空變數之間的。

使用 lateinit 是一個有風險的事情,因為非空的條件變複雜了(初始化後才是非空)。如果你不能保證所有調用都在賦值後發生,則不應使用它。但對於 Activity 的 onCreate 這種簡單的場景,還是建議使用 lateinit 的。但需要注意一點:

如果 Activity 在 onCreate 的時候初始化失敗了,你需要彈窗或直接 finish 的時候,此時你的 lateinit 變數可能沒有被賦值,而 Activity 仍會執行 onStart onResume onDestroy 這些回調。這種情況就是「沒法保證調用前變數已經初始化」的情況了。

這個時候你可以選擇將變數聲明為可空類型。也可以用 lateinit 變數專有的判斷方法::xxx.isInitialized在關鍵路徑進行判斷,比如 Activity onCreate finish 掉的話,關鍵路徑就只剩 onDestroy了(Fragmet 還有 onCreateView 和 onViewCreated)。但相比這兩種辦法,我更建議你思考,這樣複雜的情景是不是我想要的,設計是否能夠簡化?因為正確設計的程式的狀態應該是簡單清晰的。

3. 空對象模式(Null Object Pattern)

其實相對於 lateinit,我更喜歡空對象這個設計模式。它沒有 lateinit 引入的風險,是一種更簡單的狀態。空對象就是擁有這個類默認實現的對象。對於數據類來說,它的空對象可能所有成員變數都是0,false,長度為0的字元串;對於帶方法的類來說,它的空對象可能是所有方法都是空的,可以調用但沒有任何效果。這樣一個空對象,它可以幫你代替 null,臨時頂替正常實現,直到被重新賦值。同樣是初始化異常,lateinit 可能會崩潰,而空對象最多是表現異常。

可參考:https://en.wikipedia.org/wiki/Null_object_pattern

4. final

除了 Kotlin 的非空類型/可空類型,val/var(即 Java 的 final 關鍵字)也是減少變數狀態的利器。而且它比非空類型更徹底,非空類型只是不允許這個變數變為 null,val 直接不允許變數重新被賦值!聲明為 val 的變數狀態可能性更少,並發競爭的問題都沒有了。

變數狀態設計原則

經過上面的變數狀態介紹,我們按照變數狀態從簡單到複雜的順序,可以得到一個變數狀態聲明的優先順序:

  1. 聲明為 val 變數,無法滿足再考慮 var
  2. 聲明為非空變數
  3. 無法滿足聲明時賦值,優先考慮賦值為空對象
  4. 無法滿足空對象,看看是否可以用 lateinit
  5. 聲明為可空變數

版權所有,轉載請註明出處: https://sickworm.com/?p=1784

共享此文章: