您好,登錄后才能下訂單哦!
今天給大家介紹一下怎樣避免Kotlin里的陷阱。文章的內容小編覺得不錯,現在給大家分享一下,覺得有需要的朋友可以了解一下,希望對大家有所幫助,下面跟著小編的思路一起來閱讀吧。
最近 Kotlin 特別流行,并且我也贊同 Kotlin 是一個經過深思熟慮后被設計出的語言,除了下面提到的缺點之外。下面向你分析一些我在開發過程中遇到的陷阱,并且教你如何避免他們。
謎一樣的 null
在 Kotlin 當中,你可以不用考慮在你的代碼中如何處理 null 的問題,這會讓你忘記 null 是無處不在的這個說法,只不過被隱藏了起來。看看下面這個表面看起來沒有問題的類:
class Foo { private val c: String init { bar() c = "" } private fun bar() { println(c.length) } }
如果你嘗試初始化這個類,那么代碼就會拋出一個 NullPointerException。因為 bar 方法嘗試在 c 變量初始化之前就訪問它。
盡管這個代碼本身就是有問題的,才導致異常拋出。但是更糟糕的是你的編譯器不會發現這一點。
Kotlin 可以幫你在絕大部分情況下避免 null,但是你不能因此而忘記 null 的存在。否則遲早有一天你會碰上類似的問題。
來自 JDK 的 null
Kotlin 的標準庫能夠很好地處理 null。但是如果你使用了 JDK 中的類,你需要自己處理關于 JDK 方法調用可能產生的空指針。
大部分情況下 Kotlin 的標準庫就足夠了,但是有時你需要使用到 ConcurrentHashMap:
val map = ConcurrentHashMap<String, String>() map["foo"] = "bar" val bar: String = map["foo"]!!
這時,你需要使用 !! 操作符。但某些情況下你還可以使用像 (?) 這樣的對 null 安全的操作符來替換它。盡管如此,當你使用 !! 或者 ? ,或者編寫了一個適配器來使用 Java 類庫的時候,你會發現代碼因為這些修改而變的混亂。這是你無法避免的問題。
你還可能會碰上更多更可怕的問題。當你使用 JDK 類中的方法的時候,返回值可能是null,而且沒有什么像 Map 訪問一樣的語法糖。
考慮如下例子:
val queue: Queue<String> = LinkedList() queue.peek().toInt()
這種情況下,你使用了可能返回 null 值的 peek 方法。但是 Kotlin 編譯器不會提示你這個問題,所以當你的 Queue 是空隊列的的時候,可能會觸發 NullPointerException 異常。
問題在于我們使用的 Queue 是 JDK 的一個接口,并且當你查看 peek 方法的文檔時:
/** * Retrieves, but does not remove, the head of this queue, * or returns {@code null} if this queue is empty. * * @return the head of this queue, or {@code null} if this queue is empty */ E peek();
文檔中說 peek 方法會返回一個 E 類型的對象,但是 Kotlin 認為 E 是不可空的。在接下來的 Kotlin 版本中可能會解決這個問題,但是現在當你在你的工程中使用類似接口的時候,一定要注意:
val queue: Queue<String?> = LinkedList() queue.peek()?.toInt()
內部 it
當一個 lambda 表達式只有一個參數的時候,你可以在你的代碼中將其省略,并用 it 代替。
it:單參數的內部名稱。當你表達式只有一個參數的時候,這是一個很有用的特性,聲明的過程可以省略(就像 ->),并且參數名稱為 it。
問題是,當你的代碼中存在向下面例子一樣的嵌套函數的時候:
val list = listOf("foo.bar", "baz.qux") list.forEach { it.split(".").forEach { println(it) } }
it 參數會混淆。解決方法就是像下面這樣顯示的聲明:
list.forEach { item -> item.split(".").forEach { part -> println(part) } }
看起來是不是好多了!
隱藏的復制
注意觀察下面的類:
data class Foo(val bars: MutableList)
data 類提供了一系列的方法,并且你可以通過拷貝得到其鏡像。猜猜下面的代碼會輸出什么?
val bars = mutableListOf("foobar", "wombar") val foo0 = Foo(bars) val foo1 = foo0.copy() bars.add("oops") println(foo1.bars.joinToString())
控制臺會輸出 foobar, wombar, oops。問題出在 copy 方法并沒有真正地復制一個完整的對象, 而是復制了對象的引用。當你忘記編寫單元測試類,并且將你的 data 類按照不可變類來傳遞的時候,就可能出現這種問題。
解決方法就是當你使用 data 類的時候一定要多加小心,并且當你必須將其作為值對象的時候,像下面這樣:
data class Foo(val bars: List)
data 類還有一個問題:其 equals / hashCode 方法所用到的屬性不可變。你只能通過手工重寫這些方法的方式來修改返回值。謹記上面這一點。
內部方法暴露
仔細思考下面的例子:
class MyApi { fun operation0() { } internal fun hiddenOperation() { } }
當你在 Kotlin 的項目中引用這個類的時候,internal 關鍵字是生效的。但是當你從一個 Java 項目中使用的時候,hiddenOperation 就變成了一個公共方法!為了避免這種情況,我建議使用接口的方式來隱藏實現的細節:
interface MyApi { fun operation0() } class MyApiImpl: MyApi { override fun operation0() { } internal fun hiddenOperation() { } }
特殊的全局擴展
毫無疑問,擴展函數的功能非常重要。但通常,能力越大責任越大。例如,你可以編寫全局的 JDK 類擴展函數。但是當這個函數只在本地上下文中有意義,卻是全局可見的時候,就會帶來很多麻煩。
fun String.extractCustomerName() : String { // ... }
每個跳轉到你的方法的人都會不知所措。所以我認為在你編寫這樣的方法之前務必三思。下面就是一個建議:
/** * Returns an element of this [List] wrapped in an Optional * which is empty if `idx` is out of bounds. */ fun <T> List<T>.getIfPresent(idx: Int) = if (idx >= size) { Optional.empty() } else { Optional.of(get(idx)) } /** * Negates `isPresent`. */ fun <T> Optional<T>.isNotPresent() = isPresent.not()
lambdas Unit 返回值 vs Java SAM 轉換
如果你的函數參數是 lambdas 表達式,并且返回值類型是 Unit 的時候,你可以省略return 關鍵字:
fun consumeText(text: String, fn: (String) -> Unit) { } // usage consumeText("foo") { println(it) }
這是一個很有趣的特性,但是當你在 Java 代碼中調用該方法的時候會比較尷尬:
consumeText("foo", (text) -> { System.out.println(text); return Unit.INSTANCE; });
這對于 Java 端來說是不友好的,如果你想在 Java 中成功調用該方法,你需要定義如下接口:
nterface StringConsumer { fun consume(text: String) } fun consumeText(text: String, fn: StringConsumer) { }
然后你就能使用 Java 的 SAM 轉換。
consumeText("foo", System.out::println);
但是在 Kotlin 這邊看起來就很糟糕了:
consumeText("foo", object: StringConsumer { override fun consume(text: String) { println(text) } })
問題關鍵點在于只有 Java 支持 SAM 轉換,Kotlin 并不支持。我的建議是簡單的場景中,只是用 Java 的 SAM 接口作為一個消費者:
fun consumeText(text: String, fn: Consumer<String>) { } // usage from Kotlin consumeText("foo", Consumer { println(it) }) // usage from Java consumeText("foo", System.out::println);
Java 中使用不可變集合
Kotlin 提供了 JDK 集合類的不可變版本。
fun createSet(): Set<String> = setOf("foo") // ... createSet().add("bar") // oops, compile error
這是一個很好的補充。但是當你在看 Java JDK 的 Set 類 API 的時候會發現:
createSet().add("bar"); // UnsupportedOperationException
當你嘗試修改這個 Set 的時候,就會拋出這個異常,就像你使用了Collections.unmodifiableSet() 方法一樣。我不知道這種情況是否合理,但是你在使用 Kotlin 不可變版本的 Java 集合類的時候,需要謹記這一點。
接口中沒有重載
Kotlin 在接口上不支持使用 @JvmOverloads 注解,當然 override 也不行。
interface Foo { @JvmOverloads // OUCH! fun bar(qux: String) } class FooImpl : Foo { @JvmOverloads // YIKES! override fun bar(qux: String) { } }
你只能像下面這樣手動定義:
interface Foo { fun bar() fun bar(qux: String) }
要記住你可以使用 Kotlin 中的 KEEP (Kotlin Evolution and Enhancement Process) 來改善。KEEP 與 Java 中的 JEP 類似,但是與 JEP 相比要簡潔許多。
Kotlin 現下很流行,并且我也認為他是一個增強版的 Java。但是在使用 Kotlin 的時候你仍需要保持清醒,尤其是當你身處各種各樣的關于 Kotlin 的宣傳之中時。如果你要使用 Kotlin 的話,一定要注意我們在上面提到的 Kotlin 相關的缺陷。
以上就是怎樣避免Kotlin里的陷阱的全部內容了,更多與怎樣避免Kotlin里的陷阱相關的內容可以搜索億速云之前的文章或者瀏覽下面的文章進行學習哈!相信小編會給大家增添更多知識,希望大家能夠支持一下億速云!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。