當前位置:網站首頁>Stack Overflow 上最熱門的 10 個 Kotlin 問題

Stack Overflow 上最熱門的 10 個 Kotlin 問題

2022-05-14 06:42:04hi-dhl

這是 Stack Overflow 上最熱門的幾個 Kotlin 問題,每個問題如果更深入的分析,都可以單獨寫一篇文章,後面我會針對這些問題,在進一步的分析。

通過這篇文章你將學習到以下內容:

  • Array<Int>IntArray 的區別,以及如何選擇
  • IterableSequence 的區別,以及如何選擇
  • 常用的 8 種 For 循環遍曆的方法
  • 在 Kotlin 中如何使用 SAM 轉換
  • 如何聲明一個靜態成員,Java 和 Koltin 進行互操作
  • 為什麼 kotlin 中的智能轉換不能用於可變屬性,如何才能解决這個問題
  • 當重寫 Java 函數時,如何决定參數的可空性
  • 如何在一個文件中使用多個具有相同名稱的擴展函數和類

譯文

Array 和 IntArray 的區別

Array

Array<T> 可以為任何 T 類型存儲固定數量的元素。它和 Int 類型參數一起使用, 例如 Array<Int>,編譯成 Java 代碼,會生成 Integer[] 實例。我們可以通過 arrayOf 方法創建數組。

val arrayOfInts: Array<Int> = arrayOf(1, 2, 3, 4, 5)

IntArray

IntArray 可以讓我們使用基礎數據類型的數組,編譯成 Java 代碼,會生成 int[] (其它的基礎類型的數組還有 ByteArray , CharArray 等等), 我們可以通過 intArrayOf 工廠方法創建數組。

val intArray: IntArray = intArrayOf(1, 2, 3, 4, 5)

什麼時候使用 Array<Int> 或者 IntArray

默認使用 IntArray,因為它的性能更好,不需要對每個元素進行裝箱。IntArray 進行初始化的時候,默認將每個索引的值初始化為 0,代碼如下所示。

val intArray = IntArray(10)
val arrayOfInts = Array<Int>(5) { i -> i * 2 }

Array<Int> 的性能比較差,會對每個元素進行裝箱,如果你需要創建包含 null 值的數組,Kotlin 也提供了 arrayOfNulls 方法,幫助我們進行創建。

val notActualPeople: Array<Person?> = arrayOfNulls<Person>(13)

Iterable 和 Sequence 的區別

Iterable

Iterable 對應 Java 的 java.lang.Iterable, Iterable 會立即處理輸入的元素,並返回一個包含結果的新集合。我們來舉一個簡單的例子 返回年齡 > 21 前 5 個人的集合

val people: List<Person> = getPeople()
val allowedEntrance = people
		.filter { it.age >= 21 }
		.map { it.name }
		.take(5)
  • 首先通過 filter 函數檢查每個人的年齡,將結果放入到一個新的結果集中
  • 通過 map 函數對上一步得到的結果進行名字映射,然後生成一個新的列錶 list<String>
  • 通過 take 函數返回前 5 個元素,得到最終的結果集

Sequence

Sequence 是 Kotlin 中一個新的概念,用來錶示一個延遲計算的集合。Sequence 只存儲操作過程,並不處理任何元素,直到遇到終端操作符才開始處理元素,我們也可以通過 asSequence 擴展函數,將現有的集合轉換為 Sequence ,代碼如下所示。

val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
	.filter { it.age >= 21 }
	.map { it.name }
	.take(5)
	.toList()

在這個例子中, toList() 錶示終端操作符,filtermaptake 都是中間操作符,返回 Sequence 實例,當 Sequence 遇到中間操作符時,只是存儲操作過程,並不參與計算,直到遇到 toList()

Sequence 的好處它不會生成中間結果集,直接對原始列錶中的每一個人重複這個步驟,直到找到 5 個人,返回最終的結果集。

應該如何選擇

如果數據量比較小,可以使用 Iterable。雖然會創建中間結果集,在數據不大的情况下,對性能的影響不會很嚴重。

