在Java和Swift中避免空引用異常

您最近在代碼中遇到過NullPointerException(空指針異常)嗎? 如果沒有,那你一定是一個很細心的程序員。在Java應用程序中最常見的異常類型之一就是NullPointerException。只要該語言允許用戶將空值分配給一個對象,在某個時間點上對象為空將引發空指針異常,從而導致整個系統崩潰。

Java 8中引入了java.util.Optional類來處理這個問題。實際上,這些Optional's API 非常強大。有很多情況下,Optional's API 可以解決我們遇到的問題。然而,它們並不是僅僅為解決NullPointerException問題而設計的。此外,Optional本身很容易被誤用。一個很好的指標就是經常發佈的文章的數量,這些文章是關於Optionals應該或者不應該被使用的。

與Java相反,其他的開發語言,如Kotlin、Swift、Groovy等,能夠區分允許指向空值的變量和不允許指向空值的變量。換句話說,除非將變量顯式聲明為nullable(可空),否則它們不允許將空值分配給變量。在本文中,我們將概述不同編程語言中的可以減少或避免使用空值的一些特性。

Java Optionals

隨著在Java 1.8中引入的java.util.Optional類,顯著減少了空引用的情況。儘管如此,在創建或使用 Optional 時也需要注意一些問題。例如,如果值不存在, Optional.get()方法將拋出NoSuchElementException異常。如果提供的值為空,方法將拋出NullPointerException異常。因此,使用這兩種方法都與直接使用空值對象有一樣的風險。我們從 Optional中得到的一個好處是,它提供了一組更高階的函數,這些函數可以被鏈接起來,不必擔心值是否存在。

Null Checks

讓我們設計一個簡單的示例,其中有兩個類的用戶和地址,其中用戶中的必需字段只有用戶名,地址中的必需字段是street和number。任務是用給定的ID查找用戶的郵政編碼,如果沒有任何值,則返回一個空字符串。

假設還提供了UserRepository。實現這個任務的一種方法是:

在Java和Swift中避免空引用異常

上面的代碼,如果userRepository不是null,則此代碼不會拋出NullPointerException。但是,代碼中有三個if語句用於執行null檢查。檢查是否為空代碼的行數與為完成任務而編寫的代碼數量相當。

Optional Chaining

如果在不保證返回非空值的方法上使用Optionals作為返回類型,則上述實現也可以寫成:

在Java和Swift中避免空引用異常

第二個實現的代碼也第一個實現也好的很有限。空檢查只是被Optional.isPresent方法替換了。在調用每一個Optional.get()之前,都需要使用Optional.isPresent來判斷。在Java 10引入了一個更好的 Optional.orElseThrow ——它的使用方式一樣,但是方法名是警告說,如果值不存在,將拋出一個異常。

上面的代碼只是為了顯示 Optionals的醜陋用法。一種更優雅的方法是使可選API提供的一系列高階函數:

在Java和Swift中避免空引用異常

如果用戶存儲庫返回的Optional為空,則flatMap將只返回一個空可選項。否則,它將返回可選的包裝用戶的地址。這樣,就不需要進行任何空檢查。第二次flatMap調用也是如此。因此,Optional可被級聯,直到達到我們要查找的值。

Java 9增強功能

Optional API 在Java 9中進一步豐富,還有其他三個方法:or, stream 和ifPresentOrElse。

Optional.or 為連鎖選擇提供另一種可能性。例如,如果我們在內存中已經有一個用戶集合,我們想在進入存儲庫之前搜索這個集合,那麼我們可以做以下工作:

在Java和Swift中避免空引用異常

Optional.stream允許將可選的轉換為至多一個元素的流。假設我們要將userIds列表轉換為用戶列表。在Java 9中,可以這樣做:

在Java和Swift中避免空引用異常

Optional.ifPresentOrElse is similar to Optional.ifPresent from Java 1.8, but it performs a second action if the value is not present. For example, if the task was to print the ZIP code and it is provided or print a message otherwise, we could do the following:

Optional.ifPresentOrElse類似於Java 1.8中提供的Optional.ifPresent ,可是如果值不存在,則執行第二個操作。例如,如果任務是打印郵政編碼,如果提供了郵政編碼則打印,否則打印一條消息,代碼如下:

在Java和Swift中避免空引用異常

畢竟,Java最大的缺陷之一是它允許將每個非基本類型分配給null——甚至是Optional類型本身。如果findById方法只是返回null,那麼上面所描述的一切就變得毫無意義了。

Kotlin's 語言中Null類型安全

與Java不同的是,Kotlin語言的類型系統支持可空類型,這意味著除了數據類型的通常值外,還可以表示特殊值null的類型。默認情況下,所有變量都是不可空的。要聲明一個可空變量,聲明的類型後面應該有一個問號。

var user : User = null // 不能編譯,User是可空類型

var nullableUser : User? = null // 為 nullableUser對象聲明並分配一個Null值

val name = nullableUser.name // 不能編譯. 需要使用 '?.' 或 '!!'

var lastName = nullableUser?.lastName // 返回空,因為 nullableUser 是 null

val email = nullableUser!!email // compiles, but throws NullPointerException on runtime

lastName = nullableUser?.lastName ?: "" //返回空字符串

注意空安全調用之間的區別嗎?和非空斷言運算符!!正如名稱所示,如果反引用變量為null,則前者將立即返回null,而後者將拋出NullPointerException。你不想用!!除非你是nullpointerexception的愛好者。操作符類似於optionorelse。它返回在?:的左邊的表達式的值,如果它不是null。否則,它計算右邊的表達式並返回結果。.

Nullable Chaining

