Kotlin语言文档


Kotlin 语⾔⽂档Kotlin 语⾔⽂档 ⽬录⽬录 概述概述 使⽤ Kotlin 进⾏服务器端开发 使⽤ Kotlin 进⾏ Android 开发 Kotlin JavaScript 概述 Kotlin 1.1 的新特性 开始开始 基本语法 习惯⽤法 编码规范 基础基础 基本类型 包 控制流 返回和跳转 类和对象类和对象 类和继承 属性和字段 接⼝ 可⻅性修饰符 扩展 数据类 密封类 泛型 嵌套类 枚举类 对象表达式和对象声明 委托 委托属性 函数和 Lambda 表达式函数和 Lambda 表达式 函数 ⾼阶函数和 lambda 表达式 内联函数 协程 其他其他 解构声明 集合 区间 4 4 5 6 7 15 15 20 24 26 26 31 33 36 38 38 42 45 47 49 53 54 55 60 61 63 66 67 72 72 77 81 84 88 88 90 91 2 类型的检查与转换 This 表达式 相等性 操作符符重载 空安全 异常 注解 反射 类型安全的构建器 参考参考 Grammar Notation Semicolons Syntax Lexical structure 兼容性 Java 互操作Java 互操作 在 Kotlin 中调⽤ Java 代码 Java 中调⽤ Kotlin JavaScriptJavaScript 动态类型 Kotlin 中调⽤ JavaScript JavaScript 中调⽤ Kotlin JavaScript 模块 JavaScript 反射 ⼯具⼯具 编写 Kotlin 代码⽂档 使⽤ Kotlin 注解处理⼯具 使⽤ Gradle 使⽤ Maven 使⽤ Ant Kotlin 与 OSGi 编译器插件 常⻅问题常⻅问题 FAQ 与 Java 语⾔⽐较 与 Scala ⽐较【官⽅已删除】 93 95 96 97 101 103 105 109 112 118 118 118 118 118 126 127 130 130 137 143 143 145 148 150 153 154 154 157 159 164 169 172 173 177 177 180 181 3 概述概述 Kotlin ⾮常适合开发服务器端应⽤程序,允许编写简明且表现⼒强的代码, 同时保持与现有基于 Java 的技术栈的完全兼容性以及平滑的学习曲线: 表现⼒表现⼒:Kotlin 的⾰新式语⾔功能,例如⽀持类型安全的构建器 和委托属性,有助于构建强⼤⽽易于使⽤的抽象。 可伸缩性可伸缩性:Kotlin 对协程的⽀持有助于构建服务器端应⽤程序, 伸缩到适度的硬件要求以应对⼤量的客⼾端。 互操作性互操作性:Kotlin 与所有基于 Java 的框架完全兼容,可以让你保持 熟悉的技术栈,同时获得更现代化语⾔的优势。 迁移迁移:Kotlin ⽀持⼤型代码库从 Java 到 Kotlin 逐步迁移。你可以开始 ⽤ Kotlin 编写新代码,同时系统中较旧部分继续⽤ Java。 ⼯具⼯具:除了很棒的 IDE ⽀持之外,Kotlin 还为 IntelliJ IDEA Ultimate 的插件提供了框架特定的⼯具(例如 Spring)。 学习曲线学习曲线:对于 Java 开发⼈员,Kotlin ⼊⻔很容易。包含在 Kotlin 插件中的⾃动 Java 到 Kotlin 的转换器 有助于迈出第⼀步。Kotlin ⼼印 通过⼀系 列互动练习提供了语⾔主要功能的指南。 Spring 利⽤ Kotlin 的语⾔功能提供更简洁的 API, 从版本 5.0 开始。在线项⽬⽣成器允许⽤ Kotlin 快速⽣成⼀个新项⽬。 Vert.x 是在 JVM 上构建响应式 Web 应⽤程序的框架, 为 Kotlin 提供了专⻔⽀持,包括完整的⽂档。 Ktor 是由 JetBrains 构建的 Kotlin 原⽣ Web 框架,利⽤协程实现 ⾼可伸缩性,并提供易于使⽤且合乎惯⽤法的 API。 kotlinx.html 是可在 Web 应⽤程序中⽤于构建 HTML 的 DSL。 它可以作为传统模板系统(如JSP和FreeMarker)的替代品。 通过相应 Java 驱动程序进⾏持久化的可⽤选项包括直接 JDBC 访问、JPA 以及使⽤ NoSQL 数据库。 对于 JPA,kotlin-jpa 编译器插件使 Kotlin 编 译的类适应框架的要求。 Kotlin 应⽤程序可以部署到⽀持 Java Web 应⽤程序的任何主机,包括 Amazon Web Services、 Google Cloud Platform 等。 这篇博⽂提供了在 Heroku上部署 Kotlin 应⽤程序的指南。 AWS Labs 提供了⼀个⽰例项⽬,展⽰了 Kotlin 编写 AWS Lambda 函数的使⽤。 Corda 是⼀个开源的分布式分类帐平台,由各⼤银⾏提供⽀持 ,完全由 Kotlin 构建。 JetBrains 账⼾,负责 JetBrains 整个许可证销售和验证 过程的系统 100% 由 Kotlin 编写,⾃ 2015 年⽣产运⾏以来,⼀直没有重⼤问题。 使⽤ Http Servlet 创建 Web 应⽤程序和 使⽤ Spring Boot 创建 RESTful Web 服务教程 将向你展⽰如何在 Kotlin 中构建和运⾏⾮常⼩的 Web 应 ⽤程序。 关于更深⼊的介绍,请查看本站的参考⽂档及 Kotlin ⼼印。 使⽤ Kotlin 进⾏服务器端开发 — — — — — — 使⽤ Kotlin 进⾏服务器端开发的框架 — — — — — 部署 Kotlin 服务器端应⽤程序 Kotlin ⽤于服务器端的⽤⼾ 下⼀步 — — 4 Kotlin ⾮常适合开发 Android 应⽤程序,将现代语⾔的所有优势带⼊ Android 平台⽽不会引⼊任何新的限制: 兼容性兼容性:Kotlin 与 JDK 6 完全兼容,保障了 Kotlin 应⽤程序可以在较旧的 Android 设备上运⾏⽽⽆任何问题。Kotlin ⼯具在 Android Studio 中会完 全⽀持,并且兼容 Android 构建系统。 性能性能:由于⾮常相似的字节码结构,Kotlin 应⽤程序的运⾏速度与 Java 类似。 随着 Kotlin 对内联函数的⽀持,使⽤ lambda 表达式的代码通常⽐⽤ Java 写的代码运⾏得更快。 互操作性互操作性:Kotlin 可与 Java 进⾏ 100% 的互操作,允许在 Kotlin 应⽤程序中使⽤所有现有的 Android 库 。这包括注解处理,所以数据绑定和 Dagger 也是⼀样。 占⽤占⽤:Kotlin 具有⾮常紧凑的运⾏时库,可以通过使⽤ ProGuard 进⼀步减少。 在实际应⽤程序中,Kotlin 运⾏时 只增加⼏百个⽅法以及 .apk ⽂件 不到 100K ⼤⼩。 编译时⻓编译时⻓:Kotlin ⽀持⾼效的增量编译,所以对于 清理构建会有额外的开销,增量构建通常与 Java ⼀样快或者更快。 学习曲线学习曲线:对于 Java 开发⼈员,Kotlin ⼊⻔很容易。包含在 Kotlin 插件中的⾃动 Java 到 Kotlin 的转换器 有助于迈出第⼀步。Kotlin ⼼印 通过⼀系 列互动练习提供了语⾔主要功能的指南。 Kotlin 已被⼀些⼤公司成功采⽤,其中⼀些公司分享了他们的经验: Pinterest 已经成功地将 Kotlin 引⼊了他们的应⽤程序中,每个⽉有 1 亿 5 千万⼈使⽤。 Basecamp 的 Android 应⽤程序是 100% Kotlin 代码,他们报告了程序员幸福的 巨⼤差异,以及⼯作质量和速度的巨⼤改善。 Keepsafe 的 App Lock 应⽤程序也已转换为 100% Kotlin, 使源代码⾏数减少 30%、⽅法数减少 10%。 Kotlin 团队为 Android 开发提供了⼀套超越标准语⾔功能的⼯具: Kotlin Android 扩展是⼀个编译器扩展, 可以让你摆脱代码中的 findViewById() 调⽤,并将其替换为合成的编译器⽣成的 属性。 Anko 是⼀个提供围绕 Android API 的 Kotlin 友好的包装器的库 ,以及⼀个可以⽤ Kotlin 代码替换布局 .xml ⽂件的 DSL。 下载并安装 Android Studio 3.0 预览版,其中包含开箱即⽤的 Kotlin ⽀持。 按照 Android 与 Kotlin ⼊⻔教程 创建你的第⼀个 Kotlin 应⽤程序。 关于更深⼊的介绍,请查看本站的参考⽂档及 Kotlin ⼼印。 另⼀个很好的资源是 Kotlin for Android Developers, 这本书会引导你逐步完成在 Kotlin 中创建真正的 Android 应⽤程序的过程。 检出 Google 的 Kotlin 写的⽰例项⽬。 使⽤ Kotlin 进⾏ Android 开发 — — — — — — Kotlin ⽤于 Android 的案例学习 — — — ⽤于 Android 开发的⼯具 — — 下⼀步 — — — — — 5 Kotlin 提供了 JavaScript 作为⽬标平台的能⼒。它通过将 Kotlin 转换为 JavaScript 来实现。⽬前的实现⽬标是 ECMAScript 5.1,但也有 最终⽬标为 ECMAScript 2015 的计划。 当你选择 JavaScript ⽬标时,作为项⽬⼀部分的任何 Kotlin 代码以及 Kotlin 附带的标准库都会转换为 JavaScript。 但是,这不包括使⽤的 JDK 和任何 JVM 或 Java 框架或库。任何不是 Kotlin 的⽂件会在编译期间忽略掉。 Kotlin 编译器努⼒遵循以下⽬标: 提供最佳⼤⼩的输出 提供可读的 JavaScript 输出 提供与现有模块系统的互操作性 在标准库中提供相同的功能,⽆论是 JavaScript 还是 JVM ⽬标(尽最⼤可能程度)。 你可能希望在以下情景中将 Kotlin 编译为 JavaScript: 创建⾯向客⼾端 JavaScript 的 Kotlin 代码 与 DOM 元素交互与 DOM 元素交互。Kotlin 提供了⼀系列静态类型的接⼝来与⽂档对象模型(Document Object Model)交互,允许创建和更新 DOM 元素。 与图形如 WebGL 交互与图形如 WebGL 交互。你可以使⽤ Kotlin 在⽹⻚上⽤ WebGL 创建图形元素。 创建⾯向服务器端 JavaScript 的 Kotlin 代码 使⽤服务器端技术使⽤服务器端技术。你可以使⽤ Kotlin 与服务器端 JavaScript(如 node.js)进⾏交互 Kotlin 可以与现有的第三⽅库和框架(如 JQuery 或 ReactJS)⼀起使⽤。要使⽤强类型 API 访问第三⽅框架,可以使⽤ ts2kt ⼯具将 TypeScript 定义从 Definitely Typed 类型定义仓库转换为 Kotlin。或者,你可以使⽤ 动态类型访问任何框架,⽽⽆需强类型。 Kotlin 还兼容 CommonJS、AMD 和 UMD,直截了当与不同的模块系统交互。 要了解如何开始使⽤ JavaScript 平台的 Kotlin,请参考其教程。 Kotlin JavaScript 概述 — — — — 如何使⽤ — — — — — Kotlin 转 JavaScript ⼊⻔ 6 协程 其他语⾔功能 标准库 JVM 后端 JavaScript 后端 从 Kotlin 1.1 开始,JavaScript ⽬标平台不再当是实验性的。所有语⾔功能都⽀持, 并且有许多新的⼯具⽤于与前端开发环境集成。更详细改动列表,请参 ⻅下⽂ 。 Kotlin 1.1 的关键新特性是协程,它带来了 future / await 、 yield 以及类似的编程模式的 ⽀持。Kotlin 的设计中的关键特性是协程执⾏的实现是语 ⾔库的⼀部分, ⽽不是语⾔的⼀部分,所以你不必绑定任何特定的编程范式或并发库。 协程实际上是⼀个轻量级的线程,可以挂起并稍后恢复。协程通过挂起函数⽀持:对这样的函数的调⽤可能会挂起协程,并启动⼀个新的协程,我们通常使 ⽤匿名挂起函数(即挂起 lambda 表达式)。 我们来看看在外部库 kotlinx.coroutines 中实现的 async / await : // 在后台线程池中运⾏该代码 fun asyncOverlay() = async(CommonPool) { // 启动两个异步操作 val original = asyncLoadImage(“original“) val overlay = asyncLoadImage(“overlay“) // 然后应⽤叠加到两个结果 applyOverlay(original.await(), overlay.await()) } // 在 UI 上下⽂中启动新的协程 launch(UI) { // 等待异步叠加完成 val image = asyncOverlay().await() // 然后在 UI 中显⽰ showImage(image) } 这⾥,async { …… } 启动⼀个协程,当我们使⽤ await() 时,挂起协程的执⾏,⽽执⾏正在等待的操作,并且在等待的操作完成时恢复(可能在不同 的线程上) 。 标准库通过 yield 和 yieldAll 函数使⽤协程来⽀持惰性⽣成序列。 在这样的序列中,在取回每个元素之后挂起返回序列元素的代码块, 并在请求下 ⼀个元素时恢复。这⾥有⼀个例⼦: val seq = buildSequence { for (i in 1..5) { // 产⽣⼀个 i 的平⽅ yield(i * i) } // 产⽣⼀个区间 yieldAll(26..28) } // 输出该序列 println(seq.toList()) 运⾏上⾯的代码以查看结果。随意编辑它并再次运⾏! 更多信息请参⻅协程⽂档及教程。 请注意,协程⽬前还是⼀个实验性的功能实验性的功能,这意味着 Kotlin 团队不承诺 在最终的 1.1 版本时保持该功能的向后兼容性。 Kotlin 1.1 的新特性 ⽬录 — — — — — JavaScript 协程(实验性的) 7 类型别名允许你为现有类型定义备⽤名称。 这对于泛型类型(如集合)以及函数类型最有⽤。 这⾥有⼏个例⼦: typealias OscarWinners = Map fun countLaLaLand(oscarWinners: OscarWinners) = oscarWinners.count { it.value.contains(“La La Land“) } // 请注意,类型名称(初始名和类型别名)是可互换的: fun checkLaLaLandIsTheBestMovie(oscarWinners: Map) = oscarWinners[“Best picture“] == “La La Land“ 更详细信息请参阅其 KEEP。 现在可以使⽤ :: 操作符来获取指向特定对象实例的⽅法或属性的成员引⽤。 以前这只能⽤ lambda 表达式表⽰。 这⾥有⼀个例⼦: val numberRegex = “\\d+“.toRegex() val numbers = listOf(“abc“, “123“, “456“).filter(numberRegex::matches) 更详细信息请参阅其 KEEP。 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))) 更详细信息请参阅其⽂档或者 密封类 及 数据类的 KEEP。 现在可以使⽤解构声明语法来解开传递给 lambda 表达式的参数。 这⾥有⼀个例⼦: val map = mapOf(1 to “one“, 2 to “two“) // 之前 println(map.mapValues { entry -> val (key, value) = entry “$key -> $value!“ }) // 现在 println(map.mapValues { (key, value) -> “$key -> $value!“ }) 更详细信息请参阅其⽂档及其 KEEP。 对于具有多个参数的 lambda 表达式,可以使⽤ _ 字符替换不使⽤的参数的名称: 其他语⾔功能 类型别名类型别名 已绑定的可调⽤引⽤已绑定的可调⽤引⽤ 密封类和数据类密封类和数据类 lambda 表达式中的解构表达式中的解构 下划线⽤于未使⽤的参数下划线⽤于未使⽤的参数 8 map.forEach { _, value -> println(“$value!“) } 这也适⽤于解构声明: val (_, status) = getResult() 更详细信息请参阅其 KEEP。 正如在 Java 8 中⼀样,Kotlin 现在允许在数字字⾯值中使⽤下划线来分隔数字分组: val oneMillion = 1_000_000 val hexBytes = 0xFF_EC_DE_5E val bytes = 0b11010010_01101001_10010100_10010010 更详细信息请参阅其 KEEP。 对于没有⾃定义访问器、或者将 getter 定义为表达式主体的属性,现在可以省略属性的类型: data class Person(val name: String, val age: Int) { val isAdult get() = age >= 20 // 属性类型推断为 “Boolean” } 如果属性没有幕后字段,现在可以使⽤ inline 修饰符来标记该属性访问器。 这些访问器的编译⽅式与内联函数相同。 public val List.lastIndex: Int inline get() = this.size - 1 你也可以将整个属性标记为 inline ⸺这样修饰符应⽤于两个访问器。 更详细信息请参阅其⽂档及其 KEEP。 现在可以对局部变量使⽤委托属性语法。 ⼀个可能的⽤途是定义⼀个延迟求值的局部变量: val answer by lazy { println(“Calculating the answer...“) 42 } if (needAnswer()) { // 返回随机值 println(“The answer is $answer.“) // 此时计算出答案 } else { println(“Sometimes no answer is the answer...“) } 更详细信息请参阅其 KEEP。 对于委托属性,现在可以使⽤ provideDelegate 操作符拦截委托到属性之间的绑定 。 例如,如果我们想要在绑定之前检查属性名称,我们可以这样写: 数字字⾯值中的下划线数字字⾯值中的下划线 对于属性的更短语法对于属性的更短语法 内联属性访问器内联属性访问器 局部委托属性局部委托属性 委托属性绑定的拦截委托属性绑定的拦截 9 class ResourceLoader(id: ResourceID) { operator fun provideDelegate(thisRef: MyUI, property: KProperty<*>): ReadOnlyProperty { checkProperty(thisRef, property.name) …… // 属性创建 } private fun checkProperty(thisRef: MyUI, name: String) { …… } } fun bindResource(id: ResourceID): ResourceLoader { …… } 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 > printAllValues() { print(enumValues().joinToString { it.name }) } @DslMarker 注解允许限制来⾃ DSL 上下⽂中的外部作⽤域的接收者的使⽤。 考虑那个典型的 HTML 构建器⽰例: table { tr { td { +“Text“ } } } 在 Kotlin 1.0 中,传递给 td 的 lambda 表达式中的代码可以访问三个隐式接收者:传递给 table 、tr 和 td 的。 这允许你调⽤在上下⽂中没有意义 的⽅法⸺例如在 td ⾥⾯调⽤ tr ,从⽽ 在 中放置⼀个 标签。 在 Kotlin 1.1 中,你可以限制这种情况,以使只有在 td 的隐式接收者上定义的⽅法 会在传给 td 的 lambda 表达式中可⽤。你可以通过定义标记有 @DslMarker 元注解的注解 并将其应⽤于标记类的基类。 更详细信息请参阅其⽂档及其 KEEP。 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)。 泛型枚举值访问泛型枚举值访问 对于对于 DSL 中隐式接收者的作⽤域控制中隐式接收者的作⽤域控制 rem 操作符操作符 标准库 字符串到数字的转换字符串到数字的转换 10 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 就像 apply :它接受接收者、做⼀些动作、并返回该接收者。 ⼆者区别是在 apply 内部的代码块中接收者是 this , ⽽在 also 内部的代码块 中是 it(并且如果你想的话,你可以给它另⼀个名字)。 当你不想掩盖来⾃外部作⽤域的 this 时这很⽅便: fun Block.copy() = Block().also { it.content = this.content } takeIf 就像单个值的 filter 。它检查接收者是否满⾜该谓词,并 在满⾜时返回该接收者否则不满⾜时返回 null 。 结合 elvis-操作符和及早返回, 它允许编写如下结构: val outDirFile = File(outputDir.path).takeIf { it.exists() } ?: return false // 对现有的 outDirFile 做些事情 val index = input.indexOf(keyword).takeIf { it >= 0 } ?: error(“keyword not found“) // 对输⼊字符串中的关键字索引做些事情,鉴于它已找到 takeUnless 与 takeIf 相同,只是它采⽤了反向谓词。当它 不 满⾜谓词时返回接收者,否则返回 null 。因此,上⾯的⽰例之⼀可以⽤ takeUnless 重写如下: val index = input.indexOf(keyword).takeUnless { it < 0 } ?: error(“keyword not found“) 当你有⼀个可调⽤的引⽤⽽不是 lambda 时,使⽤也很⽅便: val result = string.takeUnless(String::isEmpty) 此 API 可以⽤于按照键对集合进⾏分组,并同时折叠每个组。 例如,它可以⽤于 计算⽂本中字符的频率: val frequencies = words.groupingBy { it.first() }.eachCount() 这俩函数可以⽤来简易复制映射: class ImmutablePropertyBag(map: Map) { private val mapCopy = map.toMap() } 运算符 plus 提供了⼀种将键值对添加到只读映射中以⽣成新映射的⽅法,但是没有⼀种简单的⽅法来做相反的操作:从映射中删除⼀个键采⽤不那么 直接的⽅式如 Map.filter() 或 Map.filterKeys() 。 现在运算符 minus 填补了这个空⽩。有 4 个可⽤的重载:⽤于删除单个键、键的集合、键的 序列和键的数组。 onEach() also()、、takeIf() 和和 takeUnless() groupingBy() Map.toMap() 和和 Map.toMutableMap() Map.minus(key) 11 val map = mapOf(“key“ to 42) val emptyMap = map - “key“ 这些函数可⽤于查找两个或三个给定值中的最⼩和最⼤值,其中值是原⽣数字或 Comparable 对象。每个函数还有⼀个重载,它接受⼀个额外的 Comparator 实例,如果你想⽐较⾃⾝不可⽐的对象的话。 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 }) 类似于 Array 构造函数,现在有创建 List 和 MutableList 实例的函数,并通过 调⽤ lambda 表达式来初始化每个元素: val squares = List(10) { index -> index * index } val mutable = MutableList(10) { 0 } Map 上的这个扩展函数返回⼀个与给定键相对应的现有值,或者抛出⼀个异常,提⽰找不到该键。 如果该映射是⽤ withDefault ⽣成的,这个函数将 返回默认值,⽽不是抛异常。 val map = mapOf(“key“ to 42) // 返回不可空 Int 值 42 val value: Int = map.getValue(“key“) val mapWithDefault = map.withDefault { k -> k.length } // 返回 4 val value2 = mapWithDefault.getValue(“key2“) // map.getValue(“anotherKey“) // <- 这将抛出 NoSuchElementException 这些抽象类可以在实现 Kotlin 集合类时⽤作基类。 对于实现只读集合,有 AbstractCollection 、 AbstractList 、 AbstractSet 和 AbstractMap , ⽽对于可变集合,有 AbstractMutableCollection 、 AbstractMutableList 、 AbstractMutableSet 和 AbstractMutableMap 。 在 JVM 上,这些抽象可变集合从 JDK 的抽象集合继承了⼤部分的功能。 标准库现在提供了⼀组⽤于逐个元素操作数组的函数:⽐较 ( contentEquals 和 contentDeepEquals ),哈希码计算( contentHashCode 和 contentDeepHashCode ), 以及转换成⼀个字符串( contentToString 和 contentDeepToString )。它们都⽀持 JVM (它们作为 java.util.Arrays 中的相应函数的别名)和 JS(在 Kotlin 标准库中提供实现)。 val array = arrayOf(“a“, “b“, “c“) println(array.toString()) // JVM 实现:类型及哈希乱码 println(array.contentToString()) // 良好格式化为列表 Kotlin 现在可以选择⽣成 Java 8 字节码(命令⾏选项 -jvm-target 1.8 或者Ant/Maven/Gradle 中 的相应选项)。⽬前这并不改变字节码的语义(特 别是,接⼝和 lambda 表达式中的默认⽅法 的⽣成与 Kotlin 1.0 中完全⼀样),但我们计划在以后进⼀步使⽤它。 minOf() 和和 maxOf() 类似数组的列表实例化函数类似数组的列表实例化函数 Map.getValue() 抽象集合抽象集合 数组处理函数数组处理函数 JVM 后端 Java 8 字节码⽀持字节码⽀持 Java 8 标准库⽀持标准库⽀持 12 现在有⽀持在 Java 7 和 8 中新添加的 JDK API 的标准库的独⽴版本。 如果你需要访问新的 API,请使⽤ kotlin-stdlib-jre7 和 kotlin- stdlib-jre8 maven 构件,⽽不是标准的 kotlin-stdlib 。 这些构件是在 kotlin-stdlib 之上的微⼩扩展,它们将它作为传递依赖项带到项⽬ 中。 Kotlin 现在⽀持在字节码中存储参数名。这可以使⽤命令⾏选项 -java-parameters 启⽤。 编译器现在将 const val 属性的值内联到使⽤它们的位置。 ⽤于在 lambda 表达式中捕获可变闭包变量的装箱类不再具有 volatile 字段。 此更改提⾼了性能,但在⼀些罕⻅的使⽤情况下可能导致新的竞争条件。如 果受此影响,你需要提供 ⾃⼰的同步机制来访问变量。 Kotlin 现在与javax.script API(JSR-223)集成。 其 API 允许在运⾏时求值代码段: val engine = ScriptEngineManager().getEngineByExtension(“kts“)!! engine.eval(“val x = 3“) println(engine.eval(“x + 2“)) // 输出 5 关于使⽤ API 的⽰例项⽬参⻅这⾥ 。 为 Java 9 ⽀持准备,在 kotlin-reflect.jar 库中的扩展函数和属性已移动 到 kotlin.reflect.full 包中。旧包( kotlin.reflect )中的名 称已弃⽤,将在 Kotlin 1.2 中删除。请注意,核⼼反射接⼝(如 KClass )是 Kotlin 标准库 (⽽不是 kotlin-reflect )的⼀部分,不受移动影响。 Kotlin 标准库的⼤部分⽬前可以从代码编译成 JavaScript 来使⽤。 特别是,关键类如集合( ArrayList 、 HashMap 等)、异常 ( IllegalArgumentException 等)以及其他 ⼏个关键类( StringBuilder 、 Comparator )现在都定义在 kotlin 包下。在 JVM 平台上,⼀些名 称是相应 JDK 类的 类型别名,⽽在 JS 平台上,这些类在 Kotlin 标准库中实现。 JavaScript 后端现在⽣成更加可静态检查的代码,这对 JS 代码处理⼯具(如 minifiers、 optimisers、 linters 等)更加友好。 如果你需要以类型安全的⽅式在 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 // 等等 } 字节码中的参数名字节码中的参数名 常量内联常量内联 可变闭包变量可变闭包变量 javax.scripting ⽀持⽀持 kotlin.reect.full JavaScript 后端 统⼀的标准库统⼀的标准库 更好的代码⽣成更好的代码⽣成 external 修饰符修饰符 改进的导⼊处理改进的导⼊处理 13 现在可以更精确地描述应该从 JavaScript 模块导⼊的声明。 如果在外部声明上添加 @JsModule(“<模块名>“) 注解,它会在编译期间正确导⼊ 到模 块系统(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) { jquery(“.toggle-button“).click { jquery(“.toggle-panel“).toggle(300) } } 14 开始开始 包的声明应处于源⽂件顶部: package my.demo import java.util.* // …… ⽬录与包的结构⽆需匹配:源代码可以在⽂件系统的任意位置。 参⻅包。 带有两个 Int 参数、返回 Int 的函数: fun sum(a: Int, b: Int): Int { return a + b } 将表达式作为函数体、返回值类型⾃动推断的函数: fun sum(a: Int, b: Int) = a + b 函数返回⽆意义的值: fun printSum(a: Int, b: Int): Unit { println(“sum of $a and $b is ${a + b}“) } Unit 返回类型可以省略: fun printSum(a: Int, b: Int) { println(“sum of $a and $b is ${a + b}“) } 参⻅函数。 ⼀次赋值(只读)的局部变量: val a: Int = 1 // ⽴即赋值 val b = 2 // ⾃动推断出 `Int` 类型 val c: Int // 如果没有初始值类型不能省略 c = 3 // 明确赋值 可变变量: 基本语法 定义包 定义函数 定义局部变量 15 var x = 5 // ⾃动推断出 `Int` 类型 x += 1 参⻅属性和字段。 正如 Java 和 JavaScript,Kotlin ⽀持⾏注释及块注释。 // 这是⼀个⾏注释 /* 这是⼀个多⾏的 块注释。 */ 与 Java 不同的是,Kotlin 的块注释可以嵌套。 参⻅编写 Kotlin 代码⽂档 查看关于⽂档注释语法的信息。 var a = 1 // 模板中的简单名称: val s1 = “a is $a“ a = 2 // 模板中的任意表达式: val s2 = “${s1.replace(“is“, “was“)}, but now is $a“ 参⻅字符串模板。 fun maxOf(a: Int, b: Int): Int { if (a > b) { return a } else { return b } } 使⽤ if 作为表达式: fun maxOf(a: Int, b: Int) = if (a > b) a else b 参⻅if 表达式。 当某个变量的值可以为 null 的时候,必须在声明处的类型后添加 ? 来标识该引⽤可为空。 如果 str 的内容不是数字返回 null: fun parseInt(str: String): Int? { // …… } 使⽤返回可空值的函数: 注释 使⽤字符串模板 使⽤条件表达式 使⽤可空值及 null 检测 16 fun printProduct(arg1: String, arg2: String) { val x = parseInt(arg1) val y = parseInt(arg2) // 直接使⽤ `x * y` 可能会报错,因为他们可能为 null if (x != null && y != null) { // 在空检测后,x 和 y 会⾃动转换为⾮空值(non-nullable) println(x * y) } else { println(“either '$arg1' or '$arg2' is not a number“) } } 或者 // …… if (x == null) { println(“Wrong number format in arg1: '${arg1}'“) return } if (y == null) { println(“Wrong number format in arg2: '${arg2}'“) return } // 在空检测后,x 和 y 会⾃动转换为⾮空值 println(x * y) 参⻅空安全。 is 运算符检测⼀个表达式是否某类型的⼀个实例。 如果⼀个不可变的局部变量或属性已经判断出为某类型,那么检测后的分⽀中可以直接当作该类型使 ⽤,⽆需显式转换: fun getStringLength(obj: Any): Int? { if (obj is String) { // `obj` 在该条件分⽀内⾃动转换成 `String` return obj.length } // 在离开类型检测分⽀后,`obj` 仍然是 `Any` 类型 return null } 或者 fun getStringLength(obj: Any): Int? { if (obj !is String) return null // `obj` 在这⼀分⽀⾃动转换为 `String` return obj.length } 甚⾄ 使⽤类型检测及⾃动类型转换 17 fun getStringLength(obj: Any): Int? { // `obj` 在 `&&` 右边⾃动转换成 `String` 类型 if (obj is String && obj.length > 0) { return obj.length } return null } 参⻅类 和 类型转换。 val items = listOf(“apple“, “banana“, “kiwi“) for (item in items) { println(item) } 或者 val items = listOf(“apple“, “banana“, “kiwi“) for (index in items.indices) { println(“item at $index is ${items[index]}“) } 参⻅ for 循环。 val items = listOf(“apple“, “banana“, “kiwi“) var index = 0 while (index < items.size) { println(“item at $index is ${items[index]}“) index++ } 参⻅ while 循环。 fun describe(obj: Any): String = when (obj) { 1 -> “One“ “Hello“ -> “Greeting“ is Long -> “Long“ !is String -> “Not a string“ else -> “Unknown“ } 参⻅ when 表达式。 使⽤ in 运算符来检测某个数字是否在指定区间内: val x = 10 val y = 9 if (x in 1..y+1) { println(“fits in range“) } 检测某个数字是否在指定区间外: 使⽤ for 循环 使⽤ while 循环 使⽤ when 表达式 使⽤区间(range) 18 val list = listOf(“a“, “b“, “c“) if (-1 !in 0..list.lastIndex) { println(“-1 is out of range“) } if (list.size !in list.indices) { println(“list size is out of valid list indices range too“) } 区间迭代: for (x in 1..5) { print(x) } 或数列迭代: for (x in 1..10 step 2) { print(x) } for (x in 9 downTo 0 step 3) { print(x) } 参⻅区间。 对集合进⾏迭代: for (item in items) { println(item) } 使⽤ in 运算符来判断集合内是否包含某实例: when { “orange“ in items -> println(“juicy“) “apple“ in items -> println(“apple is fine too“) } 使⽤ lambda 表达式来过滤(filter)和映射(map)集合: fruits .filter { it.startsWith(“a“) } .sortedBy { it } .map { it.toUpperCase() } .forEach { println(it) } 参⻅⾼阶函数及Lambda表达式。 使⽤集合 19 ⼀些在 Kotlin 中⼴泛使⽤的语法习惯,如果你有更喜欢的语法习惯或者⻛格,建⼀个 pull request 贡献给我们吧! data class Customer(val name: String, val email: String) 会为 Customer 类提供以下功能: 所有属性的 getters (对于 var 定义的还有 setters) equals() hashCode() toString() copy() 所有属性的 component1() 、 component2() ……等等(参⻅数据类) fun foo(a: Int = 0, b: String = ““) { …… } val positives = list.filter { x -> x > 0 } 或者可以更短: val positives = list.filter { it > 0 } println(“Name $name“) when (x) { is Foo //-> …… is Bar //-> …… else //-> …… } for ((k, v) in map) { println(“$k -> $v“) } k 、v 可以改成任意名字。 for (i in 1..100) { …… } // 闭区间:包含 100 for (i in 1 until 100) { …… } // 半开区间:不包含 100 for (x in 2..10 step 2) { …… } for (x in 10 downTo 1) { …… } if (x in 1..10) { …… } 习惯⽤法 创建创建 DTOs((POJOs/POCOs)) — — — — — — 函数的默认参数函数的默认参数 过滤过滤 list String 内插内插 类型判断类型判断 遍历遍历 map/pair型型list 使⽤区间使⽤区间 20 val list = listOf(“a“, “b“, “c“) val map = mapOf(“a“ to 1, “b“ to 2, “c“ to 3) println(map[“key“]) map[“key“] = value val p: String by lazy { // 计算该字符串 } fun String.spaceToCamelCase() { …… } “Convert this to camelcase“.spaceToCamelCase() object Resource { val name = “Name“ } val files = File(“Test“).listFiles() println(files?.size) val files = File(“Test“).listFiles() println(files?.size ?: “empty“) val data = …… val email = data[“email“] ?: throw IllegalStateException(“Email is missing!“) val data = …… data?.let { …… // 代码会执⾏到此处, 假如data不为null } 只读只读 list 只读只读 map 访问访问 map 延迟属性延迟属性 扩展函数扩展函数 创建单例创建单例 If not null 缩写缩写 If not null and else 缩写缩写 if null 执⾏⼀个语句执⾏⼀个语句 if not null 执⾏代码执⾏代码 21 fun transform(color: String): Int { return when (color) { “Red“ -> 0 “Green“ -> 1 “Blue“ -> 2 else -> throw IllegalArgumentException(“Invalid color param value“) } } fun test() { val result = try { count() } catch (e: ArithmeticException) { throw IllegalStateException(e) } // 使⽤ result } fun foo(param: Int) { val result = if (param == 1) { “one“ } else if (param == 2) { “two“ } else { “three“ } } fun arrayOfMinusOnes(size: Int): IntArray { return IntArray(size).apply { fill(-1) } } fun theAnswer() = 42 等价于 fun theAnswer(): Int { return 42 } 单表达式函数与其它惯⽤法⼀起使⽤能简化代码,例如和 when 表达式⼀起使⽤: fun transform(color: String): Int = when (color) { “Red“ -> 0 “Green“ -> 1 “Blue“ -> 2 else -> throw IllegalArgumentException(“Invalid color param value“) } 返回返回when表达式表达式 “try/catch”表达式表达式 “if”表达式表达式 返回类型为返回类型为 Unit 的⽅法的的⽅法的 Builder ⻛格⽤法⻛格⽤法 单表达式函数单表达式函数 对⼀个对象实例调⽤多个⽅法对⼀个对象实例调⽤多个⽅法 ((with)) 22 class Turtle { fun penDown() fun penUp() fun turn(degrees: Double) fun forward(pixels: Double) } val myTurtle = Turtle() with(myTurtle) { // 画⼀个 100 像素的正⽅形 penDown() for(i in 1..4) { forward(100.0) turn(90.0) } penUp() } val stream = Files.newInputStream(Paths.get(“/some/file.txt“)) stream.buffered().reader().use { reader -> println(reader.readText()) } // public final class Gson { // …… // public T fromJson(JsonElement json, Class classOfT) throws JsonSyntaxException { // …… inline fun Gson.fromJson(json): T = this.fromJson(json, T::class.java) val b: Boolean? = …… if (b == true) { …… } else { // `b` 是 false 或者 null } Java 7 的的 try with resources 对于需要泛型信息的泛型函数的适宜形式对于需要泛型信息的泛型函数的适宜形式 使⽤可空布尔使⽤可空布尔 23 此⻚⾯包含当前 Kotlin 语⾔的编码⻛格 如果拿不准的时候,默认使⽤Java的编码规范,⽐如: 使⽤驼峰法命名(并避免命名含有下划线) 类型名以⼤写字⺟开头 ⽅法和属性以⼩写字⺟开头 使⽤ 4 个空格缩进 公有函数应撰写函数⽂档,这样这些⽂档才会出现在 Kotlin Doc 中 类型和超类型之间的冒号前要有⼀个空格,⽽实例和类型之间的冒号前不要有空格: interface Foo : Bar { fun foo(a: Int): T } 在lambda表达式中, ⼤括号左右要加空格,分隔参数与代码体的箭头左右也要加空格 。lambda表达应尽可能不要写在圆括号中 list.filter { it > 10 }.map { element -> element * 2 } 在⾮嵌套的短lambda表达式中,最好使⽤约定俗成的默认参数 it 来替代显式声明参数名 。在嵌套的有参数的lambda表达式中,参数应该总是显式声 明。 有少数⼏个参数的类可以写成⼀⾏: class Person(id: Int, name: String) 具有较⻓类头的类应该格式化,以使每个主构造函数参数位于带有缩进的单独⼀⾏中。 此外,右括号应该另起⼀⾏。如果我们使⽤继承,那么超类构造函数 调⽤或者实现接⼝列表 应位于与括号相同的⾏上: class Person( id: Int, name: String, surname: String ) : Human(id, name) { // …… } 对于多个接⼝,应⾸先放置超类构造函数调⽤,然后每个接⼝应位于不同的⾏中: class Person( id: Int, name: String, surname: String ) : Human(id, name), KotlinMaker { // …… } 构造函数参数可以使⽤常规缩进或连续缩进(双倍的常规缩进)。 编码规范 命名⻛格 — — — — — 冒号 Lambda表达式 类头格式化 Unit 24 如果函数返回 Unit 类型,该返回类型应该省略: fun foo() { // 省略了 “: Unit“ } 很多场合⽆参的函数可与只读属性互换。 尽管语义相近,也有⼀些取舍的⻛格约定。 底层算法优先使⽤属性⽽不是函数: 不会抛异常 O(1) 复杂度 计算廉价(或缓存第⼀次运⾏) 不同调⽤返回相同结果 函数还是属性 — — — — 25 基础基础 在 Kotlin 中,所有东西都是对象,在这个意义上讲所以我们可以在任何变量上调⽤成员函数和属性。有些类型是内置的,因为他们的实现是优化过的。但是 ⽤⼾看起来他们就像普通的类。本节我们会描述⼤多数这些类型:数字、字符、布尔和数组。 Kotlin 处理数字在某种程度上接近 Java,但是并不完全相同。例如,对于数字没有隐式拓宽转换(如 Java 中 int 可以隐式转换为 long ⸺译者注),另 外有些情况的字⾯值略有不同。 Kotlin 提供了如下的内置类型来表⽰数字(与 Java 很相近): TypeType Bit widthBit width Double 64 Float 32 Long 64 Int 32 Short 16 Byte 8 注意在 Kotlin 中字符不是数字 数值常量字⾯值有以下⼏种: ⼗进制: 123 Long 类型⽤⼤写 L 标记: 123L ⼗六进制: 0x0F ⼆进制: 0b00001011 注意: 不⽀持⼋进制 Kotlin 同样⽀持浮点数的常规表⽰⽅法: 默认 double:123.5 、123.5e10 Float ⽤ f 或者 F 标记: 123.5f 你可以使⽤下划线使数字常量更易读: val oneMillion = 1_000_000 val creditCardNumber = 1234_5678_9012_3456L val socialSecurityNumber = 999_99_9999L val hexBytes = 0xFF_EC_DE_5E val bytes = 0b11010010_01101001_10010100_10010010 在 Java 平台数字是物理存储为 JVM 的原⽣类型,除⾮我们需要⼀个可空的引⽤(如 Int? )或泛型。 后者情况下会把数字装箱。 基本类型 数字 字⾯常量字⾯常量 — — — — — — 数字字⾯值中的下划线(⾃数字字⾯值中的下划线(⾃ 1.1 起)起) 表⽰⽅式表⽰⽅式 26 注意数字装箱不必保留同⼀性: val a: Int = 10000 print(a === a) // 输出“true” val boxedA: Int? = a val anotherBoxedA: Int? = a print(boxedA === anotherBoxedA) // !!!输出“false”!!! 另⼀⽅⾯,它保留了相等性: val a: Int = 10000 print(a == a) // 输出“true” val boxedA: Int? = a val anotherBoxedA: Int? = a print(boxedA == anotherBoxedA) // 输出“true” 由于不同的表⽰⽅式,较⼩类型并不是较⼤类型的⼦类型。 如果它们是的话,就会出现下述问题: // 假想的代码,实际上并不能编译: val a: Int? = 1 // ⼀个装箱的 Int (java.lang.Integer) val b: Long? = a // 隐式转换产⽣⼀个装箱的 Long (java.lang.Long) print(a == b) // 惊!这将输出“false”鉴于 Long 的 equals() 检测其他部分也是 Long 所以同⼀性还有相等性都会在所有地⽅悄⽆声息地失去。 因此较⼩的类型不能不能隐式转换为较⼤的类型。 这意味着在不进⾏显式转换的情况下我们不能把 Byte 型值赋给⼀个 Int 变量。 val b: Byte = 1 // OK, 字⾯值是静态检测的 val i: Int = b // 错误 我们可以显式转换来拓宽数字 val i: Int = b.toInt() // OK: 显式拓宽 每个数字类型⽀持如下的转换: toByte(): Byte toShort(): Short toInt(): Int toLong(): Long toFloat(): Float toDouble(): Double toChar(): Char 缺乏隐式类型转换并不显著,因为类型会从上下⽂推断出来,⽽算术运算会有重载做适当转换,例如: val l = 1L + 3 // Long + Int => Long Kotlin⽀持数字运算的标准集,运算被定义为相应的类成员(但编译器会将函数调⽤优化为相应的指令)。 参⻅运算符重载。 对于位运算,没有特殊字符来表⽰,⽽只可⽤中缀⽅式调⽤命名函数,例如: val x = (1 shl 2) and 0x000FF000 这是完整的位运算列表(只⽤于 Int 和 Long ): 显式转换显式转换 — — — — — — — 运算运算 27 shl(bits) ‒ 有符号左移 (Java 的 << ) shr(bits) ‒ 有符号右移 (Java 的 >> ) ushr(bits) ‒ ⽆符号右移 (Java 的 >>> ) and(bits) ‒ 位与 or(bits) ‒ 位或 xor(bits) ‒ 位异或 inv() ‒ 位⾮ 字符⽤ Char 类型表⽰。它们不能直接当作数字 fun check(c: Char) { if (c == 1) { // 错误:类型不兼容 // …… } } 字符字⾯值⽤单引号括起来: '1' 。 特殊字符可以⽤反斜杠转义。 ⽀持这⼏个转义序列:\t 、 \b 、\n 、\r 、\' 、\“ 、\\ 和 \$ 。 编码其他字符要⽤ Unicode 转义序列语法:'\uFF00' 。 我们可以显式把字符转换为 Int 数字: fun decimalDigitValue(c: Char): Int { if (c !in '0'..'9') throw IllegalArgumentException(“Out of range“) return c.toInt() - '0'.toInt() // 显式转换为数字 } 当需要可空引⽤时,像数字、字符会被装箱。装箱操作不会保留同⼀性。 布尔⽤ Boolean 类型表⽰,它有两个值:true 和 false。 若需要可空引⽤布尔会被装箱。 内置的布尔运算有: || ‒ 短路逻辑或 && ‒ 短路逻辑与 ! - 逻辑⾮ 数组在 Kotlin 中使⽤ Array 类来表⽰,它定义了 get 和 set 函数(按照运算符重载约定这会转变为 [] )和 size 属性,以及⼀些其他有⽤的成员 函数: class Array private constructor() { val size: Int operator fun get(index: Int): T operator fun set(index: Int, value: T): Unit operator fun iterator(): Iterator // …… } 我们可以使⽤库函数 arrayOf() 来创建⼀个数组并传递元素值给它,这样 arrayOf(1, 2, 3) 创建了 array [1, 2, 3]。 或者,库函数 arrayOfNulls() 可以⽤于创建⼀个指定⼤⼩、元素都为空的数组。 另⼀个选项是⽤接受数组⼤⼩和⼀个函数参数的⼯⼚函数,⽤作参数的函数能够返回 给定索引的每个元素初始值: — — — — — — — 字符 布尔 — — — 数组 28 // 创建⼀个 Array 初始化为 [“0“, “1“, “4“, “9“, “16“] val asc = Array(5, { i -> (i * i).toString() }) 如上所述,[] 运算符代表调⽤成员函数 get() 和 set() 。 注意: 与 Java 不同的是,Kotlin 中数组是不型变的(invariant)。这意味着 Kotlin 不让我们把 Array 赋值给 Array ,以防⽌可能的运 ⾏时失败(但是你可以使⽤ Array , 参⻅类型投影)。 Kotlin 也有⽆装箱开销的专⻔的类来表⽰原⽣类型数组: ByteArray 、 ShortArray 、IntArray 等等。这些类和 Array 并没有继承关系,但是 它 们有同样的⽅法属性集。它们也都有相应的⼯⼚⽅法: val x: IntArray = intArrayOf(1, 2, 3) x[0] = x[1] + x[2] 字符串⽤ String 类型表⽰。字符串是不可变的。 字符串的元素⸺字符可以使⽤索引运算符访问: s[i] 。 可以⽤ for 循环迭代字符串: for (c in str) { println(c) } Kotlin 有两种类型的字符串字⾯值: 转义字符串可以有转义字符,以及原⽣字符串可以包含换⾏和任意⽂本。转义字符串很像 Java 字符串: val s = “Hello, world!\n“ 转义采⽤传统的反斜杠⽅式。参⻅上⾯的 字符 查看⽀持的转义序列。 原⽣字符串 使⽤三个引号( “““ )分界符括起来,内部没有转义并且可以包含换⾏和任何其他字符: val text = “““ for (c in “foo“) print(c) “““ 你可以通过 trimMargin() 函数去除前导空格: val text = “““ |Tell me and I forget. |Teach me and I remember. |Involve me and I learn. |(Benjamin Franklin) “““.trimMargin() 默认 | ⽤作边界前缀,但你可以选择其他字符并作为参数传⼊,⽐如 trimMargin(“>“) 。 字符串可以包含模板表达式 ,即⼀些⼩段代码,会求值并把结果合并到字符串中。 模板表达式以美元符( $ )开头,由⼀个简单的名字构成: val i = 10 val s = “i = $i“ // 求值结果为 “i = 10“ 或者⽤花括号扩起来的任意表达式: val s = “abc“ val str = “$s.length is ${s.length}“ // 求值结果为 “abc.length is 3“ 字符串 字符串字⾯值字符串字⾯值 字符串模板字符串模板 29 原⽣字符串和转义字符串内部都⽀持模板。 如果你需要在原⽣字符串中表⽰字⾯值 $ 字符(它不⽀持反斜杠转义),你可以⽤下列语法: val price = “““ ${'$'}9.99 “““ 30 源⽂件通常以包声明开头: package foo.bar fun baz() {} class Goo {} // …… 源⽂件所有内容(⽆论是类还是函数)都包含在声明的包内。 所以上例中 baz() 的全名是 foo.bar.baz 、Goo 的全名是 foo.bar.Goo 。 如果没有指明包,该⽂件的内容属于⽆名字的默认包。 有多个包会默认导⼊到每个 Kotlin ⽂件中: kotlin.* kotlin.annotation.* kotlin.collections.* kotlin.comparisons.* (⾃ 1.1 起) kotlin.io.* kotlin.ranges.* kotlin.sequences.* kotlin.text.* 根据⽬标平台还会导⼊额外的包: JVM: java.lang.* kotlin.jvm.* JS: kotlin.js.* 除了默认导⼊之外,每个⽂件可以包含它⾃⼰的导⼊指令。 导⼊语法在语法中讲述。 可以导⼊⼀个单独的名字,如. import foo.Bar // 现在 Bar 可以不⽤限定符访问 也可以导⼊⼀个作⽤域下的所有内容(包、类、对象等): import foo.* // “foo”中的⼀切都可访问 如果出现名字冲突,可以使⽤ as 关键字在本地重命名冲突项来消歧义: import foo.Bar // Bar 可访问 import bar.Bar as bBar // bBar 代表“bar.Bar” 关键字 import 并不仅限于导⼊类;也可⽤它来导⼊其他声明: 顶层函数及属性 在对象声明中声明的函数和属性; 枚举常量 与 Java 不同,Kotlin 没有单独的 “import static“ 语法; 所有这些声明都⽤ import 关键字导⼊。 包 默认导⼊ — — — — — — — — — — — — — 导⼊ — — — 31 如果顶层声明是 private 的,它是声明它的⽂件所私有的(参⻅ 可⻅性修饰符)。 顶层声明的可⻅性 32 在 Kotlin 中,if是⼀个表达式,即它会返回⼀个值。 因此就不需要三元运算符(条件 ? 然后 : 否则),因为普通的 if 就能胜任这个⻆⾊。 // 传统⽤法 var max = a if (a < b) max = b // With else var max: Int if (a > b) { max = a } else { max = b } // 作为表达式 val max = if (a > b) a else b if的分⽀可以是代码块,最后的表达式作为该块的值: val max = if (a > b) { print(“Choose a“) a } else { print(“Choose b“) b } 如果你使⽤ if 作为表达式⽽不是语句(例如:返回它的值或者 把它赋给变量),该表达式需要有 else 分⽀。 参⻅ if 语法。 when 取代了类 C 语⾔的 switch 操作符。其最简单的形式如下: when (x) { 1 -> print(“x == 1“) 2 -> print(“x == 2“) else -> { // 注意这个块 print(“x is neither 1 nor 2“) } } when 将它的参数和所有的分⽀条件顺序⽐较,直到某个分⽀满⾜条件。 when 既可以被当做表达式使⽤也可以被当做语句使⽤。如果它被当做表达式, 符 合条件的分⽀的值就是整个表达式的值,如果当做语句使⽤, 则忽略个别分⽀的值。(像 if ⼀样,每⼀个分⽀可以是⼀个代码块,它的值 是块中最后的表 达式的值。) 如果其他分⽀都不满⾜条件将会求值 else 分⽀。 如果 when 作为⼀个表达式使⽤,则必须有 else 分⽀, 除⾮编译器能够检测出所有的可能情况都已经 覆盖了。 如果很多分⽀需要⽤相同的⽅式处理,则可以把多个分⽀条件放在⼀起,⽤逗号分隔: when (x) { 0, 1 -> print(“x == 0 or x == 1“) else -> print(“otherwise“) } 我们可以⽤任意表达式(⽽不只是常量)作为分⽀条件 控制流 If表达式 When 表达式 33 when (x) { parseInt(s) -> print(“s encodes x“) else -> print(“s does not encode x“) } 我们也可以检测⼀个值在(in)或者不在(!in)⼀个区间或者集合中: when (x) { in 1..10 -> print(“x is in the range“) in validNumbers -> print(“x is valid“) !in 10..20 -> print(“x is outside the range“) else -> print(“none of the above“) } 另⼀种可能性是检测⼀个值是(is)或者不是(!is)⼀个特定类型的值。注意: 由于智能转换,你可以访问该类型的⽅法和属性⽽⽆需 任何额外的检测。 fun hasPrefix(x: Any) = when(x) { is String -> x.startsWith(“prefix“) else -> false } when 也可以⽤来取代 if-else if链。 如果不提供参数,所有的分⽀条件都是简单的布尔表达式,⽽当⼀个分⽀的条件为真时则执⾏该分⽀: when { x.isOdd() -> print(“x is odd“) x.isEven() -> print(“x is even“) else -> print(“x is funny“) } 参⻅ when 语法。 for 循环可以对任何提供迭代器(iterator)的对象进⾏遍历,语法如下: for (item in collection) print(item) 循环体可以是⼀个代码块。 for (item: Int in ints) { // …… } 如上所述,for 可以循环遍历任何提供了迭代器的对象。即: 有⼀个成员函数或者扩展函数 iterator() ,它的返回类型 有⼀个成员函数或者扩展函数 next() ,并且 有⼀个成员函数或者扩展函数 hasNext() 返回 Boolean 。 这三个函数都需要标记为 operator 。 对数组的 for 循环会被编译为并不创建迭代器的基于索引的循环。 如果你想要通过索引遍历⼀个数组或者⼀个 list,你可以这么做: for (i in array.indices) { print(array[i]) } 注意这种“在区间上遍历”会编译成优化的实现⽽不会创建额外对象。 或者你可以⽤库函数 withIndex : For 循环 — — — 34 for ((index, value) in array.withIndex()) { println(“the element at $index is $value“) } 参⻅for 语法。 while 和 do..while 照常使⽤ while (x > 0) { x-- } do { val y = retrieveData() } while (y != null) // y 在此处可⻅ 参⻅while 语法. 在循环中 Kotlin ⽀持传统的 break 和 continue 操作符。参⻅返回和跳转。 While 循环 循环中的Break和continue 35 Kotlin 有三种结构化跳转表达式: return。默认从最直接包围它的函数或者匿名函数返回。 break。终⽌最直接包围它的循环。 continue。继续下⼀次最直接包围它的循环。 所有这些表达式都可以⽤作更⼤表达式的⼀部分: val s = person.name ?: return 这些表达式的类型是 Nothing 类型。 在 Kotlin 中任何表达式都可以⽤标签(label)来标记。 标签的格式为标识符后跟 @ 符号,例如:abc@ 、fooBar@ 都是有效的标签(参⻅语法)。 要为⼀ 个表达式加标签,我们只要在其前加标签即可。 loop@ for (i in 1..100) { // …… } 现在,我们可以⽤标签限制 break 或者continue: loop@ for (i in 1..100) { for (j in 1..100) { if (……) break@loop } } 标签限制的 break 跳转到刚好位于该标签指定的循环后⾯的执⾏点。 continue 继续标签指定的循环的下⼀次迭代。 Kotlin 有函数字⾯量、局部函数和对象表达式。因此 Kotlin 的函数可以被嵌套。 标签限制的 return 允许我们从外层函数返回。 最重要的⼀个⽤途就是从 lambda 表达式中返回。回想⼀下我们这么写的时候: fun foo() { ints.forEach { if (it == 0) return print(it) } } 这个 return 表达式从最直接包围它的函数即 foo 中返回。 (注意,这种⾮局部的返回只⽀持传给内联函数的 lambda 表达式。) 如果我们需要从 lambda 表达式中返回,我们必须给它加标签并⽤以限制 return。 fun foo() { ints.forEach lit@ { if (it == 0) return@lit print(it) } } 现在,它只会从 lambda 表达式中返回。通常情况下使⽤隐式标签更⽅便。 该标签与接受该 lambda 的函数同名。 返回和跳转 — — — Break 和 Continue 标签 标签处返回 36 fun foo() { ints.forEach { if (it == 0) return@forEach print(it) } } 或者,我们⽤⼀个匿名函数替代 lambda 表达式。 匿名函数内部的 return 语句将从该匿名函数⾃⾝返回 fun foo() { ints.forEach(fun(value: Int) { if (value == 0) return print(value) }) } 当要返⼀个回值的时候,解析器优先选⽤标签限制的 return,即 return@a 1 意为“从标签 @a 返回 1”,⽽不是“返回⼀个标签标注的表达式 (@a 1) ”。 37 类和对象类和对象 Kotlin 中使⽤关键字 class 声明类 class Invoice { } 类声明由类名、类头(指定其类型参数、主 构造函数等)和由⼤括号包围的类体构成。类头和类体都是可选的; 如果⼀个类没有类体,可以省略花括号。 class Empty 在 Kotlin 中的⼀个类可以有⼀个主构造函数主构造函数和⼀个或多个次构造函数次构造函数。主 构造函数是类头的⼀部分:它跟在类名(和可选的类型参数)后。 class Person constructor(firstName: String) { } 如果主构造函数没有任何注解或者可⻅性修饰符,可以省略这个 constructor 关键字。 class Person(firstName: String) { } 主构造函数不能包含任何的代码。初始化的代码可以放 到以 init 关键字作为前缀的初始化块(initializer blocks)初始化块(initializer blocks)中: class Customer(name: String) { init { logger.info(“Customer initialized with value ${name}“) } } 注意,主构造的参数可以在初始化块中使⽤。它们也可以在 类体内声明的属性初始化器中使⽤: class Customer(name: String) { val customerKey = name.toUpperCase() } 事实上,声明属性以及从主构造函数初始化属性,Kotlin 有简洁的语法: class Person(val firstName: String, val lastName: String, var age: Int) { // …… } 与普通属性⼀样,主构造函数中声明的属性可以是 可变的(var)或只读的(val)。 如果构造函数有注解或可⻅性修饰符,这个 constructor 关键字是必需的,并且 这些修饰符在它前⾯: class Customer public @Inject constructor(name: String) { …… } 类和继承 类 构造函数构造函数 38 更多详情,参⻅可⻅性修饰符 类也可以声明前缀有 constructor的次构造函数次构造函数: class Person { constructor(parent: Person) { parent.children.add(this) } } 如果类有⼀个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同⼀个类的另⼀个构造函数 ⽤ this 关键字即可: class Person(val name: String) { constructor(name: String, parent: Person) : this(name) { parent.children.add(this) } } 如果⼀个⾮抽象类没有声明任何(主或次)构造函数,它会有⼀个⽣成的 不带参数的主构造函数。构造函数的可⻅性是 public。如果你不希望你的类 有⼀ 个公有构造函数,你需要声明⼀个带有⾮默认可⻅性的空的主构造函数: class DontCreateMe private constructor () { } 注意注意:在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会⽣成 ⼀个额外的⽆参构造函数,它将使⽤默认值。这使得 Kotlin 更易于使⽤ 像 Jackson 或者 JPA 这样的通过⽆参构造函数创建类的实例的库。 class Customer(val customerName: String = ““) 要创建⼀个类的实例,我们就像普通函数⼀样调⽤构造函数: val invoice = Invoice() val customer = Customer(“Joe Smith“) 注意 Kotlin 并没有 new 关键字。 创建嵌套类、内部类和匿名内部类的类实例在嵌套类中有述。 类可以包含 构造函数和初始化块 函数 属性 嵌套类和内部类 对象声明 在 Kotlin 中所有类都有⼀个共同的超类 Any ,这对于没有超类型声明的类是默认超类: class Example // 从 Any 隐式继承 Any 不是 java.lang.Object ;尤其是,它除了 equals() 、hashCode() 和 toString() 外没有任何成员。 更多细节请查阅Java互操作性部分。 次构造函数 创建类的实例创建类的实例 类成员类成员 — — — — — 继承 39 要声明⼀个显式的超类型,我们把类型放到类头的冒号之后: open class Base(p: Int) class Derived(p: Int) : Base(p) 如果该类有⼀个主构造函数,其基类型可以(并且必须) ⽤(基类型的)主构造函数参数就地初始化。 如果类没有主构造函数,那么每个次构造函数必须 使⽤ super 关键字初始化其基类型,或委托给另⼀个构造函数做到这⼀点。 注意,在这种情况下,不同 的次构造函数可以调⽤基类型的不同的构造函数: class MyView : View { constructor(ctx: Context) : super(ctx) constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) } 类上的 open 标注与 Java 中 final 相反,它允许其他类 从这个类继承。默认情况下,在 Kotlin 中所有的类都是 final, 对应于 Effective Java书中的 第 17 条:要么为继承⽽设计,并提供⽂档说明,要么就禁⽌继承要么为继承⽽设计,并提供⽂档说明,要么就禁⽌继承。 我们之前提到过,Kotlin ⼒求清晰显式。与 Java 不同,Kotlin 需要显式 标注可覆盖的成员(我们称之为开放)和覆盖后的成员: open class Base { open fun v() {} fun nv() {} } class Derived() : Base() { override fun v() {} } Derived.v() 函数上必须加上 overrideoverride标注。如果没写,编译器将会报错。 如果函数没有标注 openopen 如 Base.nv() ,则⼦类中不允许定义相同签名的函 数, 不论加不加 overrideoverride。在⼀个 finalfinal 类中(没有⽤ openopen 标注的类),开放成员是禁⽌的。 标记为 override 的成员本⾝是开放的,也就是说,它可以在⼦类中覆盖。如果你想禁⽌再次覆盖,使⽤ final 关键字: open class AnotherDerived() : Base() { final override fun v() {} } 属性覆盖与⽅法覆盖类似;在超类中声明然后在派⽣类中重新声明的属性必须以 override 开头,并且它们必须具有兼容的类型。每个声明的属性可以由 具有初始化器的属性或者具有 getter ⽅法的属性覆盖。 open class Foo { open val x: Int get { …… } } class Bar1 : Foo() { override val x: Int = …… } 你也可以⽤⼀个 var 属性覆盖⼀个 val 属性,但反之则不⾏。这是允许的,因为⼀个 val 属性本质上声明了⼀个 getter ⽅法,⽽将其覆盖为 var 只 是在⼦类中额外声明⼀个 setter ⽅法。 请注意,你可以在主构造函数中使⽤ override 关键字作为属性声明的⼀部分。 覆盖⽅法覆盖⽅法 覆盖属性覆盖属性 40 interface Foo { val count: Int } class Bar1(override val count: Int) : Foo class Bar2 : Foo { override var count: Int = 0 } 在 Kotlin 中,实现继承由下述规则规定:如果⼀个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其⾃⼰的实现(也许⽤继承来 的其中之⼀)。 为了表⽰采⽤从哪个超类型继承的实现,我们使⽤由尖括号中超类型名限定的 super,如 super : open class A { open fun f() { print(“A“) } fun a() { print(“a“) } } interface B { fun f() { print(“B“) } // 接⼝成员默认就是“open”的 fun b() { print(“b“) } } class C() : A(), B { // 编译器要求覆盖 f(): override fun f() { super.f() // 调⽤ A.f() super.f() // 调⽤ B.f() } } 同时继承 A 和 B 没问题,并且 a() 和 b() 也没问题因为 C 只继承了每个函数的⼀个实现。 但是 f() 由 C 继承了两个实现,所以我们必须必须在 C 中 覆盖 f() 并且提供我们⾃⼰的实现来消除歧义。 类和其中的某些成员可以声明为 abstract。 抽象成员在本类中可以不⽤实现。 需要注意的是,我们并不需要⽤ open 标注⼀个抽象类或者函数⸺因 为这不⾔⽽喻。 我们可以⽤⼀个抽象成员覆盖⼀个⾮抽象的开放成员 open class Base { open fun f() {} } abstract class Derived : Base() { override abstract fun f() } 与 Java 或 C# 不同,在 Kotlin 中类没有静态⽅法。在⼤多数情况下,它建议简单地使⽤ 包级函数。 如果你需要写⼀个可以⽆需⽤⼀个类的实例来调⽤、但需要访问类内部的 函数(例如,⼯⼚⽅法),你可以把它写成该类内对象声明 中的⼀员。 更具体地讲,如果在你的类内声明了⼀个伴⽣对象, 你就可以使⽤像在 Java/C# 中调⽤静态⽅法相同的语法来调⽤其成员,只使⽤类名 作为限定符。 覆盖规则覆盖规则 抽象类 伴⽣对象 41 Kotlin的类可以有属性。 属性可以⽤关键字var 声明为可变的,否则使⽤只读关键字val。 class Address { var name: String = …… var street: String = …… var city: String = …… var state: String? = …… var zip: String = …… } 要使⽤⼀个属性,只要⽤名称引⽤它即可,就像 Java 中的字段: fun copyAddress(address: Address): Address { val result = Address() // Kotlin 中没有“new”关键字 result.name = address.name // 将调⽤访问器 result.street = address.street // …… return result } 声明⼀个属性的完整语法是 var [: ] [= ] [] [] 其初始器(initializer)、getter 和 setter 都是可选的。属性类型如果可以从初始器 (或者从其 getter 返回值,如下⽂所⽰)中推断出来,也可以省略。 例如: var allByDefault: Int? // 错误:需要显式初始化器,隐含默认 getter 和 setter var initialized = 1 // 类型 Int、默认 getter 和 setter ⼀个只读属性的语法和⼀个可变的属性的语法有两⽅⾯的不同:1、只读属性的⽤ val 开始代替 var 2、只读属性不允许 setter val simple: Int? // 类型 Int、默认 getter、必须在构造函数中初始化 val inferredType = 1 // 类型 Int 、默认 getter 我们可以编写⾃定义的访问器,⾮常像普通函数,刚好在属性声明内部。这⾥有⼀个⾃定义 getter 的例⼦: val isEmpty: Boolean get() = this.size == 0 ⼀个⾃定义的 setter 的例⼦: var stringRepresentation: String get() = this.toString() set(value) { setDataFromString(value) // 解析字符串并赋值给其他属性 } 按照惯例,setter 参数的名称是 value ,但是如果你喜欢你可以选择⼀个不同的名称。 ⾃ Kotlin 1.1 起,如果可以从 getter 推断出属性类型,则可以省略它: val isEmpty get() = this.size == 0 // 具有类型 Boolean 属性和字段 声明属性 Getters 和 Setters 42 如果你需要改变⼀个访问器的可⻅性或者对其注解,但是不需要改变默认的实现, 你可以定义访问器⽽不定义其实现: var setterVisibility: String = “abc“ private set // 此 setter 是私有的并且有默认实现 var setterWithAnnotation: Any? = null @Inject set // ⽤ Inject 注解此 setter Kotlin 中类不能有字段。然⽽,当使⽤⾃定义访问器时,有时有⼀个幕后字段(backing field)有时是必要的。为此 Kotlin 提供 ⼀个⾃动幕后字段,它可通 过使⽤ field 标识符访问。 var counter = 0 // 此初始器值直接写⼊到幕后字段 set(value) { if (value >= 0) field = value } field 标识符只能⽤在属性的访问器内。 如果属性⾄少⼀个访问器使⽤默认实现,或者⾃定义访问器通过 field 引⽤幕后字段,将会为该属性⽣成⼀个幕后字段。 例如,下⾯的情况下, 就没有幕后字段: val isEmpty: Boolean get() = this.size == 0 如果你的需求不符合这套“隐式的幕后字段”⽅案,那么总可以使⽤ 幕后属性(backing property): private var _table: Map? = null public val table: Map get() { if (_table == null) { _table = HashMap() // 类型参数已推断出 } return _table ?: throw AssertionError(“Set to null by another thread“) } 从各⽅⾯看,这正是与 Java 相同的⽅式。因为通过默认 getter 和 setter 访问私有属性会被优化,所以不会引⼊函数调⽤开销。 已知值的属性可以使⽤ const 修饰符标记为 编译期常量。 这些属性需要满⾜以下要求: 位于顶层或者是 object 的⼀个成员 ⽤ String 或原⽣类型 值初始化 没有⾃定义 getter 这些属性可以⽤在注解中: const val SUBSYSTEM_DEPRECATED: String = “This subsystem is deprecated“ @Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { …… } ⼀般地,属性声明为⾮空类型必须在构造函数中初始化。 然⽽,这经常不⽅便。例如:属性可以通过依赖注⼊来初始化, 或者在单元测试的 setup ⽅法中初 始化。 这种情况下,你不能在构造函数内提供⼀个⾮空初始器。 但你仍然想在类体中引⽤该属性时避免空检查。 为处理这种情况,你可以⽤ lateinit 修饰符标记该属性: 幕后字段幕后字段 幕后属性幕后属性 编译期常量 — — — 延迟初始化属性 43 public class MyTest { lateinit var subject: TestSubject @SetUp fun setup() { subject = TestSubject() } @Test fun test() { subject.method() // 直接解引⽤ } } 该修饰符只能⽤于在类体中(不是在主构造函数中)声明的 var 属性,并且仅 当该属性没有⾃定义 getter 或 setter 时。该属性必须是⾮空类型,并且不 能是 原⽣类型。 在初始化前访问⼀个 lateinit 属性会抛出⼀个特定异常,该异常明确标识该属性 被访问及它没有初始化的事实。 参⻅覆盖属性 最常⻅的⼀类属性就是简单地从幕后字段中读取(以及可能的写⼊)。 另⼀⽅⾯,使⽤⾃定义 getter 和 setter 可以实现属性的任何⾏为。 介于两者之间, 属性如何⼯作有⼀些常⻅的模式。⼀些例⼦:惰性值、 通过键值从映射读取、访问数据库、访问时通知侦听器等等。 这些常⻅⾏为可以通过使⽤委托属性实现为库。 覆盖属性 委托属性 44 Kotlin 的接⼝与 Java 8 类似,既包含抽象⽅法的声明,也包含 实现。与抽象类不同的是,接⼝⽆法保存状态。它可以有 属性但必须声明为抽象或提供访问 器实现。 使⽤关键字 interface 来定义接⼝ interface MyInterface { fun bar() fun foo() { // 可选的⽅法体 } } ⼀个类或者对象可以实现⼀个或多个接⼝。 class Child : MyInterface { override fun bar() { // ⽅法体 } } 你可以在接⼝中定义属性。在接⼝中声明的属性要么是抽象的,要么提供 访问器的实现。在接⼝中声明的属性不能有幕后字段(backing field),因此接⼝ 中声明的访问器 不能引⽤它们。 interface MyInterface { val prop: Int // 抽象的 val propertyWithImplementation: String get() = “foo“ fun foo() { print(prop) } } class Child : MyInterface { override val prop: Int = 29 } 实现多个接⼝时,可能会遇到同⼀⽅法继承多个实现的问题。例如 接⼝ 实现接⼝ 接⼝中的属性 解决覆盖冲突 45 interface A { fun foo() { print(“A“) } fun bar() } interface B { fun foo() { print(“B“) } fun bar() { print(“bar“) } } class C : A { override fun bar() { print(“bar“) } } class D : A, B { override fun foo() { super.foo() super.foo() } override fun bar() { super.bar() } } 上例中,接⼝ A 和 B 都定义了⽅法 foo() 和 bar()。 两者都实现了 foo(), 但是只有 B 实现了 bar() (bar() 在 A 中没有标记为抽象, 因为没有⽅法体时默认 为抽象)。因为 C 是⼀个实现了 A 的具体类,所以必须要重写 bar() 并 实现这个抽象⽅法。 然⽽,如果我们从 A 和 B 派⽣ D,我们需要实现 我们从多个接⼝继承的所有⽅法,并指明 D 应该如何实现它们。这⼀规则 既适⽤于继承单个实现(bar())的 ⽅法也适⽤于继承多个实现(foo())的⽅法。 46 类、对象、接⼝、构造函数、⽅法、属性和它们的 setter 都可以有 可⻅性修饰符。 (getter 总是与属性有着相同的可⻅性。) 在 Kotlin 中有这四个可⻅性修饰 符:private 、 protected 、 internal 和 public 。 如果没有显式指定修饰符的话,默认可⻅性是 public 。 下⾯将根据声明作⽤域的不同来解释。 函数、属性和类、对象和接⼝可以在顶层声明,即直接在包内: // ⽂件名:example.kt package foo fun baz() {} class Bar {} 如果你不指定任何可⻅性修饰符,默认为 public ,这意味着你的声明 将随处可⻅; 如果你声明为 private ,它只会在声明它的⽂件内可⻅; 如果你声明为 internal ,它会在相同模块内随处可⻅; protected 不适⽤于顶层声明。 例如: // ⽂件名:example.kt package foo private fun foo() {} // 在 example.kt 内可⻅ public var bar: Int = 5 // 该属性随处可⻅ private set // setter 只在 example.kt 内可⻅ internal val baz = 6 // 相同模块内可⻅ 对于类内部声明的成员: private 意味着只在这个类内部(包含其所有成员)可⻅; protected ⸺ 和 private ⼀样 + 在⼦类中可⻅。 internal ⸺ 能⻅到类声明的 本模块内 的任何客⼾端都可⻅其 internal 成员; public ⸺ 能⻅到类声明的任何客⼾端都可⻅其 public 成员。 注意 对于Java⽤⼾:Kotlin 中外部类不能访问内部类的 private 成员。 如果你覆盖⼀个 protected 成员并且没有显式指定其可⻅性,该成员还会是 protected 可⻅性。 例⼦: 可⻅性修饰符 包名 — — — — 类和接⼝ — — — — 47 open class Outer { private val a = 1 protected open val b = 2 internal val c = 3 val d = 4 // 默认 public protected class Nested { public val e: Int = 5 } } class Subclass : Outer() { // a 不可⻅ // b、c、d 可⻅ // Nested 和 e 可⻅ override val b = 5 // “b”为 protected } class Unrelated(o: Outer) { // o.a、o.b 不可⻅ // o.c 和 o.d 可⻅(相同模块) // Outer.Nested 不可⻅,Nested::e 也不可⻅ } 要指定⼀个类的的主构造函数的可⻅性,使⽤以下语法(注意你需要添加⼀个 显式 constructor 关键字): class C private constructor(a: Int) { …… } 这⾥的构造函数是私有的。默认情况下,所有构造函数都是 public ,这实际上 等于类可⻅的地⽅它就可⻅(即 ⼀个 internal 类的构造函数只能 在相 同模块内可⻅). 局部变量、函数和类不能有可⻅性修饰符。 可⻅性修饰符 internal 意味着该成员只在相同模块内可⻅。更具体地说, ⼀个模块是编译在⼀起的⼀套 Kotlin ⽂件: ⼀个 IntelliJ IDEA 模块; ⼀个 Maven 或者 Gradle 项⽬; ⼀次 <kotlinc> Ant 任务执⾏所编译的⼀套⽂件。 构造函数构造函数 局部声明局部声明 模块 — — — 48 Kotlin 同 C# 和 Gosu 类似,能够扩展⼀个类的新功能⽽⽆需继承该类或使⽤像装饰者这样的任何类型的设计模式。 这通过叫做 扩展 的特殊声明完成。 Kotlin ⽀持 扩展函数 和 扩展属性。 声明⼀个扩展函数,我们需要⽤⼀个 接收者类型 也就是被扩展的类型来作为他的前缀。 下⾯代码为 MutableList 添加⼀个 swap 函数: fun MutableList.swap(index1: Int, index2: Int) { val tmp = this[index1] // “this”对应该列表 this[index1] = this[index2] this[index2] = tmp } 这个 this 关键字在扩展函数内部对应到接收者对象(传过来的在点符号前的对象) 现在,我们对任意 MutableList 调⽤该函数了: val l = mutableListOf(1, 2, 3) l.swap(0, 2) // “swap()”内部的“this”得到“l”的值 当然,这个函数对任何 MutableList 起作⽤,我们可以泛化它: fun MutableList.swap(index1: Int, index2: Int) { val tmp = this[index1] // “this”对应该列表 this[index1] = this[index2] this[index2] = tmp } 为了在接收者类型表达式中使⽤泛型,我们要在函数名前声明泛型参数。 参⻅泛型函数。 扩展不能真正的修改他们所扩展的类。通过定义⼀个扩展,你并没有在⼀个类中插⼊新成员, 仅仅是可以通过该类型的变量⽤点表达式去调⽤这个新函 数。 我们想强调的是扩展函数是静态分发的,即他们不是根据接收者类型的虚⽅法。 这意味着调⽤的扩展函数是由函数调⽤所在的表达式的类型来决定的, ⽽ 不是由表达式运⾏时求值结果决定的。例如: open class C class D: C() fun C.foo() = “c“ fun D.foo() = “d“ fun printFoo(c: C) { println(c.foo()) } printFoo(D()) 这个例⼦会输出 “c“,因为调⽤的扩展函数只取决于 参数 c 的声明类型,该类型是 C 类。 如果⼀个类定义有⼀个成员函数和⼀个扩展函数,⽽这两个函数⼜有相同的接收者类型、相同的名字 并且都适⽤给定的参数,这种情况总是取成员函数总是取成员函数。 例如: class C { fun foo() { println(“member“) } } fun C.foo() { println(“extension“) } 如果我们调⽤ C 类型 c 的 c.foo() ,它将输出“member”,⽽不是“extension”。 扩展 扩展函数 扩展是静态解析的 49 当然,扩展函数重载同样名字但不同签名成员函数也完全可以: class C { fun foo() { println(“member“) } } fun C.foo(i: Int) { println(“extension“) } 调⽤ C().foo(1) 将输出 “extension“。 注意可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调⽤, 即使其值为 null,并且可以在函数体内检测 this == null ,这能让你 在 没有检测 null 的时候调⽤ Kotlin 中的toString():检测发⽣在扩展函数的内部。 fun Any?.toString(): String { if (this == null) return “null“ // 空检测之后,“this”会⾃动转换为⾮空类型,所以下⾯的 toString() // 解析为 Any 类的成员函数 return toString() } 和函数类似,Kotlin ⽀持扩展属性: val List.lastIndex: Int get() = size - 1 注意:由于扩展没有实际的将成员插⼊类中,因此对扩展属性来说 幕后字段是⽆效的。这就是为什么扩展属性不能有 初始化器扩展属性不能有 初始化器。他们的⾏为只能由显式提 供的 getters/setters 定义。 例如: val Foo.bar = 1 // 错误:扩展属性不能有初始化器 如果⼀个类定义有⼀个伴⽣对象 ,你也可以为伴⽣对象定义 扩展函数和属性: class MyClass { companion object { } // 将被称为 “Companion“ } fun MyClass.Companion.foo() { // …… } 就像伴⽣对象的其他普通成员,只需⽤类名作为限定符去调⽤他们 MyClass.foo() ⼤多数时候我们在顶层定义扩展,即直接在包⾥: package foo.bar fun Baz.goo() { …… } 要使⽤所定义包之外的⼀个扩展,我们需要在调⽤⽅导⼊它: 可空接收者 扩展属性 伴⽣对象的扩展 扩展的作⽤域 50 package com.example.usage import foo.bar.goo // 导⼊所有名为“goo”的扩展 // 或者 import foo.bar.* // 从“foo.bar”导⼊⼀切 fun usage(baz: Baz) { baz.goo() } 更多信息参⻅导⼊ 在⼀个类内部你可以为另⼀个类声明扩展。在这样的扩展内部,有多个 隐式接收者 ⸺ 其中的对象成员可以⽆需通过限定符访问。扩展声明所在的类的实 例称为 分发接收者,扩展⽅法调⽤所在的接收者类型的实例称为 扩展接收者 。 class D { fun bar() { …… } } class C { fun baz() { …… } fun D.foo() { bar() // 调⽤ D.bar baz() // 调⽤ C.baz } fun caller(d: D) { d.foo() // 调⽤扩展函数 } } 对于分发接收者和扩展接收者的成员名字冲突的情况,扩展接收者 优先。要引⽤分发接收者的成员你可以使⽤ 限定的 this 语法。 class C { fun D.foo() { toString() // 调⽤ D.toString() this@C.toString() // 调⽤ C.toString() } 声明为成员的扩展可以声明为 open 并在⼦类中覆盖。这意味着这些函数的分发 对于分发接收者类型是虚拟的,但对于扩展接收者类型是静态的。 扩展声明为成员 51 open class D { } class D1 : D() { } open class C { open fun D.foo() { println(“D.foo in C“) } open fun D1.foo() { println(“D1.foo in C“) } fun caller(d: D) { d.foo() // 调⽤扩展函数 } } class C1 : C() { override fun D.foo() { println(“D.foo in C1“) } override fun D1.foo() { println(“D1.foo in C1“) } } C().caller(D()) // 输出 “D.foo in C“ C1().caller(D()) // 输出 “D.foo in C1“ —— 分发接收者虚拟解析 C().caller(D1()) // 输出 “D.foo in C“ —— 扩展接收者静态解析 在Java中,我们将类命名为“*Utils”:FileUtils 、StringUtils 等,著名的 java.util.Collections 也属于同⼀种命名⽅式。 关于这些 Utils- 类的不愉快的部分是代码写成这样: // Java Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list)) 这些类名总是碍⼿碍脚的,我们可以通过静态导⼊达到这样效果: // Java swap(list, binarySearch(list, max(otherList)), max(list)) 这会变得好⼀点,但是我们并没有从 IDE 强⼤的⾃动补全功能中得到帮助。如果能这样就更好了: // Java list.swap(list.binarySearch(otherList.max()), list.max()) 但是我们不希望在 List 类内实现这些所有可能的⽅法,对吧?这时候扩展将会帮助我们。 动机 52 我们经常创建⼀些只保存数据的类。在这些类中,⼀些标准函数往往是从 数据机械推导⽽来的。在 Kotlin 中,这叫做 数据类 并标记为 data : data class User(val name: String, val age: Int) 编译器⾃动从主构造函数中声明的所有属性导出以下成员: equals() / hashCode() 对, toString() 格式是 “User(name=John, age=42)“ , componentN() 函数 按声明顺序对应于所有属性, copy() 函数(⻅下⽂)。 如果这些函数中的任何⼀个在类体中显式定义或继承⾃其基类型,则不会⽣成该函数。 为了确保⽣成的代码的⼀致性和有意义的⾏为,数据类必须满⾜以下要求: 主构造函数需要⾄少有⼀个参数; 主构造函数的所有参数需要标记为 val 或 var ; 数据类不能是抽象、开放、密封或者内部的; (在1.1之前)数据类只能实现接⼝。 ⾃ 1.1 起,数据类可以扩展其他类(⽰例请参⻅密封类)。 在 JVM 中,如果⽣成的类需要含有⼀个⽆参的构造函数,则所有的属性必须指定默认值。 (参⻅构造函数)。 data class User(val name: String = ““, val age: Int = 0) 在很多情况下,我们需要复制⼀个对象改变它的⼀些属性,但其余部分保持不变。 copy() 函数就是为此⽽⽣成。对于上⽂的 User 类,其实现会类似下 ⾯这样: fun copy(name: String = this.name, age: Int = this.age) = User(name, age) 这让我们可以写 val jack = User(name = “Jack“, age = 1) val olderJack = jack.copy(age = 2) 为数据类⽣成的 Component 函数 使它们可在解构声明中使⽤: val jane = User(“Jane“, 35) val (name, age) = jane println(“$name, $age years of age“) // 输出 “Jane, 35 years of age“ 标准库提供了 Pair 和 Triple 。尽管在很多情况下命名数据类是更好的设计选择, 因为它们通过为属性提供有意义的名称使代码更具可读性。 数据类 — — — — — — — — 复制 数据类和解构声明 标准数据类 53 密封类⽤来表⽰受限的类继承结构:当⼀个值为有限集中的 类型、⽽不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合 也是 受限的,但每个枚举常量只存在⼀个实例,⽽密封类 的⼀个⼦类可以有可包含状态的多个实例。 要声明⼀个密封类,需要在类名前⾯添加 sealed 修饰符。虽然密封类也可以 有⼦类,但是所有⼦类都必须在与密封类⾃⾝相同的⽂件中声明。(在 Kotlin 1.1 之前, 该规则更加严格:⼦类必须嵌套在密封类声明的内部)。 sealed class Expr data class Const(val number: Double) : Expr() data class Sum(val e1: Expr, val e2: Expr) : Expr() object NotANumber : Expr() (上⽂⽰例使⽤了 Kotlin 1.1 的⼀个额外的新功能:数据类扩展包括密封类在内的其他类的可能性。 ) 请注意,扩展密封类⼦类的类(间接继承者)可以放在任何位置,⽽⽆需在 同⼀个⽂件中。 使⽤密封类的关键好处在于使⽤ when 表达式 的时候,如果能够 验证语句覆盖了所有情况,就不需要为该语句再添加⼀个 else ⼦句了。 fun eval(expr: Expr): Double = when(expr) { is Const -> expr.number is Sum -> eval(expr.e1) + eval(expr.e2) NotANumber -> Double.NaN // 不再需要 `else` ⼦句,因为我们已经覆盖了所有的情况 } 密封类 54 与 Java 类似,Kotlin 中的类也可以有类型参数: class Box(t: T) { var value = t } ⼀般来说,要创建这样类的实例,我们需要提供类型参数: val box: Box = Box(1) 但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数: val box = Box(1) // 1 具有类型 Int,所以编译器知道我们说的是 Box。 Java 类型系统中最棘⼿的部分之⼀是通配符类型(参⻅ Java Generics FAQ)。 ⽽ Kotlin 中没有。 相反,它有两个其他的东西:声明处型变(declaration- site variance)与类型投影(type projections)。 ⾸先,让我们思考为什么 Java 需要那些神秘的通配符。在 Effective Java 解释了该问题⸺第28条:利⽤有限制通配符来提升 API 的灵活性。 ⾸先,Java 中的泛型是不型变的不型变的,这意味着 List 并不是不是 List 的⼦类型。 为什么这样? 如果 List 不是不型变的不型变的,它就没 ⽐ Java 的数组好到 哪去,因为如下代码会通过编译然后导致运⾏时异常: // Java List strs = new ArrayList(); List objs = strs; // !!!即将来临的问题的原因就在这⾥。Java 禁⽌这样! objs.add(1); // 这⾥我们把⼀个整数放⼊⼀个字符串列表 String s = strs.get(0); // !!! ClassCastException:⽆法将整数转换为字符串 因此,Java 禁⽌这样的事情以保证运⾏时的安全。但这样会有⼀些影响。例如,考虑 Collection 接⼝中的 addAll() ⽅法。该⽅法的签名应该是什 么?直觉上,我们会这样: // Java interface Collection …… { void addAll(Collection items); } 但随后,我们将⽆法做到以下简单的事情(这是完全安全): // Java void copyAll(Collection to, Collection from) { to.addAll(from); // !!!对于这种简单声明的 addAll 将不能编译: // Collection 不是 Collection 的⼦类型 } (在 Java 中,我们艰难地学到了这个教训,参⻅Effective Java,第25条:列表优先于数组) 这就是为什么 addAll() 的实际签名是以下这样: // Java interface Collection …… { void addAll(Collection items); } 通配符类型参数通配符类型参数 ? extends E 表⽰此⽅法接受 E 的 ⼀些⼦类型对象的集合,⽽不是 E 本⾝。 这意味着我们可以安全地从其中(该集合中的元素是 E 的⼦类的实例)读取读取 E ,但不能写⼊不能写⼊, 因为我们不知道什么对象符合那个未知的 E 的⼦类型。 反过来,该限制可以让 Collection 表⽰ 为 Collection 的⼦类型。 简⽽⾔之,带 extendsextends 限定(上界上界)的通配符类型使得类型是协变的(covariant)协变的(covariant)。 泛型 型变 55 理解为什么这个技巧能够⼯作的关键相当简单:如果只能从集合中获取项⽬,那么使⽤ String 的集合, 并且从其中读取 Object 也没问题 。反过来, 如果只能向集合中 放⼊ 项⽬,就可以⽤ Object 集合并向其中放⼊ String :在 Java 中有 List 是 List 的⼀ 个超类超类。 后者称为逆变性(contravariance)逆变性(contravariance),并且对于 List 你只能调⽤接受 String 作为参数的⽅法 (例如,你可以调⽤ add(String) 或者 set(int, String) ),当然 如果调⽤函数返回 List 中的 T ,你得到的并⾮⼀个 String ⽽是⼀个 Object 。 Joshua Bloch 称那些你只能从中读取读取的对象为⽣产者⽣产者,并称那些你只能写⼊写⼊的对象为消费者消费者。他建议:“为了灵活性最⼤化,在表⽰⽣产者或消费者的输 ⼊参数上使⽤通配符类型”,并提出了以下助记符: PECS 代表⽣产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。 注意:如果你使⽤⼀个⽣产者对象,如 List ,在该对象上不允许调⽤ add() 或 set() 。但这并不意味着 该对象是不可变的不可变的:例 如,没有什么阻⽌你调⽤ clear() 从列表中删除所有项⽬,因为 clear() 根本⽆需任何参数。通配符(或其他类型的型变)保证的唯⼀的事情是类型安类型安 全全。不可变性完全是另⼀回事。 假设有⼀个泛型接⼝ Source ,该接⼝中不存在任何以 T 作为参数的⽅法,只是⽅法返回 T 类型值: // Java interface Source { T nextT(); } 那么,在 Source 类型的变量中存储 Source 实例的引⽤是极为安全的⸺没有消费者-⽅法可以调⽤。但是 Java 并不知道这 ⼀点,并且仍然禁⽌这样操作: // Java void demo(Source strs) { Source objects = strs; // !!!在 Java 中不允许 // …… } 为了修正这⼀点,我们必须声明对象的类型为 Source ,这是毫⽆意义的,因为我们可以像以前⼀样在该对象上调⽤所有相同的 ⽅法,所以更复杂的类型并没有带来价值。但编译器并不知道。 在 Kotlin 中,有⼀种⽅法向编译器解释这种情况。这称为声明处型变声明处型变:我们可以标注 Source 的类型参数类型参数 T 来确保它仅从 Source 成员中返回返回(⽣ 产),并从不被消费。 为此,我们提供 outout 修饰符: abstract class Source { abstract fun nextT(): T } fun demo(strs: Source) { val objects: Source = strs // 这个没问题,因为 T 是⼀个 out-参数 // …… } ⼀般原则是:当⼀个类 C 的类型参数 T 被声明为 outout 时,它就只能出现在 C 的成员的输出输出-位置,但回报是 C 可以安全地作为 C 的超类。 简⽽⾔之,他们说类 C 是在参数 T 上是协变的协变的,或者说 T 是⼀个协变的协变的类型参数。 你可以认为 C 是 T 的⽣产者⽣产者,⽽不是 T 的消费者消费者。 outout修饰符称为型变注解型变注解,并且由于它在类型参数声明处提供,所以我们讲声明处型变声明处型变。 这与 Java 的使⽤处型变使⽤处型变相反,其类型⽤途通配符使得类型协变。 另外除了 outout,Kotlin ⼜补充了⼀个型变注释:inin。它使得⼀个类型参数逆变逆变:只可以被消费⽽不可以 被⽣产。逆变类的⼀个很好的例⼦是 Comparable : 声明处型变声明处型变 56 abstract class Comparable { abstract fun compareTo(other: T): Int } fun demo(x: Comparable) { x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的⼦类型 // 因此,我们可以将 x 赋给类型为 Comparable 的变量 val y: Comparable = x // OK! } 我们相信 inin 和 outout 两词是⾃解释的(因为它们已经在 C# 中成功使⽤很⻓时间了), 因此上⾯提到的助记符不是真正需要的,并且可以将其改写为更⾼的 ⽬标: 存在性(The Existential)存在性(The Existential) 转换:消费者 in, ⽣产者 out! 转换:消费者 in, ⽣产者 out! :-) 将类型参数 T 声明为 out ⾮常⽅便,并且能避免使⽤处⼦类型化的⿇烦,但是有些类实际上不能不能限制为只返回 T ! ⼀个很好的例⼦是 Array: class Array(val size: Int) { fun get(index: Int): T { ///* …… */ } fun set(index: Int, value: T) { ///* …… */ } } 该类在 T 上既不能是协变的也不能是逆变的。这造成了⼀些不灵活性。考虑下述函数: fun copy(from: Array, to: Array) { assert(from.size == to.size) for (i in from.indices) to[i] = from[i] } 这个函数应该将项⽬从⼀个数组复制到另⼀个数组。让我们尝试在实践中应⽤它: val ints: Array = arrayOf(1, 2, 3) val any = Array(3) { ““ } copy(ints, any) // 错误:期望 (Array, Array) 这⾥我们遇到同样熟悉的问题:Array 在 T 上是不型变的不型变的,因此 Array 和 Array 都不是 另⼀个的⼦类型。为什么? 再次重复, 因为 copy 可能可能做坏事,也就是说,例如它可能尝试写写⼀个 String 到 from , 并且如果我们实际上传递⼀个 Int 的数组,⼀段时间后将会抛出⼀个 ClassCastException 异常。 那么,我们唯⼀要确保的是 copy() 不会做任何坏事。我们想阻⽌它写写到 from ,我们可以: fun copy(from: Array, to: Array) { // …… } 这⾥发⽣的事情称为类型投影类型投影:我们说 from 不仅仅是⼀个数组,⽽是⼀个受限制的(投影的投影的)数组:我们只可以调⽤返回类型为类型参数 T 的⽅法,如 上,这意味着我们只能调⽤ get() 。这就是我们的使⽤处型变使⽤处型变的⽤法,并且是对应于 Java 的 Array 、 但使⽤更简单些的⽅式。 你也可以使⽤ inin 投影⼀个类型: fun fill(dest: Array, value: String) { // …… } Array 对应于 Java 的 Array ,也就是说,你可以传递⼀个 CharSequence 数组或⼀个 Object 数组给 fill() 函数。 类型投影 使⽤处型变:类型投影使⽤处型变:类型投影 57 有时你想说,你对类型参数⼀⽆所知,但仍然希望以安全的⽅式使⽤它。 这⾥的安全⽅式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是 该投影的⼦类型。 Kotlin 为此提供了所谓的星投影星投影语法: 对于 Foo ,其中 T 是⼀个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo 。 这意味着当 T 未知时,你可 以安全地从 Foo <*> 读取 TUpper 的值。 对于 Foo ,其中 T 是⼀个逆变类型参数,Foo <*> 等价于 Foo 。 这意味着当 T 未知时,没有什么可以以安全的⽅ 式写⼊ Foo <*> 。 对于 Foo ,其中 T 是⼀个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo ⽽对于写值时等价于 Foo 。 如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function ,我们可以想 象以下星投影: Function<*, String> 表⽰ Function ; Function 表⽰ Function ; Function<*, *> 表⽰ Function 。 注意:星投影⾮常像 Java 的原始类型,但是安全。 不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称之前: fun singletonList(item: T): List { // …… } fun T.basicToString() : String { // 扩展函数 // …… } 要调⽤泛型函数,在调⽤处函数名之后之后指定类型参数即可: val l = singletonList(1) 能够替换给定类型参数的所有可能类型的集合可以由泛型约束泛型约束限制。 最常⻅的约束类型是与 Java 的 extends 关键字对应的 上界上界: fun > sort(list: List) { // …… } 冒号之后指定的类型是上界上界:只有 Comparable 的⼦类型可以替代 T 。 例如 sort(listOf(1, 2, 3)) // OK。Int 是 Comparable 的⼦类型 sort(listOf(HashMap())) // 错误:HashMap 不是 Comparable> 的⼦ 类型 默认的上界(如果没有声明)是 Any? 。在尖括号中只能指定⼀个上界。 如果同⼀类型参数需要多个上界,我们需要⼀个单独的 wherewhere-⼦句: 星投影星投影 — — — — — — 泛型函数 泛型约束 上界上界 58 fun cloneWhenGreater(list: List, threshold: T): List where T : Comparable, T : Cloneable { return list.filter { it > threshold }.map { it.clone() } } 59 类可以嵌套在其他类中 class Outer { private val bar: Int = 1 class Nested { fun foo() = 2 } } val demo = Outer.Nested().foo() // == 2 类可以标记为 inner 以便能够访问外部类的成员。内部类会带有⼀个对外部类的对象的引⽤: class Outer { private val bar: Int = 1 inner class Inner { fun foo() = bar } } val demo = Outer().Inner().foo() // == 1 参⻅限定的 this 表达式以了解内部类中的 this 的消歧义⽤法。 使⽤对象表达式创建匿名内部类实例: window.addMouseListener(object: MouseAdapter() { override fun mouseClicked(e: MouseEvent) { // …… } override fun mouseEntered(e: MouseEvent) { // …… } }) 如果对象是函数式 Java 接⼝(即具有单个抽象⽅法的 Java 接⼝)的实例, 你可以使⽤带接⼝类型前缀的lambda表达式创建它: val listener = ActionListener { println(“clicked“) } 嵌套类 内部类 匿名内部类 60 枚举类的最基本的⽤法是实现类型安全的枚举 enum class Direction { NORTH, SOUTH, WEST, EAST } 每个枚举常量都是⼀个对象。枚举常量⽤逗号分隔。 因为每⼀个枚举都是枚举类的实例,所以他们可以是初始化过的。 enum class Color(val rgb: Int) { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF) } 枚举常量也可以声明⾃⼰的匿名类 enum class ProtocolState { WAITING { override fun signal() = TALKING }, TALKING { override fun signal() = WAITING }; abstract fun signal(): ProtocolState } 及相应的⽅法、以及覆盖基类的⽅法。注意,如果枚举类定义任何 成员,要使⽤分号将成员定义中的枚举常量定义分隔开,就像 在 Java 中⼀样。 就像在 Java 中⼀样,Kotlin 中的枚举类也有合成⽅法允许列出 定义的枚举常量以及通过名称获取枚举常量。这些⽅法的 签名如下(假设枚举类的名称是 EnumClass ): EnumClass.valueOf(value: String): EnumClass EnumClass.values(): Array 如果指定的名称与类中定义的任何枚举常量均不匹配,valueOf() ⽅法将抛出 IllegalArgumentException 异常。 ⾃ Kotlin 1.1 起,可以使⽤ enumValues() 和 enumValueOf() 函数以泛型的⽅式访问枚举类中的常量 : enum class RGB { RED, GREEN, BLUE } inline fun > printAllValues() { print(enumValues().joinToString { it.name }) } printAllValues() // 输出 RED, GREEN, BLUE 每个枚举常量都具有在枚举类声明中获取其名称和位置的属性: val name: String val ordinal: Int 枚举类 初始化 匿名类 使⽤枚举常量 61 枚举常量还实现了 Comparable 接⼝, 其中⾃然顺序是它们在枚举类中定义的顺序。 62 有时候,我们需要创建⼀个对某个类做了轻微改动的类的对象,⽽不⽤为之显式声明新的⼦类。 Java ⽤匿名内部类 处理这种情况。 Kotlin ⽤对象表达 式和对象声明对这个概念稍微概括了下。 要创建⼀个继承⾃某个(或某些)类型的匿名类的对象,我们会这么写: window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { // …… } override fun mouseEntered(e: MouseEvent) { // …… } }) 如果超类型有⼀个构造函数,则必须传递适当的构造函数参数给它。 多个超类型可以由跟在冒号后⾯的逗号分隔的列表指定: open class A(x: Int) { public open val y: Int = x } interface B {……} val ab: A = object : A(1), B { override val y = 15 } 任何时候,如果我们只需要“⼀个对象⽽已”,并不需要特殊超类型,那么我们可以简单地写: fun foo() { val adHoc = object { var x: Int = 0 var y: Int = 0 } print(adHoc.x + adHoc.y) } 请注意,匿名对象可以⽤作只在本地和私有作⽤域中声明的类型。如果你使⽤匿名对象作为公有函数的 返回类型或者⽤作公有属性的类型,那么该函数或 属性的实际类型 会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any 。在匿名对象 中添加的成员将⽆法访问。 class C { // 私有函数,所以其返回类型是匿名对象类型 private fun foo() = object { val x: String = “x“ } // 公有函数,所以其返回类型是 Any fun publicFoo() = object { val x: String = “x“ } fun bar() { val x1 = foo().x // 没问题 val x2 = publicFoo().x // 错误:未能解析的引⽤“x” } } 就像 Java 匿名内部类⼀样,对象表达式中的代码可以访问来⾃包含它的作⽤域的变量。 (与 Java 不同的是,这不仅限于 final 变量。) 对象表达式和对象声明 对象表达式 63 fun countClicks(window: JComponent) { var clickCount = 0 var enterCount = 0 window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { clickCount++ } override fun mouseEntered(e: MouseEvent) { enterCount++ } }) // …… } 单例模式是⼀种⾮常有⽤的模式,⽽ Kotlin(继 Scala 之后)使单例声明变得很容易: object DataProviderManager { fun registerDataProvider(provider: DataProvider) { // …… } val allDataProviders: Collection get() = // …… } 这称为对象声明。并且它总是在 object 关键字后跟⼀个名称。 就像变量声明⼀样,对象声明不是⼀个表达式,不能⽤在赋值语句的右边。 要引⽤该对象,我们直接使⽤其名称即可: DataProviderManager.registerDataProvider(……) 这些对象可以有超类型: object DefaultListener : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { // …… } override fun mouseEntered(e: MouseEvent) { // …… } } 注意注意:对象声明不能在局部作⽤域(即直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或⾮内部类中。 类内部的对象声明可以⽤ companion 关键字标记: class MyClass { companion object Factory { fun create(): MyClass = MyClass() } } 该伴⽣对象的成员可通过只使⽤类名作为限定符来调⽤: val instance = MyClass.create() 对象声明 伴⽣对象伴⽣对象 64 可以省略伴⽣对象的名称,在这种情况下将使⽤名称 Companion : class MyClass { companion object { } } val x = MyClass.Companion 请注意,即使伴⽣对象的成员看起来像其他语⾔的静态成员,在运⾏时他们 仍然是真实对象的实例成员,⽽且,例如还可以实现接⼝: interface Factory { fun create(): T } class MyClass { companion object : Factory { override fun create(): MyClass = MyClass() } } 当然,在 JVM 平台,如果使⽤ @JvmStatic 注解,你可以将伴⽣对象的成员⽣成为真正的 静态⽅法和字段。更详细信息请参⻅Java 互操作性⼀节 。 对象表达式和对象声明之间有⼀个重要的语义差别: 对象表达式是在使⽤他们的地⽅⽴即⽴即执⾏(及初始化)的 对象声明是在第⼀次被访问到时延迟延迟初始化的 伴⽣对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配 对象表达式和对象声明之间的语义差异对象表达式和对象声明之间的语义差异 — — — 65 委托模式已经证明是实现继承的⼀个很好的替代⽅式, ⽽ Kotlin 可以零样板代码地原⽣⽀持它。 类 Derived 可以继承⼀个接⼝ Base ,并将其所有共 有的⽅法委托给⼀个指定的对象: interface Base { fun print() } class BaseImpl(val x: Int) : Base { override fun print() { print(x) } } class Derived(b: Base) : Base by b fun main(args: Array) { val b = BaseImpl(10) Derived(b).print() // 输出 10 } Derived 的超类型列表中的 by-⼦句表⽰ b 将会在 Derived 中内部存储。 并且编译器将⽣成转发给 b 的所有 Base 的⽅法。 委托 类委托 66 有⼀些常⻅的属性类型,虽然我们可以在每次需要的时候⼿动实现它们, 但是如果能够为⼤家把他们只实现⼀次并放⼊⼀个库会更好。例如包括 延迟属性(lazy properties): 其值只在⾸次访问时计算, 可观察属性(observable properties): 监听器会收到有关此属性变更的通知, 把多个属性储存在⼀个映射(map)中,⽽不是每个存在单独的字段中。 为了涵盖这些(以及其他)情况,Kotlin ⽀持 委托属性: class Example { var p: String by Delegate() } 语法是: val/var <属性名>: <类型> by <表达式>。在 by 后⾯的表达式是该 委托, 因为属性对应的 get()(和 set() )会被委托给它的 getValue() 和 setValue() ⽅法。 属性的委托不必实现任何的接⼝,但是需要提供⼀个 getValue() 函数(和 setValue() ⸺对于 var 属 性)。 例如: class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return “$thisRef, thank you for delegating '${property.name}' to me!“ } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println(“$value has been assigned to '${property.name} in $thisRef.'“) } } 当我们从委托到⼀个 Delegate 实例的 p 读取时,将调⽤ Delegate 中的 getValue() 函数, 所以它第⼀个参数是读出 p 的对象、第⼆个参数保 存了对 p ⾃⾝的描述 (例如你可以取它的名字)。 例如: val e = Example() println(e.p) 输出结果: Example@33a17727, thank you for delegating ‘p’ to me! 类似地,当我们给 p 赋值时,将调⽤ setValue() 函数。前两个参数相同,第三个参数保存将要被赋予的值: e.p = “NEW“ 输出结果: NEW has been assigned to ‘p’ in Example@33a17727. 委托对象的要求规范可以在下⽂找到。 请注意,⾃ Kotlin 1.1 起你可以在函数或代码块中声明⼀个委托属性,因此它不⼀定是类的成员。 你可以在下⽂找到其⽰例。 Kotlin 标准库为⼏种有⽤的委托提供了⼯⼚⽅法。 lazy() 是接受⼀个 lambda 并返回⼀个 Lazy 实例的函数,返回的实例可以作为实现延迟属性的委托: 第⼀次调⽤ get() 会执⾏已传递给 lazy() 的 lamda 表达式并记录结果, 后续调⽤ get() 只是返回记录的结果。 委托属性 — — — 标准委托 延迟属性延迟属性 Lazy 67 val lazyValue: String by lazy { println(“computed!“) “Hello“ } fun main(args: Array) { println(lazyValue) println(lazyValue) } 这个例⼦输出: computed! Hello Hello 默认情况下,对于 lazy 属性的求值是同步锁的(synchronized)同步锁的(synchronized):该值只在⼀个线程中计算,并且所有线程 会看到相同的值。如果初始化委托的同步锁不 是必需的,这样多个线程 可以同时执⾏,那么将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。 ⽽如果你确定初始化将总 是发⽣在单个线程,那么你可以使⽤ LazyThreadSafetyMode.NONE 模式, 它不会有任何线程安全的保证和相关的开销。 Delegates.observable() 接受两个参数:初始值和修改时处理程序(handler)。 每当我们给属性赋值时会调⽤该处理程序(在赋值后执⾏)。它有三个 参数:被赋值的属性、旧值和新值: import kotlin.properties.Delegates class User { var name: String by Delegates.observable(““) { prop, old, new -> println(“$old -> $new“) } } fun main(args: Array) { val user = User() user.name = “first“ user.name = “second“ } 这个例⼦输出: -> first first -> second 如果你想能够截获⼀个赋值并“否决”它,就使⽤ vetoable() 取代 observable() 。 在属性被赋新值⽣效之前会调⽤传递给 vetoable 的处理程序。 ⼀个常⻅的⽤例是在⼀个映射(map)⾥存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应⽤中。 在这种情况下,你可以使⽤映射实 例⾃⾝作为委托来实现委托属性。 class User(val map: Map) { val name: String by map val age: Int by map } 在这个例⼦中,构造函数接受⼀个映射参数: val user = User(mapOf( “name“ to “John Doe“, “age“ to 25 )) 委托属性会从这个映射中取值(通过字符串键⸺属性的名称): 可观察属性可观察属性 Observable 把属性储存在映射中 68 println(user.name) // Prints “John Doe“ println(user.age) // Prints 25 这也适⽤于 var 属性,如果把只读的 Map 换成 MutableMap 的话: class MutableUser(val map: MutableMap) { var name: String by map var age: Int by map } 你可以将局部变量声明为委托属性。 例如,你可以使⼀个局部变量惰性初始化: fun example(computeFoo: () -> Foo) { val memoizedFoo by lazy(computeFoo) if (someCondition && memoizedFoo.isValid()) { memoizedFoo.doSomething() } } memoizedFoo 变量只会在第⼀次访问时计算。 如果 someCondition 失败,那么该变量根本不会计算。 这⾥我们总结了委托对象的要求。 对于⼀个只读只读属性(即 val 声明的),委托必须提供⼀个名为 getValue 的函数,该函数接受以下参数: thisRef ⸺ 必须与 属性所有者 类型(对于扩展属性⸺指被扩展的类型)相同或者是它的超类型, property ⸺ 必须是类型 KProperty<*> 或其超类型, 这个函数必须返回与属性相同的类型(或其⼦类型)。 对于⼀个可变可变属性(即 var 声明的),委托必须额外提供⼀个名为 setValue 的函数,该函数接受以下参数: thisRef ⸺ 同 getValue() , property ⸺ 同 getValue() , new value ⸺ 必须和属性同类型或者是它的超类型。 getValue() 或/和 setValue() 函数可以通过委托类的成员函数提供或者由扩展函数提供。 当你需要委托属性到原本未提供的这些函数的对象时后 者会更便利。 两函数都需要⽤ operator 关键字来进⾏标记。 委托类可以实现包含所需 operator ⽅法的 ReadOnlyProperty 或 ReadWriteProperty 接⼝之⼀。 这俩接⼝是在 Kotlin 标准库中声明的: interface ReadOnlyProperty { operator fun getValue(thisRef: R, property: KProperty<*>): T } interface ReadWriteProperty { operator fun getValue(thisRef: R, property: KProperty<*>): T operator fun setValue(thisRef: R, property: KProperty<*>, value: T) } 在每个委托属性的实现的背后,Kotlin 编译器都会⽣成辅助属性并委托给它。 例如,对于属性 prop ,⽣成隐藏属性 prop$delegate ,⽽访问器的代码 只是简单地委托给这个附加属性: 局部委托属性(⾃ 1.1 起) 属性委托要求 — — — — — 翻译规则翻译规则 69 class C { var prop: Type by MyDelegate() } // 这段是由编译器⽣成的相应代码: class C { private val prop$delegate = MyDelegate() var prop: Type get() = prop$delegate.getValue(this, this::prop) set(value: Type) = prop$delegate.setValue(this, this::prop, value) } Kotlin 编译器在参数中提供了关于 prop 的所有必要信息:第⼀个参数 this 引⽤到外部类 C 的实例⽽ this::prop 是 KProperty 类型的反射 对象,该对象描述 prop ⾃⾝。 请注意,直接在代码中引⽤绑定的可调⽤引⽤的语法 this::prop ⾃ Kotlin 1.1 起才可⽤。 通过定义 provideDelegate 操作符,可以扩展创建属性实现所委托对象的逻辑。 如果 by 右侧所使⽤的对象将 provideDelegate 定义为成员或 扩展函数,那么会调⽤该函数来 创建属性委托实例。 provideDelegate 的⼀个可能的使⽤场景是在创建属性时(⽽不仅在其 getter 或 setter 中)检查属性⼀致性。 例如,如果要在绑定之前检查属性名称,可以这样写: class ResourceLoader(id: ResourceID) { operator fun provideDelegate( thisRef: MyUI, prop: KProperty<*> ): ReadOnlyProperty { checkProperty(thisRef, prop.name) // 创建委托 } private fun checkProperty(thisRef: MyUI, name: String) { …… } } fun bindResource(id: ResourceID): ResourceLoader { …… } class MyUI { val image by bindResource(ResourceID.image_id) val text by bindResource(ResourceID.text_id) } provideDelegate 的参数与 getValue 相同: thisRef ⸺ 必须与 属性所有者 类型(对于扩展属性⸺指被扩展的类型)相同或者是它的超类型, property ⸺ 必须是类型 KProperty<*> 或其超类型。 在创建 MyUI 实例期间,为每个属性调⽤ provideDelegate ⽅法,并⽴即执⾏必要的验证。 如果没有这种拦截属性与其委托之间的绑定的能⼒,为了实现相同的功能, 你必须显式传递属性名,这不是很⽅便: 提供委托(⾃提供委托(⾃ 1.1 起)起) — — 70 // 检查属性名称⽽不使⽤“provideDelegate”功能 class MyUI { val image by bindResource(ResourceID.image_id, “image“) val text by bindResource(ResourceID.text_id, “text“) } fun MyUI.bindResource( id: ResourceID, propertyName: String ): ReadOnlyProperty { checkProperty(this, propertyName) // 创建委托 } 在⽣成的代码中,会调⽤ provideDelegate ⽅法来初始化辅助的 prop$delegate 属性。 ⽐较对于属性声明 val prop: Type by MyDelegate() ⽣成的代码与 上⾯(当 provideDelegate ⽅法不存在时)⽣成的代码: class C { var prop: Type by MyDelegate() } // 这段代码是当“provideDelegate”功能可⽤时 // 由编译器⽣成的代码: class C { // 调⽤“provideDelegate”来创建额外的“delegate”属性 private val prop$delegate = MyDelegate().provideDelegate(this, this::prop) val prop: Type get() = prop$delegate.getValue(this, this::prop) } 请注意,provideDelegate ⽅法只影响辅助属性的创建,并不会影响为 getter 或 setter ⽣成的代码。 71 函数和函数和 Lambda 表达式表达式 Kotlin 中的函数使⽤ fun 关键字声明 fun double(x: Int): Int { } 调⽤函数使⽤传统的⽅法 val result = double(2) 调⽤成员函数使⽤点表⽰法 Sample().foo() // 创建类 Sample 实例并调⽤ foo 函数还可以⽤中缀表⽰法调⽤,当 他们是成员函数或扩展函数 他们只有⼀个参数 他们⽤ infix 关键字标注 // 给 Int 定义扩展 infix fun Int.shl(x: Int): Int { …… } // ⽤中缀表⽰法调⽤扩展函数 1 shl 2 // 等同于这样 1.shl(2) 函数参数使⽤ Pascal 表⽰法定义,即 name: type。参数⽤逗号隔开。每个参数必须有显式类型。 fun powerOf(number: Int, exponent: Int) { …… } 函数参数可以有默认值,当省略相应的参数时使⽤默认值。与其他语⾔相⽐,这可以减少 重载数量。 函数 函数声明 函数⽤法 中缀表⽰法中缀表⽰法 — — — 参数参数 默认参数默认参数 72 fun read(b: Array, off: Int = 0, len: Int = b.size()) { …… } 默认值通过类型后⾯的 == 及给出的值来定义。 覆盖⽅法总是使⽤与基类型⽅法相同的默认参数值。 当覆盖⼀个带有默认参数值的⽅法时,必须从签名中省略默认参数值: open class A { open fun foo(i: Int = 10) { …… } } class B : A() { override fun foo(i: Int) { …… } // 不能有默认值 } 可以在调⽤函数时使⽤命名的函数参数。当⼀个函数有⼤量的参数或默认参数时这会⾮常⽅便。 给定以下函数 fun reformat(str: String, normalizeCase: Boolean = true, upperCaseFirstLetter: Boolean = true, divideByCamelHumps: Boolean = false, wordSeparator: Char = ' ') { …… } 我们可以使⽤默认参数来调⽤它 reformat(str) 然⽽,当使⽤⾮默认参数调⽤它时,该调⽤看起来就像 reformat(str, true, true, false, '_') 使⽤命名参数我们可以使代码更具有可读性 reformat(str, normalizeCase = true, upperCaseFirstLetter = true, divideByCamelHumps = false, wordSeparator = '_' ) 并且如果我们不需要所有的参数 reformat(str, wordSeparator = '_') 请注意,在调⽤ Java 函数时不能使⽤命名参数语法,因为 Java 字节码并不 总是保留函数参数的名称。 如果⼀个函数不返回任何有⽤的值,它的返回类型是 Unit 。Unit 是⼀种只有⼀个值⸺ Unit 的类型。这个 值不需要显式返回 命名参数命名参数 返回返回 Unit 的函数的函数 73 fun printHello(name: String?): Unit { if (name != null) println(“Hello ${name}“) else println(“Hi there!“) // `return Unit` 或者 `return` 是可选的 } Unit 返回类型声明也是可选的。上⾯的代码等同于 fun printHello(name: String?) { …… } 当函数返回单个表达式时,可以省略花括号并且在 == 符号之后指定代码体即可 fun double(x: Int): Int = x * 2 当返回值类型可由编译器推断时,显式声明返回类型是可选的 fun double(x: Int) = x * 2 具有块代码体的函数必须始终显式指定返回类型,除⾮他们旨在返回 Unit ,在这种情况下它是可选的。 Kotlin 不推断具有块代码体的函数的返回类型, 因为这样的函数在代码体中可能有复杂的控制流,并且返回 类型对于读者(有时甚⾄对于编译器)是不明显的。 函数的参数(通常是最后⼀个)可以⽤ vararg 修饰符标记: fun asList(vararg ts: T): List { val result = ArrayList() for (t in ts) // ts is an Array result.add(t) return result } 允许将可变数量的参数传递给函数: val list = asList(1, 2, 3) 在函数内部,类型 T 的 vararg 参数的可⻅⽅式是作为 T 数组,即上例中的 ts 变量具有类型 Array 。 只有⼀个参数可以标注为 vararg 。如果 vararg 参数不是列表中的最后⼀个参数, 可以使⽤ 命名参数语法传递其后的参数的值,或者,如果参数具有 函数类型,则通过在括号外部 传⼀个 lambda。 当我们调⽤ vararg -函数时,我们可以⼀个接⼀个地传参,例如 asList(1, 2, 3) ,或者,如果我们已经有⼀个数组 并希望将其内容传给该函数,我 们使⽤伸展(spread)伸展(spread)操作符(在数组前⾯加 * ): val a = arrayOf(1, 2, 3) val list = asList(-1, 0, *a, 4) 在 Kotlin 中函数可以在⽂件顶层声明,这意味着你不需要像⼀些语⾔如 Java、C# 或 Scala 那样创建⼀个类来保存⼀个函数。此外 除了顶层函数,Kotlin 中函数也可以声明在局部作⽤域、作为成员函数以及扩展函数。 单表达式函数单表达式函数 显式返回类型显式返回类型 可变数量的参数(可变数量的参数(Varargs)) 函数作⽤域 局部函数局部函数 74 Kotlin ⽀持局部函数,即⼀个函数在另⼀个函数内部 fun dfs(graph: Graph) { fun dfs(current: Vertex, visited: Set) { if (!visited.add(current)) return for (v in current.neighbors) dfs(v, visited) } dfs(graph.vertices[0], HashSet()) } 局部函数可以访问外部函数(即闭包)的局部变量,所以在上例中,visited 可以是局部变量。 fun dfs(graph: Graph) { val visited = HashSet() fun dfs(current: Vertex) { if (!visited.add(current)) return for (v in current.neighbors) dfs(v) } dfs(graph.vertices[0]) } 成员函数是在类或对象内部定义的函数 class Sample() { fun foo() { print(“Foo“) } } 成员函数以点表⽰法调⽤ Sample().foo() // 创建类 Sample 实例并调⽤ foo 关于类和覆盖成员的更多信息参⻅类和继承 函数可以有泛型参数,通过在函数名前使⽤尖括号指定。 fun singletonList(item: T): List { // …… } 关于泛型函数的更多信息参⻅泛型 内联函数在这⾥讲述 扩展函数在其⾃有章节讲述 ⾼阶函数和 Lambda 表达式在其⾃有章节讲述 成员函数成员函数 泛型函数 内联函数 扩展函数 ⾼阶函数和 Lambda 表达式 尾递归函数 75 Kotlin ⽀持⼀种称为尾递归的函数式编程⻛格。 这允许⼀些通常⽤循环写的算法改⽤递归函数来写,⽽⽆堆栈溢出的⻛险。 当⼀个函数⽤ tailrec 修 饰符标记并满⾜所需的形式时,编译器会优化该递归,留下⼀个快速⽽⾼效的基于循环的版本。 tailrec fun findFixPoint(x: Double = 1.0): Double = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x)) 这段代码计算余弦的不动点(fixpoint of cosine),这是⼀个数学常数。 它只是重复地从 1.0 开始调⽤ Math.cos,直到结果不再改变,产⽣ 0.7390851332151607的结果。最终代码相当于这种更传统⻛格的代码: private fun findFixPoint(): Double { var x = 1.0 while (true) { val y = Math.cos(x) if (x == y) return y x = y } } 要符合 tailrec 修饰符的条件的话,函数必须将其⾃⾝调⽤作为它执⾏的最后⼀个操作。在递归调⽤后有更多代码时,不能使⽤尾递归,并且不能⽤在 try/catch/finally 块中。⽬前尾部递归只在 JVM 后端中⽀持。 76 ⾼阶函数是将函数⽤作参数或返回值的函数。 这种函数的⼀个很好的例⼦是 lock() ,它接受⼀个锁对象和⼀个函数,获取锁,运⾏函数并释放锁: fun lock(lock: Lock, body: () -> T): T { lock.lock() try { return body() } finally { lock.unlock() } } 让我们来检查上⾯的代码:body 拥有函数类型:() -> T, 所以它应该是⼀个不带参数并且返回 T 类型值的函数。 它在 try-代码块内部调⽤、被 lock 保护,其结果由 lock()函数返回。 如果我们想调⽤ lock() 函数,我们可以把另⼀个函数传给它作为参数(参⻅函数引⽤): fun toBeSynchronized() = sharedResource.operation() val result = lock(lock, ::toBeSynchronized) 通常会更⽅便的另⼀种⽅式是传⼀个 lambda 表达式: val result = lock(lock, { sharedResource.operation() }) Lambda 表达式在下⽂会有更详细的描述,但为了继续这⼀段,让我们看⼀个简短的概述: lambda 表达式总是被⼤括号括着, 其参数(如果有的话)在 -> 之前声明(参数类型可以省略), 函数体(如果存在的话)在 -> 后⾯。 在 Kotlin 中有⼀个约定,如果函数的最后⼀个参数是⼀个函数,并且你传递⼀个 lambda 表达式作为相应的参数,你可以在圆括号之外指定它: lock (lock) { sharedResource.operation() } ⾼阶函数的另⼀个例⼦是 map() : fun List.map(transform: (T) -> R): List { val result = arrayListOf() for (item in this) result.add(transform(item)) return result } 该函数可以如下调⽤: val doubled = ints.map { value -> value * 2 } 请注意,如果 lambda 是该调⽤的唯⼀参数,则调⽤中的圆括号可以完全省略。 另⼀个有⽤的约定是,如果函数字⾯值只有⼀个参数, 那么它的声明可以省略(连同 -> ),其名称是 it 。 ints.map { it * 2 } ⾼阶函数和 lambda 表达式 ⾼阶函数 — — — it:单个参数的隐式名称:单个参数的隐式名称 77 这些约定可以写LINQ-⻛格的代码: strings.filter { it.length == 5 }.sortBy { it }.map { it.toUpperCase() } 如果 lambda 表达式的参数未使⽤,那么可以⽤下划线取代其名称: map.forEach { _, value -> println(“$value!“) } 在 lambda 表达式中解构是作为解构声明的⼀部分描述的。 使⽤内联函数有时能提⾼⾼阶函数的性能。 ⼀个 lambda 表达式或匿名函数是⼀个“函数字⾯值”,即⼀个未声明的函数, 但⽴即做为表达式传递。考虑下⾯的例⼦: max(strings, { a, b -> a.length < b.length }) 函数 max 是⼀个⾼阶函数,换句话说它接受⼀个函数作为第⼆个参数。 其第⼆个参数是⼀个表达式,它本⾝是⼀个函数,即函数字⾯值。写成函数的话,它 相当于 fun compare(a: String, b: String): Boolean = a.length < b.length 对于接受另⼀个函数作为参数的函数,我们必须为该参数指定函数类型。 例如上述函数 max 定义如下: fun max(collection: Collection, less: (T, T) -> Boolean): T? { var max: T? = null for (it in collection) if (max == null || less(max, it)) max = it return max } 参数 less 的类型是 (T, T) -> Boolean,即⼀个接受两个类型 T 的参数并返回⼀个布尔值的函数: 如果第⼀个参数⼩于第⼆个那么该函数返回 true。 在上⾯第 4 ⾏代码中,less 作为⼀个函数使⽤:通过传⼊两个 T 类型的参数来调⽤。 如上所写的是就函数类型,或者可以有命名参数,如果你想⽂档化每个参数的含义的话。 val compare: (x: T, y: T) -> Int = …… Lambda 表达式的完整语法形式,即函数类型的字⾯值如下: val sum = { x: Int, y: Int -> x + y } lambda 表达式总是被⼤括号括着, 完整语法形式的参数声明放在括号内,并有可选的类型标注, 函数体跟在⼀个 -> 符号之后。如果推断出的该 lambda 的返回类型不是 Unit ,那么该 lambda 主体中的最后⼀个(或可能是单个)表达式会视为返回值。 如果我们把所有可选标注都留下,看起来如下: 下划线⽤于未使⽤的变量(⾃下划线⽤于未使⽤的变量(⾃ 1.1 起)起) 在在 lambda 表达式中解构(⾃表达式中解构(⾃ 1.1 起)起) 内联函数 Lambda 表达式和匿名函数 函数类型函数类型 Lambda 表达式语法表达式语法 78 val sum: (Int, Int) -> Int = { x, y -> x + y } ⼀个 lambda 表达式只有⼀个参数是很常⻅的。 如果 Kotlin 可以⾃⼰计算出签名,它允许我们不声明唯⼀的参数,并且将隐含地 为我们声明其名称为 it : ints.filter { it > 0 } // 这个字⾯值是“(it: Int) -> Boolean”类型的 我们可以使⽤限定的返回语法从 lambda 显式返回⼀个值。否则,将隐式返回最后⼀个表达式的值。因此,以下两个⽚段是等价的: ints.filter { val shouldFilter = it > 0 shouldFilter } ints.filter { val shouldFilter = it > 0 return@filter shouldFilter } 请注意,如果⼀个函数接受另⼀个函数作为最后⼀个参数,lambda 表达式参数可以在 圆括号参数列表之外传递。 参⻅ callSuffix 的语法。 上⾯提供的 lambda 表达式语法缺少的⼀个东西是指定函数的返回类型的 能⼒。在⼤多数情况下,这是不必要的。因为返回类型可以⾃动推断出来。然⽽, 如果 确实需要显式指定,可以使⽤另⼀种语法: 匿名函数 。 fun(x: Int, y: Int): Int = x + y 匿名函数看起来⾮常像⼀个常规函数声明,除了其名称省略了。其函数体 可以是表达式(如上所⽰)或代码块: fun(x: Int, y: Int): Int { return x + y } 参数和返回类型的指定⽅式与常规函数相同,除了 能够从上下⽂推断出的参数类型可以省略: ints.filter(fun(item) = item > 0) 匿名函数的返回类型推断机制与正常函数⼀样:对于具有表达式函数体的匿名函数将⾃动 推断返回类型,⽽具有代码块函数体的返回类型必须显式 指定 (或者已假定为 Unit )。 请注意,匿名函数参数总是在括号内传递。 允许将函数 留在圆括号外的简写语法仅适⽤于 lambda 表达式。 Lambda表达式和匿名函数之间的另⼀个区别是 ⾮局部返回的⾏为。⼀个不带标签的 return 语句 总是在⽤ fun 关键字声明的函数中返回。这意味着 lambda 表达式中的 return 将从包含它的函数返回,⽽匿名函数中的 return 将从匿名函数⾃⾝返回。 Lambda 表达式或者匿名函数(以及局部函数和对象表达式) 可以访问其 闭包 ,即在外部作⽤域中声明的变量。 与 Java 不同的是可以修改闭包中捕获的 变量: var sum = 0 ints.filter { it > 0 }.forEach { sum += it } print(sum) Kotlin 提供了使⽤指定的 接收者对象 调⽤函数字⾯值的功能。 在函数字⾯值的函数体中,可以调⽤该接收者对象上的⽅法⽽⽆需任何额外的限定符。 这 类似于扩展函数,它允你在函数体内访问接收者对象的成员。 其⽤法的最重要的⽰例之⼀是类型安全的 Groovy-⻛格构建器。 匿名函数匿名函数 闭包闭包 带接收者的函数字⾯值带接收者的函数字⾯值 79 这样的函数字⾯值的类型是⼀个带有接收者的函数类型: sum : Int.(other: Int) -> Int 该函数字⾯值可以这样调⽤,就像它是接收者对象上的⼀个⽅法⼀样: 1.sum(2) 匿名函数语法允许你直接指定函数字⾯值的接收者类型 如果你需要使⽤带接收者的函数类型声明⼀个变量,并在之后使⽤它,这将⾮常有⽤。 val sum = fun Int.(other: Int): Int = this + other 当接收者类型可以从上下⽂推断时,lambda 表达式可以⽤作带接收者的函数字⾯值。 class HTML { fun body() { …… } } fun html(init: HTML.() -> Unit): HTML { val html = HTML() // 创建接收者对象 html.init() // 将该接收者对象传给该 lambda return html } html { // 带接收者的 lambda 由此开始 body() // 调⽤该接收者对象的⼀个⽅法 } 80 使⽤⾼阶函数会带来⼀些运⾏时的效率损失:每⼀个函数都是⼀个对象,并且会捕获⼀个闭包。 即那些在函数体内会访问到的变量。 内存分配(对于函数对 象和类)和虚拟调⽤会引⼊运⾏时间开销。 但是在许多情况下通过内联化 lambda 表达式可以消除这类的开销。 下述函数是这种情况的很好的例⼦。即 lock() 函数可以很容易地在调⽤处内联。 考虑下⾯的情况: lock(l) { foo() } 编译器没有为参数创建⼀个函数对象并⽣成⼀个调⽤。取⽽代之,编译器可以⽣成以下代码: l.lock() try { foo() } finally { l.unlock() } 这个不是我们从⼀开始就想要的吗? 为了让编译器这么做,我们需要使⽤ inline 修饰符标记 lock() 函数: inline fun lock(lock: Lock, body: () -> T): T { // …… } inline 修饰符影响函数本⾝和传给它的 lambda 表达式:所有这些都将内联 到调⽤处。 内联可能导致⽣成的代码增加,但是如果我们使⽤得当(不内联⼤函数),它将在 性能上有所提升,尤其是在循环中的“超多态(megamorphic)”调⽤处。 如果你只想被(作为参数)传给⼀个内联函数的 lamda 表达式中只有⼀些被内联,你可以⽤ noinline 修饰符标记 ⼀些函数参数: inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { // …… } 可以内联的 lambda 表达式只能在内联函数内部调⽤或者作为可内联的参数传递, 但是 noinline 的可以以任何我们喜欢的⽅式操作:存储在字段中、 传送它等等。 需要注意的是,如果⼀个内联函数没有可内联的函数参数并且没有 具体化的类型参数,编译器会产⽣⼀个警告,因为内联这样的函数 很可能并⽆益处(如 果你确认需要内联,则可以关掉该警告)。 在 Kotlin 中,我们可以只使⽤⼀个正常的、⾮限定的 return 来退出⼀个命名或匿名函数。 这意味着要退出⼀个 lambda 表达式,我们必须使⽤⼀个标 签,并且 在 lambda 表达式内部禁⽌使⽤裸 return ,因为 lambda 表达式不能使包含它的函数返回: fun foo() { ordinaryFunction { return // 错误:不能使 `foo` 在此处返回 } } 但是如果 lambda 表达式传给的函数是内联的,该 return 也可以内联,所以它是允许的: 内联函数 禁⽤内联 ⾮局部返回 81 fun foo() { inlineFunction { return // OK:该 lambda 表达式是内联的 } } 这种返回(位于 lambda 表达式中,但退出包含它的函数)称为⾮局部返回。 我们习惯了 在循环中⽤这种结构,其内联函数通常包含: fun hasZeros(ints: List): Boolean { ints.forEach { if (it == 0) return true // 从 hasZeros 返回 } return false } 请注意,⼀些内联函数可能调⽤传给它们的不是直接来⾃函数体、⽽是来⾃另⼀个执⾏ 上下⽂的 lambda 表达式参数,例如来⾃局部对象或嵌套函数。在 这种情况下,该 lambda 表达式中 也不允许⾮局部控制流。为了标识这种情况,该 lambda 表达式参数需要 ⽤ crossinline 修饰符标记: inline fun f(crossinline body: () -> Unit) { val f = object: Runnable { override fun run() = body() } // …… } break 和 continue 在内联的 lambda 表达式中还不可⽤,但我们也计划⽀持它们 有时候我们需要访问⼀个作为参数传给我们的⼀个类型: fun TreeNode.findParentOfType(clazz: Class): T? { var p = parent while (p != null && !clazz.isInstance(p)) { p = p.parent } @Suppress(“UNCHECKED_CAST“) return p as T? } 在这⾥我们向上遍历⼀棵树并且检查每个节点是不是特定的类型。 这都没有问题,但是调⽤处不是很优雅: treeNode.findParentOfType(MyTreeNode::class.java) 我们真正想要的只是传⼀个类型给该函数,即像这样调⽤它: treeNode.findParentOfType() 为能够这么做,内联函数⽀持具体化的类型参数,于是我们可以这样写: inline fun TreeNode.findParentOfType(): T? { var p = parent while (p != null && p !is T) { p = p.parent } return p as T? } 我们使⽤ reified 修饰符来限定类型参数,现在可以在函数内部访问它了, ⼏乎就像是⼀个普通的类⼀样。由于函数是内联的,不需要反射,正常的操作 符如 !is 和 as 现在都能⽤了。此外,我们还可以按照上⾯提到的⽅式调⽤它:myTree.findParentOfType() 。 虽然在许多情况下可能不需要反射,但我们仍然可以对⼀个具体化的类型参数使⽤它: 具体化的类型参数 82 inline fun membersOf() = T::class.members fun main(s: Array) { println(membersOf().joinToString(“\n“)) } 普通的函数(未标记为内联函数的)不能有具体化参数。 不具有运⾏时表⽰的类型(例如⾮具体化的类型参数或者类似于 Nothing 的虚构类型) 不能⽤作 具体化的类型参数的实参。 相关底层描述,请参⻅规范⽂档。 inline 修饰符可⽤于没有幕后字段的属性的访问器。 你可以标注独⽴的属性访问器: val foo: Foo inline get() = Foo() var bar: Bar get() = …… inline set(v) { …… } 你也可以标注整个属性,将它的两个访问器都标记为内联: inline var bar: Bar get() = …… set(v) { …… } 在调⽤处,内联访问器如同内联函数⼀样内联。 内联属性(⾃ 1.1 起) 83 在 Kotlin 1.1 中协程是实验性的。详⻅下⽂ ⼀些 API 启动⻓时间运⾏的操作(例如⽹络 IO、⽂件 IO、CPU 或 GPU 密集型任务等),并要求调⽤者阻塞直到它们完成。协程提供了⼀种避免阻塞线程并 ⽤更廉价、更可控的操作替代线程阻塞的⽅法:协程挂起。 协程通过将复杂性放⼊库来简化异步编程。程序的逻辑可以在协程中顺序地表达,⽽底层库会为我们解决其异步性。该库可以将⽤⼾代码的相关部分包装 为回调、订阅相关事件、在不同线程(甚⾄不同机器!)上调度执⾏,⽽代码则保持如同顺序执⾏⼀样简单。 许多在其他语⾔中可⽤的异步机制可以使⽤ Kotlin 协程实现为库。这包括源于 C# 和 ECMAScript 的 async/await、源于 Go 的 管道 和 select 以及源 于 C# 和 Python ⽣成器/yield。关于提供这些结构的库请参⻅其下⽂描述。 基本上,协程计算可以被挂起⽽⽆需阻塞线程。线程阻塞的代价通常是昂贵的,尤其在⾼负载时,因为只有相对少量线程实际可⽤,因此阻塞其中⼀个会导 致⼀些重要的任务被延迟。 另⼀⽅⾯,协程挂起⼏乎是⽆代价的。不需要上下⽂切换或者 OS 的任何其他⼲预。最重要的是,挂起可以在很⼤程度上由⽤⼾库控制:作为库的作者,我们 可以决定挂起时发⽣什么并根据需求优化/记⽇志/截获。 另⼀个区别是,协程不能在随机的指令中挂起,⽽只能在所谓的挂起点挂起,这会调⽤特别标记的函数。 当我们调⽤标记有特殊修饰符 suspend 的函数时,会发⽣挂起: suspend fun doSomething(foo: Foo): Bar { …… } 这样的函数称为挂起函数,因为调⽤它们可能挂起协程(如果相关调⽤的结果已经可⽤,库可以决定继续进⾏⽽不挂起)。挂起函数能够以与普通函数相同 的⽅式获取参数和返回值,但它们只能从协程和其他挂起函数中调⽤。事实上,要启动协程,必须⾄少有⼀个挂起函数,它通常是匿名的(即它是⼀个挂起 lambda 表达式)。让我们来看⼀个例⼦,⼀个简化的 async() 函数(源⾃ kotlinx.coroutines 库): fun async(block: suspend () -> T) 这⾥的 async() 是⼀个普通函数(不是挂起函数),但是它的 block 参数具有⼀个带 suspend 修饰符的函数类型: suspend () -> T 。所以,当 我们将⼀个 lambda 表达式传给 async() 时,它会是挂起 lambda 表达式,于是我们可以从中调⽤挂起函数: async { doSomething(foo) …… } 继续该类⽐,await() 可以是⼀个挂起函数(因此也可以在⼀个 async {} 块中调⽤),该函数挂起⼀个协程,直到⼀些计算完成并返回其结果: async { …… val result = computation.await() …… } 更多关于 async/await 函数实际在 kotlinx.coroutines 中如何⼯作的信息可以在这⾥找到。 请注意,挂起函数 await() 和 doSomething() 不能在像 main() 这样的普通函数中调⽤: fun main(args: Array) { doSomething() // 错误:挂起函数从⾮协程上下⽂调⽤ } 还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定 suspend 修饰符: 协程 阻塞 vs 挂起 挂起函数 84 interface Base { suspend fun foo() } class Derived: Base { override suspend fun foo() { …… } } 扩展函数(和 lambda 表达式)也可以标记为 suspend ,就像普通的⼀样。这允许创建 DSL 及其他⽤⼾可扩展的 API。在某些情况下,库作者需要阻⽌⽤ ⼾添加新⽅式来挂起协程。 为了实现这⼀点,可以使⽤ @RestrictsSuspension 注解。当接收者类/接⼝ R ⽤它标注时,所有挂起扩展都需要委托给 R 的成员或其它委托给它的 扩展。由于扩展不能⽆限相互委托(程序不会终⽌),这保证所有挂起都通过调⽤ R 的成员发⽣,库的作者就可以完全控制了。 这在少数情况是需要的,当每次挂起在库中以特殊⽅式处理时。例如,当通过 buildSequence() 函数实现下⽂所述的⽣成器时,我们需要确保在协程中 的任何挂起调⽤最终调⽤ yield() 或 yieldAll() ⽽不是任何其他函数。这就是为什么 SequenceBuilder ⽤ @RestrictsSuspension 注解: @RestrictsSuspension public abstract class SequenceBuilder { …… } 参⻅其 Github 上 的源代码。 我们不是在这⾥给出⼀个关于协程如何⼯作的完整解释,然⽽粗略地认识发⽣了什么是相当重要的。 协程完全通过编译技术实现(不需要来⾃ VM 或 OS 端的⽀持),挂起通过代码来⽣效。基本上,每个挂起函数(优化可能适⽤,但我们不在这⾥讨论)都转换 为状态机,其中的状态对应于挂起调⽤。刚好在挂起前,下⼀状态与相关局部变量等⼀起存储在编译器⽣成的类的字段中。在恢复该协程时,恢复局部变量 并且状态机从刚好挂起之后的状态进⾏。 挂起的协程可以作为保持其挂起状态与局部变量的对象来存储和传递。这种对象的类型是 Continuation ,⽽这⾥描述的整个代码转换对应于经典的延 续性传递⻛格(Continuation-passing style)。因此,挂起函数有⼀个 Continuation 类型的额外参数作为⾼级选项。 关于协程⼯作原理的更多细节可以在这个设计⽂档中找到。在其他语⾔(如 C# 或者 ECMAScript 2016)中的 async/await 的类似描述与此相关,虽然它 们实现的语⾔功能可能不像 Kotlin 协程这样通⽤。 协程的设计是实验性的,这意味着它可能在即将发布的版本中更改。当在 Kotlin 1.1 中编译协程时,默认情况下会报⼀个警告:“协程”功能是实验性的。要 移出该警告,你需要指定 opt-in 标志。 由于其实验性状态,标准库中协程相关的 API 放在 kotlin.coroutines.experimental 包下。当设计完成并且实验性状态解除时,最终的 API 会移 动到 kotlin.coroutines ,并且实验包会被保留(可能在⼀个单独的构件中)以实现向后兼容。 重要注意事项重要注意事项:我们建议库作者遵循相同惯例:给暴露基于协程 API 的包添加“experimental”后缀(如 com.example.experimental ),以使你的库保 持⼆进制兼容。当最终 API 发布时,请按照下列步骤操作: 将所有 API 复制到 com.example(没有 experimental 后缀), 保持实验包的向后兼容性。 这将最⼩化你的⽤⼾的迁移问题。 协程有三个主要组成部分: 语⾔⽀持(即如上所述的挂起功能), Kotlin 标准库中的底层核⼼ API, 可以直接在⽤⼾代码中使⽤的⾼级 API。 @RestrictsSuspension 注解注解 协程的内部机制 协程的实验性状态 — — 标准 API — — — 85 底层 API 相对较⼩,并且除了创建更⾼级的库之外,不应该使⽤它。 它由两个主要包组成: kotlin.coroutines.experimental 带有主要类型与下述原语 createCoroutine() startCoroutine() suspendCoroutine() kotlin.coroutines.experimental.intrinsics 带有甚⾄更底层的内在函数如 suspendCoroutineOrReturn 关于这些 API ⽤法的更多细节可以在这⾥找到。 kotlin.coroutines.experimental 中仅有的“应⽤程序级”函数是 buildSequence() buildIterator() 这些包含在 kotlin-stdlib 中因为他们与序列相关。这些函数(我们可以仅限于这⾥的 buildSequence() )实现了 ⽣成器 ,即提供⼀种廉价构建惰 性序列的⽅法: val fibonacciSeq = buildSequence { var a = 0 var b = 1 yield(1) while (true) { yield(a + b) val tmp = a + b a = b b = tmp } } 这通过创建⼀个协程⽣成⼀个惰性的、潜在⽆限的斐波那契数列,该协程通过调⽤ yield() 函数来产⽣连续的斐波纳契数。当在这样的序列的迭代器上 迭代每⼀步,都会执⾏⽣成下⼀个数的协程的另⼀部分。因此,我们可以从该序列中取出任何有限的数字列表,例如 fibonacciSeq.take(8).toList() 结果是 [1, 1, 2, 3, 5, 8, 13, 21]。协程⾜够廉价使这很实⽤。 为了演⽰这样⼀个序列的真正惰性,让我们在调⽤ buildSequence() 内部输出⼀些调试信息: val lazySeq = buildSequence { print(“START “) for (i in 1..5) { yield(i) print(“STEP “) } print(“END“) } // 输出序列的前三个元素 lazySeq.take(3).forEach { print(“$it “) } 运⾏上⾯的代码看,是不是我们输出前三个元素的数字与⽣成循环的 STEP 有交叉。这意味着计算确实是惰性的。要输出 1 ,我们只执⾏到第⼀个 yield(i) ,并且过程中会输出 START 。然后,输出 2 ,我们需要继续下⼀个 yield(i) ,并会输出 STEP 。3 也⼀样。永远不会输出再下⼀个 STEP(以及 END ),因为我们再也没有请求序列的后续元素。 为了⼀次产⽣值的集合(或序列),可以使⽤ yieldAll() 函数: 底层底层 API::kotlin.coroutines — — — — — kotlin.coroutines 中的⽣成器中的⽣成器 API — — 86 val lazySeq = buildSequence { yield(0) yieldAll(1..10) } lazySeq.forEach { print(“$it “) } buildIterator() 的⼯作⽅式类似于 buildSequence() ,但返回⼀个惰性迭代器。 可以通过为 SequenceBuilder 类写挂起扩展(带有上⽂描述的 @RestrictsSuspension 注解)来为 buildSequence() 添加⾃定义⽣产逻辑 (custom yielding logic): suspend fun SequenceBuilder.yieldIfOdd(x: Int) { if (x % 2 != 0) yield(x) } val lazySeq = buildSequence { for (i in 1..10) yieldIfOdd(i) } 只有与协程相关的核⼼ API 可以从 Kotlin 标准库获得。这主要包括所有基于协程的库可能使⽤的核⼼原语和接⼝。 ⼤多数基于协程的应⽤程序级API都作为单独的库发布:kotlinx.coroutines。这个库覆盖了 使⽤ kotlinx-coroutines-core 的平台⽆关异步编程 此模块包括⽀持 select 和其他便利原语的类似 Go 的管道 这个库的综合指南在这⾥。 基于 JDK 8 中的 CompletableFuture 的 API:kotlinx-coroutines-jdk8 基于 JDK 7 及更⾼版本 API 的⾮阻塞 IO(NIO):kotlinx-coroutines-nio ⽀持 Swing ( kotlinx-coroutines-swing ) 和 JavaFx ( kotlinx-coroutines-javafx ) ⽀持 RxJava:kotlinx-coroutines-rx 这些库既作为使通⽤任务易⽤的便利的 API,也作为如何构建基于协程的库的端到端⽰例。 其他⾼级其他⾼级 API::kotlinx.coroutines — — — — — — — 87 其他其他 有时把⼀个对象 解构 成很多变量会很⽅便,例如: val (name, age) = person 这种语法称为 解构声明 。⼀个解构声明同时创建多个变量。 我们已经声明了两个新变量:name 和 age ,并且可以独⽴使⽤它们: println(name) println(age) ⼀个解构声明会被编译成以下代码: val name = person.component1() val age = person.component2() 其中的 component1() 和 component2() 函数是在 Kotlin 中⼴泛使⽤的 约定原则 的另⼀个例⼦。 (参⻅像 + 和 * 、for-循环等操作符)。 任何表 达式都可以出现在解构声明的右侧,只要可以对它调⽤所需数量的 component 函数即可。 当然,可以有 component3() 和 component4() 等等。 请注意,componentN() 函数需要⽤ operator 关键字标记,以允许在解构声明中使⽤它们。 解构声明也可以⽤在 for-循环中:当你写 for ((a, b) in collection) { …… } 变量 a 和 b 的值取⾃对集合中的元素上调⽤ component1() 和 component2() 的返回值。 让我们假设我们需要从⼀个函数返回两个东西。例如,⼀个结果对象和⼀个某种状态。 在 Kotlin 中⼀个简洁的实现⽅式是声明⼀个数据类并返回其实例: data class Result(val result: Int, val status: Status) fun function(……): Result { // 各种计算 return Result(result, status) } // 现在,使⽤该函数: val (result, status) = function(……) 因为数据类⾃动声明 componentN() 函数,所以这⾥可以⽤解构声明。 注意注意:我们也可以使⽤标准类 Pair 并且让 function() 返回 Pair , 但是让数据合理命名通常更好。 可能遍历⼀个映射(map)最好的⽅式就是这样: for ((key, value) in map) { // 使⽤该 key、value 做些事情 } 解构声明 例:从函数中返回两个变量 例:解构声明和映射 88 为使其能⽤,我们应该 通过提供⼀个 iterator() 函数将映射表⽰为⼀个值的序列, 通过提供函数 component1() 和 component2() 来将每个元素呈现为⼀对。 当然事实上,标准库提供了这样的扩展: operator fun Map.iterator(): Iterator> = entrySet().iterator() operator fun Map.Entry.component1() = getKey() operator fun Map.Entry.component2() = getValue() 因此你可以在 for-循环中对映射(以及数据类实例的集合等)⾃由使⽤解构声明。 如果在解构声明中你不需要某个变量,那么可以⽤下划线取代其名称: val (_, status) = getResult() 你可以对 lambda 表达式参数使⽤解构声明语法。 如果 lambda 表达式具有 Pair 类型(或者 Map.Entry 或任何其他具有相应 componentN 函数 的类型)的参数,那么可以通过将它们放在括号中来引⼊多个新参数来取代单个新参数: map.mapValues { entry -> “${entry.value}!“ } map.mapValues { (key, value) -> “$value!“ } 注意声明两个参数和声明⼀个解构对来取代单个参数之间的区别: { a //-> …… } // ⼀个参数 { a, b //-> …… } // 两个参数 { (a, b) //-> …… } // ⼀个解构对 { (a, b), c //-> …… } // ⼀个解构对以及其他参数 如果解构的参数中的⼀个组件未使⽤,那么可以将其替换为下划线,以避免编造其名称: map.mapValues { (_, value) -> “$value!“ } 你可以指定整个解构的参数的类型或者分别指定特定组件的类型: map.mapValues { (_, value): Map.Entry -> “$value!“ } map.mapValues { (_, value: String) -> “$value!“ } — — 下划线⽤于未使⽤的变量(⾃ 1.1 起) 在 lambda 表达式中解构(⾃ 1.1 起) 89 与⼤多数语⾔不同,Kotlin 区分可变集合和不可变集合(lists、sets、maps 等)。精确控制什么时候集合可编辑有助于消除 bug 和设计良好的 API。 预先了解⼀个可变集合的只读 视图 和⼀个真正的不可变集合之间的区别是很重要的。它们都容易创建,但类型系统不能表达它们的差别,所以由你来跟踪 (是否相关)。 Kotlin 的 List 类型是⼀个提供只读操作如 size 、get 等的接⼝。和 Java 类似,它继承⾃ Collection 进⽽继承⾃ Iterable 。改变 list 的⽅法是由 MutableList 加⼊的。这⼀模式同样适⽤于 Set/MutableSet 及 Map/MutableMap 。 我们可以看下 list 及 set 类型的基本⽤法: val numbers: MutableList = mutableListOf(1, 2, 3) val readOnlyView: List = numbers println(numbers) // 输出 “[1, 2, 3]“ numbers.add(4) println(readOnlyView) // 输出 “[1, 2, 3, 4]“ readOnlyView.clear() // -> 不能编译 val strings = hashSetOf(“a“, “b“, “c“, “c“) assert(strings.size == 3) Kotlin 没有专⻔的语法结构创建 list 或 set。 要⽤标准库的⽅法,如 listOf() 、 mutableListOf() 、 setOf() 、 mutableSetOf() 。 在⾮性能关 键代码中创建 map 可以⽤⼀个简单的惯⽤法来完成:mapOf(a to b, c to d) 注意上⾯的 readOnlyView 变量(译者注:与对应可变集合变量 numbers )指向相同的底层 list 并会随之改变。 如果⼀个 list 只存在只读引⽤,我们可 以考虑该集合完全不可变。创建⼀个这样的集合的⼀个简单⽅式如下: val items = listOf(1, 2, 3) ⽬前 listOf ⽅法是使⽤ array list 实现的,但是未来可以利⽤它们知道⾃⼰不能变的事实,返回更节约内存的完全不可变的集合类型。 注意这些类型是协变的。这意味着,你可以把⼀个 List 赋值给 List 假定 Rectangle 继承⾃ Shape。对于可变集合类型这是 不允许的,因为这将导致运⾏时故障。 有时你想给调⽤者返回⼀个集合在某个特定时间的⼀个快照, ⼀个保证不会变的: class Controller { private val _items = mutableListOf() val items: List get() = _items.toList() } 这个 toList 扩展⽅法只是复制列表项,因此返回的 list 保证永远不会改变。 List 和 set 有很多有⽤的扩展⽅法值得熟悉: val items = listOf(1, 2, 3, 4) items.first() == 1 items.last() == 4 items.filter { it % 2 == 0 } // 返回 [2, 4] val rwList = mutableListOf(1, 2, 3) rwList.requireNoNulls() // 返回 [1, 2, 3] if (rwList.none { it > 6 }) println(“No items above 6“) // 输出“No items above 6” val item = rwList.firstOrNull() …… 以及所有你所期望的实⽤⼯具,例如 sort、zip、fold、reduce 等等。 Map 遵循同样模式。它们可以容易地实例化和访问,像这样: val readWriteMap = hashMapOf(“foo“ to 1, “bar“ to 2) println(readWriteMap[“foo“]) // 输出“1” val snapshot: Map = HashMap(readWriteMap) 集合 90 区间表达式由具有操作符形式 .. 的 rangeTo 函数辅以 in 和 !in 形成。 区间是为任何可⽐较类型定义的,但对于整型原⽣类型,它有⼀个优化的实 现。以下是使⽤区间的⼀些⽰例 if (i in 1..10) { // 等同于 1 <= i && i <= 10 println(i) } 整型区间( IntRange 、 LongRange 、 CharRange )有⼀个额外的特性:它们可以迭代。 编译器负责将其转换为类似 Java 的基于索引的 for-循环⽽ ⽆额外开销。 for (i in 1..4) print(i) // 输出“1234” for (i in 4..1) print(i) // 什么都不输出 如果你想倒序迭代数字呢?也很简单。你可以使⽤标准库中定义的 downTo() 函数 for (i in 4 downTo 1) print(i) // 输出“4321” 能否以不等于 1 的任意步⻓迭代数字? 当然没问题, step() 函数有助于此 for (i in 1..4 step 2) print(i) // 输出“13” for (i in 4 downTo 1 step 2) print(i) // 输出“42” 要创建⼀个不包括其结束元素的区间,可以使⽤ until 函数: for (i in 1 until 10) { // i in [1, 10) 排除了 10 println(i) } 区间实现了该库中的⼀个公共接⼝:ClosedRange 。 ClosedRange 在数学意义上表⽰⼀个闭区间,它是为可⽐较类型定义的。 它有两个端点:start 和 endInclusive 他们都包含在区间内。 其主 要操作是 contains ,通常以 in/!in 操作符形式使⽤。 整型数列( IntProgression 、 LongProgression 、 CharProgression )表⽰等差数列。 数列由 first 元素、last 元素和⾮零的 step 定义。 第⼀个元素是 first ,后续元素是前⼀个元素加上 step 。 last 元素总会被迭代命中,除⾮该数列是空的。 数列是 Iterable 的⼦类型,其中 N 分别为 Int 、 Long 或者 Char ,所以它可⽤于 for-循环以及像 map 、filter 等函数中。 对 Progression 迭代相当于 Java/JavaScript 的基于索引的 for-循环: for (int i = first; i != last; i += step) { // …… } 对于整型类型,.. 操作符创建⼀个同时实现 ClosedRange 和 *Progression 的对象。 例如,IntRange 实现了 ClosedRange 并扩 展⾃ IntProgression ,因此为 IntProgression 定义的所有操作也可⽤于 IntRange 。 downTo() 和 step() 函数的结果总是⼀个 *Progression 。 数列由在其伴⽣对象中定义的 fromClosedRange 函数构造: IntProgression.fromClosedRange(start, end, step) 数列的 last 元素这样计算:对于正的 step 找到不⼤于 end 值的最⼤值、或者对于负的 step 找到不⼩于 end 值的最⼩值,使得 (last - first) % increment == 0 。 区间 它是如何⼯作的 91 整型类型的 rangeTo() 操作符只是调⽤ *Range 类的构造函数,例如: class Int { //…… operator fun rangeTo(other: Long): LongRange = LongRange(this, other) //…… operator fun rangeTo(other: Int): IntRange = IntRange(this, other) //…… } 浮点数( Double 、 Float )未定义它们的 rangeTo 操作符,⽽使⽤标准库提供的泛型 Comparable 类型的操作符: public operator fun > T.rangeTo(that: T): ClosedRange 该函数返回的区间不能⽤于迭代。 扩展函数 downTo() 是为任何整型类型对定义的,这⾥有两个例⼦: fun Long.downTo(other: Int): LongProgression { return LongProgression.fromClosedRange(this, other.toLong(), -1L) } fun Byte.downTo(other: Int): IntProgression { return IntProgression.fromClosedRange(this.toInt(), other, -1) } 扩展函数 reversed() 是为每个 *Progression 类定义的,并且所有这些函数返回反转后的数列。 fun IntProgression.reversed(): IntProgression { return IntProgression.fromClosedRange(last, first, -step) } 扩展函数 step() 是为每个 *Progression 类定义的, 所有这些函数都返回带有修改了 step 值(函数参数)的数列。 步⻓(step)值必须始终为正, 因此该函数不会更改迭代的⽅向。 fun IntProgression.step(step: Int): IntProgression { if (step <= 0) throw IllegalArgumentException(“Step must be positive, was: $step“) return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step) } fun CharProgression.step(step: Int): CharProgression { if (step <= 0) throw IllegalArgumentException(“Step must be positive, was: $step“) return CharProgression.fromClosedRange(first, last, if (this.step > 0) step else -step) } 请注意,返回数列的 last 值可能与原始数列的 last 值不同,以便保持不变式 (last - first) % step == 0 成⽴。这⾥是⼀个例⼦: (1..12 step 2).last == 11 // 值为 [1, 3, 5, 7, 9, 11] 的数列 (1..12 step 3).last == 10 // 值为 [1, 4, 7, 10] 的数列 (1..12 step 4).last == 9 // 值为 [1, 5, 9] 的数列 ⼀些实⽤函数 rangeTo() downTo() reversed() step() 92 我们可以在运⾏时通过使⽤ is 操作符或其否定形式 !is 来检查对象是否符合给定类型: if (obj is String) { print(obj.length) } if (obj !is String) { // 与 !(obj is String) 相同 print(“Not a String“) } else { print(obj.length) } 在许多情况下,不需要在 Kotlin 中使⽤显式转换操作符,因为编译器跟踪 不可变值的 is -检查,并在需要时⾃动插⼊(安全的)转换: fun demo(x: Any) { if (x is String) { print(x.length) // x ⾃动转换为字符串 } } 编译器⾜够聪明,能够知道如果反向检查导致返回那么该转换是安全的: if (x !is String) return print(x.length) // x ⾃动转换为字符串 或者在 && 和 || 的右侧: // `||` 右侧的 x ⾃动转换为字符串 if (x !is String || x.length == 0) return // `&&` 右侧的 x ⾃动转换为字符串 if (x is String && x.length > 0) { print(x.length) // x ⾃动转换为字符串 } 这些 智能转换 ⽤于 when-表达式 和 while-循环 也⼀样: when (x) { is Int -> print(x + 1) is String -> print(x.length + 1) is IntArray -> print(x.sum()) } 请注意,当编译器不能保证变量在检查和使⽤之间不可改变时,智能转换不能⽤。 更具体地,智能转换能否适⽤根据以下规则: val 局部变量⸺总是可以; val 属性⸺如果属性是 private 或 internal,或者该检查在声明属性的同⼀模块中执⾏。智能转换不适⽤于 open 的属性或者具有⾃定义 getter 的 属性; var 局部变量⸺如果变量在检查和使⽤之间没有修改、并且没有在会修改它的 lambda 中捕获; var 属性⸺决不可能(因为该变量可以随时被其他代码修改)。 类型的检查与转换 is 和 !is 操作符 智能转换 — — — — “不安全的”转换操作符 93 通常,如果转换是不可能的,转换操作符会抛出⼀个异常。因此,我们称之为不安全的。 Kotlin 中的不安全转换由中缀操作符 as(参⻅operator precedence)完成: val x: String = y as String 请注意,null 不能转换为 String 因该类型不是可空的, 即如果 y 为空,上⾯的代码会抛出⼀个异常。 为了匹配 Java 转换语义,我们必须在转换右边 有可空类型,就像: val x: String? = y as String? 为了避免抛出异常,可以使⽤安全转换操作符 as?,它可以在失败时返回 null: val x: String? = y as? String 请注意,尽管事实上 as? 的右边是⼀个⾮空类型的 String ,但是其转换的结果是可空的。 “安全的”(可空)转换操作符 94 为了表⽰当前的 接收者 我们使⽤ this 表达式: 在类的成员中,this 指的是该类的当前对象 在扩展函数或者带接收者的函数字⾯值中, this 表⽰在点左侧传递的 接收者 参数。 如果 this 没有限定符,它指的是最内层的包含它的作⽤域。要引⽤其他作⽤域中的 this,请使⽤ 标签限定符: 要访问来⾃外部作⽤域的this(⼀个类 或者扩展函数, 或者带标签的带接收者的函数字⾯值)我们使⽤ this@label ,其中 @label 是⼀个 代指 this 来源的标签: class A { // 隐式标签 @A inner class B { // 隐式标签 @B fun Int.foo() { // 隐式标签 @foo val a = this@A // A 的 this val b = this@B // B 的 this val c = this // foo() 的接收者,⼀个 Int val c1 = this@foo // foo() 的接收者,⼀个 Int val funLit = lambda@ fun String.() { val d = this // funLit 的接收者 } val funLit2 = { s: String -> // foo() 的接收者,因为它包含的 lambda 表达式 // 没有任何接收者 val d1 = this } } } } This 表达式 — — 限定的 this 95 Kotlin 中有两种类型的相等性: 引⽤相等(两个引⽤指向同⼀对象) 结构相等(⽤ equals() 检查) 引⽤相等由 ===(以及其否定形式 !== )操作判断。a === b 当且仅当 a 和 b 指向同⼀个对象时求值为 true。 结构相等由 ==(以及其否定形式 != )操作判断。按照惯例,像 a == b 这样的表达式会翻译成 a?.equals(b) ?: (b === null) 也就是说如果 a 不是 null 则调⽤ equals(Any?) 函数,否则(即 a 是 null )检查 b 是否与 null 引⽤相等。 请注意,当与 null 显式⽐较时完全没必要优化你的代码:a == null 会被⾃动转换为 a=== null 。 相等性 — — 引⽤相等 结构相等 96 Kotlin 允许我们为⾃⼰的类型提供预定义的⼀组操作符的实现。这些操作符具有固定的符号表⽰ (如 + 或 * )和固定的优先级。为实现这样的操作符,我 们为相应的类型(即⼆元操作符左侧的类型和⼀元操作符的参数类型)提供了⼀个固定名字的成员函数 或扩展函数。 重载操作符的函数需要⽤ operator 修饰符标记。 另外,我们描述为不同操作符规范操作符重载的约定。 表达式表达式 翻译为翻译为 +a a.unaryPlus() -a a.unaryMinus() !a a.not() 这个表是说,当编译器处理例如表达式 +a 时,它执⾏以下步骤: 确定 a 的类型,令其为 T 。 为接收者 T 查找⼀个带有 operator 修饰符的⽆参函数 unaryPlus(),即成员函数或扩展函数。 如果函数不存在或不明确,则导致编译错误。 如果函数存在且其返回类型为 R ,那就表达式 +a 具有类型 R 。 注意 这些操作以及所有其他操作都针对基本类型做了优化,不会为它们引⼊函数调⽤的开销。 以下是如何重载⼀元减运算符的⽰例: data class Point(val x: Int, val y: Int) operator fun Point.unaryMinus() = Point(-x, -y) val point = Point(10, 20) println(-point) // 输出“(-10, -20)” 表达式表达式 翻译为翻译为 a++ a.inc() + ⻅下⽂ a-- a.dec() + ⻅下⽂ inc() 和 dec() 函数必须返回⼀个值,它⽤于赋值给使⽤ ++ 或 -- 操作的变量。它们不应该改变在其上调⽤ inc() 或 dec() 的对象。 编译器执⾏以下步骤来解析后缀形式的操作符,例如 a++ : 确定 a 的类型,令其为 T 。 查找⼀个适⽤于类型为 T 的接收者的、带有 operator 修饰符的⽆参数函数 inc() 。 检查函数的返回类型是 T 的⼦类型。 计算表达式的步骤是: 把 a 的初始值存储到临时存储 a0 中, 把 a.inc() 结果赋值给 a , 把 a0 作为表达式的结果返回。 对于 a-- ,步骤是完全类似的。 对于前缀形式 ++a 和 --a 以相同⽅式解析,其步骤是: 把 a.inc() 结果赋值给 a , 操作符符重载 ⼀元操作 ⼀元前缀操作符⼀元前缀操作符 — — — — 递增和递减递增和递减 — — — — — — — 97 把 a 的新值作为表达式结果返回。 表达式表达式 翻译为翻译为 a + b a.plus(b) a - b a.minus(b) a * b a.times(b) a / b a.div(b) a % b a.rem(b)、 a.mod(b) (已弃⽤) a..b a.rangeTo(b) 对于此表中的操作,编译器只是解析成翻译为列中的表达式。 请注意,⾃ Kotlin 1.1 起⽀持 rem 运算符。Kotlin 1.0 使⽤ mod 运算符,它在 Kotlin 1.1 中被弃⽤。 下⾯是⼀个从给定值起始的 Counter 类的⽰例,它可以使⽤重载的 + 运算符来增加计数。 data class Counter(var dayIndex: Int) { operator fun plus(increment: Int): Counter { return Counter(dayIndex + increment) } } 表达式表达式 翻译为翻译为 a in b b.contains(a) a !in b !b.contains(a) 对于 in 和 !in ,过程是相同的,但是参数的顺序是相反的。 表达式表达式 翻译为翻译为 a[i] a.get(i) a[i, j] a.get(i, j) a[i_1, ……, i_n] a.get(i_1, ……, i_n) a[i] = b a.set(i, b) a[i, j] = b a.set(i, j, b) a[i_1, ……, i_n] = b a.set(i_1, ……, i_n, b) ⽅括号转换为调⽤带有适当数量参数的 get 和 set 。 表达式表达式 翻译为翻译为 a() a.invoke() — ⼆元操作 算术运算符算术运算符 ⽰例⽰例 “In”操作符操作符 索引访问操作符索引访问操作符 调⽤操作符调⽤操作符 98 a(i) a.invoke(i) a(i, j) a.invoke(i, j) a(i_1, ……, i_n) a.invoke(i_1, ……, i_n) 表达式表达式 翻译为翻译为 圆括号转换为调⽤带有适当数量参数的 invoke 。 表达式表达式 翻译为翻译为 a += b a.plusAssign(b) a -= b a.minusAssign(b) a *= b a.timesAssign(b) a /= b a.divAssign(b) a %= b a.modAssign(b) 对于赋值操作,例如 a += b,编译器执⾏以下步骤: 如果右列的函数可⽤ 如果相应的⼆元函数(即 plusAssign() 对应于 plus() )也可⽤,那么报告错误(模糊)。 确保其返回类型是 Unit ,否则报告错误。 ⽣成 a.plusAssign(b) 的代码 否则试着⽣成 a = a + b 的代码(这⾥包含类型检查:a + b 的类型必须是 a 的⼦类型)。 注意:赋值在 Kotlin 中不是表达式。 表达式表达式 翻译为翻译为 a == b a?.equals(b) ?: (b === null) a != b !(a?.equals(b) ?: (b === null)) 注意:=== 和 !==(同⼀性检查)不可重载,因此不存在对他们的约定 这个 == 操作符有些特殊:它被翻译成⼀个复杂的表达式,⽤于筛选 null 值。 null == null 总是 true,对于⾮空的 x ,x == null 总是 false ⽽ 不会调⽤ x.equals() 。 表达式表达式 翻译为翻译为 a > b a.compareTo(b) > 0 a < b a.compareTo(b) < 0 a >= b a.compareTo(b) >= 0 a <= b a.compareTo(b) <= 0 所有的⽐较都转换为对 compareTo 的调⽤,这个函数需要返回 Int 值 我们可以通过中缀函数的调⽤ 来模拟⾃定义中缀操作符。 ⼴义赋值⼴义赋值 — — — — — 相等与不等操作符相等与不等操作符 ⽐较操作符⽐较操作符 命名函数的中缀调⽤ 99 Kotlin 的类型系统旨在消除来⾃代码空引⽤的危险,也称为《⼗亿美元的错误》。 许多编程语⾔(包括 Java)中最常⻅的陷阱之⼀是访问空引⽤的成员,导致空引⽤异常。在 Java 中, 这等同于 NullPointerException 或简称 NPE 。 Kotlin 的类型系统旨在从我们的代码中消除 NullPointerException 。NPE 的唯⼀可能的原因可能是 显式调⽤ throw NullPointerException() 使⽤了下⽂描述的 !! 操作符 外部 Java 代码导致的 对于初始化,有⼀些数据不⼀致(如⼀个未初始化的 this ⽤于构造函数的某个地⽅) 在 Kotlin 中,类型系统区分⼀个引⽤可以容纳 null (可空引⽤)还是不能容纳(⾮空引⽤)。 例如,String 类型的常规变量不能容纳 null: var a: String = “abc“ a = null // 编译错误 如果要允许为空,我们可以声明⼀个变量为可空字符串,写作 String? : var b: String? = “abc“ b = null // ok 现在,如果你调⽤ a 的⽅法或者访问它的属性,它保证不会导致 NPE ,这样你就可以放⼼地使⽤: val l = a.length 但是如果你想访问 b 的同⼀个属性,那么这是不安全的,并且编译器会报告⼀个错误: val l = b.length // 错误:变量“b”可能为空 但是我们还是需要访问该属性,对吧?有⼏种⽅式可以做到。 ⾸先,你可以显式检查 b 是否为 null,并分别处理两种可能: val l = if (b != null) b.length else -1 编译器会跟踪所执⾏检查的信息,并允许你在 if 内部调⽤ length 。 同时,也⽀持更复杂(更智能)的条件: if (b != null && b.length > 0) { print(“String of length ${b.length}“) } else { print(“Empty string“) } 请注意,这只适⽤于 b 是不可变的情况(即在检查和使⽤之间没有修改过的局部变量 ,或者不可覆盖并且有幕后字段的 val 成员),因为否则可能会发⽣ 在检查之后 b ⼜变为 null 的情况。 你的第⼆个选择是安全调⽤操作符,写作 ?. : b?.length 如果 b ⾮空,就返回 b.length ,否则返回 null,这个表达式的类型是 Int? 。 空安全 可空类型与⾮空类型 — — — — 在条件中检查 null 安全的调⽤ 100 安全调⽤在链式调⽤中很有⽤。例如,如果⼀个员⼯ Bob 可能会(或者不会)分配给⼀个部⻔, 并且可能有另外⼀个员⼯是该部⻔的负责⼈,那么获取 Bob 所在部⻔负责⼈(如果有的话)的名字,我们写作: bob?.department?.head?.name 如果任意⼀个属性(环节)为空,这个链式调⽤就会返回 null。 如果要只对⾮空值执⾏某个操作,安全调⽤操作符可以与 let ⼀起使⽤: val listWithNulls: List = listOf(“A“, null) for (item in listWithNulls) { item?.let { println(it) } // 输出 A 并忽略 null } 当我们有⼀个可空的引⽤ r 时,我们可以说“如果 r ⾮空,我使⽤它;否则使⽤某个⾮空的值 x ”: val l: Int = if (b != null) b.length else -1 除了完整的 if-表达式,这还可以通过 Elvis 操作符表达,写作 ?: : val l = b?.length ?: -1 如果 ?: 左侧表达式⾮空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。 请注意,当且仅当左侧为空时,才会对右侧表达式求值。 请注意,因为 throw 和 return 在 Kotlin 中都是表达式,所以它们也可以⽤在 elvis 操作符右侧。这可能会⾮常⽅便,例如,检查函数参数: fun foo(node: Node): String? { val parent = node.getParent() ?: return null val name = node.getName() ?: throw IllegalArgumentException(“name expected“) // …… } 第三种选择是为 NPE 爱好者准备的。我们可以写 b!! ,这会返回⼀个⾮空的 b 值 (例如:在我们例⼦中的 String )或者如果 b 为空,就会抛出⼀个 NPE 异常: val l = b!!.length 因此,如果你想要⼀个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期⽽⾄。 如果对象不是⽬标类型,那么常规类型转换可能会导致 ClassCastException 。 另⼀个选择是使⽤安全的类型转换,如果尝试转换不成功则返回 null: val aInt: Int? = a as? Int 如果你有⼀个可空类型元素的集合,并且想要过滤⾮空元素,你可以使⽤ filterNotNull 来实现。 val nullableList: List = listOf(1, 2, null, 4) val intList: List = nullableList.filterNotNull() Elvis 操作符 !! 操作符 安全的类型转换 可空类型的集合 101 Kotlin 中所有异常类都是 Throwable 类的⼦孙类。 每个异常都有消息、堆栈回溯信息和可选的原因。 使⽤ throw-表达式来抛出异常。 throw MyException(“Hi There!“) 使⽤ try-表达式来捕获异常。 try { // ⼀些代码 } catch (e: SomeException) { // 处理程序 } finally { // 可选的 finally 块 } 可以有零到多个 catch 块。finally 块可以省略。 但是 catch 和 finally 块⾄少应该存在⼀个。 try 是⼀个表达式,即它可以有⼀个返回值。 val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null } try-表达式的返回值是 try 块中的 最后⼀个表达式或者是(所有)catch 块中的最后⼀个表达式。 finally 块中的内容不会影响表达式的结果。 Kotlin 没有受检的异常。这其中有很多原因,但我们会提供⼀个简单的例⼦。 以下是 JDK 中 StringBuilder 类实现的⼀个⽰例接⼝ Appendable append(CharSequence csq) throws IOException; 这个签名是什么意思? 它是说,每次我追加⼀个字符串到⼀些东西(⼀个 StringBuilder 、某种⽇志、⼀个控制台等)上时 我就必须捕获那些 IOException 。 为什么?因为它可能正在执⾏ IO 操作( Writer 也实现了 Appendable )…… 所以它导致这种代码随处可⻅的出现: try { log.append(message) } catch (IOException e) { // 必须要安全 } 这并不好,参⻅《Effective Java》 第 65 条:不要忽略异常。 Bruce Eckel 在《Java 是否需要受检的异常?》(Does Java need Checked Exceptions?) 中指出: 通过⼀些⼩程序测试得出的结论是异常规范会同时提⾼开发者的⽣产⼒和代码质量,但是⼤型软件项⽬的经验表明⼀个不同的结论⸺⽣产⼒降 低、代码质量很少或没有提⾼。 其他相关引证: 《Java 的受检异常是⼀个错误》(Java's checked exceptions were a mistake)(Rod Waldhoff) 《受检异常的烦恼》(The Trouble with Checked Exceptions)(Anders Hejlsberg) 异常 异常类 Try 是⼀个表达式是⼀个表达式 受检的异常 — — 102 在 Kotlin 中 throw 是表达式,所以你可以使⽤它(⽐如)作为 Elvis 表达式的⼀部分: val s = person.name ?: throw IllegalArgumentException(“Name required“) throw 表达式的类型是特殊类型 Nothing 。 该类型没有值,⽽是⽤于标记永远不能达到的代码位置。 在你⾃⼰的代码中,你可以使⽤ Nothing 来标 记⼀个永远不会返回的函数: fun fail(message: String): Nothing { throw IllegalArgumentException(message) } 当你调⽤该函数时,编译器会知道执⾏不会超出该调⽤: val s = person.name ?: fail(“Name required“) println(s) // 在此已知“s”已初始化 与 Java 互操作性相关的信息,请参⻅ Java 互操作性章节中的异常部分。 Nothing 类型 Java 互操作性 103 注解是将元数据附加到代码的⽅法。要声明注解,请将 annotation 修饰符放在类的前⾯: annotation class Fancy 注解的附加属性可以通过⽤元注解标注注解类来指定: @Target 指定可以⽤ 该注解标注的元素的可能的类型(类、函数、属性、表达式等); @Retention 指定该注解是否 存储在编译后的 class ⽂件中,以及它在运⾏时能否通过反射可⻅ (默认都是 true); @Repeatable 允许 在单个元素上多次使⽤相同的该注解; @MustBeDocumented 指定 该注解是公有 API 的⼀部分,并且应该包含在 ⽣成的 API ⽂档中显⽰的类或⽅法的签名中。 @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION) @Retention(AnnotationRetention.SOURCE) @MustBeDocumented annotation class Fancy @Fancy class Foo { @Fancy fun baz(@Fancy foo: Int): Int { return (@Fancy 1) } } 如果需要对类的主构造函数进⾏标注,则需要在构造函数声明中添加 constructor 关键字 ,并将注解添加到其前⾯: class Foo @Inject constructor(dependency: MyDependency) { // …… } 你也可以标注属性访问器: class Foo { var x: MyDependency? = null @Inject set } 注解可以有接受参数的构造函数。 annotation class Special(val why: String) @Special(“example“) class Foo {} 允许的参数类型有: 对应于 Java 原⽣类型的类型(Int、 Long等); 字符串; 类( Foo::class ); 枚举; 其他注解; 上⾯已列类型的数组。 注解 注解声明 — — — — ⽤法⽤法 构造函数构造函数 — — — — — — 104 注解参数不能有可空类型,因为 JVM 不⽀持将 null 作为 注解属性的值存储。 如果注解⽤作另⼀个注解的参数,则其名称不以 @ 字符为前缀: annotation class ReplaceWith(val expression: String) annotation class Deprecated( val message: String, val replaceWith: ReplaceWith = ReplaceWith(““)) @Deprecated(“This function is deprecated, use === instead“, ReplaceWith(“this === other“)) 如果需要将⼀个类指定为注解的参数,请使⽤ Kotlin 类 (KClass)。Kotlin 编译器会 ⾃动将其转换为 Java 类,以便 Java 代码能够正常看到该注解和参数 。 import kotlin.reflect.KClass annotation class Ann(val arg1: KClass<*>, val arg2: KClass) @Ann(String::class, Int::class) class MyClass 注解也可以⽤于 lambda 表达式。它们会被应⽤于⽣成 lambda 表达式体的 invoke() ⽅法上。这对于像 Quasar 这样的框架很有⽤, 该框架使⽤注解 进⾏并发控制。 annotation class Suspendable val f = @Suspendable { Fiber.sleep(10) } 当对属性或主构造函数参数进⾏标注时,从相应的 Kotlin 元素 ⽣成的 Java 元素会有多个,因此在⽣成的 Java 字节码中该注解有多个可能位置 。如果要 指定精确地指定应该如何⽣成该注解,请使⽤以下语法: class Example(@field:Ann val foo, // 标注 Java 字段 @get:Ann val bar, // 标注 Java getter @param:Ann val quux) // 标注 Java 构造函数参数 可以使⽤相同的语法来标注整个⽂件。 要做到这⼀点,把带有⽬标 file 的注解放在 ⽂件的顶层、package 指令之前或者在所有导⼊之前(如果⽂件在 默认包中的话): @file:JvmName(“Foo“) package org.jetbrains.demo 如果你对同⼀⽬标有多个注解,那么可以这样来避免⽬标重复⸺在⽬标后⾯添加⽅括号 并将所有注解放在⽅括号内: class Example { @set:[Inject VisibleForTesting] var collaborator: Collaborator } ⽀持的使⽤处⽬标的完整列表为: file property(具有此⽬标的注解对 Java 不可⻅) field get(属性 getter) set(属性 setter) Lambda 表达式表达式 注解使⽤处⽬标 — — — — — 105 receiver(扩展函数或属性的接收者参数) param(构造函数参数) setparam(属性 setter 参数) delegate(为委托属性存储其委托实例的字段) 要标注扩展函数的接收者参数,请使⽤以下语法: fun @receiver:Fancy String.myExtension() { } 如果不指定使⽤处⽬标,则根据正在使⽤的注解的 @Target 注解来选择⽬标 。如果有多个适⽤的⽬标,则使⽤以下列表中的第⼀个适⽤⽬标: param property field Java 注解与 Kotlin 100% 兼容: import org.junit.Test import org.junit.Assert.* import org.junit.Rule import org.junit.rules.* class Tests { // 将 @Rule 注解应⽤于属性 getter @get:Rule val tempFolder = TemporaryFolder() @Test fun simple() { val f = tempFolder.newFile() assertEquals(42, getTheAnswer()) } } 因为 Java 编写的注解没有定义参数顺序,所以不能使⽤常规函数调⽤ 语法来传递参数。相反,你需要使⽤命名参数语法。 // Java public @interface Ann { int intValue(); String stringValue(); } // Kotlin @Ann(intValue = 1, stringValue = “abc“) class C 就像在 Java 中⼀样,⼀个特殊的情况是 value 参数;它的值⽆需显式名称指定。 // Java public @interface AnnWithValue { String value(); } // Kotlin @AnnWithValue(“abc“) class C 如果 Java 中的 value 参数具有数组类型,它会成为 Kotlin 中的⼀个 vararg 参数: — — — — — — — Java 注解 106 // Java public @interface AnnWithArrayValue { String[] value(); } // Kotlin @AnnWithArrayValue(“abc“, “foo“, “bar“) class C 对于具有数组类型的其他参数,你需要显式使⽤ arrayOf : // Java public @interface AnnWithArrayMethod { String[] names(); } // Kotlin @AnnWithArrayMethod(names = arrayOf(“abc“, “foo“, “bar“)) class C 注解实例的值会作为属性暴露给 Kotlin 代码。 // Java public @interface Ann { int value(); } // Kotlin fun foo(ann: Ann) { val i = ann.value } 107 反射是这样的⼀组语⾔和库功能,它允许在运⾏时⾃省你的程序的结构。 Kotlin 让语⾔中的函数和属性做为⼀等公⺠、并对其⾃省(即在运⾏时获悉 ⼀个 名称或者⼀个属性或函数的类型)与简单地使⽤函数式或响应式⻛格紧密相关。 在 Java 平台上,使⽤反射功能所需的运⾏时组件作为单独的 JAR ⽂件(kotlin-reflect.jar)分发。这样做是为了减少不使⽤反射功能的 应⽤程序所需的 运⾏时库的⼤⼩。如果你需要使⽤反射,请确保该 .jar⽂件添加到项⽬的 classpath 中。 最基本的反射功能是获取 Kotlin 类的运⾏时引⽤。要获取对 静态已知的 Kotlin 类的引⽤,可以使⽤ 类字⾯值 语法: val c = MyClass::class 该引⽤是 KClass 类型的值。 请注意,Kotlin 类引⽤与 Java 类引⽤不同。要获得 Java 类引⽤, 请在 KClass 实例上使⽤ .java 属性。 通过使⽤对象作为接收者,可以⽤相同的 ::class 语法获取指定对象的类的引⽤: val widget: Widget = …… assert(widget is GoodWidget) { “Bad widget: ${widget::class.qualifiedName}“ } 你可以获取对象的精确类的引⽤,例如 GoodWidget 或 BadWidget ,尽管接收者表达式的类型是 Widget 。 当我们有⼀个命名函数声明如下: fun isOdd(x: Int) = x % 2 != 0 我们可以很容易地直接调⽤它( isOdd(5) ),但是我们也可以把它作为⼀个值传递。例如传给另⼀个函数。 为此,我们使⽤ :: 操作符: val numbers = listOf(1, 2, 3) println(numbers.filter(::isOdd)) // 输出 [1, 3] 这⾥ ::isOdd 是函数类型 (Int) -> Boolean 的⼀个值。 当上下⽂中已知函数期望的类型时,:: 可以⽤于重载函数。 例如: fun isOdd(x: Int) = x % 2 != 0 fun isOdd(s: String) = s == “brillig“ || s == “slithy“ || s == “tove“ val numbers = listOf(1, 2, 3) println(numbers.filter(::isOdd)) // 引⽤到 isOdd(x: Int) 或者,你可以通过将⽅法引⽤存储在具有显式指定类型的变量中来提供必要的上下⽂: val predicate: (String) -> Boolean = ::isOdd // 引⽤到 isOdd(x: String) 如果我们需要使⽤类的成员函数或扩展函数,它需要是限定的。 例如 String::toCharArray 为类型 String 提供了⼀个扩展函数:String.() - > CharArray 。 考虑以下函数: 反射 类引⽤ 绑定的类引⽤(⾃ 1.1 起) 函数引⽤ ⽰例:函数组合⽰例:函数组合 108 fun compose(f: (B) -> C, g: (A) -> B): (A) -> C { return { x -> f(g(x)) } } 它返回⼀个传给它的两个函数的组合:compose(f, g) = f(g(*)) 。 现在,你可以将其应⽤于可调⽤引⽤: fun length(s: String) = s.length val oddLength = compose(::isOdd, ::length) val strings = listOf(“a“, “ab“, “abc“) println(strings.filter(oddLength)) // 输出 “[a, abc]“ 要把属性作为 Kotlin中 的⼀等对象来访问,我们也可以使⽤ :: 运算符: var x = 1 fun main(args: Array) { println(::x.get()) // 输出 “1“ ::x.set(2) println(x) // 输出 “2“ } 表达式 ::x 求值为 KProperty 类型的属性对象,它允许我们使⽤ get() 读取它的值,或者使⽤ name 属性来获取属性名。更多信息请参⻅ 关于 KProperty 类的⽂档。 对于可变属性,例如 var y = 1,::y 返回 KMutableProperty 类型的⼀个值, 该类型有⼀个 set() ⽅法。 属性引⽤可以⽤在不需要参数的函数处: val strs = listOf(“a“, “bc“, “def“) println(strs.map(String::length)) // 输出 [1, 2, 3] 要访问属于类的成员的属性,我们这样限定它: class A(val p: Int) fun main(args: Array) { val prop = A::p println(prop.get(A(1))) // 输出 “1“ } 对于扩展属性: val String.lastChar: Char get() = this[length - 1] fun main(args: Array) { println(String::lastChar.get(“abc“)) // 输出 “c“ } 在Java平台上,标准库包含反射类的扩展,它提供了与 Java 反射对象之间映射(参⻅ kotlin.reflect.jvm 包)。 例如,要查找⼀个⽤作 Kotlin 属性 getter 的 幕后字段或 Java⽅法,可以这样写: 属性引⽤ 与与 Java 反射的互操作性反射的互操作性 109 import kotlin.reflect.jvm.* class A(val p: Int) fun main(args: Array) { println(A::p.javaGetter) // 输出 “public final int A.getP()“ println(A::p.javaField) // 输出 “private final int A.p“ } 要获得对应于 Java 类的 Kotlin 类,请使⽤ .kotlin 扩展属性: fun getKClass(o: Any): KClass = o.javaClass.kotlin 构造函数可以像⽅法和属性那样引⽤。他们可以⽤于期待这样的函数类型对象的任何 地⽅:它与该构造函数接受相同参数并且返回相应类型的对象。 通过 使⽤ :: 操作符并添加类名来引⽤构造函数。考虑下⾯的函数, 它期待⼀个⽆参并返回 Foo 类型的函数参数: class Foo fun function(factory: () -> Foo) { val x: Foo = factory() } 使⽤ ::Foo ,类 Foo 的零参数构造函数,我们可以这样简单地调⽤它: function(::Foo) 你可以引⽤特定对象的实例⽅法。 val numberRegex = “\\d+“.toRegex() println(numberRegex.matches(“29“)) // 输出“true” val isNumber = numberRegex::matches println(isNumber(“29“)) // 输出“true” 取代直接调⽤⽅法 matches 的是我们存储其引⽤。 这样的引⽤会绑定到其接收者上。 它可以直接调⽤(如上例所⽰)或者⽤于任何期待⼀个函数类型表 达式的时候: val strings = listOf(“abc“, “124“, “a70“) println(strings.filter(numberRegex::matches)) // 输出“[124]” ⽐较绑定的类型和相应的未绑定类型的引⽤。 绑定的可调⽤引⽤有其接收者“附加”到其上,因此接收者的类型不再是参数: val isNumber: (CharSequence) -> Boolean = numberRegex::matches val matches: (Regex, CharSequence) -> Boolean = Regex::matches 属性引⽤也可以绑定: val prop = “abc“::length println(prop.get()) // 输出“3” 构造函数引⽤ 绑定的函数与属性引⽤(⾃ 1.1 起) 110 构建器(builder)的概念在 Groovy 社区中⾮常热⻔。 构建器允许以半声明(semi-declarative)的⽅式定义数据。构建器很适合⽤来⽣成 XML、 布局 UI 组 件、 描述 3D 场景以及其他更多功能…… 对于很多情况下,Kotlin 允许检查类型的构建器,这使得它们⽐ Groovy ⾃⾝的动态类型实现更具吸引⼒。 对于其余的情况,Kotlin ⽀持动态类型构建器。 考虑下⾯的代码: import com.example.html.* // 参⻅下⽂声明 fun result(args: Array) = html { head { title {+“XML encoding with Kotlin“} } body { h1 {+“XML encoding with Kotlin“} p {+“this format can be used as an alternative markup to XML“} // ⼀个具有属性和⽂本内容的元素 a(href = “http://kotlinlang.org“) {+“Kotlin“} // 混合的内容 p { +“This is some“ b {+“mixed“} +“text. For more see the“ a(href = “http://kotlinlang.org“) {+“Kotlin“} +“project“ } p {+“some text“} // 以下代码⽣成的内容 p { for (arg in args) +arg } } } 这是完全合法的 Kotlin 代码。 你可以在这⾥在线运⾏上⽂代码(修改它并在浏览器中运⾏)。 让我们来看看 Kotlin 中实现类型安全构建器的机制。 ⾸先,我们需要定义我们想要构建的模型,在本例中我们需要建模 HTML 标签。 ⽤⼀些类就可以轻易 完成。 例如,HTML 是⼀个描述 标签的类,也就是说它定义了像 和 这样的⼦标签。 (参⻅下⽂它的声明。) 现在,让我们回想下为什么我们可以在代码中这样写: html { // …… } html 实际上是⼀个函数调⽤,它接受⼀个 lambda 表达式 作为参数。 该函数定义如下: fun html(init: HTML.() -> Unit): HTML { val html = HTML() html.init() return html } 类型安全的构建器 ⼀个类型安全的构建器⽰例 实现原理 111 这个函数接受⼀个名为 init 的参数,该参数本⾝就是⼀个函数。 该函数的类型是 HTML.() -> Unit ,它是⼀个 带接收者的函数类型 。 这意味着我 们需要向函数传递⼀个 HTML 类型的实例( 接收者 ), 并且我们可以在函数内部调⽤该实例的成员。 该接收者可以通过 this 关键字访问: html { this.head { /* …… */ } this.body { /* …… */ } } ( head 和 body 是 HTML 的成员函数。) 现在,像往常⼀样,this 可以省略掉了,我们得到的东西看起来已经⾮常像⼀个构建器了: html { head { /* …… */ } body { /* …… */ } } 那么,这个调⽤做什么? 让我们看看上⾯定义的 html 函数的主体。 它创建了⼀个 HTML 的新实例,然后通过调⽤作为参数传⼊的函数来初始化它 (在我 们的⽰例中,归结为在HTML实例上调⽤ head 和 body ),然后返回此实例。 这正是构建器所应做的。 HTML 类中的 head 和 body 函数的定义与 html 类似。 唯⼀的区别是,它们将构建的实例添加到包含 HTML 实例的 children 集合中: fun head(init: Head.() -> Unit) : Head { val head = Head() head.init() children.add(head) return head } fun body(init: Body.() -> Unit) : Body { val body = Body() body.init() children.add(body) return body } 实际上这两个函数做同样的事情,所以我们可以有⼀个泛型版本,initTag : protected fun initTag(tag: T, init: T.() -> Unit): T { tag.init() children.add(tag) return tag } 所以,现在我们的函数很简单: fun head(init: Head.() -> Unit) = initTag(Head(), init) fun body(init: Body.() -> Unit) = initTag(Body(), init) 并且我们可以使⽤它们来构建 和 标签。 这⾥要讨论的另⼀件事是如何向标签体中添加⽂本。在上例中我们这样写到 html { head { title {+“XML encoding with Kotlin“} } // …… } 所以基本上,我们只是把⼀个字符串放进⼀个标签体内部,但在它前⾯有⼀个⼩的 + , 所以它是⼀个函数调⽤,调⽤⼀个前缀 unaryPlus() 操作。 该操 作实际上是由⼀个扩展函数 unaryPlus() 定义的,该函数是 TagWithText 抽象类( Title 的⽗类)的成员: 112 fun String.unaryPlus() { children.add(TextElement(this)) } 所以,在这⾥前缀 + 所做的事情是把⼀个字符串包装到⼀个 TextElement 实例中,并将其添加到 children 集合中, 以使其成为标签树的⼀个适当 的部分。 所有这些都在上⾯构建器⽰例顶部导⼊的包 com.example.html 中定义。 在最后⼀节中,你可以阅读这个包的完整定义。 使⽤ DSL 时,可能会遇到上下⽂中可以调⽤太多函数的问题。 我们可以调⽤ lambda 表达式内部每个可⽤的隐式接收者的⽅法,因此得到⼀个不⼀致的 结果,就像在另⼀个 head 内部的 head 标记那样: html { head { head {} // 应该禁⽌ } // …… } 在这个例⼦中,必须只有最近层的隐式接收者 this@head 的成员可⽤;head() 是外部接收者 this@html 的成员,所以调⽤它⼀定是⾮法的。 为了解决这个问题,在 Kotlin 1.1 中引⼊了⼀种控制接收者作⽤域的特殊机制。 为了使编译器开始控制标记,我们只是必须⽤相同的标记注解来标注在 DSL 中使⽤的所有接收者的类型。 例如,对于 HTML 构建器,我们声明⼀个注解 @HTMLTagMarker : @DslMarker annotation class HtmlTagMarker 如果⼀个注解类使⽤ @DslMarker 注解标注,那么该注解类称为 DSL 标记。 在我们的 DSL 中,所有标签类都扩展了相同的超类 Tag 。 只需使⽤ @HtmlTagMarker 来标注超类就⾜够了,之后,Kotlin 编译器会将所有继承的类视 为已标注: @HtmlTagMarker abstract class Tag(val name: String) { …… } 我们不必⽤ @HtmlTagMarker 标注 HTML 或 Head 类,因为它们的超类已标注过: class HTML() : Tag(“html“) { …… } class Head() : Tag(“head“) { …… } 在添加了这个注解之后,Kotlin 编译器就知道哪些隐式接收者是同⼀个 DSL 的⼀部分,并且只允许调⽤最近层的接收者的成员: html { head { head { } // 错误:外部接收者的成员 } // …… } 请注意,仍然可以调⽤外部接收者的成员,但是要做到这⼀点,你必须明确指定这个接收者: html { head { this@html.head { } // 可能 } // …… } 作⽤域控制:@DslMarker(⾃ 1.1 起) com.example.html 包的完整定义 113 这就是 com.example.html 包的定义(只有上⾯例⼦中使⽤的元素)。 它构建⼀个 HTML 树。代码中⼤量使⽤了扩展函数和 带接收者的 lambda 表达 式。 请注意,@DslMarker 注解在 Kotlin 1.1 起才可⽤。 package com.example.html interface Element { fun render(builder: StringBuilder, indent: String) } class TextElement(val text: String) : Element { override fun render(builder: StringBuilder, indent: String) { builder.append(“$indent$text\n“) } } @DslMarker annotation class HtmlTagMarker @HtmlTagMarker abstract class Tag(val name: String) : Element { val children = arrayListOf() val attributes = hashMapOf() protected fun initTag(tag: T, init: T.() -> Unit): T { tag.init() children.add(tag) return tag } override fun render(builder: StringBuilder, indent: String) { builder.append(“$indent<$name${renderAttributes()}>\n“) for (c in children) { c.render(builder, indent + “ “) } builder.append(“$indent\n“) } private fun renderAttributes(): String { val builder = StringBuilder() for ((attr, value) in attributes) { builder.append(“ $attr=\“$value\““) } return builder.toString() } override fun toString(): String { val builder = StringBuilder() render(builder, ““) return builder.toString() } } abstract class TagWithText(name: String) : Tag(name) { operator fun String.unaryPlus() { children.add(TextElement(this)) } } class HTML : TagWithText(“html“) { fun head(init: Head.() -> Unit) = initTag(Head(), init) fun body(init: Body.() -> Unit) = initTag(Body(), init) } class Head : TagWithText(“head“) { fun title(init: Title.() -> Unit) = initTag(Title(), init) } 114 class Title : TagWithText(“title“) abstract class BodyTag(name: String) : TagWithText(name) { fun b(init: B.() -> Unit) = initTag(B(), init) fun p(init: P.() -> Unit) = initTag(P(), init) fun h1(init: H1.() -> Unit) = initTag(H1(), init) fun a(href: String, init: A.() -> Unit) { val a = initTag(A(), init) a.href = href } } class Body : BodyTag(“body“) class B : BodyTag(“b“) class P : BodyTag(“p“) class H1 : BodyTag(“h1“) class A : BodyTag(“a“) { var href: String get() = attributes[“href“]!! set(value) { attributes[“href“] = value } } fun html(init: HTML.() -> Unit): HTML { val html = HTML() html.init() return html } 115 类型别名为现有类型提供替代名称。 如果类型名称太⻓,你可以另外引⼊较短的名称,并使⽤新的名称替代原类型名。 它有助于缩短较⻓的泛型类型。 例如,通常缩减集合类型是很有吸引⼒的: typealias NodeSet = Set typealias FileTable = MutableMap> 你可以为函数类型提供另外的别名: typealias MyHandler = (Int, String, Any) -> Unit typealias Predicate = (T) -> Boolean 你可以为内部类和嵌套类创建新名称: class A { inner class Inner } class B { inner class Inner } typealias AInner = A.Inner typealias BInner = B.Inner 类型别名不会引⼊新类型。 它们等效于相应的底层类型。 当你在代码中添加 typealias Predicate 并使⽤ Predicate 时,Kotlin 编译 器总是把它扩展为 (Int) -> Boolean 。 因此,当你需要泛型函数类型时,你可以传递该类型的变量,反之亦然: typealias Predicate = (T) -> Boolean fun foo(p: Predicate) = p(42) fun main(args: Array) { val f: (Int) -> Boolean = { it > 0 } println(foo(f)) // 输出 “true“ val p: Predicate = { it > 0 } println(listOf(1, -2).filter(p)) // 输出 “[1]“ } 类型别名 116 参考参考 This section informally explains the grammar notation used below. Terminal symbol names start with an uppercase letter, e.g. SimpleNameSimpleName. Nonterminal symbol names start with lowercase letter, e.g. kotlinFilekotlinFile. Each production starts with a colon (::). Symbol definitions may have many productions and are terminated by a semicolon (;;). Symbol definitions may be prepended with attributes, e.g. start attribute denotes a start symbol. Operator | denotes alternative. Operator * denotes iteration (zero or more). Operator + denotes iteration (one or more). Operator ? denotes option (zero or one). alpha { beta } denotes a nonempty beta-separated list of alpha's. Operator ``++'' means that no space or comment allowed between operands. Kotlin provides “semicolon inference“: syntactically, subsentences (e.g., statements, declarations etc) are separated by the pseudo-token SEMI, which stands for “semicolon or newline“. In most cases, there's no need for semicolons in Kotlin code. Relevant pages: Packages start kotlinFile : preamble topLevelObject* ; start script : preamble expression* ; preamble (used by script, kotlinFile) : fileAnnotations? packageHeader? import* ; fileAnnotations (used by preamble) : fileAnnotation* ; fileAnnotation (used by fileAnnotations) : “@“ “file“ “:“ (“[“ unescapedAnnotation+ “]“ | unescapedAnnotation) ; packageHeader (used by preamble) : modifiers “package“ SimpleName{“.“} SEMI? ; See Packages import (used by preamble) : “import“ SimpleName{“.“} (“.“ “*“ | “as“ SimpleName)? SEMI? ; See Imports Grammar Notation Symbols and naming EBNF expressions Semicolons Syntax 117 topLevelObject (used by kotlinFile) : class : object : function : property : typeAlias ; typeAlias (used by memberDeclaration, declaration, topLevelObject) : modifiers “typealias“ SimpleName typeParameters? “=“ type ; See Classes and Inheritance class (used by memberDeclaration, declaration, topLevelObject) : modifiers (“class“ | “interface“) SimpleName typeParameters? primaryConstructor? (“:“ annotations delegationSpecifier{“,“})? typeConstraints (classBody? | enumClassBody) ; primaryConstructor (used by class, object) : (modifiers “constructor“)? (“(“ functionParameter{“,“} “)“) ; classBody (used by objectLiteral, enumEntry, class, companionObject, object) : (“{“ members “}“)? ; members (used by enumClassBody, classBody) : memberDeclaration* ; delegationSpecifier (used by objectLiteral, class, companionObject, object) : constructorInvocation : userType : explicitDelegation ; explicitDelegation (used by delegationSpecifier) : userType “by“ expression ; typeParameters (used by typeAlias, class, property, function) : “<“ typeParameter{“,“} “>“ ; typeParameter (used by typeParameters) : modifiers SimpleName (“:“ userType)? ; See Generic classes typeConstraints (used by class, property, function) : (“where“ typeConstraint{“,“})? ; typeConstraint (used by typeConstraints) : annotations SimpleName “:“ type ; See Generic constraints memberDeclaration (used by members) : companionObject : object : function : property : class : typeAlias : anonymousInitializer : secondaryConstructor ; anonymousInitializer (used by memberDeclaration) : “init“ block ; companionObject (used by memberDeclaration) : modifiers “companion“ “object“ SimpleName? (“:“ delegationSpecifier{“,“})? classBody? ; valueParameters Classes Class members 118 (used by secondaryConstructor, function) : “(“ functionParameter{“,“}? “)“ ; functionParameter (used by valueParameters, primaryConstructor) : modifiers (“val“ | “var“)? parameter (“=“ expression)? ; block (used by catchBlock, anonymousInitializer, secondaryConstructor, functionBody, controlStructureBody, try, finallyBlock) : “{“ statements “}“ ; function (used by memberDeclaration, declaration, topLevelObject) : modifiers “fun“ typeParameters? (type “.“)? SimpleName typeParameters? valueParameters (“:“ type)? typeConstraints functionBody? ; functionBody (used by getter, setter, function) : block : “=“ expression ; variableDeclarationEntry (used by for, lambdaParameter, property, multipleVariableDeclarations) : SimpleName (“:“ type)? ; multipleVariableDeclarations (used by for, lambdaParameter, property) : “(“ variableDeclarationEntry{“,“} “)“ ; property (used by memberDeclaration, declaration, topLevelObject) : modifiers (“val“ | “var“) typeParameters? (type “.“)? (multipleVariableDeclarations | variableDeclarationEntry) typeConstraints (“by“ | “=“ expression SEMI?)? (getter? setter? | setter? getter?) SEMI? ; See Properties and Fields getter (used by property) : modifiers “get“ : modifiers “get“ “(“ “)“ (“:“ type)? functionBody ; setter (used by property) : modifiers “set“ : modifiers “set“ “(“ modifiers (SimpleName | parameter) “)“ functionBody ; parameter (used by functionType, setter, functionParameter) : SimpleName “:“ type ; object (used by memberDeclaration, declaration, topLevelObject) : “object“ SimpleName primaryConstructor? (“:“ delegationSpecifier{“,“})? classBody? secondaryConstructor (used by memberDeclaration) : modifiers “constructor“ valueParameters (“:“ constructorDelegationCall)? block ; constructorDelegationCall (used by secondaryConstructor) : “this“ valueArguments : “super“ valueArguments ; See Object expressions and Declarations See Enum classes enumClassBody (used by class) : “{“ enumEntries (“;“ members)? “}“ ; enumEntries (used by enumClassBody) : (enumEntry{“,“} “,“? “;“?)? ; enumEntry (used by enumEntries) : modifiers SimpleName (“(“ arguments “)“)? classBody? ; Enum classes 119 See Types type (used by namedInfix, simpleUserType, getter, atomicExpression, whenCondition, property, typeArguments, function, typeAlias, parameter, functionType, variableDeclarationEntry, lambdaParameter, typeConstraint) : typeModifiers typeReference ; typeReference (used by typeReference, nullableType, type) : “(“ typeReference “)“ : functionType : userType : nullableType : “dynamic“ ; nullableType (used by typeReference) : typeReference “?“ ; userType (used by typeParameter, catchBlock, callableReference, typeReference, delegationSpecifier, constructorInvocation, explicitDelegation) : simpleUserType{“.“} ; simpleUserType (used by userType) : SimpleName (“<“ (optionalProjection type | “*“){“,“} “>“)? ; optionalProjection (used by simpleUserType) : varianceAnnotation ; functionType (used by typeReference) : (type “.“)? “(“ parameter{“,“}? “)“ “->“ type? ; See Control structures controlStructureBody (used by whenEntry, for, if, doWhile, while) : block : blockLevelExpression ; if (used by atomicExpression) : “if“ “(“ expression “)“ controlStructureBody SEMI? (“else“ controlStructureBody)? ; try (used by atomicExpression) : “try“ block catchBlock* finallyBlock? ; catchBlock (used by try) : “catch“ “(“ annotations SimpleName “:“ userType “)“ block ; finallyBlock (used by try) : “finally“ block ; loop (used by atomicExpression) : for : while : doWhile ; for (used by loop) : “for“ “(“ annotations (multipleVariableDeclarations | variableDeclarationEntry) “in“ expression “)“ controlStructureBody ; while (used by loop) : “while“ “(“ expression “)“ controlStructureBody ; doWhile (used by loop) : “do“ controlStructureBody “while“ “(“ expression “)“ ; PrecedencePrecedence TitleTitle SymbolsSymbols Highest Postfix ++, --, ., ?., ? Types Control structures Expressions Precedence 120 Prefix -, +, ++, --, !, labelDefinition@ Type RHS :, as, as? Multiplicative *, /, % Additive +, - Range .. Infix function SimpleName Elvis ?: Named checks in, !in, is, !is Comparison <, >, <=, >= Equality ==, \!== Conjunction && Disjunction || Lowest Assignment =, +=, -=, *=, /=, %= PrecedencePrecedence TitleTitle SymbolsSymbols expression (used by for, atomicExpression, longTemplate, whenCondition, functionBody, doWhile, property, script, explicitDelegation, jump, while, arrayAccess, blockLevelExpression, if, when, valueArguments, functionParameter) : disjunction (assignmentOperator disjunction)* ; disjunction (used by expression) : conjunction (“||“ conjunction)* ; conjunction (used by disjunction) : equalityComparison (“&&“ equalityComparison)* ; equalityComparison (used by conjunction) : comparison (equalityOperation comparison)* ; comparison (used by equalityComparison) : namedInfix (comparisonOperation namedInfix)* ; namedInfix (used by comparison) : elvisExpression (inOperation elvisExpression)* : elvisExpression (isOperation type)? ; elvisExpression (used by namedInfix) : infixFunctionCall (“?:“ infixFunctionCall)* ; infixFunctionCall (used by elvisExpression) : rangeExpression (SimpleName rangeExpression)* ; rangeExpression (used by infixFunctionCall) : additiveExpression (“..“ additiveExpression)* ; additiveExpression (used by rangeExpression) : multiplicativeExpression (additiveOperation multiplicativeExpression)* ; multiplicativeExpression (used by additiveExpression) : typeRHS (multiplicativeOperation typeRHS)* ; typeRHS (used by multiplicativeExpression) : prefixUnaryExpression (typeOperation prefixUnaryExpression)* ; prefixUnaryExpression (used by typeRHS) : prefixUnaryOperation* postfixUnaryExpression ; postfixUnaryExpression (used by prefixUnaryExpression, postfixUnaryOperation) : atomicExpression postfixUnaryOperation* : callableReference postfixUnaryOperation* ; callableReference (used by postfixUnaryExpression) : (userType “?“*)? “::“ SimpleName typeArguments? ; atomicExpression (used by postfixUnaryExpression) : “(“ expression “)“ Rules 121 : literalConstant : functionLiteral : “this“ labelReference? : “super“ (“<“ type “>“)? labelReference? : if : when : try : objectLiteral : jump : loop : collectionLiteral : SimpleName ; labelReference (used by atomicExpression, jump) : “@“ ++ LabelName ; labelDefinition (used by prefixUnaryOperation, annotatedLambda) : LabelName ++ “@“ ; literalConstant (used by atomicExpression) : “true“ | “false“ : stringTemplate : NoEscapeString : IntegerLiteral : HexadecimalLiteral : CharacterLiteral : FloatLiteral : “null“ ; stringTemplate (used by literalConstant) : “\““ stringTemplateElement* “\““ ; stringTemplateElement (used by stringTemplate) : RegularStringPart : ShortTemplateEntryStart (SimpleName | “this“) : EscapeSequence : longTemplate ; longTemplate (used by stringTemplateElement) : “${“ expression “}“ ; declaration (used by statement) : function : property : class : typeAlias : object ; statement (used by statements) : declaration : blockLevelExpression ; blockLevelExpression (used by statement, controlStructureBody) : annotations (“\n“)+ expression ; multiplicativeOperation (used by multiplicativeExpression) : “*“ : “/“ : “%“ ; additiveOperation (used by additiveExpression) : “+“ : “-“ ; inOperation (used by namedInfix) : “in“ : “!in“ ; typeOperation (used by typeRHS) : “as“ : “as?“ : “:“ ; isOperation (used by namedInfix) : “is“ : “!is“ ; comparisonOperation (used by comparison) : “<“ : “>“ : “>=“ : “<=“ ; equalityOperation (used by equalityComparison) : “!=“ : “==“ ; assignmentOperator (used by expression) 122 : “=“ : “+=“ : “-=“ : “*=“ : “/=“ : “%=“ ; prefixUnaryOperation (used by prefixUnaryExpression) : “-“ : “+“ : “++“ : “--“ : “!“ : annotations : labelDefinition ; postfixUnaryOperation (used by postfixUnaryExpression) : “++“ : “--“ : “!!“ : callSuffix : arrayAccess : memberAccessOperation postfixUnaryExpression ; callSuffix (used by constructorInvocation, postfixUnaryOperation) : typeArguments? valueArguments annotatedLambda : typeArguments annotatedLambda ; annotatedLambda (used by callSuffix) : (“@“ unescapedAnnotation)* labelDefinition? functionLiteral memberAccessOperation (used by postfixUnaryOperation) : “.“ : “?.“ : “?“ ; typeArguments (used by callSuffix, callableReference, unescapedAnnotation) : “<“ type{“,“} “>“ ; valueArguments (used by callSuffix, constructorDelegationCall, unescapedAnnotation) : “(“ (SimpleName “=“)? “*“? expression{“,“} “)“ ; jump (used by atomicExpression) : “throw“ expression : “return“ ++ labelReference? expression? : “continue“ ++ labelReference? : “break“ ++ labelReference? ; functionLiteral (used by atomicExpression, annotatedLambda) : “{“ statements “}“ : “{“ lambdaParameter{“,“} “->“ statements “}“ ; lambdaParameter (used by functionLiteral) : variableDeclarationEntry : multipleVariableDeclarations (“:“ type)? ; statements (used by block, functionLiteral) : SEMI* statement{SEMI+} SEMI* ; constructorInvocation (used by delegationSpecifier) : userType callSuffix ; arrayAccess (used by postfixUnaryOperation) : “[“ expression{“,“} “]“ ; objectLiteral (used by atomicExpression) : “object“ (“:“ delegationSpecifier{“,“})? classBody ; collectionLiteral (used by atomicExpression) : “[“ element{“,“}? “]“ ; See When-expression when (used by atomicExpression) : “when“ (“(“ expression “)“)? “{“ whenEntry* “}“ ; whenEntry (used by when) : whenCondition{“,“} “->“ controlStructureBody SEMI : “else“ “->“ controlStructureBody SEMI ; whenCondition (used by whenEntry) When-expression 123 : expression : (“in“ | “!in“) expression : (“is“ | “!is“) type ; modifiers (used by typeParameter, getter, packageHeader, class, property, function, typeAlias, secondaryConstructor, enumEntry, setter, companionObject, primaryConstructor, functionParameter) : (modifier | annotations)* ; typeModifiers (used by type) : (suspendModifier | annotations)* ; modifier (used by modifiers) : classModifier : accessModifier : varianceAnnotation : memberModifier : parameterModifier : typeParameterModifier : functionModifier : propertyModifier ; classModifier (used by modifier) : “abstract“ : “final“ : “enum“ : “open“ : “annotation“ : “sealed“ : “data“ ; memberModifier (used by modifier) : “override“ : “open“ : “final“ : “abstract“ : “lateinit“ ; accessModifier (used by modifier) : “private“ : “protected“ : “public“ : “internal“ ; varianceAnnotation (used by modifier, optionalProjection) : “in“ : “out“ ; parameterModifier (used by modifier) : “noinline“ : “crossinline“ : “vararg“ ; typeParameterModifier (used by modifier) : “reified“ ; functionModifier (used by modifier) : “tailrec“ : “operator“ : “infix“ : “inline“ : “external“ : suspendModifier ; propertyModifier (used by modifier) : “const“ ; suspendModifier (used by typeModifiers, functionModifier) : “suspend“ ; annotations (used by catchBlock, prefixUnaryOperation, blockLevelExpression, for, typeModifiers, class, modifiers, typeConstraint) : (annotation | annotationList)* ; annotation Modiers Annotations 124 (used by annotations) : “@“ (annotationUseSiteTarget “:“)? unescapedAnnotation ; annotationList (used by annotations) : “@“ (annotationUseSiteTarget “:“)? “[“ unescapedAnnotation+ “]“ ; annotationUseSiteTarget (used by annotation, annotationList) : “field“ : “file“ : “property“ : “get“ : “set“ : “receiver“ : “param“ : “setparam“ : “delegate“ ; unescapedAnnotation (used by annotation, fileAnnotation, annotatedLambda, annotationList) : SimpleName{“.“} typeArguments? valueArguments? ; helper Digit (used by IntegerLiteral, HexDigit) : [“0“..“9“]; IntegerLiteral (used by literalConstant) : Digit (Digit | “_“)* FloatLiteral (used by literalConstant) : ; helper HexDigit (used by RegularStringPart, HexadecimalLiteral) : Digit | [“A“..“F“, “a“..“f“]; HexadecimalLiteral (used by literalConstant) : “0x“ HexDigit (HexDigit | “_“)*; CharacterLiteral (used by literalConstant) : ; See Basic types NoEscapeString (used by literalConstant) : <“““-quoted string>; RegularStringPart (used by stringTemplateElement) : ShortTemplateEntryStart: : “$“ EscapeSequence: : UnicodeEscapeSequence | RegularEscapeSequence UnicodeEscapeSequence: : “\u“ HexDigit{4} RegularEscapeSequence: : “\“ See String templates SEMI (used by whenEntry, if, statements, packageHeader, property, import) : ; SimpleName (used by typeParameter, catchBlock, simpleUserType, atomicExpression, LabelName, packageHeader, class, object, infixFunctionCall, function, typeAlias, parameter, callableReference, variableDeclarationEntry, stringTemplateElement, enumEntry, setter, import, companionObject, valueArguments, unescapedAnnotation, typeConstraint) : : “`“ “`“ ; See Java interoperability LabelName (used by labelReference, labelDefinition) : “@“ SimpleName; See Returns and jumps Lexical structure 125 本⻚介绍 Kotlin 不同版本以及⼦系统的兼容性保证。 兼容性意味着回答这个问题:对于给定的两个版本的 Kotlin(例如,1.2 和 1.1.5),为⼀个版本编写的代码可以与另⼀个版本⼀起使⽤吗?下⾯的列表解释 了不同版本对的兼容模式。请注意,如果版本号较⼩(即使发布时间晚于版本号较⼤的版本)那么版本较旧。对于“旧版本”我们使⽤ OV,对于“新版本”使⽤ NV。 CC⸺完全兼容(Full CCompatibility) 语⾔ ⽆语法改动(除去 bug*) 可能添加或删除新的警告/提⽰ API( kotlin-stdlib-* 、 kotlin-reflect-* ) ⽆ API 改动 可能添加/删除带有 WARNING 级的弃⽤项 ⼆进制(ABI) 运⾏时:⼆进制可以互换使⽤ 编译:⼆进制可以互换使⽤ BCLABCLA⸺语⾔和 API 向后兼容(BBackward CCompatibility for the LLanguage and AAPI) 语⾔ 可能会在 NV 中删除 OV 中已弃⽤的语法 除此之外,OV 中可编译的所有代码都可以在 NV 中编译(除去 bug*) 可能在 NV 中添加新语法 在 NV 中可以提升 OV 的⼀些限制 可能添加或删除新的警告/提⽰ API( kotlin-stdlib-* 、 kotlin-reflect-* ) 可能添加新的 API 可能添加/删除带有 WARNING 级的弃⽤项 WARNING 级的弃⽤项可能在 NV 中提升到 ERROR 级或者 HIDDEN 级 BCBBCB⸺⼆进制向后兼容(BBackward CCompatibility for BBinaries) ⼆进制(ABI) 运⾏时:NV 的⼆进制可以在 OV 的⼆进制⼯作的任何地⽅使⽤ NV 编译器:针对 OV ⼆进制编译的代码可针对 NV ⼆进制编译 OV 编译器可能不接受 NV ⼆进制(例如,展⽰较新语⾔特性或 API 的⼆进制) BCBC⸺完全向后兼容(Full BBackward CCompatibility) BC = BCLA & BCB EXPEXP⸺实验性的功能(Experimental feature) 参⻅下⽂ NONO⸺⽆兼容性保证(No compatibility guarantees) 我们会尽⼒提供顺利的迁移,但不能给出任何保证 为每个不兼容的⼦系统单独规划迁移 * 除去 bugs ⽆改动意味着如果发现⼀个重要的 bug(例如在编译器诊断或其他地⽅),修复它可能会引⼊⼀个破坏性改动,但我们总是⾮常⼩⼼对待这样 的改动。 JVM 平台的 KotlinJVM 平台的 Kotlin: 兼容性 兼容性词汇表 — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — Kotlin 发⾏版的兼容性保证 126 补丁版本更新(例如1.1.X)完全兼容 次版本更新(例如1.X)向后兼容 KotlinKotlin 1.01.0 1.0.X1.0.X 1.11.1 1.1.X1.1.X …… 2.02.0 1.01.0 - C BC BC … ? 1.0.X1.0.X C - BC BC … ? 1.11.1 BC BC - C … ? 1.1.X1.1.X BC BC C - … ? …… … … … … … … 2.02.0 ? ? ? ? … - JS 平台的 KotlinJS 平台的 Kotlin:从 Kotlin 1.1 开始,补丁版本和次版本更新为语⾔和 API 提供向后兼容性(BCLA),但没有 BCB。 KotlinKotlin 1.0.X1.0.X 1.11.1 1.1.X1.1.X …… 2.02.0 1.0.X1.0.X - EXP EXP … EXP 1.11.1 EXP - BCLA … ? 1.1.X1.1.X EXP BCLA - … ? …… … … … … … 2.02.0 EXP ? ? … - Kotlin ScriptsKotlin Scripts:补丁版本和次版本更新为语⾔和 API 提供向后兼容性(BCLA),但没有 BCB。 Kotlin 可⽤于多个平台(JVM/Android、JavaScript 以及即将推出的本地平台)。每个平台都有⾃⼰的特殊性(例如 JavaScript 没有适当的整数),因此我 们必须相应地调整语⾔。我们的⽬标是提供合理的代码可移植性,⽽不会牺牲太多。 每个平台都可能具有特定的语⾔扩展(例如 JVM 的平台类型和 JavaScript 的动态类型)或限制(例如 JVM 上与重载相关的限制),但核⼼语⾔保持不变。 标准库提供了在所有平台上可⽤的核⼼ API,我们努⼒使这些 API 在每个平台上以相同的⽅式⼯作。除此之外,标准库提供了平台相关的扩展(例如,JVM 的 java.io 或 JavaScript 的 js() )以及⼀些可以统⼀调⽤但⼯作⽅式不同的 API(例如 JVM 和 JavaScript 的正则表达式)。 实验性的功能,如 Kotlin 1.1 中的协程,可以从上⾯列出的兼容模式中豁免。这类功能需要选择性加⼊(opt-in)来使⽤才没有编译器警告。实验性的功能⾄ 少向后兼容补丁版本更新,但我们不保证任何次版本更新的兼容性(会尽可能提供迁移帮助)。 KotlinKotlin 1.11.1 1.1.X1.1.X 1.21.2 1.2.X1.2.X 1.11.1 - BC NO NO 1.1.X1.1.X BC - NO NO 1.21.2 NO NO - BC 1.2.X1.2.X NO NO BC - 我们发布早期访问预览(Early Access Preview,EAP)构建版到特殊渠道,该社区的早期采⽤者可以试⽤它们并提供反馈。这样的构建不提供任何兼容性 保证(尽管我们尽最⼤努⼒保持它们与发⾏版以及彼此之间的合理的兼容性)。这类构建版的质量预期也远低于发⾏版。Beta 测试版本也属于这⼀类别。 重要注意事项重要注意事项:通过 EAP 为 1.X(例如 1.1.0-eap-X)编译的所有⼆进制⽂件会被编译器发⾏版版本拒绝会被编译器发⾏版版本拒绝。我们不希望预发布版本编译的任何代码在稳定版 本发布后保留。这不涉及补丁版本的 EAP(例如 1.1.3-eap-X),这些 EAP 产⽣具有稳定 ABI 的构建。 当⼀个⼤团队迁移到⼀个新版本时,当⼀些开发⼈员已经更新、⽽其他⼈没有时,可能会在某个时候出现“不⼀致的状态”。为了防⽌前者编写和提交别⼈可 能⽆法编译的代码,我们提供了以下命令⾏开关(在 IDE 以及 Gradle/Maven 中也可⽤): -language-version X.Y ⸺Kotlin 语⾔版本 X.Y 的兼容性模式,对其后出现的所有语⾔功能报告错误 -api-version X.Y ⸺Kotlin API 版本 X.Y 的兼容性模式,对使⽤来⾃ Kotlin 标准库(包括编译器⽣成的代码)的新版 API 的所有代码报告错误。 — — 跨平台兼容性 实验性的功能 EAP 构建版 兼容性模式 — — ⼆进制兼容性警告 127 如果使⽤ NV Kotlin 编译器并在 classpath 中配有 OV 标准库或 OV 反射库,那么可能是项⽬配置错误的迹象。 为了防⽌编译期或运⾏时出现意外问题, 我们建议要么将依赖关系更新到 NV,要么明确指定 API 版本/语⾔版本参数。 否则编译器会检测到某些东西可能出错,并报告警告。 例如,如果 OV = 1.0 且 NV = 1.1,你可能观察到以下警告之⼀: Runtime JAR files in the classpath have the version 1.0, which is older than the API version 1.1. Consider using the runtime of version 1.1, or pass '-api-version 1.0' explicitly to restrict the available APIs to the runtime of version 1.0. 这意味着你针对版本 1.0 的标准库或反射库使⽤ Kotlin 编译器 1.1。这可以通过不同的⽅式处理: 如果你打算使⽤ 1.1 标准库中的 API 或者依赖于这些 API 的语⾔特性,那么应将依赖关系升级到版本 1.1。 如果你想保持你的代码与 1.0 标准库兼容,你可以传参 -api-version 1.0 。 如果你刚刚升级到 kotlin 1.1,但不能使⽤新的语⾔功能(例如,因为你的⼀些队友可能没有升级),你可以传参 -language-version 1.0 ,这会限 制所有的 API 和语⾔功能到 1.0。 Runtime JAR files in the classpath should have the same version. These files were found in the classpath: kotlin-reflect.jar (version 1.0) kotlin-stdlib.jar (version 1.1) Consider providing an explicit dependency on kotlin-reflect 1.1 to prevent strange errors Some runtime JAR files in the classpath have an incompatible version. Consider removing them from the classpath 这意味着你对不同版本的库有依赖性,例如 1.1 标准库和 1.0 反射库。为了防⽌在运⾏时出现微妙的错误,我们建议你使⽤所有 Kotlin 库的相同版本。在 本例中,请考虑对 1.1 反射库添加显式依赖关系。 Some JAR files in the classpath have the Kotlin Runtime library bundled into them. This may cause difficult to debug problems if there's a different version of the Kotlin Runtime library in the classpath. Consider removing these libraries from the classpath 这意味着在 classpath 中有⼀个库,它不是作为 Gradle/Maven 依赖项⽽依赖 Kotlin 标准库,⽽是与它分布在同⼀个构件中(即是被捆绑的)。这样的库可 能会导致问题,因为标准构建⼯具不认为它是 Kotlin 标准库的实例,因此它不受依赖版本解析机制的限制,你可以在 classpath 找到同⼀个库的多个版 本。请考虑联系这样的库的作者,并提出使⽤ Gradle/Maven 依赖取代的建议。 — — — 128 Java 互操作互操作 Kotlin 在设计时就考虑了 Java 互操作性。可以从 Kotlin 中⾃然地调⽤现存的 Java 代码,并且在 Java 代码中也可以 很顺利地调⽤ Kotlin 代码。在本节 中我们会介绍从 Kotlin 中调⽤ Java 代码的⼀些细节。 ⼏乎所有 Java 代码都可以使⽤⽽没有任何问题 import java.util.* fun demo(source: List) { val list = ArrayList() // “for”-循环⽤于 Java 集合: for (item in source) { list.add(item) } // 操作符约定同样有效: for (i in 0..source.size() - 1) { list[i] = source[i] // 调⽤ get 和 set } } 遵循 Java 约定的 getter 和 setter 的⽅法(名称以 get 开头的⽆参数⽅法和 以 set 开头的单参数⽅法)在 Kotlin 中表⽰为属性。 例如: import java.util.Calendar fun calendarDemo() { val calendar = Calendar.getInstance() if (calendar.firstDayOfWeek == Calendar.SUNDAY) { // 调⽤ getFirstDayOfWeek() calendar.firstDayOfWeek = Calendar.MONDAY // 调⽤ setFirstDayOfWeek() } } 请注意,如果 Java 类只有⼀个 setter,它在 Kotlin 中不会作为属性可⻅,因为 Kotlin ⽬前不⽀持只写(set-only)属性。 如果⼀个 Java ⽅法返回 void,那么从 Kotlin 调⽤时中返回 Unit 。 万⼀有⼈使⽤其返回值,它将由 Kotlin 编译器在调⽤处赋值, 因为该值本⾝是预先 知道的(是 Unit )。 ⼀些 Kotlin 关键字在 Java 中是有效标识符:in、 object、 is 等等。 如果⼀个 Java 库使⽤了 Kotlin 关键字作为⽅法,你仍然可以通过反引号(`)字符 转义它 来调⽤该⽅法 foo.`is`(bar) Java 中的任何引⽤都可能是 null,这使得 Kotlin 对来⾃ Java 的对象要求严格空安全是不现实的。 Java 声明的类型在 Kotlin 中会被特别对待并称 为平台类型。对这种类型的空检查会放宽, 因此它们的安全保证与在 Java 中相同(更多请参⻅下⽂)。 考虑以下⽰例: 在 Kotlin 中调⽤ Java 代码 Getter 和 Setter 返回 void 的⽅法 将 Kotlin 中是关键字的 Java 标识符进⾏转义 空安全和平台类型 129 val list = ArrayList() // ⾮空(构造函数结果) list.add(“Item“) val size = list.size() // ⾮空(原⽣ int) val item = list[0] // 推断为平台类型(普通 Java 对象) 当我们调⽤平台类型变量的⽅法时,Kotlin 不会在编译时报告可空性错误, 但在运⾏时调⽤可能会失败,因为空指针异常或者 Kotlin ⽣成的阻⽌空值传 播的断⾔: item.substring(1) // 允许,如果 item == null 可能会抛出异常 平台类型是不可标⽰的,意味着不能在语⾔中明确地写下它们。 当把⼀个平台值赋值给⼀个 Kotlin 变量时,可以依赖类型推断(该变量会具有推断出的的 平台类型, 如上例中 item 所具有的类型),或者我们可以选择我们期望的类型(可空或⾮空类型均可): val nullable: String? = item // 允许,没有问题 val notNull: String = item // 允许,运⾏时可能失败 如果我们选择⾮空类型,编译器会在赋值时触发⼀个断⾔。这防⽌ Kotlin 的⾮空变量保存 空值。当我们把平台值传递给期待⾮空值等的 Kotlin 函数时,也 会触发断⾔。 总的来说,编译器尽⼒阻⽌空值通过程序向远传播(尽管鉴于泛型的原因,有时这 不可能完全消除)。 如上所述,平台类型不能在程序中显式表述,因此在语⾔中没有相应语法。 然⽽,编译器和 IDE 有时需要(在错误信息中、参数信息中等)显⽰他们,所以我 们⽤ ⼀个助记符来表⽰他们: T! 表⽰“ T 或者 T? ”, (Mutable)Collection! 表⽰“可以可变或不可变、可空或不可空的 T 的 Java 集合”, Array<(out) T>! 表⽰“可空或者不可空的 T(或 T 的⼦类型)的 Java 数组” 具有可空性注解的Java类型并不表⽰为平台类型,⽽是表⽰为实际可空或⾮空的 Kotlin 类型。编译器⽀持多种可空性注解,包括: JetBrains ( org.jetbrains.annotations 包中的 @Nullable 和 @NotNull ) Android( com.android.annotations 和 android.support.annotations ) JSR-305( javax.annotation ) FindBugs( edu.umd.cs.findbugs.annotations ) Eclipse( org.eclipse.jdt.annotation ) Lombok( lombok.NonNull )。 你可以在 Kotlin 编译器源代码中找到完整的列表。 Kotlin 特殊处理⼀部分 Java 类型。这样的类型不是“按原样”从 Java 加载,⽽是 映射 到相应的 Kotlin 类型。 映射只发⽣在编译期间,运⾏时表⽰保持不 变。 Java 的原⽣类型映射到相应的 Kotlin 类型(请记住平台类型): Java 类型Java 类型 Kotlin 类型Kotlin 类型 byte kotlin.Byte short kotlin.Short int kotlin.Int long kotlin.Long char kotlin.Char float kotlin.Float double kotlin.Double boolean kotlin.Boolean ⼀些⾮原⽣的内置类型也会作映射: 平台类型表⽰法平台类型表⽰法 — — — 可空性注解可空性注解 — — — — — — 已映射类型 130 Java 类型Java 类型 Kotlin 类型Kotlin 类型 java.lang.Object kotlin.Any! java.lang.Cloneable kotlin.Cloneable! java.lang.Comparable kotlin.Comparable! java.lang.Enum kotlin.Enum! java.lang.Annotation kotlin.Annotation! java.lang.Deprecated kotlin.Deprecated! java.lang.CharSequence kotlin.CharSequence! java.lang.String kotlin.String! java.lang.Number kotlin.Number! java.lang.Throwable kotlin.Throwable! Java 的装箱原始类型映射到可空的 Kotlin 类型: Java typeJava type Kotlin typeKotlin type java.lang.Byte kotlin.Byte? java.lang.Short kotlin.Short? java.lang.Integer kotlin.Int? java.lang.Long kotlin.Long? java.lang.Character kotlin.Char? java.lang.Float kotlin.Float? java.lang.Double kotlin.Double? java.lang.Boolean kotlin.Boolean? 请注意,⽤作类型参数的装箱原始类型映射到平台类型: 例如,List 在 Kotlin 中会成为 List 。 集合类型在 Kotlin 中可以是只读的或可变的,因此 Java 集合类型作如下映射: (下表中的所有 Kotlin 类型都驻留在 kotlin.collections 包中): Java 类型Java 类型 Kotlin 只读类型Kotlin 只读类型 Kotlin 可变类型Kotlin 可变类型 加载的平台类型加载的平台类型 Iterator Iterator MutableIterator (Mutable)Iterator! Iterable Iterable MutableIterable (Mutable)Iterable! Collection Collection MutableCollection (Mutable)Collection! Set Set MutableSet (Mutable)Set! List List MutableList (Mutable)List! ListIterator ListIterator MutableListIterator (Mutable)ListIterator! Map Map MutableMap (Mutable)Map! Map.Entry Map.Entry MutableMap.MutableEntry (Mutable)Map.(Mutable)Entry! Java 的数组按下⽂所述映射: Java 类型Java 类型 Kotlin 类型Kotlin 类型 int[] kotlin.IntArray! String[] kotlin.Array<(out) String>! Kotlin 的泛型与 Java 有点不同(参⻅泛型)。当将 Java 类型导⼊ Kotlin 时,我们会执⾏⼀些转换: Java 的通配符转换成类型投影 Foo 转换成 Foo! Foo 转换成 Foo! Java的原始类型转换成星投影 Kotlin 中的 Java 泛型 — — — — 131 List 转换成 List<*>! ,即 List! 和 Java ⼀样,Kotlin 在运⾏时不保留泛型,即对象不携带传递到他们构造器中的那些类型参数的实际类型。 即 ArrayList() 和 ArrayList() 是不能区分的。 这使得执⾏ is-检测不可能照顾到泛型。 Kotlin 只允许 is-检测星投影的泛型类型: if (a is List) // 错误:⽆法检查它是否真的是⼀个 Int 列表 // but if (a is List<*>) // OK:不保证列表的内容 与 Java 不同,Kotlin 中的数组是不型变的。这意味着 Kotlin 不允许我们把⼀个 Array 赋值给⼀个 Array , 从⽽避免了可能的运⾏ 时故障。Kotlin 也禁⽌我们把⼀个⼦类的数组当做超类的数组传递给 Kotlin 的⽅法, 但是对于 Java ⽅法,这是允许的(通过 Array<(out) String>! 这种形式的平台类型)。 Java 平台上,数组会使⽤原⽣数据类型以避免装箱/拆箱操作的开销。 由于 Kotlin 隐藏了这些实现细节,因此需要⼀个变通⽅法来与 Java 代码进⾏交 互。 对于每种原⽣类型的数组都有⼀个特化的类( IntArray 、 DoubleArray 、 CharArray 等等)来处理这种情况。 它们与 Array 类⽆关,并且会 编译成 Java 原⽣类型数组以获得最佳性能。 假设有⼀个接受 int 数组索引的 Java ⽅法: public class JavaArrayExample { public void removeIndices(int[] indices) { // 在此编码…… } } 在 Kotlin 中你可以这样传递⼀个原⽣类型的数组: val javaObj = JavaArrayExample() val array = intArrayOf(0, 1, 2, 3) javaObj.removeIndices(array) // 将 int[] 传给⽅法 当编译为 JVM 字节代码时,编译器会优化对数组的访问,这样就不会引⼊任何开销: val array = arrayOf(1, 2, 3, 4) array[x] = array[x] * 2 // 不会实际⽣成对 get() 和 set() 的调⽤ for (x in array) { // 不会创建迭代器 print(x) } 即使当我们使⽤索引定位时,也不会引⼊任何开销 for (i in array.indices) {// 不会创建迭代器 array[i] += 2 } 最后,in-检测也没有额外开销 if (i in array.indices) { // 同 (i >= 0 && i < array.size) print(array[i]) } Java 类有时声明⼀个具有可变数量参数(varargs)的⽅法来使⽤索引。 — Java 数组数组 Java 可变参数 132 public class JavaArrayExample { public void removeIndices(int... indices) { // 在此编码…… } } 在这种情况下,你需要使⽤展开运算符 * 来传递 IntArray : val javaObj = JavaArray() val array = intArrayOf(0, 1, 2, 3) javaObj.removeIndicesVarArg(*array) ⽬前⽆法传递 null 给⼀个声明为可变参数的⽅法。 由于 Java ⽆法标记⽤于运算符语法的⽅法,Kotlin 允许 具有正确名称和签名的任何 Java ⽅法作为运算符重载和其他约定( invoke() 等)使⽤。 不允 许使⽤中缀调⽤语法调⽤ Java ⽅法。 在 Kotlin 中,所有异常都是⾮受检的,这意味着编译器不会强迫你捕获其中的任何⼀个。 因此,当你调⽤⼀个声明受检异常的 Java ⽅法时,Kotlin 不会强 迫你做任何事情: fun render(list: List<*>, to: Appendable) { for (item in list) { to.append(item.toString()) // Java 会要求我们在这⾥捕获 IOException } } 当 Java 类型导⼊到 Kotlin 中时,类型 java.lang.Object 的所有引⽤都成了 Any 。 ⽽因为 Any 不是平台指定的,它只声明了 toString() 、hashCode() 和 equals() 作为其成员, 所以为了能⽤到 java.lang.Object 的其他成员,Kotlin 要⽤到扩展函数。 Effective Java 第 69 条善意地建议优先使⽤并发⼯具(concurrency utilities)⽽不是 wait() 和 notify() 。 因此,类型 Any 的引⽤不提供这两个 ⽅法。 如果你真的需要调⽤它们的话,你可以将其转换为 java.lang.Object : (foo as java.lang.Object).wait() 要取得对象的 Java 类,请在类引⽤上使⽤ java 扩展属性。 val fooClass = foo::class.java 上⾯的代码使⽤了⾃ Kotlin 1.1 起⽀持的绑定的类引⽤。你也可以使⽤ javaClass 扩展属性。 val fooClass = foo.javaClass 要覆盖 clone() ,需要继承 kotlin.Cloneable : 操作符 受检异常 对象⽅法 wait()/notify() getClass() clone() 133 class Example : Cloneable { override fun clone(): Any { …… } } 不要忘记 Effective Java 的第 11 条: 谨慎地改写clone。 要覆盖 finalize() ,所有你需要做的就是简单地声明它,⽽不需要 override 关键字: class C { protected fun finalize() { // 终⽌化逻辑 } } 根据 Java 的规则,finalize() 不能是 private 的。 在 kotlin 中,类的超类中最多只能有⼀个 Java 类(以及按你所需的多个 Java 接⼝)。 Java 类的静态成员会形成该类的“伴⽣对象”。我们⽆法将这样的“伴⽣对象”作为值来传递, 但可以显式访问其成员,例如: if (Character.isLetter(a)) { // …… } Java 反射适⽤于 Kotlin 类,反之亦然。如上所述,你可以使⽤ instance::class.java , ClassName::class.java 或者 instance.javaClass 通过 java.lang.Class 来进⼊ Java 反射。 其他⽀持的情况包括为⼀个 Kotlin 属性获取⼀个 Java 的 getter/setter ⽅法或者幕后字段、为⼀个 Java 字段获取⼀个 KProperty 、为⼀个 KFunction 获取⼀个 Java ⽅法或者构造函数,反之亦然。 就像 Java 8 ⼀样,Kotlin ⽀持 SAM 转换。这意味着 Kotlin 函数字⾯值可以被⾃动的转换成 只有⼀个⾮默认⽅法的 Java 接⼝的实现,只要这个⽅法的参 数类型 能够与这个 Kotlin 函数的参数类型相匹配。 你可以这样创建 SAM 接⼝的实例: val runnable = Runnable { println(“This runs in a runnable“) } ……以及在⽅法调⽤中: val executor = ThreadPoolExecutor() // Java 签名:void execute(Runnable command) executor.execute { println(“This runs in a thread pool“) } 如果 Java 类有多个接受函数式接⼝的⽅法,那么可以通过使⽤ 将 lambda 表达式转换为特定的 SAM 类型的适配器函数来选择需要调⽤的⽅法。这些适 配器函数也会按需 由编译器⽣成。 executor.execute(Runnable { println(“This runs in a thread pool“) }) 请注意,SAM 转换只适⽤于接⼝,⽽不适⽤于抽象类,即使这些抽象类也只有⼀个 抽象⽅法。 还要注意,此功能只适⽤于 Java 互操作;因为 Kotlin 具有合适的函数类型,所以不需要将函数⾃动转换 为 Kotlin 接⼝的实现,因此不受⽀持。 nalize() 从 Java 类继承 访问静态成员 Java 反射 SAM 转换 134 要声明⼀个在本地(C 或 C++)代码中实现的函数,你需要使⽤ external 修饰符来标记它: external fun foo(x: Int): Double 其余的过程与 Java 中的⼯作⽅式完全相同。 在 Kotlin 中使⽤ JNI 135 Java 可以轻松调⽤ Kotlin 代码。 Kotlin 属性会编译成以下 Java 元素: ⼀个 getter ⽅法,名称通过加前缀 get 算出; ⼀个 setter ⽅法,名称通过加前缀 set 算出(只适⽤于 var 属性); ⼀个私有字段,与属性名称相同(仅适⽤于具有幕后字段的属性)。 例如,var firstName: String 编译成以下 Java 声明: private String firstName; public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } 如果属性的名称以 is 开头,则使⽤不同的名称映射规则:getter 的名称 与属性名称相同,并且 setter 的名称是通过将 is 替换为 set 获得。 例如,对 于属性 isOpen ,其 getter 会称做 isOpen() ,⽽其 setter 会称做 setOpen() 。 这⼀规则适⽤于任何类型的属性,并不仅限于 Boolean 。 在 org.foo.bar 包内的 example.kt ⽂件中声明的所有的函数和属性,包括扩展函数, 都编译成⼀个名为 org.foo.bar.ExampleKt 的 Java 类的静态⽅法。 // example.kt package demo class Foo fun bar() { } // Java new demo.Foo(); demo.ExampleKt.bar(); 可以使⽤ @JvmName 注解修改⽣成的 Java 类的类名: @file:JvmName(“DemoUtils“) package demo class Foo fun bar() { } // Java new demo.Foo(); demo.DemoUtils.bar(); Java 中调⽤ Kotlin 属性 — — — 包级函数 136 如果多个⽂件中⽣成了相同的 Java 类名(包名相同并且类名相同或者有相同的 @JvmName 注解)通常是错误的。然⽽,编译器能够⽣成⼀个单⼀的 Java 外观 类,它具有指定的名称且包含来⾃所有⽂件中具有该名称的所有声明。 要启⽤⽣成这样的外观,请在所有相关⽂件中使⽤ @JvmMultifileClass 注 解。 // oldutils.kt @file:JvmName(“Utils“) @file:JvmMultifileClass package demo fun foo() { } // newutils.kt @file:JvmName(“Utils“) @file:JvmMultifileClass package demo fun bar() { } // Java demo.Utils.foo(); demo.Utils.bar(); 如果需要在 Java 中将 Kotlin 属性作为字段暴露,那就需要使⽤ @JvmField 注解对其标注。 该字段将具有与底层属性相同的可⻅性。如果⼀个属性有 幕后字段(backing field)、⾮私有、没有 open / override 或者 const 修饰符并且不是被委托的属性,那么你可以⽤ @JvmField 注解该属性。 class C(id: String) { @JvmField val ID = id } // Java class JavaClient { public String getID(C c) { return c.ID; } } 延迟初始化的属性(在Java中)也会暴露为字段。 该字段的可⻅性与 lateinit 属性的 setter 相同。 在命名对象或伴⽣对象中声明的 Kotlin 属性会在该命名对象或包含伴⽣对象的类中 具有静态幕后字段。 通常这些字段是私有的,但可以通过以下⽅式之⼀暴露出来: @JvmField 注解; lateinit 修饰符; const 修饰符。 使⽤ @JvmField 标注这样的属性使其成为与属性本⾝具有相同可⻅性的静态字段。 实例字段 静态字段 — — — 137 class Key(val value: Int) { companion object { @JvmField val COMPARATOR: Comparator = compareBy { it.value } } } // Java Key.COMPARATOR.compare(key1, key2); // Key 类中的 public static final 字段 在命名对象或者伴⽣对象中的⼀个延迟初始化的属性 具有与属性 setter 相同可⻅性的静态幕后字段。 object Singleton { lateinit var provider: Provider } // Java Singleton.provider = new Provider(); // 在 Singleton 类中的 public static ⾮-final 字段 ⽤ const 标注的(在类中以及在顶层的)属性在 Java 中会成为静态字段: // ⽂件 example.kt object Obj { const val CONST = 1 } class C { companion object { const val VERSION = 9 } } const val MAX = 239 在 Java 中: int c = Obj.CONST; int d = ExampleKt.MAX; int v = C.VERSION; 如上所述,Kotlin 将包级函数表⽰为静态⽅法。 Kotlin 还可以为命名对象或伴⽣对象中定义的函数⽣成静态⽅法,如果你将这些函数标注为 @JvmStatic 的话。 如果你使⽤该注解,编译器既会在相应对象的类中⽣成静态⽅法,也会在对象⾃⾝中⽣成实例⽅法。 例如: class C { companion object { @JvmStatic fun foo() {} fun bar() {} } } 现在,foo() 在 Java 中是静态的,⽽ bar() 不是: C.foo(); // 没问题 C.bar(); // 错误:不是⼀个静态⽅法 C.Companion.foo(); // 保留实例⽅法 C.Companion.bar(); // 唯⼀的⼯作⽅式 静态⽅法 138 对于命名对象也同样: object Obj { @JvmStatic fun foo() {} fun bar() {} } 在 Java 中: Obj.foo(); // 没问题 Obj.bar(); // 错误 Obj.INSTANCE.bar(); // 没问题,通过单例实例调⽤ Obj.INSTANCE.foo(); // 也没问题 @JvmStatic  注解也可以应⽤于对象或伴⽣对象的属性, 使其 getter 和 setter ⽅法在该对象或包含该伴⽣对象的类中是静态成员。 Kotlin 的可⻅性以下列⽅式映射到 Java: private 成员编译成 private 成员; private 的顶层声明编译成包级局部声明; protected 保持 protected(注意 Java 允许访问同⼀个包中其他类的受保护成员, ⽽ Kotlin 不能,所以 Java 类会访问更⼴泛的代码); internal 声明会成为 Java 中的 public 。internal 类的成员会通过名字修饰,使其 更难以在 Java 中意外使⽤到,并且根据 Kotlin 规则使其 允许重载相同签名的成员 ⽽互不可⻅; public 保持 public 。 有时你需要调⽤有 KClass 类型参数的 Kotlin ⽅法。 因为没有从 Class 到 KClass 的⾃动转换,所以你必须通过调⽤ Class.kotlin 扩展属 性的等价形式来⼿动进⾏转换: kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class) 有时我们想让⼀个 Kotlin 中的命名函数在字节码中有另外⼀个 JVM 名称。 最突出的例⼦是由于类型擦除引发的: fun List.filterValid(): List fun List.filterValid(): List 这两个函数不能同时定义,因为它们的 JVM 签名是⼀样的:filterValid(Ljava/util/List;)Ljava/util/List; 。 如果我们真的希望它们在 Kotlin 中⽤相同名称,我们需要⽤ @JvmName 去标注其中的⼀个(或两个),并指定不同的名称作为参数: fun List.filterValid(): List @JvmName(“filterValidInt“) fun List.filterValid(): List 在 Kotlin 中它们可以⽤相同的名称 filterValid 来访问,⽽在 Java 中,它们分别是 filterValid 和 filterValidInt 。 同样的技巧也适⽤于属性 x 和函数 getX() 共存: val x: Int @JvmName(“getX_prop“) get() = 15 fun getX() = 10 可⻅性 — — — — — KClass ⽤ @JvmName 解决签名冲突 ⽣成重载 139 通常,如果你写⼀个有默认参数值的 Kotlin ⽅法,在 Java 中只会有⼀个所有参数都存在的完整参数 签名的⽅法可⻅,如果希望向 Java 调⽤者暴露多个 重载,可以使⽤ @JvmOverloads 注解。 @JvmOverloads fun f(a: String, b: Int = 0, c: String = “abc“) { …… } 对于每⼀个有默认值的参数,都会⽣成⼀个额外的重载,这个重载会把这个参数和 它右边的所有参数都移除掉。在上例中,会⽣成以下⽅法 : // Java void f(String a, int b, String c) { } void f(String a, int b) { } void f(String a) { } 该注解也适⽤于构造函数、静态⽅法等。它不能⽤于抽象⽅法,包括 在接⼝中定义的⽅法。 请注意,如次构造函数中所述,如果⼀个类的所有构造函数参数都有默认 值,那么会为其⽣成⼀个公有的⽆参构造函数。这就算 没有 @JvmOverloads 注 解也有效。 如上所述,Kotlin 没有受检异常。 所以,通常 Kotlin 函数的 Java 签名不会声明抛出异常。 于是如果我们有⼀个这样的 Kotlin 函数: // example.kt package demo fun foo() { throw IOException() } 然后我们想要在 Java 中调⽤它并捕捉这个异常: // Java try { demo.Example.foo(); } catch (IOException e) { // 错误:foo() 未在 throws 列表中声明 IOException // …… } 因为 foo() 没有声明 IOException ,我们从 Java 编译器得到了⼀个报错消息。 为了解决这个问题,要在 Kotlin 中使⽤ @Throws 注解。 @Throws(IOException::class) fun foo() { throw IOException() } 当从 Java 中调⽤ Kotlin 函数时,没⼈阻⽌我们将 null 作为⾮空参数传递。 这就是为什么 Kotlin 给所有期望⾮空参数的公有函数⽣成运⾏时检测。 这 样我们就能在 Java 代码⾥⽴即得到 NullPointerException 。 当 Kotlin 的类使⽤了声明处型变,有两种选择 可以从 Java 代码中看到它们的⽤法。让我们假设我们有以下类和两个使⽤它的函数: class Box(val value: T) interface Base class Derived : Base fun boxDerived(value: Derived): Box = Box(value) fun unboxBase(box: Box): Base = box.value 受检异常 空安全性 型变的泛型 140 ⼀种看似理所当然地将这俩函数转换成 Java 代码的⽅式可能会是: Box boxDerived(Derived value) { …… } Base unboxBase(Box box) { …… } 问题是,在 Kotlin 中我们可以这样写 unboxBase(boxDerived(“s“)) ,但是在 Java 中是⾏不通的,因为在 Java 中 类 Box 在其泛型参数 T 上 是不型变的,于是 Box 并不是 Box 的⼦类。 要使其在 Java 中⼯作,我们按以下这样定义 unboxBase : Base unboxBase(Box box) { …… } 这⾥我们使⽤ Java 的通配符类型( ? extends Base )来 通过使⽤处型变来模拟声明处型变,因为在 Java 中只能这样。 当它作为参数出现时,为了让 Kotlin 的 API 在 Java 中⼯作,对于协变定义的 Box 我们⽣成 Box 作为 Box (或者对 于逆变定义的 Foo ⽣成 Foo )。当它是⼀个返回值时, 我们不⽣成通配符,因为否则 Java 客⼾端将必须处理它们(并且它违反常⽤ Java 编码⻛格)。因此,我们的⽰例中的对应函数实际上翻译如下: // 作为返回类型——没有通配符 Box boxDerived(Derived value) { …… } // 作为参数——有通配符 Base unboxBase(Box box) { …… } 注意:当参数类型是 final 时,⽣成通配符通常没有意义,所以⽆论在什么地⽅ Box 始终转换为 Box 。 如果我们在默认不⽣成通配符的地⽅需要通配符,我们可以使⽤ @JvmWildcard 注解: fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value) // 将被转换成 // Box boxDerived(Derived value) { …… } 另⼀⽅⾯,如果我们根本不需要默认的通配符转换,我们可以使⽤ @JvmSuppressWildcards fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value // 会翻译成 // Base unboxBase(Box box) { …… } 注意:@JvmSuppressWildcards 不仅可⽤于单个类型参数,还可⽤于整个声明(如 函数或类),从⽽抑制其中的所有通配符。 类型 Nothing 是特殊的,因为它在 Java 中没有⾃然的对应。确实,每个 Java 引⽤类型,包括 java.lang.Void 都可以接受 null 值,但是 Nothing 不⾏。因此,这种类型不能在 Java 世界中 准确表⽰。这就是为什么在使⽤ Nothing 参数的地⽅ Kotlin ⽣成⼀个原始类型: fun emptyList(): List = listOf() // 会翻译成 // List emptyList() { …… } Nothing 类型翻译类型翻译 141 JavaScript 在⾯向 JVM 平台的代码中不⽀持动态类型 作为⼀种静态类型的语⾔,Kotlin仍然需要与⽆类型或松散类型的环境(例如 JavaScript⽣态系统)进⾏互操作。为了⽅便这些使⽤场景,语⾔中有 dynamic 类型可⽤: val dyn: dynamic = …… dynamic 类型基本上关闭了 Kotlin 的类型检查系统: 该类型的值可以赋值给任何变量或作为参数传递到任何位置, 任何值都可以赋值给 dynamic 类型的变量,或者传递给⼀个接受 dynamic 作为参数的函数, null -检查对这些值是禁⽤的。 dynamic 最特别的特性是,我们可以对 dynamic 变量调⽤任何任何属性或以任意参数调⽤任何任何函数 : dyn.whatever(1, “foo“, dyn) // “whatever”在任何地⽅都没有定义 dyn.whatever(*arrayOf(1, 2, 3)) 在 JavaScript 平台上,该代码将按照原样编译:在⽣成的 JavaScript 代码中,Kotlin中的 dyn.whatever(1) 变为 dyn.whatever(1) 。 当在 dynamic 类型的值上调⽤ Kotlin 写的函数时,请记住由 Kotlin 到 JavaScript 编译器执⾏的名字修饰。你可能需要使⽤ @JsName 注解 为要调⽤ 的函数分配明确的名称。 动态调⽤总是返回 dynamic 作为结果,所以我们可以⾃由地这样链接调⽤: dyn.foo().bar.baz() 当我们把⼀个 lambda 表达式传给⼀个动态调⽤时,它的所有参数默认都是 dynamic 类型的: dyn.foo { x -> x.bar() // x 是 dynamic } 使⽤ dynamic 类型值的表达式会按照原样转换为 JavaScript,并且不使⽤ Kotlin 运算符约定。 ⽀持以下运算符: ⼆元:+ 、 - 、 * 、 / 、 % 、 > 、 < 、 >= 、 <= 、 == 、 != 、 === 、 !== 、 && 、 || ⼀元 前置:- 、 + 、 ! 前置及后置:++ 、 -- 赋值:+= 、 -= 、 *= 、 /= 、 %= 索引访问: 读:d[a] ,多于⼀个参数会出错 写:d[a1] = a2 ,[] 中有多于⼀个参数会出错 in 、 !in 以及 .. 操作对于 dynamic 类型的值是禁⽤的。 动态类型 — — — — — — — — — — — 142 更多技术说明请参⻅规范⽂档。 143 Kotlin 已被设计为能够与 Java 平台轻松互操作。它将 Java 类视为 Kotlin 类,并且 Java 也将 Kotlin 类视为 Java 类。但是,JavaScript 是⼀种动态类型 语⾔,这意味着 它不会在编译期检查类型。你可以通过动态类型在 Kotlin 中⾃由地与 JavaScript 交流,但是如果你想要 Kotlin 类型系统的全部威⼒ ,你 可以为 JavaScript 库创建 Kotlin 头⽂件。 你可以使⽤ js(“……“) 函数将⼀些 JavaScript 代码嵌⼊到 Kotlin 代码中。 例如: fun jsTypeOf(o: Any): String { return js(“typeof o“) } js 的参数必须是字符串常量。因此,以下代码是不正确的: fun jsTypeOf(o: Any): String { return js(getTypeof() + “ o“) // 此处报错 } fun getTypeof() = “typeof“ 要告诉 Kotlin 某个声明是⽤纯 JavaScript 编写的,你应该⽤ external 修饰符来标记它。 当编译器看到这样的声明时,它假定相应类、函数或 属性的 实现由开发⼈员提供,因此不会尝试从声明中⽣成任何 JavaScript 代码。 这意味着你应该省略 external 声明内容的代码体。例如: external fun alert(message: Any?): Unit external class Node { val firstChild: Node fun append(child: Node): Node fun removeChild(child: Node): Node // 等等 } external val window: Window 请注意,嵌套的声明会继承 external 修饰符,即在 Node 类中,我们在 成员函数和属性之前并不放置 external 。 external 修饰符只允许在包级声明中使⽤。 你不能声明⼀个⾮ external 类的 external 成员。 在 JavaScript 中,你可以在原型或者类本⾝上定义成员。即: function MyClass() { } MyClass.sharedMember = function() { /* 实现 */ }; MyClass.prototype.ownMember = function() { /* 实现 */ }; Kotlin 中没有这样的语法。然⽽,在 Kotlin 中我们有伴⽣( companion )对象。Kotlin 以特殊的⽅式处理 external 类的伴⽣对象:替代期待⼀个对象 的是,它假定伴⽣对象的成员 就是该类⾃⾝的成员。要描述来⾃上例中的 MyClass ,你可以这样写: external class MyClass { companion object { fun sharedMember() } fun ownMember() } Kotlin 中调⽤ JavaScript 内联 JavaScript external 修饰符 声明类的(静态)成员声明类的(静态)成员 144 ⼀个外部函数可以有可选参数。 JavaScript 实现实际上如何计算这些参数的默认值,是 Kotlin 所不知道的, 因此在 Kotlin 中不可能使⽤通常的语法声明 这些参数。 你应该使⽤以下语法: external fun myFunWithOptionalArgs(x: Int, y: String = definedExternally, z: Long = definedExternally) 这意味着你可以使⽤⼀个必需参数和两个可选参数来调⽤ myFunWithOptionalArgs(它们的 默认值由⼀些 JavaScript 代码算出)。 你可以轻松扩展 JavaScript 类,因为它们是 Kotlin 类。只需定义⼀个 external 类并⽤ ⾮ external 类扩展它。例如: external open class HTMLElement : Element() { /* 成员 */ } class CustomElement : HTMLElement() { fun foo() { alert(“bar“) } } 有⼀些限制: 1. 当⼀个外部基类的函数被签名重载时,不能在派⽣类中覆盖它。 2. 不能覆盖⼀个使⽤默认参数的函数。 请注意,你⽆法⽤外部类扩展⾮外部类。 JavaScript 没有接⼝的概念。当函数期望其参数⽀持 foo 和 bar ⽅法时,只需传递实际具有这些⽅法的对象。 对于静态类型的 Kotlin,你可以使⽤接 ⼝来表达这点,例如: external interface HasFooAndBar { fun foo() fun bar() } external fun myFunction(p: HasFooAndBar) 外部接⼝的另⼀个使⽤场景是描述设置对象。例如: 声明可选参数声明可选参数 扩展扩展 JavaScript 类类 external 接⼝接⼝ 145 external interface JQueryAjaxSettings { var async: Boolean var cache: Boolean var complete: (JQueryXHR, String) -> Unit // 等等 } fun JQueryAjaxSettings(): JQueryAjaxSettings = js(“{}“) external class JQuery { companion object { fun get(settings: JQueryAjaxSettings): JQueryXHR } } fun sendQuery() { JQuery.get(JQueryAjaxSettings().apply { complete = { (xhr, data) -> window.alert(“Request complete“) } }) } 外部接⼝有⼀些限制: 1. 它们不能在 is 检查的右侧使⽤。 2. as 转换为外部接⼝总是成功(并在编译时产⽣警告)。 3. 它们不能作为具体化类型参数传递。 4. 它们不能⽤在类的字⾯值表达式(即 I::class )中。 146 Kotlin 编译器⽣成正常的 JavaScript 类,可以在 JavaScript 代码中⾃由地使⽤的函数和属性 。不过,你应该记住⼀些微妙的事情。 为了防⽌损坏全局对象,Kotlin 创建⼀个包含当前模块中所有 Kotlin 声明的对象 。所以如果你把模块命名为 myModule ,那么所有的声明都可以 通过 myModule 对象在 JavaScript 中可⽤。例如: fun foo() = “Hello“ 可以在 JavaScript 中这样调⽤: alert(myModule.foo()); 这不适⽤于当你将 Kotlin 模块编译为 JavaScript 模块时(关于这点的详细信息请参⻅ JavaScript 模块)。 在这种情况下,不会有⼀个包装对象,⽽是将 声明作为相应类型的 JavaScript 模块对外暴露。例如, 对于 CommonJS 的场景,你应该写: alert(require('myModule').foo()); Kotlin 将其包结构暴露给 JavaScript,因此除⾮你在根包中定义声明, 否则必须在 JavaScript 中使⽤完整限定的名称。例如: package my.qualified.packagename fun foo() = “Hello“ 可以在 JavaScript 中这样调⽤: alert(myModule.my.qualified.packagename.foo()); 在某些情况下(例如为了⽀持重载),Kotlin 编译器会修饰(mangle) JavaScript 代码中⽣成的函数和属性 的名称。要控制⽣成的名称,可以使⽤ @JsName 注解: // 模块“kjs” class Person(val name: String) { fun hello() { println(“Hello $name!“) } @JsName(“helloWithGreeting“) fun hello(greeting: String) { println(“$greeting $name!“) } } 现在,你可以通过以下⽅式在 JavaScript 中使⽤这个类: var person = new kjs.Person(“Dmitry“); // 引⽤到模块“kjs” person.hello(); // 输出“Hello Dmitry!” person.helloWithGreeting(“Servus“); // 输出“Servus Dmitry!” 如果我们没有指定 @JsName 注解,相应函数的名称会包含 从函数签名计算⽽来的后缀,例如 hello_61zpoe$ 。 请注意,Kotlin 编译器不会对 external 声明应⽤这种修饰,因此你不必在其上 使⽤ @JsName 。 值得注意的另⼀个例⼦是从外部类继承的⾮外部类。 在这种情况下,任何被覆盖的函数也不会被修饰。 JavaScript 中调⽤ Kotlin ⽤独⽴的 JavaScript 隔离声明 包结构 @JsName 注解注解 147 @JsName 的参数需要是⼀个常量字符串字⾯值,该字⾯值是⼀个有效的标识符。 任何尝试将⾮标识符字符串传递给 @JsName 时,编译器都会报错。 以 下⽰例会产⽣编译期错误: @JsName(“new C()“) // 此处出错 external fun newC() 除了 kotlin.Long 的 Kotlin 数字类型映射到 JavaScript Number。 kotlin.Char 映射到 JavaScript Number 来表⽰字符代码。 Kotlin 在运⾏时⽆法区分数字类型( kotlin.Long 除外),即以下代码能够⼯作: fun f() { val x: Int = 23 val y: Any = x println(y as Float) } Kotlin 保留了 kotlin.Int 、 kotlin.Byte 、 kotlin.Short 、 kotlin.Char 和 kotlin.Long 的溢出语义。 JavaScript 中没有 64 位整数,所以 kotlin.Long 没有映射到任何 JavaScript 对象, 它是由⼀个 Kotlin 类模拟的。 kotlin.String 映射到 JavaScript String。 kotlin.Any 映射到 JavaScript Object(即 new Object() 、 {} 等)。 kotlin.Array 映射到 JavaScript Array。 Kotlin 集合(即 List 、 Set 、 Map 等)没有映射到任何特定的 JavaScript 类型。 kotlin.Throwable 映射到 JavaScript Error。 Kotlin 在 JavaScript 中保留了惰性对象初始化。 Kotlin 不会在 JavaScript 中实现顶层属性的惰性初始化。 在 JavaScript 中表⽰ Kotlin 类型 — — — — — — — — — — — — 148 Kotlin 允许你将 Kotlin 项⽬编译为热⻔模块系统的 JavaScript 模块。以下是 可⽤选项的列表: 1. ⽆模块(Plain)。不为任何模块系统编译。像往常⼀样,你可以在全局作⽤域中以其名称访问模块。 默认使⽤此选项。 2. 异步模块定义(AMD,Asynchronous Module Definition),它尤其为 require.js 库所使⽤。 3. CommonJS 约定,⼴泛⽤于 node.js/npm ( require 函数和 module.exports 对象) 4. 统⼀模块定义(UMD,Unified Module Definitions),它与 AMD 和 CommonJS 兼容, 并且当在运⾏时 AMD 和 CommonJS 都不可⽤时,作 为“plain”使⽤。 选择⽬标模块系统的⽅式取决于你的构建环境: 设置每个模块: 打开“File → Project Structure…”,在“Modules”中找到你的模块并选择其下的“Kotlin”facet。在 “Module kind”字段中选择合适的模 块系统。 为整个项⽬设置: 打开“File → Settings”,选择“Build, Execution, Deployment”→“Compiler”→“Kotlin compiler”。 在 “Module kind”字段中选择合 适的模块系统。 要选择通过 Maven 编译时的模块系统,你应该设置 moduleKind 配置属性,即你的 pom.xml 应该看起来像这样: kotlin-maven-plugin org.jetbrains.kotlin ${kotlin.version} compile js commonjs 可⽤值包括:plain 、 amd 、 commonjs 、 umd 。 要选择通过 Gradle 编译时的模块系统,你应该设置 moduleKind 属性,即 compileKotlin2Js.kotlinOptions.moduleKind = “commonjs“ 可⽤的值类似于 Maven。 要告诉 Kotlin ⼀个 external 类、 包、 函数或者属性是⼀个 JavaScript 模块,你可以使⽤ @JsModule 注解。考虑你有以下 CommonJS 模块 叫“hello”: module.exports.sayHello = function(name) { alert(“Hello, “ + name); } 你应该在 Kotlin 中这样声明: JavaScript 模块 选择⽬标模块系统 在在 IntelliJ IDEA 中中 在在 Maven 中中 在在 Gradle 中中 @JsModule 注解 149 @JsModule(“hello“) external fun sayHello(name: String) ⼀些 JavaScript 库导出包(命名空间)⽽不是函数和类。 从 JavaScript ⻆度讲 它是⼀个具有⼀些成员的对象,这些成员是类、函数和属性。 将这些包作为 Kotlin 对象导⼊通常看起来不⾃然。 编译器允许使⽤以下助记符将导⼊的 JavaScript 包映射到 Kotlin 包: @file:JsModule(“extModule“) package ext.jspackage.name external fun foo() external class C 其中相应的 JavaScript 模块的声明如下: module.exports = { foo: { /* 此处⼀些代码 */ }, C: { /* 此处⼀些代码 */ } } 重要提⽰:标有 @file:JsModule 注解的⽂件⽆法声明⾮外部成员。 下⾯的⽰例会产⽣编译期错误: @file:JsModule(“extModule“) package ext.jspackage.name external fun foo() fun bar() = “!“ + foo() + “!“ // 此处报错 在前⽂⽰例中,JavaScript 模块导出单个包。 但是,⼀些 JavaScript 库会从模块中导出多个包。 Kotlin 也⽀持这种场景,尽管你必须为每个导⼊的包声明 ⼀个新的 .kt ⽂件。 例如,让我们的⽰例更复杂⼀些: module.exports = { mylib: { pkg1: { foo: function() { /* 此处⼀些代码 */ }, bar: function() { /* 此处⼀些代码 */ } }, pkg2: { baz: function() { /* 此处⼀些代码 */ } } } } 要在 Kotlin 中导⼊该模块,你必须编写两个 Kotlin 源⽂件: @file:JsModule(“extModule“) @file:JsQualifier(“mylib.pkg1“) package extlib.pkg1 external fun foo() external fun bar() 以及 将将 @JsModule 应⽤到包应⽤到包 导⼊更深的包层次结构导⼊更深的包层次结构 150 @file:JsModule(“extModule“) @file:JsQualifier(“mylib.pkg2“) package extlib.pkg2 external fun baz() 当⼀个声明具有 @JsModule 、当你并不把它编译到⼀个 JavaScript 模块时,你不能在 Kotlin 代码中使⽤它。 通常,开发⼈员将他们的库既作为 JavaScript 模块也作为可下载的 .js ⽂件分发,你可以将这些⽂件复制到 项⽬的静态资源,并通过