如果處理的數據量比較大,Sequence 是最好的選擇,因為不會創建中間結果集,內存開銷更小。

常用的 8 種 For 循環遍曆方法

我們經常會使用以下方法進行遍曆。

for (i in 0..args.size - 1) {
	println(args[i])
}

但是 Array 有一個可讀性更强的擴展屬性 lastIndex

for (i in 0..args.lastIndex) {
	println(args[i])
}

但是實際上我們不需要知道最後一個索引,有一個更加簡單的寫法。

for (i in 0 until args.size) {
	println(args[i])
}

當然你也可以使用下標擴展屬性 indices 得到它的範圍。

for (i in args.indices) {
	println(args[i])
}

還有一個更加直接的寫法,通過下面的方式直接迭代集合。

for (arg in args) {
	println(arg)
}

您也可以使用 forEach 函數,傳遞一個 lambda 錶達式來處理每個元素。

args.forEach { arg ->
	println(arg)
}

它們生成的 Java 代碼都非常的相似,在這些例子中,都增加一個索引變量,並在循環中通過索引獲取元素。但是如果我們迭代的是 List,最後兩個例子底層使用 Iterator,而其他的例子仍是通過索引獲取元素。另外還有兩個遍曆的方法:

  • withIndex 函數,它返回一個 Iterable 對象,該對象可以被解構為當前索引和元素。
for ((index, arg) in args.withIndex()) {
    println("$index: $arg")
}
  • forEachIndexed 函數,它為每個索引和參數提供了一個 lambda 錶達式。
args.forEachIndexed { index, arg ->
    println("$index: $arg")
}

如何使用 SAM 轉換

可以通過 lambda 錶達式實現 SAM 轉換,從而使代碼更簡潔,可讀性更强,我們來看一個例子。

在 Java 中定義一個 OnClickListener 接口,並聲明一個 onClick 的方法。

public interface OnClickListener {
    void onClick(Button button);
}

我們給 Button 添加 OnClickListener 監聽器,每次點擊的時候都會被調用。

public class Button {
    public void setListener(OnClickListener listener) { ... }
}

在 Kotlin 中常見的寫法,創建匿名類,實現 OnClickListener 接口。

button.setListener(object: OnClickListener {
    override fun onClick(button: Button) {
        println("Clicked!")
    }
})

如果我們使用 SAM 轉換,將使代碼更簡潔,可讀性更强。

button.setListener {
    fun onClick(button: Button) {
        println("Clicked!")
    }
}

如何聲明一個靜態成員,Java 和 Koltin 進行互操作

一個普通的類可以有靜態成員和非靜態成員,在 Kotlin 中我們常把靜態成員放到 companion object 中。

class Foo {
    companion object {
        fun x() { ... }
    }
    fun y() { ... }
}

我們也可以用 object 來聲明一個單例,替換 companion object

object Foo {
    fun x() { ... }
}

如果你不想總是通過類名去調用 Foo.x(), 而只是想使用 x(),可以聲明為頂級函數。

fun x() { ... }

另外想在 Java 中調用 Kotlin 中靜態方法,需要添加 @JvmStatic@JvmName 注解。用一張錶格匯總一下,如何在 Java 中調用 Kotlin 中的代碼。

Function declarationKotlin usageJava usage
Companion objectFoo. F ()Foo. Companion. F ();
Companion object with @JvmStaticFoo. F ()Foo. F ();
ObjectFoo. F ()Foo. INSTANCE. F ();
Object with @JvmStaticFoo. F ()Foo. F ();
Top level functionf ()UtilKt. F ();
Top level function with @JvmNamef ()Util. F ();

同樣的規則也適用於變量,@JvmField 注解用於變量上,加上 const 關鍵字,編譯時可以將常量值內聯到調用處。

Variable declarationKotlin usageJava usage
Companion objectX. XX. Companion. GetX ();
Companion object with @JvmStaticX. XX. GetX ();
Companion object with @JvmFieldX. XX. X;
Companion object with constX. XX. X;
ObjectX. XX. INSTANCE. GetX ();
Object with @JvmStaticX. XX. GetX ();
Object with @JvmFieldX. XX. X
Object with constX. XX. X;
Top level variableX. XConstKt. GetX ();
Top level variable with @JvmFieldX. XConstKt. X;
Top level variable with constxConstKt. X;
Top level variable with @JvmNamexConst. GetX ();
Top level variable with @JvmName and @JvmFieldxConst. X;
Top level variable with @JvmName and constxConst. X;

