Kotlin 1.1 新功能
發布日期:2016 年 2 月 15 日
目錄
JavaScript
從 Kotlin 1.1 開始,JavaScript 目標不再被視為實驗性功能。所有語言功能都受支援,並且有許多新工具可用於與前端開發環境整合。有關更詳細的變更列表,請參見下方。
協程 (實驗性)
Kotlin 1.1 的關鍵新功能是協程,它帶來了對 async
/await
、yield
和類似程式設計模式的支援。Kotlin 設計的關鍵特點是協程執行的實作是函式庫的一部分,而非語言本身,因此您不受限於任何特定的程式設計典範或並行處理函式庫。
協程實際上是一個輕量級執行緒,可稍後暫停和恢復。協程透過 懸掛函式 支援:呼叫此類函式可能會暫停協程,而要啟動一個新協程,我們通常使用匿名懸掛函式 (即懸掛 lambda 表達式)。
讓我們看看在外部函式庫 kotlinx.coroutines 中實作的 async
/await
:
// runs the code in the background thread pool
fun asyncOverlay() = async(CommonPool) {
// start two async operations
val original = asyncLoadImage("original")
val overlay = asyncLoadImage("overlay")
// and then apply overlay to both results
applyOverlay(original.await(), overlay.await())
}
// launches new coroutine in UI context
launch(UI) {
// wait for async overlay to complete
val image = asyncOverlay().await()
// and then show it in UI
showImage(image)
}
在這裡,async { ... }
啟動一個協程,當我們使用 await()
時,協程的執行會暫停,同時等待的操作被執行,並在等待的操作完成時恢復 (可能在不同的執行緒上)。
標準函式庫使用協程透過 yield
和 yieldAll
函式來支援惰性生成序列。在這樣的序列中,回傳序列元素的程式碼區塊在每個元素被檢索後暫停,並在請求下一個元素時恢復。以下是一個範例:
import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
val seq = buildSequence {
for (i in 1..5) {
// yield a square of i
yield(i * i)
}
// yield a range
yieldAll(26..28)
}
// print the sequence
println(seq.toList())
}
執行上面的程式碼查看結果。您可以隨意編輯並再次執行!
請注意,協程目前被視為一個 實驗性功能,這意味著 Kotlin 團隊不保證支援此功能在最終 1.1 版本發布後的向後相容性。
其他語言功能
型別別名
型別別名允許您為現有型別定義替代名稱。這對於泛型型別 (例如集合) 和函式型別最為有用。以下是一個範例:
typealias OscarWinners = Map<String, String>
fun countLaLaLand(oscarWinners: OscarWinners) =
oscarWinners.count { it.value.contains("La La Land") }
// Note that the type names (initial and the type alias) are interchangeable:
fun checkLaLaLandIsTheBestMovie(oscarWinners: Map<String, String>) =
oscarWinners["Best picture"] == "La La Land"
fun oscarWinners(): OscarWinners {
return mapOf(
"Best song" to "City of Stars (La La Land)",
"Best actress" to "Emma Stone (La La Land)",
"Best picture" to "Moonlight" /* ... */)
}
fun main(args: Array<String>) {
val oscarWinners = oscarWinners()
val laLaLandAwards = countLaLaLand(oscarWinners)
println("LaLaLandAwards = $laLaLandAwards (in our small example), but actually it's 6.")
val laLaLandIsTheBestMovie = checkLaLaLandIsTheBestMovie(oscarWinners)
println("LaLaLandIsTheBestMovie = $laLaLandIsTheBestMovie")
}
繫結可呼叫參考
您現在可以使用 ::
運算子來取得指向特定物件實例方法或屬性的成員參考。以前這只能透過 lambda 表達式來實現。以下是一個範例:
val numberRegex = "\\d+".toRegex()
val numbers = listOf("abc", "123", "456").filter(numberRegex::matches)
fun main(args: Array<String>) {
println("Result is $numbers")
}
密封類別與資料類別
Kotlin 1.1 移除了 Kotlin 1.0 中對密封類別和資料類別的一些限制。現在您可以在同一個檔案的頂層定義頂層密封類別的子類別,而不再僅限於密封類別的巢狀類別。資料類別現在可以擴充其他類別。這可以用來良好而簡潔地定義表達式類別的層次結構:
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
fun eval(expr: Expr): Double = when (expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
}
val e = eval(Sum(Const(1.0), Const(2.0)))
fun main(args: Array<String>) {
println("e is $e") // 3.0
}
更多細節請閱讀密封類別文件或 sealed class 和 data class 的 KEEPs。
Lambda 中的解構宣告
您現在可以使用解構宣告語法來解包傳遞給 lambda 的引數。以下是一個範例:
fun main(args: Array<String>) {
val map = mapOf(1 to "one", 2 to "two")
// before
println(map.mapValues { entry ->
val (key, value) = entry
"$key -> $value!"
})
// now
println(map.mapValues { (key, value) -> "$key -> $value!" })
}
未使用參數的底線
對於具有多個參數的 lambda,您可以使用 _
字元來取代您不使用的參數名稱:
fun main(args: Array<String>) {
val map = mapOf(1 to "one", 2 to "two")
map.forEach { _, value -> println("$value!") }
}
這也適用於解構宣告:
data class Result(val value: Any, val status: String)
fun getResult() = Result(42, "ok").also { println("getResult() returns $it") }
fun main(args: Array<String>) {
val (_, status) = getResult()
println("status is '$status'")
}
更多細節請閱讀 KEEP。
數值字面值中的底線
就像 Java 8 一樣,Kotlin 現在允許在數值字面值中使用底線來分隔數字組:
val oneMillion = 1_000_000
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010
fun main(args: Array<String>) {
println(oneMillion)
println(hexBytes.toString(16))
println(bytes.toString(2))
}
更多細節請閱讀 KEEP。
屬性的簡短語法
對於其 getter 定義為表達式主體的屬性,現在可以省略屬性型別:
data class Person(val name: String, val age: Int) {
val isAdult get() = age >= 20 // Property type inferred to be 'Boolean'
}
fun main(args: Array<String>) {
val akari = Person("Akari", 26)
println("$akari.isAdult = ${akari.isAdult}")
}
行內屬性存取器
如果屬性沒有支援欄位,您現在可以用 inline
修飾符標記屬性存取器。這些存取器的編譯方式與行內函式相同。
public val <T> List<T>.lastIndex: Int
inline get() = this.size - 1
fun main(args: Array<String>) {
val list = listOf('a', 'b')
// the getter will be inlined
println("Last index of $list is ${list.lastIndex}")
}
您也可以將整個屬性標記為 inline
— 這樣修飾符就會應用於兩個存取器。
局部委託屬性
您現在可以在局部變數中使用委託屬性語法。一個可能的用途是定義一個惰性求值的局部變數:
import java.util.Random
fun needAnswer() = Random().nextBoolean()
fun main(args: Array<String>) {
val answer by lazy {
println("Calculating the answer...")
42
}
if (needAnswer()) { // returns the random value
println("The answer is $answer.") // answer is calculated at this point
}
else {
println("Sometimes no answer is the answer...")
}
}
更多細節請閱讀 KEEP。
委託屬性繫結的攔截
對於委託屬性,現在可以使用 provideDelegate
運算子來攔截委託到屬性的繫結。例如,如果我們想在繫結之前檢查屬性名稱,我們可以這樣寫:
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(thisRef: MyUI, prop: KProperty<*>): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
... // property creation
}
private fun checkProperty(thisRef: MyUI, name: String) { ... }
}
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }
class MyUI {
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}
在建立 MyUI
實例期間,provideDelegate
方法將針對每個屬性被呼叫,並且可以立即執行必要的驗證。
更多細節請閱讀委託屬性文件。
泛型列舉值存取
現在可以以泛型方式枚舉列舉類別的值。
enum class RGB { RED, GREEN, BLUE }
inline fun <reified T : Enum<T>> printAllValues() {
print(enumValues<T>().joinToString { it.name })
}
fun main(args: Array<String>) {
printAllValues<RGB>() // prints RED, GREEN, BLUE
}
DSL 中隱式接收者的作用域控制
@DslMarker
註解允許在 DSL 上下文中限制使用來自外部作用域的接收者。考慮標準的 HTML 產生器範例:
table {
tr {
td { + "Text" }
}
}
在 Kotlin 1.0 中,傳遞給 td
的 lambda 中的程式碼可以存取三個隱式接收者:傳遞給 table
的、傳遞給 tr
的和傳遞給 td
的。這允許您呼叫在上下文中毫無意義的方法 — 例如在 td
內部呼叫 tr
,從而在 <td>
中放入 <tr>
標籤。
在 Kotlin 1.1 中,您可以限制這一點,以便只有在 td
的隱式接收者上定義的方法才能在傳遞給 td
的 lambda 內部使用。您透過定義自己的註解,用 @DslMarker
元註解標記它,並將其應用於標籤類別的基底類別來實現。
rem 運算子
現在 mod
運算子已被棄用,改用 rem
。有關動機請參見此問題。
標準函式庫
字串到數字轉換
String 類別上新增了許多擴充函式,可以在數字無效時不拋出例外的情況下將其轉換為數字:String.toIntOrNull(): Int?
、String.toDoubleOrNull(): Double?
等。
val port = System.getenv("PORT")?.toIntOrNull() ?: 80
此外,整數轉換函式,例如 Int.toString()
、String.toInt()
、String.toIntOrNull()
,每個都新增了一個帶有 radix
參數的重載,該參數允許指定轉換的基底 (2 到 36)。
onEach()
onEach
是一個小型但有用的擴充函式,適用於集合和序列,它允許在操作鏈中對集合/序列的每個元素執行某些動作,可能帶有副作用。在可迭代物件上,它的行為類似 forEach
但也會進一步回傳該可迭代物件的實例。而在序列上,它會回傳一個包裝序列,該序列會在元素被迭代時惰性地應用給定的動作。
inputDir.walk()
.filter { it.isFile && it.name.endsWith(".txt") }
.onEach { println("Moving $it to $outputDir") }
.forEach { moveFile(it, File(outputDir, it.toRelativeString(inputDir))) }
also()、takeIf() 和 takeUnless()
這三個是適用於任何接收者的通用擴充函式。
also
類似於 apply
:它接收接收者,對其執行一些動作,然後回傳該接收者。不同之處在於,在 apply
內部區塊中,接收者以 this
形式可用,而在 also
內部區塊中,則以 it
形式可用 (如果您願意,也可以為它取另一個名稱)。當您不想遮蔽外部作用域的 this
時,這會很方便:
class Block {
lateinit var content: String
}
fun Block.copy() = Block().also {
it.content = this.content
}
// using 'apply' instead
fun Block.copy1() = Block().apply {
this.content = this@copy1.content
}
fun main(args: Array<String>) {
val block = Block().apply { content = "content" }
val copy = block.copy()
println("Testing the content was copied:")
println(block.content == copy.content)
}
takeIf
類似於單一值的 filter
。它檢查接收者是否符合述詞 (predicate),如果符合則回傳接收者,否則回傳 null
。結合 elvis 運算子 (?😃 和提前回傳,它允許編寫以下結構:
val outDirFile = File(outputDir.path).takeIf { it.exists() } ?: return false
// do something with existing outDirFile
fun main(args: Array<String>) {
val input = "Kotlin"
val keyword = "in"
val index = input.indexOf(keyword).takeIf { it >= 0 } ?: error("keyword not found")
// do something with index of keyword in input string, given that it's found
println("'$keyword' was found in '$input'")
println(input)
println(" ".repeat(index) + "^")
}
takeUnless
與 takeIf
相同,但它接受反向的述詞。當接收者不符合述詞時,它會回傳接收者,否則回傳 null
。因此,上述其中一個範例可以用 takeUnless
改寫如下:
val index = input.indexOf(keyword).takeUnless { it < 0 } ?: error("keyword not found")
當您有可呼叫參考而非 lambda 表達式時,使用它也很方便:
private fun testTakeUnless(string: String) {
val result = string.takeUnless(String::isEmpty)
println("string = \"$string\"; result = \"$result\"")
}
fun main(args: Array<String>) {
testTakeUnless("")
testTakeUnless("abc")
}
groupingBy()
此 API 可用於按鍵對集合進行分組,並同時摺疊每個組。例如,它可以用來計算每個字母開頭的單字數量:
fun main(args: Array<String>) {
val words = "one two three four five six seven eight nine ten".split(' ')
val frequencies = words.groupingBy { it.first() }.eachCount()
println("Counting first letters: $frequencies.")
// The alternative way that uses 'groupBy' and 'mapValues' creates an intermediate map,
// while 'groupingBy' way counts on the fly.
val groupBy = words.groupBy { it.first() }.mapValues { (_, list) -> list.size }
println("Comparing the result with using 'groupBy': ${groupBy == frequencies}.")
}
Map.toMap() 和 Map.toMutableMap()
這些函式可用於輕鬆複製映射 (map):
class ImmutablePropertyBag(map: Map<String, Any>) {
private val mapCopy = map.toMap()
}
Map.minus(key)
plus
運算子提供了一種將鍵值對新增到唯讀映射以產生新映射的方法,然而以前沒有一種簡單的方法可以執行相反操作:若要從映射中移除一個鍵,您必須訴諸較不直接的方式,例如 Map.filter()
或 Map.filterKeys()
。現在 minus
運算子填補了這個空白。提供 4 個重載:用於移除單一鍵、鍵的集合、鍵的序列和鍵的陣列。
fun main(args: Array<String>) {
val map = mapOf("key" to 42)
val emptyMap = map - "key"
println("map: $map")
println("emptyMap: $emptyMap")
}
minOf() 和 maxOf()
這些函式可用於找出兩個或三個給定值中的最低和最高值,其中值是基本數字或 Comparable
物件。如果想比較本身不可比較的物件,每個函式也有一個重載,接受一個額外的 Comparator
實例。
fun main(args: Array<String>) {
val list1 = listOf("a", "b")
val list2 = listOf("x", "y", "z")
val minSize = minOf(list1.size, list2.size)
val longestList = maxOf(list1, list2, compareBy { it.size })
println("minSize = $minSize")
println("longestList = $longestList")
}
類似陣列的 List 實例化函式
類似於 Array
建構函式,現在有函式可以建立 List
和 MutableList
實例,並透過呼叫 lambda 來初始化每個元素:
fun main(args: Array<String>) {
val squares = List(10) { index -> index * index }
val mutable = MutableList(10) { 0 }
println("squares: $squares")
println("mutable: $mutable")
}
Map.getValue()
這個 Map
的擴充函式會回傳對應於給定鍵的現有值,如果找不到該鍵則拋出例外,並說明是哪個鍵未找到。如果映射是透過 withDefault
產生的,此函式將回傳預設值而不是拋出例外。
fun main(args: Array<String>) {
val map = mapOf("key" to 42)
// returns non-nullable Int value 42
val value: Int = map.getValue("key")
val mapWithDefault = map.withDefault { k -> k.length }
// returns 4
val value2 = mapWithDefault.getValue("key2")
// map.getValue("anotherKey") // <- this will throw NoSuchElementException
println("value is $value")
println("value2 is $value2")
}
抽象集合
這些抽象類別在實作 Kotlin 集合類別時可用作基底類別。對於實作唯讀集合,有 AbstractCollection
、AbstractList
、AbstractSet
和 AbstractMap
;對於可變集合,則有 AbstractMutableCollection
、AbstractMutableList
、AbstractMutableSet
和 AbstractMutableMap
。在 JVM 上,這些抽象可變集合的大部分功能繼承自 JDK 的抽象集合。
陣列操作函式
標準函式庫現在提供了一組用於陣列上逐元素操作的函式:比較 (contentEquals
和 contentDeepEquals
)、雜湊碼計算 (contentHashCode
和 contentDeepHashCode
),以及轉換為字串 (contentToString
和 contentDeepToString
)。它們同時支援 JVM (在 JVM 上,它們是 java.util.Arrays
中對應函式的別名) 和 JS (在 JS 上,實作由 Kotlin 標準函式庫提供)。
fun main(args: Array<String>) {
val array = arrayOf("a", "b", "c")
println(array.toString()) // JVM implementation: type-and-hash gibberish
println(array.contentToString()) // nicely formatted as list
}
JVM 後端
Java 8 位元組碼支援
Kotlin 現在可以選擇生成 Java 8 位元組碼 (透過 -jvm-target 1.8
命令列選項或 Ant/Maven/Gradle 中對應的選項)。目前這不會改變位元組碼的語義 (特別是,介面中的預設方法和 lambda 的生成方式與 Kotlin 1.0 完全相同),但我們計劃稍後進一步利用此功能。
Java 8 標準函式庫支援
現在標準函式庫有獨立的版本,支援 Java 7 和 8 中新增的 JDK API。如果您需要存取新的 API,請使用 kotlin-stdlib-jre7
和 kotlin-stdlib-jre8
Maven Artifact 而不是標準的 kotlin-stdlib
。這些 Artifact 是在 kotlin-stdlib
之上進行的微小擴充,它們會將其作為傳遞性依賴項引入您的專案。
位元組碼中的參數名稱
Kotlin 現在支援在位元組碼中儲存參數名稱。這可以透過使用 -java-parameters
命令列選項來啟用。
常數行內化
編譯器現在會將 const val
屬性的值行內化到使用它們的位置。
可變閉包變數
用於在 lambda 中捕捉可變閉包變數的 box 類別不再具有 volatile 欄位。此變更提升了效能,但在某些罕見的使用情境中可能導致新的競爭條件 (race conditions)。如果您受此影響,您需要提供自己的同步機制來存取變數。
javax.script 支援
Kotlin 現在與 javax.script API (JSR-223) 整合。該 API 允許在執行時期評估程式碼片段:
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
engine.eval("val x = 3")
println(engine.eval("x + 2")) // Prints out 5
有關使用該 API 的更大範例專案,請參閱此處。
kotlin.reflect.full
為了準備支援 Java 9,kotlin-reflect.jar
函式庫中的擴充函式和屬性已移至 kotlin.reflect.full
套件。舊套件 (kotlin.reflect
) 中的名稱已被棄用,並將在 Kotlin 1.2 中移除。請注意,核心反射介面 (例如 KClass
) 是 Kotlin 標準函式庫的一部分,而非 kotlin-reflect
,因此不受此移動的影響。
JavaScript 後端
統一的標準函式庫
現在,Kotlin 標準函式庫的更大一部分可以從編譯為 JavaScript 的程式碼中使用。特別是,集合 (ArrayList
、HashMap
等)、例外 (IllegalArgumentException
等) 和其他一些 (例如 StringBuilder
、Comparator
) 等關鍵類別現在都定義在 kotlin
套件下。在 JVM 上,這些名稱是相應 JDK 類別的型別別名;而在 JS 上,這些類別則是在 Kotlin 標準函式庫中實作。
更好的程式碼生成
JavaScript 後端現在生成更多靜態可檢查的程式碼,這對 JS 程式碼處理工具 (例如壓縮工具、最佳化工具、程式碼檢查工具等) 更友善。
external 修飾符
如果您需要以型別安全的方式從 Kotlin 存取在 JavaScript 中實作的類別,您可以使用 external
修飾符來編寫 Kotlin 宣告。(在 Kotlin 1.0 中,使用的是 @native
註解。)與 JVM 目標不同,JS 目標允許對類別和屬性使用 external 修飾符。例如,您可以這樣宣告 DOM Node
類別:
external class Node {
val firstChild: Node
fun appendChild(child: Node): Node
fun removeChild(child: Node): Node
// etc
}
改進的匯入處理
您現在可以更精確地描述應從 JavaScript 模組匯入的宣告。如果您在外部宣告上新增 @JsModule("<module-name>")
註解,它將在編譯期間正確匯入到模組系統 (CommonJS 或 AMD)。例如,使用 CommonJS 時,宣告將透過 require(...)
函式匯入。此外,如果您想將宣告匯入為模組或全域 JavaScript 物件,您可以使用 @JsNonModule
註解。
例如,您可以這樣將 JQuery 匯入 Kotlin 模組:
external interface JQuery {
fun toggle(duration: Int = definedExternally): JQuery
fun click(handler: (Event) -> Unit): JQuery
}
@JsModule("jquery")
@JsNonModule
@JsName("$")
external fun jquery(selector: String): JQuery
在本例中,JQuery 將作為名為 jquery
的模組匯入。或者,它也可以用作一個 $ 物件,這取決於 Kotlin 編譯器配置為使用哪個模組系統。
您可以在應用程式中這樣使用這些宣告:
fun main(args: Array<String>) {
jquery(".toggle-button").click {
jquery(".toggle-panel").toggle(300)
}
}