原文:
zh.annas-archive.org/md5/5516731C6537B7140E922B2C519B8673译者:飞龙
前言
如今,Android 应用程序开发流程非常广泛。在过去几年中,我们已经看到各种工具如何演变,使我们的生活变得更加轻松。然而,Android 应用程序开发流程的一个核心元素在很长一段时间内并没有太多改变,那就是 Java。Android 平台适应了较新版本的 Java,但要能够使用它们,我们需要等待很长时间,直到新的 Android 设备达到适当的市场普及。此外,在 Java 中开发应用程序也带来了一系列挑战,因为 Java 是一种旧语言,存在许多设计问题,由于向后兼容性约束,这些问题并不容易解决。
另一方面,Kotlin 是一种新的但稳定的语言,可以在所有 Android 设备上运行,并解决 Java 无法解决的许多问题。它为 Android 开发带来了许多经过验证的编程概念。这是一种很棒的语言,可以让开发人员的生活变得更加轻松,并允许编写更安全、更表达和更简洁的代码。
本书是一本易于跟随的实用指南,将帮助您加快并改进使用 Kotlin 进行 Android 开发的流程。我们将介绍许多快捷方式和改进方法,以及解决常见问题的新方法。通过本书,您将熟悉 Kotlin 的特性和工具,并能够完全使用 Kotlin 开发 Android 应用程序。
本书涵盖内容
第一章,开始你的 Kotlin 之旅,讨论了 Kotlin 语言,其特性和使用原因。我们将向读者介绍 Kotlin 平台,并展示 Kotlin 如何适用于 Android 开发流程。
第二章,奠定基础,主要是关于 Kotlin 的基本构建块。它介绍了各种构造、数据类型和使 Kotlin 成为一种愉快的工作语言的特性。
第三章,玩转函数,解释了定义和调用函数的各种方式。我们还将讨论函数修饰符,并查看函数可以定义的可能位置。
第四章,类和对象,讨论了与面向对象编程相关的 Kotlin 特性。您将了解不同类型的类。我们还将看到改进可读性的特性:属性操作符重载和中缀调用。
第五章,函数作为一等公民,涵盖了 Kotlin 对函数式编程和函数作为一等公民的支持。我们将更仔细地看一下 lambda、高阶函数和函数类型。
第六章,泛型是你的朋友,探讨了泛型类、接口和函数的主题。我们将更仔细地看一下 Kotlin 的泛型类型系统。
第七章,扩展函数和属性,演示了如何在不使用继承的情况下向现有类添加新行为。我们还将讨论处理集合和流处理的更简单方法。
第八章,代理,展示了 Kotlin 如何简化类委托,因为它具有内置的语言支持。我们将看到如何通过使用内置属性代理和定义自定义代理来使用它。
第九章,制作你的 Marvel 画廊应用程序,利用了本书中讨论的大多数功能,并用它来构建一个完全功能的 Kotlin Android 应用程序。
您需要什么来阅读本书
要测试和使用本书中提供的代码,您只需要安装 Android Studio。第一章,开始你的 Kotlin 之旅,解释了如何启动一个新项目以及如何检查这里提供的示例。它还描述了如何在没有安装任何程序的情况下测试这里提供的大部分代码。
本书适合谁
要使用本书,您应该熟悉两个领域:
-
您需要了解 Java 和面向对象的编程概念,包括对象、类、构造函数、接口、方法、getter、setter 和通用类型。因此,如果这个领域对您来说毫无头绪,那么要完全理解本书的其余部分将会很困难。最好从一本介绍性的 Java 书开始,然后再回到本书。
-
虽然不是强制的,但了解 Android 平台是非常有帮助的,因为它将帮助您更详细地理解所呈现的示例,并且您将更深入地了解 Kotlin 解决的问题。如果您是具有 6-12 个月经验的 Android 开发人员,或者您已经创建了一些 Android 应用程序,那么您将没问题。另一方面,如果您对 OOP 概念感到满意,但对 Android 平台的了解有限,那么您可能仍然可以应付本书的大部分内容。
保持开放的心态和渴望学习新技术将非常有帮助。如果有什么让你好奇或引起你的注意,随时可以在阅读本书的同时测试并玩耍。
约定
在本书中,您将找到一些区分不同信息类型的文本样式。以下是这些样式的一些示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“让我们看看 range 数据类型,它允许定义包含结束的范围。”
代码块设置如下:
val capitol = "England" to "London"
println(capitol.first) // Prints: England
println(capitol.second) // Prints: London
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:
ext.kotlin_version = '1.1.3'
repositories {
maven { url 'https://maven.google.com' }
jcenter()
}
任何命令行输入或输出都是这样写的:
sdk install kotlin
新术语和重要单词以粗体显示。例如,屏幕上看到的单词,例如菜单或对话框中的单词,会出现在文本中,如下所示:“设置新项目的名称、包和位置。记得勾选包括 Kotlin 支持选项。”
警告或重要说明看起来像这样。
提示和技巧看起来像这样。
第一章:开始你的 Kotlin 之旅
Kotlin 是一种优秀的语言,使 Android 开发更加简单、快速和愉快。在本章中,我们将讨论 Kotlin 的真正含义,并查看许多 Kotlin 示例,这将帮助我们构建更好的 Android 应用程序。欢迎来到 Kotlin 的惊人之旅,它将改变您对编写代码和解决常见编程问题的方式。
在本章中,我们将涵盖以下主题:
-
使用 Kotlin 的第一步
-
实用的 Kotlin 示例
-
在 Android Studio 中创建新的 Kotlin 项目
-
将现有的 Java 项目迁移到 Kotlin
-
Kotlin 标准库(stdlib)
-
为什么学习 Kotlin 是一个不错的选择
与 Kotlin 打个招呼
Kotlin 是一种现代的、静态类型的、与 Android 兼容的语言,它解决了许多Java的问题,比如空指针异常或过多的代码冗余。Kotlin 是一种受 Swift、Scala、Groovy、C#和许多其他语言启发的语言。Kotlin 是由 JetBrains 专业人员设计的,基于对开发者经验、最佳使用指南(最重要的是clean code和effective Java)以及有关该语言使用情况的数据的分析。对其他编程语言进行了深入分析。Kotlin 努力避免重复其他语言的错误,并利用它们最有用的特性。在使用 Kotlin 时,我们真的可以感觉到这是一种成熟且设计良好的语言。
Kotlin 通过提高代码质量和安全性以及提高开发人员的性能,将应用程序开发提升到一个全新的水平。Google 在 2017 年宣布正式支持 Android 平台的 Kotlin,但 Kotlin 语言已经存在一段时间了。它拥有一个非常活跃的社区,而且在 Android 平台上的 Kotlin 采用已经迅速增长。我们可以将 Kotlin 描述为一种安全、表达力强、简洁、多功能且友好的语言,它与 Java 和 JavaScript 具有很好的互操作性。让我们讨论一下这些特点:
- 安全性:Kotlin 在空指针和不可变性方面提供了安全功能。Kotlin 是静态类型的,因此在编译时就知道每个表达式的类型。编译器可以验证我们尝试访问的任何属性或方法或特定类实例是否真的存在。这应该是熟悉的 Java,它也是静态类型的,但与 Java 不同,Kotlin 的类型系统更加严格(安全)。我们必须明确告诉编译器给定的变量是否可以存储空值。这允许在编译时使程序失败,而不是在运行时抛出
NullPointerException:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00004.jpg
-
易于调试:在开发阶段可以更快地检测到错误,而不是在发布后导致应用程序崩溃,从而损害用户体验。Kotlin 提供了一种方便的方法来处理不可变数据。例如,它可以通过提供便利的接口(底层集合仍然是可变的)来区分可变(读写)和不可变(只读)集合。
-
简洁性:大部分 Java 的冗长性都被消除了。我们需要更少的代码来完成常见任务,因此样板代码的数量大大减少,甚至将 Kotlin 与 Java 8 进行比较。结果,代码也更容易阅读和理解(表达力强)。
-
互操作性:Kotlin 被设计为可以与 Java(跨语言项目)无缝协同工作。现有的 Java 库和框架可以在 Kotlin 中无需任何性能损失地工作。许多 Java 库甚至有针对 Kotlin 的版本,可以更符合 Kotlin 的习惯用法。Kotlin 类也可以直接在 Java 代码中实例化和透明地引用,而无需任何特殊的语义,反之亦然。这使我们可以将 Kotlin 整合到现有的 Android 项目中,并且可以轻松地与 Java 一起使用(如果我们愿意)。
-
多功能性:我们可以针对许多平台,包括移动应用程序(Android)、服务器端应用程序(后端)、桌面应用程序、在浏览器中运行的前端代码,甚至构建系统(Gradle)。
任何编程语言的好坏取决于其工具支持。Kotlin 在现代 IDE(如 Android Studio、IntelliJ Idea 和 Eclipse)中有出色的支持。常见任务如代码辅助或重构都得到了妥善处理。Kotlin 团队努力使每个版本的 Kotlin 插件更好。大多数错误都能迅速修复,社区提出的许多功能也得到了实现。
Kotlin 错误跟踪器:youtrack.jetbrains.com/issues/KT Kotlin slack 频道:slack.kotlinlang.org/
使用 Kotlin 进行 Android 应用程序开发变得更加高效和愉快。Kotlin 与 JDK 6 兼容,因此使用 Kotlin 创建的应用程序甚至可以在旧的 Android 设备上安全运行,这些设备先于 Android 4。
Kotlin 旨在通过结合程序设计和函数式编程的概念和元素,为您带来最佳的体验。它遵循了书籍《Effective Java》,第二版,作者 Joshua Bloch 描述的许多准则,这被认为是每个 Java 开发人员必读的书籍。
此外,Kotlin 是开源的,因此我们可以查看项目并积极参与 Kotlin 项目的任何方面,如 Kotlin 插件、编译器、文档或 Kotlin 语言本身。
令人惊叹的 Kotlin 示例
对于 Android 开发人员来说,学习 Kotlin 真的很容易,因为语法类似于 Java,而且 Kotlin 经常感觉像是自然的 Java 演变。在开始时,开发人员通常会根据 Java 的习惯编写 Kotlin 代码,但过一段时间后,很容易转移到更符合惯例的 Kotlin 解决方案。让我们看一些酷炫的 Kotlin 功能,并看看 Kotlin 在哪些地方可能通过更简单、更简洁和更灵活的方式解决常见的编程任务而提供好处。我们试图保持示例简单和自解释,但它们利用了本书各个部分的内容,所以如果目前还没有完全理解也没关系。本节的目标是专注于可能性,并展示使用 Kotlin 可以实现什么。本节不一定需要完全描述如何实现它。让我们从变量声明开始:
var name = "Igor" // Inferred type is String
name = "Marcin"
请注意,Kotlin 不需要分号。你仍然可以使用它们,但它们是可选的。我们也不需要指定变量类型,因为它是从上下文中推断出来的。每当编译器可以从上下文中推断出类型时,我们就不必明确指定它。Kotlin 是一种强类型语言,因此每个变量都有适当的类型:
var name = "Igor"
name = 2 // Error, because name type is String
变量具有推断的String类型,因此分配不同值(整数)将导致编译错误。现在,让我们看看 Kotlin 如何改进使用字符串模板添加多个字符串的方式:
val name = "Marcin"
println("My name is $name") // Prints: My name is Marcin
我们不再需要使用+字符来连接字符串。在 Kotlin 中,我们可以轻松地将单个变量甚至整个表达式合并到字符串文字中:
val name = "Igor"
println("My name is ${name.toUpperCase()}")
// Prints: My name is IGOR
在 Java 中,任何变量都可以存储空值。在 Kotlin 中,严格的空安全强制我们明确标记每个可以存储可空值的变量:
var a: String = "abc"
a = null // compilation error
var b: String? = "abc"
b = null // It is correct
向数据类型(字符串与字符串?)添加问号,我们说变量可以是可空的(可以存储空引用)。如果我们不将变量标记为可空,我们将无法将可空引用分配给它。Kotlin 还允许以适当的方式处理可空变量。我们可以使用安全调用运算符在可能为空的变量上安全调用方法:
savedInstanceState?.doSomething
只有在savedInstanceState具有非空值时,才会调用doSomething方法,否则方法调用将被忽略。这是 Kotlin 避免 Java 中常见的空指针异常的安全方式。
Kotlin 还有几种新的数据类型。让我们看看Range数据类型,它允许我们定义包含结束的范围:
for (i in 1..10) {
print(i)
} // 12345678910
Kotlin 引入了Pair数据类型,结合中缀 表示,允许我们保存一对常见的值:
val capitol = "England" to "London"
println(capitol.first) // Prints: England
println(capitol.second) // Prints: London
我们可以使用破坏性声明将其解构为单独的变量:
val (country, city) = capitol
println(country) // Prints: England
println(city) // Prints: London
我们甚至可以迭代一对对:
val capitols = listOf("England" to "London", "Poland" to "Warsaw")
for ((country, city) in capitols) {
println("Capitol of $country is $city")
}
// Prints:
// Capitol of England is London
// Capitol of Poland is Warsaw
或者,我们可以使用forEach函数:
val capitols = listOf("England" to "London", "Poland" to "Warsaw")
capitols.forEach { (country, city) ->
println("Capitol of $country is $city")
}
请注意,Kotlin 通过提供一组接口和辅助方法(List与MutableList,Set与Set与MutableSet,Map与MutableMap等)区分可变和不可变集合:
val list = listOf(1, 2, 3, 4, 5, 6) // Inferred type is List
val mutableList = mutableListOf(1, 2, 3, 4, 5, 6)
// Inferred type is MutableList
不可变集合意味着集合状态在初始化后无法更改(无法添加/删除项目)。可变集合(显然)意味着状态可以改变。
使用 lambda 表达式,我们可以以非常简洁的方式使用 Android 框架构建:
view.setOnClickListener {
println("Click")
}
Kotlin 标准库(stdlib)包含许多函数,允许我们以简单而简洁的方式对集合执行操作。我们可以轻松地对列表进行流处理:
val text = capitols.map { (country, _) -> country.toUpperCase() }
.onEach { println(it) }
.filter { it.startsWith("P") }
.joinToString (prefix = "Countries prefix P:")
// Prints: ENGLAND POLAND
println(text) // Prints: Countries prefix P: POLAND
.joinToString (prefix = "Countries prefix P:")
请注意,我们不必向 lambda 传递参数。我们还可以定义自己的 lambda,这将使我们以全新的方式编写代码。这个 lambda 将允许我们仅在 Android Marshmallow 或更新版本中运行特定的代码片段。
inline fun supportsMarshmallow(code: () -> Unit) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
code()
}
//usage
supportsMarshmallow {
println("This code will only run on Android Nougat and newer")
}
我们可以轻松地发出异步请求,并在主线程上显示响应,使用doAsync函数:
doAsync {
var result = runLongTask() // runs on background thread
uiThread {
toast(result) // run on main thread
}
}
智能转换允许我们编写代码而不执行冗余的转换:
if (x is String) {
print(x.length) // x is automatically casted to String
}
x.length //error, x is not casted to a String outside if block
if (x !is String)
return
x.length // x is automatically casted to String
Kotlin 编译器知道变量*x*在执行检查后是String类型,因此它将自动将其转换为String类型*,*允许调用String类的所有方法和访问所有属性而无需进行任何显式转换。
有时,我们有一个返回单个表达式值的简单函数。在这种情况下,我们可以使用具有表达式主体的函数来缩短语法:
fun sum(a: Int, b: Int) = a + b
println (sum(2 + 4)) // Prints: 6
使用默认参数语法,我们可以为每个函数参数定义默认值,并以各种方式调用它:
fun printMessage(product: String, amount: Int = 0,
name: String = "Anonymous") {
println("$name has $amount $product")
}
printMessage("oranges") // Prints: Anonymous has 0 oranges
printMessage("oranges", 10) // Prints: Anonymous has 10 oranges
printMessage("oranges", 10, "Johny")
// Prints: Johny has 10 oranges
唯一的限制是我们需要提供所有参数而不带默认值。我们还可以使用命名参数 语法指定函数参数:
printMessage("oranges", name = "Bill")
这也增加了在函数调用中使用多个参数时的可读性。
数据类提供了一种非常简单的方式来定义和操作数据模型中的类。要定义一个合适的数据类,我们将在类名之前使用data修饰符:
data class Ball(var size:Int, val color:String)
val ball = Ball(12, "Red")
println(ball) // Prints: Ball(size=12, color=Red)
请注意,我们有一个非常好的、人类可读的类实例的字符串表示,我们不需要new关键字来实例化类。我们还可以轻松地创建类的自定义副本:
val ball = Ball(12, "Red")
println(ball) // prints: Ball(size=12, color=Red)
val smallBall = ball.copy(size = 3)
println(smallBall) // prints: Ball(size=3, color=Red)
smallBall.size++
println(smallBall) // prints: Ball(size=4, color=Red)
println(ball) // prints: Ball(size=12, color=Red)
前面的构造使得使用不可变对象非常容易和方便。
Kotlin 中最好的功能之一是扩展。它们允许我们向现有类添加新行为(方法或属性)而不更改其实现。有时,当您使用库或框架时,您可能希望为某个类添加额外的方法或属性。扩展是添加这些缺失成员的绝佳方式。扩展减少了代码冗长,并消除了使用 Java 中已知的实用函数的需要(例如StringUtils类)。我们可以轻松地为自定义类、第三方库甚至 Android 框架类定义扩展。首先,ImageView没有从网络加载图像的能力,因此我们可以添加loadImage扩展方法来使用Picasso库(用于 Android 的图像加载库)加载图像:
fun ImageView.loadUrl(url: String) {
Picasso.with(context).load(url).into(this)
}
\\usage
imageView.loadUrl("www.test.com\\image1.png")
我们还可以向Activity类添加一个显示 toast 的简单方法:
fun Context.toast(text:String) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
}
//usage (inside Activity class)
toast("Hello")
在许多地方,使用扩展将使我们的代码更简单、更简洁。使用 Kotlin,我们可以充分利用 lambda 来进一步简化 Kotlin 代码。
在 Kotlin 中,接口可以具有默认实现,只要它们不保存任何状态:
interface BasicData {
val email:String
val name:String
get() = email.substringBefore("@")
}
在 Android 中,有许多应用程序需要延迟对象初始化直到需要(使用)它为止。为了解决这个问题,我们可以使用委托:
val retrofit by lazy {
Retrofit.Builder()
.baseUrl("https://www.github.com")
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
Retrofit(一种流行的 Android 网络框架)属性初始化将延迟到第一次访问该值时。延迟初始化可能会导致更快的 Android 应用程序启动时间,因为加载被推迟到变量被访问时。这是在类中初始化多个对象的好方法,特别是当它们并非总是需要(对于某些类的使用场景,我们可能只需要特定的对象)或者并非在类创建后立即需要时。
所有呈现的示例只是 Kotlin 可以实现的一小部分。我们将在本书中学习如何利用 Kotlin 的强大功能。
处理 Kotlin 代码
有多种管理和运行 Kotlin 代码的方式。我们将主要关注 Android Studio 和 Kotlin Playground。
Kotlin Playground
在不需要安装任何软件的情况下尝试 Kotlin 代码的最快方式是 Kotlin Playground (try.kotlinlang.org ). 我们可以在那里使用 JavaScript 或 JVM Kotlin 实现运行 Kotlin 代码,并轻松切换不同的 Kotlin 版本。所有不需要 Android 框架依赖并且可以在Kotlin Playground中执行的书中代码示例。
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00005.jpg
main函数是每个 Kotlin 应用程序的入口点。当任何应用程序启动时,都会调用这个函数,因此我们必须将书中示例的代码放在这个方法的主体中。我们可以直接放置代码,或者只是调用另一个包含更多 Kotlin 代码的函数:
fun main(args: Array<String>) {
println("Hello, world!")
}
Android 应用程序有多个入口点。main函数会被 Android 框架隐式调用,因此我们不能用它在 Android 平台上运行 Kotlin 代码。
Android Studio
所有 Android Studio 现有的工具都可以处理 Kotlin 代码。我们可以轻松使用调试、lint 检查、正确的代码辅助、重构等。大部分功能的使用方式与 Java 相同,因此最明显的变化是 Kotlin 语言的语法。我们只需要在项目中配置 Kotlin 即可。
Android 应用程序有多个入口点(不同的意图可以启动应用程序中的不同组件),并且需要 Android 框架依赖。为了运行书中的示例,我们需要扩展Activity类并在其中放置代码。
为项目配置 Kotlin
从 Android Studio 3.0 开始,Kotlin 获得了完整的工具支持。不需要安装 Kotlin 插件,Kotlin 甚至更深入地集成到了 Android 开发过程中。
要在 Android Studio 2.x 中使用 Kotlin,我们必须手动安装 Kotlin 插件。要安装它,我们需要转到 Android Studio | 文件 | 设置 | 插件 | 安装 JetBrains 插件… | Kotlin 并按下安装按钮:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00006.jpg
要使用 Kotlin,我们需要在项目中配置 Kotlin。对于现有的 Java 项目,我们需要运行在项目中配置 Kotlin操作(在 Windows 中的快捷键是Ctrl +Shift +A,在 macOS 中是command + shift + A)或者使用相应的工具 |Kotlin | 在项目中配置 Kotlin 菜单项:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00007.jpg
然后,选择 Android with Gradle:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00008.jpg
最后,我们需要选择所需的模块和适当的 Kotlin 版本:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00009.jpg
前面的配置场景也适用于最初是用 Java 创建的所有现有 Android 项目。从 Android Studio 3.0 开始,我们还可以在创建新项目时勾选包括 Kotlin 支持的复选框:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00010.jpg
在这两种情况下,在项目中配置 Kotlin命令会通过添加 Kotlin 依赖项来更新根build.gradle文件和对应模块的build.gradle文件。它还会将 Kotlin 插件添加到 Android 模块中。在撰写本书发布版本的 Android Studio 3 时尚未提供,但我们可以从预发布版本中查看构建脚本:
//build.gradle file in project root folder
buildscript {
ext.kotlin_version = '1.1'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-alpha9'
classpath "org.jetbrains.kotlin:kotlin-gradle-
plugin:$kotlin_version"
}
}
...
//build.gradle file in the selected modules
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
...
dependencies {
...
implementation 'com.android.support.constraint:constraint-
layout:1.0.2'
}
...
在 Android Plugin for Gradle 3.x 之前(随 Android Studio 3.0 提供),使用compile依赖配置而不是implementation。
要更新 Kotlin 版本(比如说在将来),我们需要在build.gradle文件(项目根文件夹)中更改kotlin_version变量的值。Gradle 文件的更改意味着项目必须同步,这样 Gradle 才能更新其配置并下载所有所需的依赖项:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00011.jpg
在新的 Android 项目中使用 Kotlin
在 Android Studio 3.x 中创建的新 Kotlin 项目中,主要活动将已经在 Kotlin 中定义,因此我们可以立即开始编写 Kotlin 代码:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00012.jpg
添加新的 Kotlin 文件类似于添加 Java 文件。只需右键单击包,然后选择新建|Kotlin 文件/类:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00013.jpg
IDE 之所以说 Kotlin 文件/类而不是简单的Kotlin 类,类似于Java 类,是因为我们可以在单个文件中定义更多成员。我们将在第二章中更详细地讨论这个问题,奠定基础。
请注意,Kotlin 源文件可以位于java源文件夹内。我们可以为 Kotlin 创建一个新的源文件夹,但这并不是必需的:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00014.jpg
运行和调试项目与 Java 完全相同,除了在项目中配置 Kotlin 之外,不需要任何额外的步骤:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00015.jpg
从 Android Studio 3.0 开始,各种 Android 模板也将允许我们选择一种语言。这是新的配置活动向导:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00016.jpg
Java 转 Kotlin 转换器(J2K)
迁移现有的 Java 项目也相当容易,因为我们可以在同一个项目中同时使用 Java 和 Kotlin。还有一些方法可以通过使用Java 转 Kotlin 转换器(J2K)将现有的 Java 代码转换为 Kotlin 代码。
第一种方法是使用将 Java 文件转换为 Kotlin命令(在 Windows 中的键盘快捷键为Alt + Shift + Ctrl + K,在 macOS 中为option + shift + command + K)将整个 Java 文件转换为 Kotlin 文件,这非常有效。第二种方法是将 Java 代码粘贴到现有的 Kotlin 文件中,代码也将被转换(将出现一个转换建议的对话框)。在学习 Kotlin 时,这可能非常有帮助。
如果我们不知道如何用 Kotlin 编写特定的代码片段,我们可以用 Java 编写,然后简单地复制到剪贴板,然后粘贴到 Kotlin 文件中。转换后的代码可能不是最符合 Kotlin 的版本,但它可以工作。IDE 将显示各种意图,以便进一步转换代码并提高其质量。在转换之前,我们需要确保 Java 代码是有效的,因为转换工具非常敏感,即使缺少一个分号,过程也会失败。J2K 转换器结合 Java 互操作性,允许逐步将 Kotlin 引入现有项目(例如,逐个转换单个类)。
运行 Kotlin 代码的替代方法
Android Studio 提供了一种在不运行 Android 应用程序的情况下运行 Kotlin 代码的替代方法。当您想要快速测试一些 Kotlin 代码而不需要进行漫长的 Android 编译和部署过程时,这是非常有用的。
运行 Kotlin 代码的方法是使用构建 Kotlin 读取评估打印循环(REPL)。 REPL 是一个简单的语言 shell,它读取单个用户输入,评估它,并打印结果:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00017.jpg
REPL 看起来像命令行,但它将为我们提供所有所需的代码提示,并且让我们访问项目内定义的各种结构(类、接口、顶级函数等):
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00018.jpg
REPL 的最大优势是速度。我们可以非常快速地测试 Kotlin 代码。
Kotlin 的内部工作原理
我们主要将重点放在 Android 上,但请记住,Kotlin 可以编译到多个平台。Kotlin 代码可以编译为Java 字节码,然后再编译为Dalvik 字节码。以下是 Android 平台的 Kotlin 构建过程的简化版本:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00019.jpg
-
扩展名为
.java的文件包含 Java 代码 -
扩展名为
.kt的文件包含 Kotlin 代码 -
扩展名为
.class的文件包含 Java 字节码 -
扩展名为
.dex的文件包含 Dalvik 字节码 -
扩展名为
.apk的文件包含AndroidManifest文件、资源和.dex文件
对于纯 Kotlin 项目,只会使用 Kotlin 编译器,但 Kotlin 也支持跨语言项目,在这种情况下,将同时使用两个编译器来编译 Android 应用程序,并且结果将在类级别合并。
Kotlin 标准库
Kotlin 标准库(stdlib)是一个非常小的库,与 Kotlin 一起分发。运行 Kotlin 编写的应用程序需要它,并且在构建过程中会自动添加到我们的应用程序中。
在 Kotlin 1.1 中,运行 Kotlin 编写的应用程序需要kotlin-runtime。事实上,在 Kotlin 1.1 中有两个组件(kotlin-runtime和kotlin-stdlib)共享了很多 Kotlin 包。为了减少混乱,这两个组件将在即将推出的 Kotlin 1.2 版本中合并为单个组件(kotlin-stdlib)。从 Kotlin 1.2 开始,运行 Kotlin 编写的应用程序需要kotlin-stdlib。
Kotlin 标准库提供了与 Kotlin 的日常工作所需的基本元素。这些包括:
-
数组、集合、列表、范围等数据类型
-
扩展
-
高阶函数
-
用于处理字符串和字符序列的各种实用工具
-
为 JDK 类提供的扩展,使得处理文件、IO 和线程变得更加方便。
使用 Kotlin 的更多原因
Kotlin 得到了 JetBrains 的强大商业支持,这家公司为许多流行的编程语言提供了非常受欢迎的 IDE(Android Studio 基于 JetBrains IntelliJ IDEA)。JetBrains 希望提高他们的代码质量和团队绩效,因此他们需要一种能解决所有 Java 问题并提供与 Java 无缝互操作性的语言。没有其他 JVM 语言符合这些要求,因此 JetBrains 最终决定创建自己的语言并开始 Kotlin 项目。如今,Kotlin 被用于他们的旗舰产品。有些人将 Kotlin 与 Java 一起使用,而另一些则是纯 Kotlin 产品。
Kotlin 是一种非常成熟的语言。事实上,它的开发早在 Google 宣布官方支持 Android 之前就开始了(第一个提交日期为 2010-11-08):
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00020.jpg
语言的初始名称是Jet。在某个时候,JetBrains 团队决定将其改名为 Kotlin。这个名字来自于圣彼得堡附近的科特林岛,类似于 Java,Java 也是以印度尼西亚岛屿命名的。
在 2016 年发布 1.0 版本后,越来越多的公司开始支持 Kotlin 项目。Gradle 将 Kotlin 支持添加到构建脚本中,Android 库的最大创建者 Square 表示他们强烈支持 Kotlin,最后,Google 宣布了官方对 Android 平台的 Kotlin 支持。这意味着 Android 团队发布的每个工具都不仅与 Java 兼容,还与 Kotlin 兼容。Google 和 JetBrains 已经开始合作,创建一个负责未来语言维护和开发的非营利基金会。所有这些都将大大增加使用 Kotlin 的公司数量。
Kotlin 也类似于苹果的 Swift 编程语言。事实上,它们如此相似,以至于一些文章关注的是差异,而不是相似之处。学习 Kotlin 对于渴望为 Android 和 iOS 开发应用程序的开发人员将非常有帮助。还有计划将 Kotlin 移植到 iOS(Kotlin/Native),所以也许我们根本不需要学习 Swift。在 Kotlin 中也可以进行全栈开发,因此我们可以开发服务器端应用程序和共享与移动客户端相同数据模型的前端客户端。
总结
我们已经讨论了 Kotlin 语言如何适用于 Android 开发,以及我们如何将 Kotlin 纳入新项目和现有项目中。我们已经看到了 Kotlin 简化了代码并使其更安全的有用示例。还有许多有趣的事情等待我们去发现。
在下一章中,我们将学习 Kotlin 的构建模块,并奠定使用 Kotlin 开发 Android 应用程序的基础。
第二章:奠定基础
本章主要讨论了构成 Kotlin 编程语言核心元素的基本构建块。每个构建块本身可能看起来微不足道,但是当它们组合在一起时,它们会创建非常强大的语言结构。我们将讨论引入了严格的空安全和智能转换的 Kotlin 类型系统。此外,我们还将看到 JVM 世界中一些新的操作符,以及与 Java 相比的许多改进。我们还将介绍处理应用程序流程和以统一方式处理相等性的新方法。
在本章中,我们将涵盖以下主题:
-
变量、值和常量
-
类型推断
-
严格的空安全
-
智能转换
-
Kotlin 数据类型
-
控制结构
-
异常处理
变量
在 Kotlin 中,我们有两种类型的变量:var或val。第一种var是可变引用(读写),可以在初始化后更新。var关键字用于定义 Kotlin 中的变量。它相当于普通(非 final)的 Java 变量。如果我们的变量需要在某个时候更改,我们应该使用var关键字进行声明。让我们看一个变量声明的例子:
fun main(args: Array<String>) {
var fruit:String = "orange" //1
fruit = "banana" //2
}
-
创建水果变量,并用变量
orange的值进行初始化 -
重新初始化水果变量为
banana的值
第二种类型的变量是只读引用。这种类型的变量在初始化后不能被重新分配。
val关键字可以包含自定义的 getter,因此在技术上它可以在每次访问时返回不同的对象。换句话说,我们无法保证对底层对象的引用是不可变的:
val random: Int
get() = Random().nextInt()
自定义 getter 将在第四章“类和对象”中更详细地讨论。
val关键字相当于带有final修饰符的 Java 变量。使用不可变变量很有用,因为它确保变量永远不会被错误地更新。不可变性的概念对于在不担心正确的数据同步的情况下处理多个线程也是有帮助的。要声明不可变变量,我们将使用val关键字:
fun main(args: Array<String>) {
val fruit:String= "orange"//1
a = "banana" //2 Error
}
-
创建水果变量,并用字符串
orange的值进行初始化 -
编译器会抛出错误,因为水果变量已经被初始化
Kotlin 还允许我们在文件级别定义变量和函数。我们将在第三章“玩转函数”中进一步讨论。
请注意,变量引用的类型(var,val)与引用本身有关,而不是所引用对象的属性。这意味着当使用只读引用(val)时,我们将无法更改指向特定对象实例的引用(我们将无法重新分配变量值),但我们仍然可以修改所引用对象的属性。让我们使用数组来看看它的运行情况:
val list = mutableListOf("a","b","c") //1
list = mutableListOf("d", "e") //2 Error
list.remove("a") //3
-
初始化可变列表
-
编译器会抛出错误,因为值引用不能被改变(重新分配)
-
编译器将允许修改列表的内容
关键字val不能保证底层对象是不可变的。
如果我们真的想确保对象不会被修改,我们必须使用不可变引用和不可变对象。幸运的是,Kotlin 的标准库包含了任何集合接口(List与MutableList,Map与MutableMap等)的不可变等价物,对于用于创建特定集合实例的辅助函数也是如此:
| 变量/值定义 | 引用可以改变 | 对象状态可以改变 |
|---|---|---|
val = listOf(1,2,3) | 否 | 否 |
val = mutableListOf(1,2,3) | 否 | 是 |
var = listOf(1,2,3) | 是 | 否 |
- var = mutableListOf(1,2,3) | 是 | 是 |
类型推断
正如我们在之前的例子中看到的,与 Java 不同,Kotlin 类型是在变量名之后定义的:
var title: String
乍一看,这可能看起来对 Java 开发人员来说很奇怪,但这个构造是 Kotlin 一个非常重要的特性的基础,称为类型推断。类型推断意味着编译器可以从上下文(分配给变量的表达式的值)中推断类型。当变量声明和初始化一起进行(单行)时,我们可以省略类型声明。让我们看一下以下变量定义:
var title: String = "Kotlin"
title变量的类型是String,但我们真的需要隐式类型声明来确定变量类型吗?在表达式的右侧,我们有一个字符串Kotlin,我们将其赋给了左侧表达式中定义的变量title。
我们指定了变量类型为String,但这是显而易见的,因为这与分配表达式的类型(Kotlin)的类型相同。幸运的是,这个事实对于 Kotlin 编译器来说也是显而易见的,所以当声明变量时我们可以省略类型,因为编译器将尝试从当前上下文中确定变量的最佳类型:
var title = "Kotlin"
请记住,类型声明被省略,但变量的类型仍然隐式设置为String,因为 Kotlin 是一种强类型语言。这就是为什么前面两个声明是相同的,Kotlin 编译器仍然能够正确验证变量的所有未来用法。这里有一个例子:
var title = "Kotlin"
title = 12 // 1, Error
- 推断类型为
String,我们正在尝试分配Int
如果我们想要将Int(值12)赋给标题变量,那么我们需要指定标题类型为String和Int的公共类型。在类型层次结构中最接近的一个是Any:
var title: Any = "Kotlin"
title = 12
Any 是 Java 对象类型的等价物。它是 Kotlin 类型层次结构的根。Kotlin 中的所有类都明确继承自类型Any,甚至是String或Int等原始类型
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00021.jpgAny 定义了三个方法:equals,toString和hashCode。Kotlin 标准库包含了这种类型的一些扩展。我们将在第七章 扩展函数和属性中讨论扩展。
正如我们所看到的,类型推断 不仅限于原始值。让我们直接从函数中推断类型:
var total = sum(10, 20)
在前面的例子中,推断的类型将与函数返回的类型相同。我们可能猜测它将是Int,但它也可能是Double,Float或其他类型。如果从上下文中不明显推断出类型,我们可以在变量名上放置插入符号,并运行 Android Studio 的表达式类型命令(对于 Windows,是Shift + Ctrl + P,对于 macOS,是箭头键+ control + P)。这将在工具提示中显示变量类型,如下所示:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00022.gif
类型推断也适用于泛型类型:
var persons = listOf(personInstance1, personInstance2)
// Inferred type: List<Person> ()
假设我们只传递Person类的实例,推断的类型将是List<Person>。listOf方法是 Kotlin 标准库中定义的一个辅助函数,允许我们创建集合。我们将在第七章 扩展函数和属性中讨论这个主题。让我们看一些更高级的例子,使用 Kotlin 标准库类型Pair,其中包含由两个值组成的一对:
var pair = "Everest" to 8848 // Inferred type: Pair<String, Int>
在前面的例子中,使用中缀函数创建了一个pair实例,这将在第四章 类和对象中讨论,但现在我们只需要知道这两个声明返回相同类型的Pair对象:
var pair = "Everest" to 8848
// Create pair using to infix method
var pair2 = Pair("Everest", 8848)
// Create Pair using constructor
类型推断也适用于更复杂的情况,例如从推断类型中推断类型。让我们使用 Kotlin 标准库的mapOf函数,并将Pair类的to方法中缀到定义map。对中的第一项将用于推断map键类型;第二项将用于推断值类型:
var map = mapOf("Mount Everest" to 8848, "K2" to 4017)
// Inferred type: Map<String, Int>
Map<String, Int>的泛型类型是从传递给Pair构造函数的参数的类型推断出来的。我们可能会想知道,如果用于创建map的推断类型的对不同会发生什么?第一对是Pair<String, Int>,第二对是Pair<String, String>:
var map = mapOf("Mount Everest" to 8848, "K2" to "4017")
// Inferred type: Map<String, Any>
在前面的情景中,Kotlin 编译器将尝试为所有对推断出一个公共类型。两对中的第一个参数都是String(Mount Everest,K2),因此在这里自然会推断出String。每对的第二个参数不同(第一对为Int,第二对为String),因此 Kotlin 需要找到最接近的公共类型。选择Any类型,因为这是上游类型层次结构中最接近的公共类型:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00023.jpg
正如我们所看到的,类型推断在大多数情况下都做得很好,但是我们仍然可以选择显式定义数据类型,例如,我们想要不同的变量类型:
var age: Int = 18
在处理整数时,Int类型总是默认选择,但是我们仍然可以显式定义不同的类型,例如,Short,以节省一些宝贵的 Android 内存:
var age: Short = 18
另一方面,如果我们需要存储更大的值,我们可以将age变量的类型定义为Long。我们可以像以前一样使用显式类型声明,也可以使用字面常量:
var age: Long = 18 // Explicitly define variable type
var age = 18L
// Use literal constant to specify value type
这两个声明是相等的,它们都将创建类型为Long的变量。
目前,我们知道代码中有更多情况可以省略类型声明,以使代码语法更简洁。然而,有些情况下,由于上下文中缺乏信息,Kotlin 编译器将无法推断类型。例如,简单的声明而没有赋值将使类型推断变得不可能:
val title // Error
在前面的例子中,变量将在稍后初始化,因此无法确定其类型。这就是为什么类型必须明确指定的原因。一般规则是,如果编译器知道表达式的类型,则可以推断类型。否则,必须明确指定。Android Studio 中的 Kotlin 插件做得很好,因为它确切地知道类型无法推断的地方,然后会突出显示错误。这使我们能够在编写代码时立即通过 IDE 显示正确的错误消息,而无需完成应用程序。
严格的空安全
根据敏捷软件评估(* p3.snf.ch/Project-144126 )研究,缺少空检查是 Java 系统中最常见的错误模式。Java 中错误的最大来源是NullPointerExceptions。它如此之大,以至于 2009 年在一次会议上,Tony Hoare 爵士为发明空引用而道歉,称其为十亿美元的错误*(en.wikipedia.org/wiki/Tony_Hoare)。
为了避免NullPointerException,我们需要编写防御性代码,在使用对象之前检查它是否为 null。许多现代编程语言,包括 Kotlin,都采取了将运行时错误转换为编译时错误的步骤,以提高编程语言的安全性。在 Kotlin 中实现这一点的一种方法是向语言类型系统添加空安全机制。这是可能的,因为 Kotlin 类型系统区分可以保存 null(可空引用)和不可以保存 null(非空引用)的引用。Kotlin 的这一单一特性使我们能够在开发的早期阶段检测到许多与NullPointerException相关的错误。编译器和 IDE 将阻止许多NullPointerException。在许多情况下,编译将失败,而不是应用程序在运行时失败。
严格的空安全是 Kotlin 类型系统的一部分。默认情况下,常规类型不能为 null(不能存储 null 引用),除非明确允许。要存储 null 引用,我们必须将变量标记为可空(允许存储 null 引用),方法是在变量类型声明中添加问号后缀。这是一个例子:
val age: Int = null //1, Error
val name: String? = null //2
-
编译器会抛出错误,因为这种类型不允许为 null。
-
编译器将允许空赋值,因为类型使用问号后缀标记为可空。
我们不允许在可能为 null 的对象上调用方法,除非在调用之前执行了空值检查:
val name: String? = null
// ...
name.toUpperCase() // error, this reference may be null
我们将在下一节学习如何处理这个问题。Kotlin 中的每种非空类型都有其对应的可空类型:Int 对应 Int?,String 对应 String? 等等。相同的规则也适用于 Android 框架中的所有类(View 对应 View?),第三方库(OkHttpClient 对应 OkHttpClient?),以及开发人员定义的所有自定义类(MyCustomClass 对应 MyCustomClass?)。这意味着每个非泛型类都可以用来定义两种类型,可空和非空。非空类型也是其可空对应类型的子类型。例如,Vehicle 除了是 Vehicle? 的子类型外,也是 Any 的子类型:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00024.jpg
Nothing 类型是一个空类型(无法实例化的类型),它不能有实例。我们将在 第三章 Playing with Functions 中详细讨论它。这种类型层次结构是为什么我们可以将非空对象(Vehicle)赋值给可空类型的变量(Vehicle?),但不能将可空对象(Vehicle?)赋值给非空变量(Vehicle)的原因:
var nullableVehicle: Vehicle?
var vehicle: Vehicle
nullableVehicle = vehicle // 1
vehicle = nullableVehicle // 2, Error
-
可以进行赋值
-
错误,因为
nullableVehicle可能为 null
我们将在接下来的章节中讨论处理可空类型的方法。现在让我们回到类型定义。在定义 泛型类型 时,有多种定义可空性的可能性,因此让我们通过比较包含类型为 Int 的泛型 ArrayList 的不同声明来检查各种集合类型。下面是一个表格,展示了关键的区别:
| 类型声明 | 列表本身可以为 null | 元素可以为 null |
|---|---|---|
ArrayList<Int> | 否 | 否 |
ArrayList<Int>? | 是 | 否 |
ArrayList<Int?> | 否 | 是 |
ArrayList<Int?>? | 是 | 是 |
理解不同的空类型声明方式很重要,因为 Kotlin 编译器强制执行它以避免 NullPointerException。这意味着编译器在访问任何可能为 null 的引用之前强制执行空值检查。现在让我们来看看 Activity 类的 onCreate 方法中常见的 Android/Java 错误:
//Java
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
savedInstanceState.getBoolean("locked");
}
在 Java 中,这段代码将编译正常,访问 null 对象将导致应用在运行时崩溃并抛出 NullPointerException。现在让我们来看看相同方法的 Kotlin 版本:
override fun onCreate(savedInstanceState: Bundle?) { //1
super.onCreate(savedInstanceState)
savedInstanceState.getBoolean("key") //2 Error
}
-
savedInstanceState定义为可空的Bundle? -
编译器会抛出错误
savedInstanceState 类型是一个平台类型,Kotlin 可以将其解释为可空或非空。我们将在接下来的章节中讨论平台类型,但现在我们将定义 savedInstanceState 为可空类型。我们这样做是因为我们知道在创建 Activity 时会传递 null。只有在使用保存的实例状态重新创建 Activity 时才会传递 Bundle 的实例:
我们将在 第三章 Playing with Functions 中讨论函数,但现在我们已经可以看到,在 Kotlin 中声明函数的语法与 Java 非常相似。
在 Kotlin 中修复上述错误的最明显的方法是以与 Java 相同的方式检查空值:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val locked: Boolean
if(savedInstanceState != null)
locked = savedInstanceState.getBoolean("locked")
else
locked = false
}
前面的结构呈现了一些样板代码,因为在 Java 开发中进行空检查是一种非常常见的操作(特别是在 Android 框架中,其中大多数元素都是可空的)。幸运的是,Kotlin 允许使用一些更简单的解决方案来处理可空变量。第一个是安全调用运算符。
安全调用
安全调用运算符只是一个问号后跟一个点。重要的是要理解,安全转换运算符将始终返回一个值。如果运算符的左侧为 null,则它将返回 null,否则将返回右侧表达式的结果:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val locked: Boolean? = savedInstanceState?.getBoolean("locked")
}
如果savedInstanceState是null,那么将返回null,否则将返回评估savedInstanceState?.getBoolean("locked")表达式的结果。请记住,可空引用调用可能始终返回可空值,因此整个表达式的结果是可空Boolean*?*。如果我们想确保获得非空布尔值,我们可以结合安全调用运算符和Elvis运算符,在下一节中讨论。
可以将安全调用运算符的多个调用链接在一起,以避免嵌套的if表达式或像这样的复杂条件:
//Java idiomatic - multiple checks
val quiz: Quiz = Quiz()
//...
val correct: Boolean?
if(quiz.currentQuestion != null) {
if(quiz.currentQuestion.answer != null ) {
//do something
}
}
//Kotlin idiomatic - multiple calls of save call operator
val quiz: Quiz = Quiz()
//...
val correct = quiz.currentQuestion?.answer?.correct
// Inferred type Boolean?
前面的链的工作方式如下–只有在answer值不为 null 时才会访问correct,只有在currentQuestion值不为 null 时才会访问answer。结果,该表达式将返回correct属性返回的值,或者如果安全调用链中的任何对象为 null,则返回 null。
Elvis 运算符
Elvis 运算符由问号后跟冒号(?:)表示,并具有以下语法:
first operand ?: second operand
Elvis 运算符的工作方式如下:如果第一个操作数不为 null,则将返回该操作数,否则将返回第二个操作数。Elvis 运算符允许我们编写非常简洁的代码。
我们可以将 Elvis 运算符应用于我们的示例,以检索变量locked,这将始终是非空的:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val locked: Boolean = savedInstanceState?.
getBoolean("locked") ?: false
}
在前面的示例中,如果savedInstanceState不为null,Elvis 运算符将返回savedInstanceState?.getBoolean("locked")表达式的值,否则将返回 false。这样我们可以确保locked变量。借助 Elvis 运算符,我们可以定义默认值。还要注意,只有在左侧为 null 时才会评估右侧表达式。然后提供将在表达式为可空时使用的默认值。回到上一节的测验示例,我们可以轻松修改代码以始终返回非空值:
val correct = quiz.currentQuestion?.answer?.correct ?: false
结果是,该表达式将返回correct属性返回的值,或者如果安全调用链中的任何对象为 null,则返回false。这意味着该值将始终被返回,因此推断出非空布尔类型。
该运算符的名称来自著名的美国歌手兼词曲作家埃尔维斯·普雷斯利,因为他的发型类似于问号。
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00025.jpg
非空断言
处理空值的另一个工具是非空断言运算符。它由双感叹号(!!)表示。该运算符将可空变量显式转换为非空变量。以下是一个使用示例:
var y: String? = "foo"
var size: Int = y!!.length
通常,我们无法将可空属性length的值分配给非空变量size。但是,作为开发人员,我们可以向编译器保证,这个可空变量在这里将有一个值。如果我们是对的,我们的应用程序将正常工作,但如果我们错了,并且变量具有空值,应用程序将抛出NullPointerException。让我们检查一下我们的活动方法onCreate():
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val locked: Boolean = savedInstanceState!!.getBoolean("locked")
}
前面的代码将编译,但这段代码会正确工作吗?正如我们之前所说的,当恢复活动实例时,savedInstanceState将被传递给onCreate方法,因此这段代码将在没有异常的情况下工作。然而,在创建活动实例时,savedInstanceState将为空(没有以前的实例可以恢复),因此将在运行时抛出NullPointerException。这种行为类似于 Java,但主要区别在于在 Java 中,访问潜在可空对象而不进行空值检查是默认行为,而在 Kotlin 中,我们必须强制进行;否则,我们将得到编译错误。
这个运算符只有少数正确的可能应用场景,所以当你使用它或在代码中看到它时,要考虑它可能存在的危险或警告。建议很少使用非空断言,并且在大多数情况下应该用安全调用或智能转换来替代。
对抗非空断言文章提供了一些有用的例子,其中非空断言运算符被其他安全的 Kotlin 构造替换,网址为bit.ly/2xg5JXt。
实际上,在这种情况下,使用非空断言运算符是没有意义的,因为我们可以使用let以更安全的方式解决我们的问题。
让
处理可空变量的另一个工具是let。这实际上不是运算符,也不是语言的特殊构造。它是在 Kotlin 标准库中定义的一个函数。让我们看看let与安全调用运算符结合的语法:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.let{
println(it.getBoolean("isLocked")) // 1
}
}
let中的savedInstanceState可以使用变量访问
命名它。
如前所述,安全调用运算符的右侧表达式只有在左侧不为空时才会被评估。在这种情况下,右侧是一个接受另一个函数(lambda)作为参数的let函数。如果savedInstanceState不为空,则将执行let后面的代码块。我们将在第七章中学习更多关于它以及如何定义这样的函数,扩展函数和属性。
空值和 Java
我们知道 Kotlin 要求明确定义可以保存空值的引用。另一方面,Java 对空值的处理更为宽松,因此我们可能会想知道 Kotlin 如何处理来自 Java 的类型(基本上是整个 Android SDK 和用 Java 编写的库)。在可能的情况下,Kotlin 编译器将从代码中确定类型的空值,并使用空值注解将类型表示为实际的可空或非可空类型。
Kotlin 编译器支持多种空值注解,包括:
-
Android (
com.android.annotations和android.support.annotations) -
JetBrains (
@Nullable和@NotNull来自org.jetbrains.annotations包) -
JSR-305 (
Javax.annotation)
我们可以在 Kotlin 编译器源代码中找到完整的列表(github.com/JetBrains/kotlin/blob/master/core/descriptor.loader.Java/src/org/jetbrains/kotlin/load/Java/JvmAnnotationNames.kt)
我们之前在 Activity 的onCreate方法中看到过这一点,其中savedInstanceState的类型被明确设置为可空类型Bundle?:
override fun onCreate(savedInstanceState: Bundle?) {
...
}
然而,有许多情况下无法确定变量的空值。所有来自 Java 的变量都可以为空,除非标记为非可空。我们可以将它们全部视为可空,并在每次访问之前进行检查,但这是不切实际的。作为这个问题的解决方案,Kotlin 引入了平台类型的概念。这些是来自 Java 类型的类型,具有放松的空值检查,这意味着每个平台类型可能是空或非空。
虽然我们不能自己声明平台类型,但这种特殊的语法存在是因为编译器和 Android Studio 有时需要显示它们。我们可以在异常消息或方法参数列表中发现平台类型。平台类型语法只是在变量类型声明中的一个感叹号后缀:
View! // View defined as platform type
我们可以将每种平台类型视为可空的,但类型的可空性通常取决于上下文,因此有时我们可以将它们视为不可空的变量。这个伪代码展示了平台类型的可能含义:
T! = T or T?
作为开发者,我们有责任决定如何处理这种类型,是可空的还是非可空的。让我们考虑一下findViewById方法的用法:
val textView = findViewById(R.id.textView)
findViewById方法实际上会返回什么?textView变量的推断类型是什么?可空类型(TestView)还是非可空类型(TextView?)?默认情况下,Kotlin 编译器对findViewById方法返回的值的可空性一无所知。这就是为什么TextView的推断类型具有平台类型View!。
这就是我们所说的开发者责任。作为开发者,我们必须决定,因为只有我们知道布局是否在所有配置(纵向、横向等)中定义了textView,还是只在其中一些配置中定义了。如果我们在当前布局中定义了适当的视图,findViewById方法将返回对该视图的引用,否则将返回 null:
val textView = findViewById(R.id.textView) as TextView // 1
val textView = findViewById(R.id.textView) as TextView? // 2
-
假设
textView在每个配置的每个布局中都存在,因此textView可以定义为非可空的。 -
假设
textView在所有布局配置中都不存在(例如,只在横向布局中存在),则textView必须定义为可空的,否则当加载没有textView的布局时,应用程序将抛出NullPointerException。
转换
转换概念得到了许多编程语言的支持。基本上,转换是将一个特定类型的对象转换为另一种类型的方法。在 Java 中,我们需要在访问其成员之前显式地转换对象,或者将其转换并将其存储在转换后的类型的变量中。Kotlin 简化了转换的概念,并通过引入智能转换将其提升到了下一个级别。
在 Kotlin 中,我们可以执行几种类型的转换:
-
显式地将对象转换为不同类型(安全转换操作符)
-
将对象隐式地转换为不同类型或可空类型转换为非可空类型(智能转换机制)
安全/不安全转换操作符
在 Java 或 Kotlin 等强类型语言中,我们需要使用转换操作符将一个类型的值显式地转换为另一个类型。典型的转换操作是将一个特定类型的对象转换为其超类型(向上转换)、子类型(向下转换)或接口的另一个对象类型。让我们从 Java 中可以执行的转换的小提示开始:
Fragment fragment = new ProductFragment();
ProductFragment productFragment = (ProductFragment) fragment;
在上面的例子中,有一个ProductFragment的实例被分配给存储Fragment数据类型的变量。为了能够将这些数据存储到只能存储ProductFragment数据类型的productFragment变量中,我们需要执行显式转换。与 Java 不同,Kotlin 有一个特殊的as关键字,表示不安全转换操作符来处理转换:
val fragment: Fragment = ProductFragment()
val productFragment: ProductFragment = fragment as ProductFragment
ProductFragment变量是Fragment的子类型,所以上面的例子将正常工作。问题在于将不兼容的类型转换为另一种类型会抛出异常ClassCastException。这就是为什么as操作符被称为不安全的转换操作符:
val fragment : String = "ProductFragment"
val productFragment : ProductFragment = fragment as
ProductFragment
\\ Exception: ClassCastException
为了解决这个问题,我们可以使用安全转换操作符as?。有时它被称为可空转换操作符。该操作符尝试将一个值转换为指定的类型,并在无法转换值时返回 null。这是一个例子:
val fragment: String = "ProductFragment"
val productFragment: ProductFragment? = fragment as?
ProductFragment
请注意,使用安全转换操作符要求我们将name变量定义为可空的(ProductFragment?而不是ProductFragment)。作为替代,我们可以使用不安全转换操作符和可空类型ProductFragment?,这样我们就可以确切地看到我们要转换为的类型:
val fragment: String = "ProductFragment"
val productFragment: ProductFragment? = fragment as
ProductFragment?
如果我们想要一个非空的productFragment变量,那么我们必须使用 Elvis 操作符分配一个默认值:
val fragment: String = "ProductFragment"
val productFragment: ProductFragment? = fragment as?
ProductFragment ?: ProductFragment()
现在,fragment as? ProductFragment表达式将在没有任何错误的情况下进行评估。如果此表达式返回一个非空值(可以执行转换),则该值将分配给productFragment变量,否则将为productFragment变量分配一个默认值(ProductFragment的新实例)。以下是这两个操作符之间的比较:
-
不安全转换(
as):在转换不可能时抛出ClassCastException -
安全转换(
as?):在转换不可能时返回 null
现在,当我们了解了安全转换和不安全转换操作符之间的区别,我们可以安全地从片段管理器中检索片段:
var productFragment: ProductFragment? = supportFragmentManager
.findFragmentById(R.id.fragment_product) as? ProductFragment
安全转换和不安全转换操作符用于转换复杂对象。在使用原始类型时,我们可以简单地使用 Kotlin 标准库的转换方法之一。Kotlin 标准库中的大多数对象都有用于简化常见类型转换的标准方法。约定是这种类型的函数具有前缀 to,并且我们要转换为的类的名称。在此示例中,Int类型使用toString方法转换为String类型:
val name: String
val age: Int = 12
name = age.toString(); // Converts Int to String
我们将在原始数据类型部分讨论原始类型及其转换。
智能转换
智能转换将一个类型的变量转换为另一个类型,但与安全转换相反,它是隐式完成的(我们不需要使用as或as?转换操作符)。智能转换仅在 Kotlin 编译器绝对确定变量在检查后不会被更改时才有效。这使它们非常适用于多线程应用程序。通常,智能转换适用于所有不可变引用(val)和本地可变引用(var)。我们有两种智能转换:
-
类型智能转换将一个类型的对象转换为另一个类型的对象
-
将可空引用转换为非空引用的空值智能转换
类型智能转换
让我们表示上一节中的Animal和Fish类:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00026.jpg
假设我们想调用isHungry方法,并且我们想检查animal是否是Fish的实例。在 Java 中,我们必须这样做:
\\Java
if (animal instanceof Fish){
Fish fish = (Fish) animal;
fish.isHungry();
//or
((Fish) animal).isHungry();
}
这段代码的问题在于它的冗余性。我们必须检查animal实例是否为Fish,然后在此检查后明确将animal转换为Fish。如果编译器能够为我们处理这些冗余的转换,那不是很好吗?事实证明,当涉及到转换时,Kotlin 编译器确实很聪明,因此它将使用智能转换机制为我们处理所有这些冗余的转换。这是智能转换的一个例子:
if(animal is Fish) {
animal.isHungry()
}
Android Studio 中的智能转换
如果智能转换不可能,Android Studio 将显示适当的错误,因此我们将确切知道是否可以使用它。当我们访问需要转换的成员时,Android Studio 会用绿色背景标记变量。https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00027.jpg
在 Kotlin 中,我们不必显式将animal实例转换为Fish,因为在类型检查后,Kotlin 编译器将能够隐式处理转换。现在,在if块内,变量animal被转换为Fish。结果与前面的 Java 示例完全相同(Kotlin 中调用操作符的实例与 Java 中相同)。因此,我们可以安全地调用isHungry方法,而无需进行任何显式转换。请注意,在这种情况下,此智能转换的范围受到if块的限制:
if(animal is Fish) {
animal.isHungry() //1
}
animal.isHungry() //2, Error
-
在这种情况下,animal 实例是 Fish,所以我们可以调用
isHungry方法。 -
在这种情况下,animal 实例仍然是 Animal,所以我们无法调用
isHungry方法。
然而,还有其他情况,智能转换范围大于单个块,就像以下示例中一样:
val fish:Fish? = // ...
if (animal !is Fish) //1
return
animal.isHungry() //1
- 从这一点开始,animal 将被隐式转换为非空 Fish
在前面的示例中,如果animal不是Fish,整个方法将从函数返回,因此编译器知道animal在代码块的其余部分必须是Fish。Kotlin 和 Java 条件表达式是惰性求值的。
这意味着在表达式condition1() && condition2()中,只有当condition1返回true时才会调用方法condition2。这就是为什么我们可以在条件表达式的右侧使用智能转换类型:
if (animal is Fish && animal.isHungry()) {
println("Fish is hungry")
}
请注意,如果animal不是Fish,则根本不会评估条件表达式的第二部分。当它被评估时,Kotlin 知道animal是Fish(智能转换)。
非空智能转换
智能转换还处理其他情况,包括空值检查。假设我们有一个标记为可空的view变量,因为我们不知道findViewById是否会返回视图或 null:
val view: View? = findViewById(R.layout.activity_shop)
我们可以使用安全调用运算符访问view的方法和属性,但在某些情况下,我们可能希望对同一对象执行更多操作。在这些情况下,智能转换可能是更好的解决方案:
val view: View?
if ( view != null ){
view.isShown()
// view is casted to non-nullable inside if code block
}
view.isShown() // error, outside if the block view is nullable
在执行此类空检查时,编译器会自动将可空视图(View*?*)转换为非空(View)。这就是为什么我们可以在if块内调用isShown方法,而不使用安全调用运算符。在if块之外,视图仍然是可空的。
每个智能转换只适用于只读变量,因为读写变量可能在执行检查和访问变量之间发生变化。
智能转换也适用于函数的return语句。如果我们在函数内部执行空值检查并使用 return 语句,那么变量也将被转换为非空:
fun setView(view: View?){
if (view == null)
return
//view is casted to non-nullable
view.isShown()
}
在这种情况下,Kotlin 绝对确定变量值不会为空,因为否则函数将调用return。函数将在第三章Playing with Functions中更详细地讨论。我们可以使用 Elvis 运算符使前面的语法更简单,并在一行中执行空值检查:
fun verifyView(view: View?){
view ?: return
//view is casted to non-nullable
view.isShown()
//..
}
而不仅仅是从函数返回,我们可能希望更明确地说明现有问题并抛出异常。然后我们可以与错误抛出一起使用 Elvis 运算符:
fun setView(view: View?){
view ?: throw RuntimeException("View is empty")
//view is casted to non-nullable
view.isShown()
}
正如我们所看到的,智能转换是一种非常强大的机制,可以减少空值检查的次数。这就是为什么它被 Kotlin 大量利用的原因。记住一般规则——智能转换只有在 Kotlin 绝对确定变量即使在另一个线程中也不会在转换后改变时才有效。
原始数据类型
在 Kotlin 中,一切都是对象(引用类型,而不是原始类型)。我们找不到像 Java 中可以使用的原始类型。这减少了代码复杂性。我们可以在任何变量上调用方法和属性。例如,这是我们如何将Int变量转换为Char:
var code: Int = 75
code.toChar()
通常(只要可能),底层类型如Int、Long或Char都经过优化(存储为原始类型),但我们仍然可以像在任何其他对象上一样调用它们的方法。
默认情况下,Java 平台将数字存储为 JVM 原始类型,但当需要可空数字引用(例如Int?)或涉及泛型时,Java 使用装箱表示。装箱意味着将原始类型包装成相应的装箱原始类型。这意味着实例的行为就像一个对象。Java 原始类型的装箱表示的示例是int 与 Integer或long 与 Long。由于 Kotlin 编译为 JVM 字节码,这里也是如此:
var weight: Int = 12 // 1
var weight: Int? = null // 2
-
值以原始类型存储
-
值以装箱整数(复合类型)存储
这意味着每次我们创建一个数字(Byte,Short,Int,Long,Double,Float),或者使用Char,Boolean,它将被存储为原始类型,除非我们将其声明为可空类型(Byte?,Char?,Array?等);否则,它将被存储为装箱表示:
var a: Int = 1 // 1
var b: Int? = null // 2
b = 12 // 3
-
a是非空的,因此它以原始类型存储 -
b为空,因此它被存储为装箱表示 -
b仍然以装箱表示存储,尽管它有一个值
无法使用原始类型对泛型类型进行参数化,因此将执行装箱。重要的是要记住,使用装箱表示(复合类型)而不是主要表示可能会导致性能损失,因为它将始终创建与原始类型表示相比的内存开销。对于包含大量元素的列表和数组,这可能是显而易见的,因此对于应用程序性能来说,使用主要表示可能至关重要。另一方面,当涉及单个变量甚至多个变量声明时,我们不必担心表示的类型,即使在内存有限的 Android 世界中也是如此。
现在让我们讨论最重要的 Kotlin 原始数据类型:数字、字符、布尔值和数组。
数字
用于数字的基本 Kotlin 数据类型等效于 Java 数值原语:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00028.jpg
然而,Kotlin 处理数字的方式与 Java 有些不同。第一个区别是数字没有隐式转换–较小的类型不会隐式转换为较大的类型:
var weight : Int = 12
var truckWeight: Long = weight // Error1
这意味着我们不能将Int类型的值分配给Long变量而不进行显式转换。正如我们所说,在 Kotlin 中,一切都是对象,所以我们可以调用方法并显式将Int类型转换为Long来解决问题:
var weight:I nt = 12
var truckWeight: Long = weight.toLong()
起初,这可能看起来像样板代码,但实际上这将使我们避免许多与数字转换相关的错误,并节省大量调试时间。这实际上是 Kotlin 语法比 Java 更多的代码量的一个罕见例子。Kotlin 标准库支持以下数字转换方法:
-
toByte():Byte -
toShort():Short -
toInt():Int -
toLong():Long -
toFloat():Float -
toDouble():Double -
toChar():Char
然而,我们可以显式指定数字文字以更改推断的变量类型:
val a: Int = 1
val b = a + 1 // Inferred type is Int
val b = a + 1L // Inferred type is Long
Kotlin 和 Java 数字之间的第二个区别是,某些情况下数字文字略有不同。对于整数值,有以下类型的文字常量:
27 // Decimals by default
27L // Longs are tagged by a upper case L suffix
0x1B // Hexadecimals are tagged by 0x prefix
0b11011 // Binaries are tagged by 0b prefix
不支持八进制文字。Kotlin 还支持浮点数的传统表示法:
27.5 // Inferred type is Double
27.5F // Inferred type is Float. Float are tagged by f or F
Char
Kotlin 中的字符存储在类型Char中。在许多方面,字符类似于字符串,因此我们将集中讨论相似之处和不同之处。要定义Char,我们必须使用单引号,与String相反,我们在其中使用双引号:
val char = 'a' \\ 1
val string = "a" \\ 2
-
定义类型为
Char的变量 -
定义类型为
String的变量
在字符和字符串中,可以使用反斜杠来转义特殊字符。支持以下转义序列:
-
\t:制表符 -
\b:退格 -
\n:换行 -
\r:换行 -
\':引号 -
\":双引号 -
\\:斜杠 -
\$:美元符号 -
\u:Unicode 转义序列
让我们定义包含阴阳 Unicode 字符(U+262F)的Char:
var yinYang = '\u262F'
数组
在 Kotlin 中,数组由Array类表示。要在 Kotlin 中创建数组,我们可以使用许多 Kotlin 标准库函数。最简单的是arrayOf():
val array = arrayOf(1,2,3) // inferred type Array<Int>
默认情况下,此函数将创建一个装箱Int的数组。如果我们想要一个包含Short或Long的数组,那么我们必须明确指定数组类型:
val array2: Array<Short> = arrayOf(1,2,3)
val array3: Array<Long> = arrayOf(1,2,3)
如前所述,使用装箱表示可能会降低应用程序的性能。这就是为什么 Kotlin 有一些专门表示原始类型数组的类,以减少装箱内存开销:ShortArray,IntArray,LongArray等。这些类与Array类没有继承关系,尽管它们具有相同的一组方法和属性。要创建这些类的实例,我们必须使用相应的工厂函数:
val array = shortArrayOf(1, 2, 3)
val array = intArrayOf(1, 2, 3)
val array = longArrayOf(1, 2, 3)
重要的是要注意并牢记这个微妙的差别,因为这些方法看起来相似,但创建了不同的类型表示:
val array = arrayOf(1,2,3) // 1
val array = longArrayOf(1, 2, 3) // 2
-
泛型数组的装箱长元素(推断类型:
Array<Long>) -
包含原始长元素的数组(推断类型:
LongArray)
知道数组的确切大小通常会提高性能,因此 Kotlin 还有另一个库函数arrayOfNulls,它创建一个给定大小的数组,其中填充了空元素:
val array = arrayOfNulls(3) // Prints: [null, null, null]
println(array) // Prints: [null, null, null]
我们还可以使用将数组大小作为第一个参数并且可以返回每个数组元素的初始值的 lambda 作为第二个参数的工厂函数来填充预定义大小的数组:
val array = Array (5) { it * 2 }
println(array) // Prints: [0, 2, 4, 8, 10]
我们将在第五章,函数作为一等公民中更详细地讨论 lambda(匿名函数)。在 Kotlin 中访问数组元素的方式与 Java 相同:
val array = arrayOf(1,2,3)
println(array[1]) //Prints: 2
元素的索引方式也与 Java 相同,意味着第一个元素的索引为 0,第二个元素的索引为 1,依此类推。并非所有的东西都一样,也存在一些差异。主要的一个是,Kotlin 中的数组与 Java 不同,Kotlin 中的数组是不变的。我们将在第六章,泛型是你的朋友中讨论变异。
布尔类型
布尔是一个逻辑类型,有两个可能的值:true和false。我们还可以使用可空布尔类型:
val isGrowing: Boolean = true
val isGrowing: Boolean? = null
布尔类型还支持通常在大多数现代编程语言中可用的标准内置操作:
-
||:逻辑或。当两个谓词中任何一个返回true时返回true。 -
&&:逻辑与。当两个谓词都返回true时返回true。 -
!:否定运算符。对于false返回true,对于true返回false。
请记住,我们只能对任何类型的条件使用非空布尔值。
与 Java 一样,在||和&&中,谓词是惰性评估的,只有在需要时才会评估(惰性连接)。
复合数据类型
让我们讨论内置到 Kotlin 中的更复杂的类型。一些数据类型与 Java 相比有重大改进,而另一些则是全新的。
字符串
Kotlin 中的字符串的行为方式与 Java 类似,但它们有一些很好的改进。
要开始访问指定索引处的字符,我们可以使用索引运算符,并以访问数组元素的方式访问字符:
val str = "abcd"
println (str[1]) // Prints: b
我们还可以访问 Kotlin 标准库中定义的各种扩展,这些扩展使得处理字符串更加容易:
val str = "abcd"
println(str.reversed()) // Prints: dcba
println(str.takeLast(2)) // Prints: cd
println("john@test.com".substringBefore("@")) // Prints: john
println("john@test.com".startsWith("@")) // Prints: false
这正是 Java 中的String类,因此这些方法不是String类的一部分。它们被定义为扩展。我们将在第七章,扩展函数和属性中学习更多关于扩展的知识。
查看String类文档以获取方法的完整列表(kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/)。
字符串模板
构建字符串是一个简单的过程,但在 Java 中通常需要长的连接表达式。让我们直接跳到一个例子。这是在 Java 中实现的由多个元素构建的字符串:
\\Java
String name = "Eva";
int age = 27;
String message = "My name is" + name + "and I am" + age + "years old";
在 Kotlin 中,我们可以通过使用字符串模板大大简化字符串创建的过程。我们可以简单地使用一个美元符号将变量放在字符串中,而不是使用连接。在插值期间,字符串占位符将被实际值替换。这里有一个例子:
val name = "Eva"
val age = 27
val message = "My name is $name and I am $age years old"
println(message)
//Prints: My name is Eva and I am 27 years old
这与连接一样高效,因为在底层编译代码创建了一个StringBuilder并将所有部分附加在一起。字符串模板不仅限于单个变量。它们还可以在${和}字符之间包含整个表达式。它可以是一个函数调用,将返回值或属性访问,如下面的片段所示:
val name = "Eva"
val message = "My name has ${name.length} characters"
println(message) //Prints: My name has 3 characters
这种语法允许我们创建更清晰的代码,而无需每次需要从变量或表达式中获取值来构造字符串时都打破字符串。
范围
范围是定义一系列值的一种方式。它由序列中的第一个和最后一个值表示。我们可以使用范围来存储重量、温度、时间和年龄。范围使用双点符号(在底层,范围使用rangeTo运算符)来定义:
val intRange = 1..4 // 1
val charRange= 'b'..'g' // 2
-
推断类型是
IntRange(相当于i >= 1 && i <= 4) -
推断类型是
CharRange(相当于从'b'到'g'的字母)
请注意,我们使用单引号来定义字符范围。
Int,Long和Char类型的范围可用于在for... each循环中迭代下一个值:
for (i in 1..5) print(i) // Prints: 1234
for (i in 'b'..'g') print(i) // Prints: bcdefg
范围可以用来检查一个值是否大于起始值并且小于结束值:
val weight = 52
val healthy = 50..75
if (weight in healthy)
println("$weight is in $healthy range")
//Prints: 52 is in 50..75 range
它也可以用于其他类型的范围,比如CharRange:
val c = 'k' // Inferred type is Char
val alphabet = 'a'..'z'
if(c in alphabet)
println("$c is character") //Prints: k is a character
在 Kotlin 中,范围是闭合的(包括结束)。这意味着范围结束值包括在范围内:
for (i in 1..1) print(i) // Prints: 123
注意,Kotlin 中的范围默认是递增的(默认情况下步长为 1):
for (i in 5..1) print(i) // Prints nothing
要以相反的顺序迭代,我们必须使用downTo函数,将步长设置为-1。就像这个例子:
for (i in 5 downTo 1) print(i) // Prints: 54321
我们也可以设置不同的步骤:
for (i in 3..6 step 2) print(i) // Prints: 35
请注意,在3..6范围内,最后一个元素没有被打印出来。这是因为步进索引在每个循环迭代中移动两步。因此,在第一次迭代中,它的值为3,在第二次迭代中,它的值为5,最后,在第三次迭代中,值将为7,因此它被忽略,因为它在范围之外。
由step函数定义的步骤必须是正的。如果我们想定义一个负步骤,那么我们应该使用downTo函数和step函数一起使用:
for (i in 9 downTo 1 step 3) print(i) // Prints: 963
集合
编程的一个非常重要的方面是使用集合。Kotlin 提供了多种类型的集合,并与 Java 相比有许多改进。我们将在第七章中讨论这个主题,扩展函数和属性。
语句与表达式
Kotlin 比 Java 更广泛地使用表达式,因此了解语句和表达式之间的区别很重要。程序基本上是一系列语句和表达式。表达式产生一个值,可以作为另一个表达式、变量赋值或函数参数的一部分使用。表达式是一个或多个操作数(被操作的数据)和零个或多个操作符(表示特定操作的标记)的序列,可以评估为单个值。
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00029.jpg
让我们回顾一些来自 Kotlin 的表达式的例子:
| 表达式(产生一个值) | 分配的值 | 类型的表达式 |
|---|---|---|
a = true | true | 布尔值 |
a = "foo" + "bar" | "foobar" | 字符串 |
a = min(2, 3) | 2 | 整数 |
a = computePosition().getX() | getX方法返回的值 | 整数 |
另一方面,语句执行一个动作,不能赋值给变量,因为它们根本没有值。语句可以包含用于定义类(class),接口(interface),变量(val,var),函数(fun),循环逻辑(break,continue)等的语言关键字。当表达式返回的值被忽略时(不将值赋给变量,不从函数中返回它,不将其用作其他表达式的一部分等),表达式也可以被视为语句。
Kotlin 是一种面向表达式的语言。这意味着在 Kotlin 中,许多在 Java 中是语句的构造在 Kotlin 中被视为表达式。第一个主要的区别是 Java 和 Kotlin 对待控制结构的方式不同。在 Java 中,它们被视为语句,而在 Kotlin 中,所有控制结构都被视为表达式,除了循环。这意味着在 Kotlin 中,我们可以使用控制结构编写非常简洁的语法。我们将在接下来的章节中看到例子。
控制流
Kotlin 有许多来自 Java 的控制流元素,但它们提供了更灵活的使用方式,在某些情况下,它们的使用更简化了。Kotlin 引入了一个称为when的新控制流构造,作为 Java switch... case的替代品。
if 语句
在本质上,Kotlin 的if子句的工作方式与 Java 相同:
val x = 5
if(x > 10){
println("greater")
} else {
println("smaller")
}
带有块体的版本如果块包含单个语句或表达式也是正确的:
val x = 5
if(x > 10)
println("greater")
else
println("smaller")
然而,Java 将if视为语句,而 Kotlin 将if视为表达式。这是主要区别,这个事实使我们能够使用更简洁的语法。例如,我们可以直接将if表达式的结果作为函数参数传递:
println(if(x > 10) "greater" else "smaller")
我们可以将我们的代码压缩成一行,因为if表达式的结果(String 类型)被评估,然后传递给println方法。当条件x > 10为true时,这个表达式将返回第一个分支(greater),否则这个表达式将返回第二个分支(smaller)。让我们看另一个例子:
val hour = 10
val greeting: String
if (hour < 18) {
greeting = "Good day"
} else {
greeting = "Good evening"
}
在前面的例子中,我们将if用作语句。但是我们知道,在 Kotlin 中,if是一个表达式,表达式的结果可以赋值给一个变量。这样我们可以直接将if表达式的结果赋值给一个 greeting 变量:
val greeting = if (hour < 18) "Good day" else "Good evening"
但有时需要在if语句的分支中放置一些其他代码。我们仍然可以将if用作表达式。然后匹配if分支的最后一行将作为结果返回:
val hour = 10
val greeting = if (hour < 18) {
//some code
"Good day"
} else {
//some code
"Good evening"
}
println(greeting) // Prints: "Good day"
如果我们将if视为表达式而不是语句,则表达式需要有一个else分支。Kotlin 版本甚至比 Java 更好。由于greeting变量被定义为非空,编译器将验证整个if表达式,并检查所有情况是否都有分支条件。由于if是一个表达式*,我们可以在字符串模板*中使用它:
val age = 18
val message = "You are ${ if (age < 18) "young" else "of age" } person"
println(message) // Prints: You are of age person
将if视为表达式给我们带来了以前在 Java 世界中无法实现的广泛可能性。
when 表达式
Kotlin 中的when表达式是一种多路分支语句。when表达式被设计为 Java switch... case语句的更强大的替代品。when语句通常提供了比大量的if... else if语句更好的替代方案,但它提供了更简洁的语法。让我们看一个例子:
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> println("x is neither 1 nor 2")
}
when表达式将其参数与所有分支一一匹配,直到满足某个分支的条件。这种行为类似于 Java 的switch... case,但我们不必在每个分支后写冗余的break语句。
与 if 子句类似,我们可以将 when 用作语句,忽略返回值,也可以将其用作表达式,并将其值赋给变量。如果 when 用作表达式,则满足分支的最后一行的值成为整体表达式的值。如果它用作语句,则值将被简单地忽略。通常情况下,如果没有一个前面的分支满足条件,else 分支将被评估:
val vehicle = "Bike"
val message= when (vehicle) {
"Car" -> {
// Some code
"Four wheels"
}
"Bike" -> {
// Some code
"Two wheels"
}
else -> {
//some code
"Unknown number of wheels"
}
}
println(message) //Prints: Two wheels
每当一个分支有多于一条指令时,我们必须将其放在由两个大括号 {...} 定义的代码块内。如果 when 被视为表达式(when 的评估结果被赋值给变量),每个块的最后一行被视为返回值。我们已经看到了 if 表达式的相同行为,所以现在我们可能已经意识到这是 Kotlin 许多构造的共同行为,包括将在本书中进一步讨论的 lambda。
如果 when 用作表达式,else 分支是强制的,除非编译器可以证明分支条件覆盖了所有可能的情况。我们还可以使用逗号将多个匹配参数放在单个分支中处理:
val vehicle = "Car"
when (vehicle) {
"Car", "Bike" -> print("Vehicle")
else -> print("Unidentified funny object")
}
when 的另一个好特性是检查变量类型的能力。我们可以轻松验证值是否是特定类型的 is 或 !is。智能转换再次变得方便,因为我们可以在分支块中访问匹配类型的方法和属性,而无需进行任何额外的检查:
val name = when (person) {
is String -> person.toUpperCase()
is User -> person.name
//Code is smart casted to String, so we can
//call String class methods
//...
}
类似地,我们可以检查范围或集合是否包含特定值。这次我们将使用 is 和 !is 关键字:
val riskAssessment = 47
val risk = when (riskAssessment) {
in 1..20 -> "negligible risk"
!in 21..40 -> "minor risk"
!in 41..60 -> "major risk"
else -> "undefined risk"
}
println(risk) // Prints: major risk
实际上,我们可以在 when 分支的右侧放置任何类型的表达式。它可以是方法调用或任何其他表达式。考虑以下示例,其中第二个 when 表达式用于 else 语句:
val riskAssessment = 80
val handleStrategy = "Warn"
val risk = when (riskAssessment) {
in 1..20 -> print("negligible risk")
!in 21..40 -> print("minor risk")
!in 41..60 -> print("major risk")
else -> when (handleStrategy){
"Warn" -> "Risk assessment warning"
"Ignore" -> "Risk ignored"
else -> "Unknown risk!"
}
}
println(risk) // Prints: Risk assessment warning
正如我们所看到的,when 是一个非常强大的构造,比 Java 的 switch 更具控制力,但它更强大的原因在于它不仅仅局限于检查相等的值。在某种程度上,它甚至可以用作 if... else if 链的替代。如果没有向 when 表达式提供参数,分支条件会表现为布尔表达式,当条件为 true 时执行分支:
private fun getPasswordErrorId(password: String) = when {
password.isEmpty() -> R.string.error_field_required
passwordInvalid(password) -> R.string.error_invalid_password
else -> null
}
所有呈现的示例都需要一个 else 分支。每当所有可能的情况都被覆盖时,我们可以省略 else 分支(穷尽 when)。让我们看一个布尔值的最简单的例子:
val large:Boolean = true
when(large){
true -> println("Big")
false -> println("Big")
}
编译器可以验证所有可能的值都已处理,因此不需要指定 else 分支。相同的逻辑适用于将在第四章中讨论的枚举和密封类,即类和对象。
Kotlin 编译器执行检查,因此我们可以确定不会漏掉任何情况。这减少了在 switch 语句内部开发人员忘记处理所有情况的常见 Java bug 的可能性(尽管多态通常是更好的解决方案)。
循环
循环是一种控制结构,重复相同的指令集,直到满足终止条件。在 Kotlin 中,循环可以迭代任何提供迭代器的内容。迭代器是一个具有两个方法的接口:hasNext 和 next。它知道如何迭代集合、范围、字符串或任何可以表示为元素序列的实体。
要迭代某些内容,我们必须提供一个 iterator() 方法。由于 String 没有这个方法,因此在 Kotlin 中它被定义为一个扩展函数。扩展将在第七章中介绍,即扩展函数和属性。
Kotlin 提供了三种类型的循环:for、while 和 do... while。它们都与其他编程语言中的循环相同,因此我们将简要讨论它们。
for 循环
在 Kotlin 中不存在经典的 Java for 循环,需要显式定义迭代器。以下是 Java 中这种类型循环的示例:
//Java
String str = "Foo Bar";
for(int i=0; i<str.length(); i++)
System.out.println(str.charAt(i));
要从头到尾迭代项目集合,我们可以简单地使用for循环:
var array = arrayOf(1, 2, 3)
for (item in array) {
print(item)
}
它也可以在没有块体的情况下定义:
for (item in array)
print(item)
如果collection是一个泛型集合,那么item将被智能转换为与泛型集合类型相对应的类型。换句话说,如果集合包含Int类型的元素,那么 item 将被智能转换为Int:
var array = arrayOf(1, 2, 3)
for (item in array)
print(item) // item is Int
我们还可以通过索引遍历集合:
for (i in array.indices)
print(array[i])
array.indices参数返回带有所有索引的IntRange。它相当于(1.. array.length - 1 )。还有一个替代的withIndex库方法,它返回一个包含索引和值的IndexedValue属性的列表。可以这样解构这些元素:
for ((index, value) in array.withIndex()) {
println("Element at $index is $value")
}
(index, value)结构被称为破坏性声明,我们将在第四章,类和对象中讨论它。
while 循环
while循环重复一个块,当其条件表达式返回true时:
while (condition) {
//code
}
还有一个do... while循环,它会重复块,只要条件表达式返回true:
do {
//code
} while (condition)
与 Java 相反,Kotlin 可以使用在do... while循环内声明的变量作为条件。
do {
var found = false
//..
} while (found)
while和do... while循环之间的主要区别在于条件表达式的评估时间。while循环在代码执行之前检查条件,如果条件不为真,则代码不会被执行。另一方面,do... while循环首先执行循环体,然后评估条件表达式,因此循环体至少会执行一次。如果这个表达式为true,循环将重复。否则,循环终止。
其他迭代
还有其他使用内置标准库函数进行集合迭代的方法,比如forEach。我们将在第七章,扩展函数和属性中介绍它们。
Break and continue
Kotlin 中的所有循环都支持经典的break和continue语句。continue语句继续执行该循环的下一次迭代,而break停止最内部封闭循环的执行:
val range = 1..6
for(i in range) {
print("$i ")
}
// prints: 1 2 3 4 5 6
现在让我们添加一个条件,并在这个条件为true时中断迭代:
val range = 1..6
for(i in range) {
print("$i ")
if (i == 3)
break
}
// prints: 1 2 3
break和continue语句在处理嵌套循环时特别有用。它们可以简化我们的控制流程,并显著减少执行的工作量,以节省宝贵的 Android 资源。让我们执行一个嵌套迭代并中断外部循环:
val intRange = 1..6
val charRange = 'A'..'B'
for(value in intRange) {
if(value == 3)
break
println("Outer loop: $value ")
for (char in charRange) {
println("\tInner loop: $char ")
}
}
// prints
Outer loop: 1
Inner loop: A
Inner loop: B
Outer loop: 2
Inner loop: A
Inner loop: B
我们使用break语句在第三次迭代开始时终止外部循环,因此嵌套循环也被终止。请注意在控制台上添加缩进的\t转义序列的使用。我们还可以利用continue语句跳过循环的当前迭代:
val intRange = 1..5
for(value in intRange) {
if(value == 3)
continue
println("Outer loop: $value ")
for (char in charRange) {
println("\tInner loop: $char ")
}
}
// prints
Outer loop: 1
Inner loop: A
Inner loop: B
Outer loop: 2
Inner loop: A
Inner loop: B
Outer loop: 4
Inner loop: A
Inner loop: B
Outer loop: 5
Inner loop: A
Inner loop: B
当当前值等于3时,我们跳过外部循环的迭代。
continue和break语句都在封闭循环上执行相应的操作。然而,有时我们希望从一个循环中终止或跳过另一个循环的迭代;例如,从内部循环中终止外部循环的迭代:
for(value in intRange) {
for (char in charRange) {
// How can we break outer loop here?
}
}
幸运的是,continue语句和break语句都有两种形式–有标签和无标签。我们已经看到了无标签,现在我们需要有标签来解决我们的问题。下面是一个使用有标签的 break 的例子:
val charRange = 'A'..'B'
val intRange = 1..6
outer@ for(value in intRange) {
println("Outer loop: $value ")
for (char in charRange) {
if(char == 'B')
break@outer
println("\tInner loop: $char ")
}
}
// prints
Outer loop: 1
Inner loop: A
@outer是标签名称。按照惯例,标签名称始终以*@*开头,后跟标签名称。标签放置在循环之前。给循环加标签允许我们使用有资格的break(break@outer),这是一种停止引用该标签的循环的执行的方法。前面的有资格的break(带标签的 break)跳转到标记有该标签的循环之后的执行点。
放置return语句将中断所有循环并从匿名或命名函数中返回:
fun doSth() {
val charRange = 'A'..'B'
val intRange = 1..6
for(value in intRange) {
println("Outer loop: $value ")
for (char in charRange) {
println("\tInner loop: $char ")
return
}
}
}
//usage
println("Before method call")
doSth()
println("After method call")
// prints
Outer loop: 1
Inner loop: A
方法调用之后:
Outer loop: 1
Inner loop: A
异常
大多数 Java 编程指南,包括《Effective Java》这本书,都提倡有效性检查的概念。这意味着我们应该始终验证参数或对象的状态,并在有效性检查失败时抛出异常。Java 异常系统有两种异常:已检查异常和未检查异常。
未检查异常意味着开发人员不必使用try... catch块来捕获异常。默认情况下,异常会一直传递到调用堆栈的最上层,因此我们可以决定在哪里捕获它们。如果我们忘记捕获它们,它们将一直传递到调用堆栈的最上层,并以适当的消息停止线程执行(因此它们会提醒我们):
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00030.jpg
Java 有一个非常强大的异常系统,在许多情况下,它迫使开发人员明确标记可能引发异常的每个函数,并通过将它们包围在try... catch块中明确捕获每个异常(已检查异常)。这对于非常小的项目效果很好,但在真正的大型应用程序中,这往往会导致冗长的代码:
// Java
try {
doSomething()
} catch (IOException e) {
// Must be safe
}
不是将异常传递到调用堆栈中,而是通过提供一个空的 catch 块来忽略它,因此它不会被正确处理,而是会消失。这种代码可能掩盖关键异常,并给人一种错误的安全感,导致意外问题和难以找到的错误。
在讨论 Kotlin 中如何处理异常之前,让我们比较一下两种类型的异常:
| 代码 | 已检查异常 | 未检查异常 |
|---|---|---|
| 函数声明 | 我们必须指定函数可能抛出的异常。 | 函数声明不包含所有抛出异常的信息。 |
| 异常处理 | 抛出异常的函数必须被try... catch块包围。 | 我们可以捕获异常并在需要时执行某些操作,但我们不是被迫这样做。异常在调用堆栈中上升。 |
Kotlin 和 Java 异常系统之间最大的区别在于,在 Kotlin 中所有异常都是未检查的。这意味着即使这是一个可能引发已捕获异常的 Java 方法,我们也永远不必用try... catch块包围一个方法。我们仍然可以这样做,但我们不是被迫这样做:
fun foo() {
throw IOException()
}
fun bar() {
foo () //no need to surround method with try-catch block
}
这种方法消除了代码冗余,并提高了安全性,因为我们不需要引入空的catch块。
try... catch块
Kotlin 的try... catch块相当于 Java 的try... catch块。让我们看一个快速的例子:
fun sendFormData(user: User?, data: Data?) { // 1
user ?: throw NullPointerException("User cannot be null")
// 2
data ?: throw NullPointerException("Data cannot be null")
//do something
}
fun onSendDataClicked() {
try { // 3
sendFormData(user, data)
} catch (e: AssertionError) { // 4
// handle error
} finally { // 5
// optional finally block
}
}
-
异常没有像 Java 那样在函数签名上指定。
-
我们检查数据的有效性,并抛出
NullPointerException(请注意,在创建对象实例时不需要使用 new 关键字)。 -
try... catch块是 Java 中的类似结构。 -
仅处理特定的异常(
AssertionError异常)。 -
finally块总是被执行。
可能有零个或多个catch块,finally块可以省略。但是,至少应该有一个catch或finally块存在。
在 Kotlin 中,异常处理try是一个表达式,因此它可以返回一个值,我们可以将其值分配给一个变量。实际分配的值是执行块的最后一个表达式。让我们检查设备上是否安装了特定的 Android 应用程序:
val result = try { // 1
context.packageManager.getPackageInfo("com.text.app", 0) //2
true
} catch (ex: PackageManager.NameNotFoundException) { // 3
false
}
-
try... catch块返回的值是由单个表达式函数返回的值。 -
如果应用程序已安装,
getPackageInfo方法将返回一个值(此值被忽略),并且将执行包含true表达式的下一行。这是try块执行的最后一个操作,因此它的值将被分配给一个变量(true)。
如果应用程序未安装,getPackageInfo将抛出PackageManager.NameNotFoundException,并且将执行catch块的最后一行,其中包含一个false表达式,因此它的值将被分配给一个变量。
编译时常量
由于val变量是只读的,在大多数情况下我们可以将其视为常量。我们需要意识到它的初始化可能会延迟,这意味着有些情况下val变量可能在编译时未被初始化,例如,将方法调用的结果赋给一个值:
val fruit:String = getName()
这个值将在运行时被赋值。然而,有些情况下我们需要在编译时知道这个值。当我们想要向注解传递参数时,需要确切的值。注解是由注解处理器处理的,它在应用程序启动之前就运行了:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00031.jpg
为了确保值在编译时是已知的(因此可以被注解处理器处理),我们需要用const修饰符标记它。让我们定义一个自定义注解MyLogger,并用一个参数定义最大日志条目,并用它注解一个Test类:
const val MAX_LOG_ENTRIES = 100
@MyLogger(MAX_LOG_ENTRIES )
// value available at compile time
class Test {}
关于使用const有一些限制我们必须意识到。第一个限制是它必须用原始类型或String类型的值进行初始化。第二个限制是它必须在顶层声明或作为对象的成员声明。我们将在第四章,类和对象中讨论对象。第三个限制是它们不能有自定义的 getter。
代表
Kotlin 为委托提供了一流的支持。与 Java 相比,这是非常有用的改进。事实上,在 Android 开发中有许多委托的应用,因此我们决定在这个主题上多花一个章节(第八章,代表)。
总结
在本章中,我们讨论了变量、值和常量之间的区别,并讨论了基本的 Kotlin 数据类型,包括范围。我们还研究了一个强制严格的空安全的 Kotlin 类型系统,以及使用各种操作符和智能转换处理可空引用的方法。我们知道,通过利用类型推断和 Kotlin 中被视为表达式的各种控制结构,我们可以编写更简洁的代码。最后,我们讨论了异常处理的方法。
在下一章中,我们将学习关于函数,并介绍不同的定义方式。我们将涵盖单表达式函数、默认参数和命名参数语法等概念,并讨论各种修饰符。
第三章:玩转函数
在之前的章节中,我们已经看到了 Kotlin 变量、类型系统和控制结构。但是要创建应用程序,我们需要允许我们构建结构的构建块。在 Java 中,类是代码的构建块。另一方面,Kotlin 支持函数式编程;因此,它可以创建整个程序或库而不需要任何类。函数是 Kotlin 中最基本的构建块。本章介绍了 Kotlin 中的函数,以及不同的函数特性和类型。
在本章中,我们将涵盖以下主题:
-
Kotlin 中的基本函数用法
-
Unit返回类型 -
可变参数
-
单表达式函数
-
尾递归函数
-
默认参数值
-
命名参数语法
-
顶层函数
-
局部函数
-
Nothing返回类型
Kotlin 中的基本函数声明和用法
最常见的程序员编写的第一个程序是用来测试某种编程语言的Hello, World!程序。它是一个完整的程序,只是在控制台上显示Hello, World!文本。我们也将从这个程序开始,因为在 Kotlin 中,它基于一个函数,只有一个函数(不需要类)。因此,Kotlin 的Hello, World!程序如下所示:
// SomeFile.kt
fun main(args: Array<String>) { // 1
println("Hello, World!") // 2, Prints: Hello, World!
}
-
函数定义了单个参数 args,其中包含用于运行程序的所有参数的数组(从命令行)。它被定义为非空,因为在没有任何参数的情况下启动程序时,会将空数组传递给方法。
-
println函数是 Kotlin 标准库中定义的 Kotlin 函数,相当于 Java 函数System.out.println。
这个程序告诉我们很多关于 Kotlin。它展示了函数的外观以及我们可以定义没有任何类的函数。首先,让我们分析函数的结构。它以fun关键字开头,然后是函数的名称,括号中的参数,以及函数体。这是另一个简单函数的示例,但这个函数返回一个值:
fun double(i: Int): Int {
return 2 * i
}
好知识框架
关于方法和函数之间的区别存在很多混淆。常见的定义如下:
函数是按名称调用的一段代码。
方法是与类(对象)实例相关联的函数。有时它被称为成员函数。
因此,简单来说,类内部的函数称为方法。在 Java 中,官方只有方法,但学术环境经常争论静态 Java 方法实际上是函数。在 Kotlin 中,我们可以定义不与任何对象相关联的函数。
调用函数的语法在 Kotlin 中与 Java 以及大多数现代编程语言中相同:
val a = double(5)
我们调用 double 函数并将其返回的值赋给一个变量。让我们讨论 Kotlin 函数的参数和返回类型的细节。
参数
在 Kotlin 函数中,参数使用 Pascal 表示法声明,每个参数的类型必须明确指定。所有参数都被定义为只读变量。无法使参数可变,因为这种行为容易出错,在 Java 中程序员经常滥用。如果有这样的需要,那么我们可以通过声明具有相同名称的局部变量来显式遮蔽参数:
fun findDuplicates(list: List<Int>): Set<Int> {
var list = list.sorted()
//...
}
这是可能的,但被视为不良实践,因此会显示警告。更好的方法是根据它们提供的数据来命名参数,根据它们提供的目的来命名变量。在大多数情况下,这些名称应该是不同的。
参数与参数 在编程社区中,参数和参数经常被认为是相同的东西。这些词不能互换使用,因为它们有不同的含义。参数是在调用函数时传递给函数的实际值。参数是指在函数声明内部声明的变量。考虑以下示例:
fun printSum(a1: Int, a2: Int) { // 1.
print(a1 + a2)
}
add(3, 5) // 2.
1 - a1 和 a2 是参数
2-3 和 5 是参数
与 Java 一样,Kotlin 中的函数可以包含多个参数:
fun printSum(a: Int, b: Int) {
val sum = a + b
print(sum)
}
提供给函数的参数可以是参数声明中指定类型的子类型。正如我们所知,在 Kotlin 中,所有非空类型的超类型是Any,因此如果我们想接受所有类型,就需要使用它:
fun presentGently(v: Any) {
println("Hello. I would like to present you: $v")
}
presentGently("Duck")
// Hello. I would like to present you: Duck
presentGently(42)
// Hello. I would like to present you: 42
要允许参数为空,类型需要被指定为可空。注意Any?是所有可空和非可空类型的超类型,因此我们可以将任何类型的对象作为参数传递:
fun presentGently(v: Any?) {
println("Hello. I would like to present you: $v")
}
presentGently(null)
// Prints: Hello. I would like to present you: null
presentGently(1)
// Prints: Hello. I would like to present you: 1
presentGently("Str")
// Prints: Hello. I would like to present you: Str
返回函数
到目前为止,大多数函数都被定义为过程(不返回任何值的函数)。但实际上,Kotlin 中没有过程,所有函数都返回某个值。当没有指定时,默认返回值是Unit实例。我们可以为演示目的显式设置它:
fun printSum(a: Int, b: Int): Unit { // 1
val sum = a + b
print(sum)
}
- 与 Java 不同,我们在 Kotlin 中定义返回类型在函数名和参数之后。
Unit对象相当于 Java 的void,但它可以被视为任何其他对象。因此我们可以将它存储在变量中:
val p = printSum(1, 2)
println(p is Unit) // Prints: true
当然,Kotlin 编码约定声称,当函数返回Unit时,类型定义应该被省略。这样代码更易读,更容易理解:
fun printSum(a: Int, b: Int) {
val sum = a + b
print(sum)
}
好知识框架 Unit 是一个单例,这意味着只有一个实例。因此所有三个条件都为真:
println(p is Unit) // 输出:true
println(p == Unit) // 输出:true println(p === Unit) // 输出:true
Kotlin 中高度支持单例模式,并且将在第四章 类和对象中更加详细地介绍。
要从返回类型为Unit的函数返回输出,我们可以简单地使用一个没有任何值的返回语句:
fun printSum(a: Int, b: Int) { // 1
if(a < 0 || b < 0) {
return // 2
}
val sum = a + b
print(sum)
// 3
}
-
没有指定返回类型,因此返回类型隐式设置为 Unit。
-
我们可以只使用没有任何值的返回。
-
当函数返回 Unit 时,返回调用是可选的。我们不必使用它。
我们也可以使用返回Unit,但不应该使用,因为那样会误导并且不易读。
当我们指定返回类型时,除了Unit,我们总是需要显式返回值:
fun sumPositive(a: Int, b: Int): Int {
if(a > 0 && b > 0) {
return a + b
}
// Error, 1
}
- 函数不会编译,因为没有指定返回值,if 条件未满足。
问题可以通过添加第二个返回语句来解决:
fun sumPositive(a: Int, b: Int): Int {
if(a >= 0 && b >= 0) {
return a + b
}
return 0
}
Vararg 参数
有时,参数的数量事先是未知的。在这种情况下,我们可以将vararg修饰符添加到参数中。它允许函数接受任意数量的参数。以下是一个示例,其中函数打印多个整数的总和:
fun printSum(vararg numbers: Int) {
val sum = numbers.sum()
print(sum)
}
printSum(1,2,3,4,5) // Prints: 15
printSum() // Prints: 0
参数将作为一个包含所有提供的值的数组在方法内部可访问。数组的类型将对应于vararg参数类型。通常我们期望它是一个持有指定类型的通用数组(Array<T>),但正如我们所知,Kotlin 有一个优化的Int数组类型称为IntArray,因此将使用这种类型。例如,这是具有类型String的vararg参数的类型:
fun printAll(vararg texts: String) {
//Inferred type of texts is Array<String>
val allTexts = texts.joinToString(",")
println("Texts are $allTexts")
}
printAll("A", "B", "C") // Prints: Texts are A,B,C
请注意,我们仍然能够在vararg参数之前或之后指定更多的参数,只要清楚哪个参数指向哪个参数即可:
fun printAll(prefix: String, postfix: String, vararg texts: String)
{
val allTexts = texts.joinToString(", ")
println("$prefix$allTexts$postfix")
}
printAll("All texts: ", "!") // Prints: All texts: !
printAll("All texts: ","!" , "Hello", "World")
// Prints: All texts: Hello, World!
此外,提供给vararg参数的参数可以是指定类型的子类型:
fun printAll(vararg texts: Any) {
val allTexts = texts.joinToString(",") // 1
println(allTexts)
}
// Usage
printAll("A", 1, 'c') // Prints: A,1,c
joinToString函数可以在列表上调用。它将元素连接成一个字符串。在第一个参数中指定了分隔符。
vararg使用的一个限制是每个函数声明只允许一个vararg参数。
当我们调用vararg参数时,我们可以逐个传递参数值,但也可以传递一个值数组。这可以使用spread操作符(*前缀数组)来实现,就像以下示例中的那样:
val texts = arrayOf("B", "C", "D")
printAll(*texts) // Prints: Texts are: B,C,D
printAll("A", *texts, "E") // Prints: Texts are: A,B,C,D,E
单表达式函数
在典型的编程过程中,许多函数只包含一个表达式。以下是这种类型函数的一个例子:
fun square(x: Int): Int {
return x * x
}
或者另一个经常在 Android 项目中找到的例子是在Activity中使用的模式,定义仅从某个视图获取文本或从视图提供其他数据以允许 Presenter 获取它们的方法:
fun getEmail(): String {
return emailView.text.toString()
}
这两个函数都被定义为返回单个表达式的结果。在第一个例子中,它是x * x乘法的结果,在第二个例子中,它是表达式emailView.text.toString()的结果。这些类型的函数在整个 Android 项目中经常被使用。以下是一些常见用例:
-
提取一些小操作(就像前面的
square函数中) -
使用多态性提供特定于类的值
-
仅创建某个对象的函数
-
在架构层之间传递数据的函数(就像前面的例子中,
Activity正在从视图传递数据到 Presenter) -
基于递归的函数式编程风格函数
这种函数经常被使用,因此 Kotlin 为这种函数提供了一种表示法。当函数返回单个表达式时,可以省略大括号和函数体。我们可以直接使用等号字符指定表达式。以这种方式定义的函数称为单表达式函数。让我们更新我们的square函数,并将其定义为单表达式函数:
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00032.jpg
正如我们所看到的,单表达式函数具有表达式体而不是块体。这种表示法更短,但整个体必须只是一个单一表达式。
在单表达式函数中,声明返回类型是可选的,因为它可以从表达式的类型中推断出来。这就是为什么我们可以简化square函数,并以这种方式定义它:
fun square(x: Int) = x * x
在 Android 应用程序中有许多地方可以利用单表达式函数。让我们考虑一下提供布局 ID 并创建ViewHolder的RecyclerView适配器:
class AddressAdapter : ItemAdapter<AddressAdapter.ViewHolder>() {
override fun getLayoutId() = R.layout.choose_address_view
override fun onCreateViewHolder(itemView: View) = ViewHolder(itemView)
// Rest of methods
}
在下面的例子中,由于单表达式函数,我们实现了高可读性。单表达式函数在函数式世界中也非常受欢迎。稍后将在关于尾递归函数的部分中描述这个例子。单表达式函数表示法也与when结构很搭配。以下是它们的连接示例,用于根据键从对象中获取特定数据(来自大型 Kotlin 项目的用例):
fun valueFromBooking(key: String, booking: Booking?) = when(key) {
// 1
"patient.nin" -> booking?.patient?.nin
"patient.email" -> booking?.patient?.email
"patient.phone" -> booking?.patient?.phone
"comment" -> booking?.comment
else -> null
}
- 不需要类型,因为它是从 when 表达式中推断出来的。
另一个常见的 Android 示例是我们可以将 when 表达式与activity方法onOptionsItemSelected结合使用,处理顶部菜单点击:
override fun onOptionsItemSelected(item: MenuItem): Boolean = when
{
item.itemId == android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
另一个例子是单表达式函数语法有用的地方,是当我们在单个对象上链式多个操作时:
fun textFormatted(text: String, name: String) = text
.trim()
.capitalize()
.replace("{name}", name)
val formatted = textFormatted("hello, {name}", "Marcin")
println(formatted) // Hello, Marcin
正如我们所看到的,单表达式函数可以使我们的代码更简洁,提高可读性。单表达式函数在 Kotlin Android 项目中经常被使用,并且在函数式编程中非常受欢迎。
命令式与声明式编程 命令式编程:这种编程范式描述了执行操作所需的确切步骤序列。对大多数程序员来说,这是最直观的。
声明式编程:这种编程范式描述了期望的结果,但不一定是实现它的步骤(行为的实现)。这意味着编程是通过表达式或声明而不是语句来完成的。函数式和逻辑编程都被描述为声明式编程风格。声明式编程通常比命令式编程更简短和可读。
尾递归函数
递归函数是调用自身的函数。让我们看一个递归函数getState的例子:
fun getState(state: State, n: Int): State =
if (n <= 0) state // 1
else getState(nextState(state), n - 1)
它们是函数式编程风格的重要组成部分,但问题在于每个递归函数调用都需要在堆栈上保留前一个函数的返回地址。当应用程序递归太深时(堆栈上有太多函数),会抛出StackOverflowError。这种限制对于递归使用来说是一个非常严重的问题。
这个问题的一个经典解决方案是使用迭代而不是递归,但这种方法表达力较弱:
fun getState(state: State, n: Int): State {
var state = state
for (i in 1..n) {
state = state.nextState()
}
return state
}
这个问题的一个合适的解决方案是使用现代语言(如 Kotlin)支持的尾递归函数。尾递归函数是一种特殊类型的递归函数,其中函数调用自身作为其执行的最后一个操作(换句话说:递归发生在函数的最后一个操作中)。这使我们能够通过编译器优化递归调用,并以更有效的方式执行递归操作,而不必担心潜在的StackOverflowError。要使函数成为尾递归,我们需要使用tailrec修饰符标记它:
tailrec fun getState(state: State, n: Int): State =
if (n <= 0) state
else getState(state.nextState(), n - 1)
要查看它是如何工作的,让我们编译这段代码并反编译成 Java。然后可以找到以下内容(简化后的代码):
public static final State getState(@NotNull State state, int n)
{
while(true) {
if(n <= 0) {
return state;
}
state = state.nextState();
n = n - 1;
}
}
实现是基于迭代的,因此不可能发生堆栈溢出错误。为使tailrec修饰符起作用,需要满足一些要求:
-
函数必须只调用自身作为其执行的最后一个操作
-
它不能在
try/catch/finally块中使用 -
在撰写本文时,它只允许在编译为 JVM 的 Kotlin 中使用
不同的调用函数的方式
有时我们需要调用一个函数并只提供选定的参数。在 Java 中,我们可以创建同一个方法的多个重载,但这种解决方案有一些局限性。第一个问题是给定方法的可能排列数量增长得非常快(2^n),使得它们非常难以维护。第二个问题是重载必须彼此可区分,因此编译器可能需要知道调用哪个重载,所以当一个方法定义了几个相同类型的参数时,我们无法定义所有可能的重载。这就是为什么在 Java 中,我们经常需要向方法传递多个空值:
// Java
printValue("abc", null, null, "!");
多个空参数提供样板。这种情况大大降低了方法的可读性。在 Kotlin 中,没有这样的问题,因为 Kotlin 有一个称为默认参数和命名参数语法的特性。
默认参数值
默认参数大多来自于 C++,这是支持默认参数的最古老的语言之一。默认参数在方法调用时为参数提供一个值。每个函数参数都可以有一个默认值。它可以是与指定类型匹配的任何值,包括 null。这样我们可以简单地定义可以以多种方式调用的函数。这是一个带有默认值的函数的示例:
fun printValue(value: String, inBracket: Boolean = true,
prefix: String = "", suffix: String = "") {
print(prefix)
if (inBracket) {
print("(${value})")
} else {
print(value)
}
println(suffix)
}
我们可以像调用普通函数一样使用这个函数(没有默认参数值的函数),为每个参数提供值(所有参数):
printValue("str", true, "","") // Prints: (str)
由于默认参数值,我们可以只为没有默认值的参数提供参数来调用函数:
printValue("str") // Prints: (str)
我们也可以提供所有没有默认值的参数,只提供一些具有默认值的参数:
printValue("str", false) // Prints: str
命名参数语法
有时我们只想为最后一个参数传递一个值。假设我们只想为后缀定义一个值,而不是为前缀和inBracket(在后缀之前定义)定义一个值。通常情况下,我们必须为所有先前的参数提供值,包括默认参数值:
printValue("str", true, true, "!") // Prints: (str)
通过使用命名参数语法,我们可以使用参数名称传递特定的参数:
printValue("str", suffix = "!") // Prints: (str)!
这允许非常灵活的语法,我们可以在调用函数时只提供选择的参数(即,从末尾开始的第一个参数和第二个参数)。这经常用于指定这个参数是什么,因为这样的调用更易读:
printValue("str", inBracket = true) // Prints: (str)
printValue("str", prefix = "Value is ") // Prints: Value is str
printValue("str", prefix = "Value is ", suffix = "!! ")
// Prints: Value is str!!
我们可以使用命名参数语法设置任何我们想要的参数,以任何顺序,只要提供所有没有默认值的参数。参数的顺序是相关的:
printValue("str", inBracket= true, prefix = "Value is ")
// Prints: Value is (str)
printValue("str", prefix = "Value is ", inBracket= true)
// Prints: Value is (str)
参数的顺序不同,但前面两个调用是等价的。
我们还可以将命名参数语法与经典调用一起使用。唯一的限制是,如果我们开始使用命名语法,我们就不能为接下来的参数使用经典语法:
printValue ("str", true, "")
printValue ("str", true, prefix = "")
printValue ("str", inBracket = true, prefix = "")
printValue ("str", inBracket = true, "") // Error
printValue ("str", inBracket = true, prefix = "", "") // Error
这个特性允许我们以非常灵活的方式调用方法,而无需定义多个方法重载。
命名参数语法对 Kotlin 程序员施加了一些额外的责任。我们需要记住,当我们更改参数名称时,可能会在项目中引起错误,因为参数名称可能在其他类中使用。如果我们使用内置的重构工具重命名参数,Android Studio 会处理它,但这只在我们的项目内有效。Kotlin 库的创建者在使用命名参数语法时应该非常小心。更改参数名称将破坏 API。请注意,当调用 Java 函数时,无法使用命名参数语法,因为 Java 字节码并不总是保留函数参数的名称。
顶层函数
我们还可以在一个简单的Hello, World!程序中观察到的另一件事是,main函数不位于任何类中。在第二章,打下基础中,我们已经提到 Kotlin 可以在顶层定义各种实体。在顶层定义的函数称为顶层函数。以下是其中一个例子:
// Test.kt
package com.example
fun printTwo() {
print(2)
}
顶层函数可以在代码中的任何地方使用(假设它们是公共的,默认可见性修饰符)。我们可以像从本地上下文中的函数一样调用它们。要访问顶层函数,我们需要使用 import 语句将其显式导入文件中。在 Android Studio 中,函数在代码提示列表中可用,因此在选择(使用)函数时会自动添加导入。例如,让我们看一个在Test.kt中定义的顶层函数,并在Main.kt文件中使用它:
// Test.kt
package com.example
fun printTwo() {
print(2)
}
// Main.kt
import com.example.printTwo
fun main(args: Array<String>) {
printTwo()
}
顶层函数通常很有用,但明智地使用它们很重要。请记住,定义公共顶层函数将增加代码提示列表中可用函数的数量(提示列表是指在编写代码时 IDE 建议的方法列表)。这是因为 IDE 会在每个上下文中建议使用公共顶层函数(因为它们可以在任何地方使用)。如果顶层函数的名称没有清楚地说明这是一个顶层函数,那么它可能会被误认为是本地上下文中的方法并被意外使用。以下是一些顶层函数的好例子:
-
factorial -
maxOf和minOf -
listOf -
println
以下是一些可能不适合作为顶层函数的函数的示例:
-
sendUserData -
showPossiblePlayers
这个规则只适用于 Kotlin 面向对象编程项目。在面向函数的编程项目中,这些是有效的顶层名称,但我们假设几乎所有函数都是在顶层定义的,而不是作为方法。
通常我们定义我们想要在特定模块或特定类中使用的函数。为了限制函数的可见性(可以使用的位置),我们可以使用可见性修饰符。我们将在第四章,类和对象中讨论可见性修饰符。
底层的顶层函数
在 Android 项目中,Kotlin 被编译成 Java 字节码,可以在 Dalvik 虚拟机(Android 5.0 之前)或 Android Runtime(Android 5.0 及更新版本)上运行。两种虚拟机只能执行类内定义的代码。为了解决这个问题,Kotlin 编译器为顶层函数生成类。类名由文件名和Kt后缀构成。在这样的类内,所有函数和属性都是静态的。例如,假设我们在Printer.kt文件中定义一个函数:
// Printer.kt
fun printTwo() {
print(2)
}
Kotlin 代码被编译成 Java 字节码。生成的字节码将类似于从以下 Java 类生成的代码:
//Java
public final class PrinterKt { // 1
public static void printTwo() { // 2
System.out.print(2); // 3
}
}
-
PrinterKt是由文件名和*Kt*后缀构成的名称。 -
所有顶层函数和属性都被编译为静态方法和变量。
-
print是一个 Kotlin 函数,但由于它是一个内联函数,它的调用在编译时被其主体替换。它的主体只包括System.out.println调用。
内联函数将在第五章中描述,作为一等公民的函数。
在 Java 字节码级别上,Kotlin 类将包含更多数据(例如参数名称)。我们还可以通过在函数调用前加上类名来从 Java 文件中访问 Kotlin 顶层函数:
//Java file, call inside some method
PrinterKt.printTwo()
这样,从 Java 调用 Kotlin 顶层函数是完全支持的。可以看出,Kotlin 与 Java 真正可以互操作。为了使 Java 中对 Kotlin 顶层函数的使用更加舒适,我们可以添加一个注解来更改 JVM 生成的类的名称。在从 Java 类中使用顶层 Kotlin 属性和函数时,这非常方便。该注解如下所示:
@file:JvmName("Printer")
我们需要在文件顶部(在包名之前)添加JvmName注解。应用此注解后,生成的类名将更改为Printer。这使我们可以在 Java 中使用Printer作为类名调用printTwo函数:
//Java
Printer.printTwo()
有时我们定义顶层函数,并且希望在单独的文件中定义它们,但也希望它们在编译为 JVM 后在同一个类中。如果我们在文件顶部使用以下注解,这是可能的:
@file:JvmMultifileClass
例如,假设我们正在制作一个数学辅助库,我们希望从 Java 中使用。我们可以定义以下文件:
// Max.kt
@file:JvmName("Math")
@file:JvmMultifileClass
package com.example.math
fun max(n1: Int, n2: Int): Int = if(n1 > n2) n1 else n2
// Min.kt
@file:JvmName("Math")
@file:JvmMultifileClass
package com.example.math
fun min(n1: Int, n2: Int): Int = if(n1 < n2) n1 else n2
我们可以在 Java 类中这样使用它:
Math.min(1, 2)
Math.max(1, 2)
由此,我们可以保持文件简短和简单,同时使它们都易于从 Java 中使用。
JvmName注解用于更改生成的类名,在创建 Kotlin 库并且也要在 Java 类中使用时特别有用。在名称冲突时也很有用。当我们在同一个包中创建了X.kt文件和一个XKt类时,可能会出现这种情况。但这很少发生,因为有一个约定,即不应该有类带有Kt后缀。
局部函数
Kotlin 允许在许多上下文中定义函数。我们可以在顶层定义函数,作为成员(在class,interface等内部),以及在其他函数内部(局部函数)。考虑以下局部函数定义的示例:
fun printTwoThreeTimes() {
fun printThree() { // 1
print(3)
}
printThree() // 2
printThree() // 2
}
-
printThree是一个局部函数,因为它位于另一个函数内部。 -
局部函数无法从其声明的函数外部访问。
局部函数内可访问的元素不必从封闭函数作为参数传递,因为它们可以直接访问。例如:
fun loadUsers(ids: List<Int>) {
var downloaded: List<User> = emptyList()
fun printLog(comment: String) {
Log.i("loadUsers (with ids $ids): $comment\nDownloaded:
$downloaded") // 1
}
for(id in ids) {
printLog("Start downloading for id $id")
downloaded += loadUser(id)
printLog("Finished downloading for id $id")
}
}
- 局部函数可以访问封闭函数内定义的注释参数和局部变量(下载和 ID)。
如果我们想将printLog定义为顶层函数,那么我们必须将ids和downloaded作为参数传递:
fun loadUsers(ids: List<Int>) {
var downloaded: List<User> = emptyList()
for(id in ids) {
printLog("Start downloading for id $id", downloaded, ids)
downloaded += loadUser(id)
printLog("Finished downloading for
id $id", downloaded, ids))
}
}
fun printLog(state: String, downloaded: List<User>, ids: List<Int>)
{
Log.i("loadUsers (with ids $ids):
$state\nDownloaded: downloaded")
}
这种实现不仅更长,而且更难维护。对printLog的更改可能需要不同的参数,而参数的更改则需要更改此函数调用中的参数。此外,如果我们更改了printLog中使用的loadUsers参数类型,那么我们还需要更改printLog的参数。如果printLog是一个局部函数,就不会出现这样的问题。这解释了何时应该使用局部函数:当我们提取的功能仅被单个函数使用,并且该功能使用此函数的元素(变量、值、参数)。此外,局部函数允许修改局部变量。就像在这个例子中:
fun makeStudentList(): List<Student> {
var students: List<Student> = emptyList()
fun addStudent(name: String, state: Student.State =
Student.State.New) {
students += Student(name, state, courses = emptyList())
}
// ...
addStudent("Adam Smith")
addStudent("Donald Duck")
// ...
return students
}
这样,我们可以提取和重用在 Java 中无法提取的功能。记住局部函数是很好的,因为它们有时允许以其他方式难以实现的代码提取。
Nothing 返回类型
有时我们需要定义一个总是抛出异常(永远不会正常终止)的函数。两个真实的用例是:
-
简化错误抛出的函数。这在错误系统重要且需要提供有关错误发生的更多数据时特别有用。(例如,查看本节中介绍的
throwError函数)。 -
在单元测试中用于抛出错误的函数。当我们需要测试代码中的错误处理时,这是很有用的。
对于这种情况,有一个特殊的类叫做Nothing。Nothing类是空类型(无人居住类型),意味着它没有实例。具有Nothing返回类型的函数不会返回任何东西,也永远不会达到return语句。它只能抛出异常。这就是为什么当我们看到函数返回Nothing时,它被设计为抛出异常。这样我们就可以区分不返回值的函数(如 Java 的void,Kotlin 的Unit)和永远不会终止的函数(返回Nothing)。让我们看一个例子,这个函数可能被用来简化单元测试中的错误抛出:
fun fail(): Nothing = throw Error()
以及构造复杂错误消息的函数,使用在定义它的上下文中可用的元素(在类或函数中):
fun processElement(element: Element) {
fun throwError(message: String): Nothing
= throw ProcessingError("Error in element $element: $message")
// ...
if (element.kind != ElementKind.METHOD)
throwError("Not a method")
// ...
}
这种函数的特点是它可以像throw语句一样使用,作为不影响函数返回类型的替代品:
fun getFirstCharOrFail(str: String): Char
= if(str.isNotEmpty()) str[0] else fail()
val name: String = getName() ?: fail()
val enclosingElement = element.enclosingElement ?: throwError ("Lack of enclosing element")
这是怎么可能的?这是Nothing类的一个特殊特性,它表现得好像它是所有可能类型的子类型:可空和非可空的。这就是为什么Nothing被称为空类型,这意味着在运行时没有值可以具有这种类型,它也是每个其他类的子类型。
https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/andr-dev-kt/img/Image00033.jpg
无人居住类型的概念在 Java 世界中是新的,这就是为什么它可能令人困惑。这个想法实际上非常简单。Nothing实例从未存在,只有可能从指定它为返回类型的函数中返回的错误。而且没有必要将Nothing添加到某些东西中以影响其类型。
总结
在本章中,我们学习了如何定义和使用函数。我们了解了如何在顶层或其他函数内定义函数。还讨论了与函数相关的不同特性——可变参数、默认名称和命名参数语法。最后,我们看到了一些 Kotlin 特殊的返回类型:Unit,它相当于 Java 的void,以及Nothing,它是一种无法定义的类型,意味着什么也不能返回(只能抛出异常)。
在下一章中,我们将看到 Kotlin 中如何定义类。类在 Kotlin 语言中也得到了特别的支持,并且引入了许多改进,超过了 Java 的定义。



被折叠的 条评论
为什么被折叠?