為什麼 kotlin 中的智能轉換不能用於可變屬性

我們先來看一段有問題的代碼。

class Dog(var toy: Toy? = null) {
    fun play() {
        if (toy != null) {
            toy.chew()
        }
    }
}

上面的代碼在編譯時無法通過,异常信息如下所示。

Kotlin: Smart cast to 'Toy' is impossible, because 'toy' is a mutable property that could have been changed by this time

出現這個問題的原因在於,執行完 toy != null 之後和 toy.chew() 方法被調用之間,這個 Dog 的實例可能被另外一個線程修改,這可能會出現 NullPointerException 异常。

如何才能解决這個問題呢

只需要將變量設置為不可變的,即用 val 聲明,那麼上面的問題就不存在,默認情况將所有的變量都用 val 聲明,除非有必要的時候,才將它們設置為 var

如果一定要聲明為 var ,那麼可以使用局部不可變的副本來解决這個問題。修改一下上面的代碼,如下所示。

class Dog(var toy: Toy? = null) {
    fun play() {
        val _toy = toy
        if (_toy != null) {
            _toy.chew()
        }
    }
}

但是還有一個更簡潔的寫法。

class Dog(var toy: Toy? = null) {
    fun play() {
        toy?.length
    }
}

當重寫 Java 函數時,如何决定參數的可空性

在 Java 中定義一個 OnClickListener 接口,並聲明一個 onClick 的方法。

public interface OnClickListener {
    void onClick(Button button);
}

在 Kotlin 中實現這個接口,並通過 IDEA 自動生成 onClick 方法,將會得到下面的方法簽名,onClick 方法參數默認為可空類型。

class KtListener: OnClickListener {
    override fun onClick(button: Button?): Unit {
        val name = button?.name ?: "Unknown button"
        println("Clicked $name")
    }
}

由於 Java 平臺沒有可空類型,而 Kotlin 中有,在這個例子中 Button 是否為空由我們來决定。默認情况下,對所有參數使用可空類型更安全,編譯器會强制我們處理這些參數。

對於已知的永遠不會空的參數,可以使用非空類型,空和非空都可以正常編譯,但是如果將方法參數聲明為非空,那麼 Kotlin 編譯器會自動注入一個空的檢查,可能會拋出 IllegalArgumentException 异常,潜在的風險很大。當然使用非空參數,代碼將會更加簡潔。

class KtListener: OnClickListener {
    override fun onClick(button: Button): Unit {
        val name = button.name
        println("Clicked $name")
    }
}

如何在一個文件中使用多個具有相同名稱的擴展函數和類

假設在不同的包中對 String 類實現了兩個相同名字的擴展函數,如果是一個普通函數,你可以使用完全限定包名來調用它,但是擴展函數不行。所以我們可以在 import 語句中使用 as 關鍵字對其重命名,代碼如下所示。

import com.example.code.indent as indent4
import com.example.square.indent as indent2

"hello world".indent4()

另外一個案例,想在同一個文件中使用來自不同包中兩個具有相同名稱的類(例如 java.util.Datejava.sql.Date ),並且您不希望通過完全限定包名來調用它們。我們也可以在 import 語句中使用 as 關鍵字對其重命名。

import java.util.Date as UtilDate
import java.sql.Date as SqlDate

現在我們就可以在這個類中,使用通過 as 關鍵字聲明的別名來引用這些類。

譯者

全文到這裏就結束了,這篇文章每個問題,都是一個知識點,後面我會針對每個問題,單獨寫一篇文章,進行更加深入的分析。

如果有幫助點個贊就是對我最大的鼓勵

代碼不止,文章不停


近期必讀熱門文章

版權聲明
本文為[hi-dhl]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/134/202205140557595478.html

隨機推薦