Kotlin 1.1 新功能
發布日期:2016 年 2 月 15 日
目錄
JavaScript
從 Kotlin 1.1 開始,JavaScript 目標不再被視為實驗性功能。所有語言功能都獲得支援,並有許多新工具用於整合前端開發環境。請參閱下方以獲取更詳細的變更列表。
協程 (實驗性)
Kotlin 1.1 的關鍵新功能是 協程 (coroutines),它帶來了 async
/await
、yield
和類似程式設計模式的支援。Kotlin 設計的關鍵特點是協程執行的實作是函式庫的一部分,而非語言本身,因此您不受任何特定程式設計範式或併發函式庫的約束。
協程實際上是一個輕量級執行緒,可以暫停並稍後恢復。協程透過 suspend 函數 獲得支援:呼叫此類函數可能會暫停協程,而要啟動一個新的協程,我們通常使用匿名 suspend 函數(即 suspend lambda 表達式)。
讓我們看看 async
/await
,它是在外部函式庫 kotlinx.coroutines 中實作的:
// 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
函數支援 惰性生成的序列 (lazily generated sequences)。在此類序列中,返回序列元素的程式碼塊在每個元素被檢索後暫停,並在請求下一個元素時恢復。這是一個範例:
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
}
閱讀密封類別文件或有關密封類別和資料類別的 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)
}
provideDelegate
方法將在 MyUI
實例建立期間為每個屬性呼叫,並且它可以立即執行必要的驗證。
閱讀委託屬性文件以獲取更多詳細資訊。
泛型列舉值存取
現在可以以泛型方式列舉列舉類別的值。
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
。它檢查接收者是否符合判斷式,如果符合則返回接收者,否則返回 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 添加鍵值對並產生新 Map 的方式,但之前沒有簡單的方法來執行相反的操作:要從 Map 中移除一個鍵,您必須訴諸於較不直接的方式,例如 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
上的這個擴展函數返回與給定鍵對應的現有值,如果找不到該鍵則拋出異常並提及哪個鍵未找到。如果 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(在此處它們充當 java.util.Arrays
中相應函數的別名)和 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 構件,而不是標準的 kotlin-stdlib
。這些構件是 kotlin-stdlib
之上的微小擴展,它們將其作為傳遞依賴項引入您的專案。
位元碼中的參數名稱
Kotlin 現在支援在位元碼中儲存參數名稱。這可以使用 -java-parameters
命令列選項啟用。
常數內聯
編譯器現在將 const val
屬性的值內聯到它們被使用的位置。
可變閉包變數
用於在 lambda 表達式中捕獲可變閉包變數的 box 類別不再具有 volatile 欄位。此更改提高了效能,但在某些罕見的使用場景中可能導致新的競爭條件。如果您受到此影響,則需要為存取變數提供自己的同步機制。
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 後端
統一標準函式庫
現在可以從編譯為 JavaScript 的程式碼中使用更大一部分 Kotlin 標準函式庫。特別是,關鍵類別如集合 (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
的模組導入。或者,它可以作為一個 `-object 使用,這取決於 Kotlin 編譯器配置為使用的模組系統。
您可以在應用程式中這樣使用這些宣告:
fun main(args: Array<String>) {
jquery(".toggle-button").click {
jquery(".toggle-panel").toggle(300)
}
}