與Java中的Optionals 一樣,Kotlin中的可空值也可以通過使用例如null-safe調用操作符進行鏈接。在Kotlin中,findZipCode方法的實現將在一個語句中完成:

fun findZipCode(userId: String) =

userRepository.findById(userId)?.address?.zipCode ?: ""

Swift

Swift的運行與Kotlin非常相似。類型必須顯式地標記才能存儲nil值。這可以通過添加?後綴運算符用於字段或變量聲明的類型。不過,這只是在Swift標準庫中定義的Optional類型的一種簡短形式。與普通類型不同,Swift選項不需要直接初始化或由構造函數初始化。它們默認為nil。Swift可選實際上是一個枚舉,它有兩種狀態:none和some,其中none表示nil, some表示一個已wrapped的對象。

var zipCode = nil // won’t compile, because zipCode is not optional

var zipCode : String = nil // same here

var zipCode : String? = nil // compiles, zipCode contains "none"

var zipCode : Optional = nil // same as above

var zipCode : String? = "1010" // zipCode contains "some" String

Implicitly Unwrapped Optionals

Optionals can also be declared as implicitly unwrapped Optional by using the ! postfix operator on the type of the variable declaration. The main difference is that these can be accessed directly without the ? or ! operators. The usage of implicitly unwrapped Optionals is highly discouraged, except in very specific situations, where they are necessary and where you can be certain, that a value exists. There are very few cases in which this mechanism is really needed, one of which is the Interface Builder Outlets for iOS or macOS.

Here is an example of how it should NOT be done:

Optionals 也可以通過使用!變量聲明類型的後綴操作符。主要的區別是這些可以直接訪問而不需要?或!操作符。強烈建議不要使用隱式展開選項,除非是在非常特定的情況下,它們是必需的,並且您可以確定值的存在。

這裡有一個不應該這樣做的例子:

// zipCode will be nil by default and is implicitly unwrapped

var zipCode : String!

/*

* if zipCode has a value, it will work fine but in this case

* it hasn’t and will therefore throw an error

*/

zipCode.append("0")

達到同樣結果的正確方法是:

var zipCode : String?

zipCode?.append("0") // this line will return nil but no error is thrown

Optional Chaining

Optional 鏈接可用於使用?後綴運算符。許多對選項的調用可以鏈接在一起,因此命名為可選鏈接。這樣的表達式總是返回一個可選項,如果鏈中任何可選項都不包含,則該表達式將包含結果對象或none。因此,必須再次檢查可選鏈的結果是否為nil。這可以通過使用可選綁定、nil-合併操作符或guard語句來避免。

/*

* Optional chaining for querying the zip code,

* where findBy, address and zipCode are Optionals

* themselves.

*/

func findZipCodeFor(userId: String) -> String? {

return userRepository.findBy(userId: userId)?.address?.zipCode

}

Optional Binding

“if let”語句提供了一種安全的方式來 unwrap Optionals。如果給定的可選項包含none,則跳過If塊。否則,將聲明一個本地常量,該常量僅在if塊中有效。這個常量可以有與可選項相同的名稱,這將導致在塊中不可見的實際可選性。除了多個展開語句外,還可以向if let語句添加布爾表達式。這些語句之間用逗號(,)分隔,它的行為類似於&&操作符。

func printZipCodeFor(user: String) {

let zipCode = userRepository.findBy(userId: user)?.address?.zipCode

if let zipCode = zipCode {

print(zipCode)

}

}

func findZipCodeFor(userId: String, inCountry country: String) -> String? {

if let address = userRepository.findBy(userId: userId)?.address,

let zipCode = address.zipCode,

address.country == inCountry {

return zipCode

}

return nil

}

Nil-Coalescing Operator

無合併運算符由??如果可選項不包含任何值,則其目的是提供一個默認值。它的行為與 Kotlin’s Elvis操作員相似(?:)

let userId = "1234"

print(findZipCodeFor(userId: userId) ?? "no zip code found for user \(userId)")

操作符還接受另一個可選值作為默認值。因此,可以將多個nil合併操作符鏈接在一起。

func findZipCodeOrCityFor(user: String) -> String {

return findZipCodeFor(userId: user)

?? findCityFor(userId: user)

?? "neither zip code nor city found for user \(user)"

}

Guard

保護語句,顧名思義,是在它後面保護代碼。在方法中,檢查方法參數的有效性通常是在最開始。但是,如果可選項不包含任何選項,它也可以打開選項(類似於可選綁定)並“保護”後面的代碼。一個保護語句只包含一個條件和/或一個未包裝的語句和一個強制的else塊。編譯器通過使用控制傳輸語句(返回、拋出、中斷、繼續)或調用從未返回類型的方法來確保這個else塊退出其封閉範圍。可選項的未包裝值可以在保護語句的封閉範圍中看到,在這裡可以像使用普通常量一樣使用它。保護語句使代碼更具可讀性,並防止大量嵌套if語句。

func update(user: String, withZipCode zipCode: String) {

guard let address = userRepository.findBy(userId: user)?.address else {

print("no address found for \(user)")

return

}

address.zipCode = zipCode

}

結論

當請求的值沒有被信任時,建議使用Java Optionals作為API的返回類型。這樣,將鼓勵API的客戶端檢查返回值是否存在,並通過使用可選的API編寫更乾淨的代碼。然而,最大的缺陷之一是Java不能強制程序員不分配null值。其他現代語言,如Kotlin和Swift,被設計成能夠區分允許表示空值的類型和不允許表示空值的類型。此外,它們提供了一組豐富的特性來處理可空變量,從而最小化空引用異常的風險。


分享到:


相關文章: